Build out MATLAB test framework and core integration coverage

This commit is contained in:
Silas Oettinghaus
2026-03-24 17:23:02 +01:00
parent 9446dfb888
commit a592ebefda
154 changed files with 2281 additions and 1305 deletions

View File

@@ -0,0 +1,321 @@
classdef IMDD_base_system_minimal_integration_test < IMDDTestCase
% First integration test for the reduced IM/DD base-system workflow.
%
% The goal of this test is not to freeze the full waveform exactly.
% Instead, it checks stable system-level contracts:
% - the reduced end-to-end chain runs without errors
% - key signal objects have the expected lengths and sampling rates
% - the DSP outputs return finite, bounded metrics
% - MLSE does not regress relative to the preceding FFE stage
%
% Thresholds are intentionally provisional in this first iteration.
% Once the team has reviewed repeated runs, these can be tightened.
properties
workflow
end
methods (TestClassSetup)
function runReducedImddWorkflowOnce(testCase)
% Run the deterministic reduced workflow once and share the
% resulting signals/metrics across all test methods.
testCase.workflow = runReducedWorkflow();
end
end
methods (Test, TestTags = {'integration', 'slow', 'imdd'})
function reducedWorkflowBuildsExpectedSignalStages(testCase)
wf = testCase.workflow;
testCase.verifyClass(wf.Tx_bits, 'Informationsignal');
testCase.verifyClass(wf.Symbols, 'Informationsignal');
testCase.verifyClass(wf.Digi_sig, 'Informationsignal');
testCase.verifyClass(wf.El_sig, 'Electricalsignal');
testCase.verifyClass(wf.Opt_sig, 'Opticalsignal');
testCase.verifyClass(wf.Rx_sig_after_pd, 'Electricalsignal');
testCase.verifyClass(wf.Scpe_sig, 'Informationsignal');
testCase.verifyClass(wf.Synced_sig, 'Informationsignal');
testCase.verifyGreaterThan(length(wf.Tx_bits.signal), 0);
testCase.verifyGreaterThan(length(wf.Symbols.signal), 0);
testCase.verifyEqual(wf.Symbols.fs, wf.params.fsym);
% After the matched filter, the signal is intentionally reduced
% to 2 samples per symbol for the downstream DSP chain.
testCase.verifyEqual(wf.Scpe_sig.fs, 2 * wf.params.fsym);
testCase.verifyEqual(wf.Synced_sig.fs, 2 * wf.params.fsym);
% The synchronized signal is explicitly cropped to 2 samples per
% transmitted symbol before equalization.
testCase.verifyEqual(length(wf.Synced_sig.signal), 2 * length(wf.Symbols.signal));
end
function reducedWorkflowProducesFiniteSignalsAndMetrics(testCase)
wf = testCase.workflow;
finiteSignals = {
wf.Digi_sig.signal
wf.El_sig.signal
wf.Opt_sig.signal
wf.Rx_sig_after_pd.signal
wf.Scpe_sig.signal
wf.Synced_sig.signal
};
for idx = 1:numel(finiteSignals)
sig = finiteSignals{idx};
testCase.verifyFalse(any(isnan(sig), 'all'));
testCase.verifyFalse(any(isinf(sig), 'all'));
end
testCase.verifyGreaterThanOrEqual(wf.ffe_results.metrics.BER, 0);
testCase.verifyLessThanOrEqual(wf.ffe_results.metrics.BER, 1);
testCase.verifyGreaterThanOrEqual(wf.mlse_results.metrics.BER, 0);
testCase.verifyLessThanOrEqual(wf.mlse_results.metrics.BER, 1);
testCase.verifyTrue(isfinite(wf.ffe_results.metrics.GMI));
testCase.verifyTrue(isfinite(wf.ffe_results.metrics.AIR));
testCase.verifyTrue(isfinite(wf.mlse_results.metrics.GMI));
testCase.verifyTrue(isfinite(wf.mlse_results.metrics.AIR));
end
function reducedWorkflowMeetsProvisionalPerformanceChecks(testCase)
wf = testCase.workflow;
% These are deliberately loose first-pass regression guards.
% Tighten them only after repeated baseline runs are reviewed.
% First observed baseline on 2026-03-24 for this reduced setup:
% FFE BER ~= 2.58e-1
%
% This threshold is intentionally loose for the first
% integration-test iteration. Tighten it only after the reduced
% workflow has been reviewed across repeated runs and code
% changes.
provisionalMaxFfeBer = 3e-1;
provisionalMaxMlseBer = 3e-1;
testCase.verifyLessThanOrEqual(wf.ffe_results.metrics.BER, provisionalMaxFfeBer);
testCase.verifyLessThanOrEqual(wf.mlse_results.metrics.BER, provisionalMaxMlseBer);
% MLSE should not regress relative to the direct FFE path on
% this deterministic reduced setup.
testCase.verifyLessThanOrEqual( ...
wf.mlse_results.metrics.BER, ...
wf.ffe_results.metrics.BER + 1e-12);
end
end
end
function workflow = runReducedWorkflow()
params = reducedWorkflowParameters();
% -------------------- TX --------------------
txPulse = Pulseformer( ...
"fsym", params.fsym, ...
"fdac", params.fdac, ...
"pulse", "rrc", ...
"pulselength", 12, ...
"alpha", params.rcalpha);
[digiSig, symbols, txBits] = PAMsource( ...
"fsym", params.fsym, ...
"M", params.M, ...
"order", params.sourceOrder, ...
"useprbs", false, ...
"fs_out", params.fdac, ...
"applyclipping", false, ...
"applypulseform", true, ...
"pulseformer", txPulse, ...
"randkey", params.randomKey, ...
"duobinary_mode", db_mode.no_db, ...
"mrds_code", 0).process();
elSig = AWG( ...
"fdac", params.fdac, ...
"f_cutoff", params.fsym, ...
"lpf_active", false, ...
"kover", params.kover, ...
"bit_resolution", 8, ...
"upsampling_method", "samplehold", ...
"precomp_sinc_rolloff", 1).process(digiSig);
elSig = elSig.normalize("mode", "oneone");
elSig = elSig .* params.driverScaling;
% -------------------- Optical Channel --------------------
optSig = EML( ...
"mode", eml_mode.im_cosinus, ...
"power", params.opticalPowerDbm, ...
"fsimu", elSig.fs, ...
"lambda", params.laserWavelengthNm, ...
"bias", params.vbias, ...
"u_pi", params.uPi, ...
"linewidth", 0, ...
"randomkey", params.randomKey + 1, ...
"alpha", 0).process(elSig);
optSig = Fiber( ...
"fsimu", optSig.fs, ...
"fiber_length", params.linkLengthKm, ...
"alpha", params.fiberAlphaDbPerKm, ...
"D", 0, ...
"lambda0", 1310, ...
"gamma", 0, ...
"Dslope", 0.07).process(optSig);
rxOptSig = Amplifier( ...
"amp_mode", "ideal_no_noise", ...
"gain_mode", "output_power", ...
"amplification_db", params.ropDbm).process(optSig);
rxSigAfterPd = Photodiode( ...
"fsimu", params.fdac * params.kover, ...
"dark_current", 0, ...
"responsivity", 1, ...
"temperature", 20, ...
"nep", 0, ...
"randomkey", params.randomKey + 2).process(rxOptSig);
rxSigFiltered = Filter( ...
"filtdegree", 4, ...
"f_cutoff", params.rxElectricalBandwidthHz, ...
"fs", params.fdac * params.kover, ...
"filterType", filtertypes.butterworth, ...
"active", true).process(rxSigAfterPd);
scopeLpf = Filter( ...
"filtdegree", 4, ...
"f_cutoff", params.scopeBandwidthHz, ...
"fs", params.fadc, ...
"filterType", filtertypes.butterworth, ...
"active", true);
scpeSig = Scope( ...
"fsimu", params.fdac * params.kover, ...
"fadc", params.fadc, ...
"delay", 0, ...
"fixed_delay", 0, ...
"filtertype", filtertypes.butterworth, ...
"samplingdelay", 0, ...
"rand_samplingdelay", 0, ...
"freq_offset", 0, ...
"samp_jitter", 0, ...
"adcresolution", 8, ...
"quantbuffer", 0.1, ...
"block_dc", 1, ...
"lpf_active", 1, ...
"H_lpf", scopeLpf).process(rxSigFiltered);
rxMatchedFilter = Pulseformer( ...
"fsym", params.fsym, ...
"fdac", 2 * params.fsym, ...
"pulse", "rrc", ...
"pulselength", 12, ...
"alpha", params.rcalpha, ...
"matched", 1);
scpeSig = rxMatchedFilter.process(scpeSig);
[syncedSig, ~] = scpeSig.tsynch("reference", symbols, "fs_ref", params.fsym, "debug_plots", 0);
syncedSig = syncedSig - mean(syncedSig.signal);
syncedSig.signal = syncedSig.signal(1 : 2 * length(symbols));
% -------------------- DSP --------------------
ffeEq = FFE( ...
"epochs_tr", 2, ...
"epochs_dd", 1, ...
"len_tr", params.lenTr, ...
"mu_dd", 1e-4, ...
"mu_tr", 1e-2, ...
"order", 21, ...
"sps", 2, ...
"decide", 0, ...
"adaption_technique", adaption_method.nlms, ...
"dd_mode", 1);
ffeResults = ffe( ...
ffeEq, ...
params.M, ...
syncedSig, ...
symbols, ...
txBits, ...
"precode_mode", db_mode.no_db, ...
"showAnalysis", 0, ...
"postFFE", [], ...
"eth_style_symbol_mapping", 0);
mlseEq = FFE( ...
"epochs_tr", 2, ...
"epochs_dd", 1, ...
"len_tr", params.lenTr, ...
"mu_dd", 1e-4, ...
"mu_tr", 1e-2, ...
"order", 21, ...
"sps", 2, ...
"decide", 0, ...
"adaption_technique", adaption_method.nlms, ...
"dd_mode", 1);
postfilter = Postfilter("ncoeff", 1, "useBurg", 1);
mlse = MLSE( ...
"duobinary_output", 0, ...
"M", params.M, ...
"trellis_states", PAMmapper(params.M, 0).levels);
[vnleResults, mlseResults] = vnle_postfilter_mlse( ...
mlseEq, ...
postfilter, ...
mlse, ...
params.M, ...
syncedSig, ...
symbols, ...
txBits, ...
"precode_mode", db_mode.no_db, ...
"showAnalysis", 0, ...
"postFFE", [], ...
"eth_style_symbol_mapping", 0);
workflow = struct();
workflow.params = params;
workflow.Digi_sig = digiSig;
workflow.Symbols = symbols;
workflow.Tx_bits = txBits;
workflow.El_sig = elSig;
workflow.Opt_sig = optSig;
workflow.Rx_sig_after_pd = rxSigAfterPd;
workflow.Scpe_sig = scpeSig;
workflow.Synced_sig = syncedSig;
workflow.ffe_results = ffeResults;
workflow.vnle_results = vnleResults;
workflow.mlse_results = mlseResults;
end
function params = reducedWorkflowParameters()
params = struct();
% Smaller, deterministic version of the IM/DD base workflow.
params.M = 4;
params.fsym = 16e9;
params.fdac = 64e9;
params.fadc = 64e9;
params.kover = 2;
params.randomKey = 1;
params.sourceOrder = 12;
params.rcalpha = 0.05;
params.lenTr = 256;
% Driver / modulator operating point.
params.uPi = 3;
params.vbiasRel = 0.5;
params.vbias = -params.vbiasRel * params.uPi;
params.driverScaling = 0.6 * (params.uPi / 2 - abs(params.vbias - params.uPi / 2));
% Optical path.
params.laserWavelengthNm = 1293;
params.opticalPowerDbm = 3;
params.linkLengthKm = 1;
params.fiberAlphaDbPerKm = 0.3;
params.ropDbm = 0;
% Receiver filtering.
params.rxElectricalBandwidthHz = 40e9;
params.scopeBandwidthHz = 25e9;
end