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.
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
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 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 initial registry supports mixers, splitters, heaters/coolers, valves, pumps, compressors, turbines, PT flashes, 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.
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, options=SolverOptions(recycle_method="broyden"))
simultaneous = CaseRunner(case, options=SolverOptions(backend="eo"))
Both backends delegate unit physics to the same kernels. The case EO adapter
solves simultaneous stream equations with nested implicit unit solves; it
doesn't expand a column into global MESH unknowns. Pure-fluid EO streams retain
enthalpy coordinates so quality isn't lost on the saturation line. Failed
solves retain their reports, and no backend silently substitutes another.
Execution compiles each complete registered unit, including feed-property
preparation and retained outputs, with dynamic operating values. 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 JAX's persistent compilation cache with
export JAX_COMPILATION_CACHE_DIR="$PWD/.jax_cache" before running commands.
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.
Acceptance and reproducibility¶
Every run retains separate evidence:
- Numerical solver and unit reports, plus final outlet replay checks.
- 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.
- Operating bounds, specification residuals, and finite named metrics.
- Economics, including correlation size ranges even when the underlying screening function clips its numerical argument.
- 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.