Skip to main content

ebi_objects/ebi_objects/
event_log_xes.rs

1#[cfg(any(test, feature = "testactivities"))]
2use ebi_activity_key::TestActivityKey;
3use crate::{
4    Activity, ActivityKey, EbiObject, Exportable, HasActivityKey, Importable, Infoable,
5    IntoTraceIterator, NumberOfTraces, TranslateActivityKey,
6    iterators::{parallel_trace_iterator::ParallelTraceIterator, trace_iterator::TraceIterator},
7    log_infoable_startend, log_infoable_stats,
8    traits::{
9        importable::{ImporterParameter, ImporterParameterValues, from_string},
10        start_end_activities::StartEndActivities,
11    },
12};
13use anyhow::{Context, Result, anyhow};
14use ebi_arithmetic::{Fraction, One};
15use ebi_derive::ActivityKey;
16use intmap::IntMap;
17use process_mining::core::event_data::case_centric::{
18    Event, EventLogClassifier, Trace,
19    xes::{XESImportOptions, export_xes_event_log, import_xes},
20};
21use std::{
22    fmt,
23    io::{BufRead, Write},
24};
25
26pub const DEFAULT_PARAMETER_ACTIVITY: &str = "concept:name";
27
28pub const XES_IMPORTER_PARAMETER_ACTIVITY: ImporterParameter = ImporterParameter::String {
29    name: "xes_event_classifier",
30    short_name: "ec",
31    explanation: "The attribute that defines, for each event, what its activity is",
32    allowed_values: None,
33    default_value: DEFAULT_PARAMETER_ACTIVITY,
34};
35
36#[derive(ActivityKey, Clone)]
37pub struct EventLogXes {
38    pub(crate) classifier: EventLogClassifier,
39    pub(crate) activity_key: ActivityKey,
40    pub rust4pm_log: process_mining::EventLog,
41}
42
43impl EventLogXes {
44    pub fn event_classifier(&self) -> &EventLogClassifier {
45        &self.classifier
46    }
47
48    pub fn create_activity_key(&mut self) {
49        self.rust4pm_log.traces.iter().for_each(|trace| {
50            trace.events.iter().for_each(|event| {
51                self.activity_key
52                    .process_activity(&self.classifier.get_class_identity(event));
53            })
54        });
55    }
56
57    pub fn retain_traces_mut<F>(&mut self, f: &mut F)
58    where
59        F: FnMut(&Trace) -> bool,
60    {
61        //get our hands free to change the traces without cloning
62        let mut rust4pm_traces = vec![];
63        std::mem::swap(&mut self.rust4pm_log.traces, &mut rust4pm_traces);
64
65        rust4pm_traces = rust4pm_traces
66            .into_iter()
67            .filter_map(|mut rust4pm_trace| {
68                if f(&mut rust4pm_trace) {
69                    Some(rust4pm_trace)
70                } else {
71                    None
72                }
73            })
74            .collect();
75
76        //swap the the traces back
77        std::mem::swap(&mut self.rust4pm_log.traces, &mut rust4pm_traces);
78    }
79
80    pub fn retain_traces<'a>(&'a mut self, f: Box<dyn Fn(&Vec<Activity>) -> bool + 'static>) {
81        let mut activity_key = self.activity_key().clone();
82
83        let event_classifier = self.event_classifier().clone();
84
85        self.rust4pm_log.traces.retain(|trace| {
86            let mut result = Vec::with_capacity(trace.events.len());
87
88            for event in trace.events.iter() {
89                let activity =
90                    activity_key.process_activity(&event_classifier.get_class_identity(event));
91
92                result.push(activity);
93            }
94
95            f(&result)
96        });
97    }
98
99    pub fn get_event(&self, trace_index: usize, event_index: usize) -> Option<&Event> {
100        self.rust4pm_log
101            .traces
102            .get(trace_index)?
103            .events
104            .get(event_index)
105    }
106}
107
108impl TranslateActivityKey for EventLogXes {
109    fn translate_using_activity_key(&mut self, to_activity_key: &mut ActivityKey) {
110        std::mem::swap(&mut self.activity_key, to_activity_key);
111        self.create_activity_key();
112        let mut cloned = self.activity_key.clone();
113        std::mem::swap(to_activity_key, &mut cloned);
114    }
115}
116
117impl Importable for EventLogXes {
118    const FILE_FORMAT_SPECIFICATION_LATEX: &str =
119        "An event log file follows the IEEE XES format~\\cite{DBLP:journals/cim/AcamporaVSAGV17}. 
120Parsing is performed by the Rust4PM crate~\\cite{DBLP:conf/bpm/KustersA24}.
121For instance:
122    \\lstinputlisting[language=xml, style=boxed]{../testfiles/a-b.xes}";
123
124    const IMPORTER_PARAMETERS: &[ImporterParameter] = &[XES_IMPORTER_PARAMETER_ACTIVITY];
125
126    fn import_as_object(
127        reader: &mut dyn BufRead,
128        parameter_values: &ImporterParameterValues,
129    ) -> Result<EbiObject> {
130        Ok(EbiObject::EventLogXes(Self::import(
131            reader,
132            parameter_values,
133        )?))
134    }
135
136    fn import(
137        reader: &mut dyn BufRead,
138        parameter_values: &ImporterParameterValues,
139    ) -> anyhow::Result<Self>
140    where
141        Self: Sized,
142    {
143        let log = import_xes(
144            reader,
145            XESImportOptions {
146                verbose: false,
147                ..Default::default()
148            },
149        );
150        let log = match log {
151            Ok(l) => l,
152            Err(e) => return Err(anyhow!("{}", e)),
153        };
154
155        //create the classifier
156        let key = parameter_values
157            .get(&XES_IMPORTER_PARAMETER_ACTIVITY)
158            .ok_or_else(|| anyhow!("expected parameter not found"))?;
159        let classifier = EventLogClassifier {
160            name: key.clone().as_string()?,
161            keys: vec![key.clone().as_string()?],
162        };
163
164        Ok((log, classifier).into())
165    }
166}
167from_string!(EventLogXes);
168
169impl Exportable for EventLogXes {
170    fn export_from_object(object: EbiObject, f: &mut dyn Write) -> Result<()> {
171        match object {
172            EbiObject::EventLog(log) => log.export(f),
173            EbiObject::EventLogXes(log) => log.export(f),
174            EbiObject::EventLogPython(log) => log.export(f),
175            EbiObject::EventLogCsv(log) => {
176                let xes: EventLogXes = log
177                    .try_into()
178                    .with_context(|| anyhow!("Cannot transform csv to xes."))?;
179                xes.export(f)
180            }
181            _ => Err(anyhow!("Cannot export as xes event log.")),
182        }
183    }
184
185    fn export(&self, f: &mut dyn std::io::Write) -> Result<()> {
186        export_xes_event_log(f, &self.rust4pm_log)?;
187        Ok(())
188    }
189}
190
191impl fmt::Display for EventLogXes {
192    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193        write!(f, "event log with {} traces", self.number_of_traces())
194    }
195}
196
197impl Infoable for EventLogXes {
198    fn info(&self, f: &mut impl std::io::Write) -> Result<()> {
199        let lengths = self.rust4pm_log.traces.iter().map(|t| t.events.len());
200        log_infoable_stats!(f, self, lengths);
201
202        writeln!(f, "")?;
203        self.activity_key().info(f)?;
204
205        log_infoable_startend!(f, self);
206
207        Ok(writeln!(f, "")?)
208    }
209}
210
211impl NumberOfTraces for EventLogXes {
212    fn number_of_traces(&self) -> usize {
213        self.rust4pm_log.traces.len()
214    }
215
216    fn number_of_events(&self) -> usize {
217        self.rust4pm_log.traces.iter().map(|t| t.events.len()).sum()
218    }
219}
220
221impl StartEndActivities for EventLogXes {
222    fn start_activities(&self) -> intmap::IntMap<Activity, Fraction> {
223        let mut result = IntMap::new();
224        for trace in self.rust4pm_log.traces.iter() {
225            if let Some(event) = trace.events.iter().next() {
226                let activity_label = self.event_classifier().get_class_identity(event);
227                let activity = self.activity_key.process_activity_attempt(&activity_label);
228                if let Some(activity) = activity {
229                    match result.entry(activity) {
230                        intmap::Entry::Occupied(mut occupied_entry) => {
231                            *occupied_entry.get_mut() += 1
232                        }
233                        intmap::Entry::Vacant(vacant_entry) => {
234                            vacant_entry.insert(Fraction::one());
235                        }
236                    }
237                }
238            }
239        }
240        result
241    }
242
243    fn end_activities(&self) -> intmap::IntMap<Activity, Fraction> {
244        let mut result = IntMap::new();
245        for trace in self.rust4pm_log.traces.iter() {
246            if let Some(event) = trace.events.iter().last() {
247                let activity_label = self.event_classifier().get_class_identity(event);
248                let activity = self.activity_key.process_activity_attempt(&activity_label);
249                if let Some(activity) = activity {
250                    match result.entry(activity) {
251                        intmap::Entry::Occupied(mut occupied_entry) => {
252                            *occupied_entry.get_mut() += 1
253                        }
254                        intmap::Entry::Vacant(vacant_entry) => {
255                            vacant_entry.insert(Fraction::one());
256                        }
257                    }
258                }
259            }
260        }
261        result
262    }
263}
264
265impl IntoTraceIterator for EventLogXes {
266    fn iter_traces(&'_ self) -> TraceIterator<'_> {
267        TraceIterator::Xes(self.into())
268    }
269
270    fn par_iter_traces(&self) -> ParallelTraceIterator<'_> {
271        self.into()
272    }
273}
274
275#[cfg(any(test, feature = "testactivities"))]
276impl TestActivityKey for EventLogXes {
277    fn test_activity_key(&self) {
278        //no activities are stored
279    }
280}