318 lines
10 KiB
Matlab
318 lines
10 KiB
Matlab
%% Plot S-parameters from Touchstone S2P or S4P (schema-aware, no RF Toolbox)
|
|
clear; clc;
|
|
|
|
[file,path] = uigetfile({"*.s2p;*.S2P;*.s4p;*.S4P","Touchstone (*.s2p, *.s4p)"});
|
|
if isequal(file,0); return; end
|
|
filename = fullfile(path,file);
|
|
%%
|
|
% Decide port count by extension (minimal-invasive, robust)
|
|
[~,~,ext] = fileparts(file);
|
|
ext = upper(ext);
|
|
|
|
if strcmp(ext,'.S4P')
|
|
[matrix, meta] = loads4p_schema(filename);
|
|
N = 4;
|
|
elseif strcmp(ext,'.S2P')
|
|
[matrix, meta] = loads2p(filename); % your original loader kept (below)
|
|
N = 2;
|
|
else
|
|
error("Unsupported extension: %s", ext);
|
|
end
|
|
|
|
% Frequency in GHz
|
|
frex = matrix(:,1);
|
|
if isfield(meta,'freqScaleHz') && ~isempty(meta.freqScaleHz)
|
|
frex = frex * meta.freqScaleHz / 1e9; % convert to GHz
|
|
else
|
|
% fallback heuristic (your original)
|
|
for i = 1:numel(frex)
|
|
if frex(i) > 1e4
|
|
frex(i) = frex(i).*1e-9;
|
|
end
|
|
end
|
|
end
|
|
|
|
figure(101);
|
|
hold on;
|
|
% overlay indexing: count lines already there
|
|
if N == 2
|
|
lines = numel(findall(gcf, 'type','line'))/4 + 1;
|
|
else
|
|
lines = numel(findall(gcf, 'type','line'))/16 + 1;
|
|
end
|
|
|
|
% styles (kept)
|
|
[CC, LL, MM] = ndgrid({1,2,3,4}, {'-', ':', '-.', '--'}, {'none','+', 'o', '*', 'square', 'diamond'});
|
|
combinations = [CC(:), LL(:), MM(:)];
|
|
cols = [0.3467 0.5360 0.6907
|
|
0.9153 0.2816 0.2878
|
|
0.4416 0.7490 0.4322
|
|
1.0000 0.5984 0.2000];
|
|
|
|
% -------------------- Plotting --------------------
|
|
if N == 2
|
|
heads = ["S11","S12","S22","S21"];
|
|
% matrix layout from loads2p:
|
|
% col pairs:
|
|
% 2-3 S11, 4-5 S21, 6-7 S12, 8-9 S22
|
|
colPairs = [2 3; 6 7; 8 9; 4 5]; % order matches heads above
|
|
subplots = 1;
|
|
|
|
for k = 1:4
|
|
if subplots, subplot(2,2,k); else, figure('Color','w'); end
|
|
hold on; grid on;
|
|
|
|
reCol = colPairs(k,1);
|
|
imCol = colPairs(k,2);
|
|
|
|
% S-parameter magnitude in dB: 20*log10(|S|)
|
|
response = 20*log10(abs(matrix(:,reCol) + 1i.*matrix(:,imCol)));
|
|
response = movmean(response,1);
|
|
|
|
plot(frex, response, ...
|
|
"Color", cols(combinations{lines,1},:), ...
|
|
"DisplayName", file, ...
|
|
"LineStyle", combinations{lines,2}, ...
|
|
"Marker", combinations{lines,3}, ...
|
|
"MarkerSize", 3);
|
|
|
|
if k == 2 || k == 3
|
|
ylim([-6 2]);
|
|
else
|
|
ylim([-30 2]);
|
|
end
|
|
xlim([0 100]);
|
|
title(heads(k));
|
|
xlabel('Frequency in GHz');
|
|
ylabel('|S| (dB)');
|
|
legend('Location','best');
|
|
end
|
|
sgtitle(sprintf('S-Parameters (2-port) [%s]', meta.headerLine));
|
|
|
|
elseif N == 4
|
|
% For s4p schema we plot all Sij in a 4x4 grid
|
|
% matrix layout from loads4p_schema:
|
|
% columns: 1=f, then pairs in order:
|
|
% S11,S12,S13,S14, S21..S24, S31..S34, S41..S44
|
|
subplots = 1;
|
|
|
|
for i = 1:4
|
|
for j = 1:4
|
|
sp = (i-1)*4 + j;
|
|
if subplots, subplot(4,4,sp); else, figure('Color','w'); end
|
|
hold on; grid on;
|
|
|
|
idx = (i-1)*4 + j; % 1..16 for Sij in row-major
|
|
reCol = 1 + 2*(idx-1) + 1; % after freq col
|
|
imCol = 1 + 2*(idx-1) + 2;
|
|
|
|
response = 20*log10(abs(matrix(:,reCol) + 1i.*matrix(:,imCol)));
|
|
plot(frex, response, ...
|
|
"Color", cols(combinations{lines,1},:), ...
|
|
"DisplayName", file, ...
|
|
"LineStyle", combinations{lines,2}, ...
|
|
"Marker", combinations{lines,3}, ...
|
|
"MarkerSize", 2);
|
|
|
|
xlim([0 100]);
|
|
title(sprintf("S%d%d", i, j));
|
|
set(gca,'FontSize',8);
|
|
|
|
% reduce clutter
|
|
if j ~= 1, yticklabels([]); end
|
|
if i ~= 4, xticklabels([]); end
|
|
if j == 1, ylabel('|S| (dB)'); end
|
|
if i == 4, xlabel('GHz'); end
|
|
end
|
|
end
|
|
sgtitle(sprintf('S-Parameters (4-port) [%s]', meta.headerLine));
|
|
end
|
|
|
|
|
|
%% ========================================================================
|
|
% Loader for S4P with the EXACT provided schema (4 lines per frequency)
|
|
% ========================================================================
|
|
function [matrix, meta] = loads4p_schema(filepath)
|
|
meta = struct();
|
|
meta.headerLine = "";
|
|
meta.freqScaleHz = []; % Hz per file unit (e.g., GHz -> 1e9)
|
|
|
|
fid = fopen(filepath,'r');
|
|
if fid < 0, error("Cannot open file: %s", filepath); end
|
|
cleaner = onCleanup(@() fclose(fid));
|
|
|
|
% Parse header: find "#"
|
|
freUnit = "";
|
|
while true
|
|
t = fgetl(fid);
|
|
if ~ischar(t), break; end
|
|
ts = strtrim(t);
|
|
if startsWith(ts,'#')
|
|
meta.headerLine = ts;
|
|
parts = split(upper(string(ts)));
|
|
if numel(parts) >= 2, freUnit = parts(2); end
|
|
|
|
switch freUnit
|
|
case "HZ", meta.freqScaleHz = 1;
|
|
case "KHZ", meta.freqScaleHz = 1e3;
|
|
case "MHZ", meta.freqScaleHz = 1e6;
|
|
case "GHZ", meta.freqScaleHz = 1e9;
|
|
otherwise, meta.freqScaleHz = [];
|
|
end
|
|
break;
|
|
end
|
|
end
|
|
|
|
data = [];
|
|
|
|
while true
|
|
l1 = nextNumericLine(fid);
|
|
if strlength(l1) == 0
|
|
break;
|
|
end
|
|
|
|
% IMPORTANT: sscanf wants char for robust behavior
|
|
v1 = sscanf(char(l1), '%f').';
|
|
if numel(v1) < 9
|
|
error("S4P schema mismatch: expected 9 numbers on first line of a block, got %d.\nLine: %s", numel(v1), char(l1));
|
|
end
|
|
|
|
l2 = nextNumericLine(fid); if strlength(l2)==0, error("Unexpected EOF after first S4P line."); end
|
|
l3 = nextNumericLine(fid); if strlength(l3)==0, error("Unexpected EOF after second S4P line."); end
|
|
l4 = nextNumericLine(fid); if strlength(l4)==0, error("Unexpected EOF after third S4P line."); end
|
|
|
|
v2 = sscanf(char(l2), '%f').';
|
|
v3 = sscanf(char(l3), '%f').';
|
|
v4 = sscanf(char(l4), '%f').';
|
|
|
|
if numel(v2) < 8 || numel(v3) < 8 || numel(v4) < 8
|
|
error("S4P schema mismatch: expected 8 numbers on continuation lines.\nL2: %s\nL3: %s\nL4: %s", char(l2), char(l3), char(l4));
|
|
end
|
|
|
|
f = v1(1);
|
|
s1 = v1(2:9); % S11..S14 (re/im pairs)
|
|
s2 = v2(1:8); % S21..S24
|
|
s3 = v3(1:8); % S31..S34
|
|
s4 = v4(1:8); % S41..S44
|
|
|
|
row = [f, s1, s2, s3, s4]; % 1 + 32 = 33 columns
|
|
data = [data; row]; %#ok<AGROW>
|
|
end
|
|
|
|
matrix = data;
|
|
end
|
|
|
|
|
|
function ln = nextNumericLine(fid)
|
|
ln = "";
|
|
|
|
while true
|
|
t = fgetl(fid);
|
|
if ~ischar(t)
|
|
ln = "";
|
|
return;
|
|
end
|
|
|
|
ts = strtrim(t);
|
|
|
|
% Skip blanks and comments and header lines
|
|
if ts=="" || startsWith(ts,'!') || startsWith(ts,'#')
|
|
continue;
|
|
end
|
|
|
|
% Remove inline comment after '!' if present
|
|
excl = strfind(ts,'!');
|
|
if ~isempty(excl)
|
|
ts = strtrim(extractBefore(ts, excl(1)));
|
|
if ts==""; continue; end
|
|
end
|
|
|
|
% Keep only if there's at least one numeric token
|
|
if ~isempty(regexp(ts,'[-+]?\d*\.?\d+(?:[eEdD][-+]?\d+)?','once'))
|
|
ln = string(ts);
|
|
return;
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
|
|
%% ========================================================================
|
|
% Your original loads2p (kept as-is, but returns meta too)
|
|
% ========================================================================
|
|
function [matrix, meta] = loads2p(filepath)
|
|
meta = struct();
|
|
meta.headerLine = "";
|
|
meta.freqScaleHz = [];
|
|
|
|
% Try to read # line for unit scaling (minimal invasive)
|
|
fid = fopen(filepath,'r');
|
|
if fid >= 0
|
|
cleaner = onCleanup(@() fclose(fid));
|
|
while true
|
|
t = fgetl(fid);
|
|
if ~ischar(t), break; end
|
|
ts = strtrim(t);
|
|
if startsWith(ts,'#')
|
|
meta.headerLine = ts;
|
|
parts = split(upper(string(ts)));
|
|
if numel(parts) >= 2
|
|
switch parts(2)
|
|
case "HZ", meta.freqScaleHz = 1;
|
|
case "KHZ", meta.freqScaleHz = 1e3;
|
|
case "MHZ", meta.freqScaleHz = 1e6;
|
|
case "GHZ", meta.freqScaleHz = 1e9;
|
|
end
|
|
end
|
|
break;
|
|
end
|
|
end
|
|
end
|
|
|
|
startRow = 2;
|
|
formatSpec = '%20s%25s%23s%23s%23s%23s%23s%23s%23s%s%[^\n\r]';
|
|
|
|
fileID = fopen(filepath,'r');
|
|
dataArray = textscan(fileID, formatSpec, 'Delimiter', '', 'WhiteSpace', '', ...
|
|
'TextType', 'string', 'HeaderLines' ,startRow-1, 'ReturnOnError', false, 'EndOfLine', '\r\n');
|
|
fclose(fileID);
|
|
|
|
raw = repmat({''},length(dataArray{1}),length(dataArray)-1);
|
|
for col=1:length(dataArray)-1
|
|
raw(1:length(dataArray{col}),col) = mat2cell(dataArray{col}, ones(length(dataArray{col}), 1));
|
|
end
|
|
numericData = NaN(size(dataArray{1},1),size(dataArray,2));
|
|
|
|
for col=[1,2,3,4,5,6,7,8,9,10]
|
|
rawData = dataArray{col};
|
|
for row=1:size(rawData, 1)
|
|
regexstr = '(?<prefix>.*?)(?<numbers>([-]*(\d+[\,]*)+[\.]{0,1}\d*[eEdD]{0,1}[-+]*\d*[i]{0,1})|([-]*(\d+[\,]*)*[\.]{1,1}\d+[eEdD]{0,1}[-+]*\d*[i]{0,1}))(?<suffix>.*)';
|
|
try
|
|
result = regexp(rawData(row), regexstr, 'names');
|
|
numbers = result.numbers;
|
|
|
|
invalidThousandsSeparator = false;
|
|
if numbers.contains(',')
|
|
thousandsRegExp = '^[-/+]*\d+?(\,\d{3})*\.{0,1}\d*$';
|
|
if isempty(regexp(numbers, thousandsRegExp, 'once'))
|
|
numbers = NaN;
|
|
invalidThousandsSeparator = true;
|
|
end
|
|
end
|
|
if ~invalidThousandsSeparator
|
|
numbers = textscan(char(strrep(numbers, ',', '')), '%f');
|
|
numericData(row, col) = numbers{1};
|
|
raw{row, col} = numbers{1};
|
|
end
|
|
catch
|
|
raw{row, col} = rawData{row};
|
|
end
|
|
end
|
|
end
|
|
|
|
R = cellfun(@(x) ~isnumeric(x) && ~islogical(x),raw);
|
|
raw(R) = {NaN};
|
|
|
|
matrix = cell2mat(raw);
|
|
end
|