Condition an MT Line With Tipper and Rotation#

This tutorial shows a pre-inversion conditioning workflow for an MT line that has both full impedance tensors and tipper data. It uses the bundled KP sample line:

data/MT/kap03lmt_edis

The aim is to make the processing decisions visible before inversion:

  • inspect raw tensor curves and tipper response;

  • identify weak frequency rows;

  • apply conservative recovery/filtering;

  • inspect static-shift factors before applying them;

  • estimate strike and plot phase tensors;

  • rotate impedance and tipper into a consistent frame.

This is an advanced tutorial. It deliberately avoids a single “automatic clean” button because MT conditioning is interpretive: every destructive or scaling operation should leave a trace in a table or figure.

Load the KP Line#

 1from pathlib import Path
 2
 3from pycsamt.api import read_edis
 4
 5edi_dir = Path("data/MT/kap03lmt_edis")
 6survey = read_edis(
 7    edi_dir,
 8    recursive=False,
 9    strict=False,
10    on_dup="replace",
11    progress=False,
12)
13sites = survey.collection
14
15print(survey.summary())

Example output:

APIFrame: edi_survey_summary
kind: edi.summary
shape: 26 rows x 6 columns
columns: station, path, n_freq, tipper, spectra, ts
numeric: 1 columns
missing: 0.0%
source: data/MT/kap03lmt_edis

Every loaded KP station has tipper rows in this sample:

station  n_freq  tipper  spectra
 kap103      20    True    False
 kap106      20    True    False
 kap109      18    True    False
 kap112      20    True    False
 kap115      20    True    False
 kap118      20    True    False

Plot Raw Tensor Curves#

Before removing frequencies or scaling tensors, plot the raw components. The off-diagonal components Zxy and Zyx normally carry the TE/TM response, while the diagonal components Zxx and Zyy reveal 3-D effects, noise, or rotation issues.

 1import numpy as np
 2
 3def rho_phase(site, comp):
 4    freq = np.asarray(site.Z.freq, dtype=float)
 5    z = np.asarray(site.Z.z, dtype=complex)[:, comp[0], comp[1]]
 6    rho = 0.2 * np.abs(z) ** 2 / np.maximum(freq, 1e-30)
 7    phase = np.angle(z, deg=True)
 8    return freq, rho, phase
 9
10for station in ["kap103", "kap112", "kap136", "kap169"]:
11    site = next(site for site in sites if site.station == station)
12    freq, rho_xy, phi_xy = rho_phase(site, (0, 1))
13    freq, rho_yx, phi_yx = rho_phase(site, (1, 0))

The generated figures show that this line is not a simple two-component data set; diagonal terms and phase behavior need to be reviewed before rotation.

Raw KP apparent resistivity tensor components
Raw KP phase tensor components

Plot Tipper Components#

Tipper data help identify lateral conductivity gradients and 3-D structure. The KP EDI files store the tipper container as site.Tip:

1site = next(site for site in sites if site.station == "kap103")
2tip = site.Tip
3
4freq = tip.freq
5tx = tip.tipper[:, 0, 0]
6ty = tip.tipper[:, 0, 1]
7
8tx_amp = abs(tx)
9ty_amp = abs(ty)
Raw KP tipper amplitudes

Build QC Tables#

Use station-level and frequency-level tables before deciding what to suppress:

 1from pycsamt.emtools import (
 2    build_qc_table,
 3    frequency_confidence_table,
 4    station_confidence_table,
 5)
 6
 7qc = build_qc_table(
 8    sites,
 9    include_skew=True,
10    recursive=False,
11    api=True,
12).to_pandas(copy=True)
13
14station_ci = station_confidence_table(
15    sites,
16    method="composite",
17    recursive=False,
18    api=True,
19).to_pandas(copy=True)
20
21freq_ci = frequency_confidence_table(
22    sites,
23    method="composite",
24    ci_hi=0.9,
25    ci_lo=0.5,
26    recursive=False,
27    api=True,
28).to_pandas(copy=True)

Example station QC:

station  n_freq  n_tip  frac_ok  snr_med  skew_med
 kap103      20     20    1.000   36.494    50.912
 kap106      20     20    1.000   33.926    54.898
 kap109      18     18    1.000   65.851    70.845
 kap112      20     20    1.000  174.211    62.911
 kap115      20     20    1.000  208.660    46.219
 kap118      20     20    1.000  203.016    25.692

The frequency screen found 518 station-frequency rows and 30 weak rows (confidence < 0.5), about 5.8 percent of the line.

KP frequency confidence by station
KP bad-frequency screening summary

Recover, Suppress, and Filter Conservatively#

For first-pass conditioning, do not invent a full new response. Use a narrow sequence that preserves the trend and marks weak rows:

 1from pycsamt.emtools import (
 2    hampel_filter_freq,
 3    notch_powerline,
 4    recover_low_confidence_frequencies,
 5    smooth_rho_phase,
 6)
 7
 8notched = notch_powerline(
 9    sites,
10    mains_hz=50.0,
11    n_harm=20,
12    tol_hz=0.06,
13    recursive=False,
14)
15
16recovered = recover_low_confidence_frequencies(
17    notched,
18    method="composite",
19    ci_hi=0.9,
20    ci_lo=0.5,
21    interpolation="linear",
22    recursive=False,
23)
24
25filtered = hampel_filter_freq(
26    recovered,
27    win=3,
28    nsig=3.0,
29    recursive=False,
30)
31
32conditioned = smooth_rho_phase(
33    filtered,
34    components="offdiag",
35    degree=3,
36    smooth_rho=True,
37    smooth_phase=True,
38    recursive=False,
39)

Power-line notching is included as a diagnostic conditioning step. If your survey has no rows near the local mains harmonics, the step should have little or no effect; keep the figure or report that proves that.

KP raw and conditioned apparent resistivity curves

Review and Apply Static Shift#

Static shift changes apparent resistivity scale. It should not change phase. Estimate factors, review them, then apply only defensible values:

 1from pycsamt.emtools import apply_ss_factors, estimate_ss_ama
 2
 3factors = estimate_ss_ama(
 4    sites,
 5    sort_by="name",
 6    half_window=3,
 7    max_skew=None,
 8    recursive=False,
 9    api=True,
10).to_pandas(copy=True)
11
12factors["fac_z_reviewed"] = factors["fac_z"].clip(
13    lower=0.35,
14    upper=2.85,
15)
16
17reviewed = factors[["station", "fac_z_reviewed"]].rename(
18    columns={"fac_z_reviewed": "fac_z"}
19)
20
21shifted = apply_ss_factors(
22    sites,
23    reviewed,
24    key="fac_z",
25    inplace=False,
26    recursive=False,
27)

The clipping in this example is not a universal rule. It is a teaching guard: large factors should be inspected, justified, or rejected before they are allowed to rescale impedance.

station  fac_z  fac_z_reviewed  n_used
 kap103   1.91            1.91      20
 kap106   1.61            1.61      20
 kap109  0.529           0.529      18
 kap112  0.237            0.35      20
 kap115   3.17            2.85      20
 kap118  0.289            0.35      20
 kap121  0.578           0.578      20
 kap123   1.35            1.35      20
KP static-shift factors before and after review clipping
KP static-shift before and after apparent resistivity

Estimate Strike and Plot Phase Tensors#

After QC and static-shift review, estimate a dominant strike direction. The example uses Swift-style strike values from the anisotropy table and a circular mean with 180-degree periodicity:

 1import numpy as np
 2
 3from pycsamt.emtools.anisotropy import analyze_anisotropy
 4
 5detail = analyze_anisotropy(shifted, recursive=False)
 6strikes = detail["strike_deg"].dropna().to_numpy()
 7
 8doubled = np.deg2rad(2.0 * strikes)
 9dominant = 0.5 * np.rad2deg(
10    np.arctan2(np.sin(doubled).mean(), np.cos(doubled).mean())
11)
12print(dominant)

For this run the dominant strike is about -3.97 degrees. The rose diagram also shows the spread, which matters more than a single number.

KP strike rose diagram

Phase tensor ellipses show orientation, ellipticity, and skew-like behavior without relying on static-shift-sensitive amplitudes:

1from pycsamt.emtools.tensor import build_phase_tensor_table
2
3pt = build_phase_tensor_table(shifted, recursive=False)
4print(pt[["station", "period", "theta", "beta", "ellipt"]].head())
KP phase tensor ellipse grid

Rotate Impedance and Tipper#

Rotate both impedance and tipper into the selected coordinate frame before exporting inversion-ready EDIs:

1import copy
2
3rotated = copy.deepcopy(shifted)
4for site in rotated:
5    site.Z.rotate(dominant)
6    if getattr(site, "Tip", None) is not None:
7        site.Tip.rotate(dominant)

The goal of rotation is not to make the data look perfect. It should reduce coordinate-frame mixing and make TE/TM separation more interpretable when the strike estimate is stable enough.

KP impedance before and after rotation

Processing Decision Table#

Summarise the choices before writing processed EDIs:

KP MT conditioning decision table

For a production run, save:

  • the raw QC tables;

  • the weak-frequency table;

  • the static-shift factor table before and after review;

  • the strike estimate and rotation angle;

  • the processed EDI folder;

  • a short note explaining any rejected stations or frequency bands.

Adapting This Tutorial#

For your own MT data, change only the input folder and representative station names first:

1edi_dir = Path("path/to/your/mt_edis")
2stations_to_plot = ["S001", "S010", "S020", "S030"]

Then rerun the same sequence. If the survey lacks tipper, skip the tipper plots but keep the tensor, QC, static-shift, phase-tensor, and rotation steps. If the strike rose is broad or multimodal, do not force a single rotation angle; split the line into domains or keep the original coordinate frame.

See Also#

Inspect and QC a Survey

One-line QC tables and confidence diagnostics.

Correct Static Shift

Conservative static-shift correction workflow.

Prepare an Occam2D Inversion

Prepare inversion files after the line has been conditioned.

Run a Pipeline From Config

Move stable processing decisions into a reusable config file.