Frequency Editing, Resampling, And QC#
pycsamt.emtools.frequency is the user-guide hub for operations that
change, evaluate, or visualize the frequency axis. It is used before
inversion, before section plotting, after quality control, and whenever
multiple stations need to share a consistent frequency grid.
The module covers four practical jobs:
selecting a usable frequency band;
dropping, masking, or recovering low-confidence rows;
regridding, decimating, smoothing, and aligning station grids;
plotting frequency coverage, apparent depth, and band-level summary diagnostics.
Full callable signatures live in the API reference. This page focuses on how to use the workflows safely, how to read their tables, and how to avoid common frequency-axis traps.
Why Frequency Editing Matters#
CSAMT/AMT/MT processing is rarely limited by one bad station value. More often, a workflow succeeds or fails because the frequency axis is inconsistent, too noisy at the band edges, or incompatible across stations. A 2-D inversion, pseudo-section, or model-comparison workflow needs clear decisions about:
which band is physically meaningful;
which frequency rows are trusted;
whether bad rows are removed or kept as missing values;
whether stations should be interpolated to a shared grid;
whether smoothing or decimation is acceptable for the next step.
The frequency module makes those decisions explicit and auditable.
Data Contract#
All editing functions accept the usual emtools input:
a directory containing EDI files,
one EDI-like object,
a
Sitescontainer,an iterable of site-like objects.
Internally the functions call ensure_sites. The familiar
recursive, on_dup, strict, and verbose arguments behave
the same way as in the rest of the user guide.
Most editing functions default to inplace=False. Keep that default
while exploring. Switch to inplace=True only when you intentionally
want to mutate the input object.
Selecting A Band#
Use select_band when you already know the frequency limits.
1from pathlib import Path
2
3from pycsamt.emtools.frequency import select_band
4
5edi_dir = Path("data/AMT/WILLY_DATA/L18PLT")
6
7mid_band = select_band(
8 edi_dir,
9 fmin=10.0,
10 fmax=1000.0,
11 inplace=False,
12)
fmin and fmax are in hertz. The convenience argument
band_hz=(10.0, 1000.0) is also accepted. If both forms are provided,
explicit fmin and fmax take precedence.
Use keep when you want specific frequencies rather than a continuous
band.
1from pycsamt.emtools.frequency import select_band
2
3selected = select_band(
4 "data/AMT/WILLY_DATA/L18PLT",
5 keep=[10.0, 100.0, 1000.0],
6)
Band selection removes rows outside the chosen band. It changes the frequency grid length.
Removing Duplicate Frequencies#
Use drop_duplicates when a station has repeated or unsorted
frequency rows.
1from pycsamt.emtools.frequency import drop_duplicates
2
3cleaned = drop_duplicates(
4 "data/AMT/WILLY_DATA/L18PLT",
5 tol=1e-10,
6 inplace=False,
7)
The function keeps the first occurrence of each frequency within the tolerance and applies the same row selection to impedance and tipper blocks when present.
Frequency Confidence#
Confidence-based editing uses frequency_confidence_table from the
QC workflow. The frequency module computes that table internally, but
you should inspect it before editing.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools.qc import frequency_confidence_table
4
5survey = "data/AMT/WILLY_DATA/L18PLT"
6confidence = frequency_confidence_table(survey)
7
8print(confidence[["station", "frequency_hz", "confidence", "flags"]].head())
9print(confidence["confidence"].describe())
10
11station = "18-001A"
12one = confidence.loc[confidence["station"] == station].sort_values("period_s")
13
14fig, ax = plt.subplots(figsize=(7, 4.5))
15ax.semilogx(one["period_s"], one["confidence"], "o-")
16ax.axhline(0.90, color="tab:red", linestyle="--", label="ci_hi")
17ax.axhline(0.50, color="tab:orange", linestyle="--", label="ci_lo")
18ax.set_xlabel("Period (s)")
19ax.set_ylabel("Confidence")
20ax.legend()
21fig.tight_layout()
station ... flags
0 18-001A ... recoverable,high_error,offdiag_mismatch,diagon...
1 18-001A ... reject,high_error,offdiag_mismatch,diagonal_le...
2 18-001A ... reject,high_error,offdiag_mismatch,diagonal_le...
3 18-001A ... reject,high_error,offdiag_mismatch,diagonal_le...
4 18-001A ... reject,high_error,offdiag_mismatch,diagonal_le...
[5 rows x 4 columns]
count 1484.000000
mean 0.658116
std 0.096143
min 0.417464
25% 0.586675
50% 0.654938
75% 0.736039
max 0.863041
Name: confidence, dtype: float64
The key lesson is simple: do not choose ci_hi or threshold before
looking at the confidence distribution. If no row reaches ci_hi,
recovery has no trusted rows to interpolate from.
Drop Or Mask Low-Confidence Rows#
Dropping removes rows from the frequency grid. Masking keeps the grid
but replaces low-confidence tensor rows with NaN.
1from pycsamt.emtools.frequency import (
2 drop_low_confidence_frequencies,
3 mask_low_confidence_frequencies,
4)
5
6survey = "data/AMT/WILLY_DATA/L18PLT"
7
8dropped = drop_low_confidence_frequencies(
9 survey,
10 threshold=0.50,
11 also="both",
12 inplace=False,
13)
14
15masked = mask_low_confidence_frequencies(
16 survey,
17 threshold=0.50,
18 also="both",
19 inplace=False,
20)
Use drop when downstream code cannot handle missing rows. Use
mask when preserving the original frequency grid matters, for
example when comparing before and after processing.
The also argument controls which blocks are edited:
Value |
Meaning |
|---|---|
|
Edit impedance only. |
|
Edit tipper only. |
|
Edit impedance and tipper when present. |
Recover Low-Confidence Rows#
recover_low_confidence_frequencies treats rows in
[ci_lo, ci_hi) as recoverable and interpolates them from trusted
rows whose confidence is at least ci_hi. Rows below ci_lo are
handled by reject.
1from pycsamt.emtools.frequency import recover_low_confidence_frequencies
2
3recovered = recover_low_confidence_frequencies(
4 "data/AMT/WILLY_DATA/L18PLT",
5 ci_hi=0.72,
6 ci_lo=0.50,
7 interpolation="linear",
8 reject="drop",
9 also="both",
10 inplace=False,
11)
Valid interpolation modes are "linear" and "nearest". Valid
reject modes are:
Reject mode |
Meaning |
|---|---|
|
Remove rows below |
|
Keep rows below |
|
Leave rejected rows unchanged. |
Recovery is interpolation, not magic. At band edges, interpolation can behave like holding the nearest trusted value. Always inspect the result when many rows are recovered.
High-Level Editing Workflow#
Use edit_frequencies_by_confidence when you want the edited sites,
a station-level report, and a row-level decision table in one object.
1from pycsamt.emtools.frequency import edit_frequencies_by_confidence
2
3result = edit_frequencies_by_confidence(
4 "data/AMT/WILLY_DATA/L18PLT",
5 mode="recover",
6 ci_hi=0.72,
7 ci_lo=0.50,
8 reject="drop",
9 interpolation="linear",
10 api=False,
11)
12
13print(result.summary())
14print(result.report.head())
15print(result.decisions.head())
FrequencyEditResult(mode='recover', method='composite', dropped=73, masked=102, recovered=867)
station n_freq_before ... n_masked_or_unfinite confidence_delta
0 18-001A 53 ... 0 0.027413
1 18-002U 53 ... 0 -0.038885
2 18-003A 53 ... 3 0.061644
3 18-004A 53 ... 3 -0.026442
4 18-005U 53 ... 1 0.026781
[5 rows x 18 columns]
station frequency_hz period_s ... present_after finite_after action
0 18-001A 10400.0 0.000096 ... True True kept
1 18-001A 8707.0 0.000115 ... True True kept
2 18-001A 7289.0 0.000137 ... True True kept
3 18-001A 6102.0 0.000164 ... True True kept
4 18-001A 5108.0 0.000196 ... True True kept
[5 rows x 10 columns]
result is a FrequencyEditResult with:
sites: edited sites.report: station-level before/after table.decisions: one row per original station-frequency row.n_dropped,n_masked,n_recovered: convenience counts.mode,method,ci_hi,ci_lo,reject,interpolation: edit metadata.
For in-memory objects, pass an independent before_sites when you
need an untouched baseline for reporting. Some lower-level editors may
mutate wrapped impedance objects while constructing edited outputs.
Station-Level Report#
frequency_edit_report compares before and after sites.
1from pycsamt.emtools.frequency import (
2 edit_frequencies_by_confidence,
3 frequency_edit_report,
4)
5
6survey = "data/AMT/WILLY_DATA/L18PLT"
7result = edit_frequencies_by_confidence(
8 survey,
9 mode="drop",
10 threshold=0.50,
11 api=False,
12)
13
14report = frequency_edit_report(
15 survey,
16 result.sites,
17 ci_hi=0.90,
18 ci_lo=0.50,
19)
20
21print(
22 report[
23 [
24 "station",
25 "n_freq_before",
26 "n_freq_after",
27 "n_dropped",
28 "n_finite_before",
29 "n_finite_after",
30 "confidence_median_before",
31 "confidence_median_after",
32 ]
33 ].head()
34)
station n_freq_before ... confidence_median_before confidence_median_after
0 18-001A 53 ... 0.711753 0.711753
1 18-002U 53 ... 0.749480 0.749480
2 18-003A 53 ... 0.666613 0.671639
3 18-004A 53 ... 0.735994 0.743510
4 18-005U 53 ... 0.728841 0.733250
[5 rows x 8 columns]
Important report columns include:
n_freq_beforeandn_freq_after: grid size change.n_finite_beforeandn_finite_after: finite tensor row counts.frac_finite_beforeandfrac_finite_after: finite row fraction.safe_fraction_*: fraction at or aboveci_hi.recoverable_fraction_*: fraction in[ci_lo, ci_hi).reject_fraction_*: fraction belowci_lo.n_dropped: number of removed frequency rows.n_masked_or_unfinite: finite rows lost to masking or missing data.confidence_delta: median confidence change.
Decision Table#
frequency_edit_decision_table records one row per original
station-frequency sample.
1from pycsamt.emtools.frequency import (
2 edit_frequencies_by_confidence,
3 frequency_edit_decision_table,
4)
5
6survey = "data/AMT/WILLY_DATA/L18PLT"
7result = edit_frequencies_by_confidence(
8 survey,
9 mode="recover",
10 ci_hi=0.72,
11 ci_lo=0.50,
12 reject="drop",
13 api=False,
14)
15
16decisions = frequency_edit_decision_table(
17 survey,
18 result.sites,
19 ci_hi=0.72,
20 ci_lo=0.50,
21)
22
23print(decisions["action"].value_counts())
action
recovered 867
kept 442
masked 102
dropped 73
Name: count, dtype: int64
Actions are:
kept: row survived unchanged.dropped: row is absent after editing.masked: row remains but is no longer finite.recovered: row was filled or changed by recovery.
Plot Edit Results#
Use the summary plot for station-level effects and the decision plot for row-level actions.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools.frequency import (
4 edit_frequencies_by_confidence,
5 plot_frequency_edit_decisions,
6 plot_frequency_edit_summary,
7)
8
9survey = "data/AMT/WILLY_DATA/L18PLT"
10result = edit_frequencies_by_confidence(
11 survey,
12 mode="recover",
13 ci_hi=0.72,
14 ci_lo=0.50,
15 reject="drop",
16 api=False,
17)
18
19fig, (ax_summary, ax_decisions) = plt.subplots(2, 1, figsize=(10, 8))
20plot_frequency_edit_summary(survey, result.sites, ci_hi=0.72, ci_lo=0.50, ax=ax_summary)
21plot_frequency_edit_decisions(survey, result.sites, ci_hi=0.72, ci_lo=0.50, ax=ax_decisions)
22fig.tight_layout()
If the decision plot is dominated by recovered or masked colors,
the edit is not conservative and should be justified.
Regrid To A Target Grid#
Use regrid_to when you already have the target frequencies.
1import numpy as np
2
3from pycsamt.emtools.frequency import regrid_to
4
5target_freq = np.array([1.0, 3.0, 10.0, 30.0, 100.0, 300.0, 1000.0])
6
7regridded = regrid_to(
8 "data/AMT/WILLY_DATA/L18PLT",
9 target_freq,
10 method="linear",
11 inplace=False,
12)
method controls interpolation behavior. The implementation supports
nearest-neighbor and log-frequency interpolation paths used by the
module’s helpers. Use a target grid that is meaningful for your
inversion or comparison, not merely convenient.
Build A Log-Spaced Grid#
Use regrid_logspace to generate a grid automatically.
1from pycsamt.emtools.frequency import regrid_logspace
2
3regular = regrid_logspace(
4 "data/AMT/WILLY_DATA/L18PLT",
5 fmin=1.0,
6 fmax=10000.0,
7 per_decade=6,
8 method="linear",
9 inplace=False,
10)
band_hz=(lo, hi) is accepted as an alias for fmin and fmax.
n_per_decade is accepted as an alias for per_decade when
per_decade is left at its default.
Decimation And Moving Average Smoothing#
Use decimate_step to keep every step row. Use smooth_mavg to
apply a moving average along frequency.
1from pycsamt.emtools.frequency import decimate_step, smooth_mavg
2
3decimated = decimate_step(
4 "data/AMT/WILLY_DATA/L18PLT",
5 step=2,
6 inplace=False,
7)
8
9smoothed = smooth_mavg(
10 "data/AMT/WILLY_DATA/L18PLT",
11 k=3,
12 on="z",
13 inplace=False,
14)
Smoothing changes the measured tensor values. Use it as a processing choice, not as a plotting convenience, and report the window size.
Align Station Grids#
Use align_grid when stations need a common grid.
1from pycsamt.emtools.frequency import align_grid
2
3union_aligned = align_grid(
4 "data/AMT/WILLY_DATA/L18PLT",
5 mode="union",
6 method="nearest",
7)
8
9intersection_aligned = align_grid(
10 "data/AMT/WILLY_DATA/L18PLT",
11 mode="intersection",
12 method="nearest",
13)
14
15ref_aligned = align_grid(
16 "data/AMT/WILLY_DATA/L18PLT",
17 mode="ref",
18 ref_station="18-001A",
19 method="nearest",
20)
The modes are:
Mode |
Meaning |
|---|---|
|
Use all frequencies present in any station. |
|
Use only frequencies present in every station. |
|
Use the grid from |
intersection can be empty or nearly empty for independently
processed data because frequencies may not match bit-for-bit. If the
intersection is empty, the function returns the input sites unchanged.
Coverage And Quality Heatmap#
plot_coverage_quality_heatmap shows which station-frequency cells
exist and how reliable they are according to relative impedance error.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools.frequency import plot_coverage_quality_heatmap
4
5fig, ax = plt.subplots(figsize=(9, 4.5))
6plot_coverage_quality_heatmap(
7 "data/AMT/WILLY_DATA/L18PLT",
8 axis="period",
9 ax=ax,
10)
11fig.tight_layout()
The color is 1 / (1 + relative_error). Values closer to 1 are
higher quality. Missing cells indicate no frequency row on the union
grid for that station.
Apparent Depth Pseudo-Section#
plot_apparent_depth_psection converts determinant apparent
resistivity and frequency into a skin-depth-style apparent depth:
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools.frequency import plot_apparent_depth_psection
4
5fig, ax = plt.subplots(figsize=(9, 4.5))
6plot_apparent_depth_psection(
7 "data/AMT/WILLY_DATA/L18PLT",
8 axis_y="period",
9 agg="median",
10 log_color=True,
11 ax=ax,
12)
13fig.tight_layout()
This is a diagnostic visualization, not an inversion result. Use it to see how apparent depth varies across stations and periods.
Band Microstrips#
plot_band_microstrips collapses periods into bands and plots three
summary metrics per station and band:
median
log10determinant apparent resistivity;median determinant phase;
median tipper amplitude when available.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools.frequency import plot_band_microstrips
4
5bands = [
6 (1e-4, 1e-3),
7 (1e-3, 1e-2),
8 (1e-2, 1e-1),
9 (1e-1, 1.0),
10]
11
12fig, ax = plt.subplots(figsize=(9, 6))
13plot_band_microstrips(
14 "data/AMT/WILLY_DATA/L18PLT",
15 bands=bands,
16 ax=ax,
17)
18fig.tight_layout()
Use microstrips for compact survey summaries. They are not a substitute for full curves when a band contains strong internal variation.
Recommended Processing Pattern#
A conservative frequency workflow looks like this:
Inspect the native frequency range and confidence table.
Select a physically meaningful band.
Decide whether bad rows should be dropped, masked, or recovered.
Save the edit report and decision table.
Regrid or align only when the next workflow requires it.
Plot coverage and apparent depth after editing.
Keep the original data available for audit.
1from pathlib import Path
2
3import matplotlib.pyplot as plt
4
5from pycsamt.emtools.frequency import (
6 edit_frequencies_by_confidence,
7 plot_apparent_depth_psection,
8 plot_coverage_quality_heatmap,
9 regrid_logspace,
10 select_band,
11)
12
13survey = "data/AMT/WILLY_DATA/L18PLT"
14out = Path("outputs/frequency_l18plt")
15out.mkdir(parents=True, exist_ok=True)
16
17banded = select_band(survey, fmin=10.0, fmax=1000.0)
18result = edit_frequencies_by_confidence(
19 banded,
20 mode="recover",
21 before_sites=banded,
22 ci_hi=0.72,
23 ci_lo=0.50,
24 reject="drop",
25 api=False,
26)
27regular = regrid_logspace(result.sites, fmin=10.0, fmax=1000.0, per_decade=6)
28
29result.report.to_csv(out / "frequency_edit_report.csv", index=False)
30result.decisions.to_csv(out / "frequency_edit_decisions.csv", index=False)
31
32fig1, ax1 = plt.subplots(figsize=(9, 4.5))
33plot_coverage_quality_heatmap(regular, ax=ax1)
34fig1.savefig(out / "coverage_quality_heatmap.png", dpi=200)
35
36fig2, ax2 = plt.subplots(figsize=(9, 4.5))
37plot_apparent_depth_psection(regular, ax=ax2)
38fig2.savefig(out / "apparent_depth_psection.png", dpi=200)
Common Failure Modes#
- Using default
ci_hiwithout checking confidence Recovery needs trusted rows at or above
ci_hi. If the survey confidence never reaches that value, recovery cannot behave as expected.- Dropping rows before checking downstream needs
Dropping shortens the grid. If the next step expects aligned grids, masking may be safer.
- Masking rows before plotting finite-only workflows
Some workflows ignore or fail on
NaNrows. Save a decision table so missing values are explainable.- Regridding too early
Interpolation can hide native acquisition problems. Inspect native confidence first.
- Empty intersection alignment
Independently processed stations may have no exact shared frequency. Use
unionor a reference station when appropriate.- Smoothing as hidden preprocessing
Moving averages change tensor values. Report the window and apply it intentionally.
Worked Example#
The gallery example uses L18PLT from data/AMT/WILLY_DATA/ and
brings in KAP03 for heterogeneous-grid alignment. It demonstrates
band selection, confidence editing, recovery, reports, decision plots,
regridding, decimation, smoothing, grid alignment, coverage heatmaps,
apparent-depth pseudo-sections, and band microstrips.
Open the rendered example here: Frequency editing, resampling, and QC (pycsamt.emtools.frequency).