Files
imdd_silas/Functions/EQ_visuals/showLevelScatter.m
2026-06-22 22:58:58 +02:00

371 lines
12 KiB
Matlab

function [symbols_for_lvl, avg_for_lvl, 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 = [-3 3]
options.showStdAnnotations (1,1) logical = true
end
fsym = resolveSymbolRate(rxInput, refSymbols, options);
[symbols_for_lvl, avg_for_lvl, xAxisUs, info] = prepareLevelScatterData(rxInput, refSymbols, fsym, options);
if options.showPlot
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 = [];
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 plotLevelScatter(symbols_for_lvl, avg_for_lvl, refSymbols, xAxisUs, options)
if isempty(symbols_for_lvl)
return
end
if isnan(options.fignum)
figure;
else
figure(options.fignum);
if options.clear
clf;
end
end
hold on
numLevels = size(symbols_for_lvl, 1);
cols = cbrewer2("Paired", 2*numLevels);
for levelIdx = 1:numLevels
scatter(xAxisUs, symbols_for_lvl(levelIdx, :), 10, ".", ...
"MarkerEdgeColor", cols((2*levelIdx)-1, :), ...
"MarkerEdgeAlpha", 0.5);
end
for levelIdx = 1:numLevels
plot(xAxisUs, avg_for_lvl(levelIdx, :), ...
"LineWidth", 1, ...
"Color", cols(2*levelIdx, :));
end
refValues = numericSignal(refSymbols);
yline(unique(refValues), "HandleVisibility", "off");
if options.showStdAnnotations
annotateLevelStd(symbols_for_lvl, avg_for_lvl, xAxisUs, options.xLimits);
end
xlabel('Time in $\mu$s');
ylabel('Normalized Amplitude');
xlim(getXLimits(options.xLimits, xAxisUs, symbols_for_lvl));
ylim(options.yLimits);
grid on
drawnow;
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.05*xMax];
end
function annotateLevelStd(symbols_for_lvl, avg_for_lvl, xAxisUs, configuredXLimits)
xLimits = getXLimits(configuredXLimits, xAxisUs, symbols_for_lvl);
xText = xLimits(1) + 0.96*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 = ['std = ', sprintf('%.3f', levelStd)];
text(xText, yText, label, ...
'Interpreter', 'none', ...
'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