Impedance-Tensor Diagnostics#
pycsamt.emtools.impedance gives direct views of the complex
impedance tensor before it is reduced to apparent resistivity, phase,
phase-tensor attributes, dimensionality classes, or inversion input.
Use this page when you want to look at the tensor itself and ask:
do
ZxyandZyxbehave like an approximately antisymmetric 1-D/2-D response?are diagonal terms small compared with off-diagonal terms?
does one station have a different complex trajectory from its neighbours?
is the determinant response stable enough to trust as a compact rotationally invariant summary?
Full callable signatures live in the API reference. This page focuses on interpretation, concrete workflows, and code.
What The Module Uses#
All public functions normalize their input through ensure_sites.
That means the same call can accept a directory of EDI files, an
existing Sites object, an EDICollection, or EDI-like objects.
Each station must expose a complex impedance array shaped
(n_frequency, 2, 2) and a frequency array.
The tensor components are indexed as:
The diagnostics on this page use Z directly. That is important:
apparent resistivity and phase are derived products, while the phasor
wheel, antisymmetry residual, and determinant track keep the complex
tensor visible.
Load A Survey Once#
Use ensure_sites first when you are building a notebook or script.
That makes the rest of the code explicit and avoids reloading files for
each plot.
1from pathlib import Path
2
3from pycsamt.emtools import ensure_sites
4
5edi_dir = Path("data/AMT/WILLY_DATA/L18PLT")
6survey = ensure_sites(
7 edi_dir,
8 recursive=True,
9 on_dup="replace",
10 strict=True,
11 verbose=1,
12)
strict=True is useful in documentation, tests, and reproducible
analysis because it fails early when no valid sites are found. In an
interactive exploratory session you may prefer strict=False so an
empty or partly broken directory can still be inspected.
Choose The Right Diagnostic#
The three public views answer different questions.
View |
Best Question |
Main Output |
|---|---|---|
phasor wheel |
How do selected complex tensor components move with period at one station? |
A polar Argand-style plot for one station. |
antisymmetry residual |
Where along the line do |
A station-period pseudo-section. |
determinant track |
Is a compact rotationally invariant station response stable, and how wide is its uncertainty band? |
Magnitude and phase curves versus period. |
The views are complementary. The phasor wheel is local and visual, the residual pseudo-section is survey-wide, and the determinant track is a station-level summary.
The Phasor Wheel#
plot_phasor_wheel draws selected impedance components as complex
phasors. For each frequency sample, the component phase becomes the
polar angle and the component magnitude becomes the radius. Colour
encodes log-period, so a station becomes a period-ordered complex
trajectory.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools import ensure_sites
4from pycsamt.emtools.impedance import plot_phasor_wheel
5
6survey = ensure_sites("data/AMT/WILLY_DATA/L18PLT", strict=True)
7
8ax = plot_phasor_wheel(
9 survey,
10 station="18-001A",
11 components=("xy", "yx"),
12 radius="abs",
13 connect=True,
14 figsize=(5.0, 5.0),
15)
16
17ax.figure.savefig("phasor_wheel_18-001A.png", dpi=200)
18plt.close(ax.figure)
Read this plot as a direct complex-plane diagnostic. If Zxy and
Zyx are close to a clean 1-D/2-D off-diagonal pair, their phasors
should be broadly opposite in phase, because the ideal relation is
Zxy ~= -Zyx. If the two arcs bend into different sectors, cross,
or change separation with period, the station is telling you that one
simple dimensional picture is probably not enough.
Use Period Bands#
The pband argument selects a period interval in seconds. This is a
simple way to ask whether shallow and deeper parts of the sounding have
different tensor behaviour.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools import ensure_sites
4from pycsamt.emtools.impedance import plot_phasor_wheel
5
6survey = ensure_sites("data/AMT/WILLY_DATA/L18PLT", strict=True)
7station = "18-001A"
8
9fig, axes = plt.subplots(
10 1,
11 2,
12 figsize=(9.5, 5.0),
13 subplot_kw={"polar": True},
14)
15
16plot_phasor_wheel(
17 survey,
18 station=station,
19 pband=(9e-5, 1e-3),
20 ax=axes[0],
21)
22axes[0].set_title("short periods")
23
24plot_phasor_wheel(
25 survey,
26 station=station,
27 pband=(1e-1, 1.0),
28 ax=axes[1],
29)
30axes[1].set_title("long periods")
31
32fig.tight_layout()
33fig.savefig("phasor_period_bands_18-001A.png", dpi=200)
34plt.close(fig)
If the short-period and long-period arcs have the same shape but
different radii, the main change is magnitude. If they rotate into
different angular sectors or the xy and yx relation changes,
the complex response itself is changing with period.
Include The Diagonal Terms#
For a simple 1-D earth, the diagonal tensor components are ideally
zero. For a 2-D earth in the correct strike coordinate system, the
off-diagonal terms dominate. In field data, Zxx and Zyy rarely
vanish exactly, but their size relative to Zxy and Zyx is still
informative.
1from pycsamt.emtools import ensure_sites
2from pycsamt.emtools.impedance import plot_phasor_wheel
3
4survey = ensure_sites("data/AMT/WILLY_DATA/L18PLT", strict=True)
5
6ax = plot_phasor_wheel(
7 survey,
8 station="18-001A",
9 components=("xy", "yx", "xx", "yy"),
10 radius="norm",
11 connect=False,
12 ms=2.5,
13)
14
15ax.figure.savefig("phasor_all_components_18-001A.png", dpi=200)
radius="norm" scales each component radius by a robust component
magnitude, which helps when one component would otherwise dominate the
display. Use radius="abs" when you need the physical size relation
to remain visible.
Compute Component Magnitudes#
The plot is useful, but a station report should also write the numbers.
The lower-level helper _get_z_block returns the validated tensor
and frequency arrays used by the plotting functions.
1import numpy as np
2
3from pycsamt.emtools import ensure_sites
4from pycsamt.emtools._core import _get_z_block, _iter_items, _name
5
6survey = ensure_sites("data/AMT/WILLY_DATA/L18PLT", strict=True)
7
8rows = []
9for index, site in enumerate(_iter_items(survey)):
10 _, z, freq = _get_z_block(site)
11 if z is None:
12 continue
13
14 rows.append(
15 {
16 "station": _name(site, index),
17 "mean_abs_zxx": float(np.nanmean(np.abs(z[:, 0, 0]))),
18 "mean_abs_zxy": float(np.nanmean(np.abs(z[:, 0, 1]))),
19 "mean_abs_zyx": float(np.nanmean(np.abs(z[:, 1, 0]))),
20 "mean_abs_zyy": float(np.nanmean(np.abs(z[:, 1, 1]))),
21 }
22 )
23
24for row in rows[:5]:
25 print(row)
{'station': '18-001A', 'mean_abs_zxx': 446.87532535812795, 'mean_abs_zxy': 808.4345401982367, 'mean_abs_zyx': 1145.0965447882752, 'mean_abs_zyy': 557.6049786167763}
{'station': '18-002U', 'mean_abs_zxx': 133.0926461161051, 'mean_abs_zxy': 620.1474584088546, 'mean_abs_zyx': 859.6134726088661, 'mean_abs_zyy': 257.84731425866534}
{'station': '18-003A', 'mean_abs_zxx': 109.30601909208245, 'mean_abs_zxy': 610.4449839811526, 'mean_abs_zyx': 388.2097256381032, 'mean_abs_zyy': 106.6560768811014}
{'station': '18-004A', 'mean_abs_zxx': 252.91269027986166, 'mean_abs_zxy': 839.1242870947025, 'mean_abs_zyx': 681.5954183841726, 'mean_abs_zyy': 300.46444606989644}
{'station': '18-005U', 'mean_abs_zxx': 202.61429236412803, 'mean_abs_zxy': 811.4948595592589, 'mean_abs_zyx': 563.3054411247073, 'mean_abs_zyy': 261.3767440473993}
This is not meant to replace phase-tensor dimensionality or skew analysis. It is a quick tensor sanity check: if the diagonal terms are large at a station, look at dimensionality, distortion, static shift, and strike diagnostics before treating that station as simple 2-D input.
Off-Diagonal Antisymmetry#
plot_offdiag_antisym_residual maps how far the off-diagonal
components depart from the ideal cancellation relation:
The implementation clips the result to 0 <= r <= 1. Values near
zero mean the off-diagonal terms cancel well. Larger values mean the
two off-diagonal terms are less antisymmetric.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools import ensure_sites
4from pycsamt.emtools.impedance import plot_offdiag_antisym_residual
5
6survey = ensure_sites("data/AMT/WILLY_DATA/L18PLT", strict=True)
7
8fig, ax = plt.subplots(figsize=(10.0, 4.8))
9plot_offdiag_antisym_residual(
10 survey,
11 vlim=0.8,
12 cmap="magma",
13 ax=ax,
14)
15ax.set_title("L18PLT off-diagonal antisymmetry residual")
16
17fig.tight_layout()
18fig.savefig("offdiag_antisymmetry_l18plt.png", dpi=200)
19plt.close(fig)
The horizontal axis is station order. The vertical axis is
log10(period). Warm columns mark stations or period bands where
Zxy and Zyx fail to cancel. That does not prove a particular
geology by itself, but it is a strong cue to compare against
phase-tensor skew, anisotropy ratio, induction arrows, and nearby
lines.
Rank Stations By Residual#
The pseudo-section shows the pattern. A table names the stations.
1import numpy as np
2import pandas as pd
3
4from pycsamt.emtools import ensure_sites
5from pycsamt.emtools._core import _get_z_block, _iter_items, _name
6
7survey = ensure_sites("data/AMT/WILLY_DATA/L18PLT", strict=True)
8
9rows = []
10for index, site in enumerate(_iter_items(survey)):
11 _, z, freq = _get_z_block(site)
12 if z is None:
13 continue
14
15 xy = np.abs(z[:, 0, 1])
16 yx = np.abs(z[:, 1, 0])
17 residual = np.abs(z[:, 0, 1] + z[:, 1, 0]) / (xy + yx + 1e-24)
18 residual = np.clip(residual, 0.0, 1.0)
19
20 rows.append(
21 {
22 "station": _name(site, index),
23 "mean_residual": float(np.nanmean(residual)),
24 "p90_residual": float(np.nanpercentile(residual, 90)),
25 "max_residual": float(np.nanmax(residual)),
26 "n_frequency": int(np.isfinite(residual).sum()),
27 }
28 )
29
30ranking = (
31 pd.DataFrame(rows)
32 .sort_values("mean_residual", ascending=False)
33 .reset_index(drop=True)
34)
35
36print(ranking.head(10))
37ranking.to_csv("impedance_antisymmetry_ranking.csv", index=False)
station mean_residual p90_residual max_residual n_frequency
0 18-016A 0.786452 0.942456 0.958245 53
1 18-018A 0.749186 0.961107 0.998407 53
2 18-017U 0.715415 0.877221 0.920305 53
3 18-023A 0.640977 0.860594 0.957943 53
4 18-021B 0.623736 0.964924 0.990659 53
5 18-021U 0.556974 0.968757 0.980216 53
6 18-022U 0.538863 0.873291 0.963180 53
7 18-024U 0.526460 0.811147 0.961759 53
8 18-015U 0.500570 0.984507 0.999133 53
9 18-025A 0.498164 0.782128 0.842395 53
Use mean_residual for a broad station ranking. Use
p90_residual when you want to highlight stations with a persistent
high-residual tail. Use max_residual only as a trigger for manual
inspection because one bad frequency can dominate it.
Compare With Anisotropy Or Skew#
The antisymmetry residual is related to, but not identical with, diagonal skew or apparent anisotropy. The best practice is to compare metrics instead of assuming they flag the same stations.
1import numpy as np
2import pandas as pd
3
4from pycsamt.emtools import anisotropy_table, ensure_sites
5from pycsamt.emtools._core import _get_z_block, _iter_items, _name
6
7survey = ensure_sites("data/AMT/WILLY_DATA/L18PLT", strict=True)
8
9residual_rows = []
10for index, site in enumerate(_iter_items(survey)):
11 _, z, freq = _get_z_block(site)
12 if z is None:
13 continue
14
15 xy = np.abs(z[:, 0, 1])
16 yx = np.abs(z[:, 1, 0])
17 residual = np.abs(z[:, 0, 1] + z[:, 1, 0]) / (xy + yx + 1e-24)
18 residual_rows.append(
19 {
20 "station": _name(site, index),
21 "antisym_mean": float(np.nanmean(np.clip(residual, 0.0, 1.0))),
22 }
23 )
24
25residual_df = pd.DataFrame(residual_rows).set_index("station")
26aniso_df = anisotropy_table(survey).set_index("station")
27
28merged = residual_df.join(
29 aniso_df[["mean_swift_skew", "mean_ratio_log10"]],
30 how="inner",
31)
32merged["abs_ratio_log10"] = merged["mean_ratio_log10"].abs()
33
34print(merged.corr(numeric_only=True))
antisym_mean ... abs_ratio_log10
antisym_mean 1.000000 ... 0.719606
mean_swift_skew -0.592220 ... -0.613406
mean_ratio_log10 0.631375 ... 0.886388
abs_ratio_log10 0.719606 ... 1.000000
[4 rows x 4 columns]
When correlations are strong, the diagnostics are probably responding to a shared tensor feature. When they disagree, inspect the stations manually. A station can have a high skew because of diagonal terms but still have off-diagonal components that nearly cancel.
Determinant Track#
plot_determinant_track summarizes a station with the determinant of
the full impedance tensor:
The plot has two panels: |det(Z)| and determinant phase versus
period. If impedance errors are available as z_err, pyCSAMT draws a
Monte Carlo uncertainty band for the determinant magnitude.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools import ensure_sites
4from pycsamt.emtools.impedance import plot_determinant_track
5
6survey = ensure_sites("data/AMT/WILLY_DATA/L18PLT", strict=True)
7
8fig = plot_determinant_track(
9 survey,
10 station="18-016A",
11 pband=(1e-4, 1.0),
12 pcts=(10.0, 50.0, 90.0),
13 n_draws=300,
14 figsize=(6.8, 4.2),
15)
16
17fig.savefig("determinant_track_18-016A.png", dpi=200)
18plt.close(fig)
The default percentile band is the 10th to 90th percentile interval.
Increase n_draws when you need a smoother band. Keep seed fixed
only if you call the lower-level determinant helper directly and need
bit-for-bit reproducibility in a report.
Compare Two Stations#
The determinant track is most useful when compared across stations with contrasting tensor diagnostics.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools import ensure_sites
4from pycsamt.emtools.impedance import plot_determinant_track
5
6survey = ensure_sites("data/AMT/WILLY_DATA/L18PLT", strict=True)
7
8fig = plt.figure(figsize=(11.0, 4.8))
9grid = fig.add_gridspec(
10 2,
11 2,
12 height_ratios=(2, 1),
13 hspace=0.08,
14 wspace=0.25,
15)
16
17left_axes = (fig.add_subplot(grid[0, 0]), fig.add_subplot(grid[1, 0]))
18right_axes = (fig.add_subplot(grid[0, 1]), fig.add_subplot(grid[1, 1]))
19
20plot_determinant_track(survey, station="18-016A", axes=left_axes)
21plot_determinant_track(survey, station="18-007U", axes=right_axes)
22
23left_axes[0].set_title("high residual station")
24right_axes[0].set_title("low residual station")
25
26fig.savefig("determinant_station_comparison.png", dpi=200)
27plt.close(fig)
Look for differences in curve smoothness, phase wrapping, band width, and period-local instability. A wide uncertainty band does not automatically make the station unusable, but it should lower your confidence in interpretations that depend on small determinant features.
Measure Determinant Band Width#
For reports, compute a simple relative band-width number instead of judging the shaded region by eye.
1import numpy as np
2import pandas as pd
3
4from pycsamt.emtools import ensure_sites
5from pycsamt.emtools._core import _get_z_block, _iter_items, _name
6from pycsamt.emtools.impedance import _det_ci
7
8survey = ensure_sites("data/AMT/WILLY_DATA/L18PLT", strict=True)
9
10rows = []
11for index, site in enumerate(_iter_items(survey)):
12 _, z, freq, z_err = _get_z_block(site, with_errors=True)
13 if z is None:
14 continue
15
16 mag, phase, band = _det_ci(
17 z,
18 freq,
19 z_err,
20 pcts=(10.0, 50.0, 90.0),
21 n_draws=300,
22 seed=0,
23 )
24 relative_width = (band[:, 1] - band[:, 0]) / (mag + 1e-24)
25
26 rows.append(
27 {
28 "station": _name(site, index),
29 "median_det_abs": float(np.nanmedian(mag)),
30 "median_relative_band_width": float(
31 np.nanmedian(relative_width)
32 ),
33 "max_relative_band_width": float(np.nanmax(relative_width)),
34 }
35 )
36
37det_quality = (
38 pd.DataFrame(rows)
39 .sort_values("median_relative_band_width", ascending=False)
40 .reset_index(drop=True)
41)
42
43print(det_quality.head(10))
44det_quality.to_csv("determinant_band_width.csv", index=False)
station median_det_abs median_relative_band_width max_relative_band_width
0 18-021U 138059.459579 1.244798 1.558835
1 18-020A 58591.215698 0.981629 1.719329
2 18-021B 304870.766789 0.675971 1.572428
3 18-022V 54687.185702 0.414231 1.390426
4 18-018A 12533.221781 0.286987 1.416562
5 18-022U 59346.090486 0.272911 1.511982
6 18-025A 22893.891856 0.259362 1.010240
7 18-013U 409823.995492 0.242223 1.320911
8 18-024U 37132.445937 0.241634 0.978317
9 18-023A 57853.829945 0.223259 1.416815
The private helper _det_ci is used here because it exposes the
computed arrays behind the public plot. For stable production code,
prefer the public plotting function unless you need these exact numbers.
Compare Neighbouring Lines#
A warm residual column is more meaningful when the same style of feature appears on neighbouring survey lines or when it lines up with known geology. Use the same colour limit when comparing lines.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools import ensure_sites
4from pycsamt.emtools.impedance import plot_offdiag_antisym_residual
5
6line18 = ensure_sites("data/AMT/WILLY_DATA/L18PLT", strict=True)
7line22 = ensure_sites("data/AMT/WILLY_DATA/L22PLT", strict=True)
8
9fig, axes = plt.subplots(1, 2, figsize=(13.0, 5.0), sharey=True)
10
11plot_offdiag_antisym_residual(line18, vlim=0.8, ax=axes[0])
12axes[0].set_title("L18PLT")
13
14plot_offdiag_antisym_residual(line22, vlim=0.8, ax=axes[1])
15axes[1].set_title("L22PLT")
16
17fig.tight_layout()
18fig.savefig("antisymmetry_line_comparison.png", dpi=200)
19plt.close(fig)
If one line is uniformly warmer, check acquisition quality, processing settings, coordinate orientation, and frequency coverage before making a geological claim. If a localized warm zone repeats across lines, it is more likely to represent real structure or a persistent distortion effect.
Common Interpretation Checks#
Use these checks before promoting an impedance diagnostic into an interpretation:
ZxyandZyxdo not cancelCompare against phase-tensor skew, Swift skew, anisotropy ratio, and induction arrows. The residual is a warning flag, not a complete dimensionality classifier.
- Diagonal components are large
Inspect whether the coordinate frame is appropriate. If a 2-D strike rotation is justified, rotate before concluding that the response is strongly 3-D.
- Only one frequency is anomalous
Treat it as a possible processing or data-quality issue. Cross-check with QC and frequency-editing tools.
- The determinant band is wide
Check whether
z_erris realistic and whether the station has noisy or sparse frequency samples.- The phasor wheel is hard to read
Use
pbandto split the period range andcomponentsto reduce clutter. Useradius="norm"when component magnitudes differ too much for one display.
Saving A Reproducible Bundle#
The following script writes the main impedance outputs for one survey: one residual pseudo-section, one station ranking, one phasor wheel, and one determinant track.
1from pathlib import Path
2
3import matplotlib.pyplot as plt
4import numpy as np
5import pandas as pd
6
7from pycsamt.emtools import ensure_sites
8from pycsamt.emtools._core import _get_z_block, _iter_items, _name
9from pycsamt.emtools.impedance import (
10 plot_determinant_track,
11 plot_offdiag_antisym_residual,
12 plot_phasor_wheel,
13)
14
15out = Path("impedance_report_l18plt")
16out.mkdir(parents=True, exist_ok=True)
17
18survey = ensure_sites("data/AMT/WILLY_DATA/L18PLT", strict=True)
19
20fig, ax = plt.subplots(figsize=(10.0, 4.8))
21plot_offdiag_antisym_residual(survey, vlim=0.8, ax=ax)
22fig.tight_layout()
23fig.savefig(out / "antisymmetry_residual.png", dpi=200)
24plt.close(fig)
25
26rows = []
27for index, site in enumerate(_iter_items(survey)):
28 _, z, freq = _get_z_block(site)
29 if z is None:
30 continue
31 xy = np.abs(z[:, 0, 1])
32 yx = np.abs(z[:, 1, 0])
33 residual = np.clip(
34 np.abs(z[:, 0, 1] + z[:, 1, 0]) / (xy + yx + 1e-24),
35 0.0,
36 1.0,
37 )
38 rows.append(
39 {
40 "station": _name(site, index),
41 "mean_residual": float(np.nanmean(residual)),
42 "p90_residual": float(np.nanpercentile(residual, 90)),
43 }
44 )
45
46ranking = pd.DataFrame(rows).sort_values(
47 "mean_residual",
48 ascending=False,
49)
50ranking.to_csv(out / "antisymmetry_ranking.csv", index=False)
51
52station = ranking.iloc[0]["station"]
53
54ax = plot_phasor_wheel(
55 survey,
56 station=station,
57 components=("xy", "yx", "xx", "yy"),
58 radius="norm",
59)
60ax.figure.savefig(out / f"phasor_{station}.png", dpi=200)
61plt.close(ax.figure)
62
63fig = plot_determinant_track(survey, station=station, n_draws=300)
64fig.savefig(out / f"determinant_{station}.png", dpi=200)
65plt.close(fig)
Worked Example#
The gallery example applies the same ideas to bundled WILLY survey lines and connects the impedance views with anisotropy rankings.
Open the rendered gallery page here: Impedance-tensor diagnostics (pycsamt.emtools.impedance).