Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
4929f41
docs: align top-level navigation
mcencini Oct 5, 2026
9ce8318
docs: add explanations landing page
mcencini Oct 5, 2026
231b713
docs: make examples an explicit course and tours
mcencini Oct 5, 2026
be8fd5c
docs: make Simulator the user-facing sequence API
mcencini Oct 5, 2026
008b6f3
docs: define Simulator as the canonical extension point
mcencini Oct 5, 2026
5de0dbb
docs: position functional wrappers as convenience API
mcencini Oct 5, 2026
067902d
docs: make the landing page class-first and current
mcencini Oct 5, 2026
ca10766
docs: add miscellaneous landing page
mcencini Oct 5, 2026
e18ec35
docs: centralize light-dark figure styling
mcencini Oct 5, 2026
0fe82ed
docs: add dark-mode-safe documentation styling
mcencini Oct 5, 2026
fb3c47f
docs: align site navigation, URLs, and figure rendering
mcencini Oct 5, 2026
5a74760
docs: make explanation figures theme-safe
mcencini Oct 5, 2026
5bed131
docs: point project links at pulserver
mcencini Oct 5, 2026
2c1913f
docs: point project links at pulserver
mcencini Oct 5, 2026
6c2899e
docs: point project links at pulserver
mcencini Oct 5, 2026
2bd09b2
docs: point project links at pulserver
mcencini Oct 5, 2026
585e209
docs: point project links at pulserver
mcencini Oct 5, 2026
4aebc26
docs: point project links at pulserver
mcencini Oct 5, 2026
6234223
docs: point project links at pulserver
mcencini Oct 5, 2026
82c2e5b
docs: point project links at pulserver
mcencini Oct 5, 2026
ac494f2
docs: point project links at pulserver
mcencini Oct 5, 2026
d993c99
docs: point project links at pulserver
mcencini Oct 5, 2026
5b13661
docs: align API section title
mcencini Oct 5, 2026
1a6df0e
docs: add explanation summary
mcencini Oct 5, 2026
1727aa9
docs: add explanation summary
mcencini Oct 5, 2026
ede6259
docs: add explanation summary
mcencini Oct 5, 2026
c33f3b4
docs: record the documentation architecture
mcencini Oct 5, 2026
30c5b06
docs: keep explanation figures transparent in dark mode
mcencini Oct 5, 2026
f4ebba2
style: apply pre-commit formatting
mcencini Oct 5, 2026
1a8d3b2
fix: repair explanation figure formatting
mcencini Oct 5, 2026
d1cc3c3
style: separate local imports for Ruff
mcencini Oct 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ body:
attributes:
value: |
Questions about how to use TorchSim, and results you are unsure about,
belong in [Discussions](https://github.com/FiRMLAB-Pisa/torchsim/discussions).
belong in [Discussions](https://github.com/pulserver/torchsim/discussions).
This form is for behaviour you can pin down.

- type: textarea
Expand Down
4 changes: 2 additions & 2 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Question, or a result you are unsure about
url: https://github.com/FiRMLAB-Pisa/torchsim/discussions
url: https://github.com/pulserver/torchsim/discussions
about: Ask how to model a sequence, or whether a signal you got is expected.
- name: Security report
url: https://github.com/FiRMLAB-Pisa/torchsim/security/policy
url: https://github.com/pulserver/torchsim/security/policy
about: Report a vulnerability privately rather than in a public issue.
4 changes: 2 additions & 2 deletions .github/SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Older releases are not patched.
Report privately, not as a public issue:

- open a draft advisory through
[Security -> Report a vulnerability](https://github.com/FiRMLAB-Pisa/torchsim/security/advisories/new),
[Security -> Report a vulnerability](https://github.com/pulserver/torchsim/security/advisories/new),
which is the preferred route; or
- email **matteo.cencini@gmail.com** if you cannot use GitHub.

Expand All @@ -36,4 +36,4 @@ merely a bug.
## Not a vulnerability

A simulation that returns wrong physics, diverges, or raises is a
[bug report](https://github.com/FiRMLAB-Pisa/torchsim/issues/new/choose).
[bug report](https://github.com/pulserver/torchsim/issues/new/choose).
32 changes: 31 additions & 1 deletion .github/skills/build-the-docs/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ build the pages without running any of them, pass `-D plot_gallery=0`.
## Where the published pages are built

`.github/workflows/docs.yml` builds them, and GitHub Pages serves them from
the `gh-pages` branch at <https://firmlab-pisa.github.io/torchsim/>.
the `gh-pages` branch at <https://pulserver.github.io/torchsim/>.

Its **HTML** job runs on every branch and pull request with the `doc` extra:
every page is written and the examples needing nothing but TorchSim are
Expand Down Expand Up @@ -93,3 +93,33 @@ The audience is MR scientists: pulse sequences and physics, not software
architecture. Describe what TorchSim does and why that is right on its own
terms. These pages are not a changelog — never justify a design by describing
the design it replaced.


## Documentation architecture

Keep the public sidebar shallow and in this order:

1. User Guide
2. Developer Guide
3. Explanations
4. Examples
5. API
6. Miscellaneous

The first four Framework examples are the Course. They teach the public
abstraction in order; the remaining example sections are standalone Tours.

For a new sequence, teach {class}`torchsim.model.Simulator` as the extension
point. A state-machine simulator has two complementary inputs: its class-level
handlers interpret commands from an incoming Pulseq/MRD description, and
`layout()` constructs the same sequence offline. Lower-level
`SequenceDescription`, `SpinPhysics` and operator machinery are important
reference/developer concepts, not competing user-facing sequence APIs.

Functional `*_sim` calls are conveniences around shipped simulators. Do not
present them as the abstraction a user implements.

Figures must remain readable at final documentation width in both light and
dark mode. Use `docs/figure_style.py` for common typography, transparent
backgrounds and foreground colors; examples set only figure-specific geometry
and scientific encodings.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,6 @@ jobs:
uses: codecov/codecov-action@v7
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: FiRMLAB-Pisa/torchsim
slug: pulserver/torchsim
files: coverage.xml
fail_ci_if_error: false
108 changes: 68 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,73 +1,101 @@
# TorchSim

TorchSim is a pure Pytorch-based MR simulator, including analytical and EPG model.

[![codecov](https://codecov.io/gh/FiRMLAB-Pisa/torchsim/graph/badge.svg?token=l8xhIVORYm)](https://codecov.io/gh/FiRMLAB-Pisa/torchsim)
[![Tests](https://github.com/FiRMLAB-Pisa/torchsim/actions/workflows/test.yml/badge.svg)](https://github.com/FiRMLAB-Pisa/torchsim/actions/workflows/test.yml)
[![Lint](https://github.com/FiRMLAB-Pisa/torchsim/actions/workflows/lint.yml/badge.svg)](https://github.com/FiRMLAB-Pisa/torchsim/actions/workflows/lint.yml)
[![License](https://img.shields.io/github/license/FiRMLAB-Pisa/torchsim)](https://github.com/FiRMLAB-Pisa/torchsim/blob/main/LICENSE.txt)
[![Codefactor](https://www.codefactor.io/repository/github/FiRMLAB-Pisa/torchsim/badge)](https://www.codefactor.io/repository/github/FiRMLAB-Pisa/torchsim)
[![Documentation](https://github.com/FiRMLAB-Pisa/torchsim/actions/workflows/docs.yml/badge.svg)](https://firmlab-pisa.github.io/torchsim/)
TorchSim is a differentiable MR signal simulator built on PyTorch, with
closed-form signal models and a fused extended-phase-graph (EPG) state machine
for pulse trains.

[![codecov](https://codecov.io/gh/pulserver/torchsim/graph/badge.svg?token=l8xhIVORYm)](https://codecov.io/gh/pulserver/torchsim)
[![Tests](https://github.com/pulserver/torchsim/actions/workflows/test.yml/badge.svg)](https://github.com/pulserver/torchsim/actions/workflows/test.yml)
[![Lint](https://github.com/pulserver/torchsim/actions/workflows/lint.yml/badge.svg)](https://github.com/pulserver/torchsim/actions/workflows/lint.yml)
[![License](https://img.shields.io/github/license/pulserver/torchsim)](https://github.com/pulserver/torchsim/blob/main/LICENSE.txt)
[![Documentation](https://github.com/pulserver/torchsim/actions/workflows/docs.yml/badge.svg)](https://pulserver.github.io/torchsim/)
[![PyPi](https://img.shields.io/pypi/v/torchsim)](https://pypi.org/project/torchsim)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![PythonVersion](https://img.shields.io/badge/Python-%3E=3.10-blue?logo=python&logoColor=white)](https://python.org)

## Features
## What it provides

TorchSim contains tools to implement parallelized and differentiable MR simulators. Specifically, we provide

1. Automatic vectorization of across multiple atoms (e.g., voxels).
2. Automatic generation of forward and jacobian methods (based on forward-mode autodiff) to be used in parameter fitting or model-based reconstructions.
3. Support for custom manual defined jacobian methods to override auto-generated jacobian.
4. Support for advanced signal models, including diffusion, flow, magnetization transfer and chemical exchange.
5. GPU support.
- Vectorized signal simulation over voxels/atoms on CPU and NVIDIA GPU.
- Forward-mode Jacobians with respect to tissue properties and reverse-mode
differentiation with respect to sequence parameters.
- Closed-form models and an EPG state machine with relaxation,
off-resonance, diffusion, flow, transmit variation, magnetization transfer
and chemical exchange.
- Parameter inference, model-based reconstruction and sequence-design tools
written against the same simulator interface.
- Pulseq and MRD sequence-description input, so the same sequence model can be
used offline or driven from a scanner stream.

## Installation

TorchSim can be installed via pip as:
Install the PyTorch build appropriate for your machine first, then TorchSim:

```bash
pip install torchsim
```

## Basic Usage
See the [User Guide](https://pulserver.github.io/torchsim/latest/user_guide.html)
for CPU, CUDA, macOS and source-build details.

## Basic usage

Using TorchSim, we can quickly implement and run MR simulations.
We also provide pre-defined simulators for several applications:
The central public object is a `Simulator`. A shipped simulator fixes the
sequence; `simulate` and `jacobian` evaluate it over the tissue you pass:

```python
import numpy as np
import torchsim

# generate a flip angle pattern
flip = np.concatenate((np.linspace(5, 60.0, 300), np.linspace(60.0, 2.0, 300), np.ones(280)*2.0))
sig, jac = torchsim.mrf_sim(flip=flip, TR=10.0, T1=1000.0, T2=100.0, diff=("T1","T2"))
from torchsim.simulators import MRFSimulator

flip = np.concatenate(
(np.linspace(5.0, 60.0, 300), np.linspace(60.0, 2.0, 300), np.full(280, 2.0))
)
sequence = MRFSimulator(flip=flip, TR=10.0)

signal, jacobian = sequence.jacobian(
("T1", "T2"),
T1=1000.0,
T2=100.0,
)
```

This way we obtained the forward pass signal (`sig`) as well as the jacobian
calculated with respect to `T1` and `T2`.
Functional helpers such as `torchsim.mrf_sim(...)` remain convenient for
one-off calls. The class interface is the canonical one for reusable models,
parameter estimation, reconstruction, optimization, Pulseq input and scanner
descriptions.

## Development
## Implementing a sequence

Subclass `torchsim.model.Simulator`. For a state-machine sequence you define:

If you are interested in improving this project, install TorchSim in editable mode:
1. the event handlers that say how excitation, refocusing, inversion,
saturation, readout and delay commands are interpreted; and
2. `layout()`, which returns those operators in order for offline use.

An incoming Pulseq/MRD description already supplies the layout, so
`Simulator.from_description()` replays its commands through the same handlers.
That gives offline design and scanner-driven simulation one public sequence
abstraction.

The executable
[Framework course](https://pulserver.github.io/torchsim/latest/generated/autoexamples/01-framework/index.html)
walks through the complete pattern.

## Development

```bash
git clone git@github.com:FiRMLAB-Pisa/torchsim
git clone git@github.com:pulserver/torchsim
cd torchsim
pip install -e ".[dev]"
pre-commit install
```

The install compiles the two C++ kernels, so it needs a C++17 compiler; CMake
and Ninja arrive as build-time wheels. `pre-commit` runs the formatter and the
linter -- both `ruff` -- on every commit, which is exactly what CI checks.
The install compiles the two C++ kernels, so it needs a C++17 compiler. CMake
and Ninja arrive as build-time dependencies. `pre-commit` runs the same Ruff
format/lint checks as CI.

## Related projects

This package is inspired by the following excellent projects:

- epyg \<<https://github.com/brennerd11/EpyG>\>
- sycomore \<<https://github.com/lamyj/sycomore/>\>
- mri-sim-py \<<https://somnathrakshit.github.io/projects/project-mri-sim-py-epg/>\>
- ssfp \<<https://github.com/mckib2/ssfp>\>
- erwin \<<https://github.com/lamyj/erwin>\>
The documentation's
[Related projects](https://pulserver.github.io/torchsim/latest/misc/related.html)
page places TorchSim among other MR simulators and links to the relevant
packages and literature.
33 changes: 33 additions & 0 deletions docs/_static/custom.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/* Keep the documentation column comfortably readable. */
.bd-main .bd-content .bd-article-container { max-width: 58rem; }
.sig { overflow-wrap: anywhere; }
.sphx-glr-thumbcontainer { min-height: 210px; }

/* Documentation figures are rendered on a transparent canvas with a
light/dark-safe foreground, so do not put them on a white card or dim them. */
html[data-theme="dark"] .bd-content img.sphx-glr-single-img,
html[data-theme="dark"] .bd-content img.sphx-glr-multi-img,
html[data-theme="dark"] .bd-content figure img {
background-color: transparent;
filter: none;
}

/* A cell with several gallery figures reads better vertically at page width. */
ul.sphx-glr-horizontal li { display: block; }
.bd-content img.sphx-glr-multi-img {
display: block;
margin: 0 auto 1rem;
max-width: 100%;
}

/* TL;DR blocks on explanation pages. */
.admonition.tldr {
border-color: var(--pst-color-primary);
}
.admonition.tldr > .admonition-title {
background-color: var(--pst-color-primary-bg);
}
.admonition.tldr > .admonition-title::after {
color: var(--pst-color-primary);
}
.admonition.tldr ul { margin-bottom: 0; }
2 changes: 1 addition & 1 deletion docs/api/index.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# API References
# API

TorchSim is organized around one idea: a **signal model** is the only thing a
sequence has to supply, and everything else -- differentiation, execution
Expand Down
43 changes: 27 additions & 16 deletions docs/api/model.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,27 @@
.. currentmodule:: torchsim.model
```

**There is one base class, and it is written in one of two ways.**
**There is one user-facing base class: {class}`Simulator`.**

{class}`Simulator` is the interface, and the only thing anything downstream
ever sees: the estimators, the model-based operator and the sequence optimizer
all take one and never ask how it arrives at its signal.
{class}`Simulator` is the sequence abstraction and the only model interface
anything downstream consumes. Parameter estimators, model-based
reconstruction and sequence design take a simulator and do not need to know
whether its sequence was built offline or arrived from a running scanner.

Implement {meth}`~Simulator.layout` when the signal has to be *played* -- a
train of pulses whose magnetization state carries from one event to the next,
which is almost every quantitative sequence. Two things are said. Which
operator plays each kind of event, by naming it in the class body, and what
order they come in. The extended phase-graph engine, the derivative, the
device placement and the memory policy all follow from that and none of them
is yours to write.
For a state-machine sequence, a subclass supplies two complementary pieces:

1. **Command handlers.** The class attributes `excitation`, `refocusing`,
`inversion`, `saturation`, `readout` and `delay` say how the RF and
ADC commands of an incoming sequence description are interpreted. This is
the scanner-facing path: {meth}`~Simulator.from_description` re-emits an
MRD/Pulseq-derived event stream through those handlers.
2. **An offline layout.** {meth}`~Simulator.layout` returns the same
operators in the order one repetition plays them. This is the
design/offline path, when no scanner description already exists.

Both routes produce the same sequence description before the state machine
runs. The EPG engine, derivatives, device placement and memory policy are
therefore shared and are not part of a sequence implementation.

```python
class SSFPMRF(Simulator):
Expand Down Expand Up @@ -47,11 +55,14 @@ operators.
Nothing is declared about the tissue. Every property a voxel can have may be
given to any simulator, and giving one is what turns its term on.

A sequence that came from somewhere else is read the same way:
{meth}`~Simulator.from_description` takes the stream an MRD client decodes, and
{meth}`~Simulator.from_pulseq` takes a Pulseq `.seq` file, or the sequence
object a design built, directly. Neither
walks a layout -- naming the simulator is what says how the events are played.
A sequence that came from somewhere else is read through those same handlers.
{func}`~torchsim.sequence.read_mrd_description` decodes the description
carried ahead of the acquisitions on an MRD stream, and
{meth}`~Simulator.from_description` turns one of those descriptions into the
chosen simulator. {meth}`~Simulator.from_pulseq` does the same from a Pulseq
`.seq` file, or from a sequence object held in memory. None of these routes
walks `layout()`: the incoming description already supplies the layout, while
the simulator class supplies its interpretation.

Implement {meth}`~Simulator.evaluate` instead when the signal has a closed
form -- a mono-exponential decay, an inversion-recovery curve, an Ernst
Expand Down
12 changes: 10 additions & 2 deletions docs/api/simulators.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,16 @@ Trains that have to be played out, run on the extended phase graph engine.

## Functional wrappers

One call, protocol and tissue together, for a signal and optionally its
derivative. What the classes above do, without holding one.
The `*_sim` functions are convenience calls for a subset of the shipped
simulators: protocol and tissue go into one function call, with an optional
Jacobian. They are not a second extension API. New sequence families are
implemented as {class}`~torchsim.model.Simulator` subclasses, so they can use
both an offline {meth}`~torchsim.model.Simulator.layout` and the same handlers
when a Pulseq/MRD description arrives from a scanner.

Prefer the simulator classes whenever the same sequence is reused, bound to a
protocol, passed to an estimator/reconstruction/design object, or constructed
from a description. The wrappers remain useful for compact one-off calls.

### Analytical

Expand Down
13 changes: 9 additions & 4 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def _cores_allowed() -> int:
DOCS_VERSION = os.environ.get("TORCHSIM_DOCS_VERSION", "latest")

#: Where the pages are served from.
PAGES_URL = "https://firmlab-pisa.github.io/torchsim"
PAGES_URL = "https://pulserver.github.io/torchsim"

# -- Options for Sphinx Gallery ----------------------------------------------

Expand Down Expand Up @@ -220,8 +220,9 @@ def _missing_imports(script: Path) -> list[str]:
# The gallery header is written in Markdown and pulled into the
# generated index.rst by an include; the file has to travel with it.
"copyfile_regex": r".*\.md",
"reset_modules": ("figure_style.reset", "seaborn"),
"binder": {
"org": "firmlab-pisa",
"org": "pulserver",
"repo": "torchsim",
"branch": "gh-pages",
"binderhub_url": "https://mybinder.org",
Expand Down Expand Up @@ -254,9 +255,10 @@ def _missing_imports(script: Path) -> list[str]:
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
# html_static_path = ["_static"]
html_static_path = ["_static"]
html_css_files = ["custom.css"]
html_theme_options = {
"repository_url": "https://github.com/FiRMLAB-Pisa/torchsim",
"repository_url": "https://github.com/pulserver/torchsim",
"use_repository_button": True,
"use_issues_button": True,
"use_edit_page_button": True,
Expand All @@ -270,6 +272,9 @@ def _missing_imports(script: Path) -> list[str]:
"version_match": DOCS_VERSION,
},
"show_version_warning_banner": True,
"show_navbar_depth": 1,
"max_navbar_depth": 3,
"navbar_persistent": [],
}

#: The theme's own sidebar, with the version switcher under the title.
Expand Down
Loading
Loading