Project 3: Monte Carlo Radiative Transfer in a Dusty Star Cluster
ASTR 596: Modeling the Universe
Instructor: Dr. Anna Rosen
Due: Friday October 17, 2025 by 11:59 PM
Working Mode: Pair Programming
Pair Programming Assignments:
Pair 1: Caden + Kasia
Pair 2: Hy + Nodoka
Triple: Paige + Aisling + Katie
Core algorithms must be independently implemented. You may share debugging strategies, test cases, and plotting code.
Learning Objectives¶
Upon successful completion of this project, you will be able to:
Core Physics Understanding:
Explain how Monte Carlo methods exactly solve the radiative transfer equation through statistical sampling rather than approximation.
Connect discrete absorption to the exponential probability distributions inherent in radiative transfer.
Demonstrate that photon packet propagation naturally samples Beer’s law () and reproduces the formal solution.
Analyze wavelength-dependent extinction and connect it to dust grain physics (e.g., why ).
Computational Skills:
Implement Monte Carlo radiative transfer algorithms for 3D homogeneous media with multiple sources.
Apply inverse transform sampling to generate optical depths from the distribution .
Design efficient ray-marching algorithms through gridded density fields with proper boundary handling.
Construct luminosity-weighted sampling schemes for multiple stellar sources across wavelength bands.
Validation & Analysis:
Validate numerical codes against analytical solutions ( for uniform media).
Quantify Monte Carlo convergence and verify its error scaling behavior.
Demonstrate energy conservation to 0.1% precision through careful luminosity bookkeeping.
Compare multi-band escape fractions to understand differential extinction .
Scientific Interpretation:
Predict observational signatures of embedded star clusters in color-magnitude diagrams.
Interpret how dust geometry creates shadows, reddening, and differential extinction patterns.
Connect computational results to real observations (e.g., JWST’s ability to penetrate dust in IR).
Assess how stellar position within the cluster affects individual escape probabilities.
Project Overview & Scientific Context¶
Mission Brief¶
You’re modeling a young star cluster embedded in a dusty molecular cloud. Your goal: build a Monte Carlo radiative transfer code to predict how dust extinction affects the observed starlight. This project connects microscopic physics (dust absorption) to macroscopic observables (colors and extinction).
Required Background from Module 4¶
This project builds directly on Module 4’s comprehensive radiative transfer framework. Please ensure you have thoroughly reviewed the following materials from the course website’s Statistical Thinking - Module 4: From Photons to Information:
Key Concepts You Should Understand¶
Before starting this project, you should be comfortable with the following concepts:
The radiative transfer equation and its statistical interpretation
Why interaction distances follow an exponential distribution
The difference between photon packets and individual photons
How discrete absorption works (all or nothing energy deposition)
Your ZAMS models from Project 1 (Tout et al. 1996 formulae)
The Physical Picture: Monte Carlo Radiative Transfer¶
Monte Carlo Radiative Transfer (MCRT) solves the radiative transfer equation through a stochastic process, meaning it uses randomness and probability to represent physical reality. Each “photon packet” represents a bundle of photons carrying a fraction of the source luminosity. The stochastic nature manifests in two key ways: (1) the random sampling of interaction distances from an exponential distribution, and (2) the random emission directions. Through the law of large numbers, these random processes converge to the deterministic solution of the radiative transfer equation.
Discrete Absorption: In this project, you’ll implement discrete absorption where each packet either:
Deposits ALL its energy at a single interaction point (when the sampled optical depth is reached), or
Escapes with ALL its energy (if it reaches the boundary first)
There is NO gradual energy loss - it’s all or nothing! As , this stochastic Monte Carlo approach converges to the exact solution of the radiative transfer equation.
The Physical System¶
Grid Setup¶
Domain: Cubic box extending from to where pc.
Resolution: 1283 Cartesian cells (use CGS units: 1 pc = cm)
Cell size: in each dimension.
Cell indexing: For position , the cell indices are:
Where int() denotes integer division (floor function)
Star Cluster Configuration¶
5 ZAMS stars with the following properties ():
| Star | Mass () | (K) | () | () | Spectral Type |
|---|---|---|---|---|---|
| 1 | 30 | ~38,500 | ~119,000 | ~7.7 | O7V |
| 2 | 20 | ~34,000 | ~43,000 | ~6.0 | O9V |
| 3 | 10 | ~25,000 | ~5,500 | ~3.9 | B0V |
| 4 | 5 | ~17,000 | ~530 | ~2.6 | B5V |
| 5 | 2 | ~9,100 | ~16 | ~1.6 | A5V |
Note: Use your ZAMS models from Project 1 to calculate exact values.
Stellar positions: Uniformly distributed random positions within a 0.753 pc sub-volume centered at origin (this keeps stars away from boundaries to avoid edge effects)
Luminosities and temperatures: From ZAMS models
Dusty Medium¶
Density Calculations:
Number density: cm (molecular hydrogen)
Molecular weight: (for H)
Proton mass: g
Gas density: g/cm
Dust-to-gas mass ratio:
Dust density: g/cm
Dust Model: Draine (2003a,b) Milky Way dust with . All data here: https://
Data file: kext
_albedo _WD _MW _5 .5A _30 _D03
Note on units: Use CGS units as your default throughout this project.
Reading the Draine opacity file:
Skip header lines (~80 lines) when reading the file, these contain metadata
Tabulated Data columns
Tabulated quantities:
lambda = wavelength in vacuo (micron)
albedo = (scattering cross section)/(extinction cross section)
= for scattered light
C_ext/H = extinction cross section per H nucleon (cm^2/H); Note: this is per H nucleon and includes effects of both absorption and scattering.
K_abs = absorption cross section per mass of dust (cm^2/gram)
<cos^2> = <cos^(theta)> for scattered light
Important: Use the K_abs column directly for mass absorption coefficient (cm/g of dust)
Band-Averaged Opacities¶
You’ll calculate Planck mean opacities for each band by integrating the Draine data weighted by each star’s Planck function:
Why Planck mean opacity? Each star emits radiation following its Planck function . The Planck mean weights the dust opacity by the actual spectrum of light emitted by the star. This gives the effective opacity “seen” by photons from that specific star. Since hot stars emit more blue light and cool stars emit more red light, each star experiences a different effective dust opacity in each band.
Important: Calculate separate opacities for each star-band combination. A 38,500 K star will have different band-averaged opacities than a 9,100 K star, even using the same dust model, because their emission spectra weight the wavelength-dependent differently.
Tip: When creating each star, compute and store for each band in a dictionary or array as a star attribute for quick lookup during packet emission.
Primary Bands:
| Band | Wavelength Range | Central | Notes |
|---|---|---|---|
| B | 390-500 nm | 445 nm | Blue optical |
| V | 500-600 nm | 551 nm | Visual |
| K | 1.95-2.40 m | 2.19 m | Near-infrared |
Important: Convert all wavelengths to cm for CGS consistency (1 nm = 10-7 cm, 1 m = 10-4 cm)
Conversion from Draine file: The file provides the wavelength-dependent (per unit dust mass), so .
Stellar Sources and Band Luminosities¶
Update your star.py class from Project 1 to calculate band-specific luminosities. Each star emits differently in each wavelength band based on its temperature:
where the denominator equals (from integrating the Planck function over all wavelengths).
The Planck function is:
Important: Each star’s band luminosity depends on its temperature. Hot stars emit predominantly in B-band while cool stars emit more in K-band. Calculate for each star individually.
Key Physics You’ll Implement¶
Optical Depth and Absorption¶
The heart of Monte Carlo radiative transfer is determining where photons interact with dust:
Sample interaction optical depth: $ where (uniform random number between 0 and 1).
Use discrete absorption:
March packet through grid accumulating optical depth:
where is the distance traveled in the current cell.
When : deposit ALL packet luminosity at that point
If packet reaches boundary before interaction: escapes with ALL luminosity
For each band (B, V, K), each packet carries:
(sum over all stars for THIS band only). This ensures uniform Monte Carlo statistics within each band’s simulation - every packet in that band contributes equally to the error regardless of which star emits it.
Critical Point: There is no partial absorption! Each packet either deposits 100% of its energy or 0%. This binary outcome, averaged over millions of packets, reproduces the continuous radiation field.
Packet Emission Geometry¶
While stellar radii are tiny compared to the box size (), packets must start from the stellar surface:
Initial position: Uniformly sampled on stellar sphere
Sample two uniform random numbers:
Convert to spherical coordinates on the unit sphere:
(polar angle, uniform in )
(azimuthal angle, uniform in )
Position on stellar surface. Let star position be then:
Initial direction: Your independently sampled isotropic direction and will be the packet’s propagation direction (i.e., unit vector ):
Important note: Store as a photon attribute!
Packet Propagation¶
Since we’re modeling pure absorption (no scattering), photon packets travel in straight lines:
Direction doesn’t change: Once emitted with direction , the packet maintains this direction
Position update:
where is the distance to the next cell boundary or interaction point.
Boundary checking: If crosses any box boundary, the packet escapes.
Cell indexing: Update cell indices after each step to determine local density and opacity.
Cell boundary crossing: Calculate distance to next cell boundary in each dimension and take the minimum to step to the next cell.
Optical depth accumulation:
Tip: Handle the special case where a direction component is exactly zero (e.g., dx = 0) by setting the distance to that boundary to infinity rather than dividing by zero.
Expected Opacity Values¶
| Band | (cm/g) | Why This Value? |
|---|---|---|
| B | Peak dust extinction near 220 nm | |
| V | Still strong extinction | |
| K | Wavelength grain size |
These apply to dust mass, not gas mass!
Important Note: Your exact escape fraction values will differ from others due to random stellar positions within the cluster. The relative ordering must always hold, but absolute values depend on where stars are placed relative to the box boundaries.
Computational Strategy¶
Grid Resolution Recommendations¶
Initial testing: 163 or 323 grid with 103 packets (fast debugging)
Development: 323 grid with 104 packets (see trends)
Analysis: 643 grid with 105 packets (smooth statistics)
Final results: 643 or 1283 grid with packets (minimizes Monte Carlo noise to ~0.1%)
Why this strategy: Start with low resolution to debug your algorithm quickly. Once working, increase resolution to see physical trends emerge. For final science analysis, depending on performance, use high packet counts to minimize Monte Carlo noise and high resolution (643 or 1283) resolution balances memory usage with spatial resolution.
Performance Measurement¶
Before running 106 packets:
Time your code with 1000 packets
Calculate packets/second
Estimate time for full runs
If estimated time > 24 hours, profile your code to find bottlenecks (see Appendix A on using
cProfile)
Implementation Phases¶
Phase 0: Core Infrastructure - Get Your Framework Working¶
Build your modular MCRT framework with proper project structure:
project3_mcrt/
├── src/
│ ├── __init__.py
│ ├── constants.py # Copy from Project 1, ensure CGS units
│ ├── star.py # Update from Project 1 with band luminosities
│ ├── zams.py # Copy from Project 1 (Tout et al. formulae)
│ ├── utils.py # Planck function, integration utilities
│ ├── dust.py # Draine opacity processing
│ ├── grid.py # 3D grid structure (CGS units)
│ ├── photon.py # Packet propagation
│ ├── transport.py # Main MCRT engine
│ ├── detectors.py # Escape tracking, observables
│ └── mcrt_viz.py # Visualization and plotting functions
├── data/
│ └── kext_albedo_WD_MW_5.5B_30
├── outputs/
│ ├── figures/
│ └── results/
├── docs/
│ ├── research_memo.md
│ └── growth_memo.md
├── tests/
│ └── test_validation.py
├── README.md
├── requirements.txt
└── project3_analysis.py # Main analysis scriptImportant:
Copy your
constants.py,star.py, andzams.pyfrom Project 1Update
star.pyto include band luminosity calculationsEnsure all units are CGS throughout your code
Use numpy.random for random number generation
During debugging, set seed for reproducibility:
np.random.seed(42)Remove seed setting for final runs to ensure proper Monte Carlo statistics
Create utils.py with:
Planck function (Blackbody intensity):
Integration wrapper using
scipy.integrate.simpsonBand integration function to compute luminosity fractions and dust opacities
Create mcrt_viz.py with:
Functions for all required plots
2D projection maps
SED plotting
Convergence analysis plots
Validation: Verify grid indexing and coordinate transformations are correct
Phase 1: Single Band Implementation¶
Start with one band (suggest V-band), then expand:
Part A - Single Star:
Place one star at box center
Implement absorption-only transport for V-band only
Verify: for uniform medium
Check energy conservation to 0.1%
Part B - All Stars:
Add all 5 stars at random positions
Implement luminosity-weighted packet emission (still V-band only)
Verify energy conservation still holds
Generate V-band absorption map
Phase 2: Multi-Band Analysis¶
Expand to all three bands and increase packet counts:
Add B-band and K-band calculations
Run with 105 packets minimum
Create output SED showing reddening
Generate absorption maps for all bands
Analyze differential extinction
Phase 3: Science Analysis and Extensions¶
Perform convergence study with (attempt 106 if feasible)
Complete required outputs and implement extensions (see Grading Rubric).
Quick Sanity Checks¶
Before diving into analysis, verify your code with these tests:
Empty box test: Set everywhere 100% of packets should escape
Extreme opacity test: Set very high nearly 0% escape (Note: high opacity reduces runtime during debugging since packets will be absorbed quickly/close to sources)
Energy conservation: for all runs (note this may be higher for very low packet counts due to Monte Carlo noise and grid resolution).
Statistical convergence: Error should scale as
Single star centering: Star at origin in uniform medium should give
Common Debug Checks¶
If your results seem wrong:
Verify not
Check you’re using not
Confirm packets that escape are counted in
Test with one packet and print every step
Verify your random number generator is working (should give uniform distribution)
Required Outputs & Analysis for Your Research Memo¶
Opacity Validation Plot:
Validate your opacity calculations from the Draine data:
X-axis: Wavelength
Y-axis: Mass absorption coefficient
Show the Draine opacity curve for
Overlay vertical shaded bands for B, V, and K filters
Include points (
plt.scatter) or horizontal lines (plt.axhline) showing calculated band-averaged opacities
Convergence Analysis:
Plot vs for each band:
X-axis: Number of packets (103 to 106, log scale)
Y-axis: Escape fraction
Three curves: B-band, V-band, K-band
Include reference line
Spectral Energy Distribution
Show how dust reddens the star cluster:
X-axis: Wavelength (mark B, V, K centers)
Y-axis: / max()
Two curves:
Intrinsic: Combined stellar emission (sum of all 5 stars’ Planck functions)
Observed: Escaped light after dust extinction (from MCRT simulation)
Divide both curves by the maximum value of the intrinsic curve
The observed curve should be suppressed at blue wavelengths relative to red
Spatial Aborption Maps (2D Projections)
Create absorption maps for each band (B, V, K) by integrating through the full box:
$
Use log color scale to show dynamic range, choose your limits wisely.
Escape Direction Map (optional)
2D histogram showing the angular distribution of escaping light:
Use spherical coordinates for packet escape directions
Create 2D histogram with and bins (you’ll need to convert Cartesian escape directions to spherical)
Color shows escaped luminosity per solid angle
Should Reveal anisotropies due to stellar positions within the box
6. Data Table¶
| Quantity | B-band | V-band | K-band |
|---|---|---|---|
| Band-averaged opacity (cm/g) | |||
| Input luminosity () | |||
| Escaped luminosity () | |||
| Escape fraction | |||
| Mean optical depth |
Analysis Discussion for Research Memo¶
Your memo should address both the physics and computational aspects. These topics are suggestions to help guide your analysis - pursue the aspects you find most interesting in your results.
Physics Results:
How dust extinction creates spectral reddening in your SED
Why escape fractions follow K > V > B (connect to dust opacity physics)
Role of stellar position and luminosity in determining escape probabilities
Numerical Analysis:
Convergence behavior and verification of Monte Carlo error scaling
Energy conservation accuracy achieved
Computational performance and any optimizations implemented
Interpretation:
What your results reveal about observing embedded clusters
How different parameters (density, dust model, inhomogeneous medium) would affect outcomes
Critical Implementation Notes¶
Key Points for Success¶
Use dust density: where
Correct sign: Sample (negative sign critical!)
Energy tracking: Maintain separate counters for absorbed and escaped luminosity
Boundary checking: Test all 6 box faces for packet escape
Packet luminosity: All packets carry regardless of source
CGS units: Ensure all calculations use CGS units consistently
CGS Unit Conversions¶
1 pc = cm
1 = cm
1 = erg/s
1 nm = 10-7 cm, 1 m = 10-4 cm
Common Pitfalls to Avoid¶
Using gas density instead of dust density (100% — error!)
Sign error in tau sampling (packets never absorb)
Lost packets at boundaries (energy not conserved)
Variable packet luminosities (breaks statistics)
Not integrating opacities properly (missing temperature weighting)
Wrong units in wavelength conversion (nm/m to cm)
Validation Requirements¶
Before analyzing results, your code MUST pass:
Test 1: Uniform Sphere¶
Single star at center, uniform medium:
Analytical:
Numerical result must agree within Monte Carlo error
Where
Test 2: Energy Conservation¶
Test 3: Convergence Scaling¶
Standard error for
Grading Rubric¶
| Component | Points | Focus |
|---|---|---|
| Core Implementation | 40 | Correct MCRT algorithms, multi-band treatment, energy conservation |
| Code Design & Quality | 20 | Modular structure, documentation, efficiency, readability |
| Research Memo & Analysis | 25 | Physics interpretation, required visualizations, quality of discussion |
| Extension | 10 | Implementation quality and comparison with baseline |
| Validation Tests | 5 | Energy conservation, uniform sphere test, convergence verification |
Extension Requirement (Choose 1)¶
Implement ONE extension and compare results with the baseline. The list below provides ideas to get you started, but you’re encouraged to propose your own extension that interests you. If you have an idea or want to discuss feasibility, come chat with me during office hours.
Physics Extensions:
Isotropic scattering with albedo ω = 0.6 and compare escape fractions with/without scattering. (Note: this will increase runtime.)
Different dust models: Compare vs using provided Draine files
Temperature calculation: Compute dust temperature from absorbed energy using and plot the projected temperature map.
Computational Extensions:
Performance optimization using
numbaJust-in-Time (JIT) compilation (demonstrate speedup). Click here for documentation and here for a 5-minute quickstart guide.Parallel processing with
multiprocessing(run bands in parallel). Click here for documentation and here for a quick tutorial.Variance reduction: Implement forced first interaction or importance sampling
Observational Extensions:
Extended wavelength coverage: Add U, R, I bands and analyze full optical SED
Color-magnitude diagram: Plot (B-V) vs V for input and output stellar populations
Escape anisotropy: Analyze directional dependence of escaped light
Analysis Extensions:
Density parameter study: Vary from 10 to 10,000 cm
Non-uniform density: Implement profile.
Turbulent density field: Add lognormal density fluctuations to uniform medium (e.g., )
Statistical study: Multiple realizations with different random stellar positions and/or masses.
Your own idea: Propose something that connects to your research interests or explores an aspect of radiative transfer you find intriguing.
Your extension must include quantitative comparison with baseline and discussion of physical implications.
Implementation Tips¶
Before you begin coding, these practical tips will save you hours of debugging:
Performance & Efficiency:
When creating each star, compute and store for each band as a star attribute for quick lookup during packet emission - don’t recalculate millions of times!
Pre-calculate and store grid boundaries (
x_min,x_max, etc.) and cell size (Δx) as attributes to avoid recalculating them during packet propagationSave intermediate results (e.g., after every 104 packets) to avoid losing progress if your code crashes during long runs - Monte Carlo results are additive
Debugging Strategy:
Start with
kappaas a simple constant (e.g., 30000 cm²/g) before implementing the full Draine integration to isolate algorithmic bugs from opacity calculation bugsDuring debugging, use
np.random.seed(42)for reproducible results. Remove the seed for final runs to ensure proper Monte Carlo statisticsIf your calculated opacities differ significantly from expected ranges, check: (1) wavelength unit conversion to cm, (2) correct reading of dust/H mass from header, (3) proper Planck weighting with stellar temperature
Numerical Robustness:
Use DIFFERENT random numbers for position sampling (‚ ) and direction sampling ‚ ) - reusing random numbers creates unwanted correlations
Handle the special case where a direction component is exactly zero (e.g., dx = 0) by setting the distance to that boundary to infinity rather than dividing by zero
Always check that
d_next> 0 before moving packet - negative or zero distances indicate a bug in boundary calculations
Getting Started¶
Review Module 4 lecture notes
Copy your
constants.py,star.py, andzams.pyfrom Project 1Start simple: Single packet, single star, uniform medium
Build incrementally: Add complexity only after validation
Test constantly: Every new feature needs a test
What Success Looks Like¶
B-band escape fraction is lowest
K-band shows dramatically less extinction than B-band
Shadow patterns visible in absorption maps
Energy conserved to 0.1% precision
Results converge smoothly with increasing
Opacity values match expected ranges
All sanity checks pass
Appendix A: Using Python’s cProfile for Performance Analysis¶
What is cProfile?¶
cProfile is Python’s built-in profiler that measures where your code spends time. It tracks every function call and measures execution time, helping identify bottlenecks.
Basic Usage¶
Method 1 - Command line:
python -m cProfile -s cumtime project3_analysis.py > profile_output.txtThe -s cumtime sorts by cumulative time spent in each function.
Method 2: In your script:
import cProfile
import pstats
# Profile a specific function
profiler = cProfile.Profile()
profiler.enable()
# Your MCRT code here
results = run_mcrt(n_packets=1000)
profiler.disable()
# Print statistics
stats = pstats.Stats(profiler)
stats.sort_stats('cumtime')
stats.print_stats(20) # Show top 20 functionsInterpreting Output¶
bash - Unknown Directive
bash - Unknown Directivencalls tottime percall cumtime percall filename:lineno(function) 1000 0.234 0.000 45.678 0.046 transport.py:45(propagate_packet) 128000 12.345 0.000 12.345 0.000 grid.py:23(get_cell_index)
ncalls: Number of times function was called
tottime: Time spent in this function (excluding sub-functions)
cumtime: Total time in function (including sub-functions)
percall: Time per call
What to Look For¶
Functions with high cumtime - these are your bottlenecks
Functions called millions of times with small percall time - consider vectorization
Unexpected functions taking significant time - may indicate bugs
Example Optimization Workflow¶
# Before optimization: profile your code
# Identify that get_cell_index takes 30% of runtime
# Optimize that specific function
# Re-profile to verify improvementRemember: Profile before optimizing! Don’t guess where the bottlenecks are.
Appendix B: Parallel Processing for Bands¶
Optional Speedup: Band Parallelization¶
Since B, V, and K bands are independent, you can run them simultaneously for ~3× speedup using Python’s multiprocessing module (click here for docs).
Key concept: Each band runs as a separate process with its own random seed and absorption map. After all bands complete, combine the results.
Considerations:
Each process needs a different random seed for independent Monte Carlo sampling
Return results from each process rather than modifying shared objects
For small test runs (<10^4 packets), parallelization overhead may make it slower
Starting point: Look into multiprocessing.Pool and its map() function to distribute bands across cores.
This optimization is entirely optional - focus on getting correct physics first.
Appendix C: Troubleshooting Checklist¶
If your results seem incorrect, check these common issues:
f_esc = 100% for all bands: You’re using rho_gas instead of rho_dust
f_esc = 0% for all bands: Sign error in tau sampling (should be -ln(xi))
Energy off by factor of 100: Not applying dust-to-gas ratio (f_D/G = 0.01)
No convergence with N: Variable packet luminosities (all packets must carry L_total/N)
Packets disappear: Not tracking both absorbed AND escaped packets
Code extremely slow: Profile first with cProfile before optimizing
“Monte Carlo: Solving intractable integrals by rolling dice since 1946”