From 4929f41431de25118c0f3692a7dd5b5fa6f886b9 Mon Sep 17 00:00:00 2001 From: Matteo Cencini <83717049+mcencini@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:32:18 +0200 Subject: [PATCH 01/31] docs: align top-level navigation --- docs/index.md | 28 ++++------------------------ 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/docs/index.md b/docs/index.md index d98dc956..cdf9fd51 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,36 +1,16 @@ +# Homepage + ```{include} ../README.md ``` ```{toctree} :hidden: :maxdepth: 2 -:caption: Guides user_guide developer_guide -``` - -```{toctree} -:hidden: -:maxdepth: 2 -:caption: Examples - +explanations/index generated/autoexamples/index -``` - -```{toctree} -:hidden: -:caption: API References - api/index -``` - -```{toctree} -:hidden: -:caption: Miscellaneous - -misc/related -misc/contributors -misc/code_of_conduct -misc/license +misc/index ``` From 9ce8318cad2c4750368ed8f470d2acf8123970b3 Mon Sep 17 00:00:00 2001 From: Matteo Cencini <83717049+mcencini@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:32:21 +0200 Subject: [PATCH 02/31] docs: add explanations landing page --- docs/explanations/index.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 docs/explanations/index.md diff --git a/docs/explanations/index.md b/docs/explanations/index.md new file mode 100644 index 00000000..8eb37ea8 --- /dev/null +++ b/docs/explanations/index.md @@ -0,0 +1,28 @@ +# Explanations + +The concepts behind TorchSim, separate from the step-by-step examples and the +API reference. + +{doc}`description` +: The sequence representation TorchSim consumes: events, RF definitions, + readout roles, Pulseq input, and the MRD description a running scanner can + send. + +{doc}`epg` +: Extended phase graphs: configuration states, RF transitions, gradient + shifts, relaxation, diffusion, flow, exchange, and the assumptions behind + the model. + +{doc}`implementation` +: How a {class}`~torchsim.model.Simulator` turns either an offline layout or + an incoming sequence description into the fused CPU/GPU state machine, and + how differentiation and execution are arranged. + +```{toctree} +:hidden: +:maxdepth: 1 + +description +epg +implementation +``` From 231b7132b1be45dae36cf538be6d5af6e012eac0 Mon Sep 17 00:00:00 2001 From: Matteo Cencini <83717049+mcencini@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:32:24 +0200 Subject: [PATCH 03/31] docs: make examples an explicit course and tours --- examples/_gallery_header.md | 56 ++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/examples/_gallery_header.md b/examples/_gallery_header.md index bcb92c38..537c7f58 100644 --- a/examples/_gallery_header.md +++ b/examples/_gallery_header.md @@ -2,24 +2,48 @@ # Examples -Worked examples, grouped by what you are trying to do. +The examples have two jobs: a short **Course** teaches the framework itself, +then **Tours** show what can be built on top of it. Every page is executable +and uses the same public interfaces documented in the API reference. -**Framework** is the vocabulary: run a sequence that ships with TorchSim, take -its derivatives, ask a simulator for physics beyond T1 and T2, write a signal -model, write an operator. +## Course -**Parameter inference** turns a measured volume into maps. The same problem is -stated once and handed to a different estimator each time, always on the same -BrainWeb slice, so that what each costs and what each gets wrong are read off -the same numbers. +Read the four **Framework** lessons in order. They are the shortest path from a +shipped sequence to one of your own: -**Sequence optimization** goes the other way and chooses the sequence. The -three pieces are always the same -- a simulator, a cost, a bounded set of -parameters -- and only the cost tells a precision design from an image-quality -one. +1. **Getting started** — run a shipped {class}`torchsim.model.Simulator`, + inspect its sequence description, differentiate the signal, and see where + execution happens. +2. **Expanded physics** — turn on off-resonance, transmit variation, + diffusion, flow, exchange and magnetization transfer without changing the + sequence abstraction. +3. **Writing a simulator** — the extension point of the framework: + subclass {class}`torchsim.model.Simulator`, choose the command handlers + that interpret an incoming Pulseq/MRD event stream, and implement + {meth}`torchsim.model.Simulator.layout` for offline construction. +4. **Custom operator** — package a preparation or readout as an operator while + leaving the state-machine kernels untouched. -**Model-based imaging** reconstructs the maps straight from k-space, with the -signal model inside the forward operator, by a linear subspace or by nonlinear -inversion. +A simulator is deliberately the common object in both directions. Offline, +`layout()` builds the sequence description. During acquisition, +`Simulator.from_description()` takes the description decoded from the +scanner's MRD stream and replays the RF/ADC commands through the simulator's +handlers. The physics, differentiation, execution policy, estimators and +reconstruction code therefore see the same object in either case. -**Miscellaneous** collects everything else. +## Tours + +The remaining sections are standalone applications. They assume the Course, +but not one another. + +**Parameter inference** compares dictionary matching, lookup tables, nonlinear +least squares and PERK on the same mapping problem. + +**Sequence optimization** differentiates through the simulator to design echo +trains, quantitative schedules and RF pulses. + +**Model-based imaging** places the simulator inside the forward model, through +a linear subspace or nonlinear inversion. + +**Miscellaneous** contains complete pipelines that do not belong to the linear +course. From be8fd5c0e644c58689b7087483d6b67848a4370f Mon Sep 17 00:00:00 2001 From: Matteo Cencini <83717049+mcencini@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:32:44 +0200 Subject: [PATCH 04/31] docs: make Simulator the user-facing sequence API --- docs/user_guide.md | 57 ++++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/docs/user_guide.md b/docs/user_guide.md index bd9fd2fc..f3dc2e12 100644 --- a/docs/user_guide.md +++ b/docs/user_guide.md @@ -184,49 +184,46 @@ seems to hang is almost always that compile. ## Your first simulation -A simulator carries a sequence and the tissue it is being asked about; what -you pass at the call is whatever is actually varying. Asking for derivatives -alongside the signal costs one extra pass: +The central public object in TorchSim is a +{class}`~torchsim.model.Simulator`. A shipped simulator fixes the sequence; +`simulate` receives the tissue and anything you want to vary. Asking for a +Jacobian is the same model and the same protocol: ```python import numpy as np -import torchsim +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)) ) -signal, jacobian = torchsim.mrf_sim( - flip=flip, TR=10.0, T1=1000.0, T2=100.0, diff=("T1", "T2") -) +sequence = MRFSimulator(flip=flip, TR=10.0) +signal, jacobian = sequence.jacobian(("T1", "T2"), T1=1000.0, T2=100.0) ``` -`signal` is the forward pass; `jacobian` holds its derivative with respect -to T1 and T2. That derivative is what a dictionary fit, a nonlinear -least-squares map, a model-based reconstruction and a sequence design all -start from, which is why it is one keyword rather than a separate call. +`signal` is the forward pass; `jacobian` holds its derivatives with +respect to T1 and T2. Dictionary fitting, nonlinear least squares, +model-based reconstruction and sequence design all consume the same simulator +interface. + +Functional calls such as {func}`torchsim.mrf_sim` are convenience wrappers +around the shipped simulator classes. They are useful for a one-off call, but +{class}`~torchsim.model.Simulator` is the interface to learn and the class to +subclass when you implement a sequence. Arrays go in and come back in whatever library you wrote them in -- NumPy here, CuPy or PyTorch elsewhere -- over the same memory rather than a copy. ## Finding your way around this documentation -{doc}`explanations/epg` -: What configuration states are, why a train of pulses generates more echoes - than it has pulses, and where relaxation, diffusion, flow and a second - proton pool enter. Read this if EPG is new, or if you want to know what - the simulator is actually computing. - -{doc}`explanations/implementation` -: How that algorithm is realized here: a sequence as a stream of events, one - fused kernel per voxel, derivatives taken forward or backward depending on - what you differentiate, and the shortcuts a run takes when your sequence - allows them. +{doc}`explanations/index` +: The conceptual pages: the sequence description, the EPG physics and the + fused implementation. {doc}`generated/autoexamples/index` -: Worked examples you can run, download or open in Colab. They go from - calling a simulator that ships with TorchSim, through writing one of your - own, to parameter inference, sequence design and model-based - reconstruction. +: The executable Course and Tours. The Course starts with a shipped simulator, + then shows how to implement a new sequence by subclassing + {class}`~torchsim.model.Simulator`; the Tours cover inference, design and + model-based reconstruction. {doc}`api/index` : The reference. Start at {doc}`api/simulators` for what ships, at @@ -240,13 +237,13 @@ CuPy or PyTorch elsewhere -- over the same memory rather than a copy. ## Getting help, and reporting what breaks **Ask a question** in -[Discussions](https://github.com/FiRMLAB-Pisa/torchsim/discussions). How to model +[Discussions](https://github.com/pulserver/torchsim/discussions). How to model a sequence, whether a signal you got is expected, which estimator suits a problem -- these belong there, and the answer is then findable by whoever asks next. **Report a bug** in -[Issues](https://github.com/FiRMLAB-Pisa/torchsim/issues/new/choose), where a +[Issues](https://github.com/pulserver/torchsim/issues/new/choose), where a form asks for what a fix needs: - the shortest script that reproduces it, pasted whole -- a sequence is enough @@ -265,8 +262,8 @@ the same form chooser. Name the paper the model comes from and the figure it would have to reproduce; that is what makes it implementable. **Report a vulnerability** privately instead: open a draft advisory from the -repository's [Security tab](https://github.com/FiRMLAB-Pisa/torchsim/security/advisories/new), -or email the address in the [security policy](https://github.com/FiRMLAB-Pisa/torchsim/blob/main/.github/SECURITY.md). +repository's [Security tab](https://github.com/pulserver/torchsim/security/advisories/new), +or email the address in the [security policy](https://github.com/pulserver/torchsim/blob/main/.github/SECURITY.md). The kernels index raw pointers, so anything reachable from ordinary arguments that reads or writes out of bounds is worth reporting that way rather than in a public issue. Wrong physics is a bug report, not a vulnerability. From 008b6f3766c8248f4de121f9aef2b9430b661dcc Mon Sep 17 00:00:00 2001 From: Matteo Cencini <83717049+mcencini@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:33:10 +0200 Subject: [PATCH 05/31] docs: define Simulator as the canonical extension point --- docs/api/model.md | 43 +++++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/docs/api/model.md b/docs/api/model.md index 0e04b744..89717b04 100644 --- a/docs/api/model.md +++ b/docs/api/model.md @@ -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): @@ -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 From 5de0dbb421df3c88e644656b98d5acdadb7ffc8b Mon Sep 17 00:00:00 2001 From: Matteo Cencini <83717049+mcencini@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:33:12 +0200 Subject: [PATCH 06/31] docs: position functional wrappers as convenience API --- docs/api/simulators.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/api/simulators.md b/docs/api/simulators.md index d08e5117..1a63fb2f 100644 --- a/docs/api/simulators.md +++ b/docs/api/simulators.md @@ -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 From 067902d7229e7465ce9f34a79b5adeb68b79c688 Mon Sep 17 00:00:00 2001 From: Matteo Cencini <83717049+mcencini@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:33:38 +0200 Subject: [PATCH 07/31] docs: make the landing page class-first and current --- README.md | 108 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 68 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index a94b0fdd..b53dfd93 100644 --- a/README.md +++ b/README.md @@ -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 \<\> -- sycomore \<\> -- mri-sim-py \<\> -- ssfp \<\> -- 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. From ca10766c19b1dcc1e8f0c1524671c10c707eefe4 Mon Sep 17 00:00:00 2001 From: Matteo Cencini <83717049+mcencini@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:34:16 +0200 Subject: [PATCH 08/31] docs: add miscellaneous landing page --- docs/misc/index.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 docs/misc/index.md diff --git a/docs/misc/index.md b/docs/misc/index.md new file mode 100644 index 00000000..184e6190 --- /dev/null +++ b/docs/misc/index.md @@ -0,0 +1,13 @@ +# Miscellaneous + +Project information that is useful to keep with the documentation but is not +part of the learning path. + +```{toctree} +:maxdepth: 1 + +related +contributors +code_of_conduct +license +``` From e18ec35f9fbe07c5682c967588182fae50d4dfc0 Mon Sep 17 00:00:00 2001 From: Matteo Cencini <83717049+mcencini@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:34:19 +0200 Subject: [PATCH 09/31] docs: centralize light-dark figure styling --- docs/figure_style.py | 57 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 docs/figure_style.py diff --git a/docs/figure_style.py b/docs/figure_style.py new file mode 100644 index 00000000..08c99c81 --- /dev/null +++ b/docs/figure_style.py @@ -0,0 +1,57 @@ +"""Figure style shared by the documentation gallery and explanation figures. + +The canvas is transparent and the foreground is a mid grey that remains +readable on both the light and dark Sphinx themes. Individual examples should +set only figure-specific geometry, colormaps and annotations. +""" + +from __future__ import annotations + +#: Documentation-column width used by full-width figures. +PAGE_WIDTH = 8.6 + +#: Mid grey with useful contrast against both the light and dark page. +FOREGROUND = "#8a8a8a" + +STYLE = { + "figure.facecolor": "none", + "axes.facecolor": "none", + "savefig.facecolor": "none", + "savefig.transparent": True, + "figure.dpi": 110, + "savefig.dpi": 110, + "figure.constrained_layout.use": True, + "font.size": 13, + "axes.titlesize": 14, + "axes.labelsize": 13, + "xtick.labelsize": 11, + "ytick.labelsize": 11, + "legend.fontsize": 11, + "figure.titlesize": 15, + "legend.frameon": False, + "text.color": FOREGROUND, + "axes.labelcolor": FOREGROUND, + "axes.titlecolor": FOREGROUND, + "axes.edgecolor": FOREGROUND, + "xtick.color": FOREGROUND, + "ytick.color": FOREGROUND, + "xtick.labelcolor": FOREGROUND, + "ytick.labelcolor": FOREGROUND, + "grid.color": FOREGROUND, + "grid.alpha": 0.3, +} + + +def apply() -> None: + """Apply the documentation figure style to Matplotlib.""" + import matplotlib + + matplotlib.rcParams.update(STYLE) + + +def reset(gallery_conf, fname) -> None: + """Reset Matplotlib, then apply the documentation style for one example.""" + import matplotlib + + matplotlib.rcdefaults() + apply() From 0fe82ed51763dd2bc604479dc2c624c11ab4eadf Mon Sep 17 00:00:00 2001 From: Matteo Cencini <83717049+mcencini@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:34:22 +0200 Subject: [PATCH 10/31] docs: add dark-mode-safe documentation styling --- docs/_static/custom.css | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 docs/_static/custom.css diff --git a/docs/_static/custom.css b/docs/_static/custom.css new file mode 100644 index 00000000..50e18305 --- /dev/null +++ b/docs/_static/custom.css @@ -0,0 +1,34 @@ +/* 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 img[src*="/generated/figures/"], +html[data-theme="dark"] .bd-content img[src^="generated/figures/"] { + 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; } From fb3c47f7d77b603bba7ab4e40c668655619a069c Mon Sep 17 00:00:00 2001 From: Matteo Cencini <83717049+mcencini@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:34:32 +0200 Subject: [PATCH 11/31] docs: align site navigation, URLs, and figure rendering --- docs/conf.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 812fddc0..c66b7510 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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 ---------------------------------------------- @@ -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", @@ -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, @@ -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. From 5a747601bd9462a87dfc74001ffe94f0c2f169d1 Mon Sep 17 00:00:00 2001 From: Matteo Cencini <83717049+mcencini@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:34:42 +0200 Subject: [PATCH 12/31] docs: make explanation figures theme-safe --- docs/explanation_figures.py | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/docs/explanation_figures.py b/docs/explanation_figures.py index b24c89be..1738cc1c 100644 --- a/docs/explanation_figures.py +++ b/docs/explanation_figures.py @@ -25,6 +25,7 @@ import numpy as np import torch +from figure_style import FOREGROUND, STYLE from torchsim.model import Simulator from torchsim.sequence import Delay, EventAction, Excitation, SPGRReadout from torchsim.simulators import FSESimulator, MRFSimulator, SPGRSimulator @@ -36,26 +37,11 @@ #: way in and type is the same size on every page. PAGE_WIDTH = 8.6 # inches -STYLE = { - "figure.dpi": 110, - "savefig.dpi": 110, - "font.size": 13, - "axes.titlesize": 14, - "axes.labelsize": 13, - "xtick.labelsize": 11, - "ytick.labelsize": 11, - "legend.fontsize": 11, - "figure.titlesize": 15, - "figure.constrained_layout.use": True, - "axes.spines.top": False, - "axes.spines.right": False, -} - TRANSVERSE = "#1f6f8b" # F states LONGITUDINAL = "#c1553b" # Z states ACCENT = "#2a9d8f" MUTED = "#8d99ae" -INK = "#22223b" +INK = FOREGROUND # White matter at 3 T, which every figure that needs a tissue is drawn on. T1_MS, T2_MS = 830.0, 80.0 @@ -417,7 +403,7 @@ def mono_exponential(): axes[0].plot( times, np.exp(-times / T2_MS), - color="black", + color=FOREGROUND, ls="--", label=r"$e^{-t/T_2}$", ) @@ -439,7 +425,7 @@ def mono_exponential(): [fits[degrees][0] for degrees in trains], color=[MUTED, TRANSVERSE, LONGITUDINAL], ) - axes[1].axhline(T2_MS, color="black", ls="--", label=f"true $T_2$ = {T2_MS:g} ms") + axes[1].axhline(T2_MS, color=FOREGROUND, ls="--", label=f"true $T_2$ = {T2_MS:g} ms") axes[1].set( xlabel="refocusing angle", ylabel="fitted $T_2$ [ms]", @@ -1406,7 +1392,7 @@ def render(directory: str | Path, only: str | None = None) -> list[Path]: continue figure = draw() path = target / f"{stem}.png" - figure.savefig(path, facecolor="white") + figure.savefig(path) plt.close(figure) written.append(path) return written From 5bed131d6c9d1d927df739f102e922783e69c326 Mon Sep 17 00:00:00 2001 From: Matteo Cencini <83717049+mcencini@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:35:02 +0200 Subject: [PATCH 13/31] docs: point project links at pulserver --- docs/developer_guide.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/developer_guide.md b/docs/developer_guide.md index 0180ec3e..49367cbb 100644 --- a/docs/developer_guide.md +++ b/docs/developer_guide.md @@ -49,7 +49,7 @@ the compiler is on the path. :::: -**Git**, and a fork of https://github.com/FiRMLAB-Pisa/torchsim if you intend to +**Git**, and a fork of https://github.com/pulserver/torchsim if you intend to open a pull request. **An NVIDIA card, if you want to touch the GPU kernels.** They are Triton, @@ -60,7 +60,7 @@ card runs it for real. ## Installing for development ```sh -git clone https://github.com/FiRMLAB-Pisa/torchsim +git clone https://github.com/pulserver/torchsim cd torchsim pip install -e ".[dev]" pre-commit install From 2c1913f0777e669cb71e3c63eda0af0ab14bd5bc Mon Sep 17 00:00:00 2001 From: Matteo Cencini <83717049+mcencini@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:35:05 +0200 Subject: [PATCH 14/31] docs: point project links at pulserver --- docs/misc/contributors.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/misc/contributors.md b/docs/misc/contributors.md index ffb28926..ddb28084 100644 --- a/docs/misc/contributors.md +++ b/docs/misc/contributors.md @@ -1,9 +1,9 @@ # Contributors -Contributions to the project can be seen [here](https://github.com/FiRMLAB-Pisa/torchsim/graphs/contributors) +Contributions to the project can be seen [here](https://github.com/pulserver/torchsim/graphs/contributors) ```{raw} html - - + + ``` From 6c2899e5ded0e8af72e173a7a06840159a5bf5f9 Mon Sep 17 00:00:00 2001 From: Matteo Cencini <83717049+mcencini@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:35:08 +0200 Subject: [PATCH 15/31] docs: point project links at pulserver --- docs/sphinx_add_colab_link.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sphinx_add_colab_link.py b/docs/sphinx_add_colab_link.py index c9d0faf5..0878f38b 100644 --- a/docs/sphinx_add_colab_link.py +++ b/docs/sphinx_add_colab_link.py @@ -55,7 +55,7 @@ def run(self): ) # Generate the Colab URL based on GitHub repo information - self.colab_url = f"https://colab.research.google.com/github/FiRMLAB-Pisa/torchsim/blob/{binder['branch']}/{on_the_branch}" + self.colab_url = f"https://colab.research.google.com/github/pulserver/torchsim/blob/{binder['branch']}/{on_the_branch}" # Create the HTML button or link self.html = f"""
From 2bd09b2f1eb0ca1aebd3d6370a6f890c72aca696 Mon Sep 17 00:00:00 2001 From: Matteo Cencini <83717049+mcencini@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:35:11 +0200 Subject: [PATCH 16/31] docs: point project links at pulserver --- scripts/publish_docs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/publish_docs.py b/scripts/publish_docs.py index ea5f1bf6..958335f0 100755 --- a/scripts/publish_docs.py +++ b/scripts/publish_docs.py @@ -130,7 +130,7 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("site", type=Path, help="a checkout of the site branch") parser.add_argument( "--url", - default="https://firmlab-pisa.github.io/torchsim", + default="https://pulserver.github.io/torchsim", help="where the site is served from", ) arguments = parser.parse_args(argv) From 585e20990df02dc35edc5cb6ce169305f575e335 Mon Sep 17 00:00:00 2001 From: Matteo Cencini <83717049+mcencini@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:35:14 +0200 Subject: [PATCH 17/31] docs: point project links at pulserver --- .github/skills/build-the-docs/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/skills/build-the-docs/SKILL.md b/.github/skills/build-the-docs/SKILL.md index 331cbeb1..c60aacf6 100644 --- a/.github/skills/build-the-docs/SKILL.md +++ b/.github/skills/build-the-docs/SKILL.md @@ -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 . +the `gh-pages` branch at . 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 From 4aebc26a02389472ce9729461eabf7b35b0426cc Mon Sep 17 00:00:00 2001 From: Matteo Cencini <83717049+mcencini@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:35:17 +0200 Subject: [PATCH 18/31] docs: point project links at pulserver --- pyproject.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5adb95b4..0cf97ded 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -111,10 +111,10 @@ dev = [ # List URLs that are relevant to your project # This field corresponds to the "Project-URL" and "Home-Page" metadata fields: [project.urls] # Optional -"Homepage" = "https://github.com/FiRMLAB-Pisa/torchsim" -"Documentation" = "https://firmlab-pisa.github.io/torchsim/" -"Bug Reports" = "https://github.com/FiRMLAB-Pisa/torchsim/issues" -"Source" = "https://github.com/FiRMLAB-Pisa/torchsim" +"Homepage" = "https://github.com/pulserver/torchsim" +"Documentation" = "https://pulserver.github.io/torchsim/" +"Bug Reports" = "https://github.com/pulserver/torchsim/issues" +"Source" = "https://github.com/pulserver/torchsim" [tool.scikit-build] minimum-version = "build-system.requires" From 62342232f11044d6acc2924049f9a10d8a8d9ca0 Mon Sep 17 00:00:00 2001 From: Matteo Cencini <83717049+mcencini@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:35:20 +0200 Subject: [PATCH 19/31] docs: point project links at pulserver --- .github/SECURITY.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 1736c537..f28c3e4b 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -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. @@ -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). From 82c2e5b07470a179c95d5cda183e4ce16d96ec13 Mon Sep 17 00:00:00 2001 From: Matteo Cencini <83717049+mcencini@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:35:23 +0200 Subject: [PATCH 20/31] docs: point project links at pulserver --- .github/ISSUE_TEMPLATE/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 0e775b75..2518bebe 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -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. From ac494f23e0c967134ddcd802926250686a0e65c0 Mon Sep 17 00:00:00 2001 From: Matteo Cencini <83717049+mcencini@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:35:26 +0200 Subject: [PATCH 21/31] docs: point project links at pulserver --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index f2c63a3a..a9c81265 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -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 From d993c997e970e8cbd82a8e33c54bf75f54efa430 Mon Sep 17 00:00:00 2001 From: Matteo Cencini <83717049+mcencini@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:35:29 +0200 Subject: [PATCH 22/31] docs: point project links at pulserver --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f0c037d4..7fd8bf55 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 From 5b13661dcca689c892f0d46303de622ad045ed75 Mon Sep 17 00:00:00 2001 From: Matteo Cencini <83717049+mcencini@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:35:45 +0200 Subject: [PATCH 23/31] docs: align API section title --- docs/api/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/index.md b/docs/api/index.md index 4d31b48c..f12a0317 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -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 From 1a6df0e277cb6d110f6b5b237c84dd092c0c8506 Mon Sep 17 00:00:00 2001 From: Matteo Cencini <83717049+mcencini@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:35:47 +0200 Subject: [PATCH 24/31] docs: add explanation summary --- docs/explanations/description.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/explanations/description.md b/docs/explanations/description.md index 5b59a6f5..526dc405 100644 --- a/docs/explanations/description.md +++ b/docs/explanations/description.md @@ -1,5 +1,16 @@ # Sequence description +```{admonition} TL;DR +:class: tldr + +- TorchSim consumes a compact sequence description made of RF, ADC and wait + events plus reusable RF definitions. +- Pulseq files and scanner MRD streams are both decoded into this same + description. +- The description states what was played; a {class}`~torchsim.model.Simulator` + supplies the handlers that decide how those commands affect the EPG state. +``` + A sequence reaches TorchSim as an **event stream**: one repetition's worth of events, each with a timestamp and the few numbers its kind carries. This page is what that stream holds, how it is read out of a Pulseq file, and how a From 1727aa97898000006225306592baf28a8953b6be Mon Sep 17 00:00:00 2001 From: Matteo Cencini <83717049+mcencini@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:35:50 +0200 Subject: [PATCH 25/31] docs: add explanation summary --- docs/explanations/epg.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/explanations/epg.md b/docs/explanations/epg.md index 53f950c7..7f87bd52 100644 --- a/docs/explanations/epg.md +++ b/docs/explanations/epg.md @@ -1,5 +1,16 @@ # Extended phase graphs +```{admonition} TL;DR +:class: tldr + +- EPG represents a voxel by transverse and longitudinal configuration states + indexed by dephasing order. +- RF pulses mix state families, gradients shift transverse orders, and + relaxation/evolution acts between events. +- TorchSim extends that state machine with off-resonance, diffusion, flow and + exchange while retaining the same sequence-level abstraction. +``` + Three pulses generate five echoes, and a train of a hundred generates far more than a hundred. A refocused train, a gradient-echo steady state, a fingerprinting schedule: in each, what you sample is not one decaying From ede6259be173726b36e4fa0c408ba4a13990eaef Mon Sep 17 00:00:00 2001 From: Matteo Cencini <83717049+mcencini@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:35:53 +0200 Subject: [PATCH 26/31] docs: add explanation summary --- docs/explanations/implementation.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/explanations/implementation.md b/docs/explanations/implementation.md index 140f8dfe..bf8bd8a8 100644 --- a/docs/explanations/implementation.md +++ b/docs/explanations/implementation.md @@ -1,5 +1,17 @@ # How TorchSim runs it +```{admonition} TL;DR +:class: tldr + +- {class}`~torchsim.model.Simulator` is the public sequence abstraction. + Offline it builds a description from `layout()`; scanner-driven use starts + from an incoming description and applies the simulator's handlers. +- The resulting event stream is packed once and executed by fused CPU or + Triton kernels, one program per voxel. +- Tissue Jacobians use forward mode; sequence optimization uses reverse mode; + execution/offload policy is shared by every simulator. +``` + {doc}`epg` says what is being computed. This page says how, and why the shape of the code is what it is: one description of a sequence, one fused kernel per voxel, and derivatives taken in whichever direction the question asks for. From c33f3b4164183f48403afe659394fe19faca039b Mon Sep 17 00:00:00 2001 From: Matteo Cencini <83717049+mcencini@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:37:09 +0200 Subject: [PATCH 27/31] docs: record the documentation architecture --- .github/skills/build-the-docs/SKILL.md | 30 ++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/.github/skills/build-the-docs/SKILL.md b/.github/skills/build-the-docs/SKILL.md index c60aacf6..3c121f7b 100644 --- a/.github/skills/build-the-docs/SKILL.md +++ b/.github/skills/build-the-docs/SKILL.md @@ -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. From 30c5b06bbdef8c81b8ba4789686d7348653e98eb Mon Sep 17 00:00:00 2001 From: Matteo Cencini <83717049+mcencini@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:37:16 +0200 Subject: [PATCH 28/31] docs: keep explanation figures transparent in dark mode --- docs/_static/custom.css | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 50e18305..c1c4c301 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -7,8 +7,7 @@ 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 img[src*="/generated/figures/"], -html[data-theme="dark"] .bd-content img[src^="generated/figures/"] { +html[data-theme="dark"] .bd-content figure img { background-color: transparent; filter: none; } From f4ebba24eac02270251063aa2c3f1f96075bf24e Mon Sep 17 00:00:00 2001 From: Matteo Cencini <83717049+mcencini@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:45:04 +0200 Subject: [PATCH 29/31] style: apply pre-commit formatting --- docs/explanation_figures.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/explanation_figures.py b/docs/explanation_figures.py index 1738cc1c..4ffaa75d 100644 --- a/docs/explanation_figures.py +++ b/docs/explanation_figures.py @@ -24,7 +24,6 @@ import matplotlib.pyplot as plt import numpy as np import torch - from figure_style import FOREGROUND, STYLE from torchsim.model import Simulator from torchsim.sequence import Delay, EventAction, Excitation, SPGRReadout @@ -425,7 +424,7 @@ def mono_exponential(): [fits[degrees][0] for degrees in trains], color=[MUTED, TRANSVERSE, LONGITUDINAL], ) - axes[1].axhline(T2_MS, color=FOREGROUND, ls="--", label=f"true $T_2$ = {T2_MS:g} ms") + axes[1].axhline(\n T2_MS, color=FOREGROUND, ls="--", label=f"true $T_2$ = {T2_MS:g} ms"\n ) axes[1].set( xlabel="refocusing angle", ylabel="fitted $T_2$ [ms]", From 1a8d3b2a5f4eaec1ce5183bdd5776f8d73426946 Mon Sep 17 00:00:00 2001 From: Matteo Cencini <83717049+mcencini@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:00:10 +0200 Subject: [PATCH 30/31] fix: repair explanation figure formatting --- docs/explanation_figures.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/explanation_figures.py b/docs/explanation_figures.py index 4ffaa75d..e452f5b2 100644 --- a/docs/explanation_figures.py +++ b/docs/explanation_figures.py @@ -424,7 +424,9 @@ def mono_exponential(): [fits[degrees][0] for degrees in trains], color=[MUTED, TRANSVERSE, LONGITUDINAL], ) - axes[1].axhline(\n T2_MS, color=FOREGROUND, ls="--", label=f"true $T_2$ = {T2_MS:g} ms"\n ) + axes[1].axhline( + T2_MS, color=FOREGROUND, ls="--", label=f"true $T_2$ = {T2_MS:g} ms" + ) axes[1].set( xlabel="refocusing angle", ylabel="fitted $T_2$ [ms]", From d1cc3c33e24e87250aee75baacb117e4272a5a8c Mon Sep 17 00:00:00 2001 From: Matteo Cencini <83717049+mcencini@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:21:28 +0200 Subject: [PATCH 31/31] style: separate local imports for Ruff --- docs/explanation_figures.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/explanation_figures.py b/docs/explanation_figures.py index e452f5b2..b2e25d3c 100644 --- a/docs/explanation_figures.py +++ b/docs/explanation_figures.py @@ -25,6 +25,7 @@ import numpy as np import torch from figure_style import FOREGROUND, STYLE + from torchsim.model import Simulator from torchsim.sequence import Delay, EventAction, Excitation, SPGRReadout from torchsim.simulators import FSESimulator, MRFSimulator, SPGRSimulator