import unittest
import numpy as np
import pandas as pd
from unittest.mock import patch, MagicMock
import math
from typing import Union, Tuple, List, Mapping, Any, Iterable
from citylearn.energy_model import Battery
# Constants used in the Battery class
ZERO_DIVISION_PLACEHOLDER = 1e-10
# Mock the Environment class that Device inherits from
[docs]
class Environment:
def __init__(self, **kwargs):
self.time_step = 0
self.episode_tracker = MagicMock()
self.episode_tracker.episode_time_steps = 100
self.time_step_ratio = kwargs.get('time_step_ratio', 1.0)
self.numpy_random_state = np.random.RandomState(kwargs.get('random_seed', 42))
[docs]
def reset(self):
self.time_step = 0
# Mock other necessary components to isolate Battery for testing
[docs]
class DataSet:
[docs]
def get_battery_sizing_data(self):
data = {
'model': ['model1', 'model2', 'model3'],
'capacity': [10.0, 20.0, 50.0],
'nominal_power': [5.0, 10.0, 25.0],
'depth_of_discharge': [0.8, 0.9, 0.85],
'efficiency': [0.92, 0.95, 0.90],
'loss_coefficient': [0.002, 0.001, 0.003],
'capacity_loss_coefficient': [1e-5, 2e-5, 5e-6]
}
df = pd.DataFrame(data)
return df
[docs]
class TestBattery(unittest.TestCase):
"""Unit tests for the Battery class"""
[docs]
def setUp(self):
"""Set up test fixtures before each test"""
self.battery = Battery(
capacity=100.0,
nominal_power=10.0,
capacity_loss_coefficient=1e-5,
power_efficiency_curve=[[0, 0.83], [0.3, 0.83], [0.7, 0.9], [0.8, 0.9], [1, 0.85]],
capacity_power_curve=[[0.0, 1], [0.8, 1], [1.0, 0.2]],
depth_of_discharge=0.8,
efficiency=0.9,
loss_coefficient=0.001,
random_seed=42
)
self.battery.episode_tracker = MagicMock()
self.battery.episode_tracker.episode_time_steps = 100
self.battery.reset()
self.original_update_electricity = self.battery.update_electricity_consumption
self.battery.update_electricity_consumption = MagicMock()
[docs]
def tearDown(self):
"""Clean up after each test"""
if hasattr(self, 'original_update_electricity'):
self.battery.update_electricity_consumption = self.original_update_electricity
[docs]
def test_initialization(self):
"""Test battery initialization with various parameters"""
battery_default = Battery(random_seed=42)
self.assertIsNotNone(battery_default.capacity)
self.assertIsNotNone(battery_default.nominal_power)
self.assertIsNotNone(battery_default.depth_of_discharge)
self.assertIsNotNone(battery_default.capacity_loss_coefficient)
battery = Battery(
capacity=200.0,
nominal_power=20.0,
capacity_loss_coefficient=2e-5,
depth_of_discharge=0.9,
efficiency=0.95,
random_seed=42
)
self.assertEqual(battery.capacity, 200.0)
self.assertEqual(battery.nominal_power, 20.0)
self.assertEqual(battery.capacity_loss_coefficient, 2e-5)
self.assertEqual(battery.depth_of_discharge, 0.9)
self.assertEqual(battery.efficiency, 0.95)
self.assertEqual(len(battery.capacity_history), 1)
self.assertEqual(battery.capacity_history[0], 200.0)
self.assertTrue(hasattr(battery, '_efficiency_history'))
[docs]
def test_property_getters(self):
"""Test property getters return expected values"""
self.assertEqual(self.battery.capacity, 100.0)
self.assertEqual(self.battery.nominal_power, 10.0)
self.assertEqual(self.battery.depth_of_discharge, 0.8)
self.assertEqual(self.battery.capacity_loss_coefficient, 1e-5)
self.assertEqual(len(self.battery.capacity_history), 1)
self.assertEqual(self.battery.capacity_history[0], 100.0)
self.assertEqual(self.battery.degraded_capacity, 100.0) # Initial value should equal capacity
[docs]
def test_property_setters(self):
"""Test property setters update values correctly"""
self.battery.capacity = 150.0
self.assertEqual(self.battery.capacity, 150.0)
self.assertEqual(self.battery.capacity_history[-1], 150.0)
self.battery.capacity_loss_coefficient = 2e-5
self.assertEqual(self.battery.capacity_loss_coefficient, 2e-5)
self.battery.depth_of_discharge = 0.9
self.assertEqual(self.battery.depth_of_discharge, 0.9)
new_power_curve = [[0, 0.8], [0.5, 0.85], [1, 0.9]]
self.battery.power_efficiency_curve = new_power_curve
self.assertTrue(np.array_equal(self.battery.power_efficiency_curve[0], np.array([0, 0.5, 1])))
self.assertTrue(np.array_equal(self.battery.power_efficiency_curve[1], np.array([0.8, 0.85, 0.9])))
new_capacity_curve = [[0, 0.9], [0.5, 0.8], [1, 0.1]]
self.battery.capacity_power_curve = new_capacity_curve
self.assertTrue(np.array_equal(self.battery.capacity_power_curve[0], np.array([0, 0.5, 1])))
self.assertTrue(np.array_equal(self.battery.capacity_power_curve[1], np.array([0.9, 0.8, 0.1])))
[docs]
def test_charge_positive_energy(self):
"""Test the charge method with positive energy (charging)"""
self.battery.force_set_soc(0.5) # Start at 50% SOC
# Test charging with small amount
self.battery.charge(5.0) # Charge with 5 kWh
# Check that SOC increased and energy_balance is positive
self.assertGreater(self.battery.soc[self.battery.time_step], 0.5)
self.assertGreater(self.battery.energy_balance[self.battery.time_step], 0)
# Verify update_electricity_consumption was called
self.battery.update_electricity_consumption.assert_called_once()
[docs]
def test_charge_negative_energy(self):
"""Test the charge method with negative energy (discharging)"""
self.battery.force_set_soc(0.9) # Start at 90% SOC
# Test discharging
self.battery.charge(-5.0) # Discharge with 5 kWh
# Check that SOC decreased and energy_balance is negative
self.assertLess(self.battery.soc[self.battery.time_step], 0.9)
self.assertLess(self.battery.energy_balance[self.battery.time_step], 0)
# Verify update_electricity_consumption was called
self.battery.update_electricity_consumption.assert_called_once()
[docs]
def test_depth_of_discharge_limit(self):
"""Test that the battery respects the depth of discharge limit when discharging"""
# Set initial state to just above the DoD limit
initial_soc = 1.0 - self.battery.depth_of_discharge + 0.05
self.battery.force_set_soc(initial_soc)
# Try to discharge more than allowed by DoD
big_discharge = -self.battery.capacity # Try to completely discharge
self.battery.charge(big_discharge)
# Check that SOC didn't go below DoD limit
min_allowed_soc = 1.0 - self.battery.depth_of_discharge
self.assertGreaterEqual(self.battery.soc[self.battery.time_step], min_allowed_soc - 1e-2)
[docs]
def test_capacity_limit(self):
"""Test that the battery respects the capacity limit when charging"""
# Set initial state to high SOC
self.battery.force_set_soc(0.9)
# Try to charge more than capacity allows
big_charge = self.battery.capacity # Try to charge with full capacity worth of energy
self.battery.charge(big_charge)
# Check that SOC didn't exceed 100%
self.assertLessEqual(self.battery.soc[self.battery.time_step], 1.0 + 1e-6) # Allow for floating-point error
[docs]
def test_degrade(self):
"""Test the battery degradation calculation"""
# Set a known state
self.battery.force_set_soc(0.5)
# Charge with a known amount of energy
self.battery.charge(10.0)
# Manually calculate expected degradation
energy_balance = self.battery.energy_balance[self.battery.time_step]
expected_degradation = self.battery.capacity_loss_coefficient * self.battery.capacity * abs(energy_balance) / (2 * self.battery.degraded_capacity)
# Call the degrade method directly and compare
actual_degradation = self.battery.degrade()
self.assertAlmostEqual(actual_degradation, expected_degradation, places=6)
# Verify capacity history has been updated after charging
self.assertEqual(len(self.battery.capacity_history), 2)
self.assertLess(self.battery.capacity_history[1], self.battery.capacity_history[0])
[docs]
def test_get_current_efficiency(self):
"""Test the efficiency calculation based on power level"""
# Test get_current_efficiency at different power levels
for power_level in [1.0, 5.0, 10.0]:
efficiency = self.battery.get_current_efficiency(power_level)
# Efficiency should be between 0 and 1
self.assertGreaterEqual(efficiency, 0.0)
self.assertLessEqual(efficiency, 1.0)
[docs]
def test_force_set_soc(self):
"""Test the force_set_soc method"""
# Try setting SOC to specific values
test_socs = [0.0, 0.3, 0.75, 1.0]
for soc in test_socs:
self.battery.force_set_soc(soc)
self.assertAlmostEqual(self.battery.soc[self.battery.time_step], soc)
# Test with invalid SOC values
with self.assertRaises(AttributeError):
self.battery.force_set_soc(-0.1)
with self.assertRaises(AttributeError):
self.battery.force_set_soc(1.1)
[docs]
def test_reset(self):
"""Test the reset method"""
# Make some changes to the battery state
self.battery.force_set_soc(0.7)
self.battery.charge(5.0)
self.battery.charge(-2.0)
# Reset the battery
self.battery.reset()
# Check that state has been reset
self.assertEqual(self.battery.time_step, 0)
self.assertEqual(len(self.battery.efficiency_history), 1)
self.assertEqual(len(self.battery.capacity_history), 1)
self.assertEqual(self.battery.capacity_history[0], self.battery.capacity)
[docs]
def test_as_dict(self):
"""Test the as_dict method for rendering"""
# Set a known state
self.battery.force_set_soc(0.6)
self.battery.charge(5.0)
# Get dict representation
dict_repr = self.battery.as_dict()
# Check that required keys exist
self.assertIn('Battery Soc-%', dict_repr)
self.assertIn('Battery (Dis)Charge-kWh', dict_repr)
# Check values
self.assertEqual(dict_repr['Battery Soc-%'], self.battery.soc[self.battery.time_step])
self.assertEqual(dict_repr['Battery (Dis)Charge-kWh'], self.battery.energy_balance[self.battery.time_step])
[docs]
@patch('citylearn.data.DataSet')
def test_autosize(self, mock_dataset):
"""Test the autosize method"""
# Set up mock dataset
mock_instance = mock_dataset.return_value
mock_instance.get_battery_sizing_data.return_value = DataSet().get_battery_sizing_data()
# Test autosizing
capacity, nominal_power, dod, efficiency, loss_coef, capacity_loss_coef = self.battery.autosize(
demand=10.0,
duration=2.0,
safety_factor=1.2
)
# Verify returned values
self.assertGreater(capacity, 0)
self.assertGreater(nominal_power, 0)
self.assertGreaterEqual(dod, 0)
self.assertLessEqual(dod, 1)
self.assertGreater(efficiency, 0)
self.assertLessEqual(efficiency, 1)
# Check autosize_config was created
self.assertIsNotNone(self.battery._autosize_config)
if __name__ == '__main__':
unittest.main()