use basin::problems::ConstrainedQuadratic;
use basin::{
Backtracking, BarrierMethod, BasicState, CostFunction, DenseMatrix,
Executor, Gradient, GradientDescent, GradientState,
LinearInequalityConstraints, TargetCost, TerminationReason,
};
fn active_problem() -> ConstrainedQuadratic<DenseMatrix, Vec<f64>> {
ConstrainedQuadratic::new(
vec![2.0, 2.0],
DenseMatrix::from_row_slice(1, 2, &[1.0, 1.0]),
vec![2.0],
)
}
#[test]
fn active_constraint_converges_to_projection() {
let problem = active_problem();
let initial = vec![0.0, 0.0];
let result = Executor::new(
problem,
BarrierMethod::new(GradientDescent::with_line_search(
Backtracking::new(),
)),
BasicState::new(initial),
)
.max_iter(50)
.run()
.unwrap();
assert_eq!(result.reason, TerminationReason::SolverConverged);
assert!(
(result.param()[0] - 1.0).abs() < 1e-4
&& (result.param()[1] - 1.0).abs() < 1e-4,
"expected (1, 1), got {:?}",
result.param()
);
}
#[test]
fn inactive_constraint_recovers_unconstrained_minimum() {
let problem = ConstrainedQuadratic::new(
vec![0.5, 0.5],
DenseMatrix::from_row_slice(1, 2, &[1.0, 1.0]),
vec![2.0],
);
let initial = vec![0.0, 0.0];
let result = Executor::new(
problem,
BarrierMethod::new(GradientDescent::with_line_search(
Backtracking::new(),
)),
BasicState::new(initial),
)
.max_iter(50)
.run()
.unwrap();
assert_eq!(result.reason, TerminationReason::SolverConverged);
assert!(
(result.param()[0] - 0.5).abs() < 1e-4
&& (result.param()[1] - 0.5).abs() < 1e-4,
"expected (0.5, 0.5), got {:?}",
result.param()
);
}
#[test]
fn infeasible_start_runs_phase_one_then_converges() {
let problem = active_problem();
let initial = vec![2.0, 2.0];
let result = Executor::new(
problem,
BarrierMethod::new(GradientDescent::with_line_search(
Backtracking::new(),
)),
BasicState::new(initial),
)
.max_iter(50)
.run()
.unwrap();
assert_eq!(result.reason, TerminationReason::SolverConverged);
assert!(
(result.param()[0] - 1.0).abs() < 1e-4
&& (result.param()[1] - 1.0).abs() < 1e-4,
"expected (1, 1), got {:?}",
result.param()
);
}
#[test]
fn target_cost_does_not_bypass_phase_one() {
let result = Executor::new(
active_problem(),
BarrierMethod::new(GradientDescent::with_line_search(
Backtracking::new(),
)),
BasicState::new(vec![2.0, 2.0]),
)
.max_iter(50)
.terminate_on(TargetCost(0.1))
.run()
.unwrap();
assert_eq!(result.reason, TerminationReason::SolverConverged);
assert!(result.param()[0] + result.param()[1] < 2.0);
}
struct LinearProbe {
a: DenseMatrix,
b: Vec<f64>,
}
impl CostFunction for LinearProbe {
type Param = Vec<f64>;
type Output = f64;
type Error = std::convert::Infallible;
fn cost(&self, x: &Vec<f64>) -> Result<f64, Self::Error> {
Ok(x.iter().map(|xi| xi * xi).sum())
}
}
impl Gradient for LinearProbe {
type Gradient = Vec<f64>;
fn gradient(&self, x: &Vec<f64>) -> Result<Vec<f64>, Self::Error> {
Ok(x.iter().map(|xi| 2.0 * xi).collect())
}
}
impl LinearInequalityConstraints for LinearProbe {
type Matrix = DenseMatrix;
fn a(&self) -> &DenseMatrix {
&self.a
}
fn b(&self) -> &Vec<f64> {
&self.b
}
}
fn run_probe(problem: LinearProbe, initial: Vec<f64>) -> TerminationReason {
Executor::new(
problem,
BarrierMethod::new(GradientDescent::with_line_search(
Backtracking::new(),
)),
BasicState::new(initial),
)
.max_iter(50)
.run()
.unwrap()
.reason
}
#[test]
fn boundary_start_runs_phase_one_then_phase_two() {
let result = Executor::new(
active_problem(),
BarrierMethod::new(GradientDescent::with_line_search(
Backtracking::new(),
)),
BasicState::new(vec![1.0, 1.0]),
)
.max_iter(50)
.run()
.unwrap();
assert_eq!(result.reason, TerminationReason::SolverConverged);
assert!(result.param()[0] + result.param()[1] < 2.0);
}
#[test]
fn inconsistent_constraints_report_failure() {
let problem = LinearProbe {
a: DenseMatrix::from_row_slice(2, 1, &[1.0, -1.0]),
b: vec![0.0, -1.0],
};
assert_eq!(
run_probe(problem, vec![0.5]),
TerminationReason::SolverFailed
);
}
#[test]
fn distant_feasible_system_is_not_reported_infeasible() {
let problem = LinearProbe {
a: DenseMatrix::from_row_slice(1, 1, &[1.0]),
b: vec![0.0],
};
let result = Executor::new(
problem,
BarrierMethod::new(GradientDescent::with_line_search(
Backtracking::new(),
)),
BasicState::new(vec![1000.0]),
)
.max_iter(50)
.run()
.unwrap();
assert_eq!(result.reason, TerminationReason::SolverConverged);
assert!(
result.param()[0] < 0.0,
"expected a strict point, got {:?}",
result.param()
);
}
#[test]
fn empty_strict_interior_reports_failure() {
let problem = LinearProbe {
a: DenseMatrix::from_row_slice(2, 1, &[1.0, -1.0]),
b: vec![0.0, 0.0],
};
let result = Executor::new(
problem,
BarrierMethod::new(GradientDescent::with_line_search(
Backtracking::new(),
)),
BasicState::new(vec![0.0]),
)
.max_iter(50)
.run()
.unwrap();
assert_eq!(result.reason, TerminationReason::SolverFailed);
}
#[test]
fn nonfinite_initial_parameter_reports_failure() {
let problem = LinearProbe {
a: DenseMatrix::from_row_slice(1, 1, &[1.0]),
b: vec![0.0],
};
assert_eq!(
run_probe(problem, vec![f64::NAN]),
TerminationReason::SolverFailed
);
}
#[test]
fn nonfinite_constraint_data_reports_failure() {
let problem = LinearProbe {
a: DenseMatrix::from_row_slice(1, 1, &[1.0]),
b: vec![f64::INFINITY],
};
assert_eq!(
run_probe(problem, vec![0.0]),
TerminationReason::SolverFailed
);
}
#[test]
fn eval_counts_are_recorded() {
let problem = active_problem();
let initial = vec![0.0, 0.0];
let result = Executor::new(
problem,
BarrierMethod::new(GradientDescent::with_line_search(
Backtracking::new(),
)),
BasicState::new(initial),
)
.max_iter(50)
.run()
.unwrap();
assert!(result.cost_evals() > 0, "no cost evals recorded");
assert!(
result.state.gradient_evals() > 0,
"no gradient evals recorded"
);
}
#[test]
fn two_constraints_both_active() {
let problem = ConstrainedQuadratic::new(
vec![2.0, 2.0],
DenseMatrix::from_row_slice(2, 2, &[1.0, 1.0, 1.0, 0.0]),
vec![2.0, 0.5],
);
let initial = vec![0.0, 0.0];
let result = Executor::new(
problem,
BarrierMethod::new(GradientDescent::with_line_search(
Backtracking::new(),
)),
BasicState::new(initial),
)
.max_iter(50)
.run()
.unwrap();
assert_eq!(result.reason, TerminationReason::SolverConverged);
assert!(
(result.param()[0] - 0.5).abs() < 1e-4
&& (result.param()[1] - 1.5).abs() < 1e-4,
"expected (0.5, 1.5), got {:?}",
result.param()
);
}