/** * Entropyk component catalogue. * * Mirrors the backend `/api/components` endpoint. Each entry defines: * - type: CLI JSON "type" string (matches crates/cli/src/run.rs create_component) * - label: human-readable name shown in the palette and node * - category: palette grouping * - ports: ordered port names (inlet/outlet/suction/discharge/...) * — used to place React Flow handles. * - color: hex colour for the node header * - params: editable parameters with type, unit, default, required flag */ export type ParamKind = "number" | "string" | "boolean"; export interface ParamMeta { key: string; label: string; kind: ParamKind; unit?: string; default?: number | string | boolean; required?: boolean; section?: string; description?: string; min?: number; max?: number; step?: number; options?: Array<{ value: string | number; label: string }>; advanced?: boolean; /** * Dymola/EES-style Fixed checkbox. * Fixed ON = value imposed (parameter or measured target). * Fixed OFF = free for the solver (calibration tuner / free unknown). */ fixable?: boolean; /** Default for the Fixed checkbox when `fixable` (true = Fixed). */ defaultFixed?: boolean; /** * When Fixed ON: use this param value as a control setpoint for the named * measure output (e.g. `saturationTemperature`, `superheat`). */ measureOutput?: string; /** * When Fixed OFF: free this actuator/calibration factor (e.g. `z_ua`). * Requires a Fixed measure on the same component to stay DoF-balanced. */ actuatorFactor?: string; /** Bounds used when the factor is free (actuator min/max). */ freeMin?: number; freeMax?: number; /** * Whether this param can be picked as a Multi-run sweep axis. * Source of truth for sweepability — replaces the old hardcoded whitelist. */ sweepable?: boolean; /** Multi-run optgroup for the param (only meaningful when `sweepable`). */ sweepGroup?: "boundaries" | "thermal" | "machine" | "global"; } /** Internal params key prefix for Fixed flags (not sent as component fields). */ export const FIXED_FLAG_PREFIX = "__fixed_"; export function fixedFlagKey(paramKey: string): string { return `${FIXED_FLAG_PREFIX}${paramKey}`; } /** Whether a param is Fixed (default = defaultFixed ?? true). */ export function isParamFixed( params: Record, meta: ParamMeta, ): boolean { if (!meta.fixable) return true; const flag = params[fixedFlagKey(meta.key)]; if (flag === true || flag === "true") return true; if (flag === false || flag === "false") return false; return meta.defaultFixed !== false; } export interface ComponentMeta { type: string; label: string; category: string; /** Short purpose shown under the component title (plain language). */ description: string; /** * Contextual help (French) shown in the Help panel. * Explain when to use it, main ports, and typical Fixed/Free usage. */ help?: string; /** Doc file under docs/components/ (without path), e.g. "flooded-evaporator". */ docSlug?: string; ports: string[]; color: string; params: ParamMeta[]; } export function genericMeta(type?: string): ComponentMeta { const cleanType = String(type || "GenericComponent"); return { type: cleanType, label: cleanType, category: "Generic", description: `Composant thermodynamique (${cleanType})`, ports: ["inlet", "outlet"], color: "#64748b", params: [], }; } /** * Port side on the node: "inlet"-like ports go on the left, "outlet"-like * ports on the right. Economizer / liquid_outlet sit on the top/bottom. */ export function portSide(port: string): "left" | "right" | "top" | "bottom" { const p = String(port || ""); if (p === "secondary_inlet") return "bottom"; if (p === "secondary_outlet") return "top"; if (p === "hot_inlet" || p === "cold_inlet") return "left"; if (p === "hot_outlet" || p === "cold_outlet") return "right"; if (p.startsWith("outlet")) return "right"; if (p === "discharge") return "right"; if (p === "liquid_outlet" || p === "vapor_outlet") return "bottom"; if (p.startsWith("inlet")) return "left"; if (p === "suction") return "left"; if (p === "economizer") return "top"; return "left"; } /** True for the secondary (heat-transfer fluid / caloporteur) ports of an exchanger. */ export function isSecondaryPort(port: string): boolean { const p = String(port || ""); return p === "secondary_inlet" || p === "secondary_outlet"; } /** * Whether a port emits flow (source) or receives it (target). * Flow in a P&ID is directional: a connection is dragged from a source * (outlet/discharge) to a target (inlet/suction). */ export function portRole(port: string): "source" | "target" { const p = String(port || ""); if ( p.startsWith("outlet") || p === "discharge" || p === "liquid_outlet" || p === "vapor_outlet" || p.endsWith("_outlet") ) { return "source"; } return "target"; } /** * Per-family node footprint (px), Dymola-style: a heat exchanger is a wide * vessel, a separator is a tall column, a pipe is a long thin run, a valve is * compact. Size encodes what the part is, not just decoration. */ export function nodeSize(type: string): { w: number; h: number } { const t = String(type || ""); if (t === "Probe") return { w: 36, h: 36 }; if (t === "SaturatedController") return { w: 184, h: 96 }; if (t.includes("Compressor")) return { w: 76, h: 76 }; if (t === "HeatExchanger") return { w: 92, h: 92 }; // Flooded is a horizontal drum; plate a tall pack; coils wide batteries. if (t.includes("Flooded")) return { w: 128, h: 92 }; if (t.includes("Bphx")) return { w: 80, h: 110 }; if (t.includes("FinCoil") || t.includes("AirCooled") || t.includes("Mchx")) return { w: 126, h: 88 }; if (t.includes("Condenser") || t.includes("Evaporator")) return { w: 126, h: 88 }; if (t.includes("Valve")) return { w: 56, h: 56 }; if (t === "Pump" || t === "Fan") return { w: 72, h: 72 }; if (t === "Pipe" || t.includes("Pipe") || t === "AirDuct") return { w: 118, h: 42 }; if (t === "Drum") return { w: 58, h: 108 }; if (t === "FlowSplitter" || t === "FlowMerger") return { w: 72, h: 64 }; if (t.endsWith("Source") || t.endsWith("Sink")) return { w: 62, h: 56 }; return { w: 88, h: 58 }; } /** Short label shown next to a handle on the node. */ export function portLabel(port: string): string { const map: Record = { inlet: "in", outlet: "out", suction: "suc", discharge: "dis", economizer: "eco", liquid_outlet: "liq", vapor_outlet: "vap", outlet_a: "A", outlet_b: "B", inlet_a: "A", inlet_b: "B", hot_inlet: "h·in", hot_outlet: "h·out", cold_inlet: "c·in", cold_outlet: "c·out", secondary_inlet: "htf·in", secondary_outlet: "htf·out", }; return map[port] ?? port; } export const COMPONENT_CATEGORIES = [ "Compressors", "Heat Exchangers", "Expansion", "Piping", "Hydraulics", "Boundaries", "Instrumentation", "Advanced", "Generic", ] as const; export const COMPONENTS: ComponentMeta[] = [ // ── Instrumentation: measurement taps for calibration (Probe = all measurements) ── { type: "Probe", label: "Probe (sonde)", category: "Instrumentation", description: "Sonde de mesure posée sur une ligne. Lit P/T/SH/SC/SST/SDT/DGT/DSH/capacité/ṁ/h à cet endroit. Toute calibration impose sa cible sur un Probe.", help: "Sonde de calibration (Probe)\n" + "Toute calibration se fait via un Probe posé sur une ligne (règle dure).\n\n" + "1. Glisse le Probe sur un fil du schéma (comme pour insérer un Pipe).\n" + "2. Choisis la grandeur mesurée (SST, SDT, DGT, DSH, SH, SC, T, P, débit, capacité, enthalpie) et le fluide.\n" + "3. Coche Fixed sur « Cible » et tape la valeur mesurée.\n" + "4. Libère le Z-factor correspondant (Z_UA, Z_dP, Z_flow…) sur le composant à calibrer.\n" + "Le solveur appaire automatiquement le Probe (mesure) et le Z-factor libre (inconnue).", ports: ["inlet", "outlet"], color: "#0ea5e9", params: [ { key: "measure", label: "Grandeur mesurée", kind: "string", default: "SH", required: true, section: "Mesure", description: "Grandeur physique mesurée à l'emplacement du Probe. SST/SDT = Tsat(P), SH/DSH = T−Tsat(P), SC = Tsat(P)−T.", options: [ { value: "SST", label: "SST (Tsat aspiration)" }, { value: "SDT", label: "SDT (Tsat refoulement)" }, { value: "DGT", label: "DGT (T° gaz refoulement)" }, { value: "DSH", label: "DSH (surch. refoulement)" }, { value: "SH", label: "SH (surchauffe)" }, { value: "SC", label: "SC (sous-refroid.)" }, { value: "T", label: "T (température)" }, { value: "P", label: "P (pression)" }, { value: "MassFlow", label: "Débit massique" }, { value: "Capacity", label: "Capacité (kW)" }, { value: "Enthalpy", label: "Enthalpie" }, ], }, { key: "fluid", label: "Fluide", kind: "string", default: "R134a", required: true, section: "Mesure", description: "Fluide frigorigène pour le calcul de Tsat/SH/SC (via CoolProp).", }, { key: "target", label: "Cible (Fixed = impose)", kind: "number", section: "Calibration", fixable: true, defaultFixed: false, // Auto: resolved from `measure` at config-build time // (SST/SDT → saturationTemperature, SH/DSH → superheat, SC → subcooling, // DGT/T → temperature, P → pressure, MassFlow → massFlowRate, // Capacity → capacity, Enthalpy → heatTransferRate as carrier). measureOutput: "auto", description: "Coche Fixed et tape la valeur mesurée (K pour températures, K pour SH/SC, Pa pour P, kg/s pour débit, W pour capacité, J/kg pour h).", }, ], }, // ── Advanced regulation (optional — most users use Fixed on Calibration tab) ── { type: "SaturatedController", label: "Regulation loop (advanced)", category: "Advanced", description: "Optional. Links a measured quantity (e.g. superheat) to a free actuator (e.g. valve opening). For simple calibration (SST + Z_UA), prefer the Calibration tab Fixed checkboxes — you do not need this block.", help: "À quoi ça sert ?\n" + "• Boucle de régulation avancée : tu imposes une grandeur mesurée et tu libères un actionneur.\n" + "• Exemple : superheat mesuré → ouverture EXV libre.\n\n" + "Quand NE PAS l’utiliser ?\n" + "• Pour calibrer Z_UA sur une SST : utilise l’onglet Calibration du composant (cases Fixed).\n" + "• Ce bloc est pour les cas multi-objectifs / priorités (override chain).\n\n" + "Ce n’est pas un composant thermodynamique : il n’a pas de ports fluide.", docSlug: undefined, ports: [], color: "#7c3aed", params: [ { key: "measure_component", label: "Composant mesuré", kind: "string", default: "evap", required: true, section: "Mesure (imposé)", description: "Nom du composant dont on lit la grandeur (ex. evap).", }, { key: "measure_output", label: "Grandeur mesurée", kind: "string", default: "saturationTemperature", required: true, section: "Mesure (imposé)", description: "Ex. saturationTemperature (SST/SDT), superheat, capacity…", }, { key: "target", label: "Consigne", kind: "number", default: 278.15, required: true, section: "Mesure (imposé)", description: "Valeur cible en SI : K pour températures, Pa, W, kg/s…", }, { key: "gain", label: "Gain", kind: "number", default: -0.5, section: "Mesure (imposé)", description: "Signe et échelle de l’erreur (consigne − mesure).", }, { key: "actuator_component", label: "Composant actionné", kind: "string", default: "evap", required: true, section: "Actionneur (libre)", description: "Composant dont on libère un facteur (souvent le même que mesuré).", }, { key: "actuator_factor", label: "Facteur libre", kind: "string", default: "z_ua", required: true, section: "Actionneur (libre)", description: "z_ua, opening, z_flow, z_power… Valeur initiale ci-dessous = point de départ.", }, { key: "initial", label: "Valeur initiale", kind: "number", default: 1.0, required: true, section: "Actionneur (libre)", description: "Départ du facteur libre (ex. Z_UA = 1).", }, { key: "min", label: "Minimum", kind: "number", default: 0.2, required: true, section: "Actionneur (libre)", }, { key: "max", label: "Maximum", kind: "number", default: 3.0, required: true, section: "Actionneur (libre)", }, { key: "band", label: "Bande de saturation", kind: "number", default: 5.0, section: "Réglages fins", min: 0.000001, advanced: true }, { key: "smooth_eps", label: "Lissage saturation", kind: "number", default: 0.001, section: "Réglages fins", min: 0.000000001, advanced: true }, { key: "alpha", label: "Lissage overrides", kind: "number", default: 0.001, section: "Réglages fins", min: 0.000000001, advanced: true }, ], }, // ── Compressors ──────────────────────────────────────────────────────── { type: "IsentropicCompressor", label: "Isentropic Compressor", category: "Compressors", description: "Compresseur physique : η_is + (option) ṁ = ρ·V·N·η_vol.", help: "Modèle : compression isentropique corrigée η_is.\n" + "h_dis = h_suc + (h_is − h_suc) / η_is\n" + "Mode émergent : ṁ = ρ_suc · V_d · N · η_vol · z_flow\n" + "T_cond / T_evap = guesses d’init si pressions émergentes côté HX.\n" + "Doc : docs/components/isentropic-compressor.md · Inventaire : correlations-and-maps.md", docSlug: "isentropic-compressor", ports: ["inlet", "outlet"], color: "#6366f1", params: [ { key: "isentropic_efficiency", label: "η isentropique", kind: "number", default: 0.75, required: true, section: "Model", min: 0.05, max: 1.0, description: "η_is : h_dis = h_suc + (h_is−h_suc)/η_is", sweepable: true, sweepGroup: "machine", }, { key: "emergent_pressure", label: "Pression émergente (ṁ par cylindrée)", kind: "boolean", default: true, section: "Model", description: "ON : ṁ = ρ·V·N·η_vol (recommandé machine réelle).", }, { key: "displacement_m3", label: "Cylindrée V_d", kind: "number", unit: "m³/tr", default: 5.0e-5, section: "Displacement map", min: 0, description: "Volume balayé par tour — requis si pression émergente.", }, { key: "speed_hz", label: "Vitesse N", kind: "number", unit: "tr/s", default: 50.0, section: "Displacement map", min: 0, }, { key: "volumetric_efficiency", label: "η volumétrique", kind: "number", default: 0.92, section: "Displacement map", min: 0.05, max: 1.0, }, { key: "t_cond_k", label: "T_cond guess", kind: "number", unit: "K", default: 323.15, section: "Init / design", description: "Guess d’init (ou pin si non émergent).", }, { key: "t_evap_k", label: "T_evap guess", kind: "number", unit: "K", default: 275.15, section: "Init / design", }, { key: "superheat_k", label: "Superheat aspiration guess", kind: "number", unit: "K", default: 5.0, section: "Init / design", }, ], }, { type: "Compressor", label: "Map Compressor (AHRI / SST-SDT)", category: "Compressors", description: "Deux modèles : AHRI 540 (M1–M10) ou polynôme bilinéaire SST/SDT.", help: "Choisis model_type :\n" + "• Ahri540 : ṁ = M1·(1−(Ps/Pd)^(1/M2))·ρ·V·N/60 ; Ẇ = M3…M10\n" + "• SstSdt : ṁ,Ẇ = a00 + a10·SST + a01·SDT + a11·SST·SDT (SST/SDT en K)\n" + "Doc : compressor-ahri540.md · correlations-and-maps.md", docSlug: "compressor-ahri540", ports: ["inlet", "outlet"], color: "#6366f1", params: [ { key: "model_type", label: "Modèle de carte", kind: "string", default: "Ahri540", section: "Model", required: true, options: [ { value: "Ahri540", label: "AHRI 540 (M1–M10)" }, { value: "SstSdt", label: "Polynôme SST / SDT (bilinéaire)" }, ], description: "Ahri540 = coefficients M1–M10. SstSdt = carte constructeur ṁ(SST,SDT), Ẇ(SST,SDT).", }, { key: "fluid", label: "Frigorigène", kind: "string", default: "R134a", section: "Machine", required: true, }, { key: "speed_rpm", label: "Vitesse", kind: "number", unit: "tr/min", default: 3000, required: true, section: "Machine", min: 1, sweepable: true, sweepGroup: "machine", }, { key: "displacement_m3", label: "Cylindrée", kind: "number", unit: "m³", default: 0.0005, required: true, section: "Machine", min: 0, }, { key: "efficiency", label: "Efficacité", kind: "number", default: 0.85, section: "Machine", min: 0.05, max: 1, }, // AHRI 540 { key: "m1", label: "M1 (flow scale)", kind: "number", default: 0.85, section: "AHRI 540 (si Ahri540)", description: "ṁ ∝ M1·(1−(Ps/Pd)^(1/M2))" }, { key: "m2", label: "M2 (PR exponent)", kind: "number", default: 2.5, section: "AHRI 540 (si Ahri540)", min: 0.01 }, { key: "m3", label: "M3 (Ẇ cool const)", kind: "number", default: 500, section: "AHRI 540 (si Ahri540)", unit: "W" }, { key: "m4", label: "M4 (Ẇ cool · PR)", kind: "number", default: 1500, section: "AHRI 540 (si Ahri540)" }, { key: "m5", label: "M5 (Ẇ cool · Ts)", kind: "number", default: -2.5, section: "AHRI 540 (si Ahri540)" }, { key: "m6", label: "M6 (Ẇ cool · Td)", kind: "number", default: 1.8, section: "AHRI 540 (si Ahri540)" }, { key: "m7", label: "M7 (Ẇ heat const)", kind: "number", default: 600, section: "AHRI 540 (si Ahri540)", unit: "W", advanced: true }, { key: "m8", label: "M8 (Ẇ heat · PR)", kind: "number", default: 1600, section: "AHRI 540 (si Ahri540)", advanced: true }, { key: "m9", label: "M9 (Ẇ heat · Ts)", kind: "number", default: -3.0, section: "AHRI 540 (si Ahri540)", advanced: true }, { key: "m10", label: "M10 (Ẇ heat · Td)", kind: "number", default: 2.0, section: "AHRI 540 (si Ahri540)", advanced: true }, // SST/SDT bilinear { key: "mf_a00", label: "ṁ a00", kind: "number", default: 0.05, section: "Map SST/SDT (si SstSdt)", description: "ṁ = a00 + a10·SST + a01·SDT + a11·SST·SDT (K)" }, { key: "mf_a10", label: "ṁ a10 (×SST)", kind: "number", default: 0.001, section: "Map SST/SDT (si SstSdt)" }, { key: "mf_a01", label: "ṁ a01 (×SDT)", kind: "number", default: 0.0005, section: "Map SST/SDT (si SstSdt)" }, { key: "mf_a11", label: "ṁ a11 (×SST·SDT)", kind: "number", default: 0.00001, section: "Map SST/SDT (si SstSdt)" }, { key: "pw_b00", label: "Ẇ b00", kind: "number", default: 1000, unit: "W", section: "Map SST/SDT (si SstSdt)", description: "Ẇ = b00 + b10·SST + b01·SDT + b11·SST·SDT (K)" }, { key: "pw_b10", label: "Ẇ b10 (×SST)", kind: "number", default: 50, section: "Map SST/SDT (si SstSdt)" }, { key: "pw_b01", label: "Ẇ b01 (×SDT)", kind: "number", default: 30, section: "Map SST/SDT (si SstSdt)" }, { key: "pw_b11", label: "Ẇ b11 (×SST·SDT)", kind: "number", default: 0.5, section: "Map SST/SDT (si SstSdt)" }, ], }, { type: "CentrifugalCompressor", label: "Centrifugal Compressor", category: "Compressors", description: "Centrifugal compressor with φ–Mach polytropic map (CLI-wired).", help: "Carte φ / Mach → tête polytropique et η_p. VFD via speed_rpm.\n" + "Modèle encore simplifié côté solveur (Jacobien partiel).", ports: ["inlet", "outlet"], color: "#4338ca", params: [ { key: "diameter_m", label: "Impeller diameter", kind: "number", unit: "m", default: 0.25, required: true }, { key: "speed_rpm", label: "Speed", kind: "number", unit: "rpm", default: 9000.0, required: true, sweepable: true, sweepGroup: "machine" }, { key: "gas_constant", label: "Gas constant R", kind: "number", unit: "J/kg·K", default: 188.9, advanced: true }, { key: "gamma", label: "Heat capacity ratio γ", kind: "number", default: 1.12, advanced: true }, ], }, { type: "ScrewEconomizerCompressor", label: "Screw Compressor (Economizer)", category: "Compressors", description: "Vis 3 ports — polynôme bilinéaire SST/SDT + presets.", help: "Carte bilinéaire :\n" + "ṁ = a00 + a10·SST + a01·SDT + a11·SST·SDT\n" + "Ẇ = b00 + b10·SST + b01·SDT + b11·SST·SDT\n" + "Presets : bitzer_generic_200kw, grasso_generic_200kw.\n" + "Ports : suction, discharge, economizer.\n" + "Doc : screw-economizer-compressor.md · correlations-and-maps.md", docSlug: "screw-economizer-compressor", ports: ["suction", "discharge", "economizer"], color: "#4f46e5", params: [ { key: "preset", label: "Preset carte", kind: "string", default: "bitzer_generic_200kw", section: "Model", options: [ { value: "bitzer_generic_200kw", label: "Bitzer generic ~200 kW" }, { value: "grasso_generic_200kw", label: "Grasso generic ~200 kW" }, { value: "", label: "Custom (defaults)" }, ], description: "Remplit les coeffs mf_* / pw_* (surchargeables).", }, { key: "frequency_hz", label: "Fréquence", kind: "number", unit: "Hz", default: 50.0, section: "Machine", sweepable: true, sweepGroup: "machine", }, { key: "nominal_frequency_hz", label: "Fréquence nominale", kind: "number", unit: "Hz", default: 50.0, section: "Machine", }, { key: "mechanical_efficiency", label: "η mécanique", kind: "number", default: 0.92, section: "Machine", min: 0.05, max: 1, }, { key: "economizer_fraction", label: "Fraction éco ṁ", kind: "number", default: 0.13, section: "Machine", min: 0, max: 1, }, { key: "volume_index", label: "Volume index Vi", kind: "number", default: 2.8, section: "Machine", min: 1.1, max: 6, description: "Built-in Vi; slide valve reduces effective Vi.", sweepable: true, sweepGroup: "machine", }, { key: "slide_valve_position", label: "Slide valve (0–1)", kind: "number", default: 1.0, section: "Machine", min: 0.05, max: 1, sweepable: true, sweepGroup: "machine", }, // Mass-flow bilinear (Bitzer defaults as numbers) { key: "mf_a00", label: "ṁ a00", kind: "number", default: 1.35, section: "Map ṁ (SST,SDT)", description: "ṁ = a00 + a10·SST + a01·SDT + a11·SST·SDT" }, { key: "mf_a10", label: "ṁ a10 (×SST)", kind: "number", default: 0.004, section: "Map ṁ (SST,SDT)" }, { key: "mf_a01", label: "ṁ a01 (×SDT)", kind: "number", default: -0.0025, section: "Map ṁ (SST,SDT)" }, { key: "mf_a11", label: "ṁ a11 (×SST·SDT)", kind: "number", default: 0.000012, section: "Map ṁ (SST,SDT)" }, { key: "pw_b00", label: "Ẇ b00", kind: "number", default: 58000, unit: "W", section: "Map Ẇ (SST,SDT)", description: "Ẇ = b00 + b10·SST + b01·SDT + b11·SST·SDT" }, { key: "pw_b10", label: "Ẇ b10 (×SST)", kind: "number", default: 180, section: "Map Ẇ (SST,SDT)" }, { key: "pw_b01", label: "Ẇ b01 (×SDT)", kind: "number", default: -280, section: "Map Ẇ (SST,SDT)" }, { key: "pw_b11", label: "Ẇ b11 (×SST·SDT)", kind: "number", default: 0.4, section: "Map Ẇ (SST,SDT)" }, ], }, // ── Heat exchangers ──────────────────────────────────────────────────── { type: "Condenser", label: "Condenser", category: "Heat Exchangers", description: "Condenseur frigo avec secondaire eau/air (ε-NTU).", help: "Rôle : rejette la chaleur du frigorigène vers l’eau ou l’air.\n\n" + "Ports : inlet/outlet frigo · secondary_in/out caloporteur.\n\n" + "ΔP frigo : dp_model=msh (Müller–Steinhagen–Heck + accélération) avec géométrie tube, " + "ou quadratic (dp_nominal), ou isobaric.\n\n" + "Machine réelle : cocher « Solve condensing pressure » et câbler Source/Sink secondaires.\n" + "Calibration : Fixed sur SDT + Z_UA libre (Z_UA défaut = 1).", docSlug: "condenser", ports: ["inlet", "outlet", "secondary_inlet", "secondary_outlet"], color: "#ef4444", params: [ { key: "manufacturer", label: "Manufacturer", kind: "string", default: "", section: "Identification" }, { key: "model", label: "Model / reference", kind: "string", default: "", section: "Identification" }, { key: "fluid", label: "Refrigerant", kind: "string", default: "R134a", section: "Fluids" }, { key: "secondary_fluid", label: "Secondary fluid", kind: "string", default: "Water", section: "Fluids" }, { key: "ua", label: "UA", kind: "number", unit: "W/K", default: 5000.0, required: true, section: "Thermal model", min: 0.0, sweepable: true, sweepGroup: "thermal" }, { key: "t_sat_k", label: "Fixed saturation temperature", kind: "number", unit: "K", default: 323.15, section: "Thermal model", description: "Used only in fixed-pressure mode. Do not treat it as a solved-cycle input when emergent pressure is enabled." }, { key: "secondary_inlet_temp_c", label: "Secondary inlet T (rating only)", kind: "number", unit: "°C", default: 30.0, section: "Secondary side (rating)", description: "Rating scalar. Prefer live secondary ports + BrineSource for system mode.", }, { key: "secondary_mass_flow_kg_s", label: "Secondary mass flow (rating only)", kind: "number", unit: "kg/s", default: 2.0, section: "Secondary side (rating)", min: 0.0, }, { key: "secondary_cp_j_per_kgk", label: "Secondary specific heat (rating only)", kind: "number", unit: "J/kg·K", default: 4186.0, section: "Secondary side (rating)", min: 0.0, advanced: true, }, { key: "secondary_humidity_ratio", label: "Secondary humidity ratio", kind: "number", unit: "kg/kg", default: 0.0, section: "Secondary side (rating)", min: 0.0, advanced: true }, { key: "dp_model", label: "Refrigerant ΔP model", kind: "string", default: "msh", section: "Hydraulics", options: [ { value: "msh", label: "MSH 1986 (tube, + accel)" }, { value: "friedel", label: "Friedel 1979 (tube, + accel)" }, { value: "quadratic", label: "Quadratic (dp_nominal)" }, { value: "isobaric", label: "Isobaric (ΔP = 0)" }, ], description: "MSH = NIST EVAP-COND default two-phase friction at mean quality + acceleration. Quadratic = Modelica Buildings k·ṁ². Flooded shell-side → isobaric.", }, { key: "tube_length_m", label: "Tube length", kind: "number", unit: "m", default: 6.0, section: "Hydraulics", min: 0.0, description: "Path length for MSH/Friedel." }, { key: "tube_diameter_m", label: "Tube diameter", kind: "number", unit: "m", default: 0.0095, section: "Hydraulics", min: 0.0, advanced: true }, { key: "n_parallel_tubes", label: "Parallel tubes", kind: "number", default: 2, section: "Hydraulics", min: 1, advanced: true }, { key: "rated_pressure_drop_pa", label: "Design ΔP (quadratic)", kind: "number", unit: "Pa", default: 15000, section: "Hydraulics", min: 0.0, advanced: true, description: "Only when dp_model=quadratic. Calibrates k = ΔP/ṁ².", }, { key: "rated_mass_flow_kg_s", label: "Design ṁ (quadratic)", kind: "number", unit: "kg/s", default: 0.05, section: "Hydraulics", min: 0.0, advanced: true, }, { key: "pressure_drop_coeff_pa_s2_kg2", label: "Pressure-drop coefficient k", kind: "number", unit: "Pa·s²/kg²", section: "Hydraulics", min: 0.0, advanced: true, description: "Direct k for quadratic mode.", }, { key: "secondary_rated_pressure_drop_pa", label: "Water/air design ΔP", kind: "number", unit: "Pa", default: 30000, section: "Secondary hydraulics", min: 0.0, description: "Secondary-side ΔP at rated ṁ (Modelica dp_nominal). NOT the refrigerant tube MSH model. 0 = isobaric water path.", }, { key: "secondary_rated_m_flow_kg_s", label: "Water/air design ṁ", kind: "number", unit: "kg/s", default: 0.45, section: "Secondary hydraulics", min: 0.0, description: "Pairs with Water/air design ΔP → k = ΔP/ṁ².", }, { key: "emergent_pressure", label: "Solve condensing pressure", kind: "boolean", default: true, section: "Solved-cycle mode", description: "ON (recommandé machine): P_cond émerge du bilan — pas un Dirichlet fixe (esprit Modelica cycle fermé)." }, { key: "subcooling_k", label: "Outlet subcooling closure", kind: "number", unit: "K", default: 5.0, section: "Solved-cycle mode", min: 0.0, description: "FIX residual when emergent_pressure is ON (+1 eq, closes P_cond with energy balance)." }, { key: "fan_head_pressure_target_c", label: "Fan head-pressure target", kind: "number", unit: "°C", section: "Controls", advanced: true }, { key: "flooded_head_pressure_target_c", label: "Flooded head-pressure target", kind: "number", unit: "°C", section: "Controls", advanced: true }, { key: "skip_pressure_eq", label: "Skip pressure equation", kind: "boolean", default: false, section: "Solver structure", advanced: true }, { key: "z_ua", label: "Z_UA (UA factor)", kind: "number", default: 1.0, section: "Calibration", min: 0.000001, fixable: true, defaultFixed: true, actuatorFactor: "z_ua", freeMin: 0.2, freeMax: 3.0, description: "Fixed = valeur figée. Décocher Fixed = libre (calibration).", }, { key: "z_dp", label: "Z_dP (pressure-drop factor)", kind: "number", default: 1.0, section: "Calibration", min: 0.000001, advanced: true, fixable: true, defaultFixed: true, actuatorFactor: "z_dp", freeMin: 0.2, freeMax: 3.0, }, ], }, { type: "Evaporator", label: "Evaporator", category: "Heat Exchangers", description: "Évaporateur DX (détente directe) avec secondaire eau/brine.", help: "Rôle : évaporateur à détente directe (sortie surchauffée).\n\n" + "Ports : inlet/outlet frigo · secondary_in/out.\n\n" + "Différence flooded : ici la sortie n’est pas noyée — superheat en sortie.\n" + "Calibration : Fixed sur SST + Z_UA libre (défaut Z_UA = 1).", docSlug: "evaporator", ports: ["inlet", "outlet", "secondary_inlet", "secondary_outlet"], color: "#3b82f6", params: [ { key: "manufacturer", label: "Manufacturer", kind: "string", default: "", section: "Identification" }, { key: "model", label: "Model / reference", kind: "string", default: "", section: "Identification" }, { key: "fluid", label: "Refrigerant", kind: "string", default: "R134a", section: "Fluids" }, { key: "secondary_fluid", label: "Secondary fluid", kind: "string", default: "Water", section: "Fluids" }, { key: "ua", label: "UA", kind: "number", unit: "W/K", default: 6000.0, required: true, section: "Thermal model", min: 0.0, sweepable: true, sweepGroup: "thermal" }, { key: "t_sat_k", label: "Fixed saturation temperature", kind: "number", unit: "K", default: 275.15, section: "Thermal model", description: "Used only in fixed-pressure mode." }, { key: "superheat_k", label: "Outlet superheat closure", kind: "number", unit: "K", default: 5.0, section: "Thermal model", min: 0.0 }, { key: "z_ua", label: "Z_UA (UA factor)", kind: "number", default: 1.0, section: "Calibration", min: 0.000001, fixable: true, defaultFixed: true, actuatorFactor: "z_ua", freeMin: 0.2, freeMax: 3.0, description: "Fixed = figé. Décocher Fixed = libre pour le solveur.", }, { key: "z_dp", label: "Z_dP (pressure-drop factor)", kind: "number", default: 1.0, section: "Calibration", min: 0.000001, advanced: true, fixable: true, defaultFixed: true, actuatorFactor: "z_dp", freeMin: 0.2, freeMax: 3.0, }, { key: "secondary_inlet_temp_c", label: "Secondary inlet T (rating only)", kind: "number", unit: "°C", default: 12.0, section: "Secondary side (rating)", description: "Rating scalar. Prefer live secondary ports + BrineSource for system mode.", }, { key: "secondary_mass_flow_kg_s", label: "Secondary mass flow (rating only)", kind: "number", unit: "kg/s", default: 1.5, section: "Secondary side (rating)", min: 0.0, }, { key: "secondary_cp_j_per_kgk", label: "Secondary specific heat (rating only)", kind: "number", unit: "J/kg·K", default: 4186.0, section: "Secondary side (rating)", min: 0.0, advanced: true, }, { key: "secondary_humidity_ratio", label: "Secondary humidity ratio", kind: "number", unit: "kg/kg", default: 0.0, section: "Secondary side (rating)", min: 0.0, advanced: true }, { key: "dp_model", label: "Refrigerant ΔP model", kind: "string", default: "msh", section: "Hydraulics", options: [ { value: "msh", label: "MSH 1986 (tube, + accel)" }, { value: "friedel", label: "Friedel 1979 (tube, + accel)" }, { value: "quadratic", label: "Quadratic (dp_nominal)" }, { value: "isobaric", label: "Isobaric (ΔP = 0)" }, ], description: "DX tube ΔP. For flooded shell-side use FloodedEvaporator (ΔP ≈ 0).", }, { key: "tube_length_m", label: "Tube length", kind: "number", unit: "m", default: 6.0, section: "Hydraulics", min: 0.0 }, { key: "tube_diameter_m", label: "Tube diameter", kind: "number", unit: "m", default: 0.0095, section: "Hydraulics", min: 0.0, advanced: true }, { key: "n_parallel_tubes", label: "Parallel tubes", kind: "number", default: 2, section: "Hydraulics", min: 1, advanced: true }, { key: "rated_pressure_drop_pa", label: "Design ΔP (quadratic)", kind: "number", unit: "Pa", default: 15000, section: "Hydraulics", min: 0.0, advanced: true, }, { key: "rated_mass_flow_kg_s", label: "Design ṁ (quadratic)", kind: "number", unit: "kg/s", default: 0.05, section: "Hydraulics", min: 0.0, advanced: true, }, { key: "pressure_drop_coeff_pa_s2_kg2", label: "Pressure-drop coefficient k", kind: "number", unit: "Pa·s²/kg²", section: "Hydraulics", min: 0.0, advanced: true, }, { key: "secondary_rated_pressure_drop_pa", label: "Water/air design ΔP", kind: "number", unit: "Pa", default: 30000, section: "Secondary hydraulics", min: 0.0, description: "Secondary-side ΔP at rated ṁ. Distinct from refrigerant dp_model. 0 = isobaric.", }, { key: "secondary_rated_m_flow_kg_s", label: "Water/air design ṁ", kind: "number", unit: "kg/s", default: 0.5, section: "Secondary hydraulics", min: 0.0, }, { key: "emergent_pressure", label: "Solve evaporating pressure", kind: "boolean", default: true, section: "Solved-cycle mode", description: "ON (recommandé machine): P_evap émerge du bilan — pas un Dirichlet fixe." }, { key: "superheat_regulated", label: "Regulate superheat with EXV controller", kind: "boolean", default: false, section: "Solved-cycle mode", description: "Drops SH residual (−1 eq). Pair with SaturatedController → EXV opening (FREE).", }, { key: "skip_pressure_eq", label: "Skip pressure equation", kind: "boolean", default: false, section: "Solver structure", advanced: true }, ], }, { type: "MchxCondenserCoil", label: "MCHX Condenser Coil", category: "Heat Exchangers", description: "Multi-channel heat exchanger air-cooled condenser coil.", ports: ["inlet", "outlet", "secondary_inlet", "secondary_outlet"], color: "#f97316", params: [ { key: "manufacturer", label: "Manufacturer", kind: "string", default: "", section: "Identification", description: "Supplier or coil manufacturer. Stored as project traceability data." }, { key: "model", label: "Model / reference", kind: "string", default: "", section: "Identification", description: "Manufacturer model, selection reference, or internal equipment code." }, { key: "ua_nominal_kw_k", label: "Nominal UA", kind: "number", unit: "kW/K", default: 15.0, required: true, section: "Thermal model", min: 0.001, description: "Overall conductance at nominal air flow." }, { key: "n_air_exponent", label: "Air-flow exponent", kind: "number", default: 0.5, section: "Thermal model", min: 0.0, max: 2.0, description: "Exponent used to scale UA with air-flow ratio." }, { key: "coil_index", label: "Coil index", kind: "number", default: 0, section: "Identification", min: 0, step: 1 }, { key: "air_inlet_temp_c", label: "Air inlet temperature", kind: "number", unit: "°C", default: 35.0, section: "Air side" }, { key: "fan_speed", label: "Fan speed ratio", kind: "number", default: 1.0, section: "Air side", min: 0.0, max: 1.0, step: 0.01, sweepable: true, sweepGroup: "machine" }, ], }, { type: "AirCooledCondenser", label: "Air-Cooled Condenser", category: "Heat Exchangers", description: "Condenser with air-side fan model. The caloporteur ports represent the ambient air stream.", ports: ["inlet", "outlet", "secondary_inlet", "secondary_outlet"], color: "#f59e0b", params: [ { key: "manufacturer", label: "Manufacturer", kind: "string", default: "", section: "Identification" }, { key: "model", label: "Model / reference", kind: "string", default: "", section: "Identification" }, { key: "fluid", label: "Refrigerant", kind: "string", default: "R134a", section: "Fluids" }, { key: "ua", label: "UA at full fan", kind: "number", unit: "W/K", default: 4000.0, required: true, section: "Thermal model", min: 0.0, sweepable: true, sweepGroup: "thermal" }, { key: "oat_k", label: "Outdoor air temperature", kind: "number", unit: "K", default: 308.15, section: "Air side", description: "Ambient dry-bulb temperature used by the condenser model.", sweepable: true, sweepGroup: "boundaries" }, { key: "approach_k", label: "Condensing approach", kind: "number", unit: "K", default: 12.0, section: "Thermal model", min: 0.0 }, ], }, { type: "FinCoilCondenser", label: "Finned-Tube Condenser Coil", category: "Heat Exchangers", description: "Round-tube plate-fin air-cooled condenser. UA and condensing temperature are derived from the actual coil face, tube rows, fin pitch and air velocity.", ports: ["inlet", "outlet", "secondary_inlet", "secondary_outlet"], color: "#d97706", params: [ { key: "manufacturer", label: "Manufacturer", kind: "string", default: "", section: "Identification", description: "Coil supplier or OEM." }, { key: "model", label: "Model / reference", kind: "string", default: "", section: "Identification", description: "Supplier selection reference or equipment code." }, { key: "fluid", label: "Refrigerant", kind: "string", default: "R134a", section: "Fluids" }, { key: "face_width_m", label: "Coil face width", kind: "number", unit: "m", default: 1.5, required: true, section: "Coil geometry", min: 0.01 }, { key: "face_height_m", label: "Coil face height", kind: "number", unit: "m", default: 1.0, required: true, section: "Coil geometry", min: 0.01 }, { key: "n_rows", label: "Tube rows", kind: "number", default: 3, required: true, section: "Coil geometry", min: 1, step: 1 }, { key: "fin_type", label: "Fin type", kind: "string", default: "louvered", section: "Fin geometry", options: [ { value: "louvered", label: "Louvered — Chang & Wang" }, { value: "slit", label: "Slit fin" }, { value: "wavy", label: "Wavy / corrugated" }, { value: "plain", label: "Plain fin" }, ], }, { key: "fin_pitch_fpi", label: "Fin pitch", kind: "number", unit: "FPI", default: 14.0, required: true, section: "Fin geometry", min: 1.0, description: "Number of fins per inch." }, { key: "fin_thickness_mm", label: "Fin thickness", kind: "number", unit: "mm", default: 0.1, section: "Fin geometry", min: 0.01 }, { key: "louver_pitch_mm", label: "Louver pitch", kind: "number", unit: "mm", default: 1.4, section: "Fin geometry", min: 0.01 }, { key: "tube_od_mm", label: "Tube outside diameter", kind: "number", unit: "mm", default: 9.525, required: true, section: "Tube geometry", min: 0.1 }, { key: "tube_pitch_mm", label: "Transverse tube pitch", kind: "number", unit: "mm", default: 25.4, required: true, section: "Tube geometry", min: 0.1, description: "Longitudinal pitch is derived using the staggered-tube ratio." }, { key: "oat_k", label: "Outdoor air temperature", kind: "number", unit: "K", default: 308.15, required: true, section: "Air side", sweepable: true, sweepGroup: "boundaries" }, { key: "air_face_velocity_m_s", label: "Air face velocity", kind: "number", unit: "m/s", default: 2.5, required: true, section: "Air side", min: 0.01, sweepable: true, sweepGroup: "boundaries" }, { key: "design_capacity_kw", label: "Design heat rejection", kind: "number", unit: "kW", default: 100.0, required: true, section: "Design point", min: 0.01 }, { key: "wet_surface", label: "Wet air-side (Wang)", kind: "boolean", default: false, section: "Air side", description: "Apply Wang wet j-factor correction when condensate forms.", }, ], }, { type: "BphxEvaporator", label: "Brazed Plate Evaporator", category: "Heat Exchangers", description: "Évaporateur à plaques brasées : géométrie + corrélation Longo/Shah → UA.", help: "Modèle : corrélation biphasique (Longo 2004 / Shah 1979 / Shah 2021) estime h et UA à partir des plaques, puis le solveur utilise un HX ε-NTU.\n\n" + "DX uniquement (sortie surchauffée). Pour un noyé calandre/tubes → FloodedEvaporator.\n\n" + "Corrélations et formules : docs/components/bphx.md", docSlug: "bphx", ports: ["inlet", "outlet", "secondary_inlet", "secondary_outlet"], color: "#0891b2", params: [ { key: "manufacturer", label: "Manufacturer", kind: "string", default: "", section: "Identification" }, { key: "model", label: "Model / reference", kind: "string", default: "", section: "Identification" }, { key: "refrigerant", label: "Refrigerant", kind: "string", default: "R134a", section: "Fluids" }, { key: "secondary_fluid", label: "Secondary fluid", kind: "string", default: "Water", section: "Fluids", description: "Water, brine, or glycol solution connected to the secondary ports." }, { key: "n_plates", label: "Number of plates", kind: "number", default: 20, required: true, section: "Plate pack", min: 2, step: 1 }, { key: "plate_length_m", label: "Effective plate length", kind: "number", unit: "m", default: 0.25, section: "Plate pack", min: 0.001 }, { key: "plate_width_m", label: "Effective plate width", kind: "number", unit: "m", default: 0.05, section: "Plate pack", min: 0.001 }, { key: "plate_thickness_mm", label: "Plate thickness", kind: "number", unit: "mm", default: 0.6, section: "Plate pack", min: 0.01 }, { key: "channel_spacing_mm", label: "Channel spacing", kind: "number", unit: "mm", default: 1.5, section: "Corrugation", min: 0.01 }, { key: "chevron_angle_deg", label: "Chevron angle", kind: "number", unit: "°", default: 60.0, section: "Corrugation", min: 10.0, max: 80.0 }, { key: "corrugation_pitch_mm", label: "Corrugation pitch", kind: "number", unit: "mm", default: 3.0, section: "Corrugation", min: 0.01 }, { key: "dh_m", label: "Hydraulic diameter override", kind: "number", unit: "m", section: "Direct manufacturer data", min: 0.000001, advanced: true, description: "Optional direct override. Use together with heat-transfer area when the supplier provides both values." }, { key: "area_m2", label: "Heat-transfer area override", kind: "number", unit: "m²", section: "Direct manufacturer data", min: 0.000001, advanced: true }, { key: "target_superheat_k", label: "Target superheat", kind: "number", unit: "K", default: 5.0, section: "Operating model", min: 0.0, description: "Brazed plate evaporators are Direct Expansion (DX) only in this project — the refrigerant outlet is always superheated. A flooded design is a system-level property (Drum + recirculation loop); use the Flooded Evaporator (shell-and-tube) component for that." }, { key: "emergent_pressure", label: "Solve evaporating pressure", kind: "boolean", default: true, section: "Solved-cycle mode", description: "ON: +1 outlet-closure residual pins h_out to target superheat so P_evap emerges from the secondary balance.", }, { key: "correlation", label: "Two-phase correlation", kind: "string", default: "Longo2004", section: "Heat-transfer model", options: [ { value: "Longo2004", label: "Longo 2004" }, { value: "Shah1979", label: "Shah 1979" }, { value: "Shah2021", label: "Shah 2021" }, ], }, { key: "dp_correlation", label: "Pressure-drop correlation", kind: "string", default: "SimplifiedChannel", section: "Pressure drop", description: "Channel friction on both hot and cold sides (independent of HTC). Default SimplifiedChannel; Martin1996 uses chevron angle.", options: [ { value: "SimplifiedChannel", label: "Simplified channel (default)" }, { value: "Martin1996", label: "Martin 1996 (chevron)" }, ], }, { key: "ua", label: "UA override", kind: "number", unit: "W/K", section: "Calibration", min: 0.0, advanced: true }, { key: "z_ua", label: "Z_UA (UA factor)", kind: "number", default: 1.0, section: "Calibration", min: 0.000001, fixable: true, defaultFixed: true, actuatorFactor: "z_ua", freeMin: 0.2, freeMax: 3.0, }, { key: "z_dp", label: "Z_dP (pressure-drop factor)", kind: "number", default: 1.0, section: "Calibration", min: 0.000001, fixable: true, defaultFixed: true, actuatorFactor: "z_dp", freeMin: 0.2, freeMax: 3.0, }, ], }, { type: "BphxCondenser", label: "Brazed Plate Condenser", category: "Heat Exchangers", description: "Condenseur à plaques brasées : géométrie + corrélation Longo/Shah → UA.", help: "Même famille que BphxEvaporator. Corrélation de condensation (Longo/Shah) → h → UA, solveur ε-NTU.\n\n" + "Détail des Nu / Re_eq / ΔP : docs/components/bphx.md", docSlug: "bphx", ports: ["inlet", "outlet", "secondary_inlet", "secondary_outlet"], color: "#dc2626", params: [ { key: "manufacturer", label: "Manufacturer", kind: "string", default: "", section: "Identification" }, { key: "model", label: "Model / reference", kind: "string", default: "", section: "Identification" }, { key: "refrigerant", label: "Refrigerant", kind: "string", default: "R134a", section: "Fluids" }, { key: "secondary_fluid", label: "Secondary fluid", kind: "string", default: "Water", section: "Fluids" }, { key: "n_plates", label: "Number of plates", kind: "number", default: 20, required: true, section: "Plate pack", min: 2, step: 1 }, { key: "plate_length_m", label: "Effective plate length", kind: "number", unit: "m", default: 0.25, section: "Plate pack", min: 0.001 }, { key: "plate_width_m", label: "Effective plate width", kind: "number", unit: "m", default: 0.05, section: "Plate pack", min: 0.001 }, { key: "plate_thickness_mm", label: "Plate thickness", kind: "number", unit: "mm", default: 0.6, section: "Plate pack", min: 0.01 }, { key: "channel_spacing_mm", label: "Channel spacing", kind: "number", unit: "mm", default: 1.5, section: "Corrugation", min: 0.01 }, { key: "chevron_angle_deg", label: "Chevron angle", kind: "number", unit: "°", default: 60.0, section: "Corrugation", min: 10.0, max: 80.0 }, { key: "corrugation_pitch_mm", label: "Corrugation pitch", kind: "number", unit: "mm", default: 3.0, section: "Corrugation", min: 0.01 }, { key: "dh_m", label: "Hydraulic diameter override", kind: "number", unit: "m", section: "Direct manufacturer data", min: 0.000001, advanced: true }, { key: "area_m2", label: "Heat-transfer area override", kind: "number", unit: "m²", section: "Direct manufacturer data", min: 0.000001, advanced: true }, { key: "target_subcooling_k", label: "Target subcooling", kind: "number", unit: "K", default: 3.0, section: "Operating model", min: 0.0 }, { key: "emergent_pressure", label: "Solve condensing pressure", kind: "boolean", default: true, section: "Solved-cycle mode", description: "ON: +1 outlet-closure residual pins h_out to target subcooling so P_cond emerges from the secondary balance.", }, { key: "correlation", label: "Two-phase correlation", kind: "string", default: "Longo2004", section: "Heat-transfer model", options: [ { value: "Longo2004", label: "Longo 2004" }, { value: "Shah1979", label: "Shah 1979" }, { value: "Shah2021", label: "Shah 2021" }, ], }, { key: "dp_correlation", label: "Pressure-drop correlation", kind: "string", default: "SimplifiedChannel", section: "Pressure drop", description: "Channel friction on both hot and cold sides (independent of HTC). Default SimplifiedChannel; Martin1996 uses chevron angle.", options: [ { value: "SimplifiedChannel", label: "Simplified channel (default)" }, { value: "Martin1996", label: "Martin 1996 (chevron)" }, ], }, { key: "ua", label: "UA override", kind: "number", unit: "W/K", section: "Calibration", min: 0.0, advanced: true }, { key: "z_ua", label: "Z_UA (UA factor)", kind: "number", default: 1.0, section: "Calibration", min: 0.000001, fixable: true, defaultFixed: true, actuatorFactor: "z_ua", freeMin: 0.2, freeMax: 3.0, }, { key: "z_dp", label: "Z_dP (pressure-drop factor)", kind: "number", default: 1.0, section: "Calibration", min: 0.000001, fixable: true, defaultFixed: true, actuatorFactor: "z_dp", freeMin: 0.2, freeMax: 3.0, }, ], }, { type: "FloodedEvaporator", label: "Flooded Evaporator", category: "Heat Exchangers", description: "Évaporateur noyé (shell & tube). Sortie vapeur saturée par défaut.", help: "Rôle : évaporateur noyé — le frigorigène bout dans la calandre, l’eau/glycol dans les tubes.\n\n" + "Ports : inlet/outlet frigo · secondary_in/out eau.\n\n" + "Mode machine : relier BrineSource → secondary_in → secondary_out → BrineSink.\n" + "Mode rating : renseigner T et débit secondaires sans câbler l’eau.\n\n" + "Calibration : onglet Calibration — Fixed sur SST + Fixed décoché sur Z_UA (défaut Z_UA = 1).\n\n" + "quality_control : laisser OFF sauf modèle de recirculation spécialisé.", docSlug: "flooded-evaporator", ports: ["inlet", "outlet", "secondary_inlet", "secondary_outlet"], color: "#06b6d4", params: [ { key: "manufacturer", label: "Manufacturer", kind: "string", default: "", section: "Identification" }, { key: "model", label: "Model / reference", kind: "string", default: "", section: "Identification" }, { key: "refrigerant", label: "Refrigerant", kind: "string", default: "R134a", section: "Fluids" }, { key: "secondary_fluid", label: "Secondary fluid", kind: "string", default: "Water", section: "Fluids" }, { key: "ua", label: "UA", kind: "number", unit: "W/K", default: 8000.0, required: true, section: "Thermal model", min: 0.0, sweepable: true, sweepGroup: "thermal" }, { key: "secondary_rated_pressure_drop_pa", label: "Tube-side water design ΔP", kind: "number", unit: "Pa", default: 40000, section: "Secondary hydraulics", min: 0.0, description: "Water/brine ΔP through the tubes at rated ṁ (quadratic). 0 = isobaric (legacy).", }, { key: "secondary_rated_m_flow_kg_s", label: "Tube-side water design ṁ", kind: "number", unit: "kg/s", default: 0.55, section: "Secondary hydraulics", min: 0.0, }, { key: "z_ua", label: "Z_UA (UA factor)", kind: "number", default: 1.0, section: "Calibration", min: 0.000001, fixable: true, defaultFixed: true, actuatorFactor: "z_ua", freeMin: 0.2, freeMax: 3.0, description: "Fixed = figé. Décocher Fixed = le solveur ajuste Z_UA.", }, { key: "z_dp", label: "Z_dP (pressure-drop factor)", kind: "number", default: 1.0, section: "Calibration", min: 0.000001, advanced: true, fixable: true, defaultFixed: true, actuatorFactor: "z_dp", freeMin: 0.2, freeMax: 3.0, }, { key: "quality_control", label: "Quality control residual", kind: "boolean", default: false, section: "Solved-cycle mode", description: "FIX: adds q_out − target residual (+1 equation). Over-constrains a closed cycle unless you FREE an actuator (EXV/level). Prefer OFF for compressor suction.", }, { key: "target_quality", label: "Outlet vapour quality target", kind: "number", default: 0.7, section: "Solved-cycle mode", min: 0.0, max: 1.0, description: "Only used when quality_control is ON. Legacy flooded recirculation target — not physical for dry suction.", }, { key: "secondary_inlet_temp_c", label: "Secondary inlet T (rating only)", kind: "number", unit: "°C", default: 12.0, section: "Secondary side (rating)", description: "Rating-mode scalar. For system solves, impose T via BrineSource on secondary_inlet instead.", }, { key: "secondary_mass_flow_kg_s", label: "Secondary mass flow (rating only)", kind: "number", unit: "kg/s", default: 1.5, section: "Secondary side (rating)", min: 0.0, description: "Rating-mode scalar. For system solves, set ṁ on BrineSource.", }, { key: "secondary_cp_j_per_kgk", label: "Secondary cp (rating only)", kind: "number", unit: "J/kg·K", default: 4186.0, section: "Secondary side (rating)", min: 0.0, advanced: true, }, { key: "ua_mode", label: "UA mode", kind: "string", default: "fixed", section: "Thermal model", options: [ { value: "fixed", label: "Fixed UA" }, { value: "cooper", label: "Cooper pool boiling" }, ], description: "cooper = outer-loop UA from Cooper + Palen (Picard).", }, { key: "area_ref_m2", label: "Refrigerant HTA", kind: "number", unit: "m²", default: 20.0, section: "Pool boiling", advanced: true, }, { key: "area_sec_m2", label: "Secondary HTA", kind: "number", unit: "m²", default: 18.0, section: "Pool boiling", advanced: true, }, ], }, { type: "HeatExchanger", label: "Generic Heat Exchanger", category: "Heat Exchangers", description: "Strict four-port two-fluid heat exchanger. Every side is a real solver edge.", ports: ["hot_inlet", "hot_outlet", "cold_inlet", "cold_outlet"], color: "#8b5cf6", params: [ { key: "manufacturer", label: "Manufacturer", kind: "string", default: "", section: "Identification" }, { key: "model", label: "Model / reference", kind: "string", default: "", section: "Identification" }, { key: "ua", label: "UA", kind: "number", unit: "W/K", default: 3000.0, required: true, section: "Thermal model", min: 0.0, sweepable: true, sweepGroup: "thermal" }, { key: "hot_fluid_id", label: "Hot-side fluid", kind: "string", default: "Water", section: "Fluids" }, { key: "cold_fluid_id", label: "Cold-side fluid", kind: "string", default: "Air", section: "Fluids" }, { key: "hot_humidity_ratio", label: "Hot-side humidity ratio", kind: "number", unit: "kg/kg", default: 0.0, section: "Psychrometrics", min: 0.0, advanced: true }, { key: "cold_humidity_ratio", label: "Cold-side humidity ratio", kind: "number", unit: "kg/kg", default: 0.01, section: "Psychrometrics", min: 0.0, advanced: true }, ], }, // ── Expansion ────────────────────────────────────────────────────────── { type: "IsenthalpicExpansionValve", label: "Expansion Valve (EXV)", category: "Expansion", description: "Détendeur isenthalpique (EXV).", help: "Rôle : fait chuter la pression frigo (h constant).\n\n" + "Ports : inlet (liquide HP) · outlet (biphasique BP).\n\n" + "Sans orifice_kv : isenthalpique seul — opening n’a aucun effet physique.\n" + "Avec orifice_kv : ṁ = Kv·opening·√(2·ρ·ΔP). Opening Fixed (défaut) = paramètre ; " + "Free = inconnue (régulation / SaturatedController).\n" + "Orifice Fixed → le compresseur lâche la loi de ṁ (débit métré par l’EXV).\n" + "t_evap_k : guess / mode pression fixée selon le cycle.", docSlug: "isenthalpic-expansion-valve", ports: ["inlet", "outlet"], color: "#10b981", params: [ { key: "t_evap_k", label: "Evaporating temp (K)", kind: "number", unit: "K", default: 275.15 }, { key: "orifice_kv", label: "Orifice Kv", kind: "number", unit: "m²", section: "Orifice", description: "Capacité d’orifice. Obligatoire pour que opening change le débit. Typique ~1e-7…1e-5 m².", }, { key: "opening", label: "Valve opening (0–1)", kind: "number", default: 1.0, min: 0, max: 1, section: "Orifice", fixable: true, defaultFixed: true, description: "Nécessite orifice_kv. Fixed = impose opening ; Free = inconnue solver.", sweepable: true, sweepGroup: "machine", }, ], }, { type: "ExpansionValve", label: "Expansion Valve (basic)", category: "Expansion", description: "Physical EXV/TXV with selectable flow model (orifice, Cd·A, Eames).", ports: ["inlet", "outlet"], color: "#10b981", params: [ { key: "fluid", label: "Fluid", kind: "string", default: "R134a", required: true }, { key: "opening", label: "Opening (0–1)", kind: "number", default: 1.0, sweepable: true, sweepGroup: "machine" }, { key: "flow_model", label: "Flow model", kind: "string", default: "orifice", options: [ { value: "orifice", label: "Isenthalpic orifice" }, { value: "exv", label: "EXV Cd·A" }, { value: "txv", label: "TXV Eames" }, ], }, { key: "beta_m2", label: "β / orifice area", kind: "number", unit: "m²", default: 1e-6, advanced: true }, { key: "cd", label: "Discharge Cd", kind: "number", default: 0.7, advanced: true }, { key: "area_max_m2", label: "A_max", kind: "number", unit: "m²", default: 2e-6, advanced: true }, { key: "bulb_pressure_pa", label: "TXV bulb P", kind: "number", unit: "Pa", default: 400000, advanced: true }, ], }, { type: "CapillaryTube", label: "Capillary Tube", category: "Expansion", description: "Adiabatic capillary (segmented ΔP, isenthalpic).", help: "Diamètre / longueur / segments. Corrélation DP MSH par défaut.", ports: ["inlet", "outlet"], color: "#059669", params: [ { key: "diameter_m", label: "Inner diameter", kind: "number", unit: "m", default: 0.001, required: true }, { key: "length_m", label: "Length", kind: "number", unit: "m", default: 2.0, required: true }, { key: "n_segments", label: "Segments", kind: "number", default: 20, advanced: true }, ], }, { type: "ReversingValve", label: "Reversing Valve (4-way)", category: "Expansion", description: "Four-way reversing valve for heat-pump mode switch.", ports: ["inlet", "outlet"], color: "#0d9488", params: [ { key: "mode", label: "Mode", kind: "string", default: "cooling", options: [ { value: "cooling", label: "Cooling" }, { value: "heating", label: "Heating" }, ], }, { key: "pressure_drop_pa", label: "Pressure drop", kind: "number", unit: "Pa", default: 5000 }, ], }, { type: "BypassValve", label: "Bypass Valve", category: "Expansion", description: "Proportional bypass valve.", ports: ["inlet", "outlet"], color: "#14b8a6", params: [ { key: "opening", label: "Opening (0–1)", kind: "number", default: 0.5, sweepable: true, sweepGroup: "machine" }, ], }, // ── Piping (Modelica/TIL media: green ref · blue water · yellow air) ──── { type: "RefrigerantPipe", label: "Pipe — refrigerant", category: "Piping", description: "Ligne frigorifique (Darcy–Weisbach). Glisser sur une ligne verte pour l’insérer.", help: "Conduit frigorigène. Dépose-le sur une connexion verte pour couper la ligne et l’insérer automatiquement.\n" + "CLI type: RefrigerantPipe (alias Pipe).\n" + "Design ΔP = 0 (défaut) → perte Darcy–Weisbach depuis L/D + ṁ.\n" + "Design ΔP > 0 → impose cette chute constante (ignore L/D pour ΔP).", ports: ["inlet", "outlet"], color: "#2e7d32", params: [ { key: "fluid", label: "Fluid", kind: "string", default: "R134a", section: "Medium" }, { key: "length_m", label: "Length", kind: "number", unit: "m", default: 3.0, required: true, section: "Geometry" }, { key: "diameter_m", label: "Inner diameter", kind: "number", unit: "m", default: 0.012, required: true, section: "Geometry" }, { key: "pressure_drop_pa", label: "Design ΔP (0 = Darcy)", kind: "number", unit: "Pa", default: 0, section: "Hydraulics", description: "0 = ΔP Darcy depuis géométrie + débit. >0 = ΔP imposé constant.", }, { key: "roughness_m", label: "Roughness", kind: "number", unit: "m", default: 0.0000015, section: "Geometry", advanced: true }, { key: "density_kg_m3", label: "Density (design)", kind: "number", unit: "kg/m³", default: 1140, section: "Properties", advanced: true }, { key: "viscosity_pa_s", label: "Viscosity (design)", kind: "number", unit: "Pa·s", default: 0.0002, section: "Properties", advanced: true }, ], }, { type: "WaterPipe", label: "Pipe — water / brine", category: "Piping", description: "Tuyauterie eau/glycol. Glisser sur une ligne bleue pour l’insérer.", help: "Conduit caloporteur. Dépose-le sur une connexion bleue (BrineSource ↔ HX) pour l’insérer dans le circuit.\n" + "Design ΔP = 0 → Darcy L/D ; >0 → ΔP imposé.", ports: ["inlet", "outlet"], color: "#1565c0", params: [ { key: "fluid", label: "Fluid", kind: "string", default: "Water", section: "Medium" }, { key: "length_m", label: "Length", kind: "number", unit: "m", default: 5.0, required: true, section: "Geometry" }, { key: "diameter_m", label: "Inner diameter", kind: "number", unit: "m", default: 0.025, required: true, section: "Geometry" }, { key: "pressure_drop_pa", label: "Design ΔP (0 = Darcy)", kind: "number", unit: "Pa", default: 0, section: "Hydraulics", description: "0 = ΔP Darcy depuis géométrie + débit. >0 = ΔP imposé constant.", }, { key: "roughness_m", label: "Roughness", kind: "number", unit: "m", default: 0.000045, section: "Geometry", advanced: true }, { key: "density_kg_m3", label: "Density", kind: "number", unit: "kg/m³", default: 998, section: "Properties", advanced: true }, { key: "viscosity_pa_s", label: "Viscosity", kind: "number", unit: "Pa·s", default: 0.001, section: "Properties", advanced: true }, ], }, { type: "AirDuct", label: "Duct — air", category: "Piping", description: "Gaine d’air. Glisser sur une ligne jaune pour l’insérer.", help: "Conduit air. Dépose-le sur une connexion jaune (AirSource / Fan / coil air) pour l’insérer.\n" + "Design ΔP = 0 → Darcy L/D ; >0 → ΔP imposé.", ports: ["inlet", "outlet"], color: "#f9a825", params: [ { key: "fluid", label: "Fluid", kind: "string", default: "Air", section: "Medium" }, { key: "length_m", label: "Length", kind: "number", unit: "m", default: 5.0, required: true, section: "Geometry" }, { key: "diameter_m", label: "Hydraulic diameter", kind: "number", unit: "m", default: 0.2, required: true, section: "Geometry" }, { key: "pressure_drop_pa", label: "Design ΔP (0 = Darcy)", kind: "number", unit: "Pa", default: 0, section: "Hydraulics", description: "0 = ΔP Darcy depuis géométrie + débit. >0 = ΔP imposé constant.", }, { key: "roughness_m", label: "Roughness", kind: "number", unit: "m", default: 0.00015, section: "Geometry", advanced: true }, { key: "density_kg_m3", label: "Density", kind: "number", unit: "kg/m³", default: 1.2, section: "Properties", advanced: true }, { key: "viscosity_pa_s", label: "Viscosity", kind: "number", unit: "Pa·s", default: 0.000018, section: "Properties", advanced: true }, ], }, { type: "Pipe", label: "Pipe (generic)", category: "Piping", description: "Legacy generic pipe — prefer medium-specific pipes above.", ports: ["inlet", "outlet"], color: "#64748b", params: [ { key: "fluid", label: "Fluid", kind: "string", default: "R134a" }, { key: "length_m", label: "Length (m)", kind: "number", unit: "m", default: 10.0, required: true }, { key: "diameter_m", label: "Diameter (m)", kind: "number", unit: "m", default: 0.022, required: true }, { key: "roughness_m", label: "Roughness (m)", kind: "number", unit: "m", default: 0.0000015 }, ], }, { type: "Drum", label: "Liquid-Vapor Separator", category: "Piping", description: "Phase separator / accumulator.", ports: ["inlet", "liquid_outlet", "vapor_outlet"], color: "#94a3b8", params: [], }, { type: "FlowSplitter", label: "Flow Splitter", category: "Piping", description: "Splits a flow into two branches.", ports: ["inlet", "outlet_a", "outlet_b"], color: "#94a3b8", params: [ { key: "split_ratio", label: "Split ratio (outlet_a)", kind: "number", default: 0.5 }, ], }, { type: "FlowMerger", label: "Flow Merger", category: "Piping", description: "Merges two flows into one.", ports: ["inlet_a", "inlet_b", "outlet"], color: "#94a3b8", params: [], }, // ── Hydraulics ───────────────────────────────────────────────────────── { type: "Pump", label: "Pump", category: "Hydraulics", description: "Pompe — impose ΔP/courbe (esprit Modelica.Fluid.Machines).", help: "Modelica: une pompe prescrit ṁ ou ΔP via sa courbe — ne pas aussi Fixed ṁ " + "sur un MassFlowSource de la même branche (surcontraint).", ports: ["inlet", "outlet"], color: "#a855f7", params: [ { key: "head_coeffs", label: "Head curve (a0,a1,a2)", kind: "string", unit: "m", default: "30,-10,-50", required: true }, { key: "eff_coeffs", label: "Efficiency curve (a0,a1,a2)", kind: "string", default: "0.5,0.3,-0.5", required: true }, { key: "density_kg_m3", label: "Fluid density (kg/m³)", kind: "number", unit: "kg/m³", default: 1000.0 }, ], }, { type: "Fan", label: "Fan", category: "Hydraulics", description: "Edge-coupled fan for air-side loops; pressure rise and shaft heat are solved on live edges.", ports: ["inlet", "outlet"], color: "#d946ef", params: [ { key: "fluid", label: "Fluid", kind: "string", default: "Air" }, { key: "speed_ratio", label: "Speed ratio", kind: "number", default: 1.0, sweepable: true, sweepGroup: "machine" }, { key: "air_density_kg_m3", label: "Air density", kind: "number", unit: "kg/m³", default: 1.204 }, { key: "design_flow_m3_s", label: "Design flow", kind: "number", unit: "m³/s", default: 0.83, required: true }, { key: "curve_p0", label: "Curve p0", kind: "number", unit: "Pa", default: 250.0 }, { key: "curve_p1", label: "Curve p1", kind: "number", default: 0.0 }, { key: "curve_p2", label: "Curve p2", kind: "number", default: -20.0 }, { key: "eff_e0", label: "Efficiency e0", kind: "number", default: 0.65 }, { key: "eff_e1", label: "Efficiency e1", kind: "number", default: 0.0 }, { key: "eff_e2", label: "Efficiency e2", kind: "number", default: 0.0 }, { key: "drive_chain", label: "Wire-to-air drive chain", kind: "boolean", default: false, description: "Apply η_VFD × η_motor on top of fan efficiency (Bernier–Bourret).", }, ], }, { type: "FreeCoolingExchanger", label: "Free Cooling Exchanger", category: "Heat Exchangers", description: "Water–water free-cooling HX (effectiveness / UA).", ports: ["cold_inlet", "cold_outlet", "hot_inlet", "hot_outlet"], color: "#7c3aed", params: [ { key: "ua", label: "UA", kind: "number", unit: "W/K", default: 10000.0, required: true, sweepable: true, sweepGroup: "thermal" }, { key: "effectiveness", label: "Effectiveness", kind: "number", default: 0.85 }, ], }, // ── Boundaries ───────────────────────────────────────────────────────── { type: "RefrigerantSource", label: "Refrigerant Source", category: "Boundaries", description: "Fixed-state refrigerant boundary.", ports: ["outlet"], color: "#f1f5f9", params: [ { key: "pressure_bar", label: "Pressure (bar)", kind: "number", unit: "bar", default: 10.0, required: true }, { key: "temperature_c", label: "Temperature (°C)", kind: "number", unit: "°C", default: 40.0 }, ], }, { type: "RefrigerantSink", label: "Refrigerant Sink", category: "Boundaries", description: "Fixed-pressure refrigerant boundary.", ports: ["inlet"], color: "#f1f5f9", params: [ { key: "pressure_bar", label: "Pressure (bar)", kind: "number", unit: "bar", default: 4.0, required: true }, ], }, { type: "BrineSource", label: "Brine / Glycol Source", category: "Boundaries", description: "Modelica MassFlowSource_T / Boundary_pT.", help: "Modelica.Fluid.Sources (doc.modelica.org):\n" + "• MassFlowSource_T — Fixed T + Fixed ṁ, Free P (défaut)\n" + "• Boundary_pT — Fixed P + Fixed T, Free ṁ\n" + "Jamais Fixed P et Fixed ṁ ensemble.\n" + "Ancre P = Sink. Double Fixed P seulement avec ΔP hydraulique (pipe).\n" + "T_out imposé : Free ṁ ici + Fixed T_out sur le Sink.", docSlug: "boundaries", ports: ["outlet"], color: "#e0f2fe", params: [ { key: "fluid", label: "Fluid", kind: "string", default: "Water" }, { key: "p_set_bar", label: "Pressure setpoint", kind: "number", unit: "bar", default: 2.0, required: true, fixable: true, defaultFixed: false, description: "Free (défaut MassFlowSource_T). Fixed ⇒ mode Boundary_pT (Free ṁ).", }, { key: "t_set_c", label: "Temperature setpoint", kind: "number", unit: "°C", default: 12.0, required: true, fixable: true, defaultFixed: true, sweepable: true, sweepGroup: "boundaries", }, { key: "m_flow_kg_s", label: "Mass flow", kind: "number", unit: "kg/s", default: 0.5, required: true, fixable: true, defaultFixed: true, description: "Fixed ⇒ Free P auto. Free ⇒ ṁ issu du bilan (avec Fixed T_out au Sink).", sweepable: true, sweepGroup: "boundaries", }, ], }, { type: "BrineSink", label: "Brine / Glycol Sink", category: "Boundaries", description: "Modelica Boundary_p — ancre de pression.", help: "Ancre P du circuit (Boundary_p). Fixed P par défaut.\n" + "Fixed T_out ⇒ Free ṁ sur la Source (pas de Fixed ṁ en même temps).", docSlug: "boundaries", ports: ["inlet"], color: "#e0f2fe", params: [ { key: "fluid", label: "Fluid", kind: "string", default: "Water" }, { key: "p_back_bar", label: "Back pressure", kind: "number", unit: "bar", default: 2.0, required: true, fixable: true, defaultFixed: true, }, { key: "t_set_c", label: "Outlet temperature", kind: "number", unit: "°C", default: 7.0, fixable: true, defaultFixed: false, description: "Fixed ON ⇒ impose h_out. Libère ṁ sur la Source.", sweepable: true, sweepGroup: "boundaries", }, ], }, { type: "AirSource", label: "Air Source", category: "Boundaries", description: "Modelica MassFlowSource_T (air).", help: "MassFlowSource_T : Fixed T + Fixed ṁ, Free P (défaut).\n" + "Boundary_pT : Fixed P + Free ṁ. Jamais Fixed P+ṁ ensemble.", ports: ["outlet"], color: "#f0fdf4", params: [ { key: "p_set_bar", label: "Pressure setpoint", kind: "number", unit: "bar", default: 1.01325, required: true, fixable: true, defaultFixed: false, }, { key: "t_dry_c", label: "Dry-bulb temperature", kind: "number", unit: "°C", default: 20.0, required: true, fixable: true, defaultFixed: true, sweepable: true, sweepGroup: "boundaries", }, { key: "rh", label: "Relative humidity", kind: "number", unit: "%", default: 50.0 }, { key: "m_flow_kg_s", label: "Mass flow", kind: "number", unit: "kg/s", default: 1.0, required: true, fixable: true, defaultFixed: true, sweepable: true, sweepGroup: "boundaries", }, ], }, { type: "AirSink", label: "Air Sink", category: "Boundaries", description: "Modelica Boundary_p (air) — ancre P.", ports: ["inlet"], color: "#f0fdf4", params: [ { key: "p_back_bar", label: "Back pressure", kind: "number", unit: "bar", default: 1.01325, required: true, fixable: true, defaultFixed: true, }, { key: "t_back_c", label: "Return temperature", kind: "number", unit: "°C", default: 25.0, fixable: true, defaultFixed: false, }, ], }, // ── Generic ──────────────────────────────────────────────────────────── { type: "Placeholder", label: "Placeholder", category: "Generic", description: "Generic placeholder with configurable equation count.", ports: ["inlet", "outlet"], color: "#e2e8f0", params: [ { key: "n_equations", label: "Number of equations", kind: "number", default: 2, required: true }, ], }, ]; export const COMPONENT_BY_TYPE: Record = Object.fromEntries( COMPONENTS.map((c) => [c.type, c]), ); /** Build the default parameter values for a component type. */ export function defaultParams(type: string): Record { const meta = COMPONENT_BY_TYPE[type]; if (!meta) return {}; const out: Record = {}; for (const p of meta.params) { if (p.default !== undefined) out[p.key] = p.default; // Seed Fixed flags so Free/Fixed state is explicit after drop if (p.fixable) { out[fixedFlagKey(p.key)] = p.defaultFixed !== false; } } return out; } /** * Value shown / used for a param: stored value, else meta.default. * Ensures Z_UA=1 etc. always appear even if an old node lacks the key. */ export function effectiveParamValue( params: Record, meta: ParamMeta, ): number | string | boolean | undefined { const v = params[meta.key]; if (v !== undefined && v !== "") return v; return meta.default; } /** Merge missing defaults into a params object (non-destructive for existing keys). */ export function mergeDefaultParams( type: string, params: Record, ): Record { const defaults = defaultParams(type); return { ...defaults, ...params }; }