seqeyes-python 0.3.9__tar.gz → 0.3.11__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.
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.11}/PKG-INFO +1 -1
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.11}/pyproject.toml +1 -1
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.11}/src/seqeyes/__init__.py +1 -1
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.11}/src/seqeyes/resources/pulseq-bundle.js +287 -10
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.11}/src/seqeyes/resources/seqeyes-labels.js +20 -4
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.11}/src/seqeyes/resources/webview-bundle.js +814 -92
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.11}/.gitignore +0 -0
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.11}/README.md +0 -0
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.11}/examples/demo.ipynb +0 -0
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.11}/examples/demo.py +0 -0
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.11}/src/seqeyes/_plot.py +0 -0
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.11}/src/seqeyes/_renderer.py +0 -0
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.11}/src/seqeyes/resources/viewer.html +0 -0
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.11}/tests/conftest.py +0 -0
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.11}/tests/debug_full.html +0 -0
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.11}/tests/debug_minimal.html +0 -0
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.11}/tests/debug_viewer.html +0 -0
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.11}/tests/debug_viewer.py +0 -0
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.11}/tests/integration_test.py +0 -0
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.11}/tests/test_plot.py +0 -0
- {seqeyes_python-0.3.9 → seqeyes_python-0.3.11}/tests/test_renderer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: seqeyes-python
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.11
|
|
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.3.
|
|
7
|
+
version = "0.3.11"
|
|
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"}
|
|
@@ -89,7 +89,7 @@ var Pulseq = (() => {
|
|
|
89
89
|
});
|
|
90
90
|
|
|
91
91
|
// package.json
|
|
92
|
-
var version = "0.3.
|
|
92
|
+
var version = "0.3.11";
|
|
93
93
|
|
|
94
94
|
// src/pulseq/decompressor.ts
|
|
95
95
|
function decompressShape(compressed, numSamples) {
|
|
@@ -2399,7 +2399,7 @@ var Pulseq = (() => {
|
|
|
2399
2399
|
decimatedSamples,
|
|
2400
2400
|
columns,
|
|
2401
2401
|
fftPoints,
|
|
2402
|
-
totalCells: columns * bins * 4,
|
|
2402
|
+
totalCells: columns * bins * (input.includeRf ? 7 : 4),
|
|
2403
2403
|
decimationFactor
|
|
2404
2404
|
};
|
|
2405
2405
|
}
|
|
@@ -4657,6 +4657,125 @@ var Pulseq = (() => {
|
|
|
4657
4657
|
return Math.floor((coreSamples - 1) / Math.max(1, factor)) + 1;
|
|
4658
4658
|
}
|
|
4659
4659
|
|
|
4660
|
+
// src/pulseq/rfAcoustic.ts
|
|
4661
|
+
function normalizePeak(values) {
|
|
4662
|
+
let peak = 0;
|
|
4663
|
+
for (let i = 0; i < values.length; i++) {
|
|
4664
|
+
const v = values[i] < 0 ? -values[i] : values[i];
|
|
4665
|
+
if (v > peak) peak = v;
|
|
4666
|
+
}
|
|
4667
|
+
if (peak > 0) for (let i = 0; i < values.length; i++) values[i] /= peak;
|
|
4668
|
+
}
|
|
4669
|
+
function rfPowerAt(rf, t) {
|
|
4670
|
+
const tp = rf.timePoints, mag = rf.magnitude;
|
|
4671
|
+
const last = tp.length - 1;
|
|
4672
|
+
if (last < 0) return 0;
|
|
4673
|
+
if (last === 0) return mag[0] * mag[0];
|
|
4674
|
+
if (t < tp[0] - GRADIENT_ENDPOINT_TOLERANCE_SEC || t > tp[last] + GRADIENT_ENDPOINT_TOLERANCE_SEC) return 0;
|
|
4675
|
+
if (t <= tp[0]) return mag[0] * mag[0];
|
|
4676
|
+
if (t >= tp[last]) return mag[last] * mag[last];
|
|
4677
|
+
let lo = 0, hi = last;
|
|
4678
|
+
while (hi - lo > 1) {
|
|
4679
|
+
const m = lo + hi >> 1;
|
|
4680
|
+
if (tp[m] <= t) lo = m;
|
|
4681
|
+
else hi = m;
|
|
4682
|
+
}
|
|
4683
|
+
const span = tp[hi] - tp[lo];
|
|
4684
|
+
const pLo = mag[lo] * mag[lo];
|
|
4685
|
+
if (span <= 0) return pLo;
|
|
4686
|
+
const pHi = mag[hi] * mag[hi];
|
|
4687
|
+
return pLo + (pHi - pLo) * (t - tp[lo]) / span;
|
|
4688
|
+
}
|
|
4689
|
+
function rfPowerArea(rf) {
|
|
4690
|
+
const tp = rf.timePoints, mag = rf.magnitude;
|
|
4691
|
+
let area = 0;
|
|
4692
|
+
for (let i = 1; i < tp.length; i++) {
|
|
4693
|
+
const pPrev = mag[i - 1] * mag[i - 1];
|
|
4694
|
+
const pCurr = mag[i] * mag[i];
|
|
4695
|
+
area += 0.5 * (pPrev + pCurr) * (tp[i] - tp[i - 1]);
|
|
4696
|
+
}
|
|
4697
|
+
return area;
|
|
4698
|
+
}
|
|
4699
|
+
function resampleRfAcousticSources(blocks, options) {
|
|
4700
|
+
const dt = options.dt;
|
|
4701
|
+
if (!(dt > 0)) throw new Error("resampleRfAcousticSources requires a positive dt.");
|
|
4702
|
+
const t0 = Number.isFinite(options.startSec) ? options.startSec : 0;
|
|
4703
|
+
const n = Math.max(0, Math.floor(options.sampleCount));
|
|
4704
|
+
const rfScale = Math.max(0, Number.isFinite(options.rfScale) ? options.rfScale : 1);
|
|
4705
|
+
const edgeMode = options.edgeMode === "absolute" ? "absolute" : "signed";
|
|
4706
|
+
const power = new Float32Array(n);
|
|
4707
|
+
const gate = new Float32Array(n);
|
|
4708
|
+
const thermo = new Float32Array(n);
|
|
4709
|
+
const control = new Float32Array(n);
|
|
4710
|
+
const empty = {
|
|
4711
|
+
t0,
|
|
4712
|
+
dt,
|
|
4713
|
+
n,
|
|
4714
|
+
power,
|
|
4715
|
+
gate,
|
|
4716
|
+
thermo,
|
|
4717
|
+
control,
|
|
4718
|
+
silent: true,
|
|
4719
|
+
eventCount: 0,
|
|
4720
|
+
subSampleEventCount: 0
|
|
4721
|
+
};
|
|
4722
|
+
if (!n) return empty;
|
|
4723
|
+
const tEnd = t0 + (n - 1) * dt;
|
|
4724
|
+
let eventCount = 0;
|
|
4725
|
+
let subSampleEventCount = 0;
|
|
4726
|
+
for (const block of blocks) {
|
|
4727
|
+
const rf = block.rf;
|
|
4728
|
+
if (!rf || !rf.timePoints || !rf.timePoints.length || !rf.magnitude.length) continue;
|
|
4729
|
+
const last = rf.timePoints.length - 1;
|
|
4730
|
+
const rfStart = rf.timePoints[0];
|
|
4731
|
+
const rfEnd = rf.timePoints[last];
|
|
4732
|
+
if (rfEnd < t0 - GRADIENT_ENDPOINT_TOLERANCE_SEC || rfStart > tEnd + GRADIENT_ENDPOINT_TOLERANCE_SEC) continue;
|
|
4733
|
+
eventCount++;
|
|
4734
|
+
const gateLo = clampIndex(Math.floor((rfStart - t0) / dt), n);
|
|
4735
|
+
const gateHi = clampIndex(Math.ceil((rfEnd - t0) / dt), n);
|
|
4736
|
+
for (let i = gateLo; i <= gateHi; i++) gate[i] = 1;
|
|
4737
|
+
const coverLo = clampIndex(Math.ceil((rfStart - t0) / dt), n);
|
|
4738
|
+
const coverHi = clampIndex(Math.floor((rfEnd - t0) / dt), n);
|
|
4739
|
+
const spansASample = rfEnd - rfStart >= dt && coverHi >= coverLo;
|
|
4740
|
+
if (spansASample) {
|
|
4741
|
+
for (let i = coverLo; i <= coverHi; i++) power[i] += rfPowerAt(rf, t0 + i * dt);
|
|
4742
|
+
} else {
|
|
4743
|
+
subSampleEventCount++;
|
|
4744
|
+
const area = last === 0 ? rf.magnitude[0] * rf.magnitude[0] * dt : rfPowerArea(rf);
|
|
4745
|
+
const mid = clampIndex(Math.round((0.5 * (rfStart + rfEnd) - t0) / dt), n);
|
|
4746
|
+
power[mid] += area / dt;
|
|
4747
|
+
}
|
|
4748
|
+
}
|
|
4749
|
+
const silent = eventCount === 0;
|
|
4750
|
+
if (silent) return { ...empty, silent: true };
|
|
4751
|
+
normalizePeak(power);
|
|
4752
|
+
const invDt = 1 / dt;
|
|
4753
|
+
for (let i = 1; i < n; i++) thermo[i] = (power[i] - power[i - 1]) * invDt;
|
|
4754
|
+
normalizePeak(thermo);
|
|
4755
|
+
const amplitudeGain = rfScale * rfScale;
|
|
4756
|
+
if (amplitudeGain !== 1) for (let i = 0; i < n; i++) thermo[i] *= amplitudeGain;
|
|
4757
|
+
for (let i = 1; i < n; i++) {
|
|
4758
|
+
const d = gate[i] - gate[i - 1];
|
|
4759
|
+
control[i] = edgeMode === "absolute" ? Math.abs(d) : d;
|
|
4760
|
+
}
|
|
4761
|
+
return { t0, dt, n, power, gate, thermo, control, silent: false, eventCount, subSampleEventCount };
|
|
4762
|
+
}
|
|
4763
|
+
function clampIndex(index, n) {
|
|
4764
|
+
if (!Number.isFinite(index)) return 0;
|
|
4765
|
+
if (index < 0) return 0;
|
|
4766
|
+
if (index > n - 1) return n - 1;
|
|
4767
|
+
return index;
|
|
4768
|
+
}
|
|
4769
|
+
function combineRfSources(sources, thermoWeight, controlWeight) {
|
|
4770
|
+
const wThermo = Number.isFinite(thermoWeight) ? thermoWeight : 1;
|
|
4771
|
+
const wControl = Number.isFinite(controlWeight) ? controlWeight : 1;
|
|
4772
|
+
const out = new Float32Array(sources.n);
|
|
4773
|
+
for (let i = 0; i < sources.n; i++) {
|
|
4774
|
+
out[i] = wThermo * sources.thermo[i] + wControl * sources.control[i];
|
|
4775
|
+
}
|
|
4776
|
+
return out;
|
|
4777
|
+
}
|
|
4778
|
+
|
|
4660
4779
|
// src/pulseq/gradSpectrum.ts
|
|
4661
4780
|
var GAMMA_HZ_PER_M_PER_MT_PER_M = 42576;
|
|
4662
4781
|
var GAMMA_HZ_PER_M_PER_T_PER_M = 42576e3;
|
|
@@ -4669,7 +4788,15 @@ var Pulseq = (() => {
|
|
|
4669
4788
|
overlap: 0.75,
|
|
4670
4789
|
oversample: 3,
|
|
4671
4790
|
targetColumns: 256,
|
|
4672
|
-
normalize: true
|
|
4791
|
+
normalize: true,
|
|
4792
|
+
// Off by default: the RF terms are research proxies with no calibrated
|
|
4793
|
+
// level, so they are opt-in rather than something a user meets by accident
|
|
4794
|
+
// in a panel whose other channels are in mT/m.
|
|
4795
|
+
includeRf: false,
|
|
4796
|
+
rfScale: 1,
|
|
4797
|
+
rfThermoWeight: 1,
|
|
4798
|
+
rfControlWeight: 1,
|
|
4799
|
+
rfEdgeMode: "signed"
|
|
4673
4800
|
});
|
|
4674
4801
|
function resolveSpectrogramParams(params) {
|
|
4675
4802
|
const merged = { ...DEFAULT_SPECTROGRAM_PARAMS, ...params ?? {} };
|
|
@@ -4682,7 +4809,12 @@ var Pulseq = (() => {
|
|
|
4682
4809
|
overlap: clamp2(finite(merged.overlap, 0.75), 0, 0.9375),
|
|
4683
4810
|
oversample: clamp2(Math.round(finite(merged.oversample, 3)), 1, 4),
|
|
4684
4811
|
targetColumns: clamp2(Math.round(finite(merged.targetColumns, 256)), 64, 512),
|
|
4685
|
-
normalize: merged.normalize !== false
|
|
4812
|
+
normalize: merged.normalize !== false,
|
|
4813
|
+
includeRf: merged.includeRf === true,
|
|
4814
|
+
rfScale: clamp2(finite(merged.rfScale, 1), 0, 1e3),
|
|
4815
|
+
rfThermoWeight: clamp2(finite(merged.rfThermoWeight, 1), 0, 1e3),
|
|
4816
|
+
rfControlWeight: clamp2(finite(merged.rfControlWeight, 1), 0, 1e3),
|
|
4817
|
+
rfEdgeMode: merged.rfEdgeMode === "absolute" ? "absolute" : "signed"
|
|
4686
4818
|
};
|
|
4687
4819
|
}
|
|
4688
4820
|
function chooseWindowSamples(params, decimatedDt, viewDurationSec, warnings) {
|
|
@@ -4786,6 +4918,29 @@ var Pulseq = (() => {
|
|
|
4786
4918
|
const dx = decimatePadded(sx, plan, padSamples, nDecimated);
|
|
4787
4919
|
const dy = decimatePadded(sy, plan, padSamples, nDecimated);
|
|
4788
4920
|
const dz = decimatePadded(sz, plan, padSamples, nDecimated);
|
|
4921
|
+
let dRfThermo = null;
|
|
4922
|
+
let dRfControl = null;
|
|
4923
|
+
let dRfCombined = null;
|
|
4924
|
+
if (params.includeRf) {
|
|
4925
|
+
const rfSources = resampleRfAcousticSources(blocks, {
|
|
4926
|
+
startSec: startSec - padSamples * raster,
|
|
4927
|
+
dt: raster,
|
|
4928
|
+
sampleCount: totalSamples,
|
|
4929
|
+
rfScale: params.rfScale,
|
|
4930
|
+
edgeMode: params.rfEdgeMode
|
|
4931
|
+
});
|
|
4932
|
+
if (rfSources.silent) {
|
|
4933
|
+
warnings.push("No RF events in the visible window; the RF proxy channels are empty.");
|
|
4934
|
+
} else if (rfSources.subSampleEventCount > 0) {
|
|
4935
|
+
warnings.push(
|
|
4936
|
+
`${rfSources.subSampleEventCount} RF event(s) are shorter than the ${formatSeconds(raster)} gradient raster and were deposited as area-preserving impulses; their edge timing is raster-quantised.`
|
|
4937
|
+
);
|
|
4938
|
+
}
|
|
4939
|
+
const combined = combineRfSources(rfSources, params.rfThermoWeight, params.rfControlWeight);
|
|
4940
|
+
dRfThermo = decimatePadded(rfSources.thermo, plan, padSamples, nDecimated);
|
|
4941
|
+
dRfControl = decimatePadded(rfSources.control, plan, padSamples, nDecimated);
|
|
4942
|
+
dRfCombined = decimatePadded(combined, plan, padSamples, nDecimated);
|
|
4943
|
+
}
|
|
4789
4944
|
const binLow = Math.max(0, Math.floor(params.fMinHz / dfBin));
|
|
4790
4945
|
const binHigh = Math.min(fftPoints / 2, Math.ceil(params.fMaxHz / dfBin));
|
|
4791
4946
|
const nFreq = Math.max(1, binHigh - binLow + 1);
|
|
@@ -4794,6 +4949,9 @@ var Pulseq = (() => {
|
|
|
4794
4949
|
const gyOut = new Float32Array(cells);
|
|
4795
4950
|
const gzOut = new Float32Array(cells);
|
|
4796
4951
|
const rssOut = new Float32Array(cells);
|
|
4952
|
+
const rfThermoOut = dRfThermo ? new Float32Array(cells) : null;
|
|
4953
|
+
const rfControlOut = dRfControl ? new Float32Array(cells) : null;
|
|
4954
|
+
const rfOut = dRfCombined ? new Float32Array(cells) : null;
|
|
4797
4955
|
const w = hannWindow(windowSamples);
|
|
4798
4956
|
const gain = params.normalize ? windowCoherentGain(w) : 1;
|
|
4799
4957
|
const invGain = gain > 0 ? 1 / gain : 1;
|
|
@@ -4806,8 +4964,18 @@ var Pulseq = (() => {
|
|
|
4806
4964
|
const magA = new Float64Array(fftPoints / 2 + 1);
|
|
4807
4965
|
const magB = new Float64Array(fftPoints / 2 + 1);
|
|
4808
4966
|
const magC = new Float64Array(fftPoints / 2 + 1);
|
|
4967
|
+
const rfActive = !!(dRfThermo && dRfControl && dRfCombined && rfThermoOut && rfControlOut && rfOut);
|
|
4968
|
+
const frameD = rfActive ? new Float64Array(fftPoints) : null;
|
|
4969
|
+
const frameE = rfActive ? new Float64Array(fftPoints) : null;
|
|
4970
|
+
const frameF = rfActive ? new Float64Array(fftPoints) : null;
|
|
4971
|
+
const magD = rfActive ? new Float64Array(fftPoints / 2 + 1) : null;
|
|
4972
|
+
const magE = rfActive ? new Float64Array(fftPoints / 2 + 1) : null;
|
|
4973
|
+
const magF = rfActive ? new Float64Array(fftPoints / 2 + 1) : null;
|
|
4974
|
+
const thermoSilent = rfActive && isAllZero(dRfThermo);
|
|
4975
|
+
const controlSilent = rfActive && isAllZero(dRfControl);
|
|
4809
4976
|
let minValue = Number.POSITIVE_INFINITY;
|
|
4810
4977
|
let maxValue = 0;
|
|
4978
|
+
let rfMaxValue = 0;
|
|
4811
4979
|
for (let col = 0; col < columns; col++) {
|
|
4812
4980
|
const offset = col * hop;
|
|
4813
4981
|
prepareFrame(frameA, dx, offset, windowSamples, w, fftPoints);
|
|
@@ -4815,6 +4983,23 @@ var Pulseq = (() => {
|
|
|
4815
4983
|
prepareFrame(frameC, dz, offset, windowSamples, w, fftPoints);
|
|
4816
4984
|
realFFTPairMagnitude(frameA, frameB, fftPoints, scratchRe, scratchIm, magA, magB);
|
|
4817
4985
|
realFFTMagnitude(frameC, fftPoints, scratchRe, scratchIm, magC);
|
|
4986
|
+
if (rfActive) {
|
|
4987
|
+
prepareFrame(frameF, dRfCombined, offset, windowSamples, w, fftPoints);
|
|
4988
|
+
realFFTMagnitude(frameF, fftPoints, scratchRe, scratchIm, magF);
|
|
4989
|
+
if (thermoSilent) magD.fill(0);
|
|
4990
|
+
if (controlSilent) magE.fill(0);
|
|
4991
|
+
if (!thermoSilent && !controlSilent) {
|
|
4992
|
+
prepareFrame(frameD, dRfThermo, offset, windowSamples, w, fftPoints);
|
|
4993
|
+
prepareFrame(frameE, dRfControl, offset, windowSamples, w, fftPoints);
|
|
4994
|
+
realFFTPairMagnitude(frameD, frameE, fftPoints, scratchRe, scratchIm, magD, magE);
|
|
4995
|
+
} else if (!thermoSilent) {
|
|
4996
|
+
prepareFrame(frameD, dRfThermo, offset, windowSamples, w, fftPoints);
|
|
4997
|
+
realFFTMagnitude(frameD, fftPoints, scratchRe, scratchIm, magD);
|
|
4998
|
+
} else if (!controlSilent) {
|
|
4999
|
+
prepareFrame(frameE, dRfControl, offset, windowSamples, w, fftPoints);
|
|
5000
|
+
realFFTMagnitude(frameE, fftPoints, scratchRe, scratchIm, magE);
|
|
5001
|
+
}
|
|
5002
|
+
}
|
|
4818
5003
|
for (let bin = binLow; bin <= binHigh; bin++) {
|
|
4819
5004
|
const row = bin - binLow;
|
|
4820
5005
|
const index = row * columns + col;
|
|
@@ -4828,6 +5013,13 @@ var Pulseq = (() => {
|
|
|
4828
5013
|
rssOut[index] = vr;
|
|
4829
5014
|
if (vr > maxValue) maxValue = vr;
|
|
4830
5015
|
if (vr < minValue) minValue = vr;
|
|
5016
|
+
if (rfActive) {
|
|
5017
|
+
const vRf = magF[bin] * invGain;
|
|
5018
|
+
rfThermoOut[index] = magD[bin] * invGain;
|
|
5019
|
+
rfControlOut[index] = magE[bin] * invGain;
|
|
5020
|
+
rfOut[index] = vRf;
|
|
5021
|
+
if (vRf > rfMaxValue) rfMaxValue = vRf;
|
|
5022
|
+
}
|
|
4831
5023
|
}
|
|
4832
5024
|
}
|
|
4833
5025
|
if (!Number.isFinite(minValue)) minValue = 0;
|
|
@@ -4845,9 +5037,17 @@ var Pulseq = (() => {
|
|
|
4845
5037
|
dfResolutionHz: decimatedRate / windowSamples,
|
|
4846
5038
|
unit: params.source === "dGdt" ? "T/m/s" : "mT/m",
|
|
4847
5039
|
source: params.source,
|
|
4848
|
-
data: {
|
|
5040
|
+
data: {
|
|
5041
|
+
gx: gxOut,
|
|
5042
|
+
gy: gyOut,
|
|
5043
|
+
gz: gzOut,
|
|
5044
|
+
rss: rssOut,
|
|
5045
|
+
...rfActive ? { rfThermo: rfThermoOut, rfControl: rfControlOut, rf: rfOut } : {}
|
|
5046
|
+
},
|
|
4849
5047
|
minValue,
|
|
4850
5048
|
maxValue,
|
|
5049
|
+
rfIncluded: rfActive,
|
|
5050
|
+
rfMaxValue,
|
|
4851
5051
|
decimationFactor: plan.factor,
|
|
4852
5052
|
decimatedRateHz: decimatedRate,
|
|
4853
5053
|
windowSamples,
|
|
@@ -4904,10 +5104,17 @@ var Pulseq = (() => {
|
|
|
4904
5104
|
gx: new Float32Array(0),
|
|
4905
5105
|
gy: new Float32Array(0),
|
|
4906
5106
|
gz: new Float32Array(0),
|
|
4907
|
-
rss: new Float32Array(0)
|
|
5107
|
+
rss: new Float32Array(0),
|
|
5108
|
+
...params.includeRf ? {
|
|
5109
|
+
rfThermo: new Float32Array(0),
|
|
5110
|
+
rfControl: new Float32Array(0),
|
|
5111
|
+
rf: new Float32Array(0)
|
|
5112
|
+
} : {}
|
|
4908
5113
|
},
|
|
4909
5114
|
minValue: 0,
|
|
4910
5115
|
maxValue: 0,
|
|
5116
|
+
rfIncluded: params.includeRf,
|
|
5117
|
+
rfMaxValue: 0,
|
|
4911
5118
|
decimationFactor,
|
|
4912
5119
|
decimatedRateHz: decimatedRate,
|
|
4913
5120
|
windowSamples,
|
|
@@ -5047,6 +5254,15 @@ var Pulseq = (() => {
|
|
|
5047
5254
|
if (value >= 10) return `${value.toFixed(0)} Hz`;
|
|
5048
5255
|
return `${value.toFixed(1)} Hz`;
|
|
5049
5256
|
}
|
|
5257
|
+
function isAllZero(values) {
|
|
5258
|
+
for (let i = 0; i < values.length; i++) if (values[i] !== 0) return false;
|
|
5259
|
+
return true;
|
|
5260
|
+
}
|
|
5261
|
+
function formatSeconds(value) {
|
|
5262
|
+
if (!Number.isFinite(value)) return "\u2014";
|
|
5263
|
+
if (value >= 1e-3) return `${(value * 1e3).toFixed(2)} ms`;
|
|
5264
|
+
return `${(value * 1e6).toFixed(1)} \xB5s`;
|
|
5265
|
+
}
|
|
5050
5266
|
|
|
5051
5267
|
// src/pulseq/gradientSound.ts
|
|
5052
5268
|
var DEFAULT_AUDIO_SAMPLE_RATE = 44100;
|
|
@@ -5121,17 +5337,78 @@ var Pulseq = (() => {
|
|
|
5121
5337
|
if (l > rawPeak) rawPeak = l;
|
|
5122
5338
|
if (r > rawPeak) rawPeak = r;
|
|
5123
5339
|
}
|
|
5124
|
-
const
|
|
5340
|
+
const rfMix = clamp01(Number.isFinite(options.rfMix) ? options.rfMix : 0.5);
|
|
5341
|
+
let rfRawPeak = 0;
|
|
5342
|
+
let rfMono = null;
|
|
5343
|
+
if (options.includeRf && rfMix > 0) {
|
|
5344
|
+
const sources = resampleRfAcousticSources(blocks, {
|
|
5345
|
+
startSec,
|
|
5346
|
+
dt,
|
|
5347
|
+
sampleCount: n,
|
|
5348
|
+
rfScale: options.rfScale,
|
|
5349
|
+
edgeMode: options.rfEdgeMode
|
|
5350
|
+
});
|
|
5351
|
+
if (!sources.silent) {
|
|
5352
|
+
const combined = combineRfSources(
|
|
5353
|
+
sources,
|
|
5354
|
+
Number.isFinite(options.rfThermoWeight) ? options.rfThermoWeight : 1,
|
|
5355
|
+
Number.isFinite(options.rfControlWeight) ? options.rfControlWeight : 1
|
|
5356
|
+
);
|
|
5357
|
+
rfMono = convolveSame(combined, kernel);
|
|
5358
|
+
for (let i = 0; i < n; i++) {
|
|
5359
|
+
const v = Math.abs(rfMono[i]);
|
|
5360
|
+
if (v > rfRawPeak) rfRawPeak = v;
|
|
5361
|
+
}
|
|
5362
|
+
}
|
|
5363
|
+
}
|
|
5364
|
+
const rfIncluded = !!(rfMono && rfRawPeak > 0);
|
|
5365
|
+
if (rfIncluded) {
|
|
5366
|
+
const gradGain = rawPeak > 0 ? (1 - rfMix) / rawPeak : 0;
|
|
5367
|
+
const rfGain = rfMix / rfRawPeak;
|
|
5368
|
+
for (let i = 0; i < n; i++) {
|
|
5369
|
+
const rfSample = rfMono[i] * rfGain;
|
|
5370
|
+
left[i] = left[i] * gradGain + rfSample;
|
|
5371
|
+
right[i] = right[i] * gradGain + rfSample;
|
|
5372
|
+
}
|
|
5373
|
+
}
|
|
5374
|
+
let mixedPeak = 0;
|
|
5375
|
+
for (let i = 0; i < n; i++) {
|
|
5376
|
+
const l = Math.abs(left[i]);
|
|
5377
|
+
const r = Math.abs(right[i]);
|
|
5378
|
+
if (l > mixedPeak) mixedPeak = l;
|
|
5379
|
+
if (r > mixedPeak) mixedPeak = r;
|
|
5380
|
+
}
|
|
5381
|
+
const silent = !(mixedPeak > 0);
|
|
5125
5382
|
if (silent) {
|
|
5126
|
-
warnings.push("No gradient activity in this window \u2014 nothing to play.");
|
|
5383
|
+
warnings.push(options.includeRf ? "Nothing to play in this window \u2014 no gradient activity and no RF events." : "No gradient activity in this window \u2014 nothing to play.");
|
|
5127
5384
|
} else {
|
|
5128
|
-
const scale = AUDIO_PEAK /
|
|
5385
|
+
const scale = AUDIO_PEAK / mixedPeak;
|
|
5129
5386
|
for (let i = 0; i < n; i++) {
|
|
5130
5387
|
left[i] *= scale;
|
|
5131
5388
|
right[i] *= scale;
|
|
5132
5389
|
}
|
|
5390
|
+
if (options.includeRf && !rfIncluded && rfMix > 0) {
|
|
5391
|
+
warnings.push("No RF events in this window; you are hearing the gradients only.");
|
|
5392
|
+
} else if (rfIncluded && rawPeak === 0) {
|
|
5393
|
+
warnings.push("No gradient activity in this window; you are hearing the RF proxy only.");
|
|
5394
|
+
}
|
|
5133
5395
|
}
|
|
5134
|
-
return {
|
|
5396
|
+
return {
|
|
5397
|
+
sampleRate,
|
|
5398
|
+
n,
|
|
5399
|
+
left,
|
|
5400
|
+
right,
|
|
5401
|
+
startSec,
|
|
5402
|
+
endSec,
|
|
5403
|
+
rawPeak,
|
|
5404
|
+
rfRawPeak,
|
|
5405
|
+
rfIncluded,
|
|
5406
|
+
silent,
|
|
5407
|
+
warnings
|
|
5408
|
+
};
|
|
5409
|
+
}
|
|
5410
|
+
function clamp01(value) {
|
|
5411
|
+
return value < 0 ? 0 : value > 1 ? 1 : value;
|
|
5135
5412
|
}
|
|
5136
5413
|
|
|
5137
5414
|
// src/pulseq/derivedWindow.ts
|
|
@@ -7,13 +7,29 @@
|
|
|
7
7
|
the 2-D context, the time→x mapping and the row geometry. The Python
|
|
8
8
|
viewer loads this file on its own, without the rest of the bundle.
|
|
9
9
|
|
|
10
|
-
The DOM and
|
|
11
|
-
|
|
10
|
+
The DOM and storage are only touched inside functions, so the tests can
|
|
11
|
+
run the shipped file in a bare VM context.
|
|
12
12
|
|
|
13
13
|
A "labels" argument is anything with `names` and `kinds`: the popup and the
|
|
14
14
|
styles need nothing more, so ⚙ works before the per-ADC values arrive. */
|
|
15
15
|
var SeqEyesLabels = (function () {
|
|
16
16
|
var STORAGE_KEY = 'seqeyes.labelStyles.v1';
|
|
17
|
+
|
|
18
|
+
/* The Python viewer inlines this file on its own, so the bundle's
|
|
19
|
+
preferences gateway may not be there. Go through it when it is, so the
|
|
20
|
+
settings popover's master switch and Forget button cover marker styles
|
|
21
|
+
too, and fall back to raw storage when it is not. */
|
|
22
|
+
function prefsApi() { return (typeof SeqEyesPrefs !== 'undefined') ? SeqEyesPrefs : null; }
|
|
23
|
+
function readStored(key) {
|
|
24
|
+
var api = prefsApi();
|
|
25
|
+
if (api) return api.get(key);
|
|
26
|
+
try { return localStorage.getItem(key); } catch (_) { return null; }
|
|
27
|
+
}
|
|
28
|
+
function writeStored(key, value) {
|
|
29
|
+
var api = prefsApi();
|
|
30
|
+
if (api) { api.set(key, value); return; }
|
|
31
|
+
try { localStorage.setItem(key, value); } catch (_) {}
|
|
32
|
+
}
|
|
17
33
|
// Tableau 10: distinct hues that stay readable on light and dark themes.
|
|
18
34
|
var PALETTE = ['#4e79a7', '#f28e2b', '#e15759', '#76b7b2', '#59a14f', '#edc948', '#b07aa1', '#ff9da7', '#9c755f', '#bab0ac'];
|
|
19
35
|
var SHAPES = ['circle', 'square', 'triangle', 'triangleDown', 'diamond', 'cross', 'plus'];
|
|
@@ -74,14 +90,14 @@ var SeqEyesLabels = (function () {
|
|
|
74
90
|
if (overrides) return overrides;
|
|
75
91
|
overrides = {};
|
|
76
92
|
try {
|
|
77
|
-
var saved = JSON.parse(
|
|
93
|
+
var saved = JSON.parse(readStored(STORAGE_KEY) || '{}');
|
|
78
94
|
if (saved && typeof saved === 'object') overrides = saved;
|
|
79
95
|
} catch (_) {}
|
|
80
96
|
return overrides;
|
|
81
97
|
}
|
|
82
98
|
|
|
83
99
|
function writeOverrides() {
|
|
84
|
-
|
|
100
|
+
writeStored(STORAGE_KEY, JSON.stringify(overrides));
|
|
85
101
|
}
|
|
86
102
|
|
|
87
103
|
/* Defaults follow the label's position in its sequence, so one sequence's
|