ubc-solar-physics 1.3.0__cp39-cp39-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.cp39-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,78 @@
1
+ use std::f64;
2
+ use numpy::ndarray::ArrayViewD;
3
+
4
+ /// Evaluate a polynomial given coefficients and an input value (x)
5
+ fn evaluate_polynomial(coefficients: &[f64], x: f64) -> f64 {
6
+ coefficients.iter().fold(0.0, |acc, &coeff| acc * x + coeff)
7
+ }
8
+
9
+ /// Evolve the battery state for a single step
10
+ fn battery_evolve(
11
+ power: f64, // Watts
12
+ tick: f64, // Seconds
13
+ state_of_charge: f64, // Dimensionless, 0 < SOC < 1
14
+ polarization_potential: f64, // Volts
15
+ polarization_resistance: f64, // Ohms
16
+ internal_resistance: f64, // Ohms
17
+ open_circuit_voltage: f64, // Volts
18
+ time_constant: f64, // Seconds
19
+ nominal_charge_capacity: f64, // Nominal charge capacity (Coulombs)
20
+ ) -> (f64, f64, f64) {
21
+ // Compute current (I) based on power input/output
22
+ let current: f64 = power / (open_circuit_voltage + polarization_potential + internal_resistance);
23
+
24
+ // Update state of charge and polarization potential
25
+ let new_state_of_charge: f64 = state_of_charge + (current * tick / nominal_charge_capacity);
26
+ let new_polarization_potential: f64 = f64::exp(-tick / time_constant) * polarization_potential
27
+ + current * polarization_resistance * (1.0 - f64::exp(-tick / time_constant));
28
+ let terminal_voltage: f64 = open_circuit_voltage + new_polarization_potential
29
+ + (current * internal_resistance); // Terminal voltage
30
+
31
+ (new_state_of_charge, new_polarization_potential, terminal_voltage)
32
+ }
33
+
34
+ pub fn update_battery_array(
35
+ delta_energy_array: ArrayViewD<'_, f64>, // W*s
36
+ tick: f64, // Seconds
37
+ initial_state_of_charge: f64, // dimensionless, 0 < SOC < 1
38
+ initial_polarization_potential: f64, // Volts
39
+ polarization_resistance: f64, // Ohms
40
+ internal_resistance_coeffs: ArrayViewD<'_, f64>, // Coefficients for internal resistance
41
+ open_circuit_voltage_coeffs: ArrayViewD<'_, f64>, // Coefficients for open-circuit voltage
42
+ time_constant: f64, // Seconds
43
+ nominal_charge_capacity: f64, // Coulombs
44
+ ) -> (Vec<f64>, Vec<f64>) {
45
+ let mut state_of_charge: f64 = initial_state_of_charge;
46
+ let mut polarization_potential: f64 = initial_polarization_potential;
47
+ let mut soc_array: Vec<f64> = Vec::with_capacity(delta_energy_array.len());
48
+ let mut voltage_array: Vec<f64> = Vec::with_capacity(delta_energy_array.len());
49
+
50
+ for &power in delta_energy_array.iter() {
51
+ // Interpolate values from coefficient
52
+ let open_circuit_voltage: f64 = evaluate_polynomial(open_circuit_voltage_coeffs.as_slice().unwrap(), state_of_charge);
53
+ let internal_resistance: f64 = evaluate_polynomial(internal_resistance_coeffs.as_slice().unwrap(), state_of_charge);
54
+
55
+ let (new_state_of_charge, new_polarization_potential, terminal_voltage) = battery_evolve(
56
+ power,
57
+ tick,
58
+ state_of_charge,
59
+ polarization_potential,
60
+ polarization_resistance,
61
+ internal_resistance,
62
+ open_circuit_voltage,
63
+ time_constant,
64
+ nominal_charge_capacity,
65
+ );
66
+
67
+ // Update state for the next iteration
68
+ state_of_charge = new_state_of_charge;
69
+ polarization_potential = new_polarization_potential;
70
+
71
+ // Store results
72
+ soc_array.push(new_state_of_charge);
73
+ voltage_array.push(terminal_voltage);
74
+ }
75
+
76
+ (soc_array, voltage_array)
77
+ }
78
+
@@ -0,0 +1,22 @@
1
+ import tomli
2
+ from pydantic import BaseModel
3
+ from typing import List
4
+ import os
5
+ import pathlib
6
+
7
+ class BatteryModelConfig(BaseModel):
8
+ R_0_data: List[float]
9
+ R_P: float
10
+ C_P: float
11
+ Q_total: float
12
+ SOC_data: List[float]
13
+ Uoc_data: List[float]
14
+ max_current_capacity: float
15
+ max_energy_capacity: float
16
+
17
+ def load_battery_config(absolute_path: str) -> BatteryModelConfig:
18
+ # Build the full path to the config file
19
+ full_path = pathlib.Path(absolute_path)
20
+ with open(full_path, 'rb') as f:
21
+ data = tomli.load(f)
22
+ return BatteryModelConfig.model_validate(data)
@@ -0,0 +1,8 @@
1
+ R_0_data = [0.002564, 0.002541, 0.002541, 0.002558, 0.002549, 0.002574, 0.002596, 0.002626, 0.002676, 0.002789]
2
+ R_P = 0.000530
3
+ C_P = 14646
4
+ Q_total = 259200
5
+ SOC_data = [0.0752, 0.1705, 0.2677, 0.366, 0.4654, 0.5666, 0.6701, 0.7767, 0.8865, 1.0]
6
+ Uoc_data = [3.481, 3.557, 3.597, 3.623, 3.660, 3.750, 3.846, 3.946, 4.056, 4.183]
7
+ max_current_capacity = 40
8
+ max_energy_capacity = 500
@@ -0,0 +1,135 @@
1
+ import numpy as np
2
+ import core
3
+ from scipy import optimize
4
+ from physics.models.battery.battery_config import BatteryModelConfig
5
+
6
+
7
+ class BatteryModel:
8
+ """
9
+ Class representing the Thevenin equivalent battery model with modular parameters.
10
+
11
+ Attributes:
12
+ R_P (float): Polarization resistance of the battery (Ohms).
13
+ C_P (float): Polarization capacitance of the battery (Farads).
14
+ max_current_capacity (float): Nominal capacity of the battery (Ah).
15
+ max_energy_capacity (float): Maximum energy capacity of the battery (Wh).
16
+ nominal_charge_capacity (float): Total charge capacity of the battery (Coulombs).
17
+ state_of_charge (float): Current state of charge (dimensionless, 0.0 to 1.0).
18
+ U_oc_coefficients (np.ndarray): Coefficients for the open-circuit voltage polynomial.
19
+ R_0_coefficients (np.ndarray): Coefficients for the ohmic resistance polynomial.
20
+ U_oc (callable): Function for open-circuit voltage as a function of state of charge (V).
21
+ R_0 (callable): Function for ohmic resistance as a function of state of charge (Ohms).
22
+ U_P (float): Current polarization potential (V).
23
+ U_L (float): Current terminal voltage (V).
24
+ tau (float): Time constant of the battery model (seconds).
25
+ """
26
+
27
+ def __init__(self, battery_config: BatteryModelConfig, state_of_charge=1):
28
+ """
29
+ Constructor for the BatteryModel class.
30
+
31
+ :param BatteryModelConfig battery_config: Configuration object containing the battery's parameters and data.
32
+ :param float state_of_charge: Initial state of charge of the battery (default is 1.0, fully charged).
33
+ """
34
+
35
+ # ----- Load Config -----
36
+
37
+ self.R_P = battery_config.R_P
38
+ self.C_P = battery_config.C_P
39
+ self.max_current_capacity = battery_config.max_current_capacity
40
+ self.max_energy_capacity = battery_config.max_energy_capacity
41
+ self.nominal_charge_capacity = battery_config.Q_total
42
+ Soc_data = battery_config.SOC_data
43
+ Uoc_data = battery_config.Uoc_data
44
+ R_0_data = battery_config.R_0_data
45
+
46
+ # ----- Initialize Parameters -----
47
+ def quintic_polynomial(x, x0, x1, x2, x3, x4):
48
+ return np.polyval(np.array([x0, x1, x2, x3, x4]), x)
49
+
50
+ self.U_oc_coefficients, _ = optimize.curve_fit(quintic_polynomial, Soc_data, Uoc_data)
51
+ self.R_0_coefficients, _ = optimize.curve_fit(quintic_polynomial, Soc_data, R_0_data)
52
+ self.U_oc = lambda soc: np.polyval(self.U_oc_coefficients, soc) # V
53
+ self.R_0 = lambda soc: np.polyval(self.R_0_coefficients, soc) # Ohms
54
+
55
+ self.U_P = 0.0 # V
56
+ self.U_L = 0.0 # V
57
+ self.state_of_charge = state_of_charge
58
+
59
+ self.tau = self.R_P * self.C_P # Characteristic Time (seconds)
60
+
61
+ # calculated the charging and discharging currents
62
+ self.discharge_current = lambda P, U_oc, U_P, R_0: ((U_oc - U_P) - np.sqrt(
63
+ np.power((U_oc - U_P), 2) - 4 * R_0 * P)) / (2 * R_0)
64
+ self.charge_current = lambda P, U_oc, U_P, R_0: (-(U_oc + U_P) + np.sqrt(
65
+ np.power((U_oc + U_P), 2) + 4 * R_0 * P)) / (2 * R_0)
66
+
67
+ def _evolve(self, power: float, tick: float) -> None:
68
+ """
69
+ Update the battery state given the power and time elapsed.
70
+
71
+ :param float power: Power applied to the battery (W). Positive for charging, negative for discharging.
72
+ :param float T: Time interval over which the power is applied (seconds).
73
+ """
74
+ soc = self.state_of_charge # State of Charge (dimensionless, 0 < soc < 1)
75
+ U_P = self.U_P # Polarization Potential (V)
76
+ R_P = self.R_P # Polarization Resistance (Ohms)
77
+ U_oc = self.U_oc(soc) # Open-Circuit Potential (V)
78
+ R_0 = self.R_0(soc) # Ohmic Resistance (Ohms)
79
+ Q = self.nominal_charge_capacity # Nominal Charge Capacity (C)
80
+
81
+ current = self.discharge_current(power, U_oc, U_P, R_0) if power <= 0 else self.charge_current(power, U_oc, U_P, R_0) # Current (A)
82
+
83
+ new_soc = soc + (current * tick / Q)
84
+ new_U_P = np.exp(-tick / self.tau) * U_P + current * R_P * (1 - np.exp(-tick / self.tau))
85
+
86
+ self.state_of_charge = new_soc
87
+ self.U_P = new_U_P
88
+ self.U_L = U_oc + U_P + (current * R_0)
89
+
90
+ def update_array(self, delta_energy_array, tick, rust=True):
91
+ """
92
+ Compute the battery's state of charge, voltage, and stored energy over time.
93
+ This function is a wrapper for the Rust-based and Python-based implementations.
94
+
95
+ :param np.ndarray delta_energy_array: Array of energy changes (J) at each time step.
96
+ :param float tick: Time interval for each step (seconds).
97
+ :param bool rust: If True, use Rust-based calculations (default is True).
98
+
99
+ :return: A tuple containing arrays for state-of-charge, voltage, and stored energy.
100
+ :rtype: tuple[np.ndarray, np.ndarray, np.ndarray]
101
+ """
102
+
103
+ if rust:
104
+ return core.update_battery_array(
105
+ delta_energy_array,
106
+ tick,
107
+ self.state_of_charge,
108
+ self.U_P,
109
+ self.R_P,
110
+ self.R_0_coefficients,
111
+ self.U_oc_coefficients,
112
+ self.tau,
113
+ self.nominal_charge_capacity
114
+ )
115
+ else:
116
+ return self._update_array_py(delta_energy_array, tick)
117
+
118
+ def _update_array_py(self, delta_energy_array, tick):
119
+ """
120
+ Perform energy calculations using Python (fallback method if Rust is disabled).
121
+
122
+ :param np.ndarray delta_energy_array: Array of energy changes (J) at each time step.
123
+ :param float tick: Time interval for each step (seconds).
124
+
125
+ :return: A tuple containing arrays for state-of-charge and voltage.
126
+ :rtype: tuple[np.ndarray, np.ndarray]
127
+ """
128
+ soc = np.empty_like(delta_energy_array, dtype=float)
129
+ voltage = np.empty_like(delta_energy_array, dtype=float)
130
+ for i, energy in enumerate(delta_energy_array):
131
+ self._evolve(energy, tick)
132
+ soc[i] = self.state_of_charge
133
+ voltage[i] = self.U_L
134
+
135
+ return soc, voltage
@@ -0,0 +1,341 @@
1
+ import numpy as np
2
+ from scipy import optimize
3
+ from filterpy.kalman import ExtendedKalmanFilter as EKF
4
+ from physics.models.battery.battery_config import BatteryModelConfig
5
+
6
+
7
+ class EKF_SOC:
8
+ def __init__(self, battery_config: BatteryModelConfig, initial_SOC=1, initial_Uc=0):
9
+ """
10
+ EKF_SOC represents the Kalman filter used for predicting state of charge.
11
+
12
+ :param BatteryModelConfig battery_config: Contains the HPPC parameters of the battery model.
13
+ :param float initial_SOC: Initial state of charge of the battery (ranges from 0 to 1 inclusive, default is 1).
14
+ :param float initial_Uc: Initial polarization voltage of the battery in volts (default is 0).
15
+ """
16
+ # Initial state
17
+ self.SOC = initial_SOC
18
+ self.Uc = initial_Uc # Polarization Voltage
19
+
20
+ # Covariance Matrices
21
+ self.Q_covariance = np.eye(2) * 0.0001
22
+ self.R_covariance = np.eye(1) * 0.5 # currently not really trusting the predicted state
23
+
24
+ # Load Config data
25
+ self.R_P = battery_config.R_P
26
+ self.C_P = battery_config.C_P
27
+ self.Q_total = battery_config.Q_total
28
+ SOC_data = battery_config.SOC_data
29
+ Uoc_data = battery_config.Uoc_data
30
+ R_0_data = battery_config.R_0_data
31
+
32
+ def quintic_polynomial(x, x0, x1, x2, x3, x4):
33
+ return np.polyval([x0, x1, x2, x3, x4], x)
34
+
35
+ U_oc_coefficients, _ = optimize.curve_fit(quintic_polynomial, SOC_data, Uoc_data)
36
+ R_0_coefficients, _ = optimize.curve_fit(quintic_polynomial, SOC_data, R_0_data)
37
+ self.U_oc = lambda soc: np.polyval(U_oc_coefficients, soc) # Open-circuit voltage as a function of SOC
38
+ self.R_0 = lambda soc: np.polyval(R_0_coefficients, soc) # Resistance as a function of SOC
39
+ self.Uoc_derivative = lambda soc: np.polyval(np.polyder(U_oc_coefficients), soc) # Derivative of Uoc wrt SOC
40
+
41
+ self.tau = self.R_P / self.C_P
42
+
43
+ # initializing the ekf object
44
+ self.ekf = EKF(dim_x=2, dim_z=1)
45
+ self.ekf.x = np.array([self.SOC, self.Uc])
46
+ self.ekf.P = np.diag([1e-6, 1e-6]) # I'm keeping low uncertainty in initial SOC and Uc
47
+ self.ekf.Q = self.Q_covariance
48
+ self.ekf.R = self.R_covariance
49
+
50
+ # For logs
51
+ self.predicted_measurement = 0
52
+
53
+ def get_SOC(self):
54
+ """
55
+ Return the current state of charge of the battery.
56
+
57
+ :return: The current state of charge.
58
+ :rtype: float
59
+ """
60
+ return self.SOC
61
+
62
+ def get_Uc(self):
63
+ """
64
+ Return the polarization voltage of the battery.
65
+
66
+ :return: The current polarization voltage.
67
+ :rtype: float
68
+ """
69
+ return self.Uc
70
+
71
+ def get_predicted_Ut(self):
72
+ """
73
+ Return the predicted terminal voltage for the last prediction step.
74
+
75
+ :return: The predicted terminal voltage.
76
+ :rtype: float
77
+ """
78
+ return self.predicted_measurement
79
+
80
+ def update_filter(self, measured_Ut, I):
81
+ """
82
+ Update the filter based on a new measurement and the predicted state.
83
+ This function should be called after `predict_state` in a typical predict-update workflow.
84
+
85
+ :param float measured_Ut: The actual voltage across the terminals of the battery.
86
+ :param float I: The current being sourced by the battery.
87
+ """
88
+ check_Terminal_V(measured_Ut)
89
+
90
+ h_jacobian = self._measurement_jacobian
91
+ Hx = self._measurement_function
92
+
93
+ self.ekf.update(z=measured_Ut, HJacobian=h_jacobian, Hx=Hx, hx_args=I)
94
+
95
+ self.SOC, self.Uc = self.ekf.x
96
+
97
+ def predict_state(self, I, time_step):
98
+ """
99
+ Predict the next evolution of the state vector (SOC, Uc).
100
+ This function should be called before updating the filter in a typical predict-update workflow.
101
+
102
+ :param float I: The current being sourced by the battery. Positive indicates current being drawn.
103
+ :param float time_step: Time elapsed between this prediction and the last updated state of the filter (seconds).
104
+ """
105
+ check_current(I)
106
+ # Control matrix B (for input current I_k)
107
+ self.ekf.B = np.array([-time_step / self.Q_total, self.R_P * (1 - np.exp(-time_step / self.tau))])
108
+ self.ekf.F = self._state_jacobian(time_step)
109
+
110
+ self.ekf.predict(u=I)
111
+ print(f'ekf prediction: {self.ekf.x_prior}')
112
+
113
+ def predict_then_update(self, measured_Ut, I, time_step):
114
+ """
115
+ Predict the next evolution of the state vector (SOC, Uc), then update the filter
116
+ based on this prediction and a measurement. Abstracts the full predict-update workflow of the EKF.
117
+
118
+ :param float measured_Ut: The actual voltage across the terminals of the battery.
119
+ :param float I: The current being sourced by the battery. Positive indicates current being drawn.
120
+ :param float time_step: Time elapsed between this prediction and the last updated state of the filter (seconds).
121
+ """
122
+ check_current(I)
123
+ check_Terminal_V(measured_Ut)
124
+
125
+ self.predict_state(I, time_step)
126
+ print(f'predicted: {self.ekf.x_prior}')
127
+
128
+ self.update_filter(measured_Ut, I)
129
+ print(f'SOC: {self.ekf.x[0]}, Uc: {self.ekf.x[1]}')
130
+
131
+ def _state_jacobian(self, time_step):
132
+ """
133
+ Return the state Jacobian matrix for the current time step.
134
+
135
+ :param float time_step: Time elapsed between this prediction and the last updated state of the filter (seconds).
136
+ :return: The state Jacobian matrix.
137
+ :rtype: np.ndarray
138
+ """
139
+ return np.array([[1, 0], [0, np.exp(-time_step / self.tau)]])
140
+
141
+ def _measurement_jacobian(self, x):
142
+ """
143
+ Return the measurement Jacobian matrix for the current state vector.
144
+
145
+ :param list[float, float] x: The state vector [SOC, Uc].
146
+ :return: The measurement Jacobian matrix.
147
+ :rtype: np.ndarray
148
+ """
149
+ SOC = x[0]
150
+ derivative = self.Uoc_derivative(SOC)
151
+ return np.array([[derivative, -1]])
152
+
153
+ def _measurement_function(self, x, I):
154
+ """
155
+ Return the measurement function relating terminal voltage to SOC and polarization voltage.
156
+
157
+ :param list[float, float] x: The state vector [SOC, Uc].
158
+ :param float I: The current being sourced by the battery.
159
+ :return: The predicted terminal voltage.
160
+ :rtype: float
161
+ """
162
+ SOC, Uc = x
163
+ R_0 = self.R_0(SOC)
164
+ Uoc = self.U_oc(SOC)
165
+ self.predicted_measurement = Uoc - Uc - R_0 * I
166
+ return self.predicted_measurement
167
+
168
+
169
+ def check_current(I):
170
+ if not isinstance(I, (float, int)):
171
+ raise TypeError(f"Invalid type for current I: {type(I)}. Expected float or int.")
172
+ if not (-45.0 <= I <= 45.0):
173
+ raise ValueError(f"Invalid value for current (I): {I}. Must be between -45.0A and 45.0A.")
174
+
175
+
176
+ def check_Terminal_V(Ut):
177
+ if not isinstance(Ut, (float, int)):
178
+ raise TypeError(f"Invalid type for measured_Ut: {type(Ut)}. Expected float or int.")
179
+ if not (0.0 <= Ut <= 5.0):
180
+ raise ValueError(f"Invalid value for terminal voltage (measured_Ut): {Ut}. Must be between 0.0 and 5.0 volts.")
181
+
182
+ import numpy as np
183
+ from filterpy.kalman import ExtendedKalmanFilter as EKF
184
+ from physics.models.battery.battery_config import BatteryModelConfig
185
+ import numpy as np
186
+
187
+ class EKF_SOC():
188
+ def __init__(self, battery_config: BatteryModelConfig, initial_SOC = 1, initial_Uc = 0):
189
+ """
190
+ EKF_SOC represents the kalman filter used for predicting state of charge.
191
+
192
+ Attributes:
193
+ battery_model_config (BatteryModelConfig): This contains the HPPC parameters of the battery model
194
+ initial_SOC (float/int): Ranges from 0 to 1 inclusive. The initial state of charge of the battery.
195
+ initial_Uc (float/int): The initial polarization volatge of the battery. (V)
196
+ """
197
+ # Inital state
198
+ self.SOC = initial_SOC
199
+ self.Uc = initial_Uc # Polarization Volatge
200
+
201
+ # Covariance Matrices
202
+ self.Q_covariance = np.eye(2) * 0.0001
203
+ self.R_covariance = np.eye(1) * 0.5 # currently not really trusting the predicted state
204
+
205
+ # Load Config data
206
+ self.R_P = battery_config.R_P
207
+ self.C_P = battery_config.C_P
208
+ self.Q_total = battery_config.Q_total
209
+ SOC_data = np.array(battery_config.SOC_data)
210
+ Uoc_data = np.array(battery_config.Uoc_data)
211
+ R_0_data = np.array(battery_config.R_0_data)
212
+
213
+ # polynomial interpolation
214
+ self.Uoc_coefficients = np.polyfit(SOC_data, Uoc_data, 7)
215
+ self.R_0_coefficients = np.polyfit(SOC_data, R_0_data, 7)
216
+ self.Uoc_derivative_coefficients = np.polyder(self.Uoc_coefficients)
217
+
218
+ self.tau = self.R_P / self.C_P
219
+
220
+ # initializing the ekf object
221
+ self.ekf = EKF(dim_x=2, dim_z=1)
222
+ self.ekf.x = np.array([self.SOC, self.Uc])
223
+ self.ekf.P = np.diag([1e-6, 1e-6]) # I'm keeping low uncertainty in initial SOC and Uc
224
+ self.ekf.Q = self.Q_covariance
225
+ self.ekf.R = self.R_covariance
226
+
227
+ # For logs
228
+ self.predicted_measurment = 0
229
+
230
+ def get_SOC(self):
231
+ """ Return the state of charge of the battery """
232
+ return self.SOC
233
+
234
+ def get_Uc(self):
235
+ """ Return the polarization voltage of the battery """
236
+ return self.Uc
237
+
238
+ def get_predicted_Ut(self):
239
+ """ Return the predicted terminal voltage for the last prediction step """
240
+ return self.predicted_measurment
241
+
242
+ def update_filter(self, measured_Ut, I):
243
+ """
244
+ Update the filter based on a new measurment, and the predicted state.
245
+ This function should be called after predict_state in a typical predict update workflow.
246
+
247
+ Attributes:
248
+ measured_Ut (float/integer): The actual voltage across the terminals of the battery.
249
+ I (float/integer): The current being sourced by the battery
250
+ """
251
+ check_Terminal_V(measured_Ut)
252
+
253
+ h_jacobian = self._measurement_jacobian
254
+ Hx = self._measurement_function
255
+
256
+ self.ekf.update(z=measured_Ut, HJacobian=h_jacobian, Hx=Hx, hx_args=I)
257
+
258
+ self.SOC, self.Uc = self.ekf.x
259
+
260
+ def predict_state(self, I, time_step):
261
+ """
262
+ Predicts the next evolution of the state vector (SOC, Uc).
263
+ This function should be called before updating the filter in a typical predict update workflow.
264
+
265
+ Attributes:
266
+ I (float/integer): The current being sourced by the battery. Positive indicated current being drawn.
267
+ time_step (float/integer): Time elapsed between this prediction and the last updated state of filter. (Seconds)
268
+ """
269
+ check_current(I)
270
+ # Control matrix B (for input current I_k)
271
+ self.ekf.B = np.array([-time_step / self.Q_total, self.R_P * (1 - np.exp(-time_step / self.tau))])
272
+ self.ekf.F = self._state_jacobian(time_step)
273
+
274
+ self.ekf.predict(u=I)
275
+ print(f'ekf prediction: {self.ekf.x_prior}')
276
+
277
+ def predict_then_update(self, measured_Ut, I, time_step):
278
+ """
279
+ Predicts the next evolution of the state vector (SOC, Uc), then updates the filter
280
+ based on this prediction and a measurement.
281
+ This function abstracts the full predict update workflow of the EKF.
282
+
283
+ Attributes:
284
+ measured_Ut (float/integer): The actual voltage across the terminals of the battery.
285
+ I (float/integer): The current being sourced by the battery. Positive indicated current being drawn.
286
+ time_step (float/integer): Time elapsed between this prediction and the last updated state of filter. (Seconds)
287
+ """
288
+ check_current(I)
289
+ check_Terminal_V(measured_Ut)
290
+
291
+ self.predict_state(I, time_step)
292
+ print(f'predicted: {self.ekf.x_prior}')
293
+
294
+ self.update_filter(measured_Ut, I)
295
+ print(f'SOC: {self.ekf.x[0]}, Uc: {self.ekf.x[1]}')
296
+
297
+ def _state_jacobian(self, time_step):
298
+ """
299
+ Returns the state jacobian for the current time
300
+
301
+ Attributes:
302
+ time_step (float/integer): Time elapsed between this prediction and the last updated state of filter. (Seconds)
303
+ """
304
+ return np.array([[1, 0], [0, np.exp(-time_step / self.tau)]])
305
+
306
+ def _measurement_jacobian(self, x):
307
+ """
308
+ Returns the measurement jacobian for the current time
309
+
310
+ Attributes:
311
+ x [float, float]: The state vector [SOC, Uc], where both values are floats or integers.
312
+ """
313
+ SOC = x[0]
314
+ derivative = np.polyval(self.Uoc_derivative_coefficients, SOC)
315
+ return np.array([[derivative, -1]])
316
+
317
+ def _measurement_function(self, x, I):
318
+ """
319
+ The customized measurement equation relating Ut to SOC and Uc
320
+
321
+ Attributes:
322
+ x [float, float]: The state vector [SOC, Uc], where both values are floats or integers.
323
+ I (float/integer): The current being sourced by the battery. Positive indicated current being drawn.
324
+ """
325
+ SOC, Uc = x
326
+ R_0 = np.polyval(self.R_0_coefficients, SOC)
327
+ Uoc = np.polyval(self.Uoc_coefficients, SOC)
328
+ self.predicted_measurment = Uoc - Uc - R_0*I
329
+ return self.predicted_measurment
330
+
331
+ def check_current(I):
332
+ if not isinstance(I, (float, int)):
333
+ raise TypeError(f"Invalid type for current I: {type(I)}. Expected float or int.")
334
+ if not (-45.0 <= I <= 45.0):
335
+ raise ValueError(f"Invalid value for current (I): {I}. Must be between -45.0A and 45.0A.")
336
+
337
+ def check_Terminal_V(Ut):
338
+ if not isinstance(Ut, (float, int)):
339
+ raise TypeError(f"Invalid type for measured_Ut: {type(Ut)}. Expected float or int.")
340
+ if not (0.0 <= Ut <= 5.0):
341
+ raise ValueError(f"Invalid value for terminal voltage (measured_Ut): {Ut}. Must be between 0.0 and 5.0 volts.")
@@ -0,0 +1 @@
1
+ pub mod battery;
@@ -0,0 +1,23 @@
1
+ # Radius of the Earth (m)
2
+ EARTH_RADIUS = 6371009
3
+
4
+ # Acceleration caused by gravity (m/s^2)
5
+ ACCELERATION_G = 9.81
6
+
7
+ # Density of Air at 15C and 101kPa (kg/m^3)
8
+ AIR_DENSITY = 1.225
9
+
10
+ # Maximum number of waypoints that can be given to generate route data
11
+ MAX_WAYPOINTS = 10
12
+
13
+ # Solar Irradiance (W/m^2)
14
+ SOLAR_IRRADIANCE = 1353
15
+
16
+ # As we currently have a limited number of API calls(60) every minute with the
17
+ # current Weather API, we must shrink the dataset significantly. As the
18
+ # OpenWeatherAPI models have a resolution of between 2.5 - 70 km, we will
19
+ # go for a resolution of 25km. Assuming we travel at 100km/h for 12 hours,
20
+ # 1200 kilometres/25 = 48 API calls
21
+ # As the Google Maps API has a resolution of around 40m between points,
22
+ # for ASC, we must cull at 625:1 (because 25,000m / 40m = 625)
23
+ REDUCTION_FACTOR = 625
@@ -0,0 +1,7 @@
1
+ from .base_lvs import BaseLVS
2
+ from .basic_lvs import BasicLVS
3
+
4
+ __all__ = [
5
+ "BaseLVS",
6
+ "BasicLVS"
7
+ ]
@@ -0,0 +1,6 @@
1
+ from abc import ABC
2
+
3
+
4
+ class BaseLVS(ABC):
5
+ def __init__(self, consumed_energy):
6
+ super().__init__()
@@ -0,0 +1,18 @@
1
+ from physics.models.lvs.base_lvs import BaseLVS
2
+
3
+
4
+ class BasicLVS(BaseLVS):
5
+
6
+ def __init__(self, consumed_energy, lvs_current, lvs_voltage):
7
+ super().__init__(consumed_energy)
8
+ self.lvs_current = lvs_current
9
+ self.lvs_voltage = lvs_voltage
10
+
11
+ def get_consumed_energy(self, tick):
12
+ """
13
+ Get the energy consumption of the Low Voltage System (current * voltage * time)
14
+
15
+ :param tick - (int) tick time passed
16
+ :returns: consumed_energy - (number) value of energy consumed
17
+ """
18
+ return self.lvs_current * self.lvs_voltage * tick
File without changes
physics/models/lvs.rs ADDED
@@ -0,0 +1 @@
1
+ mod lvs;
@@ -0,0 +1,7 @@
1
+ from .base_motor import BaseMotor
2
+ from .basic_motor import BasicMotor
3
+
4
+ __all__ = [
5
+ "BaseMotor",
6
+ "BasicMotor"
7
+ ]
@@ -0,0 +1,6 @@
1
+ from abc import ABC
2
+
3
+
4
+ class BaseMotor(ABC):
5
+ def __init__(self):
6
+ super().__init__()