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 trait2.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))
LinearConstraintsPut the bounds and linear blocks in the same set.
NonlinearInequalityConstraints.with_nonlinear_inequalities(count) and NonlinearInequalityValues
NonlinearConstraintsDeclare 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 stateBasin 2.0 state
BasicState for gradient descent, projected gradient descent, or trust regionsFirstOrderState
BasicState for SGD, basin hopping, or the barrier methodPointState
BasicState for the augmented-Lagrangian methodSelectedState
QuasiNewtonState and its backend aliases, LbfgsStateFirstOrderState
NllsStatePointState
BasicSimplexStateSimplexProgress
BasicPopulationState, CmaEsState, GlobalBestPsoStatePopulationProgress
MaLsChGenericState, MaLsChState, MaLsChSwStatePopulationProgress
NewuoaState, BobyqaState, LincoaState, GbnmState, SolisWetsStatePointState
MadsStatePointState
ConstrainedMadsState, CobylaStateSelectedState
SlsqpStateSelectedFirstOrderState
SimulatedAnnealingStateProposalState
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) with FirstOrderState::new(x) and set solver.with_m_capacity(m).
  • CMA-ES: replace CmaEsState::new(mean, sigma).with_stds(stds) with PopulationProgress::from_point(mean) and CmaEs::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) with PointState::new(x) and SolisWets::new(seed).with_initial_step_size(rho).
  • Global-best PSO: replace from_positions_and_velocities(xs, vs) with PopulationProgress::from_population(xs) and solver.with_initial_velocities(Some(vs)).
  • Population size: replace BasicPopulationState::with_size(n) with PopulationProgress::empty(). Set the size through RandomSearch::new(n, seed) or with_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 criterionReplacement
MaxIter, MaxCostEvals, MaxGradientEvals, MaxTimeExecutor: max_iter, max_cost_evals, max_gradient_evals, max_time
TargetCost, NoImprovement, NoAcceptanceExecutor: target_objective, no_objective_improvement, no_acceptance
GradientTolerance, RelativeGradientToleranceSolver: with_absolute_gradient_tolerance, with_relative_gradient_tolerance
ProjectedGradientTolerancewith_absolute_projected_gradient_tolerance; bounds come from the problem.
ParamTolerance, RelativeParamTolerancewith_absolute_step_tolerance, with_relative_step_tolerance
CostTolerance, RelativeCostTolerancewith_absolute_cost_change_tolerance, with_relative_cost_change_tolerance
SimplexTolerancewith_absolute_simplex_size_tolerance and with_absolute_simplex_cost_tolerance
CmaEsTolerancewith_absolute_distribution_size_tolerance
RhoTolerancewith_absolute_radius_tolerance, or with_absolute_step_size_tolerance for Solis-Wets
MeshTolerancewith_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.xBasin 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 u8result.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.

ComponentRemoved methodReplacement
Brent, BrentDerivative, GoldenSectionwith_tol(relative, absolute)new().with_relative_position_tolerance(relative).with_absolute_position_tolerance(absolute)
BrentRootwith_tol(relative, absolute)with_relative_position_tolerance(relative).with_absolute_position_tolerance(absolute)
Gauss-Newton and both LM variantswith_tol_gradwith_absolute_gradient_tolerance
BoundedLevenbergMarquardt (formerly Trf)with_tol_gradwith_absolute_scaled_gradient_tolerance
Both LM variantswith_tol_grad_rel, with_tol_cost_rel, with_tol_step_relwith_gradient_orthogonality_tolerance, with_relative_model_reduction_tolerance, with_relative_step_tolerance
LevenbergMarquardtQrwith_rank_tolerancewith_relative_rank_tolerance
Bounded Lbfgswith_tol_pgwith_absolute_projected_gradient_tolerance
Bfgs, Lbfgswith_epsilonwith_relative_curvature_tolerance
Gbnmwith_small_tolerance, with_flat_tolerancewith_normalized_simplex_size_tolerance, with_absolute_simplex_cost_tolerance
Gbnmwith_degeneracy_tolerances(edge, determinant)with_edge_ratio_tolerance(edge).with_normalized_determinant_tolerance(determinant)
BarrierMethod, AugmentedLagrangianMethodwith_tolwith_absolute_duality_gap_tolerance, with_absolute_feasibility_tolerance, respectively
BarrierMethodwith_phase_one_tolwith_absolute_phase_one_gap_tolerance
Newuoa, Bobyqa, Lincoa, Cobylawith_rho_beg, with_rho_endwith_initial_radius, with_final_radius
Madswith_min_poll_sizewith_minimum_poll_size
SolisWetswith_rho_initwith_initial_step_size
MaLsChCmawith_initial_sigma_fallbackwith_initial_scale_fallback
Backtrackingcwith_sufficient_decrease_coefficient
Wolfec1, c2with_sufficient_decrease_coefficient, with_curvature_coefficient
MoreThuenteftol, gtol, xtolwith_sufficient_decrease_coefficient, with_curvature_coefficient, with_relative_bracket_tolerance
HagerZhangdelta_sigma, epsilonwith_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.