Quickstart#
This tutorial walks through the core spotgp workflow in five steps:
Define a spot evolution model — choose an envelope, visibility function, and amplitude
Simulate a lightcurve — generate synthetic photometry from the model
Build the analytic kernel — compute the GP covariance from the physics
Fit the GP to data — find the MAP hyperparameters with
GPSolverInspect the fit — compare the model prediction, ACF, and PSD to the data
import numpy as np
import matplotlib.pyplot as plt
from spotgp import (
TrapezoidSymmetricEnvelope,
VisibilityFunction,
SpotEvolutionModel,
LightcurveModel,
AnalyticKernel,
GPSolver,
)
np.random.seed(42)
1. Define a spot evolution model#
A SpotEvolutionModel combines three components:
Component |
What it controls |
Key parameters |
|---|---|---|
Envelope |
Spot size vs. time |
|
Visibility |
Stellar geometry |
|
Amplitude |
Kernel scaling |
|
envelope = TrapezoidSymmetricEnvelope(lspot=14.0, tau_spot=6.0)
visibility = VisibilityFunction(peq=4.0, kappa=0.3, inc=np.pi / 3)
model = SpotEvolutionModel(envelope=envelope, visibility=visibility, sigma_k=0.01)
print(model)
2. Simulate a lightcurve#
LightcurveModel.from_spot_model places nspot spots at random longitudes, latitudes, and emergence times, then sums their flux deficits over the observation window. We add Gaussian noise to mimic a real observation.
lc = LightcurveModel.from_spot_model(
spot_model=model,
nspot=30,
tsim=150,
tsamp=1.0,
)
# Add observation noise
sigma_n = 0.3 * np.std(lc.flux)
flux_obs = lc.flux + np.random.normal(0, sigma_n, lc.flux.shape)
flux_err = np.full_like(flux_obs, sigma_n)
lc.plot_lightcurve()
plt.show()
3. Build the analytic kernel#
AnalyticKernel computes the GP covariance function directly from the spot evolution physics. Call build_jax() once to pre-compile the XLA kernels — subsequent evaluations are fast.
ak = AnalyticKernel(model).build_jax()
# Evaluate and plot the kernel
tlags = np.linspace(0, 3 * model.peq, 300)
K = ak.kernel(tlags)
fig, ax = plt.subplots(figsize=(8, 3))
ax.plot(tlags, K, color="steelblue")
ax.set_xlabel("Time lag [days]")
ax.set_ylabel("K(τ)")
ax.set_title("Analytic GP kernel")
plt.tight_layout()
plt.show()
4. Fit the GP to data#
GPSolver builds the banded covariance matrix from the kernel and finds the maximum a posteriori (MAP) hyperparameters via L-BFGS-B. Define parameter bounds, compile with build_jax(), and run fit_map().
bounds = {
"peq": (2.0, 10.0),
"kappa": (-0.5, 0.8),
"inc": (0.1, np.pi / 2),
"lspot": (2.0, 25.0),
"tau_spot": (0.5, 12.0),
"log_sigma_k": (-4.0, 0.0),
}
gp = GPSolver(lc.t, flux_obs, flux_err, model, bounds=bounds).build_jax()
theta_map, opt_result = gp.fit_map(nopt=3)
# Compare MAP estimates to true values
theta_true = dict(peq=4.0, kappa=0.3, inc=np.pi / 3,
lspot=14.0, tau_spot=6.0, sigma_k=0.01)
print(f"{'param':>12s} {'true':>8s} {'MAP':>8s}")
print("-" * 34)
for k, v_true in theta_true.items():
v_map = theta_map.get(k, theta_map.get("log_" + k, float("nan")))
print(f"{k:>12s} {v_true:8.4f} {v_map:8.4f}")
5. Inspect the fit#
GPSolver includes built-in diagnostic plots: the GP posterior prediction over the data, the autocorrelation function (ACF), and the power spectral density (PSD).
fig, axes = plt.subplots(3, 1, figsize=(10, 10))
gp.plot_prediction(theta=theta_map, ax=axes[0])
gp.plot_acf(theta=theta_map, ax=axes[1], normalize=True)
gp.plot_psd(theta=theta_map, ax=axes[2])
fig.tight_layout()
plt.show()
Next steps#
Understand the physics: Time domain and Fourier domain tutorials derive the kernel from first principles
Explore the model: Code overview describes the module architecture
Customize components: Swap in a different envelope, visibility function, or latitude distribution
Run MCMC: BlackJAX sampling and Dynesty sampling for full posterior inference