Forward Plotting#

Forward plots are quality-control tools. They help you see whether a model, grid, response, or 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 Figure or an array of axes. This makes it easy to customize labels, save files, or place plots inside your own dashboards and notebooks.

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.

Saving Figures#

Because the plotting functions return Matplotlib objects, save figures using standard Matplotlib methods.

1import matplotlib.pyplot as plt
2
3axes = plot_response_1d(response)
4fig = axes[0].get_figure()
5fig.savefig("response_1d.png", dpi=200, bbox_inches="tight")
6
7plt.close(fig)

For composite functions that return a figure directly:

1fig = plot_response_and_model_1d(response, model=model)
2fig.savefig("model_and_response.png", dpi=200, bbox_inches="tight")

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].

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

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:

1import numpy as np
2
3from pycsamt.forward import LayeredModel, TEM1DForward
4
5model = LayeredModel([60.0, 250.0, 900.0], [120.0, 700.0])
6response = TEM1DForward(np.logspace(-6, -3, 25)).run(model)
7
8ax = response.plot()

1-D Model Plots#

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

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

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.

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.

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

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

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.

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

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.

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.

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

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.

2-D Lateral Response Profiles#

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

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

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.

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].

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

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.

3-D Response Maps#

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

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

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

3-D Response Sections#

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

1from pycsamt.forward import plot_response_section_3d
2
3ax = plot_response_section_3d(
4    response3d,
5    component="xy",
6    quantity="rho_a",
7    y_row=None,
8    n_contours=5,
9)

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

3-D Tensor Component Panels#

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

1from pycsamt.forward import plot_tensor_components_3d
2
3axes = plot_tensor_components_3d(
4    response3d,
5    freq_idx=0,
6    quantity="rho_a",
7    title="Tensor component comparison",
8)

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.

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.

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

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.

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 simple feature plot can still catch many issues.

 1import matplotlib.pyplot as plt
 2import numpy as np
 3
 4def plot_dataset_sample(dataset, index=0):
 5    fig, ax = plt.subplots(figsize=(7, 3), constrained_layout=True)
 6    ax.plot(np.asarray(dataset.X[index]).ravel(), lw=1.2)
 7    ax.set_xlabel("Feature index")
 8    ax.set_ylabel("Feature value")
 9    ax.set_title(f"{dataset.solver} sample {index}")
10    return ax
11
12ax = plot_dataset_sample(dataset, index=0)

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.

 1def 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

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.

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.

Next Pages#