Modeling Heat Pump Balance Points and Low-Ambient Inverter Deratings in TypeScript
A deterministic approach to modeling cold-climate heat pump balance points, variable-speed inverter COP deratings, and auxiliary electric heat strip sizing in pure TypeScript.

When sizing residential or commercial heat pumps, traditional HVAC rules of thumb (such as 500 sq ft per ton) fail dramatically in cold climates.
As outdoor ambient temperature drops from 47°F (8.3°C) down to -5°F (-20.6°C), two opposing thermodynamic phenomena occur simultaneously:
Building heat loss increases linearly as the indoor-to-outdoor temperature difference (\(\Delta T\)) widens.
Heat pump heating capacity decreases non-linearly due to declining refrigerant mass flow rate, lower evaporator suction density, and periodic defrost cycles.
The exact outdoor temperature where these two curves intersect is the Thermal Balance Point. Below this temperature, the heat pump can no longer satisfy the building's thermal envelope on its own and requires auxiliary supplemental heat (electric resistance strips or fossil fuel dual-fuel staging).
In this article, we will implement a deterministic, client-side calculation engine in pure TypeScript to model cold-climate inverter curves, compute building load slopes, locate the balance point, and size backup heating elements per ACCA Manual S and NEEP ccASHP specifications.
The Governing Thermodynamics
1. Building Envelope Heat Loss Slope
Building envelope transmission and infiltration follow Fourier's Law of conduction and sensible ventilation load equations. For a given heating design heat loss \(Q_{\text{design}}\) calculated at the local 99% winter design temperature \(T_{\text{design}}\), the instantaneous building load at any arbitrary ambient temperature \(T_{\text{ambient}}\) is:
$$Q_{\text{loss}}(T_{\text{ambient}}) = Q_{\text{design}} \times \frac{T_{\text{indoor}} - T_{\text{ambient}}}{T_{\text{indoor}} - T_{\text{design}}}$$
Where:
\(T_{\text{indoor}}\) is the indoor thermostat heating setpoint (typically 70°F / 21.1°C).
\(Q_{\text{loss}} = 0\) whenever \(T_{\text{ambient}} \ge T_{\text{indoor}}\).
2. Compressor Low-Ambient Capacity Derating
Unlike older single-stage systems that lose over 65% of their rated heating capacity at 5°F, modern variable-speed inverter systems (such as NEEP cold-climate qualified units) maintain high mass flow via vapor injection and oversized compressors.
We model compressor heating output \(Q_{\text{hp}}(T_{\text{ambient}})\) across temperature bands using piecewise linear interpolation calibrated against AHRI 210/240-2023 rating points (47°F, 17°F, 5°F, and -5°F):
Standard Single-Stage: 100% capacity @ 47°F, 55% @ 17°F, 35% @ 5°F, 20% @ -5°F.
Cold-Climate Inverter (ccASHP): 100% capacity @ 47°F, 88% @ 17°F, 76% @ 5°F, 65% @ -5°F.
TypeScript Implementation
Here is the complete calculation module implemented with strict typing and zero external runtime dependencies:
/**
* HVACLogic Heat Pump Sizing & Thermal Balance Point Engine
* Calibrated against ACCA Manual S and NEEP ccASHP specifications.
*/
export type HeatPumpCompressorType =
| "inverter_cold_climate"
| "inverter_standard"
| "single_stage_standard";
export interface HeatPumpInput {
nominalTonnage: number; // 1.5 to 5.0 Tons
compressorType: HeatPumpCompressorType;
outdoorDesignTempF: number; // e.g. -5°F to 35°F
designHeatingLossBtu: number; // e.g. 40,000 BTU/h
designCoolingLoadBtu?: number; // e.g. 30,000 BTU/h
indoorSetpointF?: number; // Default 70°F
}
export interface CurvePoint {
outdoorTempF: number;
buildingHeatLossBtu: number;
heatPumpCapacityBtu: number;
auxiliaryDeficitBtu: number;
}
export interface HeatPumpOutput {
nominalTonnage: number;
nominalCoolingBtu: number;
nominalHeatingBtu47F: number;
heatingCapacityAtDesignBtu: number;
buildingHeatLossAtDesignBtu: number;
thermalBalancePointF: number;
auxiliaryHeatDeficitBtu: number;
recommendedAuxHeatStripKw: number;
isColdClimateQualified: boolean;
manualSCoolingRatio: number;
curvePoints: CurvePoint[];
}
export const COMPRESSOR_PERFORMANCE_FACTORS: Record<
HeatPumpCompressorType,
{ label: string; ratio17F: number; ratio5F: number; ratioMinus5F: number; isColdClimate: boolean }
> = {
inverter_cold_climate: {
label: "Cold-Climate Inverter (ccASHP / Hyper-Heat)",
ratio17F: 0.88,
ratio5F: 0.76,
ratioMinus5F: 0.65,
isColdClimate: true,
},
inverter_standard: {
label: "Standard Inverter (Variable Speed)",
ratio17F: 0.68,
ratio5F: 0.52,
ratioMinus5F: 0.38,
isColdClimate: false,
},
single_stage_standard: {
label: "Single-Stage Standard Efficiency",
ratio17F: 0.55,
ratio5F: 0.35,
ratioMinus5F: 0.20,
isColdClimate: false,
},
};
const STANDARD_HEAT_STRIP_SIZES_KW = [0, 5, 8, 10, 15, 20, 25];
/**
* Computes heat pump heating capacity at any outdoor temperature via piecewise interpolation
*/
export function getHeatPumpCapacityAtTemp(
nominalHeatingBtu: number,
tempF: number,
type: HeatPumpCompressorType
): number {
const factors = COMPRESSOR_PERFORMANCE_FACTORS[type];
if (tempF >= 47) {
const boost = 1 + (tempF - 47) * 0.005;
return Math.round(nominalHeatingBtu * Math.min(1.15, boost));
} else if (tempF >= 17) {
const frac = (tempF - 17) / (47 - 17);
const multiplier = factors.ratio17F + frac * (1.0 - factors.ratio17F);
return Math.round(nominalHeatingBtu * multiplier);
} else if (tempF >= 5) {
const frac = (tempF - 5) / (17 - 5);
const multiplier = factors.ratio5F + frac * (factors.ratio17F - factors.ratio5F);
return Math.round(nominalHeatingBtu * multiplier);
} else {
const frac = (tempF - (-5)) / (5 - (-5));
const multiplier = factors.ratioMinus5F + frac * (factors.ratio5F - factors.ratioMinus5F);
return Math.round(nominalHeatingBtu * Math.max(0.1, multiplier));
}
}
/**
* Computes building envelope heat loss at an arbitrary temperature
*/
export function getBuildingHeatLossAtTemp(
designHeatLoss: number,
designOutdoorTempF: number,
currentTempF: number,
indoorSetpoint: number = 70
): number {
if (currentTempF >= indoorSetpoint) return 0;
const designDeltaT = Math.max(10, indoorSetpoint - designOutdoorTempF);
const currentDeltaT = Math.max(0, indoorSetpoint - currentTempF);
return Math.round(designHeatLoss * (currentDeltaT / designDeltaT));
}
/**
* Main heat pump balance point and auxiliary sizing engine
*/
export function calculateHeatPumpSizing(input: HeatPumpInput): HeatPumpOutput {
const tons = Math.max(1.0, Math.min(6.0, input.nominalTonnage));
const nominalCoolingBtu = Math.round(tons * 12000);
const nominalHeatingBtu47F = Math.round(nominalCoolingBtu * 1.05);
const type = input.compressorType || "inverter_cold_climate";
const outdoorDesign = input.outdoorDesignTempF;
const designHeatingLoss = Math.max(5000, input.designHeatingLossBtu);
const designCoolingLoad = input.designCoolingLoadBtu || nominalCoolingBtu;
const indoorSetpoint = input.indoorSetpointF || 70;
// Capacity and load at winter design temperature
const heatingCapacityAtDesignBtu = getHeatPumpCapacityAtTemp(nominalHeatingBtu47F, outdoorDesign, type);
const buildingHeatLossAtDesignBtu = designHeatingLoss;
// Heating deficit requiring backup heat strips
const auxiliaryDeficitBtu = Math.max(0, buildingHeatLossAtDesignBtu - heatingCapacityAtDesignBtu);
// Match commercial electric heat strip kW rating (1 kW = 3,412.14 BTU/h)
const rawAuxKw = auxiliaryDeficitBtu / 3412.14;
let recommendedAuxHeatStripKw = 0;
if (rawAuxKw > 0) {
for (const kw of STANDARD_HEAT_STRIP_SIZES_KW) {
if (kw >= rawAuxKw) {
recommendedAuxHeatStripKw = kw;
break;
}
}
if (recommendedAuxHeatStripKw === 0) {
recommendedAuxHeatStripKw = Math.ceil(rawAuxKw / 5) * 5;
}
}
// Find Thermal Balance Point where Capacity >= Building Load
let thermalBalancePointF = outdoorDesign;
for (let t = Math.round(outdoorDesign); t <= indoorSetpoint; t++) {
const loss = getBuildingHeatLossAtTemp(designHeatingLoss, outdoorDesign, t, indoorSetpoint);
const cap = getHeatPumpCapacityAtTemp(nominalHeatingBtu47F, t, type);
if (cap >= loss) {
thermalBalancePointF = t;
break;
}
}
// Generate 15-point thermal performance curve (-10°F to 60°F)
const curvePoints: CurvePoint[] = [];
for (let t = -10; t <= 60; t += 5) {
const loss = getBuildingHeatLossAtTemp(designHeatingLoss, outdoorDesign, t, indoorSetpoint);
const cap = getHeatPumpCapacityAtTemp(nominalHeatingBtu47F, t, type);
curvePoints.push({
outdoorTempF: t,
buildingHeatLossBtu: loss,
heatPumpCapacityBtu: cap,
auxiliaryDeficitBtu: Math.max(0, loss - cap),
});
}
const manualSCoolingRatio = Number((nominalCoolingBtu / designCoolingLoad).toFixed(2));
return {
nominalTonnage: tons,
nominalCoolingBtu,
nominalHeatingBtu47F,
heatingCapacityAtDesignBtu,
buildingHeatLossAtDesignBtu,
thermalBalancePointF,
auxiliaryDeficitBtu,
recommendedAuxHeatStripKw,
isColdClimateQualified: COMPRESSOR_PERFORMANCE_FACTORS[type].isColdClimate,
manualSCoolingRatio,
curvePoints,
};
}
Vitest Unit Test Suite
We verify the engine against real-world engineering constraints:
import { describe, it, expect } from "vitest";
import {
calculateHeatPumpSizing,
getHeatPumpCapacityAtTemp,
getBuildingHeatLossAtTemp
} from "./heat-pump";
describe("Heat Pump Sizing & Thermal Balance Point Engine", () => {
it("calculates 3-ton Cold Climate Inverter balance point and aux heat strip accurately", () => {
// 3 Tons = 36k cooling, 37.8k heating @ 47°F
// Building design heat loss: 40,000 BTU/h at 5°F outdoor design temp
const res = calculateHeatPumpSizing({
nominalTonnage: 3.0,
compressorType: "inverter_cold_climate",
outdoorDesignTempF: 5,
designHeatingLossBtu: 40000,
designCoolingLoadBtu: 34000,
});
expect(res.nominalTonnage).toBe(3.0);
expect(res.nominalHeatingBtu47F).toBe(37800);
expect(res.isColdClimateQualified).toBe(true);
expect(res.thermalBalancePointF).toBeGreaterThan(15);
expect(res.thermalBalancePointF).toBeLessThan(30);
expect(res.recommendedAuxHeatStripKw).toBeGreaterThanOrEqual(5);
});
it("evaluates single-stage heat pump severe low-ambient capacity drop", () => {
// Single-stage compressors drop to 35% rated capacity at 5°F
const cap47 = getHeatPumpCapacityAtTemp(36000, 47, "single_stage_standard");
const cap5 = getHeatPumpCapacityAtTemp(36000, 5, "single_stage_standard");
expect(cap47).toBe(36000);
expect(cap5).toBe(12600); // 36,000 * 0.35 = 12,600 BTU/h
});
it("calculates building heat loss slope linearly", () => {
// 40,000 BTU loss at 10°F (Delta T = 60°F from 70°F setpoint)
// At 40°F (Delta T = 30°F), loss should be exactly 20,000 BTU/h
const loss40 = getBuildingHeatLossAtTemp(40000, 10, 40, 70);
expect(loss40).toBe(20000);
});
});
Live Interactive Workbench
To test this engine with live interactive dual-axis charts, temperature sliders, and instant Manual S oversizing diagnostics, check out the interactive Heat Pump Sizing & Balance Point Calculator on HVACLogic. You can also benchmark envelope heating requirements with the companion BTU Heating & Cooling Load Calculator and explore heating system decarbonization in the Heating Systems Engineering Guide.
All source equations run 100% client-side with zero telemetry or tracking.



