ubc-solar-physics 1.3.0__cp311-cp311-win_amd64.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 (55) hide show
  1. core.cp311-win_amd64.pyd +0 -0
  2. physics/__init__.py +14 -0
  3. physics/_version.py +16 -0
  4. physics/environment/__init__.py +15 -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 +600 -0
  14. physics/environment/meteorology/irradiant_meteorology.py +107 -0
  15. physics/environment/meteorology/meteorology.rs +138 -0
  16. physics/environment/meteorology.rs +1 -0
  17. physics/environment.rs +2 -0
  18. physics/lib.rs +132 -0
  19. physics/models/__init__.py +13 -0
  20. physics/models/arrays/__init__.py +7 -0
  21. physics/models/arrays/arrays.rs +0 -0
  22. physics/models/arrays/base_array.py +6 -0
  23. physics/models/arrays/basic_array.py +39 -0
  24. physics/models/arrays.rs +1 -0
  25. physics/models/battery/__init__.py +14 -0
  26. physics/models/battery/base_battery.py +29 -0
  27. physics/models/battery/basic_battery.py +140 -0
  28. physics/models/battery/battery.rs +78 -0
  29. physics/models/battery/battery_config.py +22 -0
  30. physics/models/battery/battery_config.toml +8 -0
  31. physics/models/battery/battery_model.py +135 -0
  32. physics/models/battery/kalman_filter.py +341 -0
  33. physics/models/battery.rs +1 -0
  34. physics/models/constants.py +23 -0
  35. physics/models/lvs/__init__.py +7 -0
  36. physics/models/lvs/base_lvs.py +6 -0
  37. physics/models/lvs/basic_lvs.py +18 -0
  38. physics/models/lvs/lvs.rs +0 -0
  39. physics/models/lvs.rs +1 -0
  40. physics/models/motor/__init__.py +7 -0
  41. physics/models/motor/base_motor.py +6 -0
  42. physics/models/motor/basic_motor.py +174 -0
  43. physics/models/motor/motor.rs +0 -0
  44. physics/models/motor.rs +1 -0
  45. physics/models/regen/__init__.py +7 -0
  46. physics/models/regen/base_regen.py +6 -0
  47. physics/models/regen/basic_regen.py +39 -0
  48. physics/models/regen/regen.rs +0 -0
  49. physics/models/regen.rs +1 -0
  50. physics/models.rs +5 -0
  51. ubc_solar_physics-1.3.0.dist-info/LICENSE +21 -0
  52. ubc_solar_physics-1.3.0.dist-info/METADATA +141 -0
  53. ubc_solar_physics-1.3.0.dist-info/RECORD +55 -0
  54. ubc_solar_physics-1.3.0.dist-info/WHEEL +5 -0
  55. ubc_solar_physics-1.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,600 @@
1
+ from physics.environment.meteorology.base_meteorology import BaseMeteorology
2
+ from physics.environment.gis.gis import calculate_path_distances
3
+ import numpy as np
4
+ from numba import jit
5
+ import core
6
+ from typing import Optional
7
+ import datetime
8
+
9
+
10
+ class CloudedMeteorology(BaseMeteorology):
11
+ """
12
+ CloudedMeteorology encapsulates meteorological data that includes
13
+ cloud cover, but not solar irradiance (necessitating manual computation).
14
+ """
15
+ def __init__(self, race, weather_forecasts):
16
+ super().__init__()
17
+
18
+ self._latitude: Optional[np.ndarray] = None
19
+ self._longitude: Optional[np.ndarray] = None
20
+ self._unix_time: Optional[np.ndarray] = None
21
+ self._cloud_cover: Optional[np.ndarray] = None
22
+
23
+ self._race = race
24
+ self._weather_forecast = weather_forecasts
25
+
26
+ self.S_0 = 1367.0 # Solar Constant, 1367W/m^2
27
+
28
+ self.last_updated_time = self._weather_forecast[0, 0, 2]
29
+
30
+ def spatially_localize(self, cumulative_distances: np.ndarray, simplify_weather: bool = False) -> None:
31
+ """
32
+
33
+ IMPORTANT: we only have weather coordinates for a discrete set of coordinates. However, the car could be at any
34
+ coordinate in between these available weather coordinates. We need to figure out what coordinate the car is at
35
+ at each timestep and then we can figure out the full weather forecast at each timestep.
36
+
37
+ For example, imagine the car is at some coordinate (10, 20). Further imagine that we have a week's worth of
38
+ weather forecasts for the following five coordinates: (5, 4), (11, 19), (20, 30), (40, 30), (0, 60). Which
39
+ set of weather forecasts should we choose? Well, we should choose the (11, 19) one since our coordinate
40
+ (10, 20) is closest to (11, 19). This is what the following code is accomplishing. However, it is not dealing
41
+ with the coordinates directly but rather is dealing with the distances between the coordinates.
42
+
43
+ Furthermore, once we have chosen a week's worth of weather forecasts for a specific coordinate, we must isolate
44
+ a single weather forecast depending on what time the car is at the coordinate (10, 20). That is the job of the
45
+ `get_weather_forecast_in_time()` method.
46
+
47
+ :param np.ndarray cumulative_distances: NumPy Array representing cumulative distances theoretically achievable for a given input speed array
48
+ :param bool simplify_weather: enable to only use a single weather coordinate (for track races without varying weather)
49
+ """
50
+
51
+ # If racing a track race, there is no need for distance calculations. We will return only the origin coordinate
52
+ # This characterizes the weather at every point along the FSGP tracks
53
+ # with the weather at a single coordinate on the track, which is great for reducing the API calls and is a
54
+ # reasonable assumption to make for FSGP only.
55
+ if simplify_weather:
56
+ self._weather_indices = np.zeros_like(cumulative_distances, dtype=int)
57
+ return
58
+
59
+ # a list of all the coordinates that we have weather data for
60
+ weather_coords = self._weather_forecast[:, 0, 0:2]
61
+
62
+ # distances between all the coordinates that we have weather data for
63
+ weather_path_distances = calculate_path_distances(weather_coords)
64
+ cumulative_weather_path_distances = np.cumsum(weather_path_distances)
65
+
66
+ # makes every even-index element negative, this allows the use of np.diff() to calculate the sum of consecutive
67
+ # elements
68
+ cumulative_weather_path_distances[::2] *= -1
69
+
70
+ # contains the average distance between two consecutive elements in the cumulative_weather_path_distances array
71
+ average_distances = np.abs(np.diff(cumulative_weather_path_distances) / 2)
72
+
73
+ return core.closest_weather_indices_loop(cumulative_distances, average_distances)
74
+
75
+ def temporally_localize(self, unix_timestamps, start_time, tick) -> None:
76
+ """
77
+
78
+ Takes in an array of indices of the weather_forecast array, and an array of timestamps. Uses those to figure out
79
+ what the weather forecast is at each time step being simulated.
80
+
81
+ we only have weather at discrete timestamps. The car however can be in any timestamp in
82
+ between. Therefore, we must be able to choose the weather timestamp that is closest to the one that the car is in
83
+ so that we can more accurately determine the weather experienced by the car at that timestamp.
84
+
85
+ For example, imagine the car is at some coordinate (x,y) at timestamp 100. Imagine we know the weather forecast
86
+ at (x,y) for five different timestamps: 0, 30, 60, 90, and 120. Which weather forecast should we
87
+ choose? Clearly, we should choose the weather forecast at 90 since it is the closest to 100. That's what the
88
+ below code is accomplishing.
89
+
90
+ :param np.ndarray unix_timestamps: (int[N]) unix timestamps of the vehicle's journey
91
+ :param int tick: length of a tick in seconds
92
+ :returns:
93
+ - A NumPy array of size [N][9]
94
+ - [9] (latitude, longitude, unix_time, timezone_offset, unix_time_corrected, wind_speed, wind_direction,
95
+ cloud_cover, precipitation, description):
96
+ :rtype: np.ndarray
97
+
98
+ """
99
+ weather_data = core.weather_in_time(unix_timestamps.astype(np.int64), self._weather_indices.astype(np.int64), self._weather_forecast, 4)
100
+ # roll_by_tick = int(3600 / tick) * (24 + start_hour - hour_from_unix_timestamp(weather_data[0, 2]))
101
+ # weather_data = np.roll(weather_data, -roll_by_tick, 0)
102
+
103
+ self._latitude = weather_data[:, 0]
104
+ self._longitude = weather_data[:, 1]
105
+ self._unix_time = weather_data[:, 2]
106
+ self._wind_speed = weather_data[:, 5]
107
+ self._wind_direction = weather_data[:, 6]
108
+ self._cloud_cover = weather_data[:, 7]
109
+
110
+ def calculate_solar_irradiances(self, coords, time_zones, local_times, elevations):
111
+ """
112
+
113
+ Calculates the Global Horizontal Irradiance from the Sun, relative to a location
114
+ on the Earth, for arrays of coordinates, times, elevations and weathers
115
+ https://www.pveducation.org/pvcdrom/properties-of-sunlight/calculation-of-solar-insolation
116
+ Note: If local_times and time_zones are both unadjusted for Daylight Savings, the
117
+ calculation will end up just the same
118
+
119
+ :param np.ndarray coords: (float[N][lat, lng]) array of latitudes and longitudes
120
+ :param np.ndarray time_zones: (int[N]) time zones at different locations in seconds relative to UTC
121
+ :param np.ndarray local_times: (int[N]) unix time that the vehicle will be at each location. (Adjusted for Daylight Savings)
122
+ :param np.ndarray elevations: (float[N]) elevation from sea level in m
123
+ :returns: (float[N]) Global Horizontal Irradiance in W/m2
124
+ :rtype: np.ndarray
125
+
126
+ """
127
+ day_of_year, local_time = core.calculate_array_ghi_times(local_times)
128
+
129
+ ghi = self._calculate_GHI(coords[:, 0], coords[:, 1], time_zones,
130
+ day_of_year, local_time, elevations, self._cloud_cover)
131
+
132
+ stationary_irradiance = self._calculate_angled_irradiance(coords[:, 0], coords[:, 1], time_zones, day_of_year,
133
+ local_time, elevations, self._cloud_cover)
134
+
135
+ # Use stationary irradiance when the car is not driving
136
+ effective_irradiance = np.where(
137
+ np.logical_not(self._race.driving_boolean),
138
+ stationary_irradiance,
139
+ ghi)
140
+
141
+ return effective_irradiance
142
+
143
+ @staticmethod
144
+ def _calculate_hour_angle(time_zone_utc, day_of_year, local_time, longitude):
145
+ """
146
+
147
+ Calculates and returns the Hour Angle of the Sun in the sky.
148
+ https://www.pveducation.org/pvcdrom/properties-of-sunlight/solar-time
149
+ Note: If local time and time_zone_utc are both unadjusted for Daylight Savings, the
150
+ calculation will end up just the same
151
+ :param np.ndarray time_zone_utc: The UTC time zone of your area in hours of UTC offset.
152
+ :param np.ndarray day_of_year: The number of the day of the current year, with January 1 being the first day of the year.
153
+ :param np.ndarray local_time: The local time in hours from midnight. (Adjust for Daylight Savings)
154
+ :param np.ndarray longitude: The longitude of a location on Earth
155
+ :returns: The Hour Angle in degrees.
156
+ :rtype: np.ndarray
157
+
158
+ """
159
+
160
+ lst = local_time_to_apparent_solar_time(time_zone_utc / 3600, day_of_year,
161
+ local_time, longitude)
162
+
163
+ hour_angle = 15 * (lst - 12)
164
+
165
+ return hour_angle
166
+
167
+ def _calculate_elevation_angle(self, latitude, longitude, time_zone_utc, day_of_year,
168
+ local_time):
169
+ """
170
+
171
+ Calculates the Elevation Angle of the Sun relative to a location on the Earth
172
+ https://www.pveducation.org/pvcdrom/properties-of-sunlight/elevation-angle
173
+ Note: If local time and time_zone_utc are both unadjusted for Daylight Savings, the
174
+ calculation will end up just the same
175
+
176
+ :param np.ndarray latitude: The latitude of a location on Earth
177
+ :param np.ndarray longitude: The longitude of a location on Earth
178
+ :param np.ndarray time_zone_utc: The UTC time zone of your area in hours of UTC offset. For example, Vancouver has time_zone_utc = -7
179
+ :param np.ndarray day_of_year: The number of the day of the current year, with January 1 being the first day of the year.
180
+ :param np.ndarray local_time: The local time in hours from midnight. (Adjust for Daylight Savings)
181
+ :returns: The elevation angle in degrees
182
+ :rtype: np.ndarray
183
+
184
+ """
185
+
186
+ # Negative declination angles: Northern Hemisphere winter
187
+ # 0 declination angle : Equinoxes (March 22, Sept 22)
188
+ # Positive declination angle: Northern Hemisphere summer
189
+ declination_angle = calculate_declination_angle(day_of_year)
190
+
191
+ # Negative hour angles: Morning
192
+ # 0 hour angle : Solar noon
193
+ # Positive hour angle: Afternoon
194
+ hour_angle = self._calculate_hour_angle(time_zone_utc, day_of_year,
195
+ local_time, longitude)
196
+ # From: https://en.wikipedia.org/wiki/Hour_angle#:~:text=At%20solar%20noon%20the%20hour,times%201.5%20hours%20before%20noon).
197
+ # "For example, at 10:30 AM local apparent time
198
+ # the hour angle is −22.5° (15° per hour times 1.5 hours before noon)."
199
+
200
+ # mathy part is delegated to a helper function to optimize for numba compilation
201
+ return compute_elevation_angle_math(declination_angle, hour_angle, latitude)
202
+
203
+ def _calculate_zenith_angle(self, latitude, longitude, time_zone_utc, day_of_year,
204
+ local_time):
205
+ """
206
+
207
+ Calculates the Zenith Angle of the Sun relative to a location on the Earth
208
+ https://www.pveducation.org/pvcdrom/properties-of-sunlight/azimuth-angle
209
+ Note: If local time and time_zone_utc are both unadjusted for Daylight Savings, the
210
+ calculation will end up just the same
211
+
212
+ :param latitude: The latitude of a location on Earth
213
+ :param longitude: The longitude of a location on Earth
214
+ :param time_zone_utc: The UTC time zone of your area in hours of UTC offset.
215
+ :param day_of_year: The number of the day of the current year, with January 1 being the first day of the year.
216
+ :param local_time: The local time in hours from midnight. (Adjust for Daylight Savings)
217
+ :return: The zenith angle in degrees
218
+ :rtype: float
219
+
220
+ """
221
+
222
+ elevation_angle = self._calculate_elevation_angle(latitude, longitude,
223
+ time_zone_utc, day_of_year, local_time)
224
+
225
+ return 90 - elevation_angle
226
+
227
+ def _calculate_azimuth_angle(self, latitude, longitude, time_zone_utc, day_of_year,
228
+ local_time):
229
+ """
230
+
231
+ Calculates the Azimuth Angle of the Sun relative to a location on the Earth.
232
+ https://www.pveducation.org/pvcdrom/properties-of-sunlight/azimuth-angle
233
+ Note: If local time and time_zone_utc are both unadjusted for Daylight Savings, the
234
+ calculation will end up just the same
235
+
236
+ :param latitude: The latitude of a location on Earth
237
+ :param longitude: The longitude of a location on Earth
238
+ :param time_zone_utc: The UTC time zone of your area in hours of UTC offset. For example, Vancouver has time_zone_utc = -7
239
+ :param day_of_year: The number of the day of the current year, with January 1 being the first day of the year.
240
+ :param local_time: The local time in hours from midnight. (Adjust for Daylight Savings)
241
+ :returns: The azimuth angle in degrees
242
+ :rtype: np.ndarray
243
+
244
+ """
245
+
246
+ declination_angle = calculate_declination_angle(day_of_year)
247
+ hour_angle = self._calculate_hour_angle(time_zone_utc, day_of_year,
248
+ local_time, longitude)
249
+
250
+ term_1 = np.sin(np.radians(declination_angle)) * \
251
+ np.sin(np.radians(latitude))
252
+
253
+ term_2 = np.cos(np.radians(declination_angle)) * \
254
+ np.sin(np.radians(latitude)) * \
255
+ np.cos(np.radians(hour_angle))
256
+
257
+ elevation_angle = self._calculate_elevation_angle(latitude, longitude,
258
+ time_zone_utc, day_of_year, local_time)
259
+
260
+ term_3 = np.float_(term_1 - term_2) / \
261
+ np.cos(np.radians(elevation_angle))
262
+
263
+ if term_3 < -1:
264
+ term_3 = -1
265
+ elif term_3 > 1:
266
+ term_3 = 1
267
+
268
+ azimuth_angle = np.arcsin(term_3)
269
+
270
+ return np.degrees(azimuth_angle)
271
+
272
+ # ----- Calculation of sunrise and sunset times -----
273
+
274
+ # ----- Calculation of modes of solar irradiance -----
275
+
276
+ def _calculate_DNI(self, latitude, longitude, time_zone_utc, day_of_year,
277
+ local_time, elevation):
278
+ """
279
+
280
+ Calculates the Direct Normal Irradiance from the Sun, relative to a location
281
+ on the Earth (clearsky)
282
+ https://www.pveducation.org/pvcdrom/properties-of-sunlight/calculation-of-solar-insolation
283
+ Note: If local time and time_zone_utc are both unadjusted for Daylight Savings, the
284
+ calculation will end up just the same
285
+
286
+ :param np.ndarray latitude: The latitude of a location on Earth
287
+ :param np.ndarray longitude: The longitude of a location on Earth
288
+ :param np.ndarray time_zone_utc: The UTC time zone of your area in hours of UTC offset.
289
+ :param np.ndarray day_of_year: The number of the day of the current year, with January 1 being the first day of the year.
290
+ :param np.ndarray local_time: The local time in hours from midnight. (Adjust for Daylight Savings)
291
+ :param np.ndarray elevation: The local elevation of a location in metres
292
+ :returns: The Direct Normal Irradiance in W/m2
293
+ :rtype: np.ndarray
294
+
295
+ """
296
+
297
+ zenith_angle = self._calculate_zenith_angle(latitude, longitude,
298
+ time_zone_utc, day_of_year, local_time)
299
+ a = 0.14
300
+
301
+ # https://www.pveducation.org/pvcdrom/properties-of-sunlight/air-mass
302
+ # air_mass = 1 / (math.cos(math.radians(zenith_angle)) + \
303
+ # 0.50572*pow((96.07995 - zenith_angle), -1.6364))
304
+
305
+ with np.errstate(invalid="ignore"):
306
+ air_mass = np.float_(1) / (np.float_(np.cos(np.radians(zenith_angle)))
307
+ + 0.50572*np.power((96.07995 - zenith_angle), -1.6364))
308
+
309
+ with np.errstate(over="ignore"):
310
+ DNI = self.S_0 * ((1 - a * elevation * 0.001) * np.power(0.7, np.power(air_mass, 0.678))
311
+ + a * elevation * 0.001)
312
+
313
+ return np.where(zenith_angle > 90, 0, DNI)
314
+
315
+ def _calculate_DHI(self, latitude, longitude, time_zone_utc, day_of_year,
316
+ local_time, elevation):
317
+ """
318
+
319
+ Calculates the Diffuse Horizontal Irradiance from the Sun, relative to a location
320
+ on the Earth (clearsky)
321
+ https://www.pveducation.org/pvcdrom/properties-of-sunlight/calculation-of-solar-insolation
322
+ Note: If local time and time_zone_utc are both unadjusted for Daylight Savings, the
323
+ calculation will end up just the same
324
+
325
+ :param np.ndarray latitude: The latitude of a location on Earth
326
+ :param np.ndarray longitude: The longitude of a location on Earth
327
+ :param np.ndarray time_zone_utc: The UTC time zone of your area in hours of UTC offset.
328
+ :param np.ndarray np.ndarray day_of_year: The number of the day of the current year, with January 1 being the first day of the year.
329
+ :param np.ndarray local_time: The local time in hours from midnight
330
+ :param np.ndarray elevation: The local elevation of a location in metres
331
+ :returns: The Diffuse Horizontal Irradiance in W/m2
332
+ :rtype: np.ndarray
333
+
334
+ """
335
+
336
+ DNI = self._calculate_DNI(latitude, longitude, time_zone_utc, day_of_year,
337
+ local_time, elevation)
338
+
339
+ DHI = 0.1 * DNI
340
+
341
+ return DHI
342
+
343
+ def _calculate_GHI(self, latitude, longitude, time_zone_utc, day_of_year,
344
+ local_time, elevation, cloud_cover):
345
+ """
346
+
347
+ Calculates the Global Horizontal Irradiance from the Sun, relative to a location
348
+ on the Earth
349
+ https://www.pveducation.org/pvcdrom/properties-of-sunlight/calculation-of-solar-insolation
350
+ Note: If local time and time_zone_utc are both unadjusted for Daylight Savings, the
351
+ calculation will end up just the same
352
+
353
+ :param np.ndarray latitude: The latitude of a location on Earth
354
+ :param np.ndarray longitude: The longitude of a location on Earth
355
+ :param np.ndarray time_zone_utc: The UTC time zone of your area in hours of UTC offset, without including the effects of Daylight Savings Time. For example, Vancouver has time_zone_utc = -8 year-round.
356
+ :param np.ndarray day_of_year: The number of the day of the current year, with January 1 being the first day of the year.
357
+ :param np.ndarray local_time: The local time in hours from midnight.
358
+ :param np.ndarray elevation: The local elevation of a location in metres
359
+ :param np.ndarray cloud_cover: A NumPy array representing cloud cover as a percentage from 0 to 100
360
+ :returns: The Global Horizontal Irradiance in W/m^2
361
+ :rtype: np.ndarray
362
+
363
+ """
364
+
365
+ DHI = self._calculate_DHI(latitude, longitude, time_zone_utc, day_of_year,
366
+ local_time, elevation)
367
+
368
+ DNI = self._calculate_DNI(latitude, longitude, time_zone_utc, day_of_year,
369
+ local_time, elevation)
370
+
371
+ zenith_angle = self._calculate_zenith_angle(latitude, longitude,
372
+ time_zone_utc, day_of_year, local_time)
373
+
374
+ GHI = DNI * np.cos(np.radians(zenith_angle)) + DHI
375
+
376
+ return self._apply_cloud_cover(GHI=GHI, cloud_cover=cloud_cover)
377
+
378
+ @staticmethod
379
+ def _apply_cloud_cover(GHI, cloud_cover):
380
+ """
381
+
382
+ Applies a cloud cover model to the GHI data.
383
+
384
+ Cloud cover adjustment follows the equation laid out here:
385
+ http://www.shodor.org/os411/courses/_master/tools/calculators/solarrad/
386
+
387
+ :param np.ndarray GHI: Global Horizontal Index in W/m^2
388
+ :param np.ndarray cloud_cover: A NumPy array representing cloud cover as a percentage from 0 to 100
389
+
390
+ :returns: GHI after considering cloud cover data
391
+ :rtype: np.ndarray
392
+
393
+ """
394
+
395
+ assert np.logical_and(cloud_cover >= 0, cloud_cover <= 100).all()
396
+
397
+ scaled_cloud_cover = cloud_cover / 100
398
+
399
+ assert np.logical_and(scaled_cloud_cover >= 0,
400
+ scaled_cloud_cover <= 1).all()
401
+
402
+ return GHI * (1 - (0.75 * np.power(scaled_cloud_cover, 3.4)))
403
+
404
+ # ----- Calculation of modes of solar irradiance, but returning numpy arrays -----
405
+ @staticmethod
406
+ def _date_convert(date):
407
+ """
408
+
409
+ Convert a date into local time.
410
+
411
+ :param datetime.datetime date: date to be converted
412
+ :return: a date converted into local time.
413
+ :rtype: int
414
+
415
+ """
416
+
417
+ return date.hour + (float(date.minute * 60 + date.second) / 3600)
418
+
419
+ def _calculate_angled_irradiance(self, latitude, longitude, time_zone_utc, day_of_year,
420
+ local_time, elevation, cloud_cover, array_angles=np.array([0, 15, 30, 45])):
421
+ """
422
+
423
+ Determine the direct and diffuse irradiance on an array which can be mounted at different angles.
424
+ During stationary charging, the car can mount the array at different angles, resulting in a higher
425
+ component of direct irradiance captured.
426
+
427
+ Uses the GHI formula, GHI = DNI*cos(zenith)+DHI but with an 'effective zenith',
428
+ the angle between the mounted panel's normal and the sun.
429
+
430
+ :param np.ndarray latitude: The latitude of a location on Earth
431
+ :param np.ndarray longitude: The longitude of a location on Earth
432
+ :param np.ndarray time_zone_utc: The UTC time zone of your area in hours of UTC offset, without including the effects of Daylight Savings Time. For example, Vancouver has time_zone_utc = -8 year-round.
433
+ :param np.ndarray day_of_year: The number of the day of the current year, with January 1 being the first day of the year.
434
+ :param np.ndarray local_time: The local time in hours from midnight.
435
+ :param np.ndarray elevation: The local elevation of a location in metres
436
+ :param np.ndarray cloud_cover: A NumPy array representing cloud cover as a percentage from 0 to 100
437
+ :param np.ndarray array_angles: An array containing the discrete angles on which the array can be mounted
438
+ :returns: The "effective Global Horizontal Irradiance" in W/m^2
439
+ :rtype: np.ndarray
440
+
441
+ """
442
+
443
+ DHI = self._calculate_DHI(latitude, longitude, time_zone_utc, day_of_year,
444
+ local_time, elevation)
445
+
446
+ DNI = self._calculate_DNI(latitude, longitude, time_zone_utc, day_of_year,
447
+ local_time, elevation)
448
+
449
+ zenith_angle = self._calculate_zenith_angle(latitude, longitude,
450
+ time_zone_utc, day_of_year, local_time)
451
+
452
+ # Calculate the absolute differences
453
+ differences = np.abs(zenith_angle[:, np.newaxis] - array_angles)
454
+
455
+ # Find the minimum difference for each element in zenith_angle
456
+ effective_zenith = np.min(differences, axis=1)
457
+
458
+ # Now effective_zenith contains the minimum absolute difference for each element in zenith_angle
459
+
460
+ GHI = DNI * np.cos(np.radians(effective_zenith)) + DHI
461
+
462
+ return self._apply_cloud_cover(GHI=GHI, cloud_cover=cloud_cover)
463
+
464
+
465
+ def local_time_to_apparent_solar_time(time_zone_utc, day_of_year, local_time,
466
+ longitude):
467
+ """
468
+
469
+ Converts between the local time to the apparent solar time and returns the apparent
470
+ solar time.
471
+ https://www.pveducation.org/pvcdrom/properties-of-sunlight/solar-time
472
+
473
+ Note: If local time and time_zone_utc are both unadjusted for Daylight Savings, the
474
+ calculation will end up just the same
475
+
476
+ :param np.ndarray time_zone_utc: The UTC time zone of your area in hours of UTC offset.
477
+ :param np.ndarray day_of_year: The number of the day of the current year, with January 1 being the first day of the year.
478
+ :param np.ndarray local_time: The local time in hours from midnight (Adjust for Daylight Savings)
479
+ :param np.ndarray longitude: The longitude of a location on Earth
480
+ :returns: The Apparent Solar Time of a location, in hours from midnight
481
+ :rtype: np.ndarray
482
+
483
+ """
484
+
485
+ lstm = calculate_LSTM(time_zone_utc)
486
+ eot = calculate_eot_correction(day_of_year)
487
+
488
+ # local solar time
489
+ lst = local_time + np.float_(longitude - lstm) / 15 + np.float_(eot) / 60
490
+
491
+ return lst
492
+
493
+
494
+ @jit(nopython=True)
495
+ def calculate_LSTM(time_zone_utc):
496
+ """
497
+
498
+ Calculates and returns the LSTM, or Local Solar Time Meridian.
499
+ https://www.pveducation.org/pvcdrom/properties-of-sunlight/solar-time
500
+
501
+ :param np.ndarray time_zone_utc: The UTC time zone of your area in hours of UTC offset.
502
+ :returns: The Local Solar Time Meridian in degrees
503
+ :rtype: np.ndarray
504
+
505
+ """
506
+
507
+ return 15 * time_zone_utc
508
+
509
+
510
+ @jit(nopython=True)
511
+ def calculate_eot_correction(day_of_year):
512
+ """
513
+
514
+ Approximates and returns the correction factor between the apparent
515
+ solar time and the mean solar time
516
+
517
+ :param np.ndarray day_of_year: The number of the day of the current year, with January 1 being the first day of the year.
518
+ :returns: The Equation of Time correction EoT in minutes, where apparent Solar Time = Mean Solar Time + EoT
519
+ :rtype: np.ndarray
520
+
521
+ """
522
+
523
+ b = np.radians((np.float_(360) / 364) * (day_of_year - 81))
524
+
525
+ eot = 9.87 * np.sin(2 * b) - 7.83 * np.cos(b) - 1.5 * np.sin(b)
526
+
527
+ return eot
528
+
529
+
530
+ def get_day_of_year_map(date):
531
+ """
532
+
533
+ Extracts day, month, year, from datetime object
534
+
535
+ :param datetime.date date: date to be decomposed
536
+
537
+ """
538
+ return get_day_of_year(date.day, date.month, date.year)
539
+
540
+
541
+ def get_day_of_year(day, month, year):
542
+ """
543
+
544
+ Calculates the day of the year, given the day, month and year.
545
+ Day refers to a number representing the nth day of the year. So, Jan 1st will be the 1st day of the year
546
+
547
+ :param int day: nth day of the year
548
+ :param int month: month
549
+ :param int year: year
550
+ :returns: day of year
551
+ :rtype: int
552
+
553
+ """
554
+
555
+ return (datetime.date(year, month, day) - datetime.date(year, 1, 1)).days + 1
556
+
557
+
558
+ @jit(nopython=True)
559
+ def calculate_declination_angle(day_of_year):
560
+ """
561
+
562
+ Calculates the Declination Angle of the Earth at a given day
563
+ https://www.pveducation.org/pvcdrom/properties-of-sunlight/declination-angle
564
+
565
+ :param np.ndarray day_of_year: The number of the day of the current year, with January 1 being the first day of the year.
566
+ :returns: The declination angle of the Earth relative to the Sun, in degrees
567
+ :rtype: np.ndarray
568
+
569
+ """
570
+
571
+ declination_angle = -23.45 * np.cos(np.radians((np.float_(360) / 365) *
572
+ (day_of_year + 10)))
573
+
574
+ return declination_angle
575
+
576
+
577
+ @jit(nopython=True)
578
+ def compute_elevation_angle_math(declination_angle, hour_angle, latitude):
579
+ """
580
+
581
+ Gets the two terms to calculate and return elevation angle, given the
582
+ declination angle, hour angle, and latitude.
583
+
584
+ This method separates the math part of the calculation from its caller
585
+ method to optimize for numba compilation.
586
+
587
+ :param np.ndarray latitude: array of latitudes
588
+ :param np.ndarray declination_angle: The declination angle of the Earth relative to the Sun
589
+ :param np.ndarray hour_angle: The hour angle of the sun in the sky
590
+ :returns: The elevation angle in degrees
591
+ :rtype: np.ndarray
592
+
593
+ """
594
+
595
+ term_1 = np.sin(np.radians(declination_angle)) * np.sin(np.radians(latitude))
596
+ term_2 = np.cos(np.radians(declination_angle)) * np.cos(np.radians(latitude)) * np.cos(np.radians(hour_angle))
597
+ elevation_angle = np.arcsin(term_1 + term_2)
598
+
599
+ return np.degrees(elevation_angle)
600
+