ubc-solar-physics 0.1.0__cp311-cp311-macosx_13_0_universal2.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 (60) hide show
  1. core.cpython-311-darwin.so +0 -0
  2. physics/__init__.py +14 -0
  3. physics/environment/__init__.py +33 -0
  4. physics/environment/base_environment.py +62 -0
  5. physics/environment/environment.rs +3 -0
  6. physics/environment/gis/__init__.py +7 -0
  7. physics/environment/gis/base_gis.py +24 -0
  8. physics/environment/gis/gis.py +374 -0
  9. physics/environment/gis/gis.rs +25 -0
  10. physics/environment/gis.rs +1 -0
  11. physics/environment/openweather_environment.py +18 -0
  12. physics/environment/race.py +89 -0
  13. physics/environment/solar_calculations/OpenweatherSolarCalculations.py +529 -0
  14. physics/environment/solar_calculations/SolcastSolarCalculations.py +41 -0
  15. physics/environment/solar_calculations/__init__.py +9 -0
  16. physics/environment/solar_calculations/base_solar_calculations.py +9 -0
  17. physics/environment/solar_calculations/solar_calculations.rs +24 -0
  18. physics/environment/solar_calculations.rs +1 -0
  19. physics/environment/solcast_environment.py +18 -0
  20. physics/environment/weather_forecasts/OpenWeatherForecast.py +308 -0
  21. physics/environment/weather_forecasts/SolcastForecasts.py +216 -0
  22. physics/environment/weather_forecasts/__init__.py +9 -0
  23. physics/environment/weather_forecasts/base_weather_forecasts.py +57 -0
  24. physics/environment/weather_forecasts/weather_forecasts.rs +116 -0
  25. physics/environment/weather_forecasts.rs +1 -0
  26. physics/environment.rs +3 -0
  27. physics/lib.rs +76 -0
  28. physics/models/__init__.py +13 -0
  29. physics/models/arrays/__init__.py +7 -0
  30. physics/models/arrays/arrays.rs +0 -0
  31. physics/models/arrays/base_array.py +6 -0
  32. physics/models/arrays/basic_array.py +39 -0
  33. physics/models/arrays.rs +1 -0
  34. physics/models/battery/__init__.py +7 -0
  35. physics/models/battery/base_battery.py +29 -0
  36. physics/models/battery/basic_battery.py +141 -0
  37. physics/models/battery/battery.rs +0 -0
  38. physics/models/battery.rs +1 -0
  39. physics/models/constants.py +23 -0
  40. physics/models/lvs/__init__.py +7 -0
  41. physics/models/lvs/base_lvs.py +6 -0
  42. physics/models/lvs/basic_lvs.py +18 -0
  43. physics/models/lvs/lvs.rs +0 -0
  44. physics/models/lvs.rs +1 -0
  45. physics/models/motor/__init__.py +7 -0
  46. physics/models/motor/base_motor.py +6 -0
  47. physics/models/motor/basic_motor.py +179 -0
  48. physics/models/motor/motor.rs +0 -0
  49. physics/models/motor.rs +1 -0
  50. physics/models/regen/__init__.py +7 -0
  51. physics/models/regen/base_regen.py +6 -0
  52. physics/models/regen/basic_regen.py +39 -0
  53. physics/models/regen/regen.rs +0 -0
  54. physics/models/regen.rs +1 -0
  55. physics/models.rs +5 -0
  56. ubc_solar_physics-0.1.0.dist-info/LICENSE +21 -0
  57. ubc_solar_physics-0.1.0.dist-info/METADATA +44 -0
  58. ubc_solar_physics-0.1.0.dist-info/RECORD +60 -0
  59. ubc_solar_physics-0.1.0.dist-info/WHEEL +5 -0
  60. ubc_solar_physics-0.1.0.dist-info/top_level.txt +1 -0
Binary file
physics/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ from .environment import (
2
+ openweather_environment,
3
+ solcast_environment,
4
+ weather_forecasts,
5
+ gis
6
+ )
7
+
8
+ from .models import (
9
+ arrays,
10
+ battery,
11
+ lvs,
12
+ motor,
13
+ regen
14
+ )
@@ -0,0 +1,33 @@
1
+ from .base_environment import BaseEnvironment
2
+ from .openweather_environment import OpenweatherEnvironment
3
+ from .solcast_environment import SolcastEnvironment
4
+ from .race import (
5
+ Race,
6
+ compile_races
7
+ )
8
+
9
+ from .solar_calculations import (
10
+ OpenweatherSolarCalculations,
11
+ SolcastSolarCalculations
12
+ )
13
+
14
+ from .weather_forecasts import (
15
+ OpenWeatherForecast,
16
+ SolcastForecasts,
17
+ )
18
+
19
+ from .gis import (
20
+ GIS,
21
+ )
22
+
23
+ __all__ = [
24
+ "OpenweatherEnvironment",
25
+ "SolcastEnvironment",
26
+ "OpenWeatherForecast",
27
+ "SolcastForecasts",
28
+ "SolcastSolarCalculations",
29
+ "OpenweatherSolarCalculations",
30
+ "GIS",
31
+ "Race",
32
+ "compile_races"
33
+ ]
@@ -0,0 +1,62 @@
1
+ class BaseEnvironment:
2
+ def __init__(self):
3
+ self._time_dt = None
4
+ self._latitude = None
5
+ self._longitude = None
6
+ self._wind_speed = None
7
+ self._wind_direction = None
8
+
9
+ @property
10
+ def time_dt(self):
11
+ if (value := self._time_dt) is not None:
12
+ return value
13
+ else:
14
+ raise ValueError("time_dt is None!")
15
+
16
+ @time_dt.setter
17
+ def time_dt(self, value):
18
+ self._time_dt = value
19
+
20
+ @property
21
+ def latitude(self):
22
+ if (value := self._latitude) is not None:
23
+ return value
24
+ else:
25
+ raise ValueError("latitude is None!")
26
+
27
+ @latitude.setter
28
+ def latitude(self, value):
29
+ self._latitude = value
30
+
31
+ @property
32
+ def longitude(self):
33
+ if (value := self._longitude) is not None:
34
+ return value
35
+ else:
36
+ raise ValueError("longitude is None!")
37
+
38
+ @longitude.setter
39
+ def longitude(self, value):
40
+ self._longitude = value
41
+
42
+ @property
43
+ def wind_speed(self):
44
+ if (value := self._wind_speed) is not None:
45
+ return value
46
+ else:
47
+ raise ValueError("wind_speed is None!")
48
+
49
+ @wind_speed.setter
50
+ def wind_speed(self, value):
51
+ self._wind_speed = value
52
+
53
+ @property
54
+ def wind_direction(self):
55
+ if (value := self._wind_direction) is not None:
56
+ return value
57
+ else:
58
+ raise ValueError("wind_direction is None!")
59
+
60
+ @wind_direction.setter
61
+ def wind_direction(self, value):
62
+ self._wind_direction = value
@@ -0,0 +1,3 @@
1
+ pub mod gis;
2
+ pub mod solar_calculations;
3
+ pub mod weather_forecasts;
@@ -0,0 +1,7 @@
1
+ from .base_gis import BaseGIS
2
+ from .gis import GIS
3
+
4
+ __all__ = [
5
+ "BaseGIS",
6
+ "GIS"
7
+ ]
@@ -0,0 +1,24 @@
1
+ from abc import ABC, abstractmethod
2
+ import numpy as np
3
+
4
+
5
+ class BaseGIS(ABC):
6
+ @abstractmethod
7
+ def calculate_closest_gis_indices(self, cumulative_distances) -> np.ndarray:
8
+ raise NotImplementedError
9
+
10
+ @abstractmethod
11
+ def get_path_elevations(self) -> np.ndarray:
12
+ raise NotImplementedError
13
+
14
+ @abstractmethod
15
+ def get_gradients(self, gis_indices) -> np.ndarray:
16
+ raise NotImplementedError
17
+
18
+ @abstractmethod
19
+ def get_time_zones(self, gis_indices) -> np.ndarray:
20
+ raise NotImplementedError
21
+
22
+ @abstractmethod
23
+ def get_path(self) -> np.ndarray:
24
+ raise NotImplementedError
@@ -0,0 +1,374 @@
1
+ import os
2
+ import logging
3
+ import math
4
+ import pathlib
5
+
6
+ import numpy as np
7
+ import sys
8
+ from tqdm import tqdm
9
+ from xml.dom import minidom
10
+ from typing import TypeAlias, Union
11
+ from haversine import haversine, Unit
12
+
13
+ from physics.environment.gis.base_gis import BaseGIS
14
+ from physics.environment.race import Race
15
+
16
+ import core
17
+
18
+ PathLike: TypeAlias = Union[pathlib.Path, str]
19
+
20
+
21
+ class GIS(BaseGIS):
22
+ def __init__(self, origin_coord, dest_coord, waypoints, race_type, route_directory: PathLike, current_coord=None, hash_key=None):
23
+ """
24
+
25
+ Initialises a GIS (geographic location system) object. This object is responsible for getting the
26
+ simulation's planned route from the Google Maps API and performing operations on the received data.
27
+
28
+ :param origin_coord: NumPy array containing the start coordinate (lat, long) of the planned travel route
29
+ :param dest_coord: NumPy array containing the end coordinate (lat, long) of the planned travel route
30
+ :param waypoints: NumPy array containing the route waypoints to travel through during simulation
31
+ :param race_type: String ("FSGP" or "ASC") stating which race is being simulated
32
+ :param hash_key: key used to identify cached data as valid for a Simulation model
33
+
34
+ """
35
+ self.current_index = 0
36
+ self.distance_remainder = 0
37
+
38
+ self.origin_coord = origin_coord
39
+ self.dest_coord = dest_coord
40
+ self.current_coord = current_coord
41
+ self.waypoints = waypoints
42
+ self.race_type = race_type
43
+
44
+ # path to file storing the route and elevation NumPy arrays
45
+ if self.race_type == Race.RaceType.FSGP:
46
+ route_file = route_directory / "route_data_FSGP.npz"
47
+ else:
48
+ route_file = route_directory / "route_data.npz"
49
+
50
+ # if the file exists, load path from file
51
+ if os.path.isfile(route_file):
52
+ with np.load(route_file) as route_data:
53
+ if route_data['hash'] == hash_key:
54
+
55
+ print("Previous route save file is being used...\n")
56
+
57
+ print("----- Route save file information -----")
58
+ for key in route_data:
59
+ print(f"> {key}: {route_data[key].shape}")
60
+
61
+ self.path = route_data['path']
62
+ self.launch_point = route_data['path'][0]
63
+ self.path_elevations = route_data['elevations']
64
+ self.path_time_zones = route_data['time_zones']
65
+ self.speed_limits = route_data['speed_limits']
66
+ self.num_unique_coords = route_data['num_unique_coords']
67
+
68
+ if current_coord is not None:
69
+ if not np.array_equal(current_coord, origin_coord):
70
+ logging.warning("Current position is not origin position. Modifying path data.\n")
71
+
72
+ # We need to find the closest coordinate along the path to the vehicle position
73
+ current_coord_index = GIS._find_closest_coordinate_index(current_coord, self.path)
74
+
75
+ # All coords before the current coordinate should be discarded
76
+ self.path = self.path[current_coord_index:]
77
+ self.path_elevations = self.path_elevations[current_coord_index:]
78
+ self.path_time_zones = self.path_time_zones[current_coord_index:]
79
+ else:
80
+ logging.warning("Route save file does not exist.\n")
81
+ logging.error("Update API cache by calling CacheAPI.py , Exiting simulation...\n")
82
+
83
+ exit()
84
+
85
+ self.path_distances = calculate_path_distances(self.path)
86
+ self.path_length = np.cumsum(calculate_path_distances(self.path[:self.num_unique_coords]))[-1]
87
+ self.path_gradients = calculate_path_gradients(self.path_elevations, self.path_distances)
88
+
89
+ @staticmethod
90
+ def process_KML_file(route_file):
91
+ """
92
+
93
+ Load the FSGP Track from a KML file exported from a Google Earth project.
94
+
95
+ Ensure to follow guidelines enumerated in this directory's `README.md` when creating and
96
+ loading new route files.
97
+
98
+ :return: Array of N coordinates (latitude, longitude) in the shape [N][2].
99
+ """
100
+ with open(route_file) as f:
101
+ data = minidom.parse(f)
102
+ kml_coordinates = data.getElementsByTagName("coordinates")[0].childNodes[0].data
103
+ coordinates: np.ndarray = np.array(parse_coordinates_from_kml(kml_coordinates))
104
+
105
+ # Google Earth exports coordinates in order longitude, latitude, when we want the opposite
106
+ return np.roll(coordinates, 1, axis=1)
107
+
108
+ def calculate_closest_gis_indices(self, distances):
109
+ """
110
+
111
+ Takes in an array of point distances from starting point, returns a list of
112
+ self.path indices of coordinates which have a distance from the starting point
113
+ closest to the point distances.
114
+
115
+ :param np.ndarray cumulative_distances: (float[N]) array of distances, where cumulative_distances[x] > cumulative_distances[x-1]
116
+ :returns: (float[N]) array of indices of path
117
+ :rtype: np.ndarray
118
+
119
+ """
120
+ return core.closest_gis_indices_loop(distances, self.path_distances)
121
+
122
+ @staticmethod
123
+ def _python_calculate_closest_gis_indices(distances, path_distances):
124
+ """
125
+
126
+ Python implementation of rust core.closest_gis_indices_loop. See parent function for documentation details.
127
+
128
+ """
129
+
130
+ current_coordinate_index = 0
131
+ result = []
132
+
133
+ with tqdm(total=len(distances), file=sys.stdout, desc="Calculating closest GIS indices") as pbar:
134
+ distance_travelled = 0
135
+ for distance in np.nditer(distances):
136
+ distance_travelled += distance
137
+
138
+ while distance_travelled > path_distances[current_coordinate_index]:
139
+ distance_travelled -= path_distances[current_coordinate_index]
140
+ current_coordinate_index += 1
141
+
142
+ if current_coordinate_index >= len(path_distances) - 1:
143
+ current_coordinate_index = len(path_distances) - 1
144
+
145
+ result.append(current_coordinate_index)
146
+ pbar.update(1)
147
+
148
+ return np.array(result)
149
+
150
+ # ----- Getters -----
151
+ def get_time_zones(self, gis_indices):
152
+ """
153
+
154
+ Takes in an array of path indices, returns the time zone at each index
155
+
156
+ :param np.ndarray gis_indices: (float[N]) array of path indices
157
+ :returns: (float[N]) array of time zones in seconds
158
+ :rtype: np.ndarray
159
+
160
+ """
161
+
162
+ return self.path_time_zones[gis_indices]
163
+
164
+ def get_gradients(self, gis_indices):
165
+ """
166
+
167
+ Takes in an array of path indices, returns the road gradient at each index
168
+
169
+ :param np.ndarray gis_indices: (float[N]) array of path indices
170
+ :returns: (float[N]) array of road gradients
171
+ :rtype np.ndarray:
172
+
173
+ """
174
+
175
+ return self.path_gradients[gis_indices]
176
+
177
+ def get_path(self):
178
+ """
179
+ Returns all N coordinates of the path in a NumPy array
180
+ [N][latitude, longitude]
181
+
182
+ :rtype: np.ndarray
183
+
184
+ """
185
+
186
+ return self.path
187
+
188
+ def get_path_elevations(self):
189
+ """
190
+
191
+ Returns all N elevations of the path in a NumPy array
192
+ [N][elevation]
193
+
194
+ :rtype: np.ndarray
195
+
196
+ """
197
+
198
+ return self.path_elevations
199
+
200
+ def get_path_distances(self):
201
+ """
202
+
203
+ Returns all N-1 distances of the path in a NumPy array
204
+ [N-1][elevation]
205
+
206
+ :rtype: np.ndarray
207
+
208
+ """
209
+
210
+ return self.path_distances
211
+
212
+ def get_path_gradients(self):
213
+ """
214
+
215
+ Returns all N-1 gradients of a path in a NumPy array
216
+ [N-1][gradient]
217
+
218
+ :rtype: np.ndarray
219
+
220
+ """
221
+
222
+ return self.path_gradients
223
+
224
+ # ----- Path calculation functions -----
225
+ def calculate_path_min_max(self):
226
+ logging.warning(f"Using deprecated function 'calculate_path_min_max()'!")
227
+ min_lat, min_long = self.path.min(axis=0)
228
+ max_lat, max_long = self.path.max(axis=0)
229
+ return [min_long, min_lat, max_long, max_lat]
230
+
231
+ def calculate_current_heading_array(self):
232
+ """
233
+
234
+ Calculates the bearing of the vehicle between consecutive points
235
+ https://www.movable-type.co.uk/scripts/latlong.html
236
+
237
+ :returns: array of bearings
238
+ :rtype: np.ndarray
239
+
240
+ """
241
+ bearing_array = np.zeros(len(self.path))
242
+
243
+ for index in range(0, len(self.path) - 1):
244
+ coord_1 = np.radians(self.path[index])
245
+ coord_2 = np.radians(self.path[index + 1])
246
+
247
+ y = math.sin(coord_2[1] - coord_1[1]) \
248
+ * math.cos(coord_2[0])
249
+
250
+ x = math.cos(coord_1[0]) \
251
+ * math.sin(coord_2[0]) \
252
+ - math.sin(coord_1[0]) \
253
+ * math.cos(coord_2[0]) \
254
+ * math.cos(coord_2[1] - coord_1[1])
255
+
256
+ theta = math.atan2(y, x)
257
+
258
+ bearing_array[index] = ((theta * 180) / math.pi + 360) % 360
259
+
260
+ bearing_array[-1] = bearing_array[-2]
261
+
262
+ return bearing_array
263
+
264
+ @staticmethod
265
+ def _calculate_vector_square_magnitude(vector):
266
+ """
267
+
268
+ Calculate the square magnitude of an input vector. Must be one-dimensional.
269
+
270
+ :param np.ndarray vector: NumPy array[N] representing a vector[N]
271
+ :return: square magnitude of the input vector
272
+ :rtype: float
273
+
274
+ """
275
+
276
+ return sum(i ** 2 for i in vector)
277
+
278
+ @staticmethod
279
+ def _find_closest_coordinate_index(current_coord, path):
280
+ """
281
+
282
+ Returns the closest coordinate to current_coord in path
283
+
284
+ :param np.ndarray current_coord: A NumPy array[N] representing a N-dimensional vector
285
+ :param np.ndarray path: A NumPy array[M][N] of M coordinates which should be N-dimensional vectors
286
+ :returns: index of the closest coordinate.
287
+ :rtype: int
288
+
289
+ """
290
+
291
+ to_current_coord_from_path = np.abs(path - current_coord)
292
+ distances_from_current_coord = np.zeros(len(to_current_coord_from_path))
293
+ for i in range(len(to_current_coord_from_path)):
294
+ # As we just need the minimum, using square magnitude will save performance
295
+ distances_from_current_coord[i] = GIS._calculate_vector_square_magnitude(to_current_coord_from_path[i])
296
+
297
+ return distances_from_current_coord.argmin()
298
+
299
+
300
+ def calculate_path_distances(coords):
301
+ """
302
+
303
+ Obtain the distance between each coordinate by approximating the spline between them
304
+ as a straight line, and use the Haversine formula (https://en.wikipedia.org/wiki/Haversine_formula)
305
+ to calculate distance between coordinates on a sphere.
306
+
307
+ :param np.ndarray coords: A NumPy array [n][latitude, longitude]
308
+ :returns path_distances: a NumPy array [n-1][distances],
309
+ :rtype: np.ndarray
310
+
311
+ """
312
+
313
+ coords_offset = np.roll(coords, (1, 1))
314
+ path_distances = []
315
+ for u, v in zip(coords, coords_offset):
316
+ path_distances.append(haversine(u, v, unit=Unit.METERS))
317
+
318
+ return np.array(path_distances)
319
+
320
+
321
+ def parse_coordinates_from_kml(coords_str: str) -> np.ndarray:
322
+ """
323
+
324
+ Parse a coordinates string from a XML (KML) file into a list of coordinates (2D vectors).
325
+ Requires coordinates in the format "39.,41.,0 39.,40.,0" which will return [ [39., 41.], [39., 40.] ].
326
+
327
+ :param coords_str: coordinates string from a XML (KML) file
328
+ :return: list of 2D vectors representing coordinates
329
+ :rtype: np.ndarray
330
+
331
+ """
332
+
333
+ def parse_coord(pair):
334
+ coord = pair.split(',')
335
+ coord.pop()
336
+ coord = [float(value) for value in coord]
337
+ return coord
338
+
339
+ return list(map(parse_coord, coords_str.split()))
340
+
341
+
342
+ def calculate_path_gradients(elevations, distances):
343
+ """
344
+
345
+ Get the approximate gradients of every point on the path.
346
+
347
+ Note:
348
+ - gradient > 0 corresponds to uphill
349
+ - gradient < 0 corresponds to downhill
350
+
351
+ :param np.ndarray elevations: [N][elevations]
352
+ :param np.ndarray distances: [N-1][distances]
353
+ :returns gradients: [N-1][gradients]
354
+ :rtype: np.ndarray
355
+
356
+ """
357
+
358
+ # subtract every next elevation with the previous elevation to
359
+ # get the difference in elevation
360
+ # [1 2 3 4 5]
361
+ # [5 1 2 3 4] -
362
+ # -------------
363
+ # [1 1 1 1]
364
+
365
+ offset = np.roll(elevations, 1)
366
+ delta_elevations = elevations - offset
367
+
368
+ # Divide the difference in elevation to get the gradient
369
+ # gradient > 0: uphill
370
+ # gradient < 0: downhill
371
+ with np.errstate(invalid='ignore'):
372
+ gradients = delta_elevations / distances
373
+
374
+ return np.nan_to_num(gradients, nan=0.)
@@ -0,0 +1,25 @@
1
+ use chrono::{Datelike, NaiveDateTime, Timelike};
2
+ use numpy::ndarray::{s, Array, Array2, ArrayViewD, ArrayViewMut2, ArrayViewMut3, Axis};
3
+
4
+ pub fn rust_closest_gis_indices_loop(
5
+ distances: ArrayViewD<'_, f64>,
6
+ path_distances: ArrayViewD<'_, f64>,
7
+ ) -> Vec<i64> {
8
+ let mut current_coord_index: usize = 0;
9
+ let mut distance_travelled: f64 = 0.0;
10
+ let mut result: Vec<i64> = Vec::with_capacity(distances.len());
11
+
12
+ for &distance in distances {
13
+ distance_travelled += distance;
14
+
15
+ while distance_travelled > path_distances[current_coord_index] {
16
+ distance_travelled -= path_distances[current_coord_index];
17
+ current_coord_index += 1;
18
+ }
19
+
20
+ current_coord_index = std::cmp::min(current_coord_index, path_distances.len() - 1);
21
+ result.push(current_coord_index as i64);
22
+ }
23
+
24
+ result
25
+ }
@@ -0,0 +1 @@
1
+ pub mod gis;
@@ -0,0 +1,18 @@
1
+ from physics.environment.base_environment import BaseEnvironment
2
+
3
+
4
+ class OpenweatherEnvironment(BaseEnvironment):
5
+ def __init__(self):
6
+ super().__init__()
7
+ self._cloud_cover = None
8
+
9
+ @property
10
+ def cloud_cover(self):
11
+ if (value := self._cloud_cover) is not None:
12
+ return value
13
+ else:
14
+ raise ValueError("cloud cover is None!")
15
+
16
+ @cloud_cover.setter
17
+ def cloud_cover(self, value):
18
+ self._cloud_cover = value
@@ -0,0 +1,89 @@
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)