classdef DP_Fiber % Dual-Polarization fiber propagation (CNLSE / Manakov) — class version % Runs full setup + propagation inside process_ (no external loop). properties (Access=public) % ---- User options (public) ---- L % [km] fiber length dz % [m] step size target (kept for compatibility; adaptive dz uses SS_* below) lambda % [nm] reference wavelength rng % RNG seed gamma % [1/W/m] nonlinear coefficient % Optional / advanced options (match legacy names where possible) fa % [Hz] sampling frequency X_alpha % [dB/100km] attenuation per 100 km (per-pol, used for X and Y) X_beta % [1..4] dispersion coefficients vector for X (Y mirrors X) D % [ps/(nm*km)] Ds % [ps/(nm^2*km)] dispersion slope Dpmd % [ps/sqrt(km)] PMD coefficient beat_len % [m] beat length (for beta(1) if X_beta is zero) corr_len % [m] correlation length (not directly used; kept for compatibility) manakov % 1=Manakov (legacy behavior: manakov=eq-1) SS_dphimax % [rad] max nonlinear phase per step (adaptive SSFM) SS_dzmax % [m] max dz (adaptive SSFM) SS_dzmin % [m] min dz (adaptive SSFM) n_waveplates % number of PMD waveplates useGPU % GPU acceleration: true, false, or 'auto' (default) useSingle % Use single precision on GPU (default: false) % ---- Internal state (persistent between calls) ---- state % struct mirroring legacy 'state' end methods (Access=public) function obj = DP_Fiber(options) % Constructor — copies fields from 'options' and sets defaults. arguments options.L options.dz options.lambda options.rng = 0 options.gamma % optional but recommended options.fa % legacy-compatible optional params options.X_alpha = 4.605170185988092e-05 % dB/100km options.X_beta = [0,0,-2.16826193914149e-26,3.56839456298263e-41] options.D = 17 % ps/(nm*km) options.Ds = 0.06 % ps/(nm^2*km) options.Dpmd = 3 % ps/sqrt(km) options.beat_len = 50 % m options.corr_len = 50 % m (kept) options.manakov = 0 % 1=Manakov options.SS_dphimax = 5e-3 % rad options.SS_dzmax = 2e4 % m options.SS_dzmin = 100 % m options.n_waveplates = 100 options.useGPU = 'auto' % 'auto', true, or false options.useSingle = false % single precision GPU end % Copy provided options into properties fn = fieldnames(options); for n = 1:numel(fn) obj.(fn{n}) = options.(fn{n}); end % Initialize empty state; will be built on first process_ call obj.state = struct(); end function signalclass_out = process(obj, signalclass_in) % Public entry point: takes a signal class with .signal (2xN) % and writes back the propagated signal. signalclass_in.signal = obj.process_(signalclass_in.signal, signalclass_in.fs); % logbook (kept as in new framework skeleton) lbdesc = 'DP_Fiber propagation (CNLSE_plain)'; if ismethod(signalclass_in, 'logbookentry') signalclass_in = signalclass_in.logbookentry(lbdesc); end signalclass_out = signalclass_in; end function signal_out = process_(obj, signal_in,fs) % Core processing — builds legacy 'state' and calls CNLSE_plain % data_in: [2 x N] complex, dual-pol envelope arguments (Input) obj signal_in fs end % ---- Basic checks if isempty(signal_in) || size(signal_in,2) ~= 2 error('DP_Fiber:Input','Expected data_in of size [2 x N].'); end if isempty(fs) error('DP_Fiber:Config','Sampling frequency options.fa is required.'); end obj.fa = fs; % ---- RNG (legacy behavior) R = RandStream("twister","Seed",obj.rng); % ---- (Re)build state if empty or size-dependent fields changed need_rebuild = ~isfield(obj.state,'nt') || (obj.state.nt ~= size(signal_in,2)); if need_rebuild % Constants c0 = 299792458; % [m/s] % Legacy state mapping st = struct(); % High-level st.L = obj.L * 1000; % [m] legacy expects meters st.polNames = {'X','Y'}; st.dt = 1/obj.fa; st.nt = max(size(signal_in)); st.omega = 2*pi*[(0:st.nt/2-1),(-st.nt/2:-1)]/(st.dt*st.nt); st.lambda = obj.lambda * 1e-9; % [m] st.D = obj.D * 1e-6; % ps/(nm*km) -> s/(nm*m) st.Dpmd = obj.Dpmd * 1e-6; % ps/sqrt(km) -> s/sqrt(km) st.Ds = obj.Ds * 1e3; % ps/(nm^2*km) -> s/(nm^2*m) st.beat_len = obj.beat_len; st.SS_dzmax = obj.SS_dzmax; st.SS_dzmin = obj.SS_dzmin; st.SS_dphimax= obj.SS_dphimax; st.chi = 0; % legacy placeholders st.psi = 0; st.manakov = obj.manakov; % 1 if eq==2 (Manakov), 0 if eq==1 (CNLSE) st.wave_plates = obj.n_waveplates; % Alpha (same X/Y) st.alpha.X = obj.X_alpha; st.alpha_lin.X = st.alpha.X/10*log(10)/1000; st.alpha.Y = st.alpha.X; st.alpha_lin.Y = st.alpha_lin.X; % Beta (dispersion) for X (Y mirrors X) if ~any(obj.X_beta) % Populate from (beat_len, D, Ds, lambda) like legacy if obj.beat_len ~= 0 b1 = pi/obj.beat_len; else b1 = 0; end b2 = 0; % PMD freq term handled elsewhere in legacy b3 = -(st.lambda.^2/(2*pi*c0))*st.D; b4 = (st.lambda^2/(2*pi*c0))^2*st.Ds + (2/st.lambda)*(st.lambda.^2/(2*pi*c0)).^2*st.D; st.beta.X = [b1, b2, b3, b4]; % keep 4 terms, legacy had 0 for beta0; b2 unused % Note: legacy stored [b0,b1,b2,b3]? Here we mirror their usage. % We follow their X_beta layout length=4. else st.beta.X = obj.X_beta; end st.beta.Y = st.beta.X; % Gamma st.gamma = obj.gamma; % PMD / birefringence (legacy waveplate model) st.corr_length = st.L / st.wave_plates; % DGD formula (Agrawal 1.1.18) st.dgd = st.Dpmd * sqrt(st.L/1000); % Dpmd in s/sqrt(km), L in m -> convert: sqrt(m/1000) % For exact legacy match, they used: state.dgd = Dpmd * sqrt(L) wisth L in meters and Dpmd already scaled. % Using their pattern: st.dgd = obj.Dpmd*1e-6 * sqrt(st.L); % match old: state.Dpmd already 1e-6*ps/sqrt(km); they used sqrt(L) with L [m] brf_multiplier = 1; if st.Dpmd == 0 brf_multiplier = 0; end % Waveplate random parameters st.pauli_mats.s0 = eye(2); st.pauli_mats.s2 = [0 1; 1 0]; st.pauli_mats.s3i = [0 1; -1 0]; st.brf.theta = (R.rand(st.wave_plates,1)*pi - 0.5*pi) * brf_multiplier; st.brf.epsilon = 0.5*asin(R.rand(st.wave_plates,1)*2-1) * brf_multiplier; st.brf.stokes = NaN(st.wave_plates,3); st.Ttest = zeros(1, st.wave_plates); st.brf.matR = cell(st.wave_plates,1); for n=1:st.wave_plates matRth = cos(st.brf.theta(n)) * st.pauli_mats.s0 - sin(st.brf.theta(n)) * st.pauli_mats.s3i; matRepsilon = complex(cos(st.brf.epsilon(n))*st.pauli_mats.s0, sin(st.brf.epsilon(n))*st.pauli_mats.s2); matR = matRth * matRepsilon; st.brf.matR{n} = matR; u1 = matR(1,1); u2 = matR(1,2); st.Ttest(n) = abs(u1).^2 + abs(u2).^2; st.brf.stokes(n,:) = [abs(u1).^2 - abs(u2).^2 , (u1) * conj(u2) + conj(u1) .* u2 , 1i*(u1) * conj(u2) - conj(u1) .* u2]; end % Frequency-dependent PMD phase term (legacy form) st.brf.db0 = (R.rand(st.wave_plates,1)*2*pi - pi) * brf_multiplier; st.brf.db1 = sqrt(3*pi/8)*(st.dgd/obj.fa)/st.wave_plates .* st.omega; st.brf.simdgd = 0; % cumsum used in legacy only for debug; keep compatibility variable: ~cumsum(st.brf.db0); % no-op to mirror legacy path % Bookkeeping st.missing_dz = 0; st.n_plates_done = 0; st.test_plates = []; st.test_plate_numbers = []; st.lin_z_test = 0; st.propagated_length = 0; % Cache obj.state = st; end % ---- Call the exact same CNLSE_plain as in the old framework x_in = signal_in(:,1).'; y_in = signal_in(:,2).'; % Determine GPU usage if ischar(obj.useGPU) || isstring(obj.useGPU) if strcmpi(obj.useGPU, 'auto') gpuFlag = []; % Let CNLSE_plain auto-detect else error('DP_Fiber:InvalidGPU', 'useGPU must be true, false, or ''auto'''); end else gpuFlag = logical(obj.useGPU); end [x_out, y_out, obj.state] = CNLSE_plain(x_in, y_in, obj.state, gpuFlag, obj.useSingle); obj.state.propagated_length = obj.state.propagated_length + obj.state.L; signal_out = [x_out; y_out].'; end end end