function h = plot_measurements_gpt(T, cfg) % Versatile plotting from your DB table (with cbrewer2 'Paired' palette). % % Usage: % h = plot_measurements_flex(dataTable, cfg) %% ---- Defaults if nargin < 2, cfg = struct; end defaults = struct( ... 'x_axis' , 'symbolrate', ... 'y_axis' , 'BER', ... 'y_scale' , 'auto', ... 'group_by' , {{'equalizer_structure','pre_emph'}}, ... 'filters' , struct, ... 'agg' , 'mean', ... 'outlier' , 'auto', ... 'mad_z' , 3, ... 'pct_limits' , [2.5 97.5], ... 'min_pts_x' , 3, ... 'show_raw' , true, ... 'show_precoded', [], ... 'show_spread' , 'none', ... 'fec_lines' , [2.2e-4 4.85e-3 2e-2], ... 'plot', struct() ... ); cfg = filldefaults(cfg, defaults); % ---- Plot defaults (new) plotdefs = struct( ... 'use_cbrewer2' , true, ... 'colormap' , 'Paired', ... % ColorBrewer 'Paired' 'paired_dark_first' , true, ... % dark for lines, light for scatter 'lineWidth' , 1.8, ... 'errWidth' , 1.0, ... 'scatterSize' , 14, ... 'scatterAlpha' , 0.35, ... 'marker' , 'o', ... 'marker_precoded' , 's', ... 'lineStyle_pre_emph_on' , '--', ... 'lineStyle_pre_emph_off', '-', ... 'legendLocation' , 'best', ... 'fecLineWidth' , 2.2, ... % thicker FEC limits 'fecColor' , [0.25 0.25 0.25], ... 'capSize' , 6, ... 'lineStyle_default' , '-', ... 'use_pre_emph_styling' , true ... ); cfg.plot = filldefaults(cfg.plot, plotdefs); %% ---- Derived/prep columns if ~ismember('pre_emph', T.Properties.VariableNames) if ~ismember('db_mode', T.Properties.VariableNames) error('Missing column "db_mode" for pre_emph derivation.'); end T.pre_emph = T.db_mode == 0; end if ~ismember(cfg.y_axis, T.Properties.VariableNames) error('y_axis "%s" not found in table.', cfg.y_axis); end isBER = startsWith(cfg.y_axis, "BER", 'IgnoreCase', true); if strcmpi(cfg.y_scale,'auto'), cfg.y_scale = tern(isBER, 'log', 'linear'); end if strcmpi(cfg.outlier,'auto'), cfg.outlier = tern(isBER, 'mad', 'none'); end if isempty(cfg.show_precoded) cfg.show_precoded = isBER && ismember('BER_precoded', T.Properties.VariableNames); end %% ---- Filters T = applyFilters(T, cfg.filters); [x_raw, x_label] = computeX(T, cfg.x_axis); y_raw = T.(cfg.y_axis); validXY = isfinite(x_raw) & isfinite(y_raw); T = T(validXY, :); x_raw = x_raw(validXY); y_raw = y_raw(validXY); if cfg.show_precoded && ismember('BER_precoded', T.Properties.VariableNames) y_raw_p = T.BER_precoded(validXY); else y_raw_p = []; end %% ---- Grouping group_by = cfg.group_by; if ~all(ismember(group_by, T.Properties.VariableNames)) error('Some group_by columns are missing in table.'); end [G, grpTbl] = findgroups(T(:, group_by)); nG = max(G); % ==== Colors (cbrewer2 'Paired' with dark/ light pairs) ==== [cols_line, cols_scatter] = buildGroupColors(nG, cfg.plot); %% ---- Plotting figure; hold on; grid on; h.lines = gobjects(nG,1); h.err = gobjects(nG,1); h.scat = gobjects(nG,1); h.lines_p = gobjects(nG,1); for gi = 1:nG idx = (G==gi); Ti = T(idx,:); xi = x_raw(idx); yi = y_raw(idx); % Aggregate per unique x [xu, ia, iu] = unique(xi); yu = nan(size(xu)); ylo = nan(size(xu)); yhi = nan(size(xu)); for k = 1:numel(xu) bin = (iu==k); yy = yi(bin); yy = yy(isfinite(yy)); if isempty(yy), continue; end km = outlierMask(yy, cfg, strcmpi(cfg.y_scale,'log')); if nnz(km) < cfg.min_pts_x, km = true(size(yy)); end yy = yy(km); if strcmpi(cfg.agg,'median'), yu(k)=median(yy,'omitnan'); elseif strcmpi(cfg.agg,'mean'), yu(k)=mean(yy,'omitnan'); elseif strcmpi(cfg.agg,'min'), yu(k)=min(yy); end if strcmpi(cfg.show_spread,'iqr') q = prctile(yy,[25 75]); ylo(k) = max(yu(k)-q(1), eps); yhi(k) = max(q(2)-yu(k), eps); end end % sort [xu, ord] = sort(xu); yu = yu(ord); ylo = ylo(ord); yhi = yhi(ord); % Styles % Decide if we style by pre_emph canStyleByPre = cfg.plot.use_pre_emph_styling && ismember('pre_emph', T.Properties.VariableNames); if canStyleByPre if any(strcmp(group_by,'pre_emph')) % pre_emph is an explicit grouping key -> take it from the group table pre = logical(grpTbl.pre_emph(gi)); else % pre_emph not grouped, but available in the rows -> infer from the members of this group pre = logical(mode(T.pre_emph(G==gi))); end ls = tern(pre, cfg.plot.lineStyle_pre_emph_on, cfg.plot.lineStyle_pre_emph_off); else % no pre-emph styling → use a single default style ls = cfg.plot.lineStyle_default; end lbl = buildLabel(grpTbl(gi,:), group_by); % Main line (dark) colL = cols_line(gi,:); h.lines(gi) = plot(xu, yu, ... 'LineWidth', cfg.plot.lineWidth, ... 'Marker', cfg.plot.marker, 'MarkerSize', 5, ... 'Color', colL, 'LineStyle', ls, ... 'DisplayName', char(lbl)); % Spread (IQR) in line color if strcmpi(cfg.show_spread,'iqr') && any(isfinite(ylo)) && any(isfinite(yhi)) h.err(gi) = errorbar(xu, yu, ylo, yhi, 'LineStyle','none', ... 'Color', colL, 'CapSize', cfg.plot.capSize, 'HandleVisibility','off'); h.err(gi).LineWidth = cfg.plot.errWidth; end % Raw kept scatter (light) if cfg.show_raw keep_all = false(size(yi)); for k = 1:numel(xu) bin = (iu==k); yy = yi(bin); km = outlierMask(yy, cfg, strcmpi(cfg.y_scale,'log')); if nnz(km) < cfg.min_pts_x, km = true(size(yy)); end keep_all(bin) = km; end colS = cols_scatter(gi,:); scatter(xi(keep_all), yi(keep_all), cfg.plot.scatterSize, colS, 'filled', ... 'MarkerFaceAlpha', cfg.plot.scatterAlpha, 'MarkerEdgeAlpha', cfg.plot.scatterAlpha, ... 'HandleVisibility','off'); end % Precoded overlay (dotted, squares), in line color if cfg.show_precoded && ~isempty(y_raw_p) && strcmpi(cfg.y_axis,'BER') ypi = y_raw_p(idx); ypu = nan(size(xu)); for k = 1:numel(xu) bin = (iu==k); yy = ypi(bin); yy = yy(isfinite(yy)); if isempty(yy), continue; end km = outlierMask(yy, cfg, true); if nnz(km) < cfg.min_pts_x, km = true(size(yy)); end yy = yy(km); if strcmpi(cfg.agg,'median'), ypu(k)=median(yy,'omitnan'); elseif strcmpi(cfg.agg,'mean'), ypu(k)=mean(yy,'omitnan'); elseif strcmpi(cfg.agg,'min'), ypu(k)=min(yy); end end h.lines_p(gi) = plot(xu, ypu, ... 'LineWidth', max(1.2, cfg.plot.lineWidth-0.2), ... 'Marker', cfg.plot.marker_precoded, 'MarkerSize', 5, ... 'Color', colL, 'LineStyle', ':', ... 'DisplayName', [char(lbl) ' (precoded)']); end end %% ---- Axes / Labels / FEC ylabel(cfg.y_axis, 'Interpreter','none'); xlabel(x_label, 'Interpreter','none'); set(gca, 'YScale', cfg.y_scale, 'FontSize', 11); legend('Location', cfg.plot.legendLocation); box on; if startsWith(cfg.y_axis,"BER",'IgnoreCase',true) for v = cfg.fec_lines yline(v, '--', 'Color', cfg.plot.fecColor, ... 'LineWidth', cfg.plot.fecLineWidth, 'HandleVisibility','off'); end ylim([1e-4, 0.3]); end end % ===== main ===== %% ===================== Helpers ===================== function cfg = filldefaults(cfg, defs) fn = fieldnames(defs); for i = 1:numel(fn) f = fn{i}; if ~isfield(cfg, f) || isempty(cfg.(f)) cfg.(f) = defs.(f); elseif isstruct(defs.(f)) && isstruct(cfg.(f)) cfg.(f) = filldefaults(cfg.(f), defs.(f)); % recursive for structs end end end function out = tern(cond, a, b) if cond, out = a; else, out = b; end end function T2 = applyFilters(T, filters) if isempty(filters), T2 = T; return; end keep = true(height(T),1); fns = fieldnames(filters); for i = 1:numel(fns) name = fns{i}; if ~ismember(name, T.Properties.VariableNames) warning('Filter column "%s" not found. Ignored.', name); %#ok<*WNTAG> continue end val = filters.(name); col = T.(name); if isa(val,'function_handle') m = val(col); if ~islogical(m) || ~isequal(size(m), size(col)) error('Filter for %s must return logical mask of same size.', name); end keep = keep & m; else keep = keep & ismember(col, val); end end T2 = T(keep,:); end function [x, label] = computeX(T, whichX) switch lower(whichX) case {'symbolrate','baudrate'} x = T.symbolrate * 1e-9; label = 'Symbol rate [GBd]'; case 'bitrate' if ~ismember('pam_level', T.Properties.VariableNames) error('bitrate requires "pam_level" column.'); end bits = log2(double(T.pam_level)); x = (T.symbolrate .* bits) * 1e-9; label = 'Bitrate [Gb/s]'; otherwise if ~ismember(whichX, T.Properties.VariableNames) error('x_axis "%s" not found in table.', whichX); end x = T.(whichX); label = whichX; end x = double(x(:)); end function keep = outlierMask(y, cfg, useLog) if isempty(y), keep = false(size(y)); return; end y = y(:); switch lower(cfg.outlier) case 'none' keep = true(size(y)); return case 'mad' z = tern(useLog, log10(y), y); med = median(z,'omitnan'); madv = median(abs(z-med),'omitnan'); if ~(isfinite(madv) && madv>0) keep = true(size(y)); return end sigma = 1.4826*madv; zz = tern(useLog, log10(y), y); keep = abs(zz - med) <= cfg.mad_z*sigma; case 'pctl' pr = prctile(y, cfg.pct_limits); keep = (y >= pr(1)) & (y <= pr(2)); otherwise error('Unknown outlier mode "%s".', cfg.outlier); end end function s = buildLabel(grpRow, group_by) parts = strings(1, numel(group_by)); for i = 1:numel(group_by) key = group_by{i}; val = grpRow.(key); if iscell(val), val = val{1}; end if islogical(val), val = tern(val,'pre-emph on','pre-emph off'); end parts(i) = sprintf('%s=%s', key, string(val)); end s = strjoin(parts, ', '); end function [cols_line, cols_scatter] = buildGroupColors(nG, plotcfg) % Build paired colors (dark for lines, light for scatter) using cbrewer2('Paired') useBrewer = plotcfg.use_cbrewer2 && exist('cbrewer2','file')==2; if useBrewer N = max(2*nG, 12); % ensure pairs available C = cbrewer2(plotcfg.colormap, N); cols_line = zeros(nG,3); cols_scatter = zeros(nG,3); for i = 1:nG if plotcfg.paired_dark_first dark = C(2*i-1, :); light = C(2*i, :); else light = C(2*i-1, :); dark = C(2*i, :); end cols_line(i,:) = dark; cols_scatter(i,:) = light; end else % Fallback: lines() + lightened scatter C = lines(max(nG,7)); cols_line = C(1:nG,:); cols_scatter = zeros(nG,3); for i = 1:nG cols_scatter(i,:) = lightenColor(cols_line(i,:), 0.5); % 50% toward white end end end function c2 = lightenColor(c, fracTowardWhite) c = c(:).'; c2 = (1-fracTowardWhite)*c + fracTowardWhite*1; end