#![allow(clippy::all, dead_code)]
use std::collections::{BTreeMap, HashMap};
use std::fs;
use wasm4pm::ml::automl::{discover_automl_classify_internal, discover_automl_forecast_internal};
use wasm4pm::ml::classification::{extract_features, knn_sweep_cv};
use wasm4pm::ml::forecasting::forecast_internal;
use wasm4pm::ml::pca::pca_internal;
use wasm4pm::ml::regression::regression_internal;
use wasm4pm::models::{AttributeValue, Event, EventLog, Trace};
fn parse_xes(content: &str) -> EventLog {
let mut log = EventLog::new();
let mut current_trace: Option<Trace> = None;
let mut current_event: Option<Event> = None;
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("<trace>") || trimmed.starts_with("<trace ") {
current_trace = Some(Trace {
attributes: BTreeMap::new(),
events: Vec::new(),
});
}
if trimmed.starts_with("</trace>") {
if let Some(t) = current_trace.take() {
log.traces.push(t);
}
}
if trimmed.starts_with("<event>") || trimmed.starts_with("<event ") {
current_event = Some(Event {
attributes: BTreeMap::new(),
});
}
if trimmed.starts_with("</event>") {
if let Some(ev) = current_event.take() {
if let Some(ref mut t) = current_trace {
t.events.push(ev);
}
}
}
if trimmed.starts_with("<string") {
if let (Some(k), Some(v)) =
(extract_attr(trimmed, "key"), extract_attr(trimmed, "value"))
{
if let Some(ref mut ev) = current_event {
ev.attributes.insert(k, AttributeValue::String(v));
} else if let Some(ref mut t) = current_trace {
t.attributes.insert(k, AttributeValue::String(v));
}
}
}
if trimmed.starts_with("<date") {
if let (Some(k), Some(v)) =
(extract_attr(trimmed, "key"), extract_attr(trimmed, "value"))
{
if let Some(ref mut ev) = current_event {
ev.attributes.insert(k, AttributeValue::Date(v));
}
}
}
}
log
}
fn extract_attr(s: &str, attr: &str) -> Option<String> {
let needle = format!("{}=\"", attr);
let start = s.find(&needle)? + needle.len();
let end = s[start..].find('"')?;
Some(s[start..start + end].to_string())
}
fn load_xes(candidates: &[&str]) -> Option<EventLog> {
for path in candidates {
if let Ok(content) = fs::read_to_string(path) {
if content.len() > 200 {
let log = parse_xes(&content);
if !log.traces.is_empty() {
eprintln!("ML tests: loaded {} traces from {}", log.traces.len(), path);
return Some(log);
}
}
}
}
None
}
const ROADTRAFFIC: &[&str] = &[
"/Users/sac/chatmangpt/pm4py/tests/input_data/roadtraffic100traces.xes",
"tests/fixtures/roadtraffic100traces.xes",
];
macro_rules! require_log {
($paths:expr, $label:expr) => {
match load_xes($paths) {
None => {
eprintln!("SKIP: {} not found", $label);
return;
}
Some(l) => l,
}
};
}
#[test]
fn pca_roadtraffic_eigenvalues_are_positive() {
let log = require_log!(ROADTRAFFIC, "roadtraffic");
let (features, _) = extract_features(&log, "concept:name");
assert!(features.len() >= 2, "Need at least 2 traces for PCA");
let result = pca_internal(&features);
assert!(
result.eigenvalues[0] >= 0.0,
"First eigenvalue must be non-negative, got {}",
result.eigenvalues[0]
);
assert!(
result.eigenvalues[1] >= 0.0,
"Second eigenvalue must be non-negative, got {}",
result.eigenvalues[1]
);
assert!(
result.explained_variance[0] >= 0.0 && result.explained_variance[0] <= 1.0,
"First explained variance must be in [0,1], got {}",
result.explained_variance[0]
);
}
#[test]
fn pca_roadtraffic_first_component_dominates() {
let log = require_log!(ROADTRAFFIC, "roadtraffic");
let (features, _) = extract_features(&log, "concept:name");
let result = pca_internal(&features);
assert!(
result.eigenvalues[0] >= result.eigenvalues[1],
"First eigenvalue ({}) must be >= second ({})",
result.eigenvalues[0],
result.eigenvalues[1]
);
}
#[test]
fn regression_roadtraffic_trace_length_vs_event_count_non_degenerate() {
let log = require_log!(ROADTRAFFIC, "roadtraffic");
let x: Vec<f64> = (0..log.traces.len()).map(|i| i as f64).collect();
let y: Vec<f64> = log.traces.iter().map(|t| t.events.len() as f64).collect();
let result = regression_internal(&x, &y);
assert!(
result.r_squared >= 0.0 && result.r_squared <= 1.0,
"R² must be in [0,1], got {}",
result.r_squared
);
assert!(
result.intercept > 0.0,
"Intercept must be positive (avg case length > 0), got {}",
result.intercept
);
}
#[test]
fn regression_roadtraffic_unique_activities_vs_case_length_correlated() {
let log = require_log!(ROADTRAFFIC, "roadtraffic");
let (features, _) = extract_features(&log, "concept:name");
let x: Vec<f64> = features.iter().map(|f| f[0]).collect();
let y: Vec<f64> = features.iter().map(|f| f[1]).collect();
let result = regression_internal(&x, &y);
assert!(
result.slope >= 0.0,
"Slope of (trace_length → unique_activities) must be non-negative, got {}",
result.slope
);
}
#[test]
fn forecast_roadtraffic_case_lengths_ewma_non_degenerate() {
let log = require_log!(ROADTRAFFIC, "roadtraffic");
let series: Vec<f64> = log.traces.iter().map(|t| t.events.len() as f64).collect();
assert!(series.len() >= 5, "Need at least 5 traces for forecast");
let result = forecast_internal(&series, 0.3);
assert!(
result.rmse >= 0.0,
"RMSE must be non-negative, got {}",
result.rmse
);
assert!(
result.next_window > 0.0,
"Forecast next_window must be positive (trace lengths > 0), got {}",
result.next_window
);
}
#[test]
fn automl_forecast_roadtraffic_selects_best_alpha() {
let log = require_log!(ROADTRAFFIC, "roadtraffic");
let windows: Vec<f64> = log.traces.iter().map(|t| t.events.len() as f64).collect();
if windows.len() < 25 {
eprintln!("SKIP: not enough windows for automl forecast CV");
return;
}
let result = discover_automl_forecast_internal(&windows);
assert!(
result.best_alpha > 0.0 && result.best_alpha < 1.0,
"best_alpha must be in (0,1), got {}",
result.best_alpha
);
assert!(
result.min_avg_rmse >= 0.0,
"min_avg_rmse must be non-negative, got {}",
result.min_avg_rmse
);
}
#[test]
fn automl_classify_roadtraffic_selects_best_k() {
let log = require_log!(ROADTRAFFIC, "roadtraffic");
let (features, labels) = extract_features(&log, "concept:name");
assert!(
!features.is_empty(),
"extract_features must return non-empty features"
);
let result = discover_automl_classify_internal(&features, &labels);
assert!(
result.best_k >= 1,
"best_k must be at least 1, got {}",
result.best_k
);
assert!(
result.max_avg_accuracy >= 0.0 && result.max_avg_accuracy <= 1.0,
"max_avg_accuracy must be in [0,1], got {}",
result.max_avg_accuracy
);
}
#[test]
fn knn_sweep_cv_roadtraffic_returns_non_degenerate_accuracy() {
let log = require_log!(ROADTRAFFIC, "roadtraffic");
let (features, labels) = extract_features(&log, "concept:name");
if features.len() < 5 {
eprintln!("SKIP: not enough traces for knn_sweep_cv");
return;
}
let accuracies = knn_sweep_cv(&features, &labels, 5, 5);
assert!(
!accuracies.is_empty(),
"knn_sweep_cv must return non-empty accuracy vector"
);
for (k, &acc) in accuracies.iter().enumerate() {
assert!(
acc >= 0.0 && acc <= 1.0,
"knn_sweep_cv accuracy at k={} must be in [0,1], got {}",
k,
acc
);
}
}
#[test]
fn extract_features_roadtraffic_produces_correct_count() {
let log = require_log!(ROADTRAFFIC, "roadtraffic");
let (features, labels) = extract_features(&log, "concept:name");
assert_eq!(
features.len(),
log.traces.len(),
"extract_features must produce one feature vector per trace"
);
assert_eq!(
labels.len(),
log.traces.len(),
"extract_features must produce one label per trace"
);
for (i, f) in features.iter().enumerate() {
assert!(
f[0] > 0.0,
"Trace {} has feature[0]=trace_length={}, must be > 0",
i,
f[0]
);
assert!(
f[1] > 0.0,
"Trace {} has feature[1]=unique_activities={}, must be > 0",
i,
f[1]
);
}
}