Migrating to Basin 3.0

Basin 3.0 makes constraint declarations optional. This guide describes the development API on main; Basin 2.0 remains the published stable release. For an upgrade from Basin 1.x, first follow the Basin 2.0 migration guide, then apply these changes.

Plain objectives and residuals

Implement CostFunction and any derivatives your solver needs. A problem used with unconstrained solvers no longer needs an empty constraint declaration. Remove implementations whose only purpose was returning NoConstraints::EMPTY. Residual, NonlinearEqualityValues, NonlinearInequalityValues, and ConstraintJacobian also stand alone.

Unconstrained solvers now ignore declared constraints. Passing a problem with box bounds to GradientDescent optimizes its objective without enforcing those bounds. Use ProjectedGradientDescent or another compatible constrained solver to enforce them. The same rule applies to unbounded solver modes and least squares: a declaration alone does not make an algorithm respect constraints.

Declare constraints directly

The former ProblemConstraints trait is now Constraints. Its associated type is Set, and its getter is constraints. The former Constraints data container is now ConstraintSet.

Add this implementation alongside your objective and derivative implementations:

use basin::{BoxBounds, ConstraintDescription, ConstraintSet, Constraints};

struct ShiftedSphere {
    lower: Vec<f64>,
    upper: Vec<f64>,
}

impl Constraints for ShiftedSphere {
    type Set = ConstraintSet<BoxBounds<Vec<f64>>>;

    fn constraints(&self) -> <Self::Set as ConstraintDescription>::Borrowed<'_> {
        ConstraintSet::new().with_bounds(BoxBounds::new(
            Some(&self.lower),
            Some(&self.upper),
        ))
    }
}

The borrowed view still supports bounds, matrices, and right-hand sides stored in independent fields. If you store the whole set, return self.constraints.borrowed() instead. Builders preserve existing blocks and change the set’s type. Constrained solvers still require the complete declaration and reject unsupported blocks; a box-only solver cannot consume additional nonlinear constraints. Validation and callback error handling are unchanged.

Attach a set to an existing objective

Constrained::new(objective, set) remains a convenience adapter. Its objective needs no empty declaration, and callbacks can be implemented independently. The supplied set becomes the complete declaration, replacing any declaration on the inner objective. Combine blocks in one ConstraintSet before wrapping when they must all be enforced.

FiniteDiff, BoundedFiniteDiff, and RobustLeastSquares continue forwarding constraint declarations when their inner problem implements Constraints. Their unconstrained evaluation paths also work without a declaration.

Update generic bounds

P: CostFunction and P: Residual no longer imply constraint metadata. Add P: Constraints wherever generic code reads P::Set or calls constraints(). For example, replace a bound such as P: CostFunction<Constraints = NoConstraints> with P: CostFunction + Constraints<Set = NoConstraints> only when your code needs that explicit restriction. Unconstrained solvers no longer require it.

Replace qualified calls to ProblemConstraints::constraint_set with Constraints::constraints, and Constraints::new() on the old data container with ConstraintSet::new().

Sampling and composed solvers

SamplingBounds no longer requires a NoConstraints declaration. Its box controls sampling rather than feasibility. SamplingDomain still explicitly reinterprets a box-constrained problem as a sampling domain.

RelaxedInner remains available for box-constrained compositions that need its problem transformation and conversion to ConstraintSolveError. Existing uses remain valid. Execution budgets, evaluation accounting, and state ownership are unchanged. Constraint sets retain their serialized fields and layout; formats that embed Rust struct names may need the new ConstraintSet name.