Skip to content

Optimization & design

Gradient-based optimizers that differentiate straight through converged flowsheets, scalar design solvers for meeting a spec, and the flowsheet-level optimization helpers.

See the optimization, design & economics guide for worked examples.

Optimizers

optimize

Differentiable numerical optimization for process design.

Fugacio's whole premise is that a flowsheet is end-to-end differentiable, so a design problem (minimize a cost, maximize a yield, hit a purity at least operating cost) is a smooth optimization that gradients can solve directly. This module supplies the optimizers, written against jax.numpy so they compose with the rest of the engine, and (crucially) it differentiates through the optimum: the solution x*(theta) of a parametric optimization problem carries exact derivatives with respect to the parameters theta by the implicit function theorem applied to the optimality (KKT) conditions, exactly as fugacio.thermo.implicit differentiates a converged flash.

The numeric core operates on a flat parameter vector, but every public entry point accepts an arbitrary JAX pytree as the decision variable (a dict of operating conditions, a Stream, ...) and flattens it internally with jax.flatten_util.ravel_pytree, so you optimize in the natural shape of your problem.

Algorithms

  • BFGS (dense inverse-Hessian quasi-Newton): the robust default for smooth unconstrained problems of modest dimension, with an Armijo backtracking line search and a curvature-safeguarded update.
  • Gradient descent with optional momentum and a line search: a simple, dependable fallback.
  • Newton: full-Hessian steps with a line search, for cheap, well-behaved Hessians (small design problems).
  • Spectral projected gradient (SPG): Barzilai-Borwein steps projected onto box bounds with a non-monotone line search, for bound-constrained problems.
  • Augmented Lagrangian: equality and inequality constraints wrapped around any of the inner solvers above (the workhorse for constrained design).
  • Levenberg-Marquardt: damped Gauss-Newton for nonlinear least squares (data fitting, multi-spec reconciliation).

Differentiation

argmin returns just the optimal decision variable and attaches an implicit-function-theorem custom_vjp: for an unconstrained minimum the stationarity condition grad_x f(x*, theta) = 0 is differentiated; with box bounds the active variables are held fixed and the reduced Hessian system is solved on the free set; with equality constraints the full KKT system is differentiated. The forward solve (however many iterations it took) never appears in the backward pass, so a gradient of an optimized design with respect to a price, a feed spec, or a model parameter costs a single linear solve.

Classes:

Name Description
OptimizeResult

Outcome of an optimization run.

Functions:

Name Description
minimize

Minimize fun(x, theta) over the decision pytree x.

argmin

The minimizer x*(theta) = argmin_x fun(x, theta), differentiable in theta.

least_squares

Solve min_x 0.5 ||residual(x, theta)||^2 by Levenberg-Marquardt.

OptimizeResult

Bases: NamedTuple

Outcome of an optimization run.

Attributes:

Name Type Description
x Any

The optimal decision variable, in the pytree structure of x0.

fun Array

Objective value at x.

grad_norm Array

Max-norm of the (projected) gradient at x, the first-order optimality residual.

n_iter Array

Number of outer iterations taken.

converged Array

Whether the optimality/feasibility tolerances were met.

constraint_violation Array

Max constraint violation (0 when unconstrained).

minimize

minimize(
    fun: Objective,
    x0: Any,
    theta: Any = None,
    *,
    method: str = "bfgs",
    bounds: tuple[Any, Any] | None = None,
    eq_constraints: Constraint | None = None,
    ineq_constraints: Constraint | None = None,
    tol: float = 1e-06,
    max_iter: int = 200,
    inner_iter: int = 100,
) -> OptimizeResult

Minimize fun(x, theta) over the decision pytree x.

Parameters:

Name Type Description Default
fun Objective

Scalar objective fun(x, theta) -> ().

required
x0 Any

Initial decision pytree (its structure defines the unknown).

required
theta Any

Optional parameter pytree forwarded to fun and the constraints.

None
method str

Unconstrained inner method, one of "bfgs" (default), "gradient-descent", or "newton". Ignored when bounds or constraints are present (SPG / augmented Lagrangian take over).

'bfgs'
bounds tuple[Any, Any] | None

Optional (lower, upper) box. Each side may be a scalar or a pytree matching x0; None on a side means unbounded.

None
eq_constraints Constraint | None

Optional h(x, theta) -> (m,) enforced = 0.

None
ineq_constraints Constraint | None

Optional g(x, theta) -> (k,) enforced <= 0.

None
tol float

First-order optimality / feasibility tolerance.

1e-06
max_iter int

Outer iteration cap.

200
inner_iter int

Inner-solve iteration cap (constrained problems only).

100

Returns:

Type Description
OptimizeResult

An OptimizeResult. For gradients of the solution with respect

OptimizeResult

to theta, use argmin.

argmin

argmin(
    fun: Objective,
    x0: Any,
    theta: Any,
    *,
    method: str = "bfgs",
    bounds: tuple[Any, Any] | None = None,
    eq_constraints: Constraint | None = None,
    ineq_constraints: Constraint | None = None,
    tol: float = 1e-07,
    max_iter: int = 200,
    inner_iter: int = 100,
) -> Any

The minimizer x*(theta) = argmin_x fun(x, theta), differentiable in theta.

Identical problem setup to minimize, but returns only the optimal decision pytree and (the point of this function) carries exact gradients with respect to theta by implicit differentiation of the optimality conditions. Use it to differentiate an optimized design with respect to prices, feed specifications, or thermodynamic-model parameters.

The implicit rule differentiates: the stationarity condition grad_x f(x*, theta) = 0 (unconstrained); the same restricted to the free variables, holding active box bounds fixed (bound-constrained); or the full KKT system [grad_x L; c] = 0 with active inequalities promoted to equalities (constrained). Gradients are independent of the iteration count.

least_squares

least_squares(
    residual: Residual,
    x0: Any,
    theta: Any = None,
    *,
    tol: float = 1e-08,
    max_iter: int = 100,
) -> OptimizeResult

Solve min_x 0.5 ||residual(x, theta)||^2 by Levenberg-Marquardt.

A damped Gauss-Newton method for nonlinear least squares: parameter reconciliation, fitting a model to several measurements at once, or driving a set of design residuals to zero. Returns an OptimizeResult whose fun is the half-sum-of-squares.

Design solvers

design

Design specifications and set-point controllers for flowsheets.

A design spec is the everyday inverse problem of process design: instead of "given the reflux ratio, what purity do I get?", you ask "what reflux ratio hits 99.5 % purity?". You nominate a manipulated variable (a degree of freedom: a duty, a reflux, a split fraction, a feed temperature) and a controlled variable (a calculated result: a purity, a recovery, a temperature) with a target, and the solver adjusts the manipulated variable until the controlled variable meets its target. Several specs are solved simultaneously, so coupled targets (a column's distillate and bottoms purity, set by reflux and reboiler duty) converge together.

Because the whole engine is differentiable, a met spec is itself differentiable: the adjusted manipulated variable (and everything computed from it) carries exact gradients with respect to the unmanipulated parameters (feed, prices, model parameters). The spec solve reuses the implicit-function-theorem root finders in fugacio.thermo.implicit, so those gradients cost a single linear solve regardless of how many iterations the spec took, and they compose with the recycle gradients from fugacio.sim.flowsheet.tear_solve.

This is the steady-state, set-point form of control: a controller here drives a controlled variable to its set point at steady state. Dynamic controllers (PID and friends) belong to the dynamic-simulation layer.

Classes:

Name Description
DesignSpec

One design specification: adjust manipulated until measure hits target.

SpecResult

Outcome of a design-spec solve.

FlowsheetOptResult

Outcome of a flowsheet optimization.

Functions:

Name Description
meet_spec

Find the manipulated value u such that measure(u, theta) == target.

solve_design

Adjust the manipulated variables so every design spec meets its target.

controller

Convenience constructor for a single set-point controller as a DesignSpec.

optimize_flowsheet

Optimize selected design variables of a flowsheet against a cost objective.

DesignSpec dataclass

DesignSpec(
    manipulated: str,
    measure: Measure,
    target: float,
    lo: float,
    hi: float,
    name: str = "",
)

One design specification: adjust manipulated until measure hits target.

Attributes:

Name Type Description
manipulated str

Key in the parameter mapping theta to adjust (the degree of freedom). Its current value seeds the search.

measure Measure

Reads the controlled variable from the solved streams, e.g. lambda s: s["distillate"].z[0] for a top mole fraction.

target float

Desired value of the controlled variable.

lo float

Lower bound on the manipulated variable (used by the bracketing solver and to keep the search physical).

hi float

Upper bound on the manipulated variable.

name str

Optional label for reporting.

SpecResult

Bases: NamedTuple

Outcome of a design-spec solve.

Attributes:

Name Type Description
theta dict[str, Any]

The parameter mapping with the manipulated variables set to their converged values (differentiable with respect to the unmanipulated entries of the input theta).

streams dict[str, Stream]

The solved flowsheet streams at the converged spec.

manipulated Array

The converged manipulated values, aligned with the specs.

residual Array

Controlled-variable errors measure - target at the solution.

converged Array

Whether every spec met its target within tolerance.

FlowsheetOptResult

Bases: NamedTuple

Outcome of a flowsheet optimization.

Attributes:

Name Type Description
theta dict[str, Any]

The parameter mapping with the optimized design variables.

streams dict[str, Stream]

The solved flowsheet at the optimum.

objective Array

Objective value at the optimum.

converged Array

Whether the optimizer met its tolerances.

n_iter Array

Iterations taken.

meet_spec

meet_spec(
    measure: Callable[[Array, Any], Array],
    target: ArrayLike,
    u0: ArrayLike,
    theta: Any = None,
    *,
    lo: ArrayLike | None = None,
    hi: ArrayLike | None = None,
    tol: float = 1e-08,
    max_iter: int = 100,
) -> Array

Find the manipulated value u such that measure(u, theta) == target.

The low-level, single-variable spec solver. When a bracket [lo, hi] is given it uses the robust bisection root finder (safe across the kinks an EOS flash produces); otherwise a damped Newton iteration. The returned u is differentiable with respect to theta by implicit differentiation.

Parameters:

Name Type Description Default
measure Callable[[Array, Any], Array]

Controlled variable measure(u, theta) -> ().

required
target ArrayLike

Desired value.

required
u0 ArrayLike

Initial guess for the manipulated variable.

required
theta Any

Differentiable parameter pytree forwarded to measure.

None
lo ArrayLike | None

Lower bracket bound; when both lo and hi are given, bisection is used.

None
hi ArrayLike | None

Upper bracket bound; when both lo and hi are given, bisection is used.

None
tol float

Convergence tolerance.

1e-08
max_iter int

Iteration cap.

100

Returns:

Type Description
Array

The manipulated value meeting the spec; differentiable in theta.

solve_design

solve_design(
    simulate: Simulate,
    theta: Mapping[str, Any],
    specs: Sequence[DesignSpec],
    *,
    tol: float = 1e-08,
    max_iter: int = 50,
) -> SpecResult

Adjust the manipulated variables so every design spec meets its target.

Solves the (generally coupled) system "set each manipulated variable so its controlled variable equals its target" with a single Newton iteration over the stacked specs, re-running simulate (recycles and all) at each step. The converged manipulated values (and the streams computed from them) are differentiable with respect to the unmanipulated entries of theta.

Parameters:

Name Type Description Default
simulate Simulate

Runs the flowsheet for a parameter mapping and returns the named output streams, e.g. flowsheet.solve wrapped to take a mapping, or any lambda th: {...}.

required
theta Mapping[str, Any]

Base parameter mapping. The manipulated keys named by the specs are overwritten by the solver; their values in theta seed it.

required
specs Sequence[DesignSpec]

The design specs to satisfy simultaneously.

required
tol float

Convergence tolerance on the controlled-variable residuals.

1e-08
max_iter int

Newton iteration cap.

50

Returns:

Type Description
SpecResult

A SpecResult.

controller

controller(
    simulate: Simulate,
    *,
    manipulated: str,
    controlled: Measure,
    set_point: float,
    lo: float,
    hi: float,
    name: str = "",
) -> DesignSpec

Convenience constructor for a single set-point controller as a DesignSpec.

Reads as control language: drive controlled to set_point by moving manipulated within [lo, hi]. Combine several with solve_design.

optimize_flowsheet

optimize_flowsheet(
    simulate: Simulate,
    objective: Callable[
        [dict[str, Stream], Mapping[str, Any]], Array
    ],
    theta0: Mapping[str, Any],
    design_vars: Sequence[str],
    *,
    bounds: Mapping[str, tuple[float, float]] | None = None,
    ineq_constraints: Callable[[Mapping[str, Any]], Array]
    | None = None,
    method: str = "bfgs",
    tol: float = 1e-06,
    max_iter: int = 200,
) -> FlowsheetOptResult

Optimize selected design variables of a flowsheet against a cost objective.

Minimizes objective(simulate(theta), theta) over the design_vars subset of theta, holding the rest fixed. The flowsheet (recycles and all) is re-solved at every objective evaluation, and gradients flow through the converged flowsheet by implicit differentiation. With a money objective from fugacio.sim.economics this is end-to-end design optimization.

Parameters:

Name Type Description Default
simulate Simulate

Runs the flowsheet for a parameter mapping; returns named streams.

required
objective Callable[[dict[str, Stream], Mapping[str, Any]], Array]

Scalar cost objective(streams, theta) -> () to minimize.

required
theta0 Mapping[str, Any]

Base parameter mapping (seeds the design variables).

required
design_vars Sequence[str]

Keys of theta that the optimizer is free to move.

required
bounds Mapping[str, tuple[float, float]] | None

Optional {var: (lo, hi)} box bounds on the design variables.

None
ineq_constraints Callable[[Mapping[str, Any]], Array] | None

Optional g(theta) -> (k,) enforced <= 0 (e.g. a minimum-purity constraint as target - purity).

None
method str

Unconstrained inner method (ignored when bounds/constraints apply).

'bfgs'
tol float

Optimality tolerance.

1e-06
max_iter int

Iteration cap.

200

Returns:

Type Description
FlowsheetOptResult

A FlowsheetOptResult.