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.
Recommended Order#
The processing order used here is:
load the KP EDI line;
plot raw
Zxx,Zxy,Zyx,Zyyapparent resistivity and phase;plot raw tipper components;
build station and frequency confidence tables;
recover or suppress low-confidence frequency rows;
apply power-line notching and conservative outlier filtering;
estimate and review static-shift factors;
apply reviewed static-shift factors;
estimate strike and plot phase tensor ellipses;
rotate impedance and tipper before inversion export.
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.
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)
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.
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.
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
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.
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())
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.
Processing Decision Table#
Summarise the choices before writing processed EDIs:
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.