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

Station name.

freq

Frequency in hertz.

period

Period in seconds.

beta_abs

Absolute phase-tensor skew angle, in degrees.

ellipt_abs

Absolute phase-tensor ellipticity.

logrho_det

log10 determinant-style apparent resistivity.

phi_det

Determinant impedance phase, in degrees.

tip_amp

Tipper amplitude when tipper data are available; otherwise NaN.

The determinant-style apparent resistivity uses the same practical EDI unit convention used elsewhere in emtools:

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

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

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

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

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

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

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_s and period_max_s: assessed period band.

  • frac_1d, frac_2d, frac_3d: dimensionality fractions.

  • beta_abs_median and beta_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_deg and strike_consensus_iqr_deg: combined strike and uncertainty.

  • strike_curve_iqr_deg: frequency-dependent strike stability.

  • rotated_to_strike and rotation_angle_deg: rotation audit.

  • groom_bailey_attempted and groom_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.

  • mu and sd: 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()
../../_images/user-guide-emtools-dimensionality-14.png

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_abs and ellipt_abs before 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_assessment before 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_amp becomes NaN. 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_deg or strike_curve_iqr_deg means 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)
../../_images/user-guide-emtools-dimensionality-15.png

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