diff --git a/.gitignore b/.gitignore index de1fa6c..6142cc1 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,8 @@ sccprj/ codegen/ -.mat +.mat + +# Local test dashboard status tracking +Tests/.last_test_run.json +Tests/reports/ diff --git a/Classes/02_optical/Optical_Demultiplex.m b/Classes/02_optical/Optical_Demultiplex.m index 8e272d2..d1762f7 100644 --- a/Classes/02_optical/Optical_Demultiplex.m +++ b/Classes/02_optical/Optical_Demultiplex.m @@ -46,7 +46,7 @@ classdef Optical_Demultiplex < handle if isempty(obj.wavelengthplan) obj.wavelengthplan = signalclass_in.lambda; %meter 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; end end diff --git a/Functions/Theory/dispersion_10km.m b/Functions/Theory/Dispersion/dispersion_10km.m similarity index 100% rename from Functions/Theory/dispersion_10km.m rename to Functions/Theory/Dispersion/dispersion_10km.m diff --git a/Functions/Theory/dispersion_contour.m b/Functions/Theory/Dispersion/dispersion_contour.m similarity index 100% rename from Functions/Theory/dispersion_contour.m rename to Functions/Theory/Dispersion/dispersion_contour.m diff --git a/Functions/Theory/dispersion_first_notch_10km.m b/Functions/Theory/Dispersion/dispersion_first_notch_10km.m similarity index 100% rename from Functions/Theory/dispersion_first_notch_10km.m rename to Functions/Theory/Dispersion/dispersion_first_notch_10km.m diff --git a/Functions/Theory/dispersion_power_fading.m b/Functions/Theory/Dispersion/dispersion_power_fading.m similarity index 100% rename from Functions/Theory/dispersion_power_fading.m rename to Functions/Theory/Dispersion/dispersion_power_fading.m diff --git a/Functions/Theory/dispersion_wavelength_notch.m b/Functions/Theory/Dispersion/dispersion_wavelength_notch.m similarity index 100% rename from Functions/Theory/dispersion_wavelength_notch.m rename to Functions/Theory/Dispersion/dispersion_wavelength_notch.m diff --git a/Functions/Theory/dispersion_wdm.m b/Functions/Theory/Dispersion/dispersion_wdm.m similarity index 100% rename from Functions/Theory/dispersion_wdm.m rename to Functions/Theory/Dispersion/dispersion_wdm.m diff --git a/power_fading_gif.m b/Functions/Theory/Dispersion/power_fading_gif.m similarity index 100% rename from power_fading_gif.m rename to Functions/Theory/Dispersion/power_fading_gif.m diff --git a/power_fading_gif_lambda_variation.m b/Functions/Theory/Dispersion/power_fading_gif_lambda_variation.m similarity index 100% rename from power_fading_gif_lambda_variation.m rename to Functions/Theory/Dispersion/power_fading_gif_lambda_variation.m diff --git a/Functions/Theory/Dissertation/loss_curve_digitized.m b/Functions/Theory/Dissertation/loss_curve_digitized.m deleted file mode 100644 index 71e05cc..0000000 --- a/Functions/Theory/Dissertation/loss_curve_digitized.m +++ /dev/null @@ -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 \ No newline at end of file diff --git a/Functions/Theory/Dissertation/wpd_datasets.csv b/Functions/Theory/Dissertation/wpd_datasets.csv deleted file mode 100644 index f204cff..0000000 --- a/Functions/Theory/Dissertation/wpd_datasets.csv +++ /dev/null @@ -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,, diff --git a/Functions/Theory/calcFWM/FWM_products.m b/Functions/Theory/calcFWM/FWM_products.m new file mode 100644 index 0000000..5429d0e --- /dev/null +++ b/Functions/Theory/calcFWM/FWM_products.m @@ -0,0 +1,69 @@ + + + +w0 = [1290:2:1290+15*2]'; +w0 = [1290:2:1290+15*2]'; +w0 = [ 1302 1304 1306 1308]'; +%w0 = [1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16]'; +m = 0.5*(numel(w0)^3 - numel(w0)^2); +a = [1,1,1]'; + +w = w0; + + + + +for o = 2:3 + + p = nchoosek(w,3); + + q = []; + parfor i = 1:size(p,1) + + q_ = perms(p(i,:)); + q = [q;q_]; + + end + p = q; + %p = unique(q,"rows"); + w_ = p(:,1) + p(:,2) - p(:,3); + a_ = ones(size(w_)).* 1/o; + + w = [w ; w_]; + a = [a ; a_]; +% w = unique(w); +% a = unique(a); + + m(end+1) = 0.5*(numel(w)^3 - numel(w)^2); + +end + +figure(11) +hold on + +lambda = min(w):max(w); + +gen = sum(lambda == w,1); +gen(gen==0) = NaN; +stem(lambda,gen,"filled",'LineWidth',1,'Marker','o','MarkerSize',2,'LineStyle',':') + +initial = sum(lambda == w0,1); +initial(initial==0) = NaN; +stem(lambda,initial,"filled",'LineWidth',1.5,'MarkerSize',5,'Marker','^'); + +xlabel('Wavelength'); +ylabel('number of FWM products'); + +grid minor +legend('Generated Products', 'Initial Channel Position') +AxesMain = gca; +fig = gcf; +fontsize(AxesMain,8,"points") + +fig.Units = "centimeters"; +fig.Position = [2 2 8.5 7]; + + + + + diff --git a/Functions/Theory/calcFWM/JLT_statistics_laser_and_zdw.m b/Functions/Theory/calcFWM/JLT_statistics_laser_and_zdw.m new file mode 100644 index 0000000..79d052f --- /dev/null +++ b/Functions/Theory/calcFWM/JLT_statistics_laser_and_zdw.m @@ -0,0 +1,43 @@ +%% Laser Offset Statistics + + figure +for i = 1 + res = 1.7e6; + n_chann = 16; + df_T_exact = (-n_chann/2+0.5:n_chann/2).* 200e9; + + for key = 1:100 + laser_frequency_imperfection(key,:) = res .* round(randn(1,n_chann)*i*100); + df_T(key,:) = df_T_exact + laser_frequency_imperfection(key,:); + end + + hold on + histogram(laser_frequency_imperfection.*1e-6,100,"Normalization","probability","EdgeColor","none","FaceAlpha",0.3,'DisplayName',['Std. Dev.: ',num2str(mean(std(laser_frequency_imperfection))*1e-6),' MHz']); + xlabel('Laser Frequency Offset in MHz'); + ylabel('Probability'); + title(['Laser deviations from exact grid.']) + +end + +%% ZDW Statistics + +for k = 1:1000 + + % Set parameters + meanUniformMin = 1309; + meanUniformMax = 1315; + meanValue = 1310; + sigma = 2; + + % Seed the random number generator (assuming Mersenne Twister) + rng(k); + + % Generate normally distributed random numbers + randomNumbers(k,:) = normrnd(meanValue, sigma, [n_chann, 1]).'; + +end + +figure; +histogram(randomNumbers,100,"Normalization","probability","EdgeColor","none"); +xlabel('ZDW in nm'); +ylabel('Probability'); \ No newline at end of file diff --git a/Functions/Theory/calcFWM/analytical_calculation_paper.m b/Functions/Theory/calcFWM/analytical_calculation_paper.m new file mode 100644 index 0000000..d088424 --- /dev/null +++ b/Functions/Theory/calcFWM/analytical_calculation_paper.m @@ -0,0 +1,65 @@ +%%FWM analysis from "Analytical Calculation of the Number of +%%Four-Wave-Mixing Products in Optical Multichannel Communication Systems" + +N_ = [4,8,16]; + +df_hz = 400e9; +center_nm = 1310; + +figure() +for i = 1:length(N_) + + vec = (2*N_(i)-1:-1:2-N_(i)) -(N_(i)/2+0.5); + channelplan_hz = nm2hz(center_nm) + (vec * df_hz) ; + channelplan_nm = hz2nm(channelplan_hz); + + [Mndg,Mdg] = getProducts(N_(i)); + total(i) = sum(Mndg) + sum(Mdg); + subplot(1,length(N_),i) + xline(calcWavelengthPlan(N_(i), df_hz, center_nm)); + hold on + stem(channelplan_nm,(Mndg+Mdg),'filled','LineWidth',1,'Marker','o','MarkerSize',2) + stem(channelplan_nm,(Mdg),'filled','LineWidth',1,'Marker','none'); + ylim([0,100]) + xlim([1260, 1365]); + grid off + xlabel('O-band wavelength region in nm'); + ylabel('Number of FWM products'); + title([num2str(N_(i)),' ch.']) + +end + + +function [Mndg,Mdg] = getProducts(N) + + s = abs(2-N-1) ; + + for n = 2-N:2*N-1 + + if n<-N + Mdg(n+s) = 0; + elseif (-N <= n)&&(n <= 0) + Mdg(n+s) = N - ceil((N-n)/2); + elseif (1 <= n)&&(n <= N) + Mdg(n+s) = N - 1 - floor(n/2) - ceil((N-n)/2); + elseif (N < n)&&(n <= 2*N) + Mdg(n+s) = N - floor(n/2); + elseif n > 2*N + Mdg(n+s) = 0; + end + + if n<-N + Mndg(n+s) = 0; + elseif (-N <= n)&&(n < 1) + Mndg(n+s) = ceil((N^2 + n^2 - 2*N - 2*n + 2*N*n)/4); + elseif (1 <= n)&&(n <= N) + Mndg(n+s) = ceil(((N^2 - 6*N - 2*n^2 + 2*n + 4)/4) + floor((N*n)/2)); + elseif (N < n)&&(n <= 2*N) + Mndg(n+s) = floor(N^2 + n^2 /4 - N*n); + elseif n > 2*N + Mndg(n+s) = 0; + end + + end +end + diff --git a/Functions/Theory/calcFWM/calcFwmEfficiency.m b/Functions/Theory/calcFWM/calcFwmEfficiency.m new file mode 100644 index 0000000..b1a3560 --- /dev/null +++ b/Functions/Theory/calcFWM/calcFwmEfficiency.m @@ -0,0 +1,23 @@ +function [eta, deltaBeta] = calcFwmEfficiency(f_i, f_j, f_k, f_0, Ds, alphaDbPerKm, Lkm) +% FWM efficiency including phase mismatch and attenuation. +% alphaDbPerKm is the power attenuation in dB/km, Lkm is the fiber length in km. + +deltaBeta = calcPhaseMatching(f_i, f_j, f_k, f_0, Ds); + +alphaNpPerM = alphaDbPerKm .* log(10) ./ 10 ./ 1e3; +Lm = Lkm .* 1e3; + +denominator = alphaNpPerM.^2 + deltaBeta.^2; +term1 = alphaNpPerM.^2 ./ denominator; + +loss_term = 1 - exp(-alphaNpPerM .* Lm); + +if abs(alphaNpPerM) < eps + term2 = 4 .* sin(deltaBeta .* Lm ./ 2).^2 ./ max((alphaNpPerM .* Lm).^2, eps); +else + term2 = 1 + 4 .* exp(-alphaNpPerM .* Lm) .* sin(deltaBeta .* Lm ./ 2).^2 ./ (loss_term.^2); +end + +eta = term1 .* term2; + +end diff --git a/Functions/Theory/calcFWM/calcFwmPower.m b/Functions/Theory/calcFWM/calcFwmPower.m new file mode 100644 index 0000000..a6b65b7 --- /dev/null +++ b/Functions/Theory/calcFWM/calcFwmPower.m @@ -0,0 +1,48 @@ +function [P_fwm, eta, deltaBeta, Leff] = calcFwmPower( ... + f_i, f_j, f_k, f_0, Ds, alphaDbPerKm, Lkm, ... + P_i, P_j, P_k, gammaWInvKmInv, degeneracyFactor) +% Calculate FWM power for a fiber with attenuation and phase mismatch. +% +% Inputs: +% f_i, f_j, f_k, f_0 : frequencies in Hz +% Ds : dispersion slope in ps / (nm^2 km) +% alphaDbPerKm : attenuation in dB/km +% Lkm : fiber length in km +% P_i, P_j, P_k : launch powers in W +% gammaWInvKmInv : nonlinear coefficient in 1/(W km) +% degeneracyFactor : typically 3 for degenerate FWM, 6 for non-degenerate + +if nargin < 8 || isempty(P_i) + P_i = 1; +end +if nargin < 9 || isempty(P_j) + P_j = P_i; +end +if nargin < 10 || isempty(P_k) + P_k = 1; +end +if nargin < 11 || isempty(gammaWInvKmInv) + gammaWInvKmInv = 1; +end +if nargin < 12 || isempty(degeneracyFactor) + degeneracyFactor = 1; +end + +[eta, deltaBeta] = calcFwmEfficiency(f_i, f_j, f_k, f_0, Ds, alphaDbPerKm, Lkm); + +alphaNpPerM = alphaDbPerKm .* log(10) ./ 10 ./ 1e3; +Lm = Lkm .* 1e3; +gammaWInvMInv = gammaWInvKmInv ./ 1e3; + +if abs(alphaNpPerM) < eps + Leff = Lm; +else + Leff = (1 - exp(-alphaNpPerM .* Lm)) ./ alphaNpPerM; +end + +P_fwm = degeneracyFactor .* eta .* ... + (gammaWInvMInv .* Leff).^2 .* ... + P_i .* P_j .* P_k .* ... + exp(-alphaNpPerM .* Lm); + +end diff --git a/Functions/Theory/calcFWM/calcPhaseMatching.m b/Functions/Theory/calcFWM/calcPhaseMatching.m new file mode 100644 index 0000000..4c2fd4f --- /dev/null +++ b/Functions/Theory/calcFWM/calcPhaseMatching.m @@ -0,0 +1,20 @@ +function deltaBeta = calcPhaseMatching(f_i, f_j, f_k, f_0, Ds) +% Approximate phase mismatch for degenerate FWM close to the ZDW. +% Inputs are frequencies in Hz. +% f_i, f_j : pump frequencies (equal in the degenerate case) +% f_k : signal frequency +% f_0 : zero-dispersion frequency +% Ds : dispersion slope in ps / (nm^2 km) + +c = physconst('LightSpeed'); + +pump_frequency = 0.5 .* (f_i + f_j); +lambda_zdw_m = c ./ f_0; +dispersion_slope_si = Ds .* 1e3; + +deltaBeta = -(2 .* pi .* lambda_zdw_m.^4 ./ c.^2) .* ... + dispersion_slope_si .* ... + (pump_frequency - f_0) .* ... + (pump_frequency - f_k).^2; + +end diff --git a/Functions/Theory/calcFWM/scriptFWM.m b/Functions/Theory/calcFWM/scriptFWM.m new file mode 100644 index 0000000..abe3685 --- /dev/null +++ b/Functions/Theory/calcFWM/scriptFWM.m @@ -0,0 +1,104 @@ +clear; +clc; + +% Sweep the degenerate pump frequency around its nominal wavelength. +pump_detuning_hz = (-800:0.01:800) .* 1e9; + +% Degenerate FWM setup: two pump photons at f_p and one signal at f_s +% generate an idler at f_i = 2*f_p - f_s. +pump_wavelength_nm = 1310; +signal_wavelength_nm = 1308; +zdw_wavelength_nm = 1310; + +f_pump_nominal = wavelength2frequency(pump_wavelength_nm, 'nm'); +f_signal_scalar = wavelength2frequency(signal_wavelength_nm, 'nm'); +f_zdw_scalar = wavelength2frequency(zdw_wavelength_nm, 'nm'); + +f_pump = f_pump_nominal + pump_detuning_hz; +f_signal = f_signal_scalar .* ones(size(f_pump)); +f_zdw = f_zdw_scalar .* ones(size(f_pump)); +f_idler = 2 .* f_pump - f_signal; + +% Fiber parameters +dispersion_slope_ps_nm2_km = 0.07; +attenuation_db_per_km = 0.21; +fiber_length_km = 10; + +% Launch powers and nonlinear coefficient +pump_power_dbm = 10; +signal_power_dbm = 10; +pump_power_w = dbm2watt(pump_power_dbm); +signal_power_w = dbm2watt(signal_power_dbm); +gamma_w_inv_km_inv = 1.3; +degeneracy_factor = 3; + +[P_fwm, eta, delta_beta, L_eff_m] = calcFwmPower( ... + f_pump, f_pump, f_signal, f_zdw, ... + dispersion_slope_ps_nm2_km, attenuation_db_per_km, fiber_length_km, ... + pump_power_w, pump_power_w, signal_power_w, ... + gamma_w_inv_km_inv, degeneracy_factor); + +f_pump_thz = f_pump .* 1e-12; +f_zdw_thz = f_zdw_scalar .* 1e-12; +f_signal_thz = f_signal_scalar .* 1e-12; +idler_power_dbm = 10 .* log10(max(P_fwm, realmin) ./ 1e-3); + +figure; +tiledlayout(2,1); + +ax1 = nexttile; +plot(ax1, f_pump_thz, eta, 'LineWidth', 2); +hold(ax1, 'on'); +xline(ax1, f_zdw_thz, '--r', 'ZDW', 'LineWidth', 1.2, ... + 'LabelOrientation', 'horizontal', 'LabelVerticalAlignment', 'bottom'); +xline(ax1, f_signal_thz, '--k', 'Signal', 'LineWidth', 1.2, ... + 'LabelOrientation', 'horizontal', 'LabelVerticalAlignment', 'middle'); +ylabel(ax1, 'FWM efficiency'); +grid(ax1, 'on'); +title(ax1, 'FWM Efficiency and Idler Power versus Pump Frequency'); + +ax2 = nexttile; +plot(ax2, f_pump_thz, idler_power_dbm, 'LineWidth', 2); +hold(ax2, 'on'); +xline(ax2, f_zdw_thz, '--r', 'ZDW', 'LineWidth', 1.2, ... + 'LabelOrientation', 'horizontal', 'LabelVerticalAlignment', 'bottom'); +xline(ax2, f_signal_thz, '--k', 'Signal', 'LineWidth', 1.2, ... + 'LabelOrientation', 'horizontal', 'LabelVerticalAlignment', 'middle'); +xlabel(ax2, 'Pump frequency (THz)'); +ylabel(ax2, 'FWM idler power (dBm)'); +grid(ax2, 'on'); + +fprintf('Pump wavelength : %.3f nm -> %.6f THz\n', ... + pump_wavelength_nm, f_pump_nominal .* 1e-12); +fprintf('Signal wavelength : %.3f nm -> %.6f THz\n', ... + signal_wavelength_nm, f_signal_scalar .* 1e-12); +fprintf('ZDW wavelength : %.3f nm -> %.6f THz\n', ... + zdw_wavelength_nm, f_zdw_scalar .* 1e-12); +fprintf('Pump launch power : %.2f dBm -> %.4g W\n', ... + pump_power_dbm, pump_power_w); +fprintf('Signal launch power : %.2f dBm -> %.4g W\n', ... + signal_power_dbm, signal_power_w); +fprintf('Peak FWM efficiency : %.4g\n', max(eta)); +fprintf('Peak FWM idler power : %.4g W (%.2f dBm)\n', ... + max(P_fwm), 10 .* log10(max(P_fwm) ./ 1e-3)); +fprintf('Effective fiber length : %.4f km\n', L_eff_m ./ 1e3); +fprintf('Idler wavelength range : %.3f nm to %.3f nm\n', ... + min(frequency2wavelength(f_idler, 'nm')), max(frequency2wavelength(f_idler, 'nm'))); +fprintf('Max |delta beta| : %.4g 1/m\n', max(abs(delta_beta))); + +function wavelength = frequency2wavelength(frequency, outputUnit) + c = physconst('LightSpeed'); + wavelength = c ./ frequency; + + switch lower(outputUnit) + case 'm' + case 'nm' + wavelength = wavelength .* 1e9; + otherwise + error('Unsupported output unit "%s". Use "m" or "nm".', outputUnit); + end +end + +function power_w = dbm2watt(power_dbm) + power_w = 1e-3 .* 10.^(power_dbm ./ 10); +end diff --git a/Functions/Theory/calcFWM/validate_fwm.m b/Functions/Theory/calcFWM/validate_fwm.m new file mode 100644 index 0000000..928a232 --- /dev/null +++ b/Functions/Theory/calcFWM/validate_fwm.m @@ -0,0 +1,124 @@ +%% Validate the analytical FWM product count against a brute-force reference +% The paper counts channel combinations, not only unique output frequencies: +% non-degenerate: i < j, k ~= i, k ~= j, n = i + j - k +% degenerate: i == j, k ~= i, n = 2*i - k + +clear; +clc; + +N_values = [4, 8, 16]; +plot_N = 8; + +fprintf('Validating analytical FWM product count from Goebel and Hanik (2008)\n'); + +for idxN = 1:numel(N_values) + N = N_values(idxN); + fprintf('\nN = %d\n', N); + + [Mndg_ana, Mdg_ana] = getProducts(N); + [Mndg_brute, Mdg_brute, n_values] = getProductsBruteForce(N); + + diff_ndg = Mndg_ana - Mndg_brute; + diff_dg = Mdg_ana - Mdg_brute; + + fprintf(' Analytical total : %d\n', sum(Mndg_ana) + sum(Mdg_ana)); + fprintf(' Brute-force total : %d\n', sum(Mndg_brute) + sum(Mdg_brute)); + + if all(diff_ndg == 0) && all(diff_dg == 0) + fprintf(' Match : yes\n'); + else + fprintf(' Match : no\n'); + fprintf(' Non-degenerate diff: %s\n', mat2str(diff_ndg)); + fprintf(' Degenerate diff : %s\n', mat2str(diff_dg)); + end + + if N == plot_N + plotComparison(n_values, Mndg_ana, Mdg_ana, Mndg_brute, Mdg_brute, N); + end +end + +function [Mndg, Mdg, n_values] = getProductsBruteForce(N) + n_values = (2 - N):(2*N - 1); + Mndg = zeros(size(n_values)); + Mdg = zeros(size(n_values)); + + for idx = 1:numel(n_values) + n = n_values(idx); + + % Degenerate products: two identical pumps and one different channel. + for i = 1:N + k = 2*i - n; + if isValidChannel(k, N) && (k ~= i) + Mdg(idx) = Mdg(idx) + 1; + end + end + + % Non-degenerate products: unordered pump pair plus one third channel. + for i = 1:N + for j = (i + 1):N + k = i + j - n; + if isValidChannel(k, N) && (k ~= i) && (k ~= j) + Mndg(idx) = Mndg(idx) + 1; + end + end + end + end +end + +function tf = isValidChannel(channel_idx, N) + tf = (channel_idx >= 1) && (channel_idx <= N) && (channel_idx == round(channel_idx)); +end + +function plotComparison(n_values, Mndg_ana, Mdg_ana, Mndg_brute, Mdg_brute, N) + figure; + + subplot(1,2,1); + stem(n_values, Mndg_ana + Mdg_ana, 'filled', 'LineWidth', 1, 'Marker', 'o', 'MarkerSize', 2); + hold on; + stem(n_values, Mdg_ana, 'filled', 'LineWidth', 1, 'Marker', 'none'); + title(['Analytical (N=', num2str(N), ')']); + xlabel('Product index n'); + ylabel('Number of FWM products'); + legend('Total', 'Degenerate'); + grid on; + + subplot(1,2,2); + stem(n_values, Mndg_brute + Mdg_brute, 'filled', 'LineWidth', 1, 'Marker', 'o', 'MarkerSize', 2); + hold on; + stem(n_values, Mdg_brute, 'filled', 'LineWidth', 1, 'Marker', 'none'); + title(['Brute force (N=', num2str(N), ')']); + xlabel('Product index n'); + ylabel('Number of FWM products'); + legend('Total', 'Degenerate'); + grid on; +end + +function [Mndg, Mdg] = getProducts(N) + s = abs(2 - N - 1); + + for n = 2 - N:2*N - 1 + if n < -N + Mdg(n + s) = 0; + elseif (-N <= n) && (n <= 0) + Mdg(n + s) = N - ceil((N - n)/2); + elseif (1 <= n) && (n <= N) + Mdg(n + s) = N - 1 - floor(n/2) - ceil((N - n)/2); + elseif (N < n) && (n <= 2*N) + Mdg(n + s) = N - floor(n/2); + elseif n > 2*N + Mdg(n + s) = 0; + end + + if n < -N + Mndg(n + s) = 0; + elseif (-N <= n) && (n < 1) + Mndg(n + s) = ceil((N^2 + n^2 - 2*N - 2*n + 2*N*n)/4); + elseif (1 <= n) && (n <= N) + Mndg(n + s) = ceil(((N^2 - 6*N - 2*n^2 + 2*n + 4)/4) + floor((N*n)/2)); + elseif (N < n) && (n <= 2*N) + Mndg(n + s) = floor(N^2 + n^2/4 - N*n); + elseif n > 2*N + Mndg(n + s) = 0; + end + end +end diff --git a/Functions/Theory/calcFWM/wavelength2frequency.m b/Functions/Theory/calcFWM/wavelength2frequency.m new file mode 100644 index 0000000..fe0f1eb --- /dev/null +++ b/Functions/Theory/calcFWM/wavelength2frequency.m @@ -0,0 +1,18 @@ +function frequency = wavelength2frequency(wavelength, inputUnit) +% Convert wavelength to optical frequency. +% Supported units: m, nm. + +c = physconst('LightSpeed'); + +switch lower(inputUnit) + case 'm' + wavelength_m = wavelength; + case 'nm' + wavelength_m = wavelength .* 1e-9; + otherwise + error('Unsupported input unit "%s". Use "m" or "nm".', inputUnit); +end + +frequency = c ./ wavelength_m; + +end diff --git a/Libs/mutual information rate TUM/LICENSE.txt b/Functions/Theory/mutual information rate TUM/LICENSE.txt similarity index 100% rename from Libs/mutual information rate TUM/LICENSE.txt rename to Functions/Theory/mutual information rate TUM/LICENSE.txt diff --git a/Libs/mutual information rate TUM/README.md b/Functions/Theory/mutual information rate TUM/README.md similarity index 100% rename from Libs/mutual information rate TUM/README.md rename to Functions/Theory/mutual information rate TUM/README.md diff --git a/Libs/mutual information rate TUM/air.m b/Functions/Theory/mutual information rate TUM/air.m similarity index 100% rename from Libs/mutual information rate TUM/air.m rename to Functions/Theory/mutual information rate TUM/air.m diff --git a/Libs/mutual information rate TUM/example_mi_cg_complex_16qam.m b/Functions/Theory/mutual information rate TUM/example_mi_cg_complex_16qam.m similarity index 100% rename from Libs/mutual information rate TUM/example_mi_cg_complex_16qam.m rename to Functions/Theory/mutual information rate TUM/example_mi_cg_complex_16qam.m diff --git a/Libs/mutual information rate TUM/example_mi_cg_real_2D_awgn_vs_phasenoise.m b/Functions/Theory/mutual information rate TUM/example_mi_cg_real_2D_awgn_vs_phasenoise.m similarity index 100% rename from Libs/mutual information rate TUM/example_mi_cg_real_2D_awgn_vs_phasenoise.m rename to Functions/Theory/mutual information rate TUM/example_mi_cg_real_2D_awgn_vs_phasenoise.m diff --git a/Libs/mutual information rate TUM/example_silas.m b/Functions/Theory/mutual information rate TUM/example_silas.m similarity index 100% rename from Libs/mutual information rate TUM/example_silas.m rename to Functions/Theory/mutual information rate TUM/example_silas.m diff --git a/Libs/mutual information rate TUM/mi_cg.m b/Functions/Theory/mutual information rate TUM/mi_cg.m similarity index 100% rename from Libs/mutual information rate TUM/mi_cg.m rename to Functions/Theory/mutual information rate TUM/mi_cg.m diff --git a/Libs/mutual information rate TUM/silas_example_mi_cg_pam.m b/Functions/Theory/mutual information rate TUM/silas_example_mi_cg_pam.m similarity index 100% rename from Libs/mutual information rate TUM/silas_example_mi_cg_pam.m rename to Functions/Theory/mutual information rate TUM/silas_example_mi_cg_pam.m diff --git a/TEST_TODO.md b/TEST_TODO.md index 786d631..179ecf9 100644 --- a/TEST_TODO.md +++ b/TEST_TODO.md @@ -8,8 +8,8 @@ It should reflect the repository state now, not the original plan. Fast profile status: - `run_all_tests('profile','fast')` -- latest verified result: `Passed: 59 | Failed: 0 | Incomplete: 0` -- explicit placeholder tests remaining: `70` +- latest verified result: `Passed: 82 | Failed: 0 | Incomplete: 0` +- explicit placeholder tests remaining: `62` 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/Informationsignal.m` -> `Tests/00_signals/Informationsignal_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/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/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/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/Scope.m` -> `Tests/03_receive/Scope_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: -- `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: `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/00_signals/Opticalsignal_test.m` - Focus: - - constructor metadata wiring - - `delay` - - `cspr` - - interaction with inherited signal behavior +- `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` +- `Tests/integration/WDM_mux_demux_optical_chain_integration_test.m` +- `Tests/integration/WDM_end_to_end_two_channel_receiver_integration_test.m` -2. `Classes/01_transmit/Signalgenerator.m` - `Tests/01_transmit/Signalgenerator_test.m` - Focus: - - constructor/config defaults - - deterministic generation path - - output type and shape +Current integration focus: -3. `Classes/02_etc/Filter.m` - `Tests/02_etc/Filter_test.m` - Focus: - - deterministic filter construction - - `process` behavior on small signals - - output rate/length invariants +- refactor the duplicated reduced IM/DD workflow setup into shared integration helpers +- tighten the IM/DD baseline naming and thresholds so the physical intent is honest +- strengthen the WDM optical-chain and end-to-end receiver fixtures into harder regression guards +- keep all integration fixtures deterministic and small enough to remain reviewable -4. `Classes/02_etc/Amplifier.m` - `Tests/02_etc/Amplifier_test.m` - Focus: - - gain behavior - - noise-free baseline - - output class and metadata behavior +## Remaining Core Tests -5. `Classes/02_optical/EML.m` - `Tests/02_optical/EML_test.m` - Focus: - - deterministic modulation path - - output class and metadata - - small synthetic fixture only +Highest-value unfinished unit tests to implement next: -6. `Classes/02_optical/Optical_Multiplex.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` +1. `Classes/02_optical/Polarization_Controller.m` `Tests/02_optical/Polarization_Controller_test.m` Focus: - deterministic transform behavior - shape preservation -9. `Classes/02_optical/DP_Fiber.m` - `Tests/02_optical/DP_Fiber_test.m` +2. `Classes/01_transmit/AWG.m` + `Tests/01_transmit/AWG_test.m` Focus: - - very small deterministic sanity checks only - - avoid long or GPU-heavy runs + - DAC-like resampling and quantization behavior + - 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 diff --git a/Tests/00_signals/Opticalsignal_test.m b/Tests/00_signals/Opticalsignal_test.m index db25918..2d6fd0d 100644 --- a/Tests/00_signals/Opticalsignal_test.m +++ b/Tests/00_signals/Opticalsignal_test.m @@ -1,10 +1,64 @@ classdef Opticalsignal_test < IMDDTestCase - % Auto-generated placeholder for Opticalsignal. - % Target: C:/Users/Silas/Documents/MATLAB/imdd_simulation/Classes/00_signals/Opticalsignal.m + methods (Test, TestTags = {'unit', 'fast', 'signals', 'optical'}) + function constructorWiresOpticalMetadata(testCase) + opt = makeOpticalsignal([1; 2; 3], 12.5e9, 1310e-9, 0.25, 2); - methods (Test, TestTags = {'placeholder', 'todo'}) - function testNotImplemented(testCase) - testCase.assumeFail("Tests for Opticalsignal are not implemented yet."); + testCase.verifyClass(opt, 'Opticalsignal'); + testCase.verifyEqual(opt.signal, [1; 2; 3]); + 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 + +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 diff --git a/Tests/01_transmit/Signalgenerator_test.m b/Tests/01_transmit/Signalgenerator_test.m index 522b856..2515464 100644 --- a/Tests/01_transmit/Signalgenerator_test.m +++ b/Tests/01_transmit/Signalgenerator_test.m @@ -1,10 +1,42 @@ classdef Signalgenerator_test < IMDDTestCase - % Auto-generated placeholder for Signalgenerator. - % Target: C:/Users/Silas/Documents/MATLAB/imdd_simulation/Classes/01_transmit/Signalgenerator.m + methods (Test, TestTags = {'unit', 'fast', 'transmit', 'signalgenerator'}) + function constructorStoresDefaultConfiguration(testCase) + gen = Signalgenerator(); - methods (Test, TestTags = {'placeholder', 'todo'}) - function testNotImplemented(testCase) - testCase.assumeFail("Tests for Signalgenerator are not implemented yet."); + testCase.verifyEqual(gen.form, signalform.sine); + testCase.verifyEqual(gen.length, 1024); + 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 diff --git a/Tests/02_etc/Amplifier_test.m b/Tests/02_etc/Amplifier_test.m index b7f4841..4d7687c 100644 --- a/Tests/02_etc/Amplifier_test.m +++ b/Tests/02_etc/Amplifier_test.m @@ -1,10 +1,69 @@ classdef Amplifier_test < IMDDTestCase - % Auto-generated placeholder for Amplifier. - % Target: C:/Users/Silas/Documents/MATLAB/imdd_simulation/Classes/02_etc/Amplifier.m + methods (Test, TestTags = {'unit', 'fast', 'etc', 'amplifier'}) + 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'}) - function testNotImplemented(testCase) - testCase.assumeFail("Tests for Amplifier are not implemented yet."); + testCase.verifyEqual(amp.amp_mode, amp_mode.ideal_no_noise); + testCase.verifyEqual(amp.gain_mode, gain_mode.gain); + 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 + +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 diff --git a/Tests/02_etc/Filter_test.m b/Tests/02_etc/Filter_test.m index 64a911f..4370655 100644 --- a/Tests/02_etc/Filter_test.m +++ b/Tests/02_etc/Filter_test.m @@ -1,10 +1,71 @@ classdef Filter_test < IMDDTestCase - % Auto-generated placeholder for Filter. - % Target: C:/Users/Silas/Documents/MATLAB/imdd_simulation/Classes/02_etc/Filter.m + methods (Test, TestTags = {'unit', 'fast', 'filter', 'etc'}) + 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'}) - function testNotImplemented(testCase) - testCase.assumeFail("Tests for Filter are not implemented yet."); + testCase.verifyEqual(flt.active, true); + testCase.verifyEqual(flt.filterType, filtertypes.butterworth); + 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 diff --git a/Tests/02_optical/DP_Fiber_test.m b/Tests/02_optical/DP_Fiber_test.m index dd355f6..14ede1f 100644 --- a/Tests/02_optical/DP_Fiber_test.m +++ b/Tests/02_optical/DP_Fiber_test.m @@ -1,10 +1,78 @@ classdef DP_Fiber_test < IMDDTestCase - % Auto-generated placeholder for DP_Fiber. - % Target: C:/Users/Silas/Documents/MATLAB/imdd_simulation/Classes/02_optical/DP_Fiber.m + methods (Test, TestTags = {'unit', 'fast', 'optical', 'dp_fiber'}) + function constructorStoresConfigurationAndStartsWithEmptyState(testCase) + fiber = makeIdentityFiber(); - methods (Test, TestTags = {'placeholder', 'todo'}) - function testNotImplemented(testCase) - testCase.assumeFail("Tests for DP_Fiber are not implemented yet."); + testCase.verifyEqual(fiber.L, 0); + testCase.verifyEqual(fiber.dz, 1); + 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 + +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 diff --git a/Tests/02_optical/EML_test.m b/Tests/02_optical/EML_test.m index e3970b1..3d16760 100644 --- a/Tests/02_optical/EML_test.m +++ b/Tests/02_optical/EML_test.m @@ -1,10 +1,100 @@ classdef EML_test < IMDDTestCase - % Auto-generated placeholder for EML. - % Target: C:/Users/Silas/Documents/MATLAB/imdd_simulation/Classes/02_optical/EML.m + methods (Test, TestTags = {'unit', 'fast', 'optical', 'eml'}) + 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'}) - function testNotImplemented(testCase) - testCase.assumeFail("Tests for EML are not implemented yet."); + testCase.verifyEqual(eml.mode, eml_mode.iq_linear); + testCase.verifyEqual(eml.fsimu, 64e9); + 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 + +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 diff --git a/Tests/02_optical/Optical_Demultiplex_test.m b/Tests/02_optical/Optical_Demultiplex_test.m index 7046cfc..5e2ec7c 100644 --- a/Tests/02_optical/Optical_Demultiplex_test.m +++ b/Tests/02_optical/Optical_Demultiplex_test.m @@ -1,10 +1,96 @@ classdef Optical_Demultiplex_test < IMDDTestCase - % Auto-generated placeholder for Optical_Demultiplex. - % Target: C:/Users/Silas/Documents/MATLAB/imdd_simulation/Classes/02_optical/Optical_Demultiplex.m + methods (Test, TestTags = {'unit', 'fast', 'optical', 'demux'}) + function processReturnsOneCellPerChannelAndPreservesMetadata(testCase) + fs = 32e9; + lambdaCenterNm = 1310; + lambdaA = 1309e-9; + lambdaB = 1311e-9; - methods (Test, TestTags = {'placeholder', 'todo'}) - function testNotImplemented(testCase) - testCase.assumeFail("Tests for Optical_Demultiplex are not implemented yet."); + sigA = makeOpticalSignal( ... + [1 2; 1 2; 1 2; 1 2], ... + 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 + +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 diff --git a/Tests/02_optical/Optical_Multiplex_test.m b/Tests/02_optical/Optical_Multiplex_test.m index cd15f90..e35adaf 100644 --- a/Tests/02_optical/Optical_Multiplex_test.m +++ b/Tests/02_optical/Optical_Multiplex_test.m @@ -1,10 +1,101 @@ classdef Optical_Multiplex_test < IMDDTestCase - % Auto-generated placeholder for Optical_Multiplex. - % Target: C:/Users/Silas/Documents/MATLAB/imdd_simulation/Classes/02_optical/Optical_Multiplex.m + methods (Test, TestTags = {'unit', 'fast', 'optical', 'mux'}) + function processCombinesChannelsAndPreservesMetadata(testCase) + fs = 32e9; + lambdaCenter = 1550; + lambdaA = 1550e-9; + lambdaB = 1550e-9; - methods (Test, TestTags = {'placeholder', 'todo'}) - function testNotImplemented(testCase) - testCase.assumeFail("Tests for Optical_Multiplex are not implemented yet."); + sigA = makeOpticalSignal( ... + [1 2; 1 2; 1 2], ... + 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 + +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 diff --git a/Tests/integration/IMDD_base_system_impairment_monotonicity_integration_test.m b/Tests/integration/IMDD_base_system_impairment_monotonicity_integration_test.m new file mode 100644 index 0000000..3b02b94 --- /dev/null +++ b/Tests/integration/IMDD_base_system_impairment_monotonicity_integration_test.m @@ -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 diff --git a/Tests/integration/IMDD_base_system_minimal_integration_test.m b/Tests/integration/IMDD_base_system_minimal_integration_test.m index b061270..ca42782 100644 --- a/Tests/integration/IMDD_base_system_minimal_integration_test.m +++ b/Tests/integration/IMDD_base_system_minimal_integration_test.m @@ -6,6 +6,7 @@ classdef IMDD_base_system_minimal_integration_test < IMDDTestCase % - the reduced end-to-end chain runs without errors % - key signal objects have the expected lengths and sampling rates % - the DSP outputs return finite, bounded metrics + % - the intermediate optical and electrical stages stay physically sane % - MLSE does not regress relative to the preceding FFE stage % % Thresholds are intentionally provisional in this first iteration. @@ -19,7 +20,7 @@ classdef IMDD_base_system_minimal_integration_test < IMDDTestCase function runReducedImddWorkflowOnce(testCase) % Run the deterministic reduced workflow once and share the % resulting signals/metrics across all test methods. - testCase.workflow = runReducedWorkflow(); + testCase.workflow = buildReducedImddWorkflow("minimal"); end end @@ -31,18 +32,30 @@ classdef IMDD_base_system_minimal_integration_test < IMDDTestCase 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.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.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 % to 2 samples per symbol for the downstream DSP chain. testCase.verifyEqual(wf.Scpe_sig.fs, 2 * wf.params.fsym); + testCase.verifyEqual(wf.Synced_sig_centered.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 @@ -56,9 +69,12 @@ classdef IMDD_base_system_minimal_integration_test < IMDDTestCase 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 }; @@ -68,6 +84,20 @@ classdef IMDD_base_system_minimal_integration_test < IMDDTestCase testCase.verifyFalse(any(isinf(sig), 'all')); 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.verifyLessThanOrEqual(wf.ffe_results.metrics.BER, 1); testCase.verifyGreaterThanOrEqual(wf.mlse_results.metrics.BER, 0); @@ -105,217 +135,11 @@ classdef IMDD_base_system_minimal_integration_test < IMDDTestCase end end end - -function workflow = runReducedWorkflow() - params = reducedWorkflowParameters(); - - % -------------------- TX -------------------- - txPulse = Pulseformer( ... - "fsym", params.fsym, ... - "fdac", params.fdac, ... - "pulse", "rrc", ... - "pulselength", 12, ... - "alpha", params.rcalpha); - - [digiSig, symbols, txBits] = PAMsource( ... - "fsym", params.fsym, ... - "M", params.M, ... - "order", params.sourceOrder, ... - "useprbs", false, ... - "fs_out", params.fdac, ... - "applyclipping", false, ... - "applypulseform", true, ... - "pulseformer", txPulse, ... - "randkey", params.randomKey, ... - "duobinary_mode", db_mode.no_db, ... - "mrds_code", 0).process(); - - elSig = AWG( ... - "fdac", params.fdac, ... - "f_cutoff", params.fsym, ... - "lpf_active", false, ... - "kover", params.kover, ... - "bit_resolution", 8, ... - "upsampling_method", "samplehold", ... - "precomp_sinc_rolloff", 1).process(digiSig); - - elSig = elSig.normalize("mode", "oneone"); - elSig = elSig .* params.driverScaling; - - % -------------------- Optical Channel -------------------- - optSig = EML( ... - "mode", eml_mode.im_cosinus, ... - "power", params.opticalPowerDbm, ... - "fsimu", elSig.fs, ... - "lambda", params.laserWavelengthNm, ... - "bias", params.vbias, ... - "u_pi", params.uPi, ... - "linewidth", 0, ... - "randomkey", params.randomKey + 1, ... - "alpha", 0).process(elSig); - - optSig = Fiber( ... - "fsimu", optSig.fs, ... - "fiber_length", params.linkLengthKm, ... - "alpha", params.fiberAlphaDbPerKm, ... - "D", 0, ... - "lambda0", 1310, ... - "gamma", 0, ... - "Dslope", 0.07).process(optSig); - - rxOptSig = Amplifier( ... - "amp_mode", "ideal_no_noise", ... - "gain_mode", "output_power", ... - "amplification_db", params.ropDbm).process(optSig); - - rxSigAfterPd = Photodiode( ... - "fsimu", params.fdac * params.kover, ... - "dark_current", 0, ... - "responsivity", 1, ... - "temperature", 20, ... - "nep", 0, ... - "randomkey", params.randomKey + 2).process(rxOptSig); - - rxSigFiltered = Filter( ... - "filtdegree", 4, ... - "f_cutoff", params.rxElectricalBandwidthHz, ... - "fs", params.fdac * params.kover, ... - "filterType", filtertypes.butterworth, ... - "active", true).process(rxSigAfterPd); - - scopeLpf = Filter( ... - "filtdegree", 4, ... - "f_cutoff", params.scopeBandwidthHz, ... - "fs", params.fadc, ... - "filterType", filtertypes.butterworth, ... - "active", true); - - scpeSig = Scope( ... - "fsimu", params.fdac * params.kover, ... - "fadc", params.fadc, ... - "delay", 0, ... - "fixed_delay", 0, ... - "filtertype", filtertypes.butterworth, ... - "samplingdelay", 0, ... - "rand_samplingdelay", 0, ... - "freq_offset", 0, ... - "samp_jitter", 0, ... - "adcresolution", 8, ... - "quantbuffer", 0.1, ... - "block_dc", 1, ... - "lpf_active", 1, ... - "H_lpf", scopeLpf).process(rxSigFiltered); - - rxMatchedFilter = Pulseformer( ... - "fsym", params.fsym, ... - "fdac", 2 * params.fsym, ... - "pulse", "rrc", ... - "pulselength", 12, ... - "alpha", params.rcalpha, ... - "matched", 1); - scpeSig = rxMatchedFilter.process(scpeSig); - - [syncedSig, ~] = scpeSig.tsynch("reference", symbols, "fs_ref", params.fsym, "debug_plots", 0); - syncedSig = syncedSig - mean(syncedSig.signal); - syncedSig.signal = syncedSig.signal(1 : 2 * length(symbols)); - - % -------------------- DSP -------------------- - ffeEq = FFE( ... - "epochs_tr", 2, ... - "epochs_dd", 1, ... - "len_tr", params.lenTr, ... - "mu_dd", 1e-4, ... - "mu_tr", 1e-2, ... - "order", 21, ... - "sps", 2, ... - "decide", 0, ... - "adaption_technique", adaption_method.nlms, ... - "dd_mode", 1); - - ffeResults = ffe( ... - ffeEq, ... - params.M, ... - syncedSig, ... - symbols, ... - txBits, ... - "precode_mode", db_mode.no_db, ... - "showAnalysis", 0, ... - "postFFE", [], ... - "eth_style_symbol_mapping", 0); - - mlseEq = FFE( ... - "epochs_tr", 2, ... - "epochs_dd", 1, ... - "len_tr", params.lenTr, ... - "mu_dd", 1e-4, ... - "mu_tr", 1e-2, ... - "order", 21, ... - "sps", 2, ... - "decide", 0, ... - "adaption_technique", adaption_method.nlms, ... - "dd_mode", 1); - postfilter = Postfilter("ncoeff", 1, "useBurg", 1); - mlse = MLSE( ... - "duobinary_output", 0, ... - "M", params.M, ... - "trellis_states", PAMmapper(params.M, 0).levels); - - [vnleResults, mlseResults] = vnle_postfilter_mlse( ... - mlseEq, ... - postfilter, ... - mlse, ... - params.M, ... - syncedSig, ... - symbols, ... - txBits, ... - "precode_mode", db_mode.no_db, ... - "showAnalysis", 0, ... - "postFFE", [], ... - "eth_style_symbol_mapping", 0); - - workflow = struct(); - workflow.params = params; - workflow.Digi_sig = digiSig; - workflow.Symbols = symbols; - workflow.Tx_bits = txBits; - workflow.El_sig = elSig; - workflow.Opt_sig = optSig; - workflow.Rx_sig_after_pd = rxSigAfterPd; - workflow.Scpe_sig = scpeSig; - workflow.Synced_sig = syncedSig; - workflow.ffe_results = ffeResults; - workflow.vnle_results = vnleResults; - workflow.mlse_results = mlseResults; +function p = signalPower(signal) + p = mean(abs(signal(:)).^2); end -function params = reducedWorkflowParameters() - params = struct(); - - % Smaller, deterministic version of the IM/DD base workflow. - params.M = 4; - params.fsym = 16e9; - params.fdac = 64e9; - params.fadc = 64e9; - params.kover = 2; - params.randomKey = 1; - params.sourceOrder = 12; - params.rcalpha = 0.05; - params.lenTr = 256; - - % Driver / modulator operating point. - params.uPi = 3; - params.vbiasRel = 0.5; - params.vbias = -params.vbiasRel * params.uPi; - params.driverScaling = 0.6 * (params.uPi / 2 - abs(params.vbias - params.uPi / 2)); - - % Optical path. - params.laserWavelengthNm = 1293; - params.opticalPowerDbm = 3; - params.linkLengthKm = 1; - params.fiberAlphaDbPerKm = 0.3; - params.ropDbm = 0; - - % Receiver filtering. - params.rxElectricalBandwidthHz = 40e9; - params.scopeBandwidthHz = 25e9; +function v = signalVariance(signal) + centered = signal(:) - mean(signal(:)); + v = mean(abs(centered).^2); end diff --git a/Tests/integration/IMDD_base_system_no_impairment_integration_test.m b/Tests/integration/IMDD_base_system_no_impairment_integration_test.m new file mode 100644 index 0000000..3104209 --- /dev/null +++ b/Tests/integration/IMDD_base_system_no_impairment_integration_test.m @@ -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 diff --git a/Tests/integration/WDM_end_to_end_two_channel_receiver_integration_test.m b/Tests/integration/WDM_end_to_end_two_channel_receiver_integration_test.m new file mode 100644 index 0000000..6f9819e --- /dev/null +++ b/Tests/integration/WDM_end_to_end_two_channel_receiver_integration_test.m @@ -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 diff --git a/Tests/integration/WDM_mux_demux_optical_chain_integration_test.m b/Tests/integration/WDM_mux_demux_optical_chain_integration_test.m new file mode 100644 index 0000000..4b2aa7b --- /dev/null +++ b/Tests/integration/WDM_mux_demux_optical_chain_integration_test.m @@ -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 diff --git a/Tests/integration/helpers/buildReducedImddWorkflow.m b/Tests/integration/helpers/buildReducedImddWorkflow.m new file mode 100644 index 0000000..f1fdcff --- /dev/null +++ b/Tests/integration/helpers/buildReducedImddWorkflow.m @@ -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 diff --git a/Tests/test_dashboard.m b/Tests/test_dashboard.m new file mode 100644 index 0000000..89b5cf5 --- /dev/null +++ b/Tests/test_dashboard.m @@ -0,0 +1,938 @@ +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.ActiveProfileRowIndices = []; + 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', '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 "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"); + 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 = "Placeholder: hidden"; + 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); + app.ActiveProfileRowIndices = rowIndices; + markRowsAsRunning(rowIndices); + runWithProgress(titleText, @() runProfileInternal(profile, rowIndices), table()); + app.ActiveProfileRowIndices = []; + 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, record); + 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 + 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, record) + if ~isfolder(reportsRoot) + mkdir(reportsRoot); + end + + gitInfo = collectGitMetadata(repoRoot); + runTimestamp = char(datetime("now", "TimeZone", "local", "Format", "yyyy-MM-dd'T'HH:mm:ssXXX")); + updateMetadataMaps(results, record, runTimestamp, gitInfo.short_commit); + + report = struct(); + report.timestamp = runTimestamp; + rowStates = inventoryRowStates(app.Inventory); + 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); + appendLog(sprintf("[%s] Saved run report: %s", stamp(), reportFile)); + end + + function updateMetadataMaps(results, record, runDate, shortCommit) + if ~isempty(record) && strlength(record.TestFile) > 0 + key = char(record.TestFile); + app.LastRunDateMap(key) = char(string(runDate)); + app.LastRunCommitMap(key) = char(string(shortCommit)); + applyStoredRunState(); + return + end + + if ~isempty(app.ActiveProfileRowIndices) + for rowIdx = app.ActiveProfileRowIndices(:)' + key = char(app.Inventory.TestFile(rowIdx)); + if isempty(key) + continue + end + 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 + source = fileread(testFile); + if contains(source, "assumeFail(") + continue + else + matchedTestFiles(end + 1, 1) = testFile; %#ok + status = "Implemented"; + tags = strjoin(extractTestTags(source), ", "); + [~, testFileName] = fileparts(testFile); + end + end + + targetLabel = fullfile('Classes', relativeDirText, [classNameText, '.m']); + targetLabel = strrep(targetLabel, "\", "/"); + + records(end + 1) = struct( ... %#ok + "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(") + continue + else + status = "Implemented"; + end + + [~, testFileName] = fileparts(testFile); + relativeTest = strrep(testFile, [testsRoot, filesep], ""); + relativeTest = strrep(relativeTest, "\", "/"); + tags = strjoin(extractTestTags(source), ", "); + + records(end + 1) = struct( ... %#ok + "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 + + inventory = inventory(inventory.Status ~= "Placeholder", :); + + 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 + 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 + 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) + 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(inventory.LastRunDate(idx)); + rows(idx).last_run_commit = char(inventory.LastRunCommit(idx)); + 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 + 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 diff --git a/projects/WDM/WDM_settings.m b/projects/WDM/WDM_settings.m deleted file mode 100644 index fd40910..0000000 --- a/projects/WDM/WDM_settings.m +++ /dev/null @@ -1,4 +0,0 @@ - - - -