Skew Diagnostics#
pycsamt.emtools.skew documents how far an impedance tensor departs
from a simple 1-D or 2-D response. It gives two complementary views of
skew:
Bahr skewness, written \(\eta\), computed directly from the complex impedance tensor
Z.Phase-tensor skew, written \(\beta\), computed through the phase-tensor table and reported in degrees.
Use skew diagnostics before 2-D preparation, dimensionality decisions, strike interpretation, frequency masking, and inversion setup. A low skew band supports a 1-D/2-D approximation. A high skew band warns that the tensor may contain 3-D structure, galvanic distortion, local noise, or component geometry that should not be forced into a simple 2-D workflow without more checks.
Full signatures and parameter defaults are maintained in the
API reference. This page explains how to use
the public workflow functions through pycsamt.emtools.
Two Skew Measures#
Bahr skewness is computed from impedance invariants. In pyCSAMT it is
available through bahr_skewness and accepts either a tensor block of
shape (n_freq, 2, 2) or a flattened block of shape (n_freq, 4).
Phase-tensor skew is the Caldwell-Bibby-Bahr phase-tensor angle
\(\beta\). skew_table returns the same table produced by the
phase-tensor workflow, including station, freq, period,
beta, and skew. In this module, skew is the phase-tensor
skew column used by the masking and voting helpers.
The two measures are related but not interchangeable. Bahr
\(\eta\) is a direct invariant of Z. Phase-tensor
\(\beta\) depends on the real and imaginary impedance parts through
the phase tensor. They can agree that a station is non-2-D while ranking
the severity differently.
Workflow Map#
Goal |
Use this |
Result |
|---|---|---|
Compute a station-frequency skew table |
|
A |
Compute Bahr skew from a Z block |
|
A one-dimensional array of \(\eta\) values. |
Mask rows above a skew threshold |
|
A |
Keep one contiguous low-skew run per station |
|
A station-wise band mask. |
Bridge short gaps inside low-skew runs |
|
A less fragmented station-wise mask. |
Keep a survey-wide low-skew band |
|
A shared frequency band selected by station vote. |
Plot skew quality across the line |
|
Pseudo-section, period ribbon, and vote-curve views. |
Plot Bahr skew for one station |
|
A single-station \(\eta\) plot against a threshold. |
Loading The Survey#
Load once with ensure_sites and reuse the same raw object for
diagnostics and before/after checks.
1from pathlib import Path
2
3from pycsamt.emtools import ensure_sites
4
5edi_dir = Path("data/AMT/WILLY_DATA/L18PLT")
6sites = ensure_sites(edi_dir, recursive=True, verbose=0)
Most masking functions default to inplace=False. That means the
returned Sites object is masked, while sites remains available
for comparison.
Phase-Tensor Skew Table#
Start with skew_table. It summarizes \(\beta\) at every
station-frequency row.
1from pycsamt.emtools import ensure_sites, skew_table
2
3sites = ensure_sites("data/AMT/WILLY_DATA/L18PLT", recursive=True)
4table = skew_table(sites)
5
6print(table.columns)
7print(table[["station", "freq", "period", "beta", "skew"]].head())
8print(table["skew"].abs().describe())
9
10station_summary = (
11 table.assign(abs_skew=table["skew"].abs())
12 .groupby("station", as_index=False)
13 .agg(
14 n_freq=("freq", "size"),
15 median_abs_skew=("abs_skew", "median"),
16 max_abs_skew=("abs_skew", "max"),
17 )
18 .sort_values("median_abs_skew")
19)
20print(station_summary.head())
21print(station_summary.tail())
Index(['station', 'freq', 'period', 's1', 's2', 'theta', 'alpha', 'beta',
'skew', 'ellipt'],
dtype='object')
station freq period beta skew
0 18-001A 10400.0 0.000096 -56.700714 -56.700714
1 18-001A 8707.0 0.000115 -54.693184 -54.693184
2 18-001A 7289.0 0.000137 -51.452210 -51.452210
3 18-001A 6102.0 0.000164 -61.983725 -61.983725
4 18-001A 5108.0 0.000196 -60.874439 -60.874439
count 1484.000000
mean 44.536824
std 25.198206
min 0.353578
25% 23.593647
50% 40.992429
75% 66.994007
max 89.910303
Name: skew, dtype: float64
station n_freq median_abs_skew max_abs_skew
14 18-015U 53 22.459269 73.709036
16 18-017U 53 22.912833 88.315919
15 18-016A 53 23.525350 87.383364
8 18-009A 53 25.288856 77.805353
9 18-010U 53 26.006304 88.810111
station n_freq median_abs_skew max_abs_skew
26 18-024U 53 63.853268 89.878126
22 18-022U 53 65.349813 89.438099
17 18-018A 53 66.547818 89.385210
23 18-022V 53 66.787861 89.910303
24 18-023A 53 67.022970 89.760407
Interpret skew and beta as angles in degrees. A common strict
phase-tensor threshold is around 3 to 6 degrees. Real CSAMT
survey lines can be much larger. When every station is above a strict
threshold, do not blindly mask the whole survey. First inspect the
distribution and decide whether the threshold is appropriate for the
question being asked.
Bahr Skewness#
Use bahr_skewness when you want an independent skew measure computed
directly from impedance.
1import numpy as np
2
3from pycsamt.emtools import bahr_skewness, ensure_sites
4
5sites = ensure_sites("data/AMT/WILLY_DATA/L18PLT", recursive=True)
6
7# Example: use the first station in the loaded Sites collection.
8station = next(iter(sites))
9z = station.z
10freq = station.freq
11
12eta = bahr_skewness(z)
13period = 1.0 / freq
14
15print(np.nanmin(eta), np.nanmedian(eta), np.nanmax(eta))
16print(period[:5], eta[:5])
0.8461749951030751 1.5856487464775848 2.9879890035270154
[9.61538462e-05 1.14850121e-04 1.37193031e-04 1.63880695e-04
1.95771339e-04] [2.97741225 2.987989 2.86896122 2.57559637 2.18481788]
The classic Bahr threshold often used as a 2-D/3-D guide is
eta = 0.4. Treat that as a diagnostic boundary, not an automatic
editing rule. Large \(\eta\) can reflect genuine 3-D structure, but
it can also reflect noise, distortion, or component problems.
Bahr Skew Plot#
plot_skewness plots Bahr \(\eta\) against log-period and draws
the threshold line.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools import ensure_sites, plot_skewness
4
5sites = ensure_sites("data/AMT/WILLY_DATA/L18PLT", recursive=True)
6station = next(iter(sites))
7
8fig, ax = plt.subplots(figsize=(7, 4))
9plot_skewness(
10 station.freq,
11 station.z,
12 threshold=0.4,
13 ax=ax,
14 title=str(getattr(station, "name", "station")),
15)
16fig.tight_layout()
17fig.savefig("bahr_skewness_18-001A.png", dpi=200)
18plt.close(fig)
Use this plot when you need to explain one station concretely. Use the survey-wide phase-tensor plots below when the question is line-scale dimensionality.
Masking By Skew#
mask_by_skew applies a phase-tensor skew threshold. The default mode
is "abs_gt": keep rows where abs(skew) <= thresh and set the
others to NaN.
1from pycsamt.emtools import ensure_sites, mask_by_skew
2
3sites = ensure_sites("data/AMT/WILLY_DATA/L18PLT", recursive=True)
4
5masked = mask_by_skew(
6 sites,
7 thresh=6.0,
8 mode="abs_gt",
9 also="both",
10 inplace=False,
11)
also controls which data blocks are masked:
"z"masks only impedance rows."tipper"masks only tipper rows."both"masks impedance and tipper rows at the same rejected frequencies.
Other modes are available for specialized workflows:
1from pycsamt.emtools import mask_by_skew
2
3# Keep only rows with skew <= +6 degrees.
4keep_not_greater = mask_by_skew(sites, thresh=6.0, mode="gt")
5
6# Keep only rows with skew >= -6 degrees.
7keep_not_less = mask_by_skew(sites, thresh=-6.0, mode="lt")
8
9# Keep rows where absolute skew is at least 6 degrees.
10# This is unusual for inversion preparation, but useful for isolating
11# high-skew examples in diagnostics.
12keep_high_skew = mask_by_skew(sites, thresh=6.0, mode="abs_lt")
Because masking writes NaN into rejected tensor rows, always verify
how many usable rows remain before sending the result to an inversion or
plotting workflow.
Counting Surviving Rows#
A simple count helper is often enough for documentation and QC tables.
1import numpy as np
2
3from pycsamt.emtools import ensure_sites, mask_by_skew
4
5raw = ensure_sites("data/AMT/WILLY_DATA/L18PLT", recursive=True)
6masked = mask_by_skew(raw, thresh=6.0, also="z", inplace=False)
7
8rows = []
9for station in masked:
10 z = station.z
11 good = np.isfinite(z).all(axis=(1, 2))
12 rows.append(
13 {
14 "station": getattr(station, "name", ""),
15 "n_total": int(z.shape[0]),
16 "n_kept": int(good.sum()),
17 "kept_fraction": float(good.mean()),
18 }
19 )
20
21print(rows[:5])
[{'station': '18-001A', 'n_total': 53, 'n_kept': 2, 'kept_fraction': 0.03773584905660377}, {'station': '18-002U', 'n_total': 53, 'n_kept': 1, 'kept_fraction': 0.018867924528301886}, {'station': '18-003A', 'n_total': 53, 'n_kept': 1, 'kept_fraction': 0.018867924528301886}, {'station': '18-004A', 'n_total': 53, 'n_kept': 2, 'kept_fraction': 0.03773584905660377}, {'station': '18-005U', 'n_total': 53, 'n_kept': 3, 'kept_fraction': 0.05660377358490566}]
This count is the sanity check that prevents a silent all-NaN result
from moving downstream. If strict thresholds keep too little data, return
to the skew distribution and decide whether a looser survey-specific
threshold is justified.
Longest Low-Skew Run#
keep_longest_low_skew is stricter than row-by-row masking. For each
station, it finds the longest contiguous frequency run satisfying
abs(skew) <= thresh and masks everything else.
1from pycsamt.emtools import ensure_sites, keep_longest_low_skew
2
3sites = ensure_sites("data/AMT/WILLY_DATA/L18PLT", recursive=True)
4
5banded = keep_longest_low_skew(
6 sites,
7 thresh=6.0,
8 min_len=3,
9 pad=1,
10 also="both",
11 fallback="keep_all",
12 inplace=False,
13)
min_len is important. If a station has only one or two isolated
low-skew rows, you probably do not want to call that a usable band.
pad expands the selected run by a few neighboring frequencies.
The fallback controls what happens when no acceptable run exists:
fallback="keep_all"preserves the station rather than destroying it. This is safer for exploratory diagnostics, but it can make a failed threshold look like a fully accepted station unless you count rows.fallback="drop_all"masks the station completely when no run passes. This is stricter and should be used only when downstream code can handle missing stations or all-NaNrows.
Closing Small Gaps#
close_skew_gaps fills short interior gaps inside a low-skew mask.
It is useful when one or two noisy frequency samples break an otherwise
continuous band.
1from pycsamt.emtools import close_skew_gaps, ensure_sites
2
3sites = ensure_sites("data/AMT/WILLY_DATA/L18PLT", recursive=True)
4
5gap_closed = close_skew_gaps(
6 sites,
7 thresh=6.0,
8 max_gap=1,
9 also="both",
10 inplace=False,
11)
Set max_gap=0 to disable gap closing. Increase it cautiously. A
large value can bridge unrelated good segments and create a band that no
longer represents a genuinely continuous low-skew interval.
Survey-Wide Low-Skew Band#
select_low_skew_band looks for frequencies supported by a fraction
of stations. The function first builds each station’s own acceptable
band, then votes on a union frequency grid. Rows are kept where at least
frac of stations support the band.
1from pycsamt.emtools import ensure_sites, select_low_skew_band
2
3sites = ensure_sites("data/AMT/WILLY_DATA/L18PLT", recursive=True)
4
5shared = select_low_skew_band(
6 sites,
7 thresh=6.0,
8 frac=0.6,
9 min_len=3,
10 pad=0,
11 also="both",
12 inplace=False,
13)
Use this function when you need one common frequency band for a line, for example before a shared inversion setup or a line-scale dimensionality statement. It can be much stricter than a plot showing the raw fraction of low-skew rows, because it votes on station-specific contiguous bands rather than isolated pointwise passes.
Traffic-Light Pseudo-Section#
plot_skew_traffic_psection colors each station-period cell by
absolute phase-tensor skew:
green:
abs(beta) <= t1;amber:
t1 < abs(beta) <= t2;red:
abs(beta) > t2.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools import ensure_sites, plot_skew_traffic_psection
4
5sites = ensure_sites("data/AMT/WILLY_DATA/L18PLT", recursive=True)
6
7fig, ax = plt.subplots(figsize=(10, 5))
8plot_skew_traffic_psection(
9 sites,
10 t1=3.0,
11 t2=6.0,
12 axis_y="logperiod",
13 ax=ax,
14)
15fig.tight_layout()
16fig.savefig("skew_traffic_psection_l18plt.png", dpi=200)
17plt.close(fig)
For highly skewed surveys, strict t1=3 and t2=6 may make the
whole line red. That is still useful: it tells you the textbook
thresholds are not identifying a usable 2-D band. For explanatory
figures, you may also plot survey-specific relaxed thresholds, but label
them clearly.
Percentile Ribbon#
plot_skew_percentile_ribbon summarizes the whole line by period. It
plots median absolute skew and percentile bands through the period axis.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools import ensure_sites, plot_skew_percentile_ribbon
4
5sites = ensure_sites("data/AMT/WILLY_DATA/L18PLT", recursive=True)
6
7fig, ax = plt.subplots(figsize=(8, 4))
8plot_skew_percentile_ribbon(
9 sites,
10 n_bins=30,
11 q_lo=25.0,
12 q_hi=75.0,
13 extra=(10.0, 90.0),
14 ax=ax,
15)
16fig.tight_layout()
17fig.savefig("skew_percentile_ribbon_l18plt.png", dpi=200)
18plt.close(fig)
Use this plot to answer: “Is there any period range where the line as a whole becomes less skewed?” A consistently high ribbon means the line does not have a clean low-skew window under the chosen metric.
Vote-Band Plot#
plot_skew_vote_band plots the fraction of stations with
abs(beta) <= thresh in log-period bins.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools import ensure_sites, plot_skew_vote_band
4
5sites = ensure_sites("data/AMT/WILLY_DATA/L18PLT", recursive=True)
6
7fig, ax = plt.subplots(figsize=(8, 4))
8plot_skew_vote_band(
9 sites,
10 thresh=6.0,
11 n_bins=40,
12 ax=ax,
13)
14fig.tight_layout()
15fig.savefig("skew_vote_band_l18plt.png", dpi=200)
16plt.close(fig)
This plot is diagnostic only. It counts pointwise low-skew rows in each
period bin. select_low_skew_band is stricter because it also requires
station-wise contiguous bands before the survey-wide vote. The two can
therefore report different apparent support for the same threshold.
Suggested Interpretation Pattern#
A robust skew review usually follows this sequence:
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools import (
4 ensure_sites,
5 plot_skew_percentile_ribbon,
6 plot_skew_traffic_psection,
7 plot_skew_vote_band,
8 skew_table,
9)
10
11sites = ensure_sites("data/AMT/WILLY_DATA/L18PLT", recursive=True)
12table = skew_table(sites)
13
14print(table["skew"].abs().describe())
15
16fig, axes = plt.subplots(3, 1, figsize=(10, 12))
17plot_skew_traffic_psection(sites, t1=3.0, t2=6.0, ax=axes[0])
18plot_skew_percentile_ribbon(sites, ax=axes[1])
19plot_skew_vote_band(sites, thresh=6.0, ax=axes[2])
20fig.tight_layout()
21fig.savefig("skew_review_panels_l18plt.png", dpi=200)
22plt.close(fig)
count 1484.000000
mean 44.536824
std 25.198206
min 0.353578
25% 23.593647
50% 40.992429
75% 66.994007
max 89.910303
Name: skew, dtype: float64
Read the outputs together:
The table gives exact station-frequency values.
The traffic-light pseudo-section shows where high skew is located.
The percentile ribbon shows whether skew improves at some periods.
The vote-band plot shows whether enough stations pass the threshold at the same period.
Only after those checks should you choose mask_by_skew,
keep_longest_low_skew, close_skew_gaps, or
select_low_skew_band.
Pitfalls#
High skew is not automatically bad data. It may be the geologic signal. Use skew masks as interpretation tools, not as a reflexive cleaning step.
Do not compare Bahr \(\eta\) and phase-tensor \(\beta\) as if they have the same units. \(\eta\) is dimensionless; \(\beta\) is an angle in degrees.
Beware of fallback behavior in keep_longest_low_skew. If
fallback="keep_all", a station that fails the threshold completely
can return with all rows preserved. Count the surviving rows and inspect
the skew table.
Use also="z" when only impedance should drive downstream processing.
Use also="both" when tipper rows must stay aligned with impedance
masking.
Worked Example#
The example uses the L18PLT survey to compare phase-tensor skew and Bahr skewness, apply skew-based masks, keep contiguous low-skew bands, close small gaps, vote on a shared low-skew band, and generate the traffic-light, percentile-ribbon, vote-band, and Bahr-skew figures.
Open the rendered gallery page here: Phase-tensor and Bahr skew diagnostics (pycsamt.emtools.skew).