Skip to content

Saved process cases and design studies

fugacio.sim.cases turns a process definition into a reusable engineering artifact. A case declares its component basis, property package, feeds, units, parameters, measurements, specifications, and optional economics. The same definition can be saved as JSON, solved with either flowsheet backend, studied, and reopened by the copilot. Both backends use the shared process graph, including sparse implicit derivatives and coupled bounded specifications.

Run a saved case

uv run fugacio example heater heater.json
uv run fugacio validate heater.json
uv run fugacio run heater.json --output run.json --report report.md

The CLI saves immutable case revisions and run artifacts in .fugacio-cases by default. Use --workspace PATH to choose another directory. A run prints its complete JSON artifact, including artifact_id. Use that ID to inspect, compare, or replay results:

uv run fugacio inspect RUN_ID --report
uv run fugacio replay RUN_ID
uv run fugacio compare BASELINE_ID CANDIDATE_ID

inspect verifies an artifact's hash and prints it as JSON. With --report, it renders any artifact kind as Markdown: a run gets its full engineering report, and a study or comparison gets a summary of its recorded fields. Nothing is recomputed.

The command exits with status 0 on success, 1 for an input or execution error, and 2 when a completed run or study fails its checks. A failed calculation is still saved. Input validation errors don't fabricate a run.

The Markdown run report lists each check with the reasons a failed one gives, the metrics, and a stream table with molar flow, mass flow (kg/s), temperature, pressure, molar vapor fraction, one mole-fraction column per component in the case's order, and enthalpy flow, followed by each unit's heat and work.

Reuse compiled kernels across commands

Each CLI command is a new process, so it compiles its kernels again. Pass --jax-cache DIR to run, sweep, optimize, sensitivities, profile, or replay to keep compiled programs in DIR and load them in later commands. Setting the FUGACIO_JAX_CACHE environment variable does the same for every command:

uv run fugacio run heater.json --jax-cache .jax_cache
export FUGACIO_JAX_CACHE="$PWD/.jax_cache"

In Python, call fugacio.sim.enable_compilation_cache(directory) before the first calculation. See the performance guide.

The Python equivalent is:

from fugacio.sim.cases import CaseRunner, CaseWorkspace, ProcessCase
from fugacio.sim.cases.examples import example_case

example_case("heater").save("heater.json")
case = ProcessCase.load("heater.json")
runner = CaseRunner(case)
baseline = runner.run(check=True)
workspace = CaseWorkspace(".fugacio-cases")
workspace.save_run(baseline)

candidate = runner.run({"temperature": {"value": 360, "unit": "K"}}, check=True)
workspace.save_run(candidate)
print(candidate.markdown())

run(check=False), the default, returns failures for inspection. run(check=True) and run.check() raise CaseAcceptanceError, which carries the complete run in its run attribute.

Format and units

Schema version 1 accepts these top-level fields:

Field Contents
schema_version, name Version and portable identifier
description Optional explanatory text
components Ordered, canonicalized component names
property_package Method, explicit options, optional inline measured fit and holdout IDs
parameters Named scalar defaults, declared units, optional bounds
reaction_sets Optional named reactions, reacting phases, and dimensional kinetic coefficients
feeds Named flow, composition, pressure, and temperature or molar enthalpy
units Registered unit kinds, names, ports, and settings
metrics Named, dimension-checked result expressions and display units
specifications Bounded manipulated parameters and target metrics
economics Explicit utility prices, operating time, and optional capital estimates

Dimensional literals require {"value": 16, "unit": "bar"}. A parameter reference is {"parameter": "pressure"}. Parameter declarations use {"value": 16, "unit": "bar", "lower": 14, "upper": 18}; defaults and bounds share the declared unit. Numerical kernels use SI. Parameters and metric results retain their original display units.

Use K, degC, or degF for absolute temperature and delta_K, delta_degC, or delta_degF for intervals. Pressure is absolute. Flow is molar, and feed z is a mole-fraction vector in the case's component order. Bare numbers are allowed only for dimensionless literals. The closed unit vocabulary includes engineering pressure, flow, power, energy, area, volume, and currency units; fugacio registry and the copilot's case_format describe the available inputs.

JSON loading rejects duplicate keys, nonfinite literals, unsupported versions, unknown fields, executable expressions, and oversized documents. Case IDs are SHA-256 hashes of normalized content. case.with_parameters(...) creates a new revision; it never mutates an existing case or rewrites an earlier run.

Each stream has one producer and at most one consumer. Introduce a splitter for material branches. Feeds must connect to the process, every unit must have a path from a feed, and the case must have an external product. Recycles are allowed and partitioned by the existing flowsheet engine.

Units and backends

The registry supports mixers, splitters, heaters/coolers, valves, pumps, compressors, turbines, flash drums, ideal component separators, two-sided heat exchangers, rigorous MESH columns, and single-reaction stoichiometric reactors. Common-package equilibrium reactors, CSTRs, PFRs, and reactive flashes share named reaction sets; columns accept reacting phase volumes. See reactive workflows for complete examples. Run fugacio registry for every kind's settings.

A flash unit requires its pressure p and exactly one of t or duty. With t it's an isothermal drum whose required heat is reported. With duty it's a drum with a specified heat input, and {"value": 0, "unit": "W"} is an adiabatic drum whose temperature follows from the energy balance, the right model for a letdown into a separator. A heater takes exactly one of t_out, duty, or vapor_fraction; a vapor fraction may be any value in [0, 1] for a pure fluid and 0 (bubble point) or 1 (dew point) for a mixture.

{
  "name": "separator",
  "kind": "flash",
  "inlets": ["letdown"],
  "outlets": ["gas", "condensate"],
  "settings": {"p": {"parameter": "letdown_pressure"}, "duty": {"value": 0, "unit": "W"}}
}

Columns retain all product streams, side draws, condenser and reboiler duties, stage temperatures and pressures, phase compositions, K-values, and traffic. Column specifications use component names and explicit physical quantities. Stage numbers start at 1. Reactions declare a stoichiometric vector in component order and a named key reactant; the case validator checks element conservation. Reaction energy includes standard formation enthalpies with the package's sensible and residual terms. The reference-fluid energy datum isn't supported for these reaction cases.

from fugacio.sim.cases import CaseRunner, SolverOptions

sequential = CaseRunner(case)                        # Broyden recycles by default
wegstein = CaseRunner(case, options=SolverOptions(recycle_method="wegstein"))
simultaneous = CaseRunner(case, options=SolverOptions(backend="eo"))

Both backends execute the same process graph and physical unit kernels. Simultaneous execution solves stream connection equations with nested implicit unit solves; it doesn't expand a column into global MESH unknowns. Stream coordinates include vapor inventory so quality isn't lost on the saturation line. Failed solves retain their reports, and no backend silently substitutes another. Execution reuses numerical kernels with dynamic operating values. Unit templates are cached for the whole process, so runners with the same structure and fixed settings share them. Columns and exchangers retain separate property and implicit solver kernels; their preparation, profiles, and reports aren't fused into another case-level compiled solver. Studies reuse one local linearization per operating point, selecting forward or reverse directions from the input/output counts. They retain the individual unit kernels. Fixed case topology and dynamic recycle parameters let optimizer trials reuse the compiled maps. Saved profiles remain available without becoming study derivative outputs. Separate CLI invocations can reuse compiled programs with --jax-cache DIR or FUGACIO_JAX_CACHE, as described above. The performance guide covers structured column solves, flowsheet coloring, reusable derivatives, profiling artifacts, and isolated benchmarks. Timings depend on hardware, JAX version, and cache state. For machines with limited memory, pass release_caches=True to sensitivities or optimization (or "release_caches": true in a CLI request). This explicitly clears process-wide in-memory JAX compilation caches between the baseline, derivative/optimization, and final audit phases. Optimization retains compiled kernels across points and collects unreachable Python objects between them. It retains the disk cache and all completed artifacts. Clearing caches doesn't eliminate the peak memory required by each compilation. The older dense implementation couldn't complete the plant derivative in a 7 GB Linux validation container; the dedicated performance workflow now checks the structured implementation against an explicit process memory limit and retains the observed result.

The registry is deliberately finite. Dynamic units, multiphase liquid inventories, custom Python functions, and arbitrary Python EO blocks remain available through their existing APIs; they aren't serialized as version 1 cases. A new portable kind needs a validator, kernel adapter, retained result definition, and tests.

Metrics and design specifications

A metric combines an expression with a display unit:

{
  "reboiler_duty": {
    "expression": {"unit": "column", "property": "reboiler_duty"},
    "unit": "MW"
  },
  "purity": {
    "expression": {"stream": "distillate", "property": "mole_fraction", "component": "propane"},
    "unit": "%"
  }
}

Expressions can reference parameters, streams, retained unit properties, column profiles, plant totals, or other metrics. Arithmetic operations are add, subtract, multiply, divide, negate, abs, square, min, and max, with their operands in args. Cycles and incompatible dimensions are rejected. No Python evaluation is involved. An undefined quantity, such as the composition of an empty stream, is unavailable and fails a declared metric.

An outer design specification frees one bounded parameter to meet one metric:

{
  "name": "target_duty",
  "parameter": "temperature",
  "metric": "duty",
  "target": {"value": 2.5, "unit": "kW"},
  "tolerance": {"value": 0.001, "unit": "W"}
}

Put this object in the case's specifications array. Coupled specifications are solved together within normalized parameter bounds. Acceptance checks the actual target error and the final bounds. Implicit derivatives follow the converged specification, not the manipulated parameter's initial guess.

Sweeps, sensitivities, and optimization

from fugacio.sim.cases import optimize, sensitivities, sweep

grid = sweep(
    runner,
    {"temperature": [{"value": t, "unit": "K"} for t in (340, 350, 360)]},
    workspace=workspace,
)
gradient = sensitivities(runner, ["temperature"], ["duty"], workspace=workspace)
design = optimize(
    runner,
    ["temperature"],
    "annual_cost",
    constraints=[{
        "metric": "product_temperature",
        "lower": {"value": 360, "unit": "K"},
        "tolerance": {"value": 1e-5, "unit": "delta_K"},
    }],
    workspace=workspace,
)

Sweeps evaluate a bounded Cartesian grid in deterministic order. Failed points remain in the manifest with a failed run or an input error. Study manifests reference immutable run IDs; StudyResult.save writes all runs before the manifest. When a workspace is provided, completed audited runs are also saved as the study progresses, so an interrupted study retains its completed work. An unacceptable baseline produces a failed study with the full baseline run; it never produces a derivative or optimization claim. Baseline comparisons align metrics by expression and dimension and show changes in parameters, packages, solver settings, and audit policy.

Sensitivity studies compare JAX derivatives with centered differences of independently accepted runs. They reject boundary points without a centered interval, failed perturbations, nonfinite derivatives, and changes between liquid, vapor, two-phase, or empty stream regimes. Reported derivatives include SI values and display-unit conversions. This is a local consistency test, not an uncertainty estimate or a guarantee of global smoothness.

Optimization uses host SLSQP with exact JAX derivatives and declared finite variable bounds. Constraints accept lower/upper or equal, plus an explicit tolerance. Trial points receive numerical checks; the baseline and final point receive full physical audits. An optimizer's success flag alone can't produce an accepted study. Final feasibility, finite derivatives, physical acceptance, and cost-correlation ranges must also pass. Results are local candidates, with no global-optimum claim. A parameter controlled by a design specification can't also be an independent study variable.

Derivative evaluations and optimizer trials reuse the accepted baseline's recycle streams and column stage states as detached initial guesses. Failed trials never replace that seed. Each study records its baseline run ID. Finite-difference points and final candidates start cold, and optimization acceptance requires the final objective and constraint metrics to agree with the warm-start trial within a scaled relative tolerance of 1e-6.

The CLI takes study arguments in a JSON request file:

uv run fugacio sweep heater.json sweep-request.json
uv run fugacio optimize heater.json optimization-request.json
uv run fugacio sensitivities heater.json sensitivity-request.json

For example, a sweep request is {"grid": {"temperature": [{"value": 340, "unit": "K"}, {"value": 360, "unit": "K"}]}}. Optimization requests use variables, objective, optional sense, and constraints. Sensitivity requests use parameters and metrics. Optional overrides set the base operating point for optimization and sensitivities.

Depropanizer and measured heater

uv run fugacio example depropanizer depropanizer.json
uv run fugacio run depropanizer.json --recycle-method broyden --report depropanizer.md
uv run fugacio example measured-heater measured-heater.json
uv run fugacio run measured-heater.json --report measured-heater.md

The depropanizer reproduces the existing 16-stage propane/butane/pentane plant case. It meets 95% propane purity and 98% recovery, transfers bottoms heat to the feed, and exposes pressure and exchanger approach as bounded design variables. Its economics include separately accumulated heating and cooling, an exchanger sized from retained duty and temperature differences, and explicit capital and utility assumptions. The supplied prices and CEPCI are illustrative screening inputs, not current quotes. Optimize annual_cost over pressure and approach; the column's purity and recovery remain internal specifications.

The measured heater embeds the ethanol-water NRTL fit from the reproducible qualification workflow. Loading verifies training observation IDs and raw source hashes, then reevaluates the declared independent publication holdout. The stored fit coefficients don't supply a trusted qualification flag. The example stays within observed training bounds. The example evidence can be regenerated with scripts/qualify.py; its source IDs and hashes remain in JSON.

Joule-Thomson separator

uv run fugacio example jt-separator jt-separator.json
uv run fugacio run jt-separator.json --jax-cache .jax_cache --report jt-separator.md

The jt-separator case expands 10 mol/s of a rich natural gas (methane, ethane, propane, and n-butane on Peng-Robinson) from 90 bar to 25 bar across an isenthalpic Joule-Thomson valve, then separates the chilled two-phase outlet in a flash drum at zero duty. The drum's temperature follows from the energy balance: about 262.4 K, condensing about 0.573 mol/s, with about 52.6% of the n-butane recovered in the condensate. The letdown pressure is a bounded parameter between 15 and 60 bar, so sweeping it trades separator temperature against heavy-ends recovery. The feed composition is illustrative.

Acceptance and reproducibility

Every run retains separate evidence:

  1. Numerical solver and unit reports, plus final outlet replay checks.
  2. Stream phase-state checks and unit/plant component and energy closure. Reactive boundaries also check elements. Empty streams have no equilibrium composition and are explicitly identified.
  3. Operating bounds, specification residuals, and finite named metrics.
  4. Economics, including correlation size ranges even when the underlying screening function clips its numerical argument.
  5. Parameter sources, model assumptions, observed applicability bounds, and independently evaluated empirical qualification.

Heat and shaft work are positive into the fluid. Heating and cooling utilities are accumulated separately; a column's condenser and reboiler don't cancel out of utility cost. Recovered shaft power is reported separately and receives no automatic electricity-sale credit. Annual cost uses USD/s internally and USD/yr for display, based on a 365.25-day year and declared operating hours.

An isothermal flash, temperature-specified mixer, or ideal component separator infers required external heat from its specified states. Its report says so; balance closure doesn't establish available equipment capacity. Unit profiles are retained, but stream audits don't independently validate every internal stage's stability. A finite-start stability search isn't a global proof.

Artifacts include the exact case, requested and solved parameter values, solver options, acceptance policy, and runtime versions. Files are strict JSON and are written atomically. Hashes detect alteration and identify revisions; they aren't signatures and don't prove that an arbitrary external author ran a calculation. Replays may differ numerically across JAX versions or hardware. Unknown empirical ranges remain unknown, and accepted process balances don't qualify an entire plant or model family.

The audit also checks declared equipment roles: pumps and compressors raise pressure, valves and turbines lower it, and liquid pumps require liquid feeds. Pressure-specified mixers, flashes, and columns require sufficient feed pressure. Reported metrics distinguish process expressions from literal input values.

Accountable copilot

from fugacio.copilot import run_design_agent
from fugacio.sim.cases import CaseWorkspace

result = run_design_agent(
    "Build a heater case and minimize annual utility cost while delivering at least 360 K.",
    provider,
    workspace=CaseWorkspace(".fugacio-cases"),
)
print(result.answer)

provider implements the existing provider-neutral interface. The design loop can obtain examples, create or load cases, update a complete revision, set parameter defaults, run cases, perform studies, and inspect stored evidence. Case mutations invalidate a pending submission. A stale revision, failed run, unknown metric, or externally loaded run that hasn't been recomputed in the session can't be submitted.

Completion requires submit_design(run_id, metrics, baseline_id=...). It takes recorded metric names, not model-authored numbers or a free-form report. The selected metrics must depend on process results; an input literal isn't computed performance. The final answer is rendered deterministically from the accepted current-case run. Unsupported provider text remains visible in the transcript and prompts the model to continue computing. Exhausted budgets return an explicit incomplete result. The general run_llm_agent remains available for explanatory questions; it doesn't enforce this stricter design-submission contract.