spectre-core 0.0.1__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.
- spectre_core/__init__.py +3 -0
- spectre_core/cfg.py +116 -0
- spectre_core/chunks/__init__.py +206 -0
- spectre_core/chunks/base.py +160 -0
- spectre_core/chunks/chunk_register.py +15 -0
- spectre_core/chunks/factory.py +26 -0
- spectre_core/chunks/library/__init__.py +8 -0
- spectre_core/chunks/library/callisto/__init__.py +0 -0
- spectre_core/chunks/library/callisto/chunk.py +101 -0
- spectre_core/chunks/library/fixed/__init__.py +0 -0
- spectre_core/chunks/library/fixed/chunk.py +185 -0
- spectre_core/chunks/library/sweep/__init__.py +0 -0
- spectre_core/chunks/library/sweep/chunk.py +400 -0
- spectre_core/dynamic_imports.py +22 -0
- spectre_core/exceptions.py +17 -0
- spectre_core/file_handlers/base.py +94 -0
- spectre_core/file_handlers/configs.py +269 -0
- spectre_core/file_handlers/json.py +36 -0
- spectre_core/file_handlers/text.py +21 -0
- spectre_core/logging.py +222 -0
- spectre_core/plotting/__init__.py +5 -0
- spectre_core/plotting/base.py +194 -0
- spectre_core/plotting/factory.py +26 -0
- spectre_core/plotting/format.py +19 -0
- spectre_core/plotting/library/__init__.py +7 -0
- spectre_core/plotting/library/frequency_cuts/panel.py +74 -0
- spectre_core/plotting/library/integral_over_frequency/panel.py +34 -0
- spectre_core/plotting/library/spectrogram/panel.py +92 -0
- spectre_core/plotting/library/time_cuts/panel.py +77 -0
- spectre_core/plotting/panel_register.py +13 -0
- spectre_core/plotting/panel_stack.py +148 -0
- spectre_core/receivers/__init__.py +6 -0
- spectre_core/receivers/base.py +415 -0
- spectre_core/receivers/factory.py +19 -0
- spectre_core/receivers/library/__init__.py +7 -0
- spectre_core/receivers/library/rsp1a/__init__.py +0 -0
- spectre_core/receivers/library/rsp1a/gr/__init__.py +0 -0
- spectre_core/receivers/library/rsp1a/gr/fixed.py +104 -0
- spectre_core/receivers/library/rsp1a/gr/sweep.py +129 -0
- spectre_core/receivers/library/rsp1a/receiver.py +68 -0
- spectre_core/receivers/library/rspduo/__init__.py +0 -0
- spectre_core/receivers/library/rspduo/gr/__init__.py +0 -0
- spectre_core/receivers/library/rspduo/gr/tuner_1_fixed.py +110 -0
- spectre_core/receivers/library/rspduo/gr/tuner_1_sweep.py +135 -0
- spectre_core/receivers/library/rspduo/receiver.py +68 -0
- spectre_core/receivers/library/test/__init__.py +0 -0
- spectre_core/receivers/library/test/gr/__init__.py +0 -0
- spectre_core/receivers/library/test/gr/cosine_signal_1.py +83 -0
- spectre_core/receivers/library/test/gr/tagged_staircase.py +93 -0
- spectre_core/receivers/library/test/receiver.py +174 -0
- spectre_core/receivers/receiver_register.py +22 -0
- spectre_core/receivers/validators.py +205 -0
- spectre_core/spectrograms/__init__.py +3 -0
- spectre_core/spectrograms/analytical.py +205 -0
- spectre_core/spectrograms/array_operations.py +77 -0
- spectre_core/spectrograms/spectrogram.py +461 -0
- spectre_core/spectrograms/transform.py +267 -0
- spectre_core/watchdog/__init__.py +6 -0
- spectre_core/watchdog/base.py +105 -0
- spectre_core/watchdog/event_handler_register.py +15 -0
- spectre_core/watchdog/factory.py +22 -0
- spectre_core/watchdog/library/__init__.py +10 -0
- spectre_core/watchdog/library/fixed/__init__.py +0 -0
- spectre_core/watchdog/library/fixed/event_handler.py +41 -0
- spectre_core/watchdog/library/sweep/event_handler.py +55 -0
- spectre_core/watchdog/watcher.py +50 -0
- spectre_core/web_fetch/callisto.py +101 -0
- spectre_core-0.0.1.dist-info/LICENSE +674 -0
- spectre_core-0.0.1.dist-info/METADATA +40 -0
- spectre_core-0.0.1.dist-info/RECORD +72 -0
- spectre_core-0.0.1.dist-info/WHEEL +5 -0
- spectre_core-0.0.1.dist-info/top_level.txt +1 -0
spectre_core/__init__.py
ADDED
spectre_core/cfg.py
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
2
|
+
# This file is part of SPECTRE
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
|
+
|
5
|
+
import os
|
6
|
+
|
7
|
+
SPECTRE_DATA_DIR_PATH = os.environ.get("SPECTRE_DATA_DIR_PATH")
|
8
|
+
if SPECTRE_DATA_DIR_PATH is None:
|
9
|
+
raise ValueError("The environment variable SPECTRE_DATA_DIR_PATH has not been set")
|
10
|
+
|
11
|
+
CHUNKS_DIR_PATH = os.environ.get("SPECTRE_CHUNKS_DIR_PATH",
|
12
|
+
os.path.join(SPECTRE_DATA_DIR_PATH, 'chunks'))
|
13
|
+
os.makedirs(CHUNKS_DIR_PATH,
|
14
|
+
exist_ok=True)
|
15
|
+
|
16
|
+
LOGS_DIR_PATH = os.environ.get("SPECTRE_LOGS_DIR_PATH",
|
17
|
+
os.path.join(SPECTRE_DATA_DIR_PATH, 'logs'))
|
18
|
+
os.makedirs(LOGS_DIR_PATH,
|
19
|
+
exist_ok=True)
|
20
|
+
|
21
|
+
JSON_CONFIGS_DIR_PATH = os.environ.get("SPECTRE_JSON_CONFIGS_DIR_PATH",
|
22
|
+
os.path.join(SPECTRE_DATA_DIR_PATH, "configs"))
|
23
|
+
os.makedirs(JSON_CONFIGS_DIR_PATH,
|
24
|
+
exist_ok=True)
|
25
|
+
|
26
|
+
DEFAULT_TIME_FORMAT = "%H:%M:%S"
|
27
|
+
DEFAULT_DATE_FORMAT = "%Y-%m-%d"
|
28
|
+
DEFAULT_DATETIME_FORMAT = f"{DEFAULT_DATE_FORMAT}T{DEFAULT_TIME_FORMAT}"
|
29
|
+
|
30
|
+
CALLISTO_INSTRUMENT_CODES = [
|
31
|
+
"ALASKA-ANCHORAGE",
|
32
|
+
"ALASKA-COHOE",
|
33
|
+
"ALASKA-HAARP",
|
34
|
+
"ALGERIA-CRAAG",
|
35
|
+
"ALMATY",
|
36
|
+
"AUSTRIA-Krumbach",
|
37
|
+
"AUSTRIA-MICHELBACH",
|
38
|
+
"AUSTRIA-OE3FLB",
|
39
|
+
"AUSTRIA-UNIGRAZ",
|
40
|
+
"Australia-ASSA",
|
41
|
+
"BIR",
|
42
|
+
"Croatia-Visnjan",
|
43
|
+
"DENMARK",
|
44
|
+
"EGYPT-Alexandria",
|
45
|
+
"EGYPT-SpaceAgency",
|
46
|
+
"FINLAND-Siuntio",
|
47
|
+
"Finland-Kempele",
|
48
|
+
"GERMANY-DLR",
|
49
|
+
"GLASGOW",
|
50
|
+
"GREENLAND",
|
51
|
+
"HUMAIN",
|
52
|
+
"HURBANOVO",
|
53
|
+
"INDIA-GAURI",
|
54
|
+
"INDIA-OOTY",
|
55
|
+
"INDIA-UDAIPUR",
|
56
|
+
"JAPAN-IBARAKI",
|
57
|
+
"KASI",
|
58
|
+
"MEXART",
|
59
|
+
"MEXICO-FCFM-UANL",
|
60
|
+
"MEXICO-LANCE-A",
|
61
|
+
"MEXICO-LANCE-B",
|
62
|
+
"MONGOLIA-UB",
|
63
|
+
"MRO",
|
64
|
+
"MRT3",
|
65
|
+
"Malaysia-Banting",
|
66
|
+
"NORWAY-EGERSUND",
|
67
|
+
"NORWAY-NY-AALESUND",
|
68
|
+
"NORWAY-RANDABERG",
|
69
|
+
"POLAND-Grotniki",
|
70
|
+
"ROMANIA",
|
71
|
+
"ROSWELL-NM",
|
72
|
+
"SPAIN-PERALEJOS",
|
73
|
+
"SSRT",
|
74
|
+
"SWISS-HB9SCT",
|
75
|
+
"SWISS-HEITERSWIL",
|
76
|
+
"SWISS-IRSOL",
|
77
|
+
"SWISS-Landschlacht",
|
78
|
+
"SWISS-MUHEN",
|
79
|
+
"TRIEST",
|
80
|
+
"TURKEY",
|
81
|
+
"UNAM",
|
82
|
+
"URUGUAY",
|
83
|
+
"USA-BOSTON",
|
84
|
+
]
|
85
|
+
|
86
|
+
def _get_date_based_dir_path(base_dir: str, year: int = None,
|
87
|
+
month: int = None, day: int = None
|
88
|
+
) -> str:
|
89
|
+
if day and not (year and month):
|
90
|
+
raise ValueError("A day requires both a month and a year")
|
91
|
+
if month and not year:
|
92
|
+
raise ValueError("A month requires a year")
|
93
|
+
|
94
|
+
date_dir_components = []
|
95
|
+
if year:
|
96
|
+
date_dir_components.append(f"{year:04}")
|
97
|
+
if month:
|
98
|
+
date_dir_components.append(f"{month:02}")
|
99
|
+
if day:
|
100
|
+
date_dir_components.append(f"{day:02}")
|
101
|
+
|
102
|
+
return os.path.join(base_dir, *date_dir_components)
|
103
|
+
|
104
|
+
|
105
|
+
def get_chunks_dir_path(year: int = None,
|
106
|
+
month: int = None,
|
107
|
+
day: int = None
|
108
|
+
) -> str:
|
109
|
+
return _get_date_based_dir_path(CHUNKS_DIR_PATH, year, month, day)
|
110
|
+
|
111
|
+
|
112
|
+
def get_logs_dir_path(year: int = None,
|
113
|
+
month: int = None,
|
114
|
+
day: int = None
|
115
|
+
) -> str:
|
116
|
+
return _get_date_based_dir_path(LOGS_DIR_PATH, year, month, day)
|
@@ -0,0 +1,206 @@
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
2
|
+
# This file is part of SPECTRE
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
|
+
|
5
|
+
from logging import getLogger
|
6
|
+
_LOGGER = getLogger(__name__)
|
7
|
+
|
8
|
+
import os
|
9
|
+
from typing import Optional
|
10
|
+
from collections import OrderedDict
|
11
|
+
import warnings
|
12
|
+
from datetime import datetime
|
13
|
+
|
14
|
+
# dynamically import all chunks
|
15
|
+
import spectre_core.chunks.library
|
16
|
+
from spectre_core.chunks.factory import get_chunk_from_tag
|
17
|
+
from spectre_core.chunks.base import BaseChunk
|
18
|
+
from spectre_core.spectrograms.spectrogram import Spectrogram
|
19
|
+
from spectre_core.spectrograms import transform
|
20
|
+
from spectre_core.cfg import (
|
21
|
+
DEFAULT_DATETIME_FORMAT,
|
22
|
+
get_chunks_dir_path
|
23
|
+
)
|
24
|
+
from spectre_core.exceptions import (
|
25
|
+
SpectrogramNotFoundError,
|
26
|
+
ChunkNotFoundError
|
27
|
+
)
|
28
|
+
|
29
|
+
class Chunks:
|
30
|
+
def __init__(self,
|
31
|
+
tag: str,
|
32
|
+
year: Optional[int] = None,
|
33
|
+
month: Optional[int] = None,
|
34
|
+
day: Optional[int] = None):
|
35
|
+
self._tag = tag
|
36
|
+
self._Chunk = get_chunk_from_tag(tag)
|
37
|
+
self._chunk_map: dict[str, BaseChunk] = OrderedDict()
|
38
|
+
self._chunk_list: list[BaseChunk] = []
|
39
|
+
self._chunk_names: list[str] = []
|
40
|
+
self.set_date(year, month, day)
|
41
|
+
|
42
|
+
|
43
|
+
@property
|
44
|
+
def year(self) -> int:
|
45
|
+
return self._year
|
46
|
+
|
47
|
+
|
48
|
+
@property
|
49
|
+
def month(self) -> int:
|
50
|
+
return self._month
|
51
|
+
|
52
|
+
|
53
|
+
@property
|
54
|
+
def day(self) -> int:
|
55
|
+
return self._day
|
56
|
+
|
57
|
+
|
58
|
+
@property
|
59
|
+
def chunks_dir_path(self) -> str:
|
60
|
+
return get_chunks_dir_path(self.year, self.month, self.day)
|
61
|
+
|
62
|
+
|
63
|
+
@property
|
64
|
+
def chunk_map(self) -> dict[str, BaseChunk]:
|
65
|
+
return self._chunk_map
|
66
|
+
|
67
|
+
|
68
|
+
@property
|
69
|
+
def chunk_list(self) -> list[BaseChunk]:
|
70
|
+
return self._chunk_list
|
71
|
+
|
72
|
+
|
73
|
+
@property
|
74
|
+
def chunk_names(self) -> list[str]:
|
75
|
+
return self._chunk_names
|
76
|
+
|
77
|
+
|
78
|
+
@property
|
79
|
+
def num_chunks(self) -> int:
|
80
|
+
return len(self.chunk_list)
|
81
|
+
|
82
|
+
|
83
|
+
def set_date(self,
|
84
|
+
year: Optional[int],
|
85
|
+
month: Optional[int],
|
86
|
+
day: Optional[int]) -> None:
|
87
|
+
self._year = year
|
88
|
+
self._month = month
|
89
|
+
self._day = day
|
90
|
+
self._update_chunk_map()
|
91
|
+
|
92
|
+
|
93
|
+
def _update_chunk_map(self) -> None:
|
94
|
+
self._chunk_map = OrderedDict() # reset cache
|
95
|
+
self._chunk_list = [] # reset cache
|
96
|
+
self._chunk_names = [] # reset cache
|
97
|
+
|
98
|
+
chunk_files = [f for (_, _, files) in os.walk(self.chunks_dir_path) for f in files]
|
99
|
+
|
100
|
+
if len(chunk_files) == 0:
|
101
|
+
warning_message = "No chunks found, setting chunk map with empty dictionary."
|
102
|
+
_LOGGER.warning(warning_message)
|
103
|
+
warnings.warn(warning_message)
|
104
|
+
return
|
105
|
+
|
106
|
+
for chunk_file in chunk_files:
|
107
|
+
file_name, _ = os.path.splitext(chunk_file)
|
108
|
+
chunk_start_time, tag = file_name.split("_", 1)
|
109
|
+
if tag == self._tag:
|
110
|
+
self._chunk_map[chunk_start_time] = self._Chunk(chunk_start_time, tag)
|
111
|
+
|
112
|
+
self._chunk_map = OrderedDict(sorted(self._chunk_map.items()))
|
113
|
+
self._chunk_names = list(self._chunk_map.keys())
|
114
|
+
self._chunk_list = list(self._chunk_map.values())
|
115
|
+
|
116
|
+
|
117
|
+
def update(self) -> None:
|
118
|
+
"""Public alias for setting chunk map"""
|
119
|
+
self._update_chunk_map()
|
120
|
+
|
121
|
+
|
122
|
+
def __iter__(self):
|
123
|
+
yield from self.chunk_list
|
124
|
+
|
125
|
+
|
126
|
+
def _get_chunk_by_chunk_start_time(self,
|
127
|
+
chunk_start_time: str) -> BaseChunk:
|
128
|
+
try:
|
129
|
+
return self.chunk_map[chunk_start_time]
|
130
|
+
except KeyError:
|
131
|
+
raise ChunkNotFoundError(f"Chunk with chunk start time {chunk_start_time} could not be found within {self.chunks_dir_path}")
|
132
|
+
|
133
|
+
|
134
|
+
def _get_chunk_by_index(self,
|
135
|
+
chunk_index: int) -> BaseChunk:
|
136
|
+
num_chunks = len(self.chunk_map)
|
137
|
+
if num_chunks == 0:
|
138
|
+
raise ChunkNotFoundError("No chunks are available")
|
139
|
+
index = chunk_index % num_chunks # Use modulo to make the index wrap around. Allows the user to iterate over all the chunks via index cyclically.
|
140
|
+
return self.chunk_list[index]
|
141
|
+
|
142
|
+
|
143
|
+
def __getitem__(self, subscript: str | int):
|
144
|
+
if isinstance(subscript, str):
|
145
|
+
return self._get_chunk_by_chunk_start_time(subscript)
|
146
|
+
elif isinstance(subscript, int):
|
147
|
+
return self._get_chunk_by_index(subscript)
|
148
|
+
|
149
|
+
|
150
|
+
def get_index_by_chunk(self,
|
151
|
+
chunk_to_match: BaseChunk) -> int:
|
152
|
+
for i, chunk in enumerate(self):
|
153
|
+
if chunk.chunk_start_time == chunk_to_match.chunk_start_time:
|
154
|
+
return i
|
155
|
+
raise ChunkNotFoundError(f"No matching chunk found for chunk {chunk_to_match.chunk_name}")
|
156
|
+
|
157
|
+
|
158
|
+
def count_chunk_files(self,
|
159
|
+
extension: str) -> int:
|
160
|
+
return sum(1 for chunk_file in self if chunk_file.has_file(extension))
|
161
|
+
|
162
|
+
|
163
|
+
def get_spectrogram_from_range(self,
|
164
|
+
start_time: str,
|
165
|
+
end_time: str) -> Spectrogram:
|
166
|
+
# Convert input strings to datetime objects
|
167
|
+
start_datetime = datetime.strptime(start_time, DEFAULT_DATETIME_FORMAT)
|
168
|
+
end_datetime = datetime.strptime(end_time, DEFAULT_DATETIME_FORMAT)
|
169
|
+
|
170
|
+
if start_datetime.day != end_datetime.day:
|
171
|
+
warning_message = "Joining spectrograms across multiple days"
|
172
|
+
_LOGGER.warning(warning_message)
|
173
|
+
warnings.warn(warning_message, RuntimeWarning)
|
174
|
+
|
175
|
+
spectrograms = []
|
176
|
+
num_fits_chunks = self.count_chunk_files("fits")
|
177
|
+
|
178
|
+
for i, chunk in enumerate(self):
|
179
|
+
# skip chunks without fits files
|
180
|
+
if not chunk.has_file("fits"):
|
181
|
+
continue
|
182
|
+
|
183
|
+
# rather than reading all files to evaluate the actual upper bound to their time range (slow)
|
184
|
+
# place an upper bound by using the chunk start datetime for the next chunk
|
185
|
+
# this assumes that the chunks are non-overlapping (reasonable assumption)
|
186
|
+
lower_bound = chunk.chunk_start_datetime
|
187
|
+
if i < num_fits_chunks:
|
188
|
+
next_chunk = self[i + 1]
|
189
|
+
upper_bound = next_chunk.chunk_start_datetime
|
190
|
+
# if there is no "next chunk" then we do have to read the file
|
191
|
+
else:
|
192
|
+
fits_chunk = chunk.get_file("fits")
|
193
|
+
upper_bound = fits_chunk.datetimes[-1]
|
194
|
+
|
195
|
+
# if the chunk overlaps with the input time range, then read the fits file
|
196
|
+
if start_datetime <= upper_bound and lower_bound <= end_datetime:
|
197
|
+
spectrogram = chunk.read_file("fits")
|
198
|
+
spectrogram = transform.time_chop(spectrogram, start_time, end_time)
|
199
|
+
# if we have a non-empty spectrogram, append it to the list of spectrograms
|
200
|
+
if spectrogram:
|
201
|
+
spectrograms.append(spectrogram)
|
202
|
+
|
203
|
+
if spectrograms:
|
204
|
+
return transform.join_spectrograms(spectrograms)
|
205
|
+
else:
|
206
|
+
raise SpectrogramNotFoundError("No spectrogram data found for the given time range")
|
@@ -0,0 +1,160 @@
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
2
|
+
# This file is part of SPECTRE
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
|
+
|
5
|
+
from datetime import datetime
|
6
|
+
from abc import abstractmethod
|
7
|
+
from typing import Optional
|
8
|
+
|
9
|
+
from scipy.signal import ShortTimeFFT, get_window
|
10
|
+
|
11
|
+
from spectre_core.file_handlers.base import BaseFileHandler
|
12
|
+
from spectre_core.cfg import get_chunks_dir_path
|
13
|
+
from spectre_core.file_handlers.configs import CaptureConfig
|
14
|
+
from spectre_core.spectrograms.spectrogram import Spectrogram
|
15
|
+
from spectre_core.cfg import DEFAULT_DATETIME_FORMAT
|
16
|
+
from spectre_core.exceptions import ChunkFileNotFoundError
|
17
|
+
|
18
|
+
class ChunkFile(BaseFileHandler):
|
19
|
+
def __init__(self,
|
20
|
+
chunk_parent_path: str,
|
21
|
+
chunk_name: str,
|
22
|
+
extension: str,
|
23
|
+
**kwargs):
|
24
|
+
self._chunk_start_time, self._tag = chunk_name.split("_")
|
25
|
+
self._chunk_start_datetime: Optional[datetime] = None
|
26
|
+
super().__init__(chunk_parent_path,
|
27
|
+
chunk_name,
|
28
|
+
extension = extension,
|
29
|
+
**kwargs)
|
30
|
+
|
31
|
+
|
32
|
+
@property
|
33
|
+
def chunk_start_time(self) -> str:
|
34
|
+
return self._chunk_start_time
|
35
|
+
|
36
|
+
|
37
|
+
@property
|
38
|
+
def chunk_start_datetime(self) -> datetime:
|
39
|
+
if self._chunk_start_datetime is None:
|
40
|
+
self._chunk_start_datetime = datetime.strptime(self.chunk_start_time, DEFAULT_DATETIME_FORMAT)
|
41
|
+
return self._chunk_start_datetime
|
42
|
+
|
43
|
+
|
44
|
+
@property
|
45
|
+
def tag(self) -> str:
|
46
|
+
return self._tag
|
47
|
+
|
48
|
+
|
49
|
+
|
50
|
+
class BaseChunk:
|
51
|
+
def __init__(self,
|
52
|
+
chunk_start_time: str,
|
53
|
+
tag: str):
|
54
|
+
self._chunk_start_time: str = chunk_start_time
|
55
|
+
self._tag: str = tag
|
56
|
+
self._chunk_files: dict[str, ChunkFile] = {}
|
57
|
+
self._chunk_start_datetime: Optional[datetime] = None
|
58
|
+
self.chunk_parent_path: str = get_chunks_dir_path(year = self.chunk_start_datetime.year,
|
59
|
+
month = self.chunk_start_datetime.month,
|
60
|
+
day = self.chunk_start_datetime.day)
|
61
|
+
self._chunk_name: str = f"{self.chunk_start_time}_{self.tag}"
|
62
|
+
|
63
|
+
|
64
|
+
@property
|
65
|
+
def chunk_start_time(self) -> str:
|
66
|
+
return self._chunk_start_time
|
67
|
+
|
68
|
+
|
69
|
+
@property
|
70
|
+
def chunk_start_datetime(self) -> datetime:
|
71
|
+
if self._chunk_start_datetime is None:
|
72
|
+
self._chunk_start_datetime = datetime.strptime(self.chunk_start_time, DEFAULT_DATETIME_FORMAT)
|
73
|
+
return self._chunk_start_datetime
|
74
|
+
|
75
|
+
|
76
|
+
@property
|
77
|
+
def tag(self) -> str:
|
78
|
+
return self._tag
|
79
|
+
|
80
|
+
|
81
|
+
@property
|
82
|
+
def chunk_name(self) -> str:
|
83
|
+
return f"{self._chunk_start_time}_{self._tag}"
|
84
|
+
|
85
|
+
|
86
|
+
@property
|
87
|
+
def extensions(self) -> list[str]:
|
88
|
+
return list(self._chunk_files.keys())
|
89
|
+
|
90
|
+
|
91
|
+
def add_file(self, chunk_file: ChunkFile) -> None:
|
92
|
+
self._chunk_files[chunk_file.extension] = chunk_file
|
93
|
+
|
94
|
+
|
95
|
+
def get_file(self, extension: str) -> ChunkFile:
|
96
|
+
try:
|
97
|
+
return self._chunk_files[extension]
|
98
|
+
except KeyError:
|
99
|
+
raise ChunkFileNotFoundError(f"No chunk file found with extension '{extension}'")
|
100
|
+
|
101
|
+
|
102
|
+
def read_file(self, extension: str):
|
103
|
+
chunk_file = self.get_file(extension)
|
104
|
+
return chunk_file.read()
|
105
|
+
|
106
|
+
|
107
|
+
def delete_file(self, extension: str, **kwargs):
|
108
|
+
chunk_file = self.get_file(extension)
|
109
|
+
try:
|
110
|
+
chunk_file.delete(**kwargs)
|
111
|
+
except FileNotFoundError as e:
|
112
|
+
raise ChunkFileNotFoundError(str(e))
|
113
|
+
|
114
|
+
|
115
|
+
def has_file(self, extension: str) -> bool:
|
116
|
+
try:
|
117
|
+
chunk_file = self.get_file(extension)
|
118
|
+
return chunk_file.exists
|
119
|
+
except ChunkFileNotFoundError:
|
120
|
+
return False
|
121
|
+
|
122
|
+
|
123
|
+
class SPECTREChunk(BaseChunk):
|
124
|
+
def __init__(self, *args, **kwargs):
|
125
|
+
super().__init__(*args, **kwargs)
|
126
|
+
|
127
|
+
self._capture_config = CaptureConfig(self._tag)
|
128
|
+
self._SFT = None # cache
|
129
|
+
|
130
|
+
|
131
|
+
@abstractmethod
|
132
|
+
def build_spectrogram(self) -> Spectrogram:
|
133
|
+
"""Create a spectrogram object derived from chunk files for this chunk."""
|
134
|
+
pass
|
135
|
+
|
136
|
+
|
137
|
+
@property
|
138
|
+
def capture_config(self) -> CaptureConfig:
|
139
|
+
return self._capture_config
|
140
|
+
|
141
|
+
|
142
|
+
@property
|
143
|
+
def SFT(self) -> ShortTimeFFT:
|
144
|
+
if self._SFT is None:
|
145
|
+
self._SFT = self.__get_SFT_instance()
|
146
|
+
return self._SFT
|
147
|
+
|
148
|
+
|
149
|
+
def __get_SFT_instance(self) -> ShortTimeFFT:
|
150
|
+
window_type = self.capture_config.get("window_type")
|
151
|
+
window_params = self.capture_config.get("window_kwargs").values()
|
152
|
+
window_size = self.capture_config.get("window_size")
|
153
|
+
window = get_window((window_type,
|
154
|
+
*window_params),
|
155
|
+
window_size)
|
156
|
+
samp_rate = self.capture_config.get("samp_rate")
|
157
|
+
return ShortTimeFFT(window,
|
158
|
+
fs=samp_rate,
|
159
|
+
fft_mode="centered",
|
160
|
+
**self.capture_config.get("STFFT_kwargs"))
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
2
|
+
# This file is part of SPECTRE
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
|
+
|
5
|
+
# Global dictionaries to hold the mappings
|
6
|
+
chunk_map = {}
|
7
|
+
|
8
|
+
# classes decorated with @register_chunk([CHUNK_KEY])
|
9
|
+
# will be added to chunk_map
|
10
|
+
def register_chunk(chunk_key: str):
|
11
|
+
def decorator(cls):
|
12
|
+
chunk_map[chunk_key] = cls
|
13
|
+
return cls
|
14
|
+
return decorator
|
15
|
+
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
2
|
+
# This file is part of SPECTRE
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
|
+
|
5
|
+
# after we decorate all chunks, we can import the chunk_key -> chunk maps
|
6
|
+
from spectre_core.chunks.base import BaseChunk
|
7
|
+
from spectre_core.file_handlers.configs import CaptureConfig
|
8
|
+
from spectre_core.exceptions import ChunkNotFoundError
|
9
|
+
|
10
|
+
|
11
|
+
def get_chunk(chunk_key: str) -> BaseChunk:
|
12
|
+
Chunk = chunk_map.get(chunk_key)
|
13
|
+
if Chunk is None:
|
14
|
+
valid_chunk_keys = list(chunk_map.keys())
|
15
|
+
raise ChunkNotFoundError(f"No chunk found for the chunk key: {chunk_key}. Valid chunk keys are: {valid_chunk_keys}")
|
16
|
+
return Chunk
|
17
|
+
|
18
|
+
def get_chunk_from_tag(tag: str) -> BaseChunk:
|
19
|
+
# if we are dealing with a callisto chunk, the chunk key is equal to the tag
|
20
|
+
if "callisto" in tag:
|
21
|
+
chunk_key = "callisto"
|
22
|
+
# otherwise, we fetch the chunk key from the capture config
|
23
|
+
else:
|
24
|
+
capture_config= CaptureConfig(tag)
|
25
|
+
chunk_key = capture_config.get('chunk_key')
|
26
|
+
return get_chunk(chunk_key)
|
@@ -0,0 +1,8 @@
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
2
|
+
# This file is part of SPECTRE
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
|
+
|
5
|
+
from spectre_core.dynamic_imports import import_target_modules
|
6
|
+
|
7
|
+
import_target_modules(__file__, __name__, "chunk")
|
8
|
+
|
File without changes
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
2
|
+
# This file is part of SPECTRE
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
|
+
|
5
|
+
from datetime import datetime, timedelta
|
6
|
+
from typing import Tuple
|
7
|
+
|
8
|
+
import numpy as np
|
9
|
+
from astropy.io import fits
|
10
|
+
from astropy.io.fits.hdu.image import PrimaryHDU
|
11
|
+
from astropy.io.fits.hdu.table import BinTableHDU
|
12
|
+
from astropy.io.fits.hdu.hdulist import HDUList
|
13
|
+
|
14
|
+
from spectre_core.chunks.chunk_register import register_chunk
|
15
|
+
from spectre_core.spectrograms.spectrogram import Spectrogram
|
16
|
+
from spectre_core.chunks.base import (
|
17
|
+
BaseChunk,
|
18
|
+
ChunkFile
|
19
|
+
)
|
20
|
+
|
21
|
+
|
22
|
+
@register_chunk('callisto')
|
23
|
+
class Chunk(BaseChunk):
|
24
|
+
def __init__(self, chunk_start_time: str, tag: str):
|
25
|
+
super().__init__(chunk_start_time, tag)
|
26
|
+
self.add_file(FitsChunk(self.chunk_parent_path, self.chunk_name))
|
27
|
+
|
28
|
+
|
29
|
+
class FitsChunk(ChunkFile):
|
30
|
+
def __init__(self, chunk_parent_path: str, chunk_name: str):
|
31
|
+
super().__init__(chunk_parent_path, chunk_name, "fits")
|
32
|
+
|
33
|
+
|
34
|
+
def read(self) -> Spectrogram:
|
35
|
+
with fits.open(self.file_path, mode='readonly') as hdulist:
|
36
|
+
primary_hdu = self._get_primary_hdu(hdulist)
|
37
|
+
dynamic_spectra = self._get_dynamic_spectra(primary_hdu)
|
38
|
+
microsecond_correction = self._get_microsecond_correction(primary_hdu)
|
39
|
+
bintable_hdu = self._get_bintable_hdu(hdulist)
|
40
|
+
times, frequencies = self._get_time_and_frequency(bintable_hdu)
|
41
|
+
spectrum_type = self._get_spectrum_type(primary_hdu)
|
42
|
+
|
43
|
+
if spectrum_type == "digits":
|
44
|
+
dynamic_spectra_linearised = self._convert_units_to_linearised(dynamic_spectra)
|
45
|
+
return Spectrogram(dynamic_spectra_linearised[::-1, :], # reverse the spectra along the frequency axis
|
46
|
+
times,
|
47
|
+
frequencies[::-1], # sort the frequencies in ascending order
|
48
|
+
self.tag,
|
49
|
+
chunk_start_time=self.chunk_start_time,
|
50
|
+
microsecond_correction = microsecond_correction,
|
51
|
+
spectrum_type = spectrum_type)
|
52
|
+
else:
|
53
|
+
raise NotImplementedError(f"SPECTRE does not currently support spectrum type with BUNITS {spectrum_type}")
|
54
|
+
|
55
|
+
|
56
|
+
@property
|
57
|
+
def datetimes(self) -> np.ndarray:
|
58
|
+
with fits.open(self.file_path, mode='readonly') as hdulist:
|
59
|
+
bintable_data = hdulist[1].data
|
60
|
+
times = bintable_data['TIME'][0]
|
61
|
+
return [self.chunk_start_datetime + timedelta(seconds=t) for t in times]
|
62
|
+
|
63
|
+
|
64
|
+
def _get_primary_hdu(self, hdulist: HDUList) -> PrimaryHDU:
|
65
|
+
return hdulist['PRIMARY']
|
66
|
+
|
67
|
+
|
68
|
+
def _get_dynamic_spectra(self, primary_hdu: PrimaryHDU) -> np.ndarray:
|
69
|
+
return primary_hdu.data
|
70
|
+
|
71
|
+
|
72
|
+
def _get_microsecond_correction(self, primary_hdu: PrimaryHDU) -> int:
|
73
|
+
date_obs = primary_hdu.header.get('DATE-OBS', None)
|
74
|
+
time_obs = primary_hdu.header.get('TIME-OBS', None)
|
75
|
+
datetime_obs = datetime.strptime(f"{date_obs}T{time_obs}", "%Y/%m/%dT%H:%M:%S.%f")
|
76
|
+
return datetime_obs.microsecond
|
77
|
+
|
78
|
+
|
79
|
+
def _get_bintable_hdu(self, hdulist: HDUList) -> BinTableHDU:
|
80
|
+
return hdulist[1]
|
81
|
+
|
82
|
+
|
83
|
+
def _get_time_and_frequency(self, bintable_hdu: BinTableHDU) -> Tuple[np.ndarray, np.ndarray]:
|
84
|
+
data = bintable_hdu.data
|
85
|
+
times = data['TIME'][0]
|
86
|
+
frequencies_MHz = data['FREQUENCY'][0]
|
87
|
+
frequencies = frequencies_MHz * 1e6 # convert to Hz
|
88
|
+
return times, frequencies
|
89
|
+
|
90
|
+
|
91
|
+
def _get_spectrum_type(self, primary_hdu: PrimaryHDU) -> str:
|
92
|
+
return primary_hdu.header.get('BUNIT', None)
|
93
|
+
|
94
|
+
|
95
|
+
def _convert_units_to_linearised(self, dynamic_spectra: np.ndarray) -> np.ndarray:
|
96
|
+
digits_floats = np.array(dynamic_spectra, dtype='float')
|
97
|
+
# conversion as per ADC specs [see email from C. Monstein]
|
98
|
+
dB = (digits_floats / 255) * (2500 / 25)
|
99
|
+
return 10 ** (dB / 10)
|
100
|
+
|
101
|
+
|
File without changes
|