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