Source code for citylearn.tests.test_battery

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 get_metadata(self): return {}
[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_max_input_power(self): """Test the get_max_input_power method at different SOC levels""" # Test at different SOC levels for soc in [0.1, 0.5, 0.9]: self.battery.force_set_soc(soc) max_power = self.battery.get_max_input_power() # Power should be within reasonable bounds and related to nominal power self.assertGreaterEqual(max_power, 0.0) self.assertLessEqual(max_power, self.battery.nominal_power * 1.1) # Allow for some tolerance # At high SOC, max power should be lower if soc > 0.8: self.assertLess(max_power, self.battery.nominal_power)
[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_get_metadata(self): """Test the get_metadata method""" metadata = self.battery.get_metadata() # Check that required keys exist self.assertIn('depth_of_discharge', metadata) self.assertIn('capacity_loss_coefficient', metadata) self.assertIn('power_efficiency_curve', metadata) self.assertIn('capacity_power_curve', metadata) # Check that values match self.assertEqual(metadata['depth_of_discharge'], self.battery.depth_of_discharge) self.assertEqual(metadata['capacity_loss_coefficient'], self.battery.capacity_loss_coefficient)
[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()