From c034f20549c21123864cb045a5a0025bdd17c3b5 Mon Sep 17 00:00:00 2001 From: sepehr Date: Sun, 19 Jul 2026 23:34:03 +0200 Subject: [PATCH] Add one-click triple calib setup and warn on orphan Probe Fixed. Guide SST/SDT/DGT pairing for evap z_ua, cond z_ua, and f_w; avoid bound f_w=1 and Fixed P/X that confuse DoF. Co-authored-by: Cursor --- .../src/components/panels/PropertiesPanel.tsx | 51 +++++-- apps/web/src/lib/calibAssist.test.ts | 36 +++++ apps/web/src/lib/calibAssist.ts | 131 +++++++++++++++++- apps/web/src/lib/componentMeta.ts | 6 +- apps/web/src/lib/configBuilder.ts | 9 +- 5 files changed, 210 insertions(+), 23 deletions(-) diff --git a/apps/web/src/components/panels/PropertiesPanel.tsx b/apps/web/src/components/panels/PropertiesPanel.tsx index 80ce19c..c8cebf6 100644 --- a/apps/web/src/components/panels/PropertiesPanel.tsx +++ b/apps/web/src/components/panels/PropertiesPanel.tsx @@ -43,6 +43,7 @@ import { calibrateAdjacentActuatorPatches, collectCalibHints, isCalibRelevantNode, + setupTripleCycleCalibPatches, withDerivedUaEff, } from "@/lib/calibAssist"; import { ComponentIcon } from "@/components/canvas/ComponentIcon"; @@ -161,6 +162,11 @@ export default function PropertiesPanel() { return calibrateAdjacentActuatorPatches(node, nodes, edges); }, [node, nodes, edges]); + const tripleCalibPatches = useMemo( + () => setupTripleCycleCalibPatches(nodes, edges), + [nodes, edges], + ); + // After a successful solve, jump to Results when selecting a part (Dymola-like). useEffect(() => { if (!node) return; @@ -345,20 +351,37 @@ export default function PropertiesPanel() { {h.message}

))} - {calibActuatorPatches.length > 0 && ( - - )} +
+ {tripleCalibPatches.length > 0 && ( + + )} + {calibActuatorPatches.length > 0 && ( + + )} +
)} diff --git a/apps/web/src/lib/calibAssist.test.ts b/apps/web/src/lib/calibAssist.test.ts index 36e5374..f195957 100644 --- a/apps/web/src/lib/calibAssist.test.ts +++ b/apps/web/src/lib/calibAssist.test.ts @@ -5,6 +5,7 @@ import { adjacentProbeNames, calibrateAdjacentActuatorPatches, collectCalibHints, + setupTripleCycleCalibPatches, withDerivedUaEff, type CalibNodeData, } from "./calibAssist"; @@ -203,6 +204,41 @@ describe("calibAssist — Fixed/Free générique", () => { expect(collectCalibHints([hx], [], hx).some((h) => h.level === "tip")).toBe(true); }); + it("setupTripleCycleCalibPatches wires SST/SDT/DGT to z_ua/f_w", () => { + const nodes = [ + node("e", "FloodedEvaporator", "evap", { ua: 9000, z_ua: 1 }), + node("p1", "Probe", "probe_1", { tsat_c: 4.4 }), + node("c", "IsentropicCompressor", "comp", { f_w: 1, z_flow: 1 }), + node("p2", "Probe", "probe_2", { + t_c: 44, + tsat_c: 36, + p_bar: 5, + x: 1, + [fixedFlagKey("p_bar")]: true, + [fixedFlagKey("x")]: true, + }), + node("d", "Condenser", "cond", { ua: 2200, z_ua: 1 }), + ]; + const edges: Edge[] = [ + { id: "e1", source: "e", target: "p1" }, + { id: "e2", source: "p1", target: "c" }, + { id: "e3", source: "c", target: "p2" }, + { id: "e4", source: "p2", target: "d" }, + ]; + const patches = setupTripleCycleCalibPatches(nodes, edges); + expect(patches.length).toBe(5); + const byId = Object.fromEntries(patches.map((p) => [p.nodeId, p.params])); + expect(byId.p1[fixedFlagKey("tsat_c")]).toBe(true); + expect(byId.p2[fixedFlagKey("t_c")]).toBe(true); + expect(byId.p2[fixedFlagKey("tsat_c")]).toBe(true); + expect(byId.p2[fixedFlagKey("p_bar")]).toBe(false); + expect(byId.p2[fixedFlagKey("x")]).toBe(false); + expect(byId.e[fixedFlagKey("z_ua")]).toBe(false); + expect(byId.d[fixedFlagKey("z_ua")]).toBe(false); + expect(byId.c[fixedFlagKey("f_w")]).toBe(false); + expect(byId.c.f_w).toBe(0.95); + }); + it("appends derived UA_eff = UA × z_ua", () => { const nodes = [node("e", "Evaporator", "evap", { ua: 8000, z_ua: 1 })]; const solved: SolvedVariable[] = [ diff --git a/apps/web/src/lib/calibAssist.ts b/apps/web/src/lib/calibAssist.ts index d262890..ac31c6c 100644 --- a/apps/web/src/lib/calibAssist.ts +++ b/apps/web/src/lib/calibAssist.ts @@ -207,6 +207,53 @@ export function collectCalibHints( } } + // Orphan Fixed measures (no compatible factor, or no Free partner) confuse DoF. + if (focus.data.type === "Probe") { + for (const m of fixedProbeMeasures(focus)) { + if (m.factors.length === 0) { + hints.push({ + level: "warn", + component: focus.data.name, + message: + `${m.label} Fixed n’a pas de facteur compatible — décoche Fixed ` + + `(ex. X/titre ne calibre rien).`, + }); + continue; + } + const neigh = adjacentNodes(focus, nodes, edges).filter((n) => n.data.type !== "Probe"); + const hasPartner = neigh.some((n) => + actuatorParams(n.data.type).some( + (ap) => ap.actuatorFactor && m.factors.includes(ap.actuatorFactor), + ), + ); + if (!hasPartner) { + hints.push({ + level: "warn", + component: focus.data.name, + message: + `${m.label} Fixed sans composant adjacent avec ${m.factors.join("/")} — ` + + `décoche Fixed ou place la Probe sur le bon fil.`, + }); + } else if (m.kind === "P") { + const zDpFree = neigh.some((n) => + actuatorParams(n.data.type).some( + (ap) => + ap.actuatorFactor === "z_dp" && !isParamFixed(n.data.params ?? {}, ap), + ), + ); + if (!zDpFree) { + hints.push({ + level: "warn", + component: focus.data.name, + message: + `P Fixed sans z_dp Free adjacent — décoche P (sinon Jacobien / DoF confus). ` + + `Pour ΔP : Free z_dp sur le HX.`, + }); + } + } + } + } + // Baseline always visible — otherwise users see nothing when all Fixed (default). if (hints.length === 0 && isCalibRelevantNode(focus)) { if (focus.data.type === "Probe") { @@ -216,9 +263,9 @@ export function collectCalibHints( level: "tip", component: focus.data.name, message: - "Fixed/Free : coche Fixed sur une mesure (Tsat, P, T, Capacity…) " + - "puis libère le facteur compatible sur le composant adjacent " + - "(z_ua, z_dp, z_flow, f_w).", + "Calib cycle : probe aspiration → Fixed Tsat (SST) + Free z_ua évap ; " + + "probe refoulement → Fixed T (DGT) + Free f_w, Fixed Tsat (SDT) + Free z_ua cond. " + + "Ne coche pas P ni X.", }); } } else { @@ -301,6 +348,84 @@ export function calibrateAdjacentZUaPatch( return zUa ?? patches[0] ?? null; } +/** + * One-click DoF for a standard chiller/HP cycle: + * probe suction → Fixed Tsat → Free evap z_ua + * probe discharge → Fixed T + Fixed Tsat → Free comp f_w + Free cond z_ua + * Unfixes orphan P/X on those probes. + */ +export function setupTripleCycleCalibPatches( + nodes: Node[], + edges: Edge[], +): CalibParamPatch[] { + const probes = nodes.filter((n) => n.data.type === "Probe"); + let suction: Node | undefined; + let discharge: Node | undefined; + let evap: Node | undefined; + let cond: Node | undefined; + let comp: Node | undefined; + + for (const n of nodes) { + const t = n.data.type; + if (!evap && t.includes("Evaporator")) evap = n; + if (!cond && t.includes("Condenser")) cond = n; + if (!comp && t.includes("Compressor")) comp = n; + } + + for (const p of probes) { + const adj = adjacentNodes(p, nodes, edges); + const nearComp = adj.some((a) => a.data.type.includes("Compressor")); + const nearEvap = adj.some((a) => a.data.type.includes("Evaporator")); + const nearCond = adj.some((a) => a.data.type.includes("Condenser")); + if (nearComp && nearEvap) suction = p; + if (nearComp && nearCond) discharge = p; + } + + if (!suction || !discharge || !evap || !cond || !comp) return []; + + const patches: CalibParamPatch[] = []; + + const suctionParams = { ...(suction.data.params ?? {}) }; + suctionParams[fixedFlagKey("tsat_c")] = true; + suctionParams[fixedFlagKey("t_c")] = false; + suctionParams[fixedFlagKey("p_bar")] = false; + suctionParams[fixedFlagKey("x")] = false; + suctionParams[fixedFlagKey("tsh_k")] = false; + suctionParams[fixedFlagKey("capacity_w")] = false; + if (suctionParams.tsat_c === undefined) suctionParams.tsat_c = 4.4; + patches.push({ nodeId: suction.id, params: suctionParams }); + + const dischargeParams = { ...(discharge.data.params ?? {}) }; + dischargeParams[fixedFlagKey("t_c")] = true; + dischargeParams[fixedFlagKey("tsat_c")] = true; + dischargeParams[fixedFlagKey("p_bar")] = false; + dischargeParams[fixedFlagKey("x")] = false; + dischargeParams[fixedFlagKey("tsh_k")] = false; + dischargeParams[fixedFlagKey("capacity_w")] = false; + if (dischargeParams.t_c === undefined) dischargeParams.t_c = 44; + if (dischargeParams.tsat_c === undefined) dischargeParams.tsat_c = 36; + patches.push({ nodeId: discharge.id, params: dischargeParams }); + + const evapParams = { ...(evap.data.params ?? {}) }; + evapParams[fixedFlagKey("z_ua")] = false; + if (typeof evapParams.z_ua !== "number") evapParams.z_ua = 1.0; + patches.push({ nodeId: evap.id, params: evapParams }); + + const condParams = { ...(cond.data.params ?? {}) }; + condParams[fixedFlagKey("z_ua")] = false; + condParams[fixedFlagKey("z_dp")] = true; + if (typeof condParams.z_ua !== "number") condParams.z_ua = 1.0; + patches.push({ nodeId: cond.id, params: condParams }); + + const compParams = { ...(comp.data.params ?? {}) }; + compParams[fixedFlagKey("f_w")] = false; + compParams[fixedFlagKey("z_flow")] = true; + compParams.f_w = 0.95; + patches.push({ nodeId: comp.id, params: compParams }); + + return patches; +} + /** * Append derived UA_eff = UA × z_ua when both are known after solve. */ diff --git a/apps/web/src/lib/componentMeta.ts b/apps/web/src/lib/componentMeta.ts index fa61c28..2d35048 100644 --- a/apps/web/src/lib/componentMeta.ts +++ b/apps/web/src/lib/componentMeta.ts @@ -531,17 +531,17 @@ export const COMPONENTS: ComponentMeta[] = [ key: "f_w", label: "f_w (rétention énergie)", kind: "number", - default: 1.0, + default: 0.95, min: 0, max: 1, section: "Calibration", fixable: true, defaultFixed: true, actuatorFactor: "f_w", - freeMin: 0.0, + freeMin: 0.5, freeMax: 1.0, description: - "Fraction du travail conservée dans le fluide : 1 = adiabatique, 0.98 ≈ 2 % perdu, 0 = tout perdu. Fixed OFF + Probe T (DGT) sur refoulement.", + "Fraction du travail conservée dans le fluide : 1 = adiabatique, 0.95 ≈ 5 % perdu. Fixed OFF + Probe T (DGT) sur refoulement. Ne pas partir à 1.0 (borne).", }, ], }, diff --git a/apps/web/src/lib/configBuilder.ts b/apps/web/src/lib/configBuilder.ts index 7fe99b0..8a12e82 100644 --- a/apps/web/src/lib/configBuilder.ts +++ b/apps/web/src/lib/configBuilder.ts @@ -611,9 +611,12 @@ export function buildModelEmbeddings( if (p.actuatorFactor && !fixed) { const n = typeof raw === "number" ? raw : Number(raw); - // Keep the UI value as Newton start (do not rewrite 1.0 → 0.3 — that - // pushed Free Z into a fragile basin and amplified HashMap-order flakes). - const initial = Number.isFinite(n) ? n : 1.0; + // Keep the UI value as Newton start. For f_w, avoid the upper bound 1.0 + // (adiabatic) which saturates the unknown and can singularize J. + let initial = Number.isFinite(n) ? n : (p.default as number) ?? 1.0; + if (p.actuatorFactor === "f_w" && Math.abs(initial - 1.0) < 1e-12) { + initial = 0.95; + } freeActs.push({ factor: p.actuatorFactor, initial,