Skip to main content

binary-galactica

LISA TDI waveforms for galactic binaries, in JAX. The default onfly backend evaluates demodulated TDI X/Y/Z directly at sparse reception times, interpolates the response, restores the analytic source phase, and transforms the three channels. It follows GLASS's LISA_spline_response / LISA_TDI_spline conventions and returns a narrow band in X, Y, Z, A, and E. JAX provides compilation, batching, and derivatives at fixed discrete settings.

The response uses analytic LISA positions by default, with instantaneous arm lengths and geometry frozen at each reception time. This is the first-generation frozen-geometry response used by that GLASS path; it does not implement TDI-2 or moving-spacecraft sequential delays. The prior link-FFT implementation is available as the explicit legacy backend="fourier" option for reference comparisons.

The on-fly spline response and legacy fast-slow link response use reverse directed-link placement differently. Their finite-frequency spectra therefore are not expected to agree, even for static equal arms; they agree in the low-frequency limit. Independent pinned-spline C fixtures validate the new response, while legacy reference tests remain under explicit backend="fourier". See the on-fly reference provenance and regeneration guide.

Install

Install from a source checkout:

git clone https://github.com/AaronDJohnson/binary_galactica.git
cd binary_galactica
python -m pip install .

For development, use an editable installation with the test and lint tools:

python -m pip install -e ".[dev]"

Python >=3.10, JAX >=0.4.33, and NumPy >=1.24 are required.

Migrating to 0.5.0: the GLASS-native amplitude exp(params[3]) is now the physical strain amplitude, the LDC/Sangria catalogue Amplitude. Through 0.4.0 every native output was half the physical signal: ucb_waveform, template_on_window, and tdi_response, on both backends and in binary_galactica.single. For the same vector they now return exactly twice their 0.4.0 values and match Sangria data. A 0.4.0 native vector v gives the same signal in 0.5.0 with v[3] - ln(2). The physical GBParams API is unchanged: tdi_waveform returns the same signal, while to_glass_vector now writes ln(amp) rather than ln(2*amp) and from_glass_vector reads amp = exp(v[3]). Native callers change meaning by that factor of two, for example petra_catalogs (petra/binary_galactica.py) with parameterization="glass". Saved native templates, SNRs, and amplitude posteriors need an explicit decision. See the physical-amplitude spec.

Migrating to 0.4.0: importing the package no longer enables jax_enable_x64. The float64 functions need it, because long-duration millihertz phases require double precision. Enable it yourself at startup, before creating JAX arrays:

import jax

jax.config.update("jax_enable_x64", True)

While it is disabled, ucb_waveform, tdi_waveform, tdi_response, template_on_window, orbit_cache, phase, frequency, amplitudes, and polarizations raise RuntimeError instead of computing in float32. Lower-level building blocks such as analytic_orbits, slow_time_grid, to_glass_vector, and from_glass_vector now follow their input dtype: float32 inputs give float32 results even with x64 enabled. Processes that keep x64 disabled can use the float32 functions in binary_galactica.single.

Generate a GLASS waveform

import jax

import binary_galactica as bg

jax.config.update("jax_enable_x64", True)

T, BW = 7_884_000.0, 512
p = bg.params_from_native(
    f0=1e-3,
    fdot=1e-18,
    amp=7.16e-22,
    phi0=0.5,
    colat=1.78,
    lon=0.0,
    psi=0.7,
    cos_iota=0.3,
    T=T,
)
wave = bg.ucb_waveform(p, T=T, BW=BW)  # backend="onfly", automatic response grid
frequencies = wave["freq_bin_indices"] / T
A, E = wave["A"], wave["E"]  # complex arrays of length BW
continuous = bg.to_continuous_ft(wave, T)

BW is the positive, even number of returned frequency bins. It controls the FFT band, while response_samples independently controls the sparse response. The default None selects a power of two from 32 through 2048, targeting at least 128 samples per observation year: 32 for a quarter year, 128 for a year, and 512 for four years. Set an integer of at least four to fix the grid. The automatic policy is a starting point, not an error bound; observations beyond 16 years particularly need an explicit convergence check. Choose both grids by convergence for the analysis: increasing BW widens the band and does not refine the on-fly response; increasing response_samples refines that interpolation and does not change the number of returned bins. ucb_alignment(p, T, N) and ucb_bw(...) retain the GLASS bandwidth heuristic, but it is not an accuracy guarantee.

The sparse response removes the phase at spacecraft 0's retarded source time, including the common orbital Doppler phase. Local polynomials through up to eight complex samples interpolate the remaining smooth response. This avoids time-derivative evaluations and singular amplitude/phase coordinates at zeros. The reference phase is restored at the FFT times. The Hermite interpolation utility remains available internally for numerical comparisons.

For correctness, omit positions and use the analytic orbit. Legacy position caches with shape (3, BW, 3) remain accepted and are interpolated cubically to response times, but a coarse cache limits orbit accuracy. All source parameters refer to time zero; t0 moves the observing interval, not the parameter reference epoch.

Native parameters

The waveform entry points use the GLASS-native vector:

[f0*T, cos(colat), longitude, ln(A_G), cos(iota), psi, phi0_G,
 fdot*T**2, fddot*T**3]

A_G is the physical strain amplitude: the LDC/Sangria catalogue Amplitude, and the value GLASS's amplitude() returns. On both backends the polarizations are h+ = A_G*(1 + cos(iota)**2) and hx = 2*A_G*cos(iota).

ucb_waveform and template_on_window accept 7, 8, or 9 entries; the eighth and ninth chirp terms are optional. ucb_alignment requires at least eight entries because it reads fdot. UCBPrior and the standard single-source proposal mix are eight-dimensional.

params_from_native is a NumPy host packing helper that returns eight entries unless d2fdt2 is supplied. Use JAX operations when constructing vectors inside a differentiated function.

Supported TDI formats are "sangria" (default), "frequency", and "phase". They reproduce GLASS's conventions, including conjugation of A/E in the phase format. Nominal arm length and transfer frequency are fixed to the pinned constants. backend="fourier" retains the old finite-band link-FFT response; it is useful for exact legacy-reference comparisons.

Arbitrary-time response

tdi_response exposes the same arbitrary-time native response in its documented source-phase-demodulated convention:

import jax.numpy as jnp

times = T * jnp.array([0.0, 0.25, 0.5, 0.75])
xyz = bg.tdi_response(p, times, T=T)  # complex array, shape (3, 4)

It is in Sangria analytic-response units: it is the raw internal GLASS spline response, before FFT normalization. After the source phase is restored, its real part is the Sangria fractional-frequency TDI signal. The waveform restores the full analytic source phase before transforming, and its raw spectrum retains the usual sqrt(T) conversion described below. times is one-dimensional. An optional position array has shape (3, n_times, 3); use analytic positions unless an existing cache must be reused.

Normalization

ucb_waveform returns raw GLASS Fourier coefficients:

h_continuous = sqrt(T) * h_GLASS

With a compatible one-sided PSD, continuous coefficients use the inner product 4 * (1/T) * sum(conj(a) * b / PSD), taking the real part as appropriate. For a bare NumPy/JAX rfft convention, also divide continuous coefficients by the time-domain sample interval.

The on-fly Fourier prefactor is sqrt(T) / (2*BW) for the raw GLASS spline response, which the public tdi_response returns unscaled. The factor of one half selects the positive-frequency contribution of the real time signal. to_continuous_ft remains sqrt(T). With these conventions, continuous coefficients divided by the sample interval match the in-band rfft of Sangria/LDC time-domain TDI generated from the same catalogue Amplitude. No conversion factor or SNR target compensates for a convention change; reusing saved templates requires an explicit convention decision.

Physical parameters and source polarizations

GBParams keeps the physical ordering (amp, f0, fdot, phi0, iota, psi, lam, beta). Pass it to tdi_waveform:

params = bg.GBParams(
    amp=1e-22,
    f0=1e-3,
    fdot=1e-18,
    phi0=0.5,
    iota=1.1,
    psi=0.7,
    lam=0.0,
    beta=-0.2,
)
physical_wave = bg.tdi_waveform(params, T=T, BW=BW, response_samples=512)
native = bg.to_glass_vector(params, T)
restored = bg.from_glass_vector(native, T)

The physical and native eight-element arrays cannot be distinguished by shape. Use tdi_waveform for physical parameters and ucb_waveform for native ones. The adapter makes their differences explicit:

Quantity Physical GBParams GLASS-native
Amplitude Full inclination factors times amp A_G = amp (LDC Amplitude), same full factors
Initial phase +phi0 in the phase function phi0_G = -phi0
Sky latitude beta cos(colat) = sin(beta)
Inclination iota cos(iota)
Frequency and chirp Hz, Hz/s f0*T, fdot*T**2

The inverse accepts only eight parameters and returns canonical inclination [0, pi] and latitude [-pi/2, pi/2]. It never silently discards a ninth, second-derivative parameter.

The original phase, frequency, amplitudes, and polarizations helpers remain available, including imports from binary_galactica.waveform. They generate constant-amplitude barycentric source polarizations. The detector model additionally applies GLASS's first-order amplitude evolution, 1 + (2/3)*(fdot/f0)*t, evaluated at retarded times. Source-only polarizations are not substitutes for detector channels.

JAX and fixed analysis windows

import jax
import jax.numpy as jnp

window = bg.template_on_window(
    p,
    T=T,
    BW=BW,
    bin_lo=int(wave["qmin"]),
    n_bins=BW,
)  # (2, BW): A/E in continuous Fourier units


def channels(native, duration):
    result = bg.ucb_waveform(native, T=duration, BW=BW)
    return jnp.stack([result["A"], result["E"]])


jacobian = jax.jacfwd(channels, argnums=0)(jnp.asarray(p), T)
catalogue = jnp.stack([jnp.asarray(p), jnp.asarray(p)])
batch = jax.jit(jax.vmap(channels, in_axes=(0, None)))(catalogue, T)

The fixed-window helper zero-fills missing bins and drops bins outside the requested window. Choose the window wide enough for the analysis. bin_lo must be an integer and n_bins a non-negative integer; fractional values, booleans, and nonfinite values raise ValueError. A zero-length window returns an empty (2, 0) array.

T may be a traced argument or a constant captured by an outer JIT wrapper. The carrier calculation preserves GLASS's float64 rounding in either case. Pass T as an argument when varying or differentiating the duration without recompiling. BW, response_samples, backend, fmt, bin_lo, and n_bins are static. ucb_waveform(..., q=...) can explicitly pin the carrier. Automatic response counts change discretely with duration. AD follows the selected grid; use an explicit response_samples when differentiating across a duration threshold. For batches with different durations, grouping by duration or using an explicit count avoids evaluating multiple automatic grid branches.

Ordinary ucb_waveform and tdi_waveform calls validate inputs and raise ValueError for invalid durations, frequencies, angles, carrier overrides, or orbit arrays. A carrier override must be a finite integer-valued scalar. Physical tdi_waveform inputs use inclination in [0, pi] and latitude in [-pi/2, pi/2]; native sky and inclination cosines must lie in [-1, 1].

Inside jit or vmap, invalid traced values produce NaN outputs, including bin metadata, because Python exceptions cannot inspect those values during tracing. Validate catalogue values before a compiled bulk calculation and check results for finiteness. Static shape and format errors still raise during tracing. Results retain the existing dictionary keys and are described by the exported WaveformResult type for editor and type-checker support.

Derivatives do not differentiate bandwidth selection or carrier jumps. The removable sinc singularity is guarded. At sky-coordinate poles, the backend regularizes singular coordinate derivatives to zero. These intentionally differ from raw C at singular inputs.

See the performance guide for repeated-call and catalogue patterns and a reproducible benchmark of waveform, fixed-window, batch, and Jacobian workloads. The benchmark separates first-call compilation from warmed execution and waits for all returned arrays.

Single precision

binary_galactica.single is a float32 implementation of the on-fly waveform for processes that keep jax_enable_x64 disabled, such as float32 analysis pipelines or float32-only accelerators. Its ucb_waveform, template_on_window, tdi_response, and tdi_waveform mirror the float64 functions, use only the on-fly response, and compute and return only float32, complex64, and int32 values, with x64 enabled or not. The float64 functions remain the reference implementation.

Float32 cannot hold f0*T, which reaches millions of cycles. Each source is therefore an integer reference bin q_ref and float32 coordinates whose frequency entry is the offset from that bin: native params[0] is f0*T - q_ref, and the physical tdi_waveform frequency field is f0 - q_ref/T in hertz. The host NumPy helpers split_native and split_physical convert float64 priors, proposals, and catalogues, choosing q_ref = floor(f0*T):

import binary_galactica as bg
from binary_galactica import single as bg32

T, BW = 7_884_000.0, 512
native = bg.params_from_native(  # float64 NumPy, as from a prior or catalogue
    f0=1e-3,
    fdot=1e-18,
    amp=7.16e-22,
    phi0=0.5,
    colat=1.78,
    lon=0.0,
    psi=0.7,
    cos_iota=0.3,
    T=T,
)
q_ref, params = bg32.split_native(native, T)  # Python int, float32 array
wave32 = bg32.ucb_waveform(params, q_ref=q_ref, T=T, BW=BW)
A32, E32 = wave32["A"], wave32["E"]  # complex64, raw GLASS units
bins32 = wave32["freq_bin_indices"]  # int32

q_ref is a Python integer or an int32 scalar. It may be traced, or batched with jax.vmap alongside the parameters; inside jit, pass numpy.int32 values, because with x64 enabled a traced Python integer is int64 and is rejected rather than wrapped. The carrier defaults to q_ref + floor(params[0]). The float64 carrier reproduces GLASS rounding and can differ by one bin at an exact boundary, so pin it with q= when comparing the two. qmin and freq_bin_indices are exact int32; the other keys and the format conventions match the float64 results. Eager invalid inputs raise ValueError; traced invalid values give NaN channels and INT32_MIN bins.

Against float64 at identical inputs, the recorded diagnostic measures A/E relative L2 errors of at most 1.2e-5 and mismatches of at most 7.7e-11, for targets of 1e-4 and 1e-8. It covers the corners of the validated range: T of 0.25 and 10 years, f0 of 0.1 and 30 mHz, and t0 of 0 and 10 years, with sources drifting four bins through fdot and one through fddot in every sign combination, all three formats, and runs with and without position caches. Stress cases add chirps driven by gravitational radiation, with chirp masses of 0.3 and 1 solar mass and fdot*T**2 of 94 to 20,000 bins, carriers pinned mid-observation up to 30,525 bins from the default, and a carrier 1,000 bins from a steady source. The physical adapter reaches 1.1e-5 and the arbitrary-time response 9.2e-6. Rounding f0*T itself to float32 instead gives a relative L2 error of 1.9 at ten years and 30 mHz in the same record. These are measurements, not error bounds; tools/diagnose_single.py reproduces them.

Only the frequency is split. The other float32 entries are inputs like any other, so converting a float64 source rounds them once, and for a strong chirp that input rounding can exceed the arithmetic error. Rounding fdot*T**2 moves the phase by up to about 6e-8 of its value times (t/T)**2/2 cycles, so the effect grows with the drift and at late epochs. The same record splits its stress chirps from unrounded float64 values and compares them with float64 at those values: split_native gave relative L2 errors of 4.7e-5 and 6.2e-5 at t0 = 0, and 2.9e-3 and 5.2e-3 at t0 = 10 years; split_physical, run at t0 = 0 only, gave up to 6.6e-5. Parameters referred to a time near the observation, with a small t0, avoid the late-epoch growth.

Raw channels near 1e-21 are representable in float32, but their squares, near 1e-42, are below its smallest normal number, about 1.2e-38. So is a millihertz A/E noise PSD: ae_noise gives 5e-42 at 4 mHz. Form inner products from whitened channels, dividing by an amplitude spectral density computed on the host, or rescale: the waveform is linear in amplitude, so subtracting log(s) from native params[3], or dividing physical amp by s, scales every channel by 1/s. tdi_response values near 1e-24 need the same care. examples/single_precision.py shows both approaches with x64 disabled.

Position caches keep the (3, BW, 3) layout on the (T, t0, BW) grid. Host float64 or float32 caches are rounded to float32, whose step near 1.5e11 m is about 5e-5 s of light time. orbit_cache is a float64 function and raises in an x64-disabled process, which can instead load a saved cache. Omitting positions evaluates the analytic orbit, with its phase reduced in paired float32 arithmetic.

On the recorded CPU benchmark, float32 calls took 0.62 to 1.23 times as long as float64 ones: similar cost, not a large speedup. See the performance guide. Accelerator timings have not been measured.

Supporting modules

The modules under binary_galactica.glass provide constants, orbit, response, on-fly interpolation, waveform generation, TDI conversion, bandwidth, alignment, Fourier conversion, fixed-window placement, priors, and proposals. Priors/proposals use NumPy on the host and native coordinates. They are building blocks rather than an inference engine: likelihoods, fitted noise, trans-dimensional searches, and WDM integration remain outside this package.

The Fisher helper preserves the donor's one-eigendirection draw, but uses two safer guards: non-finite draws and correlated jumps reaching either cosine boundary become whole-vector self-proposals. This intentionally differs from GLASS's deterministic one-percent fallback and component-only boundary revert; it does not affect waveform parity. Its zero proposal correction is valid for a Fisher decomposition fixed independently of chain state. State-dependent cache refresh remains adaptation; freeze covariance for production sampling or separately justify the adaptive scheme or reverse-density correction.

For a fixed covariance, a proposal loop can use the root-level helpers:

import numpy as np

prior = bg.ucb_prior(T=T, fmin=0.0009, fmax=0.0011)
rng = np.random.default_rng(12)
state = prior.sample(rng)
# Illustrative fixed scales: use a measured Fisher decomposition in an analysis.
fixed_eigenvalues = 1 / (0.01 * (prior.hi - prior.lo)) ** 2
fixed_eigenvectors = np.eye(8)
mix = bg.single_source_mix(
    prior=prior,
    fisher=lambda _: (fixed_eigenvalues, fixed_eigenvectors),
    T=T,
)
candidate, log_q_ratio, proposal_name = mix(state, rng)
candidate = prior.wrap(candidate)
log_prior = prior.log_prior(candidate)
# Use log_prior and log_q_ratio with your likelihood and acceptance rule.

FisherCache counts Fisher calls, not total mixture steps. With the default mixture, only about 70% of steps use Fisher proposals, so refreshing every 80 calls does not reproduce GLASS's refresh every 80 total sampler steps. The example's fixed decomposition avoids state-dependent adaptation.

Validation

pytest
ruff check .
python examples/quickstart.py
python examples/single_precision.py

The test configuration enables x64 for the session. A float64 golden archive, recorded after the 0.5.0 physical-amplitude fix, must reproduce bitwise on its recording JAX, NumPy, and CPU build, and to 1e-12 of each output's peak elsewhere. The float32 tests compare the single waveform, window, arbitrary-time response, and physical adapter with float64 at identical inputs, check that no float64 value enters their compiled programs, and run every single function in a subprocess with x64 disabled.

Legacy Fourier C reference tests cover varied sky positions, inclinations, amplitudes, phases, 7/8/9 parameters, all three formats, signed chirps, quarter-year through four-year observations, and carrier boundaries. Each of the five channels must agree to relative L2 error 1e-9 with twice the stored spectra, whose pinned polarization factors are halved. Independently of those fixtures, tests/test_physical_amplitude.py pins the physical amplitude through the physical API and a long-wavelength TDI limit. Normal tests need no C compiler, network, or sibling repository, and missing references fail.

Separate pinned-spline fixtures cover the arbitrary-time on-fly XYZ response; their offline provenance and regeneration instructions keep that validation independent of the Python implementation. Legacy finite-band reference checks select backend="fourier" explicitly.

Provenance and offline regeneration document the pinned, unchanged upstream routines and vendored FFT source. Additional tests cover conversions, normalized derivatives, batching, singular guards, window placement, and donor helper behavior. These checks establish agreement with the pinned finite-band analytic-orbit calculation, not convergence for every source or a full GLASS pipeline.

CI runs the suite on Python 3.10 with the minimum runtime dependencies, Python 3.11–3.14 with current compatible dependencies, and macOS with Python 3.12. It also checks lint, formatting, distribution metadata, and the installed wheel from outside the checkout. The workflow runs on pushes and pull requests.

To reproduce the minimum-dependency check in a clean Python 3.10 environment:

python -m pip install -c requirements/minimum.txt -e ".[dev]"
python -m pytest

The quality review follow-up records executed compatibility checks, resolved findings, and remaining release verification.

The TDI zero-crossing prototype compares bounded Brent minimization with projected root finders for a callable complex response. Its findings informed the response interpolation; it remains a separate reproducible experiment.

Attribution

The mainline method follows Cornish & Littenberg, TDI on the fly (2025), with this package's JAX implementation and local complex-polynomial interpolation. The legacy Fourier calculation follows Cornish & Littenberg, Phys. Rev. D 76, 083006 (2007). GLASS is by Tyson B. Littenberg, Neil J. Cornish, and contributors.

Citing

If you use binary-galactica, please cite it with the metadata in CITATION.cff (GitHub's "Cite this repository" button exports BibTeX and APA), and also cite GLASS and the papers its methods come from:

@misc{glass,
  author       = "Tyson B. Littenberg, Neil J. Cornish",
  title        = "GLASS",
  howpublished = "free software (Apache 2.0)",
  doi          = "10.5281/zenodo.2026177",
  year         = "2025"
}

@article{LittenbergCornish2023,
  author  = "Littenberg, Tyson B. and Cornish, Neil J.",
  title   = "Prototype global analysis of LISA data with multiple source types",
  journal = "Phys. Rev. D",
  volume  = "107",
  pages   = "063004",
  year    = "2023",
  doi     = "10.1103/PhysRevD.107.063004",
  eprint  = "2301.03673",
  archivePrefix = "arXiv"
}

@article{CornishLittenberg2025,
  author  = "Cornish, Neil J. and Littenberg, Tyson B.",
  title   = "TDI on the fly",
  year    = "2025",
  eprint  = "2506.08093",
  archivePrefix = "arXiv"
}

@article{CornishLittenberg2007,
  author  = "Cornish, Neil J. and Littenberg, Tyson B.",
  title   = "Tests of Bayesian model selection techniques for gravitational
             wave astronomy",
  journal = "Phys. Rev. D",
  volume  = "76",
  pages   = "083006",
  year    = "2007",
  doi     = "10.1103/PhysRevD.76.083006",
  eprint  = "0704.1808",
  archivePrefix = "arXiv"
}

License

binary-galactica is licensed under the Apache License, Version 2.0, Copyright 2026 Aaron D. Johnson, Tyson B. Littenberg, Neil J. Cornish, and Robert Rosati. Portions adapted from GLASS keep their upstream Apache-2.0 copyright notices; see NOTICE. The optional C reference tool under tools/glass_reference vendors KissFFT (BSD-3-Clause), which is not a runtime dependency.

Metadata

Release files for binary-galactica 0.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for binary-galactica 0.5.0
File Size Uploaded
binary_galactica-0.5.0.tar.gz 1.4 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for binary-galactica 0.5.0
File Interpreter ABI Platform
binary_galactica-0.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 1.5 MB

Release files / binary_galactica-0.5.0.tar.gz

Download URL binary_galactica-0.5.0.tar.gz
Size 1.4 MB
Tags Source
SHA-256 checksum
How to use checksums
6003d9cff70703dbbeaac7116c50815642b8a1150b48b0d6075ac5783aff7fd0
BLAKE2b-256 checksum
How to use checksums
bbcf833de34bb01d6451d0e9867a7112544c56a318130af2c0ce0fe7661f6773
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 13, 2026.

Transparency log

Release files / binary_galactica-0.5.0-py3-none-any.whl

Download URL binary_galactica-0.5.0-py3-none-any.whl
Size 108.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
359a4caf8885faabeab1c6f297c32a4f44d89cc6bc33cdddc6ee0c21060d78c3
BLAKE2b-256 checksum
How to use checksums
ac36314e681315af504706a2344af30031186f9e0a1372a2ec1b11f0ef37e4f8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 13, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.5.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page