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