ubc-solar-physics 1.7.3__cp312-cp312-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.
- physics/__init__.py +14 -0
- physics/_version.py +16 -0
- physics/environment/__init__.py +15 -0
- physics/environment/environment.rs +2 -0
- physics/environment/gis/__init__.py +7 -0
- physics/environment/gis/base_gis.py +38 -0
- physics/environment/gis/gis.py +371 -0
- physics/environment/gis/gis.rs +113 -0
- physics/environment/gis.rs +1 -0
- physics/environment/meteorology/__init__.py +3 -0
- physics/environment/meteorology/base_meteorology.py +69 -0
- physics/environment/meteorology/clouded_meteorology.py +600 -0
- physics/environment/meteorology/irradiant_meteorology.py +107 -0
- physics/environment/meteorology/meteorology.rs +138 -0
- physics/environment/meteorology.rs +1 -0
- physics/environment.rs +2 -0
- physics/lib.rs +164 -0
- physics/models/__init__.py +13 -0
- physics/models/arrays/__init__.py +7 -0
- physics/models/arrays/arrays.rs +0 -0
- physics/models/arrays/base_array.py +6 -0
- physics/models/arrays/basic_array.py +39 -0
- physics/models/arrays.rs +1 -0
- physics/models/battery/__init__.py +18 -0
- physics/models/battery/base_battery.py +29 -0
- physics/models/battery/basic_battery.py +140 -0
- physics/models/battery/battery.rs +102 -0
- physics/models/battery/battery_config.py +107 -0
- physics/models/battery/battery_config.toml +6 -0
- physics/models/battery/battery_model.py +226 -0
- physics/models/battery/kalman_filter.py +223 -0
- physics/models/battery.rs +1 -0
- physics/models/constants.py +23 -0
- physics/models/lvs/__init__.py +7 -0
- physics/models/lvs/base_lvs.py +6 -0
- physics/models/lvs/basic_lvs.py +18 -0
- physics/models/lvs/lvs.rs +0 -0
- physics/models/lvs.rs +1 -0
- physics/models/motor/__init__.py +9 -0
- physics/models/motor/advanced_motor.py +196 -0
- physics/models/motor/base_motor.py +8 -0
- physics/models/motor/basic_motor.py +193 -0
- physics/models/motor/motor.rs +0 -0
- physics/models/motor.rs +1 -0
- physics/models/regen/__init__.py +7 -0
- physics/models/regen/base_regen.py +6 -0
- physics/models/regen/basic_regen.py +52 -0
- physics/models/regen/regen.rs +0 -0
- physics/models/regen.rs +1 -0
- physics/models.rs +5 -0
- physics_rs/__init__.pyi +111 -0
- physics_rs.cpython-312-darwin.so +0 -0
- ubc_solar_physics-1.7.3.dist-info/LICENSE +21 -0
- ubc_solar_physics-1.7.3.dist-info/METADATA +142 -0
- ubc_solar_physics-1.7.3.dist-info/RECORD +57 -0
- ubc_solar_physics-1.7.3.dist-info/WHEEL +5 -0
- ubc_solar_physics-1.7.3.dist-info/top_level.txt +2 -0
physics/__init__.py
ADDED
physics/_version.py
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# file generated by setuptools_scm
|
2
|
+
# don't change, don't track in version control
|
3
|
+
TYPE_CHECKING = False
|
4
|
+
if TYPE_CHECKING:
|
5
|
+
from typing import Tuple, Union
|
6
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
7
|
+
else:
|
8
|
+
VERSION_TUPLE = object
|
9
|
+
|
10
|
+
version: str
|
11
|
+
__version__: str
|
12
|
+
__version_tuple__: VERSION_TUPLE
|
13
|
+
version_tuple: VERSION_TUPLE
|
14
|
+
|
15
|
+
__version__ = version = '1.7.3'
|
16
|
+
__version_tuple__ = version_tuple = (1, 7, 3)
|
@@ -0,0 +1,38 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
import numpy as np
|
3
|
+
from numpy.typing import ArrayLike, NDArray
|
4
|
+
|
5
|
+
|
6
|
+
class BaseGIS(ABC):
|
7
|
+
@abstractmethod
|
8
|
+
def calculate_closest_gis_indices(self, cumulative_distances) -> np.ndarray:
|
9
|
+
raise NotImplementedError
|
10
|
+
|
11
|
+
@abstractmethod
|
12
|
+
def get_path_elevations(self) -> np.ndarray:
|
13
|
+
raise NotImplementedError
|
14
|
+
|
15
|
+
@abstractmethod
|
16
|
+
def get_gradients(self, gis_indices) -> np.ndarray:
|
17
|
+
raise NotImplementedError
|
18
|
+
|
19
|
+
@abstractmethod
|
20
|
+
def get_time_zones(self, gis_indices) -> np.ndarray:
|
21
|
+
raise NotImplementedError
|
22
|
+
|
23
|
+
@abstractmethod
|
24
|
+
def get_path(self) -> np.ndarray:
|
25
|
+
raise NotImplementedError
|
26
|
+
|
27
|
+
@abstractmethod
|
28
|
+
def calculate_current_heading_array(self) -> np.ndarray:
|
29
|
+
raise NotImplementedError
|
30
|
+
|
31
|
+
def calculate_driving_speeds(
|
32
|
+
self,
|
33
|
+
average_lap_speeds: ArrayLike,
|
34
|
+
simulation_dt: int,
|
35
|
+
driving_allowed: ArrayLike,
|
36
|
+
idle_time: int
|
37
|
+
) -> NDArray[float]:
|
38
|
+
raise NotImplementedError
|
@@ -0,0 +1,371 @@
|
|
1
|
+
import logging
|
2
|
+
import math
|
3
|
+
import physics_rs
|
4
|
+
import numpy as np
|
5
|
+
import sys
|
6
|
+
|
7
|
+
from numpy.typing import ArrayLike, NDArray
|
8
|
+
from tqdm import tqdm
|
9
|
+
from xml.dom import minidom
|
10
|
+
from haversine import haversine, Unit
|
11
|
+
from physics.environment.gis.base_gis import BaseGIS
|
12
|
+
|
13
|
+
|
14
|
+
class GIS(BaseGIS):
|
15
|
+
def __init__(self, route_data, origin_coord, current_coord=None):
|
16
|
+
"""
|
17
|
+
|
18
|
+
Initialises a GIS (geographic location system) object. This object is responsible for getting the
|
19
|
+
simulation's planned route from the Google Maps API and performing operations on the received data.
|
20
|
+
|
21
|
+
Requires a map, ``route_data`` with certain keys.
|
22
|
+
1. "path": an iterable of shape [N, 2] representing N coordinates in the form (latitude, longitude).
|
23
|
+
2. "elevations": an iterable of shape [N] where each Nth element is the elevation, in meters, of the Nth path coordinate.
|
24
|
+
3. "time_zones": an iterable of shape [N] where each Nth element is the UTC time zone offset of the Nth path coordinate.
|
25
|
+
4. "num_unique_coords": the number of unique coordinates (that is, if the path is a single lap that has been tiled, how many path coordinates compose a single lap).
|
26
|
+
|
27
|
+
:param route_data: map of data containing "path", "elevations", "time_zones", and "num_unique_coords".
|
28
|
+
:param origin_coord: NumPy array containing the start coordinate (lat, long) of the planned travel route
|
29
|
+
|
30
|
+
"""
|
31
|
+
self.path = route_data['path']
|
32
|
+
self.launch_point = route_data['path'][0]
|
33
|
+
self.path_elevations = route_data['elevations']
|
34
|
+
self.path_time_zones = route_data['time_zones']
|
35
|
+
self.num_unique_coords = route_data['num_unique_coords']
|
36
|
+
|
37
|
+
if current_coord is not None:
|
38
|
+
if not np.array_equal(current_coord, origin_coord):
|
39
|
+
logging.warning("Current position is not origin position. Modifying path data.\n")
|
40
|
+
|
41
|
+
# We need to find the closest coordinate along the path to the vehicle position
|
42
|
+
current_coord_index = GIS._find_closest_coordinate_index(current_coord, self.path)
|
43
|
+
|
44
|
+
# All coords before the current coordinate should be discarded
|
45
|
+
self.path = self.path[current_coord_index:]
|
46
|
+
self.path_elevations = self.path_elevations[current_coord_index:]
|
47
|
+
self.path_time_zones = self.path_time_zones[current_coord_index:]
|
48
|
+
|
49
|
+
self.path_distances = calculate_path_distances(self.path)
|
50
|
+
self.path_length = np.cumsum(calculate_path_distances(self.path[:self.num_unique_coords]))[-1]
|
51
|
+
self.path_gradients = calculate_path_gradients(self.path_elevations, self.path_distances)
|
52
|
+
|
53
|
+
@staticmethod
|
54
|
+
def process_KML_file(route_file):
|
55
|
+
"""
|
56
|
+
|
57
|
+
Load the FSGP Track from a KML file exported from a Google Earth project.
|
58
|
+
|
59
|
+
Ensure to follow guidelines enumerated in this directory's `README.md` when creating and
|
60
|
+
loading new route files.
|
61
|
+
|
62
|
+
:return: Array of N coordinates (latitude, longitude) in the shape [N][2].
|
63
|
+
"""
|
64
|
+
with open(route_file) as f:
|
65
|
+
data = minidom.parse(f)
|
66
|
+
kml_coordinates = data.getElementsByTagName("coordinates")[0].childNodes[0].data
|
67
|
+
coordinates: np.ndarray = np.array(parse_coordinates_from_kml(kml_coordinates))
|
68
|
+
|
69
|
+
# Google Earth exports coordinates in order longitude, latitude, when we want the opposite
|
70
|
+
return np.roll(coordinates, 1, axis=1)
|
71
|
+
|
72
|
+
def calculate_closest_gis_indices(self, distances):
|
73
|
+
"""
|
74
|
+
|
75
|
+
Takes in an array of point distances from starting point, returns a list of
|
76
|
+
``self.path`` indices of coordinates which have a distance from the starting point
|
77
|
+
closest to the point distances.
|
78
|
+
|
79
|
+
:param np.ndarray distances: (float[N]) array of distances, where cumulative_distances[x] > cumulative_distances[x-1]
|
80
|
+
:returns: (float[N]) array of indices of path
|
81
|
+
:rtype: np.ndarray
|
82
|
+
|
83
|
+
"""
|
84
|
+
return physics_rs.closest_gis_indices_loop(distances, self.path_distances)
|
85
|
+
|
86
|
+
def calculate_driving_speeds(
|
87
|
+
self,
|
88
|
+
average_lap_speeds: ArrayLike,
|
89
|
+
simulation_dt: int,
|
90
|
+
driving_allowed: ArrayLike,
|
91
|
+
idle_time: int
|
92
|
+
) -> NDArray[float]:
|
93
|
+
"""
|
94
|
+
Generate valid driving speeds as a simulation-time array given a set of average speeds for each
|
95
|
+
simulated lap.
|
96
|
+
Driving speeds will only be non-zero when we are allowed to drive, and the speed
|
97
|
+
for every tick during a lap will be that lap's corresponding desired average speed for as long
|
98
|
+
as it takes to complete the lap.
|
99
|
+
|
100
|
+
:param average_lap_speeds: An array of average speeds in m/s, one for each simulated lap.
|
101
|
+
If there are more speeds given than laps available, the unused speeds will be silently ignored.
|
102
|
+
If there are too few, an error will be returned.
|
103
|
+
:param simulation_dt: The simulated tick length.
|
104
|
+
:param driving_allowed: A simulation-time boolean where the `True` elements are when we
|
105
|
+
are allowed to drive, and `False` is when we are not. Requires that (at least) the first element is
|
106
|
+
`False` due to the race beginning in the morning before we are allowed to drive.
|
107
|
+
:param idle_time: The length of time to pause driving upon processing a "0m/s" average speed.
|
108
|
+
:return: A simulation-time array of driving speeds in m/s, or an error if there weren't enough
|
109
|
+
laps provided to fill the entire simulation time.
|
110
|
+
"""
|
111
|
+
return physics_rs.get_driving_speeds(
|
112
|
+
np.array(average_lap_speeds).astype(np.float64),
|
113
|
+
simulation_dt,
|
114
|
+
np.array(driving_allowed).astype(bool),
|
115
|
+
self.path_length,
|
116
|
+
idle_time
|
117
|
+
)
|
118
|
+
|
119
|
+
@staticmethod
|
120
|
+
def _python_calculate_closest_gis_indices(distances, path_distances):
|
121
|
+
"""
|
122
|
+
|
123
|
+
Python implementation of use_compiled core.closest_gis_indices_loop. See parent function for documentation details.
|
124
|
+
|
125
|
+
"""
|
126
|
+
|
127
|
+
current_coordinate_index = 0
|
128
|
+
result = []
|
129
|
+
|
130
|
+
with tqdm(total=len(distances), file=sys.stdout, desc="Calculating closest GIS indices") as pbar:
|
131
|
+
distance_travelled = 0
|
132
|
+
for distance in np.nditer(distances):
|
133
|
+
distance_travelled += distance
|
134
|
+
|
135
|
+
while distance_travelled > path_distances[current_coordinate_index]:
|
136
|
+
distance_travelled -= path_distances[current_coordinate_index]
|
137
|
+
current_coordinate_index += 1
|
138
|
+
|
139
|
+
if current_coordinate_index >= len(path_distances) - 1:
|
140
|
+
current_coordinate_index = len(path_distances) - 1
|
141
|
+
|
142
|
+
result.append(current_coordinate_index)
|
143
|
+
pbar.update(1)
|
144
|
+
|
145
|
+
return np.array(result)
|
146
|
+
|
147
|
+
# ----- Getters -----
|
148
|
+
def get_time_zones(self, gis_indices):
|
149
|
+
"""
|
150
|
+
|
151
|
+
Takes in an array of path indices, returns the time zone at each index
|
152
|
+
|
153
|
+
:param np.ndarray gis_indices: (float[N]) array of path indices
|
154
|
+
:returns: (float[N]) array of time zones in seconds
|
155
|
+
:rtype: np.ndarray
|
156
|
+
|
157
|
+
"""
|
158
|
+
|
159
|
+
return self.path_time_zones[gis_indices]
|
160
|
+
|
161
|
+
def get_gradients(self, gis_indices):
|
162
|
+
"""
|
163
|
+
|
164
|
+
Takes in an array of path indices, returns the road gradient at each index
|
165
|
+
|
166
|
+
:param np.ndarray gis_indices: (float[N]) array of path indices
|
167
|
+
:returns: (float[N]) array of road gradients
|
168
|
+
:rtype np.ndarray:
|
169
|
+
|
170
|
+
"""
|
171
|
+
|
172
|
+
return self.path_gradients[gis_indices]
|
173
|
+
|
174
|
+
def get_path(self):
|
175
|
+
"""
|
176
|
+
Returns all N coordinates of the path in a NumPy array
|
177
|
+
[N][latitude, longitude]
|
178
|
+
|
179
|
+
:rtype: np.ndarray
|
180
|
+
|
181
|
+
"""
|
182
|
+
|
183
|
+
return self.path
|
184
|
+
|
185
|
+
def get_path_elevations(self):
|
186
|
+
"""
|
187
|
+
|
188
|
+
Returns all N elevations of the path in a NumPy array
|
189
|
+
[N][elevation]
|
190
|
+
|
191
|
+
:rtype: np.ndarray
|
192
|
+
|
193
|
+
"""
|
194
|
+
|
195
|
+
return self.path_elevations
|
196
|
+
|
197
|
+
def get_path_distances(self):
|
198
|
+
"""
|
199
|
+
|
200
|
+
Returns all N-1 distances of the path in a NumPy array
|
201
|
+
[N-1][elevation]
|
202
|
+
|
203
|
+
:rtype: np.ndarray
|
204
|
+
|
205
|
+
"""
|
206
|
+
|
207
|
+
return self.path_distances
|
208
|
+
|
209
|
+
def get_path_gradients(self):
|
210
|
+
"""
|
211
|
+
|
212
|
+
Returns all N-1 gradients of a path in a NumPy array
|
213
|
+
[N-1][gradient]
|
214
|
+
|
215
|
+
:rtype: np.ndarray
|
216
|
+
|
217
|
+
"""
|
218
|
+
|
219
|
+
return self.path_gradients
|
220
|
+
|
221
|
+
# ----- Path calculation functions -----
|
222
|
+
def calculate_path_min_max(self):
|
223
|
+
logging.warning(f"Using deprecated function 'calculate_path_min_max()'!")
|
224
|
+
min_lat, min_long = self.path.min(axis=0)
|
225
|
+
max_lat, max_long = self.path.max(axis=0)
|
226
|
+
return [min_long, min_lat, max_long, max_lat]
|
227
|
+
|
228
|
+
def calculate_current_heading_array(self):
|
229
|
+
"""
|
230
|
+
|
231
|
+
Calculates the bearing of the vehicle between consecutive points
|
232
|
+
https://www.movable-type.co.uk/scripts/latlong.html
|
233
|
+
|
234
|
+
:returns: array of bearings
|
235
|
+
:rtype: np.ndarray
|
236
|
+
|
237
|
+
"""
|
238
|
+
bearing_array = np.zeros(len(self.path))
|
239
|
+
|
240
|
+
for index in range(0, len(self.path) - 1):
|
241
|
+
coord_1 = np.radians(self.path[index])
|
242
|
+
coord_2 = np.radians(self.path[index + 1])
|
243
|
+
|
244
|
+
y = math.sin(coord_2[1] - coord_1[1]) \
|
245
|
+
* math.cos(coord_2[0])
|
246
|
+
|
247
|
+
x = math.cos(coord_1[0]) \
|
248
|
+
* math.sin(coord_2[0]) \
|
249
|
+
- math.sin(coord_1[0]) \
|
250
|
+
* math.cos(coord_2[0]) \
|
251
|
+
* math.cos(coord_2[1] - coord_1[1])
|
252
|
+
|
253
|
+
theta = math.atan2(y, x)
|
254
|
+
|
255
|
+
bearing_array[index] = ((theta * 180) / math.pi + 360) % 360
|
256
|
+
|
257
|
+
bearing_array[-1] = bearing_array[-2]
|
258
|
+
|
259
|
+
return bearing_array
|
260
|
+
|
261
|
+
@staticmethod
|
262
|
+
def _calculate_vector_square_magnitude(vector):
|
263
|
+
"""
|
264
|
+
|
265
|
+
Calculate the square magnitude of an input vector. Must be one-dimensional.
|
266
|
+
|
267
|
+
:param np.ndarray vector: NumPy array[N] representing a vector[N]
|
268
|
+
:return: square magnitude of the input vector
|
269
|
+
:rtype: float
|
270
|
+
|
271
|
+
"""
|
272
|
+
|
273
|
+
return sum(i ** 2 for i in vector)
|
274
|
+
|
275
|
+
@staticmethod
|
276
|
+
def _find_closest_coordinate_index(current_coord, path):
|
277
|
+
"""
|
278
|
+
|
279
|
+
Returns the closest coordinate to current_coord in path
|
280
|
+
|
281
|
+
:param np.ndarray current_coord: A NumPy array[N] representing a N-dimensional vector
|
282
|
+
:param np.ndarray path: A NumPy array[M][N] of M coordinates which should be N-dimensional vectors
|
283
|
+
:returns: index of the closest coordinate.
|
284
|
+
:rtype: int
|
285
|
+
|
286
|
+
"""
|
287
|
+
|
288
|
+
to_current_coord_from_path = np.abs(path - current_coord)
|
289
|
+
distances_from_current_coord = np.zeros(len(to_current_coord_from_path))
|
290
|
+
for i in range(len(to_current_coord_from_path)):
|
291
|
+
# As we just need the minimum, using square magnitude will save performance
|
292
|
+
distances_from_current_coord[i] = GIS._calculate_vector_square_magnitude(to_current_coord_from_path[i])
|
293
|
+
|
294
|
+
return distances_from_current_coord.argmin()
|
295
|
+
|
296
|
+
|
297
|
+
def calculate_path_distances(coords):
|
298
|
+
"""
|
299
|
+
|
300
|
+
Obtain the distance between each coordinate by approximating the spline between them
|
301
|
+
as a straight line, and use the Haversine formula (https://en.wikipedia.org/wiki/Haversine_formula)
|
302
|
+
to calculate distance between coordinates on a sphere.
|
303
|
+
|
304
|
+
:param np.ndarray coords: A NumPy array [n][latitude, longitude]
|
305
|
+
:returns path_distances: a NumPy array [n-1][distances],
|
306
|
+
:rtype: np.ndarray
|
307
|
+
|
308
|
+
"""
|
309
|
+
|
310
|
+
coords_offset = np.roll(coords, (1, 1))
|
311
|
+
path_distances = []
|
312
|
+
for u, v in zip(coords, coords_offset):
|
313
|
+
path_distances.append(haversine(u, v, unit=Unit.METERS))
|
314
|
+
|
315
|
+
return np.array(path_distances)
|
316
|
+
|
317
|
+
|
318
|
+
def parse_coordinates_from_kml(coords_str: str) -> np.ndarray:
|
319
|
+
"""
|
320
|
+
|
321
|
+
Parse a coordinates string from a XML (KML) file into a list of coordinates (2D vectors).
|
322
|
+
Requires coordinates in the format "39.,41.,0 39.,40.,0" which will return [ [39., 41.], [39., 40.] ].
|
323
|
+
|
324
|
+
:param coords_str: coordinates string from a XML (KML) file
|
325
|
+
:return: list of 2D vectors representing coordinates
|
326
|
+
:rtype: np.ndarray
|
327
|
+
|
328
|
+
"""
|
329
|
+
|
330
|
+
def parse_coord(pair):
|
331
|
+
coord = pair.split(',')
|
332
|
+
coord.pop()
|
333
|
+
coord = [float(value) for value in coord]
|
334
|
+
return coord
|
335
|
+
|
336
|
+
return list(map(parse_coord, coords_str.split()))
|
337
|
+
|
338
|
+
|
339
|
+
def calculate_path_gradients(elevations, distances):
|
340
|
+
"""
|
341
|
+
|
342
|
+
Get the approximate gradients of every point on the path.
|
343
|
+
|
344
|
+
Note:
|
345
|
+
- gradient > 0 corresponds to uphill
|
346
|
+
- gradient < 0 corresponds to downhill
|
347
|
+
|
348
|
+
:param np.ndarray elevations: [N][elevations]
|
349
|
+
:param np.ndarray distances: [N-1][distances]
|
350
|
+
:returns gradients: [N-1][gradients]
|
351
|
+
:rtype: np.ndarray
|
352
|
+
|
353
|
+
"""
|
354
|
+
|
355
|
+
# subtract every next elevation with the previous elevation to
|
356
|
+
# get the difference in elevation
|
357
|
+
# [1 2 3 4 5]
|
358
|
+
# [5 1 2 3 4] -
|
359
|
+
# -------------
|
360
|
+
# [1 1 1 1]
|
361
|
+
|
362
|
+
offset = np.roll(elevations, 1)
|
363
|
+
delta_elevations = elevations - offset
|
364
|
+
|
365
|
+
# Divide the difference in elevation to get the gradient
|
366
|
+
# gradient > 0: uphill
|
367
|
+
# gradient < 0: downhill
|
368
|
+
with np.errstate(invalid='ignore'):
|
369
|
+
gradients = delta_elevations / distances
|
370
|
+
|
371
|
+
return np.nan_to_num(gradients, nan=0.)
|
@@ -0,0 +1,113 @@
|
|
1
|
+
use numpy::ndarray::{ArrayViewD, ArrayView1};
|
2
|
+
|
3
|
+
pub fn rust_closest_gis_indices_loop(
|
4
|
+
distances: ArrayViewD<'_, f64>,
|
5
|
+
path_distances: ArrayViewD<'_, f64>,
|
6
|
+
) -> Vec<i64> {
|
7
|
+
let mut current_coord_index: usize = 0;
|
8
|
+
let mut distance_travelled: f64 = 0.0;
|
9
|
+
let mut result: Vec<i64> = Vec::with_capacity(distances.len());
|
10
|
+
|
11
|
+
for &distance in distances {
|
12
|
+
distance_travelled += distance;
|
13
|
+
|
14
|
+
while distance_travelled > path_distances[current_coord_index] {
|
15
|
+
distance_travelled -= path_distances[current_coord_index];
|
16
|
+
current_coord_index += 1;
|
17
|
+
if current_coord_index >= path_distances.len() {
|
18
|
+
current_coord_index = 0;
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
current_coord_index = std::cmp::min(current_coord_index, path_distances.len() - 1);
|
23
|
+
result.push(current_coord_index as i64);
|
24
|
+
}
|
25
|
+
|
26
|
+
result
|
27
|
+
}
|
28
|
+
|
29
|
+
///
|
30
|
+
/// Generate valid driving speeds as a simulation-time array given a set of average speeds for each
|
31
|
+
/// simulated lap.
|
32
|
+
/// Driving speeds will only be non-zero when we are allowed to drive, and the speed
|
33
|
+
/// for every tick during a lap will be that lap's corresponding desired average speed for as long
|
34
|
+
/// as it takes to complete the lap.
|
35
|
+
/// An average speed of 0m/s for a lap will be interpreted as "sit and charge" for `idle_time`
|
36
|
+
/// ticks.
|
37
|
+
///
|
38
|
+
/// # Arguments
|
39
|
+
///
|
40
|
+
/// * `average_speeds`: An array of average speeds in m/s, one for each simulated lap. If there are more
|
41
|
+
/// speeds given than laps available, the unused speeds will be silently ignored. If there are too
|
42
|
+
/// few, an error will be returned.
|
43
|
+
/// * `simulation_dt`: The simulated tick length
|
44
|
+
/// * `driving_allowed_boolean`: A simulation-time boolean where the `True` elements are when we
|
45
|
+
/// are allowed to drive, and `False` is when we are not.
|
46
|
+
/// * `track_length`: The length of the track in meters.
|
47
|
+
/// * `idle_time`: The number of ticks to "sit and charge" when desired.
|
48
|
+
///
|
49
|
+
/// Returns: A simulation-time array of driving speeds in m/s, or an error if there weren't enough
|
50
|
+
/// laps provided to fill the entire simulation time.
|
51
|
+
///
|
52
|
+
pub fn get_driving_speeds(
|
53
|
+
average_speeds: ArrayView1<'_, f64>, // Average speeds in m/s
|
54
|
+
simulation_dt: i64, // Time step in seconds
|
55
|
+
driving_allowed_boolean: ArrayView1<'_, bool>, // Simulation-time boolean array
|
56
|
+
track_length: f64, // Track length in meters
|
57
|
+
idle_time: i64 // Time to idle in seconds
|
58
|
+
) -> Result<Vec<f64>, &'static str> {
|
59
|
+
let ticks_to_complete_lap: Vec<i64> = average_speeds.iter().map(| &average_speed | {
|
60
|
+
if average_speed > 0.0 {
|
61
|
+
// The number of ticks is the number of seconds, divided by seconds per tick
|
62
|
+
(track_length / average_speed / simulation_dt as f64).ceil() as i64
|
63
|
+
} else {
|
64
|
+
(idle_time as f64 / simulation_dt as f64).ceil() as i64
|
65
|
+
}
|
66
|
+
}).collect();
|
67
|
+
|
68
|
+
let mut lap_index: usize = 0;
|
69
|
+
let mut lap_speed: f64 = average_speeds[lap_index];
|
70
|
+
|
71
|
+
let mut ticks_to_lap_completion: i64 = ticks_to_complete_lap[lap_index];
|
72
|
+
|
73
|
+
let mut driving_speeds: Vec<f64> = Vec::with_capacity(driving_allowed_boolean.len());
|
74
|
+
for driving_allowed in driving_allowed_boolean.iter() {
|
75
|
+
if !driving_allowed {
|
76
|
+
// If we aren't allowed to drive, speed should be zero. Also, we should mark that we are
|
77
|
+
// done our lap since it means we ended the day in the middle of the lap, and we will
|
78
|
+
// start the next day at the beginning of a new lap, not where we ended off.
|
79
|
+
|
80
|
+
// If it's the first lap, we don't want to skip because we are probably in the morning
|
81
|
+
// where we haven't begun driving yet.
|
82
|
+
if lap_index > 0 {
|
83
|
+
ticks_to_lap_completion = 0;
|
84
|
+
}
|
85
|
+
|
86
|
+
driving_speeds.push(0.0)
|
87
|
+
} else {
|
88
|
+
// If we are driving, we should decrement ticks to lap completion. If its already
|
89
|
+
// zero, that means that we are done the lap and should move onto the next lap.
|
90
|
+
if ticks_to_lap_completion > 0 {
|
91
|
+
ticks_to_lap_completion -= 1;
|
92
|
+
|
93
|
+
driving_speeds.push(lap_speed)
|
94
|
+
} else {
|
95
|
+
// To advance to the next lap, increment the index and evaluate new variables
|
96
|
+
lap_index += 1;
|
97
|
+
if lap_index >= average_speeds.len() {
|
98
|
+
return Err("Not enough average speeds!")
|
99
|
+
}
|
100
|
+
|
101
|
+
// We subtract 1 since this iteration counts for the next lap, not the one
|
102
|
+
// that we just finished
|
103
|
+
ticks_to_lap_completion = ticks_to_complete_lap[lap_index] - 1;
|
104
|
+
lap_speed = average_speeds[lap_index];
|
105
|
+
|
106
|
+
driving_speeds.push(lap_speed)
|
107
|
+
}
|
108
|
+
}
|
109
|
+
|
110
|
+
}
|
111
|
+
|
112
|
+
Ok(driving_speeds)
|
113
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
pub mod gis;
|
@@ -0,0 +1,69 @@
|
|
1
|
+
from typing import Optional
|
2
|
+
import numpy as np
|
3
|
+
from abc import ABC, abstractmethod
|
4
|
+
|
5
|
+
|
6
|
+
class BaseMeteorology(ABC):
|
7
|
+
def __init__(self):
|
8
|
+
self._wind_speed: Optional[np.ndarray] = None
|
9
|
+
self._wind_direction: Optional[np.ndarray] = None
|
10
|
+
self._solar_irradiance: Optional[np.ndarray] = None
|
11
|
+
self._weather_indices: Optional[np.ndarray] = None
|
12
|
+
|
13
|
+
def _return_if_available(self, attr):
|
14
|
+
if (value := getattr(self, attr)) is not None:
|
15
|
+
return value
|
16
|
+
else:
|
17
|
+
raise UnboundLocalError(f"{attr} is not available!")
|
18
|
+
|
19
|
+
@property
|
20
|
+
def wind_speed(self) -> np.ndarray:
|
21
|
+
"""
|
22
|
+
Return the wind speeds in m/s at every tick, if available.
|
23
|
+
|
24
|
+
:return: ``ndarray`` of wind speeds in m/s at every tick
|
25
|
+
:raises UnboundLocalError: if wind speeds are not available.
|
26
|
+
"""
|
27
|
+
return self._return_if_available("_wind_speed")
|
28
|
+
|
29
|
+
@property
|
30
|
+
def wind_direction(self) -> np.ndarray:
|
31
|
+
"""
|
32
|
+
Return the wind direction in degrees, following the meteorological convention, if available.
|
33
|
+
|
34
|
+
:return: ``ndarray`` of wind directions in degrees at every tick.
|
35
|
+
:raises UnboundLocalError: if wind directions are not available.
|
36
|
+
"""
|
37
|
+
return self._return_if_available("_wind_direction")
|
38
|
+
|
39
|
+
@property
|
40
|
+
def solar_irradiance(self) -> np.ndarray:
|
41
|
+
"""
|
42
|
+
Return the solar irradiance in W/m^2 every tick, if available.
|
43
|
+
|
44
|
+
:return: ``ndarray`` of solar irradiances in W/m^2 at every tick
|
45
|
+
:raises UnboundLocalError: if solar irradiances are not available.
|
46
|
+
"""
|
47
|
+
return self._return_if_available("_solar_irradiance")
|
48
|
+
|
49
|
+
@property
|
50
|
+
def weather_indices(self) -> np.ndarray:
|
51
|
+
"""
|
52
|
+
Return the weather indices at every tick, if available.
|
53
|
+
|
54
|
+
:return: ``ndarray`` of weather indices at every tick
|
55
|
+
:raises UnboundLocalError: if weather indices are not available.
|
56
|
+
"""
|
57
|
+
return self._return_if_available("_weather_indices")
|
58
|
+
|
59
|
+
@abstractmethod
|
60
|
+
def spatially_localize(self, cumulative_distances: np.ndarray) -> None:
|
61
|
+
raise NotImplementedError
|
62
|
+
|
63
|
+
@abstractmethod
|
64
|
+
def temporally_localize(self, unix_timestamps, start_time, tick) -> None:
|
65
|
+
raise NotImplementedError
|
66
|
+
|
67
|
+
@abstractmethod
|
68
|
+
def calculate_solar_irradiances(self, coords, time_zones, local_times, elevations):
|
69
|
+
raise NotImplementedError
|