seqlab: write and view pulseq MRI sequences in the browser
MATLAB-syntax pulseq scripting executed in-browser on numbl, generating
downloadable .seq files, plus an interactive .seq viewer (timeline, block
inspector, report) that also opens uploaded files. Includes 12 runnable
examples adapted from pulseq's demoSeq.
142 changed files+28292−0
.github/workflows/deploy.ymladded+40−0View file
@@ -0,0 +1,40 @@
1+name: Deploy to GitHub Pages
2+
3+on:
4+ push:
5+ branches: [main]
6+ workflow_dispatch:
7+
8+permissions:
9+ contents: read
10+ pages: write
11+ id-token: write
12+
13+concurrency:
14+ group: pages
15+ cancel-in-progress: false
16+
17+jobs:
18+ build:
19+ runs-on: ubuntu-latest
20+ steps:
21+ - uses: actions/checkout@v4
22+ - uses: actions/setup-node@v4
23+ with:
24+ node-version: 22
25+ cache: npm
26+ - run: npm ci
27+ - run: npm run build
28+ - uses: actions/upload-pages-artifact@v3
29+ with:
30+ path: dist
31+
32+ deploy:
33+ needs: build
34+ runs-on: ubuntu-latest
35+ environment:
36+ name: github-pages
37+ url: ${{ steps.deployment.outputs.page_url }}
38+ steps:
39+ - id: deployment
40+ uses: actions/deploy-pages@v4
.gitignoreadded+4−0View file
@@ -0,0 +1,4 @@
1+node_modules
2+dist
3+.cache
4+*.local
CLAUDE.mdadded+95−0View file
@@ -0,0 +1,95 @@
1+# CLAUDE.md
2+
3+Tips for future agents working in this repo.
4+
5+## Architecture
6+
7+```
8+src/engine/pulseq/ vendored pulseq +mr tree (86 .m files) + LICENSE +
9+ VENDORED.md; NEVER edit — re-sync with `npm run
10+ vendor-pulseq` (reads ../../pulseq by default)
11+src/engine/mrFiles.ts +mr tree as numbl boot files (vite ?raw glob), with
12+ +mr/+aux/md5.m replaced by a wrapper over numbl's
13+ `hash` builtin (the original needs Java on MATLAB)
14+src/engine/runner.ts run-per-execution wrapper over numbl/browser
15+ createNumblSession: boots idle (mip off, no
16+ persistence) with +mr + main.m staged, executes
17+ 'main;', collects *.seq via a dir() snippet, disposes.
18+ Cancel = dispose (works without cross-origin isolation).
19+src/seq/parseSeq.ts .seq parser (v1.4.x/1.5.x), column layouts and unit
20+ scales mirror pulseq's @Sequence/read.m; shape RLE
21+ decompression mirrors decompressShape.m
22+src/seq/reconstruct.ts ParsedSeq -> plottable polylines, mirroring
23+ Sequence.waveforms_and_times (trap vertices, centers-
24+ raster arbitrary grads get first/last edge points,
25+ extended trapezoids use vertices as stored)
26+src/viewer/ canvas Timeline (min/max pixel decimation, wheel zoom,
27+ drag pan, click-to-select block), ReportPanel,
28+ BlockInspector
29+src/examples/ gallery: 3 curated .m (fid/epi/gre, hand-written with
30+ the autoLabel toggle), catalog.ts (name/category/desc per
31+ id), generated-timings.ts (AUTO), and pulseq/*.m —
32+ auto-adapted demoSeq scripts written by gen-examples.mjs.
33+ ExamplesModal groups them by category.
34+test-data/golden/ .seq goldens generated by Octave and verified
35+ byte-identical against real MATLAB R2026b
36+```
37+
38+## Examples pipeline
39+
40+The gallery's pulseq demos are generated, not hand-maintained:
41+
42+- `node scripts/test-demos.mjs` batch-runs every `../../pulseq/matlab/demoSeq`
43+ script on numbl in isolated child processes. It **adapts** each (strips
44+ visualization/analysis/scanner-side-effect statements, truncates after the
45+ first `seq.write`, re-appends trailing local functions), stages +mr with the
46+ md5 + isSigPyAvailable overrides, runs it, and parses the output with the JS
47+ parser. Writes `scripts/demo-results.json`.
48+- `node scripts/gen-examples.mjs` reads that JSON and writes `src/examples/
49+ pulseq/<id>.m` for every demo that ran cleanly AND has a `catalog.ts` entry
50+ that isn't already curated (fid/epi/gre), plus `generated-timings.ts`.
51+- 12 of 42 demos run on numbl today (fid, epi, EpiRS, EpiSpinEcho,
52+ EpiSpinEchoRS, FastRadialGradientEcho, GradientEcho/gre, GRE_live_demo
53+ [_step0], RadialGradientEcho, UTE, UTE_rs). The rest hit numbl gaps — see
54+ the numbl-gap list in the `seqlab-project` memory. Re-run both scripts after
55+ numbl improvements to pick up newly-working demos.
56+- `adaptDemo` is exported from test-demos.mjs and reused by gen-examples; that
57+ file only runs the batch when executed directly (isMain guard), so importing
58+ it for `adaptDemo` is safe.
59+
60+## Key gotchas
61+
62+- **numbl >= 0.4.17.** Needs the pulseq-compat fixes (inputParser optionals,
63+ dictionary FallbackValue, containers.Map sorted keys, `hash` builtin,
64+ fnint/ppint, warning('off',id), sprintf -0, …). package.json points at
65+ `file:../../numbl` while 0.4.17 is unpublished; switch to `^0.4.17` before
66+ deploy, then `rm -rf node_modules package-lock.json && npm install`. For
67+ local numbl changes run `npm run build:lib && npm run build:browser` there.
68+- **The .seq filename is discovered, not assumed.** The runner lists `*.seq`
69+ in the session cwd after the run (COLLECT_SNIPPET); scripts may write any
70+ name(s), and each file becomes a viewer tab.
71+- **`execute('main;')`, not the script text.** Running by name keeps MATLAB
72+ script semantics (local functions allowed); the trailing semicolon
73+ suppresses a spurious `ans` display under the REPL's displayResults.
74+- **scripts/engine-test.mjs mirrors worker.ts exactly** (idle boot: cwd
75+ /project, relative workspace-file names, mainScriptPath 'repl', empty
76+ search paths). If numbl's browser worker changes its call shape, update the
77+ mirror or the test stops being representative.
78+- **Node runs the .ts sources directly** (Node >= 22.6 type stripping), which
79+ is why src/seq imports use explicit `.ts` extensions.
80+- **GRE takes ~30 s** in-browser/Node (interpreter mode; numbl's JIT declines
81+ most pulseq code — see numbl-jit-performance). FID ~1 s, EPI ~2 s. Keep
82+ gallery time labels honest.
83+- **Signature hashing:** the md5 covers the file up to but excluding the
84+ newline that precedes `[SIGNATURE]`.
85+
86+## Testing
87+
88+- `npm run engine-test` — FID through the exact worker call shape,
89+ byte-compared against test-data/golden/fid.seq.
90+- `npm run parser-test` — parser/reconstruction invariants on all 5 goldens
91+ (fid, gre, seq1-3): signature verification, block/event counts, tamper
92+ detection, RLE round-trip.
93+- `node scripts/run-example.mjs <fid|epi|gre>` — run any example headlessly.
94+- Browser verification (worker boot, editor, timeline interactions, drag &
95+ drop) is manual: `npm run dev`.
README.mdadded+43−0View file
@@ -0,0 +1,43 @@
1+# seqlab
2+
3+Write [pulseq](https://pulseq.github.io/) MRI pulse sequences in MATLAB syntax,
4+run them entirely in your browser, and explore the resulting `.seq` files in an
5+interactive viewer. No MATLAB, no installation — the unmodified pulseq `+mr`
6+toolbox executes on [numbl](https://numbl.org), a MATLAB-compatible runtime
7+that runs in a web worker, and produces `.seq` files byte-identical to desktop
8+MATLAB (including the md5 signature).
9+
10+Also works as a plain `.seq` viewer: drop any pulseq 1.4/1.5 file onto the
11+page and it is parsed instantly in JavaScript — no script run needed.
12+
13+## Features
14+
15+- **Script workspace** — MATLAB editor (CodeMirror) with the pulseq `+mr`
16+ package preloaded; press Run and watch the console stream. Scripts download
17+ as `.m` files that run unchanged in desktop MATLAB.
18+- **Interactive timeline** — RF magnitude/phase, gx/gy/gz, and ADC lanes with
19+ synchronized zoom/pan down to microsecond detail; click any block to inspect
20+ its events numerically.
21+- **Sequence report** — definitions, duration, event counts, peak gradient and
22+ slew, and md5 signature verification.
23+- **Examples gallery** — FID, EPI, and gradient-echo demos adapted from
24+ pulseq's `demoSeq`, linkable as `#example/gre` etc.
25+
26+## Development
27+
28+```sh
29+npm install
30+npm run dev # local dev server
31+npm run engine-test # headless: run the FID demo, byte-compare vs MATLAB golden
32+npm run parser-test # .seq parser + reconstruction checks vs golden files
33+node scripts/run-example.mjs gre # run any example headlessly, timed
34+```
35+
36+The pulseq `+mr` tree is vendored under `src/engine/pulseq/` (MIT, see the
37+LICENSE there); re-sync it from a pulseq checkout with `npm run vendor-pulseq`.
38+
39+## Credits
40+
41+[pulseq](https://github.com/pulseq/pulseq) is developed at the University
42+Medical Center Freiburg and is MIT-licensed. This project is not affiliated
43+with the pulseq developers.
index.htmladded+17−0View file
@@ -0,0 +1,17 @@
1+<!doctype html>
2+<html lang="en">
3+ <head>
4+ <meta charset="UTF-8" />
5+ <link rel="icon" type="image/svg+xml" href="./favicon.svg" />
6+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+ <meta
8+ name="description"
9+ content="seqlab — write pulseq MRI pulse sequences in MATLAB syntax, run them in your browser, and explore the resulting .seq files interactively."
10+ />
11+ <title>seqlab — pulseq in the browser</title>
12+ </head>
13+ <body>
14+ <div id="root"></div>
15+ <script type="module" src="/src/main.tsx"></script>
16+ </body>
17+</html>
package-lock.jsonadded+3325−0View file
This diff is 3,330 lines long and is not shown.
package.jsonadded+33−0View file
@@ -0,0 +1,33 @@
1+{
2+ "name": "seqlab",
3+ "private": true,
4+ "version": "0.0.0",
5+ "type": "module",
6+ "scripts": {
7+ "dev": "vite",
8+ "build": "tsc -b && vite build",
9+ "lint": "oxlint",
10+ "preview": "vite preview",
11+ "vendor-pulseq": "node scripts/vendor-pulseq.mjs",
12+ "engine-test": "node scripts/engine-test.mjs",
13+ "parser-test": "node scripts/parser-test.mjs"
14+ },
15+ "dependencies": {
16+ "@codemirror/language": "^6.11.3",
17+ "@codemirror/legacy-modes": "^6.5.2",
18+ "@codemirror/theme-one-dark": "^6.1.3",
19+ "codemirror": "^6.0.2",
20+ "numbl": "^0.4.18",
21+ "react": "^19.2.7",
22+ "react-dom": "^19.2.7"
23+ },
24+ "devDependencies": {
25+ "@types/node": "^24.13.2",
26+ "@types/react": "^19.2.17",
27+ "@types/react-dom": "^19.2.3",
28+ "@vitejs/plugin-react": "^6.0.3",
29+ "oxlint": "^1.71.0",
30+ "typescript": "~6.0.2",
31+ "vite": "^8.1.1"
32+ }
33+}
public/favicon.svgadded+4−0View file
@@ -0,0 +1,4 @@
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2+ <rect width="32" height="32" rx="6" fill="#12161d"/>
3+ <path d="M3 22 h5 l2 -12 l3 18 l3 -24 l3 20 l2 -8 h8" fill="none" stroke="#4da3ff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
4+</svg>
scripts/demo-results.jsonadded+427−0View file
@@ -0,0 +1,427 @@
1+[
2+ {
3+ "demo": "write_demo_automatic_TRID_labels.m",
4+ "ok": false,
5+ "error": "Builtin 'double' does not support these argument types: (cell) @ /project/+mr/@Sequence/Sequence.m:154",
6+ "seqFiles": [],
7+ "elapsedMs": 3551
8+ },
9+ {
10+ "demo": "writeCineGradientEcho.m",
11+ "ok": false,
12+ "error": "timeout or crash",
13+ "seqFiles": [],
14+ "elapsedMs": 120000,
15+ "timedOut": true
16+ },
17+ {
18+ "demo": "writeEpi_label.m",
19+ "ok": false,
20+ "error": "timeout or crash",
21+ "seqFiles": [],
22+ "elapsedMs": 120000,
23+ "timedOut": true
24+ },
25+ {
26+ "demo": "writeEpi.m",
27+ "ok": true,
28+ "error": null,
29+ "seqFiles": [
30+ {
31+ "name": "epi.seq",
32+ "bytes": 51653,
33+ "blocks": 390,
34+ "rf": 3,
35+ "adc": 192,
36+ "dur": 0.15405000000000102,
37+ "sig": true,
38+ "parseOk": true
39+ }
40+ ],
41+ "elapsedMs": 3941
42+ },
43+ {
44+ "demo": "writeEpiDiffusionRS_PMC.m",
45+ "ok": false,
46+ "error": "Undefined function or variable 'g' @ main.m:42",
47+ "seqFiles": [],
48+ "elapsedMs": 769
49+ },
50+ {
51+ "demo": "writeEpiDiffusionRS.m",
52+ "ok": false,
53+ "error": "Undefined function or variable 'g' @ main.m:42",
54+ "seqFiles": [],
55+ "elapsedMs": 664
56+ },
57+ {
58+ "demo": "writeEpiRS_label_softdelay.m",
59+ "ok": false,
60+ "error": "timeout or crash",
61+ "seqFiles": [],
62+ "elapsedMs": 120000,
63+ "timedOut": true
64+ },
65+ {
66+ "demo": "writeEpiRS_label_trans.m",
67+ "ok": false,
68+ "error": "timeout or crash",
69+ "seqFiles": [],
70+ "elapsedMs": 120000,
71+ "timedOut": true
72+ },
73+ {
74+ "demo": "writeEpiRS_label.m",
75+ "ok": false,
76+ "error": "timeout or crash",
77+ "seqFiles": [],
78+ "elapsedMs": 120000,
79+ "timedOut": true
80+ },
81+ {
82+ "demo": "writeEpiRS.m",
83+ "ok": true,
84+ "error": null,
85+ "seqFiles": [
86+ {
87+ "name": "epi_rs.seq",
88+ "bytes": 132167,
89+ "blocks": 67,
90+ "rf": 2,
91+ "adc": 64,
92+ "dur": 0.042680000000000065,
93+ "sig": true,
94+ "parseOk": true
95+ }
96+ ],
97+ "elapsedMs": 3562
98+ },
99+ {
100+ "demo": "writeEpiSpinEcho.m",
101+ "ok": true,
102+ "error": null,
103+ "seqFiles": [
104+ {
105+ "name": "epi_se.seq",
106+ "bytes": 44140,
107+ "blocks": 136,
108+ "rf": 2,
109+ "adc": 64,
110+ "dur": 0.08315000000000002,
111+ "sig": true,
112+ "parseOk": true
113+ }
114+ ],
115+ "elapsedMs": 2075
116+ },
117+ {
118+ "demo": "writeEpiSpinEchoRS.m",
119+ "ok": true,
120+ "error": null,
121+ "seqFiles": [
122+ {
123+ "name": "epi_se_rs.seq",
124+ "bytes": 136363,
125+ "blocks": 180,
126+ "rf": 9,
127+ "adc": 168,
128+ "dur": 0.21735000000000068,
129+ "sig": true,
130+ "parseOk": true
131+ }
132+ ],
133+ "elapsedMs": 4504
134+ },
135+ {
136+ "demo": "writeFastRadialGradientEcho_rot3D.m",
137+ "ok": false,
138+ "error": "cell2mat: cell contents must be numeric arrays @ +mr/addGradients.m:205",
139+ "seqFiles": [],
140+ "elapsedMs": 1083
141+ },
142+ {
143+ "demo": "writeFastRadialGradientEcho_rotExt.m",
144+ "ok": false,
145+ "error": "cell2mat: cell contents must be numeric arrays @ +mr/addGradients.m:205",
146+ "seqFiles": [],
147+ "elapsedMs": 1332
148+ },
149+ {
150+ "demo": "writeFastRadialGradientEcho.m",
151+ "ok": true,
152+ "error": null,
153+ "seqFiles": [
154+ {
155+ "name": "fast_gre_rad.seq",
156+ "bytes": 41120,
157+ "blocks": 534,
158+ "rf": 266,
159+ "adc": 256,
160+ "dur": 0.834009999999998,
161+ "sig": true,
162+ "parseOk": true
163+ }
164+ ],
165+ "elapsedMs": 13054
166+ },
167+ {
168+ "demo": "writeFid.m",
169+ "ok": true,
170+ "error": null,
171+ "seqFiles": [
172+ {
173+ "name": "fid.seq",
174+ "bytes": 2346,
175+ "blocks": 32,
176+ "rf": 16,
177+ "adc": 16,
178+ "dur": 80.32000000000001,
179+ "sig": true,
180+ "parseOk": true
181+ }
182+ ],
183+ "elapsedMs": 1729
184+ },
185+ {
186+ "demo": "writeGradientEcho_grappa.m",
187+ "ok": false,
188+ "error": "timeout or crash",
189+ "seqFiles": [],
190+ "elapsedMs": 120000,
191+ "timedOut": true
192+ },
193+ {
194+ "demo": "writeGradientEcho_label.m",
195+ "ok": false,
196+ "error": "Cannot convert struct to number @ +mr/makeLabel.m:38",
197+ "seqFiles": [],
198+ "elapsedMs": 4377
199+ },
200+ {
201+ "demo": "writeGradientEcho.m",
202+ "ok": false,
203+ "error": "timeout or crash",
204+ "seqFiles": [],
205+ "elapsedMs": 120000,
206+ "timedOut": true
207+ },
208+ {
209+ "demo": "writeGradientEcho3D.m",
210+ "ok": false,
211+ "error": "timeout or crash",
212+ "seqFiles": [],
213+ "elapsedMs": 120000,
214+ "timedOut": true
215+ },
216+ {
217+ "demo": "writeGRE_live_demo_step0.m",
218+ "ok": true,
219+ "error": null,
220+ "seqFiles": [
221+ {
222+ "name": "DEMO_grep0.seq",
223+ "bytes": 75934,
224+ "blocks": 640,
225+ "rf": 128,
226+ "adc": 128,
227+ "dur": 2.0480000000000005,
228+ "sig": true,
229+ "parseOk": true
230+ }
231+ ],
232+ "elapsedMs": 12263
233+ },
234+ {
235+ "demo": "writeGRE_live_demo.m",
236+ "ok": true,
237+ "error": null,
238+ "seqFiles": [
239+ {
240+ "name": "DEMO_gre0.seq",
241+ "bytes": 75934,
242+ "blocks": 640,
243+ "rf": 128,
244+ "adc": 128,
245+ "dur": 2.0480000000000005,
246+ "sig": true,
247+ "parseOk": true
248+ }
249+ ],
250+ "elapsedMs": 12960
251+ },
252+ {
253+ "demo": "writeGRE_multiEcho_label.m",
254+ "ok": false,
255+ "error": "Cannot convert struct to number @ +mr/makeLabel.m:38",
256+ "seqFiles": [],
257+ "elapsedMs": 4730
258+ },
259+ {
260+ "demo": "writeGRE_rfShim.m",
261+ "ok": false,
262+ "error": "Cannot convert struct to number @ +mr/makeLabel.m:38",
263+ "seqFiles": [],
264+ "elapsedMs": 4357
265+ },
266+ {
267+ "demo": "writeHASTE.m",
268+ "ok": false,
269+ "error": "Undefined function or variable 'main' @ repl:1",
270+ "seqFiles": [],
271+ "elapsedMs": 848
272+ },
273+ {
274+ "demo": "writeMPRAGE_4ge.m",
275+ "ok": false,
276+ "error": "Undefined function or variable 'getGEparams' @ main.m:20",
277+ "seqFiles": [],
278+ "elapsedMs": 1121
279+ },
280+ {
281+ "demo": "writeMPRAGE_grappa.m",
282+ "ok": false,
283+ "error": "setdiff: arguments must be numeric arrays @ main.m:33",
284+ "seqFiles": [],
285+ "elapsedMs": 1276
286+ },
287+ {
288+ "demo": "writeMPRAGE.m",
289+ "ok": false,
290+ "error": "setdiff: arguments must be numeric arrays @ main.m:31",
291+ "seqFiles": [],
292+ "elapsedMs": 980
293+ },
294+ {
295+ "demo": "writePRESS.m",
296+ "ok": true,
297+ "error": null,
298+ "seqFiles": [],
299+ "elapsedMs": 1291
300+ },
301+ {
302+ "demo": "writeRadialGradientEcho_rotExt.m",
303+ "ok": false,
304+ "error": "timeout or crash",
305+ "seqFiles": [],
306+ "elapsedMs": 120000,
307+ "timedOut": true
308+ },
309+ {
310+ "demo": "writeRadialGradientEcho.m",
311+ "ok": true,
312+ "error": null,
313+ "seqFiles": [
314+ {
315+ "name": "gre_rad.seq",
316+ "bytes": 122142,
317+ "blocks": 1385,
318+ "rf": 277,
319+ "adc": 256,
320+ "dur": 5.542769999999974,
321+ "sig": true,
322+ "parseOk": true
323+ }
324+ ],
325+ "elapsedMs": 20595
326+ },
327+ {
328+ "demo": "writeSelectiveRf.m",
329+ "ok": false,
330+ "error": "Undefined function or variable 'signal' @ main.m:42",
331+ "seqFiles": [],
332+ "elapsedMs": 1523
333+ },
334+ {
335+ "demo": "writeSemiLaser.m",
336+ "ok": true,
337+ "error": null,
338+ "seqFiles": [],
339+ "elapsedMs": 843
340+ },
341+ {
342+ "demo": "writeSpinEcho_softdelay.m",
343+ "ok": true,
344+ "error": null,
345+ "seqFiles": [],
346+ "elapsedMs": 851
347+ },
348+ {
349+ "demo": "writeSpiral.m",
350+ "ok": false,
351+ "error": "Maximum call stack size exceeded @ +mr/traj2grad.m:76",
352+ "seqFiles": [],
353+ "elapsedMs": 53815
354+ },
355+ {
356+ "demo": "writeTrufi.m",
357+ "ok": false,
358+ "error": "timeout or crash",
359+ "seqFiles": [],
360+ "elapsedMs": 120000,
361+ "timedOut": true
362+ },
363+ {
364+ "demo": "writeTSE.m",
365+ "ok": false,
366+ "error": "Undefined function or variable 'main' @ repl:1",
367+ "seqFiles": [],
368+ "elapsedMs": 851
369+ },
370+ {
371+ "demo": "writeTSEprop.m",
372+ "ok": false,
373+ "error": "Undefined function or variable 'main' @ repl:1",
374+ "seqFiles": [],
375+ "elapsedMs": 722
376+ },
377+ {
378+ "demo": "writeUTE_rs.m",
379+ "ok": true,
380+ "error": null,
381+ "seqFiles": [
382+ {
383+ "name": "ute_rs.seq",
384+ "bytes": 80934,
385+ "blocks": 2002,
386+ "rf": 1000,
387+ "adc": 960,
388+ "dur": 20.00000000000037,
389+ "sig": true,
390+ "parseOk": true
391+ }
392+ ],
393+ "elapsedMs": 28840
394+ },
395+ {
396+ "demo": "writeUTE.m",
397+ "ok": true,
398+ "error": null,
399+ "seqFiles": [
400+ {
401+ "name": "ute.seq",
402+ "bytes": 53680,
403+ "blocks": 1024,
404+ "rf": 256,
405+ "adc": 256,
406+ "dur": 2.5600000000000143,
407+ "sig": true,
408+ "parseOk": true
409+ }
410+ ],
411+ "elapsedMs": 18314
412+ },
413+ {
414+ "demo": "writeZTE_Petra_sodium.m",
415+ "ok": false,
416+ "error": "Undefined function or variable 'Kr' @ main.m:37",
417+ "seqFiles": [],
418+ "elapsedMs": 642
419+ },
420+ {
421+ "demo": "writeZTE_Petra.m",
422+ "ok": false,
423+ "error": "Undefined function or variable 'Kr' @ main.m:37",
424+ "seqFiles": [],
425+ "elapsedMs": 581
426+ }
427+]
scripts/engine-test.mjsadded+118−0View file
@@ -0,0 +1,118 @@
1+// Headless check of the run pipeline in Node against the local numbl build:
2+// stages the vendored +mr tree + the FID example exactly the way the
3+// numbl/browser worker does for an idle boot (cwd /project, relative
4+// workspace-file names, execute-by-name with mainScriptPath 'repl' and no
5+// search paths), runs it, collects *.seq via the same dir() snippet the
6+// runner uses, and byte-compares the result against the MATLAB golden.
7+import fs from 'node:fs'
8+import path from 'node:path'
9+import { fileURLToPath } from 'node:url'
10+import {
11+ executeCode,
12+ VirtualFileSystem,
13+ BrowserFileIOAdapter,
14+ BrowserSystemAdapter,
15+} from 'numbl'
16+
17+const here = path.dirname(fileURLToPath(import.meta.url))
18+const repoRoot = path.join(here, '..')
19+const mrRoot = path.join(repoRoot, 'src', 'engine', 'pulseq')
20+
21+const MD5_OVERRIDE = `function digest = md5(message, noBuiltIn)
22+ digest = hash('MD5', char(message));
23+end
24+`
25+
26+const COLLECT_SNIPPET = `seqlab_d = dir('*.seq');
27+for seqlab_i = 1:numel(seqlab_d)
28+ fprintf('SEQLAB_FILE:%s\\n', seqlab_d(seqlab_i).name);
29+end
30+`
31+
32+// Boot files, as mrFiles.ts builds them (paths relative to /project).
33+function bootFiles() {
34+ const files = []
35+ for (const entry of fs.readdirSync(path.join(mrRoot, '+mr'), { recursive: true })) {
36+ const rel = String(entry)
37+ const abs = path.join(mrRoot, '+mr', rel)
38+ if (!fs.statSync(abs).isFile() || !rel.endsWith('.m')) continue
39+ const vfsPath = `+mr/${rel}`
40+ files.push({
41+ path: vfsPath,
42+ content: vfsPath === '+mr/+aux/md5.m' ? MD5_OVERRIDE : fs.readFileSync(abs, 'utf8'),
43+ })
44+ }
45+ return files
46+}
47+
48+const script = fs.readFileSync(path.join(repoRoot, 'src', 'examples', 'fid.m'), 'utf8')
49+const files = [...bootFiles(), { path: 'main.m', content: script }]
50+
51+// Mirror of worker.ts boot (idle) + execute('main').
52+const enc = new TextEncoder()
53+const vfs = new VirtualFileSystem()
54+for (const f of files) vfs.writeFile('/project/' + f.path, enc.encode(f.content))
55+vfs.setCwd('/project')
56+const workspaceFiles = files.map((f) => ({ name: f.path, source: f.content }))
57+
58+const workerOptions = {
59+ onOutput: (text) => process.stdout.write(`[numbl] ${text}`),
60+ displayResults: true,
61+ maxIterations: 1e9,
62+ optimization: '1',
63+ fileIO: new BrowserFileIOAdapter(vfs),
64+ system: new BrowserSystemAdapter(vfs),
65+}
66+
67+const t0 = Date.now()
68+let result
69+try {
70+ result = executeCode('main;', workerOptions, workspaceFiles, 'repl', [])
71+} catch (err) {
72+ console.error('run failed:', err.message ?? err)
73+ if (err.file) console.error(` at ${err.file}:${err.line}`)
74+ process.exit(1)
75+}
76+console.log(`run finished in ${((Date.now() - t0) / 1000).toFixed(1)} s`)
77+
78+// Collect .seq files through the same follow-up execute the runner performs.
79+const collected = []
80+const collectResult = executeCode(
81+ COLLECT_SNIPPET,
82+ {
83+ ...workerOptions,
84+ onOutput: (text) => collected.push(text),
85+ initialVariableValues: result.variableValues,
86+ initialHoldState: result.holdState,
87+ implicitCwdPath: result.implicitCwdPath,
88+ },
89+ workspaceFiles,
90+ 'repl',
91+ result.searchPaths ?? [],
92+)
93+if (!collectResult) process.exit(1)
94+const names = [...collected.join('').matchAll(/^SEQLAB_FILE:(.+)$/gm)].map((m) => m[1])
95+console.log('collected .seq files:', names)
96+if (names.length !== 1 || names[0] !== 'fid.seq') {
97+ console.error('FAIL: expected exactly [fid.seq]')
98+ process.exit(1)
99+}
100+
101+const produced = Buffer.from(vfs.readFile('/project/fid.seq'))
102+const golden = fs.readFileSync(path.join(repoRoot, 'test-data', 'golden', 'fid.seq'))
103+if (produced.equals(golden)) {
104+ console.log(`PASS: fid.seq is byte-identical to the MATLAB golden (${golden.length} bytes)`)
105+} else {
106+ console.error(`FAIL: fid.seq differs from golden (${produced.length} vs ${golden.length} bytes)`)
107+ const a = produced.toString('utf8').split('\n')
108+ const b = golden.toString('utf8').split('\n')
109+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
110+ if (a[i] !== b[i]) {
111+ console.error(` first diff at line ${i + 1}:`)
112+ console.error(` produced: ${a[i]}`)
113+ console.error(` golden: ${b[i]}`)
114+ break
115+ }
116+ }
117+ process.exit(1)
118+}
scripts/gen-examples.mjsadded+64−0View file
@@ -0,0 +1,64 @@
1+// Generate the gallery's pulseq example sources from a demoSeq batch-test run.
2+// Reads scripts/demo-results.json (written by test-demos.mjs), and for every
3+// demo that ran cleanly AND has a CATALOG entry that isn't already curated,
4+// writes an adapted .m into src/examples/pulseq/<id>.m. Also writes
5+// src/examples/generated-timings.ts with a rough run-time label per example.
6+//
7+// node scripts/test-demos.mjs && node scripts/gen-examples.mjs
8+import fs from 'node:fs'
9+import path from 'node:path'
10+import { fileURLToPath } from 'node:url'
11+import { adaptDemo } from './test-demos.mjs'
12+import { CATALOG } from '../src/examples/catalog.ts'
13+
14+const here = path.dirname(fileURLToPath(import.meta.url))
15+const repoRoot = path.join(here, '..')
16+const demoDir = process.env.PULSEQ_DEMO_DIR ?? path.resolve(repoRoot, '../../pulseq/matlab/demoSeq')
17+const outDir = path.join(repoRoot, 'src', 'examples', 'pulseq')
18+
19+// Sequences hand-curated in src/examples/*.m — never auto-generate these.
20+const CURATED = new Set(['fid', 'epi', 'gre'])
21+
22+function demoToId(file) {
23+ return file.replace(/^write_?/, '').replace(/\.m$/, '')
24+}
25+
26+function approxLabel(ms) {
27+ const s = ms / 1000
28+ if (s < 1.5) return '~1 s'
29+ if (s < 3) return '~2 s'
30+ if (s < 8) return `~${Math.round(s)} s`
31+ if (s < 45) return `~${Math.round(s / 5) * 5} s`
32+ return '~1 min+'
33+}
34+
35+const results = JSON.parse(fs.readFileSync(path.join(here, 'demo-results.json'), 'utf8'))
36+
37+fs.rmSync(outDir, { recursive: true, force: true })
38+fs.mkdirSync(outDir, { recursive: true })
39+
40+// Timings for the curated examples (measured separately; gre has autoLabel off)
41+const timings = { fid: '~1 s', epi: '~2 s', gre: '~5 s' }
42+const written = []
43+
44+for (const r of results) {
45+ const ok = r.ok && r.seqFiles?.length > 0 && r.seqFiles.every((s) => s.parseOk)
46+ if (!ok) continue
47+ const id = demoToId(r.demo)
48+ if (CURATED.has(id) || !(id in CATALOG)) continue
49+ const src = adaptDemo(fs.readFileSync(path.join(demoDir, r.demo), 'utf8'))
50+ fs.writeFileSync(path.join(outDir, `${id}.m`), src)
51+ timings[id] = approxLabel(r.elapsedMs)
52+ written.push(id)
53+}
54+
55+const tsBody =
56+ '// AUTO-GENERATED by scripts/gen-examples.mjs — do not edit.\n' +
57+ '// Rough in-browser run-time labels per example id.\n' +
58+ 'export const TIMINGS: Record<string, string> = ' +
59+ JSON.stringify(timings, null, 2) +
60+ '\n'
61+fs.writeFileSync(path.join(repoRoot, 'src', 'examples', 'generated-timings.ts'), tsBody)
62+
63+console.log(`generated ${written.length} example sources:`, written.join(', '))
64+console.log('timings ->', JSON.stringify(timings))
scripts/parser-test.mjsadded+91−0View file
@@ -0,0 +1,91 @@
1+// Checks the JS-side .seq parser + waveform reconstruction against the
2+// MATLAB-golden files in test-data/golden (runs the .ts sources directly —
3+// Node >= 22.6 with type stripping).
4+import fs from 'node:fs'
5+import path from 'node:path'
6+import { fileURLToPath } from 'node:url'
7+import { parseSeq, decompressShape } from '../src/seq/parseSeq.ts'
8+import { reconstruct } from '../src/seq/reconstruct.ts'
9+
10+const here = path.dirname(fileURLToPath(import.meta.url))
11+const goldenDir = path.join(here, '..', 'test-data', 'golden')
12+
13+let failures = 0
14+function check(cond, label) {
15+ if (cond) console.log(` ok: ${label}`)
16+ else {
17+ console.error(` FAIL: ${label}`)
18+ failures++
19+ }
20+}
21+
22+// ── decompressShape unit check ──────────────────────────────────────────
23+// w = [0 0 0 1 2 3 3 3]; deriv = [0 0 0 1 1 1 0 0]; RLE: 0,0,1 1,1,1 0,0,0
24+{
25+ const w = decompressShape(8, [0, 0, 1, 1, 1, 1, 0, 0, 0])
26+ check(
27+ JSON.stringify([...w]) === JSON.stringify([0, 0, 0, 1, 2, 3, 3, 3]),
28+ 'decompressShape RLE round-trip',
29+ )
30+}
31+
32+// ── golden files ────────────────────────────────────────────────────────
33+const expectations = {
34+ 'fid.seq': { blocks: 32, rfPulses: 16, adcWindows: 16, adcSamples: 16 * 4096 },
35+ 'gre.seq': { blocks: 640, rfPulses: 128, adcWindows: 128, adcSamples: 128 * 128 },
36+ 'seq1.seq': {},
37+ 'seq2.seq': {},
38+ 'seq3.seq': {},
39+}
40+
41+for (const [name, expect] of Object.entries(expectations)) {
42+ const text = fs.readFileSync(path.join(goldenDir, name), 'utf8')
43+ console.log(`${name}:`)
44+ const seq = parseSeq(text)
45+ check(seq.version.major === 1 && seq.version.minor === 5, 'version 1.5.x')
46+ check(seq.blocks.length > 0, `has blocks (${seq.blocks.length})`)
47+ if (seq.signature) check(seq.signature.valid === true, 'md5 signature verifies')
48+
49+ // Every referenced event must resolve
50+ let dangling = 0
51+ for (const b of seq.blocks) {
52+ if (b.rfId !== 0 && !seq.rf.has(b.rfId)) dangling++
53+ for (const gid of [b.gxId, b.gyId, b.gzId]) {
54+ if (gid !== 0 && !seq.grads.has(gid)) dangling++
55+ }
56+ if (b.adcId !== 0 && !seq.adcs.has(b.adcId)) dangling++
57+ if (b.extId !== 0 && !seq.extensions.has(b.extId)) dangling++
58+ }
59+ check(dangling === 0, 'all block event references resolve')
60+
61+ const rec = reconstruct(seq)
62+ const totalDef = seq.definitions.get('TotalDuration')
63+ if (Array.isArray(totalDef)) {
64+ check(Math.abs(rec.duration - totalDef[0]) < 1e-9, `duration matches TotalDuration (${rec.duration})`)
65+ }
66+ for (const s of [rec.gx, rec.gy, rec.gz, rec.rfMag]) {
67+ let monotone = true
68+ for (let i = 1; i < s.t.length; i++) {
69+ if (s.t[i] < s.t[i - 1] - 1e-12) monotone = false
70+ }
71+ check(monotone, 'series times monotone')
72+ }
73+ if (expect.blocks !== undefined) check(seq.blocks.length === expect.blocks, `${expect.blocks} blocks`)
74+ if (expect.rfPulses !== undefined) check(rec.rfSpans.length === expect.rfPulses, `${expect.rfPulses} RF pulses`)
75+ if (expect.adcWindows !== undefined) check(rec.adcSpans.length === expect.adcWindows, `${expect.adcWindows} ADC windows`)
76+ if (expect.adcSamples !== undefined) check(rec.stats.adcSamples === expect.adcSamples, `${expect.adcSamples} ADC samples`)
77+}
78+
79+// Tampering must invalidate the signature
80+{
81+ const text = fs.readFileSync(path.join(goldenDir, 'gre.seq'), 'utf8')
82+ const tampered = text.replace('FOV 0.256', 'FOV 0.255')
83+ const seq = parseSeq(tampered)
84+ check(seq.signature?.valid === false, 'tampered file fails signature check')
85+}
86+
87+if (failures > 0) {
88+ console.error(`\n${failures} check(s) FAILED`)
89+ process.exit(1)
90+}
91+console.log('\nPASS: all parser checks passed')
scripts/run-example.mjsadded+77−0View file
@@ -0,0 +1,77 @@
1+// Dev utility: run one of the src/examples scripts headlessly through the
2+// same call shape the browser runner uses, time it, and parse the produced
3+// .seq with the JS parser. Usage: node scripts/run-example.mjs <fid|gre|epi>
4+import fs from 'node:fs'
5+import path from 'node:path'
6+import { fileURLToPath } from 'node:url'
7+import {
8+ executeCode,
9+ VirtualFileSystem,
10+ BrowserFileIOAdapter,
11+ BrowserSystemAdapter,
12+} from 'numbl'
13+import { parseSeq } from '../src/seq/parseSeq.ts'
14+import { reconstruct } from '../src/seq/reconstruct.ts'
15+
16+const name = process.argv[2] ?? 'fid'
17+const here = path.dirname(fileURLToPath(import.meta.url))
18+const repoRoot = path.join(here, '..')
19+const mrRoot = path.join(repoRoot, 'src', 'engine', 'pulseq')
20+
21+const MD5_OVERRIDE = `function digest = md5(message, noBuiltIn)
22+ digest = hash('MD5', char(message));
23+end
24+`
25+
26+const files = []
27+for (const entry of fs.readdirSync(path.join(mrRoot, '+mr'), { recursive: true })) {
28+ const rel = String(entry)
29+ const abs = path.join(mrRoot, '+mr', rel)
30+ if (!fs.statSync(abs).isFile() || !rel.endsWith('.m')) continue
31+ const vfsPath = `+mr/${rel}`
32+ files.push({
33+ path: vfsPath,
34+ content: vfsPath === '+mr/+aux/md5.m' ? MD5_OVERRIDE : fs.readFileSync(abs, 'utf8'),
35+ })
36+}
37+const script = fs.readFileSync(path.join(repoRoot, 'src', 'examples', `${name}.m`), 'utf8')
38+files.push({ path: 'main.m', content: script })
39+
40+const enc = new TextEncoder()
41+const vfs = new VirtualFileSystem()
42+for (const f of files) vfs.writeFile('/project/' + f.path, enc.encode(f.content))
43+vfs.setCwd('/project')
44+const workspaceFiles = files.map((f) => ({ name: f.path, source: f.content }))
45+
46+const t0 = Date.now()
47+try {
48+ executeCode(
49+ 'main;',
50+ {
51+ onOutput: (text) => process.stdout.write(text),
52+ displayResults: true,
53+ maxIterations: 1e9,
54+ optimization: '1',
55+ fileIO: new BrowserFileIOAdapter(vfs),
56+ system: new BrowserSystemAdapter(vfs),
57+ },
58+ workspaceFiles,
59+ 'repl',
60+ [],
61+ )
62+} catch (err) {
63+ console.error('run failed:', err.message ?? err)
64+ if (err.file) console.error(` at ${err.file}:${err.line}`)
65+ process.exit(1)
66+}
67+console.log(`\nrun finished in ${((Date.now() - t0) / 1000).toFixed(1)} s`)
68+
69+const seqName = `${name}.seq`
70+const text = new TextDecoder().decode(vfs.readFile('/project/' + seqName))
71+const seq = parseSeq(text)
72+const rec = reconstruct(seq)
73+console.log(
74+ `${seqName}: ${text.length} bytes, v${seq.version.major}.${seq.version.minor}.${seq.version.revision}, ` +
75+ `${seq.blocks.length} blocks, ${rec.rfSpans.length} RF, ${rec.adcSpans.length} ADC, ` +
76+ `duration ${rec.duration.toFixed(4)} s, signature ${seq.signature?.valid ? 'valid' : 'MISSING/INVALID'}`,
77+)
scripts/test-demos.mjsadded+233−0View file
@@ -0,0 +1,233 @@
1+// Batch-test pulseq demoSeq scripts on numbl. For each demo we auto-adapt it
2+// (truncate after the first `seq.write(...)` — everything after is plotting/
3+// analysis/install), run it through the same call shape the browser runner
4+// uses, and report whether it produced a viewable .seq, how long it took, and
5+// any error. Also parses the output with the JS parser as an extra check.
6+//
7+// node scripts/test-demos.mjs # driver: test all, print table
8+// node scripts/test-demos.mjs --one <path> # run one demo, print JSON
9+//
10+// The driver spawns each demo in its own child process (isolation + timeout).
11+import fs from 'node:fs'
12+import path from 'node:path'
13+import { fileURLToPath } from 'node:url'
14+import { spawn } from 'node:child_process'
15+
16+const here = path.dirname(fileURLToPath(import.meta.url))
17+const repoRoot = path.join(here, '..')
18+const mrRoot = path.join(repoRoot, 'src', 'engine', 'pulseq')
19+const demoDir = process.env.PULSEQ_DEMO_DIR ?? path.resolve(repoRoot, '../../pulseq/matlab/demoSeq')
20+
21+const MD5_OVERRIDE = `function digest = md5(message, noBuiltIn)\n digest = hash('MD5', char(message));\nend\n`
22+// No Python/SigPy in the browser: force the internal (non-sigpy) pulse paths.
23+const ISSIGPY_OVERRIDE = `function [sigPyOK, pythonExe] = isSigPyAvailable()\n sigPyOK = false;\n pythonExe = '';\nend\n`
24+const TIMEOUT_MS = 120_000
25+const CONCURRENCY = 6
26+
27+// Statement-leading patterns that are visualization / analysis / scanner
28+// side effects — irrelevant to generating a .seq and often unsupported
29+// (seq.plot -> gobjects, seq.sound, seq.install, testReport, k-space plots).
30+const STRIP_PATTERNS = [
31+ /^\s*seq\.(plot|paperPlot|plotK|sound|install|testReport)\b/,
32+ /^\s*\[?[\w,\s~]*\]?\s*=?\s*seq\.calculateKspacePP\b/,
33+ /^\s*(figure|hold|axis|grid|subplot|title|xlabel|ylabel|zlabel|legend|colormap|colorbar|drawnow|clf|clim|caxis|view)\b/,
34+ /^\s*(plot|plot3|plot3d|imagesc|imshow|surf|mesh|quiver|scatter|stairs|stem|area|bar|pcolor|contour)\s*\(/,
35+ /^\s*set\s*\(\s*gc[af]/,
36+ /^\s*rep\s*=\s*seq\.testReport\b/,
37+ /^\s*fprintf\s*\(\s*\[rep/,
38+]
39+
40+/**
41+ * Adapt a demoSeq script for headless generation:
42+ * - strip visualization / analysis / scanner-side-effect statements
43+ * - keep the script up to and including the first `seq.write(...)`
44+ * - re-append trailing local-function definitions (which the truncation
45+ * would otherwise drop) so calls to them still resolve
46+ */
47+export function adaptDemo(src) {
48+ const lines = src.split('\n')
49+ const writeIdx = lines.findIndex((l) => /\bseq\.write\s*\(/.test(l))
50+ const funcIdx = lines.findIndex((l) => /^\s*function\b/.test(l))
51+ const strip = (arr) => arr.filter((l) => !STRIP_PATTERNS.some((re) => re.test(l)))
52+
53+ if (writeIdx < 0) return strip(lines).join('\n') + '\n'
54+ let body = strip(lines.slice(0, writeIdx + 1))
55+ // Trailing local functions live after the write; keep them verbatim.
56+ if (funcIdx > writeIdx) {
57+ body = body.concat('', lines.slice(funcIdx))
58+ }
59+ return body.join('\n') + '\n'
60+}
61+
62+// ── child mode: run one demo ────────────────────────────────────────────
63+if (process.argv[2] === '--one') {
64+ const demoPath = process.argv[3]
65+ const { executeCode, VirtualFileSystem, BrowserFileIOAdapter, BrowserSystemAdapter } =
66+ await import('numbl')
67+ const { parseSeq } = await import('../src/seq/parseSeq.ts')
68+ const { reconstruct } = await import('../src/seq/reconstruct.ts')
69+
70+ const files = []
71+ for (const entry of fs.readdirSync(path.join(mrRoot, '+mr'), { recursive: true })) {
72+ const rel = String(entry)
73+ const abs = path.join(mrRoot, '+mr', rel)
74+ if (!fs.statSync(abs).isFile() || !rel.endsWith('.m')) continue
75+ const vfsPath = `+mr/${rel}`
76+ let content = fs.readFileSync(abs, 'utf8')
77+ if (vfsPath === '+mr/+aux/md5.m') content = MD5_OVERRIDE
78+ else if (vfsPath === '+mr/+aux/isSigPyAvailable.m') content = ISSIGPY_OVERRIDE
79+ files.push({ path: vfsPath, content })
80+ }
81+ const script = adaptDemo(fs.readFileSync(demoPath, 'utf8'))
82+ files.push({ path: 'main.m', content: script })
83+
84+ const enc = new TextEncoder()
85+ const vfs = new VirtualFileSystem()
86+ for (const f of files) vfs.writeFile('/project/' + f.path, enc.encode(f.content))
87+ vfs.setCwd('/project')
88+ const workspaceFiles = files.map((f) => ({ name: f.path, source: f.content }))
89+
90+ const result = { ok: false, error: null, seqFiles: [], elapsedMs: 0 }
91+ const t0 = Date.now()
92+ try {
93+ executeCode(
94+ 'main;',
95+ {
96+ onOutput: () => {},
97+ displayResults: false,
98+ maxIterations: 1e9,
99+ optimization: '1',
100+ fileIO: new BrowserFileIOAdapter(vfs),
101+ system: new BrowserSystemAdapter(vfs),
102+ },
103+ workspaceFiles,
104+ 'repl',
105+ [],
106+ )
107+ result.ok = true
108+ } catch (err) {
109+ result.error = String(err?.message ?? err)
110+ if (err?.file) result.error += ` @ ${err.file}:${err.line}`
111+ }
112+ result.elapsedMs = Date.now() - t0
113+
114+ // Collect + parse any .seq files
115+ const collect = executeCode(
116+ `d = dir('*.seq'); for i=1:numel(d); fprintf('F:%s\\n', d(i).name); end`,
117+ {
118+ onOutput: (t) => result.seqFiles.push(...[...t.matchAll(/^F:(.+)$/gm)].map((m) => m[1])),
119+ displayResults: false,
120+ maxIterations: 1e9,
121+ optimization: '1',
122+ fileIO: new BrowserFileIOAdapter(vfs),
123+ system: new BrowserSystemAdapter(vfs),
124+ },
125+ workspaceFiles,
126+ 'repl',
127+ [],
128+ )
129+ void collect
130+ const seqInfo = []
131+ for (const name of result.seqFiles) {
132+ try {
133+ const text = new TextDecoder().decode(vfs.readFile('/project/' + name))
134+ const seq = parseSeq(text)
135+ const rec = reconstruct(seq)
136+ seqInfo.push({
137+ name,
138+ bytes: text.length,
139+ blocks: seq.blocks.length,
140+ rf: rec.stats.rfCount,
141+ adc: rec.stats.adcCount,
142+ dur: rec.duration,
143+ sig: seq.signature?.valid ?? null,
144+ parseOk: true,
145+ })
146+ } catch (err) {
147+ seqInfo.push({ name, parseOk: false, parseErr: String(err?.message ?? err) })
148+ }
149+ }
150+ result.seqFiles = seqInfo
151+ process.stdout.write('@@SEQLAB_RESULT@@' + JSON.stringify(result) + '@@END@@')
152+ process.exit(0)
153+}
154+
155+// ── driver mode: test all demos ─────────────────────────────────────────
156+// Only runs when this file is executed directly; importing it (e.g. from
157+// gen-examples.mjs for adaptDemo) must NOT kick off the batch.
158+const isMain = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])
159+
160+function runOne(demo) {
161+ return new Promise((resolve) => {
162+ const child = spawn('node', [fileURLToPath(import.meta.url), '--one', path.join(demoDir, demo)], {
163+ cwd: repoRoot,
164+ })
165+ let out = ''
166+ const timer = setTimeout(() => child.kill('SIGKILL'), TIMEOUT_MS)
167+ child.stdout.on('data', (d) => (out += d))
168+ child.stderr.on('data', () => {})
169+ child.on('close', () => {
170+ clearTimeout(timer)
171+ const m = out.match(/@@SEQLAB_RESULT@@([\s\S]*?)@@END@@/)
172+ if (m) {
173+ try {
174+ resolve({ demo, ...JSON.parse(m[1]) })
175+ return
176+ } catch {
177+ /* fall through */
178+ }
179+ }
180+ resolve({ demo, ok: false, error: 'timeout or crash', seqFiles: [], elapsedMs: TIMEOUT_MS, timedOut: true })
181+ })
182+ })
183+}
184+
185+async function main() {
186+ const demos = fs
187+ .readdirSync(demoDir)
188+ .filter((f) => f.endsWith('.m'))
189+ .sort()
190+
191+ const results = []
192+ let idx = 0
193+ async function worker() {
194+ while (idx < demos.length) {
195+ const demo = demos[idx++]
196+ process.stderr.write(` running ${demo}…\n`)
197+ results.push(await runOne(demo))
198+ }
199+ }
200+ await Promise.all(Array.from({ length: CONCURRENCY }, worker))
201+ results.sort((a, b) => a.demo.localeCompare(b.demo))
202+
203+ let good = 0
204+ let bad = 0
205+ console.log('\n=== demoSeq on numbl ===\n')
206+ for (const r of results) {
207+ const seq = r.seqFiles?.[0]
208+ const wrote = r.seqFiles?.length > 0
209+ const parseOk = wrote && r.seqFiles.every((s) => s.parseOk)
210+ const status = r.ok && wrote && parseOk ? 'OK ' : 'ERR'
211+ if (status === 'OK ') good++
212+ else bad++
213+ const secs = (r.elapsedMs / 1000).toFixed(1).padStart(6)
214+ let detail = ''
215+ if (r.ok && seq && seq.parseOk) {
216+ detail = `${String(seq.blocks).padStart(5)} blk ${String(seq.rf).padStart(4)} rf ${String(seq.adc).padStart(5)} adc dur ${seq.dur.toFixed(3)}s sig ${seq.sig}`
217+ } else if (r.timedOut) {
218+ detail = `TIMEOUT (>${TIMEOUT_MS / 1000}s)`
219+ } else if (!wrote && r.ok) {
220+ detail = 'ran but wrote no .seq'
221+ } else if (!parseOk && wrote) {
222+ detail = `parse failed: ${r.seqFiles.find((s) => !s.parseOk)?.parseErr}`
223+ } else {
224+ detail = (r.error ?? 'unknown').slice(0, 90)
225+ }
226+ console.log(`${status} ${secs}s ${r.demo.replace(/\.m$/, '').padEnd(30)} ${detail}`)
227+ }
228+ console.log(`\n${good} OK, ${bad} failed, of ${results.length}`)
229+ fs.writeFileSync(path.join(here, 'demo-results.json'), JSON.stringify(results, null, 2))
230+ console.log('full results -> scripts/demo-results.json')
231+}
232+
233+if (isMain) await main()
scripts/vendor-pulseq.mjsadded+56−0View file
@@ -0,0 +1,56 @@
1+// Vendor the pulseq +mr MATLAB package into src/engine/pulseq/+mr so the app
2+// can stage it into the numbl VFS at runtime. Run against a local pulseq
3+// checkout: node scripts/vendor-pulseq.mjs [path-to-pulseq] (default ../../pulseq).
4+// Copies only .m files, plus pulseq's LICENSE, and records the source commit.
5+import fs from 'node:fs'
6+import path from 'node:path'
7+import { execSync } from 'node:child_process'
8+import { fileURLToPath } from 'node:url'
9+
10+const here = path.dirname(fileURLToPath(import.meta.url))
11+const repoRoot = path.join(here, '..')
12+const pulseqRoot = path.resolve(repoRoot, process.argv[2] ?? '../../pulseq')
13+const srcMr = path.join(pulseqRoot, 'matlab', '+mr')
14+const destRoot = path.join(repoRoot, 'src', 'engine', 'pulseq')
15+const destMr = path.join(destRoot, '+mr')
16+
17+if (!fs.existsSync(srcMr)) {
18+ console.error(`pulseq +mr tree not found at ${srcMr}`)
19+ process.exit(1)
20+}
21+
22+fs.rmSync(destMr, { recursive: true, force: true })
23+let count = 0
24+for (const entry of fs.readdirSync(srcMr, { recursive: true })) {
25+ const rel = String(entry)
26+ const from = path.join(srcMr, rel)
27+ if (!fs.statSync(from).isFile() || !rel.endsWith('.m')) continue
28+ const to = path.join(destMr, rel)
29+ fs.mkdirSync(path.dirname(to), { recursive: true })
30+ fs.copyFileSync(from, to)
31+ count++
32+}
33+
34+fs.copyFileSync(path.join(pulseqRoot, 'LICENSE'), path.join(destRoot, 'LICENSE'))
35+
36+let commit = 'unknown'
37+try {
38+ commit = execSync('git describe --tags --always', { cwd: pulseqRoot }).toString().trim()
39+} catch {
40+ /* not a git checkout */
41+}
42+fs.writeFileSync(
43+ path.join(destRoot, 'VENDORED.md'),
44+ `# Vendored pulseq +mr package
45+
46+Copied from https://github.com/pulseq/pulseq (\`matlab/+mr\`, .m files only)
47+at version \`${commit}\` by \`scripts/vendor-pulseq.mjs\`. Do not edit these
48+files here — update the pulseq checkout and re-run \`npm run vendor-pulseq\`.
49+
50+pulseq is MIT-licensed; see LICENSE in this directory.
51+
52+Note: at runtime the engine replaces \`+mr/+aux/md5.m\` (which needs Java on
53+MATLAB) with a wrapper over numbl's \`hash\` builtin — see src/engine/mrFiles.ts.
54+`,
55+)
56+console.log(`vendored ${count} .m files from ${srcMr} (${commit})`)
src/App.tsxadded+396−0View file
@@ -0,0 +1,396 @@
1+import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2+import ScriptEditor from './editor/ScriptEditor.tsx'
3+import Timeline from './viewer/Timeline.tsx'
4+import ReportPanel from './viewer/ReportPanel.tsx'
5+import BlockInspector from './viewer/BlockInspector.tsx'
6+import { runScript, type RunHandle } from './engine/runner.ts'
7+import { parseSeq, SeqParseError } from './seq/parseSeq.ts'
8+import { reconstruct, type Reconstructed } from './seq/reconstruct.ts'
9+import type { ParsedSeq } from './seq/types.ts'
10+import { EXAMPLES, findExample } from './examples/index.ts'
11+import ExamplesModal from './examples/ExamplesModal.tsx'
12+
13+interface ConsoleLine {
14+ kind: 'out' | 'err' | 'ok'
15+ text: string
16+}
17+
18+interface LoadedSeq {
19+ name: string
20+ text: string
21+ seq: ParsedSeq
22+ rec: Reconstructed
23+ source: 'run' | 'upload'
24+}
25+
26+type RunStatus = 'idle' | 'booting' | 'running'
27+
28+function loadSeqFile(name: string, text: string, source: 'run' | 'upload'): LoadedSeq {
29+ const seq = parseSeq(text)
30+ return { name, text, seq, rec: reconstruct(seq), source }
31+}
32+
33+function download(name: string, text: string) {
34+ const url = URL.createObjectURL(new Blob([text], { type: 'text/plain' }))
35+ const a = document.createElement('a')
36+ a.href = url
37+ a.download = name
38+ a.click()
39+ URL.revokeObjectURL(url)
40+}
41+
42+export default function App() {
43+ const [script, setScript] = useState(() => {
44+ const hashExample = readExampleFromHash()
45+ return (hashExample ?? EXAMPLES[0]).source
46+ })
47+ const [scriptName, setScriptName] = useState(() => {
48+ const hashExample = readExampleFromHash()
49+ return `${(hashExample ?? EXAMPLES[0]).id}.m`
50+ })
51+ const [consoleLines, setConsoleLines] = useState<ConsoleLine[]>([])
52+ const [runStatus, setRunStatus] = useState<RunStatus>('idle')
53+ const [loaded, setLoaded] = useState<LoadedSeq[]>([])
54+ const [activeSeq, setActiveSeq] = useState(0)
55+ const [selectedBlock, setSelectedBlock] = useState<number | null>(null)
56+ const [examplesOpen, setExamplesOpen] = useState(false)
57+ const [dropActive, setDropActive] = useState(false)
58+ const [leftWidth, setLeftWidth] = useState(46) // percent
59+ const runRef = useRef<RunHandle | null>(null)
60+ const consoleBodyRef = useRef<HTMLDivElement>(null)
61+ const fileInputRef = useRef<HTMLInputElement>(null)
62+
63+ const appendConsole = useCallback((kind: ConsoleLine['kind'], text: string) => {
64+ setConsoleLines((lines) => [...lines, { kind, text }])
65+ }, [])
66+
67+ useEffect(() => {
68+ const el = consoleBodyRef.current
69+ if (el) el.scrollTop = el.scrollHeight
70+ }, [consoleLines])
71+
72+ // doRun reads the current script through a ref so its identity is stable
73+ // (the CodeMirror keymap captures it once).
74+ const scriptRef = useRef(script)
75+ scriptRef.current = script
76+
77+ const doRun = useCallback((src?: unknown) => {
78+ if (runRef.current) return
79+ // May be called from the Run button (MouseEvent), the editor keymap (no
80+ // arg), or loadExample (explicit source string). Only a string overrides
81+ // the ref, avoiding a race with the setScript state update.
82+ const source = typeof src === 'string' ? src : scriptRef.current
83+ setConsoleLines([])
84+ setRunStatus('booting')
85+ const handle = runScript(source, {
86+ onOutput: (text) => {
87+ setRunStatus('running')
88+ appendConsole('out', text)
89+ },
90+ })
91+ runRef.current = handle
92+ handle.promise
93+ .then((result) => {
94+ if (result.aborted) {
95+ appendConsole('err', '— run cancelled —\n')
96+ return
97+ }
98+ if (!result.ok) {
99+ appendConsole('err', (result.error ?? 'unknown error') + '\n')
100+ }
101+ if (result.seqFiles.length > 0) {
102+ const files: LoadedSeq[] = []
103+ for (const f of result.seqFiles) {
104+ try {
105+ files.push(loadSeqFile(f.name, f.text, 'run'))
106+ } catch (err) {
107+ appendConsole(
108+ 'err',
109+ `failed to parse ${f.name}: ${err instanceof SeqParseError ? err.message : String(err)}\n`,
110+ )
111+ }
112+ }
113+ setLoaded(files)
114+ setActiveSeq(0)
115+ setSelectedBlock(null)
116+ } else if (result.ok) {
117+ appendConsole('err', 'The script finished without writing any .seq file — call seq.write(...).\n')
118+ }
119+ if (result.ok) {
120+ appendConsole('ok', `— finished in ${(result.elapsedMs / 1000).toFixed(1)} s —\n`)
121+ }
122+ })
123+ .catch((err) => {
124+ appendConsole('err', `engine error: ${err instanceof Error ? err.message : String(err)}\n`)
125+ })
126+ .finally(() => {
127+ runRef.current = null
128+ setRunStatus('idle')
129+ })
130+ }, [appendConsole])
131+
132+ const doCancel = useCallback(() => {
133+ runRef.current?.cancel()
134+ }, [])
135+
136+ const loadExample = useCallback((id: string) => {
137+ const ex = findExample(id)
138+ if (!ex) return
139+ // Load into the editor only; the user presses Run to execute it.
140+ setScript(ex.source)
141+ setScriptName(`${ex.id}.m`)
142+ window.location.hash = `example/${ex.id}`
143+ setExamplesOpen(false)
144+ }, [])
145+
146+ const openSeqFile = useCallback(
147+ (file: File) => {
148+ file.text().then(
149+ (text) => {
150+ try {
151+ const f = loadSeqFile(file.name, text, 'upload')
152+ setLoaded([f])
153+ setActiveSeq(0)
154+ setSelectedBlock(null)
155+ } catch (err) {
156+ appendConsole(
157+ 'err',
158+ `failed to parse ${file.name}: ${err instanceof Error ? err.message : String(err)}\n`,
159+ )
160+ }
161+ },
162+ () => appendConsole('err', `could not read ${file.name}\n`),
163+ )
164+ },
165+ [appendConsole],
166+ )
167+
168+ // Drag & drop anywhere
169+ useEffect(() => {
170+ let depth = 0
171+ const onDragEnter = (e: DragEvent) => {
172+ if (!e.dataTransfer?.types.includes('Files')) return
173+ depth++
174+ setDropActive(true)
175+ }
176+ const onDragLeave = () => {
177+ depth = Math.max(0, depth - 1)
178+ if (depth === 0) setDropActive(false)
179+ }
180+ const onDragOver = (e: DragEvent) => {
181+ if (e.dataTransfer?.types.includes('Files')) e.preventDefault()
182+ }
183+ const onDrop = (e: DragEvent) => {
184+ depth = 0
185+ setDropActive(false)
186+ const file = e.dataTransfer?.files?.[0]
187+ if (file) {
188+ e.preventDefault()
189+ openSeqFile(file)
190+ }
191+ }
192+ window.addEventListener('dragenter', onDragEnter)
193+ window.addEventListener('dragleave', onDragLeave)
194+ window.addEventListener('dragover', onDragOver)
195+ window.addEventListener('drop', onDrop)
196+ return () => {
197+ window.removeEventListener('dragenter', onDragEnter)
198+ window.removeEventListener('dragleave', onDragLeave)
199+ window.removeEventListener('dragover', onDragOver)
200+ window.removeEventListener('drop', onDrop)
201+ }
202+ }, [openSeqFile])
203+
204+ // Divider drag
205+ const onDividerDown = useCallback((e: React.MouseEvent) => {
206+ e.preventDefault()
207+ const onMove = (ev: MouseEvent) => {
208+ const pct = (ev.clientX / window.innerWidth) * 100
209+ setLeftWidth(Math.min(70, Math.max(25, pct)))
210+ }
211+ const onUp = () => {
212+ window.removeEventListener('mousemove', onMove)
213+ window.removeEventListener('mouseup', onUp)
214+ }
215+ window.addEventListener('mousemove', onMove)
216+ window.addEventListener('mouseup', onUp)
217+ }, [])
218+
219+ const active = loaded[activeSeq]
220+ const running = runStatus !== 'idle'
221+ const consoleEmpty = consoleLines.length === 0
222+
223+ const statusLabel = useMemo(() => {
224+ if (runStatus === 'booting') return 'starting engine…'
225+ if (runStatus === 'running') return 'running…'
226+ return null
227+ }, [runStatus])
228+
229+ return (
230+ <div className={`app${dropActive ? ' drop-active' : ''}`}>
231+ <header className="header">
232+ <div className="logo">
233+ <b>seqlab</b>
234+ <span className="tagline">
235+ powered by{' '}
236+ <a href="https://pulseq.github.io/" target="_blank" rel="noopener noreferrer">
237+ Pulseq
238+ </a>{' '}
239+ +{' '}
240+ <a href="https://numbl.org" target="_blank" rel="noopener noreferrer">
241+ numbl
242+ </a>
243+ </span>
244+ </div>
245+ <div className="spacer" />
246+ <button onClick={() => fileInputRef.current?.click()}>Open .seq…</button>
247+ <input
248+ ref={fileInputRef}
249+ type="file"
250+ accept=".seq,text/plain"
251+ style={{ display: 'none' }}
252+ onChange={(e) => {
253+ const file = e.target.files?.[0]
254+ if (file) openSeqFile(file)
255+ e.target.value = ''
256+ }}
257+ />
258+ </header>
259+
260+ <div className="main">
261+ <div className="left" style={{ width: `${leftWidth}%` }}>
262+ <div className="editor-toolbar">
263+ {running ? (
264+ <button className="danger" onClick={doCancel}>
265+ ■ Cancel
266+ </button>
267+ ) : (
268+ <button className="primary" onClick={doRun} title="Ctrl+Enter">
269+ ▶ Run
270+ </button>
271+ )}
272+ <button className="examples-btn" onClick={() => setExamplesOpen(true)}>
273+ ☰ Examples
274+ </button>
275+ <span className="name">{scriptName}</span>
276+ <div className="spacer" style={{ flex: 1 }} />
277+ <button onClick={() => download(scriptName, scriptRef.current)}>Download .m</button>
278+ </div>
279+ <ScriptEditor value={script} onChange={setScript} onRun={doRun} />
280+ <div className="console">
281+ <div className="console-head">
282+ Console
283+ {statusLabel && <span>· {statusLabel}</span>}
284+ </div>
285+ <div className="console-body" ref={consoleBodyRef}>
286+ {consoleEmpty && !running ? (
287+ <span className="muted" style={{ color: 'var(--text-dim)' }}>
288+ Press Run (Ctrl+Enter) to execute the script with pulseq on the numbl MATLAB
289+ runtime — entirely in this tab.
290+ </span>
291+ ) : (
292+ consoleLines.map((l, i) => (
293+ <span key={i} className={l.kind === 'out' ? undefined : l.kind}>
294+ {l.text}
295+ </span>
296+ ))
297+ )}
298+ </div>
299+ </div>
300+ </div>
301+
302+ <div className="divider" onMouseDown={onDividerDown} />
303+
304+ <div className="right">
305+ {!active ? (
306+ <div className="viewer-empty">
307+ <h2>No sequence loaded</h2>
308+ <p className="hint">
309+ Run the script on the left to generate a <code>.seq</code> file, pick an example
310+ from the gallery, or drop / open an existing <code>.seq</code> file to explore it.
311+ </p>
312+ </div>
313+ ) : (
314+ <>
315+ {loaded.length > 1 && (
316+ <div className="seq-tabs">
317+ {loaded.map((f, i) => (
318+ <button
319+ key={f.name + i}
320+ className={i === activeSeq ? 'active' : undefined}
321+ onClick={() => {
322+ setActiveSeq(i)
323+ setSelectedBlock(null)
324+ }}
325+ >
326+ {f.name}
327+ </button>
328+ ))}
329+ </div>
330+ )}
331+ <div className="viewer-section">
332+ <h3>
333+ {active.name}
334+ <span className="free" />
335+ <span className="fine">
336+ {active.source === 'run' ? 'generated by script' : 'opened file'}
337+ </span>
338+ <button onClick={() => download(active.name, active.text)}>Download .seq</button>
339+ </h3>
340+ <ReportPanel seq={active.seq} rec={active.rec} />
341+ </div>
342+ <div className="viewer-section">
343+ <h3>
344+ Timeline
345+ <span className="free" />
346+ <span className="fine">scroll to zoom · drag to pan · click a block · double-click resets</span>
347+ </h3>
348+ <Timeline
349+ rec={active.rec}
350+ selectedBlock={selectedBlock}
351+ onSelectBlock={setSelectedBlock}
352+ />
353+ </div>
354+ <div className="viewer-section">
355+ <h3>
356+ Block inspector
357+ <span className="free" />
358+ {selectedBlock !== null && (
359+ <button onClick={() => setSelectedBlock(null)}>Close</button>
360+ )}
361+ </h3>
362+ {selectedBlock !== null ? (
363+ <BlockInspector
364+ seq={active.seq}
365+ rec={active.rec}
366+ blockIndex={selectedBlock}
367+ onNavigate={(i) =>
368+ setSelectedBlock(Math.min(Math.max(i, 0), active.seq.blocks.length - 1))
369+ }
370+ />
371+ ) : (
372+ <p className="inspector-hint">
373+ Click a block in the timeline above to inspect its events.
374+ </p>
375+ )}
376+ </div>
377+ </>
378+ )}
379+ </div>
380+ </div>
381+
382+ {examplesOpen && (
383+ <ExamplesModal
384+ examples={EXAMPLES}
385+ onPick={loadExample}
386+ onClose={() => setExamplesOpen(false)}
387+ />
388+ )}
389+ </div>
390+ )
391+}
392+
393+function readExampleFromHash() {
394+ const m = window.location.hash.match(/^#example\/([\w-]+)/)
395+ return m ? findExample(m[1]) : undefined
396+}
src/editor/ScriptEditor.tsxadded+74−0View file
@@ -0,0 +1,74 @@
1+import { useEffect, useRef } from 'react'
2+import { EditorView, basicSetup } from 'codemirror'
3+import { EditorState, Compartment } from '@codemirror/state'
4+import { keymap } from '@codemirror/view'
5+import { indentUnit, StreamLanguage } from '@codemirror/language'
6+import { octave } from '@codemirror/legacy-modes/mode/octave'
7+import { oneDark } from '@codemirror/theme-one-dark'
8+
9+export interface ScriptEditorProps {
10+ value: string
11+ onChange: (value: string) => void
12+ /** Ctrl/Cmd+Enter */
13+ onRun: () => void
14+}
15+
16+const readOnlyCompartment = new Compartment()
17+
18+export default function ScriptEditor({ value, onChange, onRun }: ScriptEditorProps) {
19+ const hostRef = useRef<HTMLDivElement>(null)
20+ const viewRef = useRef<EditorView | null>(null)
21+ // Keep the latest callbacks without recreating the editor
22+ const onChangeRef = useRef(onChange)
23+ onChangeRef.current = onChange
24+ const onRunRef = useRef(onRun)
25+ onRunRef.current = onRun
26+
27+ useEffect(() => {
28+ if (!hostRef.current) return
29+ const view = new EditorView({
30+ parent: hostRef.current,
31+ state: EditorState.create({
32+ doc: value,
33+ extensions: [
34+ basicSetup,
35+ keymap.of([
36+ {
37+ key: 'Mod-Enter',
38+ run: () => {
39+ onRunRef.current()
40+ return true
41+ },
42+ },
43+ ]),
44+ StreamLanguage.define(octave),
45+ oneDark,
46+ indentUnit.of(' '),
47+ EditorView.updateListener.of((update) => {
48+ if (update.docChanged) onChangeRef.current(update.state.doc.toString())
49+ }),
50+ readOnlyCompartment.of([]),
51+ ],
52+ }),
53+ })
54+ viewRef.current = view
55+ return () => {
56+ view.destroy()
57+ viewRef.current = null
58+ }
59+ // The editor is created once; external value changes are synced below.
60+ // eslint-disable-next-line react-hooks/exhaustive-deps
61+ }, [])
62+
63+ // Sync external value changes (example loaded, etc.) into the editor
64+ useEffect(() => {
65+ const view = viewRef.current
66+ if (!view) return
67+ const current = view.state.doc.toString()
68+ if (current !== value) {
69+ view.dispatch({ changes: { from: 0, to: current.length, insert: value } })
70+ }
71+ }, [value])
72+
73+ return <div className="editor-host" ref={hostRef} />
74+}
src/engine/mrFiles.tsadded+42−0View file
@@ -0,0 +1,42 @@
1+// The vendored pulseq +mr package as numbl boot files. The tree is inlined
2+// into the bundle at build time (raw imports); see src/engine/pulseq/VENDORED.md
3+// for provenance.
4+import type { BootFile } from 'numbl/browser'
5+
6+const raw = import.meta.glob('./pulseq/+mr/**/*.m', {
7+ query: '?raw',
8+ import: 'default',
9+ eager: true,
10+}) as Record<string, string>
11+
12+// pulseq's md5 needs Java (MATLAB) or the `hash` builtin (Octave). numbl
13+// provides `hash`, but md5.m only reaches it through its isOctave branch —
14+// replace the whole file with a direct wrapper.
15+const MD5_OVERRIDE = `function digest = md5(message, noBuiltIn)
16+ digest = hash('MD5', char(message));
17+end
18+`
19+
20+// There is no Python/SigPy (and no OS `system` command) in the browser.
21+// isSigPyAvailable probes for it via system(); stub it to "not available" so
22+// pulse designers (adiabatic, SLR) take their internal non-sigpy path.
23+const ISSIGPY_OVERRIDE = `function [sigPyOK, pythonExe] = isSigPyAvailable()
24+ sigPyOK = false;
25+ pythonExe = '';
26+end
27+`
28+
29+const OVERRIDES: Record<string, string> = {
30+ '+mr/+aux/md5.m': MD5_OVERRIDE,
31+ '+mr/+aux/isSigPyAvailable.m': ISSIGPY_OVERRIDE,
32+}
33+
34+/** Boot-file list for a session: the +mr tree with app-level overrides applied. */
35+export function mrBootFiles(): BootFile[] {
36+ const files: BootFile[] = []
37+ for (const [key, content] of Object.entries(raw)) {
38+ const path = key.replace('./pulseq/', '')
39+ files.push({ path, content: OVERRIDES[path] ?? content })
40+ }
41+ return files
42+}
src/engine/pulseq/+mr/+Siemens/readasc.madded+109−0View file
@@ -0,0 +1,109 @@
1+function [asc, extra] = readasc(filePathBasic);
2+% reads Siemens ASC ascii-formatted textfiles
3+% return is a matlab structure with fields from the file(s).
4+% usage:
5+% myAsc= readasc(path,fileName);
6+% [prot, yaps] = readasc(path,fileName);
7+
8+% Ralph Strecker 16/02/2005
9+% Maxim Zaitsev 08/10/2019
10+
11+% starting from NumarisX va60a some files are split in two, like ####.asc
12+% and ####_GSWD_SAFETY.asc, therefore we need to attempt to read both parts
13+% and merge the structs
14+[FILEPATH,NAME,EXT]=fileparts(filePathBasic);
15+if exist([FILEPATH filesep NAME '_GSWD_SAFETY' EXT],'file')
16+ filePaths={filePathBasic,[FILEPATH filesep NAME '_GSWD_SAFETY' EXT]};
17+else
18+ filePaths={filePathBasic};
19+end
20+
21+for fn=1:length(filePaths)
22+ %%% read one asc file and convert it into a structure
23+ fid= fopen(filePaths{fn});
24+ if fid<0
25+ error('Failed to open file ''%s'', exiting',filePaths{fn});
26+ end
27+ endOfAsc=0;
28+
29+ %nextLine=fgetl(fid); %read next line
30+ %while nextLine~=-1
31+ while ~feof(fid)
32+ openbrack= [];
33+ closebrack= [];
34+ nextLine=strtrim(fgetl(fid));
35+ if strcmp(nextLine,'### ASCCONV END ###') % find end of mrProt in the asc file
36+ endOfAsc=1;
37+ end
38+ if isempty(nextLine) || nextLine(1)=='#'
39+ continue;
40+ end
41+ indEqualSign= findstr(nextLine,'=');
42+ if ~isempty(indEqualSign)
43+ fieldName=deblank(nextLine(1:indEqualSign-1));
44+ openbrack= findstr(fieldName,'[');
45+ closebrack= findstr(fieldName,']');
46+ if ~isempty(openbrack) & ~isempty(closebrack)
47+ fieldName(openbrack)='(';
48+ fieldName(closebrack)=')';
49+ % if strcmp(fieldName(end),fieldName(closebrack))
50+ % fieldName(closebrack)='}';
51+ % fieldName(openbrack)='{';
52+ % end
53+ for k=1:length(openbrack)
54+ counter= str2num(fieldName(openbrack(k)+1:closebrack(k)-1));
55+ fieldName= [fieldName(1:openbrack(k)),num2str(counter+1),fieldName(closebrack(k):end)];
56+ end
57+ end
58+ openclosebrack= findstr(fieldName,')(');
59+ fieldName(openclosebrack)=[];
60+ fieldName(openclosebrack)=',';
61+ %if findstr(fieldName,'atImagedNucleus')
62+ % fieldName= [fieldName,'.value'];
63+ %end
64+
65+ fieldValue= deblank(nextLine(indEqualSign+2:end));
66+ com=[strfind(fieldValue,'#') strfind(fieldValue,'//')];
67+ if ~isempty(com)
68+ com=min(com);
69+ fieldValue=fieldValue(1:(com-1));
70+ end
71+
72+ % if findstr(fieldValue,'i0') || findstr(fieldValue,'i1')
73+ % ind= findstr(fieldValue,'i');
74+ % fieldValue=[fieldValue(1:ind),'*',fieldValue(ind+1:end)];
75+ % end
76+ if ischar(fieldValue)
77+ ind=findstr(fieldValue,'"');
78+ %fieldValue(ind)=[];
79+ fieldValue(ind)='';
80+ end
81+
82+ if length(fieldValue)>1 & strcmp(fieldValue(1:2),'0x') & isempty(findstr(fieldName,'atImagedNucleus'))
83+ fieldValue= hex2dec(fieldValue(3:end)); %fieldValue is hexadecimal
84+ elseif ~isempty(str2num(fieldValue))
85+ fieldValue= str2num(fieldValue);
86+ end
87+
88+ if ischar(fieldValue) && fieldName(end)==')'
89+ fieldName(end)='}';
90+ ib=max(strfind(fieldName,'('));
91+ fieldName(ib)='{';
92+ end
93+
94+ % fprintf('< %s > ',nextLine);
95+ % fprintf('fieldName= %s fieldValue= %s\n', fieldName, mat2str(fieldValue));
96+
97+ if endOfAsc==0
98+ eval(['asc.' fieldName '=','fieldValue;']);
99+ else
100+ eval(['extra.' fieldName '=','fieldValue;']);
101+ end
102+ end
103+
104+ end
105+
106+ fclose(fid);
107+end
108+
109+end
src/engine/pulseq/+mr/+aux/+quat/conjugate.madded+10−0View file
@@ -0,0 +1,10 @@
1+function q = conjugate(q)
2+%CONJUGATE Calculate the conjugate of a quaternion
3+% A single quaternion is represented as a 1 x 4 vector with the first
4+% component being the real part and the 2nd to 4th components
5+% corresponding to the complex vector part.Collections of N quaternions
6+% can be stored as N x 4 matrices.
7+
8+q(:,2:4) = -q(:,2:4);
9+end
10+
src/engine/pulseq/+mr/+aux/+quat/fromRotMat.madded+34−0View file
@@ -0,0 +1,34 @@
1+function q = fromRotMat(R)
2+%ROTATE convert normalized quaternion q to rotation matrix
3+
4+if size(R)~=[3 3]
5+ error('Only a single rotation matrix can be processed');
6+end
7+
8+% fix 'almost zero' or 'almost 1' elements of the rotation matrix
9+roundedR = round( R );
10+boolRound = abs( roundedR - R ) <= eps; % defines which values should be fixed
11+R(boolRound) = roundedR(boolRound);
12+
13+if all(R == 0)
14+ error('Empty (or almost empty) matrix provided in place of a rotation matrix');
15+end
16+
17+qs = 0.5 * sqrt( max( 0, R(1,1) + R(2,2) + R(3,3) + 1 ));
18+
19+if abs(qs) <= eps
20+ sgn_R23 = 1; sgn_R23(-R(2,3) < 0) = -1;
21+ sgn_R13 = 1; sgn_R13(-R(1,3) < 0) = -1;
22+ sgn_R12 = 1; sgn_R12(-R(1,2) < 0) = -1;
23+
24+ q = [0 sqrt( max( 0, -0.5 *( R(2,2) + R(3,3) ))) * sgn_R23 ...
25+ sqrt( max( 0, -0.5 *( R(1,1) + R(3,3) ))) * sgn_R13 ...
26+ sqrt( max( 0, -0.5 *( R(1,1) + R(2,2) ))) * sgn_R12 ];
27+else
28+ q = [qs 0.25 *( R(3,2) - R(2,3) ) / qs ...
29+ 0.25 *( R(1,3) - R(3,1) ) / qs ...
30+ 0.25 *( R(2,1) - R(1,2) ) / qs ];
31+end
32+
33+% normalize the quaternion to account for rounding errors in the rotation matrix, etc
34+q = mr.aux.quat.normalize(q);
src/engine/pulseq/+mr/+aux/+quat/multiply.madded+24−0View file
@@ -0,0 +1,24 @@
1+function qout = multiply(q1,q2)
2+%MULTIPLY Calculate the product of two quaternions.
3+% A single quaternion is represented as a 1 x 4 vector with the first
4+% component being the real part and the 2nd to 4th components
5+% corresponding to the complex vector part.Collections of N quaternions
6+% can be stored as N x 4 matrices.
7+
8+% Calculate vector portion of quaternion product
9+% vec = s1*v2 + s2*v1 + cross(v1,v2)
10+vec = [q1(:,1).*q2(:,2) q1(:,1).*q2(:,3) q1(:,1).*q2(:,4)] + ...
11+ [q2(:,1).*q1(:,2) q2(:,1).*q1(:,3) q2(:,1).*q1(:,4)]+...
12+ [ q1(:,3).*q2(:,4)-q1(:,4).*q2(:,3) ...
13+ q1(:,4).*q2(:,2)-q1(:,2).*q2(:,4) ...
14+ q1(:,2).*q2(:,3)-q1(:,3).*q2(:,2)];
15+
16+% Calculate scalar portion of quaternion product
17+% scalar = s1*s2 - dot(v1,v2)
18+scalar = q1(:,1).*q2(:,1) - q1(:,2).*q2(:,2) - ...
19+ q1(:,3).*q2(:,3) - q1(:,4).*q2(:,4);
20+
21+qout = [scalar vec];
22+
23+end
24+
src/engine/pulseq/+mr/+aux/+quat/normalize.madded+13−0View file
@@ -0,0 +1,13 @@
1+function q = normalize(q)
2+%NORMALIZE normalizes quaternion q (or array of quaternions)
3+
4+n2=sum(q'.^2)';
5+if any(n2>0)
6+ if length(n2)>1
7+ ninv=n2(n2>0).^-0.5;
8+ q(n2>0,:)=q(n2>0,:).*ninv(:,ones(1,4));
9+ else
10+ ninv=n2^-0.5;
11+ q=q*ninv;
12+ end
13+end
src/engine/pulseq/+mr/+aux/+quat/rotate.madded+30−0View file
@@ -0,0 +1,30 @@
1+function r = rotate(q,v)
2+%ROTATE rotate vector v by quaternion q
3+% Rotation ov a vector v by a unit quaternion q can be expressed as qvq',
4+% where v is temporarily transformed to a quaternion of a form [0 v]. The
5+% function below spells it out explicitly to accelerate calculations
6+
7+% A simple quaternion product can be calculated for the vector and scalar
8+% parts of the quaternion as follows:
9+% vec = s1*v2 + s2*v1 + cross(v1,v2)
10+% scalar = s1*s2 - dot(v1,v2)
11+% We redefine variables as s1=sq=q(0), v1=vq=q(2:4), s2=0, v2=v
12+% The first part of the product can be writtes as
13+% vec1= sq*v + cross(vq,v)
14+% scl1= -dot(vq,v)
15+% The second part of the product is then
16+% vec2= -scl1*vq + sq*vec1 + cross(vec1,-vq)
17+% scl2= scl1*sq - dot(vec1,-vq)
18+% but actually scl2 sould be discarded so we don't calculate it
19+
20+% here is what ChatGPT derives
21+%r1=(q1^2+q2^2−q3^2−q4^2)*v1+2*(q2*q3−q1*q4)*v2+2*(q2*q4+q1*q3)*v3
22+%r2=2*(q2*q3+q1*q4)*v1+(q1^2−q2^2+q3^2−q4^2)*v2+2*(q3*q4−q1*q2)*v3
23+%r3=2*(q2*q4−q1*q3)*v1+2*(q3*q4+q1*q2)*v2+(q1^2−q2^2−q3^2+q4^2)*v3
24+
25+r = [(q(:,1)^2 + q(:,2)^2 - q(:,3)^2 - q(:,4)^2)*v(:,1) + 2*(q(:,2)*q(:,3) - q(:,1)*q(:,4))*v(:,2) + 2*(q(:,2)*q(:,4) + q(:,1)*q(:,3))*v(:,3) ...
26+ 2*(q(:,2)*q(:,3) + q(:,1)*q(:,4))*v(:,1) + (q(:,1)^2 - q(:,2)^2 + q(:,3)^2 - q(:,4)^2)*v(:,2) + 2*(q(:,3)*q(:,4) - q(:,1)*q(:,2))*v(:,3) ...
27+ 2*(q(:,2)*q(:,4) - q(:,1)*q(:,3))*v(:,1) + 2*(q(:,3)*q(:,4) + q(:,1)*q(:,2))*v(:,2) + (q(:,1)^2 - q(:,2)^2 - q(:,3)^2 + q(:,4)^2)*v(:,3) ];
28+
29+end
30+
src/engine/pulseq/+mr/+aux/+quat/toRotMat.madded+13−0View file
@@ -0,0 +1,13 @@
1+function r = toRotMat(q)
2+%ROTATE convert normalized quaternion q to rotation matrix
3+
4+if length(q(:))~=4
5+ error('Only a single quaternion expressed as a matlab vector of length 4 can be processed');
6+end
7+
8+r = [(1 - 2*(q(3)^2 + q(4)^2)), 2*(q(2)*q(3) - q(1)*q(4)), 2*(q(2)*q(4) + q(1)*q(3)); ...
9+ 2*(q(2)*q(3) + q(1)*q(4)), (1 - 2*(q(2)^2 + q(4)^2)), 2*(q(3)*q(4) - q(1)*q(2)); ...
10+ 2*(q(2)*q(4) - q(1)*q(3)), 2*(q(3)*q(4) + q(1)*q(2)), (1 - 2*(q(2)^2 + q(3)^2))];
11+
12+end
13+
src/engine/pulseq/+mr/+aux/+rotmat/x.madded+7−0View file
@@ -0,0 +1,7 @@
1+function m = x(al)
2+% X Rotation matrix for rotation about X to an angle al given in radians
3+c = cos(al);
4+s = sin(al);
5+m = [1 0 0; 0 c -s; 0 s c];
6+end
7+
src/engine/pulseq/+mr/+aux/+rotmat/y.madded+7−0View file
@@ -0,0 +1,7 @@
1+function m = y(al)
2+% Y Rotation matrix for rotation about Y to an angle al given in radians
3+c = cos(al);
4+s = sin(al);
5+m = [c 0 s; 0 1 0; -s 0 c];
6+end
7+
src/engine/pulseq/+mr/+aux/+rotmat/z.madded+7−0View file
@@ -0,0 +1,7 @@
1+function m = y(al)
2+% Y Rotation matrix for rotation about Y to an angle al given in radians
3+c = cos(al);
4+s = sin(al);
5+m = [c -s 0; s c 0; 0 0 1];
6+end
7+
src/engine/pulseq/+mr/+aux/@InputParserCompat/InputParserCompat.madded+20−0View file
@@ -0,0 +1,20 @@
1+classdef InputParserCompat < inputParser
2+ % this is a customized parser class to replicate Matlab's undocumented
3+ % behaviour on GNU Octave. In Matlab the optional positional arguments can
4+ % also be called as param-value-pair with an arbitrary position
5+ properties
6+ needCopyOptional = mr.aux.isOctave();
7+ end
8+
9+ methods
10+ function parse (this, varargin)
11+ if this.needCopyOptional && length(this.Optional)>0
12+ for i=1:length(this.Optional)
13+ this.Parameter(end+1)=this.Optional{i};
14+ end
15+ this.needCopyOptional = false;
16+ end
17+ parse@inputParser(this, varargin{:});
18+ end
19+ end
20+end
src/engine/pulseq/+mr/+aux/@SeqPlot/SeqPlot.madded+454−0View file
@@ -0,0 +1,454 @@
1+classdef SeqPlot < handle
2+ %plot Plot the sequence in a new figure.
3+ % plot(seqObj) Plot the sequence
4+ %
5+ % plot(...,'timeRange',[start stop]) Plot the sequence
6+ % between the times specified by start and stop.
7+ %
8+ % plot(...,'blockRange',[first last]) Plot the sequence
9+ % starting from the first specified block to the last one.
10+ %
11+ % plot(...,'timeDisp',unit) Display time in:
12+ % 's', 'ms' or 'us'.
13+ %
14+ % plot(...,'label','LIN,REP') Plot label values for ADC events:
15+ % in this example for LIN and REP labels; other valid labes are
16+ % accepted as a comma-separated list.
17+ %
18+ % plot(...,'showBlocks',1) Plot grid and tick labels at the
19+ % block boundaries. Accepts a numeric or a boolean parameter.
20+ %
21+ % plot(...,'stacked',1) Rearrange the plots such they are vertically
22+ % stacked and share the same x-axis. Accepts a numeric or a boolean
23+ % parameter.
24+ %
25+ % plot(...,'showGuides',1) How dynamic hairline guides that follow
26+ % the data cursor to help verifying event alignment. Accepts a
27+ % numeric or a boolean parameter.
28+ %
29+ % f=plot(...) Return the new figure handle.
30+ %
31+
32+ properties (Access = public)
33+ f % figure handle
34+ end
35+
36+ properties (Access = private)
37+ ax % array of plot axes handles
38+ vLines % array of vline handles
39+
40+ hSeq
41+
42+ end
43+
44+ properties (Constant = true, Hidden = true)
45+
46+ % vertical margin (px)
47+ margin = 6;
48+ % lower vertical margin (px)
49+ my1 = 45;
50+ % left horizontal margin
51+ mx1 = 70;
52+ % right horizontal margin
53+ mx2 = 5;
54+
55+ end
56+
57+
58+ methods
59+
60+ function obj = SeqPlot(seq, varargin)
61+
62+ validTimeUnits = {'s','ms','us'};
63+ validLabel = mr.getSupportedLabels();
64+ persistent parser
65+ if isempty(parser)
66+ parser = inputParser;
67+ parser.FunctionName = 'plot';
68+ parser.addParamValue('showBlocks',false,@(x)(isnumeric(x) || islogical(x)));
69+ parser.addParamValue('timeRange',[0 inf],@(x)(isnumeric(x) && length(x)==2));
70+ parser.addParamValue('blockRange',[1 inf],@(x)(isnumeric(x) && length(x)==2));
71+ parser.addParamValue('timeDisp',validTimeUnits{1},...
72+ @(x) any(validatestring(x,validTimeUnits)));
73+ parser.addParamValue('label',[]);%,@(x)(isstr(x)));%@(x) any(validatestring(x,validLabel))
74+ parser.addParamValue('hide',false);%,@(x)(isstr(x)));%@(x) any(validatestring(x,validLabel))
75+ parser.addParamValue('stacked',false);%,@(x)(isstr(x)));%@(x) any(validatestring(x,validLabel))
76+ parser.addParamValue('showGuides',true);%,@(x)(isstr(x)));%@(x) any(validatestring(x,validLabel))
77+ end
78+ parse(parser,varargin{:});
79+ opt = parser.Results;
80+
81+ if mr.aux.isOctave()
82+ if opt.stacked
83+ warning('Option stacked is not (yet) supported by Octave');
84+ opt.stacked=false;
85+ end
86+ if opt.showBlocks
87+ warning('Option stacked is not (yet) supported by Octave');
88+ opt.showBlocks=false;
89+ end
90+ end
91+
92+ obj.f=figure;
93+ obj.hSeq=seq; % Sequence is a handle-class so copying is cheap...
94+
95+ set(obj.f, 'Visible', 'off')
96+
97+ if ~mr.aux.isOctave()
98+ obj.ax = gobjects(1,6);
99+ end
100+ for i=1:6
101+ obj.ax(i)=subplot(3,2,i);
102+ end
103+ obj.ax=obj.ax([1 3 5 2 4 6]); % Re-order axes
104+ arrayfun(@(x)hold(x,'on'),obj.ax);
105+ arrayfun(@(x)grid(x,'on'),obj.ax);
106+ labels={'ADC/lbl/trig','RF mag (Hz)','RF/ADC ph (rad)','Gx (kHz/m)','Gy (kHz/m)','Gz (kHz/m)'};
107+ arrayfun(@(x)ylabel(obj.ax(x),labels{x}),1:6);
108+
109+ tFactorList = [1 1e3 1e6];
110+ tFactor = tFactorList(strcmp(opt.timeDisp,validTimeUnits));
111+ xlabel(obj.ax(3),['t (' opt.timeDisp ')']);
112+ xlabel(obj.ax(6),['t (' opt.timeDisp ')']);
113+
114+ t0=0;
115+ label_defined=false;
116+ label_indexes_2plot=[];
117+ label_legend_2plot=[];
118+ for i=1:length(validLabel)
119+ label_store.(validLabel{i})=0;
120+ if ~isempty(opt.label) && ~isempty(strfind(upper(opt.label),validLabel{i}))
121+ label_indexes_2plot=[label_indexes_2plot i];
122+ label_legend_2plot=[label_legend_2plot; validLabel{i}];
123+ end
124+ end
125+ if ~isempty(label_indexes_2plot)
126+ if mr.aux.isOctave()
127+ label_colors_2plot=turbo(length(label_indexes_2plot)+1); % need +1 because the ADC plot by itself also "eats up" one color
128+ else
129+ label_colors_2plot=parula(length(label_indexes_2plot)+1); % need +1 because the ADC plot by itself also "eats up" one color
130+ end
131+ label_colors_2plot=[label_colors_2plot(end,:); label_colors_2plot(1:end-1,:)]; % we like these colors better ?
132+ end
133+
134+ % time format
135+ switch opt.timeDisp
136+ case 'us'
137+ timeFormat='%.1f';
138+ case 'ms'
139+ timeFormat='%.4f';
140+ otherwise
141+ timeFormat='%.7f';
142+ end
143+
144+ % data cursor callback
145+ if ~mr.aux.isOctave()
146+ hDCM = datacursormode(obj.f);
147+ hDCM.UpdateFcn = @(src, event)DataTipHandler(obj,tFactor,[timeFormat ' ' opt.timeDisp],src,event);
148+ end
149+
150+ % time/block range
151+ timeRange=opt.timeRange;
152+ blockEdges=[0 cumsum(seq.blockDurations)];
153+ if opt.blockRange(1)>1 && blockEdges(opt.blockRange(1))>timeRange(1)
154+ timeRange(1)=blockEdges(opt.blockRange(1));
155+ end
156+ if isfinite(opt.blockRange(2)) && opt.blockRange(2)<length(seq.blockDurations) && blockEdges(opt.blockRange(2)+1)<timeRange(2)
157+ timeRange(2)=blockEdges(opt.blockRange(2)+1);
158+ end
159+ % block timings
160+ blockEdgesInRange=blockEdges(logical((blockEdges>=timeRange(1)).*(blockEdges<=timeRange(2))));
161+ if strcmp(opt.timeDisp,'us') && ~mr.aux.isOctave()
162+ for i=1:6
163+ xax=get(obj.ax(i),'XAxis');
164+ xax.ExponentMode='manual';
165+ xax.Exponent=0;
166+ end
167+ end
168+ if opt.showBlocks
169+ % show block edges in plots
170+ for i=1:6
171+ xax=get(obj.ax(i),'XAxis');
172+ xax.TickValues=unique(tFactor.*blockEdgesInRange);
173+ set(obj.ax(i),'XTickLabelRotation',90);
174+ %xax.MinorTickValues=tFactor.*blockEdgesInRange;
175+ %set(obj.ax(i),'XMinorTick', 'on');
176+ %set(obj.ax(i),'XMinorGrid', 'on');
177+ %set(obj.ax(i),'GridColor',0.8*[1 1 1]);
178+ %set(obj.ax(i),'MinorGridColor',0.6*[1 1 1]);
179+ %set(obj.ax(i),'MinorGridLineStyle','-');
180+ end
181+ end
182+ %
183+ gradChannels={'gx','gy','gz'};
184+
185+ % loop through blocks
186+ for iB=1:length(seq.blockEvents)
187+ block = seq.getBlock(iB);
188+ if isfield(block,'rotation')
189+ % apply the rotation to the current block and restore the block structure
190+ c=mr.rotate3D(block.rotation.rotQuaternion,block,'system',seq.sys);
191+ for i=1:3
192+ block.(gradChannels{i})=[];
193+ end
194+ for i=1:length(c)
195+ if isstruct(c{i}) && isfield(c{i},'type') && isfield(c{i},'channel')
196+ block.(['g' c{i}.channel])=c{i};
197+ end
198+ end
199+ end
200+ if t0<=timeRange(2)
201+ % update the labels / counters even if we are below the display range
202+ if isfield(block,'label') %current labels, works on the curent or next adc
203+ for i=1:length(block.label)
204+ if strcmp(block.label(i).type,'labelinc')
205+ label_store.(block.label(i).label)=...
206+ label_store.(block.label(i).label)+block.label(i).value;
207+ else
208+ label_store.(block.label(i).label)=block.label(i).value;
209+ end
210+ end
211+ label_defined=true;
212+ end
213+ end
214+ isValid = t0+seq.blockDurations(iB)>timeRange(1) && t0<=timeRange(2);
215+ if isValid
216+ if isfield(block,'trig') && ~isempty(block.trig)
217+ switch(block.trig.type)
218+ case 'output'
219+ % plot digital output triggers in the RF-TX pane
220+ p2x=plot(tFactor*(t0+block.trig.delay),0,'diamond','Color',[0 0.5 0],'Parent',obj.ax(1));
221+ p2x=plot(tFactor*(t0+block.trig.delay +[0 block.trig.duration]),[0 0],'-','Marker','.','Color',[0 0.5 0],'Parent',obj.ax(1));
222+ case 'trigger'
223+ p1x=plot(tFactor*(t0+block.trig.delay),0,'>b','Parent',obj.ax(1));
224+ p1x=plot(tFactor*(t0+block.trig.delay),0,'.b','Parent',obj.ax(1));
225+ %otherwise
226+ end
227+ end
228+ if ~isempty(block.adc)
229+ adc=block.adc;
230+ t=adc.delay + ((0:adc.numSamples-1)'+0.5)*adc.dwell; % according to the information from Klaus Scheffler and indirectly from Siemens this is the present convention (the samples are shifted by 0.5 dwell)
231+ p1=plot(tFactor*(t0+t),zeros(size(t)),'rx','Parent',obj.ax(1));
232+ if isempty(adc.phaseModulation)
233+ adc.phaseModulation=0;
234+ end
235+ full_freqOffset=adc.freqOffset+adc.freqPPM*1e-6*seq.sys.gamma*seq.sys.B0;
236+ full_phaseOffset=adc.phaseOffset+adc.phasePPM*1e-6*seq.sys.gamma*seq.sys.B0;
237+ p2=plot(tFactor*(t0+t), angle(exp(1i*(full_phaseOffset+adc.phaseModulation)).*exp(1i*2*pi*t*full_freqOffset)),'b.','MarkerSize',1,'Parent',obj.ax(3)); % plot ADC phase
238+ % labels/counters/flags
239+ if label_defined && ~isempty(label_indexes_2plot)
240+ set(obj.ax(1),'ColorOrder',label_colors_2plot);
241+ label_store_cell=struct2cell(label_store);
242+ lbl_vals=[label_store_cell{label_indexes_2plot}];
243+ t=t0+adc.delay + (adc.numSamples-1)/2*adc.dwell;
244+ p=plot(tFactor*t,lbl_vals,'.','markersize',5,'Parent',obj.ax(1));
245+ if ~isempty(label_legend_2plot)
246+ legend(obj.ax(1),p,label_legend_2plot,'location','Northwest','AutoUpdate','off');
247+ label_legend_2plot=[];
248+ end
249+ end
250+ end
251+ if ~isempty(block.rf)
252+ rf=block.rf;
253+ [tc,ic,fi]=mr.calcRfCenter(rf);
254+ if fi==0
255+ sc=rf.signal(ic);
256+ else
257+ sc=rf.signal(ic)*(1-abs(fi))+rf.signal(ic+sign(fi))*abs(fi);
258+ end
259+ if max(abs(diff(rf.t)-rf.t(2)+rf.t(1)))<1e-9 && length(rf.t)>100
260+ % homogeneous sampling and long pulses -- use lower time resolution for better display and performance
261+ dt=rf.t(2)-rf.t(1);
262+ st=max(1,round(seq.sys.gradRasterTime/dt));
263+ t=rf.t(1:st:end);
264+ s=rf.signal(1:st:end);
265+ % always include the last point for the accurate display
266+ if (t(end)~=rf.t(end))
267+ t(end+1)=rf.t(end);
268+ s(end+1)=rf.signal(end);
269+ end
270+ else
271+ t=rf.t;
272+ s=rf.signal;
273+ end
274+ sreal=max(abs(imag(s)))/max(abs(real(s)))<1e-6; %all(isreal(s));
275+ full_freqOffset=rf.freqOffset+rf.freqPPM*1e-6*seq.sys.gamma*seq.sys.B0;
276+ full_phaseOffset=rf.phaseOffset+rf.phasePPM*1e-6*seq.sys.gamma*seq.sys.B0;
277+ % If off-resonant and rectangular (2 samples), interpolate the pulse
278+ if (length(s) == 2) && (full_freqOffset ~= 0)
279+ numInterp = min(int32(abs(full_freqOffset)), 256);
280+ t = linspace(t(1), t(end), numInterp)';
281+ s = linspace(s(1), s(end), numInterp)';
282+ end
283+ if abs(s(1))~=0 % fix strangely looking phase / amplitude in the beginning
284+ s=[0; s];
285+ t=[t(1); t];
286+ %ic=ic+1;
287+ end
288+ if abs(s(end))~=0 % fix strangely looking phase / amplitude at the end
289+ s=[s; 0];
290+ t=[t; t(end)];
291+ end
292+
293+ if (sreal)
294+ p1=plot(tFactor*(t0+t+rf.delay), real(s),'Parent',obj.ax(2));
295+ p2=plot(tFactor*(t0+t+rf.delay), angle(s.*sign(real(s))*exp(1i*full_phaseOffset).*exp(1i*2*pi*t *full_freqOffset)), tFactor*(t0+tc+rf.delay), angle(sc*exp(1i*full_phaseOffset).*exp(1i*2*pi*tc*full_freqOffset)),'xb', 'Parent',obj.ax(3));
296+ else
297+ p1=plot(tFactor*(t0+t+rf.delay), abs(s),'Parent',obj.ax(2));
298+ p2=plot(tFactor*(t0+t+rf.delay), angle(s*exp(1i*full_phaseOffset).*exp(1i*2*pi*t *full_freqOffset)), tFactor*(t0+tc+rf.delay), angle(sc*exp(1i*full_phaseOffset).*exp(1i*2*pi*tc*full_freqOffset)),'xb', 'Parent',obj.ax(3));
299+ end
300+ end
301+ for j=1:length(gradChannels)
302+ grad=block.(gradChannels{j});
303+ if ~isempty(grad)
304+ if strcmp(grad.type,'grad')
305+ % we extend the shape by adding the first
306+ % and the last points in an effort of
307+ % making the display a bit less confusing...
308+ %t=grad.delay + [0; grad.t + (grad.t(2)-grad.t(1))/2; grad.t(end) + grad.t(2)-grad.t(1)];
309+ t= grad.delay+[0; grad.tt; grad.shape_dur];
310+ waveform=1e-3* [grad.first; grad.waveform; grad.last];
311+ else
312+ t=cumsum([0 grad.delay grad.riseTime grad.flatTime grad.fallTime]);
313+ waveform=1e-3*grad.amplitude*[0 0 1 1 0];
314+ end
315+ p=plot(tFactor*(t0+t),waveform,'Parent',obj.ax(3+j));
316+ end
317+ end
318+ end
319+ t0=t0+seq.blockDurations(iB);%mr.calcDuration(block);
320+ end
321+
322+ % Set axis limits and zoom properties
323+ dispRange = tFactor*[timeRange(1) min(timeRange(2),t0)];
324+ arrayfun(@(x)xlim(x,dispRange),obj.ax);
325+ linkaxes(obj.ax(:),'x')
326+ if ~mr.aux.isOctave()
327+ h = zoom(obj.f);
328+ setAxesZoomMotion(h,obj.ax(1),'horizontal');
329+ end
330+ % manually fix the phase vertical scale to +- pi
331+ ylim(obj.ax(3),[-pi pi]);
332+ % make Y-axes little bit less tight
333+ arrayfun(@(x) ylim(x, ylim(x) + 0.03*[-1 1]*sum(ylim(x).*[-1 1])), obj.ax(2:end));
334+
335+
336+ if opt.showGuides
337+ if mr.aux.isOctave()
338+ warning('Option showGuides is not implemented in Octave');
339+ else
340+ % add vertical lines and make them follow the cursor
341+ % x-position
342+ for ii = 1:numel(obj.ax)
343+ obj.vLines(ii) = xline(obj.ax(ii), 0, 'r--');
344+ end
345+ end
346+ end
347+
348+ if opt.stacked
349+ % vertical stacking is defined in guiResize
350+ set(obj.f, 'ResizeFcn', @obj.guiResize)
351+ obj.guiResize()
352+ end
353+
354+ if ~opt.hide
355+ set(obj.f, 'Visible', 'on')
356+ end
357+
358+ % do not assign to 'ans' when called without assigned variable
359+ if nargout == 0
360+ clear obj
361+ end
362+ end
363+
364+ function guiResize(obj, ~, ~)
365+ % guiResize()
366+ % Is called whenever the figure-shape is changed and makes
367+ % sure all UI elements are correctly psotitioned. This
368+ % function implements a vertical stacking of the individual
369+ % axes.
370+
371+ nAxes = numel(obj.ax);
372+ width = obj.f.Position(3);
373+ height = obj.f.Position(4);
374+
375+ axHeight = (height - (nAxes-1)*obj.margin - obj.my1) / nAxes;
376+ axWidth = width - obj.mx1 - obj.mx2;
377+
378+ for ii = 1:nAxes
379+ set(obj.ax(ii), 'units', 'pixels', 'Position', [obj.mx1, height-ii*axHeight-(ii-1)*obj.margin, axWidth, axHeight])
380+ if ii ~= nAxes
381+ set(obj.ax(ii), 'Xlabel', [])
382+ set(obj.ax(ii), 'XTickLabel', {})
383+ end
384+ end
385+ end
386+
387+ function out=DataTipHandler(obj, tfactor, timeFormat, src, event)
388+ if ~isa(event,'matlab.graphics.internal.DataTipEvent') || ...
389+ ~isprop(event, 'Position') || length(event.Position)<2 || ...
390+ ~isprop(event, 'Target')
391+ out=[];
392+ return;
393+ end
394+ ax=src.Host.Parent;
395+ % get the relevant target from the y-axes title
396+ at=lower(ax.YLabel.String);
397+ if strcmp(at(1:3),'adc') || ...
398+ (strcmp(at(1:6),'rf/adc') && strcmp(event.Target.LineStyle,'none') && strcmp(event.Target.Marker,'.')) % we need to check whether we are dealing with the ADC phase, which is also shown in the same panel as the RF
399+ field='adc';
400+ else
401+ field=at(1:2);
402+ end
403+ % create the custom data tip as tex-formatted cell array of lines
404+ t=event.Position(1);
405+ t0=t;
406+ if isa(event.Target,'matlab.graphics.chart.primitive.Line')
407+ % for trapezoid gradients the last point may belong to the next block
408+ t0=event.Target.XData(1);
409+ end
410+ iB=obj.hSeq.findBlockByTime(t0/tfactor);
411+ rb=obj.hSeq.getRawBlockContentIDs(iB);
412+ out={['\bf\color{blue}t:\rm\color{black}' sprintf(timeFormat,t)],...
413+ ['\bf\color{blue}Y:\rm\color{black}' num2str(event.Position(2))],...
414+ ''};
415+ if isempty(rb.(field))
416+ out{3}=['\bf\color{blue}blk:\rm\color{black}' num2str(iB)];
417+ % we could add handling of the trigger/label data tips here
418+ % specifically for the adc panel but it would imply a
419+ % substantial performance hit because we'd have to unpack
420+ % extensions, etc...
421+ else
422+ try
423+ switch field(1)
424+ case 'a'
425+ name = obj.hSeq.adcID2NameMap(rb.(field));
426+ case 'r'
427+ name = obj.hSeq.rfID2NameMap(rb.(field));
428+ otherwise
429+ name = obj.hSeq.gradID2NameMap(rb.(field));
430+ end
431+ out{3}=['\bf\color{blue}blk:\rm\color{black}' num2str(iB) ' \bf\color{blue}' field '\_id:\rm\color{black}' num2str(rb.(field)) ' ''\bf\color{darkGreen}' name '\rm\color{black}'''];
432+ catch
433+ out{3}=['\bf\color{blue}blk:\rm\color{black}' num2str(iB) ' \bf\color{blue}' field '\_id:\rm\color{black}' num2str(rb.(field))];
434+ end
435+ end
436+
437+ % we need to delay the call of the update, otherwise the plot
438+ % object generates an exception
439+ t=timer('StartDelay',0e-3,'Period',1e-3,'TimerFcn',@(~,~)updateGuides(obj,t));
440+ t.start();
441+ end
442+
443+ function updateGuides(obj, tPos)
444+ % updateGuides(tPos)
445+ % updates the time-position for all vertical line objects in
446+ % all axes
447+
448+ for ii = 1:numel(obj.vLines)
449+ set(obj.vLines(ii), 'Value', tPos);
450+ end
451+ end
452+ end
453+end
454+
src/engine/pulseq/+mr/+aux/BalancedBST.madded+486−0View file
@@ -0,0 +1,486 @@
1+classdef BalancedBST < handle
2+ %BALANCEDBST AVL-balanced binary search tree mapping keys to data. See
3+ % https://en.wikipedia.org/wiki/AVL_tree for further infos.
4+ %
5+ % Keys are typically char vectors (strings); data is typically a
6+ % scalar, but both can be any MATLAB type whose comparison is
7+ % well-defined (numeric or char/string for keys).
8+ %
9+ % Performance notes
10+ % -----------------
11+ % Cell arrays in MATLAB are contiguous pointer arrays with O(1)
12+ % random access — NOT linked lists. The tree traversal is
13+ % O(log n) key-comparisons. The pre-allocated parallel-array
14+ % storage avoids per-node heap allocations; doubling on overflow
15+ % gives amortised O(1) growth.
16+ %
17+ % Usage
18+ % -----
19+ % tree = mr.aux.BalancedBST();
20+ % tree.insert('alpha', 1);
21+ % tree.insert('beta', 2);
22+ %
23+ % [val, index] = tree.lookup('alpha', 0); % val=1, index=non-zero
24+ % [val, index] = tree.lookup('gamma', 0); % val=0, index=0
25+ %
26+
27+ % --- storage: parallel arrays (pre-allocated, grown as needed) -------
28+ properties (Access = private)
29+ keys % cell(1,cap) – stored keys
30+ vals % cell(1,cap) – stored data values
31+ L % int32(1,cap) – left-child index (0 = none)
32+ R % int32(1,cap) – right-child index (0 = none)
33+ H % int32(1,cap) – subtree height
34+ rootIdx % int32 – root node index (0 = empty tree)
35+ cnt % int32 – number of allocated slots (high-water mark)
36+ cap % int32 – allocated capacity
37+ keyIsChar % bool – whether the key is a char string or an arbitrary vector
38+ pathBuf % int32
39+ dirBuf % int32
40+ freeHead % int32 – head of free-slot linked list (0 = none)
41+ liveCnt % int32 – number of live (non-deleted) nodes
42+ end
43+
44+ % =====================================================================
45+ % PUBLIC INTERFACE
46+ % =====================================================================
47+ methods (Access = public)
48+
49+ function obj = BalancedBST(initialCapacity)
50+ %BALANCEDBST Construct an empty tree.
51+ % tree = mr.aux.BalancedBST()
52+ % tree = mr.aux.BalancedBST(initialCapacity)
53+ if nargin < 1, initialCapacity = 64; end
54+ obj.cap = int32(initialCapacity);
55+ obj.keys = cell(1, obj.cap);
56+ obj.vals = cell(1, obj.cap);
57+ obj.L = zeros(1, obj.cap, 'int32');
58+ obj.R = zeros(1, obj.cap, 'int32');
59+ obj.H = zeros(1, obj.cap, 'int32');
60+ obj.rootIdx = int32(0);
61+ obj.cnt = int32(0);
62+ obj.keyIsChar = false;
63+ obj.pathBuf = int32(0);
64+ obj.dirBuf = int32(0);
65+ obj.freeHead = int32(0);
66+ obj.liveCnt = int32(0);
67+ end
68+
69+ function [val, index] = lookup(obj, key, default)
70+ %LOOKUP Search for KEY in the tree.
71+ % [val, index] = tree.lookup(key, default)
72+ %
73+ % If KEY is present, VAL is the associated data and
74+ % INDEX is non-zero. Otherwise VAL = DEFAULT and INDEX is
75+ % zero.
76+ %
77+
78+ val = default;
79+ index = 0;
80+
81+ % --- cache property arrays as locals (COW = free) ------------
82+ % Every obj.prop(idx) in a loop pays ~300 ns dispatch
83+ % overhead; local variable access is ~1 ns. For a
84+ % read-only traversal COW means the snapshot is free.
85+ %kk = obj.keys;
86+ %LL = obj.L;
87+ %RR = obj.R;
88+
89+ idx = obj.rootIdx;
90+ while idx ~= int32(0)
91+ c = mr.aux.BalancedBST.compareKeys(key, obj.keys{idx});
92+ if c == 0
93+ val = obj.vals{idx}; % single property access
94+ index = idx;
95+ break;
96+ end
97+ if c < 0
98+ idx = obj.L(idx);
99+ else
100+ idx = obj.R(idx);
101+ end
102+ end
103+ end
104+
105+ function insert(obj, key, val)
106+ %INSERT Insert or update a key-value pair.
107+ % tree.insert(key, val)
108+ %
109+ % If KEY already exists its data is overwritten.
110+
111+ root = obj.rootIdx;
112+ if root == int32(0) % the tree was empty up to now
113+ obj.rootIdx = obj.allocNode(key, val);
114+ obj.keyIsChar = ischar(key);
115+ return;
116+ end
117+
118+ % --- search phase --------------------------------------------
119+ depth = int32(0);
120+ idx = root;
121+
122+ while idx ~= int32(0)
123+ c = mr.aux.BalancedBST.compareKeys(key, obj.keys{idx});
124+ depth = depth + 1;
125+ obj.pathBuf(depth) = idx;
126+ if c == 0
127+ obj.vals{idx} = val; % overwrite existing value
128+ return;
129+ elseif c < 0
130+ obj.dirBuf(depth) = int32(-1);
131+ idx = obj.L(idx);
132+ else
133+ obj.dirBuf(depth) = int32(1);
134+ idx = obj.R(idx);
135+ end
136+ end
137+
138+ % --- allocate new node and link to parent --------------------
139+ newIdx = obj.allocNode(key, val);
140+ pIdx = obj.pathBuf(depth);
141+ if obj.dirBuf(depth) < 0
142+ obj.L(pIdx) = newIdx;
143+ else
144+ obj.R(pIdx) = newIdx;
145+ end
146+
147+ % --- rebalance bottom-up -------------------------------------
148+ obj.rebalanceUp(depth);
149+ end
150+
151+ function update(obj, index, val)
152+ %UPDATE Update the value at a known node index in O(1).
153+ % tree.update(index, newVal)
154+ %
155+ % INDEX is typically obtained from a prior tree.lookup().
156+ % The key is unchanged; only the associated data is
157+ % overwritten. No rebalancing is needed.
158+ obj.vals{index} = val;
159+ end
160+
161+ function removed = remove(obj, key)
162+ %REMOVE Remove the entry with the given key.
163+ % removed = tree.remove(key)
164+ %
165+ % Returns true if KEY was found and removed, false if
166+ % KEY was not present in the tree.
167+
168+ if obj.rootIdx == int32(0)
169+ removed = false;
170+ return;
171+ end
172+
173+ % --- search for the node -------------------------------------
174+ depth = int32(0);
175+ idx = obj.rootIdx;
176+ while idx ~= int32(0)
177+ c = mr.aux.BalancedBST.compareKeys(key, obj.keys{idx});
178+ depth = depth + 1;
179+ obj.pathBuf(depth) = idx;
180+ if c == 0
181+ break;
182+ elseif c < 0
183+ obj.dirBuf(depth) = int32(-1);
184+ idx = obj.L(idx);
185+ else
186+ obj.dirBuf(depth) = int32(1);
187+ idx = obj.R(idx);
188+ end
189+ end
190+
191+ if idx == int32(0)
192+ removed = false;
193+ return;
194+ end
195+
196+ % idx == obj.pathBuf(depth) is the node to delete
197+ li = obj.L(idx);
198+ ri = obj.R(idx);
199+
200+ if li ~= 0 && ri ~= 0
201+ % --- TWO CHILDREN: replace with in-order successor --------
202+ % Go right once, then left as far as possible.
203+ obj.dirBuf(depth) = int32(1); % going right from idx
204+ succ = ri;
205+ depth = depth + 1;
206+ obj.pathBuf(depth) = succ;
207+ while obj.L(succ) ~= int32(0)
208+ obj.dirBuf(depth) = int32(-1);
209+ succ = obj.L(succ);
210+ depth = depth + 1;
211+ obj.pathBuf(depth) = succ;
212+ end
213+ % Copy successor's key/value to the target node
214+ obj.keys{idx} = obj.keys{succ};
215+ obj.vals{idx} = obj.vals{succ};
216+ % Successor has at most a right child
217+ replacement = obj.R(succ);
218+ obj.freeNode(succ);
219+ else
220+ % --- ZERO or ONE CHILD -----------------------------------
221+ if li ~= 0
222+ replacement = li;
223+ else
224+ replacement = ri; % may be 0 (leaf)
225+ end
226+ obj.freeNode(idx);
227+ end
228+
229+ % --- link replacement to parent of deleted node ---------------
230+ if depth > 1
231+ p = obj.pathBuf(depth - 1);
232+ if obj.dirBuf(depth - 1) < 0
233+ obj.L(p) = replacement;
234+ else
235+ obj.R(p) = replacement;
236+ end
237+ else
238+ obj.rootIdx = replacement;
239+ end
240+
241+ % --- rebalance from parent of deleted node upward -------------
242+ obj.rebalanceUp(depth - 1);
243+
244+ removed = true;
245+ end
246+
247+ function n = length(obj)
248+ %LENGTH Number of live key-value pairs in the tree.
249+ n = double(obj.liveCnt);
250+ end
251+
252+ % function varargout = subsref(obj, S)
253+ % %SUBSREF Overloaded subscript reference.
254+ % % val = tree('key') — equivalent to tree.lookup('key', [])
255+ % %
256+ % % Dot-reference (tree.method, tree.prop) and curly-brace
257+ % % indexing are forwarded to the built-in handler so that
258+ % % normal method calls keep working.
259+ % if S(1).type(1) == '('
260+ % key = S(1).subs{1};
261+ % [val, ~] = obj.lookup(key, []);
262+ % if numel(S) > 1
263+ % % chained indexing, e.g. tree('key').field
264+ % [varargout{1:nargout}] = subsref(val, S(2:end));
265+ % else
266+ % varargout{1} = val;
267+ % end
268+ % else
269+ % % '.' or '{}' — delegate to built-in
270+ % [varargout{1:nargout}] = builtin('subsref', obj, S);
271+ % end
272+ % end
273+
274+ function obj = subsasgn(obj, S, val)
275+ %SUBSASGN Overloaded subscript assignment.
276+ % tree('key') = val — equivalent to tree.insert('key', val)
277+ %
278+ % Dot-assignment and curly-brace assignment are forwarded
279+ % to the built-in handler.
280+ if S(1).type(1) == '(' && numel(S) == 1
281+ key = S(1).subs{1};
282+ obj.insert(key, val);
283+ else
284+ % '.' or '{}' or chained — delegate to built-in
285+ obj = builtin('subsasgn', obj, S, val);
286+ end
287+ end
288+
289+ end % public methods
290+
291+ % =====================================================================
292+ % PRIVATE HELPERS
293+ % =====================================================================
294+ methods (Access = private)
295+
296+ % ----- node allocation -------------------------------------------
297+ function idx = allocNode(obj, key, val)
298+ if obj.freeHead ~= int32(0)
299+ idx = obj.freeHead;
300+ obj.freeHead = obj.L(idx); % L was reused as next-free
301+ else
302+ obj.cnt = obj.cnt + 1;
303+ if obj.cnt > obj.cap
304+ obj.grow();
305+ end
306+ idx = obj.cnt;
307+ end
308+ obj.keys{idx} = key;
309+ obj.vals{idx} = val;
310+ obj.L(idx) = int32(0);
311+ obj.R(idx) = int32(0);
312+ obj.H(idx) = int32(1);
313+ obj.liveCnt = obj.liveCnt + 1;
314+ end
315+
316+ function freeNode(obj, idx)
317+ %FREENODE Return a slot to the free list.
318+ obj.keys{idx} = [];
319+ obj.vals{idx} = [];
320+ obj.R(idx) = int32(0);
321+ obj.H(idx) = int32(0);
322+ obj.L(idx) = obj.freeHead; % reuse L as next-free pointer
323+ obj.freeHead = idx;
324+ obj.liveCnt = obj.liveCnt - 1;
325+ end
326+
327+ function grow(obj)
328+ added = obj.cap; % double the capacity
329+ obj.keys = [obj.keys, cell(1, added)];
330+ obj.vals = [obj.vals, cell(1, added)];
331+ obj.L = [obj.L, zeros(1, added, 'int32')];
332+ obj.R = [obj.R, zeros(1, added, 'int32')];
333+ obj.H = [obj.H, zeros(1, added, 'int32')];
334+ obj.cap = obj.cap + int32(added);
335+ end
336+
337+ % ----- rebalance from pathBuf(depth) up to root ------------------
338+ function rebalanceUp(obj, depth)
339+ for i = depth:-1:1
340+ nd = obj.pathBuf(i);
341+
342+ % -- refresh height (inlined) --
343+ li = obj.L(nd); ri = obj.R(nd);
344+ lh = int32(0); rh = int32(0);
345+ if li ~= 0, lh = obj.H(li); end
346+ if ri ~= 0, rh = obj.H(ri); end
347+ obj.H(nd) = int32(1) + max(lh, rh);
348+
349+ bf = rh - lh;
350+ nnd = nd;
351+
352+ if bf < -1
353+ % left-heavy
354+ child = obj.L(nd);
355+ cli = obj.L(child); cri = obj.R(child);
356+ clh = int32(0); crh = int32(0);
357+ if cli ~= 0, clh = obj.H(cli); end
358+ if cri ~= 0, crh = obj.H(cri); end
359+ if (crh - clh) > 0 % Left-Right case
360+ gc = obj.R(child);
361+ obj.R(child) = obj.L(gc);
362+ obj.L(gc) = child;
363+ tl = obj.L(child); tr = obj.R(child);
364+ tlh = int32(0); trh = int32(0);
365+ if tl ~= 0, tlh = obj.H(tl); end
366+ if tr ~= 0, trh = obj.H(tr); end
367+ obj.H(child) = int32(1) + max(tlh, trh);
368+ tl = obj.L(gc); tr = obj.R(gc);
369+ tlh = int32(0); trh = int32(0);
370+ if tl ~= 0, tlh = obj.H(tl); end
371+ if tr ~= 0, trh = obj.H(tr); end
372+ obj.H(gc) = int32(1) + max(tlh, trh);
373+ obj.L(nd) = gc;
374+ end
375+ x = obj.L(nd);
376+ obj.L(nd) = obj.R(x);
377+ obj.R(x) = nd;
378+ tl = obj.L(nd); tr = obj.R(nd);
379+ tlh = int32(0); trh = int32(0);
380+ if tl ~= 0, tlh = obj.H(tl); end
381+ if tr ~= 0, trh = obj.H(tr); end
382+ obj.H(nd) = int32(1) + max(tlh, trh);
383+ tl = obj.L(x); tr = obj.R(x);
384+ tlh = int32(0); trh = int32(0);
385+ if tl ~= 0, tlh = obj.H(tl); end
386+ if tr ~= 0, trh = obj.H(tr); end
387+ obj.H(x) = int32(1) + max(tlh, trh);
388+ nnd = x;
389+
390+ elseif bf > 1
391+ % right-heavy
392+ child = obj.R(nd);
393+ cli = obj.L(child); cri = obj.R(child);
394+ clh = int32(0); crh = int32(0);
395+ if cli ~= 0, clh = obj.H(cli); end
396+ if cri ~= 0, crh = obj.H(cri); end
397+ if (crh - clh) < 0 % Right-Left case
398+ gc = obj.L(child);
399+ obj.L(child) = obj.R(gc);
400+ obj.R(gc) = child;
401+ tl = obj.L(child); tr = obj.R(child);
402+ tlh = int32(0); trh = int32(0);
403+ if tl ~= 0, tlh = obj.H(tl); end
404+ if tr ~= 0, trh = obj.H(tr); end
405+ obj.H(child) = int32(1) + max(tlh, trh);
406+ tl = obj.L(gc); tr = obj.R(gc);
407+ tlh = int32(0); trh = int32(0);
408+ if tl ~= 0, tlh = obj.H(tl); end
409+ if tr ~= 0, trh = obj.H(tr); end
410+ obj.H(gc) = int32(1) + max(tlh, trh);
411+ obj.R(nd) = gc;
412+ end
413+ y = obj.R(nd);
414+ obj.R(nd) = obj.L(y);
415+ obj.L(y) = nd;
416+ tl = obj.L(nd); tr = obj.R(nd);
417+ tlh = int32(0); trh = int32(0);
418+ if tl ~= 0, tlh = obj.H(tl); end
419+ if tr ~= 0, trh = obj.H(tr); end
420+ obj.H(nd) = int32(1) + max(tlh, trh);
421+ tl = obj.L(y); tr = obj.R(y);
422+ tlh = int32(0); trh = int32(0);
423+ if tl ~= 0, tlh = obj.H(tl); end
424+ if tr ~= 0, trh = obj.H(tr); end
425+ obj.H(y) = int32(1) + max(tlh, trh);
426+ nnd = y;
427+ end
428+
429+ % -- re-link to parent ------------------------------------
430+ if nnd ~= nd
431+ if i > 1
432+ p = obj.pathBuf(i-1);
433+ if obj.dirBuf(i-1) < 0
434+ obj.L(p) = nnd;
435+ else
436+ obj.R(p) = nnd;
437+ end
438+ else
439+ obj.rootIdx = nnd;
440+ end
441+ end
442+ end
443+ end
444+
445+ end % private methods
446+
447+ % =====================================================================
448+ % STATIC (key comparison)
449+ % =====================================================================
450+ methods (Static, Access = private)
451+
452+ function c = compareKeys(a, b)
453+ %COMPAREKEYS Lexicographic comparison returning -1, 0, or +1.
454+ % Handles numeric keys (scalar <, >, ==) and char/string
455+ % keys (character-by-character comparison).
456+ % [~,I]=sort({a,b});
457+ % c = diff(I)*~strcmp(a,b);
458+ %la = numel(a); lb = numel(b);
459+ %ml = min(la, lb);
460+ for k = 1:min(numel(a),numel(b))
461+ % c=sign(int32(a(k))-int32(b(k)));
462+ % if c~=0
463+ % return;
464+ % end
465+ if a(k) > b(k)
466+ c = 1;
467+ return;
468+ elseif a(k) < b(k)
469+ c = -1;
470+ return;
471+ end
472+ end
473+ c = sign(numel(a)-numel(b));
474+ % a=string(a);
475+ % b=string(b);
476+ % if a>b
477+ % c = 1;
478+ % elseif a < b
479+ % c = -1;
480+ % else
481+ % c = 0;
482+ % end
483+ end
484+
485+ end % static methods
486+end
src/engine/pulseq/+mr/+aux/containers_map_lookup_ex.madded+10−0View file
@@ -0,0 +1,10 @@
1+function out = containers_map_lookup_ex(map,key,fallback)
2+%CONTAINERS_MAP_LOOKUP this function simulates the missing
3+%containers.Map.lookup() function using try-catch clause
4+ try
5+ out=map(key);
6+ catch
7+ out=fallback;
8+ end
9+end
10+
src/engine/pulseq/+mr/+aux/containers_map_lookup_ik.madded+11−0View file
@@ -0,0 +1,11 @@
1+function out = containers_map_lookup_ik(map,key,fallback)
2+%CONTAINERS_MAP_LOOKUP this function simulates the missing
3+%containers.Map.lookup() function using the isKey function
4+%(supposingly much faster on Octave than exception-try-catch)
5+ if map.isKey(key)
6+ out=map(key);
7+ else
8+ out=fallback;
9+ end
10+end
11+
src/engine/pulseq/+mr/+aux/findFlank.madded+18−0View file
@@ -0,0 +1,18 @@
1+function xf=findFlank(x,f,c)
2+%findFlank: find the x coordinate of the left flank of function f
3+% Finds the furst value within x for which abs(f) is greater than
4+% c*max(abs(f)). If xf is not the first element of x, then a linear
5+% interpolation is applied.
6+m=max(abs(f));
7+f=abs(f)/m-c;
8+i=find(f>0,1);
9+if i>1
10+ f0=f(i-1);
11+ f1=f(i);
12+ xf=(f1*x(i-1)-f0*x(i))/(f1-f0);
13+else
14+ xf=x(1);
15+end
16+
17+end
18+
src/engine/pulseq/+mr/+aux/globalVars.madded+20−0View file
@@ -0,0 +1,20 @@
1+function out = globalVars(get_set,var,val)
2+%GLOBAL Auxilary function to get or set global variables
3+
4+persistent SupportedLabels;
5+
6+if strcmp(get_set,'get')
7+ eval(['out=' var ';']);
8+elseif strcmp(get_set,'set')
9+ if nargin<3
10+ error('globalVars: missing the third argument');
11+ end
12+ eval([var '=val;']);
13+elseif strcmp(get_set,'reset')
14+ clear(var);
15+else
16+ error('globalVars: the first parameter must be either get or set');
17+end
18+
19+end
20+
src/engine/pulseq/+mr/+aux/isOctave.madded+8−0View file
@@ -0,0 +1,8 @@
1+function OUT = isOctave ()
2+% function that checks if we are in Octave
3+ persistent IO;
4+ if (isempty (IO))
5+ IO = exist ('OCTAVE_VERSION', 'builtin');
6+ end
7+ OUT = IO;
8+end
src/engine/pulseq/+mr/+aux/isSigPyAvailable.madded+51−0View file
@@ -0,0 +1,51 @@
1+function [sigPyOK, pythonExe] = isSigPyAvailable()
2+%ISSIGPYAVAILABLE Test whether Python is available and has SigPy installed
3+% If successfull stores a persistent state after the first call to accelerate
4+% subsequent checks; use clear GLOBALS to reset the persistent state.
5+
6+persistent static_pythonExe;
7+
8+if ~isempty (static_pythonExe)
9+ sigPyOK=true;
10+ pythonExe=static_pythonExe;
11+ return;
12+end
13+
14+executables = {'python3', 'python', 'py'};
15+test_commands = {'which -a', 'where'};
16+exes = {};
17+
18+if ispc
19+ null_out = 'nul';
20+ test_commands=test_commands(2:end); % there is no which on Windows anyway...
21+else
22+ null_out = '/dev/null';
23+end
24+
25+for c=1:length(test_commands)
26+ for e=1:length(executables)
27+ cmd=sprintf('%s %s 2>%s',test_commands{c},executables{e},null_out);
28+ [status, output] = system(cmd);
29+ if status~=0, continue; end
30+ lines = regexp(output,'\n','split');
31+ for l=1:length(lines)
32+ exe=mr.aux.strstrip(lines{l});
33+ if isempty(exe), continue; end
34+ % test for Python with sigPy
35+ cmd=sprintf('"%s" -c "import sigpy" 2>%s',exe,null_out);
36+ [status, output] = system(cmd);
37+ if status ~= 0, continue; end
38+ % found it!
39+ sigPyOK=true;
40+ pythonExe=exe;
41+ static_pythonExe=exe;
42+ return;
43+ end
44+ end
45+end
46+
47+% did't find python or functional sigPy
48+sigPyOK = false;
49+pythonExe = '';
50+
51+end
src/engine/pulseq/+mr/+aux/md5.madded+150−0View file
@@ -0,0 +1,150 @@
1+function digest = md5(message, noBuiltIn)
2+ % digest = md5(message)
3+ % Compute the MD5 digest of the message, as a hexadecimal digest.
4+ % Per default uses built-in functions if available, but can be
5+ % forced to use the explicit source-code Matlab/Octave compatible
6+ % implementation by setting the optional parameter noBuiltIn to true.
7+ % Note that the built-in functions are much faster than the
8+ % pure source-code implementation of the MD5 algorithm.
9+ % The latter is based on the source code from https://rosettacode.org/wiki/MD5
10+
11+ if size(message,1)~=1 || ndims(message)~=2
12+ message=message(:).';
13+ end
14+
15+ if ~exist('noBuiltIn','var') || ~noBuiltIn
16+ if mr.aux.isOctave()
17+ digest=hash('MD5',char(message)); % Octave-specific function
18+ return;
19+ else
20+ if ~isempty(message) % java function fails for the empty string so we revert to the source-code implementation
21+ digest=md5_java(message); % Matlab Java hack
22+ return;
23+ end
24+ end
25+ end
26+
27+ % Follow the MD5 algorithm from RFC 1321 [1] and Wikipedia [2].
28+ % [1] http://tools.ietf.org/html/rfc1321
29+ % [2] http://en.wikipedia.org/wiki/MD5
30+
31+ % m is the modulus for 32-bit unsigned arithmetic.
32+ m = 2 ^ 32;
33+
34+ % s is the shift table for circshift(). Each shift is negative
35+ % because it is a left shift.
36+ s = [-7, -12, -17, -22
37+ -5, -9, -14, -20
38+ -4, -11, -16, -23
39+ -6, -10, -15, -21];
40+
41+ % t is the sine table. Each sine is a 32-bit integer, unsigned.
42+ t = floor(abs(sin(1:64)) .* m);
43+
44+ % Initialize the hash, as a row vector of 32-bit integers.
45+ digest = [hex2dec('67452301') ...
46+ hex2dec('EFCDAB89') ...
47+ hex2dec('98BADCFE') ...
48+ hex2dec('10325476')];
49+
50+ % If message contains characters, convert them to ASCII values.
51+ message = double(message);
52+ bytelen = numel(message);
53+
54+ % Pad the message by appending a 1, then appending enough 0s to make
55+ % the bit length congruent to 448 mod 512. Because we have bytes, we
56+ % append 128 '10000000', then append enough 0s '00000000's to make
57+ % the byte length congruent to 56 mod 64.
58+ message = [message, 128, zeros(1, mod(55 - bytelen, 64))];
59+
60+ % Convert the message to 32-bit integers, little endian.
61+ % For little endian, first byte is least significant byte.
62+ message = reshape(message, 4, numel(message) / 4);
63+ message = message(1,:) + ... % least significant byte
64+ message(2,:) * 256 + ...
65+ message(3,:) * 65536 + ...
66+ message(4,:) * 16777216; % most significant byte
67+
68+ % Append the bit length as a 64-bit integer, little endian.
69+ bitlen = bytelen * 8;
70+ message = [message, mod(bitlen, m), mod(bitlen / m, m)];
71+
72+ % Process each 512-bit block. Because we have 32-bit integers, each
73+ % block has 16 elements, message(k + (0:15)).
74+ for k = 1:16:numel(message)
75+ % Copy hash.
76+ a = digest(1); b = digest(2); c = digest(3); d = digest(4);
77+
78+ % Do 64 operations.
79+ for i = (1:64)
80+ % Convert b, c, d to row vectors of bits (0s and 1s).
81+ bv = dec2bin(b, 32) - '0';
82+ cv = dec2bin(c, 32) - '0';
83+ dv = dec2bin(d, 32) - '0';
84+
85+ % Find f = mix of b, c, d.
86+ % ki = index in 0:15, to message(k + ki).
87+ % sr = row in 1:4, to s(sr, :).
88+ if i <= 16 % Round 1
89+ f = (bv & cv) | (~bv & dv);
90+ ki = i - 1;
91+ sr = 1;
92+ elseif i <= 32 % Round 2
93+ f = (bv & dv) | (cv & ~dv);
94+ ki = mod(5 * i - 4, 16);
95+ sr = 2;
96+ elseif i <= 48 % Round 3
97+ f = xor(bv, xor(cv, dv));
98+ ki = mod(3 * i + 2, 16);
99+ sr = 3;
100+ else % Round 4
101+ f = xor(cv, bv | ~dv);
102+ ki = mod(7 * i - 7, 16);
103+ sr = 4;
104+ end
105+
106+ % Convert f, from row vector of bits, to 32-bit integer.
107+ f = bin2dec(char(f + '0'));
108+
109+ % Do circular shift of sum.
110+ sc = mod(i - 1, 4) + 1;
111+ sum = mod(a + f + message(k + ki) + t(i), m);
112+ sum = dec2bin(sum, 32);
113+ sum = circshift(sum, [0, s(sr, sc)]);
114+ sum = bin2dec(sum);
115+
116+ % Update a, b, c, d.
117+ temp = d;
118+ d = c;
119+ c = b;
120+ b = mod(b + sum, m);
121+ a = temp;
122+ end %for i
123+
124+ % Add hash of this block to hash of previous blocks.
125+ digest = mod(digest + [a, b, c, d], m);
126+ end %for k
127+
128+ % Convert hash from 32-bit integers, little endian, to bytes.
129+ digest = [digest % least significant byte
130+ digest / 256
131+ digest / 65536
132+ digest / 16777216]; % most significant byte
133+ digest = reshape(mod(floor(digest), 256), 1, numel(digest));
134+
135+ % Convert hash to hexadecimal.
136+ digest = dec2hex(digest);
137+ digest = lower(reshape(transpose(digest), 1, numel(digest)));
138+end %md5
139+
140+function out=md5_java(buf)
141+ import java.security.*;
142+ import java.math.*;
143+ import java.lang.String;
144+
145+ md = MessageDigest.getInstance('MD5');
146+ hash = md.digest(double(buf));
147+ bi = BigInteger(1, hash);
148+
149+ out=char(String.format('%032x', bi));
150+end
src/engine/pulseq/+mr/+aux/strstrip.madded+19−0View file
@@ -0,0 +1,19 @@
1+function out=strstrip(in)
2+%STRSTRIP: clean up the beginning and the end of a character string
3+ if ~mr.aux.isOctave
4+ if ~isempty(in)
5+ out=strip(in);
6+ else
7+ out=in;
8+ end
9+ else
10+ % octave doen't have strip
11+ while ~isempty(in) && (in(1)==' ' || in(1)=="\t" || in(1)=="\r" || in(1)=="\n")
12+ in(1)=[];
13+ end
14+ while ~isempty(in) && (in(end)==' ' || in(end)=="\t" || in(end)=="\r" || in(end)=="\n")
15+ in(end)=[];
16+ end
17+ out=in;
18+ end
19+end
src/engine/pulseq/+mr/+aux/version.madded+19−0View file
@@ -0,0 +1,19 @@
1+function [version_major, version_minor, version_revision, version_combined]=version(type)
2+%version: return the relevant version information. The type of the version
3+% information is defined by the (optional) parameter.
4+% Possible version types:
5+% 'pulseq' : (default) verson of the current Matlab package
6+% 'output' : version of the file written by the seq.write() function
7+
8+if nargin==0 || strcmp(type,'pulseq')
9+ version_major=1;
10+ version_minor=5;
11+ version_revision=2;
12+elseif strcmp(type,'output')
13+ version_major=1;
14+ version_minor=5;
15+ version_revision=1;
16+else
17+ error('Unsupported version request, type=%s',type);
18+end
19+version_combined=1000000*version_major+1000*version_minor+version_revision;
src/engine/pulseq/+mr/@EventLibrary/EventLibrary.madded+237−0View file
@@ -0,0 +1,237 @@
1+classdef EventLibrary < handle
2+ % EventLibrary Maintain a list of events.
3+ %
4+ % The class is used by the Sequence class to store events of an MRI
5+ % sequence defined using the Pulseq file format.
6+ % See http://pulseq.github.io/
7+ %
8+ % This class is designed for *performance* so some functions can be
9+ % compiled to mex files using MATLAB's code generation. A parallel list
10+ % of IDs, data, and meta information is maintained to maximise search
11+ % speed.
12+ %
13+ % Sequence Properties:
14+ % keys - A list of event IDs
15+ % data - A struct array with field 'array' to store data of varying
16+ % lengths, remaining compatible with codegen.
17+ % lengths - Corresponding lengths of the data arrays
18+ % type - Type to distinguish events in the same class (e.g.
19+ % trapezoids and arbitrary gradients)
20+ %
21+ % Sequence Methods:
22+ % find - Find an event in the library
23+ % insert - Add a new event to the library
24+ %
25+ % See also mr.Sequence
26+ %
27+ % Kelvin Layton <kelvin.layton@uniklinik-freiburg.de>
28+ % Stefan Kroboth <stefan.kroboth@uniklinik-freiburg.de>
29+ % Maxim Zaitsev <maxim.zaitsev@uniklinik-freiburg.de>
30+
31+ properties
32+ keys;
33+ data;
34+ lengths;
35+ type;
36+ keymap;
37+ lookup_key;
38+ next_free_id;
39+ %id_hit_count;
40+ end
41+
42+ methods
43+ function obj = EventLibrary()
44+ obj.keys = zeros(1,0);
45+ obj.data = struct('array',{});
46+ obj.lengths = zeros(1,0);
47+ obj.type = char(zeros(1,0));
48+ try
49+ obj.keymap = configureDictionary("string","double"); % dictionary("",[]); % use newer Matlab dictionary class if it is available -- this makes Pulseq ~40% faster
50+ obj.lookup_key = @(key,fallback) obj.keymap.lookup(key,'FallbackValue',fallback);
51+ catch
52+ %obj.keymap = containers.Map('KeyType', 'char', 'ValueType', 'double'); % fallback to containers.Map %,'UniformValues',true
53+ %if mr.aux.isOctave
54+ % obj.lookup_key = @(key,fallback) mr.aux.containers_map_lookup_ik(obj.keymap,key,fallback);
55+ %else
56+ % obj.lookup_key = @(key,fallback) mr.aux.containers_map_lookup_ex(obj.keymap,key,fallback);
57+ %end
58+ if mr.aux.isOctave
59+ obj.keymap = mr.aux.BalancedBST(256);
60+ obj.lookup_key = @(key,fallback) obj.keymap.lookup(key,fallback);
61+ else
62+ obj.keymap = containers.Map('KeyType', 'char', 'ValueType', 'double'); % fallback to containers.Map %,'UniformValues',true
63+ obj.lookup_key = @(key,fallback) mr.aux.containers_map_lookup_ex(obj.keymap,key,fallback);
64+ end
65+ end
66+ obj.next_free_id = 1;
67+ %obj.id_hit_count=[];
68+ end
69+
70+ function [id, found] = find(obj, data)
71+ %find Lookup a data structure in the given library.
72+ % [id,found]=find(lib,data) Return the index of the data in
73+ % the library. If the data does not exist in the library then
74+ % the index for the next new entry is returned.
75+ %
76+ % The data is a 1xN array with event-specific data.
77+ %
78+ % See also insert mr.Sequence.addBlock
79+
80+ % use map index for faster searches
81+ % matlab is extremely limited with regard to advanced containers
82+ % we therefore are forced to use hashed map and convert data to a
83+ % string
84+ data_string = sprintf('%.6g ', data); % precision can be discussed
85+ % try-catch code further below performs ~20% faster for MPRAGE and just a bit slower for TSE
86+ % if obj.keymap.isKey(data_string(1:end-1))
87+ % id = obj.keymap(data_string(1:end-1));
88+ % found = true;
89+ % else
90+ % id = obj.next_free_id;
91+ % found = false;
92+ % end
93+ % containers.Map does not have a proper find/lookup function so we use direct
94+ % access and catch the possible error
95+ id = obj.lookup_key(data_string(1:end-1),0);
96+ found = (id~=0);
97+ if ~found
98+ id = obj.next_free_id;
99+ end
100+ end
101+
102+ function [id, found] = find_or_insert(obj, data, type)
103+ %find Lookup a data structure in the given library.
104+ % [id,found]=find_or_insert(lib,data) Return the index of the data in
105+ % the library. If the data does not exist in the library it
106+ % is inserted right away
107+ %
108+ % The data is a 1xN array with event-specific data.
109+ %
110+ % See also insert mr.Sequence.addBlock
111+
112+ % use map index for faster searches
113+ % matlab is extremely limited with regard to advanced contasiners
114+ % we therefore are forced to use hashed map and convert data to a
115+ % string
116+ data_string = sprintf('%.6g ', data); % precision can be discussed
117+ % try-catch code further below performs ~20% faster for MPRAGE and just a bit slower for TSE
118+ % if obj.keymap.isKey(data_string(1:end-1))
119+ % id = obj.keymap(data_string(1:end-1));
120+ % found = true;
121+ % %obj.id_hit_count(id)=obj.id_hit_count(id)+1;
122+ % else
123+ % id = obj.next_free_id;
124+ % found = false;
125+ % % insert
126+ % obj.keys(id) = id;
127+ % obj.data(id).array = data;
128+ % obj.lengths(id) = length(data);
129+ % if nargin>2
130+ % obj.type(id) = type;
131+ % end
132+ % obj.keymap(data_string(1:end-1)) = id;
133+ % %obj.id_hit_count(id)=0;
134+ % obj.next_free_id=id+1; % update next_free_id
135+ % end
136+ % containers.Map does not have a proper find function so we use direct
137+ % access and catch the possible error
138+ id = obj.lookup_key(data_string(1:end-1),0);
139+ found = (id~=0);
140+ if ~found
141+ id = obj.next_free_id;
142+ % insert
143+ obj.keys(id) = id;
144+ obj.data(id).array = data;
145+ obj.lengths(id) = length(data);
146+ if nargin>2
147+ obj.type(id) = type;
148+ end
149+ obj.keymap(data_string(1:end-1)) = id;
150+ %obj.id_hit_count(id)=0;
151+ obj.next_free_id=id+1; % update next_free_id
152+ end
153+ end
154+
155+ function id=insert(obj, id, data, type)
156+ %insert Add event to library
157+ %
158+ % See also find
159+
160+ if id==0 % get the next free ID
161+ id = obj.next_free_id;
162+ end
163+
164+ obj.keys(id) = id;
165+ obj.data(id).array = data;
166+ obj.lengths(id) = length(data);
167+ if nargin>3
168+ obj.type(id) = type;
169+ end
170+
171+ % use map index for faster searches
172+ % matlab is extremely limited with regard to advanced containers
173+ % we therefore are forced to use hashed map and convert data to a
174+ % string
175+ data_string=sprintf('%.6g ', data);
176+ obj.keymap(data_string(1:end-1)) = id;
177+ %obj.id_hit_count(id)=0;
178+ if id>=obj.next_free_id
179+ obj.next_free_id=id+1; % update next_free_id
180+ end
181+ end
182+
183+ function update(obj, id, old_data, new_data, type)
184+ if length(obj.keys)>=id
185+ data_string=sprintf('%.6g ', old_data); % see EventLibrary.insert()
186+ % % this code eliminates the warning but is twice as slow as
187+ % % a simpe 'remove'
188+ % if obj.keymap.isKey(data_string(1:end-1))
189+ % obj.keymap.remove(data_string(1:end-1));
190+ % else
191+ % % see if the data have been updated already
192+ % data_string_new=sprintf('%.6g ', new_data); % see EventLibrary.insert()
193+ % if ~obj.keymap.isKey(data_string_new(1:end-1))
194+ % warning('The old key (old_data) could not be found and the new key (new_data) does not exist either, looks like there is some inconsistency going');
195+ % end
196+ % end
197+ obj.keymap.remove(data_string(1:end-1));
198+ end
199+ if nargin>4
200+ insert(obj, id, new_data, type);
201+ else
202+ insert(obj, id, new_data);
203+ end
204+ end
205+
206+ function update_data(obj, id, old_data, new_data, type)
207+ %[id, found] = find(obj, old_data);
208+ %if found
209+ if nargin>4
210+ update(obj, id, old_data, new_data, type);
211+ else
212+ update(obj, id, old_data, new_data)
213+ end
214+ %else
215+ % if nargin>3
216+ % insert(obj, id, new_data, type);
217+ % else
218+ % insert(obj, id, new_data)
219+ % end
220+ %end
221+ end
222+
223+ function out = get(obj, id)
224+ %get Get element from library by key
225+ %
226+ % See also find
227+ out = struct;
228+ out.key = obj.keys(id);
229+ out.data = obj.data(id).array;
230+ out.length = obj.lengths(id);
231+ out.type = obj.type(id);
232+ end
233+
234+ end
235+
236+end
237+
src/engine/pulseq/+mr/@EventLibrary/find_mat.madded+29−0View file
@@ -0,0 +1,29 @@
1+function [id, found] = find_mat(keys,data,lengths,newData)
2+ %find_mat Lookup a data structure in the given library.
3+ % idx=find_mat(keys,data,lengths,newDat) Return the index of the
4+ % newDat in the library. If the data doesn't exist in the library
5+ % then the index for the next new entry is returned.
6+ %
7+ % This function is compatible with the MATLAB Coder to generate a mex
8+ % file for faster execution.
9+ %
10+ % See also EventLibrary
11+
12+ found=false;
13+ id=0;
14+
15+ for i=1:length(data)
16+ if (lengths(i)==length(newData) && norm(data(i).array-newData)<1e-6)
17+ id=keys(i);
18+ found=true;
19+ break;
20+ end
21+ end
22+
23+ if isempty(keys)
24+ id=1;
25+ elseif ~found
26+ id=max(keys)+1;
27+ end
28+
29+end
src/engine/pulseq/+mr/@Sequence/Sequence.madded+2927−0View file
This diff is 2,932 lines long and is not shown.
src/engine/pulseq/+mr/@Sequence/autoLabel.madded+618−0View file
@@ -0,0 +1,618 @@
1+function [labels, aux] = autoLabel(seq, varargin)
2+% Automatically find values of the entire sequence or its part.
3+%
4+% autoLabel(seqObj) automatically detects the label evolution from the
5+% k-space analysis and applies it to the sequence. Return value of the
6+% function is the structure 'labels' with fields named after the labels
7+% used in the sequence containing the evolution of the labes for alls
8+% ADCs. This structure is similar to that returned by evalLabels().
9+%
10+% autoLabel(...,'blockRange',[first last]) Evaluate label
11+% values starting from the first specified block to the last
12+% one.
13+%
14+% autoLabel(...,'reflect',[1,2]) Reflect k-space trajectories along
15+% directions 1 and 2 (any number of directions between 0 and 3 can be
16+% specified) prior to generating label ranges. Affects both Fourier
17+% encoding dimensions (redout, phase and partition encoding) and the
18+% slice ordering. If used in combination with 'reorder', it is performed
19+% first.
20+%
21+% autoLabel(...,'reorder',[2,1]) Reorder axes of k-space trajectories
22+% (in this example by swapping directions 1 and 2 prior to generating
23+% label ranges (reordering vector can contain 2 or 3 entries). If used in
24+% combination with 'reflect', it is performed last.
25+%
26+% autoLabel(...,'mirrorFourier',true) Mirrors all Fourier encoding
27+% directions, e.g. if ifft() is used instead of fft() by the image
28+% reconstruction algorithm. It essentially reverts all Fourier encoding
29+% dimensions (readout, phase, partition encoding) but does not affect
30+% slice ordering. Can be arbitrarily combined with 'reflect'.
31+%
32+% autoLabel(...,'useLabels',labels_struct) Skip label evolution
33+% detection and only apply externally-calculated labels to the sequence.
34+%
35+% autoLabel(...,'useAux',aux_struct) use externally-calculated aux struct
36+% to apply definitions the sequence. Une combination with 'useLabels'.
37+%
38+% autoLabel(...,'skipApply',true) Skip applying label evolution to the
39+% sequence and onoly return the labaels struct.
40+%
41+% From the experience, Siemens scanners need 'mirrorFourier' if the "XYZ
42+% in TRA" coordinate mapping (default) mode is applied, which transforms
43+% the sequence by the following rotation matrix [0 -1 0; -1 0 0; 0 0 -1]
44+% prior to passing it to IDEA; On Siemens 'sortSlices'='descending' is
45+% optional, otherwise the interpreter will change the slice indexes.
46+%
47+% see evalLabels()
48+
49+validSliceSorting={'acquisition','ascending','descending'};
50+persistent parser
51+if isempty(parser)
52+ parser = inputParser;
53+ parser.FunctionName = 'autoLabel';
54+ parser.addParamValue('blockRange',[1 inf],@(x)(isnumeric(x) && length(x)==2));
55+ parser.addParamValue('useLabels',struct([]),@(x)(isempty(x) || isstruct(x)));
56+ parser.addParamValue('useAux',struct([]),@(x)(isempty(x) || isstruct(x)));
57+ parser.addParamValue('skipApply',false,@(x)(islogical(x) && isscalar(x)));
58+ parser.addParamValue('mirrorFourier', false, @(x)(islogical(x) && isscalar(x)));
59+ parser.addParamValue('reflect', [], @(x)(isnumeric(x) && numel(x)<4));
60+ parser.addParamValue('reorder', [], @(x)(isnumeric(x) && numel(x)<4));
61+ parser.addParamValue('sortSlices','acquisition',@(x) any(validatestring(x,validSliceSorting)));
62+ parser.addParamValue('noPlots', false, @(x)(islogical(x) && isscalar(x)));
63+end
64+parse(parser,varargin{:});
65+opt = parser.Results;
66+
67+if ~isempty(opt.useLabels) && (~isempty(opt.reflect) || ~isempty(opt.reorder))
68+ error('Optional parameters ''reflect'' or ''reorder'' only effective for the detection part and cannot be used together with ''useLabels''');
69+end
70+
71+if ~isempty(opt.reflect) && numel(opt.reflect)~=numel(unique(opt.reflect))
72+ error('All indices in ''reflect'' must be unique');
73+end
74+
75+if ~isempty(opt.reorder)
76+ if numel(opt.reorder)~=numel(unique(opt.reorder))
77+ error('All indices in ''reorder'' must be unique');
78+ end
79+ if numel(opt.reorder)~=3 && min(opt.reorder)==1 && max(opt.reorder)==2
80+ error('If ''reorder'' contains two indices they must be [1 2] or [2 1]');
81+ end
82+end
83+
84+if ~isfinite(opt.blockRange(2))
85+ opt.blockRange(2)=length(seq.blockEvents);
86+end
87+
88+aux = struct();
89+
90+%% index ADCs, part 1
91+blockStartTimes=cumsum([0 seq.blockDurations]);
92+blockStartTimes=blockStartTimes(opt.blockRange(1):opt.blockRange(2));
93+adcLengths=[];
94+b_adc=[];
95+for iB=opt.blockRange(1):opt.blockRange(2)
96+ raw_block = seq.getRawBlockContentIDs(iB); % this is much faster than seq.getBlock(iB)
97+ if ~isempty(raw_block.adc)
98+ libData = seq.adcLibrary.data(raw_block.adc).array;
99+ % from getBlock(): adc = cell2struct(num2cell(libData(1:end-1)), {'numSamples', 'dwell', 'delay', ...
100+ adcLengths(end+1)=libData(1); % libData(1) is adc.numSamples
101+ b_adc(end+1)=iB-opt.blockRange(1)+1;
102+ end
103+ %block = seq.getBlock(iB);
104+ %if ~isempty(block.adc)
105+ % adcLengths(end+1)=block.adc.numSamples;
106+ % b_adc(end+1)=iB-opt.blockRange(1)+1;
107+ %end
108+end
109+adcStartCounts=cumsum([1, adcLengths(1:end-1)]);
110+% b_adc=zeros(size(t_adcStarts));
111+% for i=1:numel(t_adcStarts)
112+% b_adc(i)=find(blockStartTimes<t_adcStarts(i),1,'last');
113+% end
114+
115+if isempty(opt.useLabels)
116+
117+ %% preparing calculations
118+ [ktraj_adc, t_adc, ~, ~, t_excitation, ~, slicepos, t_slicepos, gw_pp] = seq.calculateKspacePP('blockRange',opt.blockRange);
119+
120+ t_adcStarts=t_adc(adcStartCounts);
121+ firstNonNoiseAdc=find(t_adcStarts>t_excitation(1),1,'first');
122+ firstNonNoiseAdcSampe=find(t_adc>t_excitation(1),1,'first');
123+ nReadouts = numel(t_adcStarts)-firstNonNoiseAdc+1;
124+
125+ sliceGrads=zeros(size(slicepos));
126+ for i=1:3
127+ sliceGrads(i,:)=ppval(gw_pp{i},t_slicepos);
128+ end
129+
130+ if opt.mirrorFourier
131+ ktraj_adc=-ktraj_adc;
132+ end
133+ if ~isempty(opt.reflect)
134+ ktraj_adc(opt.reflect,:)=-ktraj_adc(opt.reflect,:);
135+ slicepos(opt.reflect,:)=-slicepos(opt.reflect,:);
136+ sliceGrads(opt.reflect,:)=-sliceGrads(opt.reflect,:);
137+ end
138+ if ~isempty(opt.reorder)
139+ ktraj_adc(1:numel(opt.reorder),:)=ktraj_adc(opt.reorder,:);
140+ slicepos(1:numel(opt.reorder),:)=slicepos(opt.reorder,:);
141+ sliceGrads(1:numel(opt.reorder),:)=sliceGrads(opt.reorder,:);
142+ end
143+
144+ %% slice positions
145+ [~,i]=max(abs(sliceGrads));
146+ mainSliceGradSigns=sign(sliceGrads(sub2ind(size(sliceGrads),i,1:size(sliceGrads,2))));
147+ sliceNormals=normalize(sliceGrads,'norm').*mainSliceGradSigns(ones(1,3),:);
148+ sliceOffsets=dot(slicepos,sliceNormals);
149+ sliceOffsets(~isfinite(sliceOffsets))=0;
150+ [uniqueSlicePositions, I, sliceCountersAcquisitionOrder] = unique(sliceOffsets,'stable');
151+ t_usp = t_slicepos(I);
152+ b_usp = zeros(size(t_usp));
153+ for i=1:numel(t_usp)
154+ b_usp(i) = find(blockStartTimes<t_usp(i),1,'last');
155+ end
156+ [sortedSlicePositions, ~, sliceCountersSortedPositions] = unique(sliceOffsets);
157+
158+ % aux.SliceThickness
159+ % aux.SliceGap
160+ % todo: more checks, e.g. if all slice thickeness and gaps are the same...
161+ GAs1=vecnorm(sliceGrads(:,1));
162+ if GAs1>0
163+ bSlice1=find(blockStartTimes<t_slicepos(1),1,'last');
164+ b=seq.getBlock(bSlice1);
165+ bw=mr.calcRfBandwidth(b.rf);
166+ aux.SliceThickness=bw/GAs1;
167+ if length(sortedSlicePositions)>1
168+ aux.SliceGap=diff(sortedSlicePositions(1:2))-aux.SliceThickness;
169+ end
170+ end
171+ if strcmp(opt.sortSlices,'descending')
172+ sortedSlicePositions=sortedSlicePositions(end:-1:1);
173+ sliceCountersSortedPositions = max(sliceCountersSortedPositions) + 1 - sliceCountersSortedPositions;
174+ end
175+
176+ % useful results:
177+ % uniqueSlicePositions : speaks for itself, sorted in the order of occurence
178+ % sliceCountersAcquisitionOrder : slice cointers in the acquisition order
179+ % b_usp: block indices containg RF pulses corresponding to the first
180+ % occurence of each of the unique slice positions
181+ % sortedSlicePositions : slice positions in the ascending or descending deorder
182+ % sliceCountersSorted : slice counters corresponding to the sorted slice order
183+
184+ %% index ADCs, part 2
185+ if ~strcmp(opt.sortSlices,'acquisition')
186+ uniqueSlicePositions=sortedSlicePositions;
187+ end
188+ sliceCountersAdc=zeros(1,nReadouts);
189+ for i=firstNonNoiseAdc:numel(t_adcStarts)
190+ j=find(t_slicepos<t_adcStarts(i),1,'last');
191+ sliceCountersAdc(i-firstNonNoiseAdc+1)=find(uniqueSlicePositions==sliceOffsets(j),1,'first');
192+ end
193+ % useful results:
194+ % b_adc : vector containing block indices with ADC objects
195+ % adcLengths : numbers of samples of each of the ADC objects
196+ % t_adcStarts : time point of each ADCs first sample
197+ % firstNonNoiseAdc : index of the first ADC after the noise scan (1 means no noise scan)
198+ % firstNonNoiseAdcSample : index of the first ADC sample after the noise scan
199+ % sliceCountersAdc : slice counters per ADC
200+
201+ %% readout analysis
202+ cEchoPos = zeros(1,nReadouts);
203+ tEcho = zeros(1,nReadouts);
204+ kEcho = zeros(3,nReadouts);
205+ gradReadout = zeros(3,nReadouts);
206+ signReadout = zeros(1,nReadouts);
207+ [~,kspaceCenterSample]=min(vecnorm(ktraj_adc(:,firstNonNoiseAdcSampe:end)));
208+ kspaceCenterPoint=ktraj_adc(:,kspaceCenterSample);
209+ isCartesianReadout=true;
210+ for i=1:nReadouts
211+ c1=adcStartCounts(i+firstNonNoiseAdc-1);
212+ c2=c1+adcLengths(i+firstNonNoiseAdc-1)-1;
213+ % find echo pos for each readout
214+ [~,cEchoPos(i)]=min(vecnorm(ktraj_adc(:,c1:c2)-kspaceCenterPoint)); % for echos positioned between samples there can be some jitter... to reduce jitter we compare not to 0 but to the smallest absolute...
215+ kEcho(:,i)=ktraj_adc(:,c1+cEchoPos(i)-1);
216+ t_adcThisEcho=t_adc(c1+cEchoPos(i)-1);
217+ if vecnorm(kspaceCenterPoint)>eps % this is more or less copied from seq.testRepoert()
218+ % the actual echo might be between k-space samples, try to interpolate it
219+ i2check=[];
220+ % check if adc kspace trajectory has elements left and right to index_echo
221+ if cEchoPos(i) > 1
222+ i2check=c1+cEchoPos(i)-2;
223+ end
224+ if c1+cEchoPos(i)-1 < c2
225+ i2check(end+1)=c1+cEchoPos(i);
226+ end
227+ for a=1:numel(i2check)
228+ v_i_to_0=-kEcho(:,i);
229+ v_i_to_t=ktraj_adc(:,i2check(a))-kEcho(:,i);
230+ % project v_i_to_0 to v_o_to_t
231+ p_vit=v_i_to_0'*v_i_to_t/(vecnorm(v_i_to_t)^2);
232+ if p_vit>0
233+ % we have forund a bracket for the echo and the proportionality coefficient is p_vit
234+ t_adcThisEcho=t_adcThisEcho*(1-p_vit) + t_adc(i2check(a))*p_vit;
235+ break;
236+ end
237+ end
238+ end
239+
240+ i_sliseposThisEcho=find(t_slicepos<t_adcThisEcho,1,'last');
241+ tEcho(i)=t_adcThisEcho-t_slicepos(i_sliseposThisEcho);
242+ for j=1:3
243+ gradReadout(j,i) = ppval(gw_pp{j},t_adcThisEcho); % this call is relatively expensive for long sequences, we should go away from it
244+ end
245+ if opt.mirrorFourier
246+ gradReadout(:,i)=-gradReadout(:,i);
247+ end
248+ if ~isempty(opt.reflect)
249+ gradReadout(opt.reflect,i)=-gradReadout(opt.reflect,i);
250+ end
251+ if ~isempty(opt.reorder)
252+ gradReadout(1:numel(opt.reorder),i)=gradReadout(opt.reorder,i);
253+ end
254+ % store the most central readout trajectory as a reference
255+ if c1<=kspaceCenterSample && c2>=kspaceCenterSample
256+ cCentralReadout=i; % counter corresponding to the central readout
257+ kCentralReadout=ktraj_adc(:,c1:c2);
258+ [~,cCentralReadoutCenter]=min(vecnorm(kCentralReadout));
259+ end
260+ % detect readout alternation like for EPI
261+ [~,j]=max(abs(gradReadout(:,i)));
262+ signReadout(i)=sign(gradReadout(j,i));
263+ % detect if the readout is sufficiently the same to see if the trajectory is Cartesian-like
264+ if i>1 && vecnorm(gradReadout(:,i)*signReadout(i)-gradReadout(:,1)*signReadout(1))>1e-4 % why such threshold?
265+ isCartesianReadout=false;
266+ end
267+ % % dictionary of readouts
268+ % kReadoutKey = {ktraj_adc(:,c1:c2) - ktraj_adc(:,c1+cEchoPos(i)-1)};
269+ % if i==1
270+ % readoutDict=dictionary(kReadout,i);
271+ % else
272+ % if ~readoutDict.isKey(kReadout)
273+ % readoutDict(kReadout)=i;
274+ % end
275+ % end
276+ end
277+
278+ % more readout analysis
279+ kCentralReadoutDirection=normalize(gradReadout(:,cCentralReadout),'norm');
280+ kCentralReadoutProjection=dot(kCentralReadout,kCentralReadoutDirection(:,ones(1,size(kCentralReadout,2))));
281+ dkR=median(diff(kCentralReadoutProjection));
282+
283+ % detect non-constant sampling
284+ if any(abs(diff(kCentralReadoutProjection,2)/dkR*numel(kCentralReadoutProjection))>0.1)
285+ % see if we can extraxt trapezoid resampling parameters
286+ bCentralRO=find(blockStartTimes<t_adcStarts(firstNonNoiseAdcSampe+cCentralReadout-1),1,'last');
287+ b=seq.getBlock(bCentralRO);
288+ if ~isempty(opt.reorder) && opt.reorder(1)~=1
289+ warning('EPI readout gridding parameter detection code is incompatible with the reorder option');
290+ else
291+ if isfield(b,'gx') && ~isempty(b.gx) && strcmp(b.gx.type,'trap') && isfield(b,'adc') && ~isempty(b.adc)
292+ aux.TrapezoidGriddingParameters=[b.gx.riseTime, b.gx.flatTime, b.gx.fallTime, b.adc.delay-b.gx.delay b.adc.numSamples*b.adc.dwell];
293+ % TODO: figure out a better way to find TargetGriddedSamples... In Siemens sequences it is often adc.numsamples
294+ % we need kReadout to be able to do it, so we will patch this value below (in the trajectory analysis part)
295+ aux.TargetGriddedSamples=b.adc.numSamples;
296+ end
297+ end
298+ % TODO: figure out a better way to find TargetGriddedSamples... In Siemens sequences it is often adc.numsamples
299+ end
300+
301+ % bipolar gradients: detect both and find the reflection point
302+ if any(diff(signReadout)~=0)
303+ % find an inverted sign readout close to k-space center
304+ adcCenters=adcStartCounts(firstNonNoiseAdc:end)+cEchoPos-1;
305+ isInverted=signReadout==-signReadout(cCentralReadout);
306+ [~,cInvertedReadout]=min(vecnorm(ktraj_adc(:,adcCenters(isInverted))));
307+ % here cInvertedReadout is the counter only amongst the inverted readouts, we need to restore its meaning for the total readouts vector
308+ countInverted=cumsum(isInverted);
309+ cInvertedReadout=find(cInvertedReadout==countInverted,1);
310+ assert(isInverted(cInvertedReadout));
311+ %
312+ c1=adcStartCounts(firstNonNoiseAdc+cInvertedReadout-1);
313+ c2=c1+adcLengths(firstNonNoiseAdc+firstNonNoiseAdc-1)-1;
314+ kInvertedReadoutProjection=dot(ktraj_adc(:,c1:c2),kCentralReadoutDirection(:,ones(1,size(kCentralReadout,2))));
315+ if signReadout(cCentralReadout)==1
316+ aux.kReadout = [kCentralReadoutProjection; kInvertedReadoutProjection]; % 1: positive readout, 2: negative readout
317+ else
318+ aux.kReadout = [kInvertedReadoutProjection; kCentralReadoutProjection]; % 1: positive readout, 2: negative readout
319+ end
320+ if ~opt.noPlots
321+ figure; plot(aux.kReadout(1,:),'.-'); hold on; plot(aux.kReadout(2,:),'.-');plot(aux.kReadout(2,end:-1:1),'x'); title('bipolar readout k-space alignment');
322+ legend('forward','reverse','rev.refl'); % if crosses overlap the line then REV is symmetric
323+ xlabel('sample number (acq. order)');
324+ ylabel('k-space, 1/m');
325+ end
326+ else
327+ aux.kReadout = kCentralReadoutProjection;
328+ end
329+
330+ % useful results:
331+ % isCartesianReadout : is this trajectory sufficiently Cartesian-like
332+ % gradReadout : readout gradients as 3D vectors
333+ % signReadout : sign of the dominant readout component
334+ % kCentralReadout : the most central readout
335+ % cCentralReadoutCenter : center of the above
336+ % cEchoPos : position of the echo within each ADC vector for each readout
337+ % tEcho : echo time for each readout
338+ % kEcho : k-space location of each echo
339+
340+ %% detect navigators
341+ isNavigator=false(1,nReadouts);
342+ isNavigatorCandidate = vecnorm(kEcho-kspaceCenterPoint) < 1e-4; % why this threshold?
343+ orderedReadoutIndicator=diff(kEcho-kspaceCenterPoint,1,2);
344+ i=find(max(abs(orderedReadoutIndicator'))>1e-4);
345+ orderedReadoutIndicator=orderedReadoutIndicator(i,:); % discard empty dimensions
346+ %TODO: finish this detection some day... e.g. if all(isNavigatorCandidate(1:3)) && abs(orderedReadoutIndicator(1)-orderedReadoutIndicator(2))<1e-4 && max(abs(diff(orderedReadoutIndicator(4:x))))<1e-4
347+ if nReadouts>=16 && all(isNavigatorCandidate(1:3)) && abs(orderedReadoutIndicator(1)-orderedReadoutIndicator(2))<1e-4 && ...
348+ max(abs(diff(orderedReadoutIndicator(4:16))))<1e-4
349+ aux.epiWithThreeEchoNavigator=true;
350+ % this is a real hack to close those singular k=0 lines in the middle of readouts
351+ isNavigator = (isNavigatorCandidate + circshift(isNavigatorCandidate,1) + circshift(isNavigatorCandidate,-1))>1.5;
352+ % TODO: set SEG (010 10101...) and AVG (001 00000...)
353+ end
354+
355+ %% Accelerate calculation for Cartesian-like sequences by only considering the echo position
356+ if isCartesianReadout
357+ ktraj_adc = ktraj_adc(:, adcStartCounts(firstNonNoiseAdc:end)+cEchoPos-1);
358+ else
359+ error('autoLabel() only supports sufficiently Cartesian sequences');
360+ end
361+
362+ %% Analyze the trajectory data
363+
364+ dkR=median(diff(kCentralReadoutProjection));
365+
366+ k_extent=max(abs(ktraj_adc-kspaceCenterPoint),[],2);
367+ k_scale=max(k_extent);
368+ k_threshold=abs(dkR/50);
369+
370+ % calculate/update gridding samples
371+ if any(any(abs(diff(aux.kReadout,2,2))>1e-6)) % threshold?
372+ % calculate samples based on the largest k-space step, so that we never add samples in between that we didn't measure
373+ aux.TargetGriddedSamples=floor((max(aux.kReadout(1,:))-min(aux.kReadout(1,:)))/max(abs(diff(aux.kReadout(1,:)))))+1;
374+ end
375+
376+ % detect unused dimensions and delete them
377+ if any(k_extent<k_threshold)
378+ ktraj_adc(k_extent<k_threshold,:)=[]; % delete rows
379+ kspaceCenterPoint(k_extent<k_threshold)=[];
380+ k_extent(k_extent<k_threshold)=[];
381+ end
382+
383+ % detect dK, k-space reordering and repetitions (or slices, etc)
384+ kt_sorted=sort(ktraj_adc-kspaceCenterPoint,2);
385+ dk_all=kt_sorted(:,2:end)-kt_sorted(:,1:(end-1));
386+ dk_all(dk_all<k_threshold)=NaN;
387+ dk_min=min(dk_all,[],2);
388+ dk_max=max(dk_all,[],2);
389+ dk_all(dk_all-dk_min(:,ones(1,size(dk_all,2)))>k_threshold)=NaN;
390+ dk_all_cnt=sum(isfinite(dk_all),2);
391+ dk_all(~isfinite(dk_all))=0;
392+ dk=sum(dk_all,2)./dk_all_cnt;
393+ dk(~isfinite(dk))=0; % dk vector ready
394+ [~,k0_ind]=min(sum(ktraj_adc.^2,1)); % k-space center
395+ kindex=round((ktraj_adc-ktraj_adc(:,k0_ind*ones(1,size(ktraj_adc,2))))./dk(:,ones(1,size(ktraj_adc,2))));
396+ kindex(~isfinite(kindex))=0;
397+ kindex_min=min(kindex,[],2);
398+ kindex_mat=kindex-kindex_min(:,ones(1,size(ktraj_adc,2)))+1;
399+ kindex_end=max(kindex_mat,[],2);
400+ sampler=zeros(length(uniqueSlicePositions),prod(kindex_end));
401+ repeat=zeros(1,size(ktraj_adc,2));
402+ for i=1:size(kindex_mat,2)
403+ if (~isNavigator(i))
404+ % ignore navigators
405+ switch size(kindex_mat,1)
406+ case 3
407+ ind=sub2ind(kindex_end,kindex_mat(1,i),kindex_mat(2,i),kindex_mat(3,i));
408+ case 2
409+ ind=sub2ind(kindex_end,kindex_mat(1,i),kindex_mat(2,i));
410+ otherwise
411+ ind=kindex_mat(1,i); %sub2ind(kindex_end,kindex_mat(1,i));
412+ end
413+ r=sampler(sliceCountersAdc(i),ind);
414+ repeat(i)=r;
415+ sampler(sliceCountersAdc(i),ind)=r+1;
416+ end
417+ end
418+ % if (max(repeat(:))>0)
419+ % kindex=[kindex;(repeat+1)];
420+ % kindex_mat=[kindex_mat;(repeat+1)];
421+ % kindex_end=max(kindex_mat,[],2);
422+ % end
423+ %figure; plot(kindex(1,:),kindex(2,:),'.-');
424+ %figure; plot(kindex_mat(1,:),'.-');
425+
426+ % see if some repetitions are actully echoes/contrasts
427+ nRep=max(repeat)+1;
428+ skipECO=false;
429+ if nRep>1
430+ TE=zeros(1, nRep);
431+ for i=1:nRep
432+ try
433+ if isvector(kindex)
434+ TE(i)=tEcho(kindex==0 & sliceCountersAdc==1 & repeat==(i-1)); % we assume all slices have the same TEs and only check slice 1
435+ else
436+ TE(i)=tEcho(all(kindex==0) & sliceCountersAdc==1 & repeat==(i-1)); % we assume all slices have the same TEs and only check slice 1
437+ end
438+ catch
439+ warning('unclear sequence structure, skipping TE & ECO counter detection');
440+ skipECO=true;
441+ break;
442+ end
443+ end
444+ if ~skipECO
445+ [TE_sorted, TE_order]=sort(TE);
446+ TE_cluster=cumsum([1, diff(TE_sorted)>10e-6]);
447+ unique_TE=zeros(1,max(TE_cluster));
448+ for i=1:length(unique_TE)
449+ unique_TE(i)=mean(TE_sorted(TE_cluster==i));
450+ TE(TE_order(TE_cluster==i))=unique_TE(i);
451+ end
452+ aux.TE=unique_TE; % maybe we should fill this also for single-TE sequences?
453+ echo=zeros(1,size(ktraj_adc,2));
454+ echo_rep=zeros(1,nRep);
455+ for i=1:nRep
456+ cecho=find(abs(unique_TE-TE(i))<=1e-6);
457+ echo(repeat==(i-1))=cecho;
458+ repeat(repeat==(i-1))=sum(echo_rep==cecho);
459+ echo_rep(i)=cecho;
460+ end
461+ end
462+ end
463+
464+ %% create labels struct
465+ labels=struct();
466+ % NOISE
467+ % SLC
468+ % REV
469+ % LIN,
470+ % PAR
471+ % ECO
472+ % REP
473+
474+ % TODO: SEG,ECO,REF,IMA,AVG,SET
475+
476+ nADCs=nReadouts+firstNonNoiseAdc-1;
477+ if firstNonNoiseAdc>1
478+ labels.NOISE=zeros(1,nADCs);
479+ labels.NOISE(1:(firstNonNoiseAdc-1))=1;
480+ end
481+ if any(sliceCountersAdc~=1)
482+ if firstNonNoiseAdc>1
483+ labels.SLC=zeros(1,nADCs);
484+ labels.SLC(firstNonNoiseAdc:end)=sliceCountersAdc-1;
485+ else
486+ labels.SLC=sliceCountersAdc-1;
487+ end
488+ end
489+ if any(signReadout<0)
490+ if firstNonNoiseAdc>1
491+ labels.REV=zeros(1,nADCs);
492+ labels.REV(firstNonNoiseAdc:end)=(signReadout<0);
493+ else
494+ labels.REV=(signReadout<0);
495+ end
496+ end
497+ if any(kindex_mat(1,:)~=1)
498+ if firstNonNoiseAdc>1
499+ labels.LIN=zeros(1,nADCs);
500+ labels.LIN(firstNonNoiseAdc:end)=kindex_mat(1,:)-1;
501+ else
502+ labels.LIN=kindex_mat(1,:)-1;
503+ end
504+ end
505+ if size(kindex_mat,1)>1 && any(kindex_mat(2,:)~=1)
506+ if firstNonNoiseAdc>1
507+ labels.PAR=zeros(1,nADCs);
508+ labels.PAR(firstNonNoiseAdc:end)=kindex_mat(2,:)-1;
509+ else
510+ labels.PAR=kindex_mat(2,:)-1;
511+ end
512+ end
513+ if exist('echo','var') && max(echo(:))>1
514+ if firstNonNoiseAdc>1
515+ labels.ECO=zeros(1,nADCs);
516+ labels.ECO(firstNonNoiseAdc:end)=echo(:).'-1;
517+ else
518+ labels.ECO=echo(:).'-1;
519+ end
520+ end
521+ if max(repeat(:))>0
522+ if firstNonNoiseAdc>1
523+ labels.REP=zeros(1,nADCs);
524+ labels.REP(firstNonNoiseAdc:end)=repeat(:).';
525+ else
526+ labels.REP=repeat(:).';
527+ end
528+ end
529+ if any(isNavigator)
530+ labels.NAV=isNavigator;
531+ end
532+
533+ if isfield(labels,'LIN'), aux.kSpaceCenterLine = labels.LIN(cCentralReadout); end
534+ if isfield(labels,'PAR'), aux.kSpaceCenterPartition = labels.PAR(cCentralReadout); end
535+ aux.kSpaceCenterSample = cCentralReadoutCenter-1;
536+ if length(uniqueSlicePositions)>1
537+ aux.SlicePositions = uniqueSlicePositions;
538+ end
539+ % aux.kSpacePhaseEncodingLines
540+ % aux.PhaseResolution
541+ % aux.ReadoutOversamplingFactor
542+ % aux.SliceGap
543+ % aux.SlicePositions
544+ % aux.SliceThickness
545+ % aux.TargetGriddedSamples
546+ % aux.TrapezoidGriddingParameterss
547+ % aux.AccelerationFactorPE
548+ % aux.AccelerationFactor3D
549+ % aux.FirstFourierLine
550+ % aux.FirstRefLine
551+ % aux.FirstFourier3D
552+ % aux.FirstRef3D
553+
554+else
555+ labels=opt.useLabels;
556+end
557+
558+if ~isempty(opt.useAux)
559+ aux=opt.useAux;
560+end
561+
562+%% apply labels
563+
564+if ~opt.skipApply
565+ lblNames=fieldnames(labels);
566+ warnBcp = warning ('off','mr:fixmePreviousRotationExtension');
567+ for i=1:numel(b_adc)
568+ blkLabels={};
569+
570+ for j=1:length(lblNames)
571+ if i==1 && labels.(lblNames{j})(1)~=0 || i>1 && labels.(lblNames{j})(i)~=labels.(lblNames{j})(i-1)
572+ blkLabels{end+1}=mr.makeLabel('SET',lblNames{j},labels.(lblNames{j})(i));
573+ end
574+ end
575+
576+ if ~isempty(blkLabels)
577+ iB=b_adc(i);
578+ b=seq.getBlock(iB);
579+ e=mr.block2events(b);
580+ seq.setBlock(iB,e{:},blkLabels{:});
581+ end
582+ end
583+ warning(warnBcp);
584+
585+ % convert usable fields of aux to sequence definitions
586+ fieldsToExport={'kSpaceCenterLine','kSpaceCenterPartition','kSpaceCenterSample','kSpacePhaseEncodingLines',...
587+ 'PhaseResolution','ReadoutOversamplingFactor','SliceGap','SlicePositions','SliceThickness',...
588+ 'TargetGriddedSamples','TrapezoidGriddingParameters','AccelerationFactorPE','AccelerationFactor3D',...
589+ 'FirstFourierLine','FirstRefLine','FirstFourier3D','FirstRef3D'};
590+ for i=1:length(fieldsToExport)
591+ if isfield(aux,fieldsToExport{i})
592+ prevDef=seq.getDefinition(fieldsToExport{i});
593+ if ~isempty(prevDef)
594+ warning('Overwriting existing sequence definition %s = %s', fieldsToExport{i}, num2str(prevDef));
595+ end
596+ seq.setDefinition(fieldsToExport{i},aux.(fieldsToExport{i}));
597+ end
598+ end
599+end
600+
601+%% test/plot label settings
602+if opt.noPlots
603+ return;
604+end
605+
606+if ~opt.skipApply
607+ lbls=seq.evalLabels('evolution','adc');
608+else
609+ lbls=labels;
610+end
611+lbl_names=fieldnames(lbls);
612+figure; hold on;
613+for n=1:length(lbl_names)
614+ plot(lbls.(lbl_names{n}));
615+end
616+legend(lbl_names(:));
617+title('evolution of labels/counters/flags');
618+xlabel('adc number');
src/engine/pulseq/+mr/@Sequence/calcMomentsBtensor.madded+373−0View file
@@ -0,0 +1,373 @@
1+function [B, m1, m2, m3] = calcMomentsBtensor(obj, varargin)
2+%give input arguments of calcB,calcm1,calcm2,calcm3 with true or false and
3+%Ndummy with 0,1,... as the Number of Dummy scans (scans that should be
4+%skipped for the calculation)
5+
6+%m1/m1 are of the structure array(R,3), where R denotes the number of the
7+%repetition readout (for single shot sequences) with the three orientations
8+%x,y,z respectively
9+%B is of a similar structure with array(R,3,3)
10+%CONSIDER: This so far only works for one slice or rather one measurement PER TR
11+defaultB = true;
12+defaultM = false;
13+defaultD = 0;
14+%temporalTolerance=1e-9; % 1ns
15+
16+persistent p;
17+if isempty(p)
18+ p=mr.aux.InputParserCompat;
19+ addRequired(p, 'obj');
20+
21+ % addParameter(p, 'calcB', defaultB);
22+ % addParameter(p, 'calcm1', defaultM);
23+ % addParameter(p, 'calcm2', defaultM);
24+ % addParameter(p, 'calcm3', defaultM);
25+ % addParameter(p, 'Ndummy', defaultD);
26+
27+ % Add optional arguments with default values
28+ addOptional(p, 'calcB', defaultB);
29+ addOptional(p, 'calcm1', defaultM);
30+ addOptional(p, 'calcm2', defaultM);
31+ addOptional(p, 'calcm3', defaultM);
32+ addOptional(p, 'Ndummy', defaultD);
33+end
34+
35+% Parse the input
36+parse(p, obj, varargin{:});
37+
38+% Assign the results to variables
39+calcB = p.Results.calcB;
40+calcm1 = p.Results.calcm1;
41+calcm2 = p.Results.calcm2;
42+calcm3 = p.Results.calcm3;
43+Ndummy = p.Results.Ndummy;
44+
45+%%
46+% calcB = true;
47+% calcm1 = true;
48+% calcm2 = true;
49+% calcm3 = true;
50+% Ndummy = 0;
51+%% first get gradient shape and timing properties out of the sequence
52+warning('OFF', 'mr:restoreShape')
53+[~, ~, ~, ~, t_excitation, t_refocusing, ~, ~, gw_pp] = obj.calculateKspacePP();
54+
55+R = length(t_excitation); %repetions defined over excitations
56+
57+gx_pp_l = gw_pp{1};
58+gy_pp_l = gw_pp{2};
59+gz_pp_l = gw_pp{3};
60+%% splitting pp-functions into equal parts
61+tSeq = [];
62+t_echo = [];
63+for i=1:R
64+ t_echo(end+1)=(2*t_refocusing(i) - t_excitation(i)); % TODO: fixme for double-refocused sequences
65+ tSeq = [tSeq [t_excitation(i), t_refocusing(i), t_echo(i)]];
66+end
67+
68+t1 = gx_pp_l.breaks;
69+t2 = gy_pp_l.breaks;
70+t3 = gz_pp_l.breaks;
71+tn = unique([t1,t2,t3,tSeq]);
72+
73+% % debuging / visualization
74+% tnew = linspace(0,tn(end),10000);
75+% figure; hold on;
76+% plot(tnew,ppval(gx_pp_l,tnew));
77+% plot(tnew,ppval(gy_pp_l,tnew));
78+% plot(tnew,ppval(gz_pp_l,tnew));
79+
80+gx_pp_coefs=fillPpCoefs(gx_pp_l,tn);
81+gx_pp_l = mkpp(tn,gx_pp_coefs);
82+gy_pp_coefs=fillPpCoefs(gy_pp_l,tn);
83+gy_pp_l = mkpp(tn,gy_pp_coefs);
84+gz_pp_coefs=fillPpCoefs(gz_pp_l,tn);
85+gz_pp_l = mkpp(tn,gz_pp_coefs);
86+
87+%% splitting pp at multiples of TR
88+n = (1+Ndummy);
89+gz_pp = cell(1,R);
90+gy_pp = cell(1,R);
91+gx_pp = cell(1,R);
92+for i = 1:size(gz_pp_l.breaks,2)
93+ if n == R
94+ break
95+ elseif abs(gz_pp_l.breaks(i) - t_excitation(n+1)) == 0 %skipping first excitation
96+ gz_pp{n-Ndummy} = fnbrk(gz_pp_l,[t_excitation(n) t_excitation(n+1)]);
97+ gy_pp{n-Ndummy} = fnbrk(gy_pp_l,[t_excitation(n) t_excitation(n+1)]);
98+ gx_pp{n-Ndummy} = fnbrk(gx_pp_l,[t_excitation(n) t_excitation(n+1)]);
99+ n = n +1;
100+ end
101+end
102+%add the last block as well
103+gx_pp{R}= fnbrk(gx_pp_l,[t_excitation(end) gx_pp_l.breaks(end)]);
104+gy_pp{R}= fnbrk(gy_pp_l,[t_excitation(end) gy_pp_l.breaks(end)]);
105+gz_pp{R}= fnbrk(gz_pp_l,[t_excitation(end) gz_pp_l.breaks(end)]);
106+
107+
108+%% considering effects of the rf pulse -> effective gradients
109+%i.e gradient becomes negative after
110+%the refocusing pulse
111+%as gradients are split at excitation, nothing necessary here
112+for j=1:R
113+ for i = 1:size(gx_pp{j}.coefs,1)
114+ if gx_pp{j}.breaks(:,i) == t_refocusing(j+Ndummy)
115+ gx_pp{j}.coefs(i:end,:) = -gx_pp{j}.coefs(i:end,:);
116+ gy_pp{j}.coefs(i:end,:) = -gy_pp{j}.coefs(i:end,:);
117+ gz_pp{j}.coefs(i:end,:) = -gz_pp{j}.coefs(i:end,:);
118+ end
119+ end
120+end
121+%% B-Tensor calculation
122+B = zeros(R,3,3);
123+if calcB
124+
125+ qz_pp = cell(1,R);
126+ qy_pp = cell(1,R);
127+ qx_pp = cell(1,R);
128+
129+ for i = 1:R
130+ qx_pp{i}=compat_fnint(gx_pp{i});
131+ qy_pp{i}=compat_fnint(gy_pp{i});
132+ qz_pp{i}=compat_fnint(gz_pp{i});
133+
134+ qx_pp{i}.coefs = qx_pp{i}.coefs*2*pi;
135+ qy_pp{i}.coefs = qy_pp{i}.coefs*2*pi;
136+ qz_pp{i}.coefs = qz_pp{i}.coefs*2*pi;
137+ end
138+
139+ % tnew = linspace(0,gx_pp_l.breaks(end),1000);
140+ % figure;
141+ % plot(tnew,ppval(qz_pp{2},tnew),'LineWidth',2,'Color','r');
142+ % % hold on;
143+ % % plot(tnew,ppval(qy_pp{1},tnew),'LineWidth',2,'Color','b');
144+ % %plot(tnew,ppval(qz_pp{2},tnew),'LineWidth',2,'Color','y');
145+ % hold off
146+ % title('wave vector')
147+ % legend('kx','ky','kz')
148+ % xlabel('Time [ms]')
149+ % ylabel('Wave Vector [1/m]')
150+ % grid;
151+
152+ for m = 1:R
153+ q_pp{1} = qx_pp{m};
154+ q_pp{2} = qy_pp{m};
155+ q_pp{3} = qz_pp{m};
156+ for i = 1:3
157+ for j = 1:3
158+ coefs=zeros(size(qx_pp{m}.coefs,1),5);
159+ for k=1:size(qx_pp{m}.coefs,1)
160+ coefs(k,:)=conv(q_pp{i}.coefs(k,:),q_pp{j}.coefs(k,:));
161+ end
162+ Bpp_div = mkpp(qx_pp{m}.breaks,coefs);
163+ Bpp = compat_fnint(Bpp_div);
164+ %evaluate b-value at TE
165+ B(m,i,j) = ppval(Bpp,tSeq(3 + 3*(m-1+Ndummy)));
166+ end
167+ end
168+ end
169+end
170+
171+%% calculate first moment m1
172+m1 = zeros(R,3);
173+
174+if calcm1
175+
176+ m1z_pp = cell(1,R);
177+ m1y_pp = cell(1,R);
178+ m1x_pp = cell(1,R);
179+ for i = 1:R
180+ %defining a new piecewise polynomial aquivalent to g(t)=t
181+ t_pp_coefs = zeros(length(gx_pp{i}.breaks)-1,2);
182+ t_pp_coefs(:,1) = 1;
183+ t_pp_coefs(:,2) = gx_pp{i}.breaks(1:(end-1))-gx_pp{i}.breaks(1);
184+
185+ new_cx = zeros(size(gx_pp{i}.coefs,1),size(gx_pp{i}.coefs,2)+1);
186+ new_cy = zeros(size(gy_pp{i}.coefs,1),size(gy_pp{i}.coefs,2)+1);
187+ new_cz = zeros(size(gz_pp{i}.coefs,1),size(gz_pp{i}.coefs,2)+1);
188+
189+ for k=1:size(t_pp_coefs,1)
190+ new_cx(k,:)=conv(gx_pp{i}.coefs(k,:),t_pp_coefs(k,:));
191+ new_cy(k,:)=conv(gy_pp{i}.coefs(k,:),t_pp_coefs(k,:));
192+ new_cz(k,:)=conv(gz_pp{i}.coefs(k,:),t_pp_coefs(k,:));
193+ end
194+
195+ tgx_pp = mkpp(gx_pp{i}.breaks, new_cx);
196+ tgy_pp = mkpp(gy_pp{i}.breaks, new_cy);
197+ tgz_pp = mkpp(gz_pp{i}.breaks, new_cz);
198+
199+ m1x_pp{i}=compat_fnint(tgx_pp);
200+ m1y_pp{i}=compat_fnint(tgy_pp);
201+ m1z_pp{i}=compat_fnint(tgz_pp);
202+ m1x_pp{i}.coefs = m1x_pp{i}.coefs*2*pi;
203+ m1y_pp{i}.coefs = m1y_pp{i}.coefs*2*pi;
204+ m1z_pp{i}.coefs = m1z_pp{i}.coefs*2*pi;
205+ end
206+
207+ %as an integration from 0 to TE, I will evaluate the function at TE to
208+ %gain the first order vector
209+
210+ for m=1:R
211+ m1(m,1) = ppval(m1x_pp{m},tSeq(3 + 3*(m-1+Ndummy)));%-ppval(m1x_pp{m},m1x_pp{m}.breaks(1));
212+ m1(m,2) = ppval(m1y_pp{m},tSeq(3 + 3*(m-1+Ndummy)));%-ppval(m1y_pp{m},m1y_pp{m}.breaks(1));
213+ m1(m,3) = ppval(m1z_pp{m},tSeq(3 + 3*(m-1+Ndummy)));%-ppval(m1z_pp{m},m1z_pp{m}.breaks(1));
214+ end
215+
216+end
217+%% calculate second moment m2
218+m2 = zeros(R,3);
219+
220+if calcm2
221+
222+ m2z_pp = cell(1,R);
223+ m2y_pp = cell(1,R);
224+ m2x_pp = cell(1,R);
225+ for i = 1:R
226+ t2_pp_coefs = zeros(length(gx_pp{i}.breaks)-1,3);
227+ t2_pp_coefs(:,1) = 1;
228+ t2_pp_coefs(:,2) = 2*(gx_pp{i}.breaks(1:(end-1))-gx_pp{i}.breaks(1));
229+ t2_pp_coefs(:,3) = (gx_pp{i}.breaks(1:(end-1))-gx_pp{i}.breaks(1)).^2;
230+
231+ new_cx = zeros(size(gx_pp{i}.coefs,1),size(gx_pp{i}.coefs,2)+2);
232+ new_cy = zeros(size(gy_pp{i}.coefs,1),size(gy_pp{i}.coefs,2)+2);
233+ new_cz = zeros(size(gz_pp{i}.coefs,1),size(gz_pp{i}.coefs,2)+2);
234+
235+ for k=1:size(t2_pp_coefs,1)
236+ new_cx(k,:)=conv(gx_pp{i}.coefs(k,:),t2_pp_coefs(k,:));
237+ new_cy(k,:)=conv(gy_pp{i}.coefs(k,:),t2_pp_coefs(k,:));
238+ new_cz(k,:)=conv(gz_pp{i}.coefs(k,:),t2_pp_coefs(k,:));
239+ end
240+
241+ tgx_pp = mkpp(gx_pp{i}.breaks, new_cx);
242+ tgy_pp = mkpp(gy_pp{i}.breaks, new_cy);
243+ tgz_pp = mkpp(gz_pp{i}.breaks, new_cz);
244+
245+ m2x_pp{i}=compat_fnint(tgx_pp);
246+ m2y_pp{i}=compat_fnint(tgy_pp);
247+ m2z_pp{i}=compat_fnint(tgz_pp);
248+ m2x_pp{i}.coefs = m2x_pp{i}.coefs*2*pi;
249+ m2y_pp{i}.coefs = m2y_pp{i}.coefs*2*pi;
250+ m2z_pp{i}.coefs = m2z_pp{i}.coefs*2*pi;
251+ end
252+
253+ %as an integration from 0 to TE, I will evaluate the function at TE to
254+ %gain the first order vector
255+
256+ for m=1:R
257+ m2(m,1) = ppval(m2x_pp{m},tSeq(3 + 3*(m-1+Ndummy)))-ppval(m2x_pp{m},m2x_pp{m}.breaks(1));
258+ m2(m,2) = ppval(m2y_pp{m},tSeq(3 + 3*(m-1+Ndummy)))-ppval(m2y_pp{m},m2y_pp{m}.breaks(1));
259+ m2(m,3) = ppval(m2z_pp{m},tSeq(3 + 3*(m-1+Ndummy)))-ppval(m2z_pp{m},m2z_pp{m}.breaks(1));
260+ end
261+
262+end
263+
264+
265+%% calc third moment m3:
266+m3 = zeros(R,3);
267+
268+if calcm3
269+
270+ m3z_pp = cell(1,R);
271+ m3y_pp = cell(1,R);
272+ m3x_pp = cell(1,R);
273+ for i = 1:R
274+ t3_pp_coefs = zeros(length(gx_pp{i}.breaks)-1,4);
275+ t3_pp_coefs(:,1) = 1;
276+ t3_pp_coefs(:,2) = 3*(gx_pp{i}.breaks(1:(end-1))-gx_pp{i}.breaks(1));
277+ t3_pp_coefs(:,3) = 3*(gx_pp{i}.breaks(1:(end-1))-gx_pp{i}.breaks(1)).^2;
278+ t3_pp_coefs(:,4) = (gx_pp{i}.breaks(1:(end-1))-gx_pp{i}.breaks(1)).^3;
279+ % t3_pp_breaks = gx_pp{i}.breaks;
280+ % t3_pp = mkpp(t3_pp_breaks,t3_pp_coefs);
281+ % tnew = linspace(0,gx_pp_l.breaks(end),1000);
282+ % figure;
283+ % plot(tnew,ppval(t3_pp,tnew),'LineWidth',2,'Color','r');
284+ % hold on
285+ % plot(tnew,(tnew-gx_pp{i}.breaks(1)).^3,'Color','b');
286+ % hold off
287+
288+ new_cx = zeros(size(gx_pp{i}.coefs,1),size(gx_pp{i}.coefs,2)+3);
289+ new_cy = zeros(size(gy_pp{i}.coefs,1),size(gy_pp{i}.coefs,2)+3);
290+ new_cz = zeros(size(gz_pp{i}.coefs,1),size(gz_pp{i}.coefs,2)+3);
291+
292+ for k=1:size(t3_pp_coefs,1)
293+ new_cx(k,:)=conv(gx_pp{i}.coefs(k,:),t3_pp_coefs(k,:));
294+ new_cy(k,:)=conv(gy_pp{i}.coefs(k,:),t3_pp_coefs(k,:));
295+ new_cz(k,:)=conv(gz_pp{i}.coefs(k,:),t3_pp_coefs(k,:));
296+ end
297+
298+ tgx_pp = mkpp(gx_pp{i}.breaks, new_cx);
299+ tgy_pp = mkpp(gy_pp{i}.breaks, new_cy);
300+ tgz_pp = mkpp(gz_pp{i}.breaks, new_cz);
301+
302+ m3x_pp{i}=compat_fnint(tgx_pp);
303+ m3y_pp{i}=compat_fnint(tgy_pp);
304+ m3z_pp{i}=compat_fnint(tgz_pp);
305+ m3x_pp{i}.coefs = m3x_pp{i}.coefs*2*pi;
306+ m3y_pp{i}.coefs = m3y_pp{i}.coefs*2*pi;
307+ m3z_pp{i}.coefs = m3z_pp{i}.coefs*2*pi;
308+ end
309+
310+ %as an integration from 0 to TE, I will evaluate the function at TE to
311+ %gain the first order vector
312+
313+ for m=1:R
314+ m3(m,1) = ppval(m3x_pp{m},tSeq(3 + 3*(m-1+Ndummy)))-ppval(m3x_pp{m},m3x_pp{m}.breaks(1));
315+ m3(m,2) = ppval(m3y_pp{m},tSeq(3 + 3*(m-1+Ndummy)))-ppval(m3y_pp{m},m3y_pp{m}.breaks(1));
316+ m3(m,3) = ppval(m3z_pp{m},tSeq(3 + 3*(m-1+Ndummy)))-ppval(m3z_pp{m},m3z_pp{m}.breaks(1));
317+ end
318+
319+end
320+end
321+%% functions
322+function pp1_coefs=fillPpCoefs(pp1,xn)
323+ idx1 = slookup(xn(1:end-1),pp1.breaks(1:end-1));
324+ pp1_coefs = zeros(length(xn)-1,pp1.order);
325+ for i=1:size(pp1_coefs,1)
326+ if idx1(i)>0
327+ % simple copy
328+ pp1_coefs(i,:)=pp1.coefs(idx1(i),:);
329+ elseif i > 1
330+ % copy from the left with a reference shift (else leave at 0)
331+ for k=0:(pp1.order-1)
332+ for l=0:k
333+ pp1_coefs(i,end-l) = pp1_coefs(i,end-l) + pp1_coefs(i-1,end-k)*nchoosek(k,l)*(xn(i)-xn(i-1))^(k-l);
334+ end
335+ end
336+ end
337+ end
338+end
339+
340+function idx=slookup(what,where)
341+% finds indices of values given by the sorted vector 'what' in the sorted vector 'where'
342+% for failing searches indices of 0 are returned
343+ idx=zeros(size(what));
344+ wb=1; % where bound
345+ for c=1:length(what)
346+ i=find(what(c)==where(wb:end),1);
347+ if isempty(i), continue; end
348+ idx(c)=wb+i-1;
349+ wb=wb+i;
350+ end
351+end
352+
353+function idx=sintlookup(what,where)
354+% finds indices of intervals to which the sorted vector 'what' belongs in the sorted vector 'where'
355+% for failing searches indices of 0 are returned
356+ idx=zeros(size(what));
357+ wb=1; % where bound
358+ for c=1:length(what)
359+ i=find(what(c)>=where(wb:end));
360+ if isempty(i), continue; end
361+ idx(c)=wb+i(end)-1;
362+ wb=idx(c);
363+ end
364+end
365+
366+function pp_out = compat_fnint(pp_in)
367+% Octave-compatible wrapper: use ppint (built-in) on Octave, fnint on MATLAB
368+ if mr.aux.isOctave()
369+ pp_out = ppint(pp_in);
370+ else
371+ pp_out = fnint(pp_in);
372+ end
373+end
src/engine/pulseq/+mr/@Sequence/calcPNS.madded+181−0View file
@@ -0,0 +1,181 @@
1+function [ok, pns_norm, pns_comp, t_axis]=calcPNS(obj,hardware,doPlots,calcCNS)
2+% calculate PNS using safe model implementation by Szczepankiewicz and Witzel
3+% assumes safe_pns_prediction package has been downloaded and installed in
4+% Matlab path. See http://github.com/filip-szczepankiewicz/safe_pns_prediction
5+%
6+% returns pns levels due to respective axes (normalized to 1 and not to 100%)
7+%
8+% inputs:
9+% hardware - hardware specifications. see safe_example_hw() from
10+% the safe_pns_prediction package. Alternatively a text file
11+% in the .asc format (Siemens) can be passed, e.g. for Prisma
12+% it is MP_GPA_K2309_2250V_951A_AS82.asc (we leave it as an
13+% exercise to the interested user to find were these files
14+% can be acquired from);
15+% doPlots - optional parameter (defaluts to true)
16+% calcCNS - optional parameter (defaults to false): calculate cardiac
17+% stimulation (CNS) instead of the PNS if the modela data
18+% are provided by the given platform
19+
20+if nargin < 3
21+ doPlots=true;
22+ %calcCNS=false;
23+end
24+%if nargin < 4
25+% calcCNS=false;
26+%end
27+
28+% acquire the entire gradient wave form
29+gw=obj.waveforms_and_times();
30+if doPlots
31+ figure;
32+ plot(gw{1}(1,:),gw{1}(2,:),gw{2}(1,:),gw{2}(2,:),gw{3}(1,:),gw{3}(2,:)); % plot the entire gradient shape
33+ title('gradient wave form, in T/m');
34+end
35+
36+% find beginning and end times and resample GWs to a regular sampling raster
37+tf=[];
38+tl=[];
39+for i=1:3
40+ if size(gw{i},2)>0
41+ tf(end+1)=gw{i}(1,1);
42+ tl(end+1)=gw{i}(1,end);
43+ end
44+end
45+nt_min=floor(min(tf)/obj.gradRasterTime+eps);
46+nt_max=ceil(max(tl)/obj.gradRasterTime-eps);
47+% shift raster positions to the centers of the raster periods
48+nt_min = nt_min + 0.5;
49+nt_max = nt_max - 0.5;
50+if (nt_min<0.5)
51+ nt_min=0.5
52+end
53+t_axis=(nt_min:nt_max)*obj.gradRasterTime;
54+gwr=zeros(length(t_axis),3);
55+for i=1:3
56+ if size(gw{i},2)>0
57+ gwr(:,i)=interp1(gw{i}(1,:),gw{i}(2,:),t_axis,'linear',0);
58+ end
59+end
60+
61+asc=[];
62+ascHasCNS=false;
63+if ischar(hardware)
64+ % this loads the parameters from the provided text file
65+ asc=mr.Siemens.readasc(hardware);
66+ ascHasCNS=isfield(asc, 'GradPatSup') && isfield(asc.GradPatSup, 'Phys') && isfield(asc.GradPatSup.Phys, 'CarNS');
67+end
68+
69+% for Cima.X and later we may want to calculate both CNS and PNS
70+if ~exist('calcCNS','var')
71+ if ascHasCNS
72+ calcCNS=[false,true];
73+ else
74+ calcCNS=false;
75+ end
76+end
77+
78+pns_comp=[];
79+pns_norm=[];
80+ok=[];
81+for c=calcCNS
82+ hardware=asc_to_hw(asc,c);
83+
84+ % use the Szczepankiewicz' and Witzel's implementation
85+ [pns_comp0,res]=safe_gwf_to_pns(gwr/obj.sys.gamma, NaN*ones(length(t_axis),1), obj.gradRasterTime, hardware); % the RF vector is unused in the code inside but it is zeropaded and exported ...
86+ % use the exported RF vector to detect and undo zerpopadding
87+ pns_comp=vertcat(pns_comp,0.01*pns_comp0(~isfinite(res.rf),:)');
88+ % calc pns_norm and the final ok/not_ok
89+ pns_norm=vertcat(pns_norm,vecnorm(pns_comp(end-2:end,:)));
90+ ok=vertcat(ok,all(pns_norm(end,:)<1));
91+ % ready
92+ if doPlots
93+ % plot results
94+ figure;
95+ h=safe_plot(pns_comp0, obj.gradRasterTime);
96+ if c
97+ p=[h.Parent];
98+ a=p(1);
99+ t=a.Title.String;
100+ title(strrep(t,'PNS','CNS'));
101+ end
102+ end
103+end
104+
105+end
106+
107+% local utility functions
108+
109+function hw = asc_to_hw(asc,useCNS)
110+% function hw = asc_to_hw(asc)
111+%
112+% SAFE model parameters for the asc structure as read from the asc file.
113+% See comments for units.
114+%
115+% Maxim Zaitsev 08/10/2019
116+
117+if isfield(asc,'asCOMP') && isfield(asc.asCOMP,'tName')
118+ hw.name = asc.asCOMP(1).tName;
119+else
120+ hw.name = 'unknown';
121+end
122+%hw.look_ahead = 1.0; % MZ: this is not a real hardware parameter but a coefficient, with which the final result is multiplied
123+
124+
125+if isfield(asc,'flGSWDTauX') % older format .asc file
126+ pns_struct=asc;
127+ if useCNS
128+ error('provided .asc file does not support cardiac stimulation prediction');
129+ end
130+elseif isfield(asc,'GradPatSup') % newer format .asc file (e.g. xa61)
131+ if useCNS
132+ if isfield(asc.GradPatSup.Phys, 'CarNS')
133+ pns_struct=asc.GradPatSup.Phys.CarNS;
134+ else
135+ error('provided .asc file does not support cardiac stimulation prediction');
136+ end
137+ else
138+ pns_struct=asc.GradPatSup.Phys.PNS;
139+ end
140+else
141+ error('unknown .asc file format');
142+end
143+
144+hw.x.tau1 = pns_struct.flGSWDTauX(1); % ms
145+hw.x.tau2 = pns_struct.flGSWDTauX(2); % ms
146+hw.x.tau3 = pns_struct.flGSWDTauX(3); % ms
147+hw.x.a1 = pns_struct.flGSWDAX(1);
148+hw.x.a2 = pns_struct.flGSWDAX(2);
149+hw.x.a3 = pns_struct.flGSWDAX(3);
150+hw.x.stim_limit = pns_struct.flGSWDStimulationLimitX; % T/m/s
151+hw.x.stim_thresh = pns_struct.flGSWDStimulationThresholdX; % T/m/s
152+
153+hw.y.tau1 = pns_struct.flGSWDTauY(1); % ms
154+hw.y.tau2 = pns_struct.flGSWDTauY(2); % ms
155+hw.y.tau3 = pns_struct.flGSWDTauY(3); % ms
156+hw.y.a1 = pns_struct.flGSWDAY(1);
157+hw.y.a2 = pns_struct.flGSWDAY(2);
158+hw.y.a3 = pns_struct.flGSWDAY(3);
159+hw.y.stim_limit = pns_struct.flGSWDStimulationLimitY; % T/m/s
160+hw.y.stim_thresh = pns_struct.flGSWDStimulationThresholdY; % T/m/s
161+
162+hw.z.tau1 = pns_struct.flGSWDTauZ(1); % ms
163+hw.z.tau2 = pns_struct.flGSWDTauZ(2); % ms
164+hw.z.tau3 = pns_struct.flGSWDTauZ(3); % ms
165+hw.z.a1 = pns_struct.flGSWDAZ(1);
166+hw.z.a2 = pns_struct.flGSWDAZ(2);
167+hw.z.a3 = pns_struct.flGSWDAZ(3);
168+hw.z.stim_limit = pns_struct.flGSWDStimulationLimitZ; % T/m/s
169+hw.z.stim_thresh = pns_struct.flGSWDStimulationThresholdZ; % T/m/s
170+
171+if isfield (asc, 'asGPAParameters')
172+ hw.x.g_scale = asc.asGPAParameters(1).sGCParameters.flGScaleFactorX;
173+ hw.y.g_scale = asc.asGPAParameters(1).sGCParameters.flGScaleFactorY;
174+ hw.z.g_scale = asc.asGPAParameters(1).sGCParameters.flGScaleFactorZ;
175+else
176+ hw.x.g_scale = asc.flGCGScaleFactorX; % assume older ASC files like for Trio-Tim
177+ hw.y.g_scale = asc.flGCGScaleFactorY;
178+ hw.z.g_scale = asc.flGCGScaleFactorZ;
179+end
180+
181+end
src/engine/pulseq/+mr/@Sequence/gradSpectrum.madded+173−0View file
@@ -0,0 +1,173 @@
1+function [R, Rax, F] = gradSpectrum(obj, FB, fmax, doPlot)
2+% function [R, Rax, F] = seq.gradSpectrum(FB, [fmax = 3000], [doPlot = true])
3+%
4+% Get (and optionally plot) frequency response of a Pulseq sequence.
5+%
6+% Input 'FB' is either a struct array with the forbidden bands,
7+% or the name of a Siemens ASC file.
8+%
9+% Inputs
10+% seq Pulseq sequence object
11+% FB [num_bands] Forbidden frequency bands (struct array)
12+% FB(1).freq center frequency of first forbidden band
13+% FB(1).bw bandwidth of first forbidden band
14+% FB string Siemens ASC file name
15+% fmax [1] Max frequency range
16+% doPlot TRUE/false Plot or just return values
17+%
18+% Outputs
19+% R [n] frequency response (root-sum-of-squares of all axes)
20+% Rax [3 n] frequency response for individual axes
21+% F [n] frequency locations (Hz)
22+%
23+% Function version of demoUnsorted/gradSpectrum.m
24+
25+% defaults
26+if nargin < 4
27+ doPlot = true;
28+end
29+if nargin < 3
30+ fmax = 3000;
31+end
32+if ~exist('FB','var')
33+ FB=[];
34+end
35+
36+% Read ASC file if provided
37+if ischar(FB)
38+ ascData=mr.Siemens.readasc(FB);
39+ clear FB
40+ for i=1:length(ascData.asGPAParameters(1).sGCParameters.aflAcousticResonanceFrequency)
41+ if (ascData.asGPAParameters(1).sGCParameters.aflAcousticResonanceFrequency(i)>0)
42+ FB(i).freq = ascData.asGPAParameters(1).sGCParameters.aflAcousticResonanceFrequency(i);
43+ FB(i).bw = ascData.asGPAParameters(1).sGCParameters.aflAcousticResonanceBandwidth(i);
44+ end
45+ end
46+end
47+
48+% Calculate spectrum/spectrogramm
49+dt=obj.sys.gradRasterTime; % time raster
50+nwin=5000; % 0.05s
51+os=3; % frequency oversampling for prettier peaks
52+
53+faxis=(0:(nwin/2-1))/nwin/dt/os;
54+nfmax=sum(faxis<=fmax);
55+
56+wave_data=obj.waveforms_and_times();
57+ng=length(wave_data);
58+tmax=0;
59+for i=1:ng
60+ if ~isempty(wave_data{i})
61+ tmax=max(tmax, wave_data{i}(1,end));
62+ end
63+end
64+if tmax==0
65+ error('Empty sequence passed to gradSpectrum()');
66+end
67+nt=ceil(tmax/dt);
68+tmax=nt*dt;
69+
70+gw=zeros(ng,nt);
71+for i=1:ng
72+ gw(i,:)=interp1(wave_data{i}(1,:),wave_data{i}(2,:),((1:nt)-0.5)*dt,'linear',0);
73+ % alternative (to be checked in the future)
74+ % it is actually much more appropriate to calculate the spectrium of
75+ % the derivative(!) of the gradient wave form and not the waveform
76+ % itself, at least for the cound/noise of the gradients...
77+ %gw(i,1:end-1)=diff(interp1(wave_data{i}(1,:),wave_data{i}(2,:),((1:nt)-0.5)*dt,'linear',0));
78+end
79+
80+gs=[];
81+
82+ng=size(gw,1);
83+for g=1:ng
84+ x=gw(g,:);
85+ nx = length(x);
86+
87+ nx=ceil(nx/nwin)*nwin;
88+ if nx>length(x)
89+ x=[x, zeros(1,nx-length(x))]; % zerofill
90+ end
91+
92+ nseg1=nx/nwin;
93+ xseg=zeros(nseg1*2-1,nwin*os);
94+
95+ xseg(1:2:end,1:nwin)=reshape(x,[nwin,nseg1])';
96+ if nseg1>1
97+ xseg(2:2:end,1:nwin)=reshape(x(1+nwin/2:end-nwin/2),[nwin,nseg1-1])';
98+ end
99+
100+ xseg_dc=mean(xseg,2);
101+ xseg=xseg-xseg_dc(:,ones(1,nwin*os));
102+
103+ if nseg1>1 % WARNING: this introduces an inconsistency between short and long sequences in term os the peak amplitudes
104+ cwin=0.5*(1-cos(2*pi*(1:nwin)/nwin));
105+ xseg(:,1:nwin)=xseg(:,1:nwin).*cwin(ones(size(xseg,1),1),:);
106+ end
107+
108+ fseg=abs(fft(xseg,[],2));
109+ fseg=fseg(:,1:end/2);
110+
111+ if nseg1>1
112+ gs = [gs; mean(fseg.^2).^0.5]; % sos
113+ %figure; plot(faxis(1:nfmax),sum(fseg(:,1:nfmax).^2).^0.5);
114+ else
115+ gs = [gs; abs(fseg)]; % add abs
116+ end
117+end
118+
119+% Define return values
120+F = faxis(1:nfmax);
121+Rax = gs(:,1:nfmax);
122+R = sum(gs(:,1:nfmax).^2).^0.5;
123+
124+% plot
125+if ~doPlot
126+ return;
127+end
128+
129+figure; plot(faxis(1:nfmax),gs(:,1:nfmax));
130+hold on; plot(faxis(1:nfmax),sum(gs(:,1:nfmax).^2).^0.5); % sos
131+xlabel('frequency / Hz');
132+
133+% alternative "stained glass" plots
134+% clr = repmat('rgb', [1 5]);
135+% if ~isempty(FB)
136+% for i=1:length(FB)
137+% if FB(i).freq > 0
138+% l = FB(i).freq-FB(i).bw/2;
139+% r = FB(i).freq+FB(i).bw/2;
140+% t = max(R);
141+% b = 0;
142+% h = fill([l r r l], [b b t t], clr(i));
143+% h.FaceAlpha = 0.15;
144+% end
145+% end
146+% end
147+
148+if ~isempty(FB)
149+ for i=1:length(FB)
150+ if FB(i).freq > 0
151+ if exist('xline','file')
152+ xline(FB(i).freq,'-');
153+ xline(FB(i).freq-FB(i).bw/2,'--');
154+ xline(FB(i).freq+FB(i).bw/2,'--');
155+ else
156+ % Octave fallback
157+ yl=ylim;
158+ line([1 1]*FB(i).freq, yl, 'Color','k','LineStyle','-');
159+ line([1 1]*(FB(i).freq-FB(i).bw/2), yl, 'Color','k','LineStyle','--');
160+ line([1 1]*(FB(i).freq+FB(i).bw/2), yl, 'Color','k','LineStyle','--');
161+ end
162+ end
163+ end
164+end
165+
166+legend({'Gx','Gy','Gz','Gtot'});
167+
168+%nov = floor(nsc/2);
169+%nff = max(256,2^nextpow2(nsc));
170+
171+%t = spectrogram(x,hamming(nsc),nov);%,nff);
172+%t = spectrogram(x,rectwin(nsc),nov);
173+%maxerr = max(abs(abs(t(:))-abs(s(:))))
src/engine/pulseq/+mr/@Sequence/read.madded+727−0View file
@@ -0,0 +1,727 @@
1+function read(obj,filename,varargin)
2+%READ Load sequence from file.
3+% READ(seqObj, filename, ...) Read the given filename and load sequence
4+% data into sequence object.
5+%
6+% optional parwameter 'detectRFuse' can be given to let the function
7+% infer the currently missing flags concerning the intended use of the RF
8+% pulses (excitation, refocusing, etc). These are important for the
9+% k-space trajectory calculation
10+%
11+% Examples:
12+% Load the sequence defined in gre.seq in my_sequences directory
13+%
14+% read(seqObj,'my_sequences/gre.seq')
15+%
16+% See also write
17+
18+detectRFuse=false;
19+if ~isempty(varargin) && ~isempty(strfind(varargin{:},'detectRFuse'))
20+ detectRFuse=true;
21+end
22+
23+fid = fopen(filename);
24+
25+if fid<0
26+ error('filed to open file ''%s''', filename);
27+end
28+
29+% Clear sequence data
30+%obj.blockEvents = [];
31+obj.blockEvents = {};
32+old_definitions = obj.definitions;
33+obj.definitions = containers.Map();
34+obj.gradLibrary = mr.EventLibrary();
35+obj.shapeLibrary = mr.EventLibrary();
36+obj.rfLibrary = mr.EventLibrary();
37+obj.adcLibrary = mr.EventLibrary();
38+%obj.delayLibrary = mr.EventLibrary();
39+obj.trigLibrary = mr.EventLibrary();
40+obj.labelsetLibrary = mr.EventLibrary();
41+obj.labelincLibrary = mr.EventLibrary();
42+obj.extensionStringIDs={};
43+obj.extensionNumericIDs=[];
44+
45+version_combined=0;
46+requiredDefs=struct('GradientRasterTime',false,'RadiofrequencyRasterTime',false,'AdcRasterTime',false,'BlockDurationRaster',false);
47+
48+% Load data from file
49+while true
50+ section = skipComments(fid);
51+ if section == -1
52+ break
53+ end
54+
55+ switch section
56+ case '[DEFINITIONS]'
57+ obj.definitions = readDefinitions(fid);
58+ v=obj.getDefinition('GradientRasterTime');
59+ if ~isempty(v)
60+ obj.gradRasterTime=v;
61+ requiredDefs.GradientRasterTime=true;
62+ end
63+ v=obj.getDefinition('RadiofrequencyRasterTime');
64+ if ~isempty(v)
65+ obj.rfRasterTime=v;
66+ requiredDefs.RadiofrequencyRasterTime=true;
67+ end
68+ v=obj.getDefinition('AdcRasterTime');
69+ if ~isempty(v)
70+ obj.adcRasterTime=v;
71+ requiredDefs.AdcRasterTime=true;
72+ end
73+ v=obj.getDefinition('BlockDurationRaster');
74+ if ~isempty(v)
75+ obj.blockDurationRaster=v;
76+ requiredDefs.BlockDurationRaster=true;
77+ end
78+ % octave doen't have struct2array(requiredDefs)
79+ requiredDefsC=struct2cell(requiredDefs);
80+ if version_combined >= 1004000 && ~all([requiredDefsC{:}])
81+ fn=fieldnames(requiredDefs);
82+ fn=fn([requiredDefsC{:}]==0);
83+ error(['Required definitions ' sprintf('%s ',fn{:}) 'are missing in the file']);
84+ end
85+ case '[SIGNATURE]'
86+ tmpSignDefs = readDefinitions(fid);
87+ if isKey(tmpSignDefs,'Type')
88+ obj.signatureType=tmpSignDefs('Type');
89+ end
90+ if isKey(tmpSignDefs,'Hash')
91+ obj.signatureValue=tmpSignDefs('Hash');
92+ obj.signatureFile='Text'; % we are reading a text file, so much is known for sure
93+ end
94+ case '[VERSION]'
95+ [version_major, ...
96+ version_minor, ...
97+ version_revision] = readVersion(fid);
98+ assert(version_major == obj.version_major, ...
99+ 'Unsupported version_major %d', version_major)
100+ %
101+ version_combined=1000000*version_major+1000*version_minor+version_revision;
102+ %
103+ if version_combined < 1002000
104+ error('Unsupported version %d.%d.%d, only file format revision 1.2.0 and above are supported', version_major, version_minor, version_revision);
105+ end
106+ if version_combined < 1003001
107+ warning('Loading older Pulseq format file (version %d.%d.%d) some code may function not as expected', version_major, version_minor, version_revision);
108+ end
109+ if version_combined >= 1005000 && detectRFuse
110+ warning('Option ''detectRFuse'' is not supported for file format version 1.5.0 and above');
111+ detectRFuse=false;
112+ end
113+ case '[BLOCKS]'
114+ if ~exist('version_major')
115+ error('Pulseq file MUST include [VERSION] section prior to [BLOCKS] section');
116+ end
117+ [obj.blockEvents,obj.blockDurations,delayInd_tmp] = readBlocks(fid, obj.blockDurationRaster, version_combined);
118+ case '[RF]'
119+ if version_combined >= 1005000
120+ obj.rfLibrary = readEvents(fid, [1 1 1 1 1e-6 1e-6 1 1 1 1 NaN]); % this is 1.5.x format
121+ elseif version_combined >= 1004000
122+ obj.rfLibrary = readEvents(fid, [1 1 1 1 1e-6 1 1]); % this is 1.4.x format
123+ % we fix it below
124+ else
125+ obj.rfLibrary = readEvents(fid, [1 1 1 1e-6 1 1]); % this is 1.3.x and below
126+ % we will have to scan through the library later after all the shapes have been loaded
127+ end
128+ case '[GRADIENTS]'
129+ if version_combined >= 1005000
130+ obj.gradLibrary = readEvents(fid, [1 1 1 1 1 1e-6], 'g' ,obj.gradLibrary); % this is 1.5.x format
131+ elseif version_combined >= 1004000
132+ obj.gradLibrary = readEvents(fid, [1 1 1 1e-6], 'g' ,obj.gradLibrary); % this is 1.4.x format
133+ else
134+ obj.gradLibrary = readEvents(fid, [1 1 1e-6], 'g' ,obj.gradLibrary); % this is 1.3.x and below
135+ end
136+ case '[TRAP]'
137+ obj.gradLibrary = readEvents(fid, [1 1e-6 1e-6 1e-6 1e-6], 't', obj.gradLibrary);
138+ case '[ADC]'
139+ if version_combined >= 1005000
140+ obj.adcLibrary = readEvents(fid, [1 1e-9 1e-6 1 1 1 1 1]); % this is 1.5.x format
141+ else
142+ obj.adcLibrary=readEvents(fid, [1 1e-9 1e-6 1 1]); % this is 1.4.x and older format
143+ % for now we don't have the phase vector in the ADC library
144+ %obj.adcLibrary.data = [obj.adcLibrary.data(:,1:3) 0 obj.adcLibrary.data(:,4:5)]; % import from the older format
145+ end
146+ case '[DELAYS]'
147+ if version_combined >= 1004000
148+ error('Pulseq file revision 1.4.0 and above MUST NOT contain the [DELAYS] section');
149+ end
150+ tmp_delayLibrary = readEvents(fid, 1e-6);
151+ case '[SHAPES]'
152+ obj.shapeLibrary = readShapes(fid, (version_major==1 && version_minor<4));
153+ case '[EXTENSIONS]'
154+ obj.extensionLibrary = readEvents(fid);
155+ otherwise
156+ if strncmp('extension', section, 9)
157+ extension=section(11:end);
158+ if strncmp('TRIGGERS', extension, 8)
159+ id=str2num(extension(9:end));
160+ obj.setExtensionStringAndID('TRIGGERS',id);
161+ obj.trigLibrary = readEvents(fid, [1 1 1e-6 1e-6]);
162+ elseif strncmp('LABELSET', extension, 8)
163+ id=str2num(extension(9:end));
164+ obj.setExtensionStringAndID('LABELSET',id);
165+ obj.labelsetLibrary = readAndParseEvents(fid,[],@str2num,@(s)find(strcmp(mr.getSupportedLabels,s)));
166+ elseif strncmp('LABELINC', extension, 8)
167+ id=str2num(extension(9:end));
168+ obj.setExtensionStringAndID('LABELINC',id);
169+ obj.labelincLibrary = readAndParseEvents(fid,[],@str2num,@(s)find(strcmp(mr.getSupportedLabels,s)));
170+ elseif strncmp('DELAYS', extension, 6)
171+ id=str2num(extension(7:end));
172+ obj.setExtensionStringAndID('DELAYS',id);
173+ obj.softDelayLibrary = readAndParseEvents(fid,[],@str2num,@(s) 1e-6*str2num(s),@str2num,@(s) parseSoftDelayHint(s, obj));
174+ elseif strncmp('RF_SHIMS', extension, 8)
175+ id=str2num(extension(9:end));
176+ obj.setExtensionStringAndID('RF_SHIMS',id);
177+ obj.rfShimLibrary = readAndParseEvents(fid,@preprocRfShimData);
178+ elseif strncmp('ROTATIONS', extension, 9)
179+ id=str2num(extension(10:end));
180+ obj.setExtensionStringAndID('ROTATIONS',id);
181+ obj.rotationLibrary = readEvents(fid);
182+ for i=1:length(obj.rotationLibrary.data)
183+ obj.rotationLibrary.data(i).array=mr.aux.quat.normalize(obj.rotationLibrary.data(i).array);
184+ end
185+ else
186+ warning('Ignoring unknown extension, input string: %s', extension);
187+ exts=regexp(extension, '(\s+)','split');
188+ obj.setExtensionStringAndID(exts{1}, str2num(exts{2}));
189+ skipSection(fid);
190+ end
191+ else
192+ error('Unknown section code: %s', section);
193+ end
194+ end
195+end
196+fclose(fid);
197+
198+% fix sequence data imported from older verisons
199+if version_combined < 1002000
200+ error('Unsupported version %07d, only file format revision 1.2.0 (1002000) and above are supported', version_combined);
201+end
202+
203+% a special case for ADCs as the format for them has only been updated once (in v1.5.0)
204+% we have to do it first because seq.getBlock is used in the next version porting code section (version_combined < 1004000)
205+if version_combined < 1005000
206+ % scan though the ADCs and add empty phase shape IDs
207+ for i=1:length(obj.adcLibrary.data)
208+ obj.adcLibrary.update_data(...
209+ obj.adcLibrary.keys(i), ...
210+ obj.adcLibrary.data(i).array, ...
211+ [obj.adcLibrary.data(i).array(1:3) 0 0 obj.adcLibrary.data(i).array(4:5) 0]); % add empty freqPPM, phasePPM and phase_id fields
212+ end
213+end
214+
215+% fix blocks, gradients and RF objects imported from older versions (< v1.4.0)
216+if version_combined < 1004000
217+ % fix definitions which are be missing in older files
218+ if ~obj.definitions.isKey('GradientRasterTime')
219+ obj.setDefinition('GradientRasterTime', obj.gradRasterTime);
220+ end
221+ if ~obj.definitions.isKey('RadiofrequencyRasterTime')
222+ obj.setDefinition('RadiofrequencyRasterTime', obj.rfRasterTime);
223+ end
224+ if ~obj.definitions.isKey('AdcRasterTime')
225+ obj.setDefinition('AdcRasterTime', obj.adcRasterTime);
226+ end
227+ if ~obj.definitions.isKey('BlockDurationRaster')
228+ obj.setDefinition('BlockDurationRaster', obj.blockDurationRaster);
229+ end
230+
231+ % scan through the RF objects
232+ obj.rfLibrary.type(obj.rfLibrary.keys) = 'u'; % undefined for now, we'll attempt the type detection later (see below)
233+ for i=1:length(obj.rfLibrary.data)
234+ % % need to (partially) decode the magnitude shape to find out the pulse duration
235+ %magSamples = obj.shapeLibrary.data(obj.rfLibrary.data(i).array(2)).array(1);
236+ % % create time shape
237+ %timeShape = mr.compressShape((1:magSamples)-0.5); % time shape is stored in units of RF raster
238+ %data = [timeShape.num_samples timeShape.data];
239+ %timeID = obj.shapeLibrary.find_or_insert(data);
240+ rf=rmfield(obj.rfFromLibData([obj.rfLibrary.data(i).array(1:3) 0 0 obj.rfLibrary.data(i).array(4) 0 0 obj.rfLibrary.data(i).array(5:6)],'u'),'center');
241+ center=mr.calcRfCenter(rf);
242+ obj.rfLibrary.update_data(...
243+ obj.rfLibrary.keys(i), ...
244+ obj.rfLibrary.data(i).array, ...
245+ [obj.rfLibrary.data(i).array(1:3) 0 center obj.rfLibrary.data(i).array(4) 0 0 obj.rfLibrary.data(i).array(5:6)]); % 0 between (4) and (5:6) are the freqPPM and phasePPM
246+ end
247+
248+ % scan through the gradient objects and update 't'-s (trapezoids) und 'g'-s (free-shape gradients)
249+ for i=1:length(obj.gradLibrary.data)
250+ if obj.gradLibrary.type(i)=='t' % we need to fix some trapezoids, namely ones having zero amplitude and zero ramp times
251+ if obj.gradLibrary.data(i).array(2)==0
252+ if abs(obj.gradLibrary.data(i).array(1))==0 && obj.gradLibrary.data(i).array(3) > 0
253+ obj.gradLibrary.update_data(...
254+ obj.gradLibrary.keys(i), ...
255+ obj.gradLibrary.data(i).array, ...
256+ [obj.gradLibrary.data(i).array(1) obj.gradRasterTime obj.gradLibrary.data(i).array(3)-obj.gradRasterTime obj.gradLibrary.data(i).array(4:5)],...
257+ obj.gradLibrary.type(i));
258+ end
259+ end
260+ if obj.gradLibrary.data(i).array(4)==0
261+ if abs(obj.gradLibrary.data(i).array(1))==0 && obj.gradLibrary.data(i).array(3) > 0
262+ obj.gradLibrary.update_data(...
263+ obj.gradLibrary.keys(i), ...
264+ obj.gradLibrary.data(i).array, ...
265+ [obj.gradLibrary.data(i).array(1:2) obj.gradLibrary.data(i).array(3)-obj.gradRasterTime obj.gradRasterTime obj.gradLibrary.data(i).array(5)],...
266+ obj.gradLibrary.type(i));
267+ end
268+ end
269+ end
270+ if obj.gradLibrary.type(i)=='g'
271+ % % need to (partially) decode the shape to find out the duration
272+ %nSamples = obj.shapeLibrary.data(obj.gradLibrary.data(i).array(2)).array(1);
273+ % % create time shape
274+ %timeShape = mr.compressShape((1:nSamples)-0.5); % time shape is stored in units of grad raster
275+ %data = [timeShape.num_samples timeShape.data];
276+ %timeID = obj.shapeLibrary.find_or_insert(data);
277+ obj.gradLibrary.update_data(...
278+ obj.gradLibrary.keys(i), ...
279+ obj.gradLibrary.data(i).array, ...
280+ [obj.gradLibrary.data(i).array(1) NaN NaN obj.gradLibrary.data(i).array(2) 0 obj.gradLibrary.data(i).array(3)], ... % we use NaNs to label the non-initialized first/last fields. These will be restored in the code below
281+ 'g');
282+ end
283+ end
284+
285+ % for versions prior to 1.4.0 blockDurations have not been initialized
286+ obj.blockDurations=zeros(1,length(obj.blockEvents));
287+ % scan trhough blocks and calculate durations
288+ for iB = 1:length(obj.blockEvents)
289+ b=obj.getBlock(iB);
290+ if delayInd_tmp(iB) > 0
291+ b.delay.type = 'delay';
292+ b.delay.delay = tmp_delayLibrary.data(delayInd_tmp(iB)).array;
293+ end
294+ obj.blockDurations(iB)=mr.calcDuration(b);
295+ end
296+elseif version_combined < 1005000
297+ % port from v1.4.x : RF, ADC and GRAD objects need to be updated
298+ % this needs to be done on the level of the libraries, because getBlock will fail
299+
300+ % scan though the RFs and add center, freqPPM, phasePPM and use fields
301+ obj.rfLibrary.type(obj.rfLibrary.keys) = 'u'; % undefined for now, we'll attemp the type detection later (see below)
302+ for i=1:length(obj.rfLibrary.data)
303+ % use goes into the type field, and this is done separately
304+ rf=rmfield(obj.rfFromLibData([obj.rfLibrary.data(i).array(1:4) 0 obj.rfLibrary.data(i).array(5) 0 0 obj.rfLibrary.data(i).array(6:7)],'u'),'center');
305+ center=mr.calcRfCenter(rf);
306+ obj.rfLibrary.update_data(...
307+ obj.rfLibrary.keys(i), ...
308+ obj.rfLibrary.data(i).array, ...
309+ [obj.rfLibrary.data(i).array(1:4) center obj.rfLibrary.data(i).array(5) 0 0 obj.rfLibrary.data(i).array(6:7)]); % 0 between (5) and (6:7) are the freqPPM and phasePPM
310+ end
311+ % scan through the gradient objects and update 'g'-s (free-shape gradients)
312+ for i=1:length(obj.gradLibrary.data)
313+ if obj.gradLibrary.type(i)=='g'
314+ obj.gradLibrary.update_data(...
315+ obj.gradLibrary.keys(i), ...
316+ obj.gradLibrary.data(i).array, ...
317+ [obj.gradLibrary.data(i).array(1) NaN NaN obj.gradLibrary.data(i).array(2:4)], ... % we use NaNs to label the non-initialized first/last fields. These will be restored in the code below
318+ 'g');
319+ end
320+ end
321+end
322+
323+
324+% another run through for all older versions
325+if version_combined < 1005000
326+ gradChannels={'gx','gy','gz'};
327+ gradPrevLast=zeros(1,length(gradChannels));
328+ for iB = 1:length(obj.blockEvents)
329+ b=obj.getBlock(iB);
330+ block_duration=obj.blockDurations(iB);
331+ %obj.blockDurations(iB)=block_duration;
332+ % we also need to keep track of the event IDs because some Pulseq files written by external software may contain repeated entries so searching by content will fail
333+ eventIDs=obj.blockEvents{iB};
334+ processedGradIDs=zeros(1,length(gradChannels));
335+ % update the objects by filling in the fields not contained in the
336+ % pulseq file
337+ for j=1:length(gradChannels)
338+ grad=b.(gradChannels{j});
339+ if isempty(grad)
340+ gradPrevLast(j)=0;
341+ continue;
342+ end
343+ if strcmp(grad.type,'grad')
344+ if grad.delay>0
345+ gradPrevLast(j)=0;
346+ end
347+ if isfield(grad,'first') && isfinite(grad.first)
348+ continue;
349+ end
350+ grad.first = gradPrevLast(j);
351+ % is this an extended trapezoid?
352+ if grad.time_id~=0
353+ grad.last=grad.waveform(end);
354+ grad_duration=grad.delay+grad.tt(end);
355+ else
356+ % restore samples on the edges of the gradient raster intervals
357+ % for that we need the first sample
358+ odd_step1=[grad.first 2*grad.waveform'];
359+ odd_step2=odd_step1.*(mod(1:length(odd_step1),2)*2-1);
360+ waveform_odd_rest=(cumsum(odd_step2).*(mod(1:length(odd_step2),2)*2-1))';
361+ grad.last = waveform_odd_rest(end);
362+ grad_duration=grad.delay+length(grad.waveform)*obj.gradRasterTime;
363+ end
364+ % bookkeeping
365+ gradPrevLast(j) = grad.last;
366+ if grad_duration+eps<block_duration
367+ gradPrevLast(j)=0;
368+ end
369+ %b.(gradChannels{j})=grad;
370+ % update library object
371+ % this does not work s we don't know how the amplitude was defined
372+ % amplitude = max(abs(grad.waveform));
373+ % if amplitude>0
374+ % [~,~,fnz]=find(grad.waveform,1); % find the first non-zero value and make it positive
375+ % amplitude=amplitude*sign(fnz);
376+ % end
377+ % need to recover the amplidute from the library data directly...
378+ id=eventIDs(j+2);
379+ if j>1 && any(processedGradIDs(1:j)==id)
380+ continue; % avoid repeated updates if the same gradient is applied on differen gradient axes
381+ end
382+ processedGradIDs(j)=id;
383+ amplitude=obj.gradLibrary.data(id).array(1);
384+ %
385+ old_data = [amplitude NaN NaN grad.shape_id grad.time_id grad.delay];
386+ new_data = [amplitude grad.first grad.last grad.shape_id grad.time_id grad.delay];
387+ update_data(obj.gradLibrary, id, old_data, new_data,'g');
388+ else
389+ gradPrevLast(j)=0;
390+ end
391+ end
392+ %% copy updated objects back into the event library
393+ %obj.setBlock(iB,b);
394+ end
395+
396+
397+%for iB=1:size(obj.blockEvents,1)
398+% % update the objects by filling in the fields not contained in the
399+% % pulseq file
400+% for j=1:length(gradChannels)
401+% grad=b.(gradChannels{j});
402+% if isempty(grad)
403+% continue;
404+% end
405+% if strcmp(grad.type,'grad')
406+% grad.first = grad.waveform(1); % MZ: eventually we should use extrapolation by 1/2 gradient rasters here
407+% grad.last = grad.waveform(end);
408+% b.(gradChannels{j})=grad;
409+% end;
410+% end
411+% % copy updated objects back into the event library
412+% obj.setBlock(iB,b);
413+end
414+
415+if detectRFuse
416+ % find the RF pulses, list flip angles
417+ % and work around the current (rev 1.2.0) Pulseq file format limitation
418+ % that the RF pulse use is not stored in the file
419+ for k=obj.rfLibrary.keys
420+ libData=obj.rfLibrary.data(k).array;
421+ rf=obj.rfFromLibData(libData,'u');
422+ %flipAngleDeg=abs(sum(rf.signal))*rf.t(1)*360; %we use rfex.t(1) in place of opt.system.rfRasterTime
423+ flipAngleDeg=abs(sum(rf.signal(1:end-1).*(rf.t(2:end)-rf.t(1:end-1))))*360;
424+ offresonance_ppm=1e6*rf.freqOffset/obj.sys.B0/obj.sys.gamma;
425+ % fix library %%%% if length(obj.rfLibrary.type)>=eventInd(2)
426+ if flipAngleDeg < 90.01 % we add 0.01 degree to account for rounding errors which we've experienced for very short RF pulses
427+ obj.rfLibrary.type(k) = 'e';
428+ else
429+ if rf.shape_dur > 6e-3 && offresonance_ppm >= -3.5 && offresonance_ppm <= -3.4 % approx -3.45 ppm
430+ obj.rfLibrary.type(k) = 's'; % saturation (fat-sat)
431+ else
432+ obj.rfLibrary.type(k) = 'r';
433+ end
434+ end
435+% % fix libData
436+% if length(libData) < 9
437+% if flipAngleDeg < 90.01 % we add 0.01 degree to account for rounding errors which we've experienced for very short RF pulses
438+% libData(9) = 0; % or 1 ?
439+% else
440+% libData(9) = 2; % or 1 ?
441+% end
442+% obj.rfLibrary.data(k).array=libData;
443+% end
444+ end
445+end
446+
447+return
448+
449+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
450+%%%%%%%%%%%%%%%%%%%%%%% Helper functions %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
451+
452+ function def = readDefinitions(fid)
453+ %readDefinitions Read the [DEFINITIONS] section of a sequence file.
454+ % defs=readDefinitions(fid) Read user definitions from file
455+ % identifier of an open MR sequence file and return a map of
456+ % key/value entries.
457+
458+ def = containers.Map;
459+ %line = localStrip(fgetl(fid));
460+ line = localStrip(skipComments(fid));
461+ while ischar(line) && ~(isempty(line) || line(1) == '#')
462+ tok = textscan(line, '%s');
463+ def(tok{1}{1}) = str2double(tok{1}(2:end));
464+ if ~all(isfinite(def(tok{1}{1})))
465+ def(tok{1}{1}) = strtrim(line((length(tok{1}{1})+2):end));
466+ end
467+ line = fgetl(fid);
468+ end
469+ end
470+
471+ function [major, minor, revision] = readVersion(fid)
472+ %readVersion Read the [VERSION] section of a sequence file.
473+ % defs=readVersion(fid) Read Pulseq version from file
474+ % identifier of an open MR sequence file and return it
475+
476+ major = [];
477+ minor = [];
478+ revision = [];
479+ line = fgetl(fid);
480+ while ischar(line) && ~(isempty(line) || line(1)=='#')
481+ tok = textscan(line,'%s');
482+ switch tok{1}{1}
483+ case 'major'
484+ major = str2double(tok{1}(2:end));
485+ case 'minor'
486+ minor = str2double(tok{1}(2:end));
487+ case 'revision'
488+ revision = str2double(tok{1}(2:end));
489+ end
490+ line = fgetl(fid);
491+ end
492+ end
493+
494+ function [eventTable,blockDurations,delayIDs_tmp] = readBlocks(fid, blockDurationRaster, version_combined)
495+ %readBlocks Read the [BLOCKS] section of a sequence file.
496+ % library=readBlocks(fid) Read blocks from file identifier of an
497+ % open MR sequence file and return the event table.
498+
499+ eventTable = {};
500+ blockDurations = [];
501+ delayIDs_tmp = [];
502+ line = fgetl(fid);
503+ while ischar(line) && ~(isempty(line) || line(1) == '#')
504+ blockEvents = sscanf(line, '%f')';
505+ %eventTable = [eventTable; blockEvents(2:end)];
506+ if version_combined<=1002001
507+ eventTable{blockEvents(1)} = [0 blockEvents(3:end) 0];
508+ else
509+ eventTable{blockEvents(1)} = [0 blockEvents(3:end)];
510+ end
511+ if version_combined>=1004000
512+ blockDurations(blockEvents(1)) = blockEvents(2)*blockDurationRaster;
513+ else
514+ delayIDs_tmp(blockEvents(1)) = blockEvents(2);
515+ end
516+ line = fgetl(fid);
517+ end
518+ end
519+
520+ function str = format_helper(scale)
521+ if isfinite(scale)
522+ str='%f ';
523+ else
524+ str='%c ';
525+ end
526+ end
527+
528+ function eventLibrary = readEvents(fid, scale, type, eventLibrary)
529+ %readEvents Read an event section of a sequence file.
530+ % library=readEvents(fid) Read event data from file identifier of
531+ % an open MR sequence file and return a library of events.
532+ %
533+ % library=readEvents(fid,scale) Read event data and scale
534+ % elements according to column vector scale.
535+ %
536+ % library=readEvents(fid,scale,type) Attach the type string to
537+ % elements of the library.
538+ %
539+ % library=readEvents(...,library) Append new events to the given
540+ % library.
541+ if nargin < 2
542+ scale = 1;
543+ format='%f';
544+ type_idx=[];
545+ data_mask=[];
546+ else
547+ % new in v1.5.0 : generate format string; NaN labels character param(s)
548+ format=['%f ' cell2mat(arrayfun(@format_helper,scale,'UniformOutput',false))];
549+ format(end)=[]; % matlab is so incredibly ugly!
550+ data_mask=isfinite(scale);
551+ type_idx=find(~data_mask);
552+ if length(type_idx)>2
553+ error('Only one type field (marked as NaN) can be provided');
554+ end
555+ if isempty(type_idx)
556+ data_mask=[];
557+ end
558+ end
559+ if nargin < 4
560+ eventLibrary = mr.EventLibrary();
561+ end
562+ %
563+ line = fgetl(fid);
564+ while ischar(line) && ~(isempty(line) || line(1) == '#')
565+ data = sscanf(line,format)';
566+ id = data(1);
567+ if ~isempty(type_idx)
568+ type=char(data(type_idx+1)); % need +1 because of the eventID in the first position
569+ end
570+ data = scale.*data(2:end);
571+ if nargin < 3 && isempty(type_idx)
572+ if isempty(data_mask)
573+ eventLibrary.insert(id, data);
574+ else
575+ eventLibrary.insert(id, data(data_mask));
576+ end
577+ else
578+ if isempty(data_mask)
579+ eventLibrary.insert(id, data, type);
580+ else
581+ eventLibrary.insert(id, data(data_mask), type);
582+ end
583+ end
584+
585+ line=fgetl(fid);
586+ end
587+ end
588+
589+ function eventLibrary = readAndParseEvents(fid, preproc, varargin)
590+ %readAndParseEvents Read an event section of a sequence file.
591+ % library=readAndParseEvents(fid) Read event data from file
592+ % identifier of an open MR sequence file and return a library of
593+ % events.
594+ %
595+ % library=readAndParseEvents(fid,[],parser1,parser2,...) Read event
596+ % data and convert the elements using to the provided parser.
597+ % Default parser is str2num()
598+ %
599+ % library=readAndParseEvents(fid,preproc,...) Read event data
600+ % from file and apply the preproc() to the acquired data
601+ % line-by-line prior to adding them to the event library
602+ %
603+
604+ eventLibrary = mr.EventLibrary();
605+ line = fgetl(fid);
606+ while ischar(line) && ~(isempty(line) || line(1) == '#')
607+ datas=regexp(line, '(\s+)','split');
608+ data=zeros(1,length(datas)-1);
609+ id = str2num(datas{1});
610+ for i=2:length(datas)
611+ if i>nargin-1
612+ data(i-1) = str2num(datas{i});
613+ else
614+ data(i-1) = varargin{i-1}(datas{i});
615+ end
616+ end
617+
618+ if ~exist('preproc','var') || isempty(preproc)
619+ eventLibrary.insert(id, data);
620+ else
621+ eventLibrary.insert(id, preproc(data));
622+ end
623+
624+ line=fgetl(fid);
625+ end
626+ end
627+
628+ function skipSection(fid)
629+ %skipSection Read an event section of a sequence file without
630+ % interpreting it.
631+ %
632+ line = fgetl(fid);
633+ while ischar(line) && ~(isempty(line) || line(1) == '#')
634+ line=fgetl(fid);
635+ end
636+ end
637+
638+ function shapeLibrary = readShapes(fid, forceConvertUncompressed)
639+ %readShapes Read the [SHAPES] section of a sequence file.
640+ % library=readShapes(fid) Read shapes from file identifier of an
641+ % open MR sequence file and return a library of shapes.
642+
643+ shapeLibrary = mr.EventLibrary();
644+ line = skipComments(fid);
645+ while ~(~ischar(line) || isempty(line) || ~strcmp(line(1:8), 'shape_id'))
646+ tok = textscan(line, '%s');
647+ id = str2double(tok{1}(2));
648+ line = skipComments(fid);
649+ tok = textscan(line, '%s');
650+ num_samples = str2double(tok{1}(2));
651+ data = [];
652+ line = skipComments(fid); % first sample
653+ while ischar(line) && ~(isempty(line) || line(1) == '#')
654+ data = [data sscanf(line, '%f')];
655+ %data = [data single(sscanf(line, '%f'))]; % C-code uses single precision and we had problems already due to the rounding during reading in of the shapes...
656+ line = fgetl(fid);
657+ end
658+
659+ line = skipComments(fid, true); % MZ: second parameter to prevent readShapes from reading into the next section (long-standing bug)
660+
661+ % check if conversion is needed: in v1.4.x we use length(data)==num_samples
662+ % as a marker for the uncompressed (stored) data. In older versions this condition could occur by chance
663+ if forceConvertUncompressed && length(data)==num_samples
664+ shape.data=data;
665+ shape.num_samples=num_samples;
666+ shape = mr.compressShape(mr.decompressShape(shape,true));
667+ data = [shape.num_samples shape.data];
668+ else
669+ data = [num_samples data];
670+ end
671+ shapeLibrary.insert(id, data);
672+ end
673+ end
674+
675+ function nextLine = skipComments(fid, stopBeforeSection)
676+ %skipComments Read lines of skipping blank lines and comments.
677+ % line=skipComments(fid) Read lines from valid file identifer and
678+ % return the next non-comment line.
679+
680+ if (nargin<2)
681+ stopBeforeSection=false;
682+ end
683+
684+ tmpPos=ftell(fid);
685+ line = fgetl(fid);
686+ while ischar(line) && (isempty(line) || line(1) == '#')
687+ tmpPos=ftell(fid);
688+ line = fgetl(fid);
689+ end
690+ if ischar(line)
691+ if stopBeforeSection && line(1)=='['
692+ fseek(fid,tmpPos,-1); % restore the file position
693+ nextLine = ''; % feasible (non-error) dummy return
694+ else
695+ nextLine = line;
696+ end
697+ else
698+ nextLine = -1;
699+ end
700+ end
701+
702+ function id=parseSoftDelayHint(s, seq)
703+ try
704+ id=seq.softDelayHints1(s);
705+ catch
706+ id=seq.softDelayHints1.length()+1;
707+ seq.softDelayHints1(s)=id;
708+ seq.softDelayHints2{id}=s;
709+ end
710+ end
711+
712+ function data_out=preprocRfShimData(data)
713+ if length(data)~=data(1)*2+1
714+ error('Error reading RF shim extension data');
715+ end
716+ data_out=data(2:end);
717+ end
718+
719+ % for compatibility with Octave which has no strip()
720+ function s=localStrip(s)
721+ a=1;
722+ b=length(s);
723+ while a<=b && isspace(s(a)), a=a+1; end
724+ while a<=b && isspace(s(b)), b=b-1; end
725+ s=s(a:b);
726+ end
727+end
src/engine/pulseq/+mr/@Sequence/readBinary.madded+321−0View file
@@ -0,0 +1,321 @@
1+function readBinary(obj,filename)
2+%READBINARY Load sequence from binary file.
3+% READBINARY(seqObj, filename) Read the given filename and load sequence
4+% data stored in the binary version of the Pulseq open file format. The
5+% binary format is described in the specficiation available at
6+% http://pulseq.github.io
7+%
8+% Examples:
9+% Load the sequence defined in gre.bin in sequences directory
10+%
11+% readBinary(seqObj,'sequences/gre.bin')
12+%
13+% See also writeBinary
14+
15+binaryCodes = obj.getBinaryCodes();
16+fid=fopen(filename);
17+magicNum = fread(fid,1,'int64=>int64');
18+assert(magicNum==binaryCodes.fileHeader,'Not a Pulseq binary file');
19+version_major = fread(fid,1,'int64');
20+version_minor = fread(fid,1,'int64');
21+version_revision = fread(fid,1,'int64');
22+assert(version_major==obj.version_major,'Unsupported version_major %d', version_major) % too strict - no reverse compatibility yet
23+assert(version_minor==obj.version_minor,'Unsupported version_minor %d', version_minor) % too strict - no reverse compatibility yet
24+assert(version_revision==obj.version_revision,'Unsupported version_revision %d', version_revision) % too strict - no reverse compatibility yet
25+
26+% set version
27+obj.version_major = version_major;
28+obj.version_minor = version_minor;
29+obj.version_revision = version_revision;
30+
31+% Clear sequence data
32+obj.blockEvents={};
33+obj.blockDurations=[];
34+obj.definitions=containers.Map();
35+obj.gradLibrary=mr.EventLibrary();
36+obj.shapeLibrary=mr.EventLibrary();
37+obj.rfLibrary=mr.EventLibrary();
38+obj.adcLibrary=mr.EventLibrary();
39+obj.trigLibrary=mr.EventLibrary();
40+obj.labelsetLibrary=mr.EventLibrary();
41+obj.labelincLibrary=mr.EventLibrary();
42+obj.extensionLibrary=mr.EventLibrary();
43+obj.rfShimLibrary=mr.EventLibrary();
44+obj.softDelayLibrary=mr.EventLibrary();
45+obj.softDelayHints1=containers.Map();
46+obj.softDelayHints2={};
47+obj.rotationLibrary=mr.EventLibrary();
48+obj.extensionStringIDs={};
49+obj.extensionNumericIDs=[];
50+obj.signatureType='';
51+obj.signatureFile='';
52+obj.signatureValue='';
53+
54+% Load data from file
55+while true
56+ section = int64(fread(fid,1,'int64'));
57+ if isempty(section)
58+ break
59+ end
60+
61+ switch section
62+ case binaryCodes.section.definitions
63+ obj.definitions = readDefinitions(fid);
64+ v=obj.getDefinition('GradientRasterTime');
65+ if ~isempty(v), obj.gradRasterTime=v; end
66+ v=obj.getDefinition('RadiofrequencyRasterTime');
67+ if ~isempty(v), obj.rfRasterTime=v; end
68+ v=obj.getDefinition('AdcRasterTime');
69+ if ~isempty(v), obj.adcRasterTime=v; end
70+ v=obj.getDefinition('BlockDurationRaster');
71+ if ~isempty(v), obj.blockDurationRaster=v; end
72+
73+ case binaryCodes.section.blocks
74+ [obj.blockEvents, obj.blockDurations] = readBlocks(fid, obj.blockDurationRaster);
75+
76+ case binaryCodes.section.rf
77+ % array: [amp mag_id phase_id time_shape_id center delay freqPPM phasePPM freq phase]
78+ % type (use) stored separately as a char
79+ numEvents = double(fread(fid,1,'int64'));
80+ for i=1:numEvents
81+ id = double(fread(fid,1,'int32'));
82+ amp = double(fread(fid,1,'float64'));
83+ ids = double(fread(fid,3,'int32'))'; % mag_id, phase_id, time_shape_id
84+ ctr = double(fread(fid,1,'int64')) * 1e-12; % center (ps -> s)
85+ dly = double(fread(fid,1,'int64')) * 1e-12; % delay (ps -> s)
86+ fpp = double(fread(fid,4,'float64'))'; % freqPPM, phasePPM, freq, phase
87+ use = char(fread(fid,1,'char'));
88+ obj.rfLibrary.insert(id, [amp ids(1) ids(2) ids(3) ctr dly fpp(1) fpp(2) fpp(3) fpp(4)], use);
89+ end
90+
91+ case binaryCodes.section.gradients
92+ % array: [amp first last amp_shape_id time_shape_id delay]
93+ numEvents = double(fread(fid,1,'int64'));
94+ for i=1:numEvents
95+ id = double(fread(fid,1,'int32'));
96+ afl = double(fread(fid,3,'float64'))'; % amp, first, last
97+ ids2 = double(fread(fid,2,'int32'))'; % amp_shape_id, time_shape_id
98+ dly = double(fread(fid,1,'int64')) * 1e-12; % delay (ps -> s)
99+ obj.gradLibrary.insert(id, [afl ids2 dly], 'g');
100+ end
101+
102+ case binaryCodes.section.trapezoids
103+ % array: [amp rise flat fall delay]
104+ numEvents = double(fread(fid,1,'int64'));
105+ for i=1:numEvents
106+ id = double(fread(fid,1,'int32'));
107+ amp = double(fread(fid,1,'float64'));
108+ t4 = double(fread(fid,4,'int64'))' * 1e-12; % rise,flat,fall,delay (ps->s)
109+ obj.gradLibrary.insert(id, [amp t4], 't');
110+ end
111+
112+ case binaryCodes.section.adc
113+ % array: [num dwell delay freqPPM phasePPM freq phase phase_id]
114+ numEvents = double(fread(fid,1,'int64'));
115+ for i=1:numEvents
116+ id = double(fread(fid,1,'int32'));
117+ num = double(fread(fid,1,'int64'));
118+ dwell = double(fread(fid,1,'int64')) * 1e-12; % ps -> s
119+ delay = double(fread(fid,1,'int64')) * 1e-12; % ps -> s
120+ f4 = double(fread(fid,4,'float64'))'; % freqPPM, phasePPM, freq, phase
121+ phid = double(fread(fid,1,'int32'));
122+ obj.adcLibrary.insert(id, [num dwell delay f4(1) f4(2) f4(3) f4(4) phid]);
123+ end
124+
125+ case binaryCodes.section.delays
126+ readLegacyDelays(fid);
127+
128+ case binaryCodes.section.shapes
129+ obj.shapeLibrary = readShapes(fid);
130+
131+ case binaryCodes.section.extensions
132+ % array per entry: [type ref next_id]
133+ numEvents = double(fread(fid,1,'int64'));
134+ for i=1:numEvents
135+ id = double(fread(fid,1,'int32'));
136+ data = double(fread(fid,3,'int32'))';
137+ obj.extensionLibrary.insert(id, data);
138+ end
139+
140+ case binaryCodes.section.triggers
141+ % type id(i32) then events: id(i32) type(i32) channel(i32) delay(i32,us) duration(i32,us)
142+ ext_id = double(fread(fid,1,'int32'));
143+ obj.setExtensionStringAndID('TRIGGERS', ext_id);
144+ numEvents = double(fread(fid,1,'int64'));
145+ for i=1:numEvents
146+ id = double(fread(fid,1,'int32'));
147+ tc = double(fread(fid,2,'int32'))'; % type, channel
148+ dd = double(fread(fid,2,'int64'))' * 1e-12; % delay, duration (ps->s)
149+ obj.trigLibrary.insert(id, [tc dd]);
150+ end
151+
152+ case binaryCodes.section.labelset
153+ ext_id = double(fread(fid,1,'int32'));
154+ obj.setExtensionStringAndID('LABELSET', ext_id);
155+ numEvents = double(fread(fid,1,'int64'));
156+ for i=1:numEvents
157+ id = double(fread(fid,1,'int32'));
158+ data = double(fread(fid,2,'int32'))'; % value, label_index
159+ obj.labelsetLibrary.insert(id, data);
160+ end
161+
162+ case binaryCodes.section.labelinc
163+ ext_id = double(fread(fid,1,'int32'));
164+ obj.setExtensionStringAndID('LABELINC', ext_id);
165+ numEvents = double(fread(fid,1,'int64'));
166+ for i=1:numEvents
167+ id = double(fread(fid,1,'int32'));
168+ data = double(fread(fid,2,'int32'))'; % value, label_index
169+ obj.labelincLibrary.insert(id, data);
170+ end
171+
172+ case binaryCodes.section.softdelays
173+ ext_id = double(fread(fid,1,'int32'));
174+ obj.setExtensionStringAndID('DELAYS', ext_id);
175+ numEvents = double(fread(fid,1,'int64'));
176+ for i=1:numEvents
177+ id = double(fread(fid,1,'int32'));
178+ num = double(fread(fid,1,'int32'));
179+ offset = double(fread(fid,1,'int64')) * 1e-12; % ps -> s
180+ factor = double(fread(fid,1,'float64'));
181+ hlen = double(fread(fid,1,'int32'));
182+ hint = char(fread(fid,hlen,'char')');
183+ % register hint string and get its index
184+ if obj.softDelayHints1.isKey(hint)
185+ hint_idx = obj.softDelayHints1(hint);
186+ else
187+ hint_idx = length(obj.softDelayHints2) + 1;
188+ obj.softDelayHints1(hint) = hint_idx;
189+ obj.softDelayHints2{hint_idx} = hint;
190+ end
191+ obj.softDelayLibrary.insert(id, [num offset factor hint_idx]);
192+ end
193+
194+ case binaryCodes.section.rfshims
195+ ext_id = double(fread(fid,1,'int32'));
196+ obj.setExtensionStringAndID('RF_SHIMS', ext_id);
197+ numEvents = double(fread(fid,1,'int64'));
198+ for i=1:numEvents
199+ id = double(fread(fid,1,'int32'));
200+ num_chan = double(fread(fid,1,'int32'));
201+ chan_data = double(fread(fid,2*num_chan,'float64'))';
202+ obj.rfShimLibrary.insert(id, chan_data);
203+ end
204+
205+ case binaryCodes.section.rotations
206+ ext_id = double(fread(fid,1,'int32'));
207+ obj.setExtensionStringAndID('ROTATIONS', ext_id);
208+ numEvents = double(fread(fid,1,'int64'));
209+ for i=1:numEvents
210+ id = double(fread(fid,1,'int32'));
211+ quat = double(fread(fid,4,'float64'))';
212+ obj.rotationLibrary.insert(id, mr.aux.quat.normalize(quat));
213+ end
214+
215+ case binaryCodes.section.signature
216+ type_len = double(fread(fid,1,'int32'));
217+ sig_type = char(fread(fid,type_len,'char')');
218+ hash_len = double(fread(fid,1,'int32'));
219+ hash_raw = uint8(fread(fid,hash_len,'uint8'));
220+ fread(fid,1,'int64'); % original file length prior to signature append
221+
222+ obj.signatureType = sig_type;
223+ obj.signatureFile = 'bin';
224+ if isempty(hash_raw)
225+ obj.signatureValue = '';
226+ else
227+ obj.signatureValue = lower(reshape(dec2hex(hash_raw,2)',1,[]));
228+ end
229+
230+ otherwise
231+ error('Unknown section code: %s',dec2hex(section));
232+ end
233+end
234+fclose(fid);
235+
236+
237+return
238+
239+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
240+%%%%%%%%%%%%%%%%%%%%%%% Helper functions %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
241+
242+ function def = readDefinitions(fid)
243+ %readDefinitions Read the [DEFINITIONS] section of a sequence file.
244+ % defs=readDefinitions(fid) Read user definitions from file
245+ % identifier of an open MR sequence file and return a map of
246+ % key/value entries.
247+
248+ def = containers.Map();
249+ numDefs = fread(fid,1,'int64');
250+ for iDef=1:numDefs
251+ count = fread(fid,1,'int32');
252+ key = char(fread(fid,count,'char'));
253+ if size(key,1)~=1
254+ key=key.';
255+ end
256+ count = fread(fid,1,'int32');
257+ type = char(fread(fid,1,'char'));
258+ switch type
259+ case 'f'
260+ values = double(fread(fid,count,'float64'));
261+ case 'i'
262+ values = int32(fread(fid,count,'int32'));
263+ case 'c'
264+ values = char(fread(fid,count,'char')').';
265+ if size(values,1)~=1
266+ values=values.';
267+ end
268+ if ~isempty(values) && values(end)==0
269+ values=values(1:(end-1));
270+ end
271+ otherwise
272+ error('Unknown definition type: %s', type);
273+ end
274+ def(key) = values;
275+ end
276+ end
277+
278+ function [eventTable, blockDurations] = readBlocks(fid, blockDurationRaster)
279+ %readBlocks Read the [BLOCKS] section of a binary sequence file.
280+ % Each block stores: duration(int64) + 6 event IDs (int32):
281+ % rf, gx, gy, gz, adc, ext
282+ % Returns eventTable (cell array) with 7 elements per block
283+ % [0 rf gx gy gz adc ext] and blockDurations in seconds.
284+
285+ numBlocks = double(fread(fid,1,'int64'));
286+ eventTable = cell(1, numBlocks);
287+ blockDurations = zeros(1, numBlocks);
288+ for ii = 1:numBlocks
289+ dur_raster = double(fread(fid,1,'int64'));
290+ event_ids = double(fread(fid,6,'int32'))';
291+ blockDurations(ii) = dur_raster * blockDurationRaster;
292+ eventTable{ii} = [0 event_ids]; % prepend 0 for legacy delay placeholder
293+ end
294+ end
295+
296+ function shapeLibrary = readShapes(fid)
297+ %readShapes Read the [SHAPES] section of a binary sequence file.
298+
299+ shapeLibrary=mr.EventLibrary();
300+ numShapes = fread(fid,1,'int64');
301+ for iShape=1:numShapes
302+ id = double(fread(fid,1,'int32'));
303+ numUncompressed = double(fread(fid,1,'int64'));
304+ numCompressed = double(fread(fid,1,'int64'));
305+ data = double(fread(fid,numCompressed,'float32'))';
306+ shapeData = [numUncompressed data];
307+ shapeLibrary.insert(id,shapeData);
308+ end
309+ end
310+
311+ function readLegacyDelays(fid)
312+ % readLegacyDelays Read and ignore legacy [DELAYS] binary section.
313+ % Delay events are no longer represented via obj.delayLibrary.
314+ numEvents = double(fread(fid,1,'int64'));
315+ for ii=1:numEvents
316+ fread(fid,1,'int32'); % id
317+ fread(fid,1,'int64'); % delay value
318+ end
319+ end
320+
321+end
src/engine/pulseq/+mr/@Sequence/testReport.madded+432−0View file
@@ -0,0 +1,432 @@
1+function [ report ] = testReport( obj, varargin )
2+%testReport Analyze the sequence and return a text report
3+% Currently no parameters are required. In future versions it may be
4+% possible to (de)select some tests. Optional parameter 'system' allows
5+% to test the limits against the given MR system
6+%
7+% maxim.zaitsev@uniklinik-freiburg.de
8+
9+ function skey = makeSkey(iKey)
10+ if iKey>=0
11+ skey=['p' sprintf('%d',iKey)];
12+ else
13+ skey=['m' sprintf('%d',-iKey)];
14+ end
15+ end
16+
17+ function s = optionalOut(format, number)
18+ if any(abs(number)>eps)
19+ s=sprintf(format, number);
20+ else
21+ s='';
22+ end
23+ end
24+
25+persistent parser
26+if isempty(parser)
27+ parser = inputParser;
28+ parser.FunctionName = 'testReport';
29+
30+ addParamValue(parser,'system',struct([]),@isstruct);
31+end
32+parse(parser,varargin{:});
33+opt = parser.Results;
34+
35+% find the RF pulses and list flip angles
36+flipAnglesDeg=[];
37+for k=obj.rfLibrary.keys
38+ libData=obj.rfLibrary.data(k).array;
39+ if length(obj.rfLibrary.type)>=k
40+ rf=obj.rfFromLibData(libData,obj.rfLibrary.type(k));
41+ else
42+ rf=obj.rfFromLibData(libData);
43+ end
44+ flipAnglesDeg=[flipAnglesDeg abs(sum(rf.signal(1:end-1).*(rf.t(2:end)-rf.t(1:end-1))))*360]; %we use rfex.t(1) in place of opt.system.rfRasterTime
45+end
46+flipAnglesDeg=unique(flipAnglesDeg);
47+
48+% calculate TE and TR
49+
50+[duration, numBlocks, eventCount]=obj.duration();
51+
52+[wnt.gw_data, wnt.tfp_excitation, wnt.tfp_refocusing, wnt.t_adc]=obj.waveforms_and_times();
53+%[ktraj_adc, ktraj, t_excitation, t_refocusing, t_adc] = obj.calculateKspace();
54+%[ktraj_adc, t_adc, ktraj, t_ktraj, t_excitation, t_refocusing] = obj.calculateKspacePP();
55+[ktraj_adc, t_adc, ~, ~, t_excitation, ~] = obj.calculateKspacePP('externalWaveformsAndTimes',wnt);
56+
57+% remove all ADC events that come before the first RF event (noise scans or alike)
58+if ~isempty(t_excitation)
59+ ktraj_adc=ktraj_adc(:,t_adc > t_excitation(1));
60+ t_adc = t_adc(t_adc > t_excitation(1));
61+end
62+
63+% trajectory calculation will fail for spin-echoes if seq is loaded from a
64+% file for the current file format revision (1.2.0) because we do not store
65+% the use of the RF pulses. Read function has an option 'detectRFuse' which
66+% may help...
67+
68+%
69+kabs_adc=sum(ktraj_adc.^2,1).^0.5;
70+[kabs_echo, index_echo]=min(kabs_adc);
71+t_echo=t_adc(index_echo); % just a first estimate, see if we can improve it
72+if kabs_echo>eps
73+ i2check=[];
74+ % check if adc kspace trajectory has elements left and right to index_echo
75+ if index_echo > 1
76+ i2check=[i2check (index_echo-1)];
77+ end
78+ if index_echo < length(kabs_adc)
79+ i2check=[i2check (index_echo+1)];
80+ end
81+ for a=1:numel(i2check)
82+ v_i_to_0=-ktraj_adc(:,index_echo);
83+ v_i_to_t=ktraj_adc(:,i2check(a))-ktraj_adc(:,index_echo);
84+ % project v_i_to_0 to v_o_to_t
85+ p_vit=v_i_to_0'*v_i_to_t/(vecnorm(v_i_to_t)^2);
86+ if p_vit>0
87+ % we have forund a bracket for the echo and the proportionality
88+ % coefficient is p_vit
89+ t_echo=t_adc(index_echo)*(1-p_vit) + t_adc(i2check(a))*p_vit;
90+ break;
91+ end
92+ end
93+end
94+
95+if ~isempty(t_excitation)
96+ t_ex_tmp=t_excitation(t_excitation<t_echo);
97+ TE=t_echo-t_ex_tmp(end);
98+ % TODO detect multiple TEs
99+else
100+ TE=NaN;
101+end
102+
103+if (length(t_excitation)<2)
104+ TR=duration; % best estimate for now
105+else
106+ t_ex_tmp1=t_excitation(t_excitation>t_echo);
107+ if isempty(t_ex_tmp1)
108+ TR=t_ex_tmp(end)-t_ex_tmp(end-1);
109+ else
110+ TR=t_ex_tmp1(1)-t_ex_tmp(end);
111+ end
112+ % TODO check frequency offset to detect multiple slices
113+end
114+
115+% check sequence dimensionality and spatial resolution
116+k_extent=max(abs(ktraj_adc),[],2);
117+k_scale=max(k_extent);
118+if (k_scale~=0)
119+ k_bins=4e6; % this defines our ability to separate k-space samples.
120+ % lower values give us imunity to rounding errors in k-space calculations
121+ % current code below (2nd pass) however merges neighboring cells (+-1)
122+ k_threshold=k_scale/k_bins;
123+
124+ % detect unused dimensions and delete them
125+ if any(k_extent<k_threshold)
126+ ktraj_adc(k_extent<k_threshold,:)=[]; % delete rows
127+ k_extent(k_extent<k_threshold)=[];
128+ end
129+
130+ % bin the k-space trajectory to detect repetitions / slices
131+ k_len=size(ktraj_adc,2);
132+ k_repeat=zeros(1,k_len);
133+ k_storage=zeros(1,k_len);
134+ k_storage_next=1;
135+ % the fastest option should be dictionary(), if it is available
136+ hasDict=false;
137+ try
138+ kmap=configureDictionary("string","double"); %is too new and only supported sice 2023; %containers.Map('KeyType','char','ValueType','double'); % works as well but is substantially slower
139+ hasDict=true;
140+ catch
141+ end
142+ if hasDict
143+ for i=1:k_len
144+ key = sprintf('a%d', int32(k_bins+round(ktraj_adc(:,i)/k_threshold)));
145+ k_storage_ind=kmap.lookup(key,'FallbackValue',0);
146+ if k_storage_ind==0
147+ k_storage_ind=k_storage_next;
148+ kmap(key)=k_storage_ind;
149+ k_storage_next=k_storage_next+1;
150+ end
151+ k_storage(k_storage_ind)=k_storage(k_storage_ind)+1;
152+ k_repeat(i) = k_storage(k_storage_ind);
153+ end
154+ else
155+ % we use strings in combination with structs to go fast... (matlab is strange)
156+ kmap = struct();
157+ for i=1:k_len
158+ key = sprintf('a%d', int32(k_bins+round(ktraj_adc(:,i)/k_threshold)));
159+ assert(length(key)<=63);
160+ if isfield(kmap,key)
161+ k_storage_ind = kmap.(key);
162+ else
163+ k_storage_ind=k_storage_next;
164+ kmap.(key)=k_storage_ind;
165+ k_storage_next=k_storage_next+1;
166+ end
167+ k_storage(k_storage_ind)=k_storage(k_storage_ind)+1;
168+ k_repeat(i) = k_storage(k_storage_ind);
169+ end
170+ end
171+ % % containers.Map only supports string as a key... (in older matlabs)
172+ % % in Octave we use the built-in, in Matlab the Java version
173+ % if mr.aux.isOctave()
174+ % kmap = containers.Map('KeyType', 'char', 'ValueType', 'int32');
175+ % for i=1:k_len
176+ % key_string = sprintf('%d ', int32(k_bins+round(ktraj_adc(:,i)/k_threshold)));
177+ % % containers.Map does not have a proper find function so we use direct
178+ % % access and catch the possible error
179+ % try
180+ % k_storage_ind = kmap(key_string);
181+ % catch
182+ % k_storage_ind=k_storage_next;
183+ % kmap(key_string)=k_storage_ind;
184+ % k_storage_next=k_storage_next+1;
185+ % end
186+ % k_storage(k_storage_ind)=k_storage(k_storage_ind)+1;
187+ % k_repeat(i) = k_storage(k_storage_ind);
188+ % end
189+ % else
190+ % kmap = java.util.HashMap;
191+ % for i=1:k_len
192+ % key_string = sprintf('%d ', int32(k_bins+round(ktraj_adc(:,i)/k_threshold)));
193+ % k_storage_ind = kmap.get(key_string);
194+ % if isempty(k_storage_ind)
195+ % k_storage_ind=k_storage_next;
196+ % kmap.put(key_string,k_storage_ind);
197+ % k_storage_next=k_storage_next+1;
198+ % end
199+ % k_storage(k_storage_ind)=k_storage(k_storage_ind)+1;
200+ % k_repeat(i) = k_storage(k_storage_ind);
201+ % end
202+ % end
203+ % at this point k_storage(1:(k_storage_next-1)) is our visit frequency map
204+ Repeats_max=max(k_storage(1:(k_storage_next-1)));
205+ Repeats_min=min(k_storage(1:(k_storage_next-1)));
206+ Repeats_median=median(k_storage(1:(k_storage_next-1)));
207+ Repeats_unique=unique(k_storage(1:(k_storage_next-1)));
208+ Counts_unique=zeros(size(Repeats_unique));
209+ for i=1:numel(Repeats_unique)
210+ Counts_unique(i)=sum(Repeats_unique(i)==k_storage(1:(k_storage_next-1)));
211+ end
212+
213+ ktraj_rep1=ktraj_adc(:,k_repeat==1);
214+ % TODO: think of something clever, e.g. detecting maximum delta-k
215+ % if length(k_extent)==2
216+ % dt = delaunayTriangulation(ktraj_rep1');
217+ % k = convexHull(dt);
218+ % figure; plot(dt.Points(:,1),dt.Points(:,2), '.', 'markersize',10); hold on;
219+ % plot(dt.Points(k,1),dt.Points(k,2), 'r'); hold off;
220+ % %[V,R] = voronoiDiagram(dt);
221+ % figure; voronoi(dt);
222+ % end
223+ % try to detect k-space lines or columns.
224+ k_counters=zeros(size(ktraj_rep1));
225+ dims=size(ktraj_rep1,1);
226+ %ordering=cell(1,dims);
227+ if hasDict
228+ for j=1:dims
229+ kmap=configureDictionary("int32","int32");%dictionary(int32.empty(1,0),int32.empty(1,0));%configureDictionary("int32","int32"); % containers.Map('KeyType', 'int32', 'ValueType', 'int32'); % works as well but is substantially slower
230+ k_storage=zeros(1,k_len);
231+ k_storage_next=1;
232+ for i=1:size(ktraj_rep1,2)
233+ key=int32(round(ktraj_rep1(j,i)/k_threshold));
234+ k_storage_ind = kmap.lookup(key,'FallbackValue',int32(0));
235+ if k_storage_ind==0
236+ % attempt to account for rounding errors
237+ k_storage_ind = kmap.lookup(key+1,'FallbackValue',int32(0));
238+ if k_storage_ind==0
239+ k_storage_ind = kmap.lookup(key-1,'FallbackValue',int32(0));
240+ % did not find anywhere...
241+ if k_storage_ind==0
242+ k_storage_ind=k_storage_next;
243+ kmap(key)=k_storage_ind;
244+ k_storage_next=k_storage_next+1;
245+ k_storage(k_storage_ind)=ktraj_rep1(j,i);
246+ %fprintf('%d:%d(%g) ',k_storage_ind,key,ktraj_rep1(j,i));
247+ end
248+ end
249+ end
250+ %assert(k_storage_ind==k_storage(k_storage_ind));
251+ k_counters(j,i) = k_storage_ind;
252+ end
253+ %ordering{j}=cell2mat(kmap.values);
254+ %fprintf('\n');
255+ end
256+ else
257+ for j=1:dims
258+ k_storage=zeros(1,k_len);
259+ k_storage_next=1;
260+ kmap = struct(); % use struct() as a replacement for dict()
261+ for i=1:size(ktraj_rep1,2)
262+ key=int32(round(ktraj_rep1(j,i)/k_threshold));
263+ skey=makeSkey(key);
264+ try
265+ k_storage_ind = kmap.(skey);
266+ catch
267+ skey1=makeSkey(key+1); % attempt to account for rounding errors
268+ try
269+ k_storage_ind = kmap.(skey1);
270+ catch
271+ skey1=makeSkey(key-1); % attempt to account for rounding errors
272+ try
273+ k_storage_ind = kmap.(skey1);
274+ catch
275+ k_storage_ind=k_storage_next;
276+ kmap.(skey)=k_storage_ind;
277+ k_storage_next=k_storage_next+1;
278+ k_storage(k_storage_ind)=ktraj_rep1(j,i);
279+ %fprintf('%d:%d(%g) ',k_storage_ind,key,ktraj_rep1(j,i));
280+ end
281+ end
282+ end
283+ %assert(k_storage_ind==k_storage(k_storage_ind));
284+ k_counters(j,i) = k_storage_ind;
285+ end
286+ % for i=1:size(ktraj_rep1,2)
287+ % key=int32(round(ktraj_rep1(j,i)/k_threshold));
288+ % skey=makeSkey(key);
289+ % if isfield(kmap,skey)
290+ % k_storage_ind = kmap.(skey);
291+ % else
292+ % skey1=makeSkey(key+1); % attempt to account for rounding errors
293+ % if isfield(kmap,skey1)
294+ % k_storage_ind = kmap.(skey1);
295+ % else
296+ % skey1=makeSkey(key-1); % attempt to account for rounding errors
297+ % if isfield(kmap,skey1)
298+ % k_storage_ind = kmap.(skey1);
299+ % else
300+ % k_storage_ind=k_storage_next;
301+ % kmap.(skey)=k_storage_ind;
302+ % k_storage_next=k_storage_next+1;
303+ % k_storage(k_storage_ind)=ktraj_rep1(j,i);
304+ % %fprintf('%d:%d(%g) ',k_storage_ind,key,ktraj_rep1(j,i));
305+ % end
306+ % end
307+ % end
308+ % %assert(k_storage_ind==k_storage(k_storage_ind));
309+ % k_counters(j,i) = k_storage_ind;
310+ % end
311+ %ordering{j}=cell2mat(kmap.values);
312+ %fprintf('\n');
313+ end
314+ end
315+ unique_kpositions=max(k_counters,[],2);
316+ isCartesian=(prod(unique_kpositions)==size(ktraj_rep1,2));
317+else
318+ unique_kpositions=1;
319+end
320+% check gradient amplitudes and slew rates
321+
322+% gradient waveform
323+%gw_data=obj.waveforms_and_times(); % FIXME: avoid this second call for generating gradient shapes (1st one was inside of the k-space calculation routine)
324+gws=cell(size(wnt.gw_data));
325+ga=zeros(length(wnt.gw_data),1);
326+gs=zeros(length(wnt.gw_data),1);
327+% to calculate max absolute gradients and slew rates we have to play
328+% tricks... we namely have to interpolate the data to the common time axis
329+dim1ind = @(x, n) x(n,:);
330+common_time=unique(dim1ind([wnt.gw_data{:}],1));
331+gw_ct=zeros(length(wnt.gw_data),length(common_time));
332+gs_ct=zeros(length(wnt.gw_data),length(common_time)-1);
333+for gc=1:length(wnt.gw_data)
334+ if size(wnt.gw_data{gc},2)>0
335+ gws{gc}=(wnt.gw_data{gc}(2,2:end)-wnt.gw_data{gc}(2,1:end-1))./(wnt.gw_data{gc}(1,2:end)-wnt.gw_data{gc}(1,1:end-1)); % slew
336+ % interpolate to the common time
337+ gw_ct(gc,:)=interp1(wnt.gw_data{gc}(1,:),wnt.gw_data{gc}(2,:),common_time,'linear',0);
338+ gs_ct(gc,:)=(gw_ct(gc,2:end)-gw_ct(gc,1:end-1))./(common_time(2:end)-common_time(1:end-1));
339+ % max grad/slew per channel
340+ ga(gc)=max(abs(wnt.gw_data{gc}(2,:)));
341+ gs(gc)=max(abs(gws{gc}));
342+ % TODO: calculate grad RMS values (this is an interesting task in the piece-wise-linear domain)
343+ end
344+end
345+
346+%figure; plot(common_time, gw_ct');
347+%figure; plot(common_time, sum(gw_ct.^2,1).^0.5);
348+%figure; plot(0.5*(common_time(1:end-1)+common_time(2:end)), sum(gs_ct.^2,1).^0.5);
349+
350+% max absolute value grad/slew -- check for a worst case upon rotation
351+if ~isempty(gw_ct)
352+ ga_abs=max(sum(gw_ct.^2,1).^0.5);
353+else
354+ ga_abs=0;
355+end
356+if ~isempty(gs_ct)
357+ gs_abs=max(sum(gs_ct.^2,1).^0.5);
358+else
359+ gs_abs=0;
360+end
361+
362+% check timing of blocks and delays (raster alignment)
363+[timing_ok, timing_error_report] = obj.checkTiming();
364+
365+report = { sprintf('Number of blocks: %d\n',numBlocks),...
366+ [ sprintf( 'Number of events:\n'),...
367+ optionalOut(' RF: %6d\n',eventCount(2)),...
368+ optionalOut(' Gx: %6d\n',eventCount(3)),...
369+ optionalOut(' Gy: %6d\n',eventCount(4)),...
370+ optionalOut(' Gz: %6d\n',eventCount(5)),...
371+ optionalOut(' ADC: %6d\n',eventCount(6))],...
372+ [ sprintf( 'Event library use:\n'),...
373+ optionalOut(' RF: %6d\n',numel(obj.rfLibrary.keys)),...
374+ optionalOut(' Grad: %6d\n',numel(obj.gradLibrary.keys)),...
375+ optionalOut(' Shape: %6d\n',numel(obj.shapeLibrary.keys)),...
376+ optionalOut(' ADC: %6d\n',numel(obj.adcLibrary.keys)),...
377+ optionalOut(' Extn: %6d\n',numel(obj.extensionLibrary.keys)),...
378+ optionalOut(' Trigg: %6d\n',numel(obj.trigLibrary.keys)),...
379+ optionalOut(' Label: %6d\n',numel(obj.labelsetLibrary.keys)+numel(obj.labelincLibrary.keys)),...
380+ optionalOut(' RfShm: %6d\n',numel(obj.rfShimLibrary.keys)),...
381+ optionalOut(' Rot: %6d\n',numel(obj.rotationLibrary.keys)),...
382+ optionalOut(' SoDel: %6d\n',numel(obj.softDelayLibrary.keys))],...
383+ [ sprintf('Sequence duration: %.6fs\n',duration),...
384+ sprintf('TE: %.6fs\n',TE),...
385+ sprintf('TR: %.6fs\n',TR) ],...
386+ sprintf('Flip angle: %.02f°\n', flipAnglesDeg),...
387+ sprintf('Unique k-space positions (a.k.a. columns, rows, etc): %d\n', unique_kpositions)};
388+if any(unique_kpositions>1)
389+ report = { report{:},...
390+ [ sprintf('Dimensions: %d\n', length(k_extent)),...
391+ sprintf(' Spatial resolution: %.02f mm\n', 0.5./k_extent*1e3) ],...
392+ sprintf('Repetitions/slices/contrasts: %.d range: [%.d %.d]\n', Repeats_median, Repeats_min, Repeats_max) };
393+ report = { report{:},...
394+ sprintf(' %d k-space position(s) repeated %d times\n', [Counts_unique;Repeats_unique])};
395+
396+ if isCartesian
397+ report = { report{:}, sprintf('Grid-like/Cartesian encoding trajectory detected\n') };
398+ else
399+ report = { report{:}, sprintf('Non-Cartesian/irregular encoding trajectory detected (e.g. spiral, radial, some EPI, etc)\n') };
400+ end
401+end
402+if (timing_ok)
403+ report = { report{:}, sprintf('Block timing check passed successfully\n') };
404+else
405+ report = { report{:}, [ sprintf('Block timing check failed! Error listing follows:\n'),...
406+ sprintf([timing_error_report{:}]) ] };
407+end
408+msg_ga='';
409+if ~isempty(opt.system) && any(ga > opt.system.maxGrad)
410+ msg_ga=' [some component EXCEEDED]';
411+end
412+msg_gs='';
413+if ~isempty(opt.system) && any(gs > opt.system.maxSlew)
414+ msg_gs=' [some component EXCEEDED]';
415+end
416+report = { report{:},...
417+ sprintf(['Max. Gradient: %.0f Hz/m == %.02f mT/m' msg_ga '\n'], [ga mr.convert(ga,'Hz/m','mT/m')]'),...
418+ sprintf(['Max. Slew Rate: %g Hz/m/s == %.02f T/m/s' msg_gs '\n'], [gs mr.convert(gs,'Hz/m/s','T/m/s')]') };
419+msg_ga='';
420+if ~isempty(opt.system) && ga_abs > opt.system.maxGrad
421+ msg_ga=' [EXCEEDED]';
422+end
423+msg_gs='';
424+if ~isempty(opt.system) && gs_abs > opt.system.maxSlew
425+ msg_gs=' [EXCEEDED]';
426+end
427+report = { report{:},...
428+ sprintf(['Max. Absolute Gradient: %.0f Hz/m == %.02f mT/m' msg_ga '\n'], [ga_abs mr.convert(ga_abs,'Hz/m','mT/m')]'),...
429+ sprintf(['Max. Absolute Slew Rate: %g Hz/m/s == %.02f T/m/s' msg_gs '\n'], [gs_abs mr.convert(gs_abs,'Hz/m/s','T/m/s')]') };
430+
431+end
432+
src/engine/pulseq/+mr/@Sequence/write.madded+294−0View file
@@ -0,0 +1,294 @@
1+function write(obj,filename,create_signature)
2+%WRITE Write sequence to file.
3+% WRITE(seqObj, filename) Write the sequence data to the given
4+% filename using the open file format for MR sequences.
5+%
6+% Examples:
7+% Write the sequence file to the my_sequences directory
8+%
9+% write(seqObj,'my_sequences/gre.seq')
10+%
11+% See also read
12+
13+if (nargin<3)
14+ create_signature=true;
15+end
16+
17+fid=fopen(filename, 'w');
18+assert(fid ~= -1, 'Cannot open file: %s', filename);
19+fprintf(fid, '# Pulseq sequence file\n');
20+fprintf(fid, '# Created by MATLAB mr toolbox\n\n');
21+
22+% we always write files in the default current version, which may be
23+% differen to one, loaded (and stored in the seq object)
24+[version_major, version_minor, version_revision]=mr.aux.version('output');
25+fprintf(fid, '[VERSION]\n');
26+fprintf(fid, 'major %s\n', num2str(version_major));
27+fprintf(fid, 'minor %s\n', num2str(version_minor));
28+fprintf(fid, 'revision %s\n', num2str(version_revision));
29+fprintf(fid, '\n');
30+
31+% handle RequiredExtensions definition
32+if ~isempty(obj.rotationLibrary.keys)
33+ RD=obj.getDefinition('RequiredExtensions');
34+ if isempty(RD) || isempty(strfind(RD,'ROTATIONS'))
35+ RD=mr.aux.strstrip([mr.aux.strstrip(RD) ' ROTATIONS']);
36+ obj.setDefinition('RequiredExtensions', RD);
37+ end
38+end
39+
40+if ~isempty(obj.definitions)
41+ fprintf(fid, '[DEFINITIONS]\n');
42+ keys = obj.definitions.keys;
43+ values = obj.definitions.values;
44+ for i=1:length(keys)
45+ fprintf(fid, '%s ', keys{i});
46+ if (ischar(values{i}))
47+ fprintf(fid, '%s ', values{i});
48+ else
49+ fprintf(fid, '%.9g ', values{i});
50+ end
51+ fprintf(fid, '\n');
52+ end
53+ fprintf(fid, '\n');
54+end
55+
56+fprintf(fid, '# Format of blocks:\n');
57+fprintf(fid, '# NUM DUR RF GX GY GZ ADC EXT\n');
58+fprintf(fid, '[BLOCKS]\n');
59+idFormatWidth = length(num2str(length(obj.blockEvents)));
60+idFormatStr = ['%' num2str(idFormatWidth) 'd'];
61+for i = 1:length(obj.blockEvents)
62+ %fprintf(fid,[idFormatStr ' %2d %2d %3d %3d %3d %2d 0\n'],[i obj.blockEvents(i,:)]);
63+ %fprintf(fid,[idFormatStr ' %2d %2d %3d %3d %3d %2d 0\n'],[i obj.blockEvents{i}]);
64+ bd=obj.blockDurations(i)/obj.blockDurationRaster;
65+ bdr=round(bd);
66+ assert(abs(bdr-bd)<1e-6); % this may still trigger false alarms for very long delays due to the limited accuracy of the double
67+ fprintf(fid,[idFormatStr ' %3d %3d %3d %3d %3d %2d %2d\n'], ...
68+ [i bdr obj.blockEvents{i}(2:end)]);
69+end
70+fprintf(fid, '\n');
71+
72+if ~isempty(obj.rfLibrary.keys)
73+ fprintf(fid, '# Format of RF events:\n');
74+ fprintf(fid, '# id ampl. mag_id phase_id time_shape_id center delay freqPPM phasePPM freq phase use\n');
75+ fprintf(fid, '# .. Hz .. .. .. us us ppm rad/MHz Hz rad ..\n');
76+ fprintf(fid,['# Field ''use'' is the initial of: \n# ' ...
77+ strtrim(cell2mat(cellfun(@(x) [x ' '], mr.getSupportedRfUse(), 'UniformOutput', false))) ...
78+ '\n']);
79+ fprintf(fid, '[RF]\n');
80+ keys = obj.rfLibrary.keys;
81+ for k = keys
82+ libData1 = obj.rfLibrary.data(k).array(1:4);
83+ libData2 = obj.rfLibrary.data(k).array(7:10);
84+ center = obj.rfLibrary.data(k).array(5)*1e6; % us
85+ delay = round(obj.rfLibrary.data(k).array(6)/obj.rfRasterTime)*obj.rfRasterTime*1e6; % a bit of a hack: round the delay
86+ fprintf(fid, '%d %12g %d %d %d %g %g %g %g %g %g %c\n', [k libData1 center delay], libData2, obj.rfLibrary.type(k));
87+ end
88+ fprintf(fid, '\n');
89+end
90+
91+arbGradMask = obj.gradLibrary.type == 'g';
92+trapGradMask = obj.gradLibrary.type == 't';
93+
94+if any(arbGradMask)
95+ fprintf(fid, '# Format of arbitrary gradients:\n');
96+ fprintf(fid, '# time_shape_id of 0 means default timing (stepping with grad_raster starting at 1/2 of grad_raster)\n');
97+ fprintf(fid, '# id amplitude first last amp_shape_id time_shape_id delay\n');
98+ fprintf(fid, '# .. Hz/m Hz/m Hz/m .. .. us\n');
99+ fprintf(fid, '[GRADIENTS]\n');
100+ keys = obj.gradLibrary.keys;
101+ for k = keys(arbGradMask)
102+ fprintf(fid, '%d %12g %12g %12g %d %d %d\n', ...
103+ [k obj.gradLibrary.data(k).array(1:5) ...
104+ round(obj.gradLibrary.data(k).array(6)*1e6)]);
105+ end
106+ fprintf(fid, '\n');
107+end
108+
109+if any(trapGradMask)
110+ fprintf(fid, '# Format of trapezoid gradients:\n');
111+ fprintf(fid, '# id amplitude rise flat fall delay\n');
112+ fprintf(fid, '# .. Hz/m us us us us\n');
113+ fprintf(fid, '[TRAP]\n');
114+ keys = obj.gradLibrary.keys;
115+ for k = keys(trapGradMask)
116+ data = obj.gradLibrary.data(k).array;
117+ data(2:end) = round(1e6*data(2:end));
118+ fprintf(fid, '%2d %12g %3d %4d %3d %3d\n', [k data]);
119+ end
120+ fprintf(fid, '\n');
121+end
122+
123+if ~isempty(obj.adcLibrary.keys)
124+ fprintf(fid, '# Format of ADC events:\n');
125+ fprintf(fid, '# id num dwell delay freqPPM phasePPM freq phase phase_id\n');
126+ fprintf(fid, '# .. .. ns us ppm rad/MHz Hz rad ..\n');
127+ fprintf(fid, '[ADC]\n');
128+ keys = obj.adcLibrary.keys;
129+ for k = keys
130+ data = obj.adcLibrary.data(k).array.*[1 1e9 1e6 1 1 1 1 1];
131+ fprintf(fid, '%d %d %.0f %.0f %g %g %g %g %d\n', [k data]);
132+ end
133+ fprintf(fid, '\n');
134+end
135+
136+%if ~isempty(obj.delayLibrary.keys)
137+% fprintf(fid, '# Format of delays:\n');
138+% fprintf(fid, '# id delay (us)\n');
139+% fprintf(fid, '[DELAYS]\n');
140+% keys = obj.delayLibrary.keys;
141+% for k = keys
142+% fprintf(fid, '%d %d\n', ...
143+% [k round(1e6*obj.delayLibrary.data(k).array)]);
144+% end
145+% fprintf(fid, '\n');
146+%end
147+
148+if ~isempty(obj.extensionLibrary.keys)
149+ fprintf(fid, '# Format of extension lists:\n');
150+ fprintf(fid, '# id type ref next_id\n');
151+ fprintf(fid, '# next_id of 0 terminates the list\n');
152+ fprintf(fid, '# Extension list is followed by extension specifications\n');
153+ fprintf(fid, '[EXTENSIONS]\n');
154+ keys = obj.extensionLibrary.keys;
155+ for k = keys
156+ fprintf(fid, '%d %d %d %d\n', ...
157+ [k round(obj.extensionLibrary.data(k).array)]);
158+ end
159+ fprintf(fid, '\n');
160+end
161+
162+if ~isempty(obj.trigLibrary.keys)
163+ fprintf(fid, '# Extension specification for digital output and input triggers:\n');
164+ fprintf(fid, '# id type channel delay (us) duration (us)\n');
165+% fprintf(fid, 'extension TRIGGERS 1\n'); % fixme: extension ID 1 is hardcoded here for triggers
166+ fprintf(fid, ['extension TRIGGERS ',num2str(obj.getExtensionTypeID('TRIGGERS')),'\n']);
167+
168+ keys = obj.trigLibrary.keys;
169+ for k = keys
170+ fprintf(fid, '%d %d %d %d %d\n', ...
171+ [k round(obj.trigLibrary.data(k).array.*[1 1 1e6 1e6])]);
172+ end
173+ fprintf(fid, '\n');
174+end
175+
176+if ~isempty(obj.labelsetLibrary.keys) || ~isempty(obj.labelincLibrary.keys)
177+ lbls=mr.getSupportedLabels();
178+
179+ if ~isempty(obj.labelsetLibrary.keys)
180+ fprintf(fid, '# Extension specification for setting labels:\n');
181+ fprintf(fid, '# id set labelstring\n');
182+ tid=obj.getExtensionTypeID('LABELSET');
183+ fprintf(fid, ['extension LABELSET ',num2str(tid),'\n']);
184+ keys = obj.labelsetLibrary.keys;
185+ for k = keys
186+ fprintf(fid, '%d %d %s\n', ...
187+ k, obj.labelsetLibrary.data(k).array(1),lbls{obj.labelsetLibrary.data(k).array(2)});
188+ end
189+ fprintf(fid, '\n');
190+ end
191+ if ~isempty(obj.labelincLibrary.keys)
192+ fprintf(fid, '# Extension specification for increasing labels:\n');
193+ fprintf(fid, '# id inc labelstring\n');
194+ tid=obj.getExtensionTypeID('LABELINC');
195+ fprintf(fid, ['extension LABELINC ',num2str(tid),'\n']);
196+ lbls=mr.getSupportedLabels();
197+ keys = obj.labelincLibrary.keys;
198+ for k = keys
199+ fprintf(fid, '%d %d %s\n', ...
200+ k, obj.labelincLibrary.data(k).array(1),lbls{obj.labelincLibrary.data(k).array(2)});
201+ end
202+ fprintf(fid, '\n');
203+ end
204+end
205+
206+if ~isempty(obj.softDelayLibrary.keys)
207+ fprintf(fid, '# Extension specification for soft delays:\n');
208+ fprintf(fid, '# id num offset factor hint\n');
209+ fprintf(fid, '# .. .. us .. ..\n');
210+ fprintf(fid, ['extension DELAYS ',num2str(obj.getExtensionTypeID('DELAYS')),'\n']);
211+
212+ keys = obj.softDelayLibrary.keys;
213+ for k = keys
214+ fprintf(fid, '%d %d %g %g %s\n', ...
215+ k, obj.softDelayLibrary.data(k).array(1), obj.softDelayLibrary.data(k).array(2)*1e6, obj.softDelayLibrary.data(k).array(3), obj.softDelayHints2{obj.softDelayLibrary.data(k).array(4)});
216+ end
217+ fprintf(fid, '\n');
218+end
219+
220+if ~isempty(obj.rfShimLibrary.keys)
221+ fprintf(fid, '# Extension specification for RF shimming:\n');
222+ fprintf(fid, '# id num_chan magn_c1 phase_c1 magn_c2 phase_c2 ...\n');
223+ fprintf(fid, ['extension RF_SHIMS ',num2str(obj.getExtensionTypeID('RF_SHIMS')),'\n']);
224+
225+ keys = obj.rfShimLibrary.keys;
226+ for k = keys
227+ fprintf(fid, '%d %d', [k length(obj.rfShimLibrary.data(k).array)/2]);
228+ fprintf(fid, ' %g', obj.rfShimLibrary.data(k).array);
229+ fprintf(fid, '\n');
230+ end
231+ fprintf(fid, '\n');
232+end
233+
234+if ~isempty(obj.rotationLibrary.keys)
235+ fprintf(fid, '# Extension specification for rotation events:\n');
236+ fprintf(fid, '# id RotQuat0 RotQuatX RotQuatY RotQuatZ\n');
237+ fprintf(fid, ['extension ROTATIONS ',num2str(obj.getExtensionTypeID('ROTATIONS')),'\n']);
238+
239+ keys = obj.rotationLibrary.keys;
240+ for k = keys
241+ fprintf(fid, '%d ', k );
242+ fprintf(fid, ' %g', obj.rotationLibrary.data(k).array);
243+ fprintf(fid, '\n');
244+ end
245+ fprintf(fid, '\n');
246+end
247+
248+if ~isempty(obj.shapeLibrary.keys)
249+ fprintf(fid, '# Sequence Shapes\n');
250+ fprintf(fid, '[SHAPES]\n\n');
251+ keys = obj.shapeLibrary.keys;
252+ for k = keys
253+ shape_dat = obj.shapeLibrary.data(k).array;
254+ fprintf(fid, 'shape_id %d\n', k);
255+ fprintf(fid, 'num_samples %d\n', shape_dat(1));
256+ fprintf(fid, '%.9g\n', shape_dat(2:end));
257+ fprintf(fid, '\n');
258+ end
259+end
260+
261+fclose(fid);
262+
263+if create_signature
264+ % sign the file (this version with re/loading the file is a factor 2 faster than the sprintf() based one that kept a memory-copy of the data written)
265+
266+ % re-open and read in the file
267+ fid=fopen(filename, 'r');
268+ buf=fread(fid);
269+ fclose(fid);
270+
271+ % calculate the digest
272+ md5hash=mr.aux.md5(buf);
273+ %fprintf('%s\n',md5hash);
274+
275+ % store the signature in the object
276+ obj.signatureType='md5';
277+ obj.signatureFile='text';
278+ obj.signatureValue=md5hash;
279+
280+ % re-open the file for appending
281+ fid=fopen(filename, 'a');
282+ fprintf(fid, '\n[SIGNATURE]\n'); % the preceding new line BELONGS to the signature (and needs to be sripped away to recalculate the signature)
283+ fprintf(fid, '# This is the hash of the Pulseq file, calculated right before the [SIGNATURE]\n');
284+ fprintf(fid, '# section was added. It can be reproduced/verified with md5sum if the file\n');
285+ fprintf(fid, '# trimmed to the position right above [SIGNATURE]. The new line character\n');
286+ fprintf(fid, '# preceding [SIGNATURE] BELONGS to the signature (and needs to be sripped away\n');
287+ fprintf(fid, '# for recalculating/verification)\n');
288+ fprintf(fid, 'Type md5\n');
289+ fprintf(fid, 'Hash %s\n', md5hash);
290+ fclose(fid);
291+end
292+
293+end
294+
src/engine/pulseq/+mr/@Sequence/writeBinary.madded+288−0View file
@@ -0,0 +1,288 @@
1+function writeBinary(obj,filename,create_signature)
2+%WRITEBINARY Write sequence to file in binary format.
3+% WRITEBINARY(seqObj, filename) Write the sequence data to the given
4+% filename using the binary version of the Pulseq open file format for MR
5+% sequences. The file specification is available at
6+% http://pulseq.github.io
7+%
8+% Examples:
9+% Write the sequence file to the sequences directory
10+%
11+% writeBinary(seqObj,'sequences/gre.bseq')
12+%
13+% See also readBinary
14+
15+if (nargin<3)
16+ create_signature=true;
17+end
18+
19+% handle RequiredExtensions definition (same as write())
20+if ~isempty(obj.rotationLibrary.keys)
21+ RD=obj.getDefinition('RequiredExtensions');
22+ if isempty(RD) || isempty(strfind(RD,'ROTATIONS'))
23+ RD=mr.aux.strstrip([mr.aux.strstrip(RD) ' ROTATIONS']);
24+ obj.setDefinition('RequiredExtensions', RD);
25+ end
26+end
27+
28+binaryCodes = obj.getBinaryCodes();
29+fid=fopen(filename, 'w');
30+fwrite(fid, binaryCodes.fileHeader, 'int64');
31+fwrite(fid, int64(obj.version_major), 'int64');
32+fwrite(fid, int64(obj.version_minor), 'int64');
33+fwrite(fid, int64(obj.version_revision), 'int64');
34+
35+if ~isempty(obj.definitions)
36+ fwrite(fid, binaryCodes.section.definitions, 'int64');
37+ keys = obj.definitions.keys;
38+ values = obj.definitions.values;
39+ fwrite(fid, length(keys), 'int64');
40+ for i = 1:length(keys)
41+ fwrite(fid, length(keys{i}),'int32');
42+ fwrite(fid, keys{i},'char');
43+ val = values{i};
44+ fwrite(fid, length(val), 'int32');
45+ if ischar(val)
46+ fwrite(fid, 'c', 'char');
47+ fwrite(fid, val, 'char');
48+ elseif isinteger(val)
49+ fwrite(fid, 'i', 'char');
50+ fwrite(fid, val, 'int32');
51+ elseif isfloat(val)
52+ fwrite(fid, 'f', 'char');
53+ fwrite(fid, val, 'float64');
54+ else
55+ error(['unknown type of the value type for ' keys{i} ]);
56+ end
57+ end
58+end
59+
60+% Blocks: write count, then per block: duration (int64, in blockDurationRaster units)
61+% followed by 6 event IDs (int32): rf, gx, gy, gz, adc, ext
62+fwrite(fid, binaryCodes.section.blocks, 'int64');
63+fwrite(fid, length(obj.blockEvents), 'int64');
64+for i = 1:length(obj.blockEvents)
65+ bd = obj.blockDurations(i) / obj.blockDurationRaster;
66+ bdr = round(bd);
67+ assert(abs(bdr - bd) < 1e-6);
68+ fwrite(fid, bdr, 'int64'); % block duration in raster units
69+ fwrite(fid, obj.blockEvents{i}(2:end), 'int32'); % rf, gx, gy, gz, adc, ext
70+end
71+
72+% RF: amp(f64) mag_id(i32) phase_id(i32) time_shape_id(i32) center(i64,us)
73+% delay(i64,us) freqPPM(f64) phasePPM(f64) freq(f64) phase(f64) use(char)
74+% array layout: [amp mag_id phase_id time_shape_id center delay freqPPM phasePPM freq phase]
75+if ~isempty(obj.rfLibrary.keys)
76+ keys = obj.rfLibrary.keys;
77+ fwrite(fid, binaryCodes.section.rf, 'int64');
78+ fwrite(fid, length(keys), 'int64');
79+ for k = keys
80+ data = obj.rfLibrary.data(k).array;
81+ fwrite(fid, k, 'int32');
82+ fwrite(fid, data(1), 'float64'); % amp
83+ fwrite(fid, data(2:4), 'int32'); % mag_id, phase_id, time_shape_id
84+ fwrite(fid, round(data(5)*1e12), 'int64'); % center (ps)
85+ fwrite(fid, round(data(6)*1e12), 'int64'); % delay (ps)
86+ fwrite(fid, data(7:10), 'float64'); % freqPPM, phasePPM, freq, phase
87+ fwrite(fid, obj.rfLibrary.type(k), 'char'); % use
88+ end
89+end
90+
91+arbGradMask = obj.gradLibrary.type=='g';
92+trapGradMask = obj.gradLibrary.type=='t';
93+
94+% Arbitrary gradients: amp(f64) first(f64) last(f64) amp_shape_id(i32)
95+% time_shape_id(i32) delay(i32,us)
96+% array layout: [amp first last amp_shape_id time_shape_id delay]
97+if any(arbGradMask)
98+ keys = obj.gradLibrary.keys;
99+ fwrite(fid, binaryCodes.section.gradients, 'int64');
100+ fwrite(fid, length(keys(arbGradMask)), 'int64');
101+ for k = keys(arbGradMask)
102+ data = obj.gradLibrary.data(k).array;
103+ fwrite(fid, k, 'int32');
104+ fwrite(fid, data(1:3), 'float64'); % amp, first, last
105+ fwrite(fid, data(4:5), 'int32'); % amp_shape_id, time_shape_id
106+ fwrite(fid, round(data(6)*1e12), 'int64'); % delay (ps)
107+ end
108+end
109+
110+% Trapezoid gradients: amp(f64) rise(i64,us) flat(i64,us) fall(i64,us) delay(i64,us)
111+% array layout: [amp rise flat fall delay]
112+if any(trapGradMask)
113+ keys = obj.gradLibrary.keys;
114+ fwrite(fid, binaryCodes.section.trapezoids, 'int64');
115+ fwrite(fid, length(keys(trapGradMask)), 'int64');
116+ for k = keys(trapGradMask)
117+ data = obj.gradLibrary.data(k).array;
118+ fwrite(fid, k, 'int32');
119+ fwrite(fid, data(1), 'float64'); % amp
120+ fwrite(fid, data(2:5)*1e12, 'int64'); % rise, flat, fall, delay (ps)
121+ end
122+end
123+
124+% ADC: num(i64) dwell(i64,ns) delay(i64,us) freqPPM(f64) phasePPM(f64)
125+% freq(f64) phase(f64) phase_id(i32)
126+% array layout: [num dwell delay freqPPM phasePPM freq phase phase_id]
127+if ~isempty(obj.adcLibrary.keys)
128+ keys = obj.adcLibrary.keys;
129+ fwrite(fid, binaryCodes.section.adc, 'int64');
130+ fwrite(fid, length(keys), 'int64');
131+ for k = keys
132+ data = obj.adcLibrary.data(k).array;
133+ fwrite(fid, k, 'int32');
134+ fwrite(fid, data(1), 'int64'); % num
135+ fwrite(fid, round(data(2)*1e12), 'int64'); % dwell (ps)
136+ fwrite(fid, round(data(3)*1e12), 'int64'); % delay (ps)
137+ fwrite(fid, data(4:7), 'float64'); % freqPPM, phasePPM, freq, phase
138+ fwrite(fid, data(8), 'int32'); % phase_id
139+ end
140+end
141+
142+if ~isempty(obj.shapeLibrary.keys)
143+ keys = obj.shapeLibrary.keys;
144+ fwrite(fid, binaryCodes.section.shapes, 'int64');
145+ fwrite(fid, length(keys), 'int64');
146+ for k = keys
147+ shape = obj.shapeLibrary.data(k).array;
148+ num_samples = shape(1);
149+ data = shape(2:end);
150+ fwrite(fid, k, 'int32');
151+ fwrite(fid, num_samples, 'int64'); % num uncompressed
152+ fwrite(fid, length(data), 'int64'); % num compressed
153+ fwrite(fid, data, 'float32');
154+ end
155+end
156+
157+% Extensions: id(i32) type(i32) ref(i32) next_id(i32)
158+if ~isempty(obj.extensionLibrary.keys)
159+ keys = obj.extensionLibrary.keys;
160+ fwrite(fid, binaryCodes.section.extensions, 'int64');
161+ fwrite(fid, length(keys), 'int64');
162+ for k = keys
163+ fwrite(fid, k, 'int32');
164+ fwrite(fid, round(obj.extensionLibrary.data(k).array), 'int32'); % type, ref, next_id
165+ end
166+end
167+
168+% Triggers: id(i32) type(i32) channel(i32) delay(i64,ps) duration(i64,ps)
169+if ~isempty(obj.trigLibrary.keys)
170+ keys = obj.trigLibrary.keys;
171+ fwrite(fid, binaryCodes.section.triggers, 'int64');
172+ fwrite(fid, obj.getExtensionTypeID('TRIGGERS'), 'int32'); % extension type ID
173+ fwrite(fid, length(keys), 'int64');
174+ for k = keys
175+ data = obj.trigLibrary.data(k).array;
176+ fwrite(fid, k, 'int32');
177+ fwrite(fid, data(1:2), 'int32'); % type, channel
178+ fwrite(fid, round(data(3:4)*1e12), 'int64'); % delay, duration (ps)
179+ end
180+end
181+
182+% Labels (LABELSET and LABELINC): id(i32) value(i32) label_index(i32)
183+if ~isempty(obj.labelsetLibrary.keys)
184+ keys = obj.labelsetLibrary.keys;
185+ fwrite(fid, binaryCodes.section.labelset, 'int64');
186+ fwrite(fid, obj.getExtensionTypeID('LABELSET'), 'int32'); % extension type ID
187+ fwrite(fid, length(keys), 'int64');
188+ for k = keys
189+ data = obj.labelsetLibrary.data(k).array;
190+ fwrite(fid, k, 'int32');
191+ fwrite(fid, data(1:2), 'int32'); % value, label_index
192+ end
193+end
194+
195+if ~isempty(obj.labelincLibrary.keys)
196+ keys = obj.labelincLibrary.keys;
197+ fwrite(fid, binaryCodes.section.labelinc, 'int64');
198+ fwrite(fid, obj.getExtensionTypeID('LABELINC'), 'int32'); % extension type ID
199+ fwrite(fid, length(keys), 'int64');
200+ for k = keys
201+ data = obj.labelincLibrary.data(k).array;
202+ fwrite(fid, k, 'int32');
203+ fwrite(fid, data(1:2), 'int32'); % value, label_index
204+ end
205+end
206+
207+% Soft delays: id(i32) num(i32) offset(i64,ps) factor(f64) hint_len(i32) hint(chars)
208+if ~isempty(obj.softDelayLibrary.keys)
209+ keys = obj.softDelayLibrary.keys;
210+ fwrite(fid, binaryCodes.section.softdelays, 'int64');
211+ fwrite(fid, obj.getExtensionTypeID('DELAYS'), 'int32'); % extension type ID
212+ fwrite(fid, length(keys), 'int64');
213+ for k = keys
214+ data = obj.softDelayLibrary.data(k).array;
215+ hint_str = obj.softDelayHints2{data(4)};
216+ fwrite(fid, k, 'int32');
217+ fwrite(fid, data(1), 'int32'); % num
218+ fwrite(fid, round(data(2)*1e12), 'int64'); % offset (ps)
219+ fwrite(fid, data(3), 'float64'); % factor
220+ fwrite(fid, length(hint_str), 'int32'); % hint string length
221+ fwrite(fid, hint_str, 'char'); % hint string (no null terminator)
222+ end
223+end
224+
225+% RF shims: id(i32) num_chan(i32) mag_c1(f64) phase_c1(f64) ...
226+if ~isempty(obj.rfShimLibrary.keys)
227+ keys = obj.rfShimLibrary.keys;
228+ fwrite(fid, binaryCodes.section.rfshims, 'int64');
229+ fwrite(fid, obj.getExtensionTypeID('RF_SHIMS'), 'int32'); % extension type ID
230+ fwrite(fid, length(keys), 'int64');
231+ for k = keys
232+ chan_data = obj.rfShimLibrary.data(k).array;
233+ fwrite(fid, k, 'int32');
234+ fwrite(fid, length(chan_data)/2, 'int32'); % num channels
235+ fwrite(fid, chan_data, 'float64'); % mag/phase pairs
236+ end
237+end
238+
239+% Rotations: id(i32) q0(f64) qx(f64) qy(f64) qz(f64)
240+if ~isempty(obj.rotationLibrary.keys)
241+ keys = obj.rotationLibrary.keys;
242+ fwrite(fid, binaryCodes.section.rotations, 'int64');
243+ fwrite(fid, obj.getExtensionTypeID('ROTATIONS'), 'int32'); % extension type ID
244+ fwrite(fid, length(keys), 'int64');
245+ for k = keys
246+ fwrite(fid, k, 'int32');
247+ fwrite(fid, obj.rotationLibrary.data(k).array, 'float64'); % quaternion [q0 qx qy qz]
248+ end
249+end
250+
251+fclose(fid);
252+
253+if create_signature
254+ % sign the file (this version with re/loading the file is a factor 2 faster than the sprintf() based one that kept a memory-copy of the data written)
255+
256+ % re-open and read in the file
257+ fid=fopen(filename, 'r');
258+ buf=fread(fid);
259+ fclose(fid);
260+
261+ % calculate the digest
262+ md5hash=mr.aux.md5(buf);
263+ %fprintf('%s\n',md5hash);
264+
265+ % store the signature in the seq object
266+ obj.signatureType='md5';
267+ obj.signatureFile='bin';
268+ obj.signatureValue=md5hash;
269+
270+ % re-open the file for appending
271+ fid=fopen(filename, 'a');
272+ fseek(fid, 0, 'eof'); % Octave seems to need this inspite of 'a'
273+ fpos=ftell(fid);
274+ fwrite(fid, binaryCodes.section.signature, 'int64');
275+ % signature type: length,string
276+ fwrite(fid, length(obj.signatureType), 'int32');
277+ fwrite(fid, obj.signatureType, 'char');
278+ % signature: length,data (as bytes, not characters)
279+ fwrite(fid, length(obj.signatureValue)/2, 'int32');
280+ for i=1:length(obj.signatureValue)/2
281+ fwrite(fid, hex2dec(obj.signatureValue(i*2-1:i*2)), 'uint8');
282+ end
283+ % the original length of the file prior to adding the signature for easier signature validation : int64
284+ fwrite(fid,fpos,'int64');
285+ fclose(fid);
286+end
287+
288+end
src/engine/pulseq/+mr/@Sequence/write_v141.madded+264−0View file
@@ -0,0 +1,264 @@
1+function write_v141(obj,filename,create_signature)
2+%WRITE Write sequence to file.
3+% WRITE(seqObj, filename) Write the sequence data to the given
4+% filename using the open file format for MR sequences.
5+%
6+% Examples:
7+% Write the sequence file to the my_sequences directory, relying on the
8+% previous stable file format (v143).
9+%
10+% write_v143(seqObj,'my_sequences/gre.seq')
11+%
12+% See also write, read
13+
14+if (nargin<3)
15+ create_signature=true;
16+end
17+
18+fid=fopen(filename, 'w');
19+assert(fid ~= -1, 'Cannot open file: %s', filename);
20+fprintf(fid, '# Pulseq sequence file\n');
21+fprintf(fid, '# Created by MATLAB mr toolbox\n\n');
22+
23+% we always write files in the default current version, which may be
24+% differen to one, loaded (and stored in the seq object)
25+[version_major, version_minor, version_revision]=mr.aux.version('output');
26+fprintf(fid, '[VERSION]\n');
27+fprintf(fid, 'major %s\n', num2str(1));
28+fprintf(fid, 'minor %s\n', num2str(4));
29+fprintf(fid, 'revision %s\n', num2str(1));
30+fprintf(fid, '\n');
31+
32+if ~isempty(obj.definitions)
33+ fprintf(fid, '[DEFINITIONS]\n');
34+ keys = obj.definitions.keys;
35+ values = obj.definitions.values;
36+ for i=1:length(keys)
37+ fprintf(fid, '%s ', keys{i});
38+ if (ischar(values{i}))
39+ fprintf(fid, '%s ', values{i});
40+ else
41+ fprintf(fid, '%.9g ', values{i});
42+ end
43+ fprintf(fid, '\n');
44+ end
45+ fprintf(fid, '\n');
46+end
47+
48+fprintf(fid, '# Format of blocks:\n');
49+fprintf(fid, '# NUM DUR RF GX GY GZ ADC EXT\n');
50+fprintf(fid, '[BLOCKS]\n');
51+idFormatWidth = length(num2str(length(obj.blockEvents)));
52+idFormatStr = ['%' num2str(idFormatWidth) 'd'];
53+for i = 1:length(obj.blockEvents)
54+ %fprintf(fid,[idFormatStr ' %2d %2d %3d %3d %3d %2d 0\n'],[i obj.blockEvents(i,:)]);
55+ %fprintf(fid,[idFormatStr ' %2d %2d %3d %3d %3d %2d 0\n'],[i obj.blockEvents{i}]);
56+ bd=obj.blockDurations(i)/obj.blockDurationRaster;
57+ bdr=round(bd);
58+ assert(abs(bdr-bd)<1e-6); % this may still trigger false alarms for very long delays due to the limited accuracy of the double
59+ fprintf(fid,[idFormatStr ' %3d %3d %3d %3d %3d %2d %2d\n'], ...
60+ [i bdr obj.blockEvents{i}(2:end)]);
61+end
62+fprintf(fid, '\n');
63+
64+if ~isempty(obj.rfLibrary.keys)
65+ fprintf(fid, '# Format of RF events:\n');
66+ fprintf(fid, '# id amplitude mag_id phase_id time_shape_id delay freq phase\n');
67+ fprintf(fid, '# .. Hz .... .... .... us Hz rad\n');
68+ fprintf(fid, '[RF]\n');
69+ keys = obj.rfLibrary.keys;
70+ for k = keys
71+ libData1 = obj.rfLibrary.data(k).array(1:4);
72+ full_freqPhaseOffset=obj.rfLibrary.data(k).array(9:10)+obj.rfLibrary.data(k).array(7:8)*1e-6*obj.sys.gamma*obj.sys.B0; % convert freqPPM and phasePPM to fixed offsets
73+ delay = round(obj.rfLibrary.data(k).array(6)/obj.rfRasterTime)*obj.rfRasterTime*1e6; % a bit of a hack: round the delay
74+ fprintf(fid, '%d %12g %d %d %d %g %g %g\n', [k libData1 delay full_freqPhaseOffset]);
75+ end
76+ fprintf(fid, '\n');
77+end
78+
79+arbGradMask = obj.gradLibrary.type == 'g';
80+trapGradMask = obj.gradLibrary.type == 't';
81+
82+if any(arbGradMask)
83+ fprintf(fid, '# Format of arbitrary gradients:\n');
84+ fprintf(fid, '# time_shape_id of 0 means default timing (stepping with grad_raster starting at 1/2 of grad_raster)\n');
85+ fprintf(fid, '# id amplitude amp_shape_id time_shape_id delay\n'); % do we need delay ???
86+ fprintf(fid, '# .. Hz/m .. .. us\n');
87+ fprintf(fid, '[GRADIENTS]\n');
88+ keys = obj.gradLibrary.keys;
89+ for k = keys(arbGradMask)
90+ if obj.gradLibrary.data(k).array(5)<0
91+ error("Current sequence contains arbitrary gradients with oversampling, which are not compatible with the format 1.4.1");
92+ end
93+ fprintf(fid, '%d %12g %d %d %d\n', ...
94+ [k obj.gradLibrary.data(k).array([1 4 5]) ...
95+ round(obj.gradLibrary.data(k).array(6)*1e6)]);
96+ end
97+ fprintf(fid, '\n');
98+end
99+
100+if any(trapGradMask)
101+ fprintf(fid, '# Format of trapezoid gradients:\n');
102+ fprintf(fid, '# id amplitude rise flat fall delay\n');
103+ fprintf(fid, '# .. Hz/m us us us us\n');
104+ fprintf(fid, '[TRAP]\n');
105+ keys = obj.gradLibrary.keys;
106+ for k = keys(trapGradMask)
107+ data = obj.gradLibrary.data(k).array;
108+ data(2:end) = round(1e6*data(2:end));
109+ fprintf(fid, '%2d %12g %3d %4d %3d %3d\n', [k data]);
110+ end
111+ fprintf(fid, '\n');
112+end
113+
114+if ~isempty(obj.adcLibrary.keys)
115+ fprintf(fid, '# Format of ADC events:\n');
116+ fprintf(fid, '# id num dwell delay freq phase\n');
117+ fprintf(fid, '# .. .. ns us Hz rad\n');
118+ fprintf(fid, '[ADC]\n');
119+ keys = obj.adcLibrary.keys;
120+ for k = keys
121+ data = [obj.adcLibrary.data(k).array(1:3) obj.adcLibrary.data(k).array(6:7)+obj.adcLibrary.data(k).array(4:5)*1e-6*obj.sys.gamma*obj.sys.B0].*[1 1e9 1e6 1 1]; % convert freqPPM and phasePPM to fixed offsets
122+ fprintf(fid, '%d %d %.0f %.0f %g %g\n', [k data]);
123+ end
124+ fprintf(fid, '\n');
125+end
126+
127+%if ~isempty(obj.delayLibrary.keys)
128+% fprintf(fid, '# Format of delays:\n');
129+% fprintf(fid, '# id delay (us)\n');
130+% fprintf(fid, '[DELAYS]\n');
131+% keys = obj.delayLibrary.keys;
132+% for k = keys
133+% fprintf(fid, '%d %d\n', ...
134+% [k round(1e6*obj.delayLibrary.data(k).array)]);
135+% end
136+% fprintf(fid, '\n');
137+%end
138+
139+if ~isempty(obj.extensionLibrary.keys)
140+ fprintf(fid, '# Format of extension lists:\n');
141+ fprintf(fid, '# id type ref next_id\n');
142+ fprintf(fid, '# next_id of 0 terminates the list\n');
143+ fprintf(fid, '# Extension list is followed by extension specifications\n');
144+ fprintf(fid, '[EXTENSIONS]\n');
145+ keys = obj.extensionLibrary.keys;
146+ for k = keys
147+ fprintf(fid, '%d %d %d %d\n', ...
148+ [k round(obj.extensionLibrary.data(k).array)]);
149+ end
150+ fprintf(fid, '\n');
151+end
152+
153+if ~isempty(obj.trigLibrary.keys)
154+ fprintf(fid, '# Extension specification for digital output and input triggers:\n');
155+ fprintf(fid, '# id type channel delay (us) duration (us)\n');
156+% fprintf(fid, 'extension TRIGGERS 1\n'); % fixme: extension ID 1 is hardcoded here for triggers
157+ fprintf(fid, ['extension TRIGGERS ',num2str(obj.getExtensionTypeID('TRIGGERS')),'\n']);
158+
159+ keys = obj.trigLibrary.keys;
160+ for k = keys
161+ fprintf(fid, '%d %d %d %d %d\n', ...
162+ [k round(obj.trigLibrary.data(k).array.*[1 1 1e6 1e6])]);
163+ end
164+ fprintf(fid, '\n');
165+end
166+
167+if ~isempty(obj.labelsetLibrary.keys) || ~isempty(obj.labelincLibrary.keys)
168+ lbls=mr.getSupportedLabels();
169+
170+ if ~isempty(obj.labelsetLibrary.keys)
171+ fprintf(fid, '# Extension specification for setting labels:\n');
172+ fprintf(fid, '# id set labelstring\n');
173+ tid=obj.getExtensionTypeID('LABELSET');
174+ fprintf(fid, ['extension LABELSET ',num2str(tid),'\n']);
175+ keys = obj.labelsetLibrary.keys;
176+ for k = keys
177+ fprintf(fid, '%d %d %s\n', ...
178+ k, obj.labelsetLibrary.data(k).array(1),lbls{obj.labelsetLibrary.data(k).array(2)});
179+ end
180+ fprintf(fid, '\n');
181+ end
182+ if ~isempty(obj.labelincLibrary.keys)
183+ fprintf(fid, '# Extension specification for increasing labels:\n');
184+ fprintf(fid, '# id inc labelstring\n');
185+ tid=obj.getExtensionTypeID('LABELINC');
186+ fprintf(fid, ['extension LABELINC ',num2str(tid),'\n']);
187+ lbls=mr.getSupportedLabels();
188+ keys = obj.labelincLibrary.keys;
189+ for k = keys
190+ fprintf(fid, '%d %d %s\n', ...
191+ k, obj.labelincLibrary.data(k).array(1),lbls{obj.labelincLibrary.data(k).array(2)});
192+ end
193+ fprintf(fid, '\n');
194+ end
195+end
196+
197+if ~isempty(obj.softDelayLibrary.keys)
198+ warning('WARNING! The sequence in memory uses ''soft delay'' extension, which is incompatible with the file format v1.4.1. The produced Pulseq file is only partially valid and may fail to load or operate in some cases');
199+end
200+
201+if ~isempty(obj.rotationLibrary.keys)
202+ error('WARNING! The sequence in memory uses the ''rotations'' extension, which is incompatible with the file format v1.4.1. The produced Pulseq file is likely to be invalid and would probably fail to operate');
203+end
204+
205+if ~isempty(obj.shapeLibrary.keys)
206+ fprintf(fid, '# Sequence Shapes\n');
207+ fprintf(fid, '[SHAPES]\n\n');
208+ keys = obj.shapeLibrary.keys;
209+ for k = keys
210+ shape_dat = obj.shapeLibrary.data(k).array;
211+ fprintf(fid, 'shape_id %d\n', k);
212+ fprintf(fid, 'num_samples %d\n', shape_dat(1));
213+ fprintf(fid, '%.9g\n', shape_dat(2:end));
214+ fprintf(fid, '\n');
215+ end
216+end
217+
218+fclose(fid);
219+
220+if create_signature
221+ % sign the file (this version with re/loading the file is a factor 2 faster than the sprintf() based one that kept a memory-copy of the data written)
222+
223+ % re-open and read in the file
224+ fid=fopen(filename, 'r');
225+ buf=fread(fid);
226+ fclose(fid);
227+
228+ % calculate the digest
229+ if mr.aux.isOctave()
230+ md5hash=hash('MD5',char(buf(:)')); % Octave-specific function
231+ else
232+ md5hash=md5_java(buf); % Matlab Java hack
233+ end
234+ %fprintf('%s\n',md5hash);
235+
236+ % store the signature in the object
237+ obj.signatureType='md5';
238+ obj.signatureFile='text';
239+ obj.signatureValue=md5hash;
240+
241+ % re-open the file for appending
242+ fid=fopen(filename, 'a');
243+ fprintf(fid, '\n[SIGNATURE]\n'); % the preceding new line BELONGS to the signature (and needs to be sripped away to recalculate the signature)
244+ fprintf(fid, '# This is the hash of the Pulseq file, calculated right before the [SIGNATURE] section was added\n');
245+ fprintf(fid, '# It can be reproduced/verified with md5sum if the file trimmed to the position right above [SIGNATURE]\n');
246+ fprintf(fid, '# The new line character preceding [SIGNATURE] BELONGS to the signature (and needs to be sripped away for recalculating/verification)\n');
247+ fprintf(fid, 'Type md5\n');
248+ fprintf(fid, 'Hash %s\n', md5hash);
249+ fclose(fid);
250+end
251+
252+end
253+
254+function out=md5_java(buf)
255+ import java.security.*;
256+ import java.math.*;
257+ import java.lang.String;
258+
259+ md = MessageDigest.getInstance('MD5');
260+ hash = md.digest(double(buf));
261+ bi = BigInteger(1, hash);
262+
263+ out=char(String.format('%032x', bi));
264+end
src/engine/pulseq/+mr/@TransformFOV/TransformFOV.madded+609−0View file
@@ -0,0 +1,609 @@
1+classdef TransformFOV < handle
2+
3+ properties (Access = public)
4+ rotation=[]; % 3x3 rotation matrix or empty matrix if none
5+ translation=[]; % 1x3 vector or empty matrix if none
6+ scale=[]; % 1x3 vector or empty if none
7+ end
8+
9+ properties (Access = private)
10+ use_rotation_extension=false; % whether rotations should be explicitly applied to the gradients or implicitly by means of RotationExtension
11+ prior_phase_cycle=0;
12+ rotation_quaternion=[]; % rotation quaternion version of the provided rotation matrix or empty matrix if none
13+ system;
14+ % high_accuracy;
15+ labels=struct('NOPOS',0,'NOROT',0,'NOSCL',0);
16+ end
17+
18+ methods
19+
20+ function obj = TransformFOV(varargin)
21+ % Creates an instance of the TransformFOV object, which can
22+ % then be applied to shift, rotate or scale the imaging volume
23+ % either of a single block, range of blocks or the entire
24+ % sequence. The optional input parameters are:
25+ % rotation: 3x3 rotation matrix
26+ % translation: 1x3 translation vector in the original
27+ % (non-rotated) logical Pulseq coordinates
28+ % scale: 1x3 scailing vector in the original
29+ % (non-rotated) logical Pulseq coordinates. The
30+ % provided vector is the *gradient* scailing
31+ % vector, so the FOV size is *divided* by the
32+ % provided values. Scaling of 0 on one or
33+ % several axes sets gradients on these axes to 0
34+ % transform: 4x4 homogeneous transform matrix, used as an
35+ % alternative to a combination of 'rotation' and
36+ % 'translation'. If 'tansform' is specified
37+ % neither 'translation' nor 'rotation' can be
38+ % used. However, 'scale' parameter can be used
39+ % in combination with 'transform'. In contrast
40+ % to 'translation', the translation part of the
41+ % 'transform' matrix is in the world/lab (e.g.
42+ % new, rotated) coordinates in accordance with
43+ % the standard homogeneous transform definition
44+ % use_rotation_extension: defines whether the gradient events
45+ % are rotated immediately by the function, or
46+ % whether the rotation extension should be used
47+ % and the actual rotation will then be applied
48+ % by the interpreter. If the input sequence
49+ % readily uses rotation extension and
50+ % 'use_rotation_extension' is False, the
51+ % gradient events will be transformed by the
52+ % stored rotation extension information prior to
53+ % further calculations
54+ % prior_phase_cycle: allows to specify previously accumulated
55+ % phase, e.g. for combining different instances
56+ % of the FOV position classes
57+ %
58+ % The order of transformations is as follows:
59+ % scale, translation, rotation. Translation and rotation can be
60+ % mixed in into the homogeneous transform matrix, but that
61+ % must exclude scaling, which then may still be provided
62+ % separately
63+ %
64+
65+ persistent parser
66+ if isempty(parser)
67+ parser = inputParser;
68+ parser.FunctionName = 'TransformFOV_f';
69+
70+ addParameter(parser, 'rotation', [], @(m) (isnumeric(m) && all(size(m)==[3 3])));
71+ addParameter(parser, 'translation', [], @(m) (isnumeric(m) && size(m,2)==3 && length(m)==3));
72+ addParameter(parser, 'scale', [], @(m) (isnumeric(m) && size(m,2)==3 && length(m)==3));
73+ addParameter(parser, 'transform', [], @(m) (isnumeric(m) && all(size(m)==[4 4])));
74+ addParameter(parser, 'use_rotation_extension', false, @(m) (islogical(m) && numel(m)==1));
75+ addParameter(parser, 'prior_phase_cycle', 0, @(m) (isnumeric(m) && numel(m)==1));
76+ addParameter(parser, 'system', [], @isstruct);
77+ end
78+
79+ parse(parser, varargin{:});
80+ opt = parser.Results;
81+
82+ if ~isempty(opt.transform)
83+ if ~isempty(opt.rotation) || ~isempty(opt.translation)
84+ error('Neither ''translation'' nor ''rotation'' can be provided in combination with the ''transfrom'' option');
85+ end
86+ opt.rotation=opt.transform(1:3,1:3);
87+ off=opt.transform(4,1:3); % TODO: check whether this is indeed the column
88+ opt.translation=opt.rotation*off'; % TODO: check the direction of the rotation (or inverse)
89+ %off=opt.transform(1:3,4); % alternative - translation column of the standard 4x4 homogeneous matrix
90+ %opt.translation=(opt.rotation*off)'; % TODO: check the direction of the rotation (or inverse)
91+ elseif isempty(opt.rotation) && isempty(opt.translation) && isempty(opt.scale)
92+ error('At least one transforming parameter needs to be provided');
93+ end
94+
95+ obj.rotation=opt.rotation;
96+ obj.translation=opt.translation;
97+ obj.scale=opt.scale;
98+ obj.prior_phase_cycle=opt.prior_phase_cycle;
99+ obj.use_rotation_extension=opt.use_rotation_extension;
100+
101+ if obj.use_rotation_extension && ~isempty(obj.rotation)
102+ obj.rotation_quaternion=mr.aux.quat.fromRotMat(obj.rotation);
103+ end
104+
105+ if isempty(opt.system)
106+ obj.system=mr.opts();
107+ else
108+ obj.system=opt.system;
109+ end
110+ end
111+
112+ function out=applyToBlock(obj, varargin)
113+
114+ % convert the input into a plain cell array of events
115+ if ~any(iscell(varargin))
116+ block_events=varargin;
117+ else
118+ block_events={};
119+ for i=varargin
120+ if iscell(i)
121+ block_events=[block_events i];
122+ else
123+ block_events{end+1}=i;
124+ end
125+ end
126+ end
127+
128+ % see if we've got a block as a single struct, then convert it to a cell array
129+ if (isstruct(block_events) && isfield(block_events, 'blockDuration')) || ...
130+ (iscell(block_events) && ~isempty(block_events) && isstruct(block_events{1}) && isfield(block_events{1}, 'blockDuration'))
131+ block_events=mr.block2events(block_events);
132+ end
133+
134+ % extract various mr events including {rf,adc,gx,gy,gz} from "block_events" input
135+ rf = [];
136+ adc = [];
137+ grads = cell(1,3);
138+ other = {};
139+ rotExtQuaternion = [];
140+ for i = 1:length(block_events)
141+ e=block_events{i};
142+ if length(e)==1 && isstruct(e) && isfield(e, 'type')
143+ switch e.type
144+ case 'rf'
145+ rf = e; % save rf event
146+ case 'adc'
147+ adc = e; % save adc event
148+ case {'trap', 'grad'} % if gradient event, check 'channel'
149+ if ~isfield(e, 'channel')
150+ error('unspecified gradient channel for the gradient object');
151+ end
152+ switch e.channel
153+ case 'x'
154+ grads{1} = e;
155+ case 'y'
156+ grads{2} = e;
157+ case 'z'
158+ grads{3} = e;
159+ otherwise
160+ error('unsupported gradient channel %s for the gradient object', e.channel);
161+ end
162+ case 'labelset' %{'labelset', 'labelinc'} % we dont really need 'labelinc', as all labels that are important for us are flags and have no 'inc'
163+ for j=1:length(e)
164+ switch e(j).label % this switch has only one case on purpose, it is just a lazy way of checking that we deal with a relevant label setting
165+ case {'NOPOS','NOROT','NOSCL'}
166+ obj.labels(e(j).label)=e(j).value;
167+ end
168+ end
169+ case 'rot3D'
170+ rotExtQuaternion=e.rotQuaternion;
171+ otherwise
172+ other{end+1}=e;
173+ end
174+ else
175+ other{end+1}=e;
176+ %other=[other num2cell(e)]; % e can be an array of structs, and funny enough, num2cell can convert it to a cell array...
177+ end
178+ end
179+
180+ gradRasterTime = obj.system.gradRasterTime;
181+
182+ %% scale (apply it first prior to any other transformation)
183+ if ~isempty(obj.scale)
184+ %channel2index=struct('x',1,'y',2,'z',3);
185+ for i=1:length(grads)
186+ %grads{i}=mr.scaleGrad(grads{i},obj.scale(channel2index.(grads{i}.channel)),obj.system);
187+ if ~isempty(grads{i})
188+ if (isfield(grads{i},'id'))
189+ grads{i}=mr.scaleGrad(rmfield(grads{i},'id'),obj.scale(i),obj.system);
190+ else
191+ grads{i}=mr.scaleGrad(grads{i},obj.scale(i),obj.system);
192+ end
193+ end
194+ end
195+ end
196+
197+ %% translation
198+ if ~isempty(obj.translation)
199+ % big picture of the algorithm
200+
201+ % if ~isempty(obj.translation)
202+ % phase_cycle_this_block = 0
203+ % if NOPOS==0
204+ % apply prior_phase_cycle to rf-adc
205+ % end
206+ % for i=1:3
207+ % g = grad{i};
208+ % if ~isempty(g)
209+ % if translation(i)~=0
210+ % generate piecewise polynomial of the current gradient
211+ % if NOPOS==0
212+ % apply phase and freq offsets to rf-adc by the current gradient
213+ % end
214+ % update phase_cycle_this_block by the current gradient
215+ % end
216+ % end
217+ % end
218+ % update prior_phase_cycle =+ phase_cycle_this_block
219+ % end
220+
221+ % extract the first and the last time points of possible rf or adc in the block
222+ if isempty(rf)
223+ if isempty(adc)
224+ % both ADC and RF are not defined
225+ t_end = [];
226+ t_start = [];
227+ else
228+ % only ADC is defined
229+ [t_start,t_end] = extract_time(adc);
230+ end
231+ else % RF is defined
232+ if isempty(adc)
233+ % only RF is defined
234+ [t_start,t_end] = extract_time(rf);
235+ else
236+ % both ADC and RF are defined
237+ [t_start_adc,t_end_adc] = extract_time(adc);
238+ [t_start_rf,t_end_rf] = extract_time(rf);
239+ t_start = min(t_start_adc,t_start_rf);
240+ t_end = max(t_end_adc,t_end_rf);
241+ end
242+ end
243+
244+ % remove IDs because we will change the objects below
245+ if ~isempty(rf) && isfield(rf,'id')
246+ rf=rmfield(rf,'id');
247+ end
248+ if ~isempty(adc) && isfield(adc,'id')
249+ adc=rmfield(adc,'id');
250+ end
251+
252+ % if the current block uses rotation extension and the
253+ % TransformFOV object is configured not to use the rotation
254+ % extension then we apply the rotation to the gradients now
255+ if ~obj.use_rotation_extension && ~isempty(rotExtQuaternion)
256+ grads=mr.rotate3D(rotExtQuaternion,grads,'system',obj.sys);
257+ rotExtQuaternion=[]; % now that we have applied the current rotation, we can discard it
258+ end
259+
260+ % MZ: I think we have to rotate the gradient "backwards" if
261+ % this block has 'NOROT'. We restore the grads object below
262+ % WARNING: I don't think this is compatible with obj.use_rotation_extension
263+ if obj.labels.NOROT
264+ grads_backup=grads;
265+ % MZ: HA! we could rotate obj.translation (or it's copy) in the opposite direction instead
266+ % MZ: and, we could use the same mechanism to handle the rotation extention
267+ grads=mr.rotate3D(obj.rotation',grads,'system',obj.sys); % MZ: I guess we have to rotate the gradients "back" because we are normally in local logical coordinates, which would be "rotated" if there were NOROT flag
268+ % MZ: please check if the above point is correct
269+ end
270+
271+ % define a temporary parameter for the phase cycle of the current block
272+ phase_cycle_this_block=0;
273+
274+ % apply prior_phase_cycle to rf-adc only if NOPOS==0
275+ if ~obj.labels.NOPOS % do fov positioning for the current block
276+ % if there is rf in the current block, adjust its
277+ % phase-offset
278+ if ~isempty(rf)
279+ rf.phaseOffset = rf.phaseOffset + 2*pi * obj.prior_phase_cycle;
280+ end
281+ % if there is adc in the current block, adjust its
282+ % phase-offset
283+ if ~isempty(adc)
284+ adc.phaseOffset = adc.phaseOffset + 2*pi * obj.prior_phase_cycle;
285+ end
286+ end
287+
288+ % 1- take all gradintes in the current block and make
289+ % piecewise polynimials for them
290+ % 2- only if NOPOS==0 , calculate rf and adc phase and frequency offsets based on
291+ % the single gradient
292+ % 3- update the value of 'phase_cycle_this_block'
293+ % 4- update the value of 'prior_phase_cycle'
294+ for i=1:3
295+ if abs(obj.translation(i))>eps % check whether there is shift in the specific direction (x or y or z)
296+ g = grads{i};
297+ if ~isempty(g) % check whether there is corresponding gradient in the direction of the shift (gx or gy or gz)
298+ % 1- make pp
299+ if strcmp(g.type , 'trap') % if g is a simple trapezoid
300+ if (abs(g.flatTime)>eps) % interp1 gets confused by triangular gradients (repeating sample)
301+ tt = g.delay+cumsum([0 g.riseTime g.flatTime g.fallTime]);
302+ waveform = g.amplitude*[0 1 1 0];
303+ else
304+ if (abs(g.riseTime)>eps && abs(g.fallTime)>eps) % we skip 'empty' gradients
305+ tt = g.delay+cumsum([0 g.riseTime g.fallTime]);
306+ waveform = g.amplitude*[0 1 0];
307+ else
308+ if abs(g.amplitude)>eps
309+ warning('''empty'' gradient with non-zero magnitude detected');
310+ end
311+ end
312+ end
313+ else % if g is a extended trapezoid or arbitrary gradient
314+ tt = g.delay + g.tt;
315+ waveform = g.waveform;
316+ end
317+
318+ % generate breaks and coefs of the gradient required for
319+ % making piecewise polynomial
320+ [breaks, coefs, tt_extended, waveform_extended] = generate_breaks_coefs(g, tt, waveform, gradRasterTime, t_start, t_end);
321+
322+ % make piecewise polynomial for the gradient
323+ f_pp = mkpp(breaks, coefs);
324+ % integrate it analytically
325+ if mr.aux.isOctave()
326+ fi_pp = ppint(f_pp);
327+ else
328+ fi_pp = fnint(f_pp); % MZ: TODO: check whether we need more accurate functions here
329+ end
330+
331+
332+ if ~obj.labels.NOPOS
333+ % apply adc or rf phase and freq offset contribution by only one (current) gradient
334+ % (we separate all gradients in the block and apply effect of each one to the offsets)
335+ event = {rf,adc};
336+ for j=1:length(event)
337+ e = event{j};
338+ if ~isempty(e)
339+ [t_s, t_e] = extract_time(e); % find the first and last time point of the rf or adc event
340+ is_const = is_grad_const(tt_extended, waveform_extended, t_s, t_e); % check whether the gradient is constant during the rf or adc event: 1 means constant
341+ if is_const % in case of constant gradient, we can easily adjust the frequency and phase offset of the rf or ADC event
342+ freq = obj.translation(i) * ppval(f_pp, t_s);
343+ if isfield(e,'t') %e=="rf"
344+ rf.freqOffset = rf.freqOffset + freq;
345+ phase_cycle = local_frac( accurate_mod_pp(fi_pp.breaks, fi_pp.coefs, t_s, obj.translation(i)) - freq * (t_s-rf.delay) );
346+ rf.phaseOffset = rf.phaseOffset + 2*pi*phase_cycle;
347+ else %if e=="adc"
348+ adc.freqOffset = adc.freqOffset + freq;
349+ phase_cycle = local_frac( accurate_mod_pp(fi_pp.breaks, fi_pp.coefs, t_s, obj.translation(i)) - freq * (t_s-adc.delay) );
350+ adc.phaseOffset = adc.phaseOffset + 2*pi* phase_cycle;
351+ end
352+ else
353+ % in case of non-constant gradient, we should calculate a phase vector for rf or ADC. for rf event we can easily add
354+ % the phase vector to rf.signal and for ADC event we store the phase vector and use it in image reconstruction
355+ if isfield(e,'t') %e=="rf"
356+ % calculate the frequency at the center
357+ ppval_f_center=ppval(f_pp, rf.delay+rf.center);
358+ freq = obj.translation(i) * ppval_f_center;
359+ rf.freqOffset = rf.freqOffset + freq;
360+ % ppval_fi_center = ppval(fi_pp, rf.delay+rf.center);
361+ ppval_fi_center_shift = accurate_mod_pp(fi_pp.breaks, fi_pp.coefs, rf.delay+rf.center, obj.translation(i));
362+ phase_cycle = local_frac(ppval_fi_center_shift - freq*rf.center);
363+ rf.phaseOffset = rf.phaseOffset + 2*pi*phase_cycle;
364+ phase_cycle_vector_tmp = accurate_mod_pp(fi_pp.breaks, fi_pp.coefs, rf.t+rf.delay, obj.translation(i));
365+ phase_cycle_vector = local_frac( phase_cycle_vector_tmp -ppval_f_center*(rf.t-rf.center) * obj.translation(i) - ppval_fi_center_shift );
366+ rf.signal = rf.signal .* exp(1i*2*pi*phase_cycle_vector);
367+ else % if e=="adc"
368+ % calculate the frequency at the center
369+ adc_center=0.5*adc.dwell*adc.numSamples;
370+ ppval_f_center=ppval(f_pp, adc.delay+adc_center);
371+ freq = obj.translation(i) * ppval_f_center;
372+ adc.freqOffset = adc.freqOffset + freq;
373+ % ppval_fi_center=ppval(fi_pp, adc.delay+adc_center);
374+ ppval_fi_center_shift = accurate_mod_pp(fi_pp.breaks, fi_pp.coefs, adc.delay+adc_center, obj.translation(i));
375+ % these -0.5 and +0.5 are needed to avoid unnecessay jumps for values that are very close to 0s
376+ phase_cycle = local_frac( -0.5 + local_frac(0.5 + ppval_fi_center_shift - freq*adc_center) );
377+ adc.phaseOffset = adc.phaseOffset + 2*pi*phase_cycle;
378+ adc_t=adc.dwell*(0.5:adc.numSamples-0.5);
379+ % these -0.5 and +0.5 are needed to avoid unnecessay jumps for values that are very close to 0s
380+ phase_cycle_vector_tmp = accurate_mod_pp(fi_pp.breaks, fi_pp.coefs, adc_t+adc.delay, obj.translation(i));
381+ phase_cycle_vector = local_frac( ( -0.5 + local_frac(0.5 - ppval_f_center*(adc_t-adc_center) ) ) * obj.translation(i) + ppval_fi_center_shift + phase_cycle_vector_tmp );
382+ % phase_cycle_vector = accurate_mod_pp(fi_pp.breaks, fi_pp.coefs, adc.dwell*(0:adc.numSamples-1)+adc.delay, obj.translation(i));
383+ if isempty(adc.phaseModulation)
384+ adc.phaseModulation=2*pi* phase_cycle_vector(:); % store residual adc phase for image reconstruction
385+ else
386+ adc.phaseModulation=adc.phaseModulation+2*pi* phase_cycle_vector(:); % store residual adc phase for image reconstruction
387+ end
388+ end
389+ end
390+ end
391+ end
392+ end
393+
394+ % update the phase_cycle_this_block by only the current gradient
395+ % phase_cycle_this_block = local_frac( phase_cycle_this_block + local_frac( g.area * obj.translation(i)));
396+ phase_cycle_this_block = local_frac( phase_cycle_this_block + accurate_mod_pp(fi_pp.breaks, fi_pp.coefs, tt_extended(end), obj.translation(i)) );
397+ end
398+ end
399+ end
400+
401+ % MZ: now restore the grads object
402+ if obj.labels.NOROT
403+ grads=grads_backup;
404+ end
405+
406+ % now update the phase stored for the next block
407+ obj.prior_phase_cycle = local_frac( obj.prior_phase_cycle + phase_cycle_this_block );
408+ end
409+
410+ %% rotation
411+ if ~isempty(obj.rotation) && ~obj.labels.NOROT
412+ if obj.use_rotation_extension
413+ if isempty(rotExtQuaternion)
414+ rotExtQuaternion=obj.rotation_quaternion;
415+ else
416+ rotExtQuaternion=mr.aux.quat.multiply(rotExtQuaternion, obj.rotation_quaternion); % TODO: check left or right rotation
417+ end
418+ else
419+ grads = mr.rotate3D(obj.rotation,grads,'system',obj.system);
420+ end
421+ end
422+
423+ %% rotation extension support
424+ if ~isempty(rotExtQuaternion)
425+ other{end+1}=mr.makeRotation(rotExtQuaternion);
426+ end
427+
428+ %% output
429+ out=[{rf} {adc} grads(:)' other(:)'];
430+ out=out(~cellfun(@isempty,out)); % clean up empty
431+ end
432+
433+ function seq2 = applyToSeq(obj, seq, varargin)
434+
435+ persistent parser
436+ if isempty(parser)
437+ parser = inputParser;
438+ parser.FunctionName = 'applyToSeq';
439+
440+ %parser.addRequired('seq');
441+ parser.addParamValue('sameSeq', false, @islogical); % TODO: add another option for an in-place transform
442+ parser.addParamValue('blockRange',[1 inf],@(x)(isnumeric(x) && length(x)==2));
443+ end
444+
445+ parse(parser, varargin{:});
446+ opt = parser.Results;
447+
448+ if ~isfinite(opt.blockRange(2))
449+ opt.blockRange(2)=length(seq.blockDurations);
450+ end
451+
452+ if opt.sameSeq
453+ seq2=seq;
454+ else
455+ seq2 = mr.Sequence(seq.sys);
456+ seq2.copyDefinitions(seq); % copy definitions from the source sequence
457+ end
458+
459+ obj.labels=struct('NOPOS',0,'NOROT',0,'NOSCL',0);
460+
461+ for iB=opt.blockRange(1):opt.blockRange(2)
462+ B=seq.getBlock(iB,opt.sameSeq); % second parameter means 'addIDs'
463+ B2=obj.applyToBlock(B);
464+ seq2.addBlock(B2);
465+ end
466+ end
467+ end
468+end
469+
470+%% Local Functions
471+
472+% find the first and last time point of the rf or adc input in the block
473+function [t_s, t_e] = extract_time(event)
474+ if strcmp(event.type,'adc')
475+ t_s = event.delay + event.dwell * 0.5;
476+ t_e = event.delay + event.dwell * (event.numSamples-0.5);
477+ elseif strcmp(event.type,'rf')
478+ t_s = event.delay + event.t(1);
479+ t_e = event.delay + event.t(end);
480+ end
481+end
482+
483+% check whether gradient is constant during rf or adc time points
484+
485+% inputs are: t=tt and amp=waveform of the gradient
486+% and t_start and t_end are the first and last time point of the rf or adc event
487+% flag=1 means the gradient is constant
488+function is_const = is_grad_const(t, amp, t_start, t_end)
489+ index_s = find(t <= t_start, 1, 'last');
490+ index_e = find(t >= t_end, 1, 'first');
491+ if isempty(index_s)
492+ index_s=1;
493+ end
494+ if isempty(index_e)
495+ index_e=length(t);
496+ end
497+ is_const = all(abs(amp(index_s:index_e) - amp(index_s)) <= 1e-10); % MZ: why this threshold? CHECKME
498+end
499+
500+
501+% generate breaks and coefficients for a gradient that is required for
502+% making piecwise polynomial
503+function [b, c, tt_extended, waveform_extended] = generate_breaks_coefs(g, tt, waveform, gradRasterTime, t_start, t_end)
504+if g.type=='grad'
505+ if abs(g.tt(1)-gradRasterTime/2)<eps % check whether gradient is arbitrary gradient or extended trapezoid
506+ tt = [tt(1)-gradRasterTime/2; tt(:); tt(end)+gradRasterTime/2]; % if it's arbitrary gradient, we add first and last gradient amplitudes to the waveform
507+ waveform = [g.first; waveform(:); g.last];
508+ end
509+end
510+tt_extended = tt(:)';
511+waveform_extended = waveform(:)';
512+
513+% generate initial breaks and coefs
514+b = tt_extended;
515+c = zeros(length(tt)-1,2);
516+c(:,1) = diff(waveform) ./ diff(tt);
517+c(find(isnan(c))) = 0;
518+c(find(isinf(c))) = 0;
519+c(:,2) = (waveform(1:end-1))';
520+
521+% MZ: question: why not adding (t_start,0) or (t_end,0) to tt/waveform? %
522+% MS: if we add 0 at the begining/end of the waveform and then calculate the slope of the first/last time slot, we will get a non-zero slope (if g.first or g.last are not zero).
523+% This is because the 0 value is connected to non-zero g.first or g.last and it means there is real gradient between them but should not exist.
524+% so it's better to add [0,0] at the begining or end of the coef matrix later on.
525+% modify breaks and coefs if the rf or adc event(s) is/are started (or ended)
526+% before (or after) the first (or last) time point of the gradient
527+if t_start<tt(1)
528+ b = [t_start, b];
529+ c = [0,0;c];
530+ tt_extended = [t_start, tt_extended];
531+ waveform_extended = [0, waveform_extended];
532+end
533+if t_end>tt(end)
534+ b = [b, t_end];
535+ c = [c;0,0];
536+ tt_extended = [tt_extended, t_end];
537+ waveform_extended = [waveform_extended, 0];
538+end
539+end
540+
541+
542+% % make piecewise polynomial based on time points and amplitudes of gradient event
543+% function f_pp = mkpp_linear(t,f)
544+% breaks = t;
545+% coefs = zeros(length(t)-1,2);
546+% coefs(:,1) = diff(f) ./ diff(t);
547+% coefs(find(isnan(coefs))) = 0;
548+% coefs(find(isinf(coefs))) = 0;
549+% coefs(:,2) = (f(1:end-1))';
550+% f_pp = mkpp(breaks,coefs);
551+% end
552+
553+
554+% local function returning the fractional part
555+function out=local_frac(in)
556+ out = in-floor(in);
557+end
558+
559+% % local mod function
560+% function out=local_mod(in,m)
561+% out = zeros(size(in));
562+% for i=1:length(in)
563+% if in(i)>0.5*m
564+% z=floor(in(i)/m+0.5);
565+% out(i)=in(i)-z*m;
566+% elseif in(i)<=-0.5*m
567+% z=floor(-in(i)/m+0.5);
568+% out(i)=in(i)+z*m;
569+% else
570+% out(i)=in(i);
571+% end
572+% end
573+% end
574+
575+
576+%% Accurate mathematical operations using high resolution times (sum , multiply , mod)
577+% in this function, we take the time breaks and coefficients of the
578+% piecewise polynomial of the gradient and desired time point of the rf or ADC event (t) and
579+% amount of the fov shift. and accurately calculate the area under each time
580+% slot of the gradient and multiply it by shift and finally calculate the
581+% mod of that by 1. (phase cycle)
582+function Mod = accurate_mod_pp(breaks, coefs, t, shift)
583+Mod = zeros(size(t));
584+A = [];
585+i_breaks = 0;
586+for c=1:length(t)
587+ % c
588+ index = find(breaks <= t(c), 1, 'last');
589+ t0 = t(c);
590+ area = 0;
591+
592+ while index > (i_breaks + 1)
593+ i_breaks = i_breaks +1;
594+ AB = coefs(i_breaks,1) * shift * ( (breaks(i_breaks+1)-breaks(i_breaks))^2-(breaks(i_breaks)-breaks(i_breaks))^2 );
595+ CD = coefs(i_breaks,2) * shift * ( (breaks(i_breaks+1)-breaks(i_breaks))-(breaks(i_breaks)-breaks(i_breaks)) );
596+ A = [A,local_frac( local_frac(AB) + local_frac(CD) )];
597+ end
598+
599+ if t0==breaks(i_breaks+1)
600+ area = local_frac(sum(A));
601+ else
602+ AB_n = coefs(i_breaks+1,1) * shift * ( (t0-breaks(i_breaks+1))^2-(breaks(i_breaks+1)-breaks(i_breaks+1))^2 );
603+ CD_n = coefs(i_breaks+1,2) * shift * ( (t0-breaks(i_breaks+1))-(breaks(i_breaks+1)-breaks(i_breaks+1)) );
604+ area = local_frac( local_frac(sum(A)) + local_frac(AB_n) + local_frac(CD_n) );
605+ end
606+ Mod(c) = area;
607+end
608+end
609+
src/engine/pulseq/+mr/Contents.madded+12−0View file
@@ -0,0 +1,12 @@
1+% MR MATLAB Package for MR sequence generation and reading/writing files.
2+%
3+% Files
4+% block2events - Convert a block structure to cell array.
5+% calcDuration - calcDuration Calculate the duration of an event or block.
6+% compressShape - compressShape Compress a gradient or pulse shape.
7+% decompressShape - decompressShape Decompress a gradient or pulse shape.
8+% makeAdc - makeAdc Create a ADC readout event.
9+% makeDelay - makeDelay Create a delay event.
10+% makeSincPulse - makeSincPulse Create a slice selective since pulse.
11+% makeTrapezoid - makeTrapezoid Create a trapezoid gradient event.
12+% opts - Set gradient limits of the MR system.
src/engine/pulseq/+mr/addCustomLabel.madded+17−0View file
@@ -0,0 +1,17 @@
1+function addCustomLabel(new_lbl)
2+% addCustomLabel(new_lbl)
3+% registers a new custom data label
4+
5+if ~ischar(new_lbl)
6+ error('addCustomLabel: new label should be a character string');
7+end
8+
9+supported_labels=mr.getSupportedLabels();
10+if any(ismember(supported_labels,new_lbl))
11+ warning('addCustomLabel: label %s is already known',new_lbl);
12+end
13+
14+supported_labels{end+1}=new_lbl;
15+mr.aux.globalVars('set','SupportedLabels',supported_labels);
16+
17+end
src/engine/pulseq/+mr/addGradients.madded+346−0View file
@@ -0,0 +1,346 @@
1+function grad = addGradients(grads, varargin)
2+%addGradients Superposition of several gradients on a single axis.
3+%
4+% PURPOSE
5+% Combines two or more gradient events on the same logical channel
6+% ('x', 'y', or 'z') into a single equivalent gradient event by
7+% pointwise summation of their waveforms on a common time grid. Used
8+% to merge pre-phasers, spoilers, blip-up/blip-down pairs, and similar
9+% co-scheduled gradients into one block entry consumed by
10+% mr.Sequence/addBlock.
11+%
12+% The returned struct's type depends on the inputs:
13+% - if every input is a trapezoid with identical delay, riseTime,
14+% flatTime, and fallTime, the result is a trapezoid struct with
15+% amplitudes summed (fast path);
16+% - if every input is a trapezoid or an extended trapezoid on an
17+% irregular time sampling, the result is an extended trapezoid
18+% built via mr.makeExtendedTrapezoid;
19+% - otherwise the result is an arbitrary gradient on the gradient
20+% raster built via mr.makeArbitraryGrad.
21+% In all three cases the returned struct is a valid gradient event
22+% and can be passed directly to mr.Sequence/addBlock.
23+%
24+% SIGNATURES
25+% grad = mr.addGradients(grads) % uses defaults from mr.opts()
26+% grad = mr.addGradients(grads, system) % positional system
27+% grad = mr.addGradients(grads, 'system', system) % name-value system
28+% grad = mr.addGradients(grads, system, 'maxGrad', g, ...) % override slew/amplitude caps
29+%
30+% grads must be a cell array of at least two gradient event structs,
31+% all on the same channel. Per-gradient delays are preserved; the
32+% returned gradient's delay is the smallest delay among the inputs.
33+%
34+% INPUTS
35+% grads [required] cell array of >=2 gradient event structs on the same channel
36+% system [optional] struct from mr.opts. If omitted or empty, mr.opts() defaults
37+% are used. Also accepted as 'system', sys name/value pair.
38+% maxGrad [name/value] double, Hz/m, override for max gradient amplitude used by
39+% the arbitrary-grad path. Default 0 (use system.maxGrad).
40+% maxSlew [name/value] double, Hz/m/s, override for max slew rate used by the
41+% arbitrary-grad path. Default 0 (use system.maxSlew).
42+%
43+% OUTPUT
44+% grad struct. Gradient yype depends on the input mix (see PURPOSE).
45+% Field order (this detailed description will be moved to respective mr.make... functions):
46+%
47+% Trapezoid fast path (all inputs identical-timing traps):
48+% .type 'trap'
49+% .channel 'x' | 'y' | 'z'
50+% .amplitude Hz/m, sum of input amplitudes
51+% .riseTime seconds, same as inputs
52+% .flatTime seconds, same as inputs
53+% .fallTime seconds, same as inputs
54+% .area 1/m, sum of input areas
55+% .flatArea 1/m, sum of input flat areas
56+% .delay seconds, same as inputs
57+% .first 0 (trapezoids always start at zero)
58+% .last 0 (trapezoids always end at zero)
59+%
60+% Extended trapezoid path (all trap or extended-trap inputs):
61+% .type 'grad'
62+% .channel 'x' | 'y' | 'z'
63+% .waveform Hz/m, amplitude samples on the union time grid
64+% .delay seconds, min delay across inputs
65+% .tt seconds, time-offsets of samples relative to delay
66+% .shape_dur seconds, waveform duration on the gradient raster
67+% .area 1/m, waveform area
68+% .first Hz/m, sum of inputs' first values that share the common delay
69+% .last Hz/m, sum of inputs' last values that share the max duration
70+%
71+% Arbitrary gradient fallback (mixed arbitrary + other):
72+% .type 'grad'
73+% .channel 'x' | 'y' | 'z'
74+% .waveform Hz/m, uniformly-sampled amplitudes on system.gradRasterTime
75+% (or half that if any input is oversampled-arbitrary)
76+% .delay seconds, min delay across inputs
77+% .area 1/m, waveform area
78+% .tt seconds, sample time-offsets relative to delay
79+% .shape_dur seconds, waveform duration
80+% .first Hz/m, as above
81+% .last Hz/m, as above
82+%
83+% ERRORS
84+% - 'gradients have to be passed as cell array': grads is not a cell
85+% - 'cannot add less then two gradients': numel(grads) < 2
86+% - 'cannot add gradients on different channels': inputs mix x/y/z
87+% Additional errors may propagate from mr.makeArbitraryGrad (e.g., slew
88+% rate or amplitude limit violation) when the arbitrary-grad fallback
89+% is taken.
90+%
91+% NOTES
92+% - 'system' is registered as a positional (addOptional) parameter but
93+% Pulseq's permissive input parser also accepts it as name-value.
94+% - The returned delay is the minimum delay among the inputs; shapes
95+% that start later than that are zero-padded internally so the
96+% summed waveform preserves each input's original onset time.
97+% - The fast trapezoid path triggers only when all inputs share the
98+% same delay, riseTime, flatTime, AND fallTime. Inputs with matching
99+% total duration but different rise/fall times (typical when the
100+% caller uses 'Duration' + different 'Area') fall through to the
101+% extended-trapezoid path.
102+% - If any input is an oversampled arbitrary gradient, the result is
103+% sampled at system.gradRasterTime/2 instead of system.gradRasterTime.
104+% - maxGrad / maxSlew are only consulted on the arbitrary-grad path;
105+% they are ignored on the trap and extended-trap paths (may change in future).
106+%
107+% EXAMPLE
108+% sys = mr.opts('MaxGrad', 30, 'GradUnit', 'mT/m', ...
109+% 'MaxSlew', 170, 'SlewUnit', 'T/m/s');
110+%
111+% % pre-phaser immediately followed 5 ms later by a spoiler, combined
112+% % into one readout-axis gradient event
113+% gxPre = mr.makeTrapezoid('x', 'Area', -500, 'Duration', 2e-3, 'system', sys);
114+% gxSpoil = mr.makeTrapezoid('x', 'Area', 2000, 'Duration', 2e-3, ...
115+% 'delay', 5e-3, 'system', sys);
116+% gxComb = mr.addGradients({gxPre, gxSpoil}, 'system', sys);
117+%
118+% seq = mr.Sequence(sys);
119+% seq.addBlock(gxComb);
120+%
121+% SEE ALSO
122+% mr.Sequence/addBlock, mr.opts, mr.makeTrapezoid,
123+% mr.makeExtendedTrapezoid, mr.makeArbitraryGrad, mr.calcDuration
124+%
125+% Maxim Zaitsev <maxim.zaitsev@uniklinik-freiburg.de>
126+% Stefan Kroboth <stefan.kroboth@uniklinik-freiburg.de>
127+
128+persistent parser
129+
130+if isempty(parser)
131+ parser = mr.aux.InputParserCompat;
132+ parser.FunctionName = 'addGradients';
133+ parser.addRequired('grads');
134+ parser.addOptional('system', [], @isstruct);
135+ parser.addParamValue('maxGrad', 0, @isnumeric);
136+ parser.addParamValue('maxSlew', 0, @isnumeric);
137+end
138+parse(parser, grads, varargin{:});
139+opt = parser.Results;
140+
141+if isempty(opt.system)
142+ system=mr.opts();
143+else
144+ system=opt.system;
145+end
146+
147+maxSlew = system.maxSlew;
148+maxGrad = system.maxGrad;
149+if opt.maxGrad > 0
150+ maxGrad = opt.maxGrad;
151+end
152+if opt.maxSlew > 0
153+ maxSlew = opt.maxSlew;
154+end
155+
156+if ~iscell(grads)
157+ error('gradients have to be passed as cell array');
158+end
159+
160+if length(grads)<2
161+ error('cannot add less then two gradients');
162+end
163+
164+% first gradient event defines channel:
165+channel = grads{1}.channel;
166+
167+% find out the general delay of all gradients and other statistics
168+delays = []; % TODO: preallocate instead of grow
169+firsts = [];
170+lasts = [];
171+durs=[];
172+is_trap=[];
173+is_arb=[];
174+is_osa=[]; % oversampled arbitrary grad
175+for ii = 1:length(grads)
176+ if grads{ii}.channel~=channel
177+ error('cannot add gradients on different channels');
178+ end
179+ delays = [delays, grads{ii}.delay];
180+ durs = [durs, mr.calcDuration(grads{ii})];
181+ is_trap = [is_trap, strcmp(grads{ii}.type,'trap')];
182+ if is_trap(end)
183+ is_arb = [is_arb, false];
184+ is_osa = [is_osa, false];
185+ % remember first/last
186+ firsts = [firsts, 0];
187+ lasts = [lasts, 0];
188+ else
189+ % check if this is an extended trapezoid
190+ tt_rast=grads{ii}.tt/system.gradRasterTime;
191+ is_arb = [is_arb, all(abs(tt_rast(:)'+0.5-(1:length(tt_rast)))<1e-6)];
192+ is_osa = [is_osa, all(abs(tt_rast(:)'-0.5*(1:length(tt_rast)))<1e-6)];
193+ % remember first/last
194+ firsts = [firsts, grads{ii}.first];
195+ lasts = [lasts, grads{ii}.last];
196+ end
197+end
198+common_delay = min(delays);
199+total_duration = max(durs);
200+is_etrap=(~is_trap)&(~is_arb)&(~is_osa);
201+
202+% check if we have a set of traps with the same timing
203+if all(is_trap)
204+ % now all fields are the same so we can convert cell to a normal array
205+ gradsa=cell2mat(grads);
206+ if 1==length(unique([gradsa.delay])) && ...
207+ 1==length(unique([gradsa.riseTime])) && ...
208+ 1==length(unique([gradsa.flatTime])) && ...
209+ 1==length(unique([gradsa.fallTime]))
210+ % TADA, all our gradients have the same timing, so we just add
211+ % the amplitudes!
212+ grad=gradsa(1);
213+ grad.amplitude = sum([gradsa.amplitude]);
214+ grad.area = sum([gradsa.area]);
215+ grad.flatArea = sum([gradsa.flatArea]);
216+ return;
217+ end
218+end
219+
220+% check if we only have arbitrary grads on irregular time samplings
221+% optionally mixed with trapezoids
222+if all(is_trap | is_etrap)
223+ % we can do quite efficient calculations and keep the shapes still rather simple
224+ times=[];
225+ for ii = 1:length(grads)
226+ g=grads{ii};
227+ if is_trap(ii)
228+ times = [times; cumsum([g.delay; g.riseTime; g.flatTime; g.fallTime])];
229+ else
230+ times = [times; g.delay+g.tt];
231+ end
232+ end
233+ times=unique(times); % unique() also sorts the array
234+ %times=unique(round(times/system.gradRasterTime)*system.gradRasterTime); % rounding to raster would be too crude here
235+ dt=times(2:end)-times(1:end-1);
236+ ieps=find(dt<eps);
237+ if ~isempty(ieps)
238+ dtx=[times(1); dt];
239+ dtx(ieps)=dtx(ieps)+dtx(ieps+1); % this assumes that no more than two too similar values can occur
240+ dtx(ieps+1)=[];
241+ times=cumsum(dtx);
242+ end
243+ amplitudes=zeros(size(times));
244+ for ii = 1:length(grads)
245+ g=grads{ii};
246+ if strcmp(g.type,'trap')
247+ if g.flatTime>0 % trapezoid or triangle
248+ g.tt=cumsum([0; g.riseTime; g.flatTime; g.fallTime]);
249+ g.waveform=[0; g.amplitude; g.amplitude; 0];
250+ else
251+ g.tt=cumsum([0; g.riseTime; g.fallTime]);
252+ g.waveform=[0; g.amplitude; 0];
253+ end
254+ end
255+ tt=g.delay+g.tt;
256+ % fix rounding for the first and last time points
257+ [tmin, imin]=min(abs(tt(1)-times));
258+ if tmin<eps
259+ tt(1)=times(imin);
260+ end
261+ [tmin, imin]=min(abs(tt(end)-times));
262+ if tmin<eps
263+ tt(end)=times(imin);
264+ end
265+ % give up the "ownership" of the first point of the shape if it starts at a non-zero value
266+ if abs(g.waveform(1))>eps && tt(1)>eps
267+ tt(1)=tt(1)+eps;
268+ end
269+ amplitudes=amplitudes+interp1(tt,g.waveform,times,'linear',0);
270+ end
271+ grad=mr.makeExtendedTrapezoid(channel,'amplitudes',amplitudes,'times',times,'system',system);
272+ return;
273+end
274+
275+% OK, here we convert everything to a regularly-sampled waveform
276+waveforms = {};
277+max_length = 0;
278+some_osa=any(is_osa);
279+if some_osa
280+ target_raster=system.gradRasterTime/2;
281+else
282+ target_raster=system.gradRasterTime;
283+end
284+for ii = 1:length(grads)
285+ g = grads{ii};
286+ if ~is_trap(ii)
287+ if is_arb(ii)||is_osa(ii)
288+ if some_osa && is_arb(ii)
289+ % interpolate missing samples
290+ waveforms{ii} = (g.waveform(floor(1:0.5:end))+g.waveform(ceil(1:0.5:end)))*0.5;
291+ else
292+ waveforms{ii} = g.waveform;
293+ end
294+ else
295+ waveforms{ii} = mr.pts2waveform(g.tt, g.waveform, target_raster);
296+ end
297+ else
298+ if (g.flatTime>0) % triangle or trapezoid
299+ times = [g.delay - common_delay; ...
300+ g.delay - common_delay + g.riseTime; ...
301+ g.delay - common_delay + g.riseTime + g.flatTime; ...
302+ g.delay - common_delay + g.riseTime + g.flatTime + g.fallTime];
303+ amplitudes = [0; g.amplitude; g.amplitude; 0];
304+ else
305+ times = [g.delay - common_delay; ...
306+ g.delay - common_delay + g.riseTime; ...
307+ g.delay - common_delay + g.riseTime + g.fallTime];
308+ amplitudes = [0; g.amplitude; 0];
309+ end
310+ waveforms{ii} = mr.pts2waveform(times, amplitudes, target_raster);
311+ end
312+ if size(waveforms{ii},1)==1
313+ waveforms{ii}=waveforms{ii}';
314+ end
315+ %warning('addGradient(): potentially incorrect handling of delays... TODO: fixme!');
316+ if g.delay - common_delay > 0
317+ %warning('addGradient(): zerofilling the shape, running unchecked code...');
318+ t_delay = (0:target_raster:g.delay-common_delay-target_raster).';
319+ waveforms{ii} = [t_delay*0; waveforms{ii}];
320+ end
321+ max_length = max(max_length, length(waveforms{ii}));
322+end
323+
324+w = zeros(max_length,1);
325+for ii = 1:length(grads)
326+ % % SK: Matlab is so ridiculously cumbersome...
327+ % wt = zeros(max_length, 1);
328+ % wt(1:length(waveforms{ii})) = waveforms{ii};
329+ % w = w + wt;
330+ % MZ: it is cumbersome indeed, but not so...
331+ w(1:length(waveforms{ii})) = w(1:length(waveforms{ii})) + waveforms{ii};
332+end
333+
334+grad = mr.makeArbitraryGrad(channel, w, system, ...
335+ 'maxSlew', maxSlew,...
336+ 'maxGrad', maxGrad,...
337+ 'delay', common_delay,...
338+ 'oversampling',some_osa,...
339+ 'first',sum(firsts(delays==common_delay)),...
340+ 'last',sum(lasts(durs==total_duration)));
341+% first is defined by the sum of firsts with the minimal delay (common_delay)
342+% last is defined by the sum of lasts with the maximum duration (total_duration)
343+%grad.first=sum(firsts(delays==common_delay));
344+%grad.last=sum(lasts(durs==total_duration));
345+
346+end
src/engine/pulseq/+mr/addRamps.madded+73−0View file
@@ -0,0 +1,73 @@
1+function varargout=addRamps(k,varargin)
2+%addRamps Add segments to the trajectory to ramp to and from the given trajectory.
3+% kout=addRamps(k) Add a segment to k so kout travels from 0 to k(1) and
4+% a segment so kout goes from k(end) back to 0 without violating the
5+% gradient and slew constraints.
6+%
7+% [kx,ky,...]=addRamps({kx,ky,...}) Add segments of the same length
8+% for each trajectory in the cell array.
9+%
10+% [...,rf]=addRamps(...,'rf',rf) Add a segment of zeros over the ramp
11+% times to an RF shape.
12+%
13+% See also Sequence.makeArbitraryGrad
14+
15+persistent parser
16+if isempty(parser)
17+ parser = mr.aux.InputParserCompat;
18+ parser.FunctionName = 'addRamps';
19+ parser.addRequired('k',@(x)(isnumeric(x)||iscell(x)));
20+ parser.addOptional('system',[],@isstruct);
21+ parser.addParamValue('rf',[],@isnumeric);
22+ parser.addParamValue('maxGrad',0,@isnumeric);
23+ parser.addParamValue('maxSlew',0,@isnumeric);
24+ parser.addParamValue('gradOversampling',false,@islogical);
25+
26+end
27+parse(parser,k,varargin{:});
28+opt = parser.Results;
29+
30+if isempty(opt.system)
31+ system=mr.opts();
32+else
33+ system=opt.system;
34+end
35+
36+if opt.maxGrad>0
37+ system.maxGrad=opt.maxGrad;
38+end
39+if opt.maxSlew>0
40+ system.maxSlew=opt.maxSlew;
41+end
42+
43+if iscell(opt.k)
44+ k=cell2mat(opt.k(:));
45+else
46+ k=opt.k;
47+end
48+
49+nChannels=size(k,1);
50+k=[k; zeros(3-nChannels,size(k,2))]; % Pad out with zeros if needed
51+
52+[kUp, ok1] = mr.calcRamp(zeros(3,2),k(:,1:2),system,'gradOversampling',opt.gradOversampling);
53+[kDown, ok2] = mr.calcRamp(k(:,end-1:end),zeros(3,2),system,'gradOversampling',opt.gradOversampling);
54+assert(ok1 & ok2,'Failed to calculate gradient ramps');
55+
56+kUp = [zeros(3,2), kUp]; % Add start and end points to ramps
57+kDown = [kDown, zeros(3,1)];
58+
59+k = [kUp, k, kDown]; % Add ramps to trajectory
60+
61+if isnumeric(opt.k)
62+ varargout{1} = k(1:nChannels,:);
63+else
64+ for i=1:nChannels
65+ varargout{i}=k(i,:);
66+ end
67+end
68+if ~isempty(opt.rf)
69+ varargout{end+1}=[zeros(1,size(kUp,2)*10), opt.rf, zeros(1,size(kDown,2)*10)];
70+end
71+
72+
73+end
src/engine/pulseq/+mr/align.madded+108−0View file
@@ -0,0 +1,108 @@
1+function [varargout] = align(varargin)
2+%align set alignment of the objects in the block
3+%
4+% align(align_spec, obj <, obj> <, align_spec, obj> ...);
5+%
6+% sets delays of the objects within the block to achieve the desired
7+% alignment.
8+% All previously configured delays within objects are taken into account
9+% during calculating of the block duration but then reset according to
10+% the selected alignment.
11+% Possible values for align_spec are 'left', 'center', 'right'
12+% WARNING: 'center' may break gradient raster alignment
13+% When a numerical parameter is passed amongst the events it is
14+% interpreted as the required duration of the block. If the duration of
15+% any of the events exceeds this required duration an error will be thrown.
16+% The required block duration is optionally returned as the last
17+% parameter in the output list. If used within seq.addBlock() required
18+% duration is always passed at the end of the list of the block events.
19+%
20+% See also Sequence.addBlock
21+%
22+% Maxim Zaitsev <maxim.zaitsev@uniklinik-freiburg.de>
23+
24+alignment_options={'left', 'center', 'right'};
25+% parse parameters
26+if ~ischar(varargin{1})
27+ error('first parameter must be a string');
28+end
29+curr_align=find(strcmp(varargin{1},alignment_options));
30+if isempty(curr_align)
31+ error('invalid alignment spec');
32+end
33+iobjects=[];
34+alignments=[];
35+required_duration=[];
36+
37+for i=2:length(varargin)
38+ if ischar(varargin{i})
39+ curr_align=find(strcmp(varargin{i},alignment_options));
40+ if isempty(curr_align)
41+ error('invalid alignment spec');
42+ end
43+ continue;
44+ end
45+ if isnumeric(varargin{i})
46+ if ~isempty(required_duration)
47+ error('More than one numeric parameter given to align()');
48+ end
49+ required_duration=varargin{i};
50+ continue;
51+ end
52+ iobjects(end+1)=i;
53+ alignments=[alignments curr_align];
54+end
55+
56+objects={varargin{iobjects}};
57+
58+dur=mr.calcDuration(objects);
59+if ~isempty(required_duration)
60+ if dur-required_duration>eps
61+ error('Required block duration is %g s but actual block duration is %g s', required_duration, dur);
62+ end
63+ dur=required_duration;
64+end
65+
66+% set new delays
67+for i=1:length(objects)
68+ if isfield(objects{i},'id')
69+ error('attempting to align() readily registered object! please register objects after calling this function or deregister the argument(s) by calling rmfield(...,''id'')');
70+ end
71+ switch alignments(i)
72+ case 1
73+ objects{i}.delay=0;
74+ case 2
75+ objects{i}.delay=(dur - mr.calcDuration(objects{i}) + objects{i}.delay)/2;
76+ case 3
77+ ev=objects{i};
78+ ev_dur=mr.calcDuration(ev);
79+ %if isfield(ev,'ringdownTime')
80+ % ev_dur=ev_dur+ev.ringdownTime;
81+ %end
82+ objects{i}.delay=dur - ev_dur + objects{i}.delay;
83+ if objects{i}.delay < 0
84+ error('align() attempts to set a negative delay, probably some RF pulses ignore rfRingdownTime');
85+ end
86+ end
87+end
88+
89+if nargout==length(objects)
90+ varargout=objects;
91+elseif ~isempty(required_duration) && nargout==length(objects)+1
92+ varargout=objects;
93+ varargout{end+1}=required_duration;
94+elseif nargout==1
95+ if isempty(required_duration)
96+ varargout={objects};
97+ else
98+ varargout={[objects {required_duration}]};
99+ end
100+elseif nargout<length(objects)
101+ warning('not all objects can be assigned to the output arguments; we recommend using ~ to discard output arguments explicitly.');
102+ nout=min(nargout,length(objects));
103+ varargout=objects(1:nout);
104+else
105+ error(['the number of output arguments (' num2str(nargout) ') exceeds the number of sequence objects (' num2str(length(objects)) ') passed to the function.']);
106+end
107+
108+end
src/engine/pulseq/+mr/block2events.madded+48−0View file
@@ -0,0 +1,48 @@
1+function c = block2events(b)
2+%BLOCK2EVENTS Convert a block structure to cell array.
3+% c=BLOCK2EVENTS(b) Convert the block structure to cell of
4+% sequence events.
5+%
6+% If b is already a cell array of events this array is
7+% returned unmodified.
8+
9+% strip away 1x1 cell wrapper(s) -- otherwise it conflicts with adding ready-made blocks
10+while iscell(b) && 1==length(b) && iscell(b{1})
11+ b=b{1};
12+end
13+
14+c = b; % Assume b is already a cell array of events
15+if iscell(b)
16+ first = b{1}; % Use first element to test for block structure
17+else
18+ first = b;
19+end
20+if isfield(first, 'rf')
21+ % Argument is a block structure, copy events to cell array
22+ % varargin for further processing.
23+ assert(length(b) == 1, 'Only a single block structure can be added');
24+% fields = fieldnames(first)';
25+%%%%%%%
26+% c = {};
27+% for f = fields
28+% if ~isempty(first.(char(f)))
29+% c{end+1} = first.(char(f));
30+% end
31+% end
32+%%
33+% c = cell(1,length(fields));
34+% for i = 1:length(fields)
35+% c{i} = first.(char(fields(i)));
36+% end
37+ c=struct2cell(first);
38+ %c(cellfun(@isempty,c))=[];
39+ c(cellfun('isempty',c))=[];
40+ %c=c(~cellfun('isempty',c));
41+
42+ %c(cellfun('isnumeric',c))=[]; % remove blockDuration
43+
44+elseif iscell(first)
45+ c = first;
46+end
47+
48+end
src/engine/pulseq/+mr/calcAdcSeg.madded+80−0View file
@@ -0,0 +1,80 @@
1+function [adcSegments,adcSamplesPerSegment] = calcAdcSeg(numSamples,dwell,system,mode)
2+%mr.calcAdcSeg : Calculate splitting of the ADC in segments
3+% On some scanners, notably Siemens, ADC objects that exceed a certain
4+% sample length (8192 samples on Siemens) should be splittable to N
5+% equal parts, each of which aligned to the gradient raster. Each
6+% segment, however, needs to have the number of samples smaller than
7+% system.adcSamplesLimit and divisible by system.adcSamplesDivisor to be
8+% executable on the scanner. The optional parameter mode can be either
9+% 'shorten' or 'lengthen'.
10+
11+if system.adcSamplesLimit<=0
12+ adcSamplesPerSegment=numSamples;
13+ adcSegments=1;
14+ return;
15+end
16+
17+if ~exist('mode', 'var')
18+ mode='shorten';
19+end
20+
21+if ~strcmp(mode,'shorten') && ~strcmp(mode,'lengthen')
22+ error('In mr.calcAdcSeg(...,mode) mode should be either ''shorten'' or ''lengthen''');
23+end
24+
25+t_eps=1e-9; % TODO: shift it to the system parameters???
26+
27+iGR=round(system.gradRasterTime/system.adcRasterTime);
28+assert(abs(system.gradRasterTime/system.adcRasterTime-iGR)<t_eps);
29+
30+iDwell=round(dwell/system.adcRasterTime);
31+assert(abs(dwell/system.adcRasterTime-iDwell)<t_eps);
32+
33+iCommon=lcm(iGR,iDwell); % least common multiplier
34+samplesStep=iCommon/iDwell;
35+
36+% Siemens-specific: number of samples should be divisible by system.adcSamplesDivisor
37+gcd_adcDiv=gcd(samplesStep,system.adcSamplesDivisor);
38+if gcd_adcDiv~=system.adcSamplesDivisor
39+ samplesStep=samplesStep*system.adcSamplesDivisor/gcd_adcDiv;
40+end
41+
42+if strcmp(mode,'shorten')
43+ numSamplesStepped=floor(numSamples/samplesStep);
44+else
45+ numSamplesStepped=ceil(numSamples/samplesStep);
46+end
47+
48+while numSamplesStepped>0 && numSamplesStepped<2*numSamples/samplesStep
49+ adcSegmentFactors=factor(numSamplesStepped);
50+ adcSegments=1;
51+ if(length(adcSegmentFactors)>1)
52+ % we try all permutations and pick the smallest number of segments
53+ adcSegmentFactorsPerm=perms(adcSegmentFactors);
54+ adcSegmentFactorsPermProd=cumprod(adcSegmentFactorsPerm');
55+ adcSegmentCandidates=unique(adcSegmentFactorsPermProd(:)); % this sorts the sequence
56+ for i=1:(length(adcSegmentCandidates)-1)
57+ adcSegments=adcSegmentCandidates(i);
58+ adcSamplesPerSegment=numSamplesStepped*samplesStep/adcSegments;
59+ if (adcSamplesPerSegment<=system.adcSamplesLimit && adcSegments<=128)
60+ break
61+ end
62+ end
63+ else
64+ adcSamplesPerSegment=numSamplesStepped*samplesStep;
65+ end
66+ if (adcSamplesPerSegment<=system.adcSamplesLimit && adcSegments<=128)
67+ break
68+ end
69+ if strcmp(mode,'shorten')
70+ numSamplesStepped=numSamplesStepped-1; % try again with a smaller number of samples
71+ else
72+ numSamplesStepped=numSamplesStepped+1; % try again with a greater number of samples
73+ end
74+
75+end
76+assert(numSamplesStepped>0); % we could not find a suitable segmentation...
77+assert(adcSamplesPerSegment>0);
78+assert(adcSegments<=128);
79+end
80+
src/engine/pulseq/+mr/calcDuration.madded+132−0View file
@@ -0,0 +1,132 @@
1+function duration=calcDuration(varargin)
2+%calcDuration Calculate the duration of an event, set of events, or block.
3+%
4+% PURPOSE
5+% Compute the time, in seconds, that one or more sequence events
6+% occupy. Used pervasively when laying out blocks: TE/TR delay
7+% calculations, gradient alignment, and consistency checks between
8+% block timing and event timing all call this function.
9+%
10+% SIGNATURES
11+% dur = mr.calcDuration(event) % single event struct
12+% dur = mr.calcDuration(e1, e2, ...) % max over multiple events
13+% dur = mr.calcDuration(block) % a block struct from seq.getBlock(n)
14+% dur = mr.calcDuration({e1, e2, ...}) % cell array of events (auto-unwrapped)
15+%
16+% For multiple events the return value is the maximum of each event's
17+% duration (events in a block run concurrently, not sequentially).
18+% Unknown event types are silently ignored and contribute 0 (this behaviour may
19+% change in future).
20+%
21+% INPUTS
22+% varargin [required] One or more arguments, each one of:
23+% - event struct with a .type field set to one of
24+% 'rf', 'grad', 'trap', 'adc', 'delay', 'output', 'trigger'
25+% - a cell array containing such structs or a single
26+% numeric scalar
27+% - a block struct (i.e. has a .rf field; typically
28+% obtained from mr.Sequence/getBlock). Only a single
29+% block struct may be passed.
30+% - a numeric scalar interpreted as a blockDuration
31+% field (seconds). Used internally when block2events
32+% expands a block struct; rarely passed directly by
33+% user code, but possibly as a member of cell array.
34+%
35+% OUTPUT
36+% duration double, seconds. The maximum event duration encountered.
37+% Returned as 0 if all arguments are unknown event types.
38+%
39+% ERRORS
40+% - MATLAB:assertion:failed: a numeric blockDuration argument is
41+% smaller than the maximum event duration encountered before it
42+% in argument order. Indicates an inconsistent block whose declared
43+% blockDuration is shorter than its longest event.
44+% - 'Only a single block structure can be added' (from mr.block2events):
45+% more than one argument was a block struct.
46+% - 'Index exceeds array bounds' (from mr.block2events): called with
47+% no arguments. Always pass at least one event.
48+%
49+% NOTES
50+% - Per-event duration formulas (all in seconds):
51+% rf : event.delay + event.shape_dur + event.ringdownTime
52+% grad : event.delay + event.shape_dur (arbitrary gradient)
53+% trap : event.delay + event.riseTime + event.flatTime + event.fallTime
54+% adc : event.delay + event.numSamples*event.dwell + event.deadTime
55+% delay : event.delay
56+% output : event.delay + event.duration
57+% trigger : event.delay + event.duration
58+% - For 'rf' and 'grad' events the function relies on a precomputed
59+% event.shape_dur field; it does not re-derive duration from the
60+% waveform samples.
61+% - For 'adc', the deadTime addend is the post-acquisition dead time
62+% captured at construction (adc.deadTime), not the pre-acquisition
63+% delay (which is already included via adc.delay).
64+% - Note that 'delay' is not a true Pulseq object as it is not stored
65+% in the event table; it is just used as a dummy to construct blocks
66+% with a minimal duration
67+% - Unknown .type values are silently skipped. A struct without a
68+% .type field whose value matches no case will not raise. (This
69+% behaviour may change in future)
70+%
71+% EXAMPLE
72+% sys = mr.opts('MaxGrad', 30, 'GradUnit', 'mT/m', ...
73+% 'MaxSlew', 170, 'SlewUnit', 'T/m/s');
74+% Nx = 256; fov = 256e-3; deltak = 1/fov;
75+% gx = mr.makeTrapezoid('x', sys, 'FlatArea', Nx*deltak, 'FlatTime', 6.4e-3);
76+% gxPre = mr.makeTrapezoid('x', sys, 'Area', -gx.area/2, 'Duration', 1e-3);
77+% adc = mr.makeAdc(Nx, sys, 'Duration', gx.flatTime, 'Delay', gx.riseTime);
78+% % Longest event in this readout block
79+% dur = mr.calcDuration(gxPre, gx, adc);
80+% % TE delay rounded up to gradient raster
81+% TE = 10e-3;
82+% delayTE = ceil((TE - mr.calcDuration(gx)/2)/sys.gradRasterTime)*sys.gradRasterTime;
83+%
84+% SEE ALSO
85+% mr.block2events, mr.makeDelay, mr.Sequence/getBlock,
86+% mr.Sequence/addBlock
87+
88+duration=0;
89+
90+% Convert block structure to cell array of events
91+varargin=mr.block2events(varargin);
92+
93+% Loop over events and calculate maximum duration
94+for i=1:length(varargin)
95+ events = varargin{i};
96+ for j=1:length(events)
97+ event=events(j);
98+ if isnumeric(event) % this is the "blockDuration" field
99+ assert(duration<=event);
100+ duration=event;
101+ continue;
102+ end
103+ switch event.type
104+ case 'delay' % not a true object since v140 but still used to set a minimal block duration
105+ duration=max(duration, event.delay);
106+ case 'rf'
107+ duration=max(duration, event.delay + event.shape_dur + event.ringdownTime); % rf has now a field called 'shape_dur' because it is impossible to calculate duration here in a general case...
108+ case 'grad'
109+% % duration=max(duration, event.t(end) + event.delay );
110+% % MZ: we need to increase the last timestamp by one gradient
111+% % raster time because otherwise the duration of the gradint
112+% % containing one sample will be zero
113+% % however, we do not have access to the gradient raster time
114+% % here, so we opt of a hack, but it will actually fail for the
115+% % gradint containg one sample...
116+% % event.t(2) - event.t(1) gives us one gradient raster time
117+% duration=max(duration, event.t(end) + event.t(2) - event.t(1) + event.delay );
118+ duration=max(duration, event.delay+event.shape_dur); % shaped gradient has now a field called 'shape_dur' because it is impossible to calculate duration here in a general case...
119+ case 'adc'
120+ duration=max(duration, event.delay + ...
121+ event.numSamples*event.dwell + event.deadTime);
122+ case 'trap'
123+ duration=max(duration, event.delay + event.riseTime + ...
124+ event.flatTime + event.fallTime);
125+ % duration=max(duration, event.riseTime+ event.flatTime +event.fallTime);
126+ case {'output','trigger'}
127+ duration=max(duration, event.delay + event.duration);
128+ end
129+ end
130+end
131+
132+end
src/engine/pulseq/+mr/calcRamp.madded+410−0View file
@@ -0,0 +1,410 @@
1+function [kout, success] = calcRamp(k0,kend,varargin)
2+%
3+% the aim of joink is to join the points k0 and kend in three-dimensional
4+% k-space in minimal time, observing the gradient and slew limits, and the
5+% gradient strength G0 before k0(:,2) and Gend after kend(:,1)
6+%
7+% In the context of a fixed gradient dwell time this is a discrete problem
8+% with an a priori unknown number of discretization steps. Therefore joink
9+% tries out the optimization with 0 steps, then 1 step, and so on, until
10+% all conditions can be fulfilled, thus yielding a short connection
11+%
12+% N.B. The connection found this way is not necessarily always the shortest
13+% (there are some counterexamples) but still quite short. Improvements
14+% possible.
15+%
16+% Usage: [kout success] = joink(k0,kend,MaxGrad,MaxSlew,GradDwell,MaxPoints)
17+%
18+% [kout] connecting k-space points without k0 and kend, size = [3,Nt],
19+% where Nt = number of steps between k0 and kend.
20+% k-space units: 1/m
21+%
22+% [success] a flag indicating if a solution was found with up to
23+% MaxPoints k-space points: success (1), no solution (0)
24+%
25+% [k0] Two preceding points in k-space, size = [3,2]. From these
26+% points, the starting gradient will be calculated.
27+%
28+% [kend] Two following points in k-space, size = [3,2]. From these
29+% points, the target gradient will be calculated.
30+%
31+% [MaxGrad] maximum total vector gradient strength, size = [1,1]
32+% or
33+% [MaxGrad] maximum gradient strength per coordinate, size = [3,1]
34+%
35+% [MaxSlew] maximum total vector slew rate, size = [1,1],
36+% or
37+% [MaxSlew] maximum slew rate per coordinate, size = [3,1]
38+% all slew units: T/(m*s)
39+%
40+% [GradDwell] time between two k-space points, size = [1,1], unit: s
41+%
42+% [MaxPoints] maximum number of k-space points to be used in connecting k0
43+% with kend. Keep at a reasonable order of magnitude!
44+%
45+
46+persistent parser
47+if isempty(parser)
48+ parser = mr.aux.InputParserCompat;
49+ parser.FunctionName = 'calcRamp';
50+ parser.addRequired('k0',@isnumeric);
51+ parser.addRequired('kend',@isnumeric);
52+ parser.addOptional('system',[],@isstruct);
53+ parser.addParamValue('MaxPoints',500,@isnumeric);
54+ parser.addParamValue('maxGrad',0,@isnumeric);
55+ parser.addParamValue('maxSlew',0,@isnumeric);
56+ parser.addParamValue('gradOversampling',false,@islogical);
57+end
58+parse(parser,k0,kend,varargin{:});
59+opt = parser.Results;
60+
61+if isempty(opt.system)
62+ system=mr.opts();
63+else
64+ system=opt.system;
65+end
66+
67+maxSlew=system.maxSlew;
68+maxGrad=system.maxGrad;
69+if opt.maxGrad>0
70+ maxGrad=opt.maxGrad;
71+end
72+if opt.maxSlew>0
73+ maxSlew=opt.maxSlew;
74+end
75+if opt.gradOversampling
76+ GradRaster = system.gradRasterTime/2;
77+else
78+ GradRaster = system.gradRasterTime;
79+end
80+MaxPoints = opt.MaxPoints;
81+
82+if ~mr.aux.isOctave()
83+ SaveRecLimit = get(0,'RecursionLimit');
84+ set(0,'RecursionLimit',MaxPoints+10);
85+end
86+
87+% Determine whether we are in componentwise limited mode or in total vector
88+% limited mode.
89+% mode = 0 --> total vector limited
90+% mode = 1 --> componentwise limited
91+
92+if isequal(size(maxGrad),[1 1]) && isequal(size(maxSlew),[1 1])
93+ mode = 0;
94+elseif isequal(size(maxGrad),[3 1]) && isequal(size(maxSlew),[3 1])
95+ mode = 1;
96+else
97+ error('Input value MaxGrad or MaxSlew in invalid format.');
98+end
99+
100+G0 = (k0 (:,2)-k0 (:,1))/GradRaster;
101+Gend = (kend(:,2)-kend(:,1))/GradRaster;
102+k0 = k0 (:,2);
103+kend = kend(:,1);
104+
105+success = 0;
106+kout = zeros(3,0);
107+UsePoints = 0; % first try: connecting directly
108+
109+while (success == 0) && (UsePoints <= MaxPoints)
110+ if mode == 0
111+ if (norm(G0)>maxGrad) || (norm(Gend)>maxGrad)
112+ break;
113+ end;
114+ kout = joinleft0(k0,kend,G0,Gend,UsePoints);
115+ else
116+ if (abs(G0)>abs(maxGrad)) || (abs(Gend)>abs(maxGrad))
117+ break;
118+ end;
119+ kout = joinleft1(k0,kend,G0,Gend,UsePoints);
120+ end
121+ UsePoints = UsePoints + 1;
122+end
123+
124+if ~mr.aux.isOctave()
125+ set(0,'RecursionLimit',SaveRecLimit); % set previous recursion limit
126+end
127+
128+% -------------------------------------------------------------------------
129+function ok = InsideLimits(Grad,Slew)
130+%
131+% check if both gradient and slew rates are inside the respective limits
132+%
133+if mode == 0
134+ Grad2 = sum(Grad.^2,1); % gradient vector norm squared
135+ Slew2 = sum(Slew.^2,1); % slew vector norm squared
136+ ok = (max(Grad2) <= maxGrad^2) && (max(Slew2) <= maxSlew^2);
137+else
138+ ok = (sum(max(abs(Grad),[],2) <= maxGrad) == 3) && ...
139+ (sum(max(abs(Slew),[],2) <= maxSlew) == 3);
140+end
141+
142+end % function InsideLimits
143+
144+% -------------------------------------------------------------------------
145+function koutleft = joinleft0(k0,kend,G0,Gend,UsePoints)
146+%
147+% Add one k-space point close to k0. Gradient and slew limits apply in
148+% total vector limited mode.
149+%
150+% Rationale:
151+%
152+% 0. If UsePoints == 0 the recursion stops. If k0 and kend can be joined
153+% in one GradDwell time, return success, else return "no success".
154+%
155+% 1. Calculate optimal k-space point kopt that would lie on a straight
156+% line of N=UsePoints evenly spaced points to kend. If this kopt can be
157+% reached within gradient and slew limts, kopt is the solution of this
158+% function call.
159+%
160+% 2. If kopt cannot be reached, calculate the gradient limited point kgl
161+% closest to kopt. If this point can be reached in one GradDwell time
162+% without violating the slew limit, kgl is the solution of this
163+% function call.
164+%
165+% 3. If not kgl is not inside the slew limit, the slew limited point
166+% closest to kopt, ksl, is calculated. If ksl is inside the gradient
167+% limit, ksl is the solution.
168+%
169+% 4. If neither kgl nor ksl are possible find the point kglsl closest to
170+% kopt that satisfies both limits at the same time. See
171+% illustration.fig / illustration.png
172+%
173+% 5. Call joinright0 to obtain the other points starting with a point
174+% next to kend.
175+%
176+if UsePoints == 0
177+ G = [G0 (kend-k0)/GradRaster Gend];
178+ S = (G(:,2:end)-G(:,1:end-1))/GradRaster;
179+
180+ koutleft = zeros(3,0); % no additional k-space point
181+ success = InsideLimits(G,S);
182+
183+ return;
184+end
185+
186+dk = (kend-k0)/(UsePoints+1);
187+kopt = k0+dk; % this would be on the direct
188+Gopt = (kopt-k0)/GradRaster; % line
189+Sopt = (Gopt-G0)/GradRaster;
190+
191+okGopt = (sum(Gopt.^2,1) <= maxGrad^2);
192+okSopt = (sum(Sopt.^2,1) <= maxSlew^2);
193+
194+if okGopt && okSopt
195+ kLeft = kopt;
196+else
197+ a = maxGrad*GradRaster;
198+ b = maxSlew*GradRaster^2;
199+
200+ dkprol = G0*GradRaster; % prolonged point with no change
201+ dkconn = dk-dkprol; % in gradient
202+ % slew limited closest to kopt
203+ ksl = k0 + dkprol + dkconn/norm(dkconn)*b;
204+ Gsl = (ksl-k0)/GradRaster;
205+ okGsl = (sum(Gsl.^2,1) <= maxGrad^2);
206+
207+ kgl = k0 + dk/norm(dk)*a; % gradient limited closest to
208+ Ggl = (kgl-k0)/GradRaster; % kopt
209+ Sgl = (Ggl-G0)/GradRaster;
210+ okSgl = (sum(Sgl.^2,1) <= maxSlew^2);
211+
212+ if okGsl
213+ kLeft = ksl;
214+ elseif okSgl
215+ kLeft = kgl;
216+ else
217+ c = norm(dkprol);
218+ c1 = (a^2-b^2+c^2)/(2*c); % if e.g. |Gend|<Gmax, then
219+ h = sqrt(a^2-c1^2); % a^2-c1^2 is always positive
220+ kglsl = k0 + c1*dkprol/norm(dkprol);
221+ projondkprol = (kgl*dkprol.') * dkprol/norm(dkprol);
222+ hdirection = kgl - projondkprol;
223+ kglsl = kglsl + h*hdirection/norm(hdirection);
224+ kLeft = kglsl;
225+ end
226+end
227+
228+k = joinright0(kLeft,kend,(kLeft-k0)/GradRaster,Gend,UsePoints-1);
229+
230+koutleft = [kLeft k]; % pass along result
231+
232+end % function joinleft0
233+
234+% -------------------------------------------------------------------------
235+function koutright = joinright0(k0,kend,G0,Gend,UsePoints)
236+%
237+% Add one k-space point close to kend. Gradient and slew limits apply in
238+% total vector limited mode. Rationale see joinleft0.
239+%
240+if UsePoints == 0
241+ G = [G0 (kend-k0)/GradRaster Gend];
242+ S = (G(:,2:end)-G(:,1:end-1))/GradRaster;
243+
244+ koutright = zeros(3,0); % no additional k-space point
245+ success = InsideLimits(G,S);
246+
247+ return;
248+end
249+
250+dk = (k0-kend)/(UsePoints+1);
251+kopt = kend+dk; % this would be on the direct
252+Gopt = (kend-kopt)/GradRaster; % line
253+Sopt = (Gend-Gopt)/GradRaster;
254+
255+okGopt = (sum(Gopt.^2,1) <= maxGrad^2);
256+okSopt = (sum(Sopt.^2,1) <= maxSlew^2);
257+
258+if okGopt && okSopt
259+ kRight = kopt;
260+else
261+ a = maxGrad*GradRaster;
262+ b = maxSlew*GradRaster^2;
263+
264+ dkprol = -Gend*GradRaster; % prolonged point with no change
265+ dkconn = dk-dkprol; % in gradient
266+ % slew limited closest to kopt
267+ ksl = kend + dkprol + dkconn/norm(dkconn)*b;
268+ Gsl = (kend-ksl)/GradRaster;
269+ okGsl = (sum(Gsl.^2,1) <= maxGrad^2);
270+
271+ kgl = kend + dk/norm(dk)*a; % gradient limited closest to
272+ Ggl = (kend-kgl)/GradRaster; % kopt
273+ Sgl = (Gend-Ggl)/GradRaster;
274+ okSgl = (sum(Sgl.^2,1) <= maxSlew^2);
275+
276+ if okGsl
277+ kRight = ksl;
278+ elseif okSgl
279+ kRight = kgl;
280+ else
281+ c = norm(dkprol);
282+ c1 = (a^2-b^2+c^2)/(2*c); % if e.g. |Gend|<Gmax, then
283+ h = sqrt(a^2-c1^2); % a^2-c1^2 is always positive
284+ kglsl = kend + c1*dkprol/norm(dkprol);
285+ projondkprol = (kgl*dkprol.') * dkprol/norm(dkprol);
286+ hdirection = kgl - projondkprol;
287+ kglsl = kglsl + h*hdirection/norm(hdirection);
288+ kRight = kglsl;
289+ end
290+end
291+
292+k = joinleft0(k0,kRight,G0,(kend-kRight)/GradRaster,UsePoints-1);
293+
294+koutright = [k kRight]; % pass along result
295+
296+end % function joinright0
297+
298+% -------------------------------------------------------------------------
299+function koutleft = joinleft1(k0,kend,G0,Gend,UsePoints)
300+%
301+% Add one k-space point close to k0. Gradient and slew limits apply in
302+% componentwise limited mode. Rationale is the same as in joinleft0 but
303+% it's much easier to find the point kglsl in step 4.
304+%
305+if UsePoints == 0
306+ G = [G0 (kend-k0)/GradRaster Gend];
307+ S = (G(:,2:end)-G(:,1:end-1))/GradRaster;
308+
309+ koutleft = zeros(3,0); % no additional k-space point
310+ success = InsideLimits(G,S);
311+
312+ return;
313+end
314+
315+kLeft = zeros(3,1);
316+
317+dk = (kend-k0)/(UsePoints+1);
318+kopt = k0+dk; % this would be on the direct
319+Gopt = (kopt-k0)/GradRaster; % line
320+Sopt = (Gopt-G0)/GradRaster;
321+okGopt = (abs(Gopt) <= maxGrad);
322+okSopt = (abs(Sopt) <= maxSlew);
323+
324+dkprol = G0*GradRaster;
325+dkconn = dk-dkprol;
326+ % slew limited
327+ksl = k0 + dkprol + sign(dkconn).*maxSlew*GradRaster^2;
328+Gsl = (ksl-k0)/GradRaster;
329+okGsl = (abs(Gsl) <= maxGrad);
330+ % gradient limited
331+kgl = k0 + sign(dk).*maxGrad*GradRaster;
332+Ggl = (kgl-k0)/GradRaster;
333+Sgl = (Ggl-G0)/GradRaster;
334+okSgl = (abs(Sgl) <= maxSlew);
335+
336+for ii=1:3
337+ if (okGopt(ii)==1) && (okSopt(ii)==1)
338+ kLeft(ii) = kopt(ii);
339+ elseif (okGsl(ii)==1)
340+ kLeft(ii) = ksl(ii);
341+ elseif (okSgl(ii)==1)
342+ kLeft(ii) = kgl(ii);
343+ else
344+ display('Moment mal - hier dürfte ich niemals hinkommen!');
345+ end
346+end
347+
348+k = joinright1(kLeft,kend,(kLeft-k0)/GradRaster,Gend,UsePoints-1);
349+
350+koutleft = [kLeft k]; % pass along result
351+
352+end % function joinleft1
353+
354+% -------------------------------------------------------------------------
355+function koutright = joinright1(k0,kend,G0,Gend,UsePoints)
356+%
357+% Add one k-space point close to kend. Gradient and slew limits apply in
358+% componentwise limited mode. Rationale is the same as in joinright1
359+%
360+if UsePoints == 0
361+ G = [G0 (kend-k0)/GradRaster Gend];
362+ S = (G(:,2:end)-G(:,1:end-1))/GradRaster;
363+
364+ koutright = zeros(3,0); % no additional k-space point
365+ success = InsideLimits(G,S);
366+
367+ return;
368+end
369+
370+kRight = zeros(3,1);
371+
372+dk = (k0-kend)/(UsePoints+1);
373+kopt = kend+dk; % this would be on the direct
374+Gopt = (kend-kopt)/GradRaster; % line
375+Sopt = (Gend-Gopt)/GradRaster;
376+okGopt = (abs(Gopt) <= maxGrad);
377+okSopt = (abs(Sopt) <= maxSlew);
378+
379+dkprol = -Gend*GradRaster;
380+dkconn = dk-dkprol;
381+ % slew limited
382+ksl = kend + dkprol + sign(dkconn).*maxSlew*GradRaster^2;
383+Gsl = (kend-ksl)/GradRaster;
384+okGsl = (abs(Gsl) <= maxGrad);
385+ % gradient limited
386+kgl = kend + sign(dk).*maxGrad*GradRaster;
387+Ggl = (kend-kgl)/GradRaster;
388+Sgl = (Gend-Ggl)/GradRaster;
389+okSgl = (abs(Sgl) <= maxSlew);
390+
391+for ii = 1:3
392+ if (okGopt(ii)==1) && (okSopt(ii)==1)
393+ kRight(ii) = kopt(ii);
394+ elseif (okGsl(ii)==1)
395+ kRight(ii) = ksl(ii);
396+ elseif (okSgl(ii)==1)
397+ kRight(ii) = kgl(ii);
398+ else
399+ error('Unknown error. Code should not execute');
400+ end
401+end
402+
403+k = joinleft1(k0,kRight,G0,(kend-kRight)/GradRaster,UsePoints-1);
404+
405+koutright = [k kRight]; % pass along result
406+
407+end % function joinright1
408+
409+end % main
410+% =========================================================================
src/engine/pulseq/+mr/calcRfBandwidth.madded+129−0View file
@@ -0,0 +1,129 @@
1+function [bw,fc,spectrum,f,rfs,t]=calcRfBandwidth(rf, cutoff, df, dt)
2+%calcRfBandwidth Compute the bandwidth and spectrum of an RF pulse.
3+%
4+% PURPOSE
5+% Estimates the bandwidth of an RF pulse using a simple FFT under the
6+% low-angle (small-tip) approximation. Also returns the pulse's center
7+% frequency, complex spectrum, and the corresponding frequency and
8+% resampled time axes. Typically used for plotting excitation profile
9+% magnitude vs. frequency and for sanity-checking RF pulses produced by
10+% mr.makeSincPulse, mr.makeGaussPulse, mr.makeBlockPulse,
11+% mr.makeArbitraryRf, mr.makeSLRpulse, and mr.makeAdiabaticPulse.
12+%
13+% SIGNATURES
14+% bw = mr.calcRfBandwidth(rf) % default cutoff=0.5 (FWHM), df=10 Hz, dt=1 us
15+% [bw,fc] = mr.calcRfBandwidth(rf) % also return center frequency
16+% [bw,fc,spectrum,f] = mr.calcRfBandwidth(rf) % also return spectrum and frequency axis
17+% [bw,fc,spectrum,f,rfs,t] = mr.calcRfBandwidth(rf) % also return resampled RF waveform and time axis
18+% [...] = mr.calcRfBandwidth(rf, cutoff) % override fractional cutoff
19+% [...] = mr.calcRfBandwidth(rf, cutoff, df) % override spectral resolution
20+% [...] = mr.calcRfBandwidth(rf, cutoff, df, dt) % override time sampling step
21+%
22+% Bandwidth is the frequency width at which |spectrum| drops to
23+% cutoff*max(|spectrum|), with linear interpolation at the first and
24+% last crossings. All four positional arguments must be given in order;
25+% they cannot be passed as name/value pairs.
26+%
27+% INPUTS
28+% rf [required] struct, RF event struct from mr.makeSincPulse,
29+% mr.makeBlockPulse, mr.makeGaussPulse, mr.makeArbitraryRf,
30+% mr.makeSLRpulse, or mr.makeAdiabaticPulse. Must have
31+% fields .t (seconds), .signal (complex Hz),
32+% .freqOffset (Hz), .freqPPM, .phaseOffset (radians),
33+% and .center (seconds).
34+% cutoff [optional] double, fractional threshold for bandwidth measurement,
35+% dimensionless in (0,1]. 0.5 gives FWHM. Default: 0.5.
36+% df [optional] double, spectral resolution in Hz. Default: 10.
37+% dt [optional] double, time sampling step in seconds. Default: 1e-6.
38+%
39+% OUTPUT
40+% bw double, Hz, bandwidth at the given cutoff threshold
41+% fc double, Hz, center frequency (midpoint of the two threshold crossings)
42+% spectrum complex vector, small-tip excitation response, scaled by
43+% sin(2*pi*dt*s_ref)/s_ref with s_ref = |spectrum(fc)|
44+% f double vector, Hz, frequency axis, step df, length round(1/df/dt)
45+% rfs complex vector, RF waveform resampled onto the uniform
46+% time grid t, with rf.freqOffset, rf.freqPPM, and
47+% rf.phaseOffset folded into the phase modulation
48+% t double vector, seconds, uniform time axis centered on
49+% rf.center, step dt, span 1/df
50+%
51+% NOTES
52+% - Computed from the RF envelope's Fourier transform (small-tip
53+% equivalent). The Bloch-simulated bandwidth grows modestly at
54+% large flip angles. For exact slice-profile analysis use mr.simRf.
55+% - If rf.freqPPM is non-zero, the function calls mr.opts() to fetch
56+% sys.gamma and sys.B0 for the PPM-to-Hz conversion, and emits a
57+% warning reminding the caller to set system defaults via
58+% mr.opts('setAsDefault', true). The system defaults are used
59+% silently otherwise.
60+% - The FFT length is round(1/df/dt) (default 1e5 points). Reducing df
61+% or dt below their defaults increases compute time quadratically in
62+% the product, not in either one alone.
63+% - The spectrum is normalized so that |spectrum(fc)| approximates the
64+% on-resonance small-tip excitation amplitude rather than an
65+% unnormalized FFT magnitude.
66+%
67+% EXAMPLE
68+% sys = mr.opts('MaxGrad', 30, 'GradUnit', 'mT/m', ...
69+% 'MaxSlew', 170, 'SlewUnit', 'T/m/s');
70+% rf = mr.makeSincPulse(pi/2, 'Duration', 3e-3, ...
71+% 'SliceThickness', 3e-3, 'apodization', 0.5, ...
72+% 'timeBwProduct', 4, 'system', sys);
73+% [bw, fc, spectrum, f] = mr.calcRfBandwidth(rf);
74+% fprintf('BW = %.1f Hz, center = %.2f Hz\n', bw, fc);
75+% figure; plot(f, abs(spectrum)); xlim(3*[-bw bw]);
76+% xlabel('Frequency, Hz'); title('Excitation pulse profile');
77+%
78+% SEE ALSO
79+% mr.calcRfCenter, mr.calcRfPower, mr.simRf, mr.makeSincPulse,
80+% mr.makeGaussPulse, mr.makeBlockPulse, mr.makeArbitraryRf,
81+% mr.makeSLRpulse, mr.makeAdiabaticPulse
82+%
83+
84+if nargin<2
85+ cutoff=0.5;
86+end
87+
88+if nargin<3
89+ df=10; % spectral resolution in Hz
90+end
91+
92+if nargin<4
93+ dt=1e-6; % for now default sampling rate is 1Mhz, it's probably too high
94+end
95+
96+if abs(rf.freqPPM)>eps
97+ warning('mr.calcRfBandwidth((): relying on the system properties, like B0 and gamma, stored in the global environment by callimg mr.lims(''setAsDefault'',true)');
98+ sys=mr.opts();
99+ full_freqOffset=rf.freqOffset+rf.freqPPM*1e-6*sys.gamma*sys.B0;
100+else
101+ full_freqOffset=rf.freqOffset;
102+end
103+
104+tc=rf.center;
105+
106+% resample the pulse to a resonable time array
107+nn=round(1/df/dt);
108+t=(-floor(nn/2):ceil(nn/2)-1)*dt;
109+
110+rfs=interp1(rf.t-tc,rf.signal.*exp(1i*(rf.phaseOffset+2*pi*full_freqOffset*rf.t)),t,'linear',0);
111+spectrum=fftshift(fft(fftshift(rfs)));
112+f=(-floor(nn/2):ceil(nn/2)-1)*df;
113+
114+w1=mr.aux.findFlank(f,spectrum,cutoff);
115+w2=mr.aux.findFlank(f(end:-1:1),spectrum(end:-1:1),cutoff);
116+
117+bw=w2-w1;
118+fc=(w2+w1)/2;
119+
120+% coarse STE scaling -- we normalize to the max of the spectrum, this works
121+% better with frequency-shifted pulses than the abs(sum(shape)) -- the
122+% 0-frequency response; yes, we could take the spectrum at fc but this
123+% would have a problem for non-symmetric pulses...
124+%s_ref=max(abs(spectrum));
125+s_ref=interp1(f,abs(spectrum),fc);
126+spectrum=sin(2*pi*dt*s_ref)*spectrum/s_ref;
127+
128+end
129+
src/engine/pulseq/+mr/calcRfCenter.madded+125−0View file
@@ -0,0 +1,125 @@
1+function [tc, ic, fi]=calcRfCenter(rf)
2+%calcRfCenter Calculate the effective center time of an RF pulse.
3+%
4+% PURPOSE
5+% Returns the time point of the effective rotation of an RF pulse,
6+% used to align echo timing, set off-resonance phase compensation, and
7+% compute TE/TI delays. For shaped pulses this is the peak of the RF
8+% amplitude; for block pulses it is the midpoint of the constant
9+% plateau. Zero-padding in rf.signal is treated as part of the shape.
10+% The rf.delay field is NOT included in the returned time, so callers
11+% that need an absolute time within a block typically compute
12+% rf.delay + mr.calcRfCenter(rf).
13+%
14+% SIGNATURES
15+% tc = mr.calcRfCenter(rf) % time of center, seconds
16+% [tc, ic] = mr.calcRfCenter(rf) % also integer sample index
17+% [tc, ic, fi] = mr.calcRfCenter(rf) % also fractional offset
18+%
19+% If rf has a .center field (set by the modern RF constructors
20+% mr.makeSincPulse, mr.makeBlockPulse, mr.makeGaussPulse,
21+% mr.makeArbitraryRf, mr.makeSLRpulse, mr.makeAdiabaticPulse), tc is
22+% taken directly from rf.center and ic is the nearest index in rf.t.
23+% Otherwise tc is computed from the amplitude peak of rf.signal.
24+%
25+% INPUTS
26+% rf [required] struct, RF event struct. Must have fields .t (seconds, time
27+% axis on the RF raster) and .signal (complex Hz, waveform).
28+% If field .center (seconds) is present it is used directly;
29+% otherwise the function detects the peak of abs(rf.signal).
30+% Typically produced by mr.makeSincPulse, mr.makeBlockPulse,
31+% mr.makeGaussPulse, mr.makeArbitraryRf, mr.makeSLRpulse,
32+% or mr.makeAdiabaticPulse.
33+%
34+% OUTPUT
35+% tc double, seconds, time of the RF center relative to the start
36+% of the RF shape (rf.delay not included)
37+% ic integer, 1-based index into rf.t / rf.signal of the sample
38+% nearest to tc
39+% fi double, dimensionless, fractional offset in [-0.5, 0.5] from
40+% rf.t(ic) toward the previous (negative) or next (positive)
41+% sample, normalized by the local raster step. 0 when tc lies
42+% exactly on rf.t(ic) (within 1 ns)
43+%
44+% NOTES
45+% - When rf.center is absent, the peak detector treats samples within
46+% 0.001% of max(abs(rf.signal)) as part of the same plateau and
47+% returns the midpoint. This is what makes block pulses (constant
48+% amplitude) yield a center at the middle of the pulse rather than
49+% at the first sample.
50+% - rf.delay is intentionally excluded from tc. If a sequence places
51+% an RF event after a delay, the absolute time within the block is
52+% rf.delay + tc.
53+% - Returned tc is calculated as a floating-point value in seconds
54+% relative to the beginning of the RF shape (the leading edge of the
55+% first RF raster cell of the shape); ic is the index of the raster
56+% cell (in Matlab indexing convention), which center is the closest to
57+% tc; fi captures the sub-raster offset when rf.center does not
58+% coincide with a sample (center of the RF raster cell) and is
59+% constrained to the range [-0.5 0.5).
60+%
61+% EXAMPLE
62+% sys = mr.opts('MaxGrad', 30, 'GradUnit', 'mT/m', ...
63+% 'MaxSlew', 170, 'SlewUnit', 'T/m/s', ...
64+% 'rfDeadTime', 100e-6); % bumps rf.delay to 100 us
65+% rf = mr.makeSincPulse(pi/2, 'Duration', 3e-3, ...
66+% 'SliceThickness', 3e-3, 'system', sys);
67+% % off-resonance phase compensation for an off-center slice:
68+% rf.freqOffset = sys.gamma * 1e-3 * 5e-3; % 5 mm offset
69+% rf.phaseOffset = -2*pi*rf.freqOffset*mr.calcRfCenter(rf);
70+% % absolute time of the RF center within its block:
71+% tCenter = rf.delay + mr.calcRfCenter(rf); % 100 us + 1.5 ms = 1.6 ms
72+%
73+% SEE ALSO
74+% mr.calcRfBandwidth, mr.calcRfPower, mr.makeSincPulse,
75+% mr.makeBlockPulse, mr.makeGaussPulse, mr.makeArbitraryRf,
76+% mr.makeSLRpulse, mr.makeAdiabaticPulse
77+%
78+
79+% % detect zero-padding
80+% last=length(rf.signal);
81+% for first=1:last
82+% if abs(rf.signal(first))>eps
83+% break;
84+% end
85+% end
86+% for last=last:-1:first
87+% if abs(rf.signal(last))>eps
88+% break;
89+% end
90+% end
91+
92+% rfmax=max(abs(rf.signal(first:last)));
93+% ipeak=find(abs(rf.signal(first:last))>=rfmax-eps);
94+
95+ if isfield(rf,'center')
96+ tc=rf.center;
97+ [~,ic]=min(abs(rf.t-tc));
98+ else
99+
100+ % we detect the excitation peak and if it is a plato we take its center
101+ rfmax=max(abs(rf.signal));
102+ ipeak=find(abs(rf.signal)>=rfmax*0.99999);
103+ tc=(rf.t(ipeak(1))+rf.t(ipeak(end)))/2;
104+ ic=ipeak(round(end/2));
105+ end
106+
107+ ft=tc-rf.t(ic);
108+ if ic<length(rf.t) && ft>1e-9 % 1 ns
109+ fi=ft/(rf.t(ic+1)-rf.t(ic));
110+ elseif ic>1 && ft<-1e-9 % -1 ns
111+ fi=ft/(rf.t(ic)-rf.t(ic-1));
112+ else
113+ fi=0;
114+ end
115+
116+% % detect the excitation peak (this code is far from being ideal...)
117+% rfmin=min(abs(rf.signal(first:last))); % pure max check fails for the block pulse!!!
118+% [rfmax,ic]=max(abs(rf.signal(first:last)));
119+% if (rfmax-rfmin)<=eps
120+% ic=round((last-first+1)/2); % we take the center of the pulse for block pulses
121+% tc=(rf.t(first)+rf.t(last))/2;
122+% else
123+% tc=rf.t(first-1+ic);
124+% end
125+end
src/engine/pulseq/+mr/calcRfPower.madded+92−0View file
@@ -0,0 +1,92 @@
1+function [total_energy, peak_pwr, rf_rms]=calcRfPower(rf, dt)
2+%calcRfPower Compute relative energy, peak power, and RMS amplitude of an RF pulse.
3+%
4+% PURPOSE
5+% Computes the relative energy, peak power, and RMS B1 amplitude of an
6+% RF pulse by resampling its waveform onto a uniform time grid and
7+% integrating |rf|^2. Returns 'relative' quantities in units of Hz^2
8+% and Hz, not absolute SI power, because Pulseq RF amplitudes are
9+% stored in Hz (gamma-scaled). Used for pulse-level sanity checks and
10+% as a building block for the sequence-level mr.Sequence/calcRfPower.
11+%
12+% SIGNATURES
13+% total_energy = mr.calcRfPower(rf) % default dt=1 us
14+% [total_energy, peak_pwr] = mr.calcRfPower(rf)
15+% [total_energy, peak_pwr, rf_rms] = mr.calcRfPower(rf)
16+% [...] = mr.calcRfPower(rf, dt) % override sampling step
17+%
18+% The two arguments must be passed positionally and in order; they
19+% cannot be given as name/value pairs. Outputs beyond nargout are not
20+% computed.
21+%
22+% INPUTS
23+% rf [required] struct, RF event struct from mr.makeSincPulse,
24+% mr.makeBlockPulse, mr.makeGaussPulse, mr.makeArbitraryRf,
25+% mr.makeSLRpulse, or mr.makeAdiabaticPulse. Must have
26+% fields .t (seconds), .signal (complex Hz), and .shape_dur
27+% (seconds).
28+% dt [optional] double, resampling step in seconds. Default: 1e-6.
29+%
30+% OUTPUT
31+% total_energy double, Hz (= Hz^2 * s), integral of |rf|^2 over the pulse duration
32+% peak_pwr double, Hz^2, maximum of |rf|^2 over the resampled waveform
33+% rf_rms double, Hz, RMS B1 amplitude, sqrt(total_energy/rf.shape_dur)
34+%
35+% NOTES
36+% - Outputs are 'relative': amplitude is in Hz, so total_energy is in
37+% Hz^2 * s and peak_pwr in Hz^2. To convert to SI: divide rf_rms by
38+% sys.gamma to get Tesla; divide total_energy by sys.gamma^2 to get
39+% Tesla^2 * s. Absolute SAR requires further scaling by tx-coil and
40+% subject-specific factors (reference voltage, coil design) and is
41+% not computed here.
42+% - The pulse is resampled onto a uniform grid of step dt with bin
43+% midpoints at ((0:nn-1)+0.5)*dt where nn = round(rf.shape_dur/dt).
44+% Samples outside rf.t are filled with 0 by linear extrapolation.
45+% For pulses already sampled on a uniform raster (sinc, Gauss, SLR,
46+% arbitrary, adiabatic) the default dt=1e-6 oversamples mildly; for
47+% block pulses (rf.t has only the two endpoints) resampling is
48+% required.
49+% - peak_pwr depends on dt because the resampled waveform may not hit
50+% the original peak exactly; for narrow pulses, reduce dt to tighten
51+% the estimate.
52+% - rf_rms uses rf.shape_dur, which excludes any rf.delay or
53+% post-pulse ringdown. For SAR-relevant duty cycle over a sequence
54+% use mr.Sequence/calcRfPower instead.
55+% - rf.freqOffset, rf.phaseOffset, and rf.freqPPM are ignored: the
56+% calculation works on |rf.signal|^2, which is invariant under
57+% frequency or phase modulation.
58+%
59+% EXAMPLE
60+% sys = mr.opts('MaxGrad', 30, 'GradUnit', 'mT/m', ...
61+% 'MaxSlew', 170, 'SlewUnit', 'T/m/s');
62+% rf = mr.makeSincPulse(pi/2, 'Duration', 3e-3, ...
63+% 'SliceThickness', 3e-3, 'apodization', 0.5, ...
64+% 'timeBwProduct', 4, 'system', sys);
65+% [total_energy, peak_pwr, rf_rms] = mr.calcRfPower(rf);
66+% fprintf('energy = %.3g Hz, peak = %.3g Hz^2, rms = %.3g Hz\n', ...
67+% total_energy, peak_pwr, rf_rms);
68+%
69+% SEE ALSO
70+% mr.calcRfBandwidth, mr.calcRfCenter, mr.simRf, mr.makeSincPulse,
71+% mr.makeBlockPulse, mr.makeGaussPulse, mr.makeArbitraryRf,
72+% mr.makeSLRpulse, mr.makeAdiabaticPulse
73+
74+if nargin<2
75+ dt=1e-6; % for now default sampling rate is 1Mhz
76+end
77+
78+% resample the pulse to a resonable time array
79+nn=round(rf.shape_dur/dt);
80+t=((0:(nn-1))+0.5)*dt;
81+rfs=interp1(rf.t,rf.signal,t,'linear',0);
82+% TODO: avoid resampling of already sampled pulses (above)
83+
84+rfs_sq=rfs.*conj(rfs);
85+total_energy=sum(rfs_sq)*dt;
86+if nargout>1
87+ peak_pwr=max(rfs_sq);
88+ if nargout>2
89+ rf_rms=sqrt(total_energy/rf.shape_dur);
90+ end
91+end
92+
src/engine/pulseq/+mr/checkTiming.madded+114−0View file
@@ -0,0 +1,114 @@
1+function [ is_ok, text_error, total_dur ] = checkTiming( system, varargin )
2+%checkTiming(sys, objects, ...)
3+% Function checks whether timing of the specified evets is aligned to
4+% the corresponding raster
5+
6+ if (isempty(varargin))
7+ text_error=['empty or damaged block detected' ];
8+ is_ok=false;
9+ return;
10+ end
11+ total_dur=mr.calcDuration(varargin{:});
12+ is_ok=div_check(total_dur,system.blockDurationRaster);
13+ if (is_ok)
14+ text_error='';
15+ else
16+ text_error=['total duration:' num2str(total_dur*1e6) 'us' ];
17+ end
18+ for i=1:length(varargin)
19+ e=varargin{i};
20+ if isnumeric(e) % special handling for blockDuration
21+ continue;
22+ end
23+ assert(isstruct(e), 'wrong format of the variable aguments, list of structures is expected');
24+ ok=true;
25+ if length(e)>1
26+ % for now this is only the case for arrays of extensions, but
27+ % we actually cannot check extensons anyway...
28+ continue;
29+ end
30+ if isfield(e, 'type') && (strcmp(e.type,'adc') || strcmp(e.type,'rf') || strcmp(e.type,'output'))
31+ raster=system.rfRasterTime;
32+ else
33+ raster=system.gradRasterTime;
34+ end
35+ if isfield(e, 'delay')
36+ if e.delay<-eps
37+ ok=false;
38+ end
39+ if ~div_check(e.delay,raster)
40+ ok=false;
41+ end
42+ end
43+ if isfield(e, 'duration')
44+ if ~div_check(e.duration,raster)
45+ ok=false;
46+ end
47+ end
48+ if isfield(e, 'dwell') % special case ADC
49+ if e.dwell<system.adcRasterTime || abs(round(e.dwell/system.adcRasterTime)*system.adcRasterTime - e.dwell)>1e-10
50+ ok=false;
51+ end
52+ end
53+ if isfield(e, 'type') && strcmp(e.type,'rf')
54+ % check time vector
55+ if ~div_check(e.shape_dur,system.rfRasterTime)
56+ ok=false;
57+ end
58+ if length(e.t)>=4
59+ rt=e.t/system.rfRasterTime;
60+ drt=diff(rt);
61+ if all(abs(drt(2:end)-drt(1))<1e-9/system.rfRasterTime) % 1ns -- /system.rfRasterTime is necessary because 'rt' is in RF raster units
62+ % equal stepping case -- constant dwell time
63+ e.dwell=e.t(2)-e.t(1); % add dummy dwell so that it can be logged in case of error
64+ if ~div_check(e.dwell,min(system.adcRasterTime,system.rfRasterTime)) % should we check against rfRasterTime or adcRasterTime?
65+ ok=false;
66+ end
67+ else
68+ % "extended"-shape -- all points should be on RF raster edges
69+ if any(abs(rt-round(rt))>1e-6)
70+ ok=false; % TODO: add a meaninfull error message, for now it will look very cryptic
71+ end
72+ end
73+ end
74+ end
75+ if isfield(e, 'type') && strcmp(e.type,'trap')
76+ if ~div_check(e.riseTime, system.gradRasterTime) || ~div_check(e.flatTime, system.gradRasterTime) || ~div_check(e.fallTime, system.gradRasterTime)
77+ ok=false;
78+ end
79+ end
80+ if ~ok
81+ is_ok=false;
82+ if ~isempty(text_error)
83+ text_error = [text_error ' '];
84+ end
85+ text_error = [text_error '[ '];
86+ if isfield(e, 'type')
87+ text_error = [text_error 'type:' e.type ' ' ];
88+ end
89+ if isfield(e, 'delay')
90+ text_error = [text_error 'delay:' num2str(e.delay*1e6) 'us ' ];
91+ end
92+ if isfield(e, 'duration')
93+ text_error = [text_error 'duration:' num2str(e.duration*1e6) 'us ' ];
94+ end
95+ if isfield(e, 'shape_dur')
96+ text_error = [text_error 'shape_dur:' num2str(e.shape_dur*1e6) 'us ' ];
97+ end
98+ if isfield(e, 'dwell')
99+ text_error = [text_error 'dwell:' num2str(e.dwell*1e9) 'ns ' ];
100+ end
101+ if isfield(e, 'type') && strcmp(e.type,'trap')
102+ text_error = [text_error 'riseTime:' num2str(e.riseTime*1e6) 'us flatTime:' num2str(e.flatTime*1e6) 'us fallTime:' num2str(e.fallTime*1e6) 'us '];
103+ end
104+ text_error = [text_error ']'];
105+ end
106+ end
107+end
108+
109+function out = div_check(a, b)
110+% checks wheher a can be divided by b to an accuracy of 1e-9
111+ c = a / b;
112+ out = (abs( c - round(c) ) < 1e-9);
113+end
114+
src/engine/pulseq/+mr/compile_mex.madded+56−0View file
@@ -0,0 +1,56 @@
1+%% Compile mex files from MATLAB source
2+% This script is designed to compile frequently used Pulseq functions into
3+% mex files for faster execution.
4+%
5+% The Pulseq toolbox has been designed such that these files are *not*
6+% necessary to use the toolbox, but if they are present a substantial
7+% performance improvement can be acheived.
8+%
9+% The script relies on the MATLAB Coder package, available in later
10+% versions of MATLAB.
11+%
12+
13+if isempty(which('codegen'))
14+ error('codegen not found for this version of MATLAB. Try 2014b or higher');
15+end
16+currDir=pwd;
17+if ~strcmp(currDir(end-2:end),'+mr')
18+ error('Please run from directory: pulseq/matlab/+mr')
19+end
20+
21+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
22+% Create configuration object of class 'coder.MexCodeConfig'.
23+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
24+
25+cfg = coder.config('mex');
26+cfg.GenerateReport = true;
27+cfg.SaturateOnIntegerOverflow = false;
28+cfg.IntegrityChecks = false;
29+cfg.ResponsivenessChecks = false;
30+cfg.ExtrinsicCalls = false;
31+
32+%%
33+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
34+% Define argument types for entry-point 'compressShape_mat'.
35+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
36+ARGS = cell(1,1);
37+ARGS{1} = cell(1,1);
38+ARGS{1}{1} = coder.typeof(0,[Inf 1],[1 0]);
39+
40+codegen -config cfg -o compressShape_mex ./compressShape_mat.m -args ARGS{1}
41+
42+%%
43+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
44+% Define argument types for entry-point 'find_mat'.
45+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
46+ARGS = cell(1,1);
47+ARGS{1} = cell(4,1);
48+ARGS{1}{1} = coder.typeof(0,[1 Inf],[0 1]);
49+ARGS{1}{2} = struct;
50+ARGS{1}{2}.array = coder.typeof(0,[1 Inf],[0 1]);
51+ARGS{1}{2} = coder.typeof(ARGS{1}{2},[1 Inf],[0 1]);
52+ARGS{1}{3} = coder.typeof(0,[1 Inf],[0 1]);
53+ARGS{1}{4} = coder.typeof(0,[1 Inf],[0 1]);
54+
55+codegen -config cfg -o ./@EventLibrary/find_mex ./@EventLibrary/find_mat.m -args ARGS{1}
56+
src/engine/pulseq/+mr/compressShape.madded+61−0View file
@@ -0,0 +1,61 @@
1+function s=compressShape(w, forceCompression)
2+%compressShape Compress a gradient or pulse shape.
3+% s=compressShape(w) Compress the waveform using a run-length compression
4+% scheme on the derivative. This strategy encodes constant and linear
5+% waveforms with very few samples. A structure is returned with the
6+% fields:
7+% num_samples - the number of samples in the uncompressed waveform
8+% data - containing the compressed waveform
9+%
10+% See also decompressShape
11+
12+if nargin<2
13+ forceCompression=false;
14+end
15+
16+if any(~isfinite(w))
17+ error('compressShape() received infinite samples');
18+end
19+
20+if ~forceCompression && length(w) <= 4 % avoid compressing very short shapes
21+ s.num_samples=length(w);
22+ s.data = w(:)';
23+ return;
24+end
25+
26+
27+% %MZ: old code with implicit quantization
28+% data = [w(1); diff(w(:))];
29+% maskChanges = [true; abs(diff(data))>1e-8]; % TRUE if values change
30+% vals = data(maskChanges); % Elements without repetitions
31+
32+% MZ: explicit quantization with error correction
33+quant_fac=1e-7; % single precision floating point has ~7.25 decimal places
34+ws=w./quant_fac;
35+datq=round([ws(1); diff(ws(:))]);
36+qerr=ws(:)-cumsum(datq);
37+qcor=[0; diff(round(qerr))];
38+datd=datq+qcor;
39+maskChanges=[true; diff(datd)~=0];
40+vals=datd(maskChanges).*quant_fac; % Elements without repetitions
41+
42+k = find([maskChanges', true]); % Indices of changes
43+n = diff(k)'; % Number of repetitions
44+
45+% Encode in Pulseq format
46+nExtra=n-2;
47+vals2=vals;
48+vals2(nExtra<0)=nan;
49+nExtra(nExtra<0)=nan;
50+v=[vals vals2 nExtra]';
51+v=v(isfinite(v));
52+v(abs(v)<1e-10)=0;
53+s.num_samples = length(w);
54+% decide whether compression makes sense, otherwise store the original
55+if forceCompression || s.num_samples > length(v)
56+ s.data = v';
57+else
58+ s.data = w(:)';
59+end
60+
61+
src/engine/pulseq/+mr/compressShape_mat.madded+18−0View file
@@ -0,0 +1,18 @@
1+function s=compressShape_mat(w, forceCompression)
2+%compressShape Compress a gradient or pulse shape.
3+% s=compressShape(w) Compress the waveform using a run-length compression
4+% scheme on the derivative. This strategy encodes constant and linear
5+% waveforms with very few samples. A structure is returned with the
6+% fields:
7+% num_samples - the number of samples in the uncompressed waveform
8+% data - containing the compressed waveform
9+%
10+% See also decompressShape
11+
12+if nargin<2
13+ forceCompression=false;
14+end
15+
16+s=mr.compressShape(w(:), forceCompression);
17+
18+end
src/engine/pulseq/+mr/convert.madded+84−0View file
@@ -0,0 +1,84 @@
1+function out=convert(in,varargin)
2+%OPTS Set gradient limits of the MR system.
3+% out=convert(in,from,to) Convert the numerical data in, given in
4+% specificed units 'from' to units specified in 'to'.
5+%
6+% Valid unit strings are:
7+% 'Hz/m' 'mT/m' 'rad/ms/mm' 'Hz/m/s' 'mT/m/ms' 'T/m/s' 'rad/ms/mm/ms'
8+
9+persistent parser
10+validB1Units={'Hz','T','mT','uT'}; % todo: gauss?
11+validGradUnits={'Hz/m','mT/m','rad/ms/mm'};
12+validSlewUnits={'Hz/m/s','mT/m/ms','T/m/s','rad/ms/mm/ms'};
13+validUnits=cat(2,validB1Units,validGradUnits,validSlewUnits);
14+if isempty(parser)
15+ parser = mr.aux.InputParserCompat;
16+ parser.FunctionName = 'convert';
17+ parser.addRequired('in',@isnumeric);
18+ parser.addRequired('fromUnit',...
19+ @(x) any(validatestring(x,validUnits)));
20+ parser.addOptional('toUnit',[],...
21+ @(x) any(validatestring(x,validUnits)));
22+ parser.addParamValue('gamma',42.576e6,@isnumeric); % Hz/T
23+end
24+parse(parser,in,varargin{:});
25+opt = parser.Results;
26+
27+% Set default output unit if not given
28+if isempty(opt.toUnit)
29+ if ismember(opt.fromUnit,validGradUnits)
30+ opt.toUnit = validGradUnits{1};
31+ elseif ismember(opt.fromUnit,validSlewUnits)
32+ opt.toUnit = validSlewUnits{1};
33+ elseif ismember(opt.fromUnit,validB1Units)
34+ opt.toUnit = validB1Units{1};
35+ end
36+end
37+
38+% Verify fromUnit and toUnit are in the same category.
39+if ismember(opt.fromUnit,validB1Units), fromCat = 'B1';
40+elseif ismember(opt.fromUnit,validGradUnits), fromCat = 'gradient';
41+elseif ismember(opt.fromUnit,validSlewUnits), fromCat = 'slew rate';
42+end
43+
44+if ismember(opt.toUnit,validB1Units), toCat = 'B1';
45+elseif ismember(opt.toUnit,validGradUnits), toCat = 'gradient';
46+elseif ismember(opt.toUnit,validSlewUnits), toCat = 'slew rate';
47+end
48+
49+if ~strcmp(fromCat,toCat)
50+ error('mr.convert: fromUnit ''%s'' (%s) and toUnit ''%s'' (%s) are in different unit categories.', ...
51+ opt.fromUnit, fromCat, opt.toUnit, toCat);
52+end
53+
54+% Convert to standard units
55+switch opt.fromUnit
56+ case {'Hz','Hz/m','Hz/m/s'}
57+ standard = in;
58+ case {'mT','mT/m'};
59+ standard = in*1e-3*opt.gamma;
60+ case {'uT'};
61+ standard = in*1e-6*opt.gamma;
62+ case 'rad/ms/mm'
63+ standard = in*1e6/(2*pi);
64+ case {'T','mT/m/ms','T/m/s'}
65+ standard = in*opt.gamma;
66+ case 'rad/ms/mm/ms'
67+ standard = in*1e9/(2*pi);
68+end
69+
70+% Convert from standard units
71+switch opt.toUnit
72+ case {'Hz','Hz/m','Hz/m/s'}
73+ out = standard;
74+ case {'mT','mT/m'}
75+ out = 1e3*standard/opt.gamma;
76+ case 'uT'
77+ out = 1e6*standard/opt.gamma;
78+ case 'rad/ms/mm'
79+ out = standard*2*pi*1e-6;
80+ case {'T','mT/m/ms','T/m/s'}
81+ out = standard/opt.gamma;
82+ case 'rad/ms/mm/ms'
83+ out = standard*2*pi*1e-9;
84+end
src/engine/pulseq/+mr/decompressShape.madded+69−0View file
@@ -0,0 +1,69 @@
1+function w = decompressShape(shape, forceDecompression)
2+%decompressShape Decompress a gradient or pulse shape.
3+% w=decompressShape(shape) Decompress the shape compressed with a run-length
4+% compression scheme on the derivative. The given shape is structure with
5+% the following fields:
6+% num_samples - the number of samples in the uncompressed waveform
7+% data - containing the compressed waveform
8+%
9+% See also compressShape
10+
11+
12+dataPack = shape.data;
13+dataPackLen = length(dataPack);
14+numSamples=shape.num_samples;
15+
16+if nargin<2
17+ forceDecompression=false;
18+end
19+
20+if ~forceDecompression && numSamples==dataPackLen
21+ % uncompressed shape
22+ w=dataPack';
23+ return;
24+end
25+
26+w= zeros(1, numSamples) ; % pre-allocate the result matrix
27+ % dimensons: (1,length of the data set)
28+
29+% decompression starts here
30+
31+dataPackDiff = dataPack(2:end) - dataPack(1:end-1);
32+
33+% when dataPackDiff == 0 the subsequent samples are equal ==> marker for
34+% repeats (run-length encoding)
35+dataPackMarkers=find(dataPackDiff==0.0);
36+
37+countPack= 1; % counter 1: points to the current compressed sample
38+countUnpack= 1; % counter 2: points to the current uncompressed sample
39+
40+for i=1:length(dataPackMarkers)
41+ nextPack=dataPackMarkers(i); % careful, this index may have "false positives" , e.g. if the value 3 repeats 3 times, then we will have 3 3 3
42+ currUnpackSamples=nextPack-countPack;
43+ if currUnpackSamples < 0 % this rejects false positives
44+ continue;
45+ elseif currUnpackSamples > 0 % do we have an unpacked block to copy?
46+ w(countUnpack:(countUnpack+currUnpackSamples-1)) = dataPack(countPack:(nextPack-1));
47+ countPack = countPack + currUnpackSamples;
48+ countUnpack = countUnpack + currUnpackSamples;
49+ end
50+ % now comes the packed/repeated section
51+ rep=dataPack(countPack+2)+2;
52+ w(countUnpack:(countUnpack+rep-1))=dataPack(countPack);
53+ countPack= countPack + 3;
54+ countUnpack= countUnpack + rep;
55+end
56+
57+% samples left?
58+if (countPack<=dataPackLen)
59+ assert(dataPackLen-countPack==numSamples-countUnpack);
60+ % copy the rest of the shape, it is unpacked
61+ w(countUnpack:end)= dataPack(countPack:end);
62+end
63+
64+w = cumsum(w);
65+w=w(:);
66+
67+% % test the new function against the old slow version
68+% w1=mr.decompressShape0(shape);
69+% assert(all(w==w1));
src/engine/pulseq/+mr/getSupportedLabels.madded+23−0View file
@@ -0,0 +1,23 @@
1+function supported_labels = getSupportedLabels()
2+% auxilary function
3+
4+supported_labels=mr.aux.globalVars('get','SupportedLabels');
5+if isempty(supported_labels)
6+ supported_labels={ ...
7+ ... % data counters
8+ 'SLC','SEG','REP','AVG','SET','ECO','PHS','LIN','PAR','ACQ', ... % these are copied to the corresponding MDH fields on Siemens
9+ 'TRID', ... # an integer ID of the TR (sequence segment) used by the GE interpreter (and some others) to optimize the execution on the scanner
10+ ... % data flags
11+ 'NAV','REV','SMS', ... % these are copied to the corresponding MDH fields on Siemens
12+ 'REF', 'IMA', ... % flags for parallel imaging
13+ 'OFF', ... % Offline flag that labels the data, that should not be used for the online-reconstruction (on Siemens it negates the ONLINE MDH flag)
14+ 'NOISE', ... % flag marking noise adjust scan, for parallel imaging acceleration
15+ ... % control flags/switches -- they are not affecting the data but rather the sequence itself
16+ 'PMC', ... # for MoCo/PMC Pulseq version to recognize blocks that can be prospectively corrected for motion
17+ 'NOROT','NOPOS','NOSCL', ... # instruct the interpreter to ignore the position, rotation or scaling of the FOV specified on the UI
18+ 'ONCE' ... # a 3-state flag that instructs the interpreter to alter the sequence when executing multiple repeats as follows: blocks with ONCE==0 are executed on every repetition; ONCE==1: only the first repetition; ONCE==2: only the last repetition
19+ };
20+ mr.aux.globalVars('set','SupportedLabels',supported_labels);
21+end
22+
23+end
src/engine/pulseq/+mr/getSupportedRfUse.madded+9−0View file
@@ -0,0 +1,9 @@
1+function [supported_rf_use, short_rf_use] = getSupportedRfUse()
2+% auxilary function
3+
4+supported_rf_use={'excitation','refocusing','inversion','saturation','preparation','other','undefined'};
5+if nargout>1
6+ short_rf_use=cell2mat(cellfun (@(x) x(1),supported_rf_use,'un',0));
7+end
8+
9+end
src/engine/pulseq/+mr/makeAdc.madded+146−0View file
@@ -0,0 +1,146 @@
1+function adc=makeAdc(num,varargin)
2+%makeAdc Create an ADC readout event.
3+%
4+% PURPOSE
5+% Build an ADC (analog-to-digital converter) sampling event struct.
6+% The returned struct describes when sampling occurs, how many
7+% samples are acquired, and at what dwell rate, and is consumed by
8+% mr.Sequence/addBlock to add the readout to a sequence.
9+%
10+% SIGNATURES
11+% adc = mr.makeAdc(numSamples, 'Duration', d)
12+% adc = mr.makeAdc(numSamples, 'Dwell', dt)
13+% adc = mr.makeAdc(numSamples, system, ...) % system as 2nd positional arg
14+% adc = mr.makeAdc(numSamples, ..., 'system', system) % system as name/value
15+% adc = mr.makeAdc(numSamples, ..., 'Delay', d)
16+%
17+% Exactly one of 'Duration' or 'Dwell' must be supplied.
18+% If system is omitted, mr.opts() is used.
19+% Parameter names are case-insensitive.
20+%
21+% INPUTS
22+% numSamples [required] integer, number of ADC samples (must be a whole number)
23+% system [optional] struct from mr.opts; defaults to mr.opts() if omitted
24+% 'Duration' [name/value] double, total acquisition duration, seconds. Dwell is
25+% computed as Duration/numSamples.
26+% 'Dwell' [name/value] double, per-sample dwell time, seconds. Total
27+% acquisition time is Dwell*numSamples.
28+% 'Delay' [name/value] double, delay before sampling starts, seconds,
29+% default 0. Silently bumped to system.adcDeadTime
30+% if smaller (see NOTES).
31+% 'freqOffset' [name/value] double, demodulation frequency offset, Hz, default 0
32+% 'phaseOffset' [name/value] double, demodulation phase offset, radians, default 0
33+% 'freqPPM' [name/value] double, frequency offset in PPM (relative to system
34+% B0 and gamma), default 0
35+% 'phasePPM' [name/value] double, phase offset in PPM, default 0
36+% 'phaseModulation' [name/value] vector, per-sample phase modulation, length must
37+% equal numSamples. Default: [] (no modulation).
38+%
39+% OUTPUT
40+% adc struct with fields (in order returned by fieldnames):
41+% .type char, always 'adc'
42+% .numSamples integer, number of samples (= input numSamples)
43+% .delay double, delay before sampling, seconds
44+% (>= system.adcDeadTime; see NOTES)
45+% .freqOffset double, Hz
46+% .phaseOffset double, radians
47+% .freqPPM double, PPM
48+% .phasePPM double, PPM
49+% .deadTime double, copy of system.adcDeadTime at construction, seconds
50+% .phaseModulation double vector or []
51+% .dwell double, sample dwell time, seconds
52+% (= Duration/numSamples if Duration was given,
53+% else the supplied Dwell)
54+%
55+% ERRORS
56+% - 'Either dwell or duration must be defined': both are 0, or both > 0.
57+% Exactly one must be specified.
58+% - 'ADC Phase modulation vector must have the same length as the number
59+% of samples': length(phaseModulation) ~= numSamples.
60+% - MATLAB:InputParser:* errors: numSamples is not an integer, or other
61+% validator failures (non-numeric where numeric is expected, etc.).
62+%
63+% NOTES
64+% - The .delay field is silently increased to system.adcDeadTime if the
65+% requested delay is smaller. If you set 'Delay' to 0 with a nonzero
66+% system.adcDeadTime, the returned adc.delay will equal system.adcDeadTime,
67+% not 0. Account for this when computing block timing.
68+% - system.adcDeadTime is captured into adc.deadTime at construction time;
69+% changing system later does not retroactively update existing adc events.
70+% - Caches an inputParser in a persistent variable for performance;
71+% no other global state.
72+%
73+% EXAMPLE
74+% sys = mr.opts('MaxGrad', 30, 'GradUnit', 'mT/m', ...
75+% 'MaxSlew', 170, 'SlewUnit', 'T/m/s', ...
76+% 'adcDeadTime', 20e-6);
77+% Nx = 256; fov = 256e-3; deltak = 1/fov;
78+% gx = mr.makeTrapezoid('x', sys, 'FlatArea', Nx*deltak, 'FlatTime', 6.4e-3);
79+% % ADC sampled across the readout flat top
80+% adc = mr.makeAdc(Nx, sys, 'Duration', gx.flatTime, 'Delay', gx.riseTime);
81+%
82+% SEE ALSO
83+% mr.opts, mr.makeTrapezoid, mr.Sequence/addBlock, mr.calcDuration
84+
85+persistent parser
86+if isempty(parser)
87+ parser = mr.aux.InputParserCompat;
88+ parser.FunctionName = 'makeAdc';
89+
90+ addRequired(parser,'numSamples',@(x)(isnumeric(x) && (fix(x)-x)==0));
91+ addOptional(parser,'system',[],@isstruct);
92+ addParamValue(parser,'dwell',0,@isnumeric);
93+ addParamValue(parser,'duration',0,@isnumeric);
94+ addParamValue(parser,'delay',0,@isnumeric);
95+ addParamValue(parser,'freqOffset',0,@isnumeric);
96+ addParamValue(parser,'phaseOffset',0,@isnumeric);
97+ addParamValue(parser,'freqPPM', 0, @isnumeric);
98+ addParamValue(parser,'phasePPM', 0, @isnumeric);
99+ addParamValue(parser,'phaseModulation',[],@isnumeric);
100+end
101+
102+parse(parser,num,varargin{:});
103+opt = parser.Results;
104+
105+if isempty(opt.system)
106+ system=mr.opts();
107+else
108+ system=opt.system;
109+end
110+
111+adc.type = 'adc';
112+adc.numSamples = num;
113+adc.delay = opt.delay;
114+adc.freqOffset = opt.freqOffset;
115+adc.phaseOffset = opt.phaseOffset;
116+adc.freqPPM = opt.freqPPM;
117+adc.phasePPM = opt.phasePPM;
118+adc.deadTime = system.adcDeadTime;
119+
120+if (opt.dwell==0 && opt.duration==0) || (opt.dwell>0 && opt.duration>0)
121+ error('Either dwell or duration must be defined');
122+end
123+
124+if ~isempty(opt.phaseModulation)
125+ if length(opt.phaseModulation)~=num
126+ error('ADC Phase modulation vector must have the same length as the number of samples');
127+ end
128+ adc.phaseModulation=opt.phaseModulation;
129+else
130+ adc.phaseModulation=[];
131+end
132+
133+if opt.duration > 0
134+ adc.dwell = opt.duration/opt.numSamples;
135+else
136+ adc.dwell = opt.dwell;
137+end
138+%if opt.dwell > 0
139+% adc.duration = opt.dwell*opt.numSamples;
140+%end
141+if adc.deadTime > adc.delay
142+ adc.delay = adc.deadTime; % adcDeadTime is added before the actual sampling (and also second time after the sampling period)
143+end
144+
145+end
146+
src/engine/pulseq/+mr/makeAdiabaticPulse.madded+290−0View file
@@ -0,0 +1,290 @@
1+function [rf, gz, gzr, delay] = makeAdiabaticPulse(type,varargin)
2+%makeAdiabaticPulse make an aiabatic inversion pulse
3+% a wrapper to a python function(see below). See supported params below
4+% in the 'parser' section. Currently it will probably only work on
5+% Linux. On my system I could install the required Python library by
6+% executing "pip3 install sigpy"
7+% type must be one of {'hypsec','wurst'}
8+% BE CAREFUL, some parameters only affect certain pulse types and are
9+% ignored for other; e.g. bandwidth is ignored if type='hypsec'.
10+%
11+% hypsec(n=512, beta=800, mu=4.9, dur=0.012)
12+% Design a hyperbolic secant adiabatic pulse.
13+%
14+% mu * beta becomes the amplitude of the frequency sweep
15+%
16+% Args:
17+% n (int): number of samples (should be a multiple of 4).
18+% beta (float): AM waveform parameter.
19+% mu (float): a constant, determines amplitude of frequency sweep.
20+% dur (float): pulse time (s).
21+%
22+% Returns:
23+% 2-element tuple containing
24+%
25+% - **a** (*array*): AM waveform.
26+% - **om** (*array*): FM waveform (radians/s).
27+%
28+% References:
29+% Baum, J., Tycko, R. and Pines, A. (1985). 'Broadband and adiabatic
30+% inversion of a two-level system by phase-modulated pulses'.
31+% Phys. Rev. A., 32:3435-3447.
32+%
33+% wurst(n=512, n_fac=40, bw=40000.0, dur=0.002)
34+% Design a WURST (wideband, uniform rate, smooth truncation) adiabatic
35+% inversion pulse
36+%
37+% Args:
38+% n (int): number of samples (should be a multiple of 4).
39+% n_fac (int): power to exponentiate to within AM term. ~20 or greater is
40+% typical.
41+% bw (float): pulse bandwidth.
42+% dur (float): pulse time (s).
43+%
44+%
45+% Returns:
46+% 2-element tuple containing
47+% - **a** (*array*): AM waveform.
48+% - **om** (*array*): FM waveform (radians/s).
49+%
50+% References:
51+% Kupce, E. and Freeman, R. (1995). 'Stretched Adiabatic Pulses for
52+% Broadband Spin Inversion'.
53+% J. Magn. Reson. Ser. A., 117:246-256.
54+
55+
56+validPulseTypes = {'hypsec','wurst'};
57+validPulseUses = mr.getSupportedRfUse();
58+
59+persistent parser
60+if isempty(parser)
61+ parser = mr.aux.InputParserCompat;
62+ parser.FunctionName = 'makeAdiabaticPulse';
63+
64+ % RF params
65+ addRequired(parser, 'type', @(x) any(validatestring(x,validPulseTypes)));
66+ addOptional(parser, 'system', [], @isstruct);
67+ addParamValue(parser, 'duration', 10e-3, @isnumeric);
68+ addParamValue(parser, 'freqOffset', 0, @isnumeric);
69+ addParamValue(parser, 'phaseOffset', 0, @isnumeric);
70+ addParamValue(parser, 'freqPPM', 0, @isnumeric);
71+ addParamValue(parser, 'phasePPM', 0, @isnumeric);
72+ addParamValue(parser, 'beta', 800, @isnumeric);
73+ addParamValue(parser, 'mu', 4.9, @isnumeric);
74+ addParamValue(parser, 'n_fac', 40, @isnumeric);
75+ addParamValue(parser, 'bandwidth', 40000, @isnumeric);
76+ addParamValue(parser, 'adiabaticity', 4, @isnumeric);
77+ % Slice params
78+ addParamValue(parser, 'maxGrad', 0, @isnumeric);
79+ addParamValue(parser, 'maxSlew', 0, @isnumeric);
80+ addParamValue(parser, 'sliceThickness', 0, @isnumeric);
81+ addParamValue(parser, 'delay', 0, @isnumeric);
82+ addParamValue(parser, 'dwell', 0, @isnumeric); % dummy default value
83+ % whether it is a refocusing pulse (for k-space calculation)
84+ addParamValue(parser, 'use', 'u', @(x) any(validatestring(x,validPulseUses)));
85+ % optional Python command
86+ addParamValue(parser, 'pythonCmd', '', @(x)isstring(x)||ischar(x));
87+end
88+
89+parse(parser, type, varargin{:});
90+opt = parser.Results;
91+
92+if isempty(opt.system)
93+ sys=mr.opts();
94+else
95+ sys=opt.system;
96+end
97+
98+if opt.dwell==0
99+ opt.dwell=sys.rfRasterTime;
100+end
101+
102+% find/check python
103+if ~isempty(opt.pythonCmd)
104+ [status, result]=system([opt.pythonCmd ' --version']);
105+ if status~=0
106+ error(['provided python executable ''' opt.pythonCmd ''' returns an error on the version check']);
107+ end
108+ if ispc
109+ [status, result] = system(sprintf('%s -c "import sigpy" 2>nul',opt.pythonCmd));
110+ else
111+ [status, result] = system(sprintf('%s -c "import sigpy" 2>/dev/null',opt.pythonCmd));
112+ end
113+ if status~=0
114+ error(['provided python executable ''' opt.pythonCmd ''' returns an error on the sigPy check']);
115+ end
116+ python=opt.pythonCmd;
117+else
118+ [avail, python]=mr.aux.isSigPyAvailable();
119+ if ~avail
120+ error('python executable with installed sigPy not found, please check your system PATH settings and Python installation');
121+ end
122+end
123+% add quotes in case Python install path contains spaces or alike characters
124+if python(1)~='"'
125+ python=['"' python '"'];
126+end
127+
128+Nraw = round(opt.duration/opt.dwell+eps);
129+N = floor(Nraw/4)*4; % number of points must be divisible by four -- this is a requirement of the underlying library
130+
131+if ispc()
132+ switch type
133+ case 'hypsec'
134+ cmd=[python ' -c "import sigpy.mri.rf;pulse=sigpy.mri.rf.hypsec(' ... % hypsec(n=512, beta=800, mu=4.9, dur=0.012)
135+ 'n=' num2str(N) ',beta=' num2str(opt.beta) ',' ...
136+ 'mu=' num2str(opt.mu) ',dur=' num2str(opt.duration) ...
137+ ');print(*pulse[0]);print(*pulse[1])"'];
138+ case 'wurst'
139+ cmd=[python ' -c "import sigpy.mri.rf;pulse=sigpy.mri.rf.wurst(' ... % wurst(n=512, n_fac=40, bw=40000.0, dur=0.002)
140+ 'n=' num2str(N) ',n_fac=' num2str(opt.n_fac) ',' ...
141+ 'bw=' num2str(opt.bandwidth) ',dur=' num2str(opt.duration) ...
142+ ');print(*pulse[0]);print(*pulse[1])"'];
143+ otherwise
144+ error('unsupported adiabatic pulse type');
145+ end
146+else
147+ switch type
148+ case 'hypsec'
149+ cmd=[python ' -c $''import sigpy.mri.rf\npulse=sigpy.mri.rf.hypsec(' ... % hypsec(n=512, beta=800, mu=4.9, dur=0.012)
150+ 'n=' num2str(N) ',beta=' num2str(opt.beta) ',' ...
151+ 'mu=' num2str(opt.mu) ',dur=' num2str(opt.duration) ...
152+ ')\nprint(*pulse[0])\nprint(*pulse[1])'''];
153+ case 'wurst'
154+ cmd=[python ' -c $''import sigpy.mri.rf\npulse=sigpy.mri.rf.wurst(' ... % wurst(n=512, n_fac=40, bw=40000.0, dur=0.002)
155+ 'n=' num2str(N) ',n_fac=' num2str(opt.n_fac) ',' ...
156+ 'bw=' num2str(opt.bandwidth) ',dur=' num2str(opt.duration) ...
157+ ')\nprint(*pulse[0])\nprint(*pulse[1])'''];
158+ otherwise
159+ error('unsupported adiabatic pulse type');
160+ end
161+end
162+%fprintf('cmd=%s\n',cmd);
163+[status, result]=system(cmd);
164+
165+if status~=0
166+ error('executing python command failed');
167+end
168+
169+lines = regexp(result,'\n','split'); % the response from the python call contains some garbage
170+% look for two usable result vectors
171+for i=1:length(lines)-1
172+ try
173+ am=str2num(lines{i});
174+ fm=str2num(lines{i+1});
175+ if length(am)==N && length(fm)==N
176+ break;
177+ end
178+ catch
179+ continue;
180+ end
181+end
182+if length(am)~=N || length(fm)~=N
183+ error('could not find usable data in the response of the Python command');
184+end
185+
186+pm=cumsum(fm)*opt.dwell;
187+
188+[dfm,ifm]=min(abs(fm)); % find the center of the pulse
189+% we will also use the ocasion to find the rate of change of the frequency
190+% at the center of the pulse
191+if dfm==0
192+ pm0=pm(ifm);
193+ am0=am(ifm);
194+ roc_fm0=abs(fm(ifm+1)-fm(ifm-1))/2/opt.dwell;
195+else
196+ % we need to bracket the zero-crossing
197+ if fm(ifm)*fm(ifm+1) < 0
198+ b=1;
199+ else
200+ b=-1;
201+ end
202+ pm0=(pm(ifm)*fm(ifm+b)-pm(ifm+b)*fm(ifm))/(fm(ifm+b)-fm(ifm));
203+ am0=(am(ifm)*fm(ifm+b)-am(ifm+b)*fm(ifm))/(fm(ifm+b)-fm(ifm));
204+ roc_fm0=abs(fm(ifm)-fm(ifm+b))/opt.dwell;
205+end
206+pm=pm-pm0;
207+a=(roc_fm0*opt.adiabaticity)^0.5/2/pi/am0;
208+
209+signal = a*am.*exp(1i*pm);
210+
211+if (N~=Nraw)
212+ % we need to pad the signal vector
213+ Npad=Nraw-N;
214+ signal=[zeros(1,Npad-floor(Npad/2)) signal zeros(1,floor(Npad/2))];
215+ N=Nraw;
216+end
217+
218+%BW = opt.timeBwProduct/opt.duration;
219+t = ((1:N)-0.5)*opt.dwell;
220+%flip = abs(sum(signal))*opt.dwell*2*pi;
221+
222+rf.type = 'rf';
223+rf.signal = signal;
224+rf.t = t;
225+rf.shape_dur=N*opt.dwell;
226+rf.freqOffset = opt.freqOffset;
227+rf.phaseOffset = opt.phaseOffset;
228+rf.freqPPM = opt.freqPPM;
229+rf.phasePPM = opt.phasePPM;
230+rf.deadTime = sys.rfDeadTime;
231+rf.ringdownTime = sys.rfRingdownTime;
232+rf.delay = opt.delay;
233+rf.center = mr.calcRfCenter(rf);
234+if ~isempty(opt.use)
235+ rf.use=opt.use;
236+else
237+ rf.use='inversion';
238+end
239+if rf.deadTime > rf.delay
240+ rf.delay = rf.deadTime;
241+end
242+
243+if nargout > 1
244+ assert(opt.sliceThickness > 0,'SliceThickness must be provided');
245+ if opt.maxGrad > 0
246+ sys.maxGrad = opt.maxGrad;
247+ end
248+ if opt.maxSlew > 0
249+ sys.maxSlew = opt.maxSlew;
250+ end
251+
252+ switch type
253+ case 'hypsec'
254+ BW=mr.calcRfBandwidth(rf,0.1);
255+ case 'wurst'
256+ BW=opt.bandwidth;
257+ otherwise
258+ error('unsupported pulse type')
259+ end
260+
261+ amplitude = BW/opt.sliceThickness;
262+ area = amplitude*opt.duration;
263+ gz = mr.makeTrapezoid('z', sys, 'flatTime', opt.duration, ...
264+ 'flatArea', area);
265+ gzr= mr.makeTrapezoid('z', sys, 'Area', -area*(1-rf.center/rf.shape_dur)-0.5*(gz.area-area));
266+ if rf.delay > gz.riseTime
267+ gz.delay = ceil((rf.delay - gz.riseTime)/sys.gradRasterTime)*sys.gradRasterTime; % round-up to gradient raster
268+ end
269+ if rf.delay < (gz.riseTime+gz.delay)
270+ rf.delay = gz.riseTime+gz.delay; % these are on the grad raster already which is coarser
271+ end
272+end
273+
274+% v1.4 finally eliminates RF zerofilling
275+% if rf.ringdownTime > 0
276+% tFill = (1:round(rf.ringdownTime/1e-6))*1e-6; % Round to microsecond
277+% rf.t = [rf.t rf.t(end)+tFill];
278+% rf.signal = [rf.signal, zeros(size(tFill))];
279+% end
280+if nargout > 3
281+ delay=mr.makeDelay(mr.calcDuration(rf)); % calcDuration already includes the ringdown time
282+end
283+
284+% RF amplitude check
285+rf_amplitude=max(abs(rf.signal));
286+if rf_amplitude>sys.maxB1
287+ warning('WARNING: system maximum RF amplitude exceeded (%.01f%%)', rf_amplitude/sys.maxB1*100);
288+end
289+
290+end
src/engine/pulseq/+mr/makeArbitraryGrad.madded+102−0View file
@@ -0,0 +1,102 @@
1+function grad=makeArbitraryGrad(channel,varargin)
2+%makeArbitraryGrad Create an gradient event with arbitrary waveform.
3+% g=makeArbitraryGrad(channel, waveform) Create gradient on
4+% the given channel with the specified waveform.
5+%
6+% g=makeArbitraryGrad(channel,waveform,lims) Ensure the waveform
7+% satisfies the gradient hardware constraints.
8+%
9+% See also Sequence.addBlock
10+
11+persistent parser
12+
13+if isempty(parser)
14+ validChannels = {'x','y','z'};
15+ parser = mr.aux.InputParserCompat;
16+ parser.FunctionName = 'makeArbitraryGrad';
17+ parser.addRequired('channel',...
18+ @(x) any(validatestring(x,validChannels)));
19+ parser.addRequired('waveform');
20+ parser.addOptional('system', [], @isstruct);
21+ parser.addParamValue('oversampling',false,@islogical);
22+ parser.addParamValue('maxGrad',0,@isnumeric);
23+ parser.addParamValue('maxSlew',0,@isnumeric);
24+ parser.addParamValue('delay',0,@isnumeric);
25+ parser.addParamValue('first',NaN,@isnumeric);
26+ parser.addParamValue('last',NaN,@isnumeric);
27+end
28+parse(parser,channel,varargin{:});
29+opt = parser.Results;
30+
31+if isempty(opt.system)
32+ system=mr.opts();
33+else
34+ system=opt.system;
35+end
36+
37+maxSlew=system.maxSlew;
38+maxGrad=system.maxGrad; % TODO: use this when no duration is supplied
39+if opt.maxGrad>0
40+ maxGrad=opt.maxGrad;
41+end
42+if opt.maxSlew>0
43+ maxSlew=opt.maxSlew;
44+end
45+
46+g=opt.waveform(:);
47+
48+if isfinite(opt.first)
49+ first = opt.first;
50+else
51+ warning('it will be compulsory to provide the first point of the gradient shape in the future releases; finding the first by extrapolation for now...');
52+ if opt.oversampling
53+ first = 2*g(1)-g(2); % extrapolate by 1 gradient raster
54+ else
55+ first = (3*g(1)-g(2))*0.5; % extrapolate by 1/2 gradient of the raster
56+ end
57+end
58+
59+if isfinite(opt.last)
60+ last = opt.last;
61+else
62+ warning('it will be compulsory to provide the last point of the gradient shape in the future releases; finding the last by extrapolation for now...');
63+ if opt.oversampling
64+ last = g(end)*2-g(end-1); % extrapolate by 1 gradient raster
65+ else
66+ last = (g(end)*3-g(end-1))*0.5; % extrapolate by 1/2 gradient of the raster
67+ end
68+end
69+
70+if opt.oversampling
71+ slew=[(first-g(1)); (g(2:end)-g(1:end-1)); (last-g(end))]./system.gradRasterTime*2;
72+else
73+ slew=[(first-g(1))*2; (g(2:end)-g(1:end-1)); (g(end)-last)*2]./system.gradRasterTime;
74+end
75+if ~isempty(slew) && max(abs(slew))>maxSlew
76+ error('Slew rate violation (%.0f%%)',max(abs(slew))/maxSlew*100);
77+end
78+if max(abs(g))>maxGrad
79+ error('Gradient amplitude violation (%.0f%%)',max(abs(g))/maxGrad*100);
80+end
81+
82+grad.type = 'grad';
83+grad.channel = opt.channel;
84+grad.waveform = g;
85+grad.delay = opt.delay;
86+% true timing and aux shape data
87+if opt.oversampling
88+ grad.area=sum(grad.waveform(1:2:end))*system.gradRasterTime; % undo oversamping
89+ if (mod(length(g),2)~=1)
90+ error('when oversampling is active the gradient shape vector must contain an odd number of samples');
91+ end
92+ grad.tt = (1:length(g))'*0.5*system.gradRasterTime;
93+ grad.shape_dur = (length(g)+1)*0.5*system.gradRasterTime;
94+else
95+ grad.area=sum(grad.waveform)*system.gradRasterTime;
96+ grad.tt = ((1:length(g))'-0.5)*system.gradRasterTime;
97+ grad.shape_dur = length(g)*system.gradRasterTime;
98+end
99+grad.first = first;
100+grad.last = last;
101+
102+end
src/engine/pulseq/+mr/makeArbitraryRf.madded+154−0View file
@@ -0,0 +1,154 @@
1+function [rf, gz, gzr, delay] = makeArbitraryRf(signal,flip,varargin)
2+%makeArbitraryRf Create an RF pulse with the given pulse shape.
3+% rf=makeArbitraryRf(singal, flip) Create RF pulse with complex signal
4+% and given flip angle (in radians)
5+%
6+% rf=makeArbitraryRf(..., 'freqOffset', f,'phaseOffset',p)
7+% Create arbitrary RF pulse with frequency offset and phase offset.
8+%
9+% rf=makeArbitraryRf(..., 'ppmOffset')
10+% Create arbitrary RF pulse with frequency offset specified in PPM (e.g.
11+% actual frequency offset proportional to the true Larmor frequency); can
12+% be combined with the 'freqOffset' specified in Hz.
13+%
14+% [rf, gz]=makeArbitraryRf(..., 'Bandwidth', bw, 'SliceThickness', st)
15+% Create RF pulse and corresponding slice select gradient. The bandwidth
16+% of the pulse must be given for the specified shape. You can also check
17+% yourself after creating the pulse object by calling mr.calcRfBandwidth()
18+%
19+% See also mr.calcRfBandwidth mr.makeSincPulse, Sequence.addBlock
20+
21+validPulseUses = mr.getSupportedRfUse();
22+
23+persistent parser
24+if isempty(parser)
25+ parser = mr.aux.InputParserCompat;
26+ parser.FunctionName = 'makeArbitraryRf';
27+
28+ % RF params
29+ addRequired(parser, 'signal', @isnumeric);
30+ addRequired(parser, 'flipAngle', @isnumeric);
31+ addOptional(parser, 'system', [], @isstruct);
32+ addParamValue(parser, 'freqOffset', 0, @isnumeric);
33+ addParamValue(parser, 'phaseOffset', 0, @isnumeric);
34+ addParamValue(parser, 'freqPPM', 0, @isnumeric);
35+ addParamValue(parser, 'phasePPM', 0, @isnumeric);
36+ addParamValue(parser, 'timeBwProduct', 0, @isnumeric);
37+ addParamValue(parser, 'bandwidth', 0, @isnumeric);
38+ addParamValue(parser, 'center', NaN, @isnumeric);
39+ % Slice params
40+ addParamValue(parser, 'maxGrad', 0, @isnumeric);
41+ addParamValue(parser, 'maxSlew', 0, @isnumeric);
42+ addParamValue(parser, 'sliceThickness', 0, @isnumeric);
43+ % Delay
44+ addParamValue(parser, 'delay', 0, @isnumeric);
45+ addParamValue(parser, 'dwell', 0, @isnumeric); % dummy default value
46+ % whether it is a refocusing pulse (for k-space calculation)
47+ addParamValue(parser, 'use', 'u', @(x) any(validatestring(x,validPulseUses)));
48+end
49+parse(parser, signal, flip,varargin{:});
50+opt = parser.Results;
51+
52+if isempty(opt.system)
53+ system=mr.opts();
54+else
55+ system=opt.system;
56+end
57+
58+if opt.dwell==0
59+ opt.dwell=system.rfRasterTime;
60+end
61+
62+signal = signal./abs(sum(signal.*opt.dwell))*flip/(2*pi);
63+
64+if size(signal,1)>size(signal,2)
65+ signal=signal.';
66+end
67+
68+N= length(signal);
69+duration = N*opt.dwell;
70+t = ((1:N)'-0.5)*opt.dwell;
71+
72+rf.type = 'rf';
73+rf.signal = signal(:);
74+rf.t = t;
75+rf.shape_dur=duration;
76+rf.freqOffset = opt.freqOffset;
77+rf.phaseOffset = opt.phaseOffset;
78+rf.freqPPM = opt.freqPPM;
79+rf.phasePPM = opt.phasePPM;
80+rf.deadTime = system.rfDeadTime;
81+rf.ringdownTime = system.rfRingdownTime;
82+rf.delay = opt.delay;
83+if ~isempty(opt.use)
84+ rf.use=opt.use;
85+end
86+if rf.deadTime > rf.delay
87+ rf.delay = rf.deadTime;
88+end
89+
90+if isfinite(opt.center)
91+ rf.center=opt.center;
92+ if rf.center < 0, rf.center = 0; end
93+ if rf.center > rf.shape_dur, rf.center = rf.shape_dur; end
94+else
95+ rf.center = mr.calcRfCenter(rf);
96+end
97+
98+if opt.timeBwProduct>0
99+ if opt.bandwidth > 0
100+ error('Both ''bandwidth'' and ''timeBwProduct'' cannot be specified at the same time');
101+ else
102+ opt.bandwidth=opt.timeBwProduct/duration; % QL
103+ end
104+end
105+
106+if nargout>1
107+ assert(opt.sliceThickness > 0, 'SliceThickness must be provided');
108+ assert(opt.bandwidth > 0 || opt.timeBwProduct > 0, 'Bandwidth or BW-time-product of the pulse must be provided');
109+ if opt.maxGrad > 0
110+ system.maxGrad = opt.maxGrad;
111+ end
112+ if opt.maxSlew > 0
113+ system.maxSlew = opt.maxSlew;
114+ end
115+
116+ BW = opt.bandwidth;
117+ if opt.timeBwProduct > 0
118+ BW = opt.timeBwProduct/duration;
119+ end
120+
121+ amplitude = BW/opt.sliceThickness;
122+ area = amplitude*duration;
123+ gz = mr.makeTrapezoid('z', system, 'flatTime', duration, ...
124+ 'flatArea', area);
125+
126+ if rf.delay > gz.riseTime
127+ gz.delay = ceil((rf.delay - gz.riseTime)/system.gradRasterTime)*system.gradRasterTime; % round-up to gradient raster
128+ end
129+ if rf.delay < (gz.riseTime+gz.delay)
130+ rf.delay = gz.riseTime+gz.delay; % these are on the grad raster already which is coarser
131+ end
132+
133+ if nargout > 2
134+ gzr= mr.makeTrapezoid('z', system, 'Area', -area*(1-rf.center)/rf.shape_dur-0.5*(gz.area-area));
135+ end
136+end
137+
138+% v1.4 finally eliminates RF zerofilling
139+% if rf.ringdownTime > 0
140+% tFill = (1:round(rf.ringdownTime/1e-6))*1e-6; % Round to microsecond
141+% rf.t = [rf.t rf.t(end)+tFill];
142+% rf.signal = [rf.signal, zeros(size(tFill))];
143+% end
144+if nargout > 3
145+ delay=mr.makeDelay(mr.calcDuration(rf)); % calcDuration already includes the ringdown time
146+end
147+
148+% RF amplitude check
149+rf_amplitude=max(abs(rf.signal));
150+if rf_amplitude>system.maxB1
151+ warning('WARNING: system maximum RF amplitude exceeded (%.01f%%)', rf_amplitude/system.maxB1*100);
152+end
153+
154+end
src/engine/pulseq/+mr/makeBlockPulse.madded+109−0View file
@@ -0,0 +1,109 @@
1+function [rf, delay] = makeBlockPulse(flip,varargin)
2+%makeBlockPulse Create a block pulse with optional slice selectiveness.
3+% rf=makeBlockPulse(flip, 'Duration', dur) Create block pulse
4+% with given flip angle and duration.
5+%
6+% rf=makeBlockPulse(flip, 'Bandwidth', bw) Create block pulse
7+% with given flip angle and bandwidth (Hz). The duration is calculated as
8+% 1/(4*bw)
9+%
10+% rf=makeBlockPulse(..., 'freqOffset', f,'phaseOffset',p)
11+% Create block pulse with frequency offset and phase offset.
12+%
13+% rf=makeBlockPulse(..., 'ppmOffset')
14+% Create block RF pulse with frequency offset specified in PPM (e.g.
15+% actual frequency offset proportional to the true Larmor frequency); can
16+% be combined with the 'freqOffset' specified in Hz.
17+%
18+% [rf, delay]=makeBlockPulse(...) returns the corresponding delay object
19+% that takes care of the RF ringdown time.
20+%
21+% See also Sequence.addBlock
22+
23+validPulseUses = mr.getSupportedRfUse();
24+
25+persistent parser
26+if isempty(parser)
27+ parser = mr.aux.InputParserCompat;
28+ parser.FunctionName = 'makeBlockPulse';
29+
30+ % RF params
31+ addRequired(parser, 'flipAngle', @isnumeric);
32+ addOptional(parser, 'system', [], @isstruct); % for slice grad
33+ addParamValue(parser, 'duration', 0, @isnumeric);
34+ addParamValue(parser, 'freqOffset', 0, @isnumeric);
35+ addParamValue(parser, 'phaseOffset', 0, @isnumeric);
36+ addParamValue(parser, 'freqPPM', 0, @isnumeric);
37+ addParamValue(parser, 'phasePPM', 0, @isnumeric);
38+ addParamValue(parser, 'timeBwProduct', 0, @isnumeric);
39+ addParamValue(parser, 'bandwidth', 0, @isnumeric);
40+ % Slice params
41+ addParamValue(parser, 'maxGrad', 0, @isnumeric);
42+ addParamValue(parser, 'maxSlew', 0, @isnumeric);
43+ addParamValue(parser, 'sliceThickness', 0, @isnumeric);
44+ % Delay
45+ addParamValue(parser, 'delay', 0, @isnumeric);
46+ % whether it is a refocusing pulse (for k-space calculation)
47+ addParamValue(parser, 'use', 'u', @(x) any(validatestring(x,validPulseUses)));
48+end
49+parse(parser, flip, varargin{:});
50+opt = parser.Results;
51+
52+if isempty(opt.system)
53+ system=mr.opts();
54+else
55+ system=opt.system;
56+end
57+
58+if opt.duration == 0
59+ if opt.timeBwProduct > 0 && opt.bandwidth > 0
60+ opt.duration = opt.timeBwProduct/opt.bandwidth;
61+ elseif opt.bandwidth > 0
62+ opt.duration = 1/(4*opt.bandwidth);
63+ else
64+ error('Either bandwidth or duration must be defined and must be larger than 0');
65+ end
66+end
67+
68+N = round(opt.duration/system.rfRasterTime);
69+if N == 0
70+ error('Duration is too short: it rounds to zero RF raster intervals');
71+end
72+opt.duration = N*system.rfRasterTime; % quantize the duration to the RF raster so that the achieved flip angle is exact
73+t = [0; N]*system.rfRasterTime; % we start at 0 and end at N
74+signal = opt.flipAngle/(2*pi)/opt.duration*ones(size(t));
75+
76+rf.type = 'rf';
77+rf.signal = signal;
78+rf.t = t;
79+rf.shape_dur=t(end);
80+rf.freqOffset = opt.freqOffset;
81+rf.phaseOffset = opt.phaseOffset;
82+rf.freqPPM = opt.freqPPM;
83+rf.phasePPM = opt.phasePPM;
84+rf.deadTime = system.rfDeadTime;
85+rf.ringdownTime = system.rfRingdownTime;
86+rf.delay = opt.delay;
87+rf.center = rf.shape_dur/2;
88+if ~isempty(opt.use)
89+ rf.use=opt.use;
90+end
91+if rf.deadTime > rf.delay
92+ rf.delay = rf.deadTime;
93+end
94+
95+% v1.4 finally eliminates RF zerofilling
96+% if rf.ringdownTime > 0
97+% tFill = (1:round(rf.ringdownTime/1e-6))*1e-6; % Round to microsecond
98+% rf.t = [rf.t rf.t(end)+tFill];
99+% rf.signal = [rf.signal, zeros(size(tFill))];
100+% end
101+if nargout > 1
102+ delay=mr.makeDelay(mr.calcDuration(rf)); % calcDuration already includes the ringdown time
103+end
104+
105+% RF amplitude check
106+rf_amplitude=max(abs(rf.signal));
107+if rf_amplitude>system.maxB1
108+ warning('WARNING: system maximum RF amplitude exceeded (%.01f%%)', rf_amplitude/system.maxB1*100);
109+end
src/engine/pulseq/+mr/makeDelay.madded+14−0View file
@@ -0,0 +1,14 @@
1+function del = makeDelay(delay)
2+%makeDelay Create a delay event.
3+% delay=makeDelay(del) Create delay event with given delay del.
4+%
5+% See also Sequence.addBlock
6+
7+if nargin<1
8+ error('makeDelay:invalidArguments','Must supply a delay');
9+end
10+assert(isfinite(delay) & delay>=0,'makeDelay:invalidDelay',...
11+ 'Delay (%.2f ms) is invalid',delay*1e3);
12+del.type = 'delay';
13+del.delay = delay;
14+end
src/engine/pulseq/+mr/makeDigitalOutputPulse.madded+44−0View file
@@ -0,0 +1,44 @@
1+function trig = makeDigitalOutputPulse(channel, varargin)
2+%makeDigitalOutputPulse Create a digital output pulse event a.k.a. trigger.
3+% trig_out=makeDigitalOutputPulse() Create an output trigger event on a
4+% given channel with optional given delay and
5+% duration.
6+% Possible channel values: 'osc0','osc1','ext1'
7+%
8+% See also Sequence.addBlock
9+
10+persistent parser
11+if isempty(parser)
12+ parser = inputParser;
13+ parser.FunctionName = 'makeDigitalOutputPulse';
14+
15+ addParamValue(parser, 'delay', 0, @isnumeric);
16+ addParamValue(parser, 'duration', 0, @isnumeric); % will replace with gradRadterTime below
17+ addParamValue(parser, 'system', [], @isstruct);
18+end
19+
20+if nargin<1
21+ error('makeDigitalOutputPulse:invalidArguments','Must supply a channel');
22+end
23+
24+parse(parser, varargin{:});
25+opt = parser.Results;
26+
27+if isempty(opt.system)
28+ system=mr.opts();
29+else
30+ system=opt.system;
31+end
32+
33+channel_num=find(strcmp(channel,{'osc0','osc1','ext1'}));
34+assert(~isempty(channel_num) && channel_num>0,'makeDigitalOutputPulse:invalidChannel',...
35+ 'Channel (%s) is invalid',channel);
36+trig.type = 'output';
37+trig.channel=channel;
38+trig.delay = opt.delay;
39+trig.duration = opt.duration;
40+if (trig.duration<=system.gradRasterTime)
41+ trig.duration=system.gradRasterTime;
42+end
43+
44+end
src/engine/pulseq/+mr/makeExtendedTrapezoid.madded+133−0View file
@@ -0,0 +1,133 @@
1+function grad = makeExtendedTrapezoid(channel, varargin)
2+%makeExtendedTrapezoid Create an extended trapezoid gradient event.
3+%
4+% g = makeExtendedTrapezoid(channel, lims, ...
5+% 'times', times, ...
6+% 'amplitudes', amplitudes)
7+% Create a gradient by specifying a set of points (amplitudes) at
8+% specified time points(times) at a given channel with given system
9+% limits. This function returns an extended or arbitrary gradient object,
10+% the latter if 'convert2arbitrary' is set to true.
11+%
12+% See also Sequence.addBlock mr.opts makeTrapezoid
13+%
14+% Stefan Kroboth <stefan.kroboth@uniklinik-freiburg.de>
15+
16+persistent parser
17+
18+if isempty(parser)
19+ validChannels = {'x', 'y', 'z'};
20+ parser = mr.aux.InputParserCompat;
21+ parser.FunctionName = 'makeExtendedTrapezoid';
22+ parser.addRequired('channel', ...
23+ @(x) any(validatestring(x, validChannels)));
24+ parser.addOptional('system',[],@isstruct);
25+ parser.addParamValue('times', 0, @isnumeric);
26+ parser.addParamValue('amplitudes', 0, @isnumeric);
27+ parser.addParamValue('maxGrad', 0, @isnumeric);
28+ parser.addParamValue('maxSlew', 0, @isnumeric);
29+ parser.addParamValue('skip_check', false);
30+ parser.addParamValue('convert2arbitrary', false);
31+
32+end
33+parse(parser,channel,varargin{:});
34+opt = parser.Results;
35+
36+if isempty(opt.system)
37+ system=mr.opts();
38+else
39+ system=opt.system;
40+end
41+
42+if any(size(opt.times) ~= size(opt.amplitudes))
43+ error('Times and amplitudes must have the same length.');
44+end
45+
46+if all(opt.times == 0)
47+ error('At least one of the given times must be non-zero.');
48+end
49+
50+if any(diff(opt.times)<=0)
51+ error('Times must be in ascending order and all times must be distinct.');
52+end
53+
54+if abs(round(opt.times(end)/system.gradRasterTime)*system.gradRasterTime-opt.times(end))>1e-8 % 10ns is an acceptable rounding error
55+ error('The last time point must be on a gradient raster.');
56+end
57+
58+%if all(opt.amplitudes == 0)
59+% error('At least one of the given amplitudes must be non-zero.');
60+%end
61+
62+if opt.skip_check == false && opt.times(1) > 0 && opt.amplitudes(1) ~= 0
63+ error('If first amplitude of a gradient is nonzero, it must connect to previous block!');
64+end
65+
66+maxSlew = system.maxSlew;
67+maxGrad = system.maxGrad;
68+if opt.maxGrad > 0
69+ maxGrad = opt.maxGrad;
70+end
71+if opt.maxSlew > 0
72+ maxSlew = opt.maxSlew;
73+end
74+
75+if (opt.convert2arbitrary)
76+ % represent the extended trapezoid on the regularly sampled time grid
77+ waveform = mr.pts2waveform(opt.times, opt.amplitudes, system.gradRasterTime);
78+ grad = mr.makeArbitraryGrad(channel, waveform, system, ...
79+ 'maxSlew', maxSlew,...
80+ 'maxGrad', maxGrad,...
81+ 'delay', opt.times(1));
82+else
83+ % keep the original possibly irregular sampling
84+ if any(abs(round(opt.times/system.gradRasterTime)*system.gradRasterTime-opt.times)>1e-8) % 10ns is an acceptable rounding error
85+ error('All time points must be on a gradient raster or "convert2arbitrary" option must be used.');
86+ end
87+ % check slew rate and gradient amplitude against the active limits
88+ % the convert2arbitrary branch above gets these checks via mr.makeArbitraryGrad
89+ slew = (opt.amplitudes(2:end)-opt.amplitudes(1:end-1)) ./ (opt.times(2:end)-opt.times(1:end-1));
90+ if ~isempty(slew) && max(abs(slew))>maxSlew
91+ error('Slew rate violation (%.0f%%)',max(abs(slew))/maxSlew*100);
92+ end
93+ if max(abs(opt.amplitudes))>maxGrad
94+ error('Gradient amplitude violation (%.0f%%)',max(abs(opt.amplitudes))/maxGrad*100);
95+ end
96+ %
97+ grad.type = 'grad';
98+ grad.channel = opt.channel;
99+ grad.waveform = opt.amplitudes(:);
100+ grad.delay = round(opt.times(1)/system.gradRasterTime)*system.gradRasterTime;
101+ grad.tt = opt.times(:) - grad.delay;
102+ grad.shape_dur = round(grad.tt(end)/system.gradRasterTime)*system.gradRasterTime;
103+ grad.area=0.5*sum((grad.tt(2:end)-grad.tt(1:end-1)).*(grad.waveform(2:end)+grad.waveform(1:end-1)));
104+end
105+
106+% MZ: although makeArbitraryGrad sets the .first and .last for extended
107+% trapezoids we can do it better
108+grad.first=opt.amplitudes(1);
109+grad.last=opt.amplitudes(end);
110+
111+end
112+
113+
114+
115+% figure; plot(waveform)
116+% waveform = waveform(1:end-2);
117+% opt.times = round(opt.times/system.gradRasterTime)*system.gradRasterTime; % round onto grid
118+% times_diff = diff(opt.times);
119+% amplitudes_diff = diff(opt.amplitudes);
120+% waveform = [];
121+% for ii = 1:length(opt.times)-1
122+% % SK: there are no new points after the end, therefore we dont need to
123+% % handle the overlap situation.
124+% if ii == length(opt.times)-1
125+% crop = 0;
126+% else
127+% crop = system.gradRasterTime;
128+% end
129+% y = amplitudes_diff(ii)/times_diff(ii)*...
130+% (0:system.gradRasterTime:(opt.times(ii+1)-opt.times(ii)-crop))...
131+% + opt.amplitudes(ii);
132+% waveform = [waveform y(1:end)];
133+% end
src/engine/pulseq/+mr/makeExtendedTrapezoidArea.madded+203−0View file
@@ -0,0 +1,203 @@
1+function [grad, times, amplitudes] = makeExtendedTrapezoidArea(channel, grad_start, grad_end, area, sys)
2+% Make the shortest possible extended trapezoid for a given area and edge values
3+% This version is the one with the fixed flat top and was derived from the
4+% corresponding PyPulseq version by Mehmet Emin Öztürk. This implementation
5+% is both faster and more accurate than the previous one and it runs in Octave.
6+% Main methodology in explained in Python version
7+% Some variable names might also be different
8+
9+ if nargin < 5 || isempty(sys)
10+ sys = default_opts(); % Define your default system
11+ end
12+
13+ max_slew = sys.maxSlew * 0.99;
14+ max_grad = sys.maxGrad * 0.99;
15+ raster_time = sys.gradRasterTime;
16+
17+ min_duration = max(round(calc_ramp_time(grad_end, grad_start, max_slew, raster_time) / raster_time), 2);
18+
19+ % Estimate upper bound duration
20+ max_duration = max( ...
21+ [round(calc_ramp_time(0, grad_start, max_slew, raster_time) / raster_time), ...
22+ round(calc_ramp_time(0, grad_end, max_slew, raster_time) / raster_time), ...
23+ min_duration]);
24+
25+ % Try to find a solution linearly
26+ solution = [];
27+ for duration = min_duration:max_duration
28+ solution = find_solution(duration, area, grad_start, grad_end, max_slew, max_grad, raster_time);
29+ if ~isempty(solution)
30+ break;
31+ end
32+ end
33+
34+ % Binary search if linear search failed
35+ if isempty(solution)
36+ duration = max_duration;
37+ while isempty(solution)
38+ duration = duration * 2;
39+ solution = find_solution(duration, area, grad_start, grad_end, max_slew, max_grad, raster_time);
40+ end
41+
42+ solution = binary_search(@(d) find_solution(d, area, grad_start, grad_end, max_slew, max_grad, raster_time), ...
43+ floor(duration/2), duration);
44+ end
45+
46+ time_ramp_up = solution(1) * raster_time;
47+ flat_time = solution(2) * raster_time;
48+ time_ramp_down = solution(3) * raster_time;
49+ grad_amp = solution(4);
50+
51+ % Generate final time vector and amplitudes
52+ if flat_time > 0
53+ times = cumsum([0, time_ramp_up, flat_time, time_ramp_down]);
54+ amplitudes = [grad_start, grad_amp, grad_amp, grad_end];
55+ else
56+ times = cumsum([0, time_ramp_up, time_ramp_down]);
57+ amplitudes = [grad_start, grad_amp, grad_end];
58+ end
59+
60+ grad=mr.makeExtendedTrapezoid(channel,'system',sys,'times',times, 'amplitudes', amplitudes);
61+ if abs(grad.area - area) >= 1e-3
62+ error('Could not find a solution for area=%.6f.', area);
63+ end
64+end
65+
66+
67+function time = to_raster(time_val, raster_time)
68+ time = ceil(time_val / raster_time) * raster_time;
69+end
70+
71+function t = calc_ramp_time(g1, g2, max_slew, raster_time)
72+ t = to_raster(abs(g1 - g2) / max_slew, raster_time);
73+end
74+
75+function sol = binary_search(fun, low, high)
76+ while low < high - 1
77+ mid = floor((low + high) / 2);
78+ if ~isempty(fun(mid))
79+ high = mid;
80+ else
81+ low = mid;
82+ end
83+ end
84+ sol = fun(high);
85+end
86+
87+
88+function sol = find_solution(duration, area, grad_start, grad_end, max_slew, max_grad, raster_time)
89+ sign_area = sign(area);
90+ if sign_area == 0
91+ % zero-area request (e.g. linking two trajectory segments without
92+ % changing the k-space position): sign(0) would zero out the search
93+ % direction and produce Inf ranges in the flat=0 estimates below, so
94+ % search on the side opposite to the edge gradients instead
95+ sign_area = -sign(grad_start + grad_end);
96+ if sign_area == 0
97+ sign_area = 1;
98+ end
99+ end
100+ grad_amp = sign_area * max_grad;
101+
102+ % Convert to raster steps
103+ ru_min = abs(grad_amp - grad_start) / max_slew / raster_time;
104+ rd_min = abs(grad_amp - grad_end) / max_slew / raster_time;
105+ flat_time = max(duration - ru_min - rd_min, 0);
106+
107+ % Check if feasible
108+ approx_area = ru_min * (grad_amp + grad_start) + ...
109+ rd_min * (grad_amp + grad_end) + ...
110+ 2 * flat_time * grad_amp;
111+
112+ if abs(2 * area / raster_time) > abs(approx_area)
113+ sol = [];
114+ return;
115+ end
116+
117+ % Easy early solution: max_grad
118+ ru = (duration * max_slew * raster_time + sign_area * (grad_end - grad_start)) / (2 * max_slew * raster_time);
119+ if sign_area * grad_start + ru * max_slew * raster_time > max_grad + 1e-5
120+ ru_steps = round(abs(grad_start - sign_area * max_grad) / max_slew / raster_time);
121+ rd_steps = round(abs(grad_end - sign_area * max_grad) / max_slew / raster_time);
122+ flat_steps = duration - ru_steps - rd_steps;
123+ if flat_steps > 0
124+ grad_amp = -(ru_steps * raster_time * grad_start + ...
125+ rd_steps * raster_time * grad_end - 2 * area) / ...
126+ ((ru_steps + 2 * flat_steps + rd_steps) * raster_time);
127+ amps = [grad_start, grad_amp, grad_amp, grad_end];
128+ t = cumsum([0, ru_steps, flat_steps, rd_steps]) * raster_time;
129+ slew = diff(amps) ./ diff(t);
130+ if max(abs(slew)) < max_slew + 1e-5 && max(abs(amps)) < max_grad
131+ sol = [ru_steps, flat_steps, rd_steps, grad_amp];
132+ return;
133+ end
134+ end
135+ end
136+
137+ % Conservative downscaling
138+ while abs(2 * area / raster_time) < abs(approx_area)
139+ grad_amp = grad_amp / 2;
140+ if abs(grad_amp) < abs(max_grad) / 10
141+ ru_min = 0; rd_min = 0;
142+ flat_time = max(duration - ru_min - rd_min, 0);
143+ break;
144+ end
145+ ru_min = abs(grad_amp - grad_start) / max_slew / raster_time;
146+ rd_min = abs(grad_amp - grad_end) / max_slew / raster_time;
147+ flat_time = max(duration - ru_min - rd_min, 0);
148+ approx_area = ru_min * (grad_amp + grad_start) + ...
149+ rd_min * (grad_amp + grad_end) + ...
150+ 2 * flat_time * grad_amp;
151+ end
152+
153+ % Convert to integer steps
154+ ru_min = floor(ru_min);
155+ rd_min = floor(rd_min);
156+ ru_limit = ceil(abs(sign_area * max_grad - grad_start) / max_slew / raster_time) + 1;
157+ rd_limit = ceil(abs(sign_area * max_grad - grad_end) / max_slew / raster_time) + 1;
158+
159+ % All combinations
160+ [RU, RD] = meshgrid(ru_min:ru_limit, rd_min:rd_limit);
161+ RU = RU(:);
162+ RD = RD(:);
163+ valid_mask = RD < (duration - RU);
164+ RU = RU(valid_mask);
165+ RD = RD(valid_mask);
166+
167+ % Flat = 0 case
168+ num = (2 * area - duration * (grad_end + grad_start) * raster_time);
169+ denom_min = (grad_start - grad_end + sign_area * duration * max_slew * raster_time);
170+ denom_max = (-grad_start + grad_end + sign_area * duration * max_slew * raster_time);
171+ ru_flat0_min = round(num / denom_min / raster_time);
172+ ru_flat0_max = duration - round(num / denom_max / raster_time);
173+
174+ RU = [RU; (ru_flat0_min:ru_flat0_max)'];
175+ RD = [RD; (duration - (ru_flat0_min:ru_flat0_max))'];
176+
177+ % Filter invalid ones
178+ flat = duration - RU - RD;
179+ valid = flat >= 0 & RU > 0 & RD > 0;
180+ RU = RU(valid); RD = RD(valid); flat = flat(valid);
181+
182+ % Calculate amp
183+ grad_amp = -(RU * raster_time * grad_start + RD * raster_time * grad_end - 2 * area) ./ ...
184+ ((RU + 2 * flat + RD) * raster_time);
185+
186+ % Slew
187+ slew1 = abs(grad_start - grad_amp) ./ (RU * raster_time);
188+ slew2 = abs(grad_end - grad_amp) ./ (RD * raster_time);
189+
190+ % Valid gradient/slew combinations
191+ valid = abs(grad_amp) <= max_grad + 1e-5 & slew1 <= max_slew + 1e-5 & slew2 <= max_slew + 1e-5;
192+ if ~any(valid)
193+ sol = [];
194+ return;
195+ end
196+
197+ % Pick lowest slew
198+ ind = find(valid);
199+ [~, min_idx] = min(slew1(ind) + slew2(ind));
200+ best = ind(min_idx);
201+
202+ sol = [RU(best), flat(best), RD(best), grad_amp(best)];
203+end
src/engine/pulseq/+mr/makeGaussPulse.madded+149−0View file
@@ -0,0 +1,149 @@
1+function [rf, gz, gzr, delay] = makeGaussPulse(flip,varargin)
2+%makeGaussPulse Create a [optionally slice selective] Gauss pulse.
3+% rf=makeGaussPulse(flip, 'Duration', dur) Create Gauss pulse
4+% with given flip angle (rad) and duration (s).
5+%
6+% rf=makeGaussPulse(..., 'freqOffset', f,'phaseOffset',p)
7+% Create Gauss pulse with frequency offset (Hz) and phase offset (rad).
8+%
9+% rf=makeGaussPulse(..., 'freqPPM',-3.3)
10+% Create Gaussian RF pulse with frequency offset specified in PPM (e.g.
11+% actual frequency offset proportional to the true Larmor frequency), in
12+% this example -3.3 ppm as often used for fat saturation; can be combined
13+% with the 'freqOffset' specified in Hz.
14+%
15+% [rf, gz]=makeGaussPulse(...,'SliceThickness',st) Return the
16+% slice select gradient corresponding to given slice thickness (m).
17+%
18+% [rf, gz]=makeGaussPulse(flip,lims,...) Create slice selection gradient
19+% with the specificed gradient limits (e.g. amplitude, slew).
20+%
21+% [rf, gz, gzr]=makeGaussPulse(flip,lims,...) Create slice selection and
22+% slice refocusing gradients with the specificed gradient limits
23+% (e.g. amplitude, slew) and taking into account 'centerpos' parameter
24+%
25+% See also Sequence.addBlock
26+
27+validPulseUses = mr.getSupportedRfUse();
28+
29+persistent parser
30+if isempty(parser)
31+ parser = mr.aux.InputParserCompat;
32+ parser.FunctionName = 'makeGaussPulse';
33+
34+ % RF params
35+ addRequired(parser, 'flipAngle', @isnumeric);
36+ addOptional(parser, 'system', [], @isstruct);
37+ addParamValue(parser, 'duration', 0, @isnumeric);
38+ addParamValue(parser, 'freqOffset', 0, @isnumeric);
39+ addParamValue(parser, 'phaseOffset', 0, @isnumeric);
40+ addParamValue(parser, 'freqPPM', 0, @isnumeric);
41+ addParamValue(parser, 'phasePPM', 0, @isnumeric);
42+ addParamValue(parser, 'timeBwProduct', 3, @isnumeric);
43+ addParamValue(parser, 'bandwidth', 0, @isnumeric);
44+ addParamValue(parser, 'apodization', 0, @isnumeric);
45+ addParamValue(parser, 'centerpos', 0.5, @isnumeric);
46+ % Slice params
47+ addParamValue(parser, 'maxGrad', 0, @isnumeric);
48+ addParamValue(parser, 'maxSlew', 0, @isnumeric);
49+ addParamValue(parser, 'sliceThickness', 0, @isnumeric);
50+ addParamValue(parser, 'delay', 0, @isnumeric);
51+ addParamValue(parser, 'dwell', 0, @isnumeric); % dummy default value
52+ % whether it is a refocusing pulse (for k-space calculation)
53+ addParamValue(parser, 'use', 'u', @(x) any(validatestring(x,validPulseUses)));
54+end
55+parse(parser, flip, varargin{:});
56+opt = parser.Results;
57+
58+if isempty(opt.system)
59+ system=mr.opts();
60+else
61+ system=opt.system;
62+end
63+
64+if opt.dwell==0
65+ opt.dwell=system.rfRasterTime;
66+end
67+
68+if opt.bandwidth == 0
69+ BW = opt.timeBwProduct/opt.duration;
70+else
71+ BW = opt.bandwidth;
72+end
73+alpha = opt.apodization;
74+N = round(opt.duration/opt.dwell);
75+t = ((1:N)-0.5)*opt.dwell;
76+tt = t - opt.duration*opt.centerpos;
77+window = (1.0-alpha+alpha*cos(2*pi*tt/opt.duration));
78+signal = window.*gauss(BW*tt);
79+flip = sum(signal)*opt.dwell*2*pi;
80+signal = signal*opt.flipAngle/flip;
81+
82+rf.type = 'rf';
83+rf.signal = signal;
84+rf.t = t;
85+rf.shape_dur=N*opt.dwell;
86+rf.freqOffset = opt.freqOffset;
87+rf.phaseOffset = opt.phaseOffset;
88+rf.freqPPM = opt.freqPPM;
89+rf.phasePPM = opt.phasePPM;
90+rf.deadTime = system.rfDeadTime;
91+rf.ringdownTime = system.rfRingdownTime;
92+rf.delay = opt.delay;
93+if ~isempty(opt.use)
94+ rf.use=opt.use;
95+end
96+if rf.deadTime > rf.delay
97+ rf.delay = rf.deadTime;
98+end
99+rf.center=opt.duration*opt.centerpos;
100+
101+if nargout > 1
102+ assert(opt.sliceThickness > 0,'SliceThickness must be provided');
103+ if opt.maxGrad > 0
104+ system.maxGrad = opt.maxGrad;
105+ end
106+ if opt.maxSlew > 0
107+ system.maxSlew = opt.maxSlew;
108+ end
109+
110+ amplitude = BW/opt.sliceThickness;
111+ area = amplitude*opt.duration;
112+ gz = mr.makeTrapezoid('z', system, 'flatTime', opt.duration, ...
113+ 'flatArea', area);
114+ gzr= mr.makeTrapezoid('z', system, 'Area', -area*(1-opt.centerpos)-0.5*(gz.area-area));
115+ if rf.delay > gz.riseTime
116+ gz.delay = ceil((rf.delay - gz.riseTime)/system.gradRasterTime)*system.gradRasterTime; % round-up to gradient raster
117+ end
118+ if rf.delay < (gz.riseTime+gz.delay)
119+ rf.delay = gz.riseTime+gz.delay; % these are on the grad raster already which is coarser
120+ end
121+end
122+
123+% v1.4 finally eliminates RF zerofilling
124+% if rf.ringdownTime > 0
125+% tFill = (1:round(rf.ringdownTime/1e-6))*1e-6; % Round to microsecond
126+% rf.t = [rf.t rf.t(end)+tFill];
127+% rf.signal = [rf.signal, zeros(size(tFill))];
128+% end
129+if nargout > 3
130+ delay=mr.makeDelay(mr.calcDuration(rf)); % calcDuration already includes the ringdown time
131+end
132+
133+% RF amplitude check
134+rf_amplitude=max(abs(rf.signal));
135+if rf_amplitude>system.maxB1
136+ warning('WARNING: system maximum RF amplitude exceeded (%.01f%%)', rf_amplitude/system.maxB1*100);
137+end
138+
139+function y = gauss(x)
140+ % gauss Calculate the Gaussian function:
141+ % gauss(x) = exp(-pi*x^2)
142+ %
143+ % This is a useful helper function for those without the signal
144+ % processing toolbox
145+
146+ y = exp(-pi*x.^2);
147+end
148+
149+end
src/engine/pulseq/+mr/makeHexagonGradientArea.madded+314−0View file
@@ -0,0 +1,314 @@
1+function [grad, times, amplitudes] = makeHexagonGradientArea(channel, grad_start, grad_end, area, sys)
2+% Make the shortest possible hexagonal gradien (creating an extended
3+% trapezoid object) for a given area and edge values. In contrast to
4+% mr.makeExtendedTrapezoidArea(), this function creates a generic
5+% polynomial gradient object without a plato between the vortex2 and
6+% vortex3. We expect this function to find better solutions by relaxing the
7+% constraint of two vertices having the same amplitude, however, as seen in
8+% testCase_12, this objective cannot be achieved in all cases by the current
9+% implementation.
10+% Parameters and methodology are explained in mr.makeExtendedTrapezoidArea().
11+% This function was implemented by Mehmet Emin Öztürk during his visit to
12+% Freiburg with some input from Maxim Zaitsev.
13+
14+ if nargin < 5 || isempty(sys)
15+ sys = default_opts(); % Define your default system
16+ end
17+
18+ % validate inputs the search below cannot handle (runaway loops or obscure downstream errors)
19+ if abs(grad_start) > sys.maxGrad
20+ error('grad_start amplitude violation (%.0f%%)', abs(grad_start)/sys.maxGrad*100);
21+ end
22+ if abs(grad_end) > sys.maxGrad
23+ error('grad_end amplitude violation (%.0f%%)', abs(grad_end)/sys.maxGrad*100);
24+ end
25+
26+ max_slew = sys.maxSlew * 0.99;
27+ max_grad = sys.maxGrad * 0.99;
28+ raster_time = sys.gradRasterTime;
29+
30+ min_duration = max(round(calc_ramp_time(grad_end, grad_start, max_slew, raster_time) / raster_time), 2);
31+
32+ % Estimate upper bound duration
33+ max_duration = max( ...
34+ [round(calc_ramp_time(0, grad_start, max_slew, raster_time) / raster_time), ...
35+ round(calc_ramp_time(0, grad_end, max_slew, raster_time) / raster_time), ...
36+ min_duration]);
37+
38+ % Try to find a solution linearly
39+ times = [];
40+ amplitudes = [];
41+ for duration = min_duration:max_duration
42+ [times, amplitudes] = find_solution(duration, area, grad_start, grad_end, max_slew, max_grad, raster_time);
43+ if ~isempty(times)
44+ break;
45+ end
46+ end
47+
48+ % Binary search if linear search failed
49+ if isempty(times)
50+ duration = max_duration;
51+ while isempty(times)
52+ duration = duration * 2;
53+ [times, ~] = find_solution(duration, area, grad_start, grad_end, max_slew, max_grad, raster_time);
54+ end
55+
56+ [times, amplitudes] = binary_search(@(d) find_solution(d, area, grad_start, grad_end, max_slew, max_grad, raster_time), ...
57+ floor(duration/2), duration);
58+ end
59+ % drop zero-length segments (their end points always carry equal
60+ % amplitudes, otherwise the slew-rate check would have rejected them)
61+ keep = [true, diff(times) > 0];
62+ times = times(keep);
63+ amplitudes = amplitudes(keep);
64+ grad=mr.makeExtendedTrapezoid(channel,'system',sys,'times',times, 'amplitudes', amplitudes);
65+ if abs(grad.area - area) >= 1e-3
66+ error('Could not find a solution for area=%.6f.', area);
67+ end
68+end
69+
70+
71+function time = to_raster(time_val, raster_time)
72+ time = ceil(time_val / raster_time) * raster_time;
73+end
74+
75+function t = calc_ramp_time(g1, g2, max_slew, raster_time)
76+ t = to_raster(abs(g1 - g2) / max_slew, raster_time);
77+end
78+
79+function [times, amplitudes] = binary_search(fun, low, high)
80+ while low < high - 1
81+ mid = floor((low + high) / 2);
82+ if ~isempty(fun(mid))
83+ high = mid;
84+ else
85+ low = mid;
86+ end
87+ end
88+ [times, amplitudes] = fun(high);
89+end
90+
91+
92+function [times, amplitudes] = find_solution(duration, area, grad_start, grad_end, max_slew, max_grad, raster_time)
93+% Find extended trapezoid gradient waveform for given duration
94+
95+ sign_area = sign(area);
96+ if sign_area == 0
97+ % zero-area request (e.g. linking two trajectory segments without
98+ % changing the k-space position): sign(0) would zero out the search
99+ % direction and send the loops below into runaway iterations, so
100+ % search on the side opposite to the edge gradients instead
101+ sign_area = -sign(grad_start + grad_end);
102+ if sign_area == 0
103+ sign_area = 1;
104+ end
105+ end
106+ grad_amp = sign_area * max_grad;
107+ ramp_up_times = [];
108+ ramp_down_times = [];
109+
110+ % Early estimation
111+ ru_min = abs(grad_amp - grad_start) / max_slew / raster_time;
112+ rd_min = abs(grad_amp - grad_end) / max_slew / raster_time;
113+ flat_time = max(duration - ru_min - rd_min, 0);
114+
115+ area_check = ru_min * (grad_amp + grad_start) + rd_min * (grad_amp + grad_end) + 2 * flat_time * grad_amp;
116+
117+ if abs(2 * area / raster_time) > abs(area_check)
118+ times = []; amplitudes = [];
119+ return;
120+ end
121+
122+ % Fast-case: max_grad ramp with valid flat
123+ ru = (duration * max_slew * raster_time + sign_area * (grad_end - grad_start)) / (2 * max_slew * raster_time);
124+ if sign_area * grad_start + ru * max_slew * raster_time > max_grad + 1e-5
125+ ru_steps = round(abs(grad_start - sign_area * max_grad) / max_slew / raster_time);
126+ rd_steps = round(abs(grad_end - sign_area * max_grad) / max_slew / raster_time);
127+ flat_steps = duration - ru_steps - rd_steps;
128+ if flat_steps > 0
129+ grad_amp = -(ru_steps * raster_time * grad_start + rd_steps * raster_time * grad_end - 2 * area) / ...
130+ ((ru_steps + 2 * flat_steps + rd_steps) * raster_time);
131+ amps = [grad_start, grad_amp, grad_amp, grad_end];
132+ t = cumsum([0, ru_steps, flat_steps, rd_steps]) * raster_time;
133+ slew = diff(amps) ./ diff(t);
134+ if max(abs(slew)) < max_slew + 1e-5 && max(abs(amps)) < max_grad
135+ times = t;
136+ amplitudes = amps;
137+ return;
138+ end
139+ end
140+ end
141+
142+ % Gradually reduce grad_amp if area too large
143+ while abs(2 * area / raster_time) < abs(area_check)
144+ grad_amp = grad_amp / 2;
145+ if abs(grad_amp) < abs(max_grad) / 10
146+ ru_min = 0; rd_min = 0;
147+ flat_time = max(duration - ru_min - rd_min, 0);
148+ break;
149+ end
150+ ru_min = abs(grad_amp - grad_start) / max_slew / raster_time;
151+ rd_min = abs(grad_amp - grad_end) / max_slew / raster_time;
152+ flat_time = max(duration - ru_min - rd_min, 0);
153+ area_check = ru_min * (grad_amp + grad_start) + rd_min * (grad_amp + grad_end) + 2 * flat_time * grad_amp;
154+ end
155+
156+ % Discrete timing limits
157+ ru_min = floor(ru_min);
158+ rd_min = floor(rd_min);
159+ ru_limit = ceil(abs(sign_area * max_grad - grad_start) / max_slew / raster_time);
160+ rd_limit = ceil(abs(sign_area * max_grad - grad_end) / max_slew / raster_time);
161+
162+ flat_time = duration - min(rd_min, rd_limit) - min(ru_min, ru_limit);
163+ flat_time_min = duration - rd_limit - ru_limit;
164+
165+ min_dif_area = area;
166+ i = -1;
167+
168+ while flat_time > max(flat_time_min, -1)
169+ i = i + 1;
170+ ru_max = ru_min + i;
171+ rd_max = rd_min + i;
172+ flat_time = duration - min(rd_min + i, rd_limit) - min(ru_min + i, ru_limit);
173+
174+ if flat_time <= 0
175+ % Fallback: flat_time = 0 or 1
176+ ru_0min = (2 * area - duration * (grad_end + grad_start) * raster_time) / ...
177+ (grad_start - grad_end + sign_area * duration * max_slew * raster_time) / raster_time;
178+ ru_0max = duration - (2 * area - duration * (grad_end + grad_start) * raster_time) / ...
179+ (-grad_start + grad_end + sign_area * duration * max_slew * raster_time) / raster_time;
180+
181+ ru_0min = floor(ru_0min); ru_0max = ceil(ru_0max);
182+ for ru_try = ru_0min:ru_0max
183+ ramp_up_times = [ramp_up_times, ru_try, ru_try];
184+ ramp_down_times = [ramp_down_times, duration - ru_try - 1, duration - ru_try];
185+ end
186+ break;
187+ end
188+
189+ % Gradients at corners
190+ grad_p0 = sign_area * max_slew * ru_max * raster_time + grad_start;
191+ grad_p1 = sign_area * max_slew * rd_max * raster_time + grad_end;
192+
193+ if abs(grad_p0) >= max_grad
194+ limit_option1 = abs(sign_area * max_slew * (ru_limit - 1) * raster_time + grad_start);
195+ limit_option2 = abs(((ru_limit + flat_time) * sign_area * max_grad + grad_start - grad_p1) / (ru_limit + flat_time));
196+ if limit_option1 > limit_option2
197+ grad_p0 = sign_area * max_slew * (ru_limit - 1) * raster_time + grad_start;
198+ ru_max = floor(abs(sign_area * max_grad - grad_start) / max_slew / raster_time);
199+ ru_limit = ru_max;
200+ else
201+ grad_p0 = sign_area * max_grad;
202+ ru_max = ceil(abs(sign_area * max_grad - grad_start) / max_slew / raster_time);
203+ ru_limit = ru_max;
204+ end
205+ flat_time = duration - rd_max - ru_max;
206+ end
207+
208+ if abs(grad_p1) >= max_grad
209+ limit_option1 = abs(sign_area * max_slew * (rd_limit - 1) * raster_time + grad_end);
210+ limit_option2 = abs(((rd_limit + flat_time) * sign_area * max_grad + grad_end - grad_p0) / (rd_limit + flat_time));
211+ if limit_option1 > limit_option2
212+ grad_p1 = sign_area * max_slew * (rd_limit - 1) * raster_time + grad_end;
213+ rd_max = floor(abs(sign_area * max_grad - grad_end) / max_slew / raster_time);
214+ rd_limit = rd_max;
215+ else
216+ grad_p1 = sign_area * max_grad;
217+ rd_max = ceil(abs(sign_area * max_grad - grad_end) / max_slew / raster_time);
218+ rd_limit = rd_max;
219+ end
220+ flat_time = duration - rd_max - ru_max;
221+ end
222+
223+ % Slope too high from p0 to p1
224+ if abs(grad_p0 - grad_p1) / (flat_time * raster_time) > max_slew
225+ if abs(grad_p0) < abs(grad_p1)
226+ grad_p1 = grad_p0 + flat_time * raster_time * max_slew * sign_area;
227+ rd_max = ceil(abs(grad_end - grad_p1) / max_slew / raster_time);
228+ else
229+ grad_p0 = grad_p1 + flat_time * raster_time * max_slew * sign_area;
230+ ru_max = ceil(abs(grad_start - grad_p0) / max_slew / raster_time);
231+ end
232+ flat_time = duration - rd_max - ru_max;
233+ end
234+
235+ % Compute area
236+ area_current = raster_time/2 * ...
237+ (ru_max * (grad_p0 + grad_start) + ...
238+ flat_time * (grad_p0 + grad_p1) + ...
239+ rd_max * (grad_p1 + grad_end));
240+
241+ if sign(min_dif_area) ~= sign(area - area_current)
242+ t = round(cumsum([0, ru_max, flat_time, rd_max]) * raster_time, 5);
243+ if abs(grad_p0) < abs(grad_p1)
244+ Gtest = grad_p1;
245+ corner_grad = -(grad_start * ru_max + grad_end * rd_max + Gtest * (flat_time + rd_max) - 2 * area / raster_time) / (flat_time + ru_max);
246+ amps = [grad_start, corner_grad, Gtest, grad_end];
247+ else
248+ Gtest = grad_p0;
249+ corner_grad = -(grad_start * ru_max + grad_end * rd_max + Gtest * (flat_time + ru_max) - 2 * area / raster_time) / (flat_time + rd_max);
250+ amps = [grad_start, Gtest, corner_grad, grad_end];
251+ end
252+
253+ if any(round(diff(t)/raster_time) < 1)
254+ continue;
255+ end
256+
257+ slew = diff(amps) ./ diff(t);
258+ if max(abs(slew)) <= max_slew + 1e-5
259+ times = t;
260+ amplitudes = amps;
261+ return;
262+ end
263+
264+ % fallback search if slope still too large
265+ for ru_try = ru_max-1 : duration - rd_max
266+ for rd_try = rd_max-1 : duration - ru_try
267+ flat = duration - ru_try - rd_try;
268+ grad_amp = -(ru_try * raster_time * grad_start + rd_try * raster_time * grad_end - 2 * area) / ...
269+ ((ru_try + 2 * flat + rd_try) * raster_time);
270+ amps = [grad_start, grad_amp, grad_amp, grad_end];
271+ t = cumsum([0, ru_try, flat, rd_try]) * raster_time;
272+ slew = diff(amps) ./ diff(t);
273+ if max(abs(slew)) < max_slew + 1e-5 && max(abs(amps)) < max_grad
274+ times = t;
275+ amplitudes = amps;
276+ return;
277+ end
278+ end
279+ end
280+ end
281+
282+ if abs(area_current - area) < abs(min_dif_area)
283+ min_dif_area = area - area_current;
284+ end
285+ end
286+
287+ % Fallback to triangle search
288+ ru_vec = ramp_up_times(:);
289+ rd_vec = ramp_down_times(:);
290+ valid = ru_vec .* rd_vec > 0;
291+ ru_vec = ru_vec(valid); rd_vec = rd_vec(valid);
292+ flat = duration - ru_vec - rd_vec;
293+ valid = flat >= 0;
294+ ru_vec = ru_vec(valid); rd_vec = rd_vec(valid); flat = flat(valid);
295+
296+ grad_amp = -(ru_vec * raster_time * grad_start + rd_vec * raster_time * grad_end - 2 * area) ./ ...
297+ ((ru_vec + 2 * flat + rd_vec) * raster_time);
298+
299+ slew1 = abs(grad_start - grad_amp) ./ (ru_vec * raster_time);
300+ slew2 = abs(grad_end - grad_amp) ./ (rd_vec * raster_time);
301+
302+ valid = abs(grad_amp) <= max_grad + 1e-5 & slew1 <= max_slew + 1e-5 & slew2 <= max_slew + 1e-5;
303+ idx = find(valid, 1);
304+ if isempty(idx)
305+ times = []; amplitudes = [];
306+ return;
307+ end
308+
309+ t = cumsum([0, ru_vec(idx), flat(idx), rd_vec(idx)]) * raster_time;
310+ amps = [grad_start, grad_amp(idx), grad_amp(idx), grad_end];
311+
312+ times = t;
313+ amplitudes = amps;
314+end
src/engine/pulseq/+mr/makeLabel.madded+39−0View file
@@ -0,0 +1,39 @@
1+function out = makeLabel(type, label, value)
2+%makeLabel Create a ADC Label.
3+% label=makeLabel() Create a Label event for ADC line
4+% Possible values for type are: 'SET','INC'.
5+% Label may be a counter of a flag and should be one of
6+% (counters) 'SLC','SEG','REP','AVG','SET','ECO','PHS','LIN','PAR','ACQ'
7+% (flags) 'NAV','REV','SMS','REF','IMA','NOISE'
8+% (control) 'PMC','NOROT','NOPOS','NOSCL','ONCE','TRID'
9+% Value: numeric value of the parameter (increment may be negative)
10+% or true/false for a flag
11+%
12+% See also Sequence.addBlock, mr.getSupportedLabels
13+
14+
15+supported_labels=mr.getSupportedLabels();
16+if nargin~=3
17+ error('makeLabel:invalidArguments','Must supply exactly 3 parameters');
18+end
19+if ~any(ismember(supported_labels,label))
20+ error('makeLabel:invalidArguments','Must supply a valid label');
21+end
22+if ~any(ismember({'SET','INC'},type))
23+ error('makeLabel:invalidArguments','Must supply a valid type');
24+end
25+if ~isnumeric(value) && ~islogical(value)
26+ error('makeLabel:invalidArguments','Must supply a valid numeric or logical value');
27+end
28+
29+switch(type)
30+ case 'SET'
31+ out.type = 'labelset';
32+ case 'INC'
33+ out.type = 'labelinc';
34+ otherwise
35+ error('unknown label type');
36+end
37+out.label=label;
38+out.value=value;
39+end
src/engine/pulseq/+mr/makeRfShim.madded+11−0View file
@@ -0,0 +1,11 @@
1+function rfShim = makeRfShim(shimVec)
2+%makeRfShim Create a RF shimming event.
3+% rfShim=makeRfShim(shimVec) Create an event describing RF shimmming in
4+% the current block. It is only valid if the block
5+% contains an RF pulse.
6+%
7+% See also Sequence.addBlock
8+
9+rfShim=struct('type','rfShim','shimVector',shimVec(:));
10+
11+end
src/engine/pulseq/+mr/makeRotation.madded+46−0View file
@@ -0,0 +1,46 @@
1+function rot = makeRotation( varargin )
2+% makeRotation Create a rotation extension object
3+% makeRotation( phi ) - rotation about Z (in plane), angle in rad
4+% makeRotation( phi, theta ) - rotation on a sphere, angles in rad
5+% makeRotation( axis, angle ) - rotation about a given 3D vector by a given angle in rad
6+% makeRotation( quaternion ) - rotation defined by a unit quaternion
7+% makeRotation( rot_mat ) - rotation defined by a 3x3 rotation matrix
8+if nargin<1
9+ error('makeRotation:invalidArguments','makeRotation - invalid arguments: must supply rotation parameter(s)');
10+end
11+switch numel(varargin{1})
12+ case 1
13+ phi = varargin{1};
14+ if nargin<2
15+ theta = 0.0;
16+ else
17+ theta = varargin{2};
18+ end
19+ assert( ( phi >= -pi ) && ( phi < 2*pi) , 'makeRotation:invalidTheta',...
20+ 'rotation angle phi (%.2f) is invalid. should be within [-pi,2*pi] radians',phi);
21+ assert( ( theta >= -pi ) && ( theta <= pi) , 'makeRotation:invalidPhi',...
22+ 'rotation angle theta (%.2f) is invalid. should be within [-pi,pi] radians',theta);
23+ q1=[cos(theta/2) 0 sin(theta/2) 0]; % y axis
24+ q2=[cos(phi/2) 0 0 sin(phi/2)]; % z axis
25+ rot.rotQuaternion=mr.aux.quat.multiply(q2,q1); % ok, looks like the order is right
26+ case 3
27+ v=varargin{1};
28+ v=v./sqrt(sum(v.^2));
29+ assert(nargin>1);
30+ phi = varargin{2};
31+ assert( ( abs(phi) >= 0 ) & ( abs(phi) <= pi) , 'makeRotation:invalidPhi',...
32+ 'rotation angle phi (%.2f) is invalid. should be within [0,pi] radians',phi);
33+ rot.rotQuaternion=[cos(phi/2) sin(phi/2)*v];
34+ case 4
35+ rot.rotQuaternion=mr.aux.quat.normalize(varargin{1});
36+ case 9
37+ assert(all(size(varargin{1})==[3 3]));
38+ rot.rotQuaternion=mr.aux.quat.fromRotMat(varargin{1});
39+ otherwise
40+ error('unexpected input to makeRotation');
41+end
42+
43+rot.type = 'rot3D';
44+
45+end
46+
src/engine/pulseq/+mr/makeSLRpulse.madded+225−0View file
@@ -0,0 +1,225 @@
1+function [rf, gz, gzr, delay] = makeSLRpulse(flip,varargin)
2+%makeSLRpulse make an SLR pulse
3+% a wrapper to a python function(see below). See supported params below
4+% in the 'parser' section. Currently it will probably only work on
5+% Linux. On my system I could install the required Python library by
6+% executing "pip3 install sigpy"
7+%
8+% sigpy.mri.rf.dzrf = dzrf(n=64, tb=4, ptype='st', ftype='ls', d1=0.01, d2=0.01, cancel_alpha_phs=False)
9+% Primary function for design of pulses using the SLR algorithm.
10+%
11+% Args:
12+% n (int): number of time points.
13+% tb (int): pulse time bandwidth product.
14+% ptype (string): pulse type, 'st' (small-tip excitation), 'ex' (pi/2
15+% excitation pulse), 'se' (spin-echo pulse), 'inv' (inversion), or
16+% 'sat' (pi/2 saturation pulse).
17+% ftype (string): type of filter to use: 'ms' (sinc), 'pm'
18+% (Parks-McClellan equal-ripple), 'min' (minphase using factored pm),
19+% 'max' (maxphase using factored pm), 'ls' (least squares).
20+% d1 (float): passband ripple level in :math:'M_0^{-1}'.
21+% d2 (float): stopband ripple level in :math:'M_0^{-1}'.
22+% filterType (str): filter type to use, e.g. sinc (ms),
23+% least-squares (ls), etc. Refer to sigpy.rf documentation.
24+%
25+% Returns:
26+% rf (array): designed RF pulse.
27+%
28+% References:
29+% Pauly, J., Le Roux, Patrick., Nishimura, D., and Macovski, A.(1991).
30+% Parameter Relations for the Shinnar-LeRoux Selective Excitation
31+% Pulse Design Algorithm.
32+% IEEE Transactions on Medical Imaging, Vol 10, No 1, 53-65.
33+%
34+
35+validPulseUses = mr.getSupportedRfUse();
36+
37+persistent parser
38+if isempty(parser)
39+ parser = mr.aux.InputParserCompat;
40+ parser.FunctionName = 'makeSLRpulse';
41+
42+ % RF params
43+ addRequired(parser, 'flipAngle', @isnumeric);
44+ addOptional(parser, 'system', [], @isstruct);
45+ addParamValue(parser, 'duration', 1e-3, @isnumeric);
46+ addParamValue(parser, 'freqOffset', 0, @isnumeric);
47+ addParamValue(parser, 'phaseOffset', 0, @isnumeric);
48+ addParamValue(parser, 'freqPPM', 0, @isnumeric);
49+ addParamValue(parser, 'phasePPM', 0, @isnumeric);
50+ addParamValue(parser, 'timeBwProduct', 4, @isnumeric);
51+ addParamValue(parser, 'passbandRipple', 0.01, @isnumeric);
52+ addParamValue(parser, 'stopbandRipple', 0.01, @isnumeric);
53+ addParamValue(parser, 'filterType', 'ms', @isstr);
54+ %addParamValue(parser, 'apodization', 0, @isnumeric);
55+ %addParamValue(parser, 'centerpos', 0.5, @isnumeric);
56+ % Slice params
57+ addParamValue(parser, 'maxGrad', 0, @isnumeric);
58+ addParamValue(parser, 'maxSlew', 0, @isnumeric);
59+ addParamValue(parser, 'sliceThickness', 0, @isnumeric);
60+ addParamValue(parser, 'delay', 0, @isnumeric);
61+ addParamValue(parser, 'dwell', 0, @isnumeric); % dummy default value
62+ % whether it is a refocusing pulse (for k-space calculation)
63+ addParamValue(parser, 'use', 'excitation', @(x) any(validatestring(x,validPulseUses)));
64+ addParamValue(parser, 'recenterOnSample', false, @islogical);
65+ % optional Python command
66+ addParamValue(parser, 'pythonCmd', '', @(x)isstring(x)||ischar(x));
67+end
68+parse(parser, flip, varargin{:});
69+opt = parser.Results;
70+
71+if isempty(opt.system)
72+ sys=mr.opts();
73+else
74+ sys=opt.system;
75+end
76+
77+if opt.dwell==0
78+ opt.dwell=sys.rfRasterTime;
79+end
80+
81+% find/check python
82+if ~isempty(opt.pythonCmd)
83+ [status, result]=system([opt.pythonCmd ' --version']);
84+ if status~=0
85+ error(['provided python executable ''' opt.pythonCmd ''' returns an error on the version check']);
86+ end
87+ if ispc
88+ [status, result] = system(sprintf('%s -c "import sigpy" 2>nul',opt.pythonCmd));
89+ else
90+ [status, result] = system(sprintf('%s -c "import sigpy" 2>/dev/null',opt.pythonCmd));
91+ end
92+ if status~=0
93+ error(['provided python executable ''' opt.pythonCmd ''' returns an error on the sigPy check']);
94+ end
95+ python=opt.pythonCmd;
96+else
97+ [avail, python]=mr.aux.isSigPyAvailable();
98+ if ~avail
99+ error('python executable with installed sigPy not found, please check your system PATH settings and Python installation');
100+ end
101+end
102+% add quotes in case Python install path contains spaces or alike characters
103+if python(1)~='"'
104+ python=['"' python '"'];
105+end
106+
107+add_opt='';
108+
109+switch opt.use
110+ case 'excitation'
111+ if opt.flipAngle <= pi/6
112+ ptype='st';
113+ else
114+ ptype='ex';
115+ %add_opt=',cancel_alpha_phs=True';
116+ end
117+ case 'refocusing'
118+ ptype='se';
119+ case 'inversion'
120+ ptype='inv';
121+ case 'saturation'
122+ ptype='sat';
123+ otherwise
124+ ptype='st';
125+end
126+
127+N = round(opt.duration/opt.dwell);
128+% on Windows it looks like the $ and '' are not needed and ; can be used in place of \n
129+if ispc()
130+ cmd=[python ' -c "import sigpy.mri.rf;pulse=sigpy.mri.rf.dzrf(' num2str(N) ...
131+ ',' num2str(opt.timeBwProduct) ',ptype=''' ptype '''' ...
132+ ',d1=' num2str(opt.passbandRipple) ',d2=' num2str(opt.stopbandRipple) ...
133+ ',ftype=''' opt.filterType '''' add_opt ');print(*pulse)"'];
134+else
135+ cmd=[python ' -c $''import sigpy.mri.rf\npulse=sigpy.mri.rf.dzrf(' num2str(N) ...
136+ ',' num2str(opt.timeBwProduct) ',ptype=\''' ptype '\''' ...
137+ ',d1=' num2str(opt.passbandRipple) ',d2=' num2str(opt.stopbandRipple) ...
138+ ',ftype=\''' opt.filterType '\''' add_opt ')\nprint(*pulse)'''];
139+end
140+%fprintf('cmd=%s\n',cmd);
141+[status, result]=system(cmd);
142+
143+if status~=0
144+ error('executing python command failed, error message was: %s', result);
145+end
146+
147+lines = regexp(result,'\n','split'); % the response from the python call contains some garbage
148+% look for a usable result vector
149+for i=1:length(lines)
150+ try
151+ signal = str2num(lines{i});
152+ if length(signal)==N
153+ break;
154+ end
155+ catch
156+ continue;
157+ end
158+end
159+if length(signal)~=N
160+ error('could not find usable data in the response of the Python command');
161+end
162+
163+BW = opt.timeBwProduct/opt.duration;
164+t = ((1:N)-0.5)*opt.dwell;
165+flip = abs(sum(signal))*opt.dwell*2*pi;
166+signal = signal*opt.flipAngle/flip;
167+
168+rf.type = 'rf';
169+rf.signal = signal;
170+rf.t = t;
171+rf.shape_dur=N*opt.dwell;
172+rf.freqOffset = opt.freqOffset;
173+rf.phaseOffset = opt.phaseOffset;
174+rf.freqPPM = opt.freqPPM;
175+rf.phasePPM = opt.phasePPM;
176+rf.deadTime = sys.rfDeadTime;
177+rf.ringdownTime = sys.rfRingdownTime;
178+rf.delay = opt.delay;
179+rf.center = mr.calcRfCenter(rf);
180+if ~isempty(opt.use)
181+ rf.use=opt.use;
182+end
183+if rf.deadTime > rf.delay
184+ rf.delay = rf.deadTime;
185+end
186+
187+if nargout > 1
188+ assert(opt.sliceThickness > 0,'SliceThickness must be provided');
189+ if opt.maxGrad > 0
190+ sys.maxGrad = opt.maxGrad;
191+ end
192+ if opt.maxSlew > 0
193+ sys.maxSlew = opt.maxSlew;
194+ end
195+
196+ amplitude = BW/opt.sliceThickness;
197+ area = amplitude*opt.duration;
198+ gz = mr.makeTrapezoid('z', sys, 'flatTime', opt.duration, ...
199+ 'flatArea', area);
200+ gzr= mr.makeTrapezoid('z', sys, 'Area', -area*(1-rf.center/rf.shape_dur)-0.5*(gz.area-area));
201+ if rf.delay > gz.riseTime
202+ gz.delay = ceil((rf.delay - gz.riseTime)/sys.gradRasterTime)*sys.gradRasterTime; % round-up to gradient raster
203+ end
204+ if rf.delay < (gz.riseTime+gz.delay)
205+ rf.delay = gz.riseTime+gz.delay; % these are on the grad raster already which is coarser
206+ end
207+end
208+
209+% v1.4 finally eliminates RF zerofilling
210+% if rf.ringdownTime > 0
211+% tFill = (1:round(rf.ringdownTime/1e-6))*1e-6; % Round to microsecond
212+% rf.t = [rf.t rf.t(end)+tFill];
213+% rf.signal = [rf.signal, zeros(size(tFill))];
214+% end
215+if nargout > 3
216+ delay=mr.makeDelay(mr.calcDuration(rf)); % calcDuration already includes the ringdown time
217+end
218+
219+% RF amplitude check
220+rf_amplitude=max(abs(rf.signal));
221+if rf_amplitude>sys.maxB1
222+ warning('WARNING: system maximum RF amplitude exceeded (%.01f%%)', rf_amplitude/sys.maxB1*100);
223+end
224+
225+end
src/engine/pulseq/+mr/makeSincPulse.madded+152−0View file
@@ -0,0 +1,152 @@
1+function [rf, gz, gzr, delay] = makeSincPulse(flip,varargin)
2+%makeSincPulse Create a slice selective since pulse.
3+% rf=makeSincPulse(flip, 'Duration', dur) Create sinc pulse
4+% with given flip angle (rad) and duration (s).
5+%
6+% rf=makeSincPulse(..., 'freqOffset', f,'phaseOffset',p)
7+% Create sinc pulse with frequency offset (Hz) and phase offset (rad).
8+%
9+% rf=makeSincPulse(..., 'freqPPM',-3.3)
10+% Create arbitrary RF pulse with frequency offset specified in PPM (e.g.
11+% actual frequency offset proportional to the true Larmor frequency), in
12+% this example -3.3 ppm as often used for fat saturation; can be combined
13+% with the 'freqOffset' specified in Hz.
14+%
15+% [rf, gz]=makeSincPulse(...,'SliceThickness',st) Return the
16+% slice select gradient corresponding to given slice thickness (m).
17+%
18+% [rf, gz]=makeSincPulse(flip,lims,...) Create slice selection gradient
19+% with the specificed gradient limits (e.g. amplitude, slew).
20+%
21+% [rf, gz, gzr]=makeSincPulse(flip,lims,...) Create slice selection and
22+% slice refocusing gradients with the specificed gradient limits
23+% (e.g. amplitude, slew) and taking into account 'centerpos' parameter
24+%
25+% See also Sequence.addBlock
26+
27+validPulseUses = mr.getSupportedRfUse();
28+
29+persistent parser
30+if isempty(parser)
31+ parser = mr.aux.InputParserCompat;
32+ parser.FunctionName = 'makeSincPulse';
33+
34+ % RF params
35+ addRequired(parser, 'flipAngle', @isnumeric);
36+ addOptional(parser, 'system', [], @isstruct);
37+ %addParamValue(parser, 'system', [], @isstruct);
38+ addParamValue(parser, 'duration', 0, @isnumeric);
39+ addParamValue(parser, 'freqOffset', 0, @isnumeric);
40+ addParamValue(parser, 'phaseOffset', 0, @isnumeric);
41+ addParamValue(parser, 'freqPPM', 0, @isnumeric);
42+ addParamValue(parser, 'phasePPM', 0, @isnumeric);
43+ addParamValue(parser, 'timeBwProduct', 4, @isnumeric);
44+ addParamValue(parser, 'apodization', 0, @isnumeric);
45+ addParamValue(parser, 'centerpos', 0.5, @isnumeric);
46+ % Slice params
47+ addParamValue(parser, 'maxGrad', 0, @isnumeric);
48+ addParamValue(parser, 'maxSlew', 0, @isnumeric);
49+ addParamValue(parser, 'sliceThickness', 0, @isnumeric);
50+ addParamValue(parser, 'delay', 0, @isnumeric);
51+ addParamValue(parser, 'dwell', 0, @isnumeric); % dummy default value
52+ % whether it is a refocusing pulse (for k-space calculation)
53+ addParamValue(parser, 'use', 'u', @(x) any(validatestring(x,validPulseUses)));
54+end
55+parse(parser, flip, varargin{:});
56+opt = parser.Results;
57+
58+if isempty(opt.system)
59+ system=mr.opts();
60+else
61+ system=opt.system;
62+end
63+
64+if opt.dwell==0
65+ opt.dwell=system.rfRasterTime;
66+end
67+
68+if opt.duration<=0
69+ error('rf pulse duration must be positive');
70+end
71+
72+BW = opt.timeBwProduct/opt.duration;
73+alpha = opt.apodization;
74+N = round(opt.duration/opt.dwell);
75+t = ((1:N)-0.5)'*opt.dwell;
76+tt = t - opt.duration*opt.centerpos;
77+window = (1.0-alpha+alpha*cos(2*pi*tt/opt.duration));
78+signal = window.*sinc(BW*tt);
79+flip = sum(signal)*opt.dwell*2*pi;
80+signal = signal*opt.flipAngle/flip;
81+
82+rf.type = 'rf';
83+rf.signal = signal;
84+rf.t = t;
85+rf.shape_dur=N*opt.dwell;
86+rf.freqOffset = opt.freqOffset;
87+rf.phaseOffset = opt.phaseOffset;
88+rf.freqPPM = opt.freqPPM;
89+rf.phasePPM = opt.phasePPM;
90+rf.deadTime = system.rfDeadTime;
91+rf.ringdownTime = system.rfRingdownTime;
92+rf.delay = opt.delay;
93+if ~isempty(opt.use)
94+ rf.use=opt.use;
95+end
96+if rf.deadTime > rf.delay
97+ rf.delay = rf.deadTime;
98+end
99+rf.center=opt.duration*opt.centerpos;
100+
101+if nargout > 1
102+ assert(opt.sliceThickness > 0,'SliceThickness must be provided');
103+ if opt.maxGrad > 0
104+ system.maxGrad = opt.maxGrad;
105+ end
106+ if opt.maxSlew > 0
107+ system.maxSlew = opt.maxSlew;
108+ end
109+
110+ amplitude = BW/opt.sliceThickness;
111+ area = amplitude*opt.duration;
112+ gz = mr.makeTrapezoid('z', system, 'flatTime', opt.duration, ...
113+ 'flatArea', area);
114+ gzr= mr.makeTrapezoid('z', system, 'Area', -area*(1-opt.centerpos)-0.5*(gz.area-area));
115+ if rf.delay > gz.riseTime
116+ gz.delay = ceil((rf.delay - gz.riseTime)/system.gradRasterTime)*system.gradRasterTime; % round-up to gradient raster
117+ end
118+ if rf.delay < (gz.riseTime+gz.delay)
119+ rf.delay = gz.riseTime+gz.delay; % these are on the grad raster already which is coarser
120+ end
121+end
122+
123+% v1.4 finally eliminates RF zerofilling
124+% if rf.ringdownTime > 0
125+% tFill = (1:round(rf.ringdownTime/1e-6))*1e-6; % Round to microsecond
126+% rf.t = [rf.t rf.t(end)+tFill];
127+% rf.signal = [rf.signal, zeros(size(tFill))];
128+% end
129+if nargout > 3
130+ delay=mr.makeDelay(mr.calcDuration(rf)); % calcDuration already includes the ringdown time
131+end
132+
133+% RF amplitude check
134+rf_amplitude=max(abs(rf.signal));
135+if rf_amplitude>system.maxB1
136+ warning('WARNING: system maximum RF amplitude exceeded (%.01f%%)', rf_amplitude/system.maxB1*100);
137+end
138+
139+function y = sinc(x)
140+ % sinc Calculate the sinc function:
141+ % sinc(x) = sin(pi*x)/(pi*x)
142+ %
143+ % This is a useful helper function for those without the signal
144+ % processing toolbox
145+
146+ i = find(x == 0);
147+ x(i) = 1;
148+ y = sin(pi*x)./(pi*x);
149+ y(i) = 1;
150+end
151+
152+end
src/engine/pulseq/+mr/makeSoftDelay.madded+46−0View file
@@ -0,0 +1,46 @@
1+function sd = makeSoftDelay(varargin)
2+%makeSoftDelay Create a soft delay extension event.
3+% softDelay=makeSoftDelay() Create a soft delay event, that can be used in
4+% combionation with an empty (pure delay) block
5+% e.g. to adjust TE, TR or other delays. The soft delay
6+% extension acts by rewriting the block duration
7+% based on the user input (to the interpreter)
8+% according to the equation dur=input/factor+offset.
9+% Required parameters are 'numeric ID' and 'string
10+% hint'. Optional parameter 'factor' can be either
11+% positive and negative. Optional parameter 'offset'
12+% given in seconds can also be either positive and
13+% negative. The 'hint' parameter is expected to be
14+% for identical 'numID'.
15+%
16+% See also Sequence.addBlock() Sequence.applySoftDelay()
17+
18+persistent parser
19+if isempty(parser)
20+ parser = mr.aux.InputParserCompat;
21+ parser.FunctionName = 'makeSoftDelay';
22+
23+ addRequired(parser, 'numID', @isnumeric);
24+ addRequired(parser, 'hint', @(x) ischar(x)&&~isempty(x));
25+ addOptional(parser, 'offset', 0, @isnumeric);
26+ addOptional(parser, 'factor', 1, @isnumeric);
27+end
28+
29+parse(parser, varargin{:});
30+opt = parser.Results;
31+
32+if length(regexp(opt.hint, '(\s+)','split'))>1
33+ error('makeSoftDelay: parameter ''hint'' may not contain white space characters');
34+end
35+
36+if opt.factor==0
37+ error('makeSoftDelay: parameter ''factor'' must be nonzero');
38+end
39+
40+sd.type = 'softDelay';
41+sd.num = opt.numID;
42+sd.hint = opt.hint;
43+sd.offset = opt.offset;
44+sd.factor = opt.factor;
45+
46+end
src/engine/pulseq/+mr/makeTrapezoid.madded+254−0View file
@@ -0,0 +1,254 @@
1+function grad=makeTrapezoid(channel, varargin)
2+%makeTrapezoid Create a trapezoid gradient event.
3+%
4+% PURPOSE
5+% Build a trapezoidal gradient event struct (rise / flat-top / fall)
6+% for a given logical channel. The returned struct is consumed by
7+% mr.Sequence/addBlock to add the gradient to a sequence.
8+%
9+% SIGNATURES
10+% g = mr.makeTrapezoid(channel, ...) % uses mr.opts() defaults
11+% g = mr.makeTrapezoid(channel, system, ...) % system as 2nd positional arg
12+% g = mr.makeTrapezoid(channel, ..., 'system', system) % system as name/value
13+% g = mr.makeTrapezoid(channel, ..., 'Duration', d, 'Area', a)
14+% g = mr.makeTrapezoid(channel, ..., 'FlatTime', ft, 'FlatArea', fa)
15+% g = mr.makeTrapezoid(channel, ..., 'FlatTime', ft, 'Amplitude', amp)
16+% g = mr.makeTrapezoid(channel, ..., 'Area', a) % shortest possible timing
17+%
18+% Exactly one of 'Area', 'FlatArea', or 'Amplitude' must be supplied.
19+% Timing is determined by 'FlatTime' if given, else 'Duration' if given,
20+% else the shortest realizable timing for the requested 'Area'.
21+% Parameter names are case-insensitive.
22+%
23+% INPUTS
24+% channel [required] char, 'x'|'y'|'z'
25+% system [optional] struct from mr.opts; defaults to mr.opts() if omitted
26+% 'Duration' [name/value] double, total duration including ramps, seconds, >0
27+% 'Area' [name/value] double, total gradient area including ramps, 1/m
28+% 'FlatTime' [name/value] double, flat-top duration, seconds
29+% 'FlatArea' [name/value] double, flat-top-only area, 1/m
30+% 'Amplitude' [name/value] double, flat-top amplitude, Hz/m
31+% 'maxGrad' [name/value] double, override system.maxGrad, Hz/m
32+% 'maxSlew' [name/value] double, override system.maxSlew, Hz/m/s
33+% 'riseTime' [name/value] double, force rise time, seconds
34+% 'fallTime' [name/value] double, force fall time, seconds (requires riseTime)
35+% 'delay' [name/value] double, pre-event delay, seconds, default 0
36+%
37+% OUTPUT
38+% grad struct with fields:
39+% .type char, always 'trap' (includes degenerate triangle, flatTime=0)
40+% .channel char, 'x'|'y'|'z'
41+% .amplitude double, flat-top amplitude, Hz/m
42+% .riseTime double, ramp-up duration, seconds
43+% .flatTime double, flat-top duration, seconds (0 for triangular)
44+% .fallTime double, ramp-down duration, seconds
45+% .area double, total area including ramps, 1/m
46+% (= amplitude * (flatTime + riseTime/2 + fallTime/2))
47+% .flatArea double, flat-top-only area, 1/m (= amplitude * flatTime)
48+% .delay double, pre-event delay, seconds
49+% .first double, gradient value at t=0, Hz/m (always 0 for trap)
50+% .last double, gradient value at end, Hz/m (always 0 for trap)
51+%
52+% ERRORS
53+% makeTrapezoid:invalidArguments
54+% - 'fallTime' specified without 'riseTime'.
55+% - Not exactly one of 'Area' / 'FlatArea' / 'Amplitude' supplied.
56+% - 'FlatTime' supplied without 'FlatArea' or 'Amplitude'.
57+% - Neither 'Area' nor 'Duration' supplied.
58+% makeTrapezoid:invalidDuration
59+% - Requested area cannot be realized within the requested duration
60+% under maxGrad/maxSlew. Error message reports the minimum
61+% achievable duration in microseconds.
62+% makeTrapezoid:invalidAmplitude
63+% - Computed amplitude exceeds maxGrad.
64+% Assertion failure
65+% - With explicit riseTime+duration: duration < riseTime+fallTime,
66+% or computed amplitude exceeds maxGrad.
67+%
68+% NOTES
69+% - All ramp/flat times are rounded up to system.gradRasterTime.
70+% - Internal storage uses Hz/m and Hz/m/s regardless of the units
71+% passed to mr.opts (mr.opts converts on input). Use mr.convert
72+% if you need physical units (mT/m, T/m/s, etc.).
73+% - Caches an inputParser in a persistent variable for performance;
74+% no other global state.
75+%
76+% EXAMPLE
77+% sys = mr.opts('MaxGrad', 30, 'GradUnit', 'mT/m', ...
78+% 'MaxSlew', 170, 'SlewUnit', 'T/m/s');
79+% Nx = 256; fov = 256e-3; deltak = 1/fov;
80+% % Readout gradient with fixed flat-top area
81+% gx = mr.makeTrapezoid('x', sys, 'FlatArea', Nx*deltak, 'FlatTime', 6.4e-3);
82+% % Matching prephaser: half the (negative) area of the readout
83+% gxPre = mr.makeTrapezoid('x', sys, 'Area', -gx.area/2, 'Duration', 1e-3);
84+%
85+% SEE ALSO
86+% mr.opts, mr.makeExtendedTrapezoid, mr.makeArbitraryGrad,
87+% mr.calcDuration, mr.Sequence/addBlock
88+
89+persistent parser
90+
91+if isempty(parser)
92+ validChannels = {'x','y','z'};
93+ parser = mr.aux.InputParserCompat;
94+ parser.FunctionName = 'makeTrapezoid';
95+ parser.addRequired('channel',...
96+ @(x) any(validatestring(x,validChannels)));
97+ parser.addOptional('system',[],@isstruct);
98+ parser.addParamValue('duration',0,@(x)(isnumeric(x) && x>0));
99+ parser.addParamValue('area',[],@isnumeric);
100+ parser.addParamValue('flatTime',[],@isnumeric);
101+ parser.addParamValue('flatArea',[],@isnumeric);
102+ parser.addParamValue('amplitude',[],@isnumeric);
103+ parser.addParamValue('maxGrad',0,@isnumeric);
104+ parser.addParamValue('maxSlew',0,@isnumeric);
105+ parser.addParamValue('riseTime',0,@isnumeric);
106+ parser.addParamValue('fallTime',0,@isnumeric);
107+ parser.addParamValue('delay',0,@isnumeric);
108+end
109+parse(parser,channel,varargin{:});
110+opt = parser.Results;
111+
112+if isempty(opt.system)
113+ system=mr.opts();
114+else
115+ system=opt.system;
116+end
117+
118+maxSlew=system.maxSlew;
119+%riseTime=system.riseTime;
120+maxGrad=system.maxGrad;
121+fallTime = [];
122+riseTime = [];
123+
124+if opt.maxGrad>0
125+ maxGrad=opt.maxGrad;
126+end
127+if opt.maxSlew>0
128+ maxSlew=opt.maxSlew;
129+end
130+if opt.riseTime>0
131+ riseTime=opt.riseTime;
132+end
133+if opt.fallTime>0
134+ if isempty(riseTime)
135+ error('makeTrapezoid:invalidArguments','Must always supply ''riseTime'' if ''fallTime'' is specified explicitly.');
136+ end
137+ fallTime=opt.fallTime;
138+end
139+
140+
141+if (isempty(opt.area)+isempty(opt.flatArea)+isempty(opt.amplitude))~=2
142+ error('makeTrapezoid:invalidArguments','Must supply either ''area'', ''flatArea'' or ''amplitude'', and only one of the three may be specified');
143+end
144+if ~isempty(opt.flatTime) % MZ was: opt.flatTime>0
145+ if ~isempty(opt.amplitude)
146+ amplitude = opt.amplitude;
147+ else
148+ if isempty(opt.flatArea)
149+ error('makeTrapezoid:invalidArguments','When ''flatTime'' is provided either ''flatArea'' or ''amplitude'' must be provided as well; you may consider providing ''duration'', ''area'' and optionally ramp times instead.');
150+ end
151+ amplitude = opt.flatArea/opt.flatTime;
152+ end
153+ if isempty(riseTime)
154+ riseTime = abs(amplitude)/maxSlew;
155+ riseTime = ceil(riseTime/system.gradRasterTime)*system.gradRasterTime;
156+ if riseTime==0
157+ riseTime=system.gradRasterTime;
158+ end
159+ end
160+ if isempty(fallTime)
161+ fallTime = riseTime;
162+ end
163+ flatTime = opt.flatTime;
164+elseif opt.duration>0
165+ if ~isempty(opt.amplitude)
166+ amplitude = opt.amplitude;
167+ else
168+ if isempty(riseTime)
169+ dC = 1/abs(2*maxSlew) + 1/abs(2*maxSlew);
170+ possible = opt.duration^2 > 4*abs(opt.area)*dC;
171+ if ~possible
172+ [~, t1, t2, t3]=calcShortestParamsForArea(opt.area,maxSlew,maxGrad,system.gradRasterTime);
173+ error('makeTrapezoid:invalidDuration',['Requested area is too large for this gradient. Minimum required duration for this area (accounting for the gradient raster time) is ' num2str((t1+t2+t3)*1e6) 'us']);
174+ end
175+ amplitude = ( opt.duration - sqrt(opt.duration^2 - 4*abs(opt.area)*dC) )/(2*dC);
176+ else
177+ if isempty(fallTime)
178+ fallTime = riseTime;
179+ end
180+ amplitude = opt.area/(opt.duration-0.5*riseTime-0.5*fallTime);
181+ possible = opt.duration>=(riseTime+fallTime) & abs(amplitude)<maxGrad;
182+ assert(possible,['Requested area is too large for this gradient duration. Probably amplitude is violated (' num2str(round(abs(amplitude)/maxGrad*100)) '%)']);
183+ end
184+ end
185+ if isempty(riseTime)
186+ riseTime = ceil(abs(amplitude)/maxSlew/system.gradRasterTime)*system.gradRasterTime;
187+ if(riseTime==0)
188+ riseTime=system.gradRasterTime;
189+ end
190+ end
191+ if isempty(fallTime)
192+ fallTime = riseTime;
193+ end
194+ flatTime = opt.duration-riseTime-fallTime;
195+ if isempty(opt.amplitude)
196+ % Adjust amplitude (after rounding) to achieve given area
197+ amplitude = opt.area/(riseTime/2 + fallTime/2 + flatTime);
198+ end
199+else
200+ if isempty(opt.area)
201+ error('makeTrapezoid:invalidArguments','Must supply area or duration');
202+ else
203+ % call the local function to calculate the shortest timing
204+ [amplitude, riseTime, flatTime, fallTime]=calcShortestParamsForArea(opt.area,maxSlew,maxGrad,system.gradRasterTime);
205+ end
206+end
207+if abs(amplitude)>maxGrad
208+ if isempty(opt.area)
209+ error('makeTrapezoid:invalidAmplitude',['Amplitude violation (' num2str(round(abs(amplitude)/maxGrad*100)) '%%)']);
210+ else
211+ % this error can only be produced by the failed trapezoid
212+ % calculation with the specified area (leading to exceedingly high
213+ % amplitude), the triangular blip error should have occured around
214+ % line 103
215+ [~, t1, t2, t3]=calcShortestParamsForArea(opt.area,maxSlew,maxGrad,system.gradRasterTime);
216+ error('makeTrapezoid:invalidDuration',['Requested duration is too short for the area to be realized within system limits. Minimum duration for this trapezoid (accounting for the gradient raster time) is ' num2str((t1+t2+t3)*1e6) ' us']);
217+ end
218+end
219+
220+grad.type = 'trap';
221+grad.channel = opt.channel;
222+grad.amplitude = amplitude;
223+grad.riseTime = riseTime;
224+grad.flatTime = flatTime;
225+grad.fallTime = fallTime;
226+grad.area = amplitude*(flatTime + riseTime/2 + fallTime/2);
227+grad.flatArea = amplitude*flatTime;
228+grad.delay = opt.delay;
229+grad.first = 0;
230+grad.last = 0;
231+
232+end
233+
234+function [amplitude, riseTime, flatTime, fallTime] = calcShortestParamsForArea(area,maxSlew,maxGrad,gradRasterTime)
235+ % find the shortest possible duration
236+ % first check if the area can be realized as a triangle
237+ % if not we calculate a trapezoid
238+ riseTime=ceil(sqrt(abs(area)/maxSlew)/gradRasterTime)*gradRasterTime;
239+ if riseTime < gradRasterTime % the "area" was probably 0 or almost 0 ...
240+ riseTime=gradRasterTime;
241+ end
242+ amplitude=area/riseTime;
243+ tEff=riseTime;
244+ if abs(amplitude)>maxGrad
245+ tEff=ceil(abs(area)/maxGrad/gradRasterTime)*gradRasterTime;
246+ amplitude=area/tEff;
247+ riseTime=ceil(abs(amplitude)/maxSlew/gradRasterTime)*gradRasterTime;
248+ if(riseTime==0)
249+ riseTime=gradRasterTime;
250+ end
251+ end
252+ flatTime=tEff-riseTime;
253+ fallTime=riseTime;
254+end
src/engine/pulseq/+mr/makeTrigger.madded+45−0View file
@@ -0,0 +1,45 @@
1+function trig = makeTrigger(channel, varargin)
2+%makeTrigger Create a trigger halt event.
3+% trigger=makeTrigger() Create a trigger event for a synchronisation with
4+% an external signal from a given channel with an
5+% optional given delay prio to the sync and
6+% duration after the sync.
7+% Possible channel values: 'physio1','physio2' (Siemens specific)
8+%
9+% See also Sequence.addBlock
10+
11+persistent parser
12+if isempty(parser)
13+ parser = mr.aux.InputParserCompat;
14+ parser.FunctionName = 'makeTrigger';
15+
16+ addOptional(parser, 'delay', 0, @isnumeric);
17+ addOptional(parser, 'duration', 0, @isnumeric); % will replace with gradRadterTime below
18+ addOptional(parser, 'system', [], @isstruct);
19+end
20+
21+if nargin<1
22+ error('makeTrigger:invalidArguments','Must supply a channel');
23+end
24+
25+parse(parser, varargin{:});
26+opt = parser.Results;
27+
28+if isempty(opt.system)
29+ system=mr.opts();
30+else
31+ system=opt.system;
32+end
33+
34+channel_num=find(strcmp(channel,{'physio1','physio2'}));
35+assert(~isempty(channel_num) && channel_num>0,'makeTrigger:invalidChannel',...
36+ 'Channel (%s) is invalid',channel);
37+trig.type = 'trigger';
38+trig.channel=channel;
39+trig.delay = opt.delay;
40+trig.duration = opt.duration;
41+if (trig.duration<=system.gradRasterTime)
42+ trig.duration=system.gradRasterTime;
43+end
44+
45+end
src/engine/pulseq/+mr/opts.madded+131−0View file
@@ -0,0 +1,131 @@
1+function out=opts(varargin)
2+%OPTS Set gradient limits and other system properties of the MR system.
3+% g=OPTS() Return the default amplitude and slew limits.
4+%
5+% g=OPTS('maxGrad',30,'gradUnit','mT/m') Set the maximum gradient to
6+% 30mT/m.
7+
8+persistent defaultUserOpts
9+persistent defaultStandardOpts
10+if isempty(defaultStandardOpts)
11+ defaultStandardOpts=struct(...
12+ 'maxGrad',mr.convert(40,'mT/m'),... % Default: 40 mT/m
13+ 'maxSlew',mr.convert(170,'T/m/s'),... % Default: 170 mT/m/ms
14+ 'maxB1',mr.convert(20,'uT'),... % Default: 20 uT
15+ 'maxFreqOffset',250e3,... % Default: +-250 kHz -- maximum frequency offset for ADC and RF; some systems may have it higher, but they are probably rare (e.g. Siemens Terra seems to accept 400kHz)
16+ 'riseTime',[],...
17+ 'rfDeadTime',0,...
18+ 'rfRingdownTime',0,...
19+ 'adcDeadTime',0,...
20+ 'adcRasterTime',100e-9,...
21+ 'rfRasterTime',1e-6,...
22+ 'gradRasterTime',10e-6,...
23+ 'blockDurationRaster',10e-6,...
24+ 'adcSamplesLimit',0,... % 0 means no limit
25+ 'rfSamplesLimit',0,... % 0 means no limit
26+ 'adcSamplesDivisor',4,... % the number of which the adc.numSamples should be integer multiple
27+ 'flag_trid', true,... % default: true; false -> can be used to ignore seq.addTRID() statements
28+ 'gamma',42576000,...
29+ 'B0',1.5...
30+ );
31+end
32+
33+if ~isempty(defaultUserOpts)
34+ defaultOpts=defaultUserOpts;
35+else
36+ defaultOpts=defaultStandardOpts;
37+end
38+
39+if isempty(varargin) % accelerate default constructor calls
40+ out=defaultOpts;
41+ return
42+end
43+
44+persistent parser
45+validB1Units={'Hz','T','mT','uT'}; % todo: gauss?
46+validGradUnits={'Hz/m','mT/m','rad/ms/mm'};
47+validSlewUnits={'Hz/m/s','mT/m/ms','T/m/s','rad/ms/mm/ms'};
48+if isempty(parser)
49+ parser = inputParser;
50+ parser.FunctionName = 'opts';
51+ parser.addParamValue('gradUnit',validGradUnits{1},...
52+ @(x) any(validatestring(x,validGradUnits)));
53+ parser.addParamValue('slewUnit',validSlewUnits{1},...
54+ @(x) any(validatestring(x,validSlewUnits)));
55+ parser.addParamValue('b1Unit',validB1Units{1},...
56+ @(x) any(validatestring(x,validB1Units)));
57+ parser.addParamValue('maxGrad',[],@isnumeric);
58+ parser.addParamValue('maxSlew',[],@isnumeric);
59+ parser.addParamValue('maxB1',[],@isnumeric);
60+ parser.addParamValue('riseTime',[],@isnumeric);
61+ parser.addParamValue('maxFreqOffset',defaultOpts.maxFreqOffset,@isnumeric);
62+ parser.addParamValue('rfDeadTime',defaultOpts.rfDeadTime,@isnumeric);
63+ parser.addParamValue('rfRingdownTime',defaultOpts.rfRingdownTime,@isnumeric);
64+ parser.addParamValue('adcDeadTime',defaultOpts.adcDeadTime,@isnumeric);
65+ parser.addParamValue('adcRasterTime',defaultOpts.adcRasterTime,@isnumeric);
66+ parser.addParamValue('rfRasterTime',defaultOpts.rfRasterTime,@isnumeric);
67+ parser.addParamValue('gradRasterTime',defaultOpts.gradRasterTime,@isnumeric);
68+ parser.addParamValue('blockDurationRaster',defaultOpts.blockDurationRaster,@isnumeric);
69+ parser.addParamValue('adcSamplesLimit',defaultOpts.adcSamplesLimit,@isnumeric);
70+ parser.addParamValue('rfSamplesLimit',defaultOpts.rfSamplesLimit,@isnumeric);
71+ parser.addParamValue('adcSamplesDivisor',defaultOpts.adcSamplesDivisor,@isnumeric);
72+ parser.addParamValue('flag_trid',defaultOpts.flag_trid,@(x) islogical(x) || isnumeric(x));
73+ parser.addParamValue('gamma',defaultOpts.gamma,@isnumeric); % Hz/T
74+ parser.addParamValue('B0',defaultOpts.B0,@isnumeric); % T
75+ parser.addParamValue('setAsDefault',false,@islogical);
76+ parser.addParamValue('resetDefault',false,@islogical);
77+end
78+parse(parser,varargin{:});
79+opt = parser.Results;
80+
81+if opt.resetDefault
82+ defaultUserOpts=[];
83+ parser=[];
84+ return;
85+end
86+
87+if isempty(opt.maxB1)
88+ maxB1 = defaultOpts.maxB1;
89+else
90+ maxB1 = mr.convert(opt.maxB1,opt.b1Unit,'Hz','gamma',opt.gamma);
91+end
92+if isempty(opt.maxGrad)
93+ maxGrad = defaultOpts.maxGrad;
94+else
95+ maxGrad = mr.convert(opt.maxGrad,opt.gradUnit,'Hz/m','gamma',opt.gamma);
96+end
97+if isempty(opt.maxSlew)
98+ maxSlew=defaultOpts.maxSlew;
99+else
100+ maxSlew = mr.convert(opt.maxSlew,opt.slewUnit,'Hz/m/s','gamma',opt.gamma);
101+end
102+if ~isempty(opt.riseTime)
103+ %maxSlew=[];
104+ maxSlew=maxGrad/opt.riseTime;
105+end
106+
107+out.maxGrad = maxGrad;
108+out.maxSlew = maxSlew;
109+out.maxB1 = maxB1;
110+out.maxFreqOffset = opt.maxFreqOffset;
111+out.riseTime = opt.riseTime;
112+out.rfDeadTime = opt.rfDeadTime;
113+out.rfRingdownTime = opt.rfRingdownTime;
114+out.adcDeadTime = opt.adcDeadTime;
115+out.adcRasterTime = opt.adcRasterTime;
116+out.rfRasterTime = opt.rfRasterTime;
117+out.gradRasterTime = opt.gradRasterTime;
118+out.blockDurationRaster = opt.blockDurationRaster;
119+out.adcSamplesLimit = opt.adcSamplesLimit;
120+out.rfSamplesLimit = opt.rfSamplesLimit;
121+out.adcSamplesDivisor = opt.adcSamplesDivisor;
122+out.flag_trid = logical(opt.flag_trid);
123+out.gamma=opt.gamma;
124+out.B0=opt.B0;
125+
126+if opt.setAsDefault
127+ defaultUserOpts=out;
128+ parser=[];
129+end
130+
131+end
src/engine/pulseq/+mr/pts2waveform.madded+27−0View file
@@ -0,0 +1,27 @@
1+function waveform = pts2waveform(times, amplitudes, gradRasterTime)
2+
3+grd = (round(min(times)/gradRasterTime):round(max(times)/gradRasterTime))*gradRasterTime; % the previous code was clipping the gradient now and then...
4+grd = grd(1:end-1).';
5+waveform = interp1(times, amplitudes, grd + gradRasterTime/2);
6+
7+% % times = ceil(times/gradRasterTime)*gradRasterTime; % round onto grid
8+% times = ceil(times/gradRasterTime); % round onto grid. SK: Dont multiply by
9+% % gradRasterTime here: This will
10+% % introduce numerical inaccuracies.
11+% times_diff = diff(times);
12+% amplitudes_diff = diff(amplitudes);
13+% waveform = [];
14+% for ii = 1:length(times)-1
15+% % SK: there are no new points after the end, therefore we dont need to
16+% % handle the overlap situation.
17+% if ii == length(times)-1
18+% crop = 0;
19+% else
20+% crop = 1;
21+% end
22+% y = amplitudes_diff(ii)/times_diff(ii)*...
23+% (0:1:(times(ii+1)-times(ii)-crop))...
24+% + amplitudes(ii);
25+% waveform = [waveform y(1:end)];
26+% end
27+end
src/engine/pulseq/+mr/restoreAdditionalShapeSamples.madded+46−0View file
@@ -0,0 +1,46 @@
1+function [tt_chg, waveform_chg] = restoreAdditionalShapeSamples(tt,waveform,first,last,gradRasterTime,iBlock)
2+% restore shape: if we had a
3+% trapezoid converted to shape we have to find
4+% the "corners" and we can eliminate internal
5+% samples on the straight segments
6+% but first we have to restore samples on the
7+% edges of the gradient raster intervals
8+% for that we need the first sample
9+ max_abs=max(abs(waveform));
10+ odd_step1=[first 2*waveform'];
11+ odd_step2=odd_step1.*(mod(1:length(odd_step1),2)*2-1);
12+ waveform_odd_rest=(cumsum(odd_step2).*(mod(1:length(odd_step2),2)*2-1))';
13+ waveform_odd_interp=[first; 0.5*(waveform(1:end-1)+waveform(2:end)); last];
14+ if abs(waveform_odd_rest(end)-last)>2e-5*max_abs % what's the reasonable threshold?
15+ blInfo='';
16+ if exist('iBlock')
17+ blInfo=['[block ' num2str(iBlock) '] '];
18+ end
19+ warning('mr:restoreShape',[blInfo 'Last restored point ' ...
20+ 'differs too much from the recorded last, skipping the shape restoration step; ' ...
21+ 'deviation: ' num2str(abs(waveform_odd_rest(end)-last)) 'Hz/m (' num2str(abs(waveform_odd_rest(end)-last)/max_abs*100) '%%); ' ...
22+ 'No reason to panic, this is typical for spirals. ' ...
23+ 'To disable this warning (assuming you know what you are doing) add \n' ...
24+ '`warning(''OFF'', ''mr:restoreShape'')´ to your sequence-generating script.']);
25+ tt_chg=[0 tt' tt(end)+gradRasterTime/2];
26+ waveform_chg = [first waveform' last];
27+ return;
28+ end
29+ %figure; plot([0,10e-6+grad.t'],waveform_odd_rest-waveform_odd_interp);
30+ waveform_odd_mask=abs(waveform_odd_rest-waveform_odd_interp)<=eps+2e-5*max_abs; % threshold ???
31+ waveform_odd=waveform_odd_interp.*waveform_odd_mask+waveform_odd_rest.*(1-waveform_odd_mask);
32+
33+ % combine odd & even
34+ comb=[ 0 waveform' ; waveform_odd' ];
35+ waveform_os=comb(2:end)';
36+
37+ tt_odd=(0:(length(waveform_odd_rest)-1))*gradRasterTime;
38+ tt_os=(0:(length(waveform_os)-1))*gradRasterTime*0.5;
39+
40+ waveform_even_reint=0.5*(waveform_odd_rest(1:end-1)+waveform_odd_rest(2:end));
41+
42+ maskChanges = abs([1; diff(waveform_os,2); 1])>1e-8; % TRUE if values change
43+ waveform_chg = waveform_os(maskChanges)'; % Elements without repetitions
44+ tt_chg=tt_os(maskChanges);
45+ %figure;plot(grad.tt,grad.waveform);hold on; plot(tt_chg,waveform_chg); plot(tt_chg,waveform_chg,'o');
46+end
src/engine/pulseq/+mr/rotate.madded+198−0View file
@@ -0,0 +1,198 @@
1+function [varargout] = rotate(raxis, angle, varargin)
2+%align set alignment of the objects in the block
3+%
4+% [...] = rotate(axis, angle, obj <, obj> ...);
5+%
6+% Rotates the corresponding gradinet object(s) about the given axis by
7+% the specified amount. Gradients parallel to the rotation axis and
8+% non-gradient objects are not affected.
9+% Possible rotation axes are 'x', 'y' or 'z'.
10+%
11+% The objects that contain the field 'id' but are not modified by this
12+% function are passed through without modification. For the gradients
13+% that are modified, the field 'id' is removed automatically.
14+%
15+% Optional parameter list may include the keyword 'system' followed by a
16+% system limits struct. The system can only be provided in the beginning or
17+% at the ent of the list of optional parameters.
18+%
19+% Returns either a cell-array of objects if one return parameter is
20+% provided or an explicit list of objects if multiple parameters are
21+% given. Can be used directly as a parameter of seq.addBlock().
22+%
23+% See also mr.rotate3D, Sequence.addBlock
24+%
25+% Maxim Zaitsev <maxim.zaitsev@uniklinik-freiburg.de>
26+
27+axes={'x', 'y', 'z'};
28+
29+% cycle through the objects and rotate gradients non-parallel to the
30+% given rotation axis. Rotated gradients assigned to the same axis are then
31+% added together.
32+
33+% first create indexes of the objects to be bypassed or rotated
34+irotate1=[];
35+irotate2=[];
36+ibypass=[];
37+axes2rot=axes(~strcmp(axes,raxis));
38+if length(axes2rot)~=2
39+ error('incorrect axis specification');
40+end
41+if strcmp('y',raxis)
42+ axes2rot=axes2rot(end:-1:1); % we need to reverse the list to preserve the correct handiness of the rotation matrix
43+end
44+
45+if ~isscalar(angle) || ~isnumeric(angle)
46+ error('Rotation angle needs to be a scalar value');
47+end
48+
49+% parse out the optional parameter 'system', which can only be at the beginning
50+% or in the end of the optional parameters
51+system=[];
52+if ischar(varargin{1}) && strcmp(lower(varargin{1}),'system')
53+ if ~isstruct(varargin{2}) || ~isfield(varargin{2},'gradRasterTime')
54+ error('Error parsing input parameters, keyword ''system'' is not followed by a valid system struct');
55+ end
56+ system=varargin{2};
57+ varargin=varargin(3:end);
58+elseif length(varargin)>1 && ischar(varargin{end-1}) && strcmp(lower(varargin{end-1}),'system')
59+ if ~isstruct(varargin{end}) || ~isfield(varargin{end},'gradRasterTime')
60+ error('Error parsing input parameters, keyword ''system'' is not followed by a valid system struct');
61+ end
62+ system=varargin{end};
63+ varargin=varargin(1:end-2);
64+end
65+% make this function accept ready-made blocks
66+if isstruct(varargin{1}) && isfield(varargin{1}, 'rf')
67+ varargin=mr.block2events(varargin);
68+end
69+% we need this to allow for nested mr.rotate() calls
70+if 1==length(varargin) && iscell(varargin{1})
71+ va=varargin{1};
72+else
73+ va=varargin;
74+end
75+
76+for i=1:length(va)
77+ par = va{i};
78+ if isempty(par)
79+ continue;
80+ end
81+ if isnumeric(par) || ...
82+ ((~strcmp(par.type,'grad') && ...
83+ ~strcmp(par.type,'trap')) || ...
84+ strcmp(par.channel, raxis))%['g' axis]
85+ ibypass=[ibypass i];
86+ else
87+ if strcmp(par.channel, axes2rot(1)) %['g' axes2rot(1)]
88+ irotate1=[irotate1 i];
89+ else
90+ if (strcmp(par.channel, axes2rot(2))) %['g' axes2rot(2)]
91+ irotate2=[irotate2 i];
92+ else
93+ ibypass=[ibypass i]; % should never happen
94+ end
95+ end
96+ end
97+end
98+
99+% now every gradient to be rotated generates two new gradients, one on the
100+% original axis and one on the other from the axes2rot list
101+
102+rotated1=cell(1,length(irotate1)+length(irotate2));
103+rotated2=cell(1,length(irotate1)+length(irotate2));
104+max_mag=0; % measure of the relevant amplitude
105+for i=1:length(irotate1)
106+ g=va{irotate1(i)};
107+ if isfield(g,'id'), g=rmfield(g,'id'); end
108+ max_mag=max(max_mag, getGradAbsMag(g));
109+ rotated1{i}=mr.scaleGrad(g,cos(angle));
110+ g=mr.scaleGrad(g,sin(angle));
111+ g.channel=axes2rot{2};
112+ rotated2{i}=g;
113+end
114+o=length(irotate1);
115+for i=1:length(irotate2)
116+ g=va{irotate2(i)};
117+ if isfield(g,'id'), g=rmfield(g,'id'); end
118+ max_mag=max(max_mag, getGradAbsMag(g));
119+ rotated2{i+o}=mr.scaleGrad(g,cos(angle));
120+ g=mr.scaleGrad(g,-sin(angle));
121+ g.channel=axes2rot{1};
122+ rotated1{i+o}=g;
123+end
124+
125+% eliminate zero-amplitude gradients
126+thresh=1e-6*max_mag;
127+for i=length(rotated1):-1:1
128+ if getGradAbsMag(rotated1{i})<thresh
129+ rotated1(i)=[];
130+ end
131+end
132+for i=length(rotated2):-1:1
133+ if getGradAbsMag(rotated2{i})<thresh
134+ rotated2(i)=[];
135+ end
136+end
137+
138+g=cell(1,2);
139+% now we add gradients on the corresponding axis together
140+if (length(rotated1)>1)
141+ if isempty(system)
142+ g{1}=mr.addGradients(rotated1);
143+ else
144+ g{1}=mr.addGradients(rotated1,system);
145+ end
146+else
147+ if (~isempty(rotated1))
148+ g{1}=rotated1{1};
149+ end
150+end
151+
152+if (length(rotated2)>1)
153+ if isempty(system)
154+ g{2}=mr.addGradients(rotated2);
155+ else
156+ g{2}=mr.addGradients(rotated2,system);
157+ end
158+else
159+ if (~isempty(rotated2))
160+ g{2}=rotated2{1};
161+ end
162+end
163+
164+% eliminate zero-amplitude gradients
165+for i=length(g):-1:1
166+ if isempty(g{i}) || getGradAbsMag(g{i})<thresh
167+ g(i)=[];
168+ end
169+end
170+
171+% export
172+bypass=va(ibypass);
173+out={bypass{:},g{:}};
174+
175+nout = nargout;
176+varargout = cell(1,nout);
177+if nout==1
178+ varargout{1}=out;
179+else
180+ nr=min(nout,length(out));
181+ if nout<length(out)
182+ warning('insufficient number of return parameters, some rotated gradient components might go lost');
183+ end
184+ for k=1:nr
185+ varargout{k}=out{k};
186+ end
187+end
188+
189+end
190+
191+
192+function [out] = getGradAbsMag(grad)
193+ if strcmp(grad.type,'trap')
194+ out=abs(grad.amplitude);
195+ else
196+ out=max(abs(grad.waveform));
197+ end
198+end
src/engine/pulseq/+mr/rotate3D.madded+175−0View file
@@ -0,0 +1,175 @@
1+function [varargout] = rotate3D(rotation, varargin)
2+%rotate3D rotate all objects (gradients) in the block by a rotation matrix
3+%
4+% [...] = rotate3D(rotation, obj <, obj> ...);
5+%
6+% Rotates the corresponding gradinet object(s) by the provided rotation
7+% represented either as a 3x3 rotation matrix or a unit quaternion with
8+% the scalar component at the first position, or a polar rotatiion
9+% packaged into a 2D vector [phi, theta] expressed in radians or a single
10+% scalar corresponding to a rotation about Z (e.g. phi) in radians; Keep
11+% in mind that for a single Z axis rotation mr.rotate() can be faster.
12+% Non-gradient objects are not affected.
13+%
14+% The objects that contain the field 'id' but are not modified by this
15+% function are passed through without modification. For the gradients
16+% that are modified, the field 'id' is removed automatically.
17+%
18+% Optional parameter list may include the keyword 'system' followed by a
19+% system limits struct. The system can only be provided in the beginning
20+% or at the ent of the list of optional parameters.
21+%
22+% Returns either a cell-array of objects if one return parameter is
23+% provided or an explicit list of objects if multiple parameters are
24+% given. Can be used directly as a parameter of seq.addBlock().
25+%
26+% See also mr.rotate, Sequence.addBlock
27+%
28+% Maxim Zaitsev <maxim.zaitsev@uniklinik-freiburg.de>
29+
30+% parse out the optional parameter 'system', which can only be at the beginning
31+% or in the end of the optional parameters
32+system=[];
33+if ischar(varargin{1}) && strcmp(lower(varargin{1}),'system')
34+ if ~isstruct(varargin{2}) || ~isfield(varargin{2},'gradRasterTime')
35+ error('Error parsing input parameters, keyword ''system'' is not followed by a valid system struct');
36+ end
37+ system=varargin{2};
38+ varargin=varargin(3:end);
39+elseif length(varargin)>1 && ischar(varargin{end-1}) && strcmp(lower(varargin{end-1}),'system')
40+ if ~isstruct(varargin{end}) || ~isfield(varargin{end},'gradRasterTime')
41+ error('Error parsing input parameters, keyword ''system'' is not followed by a valid system struct');
42+ end
43+ system=varargin{end};
44+ varargin=varargin(1:end-2);
45+end
46+
47+% detect rotation matrix or quaternion formats
48+if size(rotation)==[3 3]
49+ rotMat=rotation;
50+elseif length(rotation)==4
51+ rotMat=mr.aux.quat.toRotMat(rotation);
52+elseif length(rotation)==2
53+ % phi & theta
54+ phi = rotation(1);
55+ theta = rotation(2);
56+ assert( ( phi >= -pi ) && ( phi < 2*pi) , 'makeRotation:invalidTheta',...
57+ 'rotation angle phi (%.2f) is invalid. should be within [-pi,2*pi] radians',phi);
58+ assert( ( theta >= -pi ) && ( theta <= pi) , 'makeRotation:invalidPhi',...
59+ 'rotation angle theta (%.2f) is invalid. should be within [-pi,pi] radians',theta);
60+ q1=[cos(theta/2) 0 sin(theta/2) 0]; % y axis
61+ q2=[cos(phi/2) 0 0 sin(phi/2)]; % z axis
62+ rotQuaternion=mr.aux.quat.multiply(q2,q1); % ok, looks like the order is right
63+ rotMat=mr.aux.quat.toRotMat(rotQuaternion);
64+elseif numel(rotation)==1
65+ % phi
66+ phi = rotation;
67+ assert( ( phi >= -pi ) && ( phi < 2*pi) , 'makeRotation:invalidTheta',...
68+ 'rotation angle phi (%.2f) is invalid. should be within [-pi,2*pi] radians',phi);
69+ q=[cos(phi/2) 0 0 sin(phi/2)]; % z axis
70+ rotMat=mr.aux.quat.toRotMat(q);
71+else
72+ error('The parameter ''rotation'' must either bi a 3x3 matrix or a quaternion');
73+end
74+
75+% make this function accept ready-made blocks
76+if isstruct(varargin{1}) && isfield(varargin{1}, 'rf')
77+ varargin=mr.block2events(varargin);
78+end
79+% we need this to allow for nested mr.rotate() calls
80+if 1==length(varargin) && iscell(varargin{1})
81+ va=varargin{1};
82+else
83+ va=varargin;
84+end
85+
86+% first create indexes of the objects to be bypassed or rotated
87+ibypass=[];
88+grads3_in=cell(1,3);
89+axes={'x', 'y', 'z'};
90+
91+for i=1:length(va)
92+ event = va{i};
93+ if isempty(event)
94+ continue;
95+ end
96+ if isnumeric(event) || ...
97+ ((~strcmp(event.type,'grad') && ...
98+ ~strcmp(event.type,'trap')))
99+ ibypass=[ibypass i];
100+ else
101+ iAxis=find(strcmp(event.channel,axes));
102+ if ~isempty(grads3_in{iAxis})
103+ error('More than one gradient on the same axis %s provided', event.channel);
104+ end
105+ if isfield(event,'id')
106+ grads3_in{iAxis}=rmfield(event,'id');
107+ else
108+ grads3_in{iAxis}=event;
109+ end
110+ end
111+end
112+
113+max_mag=0; % measure of the relevant amplitude
114+for i=1:3
115+ if ~isempty(grads3_in{i})
116+ max_mag=max(max_mag, getGradAbsMag(grads3_in{i}));
117+ end
118+end
119+fthresh=1e-6;
120+thresh=fthresh*max_mag;
121+
122+grads_out={};
123+for j=1:3
124+ grad_out_curr=[];
125+ for i=1:3
126+ if isempty(grads3_in{i}) || ...
127+ abs(rotMat(j,i))<fthresh
128+ continue;
129+ end
130+ g=mr.scaleGrad(grads3_in{i},rotMat(j,i));
131+ g.channel=axes{j};
132+ if isempty(grad_out_curr)
133+ grad_out_curr=g;
134+ else
135+ if isempty(system)
136+ grad_out_curr=mr.addGradients({grad_out_curr,g});
137+ else
138+ grad_out_curr=mr.addGradients({grad_out_curr,g},system);
139+ end
140+ end
141+ end
142+ % only output non-zero-amplitude gradients
143+ if ~isempty(grad_out_curr) && getGradAbsMag(grad_out_curr) >= thresh
144+ grads_out{end+1}=grad_out_curr;
145+ end
146+end
147+
148+% export
149+bypass=va(ibypass);
150+out={bypass{:},grads_out{:}};
151+
152+nout = nargout;
153+varargout = cell(1,nout);
154+if nout==1
155+ varargout{1}=out;
156+else
157+ nr=min(nout,length(out));
158+ if nout<length(out)
159+ warning('insufficient number of return parameters, some rotated gradient components might go lost');
160+ end
161+ for k=1:nr
162+ varargout{k}=out{k};
163+ end
164+end
165+
166+end
167+
168+
169+function [out] = getGradAbsMag(grad)
170+ if strcmp(grad.type,'trap')
171+ out=abs(grad.amplitude);
172+ else
173+ out=max(abs(grad.waveform));
174+ end
175+end
src/engine/pulseq/+mr/scaleGrad.madded+39−0View file
@@ -0,0 +1,39 @@
1+function [grad] = scaleGrad(grad, scale, system)
2+% scaleGrad just scales the gradint with the scalar
3+% if the optional parameter 'system' is provided the result is schecked
4+% against the system limits (amolityde and slew rate)
5+
6+ if isfield(grad,'id')
7+ error('attempting to scale readily registered object! please register objects after calling this function or deregister the argument by calling rmfield(...,''id'')');
8+ end
9+
10+ if strcmp(grad.type,'trap')
11+ grad.amplitude=grad.amplitude*scale;
12+ grad.area=grad.area*scale;
13+ grad.flatArea=grad.flatArea*scale;
14+ if nargin>2
15+ if system.maxGrad<abs(grad.amplitude)
16+ error("mr.scaleGrad: maximum amplitude exceeded (%g %%)",100*abs(grad.amplitude)/system.maxGrad);
17+ end
18+ if abs(grad.amplitude)>eps && system.maxSlew<abs(grad.amplitude)/min(grad.riseTime,grad.fallTime)
19+ error("mr.scaleGrad: maximum slew rate exceeded (%g %%)",100*abs(grad.amplitude)/min(grad.riseTime,grad.fallTime)/system.maxSlew);
20+ end
21+ end
22+ else
23+ grad.waveform=grad.waveform*scale;
24+ grad.first=grad.first*scale;
25+ grad.last=grad.last*scale;
26+ if nargin>2
27+ if system.maxGrad<max(abs(grad.waveform))
28+ error("mr.scaleGrad: maximum amplitude exceeded (%g %%)",100*max(abs(grad.waveform))/system.maxGrad);
29+ end
30+ if max(abs(grad.waveform))>eps
31+ grad_max_abs_slew=max(abs(diff(grad.waveform)./diff(grad.tt)));
32+ if system.maxSlew<grad_max_abs_slew
33+ error("mr.scaleGrad: maximum slew rate exceeded (%g %%)",100*grad_max_abs_slew/system.maxSlew);
34+ end
35+ end
36+ end
37+ end
38+end
39+
src/engine/pulseq/+mr/simRf.madded+148−0View file
@@ -0,0 +1,148 @@
1+function [Mz_z,Mz_xy,F,ref_eff,Mx_xy,My_xy]=simRf(rf,rephase_factor,prephase_factor)
2+%simRf Simulate an RF pulse with the given pulse shape.
3+% [Mz_z,Mz_xy,F,ref_eff,Mx_xy,My_xy]=simRf(pulse,prephase_factor,rephase_factor)
4+% Performs a rapid RF pulse simulation based on the rotation formalism.
5+% The algorithm is optimized by using quaternions to represent rotations.
6+% The compulsory parameter 'rf' is the Pulseq RF pulse. Optional
7+% parameter 'rephase_factor' is needed in several cases e.g. to correclty
8+% visualize the phase of the magnetization for slice-selective
9+% excitation. Another optional parameter 'prephase_factor' is an
10+% experimental parameter useful for simulating refocusing pulses or
11+% spoiling needed.
12+% Return values:
13+% Mz_z,Mz_xy: z and xy comnponents of the magnetisation after the pulse
14+% assuming the unit magnetization was aligned with z before
15+% the pulse. Useful for assessing excitation RF pulses.
16+% F: frequency axis in Hz
17+% ref_eff: Refocusing efficiency of the pulse as a complex value.
18+% Magnitude of ref_eff seems to closely follow Mz_z. Phase
19+% of ref_eff is related to the effective phase of the RF
20+% pulse, e.g. the axis of the planar flip.
21+% Mx_xy,My_xy: xy magnetizations after the RF pulse assuming the unit
22+% magnetization was aligned with x or y axis prior to the
23+% pulse, respectively. Useful for detailed analyses of
24+% refocusing pulses.
25+%
26+% The implementation was inspired by the example by Dr. Tony Stoecker
27+% (https://github.com/stoeckert/mr-simu-example-ismrm19)
28+% The algorithm was rewritten to quaternions and vectorized for
29+% performance by MZ
30+%
31+
32+bw_mul=4; % simulation bandwidth (multiplier of the pulse bandwidth)
33+df=1; % spectral resolution [Hz]
34+dt=10e-6; % (re-)sampling interval
35+
36+if nargin < 2
37+ if isfield(rf,'use') && strcmp(rf.use,'refocusing')
38+ rephase_factor = 0;
39+ else
40+ rephase_factor = -(rf.shape_dur-rf.center)/rf.shape_dur;
41+ end
42+end
43+
44+if nargin < 3
45+ prephase_factor = 0;
46+end
47+
48+[bw,f0]=mr.calcRfBandwidth(rf,0.5,df*10,dt);
49+
50+% our bandwidth here is the total bandwidth relative to 0, so we have to add the center frequency
51+bw = abs(bw) + abs(f0);
52+
53+% adapt time stepping -- just some compromizes -- we stick to dt~1/bw/50
54+if bw>4e3
55+ dt=5e-6;
56+ if bw>1e4
57+ dt=2e-6;
58+ if bw>20000
59+ dt=1e-6;
60+ end
61+ end
62+end
63+
64+T = (1:round(rf.shape_dur/dt))*dt-0.5*dt; % timesteps axis [s]
65+F = 2*pi*linspace(f0-bw_mul*bw/2,f0+bw_mul*bw/2,bw/df)'; % offset frequencies [rad/s]
66+
67+if abs(rf.freqPPM)>eps || abs(rf.phasePPM)>eps
68+ warning('relying on the system properties, like B0 and gamma, stored in the global environment by callimg mr.lims(''setAsDefault'',true)');
69+ sys=mr.opts();
70+ full_freqOffset=rf.freqOffset+rf.freqPPM*1e-6*sys.gamma*sys.B0;
71+ full_phaseOffset=rf.phaseOffset+rf.phasePPM*1e-6*sys.gamma*sys.B0;
72+else
73+ full_freqOffset=rf.freqOffset;
74+ full_phaseOffset=rf.phaseOffset;
75+end
76+
77+shapea = interp1(rf.t, 2*pi*rf.signal.*exp(1i*(full_phaseOffset+2*pi*full_freqOffset*rf.t)),T,'linear',0);
78+
79+% intialize result vectors
80+M_ROT=zeros(size(F));
81+Z_ROT=zeros(size(F));
82+sf=size(F);
83+q=zeros(sf(1),4);
84+q(:,1)=1; % init rotation quaternions
85+
86+% prephaser / left spoiler
87+W = -F*dt*length(T)*prephase_factor; % effective field rotation angle
88+Q = [cos(W/2) zeros(sf) zeros(sf) sin(W/2)];
89+q=quat_multiply(q,Q);
90+
91+% RF pulse simulation
92+for j=1:length(T)
93+ W = -dt*sqrt(abs(shapea(j))^2+F.^2); % effective field rotation angles
94+ n = dt * [real(shapea(j))*ones(sf) imag(shapea(j))*ones(sf) F]./abs(W); % effective field rotation axes
95+ Q = [cos(W/2) sin(W/2).*n];
96+ q=quat_multiply(q,Q);
97+end
98+
99+% rephaser / right spoiler / refocusing pulse
100+W = -F*dt*length(T)*rephase_factor; % effective field rotation angle
101+Q = [cos(W/2) zeros(sf) zeros(sf) sin(W/2)];
102+q=quat_multiply(q,Q);
103+
104+% export results
105+F=F/(2*pi);
106+m=zeros(sf(1),4);
107+
108+% excitation: start with M0=M_z
109+m(:,4)=1;
110+m0rf=quat_multiply(quat_conj(q),quat_multiply(m,q));
111+Mz_z=m0rf(:,4);
112+Mz_xy=m0rf(:,2)+1i*m0rf(:,3);
113+
114+% refocusing: start both with M0=M_x and them M0=M_y
115+m=zeros(sf(1),4);
116+m(:,2)=1;
117+Mx_xy=quat_multiply(quat_conj(q),quat_multiply(m,q));
118+Mx_xy=Mx_xy(:,2)+1i*Mx_xy(:,3);
119+m=zeros(sf(1),4);
120+m(:,3)=1;
121+My_xy=quat_multiply(quat_conj(q),quat_multiply(m,q));
122+My_xy=My_xy(:,2)+1i*My_xy(:,3);
123+ref_eff=(Mx_xy+My_xy*1i)/2;
124+end
125+
126+function qout = quat_multiply( q, r )
127+% quat_multiply: Calculate the product of two quaternions.
128+
129+% Calculate vector portion of quaternion product
130+% vec = s1*v2 + s2*v1 + cross(v1,v2)
131+vec = [q(:,1).*r(:,2) q(:,1).*r(:,3) q(:,1).*r(:,4)] + ...
132+ [r(:,1).*q(:,2) r(:,1).*q(:,3) r(:,1).*q(:,4)]+...
133+ [ q(:,3).*r(:,4)-q(:,4).*r(:,3) ...
134+ q(:,4).*r(:,2)-q(:,2).*r(:,4) ...
135+ q(:,2).*r(:,3)-q(:,3).*r(:,2)];
136+
137+% Calculate scalar portion of quaternion product
138+% scalar = s1*s2 - dot(v1,v2)
139+scalar = q(:,1).*r(:,1) - q(:,2).*r(:,2) - ...
140+ q(:,3).*r(:,3) - q(:,4).*r(:,4);
141+
142+qout = [scalar vec];
143+end
144+
145+function q = quat_conj( q )
146+% quat_conj Calculate the conjugate of a quaternion.
147+q(:,2:4) = -q(:,2:4);
148+end
src/engine/pulseq/+mr/splitGradient.madded+82−0View file
@@ -0,0 +1,82 @@
1+function [grads] = splitGradient(grad, varargin)
2+%SplitGradient Splits a trapezoidal gradient into slew up, flat top and
3+%slew down.
4+%
5+% [grads] = splitGradient(grad)
6+% Returns the individual gradient parts (slew up, flat top and slew down)
7+% as extended trapezoid gradient objects. The delays in the individual
8+% gradient events are adapted such that addGradients(...) produces an
9+% gradient equivalent to 'grad'.
10+%
11+% See also splitGradientAt makeExtendedTrapezoid makeTrapezoid
12+% Sequence.addBlock mr.opts
13+%
14+% Stefan Kroboth <stefan.kroboth@uniklinik-freiburg.de>
15+
16+persistent parser
17+
18+if isempty(parser)
19+ parser = mr.aux.InputParserCompat;
20+ parser.FunctionName = 'splitGradient';
21+ parser.addRequired('grad', @isstruct);
22+ parser.addOptional('system', [], @isstruct);
23+end
24+parse(parser, grad, varargin{:});
25+opt = parser.Results;
26+
27+if isempty(opt.system)
28+ system=mr.opts();
29+else
30+ system=opt.system;
31+end
32+
33+gradRasterTime = system.gradRasterTime;
34+total_length = mr.calcDuration(grad);
35+
36+if strcmp(grad.type, 'trap')
37+ ch = grad.channel;
38+ grad.delay = round(grad.delay /gradRasterTime)*gradRasterTime; % MZ: was ceil
39+ grad.riseTime = round(grad.riseTime/gradRasterTime)*gradRasterTime; % MZ: was ceil
40+ grad.flatTime = round(grad.flatTime/gradRasterTime)*gradRasterTime; % MZ: was ceil
41+ grad.fallTime = round(grad.fallTime/gradRasterTime)*gradRasterTime; % MZ: was ceil
42+
43+ % ramp up
44+ times = [0, grad.riseTime];
45+ amplitudes = [0 grad.amplitude];
46+ rampup = mr.makeExtendedTrapezoid(ch, 'system', system, 'times', times,...
47+ 'amplitudes', amplitudes, ...
48+ 'skip_check', true);
49+ rampup.delay = grad.delay;
50+% rampup.t = rampup.t;
51+
52+
53+ % ramp down
54+ times = [0, grad.fallTime];
55+ amplitudes = [grad.amplitude 0];
56+ rampdown = mr.makeExtendedTrapezoid(ch, 'system', system, 'times', times,...
57+ 'amplitudes', amplitudes, ...
58+ 'skip_check', true);
59+ rampdown.delay = total_length - grad.fallTime;
60+ %rampdown.t = rampdown.t*gradRasterTime;
61+
62+ % flattop
63+ if grad.flatTime > eps
64+ times = [0, grad.flatTime];
65+ amplitudes = [grad.amplitude grad.amplitude ];
66+ flattop = mr.makeExtendedTrapezoid(ch, 'system', system, 'times', times,...
67+ 'amplitudes', amplitudes, ...
68+ 'skip_check', true);
69+ flattop.delay = (grad.delay + grad.riseTime);
70+ else
71+ % triangle -- no flatTop
72+ flattop=[];
73+ end
74+
75+ grads = [rampup flattop rampdown];
76+elseif strcmp(grad.type, 'grad')
77+ error('Splitting of arbitrary gradients is not implemented yet.');
78+else
79+ error('Splitting of unsupported event.');
80+end
81+
82+end
src/engine/pulseq/+mr/splitGradientAt.madded+166−0View file
@@ -0,0 +1,166 @@
1+function [varargout] = splitGradientAt(grad, timepoint, varargin)
2+%SplitGradient Splits a trapezoidal gradient into two extended trapezoids
3+%(currently shaped gradients) defined by the cut line.
4+%
5+% [grads] = splitGradient(grad)
6+% Returns the two gradient parts by cutting the original 'grad' at the
7+% 'timepoint' . For the input type 'trapezoid' the results are trtyurned
8+% as extended trapezoids, for 'arb' as arbitrary gradient objects. The
9+% delays in the individual gradient events are adapted such that
10+% addGradients(...) produces an gradient equivalent to 'grad'.
11+%
12+% See also splitGradient makeExtendedTrapezoid makeTrapezoid
13+% Sequence.addBlock mr.opts
14+%
15+% Maxim Zaitsev <maxim.zaitsev@uniklinik-freiburg.de>
16+% Stefan Kroboth <stefan.kroboth@uniklinik-freiburg.de>
17+
18+persistent parser
19+
20+if isempty(parser)
21+ parser = mr.aux.InputParserCompat;
22+ parser.FunctionName = 'splitGradientAt';
23+ parser.addRequired('grad', @isstruct);
24+ parser.addRequired('timepoint', @isnumeric);
25+ parser.addOptional('system', [], @isstruct);
26+end
27+parse(parser, grad, timepoint, varargin{:});
28+opt = parser.Results;
29+
30+if isfield(grad,'id')
31+ error('attempting to split readily registered object! please register objects after calling this function or deregister the argument by calling rmfield(...,''id'')');
32+end
33+
34+if isempty(opt.system)
35+ system=mr.opts();
36+else
37+ system=opt.system;
38+end
39+
40+gradRasterTime = system.gradRasterTime;
41+
42+% round the time point to the gradient raster;
43+timeindex = round(timepoint / gradRasterTime);
44+if abs(timepoint-timeindex*gradRasterTime)>1e-6
45+ warning('splitting the gradint at a point that is not on a gradient raster edge, substantial rounding is applied');
46+end
47+timepoint = timeindex * gradRasterTime;
48+timeindex = timeindex + 1; % convert to Matlab convention
49+
50+ch = grad.channel;
51+
52+if strcmp(grad.type, 'grad')
53+ % check if we have an arbitrary gradient or an exended trapezoid
54+ if abs(grad.tt(1)-0.5*gradRasterTime)<1e-10
55+ % it can be an arbitrary gradient or arbitrary gradient with oversampling
56+ isArb=all(abs(grad.tt(2:end)-grad.tt(1:end-1)-gradRasterTime)<1e-10);
57+ isArbOs=all(abs(grad.tt(2:end)-grad.tt(1:end-1)-gradRasterTime*0.5)<1e-10);
58+ if isArb || isArbOs
59+ if isArbOs
60+ % update timeindex to account for dencier sampling
61+ timeindex = (timeindex-1)*2;
62+ end
63+ % arbitrary gradient -- the most trivial conversion
64+ % if timepoint is out of range we have nothing to do
65+ if timeindex == 1 || timeindex >= length(grad.tt)
66+ varargout{1} = grad;
67+ else
68+ grad1=grad;
69+ grad2=grad;
70+ if isArbOs
71+ grad1.last=grad.waveform(timeindex);
72+ else
73+ grad1.last=0.5*(grad.waveform(timeindex-1)+grad.waveform(timeindex)); % FIXME: retrive the double-sampling point (e.g. the corner of the trapezoid)
74+ end
75+ grad2.first=grad1.last;
76+ grad2.delay=grad.delay + timepoint;
77+ grad1.tt=grad.tt(1:(timeindex-1));
78+ grad1.waveform=grad.waveform(1:(timeindex-1));
79+ if isArbOs
80+ grad2.tt=grad.tt(timeindex+1:end) - timepoint;
81+ grad2.waveform=grad.waveform(timeindex+1:end);
82+ else
83+ grad2.tt=grad.tt(timeindex:end) - timepoint;
84+ grad2.waveform=grad.waveform(timeindex:end);
85+ end
86+ grad1.shape_dur = grad1.tt(end) - grad1.tt(1) + gradRasterTime;
87+ grad2.shape_dur = grad2.tt(end) - grad2.tt(1) + gradRasterTime;
88+
89+ if nargout==1
90+ varargout{1} = [grad1 grad2];
91+ else
92+ varargout{1} = grad1;
93+ varargout{2} = grad2;
94+ end
95+ end
96+ %figure; plot(grad.tt, grad.waveform); hold on; plot(grad1.tt, grad1.waveform); plot(grad2.delay+grad2.tt, grad2.waveform);
97+ return; % early return to protect the subsequent code
98+ end
99+ end
100+
101+ % we have an extended trapezoid (by excluding arbitrary grad) -- excellent choice!
102+ times = grad.tt';
103+ amplitudes = grad.waveform'; % QC: to match the matrix size for times1 and amplitudes1 below. 2025.01.02
104+
105+elseif strcmp(grad.type, 'trap')
106+ grad.delay = round(grad.delay /gradRasterTime)*gradRasterTime; % MZ: was ceil
107+ grad.riseTime = round(grad.riseTime/gradRasterTime)*gradRasterTime; % MZ: was ceil
108+ grad.flatTime = round(grad.flatTime/gradRasterTime)*gradRasterTime; % MZ: was ceil
109+ grad.fallTime = round(grad.fallTime/gradRasterTime)*gradRasterTime; % MZ: was ceil
110+
111+ % prepare the extended trapezoid structure
112+ if grad.flatTime == 0
113+ times = [0 grad.riseTime grad.riseTime+grad.fallTime];
114+ amplitudes = [0 grad.amplitude 0];
115+ else
116+ times = [0 grad.riseTime grad.riseTime+grad.flatTime grad.riseTime+grad.flatTime+grad.fallTime];
117+ amplitudes = [0 grad.amplitude grad.amplitude 0];
118+ end
119+else
120+ error('Splitting of unsupported event.');
121+end
122+
123+% if the cutline is behind the gradient there is no second gradient to create
124+if timepoint >= grad.delay+times(end)
125+ error('trying to place the splitting time point after the end of the gradient');
126+end
127+
128+% now we have everything in the extended trapezoid structure
129+
130+% if the cutline goes through the delay we need special treatment
131+if timepoint < grad.delay
132+ times=[0 grad.delay+times];
133+ amplitudes = [0 amplitudes];
134+ grad.delay=0;
135+else
136+ timepoint = timepoint - grad.delay;
137+end
138+
139+% sample at timepoint
140+amp_tp=interp1(times, amplitudes, timepoint, 'linear'); % MZ: interp1() is not OK here for the corner situation TODO: fixme! (e.g. by restoring the corners as done in waveforms_and_times())
141+% split the data
142+teps=1e-10; % we need this because of the rounding problems
143+times1 = [ times(times<timepoint-teps) timepoint ];
144+amplitudes1 = [ amplitudes(times<timepoint-teps) amp_tp ];
145+times2 = [ timepoint times(times>timepoint+teps) ] - timepoint;
146+amplitudes2 = [ amp_tp amplitudes(times>timepoint+teps) ];
147+
148+% recreate gradients
149+grad1 = mr.makeExtendedTrapezoid(ch, 'system', system, 'times', times1,...
150+ 'amplitudes', amplitudes1, ...
151+ 'skip_check', true);
152+grad1.delay = grad.delay;
153+grad2 = mr.makeExtendedTrapezoid(ch, 'system', system, 'times', times2,...
154+ 'amplitudes', amplitudes2, ...
155+ 'skip_check', true);
156+grad2.delay = timepoint + grad.delay;
157+
158+%grads = [grad1 grad2];
159+if nargout==1
160+ varargout{1} = [grad1 grad2];
161+else
162+ varargout{1} = grad1;
163+ varargout{2} = grad2;
164+end
165+
166+end
src/engine/pulseq/+mr/traj2grad.madded+78−0View file
@@ -0,0 +1,78 @@
1+function [g sr]=traj2grad(k,varargin)
2+%traj2grad Convert a k-space trajectory to gradient waveform.
3+% g=traj2grad(k) Convert k into gradient waveform g using finite
4+% differences. The trajectory is in units of 1/m. The k-space points are
5+% assumed to be sampled on the raster edges.
6+% The size of k = [nChannel nTime].
7+%
8+% g=traj2grad(k,'RasterTime',T) Calculate gradient waveforms assuming
9+% the given raster time.
10+%
11+% See also Sequence.makeArbitraryGrad
12+
13+persistent parser
14+if isempty(parser)
15+ parser = inputParser;
16+ parser.FunctionName = 'traj2grad';
17+ parser.addRequired('k',@isnumeric);
18+ parser.addParamValue('first',[],@isnumeric);
19+ parser.addParamValue('firstGradStepHalfRaster',true,@islogical);
20+ parser.addParamValue('conservativeSlewEstimate',false,@islogical);
21+ parser.addParamValue('system',[],@isstruct);
22+ parser.addParamValue('RasterTime',[],@isnumeric);
23+end
24+parse(parser,k,varargin{:});
25+opt = parser.Results;
26+if isempty(opt.system)
27+ opt.system=mr.opts();
28+end
29+if isempty(opt.RasterTime)
30+ opt.RasterTime=opt.system.gradRasterTime;
31+end
32+if isempty(opt.first)
33+ opt.first=zeros(size(k,1),1); % QC: if the first gradient point is not given, set it to zero. 2025.01.03
34+end
35+
36+% Compute finite difference for gradients in Hz/m
37+%g=([k(:,2:end)-k(:,1:end-1) zeros(size(k,1),1)])/opt.RasterTime; % MZ: with zero-padding
38+g=[(k(:,2:end)-k(:,1:end-1))/opt.RasterTime]; % MZ: no zero-padding!
39+
40+% Compute the slew rate (time derivative of the gradient)
41+sr0=(g-[opt.first g(:,1:end-1)])/opt.RasterTime;
42+if opt.firstGradStepHalfRaster
43+ sr0(:,1)=sr0(:,1)*2; % account for the half-step in the beginning of the shape
44+end
45+
46+% now we think how to post-process the results
47+% gradient is now sampled between the k-points (on raster cell centers)
48+% whilst the slew rate is between the gradient points, except of the first
49+% point, which relies on the opt.first value (and may be a bit off anyway,
50+% but this is the best estimate that we have)
51+sr=zeros(size(sr0));
52+sr(:,1)=sr0(:,1);
53+if (opt.conservativeSlewEstimate)
54+ if opt.firstGradStepHalfRaster
55+ sr(:,2)=sr0(:,2);
56+ sr(:,3:end)=max_abs(sr0(:,2:end-1),sr0(:,3:end));
57+ else
58+ sr(:,2:end)=max_abs(sr0(:,1:end-1),sr0(:,2:end));
59+ end
60+else
61+ if opt.firstGradStepHalfRaster
62+ sr(:,2)=sr0(:,2);
63+ sr(:,3:end)=0.5*(sr0(:,2:end-1)+sr0(:,3:end));
64+ else
65+ sr(:,2:end)=0.5*(sr0(:,1:end-1)+sr0(:,2:end));
66+ end
67+end
68+
69+end
70+
71+function out=max_abs(in1, in2)
72+ if size(in2)~=size(in2)
73+ error('arrays of incompatible sizes');
74+ end
75+ abs1gtoe=abs(in1)>=abs(in2);
76+ out=in1.*abs1gtoe+in2.*(~abs1gtoe);
77+end
78+
src/engine/pulseq/+mr/verifyFileSignature.madded+93−0View file
@@ -0,0 +1,93 @@
1+function [res, storedSignature, computedSignature] = verifyFileSignature(pulseqFile)
2+% verifyFileSignature - verify the signature of the provided Pulseq file.
3+% both text and binary Pulseq files are supported.
4+%
5+
6+ fid = fopen(pulseqFile, 'r');
7+ assert(fid>=0, 'Failed to open file: %s', pulseqFile);
8+ cleanupObj = onCleanup(@() fclose(fid));
9+
10+ % check whether this is a binary file
11+ binaryCodes = mr.Sequence.getBinaryCodes();
12+ magicNum = fread(fid,1,'uint64=>uint64');
13+ isBinary = magicNum==binaryCodes.fileHeader;
14+
15+ if isBinary
16+ [ok, sigType, storedSignature, signedLen] = readBinaryFileSignature(fid,binaryCodes);
17+ else
18+ [ok, sigType, storedSignature, signedLen] = readTextFileSignature(fid);
19+ % need to close and reopen the file to reset the 'text' state of the file handle (stupid Matlab)
20+ fclose(fid);
21+ fid = fopen(pulseqFile, 'r');
22+ end
23+
24+ if ~ok
25+ error('failed to read the signature');
26+ end
27+
28+ computedSignature = computeMd5OfPrefix(fid, signedLen);
29+ res=strcmp(computedSignature,storedSignature);
30+end
31+
32+function [ok, sigType, sigHex, signedLen] = readBinaryFileSignature(fid,binaryCodes)
33+ fseek(fid, -8, 'eof');
34+ signedLen = double(fread(fid, 1, 'int64'));
35+
36+ fseek(fid, signedLen, 'bof');
37+ sectionCode = int64(fread(fid, 1, 'int64'));
38+ assert(sectionCode==binaryCodes.section.signature);
39+
40+ typeLen = double(fread(fid, 1, 'int32'));
41+ sigType = char(fread(fid, typeLen, 'char')');
42+
43+ hashLen = double(fread(fid, 1, 'int32'));
44+ hashRaw = uint8(fread(fid, hashLen, 'uint8'));
45+ if isempty(hashRaw)
46+ sigHex = '';
47+ else
48+ sigHex = lower(reshape(dec2hex(hashRaw, 2)', 1, []));
49+ end
50+ ok = true; % more checks?
51+end
52+
53+function [ok, sigType, sigHex, signedLen] = readTextFileSignature(fid)
54+ sigType='';
55+ sigHex='';
56+ signedLen=-1;
57+ fseek(fid, 0, 'bof');
58+ fLeadingNewLinePos=-1;
59+ while ~feof(fid)
60+ fpos=ftell(fid);
61+ line = fgetl(fid);
62+ if signedLen<=0
63+ % still looking for the SIGNATURE marker
64+ if length(line)>=11 && strcmp('[SIGNATURE]',line(1:11))
65+ assert(fLeadingNewLinePos>0);
66+ signedLen = fLeadingNewLinePos;
67+ end
68+ if length(line)==0
69+ fLeadingNewLinePos=fpos;
70+ else
71+ fLeadingNewLinePos=-1;
72+ end
73+ else
74+ % reading the signature
75+ if length(line)<=5 || line(1)=='#'
76+ continue;
77+ end
78+ if strcmp('Type ',line(1:5))
79+ sigType=mr.aux.strstrip(line(6:end));
80+ end
81+ if strcmp('Hash ',line(1:5))
82+ sigHex=lower(mr.aux.strstrip(line(6:end)));
83+ end
84+ end
85+ end
86+ ok = ~isempty(sigType) && ~isempty(sigHex) && signedLen>0;
87+end
88+
89+function md5Hex = computeMd5OfPrefix(fid, signedLen)
90+ fseek(fid, 0, 'bof');
91+ payload = fread(fid, signedLen);%, 'uint8=>uint8');
92+ md5Hex = mr.aux.md5(payload);
93+end
src/engine/pulseq/LICENSEadded+22−0View file
@@ -0,0 +1,22 @@
1+The MIT License (MIT)
2+
3+Copyright (c) 2015, Kelvin Layton <kelvin.layton@uniklinik-freiburg.de>
4+ 2015, Maxim Zaitsev <maxim.zaitsev@uniklinik-freiburg.de>
5+
6+Permission is hereby granted, free of charge, to any person obtaining a copy
7+of this software and associated documentation files (the "Software"), to deal
8+in the Software without restriction, including without limitation the rights
9+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+copies of the Software, and to permit persons to whom the Software is
11+furnished to do so, subject to the following conditions:
12+
13+The above copyright notice and this permission notice shall be included in all
14+copies or substantial portions of the Software.
15+
16+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+SOFTWARE.
src/engine/pulseq/VENDORED.mdadded+10−0View file
@@ -0,0 +1,10 @@
1+# Vendored pulseq +mr package
2+
3+Copied from https://github.com/pulseq/pulseq (`matlab/+mr`, .m files only)
4+at version `v1.5.1-272-g2fd6ab6` by `scripts/vendor-pulseq.mjs`. Do not edit these
5+files here — update the pulseq checkout and re-run `npm run vendor-pulseq`.
6+
7+pulseq is MIT-licensed; see LICENSE in this directory.
8+
9+Note: at runtime the engine replaces `+mr/+aux/md5.m` (which needs Java on
10+MATLAB) with a wrapper over numbl's `hash` builtin — see src/engine/mrFiles.ts.
src/engine/runner.tsadded+124−0View file
@@ -0,0 +1,124 @@
1+// Run-per-execution pulseq runner: each run boots a fresh numbl/browser
2+// session (no mip, no persistence) with the vendored +mr tree and the user's
3+// script staged as main.m, executes it, then collects every .seq file the
4+// script wrote into the session cwd. Cancel = dispose the worker, which
5+// always works (no cross-origin isolation needed).
6+import { createNumblSession, type NumblSession } from 'numbl/browser'
7+import { mrBootFiles } from './mrFiles'
8+
9+export interface SeqOutputFile {
10+ name: string
11+ text: string
12+}
13+
14+export interface RunResult {
15+ ok: boolean
16+ /** True when the run was cancelled (or timed out) rather than finishing. */
17+ aborted?: boolean
18+ /** Concatenated console output (also streamed via onOutput). */
19+ output: string
20+ /** Formatted MATLAB error message when ok is false. */
21+ error?: string
22+ /** .seq files the script wrote, in directory order. */
23+ seqFiles: SeqOutputFile[]
24+ elapsedMs: number
25+}
26+
27+export interface RunHooks {
28+ /** Streaming console output from the script. */
29+ onOutput?: (text: string) => void
30+}
31+
32+export interface RunHandle {
33+ promise: Promise<RunResult>
34+ /** Stop the run by terminating the worker. The promise resolves aborted. */
35+ cancel(): void
36+}
37+
38+const RUN_TIMEOUT_MS = 600_000
39+
40+// Lists the .seq files the script produced, on output lines the runner can
41+// pick out. Runs in the same workspace after main.m, but the session is
42+// disposed right after, so the seqlab_* variables never collide with anything.
43+const COLLECT_SNIPPET = `seqlab_d = dir('*.seq');
44+for seqlab_i = 1:numel(seqlab_d)
45+ fprintf('SEQLAB_FILE:%s\\n', seqlab_d(seqlab_i).name);
46+end
47+`
48+
49+export function runScript(script: string, hooks: RunHooks = {}): RunHandle {
50+ let session: NumblSession | null = null
51+ let cancelled = false
52+ let timeoutId: ReturnType<typeof setTimeout> | undefined
53+
54+ const cancel = () => {
55+ cancelled = true
56+ session?.dispose()
57+ }
58+
59+ const promise = (async (): Promise<RunResult> => {
60+ const t0 = performance.now()
61+ let streaming = true
62+ const outputs: string[] = []
63+ timeoutId = setTimeout(cancel, RUN_TIMEOUT_MS)
64+ try {
65+ session = await createNumblSession({
66+ files: [...mrBootFiles(), { path: 'main.m', content: script }],
67+ mip: false,
68+ persistSystem: false,
69+ optimization: '1',
70+ onOutput: (text) => {
71+ if (!streaming) return
72+ outputs.push(text)
73+ hooks.onOutput?.(text)
74+ },
75+ })
76+ // A cancel during boot has no worker to terminate yet — honor it now.
77+ if (cancelled) throw new Error('cancelled')
78+ // Semicolon: execute uses REPL display semantics, and a bare `main`
79+ // would display a spurious `ans` after the script returns.
80+ const result = await session.execute('main;')
81+ streaming = false
82+
83+ // Collect .seq outputs even after an error — the script may have
84+ // written some before failing.
85+ const seqFiles: SeqOutputFile[] = []
86+ const collect = await session.execute(COLLECT_SNIPPET)
87+ if (collect.ok) {
88+ const names = [...collect.output.matchAll(/^SEQLAB_FILE:(.+)$/gm)].map((m) => m[1])
89+ const decoder = new TextDecoder()
90+ for (const name of names) {
91+ const bytes = await session.readFile(name)
92+ seqFiles.push({ name, text: decoder.decode(bytes) })
93+ }
94+ }
95+
96+ return {
97+ ok: result.ok,
98+ aborted: result.aborted,
99+ output: outputs.join(''),
100+ error: result.error,
101+ seqFiles,
102+ elapsedMs: performance.now() - t0,
103+ }
104+ } catch (err) {
105+ if (cancelled) {
106+ return {
107+ ok: false,
108+ aborted: true,
109+ output: outputs.join(''),
110+ error: 'Run cancelled',
111+ seqFiles: [],
112+ elapsedMs: performance.now() - t0,
113+ }
114+ }
115+ throw err
116+ } finally {
117+ clearTimeout(timeoutId)
118+ session?.dispose()
119+ session = null
120+ }
121+ })()
122+
123+ return { promise, cancel }
124+}
src/examples/ExamplesModal.tsxadded+60−0View file
@@ -0,0 +1,60 @@
1+// Categorized example gallery, shown as a modal. Groups examples by their
2+// catalog category and lets the user load one (which populates the editor and
3+// runs it).
4+import { useEffect } from 'react'
5+import { CATEGORY_ORDER, type Category } from './catalog.ts'
6+import type { Example } from './index.ts'
7+
8+export interface ExamplesModalProps {
9+ examples: Example[]
10+ onPick: (id: string) => void
11+ onClose: () => void
12+}
13+
14+export default function ExamplesModal({ examples, onPick, onClose }: ExamplesModalProps) {
15+ useEffect(() => {
16+ const onKey = (e: KeyboardEvent) => {
17+ if (e.key === 'Escape') onClose()
18+ }
19+ window.addEventListener('keydown', onKey)
20+ return () => window.removeEventListener('keydown', onKey)
21+ }, [onClose])
22+
23+ const byCategory = new Map<Category, Example[]>()
24+ for (const ex of examples) {
25+ const list = byCategory.get(ex.category) ?? []
26+ list.push(ex)
27+ byCategory.set(ex.category, list)
28+ }
29+
30+ return (
31+ <div className="modal-backdrop" onClick={onClose}>
32+ <div className="modal" onClick={(e) => e.stopPropagation()}>
33+ <div className="modal-head">
34+ <h2>Examples</h2>
35+ <span className="count">{examples.length} sequences from pulseq's demoSeq</span>
36+ <div style={{ flex: 1 }} />
37+ <button onClick={onClose}>Close ✕</button>
38+ </div>
39+ <div className="modal-body">
40+ {CATEGORY_ORDER.filter((c) => byCategory.has(c)).map((category) => (
41+ <section key={category}>
42+ <h3>{category}</h3>
43+ <div className="example-grid">
44+ {byCategory.get(category)!.map((ex) => (
45+ <button key={ex.id} className="example-card" onClick={() => onPick(ex.id)}>
46+ <div className="ex-top">
47+ <b>{ex.name}</b>
48+ <span className="ex-time">{ex.approx}</span>
49+ </div>
50+ <span className="ex-desc">{ex.description}</span>
51+ </button>
52+ ))}
53+ </div>
54+ </section>
55+ ))}
56+ </div>
57+ </div>
58+ </div>
59+ )
60+}
src/examples/catalog.tsadded+246−0View file
@@ -0,0 +1,246 @@
1+// Metadata for the pulseq demoSeq examples: display name, category, and a
2+// one-line description, keyed by demo id (the source .m basename without the
3+// leading "write" and without ".m"). Only ids that both appear here AND have
4+// an adapted source bundled (see index.ts) become gallery entries, so this
5+// can safely list demos that don't yet run on numbl.
6+
7+export type Category =
8+ | 'Basics'
9+ | 'Gradient echo'
10+ | 'Spin echo & TSE'
11+ | 'EPI'
12+ | 'Non-Cartesian'
13+ | '3D & structural'
14+ | 'Spectroscopy'
15+ | 'Labels & advanced'
16+
17+export interface CatalogEntry {
18+ name: string
19+ category: Category
20+ description: string
21+}
22+
23+export const CATEGORY_ORDER: Category[] = [
24+ 'Basics',
25+ 'Gradient echo',
26+ 'Spin echo & TSE',
27+ 'EPI',
28+ 'Non-Cartesian',
29+ '3D & structural',
30+ 'Spectroscopy',
31+ 'Labels & advanced',
32+]
33+
34+// Keyed by example id. Ids match the adapted source basenames in index.ts.
35+export const CATALOG: Record<string, CatalogEntry> = {
36+ fid: {
37+ name: 'FID',
38+ category: 'Basics',
39+ description: 'Free induction decay — hard pulse + long readout, the simplest sequence.',
40+ },
41+ gre: {
42+ name: 'Gradient echo (GRE)',
43+ category: 'Gradient echo',
44+ description: 'RF-spoiled 2D GRE, 128×128; optional autoLabel for on-scanner recon.',
45+ },
46+ epi: {
47+ name: 'EPI',
48+ category: 'EPI',
49+ description: 'Echo-planar imaging, 64×64, 3 slices — one readout train per excitation.',
50+ },
51+ GRE_live_demo_step0: {
52+ name: 'GRE (live demo, step 0)',
53+ category: 'Basics',
54+ description: 'Minimal gradient echo from the pulseq live tutorial — a gentle starting point.',
55+ },
56+ GRE_live_demo: {
57+ name: 'GRE (live demo)',
58+ category: 'Gradient echo',
59+ description: 'The full pulseq live-tutorial gradient echo with spoiling and timing.',
60+ },
61+ GradientEcho_label: {
62+ name: 'GRE with labels',
63+ category: 'Labels & advanced',
64+ description: 'Gradient echo using manual LIN/SLC labels (mr.makeLabel) for reconstruction.',
65+ },
66+ GradientEcho3D: {
67+ name: 'Gradient echo 3D',
68+ category: '3D & structural',
69+ description: '3D Cartesian gradient echo with two phase-encode directions.',
70+ },
71+ GradientEcho_grappa: {
72+ name: 'GRE (GRAPPA)',
73+ category: 'Labels & advanced',
74+ description: 'Gradient echo with GRAPPA parallel-imaging sampling and reference lines.',
75+ },
76+ GRE_multiEcho_label: {
77+ name: 'GRE multi-echo',
78+ category: 'Gradient echo',
79+ description: 'Multi-echo gradient echo with per-echo labels.',
80+ },
81+ GRE_rfShim: {
82+ name: 'GRE (RF shim)',
83+ category: 'Labels & advanced',
84+ description: 'Gradient echo demonstrating multi-channel RF shimming.',
85+ },
86+ CineGradientEcho: {
87+ name: 'Cine gradient echo',
88+ category: 'Gradient echo',
89+ description: 'Segmented cine gradient echo for cardiac-style time-resolved imaging.',
90+ },
91+ Trufi: {
92+ name: 'TrueFISP / bSSFP',
93+ category: 'Gradient echo',
94+ description: 'Balanced steady-state free precession (TrueFISP) sequence.',
95+ },
96+ RadialGradientEcho: {
97+ name: 'Radial GRE',
98+ category: 'Non-Cartesian',
99+ description: 'Radial gradient echo — spokes through k-space center.',
100+ },
101+ RadialGradientEcho_rotExt: {
102+ name: 'Radial GRE (rotation ext.)',
103+ category: 'Non-Cartesian',
104+ description: 'Radial GRE using the rotation extension to rotate spokes.',
105+ },
106+ FastRadialGradientEcho: {
107+ name: 'Fast radial GRE',
108+ category: 'Non-Cartesian',
109+ description: 'Fast radial gradient echo with continuous spoke acquisition.',
110+ },
111+ FastRadialGradientEcho_rot3D: {
112+ name: 'Fast radial GRE (3D rot.)',
113+ category: 'Non-Cartesian',
114+ description: 'Fast radial GRE with 3D rotations of the readout.',
115+ },
116+ FastRadialGradientEcho_rotExt: {
117+ name: 'Fast radial GRE (rot. ext.)',
118+ category: 'Non-Cartesian',
119+ description: 'Fast radial GRE driven by the rotation extension.',
120+ },
121+ Spiral: {
122+ name: 'Spiral',
123+ category: 'Non-Cartesian',
124+ description: 'Spiral readout trajectory generated from a k-space design.',
125+ },
126+ UTE: {
127+ name: 'UTE',
128+ category: 'Non-Cartesian',
129+ description: 'Ultra-short echo time radial acquisition with ramp sampling.',
130+ },
131+ UTE_rs: {
132+ name: 'UTE (ramp-sampled)',
133+ category: 'Non-Cartesian',
134+ description: 'UTE variant with ramp-sampled readouts.',
135+ },
136+ ZTE_Petra: {
137+ name: 'ZTE (PETRA)',
138+ category: 'Non-Cartesian',
139+ description: 'Zero echo time PETRA acquisition (radial + single-point center).',
140+ },
141+ ZTE_Petra_sodium: {
142+ name: 'ZTE PETRA (sodium)',
143+ category: 'Non-Cartesian',
144+ description: 'Sodium-tuned ZTE PETRA acquisition.',
145+ },
146+ EpiSpinEcho: {
147+ name: 'EPI spin echo',
148+ category: 'EPI',
149+ description: 'Spin-echo EPI — 180° refocusing before the readout train.',
150+ },
151+ EpiSpinEchoRS: {
152+ name: 'EPI spin echo (ramp-sampled)',
153+ category: 'EPI',
154+ description: 'Ramp-sampled spin-echo EPI for faster readouts.',
155+ },
156+ EpiRS: {
157+ name: 'EPI (ramp-sampled)',
158+ category: 'EPI',
159+ description: 'Gradient-echo EPI with ramp sampling.',
160+ },
161+ EpiRS_label: {
162+ name: 'EPI-RS with labels',
163+ category: 'EPI',
164+ description: 'Ramp-sampled EPI with reconstruction labels.',
165+ },
166+ EpiRS_label_softdelay: {
167+ name: 'EPI-RS (soft delay)',
168+ category: 'EPI',
169+ description: 'Ramp-sampled labelled EPI using soft delays for adjustable timing.',
170+ },
171+ EpiRS_label_trans: {
172+ name: 'EPI-RS (transformed)',
173+ category: 'EPI',
174+ description: 'Ramp-sampled labelled EPI with a coordinate transform applied.',
175+ },
176+ Epi_label: {
177+ name: 'EPI with labels',
178+ category: 'EPI',
179+ description: 'Basic EPI with reconstruction labels.',
180+ },
181+ EpiDiffusionRS: {
182+ name: 'EPI diffusion',
183+ category: 'EPI',
184+ description: 'Diffusion-weighted ramp-sampled EPI with diffusion-encoding gradients.',
185+ },
186+ EpiDiffusionRS_PMC: {
187+ name: 'EPI diffusion (PMC)',
188+ category: 'EPI',
189+ description: 'Diffusion EPI with prospective motion-correction hooks.',
190+ },
191+ TSE: {
192+ name: 'Turbo spin echo (TSE)',
193+ category: 'Spin echo & TSE',
194+ description: 'Turbo/fast spin echo with a train of refocusing pulses.',
195+ },
196+ TSEprop: {
197+ name: 'TSE PROPELLER',
198+ category: 'Spin echo & TSE',
199+ description: 'PROPELLER TSE — rotating blades of k-space.',
200+ },
201+ HASTE: {
202+ name: 'HASTE',
203+ category: 'Spin echo & TSE',
204+ description: 'Half-Fourier single-shot turbo spin echo.',
205+ },
206+ SpinEcho_softdelay: {
207+ name: 'Spin echo (soft delay)',
208+ category: 'Spin echo & TSE',
209+ description: 'Spin echo demonstrating adjustable soft delays.',
210+ },
211+ MPRAGE: {
212+ name: 'MPRAGE',
213+ category: '3D & structural',
214+ description: '3D T1-weighted MPRAGE with inversion preparation.',
215+ },
216+ MPRAGE_grappa: {
217+ name: 'MPRAGE (GRAPPA)',
218+ category: '3D & structural',
219+ description: 'MPRAGE with GRAPPA parallel-imaging undersampling.',
220+ },
221+ MPRAGE_4ge: {
222+ name: 'MPRAGE (4 gradient echoes)',
223+ category: '3D & structural',
224+ description: 'Multi-echo MPRAGE variant.',
225+ },
226+ PRESS: {
227+ name: 'PRESS',
228+ category: 'Spectroscopy',
229+ description: 'Point-resolved spectroscopy — single-voxel double spin echo.',
230+ },
231+ SemiLaser: {
232+ name: 'semi-LASER',
233+ category: 'Spectroscopy',
234+ description: 'Single-voxel spectroscopy with adiabatic refocusing (semi-LASER).',
235+ },
236+ SelectiveRf: {
237+ name: 'Selective RF',
238+ category: 'Labels & advanced',
239+ description: 'Spatially selective 2D RF excitation.',
240+ },
241+ demo_automatic_TRID_labels: {
242+ name: 'Automatic TRID labels',
243+ category: 'Labels & advanced',
244+ description: 'Demonstrates automatic TRID (timing) label assignment.',
245+ },
246+}
src/examples/epi.madded+66−0View file
@@ -0,0 +1,66 @@
1+% Echo-planar imaging (EPI), 64x64, 3 slices.
2+% A basic educational EPI without ramp sampling: one excitation per slice,
3+% then the whole k-space plane in a single train of alternating readouts
4+% with phase blips. Adapted from pulseq's demoSeq/writeEpi.m.
5+
6+seq = mr.Sequence(); % Create a new sequence object
7+fov = 220e-3; Nx = 64; Ny = 64; % Define FOV and resolution
8+thickness = 3e-3; % slice thickness
9+Nslices = 3;
10+
11+% Set system limits
12+lims = mr.opts('MaxGrad', 32, 'GradUnit', 'mT/m', ...
13+ 'MaxSlew', 130, 'SlewUnit', 'T/m/s', ...
14+ 'rfRingdownTime', 30e-6, 'rfDeadTime', 100e-6);
15+
16+% Create 90 degree slice selection pulse and gradient
17+[rf, gz] = mr.makeSincPulse(pi/2, 'system', lims, 'Duration', 3e-3, ...
18+ 'SliceThickness', thickness, 'apodization', 0.5, 'timeBwProduct', 4, ...
19+ 'use', 'excitation');
20+
21+% Define other gradients and ADC events
22+deltak = 1/fov;
23+kWidth = Nx*deltak;
24+dwellTime = 4e-6;
25+readoutTime = Nx*dwellTime;
26+flatTime = ceil(readoutTime*1e5)*1e-5; % round-up to the gradient raster
27+gx = mr.makeTrapezoid('x', lims, 'Amplitude', kWidth/readoutTime, 'FlatTime', flatTime);
28+adc = mr.makeAdc(Nx, 'Duration', readoutTime, ...
29+ 'Delay', gx.riseTime + flatTime/2 - (readoutTime - dwellTime)/2);
30+
31+% Pre-phasing gradients
32+preTime = 8e-4;
33+gxPre = mr.makeTrapezoid('x', lims, 'Area', -gx.area/2, 'Duration', preTime);
34+gzReph = mr.makeTrapezoid('z', lims, 'Area', -gz.area/2, 'Duration', preTime);
35+gyPre = mr.makeTrapezoid('y', lims, 'Area', -Ny/2*deltak, 'Duration', preTime);
36+
37+% Phase blip in shortest possible time
38+dur = ceil(2*sqrt(deltak/lims.maxSlew)/10e-6)*10e-6;
39+gy = mr.makeTrapezoid('y', lims, 'Area', deltak, 'Duration', dur);
40+
41+% Define sequence blocks
42+for s = 1:Nslices
43+ rf.freqOffset = gz.amplitude*thickness*(s-1-(Nslices-1)/2);
44+ seq.addBlock(rf, gz);
45+ seq.addBlock(gxPre, gyPre, gzReph);
46+ for i = 1:Ny
47+ seq.addBlock(gx, adc); % Read one line of k-space
48+ seq.addBlock(gy); % Phase blip
49+ gx.amplitude = -gx.amplitude; % Reverse polarity of read gradient
50+ end
51+end
52+
53+% check whether the timing of the sequence is correct
54+[ok, error_report] = seq.checkTiming;
55+if ok
56+ fprintf('Timing check passed successfully\n');
57+else
58+ fprintf('Timing check failed! Error listing follows:\n');
59+ fprintf([error_report{:}]);
60+ fprintf('\n');
61+end
62+
63+seq.setDefinition('FOV', [fov fov thickness]);
64+seq.setDefinition('Name', 'epi');
65+
66+seq.write('epi.seq');
src/examples/fid.madded+41−0View file
@@ -0,0 +1,41 @@
1+% Free induction decay (FID) — the simplest pulseq sequence.
2+% A non-selective excitation pulse followed by a long readout,
3+% repeated 16 times. Adapted from pulseq's demoSeq/writeFid.m.
4+
5+system = mr.opts('rfRingdownTime', 20e-6, 'rfDeadTime', 100e-6, ...
6+ 'adcDeadTime', 20e-6);
7+
8+seq = mr.Sequence(system);
9+Nx = 4096; % samples per readout
10+Nrep = 16; % repetitions
11+
12+% 90-degree block (hard) excitation pulse
13+rf = mr.makeBlockPulse(pi/2, 'Duration', 0.3e-3, 'system', system, ...
14+ 'use', 'excitation');
15+
16+% ADC: long readout to capture the full decay
17+adc = mr.makeAdc(Nx, 'Duration', 512e-3, 'system', system, ...
18+ 'delay', system.adcDeadTime);
19+delayTE = 20e-3;
20+delayTR = 5000e-3;
21+
22+assert(delayTE >= mr.calcDuration(rf));
23+assert(delayTR >= mr.calcDuration(adc));
24+
25+for i = 1:Nrep
26+ seq.addBlock(rf, mr.makeDelay(delayTE));
27+ seq.addBlock(adc, mr.makeDelay(delayTR));
28+end
29+
30+[ok, error_report] = seq.checkTiming();
31+if ok
32+ fprintf('Timing check passed successfully\n');
33+else
34+ fprintf('Timing check failed! Error listing follows:\n');
35+ for k = 1:numel(error_report)
36+ fprintf('%s\n', error_report{k});
37+ end
38+end
39+
40+seq.setDefinition('Name', 'fid');
41+seq.write('fid.seq');
src/examples/generated-timings.tsadded+16−0View file
@@ -0,0 +1,16 @@
1+// AUTO-GENERATED by scripts/gen-examples.mjs — do not edit.
2+// Rough in-browser run-time labels per example id.
3+export const TIMINGS: Record<string, string> = {
4+ "fid": "~1 s",
5+ "epi": "~2 s",
6+ "gre": "~5 s",
7+ "EpiRS": "~4 s",
8+ "EpiSpinEcho": "~2 s",
9+ "EpiSpinEchoRS": "~5 s",
10+ "FastRadialGradientEcho": "~15 s",
11+ "GRE_live_demo_step0": "~10 s",
12+ "GRE_live_demo": "~15 s",
13+ "RadialGradientEcho": "~20 s",
14+ "UTE_rs": "~30 s",
15+ "UTE": "~20 s"
16+}
src/examples/gre.madded+90−0View file
@@ -0,0 +1,90 @@
1+% 2D RF-spoiled gradient echo (GRE), 128x128, single slice.
2+% The classic pulseq demo: slice-selective sinc excitation, phase encoding,
3+% RF spoiling, and reconstruction labels via seq.autoLabel.
4+% Adapted from pulseq's demoSeq/writeGradientEcho.m.
5+
6+% set system limits
7+sys = mr.opts('MaxGrad', 22, 'GradUnit', 'mT/m', ...
8+ 'MaxSlew', 120, 'SlewUnit', 'T/m/s', ...
9+ 'rfRingdownTime', 20e-6, 'rfDeadTime', 100e-6, 'adcDeadTime', 10e-6);
10+
11+seq = mr.Sequence(sys); % Create a new sequence object
12+fov = 256e-3; Nx = 128; Ny = Nx; % Define FOV and resolution
13+alpha = 10; % flip angle
14+sliceThickness = 3e-3; % slice
15+TR = 12e-3; % repetition time TR
16+TE = 5e-3; % echo time TE
17+
18+rfSpoilingInc = 84; % RF spoiling increment
19+roDuration = 3.2e-3; % ADC duration
20+
21+% autoLabel adds on-scanner reconstruction labels (LIN/PAR) by recomputing
22+% the full k-space trajectory. It is NOT needed to generate or view the
23+% sequence and dominates the runtime (~25 s here vs ~2 s without), so it is
24+% off by default. Set to true to include the labels.
25+doAutoLabel = false;
26+
27+% Create alpha-degree slice selection pulse and gradient
28+[rf, gz] = mr.makeSincPulse(alpha*pi/180, sys, 'Duration', 3e-3, ...
29+ 'SliceThickness', sliceThickness, 'apodization', 0.42, 'timeBwProduct', 4, ...
30+ 'use', 'excitation');
31+
32+% Define other gradients and ADC events
33+deltak = 1/fov;
34+gx = mr.makeTrapezoid('x', sys, 'FlatArea', Nx*deltak, 'FlatTime', roDuration);
35+adc = mr.makeAdc(Nx, sys, 'Duration', gx.flatTime, 'Delay', gx.riseTime);
36+gxPre = mr.makeTrapezoid('x', sys, 'Area', -gx.area/2, 'Duration', 1e-3);
37+gzReph = mr.makeTrapezoid('z', sys, 'Area', -gz.area/2, 'Duration', 1e-3);
38+phaseAreas = ((0:Ny-1)-Ny/2)*deltak;
39+gyPre = mr.makeTrapezoid('y', sys, 'Area', max(abs(phaseAreas)), 'Duration', mr.calcDuration(gxPre));
40+peScales = phaseAreas/gyPre.area;
41+
42+% gradient spoiling
43+gxSpoil = mr.makeTrapezoid('x', sys, 'Area', 2*Nx*deltak);
44+gzSpoil = mr.makeTrapezoid('z', sys, 'Area', 4/sliceThickness);
45+
46+% Calculate timing
47+delayTE = ceil((TE - mr.calcDuration(gxPre) - gz.fallTime - gz.flatTime/2 ...
48+ - mr.calcDuration(gx)/2)/seq.gradRasterTime)*seq.gradRasterTime;
49+delayTR = ceil((TR - mr.calcDuration(gz) - mr.calcDuration(gxPre) ...
50+ - mr.calcDuration(gx) - delayTE)/seq.gradRasterTime)*seq.gradRasterTime;
51+assert(all(delayTE >= 0));
52+assert(all(delayTR >= mr.calcDuration(gxSpoil, gzSpoil)));
53+
54+rf_phase = 0;
55+rf_inc = 0;
56+
57+% Loop over phase encodes and define sequence blocks
58+for i = 1:Ny
59+ rf.phaseOffset = rf_phase/180*pi;
60+ adc.phaseOffset = rf_phase/180*pi;
61+ rf_inc = mod(rf_inc + rfSpoilingInc, 360.0);
62+ rf_phase = mod(rf_phase + rf_inc, 360.0);
63+ %
64+ seq.addBlock(rf, gz);
65+ seq.addBlock(gxPre, mr.scaleGrad(gyPre, peScales(i)), gzReph);
66+ seq.addBlock(mr.makeDelay(delayTE));
67+ seq.addBlock(gx, adc);
68+ seq.addBlock(mr.makeDelay(delayTR), gxSpoil, mr.scaleGrad(gyPre, -peScales(i)), gzSpoil)
69+end
70+
71+% check whether the timing of the sequence is correct
72+[ok, error_report] = seq.checkTiming;
73+if ok
74+ fprintf('Timing check passed successfully\n');
75+else
76+ fprintf('Timing check failed! Error listing follows:\n');
77+ fprintf([error_report{:}]);
78+ fprintf('\n');
79+end
80+
81+% add data labels to make image reconstruction on the scanner possible
82+if doAutoLabel
83+ seq.autoLabel('mirrorFourier', true, 'sortSlices', 'descending');
84+end
85+
86+% prepare sequence export
87+seq.setDefinition('FOV', [fov fov sliceThickness]);
88+seq.setDefinition('Name', 'gre');
89+
90+seq.write('gre.seq') % Write to pulseq file
src/examples/index.tsadded+65−0View file
@@ -0,0 +1,65 @@
1+// Example registry. Sources come from two places:
2+// - curated hand-written scripts (fid/epi/gre) with friendlier comments
3+// - auto-adapted pulseq demoSeq scripts under ./pulseq/*.m, generated by
4+// scripts/gen-examples.mjs (only demos that actually run on numbl)
5+// Metadata (name/category/description) is looked up in CATALOG; run-time
6+// labels come from generated-timings.ts. An id without a CATALOG entry is
7+// skipped, so stale generated files can't leak in unlabeled.
8+import { CATALOG, type Category } from './catalog.ts'
9+import { TIMINGS } from './generated-timings.ts'
10+import fidSource from './fid.m?raw'
11+import epiSource from './epi.m?raw'
12+import greSource from './gre.m?raw'
13+
14+export interface Example {
15+ id: string
16+ name: string
17+ category: Category
18+ description: string
19+ source: string
20+ /** Rough in-browser run time, e.g. "~2 s". */
21+ approx: string
22+ /** True for the hand-written curated scripts. */
23+ curated: boolean
24+}
25+
26+const curated: Record<string, string> = {
27+ fid: fidSource,
28+ epi: epiSource,
29+ gre: greSource,
30+}
31+
32+const generatedModules = import.meta.glob('./pulseq/*.m', {
33+ query: '?raw',
34+ import: 'default',
35+ eager: true,
36+}) as Record<string, string>
37+
38+const generated: Record<string, string> = {}
39+for (const [path, source] of Object.entries(generatedModules)) {
40+ const id = path.replace(/^\.\/pulseq\//, '').replace(/\.m$/, '')
41+ if (!(id in curated)) generated[id] = source
42+}
43+
44+function build(id: string, source: string, curatedFlag: boolean): Example | null {
45+ const meta = CATALOG[id]
46+ if (!meta) return null
47+ return {
48+ id,
49+ name: meta.name,
50+ category: meta.category,
51+ description: meta.description,
52+ source,
53+ approx: TIMINGS[id] ?? '',
54+ curated: curatedFlag,
55+ }
56+}
57+
58+export const EXAMPLES: Example[] = [
59+ ...Object.entries(curated).map(([id, src]) => build(id, src, true)),
60+ ...Object.entries(generated).map(([id, src]) => build(id, src, false)),
61+].filter((e): e is Example => e !== null)
62+
63+export function findExample(id: string): Example | undefined {
64+ return EXAMPLES.find((e) => e.id === id)
65+}
src/examples/pulseq/EpiRS.madded+166−0View file
@@ -0,0 +1,166 @@
1+% this is an experimental high-performance EPI sequence
2+% which uses split gradients to overlap blips with the readout
3+% gradients combined with ramp-samping
4+
5+% Set system limits
6+sys = mr.opts('MaxGrad',32,'GradUnit','mT/m',...
7+ 'MaxSlew',130,'SlewUnit','T/m/s',...
8+ 'rfRingdownTime', 30e-6, 'rfDeadtime', 100e-6,...
9+ 'adcDeadTime', 10e-6, 'B0', 2.89, ... % this is Siemens' 3T
10+ 'setAsDefault', true ... % new way of handing over the system parameters implicitly
11+);
12+
13+seq=mr.Sequence(sys); % Create a new sequence object
14+fov=256e-3; Nx=64; Ny=Nx; % Define FOV and resolution
15+thickness=4e-3; % slice thinckness in mm
16+sliceGap=1e-3; % slice gap im mm
17+Nslices=1;
18+
19+pe_enable=1; % a flag to quickly disable phase encoding (1/0) as needed for the delay calibration
20+ro_os=1; % oversampling factor (in contrast to the product sequence we don't really need it)
21+readoutTime=4.2e-4; % this controls the readout bandwidth
22+partFourierFactor=1; % partial Fourier factor: 1: full sampling 0: start with ky=0
23+
24+% Create fat-sat pulse
25+sat_ppm=-3.45;
26+rf_fs = mr.makeGaussPulse(110*pi/180,'system',sys,'Duration',8e-3,...
27+ 'bandwidth',abs(sat_ppm*1e-6*sys.B0*sys.gamma),'freqPPM',sat_ppm,'use','saturation');
28+rf_fs.phasePPM=-2*pi*rf_fs.freqPPM*rf_fs.center; % compensate for the frequency-offset induced phase
29+rf_fs.name='fat-sat'; % useful for debugging, can be seen in seq.plot
30+gz_fs = mr.makeTrapezoid('z','delay',mr.calcDuration(rf_fs),'Area',0.1/1e-4); % spoil up to 0.1mm
31+% Create 90 degree slice selection pulse and gradient
32+[rf, gz, gzReph] = mr.makeSincPulse(pi/2,'Duration',2e-3,...
33+ 'SliceThickness',thickness,'apodization',0.42,'timeBwProduct',4,'use','excitation');
34+rf.name='rf90'; % useful for debugging, can be seen in seq.plot
35+
36+% define the output trigger to play out with every slice excitatuion
37+trig=mr.makeDigitalOutputPulse('osc0','duration', 100e-6); % possible channels: 'osc0','osc1','ext1'
38+
39+% Define other gradients and ADC events
40+deltak=1/fov;
41+kWidth = Nx*deltak;
42+
43+% Phase blip in shortest possible time
44+blip_dur = ceil(2*sqrt(deltak/sys.maxSlew)/10e-6/2)*10e-6*2; % we round-up the duration to 2x the gradient raster time
45+% the split code below fails if this really makes a trpezoid instead of a triangle...
46+gy = mr.makeTrapezoid('y','Area',-deltak,'Duration',blip_dur); % we use negative blips to save one k-space line on our way towards the k-space center
47+%gy = mr.makeTrapezoid('y',lims,'amplitude',deltak/blip_dur*2,'riseTime',blip_dur/2, 'flatTime', 0);
48+
49+% readout gradient is a truncated trapezoid with dead times at the beginnig
50+% and at the end each equal to a half of blip_dur
51+% the area between the blips should be defined by kWidth
52+% we do a two-step calculation: we first increase the area assuming maximum
53+% slewrate and then scale down the amlitude to fix the area
54+extra_area=blip_dur/2*blip_dur/2*sys.maxSlew; % check unit!;
55+gx = mr.makeTrapezoid('x','Area',kWidth+extra_area,'duration',readoutTime+blip_dur);
56+actual_area=gx.area-gx.amplitude/gx.riseTime*blip_dur/2*blip_dur/2/2-gx.amplitude/gx.fallTime*blip_dur/2*blip_dur/2/2;
57+gx.amplitude=gx.amplitude/actual_area*kWidth;
58+gx.area = gx.amplitude*(gx.flatTime + gx.riseTime/2 + gx.fallTime/2);
59+gx.flatArea = gx.amplitude*gx.flatTime;
60+gx.name='Gro'; % useful for debugging, can be seen in seq.plot
61+
62+% calculate ADC
63+% we use ramp sampling, so we have to calculate the dwell time and the
64+% number of samples, which are will be qite different from Nx and
65+% readoutTime/Nx, respectively.
66+adcDwellNyquist=deltak/gx.amplitude/ro_os;
67+% round-down dwell time to 100 ns
68+adcDwell=floor(adcDwellNyquist*1e7)*1e-7;
69+adcSamples=floor(readoutTime/adcDwell/4)*4; % on Siemens the number of ADC samples need to be divisible by 4
70+% MZ: no idea, whether ceil,round or floor is better for the adcSamples...
71+adc = mr.makeAdc(adcSamples,'Dwell',adcDwell,'Delay',blip_dur/2);
72+% realign the ADC with respect to the gradient
73+time_to_center=adc.dwell*((adcSamples-1)/2+0.5); % I've been told that Siemens samples in the center of the dwell period
74+adc.delay=round((gx.riseTime+gx.flatTime/2-time_to_center)*1e6)*1e-6; % we adjust the delay to align the trajectory with the gradient. We have to aligh the delay to 1us
75+% this rounding actually makes the sampling points on odd and even readouts
76+% to appear misalligned. However, on the real hardware this misalignment is
77+% much stronger anyways due to the grdient delays
78+
79+% FOV positioning requires alignment to grad. raster... -> TODO
80+
81+% split the blip into two halves and produce a combined synthetic gradient
82+gy_parts = mr.splitGradientAt(gy, blip_dur/2, sys);
83+[gy_blipup, gy_blipdown,~]=mr.align('right',gy_parts(1),'left',gy_parts(2),gx);
84+gy_blipdownup=mr.addGradients({gy_blipdown, gy_blipup}, sys);
85+
86+% pe_enable support
87+gy_blipup.waveform=gy_blipup.waveform*pe_enable;
88+gy_blipdown.waveform=gy_blipdown.waveform*pe_enable;
89+gy_blipdownup.waveform=gy_blipdownup.waveform*pe_enable;
90+
91+% phase encoding and partial Fourier
92+
93+Ny_pre=round(partFourierFactor*Ny/2-1); % PE steps prior to ky=0, excluding the central line
94+Ny_post=round(Ny/2+1); % PE lines after the k-space center including the central line
95+Ny_meas=Ny_pre+Ny_post;
96+
97+% Pre-phasing gradients
98+gxPre = mr.makeTrapezoid('x','Area',-gx.area/2);
99+gyPre = mr.makeTrapezoid('y','Area',Ny_pre*deltak);
100+[gxPre,gyPre,gzReph]=mr.align('right',gxPre,'left',gyPre,gzReph);
101+% relax the PE prepahser to reduce stimulation
102+gyPre = mr.makeTrapezoid('y','Area',gyPre.area,'Duration',mr.calcDuration(gxPre,gyPre,gzReph));
103+gyPre.amplitude=gyPre.amplitude*pe_enable;
104+
105+% slice positions
106+slicePositions=(thickness+sliceGap)*((0:(Nslices-1)) - (Nslices-1)/2);
107+slicePositions=slicePositions([1:2:Nslices 2:2:Nslices]); % reorder slices for an interleaved acquisition (optional)
108+
109+% Define sequence blocks
110+%seq.addBlock(mr.makeDelay(1)); % older scanners like Trio may need this
111+ % dummy delay to keep up with timing
112+for s=1:Nslices
113+ seq.addBlock(rf_fs,gz_fs);
114+ rf.freqOffset=gz.amplitude*slicePositions(s);
115+ rf.phaseOffset=-2*pi*rf.freqOffset*mr.calcRfCenter(rf); % compensate for the slice-offset induced phase
116+ seq.addBlock(rf,gz,trig);
117+ seq.addBlock(gxPre,gyPre,gzReph);
118+ for i=1:Ny_meas
119+ if i==1
120+ seq.addBlock(gx,gy_blipup,adc); % Read the first line of k-space with a single half-blip at the end
121+ elseif i==Ny_meas
122+ seq.addBlock(gx,gy_blipdown,adc); % Read the last line of k-space with a single half-blip at the beginning
123+ else
124+ seq.addBlock(gx,gy_blipdownup,adc); % Read an intermediate line of k-space with a half-blip at the beginning and a half-blip at the end
125+ end
126+ gx.amplitude = -gx.amplitude; % Reverse polarity of read gradient
127+ end
128+end
129+
130+%% check whether the timing of the sequence is correct
131+[ok, error_report]=seq.checkTiming;
132+
133+if (ok)
134+ fprintf('Timing check passed successfully\n');
135+else
136+ fprintf('Timing check failed! Error listing follows:\n');
137+ fprintf([error_report{:}]);
138+ fprintf('\n');
139+end
140+
141+%% do some visualizations
142+
143+
144+
145+rf.freqOffset=0;
146+rf.phaseOffset=0;
147+[rf_bw,rf_f0,rf_spectrum,rf_w]=mr.calcRfBandwidth(rf);
148+xlim(3*[-rf_bw rf_bw]);
149+
150+%% trajectory calculation
151+
152+% plot k-spaces
153+
154+
155+%axis off;
156+
157+%% prepare the sequence output for the scanner
158+seq.setDefinition('Name', 'epi');
159+seq.setDefinition('FOV', [fov fov max(slicePositions)-min(slicePositions)+thickness]);
160+seq.setDefinition('ReceiverGainHigh',1);
161+% the following definitions only have effect in conjunction with LABELs
162+%seq.setDefinition('SlicePositions', slicePositions);
163+%seq.setDefinition('SliceThickness', thickness);
164+%seq.setDefinition('SliceGap', sliceGap);
165+
166+seq.write('epi_rs.seq');
src/examples/pulseq/EpiSpinEcho.madded+77−0View file
@@ -0,0 +1,77 @@
1+% Define FOV and resolution
2+fov=256e-3; Nx=64; Ny=64;
3+thickness=3e-3;
4+% Set system limits
5+lims = mr.opts('MaxGrad', 32, 'GradUnit', 'mT/m',...
6+ 'MaxSlew', 130, 'SlewUnit', 'T/m/s', 'rfRingdownTime', 20e-6, ...
7+ 'rfDeadTime', 100e-6, 'adcDeadTime', 20e-6);
8+% Create a new sequence object
9+seq=mr.Sequence(lims);
10+
11+% Create 90 degree slice selection pulse and gradient
12+[rf, gz] = mr.makeSincPulse(pi/2,lims,'Duration',3e-3,...
13+ 'SliceThickness',thickness,'apodization',0.5,'timeBwProduct',4,...
14+ 'use','excitation');
15+
16+% Define other gradients and ADC events
17+deltak=1/fov;
18+kWidth = Nx*deltak;
19+readoutTime = 3.2e-4;
20+gx = mr.makeTrapezoid('x',lims,'FlatArea',kWidth,'FlatTime',readoutTime);
21+adc = mr.makeAdc(Nx,lims,'Duration',gx.flatTime,'Delay',gx.riseTime);
22+
23+% Pre-phasing gradients
24+preTime=8e-4;
25+%gxPre = mr.makeTrapezoid('x',lims,'Area',-gx.area/2-deltak/2,'Duration',preTime);
26+gzReph = mr.makeTrapezoid('z',lims,'Area',-gz.area/2,'Duration',preTime);
27+%gyPre = mr.makeTrapezoid('y',lims,'Area',-Ny/2*deltak,'Duration',preTime);
28+% we need no minus for in-plane prephasers because of the spin-echo (position reflection in k-space)
29+gxPre = mr.makeTrapezoid('x',lims,'Area',gx.area/2-deltak/2,'Duration',preTime);
30+gyPre = mr.makeTrapezoid('y',lims,'Area',Ny/2*deltak,'Duration',preTime);
31+
32+% Phase blip in shortest possible time
33+dur = ceil(2*sqrt(deltak/lims.maxSlew)/10e-6)*10e-6;
34+gy = mr.makeTrapezoid('y',lims,'Area',deltak,'Duration',dur);
35+
36+% Refocusing pulse with spoiling gradients
37+rf180 = mr.makeBlockPulse(pi,lims,'Duration',500e-6,'use','refocusing');
38+gzSpoil = mr.makeTrapezoid('z',lims,'Area',gz.area*2,'Duration',3*preTime);
39+
40+% Calculate delay time %% MZ: I thisk this is very wrong!
41+TE=60e-3;
42+durationToCenter = (Nx/2+0.5)*mr.calcDuration(gx) + Ny/2*mr.calcDuration(gy);
43+rfCenterInclDelay=rf.delay + mr.calcRfCenter(rf);
44+rf180centerInclDelay=rf180.delay + mr.calcRfCenter(rf180);
45+delayTE1=TE/2 - mr.calcDuration(gz) + rfCenterInclDelay - preTime - mr.calcDuration(gzSpoil) - rf180centerInclDelay;
46+delayTE2=TE/2 - mr.calcDuration(rf180) + rf180centerInclDelay - mr.calcDuration(gzSpoil) - durationToCenter;
47+
48+% Define sequence blocks
49+seq.addBlock(rf,gz);
50+seq.addBlock(gxPre,gyPre,gzReph);
51+seq.addBlock(mr.makeDelay(delayTE1));
52+seq.addBlock(gzSpoil);
53+seq.addBlock(rf180);
54+seq.addBlock(gzSpoil);
55+seq.addBlock(mr.makeDelay(delayTE2));
56+for i=1:Ny
57+ seq.addBlock(gx,adc); % Read one line of k-space
58+ seq.addBlock(gy); % Phase blip
59+ gx.amplitude = -gx.amplitude; % Reverse polarity of read gradient
60+end
61+seq.addBlock(mr.makeDelay(1e-4));
62+
63+%% check whether the timing of the sequence is correct
64+[ok, error_report]=seq.checkTiming;
65+
66+if (ok)
67+ fprintf('Timing check passed successfully\n');
68+else
69+ fprintf('Timing check failed! Error listing follows:\n');
70+ fprintf([error_report{:}]);
71+ fprintf('\n');
72+end
73+
74+%% export and visualization
75+seq.setDefinition('FOV', [fov fov thickness]);
76+seq.setDefinition('Name', 'epise');
77+seq.write('epi_se.seq'); % Output sequence for scanner
src/examples/pulseq/EpiSpinEchoRS.madded+187−0View file
@@ -0,0 +1,187 @@
1+% this is an experimental high-performance EPI sequence
2+% which uses split gradients to overlap blips with the readout
3+% gradients combined with ramp-samping
4+
5+seq=mr.Sequence(); % Create a new sequence object
6+fov=250e-3; Nx=64; Ny=64; % Define FOV and resolution
7+thickness=3e-3; % slice thinckness
8+Nslices=3;
9+TE=40e-3;
10+
11+pe_enable=1; % a flag to quickly disable phase encoding (1/0) as needed for the delay calibration
12+ro_os=1; % oversampling factor (in contrast to the product sequence we don't really need it)
13+readoutTime=4.2e-4; % this controls the readout bandwidth
14+partFourierFactor=0.75; % partial Fourier factor: 1: full sampling 0: start with ky=0
15+
16+tRFex=2e-3;
17+tRFref=2e-3;
18+spoilFactor=1.5; % spoiling gradient around the pi-pulse
19+
20+% Set system limits
21+lims = mr.opts('MaxGrad',32,'GradUnit','mT/m',...
22+ 'MaxSlew',130,'SlewUnit','T/m/s',...
23+ 'rfRingdownTime', 30e-6, 'rfDeadtime', 100e-6, 'adcDeadTime', 10e-6);
24+
25+% Create fat-sat pulse
26+B0=2.89; % 1.5 2.89 3.0
27+sat_ppm=-3.45;
28+sat_freq=sat_ppm*1e-6*B0*lims.gamma;
29+rf_fs = mr.makeGaussPulse(110*pi/180,'system',lims,'Duration',8e-3,...
30+ 'bandwidth',abs(sat_freq),'freqOffset',sat_freq, 'use', 'saturation');
31+rf_fs.phaseOffset=-2*pi*rf_fs.freqOffset*mr.calcRfCenter(rf_fs); % compensate for the frequency-offset induced phase
32+gz_fs = mr.makeTrapezoid('z',lims,'delay',mr.calcDuration(rf_fs),'Area',1/1e-4); % spoil up to 0.1mm
33+
34+% Create 90 degree slice selection pulse and gradient
35+[rf, gz, gzReph] = mr.makeSincPulse(pi/2,'system',lims,'Duration',tRFex,...
36+ 'SliceThickness',thickness,'apodization',0.5,'timeBwProduct',4, 'use','excitation');
37+
38+% Create 90 degree slice refocusing pulse and gradients
39+[rf180, gz180] = mr.makeSincPulse(pi,'system',lims,'Duration',tRFref,...
40+ 'SliceThickness',thickness,'apodization',0.5,'timeBwProduct',4,'PhaseOffset',pi/2,'use','refocusing');
41+% [~, gzr_t, gzr_a]=mr.makeExtendedTrapezoidArea('z',gz180.amplitude,0,-gzReph.area+0.5*gz180.amplitude*gz180.fallTime,lims);
42+% gz180n=mr.makeExtendedTrapezoid('z','system',lims,'times',[0 gz180.riseTime gz180.riseTime+gz180.flatTime+gzr_t]+gz180.delay, 'amplitudes', [0 gz180.amplitude gzr_a]);
43+[~, gzr1_t, gzr1_a]=mr.makeExtendedTrapezoidArea('z',0,gz180.amplitude,spoilFactor*gz.area,lims);
44+[~, gzr2_t, gzr2_a]=mr.makeExtendedTrapezoidArea('z',gz180.amplitude,0,-gzReph.area+spoilFactor*gz.area,lims);
45+if gz180.delay>(gzr1_t(4)-gz180.riseTime)
46+ gz180.delay=gz180.delay-(gzr1_t(4)-gz180.riseTime);
47+else
48+ rf180.delay=rf180.delay+(gzr1_t(4)-gz180.riseTime)-gz180.delay;
49+ gz180.delay=0;
50+end
51+gz180n=mr.makeExtendedTrapezoid('z','system',lims,'times',[gzr1_t gzr1_t(4)+gz180.flatTime+gzr2_t]+gz180.delay, 'amplitudes', [gzr1_a gzr2_a]);
52+
53+
54+% define the output trigger to play out with every slice excitatuion
55+trig=mr.makeDigitalOutputPulse('osc0','duration', 100e-6); % possible channels: 'osc0','osc1','ext1'
56+
57+% Define other gradients and ADC events
58+deltak=1/fov;
59+kWidth = Nx*deltak;
60+
61+% Phase blip in shortest possible time
62+blip_dur = ceil(2*sqrt(deltak/lims.maxSlew)/10e-6/2)*10e-6*2; % we round-up the duration to 2x the gradient raster time
63+% the split code below fails if this really makes a trpezoid instead of a triangle...
64+gy = mr.makeTrapezoid('y',lims,'Area',-deltak,'Duration',blip_dur); % we use negative blips to save one k-space line on our way towards the k-space center
65+%gy = mr.makeTrapezoid('y',lims,'amplitude',deltak/blip_dur*2,'riseTime',blip_dur/2, 'flatTime', 0);
66+
67+% readout gradient is a truncated trapezoid with dead times at the beginnig
68+% and at the end each equal to a half of blip_dur
69+% the area between the blips should be defined by kWidth
70+% we do a two-step calculation: we first increase the area assuming maximum
71+% slewrate and then scale down the amlitude to fix the area
72+extra_area=blip_dur/2*blip_dur/2*lims.maxSlew; % check unit!;
73+gx = mr.makeTrapezoid('x',lims,'Area',kWidth+extra_area,'duration',readoutTime+blip_dur);
74+actual_area=gx.area-gx.amplitude/gx.riseTime*blip_dur/2*blip_dur/2/2-gx.amplitude/gx.fallTime*blip_dur/2*blip_dur/2/2;
75+gx.amplitude=gx.amplitude/actual_area*kWidth;
76+gx.area = gx.amplitude*(gx.flatTime + gx.riseTime/2 + gx.fallTime/2);
77+gx.flatArea = gx.amplitude*gx.flatTime;
78+
79+% calculate ADC
80+% we use ramp sampling, so we have to calculate the dwell time and the
81+% number of samples, which are will be qite different from Nx and
82+% readoutTime/Nx, respectively.
83+adcDwellNyquist=deltak/gx.amplitude/ro_os;
84+% round-down dwell time to 100 ns
85+adcDwell=floor(adcDwellNyquist*1e7)*1e-7;
86+adcSamples=floor(readoutTime/adcDwell/4)*4; % on Siemens the number of ADC samples need to be divisible by 4
87+% MZ: no idea, whether ceil,round or floor is better for the adcSamples...
88+adc = mr.makeAdc(adcSamples,'Dwell',adcDwell,'Delay',blip_dur/2);
89+% realign the ADC with respect to the gradient
90+time_to_center=adc.dwell*((adcSamples-1)/2+0.5); % I've been told that Siemens samples in the center of the dwell period
91+adc.delay=round((gx.riseTime+gx.flatTime/2-time_to_center)*1e6)*1e-6; % we adjust the delay to align the trajectory with the gradient. We have to aligh the delay to 1us
92+% this rounding actually makes the sampling points on odd and even readouts
93+% to appear misalligned. However, on the real hardware this misalignment is
94+% much stronger anyways due to the grdient delays
95+
96+% FOV positioning requires alignment to grad. raster... -> TODO
97+
98+% split the blip into two halves and produnce a combined synthetic gradient
99+gy_parts = mr.splitGradientAt(gy, blip_dur/2, lims);
100+[gy_blipup, gy_blipdown, ~]=mr.align('right',gy_parts(1),'left',gy_parts(2),gx);
101+gy_blipdownup=mr.addGradients({gy_blipdown, gy_blipup}, lims);
102+
103+% pe_enable support
104+gy_blipup.waveform=gy_blipup.waveform*pe_enable;
105+gy_blipdown.waveform=gy_blipdown.waveform*pe_enable;
106+gy_blipdownup.waveform=gy_blipdownup.waveform*pe_enable;
107+
108+% phase encoding and partial Fourier
109+
110+Ny_pre=round(partFourierFactor*Ny/2-1); % PE steps prior to ky=0, excluding the central line
111+Ny_post=round(Ny/2+1); % PE lines after the k-space center including the central line
112+Ny_meas=Ny_pre+Ny_post;
113+
114+% Pre-phasing gradients
115+gxPre = mr.makeTrapezoid('x',lims,'Area',-gx.area/2);
116+gyPre = mr.makeTrapezoid('y',lims,'Area',Ny_pre*deltak);
117+[gxPre,gyPre]=mr.align('right',gxPre,'left',gyPre);
118+% relax the PE prepahser to reduce stimulation
119+gyPre = mr.makeTrapezoid('y',lims,'Area',gyPre.area,'Duration',mr.calcDuration(gxPre,gyPre));
120+gyPre.amplitude=gyPre.amplitude*pe_enable;
121+
122+% Calculate delay times
123+durationToCenter = (Ny_pre+0.5)*mr.calcDuration(gx);
124+rfCenterInclDelay=rf.delay + mr.calcRfCenter(rf);
125+rf180centerInclDelay=rf180.delay + mr.calcRfCenter(rf180);
126+delayTE1=ceil((TE/2 - mr.calcDuration(rf,gz) + rfCenterInclDelay - rf180centerInclDelay)/lims.gradRasterTime)*lims.gradRasterTime;
127+delayTE2=ceil((TE/2 - mr.calcDuration(rf180,gz180n) + rf180centerInclDelay - durationToCenter)/lims.gradRasterTime)*lims.gradRasterTime;
128+assert(delayTE1>=0);
129+%assert(delayTE2>=0);
130+% now we merge slice refocusing, TE delay and pre-phasers into a single
131+% block
132+delayTE2=delayTE2+mr.calcDuration(rf180,gz180n);
133+gxPre.delay=0;
134+gxPre.delay=delayTE2-mr.calcDuration(gxPre);
135+assert(gxPre.delay>=mr.calcDuration(rf180)); % gxPre may not overlap with the RF
136+gyPre.delay=mr.calcDuration(rf180);
137+assert(mr.calcDuration(gyPre)<=mr.calcDuration(gxPre)); % gyPre may not shift the timing
138+
139+% Define sequence blocks
140+
141+%seq.addBlock(mr.makeDelay(1)); % older scanners like Trio may need this
142+ % dummy delay to keep up with timing
143+
144+for s=1:Nslices
145+ seq.addBlock(rf_fs,gz_fs);
146+ rf.freqOffset=gz.amplitude*thickness*(s-1-(Nslices-1)/2);
147+ rf.phaseOffset=-2*pi*rf.freqOffset*mr.calcRfCenter(rf); % compensate for the slice-offset induced phase
148+ rf180.freqOffset=gz180.amplitude*thickness*(s-1-(Nslices-1)/2);
149+ rf180.phaseOffset=pi/2-2*pi*rf180.freqOffset*mr.calcRfCenter(rf180); % compensate for the slice-offset induced phase
150+ seq.addBlock(rf,gz,trig);
151+ seq.addBlock(mr.makeDelay(delayTE1));
152+ seq.addBlock(rf180,gz180n,mr.makeDelay(delayTE2),gxPre,gyPre);
153+ for i=1:Ny_meas
154+ if i==1
155+ seq.addBlock(gx,gy_blipup,adc); % Read the first line of k-space with a single half-blip at the end
156+ elseif i==Ny_meas
157+ seq.addBlock(gx,gy_blipdown,adc); % Read the last line of k-space with a single half-blip at the beginning
158+ else
159+ seq.addBlock(gx,gy_blipdownup,adc); % Read an intermediate line of k-space with a half-blip at the beginning and a half-blip at the end
160+ end
161+ gx.amplitude = -gx.amplitude; % Reverse polarity of read gradient
162+ end
163+end
164+
165+%% check whether the timing of the sequence is correct
166+[ok, error_report]=seq.checkTiming;
167+
168+if (ok)
169+ fprintf('Timing check passed successfully\n');
170+else
171+ fprintf('Timing check failed! Error listing follows:\n');
172+ fprintf([error_report{:}]);
173+ fprintf('\n');
174+end
175+
176+%% do some visualizations
177+
178+
179+% trajectory calculation
180+
181+% plot k-spaces
182+
183+%% prepare the sequence output for the scanner
184+seq.setDefinition('FOV', [fov fov thickness]);
185+seq.setDefinition('Name', 'epi');
186+
187+seq.write('epi_se_rs.seq');
src/examples/pulseq/FastRadialGradientEcho.madded+122−0View file
@@ -0,0 +1,122 @@
1+% set system limits (slew rate 130 and max_grad 30 work on Prisma)
2+sys = mr.opts('MaxGrad', 28, 'GradUnit', 'mT/m', ...
3+ 'MaxSlew', 120, 'SlewUnit', 'T/m/s', 'rfRingdownTime', 10e-6, ...
4+ 'rfDeadTime', 100e-6, 'adcDeadTime', 10e-6);
5+
6+seq=mr.Sequence(sys); % Create a new sequence object
7+fov=240e-3; Nx=240; % Define FOV and resolution
8+alpha=5; % flip angle
9+sliceThickness=6e-3; % slice
10+Nr=256; % number of radial spokes
11+Ndummy=10; % number of dummy scans
12+delta= pi / Nr; % angular increment; try golden angle pi*(3-5^0.5) or 0.5 of it
13+ro_dur=1200e-6; % RO duration
14+ro_os=2; % readout oversampling
15+ro_spoil=0.5; % additional k-max excursion for RO spoiling
16+sl_spoil=1.5; % spoil area compared to the slice thickness
17+
18+% TE & TR are as short as possible derived from the above parameters and
19+% the system specs below
20+
21+% more in-depth parameters
22+rfSpoilingInc=84; % RF spoiling increment
23+rfTimeBwProd = 2; % time-bandwidth product for the RF pulse
24+
25+% Create alpha-degree slice selection pulse and gradient
26+[rf, gz, gzReph] = mr.makeSincPulse(alpha*pi/180,'Duration',400e-6,...
27+ 'SliceThickness',sliceThickness,'apodization',0.5,'timeBwProduct',rfTimeBwProd,'system',sys,...
28+ 'use','excitation');
29+% gradient spoiling in slice direction
30+if sl_spoil>0
31+ sp_area_needed=sl_spoil/sliceThickness*rfTimeBwProd-gz.flatArea/2;
32+ %gzSpoil=mr.makeTrapezoid('z','Area',sp_area_needed,'system',sys,'Delay',gx.riseTime+gx.flatTime);
33+ gzSpoil=mr.makeExtendedTrapezoidArea('z', 0, gz.amplitude, sp_area_needed, sys);
34+ gz=mr.makeExtendedTrapezoid('z','times',[0, gz.flatTime,gz.flatTime+gz.fallTime],'amplitudes',[gz.amplitude,gz.amplitude,0],'system',sys);
35+ gz.delay=gzSpoil.shape_dur;
36+ gz=mr.addGradients({gz, gzSpoil}, 'system', sys);
37+ rf.delay=max(gzSpoil.shape_dur,sys.rfDeadTime);
38+ gz.delay=rf.delay-gzSpoil.shape_dur;
39+end
40+% join rephaser
41+gzReph.delay=mr.calcDuration(gz);
42+gzComb=mr.addGradients({gz, gzReph}, 'system', sys);
43+[gzSpoil,gz]=mr.splitGradientAt(gzComb,rf.delay-sys.rfDeadTime);
44+gz.delay=gz.delay-mr.calcDuration(gzSpoil);
45+rf.delay=sys.rfDeadTime;
46+clear gzComb;
47+
48+% Define other gradients and ADC events
49+deltak=1/fov;
50+gx = mr.makeTrapezoid('x','Amplitude',Nx*deltak/ro_dur,'FlatTime',ceil(ro_dur/sys.gradRasterTime)*sys.gradRasterTime,'system',sys);
51+adc = mr.makeAdc(Nx*ro_os,'Duration',ro_dur,'Delay',gx.riseTime,'system',sys);
52+gxPre = mr.makeTrapezoid('x','Area',-gx.amplitude*(ro_dur/Nx/ro_os*(Nx*ro_os/2-0.5)+0.5*gx.riseTime),'system',sys); % 0.5 is necessary to acount for the Siemens sampling in the center of the dwell periods
53+%
54+[gxPre,~]=mr.align('right', gxPre, 'right', gz);
55+addDelay=mr.calcDuration(rf)-gxPre.delay;
56+if addDelay>0
57+ gxPre.delay = gxPre.delay+ceil(addDelay/sys.gradRasterTime)*sys.gradRasterTime;
58+end
59+
60+% gradient spoiling in slice direction
61+if ro_spoil>0
62+ %ro_spoil_area=(gx.area-gx.flatArea)/2;
63+ ro_add_time=ceil(((gx.area/Nx*(Nx/2+1)*ro_spoil)/gx.amplitude)/sys.gradRasterTime)*sys.gradRasterTime;
64+ gx.flatTime=gx.flatTime+ro_add_time; % careful, areas stored in the object are now wrong
65+end
66+
67+% join slice spoiler with the slice selection
68+%if (rf.delay>mr.calcDuration()) no, this does not work to be really optimal we need a new function with start, stop and area
69+% could be done with mr.makeExtendedTrapezoidArea()
70+
71+% Calculate timing
72+% TODO: just calculate actual TE and TR here
73+
74+% start the sequence
75+rf_phase=0;
76+rf_inc=0;
77+TR=0;
78+seq.addBlock(gzSpoil);
79+[gzSpoil,~,~]=mr.align('right',gzSpoil,'left',gx,mr.makeDelay(ceil((adc.delay+adc.dwell*adc.numSamples)/sys.gradRasterTime)*sys.gradRasterTime+gzSpoil.shape_dur));
80+for i=(1-Ndummy):Nr
81+ rf.phaseOffset=rf_phase/180*pi;
82+ adc.phaseOffset=rf_phase/180*pi;
83+ rf_inc=mod(rf_inc+rfSpoilingInc, 360.0);
84+ rf_phase=mod(rf_phase+rf_inc, 360.0);
85+ %
86+ phi=delta*(i-1);
87+ seq.addBlock(mr.rotate('z',phi,rf,gz,gxPre));
88+ if (i>0)
89+ seq.addBlock(mr.rotate('z',phi,gx,adc,gzSpoil));
90+ else
91+ seq.addBlock(mr.rotate('z',phi,gx,gzSpoil));
92+ end
93+ if TR<=0
94+ TR=seq.duration;
95+ end
96+end
97+% dummy slice select to ramp down to 0 Z spoil gradient
98+seq.addBlock(gz);
99+
100+%% check whether the timing of the sequence is correct
101+[ok, error_report]=seq.checkTiming;
102+
103+if (ok)
104+ fprintf('Timing check passed successfully\n');
105+else
106+ fprintf('Timing check failed! Error listing follows:\n');
107+ fprintf([error_report{:}]);
108+ fprintf('\n');
109+end
110+
111+%% plot, etc
112+%return;
113+%% trajectory calculation
114+%[ktraj_adc, ktraj, t_excitation, t_refocusing, t_adc] = seq.calculateKspace();
115+
116+% plot k-spaces
117+
118+%
119+seq.setDefinition('FOV', [fov fov sliceThickness]);
120+seq.setDefinition('Name', 'gre_rad');
121+
122+seq.write('fast_gre_rad.seq') % Write to pulseq file
src/examples/pulseq/GRE_live_demo.madded+127−0View file
@@ -0,0 +1,127 @@
1+% step
2+% 0 ... Basic sequence
3+% 1 ... Add spoiler in read, phase and slice (vary spoiler - line 49)
4+% 2 ... Refocus in phase
5+% 3 ... Vary RF phase quasi-randomly
6+% 4 ... Make receiver phase follow transmitter phase
7+% 5 ... Add dummy scans
8+step = 0;
9+
10+% Define FOV and resolution
11+fov = 256e-3;
12+sliceThickness = 5e-3;
13+Nx = 128;
14+Ny = Nx;
15+
16+% Define sequence parameters
17+TE = 8e-3;
18+TR = 16e-3;
19+alpha=30;
20+
21+% set system limits
22+sys = mr.opts('MaxGrad',20,'GradUnit','mT/m',...
23+ 'MaxSlew',120,'SlewUnit','T/m/s',...
24+ 'rfRingdownTime', 20e-6, 'rfDeadtime', 100e-6);
25+
26+% Create a new sequence object
27+seq=mr.Sequence(sys);
28+
29+% Create slice selective alpha-pulse and corresponding gradients
30+[rf, gz, gzReph] = mr.makeSincPulse(alpha*pi/180, 'Duration', 4e-3,...
31+ 'SliceThickness', sliceThickness, 'apodization', 0.5,'timeBwProduct', 4, ...
32+ 'system' ,sys,...
33+ 'use', 'excitation');
34+
35+% Define other gradients and ADC events
36+deltak = 1/fov; % Pulseq toolbox defaults to k-space units of m^-1
37+gx = mr.makeTrapezoid('x', 'FlatArea', Nx*deltak, 'FlatTime', 6.4e-3);
38+adc = mr.makeAdc(Nx, 'Duration', gx.flatTime, 'Delay', gx.riseTime);
39+gxPre = mr.makeTrapezoid('x', 'Area', -gx.area/2, 'Duration', 2e-3);
40+phaseAreas = ((0:Ny-1)-Ny/2)*deltak;
41+
42+
43+% Calculate timing
44+delayTE = round((TE - mr.calcDuration(gxPre) - mr.calcDuration(gz)/2 ...
45+ - mr.calcDuration(gx)/2)/seq.gradRasterTime)*seq.gradRasterTime;
46+delayTR = round((TR - mr.calcDuration(gxPre) - mr.calcDuration(gz) ...
47+ - mr.calcDuration(gx) - delayTE)/seq.gradRasterTime)*seq.gradRasterTime;
48+
49+if step > 0
50+ spoilArea=4*gx.area(); % 4 "looks" good
51+ % Add spoilers in read, refocus in phase and spoiler in slice
52+ gxPost = mr.makeTrapezoid('x', 'Area', spoilArea, 'system', sys); % we pass 'system' here to calculate shortest time gradient
53+ gyPost = mr.makeTrapezoid('y', 'Area', spoilArea, 'system', sys);
54+ gzPost = mr.makeTrapezoid('z', 'Area', spoilArea, 'system', sys);
55+end
56+
57+if step > 1
58+ gyPost = mr.makeTrapezoid('y', 'Area', -max(phaseAreas(:)), 'Duration', 2e-3);
59+end
60+
61+if step > 0
62+ delayTR = delayTR - mr.calcDuration(gxPost, gyPost, gzPost);
63+end
64+
65+if step > 4
66+ start = -30; % dummy scans
67+else
68+ start = 1;
69+end
70+% Loop over phase encodes and define sequence blocks
71+for i=start:Ny
72+ if step > 2
73+ % Vary RF phase quasi-randomly
74+ % RF spoiling phase increment = 84° for smoother transient decay, https://doi.org/10.1002/mrm.1910350216, 169° for diffusion independent rf spoiling in steady-state https://doi.org/10.1371/journal.pone.0324455
75+ rand_phase = mod(84*(i^2 + i + 2), 360)*pi/180;
76+ [rf, gz] = mr.makeSincPulse(alpha*pi/180, 'Duration', 4e-3,...
77+ 'SliceThickness', 5e-3, ...
78+ 'apodization', 0.5, ...
79+ 'timeBwProduct', 4, ...
80+ 'system', sys, ...
81+ 'phaseOffset', rand_phase);
82+ end
83+ seq.addBlock(rf, gz);
84+ if (i>0) % negative or zero index -- dummy scans
85+ gyPre = mr.makeTrapezoid('y', 'Area', phaseAreas(i), 'Duration', 2e-3);
86+ else
87+ gyPre = mr.makeTrapezoid('y', 'Area', 0, 'Duration', 2e-3);
88+ end
89+ seq.addBlock(gxPre, gyPre, gzReph);
90+ seq.addBlock(mr.makeDelay(delayTE));
91+ if step > 3
92+ % Make receiver phase follow transmitter phase
93+ adc = mr.makeAdc(Nx, 'Duration', gx.flatTime,...
94+ 'Delay', gx.riseTime,...
95+ 'phaseOffset', rand_phase);
96+ end
97+ if (i>0) % negative index -- dummy scans
98+ seq.addBlock(gx, adc);
99+ else
100+ seq.addBlock(gx);
101+ end
102+ if step > 1
103+ gyPost = mr.makeTrapezoid('y', 'Area', -gyPre.area, 'Duration', 2e-3);
104+ end
105+ if step > 0
106+ % Add spoilers in read and slice and may be in phase
107+ seq.addBlock(gxPost, gyPost, gzPost);
108+ end
109+ seq.addBlock(mr.makeDelay(delayTR));
110+end
111+
112+% check whether the timing of the sequence is correct
113+[ok, error_report]=seq.checkTiming;
114+
115+if (ok)
116+ fprintf('Timing check passed successfully\n');
117+else
118+ fprintf('Timing check failed! Error listing follows:\n');
119+ fprintf([error_report{:}]);
120+ fprintf('\n');
121+end
122+
123+% export definitions
124+seq.setDefinition('FOV', [fov fov sliceThickness]);
125+seq.setDefinition('Name', ['DEMO_gre' num2str(step)]);
126+
127+seq.write(['DEMO_gre' num2str(step) '.seq']) % Write to pulseq file
src/examples/pulseq/GRE_live_demo_step0.madded+59−0View file
@@ -0,0 +1,59 @@
1+% Define FOV and resolution
2+fov = 256e-3;
3+sliceThickness = 5e-3;
4+Nx = 128;
5+Ny = Nx;
6+
7+% Define sequence parameters
8+TE = 8e-3;
9+TR = 16e-3;
10+alpha=30;
11+
12+% set system limits
13+sys = mr.opts('MaxGrad',20,'GradUnit','mT/m',...
14+ 'MaxSlew',120,'SlewUnit','T/m/s',...
15+ 'rfRingdownTime', 20e-6, 'rfDeadtime', 100e-6);
16+
17+% Create a new sequence object
18+seq=mr.Sequence(sys);
19+
20+% Create slice selection alpha-pulse and corresponding gradients
21+[rf, gz, gzReph] = mr.makeSincPulse(alpha*pi/180, 'Duration', 4e-3,...
22+ 'SliceThickness', sliceThickness, 'apodization', 0.5,'timeBwProduct', 4, ...
23+ 'system' ,sys,...
24+ 'use', 'excitation');
25+
26+% Define other gradients and ADC events
27+deltak = 1/fov; % Pulseq toolbox defaults to k-space units of m^-1
28+gx = mr.makeTrapezoid('x', 'FlatArea', Nx*deltak, 'FlatTime', 6.4e-3);
29+adc = mr.makeAdc(Nx, 'Duration', gx.flatTime, 'Delay', gx.riseTime);
30+gxPre = mr.makeTrapezoid('x', 'Area', -gx.area/2, 'Duration', 2e-3);
31+phaseAreas = ((0:Ny-1)-Ny/2)*deltak;
32+
33+% Calculate timing
34+delayTE = round((TE - mr.calcDuration(gxPre) - mr.calcDuration(gz)/2 ...
35+ - mr.calcDuration(gx)/2)/seq.gradRasterTime)*seq.gradRasterTime;
36+delayTR = round((TR - mr.calcDuration(gxPre) - mr.calcDuration(gz) ...
37+ - mr.calcDuration(gx) - delayTE)/seq.gradRasterTime)*seq.gradRasterTime;
38+% Loop over phase encodes and define sequence blocks
39+for i=1:Ny
40+ seq.addBlock(rf, gz);
41+ gyPre = mr.makeTrapezoid('y', 'Area', phaseAreas(i), 'Duration', 2e-3);
42+ seq.addBlock(gxPre, gyPre, gzReph);
43+ seq.addBlock(mr.makeDelay(delayTE));
44+ seq.addBlock(gx, adc);
45+ seq.addBlock(mr.makeDelay(delayTR));
46+end
47+
48+% check whether the timing of the sequence is correct
49+[ok, error_report]=seq.checkTiming;
50+
51+if (~ok)
52+ fprintf('Timing check failed! Sequence probably will not run on the scanner.\n');
53+end
54+
55+% export definitions
56+seq.setDefinition('FOV', [fov fov sliceThickness]);
57+seq.setDefinition('Name', 'DEMO_gre0'); % if submitting a sequence please write your name to the Name field of the definition section
58+
59+seq.write('DEMO_grep0.seq') % Write to pulseq file
src/examples/pulseq/RadialGradientEcho.madded+82−0View file
@@ -0,0 +1,82 @@
1+seq=mr.Sequence(); % Create a new sequence object
2+fov=260e-3; Nx=320; % Define FOV and resolution
3+alpha=10; % flip angle
4+sliceThickness=3e-3; % slice
5+TE=8e-3; % TE; give a vector here to have multiple TEs (e.g. for field mapping)
6+TR=20e-3; % only a single value for now
7+Nr=256; % number of radial spokes
8+Ndummy=20; % number of dummy scans
9+delta= pi / Nr; % angular increment; try golden angle pi*(3-5^0.5) or 0.5 of it
10+
11+% more in-depth parameters
12+% RF spoiling increment = 84° for smoother transient decay, https://doi.org/10.1002/mrm.1910350216, 169° for diffusion independent rf spoiling in steady-state https://doi.org/10.1371/journal.pone.0324455
13+rfSpoilingInc=84; % RF spoiling increment
14+
15+% set system limits
16+sys = mr.opts('MaxGrad', 28, 'GradUnit', 'mT/m', ...
17+ 'MaxSlew', 120, 'SlewUnit', 'T/m/s', 'rfRingdownTime', 20e-6, ...
18+ 'rfDeadTime', 100e-6, 'adcDeadTime', 10e-6);
19+
20+% Create alpha-degree slice selection pulse and gradient
21+[rf, gz] = mr.makeSincPulse(alpha*pi/180,'Duration',4e-3,...
22+ 'SliceThickness',sliceThickness,'apodization',0.5,'timeBwProduct',4,'system',sys,...
23+ 'use','excitation');
24+
25+% Define other gradients and ADC events
26+deltak=1/fov;
27+gx = mr.makeTrapezoid('x','FlatArea',Nx*deltak,'FlatTime',6.4e-3/5,'system',sys);
28+adc = mr.makeAdc(Nx,'Duration',gx.flatTime,'Delay',gx.riseTime,'system',sys);
29+gxPre = mr.makeTrapezoid('x','Area',-gx.area/2-deltak/2,'Duration',2e-3,'system',sys);
30+gzReph = mr.makeTrapezoid('z','Area',-gz.area/2,'Duration',2e-3,'system',sys);
31+
32+% gradient spoiling
33+gxSpoil=mr.makeTrapezoid('x','Area',0.5*Nx*deltak,'system',sys);
34+gzSpoil=mr.makeTrapezoid('z','Area',4/sliceThickness,'system',sys);
35+
36+% Calculate timing
37+delayTE=ceil((TE - mr.calcDuration(gxPre) - gz.fallTime - gz.flatTime/2 ...
38+ - mr.calcDuration(gx)/2)/seq.gradRasterTime)*seq.gradRasterTime;
39+delayTR=ceil((TR - mr.calcDuration(gxPre) - mr.calcDuration(gz) ...
40+ - mr.calcDuration(gx) - delayTE)/seq.gradRasterTime)*seq.gradRasterTime;
41+assert(all(delayTR>=mr.calcDuration(gxSpoil,gzSpoil)));
42+
43+rf_phase=0;
44+rf_inc=0;
45+
46+for i=(-Ndummy):Nr
47+ for c=1:length(TE)
48+ rf.phaseOffset=rf_phase/180*pi;
49+ adc.phaseOffset=rf_phase/180*pi;
50+ rf_inc=mod(rf_inc+rfSpoilingInc, 360.0);
51+ rf_phase=mod(rf_phase+rf_inc, 360.0);
52+ %
53+ seq.addBlock(rf,gz);
54+ phi=delta*(i-1);
55+ seq.addBlock(mr.rotate('z',phi,gxPre,gzReph));
56+ seq.addBlock(mr.makeDelay(delayTE(c)));
57+ if (i>0)
58+ seq.addBlock(mr.rotate('z',phi,gx,adc));
59+ else
60+ seq.addBlock(mr.rotate('z',phi,gx));
61+ end
62+ seq.addBlock(mr.rotate('z',phi,gxSpoil,gzSpoil,mr.makeDelay(delayTR)));
63+ end
64+end
65+
66+
67+%% check whether the timing of the sequence is correct
68+[ok, error_report]=seq.checkTiming;
69+
70+if (ok)
71+ fprintf('Timing check passed successfully\n');
72+else
73+ fprintf('Timing check failed! Error listing follows:\n');
74+ fprintf([error_report{:}]);
75+ fprintf('\n');
76+end
77+
78+%%
79+seq.setDefinition('FOV', [fov fov sliceThickness]);
80+seq.setDefinition('Name', 'gre_rad');
81+
82+seq.write('gre_rad.seq') % Write to pulseq file
src/examples/pulseq/UTE.madded+106−0View file
@@ -0,0 +1,106 @@
1+% a very basic UTE-like sequence, without ramp-sampling, ramp-RF and other
2+% tricks yet. Achieves TE in the range of 300-400 us
3+
4+% set system limits
5+sys = mr.opts('MaxGrad', 28, 'GradUnit', 'mT/m', ...
6+ 'MaxSlew', 100, 'SlewUnit', 'T/m/s', 'rfRingdownTime', 20e-6, ...
7+ 'rfDeadTime', 100e-6, 'adcDeadTime', 10e-6);
8+
9+seq=mr.Sequence(sys); % Create a new sequence object
10+fov=250e-3; Nx=256; % Define FOV and resolution
11+alpha=10; % flip angle
12+sliceThickness=3e-3; % slice
13+TR=10e-3; % TR
14+Nr=128; % number of radial spokes
15+delta= 2* pi / Nr; % angular increment; try golden angle pi*(3-5^0.5) or 0.5 of it
16+ro_duration=2.56e-3; % read-out time: controls RO bandwidth and T2-blurring
17+ro_os=2; % oversampling
18+ro_asymmetry=1; % 0: fully symmetric 1: half-echo
19+minRF_to_ADC_time=50e-6; % the parameter wich defines TE (together with the RO asymmetyry)
20+
21+% more in-depth parameters
22+% RF spoiling increment = 84° for smoother transient decay, https://doi.org/10.1002/mrm.1910350216, 169° for diffusion independent rf spoiling in steady-state https://doi.org/10.1371/journal.pone.0324455
23+rfSpoilingInc=84; % RF spoiling increment
24+
25+% Create alpha-degree slice selection pulse and gradient
26+[rf, gz, gzReph] = mr.makeSincPulse(alpha*pi/180,'Duration',1e-3,...
27+ 'SliceThickness',sliceThickness,'apodization',0.5,'timeBwProduct',2,...
28+ 'centerpos',1,'system',sys, 'use','excitation');
29+
30+% Align RO assymmetry to ADC samples
31+Nxo=round(ro_os*Nx);
32+ro_asymmetry = round(ro_asymmetry*Nxo/2)/Nxo*2; % check whether we need to use 2Nx or so...
33+% Define other gradients and ADC events
34+deltak=1/fov/(1+ro_asymmetry);
35+ro_area=Nx*deltak;
36+gx = mr.makeTrapezoid('x','FlatArea',ro_area,'FlatTime',ro_duration,'system',sys);
37+adc = mr.makeAdc(Nxo,'Duration',gx.flatTime,'Delay',gx.riseTime,'system',sys);
38+gxPre = mr.makeTrapezoid('x','Area',-(gx.area-ro_area)/2 -gx.amplitude*adc.dwell/2 - ro_area/2*(1-ro_asymmetry),'system',sys);
39+
40+% gradient spoiling
41+gxSpoil=mr.makeTrapezoid('x','Area',0.2*Nx*deltak,'system',sys);
42+
43+% Calculate timing
44+%ceil((TE - mr.calcDuration(gxPre) - gz.fallTime - gz.flatTime/2 ...
45+% - mr.calcDuration(gx)/2)/seq.gradRasterTime)*seq.gradRasterTime;
46+% calculate actual achieved TE
47+TE = gz.fallTime + mr.calcDuration(gxPre,gzReph)+gx.riseTime + adc.dwell*Nxo/2*(1-ro_asymmetry);
48+delayTR=ceil((TR - mr.calcDuration(gxPre,gzReph) - mr.calcDuration(gz) ...
49+ - mr.calcDuration(gx))/seq.gradRasterTime)*seq.gradRasterTime;
50+assert(all(delayTR>=mr.calcDuration(gxSpoil)));
51+
52+fprintf('TE= %d us\n', round(TE*1e6));
53+
54+if mr.calcDuration(gzReph) > mr.calcDuration(gxPre)
55+ gxPre.delay=mr.calcDuration(gzReph) - mr.calcDuration(gxPre);
56+end
57+
58+rf_phase=0;
59+rf_inc=0;
60+
61+for i=1:Nr
62+ for c=1:2
63+ rf.phaseOffset=rf_phase/180*pi;
64+ adc.phaseOffset=rf_phase/180*pi;
65+ rf_inc=mod(rf_inc+rfSpoilingInc, 360.0);
66+ rf_phase=mod(rf_phase+rf_inc, 360.0);
67+ % UTE: alternate GZ
68+ gz.amplitude=-gz.amplitude;
69+ gzReph.amplitude=-gzReph.amplitude;
70+ %
71+ seq.addBlock(rf,gz);
72+ phi=delta*(i-1);
73+ gpc=gxPre; gps=gxPre; gpc.amplitude=gxPre.amplitude*cos(phi); gps.amplitude=gxPre.amplitude*sin(phi); gps.channel='y';
74+ grc=gx; grs=gx; grc.amplitude=gx.amplitude*cos(phi); grs.amplitude=gx.amplitude*sin(phi); grs.channel='y';
75+ gsc=gxSpoil; gss=gxSpoil; gsc.amplitude=gxSpoil.amplitude*cos(phi); gss.amplitude=gxSpoil.amplitude*sin(phi); gss.channel='y';
76+ seq.addBlock(gpc,gps,gzReph);
77+ seq.addBlock(grc,grs,adc);
78+ seq.addBlock(gsc,gss,mr.makeDelay(delayTR));
79+ end
80+end
81+
82+%% check whether the timing of the sequence is correct
83+[ok, error_report]=seq.checkTiming;
84+
85+if (ok)
86+ fprintf('Timing check passed successfully\n');
87+else
88+ fprintf('Timing check failed! Error listing follows:\n');
89+ fprintf([error_report{:}]);
90+ fprintf('\n');
91+end
92+
93+%%
94+
95+%% plot gradients to check for gaps and optimality of the timing
96+gw=seq.waveforms_and_times();
97+
98+%% k-space trajectory calculation
99+
100+% plot k-spaces
101+
102+%
103+seq.setDefinition('FOV', [fov fov sliceThickness]);
104+seq.setDefinition('Name', 'ute');
105+
106+seq.write('ute.seq'); % Write to pulseq file
src/examples/pulseq/UTE_rs.madded+125−0View file
@@ -0,0 +1,125 @@
1+% a basic UTE-like sequence
2+% achieves "TE" below 100 us
3+
4+% set system limits
5+sys = mr.opts('MaxGrad', 28, 'GradUnit', 'mT/m', ...
6+ 'MaxSlew', 170, 'SlewUnit', 'T/m/s', 'rfRingdownTime', 0e-6, ...
7+ 'rfDeadTime', 100e-6, 'adcDeadTime', 10e-6);
8+
9+seq=mr.Sequence(sys); % Create a new sequence object
10+fov=240e-3; Nx=240; % Define FOV and resolution
11+alpha=10; % flip angle
12+sliceThickness=5e-3; % slice
13+TR=20e-3; % TR
14+%Nr=round(Nx*pi/2); % number of radial spokes
15+Nr=Nx*2; % number of radial spokes
16+Ndummy=20; % number of dummy scans
17+delta= 2* pi / Nr; % angular increment; try golden angle pi*(3-5^0.5) or 0.5 of it
18+rf_duration=0.5e-3; % duration of the excitation pulse
19+ro_duration=0.720e-3; % read-out time: controls RO bandwidth and T2-blurring
20+ro_os=2; % oversampling
21+minRF_to_ADC_time=70e-6; % the parameter wich defines TE together with ro_discard
22+ro_discard=0; % dummy ADC samples to discard (due to ADC filter)
23+ro_spoil=1; % extend RO to achieve spoiling
24+
25+% more in-depth parameters
26+% RF spoiling increment = 84° for smoother transient decay, https://doi.org/10.1002/mrm.1910350216, 169° for diffusion independent rf spoiling in steady-state https://doi.org/10.1371/journal.pone.0324455
27+rfSpoilingInc=84; % RF spoiling increment
28+
29+%% Create alpha-degree slice selection pulse and gradient
30+[rf, gz] = mr.makeSincPulse(alpha*pi/180,'Duration',rf_duration,...
31+ 'SliceThickness',sliceThickness,'apodization',0.5,'timeBwProduct',2,...
32+ 'centerpos',1,'system',sys,'use','excitation');
33+
34+% resample the RF pulse to the ramp
35+gza=[0 1 1 0];
36+gzt=cumsum([0 gz.riseTime gz.flatTime gz.fallTime]);
37+gzas_0=interp1(gzt+gz.delay,gza,rf.t+rf.delay);
38+rft_1=[sys.rfRasterTime:sys.rfRasterTime:rf_duration+0.5*gz.fallTime];
39+gzas_1=interp1(gzt+gz.delay,gza,rft_1+rf.delay+gz.fallTime*0.5);
40+gzas_1(~isfinite(gzas_1))=0; % we are getting a NaN sometimes
41+kzs_0=cumsum(gzas_0);
42+kzs_1=cumsum(gzas_1);
43+kzs_0=kzs_0-max(kzs_0);
44+kzs_1=kzs_1-max(kzs_1);
45+rfs_1=diff([0 interp1(kzs_0,cumsum(rf.signal),kzs_1)]);
46+rf.t=rft_1(1:end-1); % remove the 0 at the end
47+rf.signal=rfs_1(1:end-1); % remove the 0 at the end
48+rf.shape_dur=length(rf.signal)*sys.rfRasterTime;
49+gz.flatTime=ceil((gz.flatTime-gz.fallTime*0.5)/sys.gradRasterTime)*sys.gradRasterTime;
50+rf.delay=mr.calcDuration(rf,gz)-rf.shape_dur; % fix the possible time shift due to the rounding-up step above
51+rf.center=rf.t(end); % or rf.shape_dur?
52+
53+% Align RO assymmetry to ADC samples
54+Nxo=round(ro_os*Nx);
55+% Define other gradients and ADC events
56+deltak=1/fov/2;
57+ro_area=Nx*deltak;
58+gx = mr.makeTrapezoid('x','FlatArea',ro_area,'FlatTime',ro_duration,'system',sys);
59+adc_dur=floor(gx.flatTime/Nxo*1e7)*1e-7*Nxo; % round down dwell time to 100ns (Siemens ADC raster)
60+adc = mr.makeAdc(Nxo,'Duration',adc_dur,'system',sys);
61+
62+% ro-spoiling
63+gx.flatTime=gx.flatTime*ro_spoil;
64+
65+% Calculate timing
66+%ceil((TE - mr.calcDuration(gxPre) - gz.fallTime - gz.flatTime/2 ...
67+% - mr.calcDuration(gx)/2)/seq.gradRasterTime)*seq.gradRasterTime;
68+% calculate actual achieved TE
69+TE = ceil((minRF_to_ADC_time + adc.dwell*ro_discard)/seq.gradRasterTime)*seq.gradRasterTime;
70+delayTR=ceil((TR - mr.calcDuration(gz) ...
71+ - mr.calcDuration(gx) - TE)/seq.gradRasterTime)*seq.gradRasterTime;
72+
73+fprintf('TE= %d us; delay in TR:= %d us\n', round(TE*1e6), floor(delayTR*1e6));
74+
75+% set up timing
76+gx.delay=mr.calcDuration(gz)+TE;
77+adc.delay=floor((gx.delay-adc.dwell*0.5-adc.dwell*ro_discard)/sys.gradRasterTime)*sys.gradRasterTime; % take into accout 0.5 samples ADC shift
78+
79+rf_phase=0;
80+rf_inc=0;
81+
82+if Ndummy>0
83+ seq.addBlock(mr.makeLabel('SET','ONCE', 1)); % label the few following scans as preparing scans
84+end
85+
86+for i=(-Ndummy+1):Nr
87+ if Ndummy>0 && i==1
88+ seq.addBlock(mr.makeLabel('SET','ONCE', 0)); % remove the preparing scan label
89+ end
90+ for c=1:2
91+ rf.phaseOffset=rf_phase/180*pi;
92+ adc.phaseOffset=rf_phase/180*pi;
93+ rf_inc=mod(rf_inc+rfSpoilingInc, 360.0);
94+ rf_phase=mod(rf_phase+rf_inc, 360.0);
95+ % UTE: alternate GZ
96+ gz.amplitude=-gz.amplitude;
97+ %
98+ phi=delta*(i-1);
99+ grc=gx; grs=gx; grc.amplitude=gx.amplitude*cos(phi); grs.amplitude=gx.amplitude*sin(phi); grs.channel='y';
100+ if (i>0)
101+ seq.addBlock(rf,gz,grc,grs,adc);
102+ else
103+ seq.addBlock(rf,gz,grc,grs);
104+ end
105+ seq.addBlock(delayTR);
106+ end
107+end
108+
109+
110+%% check whether the timing of the sequence is correct
111+[ok, error_report]=seq.checkTiming;
112+
113+if (ok)
114+ fprintf('Timing check passed successfully\n');
115+else
116+ fprintf('Timing check failed! Error listing follows:\n');
117+ fprintf([error_report{:}]);
118+ fprintf('\n');
119+end
120+
121+%% export
122+seq.setDefinition('FOV', [fov fov sliceThickness]);
123+seq.setDefinition('Name', 'ute_rs');
124+
125+seq.write('ute_rs.seq'); % Write to pulseq file
src/index.cssadded+531−0View file
@@ -0,0 +1,531 @@
1+:root {
2+ --bg: #0f1115;
3+ --panel: #151a21;
4+ --panel-2: #12161d;
5+ --border: #262d38;
6+ --text: #d7dde7;
7+ --text-dim: #8b95a5;
8+ --accent: #4da3ff;
9+ --green: #51cf66;
10+ --red: #ff6b6b;
11+ --amber: #ffc861;
12+ color-scheme: dark;
13+}
14+
15+* {
16+ box-sizing: border-box;
17+}
18+
19+html,
20+body,
21+#root {
22+ height: 100%;
23+ margin: 0;
24+}
25+
26+body {
27+ background: var(--bg);
28+ color: var(--text);
29+ font-family:
30+ system-ui,
31+ -apple-system,
32+ 'Segoe UI',
33+ Roboto,
34+ sans-serif;
35+ font-size: 14px;
36+ overflow: hidden;
37+}
38+
39+button {
40+ font: inherit;
41+ color: inherit;
42+ background: #1d242e;
43+ border: 1px solid var(--border);
44+ border-radius: 6px;
45+ padding: 5px 12px;
46+ cursor: pointer;
47+ white-space: nowrap;
48+}
49+button:hover:not(:disabled) {
50+ background: #242d3a;
51+ border-color: #33405085;
52+}
53+button:disabled {
54+ opacity: 0.45;
55+ cursor: default;
56+}
57+button.primary {
58+ background: #1d4ed8;
59+ border-color: #2563eb;
60+}
61+button.primary:hover:not(:disabled) {
62+ background: #2158e8;
63+}
64+button.danger {
65+ background: #7f1d1d;
66+ border-color: #991b1b;
67+}
68+button.danger:hover:not(:disabled) {
69+ background: #931f1f;
70+}
71+
72+.app {
73+ display: flex;
74+ flex-direction: column;
75+ height: 100%;
76+}
77+
78+/* ── header ─────────────────────────────────────────── */
79+.header {
80+ display: flex;
81+ align-items: center;
82+ gap: 14px;
83+ padding: 0 14px;
84+ height: 48px;
85+ background: var(--panel-2);
86+ border-bottom: 1px solid var(--border);
87+ flex: none;
88+}
89+.header .logo {
90+ display: flex;
91+ align-items: baseline;
92+ gap: 8px;
93+}
94+.header .logo b {
95+ font-size: 17px;
96+ letter-spacing: 0.02em;
97+}
98+.header .logo span {
99+ color: var(--text-dim);
100+ font-size: 12.5px;
101+}
102+.header .logo .tagline a {
103+ color: var(--accent);
104+ text-decoration: none;
105+}
106+.header .logo .tagline a:hover {
107+ text-decoration: underline;
108+}
109+.header .spacer {
110+ flex: 1;
111+}
112+
113+/* ── main split ─────────────────────────────────────── */
114+.main {
115+ display: flex;
116+ flex: 1;
117+ min-height: 0;
118+}
119+.left {
120+ display: flex;
121+ flex-direction: column;
122+ min-width: 300px;
123+ border-right: 1px solid var(--border);
124+ background: var(--panel);
125+}
126+.divider {
127+ width: 5px;
128+ cursor: col-resize;
129+ background: transparent;
130+ flex: none;
131+}
132+.divider:hover {
133+ background: #2a3442;
134+}
135+.right {
136+ flex: 1;
137+ min-width: 0;
138+ display: flex;
139+ flex-direction: column;
140+ overflow-y: auto;
141+ background: var(--bg);
142+}
143+
144+/* ── editor pane ────────────────────────────────────── */
145+.editor-toolbar {
146+ display: flex;
147+ align-items: center;
148+ gap: 8px;
149+ padding: 8px 10px;
150+ border-bottom: 1px solid var(--border);
151+ flex: none;
152+}
153+.editor-toolbar .name {
154+ color: var(--text-dim);
155+ font-size: 12.5px;
156+ overflow: hidden;
157+ text-overflow: ellipsis;
158+}
159+.editor-toolbar .examples-btn {
160+ color: var(--accent);
161+ border-color: #33547a;
162+ font-weight: 500;
163+}
164+.editor-toolbar .examples-btn:hover:not(:disabled) {
165+ background: #17263a;
166+ border-color: #4177b3;
167+}
168+.editor-host {
169+ flex: 1;
170+ min-height: 0;
171+ overflow: hidden;
172+}
173+.editor-host .cm-editor {
174+ height: 100%;
175+ font-size: 15px;
176+}
177+.editor-host .cm-scroller {
178+ font-family: 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
179+}
180+
181+/* ── console ────────────────────────────────────────── */
182+.console {
183+ flex: none;
184+ height: 30%;
185+ min-height: 90px;
186+ display: flex;
187+ flex-direction: column;
188+ border-top: 1px solid var(--border);
189+ background: var(--panel-2);
190+}
191+.console-head {
192+ display: flex;
193+ align-items: center;
194+ gap: 8px;
195+ padding: 4px 10px;
196+ font-size: 11.5px;
197+ color: var(--text-dim);
198+ text-transform: uppercase;
199+ letter-spacing: 0.06em;
200+ border-bottom: 1px solid var(--border);
201+}
202+.console-body {
203+ flex: 1;
204+ overflow-y: auto;
205+ padding: 6px 10px;
206+ font-family: 'SF Mono', Menlo, Consolas, monospace;
207+ font-size: 12px;
208+ line-height: 1.5;
209+ white-space: pre-wrap;
210+ word-break: break-word;
211+}
212+.console-body .err {
213+ color: var(--red);
214+}
215+.console-body .ok {
216+ color: var(--green);
217+}
218+
219+/* ── viewer ─────────────────────────────────────────── */
220+.viewer-empty {
221+ flex: 1;
222+ display: flex;
223+ flex-direction: column;
224+ align-items: center;
225+ justify-content: center;
226+ gap: 12px;
227+ color: var(--text-dim);
228+ text-align: center;
229+ padding: 30px;
230+}
231+.viewer-empty h2 {
232+ color: var(--text);
233+ font-weight: 600;
234+ margin: 0;
235+}
236+.viewer-empty .hint {
237+ max-width: 440px;
238+ line-height: 1.55;
239+}
240+.drop-active::after {
241+ content: 'Drop .seq file to view';
242+ position: fixed;
243+ inset: 0;
244+ display: flex;
245+ align-items: center;
246+ justify-content: center;
247+ font-size: 22px;
248+ color: var(--accent);
249+ background: #0f1115d9;
250+ border: 2px dashed var(--accent);
251+ z-index: 50;
252+ pointer-events: none;
253+}
254+
255+.seq-tabs {
256+ display: flex;
257+ gap: 4px;
258+ padding: 8px 12px 0;
259+ flex: none;
260+}
261+.seq-tabs button {
262+ border-radius: 6px 6px 0 0;
263+ border-bottom: none;
264+ background: transparent;
265+ color: var(--text-dim);
266+}
267+.seq-tabs button.active {
268+ background: var(--panel);
269+ color: var(--text);
270+}
271+
272+.viewer-section {
273+ margin: 10px 12px;
274+ background: var(--panel);
275+ border: 1px solid var(--border);
276+ border-radius: 8px;
277+ overflow: hidden;
278+ flex: none;
279+}
280+.viewer-section > h3 {
281+ margin: 0;
282+ padding: 7px 12px;
283+ font-size: 11.5px;
284+ font-weight: 600;
285+ color: var(--text-dim);
286+ text-transform: uppercase;
287+ letter-spacing: 0.07em;
288+ border-bottom: 1px solid var(--border);
289+ display: flex;
290+ align-items: center;
291+ gap: 10px;
292+}
293+.viewer-section > h3 .free {
294+ flex: 1;
295+}
296+.viewer-section > h3 .fine {
297+ text-transform: none;
298+ letter-spacing: 0;
299+ font-weight: 400;
300+}
301+
302+.timeline-host {
303+ position: relative;
304+}
305+.timeline-canvas {
306+ display: block;
307+ width: 100%;
308+ cursor: crosshair;
309+}
310+.timeline-canvas.panning {
311+ cursor: grabbing;
312+}
313+
314+/* ── report ─────────────────────────────────────────── */
315+.report {
316+ display: flex;
317+ flex-wrap: wrap;
318+ gap: 8px 26px;
319+ padding: 10px 14px;
320+ font-size: 13px;
321+}
322+.report .item b {
323+ display: block;
324+ font-size: 11px;
325+ font-weight: 600;
326+ color: var(--text-dim);
327+ text-transform: uppercase;
328+ letter-spacing: 0.05em;
329+}
330+.report .item span {
331+ font-family: 'SF Mono', Menlo, Consolas, monospace;
332+ font-size: 12.5px;
333+}
334+.badge {
335+ display: inline-block;
336+ padding: 1px 8px;
337+ border-radius: 999px;
338+ font-size: 11.5px;
339+ border: 1px solid;
340+}
341+.badge.good {
342+ color: var(--green);
343+ border-color: #2f9e4466;
344+}
345+.badge.bad {
346+ color: var(--red);
347+ border-color: #e0313166;
348+}
349+.badge.neutral {
350+ color: var(--text-dim);
351+ border-color: var(--border);
352+}
353+
354+/* ── inspector ──────────────────────────────────────── */
355+.inspector {
356+ padding: 8px 12px 12px;
357+ font-size: 13px;
358+}
359+.inspector .nav {
360+ display: flex;
361+ align-items: center;
362+ gap: 8px;
363+ margin-bottom: 8px;
364+}
365+.inspector .nav .where {
366+ font-family: 'SF Mono', Menlo, Consolas, monospace;
367+ font-size: 12.5px;
368+ color: var(--text-dim);
369+}
370+.inspector table {
371+ border-collapse: collapse;
372+ width: 100%;
373+}
374+.inspector td,
375+.inspector th {
376+ text-align: left;
377+ padding: 4px 10px 4px 0;
378+ border-bottom: 1px solid #1d232d;
379+ vertical-align: top;
380+}
381+.inspector th {
382+ color: var(--text-dim);
383+ font-weight: 600;
384+ white-space: nowrap;
385+ width: 1%;
386+}
387+.inspector td {
388+ font-family: 'SF Mono', Menlo, Consolas, monospace;
389+ font-size: 12.5px;
390+}
391+.inspector .muted {
392+ color: var(--text-dim);
393+}
394+.inspector-hint {
395+ margin: 0;
396+ padding: 12px;
397+ color: var(--text-dim);
398+ font-size: 13px;
399+}
400+
401+/* dropdown menu */
402+.menu-wrap {
403+ position: relative;
404+}
405+.menu {
406+ position: absolute;
407+ top: calc(100% + 4px);
408+ right: 0;
409+ min-width: 300px;
410+ background: #1a212b;
411+ border: 1px solid var(--border);
412+ border-radius: 8px;
413+ box-shadow: 0 8px 28px #0009;
414+ z-index: 40;
415+ padding: 4px;
416+}
417+.menu button {
418+ display: block;
419+ width: 100%;
420+ text-align: left;
421+ background: transparent;
422+ border: none;
423+ padding: 8px 10px;
424+ border-radius: 6px;
425+}
426+.menu button:hover {
427+ background: #232d3a;
428+}
429+.menu button b {
430+ display: block;
431+}
432+.menu button span {
433+ display: block;
434+ color: var(--text-dim);
435+ font-size: 12px;
436+ margin-top: 1px;
437+ white-space: normal;
438+}
439+
440+/* ── examples modal ─────────────────────────────────── */
441+.modal-backdrop {
442+ position: fixed;
443+ inset: 0;
444+ background: #0a0c10cc;
445+ z-index: 60;
446+ display: flex;
447+ align-items: center;
448+ justify-content: center;
449+ padding: 30px;
450+}
451+.modal {
452+ width: min(1000px, 100%);
453+ max-height: 100%;
454+ background: var(--panel);
455+ border: 1px solid var(--border);
456+ border-radius: 12px;
457+ box-shadow: 0 20px 60px #000a;
458+ display: flex;
459+ flex-direction: column;
460+ overflow: hidden;
461+}
462+.modal-head {
463+ display: flex;
464+ align-items: baseline;
465+ gap: 12px;
466+ padding: 14px 18px;
467+ border-bottom: 1px solid var(--border);
468+ flex: none;
469+}
470+.modal-head h2 {
471+ margin: 0;
472+ font-size: 17px;
473+}
474+.modal-head .count {
475+ color: var(--text-dim);
476+ font-size: 12.5px;
477+}
478+.modal-body {
479+ overflow-y: auto;
480+ padding: 6px 18px 18px;
481+}
482+.modal-body section {
483+ margin-top: 16px;
484+}
485+.modal-body section h3 {
486+ margin: 0 0 8px;
487+ font-size: 12px;
488+ font-weight: 600;
489+ color: var(--accent);
490+ text-transform: uppercase;
491+ letter-spacing: 0.07em;
492+}
493+.example-grid {
494+ display: grid;
495+ grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
496+ gap: 8px;
497+}
498+.example-card {
499+ text-align: left;
500+ background: var(--panel-2);
501+ border: 1px solid var(--border);
502+ border-radius: 8px;
503+ padding: 10px 12px;
504+ display: flex;
505+ flex-direction: column;
506+ gap: 4px;
507+}
508+.example-card:hover {
509+ border-color: #3a4d66;
510+ background: #1a212b;
511+}
512+.example-card .ex-top {
513+ display: flex;
514+ align-items: baseline;
515+ gap: 8px;
516+}
517+.example-card .ex-top b {
518+ flex: 1;
519+ font-size: 13.5px;
520+}
521+.example-card .ex-time {
522+ color: var(--text-dim);
523+ font-size: 11px;
524+ font-family: 'SF Mono', Menlo, Consolas, monospace;
525+ white-space: nowrap;
526+}
527+.example-card .ex-desc {
528+ color: var(--text-dim);
529+ font-size: 12px;
530+ line-height: 1.45;
531+}
src/main.tsxadded+10−0View file
@@ -0,0 +1,10 @@
1+import { StrictMode } from 'react'
2+import { createRoot } from 'react-dom/client'
3+import './index.css'
4+import App from './App.tsx'
5+
6+createRoot(document.getElementById('root')!).render(
7+ <StrictMode>
8+ <App />
9+ </StrictMode>,
10+)
src/seq/md5.tsadded+74−0View file
@@ -0,0 +1,74 @@
1+// Pure-JS MD5 (RFC 1321), used to verify .seq [SIGNATURE] sections
2+// synchronously (WebCrypto has no MD5). Input is treated as a byte string.
3+
4+export function md5Hex(input: string): string {
5+ const bytes: number[] = []
6+ for (let i = 0; i < input.length; i++) bytes.push(input.charCodeAt(i) & 0xff)
7+
8+ const origLenBits = bytes.length * 8
9+ bytes.push(0x80)
10+ while (bytes.length % 64 !== 56) bytes.push(0)
11+ let len = origLenBits
12+ for (let i = 0; i < 8; i++) {
13+ bytes.push(len & 0xff)
14+ len = Math.floor(len / 256)
15+ }
16+
17+ const S = [
18+ 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9,
19+ 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10, 15,
20+ 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
21+ ]
22+ const K = Array.from({ length: 64 }, (_, i) => Math.floor(Math.abs(Math.sin(i + 1)) * 4294967296) >>> 0)
23+
24+ let a0 = 0x67452301
25+ let b0 = 0xefcdab89
26+ let c0 = 0x98badcfe
27+ let d0 = 0x10325476
28+
29+ const rotl = (x: number, c: number) => ((x << c) | (x >>> (32 - c))) >>> 0
30+
31+ for (let chunk = 0; chunk < bytes.length; chunk += 64) {
32+ const M = Array.from({ length: 16 }, (_, j) => {
33+ const o = chunk + j * 4
34+ return (bytes[o] | (bytes[o + 1] << 8) | (bytes[o + 2] << 16) | (bytes[o + 3] << 24)) >>> 0
35+ })
36+ let A = a0
37+ let B = b0
38+ let C = c0
39+ let D = d0
40+ for (let i = 0; i < 64; i++) {
41+ let F: number
42+ let g: number
43+ if (i < 16) {
44+ F = (B & C) | (~B & D)
45+ g = i
46+ } else if (i < 32) {
47+ F = (D & B) | (~D & C)
48+ g = (5 * i + 1) % 16
49+ } else if (i < 48) {
50+ F = B ^ C ^ D
51+ g = (3 * i + 5) % 16
52+ } else {
53+ F = C ^ (B | ~D)
54+ g = (7 * i) % 16
55+ }
56+ F = (F + A + K[i] + M[g]) >>> 0
57+ A = D
58+ D = C
59+ C = B
60+ B = (B + rotl(F, S[i])) >>> 0
61+ }
62+ a0 = (a0 + A) >>> 0
63+ b0 = (b0 + B) >>> 0
64+ c0 = (c0 + C) >>> 0
65+ d0 = (d0 + D) >>> 0
66+ }
67+
68+ const le = (w: number) => {
69+ let s = ''
70+ for (let i = 0; i < 4; i++) s += ((w >>> (i * 8)) & 0xff).toString(16).padStart(2, '0')
71+ return s
72+ }
73+ return le(a0) + le(b0) + le(c0) + le(d0)
74+}
src/seq/parseSeq.tsadded+436−0View file
@@ -0,0 +1,436 @@
1+// Parser for pulseq .seq files, formats v1.4.x and v1.5.x, mirroring
2+// pulseq's +mr/@Sequence/read.m (column layouts, unit scales, shape RLE).
3+// Older formats (< 1.4.0) are rejected with a clear message.
4+import { md5Hex } from './md5.ts'
5+import type {
6+ AdcEvent,
7+ ArbGradEvent,
8+ DefinitionValue,
9+ ExtensionRef,
10+ ExtensionSpec,
11+ GradEvent,
12+ ParsedSeq,
13+ RfEvent,
14+ SeqBlock,
15+ SeqVersion,
16+ Shape,
17+ TrapGradEvent,
18+} from './types.ts'
19+
20+export class SeqParseError extends Error {}
21+
22+const DEFAULT_RASTERS = {
23+ blockDuration: 1e-5,
24+ gradient: 1e-5,
25+ rf: 1e-6,
26+ adc: 1e-7,
27+}
28+
29+function tokens(line: string): string[] {
30+ return line.trim().split(/\s+/)
31+}
32+
33+function nums(line: string, context: string): number[] {
34+ const out = tokens(line).map(Number)
35+ if (out.some((v) => Number.isNaN(v))) {
36+ throw new SeqParseError(`malformed numeric line in ${context}: "${line.trim()}"`)
37+ }
38+ return out
39+}
40+
41+/** pulseq shape decompression: RLE on the derivative (v v n => v x(n+2)), then cumsum. */
42+export function decompressShape(numSamples: number, data: number[]): Float64Array {
43+ if (data.length === numSamples) {
44+ // uncompressed (stored) samples — v1.4+ marks this by matching lengths
45+ return Float64Array.from(data)
46+ }
47+ const deriv: number[] = []
48+ let i = 0
49+ while (i < data.length) {
50+ if (i + 2 < data.length && data[i] === data[i + 1]) {
51+ const v = data[i]
52+ const rep = data[i + 2] + 2
53+ for (let k = 0; k < rep; k++) deriv.push(v)
54+ i += 3
55+ } else {
56+ deriv.push(data[i])
57+ i++
58+ }
59+ }
60+ if (deriv.length !== numSamples) {
61+ throw new SeqParseError(
62+ `shape decompression produced ${deriv.length} samples, expected ${numSamples}`,
63+ )
64+ }
65+ const out = new Float64Array(numSamples)
66+ let acc = 0
67+ for (let k = 0; k < numSamples; k++) {
68+ acc += deriv[k]
69+ out[k] = acc
70+ }
71+ return out
72+}
73+
74+export function parseSeq(text: string): ParsedSeq {
75+ const lines = text.split(/\r?\n/)
76+
77+ let version: SeqVersion | null = null
78+ const definitions = new Map<string, DefinitionValue>()
79+ const blocks: SeqBlock[] = []
80+ const rf = new Map<number, RfEvent>()
81+ const grads = new Map<number, GradEvent>()
82+ const adcs = new Map<number, AdcEvent>()
83+ const shapes = new Map<number, Shape>()
84+ const extensions = new Map<number, ExtensionRef>()
85+ const extensionSpecs: ExtensionSpec[] = []
86+ let signature: ParsedSeq['signature']
87+
88+ // Raw [BLOCKS] rows; converted to seconds once rasters are known (the
89+ // [DEFINITIONS] section precedes [BLOCKS] in practice, but don't rely on it).
90+ const blockRows: number[][] = []
91+
92+ type Section =
93+ | 'none'
94+ | 'version'
95+ | 'definitions'
96+ | 'blocks'
97+ | 'rf'
98+ | 'gradients'
99+ | 'trap'
100+ | 'adc'
101+ | 'shapes'
102+ | 'extensions'
103+ | 'extension-spec'
104+ | 'signature'
105+ | 'skip'
106+ let section: Section = 'none'
107+ let currentSpec: ExtensionSpec | null = null
108+
109+ // [SHAPES] accumulation state
110+ let shapeId = -1
111+ let shapeNumSamples = -1
112+ let shapeData: number[] = []
113+ const flushShape = () => {
114+ if (shapeId < 0) return
115+ shapes.set(shapeId, {
116+ id: shapeId,
117+ numSamples: shapeNumSamples,
118+ samples: decompressShape(shapeNumSamples, shapeData),
119+ storedSamples: shapeData.length,
120+ })
121+ shapeId = -1
122+ shapeNumSamples = -1
123+ shapeData = []
124+ }
125+
126+ const requireVersion = (): SeqVersion => {
127+ if (!version) throw new SeqParseError('file must declare [VERSION] before event sections')
128+ return version
129+ }
130+
131+ for (const rawLine of lines) {
132+ const line = rawLine.trim()
133+ if (line === '' || line.startsWith('#')) continue
134+
135+ if (line.startsWith('[') && line.endsWith(']')) {
136+ if (section === 'shapes') flushShape()
137+ currentSpec = null
138+ switch (line) {
139+ case '[VERSION]':
140+ section = 'version'
141+ version = { major: 0, minor: 0, revision: '0', combined: 0 }
142+ break
143+ case '[DEFINITIONS]':
144+ section = 'definitions'
145+ break
146+ case '[BLOCKS]':
147+ section = 'blocks'
148+ break
149+ case '[RF]':
150+ requireVersion()
151+ section = 'rf'
152+ break
153+ case '[GRADIENTS]':
154+ requireVersion()
155+ section = 'gradients'
156+ break
157+ case '[TRAP]':
158+ section = 'trap'
159+ break
160+ case '[ADC]':
161+ requireVersion()
162+ section = 'adc'
163+ break
164+ case '[SHAPES]':
165+ section = 'shapes'
166+ break
167+ case '[EXTENSIONS]':
168+ section = 'extensions'
169+ break
170+ case '[SIGNATURE]':
171+ section = 'signature'
172+ break
173+ case '[DELAYS]':
174+ throw new SeqParseError(
175+ 'this file uses the pre-1.4.0 pulseq format ([DELAYS] section), which is not supported',
176+ )
177+ default:
178+ section = 'skip' // unknown section: ignore its lines
179+ break
180+ }
181+ continue
182+ }
183+
184+ // `extension NAME typeId` opens a specification subsection
185+ if ((section === 'extensions' || section === 'extension-spec') && line.startsWith('extension ')) {
186+ const t = tokens(line)
187+ currentSpec = { name: t[1], typeId: Number(t[2]), rows: [] }
188+ extensionSpecs.push(currentSpec)
189+ section = 'extension-spec'
190+ continue
191+ }
192+
193+ switch (section) {
194+ case 'version': {
195+ const [key, value] = tokens(line)
196+ const v = version!
197+ if (key === 'major') v.major = Number(value)
198+ else if (key === 'minor') v.minor = Number(value)
199+ else if (key === 'revision') v.revision = value
200+ v.combined = 1000000 * v.major + 1000 * v.minor + (parseInt(v.revision, 10) || 0)
201+ break
202+ }
203+ case 'definitions': {
204+ const t = tokens(line)
205+ const key = t[0]
206+ const rest = t.slice(1)
207+ const asNums = rest.map(Number)
208+ definitions.set(
209+ key,
210+ rest.length > 0 && asNums.every((v) => !Number.isNaN(v))
211+ ? asNums
212+ : line.trim().slice(key.length).trim(),
213+ )
214+ break
215+ }
216+ case 'blocks':
217+ blockRows.push(nums(line, '[BLOCKS]'))
218+ break
219+ case 'rf': {
220+ const v = requireVersion()
221+ if (v.combined >= 1005000) {
222+ // id amp mag_id phase_id time_id center(us) delay(us) freqPPM phasePPM freq phase use
223+ const t = tokens(line)
224+ const d = t.slice(0, 11).map(Number)
225+ if (d.some(Number.isNaN) || t.length < 12) {
226+ throw new SeqParseError(`malformed [RF] line: "${line}"`)
227+ }
228+ rf.set(d[0], {
229+ id: d[0],
230+ amp: d[1],
231+ magShapeId: d[2],
232+ phaseShapeId: d[3],
233+ timeShapeId: d[4],
234+ center: d[5] * 1e-6,
235+ delay: d[6] * 1e-6,
236+ freqPPM: d[7],
237+ phasePPM: d[8],
238+ freq: d[9],
239+ phase: d[10],
240+ use: t[11],
241+ })
242+ } else {
243+ // v1.4: id amp mag_id phase_id time_id delay(us) freq phase
244+ const d = nums(line, '[RF]')
245+ rf.set(d[0], {
246+ id: d[0],
247+ amp: d[1],
248+ magShapeId: d[2],
249+ phaseShapeId: d[3],
250+ timeShapeId: d[4],
251+ center: NaN,
252+ delay: d[5] * 1e-6,
253+ freqPPM: 0,
254+ phasePPM: 0,
255+ freq: d[6],
256+ phase: d[7],
257+ use: 'u',
258+ })
259+ }
260+ break
261+ }
262+ case 'gradients': {
263+ const v = requireVersion()
264+ const d = nums(line, '[GRADIENTS]')
265+ let ev: ArbGradEvent
266+ if (v.combined >= 1005000) {
267+ // id amp first last shape_id time_id delay(us)
268+ ev = {
269+ id: d[0],
270+ kind: 'grad',
271+ amp: d[1],
272+ first: d[2],
273+ last: d[3],
274+ shapeId: d[4],
275+ timeShapeId: d[5],
276+ delay: d[6] * 1e-6,
277+ }
278+ } else {
279+ // v1.4: id amp shape_id time_id delay(us); first/last derived on demand
280+ ev = {
281+ id: d[0],
282+ kind: 'grad',
283+ amp: d[1],
284+ first: NaN,
285+ last: NaN,
286+ shapeId: d[2],
287+ timeShapeId: d[3],
288+ delay: d[4] * 1e-6,
289+ }
290+ }
291+ grads.set(ev.id, ev)
292+ break
293+ }
294+ case 'trap': {
295+ // id amp rise(us) flat(us) fall(us) delay(us)
296+ const d = nums(line, '[TRAP]')
297+ const ev: TrapGradEvent = {
298+ id: d[0],
299+ kind: 'trap',
300+ amp: d[1],
301+ rise: d[2] * 1e-6,
302+ flat: d[3] * 1e-6,
303+ fall: d[4] * 1e-6,
304+ delay: d[5] * 1e-6,
305+ }
306+ grads.set(ev.id, ev)
307+ break
308+ }
309+ case 'adc': {
310+ const v = requireVersion()
311+ const d = nums(line, '[ADC]')
312+ if (v.combined >= 1005000) {
313+ // id num dwell(ns) delay(us) freqPPM phasePPM freq phase phase_id
314+ adcs.set(d[0], {
315+ id: d[0],
316+ num: d[1],
317+ dwell: d[2] * 1e-9,
318+ delay: d[3] * 1e-6,
319+ freqPPM: d[4],
320+ phasePPM: d[5],
321+ freq: d[6],
322+ phase: d[7],
323+ phaseShapeId: d[8] ?? 0,
324+ })
325+ } else {
326+ // v1.4: id num dwell(ns) delay(us) freq phase
327+ adcs.set(d[0], {
328+ id: d[0],
329+ num: d[1],
330+ dwell: d[2] * 1e-9,
331+ delay: d[3] * 1e-6,
332+ freqPPM: 0,
333+ phasePPM: 0,
334+ freq: d[4],
335+ phase: d[5],
336+ phaseShapeId: 0,
337+ })
338+ }
339+ break
340+ }
341+ case 'shapes': {
342+ const t = tokens(line)
343+ if (t[0] === 'shape_id') {
344+ flushShape()
345+ shapeId = Number(t[1])
346+ } else if (t[0] === 'num_samples') {
347+ shapeNumSamples = Number(t[1])
348+ } else {
349+ for (const tok of t) {
350+ const v = Number(tok)
351+ if (Number.isNaN(v)) throw new SeqParseError(`malformed shape sample: "${line}"`)
352+ shapeData.push(v)
353+ }
354+ }
355+ break
356+ }
357+ case 'extensions': {
358+ const d = nums(line, '[EXTENSIONS]')
359+ extensions.set(d[0], { id: d[0], type: d[1], ref: d[2], next: d[3] })
360+ break
361+ }
362+ case 'extension-spec':
363+ currentSpec!.rows.push(tokens(line))
364+ break
365+ case 'signature': {
366+ const [key, value] = tokens(line)
367+ signature = signature ?? { type: '', hash: '' }
368+ if (key === 'Type') signature.type = value
369+ else if (key === 'Hash') signature.hash = value
370+ break
371+ }
372+ case 'none':
373+ case 'skip':
374+ break
375+ }
376+ }
377+ if (section === 'shapes') flushShape()
378+
379+ if (!version) throw new SeqParseError('not a pulseq file: no [VERSION] section found')
380+ if (version.combined < 1004000) {
381+ throw new SeqParseError(
382+ `pulseq format ${version.major}.${version.minor}.${version.revision} is not supported (1.4.0 or later required)`,
383+ )
384+ }
385+
386+ const rasters = {
387+ blockDuration:
388+ (definitions.get('BlockDurationRaster') as number[])?.[0] ?? DEFAULT_RASTERS.blockDuration,
389+ gradient:
390+ (definitions.get('GradientRasterTime') as number[])?.[0] ?? DEFAULT_RASTERS.gradient,
391+ rf: (definitions.get('RadiofrequencyRasterTime') as number[])?.[0] ?? DEFAULT_RASTERS.rf,
392+ adc: (definitions.get('AdcRasterTime') as number[])?.[0] ?? DEFAULT_RASTERS.adc,
393+ }
394+
395+ let totalDuration = 0
396+ for (const row of blockRows) {
397+ // v1.4+: id dur rf gx gy gz adc ext
398+ if (row.length < 8) throw new SeqParseError(`malformed [BLOCKS] row (${row.length} columns)`)
399+ const duration = row[1] * rasters.blockDuration
400+ blocks.push({
401+ id: row[0],
402+ duration,
403+ rfId: row[2],
404+ gxId: row[3],
405+ gyId: row[4],
406+ gzId: row[5],
407+ adcId: row[6],
408+ extId: row[7],
409+ })
410+ totalDuration += duration
411+ }
412+
413+ if (signature?.type?.toLowerCase() === 'md5') {
414+ // The hash covers the file up to (excluding) the newline that precedes
415+ // [SIGNATURE] — that newline was added together with the section.
416+ const idx = text.indexOf('\n[SIGNATURE]')
417+ if (idx >= 0) {
418+ signature.valid = md5Hex(text.slice(0, idx)) === signature.hash.toLowerCase()
419+ }
420+ }
421+
422+ return {
423+ version,
424+ definitions,
425+ rasters,
426+ blocks,
427+ rf,
428+ grads,
429+ adcs,
430+ shapes,
431+ extensions,
432+ extensionSpecs,
433+ signature,
434+ totalDuration,
435+ }
436+}
src/seq/reconstruct.tsadded+252−0View file
@@ -0,0 +1,252 @@
1+// Turn a ParsedSeq into plottable timelines, mirroring pulseq's
2+// Sequence.waveforms_and_times conventions:
3+// - trapezoids -> 4 vertices (3 when flat == 0), skipped when empty
4+// - arbitrary gradients on the centers raster -> edge points from
5+// first/last plus the raster-center samples
6+// - extended trapezoids (time shape on raster edges) -> vertices as stored
7+// - RF samples at (i+0.5)*rfRaster or the time shape; phase includes the
8+// phase/frequency offsets
9+// Series are piecewise-linear polylines; NaN values break the line.
10+import type { ParsedSeq } from './types.ts'
11+
12+export interface Series {
13+ t: Float64Array
14+ v: Float64Array
15+}
16+
17+export interface BlockSpan {
18+ index: number
19+ id: number
20+ start: number
21+ duration: number
22+}
23+
24+export interface AdcSpan {
25+ blockIndex: number
26+ start: number
27+ end: number
28+ num: number
29+ dwell: number
30+}
31+
32+export interface RfPulseSpan {
33+ blockIndex: number
34+ start: number
35+ end: number
36+ /** Absolute time of the pulse center (NaN for v1.4 files) */
37+ center: number
38+ use: string
39+}
40+
41+export interface Reconstructed {
42+ duration: number
43+ blockSpans: BlockSpan[]
44+ gx: Series
45+ gy: Series
46+ gz: Series
47+ /** |B1| in Hz */
48+ rfMag: Series
49+ /** rad, NaN between pulses */
50+ rfPhase: Series
51+ adcSpans: AdcSpan[]
52+ rfSpans: RfPulseSpan[]
53+ stats: {
54+ maxGrad: { gx: number; gy: number; gz: number }
55+ maxSlew: { gx: number; gy: number; gz: number }
56+ rfCount: number
57+ adcCount: number
58+ adcSamples: number
59+ }
60+}
61+
62+const EPS = 1e-9
63+
64+class SeriesBuilder {
65+ t: number[] = []
66+ v: number[] = []
67+ push(t: number, v: number) {
68+ this.t.push(t)
69+ this.v.push(v)
70+ }
71+ breakLine() {
72+ if (this.v.length > 0 && !Number.isNaN(this.v[this.v.length - 1])) {
73+ this.push(this.t[this.t.length - 1], NaN)
74+ }
75+ }
76+ build(): Series {
77+ return { t: Float64Array.from(this.t), v: Float64Array.from(this.v) }
78+ }
79+}
80+
81+function wrapToPi(x: number): number {
82+ const w = ((x + Math.PI) % (2 * Math.PI)) - Math.PI
83+ return w < -Math.PI ? w + 2 * Math.PI : w
84+}
85+
86+export function reconstruct(seq: ParsedSeq): Reconstructed {
87+ const { rasters } = seq
88+ const gradBuilders = {
89+ gx: new SeriesBuilder(),
90+ gy: new SeriesBuilder(),
91+ gz: new SeriesBuilder(),
92+ }
93+ const rfMag = new SeriesBuilder()
94+ const rfPhase = new SeriesBuilder()
95+ const blockSpans: BlockSpan[] = []
96+ const adcSpans: AdcSpan[] = []
97+ const rfSpans: RfPulseSpan[] = []
98+
99+ const channels = ['gx', 'gy', 'gz'] as const
100+
101+ // Zero anchors so empty channels still draw a baseline
102+ for (const ch of channels) gradBuilders[ch].push(0, 0)
103+ rfMag.push(0, 0)
104+
105+ let t0 = 0
106+ for (let bi = 0; bi < seq.blocks.length; bi++) {
107+ const block = seq.blocks[bi]
108+ blockSpans.push({ index: bi, id: block.id, start: t0, duration: block.duration })
109+
110+ for (const ch of channels) {
111+ const gid = ch === 'gx' ? block.gxId : ch === 'gy' ? block.gyId : block.gzId
112+ if (gid === 0) continue
113+ const grad = seq.grads.get(gid)
114+ if (!grad) continue
115+ const b = gradBuilders[ch]
116+ const gt0 = t0 + grad.delay
117+ if (grad.kind === 'trap') {
118+ if (Math.abs(grad.flat) > EPS) {
119+ b.push(gt0, 0)
120+ b.push(gt0 + grad.rise, grad.amp)
121+ b.push(gt0 + grad.rise + grad.flat, grad.amp)
122+ b.push(gt0 + grad.rise + grad.flat + grad.fall, 0)
123+ } else if (Math.abs(grad.rise) > EPS && Math.abs(grad.fall) > EPS) {
124+ b.push(gt0, 0)
125+ b.push(gt0 + grad.rise, grad.amp)
126+ b.push(gt0 + grad.rise + grad.fall, 0)
127+ }
128+ // else: empty gradient, skip
129+ } else {
130+ const shape = seq.shapes.get(grad.shapeId)
131+ if (!shape) continue
132+ const n = shape.numSamples
133+ const raster = rasters.gradient
134+ let tt: Float64Array
135+ if (grad.timeShapeId === 0) {
136+ tt = new Float64Array(n)
137+ for (let i = 0; i < n; i++) tt[i] = (i + 0.5) * raster
138+ } else {
139+ const timeShape = seq.shapes.get(grad.timeShapeId)
140+ if (!timeShape) continue
141+ tt = new Float64Array(n)
142+ for (let i = 0; i < n; i++) tt[i] = timeShape.samples[i] * raster
143+ }
144+ const onCenters = Math.abs(tt[0] - 0.5 * raster) < 1e-6 * raster
145+ if (onCenters) {
146+ // v1.4 files carry no first/last; fall back to the edge samples
147+ const first = Number.isNaN(grad.first) ? grad.amp * shape.samples[0] : grad.first
148+ const last =
149+ Number.isNaN(grad.last) ? grad.amp * shape.samples[n - 1] : grad.last
150+ const shapeDur = Math.ceil((tt[n - 1] - EPS) / raster) * raster
151+ b.push(gt0, first)
152+ for (let i = 0; i < n; i++) b.push(gt0 + tt[i], grad.amp * shape.samples[i])
153+ b.push(gt0 + shapeDur, last)
154+ } else {
155+ // extended trapezoid: vertices as stored
156+ for (let i = 0; i < n; i++) b.push(gt0 + tt[i], grad.amp * shape.samples[i])
157+ }
158+ }
159+ }
160+
161+ if (block.rfId !== 0) {
162+ const ev = seq.rf.get(block.rfId)
163+ const magShape = ev ? seq.shapes.get(ev.magShapeId) : undefined
164+ if (ev && magShape) {
165+ const n = magShape.numSamples
166+ const raster = rasters.rf
167+ const phaseShape = ev.phaseShapeId !== 0 ? seq.shapes.get(ev.phaseShapeId) : undefined
168+ let tt: Float64Array
169+ if (ev.timeShapeId === 0) {
170+ tt = new Float64Array(n)
171+ for (let i = 0; i < n; i++) tt[i] = (i + 0.5) * raster
172+ } else {
173+ const timeShape = seq.shapes.get(ev.timeShapeId)
174+ tt = new Float64Array(n)
175+ for (let i = 0; i < n; i++) tt[i] = (timeShape?.samples[i] ?? 0) * raster
176+ }
177+ const rt0 = t0 + ev.delay
178+ const end = rt0 + Math.ceil((tt[n - 1] - EPS) / raster) * raster
179+ rfMag.push(rt0, 0)
180+ rfPhase.breakLine()
181+ for (let i = 0; i < n; i++) {
182+ const t = rt0 + tt[i]
183+ rfMag.push(t, Math.abs(ev.amp * magShape.samples[i]))
184+ const ph = 2 * Math.PI * (phaseShape?.samples[i] ?? 0) + ev.phase + 2 * Math.PI * ev.freq * tt[i]
185+ rfPhase.push(t, wrapToPi(ph))
186+ }
187+ rfPhase.breakLine()
188+ rfMag.push(end, 0)
189+ rfSpans.push({
190+ blockIndex: bi,
191+ start: rt0,
192+ end,
193+ center: Number.isNaN(ev.center) ? NaN : rt0 + ev.center,
194+ use: ev.use,
195+ })
196+ }
197+ }
198+
199+ if (block.adcId !== 0) {
200+ const adc = seq.adcs.get(block.adcId)
201+ if (adc) {
202+ const start = t0 + adc.delay
203+ adcSpans.push({
204+ blockIndex: bi,
205+ start,
206+ end: start + adc.num * adc.dwell,
207+ num: adc.num,
208+ dwell: adc.dwell,
209+ })
210+ }
211+ }
212+
213+ t0 += block.duration
214+ }
215+
216+ for (const ch of channels) gradBuilders[ch].push(t0, 0)
217+ rfMag.push(t0, 0)
218+
219+ const stats = {
220+ maxGrad: { gx: 0, gy: 0, gz: 0 },
221+ maxSlew: { gx: 0, gy: 0, gz: 0 },
222+ rfCount: rfSpans.length,
223+ adcCount: adcSpans.length,
224+ adcSamples: adcSpans.reduce((acc, a) => acc + a.num, 0),
225+ }
226+ const series = {
227+ gx: gradBuilders.gx.build(),
228+ gy: gradBuilders.gy.build(),
229+ gz: gradBuilders.gz.build(),
230+ }
231+ for (const ch of channels) {
232+ const { t, v } = series[ch]
233+ for (let i = 0; i < v.length; i++) {
234+ if (Number.isNaN(v[i])) continue
235+ stats.maxGrad[ch] = Math.max(stats.maxGrad[ch], Math.abs(v[i]))
236+ if (i > 0 && !Number.isNaN(v[i - 1]) && t[i] > t[i - 1] + EPS) {
237+ stats.maxSlew[ch] = Math.max(stats.maxSlew[ch], Math.abs((v[i] - v[i - 1]) / (t[i] - t[i - 1])))
238+ }
239+ }
240+ }
241+
242+ return {
243+ duration: t0,
244+ blockSpans,
245+ ...series,
246+ rfMag: rfMag.build(),
247+ rfPhase: rfPhase.build(),
248+ adcSpans,
249+ rfSpans,
250+ stats,
251+ }
252+}
src/seq/types.tsadded+132−0View file
@@ -0,0 +1,132 @@
1+// Parsed representation of a pulseq .seq file (format v1.4.x / v1.5.x).
2+
3+export interface SeqVersion {
4+ major: number
5+ minor: number
6+ revision: string
7+ combined: number // 1000000*major + 1000*minor + numeric revision
8+}
9+
10+/** One [DEFINITIONS] entry: numeric values when every token parses, else raw string. */
11+export type DefinitionValue = number[] | string
12+
13+export interface RfEvent {
14+ id: number
15+ /** Peak amplitude, Hz */
16+ amp: number
17+ magShapeId: number
18+ phaseShapeId: number
19+ timeShapeId: number
20+ /** Center of the pulse relative to its start, s (NaN in v1.4 files) */
21+ center: number
22+ /** Delay from block start, s */
23+ delay: number
24+ freqPPM: number
25+ phasePPM: number
26+ /** Frequency offset, Hz */
27+ freq: number
28+ /** Phase offset, rad */
29+ phase: number
30+ /** Initial of excitation/refocusing/inversion/saturation/preparation/other/undefined */
31+ use: string
32+}
33+
34+export interface TrapGradEvent {
35+ id: number
36+ kind: 'trap'
37+ /** Hz/m */
38+ amp: number
39+ rise: number
40+ flat: number
41+ fall: number
42+ delay: number
43+}
44+
45+export interface ArbGradEvent {
46+ id: number
47+ kind: 'grad'
48+ /** Hz/m */
49+ amp: number
50+ /** Waveform value at the start/end edge, Hz/m (NaN in v1.4 files) */
51+ first: number
52+ last: number
53+ shapeId: number
54+ timeShapeId: number
55+ delay: number
56+}
57+
58+export type GradEvent = TrapGradEvent | ArbGradEvent
59+
60+export interface AdcEvent {
61+ id: number
62+ num: number
63+ /** s */
64+ dwell: number
65+ /** s */
66+ delay: number
67+ freqPPM: number
68+ phasePPM: number
69+ /** Hz */
70+ freq: number
71+ /** rad */
72+ phase: number
73+ phaseShapeId: number
74+}
75+
76+export interface Shape {
77+ id: number
78+ numSamples: number
79+ /** Decompressed samples (numSamples long) */
80+ samples: Float64Array
81+ /** Sample count as stored in the file (=== numSamples when uncompressed) */
82+ storedSamples: number
83+}
84+
85+/** One row of the [EXTENSIONS] linked lists: id -> (type, ref) + next row. */
86+export interface ExtensionRef {
87+ id: number
88+ type: number
89+ ref: number
90+ next: number
91+}
92+
93+/** One `extension NAME typeId` specification section, rows kept as tokens. */
94+export interface ExtensionSpec {
95+ name: string
96+ typeId: number
97+ rows: string[][]
98+}
99+
100+export interface SeqBlock {
101+ id: number
102+ /** s */
103+ duration: number
104+ rfId: number
105+ gxId: number
106+ gyId: number
107+ gzId: number
108+ adcId: number
109+ extId: number
110+}
111+
112+export interface ParsedSeq {
113+ version: SeqVersion
114+ /** Insertion-ordered [DEFINITIONS] */
115+ definitions: Map<string, DefinitionValue>
116+ rasters: {
117+ blockDuration: number
118+ gradient: number
119+ rf: number
120+ adc: number
121+ }
122+ blocks: SeqBlock[]
123+ rf: Map<number, RfEvent>
124+ grads: Map<number, GradEvent>
125+ adcs: Map<number, AdcEvent>
126+ shapes: Map<number, Shape>
127+ extensions: Map<number, ExtensionRef>
128+ extensionSpecs: ExtensionSpec[]
129+ signature?: { type: string; hash: string; valid?: boolean }
130+ /** s, sum of block durations */
131+ totalDuration: number
132+}
src/viewer/BlockInspector.tsxadded+148−0View file
@@ -0,0 +1,148 @@
1+// Numeric details of one selected block: every event it references, with
2+// the library parameters spelled out in display units.
3+import type { ParsedSeq } from '../seq/types.ts'
4+import type { Reconstructed } from '../seq/reconstruct.ts'
5+import { formatNumber, formatTime, GAMMA } from './format'
6+
7+export interface BlockInspectorProps {
8+ seq: ParsedSeq
9+ rec: Reconstructed
10+ blockIndex: number
11+ onNavigate: (index: number) => void
12+}
13+
14+const RF_USE: Record<string, string> = {
15+ e: 'excitation',
16+ r: 'refocusing',
17+ i: 'inversion',
18+ s: 'saturation',
19+ p: 'preparation',
20+ o: 'other',
21+ u: 'undefined',
22+}
23+
24+function gradCell(seq: ParsedSeq, id: number): React.ReactNode {
25+ if (id === 0) return <span className="muted">—</span>
26+ const g = seq.grads.get(id)
27+ if (!g) return <span className="muted">missing #{id}</span>
28+ const mTm = (g.amp / GAMMA) * 1e3
29+ if (g.kind === 'trap') {
30+ return (
31+ <>
32+ trap #{g.id}: {formatNumber(g.amp, 6)} Hz/m ({mTm.toFixed(2)} mT/m), rise{' '}
33+ {formatTime(g.rise, 3)}, flat {formatTime(g.flat, 3)}, fall {formatTime(g.fall, 3)}
34+ {g.delay > 0 ? `, delay ${formatTime(g.delay, 3)}` : ''}
35+ </>
36+ )
37+ }
38+ const shape = seq.shapes.get(g.shapeId)
39+ return (
40+ <>
41+ arbitrary #{g.id}: peak {formatNumber(g.amp, 6)} Hz/m ({mTm.toFixed(2)} mT/m), shape #
42+ {g.shapeId} ({shape?.numSamples ?? '?'} samples)
43+ {g.timeShapeId !== 0 ? `, time shape #${g.timeShapeId}` : ''}
44+ {g.delay > 0 ? `, delay ${formatTime(g.delay, 3)}` : ''}
45+ </>
46+ )
47+}
48+
49+export default function BlockInspector({ seq, rec, blockIndex, onNavigate }: BlockInspectorProps) {
50+ const block = seq.blocks[blockIndex]
51+ const span = rec.blockSpans[blockIndex]
52+ if (!block || !span) return null
53+
54+ const rf = block.rfId !== 0 ? seq.rf.get(block.rfId) : undefined
55+ const adc = block.adcId !== 0 ? seq.adcs.get(block.adcId) : undefined
56+
57+ // Resolve the extension linked list into readable entries
58+ const extEntries: string[] = []
59+ let extId = block.extId
60+ let guard = 0
61+ while (extId !== 0 && guard++ < 100) {
62+ const ref = seq.extensions.get(extId)
63+ if (!ref) break
64+ const spec = seq.extensionSpecs.find((s) => s.typeId === ref.type)
65+ if (spec) {
66+ const row = spec.rows.find((r) => Number(r[0]) === ref.ref)
67+ extEntries.push(`${spec.name} ${row ? row.slice(1).join(' ') : `#${ref.ref}`}`)
68+ } else {
69+ extEntries.push(`type ${ref.type} #${ref.ref}`)
70+ }
71+ extId = ref.next
72+ }
73+
74+ return (
75+ <div className="inspector">
76+ <div className="nav">
77+ <button onClick={() => onNavigate(blockIndex - 1)} disabled={blockIndex <= 0}>
78+ ←
79+ </button>
80+ <button
81+ onClick={() => onNavigate(blockIndex + 1)}
82+ disabled={blockIndex >= seq.blocks.length - 1}
83+ >
84+ →
85+ </button>
86+ <span className="where">
87+ block {block.id} of {seq.blocks.length} · start {formatTime(span.start)} · duration{' '}
88+ {formatTime(span.duration)}
89+ </span>
90+ </div>
91+ <table>
92+ <tbody>
93+ <tr>
94+ <th>RF</th>
95+ <td>
96+ {rf ? (
97+ <>
98+ #{rf.id}: {RF_USE[rf.use] ?? rf.use}, peak {formatNumber(rf.amp, 5)} Hz, delay{' '}
99+ {formatTime(rf.delay, 3)}
100+ {Number.isFinite(rf.center) ? `, center ${formatTime(rf.center, 3)}` : ''}
101+ {rf.freq !== 0 ? `, freq ${formatNumber(rf.freq, 5)} Hz` : ''}
102+ {rf.phase !== 0 ? `, phase ${formatNumber(rf.phase, 5)} rad` : ''}
103+ {`, shapes m#${rf.magShapeId}/p#${rf.phaseShapeId}` +
104+ (rf.timeShapeId !== 0 ? `/t#${rf.timeShapeId}` : '')}
105+ </>
106+ ) : (
107+ <span className="muted">—</span>
108+ )}
109+ </td>
110+ </tr>
111+ <tr>
112+ <th>GX</th>
113+ <td>{gradCell(seq, block.gxId)}</td>
114+ </tr>
115+ <tr>
116+ <th>GY</th>
117+ <td>{gradCell(seq, block.gyId)}</td>
118+ </tr>
119+ <tr>
120+ <th>GZ</th>
121+ <td>{gradCell(seq, block.gzId)}</td>
122+ </tr>
123+ <tr>
124+ <th>ADC</th>
125+ <td>
126+ {adc ? (
127+ <>
128+ #{adc.id}: {adc.num} samples, dwell {formatNumber(adc.dwell * 1e9, 6)} ns, delay{' '}
129+ {formatTime(adc.delay, 3)}
130+ {adc.freq !== 0 ? `, freq ${formatNumber(adc.freq, 5)} Hz` : ''}
131+ {adc.phase !== 0 ? `, phase ${formatNumber(adc.phase, 5)} rad` : ''}
132+ </>
133+ ) : (
134+ <span className="muted">—</span>
135+ )}
136+ </td>
137+ </tr>
138+ <tr>
139+ <th>Extensions</th>
140+ <td>
141+ {extEntries.length > 0 ? extEntries.join(' · ') : <span className="muted">—</span>}
142+ </td>
143+ </tr>
144+ </tbody>
145+ </table>
146+ </div>
147+ )
148+}
src/viewer/ReportPanel.tsxadded+54−0View file
@@ -0,0 +1,54 @@
1+import type { ParsedSeq } from '../seq/types.ts'
2+import type { Reconstructed } from '../seq/reconstruct.ts'
3+import { formatGrad, formatNumber, formatSlew, formatTime } from './format'
4+
5+export interface ReportPanelProps {
6+ seq: ParsedSeq
7+ rec: Reconstructed
8+}
9+
10+export default function ReportPanel({ seq, rec }: ReportPanelProps) {
11+ const name = seq.definitions.get('Name')
12+ const fov = seq.definitions.get('FOV')
13+ const maxGrad = Math.max(rec.stats.maxGrad.gx, rec.stats.maxGrad.gy, rec.stats.maxGrad.gz)
14+ const maxSlew = Math.max(rec.stats.maxSlew.gx, rec.stats.maxSlew.gy, rec.stats.maxSlew.gz)
15+
16+ const items: [string, React.ReactNode][] = [
17+ ['Name', typeof name === 'string' ? name : '—'],
18+ ['Format', `v${seq.version.major}.${seq.version.minor}.${seq.version.revision}`],
19+ ['Duration', formatTime(rec.duration)],
20+ ['Blocks', String(seq.blocks.length)],
21+ ['RF pulses', String(rec.stats.rfCount)],
22+ ['ADC', `${rec.stats.adcCount} windows / ${rec.stats.adcSamples} samples`],
23+ ]
24+ if (Array.isArray(fov)) {
25+ items.push(['FOV', fov.map((v) => `${formatNumber(v * 1e3, 4)}`).join(' × ') + ' mm'])
26+ }
27+ if (maxGrad > 0) items.push(['Peak gradient', formatGrad(maxGrad)])
28+ if (maxSlew > 0) items.push(['Peak slew', formatSlew(maxSlew)])
29+
30+ return (
31+ <div className="report">
32+ {items.map(([label, value]) => (
33+ <div className="item" key={label}>
34+ <b>{label}</b>
35+ <span>{value}</span>
36+ </div>
37+ ))}
38+ <div className="item">
39+ <b>Signature</b>
40+ <span>
41+ {seq.signature ? (
42+ seq.signature.valid ? (
43+ <span className="badge good">md5 verified</span>
44+ ) : (
45+ <span className="badge bad">md5 mismatch</span>
46+ )
47+ ) : (
48+ <span className="badge neutral">none</span>
49+ )}
50+ </span>
51+ </div>
52+ </div>
53+ )
54+}
src/viewer/Timeline.tsxadded+448−0View file
@@ -0,0 +1,448 @@
1+// Interactive sequence timeline: stacked lanes (RF magnitude, RF phase,
2+// gx/gy/gz, ADC) over a shared time axis with wheel zoom, drag pan, and
3+// click-to-select-block. Canvas-rendered with per-pixel min/max decimation
4+// so full sequences (hundreds of thousands of RF samples) stay responsive.
5+import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
6+import type { Reconstructed, Series } from '../seq/reconstruct.ts'
7+import { formatTick, formatTime } from './format'
8+
9+export interface TimelineProps {
10+ rec: Reconstructed
11+ selectedBlock: number | null
12+ onSelectBlock: (index: number | null) => void
13+}
14+
15+interface Lane {
16+ label: string
17+ unit: string
18+ color: string
19+ height: number
20+ kind: 'series' | 'adc'
21+ series?: Series
22+ /** y range; symmetric lanes use [-max, max] */
23+ vMin: number
24+ vMax: number
25+}
26+
27+const AXIS_HEIGHT = 26
28+const GUTTER = 58
29+const MIN_SPAN = 2e-6
30+
31+function laneRange(s: Series, symmetric: boolean): [number, number] {
32+ let max = 0
33+ for (let i = 0; i < s.v.length; i++) {
34+ const v = s.v[i]
35+ if (!Number.isNaN(v)) max = Math.max(max, Math.abs(v))
36+ }
37+ if (max === 0) max = 1
38+ max *= 1.08
39+ return symmetric ? [-max, max] : [0, max]
40+}
41+
42+/** Nice tick step: 1/2/5 * 10^k so that span/step is 4..10 ticks. */
43+function tickStep(span: number): number {
44+ const raw = span / 7
45+ const mag = Math.pow(10, Math.floor(Math.log10(raw)))
46+ for (const m of [1, 2, 5, 10]) {
47+ if (raw <= m * mag) return m * mag
48+ }
49+ return 10 * mag
50+}
51+
52+function lowerBound(t: Float64Array, x: number): number {
53+ let lo = 0
54+ let hi = t.length
55+ while (lo < hi) {
56+ const mid = (lo + hi) >> 1
57+ if (t[mid] < x) lo = mid + 1
58+ else hi = mid
59+ }
60+ return lo
61+}
62+
63+export default function Timeline({ rec, selectedBlock, onSelectBlock }: TimelineProps) {
64+ const hostRef = useRef<HTMLDivElement>(null)
65+ const canvasRef = useRef<HTMLCanvasElement>(null)
66+ const [view, setView] = useState<[number, number]>([0, rec.duration || 1])
67+ const [width, setWidth] = useState(800)
68+ const [hoverT, setHoverT] = useState<number | null>(null)
69+ const [panning, setPanning] = useState(false)
70+ const dragRef = useRef<{ x0: number; view0: [number, number]; moved: boolean } | null>(null)
71+
72+ // Reset the view when a different sequence is shown
73+ useEffect(() => {
74+ setView([0, rec.duration || 1])
75+ }, [rec])
76+
77+ const lanes: Lane[] = useMemo(() => {
78+ const [rfLo, rfHi] = laneRange(rec.rfMag, false)
79+ const gx = laneRange(rec.gx, true)
80+ const gy = laneRange(rec.gy, true)
81+ const gz = laneRange(rec.gz, true)
82+ return [
83+ { label: 'RF', unit: 'Hz', color: '#ffc861', height: 88, kind: 'series', series: rec.rfMag, vMin: rfLo, vMax: rfHi },
84+ { label: 'RF φ', unit: 'rad', color: '#c49bff', height: 54, kind: 'series', series: rec.rfPhase, vMin: -Math.PI * 1.15, vMax: Math.PI * 1.15 },
85+ { label: 'GX', unit: 'Hz/m', color: '#ff6b6b', height: 72, kind: 'series', series: rec.gx, vMin: gx[0], vMax: gx[1] },
86+ { label: 'GY', unit: 'Hz/m', color: '#51cf66', height: 72, kind: 'series', series: rec.gy, vMin: gy[0], vMax: gy[1] },
87+ { label: 'GZ', unit: 'Hz/m', color: '#43a7f5', height: 72, kind: 'series', series: rec.gz, vMin: gz[0], vMax: gz[1] },
88+ { label: 'ADC', unit: '', color: '#2fd3c6', height: 36, kind: 'adc', vMin: 0, vMax: 1 },
89+ ]
90+ }, [rec])
91+
92+ const totalHeight = lanes.reduce((acc, l) => acc + l.height, 0) + AXIS_HEIGHT
93+
94+ useEffect(() => {
95+ const host = hostRef.current
96+ if (!host) return
97+ const ro = new ResizeObserver(() => setWidth(host.clientWidth))
98+ ro.observe(host)
99+ setWidth(host.clientWidth)
100+ return () => ro.disconnect()
101+ }, [])
102+
103+ const clampView = useCallback(
104+ (t0: number, t1: number): [number, number] => {
105+ const total = rec.duration || 1
106+ let span = Math.min(Math.max(t1 - t0, MIN_SPAN), total)
107+ let lo = t0
108+ if (lo < 0) lo = 0
109+ if (lo + span > total) lo = total - span
110+ return [lo, lo + span]
111+ },
112+ [rec.duration],
113+ )
114+
115+ // Keep the selected block in view (e.g. when navigated via the inspector
116+ // arrows). Pans to center it, zooming out only if it's wider than the view.
117+ // No-op when the block is already fully visible, so clicking a visible block
118+ // doesn't shift the timeline.
119+ useEffect(() => {
120+ if (selectedBlock === null) return
121+ const b = rec.blockSpans[selectedBlock]
122+ if (!b) return
123+ const bStart = b.start
124+ const bEnd = b.start + b.duration
125+ setView((v) => {
126+ const [t0, t1] = v
127+ if (bStart >= t0 && bEnd <= t1) return v // already visible
128+ const span = t1 - t0
129+ const newSpan = b.duration > span ? b.duration * 1.3 : span
130+ const center = (bStart + bEnd) / 2
131+ return clampView(center - newSpan / 2, center + newSpan / 2)
132+ })
133+ }, [selectedBlock, rec.blockSpans, clampView])
134+
135+ // ── rendering ─────────────────────────────────────────────────────────
136+ useEffect(() => {
137+ const canvas = canvasRef.current
138+ if (!canvas) return
139+ const dpr = window.devicePixelRatio || 1
140+ canvas.width = Math.round(width * dpr)
141+ canvas.height = Math.round(totalHeight * dpr)
142+ canvas.style.height = `${totalHeight}px`
143+ const ctx = canvas.getContext('2d')
144+ if (!ctx) return
145+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
146+
147+ const [t0, t1] = view
148+ const span = t1 - t0
149+ const plotW = width - GUTTER
150+ const xOf = (t: number) => GUTTER + ((t - t0) / span) * plotW
151+
152+ ctx.fillStyle = '#12161d'
153+ ctx.fillRect(0, 0, width, totalHeight)
154+
155+ // Selected block highlight (behind everything)
156+ if (selectedBlock !== null && rec.blockSpans[selectedBlock]) {
157+ const b = rec.blockSpans[selectedBlock]
158+ const x0 = Math.max(GUTTER, xOf(b.start))
159+ const x1 = Math.min(width, xOf(b.start + b.duration))
160+ if (x1 > GUTTER && x0 < width) {
161+ ctx.fillStyle = '#4da3ff1c'
162+ ctx.fillRect(x0, 0, Math.max(x1 - x0, 2), totalHeight - AXIS_HEIGHT)
163+ }
164+ }
165+
166+ // Block boundaries when they are resolvable
167+ const spans = rec.blockSpans
168+ const iFirst = Math.max(0, spans.findIndex((b) => b.start + b.duration >= t0))
169+ let visibleBlocks = 0
170+ for (let i = iFirst; i < spans.length && spans[i].start <= t1; i++) visibleBlocks++
171+ if (visibleBlocks > 0 && visibleBlocks < plotW / 6) {
172+ ctx.strokeStyle = '#ffffff10'
173+ ctx.beginPath()
174+ for (let i = iFirst; i < spans.length && spans[i].start <= t1; i++) {
175+ const x = Math.round(xOf(spans[i].start)) + 0.5
176+ if (x < GUTTER) continue
177+ ctx.moveTo(x, 0)
178+ ctx.lineTo(x, totalHeight - AXIS_HEIGHT)
179+ }
180+ ctx.stroke()
181+ }
182+
183+ let y0 = 0
184+ for (const lane of lanes) {
185+ const h = lane.height
186+ // separator + baseline
187+ ctx.strokeStyle = '#262d38'
188+ ctx.beginPath()
189+ ctx.moveTo(0, y0 + h - 0.5)
190+ ctx.lineTo(width, y0 + h - 0.5)
191+ ctx.stroke()
192+
193+ const pad = 6
194+ const yOf = (v: number) =>
195+ y0 + h - pad - ((v - lane.vMin) / (lane.vMax - lane.vMin)) * (h - 2 * pad)
196+
197+ if (lane.vMin < 0) {
198+ ctx.strokeStyle = '#ffffff14'
199+ ctx.beginPath()
200+ ctx.moveTo(GUTTER, yOf(0) + 0.5)
201+ ctx.lineTo(width, yOf(0) + 0.5)
202+ ctx.stroke()
203+ }
204+
205+ if (lane.kind === 'adc') {
206+ ctx.fillStyle = lane.color + '55'
207+ ctx.strokeStyle = lane.color
208+ for (const a of rec.adcSpans) {
209+ if (a.end < t0 || a.start > t1) continue
210+ const x0 = Math.max(GUTTER, xOf(a.start))
211+ const x1 = Math.min(width, xOf(a.end))
212+ ctx.fillRect(x0, y0 + pad, Math.max(x1 - x0, 1.5), h - 2 * pad)
213+ // sample ticks when resolvable
214+ const pxPerSample = (x1 - x0) / a.num
215+ if (pxPerSample > 4) {
216+ ctx.beginPath()
217+ for (let k = 0; k < a.num; k++) {
218+ const x = xOf(a.start + (k + 0.5) * a.dwell)
219+ if (x < GUTTER || x > width) continue
220+ ctx.moveTo(x, y0 + h / 2 - 3)
221+ ctx.lineTo(x, y0 + h / 2 + 3)
222+ }
223+ ctx.stroke()
224+ }
225+ }
226+ } else if (lane.series) {
227+ drawSeries(ctx, lane.series, xOf, yOf, t0, t1, plotW, lane.color)
228+ }
229+
230+ // lane label
231+ ctx.fillStyle = '#12161dd8'
232+ ctx.fillRect(0, y0, GUTTER - 6, h - 1)
233+ ctx.fillStyle = lane.color
234+ ctx.font = '600 11px system-ui'
235+ ctx.textBaseline = 'top'
236+ ctx.fillText(lane.label, 8, y0 + 6)
237+ if (lane.unit) {
238+ ctx.fillStyle = '#8b95a5'
239+ ctx.font = '10px system-ui'
240+ ctx.fillText(lane.unit, 8, y0 + 19)
241+ }
242+ y0 += h
243+ }
244+
245+ // ── time axis ──
246+ const axisY = totalHeight - AXIS_HEIGHT
247+ ctx.fillStyle = '#0f1115'
248+ ctx.fillRect(0, axisY, width, AXIS_HEIGHT)
249+ ctx.strokeStyle = '#262d38'
250+ ctx.beginPath()
251+ ctx.moveTo(0, axisY + 0.5)
252+ ctx.lineTo(width, axisY + 0.5)
253+ ctx.stroke()
254+ const step = tickStep(span)
255+ const first = Math.ceil(t0 / step) * step
256+ ctx.font = '10.5px system-ui'
257+ ctx.textBaseline = 'top'
258+ for (let t = first; t <= t1 + 1e-12; t += step) {
259+ const x = xOf(t)
260+ if (x < GUTTER - 1) continue
261+ ctx.strokeStyle = '#3a4452'
262+ ctx.beginPath()
263+ ctx.moveTo(x + 0.5, axisY)
264+ ctx.lineTo(x + 0.5, axisY + 5)
265+ ctx.stroke()
266+ ctx.fillStyle = '#8b95a5'
267+ const label = formatTick(t, step)
268+ ctx.fillText(label, x - ctx.measureText(label).width / 2, axisY + 8)
269+ }
270+
271+ // hover crosshair
272+ if (hoverT !== null && hoverT >= t0 && hoverT <= t1) {
273+ const x = xOf(hoverT)
274+ ctx.strokeStyle = '#ffffff30'
275+ ctx.beginPath()
276+ ctx.moveTo(x + 0.5, 0)
277+ ctx.lineTo(x + 0.5, axisY)
278+ ctx.stroke()
279+ ctx.fillStyle = '#d7dde7'
280+ ctx.font = '11px system-ui'
281+ const label = formatTime(hoverT)
282+ const tw = ctx.measureText(label).width
283+ const lx = Math.min(Math.max(x - tw / 2, GUTTER), width - tw - 4)
284+ ctx.fillStyle = '#1a212bee'
285+ ctx.fillRect(lx - 4, axisY + 6, tw + 8, 15)
286+ ctx.fillStyle = '#d7dde7'
287+ ctx.fillText(label, lx, axisY + 9)
288+ }
289+ }, [view, width, lanes, rec, selectedBlock, hoverT, totalHeight])
290+
291+ // ── interactions ──────────────────────────────────────────────────────
292+ const tAtClientX = useCallback(
293+ (clientX: number): number => {
294+ const rect = canvasRef.current!.getBoundingClientRect()
295+ const x = clientX - rect.left
296+ const [t0, t1] = view
297+ return t0 + ((x - GUTTER) / (width - GUTTER)) * (t1 - t0)
298+ },
299+ [view, width],
300+ )
301+
302+ // Native wheel listener: React's onWheel is passive and can't preventDefault
303+ useEffect(() => {
304+ const canvas = canvasRef.current
305+ if (!canvas) return
306+ const onWheel = (e: WheelEvent) => {
307+ e.preventDefault()
308+ const t = tAtClientX(e.clientX)
309+ setView(([t0, t1]) => {
310+ const factor = Math.exp(e.deltaY * 0.002)
311+ return clampView(t + (t0 - t) * factor, t + (t1 - t) * factor)
312+ })
313+ }
314+ canvas.addEventListener('wheel', onWheel, { passive: false })
315+ return () => canvas.removeEventListener('wheel', onWheel)
316+ }, [tAtClientX, clampView])
317+
318+ const onMouseDown = (e: React.MouseEvent) => {
319+ dragRef.current = { x0: e.clientX, view0: view, moved: false }
320+ setPanning(true)
321+ }
322+
323+ useEffect(() => {
324+ if (!panning) return
325+ const onMove = (e: MouseEvent) => {
326+ const drag = dragRef.current
327+ if (!drag) return
328+ const dx = e.clientX - drag.x0
329+ if (Math.abs(dx) > 3) drag.moved = true
330+ const [t0, t1] = drag.view0
331+ const dt = (dx / (width - GUTTER)) * (t1 - t0)
332+ setView(clampView(t0 - dt, t1 - dt))
333+ }
334+ const onUp = (e: MouseEvent) => {
335+ const drag = dragRef.current
336+ dragRef.current = null
337+ setPanning(false)
338+ if (drag && !drag.moved) {
339+ // click: select the block at this time
340+ const t = tAtClientX(e.clientX)
341+ const idx = rec.blockSpans.findIndex((b) => t >= b.start && t < b.start + b.duration)
342+ onSelectBlock(idx >= 0 ? idx : null)
343+ }
344+ }
345+ window.addEventListener('mousemove', onMove)
346+ window.addEventListener('mouseup', onUp)
347+ return () => {
348+ window.removeEventListener('mousemove', onMove)
349+ window.removeEventListener('mouseup', onUp)
350+ }
351+ }, [panning, width, clampView, tAtClientX, rec.blockSpans, onSelectBlock])
352+
353+ return (
354+ <div className="timeline-host" ref={hostRef}>
355+ <canvas
356+ ref={canvasRef}
357+ className={`timeline-canvas${panning ? ' panning' : ''}`}
358+ onMouseDown={onMouseDown}
359+ onDoubleClick={() => setView([0, rec.duration || 1])}
360+ onMouseMove={(e) => setHoverT(tAtClientX(e.clientX))}
361+ onMouseLeave={() => setHoverT(null)}
362+ />
363+ </div>
364+ )
365+}
366+
367+/** Draw a piecewise-linear series with per-pixel min/max decimation. */
368+function drawSeries(
369+ ctx: CanvasRenderingContext2D,
370+ s: Series,
371+ xOf: (t: number) => number,
372+ yOf: (v: number) => number,
373+ t0: number,
374+ t1: number,
375+ plotW: number,
376+ color: string,
377+) {
378+ const { t, v } = s
379+ if (t.length === 0) return
380+ let i0 = Math.max(0, lowerBound(t, t0) - 1)
381+ let i1 = Math.min(t.length - 1, lowerBound(t, t1) + 1)
382+
383+ ctx.save()
384+ ctx.beginPath()
385+ ctx.rect(GUTTER, 0, plotW, ctx.canvas.height)
386+ ctx.clip()
387+ ctx.strokeStyle = color
388+ ctx.lineWidth = 1.2
389+ ctx.lineJoin = 'round'
390+ ctx.beginPath()
391+
392+ const dense = i1 - i0 > plotW * 3
393+ if (dense) {
394+ // min/max per pixel column; each column's range includes the previous
395+ // column's closing value so adjacent strokes connect
396+ let px = -1
397+ let lo = 0
398+ let hi = 0
399+ let close = 0
400+ let open = false
401+ const flush = () => {
402+ if (open && px >= 0) {
403+ ctx.moveTo(px + 0.5, yOf(lo))
404+ ctx.lineTo(px + 0.5, yOf(hi))
405+ }
406+ }
407+ for (let i = i0; i <= i1; i++) {
408+ if (Number.isNaN(v[i])) {
409+ flush()
410+ px = -1
411+ open = false
412+ continue
413+ }
414+ const x = Math.round(xOf(t[i]))
415+ if (x !== px) {
416+ flush()
417+ const carry = open ? close : v[i]
418+ px = x
419+ lo = Math.min(v[i], carry)
420+ hi = Math.max(v[i], carry)
421+ open = true
422+ } else {
423+ if (v[i] < lo) lo = v[i]
424+ if (v[i] > hi) hi = v[i]
425+ }
426+ close = v[i]
427+ }
428+ flush()
429+ } else {
430+ let pen = false
431+ for (let i = i0; i <= i1; i++) {
432+ if (Number.isNaN(v[i])) {
433+ pen = false
434+ continue
435+ }
436+ const x = xOf(t[i])
437+ const y = yOf(v[i])
438+ if (!pen) {
439+ ctx.moveTo(x, y)
440+ pen = true
441+ } else {
442+ ctx.lineTo(x, y)
443+ }
444+ }
445+ }
446+ ctx.stroke()
447+ ctx.restore()
448+}
src/viewer/format.tsadded+40−0View file
@@ -0,0 +1,40 @@
1+// Formatting helpers shared by the viewer components.
2+
3+/** Gyromagnetic ratio of 1H, Hz/T — used to express gradients in mT/m. */
4+export const GAMMA = 42.576e6
5+
6+/** Format a time in seconds with an adaptive unit. */
7+export function formatTime(t: number, digits = 4): string {
8+ const a = Math.abs(t)
9+ if (a >= 1) return `${t.toFixed(digits)} s`
10+ if (a >= 1e-3) return `${(t * 1e3).toPrecision(digits)} ms`
11+ if (a === 0) return '0'
12+ return `${(t * 1e6).toPrecision(digits)} µs`
13+}
14+
15+/** Tick label for a time axis where the tick spacing is `step` seconds. */
16+export function formatTick(t: number, step: number): string {
17+ if (step >= 0.1) return `${trimZeros(t.toFixed(2))} s`
18+ if (step >= 1e-4) return `${trimZeros((t * 1e3).toFixed(3))} ms`
19+ return `${trimZeros((t * 1e6).toFixed(2))} µs`
20+}
21+
22+function trimZeros(s: string): string {
23+ return s.includes('.') ? s.replace(/\.?0+$/, '') : s
24+}
25+
26+/** Gradient amplitude, Hz/m -> compact display in both unit systems. */
27+export function formatGrad(hzPerM: number): string {
28+ const mTm = (hzPerM / GAMMA) * 1e3
29+ return `${(hzPerM / 1e3).toFixed(1)} kHz/m (${mTm.toFixed(2)} mT/m)`
30+}
31+
32+/** Slew rate, Hz/m/s -> display in T/m/s. */
33+export function formatSlew(hzPerMPerS: number): string {
34+ return `${(hzPerMPerS / GAMMA).toFixed(1)} T/m/s`
35+}
36+
37+export function formatNumber(v: number, digits = 6): string {
38+ if (Number.isInteger(v)) return String(v)
39+ return String(Number(v.toPrecision(digits)))
40+}
test-data/golden/fid.seqadded+93−0View file
@@ -0,0 +1,93 @@
1+# Pulseq sequence file
2+# Created by MATLAB mr toolbox
3+
4+[VERSION]
5+major 1
6+minor 5
7+revision 1
8+
9+[DEFINITIONS]
10+AdcRasterTime 1e-07
11+BlockDurationRaster 1e-05
12+GradientRasterTime 1e-05
13+Name fid
14+RadiofrequencyRasterTime 1e-06
15+TotalDuration 80.32
16+
17+# Format of blocks:
18+# NUM DUR RF GX GY GZ ADC EXT
19+[BLOCKS]
20+ 1 2000 1 0 0 0 0 0
21+ 2 500000 0 0 0 0 1 0
22+ 3 2000 1 0 0 0 0 0
23+ 4 500000 0 0 0 0 1 0
24+ 5 2000 1 0 0 0 0 0
25+ 6 500000 0 0 0 0 1 0
26+ 7 2000 1 0 0 0 0 0
27+ 8 500000 0 0 0 0 1 0
28+ 9 2000 1 0 0 0 0 0
29+10 500000 0 0 0 0 1 0
30+11 2000 1 0 0 0 0 0
31+12 500000 0 0 0 0 1 0
32+13 2000 1 0 0 0 0 0
33+14 500000 0 0 0 0 1 0
34+15 2000 1 0 0 0 0 0
35+16 500000 0 0 0 0 1 0
36+17 2000 1 0 0 0 0 0
37+18 500000 0 0 0 0 1 0
38+19 2000 1 0 0 0 0 0
39+20 500000 0 0 0 0 1 0
40+21 2000 1 0 0 0 0 0
41+22 500000 0 0 0 0 1 0
42+23 2000 1 0 0 0 0 0
43+24 500000 0 0 0 0 1 0
44+25 2000 1 0 0 0 0 0
45+26 500000 0 0 0 0 1 0
46+27 2000 1 0 0 0 0 0
47+28 500000 0 0 0 0 1 0
48+29 2000 1 0 0 0 0 0
49+30 500000 0 0 0 0 1 0
50+31 2000 1 0 0 0 0 0
51+32 500000 0 0 0 0 1 0
52+
53+# Format of RF events:
54+# id ampl. mag_id phase_id time_shape_id center delay freqPPM phasePPM freq phase use
55+# .. Hz .. .. .. us us ppm rad/MHz Hz rad ..
56+# Field 'use' is the initial of:
57+# excitation refocusing inversion saturation preparation other undefined
58+[RF]
59+1 833.333 1 2 3 150 100 0 0 0 0 e
60+
61+# Format of ADC events:
62+# id num dwell delay freqPPM phasePPM freq phase phase_id
63+# .. .. ns us ppm rad/MHz Hz rad ..
64+[ADC]
65+1 4096 125000 20 0 0 0 0 0
66+
67+# Sequence Shapes
68+[SHAPES]
69+
70+shape_id 1
71+num_samples 2
72+1
73+1
74+
75+shape_id 2
76+num_samples 2
77+0
78+0
79+
80+shape_id 3
81+num_samples 2
82+0
83+300
84+
85+
86+[SIGNATURE]
87+# This is the hash of the Pulseq file, calculated right before the [SIGNATURE]
88+# section was added. It can be reproduced/verified with md5sum if the file
89+# trimmed to the position right above [SIGNATURE]. The new line character
90+# preceding [SIGNATURE] BELONGS to the signature (and needs to be sripped away
91+# for recalculating/verification)
92+Type md5
93+Hash 379f84fe1b36c9422763fa576adeba10
test-data/golden/gre.seqadded+4139−0View file
This diff is 4,144 lines long and is not shown.
test-data/golden/seq1.seqadded+70−0View file
@@ -0,0 +1,70 @@
1+# Pulseq sequence file
2+# Created by MATLAB mr toolbox
3+
4+[VERSION]
5+major 1
6+minor 5
7+revision 1
8+
9+[DEFINITIONS]
10+AdcRasterTime 1e-07
11+BlockDurationRaster 1e-05
12+GradientRasterTime 1e-05
13+RadiofrequencyRasterTime 1e-06
14+
15+# Format of blocks:
16+# NUM DUR RF GX GY GZ ADC EXT
17+[BLOCKS]
18+1 100 1 0 0 0 0 0
19+2 83 0 1 0 0 0 0
20+3 54 0 0 2 0 0 0
21+4 24 0 0 0 3 0 0
22+5 83 0 4 5 0 0 0
23+6 83 0 0 2 1 0 0
24+7 83 0 4 0 1 0 0
25+
26+# Format of RF events:
27+# id ampl. mag_id phase_id time_shape_id center delay freqPPM phasePPM freq phase use
28+# .. Hz .. .. .. us us ppm rad/MHz Hz rad ..
29+# Field 'use' is the initial of:
30+# excitation refocusing inversion saturation preparation other undefined
31+[RF]
32+1 125 1 2 3 500 0 0 0 0 0 e
33+
34+# Format of trapezoid gradients:
35+# id amplitude rise flat fall delay
36+# .. Hz/m us us us us
37+[TRAP]
38+ 1 1.69492e+06 240 350 240 0
39+ 2 -1.66667e+06 240 60 240 0
40+ 3 833333 120 0 120 0
41+ 4 -1.69492e+06 240 350 240 0
42+ 5 1.66667e+06 240 60 240 0
43+
44+# Sequence Shapes
45+[SHAPES]
46+
47+shape_id 1
48+num_samples 2
49+1
50+1
51+
52+shape_id 2
53+num_samples 2
54+0
55+0
56+
57+shape_id 3
58+num_samples 2
59+0
60+1000
61+
62+
63+[SIGNATURE]
64+# This is the hash of the Pulseq file, calculated right before the [SIGNATURE]
65+# section was added. It can be reproduced/verified with md5sum if the file
66+# trimmed to the position right above [SIGNATURE]. The new line character
67+# preceding [SIGNATURE] BELONGS to the signature (and needs to be sripped away
68+# for recalculating/verification)
69+Type md5
70+Hash ff15b77fb6733278e09758bfb2ce45ff
test-data/golden/seq2.seqadded+75−0View file
@@ -0,0 +1,75 @@
1+# Pulseq sequence file
2+# Created by MATLAB mr toolbox
3+
4+[VERSION]
5+major 1
6+minor 5
7+revision 1
8+
9+[DEFINITIONS]
10+AdcRasterTime 1e-07
11+BlockDurationRaster 1e-05
12+GradientRasterTime 1e-05
13+RadiofrequencyRasterTime 1e-06
14+
15+# Format of blocks:
16+# NUM DUR RF GX GY GZ ADC EXT
17+[BLOCKS]
18+1 100 1 0 0 0 0 0
19+2 83 0 1 0 0 0 0
20+3 83 0 2 0 0 0 0
21+4 100 2 0 0 0 0 0
22+5 54 0 3 0 0 0 0
23+6 1000 0 4 0 0 1 0
24+
25+# Format of RF events:
26+# id ampl. mag_id phase_id time_shape_id center delay freqPPM phasePPM freq phase use
27+# .. Hz .. .. .. us us ppm rad/MHz Hz rad ..
28+# Field 'use' is the initial of:
29+# excitation refocusing inversion saturation preparation other undefined
30+[RF]
31+1 250 1 2 3 500 0 0 0 0 0 e
32+2 500 1 2 3 500 0 0 0 0 0 r
33+
34+# Format of trapezoid gradients:
35+# id amplitude rise flat fall delay
36+# .. Hz/m us us us us
37+[TRAP]
38+ 1 1.69492e+06 240 350 240 0
39+ 2 -1.69492e+06 240 350 240 0
40+ 3 -1.66667e+06 240 60 240 0
41+ 4 100200 20 9960 20 0
42+
43+# Format of ADC events:
44+# id num dwell delay freqPPM phasePPM freq phase phase_id
45+# .. .. ns us ppm rad/MHz Hz rad ..
46+[ADC]
47+1 100 100000 0 0 0 0 0 0
48+
49+# Sequence Shapes
50+[SHAPES]
51+
52+shape_id 1
53+num_samples 2
54+1
55+1
56+
57+shape_id 2
58+num_samples 2
59+0
60+0
61+
62+shape_id 3
63+num_samples 2
64+0
65+1000
66+
67+
68+[SIGNATURE]
69+# This is the hash of the Pulseq file, calculated right before the [SIGNATURE]
70+# section was added. It can be reproduced/verified with md5sum if the file
71+# trimmed to the position right above [SIGNATURE]. The new line character
72+# preceding [SIGNATURE] BELONGS to the signature (and needs to be sripped away
73+# for recalculating/verification)
74+Type md5
75+Hash 8f5da27b466bde6d330ba19d7052c0ca
test-data/golden/seq3.seqadded+138−0View file
@@ -0,0 +1,138 @@
1+# Pulseq sequence file
2+# Created by MATLAB mr toolbox
3+
4+[VERSION]
5+major 1
6+minor 5
7+revision 1
8+
9+[DEFINITIONS]
10+AdcRasterTime 1e-07
11+BlockDurationRaster 1e-05
12+GradientRasterTime 1e-05
13+RadiofrequencyRasterTime 1e-06
14+
15+# Format of blocks:
16+# NUM DUR RF GX GY GZ ADC EXT
17+[BLOCKS]
18+ 1 100 1 0 0 0 0 0
19+ 2 83 0 1 0 0 0 0
20+ 3 54 0 0 2 0 0 0
21+ 4 54 0 2 0 0 0 0
22+ 5 1000 0 3 0 0 1 1
23+ 6 100 1 0 0 0 0 0
24+ 7 83 0 1 0 0 0 0
25+ 8 48 0 0 4 0 0 0
26+ 9 54 0 2 0 0 0 0
27+10 1000 0 3 0 0 1 1
28+11 100 1 0 0 0 0 0
29+12 83 0 1 0 0 0 0
30+13 42 0 0 5 0 0 0
31+14 54 0 2 0 0 0 0
32+15 1000 0 3 0 0 1 1
33+16 100 1 0 0 0 0 0
34+17 83 0 1 0 0 0 0
35+18 34 0 0 6 0 0 0
36+19 54 0 2 0 0 0 0
37+20 1000 0 3 0 0 1 1
38+21 100 1 0 0 0 0 0
39+22 83 0 1 0 0 0 0
40+23 24 0 0 7 0 0 0
41+24 54 0 2 0 0 0 0
42+25 1000 0 3 0 0 1 1
43+26 100 1 0 0 0 0 0
44+27 83 0 1 0 0 0 0
45+28 2 0 0 8 0 0 0
46+29 54 0 2 0 0 0 0
47+30 1000 0 3 0 0 1 1
48+31 100 1 0 0 0 0 0
49+32 83 0 1 0 0 0 0
50+33 24 0 0 9 0 0 0
51+34 54 0 2 0 0 0 0
52+35 1000 0 3 0 0 1 1
53+36 100 1 0 0 0 0 0
54+37 83 0 1 0 0 0 0
55+38 34 0 0 10 0 0 0
56+39 54 0 2 0 0 0 0
57+40 1000 0 3 0 0 1 1
58+41 100 1 0 0 0 0 0
59+42 83 0 1 0 0 0 0
60+43 42 0 0 11 0 0 0
61+44 54 0 2 0 0 0 0
62+45 1000 0 3 0 0 1 1
63+46 100 1 0 0 0 0 0
64+47 83 0 1 0 0 0 0
65+48 48 0 0 12 0 0 0
66+49 54 0 2 0 0 0 0
67+50 1000 0 3 0 0 1 1
68+
69+# Format of RF events:
70+# id ampl. mag_id phase_id time_shape_id center delay freqPPM phasePPM freq phase use
71+# .. Hz .. .. .. us us ppm rad/MHz Hz rad ..
72+# Field 'use' is the initial of:
73+# excitation refocusing inversion saturation preparation other undefined
74+[RF]
75+1 62.5 1 2 3 500 0 0 0 0 0 e
76+
77+# Format of trapezoid gradients:
78+# id amplitude rise flat fall delay
79+# .. Hz/m us us us us
80+[TRAP]
81+ 1 1.69492e+06 240 350 240 0
82+ 2 -1.66667e+06 240 60 240 0
83+ 3 100200 20 9960 20 0
84+ 4 -1.66667e+06 240 0 240 0
85+ 5 -1.42857e+06 210 0 210 0
86+ 6 -1.17647e+06 170 0 170 0
87+ 7 -833333 120 0 120 0
88+ 8 0 10 0 10 0
89+ 9 833333 120 0 120 0
90+10 1.17647e+06 170 0 170 0
91+11 1.42857e+06 210 0 210 0
92+12 1.66667e+06 240 0 240 0
93+
94+# Format of ADC events:
95+# id num dwell delay freqPPM phasePPM freq phase phase_id
96+# .. .. ns us ppm rad/MHz Hz rad ..
97+[ADC]
98+1 100 100000 0 0 0 0 0 0
99+
100+# Format of extension lists:
101+# id type ref next_id
102+# next_id of 0 terminates the list
103+# Extension list is followed by extension specifications
104+[EXTENSIONS]
105+1 1 1 0
106+
107+# Extension specification for increasing labels:
108+# id inc labelstring
109+extension LABELINC 1
110+1 1 LIN
111+
112+# Sequence Shapes
113+[SHAPES]
114+
115+shape_id 1
116+num_samples 2
117+1
118+1
119+
120+shape_id 2
121+num_samples 2
122+0
123+0
124+
125+shape_id 3
126+num_samples 2
127+0
128+1000
129+
130+
131+[SIGNATURE]
132+# This is the hash of the Pulseq file, calculated right before the [SIGNATURE]
133+# section was added. It can be reproduced/verified with md5sum if the file
134+# trimmed to the position right above [SIGNATURE]. The new line character
135+# preceding [SIGNATURE] BELONGS to the signature (and needs to be sripped away
136+# for recalculating/verification)
137+Type md5
138+Hash 58f60cf3a8bdb4e1b47b2d5299cf1d74
tsconfig.app.jsonadded+26−0View file
@@ -0,0 +1,26 @@
1+{
2+ "compilerOptions": {
3+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4+ "target": "es2023",
5+ "lib": ["ES2023", "DOM", "WebWorker"],
6+ "module": "esnext",
7+ "types": ["vite/client"],
8+ "allowArbitraryExtensions": true,
9+ "skipLibCheck": true,
10+
11+ /* Bundler mode */
12+ "moduleResolution": "bundler",
13+ "allowImportingTsExtensions": true,
14+ "verbatimModuleSyntax": true,
15+ "moduleDetection": "force",
16+ "noEmit": true,
17+ "jsx": "react-jsx",
18+
19+ /* Linting */
20+ "noUnusedLocals": true,
21+ "noUnusedParameters": true,
22+ "erasableSyntaxOnly": true,
23+ "noFallthroughCasesInSwitch": true
24+ },
25+ "include": ["src"]
26+}
tsconfig.jsonadded+7−0View file
@@ -0,0 +1,7 @@
1+{
2+ "files": [],
3+ "references": [
4+ { "path": "./tsconfig.app.json" },
5+ { "path": "./tsconfig.node.json" }
6+ ]
7+}
tsconfig.node.jsonadded+23−0View file
@@ -0,0 +1,23 @@
1+{
2+ "compilerOptions": {
3+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4+ "target": "es2023",
5+ "lib": ["ES2023"],
6+ "types": ["node"],
7+ "skipLibCheck": true,
8+
9+ /* Bundler mode */
10+ "module": "nodenext",
11+ "allowImportingTsExtensions": true,
12+ "verbatimModuleSyntax": true,
13+ "moduleDetection": "force",
14+ "noEmit": true,
15+
16+ /* Linting */
17+ "noUnusedLocals": true,
18+ "noUnusedParameters": true,
19+ "erasableSyntaxOnly": true,
20+ "noFallthroughCasesInSwitch": true
21+ },
22+ "include": ["vite.config.ts"]
23+}
vite.config.tsadded+8−0View file
@@ -0,0 +1,8 @@
1+import { defineConfig } from 'vite'
2+import react from '@vitejs/plugin-react'
3+
4+// https://vite.dev/config/
5+export default defineConfig({
6+ plugins: [react()],
7+ base: './',
8+})