5f2d2b3mri-scanner: in-browser Bloch simulation of pulseq sequencesJeremy Magland 1// React hook that owns the simulation worker: start a run, stream progress,
2// cancel by terminating the worker (à la seqlab's runner).
3import { useCallback, useEffect, useRef, useState } from 'react'
4import type { RawSignal, SimProgress } from './simulate.ts'
5import type { RunMessage, WorkerOut } from './simWorker.ts'
6import type { Phantom } from '../phantom/phantomTypes.ts'
8export type SimStatus = 'idle' | 'running' | 'done' | 'error' | 'cancelled'
10export interface SimState {
11 status: SimStatus
12 progress: SimProgress | null
13 elapsedMs: number
14 result: RawSignal | null
15 error: string | null
16}
18const INITIAL: SimState = {
19 status: 'idle',
20 progress: null,
21 elapsedMs: 0,
22 result: null,
23 error: null,
24}
26export function useSimulation() {
27 const [state, setState] = useState<SimState>(INITIAL)
28 const workerRef = useRef<Worker | null>(null)
30 const teardown = useCallback(() => {
31 if (workerRef.current) {
32 workerRef.current.terminate()
33 workerRef.current = null
34 }
35 }, [])
37 useEffect(() => () => teardown(), [teardown])
39 const run = useCallback(
40 (seqText: string, phantom: Phantom) => {
41 teardown()
42 const worker = new Worker(new URL('./simWorker.ts', import.meta.url), { type: 'module' })
43 workerRef.current = worker
44 setState({ status: 'running', progress: null, elapsedMs: 0, result: null, error: null })
46 worker.onmessage = (ev: MessageEvent<WorkerOut>) => {
47 const msg = ev.data
48 if (msg.type === 'progress') {
49 setState((s) => (s.status === 'running' ? { ...s, progress: msg.progress, elapsedMs: msg.elapsedMs } : s))
50 } else if (msg.type === 'done') {
51 const result: RawSignal = {
52 re: msg.re,
53 im: msg.im,
54 numReadouts: msg.numReadouts,
55 samplesPerReadout: msg.samplesPerReadout,
56 offsets: msg.offsets,
57 maxSamplesPerReadout: msg.maxSamplesPerReadout,
58 }
59 setState((s) => ({ ...s, status: 'done', result, elapsedMs: msg.elapsedMs }))
60 teardown()
61 } else if (msg.type === 'error') {
62 setState((s) => ({ ...s, status: 'error', error: msg.message }))
63 teardown()
64 }
65 }
66 worker.onerror = (ev) => {
67 setState((s) => ({ ...s, status: 'error', error: ev.message || 'Worker error' }))
68 teardown()
69 }
71 const runMsg: RunMessage = { type: 'run', seqText, phantom }
72 worker.postMessage(runMsg)
73 },
74 [teardown],
75 )
77 const cancel = useCallback(() => {
78 teardown()
79 setState((s) => (s.status === 'running' ? { ...s, status: 'cancelled' } : s))
80 }, [teardown])
82 const reset = useCallback(() => {
83 teardown()
84 setState(INITIAL)
85 }, [teardown])
87 return { state, run, cancel, reset }
88}