refactor!: replace emptiness-as-sentinel with Option for results and weights
Third of the five remaining items on #23. `add_events_with_prior` and `TimeSlice::add_events` took `Vec<Vec<f64>>` and `Vec<Vec<Vec<f64>>>` where an empty vec meant "not supplied" — so an empty outer vec and a genuinely empty event list were the same value, and every reader had to know which. Both are now `Option`, and the "not supplied" branches read as `None` arms rather than `is_empty()` checks. Two things fell out of the change that a sentinel would have hidden: The tie pre-check iterated `results` directly. Under `Option` it needs `.iter().flatten()`, which makes explicit that a `None` results list has no ties to reject — previously an empty vec silently skipped the same loop and looked identical to "checked, found nothing". `MismatchedShape.got` could no longer be `results.len()`, because at the point of the error there may be no vec to take a length from. It is now computed as `map_or(0, Vec::len)` before the error is built. MSRV note: my first draft used let-chains for the two validations. Those need Rust 1.88 and this crate pins 1.85 — it compiled locally on 1.98 and would have failed only in the MSRV CI job. Rewritten with `is_some_and`, and verified by installing 1.85.0 and building against it rather than by assuming the removal was complete. Breaking: `TimeSlice::add_events` is public. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T5SYDExxL4vZgvunrcNSMc
This commit is contained in:
+1
-1
@@ -36,7 +36,7 @@ fn criterion_benchmark(criterion: &mut Criterion) {
|
|||||||
let kinds = vec![EventKind::Ranked; composition.len()];
|
let kinds = vec![EventKind::Ranked; composition.len()];
|
||||||
|
|
||||||
let mut time_slice = TimeSlice::new(1, P_DRAW, ConvergenceOptions::default());
|
let mut time_slice = TimeSlice::new(1, P_DRAW, ConvergenceOptions::default());
|
||||||
time_slice.add_events(composition, results, weights, kinds, &agents);
|
time_slice.add_events(composition, Some(results), Some(weights), kinds, &agents);
|
||||||
|
|
||||||
criterion.bench_function("Batch::iteration", |b| {
|
criterion.bench_function("Batch::iteration", |b| {
|
||||||
b.iter(|| time_slice.iteration(0, &agents))
|
b.iter(|| time_slice.iteration(0, &agents))
|
||||||
|
|||||||
+34
-22
@@ -651,17 +651,22 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
pub(crate) fn add_events_with_prior(
|
pub(crate) fn add_events_with_prior(
|
||||||
&mut self,
|
&mut self,
|
||||||
composition: Vec<Vec<Vec<Index>>>,
|
composition: Vec<Vec<Vec<Index>>>,
|
||||||
results: Vec<Vec<f64>>,
|
results: Option<Vec<Vec<f64>>>,
|
||||||
times: Vec<T>,
|
times: Vec<T>,
|
||||||
weights: Vec<Vec<Vec<f64>>>,
|
weights: Option<Vec<Vec<Vec<f64>>>>,
|
||||||
kinds: Vec<EventKind>,
|
kinds: Vec<EventKind>,
|
||||||
mut priors: HashMap<Index, Rating<T, D>>,
|
mut priors: HashMap<Index, Rating<T, D>>,
|
||||||
) -> Result<(), InferenceError> {
|
) -> Result<(), InferenceError> {
|
||||||
if !results.is_empty() && results.len() != composition.len() {
|
if results
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|r| r.len() != composition.len())
|
||||||
|
{
|
||||||
|
let got = results.as_ref().map_or(0, Vec::len);
|
||||||
|
|
||||||
return Err(InferenceError::MismatchedShape {
|
return Err(InferenceError::MismatchedShape {
|
||||||
kind: "results",
|
kind: "results",
|
||||||
expected: composition.len(),
|
expected: composition.len(),
|
||||||
got: results.len(),
|
got,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if times.len() != composition.len() {
|
if times.len() != composition.len() {
|
||||||
@@ -671,11 +676,16 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
got: times.len(),
|
got: times.len(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if !weights.is_empty() && weights.len() != composition.len() {
|
if weights
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|w| w.len() != composition.len())
|
||||||
|
{
|
||||||
|
let got = weights.as_ref().map_or(0, Vec::len);
|
||||||
|
|
||||||
return Err(InferenceError::MismatchedShape {
|
return Err(InferenceError::MismatchedShape {
|
||||||
kind: "weights",
|
kind: "weights",
|
||||||
expected: composition.len(),
|
expected: composition.len(),
|
||||||
got: weights.len(),
|
got,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if kinds.len() != composition.len() {
|
if kinds.len() != composition.len() {
|
||||||
@@ -690,7 +700,7 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
// including `record_draw`, which builds its results directly rather
|
// including `record_draw`, which builds its results directly rather
|
||||||
// than going through `Outcome`.
|
// than going through `Outcome`.
|
||||||
if self.p_draw == 0.0 {
|
if self.p_draw == 0.0 {
|
||||||
for (event_results, kind) in results.iter().zip(kinds.iter()) {
|
for (event_results, kind) in results.iter().flatten().zip(kinds.iter()) {
|
||||||
if !matches!(kind, EventKind::Ranked) {
|
if !matches!(kind, EventKind::Ranked) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -772,17 +782,13 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
.map(|e| composition[o[e]].clone())
|
.map(|e| composition[o[e]].clone())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let results = if results.is_empty() {
|
let results = results
|
||||||
Vec::new()
|
.as_ref()
|
||||||
} else {
|
.map(|results| (i..j).map(|e| results[o[e]].clone()).collect::<Vec<_>>());
|
||||||
(i..j).map(|e| results[o[e]].clone()).collect::<Vec<_>>()
|
|
||||||
};
|
|
||||||
|
|
||||||
let weights = if weights.is_empty() {
|
let weights = weights
|
||||||
Vec::new()
|
.as_ref()
|
||||||
} else {
|
.map(|weights| (i..j).map(|e| weights[o[e]].clone()).collect::<Vec<_>>());
|
||||||
(i..j).map(|e| weights[o[e]].clone()).collect::<Vec<_>>()
|
|
||||||
};
|
|
||||||
|
|
||||||
let kinds_chunk: Vec<EventKind> = (i..j).map(|e| kinds[o[e]]).collect();
|
let kinds_chunk: Vec<EventKind> = (i..j).map(|e| kinds[o[e]]).collect();
|
||||||
|
|
||||||
@@ -861,9 +867,9 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
let l = self.intern(loser);
|
let l = self.intern(loser);
|
||||||
self.add_events_with_prior(
|
self.add_events_with_prior(
|
||||||
vec![vec![vec![w], vec![l]]],
|
vec![vec![vec![w], vec![l]]],
|
||||||
vec![vec![1.0, 0.0]],
|
Some(vec![vec![1.0, 0.0]]),
|
||||||
vec![time],
|
vec![time],
|
||||||
vec![],
|
None,
|
||||||
vec![EventKind::Ranked],
|
vec![EventKind::Ranked],
|
||||||
HashMap::new(),
|
HashMap::new(),
|
||||||
)
|
)
|
||||||
@@ -885,9 +891,9 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
let b_idx = self.intern(b);
|
let b_idx = self.intern(b);
|
||||||
self.add_events_with_prior(
|
self.add_events_with_prior(
|
||||||
vec![vec![vec![a_idx], vec![b_idx]]],
|
vec![vec![vec![a_idx], vec![b_idx]]],
|
||||||
vec![vec![0.0, 0.0]],
|
Some(vec![vec![0.0, 0.0]]),
|
||||||
vec![time],
|
vec![time],
|
||||||
vec![],
|
None,
|
||||||
vec![EventKind::Ranked],
|
vec![EventKind::Ranked],
|
||||||
HashMap::new(),
|
HashMap::new(),
|
||||||
)
|
)
|
||||||
@@ -981,7 +987,13 @@ impl<T: Time, D: Drift<T>, O: Observer<T>, K: Eq + Hash + Clone> History<T, D, O
|
|||||||
times.push(ev.time);
|
times.push(ev.time);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.add_events_with_prior(composition, results, times, weights, kinds, priors)
|
let weights = if weights.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(weights)
|
||||||
|
};
|
||||||
|
|
||||||
|
self.add_events_with_prior(composition, Some(results), times, weights, kinds, priors)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+20
-21
@@ -277,8 +277,8 @@ impl<T: Time> TimeSlice<T> {
|
|||||||
pub fn add_events<D: Drift<T>>(
|
pub fn add_events<D: Drift<T>>(
|
||||||
&mut self,
|
&mut self,
|
||||||
composition: Vec<Vec<Vec<Index>>>,
|
composition: Vec<Vec<Vec<Index>>>,
|
||||||
results: Vec<Vec<f64>>,
|
results: Option<Vec<Vec<f64>>>,
|
||||||
weights: Vec<Vec<Vec<f64>>>,
|
weights: Option<Vec<Vec<Vec<f64>>>>,
|
||||||
kinds: Vec<EventKind>,
|
kinds: Vec<EventKind>,
|
||||||
agents: &CompetitorStore<T, D>,
|
agents: &CompetitorStore<T, D>,
|
||||||
) {
|
) {
|
||||||
@@ -328,22 +328,21 @@ impl<T: Time> TimeSlice<T> {
|
|||||||
|
|
||||||
Team {
|
Team {
|
||||||
items,
|
items,
|
||||||
output: if results.is_empty() {
|
output: match &results {
|
||||||
(event.len() - (t + 1)) as f64
|
Some(results) => results[e][t],
|
||||||
} else {
|
// No explicit result: rank by position, first team best.
|
||||||
results[e][t]
|
None => (event.len() - (t + 1)) as f64,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let weights = if weights.is_empty() {
|
let weights = match &weights {
|
||||||
teams
|
Some(weights) => weights[e].clone(),
|
||||||
|
None => teams
|
||||||
.iter()
|
.iter()
|
||||||
.map(|team| vec![1.0; team.items.len()])
|
.map(|team| vec![1.0; team.items.len()])
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>(),
|
||||||
} else {
|
|
||||||
weights[e].clone()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Event {
|
Event {
|
||||||
@@ -828,8 +827,8 @@ mod tests {
|
|||||||
vec![vec![c], vec![d]],
|
vec![vec![c], vec![d]],
|
||||||
vec![vec![e], vec![f]],
|
vec![vec![e], vec![f]],
|
||||||
],
|
],
|
||||||
vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 0.0]],
|
Some(vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 0.0]]),
|
||||||
vec![],
|
None,
|
||||||
vec![EventKind::Ranked; 3],
|
vec![EventKind::Ranked; 3],
|
||||||
&agents,
|
&agents,
|
||||||
);
|
);
|
||||||
@@ -905,8 +904,8 @@ mod tests {
|
|||||||
vec![vec![a], vec![c]],
|
vec![vec![a], vec![c]],
|
||||||
vec![vec![b], vec![c]],
|
vec![vec![b], vec![c]],
|
||||||
],
|
],
|
||||||
vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 0.0]],
|
Some(vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 0.0]]),
|
||||||
vec![],
|
None,
|
||||||
vec![EventKind::Ranked; 3],
|
vec![EventKind::Ranked; 3],
|
||||||
&agents,
|
&agents,
|
||||||
);
|
);
|
||||||
@@ -985,8 +984,8 @@ mod tests {
|
|||||||
vec![vec![a], vec![c]],
|
vec![vec![a], vec![c]],
|
||||||
vec![vec![b], vec![c]],
|
vec![vec![b], vec![c]],
|
||||||
],
|
],
|
||||||
vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 0.0]],
|
Some(vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 0.0]]),
|
||||||
vec![],
|
None,
|
||||||
vec![EventKind::Ranked; 3],
|
vec![EventKind::Ranked; 3],
|
||||||
&agents,
|
&agents,
|
||||||
);
|
);
|
||||||
@@ -1017,8 +1016,8 @@ mod tests {
|
|||||||
vec![vec![a], vec![c]],
|
vec![vec![a], vec![c]],
|
||||||
vec![vec![b], vec![c]],
|
vec![vec![b], vec![c]],
|
||||||
],
|
],
|
||||||
vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 0.0]],
|
Some(vec![vec![1.0, 0.0], vec![0.0, 1.0], vec![1.0, 0.0]]),
|
||||||
vec![],
|
None,
|
||||||
vec![EventKind::Ranked; 3],
|
vec![EventKind::Ranked; 3],
|
||||||
&agents,
|
&agents,
|
||||||
);
|
);
|
||||||
@@ -1088,8 +1087,8 @@ mod tests {
|
|||||||
vec![vec![c], vec![d]],
|
vec![vec![c], vec![d]],
|
||||||
vec![vec![a], vec![c]],
|
vec![vec![a], vec![c]],
|
||||||
],
|
],
|
||||||
vec![vec![1.0, 0.0], vec![1.0, 0.0], vec![1.0, 0.0]],
|
Some(vec![vec![1.0, 0.0], vec![1.0, 0.0], vec![1.0, 0.0]]),
|
||||||
vec![],
|
None,
|
||||||
vec![EventKind::Ranked; 3],
|
vec![EventKind::Ranked; 3],
|
||||||
&agents,
|
&agents,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user