Snapshot WIP: solver HP epic progress, BPHX/HX physics, BMAD skill refresh.
Some checks failed
CI / check (push) Has been cancelled

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>
This commit is contained in:
2026-07-19 16:35:31 +02:00
parent 88620790d6
commit 5bd180b5b8
1363 changed files with 101041 additions and 58547 deletions

View File

@@ -239,16 +239,46 @@ export function buildScenarioConfig(
edges: Edge[],
options: BuildOptions = {},
): ScenarioConfig {
// Group nodes by circuit id.
const nodeById = new Map<string, Node<EntropykNodeData>>();
for (const n of nodes) nodeById.set(n.id, n);
// Separate ModuleInstance nodes from flat atomic components
const instances: InstanceConfig[] = [];
const connections: Array<{ from: string; to: string }> = [];
const moduleNodes = nodes.filter((n) => n.data.type === "ModuleInstance");
moduleNodes.forEach((n) => {
const moduleName = String(n.data.params.module_name ?? "");
if (!moduleName) return;
const params: Record<string, number | string | boolean> = {};
for (const [k, v] of Object.entries(n.data.params)) {
if (k !== "module_name" && k !== "module_ports" && typeof v !== "undefined") {
params[k] = v;
}
}
instances.push({
of: moduleName,
name: n.data.name,
circuit: n.data.circuit ?? 0,
...(Object.keys(params).length > 0 ? { params } : {}),
});
});
// Group non-module component nodes by circuit id.
const circuitsMap = new Map<number, Node<EntropykNodeData>[]>();
for (const n of nodes) {
if (n.data.type === "ModuleInstance") continue;
const c = n.data?.circuit ?? 0;
if (!circuitsMap.has(c)) circuitsMap.set(c, []);
circuitsMap.get(c)!.push(n);
}
const nodeById = new Map<string, Node<EntropykNodeData>>();
for (const n of nodes) nodeById.set(n.id, n);
// Ensure circuit 0 exists if empty
if (circuitsMap.size === 0) {
circuitsMap.set(0, []);
}
const circuits = Array.from(circuitsMap.entries())
.sort(([a], [b]) => a - b)
@@ -259,17 +289,16 @@ export function buildScenarioConfig(
const { type, name, params } = n.data;
const canonicalParams = canonicalizeParams(type, params);
const cleaned = stripUiOnlyParams(type, canonicalParams);
// The CLI flattens unknown keys as params, so we spread them at top level.
return { type, name, ...cleaned } as Record<string, unknown>;
});
// Solver edges: both endpoints in this circuit. Refrigerant and
// caloporteur branches are preserved explicitly; heat exchangers are
// real 4-port components, not reduced to hidden secondary parameters.
// Pure circuit edges (neither endpoint is a ModuleInstance)
const circuitEdges = edges.filter((e) => {
const s = nodeById.get(e.source);
const t = nodeById.get(e.target);
if (s?.data?.circuit !== circuitId || t?.data?.circuit !== circuitId) return false;
if (!s || !t) return false;
if (s.data.type === "ModuleInstance" || t.data.type === "ModuleInstance") return false;
if (s.data.circuit !== circuitId || t.data.circuit !== circuitId) return false;
return true;
});
@@ -283,10 +312,29 @@ export function buildScenarioConfig(
return { id: circuitId, name: `Circuit ${circuitId}`, components, edges: edgeConfigs };
});
// Collect edges connected to ModuleInstances as `connections`
edges.forEach((e) => {
const s = nodeById.get(e.source);
const t = nodeById.get(e.target);
if (!s || !t) return;
if (s.data.type === "ModuleInstance" || t.data.type === "ModuleInstance") {
const fromRef =
s.data.type === "ModuleInstance"
? `${s.data.name}.${e.sourceHandle ?? "outlet"}`
: edgeRef(s, e.sourceHandle, "source");
const toRef =
t.data.type === "ModuleInstance"
? `${t.data.name}.${e.targetHandle ?? "inlet"}`
: edgeRef(t, e.targetHandle, "target");
connections.push({ from: fromRef, to: toRef });
}
});
const nodeControls = nodes
.filter((node) => node.data.type === CONTROL_NODE_TYPE)
.map(controlNodeToConfig);
// Dymola/EES Fixed checkboxes → inverse calib pairs (FIX measure + FREE z_*)
const fixedFreeControls = buildFixedFreeCalibrationControls(nodes);
const controls = mergeControls(
mergeControls(options.controls ?? [], nodeControls),
@@ -298,6 +346,8 @@ export function buildScenarioConfig(
fluid: options.fluid || "R410A",
fluid_backend: options.fluidBackend || "CoolProp",
circuits,
...(instances.length > 0 ? { instances } : {}),
...(connections.length > 0 ? { connections } : {}),
thermal_couplings: options.thermalCouplings || [],
...(controls.length > 0 ? { controls } : {}),
solver: {
@@ -328,9 +378,42 @@ export function stripUiOnlyParams(
for (const [k, v] of Object.entries(params)) {
if (k.startsWith(FIXED_FLAG_PREFIX)) continue;
if (measureOnly.has(k)) continue;
// Omit blank optional numerics (e.g. unset orifice_kv).
if (v === "" || v === null || v === undefined) continue;
out[k] = v;
}
return applyBoundaryFixSemantics(type, out, params);
return applyExvFixSemantics(type, applyBoundaryFixSemantics(type, out, params), params);
}
const EXV_TYPES = new Set(["IsenthalpicExpansionValve", "EXV"]);
/**
* Emit `fix_opening` when orifice_kv is set so the CLI can choose fixed vs
* free orifice (never infer orifice from opening alone).
*/
export function applyExvFixSemantics(
type: string,
cleaned: Record<string, number | string | boolean>,
rawParams: Record<string, number | string | boolean>,
): Record<string, number | string | boolean> {
if (!EXV_TYPES.has(type)) return cleaned;
const kv = cleaned.orifice_kv;
const kvNum = typeof kv === "number" ? kv : Number(kv);
if (!Number.isFinite(kvNum) || kvNum <= 0) {
const out: Record<string, number | string | boolean> = { ...cleaned };
delete out.orifice_kv;
delete out.fix_opening;
return out;
}
const meta = COMPONENT_BY_TYPE[type];
const openMeta = meta?.params.find((p) => p.key === "opening");
const out: Record<string, number | string | boolean> = { ...cleaned, orifice_kv: kvNum };
if (openMeta?.fixable) {
out.fix_opening = isParamFixed(rawParams, openMeta);
} else {
out.fix_opening = true;
}
return out;
}
const BOUNDARY_FIX_TYPES = new Set([