ubc-solar-physics 1.7.3__cp39-cp39-macosx_10_12_x86_64.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 (57) hide show
  1. physics/__init__.py +14 -0
  2. physics/_version.py +16 -0
  3. physics/environment/__init__.py +15 -0
  4. physics/environment/environment.rs +2 -0
  5. physics/environment/gis/__init__.py +7 -0
  6. physics/environment/gis/base_gis.py +38 -0
  7. physics/environment/gis/gis.py +371 -0
  8. physics/environment/gis/gis.rs +113 -0
  9. physics/environment/gis.rs +1 -0
  10. physics/environment/meteorology/__init__.py +3 -0
  11. physics/environment/meteorology/base_meteorology.py +69 -0
  12. physics/environment/meteorology/clouded_meteorology.py +600 -0
  13. physics/environment/meteorology/irradiant_meteorology.py +107 -0
  14. physics/environment/meteorology/meteorology.rs +138 -0
  15. physics/environment/meteorology.rs +1 -0
  16. physics/environment.rs +2 -0
  17. physics/lib.rs +164 -0
  18. physics/models/__init__.py +13 -0
  19. physics/models/arrays/__init__.py +7 -0
  20. physics/models/arrays/arrays.rs +0 -0
  21. physics/models/arrays/base_array.py +6 -0
  22. physics/models/arrays/basic_array.py +39 -0
  23. physics/models/arrays.rs +1 -0
  24. physics/models/battery/__init__.py +18 -0
  25. physics/models/battery/base_battery.py +29 -0
  26. physics/models/battery/basic_battery.py +140 -0
  27. physics/models/battery/battery.rs +102 -0
  28. physics/models/battery/battery_config.py +107 -0
  29. physics/models/battery/battery_config.toml +6 -0
  30. physics/models/battery/battery_model.py +226 -0
  31. physics/models/battery/kalman_filter.py +223 -0
  32. physics/models/battery.rs +1 -0
  33. physics/models/constants.py +23 -0
  34. physics/models/lvs/__init__.py +7 -0
  35. physics/models/lvs/base_lvs.py +6 -0
  36. physics/models/lvs/basic_lvs.py +18 -0
  37. physics/models/lvs/lvs.rs +0 -0
  38. physics/models/lvs.rs +1 -0
  39. physics/models/motor/__init__.py +9 -0
  40. physics/models/motor/advanced_motor.py +196 -0
  41. physics/models/motor/base_motor.py +8 -0
  42. physics/models/motor/basic_motor.py +193 -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 +52 -0
  48. physics/models/regen/regen.rs +0 -0
  49. physics/models/regen.rs +1 -0
  50. physics/models.rs +5 -0
  51. physics_rs/__init__.pyi +111 -0
  52. physics_rs.cpython-39-darwin.so +0 -0
  53. ubc_solar_physics-1.7.3.dist-info/LICENSE +21 -0
  54. ubc_solar_physics-1.7.3.dist-info/METADATA +142 -0
  55. ubc_solar_physics-1.7.3.dist-info/RECORD +57 -0
  56. ubc_solar_physics-1.7.3.dist-info/WHEEL +5 -0
  57. ubc_solar_physics-1.7.3.dist-info/top_level.txt +2 -0
@@ -0,0 +1,223 @@
1
+ import numpy as np
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
6
+
7
+
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:
16
+ """
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
+
44
+
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.
62
+ """
63
+ # Initial state
64
+ assert 0.0 <= initial_SOC <= 1.1, "`initial_SOC` must be in (0, 1.1]!"
65
+
66
+ self._SOC = initial_SOC # State of Charge
67
+ self._Uc = initial_Uc # Polarization Voltage
68
+
69
+ # Load Config data
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:
115
+ """
116
+ Return the current SOC of the battery.
117
+
118
+ :return: The current state of charge.
119
+ """
120
+ return self._SOC
121
+
122
+ @property
123
+ def Uc(self) -> float:
124
+ """
125
+ Return the polarization voltage of the battery.
126
+
127
+ :return: The current polarization voltage.
128
+ """
129
+ return self._Uc
130
+
131
+ @property
132
+ def Ut(self) -> float:
133
+ """
134
+ Return the predicted terminal voltage for the last prediction step.
135
+
136
+ :return: The predicted terminal voltage.
137
+ """
138
+ return self._predicted_measurement
139
+
140
+ def update_filter(self, measured_Ut, current):
141
+ """
142
+ Update the filter based on a new measurement and the predicted state.
143
+ This function should be called after `predict_state` in a typical predict-update workflow.
144
+
145
+ :param float measured_Ut: The actual voltage across the terminals of the battery.
146
+ :param float current: The current being sourced by the battery.
147
+ """
148
+ # Simple low-pass filter to current
149
+ self._filtered_I = self._alpha * self._filtered_I + (1 - self._alpha) * current
150
+
151
+ self._ekf.update(z=measured_Ut, HJacobian=self._measurement_jacobian, Hx=self._measurement_function)
152
+
153
+ self._SOC, self._Uc = self._ekf.x
154
+ self._SOC = np.clip(self._SOC, 0.0, 1.1)
155
+
156
+ def predict_state(self, current, time_step):
157
+ """
158
+ Predict the next evolution of the state vector (SOC, Uc).
159
+ This function should be called before updating the filter in a typical predict-update workflow.
160
+
161
+ :param float current: The current being sourced by the battery.
162
+ Sign convention is that positive indicates current being drawn.
163
+ :param float time_step: Time elapsed between this prediction and the last updated state of the filter (seconds).
164
+ """
165
+ # Control matrix B (for input current I_k)
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)
171
+
172
+ self._ekf.predict(u=current)
173
+ self._SOC, self._Uc = self._ekf.x
174
+
175
+ def predict_then_update(self, measured_Ut: float, current: float, time_step: float):
176
+ """
177
+ Predict the next evolution of the state vector (SOC, Uc), then update the filter
178
+ based on this prediction and a measurement. Abstracts the full predict-update workflow of the EKF.
179
+
180
+ :param float measured_Ut: The actual voltage across the terminals of the battery.
181
+ :param float current: The current being sourced by the battery. Positive indicates current being drawn.
182
+ :param float time_step: Time elapsed between this prediction and the last updated state of the filter (seconds).
183
+ """
184
+ self.predict_state(current, time_step)
185
+ self.update_filter(measured_Ut, current)
186
+
187
+ def _state_jacobian(self, time_step):
188
+ """
189
+ Return the state Jacobian matrix for the current time step.
190
+
191
+ :param float time_step: Time elapsed between this prediction and the last updated state of the filter (seconds).
192
+ :return: The state Jacobian matrix.
193
+ :rtype: np.ndarray
194
+ """
195
+ return np.array([[1, 0], [0, np.exp(-time_step / self._tau(self._SOC))]])
196
+
197
+ def _measurement_jacobian(self, x):
198
+ """
199
+ Return the measurement Jacobian matrix for the current state vector.
200
+
201
+ :param list[float, float] x: The state vector [SOC, Uc].
202
+ :return: The measurement Jacobian matrix.
203
+ :rtype: np.ndarray
204
+ """
205
+ SOC = x[0]
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]])
210
+
211
+ def _measurement_function(self, x) -> float:
212
+ """
213
+ Return the measurement function relating terminal voltage to SOC and polarization voltage.
214
+
215
+ :param list[float, float] x: The state vector [SOC, Uc].
216
+ :return: The predicted terminal voltage.
217
+ """
218
+ SOC, Uc = x
219
+ Uoc = self._U_oc(SOC)
220
+ R0 = self._R_0(SOC)
221
+ self._predicted_measurement = Uoc - Uc - R0 * self._filtered_I
222
+
223
+ return self._predicted_measurement
@@ -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,9 @@
1
+ from .base_motor import BaseMotor
2
+ from .basic_motor import BasicMotor
3
+ from .advanced_motor import AdvancedMotor
4
+
5
+ __all__ = [
6
+ "BaseMotor",
7
+ "BasicMotor",
8
+ "AdvancedMotor",
9
+ ]
@@ -0,0 +1,196 @@
1
+ import numpy as np
2
+ from haversine import haversine, Unit
3
+ from numpy.typing import NDArray
4
+ from physics.models.motor import BasicMotor
5
+
6
+
7
+ class AdvancedMotor(BasicMotor):
8
+ def __init__(self, **kwargs):
9
+ super().__init__(**kwargs)
10
+ self.cornering_coefficient = 15 # tuned to Day 1 and 3 FSGP data
11
+
12
+ def calculate_energy_in(self, required_speed_kmh, gradients, wind_speeds, tick, coords):
13
+ """
14
+ A function which takes in array of elevation, array of wind speed, required
15
+ speed, returns the consumed energy.
16
+
17
+ :param np.ndarray required_speed_kmh: (float[N]) required speed array in km/h
18
+ :param np.ndarray gradients: (float[N]) gradient at parts of the road
19
+ :param np.ndarray wind_speeds: (float[N]) speeds of wind in m/s, where > 0 means against the direction of the vehicle
20
+ :param float tick: length of 1 update cycle in seconds
21
+ :param np.ndarray coords: ([float[N,2]) The lat,lon coordinate of the car at each tick
22
+ :returns: (float[N]) energy expended by the motor at every tick
23
+ :rtype: np.ndarray
24
+
25
+ """
26
+ net_force, required_angular_speed_rads = self.calculate_net_force(required_speed_kmh, wind_speeds, gradients)
27
+
28
+ cornering_work = self.calculate_cornering_losses(required_speed_kmh, coords, tick)
29
+
30
+ motor_output_energies = required_angular_speed_rads * net_force * self.tire_radius * tick + cornering_work
31
+ motor_output_energies = np.clip(motor_output_energies, a_min=0, a_max=None)
32
+
33
+ e_m = self.calculate_motor_efficiency(required_angular_speed_rads, motor_output_energies, tick)
34
+ e_mc = self.calculate_motor_controller_efficiency(required_angular_speed_rads, motor_output_energies, tick)
35
+
36
+ motor_controller_input_energies = motor_output_energies / (e_m * e_mc)
37
+
38
+ # Filter out and replace negative energy consumption as 0
39
+ motor_controller_input_energies = np.where(motor_controller_input_energies > 0,
40
+ motor_controller_input_energies, 0)
41
+
42
+ return motor_controller_input_energies
43
+
44
+ def calculate_cornering_losses(self, required_speed_kmh, coords, tick):
45
+ """
46
+ Calculate the energy losses due to cornering based on vehicle speed and trajectory.
47
+
48
+ :param np.ndarray required_speed_kmh: (float[N]) Required speed array in km/h
49
+ :param np.ndarray coords: (float[N, 2]) Array containing latitude and longitude coordinates of the car at each tick
50
+ :param float tick: Length of one update cycle in seconds
51
+ :returns: (float[N]) Energy loss due to cornering at each tick
52
+ :rtype: np.ndarray
53
+ """
54
+ required_speed_ms = required_speed_kmh / 3.6
55
+ cornering_radii = self.calculate_radii(coords)
56
+
57
+ centripetal_lateral_force = self.vehicle_mass * (required_speed_ms ** 2) / cornering_radii
58
+ centripetal_lateral_force = np.clip(centripetal_lateral_force, a_min=0, a_max=10000)
59
+
60
+ slip_angles_degrees = self.get_slip_angle_for_tire_force(centripetal_lateral_force)
61
+ slip_angles_radians = np.radians(slip_angles_degrees)
62
+ slip_distances = np.tan(slip_angles_radians) * required_speed_ms * tick
63
+
64
+ return slip_distances * centripetal_lateral_force * self.cornering_coefficient
65
+
66
+ def calculate_radii(self, coords):
67
+ """
68
+ Calculate the cornering radii for a given set of waypoints.
69
+
70
+ :param np.ndarray coords: (float[N, 2]) Array containing latitude and longitude coordinates of the car's path
71
+ :returns: (float[N]) Array of cornering radii at each waypoint
72
+ :rtype: np.ndarray
73
+ """
74
+
75
+ # pop off last coordinate if first and last coordinate are the same
76
+ repeated_last_coordinate = False
77
+ if np.array_equal(coords[0], coords[len(coords) - 1]):
78
+ coords = coords[:-1]
79
+ repeated_last_coordinate = True
80
+
81
+ cornering_radii = np.empty(len(coords))
82
+ for i in range(len(coords)):
83
+ # if the next point or previous point is out of bounds, wrap the index around the array
84
+ i2 = (i - 1) % len(coords)
85
+ i3 = (i + 1) % len(coords)
86
+ current_point = coords[i]
87
+ previous_point = coords[i2]
88
+ next_point = coords[i3]
89
+
90
+ x1 = 0
91
+ y1 = 0
92
+ x2, y2 = self.calculate_meter_distance(current_point, previous_point)
93
+ x3, y3 = self.calculate_meter_distance(current_point, next_point)
94
+ cornering_radii[i] = self.radius_of_curvature(x1, y1, x2, y2, x3, y3)
95
+
96
+ # If the last coordinate was removed, duplicate the first radius value to the end of the array
97
+ if repeated_last_coordinate:
98
+ cornering_radii = np.append(cornering_radii, cornering_radii[0])
99
+
100
+ # ensure that super large radii are bounded by a large number, like 10000
101
+ cornering_radii = np.where(np.isnan(cornering_radii), 10000, cornering_radii)
102
+ cornering_radii = np.where(cornering_radii > 10000, 10000, cornering_radii)
103
+
104
+ return cornering_radii
105
+
106
+ def generate_slip_angle_lookup(self, min_degrees, max_degrees, num_elements):
107
+ """
108
+ Generate a lookup table of slip angles and corresponding tire forces using Pacejka's Magic Formula.
109
+
110
+ https://www.edy.es/dev/docs/pacejka-94-parameters-explained-a-comprehensive-guide/
111
+
112
+ :param float min_degrees: Minimum slip angle in degrees
113
+ :param float max_degrees: Maximum slip angle in degrees
114
+ :param int num_elements: Number of discrete elements in the lookup table
115
+ :returns: (float[num_elements], float[num_elements]) Arrays of slip angles (degrees) and corresponding tire forces (Newtons)
116
+ :rtype: tuple[np.ndarray, np.ndarray]
117
+ """
118
+
119
+ b = .25 # Stiffness
120
+ c = 2.2 # Shape
121
+ d = 2.75 # Peak
122
+ e = 1.0 # Curvature
123
+
124
+ fz = self.vehicle_mass * 9.81 # Newtons
125
+
126
+ slip_angles = np.linspace(min_degrees, max_degrees, num_elements)
127
+ tire_forces = fz * d * np.sin(
128
+ c * np.arctan(b * slip_angles - e * (b * slip_angles - np.arctan(b * slip_angles))))
129
+
130
+ return slip_angles, tire_forces
131
+
132
+ def get_slip_angle_for_tire_force(self, desired_tire_force):
133
+ slip_angles, tire_forces = self.generate_slip_angle_lookup(0, 70, 100000)
134
+
135
+ # Use the numpy interpolation function to find slip angle for the given tire force
136
+ estimated_slip_angle = np.interp(desired_tire_force, tire_forces, slip_angles)
137
+
138
+ return estimated_slip_angle
139
+
140
+ @staticmethod
141
+ def calculate_meter_distance(coord1: NDArray, coord2: NDArray):
142
+ """
143
+ Calculate the x and y distance in meters between two latitude-longitude coordinates.
144
+
145
+ :param tuple coord1: (float[2]) The (latitude, longitude) coordinates of the first point
146
+ :param tuple coord2: (float[2]) The (latitude, longitude) coordinates of the second point
147
+ :returns: (float[2]) The x (longitude) and y (latitude) distances in meters
148
+ :rtype: tuple
149
+ """
150
+ lat1, lon1 = coord1
151
+ lat2, lon2 = coord2
152
+
153
+ # Base coordinate
154
+ coord_base = (lat1, lon1)
155
+ # Coordinate for latitude difference (keep longitude the same)
156
+ coord_lat = (lat2, lon1)
157
+ # Coordinate for longitude difference (keep latitude the same)
158
+ coord_long = (lat1, lon2)
159
+
160
+ # Calculate y distance (latitude difference)
161
+ y_distance = haversine(coord_base, coord_lat, unit=Unit.METERS)
162
+ # Calculate x distance (longitude difference)
163
+ x_distance = haversine(coord_base, coord_long, unit=Unit.METERS)
164
+
165
+ if lat2 < lat1:
166
+ y_distance = -y_distance
167
+ if lon2 < lon1:
168
+ x_distance = -x_distance
169
+
170
+ return x_distance, y_distance
171
+
172
+ @staticmethod
173
+ def radius_of_curvature(x1, y1, x2, y2, x3, y3):
174
+ """
175
+ Uses the circumcircle the radius of curvature of a circle passing through three points.
176
+
177
+ :param float x1: X-coordinate of the first point
178
+ :param float y1: Y-coordinate of the first point
179
+ :param float x2: X-coordinate of the second point
180
+ :param float y2: Y-coordinate of the second point
181
+ :param float x3: X-coordinate of the third point
182
+ :param float y3: Y-coordinate of the third point
183
+ :returns: Radius of curvature of the circle passing through the three points
184
+ :rtype: float
185
+ """
186
+ numerator = np.sqrt(
187
+ ((x3 - x2) ** 2 + (y3 - y2) ** 2) *
188
+ ((x1 - x3) ** 2 + (y1 - y3) ** 2) *
189
+ ((x2 - x1) ** 2 + (y2 - y1) ** 2)
190
+ )
191
+
192
+ denominator = 2 * abs(
193
+ ((x2 - x1) * (y1 - y3) - (x1 - x3) * (y2 - y1))
194
+ )
195
+
196
+ return numerator / denominator
@@ -0,0 +1,8 @@
1
+ from abc import ABC
2
+
3
+
4
+ class BaseMotor(ABC):
5
+ def __init__(self):
6
+ super().__init__()
7
+
8
+