You can interact with this notebook online: Launch notebook

Convergence Plots

These convergence plots help diagnose the quality of TARDIS simulation results:

The plasma plots show radiation temperature and dilution factor across velocity space, indicating when the radiation field reaches equilibrium throughout the ejecta. When lines stop changing between iterations, physical conditions have converged.

The luminosity plots track:

  • Inner boundary temperature stabilization

  • Whether emitted luminosity matches requested luminosity (energy conservation)

  • Residual luminosity percentage (ideally <5%)

Properly converged plots indicate your synthetic spectrum is based on a physically self-consistent model, making it reliable for comparison with observed supernova spectra. Poor convergence suggests you may need to adjust model parameters or extend the number of iterations.

The Convergence Plots consist of two Plotly FigureWidget Subplots, the plasma_plot and the t_inner_luminosities_plot. The plots can be displayed by setting the show_convergence_plots option in the run_tardis function to True. The plots are stored in the convergence_plots attribute of the simulation object sim and can be accessed using sim.convergence_plots.plasma_plot and sim.convergence_plots.t_inner_luminosities_plot.

Note

You only need to include export_convergence_plots=True in the run_tardis function when you want to share the notebook. The function shows the plot using the Plotly notebook_connected renderer, which helps display the plot online. You don’t need to do it when running the notebook locally.

[1]:
import numpy as np
import plotly.graph_objects as go
from astropy import units as u

import tardis.visualization.plot_util as pu
from tardis import run_tardis
from tardis.io.atom_data import download_atom_data

/home/runner/work/tardis/tardis/tardis/__init__.py:23: UserWarning: Astropy is already imported externally. Astropy should be imported after TARDIS.
  warnings.warn(

Every simulation run requires atomic data and a configuration file.

Atomic Data

We recommend using the kurucz_cd23_chianti_H_He.h5 dataset.

[2]:
# We download the atomic data needed to run the simulation
download_atom_data("kurucz_cd23_chianti_H_He")
Configuration File /home/runner/.astropy/config/tardis_internal_config.yml does not exist - creating new one from default
CRITICAL:root:
********************************************************************************

TARDIS will download different kinds of data (e.g. atomic) to its data directory /home/runner/Downloads/tardis-data

TARDIS DATA DIRECTORY not specified in /home/runner/.astropy/config/tardis_internal_config.yml:

ASSUMING DEFAULT DATA DIRECTORY /home/runner/Downloads/tardis-data
 YOU CAN CHANGE THIS AT ANY TIME IN /home/runner/.astropy/config/tardis_internal_config.yml

********************************************************************************


WARNING:tardis.io.atom_data.atom_web_download:Atomic Data kurucz_cd23_chianti_H_He already exists in /home/runner/Downloads/tardis-data/kurucz_cd23_chianti_H_He.h5. Will not download - override with force_download=True.

Example Configuration File

The configuration file tardis_example.yml is used throughout this Quickstart.

[3]:
!wget -q -nc https://raw.githubusercontent.com/tardis-sn/tardis/master/docs/tardis_example.yml
[4]:
!cat tardis_example.yml
# Example YAML configuration for TARDIS
tardis_config_version: v1.0

supernova:
  luminosity_requested: 9.44 log_lsun
  time_explosion: 13 day

atom_data: kurucz_cd23_chianti_H_He.h5

model:
  structure:
    type: specific
    velocity:
      start: 1.1e4 km/s
      stop: 20000 km/s
      num: 20
    density:
      type: branch85_w7

  abundances:
    type: uniform
    O: 0.19
    Mg: 0.03
    Si: 0.52
    S: 0.19
    Ar: 0.04
    Ca: 0.03

plasma:
  disable_electron_scattering: no
  ionization: lte
  excitation: lte
  radiative_rates_type: dilute-blackbody
  line_interaction_type: macroatom

montecarlo:
  seed: 23111963
  no_of_packets: 4.0e+4
  iterations: 20
  nthreads: 1

  last_no_of_packets: 1.e+5
  no_of_virtual_packets: 10

  convergence_strategy:
    type: damped
    damping_constant: 1.0
    threshold: 0.05
    fraction: 0.8
    hold_iterations: 3
    t_inner:
      damping_constant: 0.5

spectrum:
  start: 500 angstrom
  stop: 20000 angstrom
  num: 10000

Callback Function

Callback function to extract luminosity, radiation temperature, and dilution factor at each simulation iteration.

[5]:
emitted_luminosity = []
absorbed_luminosity = []
t_rad = []
w = []


def fetch_simulation_properties(simulation):
    emitted_luminosity.append(np.copy(simulation.emitted_luminosity.value))
    absorbed_luminosity.append(np.copy(simulation.reabsorbed_luminosity.value))
    t_rad.append(np.copy(simulation.simulation_state.t_radiative.value))
    w.append(np.copy(simulation.simulation_state.dilution_factor))

Running the Simulation

To run the simulation, import the run_tardis function and create the sim object.

Note:

Get more information about the progress bars, logging configuration, and convergence plots.

[6]:
sim = run_tardis(
    "tardis_example.yml",
    log_level="ERROR",
    simulation_callbacks=[(fetch_simulation_properties,)],
)

Preparing Luminosity and Temperature Data

[7]:
luminosity_requested = sim.luminosity_requested
velocity_km_s = np.copy(sim.simulation_state.velocity.to(u.km / u.s).value)
luminosities = ["Emitted", "Absorbed", "Requested"]

value_data = {
    "Emitted": emitted_luminosity,
    "Absorbed": absorbed_luminosity,
    "Requested": [luminosity_requested.value] * sim.iterations,
    "t_inner": np.copy(sim.iterations_t_inner.value),
}

Visualization

Plasma Plot

[8]:
plasma_colorscale = pu.get_hex_color_strings(sim.iterations)
fig = go.FigureWidget().set_subplots(rows=1, cols=2, shared_xaxes=True)

# empty traces to build figure
fig.add_scatter(row=1, col=1)
fig.add_scatter(row=1, col=2)

# 2 y axes and 2 x axes correspond to the 2 subplots in the plasma plot
fig = fig.update_layout(
    xaxis={
        "tickformat": "g",
        "title": pu.axis_label_in_latex("Velocity", u.km / u.s),
    },
    xaxis2={
        "tickformat": "g",
        "title": pu.axis_label_in_latex("Velocity", u.km / u.s),
        "matches": "x",
    },
    yaxis={
        "tickformat": "g",
        "title": pu.axis_label_in_latex("T_{rad}", u.K, only_text=False),
        "nticks": 15,
    },
    yaxis2={
        "tickformat": "g",
        "title": r"$W$",
        "nticks": 15,
    },
    height=450,
    legend_title_text="Iterations",
    legend_traceorder="reversed",
    margin=dict(
        l=10, r=135, b=25, t=25, pad=0
    ),  # reduce whitespace surrounding the plot and increase right indentation to align with the t_inner and luminosity plot
)

plasma_plot = fig

for iter_num in range(sim.iterations):
    # add luminosity data in hover data in plasma plots
    customdata = len(velocity_km_s) * [
        (
            "<br>"
            "Emitted Luminosity: "
            f"{emitted_luminosity[iter_num]:.4g}"
            "<br>"
            "Requested Luminosity: "
            f"{luminosity_requested:.4g}"
            "<br>"
            "Absorbed Luminosity: "
            f"{absorbed_luminosity[iter_num]:.4g}"
        )
    ]

    # add a radiation temperature vs shell velocity trace to the plasma plot
    plasma_plot.add_scatter(
        x=velocity_km_s,
        y=np.append(t_rad[iter_num], t_rad[iter_num][-1:]),
        line_color=plasma_colorscale[iter_num],
        line_shape="hv",
        row=1,
        col=1,
        name=iter_num + 1,
        legendgroup=f"group-{iter_num}",
        showlegend=False,
        customdata=customdata,
        hovertemplate="<b>Y</b>: %{y:.3f} at <b>X</b> = %{x:,.0f}%{customdata}",
    )

    # add a dilution factor vs shell velocity trace to the plasma plot
    plasma_plot.add_scatter(
        x=velocity_km_s,
        y=np.append(w[iter_num], w[iter_num][-1:]),
        line_color=plasma_colorscale[iter_num],
        line_shape="hv",
        row=1,
        col=2,
        legendgroup=f"group-{iter_num}",
        name=iter_num + 1,
        customdata=customdata,
        hovertemplate="<b>Y</b>: %{y:.3f} at <b>X</b> = %{x:,.0f}%{customdata}",
    )

plasma_plot.show(renderer="notebook_connected")

Luminosity Plot

[9]:
t_inner_luminosities_colors = pu.get_hex_color_strings(5)
fig = go.FigureWidget().set_subplots(
    rows=3,
    cols=1,
    shared_xaxes=True,
    vertical_spacing=0.08,
    row_heights=[0.25, 0.5, 0.25],
)

# add inner boundary temperature vs iterations plot
fig.add_scatter(
    name="Inner<br>Boundary<br>Temperature",
    row=1,
    col=1,
    hovertext="text",
    marker_color=t_inner_luminosities_colors[0],
    mode="lines+markers",
)

# add luminosity vs iterations plot
# has three traces for emitted, requested and absorbed luminosities
for luminosity, line_color in zip(
    luminosities, t_inner_luminosities_colors[1:4]
):
    fig.add_scatter(
        name=luminosity + "<br>Luminosity",
        mode="lines+markers",
        row=2,
        col=1,
        marker_color=line_color,
    )

# add residual luminosity vs iterations plot
fig.add_scatter(
    name="Residual<br>Luminosity",
    row=3,
    col=1,
    marker_color=t_inner_luminosities_colors[4],
    mode="lines+markers",
)

# 3 y axes and 3 x axes correspond to the 3 subplots in the t_inner and luminosity convergence plot
fig = fig.update_layout(
    xaxis=dict(range=[0, sim.iterations + 1], dtick=2),
    xaxis2=dict(
        matches="x",
        range=[0, sim.iterations + 1],
        dtick=2,
    ),
    xaxis3=dict(
        title=r"$\mbox{Iteration Number}$",
        dtick=2,
    ),
    yaxis=dict(
        title=pu.axis_label_in_latex("T_{inner}", u.K, only_text=False),
        automargin=True,
        tickformat="g",
        exponentformat="e",
        nticks=4,
    ),
    yaxis2=dict(
        exponentformat="e",
        title=pu.axis_label_in_latex("Luminosity", u.erg / u.s),
        title_font_size=13,
        automargin=True,
        nticks=7,
    ),
    yaxis3=dict(
        title=r"$~~\text{Residual}\\\text{Luminosity[%]}$",
        title_font_size=12,
        automargin=True,
        nticks=4,
    ),
    height=630,
    hoverlabel_align="right",
    margin=dict(b=25, t=25, pad=0),  # reduces whitespace surrounding the plot
)

t_inner_luminosities_plot = fig

x = list(range(1, sim.iterations + 1))

with t_inner_luminosities_plot.batch_update():
    # traces are updated according to the order they were added
    # the first trace is of the inner boundary temperature plot
    t_inner_luminosities_plot.data[0].x = x
    t_inner_luminosities_plot.data[0].y = value_data["t_inner"]
    t_inner_luminosities_plot.data[
        0
    ].hovertemplate = "<b>%{y:.3f}</b> at X = %{x:,.0f}<extra>Inner Boundary Temperature</extra>"  # trace name in extra tag to avoid new lines in hoverdata

    # the next three for emitted, absorbed and requested luminosities
    for index, luminosity in zip(range(1, 4), luminosities):
        t_inner_luminosities_plot.data[index].x = x
        t_inner_luminosities_plot.data[index].y = value_data[luminosity]
        t_inner_luminosities_plot.data[index].hovertemplate = (
            "<b>%{y:.4g}</b>" + "<br>at X = %{x}<br>"
        )
    # last is for the residual luminosity
    y = [
        ((emitted - luminosity_requested.value) * 100)
        / luminosity_requested.value
        for emitted in value_data["Emitted"]
    ]
    t_inner_luminosities_plot.data[4].x = x
    t_inner_luminosities_plot.data[4].y = y
    t_inner_luminosities_plot.data[
        4
    ].hovertemplate = "<b>%{y:.2f}%</b> at X = %{x:,.0f}"

t_inner_luminosities_plot.show(renderer="notebook_connected")