ASTR 596 Fall 2025
Instructor: Anna Rosen
Due: Wednesday, November 26, 2025 by 11:59 PM
Overview¶
In Project 2, you built an N-body simulator in NumPy. It worked, but it was slow — simulating 500 particles probably took an hour per run. Now imagine you’re a researcher generating thousands of simulations to train a machine learning model (your final project). With Project 2’s code, that’s impossible. With JAX, it’s feasible.
Beyond speed, this project teaches you to build research-grade tools, not just scripts:
Python package structure with modules and
__init__.pyFunctional programming patterns used in modern scientific computing
Reproducible science (explicit random number generation, version control)
The transformation: From writing scripts that work once on your laptop to building organized, importable packages that work reliably for anyone.
Goals:
10-100× speedup vs. pure-Python Project 2 (nested loops)
Speedups vs. vectorized NumPy on CPU are workload-dependent; you must benchmark and report. Aim for a measurable win after JIT warmup.
JIT compilation and vectorization for significant performance gains
Reproducible simulations with JAX’s functional random number generation
Python package structure with modular organization
Foundation for your final ML project (fast ground truth generation)
Academic Integrity: Hints in this document describe algorithms and patterns; do not copy scaffolds from the internet or LLMs. Your code must be your own implementation.
Learning Objectives¶
Transform NumPy → JAX (
jit,vmap,grad)Use
jax.lax.scanfor timestepping andjax.lax.while_loopfor bounded rejection samplingImplement pure functional integrators
Generate and evolve virialized star clusters ()
Design, test, and document a scientific Python package
Profile and optimize performance
Critical Configuration¶
Part 1: Core Implementation¶
Getting started tip: A good way to familiarize yourself with JAX is to start by repurposing your Project 2 IMF and Plummer sampling modules. Convert them to use jax.random and jax.numpy, JIT-compile them, and validate they produce the same distributions. This gives you a low-stakes way to learn JAX’s functional programming patterns before tackling the more complex integrator.
Step 1: Initial Conditions in JAX¶
Critical: Clusters must start in virial equilibrium to avoid artificial dynamics.
1.1 Kroupa IMF Sampler¶
Two-segment broken power law:
Inverse-CDF for segment with slope :
Requirements:
Use
jax.random.PRNGKeyandjax.random.split(no global RNG)JIT-compile with
@jitSample exactly stars
Default limits: , (configurable)
Note: is conventional for star clusters (very massive stars are rare and short-lived)
Validate: Recover slopes from log-binned histogram
JAX control flow reminder: Inside @jit, avoid Python for/while loops on traced arrays. Use jax.lax.scan for time-stepping loops and jax.lax.while_loop for bounded rejection sampling or fixed-point iterations. These compile to efficient XLA control flow.
1.2 Plummer Sphere Positions¶
The Plummer sphere is a spherically symmetric stellar system with density profile:
where is the Plummer scale length (characteristic radius).
Radius inverse-CDF:
Angles (uniform on sphere):
Requirements:
Re-center:
Validate: Density profile matches
1.3 Virial Equilibrium Velocities¶
Goal: Initialize with virial parameter (equilibrium)
The virial parameter (or virial ratio ) is defined as:
where is total kinetic energy and is gravitational potential energy (negative).
Physical meaning:
: Virial equilibrium (), bound and stable
: Super-virial (unbound, will expand)
: Sub-virial (will contract)
Your clusters must start at to avoid artificial dynamics.
Plummer Equilibrium Velocities (Exact Distribution Function) — REQUIRED:
At radius with escape speed , draw a dimensionless speed from:
This comes from Plummer’s exact distribution function (Dejonghe 1987). The peak is at .
Acceptance-Rejection Implementation:
For star at radius , compute and
Use
jax.lax.while_loopwith boundedmax_trials(e.g., 256) to implement rejection samplingDraw and accept/reject against
Use a constant envelope with
On failure after
max_trials: Fallback deterministically to (the mode) and increment a counterRecord the fraction of particles that used the fallback — it should be . If not, tighten your envelope
Direction: Sample uniform on sphere
Compute
Vectorize with
vmapover all particles
Important JAX note: Rejection sampling with while_loop requires careful state management. Your loop must carry the accepted value, current attempt count, and a boolean indicating whether to continue.
Critical RNG requirement: Carry a PRNG key in your state and split inside while_loop/scan. No global RNG or hidden state—JAX’s functional random number generation requires explicit key threading through all operations.
Verification:
Clusters should achieve after this initialization
Re-center velocities:
Optional: Maxwellian Approximation (for comparison)
If you want to compare methods, you can also implement:
Draw components from . This is an approximation to the exact distribution but is much simpler and still produces roughly virial equilibrium.
Step 2: Gravitational Force Calculation¶
Physics: The gravitational acceleration on particle is:
where is a softening length that prevents singularities in close encounters.
Why softening? Real stellar systems have finite-size stars. In simulations, point masses can get arbitrarily close, causing:
Numerical instability ()
Unrealistically large energy changes from two-body encounters
Softening models the smoothing effect of finite stellar radii. For star clusters, typical values: pc, or adaptively: where is the half-mass radius.
Deliverable for report: State your policy (constant vs. ), the chosen value/factor , and provide a 2-line justification tied to energy drift and timestep collapse rates observed in your simulations.
Two Implementation Strategies¶
Strategy A: Direct Vectorization (Pairwise Distance Matrix).
Implement a function accelerations_matrix(x, m, eps2, G) that:
Builds all pairwise separations
Computes softened distances for all pairs
Zeroes the self-interaction terms (diagonal)
Returns accelerations
Critical: Weight each term by the mass of the source particle
Requirements:
Fully vectorized (no Python loops)
JIT-compiled with
@jax.jitFloat64 precision
Memory: temporaries
Hint: Your validation tests will check force symmetry and agreement with Strategy B to tight tolerances — this will force you to get the broadcasting and mass weighting exactly right.
Pros: Straightforward vectorization, easier to reason about
Cons: Memory: temporaries; not scalable to very large
When to use: Small-to-medium (< 1000), learning JAX fundamentals
Strategy B: vmap Over Particles
Implement a function accelerations_vmap(x, m, eps2, G) by mapping a single-particle force function over all particles.
Design considerations:
Write a helper that computes force on one particle due to all others
Exclude self-interaction by index, not by float equality — your tests will catch float comparison errors
Use
jax.vmapto vectorize over all particlesMust match Strategy A within
rtol=1e-12, atol=1e-14in float64
Deliverable for report: Compare accelerations from both strategies on the same positions/masses and assert max_rel_err < 1e-12. Submit a short table showing measured error and wall time for N=32, 64, 128.
Requirements:
JIT-compiled
Float64 precision
Agreement with Strategy A verified by tests
Pros: Lower peak memory ( per particle), composable with other transformations like grad
Cons: Slightly more complex logic, requires understanding vmap semantics
When to use: Want to learn vmap, planning to scale to larger , or composing with grad for differentiable physics
Implementation Strategy: Implement both strategies (matrix-based and vmap-based)
Compare memory usage and performance
Ensure they produce identical results (see Validation)
Wrap in
@jitfor production use
Softening guidance:
Use pc for validation tests
For realistic clusters, estimate (half-mass radius) and use
Document your choice
Step 3: Time Integration¶
Leapfrog (DKD) Integration — REQUIRED
The leapfrog integrator is second-order symplectic, meaning it exactly conserves a “shadow Hamiltonian” close to the true Hamiltonian. This gives excellent long-term energy conservation.
DKD Algorithm (Drift-Kick-Drift):
Drift:
Kick:
Drift:
Why leapfrog?
Symplectic: Preserves phase-space volume (Liouville’s theorem)
Time-reversible: Run backward to recover initial conditions (within numerical precision)
Excellent energy conservation: Typical or better for sufficiently small
Implementation Requirements:
Implement two functions following the DKD equations above:
leapfrog_step(state, dt, params)→new_statePure function (no mutation)
state = (x, v),paramscontains masses, softening, etc.Returns updated
(x_new, v_new)
integrate(state0, dt, n_steps, params)→(state_final, trajectory)Use
jax.lax.scanfor the timestepping loop (not Pythonfor)Returns final state and trajectory of states at each timestep
Requirements:
JIT-compile both functions
Use
jax.lax.scan(not Python loops) for timesteppingFloat64 precision
Deliverable: Show that by plotting error vs. timestep
Step 4: Adaptive Timestepping¶
Fixed timesteps are inefficient: During expansion, you can take large steps. During close encounters, you need small steps.
Criterion: Choose such that:
where is the Courant factor.
Physical meaning: Limit timestep so no particle moves more than under its current acceleration.
Implementation Requirements:
Compute at each integration step based on maximum acceleration
Clamp: to avoid runaway
Integrate adaptive into your
lax.scanloop (carry updateddtin state)
Validation: Plot over a simulation. It should:
Decrease during collapse/close encounters (high )
Increase during expansion (low )
Never reach or for reasonable parameters
Discussion point for report: Compare energy conservation and runtime for fixed vs. adaptive timesteps.
Step 5: Diagnostic Utilities¶
Implement helper functions for energy analysis and validation:
Required Functions:
kinetic_energy(v, m)→KTotal kinetic energy of the system
potential_energy(x, m, eps2, G)→WTotal gravitational potential energy (negative)
Formula:
Critical: Avoid double-counting pairs (each pair counted once)
Must handle softening correctly and include gravitational constant explicitly
total_energy(x, v, m, eps2, G)→(K, W, E)Returns kinetic, potential, and total energy
virial_ratio(K, W)→α_virComputes virial parameter
Requirements:
All functions JIT-compiled
Float64 precision
Numerically stable
Test Case (validates your implementation):
Two-body system with specific setup should give: , (in your units),
Use this to verify you’re not double-counting potential energy
Usage: These standardize your energy diagnostics and make plotting/validation consistent. Use them in your validation tests and include virial ratio plots in your technical report.
Part 2: Production Pipeline¶
Goal: Generate an ensemble of 10 simulations. (This production pipeline will generate the training data for your final ML project for a much larger ensemble with specific parameter ranges.)
Requirements¶
Parameter sweep:
Vary
Vary (Plummer scale length) over range
All clusters start at
Automated pipeline:
A simple Python script (e.g.,
run_ensemble.py) that generates all simulationsExplicit random seed management (log seeds for reproducibility)
Can be sequential (no need for complex parallelization)
Data storage:
Organized directory structure (e.g.,
sim_0000/,sim_0001/, ...)Each simulation saves:
Initial conditions:
Trajectories: at snapshots
Metadata: , , , , seed
Diagnostics: , ,
Format: NumPy
.npzfiles (simple and efficient) or HDF5 if you prefer. Document your choice in the report.Make sure you include a .gitignore to avoid committing large data files to your repo
Statistical validation:
Create a single summary plot showing your ensemble is physically consistent
Example: Energy conservation histogram across all runs, or distribution
Quantify: “95% of simulations achieve ”
Suggested Workflow¶
Your pipeline should:
Define parameter grid: Vary , Plummer scale over range, total mass via IMF sampling
Manage random seeds: Use a master seed, split for each simulation, log all seeds for reproducibility
Generate initial conditions: For each parameter combination, create virialized cluster
Integrate: Run simulation, save trajectories and diagnostics
Organize output: Save to structured directories with metadata
Statistical validation: Aggregate results and verify ensemble consistency
Implementation tips:
Use
itertools.productor nested loops to generate parameter combinationsSave incrementally (don’t wait until all runs complete)
Include progress logging so you know how many simulations are done
Keep it simple—a straightforward loop over parameters is perfectly fine
Part 3: Performance Analysis¶
Benchmarking Requirements¶
Force calculation comparison:
Matrix-based vs. vmap: Time both implementations for
Plot: runtime vs. on log-log scale
Quantify: Which is faster? When does the crossover occur?
JAX vs. NumPy speedup:
Compare your JAX implementation to your Project 2 vectorized NumPy code
Fair comparison: Same force calculation strategy, same timestep, measure after JIT warmup
Quantify: “JAX is Xfactor faster than NumPy for N=500”
Note: Speedups depend strongly on your Project 2 implementation quality. The order-of-magnitude of the runtime depends on your machine—measure and report on your hardware.
JIT warmup:
Measure first call (includes compilation) vs. subsequent calls
Demonstrate the cost of compilation is amortized over many runs
Scaling analysis:
Plot: total integration time vs. for fixed and adaptive
Quantify: How does your integrator scale? ? Better?
Profiling¶
Use jax.profiler.trace to identify bottlenecks. This generates a Chrome trace file that visualizes where time is spent (compilation, execution, data transfer).
Steps:
Wrap your integration call with
jax.profiler.trace(output_path)Load the trace in Chrome at
chrome://tracingAnalyze: Is force calculation JIT-compiled? Does it dominate runtime? Any unexpected Python overhead?
Report: Include one profiling screenshot showing your force calculation is JIT-compiled and dominates runtime (not Python overhead).
Validation Requirements¶
Required tests (implement at least these 6):
1. Two-body orbit:
Set up circular or elliptical orbit with known analytic solution
Integrate for 1000 steps
Verify: trajectories match theory, energy conserved to over 103 steps at sufficiently small . Show the integrator exhibits error scaling.
2. Force method agreement:
Generate a random cluster (N=100)
Compute forces with both matrix and vmap strategies
Verify:
jnp.allclose(a1, a2, rtol=1e-12, atol=1e-14)in float64
3. Force symmetry:
Set up an equilateral triangle of equal masses
Verify zero net force at the centroid, or verify pairwise force antisymmetry:
4. Units sanity check:
With , place two unit masses at positions (separation )
For this check, set softening to (i.e.,
eps2 = 0.0)—with any the analytic value changesBy symmetry, each particle experiences
Verify your force calculation returns exactly this value (tests unit handling and self-exclusion)
5. Sampler validation:
IMF: Sample 104 masses, verify power-law slopes in log-binned histogram
Plummer: Sample 103 positions, compare density profile to analytical form
Velocities: Both methods (exact and approximate) should produce
6. Gradient safety (mini-check):
Define a scalar objective for a 2-body run of 10 steps
Verify
jax.grad(lambda x0: loss(run(x0)))returns finite values in float64Purpose: Ensures your integrator is differentiable (critical for final project physics-informed learning)
N-body cluster validation (included in your 50-100 simulation ensemble):
Generate realistic clusters (N=200-500, Kroupa IMF, Plummer sphere, )
Integrate for several dynamical times
Check that remains near 1 (some evolution is physical)
Verify momentum conservation
Deliverables¶
1. Code Repository¶
Structure:
Recommended directory layout (your first Python package!):
project5-jax-nbody/
├── jax_nbody/ # Your package directory
│ ├── __init__.py # Makes this a package (can be empty)
│ ├── samplers.py # IMF, Plummer sampling
│ ├── forces.py # Force calculations
│ ├── integrators.py # Leapfrog, adaptive dt
│ └── diagnostics.py # Energy, virial ratio
├── tests/
│ ├── test_forces.py # At least 6 tests total
│ ├── test_samplers.py
│ └── ...
├── quickstart.py # Demo script for grading
├── run_ensemble.py # Generate 50-100 simulations
├── data/ # Simulation outputs
│ ├── sim_0000/
│ └── ...
└── README.mdWhy this structure?
__init__.py: Makesjax_nbody/a Python package so you canimport jax_nbody.forcesModular files: Separate concerns (sampling vs. forces vs. integration)
tests/directory: Keep tests organizedScripts at root:
quickstart.pyandrun_ensemble.pyimport from your package
Example imports in your scripts:
# In quickstart.py
from jax_nbody.samplers import sample_kroupa_imf, sample_plummer_positions
from jax_nbody.forces import compute_forces_matrix
from jax_nbody.integrators import leapfrog_integrate
# Or if you organize differently:
import jax_nbody.samplers as samplersWhat you DON’T need:
❌
pyproject.tomlorsetup.py(no pip installation)❌ Complex packaging machinery
❌ Version numbers, dependencies lists
What you DO need:
✅
__init__.pyin your package directory (can be empty)✅ Organized modules with clear responsibilities
✅ Tests that import and verify your functions
✅ Scripts that use your package
Code quality (optional): Consider using ruff check jax_nbody/ for linting, but not required
Git: 20+ meaningful commits showing development progression
RNG threading: All samplers and integrators must accept and return a JAX PRNGKey, with no hidden global RNG state. Tests will fail if randomness isn’t threaded explicitly through function calls
Quickstart for grading: quickstart.py at project root that runs N=64 for 100 steps on CPU and prints: wall-time (after JIT warmup), max|ΔE/E|, and summary. I will run python quickstart.py to verify basic functionality
2. Research Memo (1-2 pages)¶
Write a concise research memo demonstrating your implementation works correctly and that you understand JAX’s performance advantages. The primary deliverable is your complete working, validated pipeline—the memo provides essential evidence and commentary.
Required Components:
Validation Evidence (≈1 page):
All 6 required test results with key figures showing correctness
Two-body orbit plot: Energy conservation ()
Virial equilibrium plot: for virialized cluster
Multi-scale validation: Show correct physics from N=2 → N=100+ → production-scale clusters
Use informative captions explaining what each plot demonstrates
Performance Analysis (≈0.5 pages):
JAX vs. NumPy speedup table: Quantified timings for representative N values
Force strategy comparison: Matrix vs. vmap measured error and wall time (N=32, 64, 128)
Brief commentary: What dominates runtime? How does performance scale with N?
Key Design Decisions (≈0.5 pages):
Softening policy: State your choice (constant vs. ), the value/factor used, and brief justification tied to energy drift and timestep stability
Integration approach: Note your adaptive timestepping strategy
Production ensemble: Brief description of automated pipeline (50-100 runs, parameter coverage, data organization)
Format: Emphasize showing results over prose. Use tables and figures effectively. Quantify performance gains. Demonstrate you understand the tradeoffs through your results, not lengthy explanations.
3. Growth Memo¶
Submit a Growth Memo following the standard template at 03-growth-memo-template.md. This is where you reflect on your learning journey, including:
Technical skills developed: What JAX patterns can you now implement?
Key challenges & solutions: What JAX-specific pitfalls did you encounter?
AI usage reflection: How did you use AI tools to understand JAX? What worked? What didn’t? How did you verify AI suggestions? This detailed reflection belongs in the Growth Memo, NOT the research memo.
Conceptual insights: How did implementing N-body dynamics in JAX deepen your understanding of functional programming and JIT compilation?
What got you excited: Any “aha” moments with JAX transformations (jit, vmap, grad)?
The Growth Memo is graded pass/fail based on honest, thoughtful reflection. See the template for detailed prompts.
4. AI as a Learning Accelerator¶
Through Projects 1-4, you’ve demonstrated solid Python expertise. Now it’s time to leverage that foundation with modern learning tools. JAX has a sharp learning curve—its functional programming paradigm and compilation constraints are fundamentally different from NumPy. But you have an advantage: you already understand the underlying physics and computational concepts.
Use AI to understand, not to generate code. You need to write your own implementation to truly learn JAX. Instead, use AI tools (e.g., ChatGPT, Claude, Gemini) to:
Explain difficult programming concepts: “What does it mean for JAX arrays to be immutable?” or “Why can’t I use Python
ifstatements in JIT-compiled functions?”Decode error messages: JAX errors can be verbose and cryptic. Paste the error and ask “What is this tracer error telling me?”
Trace issues: “My code runs but energy conservation is terrible—what are common causes in JAX?”
Compare paradigms: “I know how to do X in NumPy with mutable arrays, how does JAX’s functional approach differ conceptually?”
Learning from mistakes is how you master new frameworks. It’s common—expected, even—to encounter JAX-specific pitfalls. These errors (tracer errors, array immutability violations, JIT compilation failures) are part of the learning process. When you hit one, use AI to understand why it happened and what concept you’re missing, then fix it yourself.
Critical caveat: Maintain a “docs first” mindset. Check the JAX documentation and Equinox API before asking AI. Always fact-check AI responses—they’re often confident but wrong about version-specific details or edge cases. Use AI to understand concepts and documentation, not as a replacement for understanding.
AI usage reflection belongs in your Growth Memo (see Deliverable 3 below), not in the research memo. The Growth Memo has dedicated sections for reflecting on AI tool usage, what worked, what didn’t, and how your approach evolved.
Common Pitfalls & Debugging Guide¶
| Issue | Symptom | Cause | Solution | Lesson |
|---|---|---|---|---|
| “Array is not writable” | ValueError when trying to modify array | Tried to mutate JAX array | Use .at[].set() or build new array | JAX arrays are values, not containers |
| “Tracer error during JIT” | TypeError with abstract tracer | Python if statement on traced value | Use jax.lax.cond() for conditionals, jax.lax.while_loop() for loops, or restructure without data-dependent control flow | JIT traces data flow, not control flow |
| Recompiling on every call | Slow even after warmup | Shape-changing arrays or data-dependent control flow | Ensure jitted functions have static shapes; avoid constructing arrays whose shape depends on runtime data inside jitted loops. Actionable: Preallocate and carry fixed-size states in scan—do not reallocate inside scan. | Static shapes required for compilation |
| First run very slow, then fast | Long pause on first call | JIT compilation overhead | Normal behavior—first call compiles, subsequent calls use compiled code | Warmup before timing |
| Energy drift worse than NumPy | Using float32 (JAX default) | Set jax.config.update("jax_enable_x64", True) at top of module | Precision matters for physics | |
| Non-reproducible results | Different answers each run | Using Python random instead of jax.random | Always use jax.random.PRNGKey and explicit splits | JAX manages randomness explicitly |
| initially | Cluster immediately expands/collapses | Velocity initialization wrong | Check acceptance-rejection or velocity dispersion formula carefully | Initial conditions are physics |
| Tests pass, simulation explodes | Insufficient validation coverage | Only tested N=2, not realistic clusters | Validate with N=100+ cluster for several dynamical times | Multi-scale validation essential |
| Code hangs during integration | Timestep → 0 | Very close encounters, too small | Use appropriate softening: | Softening prevents numerical issues |
| Massive memory usage | Out of memory error | Creating arrays without JIT | Ensure force calculation is JIT-compiled; check for memory leaks in loops | JIT optimizes memory |
Debugging Strategy¶
When something goes wrong (and it will):
Start small: Does your code work for N=2? N=3? If not, fix that first.
Check conservation laws: Energy, momentum, angular momentum are your physics smoke detectors
Visualize: Plot trajectories, , — your eyes catch patterns tests miss
Compare: Does your JAX code give the same answer as Project 2 NumPy code for identical initial conditions?
Bisect: Comment out half your code. Which half has the bug?
Read error messages carefully: JAX errors are verbose but informative
Most common mistake: Assuming initial conditions are correct. Always validate IMF slopes, Plummer density, and before running long integrations.
Essential JAX Functions & Tools¶
Core JAX¶
| Function | Purpose |
|---|---|
jax.jit | JIT compilation for speed |
jax.vmap | Vectorize over batch dimension |
jax.grad | Automatic differentiation |
jax.lax.scan | Efficient loops for timestepping |
jax.lax.while_loop | Bounded loops (e.g., rejection sampling) |
jax.random.PRNGKey | Create random seed |
jax.random.split | Split RNG state |
jax.random.uniform | Uniform distribution |
jax.random.normal | Normal distribution |
jax.numpy (as jnp) | NumPy replacement |
Note: jax.lax.scan and jax.lax.while_loop are critical for JIT-compiled loops—do not use Python for or while inside jitted functions. Additionally, when compiled jax.lax.scan is preferred for fixed-iteration loops (like timestepping) and is faster than jax.lax.while_loop and jax.lax.ifor_loop, while jax.lax.while_loop is for data-dependent loops (like rejection sampling).
Performance Tools¶
| Tool | Purpose |
|---|---|
jax.profiler.trace | Profile execution |
%timeit (IPython) or import time | Benchmark with warmup |
time.time() | Manual timing |
Optional Tools¶
| File/Tool | Purpose |
|---|---|
pytest | Testing framework (optional, but tests required) |
ruff | Linting and formatting (recommended) |
pyproject.toml | Package metadata (only if you want installable package) |
Grading Approach¶
Evaluation Method: Your grade is based on your working validated pipeline (code quality), research memo, quickstart demo, and validation evidence.
| Component | Weight | Evaluation Method |
|---|---|---|
| Code Quality & Working Pipeline | 40% | Repository inspection: JAX patterns correct, tests present, clean structure, documentation, functional implementation |
| Quickstart Demo | 20% | Does quickstart.py run and produce correct physics (energy conservation, )? |
| Research Memo | 20% | Essential validation plots with captions, JAX speedup analysis, design justification |
| Validation Evidence | 20% | Memo shows all 6 required tests passed with figures/tables proving correctness |
| Growth Memo | Pass/Fail | Honest, thoughtful reflection on learning process (graded separately) |
Note: Grading will be based on your quickstart demo output, repository code reading, and research memo. I will not be installing your package or running your full test suite—your memo must provide convincing evidence that your implementation works correctly. The Growth Memo is graded separately as pass/fail.
Looking Ahead: Final Project¶
The automated simulation pipeline you built is the foundation for your final ML project:
Training data generation: Your 50-100 simulations are a prototype. For the final project, you’ll scale to 1000+ simulations to train Gaussian Processes and Neural Networks
Fast ground truth: JAX’s 10-100× speedup makes this computationally feasible
Differentiable physics: JAX’s
gradenables physics-informed learning (gradients through entire simulations)Research-grade infrastructure: Professional package + automated pipeline = reproducible science
What’s next in the final project:
Gaussian Processes: Learn cluster evolution from your simulation ensemble
Neural Networks: Emulate N-body dynamics at 1000× speedup
Uncertainty quantification: How confident should we be in ML predictions?
The computational infrastructure you build now directly enables machine learning for astrophysics.