lisainstrument 2.2.0__tar.gz → 2.3.0__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 (104) hide show
  1. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/PKG-INFO +1 -1
  2. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/__init__.py +1 -1
  3. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/instru/__init__.py +4 -0
  4. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/instru/instru_defaults.py +36 -36
  5. lisainstrument-2.3.0/lisainstrument/instru/instru_file_reader.py +522 -0
  6. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/instru/instru_file_reader_v2_0_0.py +35 -0
  7. lisainstrument-2.2.0/lisainstrument/instru/instru_file_reader.py → lisainstrument-2.3.0/lisainstrument/instru/instru_file_reader_v2_1_0.py +46 -53
  8. lisainstrument-2.3.0/lisainstrument/instru/instru_file_reader_v2_2_0.py +461 -0
  9. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/instru/instru_formulas.py +4 -6
  10. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/instru/instru_model.py +55 -95
  11. lisainstrument-2.3.0/lisainstrument/instru/instru_model_config.py +87 -0
  12. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/instru/instru_orbsrc.py +28 -15
  13. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/instru/instru_store.py +19 -4
  14. lisainstrument-2.3.0/lisainstrument/instru/instru_store_v2_1_0.py +939 -0
  15. lisainstrument-2.3.0/lisainstrument/instru/instru_store_v2_2_0.py +943 -0
  16. lisainstrument-2.3.0/lisainstrument/instru/instru_ttlsrc.py +120 -0
  17. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/instrument.py +57 -64
  18. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/orbiting/constellation_enums.py +18 -9
  19. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/orbiting/orbit_source_interp.py +40 -5
  20. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/sigpro/__init__.py +1 -0
  21. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/sigpro/types_numpy.py +7 -0
  22. lisainstrument-2.3.0/lisainstrument/ttl/__init__.py +0 -0
  23. lisainstrument-2.3.0/lisainstrument/ttl/ttl_file_reader.py +39 -0
  24. lisainstrument-2.3.0/lisainstrument/ttl/ttl_file_writer.py +47 -0
  25. lisainstrument-2.3.0/lisainstrument/ttl/ttl_source.py +127 -0
  26. lisainstrument-2.3.0/lisainstrument/ttl/ttl_source_interp.py +214 -0
  27. lisainstrument-2.3.0/lisainstrument/ttl/ttl_source_static.py +87 -0
  28. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/pyproject.toml +1 -1
  29. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/LICENSE +0 -0
  30. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/README.md +0 -0
  31. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/__main__.py +0 -0
  32. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/cli/__init__.py +0 -0
  33. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/cli/run_simulation.py +0 -0
  34. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/freqplan/__init__.py +0 -0
  35. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/freqplan/fplan_file.py +0 -0
  36. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/freqplan/fplan_source.py +0 -0
  37. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/freqplan/fplan_source_interp.py +0 -0
  38. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/glitches/__init__.py +0 -0
  39. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/glitches/glitch_file.py +0 -0
  40. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/glitches/glitch_source.py +0 -0
  41. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/glitches/glitch_source_interp.py +0 -0
  42. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/gwsource/__init__.py +0 -0
  43. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/gwsource/gw_file.py +0 -0
  44. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/gwsource/gw_source.py +0 -0
  45. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/gwsource/hdf5util.py +0 -0
  46. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/instru/instru_filter.py +0 -0
  47. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/instru/instru_fplan.py +0 -0
  48. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/instru/instru_glitchsrc.py +0 -0
  49. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/instru/instru_gwsrc.py +0 -0
  50. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/instru/instru_locking.py +0 -0
  51. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/instru/instru_noises.py +0 -0
  52. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/instru/instru_store_v2_0_0.py +0 -0
  53. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/legacy/__init__.py +0 -0
  54. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/legacy/containers.py +0 -0
  55. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/legacy/dsp.py +0 -0
  56. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/legacy/dynamic_delay_dsp.py +0 -0
  57. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/legacy/fixed_shift_dsp.py +0 -0
  58. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/legacy/hexagon.py +0 -0
  59. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/legacy/legacy_plots.py +0 -0
  60. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/legacy/noises.py +0 -0
  61. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/legacy/pyplnoise/LICENSE +0 -0
  62. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/legacy/pyplnoise/__init__.py +0 -0
  63. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/legacy/pyplnoise/pyplnoise.py +0 -0
  64. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/noisy/__init__.py +0 -0
  65. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/noisy/estimate_psd.py +0 -0
  66. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/noisy/noise_defs.py +0 -0
  67. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/noisy/noise_defs_lisa.py +0 -0
  68. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/noisy/noise_gen_numpy.py +0 -0
  69. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/orbiting/__init__.py +0 -0
  70. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/orbiting/orbit_file.py +0 -0
  71. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/orbiting/orbit_source.py +0 -0
  72. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/sigpro/adaptive_delay_numpy.py +0 -0
  73. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/sigpro/chunked_splines.py +0 -0
  74. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/sigpro/dynamic_delay_numpy.py +0 -0
  75. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/sigpro/fir_filters_numpy.py +0 -0
  76. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/sigpro/fixed_shift_numpy.py +0 -0
  77. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/sigpro/iir_filters_numpy.py +0 -0
  78. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/sigpro/regular_interpolators.py +0 -0
  79. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/sigpro/shift_inversion_numpy.py +0 -0
  80. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/__init__.py +0 -0
  81. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/analysis.py +0 -0
  82. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/array.py +0 -0
  83. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/delay.py +0 -0
  84. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/derivative.py +0 -0
  85. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/expression.py +0 -0
  86. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/firfilter.py +0 -0
  87. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/graph_util.py +0 -0
  88. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/hdf5_store.py +0 -0
  89. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/iirfilter.py +0 -0
  90. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/integrate.py +0 -0
  91. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/noise.py +0 -0
  92. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/noise_alt.py +0 -0
  93. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/null_store.py +0 -0
  94. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/numpy_store.py +0 -0
  95. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/sampling.py +0 -0
  96. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/scheduler.py +0 -0
  97. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/scheduler_dask.py +0 -0
  98. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/scheduler_serial.py +0 -0
  99. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/segments.py +0 -0
  100. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/shift_inv.py +0 -0
  101. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/store.py +0 -0
  102. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/streams.py +0 -0
  103. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/time.py +0 -0
  104. {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: lisainstrument
3
- Version: 2.2.0
3
+ Version: 2.3.0
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
@@ -2,7 +2,7 @@
2
2
 
3
3
  from .glitches import glitch_file
4
4
  from .gwsource import gw_file
5
- from .instru import SimResultFile, sim_result_file
5
+ from .instru import SimResultFile, sim_result_file, ttl_source_from_metadata
6
6
  from .instrument import Instrument
7
7
  from .orbiting import MosaID, SatID, orbit_file
8
8
  from .streams import SchedulerConfigParallel, SchedulerConfigSerial
@@ -19,3 +19,7 @@ from lisainstrument.instru.instru_store import (
19
19
  SimResultsNumpyFull,
20
20
  store_instru_hdf5,
21
21
  )
22
+ from lisainstrument.instru.instru_ttlsrc import (
23
+ init_ttl_source,
24
+ ttl_source_from_metadata,
25
+ )
@@ -72,13 +72,13 @@ Default based on LISANode
72
72
  """
73
73
 
74
74
 
75
- TTL_COEFFS_LOCAL_PHIS: Final[dict[str, float]] = {
76
- "12": 2.005835e-03,
77
- "23": 2.105403e-04,
78
- "31": -1.815399e-03,
79
- "13": -2.865050e-04,
80
- "32": -1.986657e-03,
81
- "21": 9.368319e-04,
75
+ TTL_COEFFS_LOCAL_PHIS: Final[dict[MosaID, float]] = {
76
+ MosaID.MOSA_12: 2.005835e-03,
77
+ MosaID.MOSA_23: 2.105403e-04,
78
+ MosaID.MOSA_31: -1.815399e-03,
79
+ MosaID.MOSA_13: -2.865050e-04,
80
+ MosaID.MOSA_32: -1.986657e-03,
81
+ MosaID.MOSA_21: 9.368319e-04,
82
82
  }
83
83
  """Value to use if ttl_coeffs == "default"
84
84
 
@@ -86,39 +86,39 @@ Default values were drawn from the same distributions used for ttl_coeffs == "ra
86
86
  """
87
87
 
88
88
 
89
- TTL_COEFFS_DISTANT_PHIS: Final[dict[str, float]] = {
90
- "12": 1.623910e-03,
91
- "23": 1.522873e-04,
92
- "31": -1.842871e-03,
93
- "13": -2.091585e-03,
94
- "32": 1.300866e-03,
95
- "21": -8.445374e-04,
89
+ TTL_COEFFS_DISTANT_PHIS: Final[dict[MosaID, float]] = {
90
+ MosaID.MOSA_12: 1.623910e-03,
91
+ MosaID.MOSA_23: 1.522873e-04,
92
+ MosaID.MOSA_31: -1.842871e-03,
93
+ MosaID.MOSA_13: -2.091585e-03,
94
+ MosaID.MOSA_32: 1.300866e-03,
95
+ MosaID.MOSA_21: -8.445374e-04,
96
96
  }
97
97
  """Value to use if ttl_coeffs == "default"
98
98
 
99
99
  Default values were drawn from the same distributions used for ttl_coeffs == "random"
100
100
  """
101
101
 
102
- TTL_COEFFS_LOCAL_ETAS: Final[dict[str, float]] = {
103
- "12": -1.670389e-03,
104
- "23": 1.460681e-03,
105
- "31": -1.039064e-03,
106
- "13": 1.640473e-04,
107
- "32": 1.205353e-03,
108
- "21": -9.205764e-04,
102
+ TTL_COEFFS_LOCAL_ETAS: Final[dict[MosaID, float]] = {
103
+ MosaID.MOSA_12: -1.670389e-03,
104
+ MosaID.MOSA_23: 1.460681e-03,
105
+ MosaID.MOSA_31: -1.039064e-03,
106
+ MosaID.MOSA_13: 1.640473e-04,
107
+ MosaID.MOSA_32: 1.205353e-03,
108
+ MosaID.MOSA_21: -9.205764e-04,
109
109
  }
110
110
  """Value to use if ttl_coeffs == "default"
111
111
 
112
112
  Default values were drawn from the same distributions used for ttl_coeffs == "random"
113
113
  """
114
114
 
115
- TTL_COEFFS_DISTANT_ETAS: Final[dict[str, float]] = {
116
- "12": -1.076470e-03,
117
- "23": 5.228848e-04,
118
- "31": -5.662766e-05,
119
- "13": 1.960050e-03,
120
- "32": 9.021890e-04,
121
- "21": 1.908239e-03,
115
+ TTL_COEFFS_DISTANT_ETAS: Final[dict[MosaID, float]] = {
116
+ MosaID.MOSA_12: -1.076470e-03,
117
+ MosaID.MOSA_23: 5.228848e-04,
118
+ MosaID.MOSA_31: -5.662766e-05,
119
+ MosaID.MOSA_13: 1.960050e-03,
120
+ MosaID.MOSA_32: 9.021890e-04,
121
+ MosaID.MOSA_21: 1.908239e-03,
122
122
  }
123
123
  """Value to use if ttl_coeffs == "default"
124
124
 
@@ -165,21 +165,21 @@ Those default PPRs are based on first samples of Keplerian orbits (v2.0.dev)
165
165
  """
166
166
 
167
167
 
168
- def random_ttl_coeffs_local_phis(low=-2.2e-3, high=2.2e-3) -> dict[str, float]:
168
+ def random_ttl_coeffs_local_phis(low=-2.2e-3, high=2.2e-3) -> dict[MosaID, float]:
169
169
  """Generate random value to use if ttl_coeffs == "random" """
170
- return {m.value: np.random.uniform(low, high) for m in MosaID}
170
+ return {m: np.random.uniform(low, high) for m in MosaID}
171
171
 
172
172
 
173
- def random_ttl_coeffs_distant_phis(low=-2.4e-3, high=2.4e-3) -> dict[str, float]:
173
+ def random_ttl_coeffs_distant_phis(low=-2.4e-3, high=2.4e-3) -> dict[MosaID, float]:
174
174
  """Generate random value to use if ttl_coeffs == "random" """
175
- return {m.value: np.random.uniform(low, high) for m in MosaID}
175
+ return {m: np.random.uniform(low, high) for m in MosaID}
176
176
 
177
177
 
178
- def random_ttl_coeffs_local_etas(low=-2.2e-3, high=2.2e-3) -> dict[str, float]:
178
+ def random_ttl_coeffs_local_etas(low=-2.2e-3, high=2.2e-3) -> dict[MosaID, float]:
179
179
  """Generate random value to use if ttl_coeffs == "random" """
180
- return {m.value: np.random.uniform(low, high) for m in MosaID}
180
+ return {m: np.random.uniform(low, high) for m in MosaID}
181
181
 
182
182
 
183
- def random_ttl_coeffs_distant_etas(low=-2.4e-3, high=2.4e-3) -> dict[str, float]:
183
+ def random_ttl_coeffs_distant_etas(low=-2.4e-3, high=2.4e-3) -> dict[MosaID, float]:
184
184
  """Generate random value to use if ttl_coeffs == "random" """
185
- return {m.value: np.random.uniform(low, high) for m in MosaID}
185
+ return {m: np.random.uniform(low, high) for m in MosaID}
@@ -0,0 +1,522 @@
1
+ """The instru.instru_file_reader module allows reading data from simulation result files.
2
+
3
+
4
+ This module is not required by the simulator but supports using simulation
5
+ results in post-processing. The SimResultFile is a low-level representation of
6
+ a simulation result file, allowing direct access to datasets without having to
7
+ know the internal file format. It also provides a view of the simulation data
8
+ as a StreamBundle, allowing chunked processing of saved simulation results.
9
+ Besides chunked processing, it also allows reading all data into memory at once,
10
+ collected in a SimResultsNumpyCore or SimResultsNumpyFull instance.
11
+
12
+ For reading back instrument result files, one should normally use the function
13
+ `lisainstrument.instru.sim_result_file` since it additionally supports loading
14
+ file formats created by previous versions.
15
+ """
16
+
17
+ import json
18
+ import pathlib
19
+ from collections import defaultdict
20
+ from typing import Any, Final, TypeAlias, TypeVar
21
+
22
+ import h5py
23
+ import numpy as np
24
+ from packaging.specifiers import SpecifierSet
25
+ from packaging.version import Version
26
+
27
+ from lisainstrument.instru.instru_file_reader_v2_0_0 import (
28
+ SimResultFile as SimResultFile_V2_0_0,
29
+ )
30
+ from lisainstrument.instru.instru_file_reader_v2_1_0 import (
31
+ SimResultFile as SimResultFile_V2_1_0,
32
+ )
33
+ from lisainstrument.instru.instru_file_reader_v2_2_0 import (
34
+ SimResultFile as SimResultFile_V2_2_0,
35
+ )
36
+ from lisainstrument.instru.instru_store import (
37
+ IdxSpace,
38
+ SimFullDatasetsMOSA,
39
+ SimFullDatasetsSat,
40
+ SimMetaData,
41
+ SimResultsNumpyCore,
42
+ SimResultsNumpyFull,
43
+ datasets_metadata_dict,
44
+ make_dataset_id,
45
+ )
46
+ from lisainstrument.orbiting.constellation_enums import MosaID, SatID
47
+ from lisainstrument.streams import DatasetIdentifier, StreamBundle
48
+ from lisainstrument.streams.hdf5_store import (
49
+ DataStorageHDF5,
50
+ instru_hdf5_file_as_stream_bundle,
51
+ )
52
+ from lisainstrument.streams.numpy_store import store_bundle_numpy
53
+ from lisainstrument.version import __version__
54
+
55
+ _T = TypeVar("_T", SimResultsNumpyFull, SimResultsNumpyCore)
56
+
57
+
58
+ def _unique_index_range(ranges: list[tuple[int, int]]) -> tuple[int, int]:
59
+ """Ensure index ranges in list are identical and return that range"""
60
+ s = set(ranges)
61
+ if len(s) != 1:
62
+ msg = f"unique_index_range: ranges not identical ({ranges=})"
63
+ raise RuntimeError(msg)
64
+ (uni,) = list(s)
65
+ return uni
66
+
67
+
68
+ class SimResultFile:
69
+ """Represents a simulation result file in HDF5 format.
70
+
71
+ There are low-level methods for direct reading of datasets identified either
72
+ by name and MOSA or SC index, or by a DatasetIdentifier. For use cases where the
73
+ results fit into memory, there are methods to create a SimResultsNumpyCore or
74
+ SimResultsNumpyFull instance with the data and metadata.
75
+
76
+ For use cases dealing with large data sets, there is a method representing
77
+ the datasets as streams in a StreamBundle for use with chunked processing.
78
+ For all those methods, it is possible to restrict the data to a given time
79
+ interval.
80
+ """
81
+
82
+ def __init__(self, path: pathlib.Path | str):
83
+ """Constructor
84
+
85
+ Arguments:
86
+ path: Path of the a HDF5 file created by store_instru_hdf5
87
+ """
88
+ self._h5f_raw = h5py.File(str(path), "r")
89
+
90
+ try:
91
+
92
+ self._version: Final = Version(self._h5f.attrs["version_format"])
93
+
94
+ ds_actual = set(self._h5f.keys()) - {"debug"}
95
+ ds_debug = set(self._h5f["debug"].keys())
96
+
97
+ md = json.loads(self._h5f.attrs["metadata_json"])
98
+
99
+ self._description = str(self._h5f.attrs.get("description", None))
100
+
101
+ except:
102
+ self.close()
103
+ raise
104
+
105
+ if not SimResultFile.format_specifier().contains(self._version):
106
+ msg = f"File {path} version {self._version} not compatible with file reader"
107
+ raise RuntimeError(msg)
108
+
109
+ self._metadata = SimMetaData(**md)
110
+
111
+ ds_all = ds_actual | ds_debug
112
+ if ds_all == SimResultsNumpyFull.all_dataset_names:
113
+ self._extended = True
114
+ elif ds_all == SimResultsNumpyCore.all_dataset_names:
115
+ self._extended = False
116
+ else:
117
+ msg = f"File {path} contains invalid set of quantities"
118
+ raise RuntimeError(msg)
119
+
120
+ self._t0 = self._metadata.t0
121
+ self._sample_periods = {
122
+ IdxSpace.PHYSICS: self._metadata.physics_dt,
123
+ IdxSpace.PHYSICS_EXT: self._metadata.physics_dt,
124
+ IdxSpace.REGULAR: self._metadata.dt,
125
+ IdxSpace.TELEMETRY: self._metadata.telemetry_dt,
126
+ }
127
+
128
+ categories = datasets_metadata_dict()
129
+
130
+ isps: dict[DatasetIdentifier, IdxSpace] = {}
131
+ ranges: dict[IdxSpace, list[tuple[int, int]]] = defaultdict(list)
132
+ for dsid in self.dataset_identifier_set():
133
+ n = dsid[-2]
134
+ cat = categories[n]
135
+ rg = self._read_range(dsid)
136
+ ranges[cat.idxspace].append(rg)
137
+ isps[dsid] = cat.idxspace
138
+ self._isp_by_dsid: Final = isps
139
+ self._range_by_isp: Final = {
140
+ i: _unique_index_range(r) for i, r in ranges.items()
141
+ }
142
+
143
+ @property
144
+ def _h5f(self) -> h5py.File:
145
+ if self._h5f_raw is None:
146
+ msg = "SimResultFile used after closing it"
147
+ raise RuntimeError(msg)
148
+ return self._h5f_raw
149
+
150
+ def close(self) -> None:
151
+ """Close file
152
+
153
+ Note the interface provides a context manager, consider using `with`
154
+ mechanism instead
155
+ """
156
+ if self._h5f_raw is not None:
157
+ self._h5f_raw.close()
158
+ self._h5f_raw = None
159
+
160
+ def __del__(self):
161
+ """HDF5 file is automatically closed"""
162
+ self.close()
163
+
164
+ def __enter__(self):
165
+ """Enter context"""
166
+ return self
167
+
168
+ def __exit__(self, exc_type, exc_val, exc_tb):
169
+ """Exit context"""
170
+ self.close()
171
+
172
+ @property
173
+ def format_version(self) -> Version:
174
+ """Version number of the file format"""
175
+ return self._version
176
+
177
+ @staticmethod
178
+ def format_specifier() -> SpecifierSet:
179
+ """Version specifier set compatible with file reader"""
180
+ v = Version(__version__)
181
+ return SpecifierSet(f"=={v.major}.{v.minor}.*")
182
+
183
+ @classmethod
184
+ def check_file_version(cls, path: str | pathlib.Path) -> tuple[bool, Version]:
185
+ """Test if file version is compatible with file reader class
186
+
187
+ Arguments:
188
+ path: Path of file to check
189
+
190
+ Returns:
191
+ Whether file can be read and the file format version
192
+ """
193
+ with h5py.File(str(path), "r") as gwf:
194
+ version = Version(gwf.attrs["version_format"])
195
+ return cls.format_specifier().contains(version), version
196
+
197
+ @property
198
+ def is_extended(self) -> bool:
199
+ """Whether file contains extended set of quantities"""
200
+ return self._extended
201
+
202
+ @property
203
+ def metadata(self) -> SimMetaData:
204
+ """Simulation metadata"""
205
+ return self._metadata
206
+
207
+ @property
208
+ def description(self) -> str | None:
209
+ """Description text"""
210
+ return self._description
211
+
212
+ def dataset_identifier_set(self) -> set[DatasetIdentifier]:
213
+ """Set of all available dataset identifiers"""
214
+ if self.is_extended:
215
+ return SimResultsNumpyFull.dataset_identifier_set()
216
+ return SimResultsNumpyCore.dataset_identifier_set()
217
+
218
+ def idxspace_by_dataset_id(self, dsid: DatasetIdentifier) -> IdxSpace:
219
+ """Get index space for dataset"""
220
+ return self._isp_by_dsid[dsid]
221
+
222
+ def range_by_idxspace(self, isp: IdxSpace) -> tuple[int, int]:
223
+ """Get range for given index space"""
224
+ return self._range_by_isp[isp]
225
+
226
+ def range_by_dataset_id(self, dsid: DatasetIdentifier) -> tuple[int, int]:
227
+ """Get range for given dataset"""
228
+ return self.range_by_idxspace(self.idxspace_by_dataset_id(dsid))
229
+
230
+ def dt_by_idxspace(self, isp: IdxSpace) -> float:
231
+ """Get sample period for given index space"""
232
+ return self._sample_periods[isp]
233
+
234
+ def dt_by_dataset_id(self, dsid: DatasetIdentifier) -> float:
235
+ """Get sample period for given dataset"""
236
+ return self.dt_by_idxspace(self.idxspace_by_dataset_id(dsid))
237
+
238
+ def read_by_datset_id(
239
+ self,
240
+ dsid: DatasetIdentifier,
241
+ istart: int | None = None,
242
+ istop: int | None = None,
243
+ ) -> np.ndarray:
244
+ """Read data identified by a `DatasetIdentifier`
245
+
246
+ Optionally, one can restrict the index range. This refers to the logical
247
+ index range given returned `range_by_dataset_id()`, not necessarily starting
248
+ at zero. The returned data will contain indices `istart <= i < istop`
249
+
250
+ Arguments:
251
+ dsid: `DatasetIdentifier` specifying dataset
252
+ istart: Optionally, exclude lower indices
253
+ istop: Optionally, first index to exclude
254
+
255
+ Returns:
256
+ 1D numpy array with data
257
+ """
258
+ dspth = "/".join(dsid)
259
+ ds: h5py.Dataset = self._h5f[dspth]
260
+ aistart, aistop = self.range_by_dataset_id(dsid)
261
+ istart = aistart if istart is None else int(istart)
262
+ istop = aistop if istop is None else int(istop)
263
+ if not aistart <= istart <= istop <= aistop:
264
+ msg = (
265
+ f"SimResultFile: index range {istart}, {istop} not "
266
+ f"available for dataset {dspth}"
267
+ )
268
+ raise RuntimeError(msg)
269
+ if len(ds.shape) == 0: # pylint: disable = no-member
270
+ dat = np.empty(
271
+ istop - istart, dtype=ds.dtype # pylint: disable = no-member
272
+ )
273
+ dat[:] = ds[()]
274
+ return dat
275
+ return ds[istart - aistart : istop - aistart]
276
+
277
+ def read_by_name_and_mosa(
278
+ self,
279
+ name: str,
280
+ mosa: MosaID | str,
281
+ istart: int | None = None,
282
+ istop: int | None = None,
283
+ ) -> np.ndarray:
284
+ """Like `read_by_datset_id` but dataset is specified by name and `MosaID`
285
+
286
+ Arguments:
287
+ name: Dataset name
288
+ mosa: Read dataset for MOSA specified by `MosaID` or MOSA name
289
+ istart: Optionally, exclude lower indices
290
+ istop: Optionally, first index to exclude
291
+
292
+ Returns:
293
+ 1D numpy array with data
294
+ """
295
+
296
+ if name not in SimFullDatasetsMOSA.dataset_names():
297
+ msg = f"SimResultFile: invalid per-MOSA dataset {name}"
298
+ raise RuntimeError(msg)
299
+ cat = SimFullDatasetsMOSA.dataset_metadata()[name]
300
+ dsid = make_dataset_id(cat.actual, name, MosaID(mosa).value)
301
+ return self.read_by_datset_id(dsid, istart, istop)
302
+
303
+ def read_by_name_and_sat(
304
+ self,
305
+ name: str,
306
+ sc: SatID | str,
307
+ istart: int | None = None,
308
+ istop: int | None = None,
309
+ ) -> np.ndarray:
310
+ """Like `read_by_datset_id` but dataset is specified by name and `SatID`
311
+
312
+ Arguments:
313
+ name: Dataset name
314
+ sc: Read dataset for spacecraft specified by `SatID` or spacecraft name
315
+ istart: Optionally, exclude lower indices
316
+ istop: Optionally, first index to exclude
317
+
318
+ Returns:
319
+ 1D numpy array with data
320
+ """
321
+ if name not in SimFullDatasetsSat.dataset_names():
322
+ msg = f"SimResultFile: invalid per-spacecraft dataset {name}"
323
+ raise RuntimeError(msg)
324
+ cat = SimFullDatasetsSat.dataset_metadata()[name]
325
+ dsid = make_dataset_id(cat.actual, name, SatID(sc).value)
326
+ return self.read_by_datset_id(dsid, istart, istop)
327
+
328
+ def _read_range(self, dsid: DatasetIdentifier) -> tuple[int, int]:
329
+ """Get the index range available for a given dataset
330
+
331
+ The available indices `i` are in the range `istart <= i < istop`.
332
+
333
+ Arguments:
334
+ dsid: `DatasetIdentifier` specifying the dataset
335
+
336
+ Returns:
337
+ Tuple `(istart, istop)`
338
+ """
339
+ dspth = "/".join(dsid)
340
+ ds: h5py.Dataset = self._h5f[dspth]
341
+ istart = int(ds.attrs[DataStorageHDF5.attr_name_index_start])
342
+ istop = int(ds.attrs[DataStorageHDF5.attr_name_index_stop])
343
+ return (istart, istop)
344
+
345
+ def _restrict_range_isp(
346
+ self,
347
+ isp: IdxSpace,
348
+ t_min: float | None = None,
349
+ t_max: float | None = None,
350
+ ) -> tuple[int, int]:
351
+ """Compute index range within given time interval for a given dataset"""
352
+ dt = self.dt_by_idxspace(isp)
353
+ istart, istop = self.range_by_idxspace(isp)
354
+ if t_min is None:
355
+ i0 = istart
356
+ else:
357
+ i0 = int(np.ceil((t_min - self._t0) / dt))
358
+
359
+ if t_max is None:
360
+ i1 = istop
361
+ else:
362
+ i1 = int(np.ceil((t_max - self._t0) / dt))
363
+
364
+ return i0, i1
365
+
366
+ def _restrict_ranges(
367
+ self,
368
+ t_min: float | None = None,
369
+ t_max: float | None = None,
370
+ ) -> dict[IdxSpace, tuple[int, int]]:
371
+ """Compute index ranges within given time interval for datasets"""
372
+ return {
373
+ isp: self._restrict_range_isp(isp, t_min, t_max)
374
+ for isp in self._range_by_isp
375
+ }
376
+
377
+ def _read_datasets(
378
+ self,
379
+ cls: type[_T],
380
+ t_min: float | None = None,
381
+ t_max: float | None = None,
382
+ ) -> _T:
383
+ """Read datasets into a `DataStorageNumpy` instance"""
384
+ datasets = cls.dataset_identifier_set()
385
+ ranges_isp = self._restrict_ranges(t_min, t_max)
386
+ ranges_dsid = {
387
+ dsid: ranges_isp[self.idxspace_by_dataset_id(dsid)] for dsid in datasets
388
+ }
389
+
390
+ stb = self.as_stream_bundle(datasets, ranges_dsid)
391
+ store = store_bundle_numpy(stb)
392
+ return cls(store.as_dict(), ranges_isp, self.metadata.asdict())
393
+
394
+ def read_full(
395
+ self, t_min: float | None = None, t_max: float | None = None
396
+ ) -> SimResultsNumpyFull:
397
+ """Read extended set of quantities into memory as `SimResultsNumpyFull` instance
398
+
399
+ If the file does not contain the extended set, an RuntimeError is raised.
400
+
401
+ Optionally, on can restrict the time range for which the data samples are
402
+ read. This does not change the index space, i.e. which indices refer to which
403
+ times. It only changes the index range of the datasets, available through
404
+ the `sat_ranges` and `mosa_ranges` attributes of `SimResultsNumpyFull`.
405
+
406
+ Arguments:
407
+ t_min: Optionally, only read samples at later times
408
+ t_max: Optionally, only read samples at earlier times
409
+
410
+ Returns:
411
+ `SimResultsNumpyFull` instance with data.
412
+ """
413
+
414
+ if not self.is_extended:
415
+ msg = "SimResultFile: cannot read extended results from basic result file"
416
+ raise RuntimeError(msg)
417
+ return self._read_datasets(SimResultsNumpyFull, t_min, t_max)
418
+
419
+ def read_core(
420
+ self, t_min: float | None = None, t_max: float | None = None
421
+ ) -> SimResultsNumpyCore:
422
+ """Same as `read_full` but restricted to basic set of quantities
423
+
424
+ Arguments:
425
+ t_min: Optionally, only read samples at later times
426
+ t_max: Optionally, only read samples at earlier times
427
+
428
+ Returns:
429
+ `SimResultsNumpyCore` instance with data.
430
+ """
431
+ return self._read_datasets(SimResultsNumpyCore, t_min, t_max)
432
+
433
+ def time_window_to_index_range(
434
+ self,
435
+ dsid: DatasetIdentifier,
436
+ t_min: float | None = None,
437
+ t_max: float | None = None,
438
+ ) -> tuple[int, int]:
439
+ """Return the index range contained within a given time period
440
+
441
+ For the time grid of a given dataset, the given time interval is
442
+ mapped to an equivalent index range. More precisely, this returns
443
+ indices `i0,i1` such that sample times `t_i` for indices in the range
444
+ `i0 <= i < i1` satisfy `t_min<=t_i<t_max`.
445
+
446
+ Beware of rounding errors for the case that `t_min` or `t_max` fall
447
+ onto a sample time. For application where precise control matters,
448
+ best use time intervals starting and ending between sample times.
449
+ Alternatively, one can re-use the same floating point number as
450
+ `t_max` in one call and `t_min` in the next. Given
451
+ ´i0,i1 = time_window_to_index_range(dsid, t_a, t_b)´ and
452
+ ´i2,i3 = time_window_to_index_range(dsid, t_b, t_c)´,
453
+ it is safe to assume that ´i2==i1`. Hence, the intervals
454
+ `i0 <= i < i1` and `i2 <= i < i3` have no overlap and cover the
455
+ range `i0 <= i < i3` without gap.
456
+
457
+ The default for `t_min` is such that `i0` is the first valid index
458
+ of the dataset. The default for `t_max` results in `i1` being the
459
+ last valid index + 1.
460
+
461
+ Arguments:
462
+ t_min: Left boundary, or None for start of dataset
463
+ t_max: Right boundary, or None for end of dataset
464
+ """
465
+ isp = self.idxspace_by_dataset_id(dsid)
466
+ return self._restrict_range_isp(isp, t_min, t_max)
467
+
468
+ def as_stream_bundle(
469
+ self,
470
+ datasets: set[DatasetIdentifier] | None = None,
471
+ ranges: dict[DatasetIdentifier, tuple[int, int]] | None = None,
472
+ ) -> StreamBundle:
473
+ """Represent datasets in the file as `StreamBundle`
474
+
475
+ Arguments:
476
+ datasets: Set of quantities to include
477
+ ranges: Dictionary with optional entris restricting dataset index range
478
+
479
+ Returns:
480
+ StreamBundle with specified datasets as outputs.
481
+ """
482
+ if datasets is None:
483
+ datasets = self.dataset_identifier_set()
484
+ return instru_hdf5_file_as_stream_bundle(self._h5f, datasets, ranges=ranges)
485
+
486
+
487
+ SimResultFileTypes: TypeAlias = (
488
+ SimResultFile | SimResultFile_V2_0_0 | SimResultFile_V2_1_0 | SimResultFile_V2_2_0
489
+ )
490
+
491
+
492
+ def sim_result_file(
493
+ path: pathlib.Path | str, only_compatible: bool = False
494
+ ) -> SimResultFileTypes:
495
+ """Open simulation result file
496
+
497
+ This provides an interface with methods for reading the file.
498
+ The returned interface depends on the file format version. To require
499
+ the interface for the current version, use the option `only_compatible=True`.
500
+ In this case, trying to read a file that cannot be represented through
501
+ the current interface raises an exception.
502
+
503
+ Arguments:
504
+ path: Path of simulation result file
505
+ only_compatible: If False (default) allow reading old formats
506
+
507
+ Returns:
508
+ SimResultFile instance for reading file data
509
+ """
510
+
511
+ legacy = [SimResultFile_V2_0_0, SimResultFile_V2_1_0, SimResultFile_V2_2_0]
512
+ readers: list[Any] = [] if only_compatible else legacy
513
+ readers.append(SimResultFile)
514
+
515
+ version: Version
516
+ for reader in readers:
517
+ compatible, version = reader.check_file_version(path)
518
+ if compatible:
519
+ return reader(path)
520
+
521
+ msg = f"Unsupported simulation result file version '{version}'"
522
+ raise RuntimeError(msg)