ctao-calibpipe 0.1.0rc7__py3-none-any.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.
Potentially problematic release.
This version of ctao-calibpipe might be problematic. Click here for more details.
- calibpipe/__init__.py +5 -0
- calibpipe/_dev_version/__init__.py +9 -0
- calibpipe/_version.py +21 -0
- calibpipe/atmosphere/__init__.py +1 -0
- calibpipe/atmosphere/atmosphere_containers.py +109 -0
- calibpipe/atmosphere/meteo_data_handlers.py +485 -0
- calibpipe/atmosphere/models/README.md +14 -0
- calibpipe/atmosphere/models/__init__.py +1 -0
- calibpipe/atmosphere/models/macobac.ecsv +23 -0
- calibpipe/atmosphere/models/reference_MDPs/__init__.py +1 -0
- calibpipe/atmosphere/models/reference_MDPs/ref_density_at_15km_ctao-north_intermediate.ecsv +8 -0
- calibpipe/atmosphere/models/reference_MDPs/ref_density_at_15km_ctao-north_summer.ecsv +8 -0
- calibpipe/atmosphere/models/reference_MDPs/ref_density_at_15km_ctao-north_winter.ecsv +8 -0
- calibpipe/atmosphere/models/reference_MDPs/ref_density_at_15km_ctao-south_summer.ecsv +8 -0
- calibpipe/atmosphere/models/reference_MDPs/ref_density_at_15km_ctao-south_winter.ecsv +8 -0
- calibpipe/atmosphere/models/reference_atmospheres/__init__.py +1 -0
- calibpipe/atmosphere/models/reference_atmospheres/reference_atmo_model_v0_ctao-north_intermediate.ecsv +73 -0
- calibpipe/atmosphere/models/reference_atmospheres/reference_atmo_model_v0_ctao-north_summer.ecsv +73 -0
- calibpipe/atmosphere/models/reference_atmospheres/reference_atmo_model_v0_ctao-north_winter.ecsv +73 -0
- calibpipe/atmosphere/models/reference_atmospheres/reference_atmo_model_v0_ctao-south_summer.ecsv +73 -0
- calibpipe/atmosphere/models/reference_atmospheres/reference_atmo_model_v0_ctao-south_winter.ecsv +73 -0
- calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/__init__.py +1 -0
- calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/reference_rayleigh_extinction_profile_v0_ctao-north_intermediate.ecsv +857 -0
- calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/reference_rayleigh_extinction_profile_v0_ctao-north_summer.ecsv +857 -0
- calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/reference_rayleigh_extinction_profile_v0_ctao-north_winter.ecsv +857 -0
- calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/reference_rayleigh_extinction_profile_v0_ctao-south_summer.ecsv +857 -0
- calibpipe/atmosphere/models/reference_rayleigh_scattering_profiles/reference_rayleigh_extinction_profile_v0_ctao-south_winter.ecsv +857 -0
- calibpipe/atmosphere/templates/request_templates/__init__.py +1 -0
- calibpipe/atmosphere/templates/request_templates/copernicus.json +11 -0
- calibpipe/atmosphere/templates/request_templates/gdas.json +12 -0
- calibpipe/core/__init__.py +39 -0
- calibpipe/core/common_metadata_containers.py +195 -0
- calibpipe/core/exceptions.py +87 -0
- calibpipe/database/__init__.py +24 -0
- calibpipe/database/adapter/__init__.py +23 -0
- calibpipe/database/adapter/adapter.py +80 -0
- calibpipe/database/adapter/database_containers/__init__.py +61 -0
- calibpipe/database/adapter/database_containers/atmosphere.py +199 -0
- calibpipe/database/adapter/database_containers/common_metadata.py +148 -0
- calibpipe/database/adapter/database_containers/container_map.py +59 -0
- calibpipe/database/adapter/database_containers/observatory.py +61 -0
- calibpipe/database/adapter/database_containers/table_version_manager.py +39 -0
- calibpipe/database/adapter/database_containers/version_control.py +17 -0
- calibpipe/database/connections/__init__.py +28 -0
- calibpipe/database/connections/calibpipe_database.py +60 -0
- calibpipe/database/connections/postgres_utils.py +97 -0
- calibpipe/database/connections/sql_connection.py +103 -0
- calibpipe/database/connections/user_confirmation.py +19 -0
- calibpipe/database/interfaces/__init__.py +71 -0
- calibpipe/database/interfaces/hashable_row_data.py +54 -0
- calibpipe/database/interfaces/queries.py +180 -0
- calibpipe/database/interfaces/sql_column_info.py +67 -0
- calibpipe/database/interfaces/sql_metadata.py +6 -0
- calibpipe/database/interfaces/sql_table_info.py +131 -0
- calibpipe/database/interfaces/table_handler.py +351 -0
- calibpipe/database/interfaces/types.py +96 -0
- calibpipe/tests/data/atmosphere/molecular_atmosphere/__init__.py +0 -0
- calibpipe/tests/data/atmosphere/molecular_atmosphere/contemporary_MDP.ecsv +34 -0
- calibpipe/tests/data/atmosphere/molecular_atmosphere/macobac.csv +852 -0
- calibpipe/tests/data/atmosphere/molecular_atmosphere/macobac.ecsv +23 -0
- calibpipe/tests/data/atmosphere/molecular_atmosphere/merged_file.ecsv +1082 -0
- calibpipe/tests/data/atmosphere/molecular_atmosphere/meteo_data_copernicus.ecsv +1082 -0
- calibpipe/tests/data/atmosphere/molecular_atmosphere/meteo_data_gdas.ecsv +66 -0
- calibpipe/tests/data/atmosphere/molecular_atmosphere/observatory_configurations.json +71 -0
- calibpipe/tests/data/utils/__init__.py +0 -0
- calibpipe/tests/data/utils/meteo_data_winter_and_summer.ecsv +12992 -0
- calibpipe/tests/unittests/atmosphere/astral_testing.py +107 -0
- calibpipe/tests/unittests/atmosphere/test_meteo_data_handler.py +775 -0
- calibpipe/tests/unittests/atmosphere/test_molecular_atmosphere.py +327 -0
- calibpipe/tests/unittests/database/test_table_handler.py +66 -0
- calibpipe/tests/unittests/database/test_types.py +38 -0
- calibpipe/tests/unittests/test_bootstrap_db.py +79 -0
- calibpipe/tests/unittests/utils/test_observatory.py +309 -0
- calibpipe/tools/atmospheric_base_tool.py +78 -0
- calibpipe/tools/atmospheric_model_db_loader.py +181 -0
- calibpipe/tools/basic_tool_with_db.py +38 -0
- calibpipe/tools/contemporary_mdp_producer.py +87 -0
- calibpipe/tools/init_db.py +37 -0
- calibpipe/tools/macobac_calculator.py +82 -0
- calibpipe/tools/molecular_atmospheric_model_producer.py +197 -0
- calibpipe/tools/observatory_data_db_loader.py +71 -0
- calibpipe/tools/reference_atmospheric_model_selector.py +201 -0
- calibpipe/utils/__init__.py +10 -0
- calibpipe/utils/observatory.py +486 -0
- calibpipe/utils/observatory_containers.py +26 -0
- calibpipe/version.py +24 -0
- ctao_calibpipe-0.1.0rc7.dist-info/METADATA +86 -0
- ctao_calibpipe-0.1.0rc7.dist-info/RECORD +93 -0
- ctao_calibpipe-0.1.0rc7.dist-info/WHEEL +5 -0
- ctao_calibpipe-0.1.0rc7.dist-info/entry_points.txt +8 -0
- ctao_calibpipe-0.1.0rc7.dist-info/licenses/AUTHORS.md +13 -0
- ctao_calibpipe-0.1.0rc7.dist-info/licenses/LICENSE +21 -0
- ctao_calibpipe-0.1.0rc7.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
"""Utility module to manage observatory data."""
|
|
2
|
+
|
|
3
|
+
# Python built-in imports
|
|
4
|
+
from calendar import month_abbr, monthrange
|
|
5
|
+
from datetime import date, datetime, timedelta
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from functools import cached_property
|
|
8
|
+
|
|
9
|
+
# Third-party imports
|
|
10
|
+
import astral
|
|
11
|
+
import astropy.units as u
|
|
12
|
+
from astral.sun import sun
|
|
13
|
+
from astropy.coordinates import Latitude, Longitude
|
|
14
|
+
|
|
15
|
+
# CTA-related imports
|
|
16
|
+
from ctapipe.core.component import Component
|
|
17
|
+
from ctapipe.core.traits import (
|
|
18
|
+
CaselessStrEnum,
|
|
19
|
+
Float,
|
|
20
|
+
Int,
|
|
21
|
+
List,
|
|
22
|
+
)
|
|
23
|
+
from traitlets.config import Config
|
|
24
|
+
|
|
25
|
+
# Internal imports
|
|
26
|
+
from .observatory_containers import ObservatoryContainer, SeasonContainer
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SeasonAlias(Enum):
|
|
30
|
+
"""Seasons aliases."""
|
|
31
|
+
|
|
32
|
+
SUMMER = "SUMMER"
|
|
33
|
+
WINTER = "WINTER"
|
|
34
|
+
SPRING = "INTERMEDIATE"
|
|
35
|
+
FALL = "INTERMEDIATE"
|
|
36
|
+
INTERMEDIATE = "INTERMEDIATE"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Season(Component):
|
|
40
|
+
"""Class, describing nature seasons."""
|
|
41
|
+
|
|
42
|
+
name = CaselessStrEnum(
|
|
43
|
+
values=["SPRING", "SUMMER", "FALL", "WINTER", "INTERMEDIATE"],
|
|
44
|
+
help="Season name (e.g. summer)",
|
|
45
|
+
).tag(config=True)
|
|
46
|
+
start_month = Int(help="Start month of the season", allow_none=False).tag(
|
|
47
|
+
config=True
|
|
48
|
+
)
|
|
49
|
+
stop_month = Int(help="Stop month of the season", allow_none=False).tag(config=True)
|
|
50
|
+
start_day = Int(
|
|
51
|
+
default_value=None, help="Start day of the season", allow_none=True
|
|
52
|
+
).tag(config=True)
|
|
53
|
+
stop_day = Int(
|
|
54
|
+
default_value=None, help="Stop day of the season", allow_none=True
|
|
55
|
+
).tag(config=True)
|
|
56
|
+
|
|
57
|
+
_leap_year = (
|
|
58
|
+
2000 # Arbitrary leap year. Do not change unless you know what you're doing.
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def __init__(self, config=None, parent=None, **kwargs):
|
|
62
|
+
super().__init__(config=config, parent=parent, **kwargs)
|
|
63
|
+
self.name = self.name.upper()
|
|
64
|
+
start = date(
|
|
65
|
+
year=self._leap_year, month=self.start_month, day=self.start_day or 1
|
|
66
|
+
)
|
|
67
|
+
stop = date(
|
|
68
|
+
year=self._leap_year,
|
|
69
|
+
month=self.stop_month,
|
|
70
|
+
day=self.stop_day or monthrange(self._leap_year, self.stop_month)[1],
|
|
71
|
+
)
|
|
72
|
+
if start > stop:
|
|
73
|
+
self.log.debug(
|
|
74
|
+
"Season %s start date (%s) is greater than its stop date (%s), "
|
|
75
|
+
"rolling back start date year...",
|
|
76
|
+
self.name,
|
|
77
|
+
start,
|
|
78
|
+
stop,
|
|
79
|
+
)
|
|
80
|
+
try:
|
|
81
|
+
start = start.replace(year=start.year - 1)
|
|
82
|
+
except ValueError:
|
|
83
|
+
self.log.warning(
|
|
84
|
+
"29/02 is used as the season start date, changing it to 28/02..."
|
|
85
|
+
)
|
|
86
|
+
start = start.replace(year=start.year - 1, day=start.day - 1)
|
|
87
|
+
self._months = list(range(self.start_month, 13)) + list(
|
|
88
|
+
range(1, self.stop_month + 1)
|
|
89
|
+
)
|
|
90
|
+
self._carry_on = True
|
|
91
|
+
else:
|
|
92
|
+
self._months = list(range(self.start_month, self.stop_month + 1))
|
|
93
|
+
self._carry_on = False
|
|
94
|
+
self._start = start
|
|
95
|
+
self._stop = stop
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def cfg_from_record(record):
|
|
99
|
+
"""Return configuration dictionary from the DB record."""
|
|
100
|
+
return {
|
|
101
|
+
"Season": {
|
|
102
|
+
"name": record["name"],
|
|
103
|
+
"start_month": record["start"].month,
|
|
104
|
+
"start_day": record["start"].day,
|
|
105
|
+
"stop_month": record["stop"].month,
|
|
106
|
+
"stop_day": record["stop"].day,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def from_record(cls, record):
|
|
112
|
+
"""Create Season object from the DB record or container ``as_dict()`` representation.
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
record : dict
|
|
117
|
+
Dictionary representation of a SeasonContainer.
|
|
118
|
+
"""
|
|
119
|
+
return cls(config=Config(cls.cfg_from_record(record)))
|
|
120
|
+
|
|
121
|
+
def __contains__(self, timestamp):
|
|
122
|
+
"""Check whether a timestamp is within a season."""
|
|
123
|
+
if isinstance(timestamp, datetime):
|
|
124
|
+
timestamp = timestamp.date()
|
|
125
|
+
cast_date = timestamp.replace(year=self._leap_year)
|
|
126
|
+
if self._carry_on:
|
|
127
|
+
if (cast_date.month == 2) and (cast_date.day == 29):
|
|
128
|
+
cast_date_start = cast_date.replace(
|
|
129
|
+
year=self._leap_year - 1, day=cast_date.day - 1
|
|
130
|
+
)
|
|
131
|
+
else:
|
|
132
|
+
cast_date_start = cast_date.replace(year=self._leap_year - 1)
|
|
133
|
+
return self._start <= cast_date_start or cast_date <= self._stop
|
|
134
|
+
return self._start <= cast_date <= self._stop
|
|
135
|
+
|
|
136
|
+
def __repr__(self):
|
|
137
|
+
"""Return a string representation of the season."""
|
|
138
|
+
return (
|
|
139
|
+
f"Season {self.name}: "
|
|
140
|
+
f"from {month_abbr[self.start[0]]}, {self.start[1]} "
|
|
141
|
+
f"to {month_abbr[self.stop[0]]}, {self.stop[1]}."
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def start(self):
|
|
146
|
+
"""
|
|
147
|
+
Start of the season.
|
|
148
|
+
|
|
149
|
+
Returns
|
|
150
|
+
-------
|
|
151
|
+
tuple(int, int)
|
|
152
|
+
Season start (month, day).
|
|
153
|
+
"""
|
|
154
|
+
return (self._start.month, self._start.day)
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def stop(self):
|
|
158
|
+
"""
|
|
159
|
+
End of the season.
|
|
160
|
+
|
|
161
|
+
Returns
|
|
162
|
+
-------
|
|
163
|
+
tuple(int, int)
|
|
164
|
+
Season stop (month, day).
|
|
165
|
+
"""
|
|
166
|
+
return (self._stop.month, self._stop.day)
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def months(self):
|
|
170
|
+
"""
|
|
171
|
+
List of months in the season.
|
|
172
|
+
|
|
173
|
+
Returns
|
|
174
|
+
-------
|
|
175
|
+
list(int)
|
|
176
|
+
List of month numbers in the season.
|
|
177
|
+
"""
|
|
178
|
+
return self._months
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def reference_dates(self):
|
|
182
|
+
"""
|
|
183
|
+
Reference season start and stop dates based on internal leap year.
|
|
184
|
+
|
|
185
|
+
Returns
|
|
186
|
+
-------
|
|
187
|
+
tuple(date, date)
|
|
188
|
+
Tuple of datetime.date objects (start, stop).
|
|
189
|
+
"""
|
|
190
|
+
return (self._start, self._stop)
|
|
191
|
+
|
|
192
|
+
def container(self, observatory_name, observatory_version):
|
|
193
|
+
"""
|
|
194
|
+
Season container.
|
|
195
|
+
|
|
196
|
+
Parameters
|
|
197
|
+
----------
|
|
198
|
+
observatory_name : str
|
|
199
|
+
Name of the observatory, to which the season belongs.
|
|
200
|
+
observatory_version : int
|
|
201
|
+
Version of the observatory, to which the season belongs.
|
|
202
|
+
|
|
203
|
+
Returns
|
|
204
|
+
-------
|
|
205
|
+
SeasonContainer
|
|
206
|
+
"""
|
|
207
|
+
season_container = SeasonContainer(
|
|
208
|
+
start=self.reference_dates[0],
|
|
209
|
+
stop=self.reference_dates[1],
|
|
210
|
+
name=self.name,
|
|
211
|
+
alias=SeasonAlias[self.name.upper()].value,
|
|
212
|
+
name_Observatory=observatory_name,
|
|
213
|
+
version_Observatory=observatory_version,
|
|
214
|
+
)
|
|
215
|
+
season_container.validate()
|
|
216
|
+
return season_container
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class Observatory(Component):
|
|
220
|
+
"""Class, defining an observatory object."""
|
|
221
|
+
|
|
222
|
+
name = CaselessStrEnum(
|
|
223
|
+
values=["CTAO-NORTH", "CTAO-SOUTH"],
|
|
224
|
+
default_value="CTAO-NORTH",
|
|
225
|
+
help="Observatory name",
|
|
226
|
+
).tag(config=True)
|
|
227
|
+
latitude = Float(
|
|
228
|
+
default_value=28.7636, help="Observatory latitude in degrees", allow_none=False
|
|
229
|
+
).tag(config=True)
|
|
230
|
+
longitude = Float(
|
|
231
|
+
default_value=17.8947, help="Observatory longitude in degrees", allow_none=False
|
|
232
|
+
).tag(config=True)
|
|
233
|
+
elevation = Int(
|
|
234
|
+
default_value=2158, help="Observatory elevation in meters", allow_none=False
|
|
235
|
+
).tag(config=True)
|
|
236
|
+
seasons = List(help="Observatory meteorological seasons", minlen=2).tag(config=True)
|
|
237
|
+
version = Int(default_value=1, help="Observatory configuration version").tag(
|
|
238
|
+
config=True
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
def __init__(self, config=None, parent=None, **kwargs):
|
|
242
|
+
super().__init__(config=config, parent=parent, **kwargs)
|
|
243
|
+
self.name = self.name.upper()
|
|
244
|
+
self._longitude = Longitude(
|
|
245
|
+
angle=self.longitude, unit=u.deg, wrap_angle=180 * u.deg
|
|
246
|
+
)
|
|
247
|
+
self._latitude = Latitude(angle=self.latitude, unit=u.deg)
|
|
248
|
+
self._elevation = self.elevation * u.m
|
|
249
|
+
self._seasons = [
|
|
250
|
+
Season(config=Config(key))
|
|
251
|
+
for key in self.get_current_config()["Observatory"]["seasons"]
|
|
252
|
+
]
|
|
253
|
+
self.__check_seasons()
|
|
254
|
+
self.seasons_dict = {season.name: season for season in self._seasons}
|
|
255
|
+
|
|
256
|
+
@classmethod
|
|
257
|
+
def from_db(cls, database_configuration, site, version):
|
|
258
|
+
"""Create Observatory object from the DB record."""
|
|
259
|
+
from ..database.connections import CalibPipeDatabase
|
|
260
|
+
from ..database.interfaces import TableHandler
|
|
261
|
+
|
|
262
|
+
with CalibPipeDatabase(
|
|
263
|
+
**database_configuration,
|
|
264
|
+
) as connection:
|
|
265
|
+
observatory_table = TableHandler.read_table_from_database(
|
|
266
|
+
ObservatoryContainer,
|
|
267
|
+
connection,
|
|
268
|
+
condition=f"(c.name == '{site.upper()}') & (c.version == {version})",
|
|
269
|
+
)
|
|
270
|
+
if len(observatory_table) == 0:
|
|
271
|
+
raise ValueError(
|
|
272
|
+
f"There's no DB record for observatory {site} v{version}"
|
|
273
|
+
)
|
|
274
|
+
if len(observatory_table) > 1:
|
|
275
|
+
raise ValueError(
|
|
276
|
+
f"There're multiple DB records for observatory {site} v{version}"
|
|
277
|
+
)
|
|
278
|
+
seasons_table = TableHandler.read_table_from_database(
|
|
279
|
+
SeasonContainer,
|
|
280
|
+
connection,
|
|
281
|
+
condition=f"(c.name_Observatory == '{site.upper()}') & (c.version_Observatory == {version})",
|
|
282
|
+
)
|
|
283
|
+
observatory_record = observatory_table.to_pandas().to_dict(orient="records")[0]
|
|
284
|
+
seasons_records = seasons_table.to_pandas().to_dict(orient="records")
|
|
285
|
+
seasons_cfg = [Season.cfg_from_record(rec) for rec in seasons_records]
|
|
286
|
+
config_dict = {
|
|
287
|
+
"Observatory": {
|
|
288
|
+
"name": observatory_record["name"],
|
|
289
|
+
"latitude": observatory_record["latitude"],
|
|
290
|
+
"longitude": observatory_record["longitude"],
|
|
291
|
+
"elevation": int(observatory_record["elevation"]),
|
|
292
|
+
"seasons": seasons_cfg,
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return cls(config=Config(config_dict))
|
|
297
|
+
|
|
298
|
+
def __check_seasons(self):
|
|
299
|
+
"""
|
|
300
|
+
Check if provided seasons provide no-gaps and no-overlaps full coverage of a year.
|
|
301
|
+
|
|
302
|
+
Raises
|
|
303
|
+
------
|
|
304
|
+
ValueError
|
|
305
|
+
If the seasons overlap, if there's a gap between the seasons or they don't cover a year.
|
|
306
|
+
"""
|
|
307
|
+
dates = sorted(
|
|
308
|
+
[season.reference_dates for season in self._seasons], key=lambda x: x[0]
|
|
309
|
+
)
|
|
310
|
+
# check that there's one year between the first start and last end
|
|
311
|
+
if dates[0][0] + timedelta(days=365) != dates[-1][1]:
|
|
312
|
+
raise ValueError("The seasons don't cover a year")
|
|
313
|
+
diffs = [j[0] - i[1] for i, j in zip(dates[:-1], dates[1:])]
|
|
314
|
+
if not all(x == timedelta(days=1) for x in diffs):
|
|
315
|
+
raise ValueError("The season coverage has gaps or overlaps!")
|
|
316
|
+
|
|
317
|
+
@property
|
|
318
|
+
def coordinates(self):
|
|
319
|
+
"""
|
|
320
|
+
Observatory coordinates.
|
|
321
|
+
|
|
322
|
+
Returns
|
|
323
|
+
-------
|
|
324
|
+
astropy.coordinates.Latitude
|
|
325
|
+
Observatory's latitude.
|
|
326
|
+
astropy.coordinates.Longitude
|
|
327
|
+
Observatory's longitude.
|
|
328
|
+
"""
|
|
329
|
+
return self._latitude, self._longitude
|
|
330
|
+
|
|
331
|
+
@cached_property
|
|
332
|
+
def containers(self):
|
|
333
|
+
"""
|
|
334
|
+
Observatory containers.
|
|
335
|
+
|
|
336
|
+
Returns
|
|
337
|
+
-------
|
|
338
|
+
tuple(ObservatoryContainer, SeasonContainer)
|
|
339
|
+
Containers with observatory and season configuration data
|
|
340
|
+
used to store the observatory configuration in the DB.
|
|
341
|
+
"""
|
|
342
|
+
obs_container = ObservatoryContainer(
|
|
343
|
+
name=self.name,
|
|
344
|
+
latitude=self._latitude,
|
|
345
|
+
longitude=self._longitude,
|
|
346
|
+
elevation=self._elevation,
|
|
347
|
+
version=self.version,
|
|
348
|
+
)
|
|
349
|
+
obs_container.validate()
|
|
350
|
+
season_containers = [
|
|
351
|
+
season.container(self.name, self.version) for season in self._seasons
|
|
352
|
+
]
|
|
353
|
+
return (obs_container, *season_containers)
|
|
354
|
+
|
|
355
|
+
def select_season_data(self, data, season_name):
|
|
356
|
+
"""
|
|
357
|
+
Select data that belongs to a given season.
|
|
358
|
+
|
|
359
|
+
Parameters
|
|
360
|
+
----------
|
|
361
|
+
data : astropy.table.Table
|
|
362
|
+
Astropy table with meteorological data. Must contain 'Timestamp' column with ``astropy.time.Time``
|
|
363
|
+
season_name : str
|
|
364
|
+
Season name.
|
|
365
|
+
|
|
366
|
+
Returns
|
|
367
|
+
-------
|
|
368
|
+
astropy.table.Table
|
|
369
|
+
Selected data table according to provided season.
|
|
370
|
+
"""
|
|
371
|
+
if season_name.upper() not in self.seasons_dict.keys():
|
|
372
|
+
self.log.error(
|
|
373
|
+
"Requested season (%s) is not defined for the observatory %s\n"
|
|
374
|
+
"%s's seasons:\n%s",
|
|
375
|
+
season_name,
|
|
376
|
+
self.name,
|
|
377
|
+
self.name,
|
|
378
|
+
self.seasons_dict.keys(),
|
|
379
|
+
)
|
|
380
|
+
raise RuntimeError(
|
|
381
|
+
f"{season_name} is not present in {self.name}'s seasons."
|
|
382
|
+
)
|
|
383
|
+
mask = [
|
|
384
|
+
ts.date() in self.seasons_dict[season_name.upper()]
|
|
385
|
+
for ts in data["Timestamp"].tt.datetime
|
|
386
|
+
]
|
|
387
|
+
return data[mask]
|
|
388
|
+
|
|
389
|
+
def get_astronomical_night(self, timestamp):
|
|
390
|
+
"""
|
|
391
|
+
Calculate astronomical night.
|
|
392
|
+
|
|
393
|
+
Calculates the astronomical dusk and dawn (i.e. when the Sun is 18deg below the
|
|
394
|
+
horizon) for this observatory around a given timestamp. Returned values represent
|
|
395
|
+
the UTC timestamps of dusk and dawn.
|
|
396
|
+
|
|
397
|
+
Parameters
|
|
398
|
+
----------
|
|
399
|
+
timestamp: datetime
|
|
400
|
+
The date for which we want to request for data.
|
|
401
|
+
|
|
402
|
+
Returns
|
|
403
|
+
-------
|
|
404
|
+
tuple(datetime, datetime)
|
|
405
|
+
The astronomical dusk and dawn.
|
|
406
|
+
|
|
407
|
+
Raises
|
|
408
|
+
------
|
|
409
|
+
ValueError
|
|
410
|
+
If the provided timestamp corresponds to daytime.
|
|
411
|
+
"""
|
|
412
|
+
observer = astral.Observer(
|
|
413
|
+
latitude=self.coordinates[0].to_value(u.deg),
|
|
414
|
+
longitude=self.coordinates[1].to_value(u.deg),
|
|
415
|
+
elevation=self._elevation.to_value(u.m),
|
|
416
|
+
)
|
|
417
|
+
try:
|
|
418
|
+
sun_today = sun(
|
|
419
|
+
observer, date=timestamp, dawn_dusk_depression=18
|
|
420
|
+
) # corresponds to astronomical dusk/dawn
|
|
421
|
+
except ValueError:
|
|
422
|
+
sun_today = sun(
|
|
423
|
+
observer, date=timestamp - timedelta(days=1), dawn_dusk_depression=18
|
|
424
|
+
)
|
|
425
|
+
return sun_today["dusk"], sun_today["dawn"] + timedelta(days=1)
|
|
426
|
+
|
|
427
|
+
if (timestamp.time() < sun_today["dusk"].time()) and (
|
|
428
|
+
timestamp.time() > sun_today["dawn"].time()
|
|
429
|
+
):
|
|
430
|
+
self.log.error(
|
|
431
|
+
"The provided timestamp %s corresponds to daytime.", timestamp
|
|
432
|
+
)
|
|
433
|
+
raise ValueError(
|
|
434
|
+
f"The provided timestamp {timestamp} corresponds to daytime."
|
|
435
|
+
)
|
|
436
|
+
if sun_today["dusk"].hour < 12 and (
|
|
437
|
+
(
|
|
438
|
+
(timestamp.time() > sun_today["dusk"].time())
|
|
439
|
+
and (timestamp.time() > sun_today["dawn"].time())
|
|
440
|
+
)
|
|
441
|
+
or (
|
|
442
|
+
(timestamp.time() < sun_today["dusk"].time())
|
|
443
|
+
and (timestamp.time() < sun_today["dawn"].time())
|
|
444
|
+
)
|
|
445
|
+
):
|
|
446
|
+
self.log.error(
|
|
447
|
+
"The provided timestamp %s corresponds to daytime.", timestamp
|
|
448
|
+
)
|
|
449
|
+
raise ValueError(
|
|
450
|
+
f"The provided timestamp {timestamp} corresponds to daytime."
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
if timestamp.hour < sun_today["dusk"].hour:
|
|
454
|
+
sun_yesterday = sun(
|
|
455
|
+
observer,
|
|
456
|
+
date=timestamp - timedelta(days=1),
|
|
457
|
+
dawn_dusk_depression=18,
|
|
458
|
+
)
|
|
459
|
+
if (sun_today["dawn"] - sun_yesterday["dusk"]) > timedelta(days=1):
|
|
460
|
+
return sun_yesterday["dusk"] + timedelta(days=1), sun_today["dawn"]
|
|
461
|
+
return sun_yesterday["dusk"], sun_today["dawn"]
|
|
462
|
+
|
|
463
|
+
if timestamp.hour > sun_today["dawn"].hour:
|
|
464
|
+
sun_tomorrow = sun(
|
|
465
|
+
observer,
|
|
466
|
+
date=timestamp + timedelta(days=1),
|
|
467
|
+
dawn_dusk_depression=18,
|
|
468
|
+
)
|
|
469
|
+
return sun_today["dusk"], sun_tomorrow["dawn"]
|
|
470
|
+
return (sun_today["dusk"], sun_today["dawn"])
|
|
471
|
+
|
|
472
|
+
def get_season_from_timestamp(self, timestamp):
|
|
473
|
+
"""Get the name of the season corresponding to the timestamp.
|
|
474
|
+
|
|
475
|
+
Parameters
|
|
476
|
+
----------
|
|
477
|
+
timestamp : datetime
|
|
478
|
+
|
|
479
|
+
Returns
|
|
480
|
+
-------
|
|
481
|
+
str
|
|
482
|
+
Season name.
|
|
483
|
+
"""
|
|
484
|
+
for season in self._seasons:
|
|
485
|
+
if timestamp in season:
|
|
486
|
+
return season.name
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Third-party imports # noqa: D100
|
|
2
|
+
import astropy.units as u
|
|
3
|
+
|
|
4
|
+
# CTA-related imports
|
|
5
|
+
from ctapipe.core import Container, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ObservatoryContainer(Container):
|
|
9
|
+
"""Observatory container."""
|
|
10
|
+
|
|
11
|
+
name = Field(None, "Observatory name")
|
|
12
|
+
latitude = Field(None, "Observatory latitude", unit=u.deg)
|
|
13
|
+
longitude = Field(None, "Observatory longitude", unit=u.deg)
|
|
14
|
+
elevation = Field(None, "Observatory elevation", unit=u.m)
|
|
15
|
+
version = Field(None, "Observatory configuration version")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SeasonContainer(Container):
|
|
19
|
+
"""Season container."""
|
|
20
|
+
|
|
21
|
+
start = Field(None, "Season start timestamp")
|
|
22
|
+
stop = Field(None, "Season stop timestamp")
|
|
23
|
+
name = Field(None, "Season name")
|
|
24
|
+
alias = Field(None, "Season alias")
|
|
25
|
+
name_Observatory = Field(None, "Reference observatory name") # noqa: N815
|
|
26
|
+
version_Observatory = Field(None, "Reference observatory configuration version") # noqa: N815
|
calibpipe/version.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Version information."""
|
|
2
|
+
|
|
3
|
+
# this is adapted from https://github.com/astropy/astropy/blob/main/astropy/version.py
|
|
4
|
+
# see https://github.com/astropy/astropy/pull/10774 for a discussion on why this needed.
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
try:
|
|
8
|
+
from ._dev_version import version
|
|
9
|
+
except Exception:
|
|
10
|
+
from ._version import version
|
|
11
|
+
except Exception:
|
|
12
|
+
import warnings
|
|
13
|
+
|
|
14
|
+
warnings.warn(
|
|
15
|
+
"Could not determine version; this indicates a broken installation."
|
|
16
|
+
" Install from PyPI, using conda or from a local git repository."
|
|
17
|
+
" Installing github's autogenerated source release tarballs "
|
|
18
|
+
" does not include version information and should be avoided.",
|
|
19
|
+
)
|
|
20
|
+
del warnings
|
|
21
|
+
version = "0.0.0"
|
|
22
|
+
|
|
23
|
+
__version__ = version
|
|
24
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ctao-calibpipe
|
|
3
|
+
Version: 0.1.0rc7
|
|
4
|
+
Author: Leonid Burmistrov, Mykhailo Dalchenko, Antonio Di Pilato, Gabriel Emery, Tjark Miener, Gregoire Uhlrich, Georgios Voutsinas, Vadym Voitsekhovskyi
|
|
5
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
License-File: AUTHORS.md
|
|
10
|
+
Requires-Dist: astral
|
|
11
|
+
Requires-Dist: astropy
|
|
12
|
+
Requires-Dist: cdsapi
|
|
13
|
+
Requires-Dist: ctapipe>=0.18
|
|
14
|
+
Requires-Dist: h5py
|
|
15
|
+
Requires-Dist: molecularprofiles>=2.1.0
|
|
16
|
+
Requires-Dist: numpy
|
|
17
|
+
Requires-Dist: pandas
|
|
18
|
+
Requires-Dist: psycopg[binary]
|
|
19
|
+
Requires-Dist: pygrib
|
|
20
|
+
Requires-Dist: pyrdams>=3.0.1
|
|
21
|
+
Requires-Dist: requests>=2.27
|
|
22
|
+
Requires-Dist: requests
|
|
23
|
+
Requires-Dist: sqlalchemy>=2.0.1
|
|
24
|
+
Requires-Dist: traitlets
|
|
25
|
+
Provides-Extra: test
|
|
26
|
+
Requires-Dist: black>=22.5; extra == "test"
|
|
27
|
+
Requires-Dist: coverage-badge; extra == "test"
|
|
28
|
+
Requires-Dist: cwltool; extra == "test"
|
|
29
|
+
Requires-Dist: pylint>=2.15; extra == "test"
|
|
30
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
31
|
+
Requires-Dist: pytest-order; extra == "test"
|
|
32
|
+
Requires-Dist: pytest; extra == "test"
|
|
33
|
+
Requires-Dist: pyyaml; extra == "test"
|
|
34
|
+
Requires-Dist: pytest-requirements; extra == "test"
|
|
35
|
+
Provides-Extra: doc
|
|
36
|
+
Requires-Dist: linkify-it-py; extra == "doc"
|
|
37
|
+
Requires-Dist: myst-parser; extra == "doc"
|
|
38
|
+
Requires-Dist: nbsphinx; extra == "doc"
|
|
39
|
+
Requires-Dist: numpydoc; extra == "doc"
|
|
40
|
+
Requires-Dist: ctao-sphinx-theme; extra == "doc"
|
|
41
|
+
Requires-Dist: sphinx-argparse; extra == "doc"
|
|
42
|
+
Requires-Dist: sphinx-paramlinks; extra == "doc"
|
|
43
|
+
Requires-Dist: sphinx>=8.2.1; extra == "doc"
|
|
44
|
+
Requires-Dist: sphinx-changelog; extra == "doc"
|
|
45
|
+
Provides-Extra: dev
|
|
46
|
+
Requires-Dist: setuptools_scm; extra == "dev"
|
|
47
|
+
Requires-Dist: towncrier; extra == "dev"
|
|
48
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
49
|
+
Provides-Extra: all
|
|
50
|
+
Requires-Dist: calibpipe[dev,doc,test]; extra == "all"
|
|
51
|
+
Dynamic: license-file
|
|
52
|
+
|
|
53
|
+
# DPPS Calibration Pipeline
|
|
54
|
+
|
|
55
|
+
Welcome to `calibpipe` project. The project provides a selection of calibration tools
|
|
56
|
+
for CTA raw data calibration. For full details see [project documentation][calibpipe-doc].
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
### Installation for users
|
|
61
|
+
|
|
62
|
+
Currently the package is under active development. First, create and activate a fresh conda environment:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
mamba create -n calibpipe -c conda-forge python==3.12 ctapipe
|
|
66
|
+
mamba activate calibpipe
|
|
67
|
+
```
|
|
68
|
+
and then install `calibpipe` using `pip` and TestPyPI:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ calibpipe
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Installation for developers
|
|
75
|
+
|
|
76
|
+
## Contributing
|
|
77
|
+
|
|
78
|
+
If you would like to contribute to this project please start from reading the [Contributing Guidelines][contributing].
|
|
79
|
+
Then you can configure the project locally for development as outlined in the [Development Instructions][developing], and start to contribute.
|
|
80
|
+
If you develop a new tool, don't forget to add a corresponding record to the `[project.scripts]` section of `pyproject.toml`
|
|
81
|
+
|
|
82
|
+
Enjoy!
|
|
83
|
+
|
|
84
|
+
[contributing]:http://cta-computing.gitlab-pages.cta-observatory.org/dpps/calibrationpipeline/calibpipe/latest/development/index.html
|
|
85
|
+
[developing]:http://cta-computing.gitlab-pages.cta-observatory.org/dpps/calibrationpipeline/calibpipe/latest/getting_started/index.html#development-setup
|
|
86
|
+
[calibpipe-doc]:http://cta-computing.gitlab-pages.cta-observatory.org/dpps/calibrationpipeline/calibpipe/latest/index.html
|