1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
5pub struct DFGNode {
6 pub activity: String,
7 pub frequency: usize,
8}
9
10impl DFGNode {
11 pub fn new(activity: String, frequency: usize) -> Self {
12 DFGNode {
13 activity,
14 frequency,
15 }
16 }
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
21pub struct DFGEdge {
22 pub source: String,
23 pub target: String,
24 pub frequency: usize,
25}
26
27impl DFGEdge {
28 pub fn new(source: String, target: String, frequency: usize) -> Self {
29 DFGEdge {
30 source,
31 target,
32 frequency,
33 }
34 }
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
39pub struct DFG {
40 pub nodes: Vec<DFGNode>,
41 pub edges: Vec<DFGEdge>,
42 pub start_activities: Vec<String>,
43 pub end_activities: Vec<String>,
44}
45
46impl DFG {
47 pub fn new() -> Self {
48 DFG {
49 nodes: Vec::new(),
50 edges: Vec::new(),
51 start_activities: Vec::new(),
52 end_activities: Vec::new(),
53 }
54 }
55
56 pub fn len(&self) -> usize {
57 self.nodes.len()
58 }
59
60 pub fn is_empty(&self) -> bool {
61 self.nodes.is_empty()
62 }
63}
64
65impl Default for DFG {
66 fn default() -> Self {
67 Self::new()
68 }
69}
70
71
72use crate::dense_kernel::{fnv1a_64, DenseIndex, NodeKind, PackedKeyTable};
73use std::hash::{Hash, Hasher};
74
75#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
76pub struct Place {
77 pub id: String,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
81pub struct Transition {
82 pub id: String,
83 pub label: String,
84 pub is_invisible: Option<bool>,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
88pub struct Arc {
89 pub from: String,
90 pub to: String,
91 pub weight: Option<usize>,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize, Default)]
95pub struct PetriNet {
96 pub places: Vec<Place>,
97 pub transitions: Vec<Transition>,
98 pub arcs: Vec<Arc>,
99 pub initial_marking: PackedKeyTable<String, usize>,
100 pub final_markings: Vec<PackedKeyTable<String, usize>>,
101
102 #[serde(skip)]
104 pub cached_incidence: Option<FlatIncidenceMatrix>,
105
106 #[serde(skip)]
108 pub cached_index: Option<DenseIndex>,
109}
110
111impl PartialEq for PetriNet {
112 fn eq(&self, other: &Self) -> bool {
113 self.places == other.places
114 && self.transitions == other.transitions
115 && self.arcs == other.arcs
116 && self.initial_marking == other.initial_marking
117 && self.final_markings == other.final_markings
118 }
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
122pub struct FlatIncidenceMatrix {
123 pub data: Vec<i32>,
125 pub places_count: usize,
126 pub transitions_count: usize,
127}
128
129impl FlatIncidenceMatrix {
130 pub fn get(&self, place_idx: usize, transition_idx: usize) -> i32 {
131 self.data[place_idx * self.transitions_count + transition_idx]
132 }
133}
134
135impl PetriNet {
136 fn build_node_index(&self) -> PackedKeyTable<&str, usize> {
139 let mut map = PackedKeyTable::with_capacity(self.places.len() + self.transitions.len());
140 for (i, p) in self.places.iter().enumerate() {
141 map.insert(fnv1a_64(p.id.as_bytes()), p.id.as_str(), i);
142 }
143 let offset = self.places.len();
144 for (i, t) in self.transitions.iter().enumerate() {
145 map.insert(fnv1a_64(t.id.as_bytes()), t.id.as_str(), offset + i);
146 }
147 map
148 }
149
150 pub fn is_structural_workflow_net(&self) -> bool {
153 if self.places.is_empty() || self.transitions.is_empty() {
154 return false;
155 }
156
157 let place_count = self.places.len();
158 let total_nodes = place_count + self.transitions.len();
159 let num_words = total_nodes.div_ceil(64);
160
161 let mut in_degrees = vec![0u64; num_words];
162 let mut out_degrees = vec![0u64; num_words];
163
164 if let Some(ref index) = self.cached_index {
165 for arc in &self.arcs {
166 if let (Some(from_idx), Some(to_idx)) =
167 (index.dense_id(&arc.from), index.dense_id(&arc.to))
168 {
169 let from_idx = from_idx as usize;
170 let to_idx = to_idx as usize;
171 out_degrees[from_idx / 64] |= 1u64 << (from_idx % 64);
172 in_degrees[to_idx / 64] |= 1u64 << (to_idx % 64);
173 }
174 }
175 } else {
176 let id_to_index = self.build_node_index();
177 for arc in &self.arcs {
178 if let (Some(&from_idx), Some(&to_idx)) = (
179 id_to_index.get(fnv1a_64(arc.from.as_bytes())),
180 id_to_index.get(fnv1a_64(arc.to.as_bytes())),
181 ) {
182 out_degrees[from_idx / 64] |= 1u64 << (from_idx % 64);
183 in_degrees[to_idx / 64] |= 1u64 << (to_idx % 64);
184 }
185 }
186 }
187
188 let mut source_places_count = 0;
189 let mut sink_places_count = 0;
190
191 if let Some(ref index) = self.cached_index {
192 for p in &self.places {
194 if let Some(i) = index.dense_id(&p.id).map(|d| d as usize) {
195 let has_in = (in_degrees[i / 64] & (1u64 << (i % 64))) != 0;
196 let has_out = (out_degrees[i / 64] & (1u64 << (i % 64))) != 0;
197 if !has_in {
198 source_places_count += 1;
199 }
200 if !has_out {
201 sink_places_count += 1;
202 }
203 }
204 }
205 if source_places_count != 1 || sink_places_count != 1 {
206 return false;
207 }
208 for t in &self.transitions {
209 if let Some(i) = index.dense_id(&t.id).map(|d| d as usize) {
210 let has_in = (in_degrees[i / 64] & (1u64 << (i % 64))) != 0;
211 let has_out = (out_degrees[i / 64] & (1u64 << (i % 64))) != 0;
212 if !has_in || !has_out {
213 return false;
214 }
215 }
216 }
217 } else {
218 for i in 0..place_count {
220 let has_in = (in_degrees[i / 64] & (1u64 << (i % 64))) != 0;
221 let has_out = (out_degrees[i / 64] & (1u64 << (i % 64))) != 0;
222 if !has_in {
223 source_places_count += 1;
224 }
225 if !has_out {
226 sink_places_count += 1;
227 }
228 }
229 if source_places_count != 1 || sink_places_count != 1 {
230 return false;
231 }
232 for i in place_count..total_nodes {
233 let has_in = (in_degrees[i / 64] & (1u64 << (i % 64))) != 0;
234 let has_out = (out_degrees[i / 64] & (1u64 << (i % 64))) != 0;
235 if !has_in || !has_out {
236 return false;
237 }
238 }
239 }
240
241 true
242 }
243
244 pub fn compile_incidence(&mut self) {
246 let mut symbols = Vec::with_capacity(self.places.len() + self.transitions.len());
248 for p in &self.places {
249 symbols.push((p.id.clone(), NodeKind::Place));
250 }
251 for t in &self.transitions {
252 symbols.push((t.id.clone(), NodeKind::Transition));
253 }
254
255 if let Ok(index) = DenseIndex::compile(symbols) {
256 self.cached_index = Some(index);
257 }
258
259 self.cached_incidence = Some(self.compute_incidence());
260 }
261
262 fn compute_incidence(&self) -> FlatIncidenceMatrix {
264 let places_count = self.places.len();
265 let transitions_count = self.transitions.len();
266 let mut data = vec![0; places_count * transitions_count];
267
268 let place_row: std::collections::HashMap<&str, usize> = self
270 .places
271 .iter()
272 .enumerate()
273 .map(|(i, p)| (p.id.as_str(), i))
274 .collect();
275 let trans_col: std::collections::HashMap<&str, usize> = self
276 .transitions
277 .iter()
278 .enumerate()
279 .map(|(i, t)| (t.id.as_str(), i))
280 .collect();
281
282 for arc in &self.arcs {
283 let weight = arc.weight.unwrap_or(1) as i32;
284 if let (Some(&p_row), Some(&t_col)) = (
285 place_row.get(arc.from.as_str()),
286 trans_col.get(arc.to.as_str()),
287 ) {
288 data[p_row * transitions_count + t_col] -= weight;
289 } else if let (Some(&t_col), Some(&p_row)) = (
290 trans_col.get(arc.from.as_str()),
291 place_row.get(arc.to.as_str()),
292 ) {
293 data[p_row * transitions_count + t_col] += weight;
294 }
295 }
296
297 FlatIncidenceMatrix {
298 data,
299 places_count,
300 transitions_count,
301 }
302 }
303
304 pub fn incidence_matrix(&self) -> FlatIncidenceMatrix {
307 if let Some(ref cached) = self.cached_incidence {
308 return cached.clone();
309 }
310 self.compute_incidence()
311 }
312
313 pub fn verifies_state_equation_calculus(&self) -> bool {
316 if !self.is_structural_workflow_net() {
317 return false;
318 }
319 let w = self.incidence_matrix();
320 let p_count = self.places.len();
321 let t_count = self.transitions.len();
322
323 for t_col in 0..t_count {
324 let mut consumes = false;
325 let mut produces = false;
326 for p_row in 0..p_count {
327 let val = w.get(p_row, t_col);
328 if val < 0 {
329 consumes = true;
330 }
331 if val > 0 {
332 produces = true;
333 }
334 }
335 if !consumes || !produces {
336 return false;
337 }
338 }
339 true
340 }
341
342 pub fn structural_unsoundness_score(&self) -> f32 {
344 if self.places.is_empty() || self.transitions.is_empty() {
345 return 10.0;
346 }
347
348 let place_count = self.places.len();
349 let total_nodes = place_count + self.transitions.len();
350 let num_words = total_nodes.div_ceil(64);
351
352 let mut in_degrees = vec![0u64; num_words];
353 let mut out_degrees = vec![0u64; num_words];
354
355 if let Some(ref index) = self.cached_index {
356 for arc in &self.arcs {
357 if let (Some(from_idx), Some(to_idx)) =
358 (index.dense_id(&arc.from), index.dense_id(&arc.to))
359 {
360 let from_idx = from_idx as usize;
361 let to_idx = to_idx as usize;
362 out_degrees[from_idx / 64] |= 1u64 << (from_idx % 64);
363 in_degrees[to_idx / 64] |= 1u64 << (to_idx % 64);
364 }
365 }
366 } else {
367 let id_to_index = self.build_node_index();
368 for arc in &self.arcs {
369 if let (Some(&from_idx), Some(&to_idx)) = (
370 id_to_index.get(fnv1a_64(arc.from.as_bytes())),
371 id_to_index.get(fnv1a_64(arc.to.as_bytes())),
372 ) {
373 out_degrees[from_idx / 64] |= 1u64 << (from_idx % 64);
374 in_degrees[to_idx / 64] |= 1u64 << (to_idx % 64);
375 }
376 }
377 }
378
379 let mut score = 0.0;
380 let mut source_places_count = 0;
381 let mut sink_places_count = 0;
382 for i in 0..place_count {
383 let has_in = (in_degrees[i / 64] & (1u64 << (i % 64))) != 0;
384 let has_out = (out_degrees[i / 64] & (1u64 << (i % 64))) != 0;
385 if !has_in {
386 source_places_count += 1;
387 }
388 if !has_out {
389 sink_places_count += 1;
390 }
391 }
392
393 score += (source_places_count as f32 - 1.0).abs();
394 score += (sink_places_count as f32 - 1.0).abs();
395
396 for i in place_count..total_nodes {
397 let has_in = (in_degrees[i / 64] & (1u64 << (i % 64))) != 0;
398 let has_out = (out_degrees[i / 64] & (1u64 << (i % 64))) != 0;
399 if !has_in {
400 score += 1.0;
401 }
402 if !has_out {
403 score += 1.0;
404 }
405 }
406
407 for i in 0..place_count {
408 let has_in = (in_degrees[i / 64] & (1u64 << (i % 64))) != 0;
409 let has_out = (out_degrees[i / 64] & (1u64 << (i % 64))) != 0;
410 if !has_in && !has_out {
411 score += 2.0;
412 }
413 }
414
415 score
416 }
417
418 pub fn mdl_score(&self) -> f64 {
421 self.mdl_score_with_ontology(None)
422 }
423
424 pub fn mdl_score_with_ontology(&self, ontology_size: Option<usize>) -> f64 {
425 let t = self.transitions.len() as f64;
426 let a = self.arcs.len() as f64;
427 if t == 0.0 {
428 return 0.0;
429 }
430 let vocabulary_size = ontology_size.map(|s| s as f64).unwrap_or(t);
431 t + (a * vocabulary_size.log2())
432 }
433
434 pub fn explain(&self) -> String {
435 "This model was selected because:\n\
436 1. It achieved full replay fitness.\n\
437 2. It had the lowest MDL score among admissible candidates.\n\
438 3. It satisfied workflow-net soundness.\n\
439 4. It reproduced under manifest verification."
440 .to_string()
441 }
442
443 pub fn canonical_hash(&self) -> u64 {
445 let mut hasher = rustc_hash::FxHasher::default();
446 let mut p_ids: Vec<_> = self.places.iter().map(|p| &p.id).collect();
447 p_ids.sort();
448 for id in p_ids {
449 id.hash(&mut hasher);
450 }
451
452 let mut t_ids: Vec<_> = self.transitions.iter().map(|t| &t.id).collect();
453 t_ids.sort();
454 for id in t_ids {
455 id.hash(&mut hasher);
456 }
457
458 let mut arcs = self.arcs.clone();
459 arcs.sort_by(|a, b| (&a.from, &a.to).cmp(&(&b.from, &b.to)));
460 for arc in arcs {
461 arc.from.hash(&mut hasher);
462 arc.to.hash(&mut hasher);
463 arc.weight.unwrap_or(1).hash(&mut hasher);
464 }
465
466 hasher.finish()
467 }
468}
469
470#[cfg(test)]
471mod tests_declare {
472 use super::*;
473
474 #[test]
475 fn test_incidence_matrix_flat_parity() {
476 let mut net = PetriNet::default();
477 net.places.push(Place {
478 id: "p1".to_string(),
479 });
480 net.places.push(Place {
481 id: "p2".to_string(),
482 });
483 net.transitions.push(Transition {
484 id: "t1".to_string(),
485 label: "A".to_string(),
486 is_invisible: None,
487 });
488 net.arcs.push(Arc {
489 from: "p1".to_string(),
490 to: "t1".to_string(),
491 weight: Some(1),
492 });
493 net.arcs.push(Arc {
494 from: "t1".to_string(),
495 to: "p2".to_string(),
496 weight: Some(2),
497 });
498
499 let w = net.incidence_matrix();
500 assert_eq!(w.places_count, 2);
501 assert_eq!(w.transitions_count, 1);
502 assert_eq!(w.get(0, 0), -1); assert_eq!(w.get(1, 0), 2); net.compile_incidence();
506 assert!(net.cached_incidence.is_some());
507 assert!(net.cached_index.is_some());
508 let w_cached = net.incidence_matrix();
509 assert_eq!(w, w_cached);
510 }
511
512 #[test]
513 fn test_verifies_state_equation_calculus() {
514 let mut net = PetriNet::default();
515 net.places.push(Place {
516 id: "p1".to_string(),
517 });
518 net.places.push(Place {
519 id: "p2".to_string(),
520 });
521 net.transitions.push(Transition {
522 id: "t1".to_string(),
523 label: "A".to_string(),
524 is_invisible: None,
525 });
526 net.arcs.push(Arc {
527 from: "p1".to_string(),
528 to: "t1".to_string(),
529 weight: None,
530 });
531 net.arcs.push(Arc {
532 from: "t1".to_string(),
533 to: "p2".to_string(),
534 weight: None,
535 });
536
537 assert!(net.is_structural_workflow_net());
538 assert!(net.verifies_state_equation_calculus());
539
540 net.transitions.push(Transition {
542 id: "t2".to_string(),
543 label: "B".to_string(),
544 is_invisible: None,
545 });
546 net.arcs.push(Arc {
547 from: "t2".to_string(),
548 to: "p2".to_string(),
549 weight: None,
550 });
551
552 assert!(!net.is_structural_workflow_net());
553 assert!(!net.verifies_state_equation_calculus());
554 }
555}
556
557#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
559pub struct DeclareConstraint {
560 pub constraint_type: String,
561 pub activities: Vec<String>,
562 pub condition: String,
563}
564
565impl DeclareConstraint {
566 pub fn new(constraint_type: String, activities: Vec<String>, condition: String) -> Self {
567 DeclareConstraint {
568 constraint_type,
569 activities,
570 condition,
571 }
572 }
573}
574
575#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
577pub struct DeclareModel {
578 pub constraints: Vec<DeclareConstraint>,
579 pub activities: Vec<String>,
580}
581
582impl DeclareModel {
583 pub fn new() -> Self {
584 DeclareModel {
585 constraints: Vec::new(),
586 activities: Vec::new(),
587 }
588 }
589}
590
591impl Default for DeclareModel {
592 fn default() -> Self {
593 Self::new()
594 }
595}
596
597#[cfg(test)]
598mod tests_petri {
599 use super::*;
600
601 #[test]
602 fn test_dfg_creation() {
603 let dfg = DFG::new();
604 assert!(dfg.is_empty());
605 }
606
607}
608