From 99898da51954a6f94a1a1d4b1c0acecba7a8cefe Mon Sep 17 00:00:00 2001 From: Silas Oettinghaus Date: Wed, 8 Oct 2025 09:51:47 +0200 Subject: [PATCH] MLSE has some more class settings DSP auswertung weiter geschrieben --- Classes/04_DSP/Sequence Detection/MLSE.m | 30 +- Functions/EQ_structures/dsp_runid.m | 20 +- .../Auswertung_JLT/gmi_vs_rate.m | 9 +- .../Auswertung_JLT/plot_measurements_gpt.m | 340 ++++++++++++++++++ .../Auswertung_JLT/run_dsp_from_db.m | 2 +- .../Auswertung_JLT/run_plot_measurements.m | 48 +++ 6 files changed, 438 insertions(+), 11 deletions(-) create mode 100644 projects/HighSpeedExperiment_2024/Auswertung_JLT/plot_measurements_gpt.m create mode 100644 projects/HighSpeedExperiment_2024/Auswertung_JLT/run_plot_measurements.m diff --git a/Classes/04_DSP/Sequence Detection/MLSE.m b/Classes/04_DSP/Sequence Detection/MLSE.m index c25654c..3a8e288 100644 --- a/Classes/04_DSP/Sequence Detection/MLSE.m +++ b/Classes/04_DSP/Sequence Detection/MLSE.m @@ -6,6 +6,10 @@ classdef MLSE < handle DIR trellis_states duobinary_output + trellis_state_mode + trellis_exclusion + debug + scale_mode end methods (Access=public) @@ -19,7 +23,10 @@ classdef MLSE < handle options.DIR double = [1]; options.trellis_states double = [-3 -1 1 3]; options.duobinary_output logical = false; - + options.trellis_state_mode = 2; + options.trellis_exclusion = 0; + options.scale_mode = 2; + options.debug = 0; end % @@ -33,6 +40,12 @@ classdef MLSE < handle function [signalclass_hd,LLR,GMI] = process(obj,signalclass,ref_symbolclass) + arguments + obj + signalclass + ref_symbolclass + end + data_in = signalclass.signal; shape_in = size(data_in); data_ref = ref_symbolclass.signal; @@ -54,18 +67,25 @@ classdef MLSE < handle function [VITERBI_ESTIMATION_SYMBOLS,LLR_maxlogmap,GMI] = process_(obj,data_in,data_ref) - debug = 1; + + arguments + obj + data_in + data_ref + end - trellis_state_mode = 0; % General: States should match the target states of the prev. EQ (EQ's job was to reduce the error between signal and the target) + debug = obj.debug; + + trellis_state_mode = obj.trellis_state_mode; % General: States should match the target states of the prev. EQ (EQ's job was to reduce the error between signal and the target) % 0 = use provided states (MUST provide the correct states); % 1 = normalize to = 1 rms; % 2 = use target symbols; % 3 = use statistical levels % 3 analyzes avg of rx signal levels - can help with nonlinear impairments - trellis_exclusion = 0; % PAM-6 only (only if data is NOT precoded!) + trellis_exclusion = obj.trellis_exclusion; % PAM-6 only (only if data is NOT precoded!) - scale_mode = 0; % scale_mode: + scale_mode = obj.scale_mode; % scale_mode: % 0 = no scaling, % 1 = RMS→scale MODEL, % 2 = MMSE/time-corr→scale MODEL, diff --git a/Functions/EQ_structures/dsp_runid.m b/Functions/EQ_structures/dsp_runid.m index 09e5e27..132cc75 100644 --- a/Functions/EQ_structures/dsp_runid.m +++ b/Functions/EQ_structures/dsp_runid.m @@ -201,7 +201,18 @@ try if useviterbi mlse_ = MLSE_viterbi("duobinary_output",0,'M',M,'trellis_states',PAMmapper(M,0).levels); else - mlse_ = MLSE("duobinary_output",0,'M',M,'trellis_states',PAMmapper(M,0).levels); + + if duob_mode == db_mode.no_db && M == 6 %only for PAM-6 and no duobinary precoding, otherwise leads to false sequence estimation + trellexlusion = 1; + else + trellexlusion = 0; + end + + %state_mode 3 -> stat lvl; state_mode 2 -> use target lvls + %scale_mode 2 -> mmse adaption + + mlse_ = MLSE("duobinary_output",0,'M',M,'trellis_states',PAMmapper(M,0).levels,'scale_mode',2,'trellis_exclusion',trellexlusion,'trellis_state_mode',2); + end [ffe_results, mlse_results] = vnle_postfilter_mlse(eq_, pf_, mlse_, M, Scpe_sig, Symbols, Tx_bits, ... @@ -253,7 +264,12 @@ try if useviterbi mlse_db_ = MLSE_viterbi("duobinary_output",0,'M',M,'trellis_states',PAMmapper(M,0).levels); else - mlse_db_ = MLSE("DIR",[1,1],"duobinary_output",0,"M",M,"trellis_states",PAMmapper(M,0).levels); + if duob_mode == db_mode.no_db && M == 6 %only for PAM-6 and no duobinary precoding, otherwise leads to false sequence estimation + trellexlusion = 1; + else + trellexlusion = 0; + end + mlse_db_ = MLSE("DIR",[1,1],"duobinary_output",0,"M",M,"trellis_states",PAMmapper(M,0).levels,'scale_mode',2,'trellis_exclusion',trellexlusion,'trellis_state_mode',3); end ffe_order = [50, 5, 5]; eq_ = EQ("Ne",ffe_order,"Nb",dfe_order,"training_length",len_tr,"training_loops",5,"dd_loops",5,"K",2,"DCmu",mu_dc,"DDmu",[mu_ffe mu_dfe],"DFEmu",0.005,"FFEmu",0,"plotfinal",0,"ideal_dfe",1); diff --git a/projects/HighSpeedExperiment_2024/Auswertung_JLT/gmi_vs_rate.m b/projects/HighSpeedExperiment_2024/Auswertung_JLT/gmi_vs_rate.m index 7f84f62..26aaddb 100644 --- a/projects/HighSpeedExperiment_2024/Auswertung_JLT/gmi_vs_rate.m +++ b/projects/HighSpeedExperiment_2024/Auswertung_JLT/gmi_vs_rate.m @@ -17,7 +17,9 @@ fp.where('Runs', 'wavelength','EQUALS', 1310); % fp.where('Runs', 'db_mode','EQUALS', 1); % 0 == high preemphasis // 1 == low preemphasis fp.where('Runs', 'rop_attenuation','EQUALS', 0); -[dataTable,~] = db.queryDB(fp, db.getTableFieldNames('dashboard')); +fields = db.getTableFieldNames('power_state_info'); +fields = [fields; db.getTableFieldNames('dashboard_ungrouped_new')]; +[dataTable,~] = db.queryDB(fp, fields); eqstructures = unique(dataTable.equalizer_structure); @@ -42,12 +44,13 @@ for pre_emph = [0,1] eq_filtered = eq_filtered(eq_filtered.DIR == "1",:); end symbolrate_sorted = sortrows(eq_filtered,{'symbolrate'}, 'ascend'); + % Example data (replace these with your real vectors) symbolrate = symbolrate_sorted.symbolrate.*1e-9; % in baud bitrate = symbolrate * 2; - gmi = symbolrate_sorted.max_GMI; % BER - snr = symbolrate_sorted.max_SNR; % BER + gmi = symbolrate_sorted.GMI; % BER + snr = symbolrate_sorted.SNR; % BER cols = cbrewer2('Paired',12); dname = [char(eq_choice)]; diff --git a/projects/HighSpeedExperiment_2024/Auswertung_JLT/plot_measurements_gpt.m b/projects/HighSpeedExperiment_2024/Auswertung_JLT/plot_measurements_gpt.m new file mode 100644 index 0000000..0b5cefd --- /dev/null +++ b/projects/HighSpeedExperiment_2024/Auswertung_JLT/plot_measurements_gpt.m @@ -0,0 +1,340 @@ +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 ... +); +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 + pre = logical(grpTbl.pre_emph(gi)); + ls = tern(pre, cfg.plot.lineStyle_pre_emph_on, cfg.plot.lineStyle_pre_emph_off); + 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 diff --git a/projects/HighSpeedExperiment_2024/Auswertung_JLT/run_dsp_from_db.m b/projects/HighSpeedExperiment_2024/Auswertung_JLT/run_dsp_from_db.m index 2a82dc6..8da64d8 100644 --- a/projects/HighSpeedExperiment_2024/Auswertung_JLT/run_dsp_from_db.m +++ b/projects/HighSpeedExperiment_2024/Auswertung_JLT/run_dsp_from_db.m @@ -44,7 +44,7 @@ M = 6; % fp.where('Runs', 'pam_level','EQUALS', M); % fp.where('Runs', 'bitrate','EQUALS', 480e9); % fp.where('Runs', 'symbolrate','EQUALS', 162e9); -fp.where('Runs', 'fiber_length','EQUALS', 1); +% fp.where('Runs', 'fiber_length','EQUALS', 1); fp.where('Runs', 'is_mpi','EQUALS', 0); % fp.where('Runs', 'interference_path_length','EQUALS', 1000); % fp.where('Runs', 'loop_id','GREATER_THAN', 11); diff --git a/projects/HighSpeedExperiment_2024/Auswertung_JLT/run_plot_measurements.m b/projects/HighSpeedExperiment_2024/Auswertung_JLT/run_plot_measurements.m new file mode 100644 index 0000000..e7dd62f --- /dev/null +++ b/projects/HighSpeedExperiment_2024/Auswertung_JLT/run_plot_measurements.m @@ -0,0 +1,48 @@ +database_type = 'mysql'; +dataBase = 'labor_highspeed';%'C:\Users\Silas\Documents\MATLAB\Datensätze\sioe_labor\silas_labor_newdsp_newstructure.db'; +db = DBHandler("dataBase", [dataBase], "type", database_type); + +M = 4; +fp = QueryFilter(); +% fp.where('Runs', 'run_id','EQUALS', 987); +fp.where('Runs', 'pam_level','EQUALS', M); +fp.where('Runs', 'symbolrate','EQUALS', 150e9); +% fp.where('Runs', 'fiber_length','EQUALS', 10); +fp.where('Runs', 'is_mpi','EQUALS', 0); +% fp.where('Runs', 'interference_path_length','EQUALS', 1000); +% fp.where('Runs', 'loop_id','GREATER_THAN', 11); +% fp.where('Runs', 'sir','EQUALS',18); +% fp.where('Runs', 'wavelength','EQUALS', 1310); +fp.where('Runs', 'db_mode','EQUALS', 1); % 0 == high preemphasis // 1 == low preemphasis +fp.where('Runs', 'rop_attenuation','EQUALS', 0); + +fields = db.getTableFieldNames('power_state_info'); +fields = [fields; db.getTableFieldNames('dashboard_ungrouped_new')]; +[dataTable,~] = db.queryDB(fp, fields); + +cfg = struct; +cfg.x_axis = 'accumulated_dispersion'; % 'symbolrate' | 'baudrate' | 'bitrate' | 'wavelength' +cfg.y_axis = 'BER'; % 'BER' | 'GMI' | 'AIR' | ... +cfg.group_by = {'equalizer_structure','pre_emph'}; +cfg.filters = struct('is_mpi',0,'pam_level',M,'equalizer_structure',[equalizer_structure.vnle,equalizer_structure.ffe,equalizer_structure.vnle_pf_mlse,equalizer_structure.vnle_db_mlse,equalizer_structure.dfe]); + +cfg.y_scale = 'auto'; % auto -> log for BER*, linear otherwise +cfg.outlier = 'mad'; % simple, robust; 'none' or 'pctl' also available +cfg.show_raw = true; +cfg.show_spread = 'none'; % 'none' or 'iqr' +cfg.agg = 'mean'; % or 'median' +cfg.show_precoded = 0; +cfg.fec_lines = [2.2e-4 4.85e-3 2e-2]; % optional + +% New styling knobs +cfg.plot.use_cbrewer2 = true; +cfg.plot.colormap = 'Paired'; +cfg.plot.paired_dark_first = false; % dark for lines, light for scatter +cfg.plot.lineWidth = 2.0; +cfg.plot.errWidth = 1.2; +cfg.plot.scatterAlpha = 0.35; +cfg.plot.legendLocation = 'best'; +cfg.plot.fecLineWidth = 2.4; % thicker FEC limits + + +plot_measurements_gpt(dataTable, cfg); \ No newline at end of file