spectre-core 0.0.9__py3-none-any.whl → 0.0.10__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 (106) hide show
  1. spectre_core/__init__.py +0 -3
  2. spectre_core/_file_io/__init__.py +15 -0
  3. spectre_core/_file_io/file_handlers.py +128 -0
  4. spectre_core/capture_configs/__init__.py +29 -0
  5. spectre_core/capture_configs/_capture_config.py +85 -0
  6. spectre_core/capture_configs/_capture_templates.py +222 -0
  7. spectre_core/capture_configs/_parameters.py +110 -0
  8. spectre_core/capture_configs/_pconstraints.py +82 -0
  9. spectre_core/capture_configs/_ptemplates.py +450 -0
  10. spectre_core/capture_configs/_pvalidators.py +173 -0
  11. spectre_core/chunks/__init__.py +17 -201
  12. spectre_core/chunks/{base.py → _base.py} +15 -60
  13. spectre_core/chunks/_chunks.py +200 -0
  14. spectre_core/chunks/{factory.py → _factory.py} +6 -7
  15. spectre_core/chunks/library/{callisto/chunk.py → _callisto.py} +4 -7
  16. spectre_core/chunks/library/{fixed/chunk.py → _fixed_center_frequency.py} +7 -64
  17. spectre_core/chunks/library/_swept_center_frequency.py +103 -0
  18. spectre_core/config/__init__.py +20 -0
  19. spectre_core/config/_paths.py +77 -0
  20. spectre_core/config/_time_formats.py +15 -0
  21. spectre_core/exceptions.py +4 -5
  22. spectre_core/logging/__init__.py +11 -0
  23. spectre_core/logging/_configure.py +35 -0
  24. spectre_core/logging/_decorators.py +19 -0
  25. spectre_core/{logging.py → logging/_log_handlers.py} +13 -58
  26. spectre_core/plotting/__init__.py +7 -1
  27. spectre_core/plotting/{base.py → _base.py} +40 -20
  28. spectre_core/plotting/_format.py +18 -0
  29. spectre_core/plotting/{panel_stack.py → _panel_stack.py} +48 -48
  30. spectre_core/plotting/_panels.py +234 -0
  31. spectre_core/post_processing/__init__.py +10 -2
  32. spectre_core/post_processing/_base.py +119 -0
  33. spectre_core/post_processing/{factory.py → _factory.py} +7 -6
  34. spectre_core/post_processing/{post_processor.py → _post_processor.py} +3 -3
  35. spectre_core/post_processing/library/_fixed_center_frequency.py +115 -0
  36. spectre_core/post_processing/library/_swept_center_frequency.py +382 -0
  37. spectre_core/receivers/__init__.py +12 -2
  38. spectre_core/receivers/_base.py +352 -0
  39. spectre_core/receivers/{factory.py → _factory.py} +2 -2
  40. spectre_core/receivers/_spec_names.py +20 -0
  41. spectre_core/receivers/gr/__init__.py +3 -0
  42. spectre_core/receivers/gr/_base.py +33 -0
  43. spectre_core/receivers/gr/_rsp1a.py +158 -0
  44. spectre_core/receivers/gr/_test.py +123 -0
  45. spectre_core/receivers/library/_rsp1a.py +61 -0
  46. spectre_core/receivers/library/_test.py +221 -0
  47. spectre_core/spectrograms/__init__.py +18 -0
  48. spectre_core/spectrograms/{analytical.py → _analytical.py} +29 -27
  49. spectre_core/spectrograms/{array_operations.py → _array_operations.py} +47 -1
  50. spectre_core/spectrograms/{spectrogram.py → _spectrogram.py} +62 -35
  51. spectre_core/spectrograms/{transform.py → _transform.py} +76 -89
  52. spectre_core/{post_processing/library → wgetting}/__init__.py +4 -5
  53. spectre_core/wgetting/_callisto.py +155 -0
  54. {spectre_core-0.0.9.dist-info → spectre_core-0.0.10.dist-info}/METADATA +1 -1
  55. spectre_core-0.0.10.dist-info/RECORD +63 -0
  56. spectre_core/cfg.py +0 -116
  57. spectre_core/chunks/library/__init__.py +0 -8
  58. spectre_core/chunks/library/sweep/__init__.py +0 -0
  59. spectre_core/chunks/library/sweep/chunk.py +0 -400
  60. spectre_core/dynamic_imports.py +0 -22
  61. spectre_core/file_handlers/base.py +0 -68
  62. spectre_core/file_handlers/configs.py +0 -271
  63. spectre_core/file_handlers/json.py +0 -40
  64. spectre_core/file_handlers/text.py +0 -21
  65. spectre_core/plotting/factory.py +0 -26
  66. spectre_core/plotting/format.py +0 -19
  67. spectre_core/plotting/library/__init__.py +0 -7
  68. spectre_core/plotting/library/frequency_cuts/panel.py +0 -74
  69. spectre_core/plotting/library/integral_over_frequency/panel.py +0 -34
  70. spectre_core/plotting/library/spectrogram/panel.py +0 -92
  71. spectre_core/plotting/library/time_cuts/panel.py +0 -77
  72. spectre_core/plotting/panel_register.py +0 -13
  73. spectre_core/post_processing/base.py +0 -132
  74. spectre_core/post_processing/library/fixed/__init__.py +0 -0
  75. spectre_core/post_processing/library/fixed/event_handler.py +0 -40
  76. spectre_core/post_processing/library/sweep/event_handler.py +0 -54
  77. spectre_core/receivers/base.py +0 -422
  78. spectre_core/receivers/library/__init__.py +0 -7
  79. spectre_core/receivers/library/rsp1a/__init__.py +0 -0
  80. spectre_core/receivers/library/rsp1a/gr/__init__.py +0 -0
  81. spectre_core/receivers/library/rsp1a/gr/fixed.py +0 -104
  82. spectre_core/receivers/library/rsp1a/gr/sweep.py +0 -129
  83. spectre_core/receivers/library/rsp1a/receiver.py +0 -68
  84. spectre_core/receivers/library/rspduo/__init__.py +0 -0
  85. spectre_core/receivers/library/rspduo/gr/__init__.py +0 -0
  86. spectre_core/receivers/library/rspduo/gr/tuner_1_fixed.py +0 -114
  87. spectre_core/receivers/library/rspduo/gr/tuner_1_sweep.py +0 -131
  88. spectre_core/receivers/library/rspduo/gr/tuner_2_fixed.py +0 -120
  89. spectre_core/receivers/library/rspduo/gr/tuner_2_sweep.py +0 -119
  90. spectre_core/receivers/library/rspduo/receiver.py +0 -97
  91. spectre_core/receivers/library/test/__init__.py +0 -0
  92. spectre_core/receivers/library/test/gr/__init__.py +0 -0
  93. spectre_core/receivers/library/test/gr/cosine_signal_1.py +0 -83
  94. spectre_core/receivers/library/test/gr/tagged_staircase.py +0 -93
  95. spectre_core/receivers/library/test/receiver.py +0 -203
  96. spectre_core/receivers/validators.py +0 -231
  97. spectre_core/web_fetch/callisto.py +0 -101
  98. spectre_core-0.0.9.dist-info/RECORD +0 -74
  99. /spectre_core/chunks/{chunk_register.py → _register.py} +0 -0
  100. /spectre_core/post_processing/{event_handler_register.py → _register.py} +0 -0
  101. /spectre_core/receivers/{receiver_register.py → _register.py} +0 -0
  102. /spectre_core/{chunks/library/callisto/__init__.py → receivers/gr/_rspduo.py} +0 -0
  103. /spectre_core/{chunks/library/fixed/__init__.py → receivers/library/_rspduo.py} +0 -0
  104. {spectre_core-0.0.9.dist-info → spectre_core-0.0.10.dist-info}/LICENSE +0 -0
  105. {spectre_core-0.0.9.dist-info → spectre_core-0.0.10.dist-info}/WHEEL +0 -0
  106. {spectre_core-0.0.9.dist-info → spectre_core-0.0.10.dist-info}/top_level.txt +0 -0
@@ -2,18 +2,18 @@
2
2
  # This file is part of SPECTRE
3
3
  # SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
- import os
6
- from typing import Optional, Any
5
+ from typing import Optional
7
6
  from warnings import warn
8
7
  from datetime import datetime, timedelta
9
8
  from dataclasses import dataclass
9
+ import os
10
10
 
11
11
  import numpy as np
12
12
  from astropy.io import fits
13
13
 
14
- from spectre_core.file_handlers.configs import FitsConfig
15
- from spectre_core.cfg import DEFAULT_DATETIME_FORMAT, get_chunks_dir_path
16
- from spectre_core.spectrograms.array_operations import (
14
+ from spectre_core.capture_configs import CaptureConfig, PNames
15
+ from spectre_core.config import get_chunks_dir_path, TimeFormats
16
+ from ._array_operations import (
17
17
  find_closest_index,
18
18
  normalise_peak_intensity,
19
19
  compute_resolution,
@@ -21,6 +21,14 @@ from spectre_core.spectrograms.array_operations import (
21
21
  subtract_background,
22
22
  )
23
23
 
24
+ __all__ = [
25
+ "FrequencyCut",
26
+ "TimeCut",
27
+ "TimeTypes",
28
+ "SpectrumTypes",
29
+ "Spectrogram"
30
+ ]
31
+
24
32
  @dataclass
25
33
  class FrequencyCut:
26
34
  time: float | datetime
@@ -35,8 +43,21 @@ class TimeCut:
35
43
  times: np.ndarray
36
44
  cut: np.ndarray
37
45
  spectrum_type: str
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class TimeTypes:
50
+ SECONDS : str = "seconds"
51
+ DATETIMES: str = "datetimes"
38
52
 
39
53
 
54
+ @dataclass(frozen=True)
55
+ class SpectrumTypes:
56
+ AMPLITUDE: str = "amplitude"
57
+ POWER : str = "power"
58
+ DIGITS : str = "digits"
59
+
60
+
40
61
  class Spectrogram:
41
62
  def __init__(self,
42
63
  dynamic_spectra: np.ndarray, # holds the spectrogram data
@@ -54,6 +75,9 @@ class Spectrogram:
54
75
  self._dynamic_spectra_as_dBb: Optional[np.ndarray] = None # cache
55
76
 
56
77
  # assigned times and frequencies
78
+ if times[0] != 0:
79
+ raise ValueError(f"The first spectrum must correspond to t=0 [s]")
80
+
57
81
  self._times = times
58
82
  self._datetimes: Optional[list[datetime]] = None # cache
59
83
  self._frequencies = frequencies
@@ -141,7 +165,7 @@ class Spectrogram:
141
165
  @property
142
166
  def chunk_start_datetime(self) -> datetime:
143
167
  if self._chunk_start_datetime is None:
144
- self._chunk_start_datetime = datetime.strptime(self.chunk_start_time, DEFAULT_DATETIME_FORMAT)
168
+ self._chunk_start_datetime = datetime.strptime(self.chunk_start_time, TimeFormats.DATETIME)
145
169
  return self._chunk_start_datetime
146
170
 
147
171
 
@@ -181,9 +205,9 @@ class Spectrogram:
181
205
  # Suppress divide by zero and invalid value warnings for this block of code
182
206
  with np.errstate(divide='ignore'):
183
207
  # Depending on the spectrum type, compute the dBb values differently
184
- if self._spectrum_type == "amplitude" or self._spectrum_type == "digits":
208
+ if self._spectrum_type == SpectrumTypes.AMPLITUDE or self._spectrum_type == SpectrumTypes.DIGITS:
185
209
  self._dynamic_spectra_as_dBb = 10 * np.log10(self._dynamic_spectra / background_spectra)
186
- elif self._spectrum_type == "power":
210
+ elif self._spectrum_type == SpectrumTypes.POWER:
187
211
  self._dynamic_spectra_as_dBb = 20 * np.log10(self._dynamic_spectra / background_spectra)
188
212
  else:
189
213
  raise NotImplementedError(f"{self.spectrum_type} unrecognised, uncertain decibel conversion!")
@@ -203,12 +227,12 @@ class Spectrogram:
203
227
 
204
228
 
205
229
  def _update_background_indices_from_interval(self) -> None:
206
- start_background = datetime.strptime(self._start_background, DEFAULT_DATETIME_FORMAT)
230
+ start_background = datetime.strptime(self._start_background, TimeFormats.DATETIME)
207
231
  self._start_background_index = find_closest_index(start_background,
208
232
  self.datetimes,
209
233
  enforce_strict_bounds=True)
210
234
 
211
- end_background = datetime.strptime(self._end_background, DEFAULT_DATETIME_FORMAT)
235
+ end_background = datetime.strptime(self._end_background, TimeFormats.DATETIME)
212
236
  self._end_background_index = find_closest_index(end_background,
213
237
  self.datetimes,
214
238
  enforce_strict_bounds=True)
@@ -229,16 +253,13 @@ class Spectrogram:
229
253
 
230
254
 
231
255
  def save(self) -> None:
232
- fits_config = FitsConfig(self._tag)
233
- fits_config = fits_config if fits_config.exists else {}
234
-
235
- chunk_start_datetime = self.chunk_start_datetime
236
- chunk_parent_path = get_chunks_dir_path(year = chunk_start_datetime.year,
237
- month = chunk_start_datetime.month,
238
- day = chunk_start_datetime.day)
256
+ chunk_parent_path = get_chunks_dir_path(year = self.chunk_start_datetime.year,
257
+ month = self.chunk_start_datetime.month,
258
+ day = self.chunk_start_datetime.day)
239
259
  file_name = f"{self.chunk_start_time}_{self._tag}.fits"
240
- write_path = os.path.join(chunk_parent_path, file_name)
241
- _save_spectrogram(write_path, self, fits_config)
260
+ write_path = os.path.join(chunk_parent_path,
261
+ file_name)
262
+ _save_spectrogram(write_path, self)
242
263
 
243
264
 
244
265
  def integrate_over_frequency(self,
@@ -266,7 +287,7 @@ class Spectrogram:
266
287
  # exactly to one of the times assigned to each spectrogram. So, we compute the nearest achievable,
267
288
  # and return it from the function as output too.
268
289
  if isinstance(at_time, str):
269
- at_time = datetime.strptime(at_time, DEFAULT_DATETIME_FORMAT)
290
+ at_time = datetime.strptime(at_time, TimeFormats.DATETIME)
270
291
  index_of_cut = find_closest_index(at_time,
271
292
  self.datetimes,
272
293
  enforce_strict_bounds = True)
@@ -306,7 +327,7 @@ class Spectrogram:
306
327
  dBb: bool = False,
307
328
  peak_normalise = False,
308
329
  correct_background = False,
309
- return_time_type: str = "seconds") -> TimeCut:
330
+ return_time_type: str = TimeTypes.SECONDS) -> TimeCut:
310
331
 
311
332
  # it is important to note that the "at frequency" specified by the user likely does not correspond
312
333
  # exactly to one of the physical frequencies assigned to each spectral component. So, we compute the nearest achievable,
@@ -316,9 +337,9 @@ class Spectrogram:
316
337
  enforce_strict_bounds = True)
317
338
  frequency_of_cut = self.frequencies[index_of_cut]
318
339
 
319
- if return_time_type == "datetimes":
340
+ if return_time_type == TimeTypes.DATETIMES:
320
341
  times = self.datetimes
321
- elif return_time_type == "seconds":
342
+ elif return_time_type == TimeTypes.SECONDS:
322
343
  times = self.times
323
344
  else:
324
345
  raise ValueError(f"Invalid return_time_type. Got {return_time_type}, expected one of 'datetimes' or 'seconds'")
@@ -359,10 +380,16 @@ def _seconds_of_day(dt: datetime) -> float:
359
380
 
360
381
  # Function to create a FITS file with the specified structure
361
382
  def _save_spectrogram(write_path: str,
362
- spectrogram: Spectrogram,
363
- fits_config: FitsConfig | dict[str, Any]) -> None:
364
- if spectrogram.chunk_start_time is None:
365
- raise ValueError(f"Spectrogram must have a defined chunk_start_time. Received {spectrogram.chunk_start_time}")
383
+ spectrogram: Spectrogram) -> None:
384
+
385
+ capture_config = CaptureConfig(spectrogram.tag)
386
+ ORIGIN = capture_config.get_parameter_value(PNames.ORIGIN)
387
+ INSTRUME = capture_config.get_parameter_value(PNames.INSTRUMENT)
388
+ TELESCOP = capture_config.get_parameter_value(PNames.TELESCOPE)
389
+ OBJECT = capture_config.get_parameter_value(PNames.OBJECT)
390
+ OBS_ALT = capture_config.get_parameter_value(PNames.OBS_ALT)
391
+ OBS_LAT = capture_config.get_parameter_value(PNames.OBS_LAT)
392
+ OBS_LON = capture_config.get_parameter_value(PNames.OBS_LON)
366
393
 
367
394
  # Primary HDU with data
368
395
  primary_data = spectrogram.dynamic_spectra.astype(dtype=np.float32)
@@ -397,10 +424,10 @@ def _save_spectrogram(write_path: str,
397
424
 
398
425
  primary_hdu.header.set('DATE', start_date, 'time of observation')
399
426
  primary_hdu.header.set('CONTENT', f'{start_date} dynamic spectrogram', 'title of image')
400
- primary_hdu.header.set('ORIGIN', f'{fits_config.get("ORIGIN")}')
401
- primary_hdu.header.set('TELESCOP', f'{fits_config.get("TELESCOP")} tag: {spectrogram.tag}', 'type of instrument')
402
- primary_hdu.header.set('INSTRUME', f'{fits_config.get("INSTRUME")}')
403
- primary_hdu.header.set('OBJECT', f'{fits_config.get("OBJECT")}', 'object description')
427
+ primary_hdu.header.set('ORIGIN', f'{ORIGIN}')
428
+ primary_hdu.header.set('TELESCOP', f'{TELESCOP}', 'type of instrument')
429
+ primary_hdu.header.set('INSTRUME', f'{INSTRUME}')
430
+ primary_hdu.header.set('OBJECT', f'{OBJECT}', 'object description')
404
431
 
405
432
  primary_hdu.header.set('DATE-OBS', f'{start_date}', 'date observation starts')
406
433
  primary_hdu.header.set('TIME-OBS', f'{start_time}', 'time observation starts')
@@ -424,17 +451,17 @@ def _save_spectrogram(write_path: str,
424
451
  primary_hdu.header.set('CTYPE2', 'Frequency [Hz]', 'title of axis 2')
425
452
  primary_hdu.header.set('CDELT2', spectrogram.frequency_resolution, 'step between first and second element in axis')
426
453
 
427
- primary_hdu.header.set('OBS_LAT', f'{fits_config.get("OBS_LAT")}', 'observatory latitude in degree')
454
+ primary_hdu.header.set('OBS_LAT', f'{OBS_LAT}', 'observatory latitude in degree')
428
455
  primary_hdu.header.set('OBS_LAC', 'N', 'observatory latitude code {N,S}')
429
- primary_hdu.header.set('OBS_LON', f'{fits_config.get("OBS_LON")}', 'observatory longitude in degree')
456
+ primary_hdu.header.set('OBS_LON', f'{OBS_LON}', 'observatory longitude in degree')
430
457
  primary_hdu.header.set('OBS_LOC', 'W', 'observatory longitude code {E,W}')
431
- primary_hdu.header.set('OBS_ALT', f'{fits_config.get("OBS_ALT")}', 'observatory altitude in meter asl')
458
+ primary_hdu.header.set('OBS_ALT', f'{OBS_ALT}', 'observatory altitude in meter asl')
432
459
 
433
460
 
434
461
  # Wrap arrays in an additional dimension to mimic the e-CALLISTO storage
435
462
  times_wrapped = np.array([spectrogram.times.astype(np.float32)])
436
463
  # To mimic e-Callisto storage, convert frequencies to MHz
437
- frequencies_MHz = spectrogram.frequencies *1e-6
464
+ frequencies_MHz = spectrogram.frequencies * 1e-6
438
465
  frequencies_wrapped = np.array([frequencies_MHz.astype(np.float32)])
439
466
 
440
467
  # Binary Table HDU (extension)
@@ -5,59 +5,19 @@
5
5
  import numpy as np
6
6
  from datetime import datetime, timedelta
7
7
  from typing import Optional
8
+ from math import floor
8
9
 
9
- from spectre_core.spectrograms.array_operations import find_closest_index
10
- from spectre_core.spectrograms.spectrogram import Spectrogram
11
- from spectre_core.cfg import DEFAULT_DATETIME_FORMAT
12
-
13
-
14
- def _average_array(array: np.ndarray, average_over: int, axis=0) -> np.ndarray:
15
-
16
- # Check if average_over is an integer
17
- if type(average_over) != int:
18
- raise TypeError(f"average_over must be an integer. Got {type(average_over)}")
19
-
20
- # Get the size of the specified axis which we will average over
21
- axis_size = array.shape[axis]
22
- # Check if average_over is within the valid range
23
- if not 1 <= average_over <= axis_size:
24
- raise ValueError(f"average_over must be between 1 and the length of the axis ({axis_size})")
25
-
26
- max_axis_index = len(np.shape(array)) - 1
27
- if axis > max_axis_index: # zero indexing on specifying axis, so minus one
28
- raise ValueError(f"Requested axis is out of range of array dimensions. Axis: {axis}, max axis index: {max_axis_index}")
29
-
30
- # find the number of elements in the requested axis
31
- num_elements = array.shape[axis]
32
-
33
- # find the number of "full blocks" to average over
34
- num_full_blocks = num_elements // average_over
35
- # if num_elements is not exactly divisible by average_over, we will have some elements left over
36
- # these remaining elements will be padded with nans to become another full block
37
- remainder = num_elements % average_over
38
-
39
- # if there exists a remainder, pad the last block
40
- if remainder != 0:
41
- # initialise an array to hold the padding shape
42
- padding_shape = [(0, 0)] * array.ndim
43
- # pad after the last column in the requested axis
44
- padding_shape[axis] = (0, average_over - remainder)
45
- # pad with nan values (so to not contribute towards the mean computation)
46
- array = np.pad(array, padding_shape, mode='constant', constant_values=np.nan)
47
-
48
- # initalise a list to hold the new shape
49
- new_shape = list(array.shape)
50
- # update the shape on the requested access (to the number of blocks we will average over)
51
- new_shape[axis] = num_full_blocks + (1 if remainder else 0)
52
- # insert a new dimension, with the size of each block
53
- new_shape.insert(axis + 1, average_over)
54
- # and reshape the array to sort the array into the relevant blocks.
55
- reshaped_array = array.reshape(new_shape)
56
- # average over the newly created axis, essentially averaging over the blocks.
57
- averaged_array = np.nanmean(reshaped_array, axis=axis + 1)
58
- # return the averaged array
59
- return averaged_array
10
+ from spectre_core.config import TimeFormats
11
+ from ._array_operations import find_closest_index, average_array
12
+ from ._spectrogram import Spectrogram
60
13
 
14
+ __all__ = [
15
+ "frequency_chop",
16
+ "time_chop",
17
+ "frequency_average",
18
+ "time_average",
19
+ "join_spectrograms"
20
+ ]
61
21
 
62
22
  def frequency_chop(input_spectrogram: Spectrogram,
63
23
  start_frequency: float | int,
@@ -98,7 +58,7 @@ def frequency_chop(input_spectrogram: Spectrogram,
98
58
  def time_chop(input_spectrogram: Spectrogram,
99
59
  start_time: str,
100
60
  end_time: str,
101
- time_format: str = DEFAULT_DATETIME_FORMAT) -> Optional[Spectrogram]:
61
+ time_format: str = TimeFormats.DATETIME) -> Optional[Spectrogram]:
102
62
 
103
63
  # parse the strings as datetimes
104
64
  start_datetime = datetime.strptime(start_time, time_format)
@@ -122,18 +82,17 @@ def time_chop(input_spectrogram: Spectrogram,
122
82
  # chop the spectrogram
123
83
  transformed_dynamic_spectra = input_spectrogram.dynamic_spectra[:, start_index:end_index+1]
124
84
 
125
- # chop the times array
126
- transformed_times = input_spectrogram.times[start_index:end_index+1]
127
- #translate the chopped times array to start at zero
128
- transformed_times -= transformed_times[0]
129
-
130
85
  # compute the new start datetime following the time chop
131
86
  transformed_start_datetime = input_spectrogram.datetimes[start_index]
132
- # parse the chunk start time (as string)
133
- transformed_chunk_start_time = datetime.strftime(transformed_start_datetime, DEFAULT_DATETIME_FORMAT)
134
- # and compute the microsecond correction
87
+ # compute the microsecond correction, and chunk start time
88
+ transformed_chunk_start_time = datetime.strftime(transformed_start_datetime, TimeFormats.DATETIME)
135
89
  transformed_microsecond_correction = transformed_start_datetime.microsecond
136
90
 
91
+ # chop the times array
92
+ transformed_times = input_spectrogram.times[start_index:end_index+1]
93
+ # assign the first spectrum to t=0 [s]
94
+ transformed_times -= transformed_times[0]
95
+
137
96
  return Spectrogram(transformed_dynamic_spectra,
138
97
  transformed_times,
139
98
  input_spectrogram.frequencies,
@@ -144,33 +103,44 @@ def time_chop(input_spectrogram: Spectrogram,
144
103
 
145
104
 
146
105
  def time_average(input_spectrogram: Spectrogram,
147
- average_over: int) -> Spectrogram:
148
-
106
+ resolution: Optional[float] = None,
107
+ average_over: Optional[int] = None) -> Spectrogram:
108
+
149
109
  # spectre does not currently support averaging of non-datetime assigned spectrograms
150
110
  if input_spectrogram.chunk_start_time is None:
151
111
  raise ValueError(f"Input spectrogram is missing chunk start time. Averaging is not yet supported for non-datetime assigned spectrograms")
152
112
 
153
- # if the user has requested no averaging, just return the same instance unchanged
113
+ # if nothing is specified, do nothing
114
+ if (resolution is None) and (average_over is None):
115
+ average_over = 1
116
+
117
+ if not (resolution is not None) ^ (average_over is not None):
118
+ raise ValueError(f"Exactly one of 'resolution' or 'average_over' "
119
+ f"must be specified.")
120
+
121
+ # if the resolution is specified, compute the appropriate number of spectrums to average over
122
+ # and recall the same function
123
+ if resolution is not None:
124
+ average_over = max(1, floor(resolution / input_spectrogram.time_resolution))
125
+ return time_average(input_spectrogram, average_over=average_over)
126
+
127
+ # No averaging is required, if we have to average over every one spectrum
154
128
  if average_over == 1:
155
129
  return input_spectrogram
156
130
 
157
131
  # average the dynamic spectra array
158
- transformed_dynamic_spectra = _average_array(input_spectrogram.dynamic_spectra, average_over, axis=1)
159
- # average the times array s.t. the ith averaged spectrum is assigned the
160
- transformed_times = _average_array(input_spectrogram.times, average_over)
161
-
162
- # We need to assign timestamps to the averaged spectrums in the spectrograms. The natural way to do this
163
- # is to assign the i'th averaged spectrogram to the i'th averaged time stamp. From this,
164
- # we then need to compute the chunk start time to assig to the first averaged spectrum,
165
- # and update the microsecond correction.
166
-
167
- # define the initial spectrum as the spectrum at time index 0 in the spectrogram
168
- # then, averaged_t0 is the seconds elapsed between the input intial spectrum and the averaged intial spectrum
169
- averaged_t0 = float(transformed_times[0])
170
- # compute the updated chunk start time and the updated microsecond correction based on averaged_t0
171
- corrected_start_datetime = input_spectrogram.datetimes[0] + timedelta(seconds = averaged_t0)
172
- # then, compute the transformed chunk start time and microsecond correction
173
- transformed_chunk_start_time = corrected_start_datetime.strftime(DEFAULT_DATETIME_FORMAT)
132
+ transformed_dynamic_spectra = average_array(input_spectrogram.dynamic_spectra,
133
+ average_over,
134
+ axis=1)
135
+
136
+ # We need to assign timestamps to the averaged spectrums in the spectrograms.
137
+ # The natural way to do this is to assign the i'th averaged spectrogram
138
+ # to the i'th averaged time
139
+ transformed_times = average_array(input_spectrogram.times, average_over)
140
+
141
+ # find the new chunk start time, which we will assign to the first spectrum after averaging
142
+ corrected_start_datetime = input_spectrogram.datetimes[0] + timedelta(seconds = float(transformed_times[0]))
143
+ transformed_chunk_start_time = corrected_start_datetime.strftime(TimeFormats.DATETIME)
174
144
  transformed_microsecond_correction = corrected_start_datetime.microsecond
175
145
 
176
146
  # finally, translate the averaged time seconds to begin at t=0 [s]
@@ -183,18 +153,37 @@ def time_average(input_spectrogram: Spectrogram,
183
153
  microsecond_correction = transformed_microsecond_correction,
184
154
  spectrum_type = input_spectrogram.spectrum_type)
185
155
 
156
+
157
+
186
158
  def frequency_average(input_spectrogram: Spectrogram,
187
- average_over: int) -> Spectrogram:
159
+ resolution: Optional[float] = None,
160
+ average_over: Optional[int] = None) -> Spectrogram:
161
+
162
+ # if nothing is specified, do nothing
163
+ if (resolution is None) and (average_over is None):
164
+ average_over = 1
165
+
166
+ if not (resolution is not None) ^ (average_over is not None):
167
+ raise ValueError(f"Exactly one of 'resolution' or 'average_over' "
168
+ f"must be specified.")
169
+
170
+ # if the resolution is specified, compute the appropriate number of spectrums to average over
171
+ # and recall the same function
172
+ if resolution is not None:
173
+ average_over = max(1, floor(resolution / input_spectrogram.frequency_resolution))
174
+ return frequency_average(input_spectrogram, average_over=average_over)
188
175
 
189
- # if the user has requested no averaging, just return the same instance unchanged
176
+ # No averaging is required, if we have to average over every one spectrum
190
177
  if average_over == 1:
191
178
  return input_spectrogram
192
-
179
+
193
180
  # We need to assign physical frequencies to the averaged spectrums in the spectrograms.
194
181
  # is to assign the i'th averaged spectral component to the i'th averaged frequency.
195
182
  # average the dynamic spectra array
196
- transformed_dynamic_spectra = _average_array(input_spectrogram.dynamic_spectra, average_over, axis=0)
197
- transformed_frequencies = _average_array(input_spectrogram.frequencies, average_over)
183
+ transformed_dynamic_spectra = average_array(input_spectrogram.dynamic_spectra,
184
+ average_over,
185
+ axis=0)
186
+ transformed_frequencies = average_array(input_spectrogram.frequencies, average_over)
198
187
 
199
188
  return Spectrogram(transformed_dynamic_spectra,
200
189
  input_spectrogram.times,
@@ -213,6 +202,7 @@ def _time_elapsed(datetimes: np.ndarray) -> np.ndarray:
213
202
  # Convert the list of seconds to a NumPy array of type float32
214
203
  return np.array(elapsed_time, dtype=np.float32)
215
204
 
205
+
216
206
  # we assume that the spectrogram list is ORDERED chronologically
217
207
  # we assume there is no time overlap in any of the spectrograms in the list
218
208
  def join_spectrograms(spectrograms: list[Spectrogram]) -> Spectrogram:
@@ -254,14 +244,11 @@ def join_spectrograms(spectrograms: list[Spectrogram]) -> Spectrogram:
254
244
  start_index = end_index
255
245
 
256
246
  transformed_times = _time_elapsed(conc_datetimes)
257
-
258
- transformed_microsecond_correction = conc_datetimes[0].microsecond
259
247
 
260
248
  return Spectrogram(transformed_dynamic_spectra,
261
249
  transformed_times,
262
250
  reference_spectrogram.frequencies,
263
251
  reference_spectrogram.tag,
264
252
  chunk_start_time = reference_spectrogram.chunk_start_time,
265
- microsecond_correction = transformed_microsecond_correction,
266
- spectrum_type = reference_spectrogram.spectrum_type)
267
-
253
+ microsecond_correction = reference_spectrogram.microsecond_correction,
254
+ spectrum_type = reference_spectrogram.spectrum_type)
@@ -2,9 +2,8 @@
2
2
  # This file is part of SPECTRE
3
3
  # SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
- from spectre_core.dynamic_imports import import_target_modules
6
-
7
- import_target_modules(__file__, __name__, "event_handler")
8
-
9
-
5
+ from ._callisto import download_callisto_data, CALLISTO_INSTRUMENT_CODES
10
6
 
7
+ __all__ = [
8
+ "download_callisto_data", "CALLISTO_INSTRUMENT_CODES"
9
+ ]
@@ -0,0 +1,155 @@
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
+ import subprocess
7
+ import shutil
8
+ import gzip
9
+ from datetime import datetime
10
+ from typing import Optional
11
+
12
+ from spectre_core.config import get_spectre_data_dir_path, get_chunks_dir_path, TimeFormats
13
+
14
+ CALLISTO_INSTRUMENT_CODES = [
15
+ "ALASKA-ANCHORAGE",
16
+ "ALASKA-COHOE",
17
+ "ALASKA-HAARP",
18
+ "ALGERIA-CRAAG",
19
+ "ALMATY",
20
+ "AUSTRIA-Krumbach",
21
+ "AUSTRIA-MICHELBACH",
22
+ "AUSTRIA-OE3FLB",
23
+ "AUSTRIA-UNIGRAZ",
24
+ "Australia-ASSA",
25
+ "BIR",
26
+ "Croatia-Visnjan",
27
+ "DENMARK",
28
+ "EGYPT-Alexandria",
29
+ "EGYPT-SpaceAgency",
30
+ "FINLAND-Siuntio",
31
+ "Finland-Kempele",
32
+ "GERMANY-DLR",
33
+ "GLASGOW",
34
+ "GREENLAND",
35
+ "HUMAIN",
36
+ "HURBANOVO",
37
+ "INDIA-GAURI",
38
+ "INDIA-OOTY",
39
+ "INDIA-UDAIPUR",
40
+ "JAPAN-IBARAKI",
41
+ "KASI",
42
+ "MEXART",
43
+ "MEXICO-FCFM-UANL",
44
+ "MEXICO-LANCE-A",
45
+ "MEXICO-LANCE-B",
46
+ "MONGOLIA-UB",
47
+ "MRO",
48
+ "MRT3",
49
+ "Malaysia-Banting",
50
+ "NORWAY-EGERSUND",
51
+ "NORWAY-NY-AALESUND",
52
+ "NORWAY-RANDABERG",
53
+ "POLAND-Grotniki",
54
+ "ROMANIA",
55
+ "ROSWELL-NM",
56
+ "SPAIN-PERALEJOS",
57
+ "SSRT",
58
+ "SWISS-HB9SCT",
59
+ "SWISS-HEITERSWIL",
60
+ "SWISS-IRSOL",
61
+ "SWISS-Landschlacht",
62
+ "SWISS-MUHEN",
63
+ "TRIEST",
64
+ "TURKEY",
65
+ "UNAM",
66
+ "URUGUAY",
67
+ "USA-BOSTON",
68
+ ]
69
+
70
+ _temp_dir = os.path.join(get_spectre_data_dir_path(), "temp")
71
+
72
+
73
+ def _get_chunk_name(station: str, date: str, time: str, instrument_code: str) -> str:
74
+ dt = datetime.strptime(f"{date}T{time}", '%Y%m%dT%H%M%S')
75
+ formatted_time = dt.strftime(TimeFormats.DATETIME)
76
+ return f"{formatted_time}_callisto-{station.lower()}-{instrument_code}.fits"
77
+
78
+
79
+ def _get_chunk_components(gz_path: str):
80
+ file_name = os.path.basename(gz_path)
81
+ if not file_name.endswith(".fit.gz"):
82
+ raise ValueError(f"Unexpected file extension in {file_name}. Expected .fit.gz")
83
+
84
+ file_base_name = file_name.rstrip(".fit.gz")
85
+ parts = file_base_name.split('_')
86
+ if len(parts) != 4:
87
+ raise ValueError("Filename does not conform to the expected format of [station]_[date]_[time]_[instrument_code]")
88
+
89
+ return parts
90
+
91
+
92
+ def _get_chunk_path(gz_path: str) -> str:
93
+ station, date, time, instrument_code = _get_chunk_components(gz_path)
94
+ fits_chunk_name = _get_chunk_name(station, date, time, instrument_code)
95
+ chunk_start_time = fits_chunk_name.split('_')[0]
96
+ chunk_start_datetime = datetime.strptime(chunk_start_time, TimeFormats.DATETIME)
97
+ chunk_parent_path = get_chunks_dir_path(year = chunk_start_datetime.year,
98
+ month = chunk_start_datetime.month,
99
+ day = chunk_start_datetime.day)
100
+ if not os.path.exists(chunk_parent_path):
101
+ os.makedirs(chunk_parent_path)
102
+ return os.path.join(chunk_parent_path, fits_chunk_name)
103
+
104
+
105
+ def _unzip_file_to_chunks(gz_path: str):
106
+ fits_path = _get_chunk_path(gz_path)
107
+ with gzip.open(gz_path, 'rb') as f_in, open(fits_path, 'wb') as f_out:
108
+ shutil.copyfileobj(f_in, f_out)
109
+
110
+
111
+ def _unzip_to_chunks():
112
+ for entry in os.scandir(_temp_dir):
113
+ if entry.is_file() and entry.name.endswith('.gz'):
114
+ _unzip_file_to_chunks(entry.path)
115
+ os.remove(entry.path)
116
+
117
+
118
+ def _wget_callisto_data(instrument_code: str,
119
+ year: int,
120
+ month: int,
121
+ day: int):
122
+ date_str = f"{year:04d}/{month:02d}/{day:02d}"
123
+ base_url = f"http://soleil.i4ds.ch/solarradio/data/2002-20yy_Callisto/{date_str}/"
124
+ command = [
125
+ 'wget', '-r', '-l1', '-nd', '-np',
126
+ '-R', '.tmp',
127
+ '-A', f'{instrument_code}*.fit.gz',
128
+ '-P', _temp_dir,
129
+ base_url
130
+ ]
131
+
132
+ try:
133
+ subprocess.run(command, check=True)
134
+ except subprocess.CalledProcessError as e:
135
+ print(f"An error occurred: {e}")
136
+
137
+
138
+ def download_callisto_data(instrument_code: Optional[str],
139
+ year: Optional[int],
140
+ month: Optional[int],
141
+ day: Optional[int]):
142
+
143
+
144
+ if (year is None) or (month is None) or (day is None):
145
+ raise ValueError(f"All of year, month and day should be specified")
146
+
147
+ if not os.path.exists(_temp_dir):
148
+ os.mkdir(_temp_dir)
149
+
150
+ if instrument_code not in CALLISTO_INSTRUMENT_CODES:
151
+ raise ValueError(f"No match found for '{instrument_code}'. Expected one of {CALLISTO_INSTRUMENT_CODES}")
152
+
153
+ _wget_callisto_data(instrument_code, year, month, day)
154
+ _unzip_to_chunks()
155
+ shutil.rmtree(_temp_dir)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: spectre-core
3
- Version: 0.0.9
3
+ Version: 0.0.10
4
4
  Summary: The core Python package used by the spectre program.
5
5
  Maintainer-email: Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE