roma_lib 0.1.1

A Rust metaheuristics framework inspired by jMetal for optimization and experimentation.
Documentation
//! Core solution abstractions and builders.
//!
//! This module provides a generic `Solution<T, Q>` abstraction and
//! convenience builders for common variable types.

pub(crate) mod implementations;
pub(crate) mod traits;

use std::fmt::Display;
use std::str::FromStr;

pub use implementations::{
    binary_solution::BinarySolutionBuilder,
    pareto_crowding_solution::{
        MultiObjectiveRealSolutionBuilder, MultiObjectiveVectorRealSolutionBuilder,
    },
    permutation_solution::PermutationSolutionBuilder,
    real_solution::RealSolutionBuilder,
    string_solution::StringSolutionBuilder,
};
pub use traits::ParetoCrowdingDistanceQuality;

/// Generic optimization solution.
///
/// # Type Parameters
/// - `T`: variable type.
/// - `Q`: quality payload type (defaults to `f64`).
///
/// # Notes
/// This type is intentionally generic over `Q` so algorithms can decide how to
/// store fitness/quality information:
/// - single-objective: `f64` (default)
/// - multi-objective with rank/crowding metadata: `ParetoCrowdingDistanceQuality`
/// - custom metadata-rich payloads
#[derive(Clone, Debug)]
pub struct Solution<T, Q = f64> {
    variables: Vec<T>,
    real_lower_bounds: Option<Vec<f64>>,
    real_upper_bounds: Option<Vec<f64>>,
    /// Optional cached quality payload.
    ///
    /// This value is expected to be updated by problem evaluation and invalidated
    /// whenever variables change.
    /// For scalar optimization this is usually `f64`.
    /// For vector-based multi-objective optimization this can be `Vec<f64>`.
    /// For metadata-rich workflows this can be a custom type.
    value: Option<Q>,
}

impl<T: Display, Q: Display> Solution<T, Q> {
    /// Creates a solution with variables and no quality assigned.
    pub fn new(variables: Vec<T>) -> Self {
        Self {
            variables,
            real_lower_bounds: None,
            real_upper_bounds: None,
            value: None,
        }
    }

    /// Returns an immutable view of decision variables.
    pub fn variables(&self) -> &[T] {
        &self.variables
    }

    /// Returns a mutable view of decision variables.
    ///
    /// # Cache invalidation
    /// Calling this method invalidates cached quality immediately.
    /// Any subsequent mutation through the returned slice means this solution
    /// is no longer considered evaluated and must be re-evaluated.
    pub fn variables_mut(&mut self) -> &mut [T] {
        self.invalidate();
        &mut self.variables
    }

    /// Replaces all decision variables and invalidates quality cache.
    ///
    /// After this call, quality is cleared because decision data changed.
    /// The solution must be re-evaluated.
    pub fn set_variables(&mut self, variables: Vec<T>) {
        if self
            .real_lower_bounds
            .as_ref()
            .is_some_and(|bounds| bounds.len() != variables.len())
            || self
                .real_upper_bounds
                .as_ref()
                .is_some_and(|bounds| bounds.len() != variables.len())
        {
            self.real_lower_bounds = None;
            self.real_upper_bounds = None;
        }
        self.variables = variables;
        self.invalidate();
    }

    /// Returns the number of decision variables.
    pub fn num_variables(&self) -> usize {
        self.variables.len()
    }

    /// Clone helper kept for compatibility with legacy algorithm code.
    pub fn copy(&self) -> Self
    where
        T: Clone,
        Q: Clone,
    {
        self.clone()
    }

    pub fn get_variable(&self, index: usize) -> Option<&T> {
        self.variables.get(index)
    }

    /// Returns a mutable reference to one decision variable by index.
    ///
    /// # Cache invalidation
    /// Calling this method invalidates cached quality immediately,
    /// even if `index` is out of bounds and the returned value is `None`.
    /// This keeps a strict invariant: mutable variable access implies stale
    /// quality cache and requires re-evaluation.
    pub fn get_variable_mut(&mut self, index: usize) -> Option<&mut T> {
        self.invalidate();
        self.variables.get_mut(index)
    }

    /// Replaces one variable and invalidates quality cache.
    ///
    /// Returns `true` when index is valid.
    ///
    /// When this returns `true`, decision data changed and quality is cleared.
    /// The solution must be re-evaluated.
    pub fn set_variable(&mut self, index: usize, value: T) -> bool {
        if let Some(variable) = self.variables.get_mut(index) {
            *variable = value;
            self.invalidate();
            true
        } else {
            false
        }
    }

    /// Swaps two variables and invalidates quality cache.
    ///
    /// Returns `true` when both indexes are valid.
    ///
    /// When this returns `true`, decision data changed and quality is cleared.
    /// The solution must be re-evaluated.
    pub fn swap_variables(&mut self, i: usize, j: usize) -> bool {
        if i < self.variables.len() && j < self.variables.len() {
            self.variables.swap(i, j);
            self.invalidate();
            true
        } else {
            false
        }
    }

    /// Returns quality payload if present.
    pub fn quality(&self) -> Option<&Q> {
        self.value.as_ref()
    }

    /// Returns mutable quality payload if present.
    pub fn quality_mut(&mut self) -> Option<&mut Q> {
        self.value.as_mut()
    }

    /// Replaces quality payload.
    pub fn set_quality(&mut self, quality: Q) {
        self.value = Some(quality);
    }

    /// Returns true when quality payload is present.
    pub fn has_quality(&self) -> bool {
        self.value.is_some()
    }

    /// Invalidates the quality cache.
    ///
    /// Use this when decision variables are changed through external logic.
    /// After invalidation, the solution has no valid quality and must be
    /// re-evaluated by the problem.
    pub fn invalidate(&mut self) {
        self.value = None;
    }

    pub fn encode(&self) -> String {
        let quality_string = match &self.value {
            Some(q) => q.to_string(),
            None => "None".to_string(),
        };

        let genes: String = self
            .variables
            .iter()
            .map(|v| v.to_string())
            .collect::<Vec<_>>()
            .join(",");

        format!("{}|{}", genes, quality_string)
    }

    pub fn decode(data: &str) -> Result<Self, String>
    where
        T: FromStr,
        Q: FromStr,
    {
        let parts: Vec<&str> = data.split('|').collect();
        if parts.len() != 2 {
            return Err("Invalid format: 'genes|fitness' expected".to_string());
        }

        let variables: Vec<T> = parts[0]
            .split(',')
            .filter(|s| !s.is_empty())
            .map(|s| {
                s.parse::<T>()
                    .map_err(|_| "Error parsing variable (T)".to_string())
            })
            .collect::<Result<Vec<T>, String>>()?;

        let quality = if parts[1] == "None" || parts[1].is_empty() {
            None
        } else {
            Some(
                parts[1]
                    .parse::<Q>()
                    .map_err(|_| "Error parsing quality (Q)".to_string())?,
            )
        };

        Ok(Self {
            variables,
            real_lower_bounds: None,
            real_upper_bounds: None,
            value: quality,
        })
    }
}

impl<Q> Solution<f64, Q> {
    /// Returns the per-variable lower bounds when available.
    pub fn lower_bounds(&self) -> Option<&[f64]> {
        self.real_lower_bounds.as_deref()
    }

    /// Returns the per-variable upper bounds when available.
    pub fn upper_bounds(&self) -> Option<&[f64]> {
        self.real_upper_bounds.as_deref()
    }

    /// Returns the lower and upper bound for one variable when available.
    pub fn bounds_at(&self, index: usize) -> Option<(f64, f64)> {
        let lower = self.real_lower_bounds.as_ref()?.get(index).copied()?;
        let upper = self.real_upper_bounds.as_ref()?.get(index).copied()?;
        Some((lower, upper))
    }

    /// Sets per-variable bounds for real-valued solutions.
    pub fn set_bounds(&mut self, lower_bounds: Vec<f64>, upper_bounds: Vec<f64>) {
        debug_assert_eq!(
            lower_bounds.len(),
            self.variables.len(),
            "lower_bounds length should match variables length"
        );
        debug_assert_eq!(
            upper_bounds.len(),
            self.variables.len(),
            "upper_bounds length should match variables length"
        );
        self.real_lower_bounds = Some(lower_bounds);
        self.real_upper_bounds = Some(upper_bounds);
    }

    /// Sets uniform bounds for all variables of a real-valued solution.
    pub fn set_uniform_bounds(&mut self, lower: f64, upper: f64) {
        let size = self.variables.len();
        self.set_bounds(vec![lower; size], vec![upper; size]);
    }
}

impl<T> Solution<T, f64> {
    /// Returns the scalar quality value if present.
    pub fn try_quality_value(&self) -> Option<f64> {
        self.value
    }

    /// Returns the scalar quality value.
    ///
    /// # Panics
    /// Panics when the quality cache is missing.
    ///
    /// In optimization hot paths, silently defaulting quality can hide invalid
    /// states (non-evaluated solutions participating in selection/ranking).
    /// Use [`try_quality_value`](Self::try_quality_value) when absence is expected.
    pub fn quality_value(&self) -> f64 {
        self.value
            .expect("quality_value() called on a solution without evaluated quality")
    }

    pub fn has_bigger_quality(&self, other: &Self) -> bool {
        match (self.value, other.value) {
            (Some(a), Some(b)) => a > b,
            _ => false,
        }
    }

}

fn finalize_scalar_solution<T: Display>(variables: Vec<T>, quality: Option<f64>) -> Solution<T> {
    let mut solution = Solution::new(variables);
    if let Some(quality) = quality {
        solution.set_quality(quality);
    }
    solution
}

fn apply_bounds(
    mut variables: Vec<f64>,
    lower_bounds: &Option<Vec<f64>>,
    upper_bounds: &Option<Vec<f64>>,
) -> Vec<f64> {
    if let (Some(lower), Some(upper)) = (lower_bounds, upper_bounds) {
        debug_assert_eq!(
            lower.len(),
            variables.len(),
            "lower_bounds length should match variables length"
        );
        debug_assert_eq!(
            upper.len(),
            variables.len(),
            "upper_bounds length should match variables length"
        );

        for ((value, &lo), &up) in variables.iter_mut().zip(lower.iter()).zip(upper.iter()) {
            *value = value.clamp(lo, up);
        }
    }

    variables
}

#[cfg(test)]
mod tests {
    use super::Solution;

    #[test]
    fn quality_helpers_work_as_expected() {
        let mut s: Solution<i32> = Solution::new(vec![1, 2, 3]);
        assert!(!s.has_quality());
        assert_eq!(s.quality(), None);

        s.set_quality(4.0);
        assert!(s.has_quality());
        assert_eq!(s.quality().copied(), Some(4.0));

        s.invalidate();
        assert!(!s.has_quality());
        assert_eq!(s.quality(), None);
    }

    #[test]
    fn invalidate_clears_quality() {
        let mut s: Solution<bool> = Solution::new(vec![true, false]);
        s.set_quality(10.0);
        assert_eq!(s.quality().copied(), Some(10.0));
        s.invalidate();
        assert_eq!(s.quality(), None);
    }

    #[test]
    fn try_quality_value_reflects_presence() {
        let mut s: Solution<i32> = Solution::new(vec![1, 2, 3]);
        assert_eq!(s.try_quality_value(), None);

        s.set_quality(1.25);
        assert_eq!(s.try_quality_value(), Some(1.25));
    }

    #[test]
    #[should_panic(expected = "quality_value() called on a solution without evaluated quality")]
    fn quality_value_panics_when_missing() {
        let s: Solution<i32> = Solution::new(vec![1, 2, 3]);
        let _ = s.quality_value();
    }
}