ubc-solar-physics 1.5.0__cp312-cp312-win_amd64.whl → 1.7.0__cp312-cp312-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.
- physics/_version.py +2 -2
- physics/environment/gis/base_gis.py +14 -0
- physics/environment/gis/gis.py +37 -3
- physics/environment/gis/gis.rs +91 -3
- physics/environment/meteorology/clouded_meteorology.py +4 -4
- physics/environment/meteorology/irradiant_meteorology.py +6 -6
- physics/lib.rs +58 -26
- physics/models/battery/__init__.py +10 -6
- physics/models/battery/battery.rs +49 -25
- physics/models/battery/battery_config.py +102 -17
- physics/models/battery/battery_config.toml +6 -8
- physics/models/battery/battery_model.py +188 -97
- physics/models/battery/kalman_filter.py +145 -263
- physics_rs/__init__.pyi +111 -0
- physics_rs.cp312-win_amd64.pyd +0 -0
- {ubc_solar_physics-1.5.0.dist-info → ubc_solar_physics-1.7.0.dist-info}/METADATA +2 -1
- {ubc_solar_physics-1.5.0.dist-info → ubc_solar_physics-1.7.0.dist-info}/RECORD +20 -19
- ubc_solar_physics-1.7.0.dist-info/top_level.txt +2 -0
- core.cp312-win_amd64.pyd +0 -0
- ubc_solar_physics-1.5.0.dist-info/top_level.txt +0 -1
- {ubc_solar_physics-1.5.0.dist-info → ubc_solar_physics-1.7.0.dist-info}/LICENSE +0 -0
- {ubc_solar_physics-1.5.0.dist-info → ubc_solar_physics-1.7.0.dist-info}/WHEEL +0 -0
@@ -1,132 +1,188 @@
|
|
1
1
|
import numpy as np
|
2
|
-
from
|
3
|
-
from
|
4
|
-
from physics.models.battery
|
2
|
+
from filterpy.kalman import ExtendedKalmanFilter
|
3
|
+
from typing import Protocol, runtime_checkable, cast, Callable
|
4
|
+
from physics.models.battery import EquivalentCircuitModelConfig
|
5
|
+
from numpy.typing import NDArray
|
5
6
|
|
6
7
|
|
7
|
-
|
8
|
-
|
8
|
+
@runtime_checkable
|
9
|
+
class FilteredBatteryModelConfig(Protocol):
|
10
|
+
"""
|
11
|
+
A specification for a configuration object which contains the requisite data to specify
|
12
|
+
a `FilteredBatteryModel`.
|
13
|
+
"""
|
14
|
+
@property
|
15
|
+
def battery_model_config(self) -> EquivalentCircuitModelConfig:
|
9
16
|
"""
|
10
|
-
|
17
|
+
Configuration of the underlying `EquivalentCircuitModel`.
|
18
|
+
"""
|
19
|
+
...
|
20
|
+
|
21
|
+
@property
|
22
|
+
def process_noise_matrix(self) -> NDArray[float]:
|
23
|
+
"""
|
24
|
+
A 2x2 matrix containing the process noise covariance matrix where [0, 0] is the SOC evolution
|
25
|
+
noise and [1, 1] is the polarization potential evolution noise.
|
26
|
+
"""
|
27
|
+
...
|
28
|
+
|
29
|
+
@property
|
30
|
+
def state_covariance_matrix(self) -> NDArray[float]:
|
31
|
+
"""
|
32
|
+
A 2x2 matrix containing the state covariance matrix where [0, 0] is the SOC covariance
|
33
|
+
noise and [1, 1] is the polarization potential covariance.
|
34
|
+
"""
|
35
|
+
...
|
36
|
+
|
37
|
+
@property
|
38
|
+
def measurement_noise_vector(self) -> NDArray[float]:
|
39
|
+
"""
|
40
|
+
A 1x1 vector containing the noise expected in the terminal voltage measurement.
|
41
|
+
"""
|
42
|
+
...
|
43
|
+
|
11
44
|
|
12
|
-
|
13
|
-
|
14
|
-
|
45
|
+
class FilteredBatteryModel:
|
46
|
+
"""
|
47
|
+
`FilteredBatteryModel` is a first-order Thevenin equivalent model of a lithium-ion battery packed, wrapped
|
48
|
+
in a Kalman filter which uses voltage measurements with model predictions.
|
49
|
+
"""
|
50
|
+
def __init__(
|
51
|
+
self,
|
52
|
+
battery_config: FilteredBatteryModelConfig,
|
53
|
+
initial_SOC: float = 1.0,
|
54
|
+
initial_Uc: float = 0.0,
|
55
|
+
alpha: float = 0.9
|
56
|
+
):
|
57
|
+
"""
|
58
|
+
:param FilteredBatteryModelConfig battery_config: Contains Kalman filter state estimation configuration and
|
59
|
+
underlying equivalent circuit model configuration.
|
60
|
+
:param float initial_SOC: Initial SOC of the battery, in the range (0, 1].
|
61
|
+
:param float initial_Uc: Initial polarization voltage of the battery in Volts.
|
15
62
|
"""
|
16
63
|
# Initial state
|
17
|
-
|
18
|
-
self.Uc = initial_Uc # Polarization Voltage
|
64
|
+
assert 0.0 <= initial_SOC <= 1.1, "`initial_SOC` must be in (0, 1.1]!"
|
19
65
|
|
20
|
-
#
|
21
|
-
self.
|
22
|
-
self.R_covariance = np.eye(1) * 0.5 # currently not really trusting the predicted state
|
66
|
+
self._SOC = initial_SOC # State of Charge
|
67
|
+
self._Uc = initial_Uc # Polarization Voltage
|
23
68
|
|
24
69
|
# Load Config data
|
25
|
-
self.
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
self.
|
47
|
-
self.
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
70
|
+
self._Q_total = battery_config.battery_model_config.Q_total
|
71
|
+
|
72
|
+
# These `cast` calls just promise to the type-checker that these will map floats to floats
|
73
|
+
self._U_oc = cast(Callable[[float], float], battery_config.battery_model_config.get_Uoc)
|
74
|
+
self._R_0 = cast(Callable[[float], float], battery_config.battery_model_config.get_R_0)
|
75
|
+
self._R_P = cast(Callable[[float], float], battery_config.battery_model_config.get_R_P)
|
76
|
+
self._C_P = cast(Callable[[float], float], battery_config.battery_model_config.get_C_P)
|
77
|
+
|
78
|
+
self._tau: Callable[[float], float] = lambda soc: self._R_P(soc) * self._C_P(soc) # Characteristic Time in s
|
79
|
+
|
80
|
+
def central_difference_derivative(func, value, h=1e-6):
|
81
|
+
"""
|
82
|
+
Compute the derivative of an arbitrary function `func` at `SOC` using central difference.
|
83
|
+
|
84
|
+
:param func: The function to differentiate.
|
85
|
+
:param value: The point at which to compute the derivative.
|
86
|
+
:param h: Step size for the finite difference.
|
87
|
+
:return: The derivative of the function at `SOC`.
|
88
|
+
"""
|
89
|
+
return (func(value + h) - func(value - h)) / (2 * h)
|
90
|
+
|
91
|
+
self._dUoc_dSOC = lambda soc: central_difference_derivative(self._U_oc, np.minimum(1.0, soc)) # dUOC wrt to SOC
|
92
|
+
self._dR0_dSOC = lambda soc: central_difference_derivative(self._R_0, np.minimum(1.0, soc)) # dR0 wrt to SOC
|
93
|
+
|
94
|
+
# Initializing the EKF object
|
95
|
+
self._ekf = ExtendedKalmanFilter(dim_x=2, dim_z=1)
|
96
|
+
|
97
|
+
# State Vector
|
98
|
+
self._ekf.x = np.array([
|
99
|
+
self._SOC,
|
100
|
+
self._Uc]
|
101
|
+
)
|
102
|
+
|
103
|
+
self._ekf.P = battery_config.state_covariance_matrix
|
104
|
+
self._ekf.Q = battery_config.process_noise_matrix
|
105
|
+
self._ekf.R = battery_config.measurement_noise_vector
|
106
|
+
|
107
|
+
assert 0 <= alpha <= 1, "`alpha` should be between 0 and 1!"
|
108
|
+
self._alpha = alpha
|
109
|
+
|
110
|
+
self._filtered_I = 0
|
111
|
+
self._predicted_measurement = 0
|
112
|
+
|
113
|
+
@property
|
114
|
+
def SOC(self) -> float:
|
54
115
|
"""
|
55
|
-
Return the current
|
116
|
+
Return the current SOC of the battery.
|
56
117
|
|
57
118
|
:return: The current state of charge.
|
58
|
-
:rtype: float
|
59
119
|
"""
|
60
|
-
return self.
|
120
|
+
return self._SOC
|
61
121
|
|
62
|
-
|
122
|
+
@property
|
123
|
+
def Uc(self) -> float:
|
63
124
|
"""
|
64
125
|
Return the polarization voltage of the battery.
|
65
126
|
|
66
127
|
:return: The current polarization voltage.
|
67
|
-
:rtype: float
|
68
128
|
"""
|
69
|
-
return self.
|
129
|
+
return self._Uc
|
70
130
|
|
71
|
-
|
131
|
+
@property
|
132
|
+
def Ut(self) -> float:
|
72
133
|
"""
|
73
134
|
Return the predicted terminal voltage for the last prediction step.
|
74
135
|
|
75
136
|
:return: The predicted terminal voltage.
|
76
|
-
:rtype: float
|
77
137
|
"""
|
78
|
-
return self.
|
79
|
-
|
80
|
-
def update_filter(self, measured_Ut,
|
138
|
+
return self._predicted_measurement
|
139
|
+
|
140
|
+
def update_filter(self, measured_Ut, current):
|
81
141
|
"""
|
82
142
|
Update the filter based on a new measurement and the predicted state.
|
83
143
|
This function should be called after `predict_state` in a typical predict-update workflow.
|
84
144
|
|
85
145
|
:param float measured_Ut: The actual voltage across the terminals of the battery.
|
86
|
-
:param float
|
146
|
+
:param float current: The current being sourced by the battery.
|
87
147
|
"""
|
88
|
-
|
89
|
-
|
90
|
-
h_jacobian = self._measurement_jacobian
|
91
|
-
Hx = self._measurement_function
|
148
|
+
# Simple low-pass filter to current
|
149
|
+
self._filtered_I = self._alpha * self._filtered_I + (1 - self._alpha) * current
|
92
150
|
|
93
|
-
self.
|
151
|
+
self._ekf.update(z=measured_Ut, HJacobian=self._measurement_jacobian, Hx=self._measurement_function)
|
94
152
|
|
95
|
-
self.
|
153
|
+
self._SOC, self._Uc = self._ekf.x
|
154
|
+
self._SOC = np.clip(self._SOC, 0.0, 1.1)
|
96
155
|
|
97
|
-
def predict_state(self,
|
156
|
+
def predict_state(self, current, time_step):
|
98
157
|
"""
|
99
158
|
Predict the next evolution of the state vector (SOC, Uc).
|
100
159
|
This function should be called before updating the filter in a typical predict-update workflow.
|
101
160
|
|
102
|
-
:param float
|
161
|
+
:param float current: The current being sourced by the battery.
|
162
|
+
Sign convention is that positive indicates current being drawn.
|
103
163
|
:param float time_step: Time elapsed between this prediction and the last updated state of the filter (seconds).
|
104
|
-
"""
|
105
|
-
check_current(I)
|
164
|
+
"""
|
106
165
|
# Control matrix B (for input current I_k)
|
107
|
-
self.
|
108
|
-
|
166
|
+
self._ekf.B = np.array([
|
167
|
+
-time_step / self._Q_total,
|
168
|
+
self._R_P(self._SOC) * (1 - np.exp(-time_step / (self._tau(self._SOC)))),
|
169
|
+
])
|
170
|
+
self._ekf.F = self._state_jacobian(time_step)
|
109
171
|
|
110
|
-
self.
|
111
|
-
|
172
|
+
self._ekf.predict(u=current)
|
173
|
+
self._SOC, self._Uc = self._ekf.x
|
112
174
|
|
113
|
-
def predict_then_update(self, measured_Ut,
|
175
|
+
def predict_then_update(self, measured_Ut: float, current: float, time_step: float):
|
114
176
|
"""
|
115
177
|
Predict the next evolution of the state vector (SOC, Uc), then update the filter
|
116
178
|
based on this prediction and a measurement. Abstracts the full predict-update workflow of the EKF.
|
117
179
|
|
118
180
|
:param float measured_Ut: The actual voltage across the terminals of the battery.
|
119
|
-
:param float
|
181
|
+
:param float current: The current being sourced by the battery. Positive indicates current being drawn.
|
120
182
|
:param float time_step: Time elapsed between this prediction and the last updated state of the filter (seconds).
|
121
183
|
"""
|
122
|
-
|
123
|
-
|
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]}')
|
184
|
+
self.predict_state(current, time_step)
|
185
|
+
self.update_filter(measured_Ut, current)
|
130
186
|
|
131
187
|
def _state_jacobian(self, time_step):
|
132
188
|
"""
|
@@ -136,7 +192,7 @@ class EKF_SOC:
|
|
136
192
|
:return: The state Jacobian matrix.
|
137
193
|
:rtype: np.ndarray
|
138
194
|
"""
|
139
|
-
return np.array([[1, 0], [0, np.exp(-time_step / self.
|
195
|
+
return np.array([[1, 0], [0, np.exp(-time_step / self._tau(self._SOC))]])
|
140
196
|
|
141
197
|
def _measurement_jacobian(self, x):
|
142
198
|
"""
|
@@ -147,195 +203,21 @@ class EKF_SOC:
|
|
147
203
|
:rtype: np.ndarray
|
148
204
|
"""
|
149
205
|
SOC = x[0]
|
150
|
-
|
151
|
-
|
206
|
+
dUoc_dSOC = self._dUoc_dSOC(SOC)
|
207
|
+
dR0_dSOC = self._dR0_dSOC(SOC)
|
208
|
+
|
209
|
+
return np.array([[dUoc_dSOC - dR0_dSOC * self._filtered_I, -1]])
|
152
210
|
|
153
|
-
def _measurement_function(self, x
|
211
|
+
def _measurement_function(self, x) -> float:
|
154
212
|
"""
|
155
213
|
Return the measurement function relating terminal voltage to SOC and polarization voltage.
|
156
214
|
|
157
215
|
:param list[float, float] x: The state vector [SOC, Uc].
|
158
|
-
:param float I: The current being sourced by the battery.
|
159
216
|
:return: The predicted terminal voltage.
|
160
|
-
:rtype: float
|
161
217
|
"""
|
162
218
|
SOC, Uc = x
|
163
|
-
|
164
|
-
|
165
|
-
self.
|
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.
|
219
|
+
Uoc = self._U_oc(SOC)
|
220
|
+
R0 = self._R_0(SOC)
|
221
|
+
self._predicted_measurement = Uoc - Uc - R0 * self._filtered_I
|
246
222
|
|
247
|
-
|
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.")
|
223
|
+
return self._predicted_measurement
|
physics_rs/__init__.pyi
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
import numpy as np
|
2
|
+
|
3
|
+
|
4
|
+
def constrain_speeds(speed_limits: np.ndarray, speeds: np.ndarray, tick: int) -> np.ndarray:
|
5
|
+
"""
|
6
|
+
Constrains the vehicle speeds based on the speed limits and computes new speeds.
|
7
|
+
|
8
|
+
:param speed_limits: Array of speed limits (km/h) for each point.
|
9
|
+
:param speeds: Array of vehicle speeds (km/h).
|
10
|
+
:param tick: The time step (in some unit, e.g., seconds or ticks).
|
11
|
+
|
12
|
+
:return: A NumPy array of constrained vehicle speeds (km/h).
|
13
|
+
"""
|
14
|
+
...
|
15
|
+
|
16
|
+
|
17
|
+
def calculate_array_ghi_times(python_local_times: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
18
|
+
"""
|
19
|
+
Calculates the array of GHI times based on local times.
|
20
|
+
|
21
|
+
:param python_local_times: Array of local times (UNIX timestamps or other format).
|
22
|
+
|
23
|
+
:return: A tuple of two NumPy arrays:
|
24
|
+
- Day of year (f64)
|
25
|
+
- Local time (f64)
|
26
|
+
"""
|
27
|
+
...
|
28
|
+
|
29
|
+
|
30
|
+
def closest_gis_indices_loop(python_cumulative_distances: np.ndarray,
|
31
|
+
python_average_distances: np.ndarray) -> np.ndarray:
|
32
|
+
"""
|
33
|
+
Finds the closest GIS indices based on cumulative and average distances.
|
34
|
+
|
35
|
+
:param python_cumulative_distances: Array of cumulative distances.
|
36
|
+
:param python_average_distances: Array of average distances.
|
37
|
+
|
38
|
+
:return: A NumPy array of indices (i64) corresponding to the closest GIS indices.
|
39
|
+
"""
|
40
|
+
...
|
41
|
+
|
42
|
+
|
43
|
+
def closest_weather_indices_loop(
|
44
|
+
python_cumulative_distances: np.ndarray,
|
45
|
+
python_average_distances: np.ndarray
|
46
|
+
) -> np.ndarray:
|
47
|
+
"""
|
48
|
+
Finds the closest weather indices based on cumulative and average distances.
|
49
|
+
|
50
|
+
:param python_cumulative_distances: Array of cumulative distances.
|
51
|
+
:param python_average_distances: Array of average distances.
|
52
|
+
|
53
|
+
:return: A NumPy array of indices (i64) corresponding to the closest weather indices.
|
54
|
+
"""
|
55
|
+
...
|
56
|
+
|
57
|
+
|
58
|
+
def weather_in_time(
|
59
|
+
python_unix_timestamps: np.ndarray,
|
60
|
+
python_indices: np.ndarray,
|
61
|
+
python_weather_forecast: np.ndarray,
|
62
|
+
index: int
|
63
|
+
) -> np.ndarray:
|
64
|
+
"""
|
65
|
+
Retrieves the weather forecast at specific times for given indices.
|
66
|
+
|
67
|
+
:param python_unix_timestamps: Array of UNIX timestamps.
|
68
|
+
:param python_indices: Array of indices to look up.
|
69
|
+
:param python_weather_forecast: Array of weather forecasts.
|
70
|
+
:param index: A specific index to look up in the weather forecast.
|
71
|
+
|
72
|
+
:return: A NumPy array of weather values (f64) at the specified times and indices.
|
73
|
+
"""
|
74
|
+
...
|
75
|
+
|
76
|
+
|
77
|
+
def update_battery_state(
|
78
|
+
python_energy_or_current_array: np.ndarray,
|
79
|
+
time_step: float,
|
80
|
+
initial_state_of_charge: float,
|
81
|
+
initial_polarization_potential: float,
|
82
|
+
python_internal_resistance_lookup: np.ndarray,
|
83
|
+
python_open_circuit_voltage_lookup: np.ndarray,
|
84
|
+
python_polarization_resistance_lookup: np.ndarray,
|
85
|
+
python_polarization_capacitance_lookup: np.ndarray,
|
86
|
+
nominal_charge_capacity: float,
|
87
|
+
is_power: bool,
|
88
|
+
quantization_step: float,
|
89
|
+
min_soc: float
|
90
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
91
|
+
"""
|
92
|
+
Updates the battery state (SOC and terminal voltage) based on energy/current input.
|
93
|
+
|
94
|
+
:param python_energy_or_current_array: Array of energy or current input values (f64).
|
95
|
+
:param time_step: Time step (f64).
|
96
|
+
:param initial_state_of_charge: Initial state of charge (f64).
|
97
|
+
:param initial_polarization_potential: Initial polarization potential (f64).
|
98
|
+
:param python_internal_resistance_lookup: Array of internal resistance values (f64).
|
99
|
+
:param python_open_circuit_voltage_lookup: Array of open-circuit voltage values (f64).
|
100
|
+
:param python_polarization_resistance_lookup: Array of polarization resistance values (f64).
|
101
|
+
:param python_polarization_capacitance_lookup: Array of polarization capacitance values (f64).
|
102
|
+
:param nominal_charge_capacity: Nominal charge capacity (f64).
|
103
|
+
:param is_power: Boolean flag to indicate if the input is power (`True`) or current (`False`).
|
104
|
+
:param quantization_step: The step size used to quantize the SOC (f64).
|
105
|
+
:param min_soc: The minimum SOC used when computing the lookup tables
|
106
|
+
|
107
|
+
:return: A tuple containing:
|
108
|
+
- An array of updated SOC values (f64).
|
109
|
+
- An array of updated terminal voltage values (f64).
|
110
|
+
"""
|
111
|
+
...
|
Binary file
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: ubc-solar-physics
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.7.0
|
4
4
|
Summary: UBC Solar's Simulation Environment
|
5
5
|
Author: Fisher Xue, Mihir Nimgade, Chris Chang, David Widjaja, Justin Hua, Ilya Veksler, Renu Rajamagesh, Ritchie Xia, Erik Langille, Chris Aung, Nicolas Ric, Ishaan Trivedi, Jason Liang, Felix Toft, Mack Wilson, Jonah Lee, Tamzeed Quazi, Joshua Riefman
|
6
6
|
Author-email: UBC Solar <strategy@ubcsolar.com>
|
@@ -73,6 +73,7 @@ Requires-Dist: pandas
|
|
73
73
|
Requires-Dist: pydantic ==2.9.2
|
74
74
|
Requires-Dist: scipy
|
75
75
|
Requires-Dist: tomli
|
76
|
+
Requires-Dist: scipy-stubs
|
76
77
|
|
77
78
|
# UBC Solar Physics
|
78
79
|
|