Files
Entropyk/docs/model-ir.md
sepehr 5bd180b5b8
Some checks failed
CI / check (push) Has been cancelled
Snapshot WIP: solver HP epic progress, BPHX/HX physics, BMAD skill refresh.
Capture uncommitted solver robustness work (regularization, domain errors, linear solver lifecycle, tube DP/MSH), web workbench updates, and synced BMAD skills across IDE agent folders before starting BPHX pressure-drop.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-19 16:35:31 +02:00

5.9 KiB

Entropyk Model IR (unified schema)

Entropyk uses one declarative Model IR — a schema-versioned JSON document — as the single source of truth shared by the CLI, the web UI, and (once present) the Python and WASM bindings. There is no divergent per-frontend schema: the web graph editor serializes to the same document the CLI loads.

The IR is defined by the Rust types in crates/cli/src/config.rs. The machine-readable JSON Schema is generated from those types (via schemars) so it can never drift from what the loader accepts.

Getting the schema

# Print the canonical JSON Schema (draft-07) to stdout
cargo run -p entropyk-cli -- schema

# …or write it to a file (the committed copy lives at docs/model-ir.schema.json)
cargo run -p entropyk-cli -- schema --output docs/model-ir.schema.json

External tools and the UI can validate documents against model-ir.schema.json.

Versioning

Every document carries a schema_version. This lets the schema — and any embedded standard/norm references (SEER/SCOP/IPLV/NPLV) — evolve without silently misreading older files.

schema_version Meaning
absent Treated as "1" (legacy).
"1" Flat circuits / components / edges graph.
"2" Adds controls, subsystems, instances, connections.

The loader accepts every version listed in SUPPORTED_SCHEMA_VERSIONS (currently 1, 2) and rejects anything else with a clear error. v1 documents load unchanged — the new fields are all optional and default to empty.

Top-level fields

{
  "schema_version": "2",
  "fluid": "R134a",
  "fluid_backend": "CoolProp",

  "circuits":    [ /* flat refrigerant loops (v1) */ ],
  "thermal_couplings": [ /* inter-circuit UA links (v1) */ ],

  "subsystems":  { /* reusable parameterized templates (v2) */ },
  "instances":   [ /* template instantiations (v2) */ ],
  "connections": [ /* external edges between instance ports (v2) */ ],

  "controls":    [ /* co-solved steady-state control loops (v2) */ ],

  "solver": { "strategy": "fallback", "max_iterations": 300, "tolerance": 1e-6 }
}

subsystems + instances (hierarchical templates)

A SubsystemTemplate is a parameterized assembly of components and internal edges, exposing a reduced set of external ports. It is declared once and instantiated any number of times. At load time each instance is flattened into the flat circuits/components/edges graph — the solver never sees the hierarchy — with component names prefixed "{instance}.".

Parameter substitution: any component parameter whose value is a string "$name" is replaced by the resolved parameter (instance override → template default). Because ua, secondary_*, isentropic_efficiency, t_cond_k, … all flow through the component parameter catch-all, essentially every physical knob is parameterizable.

"subsystems": {
  "EmergentCircuit": {
    "params": { "ua_cond": 766.0, "ua_evap": 1468.0 },
    "components": [
      { "type": "IsentropicCompressor", "name": "comp", "isentropic_efficiency": 0.7 },
      { "type": "Condenser", "name": "cond", "ua": "$ua_cond" },
      { "type": "IsenthalpicExpansionValve", "name": "exv" },
      { "type": "Evaporator", "name": "evap", "ua": "$ua_evap" }
    ],
    "edges": [
      { "from": "comp:outlet", "to": "cond:inlet" },
      { "from": "cond:outlet", "to": "exv:inlet" },
      { "from": "exv:outlet", "to": "evap:inlet" },
      { "from": "evap:outlet", "to": "comp:inlet" }
    ],
    "ports": { "suction": "evap:outlet", "discharge": "comp:outlet" }
  }
},
"instances": [
  { "of": "EmergentCircuit", "name": "A", "circuit": 0, "params": { "ua_cond": 766.0 } },
  { "of": "EmergentCircuit", "name": "B", "circuit": 1, "params": { "ua_cond": 900.0 } }
]

This unlocks multi-circuit machines (e.g. 61XW System_2C, multi-module 61AQ): define the circuit once, instantiate A/B with different parameters. See crates/cli/examples/chiller_2circuit_subsystems.json.

connections wire external ports together (or literal component:port endpoints):

"connections": [ { "from": "A.discharge", "to": "B.suction" } ]

controls (co-solved steady-state control loops)

Each control loop is solved inside the same Newton system as the physics — no time integration. A SaturatedController is a saturated-PI loop with exact anti-windup that drives a measured plant output (measure) to target by manipulating an actuator factor (actuator) within [min, max].

"controls": [
  {
    "type": "SaturatedController",
    "id": "evap_capacity",
    "measure":  { "component": "evap", "output": "capacity" },
    "actuator": { "component": "comp", "factor": "z_flow", "initial": 1.0, "min": 0.5, "max": 1.5 },
    "target": 7000.0,
    "gain": 0.01,
    "band": 1.0
  }
]

Measurable outputs: capacity, heatTransferRate, superheat, subcooling, saturationTemperature, massFlowRate, pressure, temperature. Actuator Z-factors (canonical): z_flow, z_dp, z_ua, z_power, z_etav (BOLT equivalents: Z_flow_suc, Z_dpc, Z_UA, Z_power; legacy f_* names still accepted).

When an instance is involved, reference the prefixed name, e.g. "component": "A.evap". See crates/cli/examples/chiller_r134a_capacity_control.json.

One IR, every frontend

  • CLIentropyk-cli run|validate|rate|scop|seer … load this IR directly.
  • Web UI (apps/web) — the React-Flow graph serializes to this exact IR via buildScenarioConfig (apps/web/src/lib/configBuilder.ts); it emits schema_version from the shared SCHEMA_VERSION constant.
  • Python / WASM bindings — consume the same JSON document, no separate schema.