feat: page interactive, démos Play/Step et simulateur Carnot
Ajoute le pipeline PageSpec (validation, rendu, publication /p/{slug}),
les démos TipTap /demo, et le simulateur Carnot (modes frigo/PAC/moteur,
énergie kJ vs puissance W, unités K/°C/°F) avec correctifs d’équations KaTeX.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
254
memento-note/lib/simulators/carnot-cycle.ts
Normal file
254
memento-note/lib/simulators/carnot-cycle.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import type { SimulatorPlugin } from './types'
|
||||
|
||||
/**
|
||||
* Ideal Carnot machine between two reservoirs (2nd law).
|
||||
*
|
||||
* Physics (absolute temperatures only):
|
||||
* - Refrigerator: COP_R = Tc/(Th−Tc), W_min = Qc/COP_R, Qh = Qc+W
|
||||
* - Heat pump: COP_HP = Th/(Th−Tc) = COP_R+1, W_min = Qh/COP_HP
|
||||
* - Engine: η = 1−Tc/Th, W_out = η·Qh, Qc = Qh−W
|
||||
*
|
||||
* `q_cold` is the primary load magnitude (kJ or W — same number; unit is UI-only).
|
||||
* For fridge it is Qc; the view remaps for PAC / engine. Temperatures always Kelvin.
|
||||
*
|
||||
* Refs: COP_R Carnot = Tc/(Th−Tc); 1st law Qh=Qc+W; energy↔power interchangeable
|
||||
* if all rates use the same time basis (see standard thermo textbooks / Carnot fridge calculators).
|
||||
*/
|
||||
export const carnotCycleSimulator: SimulatorPlugin = {
|
||||
family: 'sim',
|
||||
id: 'carnot-cycle',
|
||||
title: {
|
||||
fr: 'Machine de Carnot (frigo / PAC / moteur)',
|
||||
en: 'Carnot machine (fridge / heat pump / engine)',
|
||||
},
|
||||
summary: {
|
||||
fr: 'Limites de Carnot entre deux sources : COP frigo Tc/(Th−Tc), COP pompe à chaleur Th/(Th−Tc), rendement moteur η=1−Tc/Th, travail ou puissance minimal(e). 1er et 2e principes.',
|
||||
en: 'Carnot limits between two reservoirs: fridge COP Tc/(Th−Tc), heat-pump COP Th/(Th−Tc), engine η=1−Tc/Th, minimum work or power. 1st and 2nd laws.',
|
||||
},
|
||||
keywords: [
|
||||
'carnot',
|
||||
'thermodynamique',
|
||||
'thermodynamics',
|
||||
'cop',
|
||||
'réfrigérateur',
|
||||
'frigo',
|
||||
'refrigerator',
|
||||
'pompe à chaleur',
|
||||
'heat pump',
|
||||
'moteur',
|
||||
'engine',
|
||||
'rendement',
|
||||
'efficiency',
|
||||
'watt',
|
||||
'puissance',
|
||||
'power',
|
||||
'deuxième principe',
|
||||
'second law',
|
||||
],
|
||||
params: [
|
||||
{
|
||||
id: 't_cold',
|
||||
symbol: 'T_c',
|
||||
label: { fr: 'Source froide', en: 'Cold reservoir' },
|
||||
min: 200,
|
||||
max: 320,
|
||||
step: 1,
|
||||
defaultValue: 260,
|
||||
unit: 'K',
|
||||
intent: 'cache',
|
||||
},
|
||||
{
|
||||
id: 't_hot',
|
||||
symbol: 'T_h',
|
||||
label: { fr: 'Source chaude', en: 'Hot reservoir' },
|
||||
min: 273,
|
||||
max: 400,
|
||||
step: 1,
|
||||
defaultValue: 300,
|
||||
unit: 'K',
|
||||
intent: 'warning',
|
||||
},
|
||||
{
|
||||
id: 'q_cold',
|
||||
symbol: 'Q_c',
|
||||
label: { fr: 'Charge (Qc frigo)', en: 'Load (fridge Qc)' },
|
||||
min: 10,
|
||||
max: 500,
|
||||
step: 5,
|
||||
defaultValue: 100,
|
||||
unit: 'kJ',
|
||||
intent: 'flow',
|
||||
},
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
id: 'cop_fridge',
|
||||
symbol: '\\mathrm{COP}_{R}',
|
||||
label: { fr: 'COP réfrigérateur', en: 'Fridge COP' },
|
||||
intent: 'output',
|
||||
digits: 2,
|
||||
},
|
||||
{
|
||||
id: 'cop_hp',
|
||||
symbol: '\\mathrm{COP}_{HP}',
|
||||
label: { fr: 'COP pompe à chaleur', en: 'Heat-pump COP' },
|
||||
intent: 'output',
|
||||
digits: 2,
|
||||
},
|
||||
{
|
||||
id: 'eta',
|
||||
symbol: '\\eta',
|
||||
label: { fr: 'Rendement moteur', en: 'Engine efficiency' },
|
||||
unit: '%',
|
||||
intent: 'highlight',
|
||||
digits: 1,
|
||||
},
|
||||
{
|
||||
id: 'w_min',
|
||||
symbol: 'W',
|
||||
label: { fr: 'Travail (énergie)', en: 'Work (energy)' },
|
||||
unit: 'kJ',
|
||||
intent: 'compute',
|
||||
digits: 1,
|
||||
},
|
||||
{
|
||||
id: 'q_hot',
|
||||
symbol: 'Q_h',
|
||||
label: { fr: 'Chaleur côté chaud', en: 'Hot-side heat' },
|
||||
unit: 'kJ',
|
||||
intent: 'flow',
|
||||
digits: 1,
|
||||
},
|
||||
],
|
||||
compute(env) {
|
||||
const tc = env.t_cold
|
||||
const th = env.t_hot
|
||||
const qc = env.q_cold
|
||||
if (!(th > tc) || !(qc > 0)) {
|
||||
return {
|
||||
cop_fridge: NaN,
|
||||
cop_hp: NaN,
|
||||
eta: NaN,
|
||||
w_min: NaN,
|
||||
q_hot: NaN,
|
||||
}
|
||||
}
|
||||
const copFridge = tc / (th - tc)
|
||||
const copHp = th / (th - tc)
|
||||
const eta = 1 - tc / th
|
||||
const wMin = qc / copFridge
|
||||
return {
|
||||
cop_fridge: copFridge,
|
||||
cop_hp: copHp,
|
||||
eta: eta * 100,
|
||||
w_min: wMin,
|
||||
q_hot: qc + wMin,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/** Operating mode for the interactive view (UI-only; physics shared). */
|
||||
export type CarnotMode = 'fridge' | 'heat_pump' | 'engine'
|
||||
|
||||
/** Energy (kJ) vs power (W) — same ratios; only unit labels change. */
|
||||
export type CarnotQuantity = 'energy' | 'power'
|
||||
|
||||
export type CarnotPhysics = {
|
||||
ok: boolean
|
||||
tc: number
|
||||
th: number
|
||||
copR: number
|
||||
copHP: number
|
||||
eta: number
|
||||
/** Heat exchanged with cold reservoir (magnitude > 0). */
|
||||
qc: number
|
||||
/** Heat exchanged with hot reservoir (magnitude > 0). */
|
||||
qh: number
|
||||
/** Work magnitude > 0 (input for fridge/PAC, output for engine). */
|
||||
w: number
|
||||
/** Reversible check: Qc/Tc ≈ Qh/Th */
|
||||
entropyOk: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve magnitudes for the selected mode.
|
||||
* `load` is the primary useful quantity:
|
||||
* - fridge: Qc extracted from cold
|
||||
* - heat_pump: Qh delivered to hot
|
||||
* - engine: Qh absorbed from hot
|
||||
*/
|
||||
export function resolveCarnotPhysics(
|
||||
tc: number,
|
||||
th: number,
|
||||
load: number,
|
||||
mode: CarnotMode
|
||||
): CarnotPhysics {
|
||||
if (!(th > tc) || !(load > 0) || !Number.isFinite(tc) || !Number.isFinite(th)) {
|
||||
return {
|
||||
ok: false,
|
||||
tc,
|
||||
th,
|
||||
copR: NaN,
|
||||
copHP: NaN,
|
||||
eta: NaN,
|
||||
qc: NaN,
|
||||
qh: NaN,
|
||||
w: NaN,
|
||||
entropyOk: false,
|
||||
}
|
||||
}
|
||||
const copR = tc / (th - tc)
|
||||
const copHP = th / (th - tc)
|
||||
const eta = 1 - tc / th
|
||||
|
||||
let qc: number
|
||||
let qh: number
|
||||
let w: number
|
||||
if (mode === 'fridge') {
|
||||
qc = load
|
||||
w = qc / copR
|
||||
qh = qc + w
|
||||
} else if (mode === 'heat_pump') {
|
||||
qh = load
|
||||
w = qh / copHP
|
||||
qc = qh - w
|
||||
} else {
|
||||
qh = load
|
||||
w = eta * qh
|
||||
qc = qh - w
|
||||
}
|
||||
|
||||
const ratioC = qc / tc
|
||||
const ratioH = qh / th
|
||||
const entropyOk =
|
||||
Number.isFinite(ratioC) &&
|
||||
Number.isFinite(ratioH) &&
|
||||
Math.abs(ratioC - ratioH) / Math.max(ratioC, ratioH, 1e-9) < 1e-6
|
||||
|
||||
return { ok: true, tc, th, copR, copHP, eta, qc, qh, w, entropyOk }
|
||||
}
|
||||
|
||||
/** Convert fridge-stored Qc load ↔ display load for other modes (fixture-compatible). */
|
||||
export function fridgeLoadFromModeLoad(
|
||||
tc: number,
|
||||
th: number,
|
||||
modeLoad: number,
|
||||
mode: CarnotMode
|
||||
): number {
|
||||
if (!(th > tc) || !(modeLoad > 0)) return modeLoad
|
||||
if (mode === 'fridge') return modeLoad
|
||||
if (mode === 'heat_pump') return modeLoad * (tc / th) // Qc = Qh · Tc/Th
|
||||
return modeLoad * (tc / th) // engine: Qc = Qh · (1−η) = Qh · Tc/Th
|
||||
}
|
||||
|
||||
export function modeLoadFromFridgeLoad(
|
||||
tc: number,
|
||||
th: number,
|
||||
fridgeQc: number,
|
||||
mode: CarnotMode
|
||||
): number {
|
||||
if (!(th > tc) || !(fridgeQc > 0)) return fridgeQc
|
||||
if (mode === 'fridge') return fridgeQc
|
||||
if (mode === 'heat_pump') return fridgeQc * (th / tc) // Qh = Qc · Th/Tc
|
||||
return fridgeQc * (th / tc) // engine Qh = Qc / (1−η) = Qc · Th/Tc
|
||||
}
|
||||
Reference in New Issue
Block a user