Homepage#
blochsim simulates the signal of an MR sequence, and its derivatives, in PyTorch. You describe your sequence once, as a signal model, and blochsim plays it over thousands of voxels at once, on the CPU or on an NVIDIA GPU.
Everything else is written once against that signal model: the derivative with respect to tissue that a parameter fit needs, the derivative with respect to the sequence that a protocol design needs, dictionary matching, model-based reconstruction, and reading the same sequence back from a Pulseq file or from the stream a scanner sends. Change the signal model and all of them follow.
blochsim ships signal models for 26 sequences (spin echo, FSE, SPGR, bSSFP, MPRAGE, MP2RAGE, MR fingerprinting, Look-Locker, CEST, ASL…), so for most work you write nothing at all.
How it works#
You construct a simulator with the sequence parameters: the flip angles, the TR, the echo spacing.
You call
simulatewith the tissue: T1, T2, and any other property you care about (B1, off-resonance, diffusion, a second exchanging pool…). A scalar is one voxel, an array is a map, and every voxel runs at once. Naming a property is what turns its physics on; what you leave out costs nothing.blochsim turns the sequence into a stream of events (pulses, delays, readouts) once, and plays it through an extended phase graph (EPG) state machine, compiled for the CPU and the GPU. Calling again with new numbers reuses the stream.
jacobiangives you the derivative with respect to tissue properties, which is what a fit descends. The flip angles are ordinary PyTorch tensors, soloss.backward()gives you the derivative with respect to the sequence, which is what a design optimizes.
Shipped sequences#
import torch
from blochsim.simulators import FSESimulator
fse = FSESimulator(ESP=5.0, TR=3000.0) # ms
flip = torch.full((48,), 180.0) # degrees, one per echo
signal = fse.simulate(flip=flip, T1=1000.0, T2=torch.tensor([80.0, 110.0, 2000.0]))
signal, dT2 = fse.jacobian("T2", flip=flip, T1=1000.0, T2=80.0)
signalhas one row per voxel and one column per echo.Times are in milliseconds and angles in degrees, as on a scanner console.
Every shipped simulator also has a function form for one-off calls, such as
blochsim.fse_sim(...).
Custom signal models#
A signal model is two things:
handlers: which operator plays each kind of event. A sequence has excitations, readouts and waits, and may have refocusing, inversion, saturation, preparation and other pulses, one handler for each Pulseq RF use. Each is a class attribute; one you do not set keeps its default (a wait is
Delay: free precession, no RF and no ADC).layout: the events of one repetition, in order, written with those handlers. It is the sequence’s default description: the constructor builds it from the parameters you pass.
from_pulseqandfrom_descriptionread the description from a.seqfile or a scanner’s stream instead, and skip the layout.
Here is saturation recovery, which saturates, waits, and reads what came back:
import torch
from blochsim import Delay, Excitation, SPGRReadout, Spoil
from blochsim import Simulator
def Saturate(flip_rad=torch.pi / 2, phase_rad=0.0):
"""A 90-degree pulse, then a spoiler."""
return Excitation(flip_rad, phase_rad) @ Spoil()
class SaturationRecovery(Simulator):
saturation = Saturate # plays a saturation pulse
excitation = Excitation # plays an excitation pulse
delay = Delay # plays a wait
readout = SPGRReadout # plays a readout, then spoils
states = 1
def layout(self, *, TS, flip):
angle = torch.deg2rad(torch.as_tensor(flip))
parts = []
for wait in torch.as_tensor(TS) * 1e-3: # ms to s
parts += [self.operators.saturation(), self.operators.delay(wait)]
parts += [self.operators.excitation(angle), self.operators.readout()]
return parts
recovery = SaturationRecovery(TS=[100.0, 400.0, 1600.0], flip=10.0)
signal = recovery.simulate(T1=830.0, T2=80.0)
layoutuses onlyself.operators, the handlers you set. You never write timestamps; each operator holds the timeline for as long as it lasts.The handlers play the events whichever way the description came, so a sequence read with
from_pulseqorfrom_descriptionstill uses them. A Pulseq file or a scanner stream carries pulses, ADC windows and timing but no gradients, so the handlers are where the dephasing lives: the readout decides whether the states are spoiled, wound on or rewound.@composes two operators into one, so a pulse and its spoiler read as the single thing you would name.
Derivatives, devices, extra physics and the estimators all work on your class without another line.
Estimation, design and reconstruction#
Parameter maps: dictionary matching, lookup tables and PERK, all built from a simulator (
blochsim.estimators).Model-based reconstruction: the signal model as a factor of the forward operator, linear (
blochsim.Subspace) or nonlinear (blochsim.ModelOperator). bartorch supplies the encoding and the Gauss-Newton solvers, and fits the same model voxel by voxel withapps.mobafit.Sequence design: flip angles, timings or RF pulse samples optimized by gradient descent through the simulation, within the scanner’s limits (
blochsim.optim.SequenceDesign).Sequences you did not write:
Simulator.from_pulseqreads a.seqfile, andSimulator.from_descriptionreads the sequence description pulserver streams from a running scanner.
Installation#
Install the PyTorch build for your machine first, then blochsim:
pip install blochsim
pip install "blochsim[cu12]" # the GPU kernels, beside a CUDA 12 build of torch
The User guide covers CUDA versions, macOS and building from source.
Learn more#
Course: five lessons, from a shipped simulator to your own signal model and your own operator.
Applications: parameter mapping, sequence design, model-based reconstruction and synthetic data, each a complete worked example.
Documentation, for the physics and every API detail.
How to cite#
If you use blochsim in your work, please cite:
@inproceedings{cencini2025pulserver,
title = {Pulserver: an open-source Pulseq-based client-server framework for vendor agnostic, interactive {MR} sequence design},
author = {Cencini, Matteo and Wang, Kang and Huang, Sherry and Schulte, Rolf F. and Sprenger, Tim
and Noll, Douglas C. and Tosetti, Michela and Nielsen, Jon-Fredrik},
booktitle = {Proceedings of the International Society for Magnetic Resonance in Medicine},
pages = {1275},
year = {2025}
}
blochsim’s state machine is the extended phase graph formalism (Weigel, 2015); please cite it too.
License#
MIT, see LICENSE.