ebi 0.3.14

A stochastic process mining utility and library
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
use crate::{
    ebi_file_handlers::{
        event_log_python::EBI_EVENT_LOG_PYTHON, labelled_petri_net::EBI_LABELLED_PETRI_NET,
        process_tree::EBI_PROCESS_TREE,
    },
    ebi_framework::{
        ebi_file_handler::EbiFileHandler,
        ebi_input::{EbiInput, EbiInputType},
        ebi_trait::EbiTrait,
        ebi_trait_object::EbiTraitObject,
        object_importers::{
            ToDeterministicFiniteAutomatonObject, ToEventLogObject, ToEventLogXesObject,
            ToFiniteLanguageObject, ToFiniteStochasticLanguageObject, ToLabelledPetriNetObject,
            ToProcessTreeObject, ToStochasticDeterministicFiniteAutomatonObject,
        },
        trait_importers::{
            ToActivitiesTrait, ToEventLogTrait, ToFiniteLanguageTrait,
            ToFiniteStochasticLanguageTrait, ToGraphableTrait, ToIterableLanguageTrait,
            ToIterableStochasticLanguageTrait, ToQueriableStochasticLanguageTrait,
            ToSemanticsTrait, ToStochasticDeterministicSemanticsTrait, ToStochasticSemanticsTrait,
        },
    },
    ebi_traits::ebi_trait_semantics::EbiTraitSemantics,
    text::Joiner,
};
use ebi_objects::{
    ActivityKey, EbiObject, EbiObjectType, EventLogPython, LabelledPetriNet, ProcessTree,
    anyhow::Result,
    ebi_arithmetic::{Fraction, is_exact_globally},
    marking::Marking,
};
use process_mining::core::event_data::case_centric::{
    AttributeValue, Attributes, Event, EventLogClassifier, Trace, to_attributes,
};
use pyo3::{
    Bound, PyAny, PyResult,
    exceptions::PyValueError,
    types::{PyAnyMethods, PyDict, PyDictMethods, PyList, PyListMethods, PySet, PySetMethods},
};
use std::{
    collections::HashMap,
    iter::Peekable,
    str::{Chars, FromStr},
};

type PyImporter = fn(&Bound<'_, PyAny>, &[&'static EbiInputType]) -> PyResult<EbiInput>;

pub const PYTHON_IMPORTERS: &[PyImporter] = &[
    usize::py_import,
    Fraction::py_import,
    EventLogPython::py_import,
    LabelledPetriNet::py_import,
    ProcessTree::py_import,
];

pub trait ImportableFromPM4Py {
    /// Tranform a PM4Py Object into an Ebi struct
    fn py_extract_object(py_object: &Bound<'_, PyAny>) -> PyResult<Self>
    where
        Self: Sized;

    /// Convert an Ebi struct into an Ebi Input, depending on the requested input types
    fn py_object_to_ebi_input(self, input_types: &[&'static EbiInputType]) -> Option<EbiInput>;

    const PY_FILE_HANDLER: Option<&'static EbiFileHandler>;

    /// Imports a PM4Py Object as its Ebi equivalent.
    fn py_import(
        py_object: &Bound<'_, PyAny>,
        input_types: &[&'static EbiInputType],
    ) -> PyResult<EbiInput>
    where
        Self: Sized,
    {
        let ebi_object = Self::py_extract_object(py_object)?;
        match Self::py_object_to_ebi_input(ebi_object, input_types) {
            Some(ebi_input) => Ok(ebi_input),
            None => Err(PyValueError::new_err(format!(
                "Importing of input parameter failed. Expected {}",
                EbiInputType::get_possible_inputs(input_types).join_with(", ", " and ")
            ))),
        }
    }
}

impl ImportableFromPM4Py for usize {
    const PY_FILE_HANDLER: Option<&'static EbiFileHandler> = None;

    fn py_extract_object(py_object: &Bound<'_, PyAny>) -> PyResult<Self> {
        py_object.extract().map_err(|_| {
            PyValueError::new_err("Expected a Python integer to convert to Rust usize")
        })
    }

    fn py_object_to_ebi_input(self, input_types: &[&'static EbiInputType]) -> Option<EbiInput> {
        for &itype in input_types {
            match itype {
                EbiInputType::Usize(..) => {
                    return Some(EbiInput::Usize(self, itype));
                }
                _ => {}
            }
        }
        None
    }
}

impl ImportableFromPM4Py for Fraction {
    const PY_FILE_HANDLER: Option<&'static EbiFileHandler> = None;

    fn py_extract_object(py_object: &Bound<'_, PyAny>) -> PyResult<Self> {
        let value: f64 = py_object
            .extract()
            .map_err(|_| PyValueError::new_err("Expected a Python double"))?;

        value
            .to_string()
            .parse()
            .map_err(|_| PyValueError::new_err("Failed to convert f64 to fraction"))
    }

    fn py_object_to_ebi_input(self, input_types: &[&'static EbiInputType]) -> Option<EbiInput> {
        for &itype in input_types {
            match itype {
                EbiInputType::Fraction(..) => {
                    if is_exact_globally() {
                        return Some(EbiInput::Fraction(self, itype));
                    } else {
                    }
                }
                _ => { /* skip other input types */ }
            }
        }
        None
    }
}

impl ImportableFromPM4Py for EventLogPython {
    const PY_FILE_HANDLER: Option<&'static EbiFileHandler> = Some(&EBI_EVENT_LOG_PYTHON);

    fn py_extract_object(py_object: &Bound<'_, PyAny>) -> PyResult<Self> {
        // Extract the list of traces from the PM4Py event log.
        let binding = py_object.getattr("_list")?;
        let py_traces = binding.cast::<PyList>()?;
        let mut traces = Vec::new();
        for py_trace in py_traces.iter() {
            let trace = trace_from_py(&py_trace)?;
            traces.push(trace);
        }
        // Get log-level attributes, if available.
        let attributes = if let Ok(attr_obj) = py_object.getattr("_attributes") {
            convert_py_dict_to_attributes(&attr_obj)?
        } else {
            vec![]
        };
        // Create a ProcessMiningEventLog instance.
        let pm_log = process_mining::EventLog {
            attributes,
            traces,
            extensions: None,
            classifiers: None,
            global_trace_attrs: None,
            global_event_attrs: None,
        };
        Ok(EventLogPython::from((
            pm_log,
            EventLogClassifier::default(),
        )))
    }

    fn py_object_to_ebi_input(self, input_types: &[&EbiInputType]) -> Option<EbiInput> {
        for &itype in input_types {
            match itype {
                EbiInputType::Object(EbiObjectType::EventLog) => {
                    return Some(EbiInput::Object(
                        self.to_event_log_object(),
                        &Self::PY_FILE_HANDLER.unwrap(),
                    ));
                }
                EbiInputType::Object(EbiObjectType::EventLogXes) => {
                    return Some(EbiInput::Object(
                        self.to_event_log_xes_object(),
                        &Self::PY_FILE_HANDLER.unwrap(),
                    ));
                }
                EbiInputType::Object(EbiObjectType::FiniteLanguage) => {
                    return Some(EbiInput::Object(
                        self.to_finite_language_object(),
                        &Self::PY_FILE_HANDLER.unwrap(),
                    ));
                }
                EbiInputType::Object(EbiObjectType::FiniteStochasticLanguage) => {
                    return Some(EbiInput::Object(
                        self.to_finite_stochastic_language_object(),
                        &Self::PY_FILE_HANDLER.unwrap(),
                    ));
                }
                EbiInputType::Object(EbiObjectType::DeterministicFiniteAutomaton) => {
                    return Some(EbiInput::Object(
                        self.to_deterministic_finite_automaton_object(),
                        &Self::PY_FILE_HANDLER.unwrap(),
                    ));
                }
                EbiInputType::Object(EbiObjectType::StochasticDeterministicFiniteAutomaton) => {
                    return Some(EbiInput::Object(
                        self.to_stochastic_deterministic_finite_automaton_object(),
                        &Self::PY_FILE_HANDLER.unwrap(),
                    ));
                }
                EbiInputType::Trait(etrait) => {
                    match etrait {
                        EbiTrait::Activities => {
                            return Some(EbiInput::Trait(
                                self.to_activities_ebi_trait_object(),
                                &Self::PY_FILE_HANDLER.unwrap(),
                            ));
                        }
                        EbiTrait::EventLog => {
                            return Some(EbiInput::Trait(
                                self.to_event_log_ebi_trait_object(),
                                &Self::PY_FILE_HANDLER.unwrap(),
                            ));
                        }
                        EbiTrait::IterableLanguage => {
                            return Some(EbiInput::Trait(
                                self.to_iterable_language_ebi_trait_object(),
                                &Self::PY_FILE_HANDLER.unwrap(),
                            ));
                        }
                        EbiTrait::IterableStochasticLanguage => {
                            return Some(EbiInput::Trait(
                                self.to_iterable_stochastic_language_ebi_trait_object(),
                                &Self::PY_FILE_HANDLER.unwrap(),
                            ));
                        }
                        EbiTrait::FiniteStochasticLanguage => {
                            return Some(EbiInput::Trait(
                                self.to_finite_stochastic_language_ebi_trait_object(),
                                &Self::PY_FILE_HANDLER.unwrap(),
                            ));
                        }
                        EbiTrait::FiniteLanguage => {
                            return Some(EbiInput::Trait(
                                self.to_finite_language_ebi_trait_object(),
                                &Self::PY_FILE_HANDLER.unwrap(),
                            ));
                        }
                        EbiTrait::QueriableStochasticLanguage => {
                            return Some(EbiInput::Trait(
                                self.to_queriable_stochastic_language_ebi_trait_object(),
                                &Self::PY_FILE_HANDLER.unwrap(),
                            ));
                        }
                        EbiTrait::Semantics => {
                            return Some(EbiInput::Trait(
                                self.to_semantics_ebi_trait_object(),
                                &Self::PY_FILE_HANDLER.unwrap(),
                            ));
                        }
                        EbiTrait::StochasticSemantics => {
                            return Some(EbiInput::Trait(
                                self.to_stochastic_semantics_ebi_trait_object(),
                                &Self::PY_FILE_HANDLER.unwrap(),
                            ));
                        }
                        EbiTrait::StochasticDeterministicSemantics => {
                            return Some(EbiInput::Trait(
                                self.to_stochastic_deterministic_semantics_ebi_trait_object(),
                                &Self::PY_FILE_HANDLER.unwrap(),
                            ));
                        }
                        _ => { /* this trait isn’t supported by EventLog; skip */ }
                    }
                }
                EbiInputType::AnyObject => {
                    let obj = EbiObject::EventLog(self.into());
                    return Some(EbiInput::Object(obj, &Self::PY_FILE_HANDLER.unwrap()));
                }
                _ => {
                    // skip other input types
                }
            }
        }
        None
    }
}

// helper functions for event logs
// A helper to convert a Python dict into our Attributes.
fn convert_py_dict_to_attributes(py_obj: &Bound<'_, PyAny>) -> PyResult<Attributes> {
    let dict = py_obj.cast::<PyDict>()?;
    let mut map = HashMap::new();
    for (key, value) in dict.iter() {
        let key_str: String = key.extract()?;
        // For simplicity, we assume every attribute is a string.
        let value_str: String = value.str()?.to_string();
        map.insert(key_str, AttributeValue::String(value_str));
    }
    Ok(to_attributes(map))
}

// Build an Event from a PyAny that represents a PM4Py Event.
pub fn event_from_py(py_event: &Bound<'_, PyAny>) -> PyResult<Event> {
    // PM4Py Event stores its data in _dict
    let dict_obj = py_event.getattr("_dict")?;
    let attributes = convert_py_dict_to_attributes(&dict_obj)?;
    Ok(Event { attributes })
}

// Build a Trace from a PyAny that represents a PM4Py Trace.
pub fn trace_from_py(py_trace: &Bound<'_, PyAny>) -> PyResult<Trace> {
    let attributes = if let Ok(attr_obj) = py_trace.getattr("_attributes") {
        convert_py_dict_to_attributes(&attr_obj)?
    } else {
        vec![]
    };
    let binding = py_trace.getattr("_list")?;
    let py_events = binding.cast::<PyList>()?;
    let mut events = Vec::new();
    for py_event in py_events.iter() {
        let event = event_from_py(&py_event)?;
        events.push(event);
    }
    Ok(Trace { attributes, events })
}

impl ImportableFromPM4Py for LabelledPetriNet {
    const PY_FILE_HANDLER: Option<&'static EbiFileHandler> = Some(&EBI_LABELLED_PETRI_NET);

    fn py_extract_object(py_object: &Bound<'_, PyAny>) -> PyResult<Self> {
        // Retrieve "places", "transitions", and "arcs" attributes as sets.
        let binding = py_object.getattr("places")?;
        let py_places = binding.cast::<PySet>().map_err(|e| {
            pyo3::exceptions::PyValueError::new_err(format!("Failed to get places: {}", e))
        })?;
        let binding = py_object.getattr("transitions")?;
        let py_transitions = binding.cast::<PySet>().map_err(|e| {
            pyo3::exceptions::PyValueError::new_err(format!("Failed to get transitions: {}", e))
        })?;
        let binding = py_object.getattr("arcs")?;
        let py_arcs = binding.cast::<PySet>().map_err(|e| {
            pyo3::exceptions::PyValueError::new_err(format!("Failed to get arcs: {}", e))
        })?;

        // Convert sets to vectors.
        let places = py_places.iter().collect::<Vec<_>>();
        let transitions = py_transitions.iter().collect::<Vec<_>>();
        let arcs = py_arcs.iter().collect::<Vec<_>>();

        let num_places = places.len();
        let num_transitions = transitions.len();

        // Create mappings using raw pointers as keys.
        let mut place_idx_map: HashMap<usize, usize> = HashMap::new();
        for (i, place) in places.iter().enumerate() {
            place_idx_map.insert(place.as_ptr() as usize, i);
        }
        let mut trans_idx_map: HashMap<usize, usize> = HashMap::new();
        for (i, trans) in transitions.iter().enumerate() {
            trans_idx_map.insert(trans.as_ptr() as usize, i);
        }

        // For this example, assume initial marking is all zeros.
        let initial_marking = Marking {
            place2token: vec![0u64; num_places],
        };

        // Build labels vector for transitions.
        let mut activity_key = ActivityKey::new();
        let mut labels = Vec::with_capacity(num_transitions);
        for trans in transitions.iter() {
            if let Ok(label_obj) = trans.getattr("label") {
                let label_str: String = label_obj.extract().unwrap_or_default();
                let act = activity_key.process_activity(&label_str);
                labels.push(Some(act));
            } else {
                labels.push(None);
            }
        }

        // Initialize mapping vectors.
        let mut transition2input_places = vec![Vec::new(); num_transitions];
        let mut transition2output_places = vec![Vec::new(); num_transitions];
        let mut transition2input_places_cardinality = vec![Vec::new(); num_transitions]; // default cardinality
        let mut transition2output_places_cardinality = vec![Vec::new(); num_transitions]; // default cardinality
        let mut place2output_transitions = vec![Vec::new(); num_places];

        // Process each arc.
        for arc in arcs {
            // Each arc should have "source" and "target" attributes.
            let source = arc.getattr("source").map_err(|e| {
                pyo3::exceptions::PyValueError::new_err(format!(
                    "Arc missing source attribute: {}",
                    e
                ))
            })?;
            let target = arc.getattr("target").map_err(|e| {
                pyo3::exceptions::PyValueError::new_err(format!(
                    "Arc missing target attribute: {}",
                    e
                ))
            })?;
            let weight = arc.getattr("weight")?.extract::<u64>().map_err(|e| {
                pyo3::exceptions::PyValueError::new_err(format!(
                    "Arc missing weight attribute: {}",
                    e
                ))
            })?;
            let source_key = source.as_ptr() as usize;
            let target_key = target.as_ptr() as usize;

            if place_idx_map.contains_key(&source_key) {
                let p_idx = *place_idx_map.get(&source_key).unwrap();
                let t_idx = *trans_idx_map.get(&target_key).ok_or_else(|| {
                    pyo3::exceptions::PyValueError::new_err(
                        "Arc target not found among transitions",
                    )
                })?;
                // Update place-to-transition mappings.
                place2output_transitions[p_idx].push(t_idx);
                transition2input_places[t_idx].push(p_idx);
                transition2input_places_cardinality[t_idx].push(weight);
            } else if trans_idx_map.contains_key(&source_key) {
                let t_idx = *trans_idx_map.get(&source_key).unwrap();
                let p_idx = *place_idx_map.get(&target_key).ok_or_else(|| {
                    pyo3::exceptions::PyValueError::new_err("Arc target not found among places")
                })?;
                // Update transition-to-place mappings.
                transition2output_places[t_idx].push(p_idx);
                place2output_transitions[p_idx].push(t_idx);
                transition2output_places_cardinality[t_idx].push(weight);
            } else {
                return Err(pyo3::exceptions::PyValueError::new_err(
                    "Arc source not found in either places or transitions",
                ));
            }
        }

        Ok(LabelledPetriNet {
            activity_key,
            initial_marking: Some(initial_marking),
            labels,
            place2output_transitions,
            transition2input_places,
            transition2output_places,
            transition2input_places_cardinality,
            transition2output_places_cardinality,
        })
    }

    fn py_object_to_ebi_input(self, input_types: &[&'static EbiInputType]) -> Option<EbiInput> {
        for &itype in input_types {
            match itype {
                EbiInputType::Object(EbiObjectType::LabelledPetriNet) => {
                    let obj = EbiObject::LabelledPetriNet(self);
                    return Some(EbiInput::Object(obj, &EBI_LABELLED_PETRI_NET));
                }
                EbiInputType::Trait(etrait) => match etrait {
                    EbiTrait::Activities => {
                        return Some(EbiInput::Trait(
                            self.to_activities_ebi_trait_object(),
                            &Self::PY_FILE_HANDLER.unwrap(),
                        ));
                    }
                    EbiTrait::Semantics => {
                        let sem = EbiTraitSemantics::Marking(Box::new(self));
                        let trait_obj = EbiTraitObject::Semantics(sem);
                        return Some(EbiInput::Trait(trait_obj, &EBI_LABELLED_PETRI_NET));
                    }
                    EbiTrait::Graphable => {
                        let trait_obj = EbiTraitObject::Graphable(Box::new(self));
                        return Some(EbiInput::Trait(trait_obj, &EBI_LABELLED_PETRI_NET));
                    }
                    _ => {}
                },
                EbiInputType::AnyObject => {
                    let obj = EbiObject::LabelledPetriNet(self);
                    return Some(EbiInput::Object(obj, &EBI_LABELLED_PETRI_NET));
                }
                _ => {}
            }
        }
        None
    }
}

impl ImportableFromPM4Py for ProcessTree {
    const PY_FILE_HANDLER: Option<&'static EbiFileHandler> = Some(&EBI_PROCESS_TREE);

    fn py_extract_object(py_object: &Bound<'_, PyAny>) -> PyResult<Self> {
        // 1) Get the `.ptree` text via Python's repr/to_string
        let repr: String = py_object
            .call_method0("to_string")?
            .extract()
            .map_err(|e| {
                PyValueError::new_err(format!("Failed to serialize ProcessTree: {}", e))
            })?;

        // 2) Run our text‐based importer
        let text = translate_pm4py_process_tree(&repr)
            .map_err(|e| PyValueError::new_err(format!("Translation error: {}", e)))?;
        // 3) parse
        let rust_tree = ProcessTree::from_str(&text)
            .map_err(|e| PyValueError::new_err(format!("Import fail: {}", e)))?;

        Ok(rust_tree)
    }

    fn py_object_to_ebi_input(self, input_types: &[&'static EbiInputType]) -> Option<EbiInput> {
        for &itype in input_types {
            match itype {
                // raw ProcessTree object
                EbiInputType::Object(EbiObjectType::ProcessTree) => {
                    return Some(EbiInput::Object(
                        self.to_process_tree_object(),
                        &Self::PY_FILE_HANDLER.unwrap(),
                    ));
                }
                EbiInputType::Object(EbiObjectType::LabelledPetriNet) => {
                    return Some(EbiInput::Object(
                        self.to_labelled_petri_net_object(),
                        &Self::PY_FILE_HANDLER.unwrap(),
                    ));
                }
                EbiInputType::Trait(EbiTrait::Activities) => {
                    return Some(EbiInput::Trait(
                        self.to_activities_ebi_trait_object(),
                        &Self::PY_FILE_HANDLER.unwrap(),
                    ));
                }
                // as the Semantics trait
                EbiInputType::Trait(EbiTrait::Semantics) => {
                    return Some(EbiInput::Trait(
                        self.to_semantics_ebi_trait_object(),
                        &Self::PY_FILE_HANDLER.unwrap(),
                    ));
                }

                // as the Graphable trait
                EbiInputType::Trait(EbiTrait::Graphable) => {
                    return Some(EbiInput::Trait(
                        self.to_graphable_ebi_trait_object(),
                        &Self::PY_FILE_HANDLER.unwrap(),
                    ));
                }

                // fallback: treat as any‐object
                EbiInputType::AnyObject => {
                    return Some(EbiInput::Object(
                        EbiObject::ProcessTree(self),
                        &Self::PY_FILE_HANDLER.unwrap(),
                    ));
                }

                _ => { /* skip unsupported */ }
            }
        }
        None
    }
}

// helper functions for process trees
fn translate_pm4py_process_tree(s: &str) -> Result<String, String> {
    // Minimal AST
    enum Node {
        Act(String),
        Tau,
        Op(&'static str, Vec<Node>),
    }
    // Parser with inline utilities
    struct P<'a> {
        it: Peekable<Chars<'a>>,
    }
    impl<'a> P<'a> {
        fn new(s: &'a str) -> Self {
            P {
                it: s.chars().peekable(),
            }
        }
        fn ws(&mut self) {
            while matches!(self.it.peek(), Some(c) if c.is_whitespace()) {
                self.it.next();
            }
        }
        fn eat(&mut self, c: char) -> bool {
            match self.it.peek() {
                Some(&x) if x == c => {
                    self.it.next();
                    true
                }
                _ => false,
            }
        }
        fn parse(&mut self) -> Result<Node, String> {
            self.ws();
            if self.eat('-') {
                self.eat('>');
                return self.op("sequence");
            }
            if self.eat('X') {
                return self.op("xor");
            }
            if self.eat('&') {
                return self.op("concurrent");
            }
            if self.eat('*') {
                return self.op("loop");
            }
            if self.eat('\'') {
                let mut lbl = String::new();
                while let Some(c) = self.it.next() {
                    if c == '\'' {
                        break;
                    } else {
                        lbl.push(c)
                    }
                }
                return Ok(Node::Act(lbl));
            }
            if self.it.clone().take(3).collect::<String>() == "tau" {
                for _ in 0..3 {
                    self.it.next();
                }
                return Ok(Node::Tau);
            }
            Err("Unexpected token".into())
        }
        fn op(&mut self, name: &'static str) -> Result<Node, String> {
            self.ws();
            if !self.eat('(') {
                return Err("Expected '('.".into());
            }
            let mut children = Vec::new();
            loop {
                self.ws();
                if self.eat(')') {
                    break;
                }
                children.push(self.parse()?);
                self.ws();
                let _ = self.eat(',');
            }
            if children.is_empty() {
                return Err(format!("Operator '{}' needs children", name));
            }
            Ok(Node::Op(name, children))
        }
    }
    // format
    fn fmt(n: &Node, indent: usize, out: &mut String) {
        let pad = "\t".repeat(indent);
        match n {
            Node::Act(l) => out.push_str(&format!("{pad}activity {l}\n")),
            Node::Tau => out.push_str(&format!("{pad}tau\n")),
            Node::Op(nm, ch) => {
                out.push_str(&format!("{pad}{nm}\n{pad}{}\n", ch.len()));
                for c in ch {
                    fmt(c, indent + 1, out);
                }
            }
        }
    }
    let mut parser = P::new(s);
    let root = parser.parse()?;
    parser.ws();
    if parser.it.peek().is_some() {
        return Err("Trailing chars".into());
    }
    let mut out = String::from("process tree\n");
    fmt(&root, 0, &mut out);
    Ok(out)
}

#[cfg(test)]
mod tests {
    use crate::{
        ebi_framework::ebi_input::EbiInputType, python::python_import::ImportableFromPM4Py,
    };
    use ebi_objects::{EventLogPython, EventLogXes, LabelledPetriNet, ProcessTree};
    use std::fs;

    #[test]
    fn py_importers_event_log() {
        let fin = fs::read_to_string("testfiles/a-b.xes").unwrap();
        let log: EventLogPython = fin.parse::<EventLogXes>().unwrap().into();

        //every trait importer must be supported
        for trait_importer in EventLogPython::PY_FILE_HANDLER.unwrap().trait_importers {
            let input_type = Box::new(EbiInputType::Trait(trait_importer.get_trait()));
            let input_type: &'static EbiInputType = Box::leak(input_type);
            let input_types = [input_type];
            println!("trait_importer {}", trait_importer);
            let value = log.clone().py_object_to_ebi_input(&input_types).unwrap();

            //check that the value is of the requested trait
            assert_eq!(
                value.get_type(),
                EbiInputType::Trait(trait_importer.get_trait())
            );
        }

        //every object importer must be supported
        for object_importer in EventLogPython::PY_FILE_HANDLER.unwrap().object_importers {
            let input_type = Box::new(EbiInputType::Object(object_importer.get_type()));
            let input_type: &'static EbiInputType = Box::leak(input_type);
            let input_types = [input_type];
            println!("object_importer {}", object_importer);
            let value = log.clone().py_object_to_ebi_input(&input_types).unwrap();

            //check that the value is of the requested trait
            assert_eq!(
                value.get_type(),
                EbiInputType::Object(object_importer.get_type())
            );
        }
    }

    #[test]
    fn py_importers_event_lpn() {
        let fin = fs::read_to_string("testfiles/a-loop.lpn").unwrap();
        let log = fin.parse::<LabelledPetriNet>().unwrap();

        //every trait importer must be supported
        for trait_importer in LabelledPetriNet::PY_FILE_HANDLER.unwrap().trait_importers {
            let input_type = Box::new(EbiInputType::Trait(trait_importer.get_trait()));
            let input_type: &'static EbiInputType = Box::leak(input_type);
            let input_types = [input_type];
            println!("trait_importer {}", trait_importer);
            let value = log.clone().py_object_to_ebi_input(&input_types).unwrap();

            //check that the value is of the requested trait
            assert_eq!(
                value.get_type(),
                EbiInputType::Trait(trait_importer.get_trait())
            );
        }

        //every object importer must be supported
        for object_importer in LabelledPetriNet::PY_FILE_HANDLER.unwrap().object_importers {
            let input_type = Box::new(EbiInputType::Object(object_importer.get_type()));
            let input_type: &'static EbiInputType = Box::leak(input_type);
            let input_types = [input_type];
            println!("object_importer {}", object_importer);
            let value = log.clone().py_object_to_ebi_input(&input_types).unwrap();

            //check that the value is of the requested trait
            assert_eq!(
                value.get_type(),
                EbiInputType::Object(object_importer.get_type())
            );
        }
    }

    #[test]
    fn py_importers_event_ptree() {
        let fin = fs::read_to_string("testfiles/tree.ptree").unwrap();
        let log = fin.parse::<ProcessTree>().unwrap();

        //every trait importer must be supported
        for trait_importer in ProcessTree::PY_FILE_HANDLER.unwrap().trait_importers {
            let input_type = Box::new(EbiInputType::Trait(trait_importer.get_trait()));
            let input_type: &'static EbiInputType = Box::leak(input_type);
            let input_types = [input_type];
            println!("trait_importer {}", trait_importer);
            let value = log.clone().py_object_to_ebi_input(&input_types).unwrap();

            //check that the value is of the requested trait
            assert_eq!(
                value.get_type(),
                EbiInputType::Trait(trait_importer.get_trait())
            );
        }

        //every object importer must be supported
        for object_importer in ProcessTree::PY_FILE_HANDLER.unwrap().object_importers {
            let input_type = Box::new(EbiInputType::Object(object_importer.get_type()));
            let input_type: &'static EbiInputType = Box::leak(input_type);
            let input_types = [input_type];
            println!("object_importer {}", object_importer);
            let value = log.clone().py_object_to_ebi_input(&input_types).unwrap();

            //check that the value is of the requested trait
            assert_eq!(
                value.get_type(),
                EbiInputType::Object(object_importer.get_type())
            );
        }
    }
}