17.6. Pipeline Steps#

Pipeline steps are the atomic processing operations used by pycsamt.pipeline.Pipeline. Each operation is defined once in the step registry and can then be used from the Python API, YAML/JSON/Python configuration files, pipeline preset recipes, and the pycsamt pipe command line.

The registry gives every operation a stable pipeline step code, such as FREQ001 or NR001. The code is intentionally short because it appears in terminal output, saved reports, plot folders, and reproducible pipeline.yaml files. Each code also has a snake-case registry name such as select_band or notch_powerline; both forms are accepted when constructing a step.

Use this page when you need to:

  • choose the right operation for a processing chain;

  • understand the order in which steps should usually run;

  • discover default parameters and diagnostic plots;

  • write a custom pipeline configuration;

  • extend the registry in a controlled way for new processing operations.

17.6.1. Step Contract#

A registered step has two layers.

StepSpec

StepSpec registry metadata. It stores the code, name, human label, category, default parameters, transform function, QC plot function entries, and whether the operation returns a modified site collection.

Step

A configured instance used inside a pipeline. It binds a StepSpec to user parameter override values.

The relationship is deterministic. If a registry entry has defaults \(\theta_{\mathrm{default}}\) and the user supplies overrides \(\theta_{\mathrm{user}}\), the configured parameters are

\[\theta_{\mathrm{step}} = \theta_{\mathrm{default}} \cup \theta_{\mathrm{user}},\]

where the right-hand values win for keys that appear in both dictionaries. This is why a short configuration can override only mains_hz for NR001 while keeping the default n_harm and tol_hz.

In Python, a step is created with pycsamt.pipeline.Step:

1>>> from pycsamt.pipeline import Step
2>>> notch = Step("NR001", mains_hz=50, n_harm=30)
3>>> notch.params
4{'mains_hz': 50, 'n_harm': 30, 'tol_hz': 0.08}
5>>> band = Step("select_band", band_hz=(0.01, 10000.0))
6>>> band.spec.code
7'FREQ001'

The two lookup styles are equivalent:

1>>> Step("NR001").spec is Step("notch_powerline").spec
2True

The first form is better for compact configs and reports. The second form is often easier to read in Python code.

17.6.2. How Steps Execute#

During a run, the pipeline applies each step in order. If the input state is \(S_{j-1}\) and the configured step is \(T_j\), then a normal transform step computes

\[S_j = T_j(S_{j-1}; \theta_j).\]

A diagnostic step instead records figures or summaries while passing the same site collection forward:

\[S_j = S_{j-1}.\]

Operationally, each step follows this sequence:

  1. count the input sites;

  2. call the step transform function;

  3. generate QC plots, if enabled;

  4. save intermediate EDI snapshots, if configured;

  5. record a pycsamt.pipeline.StepResult;

  6. pass the output sites to the next step.

For normal processing steps, returns_sites=True and the transform output becomes the input to the next step. Diagnostic-only steps use returns_sites=False. They keep the site collection unchanged and are used to generate plots or summaries at useful checkpoints.

Example result fields:

1>>> result = pipe.run(sites, outdir="results/basic_qc")
2>>> [(sr.step_code, sr.step_name, sr.ok, sr.n_sites_in, sr.n_sites_out)
3...  for sr in result.step_results]
4[('NR001', 'notch', True, 3, 3), ..., ('QC001', 'qc_snapshot', True, 3, 3)]

17.6.3. Discover Steps From The CLI#

The command-line catalogue is the fastest way to inspect your installed version:

1pycsamt pipe steps
2pycsamt pipe steps --category frequency
3pycsamt pipe steps --category noise_removal --codes-only
4pycsamt pipe steps --info NR001
5pycsamt pipe steps --info notch_powerline

Machine-readable output is useful for notebooks, dashboards, or scripts:

1pycsamt pipe steps --format json
2pycsamt pipe steps --category static_shift --format csv

Captured frequency catalogue excerpt:

 1{
 2  "frequency": [
 3    {
 4      "code": "FREQ001",
 5      "name": "select_band",
 6      "label": "Frequency Band Select",
 7      "defaults": {"band_hz": [0.001, 10000.0]},
 8      "returns_sites": true
 9    },
10    {
11      "code": "FREQ002",
12      "name": "drop_duplicates",
13      "label": "Drop Duplicate Frequencies",
14      "defaults": {},
15      "returns_sites": true
16    }
17  ]
18}

Captured NR001 information, simplified to ASCII:

 1NR001  Power-line Harmonic Notch  [noise_removal]
 2name     : notch_powerline
 3function : pycsamt.emtools.remove_noise.notch_powerline
 4defaults : mains_hz=50  n_harm=30  tol_hz=0.08
 5qc plots : nr_qc_harmonic_waterfall, nr_qc_snr_gain_profile
 6returns  : Sites (transform)
 7Usage:
 8  Step("NR001")
 9  Step("NR001", mains_hz=50)
10  Step("notch_powerline")

17.6.4. Discover Steps From Python#

The same registry is available from Python:

 1>>> from pycsamt.pipeline import categories, list_steps, lookup_step
 2>>> categories()
 3['dimensionality', 'export', 'frequency', 'noise_removal', 'preview', 'qc', 'skew', 'source_effects', 'static_shift', 'tensor']
 4>>> {c: len(list_steps(c)) for c in categories()}
 5{'dimensionality': 3, 'export': 3, 'frequency': 9, 'noise_removal': 14, 'preview': 2, 'qc': 7, 'skew': 4, 'source_effects': 2, 'static_shift': 4, 'tensor': 7}
 6>>> [(spec.code, spec.name, spec.defaults)
 7...  for spec in list_steps("frequency")[:2]]
 8[('FREQ001', 'select_band', {'band_hz': (0.001, 10000.0)}), ('FREQ002', 'drop_duplicates', {})]
 9>>> lookup_step("NR001") is lookup_step("notch_powerline")
10True

Use pycsamt.pipeline.Pipeline.step_info() when you want a formatted human-readable explanation:

1>>> from pycsamt.pipeline import Pipeline
2>>> "Power-line Harmonic Notch" in Pipeline.step_info("NR001")
3True
4>>> "SS001" in Pipeline.catalogue("static_shift")
5True

17.6.5. Registered Categories#

The current registry contains 55 built-in processing and diagnostic steps. The opt-in AI audit step (AI001, see Extending The Pipeline) is not counted here since it is not registered by default.

Category

Codes

Main purpose

frequency

FREQ001 - FREQ009

Select, clean, align, regrid, decimate, smooth, mask, and recover frequency samples.

noise_removal

NR001 - NR014

Remove power-line harmonics, smooth noisy responses, suppress outliers, apply EMAP-style filters, enforce off-diagonal consistency, and run bundled denoising chains.

static_shift

SS001 - SS004

Estimate and apply static-shift corrections using AMA, LOESS, reference-median, or bilateral-filter strategies.

tensor

TZ001 - TZ007

Rotate, antisymmetrise, clip, balance, invert, and reorient impedance tensors.

dimensionality

DIM001 - DIM003

Classify dimensionality, mask by dimensionality class, and project to a 2-D representation.

skew

SK001 - SK004

Mask or select bands using Bahr skewness and related low-skew criteria.

source_effects

SRC001 - SRC002

Correct near-field source effects and normalize source response.

qc

QC001 - QC007

Generate diagnostic snapshots without changing the site collection. QC005 - QC007 are data-driven (“smart”) diagnostics used by the method-aware presets – see Pipeline Presets.

preview

PRE001 - PRE002

Raw-vs-processed 1-D preview of a deterministic random station subset, meant to open (PRE001) and close (PRE002) a preset.

export

EXPORT001 - EXPORT003

Write processed sites out to ModEM, Occam2D, or MARE2DEM solver input files.

17.6.7. Frequency Steps#

Frequency steps define the usable sampling axis before more interpretive processing begins.

Code

Name

Use when

Defaults

FREQ001

select_band

You need to keep a scientific frequency interval and remove very low or very high frequencies from the chain.

band_hz=(0.001, 10000.0)

FREQ002

drop_duplicates

Multiple samples share the same nominal frequency and should be collapsed before alignment or interpolation.

none

FREQ003

drop_low_confidence

Low-confidence frequencies should be removed entirely.

none

FREQ004

align_grid

Sites need a common frequency grid before profile operations or cross-site comparisons.

none

FREQ005

regrid_logspace

You want a regular log-frequency sampling density.

n_per_decade=6

FREQ006

decimate

You want a lighter frequency set for quick inspection or faster downstream runs.

step=2

FREQ007

smooth_freq

Small frequency-to-frequency oscillations should be smoothed.

window=3

FREQ008

mask_low_confidence

Low-confidence samples should remain in the grid as NaN values rather than being removed.

method='composite', threshold=0.5

FREQ009

recover_low_confidence

Low-confidence samples should be recovered by interpolation after confidence gating.

method='composite', ci_hi=0.9, ci_lo=0.5, interpolation='linear'

Common frequency cleanup example:

1steps:
2  - name: drop_duplicates
3    code: FREQ002
4  - name: select_band
5    code: FREQ001
6    params:
7      band_hz: [0.01, 10000.0]
8  - name: align_grid
9    code: FREQ004

17.6.8. Noise Removal Steps#

Noise-removal steps suppress survey artifacts while preserving the interpretable impedance structure. Prefer targeted operations first; reserve the bundled denoising step for quick workflows or carefully reviewed production recipes.

Code

Name

Use when

Defaults

NR001

notch_powerline

Power-line harmonics contaminate the response.

mains_hz=50, n_harm=30, tol_hz=0.08. mains_hz="auto" is also accepted – see below.

NR002

smooth_logfreq

Responses are noisy along log-frequency.

none

NR003

shrink_group_trend

Isolated outliers should be pulled toward the group trend rather than removed aggressively.

none

NR004

hampel_filter

Spike-like outliers need robust local filtering.

win=3, nsig=3.0

NR005

spatial_median

Neighboring stations define a stable spatial trend.

none

NR006

emap_filter

You want EMAP-style spatial filtering.

none

NR007

emap_confidence

EMAP filtering should be gated by confidence and return the filtered site collection.

none

NR008

rpca_offdiag

Off-diagonal components need robust low-rank denoising.

none

NR009

enforce_offdiag

Off-diagonal impedance components should be made more internally consistent.

none

NR010

mask_incoherent

Incoherent frequency samples should be masked before interpretation.

none

NR011

fixed_length_mavg

A fixed moving-average smoother is appropriate for all or selected components.

window=5, component='all'

NR012

trimmed_mavg

You want a moving average that is less sensitive to extremes.

window=5, component='all'

NR013

correct_static_shift_spatial

A spatial-window static-shift correction is needed from the noise module.

window_m=1500.0, spacing_m=200.0, comp='det'

NR014

denoise_pipeline

A bundled denoising recipe is acceptable for a first pass or a reviewed production preset.

mains_hz=50.0, n_harm=30, tol_hz=0.08, smooth_win=5, gate_snr=2.5

Example:

 1steps:
 2  - name: notch
 3    code: NR001
 4    params:
 5      mains_hz: 60
 6      n_harm: 20
 7  - name: robust_spikes
 8    code: NR004
 9    params:
10      win: 5
11      nsig: 3.5
12  - name: incoherence_gate
13    code: NR010
mains_hz="auto" (NR001 only)

Real EDI frequency grids are usually log-spaced rather than sampled exactly on multiples of 50/60 Hz, so a fixed numeric mains_hz can silently miss harmonics that fall outside the tight +-tol_hz window. Passing the literal string "auto" instead makes the match data-driven: pyCSAMT scores 50 Hz and 60 Hz – the only two real-world AC grid frequencies – against the survey’s actual pooled frequency array, resolves to whichever explains more harmonics, then snaps each harmonic to its single nearest real sample within a relative snap_frac tolerance (default 1%, deliberately tight so a coarse, non-mains-aware grid does not get treated as evidence). If neither candidate clears a minimum-evidence bar, "auto" leaves the data untouched and warns rather than guessing:

1>>> from pycsamt.api import read_edis
2>>> from pycsamt.emtools.remove_noise import notch_powerline
3>>> survey = read_edis("data/AMT/WILLY_DATA/L18PLT", recursive=False, strict=False, progress=False)
4>>> out = notch_powerline(survey.collection, mains_hz="auto", verbose=1)

A UserWarning reports the resolved fundamental (captured against the real 28-station L18PLT line):

1notch_powerline: mains_hz="auto" resolved to 60 Hz (4/30 harmonics matched within 1%) for this survey.

Passing a plain number is completely unaffected – output is identical to every previous release. verbose=2 additionally logs every individual snapped harmonic (station, target frequency, the real frequency substituted for it, and the offset). In a config file:

1- name: notch
2  code: NR001
3  params:
4    mains_hz: auto

17.6.9. Static-Shift Steps#

Static-shift correction should normally run after basic frequency and noise cleanup. Static-shift steps modify the site collection and generate comparison plots when output plotting is enabled.

Code

Name

Use when

Defaults

SS001

correct_ss_ama

You want the standard AMA static-shift correction.

none

SS002

correct_ss_loess

You want a two-phase LOESS estimate followed by factor application.

none

SS003

correct_ss_refmedian

A reference median across stations is the preferred shift estimator.

none

SS004

correct_ss_bilateral

Shift factors should be estimated with a bilateral-style spatial filter.

half_window=4, max_skew=6.0, summary='median'

Example:

 1steps:
 2  - name: select_band
 3    code: FREQ001
 4    params:
 5      band_hz: [0.01, 10000.0]
 6  - name: notch
 7    code: NR001
 8  - name: static_shift
 9    code: SS004
10    params:
11      half_window: 5
12      max_skew: 5.0

17.6.10. Tensor Steps#

Tensor steps change tensor orientation, symmetry, or component consistency. Run them only after the frequency axis and gross noise problems are under control.

Code

Name

Use when

Defaults

TZ001

rotate_strike

The impedance tensor should be rotated to estimated strike.

method='swift'

TZ002

antisymmetrize

Off-diagonal tensor symmetry needs to be enforced or inspected.

how='rms'

TZ003

sigma_clip

Tensor outlier frequencies should be sigma-clipped.

sigma=3

TZ004

balance_offdiag

Off-diagonal components need balancing.

none

TZ005

rotate_fixed

A known fixed rotation angle should be applied.

angle=0.0

TZ006

invert_tensor

The impedance tensor should be inverted.

none

TZ007

orient_from_sensors

Sensor orientations are known and should be corrected explicitly.

ex=0.0, ey=0.0, bx=0.0, by=0.0, degrees=True

Example:

 1>>> from pycsamt.pipeline import Pipeline, Step
 2>>> pipe = Pipeline(
 3...     [
 4...         ("drop_duplicates", Step("FREQ002")),
 5...         ("select_band", Step("FREQ001", band_hz=(1.0, 10000.0))),
 6...         ("static_shift", Step("SS001")),
 7...         ("rotate", Step("TZ001", method="swift")),
 8...         ("qc", Step("QC001")),
 9...     ],
10...     name="tensor_ready",
11... )
12>>> [label for label, step in pipe]
13['drop_duplicates', 'select_band', 'static_shift', 'rotate', 'qc']

17.6.11. Dimensionality Steps#

Dimensionality steps help decide whether a frequency band or station interval is suitable for 1-D, 2-D, or 3-D assumptions.

Code

Name

Use when

Defaults

DIM001

classify_dim

You need a diagnostic classification of dimensionality. This is a diagnostic step and does not modify sites.

none

DIM002

mask_by_dim

You want to keep or mask samples by dimensionality class before further processing.

dim_type='2d'

DIM003

project_2d

The response should be projected toward a 2-D representation.

none

Example:

1steps:
2  - name: classify_dimensionality
3    code: DIM001
4  - name: keep_2d
5    code: DIM002
6    params:
7      dim_type: 2d
8  - name: project_2d
9    code: DIM003

17.6.12. Skew Steps#

Skew steps are useful when selecting lower-skew intervals for 2-D inversion or for rejecting intervals where 3-D effects dominate.

Code

Name

Use when

Defaults

SK001

mask_by_skew

Frequencies above a Bahr skewness threshold should be masked.

thresh=0.3

SK002

longest_low_skew

You want the longest continuous low-skew band.

thresh=0.3

SK003

select_skew_band

You want the lowest-skew band selected automatically.

none

SK004

close_skew_gaps

Small gaps in a low-skew interval should be closed.

thresh=3.0, max_gap=1

Example:

1pycsamt pipe run data/edis \
2    --steps FREQ002,FREQ001,FREQ004,NR001,SK001,TZ001,QC001 \
3    --out results/low_skew_rotation

17.6.13. Source-Effect Steps#

Source-effect steps are more specialized than the basic QC and denoising chain. Use them when survey geometry, transmitter effects, or diagnostic plots indicate source overprint or near-field behavior.

Code

Name

Use when

Defaults

SRC001

correct_near_field

Near-field source effects should be corrected.

none

SRC002

normalize_response

Source response normalization is needed before interpretation or comparison.

none

Example:

1steps:
2  - name: source_diagnostic
3    code: QC003
4  - name: near_field
5    code: SRC001
6  - name: normalized_source
7    code: SRC002

17.6.14. QC And Diagnostic Steps#

QC steps are diagnostic-only checkpoints. They generate figures but pass the site collection through unchanged.

Code

Name

Use when

Defaults

QC001

qc_snapshot

You want a quick-look dashboard, station confidence view, and coverage pseudo-section.

none

QC002

field_zone_snapshot

You want a field-zone classification plot.

none

QC003

source_overprint_snapshot

You want a source-overprint diagnostic before or after source-effect corrections.

none

QC004

depth_section_snapshot

You want a Bostick depth-section diagnostic snapshot.

none

Use QC snapshots at major checkpoints:

 1steps:
 2  - name: raw_qc
 3    code: QC001
 4  - name: notch
 5    code: NR001
 6  - name: cleaned_qc
 7    code: QC001
 8  - name: static_shift
 9    code: SS001
10  - name: final_qc
11    code: QC001

17.6.15. Export Steps#

Export steps write processed sites out to solver-ready input files. Unlike QC steps they are not diagnostic-only: returns_sites=True, so a real write failure propagates through the pipeline’s normal on_step_error policy rather than being silently turned into a warning.

Code

Name

Use when

Defaults

EXPORT001

export_modem

You want ModEM data, model, and control files (plus covariance for a 3-D run).

workdir='pipeline_exports/modem'

EXPORT002

export_occam2d

You want a full native Occam2D data/mesh/model/startup working directory.

workdir='pipeline_exports/occam2d'

EXPORT003

export_mare2dem

You want the MARE2DEM .emdata data file specifically.

workdir='pipeline_exports/mare2dem', data_filename='line.emdata'

EXPORT003 deliberately writes only the data file. pycsamt.models.mare2dem.builder.InputBuilder.write_resistivity/.write_settings are separate calls with their own configuration, and full mesh generation (the .poly file) is not wired into any pipeline step yet.

A step function only ever receives sites and its own parameters, with no hook into the run’s own output directory, so each export step’s workdir resolves relative to the process’s current working directory rather than following --out. Point it explicitly at wherever the run’s output should live:

 1>>> from pycsamt.pipeline import Pipeline, Step
 2>>> pipe = Pipeline(
 3...     [
 4...         ("notch", Step("NR001")),
 5...         (
 6...             "export_modem",
 7...             Step("EXPORT001", workdir="results/line22_export/modem"),
 8...         ),
 9...     ],
10...     name="export_ready",
11... )
12>>> [label for label, step in pipe]
13['notch', 'export_modem']

Captured against the real 25-station WILLY L22 line:

1pycsamt pipe run data/AMT/WILLY_DATA/L22PLT \
2    --steps NR001,EXPORT001,EXPORT002,EXPORT003 \
3    --out results/line22_export
1PipelineResult  'cli_pipeline'
2  Sites   : 25 in → 25 out
3  Steps   : 4 (4 ok, 0 err)
4  Time    : 12.15 s
5  Plots   : 2
6  Output  : results\line22_export

Files actually written under each step’s default workdir:

1pipeline_exports/modem/     control.inv  covariance.cov  data.dat  m0.ws
2pipeline_exports/occam2d/   Occam2DMesh  Occam2DModel  OccamDataFile.dat  Startup
3pipeline_exports/mare2dem/  line.emdata

17.6.16. Parameters And Defaults#

User parameters are merged on top of registry defaults. If a default is not overridden, the registry value is used. The merge is shallow and right-biased:

\[\begin{split}\theta_{\mathrm{active}}[k] = \begin{cases} \theta_{\mathrm{user}}[k], & k \in \theta_{\mathrm{user}},\\ \theta_{\mathrm{default}}[k], & k \notin \theta_{\mathrm{user}}. \end{cases}\end{split}\]
1>>> from pycsamt.pipeline import Step
2>>> step = Step("NR001", mains_hz=60)
3>>> step.params["mains_hz"]
460
5>>> step.params["n_harm"]
630
7>>> step.params["tol_hz"]
80.08

In YAML, place overrides under params:

1steps:
2  - name: notch_60hz
3    code: NR001
4    params:
5      mains_hz: 60
6      n_harm: 25
7      tol_hz: 0.1

Keep parameter names aligned with the underlying emtools function. Use pycsamt pipe steps --info CODE or Pipeline.step_info("CODE") before editing an unfamiliar step. For reviewed work, write every scientifically important parameter explicitly even when it equals the default; that makes the processing choice visible in the configuration file.

17.6.17. Step Labels#

The registry code identifies the operation. The pipeline label identifies the position or intent of that operation inside one workflow.

1steps:
2  - name: raw_notch
3    code: NR001
4  - name: post_static_shift_notch
5    code: NR001
6    params:
7      n_harm: 10

The two entries use the same operation, but reports and plot directories can distinguish them because their labels differ.

17.6.18. Diagnostic Plots#

Each StepSpec may declare QC plot function entries. When plotting is enabled, the pipeline calls those functions after a successful transform and saves the figures under the run output directory. The files are diagnostic artifacts; they do not feed back into the next transform unless a separate step explicitly uses the underlying data.

The plot call order for step \(j\) is

\[S_j = T_j(S_{j-1};\theta_j) \quad\Longrightarrow\quad \mathrm{plots}_j = \{q(S_j): q \in Q_j\},\]

where \(Q_j\) is the set of registered QC plot functions for that step. Diagnostic-only QC steps have \(S_j=S_{j-1}\), but their plots still describe the current state of the survey at that checkpoint.

Examples:

  • NR001 can generate a harmonic waterfall and SNR gain profile.

  • FREQ001 can generate band microstrips and a coverage-quality heatmap.

  • SS004 can generate static-shift delta, comparison, and summary plots.

  • QC001 can generate the quick-look dashboard, station confidence dashboard, and coverage pseudo-section.

QC plot failures are skipped so that a successful processing step is not turned into a failed run only because a diagnostic figure could not be drawn. If a plot is missing, inspect the saved report and rerun with verbose CLI output.

17.6.19. Error Handling#

Step failures are controlled by the pipeline error policy. From the CLI, use --on-error:

1pycsamt pipe run data/edis --preset basic_qc --on-error raise
2pycsamt pipe run data/edis --preset basic_qc --on-error warn
3pycsamt pipe run data/edis --preset basic_qc --on-error skip
raise

Stop immediately at the failing step.

warn

Record the error, warn, continue with the previous site collection, and return a nonzero CLI exit status if any step failed.

skip

Record the error and continue with the previous site collection without a warning. The final result is still not OK if any step failed.

Diagnostic-only steps are special: if their diagnostic transform raises, the step warns and passes the original sites through unchanged.

17.6.20. Choosing Between Similar Steps#

Use the smallest operation that matches the problem you can actually see in the data.

For harmonic contamination

Start with NR001 and tune mains_hz, n_harm, and tol_hz.

For frequency-grid problems

Start with FREQ002 followed by FREQ001 and FREQ004.

For isolated spikes

Start with NR004. Move to NR012 when the smoothing window should be robust to extremes across a broader interval.

For spatially coherent noise

Compare NR005, NR006, and NR007 with QC plots enabled.

For static shift

Start with SS001 for a standard workflow. Use SS002, SS003, or SS004 when the survey geometry and QC evidence justify a different estimator.

For 2-D inversion preparation

Combine frequency cleanup, noise removal, skew or dimensionality gates, static-shift correction, strike rotation, and final QC.

17.6.21. Full Example Configuration#

This example is explicit enough for a reviewed processing run while remaining short enough to audit:

 1name: line22_processing
 2output_dir: results/line22_processing
 3
 4steps:
 5  - name: drop_duplicates
 6    code: FREQ002
 7
 8  - name: select_band
 9    code: FREQ001
10    params:
11      band_hz: [0.01, 10000.0]
12
13  - name: align_grid
14    code: FREQ004
15
16  - name: notch_powerline
17    code: NR001
18    params:
19      mains_hz: 50
20      n_harm: 30
21      tol_hz: 0.08
22
23  - name: robust_spike_filter
24    code: NR004
25    params:
26      win: 3
27      nsig: 3.0
28
29  - name: low_skew_gate
30    code: SK001
31    params:
32      thresh: 0.3
33
34  - name: static_shift
35    code: SS001
36
37  - name: rotate_to_strike
38    code: TZ001
39    params:
40      method: swift
41
42  - name: final_qc
43    code: QC001

Run it with:

1pycsamt pipe show config/line22_processing.yaml
2pycsamt pipe run data/edis \
3    --config config/line22_processing.yaml \
4    --out results/line22_processing \
5    --on-error warn \
6    -v

17.6.22. Extension Policy#

The current pipeline registry is source-controlled and static. That is intentional: processing steps are scientific operations, so adding one should be reviewed with tests, defaults, documentation, and diagnostic behavior.

A second, runtime path exists for steps that should not be reviewed into pyCSAMT itself, such as a site-specific correction or a vendor-format adapter: register_step adds a StepSpec to the same registry without touching pyCSAMT’s source tree, and the resulting entry is usable everywhere a built-in step is. See Extending The Pipeline for the full mechanism, including how a third-party package can announce its steps through an entry point so they are found automatically rather than registered by hand in every script.

To add a new built-in step:

  1. implement or expose the transform function in the appropriate pycsamt.emtools module;

  2. add a pycsamt.pipeline.StepSpec entry in pycsamt/pipeline/_registry.py;

  3. choose a code prefix that matches the category, such as FREQ, NR, SS, TZ, DIM, SK, SRC, or QC;

  4. define conservative defaults;

  5. attach QC plot functions when useful;

  6. decide whether the step returns a modified site collection;

  7. add tests that prove lookup, parameter merging, execution, and config round-tripping;

  8. update this page and any presets that should use the new operation.

Minimal registry entry pattern:

 1>>> StepSpec(
 2...     code="NR015",
 3...     name="my_new_filter",
 4...     label="My New Noise Filter",
 5...     category="noise_removal",
 6...     mod="pycsamt.emtools.remove_noise",
 7...     fn_name="my_new_filter",
 8...     defaults={"window": 5},
 9...     qc_defs=[
10...         ("pycsamt.emtools.remove_noise", "nr_qc_station_offdiag_curves"),
11...     ],
12... )

For two-phase operations, use an override_fn wrapper that receives sites and keyword parameters, then returns the modified site collection. Static-shift steps such as SS002 and SS003 follow this pattern.

17.6.23. Testing New Steps#

At minimum, a new step should be covered by tests like these:

 1>>> from pycsamt.pipeline import Pipeline, Step, lookup_step
 2>>>
 3>>> def test_registry_lookup_for_new_step():
 4...     spec = lookup_step("NR015")
 5...     assert spec.name == "my_new_filter"
 6...     assert lookup_step("my_new_filter") is spec
 7...
 8>>> def test_step_defaults_and_overrides():
 9...     step = Step("NR015", window=7)
10...     assert step.params["window"] == 7
11...
12>>> def test_pipeline_accepts_new_step(sites):
13...     pipe = Pipeline([("new_filter", Step("NR015"))])
14...     result = pipe.run(sites, outdir=None, save_plots=False)
15...     assert result.ok

Also test YAML loading if the step is expected to be used from configuration files.

17.6.24. Troubleshooting#

Unknown step code

Run pycsamt pipe steps or pycsamt pipe steps --info CODE. Codes are exact and uppercase, for example NR001.

Unknown step name

Use the snake-case registry name, for example notch_powerline rather than notch. Pipeline labels may be arbitrary, but registry names are fixed.

A parameter is ignored or raises TypeError

Check the underlying emtools function signature and the output of pycsamt pipe steps --info CODE. Parameter names must match the transform function.

The site count changes unexpectedly

Frequency-selection, masking, or dimensionality operations can change the effective data content. Inspect StepResult.n_sites_in and StepResult.n_sites_out in the run summary.

Expected plots are missing

Confirm that plots were not disabled with --no-plots. Some QC plots are skipped when the required data are absent or when the plotting function cannot build a figure for the current site collection.

A diagnostic step appears to do nothing

QC steps intentionally pass sites through unchanged. Inspect the plot directory and report rather than expecting data changes.