use crate::{
ebi_framework::ebi_command::EbiCommand,
ebi_traits::{
ebi_trait_event_log_trace_attributes::EbiTraitEventLogTraceAttributes,
ebi_trait_finite_stochastic_language::EbiTraitFiniteStochasticLanguage,
},
math::{
average::Average,
distances::{TriangularDistanceMatrix, WeightedDistances},
distances_matrix::WeightedDistanceMatrix,
distances_triangular::WeightedTriangularDistanceMatrix,
},
techniques::sample::{self, Resampler},
};
use ebi_objects::{
Attribute,
anyhow::{Context, Result, anyhow},
ebi_arithmetic::{Fraction, OneMinus},
};
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use std::sync::{
Arc,
atomic::{AtomicUsize, Ordering},
};
pub trait BootstrapTest {
fn bootstrap_test(
&mut self,
other: &mut dyn EbiTraitFiniteStochasticLanguage,
number_of_samples: usize,
alpha: &Fraction,
) -> Result<(Fraction, bool)>;
}
pub trait StatisticalTestsLogCategoricalAttribute {
fn log_categorical_attribute(
&self,
number_of_samples: usize,
trace_attribute: Attribute,
alpha: &Fraction,
) -> Result<(Fraction, bool)>;
}
impl StatisticalTestsLogCategoricalAttribute for dyn EbiTraitEventLogTraceAttributes {
fn log_categorical_attribute(
&self,
number_of_samples: usize,
trace_attribute: Attribute,
alpha: &Fraction,
) -> Result<(Fraction, bool)> {
let traces_with_attributes = self
.get_traces_with_categorical_attribute(trace_attribute)
.into_iter()
.collect::<Vec<_>>();
if traces_with_attributes.is_empty() {
return Err(anyhow!(
"The log does not contain traces with attribute `{}`.",
trace_attribute
));
}
let mut trace_indices = Vec::with_capacity(self.number_of_traces());
let mut attribute_indices = Vec::with_capacity(self.number_of_traces());
for (ti, (_, attributes)) in traces_with_attributes.iter().enumerate() {
for (attribute, cardinality) in attributes.iter() {
for _ in 0..*cardinality {
trace_indices.push(ti);
attribute_indices.push(attribute);
}
}
}
let trace_indices = Arc::new(trace_indices);
let attribute_indices = Arc::new(attribute_indices);
let distances = TriangularDistanceMatrix::new(&traces_with_attributes);
let average_base = Arc::new(Average::new(distances)?);
let err = AtomicUsize::new(0);
log::info!("Perform the test");
let progress_bar = EbiCommand::get_progress_bar_ticks(number_of_samples);
let e: usize = (0..number_of_samples)
.into_iter()
.map(|_| {
let mut average_a = Arc::clone(&average_base).as_ref().clone();
let mut average_r = Arc::clone(&average_base).as_ref().clone();
let trace_indices = Arc::clone(&trace_indices);
let attribute_indices = Arc::clone(&attribute_indices);
let mut sample = vec![0; self.number_of_traces()];
sample::sample_indices_uniform(trace_indices.len(), &mut sample);
for x in 0..sample.len() {
let i = sample[x];
let trace_index_i = trace_indices[i];
let attribute_i = attribute_indices[i];
for y in x + 1..sample.len() {
let j = sample[y];
let trace_index_j = trace_indices[j];
let attribute_j = attribute_indices[j];
if x != y {
average_r.add(trace_index_i, trace_index_j);
if attribute_i == attribute_j {
average_a.add(trace_index_i, trace_index_j);
}
}
}
}
progress_bar.inc(1);
if let Ok(a) = average_a.average() {
let r = average_r.average().unwrap();
if &a < &r {
1
} else {
0
}
} else {
err.fetch_add(1, Ordering::Relaxed);
0
}
})
.sum();
if err.load(Ordering::Relaxed) == number_of_samples {
return Err(anyhow!("All samples were discarded."));
}
let mut p_value = Fraction::from(e);
p_value /= number_of_samples - err.load(Ordering::Relaxed);
p_value = p_value.one_minus();
let reject = &p_value < alpha;
progress_bar.finish_and_clear();
Ok((p_value, !reject))
}
}
impl BootstrapTest for dyn EbiTraitFiniteStochasticLanguage {
fn bootstrap_test(
&mut self,
other: &mut dyn EbiTraitFiniteStochasticLanguage,
number_of_samples: usize,
alpha: &Fraction,
) -> Result<(Fraction, bool)> {
log::info!("Compute the base log-log distance");
let self_other_distances: Box<dyn WeightedDistances> =
Box::new(WeightedDistanceMatrix::new(self, other));
let base_conformance = Arc::new(
self_other_distances
.earth_movers_stochastic_conformance()
.with_context(|| format!("computing base earth movers' stochastic conformance"))?,
);
log::info!("Compute the trace distances");
let self_self_distances = Arc::new(WeightedTriangularDistanceMatrix::new(self));
let resample_cache = Arc::new(self.resample_cache_init()?);
let err = AtomicUsize::new(0);
log::info!("Compute the self log-log distances");
let progress_bar = EbiCommand::get_progress_bar_ticks(number_of_samples);
let e: usize = (0..number_of_samples)
.into_par_iter()
.map(|_| {
let base_conformance = base_conformance.as_ref();
let mut self_self_distances =
WeightedDistances::clone(self_self_distances.as_ref());
let resample_cache = resample_cache.as_ref();
let sample = self.resample(resample_cache, self.number_of_traces());
sample
.into_iter()
.enumerate()
.for_each(|(index_b, weight)| {
*self_self_distances.weight_b_mut(index_b) = weight
});
let sample_conformance = self_self_distances.earth_movers_stochastic_conformance();
progress_bar.inc(1);
if let Ok(d) = sample_conformance {
if &d < &base_conformance { 1 } else { 0 }
} else {
err.fetch_add(1, Ordering::Relaxed);
0
}
})
.sum();
if err.load(Ordering::Relaxed) == number_of_samples {
return Err(anyhow!("All samples were discarded."));
}
let mut p_value = Fraction::from(e);
p_value /= number_of_samples - err.load(Ordering::Relaxed);
let reject = p_value >= alpha.clone().one_minus();
progress_bar.finish_and_clear();
Ok((p_value, !reject))
}
}
#[cfg(test)]
mod tests {
use crate::{
ebi_traits::{
ebi_trait_event_log_trace_attributes::EbiTraitEventLogTraceAttributes,
ebi_trait_finite_stochastic_language::EbiTraitFiniteStochasticLanguage,
},
techniques::bootstrap_test::{BootstrapTest, StatisticalTestsLogCategoricalAttribute},
};
use ebi_objects::{
EventLog, EventLogTraceAttributes, FiniteStochasticLanguage, ebi_arithmetic::Fraction,
};
use std::fs;
#[test]
fn cla_test() {
let fin = fs::read_to_string("testfiles/a-b.xes").unwrap();
let event_log: Box<dyn EbiTraitEventLogTraceAttributes> =
Box::new(fin.parse::<EventLogTraceAttributes>().unwrap());
let attribute = event_log
.attribute_key()
.label_to_attribute("attribute")
.unwrap();
let (_, sustain) = event_log
.log_categorical_attribute(500, attribute, &Fraction::from((1, 20)))
.unwrap();
assert!(sustain) }
#[test]
fn llup_test_single() {
let fin = fs::read_to_string("testfiles/aa.slang").unwrap();
let slpn = fin.parse::<FiniteStochasticLanguage>().unwrap();
let mut slpn2 = slpn.clone();
let mut slpn: Box<dyn EbiTraitFiniteStochasticLanguage> = Box::new(slpn);
let (_, sustain) = slpn
.bootstrap_test(&mut slpn2, 1, &Fraction::from((1, 20)))
.unwrap();
assert!(sustain);
}
#[test]
fn llup_test() {
let fin = fs::read_to_string("testfiles/a-b.xes").unwrap();
let slpn: FiniteStochasticLanguage = fin.parse::<EventLog>().unwrap().into();
let mut slpn2 = slpn.clone();
let mut slpn: Box<dyn EbiTraitFiniteStochasticLanguage> = Box::new(slpn);
let (_, sustain) = slpn
.bootstrap_test(&mut slpn2, 1, &Fraction::from((1, 20)))
.unwrap();
assert!(sustain);
}
}