Tutorial 2: Stellar Population Models#
This tutorial explores stellar population modeling using Isochrone and StellarPop classes.
Topics Covered#
Isochrone class for coeval stellar populations
StellarPop for population synthesis
Age and metallicity sequences
IMF sampling and mass functions
Synthetic cluster generation
Prerequisites#
This tutorial requires the following brutus data files:
MIST_1.2_iso_vvcrit0.0.h5- MIST isochronesnn_c3k.h5- Neural network for bolometric corrections
If you don’t have these files, run the optional download cell below.
# Optional: Download required data files (only needed if not already cached)
# This tutorial requires MIST isochrones and the C3K neural network.
# Uncomment the lines below to download them (~250 MB total).
# from brutus.data import fetch_isos, fetch_nns
# fetch_isos() # ~200 MB -- MIST isochrones
# fetch_nns() # ~50 MB -- Neural network for bolometric corrections
# Imports and setup
import numpy as np
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore')
from tutorial_utils import (
setup_tutorial,
find_brutus_data_file,
save_figure as _save_fig,
print_section,
)
info = setup_tutorial(2, title="Tutorial 02: Stellar Population Models")
plots_dir = info['plot_dir']
def save_figure(fig, name):
"""Save figure to this tutorial's plot directory."""
_save_fig(fig, 2, name)
Tutorial 02: Stellar Population Models
======================================
Checking data requirements for Tutorial 2
=========================================
Found: nn_c3k.h5
Found: MIST_1.2_iso_vvcrit0.0.h5
All required files available
Section 1: Understanding Isochrones#
Isochrones model populations of stars born at the same time with the same composition. They are fundamental for understanding star clusters and stellar populations in galaxies.
Key Concepts#
Isochrone: A line in the HR diagram connecting stars of the same age and composition but different masses
Coeval population: All stars formed at the same time (e.g., star clusters)
EEP grid: Equivalent Evolutionary Points provide consistent sampling across different masses
from brutus.core import Isochrone
from brutus.data import filters
# Initialize Isochrone
print("Loading MIST isochrones...")
mistfile = find_brutus_data_file('MIST_1.2_iso_vvcrit0.0.h5')
# Note: Isochrone doesn't take nnfile or filters as arguments
iso = Isochrone(mistfile=mistfile, verbose=False)
print(f"Loaded isochrones")
print(f" Age range: {iso.loga_grid.min():.1f} - {iso.loga_grid.max():.1f} log(years)")
print(f" Metallicity range: {iso.feh_grid.min():.2f} - {iso.feh_grid.max():.2f}")
print(f" Available predictions: {iso.predictions}")
Loading MIST isochrones...
Loaded isochrones
Age range: 5.0 - 10.3 log(years)
Metallicity range: -4.00 - 0.50
Available predictions: ['mini', 'mass', 'logl', 'logt', 'logr', 'logg', 'feh_surf', 'afe_surf']
# Generate a sample isochrone
print("\nGenerating 1 Gyr solar metallicity isochrone...")
# Set EEP grid (covers full evolution)
eep_grid = np.linspace(202, 808, 2000)
# Generate isochrone parameters
params_arr = iso.get_predictions(feh=0.0, afe=0.0, loga=9.0, eep=eep_grid)
# Convert to structured array for easier access
params = {}
for i, label in enumerate(iso.predictions):
params[label] = params_arr[:, i]
# Find valid points (where mass exists)
valid = np.isfinite(params['mini'])
print(f" Valid EEP points: {valid.sum()}/{len(eep_grid)}")
print(f" Mass range: {params['mini'][valid].min():.2f} - {params['mini'][valid].max():.2f} M☉")
# Show some example stellar parameters
print("\nExample stellar parameters along isochrone:")
for i in [100, 500, 900, 1300]:
if i < len(params['mini']) and np.isfinite(params['mini'][i]):
print(f" Star {i}: M={params['mini'][i]:.2f} M☉, "
f"Teff={10**params['logt'][i]:.0f} K, "
f"L={10**params['logl'][i]:.2f} L☉")
Generating 1 Gyr solar metallicity isochrone...
Valid EEP points: 2000/2000
Mass range: 0.14 - 2.29 M☉
Example stellar parameters along isochrone:
Star 100: M=0.35 M☉, Teff=3312 K, L=0.02 L☉
Star 500: M=1.77 M☉, Teff=7704 K, L=14.07 L☉
Star 900: M=2.05 M☉, Teff=6323 K, L=37.01 L☉
Star 1300: M=2.07 M☉, Teff=4397 K, L=169.89 L☉
Section 2: StellarPop for Population Synthesis#
StellarPop generates synthetic stellar populations by sampling from an IMF and computing population-level photometry including binaries.
Key Features#
Combines Isochrone predictions with neural network photometry
Handles binary star populations
Applies extinction and distance effects
Generates realistic synthetic clusters
from brutus.core import StellarPop
# Initialize StellarPop with photometric filters
nnfile = find_brutus_data_file('nn_c3k.h5')
filt = filters.gaia + filters.ps[:3]
print(f"Using filters: {', '.join(filt)}")
# Create StellarPop instance
pop = StellarPop(isochrone=iso, filters=filt, nnfile=nnfile, verbose=False)
print("StellarPop initialized")
Using filters: Gaia_G_MAW, Gaia_BP_MAWf, Gaia_RP_MAW, PS_g, PS_r, PS_i
StellarPop initialized
# Evaluate isochrone SEDs
print("Evaluating 1 Gyr solar metallicity isochrone...")
# Isochrone parameters
feh, afe, loga = 0.0, 0.0, 9.0 # Solar metallicity, 1 Gyr
av, rv, dist = 0.0, 3.32, 10.0 # 10 pc (absolute mags), no extinction
# Generate photometry for all EEP points along the isochrone
# Note: get_seds returns one SED per EEP point, not an IMF-sampled population
mags, params, binary_params = pop.get_seds(
feh=feh, afe=afe, loga=loga,
av=av, rv=rv, dist=dist,
binary_fraction=0.0
)
# Filter to valid (finite) models
valid = np.all(np.isfinite(mags), axis=1) & np.isfinite(params['mini'])
print(f"Evaluated {len(mags)} EEP points, {valid.sum()} with valid photometry")
print(f" Mass range: {params['mini'][valid].min():.2f} - {params['mini'][valid].max():.2f} M☉")
# Show magnitude statistics
print("\nMagnitude statistics (valid models at 10 pc):")
for i, f in enumerate(filt):
vals = mags[valid, i]
print(f" {f}: {np.median(vals):.1f} (range {vals.min():.1f} to {vals.max():.1f}) mag")
Evaluating 1 Gyr solar metallicity isochrone...
Evaluated 1710 EEP points, 1206 with valid photometry
Mass range: 0.53 - 2.29 M☉
Magnitude statistics (valid models at 10 pc):
Gaia_G_MAW: -2.0 (range -5.0 to 8.6) mag
Gaia_BP_MAWf: -0.1 (range -4.9 to 9.6) mag
Gaia_RP_MAW: -3.3 (range -5.5 to 7.6) mag
PS_g: 0.2 (range -5.0 to 9.9) mag
PS_r: -0.8 (range -5.0 to 8.8) mag
PS_i: -2.6 (range -5.1 to 8.0) mag
Saved: /mnt/c/Users/joshs/Dropbox/GitHub/brutus/tutorials/plots/tutorial_02/stellarpop_isochrone.png
Section 3: Isochrone Age Sequences#
Stellar populations evolve with time as stars leave the main sequence. Let’s explore how isochrones change with age.
Age Effects#
Main sequence turnoff moves to lower masses with age
Red giant branch develops for older populations
Horizontal branch appears in old, metal-poor populations
Color distribution becomes redder with age
Generating age sequence...
4 Myr: 1621 valid points
10 Myr: 1999 valid points
100 Myr: 2000 valid points
1 Gyr: 1865 valid points
3 Gyr: 1780 valid points
10 Gyr: 1683 valid points
Saved: /mnt/c/Users/joshs/Dropbox/GitHub/brutus/tutorials/plots/tutorial_02/age_sequences.png
Age sequence effects demonstrated
Section 4: Metallicity Effects on Stellar Populations#
Metallicity profoundly affects stellar populations, changing their colors, luminosities, and evolutionary timescales.
Metallicity Effects#
Metal-poor stars are bluer and hotter at fixed mass
RGB position is sensitive to metallicity
Main sequence width increases with metallicity spread
Galactic components have distinct metallicity distributions
Generating metallicity sequence...
[Fe/H] = -2.0: 1728 valid points
[Fe/H] = -1.0: 1741 valid points
[Fe/H] = -0.5: 1754 valid points
[Fe/H] = +0.0: 1829 valid points
[Fe/H] = +0.3: 1775 valid points
Saved: /mnt/c/Users/joshs/Dropbox/GitHub/brutus/tutorials/plots/tutorial_02/metallicity_populations.png
Metallicity effects demonstrated
Section 5: IMF Sampling and Mass Functions#
The Initial Mass Function (IMF) determines the distribution of stellar masses in a population.
Kroupa IMF#
brutus implements a Kroupa (2001) broken power-law IMF via logp_imf.
This is a two-segment power law with a shallower slope at low masses
(below the break mass) and a steeper slope at high masses:
Low mass (M ≤ 0.5 M☉): α = 1.3
High mass (M > 0.5 M☉): α = 2.3
The break mass, slopes, and mass limits are all configurable parameters
of logp_imf.
Section 6: Isochrone Comparisons Across Cluster Types#
Different stellar environments have distinct ages, metallicities, and alpha enhancements. Let’s compare isochrones for three representative cluster types to see how these parameters affect the CMD and HRD.
# Extended filter set for multi-wavelength comparison
filt_full = filters.gaia + filters.ps[:3] + filters.tmass
pop_full = StellarPop(isochrone=iso, filters=filt_full, nnfile=nnfile, verbose=False)
print(f"Using extended filter set: {', '.join(filt_full)}")
# Generate isochrones for three different cluster types
# Use absolute magnitudes (dist=10 pc) so all are on the same scale
print("\nGenerating isochrones for different cluster types...\n")
# Cluster 1: Young open cluster (Pleiades-like)
print(" Young (100 Myr, solar)...")
mags1, params1, _ = pop_full.get_seds(
feh=0.0, afe=0.0, loga=8.0, # 100 Myr
av=0.0, rv=3.32, dist=10.0, # absolute magnitudes
binary_fraction=0.0
)
valid1 = np.all(np.isfinite(mags1), axis=1)
print(f" {valid1.sum()} valid EEP points")
# Cluster 2: Intermediate age (Hyades-like)
print(" Intermediate (625 Myr, slightly metal-rich)...")
mags2, params2, _ = pop_full.get_seds(
feh=0.15, afe=0.0, loga=8.8, # 625 Myr
av=0.0, rv=3.32, dist=10.0,
binary_fraction=0.0
)
valid2 = np.all(np.isfinite(mags2), axis=1)
print(f" {valid2.sum()} valid EEP points")
# Cluster 3: Old globular cluster
# Note: We use afe=0.0 because the C3K neural network does not cover
# alpha-enhanced models. Real GCs are typically alpha-enhanced ([a/Fe]~0.3),
# but the NN returns NaN for any afe > 0.
print(" Old globular (12 Gyr, metal-poor)...")
mags3, params3, _ = pop_full.get_seds(
feh=-1.5, afe=0.0, loga=10.08, # 12 Gyr
av=0.0, rv=3.32, dist=10.0,
binary_fraction=0.0
)
valid3 = np.all(np.isfinite(mags3), axis=1)
print(f" {valid3.sum()} valid EEP points")
Using extended filter set: Gaia_G_MAW, Gaia_BP_MAWf, Gaia_RP_MAW, PS_g, PS_r, PS_i, 2MASS_J, 2MASS_H, 2MASS_Ks
Generating isochrones for different cluster types...
Young (100 Myr, solar)...
1315 valid EEP points
Intermediate (625 Myr, slightly metal-rich)...
1273 valid EEP points
Old globular (12 Gyr, metal-poor)...
1199 valid EEP points
Saved: /mnt/c/Users/joshs/Dropbox/GitHub/brutus/tutorials/plots/tutorial_02/cluster_isochrones.png
Isochrone comparisons demonstrated
Total valid EEP points: 3787
Summary and Key Takeaways#
This tutorial has covered stellar population modeling in brutus:
Key Classes#
Isochrone: Models coeval stellar populations
Interpolates MIST isochrone tables
Returns stellar parameters for populations of given age/metallicity
Covers full range of stellar masses at each age
StellarPop: Generates synthetic populations with photometry
Combines Isochrone with neural network photometry
Handles binary populations
Applies extinction and distance effects
Enables realistic cluster simulations
Physical Effects#
Age: Determines turnoff mass and RGB properties
Metallicity: Affects colors, luminosities, and evolution
IMF: Controls the mass distribution and low-mass star counts
Binaries: Create broader sequences and affect cluster dynamics
Applications#
Star cluster analysis and fitting
Stellar population synthesis
Galactic archaeology studies
Calibration of stellar parameters
Next Steps#
Tutorial 3: Model Grids and Performance Optimization
Tutorial 4: Galactic Priors and Population Synthesis
Tutorial 5: Fitting Individual Sources with BruteForce
Tutorial 6: Cluster Analysis and Bayesian Inference
print("Tutorial 2 Complete!")
print("="*60)
print("\nGenerated plots:")
for plot_file in sorted(plots_dir.glob('*.png')):
print(f" - {plot_file.name}")
Tutorial 2 Complete!
============================================================
Generated plots:
- age_sequences.png
- cluster_isochrones.png
- imf_sampling.png
- metallicity_populations.png
- ps1_luminosity_function.png
- stellarpop_isochrone.png