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 |
argmin |
The minimizer |
least_squares |
Solve |
OptimizeResult
¶
Bases: NamedTuple
Outcome of an optimization run.
Attributes:
| Name | Type | Description |
|---|---|---|
x |
Any
|
The optimal decision variable, in the pytree structure of |
fun |
Array
|
Objective value at |
grad_norm |
Array
|
Max-norm of the (projected) gradient at |
n_iter |
Array
|
Number of outer iterations taken. |
converged |
Array
|
Whether the optimality/feasibility tolerances were met. |
constraint_violation |
Array
|
Max constraint violation ( |
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 |
required |
x0
|
Any
|
Initial decision pytree (its structure defines the unknown). |
required |
theta
|
Any
|
Optional parameter pytree forwarded to |
None
|
method
|
str
|
Unconstrained inner method, one of |
'bfgs'
|
bounds
|
tuple[Any, Any] | None
|
Optional |
None
|
eq_constraints
|
Constraint | None
|
Optional |
None
|
ineq_constraints
|
Constraint | None
|
Optional |
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
|
to |
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 |
SpecResult |
Outcome of a design-spec solve. |
FlowsheetOptResult |
Outcome of a flowsheet optimization. |
Functions:
| Name | Description |
|---|---|
meet_spec |
Find the manipulated value |
solve_design |
Adjust the manipulated variables so every design spec meets its target. |
controller |
Convenience constructor for a single set-point controller as a |
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 |
measure |
Measure
|
Reads the controlled variable from the solved streams, e.g.
|
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 |
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 |
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 |
required |
target
|
ArrayLike
|
Desired value. |
required |
u0
|
ArrayLike
|
Initial guess for the manipulated variable. |
required |
theta
|
Any
|
Differentiable parameter pytree forwarded to |
None
|
lo
|
ArrayLike | None
|
Lower bracket bound; when both |
None
|
hi
|
ArrayLike | None
|
Upper bracket bound; when both |
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 |
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. |
required |
theta
|
Mapping[str, Any]
|
Base parameter mapping. The |
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 |
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 |
required |
theta0
|
Mapping[str, Any]
|
Base parameter mapping (seeds the design variables). |
required |
design_vars
|
Sequence[str]
|
Keys of |
required |
bounds
|
Mapping[str, tuple[float, float]] | None
|
Optional |
None
|
ineq_constraints
|
Callable[[Mapping[str, Any]], Array] | None
|
Optional |
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 |