use crate::pipeline::catalog::{breed_category, BreedCategory};
use std::collections::HashSet;
pub trait BreedFitnessEvaluator: Send + Sync {
fn evaluate(&self, breeds: &[String]) -> f64;
}
pub struct SubprocessFitnessEvaluator {
pub ocel_path: Option<String>,
pub wasm4pm_cli: String,
}
impl SubprocessFitnessEvaluator {
pub fn new(ocel_path: Option<String>) -> Self {
let cli = which_wasm4pm_cli();
Self {
ocel_path,
wasm4pm_cli: cli,
}
}
fn run_breed(&self, breed: &str) -> Option<f64> {
let mut cmd = std::process::Command::new(&self.wasm4pm_cli);
cmd.arg("breed").arg("run").arg(breed).arg("--score-only");
if let Some(ref path) = self.ocel_path {
cmd.arg("--ocel").arg(path);
}
let out = cmd.output().ok()?;
if !out.status.success() {
return None;
}
let stdout = std::str::from_utf8(&out.stdout).ok()?;
let val: serde_json::Value = serde_json::from_str(stdout.trim()).ok()?;
val.get("fitness")?.as_f64()
}
}
impl std::fmt::Debug for SubprocessFitnessEvaluator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SubprocessFitnessEvaluator")
.field("ocel_path", &self.ocel_path)
.field("wasm4pm_cli", &self.wasm4pm_cli)
.finish()
}
}
impl BreedFitnessEvaluator for SubprocessFitnessEvaluator {
fn evaluate(&self, breeds: &[String]) -> f64 {
if breeds.is_empty() {
return 0.0;
}
let scores: Vec<f64> = breeds.iter().filter_map(|b| self.run_breed(b)).collect();
if scores.is_empty() {
return HeuristicFitnessEvaluator.evaluate(breeds);
}
scores.iter().sum::<f64>() / scores.len() as f64
}
}
#[derive(Debug)]
pub struct HeuristicFitnessEvaluator;
fn category_for(breed: &str) -> &'static str {
match breed_category(breed) {
BreedCategory::LogicBased => "logic",
BreedCategory::RuleBased => "rule",
BreedCategory::PlanningBased => "planning",
BreedCategory::Probabilistic => "probabilistic",
BreedCategory::Temporal => "temporal",
BreedCategory::MemoryBased => "memory",
BreedCategory::MetaBased => "meta",
}
}
impl BreedFitnessEvaluator for HeuristicFitnessEvaluator {
fn evaluate(&self, breeds: &[String]) -> f64 {
if breeds.is_empty() {
return 0.0;
}
let categories: HashSet<&str> = breeds.iter().map(|b| category_for(b.as_str())).collect();
let diversity = (categories.len() as f64 / 7.0_f64).min(1.0);
let length_score = match breeds.len() {
0 => 0.0,
1 => 0.3,
2..=4 => 1.0,
n => (4.0 / n as f64).min(1.0),
};
let has_temporal = breeds.iter().any(|b| category_for(b) == "temporal");
let temporal_bonus = if has_temporal { 0.1 } else { 0.0 };
(diversity * 0.5 + length_score * 0.4 + temporal_bonus).min(1.0)
}
}
fn which_wasm4pm_cli() -> String {
for candidate in &[
"wasm4pm",
"../wasm4pm/target/debug/wasm4pm-cli",
"../wasm4pm/target/release/wasm4pm-cli",
] {
if std::process::Command::new(candidate)
.arg("--version")
.output()
.is_ok()
{
return (*candidate).to_string();
}
}
"wasm4pm".to_string()
}
pub fn auto_evaluator(ocel_path: Option<String>) -> Box<dyn BreedFitnessEvaluator> {
let cli = which_wasm4pm_cli();
if std::process::Command::new(&cli)
.arg("--version")
.output()
.is_ok()
{
return Box::new(SubprocessFitnessEvaluator {
ocel_path,
wasm4pm_cli: cli,
});
}
if let Some(path) = ocel_path.as_deref() {
if let Some(log) = crate::pipeline::ocel::read_ocel_log(path) {
if !log.events.is_empty() {
return Box::new(crate::pipeline::ocel::LogGroundedFitnessEvaluator {
profile: crate::pipeline::ocel::LogProfile::from_log(&log),
});
}
}
}
Box::new(HeuristicFitnessEvaluator)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn heuristic_empty_breeds_is_zero() {
assert_eq!(HeuristicFitnessEvaluator.evaluate(&[]), 0.0);
}
#[test]
fn heuristic_single_breed_is_low() {
let score = HeuristicFitnessEvaluator.evaluate(&["cbr".to_string()]);
assert!(
score > 0.0 && score < 0.5,
"single breed score {} should be low",
score
);
}
#[test]
fn heuristic_diverse_pipeline_scores_higher_than_homogeneous() {
let diverse = vec![
"cbr".to_string(),
"ltl_monitor".to_string(),
"asp".to_string(),
];
let homogeneous = vec!["cbr".to_string(), "cbr".to_string(), "cbr".to_string()];
let diverse_score = HeuristicFitnessEvaluator.evaluate(&diverse);
let homo_score = HeuristicFitnessEvaluator.evaluate(&homogeneous);
assert!(
diverse_score > homo_score,
"diverse ({}) should score higher than homogeneous ({})",
diverse_score,
homo_score
);
}
#[test]
fn heuristic_temporal_breed_gets_bonus() {
let with_temporal = vec!["cbr".to_string(), "ltl_monitor".to_string()];
let without_temporal = vec!["cbr".to_string(), "production_rules".to_string()];
let with_score = HeuristicFitnessEvaluator.evaluate(&with_temporal);
let without_score = HeuristicFitnessEvaluator.evaluate(&without_temporal);
assert!(
with_score >= without_score,
"temporal bonus should not reduce score ({} vs {})",
with_score,
without_score
);
}
#[test]
fn heuristic_optimal_length_scores_above_threshold() {
let optimal = vec![
"cbr".to_string(),
"ltl_monitor".to_string(),
"asp".to_string(),
];
let score = HeuristicFitnessEvaluator.evaluate(&optimal);
assert!(
score >= 0.5,
"optimal length pipeline should score >= 0.5, got {}",
score
);
}
#[test]
fn category_for_known_breeds() {
assert_eq!(category_for("ltl_monitor"), "temporal");
assert_eq!(category_for("asp"), "logic");
assert_eq!(category_for("cbr"), "rule");
assert_eq!(category_for("bayesian_network"), "probabilistic");
assert_eq!(category_for("frame"), "memory");
assert_eq!(category_for("production_rules"), "rule");
}
#[test]
fn category_for_markov_logic_is_rule_not_logic() {
assert_eq!(category_for("markov_logic"), "rule");
}
}