CSAMT Field-Zone Classification#
pycsamt.emtools.fieldzone checks whether a controlled-source AMT
measurement is far enough from the transmitter to behave like a
plane-wave MT response. This matters because the usual apparent
resistivity formula assumes far-field behavior. If the receiver is in
the near or transition zone, apparent resistivity can be biased by the
source geometry rather than only by subsurface structure.
This page is specific to controlled-source work. Natural-source AMT/MT does not have a transmitter offset, so the field-zone workflow needs either a real source-receiver distance or an explicit assumed distance for sensitivity testing.
Full callable signatures live in the API reference. This page focuses on the workflow, outputs, examples, and interpretation.
The Field-Zone Parameter#
The core quantity is the dimensionless distance:
where r is the source-receiver offset in metres and
delta_B is the Bostick depth approximation:
The measured apparent resistivity is computed from the two off-diagonal impedance modes using the practical EDI convention used in pyCSAMT:
Higher frequencies and larger offsets increase |k r| and push the
measurement toward the far field. Larger apparent resistivity increases
delta_B and can push the same offset back toward transition or near
field.
Zone Rules#
The default classification thresholds are:
1far_threshold = 3.0
2near_threshold = 0.3
The labels are assigned as:
1if kr >= far_threshold:
2 zone = "far"
3elif kr >= near_threshold:
4 zone = "transition"
5else:
6 zone = "near"
Interpret the zones as:
Zone |
Practical meaning |
|---|---|
|
Plane-wave approximation is usually acceptable. |
|
Source geometry may influence the response; inspect before inversion. |
|
Plane-wave apparent resistivity is not trustworthy without controlled-source correction or exclusion. |
Offset Inputs#
The field-zone functions need source_offset in metres. You can pass
it in three ways:
Input |
Meaning |
|---|---|
|
Use one offset for every station. |
|
Use station-specific offsets. |
|
Try to read offset-like attributes from each station object. |
When source_offset is a dictionary, missing stations are skipped
unless the station object itself has an offset attribute. For real
CSAMT, prefer station-specific offsets from survey geometry.
Classify Field Zones#
Use classify_field_zones for the main per-station, per-frequency
table.
1from pathlib import Path
2
3from pycsamt.emtools.fieldzone import classify_field_zones
4
5edi_dir = Path("data/AMT/WILLY_DATA/L18PLT")
6
7zones = classify_field_zones(
8 edi_dir,
9 source_offset=2000.0,
10 far_threshold=3.0,
11 near_threshold=0.3,
12 recursive=True,
13 on_dup="replace",
14 strict=False,
15 verbose=0,
16)
17
18print(zones.head())
19zones.to_csv("l18plt_field_zones.csv", index=False)
station freq_hz period_s ... delta_bostick_m kr zone
0 18-001A 10400.0 0.000096 ... 30.631900 65.291412 far
1 18-001A 8707.0 0.000115 ... 35.021518 57.107747 far
2 18-001A 7289.0 0.000137 ... 39.281217 50.914919 far
3 18-001A 6102.0 0.000164 ... 48.935465 40.870155 far
4 18-001A 5108.0 0.000196 ... 61.450026 32.546772 far
[5 rows x 8 columns]
The output columns are:
station: station name.freq_hzandperiod_s: measured frequency and period.offset_m: source-receiver offset used for the calculation.rho_a_ohmm: determinant-style apparent resistivity.delta_bostick_m: Bostick depth approximation.kr: dimensionless|k r|.zone:far,transition, ornear.
Single-Station Curve#
Inspecting kr against period makes the classification easy to see.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools.fieldzone import classify_field_zones
4
5zones = classify_field_zones(
6 "data/AMT/WILLY_DATA/L18PLT",
7 source_offset=2000.0,
8)
9
10station = "18-001A"
11one = zones.loc[zones["station"] == station].sort_values("period_s")
12
13fig, ax = plt.subplots(figsize=(7, 4.5))
14ax.axhspan(3.0, 1e6, color="tab:green", alpha=0.08)
15ax.axhspan(0.3, 3.0, color="tab:orange", alpha=0.10)
16ax.axhspan(1e-6, 0.3, color="tab:red", alpha=0.08)
17ax.loglog(one["period_s"], one["kr"], "o-", color="0.2")
18ax.axhline(3.0, color="0.4", linestyle="--")
19ax.axhline(0.3, color="0.4", linestyle="--")
20ax.set_xlabel("Period (s)")
21ax.set_ylabel("|k r|")
22ax.set_title(f"{station} field-zone parameter")
23fig.tight_layout()
The shaded regions show the far, transition, and near zones. Long
periods often move toward smaller kr because Bostick depth increases
as frequency decreases.
Near-Field Factor#
near_field_factor computes a continuous correction factor for the
equatorial horizontal electric dipole approximation:
where p = k r is complex. The returned nf_factor is abs(F).
When it is close to 1, the response is far-field-like. Large
departures indicate near-field bias. Apparent resistivity bias scales
approximately with abs(F)^2.
1from pycsamt.emtools.fieldzone import classify_field_zones, near_field_factor
2
3survey = "data/AMT/WILLY_DATA/L18PLT"
4offset_m = 2000.0
5
6zones = classify_field_zones(survey, offset_m)
7factors = near_field_factor(survey, offset_m)
8
9merged = zones.merge(
10 factors[["station", "freq_hz", "nf_factor"]],
11 on=["station", "freq_hz"],
12 how="inner",
13)
14
15print(
16 merged.groupby("zone")["nf_factor"]
17 .agg(["count", "mean", "median", "max"])
18)
count mean median max
zone
far 785 0.994097 0.998870 0.999999
near 220 960.277733 350.731935 16818.234683
transition 479 13.091904 2.114809 86.107095
This cross-check is useful because zone is threshold-based, while
nf_factor is continuous. In a good classification, far-zone samples
should cluster near nf_factor = 1 and near-zone samples should show
larger departures.
Plot Field Zones#
plot_field_zones maps zone labels onto station x period space and
can overlay |k r| contours.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools.fieldzone import plot_field_zones
4
5fig, ax = plt.subplots(figsize=(10, 5))
6plot_field_zones(
7 "data/AMT/WILLY_DATA/L18PLT",
8 source_offset=2000.0,
9 far_threshold=3.0,
10 near_threshold=0.3,
11 contour_kr=True,
12 kr_levels=(0.1, 0.3, 1.0, 3.0, 10.0),
13 sort_by="name",
14 period_axis=True,
15 ax=ax,
16)
17fig.tight_layout()
Use this as the main survey view. A broad red or orange band at long periods means those periods should be excluded, corrected, or treated with caution before plane-wave inversion.
Station-Specific Offsets#
Real CSAMT geometry often varies by station. Pass a dictionary when each station has its own source-receiver distance.
1from pycsamt.emtools.fieldzone import classify_field_zones
2
3offsets_m = {
4 "18-001A": 1800.0,
5 "18-002U": 1950.0,
6 "18-003A": 2100.0,
7}
8
9zones = classify_field_zones(
10 "data/AMT/WILLY_DATA/L18PLT",
11 source_offset=offsets_m,
12 verbose=1,
13)
14
15print(zones["station"].unique())
['18-001A' '18-002U' '18-003A']
Only stations with offsets are classified. With verbose=1, missing
offsets produce warnings from the classifier. In production workflows,
build this dictionary from the transmitter and receiver coordinates.
Offset Sensitivity#
If the source offset is uncertain, run a sensitivity sweep. The same observed impedances can move between far and near zones solely because the assumed offset changes.
1import pandas as pd
2
3from pycsamt.emtools.fieldzone import classify_field_zones
4
5survey = "data/AMT/WILLY_DATA/L18PLT"
6offsets = [500.0, 2000.0, 8000.0]
7
8rows = []
9for offset in offsets:
10 zones = classify_field_zones(survey, source_offset=offset)
11 fractions = zones["zone"].value_counts(normalize=True)
12 rows.append(
13 {
14 "offset_m": offset,
15 "far": fractions.get("far", 0.0),
16 "transition": fractions.get("transition", 0.0),
17 "near": fractions.get("near", 0.0),
18 }
19 )
20
21sensitivity = pd.DataFrame(rows)
22print(sensitivity)
offset_m far transition near
0 500.0 0.258760 0.386792 0.354447
1 2000.0 0.528976 0.322776 0.148248
2 8000.0 0.714286 0.283019 0.002695
Report this table when offset is assumed rather than measured. It makes the geometry dependence visible instead of hiding it inside one plot.
Comparing Offsets Side By Side#
The plotting function accepts ax so several assumed offsets can be
compared in one figure.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools.fieldzone import plot_field_zones
4
5survey = "data/AMT/WILLY_DATA/L18PLT"
6
7fig, (ax_near, ax_far) = plt.subplots(1, 2, figsize=(13, 5), sharey=True)
8plot_field_zones(survey, source_offset=500.0, ax=ax_near)
9ax_near.set_title("Offset = 500 m")
10
11plot_field_zones(survey, source_offset=8000.0, ax=ax_far)
12ax_far.set_title("Offset = 8000 m")
13
14fig.tight_layout()
This comparison is especially useful for design studies: if the target period band is near or transition at the planned offset, move the source, change the frequency band, or plan controlled-source corrections.
Illustrative Near-Field Correction#
The near-field factor can show how strongly a sounding might be biased.
The example below divides apparent resistivity by nf_factor ** 2 as
an illustrative correction. Use the appropriate controlled-source
convention and survey geometry before applying this in production.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools.fieldzone import classify_field_zones, near_field_factor
4
5survey = "data/AMT/WILLY_DATA/L18PLT"
6station = "18-001A"
7offset_m = 2000.0
8
9zones = classify_field_zones(survey, offset_m)
10factors = near_field_factor(survey, offset_m)
11merged = zones.merge(
12 factors[["station", "freq_hz", "nf_factor"]],
13 on=["station", "freq_hz"],
14 how="inner",
15)
16one = merged.loc[merged["station"] == station].sort_values("period_s")
17corrected = one["rho_a_ohmm"] / (one["nf_factor"] ** 2)
18
19fig, ax = plt.subplots(figsize=(7, 4.5))
20ax.loglog(one["period_s"], one["rho_a_ohmm"], "o-", label="measured")
21ax.loglog(one["period_s"], corrected, "s--", label="illustrative / |F|^2")
22ax.set_xlabel("Period (s)")
23ax.set_ylabel("Apparent resistivity (ohm.m)")
24ax.legend()
25fig.tight_layout()
Where nf_factor is near 1, the curves overlap. Where
nf_factor is large, the plane-wave apparent resistivity is strongly
affected by near-field behavior.
Reading The Results#
Use this interpretation order:
Confirm the source offset is real and in metres.
Inspect
krandzonebefore using long-period CSAMT data in a plane-wave inversion.Treat
transitionsamples as conditional, not automatically safe.Use
near_field_factorto quantify how severe the departure from far-field behavior may be.Run offset sensitivity when the source geometry is assumed or uncertain.
Prefer station-specific offsets when transmitter-receiver geometry is available.
Common Failure Modes#
- Empty output
No valid impedance tensors were loaded, or no source offset could be resolved for any station.
- Natural-source data without offsets
AMT/MT data do not carry a transmitter offset. You can run sensitivity examples with assumed offsets, but do not present those numbers as measured survey geometry.
- Wrong offset units
Offsets must be metres. Passing kilometres without conversion will severely misclassify the field zone.
- One offset for a whole survey
This is acceptable for illustration or a constant-offset acquisition, but station-specific geometry is safer for real CSAMT.
- Far-zone label at noisy frequencies
Field-zone classification only checks source geometry and apparent resistivity. It does not replace quality control.
Saving A Reproducible Bundle#
For reports, save the classification table, near-field factor table, offset-sensitivity summary, and field-zone pseudo-section.
1from pathlib import Path
2
3import matplotlib.pyplot as plt
4import pandas as pd
5
6from pycsamt.emtools.fieldzone import (
7 classify_field_zones,
8 near_field_factor,
9 plot_field_zones,
10)
11
12survey = "data/AMT/WILLY_DATA/L18PLT"
13offset_m = 2000.0
14out = Path("outputs/fieldzone_l18plt")
15out.mkdir(parents=True, exist_ok=True)
16
17zones = classify_field_zones(survey, offset_m)
18factors = near_field_factor(survey, offset_m)
19zones.to_csv(out / "field_zones.csv", index=False)
20factors.to_csv(out / "near_field_factor.csv", index=False)
21
22rows = []
23for offset in [500.0, 2000.0, 8000.0]:
24 z = classify_field_zones(survey, offset)
25 fractions = z["zone"].value_counts(normalize=True)
26 rows.append(
27 {
28 "offset_m": offset,
29 "far": fractions.get("far", 0.0),
30 "transition": fractions.get("transition", 0.0),
31 "near": fractions.get("near", 0.0),
32 }
33 )
34pd.DataFrame(rows).to_csv(out / "offset_sensitivity.csv", index=False)
35
36fig, ax = plt.subplots(figsize=(10, 5))
37plot_field_zones(survey, offset_m, ax=ax)
38fig.tight_layout()
39fig.savefig(out / "field_zone_pseudosection.png", dpi=200)
Worked Example#
The gallery example uses L18PLT from data/AMT/WILLY_DATA/ and
representative assumed offsets because the bundled line is natural-source
AMT and does not record transmitter geometry. It demonstrates the
|k r| relationship, one-station curves, near-field factor
cross-checks, pseudo-sections, offset sensitivity, and an illustrative
near-field correction.
Open the rendered example here: CSAMT field-zone classification (pycsamt.emtools.fieldzone).