use std::collections::{HashMap, HashSet};
use chrono::{DateTime, Utc};
use ocel::Ocel;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct NoiseSpec {
pub swap: f64,
pub drop: f64,
pub duplicate: f64,
pub seed: u64,
}
struct Rng(u64);
impl Rng {
fn new(seed: u64) -> Self {
Self(seed | 1) }
fn next_u64(&mut self) -> u64 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.0 = x;
x.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
fn chance(&mut self, p: f64) -> bool {
if p <= 0.0 {
return false;
}
if p >= 1.0 {
return true;
}
#[allow(clippy::cast_precision_loss)]
let unit = (self.next_u64() >> 11) as f64 / (1u64 << 53) as f64;
unit < p
}
}
#[must_use]
pub fn inject_noise(log: &Ocel, object_type: &str, spec: &NoiseSpec) -> Ocel {
let mut rng = Rng::new(spec.seed);
let members: HashSet<&str> = log
.objects
.iter()
.filter(|object| object.object_type == object_type)
.map(|object| object.id.as_str())
.collect();
let is_target = |index: usize| {
log.events[index]
.relationships
.iter()
.any(|relation| members.contains(relation.object_id.as_str()))
};
let mut global: Vec<usize> = (0..log.events.len()).collect();
global.sort_unstable_by_key(|&i| (log.events[i].time, i));
let mut dropped: HashSet<usize> = HashSet::new();
for &index in &global {
if is_target(index) && rng.chance(spec.drop) {
dropped.insert(index);
}
}
let mut times: Vec<DateTime<Utc>> = log.events.iter().map(|event| event.time).collect();
let mut position: Vec<usize> = vec![0; log.events.len()];
for (slot, &index) in global.iter().enumerate() {
position[index] = slot;
}
let mut traces: Vec<Vec<usize>> = Vec::new();
{
let mut slot_of: HashMap<&str, usize> = HashMap::new();
for object in &log.objects {
if object.object_type == object_type && !slot_of.contains_key(object.id.as_str()) {
slot_of.insert(object.id.as_str(), traces.len());
traces.push(Vec::new());
}
}
let mut last_event: Vec<usize> = vec![usize::MAX; traces.len()];
for &index in &global {
if dropped.contains(&index) {
continue;
}
for relation in &log.events[index].relationships {
let Some(&slot) = slot_of.get(relation.object_id.as_str()) else {
continue;
};
if last_event[slot] != index {
last_event[slot] = index;
traces[slot].push(index);
}
}
}
}
for trace in &mut traces {
for boundary in 0..trace.len().saturating_sub(1) {
if !rng.chance(spec.swap) {
continue;
}
let (a, b) = (trace[boundary], trace[boundary + 1]);
times.swap(a, b);
global.swap(position[a], position[b]);
position.swap(a, b);
trace.swap(boundary, boundary + 1);
}
}
let mut events = Vec::with_capacity(log.events.len());
let mut dup_count = 0usize;
for &index in &global {
if dropped.contains(&index) {
continue;
}
let mut event = log.events[index].clone();
event.time = times[index];
events.push(event);
if is_target(index) && rng.chance(spec.duplicate) {
dup_count += 1;
let mut copy = events[events.len() - 1].clone();
copy.id = format!("{}~dup{dup_count}", copy.id);
events.push(copy);
}
}
Ocel {
event_types: log.event_types.clone(),
object_types: log.object_types.clone(),
events,
objects: log.objects.clone(),
}
}