First-Look Survey Inspection#

pycsamt.emtools.inspect is the first module to use after loading a survey. It answers practical questions before you run deeper diagnostics, static-shift correction, dimensionality analysis, or inversion:

  • which stations loaded correctly?

  • which stations have impedance and tipper data?

  • do all stations share the same frequency grid?

  • are the apparent-resistivity and phase curves sensible?

  • where are the first obvious pseudo-section anomalies?

  • which single station deserves a full response plot?

Full callable signatures live in the API reference. This page explains the workflow and gives concrete code patterns.

Why Inspect First#

Inspection is not interpretation yet. It is the quality gate between “the files loaded” and “the data are ready for scientific decisions”. The inspection tools are intentionally plain: tables, coverage masks, simple curves, pseudo-sections, tipper components, and one complete station dashboard.

Run this stage before:

  • deciding which frequency band to keep;

  • comparing survey lines;

  • trusting tipper-based induction arrows;

  • building phase-tensor or dimensionality products;

  • fitting a model response to observed data.

Load The Survey#

The inspection functions all call ensure_sites internally, but it is still useful to normalize once at the top of a script.

 1from pathlib import Path
 2
 3from pycsamt.emtools import ensure_sites
 4
 5edi_dir = Path("data/AMT/WILLY_DATA/L18PLT")
 6survey = ensure_sites(
 7    edi_dir,
 8    recursive=True,
 9    on_dup="replace",
10    strict=True,
11    verbose=1,
12)

Use strict=True when a missing or empty dataset should stop the workflow. For exploratory notebooks, strict=False can be more convenient because the plotting functions will draw “no data” messages instead of failing immediately.

The Inspection Workflow#

The module is easiest to use as a sequence.

Step

Question

Tool

inventory

What stations, period ranges, coordinates, and tippers exist?

sites_summary

required sections

Which stations are missing mt or tipper?

list_missing_sections

frequency grid

Do stations share the same frequency samples?

frequency_coverage and plot_coverage

quick curves

Are rho/phase curves plausible?

plot_rhoa_phi

survey image

Where are station-period anomalies?

pseudosection

tipper check

Is real tipper present and stable?

plot_tipper_components

station dashboard

What does one station look like in full?

plot_station_response

Per-Site Summary#

sites_summary returns one row per station. By default it reports station name, number of frequency samples, whether tipper data are present, period range, and coordinates.

 1import pandas as pd
 2
 3from pycsamt.emtools import ensure_sites, sites_summary
 4
 5survey = ensure_sites("data/AMT/WILLY_DATA/L18PLT", strict=True)
 6
 7summary = sites_summary(survey, api=False)
 8print(summary.head())
 9
10overview = {
11    "n_sites": len(summary),
12    "has_any_tipper": bool(summary["has_tipper"].any()),
13    "n_freq_values": sorted(summary["n_freq"].unique()),
14    "period_min": float(summary["period_min"].min()),
15    "period_max": float(summary["period_max"].max()),
16}
17print(pd.Series(overview))
   station  n_freq  has_tipper  period_min  period_max        lat         lon
0  18-001A      53       False    0.000096    0.992063  32.120300  119.128833
1  18-002U      53       False    0.000096    0.992063  32.121133  119.128900
2  18-003A      53       False    0.000096    0.992063  32.122083  119.128850
3  18-004A      53       False    0.000096    0.992063  32.123333  119.128833
4  18-005U      53       False    0.000096    0.992063  32.123900  119.128833
n_sites                 28
has_any_tipper       False
n_freq_values         [53]
period_min        0.000096
period_max        0.992063
dtype: object

Read this table before plotting anything. A survey with mixed n_freq values needs frequency-grid attention. A survey with has_tipper=False everywhere should not be sent into tipper or induction-arrow interpretation.

Choose Summary Columns#

The fields argument lets you keep the inventory narrow when you are printing reports or comparing several lines.

 1from pycsamt.emtools import sites_summary
 2
 3compact = sites_summary(
 4    "data/AMT/WILLY_DATA/L18PLT",
 5    fields=(
 6        "station",
 7        "n_freq",
 8        "period_min",
 9        "period_max",
10        "has_tipper",
11    ),
12    api=False,
13)
14
15print(compact.to_string(index=False))
station  n_freq  period_min  period_max  has_tipper
18-001A      53    0.000096    0.992063       False
18-002U      53    0.000096    0.992063       False
18-003A      53    0.000096    0.992063       False
18-004A      53    0.000096    0.992063       False
18-005U      53    0.000096    0.992063       False
18-006A      53    0.000096    0.992063       False
18-007U      53    0.000096    0.992063       False
18-008U      53    0.000096    0.992063       False
18-009A      53    0.000096    0.992063       False
18-010U      53    0.000096    0.992063       False
18-011A      53    0.000096    0.992063       False
18-012A      53    0.000096    0.992063       False
18-013U      53    0.000096    0.992063       False
18-014A      53    0.000096    0.992063       False
18-015U      53    0.000096    0.992063       False
18-016A      53    0.000096    0.992063       False
18-017U      53    0.000096    0.992063       False
18-018A      53    0.000096    0.992063       False
18-019U      53    0.000096    0.992063       False
18-020A      53    0.000096    0.992063       False
18-021B      53    0.000096    0.992063       False
18-021U      53    0.000096    0.992063       False
18-022U      53    0.000096    0.992063       False
18-022V      53    0.000096    0.992063       False
18-023A      53    0.000096    0.992063       False
18-023V      53    0.000096    0.992063       False
18-024U      53    0.000096    0.992063       False
18-025A      53    0.000096    0.992063       False

The returned object may be an API-aware frame when the package API-view mode is enabled. Passing api=False gives a plain pandas DataFrame for ordinary scripts.

Missing Sections#

list_missing_sections checks whether each station has required data sections. The most common checks are "mt" for impedance and "tipper" for transfer-function data.

 1from pycsamt.emtools import ensure_sites, list_missing_sections
 2
 3survey = ensure_sites("data/AMT/WILLY_DATA/L18PLT", strict=True)
 4
 5missing = list_missing_sections(
 6    survey,
 7    require=("mt", "tipper"),
 8)
 9
10for station, sections in missing.items():
11    print(f"{station}: missing {', '.join(sections)}")
18-001A: missing tipper
18-002U: missing tipper
18-003A: missing tipper
18-004A: missing tipper
18-005U: missing tipper
18-006A: missing tipper
18-007U: missing tipper
18-008U: missing tipper
18-009A: missing tipper
18-010U: missing tipper
18-011A: missing tipper
18-012A: missing tipper
18-013U: missing tipper
18-014A: missing tipper
18-015U: missing tipper
18-016A: missing tipper
18-017U: missing tipper
18-018A: missing tipper
18-019U: missing tipper
18-020A: missing tipper
18-021B: missing tipper
18-021U: missing tipper
18-022U: missing tipper
18-022V: missing tipper
18-023A: missing tipper
18-023V: missing tipper
18-024U: missing tipper
18-025A: missing tipper

This function uses the same internal extraction helpers as the plotting tools. That matters because real EDI/Site objects can expose placeholder attributes even when a section was not actually parsed.

Check Tipper Availability Explicitly#

AMT/CSAMT lines often have no tipper. MT surveys often do. Make that difference explicit before writing code that assumes tipper exists.

 1from pycsamt.emtools import list_missing_sections
 2
 3amt_missing = list_missing_sections(
 4    "data/AMT/WILLY_DATA/L18PLT",
 5    require=("tipper",),
 6)
 7mt_missing = list_missing_sections(
 8    "data/MT/kap03lmt_edis",
 9    require=("tipper",),
10)
11
12print(f"L18PLT stations missing tipper: {len(amt_missing)}")
13print(f"KAP03 stations missing tipper: {len(mt_missing)}")
L18PLT stations missing tipper: 28
KAP03 stations missing tipper: 0

If every station is missing tipper, that is not necessarily a failure. It simply means you should stay with impedance-based inspection and use the transfer-function tools only on surveys that contain vertical-field data.

Frequency Coverage Tables#

frequency_coverage has three modes.

 1import numpy as np
 2
 3from pycsamt.emtools import ensure_sites, frequency_coverage
 4
 5survey = ensure_sites("data/MT/kap03lmt_edis", strict=True)
 6
 7per_site = frequency_coverage(survey, mode="per-site")
 8union = frequency_coverage(survey, mode="union")
 9intersection = frequency_coverage(survey, mode="intersection")
10
11print(f"stations: {len(per_site)}")
12print(f"union frequency count: {union.size}")
13print(f"common frequency count: {intersection.size}")
14
15for station, freq in per_site.items():
16    missing_from_union = np.setdiff1d(union, freq)
17    if missing_from_union.size:
18        print(station, "missing", missing_from_union.size, "samples")
stations: 26
union frequency count: 37
common frequency count: 0
kap103 missing 17 samples
kap106 missing 17 samples
kap109 missing 20 samples
kap112 missing 17 samples
kap115 missing 17 samples
kap118 missing 17 samples
kap121 missing 17 samples
kap123 missing 17 samples
kap125 missing 17 samples
kap127 missing 17 samples
kap130 missing 17 samples
kap133 missing 17 samples
kap136 missing 17 samples
kap139 missing 17 samples
kap142 missing 17 samples
kap145 missing 19 samples
kap148 missing 17 samples
kap151 missing 17 samples
kap152 missing 17 samples
kap155 missing 17 samples
kap157 missing 17 samples
kap160 missing 17 samples
kap163 missing 17 samples
kap169 missing 17 samples
kap172 missing 17 samples
kap175 missing 17 samples

Use mode="per-site" when you need station names. Use "union" to know the full survey frequency grid. Use "intersection" to know which frequencies are shared by every station.

Plot Frequency Coverage#

plot_coverage converts the frequency dictionary into a station by frequency presence mask.

 1import matplotlib.pyplot as plt
 2
 3from pycsamt.emtools import ensure_sites, plot_coverage
 4
 5survey = ensure_sites("data/MT/kap03lmt_edis", strict=True)
 6
 7fig, ax = plt.subplots(figsize=(8.0, 4.5))
 8plot_coverage(
 9    survey,
10    axis="period",
11    ax=ax,
12)
13ax.set_title("KAP03 frequency coverage")
14
15fig.tight_layout()
16fig.savefig("kap03_frequency_coverage.png", dpi=200)
17plt.close(fig)
../../_images/user-guide-emtools-inspect-07.png

The colour value is presence, not data quality. A fully covered cell only means the sample exists. Use QC, error, confidence, and frequency editing tools to decide whether the sample is reliable.

Quick Rho And Phase Curves#

plot_rhoa_phi plots apparent resistivity and phase for one or more stations. It accepts components such as "xy", "yx", "xx", and "yy" when those columns exist in the station dataframe.

 1import matplotlib.pyplot as plt
 2
 3from pycsamt.emtools import ensure_sites, plot_rhoa_phi
 4from pycsamt.emtools._core import _iter_items
 5
 6survey = ensure_sites("data/AMT/WILLY_DATA/L18PLT", strict=True)
 7
 8subset_paths = [site.edi.path for site in list(_iter_items(survey))[:4]]
 9subset = ensure_sites(subset_paths, strict=True)
10
11ax_rho, ax_phase = plot_rhoa_phi(
12    subset,
13    components=("xy", "yx"),
14    axis="period",
15    errorbar=True,
16    figsize=(8.0, 6.0),
17)
18
19ax_rho.figure.savefig("l18plt_rho_phase_subset.png", dpi=200)
20plt.close(ax_rho.figure)
../../_images/user-guide-emtools-inspect-08.png

Do not plot every station at once unless the survey is tiny. The function will draw the data, but the legend can become unreadable. Use small station subsets for first inspection, then switch to plot_station_response for a station-level deep view.

Pseudo-Sections#

pseudosection creates a period by station image from a dataframe quantity such as "rho_xy", "rho_yx", "phi_xy", or "phi_yx". Values are pivoted by station and period, with median aggregation for duplicate cells.

 1import matplotlib.pyplot as plt
 2
 3from pycsamt.emtools import ensure_sites, pseudosection
 4
 5survey = ensure_sites("data/AMT/WILLY_DATA/L18PLT", strict=True)
 6
 7fig, ax = plt.subplots(figsize=(10.0, 4.8))
 8pseudosection(
 9    survey,
10    quantity="rho_xy",
11    period_range=(1e-4, 1.0),
12    ax=ax,
13    topo=False,
14)
15ax.set_title("L18PLT rho_xy pseudo-section")
16
17fig.tight_layout()
18fig.savefig("l18plt_rho_xy_pseudosection.png", dpi=200)
19plt.close(fig)
../../_images/user-guide-emtools-inspect-09.png

The x-axis is station order. The y-axis is period. Short periods are drawn at the top because the image uses the common MT pseudo-section convention: shallow-sensitive samples above deeper-sensitive samples.

Control The Pseudo-Section Scale#

Use fixed vmin and vmax when comparing two lines. Otherwise a line with a narrow value range can look as dramatic as a line with a much stronger anomaly.

 1import matplotlib.pyplot as plt
 2
 3from pycsamt.emtools import ensure_sites, pseudosection
 4
 5line18 = ensure_sites("data/AMT/WILLY_DATA/L18PLT", strict=True)
 6line22 = ensure_sites("data/AMT/WILLY_DATA/L22PLT", strict=True)
 7
 8fig, axes = plt.subplots(1, 2, figsize=(13.0, 5.0), sharey=True)
 9
10pseudosection(line18, quantity="rho_xy", vmin=10.0, vmax=5000.0, ax=axes[0])
11axes[0].set_title("L18PLT")
12
13pseudosection(line22, quantity="rho_xy", vmin=10.0, vmax=5000.0, ax=axes[1])
14axes[1].set_title("L22PLT")
15
16fig.tight_layout()
17fig.savefig("rho_xy_line_comparison.png", dpi=200)
18plt.close(fig)
../../_images/user-guide-emtools-inspect-10.png

If topography is configured globally, pseudosection can draw an optional topography strip. Pass topo=False when you want a compact data-only panel.

Tipper Components#

plot_tipper_components draws real and imaginary parts of Tx and Ty versus period or frequency. Use it only after confirming that the survey actually contains tipper data.

 1import matplotlib.pyplot as plt
 2
 3from pycsamt.emtools import ensure_sites, plot_tipper_components
 4from pycsamt.emtools._core import _iter_items, _name
 5
 6survey = ensure_sites("data/MT/kap03lmt_edis", strict=True)
 7
 8station_names = ["kap103", "kap121", "kap142", "kap151"]
 9subset_paths = [
10    site.edi.path
11    for index, site in enumerate(_iter_items(survey))
12    if _name(site, index) in station_names
13]
14subset = ensure_sites(subset_paths, strict=True)
15
16fig, ax = plt.subplots(figsize=(8.5, 4.8))
17plot_tipper_components(
18    subset,
19    kind=("real", "imag"),
20    axis="period",
21    ax=ax,
22)
23ax.set_title("KAP03 selected tipper components")
24
25fig.tight_layout()
26fig.savefig("kap03_tipper_components.png", dpi=200)
27plt.close(fig)
../../_images/user-guide-emtools-inspect-11.png

The horizontal zero line is important. Sign changes, isolated spikes, or one station separating strongly from the others are good reasons to inspect induction arrows, tipper hodograms, and station metadata.

Full Station Response#

plot_station_response is the richest first-look figure. It shows apparent resistivity, phase, and, when available, the four tipper sub-panels for one station.

 1import matplotlib.pyplot as plt
 2
 3from pycsamt.emtools import ensure_sites, plot_station_response
 4
 5survey = ensure_sites("data/MT/kap03lmt_edis", strict=True)
 6
 7fig = plot_station_response(
 8    survey,
 9    station="kap151",
10    components=("xx", "xy", "yx", "yy"),
11    period_range=(1e-2, 2e4),
12    show_tipper=True,
13    show_error_bars=True,
14    rho_lim=None,
15    phase_lim=None,
16    tipper_lim=(-2.5, 2.5),
17    title="kap151 first-look response",
18)
19
20fig.savefig("kap151_station_response.png", dpi=200)
21plt.close(fig)
../../_images/user-guide-emtools-inspect-12.png

The first row is apparent resistivity on log-log axes. The second row is phase on a log-period x-axis. The optional third row shows Re(Tx), Im(Tx), Re(Ty), and Im(Ty). If no tipper exists or show_tipper=False, the figure uses only the impedance rows.

Overlay A Model Response#

When sites_model is supplied, the station response overlays a second dataset as dashed curves. If observed and model resistivity are both available, the function appends an RMS value to component titles. The RMS is computed in log10(rho) space.

 1import matplotlib.pyplot as plt
 2
 3from pycsamt.emtools import ensure_sites, plot_station_response, smooth_mavg
 4
 5observed = ensure_sites("data/MT/kap03lmt_edis", strict=True)
 6
 7# For demonstration only: a smoothed copy behaves like a model response.
 8# In production, pass forward-model or inversion-response EDI data here.
 9model_like = smooth_mavg(observed, k=5)
10
11fig = plot_station_response(
12    observed,
13    station="kap151",
14    sites_model=model_like,
15    components=("xy", "yx"),
16    period_range=(1e-2, 2e4),
17    show_rms=True,
18    show_tipper=False,
19    figsize=(8.5, 5.2),
20    title="kap151 observed vs model-like response",
21)
22
23fig.savefig("kap151_response_with_model_overlay.png", dpi=200, bbox_inches="tight")
24plt.close(fig)
../../_images/user-guide-emtools-inspect-13.png

Use this view after inversion or forward modelling to check whether the model misses a whole component, a period band, or only local points. A single RMS number is useful, but the curve shape tells you why the RMS is high or low.

Build A First-Look Report Bundle#

The following script writes a compact first-look bundle for a survey: summary table, missing-section table, frequency coverage, rho/phase curves for a few stations, one pseudo-section, and one station response.

 1from pathlib import Path
 2
 3import matplotlib.pyplot as plt
 4import pandas as pd
 5
 6from pycsamt.emtools import (
 7    ensure_sites,
 8    list_missing_sections,
 9    plot_coverage,
10    plot_rhoa_phi,
11    plot_station_response,
12    pseudosection,
13    sites_summary,
14)
15from pycsamt.emtools._core import _iter_items, _name
16
17out = Path("inspect_report_l18plt")
18out.mkdir(parents=True, exist_ok=True)
19
20survey = ensure_sites("data/AMT/WILLY_DATA/L18PLT", strict=True)
21
22summary = sites_summary(survey, api=False)
23summary.to_csv(out / "sites_summary.csv", index=False)
24
25missing = list_missing_sections(survey, require=("mt", "tipper"))
26missing_rows = [
27    {"station": station, "missing": ",".join(sections)}
28    for station, sections in missing.items()
29]
30pd.DataFrame(missing_rows).to_csv(out / "missing_sections.csv", index=False)
31
32fig, ax = plt.subplots(figsize=(8.0, 4.5))
33plot_coverage(survey, ax=ax)
34fig.tight_layout()
35fig.savefig(out / "frequency_coverage.png", dpi=200)
36plt.close(fig)
37
38subset_names = list(summary["station"].head(4))
39subset_paths = [
40    site.edi.path
41    for index, site in enumerate(_iter_items(survey))
42    if _name(site, index) in subset_names
43]
44subset = ensure_sites(subset_paths, strict=True)
45
46ax_rho, ax_phase = plot_rhoa_phi(subset, components=("xy", "yx"))
47ax_rho.figure.savefig(out / "rho_phase_subset.png", dpi=200)
48plt.close(ax_rho.figure)
49
50fig, ax = plt.subplots(figsize=(10.0, 4.8))
51pseudosection(survey, quantity="rho_xy", ax=ax, topo=False)
52fig.tight_layout()
53fig.savefig(out / "rho_xy_pseudosection.png", dpi=200)
54plt.close(fig)
55
56first_station = str(summary["station"].iloc[0])
57fig = plot_station_response(
58    survey,
59    station=first_station,
60    components=("xy", "yx"),
61    show_tipper=False,
62)
63fig.savefig(out / f"station_response_{first_station}.png", dpi=200, bbox_inches="tight")
64plt.close(fig)

Reading The Inspection Results#

Treat these outputs as a triage board:

n_freq differs between stations

Align or edit the frequency grid before making survey-wide pseudo-sections or station statistics that assume common samples.

All stations are missing tipper

Skip tipper diagnostics for that survey. This can be normal for AMT/CSAMT lines.

Only some stations are missing tipper

Keep those stations out of tipper maps or split the analysis into tipper-capable and impedance-only subsets.

Rho/phase curves are wildly separated

Check station metadata, static shift, data quality, and whether a few stations dominate the line.

Pseudo-section anomalies appear only at one frequency

Inspect frequency confidence and errors before interpreting that feature geologically.

Station response shows diagonal terms comparable to off-diagonals

Follow up with impedance, tensor, dimensionality, and strike tools.

Worked Example#

The gallery example uses the bundled AMT and MT datasets to show the same first-look workflow end to end.

Open the rendered gallery page here: First-look survey inspection (pycsamt.emtools.inspect).