Probabilistic Graphical Model Visualization#

A probabilistic graphical model (PGM) is a diagram that shows the dependencies between all variables in a statistical model — which parameters are free, which quantities are observed, and how they relate through deterministic functions and probability distributions.

spotgp can generate PGMs automatically from a configured GPSolver or MultiBandGPSolver. The diagram adapts to the model configuration: envelope type, whether noise is a free parameter, single- vs. multi-band mode, etc.

Diagram conventions:

Symbol

Meaning

Open circle

Latent (free) parameter

Shaded circle

Observed variable

Small dot

Fixed / known input

Ellipse

Deterministic function of parents

Rectangle (plate)

Repeated structure

Arrow

Dependency

import numpy as np
import matplotlib.pyplot as plt

from spotgp import (
    GPSolver,
    TrapezoidSymmetricEnvelope,
    TrapezoidAsymmetricEnvelope,
    VisibilityFunction,
    SpotEvolutionModel,
    PGModelVis,
)
from spotgp.multiband import MultiBandData, MultiBandGPSolver

np.random.seed(42)

# Shared synthetic data for all examples
x = np.linspace(0, 30, 200)
y = 1.0 + 0.01 * np.sin(2 * np.pi * x / 5.0) + 0.001 * np.random.randn(len(x))
yerr = 0.001 * np.ones_like(x)

1. Default single-band GP#

The simplest case: a symmetric trapezoid envelope with six free kernel parameters (\(P_{\rm eq}\), \(\kappa\), \(i\), \(\ell_{\rm spot}\), \(\tau_{\rm spot}\), \(\sigma_k\)) and no free noise term.

The PGM shows how the rotation parameters feed into the visibility function \(V(\phi)\), the envelope parameters feed into \(R_\Gamma(\tau)\), and both combine with \(\sigma_k\) to form the kernel \(K(\tau)\) that generates the observed flux \(y_i\).

hparam = dict(peq=5.0, kappa=0.3, inc=1.2, lspot=5.0, tau_spot=2.0, sigma_k=0.01)

gp = GPSolver(x, y, yerr, hparam)
fig = gp.plot_pgm()
plt.show()
Banded Cholesky: bandwidth=199, N=200, sparsity=0.0%
../_images/128b0c5eeb660e332f4c3446207e2f752c0222799b2e7120c4681feaa5b7f723.png

2. Adding white noise as a free parameter#

Setting fit_sigma_n=True adds \(\sigma_n\) as a seventh free parameter. In the PGM it connects directly to \(y_i\) (not through the kernel), reflecting that white noise is added independently to each observation.

gp_noise = GPSolver(x, y, yerr, hparam, fit_sigma_n=True)

print("Free parameters:", gp_noise.param_keys)

fig = gp_noise.plot_pgm(show_legend=True)
plt.show()
Banded Cholesky: bandwidth=199, N=200, sparsity=0.0%
Free parameters: ('peq', 'kappa', 'inc', 'lspot', 'tau_spot', 'sigma_k', 'sigma_n')
../_images/2a6454fe4e788132cc622bbd738416161a76a42de365cd25fe8b3c912c5a3fdf.png

3. Asymmetric envelope#

Switching to a TrapezoidAsymmetricEnvelope replaces \(\tau_{\rm spot}\) with separate emergence and decay timescales (\(\tau_{\rm em}\), \(\tau_{\rm dec}\)). The PGM automatically shows three parameters feeding into \(R_\Gamma(\tau)\) instead of two.

envelope_asym = TrapezoidAsymmetricEnvelope(lspot=5.0, tau_em=1.0, tau_dec=3.0)
visibility = VisibilityFunction(peq=5.0, kappa=0.3, inc=1.2)
model_asym = SpotEvolutionModel(
    envelope=envelope_asym, visibility=visibility, sigma_k=0.01,
)

gp_asym = GPSolver(x, y, yerr, model_asym, fit_sigma_n=True)

print("Free parameters:", gp_asym.param_keys)

fig = gp_asym.plot_pgm()
plt.show()
Banded Cholesky: bandwidth=199, N=200, sparsity=0.0%
Free parameters: ('peq', 'kappa', 'inc', 'lspot', 'tau_em', 'tau_dec', 'sigma_k', 'sigma_n')
../_images/1988ee8295f6ac20ccdbaa4ab971641ffb664e6f494226e2ce0e39e3be3ce258.png

4. Multi-band GP#

MultiBandGPSolver adds the spot temperature \(T_{\rm spot}\) as a free parameter and introduces the wavelength-dependent contrast factor \(c(\lambda) = 1 - B_\lambda(T_{\rm spot}) / B_\lambda(T_{\rm phot})\).

The PGM shows:

  • \(T_{\rm spot}\) (free, open circle) and \(T_{\rm phot}\) (fixed, small dot) feeding into \(c(\lambda)\)

  • \(c(\lambda)\) inside a band plate (\(b = 1, \ldots, B\)), since there is one contrast value per photometric band

  • The kernel is labeled \(K(\tau;\,\lambda)\) to indicate its wavelength dependence

# Build three-band synthetic data
bands = {}
for name, lam in [("g", 4770.0), ("r", 6231.0), ("i", 7625.0)]:
    xb = np.sort(np.random.uniform(0, 30, 100))
    yb = 1.0 + 0.005 * np.sin(2 * np.pi * xb / 5.0) + 0.001 * np.random.randn(100)
    bands[name] = {"x": xb, "y": yb, "yerr": 0.001 * np.ones(100), "wavelength": lam}

data = MultiBandData(bands)

gp_mb = MultiBandGPSolver(
    data,
    hparam,
    T_phot=5800.0,
    T_spot_init=4500.0,
    fit_sigma_n=True,
)

print("Free parameters:", gp_mb.param_keys)
print(f"Bands: {data.band_names}  (N = {data.N} total observations)")

fig = gp_mb.plot_pgm()
plt.show()
MultiBand banded Cholesky: bandwidth=299, N=300, n_bands=3, sparsity=0.0%
Free parameters: ('peq', 'kappa', 'inc', 'lspot', 'tau_spot', 'sigma_k', 'T_spot', 'sigma_n')
Bands: ['g', 'r', 'i']  (N = 300 total observations)
../_images/c540a0979517a5ec0a5255cad9ac1a3ff254baa45e366ea98be2548c43e9ef95.png

5. Parameter legend#

Pass show_legend=True to add a key below the diagram that maps each LaTeX symbol to a descriptive name. This is useful for presentations or papers where readers may not be familiar with the notation.

fig = gp_noise.plot_pgm(show_legend=True)
plt.show()
../_images/2a6454fe4e788132cc622bbd738416161a76a42de365cd25fe8b3c912c5a3fdf.png

The legend adapts to the model — here is the multi-band case, which includes \(T_{\rm spot}\) and marks \(T_{\rm phot}\) as fixed.

fig = gp_mb.plot_pgm(show_legend=True)
plt.show()
../_images/d7c3eb4e061bb4014e718fea9945e5788a9b6ceaedc14cc44d03433c14519883.png

6. Using PGModelVis directly#

You can also create a PGModelVis object directly to inspect the detected parameter groups or to customize dpi, node_scale, and font_size.

vis = PGModelVis(gp_mb)

print("Rotation params: ", vis.rotation_params)
print("Envelope params: ", vis.envelope_params)
print("Amplitude params:", vis.amplitude_params)
print("Multiband params:", vis.multiband_params)
print("Noise params:    ", vis.noise_params)

fig = vis.render(dpi=200, node_scale=1.4, font_size=13, show_legend=True)
plt.show()
Rotation params:  ['peq', 'kappa', 'inc']
Envelope params:  ['lspot', 'tau_spot']
Amplitude params: ['sigma_k']
Multiband params: ['T_spot']
Noise params:     ['sigma_n']
../_images/f34b12658f662ea28d7289563d8b561c00d36743f637ea5ce987e2060417cef6.png