restructure and organize
This commit is contained in:
@@ -1,133 +1,370 @@
|
||||
function [symbols_for_lvl,avg_for_lvl] = showLevelScatter(eq_signal,ref_symbols,options)
|
||||
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
|
||||
eq_signal
|
||||
ref_symbols
|
||||
options.fignum (1,1) double = NaN % Default to NaN if not provided
|
||||
options.displayname (1,:) char = '' % Default to an empty string if not provided
|
||||
options.f_sym =1e6;
|
||||
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
|
||||
|
||||
plot_shit = 1;
|
||||
fsym = resolveSymbolRate(rxInput, refSymbols, options);
|
||||
[symbols_for_lvl, avg_for_lvl, xAxisUs, info] = prepareLevelScatterData(rxInput, refSymbols, fsym, options);
|
||||
|
||||
if isa(eq_signal,'Signal')
|
||||
options.f_sym = eq_signal.fs;
|
||||
eq_signal = eq_signal.signal;
|
||||
assert(~isempty(options.f_sym),'No fsym given');
|
||||
if options.showPlot
|
||||
plotLevelScatter(symbols_for_lvl, avg_for_lvl, refSymbols, xAxisUs, options);
|
||||
end
|
||||
if isa(ref_symbols,'Signal')
|
||||
ref_symbols = ref_symbols.signal;
|
||||
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
|
||||
|
||||
if plot_shit
|
||||
% Determine the figure number to use or create a new figure
|
||||
if isnan(options.fignum)
|
||||
fig = figure; % Create a new figure and get its handle
|
||||
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
|
||||
fig = figure(options.fignum); % Use the specified figure number
|
||||
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);
|
||||
|
||||
rx_symbols = eq_signal; %./ rms(eq_signal);
|
||||
correct_symbols = ref_symbols;
|
||||
f_sym = options.f_sym;
|
||||
|
||||
col = cbrewer2('Paired',numel(unique(correct_symbols))*2);
|
||||
ccnt = -1;
|
||||
|
||||
levels = unique(correct_symbols);
|
||||
symbols_for_lvl = NaN(numel(levels),length(correct_symbols));
|
||||
start = 1;
|
||||
ende = length(correct_symbols);
|
||||
|
||||
for l = 1:numel(levels)
|
||||
ccnt = ccnt+2;
|
||||
|
||||
level_amplitude = levels(l);
|
||||
|
||||
symbols_for_lvl(l,correct_symbols==level_amplitude) = rx_symbols(correct_symbols==level_amplitude);
|
||||
std_lvl(l) = std(symbols_for_lvl(l,:),'omitnan');
|
||||
xax_in_sec = ((1:length(correct_symbols)) / f_sym) * 1e6;
|
||||
|
||||
if plot_shit
|
||||
scatter(xax_in_sec(start:ende),symbols_for_lvl(l,start:ende),10,'.','MarkerFaceAlpha',0.5,'MarkerEdgeAlpha',0.5,'MarkerEdgeColor',col(ccnt,:));
|
||||
hold on;
|
||||
end
|
||||
|
||||
for levelIdx = 1:numLevels
|
||||
scatter(xAxisUs, symbols_for_lvl(levelIdx, :), 10, ".", ...
|
||||
"MarkerEdgeColor", cols((2*levelIdx)-1, :), ...
|
||||
"MarkerEdgeAlpha", 0.5);
|
||||
end
|
||||
|
||||
std_lvl = round(std_lvl,2);
|
||||
|
||||
ccnt = 0;
|
||||
avg_for_lvl = NaN(numel(levels),length(correct_symbols));
|
||||
% Add the windowed/ smoothed curves
|
||||
for l = 1:numel(levels)
|
||||
ccnt = ccnt+2;
|
||||
level_amplitude = levels(l);
|
||||
|
||||
L = 500;
|
||||
movmean = 1/L .* movsum(rx_symbols(correct_symbols==level_amplitude),[L/2,L/2], 'Endpoints', 'fill');
|
||||
|
||||
avg_for_lvl(l,correct_symbols==level_amplitude) = movmean;
|
||||
|
||||
nanx = isnan(avg_for_lvl(l,:));
|
||||
t = 1:numel(avg_for_lvl(l,:));
|
||||
avg_for_lvl(l,nanx) = interp1(t(~nanx), avg_for_lvl(l,~nanx), t(nanx));
|
||||
|
||||
xax_in_sec = ((1:length(correct_symbols)) / f_sym) * 1e6;
|
||||
% xax_in_sec = 1:length(correct_symbols);
|
||||
|
||||
if plot_shit
|
||||
plot(xax_in_sec(start:ende),avg_for_lvl(l,start:ende),'Color',col(ccnt,:));
|
||||
end
|
||||
hold on
|
||||
for levelIdx = 1:numLevels
|
||||
plot(xAxisUs, avg_for_lvl(levelIdx, :), ...
|
||||
"LineWidth", 1, ...
|
||||
"Color", cols(2*levelIdx, :));
|
||||
end
|
||||
|
||||
|
||||
|
||||
if 0
|
||||
annotation(fig,'textbox',...
|
||||
[0.660523809523809 0.844444444444448 0.133523809523809 0.0603174603174607],...
|
||||
'String',['\sigma = ',num2str(std_lvl(4))],...
|
||||
'LineWidth',1.8,...
|
||||
'LineStyle','none',...
|
||||
'FontSize',12,...
|
||||
'FitBoxToText','off');
|
||||
|
||||
% Create textbox
|
||||
annotation(fig,'textbox',...
|
||||
[0.667666666666665 0.642857142857147 0.133523809523809 0.0603174603174607],...
|
||||
'String',['\sigma = ',num2str(std_lvl(3))],...
|
||||
'LineWidth',1.8,...
|
||||
'LineStyle','none',...
|
||||
'FontSize',12,...
|
||||
'FitBoxToText','off');
|
||||
|
||||
% Create textbox
|
||||
annotation(fig,'textbox',...
|
||||
[0.671238095238093 0.442857142857148 0.133523809523809 0.0603174603174608],...
|
||||
'String',['\sigma = ',num2str(std_lvl(2))],...
|
||||
'LineWidth',1.8,...
|
||||
'LineStyle','none',...
|
||||
'FontSize',12,...
|
||||
'FitBoxToText','off');
|
||||
|
||||
% Create textbox
|
||||
annotation(fig,'textbox',...
|
||||
[0.670047619047616 0.265079365079371 0.133523809523809 0.0603174603174608],...
|
||||
'String',['\sigma = ',num2str(std_lvl(1))],...
|
||||
'LineWidth',1.8,...
|
||||
'LineStyle','none',...
|
||||
'FontSize',12,...
|
||||
'FitBoxToText','off');
|
||||
refValues = numericSignal(refSymbols);
|
||||
yline(unique(refValues), "HandleVisibility", "off");
|
||||
if options.showStdAnnotations
|
||||
annotateLevelStd(symbols_for_lvl, avg_for_lvl, xAxisUs, options.xLimits);
|
||||
end
|
||||
|
||||
if plot_shit
|
||||
% yline(levels);
|
||||
xlabel('Time in $\mu$s');
|
||||
ylabel('Normalized Amplitude');
|
||||
ylim([-3 3]);
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user