Back to Blog
When Your Spreadsheet Lies: Building a Battery Calculator That Understands Chemistry
10 min readSep 13, 20264 views

When Your Spreadsheet Lies: Building a Battery Calculator That Understands Chemistry

Off-grid power systems are a hot topic, but most battery calculators miss the crucial details of battery chemistry, leading to over-spec'd or underperforming systems. Let's build a calculator that accounts for discharge curves and Peukert's law, giving you real-world capacity.

Software DesignPerformanceWeb DevelopmentToolingPhysics
Share

by Sunil Band

The Problem with Simplistic Battery Calculations

If you've ever tried to size a battery bank for an off-grid cabin, a camper van, or even a robust backup system for your home lab, you've run into the same problem I have. Every online calculator and most spreadsheets give you a suspiciously simple answer: Capacity (Ah) * Voltage (V) = Energy (Wh). Divide that by your average load, and boom, you have your run time. Sounds great, right?

Wrong. That calculation is a fundamental oversimplification that leads to either wildly oversized (and expensive) battery banks or, worse, systems that fail to deliver power when you actually need it. The problem is that a battery's "capacity" isn't a fixed number. It's heavily influenced by the discharge rate and the chemistry itself. Ignore these, and you're building on a house of cards.

I got tired of these calculators lying to me. So I decided to build one that actually respects the physics and chemistry involved. It's not just about getting the right numbers; it's about understanding why those numbers are the way they are.

Why Discharge Rate Matters: Peukert's Law

The biggest lie told by simple calculators is that a 100Ah battery will provide 100 Amps for one hour, or 1 Amp for 100 hours. This is rarely true in practice, especially for lead-acid batteries. As the discharge rate (current) increases, the available capacity decreases. This phenomenon is described by Peukert's law.

Peukert's law states that C_p = I^k * T, where C_p is the battery's theoretical capacity at a 1-amp discharge rate, I is the actual discharge current, T is the actual discharge time, and k is the Peukert exponent. The k value is almost always greater than 1, meaning that as I increases, T decreases disproportionately. A k of 1 means a perfect battery (not real), and typical lead-acid batteries have k values between 1.05 and 1.25. Lithium batteries are much closer to 1, which is one of their major advantages.

This k value is crucial. Without it, you're assuming a linear relationship that doesn't exist. My calculator needed to incorporate this. Here's a simplified way to think about it: if you pull current faster, the internal resistance of the battery dissipates more energy as heat, reducing the energy available at the terminals. It's like trying to push water through a narrow pipe – the faster you push, the more energy is lost to friction.

Discharge Curves and Depth of Discharge (DoD)

Beyond Peukert's law, battery manufacturers provide discharge curves. These graphs show the terminal voltage over time for different discharge rates. Why does this matter? Because most loads have a minimum operating voltage. If your battery voltage sags too low, your inverter might shut off, or your devices might stop working, even if there's still some energy left in the battery.

For example, an inverter might cut off at 10.5V for a 12V battery. If your discharge curve shows that at a high current draw, the battery hits 10.5V after only 60% of its rated capacity is used, then your usable capacity is effectively 60%, not 100%. Traditional calculators ignore this critical detail, assuming you can drain the battery to 0% state of charge (SoC).

Finally, Depth of Discharge (DoD) is important for battery longevity. Draining a lead-acid battery to 80% DoD repeatedly will kill it much faster than draining it to 50% DoD. Lithium batteries are more forgiving but still have recommended DoD limits for optimal cycle life. A good calculator should let you specify your desired DoD and adjust the usable capacity accordingly.

Building a Smarter Calculator: The Core Logic

I built my calculator in TypeScript, running in the browser. The frontend is simple, but the backend logic is where the magic happens. I focused on a modular approach, where different battery types could plug into the same calculation engine.

Let's look at a simplified example of how you'd implement the Peukert correction and handle a basic DoD.

First, we need a way to represent our battery data, including the Peukert exponent and nominal capacity at a known rate (usually C/20, meaning 20 hours to discharge).

typescript
type BatterySpec = {
  nominalCapacityAh: number; // Capacity at the specified 'ratedDischargeHours'
  ratedDischargeHours: number; // e.g., 20 for C/20 rate
  voltage: number; // Nominal voltage (e.g., 12V)
  peukertExponent: number; // k value (e.g., 1.15 for lead-acid, ~1.02 for LiFePO4)
  maxDoD: number; // Maximum recommended Depth of Discharge (e.g., 0.5 for lead-acid, 0.8 for LiFePO4)
};

const leadAcidBattery: BatterySpec = {
  nominalCapacityAh: 100, // 100 Ah at C/20 rate
  ratedDischargeHours: 20,
  voltage: 12,
  peukertExponent: 1.15,
  maxDoD: 0.5 // Only use 50% to maximize cycle life
};

const liFePO4Battery: BatterySpec = {
  nominalCapacityAh: 100, // 100 Ah at C/20 rate
  ratedDischargeHours: 20,
  voltage: 12.8, // Slightly higher nominal voltage
  peukertExponent: 1.02,
  maxDoD: 0.8 // Can safely use 80%
};

// Input: desired average load in Watts, and number of battery units
type SystemInput = {
  avgLoadWatts: number;
  numberOfBatteries: number;
};

function calculateUsableCapacity(
  battery: BatterySpec,
  input: SystemInput
): {
  effectiveCapacityAh: number;
  runTimeHours: number;
  effectiveEnergyWh: number;
} {
  // First, calculate the rated current at the nominal capacity rate
  const ratedCurrentA = battery.nominalCapacityAh / battery.ratedDischargeHours;

  // Calculate the total nominal capacity for the system
  const totalNominalCapacityAh = battery.nominalCapacityAh * input.numberOfBatteries;

  // Calculate the actual current draw from the battery bank
  // Assuming parallel connection, voltage remains constant.
  // This current will be split across the number of batteries.
  const systemVoltage = battery.voltage;
  const totalCurrentDrawA = input.avgLoadWatts / systemVoltage;

  // The discharge current per individual battery
  const currentPerBatteryA = totalCurrentDrawA / input.numberOfBatteries;

  // Apply Peukert's Law to get the *effective* capacity at the *current discharge rate*.
  // We need C_p, the theoretical capacity at 1A discharge, from the rated capacity.
  // C_p = I_rated^k * T_rated
  const peukertConstantCp = Math.pow(ratedCurrentA, battery.peukertExponent) * battery.ratedDischargeHours;

  // Now, use C_p and the actual current per battery to find the *effective* discharge time (T_eff)
  // T_eff = C_p / I_actual^k
  // Effective capacity at this rate = I_actual * T_eff
  const effectiveCapacityPerBatteryAh = peukertConstantCp / Math.pow(currentPerBatteryA, battery.peukertExponent - 1); // Simplified form of I_actual * T_eff

  const totalEffectiveCapacityAh = effectiveCapacityPerBatteryAh * input.numberOfBatteries;

  // Apply Depth of Discharge limit
  const usableCapacityAh = totalEffectiveCapacityAh * battery.maxDoD;

  // Calculate estimated run time
  const runTimeHours = usableCapacityAh / totalCurrentDrawA;

  // Calculate effective energy
  const effectiveEnergyWh = usableCapacityAh * systemVoltage;

  return {
    effectiveCapacityAh: usableCapacityAh,
    runTimeHours: runTimeHours,
    effectiveEnergyWh: effectiveEnergyWh
  };
}

// Example usage:
const mySystemInput: SystemInput = {
  avgLoadWatts: 200, // Running a 200W load
  numberOfBatteries: 2 // Two 12V 100Ah batteries in parallel
};

const leadAcidResults = calculateUsableCapacity(leadAcidBattery, mySystemInput);
console.log('Lead-Acid Results:', leadAcidResults);
// Expected output for lead-acid: lower run time due to Peukert and DoD

const liFePO4Results = calculateUsableCapacity(liFePO4Battery, mySystemInput);
console.log('LiFePO4 Results:', liFePO4Results);
// Expected output for LiFePO4: higher run time due to better Peukert and DoD

Let's break down that calculateUsableCapacity function:

  1. ratedCurrentA: We first figure out the current at which the nominalCapacityAh was specified. This is crucial for calculating the Peukert constant. For a C/20 rate (e.g., 100Ah over 20 hours), the rated current is 5A.
  2. totalCurrentDrawA: This is the total current your system will pull from the battery bank, based on your average load in watts and the battery's nominal voltage. We're assuming a simple resistive load here.
  3. currentPerBatteryA: If you have multiple batteries in parallel, the total current draw is distributed among them. We need the current per individual battery to apply Peukert's law accurately to each battery unit.
  4. peukertConstantCp: This is C_p from the Peukert's law equation. It's a battery-specific constant derived from the manufacturer's rated capacity and discharge rate. Think of it as the battery's 'true' capacity if discharged at a perfect 1 amp rate, ignoring internal losses.
  5. effectiveCapacityPerBatteryAh: This is where Peukert's law adjusts the capacity. Based on the actual current per battery (currentPerBatteryA) and the peukertExponent, we calculate the real, available capacity of each battery at that specific discharge rate. Notice the k-1 exponent; this is a common simplification when deriving Ah = C_p / I^(k-1).
  6. usableCapacityAh: Finally, we apply the maxDoD to the totalEffectiveCapacityAh. This gives us the real-world capacity you should rely on for daily operation to ensure battery longevity.
  7. runTimeHours: With the usableCapacityAh and the totalCurrentDrawA, we can now get a much more realistic estimate of how long your battery bank will power your load.

The Missing Piece: Discharge Curves in Detail

My example above simplified the discharge curve part by using a maxDoD. A truly sophisticated calculator would need to parse actual discharge curve data (often found in manufacturer datasheets as tables or graphs). This would involve:

  • Interpolation: Given a current draw, find the corresponding voltage drop over time.
  • Voltage Cutoff: Determine when the voltage hits your inverter's or device's minimum acceptable level.
  • Iterative Calculation: Since discharge current isn't always constant (e.g., if you have an MPPT solar charger), you might need to model segments of discharge over time.

Implementing this would mean storing a lookup table or a mathematical function for each battery type, mapping (current, stateOfCharge) -> voltage. It's a significant increase in complexity but provides even more accuracy. For most practical purposes, a well-chosen maxDoD and accurate Peukert exponent get you 80% of the way there.

Trade-offs and Gotchas

Building this calculator revealed a few trade-offs:

  • Data Availability: Finding accurate Peukert exponents and detailed discharge curves for every battery on the market is nearly impossible. Manufacturers often don't provide k directly. You usually have to derive it from two different capacity ratings at different discharge rates, or rely on community-sourced data. This means the calculator's accuracy is only as good as the input data.
  • Complexity vs. Usability: A fully featured calculator could have dozens of input fields (temperature correction, internal resistance, cable losses, inverter efficiency, etc.). I had to balance scientific rigor with not overwhelming the user. My focus was on the biggest impact factors: Peukert's law and realistic DoD.
  • Battery Aging: Batteries degrade over time, losing capacity and often seeing an increase in internal resistance. My calculator doesn't account for this dynamic aging, which would require a complex predictive model.
  • Temperature: Battery capacity is also affected by temperature. Below freezing, many batteries (especially lead-acid) see a significant drop in usable capacity. Adding a temperature correction factor is a common next step for increased accuracy.

Despite these limitations, even incorporating just Peukert's law and a realistic DoD provides a far more accurate estimate than the simplistic Ah * V formula. It forces you to confront the reality of how batteries behave under load.

Wrapping up

Stop trusting the overly optimistic battery calculators. They're built on assumptions that simply don't hold true in the real world, especially with lead-acid chemistry. Understanding the impact of discharge rate and depth of discharge isn't just academic; it directly translates to the cost, reliability, and lifespan of your off-grid power system.

If you're serious about sizing a battery bank, take the core logic from my calculateUsableCapacity function and adapt it. Start with a realistic Peukert exponent (1.1 for a general flooded lead-acid, 1.05 for AGM/Gel, 1.02 for LiFePO4) and a conservative maxDoD (0.5 for lead-acid, 0.8 for LiFePO4). You'll quickly see how much capacity you're actually working with. Try modifying mySystemInput in the example code above with different avgLoadWatts and numberOfBatteries to see the effect on run time for both lead-acid and LiFePO4. The differences can be eye-opening.

More from the blog
Available for projectsReady to make something fun 🎈

Ready to build the next system?Wanna build something awesome together?

Currently accepting high-impact opportunities in frontend engineering and scalable web applications.Got a cool idea rattling around? Let's grab a virtual coffee and turn it into something people love. ☕