Skip to main content

ebi_objects/conversions/
to_stochastic_deterministic_finite_automaton.rs

1use crate::{
2    CompressedEventLog, CompressedEventLogXes, DirectlyFollowsGraph, EventLogCsv,
3    EventLogTraceAttributes, EventLogXes, HasActivityKey, NumberOfTraces,
4    ebi_objects::{
5        compressed_event_log_trace_attributes::CompressedEventLogTraceAttributes,
6        event_log::EventLog, event_log_python::EventLogPython,
7        finite_stochastic_language::FiniteStochasticLanguage,
8        stochastic_deterministic_finite_automaton::StochasticDeterministicFiniteAutomaton,
9    },
10    traits::trace_iterators::{IntoRefTraceIterator, IntoTraceIterator},
11};
12use ebi_arithmetic::{Fraction, One, Signed, Zero};
13use std::collections::{HashMap, hash_map::Entry};
14
15impl From<FiniteStochasticLanguage> for StochasticDeterministicFiniteAutomaton {
16    fn from(value: FiniteStochasticLanguage) -> Self {
17        log::info!("convert finite stochastic language to SDFA");
18
19        let mut result = StochasticDeterministicFiniteAutomaton::new();
20        if value.number_of_traces().is_zero() {
21            result.set_initial_state(None);
22        } else {
23            let mut final_states = HashMap::new();
24            result.activity_key = value.activity_key.clone();
25
26            //create automaton
27            for (trace, probability) in &value.traces {
28                let mut state = result.get_initial_state().unwrap();
29                for activity in trace {
30                    state = result.take_or_add_transition(state, *activity, probability.clone());
31                }
32
33                match final_states.entry(state) {
34                    Entry::Occupied(mut e) => *e.get_mut() += probability,
35                    Entry::Vacant(e) => {
36                        e.insert(probability.clone());
37                    }
38                }
39            }
40
41            //count
42            let mut sums = final_states;
43            for (source, _, _, probability) in &result {
44                match sums.entry(*source) {
45                    Entry::Occupied(mut e) => *e.get_mut() += probability,
46                    Entry::Vacant(e) => {
47                        e.insert(probability.clone());
48                    }
49                }
50            }
51
52            //normalise
53            result.scale_outgoing_probabilities(sums);
54        }
55        result
56    }
57}
58
59impl From<DirectlyFollowsGraph> for StochasticDeterministicFiniteAutomaton {
60    fn from(value: DirectlyFollowsGraph) -> Self {
61        let mut result = Self::new();
62        let initial_state = 0;
63
64        //add states
65        for _ in value.activity_key().get_activities() {
66            result.add_state(); //we do not keep a map, as we can predict the numbers: 0 = initial state, 1..=n = activity states, n+1 = final state.
67        }
68
69        //start activities
70        {
71            let mut sum_of_start_activities = value
72                .start_activities
73                .iter()
74                .map(|(_, f)| f)
75                .sum::<Fraction>();
76            sum_of_start_activities += &value.empty_traces_weight;
77            for (activity, weight) in &value.start_activities {
78                result
79                    .add_transition(
80                        initial_state,
81                        *activity,
82                        activity.id + 1,
83                        weight / &sum_of_start_activities,
84                    )
85                    .unwrap(); //by construction, outgoing probability cannot become lower than 0
86            }
87
88            //emptytraces are implied
89        }
90
91        //edges
92        {
93            for activity in value.activity_key().get_activities() {
94                //gather the outgoing sum
95                let mut sum = Fraction::zero();
96                {
97                    let (_, mut i) =
98                        value.binary_search(*activity, value.activity_key().get_activity_by_id(0));
99                    while i < value.sources.len() && &value.sources[i] == activity {
100                        if value.weights[i].is_positive() {
101                            sum += &value.weights[i];
102                        }
103                        i += 1;
104                    }
105
106                    if let Some(w) = value.end_activities.get(activity)
107                        && w.is_positive()
108                    {
109                        sum += w;
110                    }
111                }
112
113                // add the edges
114                let (_, mut i) =
115                    value.binary_search(*activity, value.activity_key().get_activity_by_id(0));
116                while i < value.sources.len() && &value.sources[i] == activity {
117                    if value.weights[i].is_positive() {
118                        result
119                            .add_transition(
120                                activity.id + 1,
121                                value.targets[i],
122                                value.targets[i].id + 1,
123                                &value.weights[i] / &sum,
124                            )
125                            .unwrap(); //by construction, remaining outgoing probability cannot become negative
126                    }
127                    i += 1;
128                }
129
130                // termination is implied
131            }
132        }
133
134        result.activity_key = value.activity_key;
135
136        result
137    }
138}
139
140macro_rules! log {
141    ($t: ident) => {
142        impl From<$t> for StochasticDeterministicFiniteAutomaton {
143            fn from(value: $t) -> Self {
144                log::info!("convert event log to SDFA");
145
146                let mut result = StochasticDeterministicFiniteAutomaton::new();
147                result.activity_key = value.activity_key().clone();
148
149                if value.number_of_traces().is_zero() {
150                    result.set_initial_state(None);
151                } else {
152                    let mut final_states = HashMap::new();
153
154                    //create automaton
155                    for trace in value.iter_traces() {
156                        let mut state = result.get_initial_state().unwrap();
157
158                        for activity in trace {
159                            state = result.take_or_add_transition(
160                                state,
161                                activity.clone(),
162                                Fraction::one(),
163                            );
164                        }
165
166                        match final_states.entry(state) {
167                            std::collections::hash_map::Entry::Occupied(mut e) => {
168                                *e.get_mut() += Fraction::one()
169                            }
170                            std::collections::hash_map::Entry::Vacant(e) => {
171                                e.insert(Fraction::one());
172                            }
173                        }
174                    }
175
176                    //count
177                    let mut sums = final_states;
178                    for (source, _, _, probability) in &result {
179                        match sums.entry(*source) {
180                            std::collections::hash_map::Entry::Occupied(mut e) => {
181                                *e.get_mut() += probability
182                            }
183                            std::collections::hash_map::Entry::Vacant(e) => {
184                                e.insert(probability.clone());
185                            }
186                        }
187                    }
188
189                    //normalise
190                    result.scale_outgoing_probabilities(sums);
191                }
192
193                result
194            }
195        }
196    };
197}
198
199macro_rules! from_via_log {
200    ($t:ident) => {
201        impl From<$t> for StochasticDeterministicFiniteAutomaton {
202            fn from(value: $t) -> Self {
203                let log: EventLog = value.into();
204                log.into()
205            }
206        }
207    };
208}
209
210log!(EventLog);
211log!(EventLogTraceAttributes);
212log!(EventLogXes);
213log!(EventLogCsv);
214from_via_log!(EventLogPython);
215from_via_log!(CompressedEventLogXes);
216from_via_log!(CompressedEventLog);
217from_via_log!(CompressedEventLogTraceAttributes);
218
219#[cfg(test)]
220mod tests {
221    use ebi_arithmetic::{Fraction, Zero, f0};
222
223    use crate::{
224        DirectlyFollowsGraph, FiniteStochasticLanguage, HasActivityKey, NumberOfTraces,
225        StochasticDeterministicFiniteAutomaton,
226    };
227    use std::fs;
228
229    #[test]
230    fn slang_minprob_zero_through_sdfa() {
231        let fin = fs::read_to_string("testfiles/aa-ab-ba.slang").unwrap();
232        let slang = fin.parse::<FiniteStochasticLanguage>().unwrap();
233        assert_eq!(slang.number_of_traces(), 3);
234        let sdfa: StochasticDeterministicFiniteAutomaton = slang.into();
235        assert_eq!(sdfa.number_of_states(), 6);
236        assert_eq!(sdfa.number_of_transitions(), 5);
237    }
238
239    #[test]
240    fn dfg_to_sdfa() {
241        let fin1 = fs::read_to_string("testfiles/aa-ab-ba.dfg").unwrap();
242        let mut dfg: DirectlyFollowsGraph = fin1.parse::<DirectlyFollowsGraph>().unwrap();
243
244        let a = dfg.activity_key_mut().process_activity("a");
245        let b = dfg.activity_key_mut().process_activity("b");
246        assert_eq!(dfg.start_activities[&a], Fraction::from((2, 5)));
247        assert_eq!(dfg.start_activities[&b], Fraction::from((3, 5)));
248
249        let snfa: StochasticDeterministicFiniteAutomaton = dfg.into();
250        assert_eq!(snfa.number_of_states(), 3);
251        assert_eq!(snfa.number_of_transitions(), 5);
252        assert_eq!(snfa.sources, [0, 0, 1, 1, 2]);
253        assert_eq!(snfa.targets, [1, 2, 1, 2, 1]);
254        assert_eq!(
255            snfa.probabilities,
256            [
257                Fraction::from((2, 5)),
258                Fraction::from((3, 5)),
259                Fraction::from((1, 6)),
260                Fraction::from((1, 6)),
261                Fraction::from((3, 4))
262            ]
263        );
264        assert_eq!(
265            snfa.terminating_probabilities,
266            [f0!(), Fraction::from((2, 3)), Fraction::from((1, 4))]
267        );
268    }
269
270    #[test]
271    fn log_to_sdfa() {
272        let fin1 = fs::read_to_string("testfiles/acb-abc-ad-aded-adeded-adededed.slang").unwrap();
273        let slang = fin1.parse::<FiniteStochasticLanguage>().unwrap();
274
275        let sdfa = StochasticDeterministicFiniteAutomaton::from(slang);
276
277        assert!(
278            sdfa.terminating_probabilities
279                .contains(&Fraction::from((8, 15)))
280        );
281        assert!(
282            sdfa.terminating_probabilities
283                .contains(&Fraction::from((2, 3)))
284        );
285        assert!(
286            sdfa.terminating_probabilities
287                .contains(&Fraction::from((4, 7)))
288        );
289    }
290}