use crate::numerical::Nonlinear_systems::error::SolveError;
use nalgebra::{DMatrix, DVector};
#[derive(Debug, Clone, PartialEq)]
pub struct Bounds {
limits: Vec<(f64, f64)>,
}
impl Bounds {
pub fn new(limits: Vec<(f64, f64)>) -> Result<Self, SolveError> {
if limits.is_empty() {
return Err(SolveError::InvalidConfig(
"bounds must not be empty".to_string(),
));
}
for (index, (lower, upper)) in limits.iter().copied().enumerate() {
if lower > upper {
return Err(SolveError::InvalidConfig(format!(
"invalid bounds at index {index}: lower bound {lower} exceeds upper bound {upper}"
)));
}
}
Ok(Self { limits })
}
pub fn from_optional(limits: Option<Vec<(f64, f64)>>) -> Result<Option<Self>, SolveError> {
limits.map(Self::new).transpose()
}
pub fn len(&self) -> usize {
self.limits.len()
}
pub fn is_empty(&self) -> bool {
self.limits.is_empty()
}
pub fn as_slice(&self) -> &[(f64, f64)] {
&self.limits
}
pub fn validate(&self, x: &DVector<f64>) -> Result<(), SolveError> {
if x.len() != self.len() {
return Err(SolveError::DimensionMismatch {
expected: self.len(),
actual: x.len(),
context: "bounds validation",
});
}
for (index, value) in x.iter().copied().enumerate() {
let (lower, upper) = self.limits[index];
if value < lower || value > upper {
return Err(SolveError::InfeasibleInitialGuess {
index,
value,
lower,
upper,
});
}
}
Ok(())
}
pub fn project_in_place(&self, x: &mut DVector<f64>) {
let limit = x.len().min(self.limits.len());
for index in 0..limit {
let (lower, upper) = self.limits[index];
x[index] = x[index].clamp(lower, upper);
}
}
pub fn project(&self, x: &DVector<f64>) -> DVector<f64> {
let mut projected = x.clone();
self.project_in_place(&mut projected);
projected
}
pub fn max_step_scale(&self, x: &DVector<f64>, step: &DVector<f64>) -> f64 {
let mut scale: f64 = 1.0;
let limit = x.len().min(step.len()).min(self.limits.len());
for index in 0..limit {
let value = x[index];
let delta = step[index];
let (lower, upper) = self.limits[index];
if delta > 0.0 {
scale = scale.min((upper - value) / delta);
} else if delta < 0.0 {
scale = scale.min((lower - value) / delta);
}
}
if scale.is_finite() { scale } else { 1.0 }
}
}
pub trait NonlinearProblem {
fn dimension(&self) -> usize;
fn residual(&self, x: &DVector<f64>) -> Result<DVector<f64>, SolveError>;
fn supports_residual_into(&self) -> bool {
false
}
fn residual_into(&self, x: &DVector<f64>, out: &mut DVector<f64>) -> Result<(), SolveError> {
let residual = self.residual(x)?;
if residual.len() != out.len() {
return Err(SolveError::DimensionMismatch {
expected: self.dimension(),
actual: residual.len(),
context: "residual evaluation",
});
}
out.copy_from(&residual);
Ok(())
}
}
pub trait JacobianProvider: NonlinearProblem {
fn jacobian(&self, x: &DVector<f64>) -> Result<DMatrix<f64>, SolveError>;
fn supports_jacobian_into(&self) -> bool {
false
}
fn jacobian_into(&self, x: &DVector<f64>, out: &mut DMatrix<f64>) -> Result<(), SolveError> {
let jacobian = self.jacobian(x)?;
if jacobian.nrows() != out.nrows() || jacobian.ncols() != out.ncols() {
return Err(SolveError::InvalidConfig(format!(
"Jacobian evaluation returned {}x{}, expected {}x{}",
jacobian.nrows(),
jacobian.ncols(),
out.nrows(),
out.ncols()
)));
}
out.copy_from(&jacobian);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn project_in_place_matches_owned_projection() {
let bounds = Bounds::new(vec![(0.0, 1.0), (-2.0, 2.0), (10.0, 20.0)]).expect("bounds");
let input = DVector::from_vec(vec![-3.0, 0.5, 25.0]);
let expected = bounds.project(&input);
let mut actual = input;
bounds.project_in_place(&mut actual);
assert_eq!(actual, expected);
assert_eq!(actual.as_slice(), &[0.0, 0.5, 20.0]);
}
#[test]
fn method_workspace_reuses_affine_trial_storage() {
let base = DVector::from_vec(vec![1.0, 2.0, 3.0]);
let direction = DVector::from_vec(vec![0.5, -1.0, 2.0]);
let mut workspace = crate::numerical::Nonlinear_systems::engine::MethodWorkspace::new(3);
let initial_pointer = workspace.trial_x().as_slice().as_ptr();
workspace
.set_affine_trial(&base, -2.0, &direction)
.expect("matching dimensions");
assert_eq!(workspace.trial_x().as_slice(), &[0.0, 4.0, -1.0]);
assert_eq!(workspace.trial_x().as_slice().as_ptr(), initial_pointer);
}
}