371 lines
12 KiB
Matlab
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
|