When sizing cold-climate heat pump systems, the refrigeration cycle inevitably encounters a physical constraint: as outdoor temperatures plummet toward winter design conditions (such as 5°F or -10°F), building heat loss peaks precisely when heat pump thermal output derates.
To prevent indoor comfort collapse below the thermal balance point, modern split heat pump systems rely on auxiliary electric resistance heat strips mounted inside the air handler.
However, sizing these resistance banks introduces a dual engineering challenge:
- Thermodynamic Deficit Modeling (ACCA Manual S): Quantifying the exact hourly deficit between peak building design heat loss and certified low-ambient heat pump output, then quantizing the continuous demand to standard manufactured nominal element sizes (4.8 kW to 20.0 kW).
- Electrical Branch Circuit Compliance (NEC Article 424): Treating fixed electric space heating as a continuous load requiring a mandatory 125% Minimum Circuit Ampacity (MCA) multiplier, sizing overcurrent protection (MOPD breakers), and executing automatic multi-circuit partitioning when single-phase continuous current exceeds the 48A threshold (NEC 424.22).
In this article, we walk through how we implemented this calculation engine in pure, deterministic TypeScript with zero client-side dependencies, 100% Vitest coverage, and sub-millisecond execution.
You can inspect the live interactive tool on HVACLogic: Heat Pump Auxiliary Electric Heat Strip Sizing Calculator.
The Mathematical & Physical Model
1. Thermal Deficit Quantification
Under ACCA Manual S equipment selection protocols, supplemental resistance heat is sized to satisfy the unmet capacity at the 99% local winter outdoor design temperature:
Q_deficit = max(0, Q_loss - Q_hp)
Where:
-
Q_loss: Whole-building peak winter design heat loss (BTU/hr) derived from our Building Heat Loss Calculator. -
Q_hp: Manufacturer-certified heating capacity delivered by the heat pump at local outdoor design temperature, verified via the Heat Pump Sizing & Balance Point Tool.
Converting the net deficit to theoretical electric power demand:
kW_req = Q_deficit / 3412.142 BTU/kWh
2. Nominal Element Quantization
Field equipment does not ship in arbitrary fractions of a kilowatt. Electric resistance heater kits are manufactured in discrete, standard ratings. Our engine matches continuous demand to the smallest standard nominal element that satisfies the load:
Standard Ratings (kW) = [4.8, 5.0, 7.5, 8.0, 9.6, 10.0, 14.4, 15.0, 19.2, 20.0]
3. NEC Article 424 Electrical Load Sizing
Under National Electrical Code (NEC) Article 424.3(B), fixed electric space heating equipment is classified as a continuous load (operating for 3 hours or more).
Full Load Amps (FLA):
-
Single-Phase (1-Phase):
FLA = (kW * 1000) / Voltage -
Three-Phase (3-Phase):
FLA = (kW * 1000) / (Voltage * sqrt(3))
Minimum Circuit Ampacity (MCA):
MCA = FLA * 1.25
Multi-Circuit Subdivision (NEC 424.22):
Where total resistance heating load exceeds 48 Amperes on a single circuit, the manufacturer subdivides the assembly into multiple feeds (for example, a 15 kW element at 240V draws 62.5A FLA / 78.13A MCA, requiring a 60A primary circuit paired with a 30A secondary circuit).
4. Sensible Airflow Temperature Rise Screening
To ensure the air handler delivers safe discharge temperatures without tripping high-limit thermal cutouts:
Delta_T = Delivered_BTU / (1.08 * CFM)
Screening airflow is cross-checked against our HVAC CFM Calculator.
Implementing the Engine in Pure TypeScript
Here is the complete, self-contained implementation of the sizing engine:
export type SizingObjective = "supplemental_deficit" | "full_emergency_backup" | "defrost_tempering";
export type ElectricalVoltage = 240 | 208 | 480;
export type ElectricalPhase = 1 | 3;
export interface HeatStripInput {
designHeatLossBtu: number;
heatPumpCapacityAtDesignBtu: number;
nominalTonnage: number;
sizingObjective?: SizingObjective;
voltage?: ElectricalVoltage;
phase?: ElectricalPhase;
systemAirflowCfm?: number;
safetyOversizeFactor?: number;
}
export interface CircuitBranch {
circuitNumber: number;
elementKw: number;
flaAmps: number;
mcaAmps: number;
breakerAmps: number;
wireGaugeCu75C: string;
}
export interface HeatStripOutput {
designHeatLossBtu: number;
heatPumpCapacityAtDesignBtu: number;
netHeatingDeficitBtu: number;
sizingObjective: SizingObjective;
theoreticalRequiredKw: number;
selectedStandardKw: number;
totalDeliveredBtu: number;
excessCapacityKw: number;
sizingMarginPercentage: number;
voltage: ElectricalVoltage;
phase: ElectricalPhase;
totalFlaAmps: number;
totalMcaAmps: number;
suggestedMopdBreakerAmps: number;
isMultiCircuitRequired: boolean;
circuitBranches: CircuitBranch[];
systemAirflowCfm: number;
screeningAirflowCfm: number;
estimatedTempRiseF: number;
airflowScreeningStatus: "Adequate Airflow" | "Low Airflow Warning";
}
const STANDARD_KW_ELEMENTS = [4.8, 5.0, 7.5, 8.0, 9.6, 10.0, 14.4, 15.0, 19.2, 20.0];
const BTU_PER_KW = 3412.142;
export function calculateHeatStripSizing(input: HeatStripInput): HeatStripOutput {
const {
designHeatLossBtu,
heatPumpCapacityAtDesignBtu,
nominalTonnage,
sizingObjective = "supplemental_deficit",
voltage = 240,
phase = 1,
systemAirflowCfm = nominalTonnage * 400,
safetyOversizeFactor = 1.0,
} = input;
let netDeficitBtu = 0;
if (sizingObjective === "supplemental_deficit") {
netDeficitBtu = Math.max(0, designHeatLossBtu - heatPumpCapacityAtDesignBtu);
} else if (sizingObjective === "full_emergency_backup") {
netDeficitBtu = designHeatLossBtu;
} else {
// Defrost cycle tempering: typically ~70% of nominal tonnage heating rate
netDeficitBtu = Math.round(nominalTonnage * 12000 * 0.7);
}
const rawKwNeeded = (netDeficitBtu / BTU_PER_KW) * safetyOversizeFactor;
const theoreticalRequiredKw = Math.round(rawKwNeeded * 100) / 100;
let selectedStandardKw = STANDARD_KW_ELEMENTS[STANDARD_KW_ELEMENTS.length - 1];
for (const element of STANDARD_KW_ELEMENTS) {
if (element >= rawKwNeeded) {
selectedStandardKw = element;
break;
}
}
const totalDeliveredBtu = Math.round(selectedStandardKw * BTU_PER_KW);
const excessCapacityKw = Math.round((selectedStandardKw - theoreticalRequiredKw) * 100) / 100;
const sizingMarginPercentage = theoreticalRequiredKw > 0
? Math.round(((selectedStandardKw - theoreticalRequiredKw) / theoreticalRequiredKw) * 100)
: 0;
// Electrical load calculations (NEC Article 424)
const divisor = phase === 3 ? voltage * Math.sqrt(3) : voltage;
const totalFlaAmps = Math.round(((selectedStandardKw * 1000) / divisor) * 100) / 100;
const totalMcaAmps = Math.round((totalFlaAmps * 1.25) * 100) / 100;
const isMultiCircuitRequired = phase === 1 && totalFlaAmps > 48;
const circuitBranches: CircuitBranch[] = [];
if (!isMultiCircuitRequired) {
let breaker = 15;
if (totalMcaAmps > 50) breaker = 60;
else if (totalMcaAmps > 40) breaker = 50;
else if (totalMcaAmps > 30) breaker = 45;
else if (totalMcaAmps > 25) breaker = 35;
else if (totalMcaAmps > 20) breaker = 30;
else if (totalMcaAmps > 15) breaker = 25;
else breaker = 20;
circuitBranches.push({
circuitNumber: 1,
elementKw: selectedStandardKw,
flaAmps: totalFlaAmps,
mcaAmps: totalMcaAmps,
breakerAmps: breaker,
wireGaugeCu75C: totalMcaAmps > 40 ? "6 AWG Cu" : totalMcaAmps > 30 ? "8 AWG Cu" : "10 AWG Cu",
});
} else {
// Partitioning 15kW and 20kW split feeds
const branch1Kw = selectedStandardKw >= 19.2 ? 10.0 : selectedStandardKw >= 14.4 ? 9.6 : 10.0;
const branch2Kw = Math.round((selectedStandardKw - branch1Kw) * 10) / 10;
const fla1 = Math.round(((branch1Kw * 1000) / voltage) * 100) / 100;
const mca1 = Math.round((fla1 * 1.25) * 100) / 100;
const fla2 = Math.round(((branch2Kw * 1000) / voltage) * 100) / 100;
const mca2 = Math.round((fla2 * 1.25) * 100) / 100;
circuitBranches.push(
{ circuitNumber: 1, elementKw: branch1Kw, flaAmps: fla1, mcaAmps: mca1, breakerAmps: 60, wireGaugeCu75C: "6 AWG Cu" },
{ circuitNumber: 2, elementKw: branch2Kw, flaAmps: fla2, mcaAmps: mca2, breakerAmps: branch2Kw > 5 ? 45 : 30, wireGaugeCu75C: branch2Kw > 5 ? "8 AWG Cu" : "10 AWG Cu" }
);
}
const suggestedMopdBreakerAmps = isMultiCircuitRequired
? circuitBranches[0].breakerAmps
: circuitBranches[0].breakerAmps;
// Airflow screening & sensible Delta T
const screeningAirflowCfm = Math.round(selectedStandardKw * 45);
const effectiveCfm = Math.max(100, systemAirflowCfm);
const estimatedTempRiseF = Math.round((totalDeliveredBtu / (1.08 * effectiveCfm)) * 10) / 10;
const airflowScreeningStatus = systemAirflowCfm >= screeningAirflowCfm ? "Adequate Airflow" : "Low Airflow Warning";
return {
designHeatLossBtu,
heatPumpCapacityAtDesignBtu,
netHeatingDeficitBtu,
sizingObjective,
theoreticalRequiredKw,
selectedStandardKw,
totalDeliveredBtu,
excessCapacityKw,
sizingMarginPercentage,
voltage,
phase,
totalFlaAmps,
totalMcaAmps,
suggestedMopdBreakerAmps,
isMultiCircuitRequired,
circuitBranches,
systemAirflowCfm,
screeningAirflowCfm,
estimatedTempRiseF,
airflowScreeningStatus,
};
}
Unit Testing with Invariant Constraints
To guarantee mathematical consistency across all voltage configurations and boundary conditions, we run automated tests in Vitest:
import { describe, it, expect } from "vitest";
import { calculateHeatStripSizing } from "./heat-strip";
describe("Heat Strip Auxiliary Sizing Engine", () => {
it("correctly sizes 7.5 kW element for 21,000 BTU/hr deficit at 240V", () => {
const result = calculateHeatStripSizing({
designHeatLossBtu: 45000,
heatPumpCapacityAtDesignBtu: 24000,
nominalTonnage: 3.0,
voltage: 240,
phase: 1,
systemAirflowCfm: 1200,
});
expect(result.netHeatingDeficitBtu).toBe(21000);
expect(result.theoreticalRequiredKw).toBe(6.15);
expect(result.selectedStandardKw).toBe(7.5);
expect(result.totalDeliveredBtu).toBe(25591);
expect(result.totalFlaAmps).toBe(31.25);
expect(result.totalMcaAmps).toBe(39.06);
expect(result.isMultiCircuitRequired).toBe(false);
expect(result.estimatedTempRiseF).toBe(19.7);
expect(result.airflowScreeningStatus).toBe("Adequate Airflow");
});
it("triggers multi-circuit partitioning for 15.0 kW load exceeding 48A FLA", () => {
const result = calculateHeatStripSizing({
designHeatLossBtu: 50000,
heatPumpCapacityAtDesignBtu: 0,
nominalTonnage: 4.0,
sizingObjective: "full_emergency_backup",
voltage: 240,
phase: 1,
systemAirflowCfm: 1600,
});
expect(result.selectedStandardKw).toBe(15.0);
expect(result.totalFlaAmps).toBe(62.5);
expect(result.isMultiCircuitRequired).toBe(true);
expect(result.circuitBranches.length).toBe(2);
expect(result.circuitBranches[0].elementKw).toBe(10.0);
expect(result.circuitBranches[1].elementKw).toBe(5.0);
});
});
Key Takeaways for Building Science Web Applications
- Keep Physics in Pure Functions: Isolating thermodynamics and electrical code into zero-dependency TypeScript modules allows the exact same engine to run in client-side React UI components, Edge API endpoints, and offline PWA service workers.
- Never Return Raw Floats to Users: Physical hardware ships in standardized nominal ratings. Engineering calculators must quantize continuous math to realistic catalog offerings.
- Connect the End-to-End Workflow: By passing state seamlessly across Building Heat Loss, Heat Pump Sizing, and Auxiliary Heat Strip Sizing, users complete entire mechanical load workflows without manual re-entry.
All calculation engines are open-source and run locally in your browser with zero data tracking at HVACLogic.
Top comments (0)