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.
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/PKG-INFO +1 -1
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/__init__.py +1 -1
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/instru/__init__.py +4 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/instru/instru_defaults.py +36 -36
- lisainstrument-2.3.0/lisainstrument/instru/instru_file_reader.py +522 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/instru/instru_file_reader_v2_0_0.py +35 -0
- 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
- lisainstrument-2.3.0/lisainstrument/instru/instru_file_reader_v2_2_0.py +461 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/instru/instru_formulas.py +4 -6
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/instru/instru_model.py +55 -95
- lisainstrument-2.3.0/lisainstrument/instru/instru_model_config.py +87 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/instru/instru_orbsrc.py +28 -15
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/instru/instru_store.py +19 -4
- lisainstrument-2.3.0/lisainstrument/instru/instru_store_v2_1_0.py +939 -0
- lisainstrument-2.3.0/lisainstrument/instru/instru_store_v2_2_0.py +943 -0
- lisainstrument-2.3.0/lisainstrument/instru/instru_ttlsrc.py +120 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/instrument.py +57 -64
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/orbiting/constellation_enums.py +18 -9
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/orbiting/orbit_source_interp.py +40 -5
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/sigpro/__init__.py +1 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/sigpro/types_numpy.py +7 -0
- lisainstrument-2.3.0/lisainstrument/ttl/__init__.py +0 -0
- lisainstrument-2.3.0/lisainstrument/ttl/ttl_file_reader.py +39 -0
- lisainstrument-2.3.0/lisainstrument/ttl/ttl_file_writer.py +47 -0
- lisainstrument-2.3.0/lisainstrument/ttl/ttl_source.py +127 -0
- lisainstrument-2.3.0/lisainstrument/ttl/ttl_source_interp.py +214 -0
- lisainstrument-2.3.0/lisainstrument/ttl/ttl_source_static.py +87 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/pyproject.toml +1 -1
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/LICENSE +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/README.md +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/__main__.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/cli/__init__.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/cli/run_simulation.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/freqplan/__init__.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/freqplan/fplan_file.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/freqplan/fplan_source.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/freqplan/fplan_source_interp.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/glitches/__init__.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/glitches/glitch_file.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/glitches/glitch_source.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/glitches/glitch_source_interp.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/gwsource/__init__.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/gwsource/gw_file.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/gwsource/gw_source.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/gwsource/hdf5util.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/instru/instru_filter.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/instru/instru_fplan.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/instru/instru_glitchsrc.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/instru/instru_gwsrc.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/instru/instru_locking.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/instru/instru_noises.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/instru/instru_store_v2_0_0.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/legacy/__init__.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/legacy/containers.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/legacy/dsp.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/legacy/dynamic_delay_dsp.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/legacy/fixed_shift_dsp.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/legacy/hexagon.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/legacy/legacy_plots.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/legacy/noises.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/legacy/pyplnoise/LICENSE +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/legacy/pyplnoise/__init__.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/legacy/pyplnoise/pyplnoise.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/noisy/__init__.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/noisy/estimate_psd.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/noisy/noise_defs.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/noisy/noise_defs_lisa.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/noisy/noise_gen_numpy.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/orbiting/__init__.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/orbiting/orbit_file.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/orbiting/orbit_source.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/sigpro/adaptive_delay_numpy.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/sigpro/chunked_splines.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/sigpro/dynamic_delay_numpy.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/sigpro/fir_filters_numpy.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/sigpro/fixed_shift_numpy.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/sigpro/iir_filters_numpy.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/sigpro/regular_interpolators.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/sigpro/shift_inversion_numpy.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/__init__.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/analysis.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/array.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/delay.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/derivative.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/expression.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/firfilter.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/graph_util.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/hdf5_store.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/iirfilter.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/integrate.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/noise.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/noise_alt.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/null_store.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/numpy_store.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/sampling.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/scheduler.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/scheduler_dask.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/scheduler_serial.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/segments.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/shift_inv.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/store.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/streams.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/streams/time.py +0 -0
- {lisainstrument-2.2.0 → lisainstrument-2.3.0}/lisainstrument/version.py +0 -0
|
@@ -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
|
|
@@ -72,13 +72,13 @@ Default based on LISANode
|
|
|
72
72
|
"""
|
|
73
73
|
|
|
74
74
|
|
|
75
|
-
TTL_COEFFS_LOCAL_PHIS: Final[dict[
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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[
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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[
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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[
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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[
|
|
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
|
|
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[
|
|
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
|
|
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[
|
|
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
|
|
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[
|
|
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
|
|
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)
|