Files
imdd_silas/Functions/EQ_visuals/showLevelScatter.m
2026-07-09 12:29:41 +02:00

421 lines
13 KiB
Matlab

function [symbols_for_lvl, avg_for_lvl, xAxisUs, info] = showLevelScatter(rxInput, refSymbols, options)
%SHOWLEVELSCATTER Plot received samples separated by reference PAM level.
% Supports plain numeric vectors, Signal objects, synchronized scope cell
% arrays, and raw unsynchronized Signal input. Raw Signal input is
% synchronized to refSymbols and stitched before plotting.
arguments
rxInput
refSymbols
options.fignum (1,1) double = NaN
options.displayname (1,:) char = ''
options.f_sym double = []
options.fsym double = []
options.syncFs (1,1) double = 0
options.shiftFs (1,1) double = 0
options.shifts double = []
options.maxOccurences (1,1) double = Inf
options.normalize (1,1) logical = false
options.debug_plots (1,1) logical = false
options.showPlot (1,1) logical = true
options.clear (1,1) logical = true
options.windowLength (1,1) double {mustBePositive, mustBeInteger} = 500
options.xLimits double = []
options.yLimits (1,2) double = [-2.5 2.5]
options.showStdAnnotations (1,1) logical = true
options.scatterAlpha (1,1) double {mustBeGreaterThanOrEqual(options.scatterAlpha, 0), mustBeLessThanOrEqual(options.scatterAlpha, 1)} = 0.2
options.scatterSize (1,1) double {mustBePositive} = 1
options.avgLineMaxPoints (1,1) double {mustBePositive, mustBeInteger} = 1000
options.avgLineSmoothWindow (1,1) double {mustBePositive, mustBeInteger} = 1
end
fsym = resolveSymbolRate(rxInput, refSymbols, options);
[symbols_for_lvl, avg_for_lvl, xAxisUs, info] = prepareLevelScatterData(rxInput, refSymbols, fsym, options);
if options.showPlot
info.plot = plotLevelScatter(symbols_for_lvl, avg_for_lvl, refSymbols, xAxisUs, options);
end
end
function fsym = resolveSymbolRate(rxInput, refSymbols, options)
if ~isempty(options.fsym)
fsym = options.fsym;
elseif ~isempty(options.f_sym)
fsym = options.f_sym;
elseif isa(refSymbols, "Signal") && ~isempty(refSymbols.fs)
fsym = refSymbols.fs;
elseif isa(rxInput, "Signal") && ~isempty(rxInput.fs)
fsym = rxInput.fs;
elseif iscell(rxInput) && ~isempty(rxInput) && isa(rxInput{1}, "Signal") && ~isempty(rxInput{1}.fs)
fsym = rxInput{1}.fs;
else
fsym = 1e6;
end
end
function [symbols_for_lvl, avg_for_lvl, xAxisUs, info] = prepareLevelScatterData(rxInput, refSymbols, fsym, options)
refSignal = numericSignal(refSymbols);
info = defaultInfo(fsym);
if iscell(rxInput)
[symbols_for_lvl, avg_for_lvl, info] = prepareCellInput(rxInput, refSymbols, fsym, options, info);
elseif isa(rxInput, "Signal")
[symbols_for_lvl, avg_for_lvl, info] = prepareSignalInput(rxInput, refSymbols, fsym, options, info);
else
rxSymbols = numericSignal(rxInput);
[symbols_for_lvl, avg_for_lvl] = levelScatterForOneSequence(rxSymbols, refSignal, options.windowLength);
end
xAxisUs = ((1:size(avg_for_lvl, 2)) / fsym) * 1e6;
end
function info = defaultInfo(fsym)
info = struct();
info.found_sync = true;
info.startSamples = 1;
info.shifts = [];
info.fsym = fsym;
info.shiftFs = fsym;
info.varianceByLevel = [];
info.plot = struct();
end
function [symbols_for_lvl, avg_for_lvl, info] = prepareSignalInput(rxSignal, refSymbols, fsym, options, info)
rxAtSymbolRate = rxSignal.resample("fs_in", rxSignal.fs, "fs_out", fsym);
refSignal = numericSignal(refSymbols);
if numel(rxAtSymbolRate.signal) == numel(refSignal)
rxSymbols = rxAtSymbolRate.signal;
if options.normalize
rxSymbols = normalizeNumericRms(rxSymbols);
end
[symbols_for_lvl, avg_for_lvl] = levelScatterForOneSequence(rxSymbols, refSignal, options.windowLength);
info.varianceByLevel = var(symbols_for_lvl, 0, 2, "omitnan");
return
end
[scopeCell, shifts, shiftFs, foundSync] = synchronizeRawSignal(rxSignal, refSymbols, fsym, options);
info.found_sync = foundSync;
info.shifts = shifts;
info.shiftFs = shiftFs;
if isempty(scopeCell)
symbols_for_lvl = [];
avg_for_lvl = [];
warning("showLevelScatter:NoScopeCells", ...
"No synchronized scope signal occurrences available.");
return
end
[symbols_for_lvl, avg_for_lvl, startSamples] = stitchScopeCells(scopeCell, refSymbols, fsym, shifts, shiftFs, options);
info.startSamples = startSamples;
info.varianceByLevel = var(symbols_for_lvl, 0, 2, "omitnan");
end
function [symbols_for_lvl, avg_for_lvl, info] = prepareCellInput(scopeCell, refSymbols, fsym, options, info)
scopeCell = scopeCell(:);
if isempty(scopeCell)
symbols_for_lvl = [];
avg_for_lvl = [];
info.found_sync = false;
warning("showLevelScatter:NoScopeCells", ...
"No synchronized scope signal occurrences available.");
return
end
shiftFs = options.shiftFs;
if shiftFs <= 0
shiftFs = fsym;
end
[symbols_for_lvl, avg_for_lvl, startSamples] = stitchScopeCells(scopeCell, refSymbols, fsym, options.shifts, shiftFs, options);
info.found_sync = true;
info.shifts = options.shifts;
info.shiftFs = shiftFs;
info.startSamples = startSamples;
info.varianceByLevel = var(symbols_for_lvl, 0, 2, "omitnan");
end
function [scopeCell, shifts, shiftFs, foundSync] = synchronizeRawSignal(rxSignal, refSymbols, fsym, options)
syncFs = options.syncFs;
if syncFs <= 0
syncFs = 2*fsym;
end
syncSignal = rxSignal.resample("fs_in", rxSignal.fs, "fs_out", syncFs);
if options.normalize
syncSignal = syncSignal.normalize("mode", "rms");
end
[~, scopeCell, ~, foundSync, shifts] = syncSignal.tsynch( ...
"reference", refSymbols, ...
"fs_ref", fsym, ...
"debug_plots", options.debug_plots);
if options.shiftFs > 0
shiftFs = options.shiftFs;
else
shiftFs = syncFs;
end
end
function [symbols_for_lvl, avg_for_lvl, startSamples] = stitchScopeCells(scopeCell, refSymbols, fsym, shifts, shiftFs, options)
recordOccurrences = min(numel(scopeCell), options.maxOccurences);
scopeCell = scopeCell(1:recordOccurrences);
refSignal = numericSignal(refSymbols);
startSamples = getStartSamples(shifts, recordOccurrences, shiftFs, fsym, numel(refSignal));
levelScatter = cell(1, recordOccurrences);
levelAverage = cell(1, recordOccurrences);
for occurrenceIdx = 1:recordOccurrences
occurrence = scopeCell{occurrenceIdx};
if isa(occurrence, "Signal")
occurrence = occurrence.resample("fs_out", fsym);
occurrence = occurrence.signal;
end
[levelScatter{occurrenceIdx}, levelAverage{occurrenceIdx}] = ...
levelScatterForOneSequence(occurrence, refSignal, options.windowLength);
end
numLevels = size(levelScatter{1}, 1);
traceLength = max(startSamples(:).' + cellfun(@(x) size(x, 2), levelScatter) - 1);
symbols_for_lvl = NaN(numLevels, traceLength);
avg_for_lvl = NaN(numLevels, traceLength);
for occurrenceIdx = 1:recordOccurrences
writeIdx = startSamples(occurrenceIdx):(startSamples(occurrenceIdx) + size(levelScatter{occurrenceIdx}, 2) - 1);
symbols_for_lvl(:, writeIdx) = levelScatter{occurrenceIdx};
avg_for_lvl(:, writeIdx) = levelAverage{occurrenceIdx};
end
end
function startSamples = getStartSamples(shifts, recordOccurrences, shiftFs, fsym, symbolLength)
if isempty(shifts)
startSamples = ((0:recordOccurrences-1) .* symbolLength) + 1;
return
end
shifts = shifts(:);
if numel(shifts) ~= recordOccurrences
positiveShifts = shifts(shifts >= 0);
if numel(positiveShifts) >= recordOccurrences
shifts = positiveShifts(1:recordOccurrences);
else
warning("showLevelScatter:ShiftCountMismatch", ...
"Shift count does not match scope cell count. Using sequential stitching.");
startSamples = ((0:recordOccurrences-1) .* symbolLength) + 1;
return
end
end
startSamples = round((shifts(1:recordOccurrences) - shifts(1)) ./ shiftFs .* fsym) + 1;
end
function [symbols_for_lvl, avg_for_lvl] = levelScatterForOneSequence(rxSymbols, refSymbols, windowLength)
rxSymbols = numericSignal(rxSymbols);
refSymbols = numericSignal(refSymbols);
assert(numel(rxSymbols) == numel(refSymbols), ...
'showLevelScatter:LengthMismatch', ...
'rxInput and refSymbols must have the same number of samples after resampling/synchronization.');
levels = unique(refSymbols);
[symbols_for_lvl, levels] = splitByReferenceLevels(rxSymbols, refSymbols, levels);
avg_for_lvl = NaN(numel(levels), numel(refSymbols));
for levelIdx = 1:numel(levels)
levelMask = ~isnan(symbols_for_lvl(levelIdx, :));
levelSamples = symbols_for_lvl(levelIdx, levelMask);
if isempty(levelSamples)
continue
end
smoothWindowLength = min(windowLength, numel(levelSamples));
avg_for_lvl(levelIdx, levelMask) = movmean(levelSamples, smoothWindowLength, 'Endpoints', 'shrink');
avg_for_lvl(levelIdx, :) = interpolateMissingLevelAverage(avg_for_lvl(levelIdx, :));
end
end
function [symbols_for_lvl, levels] = splitByReferenceLevels(rxSymbols, refSymbols, levels)
supportedPamLevels = [2 4 6 8 16];
if ismember(numel(levels), supportedPamLevels)
[symbols_for_lvl, levels] = PAMmapper(numel(levels), 0).splitByReferenceLevels( ...
rxSymbols, refSymbols, ...
"levels", levels);
else
symbols_for_lvl = NaN(numel(levels), numel(refSymbols));
for levelIdx = 1:numel(levels)
levelMask = refSymbols == levels(levelIdx);
symbols_for_lvl(levelIdx, levelMask) = rxSymbols(levelMask);
end
end
end
function plotInfo = plotLevelScatter(symbols_for_lvl, avg_for_lvl, refSymbols, xAxisUs, options)
plotInfo = struct();
if isempty(symbols_for_lvl)
return
end
if isnan(options.fignum)
figHandle = figure;
else
figHandle = figure(options.fignum);
if options.clear
clf;
end
end
ax = gca;
hold(ax, "on");
numLevels = size(symbols_for_lvl, 1);
cols = cbrewer2("RdBu", numLevels);
% cols = linspecer(numLevels);
xLimits = getXLimits(options.xLimits, xAxisUs, symbols_for_lvl);
if 1
for levelIdx = numLevels:-1:1
scatter(ax, xAxisUs, symbols_for_lvl(levelIdx, :), options.scatterSize, ".", ...
"MarkerEdgeColor", cols(levelIdx, :), ...
"MarkerEdgeAlpha", options.scatterAlpha);
end
end
for levelIdx = 1:numLevels
[xReduced, yReduced] = reduceAverageLineForPlot( ...
xAxisUs, avg_for_lvl(levelIdx, :), ...
options.avgLineMaxPoints, options.avgLineSmoothWindow);
plot(ax, xReduced, yReduced, ...
"LineWidth", 2, ...
"Color", cols(levelIdx, :));
end
% refValues = unique(numericSignal(refSymbols));
% for refIdx = 1:numel(refValues)
% yline(ax, refValues(refIdx), "--", ...
% "Color", [0.35 0.35 0.35], ...
% "LineWidth", 0.75, ...
% "HandleVisibility", "off");
% end
if options.showStdAnnotations
annotateLevelStd(symbols_for_lvl, avg_for_lvl, xAxisUs, options.xLimits);
end
xlabel(ax, 'Time in $\mu$s');
ylabel(ax, 'Normalized Amplitude');
xlim(ax, xLimits);
ylim(ax, options.yLimits);
grid(ax, "on");
figure(figHandle);
drawnow;
end
function [xReduced, yReduced] = reduceAverageLineForPlot(xAxisUs, yTrace, maxPoints, smoothWindow)
valid = isfinite(xAxisUs) & isfinite(yTrace);
xValid = xAxisUs(valid);
yValid = yTrace(valid);
if isempty(xValid)
xReduced = [];
yReduced = [];
return
end
[xValid, uniqueIdx] = unique(xValid, "stable");
yValid = yValid(uniqueIdx);
if smoothWindow > 1
yValid = smoothdata(yValid, "movmean", smoothWindow);
end
numOut = min(maxPoints, numel(xValid));
if numOut >= numel(xValid)
xReduced = xValid;
yReduced = yValid;
return
end
xReduced = linspace(xValid(1), xValid(end), numOut);
yReduced = interp1(xValid, yValid, xReduced, "pchip");
end
function xLimits = getXLimits(configuredLimits, xAxisUs, symbols_for_lvl)
if ~isempty(configuredLimits)
xLimits = configuredLimits;
return
end
filledColumns = any(~isnan(symbols_for_lvl), 1);
if ~any(filledColumns)
xLimits = [xAxisUs(1), xAxisUs(end)];
return
end
lastFilledColumn = find(filledColumns, 1, "last");
xMax = xAxisUs(lastFilledColumn);
xLimits = [0, 1*xMax];
end
function annotateLevelStd(symbols_for_lvl, avg_for_lvl, xAxisUs, configuredXLimits)
xLimits = getXLimits(configuredXLimits, xAxisUs, symbols_for_lvl);
xText = xLimits(1) + 0.2*diff(xLimits);
for levelIdx = 1:size(symbols_for_lvl, 1)
levelSamples = symbols_for_lvl(levelIdx, :);
levelStd = std(levelSamples, 0, 2, 'omitnan');
if isnan(levelStd)
continue
end
levelAverage = avg_for_lvl(levelIdx, :);
yText = median(levelAverage(~isnan(levelAverage)), 'omitnan');
if isnan(yText)
yText = median(levelSamples(~isnan(levelSamples)), 'omitnan');
end
label = ['$\sigma^2_',sprintf('%d', levelIdx),'$ = ', sprintf('%.3f', levelStd)];
text(xText, yText, label, ...
'Interpreter', 'latex', ...
'HorizontalAlignment', 'right', ...
'VerticalAlignment', 'middle', ...
'FontSize', 10, ...
'BackgroundColor', 'w', ...
'Margin', 2, ...
'EdgeColor', [0.8 0.8 0.8]);
end
end
function levelAverage = interpolateMissingLevelAverage(levelAverage)
validSamples = ~isnan(levelAverage);
if nnz(validSamples) == 0
return
elseif nnz(validSamples) == 1
levelAverage(:) = levelAverage(validSamples);
return
end
t = 1:numel(levelAverage);
levelAverage(~validSamples) = interp1(t(validSamples), levelAverage(validSamples), ...
t(~validSamples), 'linear', 'extrap');
end
function values = numericSignal(signalLike)
if isa(signalLike, "Signal")
values = signalLike.signal;
else
values = signalLike;
end
values = values(:).';
end
function values = normalizeNumericRms(values)
values = values ./ sqrt(mean(values.^2, "omitnan"));
end