Skip to content

Flowsheet & recycle

The Flowsheet container and the recycle/tear solver. Flowsheet.partition groups units into strongly connected blocks (Tarjan), orders them, and selects tear streams automatically. The standalone tear_solve converges a loop by Broyden (the default), Wegstein, or Newton and differentiates its fixed point. Flowsheet assembles local unit derivatives into the shared process graph's implicit system. Flowsheet.solve_with_info keeps every block's report, each unit's heat and work, and, with a model, a physical audit of every stream.

See the flowsheeting guide for worked examples.

flowsheet

Process construction, execution, and checked implicit sensitivities.

A Flowsheet builds a shared ProcessGraph from physical unit kernels and named streams. Sequential execution orders acyclic units and converges cyclic partitions by tearing. Simultaneous execution solves the connection equations with locally assembled sparse Newton steps. Both strategies differentiate the same converged graph using local unit Jacobians and a sparse implicit solve.

Concrete process iterations run on the host, preserving compiled unit kernels. An explicit enclosing JIT stages the orchestration. Unit reports, independent stream checks, and optional physical audits remain acceptance requirements. The standalone tear_solve functions also support arbitrary floating pytrees; their small fixed-point derivative uses a dense reference system.

Classes:

Name Description
Partition

One block of the flowsheet's sequential-modular calculation order.

TearResult

Recycle state and the independently checked fixed-point residual.

UnitRecord

Energy and solve evidence a unit returned alongside its outlets.

FlowsheetResult

Named streams with per-block convergence, unit evidence, and physical audits.

Flowsheet

A declarative flowsheet: named streams produced by connected units.

Functions:

Name Description
tear_solve_with_info

Solve a recycle, reporting its actual fixed-point residual.

tear_solve

Return a converged recycle, with implicit forward and reverse derivatives.

Attributes:

Name Type Description
TEAR_METHODS tuple[str, ...]

Recycle convergence methods accepted by tear_solve and Flowsheet.solve.

TEAR_METHODS module-attribute

TEAR_METHODS: tuple[str, ...] = (
    "wegstein",
    "broyden",
    "newton",
)

Recycle convergence methods accepted by tear_solve and Flowsheet.solve.

Partition dataclass

Partition(units: tuple[str, ...], tears: tuple[str, ...])

One block of the flowsheet's sequential-modular calculation order.

Attributes:

Name Type Description
units tuple[str, ...]

Unit names in evaluation order (a valid order once tears are treated as known).

tears tuple[str, ...]

Stream names torn to break this block's recycle loops (empty for an acyclic block).

cyclic property

cyclic: bool

Whether the block contains a recycle.

TearResult

Bases: NamedTuple

Recycle state and the independently checked fixed-point residual.

UnitRecord dataclass

UnitRecord(heat: Array, work: Array, report: SolveReport)

Energy and solve evidence a unit returned alongside its outlets.

Attributes:

Name Type Description
heat Array

Heat into the fluid (W); zero if the unit reports none.

work Array

Shaft work into the fluid (W); zero if the unit reports none.

report SolveReport

The unit's own solve report (a converged placeholder for units that return plain streams).

FlowsheetResult dataclass

FlowsheetResult(
    streams: dict[str, Stream],
    reports: dict[str, SolveReport],
    units: dict[str, UnitRecord] = dict(),
    audits: dict[str, Any] = dict(),
    unit_results: dict[str, Any] = dict(),
)

Named streams with per-block convergence, unit evidence, and physical audits.

Attributes:

Name Type Description
streams dict[str, Stream]

All feeds and computed streams.

reports dict[str, SolveReport]

Solve reports keyed by recycle:, unit:, or stream: name.

units dict[str, UnitRecord]

Heat, work, and report retained from each unit's result.

audits dict[str, Any]

Physical acceptance of each stream (empty without a model).

unit_results dict[str, Any]

Complete unit pytrees when retain_results=True.

Methods:

Name Description
check

Raise with the responsible block's name if any calculation failed.

converged property

converged: Array

Whether every recycle, unit, and stream-structure check succeeded.

accepted property

accepted: Array

Converged, and every non-empty audited stream passed physical acceptance.

Without audits (no flowsheet model), this equals converged: no physical acceptance is claimed that wasn't checked.

report property

report: SolveReport

The first failed block report, or the largest converged residual.

check

check() -> None

Raise with the responsible block's name if any calculation failed.

Raises:

Type Description
ConvergenceError

For a failed recycle, unit, or stream structure.

PhysicalAcceptanceError

For an audited stream that failed physical acceptance (for example, a vapor-liquid state that is actually liquid-liquid unstable).

Flowsheet dataclass

Flowsheet(
    feeds: dict[str, Stream] = dict(),
    units: list[ProcessUnit] = list(),
    tears: dict[str, Stream] = dict(),
    model: Any = None,
    _maps: dict[Partition, Any] = dict(),
    _graphs: dict[Any, CompiledGraph] = dict(),
)

A declarative flowsheet: named streams produced by connected units.

Build a flowsheet by registering feeds and units, then call solve. Each unit is a plain function of its input streams (and the shared theta) returning one or more output streams. The flowsheet works out the calculation order itself: partition groups the units into strongly connected blocks, orders the blocks, and chooses tear streams inside every recycle loop. tear optionally designates a tear by hand together with its starting guess; automatically selected tears are seeded by one pass of the loop with an empty recycle.

Example::

fs = Flowsheet()
fs.feed("fresh", fresh_stream)
fs.unit("mixer", lambda fresh, rec, th: mix([fresh, rec]),
        inputs=("fresh", "recycle"), outputs=("mixed",))
fs.unit("drum", lambda mixed, th: flash_drum(mixed, th["T"], th["P"]),
        inputs=("mixed",), outputs=("vapor", "liquid"))
fs.unit("split", lambda liq, th: splitter(liq, [th["r"], 1 - th["r"]]),
        inputs=("liquid",), outputs=("recycle", "purge"))
streams = fs.solve({"T": 320.0, "P": 2e6, "r": 0.6})
product = streams["vapor"]

Methods:

Name Description
feed

Register a fresh feed stream by name. Returns self for chaining.

unit

Register a unit fn(*input_streams, theta) -> output stream(s).

tear

Designate stream name as a recycle tear with an initial guess.

partition

Validate connectivity and choose recycle partitions in the shared graph.

solve_with_info

Solve every block and retain reports, unit evidence, and stream audits.

compile

Reuse structural layouts without retaining a numerical solution.

solve

Return named streams after checked recycle convergence and acceptance.

solve_path

Continue between operating conditions with accepted recycle warm starts.

Attributes:

Name Type Description
graph ProcessGraph

The canonical graph used by every execution strategy.

graph property

graph: ProcessGraph

The canonical graph used by every execution strategy.

feed

feed(name: str, stream: Stream) -> Flowsheet

Register a fresh feed stream by name. Returns self for chaining.

unit

unit(
    name: str,
    fn: UnitFn,
    *,
    inputs: Sequence[str],
    outputs: Sequence[str],
) -> Flowsheet

Register a unit fn(*input_streams, theta) -> output stream(s).

fn receives the named input streams positionally followed by the shared theta pytree, and returns either a single Stream (for one output name) or a tuple/list of streams aligned with outputs.

tear

tear(name: str, guess: Stream) -> Flowsheet

Designate stream name as a recycle tear with an initial guess.

Hand-designated tears are honoured by partition (and supplemented if they do not break every loop). Designating a tear is also the way to provide a starting guess for a stream whose component list differs from the feeds', which the automatic seed cannot infer.

partition

partition() -> list[Partition]

Validate connectivity and choose recycle partitions in the shared graph.

solve_with_info

solve_with_info(
    theta: Any = None,
    *,
    guess: Mapping[str, Stream] | None = None,
    audit: bool = True,
    strategy: str = "sequential",
    linear_solver: str = "sparse",
    retain_results: bool = False,
    **tear_solve_kwargs: Any,
) -> FlowsheetResult

Solve every block and retain reports, unit evidence, and stream audits.

Failed blocks retain their best iterates for diagnosis. Use result.check() at an application boundary, or inspect result.converged and result.accepted inside JIT. With a flowsheet model and audit, every stream is audited for physical acceptance after convergence. strategy selects sequential or simultaneous on the same graph. linear_solver="dense" explicitly selects the small reference solver; the default sparse CPU solver never falls back to a dense plant matrix.

compile

compile(
    streams: Mapping[str, Stream],
    *,
    linear_solver: str = "sparse",
) -> CompiledGraph

Reuse structural layouts without retaining a numerical solution.

solve

solve(
    theta: Any = None,
    *,
    check: bool = True,
    **tear_solve_kwargs: Any,
) -> dict[str, Stream]

Return named streams after checked recycle convergence and acceptance.

Eager failures identify the responsible block, unit, or stream. Compiled failures produce nonfinite streams; :meth:solve_with_info retains the reports and best iterates for inspection. Set check=False to return best iterates.

Raises:

Type Description
ConvergenceError

If an eager block, unit, or stream check fails.

PhysicalAcceptanceError

If an audited stream fails acceptance.

solve_path

solve_path(
    start: Any,
    target: Any,
    *,
    solve_options: Mapping[str, Any] | None = None,
    **options: Any,
) -> Any

Continue between operating conditions with accepted recycle warm starts.

options are forwarded to :func:continuation_solve. Configure the recycle method and tolerances through solve_options.

tear_solve_with_info

tear_solve_with_info(
    g: Callable[[Any, Any], Any],
    tear0: Any,
    theta: Any = None,
    *,
    method: str = "broyden",
    q_min: float = -5.0,
    q_max: float = 0.0,
    tol: float = 1e-10,
    atol: float = 1e-12,
    max_iter: int = 200,
) -> TearResult

Solve a recycle, reporting its actual fixed-point residual.

g(tear, theta) and tear0 must have identical pytree structures. Both JVPs and VJPs solve the linearized fixed-point equations independently of the forward acceleration. An unconverged iterate has nonfinite sensitivities. q_max may be raised toward one to damp oscillatory maps.

Parameters:

Name Type Description Default
g Callable[[Any, Any], Any]

One flowsheet pass.

required
tear0 Any

Starting recycle state, as any floating-point pytree.

required
theta Any

Differentiable operating parameters and upstream streams.

None
method str

Broyden (default), Wegstein, or Newton.

'broyden'
q_min float

Minimum Wegstein acceleration factor.

-5.0
q_max float

Maximum Wegstein acceleration factor.

0.0
tol float

Scaled fixed-point residual tolerance.

1e-10
atol float

Absolute floor added to the per-variable scaling.

1e-12
max_iter int

Forward iteration cap.

200

Returns:

Type Description
TearResult

The best recycle state and its solve report.

Raises:

Type Description
ValueError

For an unknown method or invalid numerical options.

tear_solve

tear_solve(
    g: Callable[[Any, Any], Any],
    tear0: Any,
    theta: Any = None,
    *,
    method: str = "broyden",
    q_min: float = -5.0,
    q_max: float = 0.0,
    tol: float = 1e-10,
    atol: float = 1e-12,
    max_iter: int = 200,
    check: bool = True,
) -> Any

Return a converged recycle, with implicit forward and reverse derivatives.

Eager calls raise on failure by default. Under JAX transforms the failed value is nonfinite; use :func:tear_solve_with_info to retain diagnostics and the best iterate. check=False also returns that best iterate.

Raises:

Type Description
ConvergenceError

If an eager calculation does not converge.

ValueError

If numerical options are invalid.

Process graph

See the shared runtime guide.

graph

Canonical process connectivity and independently linearized unit equations.

A graph contains unit kernels and their ports. Both sequential and simultaneous execution use this graph. Its implicit derivative assembles local unit blocks; it never differentiates a complete recycle iteration or a nested plant solve.

Classes:

Name Description
ProcessUnit

One physical kernel and its named material ports.

Partition

One block of the flowsheet's sequential-modular calculation order.

ProcessGraph

Immutable connectivity shared by process construction and execution.

CompiledGraph

Fixed process layout with local residual and derivative assembly.

SpecifiedGraph

Connection equations with a small border of bounded design variables.

ResidualGraph

Locally assembled equations for custom residual units and specifications.

Functions:

Name Description
connection_edges

Validate unbound port declarations using the canonical graph contract.

stream_vector

Pack material, temperature, pressure, and resolved vapor inventory.

vector_stream

Reconstruct a stream from its material and phase coordinates.

unit_outlets

Read the shared unit-result contract, including plain stream functions.

ProcessUnit dataclass

ProcessUnit(
    name: str,
    fn: Callable[..., Any],
    inputs: tuple[str, ...],
    outputs: tuple[str, ...],
)

One physical kernel and its named material ports.

Partition dataclass

Partition(units: tuple[str, ...], tears: tuple[str, ...])

One block of the flowsheet's sequential-modular calculation order.

Attributes:

Name Type Description
units tuple[str, ...]

Unit names in evaluation order (a valid order once tears are treated as known).

tears tuple[str, ...]

Stream names torn to break this block's recycle loops (empty for an acyclic block).

cyclic property

cyclic: bool

Whether the block contains a recycle.

ProcessGraph dataclass

ProcessGraph(
    feeds: tuple[str, ...],
    units: tuple[ProcessUnit, ...],
    preferred_tears: tuple[str, ...] = (),
)

Immutable connectivity shared by process construction and execution.

Kernels receive input streams followed by a dynamic parameter pytree. Numerical values and property-package coefficients aren't graph metadata.

Methods:

Name Description
edges

Validate ownership and return declared stream dependencies.

partition

Order strongly connected components and select their tear streams.

compile

Bind component layouts without capturing operating values.

edges

edges() -> list[Edge]

Validate ownership and return declared stream dependencies.

partition

partition() -> list[Partition]

Order strongly connected components and select their tear streams.

compile

compile(
    streams: Mapping[str, Stream],
    *,
    linear_solver: str = "sparse",
) -> CompiledGraph

Bind component layouts without capturing operating values.

CompiledGraph

CompiledGraph(
    graph: ProcessGraph,
    streams: Mapping[str, Stream],
    *,
    linear_solver: str = "sparse",
)

Fixed process layout with local residual and derivative assembly.

Each unit linearizes once at the current point. Input-coordinate directions are applied sequentially so a column's stage tangent isn't batched by the number of plant variables. Sparse storage follows the declared connections. Instances contain structure only and never cache numerical operating points.

Methods:

Name Description
indices

Scalar coordinates of a sequence of named internal streams.

pack

Collect internal stream coordinates in deterministic unit order.

unpack

Restore all streams, retaining the supplied differentiable feeds.

residual

The connection equations, evaluated through independent unit kernels.

local_linearization

Assemble unit Jacobians and a parameter direction from shared local data.

linearize

Select sparse or explicit dense algebra over the same local assembly.

attach

Attach assembled implicit derivatives to a detached converged state.

diagnose

Describe actual storage, scalar labels, and graph incidence.

indices

indices(names: Sequence[str]) -> tuple[int, ...]

Scalar coordinates of a sequence of named internal streams.

pack

pack(streams: Mapping[str, Stream]) -> Array

Collect internal stream coordinates in deterministic unit order.

unpack

unpack(
    value: Array, feeds: Mapping[str, Stream]
) -> dict[str, Stream]

Restore all streams, retaining the supplied differentiable feeds.

residual

residual(value: Array, parameters: Any) -> Array

The connection equations, evaluated through independent unit kernels.

local_linearization

local_linearization(
    value: Array, parameters: Any, direction: Any = None
) -> tuple[Any, Array]

Assemble unit Jacobians and a parameter direction from shared local data.

linearize

linearize(
    value: Array, parameters: Any, direction: Any = None
) -> tuple[Any, Array]

Select sparse or explicit dense algebra over the same local assembly.

attach

attach(
    value: Array, parameters: Any, valid: Array
) -> Array

Attach assembled implicit derivatives to a detached converged state.

diagnose

diagnose() -> dict[str, Any]

Describe actual storage, scalar labels, and graph incidence.

SpecifiedGraph

SpecifiedGraph(
    graph: CompiledGraph,
    count: int,
    bind: Callable[..., Any],
    equations: Callable[..., Array],
)

Connection equations with a small border of bounded design variables.

bind(aux, parameters) supplies the graph's dynamic parameters and feeds. equations(streams, bound) evaluates specification residuals at that state. It mustn't solve the process. Unit physics remains in the graph; design metrics add rows and manipulated variables add columns.

Methods:

Name Description
specification_residual

Evaluate the border equations using the current connection state.

residual

Evaluate one square process and design-specification system.

linearize

Assemble local unit blocks and the small specification border.

diagnose

Describe the augmented state, including every specification border.

attach

Attach the full bounded specification system's implicit derivative.

specification_residual

specification_residual(
    value: Array, parameters: Any
) -> Array

Evaluate the border equations using the current connection state.

residual

residual(value: Array, parameters: Any) -> Array

Evaluate one square process and design-specification system.

linearize

linearize(
    value: Array, parameters: Any, direction: Any = None
) -> tuple[Any, Array]

Assemble local unit blocks and the small specification border.

diagnose

diagnose(specifications: Sequence[str]) -> dict[str, Any]

Describe the augmented state, including every specification border.

attach

attach(
    value: Array, parameters: Any, valid: Array
) -> Array

Attach the full bounded specification system's implicit derivative.

ResidualGraph

ResidualGraph(
    size: int,
    blocks: Sequence[
        tuple[tuple[int, ...], int, Callable[..., Array]]
    ],
    *,
    linear_solver: str = "sparse",
)

Locally assembled equations for custom residual units and specifications.

Each block receives only its declared scalar inputs and the dynamic process parameters. Incidence is a structural contract, never inferred from values. Compiled unit boundaries are retained in primal and derivative evaluation.

Methods:

Name Description
residual

Evaluate independently compiled residual blocks.

linearize

Assemble sparse local Jacobians and one parameter right-hand side.

attach

Attach implicit derivatives assembled from the same residual blocks.

residual

residual(value: Array, parameters: Any) -> Array

Evaluate independently compiled residual blocks.

linearize

linearize(
    value: Array, parameters: Any, direction: Any = None
) -> tuple[Any, Array]

Assemble sparse local Jacobians and one parameter right-hand side.

attach

attach(
    value: Array, parameters: Any, valid: Array
) -> Array

Attach implicit derivatives assembled from the same residual blocks.

connection_edges

connection_edges(
    feeds: Sequence[str],
    connections: Sequence[
        tuple[str, tuple[str, ...], tuple[str, ...]]
    ],
    preferred_tears: Sequence[str] = (),
) -> list[Edge]

Validate unbound port declarations using the canonical graph contract.

stream_vector

stream_vector(stream: Stream) -> Array

Pack material, temperature, pressure, and resolved vapor inventory.

Keeping both inventories preserves pure-fluid saturation quality without embedding an extra PH flash in every process connection.

vector_stream

vector_stream(
    vector: Array, components: tuple[str, ...]
) -> Stream

Reconstruct a stream from its material and phase coordinates.

unit_outlets

unit_outlets(result: Any) -> tuple[Stream, ...]

Read the shared unit-result contract, including plain stream functions.

numerics

Process-level sparse algebra and orchestration with bounded compilation.

Unit physics stays in compiled JAX kernels. Concrete process iterations run on the host; an enclosing JIT stages the same iteration with lax.while_loop. Sparse LU runs on the CPU and differentiates its matrix equation, including transpose solves and higher derivatives. No process-sized dense fallback is allocated when a sparse factorization fails.

Classes:

Name Description
SparseJacobian

A square coordinate matrix with static incidence and dynamic coefficients.

Functions:

Name Description
clear_factor_cache

Release retained sparse LU factors without clearing compiled unit kernels.

newton_iterations

Converge scaled equations without compiling the complete process.

SparseJacobian dataclass

SparseJacobian(
    values: Array,
    rows: tuple[int, ...],
    columns: tuple[int, ...],
    size: int,
)

A square coordinate matrix with static incidence and dynamic coefficients.

Duplicate entries are summed. Storage is proportional to declared nonzeros. to_dense is an explicit diagnostic/reference operation only. The CPU solve transfers coefficients when used on an accelerator; it never claims an accelerator-native sparse factorization.

Methods:

Name Description
matvec

Apply the matrix to a vector or a batch of right-hand sides.

transpose

Return the transpose without moving or duplicating coefficient storage.

scaled

Apply equation and variable scaling without densifying.

to_dense

Materialize an explicitly requested small reference matrix.

solve_with_info

Solve and check each RHS against the original matrix equation.

solve

Return a checked implicit solve, with NaN on failure.

matvec

matvec(vector: Array) -> Array

Apply the matrix to a vector or a batch of right-hand sides.

transpose

transpose() -> SparseJacobian

Return the transpose without moving or duplicating coefficient storage.

scaled

scaled(rows: Array, columns: Array) -> SparseJacobian

Apply equation and variable scaling without densifying.

to_dense

to_dense() -> Array

Materialize an explicitly requested small reference matrix.

solve_with_info

solve_with_info(rhs: Array) -> LinearResult

Solve and check each RHS against the original matrix equation.

LU pivots carry no derivative. JAX differentiates A x = b and uses the explicit transpose solve for adjoints. A singular or inaccurate result is nonfinite and retains independent linear residual evidence.

solve

solve(rhs: Array) -> Array

Return a checked implicit solve, with NaN on failure.

clear_factor_cache

clear_factor_cache() -> None

Release retained sparse LU factors without clearing compiled unit kernels.

newton_iterations

newton_iterations(
    residual: Callable[[Array, Any], Array],
    jacobian: Callable[[Array, Any], Any],
    start: Array,
    parameters: Any,
    *,
    scale: Array | None = None,
    tolerance: float = 1e-09,
    max_iterations: int = 100,
    lower: Array | None = None,
    upper: Array | None = None,
) -> SolveResult

Converge scaled equations without compiling the complete process.

jacobian supplies a structured matrix. Bounds restrict trial points; residual-decreasing backtracking retains the best state on failure. This primal routine attaches no derivative to its iteration history.

Continuation

Adaptive continuation reuses accepted states while moving between operating conditions. See the reliability guide for examples and the distinction between a converged endpoint and a partially completed path.

continuation

Adaptive operating-condition continuation with accepted-state warm starts.

Continuation is a host orchestration algorithm. Each point can use a compiled, implicitly differentiable solver. Failed trials never replace the accepted seed.

Classes:

Name Description
ContinuationStep

One attempted point on the path, including rejected trials.

ContinuationResult

Last accepted solution and the complete attempted path.

Functions:

Name Description
continuation_solve

Move between parameter pytrees, reducing the step after a failed solve.

ContinuationStep

Bases: NamedTuple

One attempted point on the path, including rejected trials.

ContinuationResult dataclass

ContinuationResult(
    value: Any,
    progress: float,
    report: SolveReport,
    steps: tuple[ContinuationStep, ...],
)

Last accepted solution and the complete attempted path.

progress == 1 and a converged report are both required for success. A failed path's value belongs to its last accepted operating point.

Methods:

Name Description
check

Raise if the requested endpoint wasn't reached.

check

check() -> None

Raise if the requested endpoint wasn't reached.

continuation_solve

continuation_solve(
    solve: Callable[[Any, Any], tuple[Any, SolveReport]],
    start: Any,
    target: Any,
    *,
    guess: Any = None,
    initial_step: float = 0.25,
    min_step: float = 0.0001,
    max_steps: int = 100,
    check: bool = True,
) -> ContinuationResult

Move between parameter pytrees, reducing the step after a failed solve.

solve(parameters, previous_value) returns a value and its report. It should return failed reports or raise ConvergenceError for numerical failures. Other exceptions propagate, since programming errors aren't convergence failures. The final solve supplies the endpoint's own implicit derivative; no derivative is defined through the adaptive path selection.

Raises:

Type Description
ValueError

If path controls or parameter tree structures are invalid.

ConvergenceError

If check is true and the endpoint can't be reached.