Solvers And Grids#
The forward package separates three responsibilities:
model containersObjects such as
pycsamt.forward.LayeredModel,pycsamt.forward.Grid2D, andpycsamt.forward.Grid3Dstore resistivity, geometry, station positions, and metadata.solversObjects such as
pycsamt.forward.MT1DForward,pycsamt.forward.TEM1DForward,pycsamt.forward.MT2DForward, andpycsamt.forward.MT3DForwardcompute synthetic responses from the model containers.response containersObjects such as
pycsamt.forward.ForwardResponse,pycsamt.forward.ForwardResponse2D, andpycsamt.forward.ForwardResponse3Dhold the predicted fields, apparent resistivities, phases, time-domain decays, station coordinates, and feature-array helpers.
This separation matters because survey design, synthetic dataset generation, machine-learning workflows, and inversion handoff all need the same response arrays but may use different model constructors and different solver settings.
Solver Map#
The currently documented forward solver families are:
Solver |
Model container |
Primary axis |
Main outputs |
|---|---|---|---|
|
|
Frequencies in Hz |
Impedance |
|
|
Frequencies in Hz |
MT-like response with optional controlled-source correction. |
|
|
Time gates in seconds |
Step-off |
|
|
Frequencies in Hz |
TE/TM impedances, apparent resistivities, phases at profile stations. |
|
|
Frequencies in Hz |
Approximate tensor components on a station grid. |
The forward solvers are deterministic for a fixed model, axis, and solver configuration. Noise is applied later by the noise utilities, which keeps the physical response and the observation model separate.
1-D Layered Models#
pycsamt.forward.LayeredModel represents a stack of horizontal layers.
The final resistivity is the halfspace, so the thickness array has one
fewer entry than resistivity.
1import numpy as np
2
3from pycsamt.forward import LayeredModel, MT1DForward
4
5model = LayeredModel(
6 resistivity=[100.0, 10.0, 500.0],
7 thickness=[300.0, 800.0],
8 name="conductive_middle_layer",
9)
10
11freqs = np.logspace(-3, 4, 40)
12response = MT1DForward(freqs=freqs).run(model)
13
14print(response.rho_a.shape)
15print(response.phase.shape)
Layered models are useful for:
quick sanity checks against textbook MT behaviour;
training small synthetic catalogues;
building starting models for inversion;
extending a 1-D model laterally into a simple 2-D grid;
testing whether the chosen frequency or time range can sense the expected target depth.
The important depth scale for frequency-domain EM is the skin depth. A common engineering estimate is:
where \(\delta\) is in metres, \(\rho\) is resistivity in \(\Omega\,m\), and \(f\) is frequency in Hz. This is not a substitute for a forward model, but it is a useful first check: high frequencies see shallower structure, while low frequencies sample deeper structure.
1-D MT And CSAMT Solvers#
MT1DForward computes a plane-wave natural-source response using layered
earth impedance recursion. CSAMT1DForward uses the same layered-earth base
and can apply a controlled-source correction when source geometry is supplied.
1import numpy as np
2
3from pycsamt.forward import CSAMT1DForward, LayeredModel, MT1DForward
4
5model = LayeredModel(
6 resistivity=[80.0, 25.0, 600.0],
7 thickness=[250.0, 900.0],
8)
9
10freqs = np.logspace(-1, 4, 32)
11
12mt_response = MT1DForward(freqs=freqs).run(model)
13
14csamt_response = CSAMT1DForward(
15 freqs=freqs,
16 source_offset=5000.0,
17 dipole_length=1000.0,
18).run(model)
19
20mt_features = mt_response.to_array(log_rho=True, include_phase=True)
21csamt_features = csamt_response.to_array(log_rho=True, include_phase=True)
For MT and CSAMT 1-D responses:
response.freqshas shape(n_freqs,);response.zhas shape(n_freqs,)and is complex;response.rho_ahas shape(n_freqs,);response.phasehas shape(n_freqs,)in degrees;response.to_array()returns a one-dimensional feature vector.
When the model is one-dimensional, every surface station would see the same response. Use 2-D or 3-D grids when lateral geometry is part of the experiment.
1-D TDEM Solver#
TEM1DForward computes a central-loop step-off response for a layered earth.
It uses time gates rather than frequencies as the primary output axis.
1import numpy as np
2
3from pycsamt.forward import LayeredModel, TEM1DForward
4
5model = LayeredModel(
6 resistivity=[60.0, 250.0, 900.0],
7 thickness=[120.0, 700.0],
8)
9
10times = np.logspace(-6, -3, 30)
11
12response = TEM1DForward(
13 times=times,
14 loop_radius=50.0,
15 n_freqs=32,
16 n_lam=80,
17).run(model)
18
19print(response.times.shape)
20print(response.dBz_dt.shape)
For TDEM responses:
response.timeshas shape(n_times,);response.dBz_dthas shape(n_times,);response.to_array()returns a log-scaled decay feature vector by default.
Use early gates to test shallow sensitivity and later gates to test deeper sensitivity. If the time range is too narrow, the inversion may fit the decay curve but remain insensitive to the target interval.
2-D Profile Grid Concepts#
pycsamt.forward.Grid2D stores a finite-difference grid for a profile.
It contains:
horizontal cell widths
dx;vertical cell heights
dz;cell resistivity matrix
resistivitywith shape(nz, nx);surface station x-positions
x_stations;padding count
n_pad;node and cell-centre coordinate helpers.
The grid uses depth-positive z coordinates. Resistivity is stored
top-to-bottom and left-to-right. Stations must lie inside the grid extent.
The core region is the scientific model of interest. Padding cells are added on
the left, right, and bottom to reduce boundary influence. The n_pad value
records how many padding cells were added so plotting and interpretation tools
can distinguish core cells from numerical buffer cells.
1from pycsamt.forward import Grid2D
2
3grid = Grid2D.halfspace(
4 rho=100.0,
5 nx=40,
6 nz=28,
7 x_max=8000.0,
8 z_max=5000.0,
9 n_pad=8,
10 pad_factor=1.3,
11 n_stations=12,
12)
13
14print(grid.resistivity.shape)
15print(grid.x_stations)
16print(grid.x_nodes[0], grid.x_nodes[-1])
2-D Profile Grid Constructors#
Use the constructor that matches the experiment you want to run.
Constructor |
Purpose |
Typical use |
|---|---|---|
|
Uniform background model. |
Baseline checks, boundary tests, and solver sanity checks. |
|
Background with one rectangular resistive or conductive block. |
Target detectability, station-spacing tests, and pseudosection training examples. |
|
Laterally extends a layered model across a profile. |
Compare 1-D and 2-D solvers, or build a simple starting model. |
|
Randomized 2-D resistivity model. |
Synthetic catalogues and stress tests. |
A compact block-anomaly grid looks like this:
1from pycsamt.forward import Grid2D, MT2DForward
2
3grid = Grid2D.with_anomaly(
4 bg_rho=500.0,
5 anomaly_rho=5.0,
6 anomaly_bounds=(2000.0, 6000.0, 300.0, 1500.0),
7 nx=50,
8 nz=35,
9 x_max=10000.0,
10 z_max=6000.0,
11 n_pad=8,
12 n_stations=16,
13)
14
15response = MT2DForward(
16 freqs=[1.0, 10.0, 100.0],
17 grid=grid,
18 verbose=True,
19).run()
anomaly_bounds are specified in core model coordinates as
(x_lo, x_hi, z_lo, z_hi). The constructor internally accounts for padding
when placing the anomaly into the full numerical grid.
2-D MT Solver#
MT2DForward solves the 2-D MT finite-difference problem for TE and TM modes.
It returns a pycsamt.forward.ForwardResponse2D.
1station_0 = response.station_response(0)
2
3print(station_0["rho_a_te"])
4print(station_0["phase_te"])
5
6features_te = response.to_feature_array(
7 mode="te",
8 log_rho=True,
9 include_phase=True,
10)
11
12features_both = response.to_feature_array(
13 mode="both",
14 log_rho=True,
15 include_phase=True,
16)
Response array shapes are always frequency first:
Attribute |
Shape |
Meaning |
|---|---|---|
|
|
Frequency axis in Hz. |
|
|
Surface station positions. |
|
|
TE impedance component. |
|
|
TM impedance component. |
|
|
TE apparent resistivity. |
|
|
TE phase in degrees. |
|
|
TM apparent resistivity. |
|
|
TM phase in degrees. |
|
|
Station-first feature matrix for ML or downstream processing. |
This difference is deliberate: physical response arrays are frequency-first, while feature matrices are station-first because each row is one training or analysis sample.
When passing a 2-D forward response to the inversion profile API, transpose the selected response arrays:
1inversion_data = {
2 "freqs": response.freqs,
3 "rho_a": response.rho_a_te.T,
4 "phase": response.phase_te.T,
5 "station_x": response.stations_x,
6}
3-D Volume Grid Concepts#
pycsamt.forward.Grid3D stores a 3-D resistivity volume and a 2-D
station layout. It contains:
cell widths
dx,dy, anddz;resistivity with shape
(nz, ny, nx);station coordinates
stations_xywith shape(n_stations, 2);padding in x, y, and z;
helpers that extract XZ and YZ slices for the quasi-3-D solver.
1from pycsamt.forward import Grid3D
2
3grid = Grid3D.halfspace(
4 rho=100.0,
5 nx=20,
6 ny=20,
7 nz=15,
8 x_max=8000.0,
9 y_max=8000.0,
10 z_max=4000.0,
11 n_pad=6,
12 nx_stations=5,
13 ny_stations=5,
14)
15
16print(grid.resistivity.shape)
17print(grid.stations_xy.shape)
The regular station grid is created over the core model and shifted into the padded numerical coordinate system. As in 2-D, stations must lie inside the full grid extent.
3-D Volume Grid Constructors#
The main constructors are:
Constructor |
Purpose |
Typical use |
|---|---|---|
|
Uniform 3-D background. |
Baseline tensor and station-grid checks. |
|
Background with one 3-D rectangular block. |
Survey design and quasi-3-D anomaly tests. |
|
Random laterally variable model. |
Synthetic training data and robustness tests. |
1from pycsamt.forward import Grid3D, MT3DForward
2
3grid = Grid3D.block_anomaly(
4 bg_rho=500.0,
5 anomaly_rho=20.0,
6 bounds=(2000.0, 6000.0, 2000.0, 6000.0, 300.0, 1500.0),
7 nx=20,
8 ny=20,
9 nz=15,
10 x_max=8000.0,
11 y_max=8000.0,
12 z_max=4000.0,
13 n_pad=6,
14 nx_stations=5,
15 ny_stations=5,
16)
17
18response = MT3DForward(
19 freqs=[1.0, 10.0, 100.0],
20 grid=grid,
21).run()
Quasi-3-D Solver#
MT3DForward is a quasi-3-D MT solver. It does not claim to replace a full
production 3-D modelling engine. Instead, it approximates tensor responses by
extracting orthogonal 2-D slices from the 3-D grid and running MT2DForward
on those slices.
Conceptually:
XZ slices are grouped by station y-row;
YZ slices are grouped by station x-column;
TE and TM responses from both slice families are combined;
off-diagonal tensor components
Z_xyandZ_yxcarry the main response;diagonal components
Z_xxandZ_yyare represented as approximate tensor outputs and should be interpreted carefully.
The output is pycsamt.forward.ForwardResponse3D.
1print(response.rho_a_xy.shape)
2print(response.phase_xy.shape)
3print(response.stations_xy.shape)
4
5features = response.to_feature_array(
6 components="xy_yx",
7 log_rho=True,
8 include_phase=True,
9)
10
11all_components = response.to_feature_array(
12 components="all",
13 log_rho=True,
14 include_phase=False,
15)
As with the 2-D response, physical arrays are frequency-first:
(n_freqs, n_stations). Feature arrays are station-first:
(n_stations, n_features).
Grid Design Rules#
Forward results are only as useful as the grid that produced them. Before trusting a response, check these points.
Positive resistivityEvery model cell must have strictly positive resistivity. The grid constructors validate this, but it is still worth checking after manual edits.
Station coverageStations must sit inside the grid. For profile experiments, station spacing should be small enough to sample the expected lateral anomaly signature.
PaddingPadding reduces boundary effects. Increase
n_padorpad_factorif the response changes noticeably when the model extent is increased.Near-surface resolutionShallow cells should resolve high-frequency or early-time sensitivity. A very coarse top layer can hide near-surface targets or create numerical artefacts.
Depth extentThe bottom of the grid should be deeper than the target and deeper than the main sensitivity range of the lowest frequencies or latest gates.
Frequency or time rangeDo not rely on the solver to compensate for missing physics in the survey design. If the chosen axis cannot see the target, the inversion will not recover it reliably.
DimensionalityUse 1-D models for layered checks, 2-D grids for profile structure, and quasi-3-D grids for survey design or synthetic AI catalogues. Move to native external backends when production 2-D or 3-D inversion files are required.
A compact grid-check helper can be useful in notebooks:
1import numpy as np
2
3def describe_grid2d(grid):
4 print(f"cells: nz={grid.nz}, nx={grid.nx}")
5 print(f"stations: {grid.n_stations}")
6 print(f"x extent: {grid.x_nodes[0]:.1f} to {grid.x_nodes[-1]:.1f} m")
7 print(f"z extent: {grid.z_nodes[0]:.1f} to {grid.z_nodes[-1]:.1f} m")
8 print(f"rho range: {np.nanmin(grid.resistivity):.3g} to "
9 f"{np.nanmax(grid.resistivity):.3g} ohm m")
10 print(f"padding cells: {grid.n_pad}")
11
12describe_grid2d(grid)
Response Containers And Feature Arrays#
Response containers preserve the physical arrays and also expose feature helpers for machine-learning and batch processing.
Response object |
Physical shape |
Feature shape |
|---|---|---|
|
1-D arrays over frequency or time. |
|
|
|
|
|
|
|
Use physical arrays when preparing inversion data, plotting scientific responses, or checking units. Use feature arrays when training models, comparing many station responses, or building tabular downstream datasets.
Debugging Solver Experiments#
When a forward response looks wrong, debug in this order:
Run a halfspace model first. A halfspace should produce smooth, stable responses.
Reduce the number of frequencies and stations until the experiment is easy to inspect.
Plot the grid with stations visible and confirm that the target is where you think it is.
Check whether the anomaly is inside the sensitivity range of the selected frequencies or time gates.
Increase padding and model extent. If the response changes strongly, the original grid was too small.
Compare a 2-D halfspace result with a 1-D MT response for the same resistivity.
Check array orientation before passing data to inversion or ML routines.
Next Pages#
Forward Configuration explains how configuration objects create grids and solvers.
Synthetic Datasets And Noise explains how to generate many forward responses.
Forward Plotting shows how to inspect models and responses.
From Forward Modelling To Inversion explains how to pass synthetic responses to inversion workflows.