O-band Amp Characterization Measurement

This commit is contained in:
Silas Labor Zizou
2025-12-18 09:56:52 +01:00
parent 08e3e00698
commit ec6f5eda86
23 changed files with 960 additions and 26 deletions

View File

@@ -12,6 +12,7 @@ classdef Exfo_laser < handle
safety_mode
end
%% Any user can call these methods to alter the laser state
methods (Access=public)
function obj = Exfo_laser(options)
@@ -169,6 +170,10 @@ classdef Exfo_laser < handle
end
end %public mehtods
% DAU must not be able to call these funcitons outside of the class!
% Hence they are private! :-)
methods (Access=private)
function v = connectLaser_(obj)
@@ -180,7 +185,7 @@ classdef Exfo_laser < handle
configureTerminator(v, "CR", "CR"); % Set the terminator to carriage return (CR)
elseif obj.lab_interface == lab_interface.gpib
% Connect via GPIB adress
visastring = ['GPIB0::',obj.connection_id,'::INSTR'];
visastring = ['GPIB1::',obj.connection_id,'::INSTR'];
v = visadev(visastring);
else
error('Interface not supported');
@@ -342,8 +347,13 @@ classdef Exfo_laser < handle
command = ['CH', num2str(channel), ':L=', num2str(desiredWavelength_nm)];
writeline(serialObj, command);
if abs(obj.cur_wavelength - desiredWavelength_nm)>30
pause(5); %it takes time to tune the laser! If this is not here it WILL break!
else
pause(1.5);
end
if serialObj.Type == visalib.InterfaceType.serial
pause(0.2);
pause(1.5);
response = readline(serialObj);
success = contains(response, 'OK');
end

View File

@@ -0,0 +1,83 @@
classdef LabDeviceTemplate < handle
% Minimal template for lab device control via VISA / SCPI
% Copy this file and adapt:
% - visaAddress
% - public API methods
% - private SCPI commands
properties (Access = public)
active logical = true
end
properties (Access = protected)
visaAddress string
end
methods (Access = public)
function obj = LabDeviceTemplate(options)
arguments
options.active logical = true
options.visaAddress string = ""
end
% apply options
fn = fieldnames(options);
for k = 1:numel(fn)
obj.(fn{k}) = options.(fn{k});
end
% optional: quick connectivity check
if obj.active
v = obj.connect();
writeline(v,"*IDN?");
idn = readline(v);
disp("Connected to: " + string(idn));
delete(v);
end
end
function success = set(obj, value)
% Example public API
v = obj.connect();
success = obj.setValue_(v, value);
delete(v);
end
function value = read(obj)
% Example readback
v = obj.connect();
value = obj.readValue_(v);
delete(v);
end
end
methods (Access = protected)
function v = connect(obj)
assert(obj.visaAddress ~= "", "No VISA address defined");
v = visadev(obj.visaAddress);
end
end
methods (Access = private)
function success = setValue_(~, v, value)
writeline(v, sprintf("SET %g", value));
success = true;
end
function value = readValue_(~, v)
writeline(v, "READ?");
value = str2double(readline(v));
end
end
end

View File

@@ -0,0 +1,327 @@
classdef OSA_Advantest < handle
% Advantest Q8384 Optical Spectrum Analyzer (GPIB)
% User inputs in nanometers
% EXAMPLE:
% span_nm = 70;
% lambda_c_nm = 1310;
% osa = OSA_Advantest( ...
% 'span_nm', span_nm, ...
% 'lambda_c_nm', lambda_c_nm, ...
% 'resolution_nm', 0.1, ...
% 'sampling_points', 10001 );
%
% osa.configure();
% [lambda_nm, psd_dBm] = osa.measure();
% figure()
% plot(lambda_nm,psd_dBm);
properties (Access = public)
span_nm double = 5
resolution_nm double = 0.05
lambda_c_nm double = 1550
sampling_points double = 1001
timeout_s double = 10
lambda_nm
psd_dBm
end
properties (Constant)
valid_resolutions_nm = [0.01 0.02 0.05 0.10 0.20 0.50]
valid_sampling_points = [101 201 501 1001 2001 5001 10001];
end
properties (Access = protected)
visaAddress string = "GPIB0::3::INSTR"
end
methods
function obj = OSA_Advantest(options)
arguments
options.visaAddress string = "GPIB0::3::INSTR"
options.span_nm double = 5
options.resolution_nm double = 0.05
options.lambda_c_nm double = 1550
options.sampling_points double = 1001
options.timeout_s double = 10
end
fn = fieldnames(options);
for k = 1:numel(fn)
obj.(fn{k}) = options.(fn{k});
end
assert(ismember(obj.resolution_nm, obj.valid_resolutions_nm), ...
'Invalid resolution. Allowed: 0.01 | 0.02 | 0.05 | 0.10 | 0.20 | 0.50 nm');
assert(ismember(obj.sampling_points, obj.valid_sampling_points), ...
'Invalid # of Samp. Points. Allowed: 101 201 501 1001 2001 5001 10001');
v = obj.connect();
writeline(v,"*IDN?");
disp(readline(v));
delete(v);
end
function configure(obj)
assert(ismember(obj.resolution_nm, obj.valid_resolutions_nm), ...
'Invalid resolution. Allowed: 0.01 | 0.02 | 0.05 | 0.10 | 0.20 | 0.50 nm');
assert(ismember(obj.sampling_points, obj.valid_sampling_points), ...
'Invalid # of Samp. Points. Allowed: 101 201 501 1001 2001 5001 10001');
v = obj.connect();
writeline(v,"FMT0,HED0,SDL0");
writeline(v,"BUZ0");
writeline(v,"QUI0");
writeline(v, sprintf("RES %gnm", obj.resolution_nm));
writeline(v, sprintf("SPT %g", obj.sampling_points));
writeline(v, sprintf("CEN %gnm", obj.lambda_c_nm));
writeline(v, sprintf("SPA %gnm", obj.span_nm));
writeline(v,"SWE 0");
delete(v);
end
function [osa_x_nm, osa_y_dBm] = measure(obj)
v = obj.connect();
writeline(v,"CSB");
writeline(v,"MEA1");
active = 1; cnt = 0;
while active
pause(1);
writeline(v,"MEA?");
active = str2double(readline(v));
cnt = cnt + 1;
if cnt > obj.timeout_s
delete(v);
error("OSA_Q8384:Timeout","Measurement timeout");
end
end
writeline(v,"PKL");
writeline(v,"ODN?");
readline(v); % number of points (not strictly needed)
writeline(v,"FMT0,HED0,SDL0");
writeline(v,"OSD0"); % Y-axis
osa_y_dBm = str2num(readline(v));
writeline(v,"OSD1"); % X-axis
osa_x_m = str2num(readline(v));
osa_x_nm = osa_x_m.*1e9;
delete(v);
obj.lambda_nm=osa_x_nm;
obj.psd_dBm=osa_y_dBm;
end
% --- Replace / extend plot() in OSA_Q8384 class ---
function plot(obj, options)
arguments
obj
options.FigureNumber double = 1
options.Hold logical = true
options.Title string = "OSA Spectrum"
options.LineWidth double = 0.8
options.Grid logical = true
options.Color (1,3) double = [0 0.4470 0.7410]
options.DisplayName string = ""
end
if isempty(obj.psd_dBm) || isempty(obj.lambda_nm)
error('Record spectrum first: run osa.measure()');
end
fig = figure(options.FigureNumber);
fig.Color = 'w';
ax = gca;
if ~options.Hold
cla(ax);
else
hold(ax,'on');
end
plot(ax, obj.lambda_nm, obj.psd_dBm, ...
'LineWidth', options.LineWidth, ...
'Color', options.Color, ...
'DisplayName', options.DisplayName);
xlabel(ax,'Wavelength [nm]');
ylabel(ax,'Optical Power [dBm]');
if options.Title ~= ""
title(ax, options.Title);
end
xlim(ax,[min(obj.lambda_nm) max(obj.lambda_nm)]);
yl = ylim(ax);
ylim(ax,[yl(1)-1 yl(2)+1]);
if options.Grid
grid(ax,'on');
grid(ax,'minor');
end
set(ax, ...
'FontSize',11, ...
'LineWidth',1, ...
'Box','on');
if options.DisplayName ~= ""
legend(ax,'show','Location','best');
end
end
function osnr = computeOSNR(obj, options)
% OSNR computation based on ratio method (RBW-independent)
% Assumes obj.lambda_nm [nm], obj.psd_dBm [dBm/RBW] exist
arguments
obj
options.bw_signal_nm double = 0.1 % channel bandwidth (0.1 for CW?!?!)
options.ref_bw_nm double = 0.1 % OSNR reference bandwidth
options.carrier_frequency = NaN;
options.noise_guard_nm = 1;
options.noise_window_nm =1;
options.debugplots = 0;
end
assert(~isempty(obj.lambda_nm) && ~isempty(obj.psd_dBm), ...
'Run measure() before computing OSNR');
obj.lambda_nm = obj.lambda_nm;
psd = obj.psd_dBm;
% --- find carrier wavelength recorded in OSA ---
% options.carrier_frequency [nm], options.noise_guard_nm, options.noise_window_nm
if ~isnan(options.carrier_frequency)
% local search window around expected CW carrier
idx_search = obj.lambda_nm > (options.carrier_frequency - options.noise_guard_nm) & ...
obj.lambda_nm < (options.carrier_frequency + options.noise_guard_nm);
[~, imax_local] = max(psd(idx_search));
lambda_search = obj.lambda_nm(idx_search);
lambda_c = lambda_search(imax_local);
[~,imax] = find(obj.lambda_nm==lambda_c);
% define local noise windows directly adjacent to signal (guarded)
idx_noise_left = obj.lambda_nm > (lambda_c - options.noise_guard_nm - options.noise_window_nm) & ...
obj.lambda_nm < (lambda_c - options.noise_guard_nm);
idx_noise_right = obj.lambda_nm > (lambda_c + options.noise_guard_nm) & ...
obj.lambda_nm < (lambda_c + options.noise_guard_nm + options.noise_window_nm);
idx_noise = idx_noise_left | idx_noise_right;
else
% fallback: global maximum (unsafe for pathological ASE cases)
[~, imax] = max(psd);
lambda_c = obj.lambda_nm(imax);
% ASE from outer quarters of spectrum
N = numel(obj.lambda_nm);
idx_noise = false(size(obj.lambda_nm));
idx_noise([3:round(N/4), round(3*N/4):N-1]) = true;
end
% --- extract signal window ---
idx_signal = obj.lambda_nm > (lambda_c - options.bw_signal_nm/2) & ...
obj.lambda_nm < (lambda_c + options.bw_signal_nm/2);
if ~any(idx_signal)
idx_signal(imax) = true;
end
cc_lambda = obj.lambda_nm(idx_signal);
cc_psd = psd(idx_signal);
% --- estimate ASE from local noise (flat ASE assumption) ---
noise_lambda = obj.lambda_nm(idx_noise);
noise_psd = psd(idx_noise);
ase_psd = interp1(noise_lambda, noise_psd, cc_lambda, ...
'linear', 'extrap');
% --- convert to linear (mW / RBW) ---
sig_lin = 10.^(cc_psd/10);
ase_lin = 10.^(ase_psd/10);
% --- integrate (RBW cancels) ---
P_sig_lin = sum(sig_lin);
P_ase_lin = sum(ase_lin);
P_sig_dB = 10*log10(P_sig_lin);
P_ase_dB = 10*log10(P_ase_lin);
osnr.p_sig_db = P_sig_dB;
osnr.p_ase_db = P_ase_dB;
% --- OSNR direct from peak value to the noise estimate out of band ---
osnr.direct_dB = psd(imax) - 10*log10(mean(ase_lin)) ;
% --- OSNR in measurement bandwidth ---
osnr.measured_dB = P_sig_dB - P_ase_dB;
% --- normalize to reference bandwidth (usually 0.1 nm) ---
osnr.ref_dB = osnr.measured_dB + ...
10*log10(options.bw_signal_nm / options.ref_bw_nm);
% --- ASE-corrected OSNR (optional definition) ---
osnr.corrected_dB = ...
10*log10(10^(P_sig_dB/10) - 10^(P_ase_dB/10)) - P_ase_dB;
osnr.corrected_ref_dB = osnr.corrected_dB + ...
10*log10(options.bw_signal_nm / options.ref_bw_nm);
% metadata
osnr.lambda_c_nm = lambda_c;
osnr.bw_signal_nm = options.bw_signal_nm;
osnr.ref_bw_nm = options.ref_bw_nm;
if options.debugplots
figure(100)
xline(lambda_c,'LineWidth',2,'DisplayName','Max. OSA Value','Color',[0.8,0.8,0.8])
obj.plot("DisplayName",'OSNR','FigureNumber',100);
yline(osnr.p_ase_db,'LineWidth',2,'DisplayName','Signal Estimation');
yline(osnr.p_sig_db,'LineWidth',2,'DisplayName','Noise Estimation');
scatter(obj.lambda_nm(idx_noise),obj.psd_dBm(idx_noise),'Marker','.','LineWidth',4,'DisplayName','Noise Samples','MarkerEdgeColor','red');
scatter(obj.lambda_nm(idx_signal),obj.psd_dBm(idx_signal),'Marker','.','LineWidth',4,'DisplayName','Signal Samples','MarkerEdgeColor','green');
scatter(obj.lambda_nm(idx_signal),ase_psd,'Marker','.','LineWidth',4,'DisplayName','Signal Samples','MarkerEdgeColor','magenta');
ylim([min(obj.psd_dBm), max([obj.psd_dBm,osnr.p_sig_db+5])]);
figure(101);hold on
xline(lambda_c,'LineWidth',2,'DisplayName','Max. OSA Value','Color',[0.8,0.8,0.8])
xline(lambda_c - options.bw_signal_nm/2,'LineWidth',2,'DisplayName','CW - 0.5 BW','Color',[0.6,0.6,0.8])
xline(lambda_c + options.bw_signal_nm/2,'LineWidth',2,'DisplayName','CW + 0.5 BW','Color',[0.6,0.8,0.8])
obj.plot("DisplayName",'OSNR','FigureNumber',101);
yline(osnr.p_ase_db,'LineWidth',2,'DisplayName','Signal Estimation');
yline(osnr.p_sig_db,'LineWidth',2,'DisplayName','Noise Estimation');
scatter(obj.lambda_nm(idx_signal),obj.psd_dBm(idx_signal),'Marker','.','LineWidth',4,'DisplayName','Signal Samples','MarkerEdgeColor','green');
scatter(obj.lambda_nm(idx_signal),ase_psd,'Marker','.','LineWidth',4,'DisplayName','Signal Samples','MarkerEdgeColor','magenta');
xlim([lambda_c - options.bw_signal_nm, lambda_c + options.bw_signal_nm])
ylim([min(obj.psd_dBm), max([obj.psd_dBm,osnr.p_sig_db+5])]);
end
end
end
methods (Access = protected)
function v = connect(obj)
v = visadev(obj.visaAddress);
end
end
end

View File

@@ -0,0 +1,168 @@
classdef OptPowerMeter8153A < handle
% Code for HEWLETT-PACKARD 8153A Optical Power Meter (2-channel).
% https://www.artisantg.com/info/Agilent_8153A_Manual.pdf?srsltid=AfmBOoqTe38sbGRNHUFXjw5uO6Br7I7aSfgAdRpx-hVvM4sTIhFYyD5x
% PAGE 166++
% Typical usage:
% pm = HPPowerMeter8153A( ...
% 'active', [false true], ...
% 'wavelength_nm', [1550 1310] );
%
% pm.setWavelength([1550 1310]); % only CH2 is modified
% p = pm.readPower(); % reads only active channels
properties (Access = public)
active logical = [true true] % channel enable
wavelength_nm double = [1550 1550] % per-channel wavelength [nm]
power_dBm double = [NaN NaN] % last read power
offset_dB double = [0 0] % per-channel power offset [dB]
avgTime_s double = [0.2 0.2];
end
properties (Constant)
avgTime_min_s = 0.02 % 20 ms
avgTime_max_s = 3600 % 1 hour
end
properties (Access = protected)
visaAddress string = "GPIB1::22::INSTR"
numChannels double = 2
end
methods
function obj = OptPowerMeter8153A(options)
arguments
options.visaAddress string = "GPIB1::22::INSTR"
options.active logical = [true true]
options.wavelength_nm double = [1550 1550]
end
fn = fieldnames(options);
for k = 1:numel(fn)
obj.(fn{k}) = options.(fn{k});
end
assert(numel(obj.active)==obj.numChannels)
assert(numel(obj.wavelength_nm)==obj.numChannels)
v = obj.connect();
writeline(v,"*IDN?");
disp(readline(v));
pause(0.1);
delete(v);
pause(0.1);
obj.setWavelength(obj.wavelength_nm);
end
function setWavelength(obj, lambda_nm)
if nargin > 1
obj.wavelength_nm = lambda_nm;
end
v = obj.connect();
for ch = 1:obj.numChannels
if obj.active(ch)
writeline(v, ...
sprintf("SENS%d:POW:WAVE %gNM", ch, obj.wavelength_nm(ch)));
end
end
pause(0.1);
delete(v);
pause(0.1);
end
function p = readPower(obj)
v = obj.connect();
for ch = 1:obj.numChannels
if obj.active(ch)
writeline(v, sprintf("READ%d:POW?", ch));
obj.power_dBm(ch) = str2double(readline(v));
else
obj.power_dBm(ch) = NaN;
end
end
delete(v);
p = obj.power_dBm;
end
function setOffsetCorrection(obj, offset_dB)
obj.offset_dB = offset_dB;
v = obj.connect();
for ch = 1:obj.numChannels
if obj.active(ch)
writeline(v, ...
sprintf("SENS%d:CORR:LOSS:INP:MAGN %gDB", ch, obj.offset_dB(ch)));
end
end
delete(v);
end
function offset_dB = getOffsetCorrection(obj)
v = obj.connect();
offset_dB = NaN(1,obj.numChannels);
for ch = 1:obj.numChannels
if obj.active(ch)
writeline(v, sprintf("SENS%d:CORR:LOSS:INP:MAGN?", ch));
offset_dB(ch) = str2double(readline(v));
end
end
delete(v);
obj.offset_dB = offset_dB;
end
function setAvgTime(obj, avgTime_s)
if any(avgTime_s < obj.avgTime_min_s) || any(avgTime_s > obj.avgTime_max_s)
oldv = avgTime_s;
avgTime_s = max(avgTime_s, obj.avgTime_min_s);
avgTime_s = min(avgTime_s, obj.avgTime_max_s);
fprintf('[HPPowerMeter8153A] Averaging time clipped:\n');
for ch = 1:obj.numChannels
if oldv(ch) ~= avgTime_s(ch)
fprintf(' CH%d: %.3g s %.3g s (allowed %.3g %.3g s)\n', ...
ch, oldv(ch), avgTime_s(ch), ...
obj.avgTime_min_s, obj.avgTime_max_s);
end
end
end
obj.avgTime_s = avgTime_s;
v = obj.connect();
for ch = 1:obj.numChannels
if obj.active(ch) && ~isnan(obj.avgTime_s(ch))
writeline(v, ...
sprintf("SENS%d:POW:ATIME %g", ch, obj.avgTime_s(ch)));
end
end
delete(v);
end
function avgTime_s = getAvgTime(obj)
v = obj.connect();
avgTime_s = NaN(1,obj.numChannels);
for ch = 1:obj.numChannels
if obj.active(ch)
writeline(v, sprintf("SENS%d:POW:ATIME?", ch));
avgTime_s(ch) = str2double(readline(v));
end
end
delete(v);
obj.avgTime_s = avgTime_s;
end
end
methods (Access = protected)
function v = connect(obj)
v = visadev(obj.visaAddress);
end
end
end

View File

@@ -20,7 +20,7 @@ classdef Thor_PDFA < handle
function obj = Thor_PDFA(options)
arguments
options.serialport_number = 'COM12'
options.serialport_number = 'COM18'
options.safety_mode = 1;
end
@@ -37,7 +37,7 @@ classdef Thor_PDFA < handle
end
function o = connectSerial(obj)
o = [];
try
% Connect to the PDFA
o = serialport(obj.serialport_number, 9600);
@@ -62,11 +62,15 @@ classdef Thor_PDFA < handle
function getStatus(obj)
o = obj.connectSerial();
obj.getStatus_(o);
if obj.safety_mode
obj
if ~isempty(o)
obj.getStatus_(o);
if obj.safety_mode
obj
end
end
end
function enablePDFA(obj)