The most common mistake in residential battery sizing and emergency backup calculations is the linear division trap:
In physical reality, electrochemical cells and power electronics never behave linearly. Under real-world load conditions, nominal runtime formulas can overestimate battery backup endurance by 30% to 50%.
Three physical mechanisms cause this discrepancy:
- Peukert's Law (Electrochemical Rate Capacity Effect): As discharge current increases, internal cell resistance (I2R) and ion diffusion bottlenecks rapidly decrease usable capacity.
- Parasitic Inverter Tare Losses (No-Load Quiescent Draw): Inverters consume continuous idle power (typically 15W to 55W) simply keeping gate drivers, control circuitry, and transformers energized, regardless of whether the output load is 10W or 1,000W.
- Electrochemical Depth of Discharge (DoD) Boundaries: Discharging lead-acid past 50% or standard lithium chemistries past 80% to 90% triggers rapid irreversible cell degradation.
In this article, we translate these electrochemical and power electronics equations into a deterministic, side-effect-free TypeScript engine.
1. The Mathematical Physics of Battery Discharge
1.1 Peukert's Electrochemical Derating Formula
Formulated in 1897 by Wilhelm Peukert, the equation governs the non-linear relationship between discharge current and available capacity:
Where:
- t = Discharge time (hours)
- H = Rated discharge hour rating (typically 20 hours for lead-acid, 1 to 5 hours for LiFePO4)
- C = Rated capacity at hour rating H (Ampere-hours, Ah)
- I = Continuous discharge current (Amperes)
-
k = Peukert exponent (dimensionless):
- Lithium Iron Phosphate (LiFePO4): 1.02 to 1.05 (near-linear)
- AGM / Sealed Lead-Acid: 1.10 to 1.20
- Flooded Lead-Acid: 1.25 to 1.40 (severe capacity collapse at high loads)
When modeled in Watt-hours (Eeffective), the effective available energy under continuous load Pload at system voltage Vnom becomes:
1.2 Inverter Conversion Efficiency & Parasitic Tare Losses
DC-to-AC power conversion incurs both conversion losses and continuous fixed overhead:
Where:
- Pac_load = Sum of connected alternating-current appliances (Watts)
- ηinverter = Operating full-load efficiency (typically 0.88 to 0.94)
- Ptare = Fixed no-load parasitic consumption (typically 15W to 45W for split-phase off-grid inverters)
At low loads (such as running a 30W CPAP machine or router overnight on a 3,000W inverter), Ptare accounts for more than 50% of the total battery drain.
2. Deterministic TypeScript Modeling Engine
We implement this modeling pipeline in pure TypeScript. The engine accepts strictly typed immutable inputs and returns a structured calculation envelope with provenance metadata.
export interface BatteryEngineInput {
nominalCapacityAh: number;
systemVoltage: number;
chemistry: "lifepo4" | "agm" | "flooded_lead_acid" | "lithium_ion";
depthOfDischargeLimit: number; // e.g. 0.80 for 80% DoD
inverterEfficiency: number; // e.g. 0.92
inverterTareLossWatts: number; // e.g. 25
acLoadWatts: number;
dcLoadWatts?: number;
}
export interface BatteryRuntimeResult {
runtimeHours: number;
effectiveCapacityWh: number;
usableCapacityWh: number;
totalContinuousDrawWatts: number;
peukertDeratingFactor: number;
effectiveDischargeAmps: number;
warnings: string[];
}
export const PEUKERT_EXPONENTS: Record<BatteryEngineInput["chemistry"], number> = {
lifepo4: 1.03,
lithium_ion: 1.05,
agm: 1.15,
flooded_lead_acid: 1.30,
};
export const STANDARD_HOUR_RATINGS: Record<BatteryEngineInput["chemistry"], number> = {
lifepo4: 1.0,
lithium_ion: 1.0,
agm: 20.0,
flooded_lead_acid: 20.0,
};
export function calculateBatteryRuntime(input: BatteryEngineInput): BatteryRuntimeResult {
const warnings: string[] = [];
const dcLoad = input.dcLoadWatts ?? 0;
const convertedAcLoad = input.acLoadWatts > 0
? (input.acLoadWatts / input.inverterEfficiency) + input.inverterTareLossWatts
: 0;
const totalContinuousDrawWatts = convertedAcLoad + dcLoad;
if (totalContinuousDrawWatts <= 0) {
throw new Error("Total connected load must be greater than 0 Watts.");
}
const nominalEnergyWh = input.nominalCapacityAh * input.systemVoltage;
const usableEnergyWh = nominalEnergyWh * input.depthOfDischargeLimit;
const rawDischargeAmps = totalContinuousDrawWatts / input.systemVoltage;
const peukertK = PEUKERT_EXPONENTS[input.chemistry];
const ratedHours = STANDARD_HOUR_RATINGS[input.chemistry];
// Rated discharge current at benchmark rating H
const ratedDischargeAmps = input.nominalCapacityAh / ratedHours;
// Peukert derating factor: (I_rated / I_actual)^(k - 1)
let peukertFactor = 1.0;
if (rawDischargeAmps > 0 && ratedDischargeAmps > 0) {
peukertFactor = Math.pow(ratedDischargeAmps / rawDischargeAmps, peukertK - 1.0);
// Clamp to realistic physical range [0.35, 1.05]
peukertFactor = Math.min(1.05, Math.max(0.35, peukertFactor));
}
const effectiveCapacityWh = usableEnergyWh * peukertFactor;
const runtimeHours = effectiveCapacityWh / totalContinuousDrawWatts;
if (input.chemistry === "flooded_lead_acid" && input.depthOfDischargeLimit > 0.50) {
warnings.push("Depth of discharge exceeds 50% for flooded lead-acid, accelerating plate sulfation.");
}
if (rawDischargeAmps > input.nominalCapacityAh * 1.5) {
warnings.push("Continuous discharge rate exceeds 1.5C, inducing thermal degradation.");
}
return {
runtimeHours: Number(runtimeHours.toFixed(2)),
effectiveCapacityWh: Math.round(effectiveCapacityWh),
usableCapacityWh: Math.round(usableEnergyWh),
totalContinuousDrawWatts: Math.round(totalContinuousDrawWatts),
peukertDeratingFactor: Number(peukertFactor.toFixed(3)),
effectiveDischargeAmps: Number(rawDischargeAmps.toFixed(2)),
warnings,
};
}
3. Comparative Benchmark: Lead-Acid vs. LiFePO4
To quantify the divergence, consider a 12V 200Ah battery bank (2,400 Wh nominal) powering an 800W continuous emergency load through an inverter with 92% efficiency and 25W tare draw:
| Parameter | Flooded Lead-Acid (k=1.30, DoD 50%) | LiFePO4 (k=1.03, DoD 90%) |
|---|---|---|
| Nominal Energy | 2,400 Wh | 2,400 Wh |
| Usable Energy (DoD) | 1,200 Wh | 2,160 Wh |
| Rated Benchmark Current | 10.0 A (C/20) | 200.0 A (1*C*) |
| Peukert Factor | (10 / 74.55)0.30 = 0.548 | (200 / 74.55)0.03 = 1.030 |
| Effective Delivered Energy | 657 Wh | 2,160 Wh |
| Calculated Runtime | 0.73 hours (44 mins) | 2.41 hours (145 mins) |
| Naive Linear Runtime | 1.34 hours (80 mins) | 2.41 hours (145 mins) |
| Linear Error Magnitude | +83.5% Overestimation | < 1.0% |
Under lead-acid chemistry, neglecting Peukert derating causes an 83% over-prediction of backup duration.
4. Vitest Invariant Verification
We enforce physical monotonicity and deterministic stability across test suites:
import { describe, it, expect } from "vitest";
import { calculateBatteryRuntime } from "./battery-runtime-engine";
describe("Battery Runtime Calculation Invariants", () => {
it("should enforce monotonic runtime reduction as load increases", () => {
const baseConfig = {
nominalCapacityAh: 100,
systemVoltage: 12,
chemistry: "lifepo4" as const,
depthOfDischargeLimit: 0.8,
inverterEfficiency: 0.92,
inverterTareLossWatts: 20,
};
const run100W = calculateBatteryRuntime({ ...baseConfig, acLoadWatts: 100 });
const run500W = calculateBatteryRuntime({ ...baseConfig, acLoadWatts: 500 });
const run1000W = calculateBatteryRuntime({ ...baseConfig, acLoadWatts: 1000 });
expect(run100W.runtimeHours).toBeGreaterThan(run500W.runtimeHours);
expect(run500W.runtimeHours).toBeGreaterThan(run1000W.runtimeHours);
});
it("should penalize high-load lead-acid runtime via Peukert exponent", () => {
const leadAcid = calculateBatteryRuntime({
nominalCapacityAh: 200,
systemVoltage: 12,
chemistry: "flooded_lead_acid",
depthOfDischargeLimit: 0.5,
inverterEfficiency: 0.90,
inverterTareLossWatts: 25,
acLoadWatts: 800,
});
expect(leadAcid.peukertDeratingFactor).toBeLessThan(0.70);
});
});
5. Conclusion & Reference Implementation
Accurate clean energy modeling requires accounting for electrochemical rate boundaries and power electronics tare dissipation.
You can inspect the full open-source mathematical modeling framework and interactive simulations at PowerLab Battery Backup Runtime Model or explore the developer API contracts at PowerLab Developer Documentation.
Top comments (0)