Equation-oriented flowsheeting¶
The equation-oriented (EO) engine assembles every unit's equations, the stream connectivity, the recycles, and any design specs into one residual system and solves it simultaneously by Newton's method, with the Jacobian supplied exactly by JAX autodiff and the converged solution differentiable by the implicit function theorem.
See the equation-oriented flowsheeting guide for worked examples. Import the public surface from the subpackage root:
Flowsheet, solution & DOF analysis¶
flowsheet
¶
Equation-oriented flowsheeting: solve the whole flowsheet as one system.
Where the sequential-modular engine (fugacio.sim.flowsheet) evaluates units in
order and converges recycles by tearing, the equation-oriented (EO) engine
collects every unit's equations, the stream connectivity, the recycles, and any
design specs into a single residual system F(x, theta) = 0 and solves it
simultaneously with Newton's method. There is no tear stream and no unit
ordering: a recycle is just a stream that two blocks happen to share, and the
global solve closes it like any other equation.
This is the formulation a differentiable core is built for. The one expensive
ingredient of an EO solver, the Jacobian dF/dx, is supplied exactly by JAX
autodiff rather than by finite differences or hand-coded analytic blocks, and the
converged solution is itself differentiable with respect to the parameters
theta (operating conditions, feeds, prices, model parameters) by the implicit
function theorem (fugacio.thermo.implicit.newton_system). So a gradient of any
product spec, duty, or cost through the entire converged plant, recycles and
all, costs a single adjoint solve.
The unknowns are the flowsheet's internal streams (per-component molar flows plus
temperature and pressure) together with any block auxiliary variables and any
freed design-spec variables. Everything is carried in a non-dimensional form (see
fugacio.sim.eo.blocks.Scales) so the Newton system stays well conditioned.
EOFlowsheet.degrees_of_freedom reports the unknown/equation balance, the EO
analogue of a specification check.
Classes:
| Name | Description |
|---|---|
DOFReport |
Degrees-of-freedom analysis of an EO flowsheet. |
EOSolution |
Converged equation-oriented flowsheet solution. |
EOFlowsheet |
Declarative equation-oriented flowsheet. |
DOFReport
¶
Bases: NamedTuple
Degrees-of-freedom analysis of an EO flowsheet.
Attributes:
| Name | Type | Description |
|---|---|---|
n_unknowns |
int
|
Total scalar unknowns (internal stream variables + block auxiliaries + freed design-spec variables). |
n_equations |
int
|
Total scalar equations (block residuals + design specs). |
degrees_of_freedom |
int
|
|
per_block |
dict[str, int]
|
Equation count contributed by each block (keyed by the block's first outlet name). |
EOSolution
dataclass
¶
EOSolution(
streams: dict[str, Stream],
aux: dict[str, Array],
specs: dict[str, Array],
residual_norm: Array,
n_unknowns: int,
n_equations: int,
)
Converged equation-oriented flowsheet solution.
Attributes:
| Name | Type | Description |
|---|---|---|
streams |
dict[str, Stream]
|
All named streams (feeds plus solved internal streams). |
aux |
dict[str, Array]
|
Solved block auxiliary variables (e.g. isentropic temperatures). |
specs |
dict[str, Array]
|
Solved values of any freed design-spec manipulated variables. |
residual_norm |
Array
|
Max-norm of the (scaled) residual at the solution. |
n_unknowns |
int
|
Number of scalar unknowns solved. |
n_equations |
int
|
Number of scalar equations. |
EOFlowsheet
dataclass
¶
EOFlowsheet(
eos: CubicEOS = PR,
kij: Array | None = None,
scales: Scales | None = None,
feeds: dict[str, Stream] = dict(),
blocks: list[Block] = list(),
specs: list[_DesignSpec] = list(),
)
Declarative equation-oriented flowsheet.
Register feeds and blocks, optionally add design specs, then call solve.
Streams are referenced by name; a block's outlet names become the system
unknowns and recycles need no special handling (just reuse a downstream
stream name as an upstream block's inlet).
Example::
fs = EOFlowsheet(eos=PR)
fs.feed("fresh", fresh_stream)
fs.add(Mixer(inlets=("fresh", "recycle"), outlets=("mixed",)))
fs.add(Flash(inlets=("mixed",), outlets=("vapor", "liquid"), t="T", p="P"))
fs.add(Splitter(inlets=("liquid",), outlets=("recycle", "purge"),
fractions=("r_recycle",)))
sol = fs.solve({"T": 320.0, "P": 20e5, "r_recycle": [0.5, 0.5]})
product = sol["vapor"]
Attributes:
| Name | Type | Description |
|---|---|---|
eos |
CubicEOS
|
Cubic equation of state used everywhere in the flowsheet. |
kij |
Array | None
|
Optional binary-interaction matrix. |
scales |
Scales | None
|
Residual/variable scales (auto-derived from the feeds by
|
Methods:
| Name | Description |
|---|---|
feed |
Register a fresh feed stream by name. Returns |
add |
Register a unit block. Returns |
spec |
Add a design spec: free parameter |
degrees_of_freedom |
Report the unknown/equation balance for the flowsheet (see |
solve |
Solve the flowsheet simultaneously and return all streams. |
feed
¶
feed(name: str, stream: Stream) -> EOFlowsheet
Register a fresh feed stream by name. Returns self for chaining.
spec
¶
spec(
manipulated: str,
measure: Measure,
target: float | Array,
*,
init: float | Array,
) -> EOFlowsheet
Add a design spec: free parameter manipulated to hit measure = target.
In EO form a design spec is simply one more equation (measure(streams) -
target = 0) and one more unknown (the freed value of manipulated,
seeded at init), solved simultaneously with the flowsheet, so coupled
specs converge together with no nested loop.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
manipulated
|
str
|
A parameter key read by some block (the degree of
freedom). Its value becomes an unknown; the value passed in
|
required |
measure
|
Measure
|
Reads the controlled variable from the solved streams. |
required |
target
|
float | Array
|
Desired value of the controlled variable. |
required |
init
|
float | Array
|
Initial guess for the manipulated variable. |
required |
Returns:
| Type | Description |
|---|---|
EOFlowsheet
|
|
degrees_of_freedom
¶
degrees_of_freedom() -> DOFReport
Report the unknown/equation balance for the flowsheet (see DOFReport).
solve
¶
solve(
params: Mapping[str, Any] | None = None,
*,
guess: Mapping[str, Stream] | None = None,
sweeps: int = 6,
tol: float = 1e-10,
max_iter: int = 60,
check_dof: bool = True,
) -> EOSolution
Solve the flowsheet simultaneously and return all streams.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
params
|
Mapping[str, Any] | None
|
Parameter mapping read by blocks (operating conditions, split fractions, ...). Gradients with respect to these (and the feeds) flow through the converged solution by implicit differentiation. |
None
|
guess
|
Mapping[str, Stream] | None
|
Optional initial guesses for specific internal streams (useful to seed a recycle); other streams get a default interior seed. |
None
|
sweeps
|
int
|
Forward sweeps used to build the initial guess. |
6
|
tol
|
float
|
Newton convergence tolerance on the scaled step. |
1e-10
|
max_iter
|
int
|
Newton iteration cap. |
60
|
check_dof
|
bool
|
If |
True
|
Returns:
| Type | Description |
|---|---|
EOSolution
|
An |
EOSolution
|
and the feeds. |
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
Unit blocks¶
blocks
¶
Equation-oriented unit blocks: each unit as a residual contribution.
A sequential-modular unit (fugacio.sim.units) is an explicit function of its
inlet streams: it computes outlets, internally converging any flash or
isentropic solve. An equation-oriented (EO) block is the same physics written as
residual equations instead. Its outlet streams are unknowns of a global system,
and the block contributes the equations that those unknowns must satisfy
(material balances, an energy balance, phase-equilibrium equifugacity, a
pressure spec, ...). The whole flowsheet, recycles and design specs included, is
then one residual system solved simultaneously by Newton's method, with the
Jacobian supplied exactly by JAX autodiff (see fugacio.sim.eo.flowsheet).
Every block carries two complementary methods:
Block.residualsreturns the block's residual vector for the EO solve. The residuals are scaled (material by a flow scale, energy by an enthalpy scale, pressures by a pressure scale, equifugacity left dimensionless) so the global Newton system is well conditioned regardless of the unit system.Block.forwardevaluates the unit explicitly by reusing the corresponding sequential-modular unit operation. It is used to build a high-quality initial guess for the EO solve (a few forward sweeps) and lets a test cross-check the two formulations against each other.
The blocks mirror the sequential-modular units one-for-one, so the same physics backs both engines and the EO solution must equal the sequential-modular solution on any flowsheet both can express (the central differential test for this layer).
Classes:
| Name | Description |
|---|---|
Scales |
Characteristic scales that non-dimensionalise the EO residual system. |
Context |
Static (non-differentiated) data shared by every block during a solve. |
Block |
Base class for an equation-oriented unit block. |
Mixer |
Adiabatic (or isothermal) mixer: combine inlet streams into one outlet. |
Splitter |
Flow splitter: one inlet to several outlets sharing composition and state. |
Heater |
Heater/cooler on a temperature or a duty specification. |
Valve |
Isenthalpic (Joule-Thomson) pressure letdown to |
Pump |
Incompressible-liquid pump to |
Compressor |
Isentropic compressor to |
Turbine |
Isentropic turbine (expander) to |
Flash |
Isothermal-isobaric two-phase flash: one inlet to vapour and liquid outlets. |
ComponentSeparator |
Idealised separator with a per-component recovery to the top product. |
Functions:
| Name | Description |
|---|---|
resolve |
Resolve a |
Scales
dataclass
¶
Scales(
flow: float = 1.0,
temperature: float = 100.0,
pressure: float = 100000.0,
enthalpy_flow: float = 10000.0,
enthalpy_molar: float = 10000.0,
entropy_molar: float = 10.0,
)
Characteristic scales that non-dimensionalise the EO residual system.
The unknowns and residuals are divided by these so the Newton system is
well conditioned: molar flows and material balances by flow, temperatures
and temperature specs by temperature, pressures and pressure specs by
pressure, total-enthalpy balances by enthalpy_flow, molar-enthalpy
balances by enthalpy_molar, and molar-entropy relations by
entropy_molar. Phase-equilibrium (equifugacity) residuals are already
dimensionless and are left unscaled.
Attributes:
| Name | Type | Description |
|---|---|---|
flow |
float
|
Characteristic molar flow (mol/s). |
temperature |
float
|
Characteristic temperature (K). |
pressure |
float
|
Characteristic pressure (Pa). |
enthalpy_flow |
float
|
Characteristic total enthalpy flow (W). |
enthalpy_molar |
float
|
Characteristic molar enthalpy (J/mol). |
entropy_molar |
float
|
Characteristic molar entropy (J/mol/K). |
Context
dataclass
¶
Context(
components: tuple[str, ...],
eos: CubicEOS = PR,
kij: Array | None = None,
scales: Scales = Scales(),
)
Static (non-differentiated) data shared by every block during a solve.
Attributes:
| Name | Type | Description |
|---|---|---|
components |
tuple[str, ...]
|
The flowsheet's component names (shared by all streams). |
eos |
CubicEOS
|
Cubic equation of state used for every equilibrium / property call. |
kij |
Array | None
|
Optional binary-interaction matrix for the EOS. |
scales |
Scales
|
Residual / variable scales (see |
Block
dataclass
¶
Base class for an equation-oriented unit block.
A block names its inlet and outlet streams (by the keys used in the flowsheet) and supplies the equations relating them. Outlet streams are unknowns of the global EO system; each block "defines" its outlet streams by contributing exactly enough residual equations.
Attributes:
| Name | Type | Description |
|---|---|---|
inlets |
tuple[str, ...]
|
Inlet stream names (must already exist as feeds or other blocks' outlets). |
outlets |
tuple[str, ...]
|
Outlet stream names defined by this block. |
Methods:
| Name | Description |
|---|---|
aux_scales |
Auxiliary unknowns introduced by the block, mapped to their scale. |
n_residuals |
Number of residual equations this block contributes. |
aux_init |
Initial values for the block's auxiliary unknowns (unscaled). |
forward |
Evaluate the unit explicitly, returning its outlet streams. |
residuals |
Scaled residual vector for the block (length |
aux_scales
¶
Auxiliary unknowns introduced by the block, mapped to their scale.
Most blocks have none. A block with an internal implicit variable (for
example a compressor's isentropic outlet temperature) declares it here so
the flowsheet allocates an unknown and the block can add its defining
equation in residuals.
aux_init
¶
aux_init(
streams: Mapping[str, Stream],
params: Mapping[str, Any],
ctx: Context,
) -> dict[str, Array]
Initial values for the block's auxiliary unknowns (unscaled).
forward
¶
forward(
streams: Mapping[str, Stream],
params: Mapping[str, Any],
ctx: Context,
) -> dict[str, Stream]
Evaluate the unit explicitly, returning its outlet streams.
Reuses the sequential-modular unit operation, so it is the reference the EO residuals are built to reproduce. Used to seed the EO solve.
Mixer
dataclass
¶
Mixer(
inlets: tuple[str, ...],
outlets: tuple[str, ...],
t: Spec | None = None,
p: Spec | None = None,
)
Bases: Block
Adiabatic (or isothermal) mixer: combine inlet streams into one outlet.
Flows add component-by-component. With t unset the outlet temperature is
set by an adiabatic energy balance (total enthalpy in equals total enthalpy
out); set t to fix the outlet temperature. The outlet pressure defaults to
the lowest inlet pressure, or is fixed by p.
Attributes:
| Name | Type | Description |
|---|---|---|
t |
Spec | None
|
Optional outlet temperature spec (literal or parameter key); |
p |
Spec | None
|
Optional outlet pressure spec; |
Methods:
| Name | Description |
|---|---|
n_residuals |
Material (per component) + pressure + energy: |
forward |
Evaluate via |
residuals |
Material balance, pressure spec, and the (adiabatic or fixed-T) energy balance. |
Splitter
dataclass
¶
Bases: Block
Flow splitter: one inlet to several outlets sharing composition and state.
Attributes:
| Name | Type | Description |
|---|---|---|
fractions |
Spec
|
Per-outlet split fractions (literal sequence/array or a
parameter key), one per name in |
Methods:
| Name | Description |
|---|---|
n_residuals |
|
forward |
Evaluate via |
residuals |
Each outlet carries its split fraction of the feed at the feed's T and P. |
Heater
dataclass
¶
Heater(
inlets: tuple[str, ...],
outlets: tuple[str, ...],
t_out: Spec | None = None,
duty: Spec | None = None,
dp: Spec = 0.0,
)
Bases: Block
Heater/cooler on a temperature or a duty specification.
Provide exactly one of t_out (outlet temperature) or duty (signed
heat added, W). dp is the pressure drop across the block.
Attributes:
| Name | Type | Description |
|---|---|---|
t_out |
Spec | None
|
Outlet temperature spec, or |
duty |
Spec | None
|
Heat-duty spec (W), or |
dp |
Spec
|
Pressure drop (Pa). |
Methods:
| Name | Description |
|---|---|
n_residuals |
Material + pressure + energy: |
forward |
Evaluate via |
residuals |
Material, pressure drop, and the temperature or duty energy balance. |
Valve
dataclass
¶
Bases: Block
Isenthalpic (Joule-Thomson) pressure letdown to p_out.
Attributes:
| Name | Type | Description |
|---|---|---|
p_out |
Spec
|
Outlet pressure spec (literal or parameter key). |
Methods:
| Name | Description |
|---|---|
n_residuals |
Material + pressure + isenthalpic energy: |
forward |
Evaluate via |
residuals |
Material, outlet-pressure spec, and conserved molar enthalpy. |
Pump
dataclass
¶
Pump(
inlets: tuple[str, ...],
outlets: tuple[str, ...],
p_out: Spec = 100000.0,
efficiency: Spec = 0.75,
)
Bases: Block
Incompressible-liquid pump to p_out with an isentropic-equivalent efficiency.
Attributes:
| Name | Type | Description |
|---|---|---|
p_out |
Spec
|
Outlet pressure spec. |
efficiency |
Spec
|
Pump efficiency in |
Methods:
| Name | Description |
|---|---|
n_residuals |
Material + pressure + energy: |
forward |
Evaluate via |
residuals |
Material, outlet pressure, and the work-deposited enthalpy balance. |
Compressor
dataclass
¶
Compressor(
inlets: tuple[str, ...],
outlets: tuple[str, ...],
p_out: Spec = 100000.0,
efficiency: Spec = 0.75,
_is_turbine: bool = False,
)
Bases: _Machine
Isentropic compressor to p_out with an isentropic efficiency (< 1).
Turbine
dataclass
¶
Turbine(
inlets: tuple[str, ...],
outlets: tuple[str, ...],
p_out: Spec = 100000.0,
efficiency: Spec = 0.75,
_is_turbine: bool = True,
)
Bases: _Machine
Isentropic turbine (expander) to p_out with an isentropic efficiency (< 1).
Flash
dataclass
¶
Bases: Block
Isothermal-isobaric two-phase flash: one inlet to vapour and liquid outlets.
outlets must be (vapor_name, liquid_name). The block contributes the
component material balances, the equifugacity equilibrium relations
(phi_i^L x_i = phi_i^V y_i), and the temperature/pressure specs on both
product streams, the same equations the sequential-modular flash_drum
converges internally.
Attributes:
| Name | Type | Description |
|---|---|---|
t |
Spec
|
Drum temperature spec (literal or parameter key). |
p |
Spec
|
Drum pressure spec (literal or parameter key). |
Methods:
| Name | Description |
|---|---|
n_residuals |
Material + equilibrium + 2 T-specs + 2 P-specs: |
forward |
Evaluate via |
residuals |
Equifugacity + material balance + temperature/pressure specs on both phases. |
ComponentSeparator
dataclass
¶
ComponentSeparator(
inlets: tuple[str, ...],
outlets: tuple[str, ...],
split_to_top: Spec = 0.5,
top_t: Spec | None = None,
top_p: Spec | None = None,
bottom_t: Spec | None = None,
bottom_p: Spec | None = None,
)
Bases: Block
Idealised separator with a per-component recovery to the top product.
outlets must be (top_name, bottom_name). split_to_top is a
per-component fraction sent to the top; the remainder leaves in the bottom.
Attributes:
| Name | Type | Description |
|---|---|---|
split_to_top |
Spec
|
Per-component recovery to the top (sequence/array or key). |
top_t |
Spec | None
|
Optional top-product temperature (defaults to the feed's). |
top_p |
Spec | None
|
Optional top-product pressure (defaults to the feed's). |
bottom_t |
Spec | None
|
Optional bottom-product temperature (defaults to the feed's). |
bottom_p |
Spec | None
|
Optional bottom-product pressure (defaults to the feed's). |
Methods:
| Name | Description |
|---|---|
n_residuals |
Two fully-defined outlets: |
forward |
Evaluate via |
residuals |
Per-component split to the top/bottom with their temperature/pressure specs. |
resolve
¶
Resolve a Spec to a JAX array.
A string is looked up in params (so the value can be a differentiable
parameter or a design-spec unknown); anything else is treated as a literal.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
spec
|
Spec
|
A literal value or a key into |
required |
params
|
Mapping[str, Any]
|
The parameter mapping passed to the solver. |
required |
Returns:
| Type | Description |
|---|---|
Array
|
The resolved value as a float array. |
Raises:
| Type | Description |
|---|---|
KeyError
|
if |
Optimization¶
optimize
¶
Optimization over an equation-oriented flowsheet.
An EO flowsheet is differentiable end to end, so a design-optimization problem
("minimize the cost / maximize the yield over these operating variables") is a
smooth program the gradient-based optimizers in fugacio.sim.optimize solve
directly. Two equivalent formulations are offered:
- Nested (
simultaneous=False, the robust default): the decision variables parameterize the flowsheet, each objective evaluation solves the flowsheet to convergence (one Newton solve), and the optimizer descends the resulting reduced objective. The flowsheet solve is differentiable, so the reduced gradient is exact. - Simultaneous / full-space (
simultaneous=True): the decision variables and the flowsheet state are optimized together, with the flowsheet equations imposed as equality constraints (an augmented-Lagrangian solve). No inner loop converges the flowsheet; feasibility and optimality are reached at once, the classic equation-oriented optimization paradigm.
Both return the optimal decision and the converged flowsheet at that optimum.
Classes:
| Name | Description |
|---|---|
EOOptResult |
Outcome of an equation-oriented flowsheet optimization. |
Functions:
| Name | Description |
|---|---|
optimize_flowsheet_eo |
Optimize an EO flowsheet over named decision variables. |
EOOptResult
dataclass
¶
EOOptResult(
decision: dict[str, Array],
objective: Array,
solution: EOSolution,
converged: Array,
constraint_violation: Array,
)
Outcome of an equation-oriented flowsheet optimization.
Attributes:
| Name | Type | Description |
|---|---|---|
decision |
dict[str, Array]
|
Optimal value of each decision variable (keyed by name). |
objective |
Array
|
Objective value at the optimum. |
solution |
EOSolution
|
The converged |
converged |
Array
|
Whether the optimizer met its tolerances. |
constraint_violation |
Array
|
Max flowsheet-equation violation (0 for the nested formulation; the feasibility residual for the simultaneous one). |
optimize_flowsheet_eo
¶
optimize_flowsheet_eo(
flowsheet: EOFlowsheet,
objective: Objective,
decisions: Mapping[str, Decision],
*,
params: Mapping[str, Any] | None = None,
simultaneous: bool = False,
tol: float = 1e-06,
max_iter: int = 200,
solve_kwargs: Mapping[str, Any] | None = None,
) -> EOOptResult
Optimize an EO flowsheet over named decision variables.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
flowsheet
|
EOFlowsheet
|
The |
required |
objective
|
Objective
|
Scalar objective |
required |
decisions
|
Mapping[str, Decision]
|
Map of parameter key -> |
required |
params
|
Mapping[str, Any] | None
|
Fixed parameters merged under the decisions for every solve. |
None
|
simultaneous
|
bool
|
Use the full-space formulation (flowsheet equations as equality constraints) instead of the nested one. |
False
|
tol
|
float
|
Optimizer tolerance. |
1e-06
|
max_iter
|
int
|
Optimizer outer-iteration cap. |
200
|
solve_kwargs
|
Mapping[str, Any] | None
|
Extra keyword arguments forwarded to |
None
|
Returns:
| Type | Description |
|---|---|
EOOptResult
|
An |