16.3. Forward Configuration#

Forward modelling should be reproducible. A synthetic response, training dataset, or grid experiment is only useful when another user can reconstruct the solver, model prior, sampled axis, station layout, noise model, and random seed.

The forward package provides three configuration dataclasses:

All three classes follow the same workflow:

  1. write an annotated template;

  2. edit the template as the project source of truth;

  3. load the template back into Python;

  4. validate before computation;

  5. build arrays, grids, solvers, or dataset keyword arguments from the config;

  6. archive the config next to datasets, figures, and interpretation notes.

16.3.1. Configuration Classes#

Class

Main purpose

Main helpers

ForwardConfig

1-D MT/CSAMT/TEM synthetic dataset generation.

freq_grid(), time_grid(), to_dataset_kwargs(), write_template(), from_file(), summary().

ForwardConfig2D

2-D MT finite-difference grid and solver setup.

freq_grid(), to_grid(), to_solver_kwargs(), write_template(), from_file(), summary().

ForwardConfig3D

Quasi-3-D MT grid and solver setup.

freq_grid(), to_grid(), to_solver_kwargs(), write_template(), from_file(), summary().

16.3.2. Template Files#

Forward configuration files can be written as Python, JSON, YML, or YAML. The file extension selects the format automatically. Templates include parameter comments, which makes them good review artifacts for scientific projects.

1>>> from pycsamt.forward import ForwardConfig, ForwardConfig2D, ForwardConfig3D
2
3>>> ForwardConfig.write_template("configs/forward_1d.yml")
4>>> ForwardConfig2D.write_template("configs/forward_2d.yml")
5>>> ForwardConfig3D.write_template("configs/forward_3d.yml")

Load the edited files with strict validation of keys:

 1>>> from pycsamt.forward import ForwardConfig
 2
 3>>> cfg = ForwardConfig.from_file("configs/forward_1d.yml", strict=True)
 4>>> cfg.validate()
 5>>> print(cfg.summary())
 6ForwardConfig
 7  solver               = 'mt1d'
 8  freq_min             = 0.0001 Hz
 9  freq_max             = 1e+04 Hz
10  n_freqs              = 30
11  n_layers             = 3–7
12  rho_min              = 1 Ω·m
13  rho_max              = 1e+04 Ω·m
14  depth_max            = 2e+03 m
15  n_samples            = 10,000
16  noise_level          = 0.05  (gaussian)
17  seed                 = None
18  n_jobs               = 1
19  output               = ./forward_dataset.npz

This is an unedited template, so every field is still the class default – edit the values that matter for the experiment before loading it back. strict=True is recommended for production runs because misspelled keys raise an error instead of being ignored. Use strict=False only when loading a file that intentionally contains extra metadata.

16.3.3. 1-D Configuration#

ForwardConfig controls the 1-D solver, frequency grid or time gate grid, random layered model prior, noise model, output path, and parallel worker count. The sampled axis is logarithmic: for frequency-domain solvers, \(f_i = 10^{a + i(b-a)/(n_f-1)}\) where \(a=\log_{10}(f_\min)\), \(b=\log_{10}(f_\max)\), and \(i=0,\ldots,n_f-1\). For TEM, the same construction is applied to time gates, \(t_i = 10^{c + i(d-c)/(n_t-1)}\), using \(c=\log_{10}(t_\min)\) and \(d=\log_{10}(t_\max)\). This makes the configuration compact while still recording every sampled point deterministically.

Setting group

Fields

Solver

solver, freq_min, freq_max, n_freqs, time_min, time_max, n_times, loop_radius.

Earth model prior

n_layers_min, n_layers_max, rho_min, rho_max, depth_max, geology.

Dataset

n_samples, noise_level, noise_type, include_phase, seed, n_jobs.

Output

output_dir, output_name, verbose.

The solver controls which sampled axis is active:

solver

Active axis

Main settings

"mt1d"

Frequency

freq_min, freq_max, n_freqs, include_phase.

"csamt1d"

Frequency

freq_min, freq_max, n_freqs, include_phase.

"tem1d"

Time gates

time_min, time_max, n_times, loop_radius.

Generate a 1-D MT training dataset:

 1>>> from pycsamt.forward import ForwardConfig, generate_dataset
 2
 3>>> cfg = ForwardConfig(
 4...     solver="mt1d",
 5...     freq_min=1e-3,
 6...     freq_max=1e4,
 7...     n_freqs=40,
 8...     n_layers_min=3,
 9...     n_layers_max=7,
10...     rho_min=1.0,
11...     rho_max=10_000.0,
12...     depth_max=3000.0,
13...     n_samples=5000,
14...     noise_level=0.05,
15...     noise_type="field",
16...     include_phase=True,
17...     seed=42,
18...     n_jobs=1,
19...     output_dir="runs/forward",
20...     output_name="mt1d_training",
21... )
22
23>>> cfg.validate()
24>>> print(cfg.summary())
25ForwardConfig
26  solver               = 'mt1d'
27  freq_min             = 0.001 Hz
28  freq_max             = 1e+04 Hz
29  n_freqs              = 40
30  n_layers             = 3–7
31  rho_min              = 1 Ω·m
32  rho_max              = 1e+04 Ω·m
33  depth_max            = 3e+03 m
34  n_samples            = 5,000
35  noise_level          = 0.05  (field)
36  seed                 = 42
37  n_jobs               = 1
38  output               = runs/forward/mt1d_training.npz
39>>> dataset = generate_dataset(**cfg.to_dataset_kwargs())

to_dataset_kwargs() builds the correct frequency or time array and constructs the output path. In this example the resolved frequency grid starts at 0.001 Hz and ends at 10000 Hz, with intermediate samples spaced evenly in \(\log_{10}(f)\). This reduces the risk of the config and the actual dataset generation call drifting apart.

16.3.4. Geological Priors#

ForwardConfig.geology can replace broad rho_min/rho_max and depth_max sampling with a named geological prior. This is useful when an AI dataset should represent a known target class instead of arbitrary layered-earth variation.

Common prior names include:

1sedimentary
2crystalline
3geothermal
4marine
5permafrost
 1>>> from pycsamt.forward import ForwardConfig, generate_dataset
 2
 3>>> cfg = ForwardConfig(
 4...     solver="mt1d",
 5...     geology="geothermal",
 6...     n_layers_min=4,
 7...     n_layers_max=7,
 8...     n_samples=20_000,
 9...     noise_type="field",
10...     seed=12,
11...     output_dir="runs/forward",
12...     output_name="geothermal_mt1d",
13... )
14
15>>> cfg.validate()
16>>> print(cfg.summary())
17ForwardConfig
18  solver               = 'mt1d'
19  freq_min             = 0.0001 Hz
20  freq_max             = 1e+04 Hz
21  n_freqs              = 30
22  n_layers             = 4–7
23  geology              = 'geothermal'
24  n_samples            = 20,000
25  noise_level          = 0.05  (field)
26  seed                 = 12
27  n_jobs               = 1
28  output               = runs/forward/geothermal_mt1d.npz
29>>> dataset = generate_dataset(**cfg.to_dataset_kwargs())

When geology is set, the geological prior should be documented in the project notes. A neural network trained on a narrow prior may not generalize outside that geological setting.

16.3.5. TEM Configuration#

TEM uses time gates rather than frequency samples. The loop radius \(a\) enters the transmitter geometry, while the configured gates \(t_i\) control where the decay curve is sampled after current shutoff. Because the response changes rapidly at early time and slowly at late time, logarithmic spacing gives useful resolution across several decades without requiring a dense linear grid.

 1>>> from pycsamt.forward import ForwardConfig, generate_dataset
 2
 3>>> cfg = ForwardConfig(
 4...     solver="tem1d",
 5...     time_min=1e-6,
 6...     time_max=1e-2,
 7...     n_times=25,
 8...     loop_radius=50.0,
 9...     n_layers_min=3,
10...     n_layers_max=6,
11...     n_samples=1000,
12...     noise_type="gaussian",
13...     noise_level=0.03,
14...     seed=5,
15...     output_dir="runs/forward",
16...     output_name="tem1d_training",
17... )
18
19>>> cfg.validate()
20>>> print(cfg.summary())
21ForwardConfig
22  solver               = 'tem1d'
23  time_min             = 1e-06 s
24  time_max             = 0.01 s
25  n_times              = 25
26  loop_radius          = 50.0 m
27  n_layers             = 3–6
28  rho_min              = 1 Ω·m
29  rho_max              = 1e+04 Ω·m
30  depth_max            = 2e+03 m
31  n_samples            = 1,000
32  noise_level          = 0.03  (gaussian)
33  seed                 = 5
34  n_jobs               = 1
35  output               = runs/forward/tem1d_training.npz
36>>> dataset = generate_dataset(**cfg.to_dataset_kwargs())

TEM generation can be slower than MT1D because the current implementation uses numerical integration for the step-off response. Start with a small n_samples value, inspect the response, then scale up.

16.3.6. 2-D Configuration#

ForwardConfig2D creates a pycsamt.forward.Grid2D and solver keyword arguments for pycsamt.forward.MT2DForward. The finite-difference grid stores resistivity as a cell model \(\rho(x,z)\). For the rectangular anomaly case, the background resistivity \(\rho_b\) is used everywhere except inside the configured box, where \(\rho(x,z)=\rho_a\) for \(x_\mathrm{lo}\le x\le x_\mathrm{hi}\) and \(z_\mathrm{lo}\le z\le z_\mathrm{hi}\). Padding extends the numerical domain beyond the core survey area so boundary conditions are less visible in the predicted response.

Setting group

Fields

Solver

freq_min, freq_max, n_freqs.

Grid

nx, nz, x_max, z_max, n_pad, pad_factor.

Earth model

bg_rho, model_type, anomaly_rho, anomaly_x_lo, anomaly_x_hi, anomaly_z_lo, anomaly_z_hi.

Stations

n_stations, station_x_max.

Output

verbose.

The supported model_type values are:

model_type

Meaning

"halfspace"

Uniform background resistivity.

"anomaly"

Rectangular anomaly inside a background.

"random"

Random 2-D grid model generated by Grid2D.random.

Example 2-D anomaly run:

 1>>> from pycsamt.forward import ForwardConfig2D, MT2DForward
 2
 3>>> cfg = ForwardConfig2D(
 4...     freq_min=1e-2,
 5...     freq_max=1e3,
 6...     n_freqs=25,
 7...     nx=50,
 8...     nz=35,
 9...     x_max=10_000.0,
10...     z_max=6000.0,
11...     n_pad=8,
12...     pad_factor=1.3,
13...     bg_rho=300.0,
14...     model_type="anomaly",
15...     anomaly_rho=10.0,
16...     anomaly_x_lo=2500.0,
17...     anomaly_x_hi=6500.0,
18...     anomaly_z_lo=400.0,
19...     anomaly_z_hi=1800.0,
20...     n_stations=16,
21...     verbose=False,
22... )
23
24>>> cfg.validate()
25>>> print(cfg.summary())
26ForwardConfig2D
27  model_type             = 'anomaly'
28  bg_rho                 = 300.0 Ω·m
29  freq range             = 0.01–1e+03 Hz  (25 pts)
30  grid (nx × nz)         = 50 × 35  (core, +8 pad)
31  x_max                  = 10000 m
32  z_max                  = 6000 m
33  n_stations             = 16
34  anomaly                = 10.0 Ω·m  x=[2500.0,6500.0]  z=[400.0,1800.0]
35
36>>> grid = cfg.to_grid()
37>>> solver = MT2DForward(grid=grid, **cfg.to_solver_kwargs())
38>>> response = solver.run()
39>>> features = response.to_feature_array(mode="both")
40>>> features.shape
41(16, 100)

Sixteen stations times the twenty-five sampled frequencies times both modes’ apparent resistivity and phase gives the hundred columns above – the same to_feature_array pattern used throughout Forward Modelling Concepts, now driven entirely by the config rather than by hand-built keyword arguments. Set verbose=True on ForwardConfig2D during long exploratory runs; it streams a per-frequency progress line to stdout, which is useful interactively but not worth capturing in a static transcript.

to_grid() accepts a seed argument for random models:

1>>> cfg = ForwardConfig2D(model_type="random")
2>>> cfg.validate()
3>>> grid = cfg.to_grid(seed=42)
4>>> grid.resistivity.shape
5(38, 56)

The requested nx=40, nz=30 core is padded on every side that is not the free surface, so the stored array is larger than the core grid: eight padding cells on each side of nx (40 + 2*8 = 56) and eight beneath nz (30 + 8 = 38), with no padding added above z=0.

16.3.7. 2-D Grid Tuning#

2-D forward runs are sensitive to grid design.

Parameter

Practical guidance

x_max

Should include all stations and enough side room that boundaries do not dominate the response.

z_max

Should extend below the expected maximum investigation depth.

nx and nz

Increase when anomalies are small or response gradients are sharp.

n_pad

Add side and bottom padding to reduce boundary artifacts.

pad_factor

Values around 1.2 to 1.5 are typical; 1.3 is a stable starting point.

n_stations

Should resolve the lateral scale of the target.

16.3.8. 3-D Configuration#

ForwardConfig3D builds a pycsamt.forward.Grid3D and keyword arguments for pycsamt.forward.MT3DForward. The model is written as \(\rho(x,y,z)\), with station samples arranged over the horizontal \(x\) and \(y\) axes. In the documented quasi-3-D path, each requested frequency still follows the same log grid as the 2-D case, while the solver uses a practical approximation to produce survey-scale synthetic responses over a 3-D resistivity volume.

Setting group

Fields

Solver

freq_min, freq_max, n_freqs, method.

Grid

nx, ny, nz, x_max, y_max, z_max, n_pad, pad_factor.

Earth model

bg_rho, model_type, anomaly bounds, n_layers, lateral_variation, corr_length.

Stations

nx_stations, ny_stations.

Output

verbose.

The supported model_type values are:

model_type

Meaning

"halfspace"

Uniform 3-D background.

"block_anomaly"

Rectangular 3-D anomaly inside a background.

"random_layered"

Random horizontal layers with optional Gaussian-random-field lateral variation.

Example quasi-3-D block anomaly:

 1>>> from pycsamt.forward import ForwardConfig3D, MT3DForward
 2
 3>>> cfg = ForwardConfig3D(
 4...     freq_min=1e-2,
 5...     freq_max=1e3,
 6...     n_freqs=15,
 7...     method="quasi3d",
 8...     nx=24,
 9...     ny=24,
10...     nz=18,
11...     x_max=9000.0,
12...     y_max=9000.0,
13...     z_max=5000.0,
14...     n_pad=8,
15...     bg_rho=500.0,
16...     model_type="block_anomaly",
17...     anomaly_rho=20.0,
18...     anomaly_x_lo=2500.0,
19...     anomaly_x_hi=6500.0,
20...     anomaly_y_lo=2500.0,
21...     anomaly_y_hi=6500.0,
22...     anomaly_z_lo=500.0,
23...     anomaly_z_hi=2000.0,
24...     nx_stations=6,
25...     ny_stations=6,
26...     verbose=False,
27... )
28
29>>> cfg.validate()
30>>> print(cfg.summary())
31ForwardConfig3D
32  method                   = 'quasi3d'
33  model_type               = 'block_anomaly'  (bg=500.0 Ω·m)
34  freq range               = 0.01–1e+03 Hz  (15 pts)
35  grid (nx×ny×nz)          = 24×24×18  (core, +8 pad)
36  extents (x,y,z)          = 9000 m × 9000 m × 5000 m
37  stations                 = 6×6 = 36 total
38  anomaly                  = 20.0 Ω·m  x=[2500.0,6500.0]  y=[2500.0,6500.0]  z=[500.0,2000.0]
39
40>>> grid = cfg.to_grid()
41>>> response = MT3DForward(grid=grid, **cfg.to_solver_kwargs()).run()
42>>> x = response.to_feature_array(components="xy_yx")
43>>> x.shape
44(36, 60)

Thirty-six stations on the 6x6 layout, times the fifteen sampled frequencies, times the xy/yx components’ apparent resistivity and phase, gives the sixty columns above – config-driven quasi-3-D behaves exactly like the hand-built pycsamt.forward.Grid3D example in Forward Modelling Concepts, which is the point of routing both through the same to_feature_array convention.

Random layered 3-D model:

 1>>> cfg = ForwardConfig3D(
 2...     model_type="random_layered",
 3...     n_layers=5,
 4...     lateral_variation=True,
 5...     corr_length=2500.0,
 6...     nx_stations=7,
 7...     ny_stations=7,
 8... )
 9
10>>> cfg.validate()
11>>> grid = cfg.to_grid(seed=42)
12>>> grid.resistivity.shape
13(23, 36, 36)

The default 20x20x15 core grid picks up the same eight-cell padding convention as the 2-D case, on every side except the free surface: nx = ny = 20 + 2*8 = 36 and nz = 15 + 8 = 23. pycsamt.forward.Grid3D stores resistivity as (nz, ny, nx), which is why the padded shape reads (23, 36, 36) rather than (36, 36, 23).

16.3.9. 3-D Configuration Notes#

ForwardConfig3D.method currently validates to "quasi3d". The underlying solver code contains experimental hooks for fuller 3-D methods, but the documented configuration path is quasi-3-D. Treat quasi-3-D outputs as survey-scale synthetic responses, not as final production 3-D inversion results.

For production 3-D modelling or inversion, continue to:

16.3.10. Validation#

Call validate() before any expensive run. Validation catches basic range errors such as negative frequencies, invalid model types, impossible anomaly bounds, and invalid station counts.

1>>> from pycsamt.forward import ForwardConfig2D
2
3>>> cfg = ForwardConfig2D(freq_min=100.0, freq_max=10.0)
4
5>>> try:
6...     cfg.validate()
7... except ValueError as exc:
8...     print(f"Configuration problem: {exc}")
9Configuration problem: freq_min must be > 0 and freq_max > freq_min.

Validation does not prove that the model is geologically meaningful. It only checks that the parameters are internally acceptable for the builder and solver.

16.3.11. Summaries And Provenance#

Each config object provides summary() and repr output that can be saved in logs or reports.

 1>>> from pathlib import Path
 2>>> from pycsamt.forward import ForwardConfig2D
 3
 4>>> cfg = ForwardConfig2D(model_type="anomaly")
 5>>> cfg.validate()
 6
 7>>> run_dir = Path("runs/forward/2d_anomaly")
 8>>> run_dir.mkdir(parents=True, exist_ok=True)
 9>>> (run_dir / "summary.txt").write_text(cfg.summary(), encoding="utf-8")
10363
11>>> cfg.to_template(run_dir / "forward_config_2d.yml")

summary() contains the Ω·m resistivity unit, so pass encoding="utf-8" explicitly – on Windows, pathlib.Path otherwise defaults to the console code page and raises UnicodeEncodeError on that character.

16.3.12. What To Record#

For reproducibility, record:

  • config file path and format;

  • solver type and dimensionality;

  • frequency grid or time-gate grid;

  • model prior or grid constructor;

  • resistivity bounds, anomaly bounds, or geology prior;

  • station layout;

  • noise type and noise level;

  • random seed;

  • output dataset or figure paths;

  • pyCSAMT version or commit;

  • any manual edits made after grid creation.

16.3.14. Common Mistakes#

The generated dataset cannot be reproduced.

Record seed, configuration file, pyCSAMT version, and output path. Avoid manually changing keyword arguments outside the config.

The run validates but the response is not useful.

Validation only checks parameter ranges. Review frequency range, grid extents, anomaly size, station spacing, and plots.

TEM generation is unexpectedly slow.

Start with fewer samples or fewer time gates. TEM1D currently uses numerical integration for the step-off response.

The 2-D model has boundary artifacts.

Increase x_max, z_max, n_pad, or adjust pad_factor.

The 3-D response is treated as a production inversion result.

ForwardConfig3D documents quasi-3-D synthetic modelling. Use ModEM or MARE2DEM for production external-engine workflows.

16.3.15. Next Steps#