ubc-solar-physics 1.3.0__cp311-cp311-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. core.cp311-win_amd64.pyd +0 -0
  2. physics/__init__.py +14 -0
  3. physics/_version.py +16 -0
  4. physics/environment/__init__.py +15 -0
  5. physics/environment/environment.rs +2 -0
  6. physics/environment/gis/__init__.py +7 -0
  7. physics/environment/gis/base_gis.py +24 -0
  8. physics/environment/gis/gis.py +337 -0
  9. physics/environment/gis/gis.rs +25 -0
  10. physics/environment/gis.rs +1 -0
  11. physics/environment/meteorology/__init__.py +3 -0
  12. physics/environment/meteorology/base_meteorology.py +69 -0
  13. physics/environment/meteorology/clouded_meteorology.py +600 -0
  14. physics/environment/meteorology/irradiant_meteorology.py +107 -0
  15. physics/environment/meteorology/meteorology.rs +138 -0
  16. physics/environment/meteorology.rs +1 -0
  17. physics/environment.rs +2 -0
  18. physics/lib.rs +132 -0
  19. physics/models/__init__.py +13 -0
  20. physics/models/arrays/__init__.py +7 -0
  21. physics/models/arrays/arrays.rs +0 -0
  22. physics/models/arrays/base_array.py +6 -0
  23. physics/models/arrays/basic_array.py +39 -0
  24. physics/models/arrays.rs +1 -0
  25. physics/models/battery/__init__.py +14 -0
  26. physics/models/battery/base_battery.py +29 -0
  27. physics/models/battery/basic_battery.py +140 -0
  28. physics/models/battery/battery.rs +78 -0
  29. physics/models/battery/battery_config.py +22 -0
  30. physics/models/battery/battery_config.toml +8 -0
  31. physics/models/battery/battery_model.py +135 -0
  32. physics/models/battery/kalman_filter.py +341 -0
  33. physics/models/battery.rs +1 -0
  34. physics/models/constants.py +23 -0
  35. physics/models/lvs/__init__.py +7 -0
  36. physics/models/lvs/base_lvs.py +6 -0
  37. physics/models/lvs/basic_lvs.py +18 -0
  38. physics/models/lvs/lvs.rs +0 -0
  39. physics/models/lvs.rs +1 -0
  40. physics/models/motor/__init__.py +7 -0
  41. physics/models/motor/base_motor.py +6 -0
  42. physics/models/motor/basic_motor.py +174 -0
  43. physics/models/motor/motor.rs +0 -0
  44. physics/models/motor.rs +1 -0
  45. physics/models/regen/__init__.py +7 -0
  46. physics/models/regen/base_regen.py +6 -0
  47. physics/models/regen/basic_regen.py +39 -0
  48. physics/models/regen/regen.rs +0 -0
  49. physics/models/regen.rs +1 -0
  50. physics/models.rs +5 -0
  51. ubc_solar_physics-1.3.0.dist-info/LICENSE +21 -0
  52. ubc_solar_physics-1.3.0.dist-info/METADATA +141 -0
  53. ubc_solar_physics-1.3.0.dist-info/RECORD +55 -0
  54. ubc_solar_physics-1.3.0.dist-info/WHEEL +5 -0
  55. ubc_solar_physics-1.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,107 @@
1
+ from physics.environment.meteorology.base_meteorology import BaseMeteorology
2
+ from physics.environment.gis.gis import calculate_path_distances
3
+ import numpy as np
4
+ import core
5
+ from typing import Optional
6
+
7
+
8
+ class IrradiantMeteorology(BaseMeteorology):
9
+ """
10
+ IrradiantMeteorology encapsulates meteorological data that includes
11
+ solar irradiance data, but not cloud cover.
12
+
13
+ """
14
+ def __init__(self, race, weather_forecasts):
15
+ self._race = race
16
+ self._raw_weather_data = weather_forecasts
17
+
18
+ self._time_dt: Optional[np.ndarray] = None
19
+ self._ghi: Optional[np.ndarray] = None
20
+ self._longitude: Optional[np.ndarray] = None
21
+ self._latitude: Optional[np.ndarray] = None
22
+
23
+ self.last_updated_time = self._raw_weather_data[0, 0, 0]
24
+
25
+ super().__init__()
26
+
27
+ def spatially_localize(self, cumulative_distances: np.ndarray, simplify_weather: bool = False) -> None:
28
+ """
29
+
30
+ :param np.ndarray cumulative_distances: NumPy Array representing cumulative distances theoretically achievable for a given input speed array
31
+ :param bool simplify_weather: enable to only use a single weather coordinate (for track races without varying weather)
32
+
33
+ """
34
+ # If racing a track race, there is no need for distance calculations. We will return only the origin coordinate
35
+ # This characterizes the weather at every point along the FSGP tracks
36
+ # with the weather at a single coordinate on the track, which is great for reducing the API calls and is a
37
+ # reasonable assumption to make for FSGP only.
38
+ if simplify_weather:
39
+ self._weather_indices = np.zeros_like(cumulative_distances, dtype=int)
40
+ return
41
+
42
+ # a list of all the coordinates that we have weather data for
43
+ weather_coords = self._raw_weather_data[:, 0, 1:3]
44
+
45
+ # distances between all the coordinates that we have weather data for
46
+ weather_path_distances = calculate_path_distances(weather_coords)
47
+ cumulative_weather_path_distances = np.cumsum(weather_path_distances)
48
+
49
+ # makes every even-index element negative, this allows the use of np.diff() to calculate the sum of consecutive
50
+ # elements
51
+ cumulative_weather_path_distances[::2] *= -1
52
+
53
+ # contains the average distance between two consecutive elements in the cumulative_weather_path_distances array
54
+ average_distances = np.abs(np.diff(cumulative_weather_path_distances) / 2)
55
+
56
+ self._weather_indices = core.closest_weather_indices_loop(cumulative_distances, average_distances)
57
+
58
+ def temporally_localize(self, unix_timestamps, start_time, tick) -> None:
59
+ """
60
+
61
+ Takes in an array of indices of the weather_forecast array, and an array of timestamps. Uses those to figure out
62
+ what the weather forecast is at each time step being simulated.
63
+
64
+ we only have weather at discrete timestamps. The car however can be in any timestamp in
65
+ between. Therefore, we must be able to choose the weather timestamp that is closest to the one that the car is in
66
+ so that we can more accurately determine the weather experienced by the car at that timestamp.
67
+
68
+ For example, imagine the car is at some coordinate (x,y) at timestamp 100. Imagine we know the weather forecast
69
+ at (x,y) for five different timestamps: 0, 30, 60, 90, and 120. Which weather forecast should we
70
+ choose? Clearly, we should choose the weather forecast at 90 since it is the closest to 100. That's what the
71
+ below code is accomplishing.
72
+
73
+ :param np.ndarray unix_timestamps: (int[N]) unix timestamps of the vehicle's journey
74
+ :param int start_time: time since the start of the race that simulation is beginning
75
+ :param int tick: length of a tick in seconds
76
+ :returns: a SolcastEnvironment object with time_dt, latitude, longitude, wind_speed, wind_direction, and ghi.
77
+ :rtype: SolcastEnvironment
78
+ """
79
+ forecasts_array = core.weather_in_time(unix_timestamps.astype(np.int64), self._weather_indices.astype(np.int64),
80
+ self._raw_weather_data, 0)
81
+
82
+ self._time_dt = forecasts_array[:, 0]
83
+ self._latitude = forecasts_array[:, 1]
84
+ self._longitude = forecasts_array[:, 2]
85
+ self._wind_speed = forecasts_array[:, 3]
86
+ self._wind_direction = forecasts_array[:, 4]
87
+ self._solar_irradiance = forecasts_array[:, 5]
88
+
89
+ def calculate_solar_irradiances(self, coords, time_zones, local_times, elevations):
90
+ """
91
+ Calculates the Global Horizontal Irradiance from the Sun, relative to a location
92
+ on the Earth, for arrays of coordinates, times, elevations and weathers
93
+ https://www.pveducation.org/pvcdrom/properties-of-sunlight/calculation-of-solar-insolation
94
+ Note: If local_times and time_zones are both unadjusted for Daylight Savings, the
95
+ calculation will end up just the same
96
+
97
+ :param np.ndarray coords: (float[N][lat, lng]) array of latitudes and longitudes
98
+ :param np.ndarray time_zones: (int[N]) time zones at different locations in seconds relative to UTC
99
+ :param np.ndarray local_times: (int[N]) unix time that the vehicle will be at each location. (Adjusted for Daylight Savings)
100
+ :param np.ndarray elevations: (float[N]) elevation from sea level in m
101
+ :returns: (float[N]) Global Horizontal Irradiance in W/m2
102
+ :rtype: np.ndarray
103
+
104
+ """
105
+ return self.solar_irradiance
106
+
107
+
@@ -0,0 +1,138 @@
1
+ use chrono::{Datelike, NaiveDateTime, Timelike};
2
+ use numpy::ndarray::{s, Array, Array2, ArrayViewD, ArrayViewMut2, ArrayViewMut3, Axis};
3
+
4
+
5
+ pub fn rust_closest_weather_indices_loop(
6
+ cumulative_distances: ArrayViewD<'_, f64>,
7
+ average_distances: ArrayViewD<'_, f64>,
8
+ ) -> Vec<i64> {
9
+ let mut current_coord_index: usize = 0;
10
+ let mut result: Vec<i64> = Vec::with_capacity(cumulative_distances.len());
11
+
12
+ for &distance in cumulative_distances {
13
+ current_coord_index = std::cmp::min(current_coord_index, average_distances.len() - 1);
14
+
15
+ if distance > average_distances[current_coord_index] {
16
+ current_coord_index += 1;
17
+
18
+ current_coord_index =
19
+ std::cmp::min(current_coord_index, average_distances.len() - 1);
20
+ }
21
+
22
+ result.push(current_coord_index as i64);
23
+ }
24
+
25
+ result
26
+ }
27
+
28
+ pub fn rust_weather_in_time(
29
+ unix_timestamps: ArrayViewD<'_, i64>,
30
+ indices: ArrayViewD<'_, i64>,
31
+ weather_forecast: ArrayViewD<f64>,
32
+ dt_index: u8
33
+ ) -> Array2<f64> {
34
+ // Obtain dimensions for arrays and slices
35
+ let weather_forecast_raw_dim = weather_forecast.raw_dim();
36
+ let full_forecast_shape = (
37
+ weather_forecast_raw_dim[0],
38
+ weather_forecast_raw_dim[1],
39
+ weather_forecast_raw_dim[2],
40
+ );
41
+ let weather_at_coord_shape = (full_forecast_shape.1, full_forecast_shape.2);
42
+ let weather_in_time_shape = (indices.len(), full_forecast_shape.2);
43
+
44
+ // Create an empty full_weather_forecast_at_coords array (all zeros)
45
+ let indexed_weather_shape = (indices.len(), full_forecast_shape.1, full_forecast_shape.2);
46
+ let mut placeholder1: Vec<f64> =
47
+ vec![0.0; indexed_weather_shape.0 * indexed_weather_shape.1 * indexed_weather_shape.2];
48
+ let mut indexed_forecast =
49
+ ArrayViewMut3::from_shape(indexed_weather_shape, &mut placeholder1).unwrap();
50
+
51
+ // Fill full_weather_forecast_at_coords with the 2d slices at [indices]
52
+ for (out_index, &coord_index) in indices.iter().enumerate() {
53
+ let slice_2d = weather_forecast
54
+ .slice(s![coord_index as usize, .., ..])
55
+ .into_shape(weather_at_coord_shape)
56
+ .unwrap();
57
+ indexed_forecast
58
+ .slice_mut(s![out_index, .., ..])
59
+ .assign(&slice_2d);
60
+ }
61
+
62
+ let mut dt_local_array = Vec::with_capacity(full_forecast_shape.1);
63
+ // Populate dt_local_array with the list of forecast's timestamps at the first coordinate
64
+ // I don't really understand how this works
65
+ let dt_local_arrayview = weather_forecast
66
+ .index_axis_move(Axis(0), 0)
67
+ .index_axis_move(Axis(1), dt_index as usize);
68
+ for &timestamp in dt_local_arrayview {
69
+ dt_local_array.push(timestamp as i64);
70
+ }
71
+
72
+ let closest_timestamp_indices =
73
+ rust_closest_timestamp_indices(unix_timestamps, dt_local_array);
74
+
75
+ // Create a mutable array of the desired shape with dummy initial values
76
+ let mut placeholder2: Vec<f64> =
77
+ vec![0.0; weather_in_time_shape.0 * weather_in_time_shape.1];
78
+ let mut weather_in_time_arrayview =
79
+ ArrayViewMut2::from_shape(weather_in_time_shape, &mut placeholder2).unwrap();
80
+ for (index_1, &index_2) in closest_timestamp_indices.iter().enumerate() {
81
+ let slice_1d = indexed_forecast
82
+ .slice(s![index_1, index_2, ..])
83
+ .into_shape(full_forecast_shape.2)
84
+ .unwrap();
85
+ weather_in_time_arrayview
86
+ .slice_mut(s![index_1, ..])
87
+ .assign(&slice_1d);
88
+ }
89
+
90
+ weather_in_time_arrayview.into_owned()
91
+ }
92
+
93
+ pub fn rust_closest_timestamp_indices(
94
+ unix_timestamps: ArrayViewD<'_, i64>,
95
+ dt_local_array: Vec<i64>,
96
+ ) -> Vec<usize> {
97
+ let mut closest_time_stamp_indices: Vec<usize> = Vec::new();
98
+
99
+ for unix_timestamp in unix_timestamps {
100
+ let unix_timestamp_array =
101
+ Array::from_elem(dt_local_array.len(), unix_timestamp as &i64);
102
+ let mut differences: Vec<i64> = Vec::new();
103
+
104
+ for i in 0..unix_timestamp_array.len() {
105
+ differences.push((unix_timestamp - dt_local_array[i]).abs());
106
+ }
107
+
108
+ let (min_index, _) = differences
109
+ .iter()
110
+ .enumerate()
111
+ .min_by_key(|(_, &v)| v)
112
+ .unwrap();
113
+ closest_time_stamp_indices.push(min_index)
114
+ }
115
+ closest_time_stamp_indices
116
+ }
117
+
118
+ pub fn rust_calculate_array_ghi_times<'a>(
119
+ local_times: ArrayViewD<'_, u64>,
120
+ ) -> (Vec<f64>, Vec<f64>) {
121
+ let mut datetimes: Vec<_> = Vec::with_capacity(local_times.len());
122
+
123
+ for &unix_time_stamp in local_times {
124
+ let datetime = NaiveDateTime::from_timestamp_opt(unix_time_stamp as i64, 0).unwrap();
125
+ datetimes.push(datetime);
126
+ }
127
+
128
+ let day_of_year_out: Vec<f64> = datetimes
129
+ .iter()
130
+ .map(|&date| date.date().ordinal() as f64)
131
+ .collect();
132
+ let local_time_out: Vec<f64> = datetimes
133
+ .iter()
134
+ .map(|&date| date.time().num_seconds_from_midnight() as f64 / 3600.0)
135
+ .collect();
136
+
137
+ (day_of_year_out, local_time_out)
138
+ }
@@ -0,0 +1 @@
1
+ pub mod meteorology;
physics/environment.rs ADDED
@@ -0,0 +1,2 @@
1
+ pub mod gis;
2
+ pub mod meteorology;
physics/lib.rs ADDED
@@ -0,0 +1,132 @@
1
+ use chrono::{Datelike, NaiveDateTime, Timelike};
2
+ use numpy::ndarray::{s, Array, Array2, ArrayViewD, ArrayViewMut2, ArrayViewMut3, Axis};
3
+ use numpy::{PyArray, PyArrayDyn, PyReadwriteArrayDyn};
4
+ use pyo3::prelude::*;
5
+ use pyo3::types::PyModule;
6
+
7
+ pub mod environment;
8
+ pub mod models;
9
+ use crate::environment::gis::gis::rust_closest_gis_indices_loop;
10
+ use crate::environment::meteorology::meteorology::{rust_calculate_array_ghi_times, rust_closest_weather_indices_loop, rust_weather_in_time, rust_closest_timestamp_indices};
11
+ use crate::models::battery::battery::update_battery_array;
12
+
13
+ fn constrain_speeds(speed_limits: ArrayViewD<f64>, speeds: ArrayViewD<f64>, tick: i32) -> Vec<f64> {
14
+ let mut distance: f64 = 0.0;
15
+ static KMH_TO_MS: f64 = 1.0 / 3.6;
16
+
17
+ let ret: Vec<f64> = speeds.iter().map(| speed: &f64 | {
18
+ let speed_limit: f64 = speed_limits[distance.floor() as usize];
19
+ let vehicle_speed: f64 =f64::min(speed_limit, *speed);
20
+ distance += vehicle_speed * KMH_TO_MS * tick as f64;
21
+ vehicle_speed
22
+ }).collect();
23
+
24
+ return ret
25
+ }
26
+
27
+ /// A Python module implemented in Rust. The name of this function is the Rust module name!
28
+ #[pymodule]
29
+ #[pyo3(name = "core")]
30
+ fn rust_simulation(_py: Python, m: &PyModule) -> PyResult<()> {
31
+ #[pyfn(m)]
32
+ #[pyo3(name = "constrain_speeds")]
33
+ fn constrain_speeds_py<'py>(py: Python<'py>, x: PyReadwriteArrayDyn<'py, f64>, y: PyReadwriteArrayDyn<'py, f64>, z: i32) -> &'py PyArrayDyn<f64> {
34
+ let x = x.as_array();
35
+ let y = y.as_array();
36
+ let result = constrain_speeds(x, y, z);
37
+ return PyArray::from_vec(py, result).to_dyn();
38
+ }
39
+
40
+ #[pyfn(m)]
41
+ #[pyo3(name = "calculate_array_ghi_times")]
42
+ fn calculate_array_ghi_times<'py>(
43
+ py: Python<'py>,
44
+ python_local_times: PyReadwriteArrayDyn<'py, u64>,
45
+ ) -> (&'py PyArrayDyn<f64>, &'py PyArrayDyn<f64>) {
46
+ let local_times = python_local_times.as_array();
47
+ let (day_of_year_out, local_time_out) = rust_calculate_array_ghi_times(local_times);
48
+ let py_day_out = PyArray::from_vec(py, day_of_year_out).to_dyn();
49
+ let py_time_out = PyArray::from_vec(py, local_time_out).to_dyn();
50
+ (py_day_out, py_time_out)
51
+ }
52
+
53
+ #[pyfn(m)]
54
+ #[pyo3(name = "closest_gis_indices_loop")]
55
+ fn closest_gis_indices_loop<'py>(
56
+ py: Python<'py>,
57
+ python_cumulative_distances: PyReadwriteArrayDyn<'py, f64>,
58
+ python_average_distances: PyReadwriteArrayDyn<'py, f64>,
59
+ ) -> &'py PyArrayDyn<i64> {
60
+ let average_distances = python_average_distances.as_array();
61
+ let cumulative_distances = python_cumulative_distances.as_array();
62
+ let result = rust_closest_gis_indices_loop(cumulative_distances, average_distances);
63
+ let py_result = PyArray::from_vec(py, result).to_dyn();
64
+ py_result
65
+ }
66
+
67
+ #[pyfn(m)]
68
+ #[pyo3(name = "closest_weather_indices_loop")]
69
+ fn closest_weather_indices_loop<'py>(
70
+ py: Python<'py>,
71
+ python_cumulative_distances: PyReadwriteArrayDyn<'py, f64>,
72
+ python_average_distances: PyReadwriteArrayDyn<'py, f64>,
73
+ ) -> &'py PyArrayDyn<i64> {
74
+ let average_distances = python_average_distances.as_array();
75
+ let cumulative_distances = python_cumulative_distances.as_array();
76
+ let result = rust_closest_weather_indices_loop(cumulative_distances, average_distances);
77
+ let py_result = PyArray::from_vec(py, result).to_dyn();
78
+ py_result
79
+ }
80
+
81
+ #[pyfn(m)]
82
+ #[pyo3(name = "weather_in_time")]
83
+ fn weather_in_time<'py>(
84
+ py: Python<'py>,
85
+ python_unix_timestamps: PyReadwriteArrayDyn<'py, i64>,
86
+ python_indices: PyReadwriteArrayDyn<'py, i64>,
87
+ python_weather_forecast: PyReadwriteArrayDyn<'py, f64>,
88
+ index: u8
89
+ ) -> &'py PyArrayDyn<f64> {
90
+ let unix_timestamps = python_unix_timestamps.as_array();
91
+ let indices = python_indices.as_array();
92
+ let weather_forecast = python_weather_forecast.as_array();
93
+ let mut result = rust_weather_in_time(unix_timestamps, indices, weather_forecast, index);
94
+ let py_result = PyArray::from_array(py, &mut result).to_dyn();
95
+ py_result
96
+ }
97
+
98
+ #[pyfn(m)]
99
+ #[pyo3(name = "update_battery_array")]
100
+ fn update_battery_array_py<'py>(
101
+ py: Python<'py>,
102
+ python_delta_energy_array: PyReadwriteArrayDyn<'py, f64>,
103
+ time_step: f64,
104
+ initial_state_of_charge: f64,
105
+ initial_polarization_potential: f64,
106
+ polarization_resistance: f64,
107
+ python_internal_resistance_coeffs: PyReadwriteArrayDyn<'py, f64>,
108
+ python_open_circuit_voltage_coeffs: PyReadwriteArrayDyn<'py, f64>,
109
+ time_constant: f64,
110
+ nominal_charge_capacity: f64,
111
+ ) -> (&'py PyArrayDyn<f64>, &'py PyArrayDyn<f64>) {
112
+ let delta_energy_array = python_delta_energy_array.as_array();
113
+ let internal_resistance_coeffs = python_internal_resistance_coeffs.as_array();
114
+ let open_circuit_voltage_coeffs = python_open_circuit_voltage_coeffs.as_array();
115
+ let (soc_array, voltage_array): (Vec<f64>, Vec<f64>) = update_battery_array(
116
+ delta_energy_array,
117
+ time_step,
118
+ initial_state_of_charge,
119
+ initial_polarization_potential,
120
+ polarization_resistance,
121
+ internal_resistance_coeffs,
122
+ open_circuit_voltage_coeffs,
123
+ time_constant,
124
+ nominal_charge_capacity,
125
+ );
126
+ let py_soc_array = PyArray::from_vec(py, soc_array).to_dyn();
127
+ let py_voltage_array = PyArray::from_vec(py, voltage_array).to_dyn();
128
+ (py_soc_array, py_voltage_array)
129
+ }
130
+
131
+ Ok(())
132
+ }
@@ -0,0 +1,13 @@
1
+ from .arrays import BasicArray
2
+ from .battery import BasicBattery
3
+ from .lvs import BasicLVS
4
+ from .motor import BasicMotor
5
+ from .regen import BasicRegen
6
+
7
+ __all__ = [
8
+ "BasicArray",
9
+ "BasicBattery",
10
+ "BasicLVS",
11
+ "BasicMotor",
12
+ "BasicRegen"
13
+ ]
@@ -0,0 +1,7 @@
1
+ from .basic_array import BasicArray
2
+ from .base_array import BaseArray
3
+
4
+ __all__ = [
5
+ "BasicArray",
6
+ "BaseArray"
7
+ ]
File without changes
@@ -0,0 +1,6 @@
1
+ from abc import ABC
2
+
3
+
4
+ class BaseArray(ABC):
5
+ def __init__(self):
6
+ super().__init__()
@@ -0,0 +1,39 @@
1
+ from physics.models.arrays.base_array import BaseArray
2
+
3
+
4
+ class BasicArray(BaseArray):
5
+
6
+ # incident_sunlight:
7
+ def __init__(self, panel_efficiency, panel_size):
8
+ super().__init__()
9
+
10
+ # solar cell efficiency
11
+ self.panel_efficiency = panel_efficiency
12
+
13
+ # solar panel size in m^2
14
+ self.panel_size = panel_size
15
+
16
+ # please do not use this.
17
+ self.solar_irradiance = 1200
18
+
19
+ def calculate_produced_energy(self, solar_irradiance, tick):
20
+ """
21
+
22
+ Returns a numpy array with the energy produced by the solar panels across
23
+ each the length of each tick
24
+
25
+ :param np.ndarray solar_irradiance: (float[N]) the global horizontal irradiance(GHI) at
26
+ each moment experienced by the vehicle, in W/m2
27
+ :param float tick: (float) the duration of a time step in seconds
28
+
29
+ :returns: (float[N]) array of energy produced by the solar panels on the vehicle
30
+ in Joules
31
+ :rtype: np.ndarray
32
+
33
+ """
34
+ return solar_irradiance * self.panel_efficiency * self.panel_size * tick
35
+
36
+ def __str__(self):
37
+ return(f"BasicArray: incident_sunlight: {self.solar_irradiance}W/m^2\n"
38
+ f"BasicArray: panel_size: {self.panel_size}m^2\n"
39
+ f"BasicArray: panel_efficiency: {self.panel_efficiency * 100}%\n")
@@ -0,0 +1 @@
1
+ mod arrays;
@@ -0,0 +1,14 @@
1
+ from .base_battery import BaseBattery
2
+ from .basic_battery import BasicBattery
3
+ from .battery_model import BatteryModel
4
+ from .kalman_filter import EKF_SOC
5
+ from .battery_config import BatteryModelConfig, load_battery_config
6
+
7
+ __all__ = [
8
+ "BaseBattery",
9
+ "BasicBattery",
10
+ "BatteryModel",
11
+ "EKF_SOC",
12
+ "BatteryModelConfig",
13
+ "load_battery_config"
14
+ ]
@@ -0,0 +1,29 @@
1
+ from abc import ABC
2
+
3
+
4
+ class BaseBattery(ABC):
5
+ def __init__(self, initial_energy, max_current_capacity, max_energy_capacity,
6
+ max_voltage, min_voltage, voltage, state_of_charge):
7
+ super().__init__()
8
+
9
+ # Constants
10
+ self.max_current_capacity = max_current_capacity # max capacity of battery (Ah)
11
+ self.max_energy_capacity = max_energy_capacity # max energy inside battery (Wh)
12
+
13
+ self.max_voltage = max_voltage # maximum battery voltage (V)
14
+ self.min_voltage = min_voltage # battery cut-off voltage (V)
15
+
16
+ # Variables
17
+ self.stored_energy = initial_energy # energy inside battery (Wh)
18
+ self.state_of_charge = state_of_charge # battery state of charge
19
+ self.voltage = voltage # terminal voltage of the battery (V)
20
+
21
+ if self.state_of_charge > 0:
22
+ self.empty = False # 1 if battery is empty, 0 if battery is not empty
23
+ else:
24
+ self.empty = True
25
+
26
+ def __str__(self):
27
+ return (f"Battery stored energy: {self.stored_energy:.2f}Wh\n"
28
+ f"Battery state of charge: {self.state_of_charge * 100:.1f}%\n"
29
+ f"Battery voltage: {self.voltage:.2f}V\n")
@@ -0,0 +1,140 @@
1
+ import numpy as np
2
+ from numpy.polynomial import Polynomial
3
+ from physics.models.battery.base_battery import BaseBattery
4
+
5
+
6
+ class BasicBattery(BaseBattery):
7
+ """
8
+ Class representing the DayBreak battery pack.
9
+
10
+ Attributes:
11
+ max_voltage (float): maximum voltage of the DayBreak battery pack (V)
12
+ min_voltage (float): minimum voltage of the DayBreak battery pack (V)
13
+ max_current_capacity (float): nominal capacity of the DayBreak battery pack (Ah)
14
+ max_energy_capacity (float): nominal energy capacity of the DayBreak battery pack (Wh)
15
+
16
+ state_of_charge (float): instantaneous battery state-of-charge (0.00 - 1.00)
17
+ discharge_capacity (float): instantaneous amount of charge extracted from battery (Ah)
18
+ voltage (float): instantaneous voltage of the battery (V)
19
+ stored_energy (float): instantaneous energy stored in the battery (Wh)
20
+ """
21
+
22
+ def __init__(self, state_of_charge, max_voltage, min_voltage, max_current_capacity, max_energy_capacity):
23
+ """
24
+
25
+ Constructor for BasicBattery class.
26
+
27
+ :param float state_of_charge: initial battery state of charge
28
+
29
+ """
30
+
31
+ # ----- DayBreak battery constants -----
32
+
33
+ self.max_voltage = max_voltage
34
+ self.min_voltage = min_voltage
35
+ self.max_current_capacity = max_current_capacity
36
+ self.max_energy_capacity = max_energy_capacity
37
+
38
+ # ----- DayBreak battery equations -----
39
+
40
+ self.calculate_voltage_from_discharge_capacity = calculate_voltage_from_discharge_capacity()
41
+
42
+ self.calculate_energy_from_discharge_capacity = calculate_energy_from_discharge_capacity()
43
+
44
+ self.calculate_soc_from_discharge_capacity = calculate_soc_from_discharge_capacity(self.max_current_capacity)
45
+
46
+ self.calculate_discharge_capacity_from_soc = calculate_discharge_capacity_from_soc(self.max_current_capacity)
47
+
48
+ self.calculate_discharge_capacity_from_energy = calculate_discharge_capacity_from_energy()
49
+
50
+ # ----- DayBreak battery variables -----
51
+
52
+ self.state_of_charge = state_of_charge
53
+
54
+ # SOC -> discharge_capacity
55
+ self.discharge_capacity = self.calculate_discharge_capacity_from_soc(self.state_of_charge)
56
+
57
+ # discharge_capacity -> voltage
58
+ self.voltage = self.calculate_voltage_from_discharge_capacity(self.discharge_capacity)
59
+
60
+ # discharge_capacity -> energy
61
+ self.stored_energy = self.max_energy_capacity - self.calculate_energy_from_discharge_capacity(
62
+ self.discharge_capacity)
63
+
64
+ # ----- DayBreak battery initialisation -----
65
+
66
+ super().__init__(self.stored_energy, self.max_current_capacity, self.max_energy_capacity,
67
+ self.max_voltage, self.min_voltage, self.voltage, self.state_of_charge)
68
+
69
+ def update_array(self, cumulative_energy_array):
70
+ """
71
+ Performs energy calculations with NumPy arrays
72
+
73
+ :param cumulative_energy_array: a NumPy array containing the cumulative energy changes at each time step
74
+ experienced by the battery
75
+
76
+ :return: soc_array – a NumPy array containing the battery state of charge at each time step
77
+
78
+ :return: voltage_array – a NumPy array containing the voltage of the battery at each time step
79
+
80
+ :return: stored_energy_array– a NumPy array containing the energy stored in the battery at each time step
81
+
82
+ """
83
+
84
+ stored_energy_array = np.full_like(cumulative_energy_array, fill_value=self.stored_energy)
85
+ stored_energy_array += cumulative_energy_array / 3600
86
+ stored_energy_array = np.clip(stored_energy_array, a_min=0, a_max=self.max_energy_capacity)
87
+
88
+ energy_discharged_array = np.full_like(cumulative_energy_array, fill_value=self.max_energy_capacity) - \
89
+ stored_energy_array
90
+
91
+ discharge_capacity_array = self.calculate_discharge_capacity_from_energy(energy_discharged_array)
92
+
93
+ soc_array = self.calculate_soc_from_discharge_capacity(discharge_capacity_array)
94
+ voltage_array = self.calculate_voltage_from_discharge_capacity(discharge_capacity_array)
95
+
96
+ return soc_array, voltage_array, stored_energy_array
97
+
98
+ def get_raw_soc(self, cumulative_energy_array):
99
+ """
100
+
101
+ Return the not truncated (SOC is allowed to go above 100% and below 0%) state of charge.
102
+
103
+ :param np.ndarray cumulative_energy_array: a NumPy array containing the cumulative energy changes at each time step
104
+ experienced by the battery
105
+
106
+ :return: a NumPy array containing the battery state of charge at each time step
107
+ :rtype: np.ndarray
108
+
109
+ """
110
+
111
+ stored_energy_array = np.full_like(cumulative_energy_array, fill_value=self.stored_energy)
112
+ stored_energy_array += cumulative_energy_array / 3600
113
+
114
+ energy_discharged_array = np.full_like(cumulative_energy_array, fill_value=self.max_energy_capacity) - stored_energy_array
115
+
116
+ discharge_capacity_array = self.calculate_discharge_capacity_from_energy(energy_discharged_array)
117
+
118
+ soc_array = self.calculate_soc_from_discharge_capacity(discharge_capacity_array)
119
+
120
+ return soc_array
121
+
122
+
123
+ def calculate_voltage_from_discharge_capacity():
124
+ return Polynomial([117.6, -0.858896]) # -0.8589x + 117.6
125
+
126
+
127
+ def calculate_energy_from_discharge_capacity():
128
+ return Polynomial([0, 117.6, -0.429448]) # -0.4294x^2 + 117.6x
129
+
130
+
131
+ def calculate_soc_from_discharge_capacity(max_current_capacity):
132
+ return Polynomial([1, -1 / max_current_capacity])
133
+
134
+
135
+ def calculate_discharge_capacity_from_soc(max_current_capacity):
136
+ return Polynomial([max_current_capacity, -max_current_capacity])
137
+
138
+
139
+ def calculate_discharge_capacity_from_energy():
140
+ return lambda x: 136.92 - np.sqrt(18747.06027 - 2.32857 * x)