Merge branch 'main' of cau-git:nt/mitarbeiter/silas/imdd_simulation

This commit is contained in:
2026-03-25 09:45:49 +01:00
29 changed files with 2587 additions and 622 deletions

6
.gitignore vendored
View File

@@ -22,4 +22,8 @@ sccprj/
codegen/ codegen/
.mat .mat
# Local test dashboard status tracking
Tests/.last_test_run.json
Tests/reports/

View File

@@ -46,7 +46,7 @@ classdef Optical_Demultiplex < handle
if isempty(obj.wavelengthplan) if isempty(obj.wavelengthplan)
obj.wavelengthplan = signalclass_in.lambda; %meter obj.wavelengthplan = signalclass_in.lambda; %meter
else else
if all(500e-9 < obj.wavelengthplan) && all(obj.wavelengthplan < 1500e-9) %check if given in nm if all(500 < obj.wavelengthplan) && all(obj.wavelengthplan < 1500) %check if given in nm
obj.wavelengthplan = obj.wavelengthplan.*1e-9; obj.wavelengthplan = obj.wavelengthplan.*1e-9;
end end
end end

View File

@@ -1,120 +0,0 @@
function loss_curve_digitized(filename)
% PLOT_WPD_DATASETS Reads and plots scattering data from a CSV file.
% filename: String containing the path to the CSV file (e.g., 'wpd_datasets.csv')
%
% This function expects a CSV with the following structure:
% Row 1: Dataset Names (every 2nd column)
% Row 2: Variable Names (X, Y, X, Y...)
% Row 3+: Numeric Data (potentially with NaNs for unequal lengths)
% --- 1. Import Data ---
if nargin < 1
filename = 'wpd_datasets.csv'; % Default filename
end
% Read the numeric data, skipping the first 2 header lines
% 'TreatAsMissing' ensures empty cells become NaNs
raw_data = readmatrix(filename, 'NumHeaderLines', 2);
% Read the first line separately to parse Dataset Names
fid = fopen(filename, 'r');
if fid == -1
error('Could not open file: %s', filename);
end
header_line = fgetl(fid);
fclose(fid);
% Split header by comma to get names
raw_names = split(header_line, ',');
% Extract non-empty names (assuming names are in col 1, 3, 5...)
dataset_names = raw_names(~cellfun('isempty', raw_names));
% --- 2. Setup Plot ---
figure('Color', 'w', 'Position', [100, 100, 800, 600]);
ax = gca;
hold(ax, 'on');
line_styles = {'-', '-', '-', '-'};
% --- 3. Iterate and Plot Each Dataset ---
num_datasets = length(dataset_names);
for i = 1:num_datasets
% Calculate column indices for X and Y
% Dataset 1: Cols 1,2 | Dataset 2: Cols 3,4 | etc.
col_x = (i-1)*2 + 1;
col_y = (i-1)*2 + 2;
% Extract data
if col_y > size(raw_data, 2)
warning('Data columns missing for dataset %d', i);
break;
end
X = raw_data(:, col_x);
Y = raw_data(:, col_y);
% Remove NaNs (missing data due to unequal lengths)
valid_mask = ~isnan(X) & ~isnan(Y);
X = X(valid_mask);
Y = Y(valid_mask);
[X, sortIdx] = sort(X);
Y = Y(sortIdx);
% 3. Smooth the Data
if numel(X) > 20
if 1
Y = smoothdata(Y, 'sgolay', 15);
else
Y = movmean(Y, 15);
end
end
% Plotting
% Using semilogy because scattering data often spans orders of magnitude
% (Adjust to 'plot' if linear scale is preferred)
p = plot(ax, X, Y, ...
'LineStyle', line_styles{mod(i-1, length(line_styles)) + 1}, ...
'Marker', 'none', ...
'Color', 'black', ...
'LineWidth', 1.5, ...
'MarkerSize', 6, ...
'DisplayName', dataset_names{i});
% Optional: Fill marker faces for better visibility
% p.MarkerFaceColor = p.Color;
% p.MarkerFaceAlpha = 0.3; % Semi-transparent fill
end
% --- 4. Styling and Formatting ---
% Axis Labels (Inferred from typical scattering plots)
xlabel(ax, 'Wavelength (\mu m)', 'FontSize', 12, 'FontWeight', 'bold');
ylabel(ax, 'Intensity / Cross-Section (a.u.)', 'FontSize', 12, 'FontWeight', 'bold');
% Title
title(ax, 'Dataset Comparison', 'FontSize', 14);
% Legend
legend(ax, 'Location', 'best', 'Interpreter', 'none', 'Box', 'on');
% Grid
grid(ax, 'on');
ax.GridAlpha = 0.3;
ax.MinorGridAlpha = 0.1;
% Set Log Scale for Y (likely required for this data type)
set(ax, 'YScale', 'log');
% Enhance axis appearance
set(ax, 'Box', 'on', 'LineWidth', 1.2, 'FontSize', 10);
hold(ax, 'off');
end

View File

@@ -1,189 +0,0 @@
Rayleigh,,Experimental,,Infrared Absorption,
X,Y,X,Y,X,Y
0.7066005680911753,3.651009696525016,0.7072188355785799,4.848577786727532,1.4943918372804705,0.009324755400827079
0.7362740977034739,3.1008424465551374,0.7104385926007812,5.059236053617898,1.5870823136443164,0.04795688074913024
0.7704634954089999,2.559836201011587,0.7143024334187478,5.3165954733738054,1.6443739975494367,0.12666181795273013
0.8246453070916075,1.9963763545469397,0.7194723146571098,4.679249634272495,1.6900695253042852,0.3029389206853443
0.8846384359818439,1.4197559292837703,0.7194729766137344,4.646181068667172,1.7344762235109785,0.7194296259968607
0.9375334039861705,1.0762856922437043,0.7227053108118038,4.236869693191961,1.7975573800551148,2.1896107578891244
0.9929980876073115,0.9010620061822204,0.7272205169484065,4.147543538340177,,
1.0420225952274251,0.6977835411732736,0.7291487965959542,4.420849736815974,,
1.0845923638007697,0.5842349714248559,0.7310784001567514,4.645798669234298,,
1.129741777340298,0.4856980392071641,0.7355902965102309,4.71201487210429,,
1.1981066716622326,0.3841608204561696,0.738821306795051,4.358286713153517,,
1.24841405122088,0.31935666463679646,0.7414069093708566,4.059822180897163,,
1.289688370679823,0.2850148847474179,0.742705668268381,3.676053825899206,,
1.3541801567910463,0.23691184007300667,0.7440031032526563,3.376112267508452,,
1.4083573347772815,0.19416797258075097,0.7452965664971838,3.235432943532804,,
1.4618851333147547,0.16723647350379714,0.7459426361628229,3.1898499103097238,,
1.50315548103395,0.15574103754742674,0.7549743723492774,3.0137121806723157,,
1.5386251028515106,0.1419883611511702,0.7620678995388148,2.971117042589562,,
1.596665459699088,0.12316075278891304,0.765291628300764,2.971049114206588,,
1.6437444767994136,0.10759838849626362,0.7756075603390012,2.9708317538173317,,
1.683725994970454,0.09949510555249974,0.7762523060913911,2.9708181693210105,,
1.717258069747722,0.09398483395032568,0.7839919029465676,2.8875658983600925,,
1.7636903552257834,0.08387517688001396,0.7885090949530442,2.767180560538318,,
1.7940013490676008,0.07701567091052414,0.7917348095848672,2.7088647150205,,
,,0.7936723566251598,2.6144535948583694,,
,,0.7962546494178423,2.5233214143921474,,
,,0.8085087904529968,2.417989089048743,,
,,0.8104423657535417,2.435165393892523,,
,,0.8201175237791369,2.3335556930105406,,
,,0.8285065000830756,2.158298098247211,,
,,0.8317335386281479,2.083056634772393,,
,,0.8356046609689853,2.024737963659226,,
,,0.8407639509013534,1.996148150112612,,
,,0.8459232408337212,1.967962031984024,,
,,0.854951005280428,1.9401206794865944,,
,,0.8601116191260452,1.8857864945455538,,
,,0.8691446792257491,1.7565635929668848,,
,,0.8743052930713663,1.7073700288086895,,
,,0.8788211611645935,1.6595617469926904,,
,,0.8826916215488064,1.624580544751916,,
,,0.8891397410293294,1.6130257674984603,,
,,0.896234592132116,1.5678305544227285,,
,,0.903974850943917,1.5131252291764425,,
,,0.90784531132813,1.4812307005435945,,
,,0.9155829223134326,1.470682044636039,,
,,0.9220303798373308,1.4706147972609174,,
,,0.9310574823274131,1.460128389693264,,
,,0.9349259568417521,1.4600883304432553,,
,,0.9433056657529459,1.4913980336424177,,
,,0.9516853746641398,1.5233791328756299,,
,,0.956844002639883,1.512557980118849,,
,,0.9671632444612435,1.4597545460975296,,
,,0.9710370146285795,1.379199997932108,,
,,0.9736206313345113,1.3123773159521164,,
,,0.9774937395452226,1.2487807938448074,,
,,0.9826543533908398,1.213807976266501,,
,,0.9903926263327669,1.1966468122906808,,
,,0.9942630867169798,1.171423198750918,,
,,1.0045796807118417,1.1630595787371243,,
,,1.0097376467309604,1.163017033545686,,
,,1.0219851681998686,1.1963787227370668,,
,,1.0290753856062829,1.2220446908209617,,
,,1.0348774354211667,1.2306917801168409,,
,,1.0471421677623152,1.052809309517664,,
,,1.0510159379296513,0.9947114748787712,,
,,1.0445565651865096,1.1302083245776995,,
,,1.0458487045177878,1.0985863367418764,,
,,1.0542449623445975,0.9398239868494617,,
,,1.0613444471437565,0.8692481092753317,,
,,1.0690886776953055,0.8039684355922487,,
,,1.0768322462902296,0.7488836107091129,,
,,1.0800586228786773,0.7279206840193796,,
,,1.0858633205200596,0.7125673758820761,,
,,1.1013418522737881,0.677981252075927,,
,,1.105210326788127,0.6779626513688453,,
,,1.1193973811672016,0.6589337368401503,,
,,1.1206888585418553,0.6450561487842132,,
,,1.123272475247787,0.613802971613336,,
,,1.129077834845794,0.5966103416145675,,
,,1.1335910551125228,0.5965912453535225,,
,,1.1413266802279516,0.6050805792103379,,
,,1.147134687652457,0.5716821968068484,,
,,1.1581006610960811,0.5401075330241505,,
,,1.167774495208427,0.524964710284852,,
,,1.17357786893656,0.521233298948351,,
,,1.1890511050372914,0.5248855006959243,,
,,1.1974327998183592,0.5248543002000403,,
,,1.2051671010205385,0.5399272739697006,,
,,1.2161264548979165,0.5475977747039503,,
,,1.2225739124218147,0.5475727356323816,,
,,1.2232186581742046,0.5475702317881956,,
,,1.2257923455307669,0.5795255128326332,,
,,1.2264284858470365,0.635494312878344,,
,,1.2264218662807902,0.6822012488404363,,
,,1.2322133247896798,0.7695840678419278,,
,,1.2354304339853828,0.8261273126225834,,
,,1.2386382757883407,0.9793976812064772,,
,,1.2399224716401234,1.0365631680970249,,
,,1.2405632456527655,1.0816189618908738,,
,,1.2457119442791393,1.1944819289049535,,
,,1.2508619668187624,1.3005429730851226,,
,,1.2521488104970433,1.3379536598639075,,
,,1.2586068593269357,1.1943726953174758,,
,,1.2605516878900995,1.0662340854711274,,
,,1.263782036218295,0.9932116176633944,,
,,1.267659116168754,0.9057092045091463,,
,,1.2721796179583538,0.8377104911187728,,
,,1.277345527456968,0.7693377754021644,,
,,1.2818653672899432,0.7166420946785479,,
,,1.2954156193961235,0.639704291832398,,
,,1.298640672071322,0.6306801565443763,,
,,1.3018657247465204,0.6217833222901857,,
,,1.3147579919678183,0.6396165438362177,,
,,1.3179764250767705,0.6769403979817149,,
,,1.3244132912946747,0.7582491296621654,,
,,1.3295613279644238,0.8433295073010272,,
,,1.336000180052202,0.9247376324361347,,
,,1.3430738485430005,1.1278180580973283,,
,,1.3449988184074253,1.2455302076050567,,
,,1.3482112939067554,1.4050952527481475,,
,,1.351423769406086,1.585102197634859,,
,,1.3533487392705106,1.7505418140103943,,
,,1.3565618767264658,1.9608478952847546,,
,,1.3604177740649368,2.243642151072621,,
,,1.3681421459177467,2.5671506730013776,,
,,1.3720059867357133,2.6977396395227498,,
,,1.3700631440424236,2.9583331848749403,,
,,1.3745670969164077,3.267039328784058,,
,,1.3758446732019438,3.711862721055264,,
,,1.377124897313979,4.099294432098773,,
,,1.381620906708467,4.929210899363566,,
,,1.381610315402473,5.521521237053952,,
,,1.382885243861511,6.453816707451638,,
,,1.3828733286422676,7.332602493027707,,
,,1.3835048352621646,8.450022001983974,,
,,1.3847837354609505,9.465318121186383,,
,,1.3892850405084358,10.753821633797266,,
,,1.3950817946703227,11.46214012981349,,
,,1.406041810504325,11.542823048802346,,
,,1.4086254272102567,10.983569572223233,,
,,1.4125144223799593,8.815541223987582,,
,,1.4138145051907332,7.8697993951128655,,
,,1.4164106990725327,6.544455667464655,,
,,1.4170686839574151,5.678973743658009,,
,,1.419664877839215,4.722584406045411,,
,,1.422267691287261,3.6583795017467264,,
,,1.4274475018749921,2.894876829955794,,
,,1.4293982880477776,2.4244991059437035,,
,,1.4319891862765801,2.133892660567146,,
,,1.4365249130685465,1.6766244945281037,,
,,1.442346821582169,1.3648832144532748,,
,,1.44946086942707,1.0800176935856405,,
,,1.4539886527395407,0.9239641429706197,,
,,1.4617467843802068,0.7363233849854021,,
,,1.4701536335130105,0.5623407795094862,,
,,1.477900511891058,0.5055620724926202,,
,,1.4856487141823544,0.44811473438788657,,
,,1.4940482817922873,0.3699994982234866,,
,,1.5005089784486785,0.32105511462339426,,
,,1.5089019264923649,0.2845721176091456,,
,,1.5166481429137875,0.2576601983353233,,
,,1.5179396202884412,0.2522337011156551,,
,,1.5359997828782272,0.23327413205434572,,
,,1.5430966198508878,0.22196482072259285,,
,,1.5469697280615993,0.21120862244291763,,
,,1.5501980905199209,0.20097457800069773,,
,,1.565674636403775,0.1953318790229525,,
,,1.5688977032090996,0.19671762962317987,,
,,1.5785662416684487,0.20236426842871638,,
,,1.5837215598610688,0.2081796533603784,,
,,1.5914545371499988,0.21721757235346942,,
,,1.5991901622654274,0.22030852031094905,,
,,1.6088620105078997,0.21873658192521878,,
,,1.61208309144335,0.22502554836218797,,
,,1.6262602164730557,0.2432590831170723,,
,,1.626903638312196,0.2467330049135315,,
,,1.6281904819904773,0.25383038758817866,,
,,1.6410748057322797,0.28430548752659124,,
,,1.660414530477476,0.29244621369149987,,
,,1.6642816810785659,0.29661578287603696,,
,,1.6707152375133467,0.34423590359793155,,
,,1.6739290369259265,0.3828666146961718,,
,,1.6758540067903511,0.4228270071256609,,
,,1.690028483993558,0.4702407837690221,,
,,1.6964732936909575,0.48374976893430716,,
,,1.6996923887565347,0.5083600695634991,,
,,1.7003318388559272,0.5380344919079171,,
1 Rayleigh Experimental Infrared Absorption
2 X Y X Y X Y
3 0.7066005680911753 3.651009696525016 0.7072188355785799 4.848577786727532 1.4943918372804705 0.009324755400827079
4 0.7362740977034739 3.1008424465551374 0.7104385926007812 5.059236053617898 1.5870823136443164 0.04795688074913024
5 0.7704634954089999 2.559836201011587 0.7143024334187478 5.3165954733738054 1.6443739975494367 0.12666181795273013
6 0.8246453070916075 1.9963763545469397 0.7194723146571098 4.679249634272495 1.6900695253042852 0.3029389206853443
7 0.8846384359818439 1.4197559292837703 0.7194729766137344 4.646181068667172 1.7344762235109785 0.7194296259968607
8 0.9375334039861705 1.0762856922437043 0.7227053108118038 4.236869693191961 1.7975573800551148 2.1896107578891244
9 0.9929980876073115 0.9010620061822204 0.7272205169484065 4.147543538340177
10 1.0420225952274251 0.6977835411732736 0.7291487965959542 4.420849736815974
11 1.0845923638007697 0.5842349714248559 0.7310784001567514 4.645798669234298
12 1.129741777340298 0.4856980392071641 0.7355902965102309 4.71201487210429
13 1.1981066716622326 0.3841608204561696 0.738821306795051 4.358286713153517
14 1.24841405122088 0.31935666463679646 0.7414069093708566 4.059822180897163
15 1.289688370679823 0.2850148847474179 0.742705668268381 3.676053825899206
16 1.3541801567910463 0.23691184007300667 0.7440031032526563 3.376112267508452
17 1.4083573347772815 0.19416797258075097 0.7452965664971838 3.235432943532804
18 1.4618851333147547 0.16723647350379714 0.7459426361628229 3.1898499103097238
19 1.50315548103395 0.15574103754742674 0.7549743723492774 3.0137121806723157
20 1.5386251028515106 0.1419883611511702 0.7620678995388148 2.971117042589562
21 1.596665459699088 0.12316075278891304 0.765291628300764 2.971049114206588
22 1.6437444767994136 0.10759838849626362 0.7756075603390012 2.9708317538173317
23 1.683725994970454 0.09949510555249974 0.7762523060913911 2.9708181693210105
24 1.717258069747722 0.09398483395032568 0.7839919029465676 2.8875658983600925
25 1.7636903552257834 0.08387517688001396 0.7885090949530442 2.767180560538318
26 1.7940013490676008 0.07701567091052414 0.7917348095848672 2.7088647150205
27 0.7936723566251598 2.6144535948583694
28 0.7962546494178423 2.5233214143921474
29 0.8085087904529968 2.417989089048743
30 0.8104423657535417 2.435165393892523
31 0.8201175237791369 2.3335556930105406
32 0.8285065000830756 2.158298098247211
33 0.8317335386281479 2.083056634772393
34 0.8356046609689853 2.024737963659226
35 0.8407639509013534 1.996148150112612
36 0.8459232408337212 1.967962031984024
37 0.854951005280428 1.9401206794865944
38 0.8601116191260452 1.8857864945455538
39 0.8691446792257491 1.7565635929668848
40 0.8743052930713663 1.7073700288086895
41 0.8788211611645935 1.6595617469926904
42 0.8826916215488064 1.624580544751916
43 0.8891397410293294 1.6130257674984603
44 0.896234592132116 1.5678305544227285
45 0.903974850943917 1.5131252291764425
46 0.90784531132813 1.4812307005435945
47 0.9155829223134326 1.470682044636039
48 0.9220303798373308 1.4706147972609174
49 0.9310574823274131 1.460128389693264
50 0.9349259568417521 1.4600883304432553
51 0.9433056657529459 1.4913980336424177
52 0.9516853746641398 1.5233791328756299
53 0.956844002639883 1.512557980118849
54 0.9671632444612435 1.4597545460975296
55 0.9710370146285795 1.379199997932108
56 0.9736206313345113 1.3123773159521164
57 0.9774937395452226 1.2487807938448074
58 0.9826543533908398 1.213807976266501
59 0.9903926263327669 1.1966468122906808
60 0.9942630867169798 1.171423198750918
61 1.0045796807118417 1.1630595787371243
62 1.0097376467309604 1.163017033545686
63 1.0219851681998686 1.1963787227370668
64 1.0290753856062829 1.2220446908209617
65 1.0348774354211667 1.2306917801168409
66 1.0471421677623152 1.052809309517664
67 1.0510159379296513 0.9947114748787712
68 1.0445565651865096 1.1302083245776995
69 1.0458487045177878 1.0985863367418764
70 1.0542449623445975 0.9398239868494617
71 1.0613444471437565 0.8692481092753317
72 1.0690886776953055 0.8039684355922487
73 1.0768322462902296 0.7488836107091129
74 1.0800586228786773 0.7279206840193796
75 1.0858633205200596 0.7125673758820761
76 1.1013418522737881 0.677981252075927
77 1.105210326788127 0.6779626513688453
78 1.1193973811672016 0.6589337368401503
79 1.1206888585418553 0.6450561487842132
80 1.123272475247787 0.613802971613336
81 1.129077834845794 0.5966103416145675
82 1.1335910551125228 0.5965912453535225
83 1.1413266802279516 0.6050805792103379
84 1.147134687652457 0.5716821968068484
85 1.1581006610960811 0.5401075330241505
86 1.167774495208427 0.524964710284852
87 1.17357786893656 0.521233298948351
88 1.1890511050372914 0.5248855006959243
89 1.1974327998183592 0.5248543002000403
90 1.2051671010205385 0.5399272739697006
91 1.2161264548979165 0.5475977747039503
92 1.2225739124218147 0.5475727356323816
93 1.2232186581742046 0.5475702317881956
94 1.2257923455307669 0.5795255128326332
95 1.2264284858470365 0.635494312878344
96 1.2264218662807902 0.6822012488404363
97 1.2322133247896798 0.7695840678419278
98 1.2354304339853828 0.8261273126225834
99 1.2386382757883407 0.9793976812064772
100 1.2399224716401234 1.0365631680970249
101 1.2405632456527655 1.0816189618908738
102 1.2457119442791393 1.1944819289049535
103 1.2508619668187624 1.3005429730851226
104 1.2521488104970433 1.3379536598639075
105 1.2586068593269357 1.1943726953174758
106 1.2605516878900995 1.0662340854711274
107 1.263782036218295 0.9932116176633944
108 1.267659116168754 0.9057092045091463
109 1.2721796179583538 0.8377104911187728
110 1.277345527456968 0.7693377754021644
111 1.2818653672899432 0.7166420946785479
112 1.2954156193961235 0.639704291832398
113 1.298640672071322 0.6306801565443763
114 1.3018657247465204 0.6217833222901857
115 1.3147579919678183 0.6396165438362177
116 1.3179764250767705 0.6769403979817149
117 1.3244132912946747 0.7582491296621654
118 1.3295613279644238 0.8433295073010272
119 1.336000180052202 0.9247376324361347
120 1.3430738485430005 1.1278180580973283
121 1.3449988184074253 1.2455302076050567
122 1.3482112939067554 1.4050952527481475
123 1.351423769406086 1.585102197634859
124 1.3533487392705106 1.7505418140103943
125 1.3565618767264658 1.9608478952847546
126 1.3604177740649368 2.243642151072621
127 1.3681421459177467 2.5671506730013776
128 1.3720059867357133 2.6977396395227498
129 1.3700631440424236 2.9583331848749403
130 1.3745670969164077 3.267039328784058
131 1.3758446732019438 3.711862721055264
132 1.377124897313979 4.099294432098773
133 1.381620906708467 4.929210899363566
134 1.381610315402473 5.521521237053952
135 1.382885243861511 6.453816707451638
136 1.3828733286422676 7.332602493027707
137 1.3835048352621646 8.450022001983974
138 1.3847837354609505 9.465318121186383
139 1.3892850405084358 10.753821633797266
140 1.3950817946703227 11.46214012981349
141 1.406041810504325 11.542823048802346
142 1.4086254272102567 10.983569572223233
143 1.4125144223799593 8.815541223987582
144 1.4138145051907332 7.8697993951128655
145 1.4164106990725327 6.544455667464655
146 1.4170686839574151 5.678973743658009
147 1.419664877839215 4.722584406045411
148 1.422267691287261 3.6583795017467264
149 1.4274475018749921 2.894876829955794
150 1.4293982880477776 2.4244991059437035
151 1.4319891862765801 2.133892660567146
152 1.4365249130685465 1.6766244945281037
153 1.442346821582169 1.3648832144532748
154 1.44946086942707 1.0800176935856405
155 1.4539886527395407 0.9239641429706197
156 1.4617467843802068 0.7363233849854021
157 1.4701536335130105 0.5623407795094862
158 1.477900511891058 0.5055620724926202
159 1.4856487141823544 0.44811473438788657
160 1.4940482817922873 0.3699994982234866
161 1.5005089784486785 0.32105511462339426
162 1.5089019264923649 0.2845721176091456
163 1.5166481429137875 0.2576601983353233
164 1.5179396202884412 0.2522337011156551
165 1.5359997828782272 0.23327413205434572
166 1.5430966198508878 0.22196482072259285
167 1.5469697280615993 0.21120862244291763
168 1.5501980905199209 0.20097457800069773
169 1.565674636403775 0.1953318790229525
170 1.5688977032090996 0.19671762962317987
171 1.5785662416684487 0.20236426842871638
172 1.5837215598610688 0.2081796533603784
173 1.5914545371499988 0.21721757235346942
174 1.5991901622654274 0.22030852031094905
175 1.6088620105078997 0.21873658192521878
176 1.61208309144335 0.22502554836218797
177 1.6262602164730557 0.2432590831170723
178 1.626903638312196 0.2467330049135315
179 1.6281904819904773 0.25383038758817866
180 1.6410748057322797 0.28430548752659124
181 1.660414530477476 0.29244621369149987
182 1.6642816810785659 0.29661578287603696
183 1.6707152375133467 0.34423590359793155
184 1.6739290369259265 0.3828666146961718
185 1.6758540067903511 0.4228270071256609
186 1.690028483993558 0.4702407837690221
187 1.6964732936909575 0.48374976893430716
188 1.6996923887565347 0.5083600695634991
189 1.7003318388559272 0.5380344919079171

View File

@@ -8,8 +8,8 @@ It should reflect the repository state now, not the original plan.
Fast profile status: Fast profile status:
- `run_all_tests('profile','fast')` - `run_all_tests('profile','fast')`
- latest verified result: `Passed: 59 | Failed: 0 | Incomplete: 0` - latest verified result: `Passed: 82 | Failed: 0 | Incomplete: 0`
- explicit placeholder tests remaining: `70` - explicit placeholder tests remaining: `62`
Mandatory workflow for every future test-writing session: Mandatory workflow for every future test-writing session:
@@ -28,10 +28,18 @@ These are already implemented and verified in the fast profile:
- `Classes/00_signals/Signal.m` -> `Tests/00_signals/Signal_test.m` - `Classes/00_signals/Signal.m` -> `Tests/00_signals/Signal_test.m`
- `Classes/00_signals/Informationsignal.m` -> `Tests/00_signals/Informationsignal_test.m` - `Classes/00_signals/Informationsignal.m` -> `Tests/00_signals/Informationsignal_test.m`
- `Classes/00_signals/Electricalsignal.m` -> `Tests/00_signals/Electricalsignal_test.m` - `Classes/00_signals/Electricalsignal.m` -> `Tests/00_signals/Electricalsignal_test.m`
- `Classes/00_signals/Opticalsignal.m` -> `Tests/00_signals/Opticalsignal_test.m`
- `Classes/01_transmit/PAMmapper.m` -> `Tests/01_transmit/PAMmapper_test.m` - `Classes/01_transmit/PAMmapper.m` -> `Tests/01_transmit/PAMmapper_test.m`
- `Classes/01_transmit/PAMsource.m` -> `Tests/01_transmit/PAMsource_test.m` - `Classes/01_transmit/PAMsource.m` -> `Tests/01_transmit/PAMsource_test.m`
- `Classes/01_transmit/Signalgenerator.m` -> `Tests/01_transmit/Signalgenerator_test.m`
- `Classes/01_transmit/Pulseformer.m` -> `Tests/01_transmit/Pulseformer_test.m` - `Classes/01_transmit/Pulseformer.m` -> `Tests/01_transmit/Pulseformer_test.m`
- `Classes/02_etc/Amplifier.m` -> `Tests/02_etc/Amplifier_test.m`
- `Classes/02_etc/Filter.m` -> `Tests/02_etc/Filter_test.m`
- `Classes/02_optical/Fiber.m` -> `Tests/02_optical/Fiber_test.m` - `Classes/02_optical/Fiber.m` -> `Tests/02_optical/Fiber_test.m`
- `Classes/02_optical/EML.m` -> `Tests/02_optical/EML_test.m`
- `Classes/02_optical/Optical_Multiplex.m` -> `Tests/02_optical/Optical_Multiplex_test.m`
- `Classes/02_optical/Optical_Demultiplex.m` -> `Tests/02_optical/Optical_Demultiplex_test.m`
- `Classes/02_optical/DP_Fiber.m` -> `Tests/02_optical/DP_Fiber_test.m`
- `Classes/03_receive/Photodiode.m` -> `Tests/03_receive/Photodiode_test.m` - `Classes/03_receive/Photodiode.m` -> `Tests/03_receive/Photodiode_test.m`
- `Classes/03_receive/Scope.m` -> `Tests/03_receive/Scope_test.m` - `Classes/03_receive/Scope.m` -> `Tests/03_receive/Scope_test.m`
- `Classes/04_DSP/TransmissionPerformance.m` -> `Tests/04_DSP/TransmissionPerformance_test.m` - `Classes/04_DSP/TransmissionPerformance.m` -> `Tests/04_DSP/TransmissionPerformance_test.m`
@@ -41,75 +49,80 @@ These are already implemented and verified in the fast profile:
Notes: Notes:
- `Opticalsignal_test.m` still appears to be a placeholder and should be treated as unfinished.
- There are multiple `Timing_Recovery` class names in the repo, so the implemented base-class test currently uses the disambiguated file name: - There are multiple `Timing_Recovery` class names in the repo, so the implemented base-class test currently uses the disambiguated file name:
`Tests/04_DSP/Timing Recovery/T_04_DSP_Timing_Recovery_Timing_Recovery_test.m` `Tests/04_DSP/Timing Recovery/T_04_DSP_Timing_Recovery_Timing_Recovery_test.m`
## Next Priority Tests ## Integration Tests
Highest-value unfinished tests to implement next: These are the current end-to-end regression guards:
1. `Classes/00_signals/Opticalsignal.m` - `Tests/integration/IMDD_base_system_minimal_integration_test.m`
`Tests/00_signals/Opticalsignal_test.m` - `Tests/integration/IMDD_base_system_no_impairment_integration_test.m`
Focus: - `Tests/integration/IMDD_base_system_impairment_monotonicity_integration_test.m`
- constructor metadata wiring - `Tests/integration/WDM_mux_demux_optical_chain_integration_test.m`
- `delay` - `Tests/integration/WDM_end_to_end_two_channel_receiver_integration_test.m`
- `cspr`
- interaction with inherited signal behavior
2. `Classes/01_transmit/Signalgenerator.m` Current integration focus:
`Tests/01_transmit/Signalgenerator_test.m`
Focus:
- constructor/config defaults
- deterministic generation path
- output type and shape
3. `Classes/02_etc/Filter.m` - refactor the duplicated reduced IM/DD workflow setup into shared integration helpers
`Tests/02_etc/Filter_test.m` - tighten the IM/DD baseline naming and thresholds so the physical intent is honest
Focus: - strengthen the WDM optical-chain and end-to-end receiver fixtures into harder regression guards
- deterministic filter construction - keep all integration fixtures deterministic and small enough to remain reviewable
- `process` behavior on small signals
- output rate/length invariants
4. `Classes/02_etc/Amplifier.m` ## Remaining Core Tests
`Tests/02_etc/Amplifier_test.m`
Focus:
- gain behavior
- noise-free baseline
- output class and metadata behavior
5. `Classes/02_optical/EML.m` Highest-value unfinished unit tests to implement next:
`Tests/02_optical/EML_test.m`
Focus:
- deterministic modulation path
- output class and metadata
- small synthetic fixture only
6. `Classes/02_optical/Optical_Multiplex.m` 1. `Classes/02_optical/Polarization_Controller.m`
`Tests/02_optical/Optical_Multiplex_test.m`
Focus:
- combining channels
- output dimensions
- metadata handling
7. `Classes/02_optical/Optical_Demultiplex.m`
`Tests/02_optical/Optical_Demultiplex_test.m`
Focus:
- deterministic channel extraction
- output dimensions
- basic round-trip expectations with multiplexing if practical
8. `Classes/02_optical/Polarization_Controller.m`
`Tests/02_optical/Polarization_Controller_test.m` `Tests/02_optical/Polarization_Controller_test.m`
Focus: Focus:
- deterministic transform behavior - deterministic transform behavior
- shape preservation - shape preservation
9. `Classes/02_optical/DP_Fiber.m` 2. `Classes/01_transmit/AWG.m`
`Tests/02_optical/DP_Fiber_test.m` `Tests/01_transmit/AWG_test.m`
Focus: Focus:
- very small deterministic sanity checks only - DAC-like resampling and quantization behavior
- avoid long or GPU-heavy runs - length and sampling-rate invariants
- deterministic small fixtures
Follow-up around the WDM integration fixtures:
- `Classes/01_transmit/AWG.m`
- `Classes/02_optical/EML.m`
- `Classes/02_optical/Polarization_Controller.m`
- `Classes/02_optical/DP_Fiber.m`
- `Classes/02_optical/Optical_Multiplex.m`
- `Classes/02_optical/Optical_Demultiplex.m`
- `Classes/03_receive/Photodiode.m`
These should be revisited together when the WDM integration tests are tightened.
Immediate integration refactor backlog:
1. Extract shared reduced IM/DD integration helpers
Targets:
- `Tests/integration/IMDD_base_system_minimal_integration_test.m`
- `Tests/integration/IMDD_base_system_no_impairment_integration_test.m`
- `Tests/integration/IMDD_base_system_impairment_monotonicity_integration_test.m`
Goal:
- remove duplicated reduced-workflow builders
- keep assertions local to each test file
2. Revisit the "no impairment" IM/DD fixture
Target:
- `Tests/integration/IMDD_base_system_no_impairment_integration_test.m`
Goal:
- ensure the name, comments, and thresholds reflect the actual physical regime
3. Harden the WDM optical-chain regression guards
Targets:
- `Tests/integration/WDM_mux_demux_optical_chain_integration_test.m`
- `Tests/integration/WDM_end_to_end_two_channel_receiver_integration_test.m`
Goal:
- improve channel-separation assertions
- add propagated-chain checks where stable
- avoid brittle waveform snapshots
## Next DSP Layer ## Next DSP Layer

View File

@@ -1,10 +1,64 @@
classdef Opticalsignal_test < IMDDTestCase classdef Opticalsignal_test < IMDDTestCase
% Auto-generated placeholder for Opticalsignal. methods (Test, TestTags = {'unit', 'fast', 'signals', 'optical'})
% Target: C:/Users/Silas/Documents/MATLAB/imdd_simulation/Classes/00_signals/Opticalsignal.m function constructorWiresOpticalMetadata(testCase)
opt = makeOpticalsignal([1; 2; 3], 12.5e9, 1310e-9, 0.25, 2);
methods (Test, TestTags = {'placeholder', 'todo'}) testCase.verifyClass(opt, 'Opticalsignal');
function testNotImplemented(testCase) testCase.verifyEqual(opt.signal, [1; 2; 3]);
testCase.assumeFail("Tests for Opticalsignal are not implemented yet."); testCase.verifyEqual(opt.fs, 12.5e9);
testCase.verifyEqual(opt.lambda, 1310e-9);
testCase.verifyEqual(opt.nase, 0.25);
testCase.verifyEqual(opt.polrot, 2);
testCase.verifyEqual(height(opt.logbook), 0);
end
function delayBySamplesAndMetersGiveTheSameResult(testCase)
fs = physconst('LightSpeed') / 1.4677;
opt = makeOpticalsignal([1; 0; -1; 2], fs, 1550e-9, 0, 0);
[bySamples, delayNs] = opt.delay("delay_samples", 2);
[byMeters, delayNm] = opt.delay("delay_meter", 2);
testCase.verifyEqual(delayNs, 2);
testCase.verifyEqual(delayNm, 2);
testCase.verifyEqual(bySamples.signal, byMeters.signal);
testCase.verifyEqual(bySamples.fs, opt.fs);
testCase.verifyClass(bySamples, 'Opticalsignal');
testCase.verifyClass(byMeters, 'Opticalsignal');
end
function csprMatchesKnownCarrierToSidebandRatio(testCase)
opt = makeOpticalsignal([3; 1; 3; 1], 10, 1550e-9, 0, 0);
actual = opt.cspr();
expected = 10 * log10(abs(mean(opt.signal)).^2 / mean(abs(opt.signal - mean(opt.signal)).^2));
testCase.verifyEqual(actual, expected, "AbsTol", 1e-12);
end
function inheritedPlusAndLengthPreserveOpticalSemantics(testCase)
left = makeOpticalsignal([1; 2; 3], 5, 1310e-9, 0.1, 1);
right = makeOpticalsignal([4; 5; 6], 5, 1310e-9, 0.2, 1);
sumSig = left + right;
testCase.verifyClass(sumSig, 'Opticalsignal');
testCase.verifyEqual(sumSig.signal, [5; 7; 9]);
testCase.verifyEqual(sumSig.fs, left.fs);
testCase.verifyEqual(sumSig.lambda, left.lambda);
testCase.verifyEqual(sumSig.nase, left.nase);
testCase.verifyEqual(sumSig.polrot, left.polrot);
testCase.verifyEqual(sumSig.length(), 3);
end end
end end
end end
function opt = makeOpticalsignal(signal, fs, lambda, nase, polrot)
base = Signal(signal);
opt = Opticalsignal(signal, ...
"fs", fs, ...
"logbook", base.logbook, ...
"lambda", lambda, ...
"nase", nase, ...
"polrot", polrot);
end

View File

@@ -1,10 +1,42 @@
classdef Signalgenerator_test < IMDDTestCase classdef Signalgenerator_test < IMDDTestCase
% Auto-generated placeholder for Signalgenerator. methods (Test, TestTags = {'unit', 'fast', 'transmit', 'signalgenerator'})
% Target: C:/Users/Silas/Documents/MATLAB/imdd_simulation/Classes/01_transmit/Signalgenerator.m function constructorStoresDefaultConfiguration(testCase)
gen = Signalgenerator();
methods (Test, TestTags = {'placeholder', 'todo'}) testCase.verifyEqual(gen.form, signalform.sine);
function testNotImplemented(testCase) testCase.verifyEqual(gen.length, 1024);
testCase.assumeFail("Tests for Signalgenerator are not implemented yet."); testCase.verifyEqual(gen.fs, 1000);
testCase.verifyEqual(gen.fsig, 50);
end
function buildSignalProducesExpectedSineWave(testCase)
gen = Signalgenerator( ...
"form", signalform.sine, ...
"length", 4, ...
"fs", 4, ...
"fsig", 1);
signal = gen.build_signal();
expected = [0 1 0 -1];
testCase.verifySize(signal, [1 4]);
testCase.verifyEqual(signal, expected, "AbsTol", 1e-12);
end
function processWrapsGeneratedSignalInInformationsignal(testCase)
gen = Signalgenerator( ...
"form", signalform.sine, ...
"length", 4, ...
"fs", 4, ...
"fsig", 1);
out = gen.process();
testCase.verifyClass(out, 'Informationsignal');
testCase.verifySize(out.signal, [1 4]);
testCase.verifyEqual(out.fs, 4);
testCase.verifyEqual(out.signal, [0 1 0 -1], "AbsTol", 1e-12);
testCase.verifyEqual(height(out.logbook), 1);
end end
end end
end end

View File

@@ -1,10 +1,69 @@
classdef Amplifier_test < IMDDTestCase classdef Amplifier_test < IMDDTestCase
% Auto-generated placeholder for Amplifier. methods (Test, TestTags = {'unit', 'fast', 'etc', 'amplifier'})
% Target: C:/Users/Silas/Documents/MATLAB/imdd_simulation/Classes/02_etc/Amplifier.m function constructorStoresConfiguredModesAndSettings(testCase)
amp = Amplifier( ...
"amp_mode", amp_mode.ideal_no_noise, ...
"gain_mode", gain_mode.gain, ...
"nase_mode", nase_mode.pass_ase, ...
"amplification_db", 7.5, ...
"noifig", 12);
methods (Test, TestTags = {'placeholder', 'todo'}) testCase.verifyEqual(amp.amp_mode, amp_mode.ideal_no_noise);
function testNotImplemented(testCase) testCase.verifyEqual(amp.gain_mode, gain_mode.gain);
testCase.assumeFail("Tests for Amplifier are not implemented yet."); testCase.verifyEqual(amp.nase_mode, nase_mode.pass_ase);
testCase.verifyEqual(amp.amplification_db, 7.5);
testCase.verifyEqual(amp.noifig, 12);
end
function gainModeAmplifiesOpticalSignalAndPreservesMetadata(testCase)
inputSignal = makeOpticalSignal([1; -1; 1; -1], 64e9, 1310e-9, 0, 0);
amp = Amplifier( ...
"amp_mode", amp_mode.ideal_no_noise, ...
"gain_mode", gain_mode.gain, ...
"nase_mode", nase_mode.pass_ase, ...
"amplification_db", 6);
outputSignal = amp.process(inputSignal);
expectedScale = 10^(6/20);
testCase.verifyClass(outputSignal, 'Opticalsignal');
testCase.verifyEqual(outputSignal.signal, expectedScale * inputSignal.signal, "AbsTol", 1e-12);
testCase.verifyEqual(outputSignal.nase, inputSignal.nase * expectedScale^2, "AbsTol", 1e-12);
testCase.verifyEqual(outputSignal.fs, inputSignal.fs);
testCase.verifyEqual(outputSignal.lambda, inputSignal.lambda);
testCase.verifyEqual(outputSignal.polrot, inputSignal.polrot);
testCase.verifyEqual(height(outputSignal.logbook), height(inputSignal.logbook) + 1);
testCase.verifyEqual(string(outputSignal.logbook.Description(end)), "Optical Amplifier ");
end
function outputPowerModeTargetsRequestedAveragePower(testCase)
inputSignal = makeOpticalSignal([1; -1; 1; -1], 64e9, 1310e-9, 0, 0);
amp = Amplifier( ...
"amp_mode", amp_mode.ideal_no_noise, ...
"gain_mode", gain_mode.output_power, ...
"nase_mode", nase_mode.pass_ase, ...
"amplification_db", 33);
outputSignal = amp.process(inputSignal);
expectedPowerW = 10^(33/10 - 3);
expectedScale = sqrt(expectedPowerW / mean(abs(inputSignal.signal).^2));
testCase.verifyClass(outputSignal, 'Opticalsignal');
testCase.verifyEqual(outputSignal.signal, expectedScale * inputSignal.signal, "AbsTol", 1e-12);
testCase.verifyEqual(outputSignal.power("unit", power_notation.W), expectedPowerW, "AbsTol", 1e-12);
testCase.verifyEqual(outputSignal.fs, inputSignal.fs);
testCase.verifyEqual(outputSignal.lambda, inputSignal.lambda);
end end
end end
end end
function sig = makeOpticalSignal(values, fs, lambda, nase, polrot)
sig = Opticalsignal(values, ...
"fs", fs, ...
"logbook", Signal(values, "fs", fs).logbook, ...
"lambda", lambda, ...
"nase", nase, ...
"polrot", polrot);
end

View File

@@ -1,10 +1,71 @@
classdef Filter_test < IMDDTestCase classdef Filter_test < IMDDTestCase
% Auto-generated placeholder for Filter. methods (Test, TestTags = {'unit', 'fast', 'filter', 'etc'})
% Target: C:/Users/Silas/Documents/MATLAB/imdd_simulation/Classes/02_etc/Filter.m function constructorBuildsFilterWhenSamplingRateAndLengthAreKnown(testCase)
flt = Filter( ...
"active", true, ...
"filterType", filtertypes.butterworth, ...
"f_cutoff", 3.5, ...
"fs", 8, ...
"signal_length", 8, ...
"filtdegree", 3, ...
"lowpass", 1);
methods (Test, TestTags = {'placeholder', 'todo'}) testCase.verifyEqual(flt.active, true);
function testNotImplemented(testCase) testCase.verifyEqual(flt.filterType, filtertypes.butterworth);
testCase.assumeFail("Tests for Filter are not implemented yet."); testCase.verifyEqual(flt.f_cutoff, 3.5);
testCase.verifyEqual(flt.fs, 8);
testCase.verifyEqual(flt.signal_length, 8);
testCase.verifyNotEmpty(flt.H);
testCase.verifyNotEmpty(flt.w);
testCase.verifyEqual(numel(flt.H), 8);
testCase.verifyEqual(numel(flt.w), 8);
end
function inactiveFilterIsExactPassThrough(testCase)
flt = Filter( ...
"active", false, ...
"filterType", filtertypes.butterworth, ...
"f_cutoff", 3.5, ...
"fs", 8, ...
"signal_length", 8, ...
"filtdegree", 3);
sig = Signal([1; 2; 3], "fs", 8);
numIn = [1; -2; 3];
sigOut = flt.process(sig);
numOut = flt.process(numIn);
testCase.verifyClass(sigOut, "Signal");
testCase.verifyEqual(sigOut.signal, sig.signal);
testCase.verifyEqual(sigOut.fs, sig.fs);
testCase.verifyEqual(height(sigOut.logbook), height(sig.logbook));
testCase.verifyEqual(numOut, numIn);
end
function activeButterworthFilterPreservesConstantSignals(testCase)
flt = Filter( ...
"active", true, ...
"filterType", filtertypes.butterworth, ...
"f_cutoff", 3.5, ...
"fs", 8, ...
"signal_length", 8, ...
"filtdegree", 3, ...
"lowpass", 1);
sig = Signal(ones(8, 1), "fs", 8);
numIn = ones(8, 1);
sigOut = flt.process(sig);
numOut = flt.process_(numIn);
testCase.verifyEqual(size(sigOut.signal), size(sig.signal));
testCase.verifyEqual(sigOut.signal, ones(8, 1), "AbsTol", 1e-12);
testCase.verifyEqual(sigOut.fs, sig.fs);
testCase.verifyEqual(height(sigOut.logbook), 1);
testCase.verifyEqual(size(numOut), size(numIn));
testCase.verifyEqual(numOut, ones(8, 1), "AbsTol", 1e-12);
end end
end end
end end

View File

@@ -1,10 +1,78 @@
classdef DP_Fiber_test < IMDDTestCase classdef DP_Fiber_test < IMDDTestCase
% Auto-generated placeholder for DP_Fiber. methods (Test, TestTags = {'unit', 'fast', 'optical', 'dp_fiber'})
% Target: C:/Users/Silas/Documents/MATLAB/imdd_simulation/Classes/02_optical/DP_Fiber.m function constructorStoresConfigurationAndStartsWithEmptyState(testCase)
fiber = makeIdentityFiber();
methods (Test, TestTags = {'placeholder', 'todo'}) testCase.verifyEqual(fiber.L, 0);
function testNotImplemented(testCase) testCase.verifyEqual(fiber.dz, 1);
testCase.assumeFail("Tests for DP_Fiber are not implemented yet."); testCase.verifyEqual(fiber.lambda, 1550);
testCase.verifyEqual(fiber.rng, 7);
testCase.verifyEqual(fiber.gamma, 0);
testCase.verifyEqual(fiber.fa, 64e9);
testCase.verifyEqual(fiber.X_alpha, 0);
testCase.verifyEqual(fiber.D, 0);
testCase.verifyEqual(fiber.Ds, 0);
testCase.verifyEqual(fiber.Dpmd, 0);
testCase.verifyEqual(fiber.n_waveplates, 1);
testCase.verifyEqual(fiber.useGPU, false);
testCase.verifyEqual(fiber.useSingle, false);
testCase.verifyTrue(isstruct(fiber.state));
testCase.verifyEmpty(fieldnames(fiber.state));
end
function processPreservesAZeroImpairmentDualPolarizationSignal(testCase)
fiber = makeIdentityFiber();
inputMatrix = [ ...
1 + 1i, 2 - 1i; ...
0.5, -0.25i; ...
-1 + 0.2i, 0.75 - 0.5i; ...
0, 0.25 + 0.75i];
sigIn = Signal(inputMatrix, "fs", fiber.fa);
sigOut = fiber.process(sigIn);
testCase.verifyClass(sigOut, "Signal");
testCase.verifyEqual(sigOut.fs, sigIn.fs);
testCase.verifyEqual(size(sigOut.signal), size(sigIn.signal));
testCase.verifyEqual(sigOut.signal, sigIn.signal, "AbsTol", 1e-12);
testCase.verifyEqual(height(sigOut.logbook), 1);
end
function process_ProducesIdentityOutputForRawMatrixInput(testCase)
fiber = makeIdentityFiber();
inputMatrix = [ ...
1 + 1i, 2 - 1i; ...
0.5, -0.25i; ...
-1 + 0.2i, 0.75 - 0.5i; ...
0, 0.25 + 0.75i];
sigOut = fiber.process_(inputMatrix, fiber.fa);
testCase.verifyEqual(size(sigOut), size(inputMatrix));
testCase.verifyEqual(sigOut, inputMatrix, "AbsTol", 1e-12);
end end
end end
end end
function fiber = makeIdentityFiber()
fiber = DP_Fiber( ...
"L", 0, ...
"dz", 1, ...
"lambda", 1550, ...
"rng", 7, ...
"gamma", 0, ...
"fa", 64e9, ...
"X_alpha", 0, ...
"D", 0, ...
"Ds", 0, ...
"Dpmd", 0, ...
"beat_len", 1e9, ...
"corr_len", 1e9, ...
"manakov", 0, ...
"SS_dphimax", 5e-3, ...
"SS_dzmax", 1, ...
"SS_dzmin", 1, ...
"n_waveplates", 1, ...
"useGPU", false, ...
"useSingle", false);
end

View File

@@ -1,10 +1,100 @@
classdef EML_test < IMDDTestCase classdef EML_test < IMDDTestCase
% Auto-generated placeholder for EML. methods (Test, TestTags = {'unit', 'fast', 'optical', 'eml'})
% Target: C:/Users/Silas/Documents/MATLAB/imdd_simulation/Classes/02_optical/EML.m function constructorStoresDerivedScalarsForDeterministicIqMode(testCase)
eml = EML( ...
"mode", eml_mode.iq_linear, ...
"fsimu", 64e9, ...
"lambda", 1550, ...
"power", 0, ...
"linewidth", 0, ...
"alpha", 0, ...
"ampl_imbal", 0, ...
"pha_imbal", 0, ...
"bias", 0, ...
"u_pi", 1, ...
"randomkey", 7);
methods (Test, TestTags = {'placeholder', 'todo'}) testCase.verifyEqual(eml.mode, eml_mode.iq_linear);
function testNotImplemented(testCase) testCase.verifyEqual(eml.fsimu, 64e9);
testCase.assumeFail("Tests for EML are not implemented yet."); testCase.verifyEqual(eml.lambda, 1550);
testCase.verifyEqual(eml.power, 0);
testCase.verifyEqual(eml.linewidth, 0);
testCase.verifyEqual(eml.alpha, 0);
testCase.verifyEqual(eml.field, sqrt(1e-3), "AbsTol", 1e-12);
testCase.verifyEqual(eml.noisefactor, 0, "AbsTol", 1e-12);
testCase.verifyEqual(eml.phase, 0, "AbsTol", 1e-12);
end
function process_ProducesDeterministicIqLinearOutput(testCase)
eml = EML( ...
"mode", eml_mode.iq_linear, ...
"fsimu", 32e9, ...
"lambda", 1310, ...
"power", 3, ...
"linewidth", 0, ...
"alpha", 0, ...
"ampl_imbal", 0, ...
"pha_imbal", 0, ...
"bias", 0, ...
"u_pi", 2, ...
"randomkey", 11);
inputSig = makeElectricalsignal([1 + 1i; -2 + 0.5i; 0.25 - 0.75i], 32e9);
[optField, emlOut] = eml.process_(inputSig.signal);
expectedField = eml.field .* inputSig.signal ./ eml.u_pi;
testCase.verifyEqual(optField, expectedField, "AbsTol", 1e-12);
testCase.verifyEqual(emlOut.signal_len, length(inputSig));
testCase.verifyEqual(emlOut.phase, 0, "AbsTol", 1e-12);
end
function processReturnsOpticalsignalWithUpdatedMetadata(testCase)
eml = EML( ...
"mode", eml_mode.iq_linear, ...
"fsimu", 64e9, ...
"lambda", 1550, ...
"power", 0, ...
"linewidth", 0, ...
"alpha", 0, ...
"ampl_imbal", 0, ...
"pha_imbal", 0, ...
"bias", 0, ...
"u_pi", 1, ...
"randomkey", 19);
inputSig = makeElectricalsignal([1 + 1i; -1 - 1i], 64e9);
[outputSig, emlOut] = eml.process(inputSig);
expectedField = eml.field .* inputSig.signal ./ eml.u_pi;
testCase.verifyClass(outputSig, 'Opticalsignal');
testCase.verifyEqual(outputSig.signal, expectedField, "AbsTol", 1e-12);
testCase.verifyEqual(outputSig.fs, eml.fsimu);
testCase.verifyEqual(outputSig.lambda, eml.lambda * 1e-9, "AbsTol", 1e-15);
testCase.verifyEqual(height(outputSig.logbook), height(inputSig.logbook) + 1);
testCase.verifyTrue(contains(string(outputSig.logbook.Description(end)), "nm Laser"));
testCase.verifyEqual(emlOut.signal_len, length(inputSig));
end end
end end
end end
function sig = makeElectricalsignal(values, fs)
sig = Electricalsignal(values, ...
"fs", fs, ...
"logbook", emptyLogbook());
end
function lb = emptyLogbook()
SignalType = [];
TimeStamp = [];
Length = [];
SignalPower = [];
Nase = [];
SignalCopy = [];
ModifierName = [];
ModifierCopy = {};
Description = [];
lb = table(SignalType, TimeStamp, Length, SignalPower, Nase, SignalCopy, ModifierName, ModifierCopy, Description);
end

View File

@@ -1,10 +1,96 @@
classdef Optical_Demultiplex_test < IMDDTestCase classdef Optical_Demultiplex_test < IMDDTestCase
% Auto-generated placeholder for Optical_Demultiplex. methods (Test, TestTags = {'unit', 'fast', 'optical', 'demux'})
% Target: C:/Users/Silas/Documents/MATLAB/imdd_simulation/Classes/02_optical/Optical_Demultiplex.m function processReturnsOneCellPerChannelAndPreservesMetadata(testCase)
fs = 32e9;
lambdaCenterNm = 1310;
lambdaA = 1309e-9;
lambdaB = 1311e-9;
methods (Test, TestTags = {'placeholder', 'todo'}) sigA = makeOpticalSignal( ...
function testNotImplemented(testCase) [1 2; 1 2; 1 2; 1 2], ...
testCase.assumeFail("Tests for Optical_Demultiplex are not implemented yet."); fs, ...
lambdaA, ...
0.10);
sigB = makeOpticalSignal( ...
[3 4; 3 4; 3 4; 3 4], ...
fs, ...
lambdaB, ...
0.90);
mux = Optical_Multiplex( ...
"fs_in", fs, ...
"fs_out", fs, ...
"lambda_center", lambdaCenterNm, ...
"delta_f", 0, ...
"random_key", 0, ...
"attenuation", 0, ...
"filtype", 3, ...
"B", 200e9);
muxOut = mux.process({sigA, sigB});
demux = Optical_Demultiplex( ...
"fs_in", muxOut.fs, ...
"fs_out", muxOut.fs, ...
"lambda_center", lambdaCenterNm, ...
"attenuation", 0, ...
"filtype", 3, ...
"B", 200e9);
out = demux.process(muxOut);
testCase.verifyClass(out, 'cell');
testCase.verifyNumElements(out, 2);
testCase.verifyClass(out{1}, 'Opticalsignal');
testCase.verifyClass(out{2}, 'Opticalsignal');
testCase.verifyEqual(size(out{1}.signal), size(muxOut.signal));
testCase.verifyEqual(size(out{2}.signal), size(muxOut.signal));
testCase.verifyEqual(out{1}.fs, fs);
testCase.verifyEqual(out{2}.fs, fs);
testCase.verifyEqual(out{1}.lambda, muxOut.lambda(1), 'AbsTol', 1e-12);
testCase.verifyEqual(out{2}.lambda, muxOut.lambda(2), 'AbsTol', 1e-12);
end
function roundTripThroughMuxAndDemuxPreservesChannelMagnitudes(testCase)
fs = 16e9;
lambdaCenterNm = 1310;
sigA = makeOpticalSignal(ones(8, 2), fs, 1309.5e-9, 0.0);
mux = Optical_Multiplex( ...
"fs_in", fs, ...
"fs_out", fs, ...
"lambda_center", lambdaCenterNm, ...
"delta_f", 0, ...
"random_key", 0, ...
"attenuation", 0, ...
"filtype", 3, ...
"B", 200e9);
muxOut = mux.process({sigA});
demux = Optical_Demultiplex( ...
"fs_in", muxOut.fs, ...
"fs_out", muxOut.fs, ...
"lambda_center", lambdaCenterNm, ...
"attenuation", 0, ...
"filtype", 3, ...
"B", 200e9);
out = demux.process(muxOut);
testCase.verifyNumElements(out, 1);
testCase.verifyEqual(size(out{1}.signal), size(sigA.signal));
testCase.verifyEqual(out{1}.fs, fs);
testCase.verifyEqual(out{1}.lambda, muxOut.lambda(1), 'AbsTol', 1e-12);
testCase.verifyEqual(mean(abs(out{1}.signal), 'all'), mean(abs(sigA.signal), 'all'), 'AbsTol', 1e-6);
end end
end end
end end
function sig = makeOpticalSignal(signal, fs, lambda, polrot)
base = Signal(signal, "fs", fs);
sig = Opticalsignal( ...
signal, ...
"fs", fs, ...
"logbook", base.logbook, ...
"lambda", lambda, ...
"nase", 0, ...
"polrot", polrot);
end

View File

@@ -1,10 +1,101 @@
classdef Optical_Multiplex_test < IMDDTestCase classdef Optical_Multiplex_test < IMDDTestCase
% Auto-generated placeholder for Optical_Multiplex. methods (Test, TestTags = {'unit', 'fast', 'optical', 'mux'})
% Target: C:/Users/Silas/Documents/MATLAB/imdd_simulation/Classes/02_optical/Optical_Multiplex.m function processCombinesChannelsAndPreservesMetadata(testCase)
fs = 32e9;
lambdaCenter = 1550;
lambdaA = 1550e-9;
lambdaB = 1550e-9;
methods (Test, TestTags = {'placeholder', 'todo'}) sigA = makeOpticalSignal( ...
function testNotImplemented(testCase) [1 2; 1 2; 1 2], ...
testCase.assumeFail("Tests for Optical_Multiplex are not implemented yet."); fs, ...
lambdaA, ...
0.10);
sigB = makeOpticalSignal( ...
[3 4; 3 4; 3 4], ...
fs, ...
lambdaB, ...
0.90);
mux = Optical_Multiplex( ...
"fs_in", fs, ...
"fs_out", fs, ...
"lambda_center", lambdaCenter, ...
"delta_f", 0, ...
"random_key", 0, ...
"attenuation", 0, ...
"filtype", 3);
out = mux.process({sigA, sigB});
expected = combineSingleChannelMuxOutputs({sigA, sigB}, mux);
testCase.verifyClass(out, 'Opticalsignal');
testCase.verifyEqual(size(out.signal), [3 2]);
testCase.verifyEqual(out.signal, expected, 'AbsTol', 1e-12);
testCase.verifyEqual(out.fs, fs);
testCase.verifyEqual(out.lambda, [lambdaA, lambdaB], 'AbsTol', 1e-9);
testCase.verifyEqual(out.polrot, [0.10, 0.90], 'AbsTol', 1e-12);
testCase.verifyEqual(height(out.logbook), 2);
testCase.verifyEqual(string(out.logbook.Description(end)), "Opt. Mux. ");
end
function processSupportsMoreThanOneInputChannel(testCase)
fs = 16e9;
lambdaCenter = 1310;
sig1 = makeOpticalSignal(ones(4, 2), fs, lambdaCenter * 1e-9, 0.0);
sig2 = makeOpticalSignal(2 * ones(4, 2), fs, lambdaCenter * 1e-9, 0.5);
sig3 = makeOpticalSignal(3 * ones(4, 2), fs, lambdaCenter * 1e-9, 1.0);
mux = Optical_Multiplex( ...
"fs_in", fs, ...
"fs_out", fs, ...
"lambda_center", lambdaCenter, ...
"delta_f", 0, ...
"random_key", 0, ...
"attenuation", 0, ...
"filtype", 3);
out = mux.process({sig1, sig2, sig3});
expected = combineSingleChannelMuxOutputs({sig1, sig2, sig3}, mux);
testCase.verifySize(out.signal, [4, 2]);
testCase.verifyEqual(out.signal, expected, 'AbsTol', 1e-12);
testCase.verifyEqual(numel(out.lambda), 3);
testCase.verifyEqual(numel(out.polrot), 3);
end end
end end
end end
function sig = makeOpticalSignal(signal, fs, lambda, polrot)
base = Signal(signal, "fs", fs);
sig = Opticalsignal( ...
signal, ...
"fs", fs, ...
"logbook", base.logbook, ...
"lambda", lambda, ...
"nase", 0, ...
"polrot", polrot);
end
function expected = combineSingleChannelMuxOutputs(signals, muxPrototype)
expected = [];
for idx = 1:numel(signals)
mux = Optical_Multiplex( ...
"fs_in", muxPrototype.fs_in, ...
"fs_out", muxPrototype.fs_out, ...
"lambda_center", muxPrototype.lambda_center, ...
"delta_f", muxPrototype.delta_f, ...
"random_key", muxPrototype.random_key, ...
"attenuation", muxPrototype.attenuation, ...
"filtype", muxPrototype.filtype);
singleOut = mux.process({signals{idx}});
if isempty(expected)
expected = zeros(size(singleOut.signal));
end
expected = expected + singleOut.signal;
end
end

View File

@@ -0,0 +1,103 @@
classdef IMDD_base_system_impairment_monotonicity_integration_test < IMDDTestCase
% Integration test that compares a clean baseline IM/DD workflow
% against a deliberately impaired variant.
%
% The aim here is monotonic behavior, not exact waveform matching.
% If the channel is made worse, the recovered performance should not
% improve, and the intermediate physical metrics should reflect that.
properties
baseline
impaired
end
methods (TestClassSetup)
function runScenarioPairOnce(testCase)
% Build both scenarios once and share them across the test
% methods. The chain itself is deterministic, so the two
% results are directly comparable.
testCase.baseline = buildReducedImddWorkflow("baseline");
testCase.impaired = buildReducedImddWorkflow("impaired");
end
end
methods (Test, TestTags = {'integration', 'slow', 'imdd'})
function impairedScenarioDoesNotOutperformBaseline(testCase)
base = testCase.baseline;
bad = testCase.impaired;
% The impaired configuration should not beat the baseline.
testCase.verifyLessThanOrEqual( ...
base.ffe_results.metrics.BER, ...
bad.ffe_results.metrics.BER + 1e-12);
testCase.verifyLessThanOrEqual( ...
base.mlse_results.metrics.BER, ...
bad.mlse_results.metrics.BER + 1e-12);
end
function bothScenariosStayFiniteAndExposeTheImpairment(testCase)
base = testCase.baseline;
bad = testCase.impaired;
finiteSignals = {
base.Digi_sig.signal
base.El_sig.signal
base.Opt_sig_tx.signal
base.Opt_sig.signal
base.Rx_sig_after_pd.signal
base.Rx_sig_filtered.signal
base.Scpe_sig_pre_mf.signal
base.Scpe_sig.signal
base.Synced_sig_centered.signal
base.Synced_sig.signal
bad.Digi_sig.signal
bad.El_sig.signal
bad.Opt_sig_tx.signal
bad.Opt_sig.signal
bad.Rx_sig_after_pd.signal
bad.Rx_sig_filtered.signal
bad.Scpe_sig_pre_mf.signal
bad.Scpe_sig.signal
bad.Synced_sig_centered.signal
bad.Synced_sig.signal
};
for idx = 1:numel(finiteSignals)
sig = finiteSignals{idx};
testCase.verifyFalse(any(isnan(sig), 'all'));
testCase.verifyFalse(any(isinf(sig), 'all'));
end
% The impaired scenario is defined by a lower received power and
% a tighter electrical receiver bandwidth.
testCase.verifyLessThan(bad.params.ropDbm, base.params.ropDbm);
testCase.verifyLessThan(bad.params.rxElectricalBandwidthHz, base.params.rxElectricalBandwidthHz);
testCase.verifyLessThan(bad.params.scopeBandwidthHz, base.params.scopeBandwidthHz);
basePdPower = signalPower(base.Rx_sig_after_pd.signal);
badPdPower = signalPower(bad.Rx_sig_after_pd.signal);
baseRxPower = signalPower(base.Rx_sig_filtered.signal);
badRxPower = signalPower(bad.Rx_sig_filtered.signal);
baseScopePower = signalPower(base.Scpe_sig_pre_mf.signal);
badScopePower = signalPower(bad.Scpe_sig_pre_mf.signal);
testCase.verifyGreaterThan(basePdPower, badPdPower);
testCase.verifyGreaterThan(baseRxPower, badRxPower);
testCase.verifyGreaterThan(baseScopePower, badScopePower);
% Both scenarios must still produce valid metrics.
testCase.verifyGreaterThanOrEqual(base.ffe_results.metrics.BER, 0);
testCase.verifyLessThanOrEqual(base.ffe_results.metrics.BER, 1);
testCase.verifyGreaterThanOrEqual(bad.ffe_results.metrics.BER, 0);
testCase.verifyLessThanOrEqual(bad.ffe_results.metrics.BER, 1);
testCase.verifyGreaterThanOrEqual(base.mlse_results.metrics.BER, 0);
testCase.verifyLessThanOrEqual(base.mlse_results.metrics.BER, 1);
testCase.verifyGreaterThanOrEqual(bad.mlse_results.metrics.BER, 0);
testCase.verifyLessThanOrEqual(bad.mlse_results.metrics.BER, 1);
end
end
end
function p = signalPower(signal)
p = mean(abs(signal(:)).^2);
end

View File

@@ -6,6 +6,7 @@ classdef IMDD_base_system_minimal_integration_test < IMDDTestCase
% - the reduced end-to-end chain runs without errors % - the reduced end-to-end chain runs without errors
% - key signal objects have the expected lengths and sampling rates % - key signal objects have the expected lengths and sampling rates
% - the DSP outputs return finite, bounded metrics % - the DSP outputs return finite, bounded metrics
% - the intermediate optical and electrical stages stay physically sane
% - MLSE does not regress relative to the preceding FFE stage % - MLSE does not regress relative to the preceding FFE stage
% %
% Thresholds are intentionally provisional in this first iteration. % Thresholds are intentionally provisional in this first iteration.
@@ -19,7 +20,7 @@ classdef IMDD_base_system_minimal_integration_test < IMDDTestCase
function runReducedImddWorkflowOnce(testCase) function runReducedImddWorkflowOnce(testCase)
% Run the deterministic reduced workflow once and share the % Run the deterministic reduced workflow once and share the
% resulting signals/metrics across all test methods. % resulting signals/metrics across all test methods.
testCase.workflow = runReducedWorkflow(); testCase.workflow = buildReducedImddWorkflow("minimal");
end end
end end
@@ -31,18 +32,30 @@ classdef IMDD_base_system_minimal_integration_test < IMDDTestCase
testCase.verifyClass(wf.Symbols, 'Informationsignal'); testCase.verifyClass(wf.Symbols, 'Informationsignal');
testCase.verifyClass(wf.Digi_sig, 'Informationsignal'); testCase.verifyClass(wf.Digi_sig, 'Informationsignal');
testCase.verifyClass(wf.El_sig, 'Electricalsignal'); testCase.verifyClass(wf.El_sig, 'Electricalsignal');
testCase.verifyClass(wf.Opt_sig_tx, 'Opticalsignal');
testCase.verifyClass(wf.Opt_sig, 'Opticalsignal'); testCase.verifyClass(wf.Opt_sig, 'Opticalsignal');
testCase.verifyClass(wf.Rx_sig_after_pd, 'Electricalsignal'); testCase.verifyClass(wf.Rx_sig_after_pd, 'Electricalsignal');
testCase.verifyClass(wf.Scpe_sig_pre_mf, 'Informationsignal');
testCase.verifyClass(wf.Scpe_sig, 'Informationsignal'); testCase.verifyClass(wf.Scpe_sig, 'Informationsignal');
testCase.verifyClass(wf.Synced_sig_centered, 'Informationsignal');
testCase.verifyClass(wf.Synced_sig, 'Informationsignal'); testCase.verifyClass(wf.Synced_sig, 'Informationsignal');
testCase.verifyGreaterThan(length(wf.Tx_bits.signal), 0); testCase.verifyGreaterThan(length(wf.Tx_bits.signal), 0);
testCase.verifyGreaterThan(length(wf.Symbols.signal), 0); testCase.verifyGreaterThan(length(wf.Symbols.signal), 0);
testCase.verifyEqual(wf.Digi_sig.fs, wf.params.fdac);
% Tx_bits is a bit-level container and does not carry a
% sampling-rate contract on this path.
testCase.verifyEqual(wf.Symbols.fs, wf.params.fsym); testCase.verifyEqual(wf.Symbols.fs, wf.params.fsym);
testCase.verifyEqual(wf.El_sig.fs, wf.params.fdac * wf.params.kover);
testCase.verifyEqual(wf.Opt_sig_tx.fs, wf.El_sig.fs);
testCase.verifyEqual(wf.Opt_sig.fs, wf.Opt_sig_tx.fs);
testCase.verifyEqual(wf.Rx_sig_after_pd.fs, wf.Opt_sig.fs);
testCase.verifyEqual(wf.Scpe_sig_pre_mf.fs, wf.params.fadc);
% After the matched filter, the signal is intentionally reduced % After the matched filter, the signal is intentionally reduced
% to 2 samples per symbol for the downstream DSP chain. % to 2 samples per symbol for the downstream DSP chain.
testCase.verifyEqual(wf.Scpe_sig.fs, 2 * wf.params.fsym); testCase.verifyEqual(wf.Scpe_sig.fs, 2 * wf.params.fsym);
testCase.verifyEqual(wf.Synced_sig_centered.fs, 2 * wf.params.fsym);
testCase.verifyEqual(wf.Synced_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 % The synchronized signal is explicitly cropped to 2 samples per
@@ -56,9 +69,12 @@ classdef IMDD_base_system_minimal_integration_test < IMDDTestCase
finiteSignals = { finiteSignals = {
wf.Digi_sig.signal wf.Digi_sig.signal
wf.El_sig.signal wf.El_sig.signal
wf.Opt_sig_tx.signal
wf.Opt_sig.signal wf.Opt_sig.signal
wf.Rx_sig_after_pd.signal wf.Rx_sig_after_pd.signal
wf.Scpe_sig_pre_mf.signal
wf.Scpe_sig.signal wf.Scpe_sig.signal
wf.Synced_sig_centered.signal
wf.Synced_sig.signal wf.Synced_sig.signal
}; };
@@ -68,6 +84,20 @@ classdef IMDD_base_system_minimal_integration_test < IMDDTestCase
testCase.verifyFalse(any(isinf(sig), 'all')); testCase.verifyFalse(any(isinf(sig), 'all'));
end end
txOptPower = signalPower(wf.Opt_sig_tx.signal);
fiberOptPower = signalPower(wf.Opt_sig.signal);
pdPower = signalPower(wf.Rx_sig_after_pd.signal);
preMfVariance = signalVariance(wf.Scpe_sig_pre_mf.signal);
mfVariance = signalVariance(wf.Scpe_sig.signal);
centeredMean = mean(wf.Synced_sig_centered.signal(:));
testCase.verifyGreaterThan(txOptPower, 0);
testCase.verifyGreaterThan(fiberOptPower, 0);
testCase.verifyGreaterThan(pdPower, 0);
testCase.verifyGreaterThan(preMfVariance, 0);
testCase.verifyGreaterThan(mfVariance, 0);
testCase.verifyLessThanOrEqual(abs(centeredMean), 1e-12);
testCase.verifyGreaterThanOrEqual(wf.ffe_results.metrics.BER, 0); testCase.verifyGreaterThanOrEqual(wf.ffe_results.metrics.BER, 0);
testCase.verifyLessThanOrEqual(wf.ffe_results.metrics.BER, 1); testCase.verifyLessThanOrEqual(wf.ffe_results.metrics.BER, 1);
testCase.verifyGreaterThanOrEqual(wf.mlse_results.metrics.BER, 0); testCase.verifyGreaterThanOrEqual(wf.mlse_results.metrics.BER, 0);
@@ -105,217 +135,11 @@ classdef IMDD_base_system_minimal_integration_test < IMDDTestCase
end end
end end
end end
function p = signalPower(signal)
function workflow = runReducedWorkflow() p = mean(abs(signal(:)).^2);
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 end
function params = reducedWorkflowParameters() function v = signalVariance(signal)
params = struct(); centered = signal(:) - mean(signal(:));
v = mean(abs(centered).^2);
% 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 end

View File

@@ -0,0 +1,109 @@
classdef IMDD_base_system_no_impairment_integration_test < IMDDTestCase
% Integration test for a reduced IM/DD workflow with no added channel
% impairments.
%
% The purpose of this test is to guard the near-ideal system behavior:
% the full chain still runs, the signal objects stay well-formed, and
% the DSP stages achieve very low error rates when fiber and receiver
% impairments are neutralized as far as practical.
properties
workflow
end
methods (TestClassSetup)
function runNoImpairmentWorkflowOnce(testCase)
% Run the deterministic workflow once and reuse the result for
% all test methods.
testCase.workflow = buildReducedImddWorkflow("no-impairment");
end
end
methods (Test, TestTags = {'integration', 'slow', 'imdd'})
function noImpairmentWorkflowBuildsExpectedSignalStages(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_tx, 'Opticalsignal');
testCase.verifyClass(wf.Opt_sig, 'Opticalsignal');
testCase.verifyClass(wf.Rx_sig_after_pd, 'Electricalsignal');
testCase.verifyClass(wf.Scpe_sig_pre_mf, 'Informationsignal');
testCase.verifyClass(wf.Scpe_sig, 'Informationsignal');
testCase.verifyClass(wf.Synced_sig_centered, '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);
testCase.verifyEqual(wf.Digi_sig.fs, wf.params.fdac);
testCase.verifyEqual(wf.El_sig.fs, wf.params.fdac * wf.params.kover);
testCase.verifyEqual(wf.Opt_sig_tx.fs, wf.El_sig.fs);
testCase.verifyEqual(wf.Opt_sig.fs, wf.Opt_sig_tx.fs);
testCase.verifyEqual(wf.Rx_sig_after_pd.fs, wf.Opt_sig.fs);
testCase.verifyEqual(wf.Scpe_sig_pre_mf.fs, wf.params.fadc);
testCase.verifyEqual(wf.Scpe_sig.fs, 2 * wf.params.fsym);
testCase.verifyEqual(wf.Synced_sig_centered.fs, 2 * wf.params.fsym);
testCase.verifyEqual(wf.Synced_sig.fs, 2 * wf.params.fsym);
testCase.verifyEqual(length(wf.Synced_sig.signal), 2 * length(wf.Symbols.signal));
end
function noImpairmentWorkflowProducesFiniteSignalsAndMetrics(testCase)
wf = testCase.workflow;
finiteSignals = {
wf.Digi_sig.signal
wf.El_sig.signal
wf.Opt_sig_tx.signal
wf.Opt_sig.signal
wf.Rx_sig_after_pd.signal
wf.Scpe_sig_pre_mf.signal
wf.Scpe_sig.signal
wf.Synced_sig_centered.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.verifyEqual(wf.params.linkLengthKm, 0);
testCase.verifyEqual(wf.params.fiberAlphaDbPerKm, 0);
testCase.verifyEqual(wf.params.rxFilterActive, false);
testCase.verifyEqual(wf.params.scopeLpfActive, false);
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 noImpairmentWorkflowMeetsPerformanceChecks(testCase)
wf = testCase.workflow;
% This is a no-added-impairment baseline, not a mathematical
% idealization of the full chain. The BER is therefore bounded
% rather than expected to be near zero.
maxFfeBer = 3e-1;
maxMlseBer = 5e-2;
testCase.verifyLessThanOrEqual(wf.ffe_results.metrics.BER, maxFfeBer);
testCase.verifyLessThanOrEqual(wf.mlse_results.metrics.BER, maxMlseBer);
% MLSE should never perform worse than the direct FFE path in
% this no-impairment regime.
testCase.verifyLessThanOrEqual( ...
wf.mlse_results.metrics.BER, ...
wf.ffe_results.metrics.BER + 1e-12);
end
end
end

View File

@@ -0,0 +1,222 @@
classdef WDM_end_to_end_two_channel_receiver_integration_test < IMDDTestCase
% End-to-end WDM receiver integration test.
%
% The fixture is intentionally small, deterministic, and aligned with
% the repo's WDM workflow:
% electrical drive -> EML -> polarization control -> optical mux
% -> launch amplifier -> optical demux -> photodiode
%
% The receiver-side check is based on a matched projection score and a
% normalized correlation check. That makes the test useful for
% regression while staying far away from a brittle golden waveform.
properties
workflow
end
methods (TestClassSetup)
function buildWorkflowOnce(testCase)
testCase.workflow = buildReceiverWorkflow();
end
end
methods (Test, TestTags = {'integration', 'slow', 'optical', 'wdm', 'receiver'})
function receiverChainHasExpectedStagesAndMetadata(testCase)
wf = testCase.workflow;
testCase.verifyClass(wf.muxOut, 'Opticalsignal');
testCase.verifyClass(wf.launchOut, 'Opticalsignal');
testCase.verifyClass(wf.demuxOut, 'cell');
testCase.verifyClass(wf.rxOut, 'cell');
testCase.verifyNumElements(wf.demuxOut, wf.params.numChannels);
testCase.verifyNumElements(wf.rxOut, wf.params.numChannels);
testCase.verifyEqual(wf.muxOut.fs, wf.params.fsMux);
testCase.verifyEqual(wf.launchOut.fs, wf.params.fsMux);
testCase.verifyEqual(wf.demuxOut{1}.fs, wf.params.fsBase);
testCase.verifyEqual(wf.demuxOut{2}.fs, wf.params.fsBase);
testCase.verifyEqual(wf.rxOut{1}.fs, wf.params.fsBase);
testCase.verifyEqual(wf.rxOut{2}.fs, wf.params.fsBase);
testCase.verifyEqual(numel(wf.muxOut.lambda), wf.params.numChannels);
testCase.verifyEqual(wf.muxOut.lambda, wf.channelPlanM, "AbsTol", 1e-12);
testCase.verifyEqual(wf.demuxOut{1}.lambda, wf.channelPlanM(1), "AbsTol", 1e-12);
testCase.verifyEqual(wf.demuxOut{2}.lambda, wf.channelPlanM(2), "AbsTol", 1e-12);
testCase.verifyTrue(all(isfinite(wf.muxOut.signal), "all"));
testCase.verifyTrue(all(isfinite(wf.launchOut.signal), "all"));
testCase.verifyTrue(all(isfinite(wf.demuxOut{1}.signal), "all"));
testCase.verifyTrue(all(isfinite(wf.demuxOut{2}.signal), "all"));
testCase.verifyTrue(all(isfinite(wf.rxOut{1}.signal), "all"));
testCase.verifyTrue(all(isfinite(wf.rxOut{2}.signal), "all"));
testCase.verifyGreaterThan(height(wf.muxOut.logbook), 0);
testCase.verifyGreaterThan(height(wf.demuxOut{1}.logbook), 0);
testCase.verifyGreaterThan(height(wf.rxOut{1}.logbook), 0);
end
function intendedBranchBeatsWrongBranchAfterDetection(testCase)
wf = testCase.workflow;
rx1 = centeredSignal(wf.rxOut{1}.signal);
rx2 = centeredSignal(wf.rxOut{2}.signal);
drive1 = centeredSignal(wf.driveRefs{1}.signal);
drive2 = centeredSignal(wf.driveRefs{2}.signal);
selfCorr1 = normalizedCorrelation(rx1, drive1);
selfCorr2 = normalizedCorrelation(rx2, drive2);
crossCorr1 = normalizedCorrelation(rx2, drive1);
crossCorr2 = normalizedCorrelation(rx1, drive2);
selfScore1 = matchedProjectionScore(rx1, drive1);
selfScore2 = matchedProjectionScore(rx2, drive2);
crossScore1 = matchedProjectionScore(rx2, drive1);
crossScore2 = matchedProjectionScore(rx1, drive2);
testCase.verifyGreaterThan(selfCorr1, crossCorr1);
testCase.verifyGreaterThan(selfCorr2, crossCorr2);
testCase.verifyGreaterThan(selfScore1, crossScore1);
testCase.verifyGreaterThan(selfScore2, crossScore2);
% Loose first-pass guardrails. The exact values can be tightened
% later once the baseline is reviewed across repeated runs.
testCase.verifyGreaterThan(selfCorr1, 0.05);
testCase.verifyGreaterThan(selfCorr2, 0.05);
testCase.verifyGreaterThan(selfScore1, 0);
testCase.verifyGreaterThan(selfScore2, 0);
end
end
end
function workflow = buildReceiverWorkflow()
params = receiverIntegrationParameters();
channelPlanNm = calcWavelengthPlan(params.numChannels, params.channelSpacingHz, params.lambdaCenterNm);
channelPlanM = channelPlanNm * 1e-9;
driveRefs = cell(1, params.numChannels);
opticalRefs = cell(1, params.numChannels);
for ch = 1:params.numChannels
driveRefs{ch} = makeElectricalSignal(params.driveSignals{ch}, params.fsBase);
optical = EML( ...
"mode", eml_mode.im_cosinus, ...
"fsimu", params.fsBase, ...
"lambda", channelPlanNm(ch), ...
"power", params.laserPowerDbm, ...
"linewidth", 0, ...
"alpha", 0, ...
"ampl_imbal", 0, ...
"pha_imbal", 0, ...
"bias", params.vbias, ...
"u_pi", params.uPi, ...
"randomkey", params.randomKey + ch).process(driveRefs{ch});
% The WDM mux expects dual-polarization optical signals.
opticalRefs{ch} = Polarization_Controller( ...
"mode", "rot_power", ...
"desired_power", 50).process(optical);
end
mux = Optical_Multiplex( ...
"fs_in", params.fsBase, ...
"fs_out", params.fsMux, ...
"lambda_center", params.lambdaCenterNm, ...
"delta_f", 0, ...
"random_key", 0, ...
"attenuation", 0, ...
"filtype", 1, ...
"B", 200e9);
muxOut = mux.process(opticalRefs);
launchOut = Amplifier( ...
"amp_mode", amp_mode.ideal_no_noise, ...
"gain_mode", gain_mode.output_power, ...
"nase_mode", nase_mode.pass_ase, ...
"amplification_db", params.launchPowerDbm).process(muxOut);
demux = Optical_Demultiplex( ...
"fs_in", launchOut.fs, ...
"fs_out", params.fsBase, ...
"lambda_center", params.lambdaCenterNm, ...
"attenuation", 0, ...
"filtype", 1, ...
"B", 200e9);
demuxOut = demux.process(launchOut);
rxOut = cell(1, params.numChannels);
for ch = 1:params.numChannels
rxOut{ch} = Photodiode( ...
"fsimu", params.fsBase, ...
"responsivity", 1, ...
"dark_current", 0, ...
"temperature", 20, ...
"nep", 0, ...
"randomkey", params.randomKey + 100 + ch).process(demuxOut{ch});
end
workflow = struct();
workflow.params = params;
workflow.channelPlanNm = channelPlanNm;
workflow.channelPlanM = channelPlanM;
workflow.driveRefs = driveRefs;
workflow.opticalRefs = opticalRefs;
workflow.muxOut = muxOut;
workflow.launchOut = launchOut;
workflow.demuxOut = demuxOut;
workflow.rxOut = rxOut;
end
function params = receiverIntegrationParameters()
params = struct();
% Small fixture, but still close to the WDM project conventions.
params.numChannels = 2;
params.channelSpacingHz = 200e9;
params.lambdaCenterNm = 1310;
params.fsBase = 64e9;
params.fsMux = 4 * params.fsBase;
params.laserPowerDbm = 3;
params.launchPowerDbm = params.laserPowerDbm + 10 * log10(params.numChannels);
params.uPi = 4.6;
params.vbias = -0.5 * params.uPi;
params.randomKey = 21;
n = (0:63).';
params.driveSignals = {
0.18 * sin(2*pi*n/16) + 0.03 * cos(2*pi*n/8)
0.17 * cos(2*pi*n/11 + pi/5) - 0.04 * sin(2*pi*n/5)
};
end
function sig = makeElectricalSignal(values, fs)
base = Signal(values, "fs", fs);
sig = Electricalsignal( ...
values, ...
"fs", fs, ...
"logbook", base.logbook);
end
function centered = centeredSignal(signal)
centered = signal(:) - mean(signal(:));
end
function corrVal = normalizedCorrelation(candidate, reference)
denom = sqrt(sum(abs(candidate).^2) * sum(abs(reference).^2));
if denom == 0
corrVal = 0;
return
end
corrVal = abs(sum(conj(candidate) .* reference)) / denom;
end
function score = matchedProjectionScore(candidate, reference)
denom = sum(abs(reference).^2);
if denom == 0
score = 0;
return
end
score = abs(sum(conj(candidate) .* reference)).^2 / denom;
end

View File

@@ -0,0 +1,245 @@
classdef WDM_mux_demux_optical_chain_integration_test < IMDDTestCase
% Integration test for the WDM optical subchain.
%
% The goal is to verify the mux/demux behavior in a way that matches
% the repo's WDM workflows:
% electrical drive -> EML -> polarization control -> optical mux
% -> launch amplifier -> optical demux
%
% The electrical stimuli are intentionally small and deterministic so
% the test isolates the optical chain rather than the TX waveform
% generation stack.
properties
workflow
end
methods (TestClassSetup)
function buildWorkflowOnce(testCase)
testCase.workflow = buildWdmWorkflow();
end
end
methods (Test, TestTags = {'integration', 'slow', 'optical', 'wdm'})
function muxAndDemuxPreserveMetadataAndSamplingRates(testCase)
wf = testCase.workflow;
numChannels = wf.params.numChannels;
testCase.verifyClass(wf.muxOut, 'Opticalsignal');
testCase.verifyClass(wf.launchOut, 'Opticalsignal');
testCase.verifyClass(wf.demuxOut, 'cell');
testCase.verifyNumElements(wf.demuxOut, numChannels);
testCase.verifyEqual(wf.muxOut.fs, wf.params.fsMux);
testCase.verifyEqual(wf.launchOut.fs, wf.params.fsMux);
for ch = 1:numChannels
testCase.verifyEqual(wf.demuxOut{ch}.fs, wf.params.fsBase);
end
testCase.verifyEqual(numel(wf.muxOut.lambda), numChannels);
testCase.verifyEqual(wf.muxOut.lambda, wf.channelPlanM, "AbsTol", 5e-9);
for ch = 1:numChannels
testCase.verifyEqual(wf.demuxOut{ch}.lambda, wf.channelPlanM(ch), "AbsTol", 5e-9);
end
testCase.verifyTrue(all(isfinite(wf.muxOut.signal), "all"));
testCase.verifyTrue(all(isfinite(wf.launchOut.signal), "all"));
for ch = 1:numChannels
testCase.verifyTrue(all(isfinite(wf.demuxOut{ch}.signal), "all"));
end
testCase.verifyGreaterThan(height(wf.muxOut.logbook), 0);
for ch = 1:numChannels
testCase.verifyGreaterThan(height(wf.demuxOut{ch}.logbook), 0);
end
end
function intendedChannelIsRecoveredBetterThanCrossTalk(testCase)
wf = testCase.workflow;
numChannels = wf.params.numChannels;
branchLambdaIdx = zeros(numChannels, 1);
branchPower = zeros(numChannels, 1);
for outCh = 1:numChannels
branchSignal = wf.demuxOut{outCh}.signal;
branchPower(outCh) = mean(abs(branchSignal).^2, 'all');
branchLambdaIdx(outCh) = nearestWavelengthIndex( ...
wf.demuxOut{outCh}.lambda, ...
wf.channelPlanM);
end
referencePower = zeros(numChannels, 1);
for refCh = 1:numChannels
referencePower(refCh) = mean(abs(wf.channelRefs{refCh}.signal).^2, 'all');
end
referenceFraction = referencePower / sum(referencePower);
recoveredFraction = branchPower / sum(branchPower);
for ch = 1:numChannels
matchedRef = branchLambdaIdx(ch);
otherIdx = setdiff(1:numChannels, matchedRef);
matchedError = abs(recoveredFraction(ch) - referenceFraction(matchedRef));
offErrors = abs(recoveredFraction(ch) - referenceFraction(otherIdx));
% The recovered branch should keep non-trivial optical power
% after demux, even when compared against the leaked branches.
testCase.verifyGreaterThan(branchPower(ch), 0);
testCase.verifyLessThanOrEqual(matchedError, min(offErrors) + 1e-3);
testCase.verifyLessThan(matchedError, 0.2);
% Correlation is kept as a secondary diagnostic metric.
testCase.verifyTrue(isfinite(normalizedCorrelation( ...
centredPowerEnvelope(wf.demuxOut{ch}.signal), ...
centredPowerEnvelope(wf.channelRefs{matchedRef}.signal))));
end
end
end
end
function workflow = buildWdmWorkflow()
params = wdmIntegrationParameters();
channelPlanNm = calcWavelengthPlan(params.numChannels, params.channelSpacingHz, params.lambdaCenterNm);
channelPlanM = channelPlanNm * 1e-9;
% Build the WDM channels separately so the test can later compare the
% recovered branch against the original reference branch.
channelRefs = cell(1, params.numChannels);
for ch = 1:params.numChannels
drive = makeElectricalSignal(params.driveSignals{ch}, params.fsBase);
optical = EML( ...
"mode", eml_mode.im_cosinus, ...
"fsimu", params.fsBase, ...
"lambda", channelPlanNm(ch), ...
"power", params.laserPowerDbm(ch), ...
"linewidth", 0, ...
"alpha", 0, ...
"ampl_imbal", 0, ...
"pha_imbal", 0, ...
"bias", params.vbias, ...
"u_pi", params.uPi, ...
"randomkey", params.randomKey + ch).process(drive);
% Create a dual-polarization optical channel, mirroring the WDM
% scripts where the mux receives DP optical signals.
channelRefs{ch} = Polarization_Controller( ...
"mode", "rot_power", ...
"desired_power", 100).process(optical);
end
mux = Optical_Multiplex( ...
"fs_in", params.fsBase, ...
"fs_out", params.fsMux, ...
"lambda_center", params.lambdaCenterNm, ...
"delta_f", 0, ...
"random_key", 0, ...
"attenuation", 0, ...
"filtype", 1, ...
"B", 120e9);
muxOut = mux.process(channelRefs);
% A very small deterministic propagation hop keeps the fixture closer
% to the WDM workflows without turning this into a full channel study.
wdmFiberOut = DP_Fiber( ...
"L", 0, ...
"dz", 1, ...
"lambda", params.lambdaCenterNm, ...
"rng", 0, ...
"gamma", 0, ...
"fa", muxOut.fs, ...
"X_alpha", 0, ...
"D", 0, ...
"Ds", 0, ...
"Dpmd", 0, ...
"beat_len", 1e9, ...
"corr_len", 1e9, ...
"manakov", 0, ...
"SS_dphimax", 1e-2, ...
"SS_dzmax", 1, ...
"SS_dzmin", 1, ...
"n_waveplates", 1, ...
"useGPU", false, ...
"useSingle", false).process(muxOut);
% The WDM project scripts normally apply a launch amplifier after the
% mux. We keep that stage here, but with deterministic no-noise settings.
launchOut = Amplifier( ...
"amp_mode", amp_mode.ideal_no_noise, ...
"gain_mode", gain_mode.output_power, ...
"nase_mode", nase_mode.pass_ase, ...
"amplification_db", params.launchPowerDbm).process(wdmFiberOut);
demux = Optical_Demultiplex( ...
"fs_in", launchOut.fs, ...
"fs_out", params.fsBase, ...
"lambda_center", params.lambdaCenterNm, ...
"attenuation", 0, ...
"filtype", 1, ...
"B", 200e9);
demuxOut = demux.process(launchOut);
workflow = struct();
workflow.params = params;
workflow.channelPlanNm = channelPlanNm;
workflow.channelPlanM = channelPlanM;
workflow.channelRefs = channelRefs;
workflow.muxOut = muxOut;
workflow.wdmFiberOut = wdmFiberOut;
workflow.launchOut = launchOut;
workflow.demuxOut = demuxOut;
end
function params = wdmIntegrationParameters()
params = struct();
% Keep the fixture small, deterministic, and close to the WDM scripts.
params.numChannels = 4;
params.channelSpacingHz = 400e9;
params.lambdaCenterNm = 1310;
params.fsBase = 64e9;
params.fsMux = 4 * params.fsBase;
params.laserPowerDbm = [-3, 0, 3, 6];
params.launchPowerDbm = 3 + 10*log10(params.numChannels);
params.uPi = 4.6;
params.vbias = -0.5 * params.uPi;
params.randomKey = 11;
n = (0:63).';
params.driveSignals = {
0.18 * sin(2*pi*n/16) + 0.03 * cos(2*pi*n/8)
0.17 * cos(2*pi*n/11 + pi/5) - 0.04 * sin(2*pi*n/5)
0.16 * sin(2*pi*n/7 + pi/7) + 0.02 * cos(2*pi*n/4)
0.14 * cos(2*pi*n/9) - 0.05 * sin(2*pi*n/6 + pi/8)
};
end
function sig = makeElectricalSignal(values, fs)
base = Signal(values, "fs", fs);
sig = Electricalsignal( ...
values, ...
"fs", fs, ...
"logbook", base.logbook);
end
function corrVal = normalizedCorrelation(refSignal, candidateSignal)
ref = refSignal(:);
cand = candidateSignal(:);
denom = sqrt(sum(abs(ref).^2) * sum(abs(cand).^2));
if denom == 0
corrVal = 0;
return
end
corrVal = abs(sum(conj(ref) .* cand)) / denom;
end
function idx = nearestWavelengthIndex(value, plan)
[~, idx] = min(abs(plan(:) - value));
end
function centeredEnvelope = centredPowerEnvelope(signal)
powerEnvelope = abs(signal(:)).^2;
centeredEnvelope = powerEnvelope - mean(powerEnvelope);
end

View File

@@ -0,0 +1,290 @@
function workflow = buildReducedImddWorkflow(scenario)
%BUILDREDUCEDIMDDWORKFLOW Build a reduced deterministic IM/DD workflow.
%
% The helper keeps the stage order aligned with the project workflow while
% allowing tests to switch between representative scenarios:
% - "minimal": reduced regression baseline
% - "no-impairment": near-ideal baseline with impairment knobs neutralized
% - "baseline": reference point for monotonicity checks
% - "impaired": intentionally degraded variant for monotonicity checks
arguments
scenario (1, 1) string = "minimal"
end
params = scenarioParameters(scenario);
workflow = runReducedWorkflow(params, scenario);
end
function workflow = runReducedWorkflow(params, scenario)
% -------------------- TX --------------------
txPulse = Pulseformer( ...
"fsym", params.fsym, ...
"fdac", params.fdac, ...
"pulse", "rrc", ...
"pulselength", params.pulseLength, ...
"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", params.awgLpfActive, ...
"kover", params.kover, ...
"bit_resolution", params.awgBitResolution, ...
"upsampling_method", params.awgUpsamplingMethod, ...
"precomp_sinc_rolloff", params.awgPrecompSincRolloff, ...
"normalize2dac", params.awgNormalize2dac).process(digiSig);
elSig = elSig.normalize("mode", "oneone");
elSig = elSig .* params.driverScaling;
% -------------------- Optical Channel --------------------
optSigTx = 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", optSigTx.fs, ...
"fiber_length", params.linkLengthKm, ...
"alpha", params.fiberAlphaDbPerKm, ...
"D", 0, ...
"lambda0", 1310, ...
"gamma", 0, ...
"Dslope", 0.07).process(optSigTx);
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( ...
"active", params.rxFilterActive, ...
"filterType", filtertypes.butterworth, ...
"f_cutoff", params.rxElectricalBandwidthHz, ...
"fs", params.fdac * params.kover, ...
"signal_length", 0, ...
"filtdegree", 4, ...
"lowpass", 1).process(rxSigAfterPd);
scopeLpf = Filter( ...
"active", params.scopeLpfActive, ...
"filterType", filtertypes.butterworth, ...
"f_cutoff", params.scopeBandwidthHz, ...
"fs", params.fadc, ...
"signal_length", 0, ...
"filtdegree", 4, ...
"lowpass", 1);
scpeSigPreMf = Scope( ...
"fsimu", params.fdac * params.kover, ...
"fadc", params.fadc, ...
"adcresolution", params.scopeAdcResolution, ...
"quantbuffer", params.scopeQuantBuffer, ...
"delay", 0, ...
"fixed_delay", 0, ...
"filtertype", filtertypes.butterworth, ...
"samplingdelay", 0, ...
"rand_samplingdelay", 0, ...
"freq_offset", 0, ...
"samp_jitter", 0, ...
"block_dc", 1, ...
"lpf_active", params.scopeLpfActive, ...
"H_lpf", scopeLpf).process(rxSigFiltered);
rxMatchedFilter = Pulseformer( ...
"fsym", params.fsym, ...
"fdac", 2 * params.fsym, ...
"pulse", "rrc", ...
"pulselength", params.pulseLength, ...
"alpha", params.rcalpha, ...
"matched", 1);
scpeSig = rxMatchedFilter.process(scpeSigPreMf);
[syncedSig, ~] = scpeSig.tsynch( ...
"reference", symbols, ...
"fs_ref", params.fsym, ...
"debug_plots", 0);
syncedCenteredSig = syncedSig - mean(syncedSig.signal);
syncedSig = syncedCenteredSig;
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.scenario = scenario;
workflow.params = params;
workflow.Digi_sig = digiSig;
workflow.Symbols = symbols;
workflow.Tx_bits = txBits;
workflow.El_sig = elSig;
workflow.Opt_sig_tx = optSigTx;
workflow.Opt_sig = optSig;
workflow.Rx_sig_after_pd = rxSigAfterPd;
workflow.Rx_sig_filtered = rxSigFiltered;
workflow.Scpe_sig_pre_mf = scpeSigPreMf;
workflow.Scpe_sig = scpeSig;
workflow.Synced_sig_centered = syncedCenteredSig;
workflow.Synced_sig = syncedSig;
workflow.ffe_results = ffeResults;
workflow.vnle_results = vnleResults;
workflow.mlse_results = mlseResults;
end
function params = scenarioParameters(scenario)
params = commonParameters();
switch lower(string(scenario))
case "minimal"
params.ropDbm = 0;
params.rxElectricalBandwidthHz = 40e9;
params.scopeBandwidthHz = 25e9;
case "no-impairment"
params.randomKey = 11;
params.linkLengthKm = 0;
params.fiberAlphaDbPerKm = 0;
params.ropDbm = 3;
params.rxElectricalBandwidthHz = 200e9;
params.scopeBandwidthHz = 200e9;
params.rxFilterActive = false;
params.scopeLpfActive = false;
params.scopeAdcResolution = 24;
params.scopeQuantBuffer = 0.05;
params.awgLpfActive = false;
params.awgBitResolution = 12;
params.awgUpsamplingMethod = upsampling_mode.resample;
params.awgNormalize2dac = true;
case "baseline"
params.ropDbm = 0;
params.rxElectricalBandwidthHz = 40e9;
params.scopeBandwidthHz = 25e9;
case "impaired"
params.ropDbm = -15;
params.rxElectricalBandwidthHz = 12e9;
params.scopeBandwidthHz = 12e9;
otherwise
error("buildReducedImddWorkflow:UnknownScenario", ...
"Unknown reduced IM/DD scenario '%s'.", scenario);
end
end
function params = commonParameters()
params = struct();
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.pulseLength = 12;
params.lenTr = 256;
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));
params.laserWavelengthNm = 1293;
params.opticalPowerDbm = 3;
params.linkLengthKm = 1;
params.fiberAlphaDbPerKm = 0.3;
params.ropDbm = 0;
params.rxElectricalBandwidthHz = 40e9;
params.scopeBandwidthHz = 25e9;
params.rxFilterActive = true;
params.scopeLpfActive = true;
params.scopeAdcResolution = 8;
params.scopeQuantBuffer = 0.1;
params.awgLpfActive = false;
params.awgBitResolution = 8;
params.awgUpsamplingMethod = upsampling_mode.samplehold;
params.awgPrecompSincRolloff = 1;
params.awgNormalize2dac = false;
end

927
Tests/test_dashboard.m Normal file
View File

@@ -0,0 +1,927 @@
function app = test_dashboard(options)
%TEST_DASHBOARD Minimal GUI for test discovery and execution.
% test_dashboard() opens a small dashboard that:
% - discovers implemented, placeholder, and missing class-backed tests
% - lists standalone tests such as integration or helper tests
% - runs the selected test file
% - runs the existing fast/full test profiles
%
% app = test_dashboard("Visible", false) builds the dashboard without
% showing it. This is useful for non-interactive validation.
arguments
options.Visible (1, 1) logical = true
end
app = struct();
app.Inventory = table();
app.SelectedRow = [];
app.LastRunStatusMap = containers.Map('KeyType', 'char', 'ValueType', 'char');
app.LastRunSummaryMap = containers.Map('KeyType', 'char', 'ValueType', 'char');
app.LastRunDateMap = containers.Map('KeyType', 'char', 'ValueType', 'char');
app.LastRunCommitMap = containers.Map('KeyType', 'char', 'ValueType', 'char');
testsRoot = fileparts(mfilename("fullpath"));
repoRoot = fileparts(testsRoot);
reportsRoot = fullfile(testsRoot, 'reports');
latestRunFile = fullfile(testsRoot, '.last_test_run.json');
createUi();
refreshInventory();
if options.Visible
app.Figure.Visible = "on";
end
function createUi()
app.Figure = uifigure( ...
"Name", "IMDD Test Dashboard", ...
"Position", [80, 80, 1360, 820], ...
"Color", [0.965, 0.973, 0.988], ...
"Visible", "off");
app.Layout = uigridlayout(app.Figure, [3, 1]);
app.Layout.RowHeight = {92, "1x", 150};
app.Layout.Padding = [14, 14, 14, 14];
app.Layout.RowSpacing = 12;
app.HeaderPanel = uipanel(app.Layout, ...
"Title", "Suite Overview", ...
"BackgroundColor", [0.985, 0.989, 0.997]);
app.HeaderPanel.Layout.Row = 1;
headerGrid = uigridlayout(app.HeaderPanel, [2, 10]);
headerGrid.RowHeight = {28, 32};
headerGrid.ColumnWidth = {110, 110, 110, 90, 90, 90, "1x", 120, 120, 120};
headerGrid.Padding = [10, 8, 10, 8];
app.ImplementedLabel = makeMetricLabel(headerGrid, "Implemented", 1);
app.PlaceholderLabel = makeMetricLabel(headerGrid, "Placeholder", 2);
app.MissingLabel = makeMetricLabel(headerGrid, "Missing", 3);
app.PassedLabel = makeMetricLabel(headerGrid, "Passed", 4);
app.FailedLabel = makeMetricLabel(headerGrid, "Failed", 5);
app.IncompleteLabel = makeMetricLabel(headerGrid, "Incomplete", 6);
app.TotalLabel = makeMetricLabel(headerGrid, "Total", 7);
app.FilterDropDown = uidropdown(headerGrid, ...
"Items", {'All', 'Implemented', 'Placeholder', 'Missing', 'Standalone'}, ...
"Value", 'All', ...
"ValueChangedFcn", @filterChanged);
app.FilterDropDown.Layout.Row = 2;
app.FilterDropDown.Layout.Column = 8;
app.RefreshButton = uibutton(headerGrid, ...
"Text", "Refresh", ...
"ButtonPushedFcn", @refreshInventoryButtonPushed);
app.RefreshButton.Layout.Row = 2;
app.RefreshButton.Layout.Column = 9;
app.OpenButton = uibutton(headerGrid, ...
"Text", "Open Selected", ...
"ButtonPushedFcn", @openSelectedTest);
app.OpenButton.Layout.Row = 2;
app.OpenButton.Layout.Column = 10;
app.TablePanel = uipanel(app.Layout, ...
"Title", "Discovered Tests", ...
"BackgroundColor", [0.985, 0.989, 0.997]);
app.TablePanel.Layout.Row = 2;
tableGrid = uigridlayout(app.TablePanel, [2, 1]);
tableGrid.RowHeight = {36, "1x"};
tableGrid.Padding = [10, 8, 10, 10];
buttonGrid = uigridlayout(tableGrid, [1, 6]);
buttonGrid.ColumnWidth = {150, 120, 120, 140, 170, "1x"};
buttonGrid.Padding = [0, 0, 0, 0];
buttonGrid.ColumnSpacing = 10;
app.RunSelectedButton = uibutton(buttonGrid, ...
"Text", "Run Selected", ...
"ButtonPushedFcn", @runSelectedTest);
app.RunFastButton = uibutton(buttonGrid, ...
"Text", "Run Fast", ...
"ButtonPushedFcn", @(~, ~) runProfile("fast"));
app.RunFullButton = uibutton(buttonGrid, ...
"Text", "Run Full", ...
"ButtonPushedFcn", @(~, ~) runProfile("full"));
app.RunIntegrationButton = uibutton(buttonGrid, ...
"Text", "Run Integration", ...
"ButtonPushedFcn", @(~, ~) runProfile("integration"));
app.GenerateButton = uibutton(buttonGrid, ...
"Text", "Generate Missing Tests", ...
"ButtonPushedFcn", @generateMissingTests);
app.TableHintLabel = uilabel(buttonGrid, ...
"Text", "Select a row to run or open the corresponding test file.", ...
"HorizontalAlignment", "right", ...
"FontColor", [0.28, 0.32, 0.38]);
app.TableHintLabel.Layout.Column = 6;
app.Table = uitable(tableGrid, ...
"ColumnName", {"Status", "Last Run", "Result", "Date", "Commit", "Scope", "Target", "Test File", "Tags"}, ...
"ColumnEditable", false(1, 9), ...
"ColumnSortable", true(1, 9), ...
"ColumnWidth", {95, 95, 90, 150, 90, 100, 240, 300, "auto"}, ...
"SelectionType", "row", ...
"Multiselect", "off", ...
"CellSelectionCallback", @tableSelectionChanged, ...
"FontName", "Consolas");
app.Table.Layout.Row = 2;
app.LogPanel = uipanel(app.Layout, ...
"Title", "Run Log", ...
"BackgroundColor", [0.985, 0.989, 0.997]);
app.LogPanel.Layout.Row = 3;
logGrid = uigridlayout(app.LogPanel, [1, 1]);
logGrid.Padding = [10, 8, 10, 10];
app.LogArea = uitextarea(logGrid, ...
"Editable", "off", ...
"FontName", "Consolas", ...
"Value", {'IMDD Test Dashboard ready.'});
end
function label = makeMetricLabel(parent, titleText, columnIdx)
label = uilabel(parent, ...
"Text", sprintf("%s: --", titleText), ...
"FontWeight", "bold", ...
"FontSize", 13, ...
"FontColor", [0.12, 0.18, 0.26]);
label.Layout.Row = 1;
label.Layout.Column = columnIdx;
end
function refreshInventoryButtonPushed(~, ~)
refreshInventory();
end
function refreshInventory()
app.Inventory = buildInventory(repoRoot, testsRoot);
app.SelectedRow = [];
loadPersistedRunState();
updateSummary(app.Inventory);
applyFilter();
appendLog(sprintf("[%s] Refreshed test inventory.", stamp()));
end
function filterChanged(~, ~)
applyFilter();
end
function applyFilter()
filtered = app.Inventory;
switch string(app.FilterDropDown.Value)
case "Implemented"
filtered = filtered(filtered.Status == "Implemented", :);
case "Placeholder"
filtered = filtered(filtered.Status == "Placeholder", :);
case "Missing"
filtered = filtered(filtered.Status == "Missing", :);
case "Standalone"
filtered = filtered(filtered.Scope == "Standalone", :);
otherwise
% keep all rows
end
app.Table.Data = filtered(:, ["Status", "LastRun", "ResultSummary", "LastRunDate", "LastRunCommit", "Scope", "Target", "TestFile", "Tags"]);
applyRowStyles(filtered);
end
function updateSummary(inventory)
implementedCount = nnz(inventory.Status == "Implemented");
placeholderCount = nnz(inventory.Status == "Placeholder");
missingCount = nnz(inventory.Status == "Missing");
passedCount = nnz(inventory.LastRun == "Passed");
failedCount = nnz(inventory.LastRun == "Failed");
incompleteCount = nnz(inventory.LastRun == "Incomplete");
totalCount = height(inventory);
app.ImplementedLabel.Text = sprintf("Implemented: %d", implementedCount);
app.PlaceholderLabel.Text = sprintf("Placeholder: %d", placeholderCount);
app.MissingLabel.Text = sprintf("Missing: %d", missingCount);
app.PassedLabel.Text = sprintf("Passed: %d", passedCount);
app.FailedLabel.Text = sprintf("Failed: %d", failedCount);
app.IncompleteLabel.Text = sprintf("Incomplete: %d", incompleteCount);
app.TotalLabel.Text = sprintf("Total: %d", totalCount);
app.PassedLabel.FontColor = metricColor(passedCount > 0, [0.18, 0.56, 0.24]);
app.FailedLabel.FontColor = metricColor(failedCount > 0, [0.78, 0.22, 0.22]);
app.IncompleteLabel.FontColor = metricColor(incompleteCount > 0, [0.72, 0.48, 0.08]);
end
function applyRowStyles(filtered)
removeStyle(app.Table);
greenStyle = uistyle("BackgroundColor", [0.90, 0.97, 0.92]);
amberStyle = uistyle("BackgroundColor", [0.995, 0.95, 0.85]);
redStyle = uistyle("BackgroundColor", [0.985, 0.90, 0.90]);
blueStyle = uistyle("BackgroundColor", [0.90, 0.94, 0.995]);
runningStyle = uistyle("BackgroundColor", [0.89, 0.95, 1.00]);
for rowIdx = 1:height(filtered)
if filtered.LastRun(rowIdx) == "Passed"
addStyle(app.Table, greenStyle, "row", rowIdx);
elseif filtered.LastRun(rowIdx) == "Failed"
addStyle(app.Table, redStyle, "row", rowIdx);
elseif filtered.LastRun(rowIdx) == "Incomplete"
addStyle(app.Table, amberStyle, "row", rowIdx);
elseif filtered.LastRun(rowIdx) == "Running"
addStyle(app.Table, runningStyle, "row", rowIdx);
elseif filtered.Scope(rowIdx) == "Standalone"
addStyle(app.Table, blueStyle, "row", rowIdx);
elseif filtered.Status(rowIdx) == "Implemented"
addStyle(app.Table, greenStyle, "row", rowIdx);
elseif filtered.Status(rowIdx) == "Placeholder"
addStyle(app.Table, amberStyle, "row", rowIdx);
else
addStyle(app.Table, redStyle, "row", rowIdx);
end
end
end
function tableSelectionChanged(src, event)
if isempty(event.Indices)
app.SelectedRow = [];
return
end
selectedRow = event.Indices(1);
visibleRows = src.Data;
selectedTestFile = string(visibleRows.TestFile(selectedRow));
matchIdx = find(app.Inventory.TestFile == selectedTestFile, 1);
if isempty(matchIdx)
selectedTarget = string(visibleRows.Target(selectedRow));
matchIdx = find(app.Inventory.Target == selectedTarget, 1);
end
app.SelectedRow = matchIdx;
end
function openSelectedTest(~, ~)
record = selectedRecord();
if isempty(record)
uialert(app.Figure, "Select a test row first.", "No Selection");
return
end
if strlength(record.TestFile) == 0 || ~isfile(record.TestFile)
uialert(app.Figure, "The selected class does not have a test file yet.", "No Test File");
return
end
edit(record.TestFile);
appendLog(sprintf("[%s] Opened %s", stamp(), record.TestFile));
end
function runSelectedTest(~, ~)
record = selectedRecord();
if isempty(record)
uialert(app.Figure, "Select a test row first.", "No Selection");
return
end
if strlength(record.TestFile) == 0 || ~isfile(record.TestFile)
uialert(app.Figure, "The selected class does not have a runnable test file yet.", "Missing Test");
return
end
titleText = sprintf("Running %s", record.TestFileName);
markRowAsRunning(record);
runWithProgress(titleText, @() runSingleTestFile(record.TestFile), record);
end
function runProfile(profile)
titleText = sprintf("Running profile '%s'", profile);
rowIndices = profileRowIndices(profile);
markRowsAsRunning(rowIndices);
runWithProgress(titleText, @() runProfileInternal(profile, rowIndices), table());
end
function generateMissingTests(~, ~)
setupRepositoryPath();
try
summary = generateTests("verbose", false);
refreshInventory();
appendLog(sprintf("[%s] Generated missing tests: %d new, %d existing skipped, %d classes tracked.", ...
stamp(), summary.generated, summary.skippedExisting, summary.classCount));
catch err
appendLog(sprintf("[%s] generateTests failed.", stamp()));
appendLog(getReport(err, "extended", "hyperlinks", "off"));
uialert(app.Figure, err.message, "Generate Tests Failed");
end
end
function runWithProgress(titleText, runner, record)
try
[output, results] = runner();
appendLog(sprintf("[%s] %s", stamp(), titleText));
appendLog(output);
updateRunStateFromResults(results, record);
writeRunArtifacts(titleText, results);
catch err
appendLog(sprintf("[%s] %s failed.", stamp(), titleText));
appendLog(getReport(err, "extended", "hyperlinks", "off"));
updateFailureState(record);
uialert(app.Figure, err.message, "Run Failed");
end
end
function [output, results] = runProfileInternal(profile, rowIndices)
setupRepositoryPath();
profile = string(profile);
results = matlab.unittest.TestResult.empty(0, 1);
logLines = strings(0, 1);
logLines(end + 1, 1) = sprintf("Running profile '%s' with %d test files.", profile, numel(rowIndices));
for idx = 1:numel(rowIndices)
record = app.Inventory(rowIndices(idx), :);
if strlength(record.TestFile) == 0 || ~isfile(record.TestFile)
continue
end
logLines(end + 1, 1) = sprintf("Running %s", record.TestFileName);
drawnow;
currentResults = runtests(char(record.TestFile));
currentResults = currentResults(:);
if isempty(results)
results = currentResults;
else
results = [results; currentResults]; %#ok<AGROW>
end
updateRunStateFromResults(currentResults, record);
[statusText, summaryText] = summarizeResults(currentResults);
logLines(end + 1, 1) = sprintf("Finished %s: %s (%s)", ...
record.TestFileName, statusText, summaryText);
drawnow;
end
logLines(end + 1, 1) = sprintf("Passed: %d | Failed: %d | Incomplete: %d", ...
nnz([results.Passed]), nnz([results.Failed]), nnz([results.Incomplete]));
output = strjoin(logLines, newline);
end
function [output, results] = runSingleTestFile(testFile)
setupRepositoryPath();
results = runtests(char(testFile));
output = evalc('disp(results);');
end
function record = selectedRecord()
record = table();
if isempty(app.SelectedRow)
return
end
if app.SelectedRow < 1 || app.SelectedRow > height(app.Inventory)
return
end
record = app.Inventory(app.SelectedRow, :);
end
function setupRepositoryPath()
addpath(repoRoot);
addpath(testsRoot);
folders = ["Classes", "Functions", "Datatypes", "Libs"];
for folder = folders
fullFolder = fullfile(repoRoot, folder);
if isfolder(fullFolder)
addpath(genpath(fullFolder));
end
end
end
function appendLog(textIn)
newLines = string(splitlines(string(textIn)));
current = string(app.LogArea.Value);
current = current(current ~= "");
newLines = newLines(newLines ~= "");
combined = [current; newLines];
if isempty(combined)
combined = " ";
end
app.LogArea.Value = cellstr(combined);
drawnow;
end
function markRowAsRunning(record)
if isempty(record) || strlength(record.TestFile) == 0
return
end
updateMappedState(char(record.TestFile), 'Running', '...');
applyStoredRunState();
pause(0.05);
end
function markRowsAsRunning(rowIndices)
for idx = 1:numel(rowIndices)
key = char(app.Inventory.TestFile(rowIndices(idx)));
if ~isempty(key)
updateMappedState(key, 'Running', '...');
end
end
applyStoredRunState();
pause(0.05);
end
function updateRunStateFromResults(results, record)
if ~isempty(record)
[statusText, summaryText] = summarizeResults(results);
updateMappedState(char(record.TestFile), statusText, summaryText);
else
resultNames = string({results.Name})';
uniqueNames = unique(resultNames);
for idx = 1:numel(uniqueNames)
currentResults = results(resultNames == uniqueNames(idx));
[statusText, summaryText] = summarizeResults(currentResults);
targetRows = find(app.Inventory.TestFileName == uniqueNames(idx));
for rowIdx = targetRows'
updateMappedState(char(app.Inventory.TestFile(rowIdx)), statusText, summaryText);
end
end
end
applyStoredRunState();
end
function updateFailureState(record)
if ~isempty(record) && strlength(record.TestFile) > 0
updateMappedState(char(record.TestFile), 'Failed', 'error');
applyStoredRunState();
end
end
function updateMappedState(key, statusText, summaryText)
if isempty(key)
return
end
app.LastRunStatusMap(key) = char(statusText);
app.LastRunSummaryMap(key) = char(summaryText);
end
function applyStoredRunState()
for idx = 1:height(app.Inventory)
key = char(app.Inventory.TestFile(idx));
if isempty(key)
continue
end
if isKey(app.LastRunStatusMap, key)
app.Inventory.LastRun(idx) = string(app.LastRunStatusMap(key));
app.Inventory.ResultSummary(idx) = string(app.LastRunSummaryMap(key));
if isKey(app.LastRunDateMap, key)
app.Inventory.LastRunDate(idx) = string(app.LastRunDateMap(key));
end
if isKey(app.LastRunCommitMap, key)
app.Inventory.LastRunCommit(idx) = string(app.LastRunCommitMap(key));
end
end
end
updateSummary(app.Inventory);
applyFilter();
end
function loadPersistedRunState()
app.LastRunStatusMap = containers.Map('KeyType', 'char', 'ValueType', 'char');
app.LastRunSummaryMap = containers.Map('KeyType', 'char', 'ValueType', 'char');
app.LastRunDateMap = containers.Map('KeyType', 'char', 'ValueType', 'char');
app.LastRunCommitMap = containers.Map('KeyType', 'char', 'ValueType', 'char');
if ~isfile(latestRunFile)
return
end
try
payload = jsondecode(fileread(latestRunFile));
fallbackDate = stringOrEmpty(payload, 'timestamp');
fallbackCommit = "";
if isfield(payload, 'git')
fallbackCommit = stringOrEmpty(payload.git, 'short_commit');
end
if isfield(payload, 'row_states')
rowStates = payload.row_states;
for idx = 1:numel(rowStates)
key = stringOrEmpty(rowStates(idx), 'test_file');
if strlength(key) == 0
continue
end
app.LastRunStatusMap(char(key)) = char(stringOrEmpty(rowStates(idx), 'last_run'));
app.LastRunSummaryMap(char(key)) = char(stringOrEmpty(rowStates(idx), 'result_summary'));
rowDate = stringOrEmpty(rowStates(idx), 'last_run_date');
rowCommit = stringOrEmpty(rowStates(idx), 'last_run_commit');
if strlength(rowDate) == 0
rowDate = fallbackDate;
end
if strlength(rowCommit) == 0
rowCommit = fallbackCommit;
end
app.LastRunDateMap(char(key)) = char(rowDate);
app.LastRunCommitMap(char(key)) = char(rowCommit);
end
end
applyStoredRunState();
catch
% Ignore malformed local cache files and rebuild from scratch.
end
end
function writeRunArtifacts(runLabel, results)
if ~isfolder(reportsRoot)
mkdir(reportsRoot);
end
gitInfo = collectGitMetadata(repoRoot);
report = struct();
report.timestamp = char(datetime("now", "TimeZone", "local", "Format", "yyyy-MM-dd'T'HH:mm:ssXXX"));
rowStates = inventoryRowStates(app.Inventory, report.timestamp, gitInfo.short_commit);
report.run_label = char(runLabel);
report.repo_root = repoRoot;
report.git = gitInfo;
report.totals = struct( ...
'passed', nnz([results.Passed]), ...
'failed', nnz([results.Failed]), ...
'incomplete', nnz([results.Incomplete]));
report.method_results = resultEntries(results);
report.row_states = rowStates;
jsonText = jsonencode(report, PrettyPrint=true);
writeTextFile(latestRunFile, jsonText);
reportFile = fullfile(reportsRoot, sprintf('%s_%s.json', ...
datestr(now, 'yyyy-mm-dd_HHMMSS'), sanitizeFileLabel(runLabel)));
writeTextFile(reportFile, jsonText);
updateMetadataMaps(report.timestamp, gitInfo.short_commit);
appendLog(sprintf("[%s] Saved run report: %s", stamp(), reportFile));
end
function updateMetadataMaps(runDate, shortCommit)
for idx = 1:height(app.Inventory)
key = char(app.Inventory.TestFile(idx));
if isempty(key)
continue
end
if isKey(app.LastRunStatusMap, key)
app.LastRunDateMap(key) = char(string(runDate));
app.LastRunCommitMap(key) = char(string(shortCommit));
end
end
applyStoredRunState();
end
function rowIndices = profileRowIndices(profile)
rowIndices = [];
normalizedTags = "," + erase(app.Inventory.Tags, " ") + ",";
switch string(profile)
case "fast"
rowIndices = find(app.Inventory.Status ~= "Missing" & ...
contains(normalizedTags, ",fast,") & ...
~contains(normalizedTags, ",placeholder,") & ...
~contains(normalizedTags, ",hardware,") & ...
~contains(normalizedTags, ",slow,"));
case "full"
rowIndices = find(app.Inventory.Status ~= "Missing" & ...
~contains(normalizedTags, ",placeholder,") & ...
~contains(normalizedTags, ",hardware,"));
case "integration"
rowIndices = find(app.Inventory.Status ~= "Missing" & ...
contains(normalizedTags, ",integration,") & ...
~contains(normalizedTags, ",placeholder,") & ...
~contains(normalizedTags, ",hardware,"));
case "placeholder"
rowIndices = find(contains(normalizedTags, ",placeholder,"));
end
end
function textOut = stamp()
textOut = char(datetime("now", "Format", "yyyy-MM-dd HH:mm:ss"));
end
end
function color = metricColor(hasRuns, activeColor)
if hasRuns
color = activeColor;
else
color = [0.48, 0.52, 0.58];
end
end
function inventory = buildInventory(repoRoot, testsRoot)
repoRoot = char(string(repoRoot));
testsRoot = char(string(testsRoot));
classesRoot = fullfile(repoRoot, 'Classes');
classFiles = collectMFiles(classesRoot);
records = struct( ...
"Status", {}, ...
"LastRun", {}, ...
"ResultSummary", {}, ...
"LastRunDate", {}, ...
"LastRunCommit", {}, ...
"Scope", {}, ...
"Target", {}, ...
"TargetFile", {}, ...
"TestFile", {}, ...
"TestFileName", {}, ...
"Tags", {});
matchedTestFiles = strings(0, 1);
for idx = 1:numel(classFiles)
classFile = fullfile(classFiles(idx).folder, classFiles(idx).name);
if ~isClassFile(classFile)
continue
end
relativePath = strrep(classFile, [classesRoot, filesep], "");
[relativeDir, className] = fileparts(relativePath);
relativeDirText = char(join(string(relativeDir), ""));
classNameText = char(string(className));
testFile = findMatchingTestFile(testsRoot, relativeDirText, classNameText);
if strlength(testFile) == 0
status = "Missing";
tags = "";
testFileName = "";
else
matchedTestFiles(end + 1, 1) = testFile; %#ok<AGROW>
source = fileread(testFile);
if contains(source, "assumeFail(")
status = "Placeholder";
else
status = "Implemented";
end
tags = strjoin(extractTestTags(source), ", ");
[~, testFileName] = fileparts(testFile);
end
targetLabel = fullfile('Classes', relativeDirText, [classNameText, '.m']);
targetLabel = strrep(targetLabel, "\", "/");
records(end + 1) = struct( ... %#ok<AGROW>
"Status", status, ...
"LastRun", "", ...
"ResultSummary", "", ...
"LastRunDate", "", ...
"LastRunCommit", "", ...
"Scope", "Class", ...
"Target", string(targetLabel), ...
"TargetFile", string(classFile), ...
"TestFile", string(testFile), ...
"TestFileName", string(testFileName), ...
"Tags", string(tags));
end
allTestFiles = dir(fullfile(testsRoot, "**", "*_test.m"));
for idx = 1:numel(allTestFiles)
testFile = fullfile(allTestFiles(idx).folder, allTestFiles(idx).name);
if any(strcmpi(testFile, matchedTestFiles))
continue
end
source = fileread(testFile);
if contains(source, "assumeFail(")
status = "Placeholder";
else
status = "Implemented";
end
[~, testFileName] = fileparts(testFile);
relativeTest = strrep(testFile, [testsRoot, filesep], "");
relativeTest = strrep(relativeTest, "\", "/");
tags = strjoin(extractTestTags(source), ", ");
records(end + 1) = struct( ... %#ok<AGROW>
"Status", status, ...
"LastRun", "", ...
"ResultSummary", "", ...
"LastRunDate", "", ...
"LastRunCommit", "", ...
"Scope", "Standalone", ...
"Target", string(relativeTest), ...
"TargetFile", "", ...
"TestFile", string(testFile), ...
"TestFileName", string(testFileName), ...
"Tags", string(tags));
end
inventory = struct2table(records);
if isempty(inventory)
inventory = table(strings(0, 1), strings(0, 1), strings(0, 1), ...
strings(0, 1), strings(0, 1), strings(0, 1), strings(0, 1), ...
strings(0, 1), strings(0, 1), strings(0, 1), strings(0, 1), ...
"VariableNames", ["Status", "LastRun", "ResultSummary", "LastRunDate", ...
"LastRunCommit", "Scope", "Target", "TargetFile", "TestFile", ...
"TestFileName", "Tags"]);
return
end
statusOrder = containers.Map( ...
{'Implemented', 'Missing', 'Placeholder'}, ...
{1, 2, 3});
scopeOrder = containers.Map({'Class', 'Standalone'}, {1, 2});
statusRank = zeros(height(inventory), 1);
scopeRank = zeros(height(inventory), 1);
for idx = 1:height(inventory)
statusRank(idx) = statusOrder(char(inventory.Status(idx)));
scopeRank(idx) = scopeOrder(char(inventory.Scope(idx)));
end
inventory.statusRank = statusRank;
inventory.scopeRank = scopeRank;
inventory = sortrows(inventory, {'scopeRank', 'statusRank', 'Target'});
inventory = removevars(inventory, {'statusRank', 'scopeRank'});
end
function testFile = findMatchingTestFile(testsRoot, relativeDir, className)
testsRoot = char(string(testsRoot));
relativeDir = char(join(string(relativeDir), ""));
className = char(string(className));
testDir = fullfile(testsRoot, relativeDir);
testFile = "";
if ~isfolder(testDir)
return
end
exactFile = fullfile(testDir, [className, '_test.m']);
if isfile(exactFile)
testFile = string(exactFile);
return
end
candidates = dir(fullfile(testDir, '*_test.m'));
if isempty(candidates)
return
end
candidateNames = string({candidates.name});
suffix = ['_', className, '_test.m'];
matchMask = endsWith(candidateNames, suffix, "IgnoreCase", true);
if any(matchMask)
firstMatch = candidates(find(matchMask, 1));
testFile = string(fullfile(firstMatch.folder, firstMatch.name));
end
end
function tags = extractTestTags(source)
tokens = regexp(source, "TestTags\s*=\s*\{([^}]*)\}", "tokens", "once");
if isempty(tokens)
tags = strings(0, 1);
return
end
tags = regexp(tokens{1}, "'([^']*)'", "tokens");
tags = string(cellfun(@(entry) entry{1}, tags, "UniformOutput", false));
end
function tf = isClassFile(filePath)
source = fileread(filePath);
tf = contains(source, "classdef");
end
function files = collectMFiles(rootFolder)
pathString = genpath(rootFolder);
folders = regexp(pathString, pathsep, "split");
folders = folders(~cellfun("isempty", folders));
files = dir(fullfile(rootFolder, "*.m"));
files = files([]); %#ok<NASGU>
files = dir(fullfile(rootFolder, "*.m"));
files = files([]);
for idx = 1:numel(folders)
currentFiles = dir(fullfile(folders{idx}, "*.m"));
if isempty(currentFiles)
continue
end
currentFiles = currentFiles(~[currentFiles.isdir]);
files = [files; currentFiles]; %#ok<AGROW>
end
end
function [statusText, summaryText] = summarizeResults(results)
passedCount = nnz([results.Passed]);
failedCount = nnz([results.Failed]);
incompleteCount = nnz([results.Incomplete]);
if failedCount > 0
statusText = "Failed";
elseif incompleteCount > 0
statusText = "Incomplete";
else
statusText = "Passed";
end
summaryText = sprintf('%d/%d/%d', passedCount, failedCount, incompleteCount);
end
function out = stringOrEmpty(structValue, fieldName)
if isfield(structValue, fieldName)
out = string(structValue.(fieldName));
else
out = "";
end
end
function rows = inventoryRowStates(inventory, lastRunDate, lastRunCommit)
rows = repmat(struct( ...
'test_file', '', ...
'target', '', ...
'scope', '', ...
'last_run', '', ...
'result_summary', '', ...
'last_run_date', '', ...
'last_run_commit', ''), height(inventory), 1);
for idx = 1:height(inventory)
rows(idx).test_file = char(inventory.TestFile(idx));
rows(idx).target = char(inventory.Target(idx));
rows(idx).scope = char(inventory.Scope(idx));
rows(idx).last_run = char(inventory.LastRun(idx));
rows(idx).result_summary = char(inventory.ResultSummary(idx));
rows(idx).last_run_date = char(string(lastRunDate));
rows(idx).last_run_commit = char(string(lastRunCommit));
end
end
function entries = resultEntries(results)
entries = repmat(struct( ...
'name', '', ...
'passed', false, ...
'failed', false, ...
'incomplete', false, ...
'duration_seconds', 0), numel(results), 1);
for idx = 1:numel(results)
entries(idx).name = char(string(results(idx).Name));
entries(idx).passed = results(idx).Passed;
entries(idx).failed = results(idx).Failed;
entries(idx).incomplete = results(idx).Incomplete;
entries(idx).duration_seconds = durationToSeconds(results(idx).Duration);
end
end
function gitInfo = collectGitMetadata(repoRoot)
gitInfo = struct();
gitInfo.commit = strtrim(runCommand(sprintf('git -C "%s" rev-parse HEAD', repoRoot)));
gitInfo.short_commit = strtrim(runCommand(sprintf('git -C "%s" rev-parse --short HEAD', repoRoot)));
gitInfo.branch = strtrim(runCommand(sprintf('git -C "%s" rev-parse --abbrev-ref HEAD', repoRoot)));
statusOutput = runCommand(sprintf('git -C "%s" status --porcelain', repoRoot));
gitInfo.dirty = ~isempty(strtrim(statusOutput));
end
function output = runCommand(commandText)
[status, output] = system(commandText);
if status ~= 0
output = '';
end
end
function writeTextFile(filePath, content)
fid = fopen(filePath, 'w');
assert(fid ~= -1, 'Could not write file: %s', filePath);
cleaner = onCleanup(@() fclose(fid)); %#ok<NASGU>
fprintf(fid, '%s', content);
end
function label = sanitizeFileLabel(labelText)
label = regexprep(char(string(labelText)), '[^A-Za-z0-9]+', '_');
label = regexprep(label, '^_+|_+$', '');
if isempty(label)
label = 'run';
end
end
function value = durationToSeconds(durationValue)
try
value = seconds(durationValue);
if isa(value, 'duration')
value = str2double(char(value));
end
catch
try
value = posixtime(datetime(durationValue) - datetime(0, 0, 0));
catch
durationText = char(string(durationValue));
tokens = regexp(durationText, '(\d+):(\d+):([\d\.]+)', 'tokens', 'once');
if isempty(tokens)
value = NaN;
else
value = str2double(tokens{1}) * 3600 + ...
str2double(tokens{2}) * 60 + ...
str2double(tokens{3});
end
end
end
if ~isnumeric(value) || ~isscalar(value)
value = NaN;
end
end

View File

@@ -1,4 +0,0 @@