Plotting

maxwellbloch.plot provides Plotly-based visualisation primitives for MBSolve and OBSolve results. Figures are interactive in the browser — hover to read values, drag to zoom, click the legend to toggle traces.

Installation

The plotting module requires the optional [plot] dependencies:

pip install maxwellbloch[plot]

or with uv:

uv pip install maxwellbloch[plot]

kaleido is included in [plot] and is required for static PNG export (fig.write_image()). If you only need interactive figures in a notebook you can install plotly alone.

Usage pattern

All plot functions take a solved MBSolve instance as their first argument and return a plotly.graph_objects.Figure. They never call .show() internally — that is always your call:

from maxwellbloch import plot

fig = plot.field_spacetime(mbs)         # returns a Figure
fig.show(renderer='notebook_connected') # interactive in Jupyter
fig.write_image('output.png')           # static PNG via kaleido

Setup: solve a two-level absorber

We use a weak Gaussian probe propagating through a two-level absorber. This is enough to demonstrate every primitive.

[1]:
from maxwellbloch import mb_solve, plot

mb_solve_json = """
{
  "atom": {
    "num_states": 2,
    "decays": [{"channels": [[0, 1]], "rate": 1.0}],
    "fields": [
      {
        "label": "probe",
        "coupled_levels": [[0, 1]],
        "rabi_freq": 0.01,
        "rabi_freq_t_func": "gaussian",
        "rabi_freq_t_args": {"ampl": 1.0, "centre": 0.0, "fwhm": 1.0}
      }
    ]
  },
  "t_min": -3.0,
  "t_max": 6.0,
  "t_steps": 120,
  "z_min": 0.0,
  "z_max": 1.0,
  "z_steps": 20,
  "z_steps_inner": 2,
  "interaction_strengths": [2.0],
  "savefile": "plotting-two-level"
}
"""

mbs = mb_solve.MBSolve.from_json_str(mb_solve_json)
Omegas_zt, states_zt = mbs.mbsolve()
100%|██████████| 20/20 [00:00<00:00, 64.33z/s, Ω_max=0.0155]
Saving MBSolve to plotting-two-level.qu

field_spacetime — space-time heatmap of |Ω(z, t)|

The most common view: the full propagation in one figure. Hover over any pixel to read the exact (z, t, |Ω|) values.

[2]:
fig = plot.field_spacetime(mbs)
fig.show(renderer='notebook_connected')

field_envelope — pulse shape at fixed z

Compare the input pulse (z = z_min) to the output pulse (z = z_max). The probe is absorbed as it propagates through the medium.

[3]:
fig = plot.field_envelope(mbs, z_indices=[0, -1])
fig.show(renderer='notebook_connected')

field_z_profile_anim — animated spatial profile

Watch the pulse propagate through the medium. Each frame shows the field envelope \(|\Omega(z)|\) at a single instant. Use the slider to scrub to a specific time, or press ▶ Play to animate.

[4]:
fig = plot.field_z_profile_anim(mbs)
fig.show(renderer='notebook_connected')

pulse_area — area theorem

The pulse area \(\mathcal{A} = \int |\Omega(z,t)|\, dt / \pi\) decreases from the input value as the probe is absorbed.

[5]:
fig = plot.pulse_area(mbs)
fig.show(renderer='notebook_connected')

spectrum — frequency-domain absorption

Computes the absorption spectrum via Fourier transform of the time-domain field at z_idx (default: exit face). This works in the linear (weak-field) regime; for strong fields the result is the nonlinear transmission, not the susceptibility.

Key options:

  • freq_range — clip the displayed frequency axis to |f| freq_range (γ), hiding the noisy high-frequency wings.

  • show_dispersion=True — add the dispersive component on a secondary axis.

  • freq_scale="arcsinh" — compress the frequency axis nonlinearly (linear near zero, log-like at large |f|) so narrow and broad features coexist. arcsinh_scale sets the width of the linear region in γ.

  • window — apply a SciPy window (e.g. "hann") to the time-domain field before the FFT to reduce spectral leakage from truncated free-induction decay.

[6]:
# Narrow window around the resonance with dispersion on secondary axis
fig = plot.spectrum(mbs, freq_range=3.0, show_dispersion=True)
fig.show(renderer='notebook_connected')
[7]:
# Arcsinh frequency scale (linear ±1 γ, log-like beyond) with Hann windowing
# to reduce leakage from the free-induction decay tail
fig = plot.spectrum(mbs, freq_scale="arcsinh", arcsinh_scale=1.0, window="hann")
fig.show(renderer='notebook_connected')

population — state populations vs time

The diagonal elements \(\rho_{ii}(t)\) of the density matrix at a fixed z position. The probe drives population from |0⟩ to |1⟩ and back.

[8]:
fig = plot.population(mbs, z_idx=0)
fig.show(renderer='notebook_connected')

population_spacetime — population heatmap

Like field_spacetime but for a density-matrix diagonal element. Useful for visualising population inversion, storage, and retrieval.

[9]:
fig = plot.population_spacetime(mbs, state_idx=1)
fig.show(renderer='notebook_connected')

coherence — off-diagonal density matrix elements

Plot \(|\rho_{01}(t)|\), \(\mathrm{Re}(\rho_{01})\), or \(\mathrm{Im}(\rho_{01})\) at a fixed z. The coherence is proportional to the macroscopic polarisation of the medium and drives the field evolution.

[10]:
fig = plot.coherence(mbs, 0, 1, z_idx=0, component='abs')
fig.show(renderer='notebook_connected')

Static PNG export

Any figure can be saved as a static PNG using fig.write_image(). This requires kaleido (included in maxwellbloch[plot]).

fig = plot.field_spacetime(mbs)
fig.write_image('field_spacetime.png', scale=2)  # scale=2 for high-DPI

SVG and PDF are also supported:

fig.write_image('field_spacetime.svg')
fig.write_image('field_spacetime.pdf')