%% 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 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 = '(?.*?)(?([-]*(\d+[\,]*)+[\.]{0,1}\d*[eEdD]{0,1}[-+]*\d*[i]{0,1})|([-]*(\d+[\,]*)*[\.]{1,1}\d+[eEdD]{0,1}[-+]*\d*[i]{0,1}))(?.*)'; 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