lisainstrument 2.1.0__tar.gz → 2.2.1__tar.gz

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 (94) hide show
  1. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/PKG-INFO +5 -6
  2. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/instru/instru_file_reader.py +53 -14
  3. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/instru/instru_file_reader_v2_0_0.py +28 -3
  4. lisainstrument-2.2.1/lisainstrument/instru/instru_file_reader_v2_1_0.py +426 -0
  5. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/instru/instru_formulas.py +8 -2
  6. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/instru/instru_model.py +6 -0
  7. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/instru/instru_noises.py +11 -0
  8. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/instru/instru_store.py +4 -0
  9. lisainstrument-2.2.1/lisainstrument/instru/instru_store_v2_1_0.py +939 -0
  10. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/instrument.py +27 -10
  11. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/noisy/__init__.py +5 -0
  12. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/noisy/noise_defs.py +212 -25
  13. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/noisy/noise_defs_lisa.py +210 -62
  14. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/pyproject.toml +5 -4
  15. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/LICENSE +0 -0
  16. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/README.md +0 -0
  17. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/__init__.py +0 -0
  18. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/__main__.py +0 -0
  19. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/cli/__init__.py +0 -0
  20. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/cli/run_simulation.py +0 -0
  21. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/freqplan/__init__.py +0 -0
  22. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/freqplan/fplan_file.py +0 -0
  23. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/freqplan/fplan_source.py +0 -0
  24. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/freqplan/fplan_source_interp.py +0 -0
  25. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/glitches/__init__.py +0 -0
  26. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/glitches/glitch_file.py +0 -0
  27. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/glitches/glitch_source.py +0 -0
  28. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/glitches/glitch_source_interp.py +0 -0
  29. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/gwsource/__init__.py +0 -0
  30. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/gwsource/gw_file.py +0 -0
  31. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/gwsource/gw_source.py +0 -0
  32. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/gwsource/hdf5util.py +0 -0
  33. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/instru/__init__.py +0 -0
  34. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/instru/instru_defaults.py +0 -0
  35. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/instru/instru_filter.py +0 -0
  36. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/instru/instru_fplan.py +0 -0
  37. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/instru/instru_glitchsrc.py +0 -0
  38. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/instru/instru_gwsrc.py +0 -0
  39. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/instru/instru_locking.py +0 -0
  40. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/instru/instru_orbsrc.py +0 -0
  41. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/instru/instru_store_v2_0_0.py +0 -0
  42. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/legacy/__init__.py +0 -0
  43. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/legacy/containers.py +0 -0
  44. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/legacy/dsp.py +0 -0
  45. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/legacy/dynamic_delay_dsp.py +0 -0
  46. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/legacy/fixed_shift_dsp.py +0 -0
  47. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/legacy/hexagon.py +0 -0
  48. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/legacy/legacy_plots.py +0 -0
  49. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/legacy/noises.py +0 -0
  50. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/legacy/pyplnoise/LICENSE +0 -0
  51. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/legacy/pyplnoise/__init__.py +0 -0
  52. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/legacy/pyplnoise/pyplnoise.py +0 -0
  53. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/noisy/estimate_psd.py +0 -0
  54. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/noisy/noise_gen_numpy.py +0 -0
  55. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/orbiting/__init__.py +0 -0
  56. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/orbiting/constellation_enums.py +0 -0
  57. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/orbiting/orbit_file.py +0 -0
  58. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/orbiting/orbit_source.py +0 -0
  59. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/orbiting/orbit_source_interp.py +0 -0
  60. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/sigpro/__init__.py +0 -0
  61. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/sigpro/adaptive_delay_numpy.py +0 -0
  62. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/sigpro/chunked_splines.py +0 -0
  63. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/sigpro/dynamic_delay_numpy.py +0 -0
  64. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/sigpro/fir_filters_numpy.py +0 -0
  65. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/sigpro/fixed_shift_numpy.py +0 -0
  66. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/sigpro/iir_filters_numpy.py +0 -0
  67. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/sigpro/regular_interpolators.py +0 -0
  68. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/sigpro/shift_inversion_numpy.py +0 -0
  69. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/sigpro/types_numpy.py +0 -0
  70. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/streams/__init__.py +0 -0
  71. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/streams/analysis.py +0 -0
  72. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/streams/array.py +0 -0
  73. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/streams/delay.py +0 -0
  74. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/streams/derivative.py +0 -0
  75. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/streams/expression.py +0 -0
  76. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/streams/firfilter.py +0 -0
  77. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/streams/graph_util.py +0 -0
  78. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/streams/hdf5_store.py +0 -0
  79. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/streams/iirfilter.py +0 -0
  80. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/streams/integrate.py +0 -0
  81. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/streams/noise.py +0 -0
  82. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/streams/noise_alt.py +0 -0
  83. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/streams/null_store.py +0 -0
  84. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/streams/numpy_store.py +0 -0
  85. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/streams/sampling.py +0 -0
  86. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/streams/scheduler.py +0 -0
  87. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/streams/scheduler_dask.py +0 -0
  88. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/streams/scheduler_serial.py +0 -0
  89. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/streams/segments.py +0 -0
  90. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/streams/shift_inv.py +0 -0
  91. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/streams/store.py +0 -0
  92. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/streams/streams.py +0 -0
  93. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/streams/time.py +0 -0
  94. {lisainstrument-2.1.0 → lisainstrument-2.2.1}/lisainstrument/version.py +0 -0
@@ -1,18 +1,17 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: lisainstrument
3
- Version: 2.1.0
3
+ Version: 2.2.1
4
4
  Summary: Simulates the LISA measurement chain (noise and signals) and generates telemetry data
5
5
  Home-page: https://gitlab.in2p3.fr/lisa-simulation/instrument
6
6
  License: BSD 3-Clause
7
7
  Author: Jean-Baptiste Bayle
8
8
  Author-email: j2b.bayle@gmail.com
9
- Maintainer: Jean-Baptiste Bayle
10
- Maintainer-email: j2b.bayle@gmail.com
11
- Requires-Python: >=3.10,<4.0
9
+ Maintainer: Wolfgang Kastaun
10
+ Maintainer-email: wolfgang.kastaun@aei.mpg.de
11
+ Requires-Python: >=3.11,<4.0
12
12
  Classifier: Intended Audience :: Science/Research
13
13
  Classifier: License :: Other/Proprietary License
14
14
  Classifier: Programming Language :: Python :: 3
15
- Classifier: Programming Language :: Python :: 3.10
16
15
  Classifier: Programming Language :: Python :: 3.11
17
16
  Classifier: Programming Language :: Python :: 3.12
18
17
  Classifier: Programming Language :: Python :: 3.13
@@ -27,7 +26,7 @@ Requires-Dist: lisaconstants (>=2.0.1,<3.0.0)
27
26
  Requires-Dist: matplotlib (>=3.9.2,<4.0.0)
28
27
  Requires-Dist: numpy (>=2.1.2,<3.0.0)
29
28
  Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
30
- Requires-Dist: scipy (>=1.14.1,<2.0.0)
29
+ Requires-Dist: scipy (>=1.17.1,<2.0.0)
31
30
  Requires-Dist: typing-extensions (>=4.15.0,<5.0.0)
32
31
  Project-URL: Documentation, https://lisa-simulation.pages.in2p3.fr/instrument
33
32
  Project-URL: Download, https://gitlab.in2p3.fr/lisa-simulation/instrument/-/releases
@@ -27,6 +27,9 @@ from packaging.version import Version
27
27
  from lisainstrument.instru.instru_file_reader_v2_0_0 import (
28
28
  SimResultFile as SimResultFile_V2_0_0,
29
29
  )
30
+ from lisainstrument.instru.instru_file_reader_v2_1_0 import (
31
+ SimResultFile as SimResultFile_V2_1_0,
32
+ )
30
33
  from lisainstrument.instru.instru_store import (
31
34
  IdxSpace,
32
35
  SimFullDatasetsMOSA,
@@ -79,17 +82,30 @@ class SimResultFile:
79
82
  Arguments:
80
83
  path: Path of the a HDF5 file created by store_instru_hdf5
81
84
  """
82
- self._h5f = h5py.File(str(path), "r")
83
- self._version: Final = Version(self._h5f.attrs["version_format"])
85
+ self._h5f_raw = h5py.File(str(path), "r")
86
+
87
+ try:
88
+
89
+ self._version: Final = Version(self._h5f.attrs["version_format"])
90
+
91
+ ds_actual = set(self._h5f.keys()) - {"debug"}
92
+ ds_debug = set(self._h5f["debug"].keys())
93
+
94
+ md = json.loads(self._h5f.attrs["metadata_json"])
95
+
96
+ self._description = str(self._h5f.attrs.get("description", None))
97
+
98
+ except:
99
+ self.close()
100
+ raise
84
101
 
85
102
  if not SimResultFile.format_specifier().contains(self._version):
86
103
  msg = f"File {path} version {self._version} not compatible with file reader"
87
104
  raise RuntimeError(msg)
88
105
 
89
- ds_actual = set(self._h5f.keys()) - {"debug"}
90
- ds_debug = set(self._h5f["debug"].keys())
91
- ds_all = ds_actual | ds_debug
106
+ self._metadata = SimMetaData(**md)
92
107
 
108
+ ds_all = ds_actual | ds_debug
93
109
  if ds_all == SimResultsNumpyFull.all_dataset_names:
94
110
  self._extended = True
95
111
  elif ds_all == SimResultsNumpyCore.all_dataset_names:
@@ -98,11 +114,6 @@ class SimResultFile:
98
114
  msg = f"File {path} contains invalid set of quantities"
99
115
  raise RuntimeError(msg)
100
116
 
101
- md = json.loads(self._h5f.attrs["metadata_json"])
102
- self._metadata = SimMetaData(**md)
103
-
104
- self._description = str(self._h5f.attrs.get("description", None))
105
-
106
117
  self._t0 = self._metadata.t0
107
118
  self._sample_periods = {
108
119
  IdxSpace.PHYSICS: self._metadata.physics_dt,
@@ -126,9 +137,34 @@ class SimResultFile:
126
137
  i: _unique_index_range(r) for i, r in ranges.items()
127
138
  }
128
139
 
140
+ @property
141
+ def _h5f(self) -> h5py.File:
142
+ if self._h5f_raw is None:
143
+ msg = "SimResultFile used after closing it"
144
+ raise RuntimeError(msg)
145
+ return self._h5f_raw
146
+
147
+ def close(self) -> None:
148
+ """Close file
149
+
150
+ Note the interface provides a context manager, consider using `with`
151
+ mechanism instead
152
+ """
153
+ if self._h5f_raw is not None:
154
+ self._h5f_raw.close()
155
+ self._h5f_raw = None
156
+
129
157
  def __del__(self):
130
- """File is automatically closed"""
131
- self._h5f.close()
158
+ """HDF5 file is automatically closed"""
159
+ self.close()
160
+
161
+ def __enter__(self):
162
+ """Enter context"""
163
+ return self
164
+
165
+ def __exit__(self, exc_type, exc_val, exc_tb):
166
+ """Exit context"""
167
+ self.close()
132
168
 
133
169
  @property
134
170
  def format_version(self) -> Version:
@@ -412,7 +448,7 @@ class SimResultFile:
412
448
 
413
449
  def sim_result_file(
414
450
  path: pathlib.Path | str, only_compatible: bool = False
415
- ) -> SimResultFile | SimResultFile_V2_0_0:
451
+ ) -> SimResultFile | SimResultFile_V2_0_0 | SimResultFile_V2_1_0:
416
452
  """Open simulation result file
417
453
 
418
454
  This provides an interface with methods for reading the file.
@@ -429,9 +465,12 @@ def sim_result_file(
429
465
  SimResultFile instance for reading file data
430
466
  """
431
467
 
432
- readers: list[type[SimResultFile] | type[SimResultFile_V2_0_0]] = [SimResultFile]
468
+ readers: list[
469
+ type[SimResultFile] | type[SimResultFile_V2_0_0] | type[SimResultFile_V2_1_0]
470
+ ] = [SimResultFile]
433
471
  if not only_compatible:
434
472
  readers.append(SimResultFile_V2_0_0)
473
+ readers.append(SimResultFile_V2_1_0)
435
474
 
436
475
  version: Version
437
476
  for reader in readers:
@@ -70,7 +70,7 @@ class SimResultFile:
70
70
  Arguments:
71
71
  path: Path of the a HDF5 file created by store_instru_hdf5
72
72
  """
73
- self._h5f = h5py.File(str(path), "r")
73
+ self._h5f_raw = h5py.File(str(path), "r")
74
74
  self._version: Final = Version(self._h5f.attrs["version_format"])
75
75
 
76
76
  if not SimResultFile.format_specifier().contains(self._version):
@@ -117,9 +117,34 @@ class SimResultFile:
117
117
  i: _unique_index_range(r) for i, r in ranges.items()
118
118
  }
119
119
 
120
+ @property
121
+ def _h5f(self) -> h5py.File:
122
+ if self._h5f_raw is None:
123
+ msg = "SimResultFile used after closing it"
124
+ raise RuntimeError(msg)
125
+ return self._h5f_raw
126
+
127
+ def close(self) -> None:
128
+ """Close file
129
+
130
+ Note the interface provides a context manager, consider using `with`
131
+ mechanism instead
132
+ """
133
+ if self._h5f_raw is not None:
134
+ self._h5f_raw.close()
135
+ self._h5f_raw = None
136
+
120
137
  def __del__(self):
121
- """File is automatically closed"""
122
- self._h5f.close()
138
+ """HDF5 file is automatically closed"""
139
+ self.close()
140
+
141
+ def __enter__(self):
142
+ """Enter context"""
143
+ return self
144
+
145
+ def __exit__(self, exc_type, exc_val, exc_tb):
146
+ """Exit context"""
147
+ self.close()
123
148
 
124
149
  @property
125
150
  def format_version(self) -> Version:
@@ -0,0 +1,426 @@
1
+ # pylint: disable = duplicate-code
2
+ # The duplication is on purpose, we do not want to entangle readers for
3
+ # different formats, even though they are very similar.
4
+ """File reader for old 2.1.0 instrument file format
5
+
6
+ Files created by version 2.0.1 cannot be represented by the current interface
7
+ and data structures because they are missing some metadata items. To support
8
+ reading them, we keep the old interface here, which can be used by
9
+ `instru_file_reader.sim_results_file` to read the old format.
10
+ """
11
+
12
+ import json
13
+ import pathlib
14
+ from collections import defaultdict
15
+ from typing import Final, TypeVar
16
+
17
+ import h5py
18
+ import numpy as np
19
+ from packaging.specifiers import SpecifierSet
20
+ from packaging.version import Version
21
+
22
+ from lisainstrument.instru.instru_store_v2_1_0 import (
23
+ IdxSpace,
24
+ SimFullDatasetsMOSA,
25
+ SimFullDatasetsSat,
26
+ SimMetaData,
27
+ SimResultsNumpyCore,
28
+ SimResultsNumpyFull,
29
+ datasets_metadata_dict,
30
+ make_dataset_id,
31
+ )
32
+ from lisainstrument.orbiting.constellation_enums import MosaID, SatID
33
+ from lisainstrument.streams import DatasetIdentifier, StreamBundle
34
+ from lisainstrument.streams.hdf5_store import (
35
+ DataStorageHDF5,
36
+ instru_hdf5_file_as_stream_bundle,
37
+ )
38
+ from lisainstrument.streams.numpy_store import store_bundle_numpy
39
+
40
+ _T = TypeVar("_T", SimResultsNumpyFull, SimResultsNumpyCore)
41
+
42
+
43
+ def _unique_index_range(ranges: list[tuple[int, int]]) -> tuple[int, int]:
44
+ """Ensure index ranges in list are identical and return that range"""
45
+ s = set(ranges)
46
+ if len(s) != 1:
47
+ msg = f"unique_index_range: ranges not identical ({ranges=})"
48
+ raise RuntimeError(msg)
49
+ (uni,) = list(s)
50
+ return uni
51
+
52
+
53
+ class SimResultFile:
54
+ """Represents a simulation result file in HDF5 format.
55
+
56
+ There are low-level methods for direct reading of datasets identified either
57
+ by name and MOSA or SC index, or by a DatasetIdentifier. For use cases where the
58
+ results fit into memory, there are methods to create a SimResultsNumpyCore or
59
+ SimResultsNumpyFull instance with the data and metadata.
60
+
61
+ For use cases dealing with large data sets, there is a method representing
62
+ the datasets as streams in a StreamBundle for use with chunked processing.
63
+ For all those methods, it is possible to restrict the data to a given time
64
+ interval.
65
+ """
66
+
67
+ def __init__(self, path: pathlib.Path | str):
68
+ """Constructor
69
+
70
+ Arguments:
71
+ path: Path of the a HDF5 file created by store_instru_hdf5
72
+ """
73
+ self._h5f_raw = h5py.File(str(path), "r")
74
+ self._version: Final = Version(self._h5f.attrs["version_format"])
75
+
76
+ if not SimResultFile.format_specifier().contains(self._version):
77
+ msg = f"File {path} version {self._version} not compatible with file reader"
78
+ raise RuntimeError(msg)
79
+
80
+ ds_actual = set(self._h5f.keys()) - {"debug"}
81
+ ds_debug = set(self._h5f["debug"].keys())
82
+ ds_all = ds_actual | ds_debug
83
+
84
+ if ds_all == SimResultsNumpyFull.all_dataset_names:
85
+ self._extended = True
86
+ elif ds_all == SimResultsNumpyCore.all_dataset_names:
87
+ self._extended = False
88
+ else:
89
+ msg = f"File {path} contains invalid set of quantities"
90
+ raise RuntimeError(msg)
91
+
92
+ md = json.loads(self._h5f.attrs["metadata_json"])
93
+ self._metadata = SimMetaData(**md)
94
+
95
+ self._description = str(self._h5f.attrs.get("description", None))
96
+
97
+ self._t0 = self._metadata.t0
98
+ self._sample_periods = {
99
+ IdxSpace.PHYSICS: self._metadata.physics_dt,
100
+ IdxSpace.PHYSICS_EXT: self._metadata.physics_dt,
101
+ IdxSpace.REGULAR: self._metadata.dt,
102
+ IdxSpace.TELEMETRY: self._metadata.telemetry_dt,
103
+ }
104
+
105
+ categories = datasets_metadata_dict()
106
+
107
+ isps: dict[DatasetIdentifier, IdxSpace] = {}
108
+ ranges: dict[IdxSpace, list[tuple[int, int]]] = defaultdict(list)
109
+ for dsid in self.dataset_identifier_set():
110
+ n = dsid[-2]
111
+ cat = categories[n]
112
+ rg = self._read_range(dsid)
113
+ ranges[cat.idxspace].append(rg)
114
+ isps[dsid] = cat.idxspace
115
+ self._isp_by_dsid: Final = isps
116
+ self._range_by_isp: Final = {
117
+ i: _unique_index_range(r) for i, r in ranges.items()
118
+ }
119
+
120
+ @property
121
+ def _h5f(self) -> h5py.File:
122
+ if self._h5f_raw is None:
123
+ msg = "SimResultFile used after closing it"
124
+ raise RuntimeError(msg)
125
+ return self._h5f_raw
126
+
127
+ def close(self) -> None:
128
+ """Close file
129
+
130
+ Note the interface provides a context manager, consider using `with`
131
+ mechanism instead
132
+ """
133
+ if self._h5f_raw is not None:
134
+ self._h5f_raw.close()
135
+ self._h5f_raw = None
136
+
137
+ def __del__(self):
138
+ """HDF5 file is automatically closed"""
139
+ self.close()
140
+
141
+ def __enter__(self):
142
+ """Enter context"""
143
+ return self
144
+
145
+ def __exit__(self, exc_type, exc_val, exc_tb):
146
+ """Exit context"""
147
+ self.close()
148
+
149
+ @property
150
+ def format_version(self) -> Version:
151
+ """Version number of the file format"""
152
+ return self._version
153
+
154
+ @staticmethod
155
+ def format_specifier() -> SpecifierSet:
156
+ """Version specifier set compatible with file reader"""
157
+ v = Version("2.1.0")
158
+ return SpecifierSet(f"=={v}")
159
+
160
+ @classmethod
161
+ def check_file_version(cls, path: str | pathlib.Path) -> tuple[bool, Version]:
162
+ """Test if file version is compatible with file reader class
163
+
164
+ Arguments:
165
+ path: Path of file to check
166
+
167
+ Returns:
168
+ Whether file can be read and the file format version
169
+ """
170
+ with h5py.File(str(path), "r") as gwf:
171
+ version = Version(gwf.attrs["version_format"])
172
+ return cls.format_specifier().contains(version), version
173
+
174
+ @property
175
+ def is_extended(self) -> bool:
176
+ """Whether file contains extended set of quantities"""
177
+ return self._extended
178
+
179
+ @property
180
+ def metadata(self) -> SimMetaData:
181
+ """Simulation metadata"""
182
+ return self._metadata
183
+
184
+ @property
185
+ def description(self) -> str | None:
186
+ """Description text"""
187
+ return self._description
188
+
189
+ def dataset_identifier_set(self) -> set[DatasetIdentifier]:
190
+ """Set of all available dataset identifiers"""
191
+ if self.is_extended:
192
+ return SimResultsNumpyFull.dataset_identifier_set()
193
+ return SimResultsNumpyCore.dataset_identifier_set()
194
+
195
+ def idxspace_by_dataset_id(self, dsid: DatasetIdentifier) -> IdxSpace:
196
+ """Get index space for dataset"""
197
+ return self._isp_by_dsid[dsid]
198
+
199
+ def range_by_idxspace(self, isp: IdxSpace) -> tuple[int, int]:
200
+ """Get range for given index space"""
201
+ return self._range_by_isp[isp]
202
+
203
+ def range_by_dataset_id(self, dsid: DatasetIdentifier) -> tuple[int, int]:
204
+ """Get range for given dataset"""
205
+ return self.range_by_idxspace(self.idxspace_by_dataset_id(dsid))
206
+
207
+ def dt_by_idxspace(self, isp: IdxSpace) -> float:
208
+ """Get sample period for given index space"""
209
+ return self._sample_periods[isp]
210
+
211
+ def dt_by_dataset_id(self, dsid: DatasetIdentifier) -> float:
212
+ """Get sample period for given dataset"""
213
+ return self.dt_by_idxspace(self.idxspace_by_dataset_id(dsid))
214
+
215
+ def read_by_datset_id(
216
+ self,
217
+ dsid: DatasetIdentifier,
218
+ istart: int | None = None,
219
+ istop: int | None = None,
220
+ ) -> np.ndarray:
221
+ """Read data identified by a `DatasetIdentifier`
222
+
223
+ Optionally, one can restrict the index range. This refers to the logical
224
+ index range given returned `range_by_dataset_id()`, not necessarily starting
225
+ at zero. The returned data will contain indices `istart <= i < istop`
226
+
227
+ Arguments:
228
+ dsid: `DatasetIdentifier` specifying dataset
229
+ istart: Optionally, exclude lower indices
230
+ istop: Optionally, first index to exclude
231
+
232
+ Returns:
233
+ 1D numpy array with data
234
+ """
235
+ dspth = "/".join(dsid)
236
+ ds: h5py.Dataset = self._h5f[dspth]
237
+ aistart, aistop = self.range_by_dataset_id(dsid)
238
+ istart = aistart if istart is None else int(istart)
239
+ istop = aistop if istop is None else int(istop)
240
+ if not aistart <= istart <= istop <= aistop:
241
+ msg = (
242
+ f"SimResultFile: index range {istart}, {istop} not "
243
+ f"available for dataset {dspth}"
244
+ )
245
+ raise RuntimeError(msg)
246
+ if len(ds.shape) == 0: # pylint: disable = no-member
247
+ dat = np.empty(
248
+ istop - istart, dtype=ds.dtype # pylint: disable = no-member
249
+ )
250
+ dat[:] = ds[()]
251
+ return dat
252
+ return ds[istart - aistart : istop - aistart]
253
+
254
+ def read_by_name_and_mosa(
255
+ self,
256
+ name: str,
257
+ mosa: MosaID | str,
258
+ istart: int | None = None,
259
+ istop: int | None = None,
260
+ ) -> np.ndarray:
261
+ """Like `read_by_datset_id` but dataset is specified by name and `MosaID`
262
+
263
+ Arguments:
264
+ name: Dataset name
265
+ mosa: Read dataset for MOSA specified by `MosaID` or MOSA name
266
+ istart: Optionally, exclude lower indices
267
+ istop: Optionally, first index to exclude
268
+
269
+ Returns:
270
+ 1D numpy array with data
271
+ """
272
+
273
+ if name not in SimFullDatasetsMOSA.dataset_names():
274
+ msg = f"SimResultFile: invalid per-MOSA dataset {name}"
275
+ raise RuntimeError(msg)
276
+ cat = SimFullDatasetsMOSA.dataset_metadata()[name]
277
+ dsid = make_dataset_id(cat.actual, name, MosaID(mosa).value)
278
+ return self.read_by_datset_id(dsid, istart, istop)
279
+
280
+ def read_by_name_and_sat(
281
+ self,
282
+ name: str,
283
+ sc: SatID | str,
284
+ istart: int | None = None,
285
+ istop: int | None = None,
286
+ ) -> np.ndarray:
287
+ """Like `read_by_datset_id` but dataset is specified by name and `SatID`
288
+
289
+ Arguments:
290
+ name: Dataset name
291
+ sc: Read dataset for spacecraft specified by `SatID` or spacecraft name
292
+ istart: Optionally, exclude lower indices
293
+ istop: Optionally, first index to exclude
294
+
295
+ Returns:
296
+ 1D numpy array with data
297
+ """
298
+ if name not in SimFullDatasetsSat.dataset_names():
299
+ msg = f"SimResultFile: invalid per-spacecraft dataset {name}"
300
+ raise RuntimeError(msg)
301
+ cat = SimFullDatasetsSat.dataset_metadata()[name]
302
+ dsid = make_dataset_id(cat.actual, name, SatID(sc).value)
303
+ return self.read_by_datset_id(dsid, istart, istop)
304
+
305
+ def _read_range(self, dsid: DatasetIdentifier) -> tuple[int, int]:
306
+ """Get the index range available for a given dataset
307
+
308
+ The available indices `i` are in the range `istart <= i < istop`.
309
+
310
+ Arguments:
311
+ dsid: `DatasetIdentifier` specifying the dataset
312
+
313
+ Returns:
314
+ Tuple `(istart, istop)`
315
+ """
316
+ dspth = "/".join(dsid)
317
+ ds: h5py.Dataset = self._h5f[dspth]
318
+ istart = int(ds.attrs[DataStorageHDF5.attr_name_index_start])
319
+ istop = int(ds.attrs[DataStorageHDF5.attr_name_index_stop])
320
+ return (istart, istop)
321
+
322
+ def _restrict_range_isp(
323
+ self,
324
+ isp: IdxSpace,
325
+ t_min: float | None = None,
326
+ t_max: float | None = None,
327
+ ) -> tuple[int, int]:
328
+ """Compute index range within given time interval for a given dataset"""
329
+ dt = self.dt_by_idxspace(isp)
330
+ istart, istop = self.range_by_idxspace(isp)
331
+ if t_min is None:
332
+ i0 = istart
333
+ else:
334
+ i0 = int(np.ceil((t_min - self._t0) / dt))
335
+
336
+ if t_max is None:
337
+ i1 = istop
338
+ else:
339
+ i1 = int(np.ceil((t_max - self._t0) / dt))
340
+
341
+ return i0, i1
342
+
343
+ def _restrict_ranges(
344
+ self,
345
+ t_min: float | None = None,
346
+ t_max: float | None = None,
347
+ ) -> dict[IdxSpace, tuple[int, int]]:
348
+ """Compute index ranges within given time interval for datasets"""
349
+ return {
350
+ isp: self._restrict_range_isp(isp, t_min, t_max)
351
+ for isp in self._range_by_isp
352
+ }
353
+
354
+ def _read_datasets(
355
+ self,
356
+ cls: type[_T],
357
+ t_min: float | None = None,
358
+ t_max: float | None = None,
359
+ ) -> _T:
360
+ """Read datasets into a `DataStorageNumpy` instance"""
361
+ datasets = cls.dataset_identifier_set()
362
+ ranges_isp = self._restrict_ranges(t_min, t_max)
363
+ ranges_dsid = {
364
+ dsid: ranges_isp[self.idxspace_by_dataset_id(dsid)] for dsid in datasets
365
+ }
366
+
367
+ stb = self.as_stream_bundle(datasets, ranges_dsid)
368
+ store = store_bundle_numpy(stb)
369
+ return cls(store.as_dict(), ranges_isp, self.metadata.asdict())
370
+
371
+ def read_full(
372
+ self, t_min: float | None = None, t_max: float | None = None
373
+ ) -> SimResultsNumpyFull:
374
+ """Read extended set of quantities into memory as `SimResultsNumpyFull` instance
375
+
376
+ If the file does not contain the extended set, an RuntimeError is raised.
377
+
378
+ Optionally, on can restrict the time range for which the data samples are
379
+ read. This does not change the index space, i.e. which indices refer to which
380
+ times. It only changes the index range of the datasets, available through
381
+ the `sat_ranges` and `mosa_ranges` attributes of `SimResultsNumpyFull`.
382
+
383
+ Arguments:
384
+ t_min: Optionally, only read samples at later times
385
+ t_max: Optionally, only read samples at earlier times
386
+
387
+ Returns:
388
+ `SimResultsNumpyFull` instance with data.
389
+ """
390
+
391
+ if not self.is_extended:
392
+ msg = "SimResultFile: cannot read extended results from basic result file"
393
+ raise RuntimeError(msg)
394
+ return self._read_datasets(SimResultsNumpyFull, t_min, t_max)
395
+
396
+ def read_core(
397
+ self, t_min: float | None = None, t_max: float | None = None
398
+ ) -> SimResultsNumpyCore:
399
+ """Same as `read_full` but restricted to basic set of quantities
400
+
401
+ Arguments:
402
+ t_min: Optionally, only read samples at later times
403
+ t_max: Optionally, only read samples at earlier times
404
+
405
+ Returns:
406
+ `SimResultsNumpyCore` instance with data.
407
+ """
408
+ return self._read_datasets(SimResultsNumpyCore, t_min, t_max)
409
+
410
+ def as_stream_bundle(
411
+ self,
412
+ datasets: set[DatasetIdentifier] | None = None,
413
+ ranges: dict[DatasetIdentifier, tuple[int, int]] | None = None,
414
+ ) -> StreamBundle:
415
+ """Represent datasets in the file as `StreamBundle`
416
+
417
+ Arguments:
418
+ datasets: Set of quantities to include
419
+ ranges: Dictionary with optional entris restricting dataset index range
420
+
421
+ Returns:
422
+ StreamBundle with specified datasets as outputs.
423
+ """
424
+ if datasets is None:
425
+ datasets = self.dataset_identifier_set()
426
+ return instru_hdf5_file_as_stream_bundle(self._h5f, datasets, ranges=ranges)
@@ -245,22 +245,28 @@ def scet_wrt_tps_distant(
245
245
  def adjacent_carrier_fluctuations(
246
246
  local_carrier_fluctuations_: np.ndarray | float,
247
247
  backlink_noises_: np.ndarray | float,
248
+ reciprocal_backlink_noises_: np.ndarray | float,
248
249
  *,
249
250
  central_freq: float,
250
251
  ) -> np.ndarray | float:
251
252
  """Compute adjacent_carrier_fluctuations"""
252
- return local_carrier_fluctuations_ + central_freq * backlink_noises_
253
+ return local_carrier_fluctuations_ + central_freq * (
254
+ backlink_noises_ + reciprocal_backlink_noises_
255
+ )
253
256
 
254
257
 
255
258
  @stream_expression(np.float64)
256
259
  def adjacent_usb_fluctuations(
257
260
  adjacent_usb_fluctuations_: np.ndarray | float,
258
261
  backlink_noises_: np.ndarray | float,
262
+ reciprocal_backlink_noises_: np.ndarray | float,
259
263
  *,
260
264
  central_freq: float,
261
265
  ) -> np.ndarray | float:
262
266
  """Compute adjacent_usb_fluctuations"""
263
- return adjacent_usb_fluctuations_ + central_freq * backlink_noises_
267
+ return adjacent_usb_fluctuations_ + central_freq * (
268
+ backlink_noises_ + reciprocal_backlink_noises_
269
+ )
264
270
 
265
271
 
266
272
  @for_each_mosa
@@ -456,6 +456,7 @@ class ModelConstellation: # pylint: disable = too-few-public-methods
456
456
  mosa.value: compute.adjacent_usb_fluctuations(
457
457
  self.local_usb_fluctuations[mosa.adjacent.value],
458
458
  self.backlink_noises[mosa.value],
459
+ self.reciprocal_backlink_noises[mosa.sat.value],
459
460
  central_freq=cfg.central_freq,
460
461
  )
461
462
  for mosa in MosaID
@@ -1511,6 +1512,9 @@ class ModelConstellation: # pylint: disable = too-few-public-methods
1511
1512
  ## Backlink noise
1512
1513
 
1513
1514
  self.backlink_noises = self._generate_noise_mosas(noises.noise_def_backlink)
1515
+ self.reciprocal_backlink_noises = self._generate_noise_sats(
1516
+ noises.noise_def_reciprocal_backlink
1517
+ )
1514
1518
 
1515
1519
  ## Test-mass acceleration noise
1516
1520
 
@@ -1704,6 +1708,7 @@ class ModelConstellation: # pylint: disable = too-few-public-methods
1704
1708
  compute.adjacent_carrier_fluctuations(
1705
1709
  results.local_carrier_fluctuations[mosa.adjacent],
1706
1710
  self.backlink_noises[mosa.value],
1711
+ self.reciprocal_backlink_noises[mosa.sat.value],
1707
1712
  central_freq=cfg.central_freq,
1708
1713
  )
1709
1714
  )
@@ -1863,6 +1868,7 @@ class ModelConstellation: # pylint: disable = too-few-public-methods
1863
1868
  compute.adjacent_carrier_fluctuations(
1864
1869
  results.local_carrier_fluctuations[mosa.adjacent],
1865
1870
  self.backlink_noises[mosa.value],
1871
+ self.reciprocal_backlink_noises[mosa.sat.value],
1866
1872
  central_freq=cfg.central_freq,
1867
1873
  )
1868
1874
  )