seqeyes-python 0.2.6__py3-none-any.whl → 0.2.7__py3-none-any.whl
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.
- seqeyes/__init__.py +1 -1
- seqeyes/resources/pulseq-bundle.js +570 -30
- {seqeyes_python-0.2.6.dist-info → seqeyes_python-0.2.7.dist-info}/METADATA +1 -1
- seqeyes_python-0.2.7.dist-info/RECORD +8 -0
- seqeyes_python-0.2.6.dist-info/RECORD +0 -8
- {seqeyes_python-0.2.6.dist-info → seqeyes_python-0.2.7.dist-info}/WHEEL +0 -0
seqeyes/__init__.py
CHANGED
|
@@ -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.
|
|
56
|
+
var version = "0.2.7";
|
|
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/
|
|
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 (!
|
|
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
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
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
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
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]) <=
|
|
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 -
|
|
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 +
|
|
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] -
|
|
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]) <=
|
|
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 +
|
|
2203
|
-
while (currentT < targetT -
|
|
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 -
|
|
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
|
|
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]) <=
|
|
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 +
|
|
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] +
|
|
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;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: seqeyes-python
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
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
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
seqeyes/__init__.py,sha256=CkueqLPBYnhJ9e4fuwd-FMMbeN5c9QGoGq_xArH7Nso,944
|
|
2
|
+
seqeyes/_plot.py,sha256=v5cPAn3mO017lQlNR4bthC0QgIBK1YxjygJTAPFUz1E,8114
|
|
3
|
+
seqeyes/_renderer.py,sha256=vIB0lBeuOyK6p3MyQ0qeBy-JoEUVGISxtfdny7JoqKE,9862
|
|
4
|
+
seqeyes/resources/pulseq-bundle.js,sha256=CEmr0iT7GW_PswlxBJpD21q7e9igqLzotzywjBQbTEY,127007
|
|
5
|
+
seqeyes/resources/viewer.html,sha256=AovT33ptVfkP2iEaIBklRQGZV1tag_OBQardKtxpru8,88066
|
|
6
|
+
seqeyes_python-0.2.7.dist-info/METADATA,sha256=89YG68tvjFmsNplgLUpI6RiSSah4Tui3Nf6uZELYdaw,4502
|
|
7
|
+
seqeyes_python-0.2.7.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
8
|
+
seqeyes_python-0.2.7.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
seqeyes/__init__.py,sha256=GpvQX637Z8yqYHz6arELQmXasuQm8dC-sJu6M1IP0Fs,944
|
|
2
|
-
seqeyes/_plot.py,sha256=v5cPAn3mO017lQlNR4bthC0QgIBK1YxjygJTAPFUz1E,8114
|
|
3
|
-
seqeyes/_renderer.py,sha256=vIB0lBeuOyK6p3MyQ0qeBy-JoEUVGISxtfdny7JoqKE,9862
|
|
4
|
-
seqeyes/resources/pulseq-bundle.js,sha256=IJpZwkgi3-pftb3qVMuUNK1xMQwkVpbXecuj_jem2JE,104992
|
|
5
|
-
seqeyes/resources/viewer.html,sha256=AovT33ptVfkP2iEaIBklRQGZV1tag_OBQardKtxpru8,88066
|
|
6
|
-
seqeyes_python-0.2.6.dist-info/METADATA,sha256=9jH27I0J6qeaDP9gjHtvDdsjRHDifiwstn4GueZ-4Hw,4502
|
|
7
|
-
seqeyes_python-0.2.6.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
8
|
-
seqeyes_python-0.2.6.dist-info/RECORD,,
|
|
File without changes
|