use std::collections::HashSet;
pub use crate::analysis::FreedomAnalysis;
use crate::analysis::{Analysis, NoAnalysis, SolveOutcomeAnalysis};
pub use crate::constraint_request::ConstraintRequest;
pub use crate::constraints::Constraint;
use crate::constraints::ConstraintEntry;
pub use crate::solver::Config;
pub use crate::id::{Id, IdGenerator};
use crate::solver::Model;
use faer::linalg::svd::SvdError;
use faer::sparse::linalg::LuError;
use faer::sparse::{CreationError, FaerError};
pub use warnings::{Warning, WarningContent};
mod analysis;
mod constraint_request;
mod constraints;
pub mod datatypes;
mod id;
mod solver;
#[cfg(test)]
mod tests;
pub mod textual;
mod vector;
mod warnings;
const EPSILON: f64 = 1e-4;
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum Error {
#[error("{0}")]
NonLinearSystemError(#[from] NonLinearSystemError),
#[error("Solver error {0}")]
Solver(Box<dyn std::error::Error>),
#[error("No guess was given for point {label}")]
MissingGuess { label: String },
#[error("You gave a guess for points which weren't defined: {labels:?}")]
UnusedGuesses { labels: Vec<String> },
#[error("You referred to the point {label} but it was never defined")]
UndefinedPoint { label: String },
}
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum NonLinearSystemError {
#[error("ID {0} not found")]
NotFound(Id),
#[error(
"There should be exactly 1 guess per variable, but you supplied {labels} variables and must {guesses} guesses"
)]
WrongNumberGuesses { labels: usize, guesses: usize },
#[error(
"Constraint {constraint_id} references variable {variable} but no such variable appears in your initial guesses."
)]
MissingGuess { constraint_id: usize, variable: Id },
#[error("Could not create matrix: {error}")]
FaerMatrix {
#[from]
error: CreationError,
},
#[error("Something went wrong in faer: {error}")]
Faer {
#[from]
error: FaerError,
},
#[error("Something went wrong doing matrix solves in faer: {error}")]
FaerSolve {
#[from]
error: LuError,
},
#[error("Something went wrong doing SVD in faer")]
FaerSvd(SvdError),
#[error("Could not find a solution in the allowed number of iterations")]
DidNotConverge,
#[error("Cannot solve an empty system")]
EmptySystemNotAllowed,
}
#[derive(Debug)]
pub struct SolveOutcome {
pub unsatisfied: Vec<usize>,
pub final_values: Vec<f64>,
pub iterations: usize,
pub warnings: Vec<Warning>,
pub priority_solved: u32,
}
#[derive(Debug)]
pub struct SolveOutcomeFreedomAnalysis {
pub analysis: FreedomAnalysis,
pub outcome: SolveOutcome,
}
impl AsRef<SolveOutcome> for SolveOutcomeFreedomAnalysis {
fn as_ref(&self) -> &SolveOutcome {
&self.outcome
}
}
impl SolveOutcome {
pub fn is_satisfied(&self) -> bool {
self.unsatisfied.is_empty()
}
pub fn is_unsatisfied(&self) -> bool {
!self.is_satisfied()
}
}
#[derive(Debug)]
pub struct FailureOutcome {
pub error: Error,
pub warnings: Vec<Warning>,
pub num_vars: usize,
pub num_eqs: usize,
}
pub fn solve_with_priority(
reqs: &[ConstraintRequest],
initial_guesses: Vec<(Id, f64)>,
config: Config,
) -> Result<SolveOutcome, FailureOutcome> {
let out = solve_with_priority_inner::<NoAnalysis>(reqs, initial_guesses, config)?;
Ok(out.outcome)
}
pub fn solve_with_priority_analysis(
reqs: &[ConstraintRequest],
initial_guesses: Vec<(Id, f64)>,
config: Config,
) -> Result<SolveOutcomeFreedomAnalysis, FailureOutcome> {
let out = solve_with_priority_inner::<FreedomAnalysis>(reqs, initial_guesses, config)?;
Ok(SolveOutcomeFreedomAnalysis {
analysis: out.analysis,
outcome: out.outcome,
})
}
pub(crate) fn solve_with_priority_inner<A: Analysis>(
reqs: &[ConstraintRequest],
initial_guesses: Vec<(Id, f64)>,
config: Config,
) -> Result<SolveOutcomeAnalysis<A>, FailureOutcome> {
if reqs.is_empty() {
return Ok(SolveOutcomeAnalysis {
analysis: A::no_constraints(),
outcome: SolveOutcome {
unsatisfied: Vec::new(),
final_values: initial_guesses
.into_iter()
.map(|(_id, guess)| guess)
.collect(),
iterations: 0,
warnings: Vec::new(),
priority_solved: 0,
},
});
}
let reqs: Vec<_> = reqs
.iter()
.enumerate()
.map(|(id, c)| ConstraintEntry {
constraint: &c.constraint,
priority: c.priority,
id,
})
.collect();
let priorities: HashSet<_> = reqs.iter().map(|c| c.priority).collect();
let mut priorities: Vec<_> = priorities.into_iter().collect();
let lowest_priority = priorities.iter().min().copied().unwrap_or(0);
priorities.sort();
let mut res = None;
let total_constraints = reqs.len();
let mut constraint_subset: Vec<ConstraintEntry> = Vec::with_capacity(total_constraints);
for curr_max_priority in priorities {
constraint_subset.clear();
for req in &reqs {
if req.priority <= curr_max_priority {
constraint_subset.push(req.to_owned()); }
}
let solve_res = solve_inner(
constraint_subset.as_slice(),
initial_guesses.clone(),
config,
);
match solve_res {
Ok(outcome) => {
if outcome.outcome.is_unsatisfied() {
return Ok(res.unwrap_or(outcome));
}
res = Some(outcome);
}
Err(e) => {
return res.ok_or(e);
}
}
}
Ok(res.unwrap_or(SolveOutcomeAnalysis {
analysis: A::no_constraints(),
outcome: SolveOutcome {
unsatisfied: Vec::new(),
final_values: initial_guesses
.into_iter()
.map(|(_id, guess)| guess)
.collect(),
iterations: 0,
warnings: Vec::new(),
priority_solved: lowest_priority,
},
}))
}
fn solve_inner<A: Analysis>(
constraints: &[ConstraintEntry],
initial_guesses: Vec<(Id, f64)>,
config: Config,
) -> Result<SolveOutcomeAnalysis<A>, FailureOutcome> {
let num_vars = initial_guesses.len();
let num_eqs = constraints
.iter()
.map(|c| c.constraint.residual_dim())
.sum();
let (all_variables, mut values): (Vec<Id>, Vec<f64>) = initial_guesses.into_iter().unzip();
let mut warnings = warnings::lint(constraints);
let initial_values = values.clone();
let mut model = match Model::new(constraints, all_variables, initial_values, config) {
Ok(o) => o,
Err(e) => {
return Err(FailureOutcome {
error: e.into(),
warnings,
num_vars,
num_eqs,
});
}
};
let mut unsatisfied: Vec<usize> = Vec::new();
let outcome = model.solve_gauss_newton(&mut values, config);
warnings.extend(model.warnings.lock().unwrap().drain(..));
let success = match outcome {
Ok(o) => o,
Err(e) => {
return Err(FailureOutcome {
error: e.into(),
warnings,
num_vars,
num_eqs,
});
}
};
let cs: Vec<_> = constraints.iter().map(|c| c.constraint).collect();
let layout = crate::solver::Layout::new(&Vec::new(), cs.as_slice(), config);
for constraint in constraints.iter() {
let mut residual0 = 0.0;
let mut residual1 = 0.0;
let mut degenerate = false;
constraint.constraint.residual(
&layout,
&values,
&mut residual0,
&mut residual1,
&mut degenerate,
);
let satisfied = match constraint.constraint.residual_dim() {
1 => residual0.abs() < EPSILON,
2 => residual0.abs() < EPSILON && residual1.abs() < EPSILON,
other => unreachable!(
"Unsupported number of residuals {other}, the `residual` method must be modified."
),
};
if !satisfied {
unsatisfied.push(constraint.id);
}
}
let analysis = match A::analyze(model) {
Ok(o) => o,
Err(e) => {
return Err(FailureOutcome {
error: e.into(),
warnings,
num_vars,
num_eqs,
});
}
};
Ok(SolveOutcomeAnalysis {
outcome: SolveOutcome {
priority_solved: 0,
unsatisfied,
final_values: values,
iterations: success.iterations,
warnings,
},
analysis,
})
}