Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Project 5: JAX N-Body Engine

San Diego State University

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:

The transformation: From writing scripts that work once on your laptop to building organized, importable packages that work reliably for anyone.

Goals:

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


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:

ξ(m){m1.30.08m/M<0.5m2.30.5m/M100\xi(m) \propto \begin{cases} m^{-1.3} & 0.08 \le m/M_\odot < 0.5 \\ m^{-2.3} & 0.5 \le m/M_\odot \le 100 \end{cases}

Inverse-CDF for segment [a,b][a, b] with slope α1\alpha \neq 1:

m(u)=[a1α+u(b1αa1α)]1/(1α),uU(0,1)m(u) = \left[ a^{1-\alpha} + u(b^{1-\alpha} - a^{1-\alpha}) \right]^{1/(1-\alpha)}, \quad u \sim \mathcal{U}(0,1)

Requirements:

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:

ρ(r)=3Mtotal4πa3(1+r2a2)5/2\rho(r) = \frac{3M_{\rm total}}{4\pi a^3} \left(1 + \frac{r^2}{a^2}\right)^{-5/2}

where aa is the Plummer scale length (characteristic radius).

Radius inverse-CDF:

r(u)=a(u2/31)1/2,uU(0,1)r(u) = a \left( u^{-2/3} - 1 \right)^{-1/2}, \quad u \sim \mathcal{U}(0,1)

Angles (uniform on sphere):

cosθU(1,1),ϕU(0,2π)\cos\theta \sim \mathcal{U}(-1, 1), \quad \phi \sim \mathcal{U}(0, 2\pi)

Requirements:

1.3 Virial Equilibrium Velocities

Goal: Initialize with virial parameter αvir=1\alpha_{\rm vir} = 1 (equilibrium)

The virial parameter (or virial ratio QQ) is defined as:

αvir=2KW\alpha_{\rm vir} = \frac{2K}{|W|}

where KK is total kinetic energy and WW is gravitational potential energy (negative).

Physical meaning:

Your clusters must start at αvir=1±0.05\alpha_{\rm vir} = 1 \pm 0.05 to avoid artificial dynamics.

Plummer Equilibrium Velocities (Exact Distribution Function) — REQUIRED:

At radius rr with escape speed vesc(r)=2ψ(r)v_{\rm esc}(r) = \sqrt{2\psi(r)}, draw a dimensionless speed q=v/vesc[0,1]q = v/v_{\rm esc} \in [0,1] from:

f(q)q2(1q2)7/2f(q) \propto q^2 (1 - q^2)^{7/2}

This comes from Plummer’s exact distribution function (Dejonghe 1987). The peak is at q=2/30.471q^\star = \sqrt{2}/3 \approx 0.471.

Acceptance-Rejection Implementation:

  1. For star at radius rr, compute ψ(r)=GMtotalr2+a2\psi(r) = \frac{GM_{\rm total}}{\sqrt{r^2 + a^2}} and vesc=2ψv_{\rm esc} = \sqrt{2\psi}

  2. Use jax.lax.while_loop with bounded max_trials (e.g., 256) to implement rejection sampling

  3. Draw qU(0,1)q \sim \mathcal{U}(0,1) and accept/reject against f(q)q2(1q2)7/2f(q) \propto q^2 (1-q^2)^{7/2}

  4. Use a constant envelope M=f(q)M = f(q^\star) with q=2/3q^\star = \sqrt{2}/3

  5. On failure after max_trials: Fallback deterministically to q=qq = q^\star (the mode) and increment a counter

  6. Record the fraction of particles that used the fallback — it should be 1%\ll 1\%. If not, tighten your envelope

  7. Direction: Sample uniform on sphere

  8. Compute v=qvescn^\vec{v} = q v_{\rm esc} \hat{n}

  9. Vectorize with vmap over 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:

Optional: Maxwellian Approximation (for comparison)

If you want to compare methods, you can also implement:

vdisp(r)=16vesc(r)v_{\rm disp}(r) = \frac{1}{\sqrt{6}} v_{\rm esc}(r)

Draw components from N(0,vdisp2)\mathcal{N}(0, v_{\rm disp}^2). 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 ii is:

ai=Gjimjrirj(rij2+ϵ2)3/2\vec{a}_i = -G \sum_{j \neq i} m_j \frac{\vec{r}_i - \vec{r}_j}{(r_{ij}^2 + \epsilon^2)^{3/2}}

where ϵ\epsilon 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:

  1. Numerical instability (r0ar \to 0 \Rightarrow a \to \infty)

  2. Unrealistically large energy changes from two-body encounters

Softening models the smoothing effect of finite stellar radii. For star clusters, typical values: ϵ0.10.5\epsilon \sim 0.1 - 0.5 pc, or adaptively: ϵ0.3rh/N\epsilon \sim 0.3 r_h / \sqrt{N} where rhr_h is the half-mass radius.

Deliverable for report: State your ϵ\epsilon policy (constant vs. ϵrh/N\epsilon \propto r_h/\sqrt{N}), the chosen value/factor κ\kappa, 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:

  1. Builds all pairwise separations Δrij=rirj\Delta \vec{r}_{ij} = \vec{r}_i - \vec{r}_j

  2. Computes softened distances rij2+ϵ2r_{ij}^2 + \epsilon^2 for all pairs

  3. Zeroes the self-interaction terms (diagonal)

  4. Returns accelerations ai=GjimjΔrij(rij2+ϵ2)3/2\vec{a}_i = -G \sum_{j \neq i} m_j \frac{\Delta \vec{r}_{ij}}{(r_{ij}^2 + \epsilon^2)^{3/2}}

    • Critical: Weight each term by the mass mjm_j of the source particle

Requirements:

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.

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:

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:

Pros: Lower peak memory (O(N)O(N) 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 NN, or composing with grad for differentiable physics


Implementation Strategy: Implement both strategies (matrix-based and vmap-based)

Softening guidance:


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):

  1. Drift: x1/2=xn+vnΔt2\vec{x}_{1/2} = \vec{x}_n + \vec{v}_n \cdot \frac{\Delta t}{2}

  2. Kick: vn+1=vn+a(x1/2)Δt\vec{v}_{n+1} = \vec{v}_n + \vec{a}(\vec{x}_{1/2}) \cdot \Delta t

  3. Drift: xn+1=x1/2+vn+1Δt2\vec{x}_{n+1} = \vec{x}_{1/2} + \vec{v}_{n+1} \cdot \frac{\Delta t}{2}

Why leapfrog?

Implementation Requirements:

Implement two functions following the DKD equations above:

  1. leapfrog_step(state, dt, params)new_state

    • Pure function (no mutation)

    • state = (x, v), params contains masses, softening, etc.

    • Returns updated (x_new, v_new)

  2. integrate(state0, dt, n_steps, params)(state_final, trajectory)

    • Use jax.lax.scan for the timestepping loop (not Python for)

    • Returns final state and trajectory of states at each timestep

Requirements:


Step 4: Adaptive Timestepping

Fixed timesteps are inefficient: During expansion, you can take large steps. During close encounters, you need small steps.

Criterion: Choose Δt\Delta t such that:

Δt=Cmini(ϵai)1/2\Delta t = C \min_i \left( \frac{\epsilon}{|\vec{a}_i|} \right)^{1/2}

where C0.010.1C \sim 0.01 - 0.1 is the Courant factor.

Physical meaning: Limit timestep so no particle moves more than ϵ\sim \epsilon under its current acceleration.

Implementation Requirements:

Validation: Plot Δt(t)\Delta t(t) over a simulation. It should:

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:

  1. kinetic_energy(v, m)K

    • Total kinetic energy of the system

  2. potential_energy(x, m, eps2, G)W

    • Total gravitational potential energy (negative)

    • Formula: W=Gi<jmimjrij2+ϵ2W = -G \sum_{i<j} \frac{m_i m_j}{\sqrt{r_{ij}^2 + \epsilon^2}}

    • Critical: Avoid double-counting pairs (each pair i,ji,j counted once)

    • Must handle softening correctly and include gravitational constant GG explicitly

  3. total_energy(x, v, m, eps2, G)(K, W, E)

    • Returns kinetic, potential, and total energy

    • E=K+WE = K + W

  4. virial_ratio(K, W)α_vir

    • Computes virial parameter αvir=2K/W\alpha_{\rm vir} = 2K / |W|

Requirements:

Test Case (validates your implementation):

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

  1. Parameter sweep:

    • Vary N[200,500]N \in [200, 500]

    • Vary aa (Plummer scale length) over 10×\sim 10\times range

    • All clusters start at αvir=1±0.05\alpha_{\rm vir} = 1 \pm 0.05

  2. Automated pipeline:

    • A simple Python script (e.g., run_ensemble.py) that generates all simulations

    • Explicit random seed management (log seeds for reproducibility)

    • Can be sequential (no need for complex parallelization)

  3. Data storage:

    • Organized directory structure (e.g., sim_0000/, sim_0001/, ...)

    • Each simulation saves:

      • Initial conditions: (x0,v0,m)(x_0, v_0, m)

      • Trajectories: (x(t),v(t))(x(t), v(t)) at 50100\sim 50-100 snapshots

      • Metadata: NN, aa, MtotalM_{\rm total}, αvir,0\alpha_{\rm vir,0}, seed

      • Diagnostics: E(t)E(t), αvir(t)\alpha_{\rm vir}(t), Δt(t)\Delta t(t)

    • Format: NumPy .npz files (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

  4. Statistical validation:

    • Create a single summary plot showing your ensemble is physically consistent

    • Example: Energy conservation histogram across all runs, or αvir\alpha_{\rm vir} distribution

    • Quantify: “95% of simulations achieve ΔE/E<105|\Delta E/E| < 10^{-5}

Suggested Workflow

Your pipeline should:

  1. Define parameter grid: Vary N[200,500]N \in [200, 500], Plummer scale aa over 10×\sim 10\times range, total mass via IMF sampling

  2. Manage random seeds: Use a master seed, split for each simulation, log all seeds for reproducibility

  3. Generate initial conditions: For each parameter combination, create virialized cluster

  4. Integrate: Run simulation, save trajectories and diagnostics

  5. Organize output: Save to structured directories with metadata

  6. Statistical validation: Aggregate results and verify ensemble consistency

Implementation tips:


Part 3: Performance Analysis

Benchmarking Requirements

  1. Force calculation comparison:

    • Matrix-based vs. vmap: Time both implementations for N{50,100,200,300,400,500}N \in \{50, 100, 200, 300, 400, 500\}

    • Plot: runtime vs. NN on log-log scale

    • Quantify: Which is faster? When does the crossover occur?

  2. 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.

  3. JIT warmup:

    • Measure first call (includes compilation) vs. subsequent calls

    • Demonstrate the cost of compilation is amortized over many runs

  4. Scaling analysis:

    • Plot: total integration time vs. NN for fixed tfinalt_{\rm final} and adaptive Δt\Delta t

    • Quantify: How does your integrator scale? O(N2)O(N^2)? 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:

  1. Wrap your integration call with jax.profiler.trace(output_path)

  2. Load the trace in Chrome at chrome://tracing

  3. Analyze: 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:

2. Force method agreement:

3. Force symmetry:

4. Units sanity check:

5. Sampler validation:

6. Gradient safety (mini-check):

N-body cluster validation (included in your 50-100 simulation ensemble):


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.md

Why this structure?

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 samplers

What you DON’T need:

What you DO need:

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 αvir\alpha_{\rm vir} 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:

  1. Validation Evidence (≈1 page):

    • All 6 required test results with key figures showing correctness

    • Two-body orbit plot: Energy conservation (maxΔE/E<108\max|\Delta E/E| < 10^{-8})

    • Virial equilibrium plot: αvir=1±0.05\alpha_{\rm vir} = 1 \pm 0.05 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

  2. 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?

  3. Key Design Decisions (≈0.5 pages):

    • Softening policy: State your ϵ\epsilon choice (constant vs. ϵrh/N\epsilon \propto r_h/\sqrt{N}), 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:

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:

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

IssueSymptomCauseSolutionLesson
“Array is not writable”ValueError when trying to modify arrayTried to mutate JAX arrayUse .at[].set() or build new arrayJAX arrays are values, not containers
“Tracer error during JIT”TypeError with abstract tracerPython if statement on traced valueUse jax.lax.cond() for conditionals, jax.lax.while_loop() for loops, or restructure without data-dependent control flowJIT traces data flow, not control flow
Recompiling on every callSlow even after warmupShape-changing arrays or data-dependent control flowEnsure 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 fastLong pause on first callJIT compilation overheadNormal behavior—first call compiles, subsequent calls use compiled codeWarmup before timing
Energy drift worse than NumPydE/E>106|dE/E| > 10^{-6}Using float32 (JAX default)Set jax.config.update("jax_enable_x64", True) at top of modulePrecision matters for physics
Non-reproducible resultsDifferent answers each runUsing Python random instead of jax.randomAlways use jax.random.PRNGKey and explicit splitsJAX manages randomness explicitly
αvir1\alpha_{\rm vir} \neq 1 initiallyCluster immediately expands/collapsesVelocity initialization wrongCheck acceptance-rejection or velocity dispersion formula carefullyInitial conditions are physics
Tests pass, simulation explodesInsufficient validation coverageOnly tested N=2, not realistic clustersValidate with N=100+ cluster for several dynamical timesMulti-scale validation essential
Code hangs during integrationTimestep → 0Very close encounters, ϵ\epsilon too smallUse appropriate softening: ϵ0.3rh/N\epsilon \sim 0.3 r_h/\sqrt{N}Softening prevents numerical issues
Massive memory usageOut of memory errorCreating O(N2)O(N^2) arrays without JITEnsure force calculation is JIT-compiled; check for memory leaks in loopsJIT optimizes memory

Debugging Strategy

When something goes wrong (and it will):

  1. Start small: Does your code work for N=2? N=3? If not, fix that first.

  2. Check conservation laws: Energy, momentum, angular momentum are your physics smoke detectors

  3. Visualize: Plot trajectories, αvir(t)\alpha_{\rm vir}(t), Δt(t)\Delta t(t) — your eyes catch patterns tests miss

  4. Compare: Does your JAX code give the same answer as Project 2 NumPy code for identical initial conditions?

  5. Bisect: Comment out half your code. Which half has the bug?

  6. 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 αvir\alpha_{\rm vir} before running long integrations.


Essential JAX Functions & Tools

Core JAX

FunctionPurpose
jax.jitJIT compilation for speed
jax.vmapVectorize over batch dimension
jax.gradAutomatic differentiation
jax.lax.scanEfficient loops for timestepping
jax.lax.while_loopBounded loops (e.g., rejection sampling)
jax.random.PRNGKeyCreate random seed
jax.random.splitSplit RNG state
jax.random.uniformUniform distribution
jax.random.normalNormal 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

ToolPurpose
jax.profiler.traceProfile execution
%timeit (IPython) or import timeBenchmark with warmup
time.time()Manual timing

Optional Tools

File/ToolPurpose
pytestTesting framework (optional, but tests required)
ruffLinting and formatting (recommended)
pyproject.tomlPackage 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.

ComponentWeightEvaluation Method
Code Quality & Working Pipeline40%Repository inspection: JAX patterns correct, tests present, clean structure, documentation, functional implementation
Quickstart Demo20%Does quickstart.py run and produce correct physics (energy conservation, αvir1\alpha_{\rm vir} \approx 1)?
Research Memo20%Essential validation plots with captions, JAX speedup analysis, design justification
Validation Evidence20%Memo shows all 6 required tests passed with figures/tables proving correctness
Growth MemoPass/FailHonest, 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:

What’s next in the final project:

The computational infrastructure you build now directly enables machine learning for astrophysics.