Skip to content

Commit e2c52d7

Browse files
committed
Initial Bayesian-DSC project
0 parents  commit e2c52d7

10 files changed

Lines changed: 556 additions & 0 deletions

File tree

.github/workflows/tests.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
unit-tests:
9+
runs-on: windows-latest
10+
steps:
11+
- name: Check out repository
12+
uses: actions/checkout@v4
13+
14+
- name: Set up Python
15+
uses: actions/setup-python@v5
16+
with:
17+
python-version: "3.11"
18+
19+
- name: Install dependencies
20+
run: |
21+
python -m pip install --upgrade pip
22+
pip install -r requirements.txt
23+
24+
- name: Run unit tests
25+
run: python -m unittest discover -s tests

.gitignore

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
.venv/
2+
__pycache__/
3+
*.py[cod]
4+
*.pyo
5+
*.pyd
6+
.Python
7+
.pytest_cache/
8+
.coverage
9+
htmlcov/
10+
11+
# CmdStan / CmdStanPy
12+
.cmdstan/
13+
cmdstan/
14+
src/stan_models/*.exe
15+
src/stan_models/*.hpp
16+
src/stan_models/*.o
17+
src/stan_models/*.d
18+
cmdstan-*.csv
19+
cmdstan-*.txt
20+
21+
# Generated outputs
22+
fit_result.png
23+
24+
# OS / editor noise
25+
.DS_Store
26+
Thumbs.db
27+
.vscode/
28+
.idea/

README.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Bayesian-DSC
2+
3+
`Bayesian-DSC` is a small portfolio project that demonstrates Bayesian fitting of Dynamic Susceptibility Contrast (DSC) MRI concentration-time curves with `CmdStanPy`. The core design goal is to compile the Stan model once when the fitter is instantiated, then reuse that compiled executable for repeated curve fitting.
4+
5+
## Features
6+
7+
- Uses a pre-compiled Stan gamma-variate model through `CmdStanPy`
8+
- Exposes a backend-swappable `BaseDSCFitter` abstraction for future PyMC or NumPyro implementations
9+
- Fits noisy DSC concentration-time curves with Bayesian inference
10+
- Returns posterior means and 95% HDI bounds for `A`, `alpha`, `beta`, `t0`, `sigma`, `CBV`, and `MTT`
11+
- Includes a reproducible synthetic-data simulation and plot export
12+
13+
## Installation
14+
15+
1. Create a virtual environment in the project root:
16+
17+
```bash
18+
python -m venv .venv
19+
```
20+
21+
2. Activate it.
22+
23+
On Windows PowerShell:
24+
25+
```powershell
26+
.\.venv\Scripts\Activate.ps1
27+
```
28+
29+
3. Install the Python dependencies:
30+
31+
```bash
32+
pip install -r requirements.txt
33+
```
34+
35+
4. Install the underlying CmdStan toolchain.
36+
37+
On Windows, the simplest option is to let `cmdstanpy` install the required compiler toolchain if needed:
38+
39+
```bash
40+
python -m cmdstanpy.install_cmdstan -c
41+
```
42+
43+
This downloads and builds CmdStan under the user CmdStan directory, typically `C:\Users\<username>\.cmdstan`.
44+
45+
## Usage
46+
47+
Run the synthetic demonstration script from the project root:
48+
49+
```bash
50+
python simulate_dsc.py
51+
```
52+
53+
On your Windows setup, the easiest option is the included helper script, which configures the RTools toolchain before launching the demo:
54+
55+
```powershell
56+
.\run_simulation.ps1
57+
```
58+
59+
The script will:
60+
61+
- generate a synthetic DSC concentration-time curve
62+
- add Gaussian noise
63+
- compile and sample the Stan model
64+
- print true parameters, posterior means, and 95% HDI intervals
65+
- save `fit_result.png` in the project root
66+
67+
If you prefer to avoid activating the environment explicitly on Windows, you can run:
68+
69+
```powershell
70+
.\.venv\Scripts\python simulate_dsc.py
71+
```
72+
73+
If Stan compilation on Windows picks up the wrong `C:\MinGW` toolchain, prefer the helper script above because it prepends the correct RTools paths automatically.
74+
75+
## Tests
76+
77+
Run the lightweight unit tests from the project root:
78+
79+
```bash
80+
.\.venv\Scripts\python -m unittest discover -s tests
81+
```
82+
83+
## Project Structure
84+
85+
```text
86+
.
87+
|-- .gitignore
88+
|-- README.md
89+
|-- requirements.txt
90+
|-- run_simulation.ps1
91+
|-- simulate_dsc.py
92+
|-- tests
93+
| `-- test_bayesian_fitter.py
94+
`-- src
95+
|-- __init__.py
96+
|-- bayesian_fitter.py
97+
`-- stan_models
98+
`-- gamma_variate.stan
99+
```
100+
101+
## Notes
102+
103+
- The first `DSCBayesianFitter` instantiation may take longer because Stan compilation happens at that point.
104+
- Subsequent calls to `.fit_curve(...)` on the same fitter instance reuse the already compiled executable.
105+
- If the Stan source is unchanged and the executable already exists, CmdStanPy can also reuse that executable on later runs.
106+
- If compilation or sampling fails, the wrapper safely returns `NaN` outputs instead of crashing.
107+
- The repository `.gitignore` excludes the local virtual environment, Python caches, CmdStan directories, and the generated `fit_result.png`.
108+
- On Windows systems that already have an older `C:\MinGW` toolchain installed, make sure the RTools paths provided by `cmdstanpy.install_cmdstan -c` take precedence when compiling Stan models.

requirements.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
numpy
2+
cmdstanpy
3+
matplotlib
4+
arviz

run_simulation.ps1

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
param(
2+
[Parameter(ValueFromRemainingArguments = $true)]
3+
[string[]]$ScriptArgs
4+
)
5+
6+
$ErrorActionPreference = "Stop"
7+
8+
$projectRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
9+
$pythonExe = Join-Path $projectRoot ".venv\Scripts\python.exe"
10+
$rtoolsRoot = Join-Path $env:USERPROFILE ".cmdstan\RTools40"
11+
12+
if (-not (Test-Path $pythonExe)) {
13+
throw "Virtual environment Python not found at $pythonExe"
14+
}
15+
16+
if (-not (Test-Path $rtoolsRoot)) {
17+
throw "RTools40 was not found at $rtoolsRoot"
18+
}
19+
20+
$env:Path = (Join-Path $rtoolsRoot "usr\bin") + ";" + (Join-Path $rtoolsRoot "mingw64\bin") + ";" + $env:Path
21+
$env:MAKE = "mingw32-make"
22+
23+
& $pythonExe (Join-Path $projectRoot "simulate_dsc.py") @ScriptArgs

simulate_dsc.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""Run a synthetic DSC-MRI Bayesian fitting demonstration."""
2+
3+
from __future__ import annotations
4+
5+
import math
6+
from pathlib import Path
7+
8+
import matplotlib.pyplot as plt
9+
import numpy as np
10+
11+
from src.bayesian_fitter import DSCBayesianFitter, gamma_variate_curve
12+
13+
14+
def main() -> None:
15+
"""Generate synthetic DSC data, fit it, and save a comparison plot."""
16+
rng = np.random.default_rng(seed=42)
17+
t = np.linspace(0.0, 50.0, 50)
18+
19+
true_params = {
20+
"A": 5.0,
21+
"alpha": 3.0,
22+
"beta": 1.5,
23+
"t0": 10.0,
24+
}
25+
26+
clean_curve = gamma_variate_curve(
27+
t,
28+
amplitude=true_params["A"],
29+
alpha=true_params["alpha"],
30+
beta=true_params["beta"],
31+
t0=true_params["t0"],
32+
)
33+
noise_sigma = 0.35
34+
c_obs = clean_curve + rng.normal(loc=0.0, scale=noise_sigma, size=t.shape)
35+
36+
stan_path = Path("src") / "stan_models" / "gamma_variate.stan"
37+
fitter = DSCBayesianFitter(stan_file=stan_path)
38+
fit_result = fitter.fit_curve(t, c_obs, num_samples=1000)
39+
40+
fitted_curve = gamma_variate_curve(
41+
t,
42+
amplitude=fit_result["A"],
43+
alpha=fit_result["alpha"],
44+
beta=fit_result["beta"],
45+
t0=fit_result["t0"],
46+
)
47+
true_cbv = float(
48+
true_params["A"]
49+
* math.gamma(true_params["alpha"] + 1.0)
50+
* (true_params["beta"] ** (true_params["alpha"] + 1.0))
51+
)
52+
true_mtt = float(true_params["alpha"] * true_params["beta"])
53+
54+
print("True parameters:")
55+
for key, value in true_params.items():
56+
print(f" {key}: {value:.4f}")
57+
print(f" cbv: {true_cbv:.4f}")
58+
print(f" mtt: {true_mtt:.4f}")
59+
60+
print("\nPosterior means with 95% HDI:")
61+
for key in ("A", "alpha", "beta", "t0", "sigma", "cbv", "mtt"):
62+
print(
63+
f" {key}: {fit_result[key]:.4f} "
64+
f"[{fit_result[f'{key}_hdi_lower']:.4f}, {fit_result[f'{key}_hdi_upper']:.4f}]"
65+
)
66+
67+
plt.figure(figsize=(10, 6))
68+
plt.scatter(t, c_obs, color="tab:blue", label="Noisy observations", alpha=0.8)
69+
plt.plot(t, clean_curve, color="tab:green", linewidth=2, label="Ground truth")
70+
plt.plot(t, fitted_curve, color="tab:red", linewidth=2, label="Posterior mean fit")
71+
plt.xlabel("Time (s)")
72+
plt.ylabel("Concentration")
73+
plt.title("Bayesian DSC-MRI Gamma-Variate Fit")
74+
plt.legend()
75+
plt.tight_layout()
76+
plt.savefig("fit_result.png", dpi=200)
77+
plt.close()
78+
79+
80+
if __name__ == "__main__":
81+
main()

src/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Bayesian-DSC package."""
2+
3+
from .bayesian_fitter import BaseDSCFitter, DSCBayesianFitter, gamma_variate_curve
4+
5+
__all__ = ["BaseDSCFitter", "DSCBayesianFitter", "gamma_variate_curve"]

0 commit comments

Comments
 (0)