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:
+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(
|
||||
&mut self,
|
||||
composition: Vec<Vec<Vec<Index>>>,
|
||||
results: Vec<Vec<f64>>,
|
||||
results: Option<Vec<Vec<f64>>>,
|
||||
times: Vec<T>,
|
||||
weights: Vec<Vec<Vec<f64>>>,
|
||||
weights: Option<Vec<Vec<Vec<f64>>>>,
|
||||
kinds: Vec<EventKind>,
|
||||
mut priors: HashMap<Index, Rating<T, D>>,
|
||||
) -> 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 {
|
||||
kind: "results",
|
||||
expected: composition.len(),
|
||||
got: results.len(),
|
||||
got,
|
||||
});
|
||||
}
|
||||
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(),
|
||||
});
|
||||
}
|
||||
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 {
|
||||
kind: "weights",
|
||||
expected: composition.len(),
|
||||
got: weights.len(),
|
||||
got,
|
||||
});
|
||||
}
|
||||
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
|
||||
// than going through `Outcome`.
|
||||
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) {
|
||||
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())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let results = if results.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
(i..j).map(|e| results[o[e]].clone()).collect::<Vec<_>>()
|
||||
};
|
||||
let results = results
|
||||
.as_ref()
|
||||
.map(|results| (i..j).map(|e| results[o[e]].clone()).collect::<Vec<_>>());
|
||||
|
||||
let weights = if weights.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
(i..j).map(|e| weights[o[e]].clone()).collect::<Vec<_>>()
|
||||
};
|
||||
let weights = weights
|
||||
.as_ref()
|
||||
.map(|weights| (i..j).map(|e| weights[o[e]].clone()).collect::<Vec<_>>());
|
||||
|
||||
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);
|
||||
self.add_events_with_prior(
|
||||
vec![vec![vec![w], vec![l]]],
|
||||
vec![vec![1.0, 0.0]],
|
||||
Some(vec![vec![1.0, 0.0]]),
|
||||
vec![time],
|
||||
vec![],
|
||||
None,
|
||||
vec![EventKind::Ranked],
|
||||
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);
|
||||
self.add_events_with_prior(
|
||||
vec![vec![vec![a_idx], vec![b_idx]]],
|
||||
vec![vec![0.0, 0.0]],
|
||||
Some(vec![vec![0.0, 0.0]]),
|
||||
vec![time],
|
||||
vec![],
|
||||
None,
|
||||
vec![EventKind::Ranked],
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user