16.13. Forward Plotting#

Forward plots are quality control tools. They help you see whether a model, grid, forward response, or synthetic dataset looks physically plausible before it is used for inversion, AI training, reports, or tutorials.

In pyCSAMT, plotting functions are deliberately lightweight wrappers around Matplotlib. They do not run the solver. They inspect objects that already exist:

Most plotting functions return Matplotlib Axes objects, while composite figures return a Matplotlib figure or an array of axes. This makes it easy to customize labels, save files, or place plots inside your own dashboards and notebooks. The helpers deliberately keep plotting separate from modelling: the solver produces the arrays, and the plotting layer only makes those arrays auditable.

16.13.1. Plot Selection Guide#

Use this table to choose the right function.

Function

Input

Use

plot_response_1d

ForwardResponse

Apparent resistivity and phase curves for MT/CSAMT responses.

plot_model_1d

LayeredModel or list of models

Layered resistivity-depth profiles.

plot_response_and_model_1d

ForwardResponse plus optional LayeredModel

One-page validation view for a 1-D forward run.

plot_model_2d

Grid2D

2-D resistivity model with optional station markers.

plot_pseudosection_2d

ForwardResponse2D

Period-by-station pseudo-section for TE or TM response quantities.

plot_response_profiles

ForwardResponse2D

Lateral profiles at selected frequencies.

plot_model_3d

Grid3D

Orthogonal XZ, YZ, and XY slices through a 3-D volume.

plot_response_map_3d

ForwardResponse3D

Map view of one tensor component at one frequency.

plot_response_section_3d

ForwardResponse3D

Pseudo-section through one y-row of a 3-D station layout.

plot_tensor_components_3d

ForwardResponse3D

Four-panel tensor component map.

The plotting helpers use project style controls from pycsamt.api.style and axis controls from pycsamt.api.control. For example, apparent resistivity is displayed in a log-style geophysical convention, and station markers use the shared station rendering presets.

16.13.2. Saving Figures#

Because the plotting functions return Matplotlib objects, save figures using standard Matplotlib methods. Any response works for this pattern; a quick halfspace keeps the point focused on saving, not on the model:

 1>>> import numpy as np
 2>>> import matplotlib.pyplot as plt
 3
 4>>> from pycsamt.forward import LayeredModel, MT1DForward, plot_response_1d
 5
 6>>> model = LayeredModel([100.0], [])
 7>>> response = MT1DForward(np.logspace(-3, 4, 10)).run(model)
 8
 9>>> axes = plot_response_1d(response)
10>>> fig = axes[0].get_figure()
11>>> fig.savefig("response_1d.png", dpi=200, bbox_inches="tight")
12
13>>> plt.close(fig)

For composite functions that return a figure directly:

1>>> from pycsamt.forward import plot_response_and_model_1d
2
3>>> fig = plot_response_and_model_1d(response, model=model)
4>>> fig.savefig("model_and_response.png", dpi=200, bbox_inches="tight")
5>>> plt.close(fig)

16.13.3. 1-D Response Plots#

Use plot_response_1d for MT and CSAMT apparent resistivity and phase curves. It returns two axes: [ax_rho, ax_phase].

 1>>> import numpy as np
 2
 3>>> from pycsamt.forward import LayeredModel, MT1DForward, plot_response_1d
 4
 5>>> model = LayeredModel(
 6...     resistivity=[100.0, 10.0, 500.0],
 7...     thickness=[300.0, 800.0],
 8...     name="conductive_middle_layer",
 9... )
10
11>>> response = MT1DForward(freqs=np.logspace(-3, 4, 40)).run(model)
12
13>>> axes = plot_response_1d(
14...     response,
15...     modes="both",
16...     title="MT1D response",
17... )
MT1D apparent resistivity and phase response.

The two returned axes show apparent resistivity and phase against period. For the conductive middle layer, the resistivity curve drops over the period band that is most sensitive to the layer.#

The example produces two axes and a 40-sample response:

>>> len(axes), response.rho_a.shape, response.phase.shape
(2, (40,), (40,))

Read this plot as a quick physical sanity check:

  • a simple halfspace should produce a smooth response;

  • a conductive layer should lower apparent resistivity over its sensitivity band;

  • phase should remain finite and plausible;

  • sharp noise or oscillation in a clean synthetic response usually signals an input or solver setup problem.

plot_response_1d expects frequency-domain responses. TDEM responses can be quickly inspected with the response object’s own plot method:

 1>>> import numpy as np
 2
 3>>> from pycsamt.forward import LayeredModel, TEM1DForward
 4
 5>>> model = LayeredModel([60.0, 250.0, 900.0], [120.0, 700.0])
 6>>> response = TEM1DForward(np.logspace(-6, -3, 25)).run(model)
 7
 8>>> ax = response.plot()
 9>>> print(response.times.shape, response.dBz_dt.shape)
10(25,) (25,)
TEM1D step-off dBz/dt response on log-log axes.

response.plot() is a quick diagnostic, not a publication figure – it picks its axes and labels from response.method automatically, which is why no mode or quantity argument is needed the way the 2-D and 3-D plotting functions require one.#

A TEM response uses time gates instead of frequency samples, and response.plot() reads them off the same object rather than needing a separate axis argument.

16.13.4. 1-D Model Plots#

Use plot_model_1d to inspect one or more layered models. It returns one axis.

 1>>> from pycsamt.forward import LayeredModel, plot_model_1d
 2
 3>>> truth = LayeredModel([80.0, 25.0, 600.0], [250.0, 900.0], name="truth")
 4>>> start = LayeredModel([100.0, 50.0, 500.0], [300.0, 1000.0], name="start")
 5
 6>>> ax = plot_model_1d(
 7...     [truth, start],
 8...     labels=["truth", "starting model"],
 9...     depth_max=2000.0,
10...     title="Layered models",
11... )
Truth and starting layered-earth resistivity models.

Overlaying the truth and starting model makes the prior information visible before running a synthetic recovery test.#

This is especially useful when preparing a synthetic recovery test. Plot the truth model and the starting model together so it is clear how much prior information the inversion receives.

16.13.5. 1-D Composite View#

plot_response_and_model_1d creates the canonical validation figure for a single 1-D forward run. It returns a Matplotlib Figure.

 1>>> import numpy as np
 2
 3>>> from pycsamt.forward import (
 4...     LayeredModel,
 5...     MT1DForward,
 6...     plot_response_and_model_1d,
 7... )
 8
 9>>> model = LayeredModel([100.0, 20.0, 800.0], [300.0, 1000.0])
10>>> response = MT1DForward(np.logspace(-3, 4, 40)).run(model)
11
12>>> fig = plot_response_and_model_1d(
13...     response,
14...     model=model,
15...     title="Forward validation",
16... )
Composite 1-D earth model, apparent resistivity, and phase figure.

The composite view keeps the model and response together, which is useful when reviewing whether a curve feature is consistent with a layer boundary.#

If model is omitted, the function returns a two-panel response-only figure.

16.13.6. 2-D Grid Model Plots#

Use plot_model_2d to display a pycsamt.forward.Grid2D. By default, it clips padding cells and displays the core model region.

 1>>> from pycsamt.forward import Grid2D, plot_model_2d
 2
 3>>> grid = 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
15>>> ax = plot_model_2d(
16...     grid,
17...     clip_core=True,
18...     show_stations=True,
19...     station_preset="inversion",
20...     title="2-D anomaly model",
21... )
Two-dimensional resistivity grid with a conductive anomaly and stations.

The core model is shown without padding cells. Station markers provide a quick check that the station layout samples the anomaly.#

Important options:

log_scale

When True, the colour scale is \(\log_{10}\rho\). This is usually best for resistivity models.

clip_core

When True, padding cells are hidden. Use clip_core=False when debugging whether padding is large enough.

show_stations

When True, stations are drawn along the surface. If stations appear outside the core model, revisit grid construction.

16.13.7. 2-D Pseudo-Sections#

Use plot_pseudosection_2d to show a period-by-station view of a 2-D MT response. It works on pycsamt.forward.ForwardResponse2D.

 1>>> from pycsamt.forward import MT2DForward, plot_pseudosection_2d
 2
 3>>> response = MT2DForward(
 4...     freqs=[1.0, 10.0, 100.0],
 5...     grid=grid,
 6...     verbose=False,
 7... ).run()
 8
 9>>> ax = plot_pseudosection_2d(
10...     response,
11...     mode="te",
12...     quantity="rho_a",
13...     n_contours=6,
14...     title="TE apparent resistivity",
15... )
TE apparent resistivity pseudo-section from a 2-D forward response.

The pseudo-section arranges response values by station and period. It is a display of the forward response, not a recovered depth model.#

Valid mode values are "te" and "tm". Valid quantity values are "rho_a" and "phase". Apparent resistivity uses a jet_r style colour map by default, while phase uses RdBu_r.

Pseudo-sections are not subsurface images. They display response variations as a function of period and station distance. Use them to see whether a target creates a coherent response pattern, not to interpret target geometry directly.

16.13.8. 2-D Lateral Response Profiles#

Use plot_response_profiles to inspect how the response varies along the profile at selected frequencies.

1>>> from pycsamt.forward import plot_response_profiles
2
3>>> ax = plot_response_profiles(
4...     response,
5...     mode="te",
6...     quantity="rho_a",
7...     freq_indices=[0, 1, 2],
8...     title="Lateral response profiles",
9... )
Lateral apparent-resistivity profiles at selected frequencies.

Frequency slices make lateral shifts and broadening easier to see than in a filled pseudo-section alone.#

If freq_indices is omitted, the function chooses a small number of approximately evenly spaced frequencies. This plot is useful for detecting whether an anomaly response is localized, broad, shifted, or absent.

16.13.9. 3-D Model Slice Plots#

Use plot_model_3d to inspect a pycsamt.forward.Grid3D. It returns three axes: [ax_xz, ax_yz, ax_xy].

 1>>> from pycsamt.forward import Grid3D, plot_model_3d
 2
 3>>> grid3d = 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
18>>> axes = plot_model_3d(
19...     grid3d,
20...     clip_core=True,
21...     show_stations=True,
22...     title="3-D block anomaly",
23... )
Orthogonal slices through a 3-D block-anomaly resistivity model.

Orthogonal slices expose the same resistivity volume from three directions, so misplaced bounds are usually visible before a solver run.#

The three panels show:

  • XZ slice through the middle y position;

  • YZ slice through the middle x position;

  • XY slice through the middle z position.

Station positions are overlaid on the XY panel.

16.13.10. 3-D Response Maps#

Use plot_response_map_3d to display one response component at one frequency as a map-view station scatter plot.

 1>>> from pycsamt.forward import MT3DForward, plot_response_map_3d
 2
 3>>> response3d = MT3DForward(
 4...     freqs=[1.0, 10.0, 100.0],
 5...     grid=grid3d,
 6...     verbose=False,
 7... ).run()
 8
 9>>> ax = plot_response_map_3d(
10...     response3d,
11...     freq_idx=0,
12...     component="xy",
13...     quantity="rho_a",
14...     show_labels=True,
15...     title="Zxy apparent resistivity map",
16... )
Map-view Zxy apparent resistivity response over a 3-D station grid.

A response map shows one tensor component at one frequency across the station grid.#

Valid components are "xy", "yx", "xx", and "yy". Valid quantities are "rho_a" and "phase".

16.13.11. 3-D Response Sections#

Use plot_response_section_3d for a period-by-station pseudo-section through one y-row of the station layout.

1>>> from pycsamt.forward import plot_response_section_3d
2
3>>> ax = plot_response_section_3d(
4...     response3d,
5...     component="xy",
6...     quantity="rho_a",
7...     y_row=None,
8...     n_contours=5,
9... )
Period-by-station 3-D response section through the middle y-row.

With y_row=None, the section is extracted through the middle row of the station grid.#

When y_row=None, the middle y-row is selected. Use an explicit row index to inspect different profile lines through the station grid.

16.13.12. 3-D Tensor Component Panels#

Use plot_tensor_components_3d to compare all four tensor components at one frequency.

1>>> from pycsamt.forward import plot_tensor_components_3d
2
3>>> axes = plot_tensor_components_3d(
4...     response3d,
5...     freq_idx=0,
6...     quantity="rho_a",
7...     title="Tensor component comparison",
8... )
Four tensor-component apparent-resistivity maps from a 3-D response.

The four-panel layout keeps diagonal and off-diagonal components comparable at the same frequency and colour scale.#

The panels are arranged as:

1Zxx  Zxy
2Zyx  Zyy

For quasi-3-D responses, interpret diagonal components with care. The quasi-3-D solver is useful for survey design and synthetic AI examples, but it is not a substitute for a production full-3-D modelling engine.

16.13.13. Plotting Noisy Responses#

Always plot a few clean and noisy responses before training an AI model. This prevents accidental use of unrealistic noise levels or corrupted axes.

 1>>> import numpy as np
 2
 3>>> from pycsamt.forward import (
 4...     FieldRealisticNoise,
 5...     LayeredModel,
 6...     MT1DForward,
 7...     plot_response_1d,
 8... )
 9
10>>> model = LayeredModel([100.0, 20.0, 800.0], [300.0, 1000.0])
11>>> clean = MT1DForward(np.logspace(-3, 4, 40)).run(model)
12>>> noisy = FieldRealisticNoise(base_level=0.05).apply(clean, seed=10)
13
14>>> axes = plot_response_1d(clean, label_te="clean", color_te="0.2")
15>>> _ = plot_response_1d(
16...     noisy,
17...     label_te="noisy",
18...     color_te="firebrick",
19...     axes=axes,
20... )
Clean and noisy MT1D apparent resistivity and phase curves overlaid.

A noisy sample should perturb the clean curve without destroying the physically meaningful trend.#

When overlaying multiple responses, keep labels explicit. A noisy synthetic curve should still look like a possible field response. If the curve is dominated by spikes or negative-looking artefacts, reduce the noise level or review the noise model.

16.13.14. Plotting Dataset Samples#

ForwardDataset stores feature vectors, not full response objects. For dataset QA, plot feature vectors directly or regenerate selected examples from the original configuration. A small real dataset makes this concrete:

 1>>> import numpy as np
 2>>> import matplotlib.pyplot as plt
 3
 4>>> from pycsamt.forward import generate_dataset
 5
 6>>> dataset = generate_dataset(
 7...     solver="mt1d",
 8...     n_samples=6,
 9...     freqs=np.logspace(-3, 4, 40),
10...     n_layers=(3, 7),
11...     rho_range=(1.0, 10000.0),
12...     depth_max=3000.0,
13...     noise_level=0.05,
14...     noise_type="field",
15...     include_phase=True,
16...     seed=42,
17...     verbose=False,
18... )
19>>> print(dataset)
20ForwardDataset(n=6, n_features=80, n_params=13, solver='mt1d')

A simple feature plot can still catch many issues:

 1>>> def plot_dataset_sample(dataset, index=0):
 2...     fig, ax = plt.subplots(figsize=(7, 3), constrained_layout=True)
 3...     ax.plot(np.asarray(dataset.X[index]).ravel(), lw=1.2)
 4...     ax.set_xlabel("Feature index")
 5...     ax.set_ylabel("Feature value")
 6...     ax.set_title(f"{dataset.solver} sample {index}")
 7...     return ax
 8...
 9
10>>> ax = plot_dataset_sample(dataset, index=0)
One synthetic dataset feature vector plotted by feature index.

A raw feature-vector plot is not geophysical interpretation, but it quickly exposes NaNs, clipping, scale jumps, or unexpected feature ordering.#

For MT/CSAMT datasets with phase included, the first half of the feature vector is log apparent resistivity and the second half is phase. Splitting the feature vector before plotting often makes the QA clearer.

 1>>> def plot_mt_feature_sample(dataset, index=0):
 2...     n_freqs = len(dataset.freqs)
 3...     x = dataset.X[index]
 4...     log_rho = x[:n_freqs]
 5...     phase = x[n_freqs:2 * n_freqs]
 6...
 7...     fig, axes = plt.subplots(2, 1, figsize=(7, 5), sharex=True)
 8...     period = 1.0 / dataset.freqs
 9...     axes[0].semilogx(period, log_rho)
10...     axes[0].set_ylabel("log10 rho_a")
11...     axes[1].semilogx(period, phase)
12...     axes[1].set_ylabel("phase")
13...     axes[1].set_xlabel("period (s)")
14...     return axes
15...
16
17>>> axes = plot_mt_feature_sample(dataset, index=0)
MT feature vector split into log apparent resistivity and phase panels.

Splitting the feature vector restores the physical axes and makes dataset QA easier to compare with ordinary MT response plots.#

16.13.15. Plotting Checklist#

Use plots to check:

  • simple halfspace responses are smooth;

  • apparent resistivity and phase use the expected axes and units;

  • conductive anomalies produce plausible low-resistivity response zones;

  • station positions align with the model;

  • padding cells are not dominating the displayed interpretation region;

  • noisy synthetic samples still look like possible field data;

  • TE/TM and tensor component labels are consistent;

  • pseudo-section patterns move sensibly with frequency or period;

  • dataset feature vectors match the model architecture expected by AI code.

16.13.16. Common Mistakes#

Interpreting pseudo-sections as geology

Pseudo-sections display response patterns. They are not inversion models.

Forgetting that 2-D and 3-D response arrays are frequency-first

Plotting functions use physical response arrays with shape (n_freqs, n_stations). Some machine-learning and inversion handoff arrays are station-first.

Plotting padded cells as interpretation

Padding is numerical buffer. Use clip_core=True for interpretation and clip_core=False only for grid debugging.

Comparing figures with different colour limits

Use common vmin and vmax when comparing models or responses.

Skipping noisy-sample plots

Noise settings that look reasonable numerically can still produce unrealistic synthetic data.

16.13.17. Next Pages#