18.14. Broken Hill: MT Field Data To A 3-D Resistivity Model#

Every other tutorial in this section either stops at prepared inversion input (Prepare A ModEM Inversion) or runs a small synthetic or profile-scale case. This one follows a single real magnetotelluric survey all the way through: 21 wideband stations recorded over the world-class Broken Hill Pb-Zn-Ag deposit (New South Wales, Australia), from the raw EDI files, through dimensionality analysis and ModEM input construction, to the published 3-D inversion model of AlQahtani et al. (2026) and its geological interpretation.

The survey, the data, and the model were released open-access with the paper. pyCSAMT bundles a working subset under data/MT/broken-hill; see that folder’s README.md for the exact provenance and the citations you must reproduce if you use the data (repeated under See Also below).

18.14.1. What You Will Learn#

After this tutorial you should be able to:

  • load a real EDI survey whose station identifiers are unset in the header, and understand why pyCSAMT still keeps the 21 soundings distinct;

  • run station_confidence_table() and read the spread of composite confidence across a small wideband survey;

  • decide between a 2-D and a 3-D inversion from build_phase_tensor_table() and a multi-frequency phase-tensor map, rather than by assumption;

  • build native ModEM 3-D input from the EDIs with InputBuilder;

  • load a completed ModEM run with InversionResult, check how well the model fits the data, and render geo-referenced depth maps, a depth-integrated conductance map, and an arbitrary-azimuth vertical section;

  • connect those figures back to the paper’s central result – a resistive upper crust hosting discrete shallow conductors on the ore horizon.

18.14.2. Starting Point#

The bundled data lives under data/MT/broken-hill:

edis/

21 Phoenix EMpower EDI exports (BH_1 … BH_21), each carrying a full impedance tensor and a vertical-field (tipper) transfer function.

final-models/

The published ModEM inversion: BH_31.dat (the observed data with its real error floors), BH_31_NLCG_030.dat (the model response), BH_31_NLCG_030.res (residuals), and the recovered resistivity model BH_31_NLCG_030.rho (WS format).

Every path below is a plain string. To run this tutorial against your own MT survey, replace data/MT/broken-hill/edis with your EDI directory; the inversion-result section needs a completed ModEM run directory in place of data/MT/broken-hill/final-models.

1>>> from pathlib import Path
2
3>>> import matplotlib.pyplot as plt
4>>> import numpy as np
5
6>>> run_root = Path("runs/broken_hill")
7>>> run_root.mkdir(parents=True, exist_ok=True)
8>>> figure_dir = run_root / "figures"
9>>> figure_dir.mkdir(exist_ok=True)

18.14.3. Loading The Survey#

pycsamt.api.read_edis() reads the directory into a survey object whose .collection is the station container the rest of the workflow uses.

 1>>> from pycsamt.api import read_edis
 2
 3>>> survey = read_edis(
 4...     "data/MT/broken-hill/edis",
 5...     recursive=False,
 6...     progress=False,
 7... )
 8>>> sites = survey.collection
 9>>> print(len(sites))
1021
11
12>>> print(sorted(sites.stations())[:3])
13['BH_10_imp_rev', 'BH_11_imp', 'BH_12_imp_rev']

Twenty-one stations, keyed by their file stems. That fallback matters here: EMpower writes a literal DATAID=None into every >HEAD block – the real station name lives only in >INFO and the filename – so a collection keyed on the header identifier alone would collapse all 21 soundings onto a single "None" key. pyCSAMT treats the placeholder as missing and falls back to the filename stem, which is why len(sites) is 21 and not 1.

18.14.4. Station Confidence#

station_confidence_table() scores each sounding on the smoothness, coverage, and internal consistency of its transfer functions. The "composite" method blends those into one number in [0, 1]; see Inspect and QC a Survey for the full breakdown.

 1>>> from pycsamt.emtools.qc import station_confidence_table
 2
 3>>> confidence = station_confidence_table(sites, method="composite", api=True)
 4>>> table = confidence.to_pandas(copy=True)
 5>>> print(table[["station", "confidence", "coverage"]].head(5).to_string(index=False))
 6      station  confidence  coverage
 7BH_10_imp_rev    0.856651       1.0
 8    BH_11_imp    0.739758       0.8
 9BH_12_imp_rev    0.816676       1.0
10BH_13_imp_rev    0.815596       1.0
11BH_14_imp_rev    0.812210       1.0
12
13>>> print(round(table["confidence"].min(), 3),
14...       round(table["confidence"].median(), 3),
15...       round(table["confidence"].max(), 3))
160.627 0.778 0.876

Composite confidence runs from 0.63 to 0.88 with a median of 0.78 – tight for a real survey, and nothing near a rejection threshold. The one station at coverage = 0.8 (BH_11) simply has a shorter usable band than the rest; that is worth noting before meshing but is not a reason to drop the station. All 21 move forward.

Every station also carries geographic coordinates and a real tipper, which is what makes the dimensionality and mapping steps below possible:

 1>>> from pycsamt.emtools import ensure_sites
 2
 3>>> S = ensure_sites("data/MT/broken-hill/edis", recursive=False)
 4>>> n_coord = sum(
 5...     1 for s in S
 6...     if s.coords and np.isfinite(s.coords[0]) and np.isfinite(s.coords[1])
 7... )
 8>>> print(n_coord, "/", len(S))
 921 / 21
10
11>>> lats = np.array([s.coords[0] for s in S])
12>>> lons = np.array([s.coords[1] for s in S])
13>>> print(round(float(np.ptp(lats)), 3), round(float(np.ptp(lons)), 3))
140.178 0.187

The survey spans about 0.18 degrees in each direction – roughly 20 km north-south by 18 km east-west at this latitude – an areal deployment, not a single line. That geometry is the first argument for a 3-D inversion; the phase tensor is the second.

18.14.5. Is The Response 2-D Or 3-D?#

A 2-D inversion assumes the subsurface can be described by a single geoelectric strike, with no along-strike variation. The phase tensor tests that assumption directly: its skew angle \(\beta\) is a rotationally invariant measure of three-dimensionality that is immune to galvanic static shift, and \(|\beta| \gtrsim 3^\circ\) is the conventional threshold for calling a response 3-D.

 1>>> from pycsamt.emtools import build_phase_tensor_table
 2
 3>>> df = build_phase_tensor_table(S)
 4>>> print(len(df), df["station"].nunique())
 51953 21
 6
 7>>> band = (df["period"] >= 1 / 1000) & (df["period"] <= 1 / 0.008)
 8>>> frac_3d = (df.loc[band, "skew"].abs() > 3).mean()
 9>>> print(round(float(frac_3d), 3))
100.397

Across the 0.008-1000 Hz band that the published inversion uses, 40% of all (station, period) cells exceed the 3-degree skew threshold. That is far too much three-dimensionality to justify a 2-D inversion. The plot_phase_tensor_map_grid() view shows where and at what frequency it concentrates:

 1>>> from pycsamt.emtools import plot_phase_tensor_map_grid
 2
 3>>> fig = plot_phase_tensor_map_grid(
 4...     S,
 5...     frequencies=[30.0, 3.0, 0.3, 0.03],
 6...     c_by="skew",
 7...     tipper_convention="parkinson",
 8...     ellipse_scale=1.3,
 9...     station_labels="none",
10...     ref_ellipse="none",
11...     suptitle="Broken Hill -- phase tensor (skew) + Parkinson arrows",
12...     recursive=False,
13... )
14>>> fig.savefig(figure_dir / "phase_tensor_grid.png", dpi=200, bbox_inches="tight")
../_images/phase_tensor_grid.png

Phase-tensor ellipses filled with skew \(\beta\) (blue negative, red positive) and real induction arrows in the Parkinson convention, at 30, 3, 0.3 and 0.03 Hz.#

At 30 Hz – shallowest – the skew is a scatter of blue and red and the ellipses are short and variably oriented: near-surface 3-D structure. By 0.03 Hz almost every ellipse is a similar pale blue and the long axes have aligned NW-SE, so the deep section behaves as a coherent 2-D layer. The induction arrows tell the same story: long at high frequency near the western stations, short and consistent at low frequency. A shallow section broken up by 3-D bodies over a simpler deep section is exactly the case a 2-D inversion cannot represent – and, as it turns out, exactly the geology the paper describes.

18.14.6. Building ModEM 3-D Input#

ModEmConfig holds every mesh, error-floor, and inversion-control choice; InputBuilder turns it plus the sites into a native ModEM run directory. The configuration below follows the reasoning in Prepare A ModEM Inversion – a 250 m core cell to match the 2.5 km station spacing, a 50 m first earth layer, a resistive 1000 \(\Omega\cdot\mathrm{m}\) half-space start because the region is known to be resistive, and the full impedance tensor because a 3-D area survey cannot lean on a presumed strike.

 1>>> from pycsamt.models.modem import ModEmConfig, InputBuilder
 2
 3>>> cfg = ModEmConfig(
 4...     mode="3d",
 5...     component_type="Full_Impedance",
 6...     error_floor_z=0.05,
 7...     initial_rho=1000.0,
 8...     n_airlayers=6,
 9...     cell_size_h=250.0,
10...     cell_size_v_top=50.0,
11...     depth_scale=1.1,
12...     n_padding_xy=7,
13...     nz=45,
14...     smooth_x=0.3,
15...     smooth_y=0.3,
16...     smooth_z=0.3,
17...     n_smooth_iter=2,
18...     max_iterations=100,
19...     target_rms=1.05,
20...     binary_3d="Mod3DMT",
21... )
22>>> workdir = run_root / "modem_input"
23>>> written = InputBuilder(config=cfg).build(sites, workdir=workdir)
24>>> print(sorted(p.name for p in workdir.iterdir()))
25['control.inv', 'covariance.cov', 'data.dat', 'fwd_control.ctrl', 'm0.ws']
26
27>>> from pycsamt.models.modem import ModEmData
28
29>>> built = ModEmData.read(written["data"])
30>>> print(built.n_sites, built.n_periods)
3121 100

The builder keeps every frequency present in the EDI files – 100 of them here. A production run would decimate to roughly 7 per decade first (Prepare an Occam2D Inversion shows FrequencyFilter doing exactly that); the published Broken Hill run used 24 periods over 0.003-333 s. Running Mod3DMT on workdir is an external step covered in Run Classical Inversions: Occam2D, ModEM, and MARE2DEM. The rest of this tutorial picks up from the authors’ completed run.

18.14.7. The Published Inversion#

InversionResult scans a run directory and loads whatever it finds – models, observed and predicted data, covariance, control, and log.

 1>>> from pycsamt.models.modem import InversionResult
 2
 3>>> result = InversionResult("data/MT/broken-hill/final-models")
 4>>> print(result.mode, sorted(result.models))
 53d ['iter_0030']
 6
 7>>> model = result.model_final
 8>>> print(model.shape)
 9(55, 120, 129)
10
11>>> rho = model.rho_linear
12>>> print(tuple(int(v) for v in np.percentile(rho, [2, 50, 98])))
13(55, 247, 1858)

The recovered model is a 129 x 120 cell grid, 55 layers deep. Its resistivity runs from about 55 \(\Omega\cdot\mathrm{m}\) at the conductive 2nd percentile to about 1860 \(\Omega\cdot\mathrm{m}\) at the resistive 98th, with a median near 250 – a resistive model with a conductive tail, the numeric signature of “mostly resistive crust with a few discrete conductors”.

Before interpreting the model, check that it actually fits the data. PlotMisfitMap computes the normalised root-mean-square misfit per station and overall.

1>>> from pycsamt.models.modem.plot import PlotMisfitMap
2
3>>> fig = PlotMisfitMap(
4...     result=result,
5...     use_lonlat=False,
6...     show_line_labels=False,
7... ).plot()
8>>> fig.savefig(figure_dir / "misfit_map.png", dpi=200, bbox_inches="tight")
../_images/misfit_map.png

Per-station impedance misfit for the published model. The subtitle reports the overall RMS.#

The overall impedance RMS is about 2.5, and most stations sit between 1.5 and 2.5 (green to pale yellow). Two stations reach RMS ~4 (dark red). That is a real, honestly-not-converged inversion: RMS 2.5 is well above the ideal target of 1.0, so absolute resistivity values and sharp boundaries carry less weight than the broad-scale contrast between resistive and conductive domains. No ModEM .log ships with the published model, so the iteration-by-iteration RMS history is not available; result.final_rms is nan for that reason.

18.14.8. Geo-Referenced Depth Maps#

PlotDepthMap draws horizontal slices as geographic maps. ModEM data files reduce longitude by 100 degrees (the Broken Hill origin reads 41.53481), so the true value is passed explicitly as origin_lon.

 1>>> from pycsamt.models.modem.plot import PlotDepthMap
 2
 3>>> origin = dict(origin_lat=-31.95556, origin_lon=141.53481)
 4>>> fig = PlotDepthMap(
 5...     result,
 6...     depths={"(a) 1 km": 1000, "(b) 2 km": 2000,
 7...             "(c) 3 km": 3000, "(d) 5 km": 5000},
 8...     rho_range=(1, 10000),
 9...     mask_outside_hull=True,
10...     contours=[1000],
11...     scalebar=True,
12...     north_arrow=True,
13...     title="Broken Hill ModEM -- resistivity depth slices",
14...     **origin,
15... ).plot()
16>>> fig.savefig(figure_dir / "depth_slices.png", dpi=200, bbox_inches="tight")
../_images/depth_slices.png

Resistivity at 1, 2, 3 and 5 km. mask_outside_hull=True blanks cells beyond the station convex hull; the black line is the 1000 \(\Omega\cdot\mathrm{m}\) contour.#

The crust is resistive everywhere (blue, > 1000 \(\Omega\cdot\mathrm{m}\)), and the resistive core inside the 1000 \(\Omega\cdot\mathrm{m}\) contour grows with depth. At 1 km a handful of small conductive patches (yellow, < 30 \(\Omega\cdot\mathrm{m}\)) sit along the station line in the centre of the survey; by 3 km they have faded. Shallow, discrete, and shrinking downward – consistent with mineralised horizons rather than a deep-rooted conductor.

The most compact single view is a conductance map, the depth integral of conductivity over a window, \(S = \int_{z_1}^{z_2} \sigma \, dz\), in siemens.

 1>>> fig = PlotDepthMap(
 2...     result,
 3...     quantity="conductance",
 4...     conductance_window=(1000, 6000),
 5...     render="gouraud",
 6...     cmap="magma",
 7...     norm="linear",
 8...     rho_range=(10, 50),
 9...     smooth_sigma=0.8,
10...     mask_outside_hull=True,
11...     station_color="white",
12...     cbar_orientation="horizontal",
13...     title="Broken Hill -- conductance, 1-6 km",
14...     **origin,
15... ).plot()
16>>> fig.savefig(figure_dir / "conductance.png", dpi=200, bbox_inches="tight")
../_images/conductance.png

Conductance integrated over 1-6 km. render="gouraud" interpolates between cell centres for the smooth appearance used in published conductance maps.#

The interior of the survey is resistive (dark, ~10 S). The conductive anomalies (yellow, up to ~50 S) form an arc along the eastern and south-eastern margin, over the mapped trace of the sulphide-bearing Broken Hill Group. This is the map an MT mineral survey is usually reduced to before it is overlaid on geology and potential-field data.

18.14.9. A Vertical Section#

PlotSection in arbitrary-azimuth mode cuts the model along any geographic line. A NW-SE cut runs across the regional structural grain.

 1>>> from pycsamt.models.modem.plot import PlotSection
 2
 3>>> fig = PlotSection(
 4...     result=result,
 5...     start_point=(-31.860, 141.490),
 6...     end_point=(-32.020, 141.610),
 7...     use_latlon=True,
 8...     origin_lat=-31.95556,
 9...     origin_lon=141.53481,
10...     n_samples=320,
11...     depth_max=8000.0,
12...     rho_min=1.0,
13...     rho_max=5000.0,
14...     show_station_names=False,
15...     station_tol=2500.0,
16...     figsize=(11.5, 4.6),
17...     title="Broken Hill ModEM -- section B-B' (NW -> SE)",
18... ).plot()
19>>> fig.savefig(figure_dir / "section.png", dpi=200, bbox_inches="tight")
../_images/section.png

NW-SE section across strike. depth_max=8000 clips the featureless deep model the shallow survey does not constrain.#

The section resolves what the depth maps only hint at: the conductors (red/orange, < 10 \(\Omega\cdot\mathrm{m}\)) are discrete bodies confined to the top ~1.5 km of an otherwise uniformly resistive (> 1000 \(\Omega\cdot\mathrm{m}\)) crust. There is no conductive pathway connecting them downward. Because the section is defined by latitude and longitude it can be drawn along the exact trace of a mapped geological cross-section for a one-to-one comparison – which is how the paper ties each conductor to a specific unit of the Broken Hill Group.

18.14.10. What The Model Means#

Read together, the figures reproduce the paper’s central observation. The Broken Hill mineral system – one of the largest lead-zinc-silver deposits ever found – sits in a predominantly resistive upper crust. The only conductive anomalies are small, shallow (< 3 km), and spatially tied to the sulphide-rich Broken Hill Group, chiefly the Hores Gneiss. Crucially, none of them connects downward to a lower-crustal conductor.

That matters for exploration. The prevailing “source-to-sink” model expects a major ore system to be underlain by a steep, crustal-scale conductive pathway that once carried metal-bearing fluids. Broken Hill does not preserve one: the authors argue it was severed during granulite-facies metamorphism and later orogeny, leaving only the shallow mineralised horizons behind. Had the deposit’s geology been hidden under sedimentary cover, an exploration program that chased large conductors would have walked straight past it. A resistive MT signature is not, on its own, evidence of a barren crust.

18.14.11. Reusing This With Your Own Data#

  1. Point read_edis() at your EDI directory. Check len(sites) against the file count – if it collapses, your EDIs likely share a placeholder DATAID, which pyCSAMT already works around by station filename.

  2. Run station_confidence_table() and look at the low-confidence tail before meshing.

  3. Use build_phase_tensor_table() and plot_phase_tensor_map_grid() to choose 2-D or 3-D. High skew over a meaningful frequency range, or an areal station layout, both point to 3-D.

  4. Build input with InputBuilder, decimate the frequency band, and run Mod3DMT externally (Run Classical Inversions: Occam2D, ModEM, and MARE2DEM).

  5. Load the finished run with InversionResult, check PlotMisfitMap first, and only then interpret PlotDepthMap and PlotSection.

18.14.12. See Also#

Prepare A ModEM Inversion

The full ModEmConfig/InputBuilder walkthrough, including how each mesh and error-floor number is justified from the data.

Run Classical Inversions: Occam2D, ModEM, and MARE2DEM

Compiling and running Mod3DMT (and Occam2D, MARE2DEM), and why most environments should treat the run step as a dry run.

Phase Tensor And Impedance Tensor Tools

The phase-tensor toolkit in depth, including plot_phase_tensor_map_grid.

ModEM

The ModEM integration layer, PlotDepthMap, and PlotSection.

data/MT/broken-hill/README.md

Full data provenance, the survey characteristics, and the required citations.

If you use the Broken Hill data or model, cite both the article and the data release, given in full below.

Data and model citation

AlQahtani, Y., Ozaydin, S., Chatzaras, V., Rey, P. F., & Passos, T. (2026). Why does the Broken Hill deposit sit in resistive crust? Magnetotelluric evidence for metamorphic decoupling of a world-class mineral system. Journal of Geophysical Research: Solid Earth, 131, e2026JB035666. https://doi.org/10.1029/2026JB035666

AlQahtani, Y., Ozaydin, S., Chatzaras, V., Rey, P. F., & Passos, T. (2026). Open data for the article “Why does the Broken Hill deposit sit in resistive crust?”. Zenodo. https://doi.org/10.5281/zenodo.21272106

The survey was acquired by the University of Sydney; the authors acknowledge the Wilyakali/Wiljagali people, Traditional Custodians of the land on which it was recorded. The inversion used ModEM (Kelbert et al., 2014, Computers & Geosciences 66, 40-53).