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:
pycsamt.forward.ForwardConfigfor 1-D MT, CSAMT, and TEM dataset generation;pycsamt.forward.ForwardConfig2Dfor 2-D MT finite-difference runs;pycsamt.forward.ForwardConfig3Dfor quasi-3-D MT forward runs.
All three classes follow the same workflow:
write an annotated template;
edit the template as the project source of truth;
load the template back into Python;
validate before computation;
build arrays, grids, solvers, or dataset keyword arguments from the config;
archive the config next to datasets, figures, and interpretation notes.
16.3.1. Configuration Classes#
Class |
Main purpose |
Main helpers |
|---|---|---|
|
1-D MT/CSAMT/TEM synthetic dataset generation. |
|
|
2-D MT finite-difference grid and solver setup. |
|
|
Quasi-3-D MT grid and solver setup. |
|
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 |
|
Earth model prior |
|
Dataset |
|
Output |
|
The solver controls which sampled axis is active:
|
Active axis |
Main settings |
|---|---|---|
|
Frequency |
|
|
Frequency |
|
|
Time gates |
|
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 |
|
Grid |
|
Earth model |
|
Stations |
|
Output |
|
The supported model_type values are:
|
Meaning |
|---|---|
|
Uniform background resistivity. |
|
Rectangular anomaly inside a background. |
|
Random 2-D grid model generated by |
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 |
|---|---|
|
Should include all stations and enough side room that boundaries do not dominate the response. |
|
Should extend below the expected maximum investigation depth. |
|
Increase when anomalies are small or response gradients are sharp. |
|
Add side and bottom padding to reduce boundary artifacts. |
|
Values around |
|
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 |
|
Grid |
|
Earth model |
|
Stations |
|
Output |
|
The supported model_type values are:
|
Meaning |
|---|---|
|
Uniform 3-D background. |
|
Rectangular 3-D anomaly inside a background. |
|
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.13. Recommended Run Layout#
1runs/
2 forward/
3 mt1d_training/
4 forward_config.yml
5 summary.txt
6 mt1d_training.npz
7 sample_responses.png
8 profile_2d_anomaly/
9 forward_config_2d.yml
10 summary.txt
11 model.png
12 pseudosection_te.png
13 survey_quasi3d/
14 forward_config_3d.yml
15 summary.txt
16 response_maps/
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 adjustpad_factor.The 3-D response is treated as a production inversion result.ForwardConfig3Ddocuments quasi-3-D synthetic modelling. Use ModEM or MARE2DEM for production external-engine workflows.
16.3.15. Next Steps#
Solvers And Grids explains how configs create models and solvers.
Synthetic Datasets And Noise explains dataset generation and train/validation splits.
Forward Plotting shows how to inspect configured runs.
From Forward Modelling To Inversion explains synthetic recovery workflows.