Skip to main content

Battery Simulation Tutorial

This tutorial will guide you through the process of creating a battery simulation script. We will start by understanding the battery model, then delve into the functions used in the script, and finally, we will walk through the script step by step.

Table of Contents


Introduction

Battery simulation is crucial for understanding battery behavior under various conditions without the need for physical prototypes. This tutorial will help you create a script that simulates a battery using a detailed battery model, power demand profiles, and temperature profiles.


Understanding the Battery Model

Before diving into the script, it's essential to understand the battery model we're using. The battery model is defined in a JSON format and contains detailed parameters that characterize the battery's behavior.

Key Components of the Battery Model:

  • Cell Specifications: Defines the individual cell's characteristics, such as voltage, capacity, chemistry, thermal properties, and resistance components.

  • Open Circuit Voltage (OCV) Curves: Provides the OCV values for different states of charge (SoC) and temperatures during charging and discharging.

  • Resistance Factors: Includes factors that affect the cell's resistance based on SoC, temperature, and state of health (SoH).

  • Aging Parameters: Defines how the battery's capacity degrades over time due to calendar aging and cyclic aging.

  • Architecture: Specifies how cells are arranged in series and parallel configurations within modules, stacks, and the overall battery.

Example: Cell Specifications

{
"cell": {
"name": "EP LFP Cell",
"cellChemistry": "LFP",
"voltage": 3.22,
"cellCapacity": 64.05,
"cycleLife": 2000.0,
"mass": 0.001,
"surface": 0.0225,
"specificHeat": 300.0,
"weight": 1.0,
"convectionSurf": 0.0475,
"convectionH": 1,
"ocvCharge": [
[
3.126,
3.126,
3.126,
3.126,
3.126,
3.126
],
[
3.138,
3.138,
3.138,
3.138,
3.138,
3.138
],
[
3.169,
3.169,
3.169,
3.169,
3.169,
3.169
],
[
3.2,
3.2,
3.2,
3.2,
3.2,
3.2
],
[
3.21,
3.21,
3.21,
3.21,
3.21,
3.21
],
[
3.217,
3.217,
3.217,
3.217,
3.217,
3.217
],
[
3.223,
3.223,
3.223,
3.223,
3.223,
3.223
],
[
3.228,
3.228,
3.228,
3.228,
3.228,
3.228
],
[
3.232,
3.232,
3.232,
3.232,
3.232,
3.232
],
[
3.236,
3.236,
3.236,
3.236,
3.236,
3.236
],
"...",
],
"ocvDischarge": [
[
3.086,
3.086,
3.086,
3.086,
3.086,
3.086
],
[
3.097,
3.097,
3.097,
3.097,
3.097,
3.097
],
[
3.125,
3.125,
3.125,
3.125,
3.125,
3.125
],
[
3.154,
3.154,
3.154,
3.154,
3.154,
3.154
],
[
3.167,
3.167,
3.167,
3.167,
3.167,
3.167
],
[
3.178,
3.178,
3.178,
3.178,
3.178,
3.178
],
[
3.189,
3.189,
3.189,
3.189,
3.189,
3.189
],
[
3.194,
3.194,
3.194,
3.194,
3.194,
3.194
],
[
3.196,
3.196,
3.196,
3.196,
3.196,
3.196
],
[
3.199,
3.199,
3.199,
3.199,
3.199,
3.199
],
"...",

],
"temperatureRange": [
5,
15,
25,
35,
45,
55
],
"socRange": [
0.0,
0.01,
0.02,
0.03,
0.04,
0.05,
0.06,
0.07,
0.08,
0.09,
0.1,
"..."
],
"soh": {
"endOfLifeSOH": 0.8,
"CalendarAgingParams": {
"LTableCalSOC": {
"name": "LTableCalSOC",
"unit": "%",
"index": [
0,
1
],
"factor": [
0,
1
],
"kCalSOC": 0.0001
},
"Tmax": 60
},
"CyclicAgingParams": {
"stdCycleLife": 2000
}
},
"rComponent": [
0.00046272,
0.0005904,
0.000125136
],
"rSocFactor": {
"soc": [
1.0,
0.9,
0.8,
0.7,
0.6,
0.5,
0.4,
0.3,
0.2,
0.15,
0.1,
0.05,
0.01,
0.0
],
"rFactorDischarge": [
0.88,
0.89,
0.925,
0.964225,
0.992394,
1,
1.207,
1.55,
1.9,
2.05,
2.02,
2.4,
2.8,
2.8
],
"rFactorCharge": [
2.1,
1.49,
1.22,
1.1,
1.16,
1.21,
1.18,
1.0,
0.4,
0.05,
0.001,
0.003,
0.03,
0.03
]
},
"rTempFactor": {
"temp": [
0,
25,
40,
55,
80
],
"rFactor": [
1,
1.0,
1,
1,
1
]
},
"rSohFactor": {
"soh": [
1.0,
0.8,
0.6,
0.4
],
"rFactor": [
1.0,
1.38,
1.83,
2.3
]
},
"cComponent": [
0.0,
158350.5354,
62834.88
]
},
"battery": {
"name": "EP Battery",
"forcedConvectionMultiplier": 20.0
},
"architecture": {
"name": "EP Battery",
"cellsSeries": 1,
"cellsParallel": 1,
"modulesSeries": 1,
"modulesParallel": 1,
"stacksSeries": 24,
"stacksParallel": 1
},
"start": 0,
"length": 0,
"step": 1,
"serialNumber": "BAT-001"
}

Certainly! Below is a comprehensive explanation of your battery simulation model structured into multiple markdown tables. Each table corresponds to a specific section of your JSON configuration, detailing the parameters, their descriptions, and example values where applicable.


Cell Configuration

ParameterDescriptionValue/Example
nameName of the cell model."EP LFP Cell"
cellChemistryChemistry type of the cell."LFP"
voltageNominal voltage of the cell in volts (V).3.22
cellCapacityCapacity of the cell in ampere-hours (Ah).64.05
cycleLifeExpected number of charge-discharge cycles before end of life.2000.0
massMass of the cell in kilograms (kg).0.001
surfaceSurface area of the cell in square meters (m²).0.0225
specificHeatSpecific heat capacity of the cell material in J/(kg·K).300.0
weightWeight of the cell in kilograms (kg).1.0
convectionSurfSurface area available for convection cooling in square meters (m²).0.0475
convectionHConvective heat transfer coefficient.1
ocvChargeOpen Circuit Voltage (OCV) during charge at different states of charge (SOC).List of lists with voltage values (see example in JSON).
ocvDischargeOpen Circuit Voltage (OCV) during discharge at different states of charge (SOC).List of lists with voltage values (see example in JSON).
temperatureRangeTemperature range for simulation in degrees Celsius (°C).[5, 15, 25, 35, 45, 55]
socRangeState of Charge (SOC) range for simulation (0.0 to 1.0).[0.0, 0.01, 0.02, ..., 0.1, "..."]
sohState of Health (SOH) parameters, including aging models.See State of Health Configuration below.
rComponentInternal resistance components of the cell.[0.00046272, 0.0005904, 0.000125136]
rSocFactorResistance factors based on SOC for charge and discharge.See Resistance SOC Factor Configuration below.
rTempFactorResistance factors based on temperature.See Resistance Temperature Factor Configuration below.
rSohFactorResistance factors based on SOH.See Resistance SOH Factor Configuration below.
cComponentCapacitance components of the cell.[0.0, 158350.5354, 62834.88]

State of Health (SOH) Configuration

ParameterDescriptionValue/Example
endOfLifeSOHSOH threshold indicating end of life (e.g., 80% SOH).0.8
CalendarAgingParamsParameters related to calendar aging.See Calendar Aging Parameters below.
CyclicAgingParamsParameters related to cyclic aging.See Cyclic Aging Parameters below.

a. Calendar Aging Parameters

ParameterDescriptionValue/Example
LTableCalSOCLookup table for calendar aging based on SOC.See LTableCalSOC Parameters below.
TmaxMaximum temperature for calendar aging (°C).60
i. LTableCalSOC Parameters
ParameterDescriptionValue/Example
nameName of the SOC lookup table."LTableCalSOC"
unitUnit of measurement for SOC."%"
indexSOC indices for the lookup table.[0, 1]
factorAging factors corresponding to SOC indices.[0, 1]
kCalSOCCalendar aging rate constant based on SOC.0.0001

b. Cyclic Aging Parameters

ParameterDescriptionValue/Example
stdCycleLifeStandard cycle life (number of cycles).2000

Resistance Factors Configuration

a. Resistance SOC & Temperature Factor

ParameterDescriptionValue/Example
socRangeState of Charge (SOC) values used for resistance factor interpolation.[1.0, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.15, 0.1, 0.05, 0.01, 0.0]
temperaturerangeTemperature values used for the resistance factor interpolation[0.88, 0.89, 0.925, 0.964225, 0.992394, 1, 1.207, 1.55, 1.9, 2.05, 2.02, 2.4, 2.8, 2.8]
rFactorDischarge
State of Charge (SOC) IndexRFactor Values During Discharge
0[5.050408,2.525248,1.587784,0.88,0.784344,0.765248]
1[5.107799,2.553944,1.605827,0.89, 0.793257,0.773944]
2[5.308668,2.65438,1.668978,0.925, 0.824453,0.80438]
3[5.533784,2.76694,1.739751,0.964225,0.859414,0.83849]
......
rFactorCharge
State of Charge (SOC) IndexRFactor Values During Charge
0[12.05211,6.02616,3.78903,2.1,1.87173,1.82616]
1[8.551259,4.275704,2.688407,1.49,1.328037,1.295704]
2[6.31301,3.15656,1.98473,1.1,0.98043,0.95656]
3[6.657356,3.328736,2.092988,1.16,1.033908,1.008736]
......

c. Resistance SOH Factor

ParameterDescriptionValue/Example
sohState of Health (SOH) values used for resistance factor interpolation.[1.0, 0.8, 0.6, 0.4]
rFactorResistance factors at corresponding SOH values.[1.0, 1.38, 1.83, 2.3]

Battery Configuration

ParameterDescriptionValue/Example
nameName of the battery model."EP Battery"
forcedConvectionMultiplierMultiplier for forced convection cooling effectiveness.20.0

Architecture Configuration

ParameterDescriptionValue/Example
nameName of the battery architecture."EP Battery"
cellsSeriesNumber of cells connected in series within a module.1
cellsParallelNumber of cells connected in parallel within a module.1
modulesSeriesNumber of modules connected in series within a stack.1
modulesParallelNumber of modules connected in parallel within a stack.1
stacksSeriesNumber of stacks connected in series within the battery.24
stacksParallelNumber of stacks connected in parallel within the battery.1

Simulation Parameters

ParameterDescriptionValue/Example
startStart time of the simulation (relative).0
lengthTotal length/duration of the simulation.0
stepTime step increment for the simulation.1
serialNumberUnique identifier for the battery asset."BAT-001"

Additional Components

a. Resistance Components (rComponent)

IndexDescriptionValue
0First internal resistance component.0.00046272
1Second internal resistance component.0.0005904
2Third internal resistance component.0.000125136

b. Capacitance Components (cComponent)

IndexDescriptionValue
0First capacitance component.0.0
1Second capacitance component.158350.5354
2Third capacitance component.62834.88

Overview of OCV (Open Circuit Voltage) Profiles

a. OCV Charge (ocvCharge)

State of Charge (SOC) IndexOCV Values During Charge (V)
0[3.126, 3.126, 3.126, 3.126, 3.126, 3.126]
1[3.138, 3.138, 3.138, 3.138, 3.138, 3.138]
2[3.169, 3.169, 3.169, 3.169, 3.169, 3.169]
3[3.2, 3.2, 3.2, 3.2, 3.2, 3.2]
4[3.21, 3.21, 3.21, 3.21, 3.21, 3.21]
5[3.217, 3.217, 3.217, 3.217, 3.217, 3.217]
6[3.223, 3.223, 3.223, 3.223, 3.223, 3.223]
7[3.228, 3.228, 3.228, 3.228, 3.228, 3.228]
8[3.232, 3.232, 3.232, 3.232, 3.232, 3.232]
9[3.236, 3.236, 3.236, 3.236, 3.236, 3.236]
......

b. OCV Discharge (ocvDischarge)

State of Charge (SOC) IndexOCV Values During Discharge (V)
0[3.086, 3.086, 3.086, 3.086, 3.086, 3.086]
1[3.097, 3.097, 3.097, 3.097, 3.097, 3.097]
2[3.125, 3.125, 3.125, 3.125, 3.125, 3.125]
3[3.154, 3.154, 3.154, 3.154, 3.154, 3.154]
4[3.167, 3.167, 3.167, 3.167, 3.167, 3.167]
5[3.178, 3.178, 3.178, 3.178, 3.178, 3.178]
6[3.189, 3.189, 3.189, 3.189, 3.189, 3.189]
7[3.194, 3.194, 3.194, 3.194, 3.194, 3.194]
8[3.196, 3.196, 3.196, 3.196, 3.196, 3.196]
9[3.199, 3.199, 3.199, 3.199, 3.199, 3.199]
......

Note: The "..." indicates continuation of similar data patterns beyond the provided indices.


Summary

This model provides a comprehensive configuration for simulating the behavior of an LFP (Lithium Iron Phosphate) battery cell within a larger battery architecture. Key aspects include:

  • Cell Characteristics: Defines the physical and chemical properties of individual cells.
  • State of Health (SOH): Models the degradation of the battery over time through calendar and cyclic aging parameters.
  • Resistance and Capacitance Factors: Incorporates how SOC, temperature, and SOH impact the internal resistance and capacitance of the cells.
  • Battery Architecture: Specifies how cells, modules, and stacks are organized within the battery system.
  • Simulation Parameters: Controls the timing and progression of the simulation.

This detailed configuration ensures accurate and realistic simulation of battery performance, degradation, and thermal behavior under various operating conditions.



Key Functions Used in the Script

The script utilizes several key functions to manage the simulation's time steps, battery state transitions, data preprocessing, and value interpolation.

Time Step Update Function

def update_time_step(battery_capacity, current, prev_dt_s, t, min_dt_s=0.1, maximum_dt_s=60.0, dt_to_next_profile_input=0, sampling_multiplier=1.0, sampling_relaxation_factor=1.2, debug=False):
# Function body...

Purpose: Dynamically adjusts the simulation's time step (dt_s) based on the battery's capacity, current, and other parameters to ensure numerical stability and accuracy.

Parameters:

  • battery_capacity: The battery's capacity in Ampere-hours (Ah).
  • current: The current flowing through the battery in Amperes (A).
  • prev_dt_s: The previous time step in seconds.
  • t: The current simulation time.
  • min_dt_s: The minimum allowable time step.
  • maximum_dt_s: The maximum allowable time step.
  • dt_to_next_profile_input: Time until the next profile input.
  • sampling_multiplier and sampling_relaxation_factor: Factors to control the time step adjustments.

Returns: Updated time step, simulation time, and previous time step.

Battery State Machine

def batteryStateMachine(state, minVoltage, maxCelltemp, current, requiredCurrent, temperatureSafety, temperatureRecovery, maxCurrent, voltageCuttoff, tProfile):
# Function body...

Purpose: Manages the battery's operational state based on safety conditions such as voltage limits, temperature thresholds, and current limits.

Parameters:

  • state: The current state of the battery (e.g., "NORMAL", "TEMP_SHUTOFF").
  • minVoltage: Minimum voltage across all cells.
  • maxCelltemp: Maximum temperature among all cells.
  • current: The actual current being applied.
  • requiredCurrent: The desired current based on power demand.
  • temperatureSafety and temperatureRecovery: Safety temperature thresholds.
  • maxCurrent: Maximum allowable current.
  • voltageCuttoff: Minimum safe voltage.

Returns: Updated state and adjusted current.

Lean Fill Forward Function

def lean_fill_forward(df, delta='1s'):
# Function body...

Purpose: Adjusts the power profile DataFrame to maintain signal integrity during interpolation by adding rows just before existing ones.

Parameters:

  • df: The DataFrame containing the power profile.
  • delta: The time difference to subtract from each timestamp for the new row.

Returns: Adjusted DataFrame with additional rows.

Interpolation Function

def interpolateValue(ts, timestamps, values, lastTsIdx=None, debug=False):
# Function body...

Purpose: Interpolates values from arrays of timestamps and values based on a given timestamp.

Parameters:

  • ts: The timestamp for which the value is to be interpolated.
  • timestamps: Array of timestamps.
  • values: Array of values corresponding to the timestamps.
  • lastTsIdx: Index of the last timestamp.
  • debug: Enables debug mode.

Returns: Interpolated value, time difference to the next value, and position index.


Step-by-Step Script Explanation

Now, let's walk through the script step by step to understand how it works.

Setting Up the Client and Retrieving Assets

altergo_client = Client(apiKey="your_api_key", companyName='demo')
battery_asset = altergo_client.getAssetById("asset_id")
datasets = altergo_client.getDatasets(filterBy={"asset": battery_asset.serial_number})

Explanation:

  • Client Initialization: Creates a client object to interact with the system using an API key.
  • Retrieving the Battery Asset: Fetches the battery asset using its ID.
  • Retrieving Datasets: Retrieves datasets associated with the battery's serial number.

Loading the Battery Specification

with open('BAT-001_battery_specification.json') as f:
simSpec = json.load(f)

Explanation:

  • Loading JSON File: Reads the battery specification from a JSON file into a dictionary (simSpec).

Building the Battery Model

cell = Cell(simSpec["cell"])
battery = Battery()
battery.build(cell, simSpec)
battery.describe()

Explanation:

  • Creating Cell Object: Initializes a cell object with the specifications from simSpec.
  • Creating Battery Object: Initializes a battery object.
  • Building Battery Structure: Constructs the battery architecture based on the cell and specifications.
  • Describing Battery: Outputs the battery's configuration details.

Creating Simulation Sensors

digitalTwinBatterySensors = {
"Power Demand": Sensor(...),
"Power": Sensor(...),
# Additional sensors...
}

for i in range(3):
suffix = 'max' if i == 1 else 'min' if i == 0 else 'avg'
digitalTwinBatterySensors[f"Voltage Stack Aggregate|{i}"] = Sensor(...)
digitalTwinBatterySensors[f"Temperature Stack Aggregate|{i}"] = Sensor(...)

Explanation:

  • Defining Sensors: Sets up sensors to monitor various parameters like power demand, SoC, voltage, current, temperature, etc.
  • Creating Aggregate Sensors: Adds sensors for maximum, minimum, and average values of voltage and temperature across the battery stack.

Retrieving Power and Temperature Profiles

power_profile_df = pd.read_csv('power_profile.csv')
power_profile_df['t'] = pd.to_datetime(power_profile_df['t'], utc=True)
power_profile_df.set_index('t', inplace=True)

ambient_temperature_profile_df = power_profile_df.copy()
ambient_temperature_profile_df['Ambient temperature'] = 25

Explanation:

  • Loading Power Profile: Reads the power profile from a CSV file into a DataFrame.
  • Setting Timestamp Index: Converts the 't' column to datetime and sets it as the index.
  • Creating Temperature Profile: Copies the power profile DataFrame and sets a constant ambient temperature of 25°C.

Generating the Power Demand Profile

batteryCapacity = battery.capacity.init
batteryVoltage = battery.getVoltage(SIM_TARGETED_DEPTH)

maxChargeCurrent = 0.75 * batteryCapacity
maxDischargeCurrent = 1.0 * batteryCapacity

power_profile_df['charge_current'] = maxChargeCurrent * power_profile_df['normalized_charge_power']
power_profile_df['discharge_current'] = maxDischargeCurrent * power_profile_df['normalized_discharge_power']

power_profile_df['totalCurrent'] = power_profile_df['charge_current'] - power_profile_df['discharge_current']
power_profile_df['power_demand'] = power_profile_df['totalCurrent'] * batteryVoltage

Explanation:

  • Calculating Battery Capacity and Voltage: Retrieves the initial battery capacity and voltage.
  • Defining Maximum Currents: Sets maximum allowable charge and discharge currents.
  • Calculating Charge and Discharge Currents: Multiplies normalized power profiles by maximum currents.
  • Calculating Total Current and Power Demand: Computes the net current and power demand for the battery.

Simulation Time Management

startTime = power_profile_df.index[0].timestamp()
endTime = power_profile_df.index[-1].timestamp()
t = startTime
dt_s = 60.0
prev_dt_s = 0.0
maximum_dt_s = 60.0
minimum_dt_s = 1

Explanation:

  • Initializing Time Variables: Sets up the simulation start and end times, time step (dt_s), and time step constraints.

Initializing Battery State

initSoC = 1
initTemperature = 25

analysedElements = []
battery.getElementsByDepth(analysedElements, SIM_TARGETED_DEPTH)

for element in analysedElements:
element: ElectroChemEntity
element.soc = initSoC
element.soh = initSoH
element.temperature = initTemperature
element.calculateNextStep(0, 1)


battery.getVoltage(SIM_TARGETED_DEPTH)

Explanation:

  • Setting Initial Conditions: Defines the initial state of charge (SoC) and temperature.
  • Initializing Battery Elements: Sets the SoC and temperature for each cell/module/stack as per the simulation depth.
  • Calculating Initial Voltage: Computes the battery's voltage based on the initialized elements.

Running the Simulation

while t < endTime:
# Simulation loop
required_battery_power, dt_to_next_profile_input, lastLoadTsIdx = interpolateValue(...)
ambient_temperature, _, lastTempTsIdx = interpolateValue(...)

effectiveEnergy += (battery.voltage * abs(battery.current)) * dt_s / 3600

state, current = batteryStateMachine(...)

dt_s, t, prev_dt_s = update_time_step(...)

battery.setCurrent(current, SIM_TARGETED_DEPTH)

for el in analysedElements:
el.calculateNextStep(dt_s, ambient_temperature)

battery.getVoltage(SIM_TARGETED_DEPTH)

# Logging sensor data
digitalTwinBatterySensors["SoC"].significantAppend(...)
# Additional sensor logging...

Explanation:

  • Simulation Loop: Runs the simulation until the end time is reached.
  • Interpolating Power and Temperature Values: Retrieves the required power demand and ambient temperature at the current simulation time.
  • Updating Effective Energy: Accumulates the energy consumed or supplied.
  • Managing Battery State: Adjusts the battery state and current based on safety checks.
  • Updating Time Step: Adjusts the simulation time step dynamically.
  • Calculating Next State: Updates each battery element's state based on the current and time step.
  • Logging Data: Records sensor data for analysis.

Analyzing and Visualizing Results

dtDf = dataframeFromSensors(digitalTwinBatterySensors)
dtDf = dtDf.ffill()
dtDf.index = pd.to_datetime(dtDf.index)
dtDf.to_csv('battery_simulation.csv')

plot_data(dtDf, title="Battery Simulation")

Explanation:

  • Creating DataFrame from Sensors: Converts the sensor data into a DataFrame.
  • Forward Filling Missing Data: Fills missing values to maintain data continuity.
  • Saving Simulation Results: Exports the simulation data to a CSV file.
  • Visualizing Data: Plots the simulation results for analysis.

Example Code

# templates/quickstart/main.py

import json
import numpy as np
import pandas as pd
import plotly.graph_objects as go
from datetime import datetime, timezone
from typing import Dict

from altergo_sdk.api.altergo_api import Client
from altergo_sdk.tools.sim import Sensor, update_time_step
from altergo_sdk.tools.toolbox import dataframeFromSensors

from lair.components.battery_iq.battery import Battery, Cell, ElectroChemEntity, Stack
from lair.components.battery_iq.clone import batteryStateMachine
from lair.components.battery_iq.battery_architecture_builder.batteryArchitectureBuilder import BatteryArchitectureBuilder
from lair.tools.simulation.timeseries import lean_fill_forward, preprocess_df, interpolateValue

from debug_utils import debug_print, plot_data # Importing the debug function

### SIMULATION DATA PREPARATION ###
SIM_TARGETED_DEPTH = 1
DEBUG = False

altergo_client = Client(apiKey="a275db50c5974c878892b809b1c5f1fe", companyName='demo')

battery_asset = altergo_client.getAssetById("66c84ba12330e9a3a940b1e0")
datasets = altergo_client.getDatasets(filterBy={"asset": battery_asset.serial_number})

print("Battery asset:", battery_asset.serial_number)

# Load simulation specification
with open('BAT-001_battery_specification.json') as f:
simSpec = json.load(f)

cell = Cell(simSpec["cell"])
battery = Battery()
battery.build(cell, simSpec)
battery.describe()

# Initialize sensors
digitalTwinBatterySensors: Dict[str, Sensor] = {
"Power Demand": Sensor(data=[], time=[], lastSigVal=0.0, sigVarTh=1, unit="W", axis=3, synced=1, plotted=1, timeVarTh=60),
"Power": Sensor(data=[], time=[], lastSigVal=0.0, sigVarTh=300, unit="W", axis=3, synced=1, plotted=1, timeVarTh=60),
"SoC": Sensor(data=[], time=[], lastSigVal=0.0, sigVarTh=.1, unit="%", axis=3, synced=1, plotted=1, timeVarTh=60),
"Voltage": Sensor(data=[], time=[], lastSigVal=0.0, sigVarTh=0.01, unit="V", axis=3, synced=1, plotted=1, timeVarTh=60),
"Current": Sensor(data=[], time=[], lastSigVal=0.0, sigVarTh=.1, unit="A", axis=3, synced=1, plotted=1, timeVarTh=60),
"Ambient Temperature": Sensor(data=[], time=[], lastSigVal=0.0, sigVarTh=1, unit="°C", axis=3, synced=1, plotted=1, timeVarTh=60),
}

for i in range(3):
suffix = 'min' if i == 0 else 'max' if i == 1 else 'avg'
digitalTwinBatterySensors[f"Temperature Stack Aggregate|{i}"] = Sensor(
data=[], time=[], lastSigVal=0.0, name=f"temperature_{suffix}_coarse",
sigVarTh=0.01, unit="°C", axis=3, synced=1, plotted=1, timeVarTh=60
)

# Load profiles
power_profile_df = pd.read_csv('power_profile.csv', parse_dates=['t'], index_col='t')
power_profile_df = lean_fill_forward(power_profile_df, delta='1000ms')
power_profile_df.to_csv('new_power_profile.csv')

ambient_temperature_profile_df = power_profile_df.copy()
ambient_temperature_profile_df['Ambient Temperature'] = 25
ambient_temperature_profile_df.drop(columns=['normalized_discharge_power','normalized_charge_power','total','isResting'], inplace=True)

simulation_cutoffs = json.load(open('simulation_cutoffs.json'))

# Initialize simulation parameters
initSoC = 1
initSoH = 1
initTemperature = 25

batteryCapacity = battery.capacity.init # Ah
batteryVoltage = battery.getVoltage(SIM_TARGETED_DEPTH)

maxChargeCurrent = 0.75 * batteryCapacity
maxDischargeCurrent = 1.0 * batteryCapacity

# Generate currents
power_profile_df['charge_current'] = maxChargeCurrent * power_profile_df['normalized_charge_power']
power_profile_df['discharge_current'] = maxDischargeCurrent * power_profile_df['normalized_discharge_power']
power_profile_df['totalCurrent'] = power_profile_df['charge_current'] - power_profile_df['discharge_current']
power_profile_df['power_demand'] = power_profile_df['totalCurrent'] * batteryVoltage
power_profile_df['totalElapsedTime'] = (power_profile_df.index - power_profile_df.index[0]).total_seconds()
power_profile_df['DeltaTime'] = power_profile_df['totalElapsedTime'].diff().fillna(0).astype(int)
power_profile_df = lean_fill_forward(power_profile_df, delta='1000ms')

# Simulation time management
startTime = power_profile_df.index[0].timestamp()
endTime = power_profile_df.index[-1].timestamp()
t = startTime
dt_s = 60.0
prev_dt_s = 0.0
maximum_dt_s = 60.0
minimum_dt_s = 1
dt_to_next_profile_input = 0
analysedElements = []
battery.getElementsByDepth(analysedElements, SIM_TARGETED_DEPTH)

# Initialize battery state
for element in analysedElements:
element: ElectroChemEntity
element.soc = initSoC
element.soh = initSoH
element.temperature = initTemperature
element.calculateNextStep(0, 1)

battery.getVoltage(SIM_TARGETED_DEPTH)

preprocessed_power_profile = preprocess_df(power_profile_df, "power_demand")
preprocessed_temperature_profile = preprocess_df(ambient_temperature_profile_df, "Ambient Temperature")
effectiveEnergy = 0

# Simulation parameters
temperatureSafety = 55 # °C
hysteresisTemperature = 10 # °C
temperatureRecovery = temperatureSafety - hysteresisTemperature # °C
coolingForcedConvectionMultiplier = simSpec['battery']['forcedConvectionMultiplier']
restingForcedConvectionMultiplier = 1
startCoolingTemperature = 80 # °C
maxCoolingTemperature = 70 # °C
maxFanRPM = 5700 # RPM
lowVoltageSafetyCutoff = 2.3 # V
endOfChargeTh = simulation_cutoffs["maxSoC"]
chargeTaperStartTh = endOfChargeTh - 0.05
state = "NORMAL"

power_timestamps, power_values = preprocessed_power_profile
temperature_timestamps, temperature_values = preprocessed_temperature_profile

print("Simulation Start")

lastLoadTsIdx = 0
lastTempTsIdx = 0
t_profile = 0
effectiveEnergy = 0

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

t_profile = t - 0 * (power_profile_df.index[-1] - power_profile_df.index[0]).total_seconds()

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
)

effectiveEnergy += (battery.voltage * abs(battery.current)) * dt_s / 3600

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)

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

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))

for el in analysedElements:
el: Stack
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)

# 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")


Conclusion

By following this tutorial, you've learned how to set up a battery simulation using a detailed battery model, manage simulation time steps dynamically, handle battery state transitions based on safety parameters, and analyze the results. Understanding each component and function allows for customization and extension of the simulation to suit different battery configurations and operational scenarios.


Note: Always ensure that the battery specifications and safety parameters are accurately defined to reflect real-world conditions and prevent unrealistic simulation results.