Skip to main content

ocel_mine/
variants.rs

1//! Per-type trace variants.
2//!
3//! A trace is one object's E2O-linked events ordered by `(time, event index)`;
4//! a variant is the activity sequence shared by objects of one type. Computed
5//! without flattening: only objects of the requested type contribute, so
6//! divergence/convergence across types cannot distort counts.
7//!
8//! Hot-path design: activity names are interned to `u16` ids (cheap sequence
9//! hashing/comparison), events are sorted once globally instead of per trace,
10//! and no strings are cloned until the final report is assembled.
11
12use std::collections::HashMap;
13
14use ocel::Ocel;
15use serde::Serialize;
16
17use crate::trace;
18
19/// One activity sequence and how many objects of the type follow it.
20#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
21pub struct Variant {
22    /// Activity (event type) names in trace order.
23    pub activities: Vec<String>,
24    /// Objects whose trace is exactly this sequence.
25    pub count: usize,
26    /// One object id exhibiting this variant.
27    pub example: String,
28}
29
30/// Trace variants of every object of one type.
31#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
32#[serde(rename_all = "camelCase")]
33pub struct VariantsReport {
34    pub object_type: String,
35    /// Objects of the type in the log.
36    pub objects: usize,
37    /// Objects with at least one linked event (only these form variants).
38    pub with_events: usize,
39    /// Variants sorted by descending count, ties by activity sequence.
40    pub variants: Vec<Variant>,
41}
42
43/// Compute trace variants for `object_type`.
44///
45/// An event linked to the same object through several qualifiers counts once.
46/// Same-timestamp events are ordered by their index in `log.events`, making the
47/// result deterministic.
48#[must_use]
49pub fn variants(log: &Ocel, object_type: &str) -> VariantsReport {
50    let traces = trace::build(log, object_type);
51    let sequences: Vec<Vec<u16>> = traces
52        .steps
53        .iter()
54        .map(|steps| steps.iter().map(|&(activity, _)| activity).collect())
55        .collect();
56
57    // Group identical sequences; keys borrow the sequence buffers.
58    let mut groups: HashMap<&[u16], (usize, usize)> = HashMap::new();
59    let mut with_events = 0usize;
60    for (slot, sequence) in sequences.iter().enumerate() {
61        if sequence.is_empty() {
62            continue;
63        }
64        with_events += 1;
65        let entry = groups.entry(sequence.as_slice()).or_insert((0, slot));
66        entry.0 += 1;
67    }
68
69    let mut variants: Vec<Variant> = groups
70        .into_iter()
71        .map(|(sequence, (count, example_slot))| Variant {
72            activities: sequence
73                .iter()
74                .map(|&id| traces.activity_names[id as usize].to_owned())
75                .collect(),
76            count,
77            example: traces.object_ids[example_slot].to_owned(),
78        })
79        .collect();
80    variants.sort_unstable_by(|a, b| {
81        b.count
82            .cmp(&a.count)
83            .then_with(|| a.activities.cmp(&b.activities))
84    });
85
86    VariantsReport {
87        object_type: object_type.to_owned(),
88        objects: traces.object_ids.len(),
89        with_events,
90        variants,
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use chrono::{TimeZone, Utc};
98    use ocel::{Event, EventType, Object, ObjectType, Relationship};
99
100    fn rel(object_id: &str) -> Relationship {
101        Relationship {
102            object_id: object_id.into(),
103            qualifier: "q".into(),
104        }
105    }
106
107    fn event(id: &str, event_type: &str, minute: u32, objects: &[&str]) -> Event {
108        Event {
109            id: id.into(),
110            event_type: event_type.into(),
111            time: Utc.with_ymd_and_hms(2026, 1, 1, 9, minute, 0).unwrap(),
112            attributes: vec![],
113            relationships: objects.iter().map(|o| rel(o)).collect(),
114        }
115    }
116
117    fn log(events: Vec<Event>, objects: &[&str]) -> Ocel {
118        let mut builder = Ocel::builder();
119        for name in ["created", "closed", "changed"] {
120            builder.add_event_type(EventType {
121                name: name.into(),
122                attributes: vec![],
123            });
124        }
125        builder.add_object_type(ObjectType {
126            name: "task".into(),
127            attributes: vec![],
128        });
129        for id in objects {
130            builder.add_object(Object {
131                id: (*id).into(),
132                object_type: "task".into(),
133                attributes: vec![],
134                relationships: vec![],
135            });
136        }
137        for e in events {
138            builder.add_event(e);
139        }
140        builder.build().expect("valid log")
141    }
142
143    #[test]
144    fn groups_identical_sequences() {
145        let log = log(
146            vec![
147                event("e1", "created", 0, &["a"]),
148                event("e2", "closed", 1, &["a"]),
149                event("e3", "created", 2, &["b"]),
150                event("e4", "closed", 3, &["b"]),
151                event("e5", "created", 4, &["c"]),
152            ],
153            &["a", "b", "c"],
154        );
155        let report = variants(&log, "task");
156        assert_eq!(report.objects, 3);
157        assert_eq!(report.with_events, 3);
158        assert_eq!(report.variants.len(), 2);
159        assert_eq!(report.variants[0].activities, ["created", "closed"]);
160        assert_eq!(report.variants[0].count, 2);
161        assert_eq!(report.variants[1].activities, ["created"]);
162        assert_eq!(report.variants[1].count, 1);
163        assert_eq!(report.variants[1].example, "c");
164    }
165
166    #[test]
167    fn duplicate_links_from_one_event_count_once() {
168        let mut e = event("e1", "created", 0, &["a", "a"]);
169        e.relationships[1].qualifier = "other".into();
170        let log = log(vec![e], &["a"]);
171        let report = variants(&log, "task");
172        assert_eq!(report.variants[0].activities, ["created"]);
173    }
174
175    #[test]
176    fn same_timestamp_orders_by_event_index() {
177        let log = log(
178            vec![
179                event("e2", "closed", 5, &["a"]),
180                event("e1", "created", 5, &["a"]),
181            ],
182            &["a"],
183        );
184        let report = variants(&log, "task");
185        // e2 comes first in log.events, so index order puts "closed" first.
186        assert_eq!(report.variants[0].activities, ["closed", "created"]);
187    }
188
189    #[test]
190    fn objects_without_events_form_no_variant() {
191        let log = log(vec![event("e1", "created", 0, &["a"])], &["a", "b"]);
192        let report = variants(&log, "task");
193        assert_eq!(report.objects, 2);
194        assert_eq!(report.with_events, 1);
195        assert_eq!(report.variants.len(), 1);
196    }
197
198    #[test]
199    fn unknown_type_is_empty() {
200        let log = log(vec![], &[]);
201        let report = variants(&log, "nope");
202        assert_eq!(report.objects, 0);
203        assert!(report.variants.is_empty());
204    }
205}