Metadata-Version: 2.4
Name: gw-nomad
Version: 0.1.0
Summary: NOMAD: NOn-stationary Modulation-Aware Denoiser for sgn
Author-email: Zach Yarbrough <zach.yarbrough@ligo.org>
Maintainer-email: Zach Yarbrough <zach.yarbrough@ligo.org>
License-Expression: GPL-3.0-or-later
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering :: Astronomy
Classifier: Topic :: Scientific/Engineering :: Physics
Requires-Python: >=3.11
Requires-Dist: h5py
Requires-Dist: numpy
Requires-Dist: pyyaml
Requires-Dist: scipy>=1.10
Requires-Dist: sgn-arrakis
Requires-Dist: sgn-ligo
Requires-Dist: sgn-ts
Requires-Dist: sgn>=0.6
Provides-Extra: convert
Requires-Dist: dill; extra == 'convert'
Provides-Extra: dev
Requires-Dist: dill; extra == 'dev'
Requires-Dist: markdown-callouts>=0.2; extra == 'dev'
Requires-Dist: markdown-exec>=0.5; extra == 'dev'
Requires-Dist: matplotlib; extra == 'dev'
Requires-Dist: mkdocs-coverage>=0.2; extra == 'dev'
Requires-Dist: mkdocs-gen-files>=0.3; extra == 'dev'
Requires-Dist: mkdocs-literate-nav>=0.4; extra == 'dev'
Requires-Dist: mkdocs-material-igwn; extra == 'dev'
Requires-Dist: mkdocs-section-index>=0.3; extra == 'dev'
Requires-Dist: mkdocs>=1.3; extra == 'dev'
Requires-Dist: mkdocstrings[python]; extra == 'dev'
Requires-Dist: mypy; extra == 'dev'
Requires-Dist: mypy-extensions; extra == 'dev'
Requires-Dist: pip; extra == 'dev'
Requires-Dist: pytest; extra == 'dev'
Requires-Dist: pytest-cov; extra == 'dev'
Requires-Dist: ruff; extra == 'dev'
Requires-Dist: toml>=0.10; extra == 'dev'
Provides-Extra: docs
Requires-Dist: markdown-callouts>=0.2; extra == 'docs'
Requires-Dist: markdown-exec>=0.5; extra == 'docs'
Requires-Dist: mkdocs-coverage>=0.2; extra == 'docs'
Requires-Dist: mkdocs-gen-files>=0.3; extra == 'docs'
Requires-Dist: mkdocs-literate-nav>=0.4; extra == 'docs'
Requires-Dist: mkdocs-material-igwn; extra == 'docs'
Requires-Dist: mkdocs-section-index>=0.3; extra == 'docs'
Requires-Dist: mkdocs>=1.3; extra == 'docs'
Requires-Dist: mkdocstrings[python]; extra == 'docs'
Requires-Dist: toml>=0.10; extra == 'docs'
Provides-Extra: lint
Requires-Dist: mypy; extra == 'lint'
Requires-Dist: mypy-extensions; extra == 'lint'
Requires-Dist: pip; extra == 'lint'
Requires-Dist: ruff; extra == 'lint'
Provides-Extra: plot
Requires-Dist: matplotlib; extra == 'plot'
Provides-Extra: test
Requires-Dist: dill; extra == 'test'
Requires-Dist: matplotlib; extra == 'test'
Requires-Dist: pytest; extra == 'test'
Requires-Dist: pytest-cov; extra == 'test'
Description-Content-Type: text/markdown

# NOMAD

**NOn-stationary Modulation-Aware Denoiser** — an [sgn](https://git.ligo.org/greg/sgn)-based
reimplementation of [NonSENS](https://git.ligo.org/gabriele-vajente/nonsens)
(Vajente et al., PRD 101, 042003) streaming noise subtraction for
gravitational-wave strain data.

NOMAD measures excess noise in a target strain channel from auxiliary "noise
witness" channels that couple into strain through transfer functions whose
amplitude is modulated in time by slow "modulation witness" channels. The
noise estimate is a sum of terms `alpha_i(f) * s_i(t)`, where each `s_i` is a
modulated signal (noise witness × products of modulation witnesses) and each
`alpha_i` is an optimal filter learned offline. The stationary case
(`modulation_order = 0`) is plain multi-channel Wiener filtering.

NOMAD's primary production role is the noise **measurement**: the SGN
calibration pipeline's CAL-NOLINES → CAL-CLEAN stage is a plain subtractor
that needs some other process to compute what to subtract (in O4 this was the
NonSENS front-end installation). `nomad-subtract --measure-only
--publish-noise` is that process, publishing the noise-estimate channel to
arrakis on witnesses alone. It can also do the subtraction itself and publish
or frame the cleaned channel directly.

## Status

- **v0.1**: streaming application of trained models as an sgn pipeline,
  reading witness and target channels from
  [arrakis](https://docs.ligo.org/ngdd/arrakis-python/) (or frame files) and
  publishing the noise estimate and/or subtracted strain back to arrakis
  (or frames).
- **v0.2**: native model training (`nomad-train`, scipy L-BFGS over the
  analytic cost/gradient — no TensorFlow), plus conversion of original
  nonsens pickles (`nomad-convert-model`).

## Installation

```bash
pip install .            # runtime
pip install .[convert]   # + dill, for converting original nonsens pickles
```

## Usage

Train a model (or convert an existing NonSENS pickle):

```bash
nomad-train --config lho_isc.yaml --data-source frames \
  --frame-cache O4.cache --start 1400000000 --end 1400000600 \
  --output lho_isc.h5

nomad-convert-model lho_isc.pickle lho_isc.h5   # legacy pickles
```

The training config's `target` defaults to `{ifo}:GDS-CALIB_STRAIN_NOLINES`:
the noise estimate is consumed downstream of the line subtraction, so
training against NOLINES keeps already-removed lines out of the CSD
estimates. Set `target` in the config (or pass `--target`) to train against
any other channel, e.g. raw `GDS-CALIB_STRAIN` — as in the original nonsens,
the target is simply whatever channel you name. A minimal config:

```yaml
ifo: H1
noise_witnesses: [ASC-DHARD_P_OUT_DQ, ASC-DHARD_Y_OUT_DQ, 60.0]  # numbers = synthesized lines
modulation_witnesses: []
fs: 1024
t_fft: 10
fband: [8, 256]
n_sos: 10
glitches: {band: [15, 20], threshold: 3.0e-20}
```

Production: measure the excess noise and publish the estimate for the
calibration pipeline's CLEAN subtractor (no target channel needed):

```bash
export ARRAKIS_SERVER=grpc://...
nomad-subtract --model lho_isc.h5 --data-source arrakis --queue-timeout 30 \
  --measure-only --publish-noise --publisher-id nomad-h1 \
  --noise-channel-name H1:CAL-NOMAD_NOISE_ESTIMATE
```

Standalone cleaning — arrakis in, subtracted strain out:

```bash
nomad-subtract --model lho_isc.h5 --data-source arrakis --queue-timeout 30 \
  --publish-arrakis --publisher-id nomad-h1 \
  --output-channel-name H1:GDS-CALIB_STRAIN_NOMAD
```

Published channels must be pre-registered with the arrakis server under the
given `--publisher-id`, at the target sample rate (float64). Note the noise
estimate carries the model's FIR high-pass group delay (`length/2` seconds)
as stream latency; timestamps are true GPS times, so a timestamp-aligned
downstream subtractor handles it naturally.

Run offline — frames in, frames out:

```bash
nomad-subtract --model lho_isc.h5 --data-source frames \
  --frame-cache O3.cache --start 1242441180 --end 1242443180 \
  --frame-output-path '{instruments}-{description}-{gps_start_time}-{duration}.gwf'
```

Channels rarely share a frame type (strain lives in HOFT frames, witnesses
in raw R frames); pass each extra cache with `--auxiliary-cache`
(repeatable). Cache entries are grouped by frame type, one file per type is
probed for its channels, and each channel is read from whichever frame type
provides it — a single concatenated mixed cache works too. This applies to
`nomad-train` as well.

## Development

```bash
hatch test           # run the test suite
hatch run check      # mypy
```

Golden-comparison tests against the original nonsens implementation are
skipped unless `NOMAD_GOLDEN=1` is set and `nonsens` is importable.
