Development documentation · 0.2.3 30c2f8ba · Intro examples tested with 0.2.3 · Version details · Known limitations

GWexpy for GWpy Users#

This page is the entry point for moving from GWpy to GWexpy.
It is not intended to be a full API catalog. The goal is to understand, quickly, what still works as-is and where the high-value differences start.

If you want a difference-oriented index of added APIs, see GWpy Difference API Index.
If you want the full API surface regardless of GWpy compatibility, use the API Reference.

The project-wide GWpy compatibility policy requires corresponding GWpy APIs to preserve default finite numerical results, sample selection, axis information, and successful completion. Use GWexpy-specific APIs or explicit opt-in options when you intentionally want different numerical behavior. The policy’s sole named, human-approved safety exception, non_intersecting_window_safety, covers a completely disjoint HDF5 read window and must satisfy every policy gate; it does not authorize broader divergence.

Check the documentation version against your installed package and read Known limitations before migrating. The recipes below assume your existing inputs, such as cache and channels. For a complete synthetic example that needs no external data, run the Quickstart.

What to lock in first#

  • Single-channel workflows are often still familiar. For TimeSeries / FrequencySeries / Spectrogram, many basic read, plot, and spectral-analysis paths can be tried first by swapping imports to gwexpy.

  • The biggest shift is multi-channel processing. Instead of manually looping over TimeSeriesDict, you can convert it with to_matrix() and treat the channel set as a batch-analysis container.

  • The Field API is a GWexpy-only extension. ScalarField and FieldList / FieldDict cover spatially indexed data and batch operations across multiple fields, which GWpy does not provide as a standard layer.

  • Some external-library calls move onto the data object itself. Representative examples are .find_peaks(), .fit(), .hht(), and .arima().

  • Direct I/O and interop have their own guides. Treat File I/O Supported Formats Guide as the source of truth for read(..., format=...) / write(..., format=...), and Interop / Conversion Guide as the source of truth for to_*() / from_*() conversions.

Where migration usually pays off first#

Goal

First difference to check

Where to go next

Analyse many channels together

TimeSeriesDict.to_matrix() -> TimeSeriesMatrix

Matrix Tutorial

Work with spatially indexed data or multiple fields together

ScalarField, FieldList, FieldDict

Field API Intro, GWpy Difference API Index

Reduce direct SciPy / Statsmodels plumbing

added object-level APIs

GWpy Difference API Index

Move existing single-channel code quickly

swap imports first, then add difference APIs only where needed

Quickstart

Understand result-sharing behavior

Transparent Pickle compatibility

GWpy Difference API Index

Recipe 1: Move manual TimeSeriesDict loops toward TimeSeriesMatrix#

In GWpy-style code, pairwise comparisons or per-channel spectral logic often become explicit loops.
In GWexpy, to_matrix() is the main entry point for treating the whole channel set as a batch-analysis object.

GWpy style#

from gwpy.timeseries import TimeSeriesDict

tsd = TimeSeriesDict.read(cache, channels)
reference = tsd["H1:STRAIN"]

csd = {}
for name, ts in tsd.items():
    if name == "H1:STRAIN":
        continue
    csd[name] = ts.csd(reference, fftlength=4)

GWexpy style#

from gwexpy.timeseries import TimeSeriesDict

tsd = TimeSeriesDict.read(cache, channels)
matrix = tsd.to_matrix()
reference = tsd["H1:STRAIN"]

csm = matrix.csd(reference, fftlength=4)
csm.plot().show()

This difference matters when:

  • you want fewer manual loops and more object-level batch operations

  • you want multi-channel analysis to stay in TimeSeriesMatrix / FrequencySeriesMatrix

Related pages:

Recipe 2: Pull external function calls back onto the data object#

In GWpy-based code, the natural pattern is often “extract arrays, then call SciPy or Statsmodels directly.”
In GWexpy, part of that workflow is exposed as methods on the data object itself.

GWpy style#

import numpy as np
from scipy.signal import find_peaks
from gwpy.frequencyseries import FrequencySeries

spec = FrequencySeries(...)
peaks, props = find_peaks(np.asarray(spec.value), height=0.2)

GWexpy style#

from gwexpy.frequencyseries import FrequencySeries

spec = FrequencySeries(...)
peaks, props = spec.find_peaks(threshold=0.2)

The same direction applies to other added APIs in gwexpy, including:

  • .fit() for object-level fitting workflows

  • .hht() for Hilbert-Huang Transform analysis

  • .arima() for time-series modelling and forecasting

Related pages:

Recipe 3: Single-channel code often does not need a full rewrite#

If you already know GWpy’s base classes, you do not need to redesign everything up front.
In many cases, the practical migration path is: change imports first, then adopt GWexpy-specific APIs only where they help.

GWpy style#

from gwpy.timeseries import TimeSeries

ts = TimeSeries.read("data.gwf", "H1:STRAIN")
asd = ts.asd(fftlength=4)
asd.plot().show()

GWexpy style#

from gwexpy.timeseries import TimeSeries

ts = TimeSeries.read("data.gwf", "H1:STRAIN")
asd = ts.asd(fftlength=4)
asd.plot().show()

Practical reading of this difference:

  • start by carrying over existing single-channel workflows

  • add GWexpy-only APIs later, where multi-channel or higher-level analysis benefits matter

Related pages:

Recipe 4: Group ScalarField objects with FieldList / FieldDict#

Typical GWpy migration paths focus on series-like objects, and GWpy does not provide a standard container layer for field-like data with shared spatial metadata. In GWexpy, you can keep a single field in ScalarField and manage multiple related fields through FieldList / FieldDict.

GWpy style#

import numpy as np

field_a = np.random.randn(8, 3, 3, 3)
field_b = np.random.randn(8, 3, 3, 3)

fields = {"before": field_a, "after": field_b}

# axis metadata and units must be tracked separately

GWexpy style#

import numpy as np
from gwexpy.fields import ScalarField, FieldDict

fields = FieldDict(
    {
        "before": ScalarField(np.random.randn(8, 3, 3, 3)),
        "after": ScalarField(np.random.randn(8, 3, 3, 3)),
    },
    validate=True,
)

fft_fields = fields.fft_space_all()

This difference matters when:

  • you want multiple fields to share unit/axis/domain checks instead of managing metadata by hand

  • you want spatial transforms or selections to stay at the collection level rather than writing field-by-field loops

Related pages:

Recipe 5: Read Pickle sharing in terms of compatibility#

GWexpy is designed with result sharing in mind, including cases where the receiver does not have GWexpy installed.
The point here is not generic Pickle safety advice, but what a GWpy user can expect operationally.

GWpy style#

import pickle
from gwpy.timeseries import TimeSeries

ts = TimeSeries(...)

with open("result.pkl", "wb") as f:
    pickle.dump(ts, f)

# sharing assumes the receiving side can read the same object type

GWexpy style#

import pickle
from gwexpy.timeseries import TimeSeries

ts = TimeSeries(...)

with open("result.pkl", "wb") as f:
    pickle.dump(ts, f)

# the receiving side can restore it as a GWpy base object even without GWexpy

Important

Only load Pickle data from trusted sources.

Related pages:

Recipe 6: Bridge to python-control#

In GWpy-based code, using control.bode() or similar functions requires manually extracting a FrequencySeries value as a numpy array and passing it to control.frd().
In GWexpy, to_control_frd() handles the conversion in one call, removing that boilerplate.

GWpy style#

import control
import numpy as np
from gwpy.frequencyseries import FrequencySeries

spec = FrequencySeries.read("data.hdf5", "H1:STRAIN_ASD")

# must manually extract numpy arrays before building the FRD object
omega = 2 * np.pi * np.asarray(spec.frequencies.value)
frd_sys = control.frd(np.asarray(spec.value), omega)

# draw Bode plot
control.bode(frd_sys)

GWexpy style#

import control
from gwexpy.frequencyseries import FrequencySeries

spec = FrequencySeries.read("data.hdf5", "H1:STRAIN_ASD")

# one-line conversion and round-trip
frd_sys = spec.to_control_frd()                      # FrequencySeries → control.FrequencyResponseData
control.bode(frd_sys)                                # pass directly to Bode / Nichols / Nyquist

spec2 = FrequencySeries.from_control_frd(frd_sys)    # control.FRD → FrequencySeries

This difference matters when:

  • you want to treat an ASD / PSD spectrum directly as a control-system FRD model

  • you want to skip manual numpy extraction before passing data to control.bode() / control.nichols() / control.nyquist()

  • you want to restore a processed FRD back to a FrequencySeries and feed it into an existing plot or statistics pipeline

  • you want to convert each channel in a multi-channel spectrum with FrequencySeriesDict.to_control_frd()

Related pages:

Where GWexpy sits relative to other GWpy-adjacent packages#

GWexpy is not the only package that extends GWpy, and moving to it does not mean leaving the rest of the ecosystem behind. spicypy adds signal-processing and control-systems methods, GWDama organises data into HDF5 datasets for later analysis, and the detector-characterization tools build operator-facing workflows on top of GWpy. GWexpy occupies the container, I/O, and analysis-primitive layer between GWpy and those workflows, so the usual pattern is to combine them rather than to choose one.

For the full comparison, including which projects GWexpy bridges to and which it deliberately leaves out of scope, see Where GWexpy Sits in the GW Python Ecosystem.

Treat direct I/O and external-library conversion as separate guides#

This page intentionally does not duplicate the I/O-format list or the external-library conversion list.
Use the dedicated guides below as the source of truth:

Next Steps#