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
@@ -0,0 +1,308 @@
1
+ """
2
+ A class to extract local and path weather predictions such as wind_speed,
3
+ wind_direction, cloud_cover and weather type from OpenWeather.
4
+ """
5
+ import numpy as np
6
+ import datetime
7
+
8
+ from physics.environment import OpenweatherEnvironment
9
+ from physics.environment.weather_forecasts.base_weather_forecasts import BaseWeatherForecasts
10
+ from physics.environment.race import Race
11
+ from haversine import haversine, Unit
12
+
13
+ import core
14
+
15
+
16
+ class OpenWeatherForecast(BaseWeatherForecasts):
17
+ """
18
+ Class that gathers weather data and performs calculations on it to allow the implementation of weather phenomenon
19
+ such as changes in wind speeds and cloud cover in the simulation.
20
+
21
+ Attributes:
22
+ coords (NumPy array [N][lat, long]): a list of N coordinates for which to gather weather forecasts for
23
+ origin_coord (NumPy array [lat, long]): the starting coordinate
24
+ dest_coord (NumPy array [lat, long]): the ending coordinate
25
+ last_updated_time (int): value that tells us the starting time after which we have weather data available
26
+
27
+ weather_forecast (NumPy array [N][T][9]): array that stores the complete weather forecast data. N represents the
28
+ number of coordinates, T represents time length which differs depending on the `weather_data_frequency`
29
+ argument ("current" -> T = 1 ; "hourly" -> T = 24 ; "daily" -> T = 8). The last 9 represents the number of
30
+ weather forecast fields available. These are: (latitude, longitude, dt (UNIX time), timezone_offset
31
+ (in seconds), dt + timezone_offset (local time), wind_speed, wind_direction, cloud_cover, description_id)
32
+ """
33
+
34
+ def __init__(self, coords, race: Race, origin_coord=None, hash_key=None):
35
+
36
+ """
37
+
38
+ Initializes the instance of a WeatherForecast class
39
+
40
+ :param origin_coord: A NumPy array of a single [latitude, longitude]
41
+ :param str provider: string indicating weather provider
42
+ :param coords: A NumPy array of [latitude, longitude]
43
+ :param hash_key: key used to identify cached data as valid for a Simulation model
44
+
45
+ """
46
+ super().__init__(coords, race, "OPENWEATHER", origin_coord, hash_key)
47
+
48
+ self.last_updated_time = self.weather_forecast[0, 0, 2]
49
+
50
+ start_time_unix = self.weather_forecast[0, 0, 2]
51
+ end_time_unix = self.weather_forecast[0, -1, 2]
52
+ start_time = date_from_unix_timestamp(start_time_unix)
53
+ end_time = date_from_unix_timestamp(end_time_unix)
54
+
55
+ print("----- Weather save file information -----\n")
56
+ print(f"--- Data time range ---")
57
+ print(f"Start time (UTC)f: {start_time} [{start_time_unix:.0f}]\n"
58
+ f"End time (UTC): {end_time} [{end_time_unix:.0f}]\n")
59
+
60
+
61
+ def calculate_closest_weather_indices(self, cumulative_distances):
62
+ """
63
+
64
+ :param np.ndarray cumulative_distances: NumPy Array representing cumulative distances theoretically achievable for a given input speed array
65
+ :returns: array of the closest weather indices.
66
+ :rtype: np.ndarray
67
+
68
+ """
69
+
70
+ """
71
+ IMPORTANT: we only have weather coordinates for a discrete set of coordinates. However, the car could be at any
72
+ coordinate in between these available weather coordinates. We need to figure out what coordinate the car is at
73
+ at each timestep and then we can figure out the full weather forecast at each timestep.
74
+
75
+ For example, imagine the car is at some coordinate (10, 20). Further imagine that we have a week's worth of
76
+ weather forecasts for the following five coordinates: (5, 4), (11, 19), (20, 30), (40, 30), (0, 60). Which
77
+ set of weather forecasts should we choose? Well, we should choose the (11, 19) one since our coordinate
78
+ (10, 20) is closest to (11, 19). This is what the following code is accomplishing. However, it is not dealing
79
+ with the coordinates directly but rather is dealing with the distances between the coordinates.
80
+
81
+ Furthermore, once we have chosen a week's worth of weather forecasts for a specific coordinate, we must isolate
82
+ a single weather forecast depending on what time the car is at the coordinate (10, 20). That is the job of the
83
+ `get_weather_forecast_in_time()` method.
84
+
85
+ """
86
+
87
+ # if racing FSGP, there is no need for distance calculations. We will return only the origin coordinate
88
+ # This characterizes the weather at every point along the FSGP tracks
89
+ # with the weather at a single coordinate on the track, which is great for reducing the API calls and is a
90
+ # reasonable assumption to make for FSGP only.
91
+ if self.race.race_type == Race.FSGP:
92
+ result = np.zeros_like(cumulative_distances, dtype=int)
93
+ return result
94
+
95
+ # a list of all the coordinates that we have weather data for
96
+ weather_coords = self.weather_forecast[:, 0, 0:2]
97
+
98
+ # distances between all the coordinates that we have weather data for
99
+ weather_path_distances = calculate_path_distances(weather_coords)
100
+ cumulative_weather_path_distances = np.cumsum(weather_path_distances)
101
+
102
+ # makes every even-index element negative, this allows the use of np.diff() to calculate the sum of consecutive
103
+ # elements
104
+ cumulative_weather_path_distances[::2] *= -1
105
+
106
+ # contains the average distance between two consecutive elements in the cumulative_weather_path_distances array
107
+ average_distances = np.abs(np.diff(cumulative_weather_path_distances) / 2)
108
+
109
+ return core.closest_weather_indices_loop(cumulative_distances, average_distances)
110
+
111
+ @staticmethod
112
+ def _python_calculate_closest_weather_indices(cumulative_distances, average_distances):
113
+ """
114
+
115
+ Python implementation of calculate_closest_weather_indices. See parent function for documentation details.
116
+
117
+ """
118
+
119
+ current_coordinate_index = 0
120
+ result = []
121
+
122
+ for distance in np.nditer(cumulative_distances):
123
+
124
+ # makes sure the current_coordinate_index does not exceed its maximum value
125
+ if current_coordinate_index > len(average_distances) - 1:
126
+ current_coordinate_index = len(average_distances) - 1
127
+
128
+ if distance > average_distances[current_coordinate_index]:
129
+ current_coordinate_index += 1
130
+ if current_coordinate_index > len(average_distances) - 1:
131
+ current_coordinate_index = len(average_distances) - 1
132
+
133
+ result.append(current_coordinate_index)
134
+
135
+ return np.array(result)
136
+
137
+ @staticmethod
138
+ def _python_calculate_closest_timestamp_indices(unix_timestamps, dt_local_array):
139
+ """
140
+
141
+ Python implementation to find the indices of the closest timestamps in dt_local_array and package them into a NumPy Array
142
+
143
+ :param np.ndarray unix_timestamps: NumPy Array (float[N]) of unix timestamps of the vehicle's journey
144
+ :param np.ndarray dt_local_array: NumPy Array (float[N]) of local times, represented as unix timestamps
145
+ :returns: NumPy Array of (int[N]) containing closest timestamp indices used by get_weather_forecast_in_time
146
+ :rtype: np.ndarray
147
+
148
+ """
149
+ closest_time_stamp_indices = []
150
+ for unix_timestamp in unix_timestamps:
151
+ unix_timestamp_array = np.full_like(dt_local_array, fill_value=unix_timestamp)
152
+ differences = np.abs(unix_timestamp_array - dt_local_array)
153
+ minimum_index = np.argmin(differences)
154
+ closest_time_stamp_indices.append(minimum_index)
155
+
156
+ return np.asarray(closest_time_stamp_indices, dtype=np.int32)
157
+
158
+ def get_weather_forecast_in_time(self, indices, unix_timestamps, start_hour, tick):
159
+ """
160
+
161
+ Takes in an array of indices of the weather_forecast array, and an array of timestamps. Uses those to figure out
162
+ what the weather forecast is at each time step being simulated.
163
+
164
+ we only have weather at discrete timestamps. The car however can be in any timestamp in
165
+ between. Therefore, we must be able to choose the weather timestamp that is closest to the one that the car is in
166
+ so that we can more accurately determine the weather experienced by the car at that timestamp.
167
+
168
+ For example, imagine the car is at some coordinate (x,y) at timestamp 100. Imagine we know the weather forecast
169
+ at (x,y) for five different timestamps: 0, 30, 60, 90, and 120. Which weather forecast should we
170
+ choose? Clearly, we should choose the weather forecast at 90 since it is the closest to 100. That's what the
171
+ below code is accomplishing.
172
+
173
+ :param np.ndarray indices: (int[N]) coordinate indices of self.weather_forecast
174
+ :param np.ndarray unix_timestamps: (int[N]) unix timestamps of the vehicle's journey
175
+ :param int start_hour: the starting hour of simulation
176
+ :param int tick: length of a tick in seconds
177
+ :returns:
178
+ - A NumPy array of size [N][9]
179
+ - [9] (latitude, longitude, unix_time, timezone_offset, unix_time_corrected, wind_speed, wind_direction,
180
+ cloud_cover, precipitation, description):
181
+ :rtype: np.ndarray
182
+
183
+ """
184
+ weather_data = core.weather_in_time(unix_timestamps.astype(np.int64), indices.astype(np.int64), self.weather_forecast, 4)
185
+ roll_by_tick = int(3600 / tick) * (24 + start_hour - hour_from_unix_timestamp(weather_data[0, 2]))
186
+ weather_data = np.roll(weather_data, -roll_by_tick, 0)
187
+
188
+ weather_object = OpenweatherEnvironment()
189
+ weather_object.latitude = weather_data[:, 0]
190
+ weather_object.longitude = weather_data[:, 1]
191
+ weather_object.unix_time = weather_data[:, 2]
192
+ weather_object.wind_speed = weather_data[:, 5]
193
+ weather_object.wind_direction = weather_data[:, 6]
194
+ weather_object.cloud_cover = weather_data[:, 7]
195
+
196
+ return weather_object
197
+
198
+ def _python_get_weather_in_time(self, unix_timestamps, indices):
199
+ full_weather_forecast_at_coords = self.weather_forecast[indices]
200
+ dt_local_array = full_weather_forecast_at_coords[0, :, 4]
201
+
202
+ temp_0 = np.arange(0, full_weather_forecast_at_coords.shape[0])
203
+ closest_timestamp_indices = self._python_calculate_closest_timestamp_indices(unix_timestamps, dt_local_array)
204
+
205
+ return full_weather_forecast_at_coords[temp_0, closest_timestamp_indices]
206
+
207
+ @staticmethod
208
+ def _get_array_directional_wind_speed(vehicle_bearings, wind_speeds, wind_directions):
209
+ """
210
+
211
+ Returns the array of wind speed in m/s, in the direction opposite to the
212
+ bearing of the vehicle
213
+
214
+
215
+ :param np.ndarray vehicle_bearings: (float[N]) The azimuth angles that the vehicle in, in degrees
216
+ :param np.ndarray wind_speeds: (float[N]) The absolute speeds in m/s
217
+ :param np.ndarray wind_directions: (float[N]) The wind direction in the meteorlogical convention. To convert from meteorlogical convention to azimuth angle, use (x + 180) % 360
218
+ :returns: The wind speeds in the direction opposite to the bearing of the vehicle
219
+ :rtype: np.ndarray
220
+
221
+ """
222
+
223
+ # wind direction is 90 degrees meteorological, so it is 270 degrees azimuthal. car is 90 degrees
224
+ # cos(90 - 90) = cos(0) = 1. Wind speed is moving opposite to the car,
225
+ # car is 270 degrees, cos(90-270) = -1. Wind speed is in direction of the car.
226
+ return wind_speeds * (np.cos(np.radians(wind_directions - vehicle_bearings)))
227
+
228
+ @staticmethod
229
+ def _get_weather_advisory(weather_id):
230
+ """
231
+
232
+ Returns a string indicating the type of weather to expect, from the standardized
233
+ weather code passed as a parameter
234
+
235
+ https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2
236
+ :param int weather_id: Weather ID
237
+ :return: type of weather advisory
238
+ :rtype: str
239
+
240
+ """
241
+
242
+ if 200 <= weather_id < 300:
243
+ return "Thunderstorm"
244
+ elif 300 <= weather_id < 500:
245
+ return "Drizzle"
246
+ elif 500 <= weather_id < 600:
247
+ return "Rain"
248
+ elif 600 <= weather_id < 700:
249
+ return "Snow"
250
+ elif weather_id == 800:
251
+ return "Clear"
252
+ else:
253
+ return "Unknown"
254
+
255
+
256
+ def date_from_unix_timestamp(unix_timestamp):
257
+ """
258
+
259
+ Return a stringified UTC datetime from UNIX timestamped.
260
+
261
+ :param int unix_timestamp: A unix timestamp
262
+
263
+ :returns: A string of the UTC representation of the UNIX timestamp in the format Y-m-d H:M:S
264
+ :rtype: str
265
+
266
+ """
267
+
268
+ return datetime.datetime.utcfromtimestamp(unix_timestamp).strftime('%Y-%m-%d %H:%M:%S')
269
+
270
+
271
+ def calculate_path_distances(coords):
272
+ """
273
+
274
+ Obtain the distance between each coordinate by approximating the spline between them
275
+ as a straight line, and use the Haversine formula (https://en.wikipedia.org/wiki/Haversine_formula)
276
+ to calculate distance between coordinates on a sphere.
277
+
278
+ :param np.ndarray coords: A NumPy array [n][latitude, longitude]
279
+ :returns path_distances: a NumPy array [n-1][distances],
280
+ :rtype: np.ndarray
281
+
282
+ """
283
+
284
+ coords_offset = np.roll(coords, (1, 1))
285
+ path_distances = []
286
+ for u, v in zip(coords, coords_offset):
287
+ path_distances.append(haversine(u, v, unit=Unit.METERS))
288
+
289
+ return np.array(path_distances)
290
+
291
+
292
+ def hour_from_unix_timestamp(unix_timestamp):
293
+ """
294
+
295
+ Return the hour of a UNIX timestamp.
296
+
297
+ :param float unix_timestamp: a UNIX timestamp
298
+ :return: hour of UTC datetime from unix timestamp
299
+ :rtype: int
300
+
301
+ """
302
+
303
+ val = datetime.datetime.utcfromtimestamp(unix_timestamp)
304
+ return val.hour
305
+
306
+
307
+ if __name__ == "__main__":
308
+ pass
@@ -0,0 +1,216 @@
1
+ """
2
+ A class to extract local and path weather predictions such as wind_speed,
3
+ wind_direction, cloud_cover and weather type using data from Solcast.
4
+ """
5
+ import numpy as np
6
+ import os
7
+ import logging
8
+
9
+ from simulation.model.environment.weather_forecasts import BaseWeatherForecasts
10
+ from simulation.model.environment import SolcastEnvironment
11
+ from simulation.common import helpers, constants, Race
12
+
13
+ import core
14
+
15
+
16
+ class SolcastForecasts(BaseWeatherForecasts):
17
+ """
18
+ Class that gathers weather data and performs calculations on it to allow the implementation of weather phenomenon
19
+ such as changes in wind speeds and cloud cover in the simulation.
20
+
21
+ Attributes:
22
+ coords (NumPy array [N][lat, long]): a list of N coordinates for which to gather weather forecasts for
23
+ origin_coord (NumPy array [lat, long]): the starting coordinate
24
+ dest_coord (NumPy array [lat, long]): the ending coordinate
25
+ last_updated_time (int): value that tells us the starting time after which we have weather data available
26
+
27
+ weather_forecast (NumPy array [N][T][6]): array that stores the complete weather forecast data. N represents the
28
+ number of coordinates, T represents time length with temporal granularity defined in settings_*.json.
29
+ The last 6 represents the number of weather forecast fields available. These are: time_dt (UTC UNIX time),
30
+ latitude, longitude, wind_speed (m/s), wind_direction (meteorological convention), ghi (W/m^2)
31
+ """
32
+
33
+ def __init__(self, coords, race: Race, origin_coord=None, hash_key=None):
34
+
35
+ """
36
+
37
+ Initializes the instance of a WeatherForecast class
38
+
39
+ :param origin_coord: A NumPy array of a single [latitude, longitude]
40
+ :param str provider: string indicating weather provider
41
+ :param coords: A NumPy array of [latitude, longitude]
42
+ :param hash_key: key used to identify cached data as valid for a Simulation model
43
+
44
+ """
45
+ super().__init__(coords, race, "SOLCAST", origin_coord, hash_key)
46
+ self.race = race
47
+ self.last_updated_time = self.weather_forecast[0, 0, 0]
48
+
49
+ def calculate_closest_weather_indices(self, cumulative_distances):
50
+ """
51
+
52
+ :param np.ndarray cumulative_distances: NumPy Array representing cumulative distances theoretically achievable for a given input speed array
53
+ :returns: array of the closest weather indices.
54
+ :rtype: np.ndarray
55
+
56
+ """
57
+
58
+ """
59
+ IMPORTANT: we only have weather coordinates for a discrete set of coordinates. However, the car could be at any
60
+ coordinate in between these available weather coordinates. We need to figure out what coordinate the car is at
61
+ at each timestep and then we can figure out the full weather forecast at each timestep.
62
+
63
+ For example, imagine the car is at some coordinate (10, 20). Further imagine that we have a week's worth of
64
+ weather forecasts for the following five coordinates: (5, 4), (11, 19), (20, 30), (40, 30), (0, 60). Which
65
+ set of weather forecasts should we choose? Well, we should choose the (11, 19) one since our coordinate
66
+ (10, 20) is closest to (11, 19). This is what the following code is accomplishing. However, it is not dealing
67
+ with the coordinates directly but rather is dealing with the distances between the coordinates.
68
+
69
+ Furthermore, once we have chosen a week's worth of weather forecasts for a specific coordinate, we must isolate
70
+ a single weather forecast depending on what time the car is at the coordinate (10, 20). That is the job of the
71
+ `get_weather_forecast_in_time()` method.
72
+
73
+ """
74
+
75
+ # if racing FSGP, there is no need for distance calculations. We will return only the origin coordinate
76
+ # This characterizes the weather at every point along the FSGP tracks
77
+ # with the weather at a single coordinate on the track, which is great for reducing the API calls and is a
78
+ # reasonable assumption to make for FSGP only.
79
+ if self.race.race_type == Race.FSGP:
80
+ result = np.zeros_like(cumulative_distances, dtype=int)
81
+ return result
82
+
83
+ # a list of all the coordinates that we have weather data for
84
+ weather_coords = self.weather_forecast[:, 0, 1:3]
85
+
86
+ # distances between all the coordinates that we have weather data for
87
+ weather_path_distances = helpers.calculate_path_distances(weather_coords)
88
+ cumulative_weather_path_distances = np.cumsum(weather_path_distances)
89
+
90
+ # makes every even-index element negative, this allows the use of np.diff() to calculate the sum of consecutive
91
+ # elements
92
+ cumulative_weather_path_distances[::2] *= -1
93
+
94
+ # contains the average distance between two consecutive elements in the cumulative_weather_path_distances array
95
+ average_distances = np.abs(np.diff(cumulative_weather_path_distances) / 2)
96
+
97
+ return core.closest_weather_indices_loop(cumulative_distances, average_distances)
98
+
99
+ @staticmethod
100
+ def _python_calculate_closest_weather_indices(cumulative_distances, average_distances):
101
+ """
102
+
103
+ Python implementation of calculate_closest_weather_indices. See parent function for documentation details.
104
+
105
+ """
106
+
107
+ current_coordinate_index = 0
108
+ result = []
109
+
110
+ for distance in np.nditer(cumulative_distances):
111
+
112
+ # makes sure the current_coordinate_index does not exceed its maximum value
113
+ if current_coordinate_index > len(average_distances) - 1:
114
+ current_coordinate_index = len(average_distances) - 1
115
+
116
+ if distance > average_distances[current_coordinate_index]:
117
+ current_coordinate_index += 1
118
+ if current_coordinate_index > len(average_distances) - 1:
119
+ current_coordinate_index = len(average_distances) - 1
120
+
121
+ result.append(current_coordinate_index)
122
+
123
+ return np.array(result)
124
+
125
+ @staticmethod
126
+ def _python_calculate_closest_timestamp_indices(unix_timestamps, dt_local_array):
127
+ """
128
+
129
+ Python implementation to find the indices of the closest timestamps in dt_local_array and package them into a NumPy Array
130
+
131
+ :param np.ndarray unix_timestamps: NumPy Array (float[N]) of unix timestamps of the vehicle's journey
132
+ :param np.ndarray dt_local_array: NumPy Array (float[N]) of local times, represented as unix timestamps
133
+ :returns: NumPy Array of (int[N]) containing closest timestamp indices used by get_weather_forecast_in_time
134
+ :rtype: np.ndarray
135
+
136
+ """
137
+ closest_time_stamp_indices = []
138
+ for unix_timestamp in unix_timestamps:
139
+ unix_timestamp_array = np.full_like(dt_local_array, fill_value=unix_timestamp)
140
+ differences = np.abs(unix_timestamp_array - dt_local_array)
141
+ minimum_index = np.argmin(differences)
142
+ closest_time_stamp_indices.append(minimum_index)
143
+
144
+ return np.asarray(closest_time_stamp_indices, dtype=np.int32)
145
+
146
+ def get_weather_forecast_in_time(self, indices, unix_timestamps, start_time, tick) -> SolcastEnvironment:
147
+ """
148
+
149
+ Takes in an array of indices of the weather_forecast array, and an array of timestamps. Uses those to figure out
150
+ what the weather forecast is at each time step being simulated.
151
+
152
+ we only have weather at discrete timestamps. The car however can be in any timestamp in
153
+ between. Therefore, we must be able to choose the weather timestamp that is closest to the one that the car is in
154
+ so that we can more accurately determine the weather experienced by the car at that timestamp.
155
+
156
+ For example, imagine the car is at some coordinate (x,y) at timestamp 100. Imagine we know the weather forecast
157
+ at (x,y) for five different timestamps: 0, 30, 60, 90, and 120. Which weather forecast should we
158
+ choose? Clearly, we should choose the weather forecast at 90 since it is the closest to 100. That's what the
159
+ below code is accomplishing.
160
+
161
+ :param np.ndarray indices: (int[N]) coordinate indices of self.weather_forecast
162
+ :param np.ndarray unix_timestamps: (int[N]) unix timestamps of the vehicle's journey
163
+ :param int start_time: time since the start of the race that simulation is beginning
164
+ :param int tick: length of a tick in seconds
165
+ :returns: a SolcastEnvironment object with time_dt, latitude, longitude, wind_speed, wind_direction, and ghi.
166
+ :rtype: SolcastEnvironment
167
+ """
168
+ forecasts_array = core.weather_in_time(unix_timestamps.astype(np.int64), indices.astype(np.int64), self.weather_forecast, 0)
169
+
170
+ # roll_by_tick = int(3600 / tick) * helpers.hour_from_unix_timestamp(forecasts_array[0, 0])
171
+ # forecasts_array = np.roll(forecasts_array, -roll_by_tick, 0)
172
+
173
+ weather_object = SolcastEnvironment()
174
+
175
+ weather_object.time_dt = forecasts_array[:, 0]
176
+ weather_object.latitude = forecasts_array[:, 1]
177
+ weather_object.longitude = forecasts_array[:, 2]
178
+ weather_object.wind_speed = forecasts_array[:, 3]
179
+ weather_object.wind_direction = forecasts_array[:, 4]
180
+ weather_object.ghi = forecasts_array[:, 5]
181
+
182
+ return weather_object
183
+
184
+ def _python_get_weather_in_time(self, unix_timestamps, indices):
185
+ full_weather_forecast_at_coords = self.weather_forecast[indices]
186
+ dt_local_array = full_weather_forecast_at_coords[0, :, 0]
187
+
188
+ temp_0 = np.arange(0, full_weather_forecast_at_coords.shape[0])
189
+ closest_timestamp_indices = self._python_calculate_closest_timestamp_indices(unix_timestamps, dt_local_array)
190
+
191
+ return full_weather_forecast_at_coords[temp_0, closest_timestamp_indices]
192
+
193
+ @staticmethod
194
+ def _get_array_directional_wind_speed(vehicle_bearings, wind_speeds, wind_directions):
195
+ """
196
+
197
+ Returns the array of wind speed in m/s, in the direction opposite to the
198
+ bearing of the vehicle
199
+
200
+
201
+ :param np.ndarray vehicle_bearings: (float[N]) The azimuth angles that the vehicle in, in degrees
202
+ :param np.ndarray wind_speeds: (float[N]) The absolute speeds in m/s
203
+ :param np.ndarray wind_directions: (float[N]) The wind direction in the meteorlogical convention. To convert from meteorlogical convention to azimuth angle, use (x + 180) % 360
204
+ :returns: The wind speeds in the direction opposite to the bearing of the vehicle
205
+ :rtype: np.ndarray
206
+
207
+ """
208
+
209
+ # wind direction is 90 degrees meteorological, so it is 270 degrees azimuthal. car is 90 degrees
210
+ # cos(90 - 90) = cos(0) = 1. Wind speed is moving opposite to the car,
211
+ # car is 270 degrees, cos(90-270) = -1. Wind speed is in direction of the car.
212
+ return wind_speeds * (np.cos(np.radians(wind_directions - vehicle_bearings)))
213
+
214
+
215
+ if __name__ == "__main__":
216
+ pass
@@ -0,0 +1,9 @@
1
+ from .OpenWeatherForecast import OpenWeatherForecast
2
+ from .SolcastForecasts import SolcastForecasts
3
+ from .base_weather_forecasts import BaseWeatherForecasts
4
+
5
+ __all__ = [
6
+ "OpenWeatherForecast",
7
+ "SolcastForecasts",
8
+ "BaseWeatherForecasts"
9
+ ]
@@ -0,0 +1,57 @@
1
+ import functools
2
+ from abc import ABC, abstractmethod
3
+
4
+ import dill
5
+ import numpy as np
6
+ from simulation.common import Race, constants, helpers
7
+ from simulation.cache.weather import weather_directory
8
+ import logging
9
+ import os
10
+
11
+
12
+ class BaseWeatherForecasts(ABC):
13
+ def __init__(self, coords, race: Race, provider: str, origin_coord=None, hash_key=None):
14
+ self.race = race
15
+
16
+ if origin_coord is not None:
17
+ self.origin_coord = np.array(origin_coord)
18
+ else:
19
+ self.origin_coord = coords[0]
20
+ self.dest_coord = coords[-1]
21
+
22
+ if self.race.race_type == Race.ASC:
23
+ self.coords = coords[::constants.REDUCTION_FACTOR]
24
+ weather_file = weather_directory / f"weather_data_{provider}.npz"
25
+ elif self.race.race_type == Race.FSGP:
26
+ self.coords = np.array([coords[0], coords[-1]])
27
+ weather_file = weather_directory / f"weather_data_FSGP_{provider}.npz"
28
+ else:
29
+ raise ValueError(f"base_weather_forecasts has not implemented retrieving race {repr(self.race.race_type)}")
30
+
31
+ # if the file exists, load path from file
32
+ if os.path.isfile(weather_file):
33
+ if provider == "SOLCAST":
34
+ with open(weather_file, 'rb') as file:
35
+ weather_data = dill.load(file)
36
+ elif provider == "OPENWEATHER":
37
+ weather_data = np.load(weather_file)
38
+ else:
39
+ raise ValueError(f"base_weather_forecasts has not implemented retrieving provider {provider}")
40
+
41
+ if weather_data['hash'] == hash_key:
42
+
43
+ print("Previous weather save file is being used...\n")
44
+
45
+ self.weather_forecast = weather_data['weather_forecast']
46
+
47
+ else:
48
+ logging.error("Get or update cached weather data by invoking cache_data.py\nExiting simulation...")
49
+ exit()
50
+
51
+ @abstractmethod
52
+ def calculate_closest_weather_indices(self, cumulative_distances) -> np.ndarray:
53
+ raise NotImplementedError
54
+
55
+ @abstractmethod
56
+ def get_weather_forecast_in_time(self, indices, unix_timestamps, start_hour, tick) -> np.ndarray:
57
+ raise NotImplementedError