Migrating to Basin 2.0
This guide covers the changes needed to move existing Basin 1.x code to 2.0. Start with your dependency features and problem definitions, then update state construction, stopping conditions, and result handling. Check the changed solver defaults before comparing numerical results.
The API reference documents full signatures and settings. If you have saved runs, read checkpoint files before upgrading.
Update your dependency features
Basin 2.0 enables no features by default. If you import basin::problems, add problems explicitly, keeping any other features you already use:
[dependencies]
basin = { version = "2", features = ["problems"] } The unversioned backend features now follow the newest supported release. nalgebra and nalgebra-lapack move from nalgebra 0.34 to 0.35, which requires
Rust 1.89. To keep nalgebra 0.34, use nalgebra_v0_34 and nalgebra_v0_34-lapack instead. The package MSRV remains Rust 1.87 for features
that do not select nalgebra 0.35.
Declare constraints on every problem
Every CostFunction and Residual implementation now also needs ProblemConstraints. For an unconstrained problem, add:
impl basin::ProblemConstraints for MyProblem {
type Constraints = basin::NoConstraints;
fn constraint_set(&self) -> basin::NoConstraints {
basin::NoConstraints::EMPTY
}
} For a constrained problem, replace the old constraint traits with one typed Constraints set. You can keep bounds and matrices in their existing fields
and borrow them from constraint_set. A box-only problem, for example, becomes:
use basin::{BoxBounds, ConstraintDescription, Constraints, ProblemConstraints};
struct BoxProblem {
lower: Vec<f64>,
upper: Vec<f64>,
}
impl ProblemConstraints for BoxProblem {
type Constraints = Constraints<BoxBounds<Vec<f64>>>;
fn constraint_set(
&self,
) -> <Self::Constraints as ConstraintDescription>::Borrowed<'_> {
Constraints::new()
.with_bounds(BoxBounds::new(Some(&self.lower), Some(&self.upper)))
}
} Build the set from the blocks your problem has:
| Removed 1.x trait | 2.0 replacement |
|---|---|
BoxConstraints | .with_bounds(BoxBounds::new(lower, upper)) |
LinearEqualityConstraints | .with_linear_equalities(LinearBlock::new(matrix, rhs)) |
LinearInequalityConstraints | .with_linear_inequalities(LinearBlock::new(matrix, rhs)) |
LinearConstraints | Put the bounds and linear blocks in the same set. |
NonlinearInequalityConstraints | .with_nonlinear_inequalities(count) and NonlinearInequalityValues |
NonlinearConstraints | Declare the nonlinear blocks and implement NonlinearEqualityValues and/or NonlinearInequalityValues. |
Declare the complete constraint set. A solver must support every declared kind;
a box-only solver cannot accept a problem that also declares nonlinear
constraints. Remove FoldedConstraints::new(problem) when using COBYLA and pass
the declared problem directly.
Nonlinear value traits now declare their own Param, Values, and Error types. Their callbacks return Result<Values, ConstraintSolveError<Error>>. ConstraintJacobian likewise declares Param, Jacobian, and Error and
returns the wrapped error type. Its rows contain nonlinear equalities first,
then nonlinear inequalities; exclude linear rows. Equalities use h(x) = 0,
and inequalities use c(x) <= 0.
Constrained solves also return ConstraintSolveError<E>. Update error handling
to distinguish Evaluation(E) from InvalidConstraints(ConstraintError).
Existing callback errors can be wrapped with map_err(ConstraintSolveError::Evaluation).
Update hybrid solver adapters
MA-LS now requires sampling bounds on an unconstrained objective. Wrap an
existing box-only problem in SamplingDomain::new(problem) to keep using its
box for sampling while allowing local chains to move outside it.
For bounded CMA or DE injection with an unconstrained local solver, wrap that
solver in RelaxedInner:
let solver = basin::DeInject::with_inner_solver(
basin::De::new(42),
basin::RelaxedInner::new(basin::NelderMead::new()),
); A local solver that enforces the same box, such as bounded L-BFGS, can still be passed directly.
Replace algorithm-specific states
Solvers now own their models, history, random generators, and workspace. Shared
states hold evaluated points, costs, and progress. If your code uses Executor::from_start, it already lets the solver choose the state:
let result = Executor::from_start(problem, solver, x0)
.max_iter(1_000)
.run()?; If you construct states explicitly, replace the old types as follows. Generic
parameters are omitted here for readability; shared vector states use <V, F = f64>.
| Basin 1.x state | Basin 2.0 state |
|---|---|
BasicState for gradient descent, projected gradient descent, or trust regions | FirstOrderState |
BasicState for SGD, basin hopping, or the barrier method | PointState |
BasicState for the augmented-Lagrangian method | SelectedState |
QuasiNewtonState and its backend aliases, LbfgsState | FirstOrderState |
NllsState | PointState |
BasicSimplexState | SimplexProgress |
BasicPopulationState, CmaEsState, GlobalBestPsoState | PopulationProgress |
MaLsChGenericState, MaLsChState, MaLsChSwState | PopulationProgress |
NewuoaState, BobyqaState, LincoaState, GbnmState, SolisWetsState | PointState |
MadsState | PointState |
ConstrainedMadsState, CobylaState | SelectedState |
SlsqpState | SelectedFirstOrderState |
SimulatedAnnealingState | ProposalState |
ScalarState<F> | PointState<F, F> |
ScalarGradientState<F> | FirstOrderState<F, F> |
Point-like states use new(x). For Nelder-Mead, use SimplexProgress::new(x) or SimplexProgress::from_simplex(vertices). Random
search, DE, SSGA, PSO, and MA-LS take PopulationProgress::empty() to sample a
population or from_population(members) to use explicit members. Construction
does not evaluate a point: checked current() and best() accessors return None until a record is available.
Several initialization settings moved from state to solver. Carry these over when replacing the state constructor:
- L-BFGS: replace
LbfgsState::new(x, m)withFirstOrderState::new(x)and setsolver.with_m_capacity(m). - CMA-ES: replace
CmaEsState::new(mean, sigma).with_stds(stds)withPopulationProgress::from_point(mean)andCmaEs::new(seed, sigma).with_stds(stds). Apply the same change to bounded CMA. Specify the matrix type on the solver if it was previously inferred from state. - Solis-Wets: replace
SolisWetsState::new(x, rho)withPointState::new(x)andSolisWets::new(seed).with_initial_step_size(rho). - Global-best PSO: replace
from_positions_and_velocities(xs, vs)withPopulationProgress::from_population(xs)andsolver.with_initial_velocities(Some(vs)). - Population size: replace
BasicPopulationState::with_size(n)withPopulationProgress::empty(). Set the size throughRandomSearch::new(n, seed)orwith_pop_size(n)on DE and SSGA. - BFGS: configure a custom initial inverse Hessian through
solver.with_initial_inverse_hessian(matrix)rather than state.
For these solvers, reusing progress starts a fresh run and resets the model, history, and RNG. Population searches reuse and reevaluate supplied members; CMA rebuilds its distribution around the reported point. To continue the same trajectory, retain both solver and state; see checkpoint files.
Read algorithm diagnostics from the solver
Move calls for algorithm quantities from state to solver. This includes BFGS’s inverse_hessian(), CMA’s mean() and sigma(), annealing’s temperature(),
PSO’s velocities and personal bests, MADS’s mesh settings, interpolation solvers’ rho(), and SLSQP’s stationarity and multipliers. Some getters now return Option because the model is unavailable before initialization.
Use run_with_solver() when you need the final solver, or stepper.solver() between steps. To migrate an observer that previously read
these quantities from state, use observe_solver:
use basin::{ObserverMode, State};
executor.observe_solver(
|state, solver, event| {
println!("{event:?}: {} {}", state.iter(), solver.temperature());
},
ObserverMode::Every(10),
).run()?; This example assumes an annealing executor. Progress-only observers can keep
using Observe<S>.
Update readers of evaluated progress
Constrained states return a ViolationReport alongside the point and objective
in current() and best(). Read report.value for the numeric violation.
The measure remains solver-specific: squared L2 for constrained MADS, L1 for
SLSQP, maximum violation for COBYLA, and L2 for the augmented-Lagrangian method.
Population costs are no longer guaranteed to be sorted. Bounded CMA now reports
clipped points with raw objective costs; read genotypes() and penalized_costs() from the solver if you need its internal ranking.
SGD now returns its last evaluated point with its matching cost. If costs are
refreshed less often than every step, this can lag behind the working iterate.
Use with_cost_eval_every(1) to report every iterate, or read working_param() from the retained solver.
Move stopping conditions to their owners
TerminationCriterion, its built-in criterion types, and terminate_on are
removed. Put numerical tolerances on the solver and execution limits on the
executor:
let solver = GradientDescent::new(0.1)
.with_absolute_gradient_tolerance(1e-8);
let result = Executor::from_start(problem, solver, x0)
.max_iter(1_000)
.max_cost_evals(10_000)
.run()?; Use this mapping for existing criteria. The same split applies to InnerExecutor; composed solvers’ inner_terminate_on methods are also removed.
| Removed criterion | Replacement |
|---|---|
MaxIter, MaxCostEvals, MaxGradientEvals, MaxTime | Executor: max_iter, max_cost_evals, max_gradient_evals, max_time |
TargetCost, NoImprovement, NoAcceptance | Executor: target_objective, no_objective_improvement, no_acceptance |
GradientTolerance, RelativeGradientTolerance | Solver: with_absolute_gradient_tolerance, with_relative_gradient_tolerance |
ProjectedGradientTolerance | with_absolute_projected_gradient_tolerance; bounds come from the problem. |
ParamTolerance, RelativeParamTolerance | with_absolute_step_tolerance, with_relative_step_tolerance |
CostTolerance, RelativeCostTolerance | with_absolute_cost_change_tolerance, with_relative_cost_change_tolerance |
SimplexTolerance | with_absolute_simplex_size_tolerance and with_absolute_simplex_cost_tolerance |
CmaEsTolerance | with_absolute_distribution_size_tolerance |
RhoTolerance | with_absolute_radius_tolerance, or with_absolute_step_size_tolerance for Solis-Wets |
MeshTolerance | with_absolute_poll_size_tolerance |
Optional tolerances use None to disable a check; zero now means an exact-zero
threshold. Repeating a setter replaces that setting. Distinct numerical tests
usually combine with OR, while Nelder-Mead’s enabled simplex size and cost-spread
tests combine with AND. Check the solver’s documented norm and scaling when
replacing a generic criterion.
target_cost and no_improvement are renamed to target_objective and no_objective_improvement. They require an objective-ordered incumbent. SelectedState and SelectedFirstOrderState do not provide that guarantee:
their solvers may select a higher-cost point as feasibility improves. For those
states, use an application stop that checks both violation and objective.
Replace custom criteria with stop_when, returning Some(ApplicationStop) to
stop or None to continue:
executor.stop_when(|state| {
(state.iter() >= 500).then(|| basin::ApplicationStop::new("user_requested"))
}) For reusable inner solves with captured history, use stop_when_factory to
create a fresh closure for each run. Composed solvers expose inner_stop_when_factory.
Execution controls now run in a fixed order before numerical convergence, regardless of the old criterion registration order. Budgets are checked after initialization and between steps, so one step can exceed an evaluation budget.
Update evaluation budgets
Shared states’ cost_evals() and gradient_evals() now count only their named
callback categories. They no longer fold other evaluations into those totals.
This matters especially for least squares, which typically makes no cost calls:
replace max_cost_evals(n) with max_evaluations(EvaluationKind::Residual, n).
Nonlinear constraint callbacks count as residual evaluations, and constraint Jacobians count as Jacobian evaluations.
Use state.counts() or result.counts for all categories. Budget Hessians,
Jacobian calls, and inner-solver derivative work explicitly with max_evaluations(kind, n), or use EvaluationKind::TotalWork for their sum,
including objective calls. On states without gradients, including SGD’s new PointState, replace max_gradient_evals(n) with max_evaluations(EvaluationKind::Gradient, n).
Read the termination report
Replace result.reason with result.report.termination and matches on TerminationReason with matches on Termination:
match &result.report.termination {
basin::Termination::Converged(_) => println!("converged"),
basin::Termination::Limit(limits) => println!("budget exhausted: {limits:?}"),
stop => println!("stopped: {stop:?}"),
} A numerical failure is reported through Termination::Failed with a returned
state; callback errors still return Err. Code that treats every Ok result
as convergence should inspect the termination instead.
Update observers and steppers to receive the report:
| Basin 1.x | Basin 2.0 |
|---|---|
Observe::observe_final(..., &TerminationReason) | observe_final(..., &TerminationReport<S::Float>) |
ObservationEvent::Final(reason) | ObservationEvent::Final(&report) |
StepOutcome::Stopped(reason) | StepOutcome::Stopped(report) |
Stepper::finished() -> Option<TerminationReason> | Option<&TerminationReport<S::Float>> |
reason as u8 | result.report.code().as_u8() |
Final observer events borrow the report; clone it if you need to retain it. StepOutcome is now Clone rather than Copy. The numeric termination code
preserves the 1.x identifiers but cannot represent every detail of the report.
Shared gradient checks retain the AbsoluteGradientSquared and RelativeGradientSquared test identities. Their evidence uses FactoredUpperBound when squaring a gradient norm or tolerance would overflow
or erase a nonzero value. Handle that variant when reading measurements.
Relative gradient checks retain the initial unsquared norm in solver history;
recreate serialized prerelease checkpoints rather than loading the old squared
history into Basin 2.0.
Custom backends can override NormSquared::norm_l2 with a scaled Euclidean
norm calculation. Its compatibility default takes the square root of norm_squared, which can lose tiny nonzero values or overflow. All built-in
backends provide scaled implementations, and shared gradient and step checks
use this method without requiring another vector trait.
Update solver names and settings
Rename Trf
Rename Trf<V, M, F> to BoundedLevenbergMarquardt<V, M, F> and basin::solver::trf to basin::solver::bounded_levenberg_marquardt. The
algorithm and generic parameters are unchanged. Use the renamed type to keep
the old method; TrustRegionReflective is a separate solver.
Adjust explicit type annotations
NelderMead, Gbnm, Cobyla, SolisWets, and GlobalBestPso now include the
parameter vector type before the scalar. For example, Cobyla::<f32> becomes Cobyla::<Vec<f32>, f32>. Inferred constructor calls usually need no annotation.
For GaussNewton with f32, supply its third parameter: GaussNewton<Vec<f32>, DenseMatrix<f32>, f32>. Generic wrappers must carry F: Scalar where required by the solver type.
L-BFGS now uses Lbfgs<V, F, Mode, L>. Replace Lbfgs::<Unbounded>::new() with Lbfgs::new().unbounded(), and likewise call .unbounded() after with_line_search(search) for an unbounded custom search. Lbfgsb remains the bounded alias.
Replace abbreviated setters
The deprecated aliases below are removed. Call the replacement on the same solver or line search.
| Component | Removed method | Replacement |
|---|---|---|
Brent, BrentDerivative, GoldenSection | with_tol(relative, absolute) | new().with_relative_position_tolerance(relative).with_absolute_position_tolerance(absolute) |
BrentRoot | with_tol(relative, absolute) | with_relative_position_tolerance(relative).with_absolute_position_tolerance(absolute) |
| Gauss-Newton and both LM variants | with_tol_grad | with_absolute_gradient_tolerance |
BoundedLevenbergMarquardt (formerly Trf) | with_tol_grad | with_absolute_scaled_gradient_tolerance |
| Both LM variants | with_tol_grad_rel, with_tol_cost_rel, with_tol_step_rel | with_gradient_orthogonality_tolerance, with_relative_model_reduction_tolerance, with_relative_step_tolerance |
LevenbergMarquardtQr | with_rank_tolerance | with_relative_rank_tolerance |
Bounded Lbfgs | with_tol_pg | with_absolute_projected_gradient_tolerance |
Bfgs, Lbfgs | with_epsilon | with_relative_curvature_tolerance |
Gbnm | with_small_tolerance, with_flat_tolerance | with_normalized_simplex_size_tolerance, with_absolute_simplex_cost_tolerance |
Gbnm | with_degeneracy_tolerances(edge, determinant) | with_edge_ratio_tolerance(edge).with_normalized_determinant_tolerance(determinant) |
BarrierMethod, AugmentedLagrangianMethod | with_tol | with_absolute_duality_gap_tolerance, with_absolute_feasibility_tolerance, respectively |
BarrierMethod | with_phase_one_tol | with_absolute_phase_one_gap_tolerance |
Newuoa, Bobyqa, Lincoa, Cobyla | with_rho_beg, with_rho_end | with_initial_radius, with_final_radius |
Mads | with_min_poll_size | with_minimum_poll_size |
SolisWets | with_rho_init | with_initial_step_size |
MaLsChCma | with_initial_sigma_fallback | with_initial_scale_fallback |
Backtracking | c | with_sufficient_decrease_coefficient |
Wolfe | c1, c2 | with_sufficient_decrease_coefficient, with_curvature_coefficient |
MoreThuente | ftol, gtol, xtol | with_sufficient_decrease_coefficient, with_curvature_coefficient, with_relative_bracket_tolerance |
HagerZhang | delta_sigma, epsilon | with_wolfe_coefficients, with_relative_cost_relaxation_tolerance |
Configure barrier and augmented-Lagrangian inner solvers
BarrierMethod::new, AugmentedLagrangianMethod::new, and with_inner_grad_tol are removed. Use with_inner_solver(inner) and configure
the inner solver’s convergence before passing it in:
let solver = BarrierMethod::with_inner_solver(
GradientDescent::with_line_search(Backtracking::new())
.with_absolute_gradient_tolerance(1e-8),
); The outer methods no longer install an implicit inner gradient test. They also
now require surrogate stationarity alongside feasibility or duality gap, which
can change when a run converges. Set with_absolute_stationarity_tolerance(None) and with_relative_stationarity_tolerance(None) to recover the previous
feasibility/gap-only stopping rule.
Check changed solver defaults
Nelder-Mead stops on simplex collapse by default
Nelder-Mead now enables both simplex size and cost-spread checks, including in
adaptive and projected modes. Both absolute tolerances default to 1e-8; both
relative tolerances default to about 1.5e-8 for f64 and 3.8e-6 for f32.
The two checks must both pass. A run that previously continued to its execution
limit may therefore stop earlier.
To retain the 1.x behavior with no simplex convergence check, disable all four settings:
let solver = NelderMead::new()
.with_absolute_simplex_size_tolerance(None)
.with_relative_simplex_size_tolerance(None)
.with_absolute_simplex_cost_tolerance(None)
.with_relative_simplex_cost_tolerance(None); If you previously configured only absolute tolerances, disable both relative
settings to preserve that policy. Simplex setters now return NelderMead directly; remove explicit ConfiguredSolver<..., SimplexChecks<...>> annotations
used for those settings. The Nelder-Mead reference gives the comparison formulas and scaling details.
Bounded L-BFGS changes its f64 tolerances
The default absolute projected-gradient tolerance changes from 1e-10 to 1e-9. A native relative-cost reduction test is also enabled at 10 * f64::EPSILON, so a small reduction can stop the run before the gradient
test passes. To retain the old defaults:
let solver = basin::Lbfgsb::<Vec<f64>>::new()
.with_absolute_projected_gradient_tolerance(1e-10)
.with_relative_cost_reduction_tolerance(None); The f32 and unbounded defaults are unchanged.
LM uses trust-region damping by default
LevenbergMarquardt and LevenbergMarquardtQr now default to LmDamping::TrustRegion for both scalar types. To retain Nielsen damping:
let solver: basin::LevenbergMarquardt<Vec<f64>, basin::DenseMatrix> =
basin::LevenbergMarquardt::new()
.with_damping(basin::LmDamping::Nielsen); with_tau() only configures Nielsen damping; it does not select it. With the
new default, use with_initial_step_bound() to configure the initial trust
radius.
Use constructors for public records
Result types, evaluation counts, line-search configurations, and many diagnostic
records are now non-exhaustive. Replace struct literals and struct update syntax
with public constructors or builders. For example, use OptimizationResult::new(state, counts, report) and Wolfe::new().with_curvature_coefficient(0.8).
For EvalCounts, start with EvalCounts::default() and assign the fields you
need. Include .. when destructuring non-exhaustive records, and a fallback
arm when matching non-exhaustive enums. Public fields remain available for
reading and assignment.
Move saved runs before upgrading
A 1.x exact checkpoint cannot be resumed in 2.0. Keep the matching 1.x application to finish the run, or load the checkpoint there, export its parameters in an application-owned format, and start a fresh 2.0 run. The same approach works for state files whose types or layouts changed.
Basin 2.0 also removes the readers for legacy bincode files. State files must use the postcard format introduced in Basin 1.15; that file format alone does not make an old state’s serialized fields compatible with its 2.0 replacement. Exact files require matching Basin versions and concrete solver and state types.
Within 2.0, retain solver and state together for exact continuation:
let checkpoint = executor.run_with_solver()?.into_checkpoint();
let result = Executor::resume_from_checkpoint(problem, checkpoint)
.max_iter(2_000)
.run()?; State-only annealing and PSO snapshots no longer support Executor::resume.
Passing progress to Executor::new starts a fresh run at iteration zero; exact
continuation preserves the iteration count and model history. Reattach execution
controls and observers when resuming.
If you implement solvers or backends
Custom Solver::next_iter implementations now return Result<SolverStep<S>, E> instead of (S, Option<TerminationReason>). Use SolverStep::completed(state) to continue, stopped(state, termination) for a partial stopping step, or completed_with_termination(state, termination) for a completed stopping step.
Only completed steps increment the iteration count. terminate and check_convergence now return Option<Termination<S::Float>>, and State::Float must implement Scalar.
Replace run_loop(..., criteria, max_iter) with run_loop_with_control(problem, state, solver, &mut control) and configure a RunControl. In custom inner solvers, replace ResumableInner::segment_criteria with configure_segment(&mut self, state, control), and change MemeticInner::seed_scaled to take &mut self.
Custom matrix implementations must move shape methods from MatrixIndex into MatrixShape. Custom vector backends using BFGS must implement DenseBackend for matrix inference, or select the matrix explicitly with Bfgs::<V, F, M, L>::with_matrix_and_line_search(search). Stateful custom line
searches must implement LineSearch::reset so fresh runs reset their history.