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 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 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 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 }
280}