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.
Files changed (72) hide show
  1. spectre_core/__init__.py +3 -0
  2. spectre_core/cfg.py +116 -0
  3. spectre_core/chunks/__init__.py +206 -0
  4. spectre_core/chunks/base.py +160 -0
  5. spectre_core/chunks/chunk_register.py +15 -0
  6. spectre_core/chunks/factory.py +26 -0
  7. spectre_core/chunks/library/__init__.py +8 -0
  8. spectre_core/chunks/library/callisto/__init__.py +0 -0
  9. spectre_core/chunks/library/callisto/chunk.py +101 -0
  10. spectre_core/chunks/library/fixed/__init__.py +0 -0
  11. spectre_core/chunks/library/fixed/chunk.py +185 -0
  12. spectre_core/chunks/library/sweep/__init__.py +0 -0
  13. spectre_core/chunks/library/sweep/chunk.py +400 -0
  14. spectre_core/dynamic_imports.py +22 -0
  15. spectre_core/exceptions.py +17 -0
  16. spectre_core/file_handlers/base.py +94 -0
  17. spectre_core/file_handlers/configs.py +269 -0
  18. spectre_core/file_handlers/json.py +36 -0
  19. spectre_core/file_handlers/text.py +21 -0
  20. spectre_core/logging.py +222 -0
  21. spectre_core/plotting/__init__.py +5 -0
  22. spectre_core/plotting/base.py +194 -0
  23. spectre_core/plotting/factory.py +26 -0
  24. spectre_core/plotting/format.py +19 -0
  25. spectre_core/plotting/library/__init__.py +7 -0
  26. spectre_core/plotting/library/frequency_cuts/panel.py +74 -0
  27. spectre_core/plotting/library/integral_over_frequency/panel.py +34 -0
  28. spectre_core/plotting/library/spectrogram/panel.py +92 -0
  29. spectre_core/plotting/library/time_cuts/panel.py +77 -0
  30. spectre_core/plotting/panel_register.py +13 -0
  31. spectre_core/plotting/panel_stack.py +148 -0
  32. spectre_core/receivers/__init__.py +6 -0
  33. spectre_core/receivers/base.py +415 -0
  34. spectre_core/receivers/factory.py +19 -0
  35. spectre_core/receivers/library/__init__.py +7 -0
  36. spectre_core/receivers/library/rsp1a/__init__.py +0 -0
  37. spectre_core/receivers/library/rsp1a/gr/__init__.py +0 -0
  38. spectre_core/receivers/library/rsp1a/gr/fixed.py +104 -0
  39. spectre_core/receivers/library/rsp1a/gr/sweep.py +129 -0
  40. spectre_core/receivers/library/rsp1a/receiver.py +68 -0
  41. spectre_core/receivers/library/rspduo/__init__.py +0 -0
  42. spectre_core/receivers/library/rspduo/gr/__init__.py +0 -0
  43. spectre_core/receivers/library/rspduo/gr/tuner_1_fixed.py +110 -0
  44. spectre_core/receivers/library/rspduo/gr/tuner_1_sweep.py +135 -0
  45. spectre_core/receivers/library/rspduo/receiver.py +68 -0
  46. spectre_core/receivers/library/test/__init__.py +0 -0
  47. spectre_core/receivers/library/test/gr/__init__.py +0 -0
  48. spectre_core/receivers/library/test/gr/cosine_signal_1.py +83 -0
  49. spectre_core/receivers/library/test/gr/tagged_staircase.py +93 -0
  50. spectre_core/receivers/library/test/receiver.py +174 -0
  51. spectre_core/receivers/receiver_register.py +22 -0
  52. spectre_core/receivers/validators.py +205 -0
  53. spectre_core/spectrograms/__init__.py +3 -0
  54. spectre_core/spectrograms/analytical.py +205 -0
  55. spectre_core/spectrograms/array_operations.py +77 -0
  56. spectre_core/spectrograms/spectrogram.py +461 -0
  57. spectre_core/spectrograms/transform.py +267 -0
  58. spectre_core/watchdog/__init__.py +6 -0
  59. spectre_core/watchdog/base.py +105 -0
  60. spectre_core/watchdog/event_handler_register.py +15 -0
  61. spectre_core/watchdog/factory.py +22 -0
  62. spectre_core/watchdog/library/__init__.py +10 -0
  63. spectre_core/watchdog/library/fixed/__init__.py +0 -0
  64. spectre_core/watchdog/library/fixed/event_handler.py +41 -0
  65. spectre_core/watchdog/library/sweep/event_handler.py +55 -0
  66. spectre_core/watchdog/watcher.py +50 -0
  67. spectre_core/web_fetch/callisto.py +101 -0
  68. spectre_core-0.0.1.dist-info/LICENSE +674 -0
  69. spectre_core-0.0.1.dist-info/METADATA +40 -0
  70. spectre_core-0.0.1.dist-info/RECORD +72 -0
  71. spectre_core-0.0.1.dist-info/WHEEL +5 -0
  72. spectre_core-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,3 @@
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
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