Skip to main content

ringkernel_procint/models/
conformance.rs

1//! Conformance checking types for process intelligence.
2//!
3//! Defines metrics and results for validating traces against models.
4
5use super::{ActivityId, TraceId};
6use rkyv::{Archive, Deserialize, Serialize};
7
8/// Conformance checking status.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Archive, Serialize, Deserialize)]
10#[repr(u8)]
11pub enum ConformanceStatus {
12    /// Trace fully conforms to model.
13    #[default]
14    Conformant = 0,
15    /// Wrong activity sequence.
16    WrongSequence = 1,
17    /// Missing expected activity.
18    MissingActivity = 2,
19    /// Extra unexpected activity.
20    ExtraActivity = 3,
21    /// General deviation.
22    Deviation = 4,
23    /// Timing violation.
24    TimingViolation = 5,
25}
26
27impl ConformanceStatus {
28    /// Get display name.
29    pub fn name(&self) -> &'static str {
30        match self {
31            ConformanceStatus::Conformant => "Conformant",
32            ConformanceStatus::WrongSequence => "Wrong Sequence",
33            ConformanceStatus::MissingActivity => "Missing Activity",
34            ConformanceStatus::ExtraActivity => "Extra Activity",
35            ConformanceStatus::Deviation => "Deviation",
36            ConformanceStatus::TimingViolation => "Timing Violation",
37        }
38    }
39}
40
41/// Compliance level classification.
42#[derive(
43    Debug,
44    Clone,
45    Copy,
46    PartialEq,
47    Eq,
48    PartialOrd,
49    Ord,
50    Hash,
51    Default,
52    Archive,
53    Serialize,
54    Deserialize,
55)]
56#[repr(u8)]
57pub enum ComplianceLevel {
58    /// 95%+ fitness.
59    #[default]
60    FullyCompliant = 0,
61    /// 80-95% fitness.
62    MostlyCompliant = 1,
63    /// 50-80% fitness.
64    PartiallyCompliant = 2,
65    /// <50% fitness.
66    NonCompliant = 3,
67}
68
69impl ComplianceLevel {
70    /// Get color for UI (RGB).
71    pub fn color(&self) -> [u8; 3] {
72        match self {
73            ComplianceLevel::FullyCompliant => [40, 167, 69], // Green
74            ComplianceLevel::MostlyCompliant => [255, 193, 7], // Yellow
75            ComplianceLevel::PartiallyCompliant => [255, 152, 0], // Orange
76            ComplianceLevel::NonCompliant => [220, 53, 69],   // Red
77        }
78    }
79
80    /// Get from fitness score.
81    pub fn from_fitness(fitness: f32) -> Self {
82        if fitness >= 0.95 {
83            ComplianceLevel::FullyCompliant
84        } else if fitness >= 0.80 {
85            ComplianceLevel::MostlyCompliant
86        } else if fitness >= 0.50 {
87            ComplianceLevel::PartiallyCompliant
88        } else {
89            ComplianceLevel::NonCompliant
90        }
91    }
92
93    /// Get display name.
94    pub fn name(&self) -> &'static str {
95        match self {
96            ComplianceLevel::FullyCompliant => "Fully Compliant",
97            ComplianceLevel::MostlyCompliant => "Mostly Compliant",
98            ComplianceLevel::PartiallyCompliant => "Partially Compliant",
99            ComplianceLevel::NonCompliant => "Non-Compliant",
100        }
101    }
102}
103
104/// Alignment move type for trace-model alignment.
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Archive, Serialize, Deserialize)]
106#[repr(u8)]
107pub enum AlignmentType {
108    /// Both log and model advance (matching).
109    #[default]
110    Synchronous = 0,
111    /// Extra activity in log (not in model).
112    LogMove = 1,
113    /// Missing activity in log (expected by model).
114    ModelMove = 2,
115}
116
117impl AlignmentType {
118    /// Get the cost of this alignment move.
119    pub fn cost(&self) -> u32 {
120        match self {
121            AlignmentType::Synchronous => 0,
122            AlignmentType::LogMove => 1,
123            AlignmentType::ModelMove => 1,
124        }
125    }
126}
127
128/// Single alignment move.
129#[derive(Debug, Clone, Copy, Default, Archive, Serialize, Deserialize)]
130#[repr(C)]
131pub struct AlignmentMove {
132    /// Type of move.
133    pub move_type: u8,
134    /// Padding.
135    pub _padding: [u8; 3],
136    /// Activity in log (0xFFFFFFFF if model-only).
137    pub log_activity: u32,
138    /// Activity in model (0xFFFFFFFF if log-only).
139    pub model_activity: u32,
140    /// Cost of this move.
141    pub cost: u32,
142}
143
144impl AlignmentMove {
145    /// Create a synchronous move.
146    pub fn synchronous(activity: ActivityId) -> Self {
147        Self {
148            move_type: AlignmentType::Synchronous as u8,
149            log_activity: activity,
150            model_activity: activity,
151            cost: 0,
152            _padding: [0; 3],
153        }
154    }
155
156    /// Create a log move (extra activity in log).
157    pub fn log_move(activity: ActivityId) -> Self {
158        Self {
159            move_type: AlignmentType::LogMove as u8,
160            log_activity: activity,
161            model_activity: u32::MAX,
162            cost: 1,
163            _padding: [0; 3],
164        }
165    }
166
167    /// Create a model move (missing activity in log).
168    pub fn model_move(activity: ActivityId) -> Self {
169        Self {
170            move_type: AlignmentType::ModelMove as u8,
171            log_activity: u32::MAX,
172            model_activity: activity,
173            cost: 1,
174            _padding: [0; 3],
175        }
176    }
177
178    /// Get the alignment type.
179    pub fn get_type(&self) -> AlignmentType {
180        match self.move_type {
181            0 => AlignmentType::Synchronous,
182            1 => AlignmentType::LogMove,
183            2 => AlignmentType::ModelMove,
184            _ => AlignmentType::Synchronous,
185        }
186    }
187}
188
189/// GPU-compatible conformance result (64 bytes).
190#[derive(Debug, Clone, Copy, Default, Archive, Serialize, Deserialize)]
191#[repr(C, align(64))]
192pub struct ConformanceResult {
193    /// Trace identifier.
194    pub trace_id: u64,
195    /// Model identifier.
196    pub model_id: u32,
197    /// Conformance status.
198    pub status: u8,
199    /// Compliance level.
200    pub compliance_level: u8,
201    /// Padding.
202    pub _padding1: [u8; 2],
203    /// Fitness score (0.0 - 1.0).
204    pub fitness: f32,
205    /// Precision score (0.0 - 1.0).
206    pub precision: f32,
207    /// Generalization score (0.0 - 1.0).
208    pub generalization: f32,
209    /// Simplicity score (0.0 - 1.0).
210    pub simplicity: f32,
211    /// Number of missing activities.
212    pub missing_count: u16,
213    /// Number of extra activities.
214    pub extra_count: u16,
215    /// Total alignment cost.
216    pub alignment_cost: u32,
217    /// Number of alignment moves.
218    pub alignment_length: u32,
219    /// Reserved.
220    pub _reserved: [u8; 16],
221}
222
223// Verify size
224const _: () = assert!(std::mem::size_of::<ConformanceResult>() == 64);
225
226impl ConformanceResult {
227    /// Create a new conformant result.
228    pub fn conformant(trace_id: TraceId, model_id: u32) -> Self {
229        Self {
230            trace_id,
231            model_id,
232            status: ConformanceStatus::Conformant as u8,
233            compliance_level: ComplianceLevel::FullyCompliant as u8,
234            fitness: 1.0,
235            precision: 1.0,
236            generalization: 0.8,
237            simplicity: 1.0,
238            ..Default::default()
239        }
240    }
241
242    /// Create a non-conformant result with deviations.
243    pub fn with_deviations(
244        trace_id: TraceId,
245        model_id: u32,
246        missing: u16,
247        extra: u16,
248        alignment_cost: u32,
249    ) -> Self {
250        let total = missing + extra;
251        let fitness = if total > 0 {
252            1.0 - (alignment_cost as f32 / (alignment_cost + 10) as f32)
253        } else {
254            1.0
255        };
256
257        let status = if missing > 0 {
258            ConformanceStatus::MissingActivity
259        } else if extra > 0 {
260            ConformanceStatus::ExtraActivity
261        } else {
262            ConformanceStatus::Deviation
263        };
264
265        Self {
266            trace_id,
267            model_id,
268            status: status as u8,
269            compliance_level: ComplianceLevel::from_fitness(fitness) as u8,
270            fitness,
271            precision: 1.0 - (extra as f32 / 10.0).min(1.0),
272            generalization: 0.8,
273            simplicity: 1.0,
274            missing_count: missing,
275            extra_count: extra,
276            alignment_cost,
277            ..Default::default()
278        }
279    }
280
281    /// Get conformance status.
282    pub fn get_status(&self) -> ConformanceStatus {
283        match self.status {
284            0 => ConformanceStatus::Conformant,
285            1 => ConformanceStatus::WrongSequence,
286            2 => ConformanceStatus::MissingActivity,
287            3 => ConformanceStatus::ExtraActivity,
288            4 => ConformanceStatus::Deviation,
289            5 => ConformanceStatus::TimingViolation,
290            _ => ConformanceStatus::Conformant,
291        }
292    }
293
294    /// Get compliance level.
295    pub fn get_compliance_level(&self) -> ComplianceLevel {
296        match self.compliance_level {
297            0 => ComplianceLevel::FullyCompliant,
298            1 => ComplianceLevel::MostlyCompliant,
299            2 => ComplianceLevel::PartiallyCompliant,
300            3 => ComplianceLevel::NonCompliant,
301            _ => ComplianceLevel::FullyCompliant,
302        }
303    }
304
305    /// Check if conformant.
306    pub fn is_conformant(&self) -> bool {
307        self.status == ConformanceStatus::Conformant as u8
308    }
309
310    /// Calculate F-score from fitness and precision.
311    pub fn f_score(&self) -> f32 {
312        if self.fitness + self.precision > 0.0 {
313            2.0 * self.fitness * self.precision / (self.fitness + self.precision)
314        } else {
315            0.0
316        }
317    }
318}
319
320/// Process model for conformance checking.
321#[derive(Debug, Clone)]
322pub struct ProcessModel {
323    /// Model identifier.
324    pub id: u32,
325    /// Model name.
326    pub name: String,
327    /// Model type.
328    pub model_type: ProcessModelType,
329    /// Start activities.
330    pub start_activities: Vec<ActivityId>,
331    /// End activities.
332    pub end_activities: Vec<ActivityId>,
333    /// Valid transitions (source, target).
334    pub transitions: Vec<(ActivityId, ActivityId)>,
335}
336
337impl ProcessModel {
338    /// Create a new process model.
339    pub fn new(id: u32, name: impl Into<String>, model_type: ProcessModelType) -> Self {
340        Self {
341            id,
342            name: name.into(),
343            model_type,
344            start_activities: Vec::new(),
345            end_activities: Vec::new(),
346            transitions: Vec::new(),
347        }
348    }
349
350    /// Add a transition.
351    pub fn add_transition(&mut self, source: ActivityId, target: ActivityId) {
352        self.transitions.push((source, target));
353    }
354
355    /// Check if a transition is valid.
356    pub fn has_transition(&self, source: ActivityId, target: ActivityId) -> bool {
357        self.transitions.contains(&(source, target))
358    }
359}
360
361/// Process model type.
362#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Archive, Serialize, Deserialize)]
363#[repr(u8)]
364pub enum ProcessModelType {
365    /// Directly-Follows Graph.
366    #[default]
367    DFG = 0,
368    /// Petri Net.
369    PetriNet = 1,
370    /// BPMN model.
371    BPMN = 2,
372    /// Process Tree.
373    ProcessTree = 3,
374    /// DECLARE constraints.
375    Declare = 4,
376}
377
378#[cfg(test)]
379mod tests {
380    use super::*;
381
382    #[test]
383    fn test_conformance_result_size() {
384        assert_eq!(std::mem::size_of::<ConformanceResult>(), 64);
385    }
386
387    #[test]
388    fn test_compliance_level_from_fitness() {
389        assert_eq!(
390            ComplianceLevel::from_fitness(0.98),
391            ComplianceLevel::FullyCompliant
392        );
393        assert_eq!(
394            ComplianceLevel::from_fitness(0.85),
395            ComplianceLevel::MostlyCompliant
396        );
397        assert_eq!(
398            ComplianceLevel::from_fitness(0.60),
399            ComplianceLevel::PartiallyCompliant
400        );
401        assert_eq!(
402            ComplianceLevel::from_fitness(0.30),
403            ComplianceLevel::NonCompliant
404        );
405    }
406
407    #[test]
408    fn test_alignment_moves() {
409        let sync = AlignmentMove::synchronous(1);
410        assert_eq!(sync.cost, 0);
411        assert_eq!(sync.get_type(), AlignmentType::Synchronous);
412
413        let log = AlignmentMove::log_move(2);
414        assert_eq!(log.cost, 1);
415        assert_eq!(log.get_type(), AlignmentType::LogMove);
416
417        let model = AlignmentMove::model_move(3);
418        assert_eq!(model.cost, 1);
419        assert_eq!(model.get_type(), AlignmentType::ModelMove);
420    }
421}