seqeyes-python 0.2.6__tar.gz → 0.2.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: seqeyes-python
3
- Version: 0.2.6
3
+ Version: 0.2.8
4
4
  Summary: Interactive Pulseq MRI sequence viewer for Jupyter — lightweight, beautiful, replaces seq.plot() in pypulseq.
5
5
  Project-URL: Homepage, https://github.com/bughht/seqeyes_plugin
6
6
  Project-URL: Repository, https://github.com/bughht/seqeyes_plugin
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "seqeyes-python"
7
- version = "0.2.6"
7
+ version = "0.2.8"
8
8
  description = "Interactive Pulseq MRI sequence viewer for Jupyter — lightweight, beautiful, replaces seq.plot() in pypulseq."
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -29,4 +29,4 @@ from seqeyes._plot import set, reset
29
29
  from seqeyes._renderer import SeqEyesViewer
30
30
 
31
31
  __all__ = ["set", "reset", "SeqEyesViewer"]
32
- __version__ = "0.2.6"
32
+ __version__ = "0.2.8"
@@ -23,15 +23,23 @@ var Pulseq = (() => {
23
23
  // web/pulseq-browser.ts
24
24
  var pulseq_browser_exports = {};
25
25
  __export(pulseq_browser_exports, {
26
+ INTERACTIVE_COMPUTE_LIMITS: () => INTERACTIVE_COMPUTE_LIMITS,
26
27
  PACKAGE_VERSION: () => PACKAGE_VERSION,
27
28
  calculateKspace: () => calculateKspace,
28
29
  calculateM1: () => calculateM1,
30
+ calculateM1Coarse: () => calculateM1Coarse,
29
31
  calculatePns: () => calculatePns,
32
+ calculatePnsCoarse: () => calculatePnsCoarse,
30
33
  decodeAllBlocks: () => decodeAllBlocks,
31
34
  detectSequenceTiming: () => detectSequenceTiming,
35
+ estimateDerivedCost: () => estimateDerivedCost,
36
+ estimateKspaceCost: () => estimateKspaceCost,
37
+ estimateKspacePeakMemoryBytes: () => estimateKspacePeakMemoryBytes,
32
38
  exportKspaceArtifacts: () => exportKspaceArtifacts,
33
39
  exportKspaceArtifactsFromBytes: () => exportKspaceArtifactsFromBytes,
34
40
  exportKspaceArtifactsFromSequence: () => exportKspaceArtifactsFromSequence,
41
+ formatMemorySize: () => formatMemorySize,
42
+ formatSampleCount: () => formatSampleCount,
35
43
  formatTrajectoryText: () => formatTrajectoryText,
36
44
  getTotalDuration: () => getTotalDuration,
37
45
  hasPulseqBinaryMagic: () => hasPulseqBinaryMagic,
@@ -39,11 +47,13 @@ var Pulseq = (() => {
39
47
  parseSequenceBinary: () => parseSequenceBinary,
40
48
  parseSequenceBytes: () => parseSequenceBytes,
41
49
  parseSequenceText: () => parseSequenceText,
42
- safePnsModel: () => safePnsModel
50
+ safePnsModel: () => safePnsModel,
51
+ selectM1WindowBlocks: () => selectM1WindowBlocks,
52
+ selectPnsWindowBlocks: () => selectPnsWindowBlocks
43
53
  });
44
54
 
45
55
  // package.json
46
- var version = "0.2.6";
56
+ var version = "0.2.8";
47
57
 
48
58
  // src/pulseq/decompressor.ts
49
59
  function decompressShape(compressed, numSamples) {
@@ -1784,6 +1794,11 @@ var Pulseq = (() => {
1784
1794
  for (const b of blocks) {
1785
1795
  if (b.adc) totalAdcSamples += b.adc.numSamples;
1786
1796
  }
1797
+ if (_options?.maxAdcSamples && totalAdcSamples > _options.maxAdcSamples) return null;
1798
+ if (_options?.maxGridPoints && totalDuration > 0) {
1799
+ const rasterPointCount = Math.max(2, Math.round(totalDuration / GR) + 1);
1800
+ if (rasterPointCount + totalAdcSamples > _options.maxGridPoints) return null;
1801
+ }
1787
1802
  const adcT = new Float64Array(totalAdcSamples);
1788
1803
  let adcIdx = 0;
1789
1804
  for (const b of blocks) {
@@ -2015,8 +2030,140 @@ var Pulseq = (() => {
2015
2030
  return [gx, gy, gz];
2016
2031
  }
2017
2032
 
2018
- // src/pulseq/m1.ts
2033
+ // src/pulseq/boundedSeries.ts
2034
+ var BoundedSeriesBuilder = class {
2035
+ constructor(startSec, endSec, maxPoints) {
2036
+ __publicField(this, "startSec", startSec);
2037
+ __publicField(this, "endSec", endSec);
2038
+ __publicField(this, "buckets");
2039
+ __publicField(this, "bucketCount");
2040
+ __publicField(this, "span");
2041
+ this.bucketCount = Math.max(1, Math.floor(Math.max(4, maxPoints) / 4));
2042
+ this.buckets = new Array(this.bucketCount);
2043
+ this.span = Math.max(0, endSec - startSec);
2044
+ }
2045
+ add(tSec, value) {
2046
+ if (!Number.isFinite(tSec) || !Number.isFinite(value)) return;
2047
+ const normalized = this.span > 0 ? (tSec - this.startSec) / this.span : 0;
2048
+ const index = Math.max(0, Math.min(
2049
+ this.bucketCount - 1,
2050
+ Math.floor(normalized * this.bucketCount)
2051
+ ));
2052
+ const bucket = this.buckets[index];
2053
+ if (!bucket) {
2054
+ this.buckets[index] = {
2055
+ firstT: tSec,
2056
+ firstV: value,
2057
+ minV: value,
2058
+ maxV: value,
2059
+ lastT: tSec,
2060
+ lastV: value
2061
+ };
2062
+ return;
2063
+ }
2064
+ if (value < bucket.minV) {
2065
+ bucket.minV = value;
2066
+ }
2067
+ if (value > bucket.maxV) {
2068
+ bucket.maxV = value;
2069
+ }
2070
+ bucket.lastT = tSec;
2071
+ bucket.lastV = value;
2072
+ }
2073
+ finish() {
2074
+ const startTime = [];
2075
+ const endTime = [];
2076
+ const min = [];
2077
+ const max = [];
2078
+ const first = [];
2079
+ const last = [];
2080
+ for (const bucket of this.buckets) {
2081
+ if (!bucket) continue;
2082
+ startTime.push(bucket.firstT);
2083
+ endTime.push(bucket.lastT);
2084
+ min.push(bucket.minV);
2085
+ max.push(bucket.maxV);
2086
+ first.push(bucket.firstV);
2087
+ last.push(bucket.lastV);
2088
+ }
2089
+ return {
2090
+ startTime: new Float64Array(startTime),
2091
+ endTime: new Float64Array(endTime),
2092
+ min: new Float64Array(min),
2093
+ max: new Float64Array(max),
2094
+ first: new Float64Array(first),
2095
+ last: new Float64Array(last)
2096
+ };
2097
+ }
2098
+ };
2099
+
2100
+ // src/pulseq/gradientSampler.ts
2019
2101
  var TIME_EPS = 1e-15;
2102
+ function decodedGradientTimeRange(blocks) {
2103
+ let first = Number.POSITIVE_INFINITY;
2104
+ let last = Number.NEGATIVE_INFINITY;
2105
+ for (const block of blocks) {
2106
+ for (const gradient of [block.gx, block.gy, block.gz]) {
2107
+ if (!gradient?.timePoints.length) continue;
2108
+ const gradientFirst = gradient.timePoints[0];
2109
+ const gradientLast = gradient.timePoints[gradient.timePoints.length - 1];
2110
+ if (Number.isFinite(gradientFirst)) first = Math.min(first, gradientFirst);
2111
+ if (Number.isFinite(gradientLast)) last = Math.max(last, gradientLast);
2112
+ }
2113
+ }
2114
+ return Number.isFinite(first) && Number.isFinite(last) && last >= first ? { first, last } : void 0;
2115
+ }
2116
+ function createDecodedGradientSampler(blocks, channel) {
2117
+ const events = [];
2118
+ for (const block of blocks) {
2119
+ const gradient = block[channel];
2120
+ if (!gradient?.timePoints.length || !gradient.waveform.length) continue;
2121
+ const n = Math.min(gradient.timePoints.length, gradient.waveform.length);
2122
+ if (n < 1) continue;
2123
+ events.push({
2124
+ gradient,
2125
+ first: gradient.timePoints[0],
2126
+ last: gradient.timePoints[n - 1]
2127
+ });
2128
+ }
2129
+ events.sort((a, b) => a.first - b.first);
2130
+ let eventIndex = 0;
2131
+ let pointIndex = 0;
2132
+ let previousTime = Number.NEGATIVE_INFINITY;
2133
+ return (timeSec) => {
2134
+ if (timeSec < previousTime - TIME_EPS) {
2135
+ eventIndex = 0;
2136
+ pointIndex = 0;
2137
+ }
2138
+ previousTime = timeSec;
2139
+ while (eventIndex < events.length && events[eventIndex].last < timeSec - TIME_EPS) {
2140
+ eventIndex++;
2141
+ pointIndex = 0;
2142
+ }
2143
+ if (eventIndex >= events.length) return 0;
2144
+ const event = events[eventIndex];
2145
+ if (timeSec < event.first - TIME_EPS || timeSec > event.last + TIME_EPS) return 0;
2146
+ const times = event.gradient.timePoints;
2147
+ const values = event.gradient.waveform;
2148
+ const n = Math.min(times.length, values.length);
2149
+ if (Math.abs(timeSec - event.last) <= TIME_EPS && eventIndex + 1 < events.length) {
2150
+ const next = events[eventIndex + 1];
2151
+ if (Math.abs(next.first - timeSec) <= TIME_EPS && next.gradient.waveform.length) {
2152
+ return 0.5 * (values[n - 1] + next.gradient.waveform[0]);
2153
+ }
2154
+ }
2155
+ while (pointIndex + 1 < n && times[pointIndex + 1] <= timeSec + TIME_EPS) pointIndex++;
2156
+ if (pointIndex >= n - 1 || timeSec <= times[pointIndex] + TIME_EPS) return values[pointIndex];
2157
+ const t0 = times[pointIndex];
2158
+ const t1 = times[pointIndex + 1];
2159
+ if (!(t1 > t0)) return values[pointIndex];
2160
+ const alpha = (timeSec - t0) / (t1 - t0);
2161
+ return values[pointIndex] + alpha * (values[pointIndex + 1] - values[pointIndex]);
2162
+ };
2163
+ }
2164
+
2165
+ // src/pulseq/m1.ts
2166
+ var TIME_EPS2 = 1e-15;
2020
2167
  function calculateM1(blocks, gradientRaster, options = {}) {
2021
2168
  const referenceMode = normalizeReferenceMode(options.referenceMode);
2022
2169
  if (!blocks.length) {
@@ -2039,10 +2186,38 @@ var Pulseq = (() => {
2039
2186
  const excitationTimes = rfEvents.filter((rf) => rf.use === "e").map((rf) => rf.tSec);
2040
2187
  const refocusingTimes = rfEvents.filter((rf) => rf.use === "r").map((rf) => rf.tSec);
2041
2188
  const events = buildWalkerEvents(rfEvents);
2189
+ appendM1AdvisoryWarnings(rfEvents, tMin, warnings);
2190
+ const rasterSec = gradientRaster > 0 ? gradientRaster : 1e-5;
2191
+ if (rasterSec <= 0) {
2192
+ return invalidM1("gradientRaster must be positive.", referenceMode);
2193
+ }
2194
+ const samples = buildSampleTimes(tMin, tMax, rasterSec);
2195
+ const x = walkM1(gx, samples, events, excitationTimes, tMin, referenceMode);
2196
+ const y = walkM1(gy, samples, events, excitationTimes, tMin, referenceMode);
2197
+ const z = walkM1(gz, samples, events, excitationTimes, tMin, referenceMode);
2198
+ if (x.t.length !== y.t.length || x.t.length !== z.t.length) {
2199
+ warnings.push(`Internal warning: per-axis M1 output sizes disagree (${x.t.length}, ${y.t.length}, ${z.t.length}). Plot may be inconsistent.`);
2200
+ }
2201
+ return {
2202
+ valid: true,
2203
+ ok: true,
2204
+ referenceMode,
2205
+ tSec: new Float64Array(x.t),
2206
+ m1x: new Float64Array(x.m1),
2207
+ m1y: new Float64Array(y.m1),
2208
+ m1z: new Float64Array(z.m1),
2209
+ warnings,
2210
+ excitationTimesSec: new Float64Array(excitationTimes),
2211
+ refocusingTimesSec: new Float64Array(refocusingTimes)
2212
+ };
2213
+ }
2214
+ function appendM1AdvisoryWarnings(rfEvents, tMin, warnings) {
2042
2215
  let recentExcCount = 0;
2043
2216
  let lastExcT = -1e9;
2217
+ let excitationCount = 0;
2044
2218
  for (const rf of rfEvents) {
2045
2219
  if (rf.use === "e") {
2220
+ excitationCount++;
2046
2221
  if (rf.tSec - lastExcT < 0.1) recentExcCount++;
2047
2222
  lastExcT = rf.tSec;
2048
2223
  }
@@ -2052,28 +2227,122 @@ var Pulseq = (() => {
2052
2227
  `Sequence shows ${recentExcCount} closely-spaced (<100 ms) excitation events. This pattern is consistent with a steady-state sequence for which the simplified reset/flip bookkeeping does NOT model coherent pathway interference. Treat the M1 curve as advisory only.`
2053
2228
  );
2054
2229
  }
2055
- if (!excitationTimes.length) {
2230
+ if (!excitationCount) {
2056
2231
  warnings.push(`No excitation RF events found in sequence. M1 will be integrated from t=${tMin.toFixed(6)} s with no signal basis.`);
2057
2232
  }
2058
- const rasterSec = gradientRaster > 0 ? gradientRaster : 1e-5;
2059
- if (rasterSec <= 0) {
2060
- return invalidM1("gradientRaster must be positive.", referenceMode);
2061
- }
2062
- const samples = buildSampleTimes(tMin, tMax, rasterSec);
2063
- const x = walkM1(gx, samples, events, excitationTimes, tMin, referenceMode);
2064
- const y = walkM1(gy, samples, events, excitationTimes, tMin, referenceMode);
2065
- const z = walkM1(gz, samples, events, excitationTimes, tMin, referenceMode);
2066
- if (x.t.length !== y.t.length || x.t.length !== z.t.length) {
2067
- warnings.push(`Internal warning: per-axis M1 output sizes disagree (${x.t.length}, ${y.t.length}, ${z.t.length}). Plot may be inconsistent.`);
2233
+ }
2234
+ function calculateM1Coarse(blocks, gradientRaster, options = {}) {
2235
+ const referenceMode = normalizeReferenceMode(options.referenceMode);
2236
+ const emptySeries = () => ({
2237
+ startTime: new Float64Array(),
2238
+ endTime: new Float64Array(),
2239
+ min: new Float64Array(),
2240
+ max: new Float64Array(),
2241
+ first: new Float64Array(),
2242
+ last: new Float64Array()
2243
+ });
2244
+ const invalid = (error) => ({
2245
+ valid: false,
2246
+ ok: false,
2247
+ coarse: true,
2248
+ referenceMode,
2249
+ error,
2250
+ startSec: 0,
2251
+ endSec: 0,
2252
+ x: emptySeries(),
2253
+ y: emptySeries(),
2254
+ z: emptySeries(),
2255
+ warnings: [],
2256
+ excitationTimesSec: new Float64Array(),
2257
+ refocusingTimesSec: new Float64Array()
2258
+ });
2259
+ if (!blocks.length) return invalid("Empty or invalid block list.");
2260
+ if (!(gradientRaster > 0)) return invalid("gradientRaster must be positive.");
2261
+ const range = decodedGradientTimeRange(blocks);
2262
+ if (!range) return invalid("No gradient waveform available for M1.");
2263
+ const warnings = [];
2264
+ const rfEvents = collectRfEvents(blocks, warnings);
2265
+ appendM1AdvisoryWarnings(rfEvents, range.first, warnings);
2266
+ const excitationTimes = rfEvents.filter((rf) => rf.use === "e").map((rf) => rf.tSec);
2267
+ const refocusingTimes = rfEvents.filter((rf) => rf.use === "r").map((rf) => rf.tSec);
2268
+ const events = buildWalkerEvents(rfEvents);
2269
+ const startSec = Math.min(range.first, events[0]?.tSec ?? range.first);
2270
+ const endSec = Math.max(range.last, events[events.length - 1]?.tSec ?? range.last);
2271
+ const maxPoints = Math.max(1024, Math.min(12e4, options.maxPoints ?? 3e4));
2272
+ const maxBuckets = Math.floor(maxPoints / 4);
2273
+ const builders = [
2274
+ new BoundedSeriesBuilder(startSec, endSec, maxPoints),
2275
+ new BoundedSeriesBuilder(startSec, endSec, maxPoints),
2276
+ new BoundedSeriesBuilder(startSec, endSec, maxPoints)
2277
+ ];
2278
+ const samplers = [
2279
+ createDecodedGradientSampler(blocks, "gx"),
2280
+ createDecodedGradientSampler(blocks, "gy"),
2281
+ createDecodedGradientSampler(blocks, "gz")
2282
+ ];
2283
+ const unsignedM0 = [0, 0, 0];
2284
+ const unsignedM1 = [0, 0, 0];
2285
+ let sign = 1;
2286
+ let tReset = excitationTimes.length ? excitationTimes[0] : range.first;
2287
+ if (range.first < tReset) tReset = range.first;
2288
+ let currentT = tReset;
2289
+ const reported = (axis, tSec) => referenceMode === "observationTime" ? sign * (unsignedM1[axis] - (tSec - tReset) * unsignedM0[axis]) : sign * unsignedM1[axis];
2290
+ const advanceTo = (targetT) => {
2291
+ if (!(targetT > currentT + TIME_EPS2)) return;
2292
+ for (let axis = 0; axis < 3; axis++) {
2293
+ const ga = samplers[axis](currentT);
2294
+ const gb = samplers[axis](targetT);
2295
+ const integrated = integrateLinearSegment(currentT, targetT, tReset, ga, gb);
2296
+ unsignedM0[axis] += integrated[0];
2297
+ unsignedM1[axis] += integrated[1];
2298
+ }
2299
+ currentT = targetT;
2300
+ };
2301
+ const addReported = (tSec) => {
2302
+ for (let axis = 0; axis < 3; axis++) builders[axis].add(tSec, reported(axis, tSec));
2303
+ };
2304
+ const regularCount = Math.floor((range.last - range.first) / gradientRaster) + 1;
2305
+ const regularLast = range.first + Math.max(0, regularCount - 1) * gradientRaster;
2306
+ const hasFinalSample = regularLast < range.last - TIME_EPS2;
2307
+ const totalSamples = regularCount + (hasFinalSample ? 1 : 0);
2308
+ let eventIndex = 0;
2309
+ let sampleIndex = 0;
2310
+ while (eventIndex < events.length || sampleIndex < totalSamples) {
2311
+ const eventTime = eventIndex < events.length ? events[eventIndex].tSec : Number.POSITIVE_INFINITY;
2312
+ const sampleTime = sampleIndex < totalSamples ? sampleIndex < regularCount ? range.first + sampleIndex * gradientRaster : range.last : Number.POSITIVE_INFINITY;
2313
+ if (eventTime <= sampleTime) {
2314
+ advanceTo(eventTime);
2315
+ if (events[eventIndex].kind === "reset") {
2316
+ sign = 1;
2317
+ tReset = eventTime;
2318
+ currentT = eventTime;
2319
+ unsignedM0.fill(0);
2320
+ unsignedM1.fill(0);
2321
+ for (const builder of builders) builder.add(eventTime, 0);
2322
+ } else {
2323
+ addReported(eventTime);
2324
+ sign = -sign;
2325
+ }
2326
+ eventIndex++;
2327
+ } else {
2328
+ advanceTo(sampleTime);
2329
+ addReported(sampleTime);
2330
+ sampleIndex++;
2331
+ }
2068
2332
  }
2333
+ warnings.push(
2334
+ `Showing a bounded full-sequence M1 envelope (at most ${maxBuckets.toLocaleString()} buckets per axis). Zoom to 100 TRs or fewer for an automatic detailed calculation.`
2335
+ );
2069
2336
  return {
2070
2337
  valid: true,
2071
2338
  ok: true,
2339
+ coarse: true,
2072
2340
  referenceMode,
2073
- tSec: new Float64Array(x.t),
2074
- m1x: new Float64Array(x.m1),
2075
- m1y: new Float64Array(y.m1),
2076
- m1z: new Float64Array(z.m1),
2341
+ startSec,
2342
+ endSec,
2343
+ x: builders[0].finish(),
2344
+ y: builders[1].finish(),
2345
+ z: builders[2].finish(),
2077
2346
  warnings,
2078
2347
  excitationTimesSec: new Float64Array(excitationTimes),
2079
2348
  refocusingTimesSec: new Float64Array(refocusingTimes)
@@ -2112,7 +2381,7 @@ var Pulseq = (() => {
2112
2381
  function appendGradientPoint(series, t, value) {
2113
2382
  if (!Number.isFinite(t) || !Number.isFinite(value)) return;
2114
2383
  const last = series.time.length - 1;
2115
- if (last >= 0 && Math.abs(t - series.time[last]) <= TIME_EPS) {
2384
+ if (last >= 0 && Math.abs(t - series.time[last]) <= TIME_EPS2) {
2116
2385
  series.value[last] = 0.5 * (series.value[last] + value);
2117
2386
  } else if (last < 0 || t > series.time[last]) {
2118
2387
  series.time.push(t);
@@ -2162,7 +2431,7 @@ var Pulseq = (() => {
2162
2431
  const samples = [];
2163
2432
  const nSamples = Math.floor((tMax - tMin) / rasterSec) + 1;
2164
2433
  for (let i = 0; i < nSamples; i++) samples.push(tMin + i * rasterSec);
2165
- if (!samples.length || samples[samples.length - 1] < tMax - TIME_EPS) samples.push(tMax);
2434
+ if (!samples.length || samples[samples.length - 1] < tMax - TIME_EPS2) samples.push(tMax);
2166
2435
  return samples;
2167
2436
  }
2168
2437
  function walkM1(gradient, samples, events, excitationTimes, tMin, referenceMode) {
@@ -2176,16 +2445,16 @@ var Pulseq = (() => {
2176
2445
  let unsignedM1 = 0;
2177
2446
  let gradientIndex = -1;
2178
2447
  const seekGradient = (t) => {
2179
- while (gradientIndex + 1 < gradient.time.length && gradient.time[gradientIndex + 1] <= t + TIME_EPS) {
2448
+ while (gradientIndex + 1 < gradient.time.length && gradient.time[gradientIndex + 1] <= t + TIME_EPS2) {
2180
2449
  gradientIndex++;
2181
2450
  }
2182
2451
  };
2183
2452
  const sampleGradient = (t) => {
2184
2453
  const n = gradient.time.length;
2185
- if (n === 0 || t < gradient.time[0] - TIME_EPS || t > gradient.time[n - 1] + TIME_EPS) return 0;
2454
+ if (n === 0 || t < gradient.time[0] - TIME_EPS2 || t > gradient.time[n - 1] + TIME_EPS2) return 0;
2186
2455
  seekGradient(t);
2187
2456
  if (gradientIndex < 0) return 0;
2188
- if (gradientIndex >= n - 1 || Math.abs(t - gradient.time[gradientIndex]) <= TIME_EPS) {
2457
+ if (gradientIndex >= n - 1 || Math.abs(t - gradient.time[gradientIndex]) <= TIME_EPS2) {
2189
2458
  return gradient.value[gradientIndex];
2190
2459
  }
2191
2460
  const t0 = gradient.time[gradientIndex];
@@ -2199,8 +2468,8 @@ var Pulseq = (() => {
2199
2468
  return sign * unsignedM1;
2200
2469
  };
2201
2470
  const advanceTo = (targetT) => {
2202
- if (!(targetT > currentT + TIME_EPS)) return;
2203
- while (currentT < targetT - TIME_EPS) {
2471
+ if (!(targetT > currentT + TIME_EPS2)) return;
2472
+ while (currentT < targetT - TIME_EPS2) {
2204
2473
  seekGradient(currentT);
2205
2474
  let nextT = gradientIndex + 1 < gradient.time.length ? Math.min(targetT, gradient.time[gradientIndex + 1]) : targetT;
2206
2475
  if (!(nextT > currentT)) nextT = targetT;
@@ -2220,7 +2489,7 @@ var Pulseq = (() => {
2220
2489
  if (nextEvtT <= nextSampT) {
2221
2490
  advanceTo(nextEvtT);
2222
2491
  if (events[ei].kind === "reset") {
2223
- if (!outT.length || outT[outT.length - 1] < nextEvtT - TIME_EPS) {
2492
+ if (!outT.length || outT[outT.length - 1] < nextEvtT - TIME_EPS2) {
2224
2493
  outT.push(nextEvtT);
2225
2494
  outM1.push(0);
2226
2495
  } else {
@@ -2259,7 +2528,7 @@ var Pulseq = (() => {
2259
2528
 
2260
2529
  // src/pulseq/pns.ts
2261
2530
  var GAMMA_HZ_PER_T = 42576e3;
2262
- var TIME_EPS2 = 1e-15;
2531
+ var TIME_EPS3 = 1e-15;
2263
2532
  function parsePnsHardwareAsc(text) {
2264
2533
  const asc = parseAscText(text);
2265
2534
  const prefix = resolvePnsPrefix(asc);
@@ -2391,6 +2660,148 @@ var Pulseq = (() => {
2391
2660
  }
2392
2661
  return { valid: true, ok, timeSec, pnsX, pnsY, pnsZ, pnsNorm };
2393
2662
  }
2663
+ function calculatePnsCoarse(blocks, gradientRaster, hardware, options = {}) {
2664
+ const emptySeries = () => ({
2665
+ startTime: new Float64Array(),
2666
+ endTime: new Float64Array(),
2667
+ min: new Float64Array(),
2668
+ max: new Float64Array(),
2669
+ first: new Float64Array(),
2670
+ last: new Float64Array()
2671
+ });
2672
+ const invalid = (error) => ({
2673
+ valid: false,
2674
+ ok: false,
2675
+ coarse: true,
2676
+ error,
2677
+ startSec: 0,
2678
+ endSec: 0,
2679
+ x: emptySeries(),
2680
+ y: emptySeries(),
2681
+ z: emptySeries(),
2682
+ norm: emptySeries(),
2683
+ warnings: []
2684
+ });
2685
+ const gammaHzPerT = options.gammaHzPerT ?? GAMMA_HZ_PER_T;
2686
+ if (!hardware.valid) return invalid("PNS hardware is not initialized.");
2687
+ if (!blocks.length) return invalid("No sequence loaded.");
2688
+ if (!(gradientRaster > 0) || !(gammaHzPerT > 0)) return invalid("Missing GradientRasterTime or gamma.");
2689
+ const range = decodedGradientTimeRange(blocks);
2690
+ if (!range || !(range.last > range.first)) return invalid("No gradient waveform available for PNS.");
2691
+ const dtSec = gradientRaster;
2692
+ let ntMin = Math.floor(range.first / dtSec + Number.EPSILON) + 0.5;
2693
+ const ntMax = Math.ceil(range.last / dtSec - Number.EPSILON) - 0.5;
2694
+ if (ntMin < 0.5) ntMin = 0.5;
2695
+ if (ntMax < ntMin) return invalid("Unable to build regular PNS raster.");
2696
+ const nSamples = Math.floor(ntMax - ntMin + 1);
2697
+ if (nSamples < 2) return invalid("Too few samples for PNS computation.");
2698
+ const longestTauMs = Math.max(
2699
+ hardware.x.tau1Ms,
2700
+ hardware.x.tau2Ms,
2701
+ hardware.x.tau3Ms,
2702
+ hardware.y.tau1Ms,
2703
+ hardware.y.tau2Ms,
2704
+ hardware.y.tau3Ms,
2705
+ hardware.z.tau1Ms,
2706
+ hardware.z.tau2Ms,
2707
+ hardware.z.tau3Ms
2708
+ );
2709
+ const zptSec = longestTauMs * 4 / 1e3;
2710
+ const preCount = Math.max(0, Math.round(zptSec / (4 * dtSec)));
2711
+ const postCount = Math.max(0, Math.round(zptSec / dtSec));
2712
+ const totalSamples = preCount + nSamples + postCount;
2713
+ const hasAnyNonTrap = blocks.some((block) => block.gx?.type === "arb" || block.gy?.type === "arb" || block.gz?.type === "arb");
2714
+ const hasAnyLabelExt = blocks.some((block) => !!(block.labelSets?.length || block.labelIncs?.length));
2715
+ const shift = hasAnyNonTrap || hasAnyLabelExt ? 1 : 0;
2716
+ const stimLength = Math.max(0, totalSamples - 1);
2717
+ const desiredStimIndex = (origIndex) => {
2718
+ const paddedIndex = preCount + origIndex;
2719
+ if (shift > 0 && hasAnyLabelExt && origIndex === nSamples - 1) {
2720
+ return Math.min(paddedIndex, stimLength - 1);
2721
+ }
2722
+ return paddedIndex - shift;
2723
+ };
2724
+ const startSec = ntMin * dtSec;
2725
+ const endSec = (ntMin + nSamples - 1) * dtSec;
2726
+ const maxPoints = Math.max(1024, Math.min(12e4, options.maxPoints ?? 3e4));
2727
+ const maxBuckets = Math.floor(maxPoints / 4);
2728
+ const builders = [
2729
+ new BoundedSeriesBuilder(startSec, endSec, maxPoints),
2730
+ new BoundedSeriesBuilder(startSec, endSec, maxPoints),
2731
+ new BoundedSeriesBuilder(startSec, endSec, maxPoints),
2732
+ new BoundedSeriesBuilder(startSec, endSec, maxPoints)
2733
+ ];
2734
+ const samplers = [
2735
+ createDecodedGradientSampler(blocks, "gx"),
2736
+ createDecodedGradientSampler(blocks, "gy"),
2737
+ createDecodedGradientSampler(blocks, "gz")
2738
+ ];
2739
+ const hardwareAxes = [hardware.x, hardware.y, hardware.z];
2740
+ const filterStates = hardwareAxes.map((axis) => createPnsFilterState(axis, dtSec));
2741
+ const paddedValue = (axis, index) => {
2742
+ if (index < preCount || index >= preCount + nSamples) return 0;
2743
+ const rasterIndex = index - preCount;
2744
+ return samplers[axis]((ntMin + rasterIndex) * dtSec) / gammaHzPerT;
2745
+ };
2746
+ const previous = [paddedValue(0, 0), paddedValue(1, 0), paddedValue(2, 0)];
2747
+ let outputIndex = 0;
2748
+ let ok = true;
2749
+ for (let stimIndex = 0; stimIndex < stimLength; stimIndex++) {
2750
+ const normalized = [0, 0, 0];
2751
+ for (let axis = 0; axis < 3; axis++) {
2752
+ const current = paddedValue(axis, stimIndex + 1);
2753
+ const derivative = (current - previous[axis]) / dtSec;
2754
+ previous[axis] = current;
2755
+ normalized[axis] = updatePnsFilter(filterStates[axis], derivative);
2756
+ }
2757
+ while (outputIndex < nSamples && desiredStimIndex(outputIndex) < stimIndex) outputIndex++;
2758
+ if (outputIndex < nSamples && desiredStimIndex(outputIndex) === stimIndex) {
2759
+ const timeSec = (ntMin + outputIndex) * dtSec;
2760
+ const norm = Math.hypot(normalized[0], normalized[1], normalized[2]);
2761
+ builders[0].add(timeSec, normalized[0]);
2762
+ builders[1].add(timeSec, normalized[1]);
2763
+ builders[2].add(timeSec, normalized[2]);
2764
+ builders[3].add(timeSec, norm);
2765
+ if (norm >= 1) ok = false;
2766
+ outputIndex++;
2767
+ }
2768
+ }
2769
+ const warnings = [
2770
+ `Showing a bounded full-sequence PNS envelope (at most ${maxBuckets.toLocaleString()} buckets per curve). Zoom to 100 TRs or fewer for an automatic detailed calculation.`
2771
+ ];
2772
+ return {
2773
+ valid: true,
2774
+ ok,
2775
+ coarse: true,
2776
+ startSec,
2777
+ endSec,
2778
+ x: builders[0].finish(),
2779
+ y: builders[1].finish(),
2780
+ z: builders[2].finish(),
2781
+ norm: builders[3].finish(),
2782
+ warnings
2783
+ };
2784
+ }
2785
+ function createPnsFilterState(hardware, dtSec) {
2786
+ const dtMs = dtSec * 1e3;
2787
+ return {
2788
+ alpha1: lowpassAlpha(hardware.tau1Ms, dtMs),
2789
+ alpha2: lowpassAlpha(hardware.tau2Ms, dtMs),
2790
+ alpha3: lowpassAlpha(hardware.tau3Ms, dtMs),
2791
+ lp1: 0,
2792
+ lp2: 0,
2793
+ lp3: 0,
2794
+ hardware
2795
+ };
2796
+ }
2797
+ function updatePnsFilter(state, derivative) {
2798
+ state.lp1 = state.alpha1 * derivative + (1 - state.alpha1) * state.lp1;
2799
+ state.lp2 = state.alpha2 * Math.abs(derivative) + (1 - state.alpha2) * state.lp2;
2800
+ state.lp3 = state.alpha3 * derivative + (1 - state.alpha3) * state.lp3;
2801
+ const hw = state.hardware;
2802
+ const numerator = hw.a1 * Math.abs(state.lp1) + hw.a2 * state.lp2 + hw.a3 * Math.abs(state.lp3);
2803
+ return numerator / (hw.stimLimit > 0 ? hw.stimLimit : 1) * hw.gScale;
2804
+ }
2394
2805
  function safePnsModel(dgdt, dtSec, hw) {
2395
2806
  return runPnsModel(dgdt.length, (index) => dgdt[index], dtSec, hw);
2396
2807
  }
@@ -2529,7 +2940,7 @@ var Pulseq = (() => {
2529
2940
  function appendGradientPoint2(series, t, value) {
2530
2941
  if (!Number.isFinite(t) || !Number.isFinite(value)) return;
2531
2942
  const last = series.time.length - 1;
2532
- if (last >= 0 && Math.abs(t - series.time[last]) <= TIME_EPS2) {
2943
+ if (last >= 0 && Math.abs(t - series.time[last]) <= TIME_EPS3) {
2533
2944
  series.value[last] = 0.5 * (series.value[last] + value);
2534
2945
  } else if (last < 0 || t > series.time[last]) {
2535
2946
  series.time.push(t);
@@ -2557,9 +2968,9 @@ var Pulseq = (() => {
2557
2968
  return (t) => {
2558
2969
  const n = series.time.length;
2559
2970
  if (n === 0 || t < series.time[0] || t > series.time[n - 1]) return 0;
2560
- while (index + 1 < n && series.time[index + 1] <= t + TIME_EPS2) index++;
2971
+ while (index + 1 < n && series.time[index + 1] <= t + TIME_EPS3) index++;
2561
2972
  if (index < 0) return 0;
2562
- if (index >= n - 1 || t <= series.time[index] + TIME_EPS2) return series.value[index];
2973
+ if (index >= n - 1 || t <= series.time[index] + TIME_EPS3) return series.value[index];
2563
2974
  const t0 = series.time[index];
2564
2975
  const t1 = series.time[index + 1];
2565
2976
  if (!(t1 > t0)) return series.value[index];
@@ -2568,6 +2979,135 @@ var Pulseq = (() => {
2568
2979
  };
2569
2980
  }
2570
2981
 
2982
+ // src/pulseq/derivedWindow.ts
2983
+ function selectM1WindowBlocks(blocks, startSec, endSec) {
2984
+ const displayStartSec = finiteMin(startSec, endSec);
2985
+ const displayEndSec = finiteMax(startSec, endSec, displayStartSec);
2986
+ let calculationStartSec = displayStartSec;
2987
+ for (const block of blocks) {
2988
+ const center = block.rf?.centerTime;
2989
+ if (center === void 0 || center > displayStartSec) continue;
2990
+ const use = (block.rf?.use || "").toLowerCase();
2991
+ if (use === "e") calculationStartSec = Math.min(center, block.startTime);
2992
+ }
2993
+ return {
2994
+ blocks: overlappingBlocks(blocks, calculationStartSec, displayEndSec),
2995
+ calculationStartSec,
2996
+ displayStartSec,
2997
+ displayEndSec
2998
+ };
2999
+ }
3000
+ function selectPnsWindowBlocks(blocks, startSec, endSec, hardware) {
3001
+ const displayStartSec = finiteMin(startSec, endSec);
3002
+ const displayEndSec = finiteMax(startSec, endSec, displayStartSec);
3003
+ const longestTauMs = Math.max(
3004
+ hardware.x.tau1Ms,
3005
+ hardware.x.tau2Ms,
3006
+ hardware.x.tau3Ms,
3007
+ hardware.y.tau1Ms,
3008
+ hardware.y.tau2Ms,
3009
+ hardware.y.tau3Ms,
3010
+ hardware.z.tau1Ms,
3011
+ hardware.z.tau2Ms,
3012
+ hardware.z.tau3Ms
3013
+ );
3014
+ const calculationStartSec = Math.max(0, displayStartSec - longestTauMs * 4 / 1e3);
3015
+ return {
3016
+ blocks: overlappingBlocks(blocks, calculationStartSec, displayEndSec),
3017
+ calculationStartSec,
3018
+ displayStartSec,
3019
+ displayEndSec
3020
+ };
3021
+ }
3022
+ function overlappingBlocks(blocks, startSec, endSec) {
3023
+ return blocks.filter((block) => block.startTime + block.duration > startSec && block.startTime <= endSec);
3024
+ }
3025
+ function finiteMin(a, b) {
3026
+ const aa = Number.isFinite(a) ? a : 0;
3027
+ const bb = Number.isFinite(b) ? b : aa;
3028
+ return Math.max(0, Math.min(aa, bb));
3029
+ }
3030
+ function finiteMax(a, b, fallback) {
3031
+ const aa = Number.isFinite(a) ? a : fallback;
3032
+ const bb = Number.isFinite(b) ? b : aa;
3033
+ return Math.max(fallback, Math.max(aa, bb));
3034
+ }
3035
+
3036
+ // src/pulseq/computeBudget.ts
3037
+ var INTERACTIVE_COMPUTE_LIMITS = Object.freeze({
3038
+ kspaceRasterSamples: 12e6,
3039
+ kspaceAdcSamples: 8e6,
3040
+ kspaceGridCandidates: 18e6,
3041
+ derivedRasterSamples: 2e6
3042
+ });
3043
+ function estimateKspaceCost(blocks, gradientRaster, totalDuration) {
3044
+ let adcSamples = 0;
3045
+ let gradientSupportPoints = 0;
3046
+ let rfSupportPoints = 0;
3047
+ for (const block of blocks) {
3048
+ if (block.adc?.numSamples && block.adc.numSamples > 0) {
3049
+ adcSamples += block.adc.numSamples;
3050
+ }
3051
+ for (const gradient of [block.gx, block.gy, block.gz]) {
3052
+ if (gradient && gradient.type !== "none" && gradient.timePoints.length >= 2) {
3053
+ gradientSupportPoints += 2;
3054
+ }
3055
+ }
3056
+ if (block.rf) rfSupportPoints += block.rf.use === "r" ? 2 : 3;
3057
+ }
3058
+ const rasterSamples = gradientRaster > 0 && totalDuration > 0 ? Math.max(2, Math.round(totalDuration / gradientRaster) + 1) : 0;
3059
+ const gridCandidatePoints = rasterSamples + adcSamples + gradientSupportPoints + rfSupportPoints + 2;
3060
+ return { rasterSamples, adcSamples, gridCandidatePoints };
3061
+ }
3062
+ function estimateKspacePeakMemoryBytes(estimate) {
3063
+ const gridBytes = Math.max(0, estimate.gridCandidatePoints) * 96;
3064
+ const adcAndTransferBytes = Math.max(0, estimate.adcSamples) * 104;
3065
+ return Math.ceil(Math.min(Number.MAX_SAFE_INTEGER, (gridBytes + adcAndTransferBytes) * 1.25));
3066
+ }
3067
+ function estimateDerivedCost(blocks, gradientRaster) {
3068
+ let firstGradientTime = Infinity;
3069
+ let lastGradientTime = -Infinity;
3070
+ for (const block of blocks) {
3071
+ for (const gradient of [block.gx, block.gy, block.gz]) {
3072
+ const times = gradient?.timePoints;
3073
+ if (!times?.length) continue;
3074
+ const first = times[0];
3075
+ const last = times[times.length - 1];
3076
+ if (Number.isFinite(first) && first < firstGradientTime) firstGradientTime = first;
3077
+ if (Number.isFinite(last) && last > lastGradientTime) lastGradientTime = last;
3078
+ }
3079
+ }
3080
+ if (!Number.isFinite(firstGradientTime) || !Number.isFinite(lastGradientTime) || lastGradientTime < firstGradientTime || gradientRaster <= 0) {
3081
+ return { rasterSamples: 0, firstGradientTime: null, lastGradientTime: null };
3082
+ }
3083
+ const span = lastGradientTime - firstGradientTime;
3084
+ let rasterSamples = Math.max(1, Math.floor(span / gradientRaster) + 1);
3085
+ const finalRasterTime = firstGradientTime + (rasterSamples - 1) * gradientRaster;
3086
+ if (finalRasterTime < lastGradientTime - 1e-15) rasterSamples++;
3087
+ return { rasterSamples, firstGradientTime, lastGradientTime };
3088
+ }
3089
+ function formatSampleCount(value) {
3090
+ if (value >= 1e6) return `${(value / 1e6).toFixed(1)} million`;
3091
+ if (value >= 1e3) return `${(value / 1e3).toFixed(1)} thousand`;
3092
+ return String(value);
3093
+ }
3094
+ function formatMemorySize(bytes) {
3095
+ const safeBytes = Math.max(0, Number.isFinite(bytes) ? bytes : 0);
3096
+ const kib = 1024;
3097
+ const mib = kib * 1024;
3098
+ const gib = mib * 1024;
3099
+ if (safeBytes >= gib) {
3100
+ const value = safeBytes / gib;
3101
+ return `${value.toFixed(value >= 10 ? 0 : 1)} GiB`;
3102
+ }
3103
+ if (safeBytes >= mib) {
3104
+ const value = safeBytes / mib;
3105
+ return `${value.toFixed(value >= 10 ? 0 : 1)} MiB`;
3106
+ }
3107
+ if (safeBytes >= kib) return `${(safeBytes / kib).toFixed(1)} KiB`;
3108
+ return `${Math.round(safeBytes)} bytes`;
3109
+ }
3110
+
2571
3111
  // src/pulseq/trdetect.ts
2572
3112
  var GAMMA_HZ_T2 = 42576e3;
2573
3113
  var DEFAULT_B0_T2 = 3;
File without changes