use crate::errors::{QlError, QlResult};
use crate::math::array::Array;
use crate::math::optimization::endcriteria::{EndCriteria, EndCriteriaType};
use crate::math::optimization::method::OptimizationMethod;
use crate::math::optimization::problem::Problem;
use crate::require;
use itofin_optimize::{
Bounds, Common, Converged, DifferentialEvolutionOptions, Method, Minimize, MinimizeError,
Problem as ScalarProblem, Termination, minimize,
};
#[derive(Debug, Clone)]
pub struct DifferentialEvolution {
bounds: Bounds,
options: DifferentialEvolutionOptions,
common: Common,
last_result: Option<Minimize>,
}
impl DifferentialEvolution {
pub fn new(
bounds: Bounds,
options: DifferentialEvolutionOptions,
common: Common,
) -> QlResult<Self> {
require!(
bounds
.lower
.iter()
.chain(&bounds.upper)
.all(|v| v.is_finite()),
"differential evolution calibration requires finite bounds"
);
let validation = ScalarProblem {
x0: bounds.lower.clone(),
bounds: Some(bounds.clone()),
};
validate(&validation, &options, &common)?;
let common = options
.budgets(&common)
.map_err(|e| ql_error(e.to_string()))?;
Ok(Self {
bounds,
options,
common,
last_result: None,
})
}
pub fn last_result(&self) -> Option<&Minimize> {
self.last_result.as_ref()
}
}
fn ql_error(message: impl Into<String>) -> QlError {
QlError::new(message, file!(), line!())
}
fn validate(
problem: &ScalarProblem,
options: &DifferentialEvolutionOptions,
common: &Common,
) -> QlResult<()> {
problem.validate().map_err(|e| ql_error(e.to_string()))?;
common.validate().map_err(|e| ql_error(e.to_string()))?;
Method::DifferentialEvolution(options.clone())
.validate(problem)
.map_err(|e| ql_error(e.to_string()))?;
options
.budgets(common)
.map_err(|e| ql_error(e.to_string()))?;
Ok(())
}
impl OptimizationMethod for DifferentialEvolution {
fn minimize(
&mut self,
problem: &mut Problem<'_>,
end_criteria: &EndCriteria,
) -> QlResult<EndCriteriaType> {
self.last_result = None;
let initial = problem.current_value().clone();
require!(
initial.size() == self.bounds.lower.len(),
"calibration bounds must match the free parameter count"
);
require!(
problem.constraint().test(&initial),
"initial calibration parameters violate the constraint"
);
let lower = problem.constraint().lower_bound(&initial);
let upper = problem.constraint().upper_bound(&initial);
require!(
lower.size() == initial.size() && upper.size() == initial.size(),
"constraint bounds must match the free parameter count"
);
require!(
lower.iter().chain(upper.iter()).all(|v| !v.is_nan()),
"constraint bounds must not contain NaN"
);
let bounds = Bounds {
lower: self
.bounds
.lower
.iter()
.zip(lower.iter())
.map(|(a, b)| a.max(*b))
.collect(),
upper: self
.bounds
.upper
.iter()
.zip(upper.iter())
.map(|(a, b)| a.min(*b))
.collect(),
};
require!(
bounds
.lower
.iter()
.zip(&bounds.upper)
.all(|(lower, upper)| lower <= upper),
"calibration bounds have an empty constraint intersection"
);
let x0: Vec<f64> = initial
.iter()
.zip(bounds.lower.iter().zip(&bounds.upper))
.map(|(value, (lower, upper))| value.clamp(*lower, *upper))
.collect();
require!(
problem.constraint().test(&Array::from(x0.clone())),
"bounded initial calibration candidate violates the constraint"
);
let scalar = ScalarProblem {
x0,
bounds: Some(bounds),
};
let common = Common {
maxiter: Some(
self.common
.maxiter
.unwrap_or(end_criteria.max_iterations())
.min(end_criteria.max_iterations()),
),
..self.common.clone()
};
validate(&scalar, &self.options, &common)?;
problem.reset();
let mut objective = |coordinates: &[f64]| -> QlResult<f64> {
let candidate = Array::from(coordinates.to_vec());
require!(
problem.constraint().test(&candidate),
"differential evolution candidate violates the calibration constraint; supply a wholly feasible box"
);
Ok(problem.value(&candidate))
};
let result = minimize(
&mut objective,
&scalar,
&Method::DifferentialEvolution(self.options.clone()),
&common,
)
.map_err(|error| match error {
MinimizeError::InvalidInput(error) => ql_error(error.to_string()),
MinimizeError::Objective(error) => error,
})?;
let outcome = match result.status {
Termination::Converged(Converged::XTol) => Ok(EndCriteriaType::StationaryPoint),
Termination::Converged(Converged::FTol) => Ok(EndCriteriaType::StationaryFunctionValue),
Termination::MaxIterations => Ok(EndCriteriaType::MaxIterations),
Termination::MaxEvaluations => Ok(EndCriteriaType::Unknown),
status => Err(ql_error(format!("global calibration stopped: {status}"))),
};
if outcome.is_ok() {
problem.set_current_value(Array::from(result.x.clone()));
problem.set_function_value(result.fun);
}
self.last_result = Some(result);
outcome
}
fn global_result(&self) -> Option<&Minimize> {
self.last_result()
}
}
#[cfg(test)]
mod tests;