Dimensionality Assessment#
pycsamt.emtools.dimensionality asks a practical question before
interpretation or inversion: does each station-frequency sample behave
like 1-D, 2-D, or 3-D electromagnetic structure?
The module provides two complementary routes:
a rule-based classifier using phase-tensor skew and ellipticity;
a sparse dictionary workflow that learns patterns from several impedance and phase-tensor features.
It also provides helper workflows for 2-D inversion preparation: masking 3-D samples, rotating data to strike, antisymmetrizing the off-diagonal impedance tensor, and writing a pre-2D assessment table.
Full callable signatures live in the API reference. This page focuses on workflow, outputs, interpretation, and concrete line-numbered examples.
Why Dimensionality Comes First#
A 1-D inversion assumes horizontally layered structure. A 2-D inversion assumes a dominant strike direction and negligible variation along that strike. Real CSAMT/AMT data can violate both assumptions because of local conductors, galvanic distortion, 3-D bodies, station effects, or complex geology.
Dimensionality diagnostics do not replace inversion. They tell you how much caution to use before choosing an inversion strategy, period band, strike rotation, or masking rule.
Core Features#
phase_features_table builds one row per station and frequency. The
features combine phase-tensor quantities with determinant-style
impedance quantities:
Column |
Meaning |
|---|---|
|
Station name. |
|
Frequency in hertz. |
|
Period in seconds. |
|
Absolute phase-tensor skew angle, in degrees. |
|
Absolute phase-tensor ellipticity. |
|
|
|
Determinant impedance phase, in degrees. |
|
Tipper amplitude when tipper data are available; otherwise
|
The determinant-style apparent resistivity uses the same practical EDI
unit convention used elsewhere in emtools:
Rule-Based Labels#
The default rule uses two thresholds:
1skew_th = 3.0
2ellipt_th = 0.2
The class labels are:
1dim = 0 -> 1-D
2dim = 1 -> 2-D
3dim = 2 -> 3-D
The rule is intentionally simple:
1if beta_abs <= skew_th and ellipt_abs <= ellipt_th:
2 dim = 0
3elif beta_abs <= skew_th and ellipt_abs > ellipt_th:
4 dim = 1
5else:
6 dim = 2
In other words, high phase-tensor skew pushes a sample into the 3-D class. Low skew with low ellipticity is treated as 1-D. Low skew with higher ellipticity is treated as 2-D.
Build The Feature Table#
Start with the raw feature table before interpreting any labels.
1from pathlib import Path
2
3from pycsamt.emtools.dimensionality import phase_features_table
4
5edi_dir = Path("data/AMT/WILLY_DATA/L18PLT")
6
7features = phase_features_table(
8 edi_dir,
9 recursive=True,
10 on_dup="replace",
11 strict=False,
12 verbose=0,
13)
14
15cols = [
16 "station",
17 "freq",
18 "period",
19 "beta_abs",
20 "ellipt_abs",
21 "logrho_det",
22 "phi_det",
23 "tip_amp",
24]
25print(features[cols].head())
station freq period ... logrho_det phi_det tip_amp
0 18-001A 10400.0 0.000096 ... 1.886481 63.023441 NaN
1 18-001A 8707.0 0.000115 ... 1.925638 61.655406 NaN
2 18-001A 7289.0 0.000137 ... 1.948138 59.889966 NaN
3 18-001A 6102.0 0.000164 ... 2.061820 58.836496 NaN
4 18-001A 5108.0 0.000196 ... 2.182395 51.323119 NaN
[5 rows x 8 columns]
Line 7 loads the EDI directory through the shared ensure_sites
machinery. Lines 15-24 show the columns most often used in downstream
checks.
Inspect One Station#
A single-station view makes the thresholds tangible.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools.dimensionality import phase_features_table
4
5skew_th = 3.0
6ellipt_th = 0.2
7
8features = phase_features_table("data/AMT/WILLY_DATA/L18PLT")
9station = "18-001A"
10one = features.loc[features["station"] == station].sort_values("period")
11
12fig, (ax_beta, ax_ellipt) = plt.subplots(
13 2,
14 1,
15 figsize=(7, 6),
16 sharex=True,
17)
18
19ax_beta.semilogx(one["period"], one["beta_abs"], "o-")
20ax_beta.axhline(skew_th, color="0.4", linestyle="--")
21ax_beta.set_ylabel("|beta| (deg)")
22
23ax_ellipt.semilogx(one["period"], one["ellipt_abs"], "o-", color="C3")
24ax_ellipt.axhline(ellipt_th, color="0.4", linestyle="--")
25ax_ellipt.set_xlabel("Period (s)")
26ax_ellipt.set_ylabel("Ellipticity")
27
28fig.suptitle(f"{station} dimensionality features")
29fig.tight_layout()
If beta_abs stays above the skew threshold over most periods, the
station will be classified mostly 3-D regardless of ellipticity. If
beta_abs is low, ellipticity separates 1-D from 2-D behavior.
Classify The Survey#
Use classify_dimensionality to add the dim label to the feature
table.
1import pandas as pd
2
3from pycsamt.emtools.dimensionality import classify_dimensionality
4
5dim = classify_dimensionality(
6 "data/AMT/WILLY_DATA/L18PLT",
7 skew_th=3.0,
8 ellipt_th=0.2,
9)
10
11counts = (
12 dim.groupby("station")["dim"]
13 .value_counts(normalize=True)
14 .rename("fraction")
15 .reset_index()
16)
17
18label = {0: "1D", 1: "2D", 2: "3D"}
19counts["label"] = counts["dim"].map(label)
20print(counts.head(12))
station dim fraction label
0 18-001A 2 0.981132 3D
1 18-001A 1 0.018868 2D
2 18-002U 2 0.981132 3D
3 18-002U 1 0.018868 2D
4 18-003A 2 0.981132 3D
5 18-003A 1 0.018868 2D
6 18-004A 2 0.981132 3D
7 18-004A 1 0.018868 2D
8 18-005U 2 0.943396 3D
9 18-005U 1 0.056604 2D
10 18-006A 2 0.962264 3D
11 18-006A 1 0.037736 2D
Line 5 uses the default rule. Lines 11-15 compute station-level fractions so you can see whether a station is mostly 1-D/2-D or mostly 3-D.
Read The Rule In Feature Space#
The clearest way to understand the rule is to plot beta_abs against
ellipt_abs.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools.dimensionality import classify_dimensionality
4
5skew_th = 3.0
6ellipt_th = 0.2
7dim = classify_dimensionality(
8 "data/AMT/WILLY_DATA/L18PLT",
9 skew_th=skew_th,
10 ellipt_th=ellipt_th,
11)
12
13colors = {0: "tab:green", 1: "tab:blue", 2: "tab:red"}
14labels = {0: "1-D", 1: "2-D", 2: "3-D"}
15
16fig, ax = plt.subplots(figsize=(6.5, 5.5))
17for cls in (2, 1, 0):
18 sub = dim.loc[dim["dim"] == cls]
19 ax.scatter(
20 sub["beta_abs"],
21 sub["ellipt_abs"],
22 s=8,
23 alpha=0.45,
24 color=colors[cls],
25 label=labels[cls],
26 )
27
28ax.axvline(skew_th, color="0.2", linestyle="--")
29ax.axhline(ellipt_th, color="0.2", linestyle="--")
30ax.set_xlabel("|beta| (deg)")
31ax.set_ylabel("Ellipticity")
32ax.legend()
33fig.tight_layout()
The vertical line is the 3-D boundary. The horizontal line separates 1-D and 2-D only on the low-skew side of the plot.
Threshold Sensitivity#
Do not tune thresholds blindly. Sweep them and report how the dimensionality fractions change.
1import numpy as np
2import pandas as pd
3
4from pycsamt.emtools.dimensionality import phase_features_table
5
6features = phase_features_table("data/AMT/WILLY_DATA/L18PLT")
7
8beta = features["beta_abs"].to_numpy()
9ellipt = features["ellipt_abs"].to_numpy()
10skew_thresholds = np.array([1, 2, 3, 5, 8, 12, 18, 25, 35, 50])
11ellipt_th = 0.2
12
13rows = []
14for skew_th in skew_thresholds:
15 low_skew = beta <= skew_th
16 frac_1d = np.mean(low_skew & (ellipt <= ellipt_th))
17 frac_2d = np.mean(low_skew & (ellipt > ellipt_th))
18 frac_3d = 1.0 - frac_1d - frac_2d
19 rows.append(
20 {
21 "skew_th": skew_th,
22 "frac_1d": frac_1d,
23 "frac_2d": frac_2d,
24 "frac_3d": frac_3d,
25 }
26 )
27
28sensitivity = pd.DataFrame(rows)
29print(sensitivity)
skew_th frac_1d frac_2d frac_3d
0 1 0.001348 0.005391 0.993261
1 2 0.002022 0.012803 0.985175
2 3 0.002022 0.018868 0.979111
3 5 0.004717 0.028302 0.966981
4 8 0.008086 0.053235 0.938679
5 12 0.013477 0.079515 0.907008
6 18 0.018868 0.148922 0.832210
7 25 0.022237 0.249326 0.728437
8 35 0.033693 0.395553 0.570755
9 50 0.044474 0.537736 0.417790
If the 3-D fraction stays high over a wide threshold range, the result is probably a property of the data. If the fractions flip abruptly near one threshold, report that sensitivity with the interpretation.
Plot The Dimensionality Grid#
plot_dim_confidence_grid maps class labels onto station x period
space. Color shows class. Opacity shows a confidence margin from the
threshold rule.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools.dimensionality import plot_dim_confidence_grid
4
5fig, ax = plt.subplots(figsize=(9, 4.5))
6plot_dim_confidence_grid(
7 "data/AMT/WILLY_DATA/L18PLT",
8 skew_th=3.0,
9 ellipt_th=0.2,
10 ax=ax,
11)
12fig.tight_layout()
This plot is best for pattern recognition. Look for coherent regions by station and period, not isolated cells.
Plot Period-Band Occupancy#
plot_dim_occupancy_area collapses station detail into period-band
fractions.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools.dimensionality import plot_dim_occupancy_area
4
5fig, ax = plt.subplots(figsize=(9, 3.8))
6plot_dim_occupancy_area(
7 "data/AMT/WILLY_DATA/L18PLT",
8 skew_th=3.0,
9 ellipt_th=0.2,
10 n_bands=24,
11 ax=ax,
12)
13fig.tight_layout()
Use this when you need to say whether 3-D behavior is concentrated at short periods, long periods, or spread across the whole band.
Map Dimensionality At One Period#
plot_dim_map chooses the nearest available period for each station
and maps the dimensionality class in station coordinates.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools.dimensionality import plot_dim_map
4
5fig, ax = plt.subplots(figsize=(8, 6))
6plot_dim_map(
7 "data/AMT/WILLY_DATA/L18PLT",
8 period=0.01,
9 skew_th=3.0,
10 ellipt_th=0.2,
11 ax=ax,
12)
13fig.tight_layout()
Use this for spatial checks. A line-wide class change is different from one station acting anomalously.
Pre-2D Inversion Assessment#
pre2d_inversion_assessment is the audit table to save before a 2-D
inversion. It combines dimensionality fractions, strike estimates,
strike stability, rotation status, and Groom-Bailey status.
1from pycsamt.emtools.dimensionality import pre2d_inversion_assessment
2
3assessment = pre2d_inversion_assessment(
4 "data/AMT/WILLY_DATA/L18PLT",
5 band=(0.001, 1.0),
6 skew_th=3.0,
7 ellipt_th=0.2,
8 rotation_applied=False,
9 rotation_method="consensus",
10 groom_bailey_attempted=False,
11 groom_bailey_applied=False,
12 groom_bailey_reason="Not attempted in this screening run.",
13)
14
15print(
16 assessment[
17 [
18 "station",
19 "frac_1d",
20 "frac_2d",
21 "frac_3d",
22 "strike_consensus_deg",
23 "strike_consensus_iqr_deg",
24 "strike_curve_iqr_deg",
25 "recommendation",
26 ]
27 ].head()
28)
station frac_1d ... strike_curve_iqr_deg recommendation
0 18-001A 0.0 ... 66.9 review_3d_effects_before_2d
1 18-002U 0.0 ... 96.0 review_3d_effects_before_2d
2 18-003A 0.0 ... 65.0 review_3d_effects_before_2d
3 18-004A 0.0 ... 89.5 review_3d_effects_before_2d
4 18-005U 0.0 ... 72.1 review_3d_effects_before_2d
[5 rows x 8 columns]
Important columns include:
period_min_sandperiod_max_s: assessed period band.frac_1d,frac_2d,frac_3d: dimensionality fractions.beta_abs_medianandbeta_abs_p95: skew summary.ellipt_abs_median: ellipticity summary.strike_sweep_deg: impedance-sweep strike estimate.strike_pt_deg: phase-tensor strike estimate.strike_consensus_degandstrike_consensus_iqr_deg: combined strike and uncertainty.strike_curve_iqr_deg: frequency-dependent strike stability.rotated_to_strikeandrotation_angle_deg: rotation audit.groom_bailey_attemptedandgroom_bailey_applied: distortion correction audit.recommendation: simple screening recommendation.
This table is useful in manuscripts and reports because it records not only the selected strike, but also whether the assumptions behind a 2-D workflow were checked.
Masking 3-D Samples#
mask_by_dimensionality replaces samples outside the selected classes
with NaN in the impedance tensor and tipper when present.
1from pycsamt.emtools.dimensionality import mask_by_dimensionality
2
3masked = mask_by_dimensionality(
4 "data/AMT/WILLY_DATA/L18PLT",
5 keep=(0, 1),
6 inplace=False,
7)
By default, keep=(0, 1) keeps 1-D and 2-D samples and masks 3-D
samples. Use this carefully: masking can remove large parts of a real
survey if the line is strongly 3-D.
Projecting To A 2-D Tensor Form#
project_to_2d rotates data to strike and optionally
antisymmetrizes the off-diagonal tensor terms.
1from pycsamt.emtools.dimensionality import project_to_2d
2
3projected = project_to_2d(
4 "data/AMT/WILLY_DATA/L18PLT",
5 strike=None,
6 method="swift",
7 antisym=True,
8 inplace=False,
9)
When strike=None, pyCSAMT estimates strike before rotation. Pass an
explicit strike angle when you have already selected one from your
assessment table or a separate structural analysis.
Sparse Dictionary Workflow#
The dictionary route learns patterns from four standardized features:
1beta_abs
2ellipt_abs
3logrho_det
4tip_amp
It is unsupervised. It does not know geology. It learns atoms that represent repeated feature patterns, encodes each sample with sparse coefficients, and assigns a dimensionality class from the dominant atom.
1from pycsamt.emtools.dimensionality import (
2 encode_dimensionality,
3 learn_dim_dictionary,
4)
5
6model = learn_dim_dictionary(
7 "data/AMT/WILLY_DATA/L18PLT",
8 n_atoms=6,
9 lam=0.05,
10 n_iter=40,
11 code_iter=50,
12)
13
14encoded = encode_dimensionality(
15 "data/AMT/WILLY_DATA/L18PLT",
16 model,
17 lam=0.05,
18 code_iter=50,
19)
20
21print(encoded.filter(regex="station|period|dim_pred|^a").head())
station period a0 a1 a2 a3 a4 a5 dim_pred
0 18-001A 0.000096 0.0 -1.327892 0.896831 0.0 0.016784 -0.00000 1
1 18-001A 0.000115 0.0 -1.317513 0.798761 0.0 0.000000 -0.00000 1
2 18-001A 0.000137 0.0 -1.361449 0.662154 0.0 0.000000 -0.02091 1
3 18-001A 0.000164 -0.0 -1.086200 0.800304 -0.0 0.350492 0.00000 1
4 18-001A 0.000196 -0.0 -0.336239 0.791009 -0.0 0.194019 0.00000 2
The model dictionary contains:
D: learned atom matrix.A: sparse code matrix for the training samples.muandsd: feature standardization values.feat: feature names.meta: sample metadata such as station names and periods.
The encoded table keeps the feature columns and adds:
a0,a1, …: sparse atom coefficients.dim_pred: dictionary-derived dimensionality label.
Compare Rule Labels And Dictionary Labels#
The dictionary workflow is most useful as an independent check, not as a replacement for the rule.
1import pandas as pd
2
3from pycsamt.emtools.dimensionality import (
4 classify_dimensionality,
5 encode_dimensionality,
6 learn_dim_dictionary,
7)
8
9survey = "data/AMT/WILLY_DATA/L18PLT"
10rule = classify_dimensionality(survey)
11model = learn_dim_dictionary(survey, n_atoms=6)
12encoded = encode_dimensionality(survey, model)
13
14compare = rule[["station", "period", "dim"]].merge(
15 encoded[["station", "period", "dim_pred"]],
16 on=["station", "period"],
17 how="inner",
18)
19agreement = (compare["dim"] == compare["dim_pred"]).mean()
20print(f"rule/dictionary agreement = {agreement:.2%}")
rule/dictionary agreement = 68.13%
Agreement near 100 percent means the learned atoms reproduce the rule. Lower agreement means the data-driven features are separating samples differently. That can be useful, but it needs interpretation.
Dictionary Masking And Atom Plots#
mask_by_dictionary applies the same idea as mask_by_dimensionality
but uses dim_pred from the learned dictionary. plot_atom_psection
shows which learned atom dominates each station-period cell.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools.dimensionality import (
4 learn_dim_dictionary,
5 mask_by_dictionary,
6 plot_atom_psection,
7)
8
9survey = "data/AMT/WILLY_DATA/L18PLT"
10model = learn_dim_dictionary(survey, n_atoms=6)
11
12masked = mask_by_dictionary(
13 survey,
14 model,
15 keep=(0, 1),
16 inplace=False,
17)
18
19fig, ax = plt.subplots(figsize=(9, 4.8))
20plot_atom_psection(survey, model, energy="l2", ax=ax)
21fig.tight_layout()
Use the atom pseudo-section to check whether one learned pattern is localized in a period band or station group. A model that produces random-looking atom occupancy is not a useful diagnostic.
Reading The Results#
Use this interpretation order:
Inspect
beta_absandellipt_absbefore accepting labels.Report the thresholds used for rule-based classification.
Sweep thresholds when the conclusion depends on the default values.
Treat broad station-period patterns as stronger evidence than isolated cells.
Save
pre2d_inversion_assessmentbefore a 2-D inversion.Use dictionary labels as an independent check, not as a black-box replacement for the phase-tensor rule.
Common Failure Modes#
- Mostly 3-D classifications
This may be a real survey property, especially in complex geology. Do a threshold sweep before changing thresholds to get the result you hoped for.
- No tipper data
tip_ampbecomesNaN. The dictionary workflow replaces missing values during standardization, but you should still record that tipper information was absent.- Unstable strike
A wide
strike_consensus_iqr_degorstrike_curve_iqr_degmeans strike is not stable across methods or period. Review the band before rotating to 2-D.- Masking removes too much data
If most samples are 3-D,
keep=(0, 1)may leave too little for a stable inversion. Consider changing the period band rather than masking blindly.- Dictionary disagreement
Rule and dictionary labels can disagree because they use different information. Inspect atom occupancy and feature distributions before using dictionary masks operationally.
Saving A Reproducible Bundle#
For reporting, save the raw features, rule labels, pre-2D assessment, and dictionary encoding.
1from pathlib import Path
2
3import matplotlib.pyplot as plt
4
5from pycsamt.emtools.dimensionality import (
6 classify_dimensionality,
7 encode_dimensionality,
8 learn_dim_dictionary,
9 phase_features_table,
10 plot_dim_confidence_grid,
11 pre2d_inversion_assessment,
12)
13
14survey = "data/AMT/WILLY_DATA/L18PLT"
15out = Path("outputs/dimensionality_l18plt")
16out.mkdir(parents=True, exist_ok=True)
17
18features = phase_features_table(survey)
19rule = classify_dimensionality(survey)
20assessment = pre2d_inversion_assessment(survey, band=(0.001, 1.0))
21model = learn_dim_dictionary(survey, n_atoms=6)
22encoded = encode_dimensionality(survey, model)
23
24features.to_csv(out / "phase_features.csv", index=False)
25rule.to_csv(out / "rule_dimensionality.csv", index=False)
26assessment.to_csv(out / "pre2d_assessment.csv", index=False)
27encoded.to_csv(out / "dictionary_encoding.csv", index=False)
28
29fig, ax = plt.subplots(figsize=(9, 4.5))
30plot_dim_confidence_grid(survey, ax=ax)
31fig.tight_layout()
32fig.savefig(out / "dimensionality_confidence_grid.png", dpi=200)
Worked Example#
The gallery example uses L18PLT from data/AMT/WILLY_DATA/. It
starts from one-station skew and ellipticity curves, shows the
rule-based feature-space partition, sweeps thresholds, plots the
pseudo-section and occupancy views, checks 2-D projection, and compares
rule-based labels with dictionary-learned labels.
Open the rendered example here: Phase-tensor dimensionality: rule-based and dictionary-learned (pycsamt.emtools.dimensionality).