Snapshot WIP: solver HP epic progress, BPHX/HX physics, BMAD skill refresh.
Some checks failed
CI / check (push) Has been cancelled
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:
@@ -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([
|
||||
|
||||
Reference in New Issue
Block a user