Assigning Simulated FRBs to Host Galaxies
This notebook demonstrates how to use the astropath.simulations.assign_host module to assign simulated FRBs to host galaxies from a catalog.
The assignment process:
Matches FRBs to galaxies by apparent magnitude (brighter FRBs → brighter galaxies)
Randomly places FRBs within their host galaxy
Applies realistic localization errors to create observed coordinates
Output includes:
Observed FRB coordinates (with localization error)
True FRB coordinates (within the galaxy)
Host galaxy properties (ID, magnitude, size)
Offset statistics (galaxy offset, localization error)
[1]:
import os
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from pathlib import Path
from matplotlib.patches import Ellipse
# Set up plotting style
plt.rcParams['figure.figsize'] = (10, 6)
plt.rcParams['font.size'] = 12
[2]:
from astropath.simulations import generate_frbs, assign_frbs_to_hosts
from astropath.simulations.generate_frbs import load_chime_cat1_DMeg
/home/xavier/Projects/FRB/FRB/frb/halos/hmf.py:51: UserWarning: hmf_emulator not imported. Hope you are not intending to use the hmf.py module..
warnings.warn("hmf_emulator not imported. Hope you are not intending to use the hmf.py module..")
Step 1: Generate FRB Population
First, we generate a population of FRBs using generate_frbs(). This gives us FRBs with realistic DM, redshift, and host galaxy magnitude distributions.
[3]:
# Generate 500 CHIME FRBs following the Cat1 DM distribution
n_frbs = 1000
seed = 42
#frbs = generate_frbs(n_frbs, 'CHIME', seed=seed)
DMeg = load_chime_cat1_DMeg()
frbs = generate_frbs(n_frbs, 'CHIME', seed=seed,
dm_catalog=DMeg, dm_range=(0., 3000.))
print(f"Generated {len(frbs)} FRBs")
print(f"\nFRB properties:")
print(f" DM range: {frbs['DMeg'].min():.0f} - {frbs['DMeg'].max():.0f} pc/cm³")
print(f" z range: {frbs['z'].min():.3f} - {frbs['z'].max():.3f}")
print(f" M_r range: {frbs['M_r'].min():.2f} - {frbs['M_r'].max():.2f}")
print(f" m_r range: {frbs['m_r'].min():.2f} - {frbs['m_r'].max():.2f}")
frbs.head()
Loading P(DM,z) grid from /home/xavier/Projects/FRB/FRB/frb/data/DM/CHIME_pzdm.npz
Sampling DM values
Sampling redshifts
Sampling host galaxy absolute magnitudes
Using Lz values to sample host galaxy absolute magnitudes
Generated 1000 FRBs
FRB properties:
DM range: 4 - 2421 pc/cm³
z range: 0.010 - 2.156
M_r range: -24.50 - -15.48
m_r range: 10.94 - 28.64
[3]:
| DMeg | z | M_r | m_r | |
|---|---|---|---|---|
| 0 | 332.544726 | 0.129937 | -21.388885 | 17.609148 |
| 1 | 1321.835444 | 1.159081 | -21.443314 | 23.111602 |
| 2 | 702.774725 | 0.896607 | -18.183908 | 25.684164 |
| 3 | 535.990017 | 0.598077 | -21.433778 | 21.360838 |
| 4 | 165.368080 | 0.195489 | -21.351292 | 18.620048 |
Step 2: Load Galaxy Catalog
We’ll use a real galaxy catalog if available (combined HSC/DECaLs/HECATE), otherwise create a mock catalog.
The catalog must have these columns:
ra,dec: Coordinates (degrees)mag_best: Apparent r-band magnitudehalf_light: Half-light radius (arcsec)ID: Unique identifier
[4]:
def create_mock_galaxy_catalog(n_galaxies=10000, seed=42):
"""
Create a mock galaxy catalog for demonstration.
This is used as a fallback if real catalog is not available.
"""
np.random.seed(seed)
# Create galaxies in a 2x2 degree field
ra = np.random.uniform(150.0, 152.0, n_galaxies)
dec = np.random.uniform(2.0, 4.0, n_galaxies)
# Magnitude distribution (peaks around m_r ~ 21-22)
mag_best = np.random.beta(a=2, b=5, size=n_galaxies) * 8 + 18
mag_best = np.clip(mag_best, 18., 26.)
# Half-light radii (log-normal distribution)
half_light = np.random.lognormal(mean=np.log(0.5), sigma=0.5, size=n_galaxies)
half_light = np.clip(half_light, 0.1, 3.0)
df = pd.DataFrame({
'ra': ra,
'dec': dec,
'mag_best': mag_best,
'half_light': half_light,
'ID': np.arange(n_galaxies)
})
return df
def load_galaxy_catalog(seed=42):
"""
Load galaxy catalog - tries real catalog first, falls back to mock.
Real catalog: $FRB_APATH/combined_HSC_DECaLs_HECATE_galaxies_hecatecut.parquet
"""
# Try to load real catalog
frb_apath = os.environ.get('FRB_APATH')
if frb_apath is not None:
catalog_path = Path(frb_apath) / 'combined_HSC_DECaLs_HECATE_galaxies_hecatecut.parquet'
if catalog_path.exists():
print(f"Loading real galaxy catalog from:")
print(f" {catalog_path}")
galaxies = pd.read_parquet(catalog_path)
# Verify required columns
required_cols = ['ra', 'dec', 'mag_best', 'half_light', 'ID']
if all(col in galaxies.columns for col in required_cols):
print(f" ✓ Loaded {len(galaxies):,} galaxies from real catalog")
return galaxies, True
else:
print(f" ⚠ Missing required columns, falling back to mock catalog")
# Fall back to mock catalog
print("Creating mock galaxy catalog...")
print(" (Set $FRB_APATH to use real HSC/DECaLs/HECATE catalog)")
galaxies = create_mock_galaxy_catalog(n_galaxies=10000, seed=seed)
print(f" Created {len(galaxies):,} mock galaxies")
return galaxies, False
# Load catalog (real or mock)
galaxies, is_real = load_galaxy_catalog(seed=42)
print(f"\nGalaxy catalog properties:")
print(f" Type: {'Real (HSC/DECaLs/HECATE)' if is_real else 'Mock'}")
print(f" N_galaxies: {len(galaxies):,}")
print(f" RA range: {galaxies['ra'].min():.2f} - {galaxies['ra'].max():.2f} deg")
print(f" Dec range: {galaxies['dec'].min():.2f} - {galaxies['dec'].max():.2f} deg")
print(f" Mag range: {galaxies['mag_best'].min():.2f} - {galaxies['mag_best'].max():.2f}")
print(f" Size range: {galaxies['half_light'].min():.2f} - {galaxies['half_light'].max():.2f} arcsec")
galaxies.head()
Loading real galaxy catalog from:
/home/xavier/Projects/FRB/data/Catalogs/combined_HSC_DECaLs_HECATE_galaxies_hecatecut.parquet
✓ Loaded 2,297,005 galaxies from real catalog
Galaxy catalog properties:
Type: Real (HSC/DECaLs/HECATE)
N_galaxies: 2,297,005
RA range: 0.17 - 359.92 deg
Dec range: -22.67 - 79.20 deg
Mag range: 9.75 - 28.00
Size range: 0.00 - 111.73 arcsec
[4]:
| DECaL_r | DECaL_ID | ra | dec | gaia_pointsource | shapedev_r | shapeexp_r | type | mag | ang_size | ... | logSFR_GSW | logM_GSW | MIN_SNR | METAL | FLAG_METAL | CLASS_SP | AGN_S17 | AGN_HEC | mag_best | half_light | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 20.342830 | 8.796112e+15 | 33.714628 | -7.532246 | 0.0 | 0.794133 | 0.000000 | DEV | 20.342830 | 0.794133 | ... | NaN | NaN | NaN | NaN | NaN | NaN | None | None | 20.342830 | 0.794133 |
| 1 | 21.302168 | 8.796112e+15 | 33.543950 | -7.531977 | 0.0 | 0.000000 | 1.474610 | EXP | 21.302168 | 1.474610 | ... | NaN | NaN | NaN | NaN | NaN | NaN | None | None | 21.302168 | 1.474610 |
| 2 | 18.706774 | 8.796112e+15 | 33.731347 | -7.531399 | 0.0 | 0.000000 | 5.484947 | EXP | 18.706774 | 5.484947 | ... | NaN | NaN | NaN | NaN | NaN | NaN | None | None | 18.706774 | 5.484947 |
| 3 | 21.775625 | 8.796112e+15 | 33.730953 | -7.533122 | 0.0 | 0.000000 | 1.568451 | EXP | 21.775625 | 1.568451 | ... | NaN | NaN | NaN | NaN | NaN | NaN | None | None | 21.775625 | 1.568451 |
| 4 | 21.368427 | 8.796112e+15 | 33.731264 | -7.529217 | 0.0 | 0.000000 | 4.333760 | EXP | 21.368427 | 4.333760 | ... | NaN | NaN | NaN | NaN | NaN | NaN | None | None | 21.368427 | 4.333760 |
5 rows × 122 columns
Visualize Galaxy Catalog
[5]:
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# Sky distribution
ax = axes[0]
# For real catalog, sample subset for visualization (too many points)
if len(galaxies) > 50000:
plot_galaxies = galaxies.sample(n=50000, random_state=42)
plot_title = f'Galaxy Catalog: Sky Distribution\n(showing 50k of {len(galaxies):,} galaxies)'
else:
plot_galaxies = galaxies
plot_title = 'Galaxy Catalog: Sky Distribution'
scatter = ax.scatter(plot_galaxies['ra'], plot_galaxies['dec'],
c=plot_galaxies['mag_best'], s=1, alpha=0.5, cmap='viridis_r')
ax.set_xlabel('RA (deg)')
ax.set_ylabel('Dec (deg)')
ax.set_title(plot_title)
plt.colorbar(scatter, ax=ax, label='Magnitude (m_r)')
# Magnitude distribution
ax = axes[1]
ax.hist(galaxies['mag_best'], bins=40, alpha=0.7, edgecolor='black')
ax.set_xlabel('Apparent Magnitude (m_r)')
ax.set_ylabel('Number of Galaxies')
ax.set_title('Magnitude Distribution')
ax.axvline(galaxies['mag_best'].median(), color='red', linestyle='--',
label=f"Median: {galaxies['mag_best'].median():.2f}")
ax.legend()
plt.tight_layout()
plt.show()
Step 3: Assign FRBs to Host Galaxies
Now we assign FRBs to host galaxies. The key parameter is the localization error ellipse (a, b, PA):
a: Semi-major axis (arcsec)b: Semi-minor axis (arcsec)PA: Position angle (degrees, East of North)
We’ll use CHIME-like localization: ~0.5” × 0.3” ellipse.
[6]:
# CHIME-like localization
localization = (0.5, 0.3, 45.) # (a, b, PA) in arcsec, arcsec, degrees
print(f"Localization error ellipse:")
print(f" Semi-major axis (a): {localization[0]}\"")
print(f" Semi-minor axis (b): {localization[1]}\"")
print(f" Position angle: {localization[2]}°")
print()
# Assign FRBs to hosts
assignments = assign_frbs_to_hosts(
frb_df=frbs,
galaxy_catalog=galaxies,
localization=localization,
scale=0.5, #trim_catalog=60*units.arcmin,
#mag_range=(17., 28.), # Only assign FRBs with hosts in this mag range
seed=seed
)
print(f"\nSuccessfully assigned {len(assignments)} FRBs to hosts")
print(f"(Filtered from {len(frbs)} based on magnitude range)")
assignments.head(10)
Localization error ellipse:
Semi-major axis (a): 0.5"
Semi-minor axis (b): 0.3"
Position angle: 45.0°
Assigning 1000 FRBs to hosts (filtered from 1000)
Iteration 1: 1000 FRBs remaining
Brightest unassigned FRB: m_r = 10.94 deg
Max magnitude separation: 0.6403 deg deg
Iteration 2: 9 FRBs remaining
Brightest unassigned FRB: m_r = 15.21 deg
Iteration 3: 3 FRBs remaining
Brightest unassigned FRB: m_r = 28.33 deg
Iteration 4: 2 FRBs remaining
Brightest unassigned FRB: m_r = 28.33 deg
Iteration 5: 1 FRBs remaining
Brightest unassigned FRB: m_r = 28.64 deg
Assignment complete after 5 iterations
Generating FRB positions within galaxies...
Applying localization error...
Successfully assigned 1000 FRBs to hosts
(Filtered from 1000 based on magnitude range)
[6]:
| ra | dec | true_ra | true_dec | gal_ID | gal_off | mag | half_light | loc_off | FRB_ID | a | b | PA | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 37.089154 | -6.054853 | 37.089347 | -6.054824 | 8796112420800600 | 0.512163 | 17.609034 | 2.182818 | 0.698880 | 0 | 0.5 | 0.3 | 45.0 |
| 1 | 35.011052 | -3.716714 | 35.010984 | -3.716666 | 38554392407860101 | 0.644914 | 23.111603 | 0.428497 | 0.300721 | 1 | 0.5 | 0.3 | 45.0 |
| 2 | 35.636934 | -5.041360 | 35.637147 | -5.041240 | 37489519691325868 | 0.261397 | 25.684164 | 0.397035 | 0.875139 | 2 | 0.5 | 0.3 | 45.0 |
| 3 | 37.650019 | -6.093941 | 37.649983 | -6.094068 | 8796112420930500 | 0.471343 | 21.360844 | 1.032580 | 0.476924 | 3 | 0.5 | 0.3 | 45.0 |
| 4 | 122.136019 | 38.601062 | 122.135835 | 38.601035 | 8797227703930651 | 0.124636 | 18.620045 | 1.469550 | 0.527688 | 4 | 0.5 | 0.3 | 45.0 |
| 5 | 35.389044 | -4.989786 | 35.389123 | -4.989930 | 8796112796454681 | 0.025617 | 20.312939 | 0.302095 | 0.588104 | 5 | 0.5 | 0.3 | 45.0 |
| 6 | 191.133921 | 50.243986 | 191.134233 | 50.244006 | 8797230892582680 | 0.060900 | 16.888270 | 2.035483 | 0.723296 | 6 | 0.5 | 0.3 | 45.0 |
| 7 | 35.392441 | -3.684912 | 35.392306 | -3.685049 | 38554117529957271 | 0.369458 | 25.616621 | 0.367394 | 0.690091 | 7 | 0.5 | 0.3 | 45.0 |
| 8 | 122.081587 | 38.729600 | 122.081308 | 38.729418 | 8797227777852988 | 2.639463 | 18.471410 | 5.743693 | 1.022398 | 8 | 0.5 | 0.3 | 45.0 |
| 9 | 36.586242 | -5.031810 | 36.586199 | -5.031796 | 37494330054697820 | 0.393669 | 22.124790 | 0.639463 | 0.162957 | 9 | 0.5 | 0.3 | 45.0 |
Step 4: Analyze Results
Offset Statistics
[7]:
print("Offset Statistics")
print("=" * 60)
print("\nGalaxy Offset (FRB position from galaxy center):")
print(f" Mean: {assignments['gal_off'].mean():.3f}\"")
print(f" Median: {assignments['gal_off'].median():.3f}\"")
print(f" Std: {assignments['gal_off'].std():.3f}\"")
print(f" Max: {assignments['gal_off'].max():.3f}\"")
print("\nLocalization Offset (observed - true position):")
print(f" Mean: {assignments['loc_off'].mean():.3f}\"")
print(f" Median: {assignments['loc_off'].median():.3f}\"")
print(f" Std: {assignments['loc_off'].std():.3f}\"")
print(f" Max: {assignments['loc_off'].max():.3f}\"")
print("\nHost Galaxy Magnitudes:")
print(f" Mean: {assignments['mag'].mean():.2f}")
print(f" Median: {assignments['mag'].median():.2f}")
print(f" Range: {assignments['mag'].min():.2f} - {assignments['mag'].max():.2f}")
Offset Statistics
============================================================
Galaxy Offset (FRB position from galaxy center):
Mean: 0.558"
Median: 0.302"
Std: 1.031"
Max: 18.265"
Localization Offset (observed - true position):
Mean: 0.487"
Median: 0.443"
Std: 0.268"
Max: 1.413"
Host Galaxy Magnitudes:
Mean: 20.38
Median: 20.56
Range: 10.96 - 28.00
Offset Distributions
[8]:
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
# Galaxy offsets
ax = axes[0]
ax.hist(np.log10(assignments['gal_off']), bins=30, alpha=0.7, edgecolor='black', color='#1f77b4')
ax.axvline(assignments['gal_off'].median(), color='red', linestyle='--', linewidth=2,
label=f"Median: {assignments['gal_off'].median():.3f}\"")
ax.set_xlabel('log10 Galaxy Offset (arcsec)')
ax.set_ylabel('Number of FRBs')
#ax.set_xscale('log')
ax.set_title('FRB Offsets from Galaxy Centers')
ax.legend()
# Localization errors
ax = axes[1]
ax.hist(assignments['loc_off'], bins=30, alpha=0.7, edgecolor='black', color='#ff7f0e')
ax.axvline(assignments['loc_off'].median(), color='red', linestyle='--', linewidth=2,
label=f"Median: {assignments['loc_off'].median():.3f}\"")
ax.set_xlabel('Localization Error (arcsec)')
ax.set_ylabel('Number of FRBs')
ax.set_title('Localization Error Distribution')
ax.legend()
plt.tight_layout()
plt.show()
Host Galaxy Properties
[9]:
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
# Host magnitude distribution
ax = axes[0]
ax.hist(assignments['mag'], bins=30, alpha=0.7, edgecolor='black', color='#2ca02c')
ax.axvline(assignments['mag'].median(), color='red', linestyle='--', linewidth=2,
label=f"Median: {assignments['mag'].median():.2f}")
ax.set_xlabel('Host Magnitude (m_r)')
ax.set_ylabel('Number of FRBs')
ax.set_title('Host Galaxy Magnitude Distribution')
ax.legend()
# Offset vs magnitude
ax = axes[1]
ax.scatter(assignments['mag'], assignments['gal_off'], alpha=0.5, s=20, color='#1f77b4')
ax.set_xlabel('Host Magnitude (m_r)')
ax.set_ylabel('Galaxy Offset (arcsec)')
ax.set_title('Offset vs Host Magnitude')
ax.grid(alpha=0.3)
plt.tight_layout()
plt.show()
Step 5: Visualize FRB-Host Associations
Let’s visualize a few example FRB-host associations, showing:
Galaxy center (blue)
True FRB position in galaxy (green)
Observed FRB position with localization error (red)
Localization error ellipse
[10]:
def plot_frb_host_association(assignments, galaxies, idx, ax=None):
"""
Plot a single FRB-host association showing galaxy, true position,
observed position, and localization ellipse.
"""
if ax is None:
fig, ax = plt.subplots(figsize=(8, 8))
# Get FRB and galaxy data
frb = assignments.iloc[idx]
galaxy = galaxies[galaxies['ID'] == frb['gal_ID']].iloc[0]
# Convert to offset from galaxy center (in arcsec)
# Approximate: 1 deg ≈ 3600 arcsec
true_ra_off = (frb['true_ra'] - galaxy['ra']) * 3600 * np.cos(np.radians(galaxy['dec']))
true_dec_off = (frb['true_dec'] - galaxy['dec']) * 3600
obs_ra_off = (frb['ra'] - galaxy['ra']) * 3600 * np.cos(np.radians(galaxy['dec']))
obs_dec_off = (frb['dec'] - galaxy['dec']) * 3600
# Plot galaxy center
ax.plot(0, 0, 'o', markersize=15, color='blue', label='Galaxy center', zorder=3)
# Plot galaxy half-light circle
circle = plt.Circle((0, 0), galaxy['half_light'], color='blue',
fill=False, linestyle='--', linewidth=2, alpha=0.5)
ax.add_patch(circle)
# Plot true FRB position
ax.plot(true_ra_off, true_dec_off, '*', markersize=20, color='green',
label='True FRB position', zorder=4)
# Plot observed FRB position
ax.plot(obs_ra_off, obs_dec_off, '*', markersize=20, color='red',
label='Observed FRB position', zorder=5)
# Plot localization error ellipse
ellipse = Ellipse((obs_ra_off, obs_dec_off),
width=2*frb['a'], height=2*frb['b'],
angle=frb['PA'],
edgecolor='red', facecolor='none',
linewidth=2, linestyle=':', alpha=0.7)
ax.add_patch(ellipse)
# Connect true and observed positions
ax.plot([true_ra_off, obs_ra_off], [true_dec_off, obs_dec_off],
'k--', linewidth=1, alpha=0.5, zorder=2)
# Set limits based on max offset
max_offset = max(abs(true_ra_off), abs(true_dec_off),
abs(obs_ra_off), abs(obs_dec_off)) * 1.5
max_offset = max(max_offset, 2.0) # At least 2 arcsec
ax.set_xlim(-max_offset, max_offset)
ax.set_ylim(-max_offset, max_offset)
ax.set_xlabel('ΔRA (arcsec)', fontsize=12)
ax.set_ylabel('ΔDec (arcsec)', fontsize=12)
ax.set_title(f'FRB {idx}: m_r={frb["mag"]:.1f}, '
f'gal_off={frb["gal_off"]:.2f}", loc_off={frb["loc_off"]:.2f}"',
fontsize=11)
ax.legend(fontsize=10)
ax.grid(alpha=0.3)
ax.set_aspect('equal')
return ax
# Plot 4 example associations
fig, axes = plt.subplots(2, 2, figsize=(14, 14))
axes = axes.flatten()
# Select FRBs with different characteristics
example_indices = [
0, # First FRB
np.argmax(assignments['gal_off'].values), # Largest galaxy offset
np.argmax(assignments['loc_off'].values), # Largest localization error
len(assignments) // 2 # Middle FRB
]
for i, idx in enumerate(example_indices):
plot_frb_host_association(assignments, galaxies, idx, ax=axes[i])
plt.tight_layout()
plt.show()
Step 6: Comparing Different Localizations
Different surveys have different localization capabilities. Let’s compare CHIME, DSA, and ASKAP-like localizations.
[12]:
# Generate FRBs for different surveys
frbs_surveys = {
'CHIME': generate_frbs(300, 'CHIME', seed=42),
'DSA': generate_frbs(300, 'DSA', seed=42),
'ASKAP': generate_frbs(300, 'ASKAP', seed=42)
}
# Define survey-specific localizations
localizations = {
'CHIME': (0.1, 0.1, 45.), # ~0.1" × 0.1" ellipse (HCO)
'DSA': (1.0, 1.0, 0.), # ~1.0" circular (less precision)
'ASKAP': (0.5, 0.5, 30.) # ~0.5" × 0.5" circular (medium)
}
# Assign FRBs to hosts for each survey
assignments_surveys = {}
for survey in ['CHIME', 'DSA', 'ASKAP']:
print(f"\nAssigning {survey} FRBs...")
assignments_surveys[survey] = assign_frbs_to_hosts(
frb_df=frbs_surveys[survey],
galaxy_catalog=galaxies,
localization=localizations[survey],
mag_range=(17., 28.),
seed=42
)
print(f" Assigned {len(assignments_surveys[survey])} FRBs")
Loading P(DM,z) grid from /home/xavier/Projects/FRB/FRB/frb/data/DM/CHIME_pzdm.npz
Sampling DM values
Sampling redshifts
Sampling host galaxy absolute magnitudes
Using Lz values to sample host galaxy absolute magnitudes
Loading P(DM,z) grid from /home/xavier/Projects/FRB/FRB/frb/data/DM/DSA_pzdm.npz
Sampling DM values
Sampling redshifts
Sampling host galaxy absolute magnitudes
Using Lz values to sample host galaxy absolute magnitudes
Loading P(DM,z) grid from /home/xavier/Projects/FRB/FRB/frb/data/DM/CRAFT_class_I_and_II_pzdm.npz
Sampling DM values
Sampling redshifts
Sampling host galaxy absolute magnitudes
Using Lz values to sample host galaxy absolute magnitudes
Assigning CHIME FRBs...
Assigning 269 FRBs to hosts (filtered from 300)
Iteration 1: 269 FRBs remaining
Brightest unassigned FRB: m_r = 17.13 deg
Max magnitude separation: 0.0001 deg deg
Assignment complete after 1 iterations
Generating FRB positions within galaxies...
Applying localization error...
Assigned 269 FRBs
Assigning DSA FRBs...
Assigning 266 FRBs to hosts (filtered from 300)
Iteration 1: 266 FRBs remaining
Brightest unassigned FRB: m_r = 17.08 deg
Max magnitude separation: 0.0001 deg deg
Iteration 2: 1 FRBs remaining
Brightest unassigned FRB: m_r = 22.44 deg
Assignment complete after 2 iterations
Generating FRB positions within galaxies...
Applying localization error...
Assigned 266 FRBs
Assigning ASKAP FRBs...
Assigning 218 FRBs to hosts (filtered from 300)
Iteration 1: 218 FRBs remaining
Brightest unassigned FRB: m_r = 17.01 deg
Max magnitude separation: 0.0001 deg deg
Assignment complete after 1 iterations
Generating FRB positions within galaxies...
Applying localization error...
Assigned 218 FRBs
Compare Localization Performance
[13]:
print("Localization Performance Comparison")
print("=" * 70)
print(f"{'Survey':<10} {'Ellipse (a×b)':<20} {'Med gal_off':<15} {'Med loc_off':<15}")
print("-" * 70)
for survey in ['CHIME', 'DSA', 'ASKAP']:
loc = localizations[survey]
assign = assignments_surveys[survey]
print(f"{survey:<10} {loc[0]:.1f}\" × {loc[1]:.1f}\""
f"{'':>6} {assign['gal_off'].median():.3f}\""
f"{'':>6} {assign['loc_off'].median():.3f}\"")
Localization Performance Comparison
======================================================================
Survey Ellipse (a×b) Med gal_off Med loc_off
----------------------------------------------------------------------
CHIME 0.1" × 0.1" 0.276" 0.117"
DSA 1.0" × 1.0" 0.243" 1.119"
ASKAP 0.5" × 0.5" 0.340" 0.567"
[14]:
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
colors = ['#1f77b4', '#ff7f0e', '#2ca02c']
for ax, (survey, color) in zip(axes, zip(['CHIME', 'DSA', 'ASKAP'], colors)):
assign = assignments_surveys[survey]
# Plot both offsets
bins = np.linspace(0, 2.0, 30)
ax.hist(assign['gal_off'], bins=bins, alpha=0.5, label='Galaxy offset',
color=color, edgecolor='black')
ax.hist(assign['loc_off'], bins=bins, alpha=0.5, label='Localization error',
color='red', edgecolor='black')
ax.set_xlabel('Offset (arcsec)')
ax.set_ylabel('Number of FRBs')
ax.set_title(f'{survey}\n'
f'({localizations[survey][0]:.1f}" × {localizations[survey][1]:.1f}")')
ax.legend()
ax.set_xlim(0, 2.0)
plt.suptitle('Offset Distributions by Survey', fontsize=14, y=1.02)
plt.tight_layout()
plt.show()
Step 7: Effect of Scale Parameter
The scale parameter controls how concentrated FRBs are near galaxy centers.
Smaller scale → more concentrated
Larger scale → more spread out
[15]:
# Compare different scale parameters
scales = [0.5, 1.0, 2.0]
assignments_scale = {}
for scale in scales:
print(f"Assigning with scale={scale}...")
assignments_scale[scale] = assign_frbs_to_hosts(
frb_df=frbs_surveys['CHIME'][:200], # Use subset for speed
galaxy_catalog=galaxies,
localization=(0.5, 0.3, 45.),
scale=scale,
mag_range=(17., 28.),
seed=42
)
Assigning with scale=0.5...
Assigning 181 FRBs to hosts (filtered from 200)
Iteration 1: 181 FRBs remaining
Brightest unassigned FRB: m_r = 17.13 deg
Max magnitude separation: 0.0001 deg deg
Assignment complete after 1 iterations
Generating FRB positions within galaxies...
Applying localization error...
Assigning with scale=1.0...
Assigning 181 FRBs to hosts (filtered from 200)
Iteration 1: 181 FRBs remaining
Brightest unassigned FRB: m_r = 17.13 deg
Max magnitude separation: 0.0001 deg deg
Assignment complete after 1 iterations
Generating FRB positions within galaxies...
Applying localization error...
Assigning with scale=2.0...
Assigning 181 FRBs to hosts (filtered from 200)
Iteration 1: 181 FRBs remaining
Brightest unassigned FRB: m_r = 17.13 deg
Max magnitude separation: 0.0001 deg deg
Assignment complete after 1 iterations
Generating FRB positions within galaxies...
Applying localization error...
[16]:
fig, ax = plt.subplots(figsize=(10, 6))
colors = ['#1f77b4', '#ff7f0e', '#2ca02c']
for scale, color in zip(scales, colors):
assign = assignments_scale[scale]
ax.hist(assign['gal_off'], bins=30, alpha=0.5, label=f'scale={scale}',
color=color, edgecolor='black', density=True)
ax.set_xlabel('Galaxy Offset (arcsec)')
ax.set_ylabel('Density')
ax.set_title('Effect of Scale Parameter on Galaxy Offset Distribution')
ax.legend()
ax.grid(alpha=0.3)
plt.tight_layout()
plt.show()
# Print statistics
print("\nMedian galaxy offsets:")
for scale in scales:
median_off = assignments_scale[scale]['gal_off'].median()
print(f" scale={scale}: {median_off:.3f}\"")
Median galaxy offsets:
scale=0.5: 0.250"
scale=1.0: 0.500"
scale=2.0: 0.766"
Step 8: Magnitude Matching Verification
The assignment algorithm matches FRBs to galaxies by magnitude. Let’s verify that brighter FRBs are indeed assigned to brighter galaxies.
[17]:
# Get original FRB magnitudes (before assignment)
# Match by FRB_ID
frb_mags = []
host_mags = []
ss = 0
for _, row in assignments.iterrows():
#print(ss)
frb_mag = frbs.iloc[int(row['FRB_ID'])]['m_r']
frb_mags.append(frb_mag)
host_mags.append(row['mag'])
ss += 1
frb_mags = np.array(frb_mags)
host_mags = np.array(host_mags)
# Calculate correlation
correlation = np.corrcoef(frb_mags, host_mags)[0, 1]
print(f"Correlation between FRB m_r and host m_r: {correlation:.3f}")
print("(Should be close to 1.0, indicating successful magnitude matching)")
Correlation between FRB m_r and host m_r: 1.000
(Should be close to 1.0, indicating successful magnitude matching)
[18]:
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
# Scatter plot
ax = axes[0]
ax.scatter(frb_mags, host_mags, alpha=0.5, s=20)
ax.plot([17, 28], [17, 28], 'r--', linewidth=2, label='Perfect match')
ax.set_xlabel('FRB Expected Host Magnitude (m_r)')
ax.set_ylabel('Assigned Host Magnitude')
ax.set_title(f'Magnitude Matching\n(correlation = {correlation:.3f})')
ax.legend()
ax.grid(alpha=0.3)
ax.set_aspect('equal')
# Difference histogram
ax = axes[1]
mag_diff = host_mags - frb_mags
ax.hist(mag_diff, bins=40, alpha=0.7, edgecolor='black')
ax.axvline(0, color='red', linestyle='--', linewidth=2, label='Perfect match')
ax.axvline(mag_diff.mean(), color='orange', linestyle='--', linewidth=2,
label=f'Mean: {mag_diff.mean():.3f}')
ax.set_xlabel('Magnitude Difference (assigned - expected)')
ax.set_ylabel('Number of FRBs')
ax.set_title('Magnitude Matching Error')
ax.legend()
ax.grid(alpha=0.3)
plt.tight_layout()
plt.show()
print(f"\nMagnitude matching statistics:")
print(f" Mean difference: {mag_diff.mean():.3f} mag")
print(f" Median difference: {np.median(mag_diff):.3f} mag")
print(f" Std deviation: {mag_diff.std():.3f} mag")
Magnitude matching statistics:
Mean difference: -0.002 mag
Median difference: 0.000 mag
Std deviation: 0.027 mag
Step 9: Saving Results
Save the assignment results for later use or PATH analysis.
[19]:
# Save to CSV
# assignments.to_csv('frb_host_assignments.csv', index=False)
print("Example: Save to CSV")
print(" assignments.to_csv('frb_host_assignments.csv', index=False)")
# Save to Parquet (more efficient for large datasets)
# assignments.to_parquet('frb_host_assignments.parquet', index=False)
print("\nExample: Save to Parquet")
print(" assignments.to_parquet('frb_host_assignments.parquet', index=False)")
print("\nOutput columns:")
for col in assignments.columns:
print(f" - {col}")
Example: Save to CSV
assignments.to_csv('frb_host_assignments.csv', index=False)
Example: Save to Parquet
assignments.to_parquet('frb_host_assignments.parquet', index=False)
Output columns:
- ra
- dec
- true_ra
- true_dec
- gal_ID
- gal_off
- mag
- half_light
- loc_off
- FRB_ID
- a
- b
- PA
Summary
This notebook demonstrated the complete workflow for assigning FRBs to host galaxies:
Generate FRBs: Create realistic FRB populations with
generate_frbs()Load catalog: Use real galaxy catalog (HSC/DECaLs/HECATE) if available, otherwise mock
Assign hosts: Use
assign_frbs_to_hosts()with appropriate localizationAnalyze results: Examine offset distributions and host properties
Compare surveys: Test different localization scenarios
Verify matching: Confirm magnitude-based assignment worked correctly
Using Real Data
This notebook automatically uses the real combined HSC/DECaLs/HECATE galaxy catalog if available:
Set environment variable:
export FRB_APATH=/path/to/data/CatalogsCatalog file:
combined_HSC_DECaLs_HECATE_galaxies_hecatecut.parquetContains ~2.3 million galaxies from real surveys
Falls back to mock catalog if not available
Key Parameters
localization:
(a, b, PA)- Error ellipse defining localization precisionmag_range:
(min, max)- Magnitude range for FRB filteringscale: Controls galaxy offset concentration (default: 2.0)
seed: Random seed for reproducibility
Output
The assignments DataFrame contains:
Observed and true FRB coordinates
Host galaxy properties
Offset statistics
Localization parameters
Next Steps
Run PATH analysis on the assigned FRBs
Compare assigned hosts with PATH posteriors
Study systematic biases in host association
Test different survey strategies
Apply to real FRB observations
[ ]: