Add one-click triple calib setup and warn on orphan Probe Fixed.
Some checks failed
CI / check (push) Has been cancelled
Some checks failed
CI / check (push) Has been cancelled
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 <cursoragent@cursor.com>
This commit is contained in:
@@ -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,10 +351,26 @@ export default function PropertiesPanel() {
|
||||
{h.message}
|
||||
</p>
|
||||
))}
|
||||
<div className="flex flex-wrap gap-1 pt-0.5">
|
||||
{tripleCalibPatches.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
className="rounded border border-sky-700 bg-sky-700 px-2 py-1 text-[10px] font-medium text-white hover:bg-sky-800"
|
||||
title="probe_1 Tsat→evap z_ua · probe_2 T→f_w · probe_2 Tsat→cond z_ua"
|
||||
onClick={() => {
|
||||
const map = new Map(
|
||||
tripleCalibPatches.map((p) => [p.nodeId, p.params]),
|
||||
);
|
||||
updateNodesParams(map);
|
||||
}}
|
||||
>
|
||||
Configurer f_w + z_ua evap/cond
|
||||
</button>
|
||||
)}
|
||||
{calibActuatorPatches.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
className="mt-0.5 rounded border border-sky-600 bg-white px-2 py-1 text-[10px] font-medium text-sky-800 hover:bg-sky-100"
|
||||
className="rounded border border-sky-600 bg-white px-2 py-1 text-[10px] font-medium text-sky-800 hover:bg-sky-100"
|
||||
onClick={() => {
|
||||
const map = new Map(
|
||||
calibActuatorPatches.map((p) => [p.nodeId, p.params]),
|
||||
@@ -360,6 +382,7 @@ export default function PropertiesPanel() {
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isRegLoop && panelMode === "results" && (
|
||||
|
||||
@@ -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[] = [
|
||||
|
||||
@@ -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<CalibNodeData>[],
|
||||
edges: Edge[],
|
||||
): CalibParamPatch[] {
|
||||
const probes = nodes.filter((n) => n.data.type === "Probe");
|
||||
let suction: Node<CalibNodeData> | undefined;
|
||||
let discharge: Node<CalibNodeData> | undefined;
|
||||
let evap: Node<CalibNodeData> | undefined;
|
||||
let cond: Node<CalibNodeData> | undefined;
|
||||
let comp: Node<CalibNodeData> | 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.
|
||||
*/
|
||||
|
||||
@@ -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).",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user