The most common mistake in residential battery sizing and emergency backup calculations is the linear division trap:
Runtime (hours)=Nominal Battery Capacity (Watt-hours)Connected Load (Watts) \text{Runtime (hours)} = \frac{\text{Nominal Battery Capacity (Watt-hours)}}{\text{Connected Load (Watts)}}
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:
t=H(CI⋅H)k t = H \left( \frac{C}{I \cdot H} \right)^k
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:
Eeffective=Vnom×C×(C⋅VnomPload⋅H)k−1 E_{\text{effective}} = V_{\text{nom}} \times C \times \left( \frac{C \cdot V_{\text{nom}}}{P_{\text{load}} \cdot H} \right)^{k – 1}
1.2 Inverter Conversion Efficiency & Parasitic Tare Losses
DC-to-AC power conversion incurs both conversion losses and continuous fixed overhead:
ParseError: KaTeX parse error: Expected ‘EOF’, got ‘_’ at position 18: …_{\text{battery_̲draw}} = \frac{…
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,
};
}
Enter fullscreen mode Exit fullscreen mode
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:
Total Battery Draw=800 W0.92+25 W=894.57 W \text{Total Battery Draw} = \frac{800\text{ W}}{0.92} + 25\text{ W} = 894.57\text{ W}
Discharge Current=894.57 W12 V=74.55 A \text{Discharge Current} = \frac{894.57\text{ W}}{12\text{ V}} = 74.55\text{ A}
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);
});
});
Enter fullscreen mode Exit fullscreen mode
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.