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