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>
228 lines
7.8 KiB
TypeScript
228 lines
7.8 KiB
TypeScript
/**
|
||
* Modelica / Dymola-style post-solve variables for a selected component.
|
||
* Built from diagram wiring + enriched `SimulationResult.state` edges.
|
||
*/
|
||
|
||
import type { Edge, Node } from "@xyflow/react";
|
||
import type { EntropykNodeData } from "./configBuilder";
|
||
import { COMPONENT_BY_TYPE, isSecondaryPort } from "./componentMeta";
|
||
import type { SimulationResult } from "./api";
|
||
|
||
export interface PortVariable {
|
||
port: string;
|
||
role: "in" | "out";
|
||
stream: "primary" | "secondary" | "other";
|
||
pressure_bar: number | null;
|
||
temperature_c: number | null;
|
||
tsat_c: number | null;
|
||
enthalpy_kj_kg: number | null;
|
||
mass_flow_kg_s: number | null;
|
||
edgeIndex: number;
|
||
}
|
||
|
||
export interface ComponentInspector {
|
||
title: string;
|
||
typeLabel: string;
|
||
type: string;
|
||
status: string | null;
|
||
ports: PortVariable[];
|
||
/** Primary-side pressure drop [bar] (in − out). */
|
||
delta_p_bar: number | null;
|
||
/** Secondary-side pressure drop [bar]. */
|
||
delta_p_sec_bar: number | null;
|
||
/** m·Δh on primary [kW]. */
|
||
q_primary_kw: number | null;
|
||
/** m·Δh on secondary [kW]. */
|
||
q_secondary_kw: number | null;
|
||
/** |W| shaft estimate for compressors/fans/pumps [kW]. */
|
||
work_kw: number | null;
|
||
superheat_k: number | null;
|
||
subcooling_k: number | null;
|
||
summaryLines: string[];
|
||
}
|
||
|
||
type StateEntry = NonNullable<SimulationResult["state"]>[number];
|
||
|
||
function streamOf(port: string): PortVariable["stream"] {
|
||
if (isSecondaryPort(port)) return "secondary";
|
||
if (/inlet|outlet|suction|discharge/i.test(port)) return "primary";
|
||
return "other";
|
||
}
|
||
|
||
function roleOf(port: string, isSource: boolean): PortVariable["role"] {
|
||
if (/outlet|discharge/i.test(port)) return "out";
|
||
if (/inlet|suction/i.test(port)) return "in";
|
||
return isSource ? "out" : "in";
|
||
}
|
||
|
||
function findStateForEdge(
|
||
edgeIndex: number,
|
||
edge: Edge,
|
||
nodes: Node<EntropykNodeData>[],
|
||
state: StateEntry[] | undefined,
|
||
): StateEntry | undefined {
|
||
if (!state?.length) return undefined;
|
||
const byIndex = state[edgeIndex];
|
||
if (byIndex) return byIndex;
|
||
|
||
const src = nodes.find((n) => n.id === edge.source)?.data.name;
|
||
const tgt = nodes.find((n) => n.id === edge.target)?.data.name;
|
||
if (!src || !tgt) return undefined;
|
||
return state.find((s) => s.source === src && s.target === tgt);
|
||
}
|
||
|
||
/** Build Modelica-style variables for the selected component after a solve. */
|
||
export function buildComponentInspector(
|
||
node: Node<EntropykNodeData>,
|
||
nodes: Node<EntropykNodeData>[],
|
||
edges: Edge[],
|
||
result: SimulationResult | null,
|
||
): ComponentInspector {
|
||
const meta = COMPONENT_BY_TYPE[node.data.type];
|
||
const title = node.data.name;
|
||
const typeLabel =
|
||
node.data.type === "ModuleInstance"
|
||
? `Sous-système Module (${String(node.data.params.module_name ?? "ekmod")})`
|
||
: (meta?.label ?? node.data.type);
|
||
const status = result ? String(result.status) : null;
|
||
const state = result?.state;
|
||
|
||
const ports: PortVariable[] = [];
|
||
edges.forEach((edge, edgeIndex) => {
|
||
const touches =
|
||
edge.source === node.id || edge.target === node.id;
|
||
if (!touches) return;
|
||
const isSource = edge.source === node.id;
|
||
const handle = isSource
|
||
? (edge.sourceHandle ?? "outlet")
|
||
: (edge.targetHandle ?? "inlet");
|
||
const entry = findStateForEdge(edgeIndex, edge, nodes, state);
|
||
const stream = streamOf(handle);
|
||
// Infer secondary from partner name when handle is plain outlet (BrineSource).
|
||
const partnerName = isSource
|
||
? nodes.find((n) => n.id === edge.target)?.data.name ?? ""
|
||
: nodes.find((n) => n.id === edge.source)?.data.name ?? "";
|
||
const selfName = node.data.name;
|
||
const looksWater =
|
||
stream === "secondary" ||
|
||
/water|brine|glycol/i.test(`${selfName} ${partnerName}`) ||
|
||
/Brine|WaterPipe/i.test(node.data.type);
|
||
const looksAir =
|
||
/air|duct/i.test(`${selfName} ${partnerName}`) || /AirSource|AirSink|AirDuct/i.test(node.data.type);
|
||
|
||
ports.push({
|
||
port: handle,
|
||
role: roleOf(handle, isSource),
|
||
stream: looksAir ? "other" : looksWater ? "secondary" : stream,
|
||
pressure_bar: entry?.pressure_bar ?? null,
|
||
temperature_c: entry?.temperature_c ?? null,
|
||
// Tsat only meaningful on refrigerant — hide for water/air loops.
|
||
tsat_c:
|
||
looksWater || looksAir ? null : (entry?.saturation_temperature_c ?? null),
|
||
enthalpy_kj_kg: entry?.enthalpy_kj_kg ?? null,
|
||
mass_flow_kg_s: entry?.mass_flow_kg_s ?? null,
|
||
edgeIndex,
|
||
});
|
||
});
|
||
|
||
// Prefer meta port order
|
||
const order = meta?.ports ?? [];
|
||
ports.sort((a, b) => {
|
||
const ia = order.indexOf(a.port);
|
||
const ib = order.indexOf(b.port);
|
||
if (ia >= 0 && ib >= 0) return ia - ib;
|
||
if (ia >= 0) return -1;
|
||
if (ib >= 0) return 1;
|
||
return a.port.localeCompare(b.port);
|
||
});
|
||
|
||
const primaryIn = ports.find((p) => p.stream === "primary" && p.role === "in");
|
||
const primaryOut = ports.find((p) => p.stream === "primary" && p.role === "out");
|
||
const secIn = ports.find((p) => p.stream === "secondary" && p.role === "in");
|
||
const secOut = ports.find((p) => p.stream === "secondary" && p.role === "out");
|
||
|
||
const delta_p_bar =
|
||
primaryIn?.pressure_bar != null && primaryOut?.pressure_bar != null
|
||
? primaryIn.pressure_bar - primaryOut.pressure_bar
|
||
: null;
|
||
const delta_p_sec_bar =
|
||
secIn?.pressure_bar != null && secOut?.pressure_bar != null
|
||
? secIn.pressure_bar - secOut.pressure_bar
|
||
: null;
|
||
|
||
const q_primary_kw = streamDutyKw(primaryIn, primaryOut);
|
||
const q_secondary_kw = streamDutyKw(secIn, secOut);
|
||
|
||
const isMachine = /Compressor|Pump|Fan|Centrifugal/i.test(node.data.type);
|
||
const work_kw = isMachine && q_primary_kw != null ? Math.abs(q_primary_kw) : null;
|
||
|
||
let superheat_k: number | null = null;
|
||
let subcooling_k: number | null = null;
|
||
if (
|
||
primaryOut?.temperature_c != null &&
|
||
primaryOut.tsat_c != null &&
|
||
/Evaporator|Flooded|BphxEvap/i.test(node.data.type)
|
||
) {
|
||
superheat_k = primaryOut.temperature_c - primaryOut.tsat_c;
|
||
}
|
||
if (
|
||
primaryOut?.temperature_c != null &&
|
||
primaryOut.tsat_c != null &&
|
||
/Condenser|BphxCond|GasCooler/i.test(node.data.type)
|
||
) {
|
||
subcooling_k = primaryOut.tsat_c - primaryOut.temperature_c;
|
||
}
|
||
// Suction superheat at compressor inlet
|
||
if (
|
||
primaryIn?.temperature_c != null &&
|
||
primaryIn.tsat_c != null &&
|
||
/Compressor/i.test(node.data.type)
|
||
) {
|
||
superheat_k = primaryIn.temperature_c - primaryIn.tsat_c;
|
||
}
|
||
|
||
const summaryLines: string[] = [];
|
||
if (delta_p_bar != null) summaryLines.push(`ΔP = ${fmt(delta_p_bar, 3)} bar`);
|
||
if (delta_p_sec_bar != null) summaryLines.push(`ΔP sec = ${fmt(delta_p_sec_bar, 3)} bar`);
|
||
if (q_primary_kw != null) summaryLines.push(`Q̇ = ${fmt(Math.abs(q_primary_kw), 2)} kW`);
|
||
if (q_secondary_kw != null) summaryLines.push(`Q̇ sec = ${fmt(Math.abs(q_secondary_kw), 2)} kW`);
|
||
if (work_kw != null) summaryLines.push(`Ẇ = ${fmt(work_kw, 2)} kW`);
|
||
if (superheat_k != null) summaryLines.push(`SH = ${fmt(superheat_k, 2)} K`);
|
||
if (subcooling_k != null) summaryLines.push(`SC = ${fmt(subcooling_k, 2)} K`);
|
||
const m = primaryIn?.mass_flow_kg_s ?? primaryOut?.mass_flow_kg_s;
|
||
if (m != null) summaryLines.push(`ṁ = ${fmt(m, 4)} kg/s`);
|
||
|
||
return {
|
||
title,
|
||
typeLabel,
|
||
type: node.data.type,
|
||
status,
|
||
ports,
|
||
delta_p_bar,
|
||
delta_p_sec_bar,
|
||
q_primary_kw,
|
||
q_secondary_kw,
|
||
work_kw,
|
||
superheat_k,
|
||
subcooling_k,
|
||
summaryLines,
|
||
};
|
||
}
|
||
|
||
function streamDutyKw(
|
||
inn: PortVariable | undefined,
|
||
out: PortVariable | undefined,
|
||
): number | null {
|
||
if (!inn || !out) return null;
|
||
const m = inn.mass_flow_kg_s ?? out.mass_flow_kg_s;
|
||
const hIn = inn.enthalpy_kj_kg;
|
||
const hOut = out.enthalpy_kj_kg;
|
||
if (m == null || hIn == null || hOut == null) return null;
|
||
// kW = kg/s · kJ/kg
|
||
return m * (hOut - hIn);
|
||
}
|
||
|
||
function fmt(v: number, digits: number): string {
|
||
return Number.isFinite(v) ? v.toFixed(digits) : "—";
|
||
}
|