Induction arrows and tipper diagnostics (pycsamt.emtools.tf)#

pycsamt.emtools.tf turns the vertical-field (tipper) component of a transfer function into induction-vector diagnostics: complex-plane hodograms, polar plots, arrow maps at one or several periods, azimuth roses, period pseudo-sections, and a publication-style multi-panel map. All functions accept the same flexible input as the rest of emtools — a path, an EDIFile/EDICollection, an APISurvey, or an iterable of site-like objects.

This example works through the module from the simplest view (one station, raw tipper) to the most composite one (a multi-period, multi-panel map), using KAP03 — a real long-period MT profile from SAMTEX bundled at data/MT/kap03lmt_edis (see data/MT/README.md for origin and the required SAMTEX attribution). Unlike the AMT lines in data/AMT/WILLY_DATA/, KAP03 was recorded with a real vertical-field sensor, so every arrow below is genuine tipper, not synthetic.

The 26 stations run SW→NE (kap103kap175, profile index 0…25 below), covering periods from 25 s to about 17,067 s (~4.7 h). Their horizontal positions are not stored as flat easting/northing in these EDIs, so the station-position fallback in emtools is profile index — that affects only the horizontal spacing drawn below, not the tipper values themselves. The measurement axes (HX/HY at azimuth 0°/90°, ZROT) are geomagnetic-north-aligned at all but 3 of the 26 stations, so the tipper components themselves are in a consistent, near-geographic frame — this example still avoids naming compass directions for on-screen azimuths, since tf.py’s polar/rose plots apply their own north-up, clockwise screen convention on top of that.

Throughout, azimuth comparisons below are expressed either as rotation amounts (degrees of change, which do not depend on that screen convention) or read directly off a chart’s own printed degree labels — both are safe regardless of the underlying convention.

Load the KAP03 survey#

_datasets.py is a small shared helper (not itself a gallery example) that resolves pyCSAMT’s bundled sample datasets by name, so every emtools example script can load real data in one line instead of repeating path/read_edis boilerplate. Passing the resulting APISurvey directly to each plotting call below re-uses the already-parsed data — no re-reading from disk between figures.

import numpy as np
from _datasets import load_survey

from pycsamt.emtools import (
    plot_induction_arrows,
    plot_induction_convention,
    plot_induction_map,
    plot_induction_multiperiod_map,
    plot_induction_rose,
    plot_induction_section,
    plot_tipper_hodograms,
    plot_tipper_polar,
)

survey = load_survey("mt_kap03")
names = survey.stations  # already SW->NE, kap103...kap175
s0 = survey.get_site(names[0])
period = 1.0 / s0.Z.freq
p_min, p_med, p_max = (
    float(period.min()),
    float(np.median(period)),
    float(period.max()),
)

# Rank stations by mean tipper amplitude across the whole band — the
# first real diagnostic: which sites see the strongest response overall.
amplitude = {}
for name in names:
    site = survey.get_site(name)
    if site.has_tipper:
        amplitude[name] = float(np.abs(site.Tip.tipper).mean())
strongest = max(amplitude, key=amplitude.get)
ranked = sorted(amplitude, key=amplitude.get, reverse=True)
print(
    f"strongest station (whole-band mean): {strongest} ({amplitude[strongest]:.3f})"
)
print("next four:", [(n, round(amplitude[n], 3)) for n in ranked[1:5]])
strongest station (whole-band mean): kap151 (0.740)
next four: [('kap121', 0.305), ('kap127', 0.305), ('kap123', 0.294), ('kap106', 0.256)]

1. Tipper hodograms — the raw building block#

Before looking at arrows or roses, it helps to see the tipper itself: for one station, plot_tipper_hodograms() plots T_x and T_y as loci in the complex plane, one colour per period band, so the period-dependent behaviour is visible with no azimuth/magnitude conversion in the way.

kap151 has the largest whole-band-mean response of all 26 stations — nearly 2.5x the next-strongest sites (kap121, kap127) — which makes it the natural single station to inspect in detail before looking at the full profile.

plot_tipper_hodograms(
    survey,
    station=strongest,
    bands=[(p_min, 200.0), (200.0, 2000.0), (2000.0, p_max)],
    unit_circle=True,
)
kap151 • Tx, kap151 • Ty
<Figure size 640x320 with 2 Axes>

Reading this figure. The shortest-period band (dark) and, especially, the 200-2000 s band both reach well outside the unit circle — real tipper amplitudes routinely exceed 1 for a strong, genuine anomaly, and kap151’s do, peaking with |T| ≈ 2.3 near 200 s. The longest-period band (light) contracts back down close to the origin. So this is not a response that simply grows or shrinks monotonically with period: it is concentrated in a broad but bounded window roughly from 50 s to 1600 s, which the pseudo-section further below confirms directly.

2. Polar tipper — magnitude and rotation vs. period#

plot_tipper_polar() folds the real tipper component into (azimuth, magnitude) per frequency, coloured by log10(period), for a single station.

plot_tipper_polar(survey, station=strongest, component="real")
kap151 — tipper polar [real]
<PolarAxes: title={'center': 'kap151 — tipper polar [real]'}>

Reading this figure. The radius (magnitude) is small at the shortest recorded period, swings out to its largest value around the middle of the band, then contracts back in toward the longest periods — the same peaked, band-limited behaviour seen in the hodogram above, now visible directly as a loop rather than as two separate complex-plane panels. The polar angle also sweeps through roughly 140-150° across the band; a smooth, one-directional sweep like this (rather than a scattered cloud) is a good sign that the tipper estimate is well-conditioned at this site.

3. Induction-arrow map at one period#

plot_induction_map() draws both the real (solid) and imaginary (dashed) Parkinson arrows for every station at one period, coloured by |T|, with a reference-length arrow and a colorbar — the richer, single-period counterpart to the profile view in the next section. scale is set explicitly here because the stations’ 1-unit index spacing would otherwise make the default auto-scaled arrows too small to see next to the full 26-station axis.

plot_induction_map(survey, period=2000.0, station_labels=True, scale=4.0)
Induction arrows  —  T = 2000 s  [park]
<Axes: title={'center': 'Induction arrows  —  T = 2000 s  [park]'}, xlabel='x / Easting  (m)', ylabel='y / Northing  (m)'>

Reading this figure. At T = 2000 s the largest arrows sit at the SW end of the profile (around kap106-kap121, profile index 1-6), with a second, smaller cluster around kap142-kap152 (index 14-18). kap151 itself — the overall strongest station by whole-band mean — shows only a modest arrow here: its exceptional response is concentrated at shorter periods (see above), not at 2000 s. That is a useful caution about whole-band averages: they can hide exactly when in the band a station’s anomaly actually occurs.

4. Induction arrows across three periods#

plot_induction_arrows() overlays arrows at several periods on the same profile axis, one colour per period — the fastest way to see how the response changes with period along a whole line at once.

plot_induction_arrows(
    survey,
    periods=[p_min, p_med, p_max],
    convention="park",
    strike_ticks=False,
)
plot tf
<Axes: xlabel='Station index / x', ylabel='Arrow (arb.)'>

Reading this figure. With 26 stations sharing the same index-based y = 0 baseline, the arrows overlap enough that direction is hard to compare station-to-station by eye here — the rose and pseudo-section below are the better tools for that. Numerically, though, this three-period choice is not arbitrary: kap127 has the single largest response at the shortest period (25 s), kap151 at the middle period, and the kap103-kap121 cluster at the longest period — three different parts of the profile take turns dominating as period changes, which is exactly what this figure is built to show at a glance once you know where to look.

5. Sign-convention comparison#

Induction vectors are drawn with two common sign conventions (Parkinson and Wiese, 90° apart) and two components (real and imaginary). plot_induction_convention() lays out all four combinations side by side for one period so the difference is visible directly, rather than memorised from a formula.

plot_induction_convention(survey, period=p_med, station_labels=False)
Induction arrow conventions  —  T = 666.667 s, Parkinson — Real, Parkinson — Imaginary, Wiese — Real, Wiese — Imaginary
array([[<Axes: title={'center': 'Parkinson — Real'}, xlabel='x  (m)', ylabel='y  (m)'>,
        <Axes: title={'center': 'Parkinson — Imaginary'}, xlabel='x  (m)', ylabel='y  (m)'>],
       [<Axes: title={'center': 'Wiese — Real'}, xlabel='x  (m)', ylabel='y  (m)'>,
        <Axes: title={'center': 'Wiese — Imaginary'}, xlabel='x  (m)', ylabel='y  (m)'>]],
      dtype=object)

Reading this figure. The four panels contain the same underlying data rotated/selected differently: Wiese arrows are the Parkinson arrows rotated 90°, and the imaginary panels pick out the out-of-phase part of the response. When reading induction vectors from a paper, always check which convention the author used — a Parkinson arrow pointing at a conductor becomes a Wiese arrow pointing 90° away from it, a common source of misread MT figures.

6. Azimuth rose — short vs. long period#

plot_induction_rose() histograms arrow azimuths, across all stations, into a rose diagram. Restricting the period band with pband lets you compare shallow vs. deep-sounding behaviour for the whole profile at once, rather than one station at a time.

plot_induction_rose(
    survey,
    component="real",
    pband=(p_min, 200.0),
    title="Induction rose — short period (25-200 s)",
)
Induction rose — short period (25-200 s)
<PolarAxes: title={'center': 'Induction rose — short period (25-200 s)'}>
plot_induction_rose(
    survey,
    component="real",
    pband=(2000.0, p_max),
    title="Induction rose — long period (2000-17067 s)",
)
Induction rose — long period (2000-17067 s)
<PolarAxes: title={'center': 'Induction rose — long period (2000-17067 s)'}>

Reading these figures. The short-period rose spreads bars across most of the circle, with no single dominant sector — a signature of scattered, near-surface heterogeneity. The long-period rose, by contrast, concentrates almost entirely within a narrow arc of the circle. That contrast is not just a rose-diagram artefact: computed directly from the data, the 6 strongest short-period stations scatter across more than 100° of azimuth from each other, while the 6 strongest long-period stations agree to within about 5°. A profile where the strongest responders point the same way only gets tighter as period grows is good evidence for one coherent, laterally continuous deep conductor, rather than several unrelated shallow ones.

7. Period pseudo-section#

plot_induction_section() grids |T| onto a station × log10(period) pseudo-section — the most compact single view of how the tipper anomaly is distributed both along the profile and with period at once.

plot_induction_section(survey, component="abs")
Tipper section  [abs]
<Axes: title={'center': 'Tipper section  [abs]'}, xlabel='Station', ylabel='$\\log_{10}(T)$ (s)'>

Reading this figure. One column — kap151 — is saturated dark across nearly the whole plotted period range, the same broad 50-1600 s window identified in the hodogram and polar plots. A fainter, shorter-lived warm patch sits around kap121-kap130 at short-to-mid period, matching the 25 s ranking above. The SW-end columns (kap103-kap118) show a mild warming trend toward the bottom (longer periods) of the section — the same long-period dominance seen in the rose and arrow map, though here it only reads as a faint tint because kap151’s much larger magnitude sets the colour scale. That is a real limitation of any single colour-coded section: always cross-check a dominant anomaly against the other views before assuming it is the only thing there.

8. Multi-period map (publication style)#

plot_induction_multiperiod_map() stacks one panel per period with real Parkinson arrows over a background raster, in the style used in published induction-vector figures (e.g. Boukhalfa et al., 2020). It is the most composite function in this module — combining what sections 3, 4, and 7 showed separately into one figure meant for a paper or report.

These EDIs carry no real topography, so the greyscale-to-green background here is the function’s built-in synthetic terrain placeholder, not real elevation — pass your own background/background_extent arrays (e.g. from a DEM) to replace it in a real report. Without an explicit tipper_data override, this function’s default EDI read-back keeps only the Tx component (arrows would be purely horizontal); the tipper_data dict built below supplies the genuine 2-component tipper instead, exactly as the function’s own docstring recommends for real 2-D vectors.

periods = [p_min, 653.2, 2000.0, p_max]
tipper_data = {}
for p in periods:
    rows = []
    for name in names:
        site = survey.get_site(name)
        t = site.Tip.tipper[:, 0, :]
        per = 1.0 / site.Z.freq
        j = int(np.argmin(np.abs(per - p)))
        rows.append([t[j, 0], t[j, 1]])
    tipper_data[p] = np.array(rows, dtype=complex)

plot_induction_multiperiod_map(
    survey,
    periods=periods,
    tipper_data=tipper_data,
    arrow_scale=6.0,
    show_background_cbar=False,
    station_labels=False,
    title="KAP03 — induction vectors across the recorded period band",
)
KAP03 — induction vectors across the recorded period band
(<Figure size 850x1260 with 4 Axes>, array([<Axes: ylabel='y  (m)'>, <Axes: ylabel='y  (m)'>,
       <Axes: ylabel='y  (m)'>, <Axes: xlabel='x  (m)', ylabel='y  (m)'>],
      dtype=object))

Reading this figure. Panel (A), 25 s, shows one clearly oversized arrow near the SW-mid profile (kap127) and small arrows elsewhere. Panel (B), 653 s, is dominated by a single much larger arrow (kap151) pointing a visibly different way from its smaller, mutually aligned neighbours — the same “strong but different direction” signature found numerically above. Panel (C), 2000 s, has no outlier at all; the modest response concentrates in the SW segment. Panel (D), the longest period (~17,067 s), is the cleanest: the SW segment’s arrows are both visibly larger and more consistently aligned than the rest of the profile — the figure a report would show to justify focusing a 2-D/3-D inversion on that part of the line.

Total running time of the script: (0 minutes 1.827 seconds)

Gallery generated by Sphinx-Gallery