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,
|
calibrateAdjacentActuatorPatches,
|
||||||
collectCalibHints,
|
collectCalibHints,
|
||||||
isCalibRelevantNode,
|
isCalibRelevantNode,
|
||||||
|
setupTripleCycleCalibPatches,
|
||||||
withDerivedUaEff,
|
withDerivedUaEff,
|
||||||
} from "@/lib/calibAssist";
|
} from "@/lib/calibAssist";
|
||||||
import { ComponentIcon } from "@/components/canvas/ComponentIcon";
|
import { ComponentIcon } from "@/components/canvas/ComponentIcon";
|
||||||
@@ -161,6 +162,11 @@ export default function PropertiesPanel() {
|
|||||||
return calibrateAdjacentActuatorPatches(node, nodes, edges);
|
return calibrateAdjacentActuatorPatches(node, nodes, edges);
|
||||||
}, [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).
|
// After a successful solve, jump to Results when selecting a part (Dymola-like).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!node) return;
|
if (!node) return;
|
||||||
@@ -345,20 +351,37 @@ export default function PropertiesPanel() {
|
|||||||
{h.message}
|
{h.message}
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
{calibActuatorPatches.length > 0 && (
|
<div className="flex flex-wrap gap-1 pt-0.5">
|
||||||
<button
|
{tripleCalibPatches.length > 0 && (
|
||||||
type="button"
|
<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"
|
type="button"
|
||||||
onClick={() => {
|
className="rounded border border-sky-700 bg-sky-700 px-2 py-1 text-[10px] font-medium text-white hover:bg-sky-800"
|
||||||
const map = new Map(
|
title="probe_1 Tsat→evap z_ua · probe_2 T→f_w · probe_2 Tsat→cond z_ua"
|
||||||
calibActuatorPatches.map((p) => [p.nodeId, p.params]),
|
onClick={() => {
|
||||||
);
|
const map = new Map(
|
||||||
updateNodesParams(map);
|
tripleCalibPatches.map((p) => [p.nodeId, p.params]),
|
||||||
}}
|
);
|
||||||
>
|
updateNodesParams(map);
|
||||||
Libérer facteurs compatibles (Free)
|
}}
|
||||||
</button>
|
>
|
||||||
)}
|
Configurer f_w + z_ua evap/cond
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{calibActuatorPatches.length > 0 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
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]),
|
||||||
|
);
|
||||||
|
updateNodesParams(map);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Libérer facteurs compatibles (Free)
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
adjacentProbeNames,
|
adjacentProbeNames,
|
||||||
calibrateAdjacentActuatorPatches,
|
calibrateAdjacentActuatorPatches,
|
||||||
collectCalibHints,
|
collectCalibHints,
|
||||||
|
setupTripleCycleCalibPatches,
|
||||||
withDerivedUaEff,
|
withDerivedUaEff,
|
||||||
type CalibNodeData,
|
type CalibNodeData,
|
||||||
} from "./calibAssist";
|
} from "./calibAssist";
|
||||||
@@ -203,6 +204,41 @@ describe("calibAssist — Fixed/Free générique", () => {
|
|||||||
expect(collectCalibHints([hx], [], hx).some((h) => h.level === "tip")).toBe(true);
|
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", () => {
|
it("appends derived UA_eff = UA × z_ua", () => {
|
||||||
const nodes = [node("e", "Evaporator", "evap", { ua: 8000, z_ua: 1 })];
|
const nodes = [node("e", "Evaporator", "evap", { ua: 8000, z_ua: 1 })];
|
||||||
const solved: SolvedVariable[] = [
|
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).
|
// Baseline always visible — otherwise users see nothing when all Fixed (default).
|
||||||
if (hints.length === 0 && isCalibRelevantNode(focus)) {
|
if (hints.length === 0 && isCalibRelevantNode(focus)) {
|
||||||
if (focus.data.type === "Probe") {
|
if (focus.data.type === "Probe") {
|
||||||
@@ -216,9 +263,9 @@ export function collectCalibHints(
|
|||||||
level: "tip",
|
level: "tip",
|
||||||
component: focus.data.name,
|
component: focus.data.name,
|
||||||
message:
|
message:
|
||||||
"Fixed/Free : coche Fixed sur une mesure (Tsat, P, T, Capacity…) " +
|
"Calib cycle : probe aspiration → Fixed Tsat (SST) + Free z_ua évap ; " +
|
||||||
"puis libère le facteur compatible sur le composant adjacent " +
|
"probe refoulement → Fixed T (DGT) + Free f_w, Fixed Tsat (SDT) + Free z_ua cond. " +
|
||||||
"(z_ua, z_dp, z_flow, f_w).",
|
"Ne coche pas P ni X.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -301,6 +348,84 @@ export function calibrateAdjacentZUaPatch(
|
|||||||
return zUa ?? patches[0] ?? null;
|
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.
|
* Append derived UA_eff = UA × z_ua when both are known after solve.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -531,17 +531,17 @@ export const COMPONENTS: ComponentMeta[] = [
|
|||||||
key: "f_w",
|
key: "f_w",
|
||||||
label: "f_w (rétention énergie)",
|
label: "f_w (rétention énergie)",
|
||||||
kind: "number",
|
kind: "number",
|
||||||
default: 1.0,
|
default: 0.95,
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 1,
|
max: 1,
|
||||||
section: "Calibration",
|
section: "Calibration",
|
||||||
fixable: true,
|
fixable: true,
|
||||||
defaultFixed: true,
|
defaultFixed: true,
|
||||||
actuatorFactor: "f_w",
|
actuatorFactor: "f_w",
|
||||||
freeMin: 0.0,
|
freeMin: 0.5,
|
||||||
freeMax: 1.0,
|
freeMax: 1.0,
|
||||||
description:
|
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) {
|
if (p.actuatorFactor && !fixed) {
|
||||||
const n = typeof raw === "number" ? raw : Number(raw);
|
const n = typeof raw === "number" ? raw : Number(raw);
|
||||||
// Keep the UI value as Newton start (do not rewrite 1.0 → 0.3 — that
|
// Keep the UI value as Newton start. For f_w, avoid the upper bound 1.0
|
||||||
// pushed Free Z into a fragile basin and amplified HashMap-order flakes).
|
// (adiabatic) which saturates the unknown and can singularize J.
|
||||||
const initial = Number.isFinite(n) ? n : 1.0;
|
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({
|
freeActs.push({
|
||||||
factor: p.actuatorFactor,
|
factor: p.actuatorFactor,
|
||||||
initial,
|
initial,
|
||||||
|
|||||||
Reference in New Issue
Block a user