#![doc = include_str!("../README.md")]
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::error::*;
pub use crate::solver::Config;
pub use crate::id::{Id, IdGenerator};
use crate::solver::Model;
pub use solve_outcome::{FailureOutcome, SolveOutcome, SolveOutcomeFreedomAnalysis};
pub use warnings::{Warning, WarningContent};
mod analysis;
mod constraint_request;
mod constraints;
pub mod datatypes;
mod error;
mod id;
#[cfg(feature = "residual-viz")]
pub mod residual_viz;
mod solve_outcome;
mod solver;
#[cfg(test)]
mod tests;
pub mod textual;
mod vector;
mod warnings;
const EPSILON: f64 = 1e-4;
pub fn solve(
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_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(error) => {
return Err(FailureOutcome {
error,
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(error) => {
return Err(FailureOutcome {
error,
warnings,
num_vars,
num_eqs,
});
}
};
let cs: Vec<_> = constraints.iter().map(|c| c.constraint).collect();
let layout = solver::Layout::new(&Vec::new(), cs.as_slice(), config);
for constraint in constraints {
let mut residual0 = 0.0;
let mut residual1 = 0.0;
let mut residual2 = 0.0;
let mut degenerate = false;
constraint.constraint.residual(
&layout,
&values,
&mut residual0,
&mut residual1,
&mut residual2,
&mut degenerate,
);
let satisfied = is_satisfied(
constraint.constraint.residual_dim(),
[residual0, residual1, residual2],
);
if !satisfied {
unsatisfied.push(constraint.id);
}
}
let analysis = match A::analyze(model) {
Ok(o) => o,
Err(error) => {
return Err(FailureOutcome {
error,
warnings,
num_vars,
num_eqs,
});
}
};
let lowest_priority = constraints
.iter()
.map(|c| c.priority)
.max()
.unwrap_or_default();
Ok(SolveOutcomeAnalysis {
outcome: SolveOutcome {
priority_solved: lowest_priority,
unsatisfied,
final_values: values,
iterations: success.iterations,
warnings,
},
analysis,
})
}
fn is_satisfied(residual_dim: usize, residuals: [f64; 3]) -> bool {
let sat0 = residuals[0].abs() < EPSILON;
let sat1 = residuals[1].abs() < EPSILON;
let sat2 = residuals[2].abs() < EPSILON;
match residual_dim {
1 => sat0,
2 => sat0 && sat1,
3 => sat0 && sat1 && sat2,
other => unreachable!(
"Unsupported number of residuals {other}, the `residual` method must be modified."
),
}
}
#[cfg(test)]
mod basic_tests {
use super::*;
#[test]
fn test_is_satisfied_0() {
let actual = is_satisfied(1, [1e-8, 44.0, 44.0]);
let expected = true;
assert_eq!(actual, expected);
}
#[test]
fn test_is_satisfied_1() {
let actual = is_satisfied(2, [1e-8, 1e-8, 44.0]);
let expected = true;
assert_eq!(actual, expected);
}
#[test]
fn test_is_satisfied_2() {
let actual = is_satisfied(3, [1e-8, 1e-8, 1e-8]);
let expected = true;
assert_eq!(actual, expected);
}
#[test]
fn test_is_unsatisfied_0() {
let actual = is_satisfied(1, [44.0, 44.0, 44.0]);
let expected = false;
assert_eq!(actual, expected);
}
#[test]
fn test_is_unsatisfied_1() {
let actual = is_satisfied(2, [1e-8, 44.0, 44.0]);
let expected = false;
assert_eq!(actual, expected);
}
#[test]
fn test_is_unsatisfied_2() {
let actual = is_satisfied(3, [44.0, 1e-8, 1e-8]);
let expected = false;
assert_eq!(actual, expected);
}
}