binary-galactica 0.5.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.
- binary_galactica/__init__.py +139 -0
- binary_galactica/constants.py +18 -0
- binary_galactica/detector.py +142 -0
- binary_galactica/glass/__init__.py +118 -0
- binary_galactica/glass/_precision.py +49 -0
- binary_galactica/glass/_twofloat.py +117 -0
- binary_galactica/glass/_validation.py +94 -0
- binary_galactica/glass/alignment.py +170 -0
- binary_galactica/glass/bandwidth.py +168 -0
- binary_galactica/glass/compat.py +146 -0
- binary_galactica/glass/constants.py +83 -0
- binary_galactica/glass/convert.py +91 -0
- binary_galactica/glass/fourier.py +387 -0
- binary_galactica/glass/interpolation.py +255 -0
- binary_galactica/glass/onfly.py +195 -0
- binary_galactica/glass/orbit.py +163 -0
- binary_galactica/glass/prior.py +306 -0
- binary_galactica/glass/proposals.py +488 -0
- binary_galactica/glass/response.py +146 -0
- binary_galactica/glass/single.py +455 -0
- binary_galactica/glass/sparse.py +113 -0
- binary_galactica/glass/tdi.py +159 -0
- binary_galactica/glass/types.py +24 -0
- binary_galactica/glass/waveform.py +360 -0
- binary_galactica/parameters.py +221 -0
- binary_galactica/py.typed +0 -0
- binary_galactica/single.py +462 -0
- binary_galactica/waveform.py +196 -0
- binary_galactica-0.5.0.dist-info/METADATA +614 -0
- binary_galactica-0.5.0.dist-info/RECORD +37 -0
- binary_galactica-0.5.0.dist-info/WHEEL +4 -0
- binary_galactica-0.5.0.dist-info/licenses/LICENSE +203 -0
- binary_galactica-0.5.0.dist-info/licenses/NOTICE +42 -0
- binary_galactica-0.5.0.dist-info/licenses/tools/glass_reference/LICENSE-GLASS +201 -0
- binary_galactica-0.5.0.dist-info/licenses/tools/glass_reference/vendor/kissfft/COPYING +11 -0
- binary_galactica-0.5.0.dist-info/licenses/tools/glass_reference/vendor/kissfft/LICENSES/BSD-3-Clause +35 -0
- binary_galactica-0.5.0.dist-info/licenses/tools/glass_reference/vendor/kissfft/LICENSES/Unlicense +30 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Galactic-binary LISA TDI waveforms in JAX.
|
|
2
|
+
|
|
3
|
+
``ucb_waveform`` uses GLASS-native parameter vectors; ``tdi_waveform`` accepts
|
|
4
|
+
physical ``GBParams``. Both return frequency-domain LISA detector channels.
|
|
5
|
+
The original ``polarizations`` function remains a source-only interface.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
# Importing the package leaves JAX's process-global configuration alone.
|
|
11
|
+
#
|
|
12
|
+
# Over a four-year observation the phase of a millihertz binary accumulates to
|
|
13
|
+
# ~1e6 radians. Held in float32 that carries ~0.06 rad of error -- more than
|
|
14
|
+
# enough to destroy a matched filter, yet far too small to notice in a plot.
|
|
15
|
+
# The float64 functions therefore raise RuntimeError unless the application has
|
|
16
|
+
# enabled double precision with jax.config.update("jax_enable_x64", True); they
|
|
17
|
+
# never fall back to float32 silently. binary_galactica.single provides float32
|
|
18
|
+
# waveforms, with the frequency split into a reference bin and a small offset,
|
|
19
|
+
# for processes that keep x64 disabled.
|
|
20
|
+
from binary_galactica import single
|
|
21
|
+
from binary_galactica.constants import DAY, YEAR
|
|
22
|
+
from binary_galactica.detector import tdi_waveform
|
|
23
|
+
from binary_galactica.glass import (
|
|
24
|
+
BACKENDS,
|
|
25
|
+
DEFAULT_RESPONSE_SAMPLES,
|
|
26
|
+
FORMATS,
|
|
27
|
+
FSTAR,
|
|
28
|
+
GLASS_TO_CONTINUOUS,
|
|
29
|
+
LARM,
|
|
30
|
+
SNR_MIN,
|
|
31
|
+
UCB_MODEL_NP,
|
|
32
|
+
Alignment,
|
|
33
|
+
ExtrinsicPriorDraw,
|
|
34
|
+
FisherCache,
|
|
35
|
+
FisherProposal,
|
|
36
|
+
PriorDraw,
|
|
37
|
+
ProposalMix,
|
|
38
|
+
UCBPrior,
|
|
39
|
+
WaveformResult,
|
|
40
|
+
ae_noise,
|
|
41
|
+
analytic_orbits,
|
|
42
|
+
analytic_snr,
|
|
43
|
+
arm_vectors,
|
|
44
|
+
detector_tensor,
|
|
45
|
+
draw_from_fisher,
|
|
46
|
+
fm_shift,
|
|
47
|
+
orbit_cache,
|
|
48
|
+
params_from_native,
|
|
49
|
+
polarization_tensor,
|
|
50
|
+
psi_phi_jump,
|
|
51
|
+
single_source_mix,
|
|
52
|
+
slow_time_grid,
|
|
53
|
+
snr_cut,
|
|
54
|
+
synthesize,
|
|
55
|
+
tdi_response,
|
|
56
|
+
template_on_window,
|
|
57
|
+
to_continuous_ft,
|
|
58
|
+
ucb_alignment,
|
|
59
|
+
ucb_bandwidth,
|
|
60
|
+
ucb_bw,
|
|
61
|
+
ucb_fdot,
|
|
62
|
+
ucb_prior,
|
|
63
|
+
ucb_waveform,
|
|
64
|
+
xyz_to_ae,
|
|
65
|
+
xyz_to_aet,
|
|
66
|
+
)
|
|
67
|
+
from binary_galactica.parameters import (
|
|
68
|
+
NUM_PARAMS,
|
|
69
|
+
PARAMETER_NAMES,
|
|
70
|
+
GBParams,
|
|
71
|
+
from_glass_vector,
|
|
72
|
+
to_glass_vector,
|
|
73
|
+
)
|
|
74
|
+
from binary_galactica.waveform import (
|
|
75
|
+
amplitudes,
|
|
76
|
+
frequency,
|
|
77
|
+
phase,
|
|
78
|
+
polarizations,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
__version__ = "0.5.0"
|
|
82
|
+
|
|
83
|
+
__all__ = [
|
|
84
|
+
"BACKENDS",
|
|
85
|
+
"DEFAULT_RESPONSE_SAMPLES",
|
|
86
|
+
"tdi_response",
|
|
87
|
+
"DAY",
|
|
88
|
+
"NUM_PARAMS",
|
|
89
|
+
"PARAMETER_NAMES",
|
|
90
|
+
"YEAR",
|
|
91
|
+
"GBParams",
|
|
92
|
+
"amplitudes",
|
|
93
|
+
"frequency",
|
|
94
|
+
"phase",
|
|
95
|
+
"polarizations",
|
|
96
|
+
"FORMATS",
|
|
97
|
+
"FSTAR",
|
|
98
|
+
"GLASS_TO_CONTINUOUS",
|
|
99
|
+
"LARM",
|
|
100
|
+
"SNR_MIN",
|
|
101
|
+
"UCB_MODEL_NP",
|
|
102
|
+
"Alignment",
|
|
103
|
+
"ExtrinsicPriorDraw",
|
|
104
|
+
"FisherCache",
|
|
105
|
+
"FisherProposal",
|
|
106
|
+
"PriorDraw",
|
|
107
|
+
"ProposalMix",
|
|
108
|
+
"UCBPrior",
|
|
109
|
+
"WaveformResult",
|
|
110
|
+
"ae_noise",
|
|
111
|
+
"analytic_orbits",
|
|
112
|
+
"analytic_snr",
|
|
113
|
+
"arm_vectors",
|
|
114
|
+
"detector_tensor",
|
|
115
|
+
"draw_from_fisher",
|
|
116
|
+
"fm_shift",
|
|
117
|
+
"orbit_cache",
|
|
118
|
+
"params_from_native",
|
|
119
|
+
"polarization_tensor",
|
|
120
|
+
"psi_phi_jump",
|
|
121
|
+
"single_source_mix",
|
|
122
|
+
"slow_time_grid",
|
|
123
|
+
"snr_cut",
|
|
124
|
+
"synthesize",
|
|
125
|
+
"template_on_window",
|
|
126
|
+
"to_continuous_ft",
|
|
127
|
+
"ucb_alignment",
|
|
128
|
+
"ucb_bandwidth",
|
|
129
|
+
"ucb_bw",
|
|
130
|
+
"ucb_fdot",
|
|
131
|
+
"ucb_prior",
|
|
132
|
+
"ucb_waveform",
|
|
133
|
+
"xyz_to_ae",
|
|
134
|
+
"xyz_to_aet",
|
|
135
|
+
"tdi_waveform",
|
|
136
|
+
"to_glass_vector",
|
|
137
|
+
"from_glass_vector",
|
|
138
|
+
"single",
|
|
139
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Physical constants and time units, in SI.
|
|
2
|
+
|
|
3
|
+
Values follow the LISA Data Challenge conventions. The GLASS backend keeps
|
|
4
|
+
its own pinned literals in :mod:`binary_galactica.glass.constants`; its
|
|
5
|
+
sidereal-year literal differs slightly from the physical unit exposed here.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
#: Sidereal year in seconds (LDC ``YRSID_SI``). LISA observation times are
|
|
11
|
+
#: conventionally quoted in sidereal years because the constellation's orbit is
|
|
12
|
+
#: tied to Earth's, so this -- not the Julian year -- is the natural unit.
|
|
13
|
+
YEAR: float = 31_558_149.763_545_603
|
|
14
|
+
|
|
15
|
+
#: Solar day in seconds.
|
|
16
|
+
DAY: float = 86_400.0
|
|
17
|
+
|
|
18
|
+
__all__ = ["DAY", "YEAR"]
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""LISA detector waveforms from the package's physical source parameters."""
|
|
2
|
+
|
|
3
|
+
from functools import partial
|
|
4
|
+
|
|
5
|
+
import jax
|
|
6
|
+
import jax.numpy as jnp
|
|
7
|
+
import numpy as np
|
|
8
|
+
from jax import Array
|
|
9
|
+
from jax.typing import ArrayLike
|
|
10
|
+
|
|
11
|
+
from binary_galactica.glass import _validation
|
|
12
|
+
from binary_galactica.glass.types import WaveformResult
|
|
13
|
+
from binary_galactica.glass.waveform import (
|
|
14
|
+
DEFAULT_RESPONSE_SAMPLES,
|
|
15
|
+
_options,
|
|
16
|
+
ucb_waveform,
|
|
17
|
+
)
|
|
18
|
+
from binary_galactica.parameters import GBParams, to_glass_vector
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def tdi_waveform( # noqa: N803 -- names follow the GLASS waveform API
|
|
22
|
+
theta: ArrayLike | GBParams,
|
|
23
|
+
*,
|
|
24
|
+
T: ArrayLike,
|
|
25
|
+
BW: int,
|
|
26
|
+
t0: ArrayLike = 0.0,
|
|
27
|
+
fmt: str = "sangria",
|
|
28
|
+
positions: ArrayLike | None = None,
|
|
29
|
+
backend: str = "onfly",
|
|
30
|
+
response_samples: int | None = DEFAULT_RESPONSE_SAMPLES,
|
|
31
|
+
) -> WaveformResult:
|
|
32
|
+
"""Generate the raw GLASS TDI spectrum for physical ``GBParams``.
|
|
33
|
+
|
|
34
|
+
This is the physical-coordinate counterpart of :func:`ucb_waveform`,
|
|
35
|
+
whose input array instead uses GLASS-native coordinates. The adapter
|
|
36
|
+
converts amplitude, phase, sky angles, inclination and time scales using
|
|
37
|
+
:func:`to_glass_vector`. It includes GLASS's first-order amplitude
|
|
38
|
+
evolution, which the original source-only ``polarizations`` omits.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
theta: One physical ``GBParams`` or ``(8,)`` physical array. Use
|
|
42
|
+
``jax.vmap`` to evaluate multiple sources. All fields must be
|
|
43
|
+
finite, ``amp`` and ``f0`` positive, ``iota`` in ``[0, pi]``,
|
|
44
|
+
and ``beta`` in ``[-pi/2, pi/2]``.
|
|
45
|
+
T: Positive observation duration in seconds. May be traced or
|
|
46
|
+
captured as a constant in surrounding JIT functions.
|
|
47
|
+
BW: Positive even number of slow samples and output bins. Static;
|
|
48
|
+
choose it by convergence for the intended analysis.
|
|
49
|
+
t0: Time of the first observation, in seconds from the source's
|
|
50
|
+
reference epoch. The parameters remain defined at time zero.
|
|
51
|
+
fmt: ``"sangria"``, ``"frequency"``, or ``"phase"`` TDI convention.
|
|
52
|
+
positions: Optional ``(3, BW, 3)`` spacecraft positions on the exact
|
|
53
|
+
``(T, t0, BW)`` slow grid, as returned by ``orbit_cache``.
|
|
54
|
+
On the on-fly path this cache is interpolated; its sampling limits
|
|
55
|
+
geometry accuracy. Omit it to evaluate analytic geometry directly.
|
|
56
|
+
backend: ``"onfly"`` (default) or the legacy ``"fourier"`` path.
|
|
57
|
+
response_samples: ``None`` selects a duration-dependent power-of-two
|
|
58
|
+
grid of 32–2048 samples, targeting 128 samples per year. An explicit
|
|
59
|
+
static integer of at least four fixes the count independently of
|
|
60
|
+
``BW``. Unused by the legacy backend. Pass an explicit count when
|
|
61
|
+
differentiating ``T`` across automatic grid thresholds: automatic
|
|
62
|
+
selection is piecewise and changes the interpolation grid.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
A dictionary with complex ``X/Y/Z/A/E`` arrays, ``qmin``, and absolute
|
|
66
|
+
``freq_bin_indices``. Channels have length ``BW`` and raw GLASS
|
|
67
|
+
normalization. ``to_continuous_ft(result, T)`` multiplies by sqrt(T).
|
|
68
|
+
|
|
69
|
+
Notes:
|
|
70
|
+
Derivatives treat the discrete carrier and bandwidth choices as fixed.
|
|
71
|
+
The backend regularizes the sky-pole coordinate singularity. Those
|
|
72
|
+
coordinate-boundary derivatives are not exact physical derivatives.
|
|
73
|
+
Invalid eager inputs raise ``ValueError``. Invalid values supplied
|
|
74
|
+
through JAX tracers yield NaN in every output; shapes are checked
|
|
75
|
+
during tracing as well. See :func:`ucb_waveform` for grid validation.
|
|
76
|
+
|
|
77
|
+
"""
|
|
78
|
+
_validation.require_x64()
|
|
79
|
+
response_samples = _options(backend, response_samples)
|
|
80
|
+
duration, t0, positions = _validation.observation(T, BW, t0, positions)
|
|
81
|
+
# Host scalar fields can be packed once by validation. Keep to_array's
|
|
82
|
+
# broadcasting and traced packing for array-valued fields.
|
|
83
|
+
if isinstance(theta, GBParams) and not all(np.isscalar(field) for field in theta):
|
|
84
|
+
theta = theta.to_array()
|
|
85
|
+
theta = _validation.finite_array(theta, "theta")
|
|
86
|
+
if theta.shape != (8,):
|
|
87
|
+
raise ValueError(f"theta must have shape (8,); got {theta.shape}")
|
|
88
|
+
if not isinstance(theta, jax.core.Tracer):
|
|
89
|
+
p = np.asarray(theta)
|
|
90
|
+
if p[0] <= 0 or p[1] <= 0:
|
|
91
|
+
raise ValueError("amp and f0 must be positive")
|
|
92
|
+
if not 0 <= p[4] <= np.pi:
|
|
93
|
+
raise ValueError("iota must lie in [0, pi]")
|
|
94
|
+
if not -np.pi / 2 <= p[7] <= np.pi / 2:
|
|
95
|
+
raise ValueError("beta must lie in [-pi/2, pi/2]")
|
|
96
|
+
return _tdi_waveform(
|
|
97
|
+
theta,
|
|
98
|
+
T=duration,
|
|
99
|
+
BW=int(BW),
|
|
100
|
+
t0=t0,
|
|
101
|
+
fmt=fmt,
|
|
102
|
+
positions=positions,
|
|
103
|
+
backend=backend,
|
|
104
|
+
response_samples=response_samples,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@partial(jax.jit, static_argnames=("BW", "fmt", "backend", "response_samples"))
|
|
109
|
+
def _tdi_waveform(
|
|
110
|
+
theta: Array,
|
|
111
|
+
*,
|
|
112
|
+
T: Array,
|
|
113
|
+
BW: int,
|
|
114
|
+
t0: Array,
|
|
115
|
+
fmt: str,
|
|
116
|
+
positions: Array | None,
|
|
117
|
+
backend: str,
|
|
118
|
+
response_samples: int | None,
|
|
119
|
+
) -> WaveformResult:
|
|
120
|
+
"""Convert physical coordinates inside JIT and preserve invalid domains."""
|
|
121
|
+
valid = (
|
|
122
|
+
jnp.all(jnp.isfinite(theta))
|
|
123
|
+
& (theta[0] > 0)
|
|
124
|
+
& (theta[1] > 0)
|
|
125
|
+
& (theta[4] >= 0)
|
|
126
|
+
& (theta[4] <= jnp.pi)
|
|
127
|
+
& (jnp.abs(theta[7]) <= jnp.pi / 2)
|
|
128
|
+
)
|
|
129
|
+
native = jnp.where(valid, to_glass_vector(theta, T), jnp.nan)
|
|
130
|
+
return ucb_waveform(
|
|
131
|
+
native,
|
|
132
|
+
T=T,
|
|
133
|
+
BW=BW,
|
|
134
|
+
t0=t0,
|
|
135
|
+
fmt=fmt,
|
|
136
|
+
positions=positions,
|
|
137
|
+
backend=backend,
|
|
138
|
+
response_samples=response_samples,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
__all__ = ["tdi_waveform"]
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Copyright 2019 Tyson B. Littenberg & Neil J. Cornish
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""GLASS-compatible galactic-binary TDI waveforms in JAX.
|
|
15
|
+
|
|
16
|
+
The default waveform constructs first-generation frozen-geometry XYZ directly
|
|
17
|
+
at sparse reception times, interpolates its demodulated analytic response, and
|
|
18
|
+
then Fourier transforms it. It follows the pinned GLASS
|
|
19
|
+
``LISA_spline_response`` / ``LISA_TDI_spline`` path from commit
|
|
20
|
+
``079c6a3a6fcf380a7c107f39640fff7fc720d0f1``. The explicit
|
|
21
|
+
``backend="fourier"`` option retains GLASS's earlier fast-slow / reduced-DFT
|
|
22
|
+
link-response construction for legacy reference comparisons.
|
|
23
|
+
|
|
24
|
+
Quick start
|
|
25
|
+
-----------
|
|
26
|
+
>>> import jax
|
|
27
|
+
>>> jax.config.update("jax_enable_x64", True)
|
|
28
|
+
>>> import binary_galactica.glass
|
|
29
|
+
>>> T, BW = 7884000.0, 64
|
|
30
|
+
>>> p = binary_galactica.glass.params_from_native(
|
|
31
|
+
... f0=1e-3, fdot=1e-18, amp=7.16e-22, phi0=0.5,
|
|
32
|
+
... colat=1.78, lon=0.0, psi=0.7, cos_iota=0.3, T=T)
|
|
33
|
+
>>> w = binary_galactica.glass.ucb_waveform(p, T=T, BW=BW)
|
|
34
|
+
>>> w["A"].shape
|
|
35
|
+
(64,)
|
|
36
|
+
|
|
37
|
+
The returned spectrum is in GLASS's amplitude convention and occupies
|
|
38
|
+
absolute frequency bins ``qmin ... qmin + BW - 1``. Use
|
|
39
|
+
:func:`binary_galactica.glass.convert.to_continuous_ft` for the continuous-Fourier
|
|
40
|
+
convention used elsewhere in this repository.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from __future__ import annotations
|
|
44
|
+
|
|
45
|
+
from .alignment import Alignment, ucb_alignment
|
|
46
|
+
from .bandwidth import ae_noise, analytic_snr, ucb_bandwidth, ucb_bw
|
|
47
|
+
from .compat import template_on_window
|
|
48
|
+
from .constants import FSTAR, LARM, UCB_MODEL_NP
|
|
49
|
+
from .convert import GLASS_TO_CONTINUOUS, to_continuous_ft
|
|
50
|
+
from .orbit import analytic_orbits, arm_vectors, slow_time_grid
|
|
51
|
+
from .prior import SNR_MIN, UCBPrior, snr_cut, ucb_fdot, ucb_prior
|
|
52
|
+
from .proposals import (
|
|
53
|
+
ExtrinsicPriorDraw,
|
|
54
|
+
FisherCache,
|
|
55
|
+
FisherProposal,
|
|
56
|
+
PriorDraw,
|
|
57
|
+
ProposalMix,
|
|
58
|
+
draw_from_fisher,
|
|
59
|
+
fm_shift,
|
|
60
|
+
psi_phi_jump,
|
|
61
|
+
single_source_mix,
|
|
62
|
+
)
|
|
63
|
+
from .response import detector_tensor, polarization_tensor
|
|
64
|
+
from .tdi import FORMATS, synthesize, xyz_to_ae, xyz_to_aet
|
|
65
|
+
from .types import WaveformResult
|
|
66
|
+
from .waveform import (
|
|
67
|
+
BACKENDS,
|
|
68
|
+
DEFAULT_RESPONSE_SAMPLES,
|
|
69
|
+
orbit_cache,
|
|
70
|
+
params_from_native,
|
|
71
|
+
tdi_response,
|
|
72
|
+
ucb_waveform,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
__all__ = [
|
|
76
|
+
"BACKENDS",
|
|
77
|
+
"DEFAULT_RESPONSE_SAMPLES",
|
|
78
|
+
"tdi_response",
|
|
79
|
+
"Alignment",
|
|
80
|
+
"FORMATS",
|
|
81
|
+
"FSTAR",
|
|
82
|
+
"GLASS_TO_CONTINUOUS",
|
|
83
|
+
"LARM",
|
|
84
|
+
"UCB_MODEL_NP",
|
|
85
|
+
"WaveformResult",
|
|
86
|
+
"ae_noise",
|
|
87
|
+
"analytic_orbits",
|
|
88
|
+
"analytic_snr",
|
|
89
|
+
"arm_vectors",
|
|
90
|
+
"detector_tensor",
|
|
91
|
+
"orbit_cache",
|
|
92
|
+
"params_from_native",
|
|
93
|
+
"ExtrinsicPriorDraw",
|
|
94
|
+
"FisherCache",
|
|
95
|
+
"FisherProposal",
|
|
96
|
+
"PriorDraw",
|
|
97
|
+
"ProposalMix",
|
|
98
|
+
"SNR_MIN",
|
|
99
|
+
"UCBPrior",
|
|
100
|
+
"draw_from_fisher",
|
|
101
|
+
"fm_shift",
|
|
102
|
+
"polarization_tensor",
|
|
103
|
+
"psi_phi_jump",
|
|
104
|
+
"single_source_mix",
|
|
105
|
+
"slow_time_grid",
|
|
106
|
+
"snr_cut",
|
|
107
|
+
"synthesize",
|
|
108
|
+
"ucb_fdot",
|
|
109
|
+
"ucb_prior",
|
|
110
|
+
"template_on_window",
|
|
111
|
+
"to_continuous_ft",
|
|
112
|
+
"ucb_alignment",
|
|
113
|
+
"ucb_bandwidth",
|
|
114
|
+
"ucb_bw",
|
|
115
|
+
"ucb_waveform",
|
|
116
|
+
"xyz_to_ae",
|
|
117
|
+
"xyz_to_aet",
|
|
118
|
+
]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Working precision for kernels that follow the dtype of their inputs.
|
|
2
|
+
|
|
3
|
+
Float64 inputs keep float64 arithmetic and results; float32 inputs compute
|
|
4
|
+
float32 and complex64 values throughout, even with x64 enabled. Python scalars
|
|
5
|
+
are weakly typed in JAX: they adopt the working precision without promoting
|
|
6
|
+
it. NumPy float64 values and default-dtype constructors are not, so kernels
|
|
7
|
+
cast them to the working dtype explicitly.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import jax
|
|
11
|
+
import jax.numpy as jnp
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
#: Precision of every contraction. The default lets float32 contractions use
|
|
15
|
+
#: TF32 on NVIDIA GPUs and reduced-precision passes on TPUs. CPU float64
|
|
16
|
+
#: ignores the setting, so float64 results do not change.
|
|
17
|
+
CONTRACTION = jax.lax.Precision.HIGHEST
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def real_dtype(*values) -> np.dtype:
|
|
21
|
+
"""Return the real working dtype of array-like ``values``, at least float32.
|
|
22
|
+
|
|
23
|
+
Complex values give their component dtype. Integer and boolean inputs, and
|
|
24
|
+
Python scalars on their own, give JAX's default floating dtype: float64
|
|
25
|
+
only when x64 is enabled.
|
|
26
|
+
"""
|
|
27
|
+
# Nested sequences have no dtype of their own; their elements decide it.
|
|
28
|
+
leaves = jax.tree_util.tree_leaves(values)
|
|
29
|
+
dtype = jnp.result_type(*leaves) if leaves else jnp.result_type(float)
|
|
30
|
+
if not jnp.issubdtype(dtype, jnp.inexact):
|
|
31
|
+
dtype = jnp.result_type(float)
|
|
32
|
+
return jnp.promote_types(jnp.finfo(dtype).dtype, jnp.float32)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def complex_dtype(*values) -> np.dtype:
|
|
36
|
+
"""Return the complex dtype with the precision of :func:`real_dtype`."""
|
|
37
|
+
return jnp.promote_types(real_dtype(*values), jnp.complex64)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def cast(value, dtype):
|
|
41
|
+
"""Return ``value`` in ``dtype``, leaving Python scalars weakly typed.
|
|
42
|
+
|
|
43
|
+
A Python ``int`` or ``float`` already adopts the precision of the arrays it
|
|
44
|
+
meets, so it is returned unchanged; arithmetic on it, such as ``T**3``,
|
|
45
|
+
stays in Python double precision.
|
|
46
|
+
"""
|
|
47
|
+
if type(value) in (int, float):
|
|
48
|
+
return value
|
|
49
|
+
return jnp.asarray(value, dtype=dtype)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Float32 pairs that carry about twice float32's precision.
|
|
2
|
+
|
|
3
|
+
A value is an unevaluated sum ``(hi, lo)`` of float32 arrays, as in
|
|
4
|
+
double-single arithmetic: products and quotients keep about 2**-46 relative
|
|
5
|
+
precision. Plain float32 resolves ``t/YEAR`` at multi-year times, or a
|
|
6
|
+
phase of hundreds of cycles, only to about 1e-6 cycles.
|
|
7
|
+
|
|
8
|
+
The textbook error-free product subtracts a rounded product from the exact
|
|
9
|
+
one. XLA may contract ``a*b + c`` into a fused multiply-add, as measured on
|
|
10
|
+
an arm64 CPU, which silently changes that correction. Products here are
|
|
11
|
+
instead formed from operands split into halves of at most 12 significant
|
|
12
|
+
bits, so every partial product is exact in float32 and fusing it into an
|
|
13
|
+
addition rounds the same exact sum. Additions use Knuth's two-sum, which has
|
|
14
|
+
no products. Splitting masks the low mantissa bits, so it involves no
|
|
15
|
+
arithmetic.
|
|
16
|
+
|
|
17
|
+
Derivatives pass through: split halves carry no tangent, a two-sum's error
|
|
18
|
+
term carries none, and each pair's derivative is that of the exact
|
|
19
|
+
expression, evaluated in float32.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import jax
|
|
25
|
+
import jax.numpy as jnp
|
|
26
|
+
import numpy as np
|
|
27
|
+
from jax import Array
|
|
28
|
+
|
|
29
|
+
#: An unevaluated sum ``hi + lo`` of float32 values.
|
|
30
|
+
Pair = tuple[Array, Array]
|
|
31
|
+
|
|
32
|
+
#: Clears the low 12 of float32's 23 stored mantissa bits.
|
|
33
|
+
_HIGH_BITS = np.int32(-(1 << 12))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def split(x) -> Pair:
|
|
37
|
+
"""Return ``(hi, lo)`` with ``hi + lo == x`` and at most 12 bits in each."""
|
|
38
|
+
x = jnp.asarray(x, dtype=jnp.float32)
|
|
39
|
+
bits = jax.lax.bitcast_convert_type(x, jnp.int32)
|
|
40
|
+
hi = jax.lax.bitcast_convert_type(bits & _HIGH_BITS, jnp.float32)
|
|
41
|
+
return hi, x - hi
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def two_sum(a, b) -> Pair:
|
|
45
|
+
"""Return the rounded sum and its exact rounding error (Knuth)."""
|
|
46
|
+
s = a + b
|
|
47
|
+
bb = s - a
|
|
48
|
+
return s, (a - (s - bb)) + (b - bb)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def pair(x) -> Pair:
|
|
52
|
+
"""Return float32 ``x`` as a pair with a zero low part."""
|
|
53
|
+
x = jnp.asarray(x, dtype=jnp.float32)
|
|
54
|
+
return x, jnp.zeros_like(x)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def constant(value) -> tuple[np.ndarray, np.ndarray]:
|
|
58
|
+
"""Round a host float64 constant or array to a float32 pair."""
|
|
59
|
+
value = np.asarray(value, dtype=np.float64)
|
|
60
|
+
hi = value.astype(np.float32)
|
|
61
|
+
return hi, (value - hi).astype(np.float32)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def product(x, y) -> Pair:
|
|
65
|
+
"""Return float32 ``x*y`` as a pair, from exact partial products."""
|
|
66
|
+
x1, x2 = split(x)
|
|
67
|
+
y1, y2 = split(y)
|
|
68
|
+
s, e1 = two_sum(x1 * y1, x1 * y2)
|
|
69
|
+
s, e2 = two_sum(s, x2 * y1)
|
|
70
|
+
return two_sum(s, (e1 + e2) + x2 * y2)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def add(a: Pair, b: Pair) -> Pair:
|
|
74
|
+
"""Return the pair sum ``a + b``."""
|
|
75
|
+
s, e = two_sum(a[0], b[0])
|
|
76
|
+
return two_sum(s, e + (a[1] + b[1]))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def multiply(a: Pair, b: Pair) -> Pair:
|
|
80
|
+
"""Return the pair product ``a*b``."""
|
|
81
|
+
hi, lo = product(a[0], b[0])
|
|
82
|
+
# The cross terms are about 2**-24 of the product; rounding them is harmless.
|
|
83
|
+
return two_sum(hi, lo + (a[0] * b[1] + a[1] * b[0]))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def divide(a, b) -> Pair:
|
|
87
|
+
"""Return float32 ``a/b`` as a pair, correcting the quotient's remainder."""
|
|
88
|
+
quotient = a / b
|
|
89
|
+
hi, lo = product(quotient, b)
|
|
90
|
+
# a - hi is exact because hi is within a rounding of a.
|
|
91
|
+
return two_sum(quotient, ((a - hi) - lo) / b)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def fraction(a: Pair) -> Pair:
|
|
95
|
+
"""Subtract the integer nearest ``a``, leaving a pair near ``[-0.5, 0.5]``."""
|
|
96
|
+
hi = a[0] - jnp.round(a[0])
|
|
97
|
+
return two_sum(hi, a[1])
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def value(a: Pair) -> Array:
|
|
101
|
+
"""Round a pair to float32."""
|
|
102
|
+
return a[0] + a[1]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
__all__ = [
|
|
106
|
+
"Pair",
|
|
107
|
+
"add",
|
|
108
|
+
"constant",
|
|
109
|
+
"divide",
|
|
110
|
+
"fraction",
|
|
111
|
+
"multiply",
|
|
112
|
+
"pair",
|
|
113
|
+
"product",
|
|
114
|
+
"split",
|
|
115
|
+
"two_sum",
|
|
116
|
+
"value",
|
|
117
|
+
]
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Eager boundary checks shared by the native and physical waveform APIs."""
|
|
2
|
+
|
|
3
|
+
import jax
|
|
4
|
+
import jax.numpy as jnp
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def require_x64():
|
|
9
|
+
"""Refuse to run a float64 entry point while JAX would compute in float32."""
|
|
10
|
+
# read() gives the effective value, including jax.experimental.enable_x64
|
|
11
|
+
# scopes, on every supported JAX; it never changes the setting.
|
|
12
|
+
if not jax.config.read("jax_enable_x64"):
|
|
13
|
+
raise RuntimeError(
|
|
14
|
+
"binary_galactica's float64 functions need JAX double precision, "
|
|
15
|
+
"which is disabled. Call "
|
|
16
|
+
'jax.config.update("jax_enable_x64", True) at startup, before '
|
|
17
|
+
"creating JAX arrays, or use binary_galactica.single for float32 "
|
|
18
|
+
"waveforms."
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def integer(value, name, *, minimum=None):
|
|
23
|
+
"""Require a concrete integer index/count without truncating or accepting bools."""
|
|
24
|
+
requirement = {
|
|
25
|
+
None: "an integer",
|
|
26
|
+
0: "a non-negative integer",
|
|
27
|
+
1: "a positive integer",
|
|
28
|
+
}[minimum]
|
|
29
|
+
if (
|
|
30
|
+
isinstance(value, (bool, np.bool_))
|
|
31
|
+
or not isinstance(value, (int, np.integer))
|
|
32
|
+
or (minimum is not None and value < minimum)
|
|
33
|
+
):
|
|
34
|
+
raise ValueError(f"{name} must be {requirement}; got {value!r}")
|
|
35
|
+
return int(value)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _contains_jax(value):
|
|
39
|
+
"""Detect JAX values and conversion protocols before packing sequences."""
|
|
40
|
+
if isinstance(value, (jax.Array, jax.core.Tracer)) or callable(
|
|
41
|
+
getattr(value, "__jax_array__", None)
|
|
42
|
+
):
|
|
43
|
+
return True
|
|
44
|
+
return isinstance(value, (list, tuple)) and any(_contains_jax(x) for x in value)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def finite_array(value, name, *, dtype=np.float64):
|
|
48
|
+
"""Validate floating inputs, keeping host values off device until the JIT call.
|
|
49
|
+
|
|
50
|
+
Values are cast to ``dtype``: float64 for the double-precision API and
|
|
51
|
+
float32 for :mod:`binary_galactica.single`. A host value too large for
|
|
52
|
+
float32 becomes infinite and is rejected like any other nonfinite input.
|
|
53
|
+
"""
|
|
54
|
+
# Packing host scalars with jnp dispatches before the compiled waveform.
|
|
55
|
+
# JAX arrays and nested tracers must stay in JAX to preserve placement/AD.
|
|
56
|
+
if _contains_jax(value):
|
|
57
|
+
value = jnp.asarray(value, dtype=dtype)
|
|
58
|
+
else:
|
|
59
|
+
with np.errstate(over="ignore"):
|
|
60
|
+
value = np.asarray(value, dtype=dtype)
|
|
61
|
+
if not isinstance(value, jax.core.Tracer) and not np.all(np.isfinite(value)):
|
|
62
|
+
raise ValueError(f"{name} must be finite")
|
|
63
|
+
return value
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def scalar(value, name, *, positive=False, dtype=np.float64):
|
|
67
|
+
"""Validate scalar shape during tracing, and concrete value eagerly."""
|
|
68
|
+
value = finite_array(value, name, dtype=dtype)
|
|
69
|
+
if value.ndim != 0:
|
|
70
|
+
raise ValueError(f"{name} must be a scalar; got shape {value.shape}")
|
|
71
|
+
if positive and not isinstance(value, jax.core.Tracer) and np.asarray(value) <= 0:
|
|
72
|
+
raise ValueError(f"{name} must be positive")
|
|
73
|
+
return value
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def observation(T, BW, t0, positions, *, dtype=np.float64):
|
|
77
|
+
"""Check the shared observation arguments at the public boundary."""
|
|
78
|
+
if (
|
|
79
|
+
isinstance(BW, bool)
|
|
80
|
+
or not isinstance(BW, (int, np.integer))
|
|
81
|
+
or BW <= 0
|
|
82
|
+
or BW % 2
|
|
83
|
+
):
|
|
84
|
+
raise ValueError(f"BW must be a positive even integer; got {BW!r}")
|
|
85
|
+
T = scalar(T, "T", positive=True, dtype=dtype)
|
|
86
|
+
t0 = scalar(t0, "t0", dtype=dtype)
|
|
87
|
+
if positions is not None:
|
|
88
|
+
positions = finite_array(positions, "positions", dtype=dtype)
|
|
89
|
+
if positions.shape != (3, BW, 3):
|
|
90
|
+
raise ValueError(
|
|
91
|
+
"positions must have shape (3, BW, 3); "
|
|
92
|
+
f"got {positions.shape} for BW={BW}"
|
|
93
|
+
)
|
|
94
|
+
return T, t0, positions
|