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? |
|
required sections |
Which stations are missing |
|
frequency grid |
Do stations share the same frequency samples? |
|
quick curves |
Are rho/phase curves plausible? |
|
survey image |
Where are station-period anomalies? |
|
tipper check |
Is real tipper present and stable? |
|
station dashboard |
What does one station look like in full? |
|
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)
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)
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)
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)
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)
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)
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)
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_freqdiffers between stationsAlign 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).