roma_lib 0.1.0

A Rust metaheuristics framework inspired by jMetal for optimization and experimentation.
Documentation
use roma::algorithms::{
    Algorithm, HillClimbing, HillClimbingParameters, ImprovementDirection, TerminationCriteria,
    TerminationCriterion,
};
use roma::observer::{AlgorithmEvent, AlgorithmObserver, Observable};
use roma::operator::BitFlipMutation;
use roma::problem::Problem;
use roma::solution::Solution;
use roma::utils::Random;
use std::sync::{Arc, Mutex};

struct CapturePresentationObserver {
    rendered_solutions: Arc<Mutex<Vec<String>>>,
}

impl CapturePresentationObserver {
    fn new(rendered_solutions: Arc<Mutex<Vec<String>>>) -> Self {
        Self { rendered_solutions }
    }
}

impl AlgorithmObserver<bool> for CapturePresentationObserver {
    fn update(&mut self, event: &AlgorithmEvent<bool>) {
        if let AlgorithmEvent::ExecutionStateUpdated { state } = event {
            self.rendered_solutions
                .lock()
                .expect("observer buffer lock should not be poisoned")
                .push(state.best_solution_presentation.clone());
        }
    }

    fn name(&self) -> &str {
        "CapturePresentationObserver"
    }
}

struct FormattedBinaryProblem {
    description: String,
    variables: usize,
}

impl Problem<bool> for FormattedBinaryProblem {
    fn new() -> Self {
        Self {
            description: "Formatted binary problem".to_string(),
            variables: 8,
        }
    }

    fn evaluate(&self, solution: &mut Solution<bool>) {
        let selected = solution.variables().iter().filter(|&&value| value).count() as f64;
        solution.set_quality(selected);
    }

    fn create_solution(&self, rng: &mut Random) -> Solution<bool> {
        let variables: Vec<bool> = (0..self.variables).map(|_| rng.coin_flip()).collect();
        Solution::new(variables)
    }

    fn set_problem_description(&mut self, description: String) {
        self.description = description;
    }

    fn get_problem_description(&self) -> String {
        self.description.clone()
    }

    fn get_improvement_direction(&self) -> ImprovementDirection {
        ImprovementDirection::Maximize
    }

    fn format_solution(&self, solution: &Solution<bool>) -> String {
        let selected = solution.variables().iter().filter(|&&value| value).count();
        let quality = solution.try_quality_value().unwrap_or(0.0);
        format!(
            "selected={}/{}, quality={:.3}",
            selected, self.variables, quality
        )
    }
}

#[test]
fn observer_receives_problem_specific_solution_presentation() {
    let problem = FormattedBinaryProblem::new();
    let parameters = HillClimbingParameters::new(
        BitFlipMutation::new(),
        0.3,
        TerminationCriteria::new(vec![TerminationCriterion::MaxIterations(4)]),
    )
    .with_seed(42);
    let mut algorithm = HillClimbing::new(parameters);

    let rendered_solutions = Arc::new(Mutex::new(Vec::<String>::new()));
    algorithm.add_observer(Box::new(CapturePresentationObserver::new(
        rendered_solutions.clone(),
    )));

    let _ = algorithm
        .run(&problem)
        .expect("hill climbing execution should succeed");

    let rendered = rendered_solutions
        .lock()
        .expect("observer buffer lock should not be poisoned");
    assert!(
        !rendered.is_empty(),
        "observer should receive at least one snapshot"
    );
    assert!(
        rendered.iter().all(|line| line.starts_with("selected=")),
        "all snapshots should use problem-specific rendering"
    );
    assert!(
        rendered.iter().all(|line| line.contains("quality=")),
        "rendered output should include quality"
    );
}