Turn a rotation curve into posterior constraints on bulge, disk, and halo mass — with uncertainties, diagnostics, and publication-ready figures.
A galaxy's spin encodes how its mass is arranged. Given measured circular velocities
Data, median model, 68% posterior band, and bulge / disk / halo contributions.
Left: data − model residuals. Right: scaled enclosed mass
| Challenge | What this repo gives you |
|---|---|
| Mass components are degenerate | Joint posterior over |
| A |
Credible intervals, predictive bands, and Gelman–Rubin |
| Homework MCMC code is often wrong | Corrected physics, adaptive MH, mock recovery, CI tests |
| Results need to communicate | Fit, residuals, enclosed mass, traces, and corner plots |
Originally a computational-methods homework. Rebuilt into a small, installable inference toolkit you can run, test, and show in a portfolio.
-
Physics-correct bulge + disk + halo rotation curve (
$v_c^2 = v_b^2 + v_d^2 + v_h^2$ ) - Adaptive Metropolis–Hastings in log-parameter space
-
Optional free halo scale
$A_h$ and noise$\sigma$ -
Multi-chain diagnostics (acceptance rate + Gelman–Rubin
$\hat{R}$ ) -
Figures out of the box: fit, residuals, enclosed mass
$M(< R)$ , traces, corner -
Reproducible outputs: JSON summary with seed + copy-paste command; samples as
.npyand.csv - Mock recovery check proving the sampler recovers known masses
- Legacy C pipeline kept for teaching / comparison
- GitHub Actions CI running the test suite on every push
| Symbol | Default [kpc] | Role |
|---|---|---|
| 0.2497 | Bulge scale (fixed) | |
| 5.16 | Disk scale length (fixed) | |
| 0.3105 | Disk scale height (fixed) | |
| 64.3 | Halo scale (optional free via --fit-ah) |
Likelihood: independent Gaussian errors with scale
Priors: uniform on
Details: docs/theory.md.
python3 -m pip install -r requirements.txt
python3 -m pip install -e .
make test # unit + mock-recovery tests
make mock # synthetic truth recovery (standalone)
make quick # short smoke run
make run # full inference: --fit-ah --fit-sigmaOutputs go to results/:
| File | Contents |
|---|---|
rotation_curve_fit.png |
Data, median model, 68% band, component curves |
residuals.png |
Data − model vs radius |
enclosed_mass.png |
Scaled |
mcmc_traces.png |
Chain traces |
posterior_corner.png |
Pairwise posteriors |
posterior_summary.txt / .json
|
Medians, intervals, |
posterior_samples.npy / .csv
|
Combined posterior draws |
Full CLI and API notes: docs/usage.md.
Dataset: data/RadialVelocities.dat (300 points,
Run: --fit-ah --fit-sigma (4 chains × 20k steps).
| Parameter | Median | 16%–84% |
|---|---|---|
| weakly constrained | ||
|
|
||
|
|
||
|
|
|
|
|
|
|
Takeaway: the disk shapes the inner curve; the halo sustains the outer velocities; this dataset does not require a significant bulge.
Corner plot of the joint posterior (masses, halo scale
Multi-chain traces (log scale). Vertical dotted line marks burn-in.
data/ Observed rotation curve
src/galaxy_mcmc/ Installable Python package
model.py Bulge + disk + halo potential
likelihood.py Priors + Gaussian likelihood
mcmc.py Adaptive MH + Gelman–Rubin
plotting.py Fit / residual / mass / diagnostics figures
io.py Data loader
scripts/run_inference.py Main CLI
scripts/mock_recovery.py Synthetic recovery check
tests/ Pytest suite
docs/ Theory, usage, legacy notes + figures
legacy/ Corrected original C homework pipeline
.github/workflows/ CI (pytest)
results/ Generated outputs (local)
The early C Metropolis–Hastings sketch had real bugs (integer division in exponents, pow(R,R) instead of
- Uses the correct circular-velocity formula (quadrature sum)
- Evaluates a full-dataset Gaussian likelihood
- Samples in log-parameter space with adaptive proposals
- Adds multi-chain
$\hat{R}$ , optional$A_h$ /$\sigma$ , mock recovery, and CI
The cleaned C code remains under legacy/ for teaching:
cd legacy && make -f Tarea5.mkfrom galaxy_mcmc import GalaxyPotential, load_rotation_curve
from galaxy_mcmc.likelihood import RotationCurvePosterior
from galaxy_mcmc.mcmc import run_ensemble
R, V = load_rotation_curve()
posterior = RotationCurvePosterior(
R, V, GalaxyPotential(), fit_ah=True, fit_sigma=True
)
chains = run_ensemble(posterior, n_chains=4, n_steps=25_000, burn_in=5_000)
samples = chains[0].samples # burned-in physical parameters
print(chains[0].summary())| Doc | Contents |
|---|---|
| Theory | Potentials, likelihood, priors, MCMC, historical bugs |
| Usage | Install, CLI flags, interpreting outputs, own data, API |
| Legacy C | Build/run the educational C pipeline |
Developed as a Monte Carlo / Bayesian parameter-estimation assignment on galactic rotation curves by Juan Prada, then rebuilt as a reusable inference package with corrected models, diagnostics, and documentation.
If you reuse this in coursework or a portfolio, please keep attribution and link back to the repository.




