Gives enthalpy, entropy, heat capacity, density and saturation values of a pure fluid, and builds a component from them.
How the precision is kept
The property values come from the CoolProp library (version 6.6.0 by the path in the file), loaded as WebAssembly the first time a pure fluid is asked for. This file only shifts them to the engine's reference state and takes differences, in 64-bit. The library's own correlations set the accuracy; its code was not part of this review.
Open item. Scale the temperature before the heat-capacity fit, as the comment in fitCp says, or solve it by a method that does not form sums of T⁴.
Calculations under this policy
5 calculations in 1 source file. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.
The dilute-gas state at 1e-4 mol/m³ stands in for the ideal gas. The offsets are computed once per fluid and kept.
process/coolprop.js
lines 44 to 49
CadCraft 0.8.2
function reference(cp, id) {
if (refs.has(id)) return refs.get(id);
const f = fluidOf(id), H0 = cp.PropsSI('Hmolar', 'T', T0, 'Dmolar', 1e-4, f), S0 = cp.PropsSI('Smolar', 'T', T0, 'Dmolar', 1e-4, f);
const o = { dH: (COMPONENTS[id]?.hf ?? 0) - H0, dS: -(S0 - R * Math.log(1e5 / (1e-4 * R * T0))), hasHf: COMPONENTS[id]?.hf != null }; // 1e-4 mol/m3 at T0 is 1e-4 R T0 Pa
refs.set(id, o); return o;
}
Properties at a temperature and pressure
64-bit
Enthalpy, entropy, heat capacity, molar density, molar mass and phase of a pure fluid.
H = H_CoolProp + ΔH; S = S_CoolProp + ΔS; M (g/mol) = M (kg/mol) × 1000
One addition per property on the library's value. A state the library cannot evaluate raises an error instead of returning a number.
process/coolprop.js
lines 51 to 58
CadCraft 0.8.2
export function props(cp, id, T, P) {
const f = fluidOf(id), o = reference(cp, id), q = (k) => cp.PropsSI(k, 'T', T, 'P', P, f);
const H = q('Hmolar'), S = q('Smolar');
if (!Number.isFinite(H) || !Number.isFinite(S)) throw new Error('CoolProp has no state for ' + f + ' at ' + T + ' K and ' + P + ' Pa.');
const out = { H: H + o.dH, S: S + o.dS, Cp: q('Cpmolar'), rho: q('Dmolar'), MW: g(cp, 'molar_mass', f) * 1000, phase: phaseOf(cp, f, T, P) };
for (const [k, key] of [['mu', 'viscosity'], ['k', 'conductivity']]) { const v = q(key); if (Number.isFinite(v)) out[k] = v; }
return out;
}
Saturation values
64-bit
Vapor pressure, saturated liquid and vapor densities and heat of vaporization at a temperature.
ΔH_vap = H(T, vapor) − H(T, liquid)
The heat of vaporization is a difference of two library enthalpies, which approaches zero near the critical point.
process/coolprop.js
lines 66 to 70
CadCraft 0.8.2
export function saturation(cp, id, T) {
const f = fluidOf(id), q = (k, Q) => cp.PropsSI(k, 'T', T, 'Q', Q, f);
const P = q('P', 0); if (!Number.isFinite(P)) throw new Error('CoolProp: ' + f + ' has no saturation state at ' + T + ' K (above the critical point or below the triple point).');
return { T, P, rhoL: q('Dmolar', 0), rhoV: q('Dmolar', 1), hvap: q('Hmolar', 1) - q('Hmolar', 0) };
}
Ideal-gas heat-capacity fit
64-bit
A quadratic C_p ÷ R = A + B T + C T² fitted to the dilute-gas enthalpy slope over a temperature range.
C_p(T) ≈ H(T + 0.5) − H(T − 0.5) at 13 temperatures; normal equations M s = v solved by 3×3 determinants
Each heat capacity is a difference of two enthalpies 1 K apart. The basis is 1, T, T² with T in kelvin; the comment says the temperature is scaled, but the code does not scale it, so the matrix holds sums of T⁴ next to a count of 13 and the determinants cancel heavily. The worst relative misfit is returned.
process/coolprop.js
lines 75 to 87
CadCraft 0.8.2
/** Ideal-gas Cp/R = A + B T + C T^2 fitted (least squares) to the dilute-gas enthalpy slope of CoolProp over `range` K. */
export function fitCp(cp, id, range = [250, 700]) {
const f = fluidOf(id), pts = [];
for (let i = 0; i <= 12; i++) { const T = range[0] + (range[1] - range[0]) * i / 12, h = cp.PropsSI('Hmolar', 'T', T + 0.5, 'Dmolar', 1e-4, f) - cp.PropsSI('Hmolar', 'T', T - 0.5, 'Dmolar', 1e-4, f); if (Number.isFinite(h)) pts.push([T, h / R]); }
if (pts.length < 4) throw new Error('CoolProp: no ideal-gas heat capacity for ' + f);
const M = [[0, 0, 0], [0, 0, 0], [0, 0, 0]], v = [0, 0, 0];
for (const [T, c] of pts) { const b = [1, T, T * T]; for (let i = 0; i < 3; i++) { v[i] += b[i] * c; for (let j = 0; j < 3; j++) M[i][j] += b[i] * b[j]; } }
// 3x3 solve by Cramer's rule (well-conditioned after scaling T)
const det = (m) => m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0]) + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]);
const d = det(M), s = [0, 1, 2].map((k) => det(M.map((row, i) => row.map((x, j) => (j === k ? v[i] : x)))) / d);
let worst = 0; for (const [T, c] of pts) worst = Math.max(worst, Math.abs(s[0] + s[1] * T + s[2] * T * T - c) / c);
return { A: s[0], B: s[1], C: s[2], D: 0, worst };
}
Component from a fluid
64-bit
A component for the property package from CoolProp's critical constants, acentric factor, boiling point and the heat-capacity fit.
Constants are passed on as the library returns them. The formation enthalpy comes from the component table when the fluid is there and is 0, with a note, when it is not.
process/coolprop.js
lines 89 to 100
CadCraft 0.8.2
/** A component for defineComponent() from CoolProp's own constants. `id` is the CH.1 id (a new id may be given with opts.fluid naming the CoolProp fluid). */
export function componentFromFluid(cp, id, { fluid, newId } = {}) {
const f = fluid || fluidOf(id);
const Tc = g(cp, 'Tcrit', f), Pc = g(cp, 'pcrit', f), omega = g(cp, 'acentric', f), MW = g(cp, 'molar_mass', f) * 1000;
if (![Tc, Pc, omega, MW].every(Number.isFinite)) throw new Error('CoolProp does not know "' + f + '".');
const Tb = cp.PropsSI('T', 'P', 101325, 'Q', 0, f), dH = Number.isFinite(Tb) ? cp.PropsSI('Hmolar', 'P', 101325, 'Q', 1, f) - cp.PropsSI('Hmolar', 'P', 101325, 'Q', 0, f) : null;
const known = COMPONENTS[id], fit = fitFluid(cp, f);
const notes = ['Constants and the heat-capacity fit are CoolProp ' + (cp.get_global_param_string ? String(cp.get_global_param_string('version') || '') : '') + '; the heat-capacity fit is a quadratic over 250–700 K (worst error ' + (fit.worst * 100).toFixed(2) + ' %).'];
if (!known || known.hf == null) notes.push('Formation enthalpy is not in CoolProp: hf = 0, so reaction energy balances with this component are wrong until a value is typed.');
return { id: newId || id, name: f, formula: known?.formula || '', MW, Tc, Pc: Pc / 1e5, omega, Tb: Number.isFinite(Tb) ? Tb : null, dHvapTb: dH,
antoine: null, cp: { A: fit.A, B: fit.B, C: fit.C, D: 0 }, hf: known?.hf ?? 0, gf: known?.gf ?? null, src: 'CoolProp (MIT); see the add-on notes', notes };
}
In detail
Method
PropsSI calls; a reference offset from the dilute gas at 298.15 K; a quadratic least-squares fit of the ideal-gas heat capacity by normal equations and Cramer's rule.
Number types
The values PropsSI returns, as JavaScript numbers; offsets and fits in 64-bit.
Limits
CoolProp has no formation enthalpies, so a fluid that is not in the component table gets 0 and a note. The heat-capacity fit is a quadratic over 250 to 700 K and reports its own worst error.
Approach: 64-bit, stated limit
The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.