\>
+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.
diff --git a/docs/_static/custom.css b/docs/_static/custom.css
new file mode 100644
index 00000000..c1c4c301
--- /dev/null
+++ b/docs/_static/custom.css
@@ -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; }
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
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
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
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.
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
diff --git a/docs/explanation_figures.py b/docs/explanation_figures.py
index b24c89be..b2e25d3c 100644
--- a/docs/explanation_figures.py
+++ b/docs/explanation_figures.py
@@ -24,6 +24,7 @@
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
@@ -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,9 @@ 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 +1394,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
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
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
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.
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
+```
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()
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
```
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
-
-
+
+
```
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
+```
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"""
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.
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.
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"
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)