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.
- core.cpython-311-darwin.so +0 -0
- physics/__init__.py +14 -0
- physics/environment/__init__.py +33 -0
- physics/environment/base_environment.py +62 -0
- physics/environment/environment.rs +3 -0
- physics/environment/gis/__init__.py +7 -0
- physics/environment/gis/base_gis.py +24 -0
- physics/environment/gis/gis.py +374 -0
- physics/environment/gis/gis.rs +25 -0
- physics/environment/gis.rs +1 -0
- physics/environment/openweather_environment.py +18 -0
- physics/environment/race.py +89 -0
- physics/environment/solar_calculations/OpenweatherSolarCalculations.py +529 -0
- physics/environment/solar_calculations/SolcastSolarCalculations.py +41 -0
- physics/environment/solar_calculations/__init__.py +9 -0
- physics/environment/solar_calculations/base_solar_calculations.py +9 -0
- physics/environment/solar_calculations/solar_calculations.rs +24 -0
- physics/environment/solar_calculations.rs +1 -0
- physics/environment/solcast_environment.py +18 -0
- physics/environment/weather_forecasts/OpenWeatherForecast.py +308 -0
- physics/environment/weather_forecasts/SolcastForecasts.py +216 -0
- physics/environment/weather_forecasts/__init__.py +9 -0
- physics/environment/weather_forecasts/base_weather_forecasts.py +57 -0
- physics/environment/weather_forecasts/weather_forecasts.rs +116 -0
- physics/environment/weather_forecasts.rs +1 -0
- physics/environment.rs +3 -0
- physics/lib.rs +76 -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 +7 -0
- physics/models/battery/base_battery.py +29 -0
- physics/models/battery/basic_battery.py +141 -0
- physics/models/battery/battery.rs +0 -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 +7 -0
- physics/models/motor/base_motor.py +6 -0
- physics/models/motor/basic_motor.py +179 -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 +39 -0
- physics/models/regen/regen.rs +0 -0
- physics/models/regen.rs +1 -0
- physics/models.rs +5 -0
- ubc_solar_physics-0.1.0.dist-info/LICENSE +21 -0
- ubc_solar_physics-0.1.0.dist-info/METADATA +44 -0
- ubc_solar_physics-0.1.0.dist-info/RECORD +60 -0
- ubc_solar_physics-0.1.0.dist-info/WHEEL +5 -0
- 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,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
|