Resources Numerical accuracy Calculation policy

Pure-fluid properties from CoolProp

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.

process/coolprop.js CadCraft 0.8.2 64-bit 5 calculations Reviewed

Tests None found by the review

Reference-state offsets

64-bit

The shifts that put CoolProp's enthalpy and entropy on the engine's reference state.

ΔH = h_f − H(298.15 K, 1e-4 mol/m³); ΔS = −(S(298.15 K, 1e-4 mol/m³) − R ln(1e5 ÷ (1e-4 R T₀)))

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.

P_c (bar) = P_c (Pa) ÷ 1e5; M (g/mol) = M × 1000; ΔH_vap(T_b) = H(1 atm, vapor) − H(1 atm, liquid)

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.

Every policy that takes this approach