Simulator#

class torchsim.model.Simulator(*args, **kwargs)[source]#

Bases: _SignalModel

A protocol: what a sequence plays, and the physics behind it.

The one thing anything downstream takes. Parameter inference, model-based reconstruction and sequence design are written against this and never ask how the signal is arrived at.

Subclasses set model and implement layout(), which returns the operators of one repetition in the order they are played. A protocol with a closed form – a steady state that needs no state machine – implements evaluate() instead and never reaches layout(); its model then carries only the property declaration, since there are no events for operators to realize.

The constructor takes the keywords simulate() takes, and fixes them; a call overrides. So a sequence is written once with the tissue it is being asked about already on it, and what is left to give per call is whatever is actually varying – the design under optimization, the map being fitted.

A model that composes others rather than declaring physics of its own names its properties in the class body and writes whatever constructor suits it: every setting has a value at the class level, so one that never reaches this constructor still answers simulate().

model#

The physics behind the protocol.

Type:

SpinPhysics, optional

states#

Configuration orders to carry, or None to size them from the winding the description asks for.

Type:

int, optional

Methods

bind

This simulator with more fixed on it, values or settings alike.

describe

Return the description this protocol plays.

evaluate

Run one simulation of the described protocol.

from_description

Return a simulator over a stream someone else assembled.

from_pulseq

Return a simulator over one repetition of a Pulseq sequence.

jacobian

Return the signal and its derivative with respect to diff.

layout

Return the operators of one repetition, in the order they play.

played

Return the protocol as it will be laid out.

repetition_s

Return how long one repetition lasts, given what the layout played.

simulate

Return the recorded signal.

bind(**values)[source]#

This simulator with more fixed on it, values or settings alike.

A property or a protocol argument is held for the next call; a setting – the pulse the events drive, where across the slice to work it out, how many orders to carry – is applied to the copy instead, because it changes what is simulated rather than what is simulated with.

property operators#

What plays each kind of event, from this simulator’s physics.

A layout reads its operators here. Each is resolved while a description is being assembled and never again: what the layout produces carries its own action word, and the run consults no slot.

property variables#

The protocol arguments this simulator’s layout takes.

What a sequence is written in, as against the tissue it is played on: exposes and accepts name the properties, this names the flip angles, spacings and times. Everything here can be fixed on the constructor, given at the call, or carried as a tensor a cost is differentiated back through.

to(device)[source]#

This simulator, with everything it holds on device.

A simulator carries its protocol – echo times, a flip train – and whatever tissue is fixed on it, and the two have to arrive on a card together: properties moved on their own would be multiplied against echo times still on the host.

Parameters:

device (torch.device or str) – Where to put it.

Returns:

A copy. This one is left where it was.

Return type:

Simulator

layout(**protocol)[source]#

Return the operators of one repetition, in the order they play.

A bare operator starts where the one before it ended; one given as (offset_s, operator) starts that far into the repetition instead, which is how a sequence that times itself from an echo says so.

Raises:

NotImplementedError – If the subclass implements neither this nor describe().

repetition_s(played_s, **protocol)[source]#

Return how long one repetition lasts, given what the layout played.

The default is the span the layout covers. A sequence whose TR is longer than what it plays – a refocused train waiting out its recovery – says so here, and only a run of more than one repetition can tell the difference.

played(**sequence)[source]#

Return the protocol as it will be laid out.

The constructor’s arguments, with anything given at the call overriding them, every array read as torch. Anything naming a run setting is left out: those describe the run, and a layout has no use for them.

property rf_raster_time_s#

The dwell this simulator’s RF shapes are sampled on.

describe(**protocol)[source]#

Return the description this protocol plays.

classmethod from_pulseq(source, *, tr_index=None, **settings)[source]#

Return a simulator over one repetition of a Pulseq sequence.

The offline half of from_description(): the same events, read from the file a scanner would be given rather than from the stream it sends back. The file states how many blocks a repetition holds, so nothing here searches for the period; naming the simulator is what says how those events are to be played.

Parameters:
  • source (str, Path or sequence) – A .seq file, or a sequence in memory with pypulseq’s reading interface – pypulseq’s own Sequence, or pypulseqpp’s.

  • tr_index (int, optional) – Which repetition to read, counted in whole repetitions. Defaults to the file’s TRRef definition, and otherwise to the first repetition that acquires – which is refused when the repetitions differ in their pulses.

  • settings (Any) – Run settings and tissue, as the constructor takes them.

Return type:

A simulator playing that repetition.

classmethod from_description(description, model=None, **settings)[source]#

Return a simulator over a stream someone else assembled.

This is the path a description arriving from a scanner takes: FSESimulator.from_description(stream) says the events are to be read as a refocused train, and the only thing left to give is the tissue. Echo spacing, echo train length, flip angles and pulse shapes are in the stream and are not named again.

Which simulator you call it on is the whole of what you choose, and it matters. A description says what was played – an RF pulse, tagged with the use its designer gave it, and an ADC window – and says nothing about the gradients between them, because the transport carries none. The dephasing lives in the handlers instead: a SSFPFidReadout() winds one order after every sample, a SSFPEchoReadout() winds it before, a SPGRReadout() spoils, and a refocusing pulse is crushed either side. So the events are re-emitted through this model’s own operators rather than taken as they arrive.

Parameters:
  • description (SequenceDescription) – The stream, as the MRD client decodes it or a Pulseq design exports it.

  • model (SpinPhysics, optional) – The physics to read it with. Defaults to this simulator’s own, which is what naming a concrete one is for.

  • settings (Any, optional) – Run settings and tissue, as the constructor takes them.

Raises:

ValueError – If called on Simulator itself, which names no handlers and so says nothing about how the stream is to be read.

evaluate(properties, **sequence)[source]#

Run one simulation of the described protocol.

states, repetitions, record, device and execution describe the run and are taken here, each falling back to what the constructor was given; everything else overrides a protocol argument.