gwmock-data 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gwmock_data/__init__.py +13 -0
- gwmock_data/_optional.py +35 -0
- gwmock_data/io/__init__.py +7 -0
- gwmock_data/io/hdf5.py +88 -0
- gwmock_data/sampling_grid.py +167 -0
- gwmock_data/timeseries.py +428 -0
- gwmock_data/version.py +11 -0
- gwmock_data-0.1.0.dist-info/METADATA +59 -0
- gwmock_data-0.1.0.dist-info/RECORD +11 -0
- gwmock_data-0.1.0.dist-info/WHEEL +4 -0
- gwmock_data-0.1.0.dist-info/licenses/LICENSE +26 -0
gwmock_data/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Top-level package for gwmock_data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
# PEP 810: defer importlib.metadata (loaded by version.py) on 3.15+.
|
|
6
|
+
# Ignored on 3.13/3.14 where the lazy import machinery does not exist.
|
|
7
|
+
__lazy_modules__ = ["gwmock_data.sampling_grid", "gwmock_data.timeseries", "gwmock_data.version"]
|
|
8
|
+
|
|
9
|
+
from gwmock_data.sampling_grid import SamplingGrid
|
|
10
|
+
from gwmock_data.timeseries import TimeSeries, as_timeseries
|
|
11
|
+
from gwmock_data.version import __version__
|
|
12
|
+
|
|
13
|
+
__all__ = ["SamplingGrid", "TimeSeries", "__version__", "as_timeseries"]
|
gwmock_data/_optional.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Import an optional package, naming the extra that installs it when it is missing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
from types import ModuleType
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def require(module: str, extra: str, feature: str) -> ModuleType:
|
|
10
|
+
"""Return ``module``, importing it; say which extra installs it if it is missing.
|
|
11
|
+
|
|
12
|
+
Only ``module`` itself missing, or a package it sits in, is reported that way. Any other
|
|
13
|
+
missing module -- one it needs, or a part of an installed package that fails to import -- is
|
|
14
|
+
re-raised unchanged, since installing the extra would not help.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
module: Dotted module name, such as ``"gwpy.timeseries"``.
|
|
18
|
+
extra: The ``gwmock-data`` extra that installs it, such as ``"gwpy"``.
|
|
19
|
+
feature: What needs it, for the message, such as ``"Frame (GWF) output"``.
|
|
20
|
+
|
|
21
|
+
Raises:
|
|
22
|
+
ModuleNotFoundError: If the package is not installed.
|
|
23
|
+
"""
|
|
24
|
+
try:
|
|
25
|
+
return importlib.import_module(module)
|
|
26
|
+
except ModuleNotFoundError as exc:
|
|
27
|
+
parts = module.split(".")
|
|
28
|
+
enclosing = {".".join(parts[: index + 1]) for index in range(len(parts))}
|
|
29
|
+
if exc.name not in enclosing:
|
|
30
|
+
raise
|
|
31
|
+
root = parts[0]
|
|
32
|
+
raise ModuleNotFoundError(
|
|
33
|
+
f"{feature} needs {root}, which is not installed. Install it with: pip install 'gwmock-data[{extra}]'",
|
|
34
|
+
name=exc.name,
|
|
35
|
+
) from exc
|
gwmock_data/io/hdf5.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Read and write time series as HDF5, in the layout GWpy's ``TimeSeriesDict`` uses.
|
|
2
|
+
|
|
3
|
+
One dataset per series, named by its key and gzip-compressed, with the attributes GWpy writes and
|
|
4
|
+
reads: ``x0`` (the GPS time of the first sample), ``dx`` (the sample spacing, in seconds),
|
|
5
|
+
``xunit`` (``"s"``), ``unit`` and, for a series that has one, ``name``. A file written here therefore loads with
|
|
6
|
+
``gwpy.timeseries.TimeSeriesDict.read(path, format="hdf5")``, and a file GWpy wrote loads here.
|
|
7
|
+
|
|
8
|
+
Only ``h5py`` is needed, imported when a function is called; GWpy is not.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections.abc import Mapping
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
from gwmock_data._optional import require
|
|
19
|
+
from gwmock_data.timeseries import TimeSeries, as_timeseries
|
|
20
|
+
|
|
21
|
+
__all__ = ["read_hdf5", "write_hdf5"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def write_hdf5(series: Mapping[str, TimeSeries], path: str | Path) -> None:
|
|
25
|
+
"""Write each series to ``path`` as one HDF5 dataset named by its key, replacing the file.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
series: Mapping of dataset name to a one-dimensional time series (this package's or
|
|
29
|
+
GWpy's).
|
|
30
|
+
path: Output file.
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
ValueError: If a series is not one-dimensional.
|
|
34
|
+
ModuleNotFoundError: If ``h5py`` is not installed.
|
|
35
|
+
"""
|
|
36
|
+
h5py = require("h5py", "hdf5", "HDF5 output")
|
|
37
|
+
channels = {key: as_timeseries(value) for key, value in series.items()}
|
|
38
|
+
for key, channel in channels.items():
|
|
39
|
+
if len(channel.data.shape) != 1:
|
|
40
|
+
raise ValueError(f"{key!r} must be one-dimensional to write; it has shape {tuple(channel.data.shape)}.")
|
|
41
|
+
with h5py.File(Path(path), "w") as handle:
|
|
42
|
+
for key, channel in channels.items():
|
|
43
|
+
dataset = handle.create_dataset(key, data=np.asarray(channel.data), compression="gzip")
|
|
44
|
+
dataset.attrs["x0"] = channel.t0
|
|
45
|
+
dataset.attrs["dx"] = channel.dt
|
|
46
|
+
dataset.attrs["xunit"] = "s"
|
|
47
|
+
dataset.attrs["unit"] = channel.unit or ""
|
|
48
|
+
if channel.name is not None:
|
|
49
|
+
dataset.attrs["name"] = channel.name
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def read_hdf5(path: str | Path) -> dict[str, TimeSeries]:
|
|
53
|
+
"""Read every dataset in ``path`` written in the layout :func:`write_hdf5` writes.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
path: Input file.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Mapping of dataset name to :class:`~gwmock_data.timeseries.TimeSeries`, in file order.
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
ValueError: If a dataset lacks the ``x0`` or ``dx`` attribute, or its time unit is not seconds.
|
|
63
|
+
ModuleNotFoundError: If ``h5py`` is not installed.
|
|
64
|
+
"""
|
|
65
|
+
h5py = require("h5py", "hdf5", "HDF5 input")
|
|
66
|
+
result = {}
|
|
67
|
+
with h5py.File(Path(path), "r") as handle:
|
|
68
|
+
for key, dataset in handle.items():
|
|
69
|
+
attrs = dataset.attrs
|
|
70
|
+
if "x0" not in attrs or "dx" not in attrs:
|
|
71
|
+
raise ValueError(f"dataset {key!r} has no x0 and dx attributes, so it is not a time series.")
|
|
72
|
+
xunit = _text(attrs.get("xunit", "s"))
|
|
73
|
+
if xunit not in ("s", "second"):
|
|
74
|
+
raise ValueError(f"dataset {key!r} has a time axis in {xunit!r}; only seconds are supported.")
|
|
75
|
+
name = _text(attrs["name"]) if "name" in attrs else None
|
|
76
|
+
result[key] = TimeSeries(
|
|
77
|
+
dataset[...],
|
|
78
|
+
t0=float(attrs["x0"]),
|
|
79
|
+
sample_rate=1.0 / float(attrs["dx"]),
|
|
80
|
+
name=name,
|
|
81
|
+
unit=_text(attrs.get("unit", "")) or None,
|
|
82
|
+
)
|
|
83
|
+
return result
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _text(value: object) -> str:
|
|
87
|
+
"""Return an HDF5 string attribute as ``str``, whether h5py read it as bytes or text."""
|
|
88
|
+
return value.decode("utf-8") if isinstance(value, bytes) else str(value)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""A uniform sample lattice, and the arithmetic for placing GPS times on it.
|
|
2
|
+
|
|
3
|
+
Superposing one series onto another is exact when the two share a sample lattice: the operation
|
|
4
|
+
is an integer-offset add. When they do not, something has to resample, and the resampling sets the
|
|
5
|
+
accuracy of whatever is built on it. A :class:`SamplingGrid` names the lattice so code can decide,
|
|
6
|
+
exactly, whether a time is on it.
|
|
7
|
+
|
|
8
|
+
GPS times near 1.4e9 s carry float64 rounding of a sizeable fraction of a sample at kilohertz rates,
|
|
9
|
+
so "on the lattice" is judged with a tolerance derived from that resolution, never a fixed one.
|
|
10
|
+
|
|
11
|
+
This module holds only the lattice arithmetic and its validation, and imports nothing but NumPy.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
#: Timestamp ULPs of slack allowed when deciding whether a time is on the lattice. The
|
|
21
|
+
#: tolerance has to be derived from float64 resolution rather than fixed: one ULP of a GPS
|
|
22
|
+
#: timestamp near 1.4e9 is about 2.4e-7 s, which at 2048 Hz is already 4.9e-4 samples. A fixed
|
|
23
|
+
#: 1e-6-sample tolerance -- the first thing written here -- was therefore some 500x *tighter*
|
|
24
|
+
#: than the timestamps can represent, and would have falsely rejected lattice times produced by
|
|
25
|
+
#: ordinary arithmetic. It survived initial testing only because 64 s at 2048 Hz happens to be
|
|
26
|
+
#: exact in binary; a non-power-of-two segment duration would have broken it.
|
|
27
|
+
_LATTICE_TOLERANCE_ULPS = 8.0
|
|
28
|
+
|
|
29
|
+
#: Smallest tolerance to use, in samples, so that a tiny epoch does not make the check
|
|
30
|
+
#: unreasonably strict.
|
|
31
|
+
_MINIMUM_TOLERANCE_SAMPLES = 1e-9
|
|
32
|
+
|
|
33
|
+
#: Largest tolerance to use, in samples. Well below the half-sample displacements the check
|
|
34
|
+
#: exists to catch, so no plausible ULP growth can make it accept a genuinely off-lattice time.
|
|
35
|
+
_MAXIMUM_TOLERANCE_SAMPLES = 1e-2
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class SamplingGrid:
|
|
40
|
+
"""A uniform sample lattice: sample ``k`` sits at ``epoch + k / sampling_frequency``.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
epoch: GPS time of sample zero.
|
|
44
|
+
sampling_frequency: Sample rate in Hz.
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
ValueError: If ``sampling_frequency`` is not positive and finite, or ``epoch`` is not
|
|
48
|
+
finite.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
epoch: float
|
|
52
|
+
sampling_frequency: float
|
|
53
|
+
|
|
54
|
+
def __post_init__(self) -> None:
|
|
55
|
+
"""Validate the lattice definition."""
|
|
56
|
+
if not np.isfinite(self.epoch):
|
|
57
|
+
raise ValueError(f"epoch must be finite; got {self.epoch}.")
|
|
58
|
+
if not np.isfinite(self.sampling_frequency) or self.sampling_frequency <= 0.0:
|
|
59
|
+
raise ValueError(f"sampling_frequency must be positive and finite; got {self.sampling_frequency}.")
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def sample_spacing(self) -> float:
|
|
63
|
+
"""Seconds between samples."""
|
|
64
|
+
return 1.0 / self.sampling_frequency
|
|
65
|
+
|
|
66
|
+
def lattice_tolerance_samples(self, gps_time: np.ndarray | float) -> float:
|
|
67
|
+
"""Return the on-lattice tolerance, in samples, appropriate to these timestamps.
|
|
68
|
+
|
|
69
|
+
Scaled by the float64 resolution of the times involved, because at GPS magnitudes that
|
|
70
|
+
resolution is coarser than any fixed tolerance worth writing down: near 1.4e9 one ULP
|
|
71
|
+
is already ~5e-4 samples at 2048 Hz.
|
|
72
|
+
|
|
73
|
+
An empty input is answered from the epoch alone. A chunked or filtered caller can
|
|
74
|
+
legitimately arrive with nothing to check, and reducing over an empty array would raise
|
|
75
|
+
here instead of letting the (vacuously satisfied) lattice check return empty.
|
|
76
|
+
"""
|
|
77
|
+
times = np.asarray(gps_time, dtype=float)
|
|
78
|
+
magnitude = abs(float(self.epoch))
|
|
79
|
+
if times.size:
|
|
80
|
+
magnitude = max(magnitude, float(np.max(np.abs(times))))
|
|
81
|
+
ulp_samples = float(np.spacing(magnitude)) * self.sampling_frequency
|
|
82
|
+
return float(
|
|
83
|
+
np.clip(_LATTICE_TOLERANCE_ULPS * ulp_samples, _MINIMUM_TOLERANCE_SAMPLES, _MAXIMUM_TOLERANCE_SAMPLES)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def index_of(self, gps_time: np.ndarray | float) -> np.ndarray:
|
|
87
|
+
"""Return the (generally fractional) lattice index of *gps_time*.
|
|
88
|
+
|
|
89
|
+
The subtraction is done before the multiplication: GPS times are ~1.4e9 and the
|
|
90
|
+
spacing is ~5e-4 s, so scaling first would lose the sub-sample part entirely.
|
|
91
|
+
"""
|
|
92
|
+
return (np.asarray(gps_time, dtype=float) - self.epoch) * self.sampling_frequency
|
|
93
|
+
|
|
94
|
+
def time_of(self, index: np.ndarray | int) -> np.ndarray:
|
|
95
|
+
"""Return the GPS time of lattice index *index*."""
|
|
96
|
+
return self.epoch + np.asarray(index, dtype=float) * self.sample_spacing
|
|
97
|
+
|
|
98
|
+
def split_index(self, gps_time: np.ndarray | float) -> tuple[np.ndarray, np.ndarray]:
|
|
99
|
+
"""Split *gps_time* into the preceding lattice index and a fractional remainder.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
``(index, fraction)`` with ``fraction`` in ``[0, 1)``, such that
|
|
103
|
+
``gps_time == time_of(index) + fraction * sample_spacing``. The fraction is what
|
|
104
|
+
the device must absorb for the sample to land on the lattice.
|
|
105
|
+
"""
|
|
106
|
+
exact = self.index_of(gps_time)
|
|
107
|
+
index = np.floor(exact)
|
|
108
|
+
return index.astype(np.int64), exact - index
|
|
109
|
+
|
|
110
|
+
def is_on_lattice(self, gps_time: np.ndarray | float) -> np.ndarray:
|
|
111
|
+
"""Return whether each *gps_time* coincides with a lattice sample."""
|
|
112
|
+
exact = self.index_of(gps_time)
|
|
113
|
+
return np.abs(exact - np.round(exact)) <= self.lattice_tolerance_samples(gps_time)
|
|
114
|
+
|
|
115
|
+
def require_on_lattice(self, gps_time: np.ndarray | float, *, name: str) -> np.ndarray:
|
|
116
|
+
"""Return integer lattice indices, rejecting any time that is off-lattice.
|
|
117
|
+
|
|
118
|
+
Rejected rather than silently rounded: rounding would move a signal by up to half a
|
|
119
|
+
sample without telling anyone, and silently accepting an off-lattice time is exactly
|
|
120
|
+
how the off-lattice resampling error described in this module's docstring got in.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
gps_time: Time(s) that must coincide with lattice samples.
|
|
124
|
+
name: What is being checked, for the error message.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Integer lattice indices, same shape as the input.
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
ValueError: If any time is not on the lattice.
|
|
131
|
+
"""
|
|
132
|
+
times = np.atleast_1d(np.asarray(gps_time, dtype=float))
|
|
133
|
+
exact = self.index_of(times)
|
|
134
|
+
offenders = np.abs(exact - np.round(exact)) > self.lattice_tolerance_samples(times)
|
|
135
|
+
if np.any(offenders):
|
|
136
|
+
first = int(np.argmax(offenders))
|
|
137
|
+
raise ValueError(
|
|
138
|
+
f"{name} must lie on the sampling grid (epoch={self.epoch!r}, "
|
|
139
|
+
f"sampling_frequency={self.sampling_frequency!r}), but entry {first} at "
|
|
140
|
+
f"{times[first]!r} is {exact[first] - round(float(exact[first])):+.6g} samples off. "
|
|
141
|
+
f"Rounding it would displace the data by up to half a sample; choose segment "
|
|
142
|
+
f"starts on the grid, or build the grid from them."
|
|
143
|
+
)
|
|
144
|
+
return np.round(exact).astype(np.int64).reshape(np.shape(gps_time))
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def from_segment_starts(cls, segment_start_times: np.ndarray, sampling_frequency: float) -> SamplingGrid:
|
|
148
|
+
"""Build a grid anchored on the first segment start.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
segment_start_times: GPS start times, which must themselves be mutually
|
|
152
|
+
consistent with one lattice at this sample rate.
|
|
153
|
+
sampling_frequency: Sample rate in Hz.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
A grid whose epoch is the first segment start.
|
|
157
|
+
|
|
158
|
+
Raises:
|
|
159
|
+
ValueError: If ``segment_start_times`` is empty, or the starts do not share one
|
|
160
|
+
lattice — which would make a single canonical grid impossible.
|
|
161
|
+
"""
|
|
162
|
+
starts = np.atleast_1d(np.asarray(segment_start_times, dtype=float))
|
|
163
|
+
if starts.size == 0:
|
|
164
|
+
raise ValueError("segment_start_times must not be empty.")
|
|
165
|
+
grid = cls(epoch=float(starts[0]), sampling_frequency=sampling_frequency)
|
|
166
|
+
grid.require_on_lattice(starts, name="segment_start_times")
|
|
167
|
+
return grid
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
"""A uniformly sampled time series whose samples are a NumPy or a JAX array.
|
|
2
|
+
|
|
3
|
+
:class:`TimeSeries` holds an array and the few numbers that place it in time: the GPS time of the
|
|
4
|
+
first sample and the sample rate. Time runs along the **last** axis; leading axes are allowed and
|
|
5
|
+
share that timing, so the strain of several detectors over one segment can be a single series of
|
|
6
|
+
shape ``(n_detectors, n_samples)``.
|
|
7
|
+
|
|
8
|
+
The array is kept as given. A NumPy array stays on the host, and a JAX array stays where it is,
|
|
9
|
+
on a GPU included, through every method here: each works through the array's own namespace
|
|
10
|
+
(``data.__array_namespace__()``), never by converting to NumPy. :meth:`TimeSeries.to_numpy` and
|
|
11
|
+
:meth:`TimeSeries.to_jax` move data deliberately.
|
|
12
|
+
|
|
13
|
+
The class is deliberately small. It has no arithmetic, spectral or plotting methods: code that
|
|
14
|
+
needs them works on :attr:`TimeSeries.data`, and a caller who wants GWpy's API converts with
|
|
15
|
+
:meth:`TimeSeries.to_gwpy`. Conversions to and from GWpy and PyCBC import those packages only when
|
|
16
|
+
called.
|
|
17
|
+
|
|
18
|
+
Under JAX, :func:`register_pytree` makes a series a pytree, so it passes through ``jax.jit`` and
|
|
19
|
+
``jax.vmap``. The samples and ``t0`` are leaves; the sample rate, name and unit are static. The
|
|
20
|
+
modules that use JAX register it; importing this module never imports JAX. JAX must be in 64-bit mode
|
|
21
|
+
(``jax.config.update("jax_enable_x64", True)``): GPS times need float64, and in 32-bit mode the
|
|
22
|
+
methods that would lose it raise instead.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import dataclasses
|
|
28
|
+
import functools
|
|
29
|
+
import math
|
|
30
|
+
import sys
|
|
31
|
+
import threading
|
|
32
|
+
from dataclasses import dataclass
|
|
33
|
+
from typing import TYPE_CHECKING, Any
|
|
34
|
+
|
|
35
|
+
import numpy as np
|
|
36
|
+
|
|
37
|
+
from gwmock_data._optional import require
|
|
38
|
+
from gwmock_data.sampling_grid import SamplingGrid
|
|
39
|
+
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
from types import ModuleType
|
|
42
|
+
|
|
43
|
+
import gwpy.timeseries
|
|
44
|
+
import pycbc.types
|
|
45
|
+
|
|
46
|
+
__all__ = ["TimeSeries", "as_timeseries", "register_pytree"]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _namespace(data: Any) -> ModuleType:
|
|
50
|
+
"""Return the array namespace of ``data``: ``numpy``, ``jax.numpy``, or another array-API library."""
|
|
51
|
+
return data.__array_namespace__()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True, eq=False)
|
|
55
|
+
class TimeSeries:
|
|
56
|
+
"""A uniformly sampled series: sample ``k`` along the last axis is at ``t0 + k / sample_rate``.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
data: The samples, an array with at least one axis; time is the last. A NumPy or a JAX
|
|
60
|
+
array is kept as given. Anything else is converted with :func:`numpy.asarray`.
|
|
61
|
+
t0: GPS time of the first sample, in seconds.
|
|
62
|
+
sample_rate: Samples per second, in Hz.
|
|
63
|
+
name: Optional label, such as a detector or channel name. It labels the whole series.
|
|
64
|
+
unit: Optional unit of the samples, such as ``"strain"``.
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
ValueError: If ``data`` has no axis, or ``t0`` or ``sample_rate`` is not finite, or
|
|
68
|
+
``sample_rate`` is not positive.
|
|
69
|
+
TypeError: If ``t0`` is not a single number.
|
|
70
|
+
|
|
71
|
+
Two series are equal only if they are the same object: comparing arrays elementwise has no
|
|
72
|
+
single truth value, and a JAX array cannot be compared without a device round trip.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
data: Any
|
|
76
|
+
t0: float
|
|
77
|
+
sample_rate: float
|
|
78
|
+
name: str | None = None
|
|
79
|
+
unit: str | None = None
|
|
80
|
+
|
|
81
|
+
def __post_init__(self) -> None:
|
|
82
|
+
"""Validate the timing and normalise the samples to an array."""
|
|
83
|
+
data = self.data
|
|
84
|
+
if not hasattr(data, "__array_namespace__"):
|
|
85
|
+
data = np.asarray(data)
|
|
86
|
+
object.__setattr__(self, "data", data)
|
|
87
|
+
if len(data.shape) == 0:
|
|
88
|
+
raise ValueError("data must have at least one axis; time is the last.")
|
|
89
|
+
sample_rate = float(self.sample_rate)
|
|
90
|
+
if not (math.isfinite(sample_rate) and sample_rate > 0.0):
|
|
91
|
+
raise ValueError(f"sample_rate must be positive and finite; got {self.sample_rate!r}.")
|
|
92
|
+
object.__setattr__(self, "sample_rate", sample_rate)
|
|
93
|
+
try:
|
|
94
|
+
t0 = float(self.t0)
|
|
95
|
+
except TypeError as exc:
|
|
96
|
+
# A traced value inside a JAX transformation has no number yet to check; it was checked
|
|
97
|
+
# when the series entered the transformation. Matched by name so JAX is not imported.
|
|
98
|
+
if type(exc).__name__ == "ConcretizationTypeError":
|
|
99
|
+
return
|
|
100
|
+
raise TypeError(f"t0 must be a single number; got {self.t0!r}.") from exc
|
|
101
|
+
if not math.isfinite(t0):
|
|
102
|
+
raise ValueError(f"t0 must be finite; got {self.t0!r}.")
|
|
103
|
+
object.__setattr__(self, "t0", t0)
|
|
104
|
+
|
|
105
|
+
# ----------------------------------------------------------------------------------------
|
|
106
|
+
# Derived quantities
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def value(self) -> Any:
|
|
110
|
+
"""The samples; an alias of :attr:`data`, the name GWpy uses."""
|
|
111
|
+
return self.data
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def xp(self) -> ModuleType:
|
|
115
|
+
"""The array namespace of :attr:`data`, such as ``numpy`` or ``jax.numpy``."""
|
|
116
|
+
return _namespace(self.data)
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def n_samples(self) -> int:
|
|
120
|
+
"""Number of samples along the time axis."""
|
|
121
|
+
return int(self.data.shape[-1])
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def dt(self) -> float:
|
|
125
|
+
"""Seconds between samples."""
|
|
126
|
+
return 1.0 / self.sample_rate
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def duration(self) -> float:
|
|
130
|
+
"""Seconds spanned by the samples: ``n_samples * dt``."""
|
|
131
|
+
return self.n_samples * self.dt
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def end(self) -> float:
|
|
135
|
+
"""GPS time of the sample after the last, on the series' lattice: ``t0 + n_samples * dt``."""
|
|
136
|
+
return self._time_of(self.n_samples)
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def grid(self) -> SamplingGrid:
|
|
140
|
+
"""The sample lattice of this series."""
|
|
141
|
+
return SamplingGrid(epoch=self.t0, sampling_frequency=self.sample_rate)
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def times(self) -> Any:
|
|
145
|
+
"""GPS time of each sample, in the namespace of :attr:`data`, as float64.
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
RuntimeError: If the namespace cannot hold float64, as JAX cannot outside its 64-bit
|
|
149
|
+
mode. In single precision a GPS time near 1.4e9 s resolves only to 128 s, so every
|
|
150
|
+
sample of a typical series would get the same time.
|
|
151
|
+
"""
|
|
152
|
+
xp = self.xp
|
|
153
|
+
times = self.t0 + xp.arange(self.n_samples, dtype=xp.float64) * self.dt
|
|
154
|
+
if times.dtype != xp.float64:
|
|
155
|
+
raise RuntimeError(_NO_FLOAT64)
|
|
156
|
+
return times
|
|
157
|
+
|
|
158
|
+
# ----------------------------------------------------------------------------------------
|
|
159
|
+
# New series from this one
|
|
160
|
+
|
|
161
|
+
def with_data(self, data: Any) -> TimeSeries:
|
|
162
|
+
"""Return a series with the same timing, name and unit, holding ``data``."""
|
|
163
|
+
return dataclasses.replace(self, data=data)
|
|
164
|
+
|
|
165
|
+
def copy(self) -> TimeSeries:
|
|
166
|
+
"""Return a series whose samples are a copy of these."""
|
|
167
|
+
return self.with_data(self.xp.asarray(self.data, copy=True))
|
|
168
|
+
|
|
169
|
+
def crop(self, start: float | None = None, end: float | None = None) -> TimeSeries:
|
|
170
|
+
"""Return the samples at GPS times in ``[start, end)``.
|
|
171
|
+
|
|
172
|
+
The bounds decide the result's length, so ``crop`` needs a concrete ``t0``: it cannot run
|
|
173
|
+
inside a JAX transformation over the series.
|
|
174
|
+
|
|
175
|
+
The result starts at the first sample at or after ``start`` and stops before the first at
|
|
176
|
+
or after ``end``. A bound that falls on a sample, to within the float64 resolution of GPS
|
|
177
|
+
times (:meth:`~gwmock_data.sampling_grid.SamplingGrid.lattice_tolerance_samples`), counts
|
|
178
|
+
as on it, so cropping at a sample's own timestamp keeps that sample however large the GPS
|
|
179
|
+
time. Bounds outside the series are clipped to it.
|
|
180
|
+
|
|
181
|
+
GWpy differs for a bound between samples: it rounds down, keeping the sample before
|
|
182
|
+
``start``. Here every kept sample is at or after ``start``.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
start: First GPS time to keep; ``None`` keeps from the first sample.
|
|
186
|
+
end: GPS time to stop before; ``None`` keeps to the last sample.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
The cropped series, possibly with no samples. Its samples are a view where the array
|
|
190
|
+
library makes one.
|
|
191
|
+
|
|
192
|
+
Raises:
|
|
193
|
+
ValueError: If a bound is not finite, or ``end`` is before ``start``.
|
|
194
|
+
"""
|
|
195
|
+
for bound_name, bound in (("start", start), ("end", end)):
|
|
196
|
+
if bound is not None and not math.isfinite(bound):
|
|
197
|
+
raise ValueError(f"{bound_name} must be finite; got {bound!r}.")
|
|
198
|
+
if start is not None and end is not None and end < start:
|
|
199
|
+
raise ValueError(f"end ({end}) is before start ({start}).")
|
|
200
|
+
first = 0 if start is None else self._first_index_at_or_after(start)
|
|
201
|
+
stop = self.n_samples if end is None else self._first_index_at_or_after(end)
|
|
202
|
+
first = min(max(first, 0), self.n_samples)
|
|
203
|
+
stop = min(max(stop, first), self.n_samples)
|
|
204
|
+
return dataclasses.replace(self, data=self.data[..., first:stop], t0=self._time_of(first))
|
|
205
|
+
|
|
206
|
+
def pad(self, before: int = 0, after: int = 0) -> TimeSeries:
|
|
207
|
+
"""Return the series with ``before`` zeros prepended and ``after`` appended, in samples.
|
|
208
|
+
|
|
209
|
+
``t0`` moves back by ``before`` samples, so the existing samples keep their times. The
|
|
210
|
+
counts decide the result's length, so inside a JAX transformation they must be Python
|
|
211
|
+
integers, not traced values.
|
|
212
|
+
|
|
213
|
+
Raises:
|
|
214
|
+
ValueError: If either count is negative.
|
|
215
|
+
"""
|
|
216
|
+
if before < 0 or after < 0:
|
|
217
|
+
raise ValueError(f"pad counts must not be negative; got before={before}, after={after}.")
|
|
218
|
+
xp = self.xp
|
|
219
|
+
lead = self.data.shape[:-1]
|
|
220
|
+
parts = [
|
|
221
|
+
xp.zeros((*lead, before), dtype=self.data.dtype),
|
|
222
|
+
self.data,
|
|
223
|
+
xp.zeros((*lead, after), dtype=self.data.dtype),
|
|
224
|
+
]
|
|
225
|
+
return dataclasses.replace(self, data=xp.concatenate(parts, axis=-1), t0=self._time_of(-before))
|
|
226
|
+
|
|
227
|
+
def is_compatible(self, other: TimeSeries) -> bool:
|
|
228
|
+
"""Return whether ``other`` has the same sample rate and unit, so the two can be combined."""
|
|
229
|
+
return self.sample_rate == other.sample_rate and self.unit == other.unit
|
|
230
|
+
|
|
231
|
+
def _time_of(self, index: int) -> Any:
|
|
232
|
+
# The arithmetic of SamplingGrid.time_of, written out so it also works on a traced t0.
|
|
233
|
+
return self.t0 + index * self.dt
|
|
234
|
+
|
|
235
|
+
def _first_index_at_or_after(self, gps_time: float) -> int:
|
|
236
|
+
grid = self.grid
|
|
237
|
+
exact = float(grid.index_of(gps_time))
|
|
238
|
+
return math.ceil(exact - grid.lattice_tolerance_samples(gps_time))
|
|
239
|
+
|
|
240
|
+
def __array__(self, dtype: Any = None, copy: bool | None = None) -> np.ndarray:
|
|
241
|
+
"""Return the samples as a NumPy array, so ``numpy.asarray(series)`` works as for GWpy.
|
|
242
|
+
|
|
243
|
+
Follows NumPy's protocol: ``copy=True`` always copies, ``None`` copies only when the dtype
|
|
244
|
+
or the array library requires it, and ``False`` raises ``ValueError`` when a copy would be
|
|
245
|
+
required -- a dtype change, or samples that are not a NumPy array.
|
|
246
|
+
"""
|
|
247
|
+
needs_copy = not isinstance(self.data, np.ndarray) or (dtype is not None and np.dtype(dtype) != self.data.dtype)
|
|
248
|
+
if copy is False and needs_copy:
|
|
249
|
+
raise ValueError("these samples cannot be returned as a NumPy array without a copy.")
|
|
250
|
+
array = np.asarray(self.data, dtype=dtype)
|
|
251
|
+
return np.array(array, copy=True) if copy else array
|
|
252
|
+
|
|
253
|
+
# ----------------------------------------------------------------------------------------
|
|
254
|
+
# Moving data and converting
|
|
255
|
+
|
|
256
|
+
def to_numpy(self) -> TimeSeries:
|
|
257
|
+
"""Return the series with its samples as a NumPy array on the host."""
|
|
258
|
+
return self.with_data(np.asarray(self.data))
|
|
259
|
+
|
|
260
|
+
def to_jax(self, device: Any = None) -> TimeSeries:
|
|
261
|
+
"""Return the series with its samples as a JAX array, on ``device`` if given.
|
|
262
|
+
|
|
263
|
+
Raises:
|
|
264
|
+
ModuleNotFoundError: If JAX is not installed.
|
|
265
|
+
RuntimeError: If JAX is not in 64-bit mode, in which it would round double-precision
|
|
266
|
+
samples to single precision.
|
|
267
|
+
"""
|
|
268
|
+
jax = require("jax", "jax", "TimeSeries.to_jax")
|
|
269
|
+
_require_x64(jax)
|
|
270
|
+
return self.with_data(jax.device_put(jax.numpy.asarray(self.data), device))
|
|
271
|
+
|
|
272
|
+
def to_gwpy(self) -> gwpy.timeseries.TimeSeries:
|
|
273
|
+
"""Return a GWpy ``TimeSeries`` with the same samples, timing, name and unit.
|
|
274
|
+
|
|
275
|
+
NumPy samples are shared, not copied, as :meth:`from_gwpy` shares GWpy's, so a write through
|
|
276
|
+
either object is seen by the other. JAX samples are brought to the host with
|
|
277
|
+
:func:`numpy.asarray`; for a CPU array that is a read-only view of JAX's buffer.
|
|
278
|
+
|
|
279
|
+
Raises:
|
|
280
|
+
ValueError: If the series has more than one axis; GWpy's is one-dimensional.
|
|
281
|
+
ModuleNotFoundError: If GWpy is not installed.
|
|
282
|
+
"""
|
|
283
|
+
if len(self.data.shape) != 1:
|
|
284
|
+
raise ValueError(f"a GWpy TimeSeries is one-dimensional; this series has shape {tuple(self.data.shape)}.")
|
|
285
|
+
timeseries = require("gwpy.timeseries", "gwpy", "TimeSeries.to_gwpy")
|
|
286
|
+
return timeseries.TimeSeries(
|
|
287
|
+
np.asarray(self.data),
|
|
288
|
+
t0=self.t0,
|
|
289
|
+
sample_rate=self.sample_rate,
|
|
290
|
+
name=self.name,
|
|
291
|
+
unit=self.unit,
|
|
292
|
+
copy=False,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
@classmethod
|
|
296
|
+
def from_gwpy(cls, series: gwpy.timeseries.TimeSeries) -> TimeSeries:
|
|
297
|
+
"""Return a series with the samples, timing, name and unit of a GWpy ``TimeSeries``.
|
|
298
|
+
|
|
299
|
+
The samples are GWpy's array itself, not a copy. A dimensionless GWpy series gets
|
|
300
|
+
``unit=None``.
|
|
301
|
+
|
|
302
|
+
Raises:
|
|
303
|
+
ValueError: If the GWpy series is irregularly sampled.
|
|
304
|
+
"""
|
|
305
|
+
try:
|
|
306
|
+
sample_rate = series.sample_rate.to_value("Hz")
|
|
307
|
+
except AttributeError as exc:
|
|
308
|
+
# GWpy has no sample rate for a series built from an explicit, irregular times array.
|
|
309
|
+
raise ValueError(
|
|
310
|
+
"a TimeSeries is uniformly sampled; this GWpy series has an irregular time index."
|
|
311
|
+
) from exc
|
|
312
|
+
unit = series.unit.to_string() if series.unit is not None else ""
|
|
313
|
+
return cls(
|
|
314
|
+
series.value,
|
|
315
|
+
t0=series.t0.to_value("s"),
|
|
316
|
+
sample_rate=sample_rate,
|
|
317
|
+
name=None if series.name is None else str(series.name),
|
|
318
|
+
unit=unit or None,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
def to_pycbc(self) -> pycbc.types.TimeSeries:
|
|
322
|
+
"""Return a PyCBC ``TimeSeries`` with the same samples and timing.
|
|
323
|
+
|
|
324
|
+
PyCBC series carry neither a name nor a unit.
|
|
325
|
+
|
|
326
|
+
Raises:
|
|
327
|
+
ValueError: If the series has more than one axis; PyCBC's is one-dimensional.
|
|
328
|
+
ModuleNotFoundError: If PyCBC is not installed.
|
|
329
|
+
"""
|
|
330
|
+
if len(self.data.shape) != 1:
|
|
331
|
+
raise ValueError(f"a PyCBC TimeSeries is one-dimensional; this series has shape {tuple(self.data.shape)}.")
|
|
332
|
+
types = require("pycbc.types", "pycbc", "TimeSeries.to_pycbc")
|
|
333
|
+
return types.TimeSeries(np.asarray(self.data), delta_t=self.dt, epoch=self.t0)
|
|
334
|
+
|
|
335
|
+
@classmethod
|
|
336
|
+
def from_pycbc(
|
|
337
|
+
cls, series: pycbc.types.TimeSeries, *, name: str | None = None, unit: str | None = None
|
|
338
|
+
) -> TimeSeries:
|
|
339
|
+
"""Return a series with the samples and timing of a PyCBC ``TimeSeries``."""
|
|
340
|
+
return cls(
|
|
341
|
+
np.asarray(series.numpy()),
|
|
342
|
+
t0=float(series.start_time),
|
|
343
|
+
sample_rate=float(series.sample_rate),
|
|
344
|
+
name=name,
|
|
345
|
+
unit=unit,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def as_timeseries(series: TimeSeries | gwpy.timeseries.TimeSeries) -> TimeSeries:
|
|
350
|
+
"""Return ``series`` as a :class:`TimeSeries`: itself, or converted from a GWpy ``TimeSeries``.
|
|
351
|
+
|
|
352
|
+
Functions that take a time series call this on their input, so they accept either kind.
|
|
353
|
+
|
|
354
|
+
Raises:
|
|
355
|
+
TypeError: If ``series`` is neither.
|
|
356
|
+
"""
|
|
357
|
+
if isinstance(series, TimeSeries):
|
|
358
|
+
return series
|
|
359
|
+
# A GWpy series can only exist if GWpy is imported, so it is not imported here to find out.
|
|
360
|
+
gwpy_timeseries = sys.modules.get("gwpy.timeseries")
|
|
361
|
+
if gwpy_timeseries is not None and isinstance(series, gwpy_timeseries.TimeSeries):
|
|
362
|
+
return TimeSeries.from_gwpy(series)
|
|
363
|
+
raise TypeError(f"expected a gwmock_data.TimeSeries or a gwpy.timeseries.TimeSeries; got {type(series).__name__}.")
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
_NO_FLOAT64 = (
|
|
367
|
+
"TimeSeries needs float64 timing, which this array library is not providing; with JAX, call "
|
|
368
|
+
'jax.config.update("jax_enable_x64", True) first. '
|
|
369
|
+
"In single precision a GPS time near 1.4e9 s resolves only to 128 s, so every sample of "
|
|
370
|
+
"a typical series would get the same time."
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _require_x64(jax: ModuleType) -> None:
|
|
375
|
+
"""Refuse, rather than degrade, when JAX is in 32-bit mode."""
|
|
376
|
+
if not jax.config.jax_enable_x64:
|
|
377
|
+
raise RuntimeError(_NO_FLOAT64)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
#: Serialises the first registrations: the cache alone lets concurrent first calls both run.
|
|
381
|
+
_REGISTRATION_LOCK = threading.Lock()
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def register_pytree() -> None:
|
|
385
|
+
"""Register :class:`TimeSeries` as a JAX pytree; safe to call more than once, from any thread.
|
|
386
|
+
|
|
387
|
+
The samples and ``t0`` are leaves, so a batch of series differing in start time can be mapped
|
|
388
|
+
over without recompiling. The sample rate, name and unit are static: they decide shapes and
|
|
389
|
+
meaning, not values.
|
|
390
|
+
|
|
391
|
+
Unflattening bypasses validation, because inside a transformation the leaves are tracers,
|
|
392
|
+
which cannot be checked as numbers. A series rebuilt with its start time as a JAX array that is
|
|
393
|
+
not float64 -- what ``jit``, ``vmap``, ``grad`` and ``device_put`` produce in JAX's 32-bit mode --
|
|
394
|
+
raises ``RuntimeError``; structural uses that leave it a Python float, such as ``jax.tree.map``
|
|
395
|
+
or ``jax.device_get``, work in either mode.
|
|
396
|
+
|
|
397
|
+
Raises:
|
|
398
|
+
ModuleNotFoundError: If JAX is not installed.
|
|
399
|
+
"""
|
|
400
|
+
with _REGISTRATION_LOCK:
|
|
401
|
+
_register_pytree_once()
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
# Cached, so registration, which JAX keeps per process, happens once.
|
|
405
|
+
@functools.cache
|
|
406
|
+
def _register_pytree_once() -> None:
|
|
407
|
+
jax = require("jax", "jax", "Registering TimeSeries with JAX")
|
|
408
|
+
tree_util = jax.tree_util
|
|
409
|
+
|
|
410
|
+
def flatten(series: TimeSeries) -> tuple[tuple[Any, Any], tuple[float, str | None, str | None]]:
|
|
411
|
+
return (series.data, series.t0), (series.sample_rate, series.name, series.unit)
|
|
412
|
+
|
|
413
|
+
def unflatten(static: tuple[float, str | None, str | None], leaves: tuple[Any, Any]) -> TimeSeries:
|
|
414
|
+
# Checked where it can go wrong: JAX has turned t0 into one of its arrays -- a tracer under
|
|
415
|
+
# jit, vmap or grad, or a device array after device_put -- and, in 32-bit mode, a
|
|
416
|
+
# single-precision one, which cannot tell samples apart. A tree map or device_get that
|
|
417
|
+
# leaves t0 a Python float passes in either mode.
|
|
418
|
+
t0 = leaves[1]
|
|
419
|
+
if isinstance(t0, jax.Array) and t0.dtype != jax.numpy.float64:
|
|
420
|
+
raise RuntimeError(_NO_FLOAT64)
|
|
421
|
+
series = object.__new__(TimeSeries)
|
|
422
|
+
for field_name, value in zip(("data", "t0"), leaves, strict=True):
|
|
423
|
+
object.__setattr__(series, field_name, value)
|
|
424
|
+
for field_name, value in zip(("sample_rate", "name", "unit"), static, strict=True):
|
|
425
|
+
object.__setattr__(series, field_name, value)
|
|
426
|
+
return series
|
|
427
|
+
|
|
428
|
+
tree_util.register_pytree_node(TimeSeries, flatten, unflatten)
|
gwmock_data/version.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""A script to infer the version number from the metadata."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
__version__ = version("gwmock_data")
|
|
9
|
+
except PackageNotFoundError:
|
|
10
|
+
# Fallback for source checkouts or environments without installed metadata.
|
|
11
|
+
__version__ = "0+unknown"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: gwmock-data
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Shared data structures for the gwmock packages
|
|
5
|
+
Project-URL: Documentation, https://leuven-gravity-institute.github.io/gwmock-data
|
|
6
|
+
Project-URL: Source, https://github.com/Leuven-Gravity-Institute/gwmock-data
|
|
7
|
+
Project-URL: Tracker, https://github.com/Leuven-Gravity-Institute/gwmock-data/issues
|
|
8
|
+
Project-URL: Home, https://github.com/Leuven-Gravity-Institute/gwmock-data
|
|
9
|
+
Project-URL: Release Notes, https://github.com/Leuven-Gravity-Institute/gwmock-data/releases
|
|
10
|
+
Author: Isaac C. F. Wong
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
19
|
+
Requires-Python: >=3.13
|
|
20
|
+
Requires-Dist: numpy>=2.2
|
|
21
|
+
Provides-Extra: gwpy
|
|
22
|
+
Requires-Dist: gwpy>=4.0.0; extra == 'gwpy'
|
|
23
|
+
Provides-Extra: hdf5
|
|
24
|
+
Requires-Dist: h5py>=3.12; extra == 'hdf5'
|
|
25
|
+
Provides-Extra: jax
|
|
26
|
+
Requires-Dist: jax>=0.5.0; extra == 'jax'
|
|
27
|
+
Provides-Extra: pycbc
|
|
28
|
+
Requires-Dist: pycbc>=2.6.0; extra == 'pycbc'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# gwmock-data
|
|
32
|
+
|
|
33
|
+
[](https://github.com/Leuven-Gravity-Institute/gwmock-data/actions/workflows/ci.yml)
|
|
34
|
+
[](https://results.pre-commit.ci/latest/github/Leuven-Gravity-Institute/gwmock-data/main)
|
|
35
|
+
[](https://leuven-gravity-institute.github.io/gwmock-data/)
|
|
36
|
+
[](https://codecov.io/gh/Leuven-Gravity-Institute/gwmock-data)
|
|
37
|
+
[](https://pypi.org/project/gwmock-data/)
|
|
38
|
+
[](https://pypi.org/project/gwmock-data/)
|
|
39
|
+
[](LICENSE)
|
|
40
|
+
[](https://github.com/astral-sh/ruff)
|
|
41
|
+
[](https://scientific-python.org/specs/spec-0000/)
|
|
42
|
+
|
|
43
|
+
Shared data structures for the gwmock packages: the types they pass between each
|
|
44
|
+
other, so every package reads and writes them the same way.
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install gwmock-data
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Development
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
uv sync --group dev
|
|
56
|
+
uv run pytest
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
gwmock_data/__init__.py,sha256=v0iS9KHxSpL9jjQZbgfSx4UhU7J1Cj_cSHaDMHFYpbo,547
|
|
2
|
+
gwmock_data/_optional.py,sha256=v4k7eNEzYhWQtKUgo5Y9QsTvxLa1-0Hi97tCuFBOLMQ,1395
|
|
3
|
+
gwmock_data/sampling_grid.py,sha256=wZQp1d-u1dJsclZAfdaNK2XXCJkpsJH1NoN08yomYC4,7995
|
|
4
|
+
gwmock_data/timeseries.py,sha256=rCN24Rrh4hanBOibnpUZeTu5LCMZROhfe0fK51O6kbQ,19187
|
|
5
|
+
gwmock_data/version.py,sha256=hv5yYdb-Wkr9lKNCSZSQrMnjNoKnaUBK05xgKDhDoEY,346
|
|
6
|
+
gwmock_data/io/__init__.py,sha256=GD3HMrdLrRg2VYbtKvXQC89YpI1HozWEoqzjqijEwxg,169
|
|
7
|
+
gwmock_data/io/hdf5.py,sha256=HdAjIjMpz6xU6dh7BWuTG7701yUmizQrd3snf3t-IYc,3675
|
|
8
|
+
gwmock_data-0.1.0.dist-info/METADATA,sha256=XsF8cDdtt8_96Epi-pii65H2hN80N6uyg35UDm9sbT0,2938
|
|
9
|
+
gwmock_data-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
10
|
+
gwmock_data-0.1.0.dist-info/licenses/LICENSE,sha256=9k1btU36CFFvEBAdimpM63ozNUIPDwhowqECuviFuLw,1481
|
|
11
|
+
gwmock_data-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Copyright 2026, Isaac C. F. Wong
|
|
2
|
+
|
|
3
|
+
Redistribution and use in source and binary forms, with or without modification,
|
|
4
|
+
are permitted provided that the following conditions are met:
|
|
5
|
+
|
|
6
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
7
|
+
list of conditions and the following disclaimer.
|
|
8
|
+
|
|
9
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
10
|
+
this list of conditions and the following disclaimer in the documentation
|
|
11
|
+
and/or other materials provided with the distribution.
|
|
12
|
+
|
|
13
|
+
3. Neither the name of the copyright holder nor the names of its contributors
|
|
14
|
+
may be used to endorse or promote products derived from this software without
|
|
15
|
+
specific prior written permission.
|
|
16
|
+
|
|
17
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND
|
|
18
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
19
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
20
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
|
21
|
+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
22
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
23
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
24
|
+
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
25
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
26
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|