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
- Battery Simulation Tutorial
- Table of Contents
- Introduction
- Understanding the Battery Model
- Key Functions Used in the Script
- Step-by-Step Script Explanation
- Analyzing and Visualizing Results
- Example Code
- Conclusion
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
| Parameter | Description | Value/Example |
|---|---|---|
name | Name of the cell model. | "EP LFP Cell" |
cellChemistry | Chemistry type of the cell. | "LFP" |
voltage | Nominal voltage of the cell in volts (V). | 3.22 |
cellCapacity | Capacity of the cell in ampere-hours (Ah). | 64.05 |
cycleLife | Expected number of charge-discharge cycles before end of life. | 2000.0 |
mass | Mass of the cell in kilograms (kg). | 0.001 |
surface | Surface area of the cell in square meters (m²). | 0.0225 |
specificHeat | Specific heat capacity of the cell material in J/(kg·K). | 300.0 |
weight | Weight of the cell in kilograms (kg). | 1.0 |
convectionSurf | Surface area available for convection cooling in square meters (m²). | 0.0475 |
convectionH | Convective heat transfer coefficient. | 1 |
ocvCharge | Open Circuit Voltage (OCV) during charge at different states of charge (SOC). | List of lists with voltage values (see example in JSON). |
ocvDischarge | Open Circuit Voltage (OCV) during discharge at different states of charge (SOC). | List of lists with voltage values (see example in JSON). |
temperatureRange | Temperature range for simulation in degrees Celsius (°C). | [5, 15, 25, 35, 45, 55] |
socRange | State of Charge (SOC) range for simulation (0.0 to 1.0). | [0.0, 0.01, 0.02, ..., 0.1, "..."] |
soh | State of Health (SOH) parameters, including aging models. | See State of Health Configuration below. |
rComponent | Internal resistance components of the cell. | [0.00046272, 0.0005904, 0.000125136] |
rSocFactor | Resistance factors based on SOC for charge and discharge. | See Resistance SOC Factor Configuration below. |
rTempFactor | Resistance factors based on temperature. | See Resistance Temperature Factor Configuration below. |
rSohFactor | Resistance factors based on SOH. | See Resistance SOH Factor Configuration below. |
cComponent | Capacitance components of the cell. | [0.0, 158350.5354, 62834.88] |
State of Health (SOH) Configuration
| Parameter | Description | Value/Example |
|---|---|---|
endOfLifeSOH | SOH threshold indicating end of life (e.g., 80% SOH). | 0.8 |
CalendarAgingParams | Parameters related to calendar aging. | See Calendar Aging Parameters below. |
CyclicAgingParams | Parameters related to cyclic aging. | See Cyclic Aging Parameters below. |
a. Calendar Aging Parameters
| Parameter | Description | Value/Example |
|---|---|---|
LTableCalSOC | Lookup table for calendar aging based on SOC. | See LTableCalSOC Parameters below. |
Tmax | Maximum temperature for calendar aging (°C). | 60 |
i. LTableCalSOC Parameters
| Parameter | Description | Value/Example |
|---|---|---|
name | Name of the SOC lookup table. | "LTableCalSOC" |
unit | Unit of measurement for SOC. | "%" |
index | SOC indices for the lookup table. | [0, 1] |
factor | Aging factors corresponding to SOC indices. | [0, 1] |
kCalSOC | Calendar aging rate constant based on SOC. | 0.0001 |
b. Cyclic Aging Parameters
| Parameter | Description | Value/Example |
|---|---|---|
stdCycleLife | Standard cycle life (number of cycles). | 2000 |
Resistance Factors Configuration
a. Resistance SOC & Temperature Factor
| Parameter | Description | Value/Example |
|---|---|---|
socRange | State 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] |
temperaturerange | Temperature 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) Index | RFactor 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) Index | RFactor 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
| Parameter | Description | Value/Example |
|---|---|---|
soh | State of Health (SOH) values used for resistance factor interpolation. | [1.0, 0.8, 0.6, 0.4] |
rFactor | Resistance factors at corresponding SOH values. | [1.0, 1.38, 1.83, 2.3] |
Battery Configuration
| Parameter | Description | Value/Example |
|---|---|---|
name | Name of the battery model. | "EP Battery" |
forcedConvectionMultiplier | Multiplier for forced convection cooling effectiveness. | 20.0 |
Architecture Configuration
| Parameter | Description | Value/Example |
|---|---|---|
name | Name of the battery architecture. | "EP Battery" |
cellsSeries | Number of cells connected in series within a module. | 1 |
cellsParallel | Number of cells connected in parallel within a module. | 1 |
modulesSeries | Number of modules connected in series within a stack. | 1 |
modulesParallel | Number of modules connected in parallel within a stack. | 1 |
stacksSeries | Number of stacks connected in series within the battery. | 24 |
stacksParallel | Number of stacks connected in parallel within the battery. | 1 |
Simulation Parameters
| Parameter | Description | Value/Example |
|---|---|---|
start | Start time of the simulation (relative). | 0 |
length | Total length/duration of the simulation. | 0 |
step | Time step increment for the simulation. | 1 |
serialNumber | Unique identifier for the battery asset. | "BAT-001" |
Additional Components
a. Resistance Components (rComponent)
| Index | Description | Value |
|---|---|---|
| 0 | First internal resistance component. | 0.00046272 |
| 1 | Second internal resistance component. | 0.0005904 |
| 2 | Third internal resistance component. | 0.000125136 |
b. Capacitance Components (cComponent)
| Index | Description | Value |
|---|---|---|
| 0 | First capacitance component. | 0.0 |
| 1 | Second capacitance component. | 158350.5354 |
| 2 | Third capacitance component. | 62834.88 |
Overview of OCV (Open Circuit Voltage) Profiles
a. OCV Charge (ocvCharge)
| State of Charge (SOC) Index | OCV 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) Index | OCV 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_multiplierandsampling_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.temperatureSafetyandtemperatureRecovery: 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.