ubc-solar-physics 1.0.5__cp311-cp311-win_amd64.whl → 1.2.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.
core.cp311-win_amd64.pyd CHANGED
Binary file
physics/_version.py CHANGED
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '1.0.5'
16
- __version_tuple__ = version_tuple = (1, 0, 5)
15
+ __version__ = version = '1.2.0'
16
+ __version_tuple__ = version_tuple = (1, 2, 0)
@@ -1,8 +1,3 @@
1
- from .race import (
2
- Race,
3
- compile_races
4
- )
5
-
6
1
  from .gis import (
7
2
  GIS,
8
3
  )
@@ -17,6 +12,4 @@ __all__ = [
17
12
  "IrradiantMeteorology",
18
13
  "CloudedMeteorology",
19
14
  "GIS",
20
- "Race",
21
- "compile_races"
22
15
  ]
@@ -1,7 +1,6 @@
1
1
  from physics.environment.meteorology.base_meteorology import BaseMeteorology
2
2
  from physics.environment.gis.gis import calculate_path_distances
3
3
  import numpy as np
4
- from physics.environment.race import Race
5
4
  from numba import jit
6
5
  import core
7
6
  from typing import Optional
@@ -28,7 +27,7 @@ class CloudedMeteorology(BaseMeteorology):
28
27
 
29
28
  self.last_updated_time = self._weather_forecast[0, 0, 2]
30
29
 
31
- def spatially_localize(self, cumulative_distances: np.ndarray) -> None:
30
+ def spatially_localize(self, cumulative_distances: np.ndarray, simplify_weather: bool = False) -> None:
32
31
  """
33
32
 
34
33
  IMPORTANT: we only have weather coordinates for a discrete set of coordinates. However, the car could be at any
@@ -46,14 +45,14 @@ class CloudedMeteorology(BaseMeteorology):
46
45
  `get_weather_forecast_in_time()` method.
47
46
 
48
47
  :param np.ndarray cumulative_distances: NumPy Array representing cumulative distances theoretically achievable for a given input speed array
49
-
48
+ :param bool simplify_weather: enable to only use a single weather coordinate (for track races without varying weather)
50
49
  """
51
50
 
52
- # if racing FSGP, there is no need for distance calculations. We will return only the origin coordinate
51
+ # If racing a track race, there is no need for distance calculations. We will return only the origin coordinate
53
52
  # This characterizes the weather at every point along the FSGP tracks
54
53
  # with the weather at a single coordinate on the track, which is great for reducing the API calls and is a
55
54
  # reasonable assumption to make for FSGP only.
56
- if self._race.race_type == Race.FSGP:
55
+ if simplify_weather:
57
56
  self._weather_indices = np.zeros_like(cumulative_distances, dtype=int)
58
57
  return
59
58
 
@@ -1,7 +1,6 @@
1
1
  from physics.environment.meteorology.base_meteorology import BaseMeteorology
2
2
  from physics.environment.gis.gis import calculate_path_distances
3
3
  import numpy as np
4
- from physics.environment.race import Race
5
4
  import core
6
5
  from typing import Optional
7
6
 
@@ -25,16 +24,18 @@ class IrradiantMeteorology(BaseMeteorology):
25
24
 
26
25
  super().__init__()
27
26
 
28
- def spatially_localize(self, cumulative_distances: np.ndarray) -> None:
27
+ def spatially_localize(self, cumulative_distances: np.ndarray, simplify_weather: bool = False) -> None:
29
28
  """
30
29
 
31
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
+
32
33
  """
33
- # if racing FSGP, there is no need for distance calculations. We will return only the origin coordinate
34
+ # If racing a track race, there is no need for distance calculations. We will return only the origin coordinate
34
35
  # This characterizes the weather at every point along the FSGP tracks
35
36
  # with the weather at a single coordinate on the track, which is great for reducing the API calls and is a
36
37
  # reasonable assumption to make for FSGP only.
37
- if self._race.race_type == Race.FSGP:
38
+ if simplify_weather:
38
39
  self._weather_indices = np.zeros_like(cumulative_distances, dtype=int)
39
40
  return
40
41
 
physics/lib.rs CHANGED
@@ -8,6 +8,7 @@ pub mod environment;
8
8
  pub mod models;
9
9
  use crate::environment::gis::gis::rust_closest_gis_indices_loop;
10
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;
11
12
 
12
13
  fn constrain_speeds(speed_limits: ArrayViewD<f64>, speeds: ArrayViewD<f64>, tick: i32) -> Vec<f64> {
13
14
  let mut distance: f64 = 0.0;
@@ -94,5 +95,38 @@ fn rust_simulation(_py: Python, m: &PyModule) -> PyResult<()> {
94
95
  py_result
95
96
  }
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
+
97
131
  Ok(())
98
132
  }
@@ -1,7 +1,14 @@
1
1
  from .base_battery import BaseBattery
2
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
3
6
 
4
7
  __all__ = [
5
8
  "BaseBattery",
6
- "BasicBattery"
9
+ "BasicBattery",
10
+ "BatteryModel",
11
+ "EKF_SOC",
12
+ "BatteryModelConfig",
13
+ "load_battery_config"
7
14
  ]
@@ -1,6 +1,5 @@
1
1
  import numpy as np
2
2
  from numpy.polynomial import Polynomial
3
-
4
3
  from physics.models.battery.base_battery import BaseBattery
5
4
 
6
5
 
@@ -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,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,180 @@
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.")
physics/models/battery.rs CHANGED
@@ -1 +1 @@
1
- mod battery;
1
+ pub mod battery;
physics/models.rs CHANGED
@@ -1,5 +1,5 @@
1
1
  mod arrays;
2
- mod battery;
2
+ pub mod battery;
3
3
  mod lvs;
4
4
  mod motor;
5
5
  mod regen;
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: ubc-solar-physics
3
- Version: 1.0.5
3
+ Version: 1.2.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>
@@ -39,34 +39,40 @@ Classifier: Topic :: Scientific/Engineering :: Physics
39
39
  Requires-Python: >=3.9
40
40
  Description-Content-Type: text/markdown
41
41
  License-File: LICENSE
42
- Requires-Dist: backports.tarfile ==1.2.0
43
- Requires-Dist: certifi ==2024.7.4
44
- Requires-Dist: charset-normalizer ==3.3.2
45
- Requires-Dist: dill ==0.3.8
46
- Requires-Dist: haversine ==2.8.1
47
- Requires-Dist: idna ==3.7
48
- Requires-Dist: importlib-metadata ==8.2.0
49
- Requires-Dist: jaraco.classes ==3.4.0
50
- Requires-Dist: jaraco.context ==5.3.0
51
- Requires-Dist: jaraco.functools ==4.0.2
52
- Requires-Dist: keyring ==25.3.0
53
- Requires-Dist: llvmlite ==0.43.0
54
- Requires-Dist: markdown-it-py ==3.0.0
55
- Requires-Dist: mdurl ==0.1.2
56
- Requires-Dist: more-itertools ==10.4.0
57
- Requires-Dist: nh3 ==0.2.18
58
- Requires-Dist: numba ==0.60.0
59
- Requires-Dist: numpy ==2.0.1
60
- Requires-Dist: pkginfo ==1.10.0
61
- Requires-Dist: Pygments ==2.18.0
62
- Requires-Dist: readme-renderer ==44.0
63
- Requires-Dist: requests ==2.32.3
64
- Requires-Dist: requests-toolbelt ==1.0.0
65
- Requires-Dist: rfc3986 ==2.0.0
66
- Requires-Dist: rich ==13.7.1
67
- Requires-Dist: tqdm ==4.66.5
68
- Requires-Dist: urllib3 ==2.2.2
69
- Requires-Dist: zipp ==3.20.0
42
+ Requires-Dist: backports.tarfile==1.2.0
43
+ Requires-Dist: certifi==2024.7.4
44
+ Requires-Dist: charset-normalizer==3.3.2
45
+ Requires-Dist: dill==0.3.8
46
+ Requires-Dist: haversine==2.8.1
47
+ Requires-Dist: idna==3.7
48
+ Requires-Dist: importlib_metadata==8.2.0
49
+ Requires-Dist: jaraco.classes==3.4.0
50
+ Requires-Dist: jaraco.context==5.3.0
51
+ Requires-Dist: jaraco.functools==4.0.2
52
+ Requires-Dist: keyring==25.3.0
53
+ Requires-Dist: llvmlite==0.43.0
54
+ Requires-Dist: markdown-it-py==3.0.0
55
+ Requires-Dist: mdurl==0.1.2
56
+ Requires-Dist: more-itertools==10.4.0
57
+ Requires-Dist: nh3==0.2.18
58
+ Requires-Dist: numba==0.60.0
59
+ Requires-Dist: numpy==2.0.1
60
+ Requires-Dist: pkginfo==1.10.0
61
+ Requires-Dist: Pygments==2.18.0
62
+ Requires-Dist: readme_renderer==44.0
63
+ Requires-Dist: requests==2.32.3
64
+ Requires-Dist: requests-toolbelt==1.0.0
65
+ Requires-Dist: rfc3986==2.0.0
66
+ Requires-Dist: rich==13.7.1
67
+ Requires-Dist: tqdm==4.66.5
68
+ Requires-Dist: urllib3==2.2.2
69
+ Requires-Dist: zipp==3.20.0
70
+ Requires-Dist: filterpy==1.4.5
71
+ Requires-Dist: toml==0.10.2
72
+ Requires-Dist: pandas
73
+ Requires-Dist: pydantic==2.9.2
74
+ Requires-Dist: scipy
75
+ Requires-Dist: tomli
70
76
 
71
77
  # UBC Solar Physics
72
78
 
@@ -1,26 +1,25 @@
1
- core.cp311-win_amd64.pyd,sha256=vGoriverQCPxPy-B-enbNpXuDJbDkoHQXBGpPgPVLoA,366080
1
+ core.cp311-win_amd64.pyd,sha256=WyxeKn8ggdsv_5r8z62OLFhDH1bKIlQW7iwmfKyytEA,382464
2
2
  physics/__init__.py,sha256=jRV9J_eGh0vNXEfFrILqcM6xxVjyqm3XwKAg1B1IPBs,183
3
- physics/_version.py,sha256=woKIMJpGXhD8vlRlc_pXp8tq7OtKQDUDMRWt5L7aJ2g,427
3
+ physics/_version.py,sha256=DnUqy-xWN4z1XWOsowoRN4Vz2GPuDm_18GEJc3VGkWg,427
4
4
  physics/environment.rs,sha256=OghmBkvHLZvzzuVsXUmV2lR3X_tEwuB9sT2TGZLQC6E,36
5
- physics/lib.rs,sha256=lH7bim9AIVvn58XmwzLk5RqhcotvX6l0IezT6Rm41zY,4246
6
- physics/models.rs,sha256=iyhO-vQsAYrIQJHYONUvtuTqQ3HtlaTeOIZqCOH3JkU,59
7
- physics/environment/__init__.py,sha256=kT6VKLewIz8zdnEGIv-E6h9mzflXYF105b_qNn9r72Y,315
5
+ physics/lib.rs,sha256=FqnhKkotYKJCu8v1vbov2QW9s0apay7-BnEcUgxOakU,5798
6
+ physics/models.rs,sha256=747ABP-D1XKxA6X_MNh1PbmST0zsxpxhP_pEWjbR46c,63
7
+ physics/environment/__init__.py,sha256=se_LVo4aWZKcZgbbK1KwwhHG8SH2zS1g6TEPw0GOZSs,225
8
8
  physics/environment/environment.rs,sha256=-VztdV2_GSlRbyIV_Pt6gKPVxpuNXpjLgAmoervonLg,34
9
9
  physics/environment/gis.rs,sha256=9R7G0cjf5PxQAz-CSryA6-KGfrh1eSwRhJ6qF8KfjDE,12
10
10
  physics/environment/meteorology.rs,sha256=naWb7qYrtMkCE_tLAkM474fmxaufhCkyhy3TTUQ4Yw4,20
11
- physics/environment/race.py,sha256=XuZaNli-68pH0Fy2h2yVKcf4ICsFNE651VtY_aDE754,3017
12
11
  physics/environment/gis/__init__.py,sha256=SjqhVjuDbZln636zOFROq1tWPfadghkuYz8aheflyxA,96
13
12
  physics/environment/gis/base_gis.py,sha256=WJMwpuxjmHuV-dS5HwWvLxARNd7JRQyd3IBptuxNAI0,656
14
13
  physics/environment/gis/gis.py,sha256=I04ABXsNOmeikCajBtl9a5oW6NzMBPc8nG53oiIicqw,11737
15
14
  physics/environment/gis/gis.rs,sha256=jMkVmlUNl5cz7GF1QVkMNoRb58YUOe4D95EdhBJ4anM,876
16
15
  physics/environment/meteorology/__init__.py,sha256=mvjJw_0nNLIdh80F_yTaRC3Sw3oI-z1L0J5cOK_ei0k,157
17
16
  physics/environment/meteorology/base_meteorology.py,sha256=n0JsEXQLuciEatQp_S0QBXd_HQzCY5LeIQeVKgl5O-8,2487
18
- physics/environment/meteorology/clouded_meteorology.py,sha256=Vjeb0CM-6HgcGNrTTFCZJ10jKFeWBnIrM938wkPNQaQ,28135
19
- physics/environment/meteorology/irradiant_meteorology.py,sha256=IOPEKtU0J6aMZ8DixsssdARnpeKWMqlThjs6VcG80a4,5536
17
+ physics/environment/meteorology/clouded_meteorology.py,sha256=H4jqQmaf1UiARQ4T5HmWE0ChS6IJhlvumj0lfoe52cw,28241
18
+ physics/environment/meteorology/irradiant_meteorology.py,sha256=BNuINbPfNQO5dB9AE9bMMdN4IUWK5HpRmEXONSykpVc,5646
20
19
  physics/environment/meteorology/meteorology.rs,sha256=a5XlYhb34xvPKuGp1etTQZlSqm9qTd7UXuN6H0-jXfY,5142
21
20
  physics/models/__init__.py,sha256=YgSvt4iBbcoH55XskiK9uE3VXxqCh-ZoIbAWogNUK7U,268
22
21
  physics/models/arrays.rs,sha256=rtthXq7PDjL30lIt8y9L2xFAPJE5o_ltmCbOGzzOxrc,11
23
- physics/models/battery.rs,sha256=GzPA7uaF5FdewEFZL2eOt9ZeA1GzfRDQ9db_qkEpLR8,12
22
+ physics/models/battery.rs,sha256=fTL9O20fQarT_CFsmMSqVEZNe_sTejWMaAR8Fc-z_ak,16
24
23
  physics/models/constants.py,sha256=GMD4hYO1FKoni3MNPvcyYg2EKGrgKxvOnxVKlEapUEc,839
25
24
  physics/models/lvs.rs,sha256=uyJ1ZZ1Phq8cWCzr2aevCWzt8MlhCw9DO5ObUvEs8ds,8
26
25
  physics/models/motor.rs,sha256=Iya1C_YF09KMy-9N-Mt-rBf1EIAs2Bf3Q4eDvyFuAoc,10
@@ -29,10 +28,13 @@ physics/models/arrays/__init__.py,sha256=Ds36SXwtCBnoq1xDOVlqA4kMBcOqS1wa9gLTn7T
29
28
  physics/models/arrays/arrays.rs,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
29
  physics/models/arrays/base_array.py,sha256=KjTX_0MCvWSEf4irXd3P1ITsgjhFUqK1Cf0MbSQwLuk,101
31
30
  physics/models/arrays/basic_array.py,sha256=-6yj85XkuySrjnLVmLnU35A4EOatiw4QpkLcC7n1WXU,1371
32
- physics/models/battery/__init__.py,sha256=y0sM5d4aUdpHnrcx4fNRLnThOHofCcCSAxDh7OIDGTY,138
31
+ physics/models/battery/__init__.py,sha256=huRYMnfw035kqHyWeApAvdCaSf9yAQZMcQMnPutIXLE,376
33
32
  physics/models/battery/base_battery.py,sha256=yU-QopEEQ83kw4CUvJ2MEhYyj3AM3LYY_hvdZ2wwW7c,1232
34
- physics/models/battery/basic_battery.py,sha256=yFgFIXdTuNrp6E0jDYERowY9-pkTSWyoIs_Ksad2fyI,5896
35
- physics/models/battery/battery.rs,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
+ physics/models/battery/basic_battery.py,sha256=5o-7g5xflhNLKuJyeqOY-1rLIOSIy_CJ0U4GEqeQO1E,5894
34
+ physics/models/battery/battery.rs,sha256=0wIQVli7UOWgKXT96cQLWisLQKo5UE08X4B9dl09USI,3649
35
+ physics/models/battery/battery_config.py,sha256=Dsi7cXR8SL0v7aSTuihhB6il9-8h1a2P8qrGbdvlf8Q,617
36
+ physics/models/battery/battery_model.py,sha256=kHn-xOyFBzWycpd-9Wn75fzHYZ512wXOi1dUrlt3a5M,6313
37
+ physics/models/battery/kalman_filter.py,sha256=Lr7J2vwO1HwN6miNFAYCfhZiZ2j72bwmUt-vubpo0Ks,7371
36
38
  physics/models/lvs/__init__.py,sha256=ZBips6zW4Lot7SkQZMZt_OGRNUqgOfUlDtBA5lfUkM4,114
37
39
  physics/models/lvs/base_lvs.py,sha256=kVLfGd9Qwql4-6u86uwHbJoFCgYpG07r0cAR2Ngsq38,116
38
40
  physics/models/lvs/basic_lvs.py,sha256=xNXeN6RGSZkJLhtcW0n2xZU6YIOT4bKUIbOFdmh4zc0,621
@@ -45,8 +47,8 @@ physics/models/regen/__init__.py,sha256=JzyRYKwT89FQ6_p9ofCqusl2fnWGHulyiK4P4f8L
45
47
  physics/models/regen/base_regen.py,sha256=lY44jrTSHEo8Xv7hKCjo4C3Jx0PUgilyITHwQchT2bM,101
46
48
  physics/models/regen/basic_regen.py,sha256=RY730lQLJ_gKkm2wJ68t1OPTmcz9xxGmu0yBLwHCGoQ,1811
47
49
  physics/models/regen/regen.rs,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
- ubc_solar_physics-1.0.5.dist-info/LICENSE,sha256=1Vq7OikLHh7N0xsmTPHCmPkOxk1AXrMK9k1a1icQFlk,1087
49
- ubc_solar_physics-1.0.5.dist-info/METADATA,sha256=eA7lGkqgTd_9qYl4GWk6DJIYKks26ZPnL99oAS7luwQ,4975
50
- ubc_solar_physics-1.0.5.dist-info/WHEEL,sha256=nSybvzWlmdJnHiUQSY-d7V1ycwEVUTqXiTvr2eshg44,102
51
- ubc_solar_physics-1.0.5.dist-info/top_level.txt,sha256=aws060Zz-1h0Kx76JzcE1gLA_AfS1lrRtTCsyUYwDvM,8
52
- ubc_solar_physics-1.0.5.dist-info/RECORD,,
50
+ ubc_solar_physics-1.2.0.dist-info/LICENSE,sha256=1Vq7OikLHh7N0xsmTPHCmPkOxk1AXrMK9k1a1icQFlk,1087
51
+ ubc_solar_physics-1.2.0.dist-info/METADATA,sha256=nPuG2LtB0tkAdyZqiH_xYFiVCR4DmG0aFDQwPj-zBEU,5107
52
+ ubc_solar_physics-1.2.0.dist-info/WHEEL,sha256=yNnHoQL2GZYIUXm9YvoaBpFjGlUoK9qq9oqYeudrWlE,101
53
+ ubc_solar_physics-1.2.0.dist-info/top_level.txt,sha256=aws060Zz-1h0Kx76JzcE1gLA_AfS1lrRtTCsyUYwDvM,8
54
+ ubc_solar_physics-1.2.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.43.0)
2
+ Generator: setuptools (75.8.0)
3
3
  Root-Is-Purelib: false
4
4
  Tag: cp311-cp311-win_amd64
5
5
 
@@ -1,89 +0,0 @@
1
- """
2
- This class collects the constants that are related to a specific competition.
3
- """
4
- import pathlib
5
-
6
- import numpy as np
7
- import pickle
8
- import enum
9
- import json
10
- import os
11
-
12
-
13
- class Race:
14
- class RaceType(enum.Enum):
15
- ASC = "ASC"
16
- FSGP = "FSGP"
17
-
18
- def __str__(self):
19
- match self.value:
20
- case "ASC":
21
- return "ASC"
22
- case "FSGP":
23
- return "FSGP"
24
-
25
- def __reduce__(self):
26
- return self.__class__, (self.name,)
27
-
28
- def __contains__(self, item):
29
- return item == "ASC" or item == "FSGP"
30
-
31
- def __repr__(self):
32
- return str(self)
33
-
34
- ASC = RaceType.ASC
35
- FSGP = RaceType.FSGP
36
-
37
- def __init__(self, race_type: RaceType, race_constants: dict):
38
- self.race_type = race_type
39
-
40
- self.days = race_constants["days"]
41
- self.tiling = race_constants["tiling"]
42
- self.date = (race_constants["start_year"], race_constants["start_month"], race_constants["start_day"])
43
-
44
- self.race_duration = len(self.days) * 24 * 60 * 60 # Duration (s)
45
- self.driving_boolean = self.make_time_boolean("driving")
46
- self.charging_boolean = self.make_time_boolean("charging")
47
-
48
- def __str__(self):
49
- return str(self.race_type)
50
-
51
- def write(self, race_directory: pathlib.Path):
52
- with open(race_directory / f"{str(self.race_type)}.pkl", 'wb') as outfile:
53
- pickle.dump(self, outfile, protocol=pickle.HIGHEST_PROTOCOL)
54
-
55
- def make_time_boolean(self, boolean_type: str):
56
- boolean: np.ndarray = np.empty(self.race_duration, dtype=np.int8)
57
- DAY_LENGTH: int = 24 * 60 * 60 # Length of a day in seconds
58
-
59
- for tick in range(len(boolean)):
60
- day: int = tick // DAY_LENGTH # Integer division to determine how many days have passed
61
- time_of_day = tick % DAY_LENGTH # Time of day in seconds where 0 is midnight and 43200 is noon
62
- begin, end = self.days[str(day)][boolean_type]
63
-
64
- # If the time of day is between the beginning and end, then the boolean is True, else False
65
- boolean[tick] = begin <= time_of_day < end
66
-
67
- return boolean
68
-
69
-
70
- def load_race(race_type: Race.RaceType, race_directory: pathlib.Path) -> Race:
71
- with open(race_directory / f"{str(race_type)}.pkl", 'rb') as infile:
72
- return pickle.load(infile)
73
-
74
-
75
- def compile_races(config_directory: pathlib.Path, race_directory: pathlib.Path):
76
- fsgp_config_path = os.path.join(config_directory, f"settings_FSGP.json")
77
- asc_config_path = os.path.join(config_directory, f"settings_ASC.json")
78
-
79
- with open(fsgp_config_path) as f:
80
- fsgp_race_constants = json.load(f)
81
-
82
- with open(asc_config_path) as f:
83
- asc_race_constants = json.load(f)
84
-
85
- fsgp = Race(Race.FSGP, fsgp_race_constants)
86
- fsgp.write(race_directory)
87
-
88
- asc = Race(Race.ASC, asc_race_constants)
89
- asc.write(race_directory)