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:

\[|k r| = {r \over \delta_B}\]

where r is the source-receiver offset in metres and delta_B is the Bostick depth approximation:

\[\delta_B = 356 \sqrt{\rho_a / f}\]

The measured apparent resistivity is computed from the two off-diagonal impedance modes using the practical EDI convention used in pyCSAMT:

\[\rho_a = \sqrt{ \left(0.2 {|Z_{xy}|^2 \over f}\right) \left(0.2 {|Z_{yx}|^2 \over f}\right) }\]

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

far

Plane-wave approximation is usually acceptable.

transition

Source geometry may influence the response; inspect before inversion.

near

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

source_offset=2000.0

Use one offset for every station.

source_offset={"18-001A": 1800.0, ...}

Use station-specific offsets.

source_offset=None

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_hz and period_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, or near.

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()
../../_images/user-guide-emtools-fieldzone-02.png

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:

\[F(p) = 1 - {3 \over p^2} + {3 \over p^3}\]

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()
../../_images/user-guide-emtools-fieldzone-04.png

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()
../../_images/user-guide-emtools-fieldzone-07.png

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()
../../_images/user-guide-emtools-fieldzone-08.png

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 kr and zone before using long-period CSAMT data in a plane-wave inversion.

  • Treat transition samples as conditional, not automatically safe.

  • Use near_field_factor to 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)
../../_images/user-guide-emtools-fieldzone-09.png

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).