Source Effects And Near-Field Correction#
pycsamt.emtools.source_effects helps diagnose when the artificial
CSAMT transmitter is influencing the measured response. Natural-source
MT interpretation assumes a plane-wave source. CSAMT does not have that
luxury: the transmitter has a finite offset from each receiver, and the
offset can control whether a station-frequency row behaves like near
field, transition field, or far field.
The module contains two related but independent families of tools:
Yan and Fu / Da et al. source-overprint diagnostics, based on the ground-wave to surface-wave amplitude ratio \(\beta_{Ey}\).
Wang and Lin normalized-response and near-field correction tools, based on skin-depth field zones and an equatorial horizontal electric dipole correction factor.
Full function signatures and parameter defaults are maintained in the
API reference. This guide uses the public
two-level imports from pycsamt.emtools.
Why Offset Matters#
Every source-effect calculation needs a source-receiver offset r.
Standard EDI files usually do not store the CSAMT transmitter geometry,
so you must provide the offset explicitly unless your station objects
already carry an attribute such as source_offset, offset, or
dist.
The offset can be supplied as a scalar or as a station dictionary:
1source_offset = 2000.0
2
3source_offset_by_station = {
4 "18-001A": 1800.0,
5 "18-002A": 1950.0,
6 "18-003A": 2100.0,
7}
Use a scalar only when the same representative offset is justified for all stations. For field processing, prefer a station dictionary derived from transmitter and receiver coordinates.
Workflow Map#
Goal |
Use this |
Output |
|---|---|---|
Evaluate pure overprint beta |
|
\(\beta_{Ey}\) in percent for arrays or scalars. |
Build per-frequency overprint table |
|
Long-form table with |
Summarize overprint by station |
|
Station table with max/mean beta, fraction flagged, and slopes. |
Plot beta pseudo-section |
|
Station-period map of \(\beta_{Ey}\). |
Normalize response |
|
Apparent-resistivity ratio, phase residual, field zone, and |
Correct near-field response |
|
|
Plot normalized response |
|
Two-panel pseudo-section of normalized resistivity and phase. |
Loading A Survey#
Load the survey once with ensure_sites. Keep the raw object unchanged
while you inspect source effects and test correction settings.
1from pycsamt.emtools import ensure_sites
2
3sites = ensure_sites("data/AMT/WILLY_DATA/L18PLT", recursive=True)
4source_offset = 2000.0
The examples below use 2000.0 meters only as a concrete processing
example. In a real CSAMT project, replace it with measured geometry.
Overprint Beta#
overprint_beta is the pure mathematical interface. It does not need
EDI files. It evaluates the Yan and Fu ground-wave to surface-wave ratio
and returns \(\beta_{Ey}\) in percent.
1import numpy as np
2
3from pycsamt.emtools import BETA_THRESH_PCT, overprint_beta
4
5freq = np.logspace(-1, 3, 60)
6rho = 300.0
7
8for offset in (500.0, 2000.0, 8000.0):
9 beta_pct = overprint_beta(rho=rho, freq=freq, offset=offset)
10 contaminated = freq[beta_pct > BETA_THRESH_PCT]
11 if contaminated.size:
12 print(
13 f"offset={offset:g} m: beta>{BETA_THRESH_PCT:g}% "
14 f"up to {contaminated.max():.3g} Hz"
15 )
offset=500 m: beta>3% up to 1e+03 Hz
offset=2000 m: beta>3% up to 392 Hz
offset=8000 m: beta>3% up to 27.6 Hz
BETA_THRESH_PCT is 3.0. Values above that threshold indicate
potential source overprint under the Yan and Fu criterion. The threshold
is useful, but the exact result depends strongly on rho, frequency,
and offset.
Per-Frequency Overprint Detection#
detect_source_overprint applies overprint_beta to every
station-frequency row using apparent resistivity computed from the
observed impedance tensor.
1from pycsamt.emtools import detect_source_overprint, ensure_sites
2
3sites = ensure_sites("data/AMT/WILLY_DATA/L18PLT", recursive=True)
4
5detail = detect_source_overprint(
6 sites,
7 source_offset=2000.0,
8 beta_threshold=3.0,
9)
10
11print(detail.head())
12print(detail["beta_pct"].describe())
13print(detail["overprint_flag"].value_counts())
station freq_hz period_s ... kr beta_pct overprint_flag
0 18-001A 10400.0 0.000096 ... 65.313194 2.893811e-17 False
1 18-001A 8707.0 0.000115 ... 57.126799 8.278746e-15 False
2 18-001A 7289.0 0.000137 ... 50.931906 5.904384e-13 False
3 18-001A 6102.0 0.000164 ... 40.883790 5.793214e-10 False
4 18-001A 5108.0 0.000196 ... 32.557630 1.670487e-07 False
[5 rows x 8 columns]
count 1.484000e+03
mean 2.321404e+01
std 2.174905e+01
min 1.008563e-38
25% 7.988633e-02
50% 1.701784e+01
75% 4.876054e+01
max 4.999804e+01
Name: beta_pct, dtype: float64
overprint_flag
True 944
False 540
Name: count, dtype: int64
The returned table has one row per station and frequency:
station, freq_hz, period_s, offset_m, rho_a_ohmm,
kr, beta_pct, overprint_flag
Rows with unknown offset keep the station and frequency information, but
kr and beta_pct are NaN. That is intentional: source-effect
diagnostics cannot be inferred honestly without geometry.
Station-Level Summary#
source_overprint_table summarizes the long-form table by station. It
adds maximum and mean \(\beta\), the number and fraction of flagged
rows, and a low-/high-frequency slope comparison inspired by Da et al.
1from pycsamt.emtools import ensure_sites, source_overprint_table
2
3sites = ensure_sites("data/AMT/WILLY_DATA/L18PLT", recursive=True)
4
5summary = source_overprint_table(
6 sites,
7 source_offset=2000.0,
8 beta_threshold=3.0,
9 f_split=50.0,
10)
11
12cols = [
13 "station",
14 "beta_max_pct",
15 "beta_mean_pct",
16 "n_overprint",
17 "overprint_frac",
18 "lf_slope",
19 "hf_slope",
20 "slope_delta",
21 "overprint_flag",
22]
23print(summary[cols].sort_values("overprint_frac", ascending=False).head())
station beta_max_pct beta_mean_pct ... hf_slope slope_delta overprint_flag
20 18-021B 49.992100 29.888614 ... 0.071821 -0.115000 True
21 18-021U 49.996806 30.257094 ... 0.268957 -0.598151 True
14 18-015U 49.902471 29.096502 ... -0.443341 1.169621 True
0 18-001A 49.994860 26.105160 ... -0.319272 0.165193 True
19 18-020A 49.997356 27.507910 ... 0.334994 -1.132669 True
[5 rows x 9 columns]
f_split separates low- and high-frequency bands for slope analysis.
Choose it from the actual survey frequency range. If the split falls
outside the sampled range, one of the slope columns will be NaN.
Overprint Pseudo-Section#
plot_overprint_section maps \(\beta_{Ey}\) across station and
period. It can contour key beta levels, including the 3 percent
threshold.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools import ensure_sites, plot_overprint_section
4
5sites = ensure_sites("data/AMT/WILLY_DATA/L18PLT", recursive=True)
6
7fig, ax = plt.subplots(figsize=(10, 5))
8plot_overprint_section(
9 sites,
10 source_offset=2000.0,
11 beta_threshold=3.0,
12 beta_levels=(1.0, 3.0, 10.0, 30.0),
13 period_axis=True,
14 log_y=True,
15 ax=ax,
16)
17fig.tight_layout()
18fig.savefig("source_overprint_section_l18plt.png", dpi=200)
19plt.close(fig)
Use this plot to see whether source contamination is localized to specific stations, periods, or broad regions of the line. If most of the plot sits above the threshold, the assumed offset may place much of the survey outside a clean far-field regime.
Normalized Response#
normalize_response implements the Wang and Lin view of source
effects. It computes:
It also classifies each row using the skin-depth relation \(\delta = 503\sqrt{\rho_a/f}\) and the source offset:
nearwhenr / delta < 0.5;transitionwhen0.5 <= r / delta < 4;farwhenr / delta >= 4.
1from pycsamt.emtools import ensure_sites, normalize_response
2
3sites = ensure_sites("data/AMT/WILLY_DATA/L18PLT", recursive=True)
4
5norm = normalize_response(
6 sites,
7 rho_ref=300.0,
8 source_offset=2000.0,
9 comp="det",
10 phi_ref_deg=45.0,
11)
12
13print(norm.head())
14print(norm["zone"].value_counts(dropna=False))
15print(norm[["station", "freq_hz", "rho_n", "phi_diff_deg", "zone", "kr"]].head())
station freq_hz period_s ... phi_diff_deg zone kr
0 18-001A 10400.0 0.000096 ... -104.871322 far 46.210224
1 18-001A 8707.0 0.000115 ... -105.705417 far 40.418207
2 18-001A 7289.0 0.000137 ... -106.709024 far 36.035211
3 18-001A 6102.0 0.000164 ... -107.090701 far 28.925994
4 18-001A 5108.0 0.000196 ... -112.671464 far 23.035091
[5 rows x 11 columns]
zone
far 603
transition 468
near 413
Name: count, dtype: int64
station freq_hz rho_n phi_diff_deg zone kr
0 18-001A 10400.0 0.256661 -104.871322 far 46.210224
1 18-001A 8707.0 0.280878 -105.705417 far 40.418207
2 18-001A 7289.0 0.295813 -106.709024 far 36.035211
3 18-001A 6102.0 0.384325 -107.090701 far 28.925994
4 18-001A 5108.0 0.507310 -112.671464 far 23.035091
Use comp="det" for a determinant-style response, or "xy" /
"yx" when a specific off-diagonal component is the interpretation
target.
Normalized-Response Plot#
plot_normalized_response draws the normalized resistivity and
subtracted phase as two side-by-side pseudo-sections.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools import ensure_sites, plot_normalized_response
4
5sites = ensure_sites("data/AMT/WILLY_DATA/L18PLT", recursive=True)
6
7fig, axes = plt.subplots(1, 2, figsize=(13, 5))
8plot_normalized_response(
9 sites,
10 rho_ref=300.0,
11 source_offset=2000.0,
12 comp="det",
13 phi_ref_deg=45.0,
14 axes=axes,
15)
16fig.tight_layout()
17fig.savefig("source_normalized_response_l18plt.png", dpi=200)
18plt.close(fig)
The left panel answers whether apparent resistivity is high or low
relative to the reference half-space. The right panel answers whether
phase is above or below the reference phase. Read both panels alongside
the field-zone column from normalize_response.
Near-Field Correction#
correct_near_field divides each impedance tensor row by a complex
near-field factor:
The factor tends toward 1 in the far field. In the near field it can
be very large, so the correction can strongly change apparent
resistivity.
1from pycsamt.emtools import correct_near_field, ensure_sites
2
3sites = ensure_sites("data/AMT/WILLY_DATA/L18PLT", recursive=True)
4
5corrected = correct_near_field(
6 sites,
7 source_offset=2000.0,
8 inplace=False,
9)
Use inplace=False while testing. If a correction changes a station by
orders of magnitude, treat that as a diagnostic result, not just a
processed output. It means the raw response was far from the plane-wave
assumption under the supplied offset.
Comparing Before And After#
You can compare source-effect diagnostics before and after correction without reaching into private helpers. For example, compare normalized response tables:
1from pycsamt.emtools import (
2 correct_near_field,
3 ensure_sites,
4 normalize_response,
5)
6
7raw = ensure_sites("data/AMT/WILLY_DATA/L18PLT", recursive=True)
8corrected = correct_near_field(raw, source_offset=2000.0, inplace=False)
9
10before = normalize_response(raw, rho_ref=300.0, source_offset=2000.0)
11after = normalize_response(corrected, rho_ref=300.0, source_offset=2000.0)
12
13joined = before.merge(
14 after,
15 on=["station", "freq_hz"],
16 suffixes=("_raw", "_corrected"),
17)
18joined["rho_n_ratio"] = joined["rho_n_corrected"] / joined["rho_n_raw"]
19
20print(
21 joined[
22 ["station", "freq_hz", "zone_raw", "rho_n_raw", "rho_n_corrected", "rho_n_ratio"]
23 ].head()
24)
station freq_hz zone_raw rho_n_raw rho_n_corrected rho_n_ratio
0 18-001A 10400.0 far 0.256661 0.256665 1.000015
1 18-001A 8707.0 far 0.280878 0.280884 1.000022
2 18-001A 7289.0 far 0.295813 0.295822 1.000031
3 18-001A 6102.0 far 0.384325 0.384347 1.000059
4 18-001A 5108.0 far 0.507310 0.507369 1.000115
This style keeps the comparison in public tables and is easier to document than extracting impedance arrays directly.
Combining The Two Diagnostics#
The Yan/Fu beta flag and Wang/Lin field-zone label come from different physical arguments. Agreement between them is a strong warning that source geometry is controlling part of the response.
1from pycsamt.emtools import (
2 detect_source_overprint,
3 ensure_sites,
4 normalize_response,
5)
6
7sites = ensure_sites("data/AMT/WILLY_DATA/L18PLT", recursive=True)
8
9beta = detect_source_overprint(sites, source_offset=2000.0)
10zones = normalize_response(sites, rho_ref=300.0, source_offset=2000.0)
11
12merged = beta.merge(
13 zones[["station", "freq_hz", "zone", "kr"]],
14 on=["station", "freq_hz"],
15 how="left",
16)
17
18print(merged.groupby("zone")["overprint_flag"].mean())
19print(merged.groupby("zone")["beta_pct"].describe())
zone
far 0.104478
near 1.000000
transition 1.000000
Name: overprint_flag, dtype: float64
count mean std ... 50% 75% max
zone ...
far 603.0 0.763099 1.397676 ... 0.003908 0.796716 5.785135
near 413.0 49.569662 0.497681 ... 49.797164 49.934040 49.998036
transition 468.0 28.882940 14.214307 ... 30.364481 43.199542 47.934809
[3 rows x 8 columns]
If near and transition rows are usually overprint-flagged while
far rows are mostly unflagged, the two methods are telling a
consistent story. If they disagree, inspect the assumed offset,
reference resistivity, and frequency range.
Choosing Offsets#
For real processing, offsets should come from survey geometry. A useful pattern is to build a station dictionary and pass it to every function:
1from pycsamt.emtools import (
2 detect_source_overprint,
3 normalize_response,
4 source_overprint_table,
5)
6
7offset_by_station = {
8 "18-001A": 1800.0,
9 "18-002A": 1900.0,
10 "18-003A": 2050.0,
11 # Continue for the full line.
12}
13
14detail = detect_source_overprint(sites, source_offset=offset_by_station)
15summary = source_overprint_table(sites, source_offset=offset_by_station)
16norm = normalize_response(sites, source_offset=offset_by_station)
Keep the same offset dictionary across all source-effect diagnostics so the beta table, normalized-response table, field zones, and correction are comparable.
Suggested Review Sequence#
Use this sequence before applying a correction:
1from pycsamt.emtools import (
2 detect_source_overprint,
3 normalize_response,
4 source_overprint_table,
5)
6
7detail = detect_source_overprint(sites, source_offset=2000.0)
8summary = source_overprint_table(sites, source_offset=2000.0, f_split=50.0)
9norm = normalize_response(sites, rho_ref=300.0, source_offset=2000.0)
10
11print(detail["overprint_flag"].mean())
12print(summary.sort_values("overprint_frac", ascending=False).head())
13print(norm["zone"].value_counts(dropna=False))
0.6361185983827493
station n_freq offset_m ... hf_slope slope_delta overprint_flag
20 18-021B 53 2000.0 ... 0.071821 -0.115000 True
21 18-021U 53 2000.0 ... 0.268957 -0.598151 True
14 18-015U 53 2000.0 ... -0.443341 1.169621 True
0 18-001A 53 2000.0 ... -0.319272 0.165193 True
19 18-020A 53 2000.0 ... 0.334994 -1.132669 True
[5 rows x 11 columns]
zone
far 603
transition 468
near 413
Name: count, dtype: int64
Then plot:
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools import plot_normalized_response, plot_overprint_section
4
5fig, ax = plt.subplots(figsize=(10, 5))
6plot_overprint_section(sites, source_offset=2000.0, ax=ax)
7fig.tight_layout()
8fig.savefig("source_review_overprint_section_l18plt.png", dpi=200)
9plt.close(fig)
10
11fig, axes = plt.subplots(1, 2, figsize=(13, 5))
12plot_normalized_response(
13 sites,
14 rho_ref=300.0,
15 source_offset=2000.0,
16 axes=axes,
17)
18fig.tight_layout()
19fig.savefig("source_review_normalized_response_l18plt.png", dpi=200)
20plt.close(fig)
Correct only after the diagnostics show that correction is scientifically justified and after the offset geometry has been checked.
Pitfalls#
Do not invent the source offset from the impedance. The offset is survey geometry and should come from field records or transmitter/receiver coordinates.
Do not interpret a representative scalar offset as a final result for a line with varying transmitter distance. A scalar is fine for examples or sensitivity tests; station-specific offsets are better for processing.
Do not treat near-field correction as harmless smoothing. It changes the impedance tensor and can alter apparent resistivity by large factors in near-field rows.
Do not use the Da et al. slope columns without checking f_split. A
split outside the sampled frequency range produces undefined low- or
high-frequency slopes.
Worked Example#
The example uses the L18PLT survey with an explicitly stated representative offset. It demonstrates the pure beta formula, per-row overprint detection, station summaries, overprint pseudo-sections, normalized response, near-field correction, and comparison between the two independent source-effect diagnostics.
Open the rendered gallery page here: CSAMT source overprint and near-field effects (pycsamt.emtools.source_effects).