Skip to main content

sim_lib_serial_core/
aggregate.rs

1//! Aggregate policies and validation evidence.
2
3use crate::alphabet::{validate_alphabet, validate_stable_id};
4use crate::{AggregateRuleError, AlphabetId, SerialAlphabet};
5use std::collections::{BTreeMap, BTreeSet};
6use std::fmt::{Display, Formatter};
7
8/// One alphabet symbol paired with its declared aggregate count.
9pub type SymbolCount<S> = (S, usize);
10
11/// Stable identity of one class in a projected aggregate.
12#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
13pub struct ProjectionId(String);
14
15impl ProjectionId {
16    /// Validates and constructs a projected-class id.
17    pub fn try_new(value: impl Into<String>) -> Result<Self, AggregateRuleError> {
18        let value = value.into();
19        validate_stable_id(&value).map_err(|reason| AggregateRuleError::InvalidProjectionId {
20            value: value.clone(),
21            reason,
22        })?;
23        Ok(Self(value))
24    }
25
26    /// Returns the stable text identity.
27    pub fn as_str(&self) -> &str {
28        &self.0
29    }
30}
31
32impl Display for ProjectionId {
33    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
34        Display::fmt(&self.0, formatter)
35    }
36}
37
38/// Symbol-based declaration of one projected aggregate class.
39#[derive(Clone, Debug, PartialEq, Eq)]
40pub struct ProjectedClassSpec<S> {
41    /// Stable class identity.
42    pub id: ProjectionId,
43    /// Source-alphabet symbols that project to this class.
44    pub symbols: Vec<S>,
45    /// Required number of class occurrences in the series.
46    pub multiplicity: usize,
47}
48
49impl<S> ProjectedClassSpec<S> {
50    /// Constructs a class specification. Full membership is validated by the rule constructor.
51    pub fn new(id: ProjectionId, symbols: Vec<S>, multiplicity: usize) -> Self {
52        Self {
53            id,
54            symbols,
55            multiplicity,
56        }
57    }
58}
59
60/// Public category of an [`AggregateRule`].
61#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
62pub enum AggregateRuleKind {
63    /// Every alphabet symbol occurs exactly once.
64    ExhaustiveExactlyOnce,
65    /// Symbols may be omitted but no symbol may repeat.
66    NoRepeat,
67    /// Every symbol has an explicitly declared positive multiplicity.
68    DeclaredMultiplicity,
69    /// Every non-omitted symbol occurs exactly once.
70    DeclaredOmissions,
71    /// Occurrence requirements apply to declared projection classes.
72    ProjectedAggregate,
73    /// Any finite order of alphabet members is accepted, including repeats.
74    FreeOrder,
75}
76
77/// Aggregate rule data compiled from symbols into private canonical positions.
78/// Callers never provide raw ordinals.
79#[derive(Clone, Debug, PartialEq, Eq)]
80pub enum AggregateRule {
81    /// Every alphabet symbol occurs exactly once.
82    ExhaustiveExactlyOnce,
83    /// Symbols may be omitted but no symbol may repeat.
84    NoRepeat,
85    /// Explicit per-symbol multiplicity data.
86    DeclaredMultiplicity(DeclaredCounts),
87    /// Explicit set of omitted symbols; every other symbol occurs once.
88    DeclaredOmissions(DeclaredCounts),
89    /// Explicit projection classes and their required multiplicities.
90    ProjectedAggregate(ProjectedRule),
91    /// Any finite order of alphabet members, including repeats.
92    FreeOrder,
93}
94
95/// Alphabet-bound expected counts retained by a declared aggregate rule.
96#[derive(Clone, Debug, PartialEq, Eq)]
97pub struct DeclaredCounts {
98    alphabet_id: AlphabetId,
99    expected: Vec<usize>,
100}
101
102/// One compiled projected class.
103#[derive(Clone, Debug, PartialEq, Eq)]
104pub struct ProjectedClassRule {
105    id: ProjectionId,
106    members: Vec<usize>,
107    multiplicity: usize,
108}
109
110/// Alphabet-bound projected aggregate data.
111#[derive(Clone, Debug, PartialEq, Eq)]
112pub struct ProjectedRule {
113    alphabet_id: AlphabetId,
114    cardinality: usize,
115    classes: Vec<ProjectedClassRule>,
116    class_by_position: Vec<usize>,
117}
118
119impl AggregateRule {
120    /// Constructs an exhaustive exactly-once rule.
121    pub const fn exhaustive_exactly_once() -> Self {
122        Self::ExhaustiveExactlyOnce
123    }
124
125    /// Constructs a no-repeat rule.
126    pub const fn no_repeat() -> Self {
127        Self::NoRepeat
128    }
129
130    /// Constructs a free-order rule.
131    pub const fn free_order() -> Self {
132        Self::FreeOrder
133    }
134
135    /// Compiles an explicit positive multiplicity for every alphabet symbol.
136    pub fn declared_multiplicity<A, I>(
137        alphabet: &A,
138        declarations: I,
139    ) -> Result<Self, AggregateRuleError>
140    where
141        A: SerialAlphabet,
142        I: IntoIterator<Item = (A::Symbol, usize)>,
143    {
144        let positions = validate_alphabet(alphabet)?;
145        let mut expected = vec![None; alphabet.symbols().len()];
146        for (symbol, multiplicity) in declarations {
147            let Some(&position) = positions.get(&symbol) else {
148                return Err(AggregateRuleError::ForeignSymbol {
149                    alphabet_id: alphabet.id().clone(),
150                });
151            };
152            if expected[position].is_some() {
153                return Err(AggregateRuleError::DuplicateDeclaration { position });
154            }
155            if multiplicity == 0 {
156                return Err(AggregateRuleError::ZeroMultiplicity { position });
157            }
158            expected[position] = Some(multiplicity);
159        }
160        let expected = expected
161            .into_iter()
162            .enumerate()
163            .map(|(position, count)| {
164                count.ok_or(AggregateRuleError::MissingDeclaration { position })
165            })
166            .collect::<Result<Vec<_>, _>>()?;
167        checked_total(&expected)?;
168        Ok(Self::DeclaredMultiplicity(DeclaredCounts {
169            alphabet_id: alphabet.id().clone(),
170            expected,
171        }))
172    }
173
174    /// Compiles a set of symbols to omit; every remaining symbol is required once.
175    pub fn declared_omissions<A, I>(alphabet: &A, omissions: I) -> Result<Self, AggregateRuleError>
176    where
177        A: SerialAlphabet,
178        I: IntoIterator<Item = A::Symbol>,
179    {
180        let positions = validate_alphabet(alphabet)?;
181        let mut expected = vec![1; alphabet.symbols().len()];
182        let mut omitted = BTreeSet::new();
183        for symbol in omissions {
184            let Some(&position) = positions.get(&symbol) else {
185                return Err(AggregateRuleError::ForeignSymbol {
186                    alphabet_id: alphabet.id().clone(),
187                });
188            };
189            if !omitted.insert(position) {
190                return Err(AggregateRuleError::DuplicateDeclaration { position });
191            }
192            expected[position] = 0;
193        }
194        if omitted.is_empty() {
195            return Err(AggregateRuleError::NoOmissions);
196        }
197        if omitted.len() == alphabet.symbols().len() {
198            return Err(AggregateRuleError::OmitsEverything(alphabet.id().clone()));
199        }
200        Ok(Self::DeclaredOmissions(DeclaredCounts {
201            alphabet_id: alphabet.id().clone(),
202            expected,
203        }))
204    }
205
206    /// Compiles a complete, disjoint projection of alphabet symbols into classes.
207    pub fn projected_aggregate<A, I>(alphabet: &A, classes: I) -> Result<Self, AggregateRuleError>
208    where
209        A: SerialAlphabet,
210        I: IntoIterator<Item = ProjectedClassSpec<A::Symbol>>,
211    {
212        let positions = validate_alphabet(alphabet)?;
213        let mut class_ids = BTreeSet::new();
214        let mut class_by_position = vec![None; alphabet.symbols().len()];
215        let mut compiled = Vec::new();
216        let mut total = 0usize;
217        for spec in classes {
218            if !class_ids.insert(spec.id.clone()) {
219                return Err(AggregateRuleError::DuplicateProjectionId(spec.id));
220            }
221            if spec.symbols.is_empty() {
222                return Err(AggregateRuleError::EmptyProjectionClass(spec.id));
223            }
224            let class_index = compiled.len();
225            let mut members = Vec::with_capacity(spec.symbols.len());
226            for symbol in spec.symbols {
227                let Some(&position) = positions.get(&symbol) else {
228                    return Err(AggregateRuleError::ForeignSymbol {
229                        alphabet_id: alphabet.id().clone(),
230                    });
231                };
232                if class_by_position[position].replace(class_index).is_some() {
233                    return Err(AggregateRuleError::DuplicateProjectionMember { position });
234                }
235                members.push(position);
236            }
237            total = total
238                .checked_add(spec.multiplicity)
239                .ok_or(AggregateRuleError::MultiplicityOverflow)?;
240            compiled.push(ProjectedClassRule {
241                id: spec.id,
242                members,
243                multiplicity: spec.multiplicity,
244            });
245        }
246        for (position, class) in class_by_position.iter().enumerate() {
247            if class.is_none() {
248                return Err(AggregateRuleError::MissingProjectionMember { position });
249            }
250        }
251        if total == 0 {
252            return Err(AggregateRuleError::OmitsEverything(alphabet.id().clone()));
253        }
254        Ok(Self::ProjectedAggregate(ProjectedRule {
255            alphabet_id: alphabet.id().clone(),
256            cardinality: alphabet.symbols().len(),
257            classes: compiled,
258            class_by_position: class_by_position.into_iter().flatten().collect(),
259        }))
260    }
261
262    /// Returns the public category of this rule.
263    pub const fn kind(&self) -> AggregateRuleKind {
264        match self {
265            Self::ExhaustiveExactlyOnce => AggregateRuleKind::ExhaustiveExactlyOnce,
266            Self::NoRepeat => AggregateRuleKind::NoRepeat,
267            Self::DeclaredMultiplicity(_) => AggregateRuleKind::DeclaredMultiplicity,
268            Self::DeclaredOmissions(_) => AggregateRuleKind::DeclaredOmissions,
269            Self::ProjectedAggregate(_) => AggregateRuleKind::ProjectedAggregate,
270            Self::FreeOrder => AggregateRuleKind::FreeOrder,
271        }
272    }
273
274    /// Returns symbol/count declarations for a declared multiplicity or omission rule.
275    pub fn declared_counts<A>(
276        &self,
277        alphabet: &A,
278    ) -> Result<Option<Vec<SymbolCount<A::Symbol>>>, AggregateRuleError>
279    where
280        A: SerialAlphabet,
281    {
282        let counts = match self {
283            Self::DeclaredMultiplicity(counts) | Self::DeclaredOmissions(counts) => counts,
284            _ => return Ok(None),
285        };
286        counts.validate_for(alphabet)?;
287        Ok(Some(
288            alphabet
289                .symbols()
290                .iter()
291                .cloned()
292                .zip(counts.expected.iter().copied())
293                .collect(),
294        ))
295    }
296
297    /// Returns symbolic projected-class declarations for a projected rule.
298    pub fn projected_classes<A>(
299        &self,
300        alphabet: &A,
301    ) -> Result<Option<Vec<ProjectedClassSpec<A::Symbol>>>, AggregateRuleError>
302    where
303        A: SerialAlphabet,
304    {
305        let Self::ProjectedAggregate(rule) = self else {
306            return Ok(None);
307        };
308        rule.validate_for(alphabet)?;
309        Ok(Some(
310            rule.classes
311                .iter()
312                .map(|class| {
313                    ProjectedClassSpec::new(
314                        class.id.clone(),
315                        class
316                            .members
317                            .iter()
318                            .map(|&position| alphabet.symbols()[position].clone())
319                            .collect(),
320                        class.multiplicity,
321                    )
322                })
323                .collect(),
324        ))
325    }
326
327    pub(crate) fn declared(&self) -> Option<&DeclaredCounts> {
328        match self {
329            Self::DeclaredMultiplicity(counts) | Self::DeclaredOmissions(counts) => Some(counts),
330            _ => None,
331        }
332    }
333
334    pub(crate) fn projected(&self) -> Option<&ProjectedRule> {
335        match self {
336            Self::ProjectedAggregate(rule) => Some(rule),
337            _ => None,
338        }
339    }
340}
341
342impl DeclaredCounts {
343    pub(crate) fn validate_for<A: SerialAlphabet>(
344        &self,
345        alphabet: &A,
346    ) -> Result<(), AggregateRuleError> {
347        validate_binding(&self.alphabet_id, self.expected.len(), alphabet)
348    }
349
350    pub(crate) fn expected(&self) -> &[usize] {
351        &self.expected
352    }
353}
354
355impl ProjectedRule {
356    pub(crate) fn validate_for<A: SerialAlphabet>(
357        &self,
358        alphabet: &A,
359    ) -> Result<(), AggregateRuleError> {
360        validate_binding(&self.alphabet_id, self.cardinality, alphabet)
361    }
362
363    pub(crate) fn required_len(&self) -> Result<usize, AggregateRuleError> {
364        checked_total(
365            &self
366                .classes
367                .iter()
368                .map(|class| class.multiplicity)
369                .collect::<Vec<_>>(),
370        )
371    }
372
373    pub(crate) fn class_by_position(&self) -> &[usize] {
374        &self.class_by_position
375    }
376
377    pub(crate) fn classes(&self) -> &[ProjectedClassRule] {
378        &self.classes
379    }
380}
381
382impl ProjectedClassRule {
383    pub(crate) fn id(&self) -> &ProjectionId {
384        &self.id
385    }
386
387    pub(crate) fn multiplicity(&self) -> usize {
388        self.multiplicity
389    }
390}
391
392/// Observed and expected counts for one projected class.
393#[derive(Clone, Debug, PartialEq, Eq)]
394pub struct ProjectedClassEvidence {
395    /// Stable projected-class id.
396    pub id: ProjectionId,
397    /// Required number of occurrences.
398    pub expected: usize,
399    /// Observed number of occurrences.
400    pub observed: usize,
401}
402
403/// Construction evidence retained by a valid [`crate::Series`].
404#[derive(Clone, Debug, PartialEq, Eq)]
405pub struct AggregateLedger<S>
406where
407    S: Clone + Eq + Ord + std::fmt::Debug,
408{
409    pub(crate) alphabet_id: AlphabetId,
410    pub(crate) rule: AggregateRuleKind,
411    pub(crate) series_len: usize,
412    pub(crate) observed: BTreeMap<S, usize>,
413    pub(crate) expected: Option<BTreeMap<S, usize>>,
414    pub(crate) omitted: Vec<S>,
415    pub(crate) repeated: Vec<S>,
416    pub(crate) projected: Vec<ProjectedClassEvidence>,
417}
418
419impl<S> AggregateLedger<S>
420where
421    S: Clone + Eq + Ord + std::fmt::Debug,
422{
423    /// Stable alphabet identity validated by this ledger.
424    pub fn alphabet_id(&self) -> &AlphabetId {
425        &self.alphabet_id
426    }
427
428    /// Aggregate rule category applied during validation.
429    pub fn rule(&self) -> AggregateRuleKind {
430        self.rule
431    }
432
433    /// Number of ordered positions validated.
434    pub fn series_len(&self) -> usize {
435        self.series_len
436    }
437
438    /// Observed count of `symbol`, or `None` when it is outside the alphabet.
439    pub fn observed_count(&self, symbol: &S) -> Option<usize> {
440        self.observed.get(symbol).copied()
441    }
442
443    /// Declared expected count of `symbol` when this rule has per-symbol expectations.
444    pub fn expected_count(&self, symbol: &S) -> Option<usize> {
445        self.expected
446            .as_ref()
447            .and_then(|counts| counts.get(symbol).copied())
448    }
449
450    /// Alphabet symbols absent from the supplied series.
451    pub fn omitted_symbols(&self) -> &[S] {
452        &self.omitted
453    }
454
455    /// Alphabet symbols occurring more than once.
456    pub fn repeated_symbols(&self) -> &[S] {
457        &self.repeated
458    }
459
460    /// Projected-class count evidence, empty for non-projected rules.
461    pub fn projected_classes(&self) -> &[ProjectedClassEvidence] {
462        &self.projected
463    }
464
465    /// Returns true when every alphabet symbol occurred exactly once.
466    pub fn is_exhaustive_exactly_once(&self) -> bool {
467        self.omitted.is_empty()
468            && self.repeated.is_empty()
469            && self.observed.values().all(|count| *count == 1)
470    }
471}
472
473fn validate_binding<A: SerialAlphabet>(
474    rule_id: &AlphabetId,
475    cardinality: usize,
476    alphabet: &A,
477) -> Result<(), AggregateRuleError> {
478    validate_alphabet(alphabet)?;
479    if rule_id != alphabet.id() {
480        return Err(AggregateRuleError::AlphabetMismatch {
481            rule_id: rule_id.clone(),
482            series_id: alphabet.id().clone(),
483        });
484    }
485    if cardinality != alphabet.symbols().len() {
486        return Err(AggregateRuleError::CardinalityMismatch {
487            expected: cardinality,
488            found: alphabet.symbols().len(),
489        });
490    }
491    Ok(())
492}
493
494fn checked_total(counts: &[usize]) -> Result<usize, AggregateRuleError> {
495    counts.iter().try_fold(0usize, |total, &count| {
496        total
497            .checked_add(count)
498            .ok_or(AggregateRuleError::MultiplicityOverflow)
499    })
500}