Reliable solves and phase-preserving streams¶
A finite outlet temperature doesn't establish that a calculation converged. Fugacio provides numerical reports for roots, recycles, rigorous columns, equation-oriented flowsheets, energy-balanced units, and optimization. Use these reports when accepting a design or presenting a result.
Read a solve report¶
import jax.numpy as jnp
from fugacio.thermo import newton_system_with_info, require_converged
result = newton_system_with_info(
lambda x, target: x**2 - target,
jnp.array([1.0]),
jnp.array([4.0]),
lower=jnp.array([0.0]),
)
require_converged(result.report, "design equation", ("target relation",))
result.value # [2.0]
result.report.to_dict() # Strict JSON on the host
Reports contain a status, an iteration count, the maximum absolute scaled
residual, the last scaled step, and the index of the largest residual. A small
step alone doesn't establish convergence. Status codes distinguish convergence,
iteration limits, nonfinite values, stalled iterations, invalid inputs, and
infeasible specifications. ConvergenceError retains the report and calculation
context. Units that independently verify an energy balance report zero numerical
iterations for that verification; this isn't a count of their internal flash
iterations.
newton_system_with_info accepts characteristic variable and equation scales and
optional bounds. Its Newton search reduces the actual residual and keeps trial
variables within the bounds. Bounds aid the search; they don't replace any
original equation. The scalar bracketed_root_with_info checks both a valid
bracket and the resulting residual, so a discontinuity can't pass merely because
the bracket became narrow. Existing low-level value-only root functions retain
their compatibility behavior. Use the reporting variants to accept their results.
Results that carry a new report field should be accessed by attribute rather
than positional tuple unpacking. Stream also has an additional array leaf for
phase inventory. Checked flowsheet and column entry points now raise on concrete
convergence failures by default.
Keep a stream's phase state¶
Temperature and pressure don't determine the quality of a pure saturated fluid.
Stream therefore carries optional vapor component flows, vapor_n, in addition
to total component flows. Heater, valve, machine, flash, and rigorous-column
outputs retain their resolved phase inventories. Splitters scale both inventories.
Stream enthalpy, entropy, and volume use those inventories instead of repeating a
PT flash that would discard saturation quality.
from fugacio.sim import Stream, package_for, splitter
steam = package_for(["water"], "iapws")
wet = Stream.from_ph(
("water",), jnp.ones(1), 2.0, 1e5, 25000.0, model=steam,
)
a, b = splitter(wet, jnp.array([0.3, 0.7]))
wet.check()
Stream.from_ps accepts pressure and molar entropy. For known saturated endpoints,
Stream.from_fractions(..., phase="liquid") or phase="vapor" selects the branch.
The default constructor retains PT behavior. Empty streams have zero extensive
properties and use a finite trial composition during property evaluation.
Use the same property package and energy reference throughout an energy balance.
The stream doesn't store a property package. Named package factories bind the
component order and reject a mismatched stream. stream.reordered(components)
reorders material and phase inventories together. Packages constructed directly
from arrays can't infer component names; the caller must keep their order aligned.
Pure-fluid PH and PS flashes on mixture packages solve quality and temperature together inside the saturation region. The reference-fluid package uses its published PH and PS state functions. Equation-oriented pure-fluid streams use molar enthalpy as their thermal coordinate so that quality remains an unknown when saturation temperature is fixed.
Accept a flowsheet calculation¶
result = fs.solve_with_info(parameters, method="broyden")
result.check()
streams = result.streams
for name, report in result.reports.items():
print(name, report.to_dict())
Sequential flowsheet reports identify recycle blocks and invalid named streams.
fs.solve() checks those reports by default. solve_with_info retains best
iterates for diagnosis; a unit that can't close its own energy equation can still
raise a ConvergenceError before a complete flowsheet result exists.
EOFlowsheet.solve() returns an EOSolution with a report and checks concrete
failures by default. check=False exposes its best iterate. Errors identify the
responsible block equation or design specification. EO flash blocks impose zero
flow for an absent phase instead of imposing equifugacity on a phase that doesn't
exist. Rigorous-column errors identify stage material, equilibrium, or energy
rows and column specifications.
Heat exchangers retain their documented second-law cap for an infeasible request.
Their report marks a missed specification as infeasible, and result.check()
raises. This lets an interactive caller inspect the capped result explicitly.
The copilot checks this report and returns structured errors instead of using the
capped result as a successful specification. Tool inputs and outputs must be
strict JSON; nonfinite numbers cause an error.
Exchanger temperature curves preserve the supplied inlet states. Pressure drops are distributed linearly along each stream's flow direction, and each outlet's PH state closes the shared duty. Single-phase bulk properties differentiate the existing phase directly, so an absent phase's equilibrium derivative can't invalidate an otherwise regular property sensitivity.
Single-phase PH and PS results retain absent-phase trial compositions and K values for inspection, but those trial values don't carry derivatives. Present phase amounts follow the material balance, and energy sensitivities follow the existing phase's property equations.
Improve convergence and inspect specifications¶
EO solves reuse their most recent concrete converged state by default.
warm_start=False disables reuse, and guess=... supplies named stream guesses.
The property-package parameters remain dynamic inputs to the cached solve.
Sequential solves accept guess=previous.streams for their recycle seeds.
A rigorous column's warm_start() method returns the full stage state accepted
by its guess argument. If a direct NRTL column solve fails, the solver can
start at ideal activity and restore the activity parameters in increments.
homotopy=False disables this fallback. Each increment has its own Newton
iteration limit; the final report verifies the requested model's equations.
Both flowsheet engines support this host-side continuation method. Failed trials
reduce the step and retain the previous accepted state; successful steps advance
toward the requested endpoint. The result records every trial and its report.
If the endpoint isn't reached, the result isn't marked converged, even when the
last accepted intermediate point solved successfully. continuation_solve
accepts a custom solver callback for other problems. solve_path accepts
recycle methods and tolerances through solve_options.
Differentiate a separate endpoint solve, using the accepted state as its guess;
the adaptive path selection itself isn't differentiable.
EOFlowsheet.degrees_of_freedom() checks equation counts. A square system can
still be singular. EOFlowsheet.diagnose(parameters, guess=...) evaluates the
scaled Jacobian and reports numerical rank, conditioning, zero equation rows, and
unconstrained variables. This is a local numerical diagnostic at the supplied
state, not a global proof of structural solvability.
Differentiate only valid solutions¶
The shared vector Newton, fixed-point, and recycle solves use implicit JVP rules. JAX transposes the linearized residual solve for reverse derivatives; the adjoint doesn't repeat a possibly divergent fixed-point iteration. The rules support JVP, VJP, JIT, batching, and higher derivatives at smooth, nonsingular solutions. Nested thermodynamic models must also support the requested transformation.
argmin differentiates a fixed-size KKT system containing equalities, active
inequalities, and active bounds. Inactive multiplier equations have identity
rows, preventing inactive constraints from making the matrix singular. Optimizer
reports check constrained stationarity and feasibility. Failed optimization can't
supply a valid solution sensitivity.
Host-side checked calculations raise on failure. Inside compiled calculations, checked flowsheet values and failed implicit sensitivities become nonfinite, and reporting APIs retain the failure status. Carry the report through JIT and inspect it before accepting results. Phase transitions, active-set changes, redundant constraints, and singular roots can make a derivative undefined even when the primal residual is small. A successful residual check doesn't prove smoothness or global optimality.
Run the plant acceptance cases¶
The plant tests exercise public stream properties across unit boundaries. They check the converged process, including its external material and energy balances.
| Case | Acceptance checks |
|---|---|
| HDA with hydrogen recycle | Carbon and hydrogen closure, reaction formation enthalpy, furnace and cooler duties, pressure letdown, and benzene recovery. |
| Depropanizer with feed preheat | Purity and recovery specifications, column and exchanger energy closure, recycle closure, and heat-recovery sensitivity against finite differences. |
| Ethanol/water with NRTL | A rigorous column inside a converged economizer loop, product composition, heat recovery, and the whole-train energy balance. |
| Phase-changing water utility | Preserved quality through heating and letdown, condensation and pumping energy closure on PR and IAPWS, and an operating-point optimization checked against finite differences. |
uv run pytest packages/fugacio-sim/tests/test_plants.py
uv run pytest packages/fugacio-sim/tests/test_utility_reliability.py
These cases carry the plant marker and run in CI. Their nested JAX solvers can
take several minutes to compile on the first run. The HDA case uses a specified
conversion and an ideal component separator; its energy accounting includes the
heat required by that separator.