Skip to main content

Real-time Battery Model Loop

In this section, we'll walk through the main loop of our battery simulation code, explaining each step in detail. This simulation models the behavior of a battery over time, considering factors like power demand, temperature, state of charge (SoC), and more.

Main Simulation Loop

The main simulation loop runs while the current time t is less than the endTime. Here's the code:

while t < endTime:
debug_print(DEBUG, f"Time: {datetime.fromtimestamp(t, timezone.utc)}")

# Adjust the profile time
t_profile = t - 0 * (power_profile_df.index[-1] - power_profile_df.index[0]).total_seconds()

# Interpolate required battery power and ambient temperature
required_battery_power, dt_to_next_profile_input, lastLoadTsIdx = interpolateValue(
t_profile, power_timestamps, power_values, lastLoadTsIdx
)
ambient_temperature, _, lastTempTsIdx = interpolateValue(
t_profile, temperature_timestamps, temperature_values, lastTempTsIdx
)

# Update effective energy
effectiveEnergy += (battery.voltage * abs(battery.current)) * dt_s / 3600

# Calculate temperature statistics
maxTemperature = max(analysedElements, key=lambda x: x.temperature).temperature
minTemperature = min(analysedElements, key=lambda x: x.temperature).temperature
averageTemperature = sum(el.temperature for el in analysedElements) / len(analysedElements)

# Calculate voltage statistics
maxVoltage = max(analysedElements, key=lambda x: x.voltage).voltage
minVoltage = min(analysedElements, key=lambda x: x.voltage).voltage
averageVoltage = sum(el.voltage for el in analysedElements) / len(analysedElements)

debug_print(DEBUG, f"SoC: {battery.soc}, Voltage: {battery.voltage}")

# Manage battery current
requiredCurrent = (required_battery_power) / battery.voltage

# Implement charge tapering logic
if chargeTaperStartTh <= battery.soc < simulation_cutoffs["maxSoC"] and requiredCurrent > 0:
scaling_factor = (simulation_cutoffs["maxSoC"] - battery.soc) / (simulation_cutoffs["maxSoC"] - chargeTaperStartTh)
requiredCurrent *= scaling_factor

current = requiredCurrent

# Safety checks
minCelltemp = min(el.temperature for el in analysedElements)
maxCelltemp = max(el.temperature for el in analysedElements)

debug_print(DEBUG, f"Max Temperature: {maxCelltemp}°C")

# State Machine
state, current = batteryStateMachine(
state, minVoltage, maxCelltemp, current, requiredCurrent,
temperatureSafety, temperatureRecovery, simulation_cutoffs['maxCurrent'],
lowVoltageSafetyCutoff, t_profile
)

# Update time step
dt_s, t, prev_dt_s = update_time_step(
batteryCapacity, current, prev_dt_s, t,
min_dt_s=minimum_dt_s, maximum_dt_s=maximum_dt_s,
dt_to_next_profile_input=dt_to_next_profile_input,
sampling_multiplier=1.0, sampling_relaxation_factor=1.2,
debug=DEBUG
)

battery.setCurrent(current, SIM_TARGETED_DEPTH)

# Cooling logic
if maxCelltemp <= startCoolingTemperature:
forcedConvectionMultiplier = restingForcedConvectionMultiplier
elif maxCelltemp >= maxCoolingTemperature:
forcedConvectionMultiplier = coolingForcedConvectionMultiplier
else:
scaling_factor = ((maxCelltemp - startCoolingTemperature) /
(maxCoolingTemperature - startCoolingTemperature))
forcedConvectionMultiplier = 1 + ((coolingForcedConvectionMultiplier - 1) * scaling_factor)

fanRPM = (forcedConvectionMultiplier - 1) * (maxFanRPM / (coolingForcedConvectionMultiplier - 1))

# Update battery elements
for el in analysedElements:
el.calculateNextStep(dt_s, ambient_temperature)

battery.getVoltage(SIM_TARGETED_DEPTH)
power = battery.voltage * battery.current
battery.soc = min(el.soc for el in analysedElements)

tDateUTCTimestamp = datetime.fromtimestamp(t, timezone.utc).timestamp()

# Update sensors
digitalTwinBatterySensors["SoC"].significantAppend(float(battery.soc) * 100, tDateUTCTimestamp)
digitalTwinBatterySensors["Temperature Stack Aggregate|0"].significantAppend(minTemperature, tDateUTCTimestamp)
digitalTwinBatterySensors["Temperature Stack Aggregate|1"].significantAppend(maxTemperature, tDateUTCTimestamp)
digitalTwinBatterySensors["Temperature Stack Aggregate|2"].significantAppend(averageTemperature, tDateUTCTimestamp)
digitalTwinBatterySensors["Voltage"].significantAppend(battery.voltage, tDateUTCTimestamp)
digitalTwinBatterySensors["Current"].significantAppend(battery.current, tDateUTCTimestamp)
digitalTwinBatterySensors["Power Demand"].significantAppend(required_battery_power, tDateUTCTimestamp)
digitalTwinBatterySensors["Power"].significantAppend(power, tDateUTCTimestamp)
digitalTwinBatterySensors["Ambient Temperature"].significantAppend(ambient_temperature, tDateUTCTimestamp)

Let's break down each part of the loop.

1. Time Management

while t < endTime:
debug_print(DEBUG, f"Time: {datetime.fromtimestamp(t, timezone.utc)}")
  • Purpose: The loop runs until the simulation time t reaches endTime.
  • Debugging: If DEBUG is enabled, it prints the current simulation time in UTC.

2. Adjusting Profile Time

t_profile = t - 0 * (power_profile_df.index[-1] - power_profile_df.index[0]).total_seconds()
  • Purpose: Adjusts the profile time t_profile used for interpolation. The multiplication by 0 effectively does nothing here, but it might be a placeholder for future adjustments.

3. Interpolating Power Demand and Ambient Temperature

required_battery_power, dt_to_next_profile_input, lastLoadTsIdx = interpolateValue(
t_profile, power_timestamps, power_values, lastLoadTsIdx
)
ambient_temperature, _, lastTempTsIdx = interpolateValue(
t_profile, temperature_timestamps, temperature_values, lastTempTsIdx
)
  • Purpose: Interpolates the required battery power and ambient temperature at the current time t_profile.
  • Functions Used:
    • interpolateValue: A function that interpolates values based on time series data.

4. Updating Effective Energy

effectiveEnergy += (battery.voltage * abs(battery.current)) * dt_s / 3600
  • Purpose: Calculates the energy consumed or supplied during the time step dt_s and adds it to effectiveEnergy.
  • Explanation: Energy (Wh) = Voltage (V) * Current (A) * Time (h).

5. Calculating Temperature Statistics

maxTemperature = max(analysedElements, key=lambda x: x.temperature).temperature
minTemperature = min(analysedElements, key=lambda x: x.temperature).temperature
averageTemperature = sum(el.temperature for el in analysedElements) / len(analysedElements)
  • Purpose: Computes the maximum, minimum, and average temperatures of the battery elements.
  • Variables:
    • analysedElements: A list of battery elements (e.g., cells) being analyzed.

6. Calculating Voltage Statistics

maxVoltage = max(analysedElements, key=lambda x: x.voltage).voltage
minVoltage = min(analysedElements, key=lambda x: x.voltage).voltage
averageVoltage = sum(el.voltage for el in analysedElements) / len(analysedElements)
  • Purpose: Computes the maximum, minimum, and average voltages of the battery elements.

7. Debugging SoC and Voltage

debug_print(DEBUG, f"SoC: {battery.soc}, Voltage: {battery.voltage}")
  • Purpose: Prints the current State of Charge (SoC) and voltage of the battery if debugging is enabled.

8. Managing Battery Current

requiredCurrent = (required_battery_power) / battery.voltage
  • Purpose: Calculates the required current based on the power demand and current battery voltage.
  • Explanation: Current (A) = Power (W) / Voltage (V).

Charge Tapering Logic

if chargeTaperStartTh <= battery.soc < simulation_cutoffs["maxSoC"] and requiredCurrent > 0:
scaling_factor = (simulation_cutoffs["maxSoC"] - battery.soc) / (simulation_cutoffs["maxSoC"] - chargeTaperStartTh)
requiredCurrent *= scaling_factor
  • Purpose: Implements charge tapering as the battery approaches maximum SoC to prevent overcharging.
  • Variables:
    • chargeTaperStartTh: SoC threshold to start tapering the charge.
    • simulation_cutoffs: Dictionary containing simulation limits like maxSoC.
  • Explanation:
    • The scaling_factor reduces the charging current linearly as SoC approaches maxSoC.

9. Safety Checks

minCelltemp = min(el.temperature for el in analysedElements)
maxCelltemp = max(el.temperature for el in analysedElements)

debug_print(DEBUG, f"Max Temperature: {maxCelltemp}°C")
  • Purpose: Checks the minimum and maximum cell temperatures for safety.
  • Action: Prints the maximum temperature for debugging.

10. State Machine for Battery Management

state, current = batteryStateMachine(
state, minVoltage, maxCelltemp, current, requiredCurrent,
temperatureSafety, temperatureRecovery, simulation_cutoffs['maxCurrent'],
lowVoltageSafetyCutoff, t_profile
)
  • Purpose: Manages the battery state (e.g., Normal, Overheating, Overcurrent) and adjusts the current accordingly.
  • Functions Used:
    • batteryStateMachine: A function that updates the state of the battery and possibly modifies the current based on safety thresholds.
  • Variables:
    • state: Current state of the battery.
    • temperatureSafety: Temperature threshold for safety cut-off.
    • temperatureRecovery: Temperature at which battery can resume normal operation.
    • lowVoltageSafetyCutoff: Minimum voltage threshold.

11. Updating Time Step

dt_s, t, prev_dt_s = update_time_step(
batteryCapacity, current, prev_dt_s, t,
min_dt_s=minimum_dt_s, maximum_dt_s=maximum_dt_s,
dt_to_next_profile_input=dt_to_next_profile_input,
sampling_multiplier=1.0, sampling_relaxation_factor=1.2,
debug=DEBUG
)
  • Purpose: Dynamically adjusts the simulation time step dt_s for accuracy and performance.
  • Functions Used:
    • update_time_step: Adjusts dt_s based on current conditions to ensure numerical stability.
  • Variables:
    • batteryCapacity: Battery capacity in Ampere-hours.
    • prev_dt_s: Previous time step.
    • t: Current simulation time.
    • dt_to_next_profile_input: Time until the next profile input.

12. Setting Battery Current

battery.setCurrent(current, SIM_TARGETED_DEPTH)
  • Purpose: Updates the battery's current at the specified simulation depth.
  • Variables:
    • SIM_TARGETED_DEPTH: The depth in the battery hierarchy (e.g., cell level, module level) where the current is applied.

13. Cooling Logic

if maxCelltemp <= startCoolingTemperature:
forcedConvectionMultiplier = restingForcedConvectionMultiplier
elif maxCelltemp >= maxCoolingTemperature:
forcedConvectionMultiplier = coolingForcedConvectionMultiplier
else:
scaling_factor = ((maxCelltemp - startCoolingTemperature) /
(maxCoolingTemperature - startCoolingTemperature))
forcedConvectionMultiplier = 1 + ((coolingForcedConvectionMultiplier - 1) * scaling_factor)

fanRPM = (forcedConvectionMultiplier - 1) * (maxFanRPM / (coolingForcedConvectionMultiplier - 1))
  • Purpose: Adjusts the cooling system based on the maximum cell temperature.
  • Variables:
    • startCoolingTemperature: Temperature to start increasing cooling.
    • maxCoolingTemperature: Temperature at which maximum cooling is applied.
    • restingForcedConvectionMultiplier: Cooling multiplier when the system is at rest.
    • coolingForcedConvectionMultiplier: Maximum cooling multiplier.
    • maxFanRPM: Maximum fan RPM.

14. Updating Battery Elements

for el in analysedElements:
el.calculateNextStep(dt_s, ambient_temperature)
  • Purpose: Updates each battery element (e.g., cells) for the next time step.
  • Functions Used:
    • calculateNextStep: Updates the state of an element based on the time step and ambient conditions.
  • Variables:
    • dt_s: Time step in seconds.
    • ambient_temperature: Ambient temperature in degrees Celsius.

15. Updating Battery Voltage and SoC

battery.getVoltage(SIM_TARGETED_DEPTH)
power = battery.voltage * battery.current
battery.soc = min(el.soc for el in analysedElements)
  • Purpose:
    • getVoltage: Retrieves the battery voltage at the specified depth.
    • power: Calculates the power output/input.
    • battery.soc: Updates the battery's SoC to the minimum SoC among all elements to ensure safety.

16. Timestamp for Sensor Data

tDateUTCTimestamp = datetime.fromtimestamp(t, timezone.utc).timestamp()
  • Purpose: Converts the simulation time t to a UTC timestamp for recording sensor data.

17. Updating Sensors

digitalTwinBatterySensors["SoC"].significantAppend(float(battery.soc) * 100, tDateUTCTimestamp)
digitalTwinBatterySensors["Temperature Stack Aggregate|0"].significantAppend(minTemperature, tDateUTCTimestamp)
digitalTwinBatterySensors["Temperature Stack Aggregate|1"].significantAppend(maxTemperature, tDateUTCTimestamp)
digitalTwinBatterySensors["Temperature Stack Aggregate|2"].significantAppend(averageTemperature, tDateUTCTimestamp)
digitalTwinBatterySensors["Voltage"].significantAppend(battery.voltage, tDateUTCTimestamp)
digitalTwinBatterySensors["Current"].significantAppend(battery.current, tDateUTCTimestamp)
digitalTwinBatterySensors["Power Demand"].significantAppend(required_battery_power, tDateUTCTimestamp)
digitalTwinBatterySensors["Power"].significantAppend(power, tDateUTCTimestamp)
digitalTwinBatterySensors["Ambient Temperature"].significantAppend(ambient_temperature, tDateUTCTimestamp)
  • Purpose: Records the simulation data into the digitalTwinBatterySensors for analysis and plotting.
  • Functions Used:
    • significantAppend: Appends data to the sensor if it's significantly different from the last recorded value to reduce data size.

Finalizing the DataFrame

After the simulation loop, we process and save the collected data.

# Finalize DataFrame
dtDf = dataframeFromSensors(digitalTwinBatterySensors).ffill()
dtDf.index = pd.to_datetime(dtDf.index, unit='s', utc=True)
dtDf.to_csv('battery_simulation.csv')

# Check for duplicate indices
duplicate_count = dtDf.index.duplicated().sum()
print(f"Duplicate index count: {duplicate_count}")

plot_data(dtDf, title="Battery Simulation")
  • Purpose:
    • Converts the sensor data into a pandas DataFrame.
    • Forward-fills any missing data to ensure continuity.
    • Saves the DataFrame to a CSV file.
    • Checks for any duplicate timestamps (indices).
    • Plots the data for visualization.

Understanding the Battery Class

In our battery simulation, the Battery class is a central component that represents the entire battery system. It inherits from the Assembly class and encapsulates multiple layers of hierarchy, including stacks, modules, and cells. This section delves into the Battery class, explaining its structure, methods, and how it integrates various components to simulate battery behavior accurately.

The Role of the Battery Class

The Battery class models the complete battery assembly, consisting of:

  • Stacks: Groups of modules connected in series or parallel.
  • Modules: Groups of cells connected in series or parallel.
  • Cells: The fundamental electrochemical units of the battery.

By organizing these components hierarchically, the Battery class allows us to simulate complex battery architectures and analyze their electrical and thermal behaviors under different conditions.

Structure of the Battery Class

Here's the definition of the Battery class:

class Battery(Assembly):
def __init__(self):
Assembly.__init__(self, elements=[], positions=[])
self.smu = Smu(self)

## Cooling parameters
self.forcedConvectionMultiplier = 1.0
self.restingCoolingFactorMultiplier = 1.0

## Noise parameters for simulation variability
self.cellCapNoise = 0.0
self.cellImpNoise = 0.0
self.cellSocNoise = 0.0

## Battery architecture parameters
self.cellP = None
self.cellS = None
self.modP = None
self.modS = None
self.stackP = None
self.stackS = None

## Generate module and stack templates
self.genModule = Module()
self.genStack = Stack()

self.totalCellsInParallel = 0
self.totalCellsInSeries = 0

self.pdu = Pdu(self)

Key Attributes

  • elements: A list that holds the stacks in the battery.
  • positions: Specifies the positions of the stacks.
  • forcedConvectionMultiplier: Adjusts the cooling based on external factors.
  • cellCapNoise, cellImpNoise, cellSocNoise: Introduce variability in capacity, impedance, and State of Charge (SoC) among cells to simulate real-world inconsistencies.
  • Architecture Parameters:
    • cellP, cellS: Number of cells in parallel and series within a module.
    • modP, modS: Number of modules in parallel and series within a stack.
    • stackP, stackS: Number of stacks in parallel and series within the battery.

Supporting Components

  • Smu: Stack Management Unit responsible for managing the stacks.
  • Pdu: Power Distribution Unit that manages the power flow within the battery.

Building the Battery Architecture

The build method constructs the battery's architecture based on specified parameters:

def build(self, cell: Cell, params, batteryParams=None):
## Extract architecture parameters
self.cellP = params["architecture"]["cellsParallel"]
self.cellS = params["architecture"]["cellsSeries"]
self.modP = params["architecture"]["modulesParallel"]
self.modS = params["architecture"]["modulesSeries"]
self.stackP = params["architecture"]["stacksParallel"]
self.stackS = params["architecture"]["stacksSeries"]

## Set cooling parameters if provided
if batteryParams is not None:
if 'forcedConvectionMultiplier' in batteryParams:
self.forcedConvectionMultiplier = batteryParams['forcedConvectionMultiplier']
if 'restingConvectionMultiplier' in batteryParams:
self.restingCoolingFactorMultiplier = batteryParams['restingConvectionMultiplier']

## Calculate total cells in parallel and series
self.totalCellsInParallel = self.cellP * self.modP * self.stackP
self.totalCellsInSeries = self.modS * self.stackS * self.cellS

## Build the modules, stacks, and assemble the battery
## ...

Steps in Building the Battery

  1. Set Architecture Parameters: Define how many cells, modules, and stacks are connected in series and parallel.
  2. Configure Cooling Parameters: Adjust the cooling behavior if additional parameters are provided.
  3. Calculate Total Cells:
    • Total Cells in Parallel: cellP×modP×stackP\text{cellP} \times \text{modP} \times \text{stackP}
    • Total Cells in Series: cellS×modS×stackS\text{cellS} \times \text{modS} \times \text{stackS}
  4. Create Module Templates: Build prototype modules containing cells.
  5. Create Stack Templates: Assemble modules into prototype stacks.
  6. Assemble the Battery: Combine stacks to form the complete battery.

Example Architecture Calculation

Suppose we have the following parameters:

  • Cells in Series (cellS): 10
  • Cells in Parallel (cellP): 5
  • Modules in Series (modS): 4
  • Modules in Parallel (modP): 2
  • Stacks in Series (stackS): 1
  • Stacks in Parallel (stackP): 1

Total Cells in Series:

Total Cells in Series=cellS×modS×stackS=10×4×1=40\text{Total Cells in Series} = \text{cellS} \times \text{modS} \times \text{stackS} = 10 \times 4 \times 1 = 40

Total Cells in Parallel:

Total Cells in Parallel=cellP×modP×stackP=5×2×1=10\text{Total Cells in Parallel} = \text{cellP} \times \text{modP} \times \text{stackP} = 5 \times 2 \times 1 = 10

Total Number of Cells:

Total Cells=Total Cells in Series×Total Cells in Parallel=40×10=400\text{Total Cells} = \text{Total Cells in Series} \times \text{Total Cells in Parallel} = 40 \times 10 = 400

Initializing the Battery

After building the battery architecture, we need to initialize the state of each cell:

def initialize(self, initSoC=1, initSoH=1, initTamb=25.0, targetedDepth=LEVEL_STACK):
## Normalize input parameters
self.soc = normalize_value(initSoC, threshold=1.0, scale=100.0, normalize=True, clip_min=0, clip_max=100)
self.soh.value = normalize_value(initSoH, threshold=1.0, scale=100.0, normalize=True, clip_min=0, clip_max=100)

## Get all elements at the targeted depth (e.g., cells)
analysedElements = []
self.getElementsByDepth(analysedElements, targetedDepth)

## Initialize each element
for element in analysedElements:
element.soc = self.soc
element.temperature = initTamb
element.calculateNextStep(1, initTamb)

Initialization Steps

  1. Normalize Initial Conditions:
    • State of Charge (SoC): Ensure it is between 0% and 100%.
    • State of Health (SoH): Ensure it is between 0% and 100%.
  2. Select Elements for Initialization:
    • Use getElementsByDepth to retrieve all elements at the desired level (e.g., cells).
  3. Initialize Each Element:
    • Set the initial SoC and temperature.
    • Call calculateNextStep to update the internal state based on the initial conditions.

Normalization Function

The normalize_value function ensures that input values are within acceptable ranges:

def normalize_value(value, threshold=1.0, scale=100.0, normalize=True, clip_min=0, clip_max=100):
if normalize:
value = value / scale
value = max(min(value, clip_max / scale), clip_min / scale)
return value

Calculating Battery Voltage and Capacity

The battery voltage and capacity depend on how cells are connected:

Total Voltage

Cells connected in series increase the total voltage:

Vtotal=Vcell×NseriesV_{\text{total}} = V_{\text{cell}} \times N_{\text{series}}

Where:

  • VcellV_{\text{cell}} is the nominal voltage of a single cell.
  • NseriesN_{\text{series}} is the total number of cells connected in series.

Total Capacity

Cells connected in parallel increase the total capacity:

Ctotal=Ccell×NparallelC_{\text{total}} = C_{\text{cell}} \times N_{\text{parallel}}

Where:

  • CcellC_{\text{cell}} is the capacity of a single cell (in Ampere-hours).
  • NparallelN_{\text{parallel}} is the total number of cells connected in parallel.

Example Calculation

Using the previous architecture with:

  • Nseries=40N_{\text{series}} = 40
  • Nparallel=10N_{\text{parallel}} = 10
  • Vcell=3.6VV_{\text{cell}} = 3.6 \, \text{V}
  • Ccell=3.2AhC_{\text{cell}} = 3.2 \, \text{Ah}

Total Voltage:

Vtotal=3.6V×40=144VV_{\text{total}} = 3.6 \, \text{V} \times 40 = 144 \, \text{V}

Total Capacity:

Ctotal=3.2Ah×10=32AhC_{\text{total}} = 3.2 \, \text{Ah} \times 10 = 32 \, \text{Ah}

Thermal Management in the Battery Class

The battery's thermal behavior is critical for safety and performance. The Battery class manages thermal properties and cooling mechanisms.

Heat Generation

Heat generated in a cell due to internal resistance is calculated as:

Qloss=I2×RinternalQ_{\text{loss}} = I^2 \times R_{\text{internal}}

Where:

  • QlossQ_{\text{loss}} is the power loss due to heat (in Watts).
  • II is the current through the cell (in Amperes).
  • RinternalR_{\text{internal}} is the internal resistance of the cell (in Ohms).

Temperature Update

The temperature change in a cell is computed using the heat capacity and thermal resistance:

ΔT=Qloss×Δthconv×Asurface×(TcellTambient)×Δtm×cp\Delta T = \frac{Q_{\text{loss}} \times \Delta t - h_{\text{conv}} \times A_{\text{surface}} \times (T_{\text{cell}} - T_{\text{ambient}}) \times \Delta t}{m \times c_p}

Where:

  • ΔT\Delta T is the change in temperature (in Kelvin).
  • Δt\Delta t is the time step (in seconds).
  • hconvh_{\text{conv}} is the convective heat transfer coefficient.
  • AsurfaceA_{\text{surface}} is the surface area of the cell (in square meters).
  • TcellT_{\text{cell}} is the current temperature of the cell.
  • TambientT_{\text{ambient}} is the ambient temperature.
  • mm is the mass of the cell (in kilograms).
  • cpc_p is the specific heat capacity of the cell material (in J/kg·K).

Cooling Multipliers

The forcedConvectionMultiplier adjusts the cooling based on operating conditions:

  • Resting State: Minimal cooling is applied.
  • Active Cooling: When temperatures rise, the cooling multiplier increases to dissipate more heat.

Example Temperature Update

Assuming:

  • Qloss=5WQ_{\text{loss}} = 5 \, \text{W}
  • Δt=1s\Delta t = 1 \, \text{s}
  • hconv=10W/m2Kh_{\text{conv}} = 10 \, \text{W/m}^2\text{K}
  • Asurface=0.025m2A_{\text{surface}} = 0.025 \, \text{m}^2
  • Tcell=35°CT_{\text{cell}} = 35 \, \text{°C}
  • Tambient=25°CT_{\text{ambient}} = 25 \, \text{°C}
  • m=0.06kgm = 0.06 \, \text{kg}
  • cp=800J/kg\cdotpKc_p = 800 \, \text{J/kg·K}

Calculate ΔT\Delta T:

First, compute the cooling term:

Qcooling=hconv×Asurface×(TcellTambient)=10×0.025×(3525)=2.5WQ_{\text{cooling}} = h_{\text{conv}} \times A_{\text{surface}} \times (T_{\text{cell}} - T_{\text{ambient}}) = 10 \times 0.025 \times (35 - 25) = 2.5 \, \text{W}

Net heat:

Qnet=QlossQcooling=52.5=2.5WQ_{\text{net}} = Q_{\text{loss}} - Q_{\text{cooling}} = 5 - 2.5 = 2.5 \, \text{W}

Temperature change:

ΔT=Qnet×Δtm×cp=2.5×10.06×8000.052K\Delta T = \frac{Q_{\text{net}} \times \Delta t}{m \times c_p} = \frac{2.5 \times 1}{0.06 \times 800} \approx 0.052 \, \text{K}

So, the cell temperature increases by approximately 0.052K0.052 \, \text{K} during this time step.

Managing State of Charge (SoC) and State of Health (SoH)

State of Charge Update

The SoC decreases or increases based on the current flow:

ΔSoC=I×ΔtCcell×3600\Delta \text{SoC} = \frac{I \times \Delta t}{C_{\text{cell}} \times 3600}

  • Discharging: I>0I > 0
  • Charging: I<0I < 0

State of Health Update

The SoH degrades over time due to:

  • Calendar Aging: Degradation over time regardless of use.
  • Cycle Aging: Degradation due to charge-discharge cycles.
Calendar Aging Model

The capacity loss due to calendar aging can be modeled as:

Qloss, cal=kcal×α(SOC,T)×tQ_{\text{loss, cal}} = k_{\text{cal}} \times \alpha(SOC, T) \times \sqrt{t}

Where:

  • kcalk_{\text{cal}} is a calendar aging rate constant.
  • α(SOC,T)\alpha(SOC, T) is a factor depending on SoC and temperature.
  • tt is time in days.
Cycle Aging Model

Capacity loss per equivalent full cycle:

Qloss, cycle=1SoHEOLNcycleQ_{\text{loss, cycle}} = \frac{1 - \text{SoH}_{\text{EOL}}}{N_{\text{cycle}}}

Where:

  • SoHEOL\text{SoH}_{\text{EOL}} is the end-of-life SoH threshold (e.g., 80%).
  • NcycleN_{\text{cycle}} is the standard cycle life (e.g., 2000 cycles).

Total Capacity Loss

Total capacity loss combines calendar and cycle aging:

Qloss, total=Qloss, cal+Qloss, cycleQ_{\text{loss, total}} = Q_{\text{loss, cal}} + Q_{\text{loss, cycle}}

Updating SoH

The SoH is updated by subtracting the total capacity loss:

SoHnew=SoHoldQloss, total\text{SoH}_{\text{new}} = \text{SoH}_{\text{old}} - Q_{\text{loss, total}}

Voltage Calculation with Internal Resistance

The voltage of the battery is affected by internal resistance and can be calculated as:

Vbattery=VOCVI×RtotalV_{\text{battery}} = V_{\text{OCV}} - I \times R_{\text{total}}

Where:

  • VOCVV_{\text{OCV}} is the open-circuit voltage, a function of SoC and temperature.
  • II is the current (positive for discharging, negative for charging).
  • RtotalR_{\text{total}} is the total internal resistance of the battery.

Internal Resistance

The total internal resistance depends on:

  • Cell Resistance: Varies with SoC, temperature, and SoH.
  • Connections: Additional resistance from interconnections between cells, modules, and stacks.

For cells connected in series:

Rseries=i=1NseriesRcell,iR_{\text{series}} = \sum_{i=1}^{N_{\text{series}}} R_{\text{cell}, i}

For cells connected in parallel (assuming identical cells):

Rparallel=RcellNparallelR_{\text{parallel}} = \frac{R_{\text{cell}}}{N_{\text{parallel}}}

Total internal resistance:

Rtotal=Rseries+RparallelR_{\text{total}} = R_{\text{series}} + R_{\text{parallel}}

The calculateNextStep Method

In this subsection, we delve into the calculateNextStep method of the ElectroChemEntity class. This method is pivotal in updating the state of each battery element (cells, modules, stacks) during each simulation time step. It models both the electrical and thermal behaviors, accounting for factors like current flow, temperature changes, internal resistance, and state of charge (SoC).

Overview of calculateNextStep

The calculateNextStep method performs the following key functions:

  1. Recalculates Open Circuit Voltage (OCV) based on the current SoC and temperature.
  2. Updates Resistance Factors that depend on SoC, temperature, and State of Health (SoH).
  3. Calculates Power Loss due to internal resistance.
  4. Updates Temperature of the cell using thermal equations.
  5. Updates Voltage Components in the RC circuit model.
  6. Updates State of Charge (SoC) based on the current and capacity.
  7. Updates OCV Hysteresis to model voltage hysteresis effects.
  8. Accumulates Equivalent Cycles to model battery aging.

Let's explore each of these steps in detail.

1. Recalculating Open Circuit Voltage (OCV)

The OCV of a battery cell is a function of its SoC and temperature. Recalculating OCV is crucial because it serves as the reference voltage from which we compute other voltage drops due to internal resistances and hysteresis.

OCV Calculation

The OCV is calculated using bilinear interpolation from the OCV lookup tables:

VOCV=(1H)×VOCV, Discharge(SoC,T)+H×VOCV, Charge(SoC,T)V_{\text{OCV}} = (1 - H) \times V_{\text{OCV, Discharge}}(\text{SoC}, T) + H \times V_{\text{OCV, Charge}}(\text{SoC}, T)

Where:

  • VOCV, DischargeV_{\text{OCV, Discharge}} is the discharge OCV from the lookup table.
  • VOCV, ChargeV_{\text{OCV, Charge}} is the charge OCV from the lookup table.
  • HH is the hysteresis factor (ranges from 0 to 1).

This calculation accounts for the hysteresis effect, where the OCV differs during charging and discharging.

2. Updating Resistance Factors

The internal resistance of a battery changes with SoC, temperature, and SoH. These factors are represented as multipliers that adjust the base resistance to reflect current operating conditions.

SoC-Dependent Resistance Factor

The SoC resistance factor is calculated using linear interpolation:

RSoC=(1H)×RSoC, Discharge(SoC)+H×RSoC, Charge(SoC)\text{R}_{\text{SoC}} = (1 - H) \times \text{R}_{\text{SoC, Discharge}}(\text{SoC}) + H \times \text{R}_{\text{SoC, Charge}}(\text{SoC})

Temperature-Dependent Resistance Factor

The temperature resistance factor is:

RTemp=interpolate(T,Temperature Range,R Factor Range)\text{R}_{\text{Temp}} = \text{interpolate}(T, \text{Temperature Range}, \text{R Factor Range})

SoH-Dependent Resistance Factor

The SoH resistance factor accounts for degradation:

RSoH=interpolate(SoH,SoH Range,R Factor Range)\text{R}_{\text{SoH}} = \text{interpolate}(\text{SoH}, \text{SoH Range}, \text{R Factor Range})

Total Resistance Factor

The total resistance factor is the product of the individual factors:

RTotal=RSoC×RTemp×RSoH\text{R}_{\text{Total}} = \text{R}_{\text{SoC}} \times \text{R}_{\text{Temp}} \times \text{R}_{\text{SoH}}

3. Calculating Power Loss

Power loss due to internal resistance is calculated as:

PLoss=VOCVV×IP_{\text{Loss}} = |V_{\text{OCV}} - V| \times |I|

Alternatively, since V=VOCVI×RTotalV = V_{\text{OCV}} - I \times R_{\text{Total}}:

PLoss=I2×RTotalP_{\text{Loss}} = I^2 \times R_{\text{Total}}

4. Updating Temperature

The temperature update considers the heat generated by power loss and the heat dissipated through convection:

ΔT=1m×cp(PLosshconv×Asurface×FCM×(TTamb))×Δt\Delta T = \frac{1}{m \times c_p} \left( P_{\text{Loss}} - h_{\text{conv}} \times A_{\text{surface}} \times \text{FCM} \times (T - T_{\text{amb}}) \right) \times \Delta t

Where:

  • mm is the mass of the cell.
  • cpc_p is the specific heat capacity.
  • hconvh_{\text{conv}} is the convective heat transfer coefficient.
  • AsurfaceA_{\text{surface}} is the surface area.
  • FCM\text{FCM} is the forced convection multiplier.
  • Δt\Delta t is the time step.

The new temperature:

Tnew=Told+ΔTT_{\text{new}} = T_{\text{old}} + \Delta T

5. Updating Voltage Components

The battery's transient response is modeled using RC circuits. Each RC element is updated as:

Vi=Vi,old×eΔtRiCi+(1eΔtRiCi)×I×Ri×RTotalV_i = V_{i, \text{old}} \times e^{- \frac{\Delta t}{R_i C_i}} + \left( 1 - e^{- \frac{\Delta t}{R_i C_i}} \right) \times I \times R_i \times \text{R}_{\text{Total}}

Where:

  • ViV_i is the voltage across the ii-th RC element.
  • RiR_i and CiC_i are the resistance and capacitance of the ii-th RC element.

The terminal voltage:

V=VOCVViI×RSeries×RTotalV = V_{\text{OCV}} - \sum V_i - I \times R_{\text{Series}} \times \text{R}_{\text{Total}}

6. Updating State of Charge (SoC)

SoC changes based on the current:

ΔSoC=I×ΔtCcell×3600\Delta \text{SoC} = \frac{I \times \Delta t}{C_{\text{cell}} \times 3600}

Updated SoC:

SoCnew=SoCold+ΔSoC\text{SoC}_{\text{new}} = \text{SoC}_{\text{old}} + \Delta \text{SoC}

7. Updating OCV Hysteresis

The OCV hysteresis factor HH models the difference between charge and discharge OCV curves. It's updated based on the change in SoC:

τSoC=eΔSoCkHyst\tau_{\text{SoC}} = e^{- \frac{|\Delta \text{SoC}|}{k_{\text{Hyst}}}}

Where kHystk_{\text{Hyst}} is a hysteresis constant.

Updating HH:

  • For discharging (I>0I > 0):

    Hnew=(1τSoC)×0+τSoC×HoldH_{\text{new}} = (1 - \tau_{\text{SoC}}) \times 0 + \tau_{\text{SoC}} \times H_{\text{old}}

  • For charging (I<0I < 0):

    Hnew=(1τSoC)×1+τSoC×HoldH_{\text{new}} = (1 - \tau_{\text{SoC}}) \times 1 + \tau_{\text{SoC}} \times H_{\text{old}}

8. Accumulating Equivalent Cycles

Equivalent cycles are a measure of battery aging:

EqCyclesnew=EqCyclesold+ΔSoC2\text{EqCycles}_{\text{new}} = \text{EqCycles}_{\text{old}} + \frac{|\Delta \text{SoC}|}{2}

Detailed Algorithm

Here's the step-by-step process:

  1. Check for OCV Recalculation: If significant changes in SoC or temperature occurred.
  2. Calculate OCV: Using the current SoC, temperature, and hysteresis factor.
  3. Update Resistance Factors: For SoC, temperature, and SoH.
  4. Compute Total Resistance Factor: Multiply individual resistance factors.
  5. Calculate Power Loss: Using the total resistance and current.
  6. Update Temperature: Apply the thermal model with convection cooling.
  7. Update Voltage Components: Update RC circuit voltages.
  8. Compute Terminal Voltage: Sum OCV and voltage drops.
  9. Update SoC: Based on the current and capacity.
  10. Update OCV Hysteresis: Adjust hysteresis factor for charging/discharging.
  11. Accumulate Equivalent Cycles: For aging calculation.
  12. Update Previous States: Store current SoC, temperature, and SoH.

Code Implementation

def calculateNextStep(self, stepTime=0.0, Tamb=25.0):
## 1. Recalculate OCV if needed
if (
self.SoCaccumulator > self.SocOcvVariationth
or self.Taccumulator > self.temperatureVariationth
or self.SoCaccumulator == -1
):
self.ocVoltage = self.calculateOCV(self.soc, self.temperature)
self.ocVoltagef = (
self.ocVoltage * self.ocVoltageData.smoothing
+ (1 - self.ocVoltageData.smoothing) * self.ocVoltagef
)

## 2. Update resistance factors
if self.SoCaccumulator > self.SocOcvVariationth or self.SoCaccumulator == -1:
self.socRfactor = self.calculateSocRfactor()
self.SoCaccumulator = 0.0

if self.Taccumulator > self.temperatureVariationth:
self.tempRfactor = self.calculateTempRfactor()
self.Taccumulator = 0.0

if self.SoHaccumulator > self.soHVariationTh:
self.sohRfactor = self.calculateSohRfactor()
self.SoHaccumulator = 0

rFactor = self.socRfactor * self.tempRfactor * self.sohRfactor

## 3. Calculate power loss
self.powerLoss = abs(self.ocVoltage - self.voltage) * abs(self.current)

## 4. Update temperature
dT = (stepTime / (self.physics.mass * self.thermals.C) * (
self.powerLoss - self.physics.surface * self.thermals.convH *
self.forcedConvectionMultiplier * (self.temperature - Tamb)))
self.temperature += dT
self.coreTemperature = self.temperature

## 5. Update voltage components
self.updateVoltageComponents(stepTime, rFactor)

## 6. Update SoC
dSOC = self.current * stepTime / (self.capacity.init * 3600.0 * self.soh.value)
self.soc += dSOC

## 7. Update OCV hysteresis
self.updateOcvHysteresis(dSOC)

## 8. Accumulate equivalent cycles
self.eqCycles += abs(dSOC / 2.0)

## Update accumulators
self.SoCaccumulator += abs(self.soc - self.prevSoc)
self.Taccumulator += abs(self.temperature - self.prevTemperature)
self.SoHaccumulator += abs(self.soh.value - self.prevSoH)

## Store previous states
self.prevSoc = self.soc
self.prevTemperature = self.temperature
self.prevSoH = self.soh.value

Functions Within calculateNextStep

  • calculateOCV: Computes the OCV based on SoC and temperature.
  • calculateSocRfactor: Calculates the resistance factor due to SoC.
  • calculateTempRfactor: Calculates the resistance factor due to temperature.
  • calculateSohRfactor: Calculates the resistance factor due to SoH.
  • updateVoltageComponents: Updates the RC circuit voltages.
  • updateOcvHysteresis: Updates the hysteresis factor based on SoC change.

Importance of calculateNextStep

  • Dynamic Modeling: Accurately reflects changes in battery behavior over time.
  • Thermal Management: Simulates temperature effects, crucial for safety.
  • Performance Prediction: Helps in predicting how the battery will perform under different loads and temperatures.
  • Aging Simulation: Incorporates mechanisms to simulate capacity fade and resistance increase over time.

By thoroughly understanding the calculateNextStep method, we can adjust the battery model to more closely match specific battery chemistries and configurations, leading to more accurate simulations and better battery designs.

Conclusion

This simulation models the complex interactions within a battery system, considering electrical, thermal, and aging effects. By stepping through the code, we can see how each component contributes to the overall behavior of the battery over time.

Understanding this code allows us to tweak parameters, improve the model, and predict how a real battery system might perform under various conditions. This is invaluable for designing batteries that are efficient, safe, and long-lasting.


Feel free to refer back to this tutorial as you experiment with the simulation or adapt it for different battery configurations and use cases.