Commissioner: from GUI to a saved analysis#
Reproduce a familiar commissioning workflow in Python: load channels, select a time span, compare ASD and coherence, and save figures with the analysis settings. This lesson is for users of DiagGUI, ndscope, or Virgo dataDisplay who already choose channels and spectral settings in a GUI. No Python expertise is assumed beyond following the commands below; First analysis explains the syntax if needed.
Prerequisites: GWexpy installed. The main example uses synthetic data and core dependencies, including HDF5 support; it requires no detector connection. Study goal: 20–30 minutes. Script runtime goal: seconds on a laptop, depending on the environment.
Map GUI choices to code#
GUI action or setting |
Script equivalent |
|---|---|
Select channels |
Keys in a |
Load an ndscope recording |
|
Choose a time interval |
|
Choose an ASD FFT length and overlap |
|
Choose a reference channel |
|
Save traces as a figure |
|
Record the analysis setup |
A JSON file of channel names, times, and spectral settings |
Virgo dataDisplay is included as a familiar workflow background. This tutorial demonstrates the explicit ndscope and DiagGUI formats below; it does not define a direct dataDisplay reader.
Run a complete local workflow#
Download commissioner.py into a working folder.
In a terminal, activate the environment from the installation guide and run:
python commissioner.py
The script creates commissioner-output/ with channels.hdf5, asd.png, coherence.png, and analysis-parameters.json.
Rerunning it replaces these tutorial outputs.
Open the PNG files in an image viewer and the JSON file in a text editor.
"""Save synthetic ndscope data, analyze a segment, and record the settings."""
import json
import platform
from importlib.metadata import version
from pathlib import Path
from gwexpy.noise.wave import gaussian, sine
from gwexpy.timeseries import TimeSeriesDict
# settings-begin
output = Path("commissioner-output")
output.mkdir(exist_ok=True)
parameters = {
"sample_rate_hz": 512,
"t0_gps_s": 1400000000,
"duration_s": 32,
"unit": "V",
"tone_hz": 40,
"noise_std_v": {"X1:REFERENCE": 0.3, "X1:SENSOR": 0.8},
"noise_seeds": {"X1:REFERENCE": 10, "X1:SENSOR": 20},
"crop_offset_s": [4, 28],
"fftlength_s": 2,
"overlap_s": 1,
"window": "hann",
"asd_method": "welch",
"reference_channel": "X1:REFERENCE",
"sensor_channel": "X1:SENSOR",
}
# settings-end
# data-begin
settings = dict(
duration=parameters["duration_s"],
sample_rate=parameters["sample_rate_hz"],
t0=parameters["t0_gps_s"],
unit=parameters["unit"],
)
tone = sine(frequency=parameters["tone_hz"], **settings)
channels = TimeSeriesDict(
{
name: tone
+ gaussian(std=parameters["noise_std_v"][name], seed=seed, **settings)
for name, seed in parameters["noise_seeds"].items()
}
)
data_path = output / "channels.hdf5"
channels.write(data_path, format="hdf.ndscope", overwrite=True)
loaded = TimeSeriesDict.read(data_path, format="hdf.ndscope")
print("Loaded channels:", list(loaded))
# data-end
# analysis-begin
t0 = parameters["t0_gps_s"]
start = t0 + parameters["crop_offset_s"][0]
end = t0 + parameters["crop_offset_s"][1]
segment = loaded.copy().crop(start, end)
spectral_settings = dict(
fftlength=parameters["fftlength_s"],
overlap=parameters["overlap_s"],
window=parameters["window"],
)
spectra = segment.asd(method=parameters["asd_method"], **spectral_settings)
asd_plot = spectra.plot(xlim=(1, 256), ylabel=r"ASD [V/$\sqrt{\mathrm{Hz}}$]")
asd_plot.gca().legend()
asd_plot.savefig(output / "asd.png")
reference = segment[parameters["reference_channel"]]
sensor = segment[parameters["sensor_channel"]]
coherence = sensor.coherence(reference, **spectral_settings)
coherence_plot = coherence.plot(
xlim=(1, 256), ylim=(0, 1), yscale="linear", ylabel="Magnitude-squared coherence"
)
coherence_plot.savefig(output / "coherence.png")
# analysis-end
# save-begin
parameters["crop_start_gps_s"] = start
parameters["crop_end_gps_s"] = end
parameters["input_file"] = str(data_path)
parameters["versions"] = {
package: version(package)
for package in ("gwexpy", "gwpy", "numpy", "scipy", "astropy", "h5py")
}
parameters["versions"]["python"] = platform.python_version()
(output / "analysis-parameters.json").write_text(
json.dumps(parameters, indent=2) + "\n", encoding="utf-8"
)
print("Saved data, figures, and analysis-parameters.json in", output)
# save-end
Inspect the saved data and time selection#
The script makes two 32-second voltage channels sampled at 512 Hz and beginning at GPS 1400000000. Both contain a 40 Hz sine wave plus independently seeded Gaussian noise. The channel labels are synthetic examples.
channels.write(..., format="hdf.ndscope") creates an ndscope-format HDF5 file.
TimeSeriesDict.read(..., format="hdf.ndscope") loads the channel values and their metadata.
The printed list contains X1:REFERENCE and X1:SENSOR.
These public calls load their required I/O handler on demand.
The crop selects offsets 4 through 28 seconds relative to the start: GPS 1400000004 up to, but excluding, GPS 1400000028.
The resulting 24-second segment is shared by both channels.
The script copies the collection before cropping because TimeSeriesDict.crop() updates its collection.
For your own recording, inspect channel names, start times, sample rates, and units before choosing the interval and reference.
Interpret the two figures#
The ASD figure shows a line near 40 Hz in both channels and a higher broadband floor in X1:SENSOR.
Its unit is V per square root Hz.
The script uses a Hann window, 2-second FFT segments, 1-second overlap, and Welch averaging; the frequency-bin spacing is 0.5 Hz.
The second figure shows magnitude-squared coherence, a dimensionless measure between zero and one of linear association at each frequency. Coherence should rise near the shared 40 Hz tone. Away from that tone, the independent noise produces a smaller, fluctuating estimate; finite averaging does not give exactly zero. A high value identifies shared spectral content, but does not by itself establish the direction of a physical coupling.
analysis-parameters.json records the source file, channel choices, absolute crop bounds, FFT settings, the seeds used for the synthetic data, and the Python and package versions.
Keep this file with the figures so that the calculation can be repeated.
Read a DiagGUI time-series export#
Install the optional dttxml dependency for this section in the same environment:
python -m pip install dttxml
Download the small synthetic commissioner.xml sample into your working folder.
Save the following code as read_diaggui.py alongside it and run python read_diaggui.py:
from gwexpy.timeseries import TimeSeriesDict
diaggui = TimeSeriesDict.read(
"commissioner.xml", format="xml.diaggui", products="TS", unit="V"
)
print(list(diaggui))
for name, channel in diaggui.items():
print(name, channel.t0, channel.sample_rate, channel.unit)
plot = diaggui.plot()
plot.savefig("diaggui-timeseries.png")
products="TS" selects saved time-series data.
The synthetic sample contains four voltage samples at 4 Hz under TEST:SYNTHETIC_INPUT.
The example passes unit="V" explicitly because this time-series adapter does not recover the physical unit from the XML; this unit is a known calibration assumption for the supplied sample.
For your own export, supply the unit justified by its calibration.
DiagGUI can also save frequency-domain products; those are separate spectra and use the corresponding frequency-series readers.
An export must contain time-series data for this example.
See I/O formats for the product-specific entry points when loading your own saved spectral results.
Further reading#
First analysis: Python syntax and plot interpretation.
TimeSeriesMatrix basics: work with a larger aligned channel set.
Case studies: adapt a complete analysis to a measurement.