-
Notifications
You must be signed in to change notification settings - Fork 0
/
generate.py
207 lines (159 loc) Β· 6.84 KB
/
generate.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
"""functions for generating new fields"""
import numpy as np
import pandas as pd
from skyfield import api as skyfield_api
from skyfield import almanac
from nowcastlib.pipeline import structs
# {{{ sec_day
def _trig_sec(datetime_series, trig_func):
"""takes the sine or cos of the current second of the day"""
seconds_in_day = 24 * 60 * 60
secs_since_midnight = (
datetime_series - datetime_series.dt.normalize()
).total_seconds()
return trig_func((2 * np.pi * secs_since_midnight) / seconds_in_day)
def _sin_sec(datetime_series):
"""takes the sine of the current second of the day"""
return _trig_sec(datetime_series, np.sin)
def _cos_sec(datetime_series):
"""takes the cosine of the current second of the day"""
return _trig_sec(datetime_series, np.cos)
# }}}
# {{{ day_year
def _trig_day_year(datetime_series, trig_func):
"""takes the sine or cosine of the current day of the year"""
days_in_year = np.where(datetime_series.dt.is_leap_year, 366, 365)
days_since_newyears = datetime_series.dt.day_of_year
return trig_func((2 * np.pi * days_since_newyears) / days_in_year)
def _sin_day_year(datetime_series):
"""takes the sine of the current day of the year"""
return _trig_day_year(datetime_series, np.sin)
def _cos_day_year(datetime_series):
"""takes the cosine of the current day of the year"""
return _trig_day_year(datetime_series, np.cos)
# }}}
# {{{ day_week
def _trig_day_week(datetime_series, trig_func):
"""takes the sine or cosine of the current day number of the week (7)"""
days_in_week = 7
day_of_week = datetime_series.dt.day_of_week + 1
return trig_func((2 * np.pi * day_of_week) / days_in_week)
def _sin_day_week(datetime_series):
"""takes the sine of the current day number of the week (7)"""
return _trig_day_week(datetime_series, np.sin)
def _cos_day_week(datetime_series):
"""takes the cosine of the current day number of the week (7)"""
return _trig_day_week(datetime_series, np.cos)
# }}}
# {{{ month_year
def _trig_month_year(datetime_series, trig_func):
"""takes the sine or cosine of the current month number of the year (12)"""
months_in_year = 12
month_of_year = datetime_series.dt.month
return trig_func((2 * np.pi * month_of_year) / months_in_year)
def _sin_month_year(datetime_series):
"""takes the sine of the current month number of the year (12)"""
return _trig_month_year(datetime_series, np.sin)
def _cos_month_year(datetime_series):
"""takes the cosine of the current month number of the year (12)"""
return _trig_month_year(datetime_series, np.cos)
# }}}
def _is_weekend(datetime_series):
"""returns a boolean series of 1 if weekend and 0 otherwise"""
return (datetime_series.dt.day_of_week >= 4).astype(int)
# {{{ time since last sunset
def _t_since_sunset(
datetime_series,
lat=-24.6275,
lon=-70.4044,
elevation=2635,
):
"""returns how many seconds have elapsed since sunset"""
# loading skyfield objects
eph = skyfield_api.load("de421.bsp")
skyfield_ts = skyfield_api.load.timescale()
location = skyfield_api.wgs84.latlon(lat, lon, elevation_m=elevation)
# start and end time in UTC in python datetime format
start_ts = skyfield_ts.from_datetime(
# minus one day because we are interested in _previous_ sunset
(datetime_series[0] - pd.to_timedelta(1, unit="d"))
.tz_localize("UTC")
.to_pydatetime()
)
end_ts = skyfield_ts.from_datetime(
datetime_series[-1].tz_localize("UTC").to_pydatetime()
)
# find when sunsets occurred between our start and end dates
times, rise_set_mask = almanac.find_discrete(
start_ts, end_ts, almanac.sunrise_sunset(eph, location)
)
sunsets = pd.to_datetime(times.utc_iso())[
~np.array(rise_set_mask).astype(bool)
].tz_localize(None)
# find when the most recent sunset was for each timestep in input datetime series
sunset_idxs = np.zeros(len(datetime_series), dtype=int)
for sunset in sunsets[1:]:
change = np.where(datetime_series > sunset)[0][0]
sunset_idxs[change:] += 1
prev_sunset = sunsets[sunset_idxs]
# find how many seconds since most recent sunset for each timestep
return (datetime_series - prev_sunset).dt.total_seconds()
def _trig_t_since_sunset(
datetime_series, trig_func, lat=-24.6275, lon=-70.4044, elevation=2635
):
"""
returns the sine or cosine of how many seconds have
elapsed since sunset out of 86400
"""
seconds_in_day = 60 * 60 * 24
return trig_func(
2
* np.pi
* _t_since_sunset(datetime_series, lat, lon, elevation)
/ seconds_in_day
)
def _sin_t_since_sunset(datetime_series, lat=-24.6275, lon=-70.4044, elevation=2635):
"""
returns the cosine of how many seconds have
elapsed since sunset out of 86400
"""
return _trig_t_since_sunset(datetime_series, np.sin, lat, lon, elevation)
def _cos_t_since_sunset(datetime_series, lat=-24.6275, lon=-70.4044, elevation=2635):
"""
returns the sine of how many seconds have
elapsed since sunset out of 86400
"""
return _trig_t_since_sunset(datetime_series, np.cos, lat, lon, elevation)
# }}}
def _sun_elevation(datetime_series, lat=-24.6275, lon=-70.4044, elevation=2635):
"""returns the sun elevation in degrees at each timestamp for a given location"""
# load skyfield reqs
eph = skyfield_api.load("de421.bsp")
skyfield_ts = skyfield_api.load.timescale()
# specify location is relative to earth in the context of solar system barycentre
location = eph["earth"] + skyfield_api.wgs84.latlon(lat, lon, elevation_m=elevation)
# get the astrometric and apparent positions of the sun from this location
astrometric_pos = location.at(
skyfield_ts.from_datetimes(
datetime_series.dt.tz_localize("UTC").to_pydatetime()
)
).observe(eph["sun"])
apparent_pos = astrometric_pos.apparent()
# finally can calculate the altitude (aka the elevation), along with other metrics
alt, _, _ = apparent_pos.altaz()
return alt.degrees
function_map: dict = {
structs.GeneratorFunction.SUN_ELEVATION: _sun_elevation,
structs.GeneratorFunction.T_SINCE_SUNSET: _t_since_sunset,
structs.GeneratorFunction.SIN_T_SINCE_SUNSET: _sin_t_since_sunset,
structs.GeneratorFunction.COS_T_SINCE_SUNSET: _cos_t_since_sunset,
structs.GeneratorFunction.SIN_SEC: _sin_sec,
structs.GeneratorFunction.COS_SEC: _cos_sec,
structs.GeneratorFunction.SIN_DAY_YEAR: _sin_day_year,
structs.GeneratorFunction.COS_DAY_YEAR: _cos_day_year,
structs.GeneratorFunction.SIN_DAY_WEEK: _sin_day_week,
structs.GeneratorFunction.COS_DAY_WEEK: _cos_day_week,
structs.GeneratorFunction.SIN_MONTH_YEAR: _sin_month_year,
structs.GeneratorFunction.COS_MONTH_YEAR: _cos_month_year,
structs.GeneratorFunction.IS_WEEKEND: _is_weekend,
}