function SpectraScalingGUI() % ========================================================================= % SpectraScalingGUI — Response Spectrum Scaling Tool % Ground Motion Selection and Scaling for Seismic Analysis % ========================================================================= % % Author: doc. Ing. Özgür Yurdakul, Ph.D. % Faculty of Transport Engineering % University of Pardubice % % Version: 1.0 (April 2026) % % License: Creative Commons Attribution-NonCommercial-ShareAlike 4.0 % International (CC BY-NC-SA 4.0) % https://creativecommons.org/licenses/by-nc-sa/4.0/ % % Acknowledgement: % This tool was developed within the framework of a project % co-financed from the state budget by the Technology Agency % of the Czech Republic under the SIGMA Programme within the % M.ERA-NET Cofund. % % Description: % GUI-based tool for selecting and scaling ground motion response % spectra to a user-defined target design spectrum. Supports % arbitrary CSV input formats with interactive column selection. % % Tabs: % Tab 1 – Target Spectrum : load from CSV, set building period T, % define plot axis limits % Tab 2 – Ground Motions : load multiple records from CSV, % select T and Sa columns, component % (EW / NS / GM), manual or auto SF % Tab 3 – Results : goodness-of-fit metrics over [0.2T-1.5T], % export scale factors and mean spectrum % % Supported CSV Formats: % - Named columns (e.g. T_s | PSa_EW_g | PSa_GM_g | ...) % - Plain two-column (no header): T | Sa % - Any multi-column CSV; column selection via interactive dialog % % Scale Factor Bounds: min = 0.1 | max = 3.0 % % Goodness-of-Fit Metrics (computed over [0.2T - 1.5T]): % - Min ratio : min(Sa_mean / Sa_target) % - Mean ratio : mean(Sa_mean / Sa_target) % - RMSE : root mean square error (g) % - R2 : coefficient of determination % % Notes: % - All existing SpectraScalingGUI windows are closed on startup. % - Building period T must be entered before plotting markers. % - CSV auto-detects header vs. no-header; column selector shown % for every file loaded. % % Unit System: Period : s | Sa : g % % Requirements: MATLAB R2020b or later (uifigure / App Designer) % % ========================================================================= %% ── Close any existing instances ──────────────────────────────────────── existingFigs = findall(0,'Type','figure','Tag','SpectraScalingGUI'); if ~isempty(existingFigs) delete(existingFigs); end %% ── Figure ─────────────────────────────────────────────────────────────── fig = uifigure(... 'Name','Response Spectrum Scaling – TBDY 2018', ... 'Position',[80 60 1280 760], ... 'Resize','on', ... 'Tag','SpectraScalingGUI'); % ── Application state ──────────────────────────────────────────────────── d.Records = {}; d.Target = []; d.T_struct = 0; d.savePath = ''; d.standard = 'TBDY 2018'; fig.UserData = d; hMarkers = gobjects(0); % stores xline handles for explicit deletion %% ── Menu ───────────────────────────────────────────────────────────────── mFile = uimenu(fig,'Text','File'); uimenu(mFile,'Text','New Project', 'MenuSelectedFcn',@cbNew); uimenu(mFile,'Text','Open Project...', 'MenuSelectedFcn',@cbOpen); uimenu(mFile,'Text','Save Project', 'MenuSelectedFcn',@(~,~)cbSave(false)); uimenu(mFile,'Text','Save Project As...', 'MenuSelectedFcn',@(~,~)cbSave(true), ... 'Separator','on'); mExp = uimenu(fig,'Text','Export'); uimenu(mExp,'Text','Export Figure (PNG / PDF)...', 'MenuSelectedFcn',@cbExportFig); uimenu(mExp,'Text','Export Scale Factors (CSV)...','MenuSelectedFcn',@cbExportCSV); %% ── Root grid ──────────────────────────────────────────────────────────── rootGL = uigridlayout(fig,[1,2]); rootGL.ColumnWidth = {400,'1x'}; rootGL.Padding = [6 6 6 6]; rootGL.ColumnSpacing = 10; % Left: tab group directly in grid cell (auto-fills height) tg = uitabgroup(rootGL); tab1 = uitab(tg,'Title',' Target Spectrum '); tab2 = uitab(tg,'Title',' Ground Motions '); tab3 = uitab(tg,'Title',' Results '); % Right: axes panel rightPanel = uipanel(rootGL,'BorderType','line','BorderColor',[0.75 0.75 0.75]); axGL = uigridlayout(rightPanel,[1,1]); axGL.Padding = [8 8 8 8]; ax = uiaxes(axGL); xlabel(ax,'Period T (s)'); ylabel(ax,'S_a (g)'); title(ax,'Response Spectra'); ax.Box = 'on'; grid(ax,'on'); hold(ax,'on'); ax.FontSize = 11; %% ═══════════════════════════════════════════════════════════════════════ %% TAB 1 — Target Spectrum %% ═══════════════════════════════════════════════════════════════════════ g1 = uigridlayout(tab1,[24,1]); g1.RowHeight = {22,22,36,22,26,26,22,26,26,14,22,26,14,22,26,26,26,26,14,36,14,14,14,14}; g1.Padding = [12 14 12 10]; g1.RowSpacing = 6; row = 1; h = uilabel(g1,'Text','Target Design Spectrum', ... 'FontWeight','bold','FontSize',12); h.Layout.Row = row; h.Layout.Column = 1; % ---- Standard selector ---- row = 2; gStd = uigridlayout(g1,[1,2]); gStd.ColumnWidth = {'1x','2x'}; gStd.Padding = [0 0 0 0]; gStd.ColumnSpacing = 8; gStd.Layout.Row = row; gStd.Layout.Column = 1; h = uilabel(gStd,'Text','Standard:','FontWeight','bold','FontSize',11); h.Layout.Row = 1; h.Layout.Column = 1; dd_standard = uidropdown(gStd, ... 'Items',{'TBDY 2018','EC8 EN 1998-1:2004'}, ... 'Value','TBDY 2018', ... 'ValueChangedFcn',@(s,~)cbStandardChanged(s.Value)); dd_standard.Layout.Row = 1; dd_standard.Layout.Column = 2; row = 3; btnLoadTarget = uibutton(g1,'Text','📂 Load Target Spectrum from CSV...', ... 'BackgroundColor',[0.20 0.45 0.75],'FontColor','white','FontWeight','bold'); btnLoadTarget.Layout.Row = row; btnLoadTarget.Layout.Column = 1; btnLoadTarget.ButtonPushedFcn = @(~,~)cbLoadTarget(); row = 4; lblTargetStatus = uilabel(g1,'Text','No target spectrum loaded.', ... 'FontSize',10,'FontColor',[0.50 0.50 0.50]); lblTargetStatus.Layout.Row = row; lblTargetStatus.Layout.Column = 1; row = 5; lblTargetFile = uilabel(g1,'Text','', ... 'FontSize',10,'FontColor',[0.35 0.35 0.35]); lblTargetFile.Layout.Row = row; lblTargetFile.Layout.Column = 1; row = 6; lblTargetCols = uilabel(g1,'Text','', ... 'FontSize',10,'FontColor',[0.35 0.35 0.35]); lblTargetCols.Layout.Row = row; lblTargetCols.Layout.Column = 1; row = 7; h = makeSep(g1,'Building Period'); h.Layout.Row = row; h.Layout.Column = 1; row = 8; gT = uigridlayout(g1,[1,2]); gT.ColumnWidth = {'1x','1x'}; gT.Padding = [0 0 0 0]; gT.ColumnSpacing = 8; gT.Layout.Row = row; gT.Layout.Column = 1; h = uilabel(gT,'Text','T₁ (s) — required','FontWeight','bold','FontSize',11); h.Layout.Row = 1; h.Layout.Column = 1; efT = uieditfield(gT,'numeric','Value',0, ... 'Limits',[0 100], ... 'ValueChangedFcn',@(s,~)cbTChanged(s.Value)); efT.Layout.Row = 1; efT.Layout.Column = 2; row = 9; lblTinfo = uilabel(g1,'Text','⚠ Enter building period to show period markers.', ... 'FontSize',10,'FontColor',[0.70 0.45 0.05]); lblTinfo.Layout.Row = row; lblTinfo.Layout.Column = 1; row = 10; h = uilabel(g1,'Text',''); h.Layout.Row = row; h.Layout.Column = 1; row = 11; h = makeSep(g1,'Computed from T₁'); h.Layout.Row = row; h.Layout.Column = 1; row = 12; gTcomp = uigridlayout(g1,[1,3]); gTcomp.ColumnWidth = {'1x','1x','1x'}; gTcomp.Padding = [0 0 0 0]; gTcomp.Layout.Row = row; gTcomp.Layout.Column = 1; lblT_val = uilabel(gTcomp,'Text','T₁ = --','FontSize',10,'FontColor',[0.30 0.30 0.30]); lbl02T_v = uilabel(gTcomp,'Text','0.2T₁ = --','FontSize',10,'FontColor',[0.30 0.30 0.30]); lbl15T_v = uilabel(gTcomp,'Text','1.5T₁ = --','FontSize',10,'FontColor',[0.30 0.30 0.30]); lblT_val.Layout.Row = 1; lblT_val.Layout.Column = 1; lbl02T_v.Layout.Row = 1; lbl02T_v.Layout.Column = 2; lbl15T_v.Layout.Row = 1; lbl15T_v.Layout.Column = 3; row = 13; h = uilabel(g1,'Text',''); h.Layout.Row = row; h.Layout.Column = 1; row = 14; h = makeSep(g1,'Plot Axis Limits'); h.Layout.Row = row; h.Layout.Column = 1; % X limits row row = 15; gXlim = uigridlayout(g1,[1,4]); gXlim.ColumnWidth = {'1x','1x','1x','1x'}; gXlim.Padding = [0 0 0 0]; gXlim.ColumnSpacing = 6; gXlim.Layout.Row = row; gXlim.Layout.Column = 1; h = uilabel(gXlim,'Text','X min (s)','FontSize',10); h.Layout.Row=1; h.Layout.Column=1; efXmin = uieditfield(gXlim,'numeric','Value',0,'Limits',[0 Inf]); efXmin.Layout.Row=1; efXmin.Layout.Column=2; h = uilabel(gXlim,'Text','X max (s)','FontSize',10); h.Layout.Row=1; h.Layout.Column=3; efXmax = uieditfield(gXlim,'numeric','Value',4,'Limits',[0 Inf]); efXmax.Layout.Row=1; efXmax.Layout.Column=4; % Y limits row row = 16; gYlim = uigridlayout(g1,[1,4]); gYlim.ColumnWidth = {'1x','1x','1x','1x'}; gYlim.Padding = [0 0 0 0]; gYlim.ColumnSpacing = 6; gYlim.Layout.Row = row; gYlim.Layout.Column = 1; h = uilabel(gYlim,'Text','Y min (g)','FontSize',10); h.Layout.Row=1; h.Layout.Column=1; efYmin = uieditfield(gYlim,'numeric','Value',0,'Limits',[0 Inf]); efYmin.Layout.Row=1; efYmin.Layout.Column=2; h = uilabel(gYlim,'Text','Y max (g)','FontSize',10); h.Layout.Row=1; h.Layout.Column=3; efYmax = uieditfield(gYlim,'numeric','Value',3,'Limits',[0 Inf]); efYmax.Layout.Row=1; efYmax.Layout.Column=4; % Buttons row: Apply | Auto row = 17; gAxBtns = uigridlayout(g1,[1,2]); gAxBtns.ColumnWidth = {'1x','1x'}; gAxBtns.Padding = [0 0 0 0]; gAxBtns.ColumnSpacing = 8; gAxBtns.Layout.Row = row; gAxBtns.Layout.Column = 1; btnApplyAx = uibutton(gAxBtns,'Text','Apply Limits', ... 'BackgroundColor',[0.20 0.45 0.75],'FontColor','white','FontWeight','bold'); btnApplyAx.Layout.Row=1; btnApplyAx.Layout.Column=1; btnApplyAx.ButtonPushedFcn = @(~,~)cbApplyAxLimits(); btnAutoAx = uibutton(gAxBtns,'Text','Reset to Auto'); btnAutoAx.Layout.Row=1; btnAutoAx.Layout.Column=2; btnAutoAx.ButtonPushedFcn = @(~,~)cbAutoAxLimits(); %% ═══════════════════════════════════════════════════════════════════════ %% TAB 2 — Ground Motions %% ═══════════════════════════════════════════════════════════════════════ g2 = uigridlayout(tab2,[12,2]); g2.RowHeight = {26,22,36,36,'1x',36,36,36,8,26,26,8}; g2.ColumnWidth = {'1x','1x'}; g2.Padding = [12 14 12 10]; g2.RowSpacing = 5; row = 1; h = uilabel(g2,'Text','Ground Motion Records', ... 'FontWeight','bold','FontSize',12); h.Layout.Row = row; h.Layout.Column = [1 2]; row = 2; h = uilabel(g2,'Text', ... 'Each file: select T column, Sa column, and component (EW / NS / GM)', ... 'FontSize',10,'FontColor',[0.40 0.40 0.40],'FontAngle','italic'); h.Layout.Row = row; h.Layout.Column = [1 2]; row = 3; btnAdd = uibutton(g2,'Text','➕ Add CSV File(s)...', ... 'BackgroundColor',[0.15 0.52 0.22],'FontColor','white','FontWeight','bold'); btnAdd.Layout.Row = row; btnAdd.Layout.Column = [1 2]; btnAdd.ButtonPushedFcn = @(~,~)cbAddFiles(); row = 4; btnRem = uibutton(g2,'Text','✖ Remove Selected Row', ... 'BackgroundColor',[0.72 0.15 0.12],'FontColor','white','FontWeight','bold'); btnRem.Layout.Row = row; btnRem.Layout.Column = [1 2]; btnRem.ButtonPushedFcn = @(~,~)cbRemove(); row = 5; recTable = uitable(g2, ... 'ColumnName', {'Record Name','SF','Comp.','T col','Sa col','OK'}, ... 'ColumnEditable',[false true false false false false], ... 'ColumnWidth', {120, 50, 40, 52, 52, 30}, ... 'FontSize',10,'RowStriping','on'); recTable.Layout.Row = row; recTable.Layout.Column = [1 2]; row = 6; btnApply = uibutton(g2,'Text','✔ Apply SFs and Replot', ... 'BackgroundColor',[0.20 0.45 0.75],'FontColor','white','FontWeight','bold'); btnApply.Layout.Row = row; btnApply.Layout.Column = [1 2]; btnApply.ButtonPushedFcn = @(~,~)cbApplyAndPlot(); row = 7; btnAuto = uibutton(g2,'Text','⚡ Auto-Scale (Least Squares [0.2T₁ – 1.5T₁])', ... 'BackgroundColor',[0.50 0.25 0.72],'FontColor','white','FontWeight','bold'); btnAuto.Layout.Row = row; btnAuto.Layout.Column = [1 2]; btnAuto.ButtonPushedFcn = @(~,~)cbAutoScale(); row = 8; btnReset = uibutton(g2,'Text','↺ Reset All SFs to 1.0', ... 'BackgroundColor',[0.55 0.55 0.55],'FontColor','white','FontWeight','bold'); btnReset.Layout.Row = row; btnReset.Layout.Column = [1 2]; btnReset.ButtonPushedFcn = @(~,~)cbResetSFs(); row = 9; h = uilabel(g2,'Text',''); h.Layout.Row = row; h.Layout.Column = [1 2]; row = 10; lblRecStatus = uilabel(g2,'Text','No records loaded.', ... 'FontSize',10,'FontColor',[0.50 0.50 0.50]); lblRecStatus.Layout.Row = row; lblRecStatus.Layout.Column = [1 2]; row = 11; lblAutoInfo = uilabel(g2,'Text','','FontSize',10,'FontColor',[0.05 0.50 0.05]); lblAutoInfo.Layout.Row = row; lblAutoInfo.Layout.Column = [1 2]; %% ═══════════════════════════════════════════════════════════════════════ %% TAB 3 — Results %% ═══════════════════════════════════════════════════════════════════════ g3 = uigridlayout(tab3,[22,2]); g3.RowHeight = repmat({24},1,22); g3.ColumnWidth = {'1x','1x'}; g3.Padding = [12 14 12 10]; g3.RowSpacing = 4; row = 1; lblCompTitle = uilabel(g3,'Text','Compliance Check [0.2T₁ – 1.5T₁] window (TBDY 2018)', ... 'FontWeight','bold','FontSize',11); lblCompTitle.Layout.Row = row; lblCompTitle.Layout.Column = [1 2]; row = 2; h = makeSep(g3,'Summary'); h.Layout.Row = row; h.Layout.Column = [1 2]; resLabels = {'Records loaded','Target spectrum','Building T₁ (s)','0.2T₁ (s)','Upper T (s)'}; resVars = cell(1,5); for k = 1:5 makeLabel(g3, resLabels{k}, row+k, 1); resVars{k} = uilabel(g3,'Text','--','FontWeight','bold'); resVars{k}.Layout.Row = row+k; resVars{k}.Layout.Column = 2; end lblNrec = resVars{1}; lblTgt = resVars{2}; lblTr = resVars{3}; lbl02Tr = resVars{4}; lbl15Tr = resVars{5}; row = 8; h = makeSep(g3,'Goodness of Fit (mean vs target)'); h.Layout.Row = row; h.Layout.Column = [1 2]; row = 9; makeLabel(g3,'Min ratio (mean / target)',row,1); lblRatio = uilabel(g3,'Text','--','FontWeight','bold'); lblRatio.Layout.Row = row; lblRatio.Layout.Column = 2; row = 10; makeLabel(g3,'Mean ratio (mean / target)',row,1); lblMeanRatio = uilabel(g3,'Text','--','FontWeight','bold'); lblMeanRatio.Layout.Row = row; lblMeanRatio.Layout.Column = 2; row = 11; makeLabel(g3,'RMSE (g)',row,1); lblRMSE = uilabel(g3,'Text','--','FontWeight','bold'); lblRMSE.Layout.Row = row; lblRMSE.Layout.Column = 2; row = 12; makeLabel(g3,'R²',row,1); lblR2 = uilabel(g3,'Text','--','FontWeight','bold'); lblR2.Layout.Row = row; lblR2.Layout.Column = 2; row = 13; hEC8sep1 = makeSep(g3,'EC8 §3.2.3.1.2(4)(c) — 90% Lower Bound'); hEC8sep1.Layout.Row = row; hEC8sep1.Layout.Column = [1 2]; hEC8sep1.Visible = 'off'; row = 14; hEC8lbl1 = uilabel(g3,'Text','Min(mean/target) ≥ 0.90?','FontSize',10); hEC8lbl1.Layout.Row = row; hEC8lbl1.Layout.Column = 1; hEC8lbl1.Visible = 'off'; lblEC8_90 = uilabel(g3,'Text','--','FontWeight','bold'); lblEC8_90.Layout.Row = row; lblEC8_90.Layout.Column = 2; lblEC8_90.Visible = 'off'; row = 15; hEC8lbl2 = uilabel(g3,'Text','Points failing 90% bound','FontSize',10); hEC8lbl2.Layout.Row = row; hEC8lbl2.Layout.Column = 1; hEC8lbl2.Visible = 'off'; lblEC8_fail = uilabel(g3,'Text','--','FontWeight','bold'); lblEC8_fail.Layout.Row = row; lblEC8_fail.Layout.Column = 2; lblEC8_fail.Visible = 'off'; row = 16; hEC8sep2 = makeSep(g3,'EC8 §4.3.3.4.3 — 3 vs 7 Rule'); hEC8sep2.Layout.Row = row; hEC8sep2.Layout.Column = [1 2]; hEC8sep2.Visible = 'off'; row = 17; hEC8lbl3 = uilabel(g3,'Text','N records','FontSize',10); hEC8lbl3.Layout.Row = row; hEC8lbl3.Layout.Column = 1; hEC8lbl3.Visible = 'off'; lblEC8_N = uilabel(g3,'Text','--','FontWeight','bold'); lblEC8_N.Layout.Row = row; lblEC8_N.Layout.Column = 2; lblEC8_N.Visible = 'off'; row = 18; hEC8lbl4 = uilabel(g3,'Text','Design value rule','FontSize',10); hEC8lbl4.Layout.Row = row; hEC8lbl4.Layout.Column = 1; hEC8lbl4.Visible = 'off'; lblEC8_rule = uilabel(g3,'Text','--','FontWeight','bold','FontColor',[0.70 0.35 0.05]); lblEC8_rule.Layout.Row = row; lblEC8_rule.Layout.Column = 2; lblEC8_rule.Visible = 'off'; % Collect all EC8 handles for easy show/hide ec8_widgets = [hEC8sep1, hEC8lbl1, lblEC8_90, hEC8lbl2, lblEC8_fail, ... hEC8sep2, hEC8lbl3, lblEC8_N, hEC8lbl4, lblEC8_rule]; row = 19; h = uilabel(g3,'Text',''); h.Layout.Row = row; h.Layout.Column = [1 2]; row = 20; btnCheck = uibutton(g3,'Text','🔍 Check Compliance', ... 'BackgroundColor',[0.20 0.45 0.75],'FontColor','white','FontWeight','bold'); btnCheck.Layout.Row = row; btnCheck.Layout.Column = [1 2]; btnCheck.ButtonPushedFcn = @(~,~)cbCheck(); row = 21; h = uilabel(g3,'Text',''); h.Layout.Row = row; h.Layout.Column = [1 2]; row = 22; btnExpRes = uibutton(g3,'Text','📋 Export Results (CSV)...'); btnExpRes.Layout.Row = row; btnExpRes.Layout.Column = [1 2]; btnExpRes.ButtonPushedFcn = @cbExportCSV; %% ═══════════════════════════════════════════════════════════════════════ %% CALLBACKS (nested — share fig scope) %% ═══════════════════════════════════════════════════════════════════════ %% ── Standard Changed ─────────────────────────────────────────────── function cbStandardChanged(val) d = fig.UserData; d.standard = val; fig.UserData = d; isEC8 = strcmp(val,'EC8 EN 1998-1:2004'); vis = 'off'; if isEC8, vis = 'on'; end % Show/hide EC8 compliance widgets for w = ec8_widgets w.Visible = vis; end if isEC8 lblCompTitle.Text = 'Compliance Check [0.2T₁ – 2T₁] window (EC8 EN 1998-1:2004)'; btnAuto.Text = '⚡ Auto-Scale (Least Squares [0.2T₁ – 2T₁])'; else lblCompTitle.Text = 'Compliance Check [0.2T₁ – 1.5T₁] window (TBDY 2018)'; btnAuto.Text = '⚡ Auto-Scale (Least Squares [0.2T₁ – 1.5T₁])'; end cbTChanged(efT.Value); end %% ── Axis Limits ──────────────────────────────────────────────────── function cbApplyAxLimits() xlo = efXmin.Value; xhi = efXmax.Value; ylo = efYmin.Value; yhi = efYmax.Value; if xhi <= xlo uialert(fig,'X max must be greater than X min.','Axis Limits'); return; end if yhi <= ylo uialert(fig,'Y max must be greater than Y min.','Axis Limits'); return; end xlim(ax,[xlo xhi]); ylim(ax,[ylo yhi]); end function cbAutoAxLimits() xlim(ax,'auto'); ylim(ax,'auto'); % Read back actual auto limits and populate the fields xl = xlim(ax); yl = ylim(ax); efXmin.Value = xl(1); efXmax.Value = xl(2); efYmin.Value = yl(1); efYmax.Value = yl(2); end %% ── Load Target Spectrum ─────────────────────────────────────────── function cbLoadTarget() [fname, fpath] = uigetfile('*.csv','Select Target Spectrum CSV...'); if isequal(fname,0), return; end fullpath = fullfile(fpath,fname); try [rawData, colNames, hasHeader] = readCSVauto(fullpath); catch ME uialert(fig,sprintf('Cannot read file:\n%s',ME.message),'Read Error'); return; end sel = columnSelectorDialog(fig, fname, rawData, colNames, hasHeader, false); if isempty(sel), return; end % cancelled d = fig.UserData; d.Target = struct( ... 'T', rawData(:, sel.Tcol), ... 'Sa', rawData(:, sel.Sacol), ... 'label', strrep(fname,'.csv',''), ... 'Tcol', colNames{sel.Tcol}, ... 'Sacol', colNames{sel.Sacol}); fig.UserData = d; lblTargetStatus.Text = sprintf('✔ Target loaded: %s', strrep(fname,'.csv','')); lblTargetStatus.FontColor = [0.05 0.50 0.05]; lblTargetFile.Text = sprintf('File: %s', fname); lblTargetCols.Text = sprintf('T col: %s Sa col: %s', ... colNames{sel.Tcol}, colNames{sel.Sacol}); redrawAxes(); end %% ── Building Period ──────────────────────────────────────────────── function cbTChanged(val) d = fig.UserData; isEC8 = strcmp(d.standard,'EC8 EN 1998-1:2004'); Tmult = 2.0; Tlbl = '2T₁'; if ~isEC8, Tmult = 1.5; Tlbl = '1.5T₁'; end if val <= 0 d.T_struct = 0; lblTinfo.Text = '⚠ Enter building period to show period markers.'; lblTinfo.FontColor = [0.70 0.45 0.05]; lblT_val.Text = 'T₁ = --'; lbl02T_v.Text = '0.2T₁ = --'; lbl15T_v.Text = [Tlbl ' = --']; else d.T_struct = val; lblTinfo.Text = sprintf('Markers active: 0.2T₁ = %.3f s %s = %.3f s', ... 0.2*val, Tlbl, Tmult*val); lblTinfo.FontColor = [0.05 0.50 0.05]; lblT_val.Text = sprintf('T₁ = %.3f s', val); lbl02T_v.Text = sprintf('0.2T₁ = %.3f s', 0.2*val); lbl15T_v.Text = sprintf('%s = %.3f s', Tlbl, Tmult*val); end fig.UserData = d; redrawAxes(); end %% ── Add Ground Motion Files ──────────────────────────────────────── function cbAddFiles() [files, path] = uigetfile('*.csv', ... 'Select Spectrum CSV File(s)','MultiSelect','on'); if isequal(files,0), return; end if ischar(files), files = {files}; end d = fig.UserData; for k = 1:numel(files) fpath = fullfile(path, files{k}); try [rawData, colNames, hasHeader] = readCSVauto(fpath); catch ME uialert(fig,sprintf('Cannot read %s\n\n%s',files{k},ME.message),'Read Error'); continue; end sel = columnSelectorDialog(fig, files{k}, rawData, colNames, hasHeader, true); if isempty(sel), continue; end rec.name = strrep(files{k},'.csv',''); rec.T = rawData(:, sel.Tcol); rec.Sa = rawData(:, sel.Sacol); rec.SF = 1.0; rec.file = fpath; rec.comp = sel.comp; rec.Tcol = colNames{sel.Tcol}; rec.Sacol = colNames{sel.Sacol}; d.Records{end+1} = rec; end fig.UserData = d; refreshTable(); updateRecStatus(); redrawAxes(); end %% ── Remove ───────────────────────────────────────────────────────── function cbRemove() d = fig.UserData; sel = recTable.Selection; if isempty(sel) uialert(fig,'Click a row in the table first.','Remove Record'); return; end rows = unique(sel(:,1)); d.Records(rows) = []; fig.UserData = d; refreshTable(); updateRecStatus(); redrawAxes(); end %% ── Apply SFs ────────────────────────────────────────────────────── function cbApplyAndPlot() d = fig.UserData; tblD = recTable.Data; for i = 1:numel(d.Records) if i <= size(tblD,1) && ~isempty(tblD{i,2}) d.Records{i}.SF = tblD{i,2}; end end fig.UserData = d; redrawAxes(); end %% ── Auto-Scale ───────────────────────────────────────────────────── function cbAutoScale() d = fig.UserData; if d.T_struct <= 0 uialert(fig,'Please enter the building period T first (Tab 1).','Auto-Scale'); return; end if isempty(d.Target) uialert(fig,'Please load a target spectrum first (Tab 1).','Auto-Scale'); return; end if isempty(d.Records) uialert(fig,'No ground motion records loaded.','Auto-Scale'); return; end isEC8 = strcmp(d.standard,'EC8 EN 1998-1:2004'); T1 = 0.2*d.T_struct; T2 = d.T_struct * (2.0 * isEC8 + 1.5 * ~isEC8); commonT = linspace(T1,T2,300)'; SaTgt = interp1(d.Target.T, d.Target.Sa, commonT,'linear',NaN); validTgt = ~isnan(SaTgt); for i = 1:numel(d.Records) r = d.Records{i}; SaRec = interp1(r.T, r.Sa, commonT,'linear',NaN); v = validTgt & ~isnan(SaRec) & SaRec > 0; if sum(v) < 2, continue; end SF = (SaRec(v)'*SaRec(v)) \ (SaRec(v)'*SaTgt(v)); d.Records{i}.SF = min(max(SF, 0.1), 3); end fig.UserData = d; refreshTable(); lblAutoInfo.Text = sprintf('✔ Auto-scale applied to %d records.', numel(d.Records)); lblAutoInfo.FontColor = [0.05 0.50 0.05]; redrawAxes(); end %% ── Reset SFs ────────────────────────────────────────────────────── function cbResetSFs() d = fig.UserData; for i = 1:numel(d.Records) d.Records{i}.SF = 1.0; end fig.UserData = d; refreshTable(); lblAutoInfo.Text = 'All SFs reset to 1.0.'; lblAutoInfo.FontColor = [0.50 0.50 0.50]; redrawAxes(); end %% ── Compliance Check ─────────────────────────────────────────────── function cbCheck() d = fig.UserData; isEC8 = strcmp(d.standard,'EC8 EN 1998-1:2004'); lblNrec.Text = num2str(numel(d.Records)); if isempty(d.Target) lblTgt.Text='None loaded'; lblRatio.Text='--'; lblMeanRatio.Text='--'; lblRMSE.Text='--'; lblR2.Text='--'; return; end lblTgt.Text = d.Target.label; if d.T_struct <= 0 lblTr.Text = 'Not set'; uialert(fig,'Enter the building period T₁ first.','Check'); return; end T_s = d.T_struct; Tmult = 2.0 * isEC8 + 1.5 * ~isEC8; T1 = 0.2*T_s; T2 = Tmult*T_s; lblTr.Text = sprintf('%.3f s', T_s); lbl02Tr.Text = sprintf('%.3f s', T1); lbl15Tr.Text = sprintf('%.3f s', T2); if isempty(d.Records) lblRMSE.Text='--'; lblR2.Text='--'; return; end n = numel(d.Records); commonT = linspace(T1, T2, 300)'; SaTgt = interp1(d.Target.T, d.Target.Sa, commonT,'linear',NaN); sumSa = zeros(numel(commonT),1); for i = 1:n r = d.Records{i}; sumSa = sumSa + interp1(r.T, r.Sa*r.SF, commonT,'linear',NaN); end avgSa = sumSa / n; valid = ~isnan(SaTgt) & ~isnan(avgSa) & SaTgt > 0; if sum(valid) < 2 uialert(fig,sprintf('Insufficient overlap in [0.2T₁–%.1fT₁].',Tmult),'Check'); return; end av = avgSa(valid); tv = SaTgt(valid); % Standard metrics ratios = av ./ tv; minRatio = min(ratios); meanRatio = mean(ratios); rmse = sqrt(mean((av - tv).^2)); ssTot = sum((tv - mean(tv)).^2); ssRes = sum((tv - av).^2); R2 = 1 - ssRes/ssTot; lblRatio.Text = sprintf('%.4f', minRatio); lblMeanRatio.Text = sprintf('%.4f', meanRatio); lblRMSE.Text = sprintf('%.4f g', rmse); lblR2.Text = sprintf('%.4f', R2); % ── EC8 §3.2.3.1.2(4)(c): mean ≥ 0.90 × target ───────────────── if isEC8 passing = ratios >= 0.90; nFail = sum(~passing); pass90 = (nFail == 0); if pass90 lblEC8_90.Text = '✔ PASS'; lblEC8_90.FontColor = [0.05 0.50 0.05]; lblEC8_fail.FontColor = [0.30 0.30 0.30]; else lblEC8_90.Text = '✖ FAIL'; lblEC8_90.FontColor = [0.75 0.05 0.05]; lblEC8_fail.FontColor = [0.75 0.05 0.05]; end lblEC8_fail.Text = sprintf('%d / %d points below 90%%', nFail, sum(valid)); end % ── EC8 §4.3.3.4.3(3)-(4): 3 vs 7 rule ────────────────────────── if isEC8 lblEC8_N.Text = sprintf('%d records', n); if n >= 7 lblEC8_rule.Text = 'N ≥ 7 → use MEAN response'; lblEC8_rule.FontColor = [0.05 0.45 0.05]; else lblEC8_rule.Text = sprintf('N = %d < 7 → use MAX (envelope)', n); lblEC8_rule.FontColor = [0.72 0.35 0.05]; end end end %% ── File menu ────────────────────────────────────────────────────── function cbNew(~,~) ans2 = uiconfirm(fig,'Start a new project? Unsaved data will be lost.', ... 'New Project','Options',{'Yes','Cancel'},'DefaultOption','Cancel'); if strcmp(ans2,'Cancel'), return; end d = fig.UserData; d.Records = {}; d.Target = []; d.T_struct = 0; d.savePath = ''; fig.UserData = d; efT.Value = 0; refreshTable(); updateRecStatus(); redrawAxes(); lblTargetStatus.Text = 'No target spectrum loaded.'; lblTargetStatus.FontColor = [0.50 0.50 0.50]; lblTargetFile.Text = ''; lblTargetCols.Text = ''; lblAutoInfo.Text = ''; lblTinfo.Text = '⚠ Enter building period to show period markers.'; lblTinfo.FontColor = [0.70 0.45 0.05]; lblT_val.Text='T₁ = --'; lbl02T_v.Text='0.2T₁ = --'; lbl15T_v.Text='1.5T₁ = --'; lblEC8_90.Text='--'; lblEC8_fail.Text='--'; lblEC8_N.Text='--'; lblEC8_rule.Text='--'; end function cbOpen(~,~) [f,p] = uigetfile('*.mat','Open Project...'); if isequal(f,0), return; end try s = load(fullfile(p,f),'projectData'); pd = s.projectData; fig.UserData = pd; % Restore T field if isfield(pd,'T_struct') && pd.T_struct > 0 efT.Value = pd.T_struct; cbTChanged(pd.T_struct); end % Restore target labels if ~isempty(pd.Target) lblTargetStatus.Text = sprintf('✔ Target loaded: %s', pd.Target.label); lblTargetStatus.FontColor = [0.05 0.50 0.05]; lblTargetFile.Text = pd.Target.label; lblTargetCols.Text = sprintf('T col: %s Sa col: %s', ... pd.Target.Tcol, pd.Target.Sacol); end refreshTable(); updateRecStatus(); redrawAxes(); catch ME uialert(fig,sprintf('Cannot open project:\n%s',ME.message),'Open Error'); end end function cbSave(forceNew) d = fig.UserData; if forceNew || isempty(d.savePath) [f,p] = uiputfile('*.mat','Save Project As...','SpectraProject.mat'); if isequal(f,0), return; end d.savePath = fullfile(p,f); fig.UserData = d; end projectData = fig.UserData; %#ok save(d.savePath,'projectData'); fig.Name = sprintf('Response Spectrum Scaling – TBDY 2018 [%s]', d.savePath); end function cbExportFig(~,~) [f,p] = uiputfile({'*.png';'*.pdf';'*.emf'},'Export Figure...','Spectra.png'); if isequal(f,0), return; end exportgraphics(fig, fullfile(p,f),'Resolution',300); end function cbExportCSV(~,~) d = fig.UserData; if isempty(d.Records) uialert(fig,'No records to export.','Export'); return; end [f,p] = uiputfile('*.csv','Export Results (CSV)...','ScalingResults.csv'); if isequal(f,0), return; end % ── Part 1: scale factors ────────────────────────────────────── names = cellfun(@(r){r.name}, d.Records)'; SFs = cellfun(@(r) r.SF, d.Records)'; comps = cellfun(@(r){r.comp},d.Records)'; SFtbl = table(names, SFs, comps, ... 'VariableNames',{'RecordName','ScaleFactor','Component'}); % ── Part 2: mean scaled spectrum ─────────────────────────────── commonT = d.Records{1}.T; sumSa = zeros(size(commonT)); for i = 1:numel(d.Records) r = d.Records{i}; sumSa = sumSa + interp1(r.T, r.Sa*r.SF, commonT,'linear',NaN); end avgSa = sumSa / numel(d.Records); MeanTbl = table(commonT, avgSa, ... 'VariableNames',{'T_s','Sa_mean_g'}); % ── Write: SF block, blank line, mean spectrum block ─────────── fid = fopen(fullfile(p,f),'w'); fprintf(fid,'Scale Factors\n'); fprintf(fid,'RecordName,ScaleFactor,Component\n'); for k = 1:height(SFtbl) fprintf(fid,'%s,%.6f,%s\n', SFtbl.RecordName{k}, ... SFtbl.ScaleFactor(k), SFtbl.Component{k}); end fprintf(fid,'\nMean Scaled Spectrum\n'); fprintf(fid,'T_s,Sa_mean_g\n'); for k = 1:height(MeanTbl) fprintf(fid,'%.6f,%.6f\n', MeanTbl.T_s(k), MeanTbl.Sa_mean_g(k)); end fclose(fid); uialert(fig, sprintf('Exported:\n• %d scale factors\n• %d mean spectrum points\nFile: %s', ... height(SFtbl), height(MeanTbl), f), 'Export','Icon','success'); end %% ═══════════════════════════════════════════════════════════════════════ %% HELPERS (nested) %% ═══════════════════════════════════════════════════════════════════════ function refreshTable() d = fig.UserData; n = numel(d.Records); data = cell(n,6); for i = 1:n data{i,1} = d.Records{i}.name; data{i,2} = d.Records{i}.SF; data{i,3} = d.Records{i}.comp; data{i,4} = d.Records{i}.Tcol; data{i,5} = d.Records{i}.Sacol; data{i,6} = 'OK'; end recTable.Data = data; end function updateRecStatus() d = fig.UserData; n = numel(d.Records); if n == 0 lblRecStatus.Text = 'No records loaded.'; lblRecStatus.FontColor = [0.50 0.50 0.50]; else lblRecStatus.Text = sprintf('✔ %d record(s) loaded.', n); lblRecStatus.FontColor = [0.05 0.50 0.05]; end end function redrawAxes() cla(ax); % Explicitly delete any surviving xline marker handles delete(hMarkers(isvalid(hMarkers))); hMarkers = gobjects(0); hold(ax,'on'); grid(ax,'on'); d = fig.UserData; T_s = d.T_struct; % 0 means not yet entered % Target spectrum if ~isempty(d.Target) plot(ax, d.Target.T, d.Target.Sa, ... 'k-','LineWidth',2.2,'DisplayName', ... sprintf('Target – %s', d.Target.label)); end % Individual records + mean n = numel(d.Records); if n > 0 clr = lines(n); commonT = d.Records{1}.T; sumSa = zeros(size(commonT)); for i = 1:n r = d.Records{i}; sSa = r.Sa * r.SF; plot(ax, r.T, sSa, ... 'Color',[clr(i,:) 0.45],'LineWidth',0.9, ... 'DisplayName', ... sprintf('%s [%s] SF=%.3f', r.name, r.comp, r.SF)); sumSa = sumSa + interp1(r.T, sSa, commonT,'linear',NaN); end avgSa = sumSa / n; plot(ax, commonT, avgSa, 'r-','LineWidth',2.8, ... 'DisplayName',sprintf('Mean (N=%d)', n)); end % Period markers — only if T has been explicitly set % HandleVisibility=off keeps them out of the legend if T_s > 0 isEC8 = strcmp(d.standard,'EC8 EN 1998-1:2004'); Tmult = 1.5; Tlbl = '1.5T₁'; if isEC8, Tmult = 2.0; Tlbl = '2T₁'; end hMarkers(1) = xline(ax, T_s, 'b-', sprintf('T₁ = %.3f s', T_s), ... 'LineWidth',1.4,'LabelVerticalAlignment','bottom', ... 'LabelHorizontalAlignment','right', ... 'HandleVisibility','off'); hMarkers(2) = xline(ax, 0.2*T_s, 'b--', sprintf('0.2T₁ = %.3f s', 0.2*T_s), ... 'LineWidth',1.0,'LabelVerticalAlignment','bottom', ... 'HandleVisibility','off'); hMarkers(3) = xline(ax, Tmult*T_s, 'b--', sprintf('%s = %.3f s', Tlbl, Tmult*T_s), ... 'LineWidth',1.0,'LabelVerticalAlignment','bottom', ... 'HandleVisibility','off'); end xlabel(ax,'Period T (s)','FontSize',11); ylabel(ax,'S_a (g)','FontSize',11); title(ax, sprintf('Response Spectra [%d records]', n),'FontSize',12); if n <= 14 legend(ax,'Location','northeast','FontSize',8,'Interpreter','none'); end % Apply user-defined axis limits if non-trivial xlo = efXmin.Value; xhi = efXmax.Value; ylo = efYmin.Value; yhi = efYmax.Value; if xhi > xlo, xlim(ax,[xlo xhi]); else, xlim(ax,'auto'); end if yhi > ylo, ylim(ax,[ylo yhi]); else, ylim(ax,[0 Inf]); end end end % SpectraScalingGUI %% ═══════════════════════════════════════════════════════════════════════ %% READ CSV (auto-detect header) %% ═══════════════════════════════════════════════════════════════════════ function [data, colNames, hasHeader] = readCSVauto(fpath) fid = fopen(fpath,'r'); firstLine = fgetl(fid); fclose(fid); tokens = strsplit(strtrim(firstLine), {',',' ','\t'}); nums = str2double(tokens); hasHeader = any(isnan(nums)); if hasHeader T = readtable(fpath,'VariableNamingRule','preserve'); colNames = T.Properties.VariableNames; data = table2array(T); else T = readtable(fpath,'ReadVariableNames',false,'VariableNamingRule','preserve'); nCols = width(T); colNames = arrayfun(@(k)sprintf('Column %d',k), 1:nCols,'UniformOutput',false); data = table2array(T); end end %% ═══════════════════════════════════════════════════════════════════════ %% COLUMN SELECTOR DIALOG %% showComp = true → show component dropdown (for ground motions) %% showComp = false → no component dropdown (for target spectrum) %% ═══════════════════════════════════════════════════════════════════════ function sel = columnSelectorDialog(parentFig, fname, rawData, colNames, hasHeader, showComp) sel = []; nCols = numel(colNames); nPrev = min(size(rawData,1), 6); dlgH = 380 + 32*showComp; % taller if component row shown dlg = uifigure('Name', sprintf('Column Selector – %s', fname), ... 'Position',[0 0 530 dlgH], ... 'WindowStyle','modal', ... 'Resize','off', ... 'Tag','ColSelDlg'); movegui(dlg,'center'); nRows = 6 + showComp; % grid rows rowH = {22,'1x',26,26,26,14,36}; if showComp rowH = {22,'1x',26,26,26,26,14,36}; end dlgGL = uigridlayout(dlg, [nRows+1, 1]); dlgGL.RowHeight = rowH; dlgGL.Padding = [14 12 14 12]; dlgGL.RowSpacing = 6; % Title h = uilabel(dlgGL,'Text',sprintf('File: %s', fname), ... 'FontWeight','bold','FontSize',11); h.Layout.Row = 1; h.Layout.Column = 1; % Preview table colDisp = colNames; if ~hasHeader colDisp = cellfun(@(n,v) sprintf('%s (%.4g)', n, v), ... colNames, num2cell(rawData(1,:)),'UniformOutput',false); end tblPrev = uitable(dlgGL, ... 'Data', num2cell(rawData(1:nPrev,:)), ... 'ColumnName', colDisp, ... 'RowName', {}, ... 'ColumnEditable', false(1,nCols), ... 'FontSize',10); tblPrev.Layout.Row = 2; tblPrev.Layout.Column = 1; % T column gR3 = uigridlayout(dlgGL,[1,2]); gR3.ColumnWidth={'1x','2x'}; gR3.Padding=[0 0 0 0]; gR3.Layout.Row=3; gR3.Layout.Column=1; h=uilabel(gR3,'Text','Period T column:','FontSize',11,'FontWeight','bold'); h.Layout.Row=1; h.Layout.Column=1; ddT = uidropdown(gR3,'Items',colNames,'Value',colNames{1}); ddT.Layout.Row=1; ddT.Layout.Column=2; % Sa column gR4 = uigridlayout(dlgGL,[1,2]); gR4.ColumnWidth={'1x','2x'}; gR4.Padding=[0 0 0 0]; gR4.Layout.Row=4; gR4.Layout.Column=1; h=uilabel(gR4,'Text','Sa column:','FontSize',11,'FontWeight','bold'); h.Layout.Row=1; h.Layout.Column=1; ddSa = uidropdown(gR4,'Items',colNames,'Value',colNames{min(2,nCols)}); ddSa.Layout.Row=1; ddSa.Layout.Column=2; % Component (optional) ddComp = []; if showComp gR5 = uigridlayout(dlgGL,[1,2]); gR5.ColumnWidth={'1x','2x'}; gR5.Padding=[0 0 0 0]; gR5.Layout.Row=5; gR5.Layout.Column=1; h=uilabel(gR5,'Text','Component:','FontSize',11,'FontWeight','bold'); h.Layout.Row=1; h.Layout.Column=1; ddComp = uidropdown(gR5, ... 'Items',{'GM – Geometric Mean','EW – East-West','NS – North-South'}, ... 'Value','GM – Geometric Mean'); ddComp.Layout.Row=1; ddComp.Layout.Column=2; end % Spacer spRow = 5 + showComp; h=uilabel(dlgGL,'Text',''); h.Layout.Row=spRow+1; h.Layout.Column=1; % OK / Cancel gBtns = uigridlayout(dlgGL,[1,2]); gBtns.ColumnWidth={'1x','1x'}; gBtns.Padding=[0 0 0 0]; gBtns.Layout.Row = spRow+2; gBtns.Layout.Column = 1; btnOK = uibutton(gBtns,'Text','OK', ... 'BackgroundColor',[0.20 0.45 0.75],'FontColor','white','FontWeight','bold'); btnOK.Layout.Row=1; btnOK.Layout.Column=1; btnOK.ButtonPushedFcn = @doOK; btnCancel = uibutton(gBtns,'Text','Cancel'); btnCancel.Layout.Row=1; btnCancel.Layout.Column=2; btnCancel.ButtonPushedFcn = @(~,~)uiresume(dlg); uiwait(dlg); if isvalid(dlg) sel = dlg.UserData; delete(dlg); end function doOK(~,~) Tidx = find(strcmp(colNames, ddT.Value), 1); Saidx = find(strcmp(colNames, ddSa.Value), 1); out = struct('Tcol',Tidx,'Sacol',Saidx,'comp','GM'); if showComp && ~isempty(ddComp) cv = ddComp.Value; if contains(cv,'EW'), out.comp = 'EW'; elseif contains(cv,'NS'), out.comp = 'NS'; else, out.comp = 'GM'; end end dlg.UserData = out; uiresume(dlg); end end %% ═══════════════════════════════════════════════════════════════════════ %% UI CONSTRUCTION HELPERS %% ═══════════════════════════════════════════════════════════════════════ function h = makeSep(parent, txt) h = uilabel(parent,'Text',[' ' txt], ... 'FontAngle','italic','FontSize',10, ... 'FontColor',[0.28 0.28 0.28], ... 'BackgroundColor',[0.88 0.88 0.93]); end function makeLabel(parent, txt, row, col) h = uilabel(parent,'Text',txt,'FontSize',10); h.Layout.Row = row; h.Layout.Column = col; end