seqeyes-python 0.3.10__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.10 → seqeyes_python-0.3.11}/PKG-INFO +1 -1
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.11}/pyproject.toml +1 -1
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.11}/src/seqeyes/__init__.py +1 -1
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.11}/src/seqeyes/resources/pulseq-bundle.js +287 -10
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.11}/src/seqeyes/resources/webview-bundle.js +324 -38
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.11}/.gitignore +0 -0
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.11}/README.md +0 -0
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.11}/examples/demo.ipynb +0 -0
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.11}/examples/demo.py +0 -0
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.11}/src/seqeyes/_plot.py +0 -0
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.11}/src/seqeyes/_renderer.py +0 -0
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.11}/src/seqeyes/resources/seqeyes-labels.js +0 -0
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.11}/src/seqeyes/resources/viewer.html +0 -0
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.11}/tests/conftest.py +0 -0
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.11}/tests/debug_full.html +0 -0
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.11}/tests/debug_minimal.html +0 -0
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.11}/tests/debug_viewer.html +0 -0
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.11}/tests/debug_viewer.py +0 -0
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.11}/tests/integration_test.py +0 -0
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.11}/tests/test_plot.py +0 -0
- {seqeyes_python-0.3.10 → 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
|
|
@@ -279,18 +279,26 @@ function decodeB64F32(b64,n){
|
|
|
279
279
|
function deserializeSpectrogram(payload){
|
|
280
280
|
if(!payload)return null;
|
|
281
281
|
var cells=payload.nTime*payload.nFreq;
|
|
282
|
+
var data={
|
|
283
|
+
gx:decodeB64F32(payload.gxB64,cells),
|
|
284
|
+
gy:decodeB64F32(payload.gyB64,cells),
|
|
285
|
+
gz:decodeB64F32(payload.gzB64,cells),
|
|
286
|
+
rss:decodeB64F32(payload.rssB64,cells)
|
|
287
|
+
};
|
|
288
|
+
/* RF blobs travel only when the RF proxy was computed. */
|
|
289
|
+
if(payload.rfB64&&payload.rfThermoB64&&payload.rfControlB64){
|
|
290
|
+
data.rfThermo=decodeB64F32(payload.rfThermoB64,cells);
|
|
291
|
+
data.rfControl=decodeB64F32(payload.rfControlB64,cells);
|
|
292
|
+
data.rf=decodeB64F32(payload.rfB64,cells);
|
|
293
|
+
}
|
|
282
294
|
return{
|
|
283
295
|
nTime:payload.nTime,nFreq:payload.nFreq,
|
|
284
296
|
tStartSec:payload.tStartSec,tStepSec:payload.tStepSec,
|
|
285
297
|
fStartHz:payload.fStartHz,fStepHz:payload.fStepHz,
|
|
286
298
|
dtResolutionSec:payload.dtResolutionSec,dfResolutionHz:payload.dfResolutionHz,
|
|
287
299
|
unit:payload.unit,source:payload.source,
|
|
288
|
-
data:
|
|
289
|
-
|
|
290
|
-
gy:decodeB64F32(payload.gyB64,cells),
|
|
291
|
-
gz:decodeB64F32(payload.gzB64,cells),
|
|
292
|
-
rss:decodeB64F32(payload.rssB64,cells)
|
|
293
|
-
},
|
|
300
|
+
data:data,
|
|
301
|
+
rfIncluded:!!payload.rfIncluded,rfMaxValue:payload.rfMaxValue||0,
|
|
294
302
|
minValue:payload.minValue,maxValue:payload.maxValue,
|
|
295
303
|
decimationFactor:payload.decimationFactor,decimatedRateHz:payload.decimatedRateHz,
|
|
296
304
|
windowSamples:payload.windowSamples,hopSamples:payload.hopSamples,fftPoints:payload.fftPoints,
|
|
@@ -308,6 +316,8 @@ function deserializeGradientSound(payload){
|
|
|
308
316
|
startSec:payload.startSec,
|
|
309
317
|
endSec:payload.endSec,
|
|
310
318
|
silent:!!payload.silent,
|
|
319
|
+
rfIncluded:!!payload.rfIncluded,
|
|
320
|
+
warnings:payload.warnings||[],
|
|
311
321
|
left:decodeB64F32(payload.leftB64,payload.n),
|
|
312
322
|
right:decodeB64F32(payload.rightB64,payload.n)
|
|
313
323
|
};
|
|
@@ -1976,10 +1986,18 @@ window.SeqEyesPanelHost={
|
|
|
1976
1986
|
},
|
|
1977
1987
|
requestAudio:function(requestId,startSec,endSec,options){
|
|
1978
1988
|
if(!vscApi)return;
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1989
|
+
/* The whole options object crosses, the way requestSpectrogram already
|
|
1990
|
+
ships `params`. Enumerating fields here meant every audio option added to
|
|
1991
|
+
panel.js was silently dropped on this lane only: the RF proxy reached the
|
|
1992
|
+
extension as `undefined`, so it synthesised no RF and fell back to its
|
|
1993
|
+
default mix. There is no per-field list left to drift. */
|
|
1994
|
+
var message={};
|
|
1995
|
+
for(var key in options)if(Object.prototype.hasOwnProperty.call(options,key))message[key]=options[key];
|
|
1996
|
+
message.command='synthesizeGradientSound';
|
|
1997
|
+
message.requestId=requestId;
|
|
1998
|
+
message.startSec=startSec;
|
|
1999
|
+
message.endSec=endSec;
|
|
2000
|
+
vscApi.postMessage(message);
|
|
1983
2001
|
}
|
|
1984
2002
|
};
|
|
1985
2003
|
|
|
@@ -3992,6 +4010,33 @@ function sgValueFloor(spec) {
|
|
|
3992
4010
|
return m > 0 ? m * 1e-6 : 1e-12;
|
|
3993
4011
|
}
|
|
3994
4012
|
|
|
4013
|
+
var SG_RF_CHANNELS = { rf: 1, rfThermo: 1, rfControl: 1 };
|
|
4014
|
+
|
|
4015
|
+
function sgIsRfChannel(key) { return SG_RF_CHANNELS[key] === 1; }
|
|
4016
|
+
|
|
4017
|
+
/**
|
|
4018
|
+
* dB reference for one channel.
|
|
4019
|
+
*
|
|
4020
|
+
* Gradient channels are absolute — dB re 1 mT/m (or 1 T/m/s) — so their
|
|
4021
|
+
* reference is 1 and the number means the same thing in every view. The RF
|
|
4022
|
+
* proxy has no unit at all, so it is referenced to its own peak in the current
|
|
4023
|
+
* view: 0 dB is "the loudest thing the RF proxy does here". Those two readings
|
|
4024
|
+
* are not comparable, which is the honest situation; pretending otherwise would
|
|
4025
|
+
* need a coil transfer function nobody has measured.
|
|
4026
|
+
*/
|
|
4027
|
+
function sgChannelRefDb(spec, key) {
|
|
4028
|
+
if (!sgIsRfChannel(key)) return 0;
|
|
4029
|
+
var peak = spec && spec.rfMaxValue > 0 ? spec.rfMaxValue : 0;
|
|
4030
|
+
return peak > 0 ? 20 * Math.log10(peak) : 0;
|
|
4031
|
+
}
|
|
4032
|
+
|
|
4033
|
+
/** Per-channel magnitude floor, scaled to that channel's own peak. */
|
|
4034
|
+
function sgValueFloorFor(spec, key) {
|
|
4035
|
+
if (!sgIsRfChannel(key)) return sgValueFloor(spec);
|
|
4036
|
+
var peak = spec && spec.rfMaxValue > 0 ? spec.rfMaxValue : 0;
|
|
4037
|
+
return peak > 0 ? peak * 1e-6 : 1e-12;
|
|
4038
|
+
}
|
|
4039
|
+
|
|
3995
4040
|
function sgFmtDb(v) {
|
|
3996
4041
|
if (!isFinite(v)) return '--';
|
|
3997
4042
|
return (v >= 0 ? '+' : '') + v.toFixed(1);
|
|
@@ -4053,10 +4098,11 @@ function sgAutoWindowLevel(spec, key, previous) {
|
|
|
4053
4098
|
var n = data.length;
|
|
4054
4099
|
if (!n) return fallback;
|
|
4055
4100
|
|
|
4056
|
-
var floorValue =
|
|
4101
|
+
var floorValue = sgValueFloorFor(spec, key);
|
|
4102
|
+
var refDb = sgChannelRefDb(spec, key);
|
|
4057
4103
|
var stride = Math.max(1, Math.floor(n / 4096));
|
|
4058
4104
|
var samples = [];
|
|
4059
|
-
for (var i = 0; i < n; i += stride) samples.push(sgToDb(data[i], floorValue));
|
|
4105
|
+
for (var i = 0; i < n; i += stride) samples.push(sgToDb(data[i], floorValue) - refDb);
|
|
4060
4106
|
if (!samples.length) return fallback;
|
|
4061
4107
|
samples.sort(function (a, b) { return a - b; });
|
|
4062
4108
|
|
|
@@ -4149,8 +4195,10 @@ function sgBuildImageData(context) {
|
|
|
4149
4195
|
var data = spec.data[context.channel] || spec.data.rss;
|
|
4150
4196
|
var lut = context.lut;
|
|
4151
4197
|
var wl = context.windowLevel;
|
|
4152
|
-
var floorValue =
|
|
4153
|
-
|
|
4198
|
+
var floorValue = sgValueFloorFor(spec, context.channel);
|
|
4199
|
+
// The channel reference folds into the window's lower edge, so a per-cell
|
|
4200
|
+
// subtraction never enters the inner loop.
|
|
4201
|
+
var low = wl.level - wl.width / 2 + sgChannelRefDb(spec, context.channel);
|
|
4154
4202
|
var invWidth = wl.width > 0 ? 1 / wl.width : 1;
|
|
4155
4203
|
var logScale = 20 / Math.LN10;
|
|
4156
4204
|
|
|
@@ -4538,13 +4586,19 @@ function sgDrawSpectrum(context) {
|
|
|
4538
4586
|
ctx.strokeStyle = trace.color;
|
|
4539
4587
|
ctx.lineWidth = trace.key === 'rss' ? 1.6 : 1.1;
|
|
4540
4588
|
ctx.globalAlpha = trace.key === 'rss' ? 1 : 0.9;
|
|
4589
|
+
// Dashed for the RF proxies: the same dB axis carries two different
|
|
4590
|
+
// references, and a reader should not have to consult the legend to see
|
|
4591
|
+
// which traces are the uncalibrated ones.
|
|
4592
|
+
ctx.setLineDash(sgIsRfChannel(trace.key) ? [4, 3] : []);
|
|
4593
|
+
var traceFloor = sgValueFloorFor(spec, trace.key);
|
|
4594
|
+
var traceRefDb = sgChannelRefDb(spec, trace.key);
|
|
4541
4595
|
ctx.beginPath();
|
|
4542
4596
|
var started = false;
|
|
4543
4597
|
for (var row = 0; row < values.length; row++) {
|
|
4544
4598
|
var f = spec.fStartHz + row * spec.fStepHz;
|
|
4545
4599
|
if (f < range.fMin || f > range.fMax) continue;
|
|
4546
4600
|
var fp = freqPos(f);
|
|
4547
|
-
var mp = magPos(sgToDb(values[row],
|
|
4601
|
+
var mp = magPos(sgToDb(values[row], traceFloor) - traceRefDb);
|
|
4548
4602
|
var xx = rotated ? mp : fp;
|
|
4549
4603
|
var yy = rotated ? fp : mp;
|
|
4550
4604
|
if (!started) { ctx.moveTo(xx, yy); started = true; } else ctx.lineTo(xx, yy);
|
|
@@ -4552,6 +4606,7 @@ function sgDrawSpectrum(context) {
|
|
|
4552
4606
|
ctx.stroke();
|
|
4553
4607
|
}
|
|
4554
4608
|
ctx.globalAlpha = 1;
|
|
4609
|
+
ctx.setLineDash([]);
|
|
4555
4610
|
ctx.restore();
|
|
4556
4611
|
return rect;
|
|
4557
4612
|
}
|
|
@@ -4562,7 +4617,6 @@ function sgSpectrumScale(spec, slice, windowLevel, freeY, traces) {
|
|
|
4562
4617
|
return { min: windowLevel.level - windowLevel.width / 2, max: windowLevel.level + windowLevel.width / 2 };
|
|
4563
4618
|
}
|
|
4564
4619
|
if (!spec || !slice) return { min: -80, max: 0 };
|
|
4565
|
-
var floorValue = sgValueFloor(spec);
|
|
4566
4620
|
var max = -Infinity;
|
|
4567
4621
|
var keys = [];
|
|
4568
4622
|
for (var t = 0; t < traces.length; t++) if (traces[t].visible) keys.push(traces[t].key);
|
|
@@ -4570,8 +4624,10 @@ function sgSpectrumScale(spec, slice, windowLevel, freeY, traces) {
|
|
|
4570
4624
|
for (var k = 0; k < keys.length; k++) {
|
|
4571
4625
|
var values = slice[keys[k]];
|
|
4572
4626
|
if (!values) continue;
|
|
4627
|
+
var floorValue = sgValueFloorFor(spec, keys[k]);
|
|
4628
|
+
var refDb = sgChannelRefDb(spec, keys[k]);
|
|
4573
4629
|
for (var i = 0; i < values.length; i++) {
|
|
4574
|
-
var db = sgToDb(values[i], floorValue);
|
|
4630
|
+
var db = sgToDb(values[i], floorValue) - refDb;
|
|
4575
4631
|
if (db > max) max = db;
|
|
4576
4632
|
}
|
|
4577
4633
|
}
|
|
@@ -4603,8 +4659,8 @@ function sgDetectHotBands(spec, bands, windowLevel, channel) {
|
|
|
4603
4659
|
var flags = [];
|
|
4604
4660
|
if (!spec || !spec.nTime || !bands || !bands.length) return flags;
|
|
4605
4661
|
var data = spec.data[channel] || spec.data.rss;
|
|
4606
|
-
var floorValue =
|
|
4607
|
-
var threshold = windowLevel.level + windowLevel.width / 2;
|
|
4662
|
+
var floorValue = sgValueFloorFor(spec, channel);
|
|
4663
|
+
var threshold = windowLevel.level + windowLevel.width / 2 + sgChannelRefDb(spec, channel);
|
|
4608
4664
|
for (var i = 0; i < bands.length; i++) {
|
|
4609
4665
|
var half = (bands[i].bwHz || 0) / 2;
|
|
4610
4666
|
var rowLow = Math.max(0, Math.floor((bands[i].freqHz - half - spec.fStartHz) / spec.fStepHz));
|
|
@@ -4636,7 +4692,12 @@ function sgSampleCell(spec, channel, timeSec, freqHz) {
|
|
|
4636
4692
|
gx: spec.data.gx[index],
|
|
4637
4693
|
gy: spec.data.gy[index],
|
|
4638
4694
|
gz: spec.data.gz[index],
|
|
4639
|
-
rss: spec.data[channel] ? spec.data[channel][index] : spec.data.rss[index]
|
|
4695
|
+
rss: spec.data[channel] ? spec.data[channel][index] : spec.data.rss[index],
|
|
4696
|
+
/* null, not 0, when RF was not computed: the readout must be able to tell
|
|
4697
|
+
"no RF proxy in this matrix" from "the RF proxy is silent here". */
|
|
4698
|
+
rfThermo: spec.data.rfThermo ? spec.data.rfThermo[index] : null,
|
|
4699
|
+
rfControl: spec.data.rfControl ? spec.data.rfControl[index] : null,
|
|
4700
|
+
rf: spec.data.rf ? spec.data.rf[index] : null
|
|
4640
4701
|
};
|
|
4641
4702
|
}
|
|
4642
4703
|
|
|
@@ -4989,7 +5050,30 @@ var SeqEyesPanel = (function () {
|
|
|
4989
5050
|
overlap: getNum('seqeyes.spectrogram.overlap', 0.75),
|
|
4990
5051
|
oversample: getNum('seqeyes.spectrogram.oversample', 3),
|
|
4991
5052
|
targetColumns: 256,
|
|
4992
|
-
normalize: true
|
|
5053
|
+
normalize: true,
|
|
5054
|
+
/* RF acoustic proxy (rfAcoustic.ts). Off by default and opt-in: these
|
|
5055
|
+
channels are uncalibrated research proxies with no unit, unlike every
|
|
5056
|
+
other channel in this panel. The two mechanism weights stay at the
|
|
5057
|
+
upstream 1:1 default — that ratio is a placeholder until it is fitted to
|
|
5058
|
+
measurements, so exposing it as a dial would suggest a meaning it has
|
|
5059
|
+
not earned. */
|
|
5060
|
+
includeRf: getBool('seqeyes.spectrogram.rf', false),
|
|
5061
|
+
rfScale: getNum('seqeyes.spectrogram.rfscale', 1),
|
|
5062
|
+
rfThermoWeight: 1,
|
|
5063
|
+
rfControlWeight: 1,
|
|
5064
|
+
rfEdgeMode: 'signed',
|
|
5065
|
+
/* Playback only: crossfade between the gradient sound and the RF proxy,
|
|
5066
|
+
each normalised to its own peak first. There is no correct default —
|
|
5067
|
+
the two have no common unit — so an even blend is the honest one. */
|
|
5068
|
+
rfMix: getNum('seqeyes.spectrogram.rfmix', 0.5)
|
|
5069
|
+
};
|
|
5070
|
+
/* Which matrix the image layer paints. The spectrum pane still draws every
|
|
5071
|
+
visible trace; this only picks the colourmapped one. */
|
|
5072
|
+
var imageChannel = get('seqeyes.spectrogram.imagechannel') || 'rss';
|
|
5073
|
+
var SG_RF_SLICE_KEYS = ['rfThermo', 'rfControl', 'rf'];
|
|
5074
|
+
var SG_IMAGE_CHANNEL_LABELS = {
|
|
5075
|
+
rss: 'Σ', gx: 'Gx', gy: 'Gy', gz: 'Gz',
|
|
5076
|
+
rf: 'RF proxy', rfThermo: 'RF thermo', rfControl: 'RF ctrl'
|
|
4993
5077
|
};
|
|
4994
5078
|
var colormapName = get('seqeyes.spectrogram.colormap') || 'viridis';
|
|
4995
5079
|
if (SG_COLORMAP_NAMES.indexOf(colormapName) < 0) colormapName = 'viridis';
|
|
@@ -4998,7 +5082,10 @@ var SeqEyesPanel = (function () {
|
|
|
4998
5082
|
{ key: 'gx', label: 'Gx', varName: '--gx', visible: getBool('seqeyes.spectrogram.trace.gx', true) },
|
|
4999
5083
|
{ key: 'gy', label: 'Gy', varName: '--gy', visible: getBool('seqeyes.spectrogram.trace.gy', true) },
|
|
5000
5084
|
{ key: 'gz', label: 'Gz', varName: '--gz', visible: getBool('seqeyes.spectrogram.trace.gz', true) },
|
|
5001
|
-
{ key: 'rss', label: 'Σ', varName: '--fg', visible: getBool('seqeyes.spectrogram.trace.rss', true) }
|
|
5085
|
+
{ key: 'rss', label: 'Σ', varName: '--fg', visible: getBool('seqeyes.spectrogram.trace.rss', true) },
|
|
5086
|
+
/* Drawn only while the matrix carries them, i.e. while RF is enabled. */
|
|
5087
|
+
{ key: 'rfThermo', label: 'RF thermo', varName: '--rf', rf: true, visible: getBool('seqeyes.spectrogram.trace.rfThermo', true) },
|
|
5088
|
+
{ key: 'rfControl', label: 'RF ctrl', varName: '--tr', rf: true, visible: getBool('seqeyes.spectrogram.trace.rfControl', true) }
|
|
5002
5089
|
];
|
|
5003
5090
|
var freeY = getBool('seqeyes.spectrogram.freeY', false);
|
|
5004
5091
|
var bandsVisible = getBool('seqeyes.spectrogram.bands', true);
|
|
@@ -5031,6 +5118,11 @@ var SeqEyesPanel = (function () {
|
|
|
5031
5118
|
var audioActivationId = 0;
|
|
5032
5119
|
var audioActivationPending = false;
|
|
5033
5120
|
var audioWindow = null; // buffered range plus the first-pass offset
|
|
5121
|
+
/* What the host actually put in the last audio buffer. The panel asks for
|
|
5122
|
+
RF, but only the reply says whether any arrived, and the two lanes reach
|
|
5123
|
+
the synthesiser by different routes — so this is reported rather than
|
|
5124
|
+
assumed from `params.includeRf`. */
|
|
5125
|
+
var audioRfIncluded = null;
|
|
5034
5126
|
var playheadFrame = 0;
|
|
5035
5127
|
|
|
5036
5128
|
var AUDIO_SAMPLE_RATE = 44100;
|
|
@@ -5237,7 +5329,9 @@ var SeqEyesPanel = (function () {
|
|
|
5237
5329
|
|
|
5238
5330
|
function paramsSignature() {
|
|
5239
5331
|
return [params.source, params.fMaxHz, params.windowSamples, params.overlap,
|
|
5240
|
-
params.oversample, params.targetColumns, params.normalize ? 1 : 0
|
|
5332
|
+
params.oversample, params.targetColumns, params.normalize ? 1 : 0,
|
|
5333
|
+
params.includeRf ? 1 : 0, params.rfScale, params.rfThermoWeight,
|
|
5334
|
+
params.rfControlWeight, params.rfEdgeMode].join('|');
|
|
5241
5335
|
}
|
|
5242
5336
|
|
|
5243
5337
|
function cacheKey(startSec, endSec) {
|
|
@@ -5299,6 +5393,11 @@ var SeqEyesPanel = (function () {
|
|
|
5299
5393
|
oversample: params.oversample,
|
|
5300
5394
|
targetColumns: params.targetColumns,
|
|
5301
5395
|
normalize: params.normalize,
|
|
5396
|
+
includeRf: params.includeRf,
|
|
5397
|
+
rfScale: params.rfScale,
|
|
5398
|
+
rfThermoWeight: params.rfThermoWeight,
|
|
5399
|
+
rfControlWeight: params.rfControlWeight,
|
|
5400
|
+
rfEdgeMode: params.rfEdgeMode,
|
|
5302
5401
|
trTimeSec: h.getTrTimeSec ? h.getTrTimeSec() : 0
|
|
5303
5402
|
});
|
|
5304
5403
|
}
|
|
@@ -5335,13 +5434,29 @@ var SeqEyesPanel = (function () {
|
|
|
5335
5434
|
render();
|
|
5336
5435
|
}
|
|
5337
5436
|
|
|
5437
|
+
/**
|
|
5438
|
+
* Image channel that is actually present in the current matrix.
|
|
5439
|
+
*
|
|
5440
|
+
* A remembered `rf` preference outlives the RF toggle and survives a reload
|
|
5441
|
+
* into a matrix that has no RF channels, so the fallback is not defensive
|
|
5442
|
+
* padding — it is the normal path the first time the panel opens with RF off.
|
|
5443
|
+
*/
|
|
5444
|
+
function activeImageChannel() {
|
|
5445
|
+
if (currentSpec && currentSpec.data && currentSpec.data[imageChannel]) return imageChannel;
|
|
5446
|
+
return 'rss';
|
|
5447
|
+
}
|
|
5448
|
+
|
|
5338
5449
|
function applySpectrogram(spec) {
|
|
5339
5450
|
currentSpec = spec;
|
|
5340
5451
|
currentView = { startSec: spec.requestedStartSec, endSec: spec.requestedEndSec };
|
|
5341
|
-
if (windowLevelAuto) windowLevel = sgAutoWindowLevel(spec,
|
|
5452
|
+
if (windowLevelAuto) windowLevel = sgAutoWindowLevel(spec, activeImageChannel(), windowLevel);
|
|
5342
5453
|
clampFrequencyRange();
|
|
5343
5454
|
imageDirty = true;
|
|
5344
|
-
hotBands = sgDetectHotBands(spec, acousticBands, windowLevel,
|
|
5455
|
+
hotBands = sgDetectHotBands(spec, acousticBands, windowLevel, activeImageChannel());
|
|
5456
|
+
// The RF chips and the image-channel options both depend on whether this
|
|
5457
|
+
// matrix carries RF, which is only known once it arrives.
|
|
5458
|
+
buildLegend();
|
|
5459
|
+
syncControls();
|
|
5345
5460
|
publishNotices();
|
|
5346
5461
|
render();
|
|
5347
5462
|
if (requestStartedAt) {
|
|
@@ -5384,9 +5499,18 @@ var SeqEyesPanel = (function () {
|
|
|
5384
5499
|
var anyHot = false;
|
|
5385
5500
|
for (var k = 0; k < hotBands.length; k++) if (hotBands[k]) anyHot = true;
|
|
5386
5501
|
if (anyHot) {
|
|
5387
|
-
|
|
5502
|
+
// The detection runs on whichever channel the image shows, so the wording
|
|
5503
|
+
// has to name that channel rather than always claiming "gradient".
|
|
5504
|
+
messages.push((sgIsRfChannel(activeImageChannel()) ? 'RF proxy energy' : 'Gradient energy')
|
|
5505
|
+
+ ' falls inside a forbidden acoustic band (advisory: '
|
|
5388
5506
|
+ 'this compares against the current display window, and is not a compliance check).');
|
|
5389
5507
|
}
|
|
5508
|
+
if (currentSpec && currentSpec.rfIncluded) {
|
|
5509
|
+
messages.push('RF acoustic channels are a reduced-order research proxy '
|
|
5510
|
+
+ '(thermoacoustic d|B1|²/dt plus a hypothesised TX-switching term), with no calibrated '
|
|
5511
|
+
+ 'level and no scanner resonance model. They are shown on their own relative scale and '
|
|
5512
|
+
+ 'must not be compared with, or added to, the gradient channels.');
|
|
5513
|
+
}
|
|
5390
5514
|
notice('spectrogram', messages.length ? messages : null);
|
|
5391
5515
|
}
|
|
5392
5516
|
|
|
@@ -5417,7 +5541,7 @@ var SeqEyesPanel = (function () {
|
|
|
5417
5541
|
if (imageDirty || !imageCache || imageCache.width !== pixelW || imageCache.height !== pixelH) {
|
|
5418
5542
|
imageCache = sgBuildImageData({
|
|
5419
5543
|
spectrogram: currentSpec,
|
|
5420
|
-
channel:
|
|
5544
|
+
channel: activeImageChannel(),
|
|
5421
5545
|
lut: lut,
|
|
5422
5546
|
windowLevel: windowLevel,
|
|
5423
5547
|
view: currentView,
|
|
@@ -5512,9 +5636,22 @@ var SeqEyesPanel = (function () {
|
|
|
5512
5636
|
out.gz[row] = spec.data.gz[index];
|
|
5513
5637
|
out.rss[row] = spec.data.rss[index];
|
|
5514
5638
|
}
|
|
5639
|
+
addRfSliceColumns(out, spec, col);
|
|
5515
5640
|
return out;
|
|
5516
5641
|
}
|
|
5517
5642
|
|
|
5643
|
+
/** Copy the RF rows of one column into a slice, when the matrix has them. */
|
|
5644
|
+
function addRfSliceColumns(out, spec, col) {
|
|
5645
|
+
for (var i = 0; i < SG_RF_SLICE_KEYS.length; i++) {
|
|
5646
|
+
var key = SG_RF_SLICE_KEYS[i];
|
|
5647
|
+
var source = spec.data[key];
|
|
5648
|
+
if (!source) continue;
|
|
5649
|
+
var target = new Float32Array(spec.nFreq);
|
|
5650
|
+
for (var row = 0; row < spec.nFreq; row++) target[row] = source[row * spec.nTime + col];
|
|
5651
|
+
out[key] = target;
|
|
5652
|
+
}
|
|
5653
|
+
}
|
|
5654
|
+
|
|
5518
5655
|
var averageCache = null;
|
|
5519
5656
|
function sliceAverage() {
|
|
5520
5657
|
var spec = currentSpec;
|
|
@@ -5538,6 +5675,21 @@ var SeqEyesPanel = (function () {
|
|
|
5538
5675
|
out.gx[row] = rx; out.gy[row] = ry; out.gz[row] = rz;
|
|
5539
5676
|
out.rss[row] = Math.sqrt(rx * rx + ry * ry + rz * rz);
|
|
5540
5677
|
}
|
|
5678
|
+
// The RF channels average on their own. `rss` is rebuilt from the per-axis
|
|
5679
|
+
// means above because it is a combination of three signals; `rf` is already
|
|
5680
|
+
// the transform of one combined signal, so it is simply averaged.
|
|
5681
|
+
for (var i = 0; i < SG_RF_SLICE_KEYS.length; i++) {
|
|
5682
|
+
var key = SG_RF_SLICE_KEYS[i];
|
|
5683
|
+
var source = spec.data[key];
|
|
5684
|
+
if (!source) continue;
|
|
5685
|
+
var target = new Float32Array(spec.nFreq);
|
|
5686
|
+
for (var r = 0; r < spec.nFreq; r++) {
|
|
5687
|
+
var b = r * spec.nTime, acc = 0;
|
|
5688
|
+
for (var c = 0; c < spec.nTime; c++) { var v = source[b + c]; acc += v * v; }
|
|
5689
|
+
target[r] = Math.sqrt(acc / spec.nTime);
|
|
5690
|
+
}
|
|
5691
|
+
out[key] = target;
|
|
5692
|
+
}
|
|
5541
5693
|
averageCache = { spec: spec, slice: out };
|
|
5542
5694
|
return out;
|
|
5543
5695
|
}
|
|
@@ -5553,7 +5705,9 @@ var SeqEyesPanel = (function () {
|
|
|
5553
5705
|
for (var i = 0; i < traces.length; i++) {
|
|
5554
5706
|
resolved.push({
|
|
5555
5707
|
key: traces[i].key,
|
|
5556
|
-
|
|
5708
|
+
// An RF trace the matrix does not carry is not drawn, whatever the
|
|
5709
|
+
// remembered legend state says.
|
|
5710
|
+
visible: traces[i].visible && (!traces[i].rf || !!(slice && slice[traces[i].key])),
|
|
5557
5711
|
color: (style.getPropertyValue(traces[i].varName) || '#888').trim()
|
|
5558
5712
|
});
|
|
5559
5713
|
}
|
|
@@ -5582,13 +5736,25 @@ var SeqEyesPanel = (function () {
|
|
|
5582
5736
|
if (currentSpec && currentSpec.nTime) {
|
|
5583
5737
|
parts.push('dt ' + sgFmtResolution(currentSpec.dtResolutionSec));
|
|
5584
5738
|
parts.push('df ' + sgFmtHz(currentSpec.dfResolutionHz) + ' Hz');
|
|
5585
|
-
|
|
5739
|
+
var channel = activeImageChannel();
|
|
5740
|
+
parts.push('img ' + (SG_IMAGE_CHANNEL_LABELS[channel] || channel));
|
|
5741
|
+
// The unit belongs to the channel on screen, so an RF image must not be
|
|
5742
|
+
// labelled mT/m.
|
|
5743
|
+
parts.push('unit ' + (sgIsRfChannel(channel) ? 'relative (0 dB = RF peak in view)' : currentSpec.unit));
|
|
5586
5744
|
parts.push('D ' + currentSpec.decimationFactor + '×');
|
|
5587
5745
|
parts.push(currentSpec.nTime + '×' + currentSpec.nFreq);
|
|
5588
5746
|
parts.push('W ' + windowLevel.width.toFixed(1) + ' dB L ' + sgFmtDb(windowLevel.level) + ' dB');
|
|
5589
5747
|
} else if (busy) {
|
|
5590
5748
|
parts.push('Calculating…');
|
|
5591
5749
|
}
|
|
5750
|
+
if (params.includeRf && audioRfIncluded !== null) {
|
|
5751
|
+
// Says whether the buffer being played actually contains RF, which is not
|
|
5752
|
+
// the same question as whether the RF channels were requested — and at
|
|
5753
|
+
// what share, since a low mix is the other way RF goes unnoticed.
|
|
5754
|
+
parts.push(audioRfIncluded
|
|
5755
|
+
? 'audio RF ✓ ' + Math.round(params.rfMix * 100) + '%'
|
|
5756
|
+
: 'audio RF ✗ (none reached the buffer)');
|
|
5757
|
+
}
|
|
5592
5758
|
if (isFinite(playheadTimeSec)) parts.push('▶ ' + sgFmtTime(playheadTimeSec, timeUnit()));
|
|
5593
5759
|
else if (isFinite(markerTimeSec)) parts.push('marker ' + sgFmtTime(markerTimeSec, timeUnit()));
|
|
5594
5760
|
else if (currentSpec && currentSpec.nTime) {
|
|
@@ -5596,7 +5762,11 @@ var SeqEyesPanel = (function () {
|
|
|
5596
5762
|
}
|
|
5597
5763
|
if (hoverReadout) parts.push(hoverReadout);
|
|
5598
5764
|
out.textContent = parts.join(' ');
|
|
5599
|
-
out.title =
|
|
5765
|
+
out.title = currentSpec && currentSpec.rfIncluded
|
|
5766
|
+
? 'Simulated gradient spectral content — not calibrated sound pressure level. '
|
|
5767
|
+
+ 'The RF channels are an uncalibrated research proxy on a separate relative scale; '
|
|
5768
|
+
+ 'they cannot be compared with the gradient channels or summed with them.'
|
|
5769
|
+
: 'Simulated gradient spectral content — not calibrated sound pressure level.';
|
|
5600
5770
|
}
|
|
5601
5771
|
|
|
5602
5772
|
var hoverReadout = '';
|
|
@@ -5631,15 +5801,15 @@ var SeqEyesPanel = (function () {
|
|
|
5631
5801
|
};
|
|
5632
5802
|
windowLevelAuto = false;
|
|
5633
5803
|
imageDirty = true;
|
|
5634
|
-
if (currentSpec) hotBands = sgDetectHotBands(currentSpec, acousticBands, windowLevel,
|
|
5804
|
+
if (currentSpec) hotBands = sgDetectHotBands(currentSpec, acousticBands, windowLevel, activeImageChannel());
|
|
5635
5805
|
render();
|
|
5636
5806
|
}
|
|
5637
5807
|
|
|
5638
5808
|
function resetWindowLevel() {
|
|
5639
5809
|
windowLevelAuto = true;
|
|
5640
|
-
if (currentSpec) windowLevel = sgAutoWindowLevel(currentSpec,
|
|
5810
|
+
if (currentSpec) windowLevel = sgAutoWindowLevel(currentSpec, activeImageChannel(), windowLevel);
|
|
5641
5811
|
imageDirty = true;
|
|
5642
|
-
if (currentSpec) hotBands = sgDetectHotBands(currentSpec, acousticBands, windowLevel,
|
|
5812
|
+
if (currentSpec) hotBands = sgDetectHotBands(currentSpec, acousticBands, windowLevel, activeImageChannel());
|
|
5643
5813
|
render();
|
|
5644
5814
|
}
|
|
5645
5815
|
|
|
@@ -5724,7 +5894,13 @@ var SeqEyesPanel = (function () {
|
|
|
5724
5894
|
h.requestAudio(pendingAudioId, range.startSec, range.endSec, {
|
|
5725
5895
|
sampleRate: 44100,
|
|
5726
5896
|
source: params.source,
|
|
5727
|
-
channelWeights: [1, 1, 1]
|
|
5897
|
+
channelWeights: [1, 1, 1],
|
|
5898
|
+
includeRf: params.includeRf,
|
|
5899
|
+
rfScale: params.rfScale,
|
|
5900
|
+
rfThermoWeight: params.rfThermoWeight,
|
|
5901
|
+
rfControlWeight: params.rfControlWeight,
|
|
5902
|
+
rfEdgeMode: params.rfEdgeMode,
|
|
5903
|
+
rfMix: params.rfMix
|
|
5728
5904
|
});
|
|
5729
5905
|
syncTransport();
|
|
5730
5906
|
}
|
|
@@ -5777,16 +5953,28 @@ var SeqEyesPanel = (function () {
|
|
|
5777
5953
|
function deliverAudio(id, payload) {
|
|
5778
5954
|
if (id !== pendingAudioId) return;
|
|
5779
5955
|
pendingAudioId = 0;
|
|
5956
|
+
// The host knows which sources were enabled, so it words the refusal; the
|
|
5957
|
+
// panel used to say "no gradient activity" even when RF was the source the
|
|
5958
|
+
// user was listening for.
|
|
5959
|
+
var emptyMessage = params.includeRf
|
|
5960
|
+
? 'Nothing to play in this window — no gradient activity and no RF events.'
|
|
5961
|
+
: 'No gradient activity in this window — nothing to play.';
|
|
5780
5962
|
if (!payload || !payload.left || !payload.left.length) {
|
|
5781
|
-
notice('gradientSound',
|
|
5963
|
+
notice('gradientSound', (payload && payload.warnings && payload.warnings.length)
|
|
5964
|
+
? payload.warnings : emptyMessage);
|
|
5782
5965
|
syncTransport();
|
|
5783
5966
|
return;
|
|
5784
5967
|
}
|
|
5785
5968
|
if (payload.silent) {
|
|
5786
|
-
notice('gradientSound',
|
|
5969
|
+
notice('gradientSound', (payload.warnings && payload.warnings.length)
|
|
5970
|
+
? payload.warnings : emptyMessage);
|
|
5787
5971
|
syncTransport();
|
|
5788
5972
|
return;
|
|
5789
5973
|
}
|
|
5974
|
+
// Partial-source notes ("hearing the RF proxy only") are advisory, not a
|
|
5975
|
+
// refusal, so they are published without stopping playback.
|
|
5976
|
+
notice('gradientSound', (payload.warnings && payload.warnings.length) ? payload.warnings : null);
|
|
5977
|
+
audioRfIncluded = !!payload.rfIncluded;
|
|
5790
5978
|
if (!SeqEyesAudio.load(payload.sampleRate, payload.left, payload.right, payload.startSec)) {
|
|
5791
5979
|
if (SeqEyesAudio.contextState() !== 'running') audioActivationFailure();
|
|
5792
5980
|
else notice('gradientSound', 'This host could not create an audio buffer.');
|
|
@@ -5846,6 +6034,7 @@ var SeqEyesPanel = (function () {
|
|
|
5846
6034
|
playheadTimeSec = NaN;
|
|
5847
6035
|
pendingAudioId = 0;
|
|
5848
6036
|
audioWindow = null;
|
|
6037
|
+
audioRfIncluded = null;
|
|
5849
6038
|
syncTransport();
|
|
5850
6039
|
render();
|
|
5851
6040
|
}
|
|
@@ -5901,7 +6090,7 @@ var SeqEyesPanel = (function () {
|
|
|
5901
6090
|
|
|
5902
6091
|
function setAcousticBands(bands) {
|
|
5903
6092
|
acousticBands = Array.isArray(bands) ? bands.slice() : [];
|
|
5904
|
-
hotBands = currentSpec ? sgDetectHotBands(currentSpec, acousticBands, windowLevel,
|
|
6093
|
+
hotBands = currentSpec ? sgDetectHotBands(currentSpec, acousticBands, windowLevel, activeImageChannel()) : [];
|
|
5905
6094
|
buildLegend();
|
|
5906
6095
|
publishNotices();
|
|
5907
6096
|
render();
|
|
@@ -5916,10 +6105,15 @@ var SeqEyesPanel = (function () {
|
|
|
5916
6105
|
var style = getComputedStyle(document.body);
|
|
5917
6106
|
|
|
5918
6107
|
for (var i = 0; i < traces.length; i++) {
|
|
6108
|
+
// RF chips appear only once the matrix carries RF, so the legend never
|
|
6109
|
+
// offers a toggle that would do nothing.
|
|
6110
|
+
if (traces[i].rf && !(currentSpec && currentSpec.data && currentSpec.data[traces[i].key])) continue;
|
|
5919
6111
|
(function (trace) {
|
|
5920
6112
|
var chip = document.createElement('div');
|
|
5921
6113
|
chip.className = 'li' + (trace.visible ? '' : ' off');
|
|
5922
|
-
chip.title =
|
|
6114
|
+
chip.title = trace.rf
|
|
6115
|
+
? 'Toggle the ' + trace.label + ' trace (uncalibrated proxy, dB relative to its own peak in this view)'
|
|
6116
|
+
: 'Toggle the ' + trace.label + ' spectrum trace';
|
|
5923
6117
|
var swatch = document.createElement('div');
|
|
5924
6118
|
swatch.className = 'ld';
|
|
5925
6119
|
swatch.style.background = (style.getPropertyValue(trace.varName) || '#888').trim();
|
|
@@ -5990,6 +6184,32 @@ var SeqEyesPanel = (function () {
|
|
|
5990
6184
|
setValue('sgWin', String(params.windowSamples));
|
|
5991
6185
|
setValue('sgOverlap', String(params.overlap));
|
|
5992
6186
|
setValue('sgOversample', String(params.oversample));
|
|
6187
|
+
|
|
6188
|
+
var rfToggle = el('sgRf');
|
|
6189
|
+
if (rfToggle) {
|
|
6190
|
+
rfToggle.setAttribute('aria-pressed', params.includeRf ? 'true' : 'false');
|
|
6191
|
+
rfToggle.classList.toggle('on', params.includeRf);
|
|
6192
|
+
}
|
|
6193
|
+
var rfScale = el('sgRfScale');
|
|
6194
|
+
if (rfScale) {
|
|
6195
|
+
setValue('sgRfScale', String(params.rfScale));
|
|
6196
|
+
rfScale.disabled = !params.includeRf;
|
|
6197
|
+
}
|
|
6198
|
+
var rfMix = el('sgRfMix');
|
|
6199
|
+
if (rfMix) {
|
|
6200
|
+
setValue('sgRfMix', String(Math.round(params.rfMix * 100)));
|
|
6201
|
+
rfMix.disabled = !params.includeRf;
|
|
6202
|
+
}
|
|
6203
|
+
var image = el('sgImageChannel');
|
|
6204
|
+
if (image) {
|
|
6205
|
+
// RF options are removed rather than disabled: a disabled option that the
|
|
6206
|
+
// select is currently showing is a state the user cannot get out of.
|
|
6207
|
+
for (var o = image.options.length - 1; o >= 0; o--) {
|
|
6208
|
+
if (sgIsRfChannel(image.options[o].value)) image.options[o].hidden = !params.includeRf;
|
|
6209
|
+
}
|
|
6210
|
+
image.value = activeImageChannel();
|
|
6211
|
+
}
|
|
6212
|
+
|
|
5993
6213
|
var clear = el('sgMarkerClear');
|
|
5994
6214
|
if (clear) clear.disabled = !isFinite(markerTimeSec);
|
|
5995
6215
|
}
|
|
@@ -6103,6 +6323,56 @@ var SeqEyesPanel = (function () {
|
|
|
6103
6323
|
onParamChanged(true);
|
|
6104
6324
|
};
|
|
6105
6325
|
|
|
6326
|
+
var image = el('sgImageChannel');
|
|
6327
|
+
if (image) image.onchange = function () {
|
|
6328
|
+
imageChannel = this.value;
|
|
6329
|
+
set('seqeyes.spectrogram.imagechannel', imageChannel);
|
|
6330
|
+
// Channels live on different scales, so a channel change invalidates the
|
|
6331
|
+
// contrast as thoroughly as new data would.
|
|
6332
|
+
windowLevelAuto = true;
|
|
6333
|
+
if (currentSpec) windowLevel = sgAutoWindowLevel(currentSpec, activeImageChannel(), windowLevel);
|
|
6334
|
+
if (currentSpec) hotBands = sgDetectHotBands(currentSpec, acousticBands, windowLevel, activeImageChannel());
|
|
6335
|
+
imageDirty = true;
|
|
6336
|
+
publishNotices();
|
|
6337
|
+
render();
|
|
6338
|
+
};
|
|
6339
|
+
|
|
6340
|
+
// Each RF control changes what is being synthesised, so a buffer already
|
|
6341
|
+
// playing is stale the moment one moves.
|
|
6342
|
+
var rfToggle = el('sgRf');
|
|
6343
|
+
if (rfToggle) rfToggle.onclick = function () {
|
|
6344
|
+
stopPlayback();
|
|
6345
|
+
params.includeRf = !params.includeRf;
|
|
6346
|
+
set('seqeyes.spectrogram.rf', params.includeRf ? '1' : '0');
|
|
6347
|
+
if (!params.includeRf && sgIsRfChannel(imageChannel)) {
|
|
6348
|
+
imageChannel = 'rss';
|
|
6349
|
+
set('seqeyes.spectrogram.imagechannel', imageChannel);
|
|
6350
|
+
}
|
|
6351
|
+
onParamChanged(true);
|
|
6352
|
+
buildLegend();
|
|
6353
|
+
};
|
|
6354
|
+
|
|
6355
|
+
var rfScale = el('sgRfScale');
|
|
6356
|
+
if (rfScale) rfScale.onchange = function () {
|
|
6357
|
+
stopPlayback();
|
|
6358
|
+
var value = parseFloat(this.value);
|
|
6359
|
+
params.rfScale = isFinite(value) && value >= 0 ? value : 1;
|
|
6360
|
+
set('seqeyes.spectrogram.rfscale', String(params.rfScale));
|
|
6361
|
+
onParamChanged(true);
|
|
6362
|
+
};
|
|
6363
|
+
|
|
6364
|
+
var rfMix = el('sgRfMix');
|
|
6365
|
+
if (rfMix) rfMix.oninput = function () {
|
|
6366
|
+
stopPlayback();
|
|
6367
|
+
var value = parseFloat(this.value);
|
|
6368
|
+
params.rfMix = isFinite(value) ? Math.max(0, Math.min(1, value / 100)) : 0.5;
|
|
6369
|
+
set('seqeyes.spectrogram.rfmix', String(params.rfMix));
|
|
6370
|
+
// Playback only: the spectrogram keeps the mechanisms on separate
|
|
6371
|
+
// channels, so this must not trigger a recompute.
|
|
6372
|
+
syncControls();
|
|
6373
|
+
syncTransport();
|
|
6374
|
+
};
|
|
6375
|
+
|
|
6106
6376
|
var win = el('sgWin');
|
|
6107
6377
|
if (win) win.onchange = function () { params.windowSamples = parseFloat(this.value) || 0; onParamChanged(true); };
|
|
6108
6378
|
var overlap = el('sgOverlap');
|
|
@@ -6351,6 +6621,15 @@ var SeqEyesPanel = (function () {
|
|
|
6351
6621
|
+ ' Gy ' + sgFmtDb(sgToDb(cell.gy, floorValue))
|
|
6352
6622
|
+ ' Gz ' + sgFmtDb(sgToDb(cell.gz, floorValue))
|
|
6353
6623
|
+ ' Σ ' + sgFmtDb(sgToDb(cell.rss, floorValue)) + ' dB';
|
|
6624
|
+
if (cell.rf !== null) {
|
|
6625
|
+
// Reported against the RF peak, matching the axis the RF traces are drawn
|
|
6626
|
+
// on, and flagged `rel` so it is never read as dB re 1 mT/m.
|
|
6627
|
+
var rfFloor = sgValueFloorFor(currentSpec, 'rf');
|
|
6628
|
+
var rfRef = sgChannelRefDb(currentSpec, 'rf');
|
|
6629
|
+
text += ' RF ' + sgFmtDb(sgToDb(cell.rf, rfFloor) - rfRef)
|
|
6630
|
+
+ ' (th ' + sgFmtDb(sgToDb(cell.rfThermo, rfFloor) - rfRef)
|
|
6631
|
+
+ ', ct ' + sgFmtDb(sgToDb(cell.rfControl, rfFloor) - rfRef) + ') dB rel';
|
|
6632
|
+
}
|
|
6354
6633
|
var bandIndex = sgBandAtFrequency(acousticBands, cell.freqHz);
|
|
6355
6634
|
if (bandIndex >= 0) {
|
|
6356
6635
|
var band = acousticBands[bandIndex];
|
|
@@ -6591,6 +6870,13 @@ var SeqEyesPanel = (function () {
|
|
|
6591
6870
|
dtResolutionSec: currentSpec ? currentSpec.dtResolutionSec : null,
|
|
6592
6871
|
dfResolutionHz: currentSpec ? currentSpec.dfResolutionHz : null,
|
|
6593
6872
|
decimationFactor: currentSpec ? currentSpec.decimationFactor : null,
|
|
6873
|
+
includeRf: params.includeRf,
|
|
6874
|
+
rfScale: params.rfScale,
|
|
6875
|
+
rfMix: params.rfMix,
|
|
6876
|
+
audioRfIncluded: audioRfIncluded,
|
|
6877
|
+
imageChannel: activeImageChannel(),
|
|
6878
|
+
rfIncluded: !!(currentSpec && currentSpec.rfIncluded),
|
|
6879
|
+
rfMaxValue: currentSpec ? (currentSpec.rfMaxValue || 0) : 0,
|
|
6594
6880
|
warnings: currentSpec ? currentSpec.warnings.slice() : [],
|
|
6595
6881
|
error: lastError
|
|
6596
6882
|
};
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|