Resources Numerical accuracy Calculation policy

Heat-exchanger network design

Proposes the matches, heaters and coolers of a heat-exchanger network from stream data by the pinch design method, with the duty and end temperatures of each.

How the precision is kept

Duties are CP × ΔT and temperatures move by Q ÷ CP, all in 64-bit. Each match takes the smaller remaining duty and the remainders are subtracted in turn, so a stream's leftover carries the rounding of those subtractions; leftovers of 1e-6 or less are ignored. The tolerances are absolute, in the units of the input.

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/hen.js CadCraft 0.8.2 64-bit 5 calculations Reviewed

Tests tools/process-test/test-draw4.mjs

A stream's duty on one side of the pinch

64-bit

How much heat a stream has to give or take above the pinch, and below it.

above, hot: Q = CP × (Ts − max(Tt, T_pinch)); above, cold: Q = CP × (Tt − max(Ts, T_pinch))

One multiplication of CP by a temperature difference in 64-bit. A stream counts on a side only when it passes the pinch by more than 1e-9, an absolute tolerance in the temperature unit.

  • process/hen.js
  • lines 22 to 29
  • CadCraft 0.8.2
const part = (s, above) => {
  if (above) {
    if (s.hot) { if (s.Ts <= Th + EPS) return null; const near = Math.max(s.Tt, Th); return { near, far: s.Ts, duty: s.CP * (s.Ts - near) }; }
    if (s.Tt <= Tc + EPS) return null; const near = Math.max(s.Ts, Tc); return { near, far: s.Tt, duty: s.CP * (s.Tt - near) };
  }
  if (s.hot) { if (s.Tt >= Th - EPS) return null; const near = Math.min(s.Ts, Th); return { near, far: s.Tt, duty: s.CP * (near - s.Tt) }; }
  if (s.Ts >= Tc - EPS) return null; const near = Math.min(s.Tt, Tc); return { near, far: s.Ts, duty: s.CP * (near - s.Ts) };
};

Choosing the next match

64-bit

Which hot and cold stream are matched next: streams at the pinch first, then the largest duty.

score = 1e9 × (streams at the pinch) + min(remaining hot, remaining cold)

The preference for pinch streams is made by adding 1e9 to the duty. A duty of 1e9 or more would outrank it, and after 1e9 is added, duties that differ by less than about 1e-7 can score the same.

  • process/hen.js
  • lines 38 to 42
  • CadCraft 0.8.2
const cpOk = above ? h.s.CP <= c.s.CP + EPS : h.s.CP >= c.s.CP - EPS;
if (h.atPinch && c.atPinch && !cpOk) continue;                      // the CP rule binds a match of two streams that both touch the pinch
const q = Math.min(h.rem, c.rem), pinchTouch = (h.atPinch ? 1 : 0) + (c.atPinch ? 1 : 0);
const score = (pinchTouch * 1e9) + q;
if (!best || score > best.score) best = { h, c, q, score, cpOk };

Duty and end temperatures of a match

64-bit

The heat a match exchanges, the temperatures of both streams at its two ends and whether it keeps the minimum temperature difference.

Q = min(remaining hot, remaining cold); ΔT_hot = Q ÷ CP_hot; ΔT_cold = Q ÷ CP_cold; d₁ = |T_hot,₀ − T_cold,₀|; d₂ = |T_hot,₁ − T_cold,₁|

64-bit; each stream's temperature and remaining duty are updated by adding and subtracting, match after match. The match is flagged when the smaller end difference is below ΔTmin − 1e-6. The warning text rounds the difference to two decimals; the stored values are not rounded.

  • process/hen.js
  • lines 45 to 52
  • CadCraft 0.8.2
const { h, c, q } = best, dh = q / h.s.CP, dc = q / c.s.CP;
const hotT = [h.T, h.T + sign * dh], coldT = [c.T, c.T + sign * dc];
// counter-current ends: the pinch-side end of the hot stream faces the pinch-side end of the cold one
const d1 = Math.abs(hotT[0] - coldT[0]), d2 = Math.abs(hotT[1] - coldT[1]);
const ok = Math.min(d1, d2) >= dTmin - 1e-6;
if (!ok) problems.push(prob('hen.dtmin', 'warning', `Match ${h.s.id}/${c.s.id} (${region} the pinch) leaves ${Math.min(d1, d2).toFixed(2)} between the streams at one end, below the minimum ${dTmin}.`));
matches.push({ region, hot: h.s.id, cold: c.s.id, Q: q, hotT, coldT, ok, dT: [d1, d2], cpRule: best.cpOk });
h.rem -= q; c.rem -= q; h.T += sign * dh; c.T += sign * dc; h.atPinch = false; c.atPinch = false;

Heaters, coolers and totals

64-bit

The heating and cooling the matches leave over, and the number of units against the minimum.

Q_H = Σ heater duties; Q_C = Σ cooler duties; units = matches + heaters + coolers

A leftover above 1e-6 becomes a heater or cooler with the full 64-bit remainder. The warning compares Q_H and Q_C with the targets plus 1e-6; its text rounds them to two decimals.

  • process/hen.js
  • lines 54 to 61
  • CadCraft 0.8.2
  for (const c of cs) if (c.rem > 1e-6 && above) heaters.push({ stream: c.s.id, Q: c.rem, from: c.T, to: c.far });
  for (const h of hs) if (h.rem > 1e-6 && !above) coolers.push({ stream: h.s.id, Q: h.rem, from: h.T, to: h.far });
  // what no match and no allowed utility can take: a hot stream left above the pinch, a cold one left below (that would cross the pinch with a utility)
  for (const h of hs) if (h.rem > 1e-6 && above) { problems.push(prob('hen.hotleft', 'warning', `Stream ${h.s.id} has ${h.rem.toFixed(2)} kW left above the pinch that no allowed match took (a cooler there would cost energy); split the stream or accept the penalty.`)); coolers.push({ stream: h.s.id, Q: h.rem, from: h.T, to: h.far, crosses: true }); }
  for (const c of cs) if (c.rem > 1e-6 && !above) { problems.push(prob('hen.coldleft', 'warning', `Stream ${c.s.id} has ${c.rem.toFixed(2)} kW left below the pinch that no allowed match took (a heater there would cost energy); split the stream or accept the penalty.`)); heaters.push({ stream: c.s.id, Q: c.rem, from: c.T, to: c.far, crosses: true }); }
}
const sumQ = (l) => l.reduce((a, x) => a + x.Q, 0), QH = sumQ(heaters), QC = sumQ(coolers), mu = minUnits(S, dTmin).total, units = matches.length + heaters.length + coolers.length;
if (QH > pt.QHmin + 1e-6 || QC > pt.QCmin + 1e-6) problems.push(prob('hen.target', 'warning', `The network uses ${QH.toFixed(2)} of heating and ${QC.toFixed(2)} of cooling; the targets are ${pt.QHmin.toFixed(2)} and ${pt.QCmin.toFixed(2)}.`));

Numbers written on the grid drawing

64-bit

The CP, temperatures and duties shown as text on the network grid.

label = value rounded to 1 decimal

CP, temperatures and duties are written into the drawing's text entities rounded to one decimal with toFixed(1). The design object keeps the unrounded values.

  • process/hen.js
  • line 73
  • CadCraft 0.8.2
const f = (v) => +Number(v).toFixed(1);

In detail

Method
Pinch design method with the CP rule at the pinch and the tick-off rule; the file cites Linnhoff and Hindmarsh 1983. Pinch temperatures and targets come from problemTable in pinch.js.
Number types
64-bit numbers.
Tolerances
1e-9 for a stream reaching the pinch, a zero duty and the CP rule; 1e-7 for a stream starting at the pinch; 1e-6 for the minimum temperature difference, leftover duties and the comparison with the targets. All absolute.
Limits
A first design, as the file says: no stream splits and no optimization of the temperatures between matches. The matching loop stops after 200 matches on each side of the pinch.

Approach: 64-bit, direct

Every value is a 64-bit number from input to result. Formulas are closed form or a direct solve, and every comparison uses a stated tolerance.

Every policy that takes this approach