From 5c23347a7e59ae9d3b4485584342636ced8a6226 Mon Sep 17 00:00:00 2001 From: Thomas Dehaeze Date: Tue, 4 Feb 2025 14:10:54 +0100 Subject: [PATCH] Add tangled matlab files --- matlab/mat/nass_model_controller.mat | Bin 232 -> 232 bytes matlab/mat/nass_model_disturbances.mat | Bin 53849 -> 109021 bytes matlab/src/initializeStewartPlatform.m | 60 +- matlab/test_id31_1_metrology.m | 302 ++++++ matlab/test_id31_2_open_loop_plant.m | 793 +++++++++++++++ matlab/test_id31_3_iff.m | 561 +++++++++++ matlab/test_id31_4_hac.m | 583 +++++++++++ matlab/test_id31_5_experiments.m | 1222 ++++++++++++++++++++++++ test-bench-id31.org | 114 +-- 9 files changed, 3528 insertions(+), 107 deletions(-) create mode 100644 matlab/test_id31_1_metrology.m create mode 100644 matlab/test_id31_2_open_loop_plant.m create mode 100644 matlab/test_id31_3_iff.m create mode 100644 matlab/test_id31_4_hac.m create mode 100644 matlab/test_id31_5_experiments.m diff --git a/matlab/mat/nass_model_controller.mat b/matlab/mat/nass_model_controller.mat index 319d6b21dbbe1fe4be766bd4085cefa8b30995ed..7ea6dca5ef1206ce05e3069ee111cfdb78f49372 100644 GIT binary patch delta 40 vcmaFC_=0hQi9|?gs)Ac;l7fPXf}x3(fuWUwxq^{_k?F)h<%tQb6H7_}^k)m4 delta 40 vcmaFC_=0hQi9~Q>iGo{dl7fPvf}yFEsgaejxq^{_k?F)h<%tQb6H7_}^aTr@ diff --git a/matlab/mat/nass_model_disturbances.mat b/matlab/mat/nass_model_disturbances.mat index 6ad34467840045fd8e0eabf7ba118f335ad9ca64..5e6e00096982b6bad7e093e41b5538e79ee6f40b 100644 GIT binary patch delta 74025 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℞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 delta 18437 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 + +% The developed short-stroke metrology system is schematically shown in Figure ref:fig:test_id31_metrology_kinematics. +% The point of interest is indicated by the blue frame $\{B\}$, which is located $H = 150\,mm$ above the nano-hexapod's top platform. +% The spheres have a diameter $d = 25.4\,mm$, and indicated dimensions are $l_1 = 60\,mm$ and $l_2 = 16.2\,mm$. +% In order to compute the pose of the $\{B\}$ frame with respect to the granite (i.e. with respect to the fixed interferometer heads), the measured (small) displacements $[d_1,\ d_2,\ d_3,\ d_4,\ d_5]$ by the interferometers are first written as a function of the (small) linear and angular motion of the $\{B\}$ frame $[D_x,\ D_y,\ D_z,\ R_x,\ R_y]$ eqref:eq:test_id31_metrology_kinematics. + +% \begin{equation}\label{eq:test_id31_metrology_kinematics} +% d_1 = D_y - l_2 R_x, \quad d_2 = D_y + l_1 R_x, \quad d_3 = -D_x - l_2 R_y, \quad d_4 = -D_x + l_1 R_y, \quad d_5 = -D_z +% \end{equation} + +% #+attr_latex: :options [b]{0.48\linewidth} +% #+begin_minipage +% #+name: fig:test_id31_metrology_kinematics +% #+caption: Schematic of the measurement system. Measured distances are indicated by red arrows. +% #+attr_latex: :scale 1 :float nil +% [[file:figs/test_id31_metrology_kinematics.png]] +% #+end_minipage +% \hfill +% #+attr_latex: :options [b]{0.48\linewidth} +% #+begin_minipage +% #+name: fig:test_id31_align_top_sphere_comparators +% #+attr_latex: :width \linewidth :float nil +% #+caption: The top sphere is aligned with the rotation axis of the spindle using two probes. +% [[file:figs/test_id31_align_top_sphere_comparators.jpg]] +% #+end_minipage + +% The five equations eqref:eq:test_id31_metrology_kinematics can be written in a matrix form, and then inverted to have the pose of the $\{B\}$ frame as a linear combination of the measured five distances by the interferometers eqref:eq:test_id31_metrology_kinematics_inverse. + +% \begin{equation}\label{eq:test_id31_metrology_kinematics_inverse} +% \begin{bmatrix} +% D_x \\ D_y \\ D_z \\ R_x \\ R_y +% \end{bmatrix} = {\underbrace{\begin{bmatrix} +% 0 & 1 & 0 & -l_2 & 0 \\ +% 0 & 1 & 0 & l_1 & 0 \\ +% -1 & 0 & 0 & 0 & -l_2 \\ +% -1 & 0 & 0 & 0 & l_1 \\ +% 0 & 0 & -1 & 0 & 0 +% \end{bmatrix}}_{\bm{J_d}}}^{-1} \cdot \begin{bmatrix} +% d_1 \\ d_2 \\ d_3 \\ d_4 \\ d_5 +% \end{bmatrix} +% \end{equation} + + +%% Geometrical parameters of the metrology system +H = 150e-3; +l1 = (150-48-42)*1e-3; +l2 = (76.2+48+42-150)*1e-3; + +% Computation of the Transformation matrix +Hm = [ 0 1 0 -l2 0; + 0 1 0 l1 0; + -1 0 0 0 -l2; + -1 0 0 0 l1; + 0 0 -1 0 0]; + +% Fine Alignment of reference spheres using interferometers +% <> + +% Thanks to the first alignment of the two reference spheres with the spindle axis (Section ref:ssec:test_id31_metrology_sphere_rought_alignment) and to the fine adjustment of the interferometers orientations (Section ref:ssec:test_id31_metrology_alignment), the spindle can perform complete rotations while still having interference for all five interferometers. +% This metrology can therefore be used to better align the axis defined by the two spheres' center with the spindle axis. + +% The alignment process is made by few iterations. +% First, the spindle is scanned and the alignment errors are recorded. +% From the errors, the motion of the micro-hexapod to better align the spheres with the spindle axis is computed and the micro-hexapod is positioned accordingly. +% Then, the spindle is scanned again, and the new alignment errors are recorded. + +% This iterative process is first performed for angular errors (Figure ref:fig:test_id31_metrology_align_rx_ry) and then for lateral errors (Figure ref:fig:test_id31_metrology_align_dx_dy). +% The remaining errors after alignment is in the order of $\pm5\,\mu\text{rad}$ in $R_x$ and $R_y$ orientations, $\pm 1\,\mu m$ in $D_x$ and $D_y$ directions and less than $0.1\,\mu m$ vertically. + + +%% Angular alignment +% Load Data +data_it0 = h5scan(data_dir, 'alignment', 'h1rx_h1ry', 1); +data_it1 = h5scan(data_dir, 'alignment', 'h1rx_h1ry_0002', 3); +data_it2 = h5scan(data_dir, 'alignment', 'h1rx_h1ry_0002', 5); + +% Offset wrong points +i_it0 = find(abs(data_it0.Rx_int_filtered(2:end)-data_it0.Rx_int_filtered(1:end-1))>1e-5); +data_it0.Rx_int_filtered(i_it0+1:end) = data_it0.Rx_int_filtered(i_it0+1:end) + data_it0.Rx_int_filtered(i_it0) - data_it0.Rx_int_filtered(i_it0+1); +i_it1 = find(abs(data_it1.Rx_int_filtered(2:end)-data_it1.Rx_int_filtered(1:end-1))>1e-5); +data_it1.Rx_int_filtered(i_it1+1:end) = data_it1.Rx_int_filtered(i_it1+1:end) + data_it1.Rx_int_filtered(i_it1) - data_it1.Rx_int_filtered(i_it1+1); +i_it2 = find(abs(data_it2.Rx_int_filtered(2:end)-data_it2.Rx_int_filtered(1:end-1))>1e-5); +data_it2.Rx_int_filtered(i_it2+1:end) = data_it2.Rx_int_filtered(i_it2+1:end) + data_it2.Rx_int_filtered(i_it2) - data_it2.Rx_int_filtered(i_it2+1); + +% Compute circle fit and get radius +[~, ~, R_it0, ~] = circlefit(1e6*data_it0.Rx_int_filtered, 1e6*data_it0.Ry_int_filtered); +[~, ~, R_it1, ~] = circlefit(1e6*data_it1.Rx_int_filtered, 1e6*data_it1.Ry_int_filtered); +[~, ~, R_it2, ~] = circlefit(1e6*data_it2.Rx_int_filtered, 1e6*data_it2.Ry_int_filtered); + +%% Rx/Ry alignment of the spheres using the micro-station +figure; +hold on; +plot(1e6*data_it0.Rx_int_filtered, 1e6*data_it0.Ry_int_filtered, '-', ... + 'DisplayName', sprintf('$R_0 = %.0f \\mu$rad', R_it0)) +plot(1e6*data_it1.Rx_int_filtered, 1e6*data_it1.Ry_int_filtered, '-', ... + 'DisplayName', sprintf('$R_1 = %.0f \\mu$rad', R_it1)) +plot(1e6*data_it2.Rx_int_filtered, 1e6*data_it2.Ry_int_filtered, '-', 'color', colors(5,:), ... + 'DisplayName', sprintf('$R_2 = %.0f \\mu$rad', R_it2)) +hold off; +xlabel('$R_x$ [$\mu$rad]'); ylabel('$R_y$ [$\mu$rad]'); +axis equal +legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1); +xlim([-600, 300]); +ylim([-100, 800]); + +%% Eccentricity alignment +% Load Data +data_it0 = h5scan(data_dir, 'alignment', 'h1rx_h1ry_0002', 5); +data_it1 = h5scan(data_dir, 'alignment', 'h1dx_h1dy', 1); + +% Offset wrong points +i_it0 = find(abs(data_it0.Dy_int_filtered(2:end)-data_it0.Dy_int_filtered(1:end-1))>1e-5); +data_it0.Dy_int_filtered(i_it0+1:end) = data_it0.Dy_int_filtered(i_it0+1:end) + data_it0.Dy_int_filtered(i_it0) - data_it0.Dy_int_filtered(i_it0+1); + +% Compute circle fit and get radius +[~, ~, R_it0, ~] = circlefit(1e6*data_it0.Dx_int_filtered, 1e6*data_it0.Dy_int_filtered); +[~, ~, R_it1, ~] = circlefit(1e6*data_it1.Dx_int_filtered, 1e6*data_it1.Dy_int_filtered); + +%% Dx/Dy alignment of the spheres using the micro-station +figure; +hold on; +plot(1e6*data_it0.Dx_int_filtered, 1e6*data_it0.Dy_int_filtered, '-', ... + 'DisplayName', sprintf('$R_0 = %.0f \\mu$m', R_it0)) +plot(1e6*data_it1.Dx_int_filtered, 1e6*data_it1.Dy_int_filtered, '-', ... + 'DisplayName', sprintf('$R_1 = %.0f \\mu$m', R_it1)) +hold off; +xlabel('$D_x$ [$\mu$m]'); ylabel('$D_y$ [$\mu$m]'); +axis equal +legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1); +xlim([-1, 21]); +ylim([-8, 14]); + +% Estimated measurement volume +% <> + +% Because the interferometers are pointing to spheres and not flat surfaces, the lateral acceptance is limited. +% In order to estimate the metrology acceptance, the micro-hexapod is used to perform three accurate scans of $\pm 1\,mm$, respectively along the $x$, $y$ and $z$ axes. +% During these scans, the 5 interferometers are recorded individually, and the ranges in which each interferometer has enough coupling efficiency to be able to measure the displacement are estimated. +% Results are summarized in Table ref:tab:test_id31_metrology_acceptance. +% The obtained lateral acceptance for pure displacements in any direction is estimated to be around $+/-0.5\,mm$, which is enough for the current application as it is well above the micro-station errors to be actively corrected by the NASS. + + +%% Estimated acceptance of the metrology +% This is estimated by moving the spheres using the micro-hexapod + +% Dx +data_dx = h5scan(data_dir, 'metrology_acceptance_new_align', 'dx', 1); + +dx_acceptance = zeros(5,1); + +for i = [1:size(dx_acceptance, 1)] + % Find range in which the interferometers are measuring displacement + dx_di = diff(data_dx.(sprintf('d%i', i))) == 0; + if sum(dx_di) > 0 + dx_acceptance(i) = data_dx.h1tx(find(dx_di(501:end), 1) + 500) - ... + data_dx.h1tx(find(flip(dx_di(1:500)), 1)); + else + dx_acceptance(i) = data_dx.h1tx(end) - data_dx.h1tx(1); + end +end + +% Dy +data_dy = h5scan(data_dir, 'metrology_acceptance_new_align', 'dy', 1); + +dy_acceptance = zeros(5,1); + +for i = [1:size(dy_acceptance, 1)] + % Find range in which the interferometers are measuring displacement + dy_di = diff(data_dy.(sprintf('d%i', i))) == 0; + if sum(dy_di) > 0 + dy_acceptance(i) = data_dy.h1ty(find(dy_di(501:end), 1) + 500) - ... + data_dy.h1ty(find(flip(dy_di(1:500)), 1)); + else + dy_acceptance(i) = data_dy.h1ty(end) - data_dy.h1ty(1); + end +end + +% Dz +data_dz = h5scan(data_dir, 'metrology_acceptance_new_align', 'dz', 1); + +dz_acceptance = zeros(5,1); + +for i = [1:size(dz_acceptance, 1)] + % Find range in which the interferometers are measuring displacement + dz_di = diff(data_dz.(sprintf('d%i', i))) == 0; + if sum(dz_di) > 0 + dz_acceptance(i) = data_dz.h1tz(find(dz_di(501:end), 1) + 500) - ... + data_dz.h1tz(find(flip(dz_di(1:500)), 1)); + else + dz_acceptance(i) = data_dz.h1tz(end) - data_dz.h1tz(1); + end +end + +% Estimated measurement errors +% <> + +% When using the NASS, the accuracy of the sample's positioning is determined by the accuracy of the external metrology. +% However, the validation of the nano-hexapod, the associated instrumentation and the control architecture is independent of the accuracy of the metrology system. +% Only the bandwidth and noise characteristics of the external metrology are important. +% Yet, some elements effecting the accuracy of the metrology are discussed here. + +% First, the "metrology kinematics" (discussed in Section ref:ssec:test_id31_metrology_kinematics) is only approximate (i.e. valid for very small displacements). +% This can be easily seen when performing lateral $[D_x,\,D_y]$ scans using the micro-hexapod while recording the vertical interferometer (Figure ref:fig:test_id31_xy_map_sphere). +% As the interferometer is pointing to a sphere and not to a plane, lateral motion of the sphere is seen as a vertical motion by the top interferometer. + +% Then, the reference spheres have some deviations with respect to an ideal sphere [fn:6]. +% They are initially meant to be used with capacitive sensors which are integrating the shape errors over large surfaces. +% When using interferometers, the size of the "light spot" on the sphere surface is a circle with a diameter approximately equal to $50\,\mu m$, and therefore the measurement is more sensitive to shape errors with small features. + +% As the light from the interferometer is travelling through air (as opposed to being in vacuum), the measured distance is sensitive to any variation in the refractive index of the air. +% Therefore, any variation of air temperature, pressure or humidity will induce measurement errors. +% For instance, for a measurement length of $40\,mm$, a temperature variation of $0.1\,{}^oC$ (which is typical for the ID31 experimental hutch) induces an errors in the distance measurement of $\approx 4\,nm$. + +% Interferometers are also affected by noise [[cite:&watchi18_review_compac_inter]]. +% The effect of the noise on the translation and rotation measurements is estimated in Figure ref:fig:test_id31_interf_noise. + + +%% Interferometer noise estimation +data = load("test_id31_interf_noise.mat"); + +Ts = 1e-4; +Nfft = floor(5/Ts); +win = hanning(Nfft); +Noverlap = floor(Nfft/2); + +[pxx_int, f] = pwelch(detrend(data.d, 0), win, Noverlap, Nfft, 1/Ts); + +% Uncorrelated noise: square root of the sum of the squares +pxx_cart = pxx_int*sum(inv(Hm).^2, 2)'; + +rms_dxy = sqrt(trapz(f(f>1), pxx_cart((f>1),1))); % < 0.3 nm RMS +rms_dz = sqrt(trapz(f(f>1), pxx_cart((f>1),3))); % < 0.3 nm RMS +rms_rxy = sqrt(trapz(f(f>1), pxx_cart((f>1),4))); % 5 nrad RMS + +figure; +hold on; +plot(f, sqrt(pxx_cart(:,1)), 'DisplayName', sprintf('$D_{x,y}$, %.1f nmRMS', rms_dxy)); +plot(f, sqrt(pxx_cart(:,3)), 'DisplayName', sprintf('$D_{z}$, %.1f nmRMS', rms_dz)); +plot(f, sqrt(pxx_cart(:,4)), 'DisplayName', sprintf('$R_{x,y}$, %.1f nradRMS', rms_rxy)); +set(gca, 'xscale', 'log'); set(gca, 'yscale', 'log'); +xlabel('Frequency [Hz]'); ylabel('ASD $\left[\frac{nm,\ nrad}{\sqrt{Hz}}\right]$') +xlim([1, 1e3]); ylim([1e-3, 1]); +leg = legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; + +%% X-Y scan with the micro-hexapod, and record of the vertical interferometer +data = h5scan(data_dir, 'metrology_acceptance', 'after_int_align_meshXY', 1); + +x = 1e3*detrend(data.h1tx, 0); % [um] +y = 1e3*detrend(data.h1ty, 0); % [um] +z = 1e6*data.Dz_int_filtered - max(data.Dz_int_filtered); % [um] + +mdl = scatteredInterpolant(x, y, z); +[xg, yg] = meshgrid(unique(x), unique(y)); +zg = mdl(xg, yg); + +% Fit a sphere to the data +[sphere_center,sphere_radius] = sphereFit(1e-3*[x, y, z]); + +%% XY mapping of the Z measurement by the interferometer +figure; +[~,c] = contour3(xg,yg,zg,30); +c.LineWidth = 3; +xlabel('$D_x$ [$\mu$m]'); +ylabel('$D_y$ [$\mu$m]'); +zlabel('$D_z$ [$\mu$m]'); +zlim([-1, 0]); +xticks(-100:50:100); yticks(-100:50:100); zticks(-1:0.2:0); diff --git a/matlab/test_id31_2_open_loop_plant.m b/matlab/test_id31_2_open_loop_plant.m new file mode 100644 index 0000000..aa68087 --- /dev/null +++ b/matlab/test_id31_2_open_loop_plant.m @@ -0,0 +1,793 @@ +% Matlab Init :noexport:ignore: + +%% test_id31_2_open_loop_plant.m + +%% Clear Workspace and Close figures +clear; close all; clc; + +%% Intialize Laplace variable +s = zpk('s'); + +%% Path for functions, data and scripts +addpath('./mat/'); % Path for Data +addpath('./src/'); % Path for functions +addpath('./STEPS/'); % Path for STEPS +addpath('./subsystems/'); % Path for Subsystems Simulink files + +%% Data directory +data_dir = './mat/'; + +% Simulink Model name +mdl = 'nass_model_id31'; + +%% Colors for the figures +colors = colororder; + +%% Frequency Vector +freqs = logspace(log10(1), log10(2e3), 1000); + +%% Sampling Time +Ts = 1e-4; + +%% Specifications for Experiments +specs_dz_peak = 50; % [nm] +specs_dy_peak = 100; % [nm] +specs_ry_peak = 0.85; % [urad] +specs_dz_rms = 15; % [nm RMS] +specs_dy_rms = 30; % [nm RMS] +specs_ry_rms = 0.25; % [urad RMS] + +% Open-Loop Plant Identification +% <> + +% The plant dynamics is first identified for a fixed spindle angle (at $0\,\text{deg}$) and without any payload. +% The model dynamics is also identified in the same conditions. + +% A first comparison between the model and the measured dynamics is done in Figure ref:fig:test_id31_first_id. +% A good match can be observed for the diagonal dynamics (except the high frequency modes which are not modeled). +% However, the coupling for the transfer function from command signals $\bm{u}$ to the estimated strut motion from the external metrology $\bm{\epsilon\mathcal{L}}$ is larger than expected (Figure ref:fig:test_id31_first_id_int). + +% The experimental time delay estimated from the FRF (Figure ref:fig:test_id31_first_id_int) is larger than expected. +% After investigation, it was found that the additional delay was due to a digital processing unit[fn:3] that was used to get the interferometers' signals in the Speedgoat. +% This issue was later solved. + + +%% Identify the plant dynamics using the Simscape model + +% Initialize each Simscape model elements +initializeGround(); +initializeGranite(); +initializeTy(); +initializeRy(); +initializeRz(); +initializeMicroHexapod(); +initializeNanoHexapod('flex_bot_type', '2dof', ... + 'flex_top_type', '3dof', ... + 'motion_sensor_type', 'plates', ... + 'actuator_type', '2dof'); +initializeSample('type', '0'); + +initializeSimscapeConfiguration('gravity', false); +initializeDisturbances('enable', false); +initializeLoggingConfiguration('log', 'none'); +initializeController('type', 'open-loop'); +initializeReferences(); + +% Input/Output definition +clear io; io_i = 1; +io(io_i) = linio([mdl, '/Controller'], 1, 'openinput'); io_i = io_i + 1; % Actuator Inputs [V] +io(io_i) = linio([mdl, '/Micro-Station'], 3, 'openoutput', [], 'Vs'); io_i = io_i + 1; % Force Sensors voltages [V] +io(io_i) = linio([mdl, '/Tracking Error'], 1, 'openoutput', [], 'EdL'); io_i = io_i + 1; % Position Errors [m] + +% With no payload +Gm = exp(-1e-4*s)*linearize(mdl, io); +Gm.InputName = {'u1', 'u2', 'u3', 'u4', 'u5', 'u6'}; +Gm.OutputName = {'Vs1', 'Vs2', 'Vs3', 'Vs4', 'Vs5', 'Vs6', ... + 'eL1', 'eL2', 'eL3', 'eL4', 'eL5', 'eL6'}; + +%% Identify the plant from experimental data + +% Load identification data +data = load('2023-08-08_16-17_ol_plant_m0_Wz0.mat'); + +% Frequency analysis parameters +Ts = 1e-4; % Sampling Time [s] +Nfft = floor(2.0/Ts); +win = hanning(Nfft); +Noverlap = floor(Nfft/2); +[~, f] = tfestimate(data.uL1.id_plant, data.uL1.e_L1, win, Noverlap, Nfft, 1/Ts); + +G_iff = zeros(length(f), 6, 6); % Force sensor outputs +G_int = zeros(length(f), 6, 6); % Estimated strut errors +for i_strut = 1:6 + Vs = [data.(sprintf("uL%i", i_strut)).Vs1 ; data.(sprintf("uL%i", i_strut)).Vs2 ; data.(sprintf("uL%i", i_strut)).Vs3 ; data.(sprintf("uL%i", i_strut)).Vs4 ; data.(sprintf("uL%i", i_strut)).Vs5 ; data.(sprintf("uL%i", i_strut)).Vs6]'; + eL = [data.(sprintf("uL%i", i_strut)).e_L1 ; data.(sprintf("uL%i", i_strut)).e_L2 ; data.(sprintf("uL%i", i_strut)).e_L3 ; data.(sprintf("uL%i", i_strut)).e_L4 ; data.(sprintf("uL%i", i_strut)).e_L5 ; data.(sprintf("uL%i", i_strut)).e_L6]'; + dL = [data.(sprintf("uL%i", i_strut)).dL1 ; data.(sprintf("uL%i", i_strut)).dL2 ; data.(sprintf("uL%i", i_strut)).dL3 ; data.(sprintf("uL%i", i_strut)).dL4 ; data.(sprintf("uL%i", i_strut)).dL5 ; data.(sprintf("uL%i", i_strut)).dL6]'; + + G_iff(:,:,i_strut) = tfestimate(data.(sprintf("uL%i", i_strut)).id_plant, Vs, win, Noverlap, Nfft, 1/Ts); + G_int(:,:,i_strut) = tfestimate(data.(sprintf("uL%i", i_strut)).id_plant, eL, win, Noverlap, Nfft, 1/Ts); +end + +%% Obtained transfer function from generated voltages to measured voltages on the piezoelectric force sensor +figure; +tiledlayout(3, 1, 'TileSpacing', 'compact', 'Padding', 'None'); + +ax1 = nexttile([2,1]); +hold on; +for i = 1:5 + for j = i+1:6 + plot(f, abs(G_int(:, i, j)), 'color', [colors(1,:), 0.2], ... + 'HandleVisibility', 'off'); + plot(freqs, abs(squeeze(freqresp(Gm(sprintf('eL%i', i), sprintf('u%i', j)), freqs, 'Hz'))), 'color', [colors(2,:), 0.2], ... + 'HandleVisibility', 'off'); + end +end +plot(f, abs(G_int(:, 1, 1)), 'color', [colors(1,:)], ... + 'DisplayName', '$-\epsilon\mathcal{L}_i/u_i$ meas'); +plot(freqs, abs(squeeze(freqresp(Gm(sprintf('eL%i', 1), sprintf('u%i', 1)), freqs, 'Hz'))), 'color', [colors(2,:)], ... + 'DisplayName', '$-\epsilon\mathcal{L}_i/u_i$ model'); +for i = 2:6 + plot(f, abs(G_int(:,i, i)), 'color', [colors(1,:)], ... + 'HandleVisibility', 'off'); + plot(freqs, abs(squeeze(freqresp(Gm(sprintf('eL%i', i), sprintf('u%i', i)), freqs, 'Hz'))), 'color', [colors(2,:)], ... + 'HandleVisibility', 'off'); +end +plot(f, abs(G_int(:, 1, 2)), 'color', [colors(1,:), 0.2], ... + 'DisplayName', '$-\epsilon\mathcal{L}_i/u_j$ meas'); +plot(freqs, abs(squeeze(freqresp(Gm(sprintf('eL%i', 1), sprintf('u%i', 2)), freqs, 'Hz'))), 'color', [colors(2,:), 0.2], ... + 'DisplayName', '$-\epsilon\mathcal{L}_i/u_j$ model'); +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log'); +ylabel('Amplitude [m/V]'); set(gca, 'XTickLabel',[]); +ylim([2e-9, 2e-4]); +leg = legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 2); +leg.ItemTokenSize(1) = 15; + +ax2 = nexttile; +hold on; +for i = 1:6 + plot(f, 180/pi*angle(G_int(:,i, i)), 'color', [colors(1,:)]); +end +for i = 1:6 + plot(freqs, 180/pi*angle(squeeze(freqresp(Gm(sprintf('eL%i', i), sprintf('u%i', i)), freqs, 'Hz'))), 'color', [colors(2,:)]); +end +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'lin'); +xlabel('Frequency [Hz]'); ylabel('Phase [deg]'); +hold off; +yticks(-360:90:360); +ylim([-90, 180]) + +linkaxes([ax1,ax2],'x'); +xlim([1, 1e3]); + +%% Comparison between the measured dynamics and the model dynamics - Force Sensors +figure; +tiledlayout(3, 1, 'TileSpacing', 'compact', 'Padding', 'None'); + +ax1 = nexttile([2,1]); +hold on; +for i = 1:5 + for j = i+1:6 + plot(f, abs(G_iff(:, i, j)), 'color', [colors(1,:), 0.2], ... + 'HandleVisibility', 'off'); + plot(freqs, abs(squeeze(freqresp(Gm(sprintf('Vs%i', i), sprintf('u%i', j)), freqs, 'Hz'))), 'color', [colors(2,:), 0.2], ... + 'HandleVisibility', 'off'); + end +end +plot(f, abs(G_iff(:,1, 1)), 'color', [colors(1,:)], ... + 'DisplayName', '$V_{s,i}/u_i$ meas'); +plot(freqs, abs(squeeze(freqresp(Gm(sprintf('Vs%i', 1), sprintf('u%i', 1)), freqs, 'Hz'))), 'color', [colors(2,:)], ... + 'DisplayName', '$V_{s,i}/u_i$ model'); +for i = 2:6 + plot(f, abs(G_iff(:,i, i)), 'color', [colors(1,:)], ... + 'HandleVisibility', 'off'); + plot(freqs, abs(squeeze(freqresp(Gm(sprintf('Vs%i', i), sprintf('u%i', i)), freqs, 'Hz'))), 'color', [colors(2,:)], ... + 'HandleVisibility', 'off'); +end +plot(f, abs(G_iff(:, 1, 2)), 'color', [colors(1,:), 0.2], ... + 'DisplayName', '$V_{s,i}/u_j$ meas'); +plot(freqs, abs(squeeze(freqresp(Gm(sprintf('Vs%i', 1), sprintf('u%i', 2)), freqs, 'Hz'))), 'color', [colors(2,:), 0.2], ... + 'DisplayName', '$V_{s,i}/u_j$ model'); +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log'); +ylabel('Amplitude [V/V]'); set(gca, 'XTickLabel',[]); +ylim([5e-5, 4e1]); +leg = legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 2); +leg.ItemTokenSize(1) = 15; + +ax2 = nexttile; +hold on; +for i = 1:6 + plot(f, 180/pi*angle(G_iff(:,i, i)), 'color', [colors(1,:)]); +end +for i = 1:6 + plot(freqs, 180/pi*angle(squeeze(freqresp(Gm(sprintf('Vs%i', i), sprintf('u%i', i)), freqs, 'Hz'))), 'color', [colors(2,:)]); +end +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'lin'); +xlabel('Frequency [Hz]'); ylabel('Phase [deg]'); +hold off; +yticks(-360:90:360); +ylim([-90, 180]) + +linkaxes([ax1,ax2],'x'); +xlim([1, 1e3]); + +% Better Angular Alignment +% <> + +% One possible explanation of the increased coupling observed in Figure ref:fig:test_id31_first_id_int is the poor alignment between the external metrology axes (i.e. the interferometer supports) and the nano-hexapod axes. +% To estimate this alignment, a decentralized low-bandwidth feedback controller based on the nano-hexapod encoders was implemented. +% This allowed to perform two straight movements of the nano-hexapod along its $x$ and $y$ axes. +% During these two movements, the external metrology measurement was recorded and are shown in Figure ref:fig:test_id31_Rz_align_error. +% It was found that there is a misalignment of 2.7 degrees (rotation along the vertical axis) between the interferometer axes and nano-hexapod axes. +% This was corrected by adding an offset to the spindle angle. +% After alignment, the same movement was performed using the nano-hexapod while recording the signal of the external metrology. +% Results shown in Figure ref:fig:test_id31_Rz_align_correct are indeed indicating much better alignment. + + +%% Load Data +data_1_dx = h5scan(data_dir, 'align_int_enc_Rz', 'tx_first_scan', 2); +data_1_dy = h5scan(data_dir, 'align_int_enc_Rz', 'tx_first_scan', 3); +data_2_dx = h5scan(data_dir, 'align_int_enc_Rz', 'verif-after-correct-offset', 1); +data_2_dy = h5scan(data_dir, 'align_int_enc_Rz', 'verif-after-correct-offset', 2); + +% Estimation of Rz misalignment +p1 = polyfit(data_1_dx.Dx_int_filtered, data_1_dx.Dy_int_filtered, 1); +p2 = polyfit(data_1_dy.Dx_int_filtered, data_1_dy.Dy_int_filtered, 1); + +Rz_error = (atan(p1(1)) + atan(p2(1))-pi/2)/2; % ~3 degrees + +%% Estimation of the Rz misalignment +figure; +hold on; +plot(1e6*data_1_dx.Dx_int_filtered, 1e6*data_1_dx.Dy_int_filtered, 'color', colors(2,:), 'DisplayName', 'Measurement') +plot(1e6*data_1_dy.Dx_int_filtered, 1e6*data_1_dy.Dy_int_filtered, 'color', colors(2,:), 'HandleVisibility', 'off') +plot( 1e6*[-10:10]*cos(Rz_error), 1e6*[-10:10]*sin(Rz_error), 'k--', 'DisplayName', sprintf('$\\epsilon_{R_z} = %.1f$ deg', Rz_error*180/pi)) +plot(-1e6*[-10:10]*sin(Rz_error), 1e6*[-10:10]*cos(Rz_error), 'k--', 'HandleVisibility', 'off') +hold off; +xlabel('Interf $D_x$ [$\mu$m]'); +ylabel('Interf $D_y$ [$\mu$m]'); +axis equal +xlim([-10, 10]); ylim([-10, 10]); +xticks([-10:5:10]); yticks([-10:5:10]); +leg = legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; + +% Estimation of Rz misalignment after correcting the Rz angle +p1 = polyfit(data_2_dx.Dx_int_filtered, data_2_dx.Dy_int_filtered, 1); +p2 = polyfit(data_2_dy.Dx_int_filtered, data_2_dy.Dy_int_filtered, 1); + +Rz_error = (atan(p1(1)) + atan(p2(1))-pi/2)/2; % ~0.2 degrees + +%% Estimation of the Rz misalignment after correcting the Rz offset +figure; +hold on; +plot(1e6*data_2_dx.Dx_int_filtered, 1e6*data_2_dx.Dy_int_filtered, 'color', colors(5,:), 'DisplayName', 'Measurement') +plot(1e6*data_2_dy.Dx_int_filtered, 1e6*data_2_dy.Dy_int_filtered, 'color', colors(5,:), 'HandleVisibility', 'off') +plot( 1e6*[-10:10]*cos(Rz_error), 1e6*[-10:10]*sin(Rz_error), 'k--', 'DisplayName', sprintf('$\\epsilon_{R_z} = %.1f$ deg', Rz_error*180/pi)) +plot(-1e6*[-10:10]*sin(Rz_error), 1e6*[-10:10]*cos(Rz_error), 'k--', 'HandleVisibility', 'off') +hold off; +xlabel('Interf $D_x$ [$\mu$m]'); +ylabel('Interf $D_y$ [$\mu$m]'); +axis equal +xlim([-10, 10]); ylim([-10, 10]); +xticks([-10:5:10]); yticks([-10:5:10]); +leg = legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; + + + +% #+name: fig:test_id31_Rz_align_error +% #+caption: Measurement of the Nano-Hexapod axes in the frame of the external metrology. Before alignment (\subref{fig:test_id31_Rz_align_error}) and after alignment (\subref{fig:test_id31_Rz_align_correct}). +% #+attr_latex: :options [htbp] +% #+begin_figure +% #+attr_latex: :caption \subcaption{\label{fig:test_id31_Rz_align_error}Before alignment} +% #+attr_latex: :options {0.49\textwidth} +% #+begin_subfigure +% #+attr_latex: :scale 1 +% [[file:figs/test_id31_Rz_align_error.png]] +% #+end_subfigure +% #+attr_latex: :caption \subcaption{\label{fig:test_id31_Rz_align_correct}After alignment} +% #+attr_latex: :options {0.49\textwidth} +% #+begin_subfigure +% #+attr_latex: :scale 1 +% [[file:figs/test_id31_Rz_align_correct.png]] +% #+end_subfigure +% #+end_figure + +% The plant dynamics was identified again after the fine alignment and is compared with the model dynamics in Figure ref:fig:test_id31_first_id_int_better_rz_align. +% Compared to the initial identification shown in Figure ref:fig:test_id31_first_id_int, the obtained coupling has decreased and is now close to the coupling obtained with the multi-body model. +% At low frequency (below $10\,\text{Hz}$) all the off-diagonal elements have an amplitude $\approx 100$ times lower compared to the diagonal elements, indicating that a low bandwidth feedback controller can be implemented in a decentralized way (i.e. $6$ SISO controllers). +% Between $650\,\text{Hz}$ and $1000\,\text{Hz}$, several modes can be observed that are due to flexible modes of the top platform and modes of the two spheres adjustment mechanism. +% The flexible modes of the top platform can be passively damped while the modes of the two reference spheres should not be present in the final application. + + +%% Identification of the plant after Rz alignment +data_align = load('2023-08-17_17-37_ol_plant_m0_Wz0_new_Rz_align.mat'); + +G_int_align = zeros(length(f), 6, 6); +for i_strut = 1:6 + eL = [data_align.(sprintf("uL%i", i_strut)).e_L1 ; data_align.(sprintf("uL%i", i_strut)).e_L2 ; data_align.(sprintf("uL%i", i_strut)).e_L3 ; data_align.(sprintf("uL%i", i_strut)).e_L4 ; data_align.(sprintf("uL%i", i_strut)).e_L5 ; data_align.(sprintf("uL%i", i_strut)).e_L6]'; + + G_int_align(:,:,i_strut) = tfestimate(data_align.(sprintf("uL%i", i_strut)).id_plant, eL, win, Noverlap, Nfft, 1/Ts); +end + +%% Obtained transfer function from generated voltages to measured voltages on the piezoelectric force sensor +figure; +tiledlayout(1, 1, 'TileSpacing', 'compact', 'Padding', 'None'); + +nexttile(); +hold on; +for i = 1:5 + for j = i+1:6 + plot(f, abs(G_int_align(:, i, j)), 'color', [colors(1,:), 0.2], ... + 'HandleVisibility', 'off'); + plot(freqs, abs(squeeze(freqresp(Gm(sprintf('eL%i', i), sprintf('u%i', j)), freqs, 'Hz'))), 'color', [colors(2,:), 0.2], ... + 'HandleVisibility', 'off'); + end +end +plot(f, abs(G_int_align(:, 1, 1)), 'color', [colors(1,:)], ... + 'DisplayName', '$\epsilon\mathcal{L}_i/u_i$ meas'); +plot(freqs, abs(squeeze(freqresp(Gm(sprintf('eL%i', 1), sprintf('u%i', 1)), freqs, 'Hz'))), 'color', [colors(2,:)], ... + 'DisplayName', '$\epsilon\mathcal{L}_i/u_i$ model'); +for i = 2:6 + plot(f, abs(G_int_align(:,i, i)), 'color', [colors(1,:)], ... + 'HandleVisibility', 'off'); + plot(freqs, abs(squeeze(freqresp(Gm(sprintf('eL%i', i), sprintf('u%i', i)), freqs, 'Hz'))), 'color', [colors(2,:)], ... + 'HandleVisibility', 'off'); +end +plot(f, abs(G_int_align(:, 1, 2)), 'color', [colors(1,:), 0.2], ... + 'DisplayName', '$\epsilon\mathcal{L}_i/u_j$ meas'); +plot(freqs, abs(squeeze(freqresp(Gm(sprintf('eL%i', 1), sprintf('u%i', 2)), freqs, 'Hz'))), 'color', [colors(2,:), 0.2], ... + 'DisplayName', '$\epsilon\mathcal{L}_i/u_j$ model'); +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log'); +xlabel('Frequency [Hz]'); ylabel('Amplitude [m/V]'); +xlim([1, 1e3]); ylim([2e-9, 2e-4]); +leg = legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 2); +leg.ItemTokenSize(1) = 15; + +% Effect of Payload Mass +% <> + +% In order to see how the system dynamics changes with the payload, open-loop identification was performed for four payload conditions that are shown in Figure ref:fig:test_id31_picture_masses. +% The obtained direct terms are compared with the model dynamics in Figure ref:fig:test_nhexa_comp_simscape_diag_masses. +% It is shown that the model dynamics well predicts the measured dynamics for all payload conditions. +% Therefore the model can be used for model-based control is necessary. + +% It is interesting to note that the anti-resonances in the force sensor plant are now appearing as minimum-phase, as the model predicts (Figure ref:fig:test_id31_comp_simscape_iff_diag_masses). + +% #+name: fig:test_id31_picture_masses +% #+caption: The four tested payload conditions. (\subref{fig:test_id31_picture_mass_m0}) without payload. (\subref{fig:test_id31_picture_mass_m1}) with $13\,\text{kg}$ payload. (\subref{fig:test_id31_picture_mass_m2}) with $26\,\text{kg}$ payload. (\subref{fig:test_id31_picture_mass_m3}) with $39\,\text{kg}$ payload. +% #+attr_latex: :options [htbp] +% #+begin_figure +% #+attr_latex: :caption \subcaption{\label{fig:test_id31_picture_mass_m0}$m=0\,\text{kg}$} +% #+attr_latex: :options {0.24\textwidth} +% #+begin_subfigure +% #+attr_latex: :width 0.99\linewidth +% [[file:figs/test_id31_picture_mass_m0.jpg]] +% #+end_subfigure +% #+attr_latex: :caption \subcaption{\label{fig:test_id31_picture_mass_m1}$m=13\,\text{kg}$} +% #+attr_latex: :options {0.24\textwidth} +% #+begin_subfigure +% #+attr_latex: :width 0.99\linewidth +% [[file:figs/test_id31_picture_mass_m1.jpg]] +% #+end_subfigure +% #+attr_latex: :caption \subcaption{\label{fig:test_id31_picture_mass_m2}$m=26\,\text{kg}$} +% #+attr_latex: :options {0.24\textwidth} +% #+begin_subfigure +% #+attr_latex: :width 0.99\linewidth +% [[file:figs/test_id31_picture_mass_m2.jpg]] +% #+end_subfigure +% #+attr_latex: :caption \subcaption{\label{fig:test_id31_picture_mass_m3}$m=39\,\text{kg}$} +% #+attr_latex: :options {0.24\textwidth} +% #+begin_subfigure +% #+attr_latex: :width 0.99\linewidth +% [[file:figs/test_id31_picture_mass_m3.jpg]] +% #+end_subfigure +% #+end_figure + + +%% Identify the model dynamics for all payload conditions +% Initialize each Simscape model elements +initializeGround(); +initializeGranite(); +initializeTy(); +initializeRy(); +initializeRz(); +initializeMicroHexapod(); +initializeNanoHexapod('flex_bot_type', '2dof', ... + 'flex_top_type', '3dof', ... + 'motion_sensor_type', 'plates', ... + 'actuator_type', '2dof'); +initializeSample('type', '0'); + +initializeSimscapeConfiguration('gravity', false); +initializeDisturbances('enable', false); +initializeLoggingConfiguration('log', 'none'); +initializeController('type', 'open-loop'); +initializeReferences(); + +% Input/Output definition +clear io; io_i = 1; +io(io_i) = linio([mdl, '/Controller'], 1, 'openinput'); io_i = io_i + 1; % Actuator Inputs +io(io_i) = linio([mdl, '/Micro-Station'], 3, 'openoutput', [], 'Vs'); io_i = io_i + 1; % Force Sensors +io(io_i) = linio([mdl, '/Tracking Error'], 1, 'openoutput', [], 'EdL'); io_i = io_i + 1; % Position Errors + +initializeSample('type', '0'); +Gm_m0_Wz0 = linearize(mdl, io); +Gm_m0_Wz0.InputName = {'u1', 'u2', 'u3', 'u4', 'u5', 'u6'}; +Gm_m0_Wz0.OutputName = {'Vs1', 'Vs2', 'Vs3', 'Vs4', 'Vs5', 'Vs6', ... + 'eL1', 'eL2', 'eL3', 'eL4', 'eL5', 'eL6'}; + +initializeSample('type', '1'); +Gm_m1_Wz0 = linearize(mdl, io); +Gm_m1_Wz0.InputName = {'u1', 'u2', 'u3', 'u4', 'u5', 'u6'}; +Gm_m1_Wz0.OutputName = {'Vs1', 'Vs2', 'Vs3', 'Vs4', 'Vs5', 'Vs6', ... + 'eL1', 'eL2', 'eL3', 'eL4', 'eL5', 'eL6'}; + +initializeSample('type', '2'); +Gm_m2_Wz0 = linearize(mdl, io); +Gm_m2_Wz0.InputName = {'u1', 'u2', 'u3', 'u4', 'u5', 'u6'}; +Gm_m2_Wz0.OutputName = {'Vs1', 'Vs2', 'Vs3', 'Vs4', 'Vs5', 'Vs6', ... + 'eL1', 'eL2', 'eL3', 'eL4', 'eL5', 'eL6'}; + +initializeSample('type', '3'); +Gm_m3_Wz0 = linearize(mdl, io); +Gm_m3_Wz0.InputName = {'u1', 'u2', 'u3', 'u4', 'u5', 'u6'}; +Gm_m3_Wz0.OutputName = {'Vs1', 'Vs2', 'Vs3', 'Vs4', 'Vs5', 'Vs6', ... + 'eL1', 'eL2', 'eL3', 'eL4', 'eL5', 'eL6'}; + +%% Identify the plant from experimental data - All payloads + +% Load identification data +data_m0_Wz0 = load('2023-08-08_16-17_ol_plant_m0_Wz0.mat'); +data_m1_Wz0 = load('2023-08-08_18-57_ol_plant_m1_Wz0.mat'); +data_m2_Wz0 = load('2023-08-08_17-12_ol_plant_m2_Wz0.mat'); +data_m3_Wz0 = load('2023-08-08_18-20_ol_plant_m3_Wz0.mat'); + +% Sampling Time [s] +Ts = 1e-4; + +% Hannning Windows +Nfft = floor(2.0/Ts); +win = hanning(Nfft); +Noverlap = floor(Nfft/2); + +% And we get the frequency vector +[~, f] = tfestimate(data_m0_Wz0.uL1.id_plant, data_m0_Wz0.uL1.e_L1, win, Noverlap, Nfft, 1/Ts); + +% No payload +G_iff_m0_Wz0 = zeros(length(f), 6, 6); +for i_strut = 1:6 + eL = [data_m0_Wz0.(sprintf("uL%i", i_strut)).Vs1 ; data_m0_Wz0.(sprintf("uL%i", i_strut)).Vs2 ; data_m0_Wz0.(sprintf("uL%i", i_strut)).Vs3 ; data_m0_Wz0.(sprintf("uL%i", i_strut)).Vs4 ; data_m0_Wz0.(sprintf("uL%i", i_strut)).Vs5 ; data_m0_Wz0.(sprintf("uL%i", i_strut)).Vs6]'; + + G_iff_m0_Wz0(:,:,i_strut) = tfestimate(data_m0_Wz0.(sprintf("uL%i", i_strut)).id_plant, eL, win, Noverlap, Nfft, 1/Ts); +end + +G_int_m0_Wz0 = zeros(length(f), 6, 6); +for i_strut = 1:6 + eL = [data_m0_Wz0.(sprintf("uL%i", i_strut)).e_L1 ; data_m0_Wz0.(sprintf("uL%i", i_strut)).e_L2 ; data_m0_Wz0.(sprintf("uL%i", i_strut)).e_L3 ; data_m0_Wz0.(sprintf("uL%i", i_strut)).e_L4 ; data_m0_Wz0.(sprintf("uL%i", i_strut)).e_L5 ; data_m0_Wz0.(sprintf("uL%i", i_strut)).e_L6]'; + + G_int_m0_Wz0(:,:,i_strut) = tfestimate(data_m0_Wz0.(sprintf("uL%i", i_strut)).id_plant, eL, win, Noverlap, Nfft, 1/Ts); +end + +% 1 "payload layer" +G_iff_m1_Wz0 = zeros(length(f), 6, 6); +for i_strut = 1:6 + eL = [data_m1_Wz0.(sprintf("uL%i", i_strut)).Vs1 ; data_m1_Wz0.(sprintf("uL%i", i_strut)).Vs2 ; data_m1_Wz0.(sprintf("uL%i", i_strut)).Vs3 ; data_m1_Wz0.(sprintf("uL%i", i_strut)).Vs4 ; data_m1_Wz0.(sprintf("uL%i", i_strut)).Vs5 ; data_m1_Wz0.(sprintf("uL%i", i_strut)).Vs6]'; + + G_iff_m1_Wz0(:,:,i_strut) = tfestimate(data_m1_Wz0.(sprintf("uL%i", i_strut)).id_plant, eL, win, Noverlap, Nfft, 1/Ts); +end + +G_int_m1_Wz0 = zeros(length(f), 6, 6); +for i_strut = 1:6 + eL = [data_m1_Wz0.(sprintf("uL%i", i_strut)).e_L1 ; data_m1_Wz0.(sprintf("uL%i", i_strut)).e_L2 ; data_m1_Wz0.(sprintf("uL%i", i_strut)).e_L3 ; data_m1_Wz0.(sprintf("uL%i", i_strut)).e_L4 ; data_m1_Wz0.(sprintf("uL%i", i_strut)).e_L5 ; data_m1_Wz0.(sprintf("uL%i", i_strut)).e_L6]'; + + G_int_m1_Wz0(:,:,i_strut) = tfestimate(data_m1_Wz0.(sprintf("uL%i", i_strut)).id_plant, eL, win, Noverlap, Nfft, 1/Ts); +end + +% 2 "payload layers" +G_iff_m2_Wz0 = zeros(length(f), 6, 6); +for i_strut = 1:6 + eL = [data_m2_Wz0.(sprintf("uL%i", i_strut)).Vs1 ; data_m2_Wz0.(sprintf("uL%i", i_strut)).Vs2 ; data_m2_Wz0.(sprintf("uL%i", i_strut)).Vs3 ; data_m2_Wz0.(sprintf("uL%i", i_strut)).Vs4 ; data_m2_Wz0.(sprintf("uL%i", i_strut)).Vs5 ; data_m2_Wz0.(sprintf("uL%i", i_strut)).Vs6]'; + + G_iff_m2_Wz0(:,:,i_strut) = tfestimate(data_m2_Wz0.(sprintf("uL%i", i_strut)).id_plant, eL, win, Noverlap, Nfft, 1/Ts); +end + +G_int_m2_Wz0 = zeros(length(f), 6, 6); +for i_strut = 1:6 + eL = [data_m2_Wz0.(sprintf("uL%i", i_strut)).e_L1 ; data_m2_Wz0.(sprintf("uL%i", i_strut)).e_L2 ; data_m2_Wz0.(sprintf("uL%i", i_strut)).e_L3 ; data_m2_Wz0.(sprintf("uL%i", i_strut)).e_L4 ; data_m2_Wz0.(sprintf("uL%i", i_strut)).e_L5 ; data_m2_Wz0.(sprintf("uL%i", i_strut)).e_L6]'; + + G_int_m2_Wz0(:,:,i_strut) = tfestimate(data_m2_Wz0.(sprintf("uL%i", i_strut)).id_plant, eL, win, Noverlap, Nfft, 1/Ts); +end + +% 3 "payload layers" +G_iff_m3_Wz0 = zeros(length(f), 6, 6); +for i_strut = 1:6 + eL = [data_m3_Wz0.(sprintf("uL%i", i_strut)).Vs1 ; data_m3_Wz0.(sprintf("uL%i", i_strut)).Vs2 ; data_m3_Wz0.(sprintf("uL%i", i_strut)).Vs3 ; data_m3_Wz0.(sprintf("uL%i", i_strut)).Vs4 ; data_m3_Wz0.(sprintf("uL%i", i_strut)).Vs5 ; data_m3_Wz0.(sprintf("uL%i", i_strut)).Vs6]'; + + G_iff_m3_Wz0(:,:,i_strut) = tfestimate(data_m3_Wz0.(sprintf("uL%i", i_strut)).id_plant, eL, win, Noverlap, Nfft, 1/Ts); +end + +G_int_m3_Wz0 = zeros(length(f), 6, 6); +for i_strut = 1:6 + eL = [data_m3_Wz0.(sprintf("uL%i", i_strut)).e_L1 ; data_m3_Wz0.(sprintf("uL%i", i_strut)).e_L2 ; data_m3_Wz0.(sprintf("uL%i", i_strut)).e_L3 ; data_m3_Wz0.(sprintf("uL%i", i_strut)).e_L4 ; data_m3_Wz0.(sprintf("uL%i", i_strut)).e_L5 ; data_m3_Wz0.(sprintf("uL%i", i_strut)).e_L6]'; + + G_int_m3_Wz0(:,:,i_strut) = tfestimate(data_m3_Wz0.(sprintf("uL%i", i_strut)).id_plant, eL, win, Noverlap, Nfft, 1/Ts); +end + +%% Obtained transfer function from generated voltages to measured voltages on the piezoelectric force sensor +figure; +tiledlayout(3, 1, 'TileSpacing', 'compact', 'Padding', 'None'); + +ax1 = nexttile([2,1]); +hold on; +plot(f, abs(G_int_m0_Wz0(:, 1, 1)), 'color', [colors(1,:), 0.5], ... + 'DisplayName', 'Meas (0kg)'); +for i = 2:6 + plot(f, abs(G_int_m0_Wz0(:,i, i)), 'color', [colors(1,:), 0.5], ... + 'HandleVisibility', 'off') +end +plot(f, abs(G_int_m1_Wz0(:, 1, 1)), 'color', [colors(2,:), 0.5], ... + 'DisplayName', 'Meas (13kg)'); +for i = 2:6 + plot(f, abs(G_int_m1_Wz0(:,i, i)), 'color', [colors(2,:), 0.5], ... + 'HandleVisibility', 'off') +end +plot(f, abs(G_int_m2_Wz0(:, 1, 1)), 'color', [colors(3,:), 0.5], ... + 'DisplayName', 'Meas (26kg)'); +for i = 2:6 + plot(f, abs(G_int_m2_Wz0(:,i, i)), 'color', [colors(3,:), 0.5], ... + 'HandleVisibility', 'off') +end +plot(f, abs(G_int_m3_Wz0(:, 1, 1)), 'color', [colors(4,:), 0.5], ... + 'DisplayName', 'Meas (39kg)'); +for i = 2:6 + plot(f, abs(G_int_m3_Wz0(:,i, i)), 'color', [colors(4,:), 0.5], ... + 'HandleVisibility', 'off') +end +plot(freqs, abs(squeeze(freqresp(Gm_m0_Wz0('eL1', 'u1'), freqs, 'Hz'))), '--', 'color', colors(1,:), ... + 'DisplayName', 'Model (0kg)'); +plot(freqs, abs(squeeze(freqresp(Gm_m1_Wz0('eL1', 'u1'), freqs, 'Hz'))), '--', 'color', colors(2,:), ... + 'DisplayName', 'Model (13kg)'); +plot(freqs, abs(squeeze(freqresp(Gm_m2_Wz0('eL1', 'u1'), freqs, 'Hz'))), '--', 'color', colors(3,:), ... + 'DisplayName', 'Model (26kg)'); +plot(freqs, abs(squeeze(freqresp(Gm_m3_Wz0('eL1', 'u1'), freqs, 'Hz'))), '--', 'color', colors(4,:), ... + 'DisplayName', 'Model (39kg)'); +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log'); +ylabel('Amplitude [m/V]'); set(gca, 'XTickLabel',[]); +ylim([1e-8, 5e-4]); +leg = legend('location', 'southwest', 'FontSize', 8, 'NumColumns', 2); +leg.ItemTokenSize(1) = 15; + +ax2 = nexttile; +hold on; +for i =1:6 + plot(f, 180/pi*angle(G_int_m0_Wz0(:,i, i)), 'color', [colors(1,:), 0.5]); +end +for i =1:6 + plot(f, 180/pi*angle(G_int_m1_Wz0(:,i, i)), 'color', [colors(2,:), 0.5]); +end +for i =1:6 + plot(f, 180/pi*angle(G_int_m2_Wz0(:,i, i)), 'color', [colors(3,:), 0.5]); +end +for i =1:6 + plot(f, 180/pi*angle(G_int_m3_Wz0(:,i, i)), 'color', [colors(4,:), 0.5]); +end +plot(freqs, 180/pi*angle(squeeze(freqresp(Gm_m0_Wz0('eL1', 'u1'), freqs, 'Hz'))), '--', 'color', colors(1,:)) +plot(freqs, 180/pi*angle(squeeze(freqresp(Gm_m1_Wz0('eL1', 'u1'), freqs, 'Hz'))), '--', 'color', colors(2,:)) +plot(freqs, 180/pi*angle(squeeze(freqresp(Gm_m2_Wz0('eL1', 'u1'), freqs, 'Hz'))), '--', 'color', colors(3,:)) +plot(freqs, 180/pi*angle(squeeze(freqresp(Gm_m3_Wz0('eL1', 'u1'), freqs, 'Hz'))), '--', 'color', colors(4,:)) +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'lin'); +xlabel('Frequency [Hz]'); ylabel('Phase [deg]'); +hold off; +yticks(-360:90:360); +ylim([-90, 180]) + +linkaxes([ax1,ax2],'x'); +xlim([10, 5e2]); +xticks([10, 20, 50, 100, 200, 500]) + +%% Obtained transfer function from generated voltages to measured voltages on the piezoelectric force sensor +figure; +tiledlayout(3, 1, 'TileSpacing', 'compact', 'Padding', 'None'); + +ax1 = nexttile([2,1]); +hold on; +plot(f, abs(G_iff_m0_Wz0(:, 1, 1)), 'color', [colors(1,:), 0.5], ... + 'DisplayName', 'Meas (0kg)'); +for i = 2:6 + plot(f, abs(G_iff_m0_Wz0(:,i, i)), 'color', [colors(1,:), 0.5], ... + 'HandleVisibility', 'off') +end +plot(f, abs(G_iff_m1_Wz0(:, 1, 1)), 'color', [colors(2,:), 0.5], ... + 'DisplayName', 'Meas (13kg)'); +for i = 2:6 + plot(f, abs(G_iff_m1_Wz0(:,i, i)), 'color', [colors(2,:), 0.5], ... + 'HandleVisibility', 'off') +end +plot(f, abs(G_iff_m2_Wz0(:, 1, 1)), 'color', [colors(3,:), 0.5], ... + 'DisplayName', 'Meas (26kg)'); +for i = 2:6 + plot(f, abs(G_iff_m2_Wz0(:,i, i)), 'color', [colors(3,:), 0.5], ... + 'HandleVisibility', 'off') +end +plot(f, abs(G_iff_m3_Wz0(:, 1, 1)), 'color', [colors(4,:), 0.5], ... + 'DisplayName', 'Meas (39kg)'); +for i = 2:6 + plot(f, abs(G_iff_m3_Wz0(:,i, i)), 'color', [colors(4,:), 0.5], ... + 'HandleVisibility', 'off') +end +plot(freqs, abs(squeeze(freqresp(Gm_m0_Wz0('Vs1', 'u1'), freqs, 'Hz'))), '--', 'color', colors(1,:), ... + 'DisplayName', 'Model (0kg)'); +plot(freqs, abs(squeeze(freqresp(Gm_m1_Wz0('Vs1', 'u1'), freqs, 'Hz'))), '--', 'color', colors(2,:), ... + 'DisplayName', 'Model (13kg)'); +plot(freqs, abs(squeeze(freqresp(Gm_m2_Wz0('Vs1', 'u1'), freqs, 'Hz'))), '--', 'color', colors(3,:), ... + 'DisplayName', 'Model (26kg)'); +plot(freqs, abs(squeeze(freqresp(Gm_m3_Wz0('Vs1', 'u1'), freqs, 'Hz'))), '--', 'color', colors(4,:), ... + 'DisplayName', 'Model (39kg)'); +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log'); +ylabel('Amplitude [V/V]'); set(gca, 'XTickLabel',[]); +ylim([1e-2, 4e1]); +leg = legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 2); +leg.ItemTokenSize(1) = 15; + +ax2 = nexttile; +hold on; +for i =1:6 + plot(f, 180/pi*angle(G_iff_m0_Wz0(:,i, i)), 'color', [colors(1,:), 0.5]); +end +for i =1:6 + plot(f, 180/pi*angle(G_iff_m1_Wz0(:,i, i)), 'color', [colors(2,:), 0.5]); +end +for i =1:6 + plot(f, 180/pi*angle(G_iff_m2_Wz0(:,i, i)), 'color', [colors(3,:), 0.5]); +end +for i =1:6 + plot(f, 180/pi*angle(G_iff_m3_Wz0(:,i, i)), 'color', [colors(4,:), 0.5]); +end +plot(freqs, 180/pi*angle(squeeze(freqresp(Gm_m0_Wz0('Vs1', 'u1'), freqs, 'Hz'))), '--', 'color', colors(1,:)) +plot(freqs, 180/pi*angle(squeeze(freqresp(Gm_m1_Wz0('Vs1', 'u1'), freqs, 'Hz'))), '--', 'color', colors(2,:)) +plot(freqs, 180/pi*angle(squeeze(freqresp(Gm_m2_Wz0('Vs1', 'u1'), freqs, 'Hz'))), '--', 'color', colors(3,:)) +plot(freqs, 180/pi*angle(squeeze(freqresp(Gm_m3_Wz0('Vs1', 'u1'), freqs, 'Hz'))), '--', 'color', colors(4,:)) +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'lin'); +xlabel('Frequency [Hz]'); ylabel('Phase [deg]'); +hold off; +yticks(-360:90:360); +ylim([-90, 180]) + +linkaxes([ax1,ax2],'x'); +xlim([10, 5e2]); +xticks([10, 20, 50, 100, 200, 500]) + +% Effect of Spindle Rotation +% <> + +% To verify that all the kinematics in Figure ref:fig:test_id31_block_schematic_plant are correct and to check whether the system dynamics is affected by Spindle rotation of not, three identification experiments were performed: no spindle rotation, spindle rotation at $36\,\text{deg}/s$ and at $180\,\text{deg}/s$. + +% The comparison of the obtained dynamics from command signal $u$ to estimated strut error $\epsilon\mathcal{L}$ is done in Figure ref:fig:test_id31_effect_rotation. +% Both direct terms (Figure ref:fig:test_id31_effect_rotation_direct) and coupling terms (Figure ref:fig:test_id31_effect_rotation_coupling) are unaffected by the rotation. +% The same can be observed for the dynamics from the command signal to the encoders and to the force sensors. +% This confirms that the rotation has no significant effect on the plant dynamics. +% This also indicates that the metrology kinematics is correct and is working in real time. + + +%% Identify the model dynamics with Spindle rotation +initializeSample('type', '0'); +initializeReferences(... + 'Rz_type', 'rotating', ... + 'Rz_period', 360/36); % 36 deg/s, 6rpm +Gm_m0_Wz36 = linearize(mdl, io, 0.1); +Gm_m0_Wz36.InputName = {'u1', 'u2', 'u3', 'u4', 'u5', 'u6'}; +Gm_m0_Wz36.OutputName = {'Vs1', 'Vs2', 'Vs3', 'Vs4', 'Vs5', 'Vs6', ... + 'eL1', 'eL2', 'eL3', 'eL4', 'eL5', 'eL6'}; + +initializeReferences(... + 'Rz_type', 'rotating', ... + 'Rz_period', 360/180); % 180 deg/s, 30rpm +Gm_m0_Wz180 = linearize(mdl, io, 0.1); +Gm_m0_Wz180.InputName = {'u1', 'u2', 'u3', 'u4', 'u5', 'u6'}; +Gm_m0_Wz180.OutputName = {'Vs1', 'Vs2', 'Vs3', 'Vs4', 'Vs5', 'Vs6', ... + 'eL1', 'eL2', 'eL3', 'eL4', 'eL5', 'eL6'}; + +%% Identify the plant from experimental data - Effect of rotation + +% Load identification data +data_m0_Wz36 = load('2023-08-08_16-28_ol_plant_m0_Wz36.mat'); +data_m0_Wz180 = load('2023-08-08_16-45_ol_plant_m0_Wz180.mat'); + +% Spindle Rotation at 36 deg/s +G_iff_m0_Wz36 = zeros(length(f), 6, 6); +for i_strut = 1:6 + eL = [data_m0_Wz36.(sprintf("uL%i", i_strut)).Vs1 ; data_m0_Wz36.(sprintf("uL%i", i_strut)).Vs2 ; data_m0_Wz36.(sprintf("uL%i", i_strut)).Vs3 ; data_m0_Wz36.(sprintf("uL%i", i_strut)).Vs4 ; data_m0_Wz36.(sprintf("uL%i", i_strut)).Vs5 ; data_m0_Wz36.(sprintf("uL%i", i_strut)).Vs6]'; + + G_iff_m0_Wz36(:,:,i_strut) = tfestimate(data_m0_Wz36.(sprintf("uL%i", i_strut)).id_plant, eL, win, Noverlap, Nfft, 1/Ts); +end + +G_int_m0_Wz36 = zeros(length(f), 6, 6); +for i_strut = 1:6 + eL = [data_m0_Wz36.(sprintf("uL%i", i_strut)).e_L1 ; data_m0_Wz36.(sprintf("uL%i", i_strut)).e_L2 ; data_m0_Wz36.(sprintf("uL%i", i_strut)).e_L3 ; data_m0_Wz36.(sprintf("uL%i", i_strut)).e_L4 ; data_m0_Wz36.(sprintf("uL%i", i_strut)).e_L5 ; data_m0_Wz36.(sprintf("uL%i", i_strut)).e_L6]'; + + G_int_m0_Wz36(:,:,i_strut) = tfestimate(data_m0_Wz36.(sprintf("uL%i", i_strut)).id_plant, eL, win, Noverlap, Nfft, 1/Ts); +end + +% Spindle Rotation at 180 deg/s +G_iff_m0_Wz180 = zeros(length(f), 6, 6); +for i_strut = 1:6 + eL = [data_m0_Wz180.(sprintf("uL%i", i_strut)).Vs1 ; data_m0_Wz180.(sprintf("uL%i", i_strut)).Vs2 ; data_m0_Wz180.(sprintf("uL%i", i_strut)).Vs3 ; data_m0_Wz180.(sprintf("uL%i", i_strut)).Vs4 ; data_m0_Wz180.(sprintf("uL%i", i_strut)).Vs5 ; data_m0_Wz180.(sprintf("uL%i", i_strut)).Vs6]'; + + G_iff_m0_Wz180(:,:,i_strut) = tfestimate(data_m0_Wz180.(sprintf("uL%i", i_strut)).id_plant, eL, win, Noverlap, Nfft, 1/Ts); +end + +G_int_m0_Wz180 = zeros(length(f), 6, 6); +for i_strut = 1:6 + eL = [data_m0_Wz180.(sprintf("uL%i", i_strut)).e_L1 ; data_m0_Wz180.(sprintf("uL%i", i_strut)).e_L2 ; data_m0_Wz180.(sprintf("uL%i", i_strut)).e_L3 ; data_m0_Wz180.(sprintf("uL%i", i_strut)).e_L4 ; data_m0_Wz180.(sprintf("uL%i", i_strut)).e_L5 ; data_m0_Wz180.(sprintf("uL%i", i_strut)).e_L6]'; + + G_int_m0_Wz180(:,:,i_strut) = tfestimate(data_m0_Wz180.(sprintf("uL%i", i_strut)).id_plant, eL, win, Noverlap, Nfft, 1/Ts); +end + +% The identified dynamics are then saved for further use. +save('./mat/test_id31_simscape_open_loop_plants.mat', 'Gm_m0_Wz0', 'Gm_m0_Wz36', 'Gm_m0_Wz180', 'Gm_m1_Wz0', 'Gm_m2_Wz0', 'Gm_m3_Wz0'); +save('./mat/test_id31_identified_open_loop_plants.mat', 'G_int_m0_Wz0', 'G_int_m0_Wz36', 'G_int_m0_Wz180', 'G_int_m1_Wz0', 'G_int_m2_Wz0', 'G_int_m3_Wz0', ... + 'G_iff_m0_Wz0', 'G_iff_m0_Wz36', 'G_iff_m0_Wz180', 'G_iff_m1_Wz0', 'G_iff_m2_Wz0', 'G_iff_m3_Wz0', 'f'); + +figure; +tiledlayout(1, 1, 'TileSpacing', 'compact', 'Padding', 'None'); + +nexttile(); +hold on; +plot(f, abs(G_int_m0_Wz0(:, 1, 1)), 'color', [colors(1,:), 0.5], ... + 'DisplayName', '$\Omega_z = 0$'); +plot(f, abs(G_int_m0_Wz36(:, 1, 1)), 'color', [colors(2,:), 0.5], ... + 'DisplayName', '$\Omega_z = 36$ deg/s'); +plot(f, abs(G_int_m0_Wz180(:, 1, 1)), 'color', [colors(3,:), 0.5], ... + 'DisplayName', '$\Omega_z = 180$ deg/s'); +for i = 2:6 + plot(f, abs(G_int_m0_Wz0(:,i, i)), 'color', [colors(1,:), 0.5], ... + 'HandleVisibility', 'off') + plot(f, abs(G_int_m0_Wz36(:,i, i)), 'color', [colors(2,:), 0.5], ... + 'HandleVisibility', 'off') + plot(f, abs(G_int_m0_Wz180(:,i, i)), 'color', [colors(3,:), 0.5], ... + 'HandleVisibility', 'off') +end +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log'); +xlabel('Frequency [Hz]'); ylabel('Amplitude [m/V]'); +leg = legend('location', 'southwest', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; +xlim([10, 1e3]); ylim([1e-8, 2e-4]) + +figure; +tiledlayout(1, 1, 'TileSpacing', 'compact', 'Padding', 'None'); + +nexttile(); +hold on; +plot(f, abs(G_int_m0_Wz0(:, 1, 2)), 'color', [colors(1,:), 0.5], ... + 'DisplayName', '$\Omega_z = 0$'); +plot(f, abs(G_int_m0_Wz36(:, 1, 2)), 'color', [colors(2,:), 0.5], ... + 'DisplayName', '$\Omega_z = 36$ deg/s'); +plot(f, abs(G_int_m0_Wz180(:, 1, 2)), 'color', [colors(3,:), 0.5], ... + 'DisplayName', '$\Omega_z = 180$ deg/s'); +for i = 1:5 + for j = i+1:6 + plot(f, abs(G_int_m0_Wz0(:, i, j)), 'color', [colors(1,:), 0.5], ... + 'HandleVisibility', 'off'); + plot(f, abs(G_int_m0_Wz36(:, i, j)), 'color', [colors(2,:), 0.5], ... + 'HandleVisibility', 'off'); + plot(f, abs(G_int_m0_Wz180(:, i, j)), 'color', [colors(3,:), 0.5], ... + 'HandleVisibility', 'off'); + end +end +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log'); +xlabel('Frequency [Hz]'); ylabel('Amplitude [m/V]'); +leg = legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; +xlim([10, 1e3]); ylim([1e-8, 2e-4]) diff --git a/matlab/test_id31_3_iff.m b/matlab/test_id31_3_iff.m new file mode 100644 index 0000000..260b9ea --- /dev/null +++ b/matlab/test_id31_3_iff.m @@ -0,0 +1,561 @@ +% Matlab Init :noexport:ignore: + +%% test_id31_3_iff.m + +%% Clear Workspace and Close figures +clear; close all; clc; + +%% Intialize Laplace variable +s = zpk('s'); + +%% Path for functions, data and scripts +addpath('./mat/'); % Path for Data +addpath('./src/'); % Path for functions +addpath('./STEPS/'); % Path for STEPS +addpath('./subsystems/'); % Path for Subsystems Simulink files + +%% Data directory +data_dir = './mat/'; + +% Simulink Model name +mdl = 'nass_model_id31'; + +%% Colors for the figures +colors = colororder; + +%% Frequency Vector +freqs = logspace(log10(1), log10(2e3), 1000); + +%% Sampling Time +Ts = 1e-4; + +%% Specifications for Experiments +specs_dz_peak = 50; % [nm] +specs_dy_peak = 100; % [nm] +specs_ry_peak = 0.85; % [urad] +specs_dz_rms = 15; % [nm RMS] +specs_dy_rms = 30; % [nm RMS] +specs_ry_rms = 0.25; % [urad RMS] + +% IFF Plant +% <> + +% As the multi-body model is going to be used to evaluate the stability of the IFF controller and to optimize the achievable damping, it is first checked whether this model accurately represents the system dynamics. + +% In the previous section (Figure ref:fig:test_id31_comp_simscape_iff_diag_masses), it was shown that the model well captures the dynamics from each actuator to its collocated force sensor, and that for all considered payloads. +% Nevertheless, it is also important to well model the coupling in the system. +% To very that, instead of comparing the 36 elements of the $6 \times 6$ frequency response matrix from $\bm{u}$ to $\bm{V_s}$, only 6 elements are compared in Figure ref:fig:test_id31_comp_simscape_Vs. +% Similar results are obtained for all other 30 elements and for the different tested payload conditions. +% This confirms that the multi-body model can be used to tune the IFF controller. + + +% Load identified FRF for IFF Plant and Multi-Body Model +load('test_id31_identified_open_loop_plants.mat', 'G_iff_m0_Wz0', 'G_iff_m1_Wz0', 'G_iff_m2_Wz0', 'G_iff_m3_Wz0', 'f'); +load('test_id31_simscape_open_loop_plants.mat', 'Gm_m0_Wz0', 'Gm_m1_Wz0', 'Gm_m2_Wz0', 'Gm_m3_Wz0'); + +figure; +tiledlayout(2, 3, 'TileSpacing', 'tight', 'Padding', 'tight'); + +ax1 = nexttile(); +hold on; +plot(f, abs(G_iff_m0_Wz0(:, 1, 1))); +plot(freqs, abs(squeeze(freqresp(Gm_m0_Wz0('Vs1', 'u1'), freqs, 'Hz')))); +text(12, 4e1, '$V_{s1}/u_{1}$', 'Horiz','left', 'Vert','top') +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log'); +set(gca, 'XTickLabel',[]); ylabel('Amplitude [m/V]'); +yticks([1e-2, 1e-1, 1e0, 1e1]); + +ax2 = nexttile(); +hold on; +plot(f, abs(G_iff_m0_Wz0(:, 2, 1))); +plot(freqs, abs(squeeze(freqresp(Gm_m0_Wz0('Vs2', 'u1'), freqs, 'Hz')))); +text(12, 4e1, '$V_{s2}/u_{1}$', 'Horiz','left', 'Vert','top') +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log'); +set(gca, 'XTickLabel',[]); set(gca, 'YTickLabel',[]); + +ax3 = nexttile(); +hold on; +plot(f, abs(G_iff_m0_Wz0(:, 3, 1)), ... + 'DisplayName', 'Measurements'); +plot(freqs, abs(squeeze(freqresp(Gm_m0_Wz0('Vs3', 'u1'), freqs, 'Hz'))), ... + 'DisplayName', 'Model (2-DoF APA)'); +text(12, 4e1, '$V_{s3}/u_{1}$', 'Horiz','left', 'Vert','top') +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log'); +set(gca, 'XTickLabel',[]); set(gca, 'YTickLabel',[]); +leg = legend('location', 'southwest', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; + +ax4 = nexttile(); +hold on; +plot(f, abs(G_iff_m0_Wz0(:, 4, 1))); +plot(freqs, abs(squeeze(freqresp(Gm_m0_Wz0('Vs4', 'u1'), freqs, 'Hz')))); +text(12, 4e1, '$V_{s4}/u_{1}$', 'Horiz','left', 'Vert','top') +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log'); +xlabel('Frequency [Hz]'); ylabel('Amplitude [m/V]'); +xticks([10, 20, 50, 100, 200]) +yticks([1e-2, 1e-1, 1e0, 1e1]); + +ax5 = nexttile(); +hold on; +plot(f, abs(G_iff_m0_Wz0(:, 5, 1))); +plot(freqs, abs(squeeze(freqresp(Gm_m0_Wz0('Vs5', 'u1'), freqs, 'Hz')))); +text(12, 4e1, '$V_{s5}/u_{1}$', 'Horiz','left', 'Vert','top') +hold off; +xlabel('Frequency [Hz]'); set(gca, 'YTickLabel',[]); +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log'); +xticks([10, 20, 50, 100, 200]) + +ax6 = nexttile(); +hold on; +plot(f, abs(G_iff_m0_Wz0(:, 6, 1))); +plot(freqs, abs(squeeze(freqresp(Gm_m0_Wz0('Vs6', 'u1'), freqs, 'Hz')))); +text(12, 4e1, '$V_{s6}/u_{1}$', 'Horiz','left', 'Vert','top') +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log'); +xlabel('Frequency [Hz]'); set(gca, 'YTickLabel',[]); +xticks([10, 20, 50, 100, 200]) + +linkaxes([ax1,ax2,ax3,ax4,ax5,ax6],'xy'); +xlim([10, 5e2]); ylim([1e-2, 5e1]); + +% IFF Controller +% <> + +% A decentralized IFF controller was designed such that it adds damping to the suspension modes of the nano-hexapod for all considered payloads. +% The frequency of the suspension modes are ranging from $\approx 30\,\text{Hz}$ to $\approx 250\,\text{Hz}$ (Figure ref:fig:test_id31_comp_simscape_iff_diag_masses), and therefore the IFF controller should provide integral action in this frequency range. +% A second order high pass filter (cut-off frequency of $10\,\text{Hz}$) was added to limit the low frequency gain eqref:eq:test_id31_Kiff. + +% \begin{equation}\label{eq:test_id31_Kiff} +% K_{\text{IFF}} = g_0 \cdot \underbrace{\frac{1}{s}}_{\text{int}} \cdot \underbrace{\frac{s^2/\omega_z^2}{s^2/\omega_z^2 + 2\xi_z s /\omega_z + 1}}_{\text{2nd order LPF}},\quad \left(g_0 = -100,\ \omega_z = 2\pi10\,\text{rad/s},\ \xi_z = 0.7\right) +% \end{equation} + +% The bode plot of the decentralized IFF controller is shown in Figure ref:fig:test_id31_Kiff_bode_plot and the "decentralized loop-gains" for all considered payload masses are shown in Figure ref:fig:test_id31_Kiff_loop_gain. +% It can be seen that the loop-gain is larger than $1$ around suspension modes indicating that some damping should be added to the suspension modes. + + +%% IFF Controller Design +% Second order high pass filter +wz = 2*pi*10; +xiz = 0.7; +Ghpf = (s^2/wz^2)/(s^2/wz^2 + 2*xiz*s/wz + 1); + +% IFF Controller +Kiff = -1e2 * ... % Gain + 1/(0.01*2*pi + s) * ... % LPF: provides integral action + Ghpf * ... % 2nd order HPF (limit low frequency gain) + eye(6); % Diagonal 6x6 controller (i.e. decentralized) + +Kiff.InputName = {'Vs1', 'Vs2', 'Vs3', 'Vs4', 'Vs5', 'Vs6'}; +Kiff.OutputName = {'u1', 'u2', 'u3', 'u4', 'u5', 'u6'}; + +% The designed IFF controller is saved +save('./mat/test_id31_K_iff.mat', 'Kiff'); + +%% Bode plot of the designed decentralized IFF controller +figure; +tiledlayout(3, 1, 'TileSpacing', 'compact', 'Padding', 'None'); + +ax1 = nexttile([2,1]); +hold on; +plot(f, abs(squeeze(freqresp(Kiff(1,1), f, 'Hz'))), 'color', colors(1,:)); +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log'); +ylabel('Amplitude'); set(gca, 'XTickLabel',[]); +ylim([1e-2, 1e1]); + +ax2 = nexttile; +hold on; +plot(f, 180/pi*angle(squeeze(freqresp(Kiff(1,1), f, 'Hz'))), 'color', colors(1,:)); +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'lin'); +xlabel('Frequency [Hz]'); ylabel('Phase [deg]'); +hold off; +yticks(-360:90:360); +ylim([-180, 180]) + +linkaxes([ax1,ax2],'x'); +xlim([1, 1e3]); + +%% Loop gain for the decentralized IFF controller +Kiff_frf = squeeze(freqresp(Kiff(1,1), f, 'Hz')); + +figure; +tiledlayout(3, 1, 'TileSpacing', 'compact', 'Padding', 'None'); + +ax1 = nexttile([2,1]); +hold on; +plot(f, abs(G_iff_m0_Wz0(:, 1, 1).*Kiff_frf), 'color', colors(1,:), ... + 'DisplayName', '$m = 0$ kg'); +plot(f, abs(G_iff_m1_Wz0(:, 1, 1).*Kiff_frf), 'color', colors(2,:), ... + 'DisplayName', '$m = 13$ kg'); +plot(f, abs(G_iff_m2_Wz0(:, 1, 1).*Kiff_frf), 'color', colors(3,:), ... + 'DisplayName', '$m = 26$ kg'); +plot(f, abs(G_iff_m3_Wz0(:, 1, 1).*Kiff_frf), 'color', colors(4,:), ... + 'DisplayName', '$m = 39$ kg'); +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log'); +ylabel('Loop Gain'); set(gca, 'XTickLabel',[]); +ylim([1e-2, 1e1]); +leg = legend('location', 'northwest', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; + +ax2 = nexttile; +hold on; +plot(f, 180/pi*angle(-G_iff_m0_Wz0(:,1,1).*Kiff_frf), 'color', colors(1,:)); +plot(f, 180/pi*angle(-G_iff_m1_Wz0(:,1,1).*Kiff_frf), 'color', colors(2,:)); +plot(f, 180/pi*angle(-G_iff_m2_Wz0(:,1,1).*Kiff_frf), 'color', colors(3,:)); +plot(f, 180/pi*angle(-G_iff_m3_Wz0(:,1,1).*Kiff_frf), 'color', colors(4,:)); +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'lin'); +xlabel('Frequency [Hz]'); ylabel('Phase [deg]'); +hold off; +yticks(-360:90:360); +ylim([-180, 180]) + +linkaxes([ax1,ax2],'x'); +xlim([1, 1e3]); + + + +% #+name: fig:test_id31_Kiff +% #+caption: Bode plot of the decentralized IFF controller (\subref{fig:test_id31_Kiff_bode_plot}). The decentralized controller $K_{\text{IFF}}$ multiplied by the identified dynamics from $u_1$ to $V_{s1}$ for all payloads are shown in (\subref{fig:test_id31_Kiff_loop_gain}) +% #+attr_latex: :options [htbp] +% #+begin_figure +% #+attr_latex: :caption \subcaption{\label{fig:test_id31_Kiff_bode_plot}Bode plot of $K_{\text{IFF}}$} +% #+attr_latex: :options {0.49\textwidth} +% #+begin_subfigure +% #+attr_latex: :width 0.95\linewidth +% [[file:figs/test_id31_Kiff_bode_plot.png]] +% #+end_subfigure +% #+attr_latex: :caption \subcaption{\label{fig:test_id31_Kiff_loop_gain}Decentralized Loop gains} +% #+attr_latex: :options {0.49\textwidth} +% #+begin_subfigure +% #+attr_latex: :width 0.95\linewidth +% [[file:figs/test_id31_Kiff_loop_gain.png]] +% #+end_subfigure +% #+end_figure + +% To estimate the added damping, a root-locus plot is computed using the multi-body model (Figure ref:fig:test_id31_iff_root_locus_m0). +% It can be seen that for all considered payloads, the poles are bounded to the "left-half plane" indicating that the decentralized IFF is robust. +% The closed-loop poles for the chosen value of the gain are displayed by black crosses. +% It can be seen that while damping can be added for all payloads (as compared to the open-loop case), the optimal value of the gain is different for each payload. +% For low payload masses, a higher value of the IFF controller gain could lead to better damping. +% However, in this study, it was chosen to implement a fix (i.e. non-adaptive) decentralized IFF controller. + + +%% Root Locus for IFF +gains = logspace(-2, 2, 100); +Gm_iff_m0 = Gm_m0_Wz0({'Vs1', 'Vs2', 'Vs3', 'Vs4', 'Vs5', 'Vs6'}, {'u1', 'u2', 'u3', 'u4', 'u5', 'u6'}); +Gm_iff_m1 = Gm_m1_Wz0({'Vs1', 'Vs2', 'Vs3', 'Vs4', 'Vs5', 'Vs6'}, {'u1', 'u2', 'u3', 'u4', 'u5', 'u6'}); +Gm_iff_m2 = Gm_m2_Wz0({'Vs1', 'Vs2', 'Vs3', 'Vs4', 'Vs5', 'Vs6'}, {'u1', 'u2', 'u3', 'u4', 'u5', 'u6'}); +Gm_iff_m3 = Gm_m3_Wz0({'Vs1', 'Vs2', 'Vs3', 'Vs4', 'Vs5', 'Vs6'}, {'u1', 'u2', 'u3', 'u4', 'u5', 'u6'}); + +figure; +tiledlayout(1, 1, 'TileSpacing', 'compact', 'Padding', 'None'); +nexttile(); +hold on; +plot(real(pole(Gm_iff_m0)), imag(pole(Gm_iff_m0)), 'x', 'color', colors(1,:), ... + 'DisplayName', '$g = 0$'); +plot(real(tzero(Gm_iff_m0)), imag(tzero(Gm_iff_m0)), 'o', 'color', colors(1,:), ... + 'HandleVisibility', 'off'); + +for g = gains + clpoles = pole(feedback(Gm_iff_m0, g*Kiff, +1)); + plot(real(clpoles), imag(clpoles), '.', 'color', colors(1,:), ... + 'HandleVisibility', 'off'); +end + +% Optimal gain +clpoles = pole(feedback(Gm_iff_m0, Kiff, +1)); +plot(real(clpoles), imag(clpoles), 'kx', ... + 'DisplayName', '$g_{opt}$'); +hold off; +axis equal; +xlim([-600, 0]); ylim([0, 1500]); +xticks([-600:300:0]); +yticks([0:300:1500]); +set(gca, 'XTickLabel',[]); set(gca, 'YTickLabel',[]); +xlabel('Real part'); ylabel('Imaginary part'); + +%% description +figure; +tiledlayout(1, 1, 'TileSpacing', 'compact', 'Padding', 'None'); +nexttile(); +hold on; +plot(real(pole(Gm_iff_m1)), imag(pole(Gm_iff_m1)), 'x', 'color', colors(2,:), ... + 'DisplayName', '$g = 0$'); +plot(real(tzero(Gm_iff_m1)), imag(tzero(Gm_iff_m1)), 'o', 'color', colors(2,:), ... + 'HandleVisibility', 'off'); + +for g = gains + clpoles = pole(feedback(Gm_iff_m1, g*Kiff, +1)); + plot(real(clpoles), imag(clpoles), '.', 'color', colors(2,:), ... + 'HandleVisibility', 'off'); +end + +% Optimal gain +clpoles = pole(feedback(Gm_iff_m1, Kiff, +1)); +plot(real(clpoles), imag(clpoles), 'kx', ... + 'DisplayName', '$g_{opt}$'); +hold off; +axis equal; +xlim([-200, 0]); ylim([0, 500]); +set(gca, 'XTickLabel',[]); set(gca, 'YTickLabel',[]); +xlabel('Real part'); ylabel('Imaginary part'); + +figure; +tiledlayout(1, 1, 'TileSpacing', 'compact', 'Padding', 'None'); +nexttile(); +hold on; +plot(real(pole(Gm_iff_m2)), imag(pole(Gm_iff_m2)), 'x', 'color', colors(3,:), ... + 'DisplayName', '$g = 0$'); +plot(real(tzero(Gm_iff_m2)), imag(tzero(Gm_iff_m2)), 'o', 'color', colors(3,:), ... + 'HandleVisibility', 'off'); + +for g = gains + clpoles = pole(feedback(Gm_iff_m2, g*Kiff, +1)); + plot(real(clpoles), imag(clpoles), '.', 'color', colors(3,:), ... + 'HandleVisibility', 'off'); +end + +% Optimal gain +clpoles = pole(feedback(Gm_iff_m2, Kiff, +1)); +plot(real(clpoles), imag(clpoles), 'kx', ... + 'DisplayName', '$g_{opt}$'); +hold off; +axis equal; +xlim([-200, 0]); ylim([0, 500]); +set(gca, 'XTickLabel',[]); set(gca, 'YTickLabel',[]); +xlabel('Real part'); ylabel('Imaginary part'); + +figure; +tiledlayout(1, 1, 'TileSpacing', 'compact', 'Padding', 'None'); +nexttile(); +hold on; +plot(real(pole(Gm_iff_m3)), imag(pole(Gm_iff_m3)), 'x', 'color', colors(4,:), ... + 'DisplayName', '$g = 0$'); +plot(real(tzero(Gm_iff_m3)), imag(tzero(Gm_iff_m3)), 'o', 'color', colors(4,:), ... + 'HandleVisibility', 'off'); + +for g = gains + clpoles = pole(feedback(Gm_iff_m3, g*Kiff, +1)); + plot(real(clpoles), imag(clpoles), '.', 'color', colors(4,:), ... + 'HandleVisibility', 'off'); +end + +% Optimal gain +clpoles = pole(feedback(Gm_iff_m3, Kiff, +1)); +plot(real(clpoles), imag(clpoles), 'kx', ... + 'DisplayName', '$g_{opt}$'); +hold off; +axis equal; +xlim([-200, 0]); ylim([0, 500]); +set(gca, 'XTickLabel',[]); set(gca, 'YTickLabel',[]); +xlabel('Real part'); ylabel('Imaginary part'); + +% Damped Plant +% <> + +% As the model is accurately modelling the system dynamics, it can be used to estimate the damped plant, i.e. the transfer functions from $\bm{u}^\prime$ to $\bm{\mathcal{L}}$. +% The obtained damped plants are compared to the open-loop plants in Figure ref:fig:test_id31_comp_ol_iff_plant_model. +% The peak amplitudes corresponding to the suspension modes are approximately reduced by a factor $10$ for all considered payloads, showing the effectiveness of the decentralized IFF control strategy. + +% In order to experimentally validate the Decentralized IFF controller, it was implemented and the damped plants (i.e. the transfer function from $\bm{u}^\prime$ to $\bm{\epsilon\mathcal{L}}$) were identified for all payload conditions. +% The obtained frequency response functions are compared with the model in Figure ref:fig:test_id31_hac_plant_effect_mass verifying the good correlation between the predicted damped plant using the multi-body model and the experimental results. + + +%% Estimate damped plant from Multi-Body model +Gm_hac_m0_Wz0 = feedback(Gm_m0_Wz0, Kiff, 'name', +1); +Gm_hac_m1_Wz0 = feedback(Gm_m1_Wz0, Kiff, 'name', +1); +Gm_hac_m2_Wz0 = feedback(Gm_m2_Wz0, Kiff, 'name', +1); +Gm_hac_m3_Wz0 = feedback(Gm_m3_Wz0, Kiff, 'name', +1); + +% Check Stability +if not(isstable(Gm_hac_m0_Wz0) && isstable(Gm_hac_m1_Wz0) && isstable(Gm_hac_m2_Wz0) && isstable(Gm_hac_m3_Wz0)) + warning("One of the damped system with decentralized IFF is not stable"); +end + +% The estimated damped plants from the multi-body model are saved +save('./mat/test_id31_simscape_damped_plants.mat', 'Gm_hac_m0_Wz0', 'Gm_hac_m1_Wz0', 'Gm_hac_m2_Wz0', 'Gm_hac_m3_Wz0'); + +%% Comparison of the open-loop plants and the estimated damped plant with IFF +figure; +tiledlayout(3, 1, 'TileSpacing', 'compact', 'Padding', 'None'); + +ax1 = nexttile([2,1]); +hold on; +plot(freqs, abs(squeeze(freqresp(Gm_m0_Wz0('eL1', 'u1'), freqs, 'Hz'))), 'color', [colors(1,:), 0.3], ... + 'DisplayName', '$-\epsilon\mathcal{L}_{1}/u_1$ - 0 kg'); +plot(freqs, abs(squeeze(freqresp(Gm_m1_Wz0('eL1', 'u1'), freqs, 'Hz'))), 'color', [colors(2,:), 0.3], ... + 'DisplayName', '$-\epsilon\mathcal{L}_{1}/u_1$ - 13 kg'); +plot(freqs, abs(squeeze(freqresp(Gm_m2_Wz0('eL1', 'u1'), freqs, 'Hz'))), 'color', [colors(3,:), 0.3], ... + 'DisplayName', '$-\epsilon\mathcal{L}_{1}/u_1$ - 26 kg'); +plot(freqs, abs(squeeze(freqresp(Gm_m3_Wz0('eL1', 'u1'), freqs, 'Hz'))), 'color', [colors(4,:), 0.3], ... + 'DisplayName', '$-\epsilon\mathcal{L}_{1}/u_1$ - 39 kg'); +plot(freqs, abs(squeeze(freqresp(Gm_hac_m0_Wz0('eL1', 'u1'), freqs, 'Hz'))), 'color', colors(1,:), ... + 'DisplayName', '$-\epsilon\mathcal{L}_{1}/u_1^\prime$ - 0 kg'); +plot(freqs, abs(squeeze(freqresp(Gm_hac_m1_Wz0('eL1', 'u1'), freqs, 'Hz'))), 'color', colors(2,:), ... + 'DisplayName', '$-\epsilon\mathcal{L}_{1}/u_1^\prime$ - 13 kg'); +plot(freqs, abs(squeeze(freqresp(Gm_hac_m2_Wz0('eL1', 'u1'), freqs, 'Hz'))), 'color', colors(3,:), ... + 'DisplayName', '$-\epsilon\mathcal{L}_{1}/u_1^\prime$ - 26 kg'); +plot(freqs, abs(squeeze(freqresp(Gm_hac_m3_Wz0('eL1', 'u1'), freqs, 'Hz'))), 'color', colors(4,:), ... + 'DisplayName', '$-\epsilon\mathcal{L}_{1}/u_1^\prime$ - 39 kg'); +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log'); +ylabel('Amplitude [m/V]'); set(gca, 'XTickLabel',[]); +ylim([1e-7, 4e-4]); +leg = legend('location', 'southwest', 'FontSize', 8, 'NumColumns', 2); +leg.ItemTokenSize(1) = 15; + +ax2 = nexttile; +hold on; +plot(freqs, 180/pi*angle(squeeze(freqresp(Gm_m0_Wz0('eL1','u1'), freqs, 'Hz'))), 'color', [colors(1,:), 0.3]); +plot(freqs, 180/pi*angle(squeeze(freqresp(Gm_m1_Wz0('eL1','u1'), freqs, 'Hz'))), 'color', [colors(2,:), 0.3]); +plot(freqs, 180/pi*angle(squeeze(freqresp(Gm_m2_Wz0('eL1','u1'), freqs, 'Hz'))), 'color', [colors(3,:), 0.3]); +plot(freqs, 180/pi*angle(squeeze(freqresp(Gm_m3_Wz0('eL1','u1'), freqs, 'Hz'))), 'color', [colors(4,:), 0.3]); +plot(freqs, 180/pi*unwrap(angle(squeeze(freqresp(Gm_hac_m0_Wz0('eL1','u1'), freqs, 'Hz')))), 'color', colors(1,:)); +plot(freqs, 180/pi*unwrap(angle(squeeze(freqresp(Gm_hac_m1_Wz0('eL1','u1'), freqs, 'Hz')))), 'color', colors(2,:)); +plot(freqs, 180/pi*unwrap(angle(squeeze(freqresp(Gm_hac_m2_Wz0('eL1','u1'), freqs, 'Hz')))), 'color', colors(3,:)); +plot(freqs, 180/pi*unwrap(angle(squeeze(freqresp(Gm_hac_m3_Wz0('eL1','u1'), freqs, 'Hz')))), 'color', colors(4,:)); +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'lin'); +xlabel('Frequency [Hz]'); ylabel('Phase [deg]'); +hold off; +yticks(-360:90:360); +ylim([-20, 200]) + +linkaxes([ax1,ax2],'x'); +xlim([1, 1e3]); + +%% Identification of the damped Plant (transfer function from u' to dL) + +% Load identification data +data_m0 = load('2023-08-17_17-53_damp_plant_m0_Wz0.mat'); +data_m1 = load('2023-08-10_16-01_damp_plant_m1_Wz0.mat'); +data_m2 = load('2023-08-10_17-28_damp_plant_m2_Wz0.mat'); +data_m3 = load('2023-08-10_18-16_damp_plant_m3_Wz0.mat'); + +% Hannning Windows +Ts = 1e-4; % Sampling Time [s] +Nfft = floor(2.0/Ts); +win = hanning(Nfft); +Noverlap = floor(Nfft/2); + +% And we get the frequency vector +[~, f] = tfestimate(data_m0.uL1.id_plant, data_m0.uL1.e_L1, win, Noverlap, Nfft, 1/Ts); + +% Identification without any payload +G_hac_m0_Wz0 = zeros(length(f), 6, 6); +for i_strut = 1:6 + eL = [data_m0.(sprintf("uL%i", i_strut)).e_L1 ; data_m0.(sprintf("uL%i", i_strut)).e_L2 ; data_m0.(sprintf("uL%i", i_strut)).e_L3 ; data_m0.(sprintf("uL%i", i_strut)).e_L4 ; data_m0.(sprintf("uL%i", i_strut)).e_L5 ; data_m0.(sprintf("uL%i", i_strut)).e_L6]'; + + G_hac_m0_Wz0(:,:,i_strut) = tfestimate(data_m0.(sprintf("uL%i", i_strut)).id_plant, -detrend(eL, 0), win, Noverlap, Nfft, 1/Ts); +end + +% Identification with 1 "payload layer" +G_hac_m1_Wz0 = zeros(length(f), 6, 6); +for i_strut = 1:6 + eL = [data_m1.(sprintf("uL%i", i_strut)).e_L1 ; data_m1.(sprintf("uL%i", i_strut)).e_L2 ; data_m1.(sprintf("uL%i", i_strut)).e_L3 ; data_m1.(sprintf("uL%i", i_strut)).e_L4 ; data_m1.(sprintf("uL%i", i_strut)).e_L5 ; data_m1.(sprintf("uL%i", i_strut)).e_L6]'; + + G_hac_m1_Wz0(:,:,i_strut) = tfestimate(data_m1.(sprintf("uL%i", i_strut)).id_plant, -detrend(eL, 0), win, Noverlap, Nfft, 1/Ts); +end + +% Identification with 2 "payload layers" +G_hac_m2_Wz0 = zeros(length(f), 6, 6); +for i_strut = 1:6 + eL = [data_m2.(sprintf("uL%i", i_strut)).e_L1 ; data_m2.(sprintf("uL%i", i_strut)).e_L2 ; data_m2.(sprintf("uL%i", i_strut)).e_L3 ; data_m2.(sprintf("uL%i", i_strut)).e_L4 ; data_m2.(sprintf("uL%i", i_strut)).e_L5 ; data_m2.(sprintf("uL%i", i_strut)).e_L6]'; + + G_hac_m2_Wz0(:,:,i_strut) = tfestimate(data_m2.(sprintf("uL%i", i_strut)).id_plant, -detrend(eL, 0), win, Noverlap, Nfft, 1/Ts); +end + +% Identification with 3 "payload layers" +G_hac_m3_Wz0 = zeros(length(f), 6, 6); +for i_strut = 1:6 + eL = [data_m3.(sprintf("uL%i", i_strut)).e_L1 ; data_m3.(sprintf("uL%i", i_strut)).e_L2 ; data_m3.(sprintf("uL%i", i_strut)).e_L3 ; data_m3.(sprintf("uL%i", i_strut)).e_L4 ; data_m3.(sprintf("uL%i", i_strut)).e_L5 ; data_m3.(sprintf("uL%i", i_strut)).e_L6]'; + + G_hac_m3_Wz0(:,:,i_strut) = tfestimate(data_m3.(sprintf("uL%i", i_strut)).id_plant, -detrend(eL, 0), win, Noverlap, Nfft, 1/Ts); +end + +% The identified dynamics are then saved for further use. +save('./mat/test_id31_identified_damped_plants.mat', 'G_hac_m0_Wz0', 'G_hac_m1_Wz0', 'G_hac_m2_Wz0', 'G_hac_m3_Wz0', 'f'); + +%% Comparison of the identified HAC plant and the HAC plant extracted from the simscape model +figure; +tiledlayout(3, 1, 'TileSpacing', 'compact', 'Padding', 'None'); + +ax1 = nexttile([2,1]); +hold on; +plot(f, abs(G_hac_m0_Wz0(:, 1, 1)), 'color', [colors(1,:), 0.2], ... + 'DisplayName', '$m = 0$ kg'); +for i = 2:6 + plot(f, abs(G_hac_m0_Wz0(:,i, i)), 'color', [colors(1,:), 0.2], ... + 'HandleVisibility', 'off') +end +plot(f, abs(G_hac_m1_Wz0(:, 1, 1)), 'color', [colors(2,:), 0.2], ... + 'DisplayName', '$m = 13$ kg'); +for i = 2:6 + plot(f, abs(G_hac_m1_Wz0(:,i, i)), 'color', [colors(2,:), 0.2], ... + 'HandleVisibility', 'off') +end +plot(f, abs(G_hac_m2_Wz0(:, 1, 1)), 'color', [colors(3,:), 0.2], ... + 'DisplayName', '$m = 26$ kg'); +for i = 2:6 + plot(f, abs(G_hac_m2_Wz0(:,i, i)), 'color', [colors(3,:), 0.2], ... + 'HandleVisibility', 'off') +end +plot(f, abs(G_hac_m3_Wz0(:, 1, 1)), 'color', [colors(4,:), 0.2], ... + 'DisplayName', '$m = 39$ kg'); +for i = 2:6 + plot(f, abs(G_hac_m3_Wz0(:,i, i)), 'color', [colors(4,:), 0.2], ... + 'HandleVisibility', 'off') +end +plot(freqs, abs(squeeze(freqresp(Gm_hac_m0_Wz0('eL1', 'u1'), freqs, 'Hz'))), '--', 'color', colors(1,:), ... + 'DisplayName', '$m = 0$ kg (model)'); +plot(freqs, abs(squeeze(freqresp(Gm_hac_m1_Wz0('eL1', 'u1'), freqs, 'Hz'))), '--', 'color', colors(2,:), ... + 'DisplayName', '$m = 13$ kg (model)'); +plot(freqs, abs(squeeze(freqresp(Gm_hac_m2_Wz0('eL1', 'u1'), freqs, 'Hz'))), '--', 'color', colors(3,:), ... + 'DisplayName', '$m = 26$ kg (model)'); +plot(freqs, abs(squeeze(freqresp(Gm_hac_m3_Wz0('eL1', 'u1'), freqs, 'Hz'))), '--', 'color', colors(4,:), ... + 'DisplayName', '$m = 39$ kg (model)'); +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log'); +ylabel('Amplitude [m/V]'); set(gca, 'XTickLabel',[]); +ylim([2e-7, 3e-5]); +leg = legend('location', 'southwest', 'FontSize', 8, 'NumColumns', 2); +leg.ItemTokenSize(1) = 15; + +ax2 = nexttile; +hold on; +plot(f, 180/pi*unwrapphase(angle(G_hac_m0_Wz0(:,1,1)), f), 'color', [colors(1,:), 0.2]); +for i = 2:6 + plot(f, 180/pi*unwrapphase(angle(G_hac_m0_Wz0(:,i, i)), f), 'color', [colors(1,:), 0.2]); +end +plot(f, 180/pi*unwrapphase(angle(G_hac_m1_Wz0(:,1,1)), f), 'color', [colors(2,:), 0.2]); +for i = 2:6 + plot(f, 180/pi*unwrapphase(angle(G_hac_m1_Wz0(:,i, i)), f), 'color', [colors(2,:), 0.2]); +end +plot(f, 180/pi*unwrapphase(angle(G_hac_m2_Wz0(:,1,1)), f), 'color', [colors(3,:), 0.2]); +for i = 2:6 + plot(f, 180/pi*unwrapphase(angle(G_hac_m2_Wz0(:,i, i)), f), 'color', [colors(3,:), 0.2]); +end +plot(f, 180/pi*unwrapphase(angle(G_hac_m3_Wz0(:,1,1)), f), 'color', [colors(4,:), 0.2]); +for i = 2:6 + plot(f, 180/pi*unwrapphase(angle(G_hac_m3_Wz0(:,i, i)), f), 'color', [colors(4,:), 0.2]); +end +plot(freqs, 180/pi*unwrapphase(angle(squeeze(freqresp(-exp(-3e-4*s)*Gm_hac_m0_Wz0('eL1', 'u1'), freqs, 'Hz'))), f), '--', 'color', colors(1,:)); +plot(freqs, 180/pi*unwrapphase(angle(squeeze(freqresp(-exp(-3e-4*s)*Gm_hac_m1_Wz0('eL1', 'u1'), freqs, 'Hz'))), f), '--', 'color', colors(2,:)); +plot(freqs, 180/pi*unwrapphase(angle(squeeze(freqresp(-exp(-3e-4*s)*Gm_hac_m2_Wz0('eL1', 'u1'), freqs, 'Hz'))), f), '--', 'color', colors(3,:)); +plot(freqs, 180/pi*unwrapphase(angle(squeeze(freqresp(-exp(-3e-4*s)*Gm_hac_m3_Wz0('eL1', 'u1'), freqs, 'Hz'))), f), '--', 'color', colors(4,:)); +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'lin'); +xlabel('Frequency [Hz]'); ylabel('Phase [deg]'); +hold off; +yticks(-360:90:360); +ylim([-270, 20]) + +linkaxes([ax1,ax2],'x'); +xlim([1, 5e2]); diff --git a/matlab/test_id31_4_hac.m b/matlab/test_id31_4_hac.m new file mode 100644 index 0000000..5f9ccbf --- /dev/null +++ b/matlab/test_id31_4_hac.m @@ -0,0 +1,583 @@ +% Matlab Init :noexport:ignore: + +%% test_id31_4_hac.m + +%% Clear Workspace and Close figures +clear; close all; clc; + +%% Intialize Laplace variable +s = zpk('s'); + +%% Path for functions, data and scripts +addpath('./mat/'); % Path for Data +addpath('./src/'); % Path for functions +addpath('./STEPS/'); % Path for STEPS +addpath('./subsystems/'); % Path for Subsystems Simulink files + +%% Data directory +data_dir = './mat/'; + +% Simulink Model name +mdl = 'nass_model_id31'; + +%% Colors for the figures +colors = colororder; + +%% Frequency Vector +freqs = logspace(log10(1), log10(2e3), 1000); + +%% Sampling Time +Ts = 1e-4; + +%% Specifications for Experiments +specs_dz_peak = 50; % [nm] +specs_dy_peak = 100; % [nm] +specs_ry_peak = 0.85; % [urad] +specs_dz_rms = 15; % [nm RMS] +specs_dy_rms = 30; % [nm RMS] +specs_ry_rms = 0.25; % [urad RMS] + +% Damped Plant +% <> + +% To verify whether the multi body model accurately represents the measured damped dynamics, both direct terms and coupling terms corresponding to the first actuator are compared in Figure ref:fig:test_id31_comp_simscape_hac. +% Considering the complexity of the system's dynamics, the model can be considered to well represent the system's dynamics, and can therefore be used to tune the feedback controller and evaluate its performances. + + +% Load the estimated damped plant from the multi-body model +load('test_id31_simscape_damped_plants.mat', 'Gm_hac_m0_Wz0', 'Gm_hac_m1_Wz0', 'Gm_hac_m2_Wz0', 'Gm_hac_m3_Wz0'); +% Load the measured damped plants +load('test_id31_identified_damped_plants.mat', 'G_hac_m0_Wz0', 'G_hac_m1_Wz0', 'G_hac_m2_Wz0', 'G_hac_m3_Wz0', 'f'); +% Load the undamped plant for comparison +load('test_id31_identified_open_loop_plants.mat', 'G_int_m0_Wz0', 'G_int_m1_Wz0', 'G_int_m2_Wz0', 'G_int_m3_Wz0', 'f'); + +figure; +tiledlayout(2, 3, 'TileSpacing', 'tight', 'Padding', 'tight'); + +ax1 = nexttile(); +hold on; +plot(f, abs(G_hac_m0_Wz0(:, 1, 1))); +plot(freqs, abs(squeeze(freqresp(Gm_hac_m0_Wz0('eL1', 'u1'), freqs, 'Hz')))); +text(12, 3e-5, '$\epsilon_{\mathcal{L}1}/u_1^\prime$', 'Horiz','left', 'Vert','top') +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log'); +set(gca, 'XTickLabel',[]); ylabel('Amplitude [m/V]'); +yticks([1e-7, 1e-6, 1e-5]); + +ax2 = nexttile(); +hold on; +plot(f, abs(G_hac_m0_Wz0(:, 2, 1))); +plot(freqs, abs(squeeze(freqresp(Gm_hac_m0_Wz0('eL2', 'u1'), freqs, 'Hz')))); +text(12, 3e-5, '$\epsilon_{\mathcal{L}2}/u_1^\prime$', 'Horiz','left', 'Vert','top') +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log'); +set(gca, 'XTickLabel',[]); set(gca, 'YTickLabel',[]); + +ax3 = nexttile(); +hold on; +plot(f, abs(G_hac_m0_Wz0(:, 3, 1))) +plot(freqs, abs(squeeze(freqresp(Gm_hac_m0_Wz0('eL3', 'u1'), freqs, 'Hz')))) +text(12, 3e-5, '$\epsilon_{\mathcal{L}3}/u_1^\prime$', 'Horiz','left', 'Vert','top') +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log'); +set(gca, 'XTickLabel',[]); set(gca, 'YTickLabel',[]); + +ax4 = nexttile(); +hold on; +plot(f, abs(G_hac_m0_Wz0(:, 4, 1))); +plot(freqs, abs(squeeze(freqresp(Gm_hac_m0_Wz0('eL4', 'u1'), freqs, 'Hz')))); +text(12, 3e-5, '$\epsilon_{\mathcal{L}4}/u_1^\prime$', 'Horiz','left', 'Vert','top') +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log'); +xlabel('Frequency [Hz]'); ylabel('Amplitude [m/V]'); +xticks([10, 20, 50, 100, 200]) +yticks([1e-7, 1e-6, 1e-5]); + +ax5 = nexttile(); +hold on; +plot(f, abs(G_hac_m0_Wz0(:, 5, 1))); +plot(freqs, abs(squeeze(freqresp(Gm_hac_m0_Wz0('eL5', 'u1'), freqs, 'Hz')))); +text(12, 3e-5, '$\epsilon_{\mathcal{L}5}/u_1^\prime$', 'Horiz','left', 'Vert','top') +hold off; +xlabel('Frequency [Hz]'); set(gca, 'YTickLabel',[]); +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log'); +xticks([10, 20, 50, 100, 200]) + +ax6 = nexttile(); +hold on; +plot(f, abs(G_hac_m0_Wz0(:, 6, 1)), ... + 'DisplayName', 'Measurements'); +plot(freqs, abs(squeeze(freqresp(Gm_hac_m0_Wz0('eL6', 'u1'), freqs, 'Hz'))), ... + 'DisplayName', 'Model (2-DoF APA)'); +text(12, 3e-5, '$\epsilon_{\mathcal{L}6}/u_1^\prime$', 'Horiz','left', 'Vert','top') +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log'); +xlabel('Frequency [Hz]'); set(gca, 'YTickLabel',[]); +xticks([10, 20, 50, 100, 200]) +leg = legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; + +linkaxes([ax1,ax2,ax3,ax4,ax5,ax6],'xy'); +xlim([10, 5e2]); ylim([1e-7, 4e-5]); + + + +% #+name: fig:test_id31_comp_simscape_hac +% #+caption: Comparison of the measured (in blue) and modeled (in red) frequency transfer functions from the first control signal ($u_1^\prime$) of the damped plant to the estimated errors ($\epsilon_{\mathcal{L}_i}$) in the frame of the six struts by the external metrology +% #+RESULTS: +% [[file:figs/test_id31_comp_simscape_hac.png]] + +% The challenge here is to tune an high authority controller such that it is robust to the change of dynamics due to different payloads being used. +% Doing that without using the HAC-LAC strategy would require to design a controller which gives good performances for all the undamped dynamics (blue curves in Figure ref:fig:test_id31_comp_all_undamped_damped_plants), which is a very complex control problem. +% With the HAC-LAC strategy, the designed controller instead has to be be robust to all the damped dynamics (red curves in Figure ref:fig:test_id31_comp_all_undamped_damped_plants), which is easier from a control perspective. +% This is one of the key benefit of using the HAC-LAC strategy. + + +%% Comparison of all the undamped FRF and all the damped FRF +figure; +tiledlayout(3, 1, 'TileSpacing', 'compact', 'Padding', 'None'); + +ax1 = nexttile([2,1]); +hold on; +plot(f, abs(G_int_m0_Wz0(:,1,1)), 'color', [colors(1,:), 0.5], 'DisplayName', 'Undamped - $\epsilon\mathcal{L}_i/u_i$'); +plot(f, abs(G_hac_m0_Wz0(:,1,1)), 'color', [colors(2,:), 0.5], 'DisplayName', 'damped - $\epsilon\mathcal{L}_i/u_i^\prime$'); +for i = 1:6 + plot(f, abs(G_int_m0_Wz0(:,i, i)), 'color', [colors(1,:), 0.5], 'HandleVisibility', 'off'); + plot(f, abs(G_int_m1_Wz0(:,i, i)), 'color', [colors(1,:), 0.5], 'HandleVisibility', 'off'); + plot(f, abs(G_int_m2_Wz0(:,i, i)), 'color', [colors(1,:), 0.5], 'HandleVisibility', 'off'); + plot(f, abs(G_int_m3_Wz0(:,i, i)), 'color', [colors(1,:), 0.5], 'HandleVisibility', 'off'); +end +for i = 1:6 + plot(f, abs(G_hac_m0_Wz0(:,i, i)), 'color', [colors(2,:), 0.5], 'HandleVisibility', 'off'); + plot(f, abs(G_hac_m1_Wz0(:,i, i)), 'color', [colors(2,:), 0.5], 'HandleVisibility', 'off'); + plot(f, abs(G_hac_m2_Wz0(:,i, i)), 'color', [colors(2,:), 0.5], 'HandleVisibility', 'off'); + plot(f, abs(G_hac_m3_Wz0(:,i, i)), 'color', [colors(2,:), 0.5], 'HandleVisibility', 'off'); +end +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log'); +ylabel('Amplitude [m/V]'); set(gca, 'XTickLabel',[]); +leg = legend('location', 'southwest', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; +ylim([2e-7, 4e-4]); + +ax2 = nexttile; +hold on; +for i =1:6 + plot(f, 180/pi*unwrapphase(angle(-G_int_m0_Wz0(:,i, i)), f), 'color', [colors(1,:), 0.5]); + plot(f, 180/pi*unwrapphase(angle(-G_int_m1_Wz0(:,i, i)), f), 'color', [colors(1,:), 0.5]); + plot(f, 180/pi*unwrapphase(angle(-G_int_m2_Wz0(:,i, i)), f), 'color', [colors(1,:), 0.5]); + plot(f, 180/pi*unwrapphase(angle(-G_int_m3_Wz0(:,i, i)), f), 'color', [colors(1,:), 0.5]); +end +for i = 1:6 + plot(f, 180/pi*unwrapphase(angle(G_hac_m0_Wz0(:,i, i)), f), 'color', [colors(2,:), 0.5]); + plot(f, 180/pi*unwrapphase(angle(G_hac_m1_Wz0(:,i, i)), f), 'color', [colors(2,:), 0.5]); + plot(f, 180/pi*unwrapphase(angle(G_hac_m2_Wz0(:,i, i)), f), 'color', [colors(2,:), 0.5]); + plot(f, 180/pi*unwrapphase(angle(G_hac_m3_Wz0(:,i, i)), f), 'color', [colors(2,:), 0.5]); +end +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'lin'); +xlabel('Frequency [Hz]'); ylabel('Phase [deg]'); +hold off; +yticks(-360:90:360); +ylim([-270, 20]) + +linkaxes([ax1,ax2],'x'); +xlim([1, 5e2]); + +% Interaction Analysis +% <> + +% As the control strategy here is to apply a diagonal control in the frame of the struts, it is important to determine the frequency at which multivariable effects become significant, as this represents a critical limitation of the control approach. +% To conduct this interaction analysis, the acrfull:rga $\bm{\Lambda_G}$ is first computed using eqref:eq:test_id31_rga for the plant dynamics identified with the multiple payload masses. + +% \begin{equation}\label{eq:test_id31_rga} +% \bm{\Lambda_G}(\omega) = \bm{G}(j\omega) \star \left(\bm{G}(j\omega)^{-1}\right)^{T}, \quad (\star \text{ means element wise multiplication}) +% \end{equation} + +% Then, acrshort:rga numbers are computed using eqref:eq:test_id31_rga_number and are use as a metric for interaction [[cite:&skogestad07_multiv_feedb_contr chapt. 3.4]]. + +% \begin{equation}\label{eq:test_id31_rga_number} +% \text{RGA number}(\omega) = \|\bm{\Lambda_G}(\omega) - \bm{I}\|_{\text{sum}} +% \end{equation} + +% The obtained acrshort:rga numbers are compared in Figure ref:fig:test_id31_hac_rga_number. +% The results indicates that higher payload masses increase the coupling when implementing control in the strut reference frame (i.e., decentralized approach). +% This indicates that it is progressively more challenging to achieve high bandwidth performance as the payload mass increases. +% This behavior can be attributed to the fundamental approach of implementing control in the frame of the struts. +% Indeed, above the suspension modes of the nano-hexapod, the induced motion by the piezoelectric actuators is no longer dictated by the kinematics but rather by the inertia of the different parts. +% This design choice, while beneficial for system simplicity, introduces inherent limitations in the system's ability to handle larger masses at high frequency. + + +%% Interaction Analysis - RGA Number +rga_m0 = zeros(1,size(G_hac_m0_Wz0,1)); +for i = 1:length(rga_m0) + rga_m0(i) = sum(sum(abs(inv(squeeze(G_hac_m0_Wz0(i,:,:)).').*squeeze(G_hac_m0_Wz0(i,:,:)) - eye(6)))); +end + +rga_m1 = zeros(1,size(G_hac_m1_Wz0,1)); +for i = 1:length(rga_m1) + rga_m1(i) = sum(sum(abs(inv(squeeze(G_hac_m1_Wz0(i,:,:)).').*squeeze(G_hac_m1_Wz0(i,:,:)) - eye(6)))); +end + +rga_m2 = zeros(1,size(G_hac_m2_Wz0,1)); +for i = 1:length(rga_m2) + rga_m2(i) = sum(sum(abs(inv(squeeze(G_hac_m2_Wz0(i,:,:)).').*squeeze(G_hac_m2_Wz0(i,:,:)) - eye(6)))); +end + +rga_m3 = zeros(1,size(G_hac_m3_Wz0,1)); +for i = 1:length(rga_m3) + rga_m3(i) = sum(sum(abs(inv(squeeze(G_hac_m3_Wz0(i,:,:)).').*squeeze(G_hac_m3_Wz0(i,:,:)) - eye(6)))); +end + +%% RGA-number for the damped plants - Comparison of all the payload conditions +figure; +hold on; +plot(f, rga_m0, 'DisplayName', '$m = 0$ kg') +plot(f, rga_m1, 'DisplayName', '$m = 13$ kg') +plot(f, rga_m2, 'DisplayName', '$m = 26$ kg') +plot(f, rga_m3, 'DisplayName', '$m = 39$ kg') +xlabel('Frequency [Hz]'); ylabel('RGA number'); +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'lin'); +xlim([1, 1e2]); ylim([0, 10]); +leg = legend('location', 'northwest', 'FontSize', 8, 'NumColumns', 2); +leg.ItemTokenSize(1) = 15; + +% Robust Controller Design +% <> + +% A diagonal controller was designed to be robust to change of payloads, which means that every damped plants shown in Figure ref:fig:test_id31_comp_all_undamped_damped_plants should be considered during the controller design. +% For this controller design, a crossover frequency of $5\,\text{Hz}$ was chosen to limit multivariable effects as explain in Section ref:sec:test_id31_hac_interaction_analysis. +% One integrator is added to increase the low frequency gain, a lead is added around $5\,\text{Hz}$ to increase the stability margins and a first order low pass filter with a cut-off frequency of $30\,\text{Hz}$ is added to improve the robustness to dynamical uncertainty at high frequency. +% The controller transfer function is shown in eqref:eq:test_id31_robust_hac. + +% \begin{equation}\label{eq:test_id31_robust_hac} +% K_{\text{HAC}}(s) = g_0 \cdot \underbrace{\frac{\omega_c}{s}}_{\text{int}} \cdot \underbrace{\frac{1}{\sqrt{\alpha}}\frac{1 + \frac{s}{\omega_c/\sqrt{\alpha}}}{1 + \frac{s}{\omega_c\sqrt{\alpha}}}}_{\text{lead}} \cdot \underbrace{\frac{1}{1 + \frac{s}{\omega_0}}}_{\text{LPF}}, \quad \left( \omega_c = 2\pi5\,\text{rad/s},\ \alpha = 2,\ \omega_0 = 2\pi30\,\text{rad/s} \right) +% \end{equation} + +% The obtained "decentralized" loop-gains (i.e. the diagonal element of the controller times the diagonal terms of the plant) are shown in Figure ref:fig:test_id31_hac_loop_gain. +% Closed-loop stability is verified by computing the characteristic Loci (Figure ref:fig:test_id31_hac_characteristic_loci). +% However, small stability margins are observed for the highest mass, indicating that some multivariable effects are in play. + + +%% HAC Design +% Wanted crossover +wc = 2*pi*5; % [rad/s] + +% Integrator +H_int = wc/s; + +% Lead to increase phase margin +a = 2; % Amount of phase lead / width of the phase lead / high frequency gain +H_lead = 1/sqrt(a)*(1 + s/(wc/sqrt(a)))/(1 + s/(wc*sqrt(a))); + +% Low Pass filter to increase robustness +H_lpf = 1/(1 + s/2/pi/30); + +% Gain to have unitary crossover at 5Hz +[~, i_f] = min(abs(f - wc/2/pi)); +H_gain = 1./abs(G_hac_m0_Wz0(i_f, 1, 1)); + +% Decentralized HAC +Khac = H_gain * ... % Gain + H_int * ... % Integrator + H_lpf * ... % Low Pass filter + eye(6); % 6x6 Diagonal + +% The designed HAC controller is saved +save('./mat/test_id31_K_hac_robust.mat', 'Khac'); + +%% Decentralized Loop gain for the High Authority Controller +figure; +tiledlayout(3, 1, 'TileSpacing', 'Compact', 'Padding', 'None'); + +ax1 = nexttile([2,1]); +hold on; +plot(f(2:end), abs(G_hac_m0_Wz0(:,1, 1).*squeeze(freqresp(Khac(1,1), f(2:end), 'Hz'))), 'color', colors(1,:), 'DisplayName', '$0$ kg'); +plot(f(2:end), abs(G_hac_m1_Wz0(:,1, 1).*squeeze(freqresp(Khac(1,1), f(2:end), 'Hz'))), 'color', colors(2,:), 'DisplayName', '$13$ kg'); +plot(f(2:end), abs(G_hac_m2_Wz0(:,1, 1).*squeeze(freqresp(Khac(1,1), f(2:end), 'Hz'))), 'color', colors(3,:), 'DisplayName', '$26$ kg'); +plot(f(2:end), abs(G_hac_m3_Wz0(:,1, 1).*squeeze(freqresp(Khac(1,1), f(2:end), 'Hz'))), 'color', colors(4,:), 'DisplayName', '$39$ kg'); +xline(5, '--', 'linewidth', 1, 'color', [0,0,0,0.2], 'HandleVisibility', 'off') +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log'); +ylabel('Loop Gain'); set(gca, 'XTickLabel',[]); +ylim([1e-5, 1e2]); +leg = legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 2); +leg.ItemTokenSize(1) = 15; + +ax2 = nexttile; +hold on; +plot(f(2:end), 180/pi*angle(G_hac_m0_Wz0(:,1, 1).*squeeze(freqresp(Khac(1,1), f(2:end), 'Hz'))), 'color', colors(1,:)); +plot(f(2:end), 180/pi*angle(G_hac_m1_Wz0(:,1, 1).*squeeze(freqresp(Khac(1,1), f(2:end), 'Hz'))), 'color', colors(2,:)); +plot(f(2:end), 180/pi*angle(G_hac_m2_Wz0(:,1, 1).*squeeze(freqresp(Khac(1,1), f(2:end), 'Hz'))), 'color', colors(3,:)); +plot(f(2:end), 180/pi*angle(G_hac_m3_Wz0(:,1, 1).*squeeze(freqresp(Khac(1,1), f(2:end), 'Hz'))), 'color', colors(4,:)); +xline(5, '--', 'linewidth', 1, 'color', [0,0,0,0.2]) +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'lin'); +xlabel('Frequency [Hz]'); ylabel('Phase [deg]'); +hold off; +yticks(-360:90:360); +ylim([-180, 180]) + +linkaxes([ax1,ax2],'x'); +xlim([1, 1e3]); + +%% Compute the Eigenvalues of the loop gain +Ldet = zeros(4, 6, length(f)); + +% Loop gain +Lmimo = pagemtimes(permute(G_hac_m0_Wz0, [2,3,1]),squeeze(freqresp(Khac, f, 'Hz'))); +for i_f = 2:length(f) + Ldet(1,:, i_f) = eig(squeeze(Lmimo(:,:,i_f))); +end + +Lmimo = pagemtimes(permute(G_hac_m1_Wz0, [2,3,1]),squeeze(freqresp(Khac, f, 'Hz'))); +for i_f = 2:length(f) + Ldet(2,:, i_f) = eig(squeeze(Lmimo(:,:,i_f))); +end + +Lmimo = pagemtimes(permute(G_hac_m2_Wz0, [2,3,1]),squeeze(freqresp(Khac, f, 'Hz'))); +for i_f = 2:length(f) + Ldet(3,:, i_f) = eig(squeeze(Lmimo(:,:,i_f))); +end + +Lmimo = pagemtimes(permute(G_hac_m3_Wz0, [2,3,1]),squeeze(freqresp(Khac, f, 'Hz'))); +for i_f = 2:length(f) + Ldet(4,:, i_f) = eig(squeeze(Lmimo(:,:,i_f))); +end + +%% Plot of the eigenvalues of L in the complex plane +figure; +hold on; +plot(real(squeeze(Ldet(1, 1,:))), imag(squeeze(Ldet(1, 1,:))), ... + '.', 'color', colors(1, :), ... + 'DisplayName', '$m = 0$ kg'); +plot(real(squeeze(Ldet(2, 1,:))), imag(squeeze(Ldet(2, 1,:))), ... + '.', 'color', colors(2, :), ... + 'DisplayName', '$m = 13$ kg'); +plot(real(squeeze(Ldet(3, 1,:))), imag(squeeze(Ldet(3, 1,:))), ... + '.', 'color', colors(3, :), ... + 'DisplayName', '$m = 26$ kg'); +plot(real(squeeze(Ldet(4, 1,:))), imag(squeeze(Ldet(4, 1,:))), ... + '.', 'color', colors(4, :), ... + 'DisplayName', '$m = 39$ kg'); +for i_mass = 1:4 + plot(real(squeeze(Ldet(i_mass, 1,:))), -imag(squeeze(Ldet(i_mass, 1,:))), ... + '.', 'color', colors(i_mass, :), ... + 'HandleVisibility', 'off'); + for i = 1:6 + plot(real(squeeze(Ldet(i_mass, i,:))), imag(squeeze(Ldet(i_mass, i,:))), ... + '.', 'color', colors(i_mass, :), ... + 'HandleVisibility', 'off'); + plot(real(squeeze(Ldet(i_mass, i,:))), -imag(squeeze(Ldet(i_mass, i,:))), ... + '.', 'color', colors(i_mass, :), ... + 'HandleVisibility', 'off'); + end +end +plot(-1, 0, 'kx', 'HandleVisibility', 'off'); +hold off; +set(gca, 'XScale', 'lin'); set(gca, 'YScale', 'lin'); +xlabel('Real'); ylabel('Imag'); +leg = legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 2); +leg.ItemTokenSize(1) = 15; +axis square +xlim([-1.5, 0.5]); ylim([-1, 1]); + +% Performance estimation with simulation of Tomography scans +% <> + +% To estimate the performances that can be expected with this HAC-LAC architecture and the designed controller, simulations of tomography experiments were performed[fn:4]. +% The rotational velocity was set to $180\,\text{deg/s}$, and no payload was added on top of the nano-hexapod. +% An open-loop simulation and a closed-loop simulation were performed and compared in Figure ref:fig:test_id31_tomo_m0_30rpm_robust_hac_iff_sim. +% The obtained closed-loop positioning accuracy was found to comply with the requirements as it succeeded to keep the point of interest on the beam (Figure ref:fig:test_id31_tomo_m0_30rpm_robust_hac_iff_sim_yz). + + +%% Tomography experiment +% Sample is not centered with the rotation axis +% This is done by offsetfing the micro-hexapod by 0.9um +P_micro_hexapod = [2.5e-6; 0; -0.3e-6]; % [m] + +open(mdl); +set_param(mdl, 'StopTime', '3'); % 6 turns at 180deg/s (30rpm) + +initializeGround(); +initializeGranite(); +initializeTy(); +initializeRy(); +initializeRz(); +initializeMicroHexapod('AP', P_micro_hexapod); +initializeNanoHexapod('flex_bot_type', '2dof', ... + 'flex_top_type', '3dof', ... + 'motion_sensor_type', 'plates', ... + 'actuator_type', '2dof'); +initializeSample('type', '0'); + +initializeSimscapeConfiguration('gravity', false); +initializeLoggingConfiguration('log', 'all', 'Ts', 1e-4); +initializeController('type', 'open-loop'); + +initializeDisturbances(... + 'Dw_x', true, ... % Ground Motion - X direction + 'Dw_y', true, ... % Ground Motion - Y direction + 'Dw_z', true, ... % Ground Motion - Z direction + 'Fdy_x', false, ... % Translation Stage - X direction + 'Fdy_z', false, ... % Translation Stage - Z direction + 'Frz_x', true, ... % Spindle - X direction + 'Frz_y', true, ... % Spindle - Y direction + 'Frz_z', true); % Spindle - Z direction + +initializeReferences(... + 'Rz_type', 'rotating', ... + 'Rz_period', 360/180, ... % 180deg/s, 30rpm + 'Dh_pos', [P_micro_hexapod; 0; 0; 0]); + +% Open-Loop Simulation +sim(mdl); +exp_tomo_ol_m0_Wz180 = simout; + +% Closed-Loop Simulation +load('test_id31_K_iff.mat', 'Kiff'); +load('test_id31_K_hac_robust.mat', 'Khac'); +initializeController('type', 'hac-iff'); +initializeSample('type', '0'); +sim(mdl); +exp_tomo_cl_m0_Wz180 = simout; + +% Save the simulation results +save('./mat/test_id31_exp_tomo_ol_cl_30rpm_sim.mat', 'exp_tomo_ol_m0_Wz180', 'exp_tomo_cl_m0_Wz180'); + +%% Simulation of tomography experiment - no payload, 30rpm - XY errors +figure; +hold on; +plot(1e6*exp_tomo_ol_m0_Wz180.y.x.Data, 1e6*exp_tomo_ol_m0_Wz180.y.y.Data, 'DisplayName', 'OL') +plot(1e6*exp_tomo_cl_m0_Wz180.y.x.Data(1:2e3), 1e6*exp_tomo_cl_m0_Wz180.y.y.Data(1:2e3), 'color', colors(3,:), 'HandleVisibility', 'off') +plot(1e6*exp_tomo_cl_m0_Wz180.y.x.Data(2e3:end), 1e6*exp_tomo_cl_m0_Wz180.y.y.Data(2e3:end), 'color', colors(2,:), 'DisplayName', 'CL') +hold off; +xlabel('$D_x$ [$\mu$m]'); ylabel('$D_y$ [$\mu$m]'); +axis equal +xlim([-3, 3]); ylim([-3, 3]); +xticks([-3:1:3]); +yticks([-3:1:3]); +leg = legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; + +%% Simulation of tomography experiment - no payload, 30rpm - YZ errors +figure; +tiledlayout(2, 1, 'TileSpacing', 'compact', 'Padding', 'None'); + +ax1 = nexttile(); +hold on; +plot(1e6*exp_tomo_ol_m0_Wz180.y.y.Data, 1e6*exp_tomo_ol_m0_Wz180.y.z.Data, 'DisplayName', 'OL') +plot(1e6*exp_tomo_cl_m0_Wz180.y.y.Data(1:2e3), 1e6*exp_tomo_cl_m0_Wz180.y.z.Data(1:2e3), 'color', colors(3,:), 'HandleVisibility', 'off') +plot(1e6*exp_tomo_cl_m0_Wz180.y.y.Data(2e3:end), 1e6*exp_tomo_cl_m0_Wz180.y.z.Data(2e3:end), 'color', colors(2,:), 'DisplayName', 'CL') +hold off; +xlabel('$D_y$ [$\mu$m]'); ylabel('$D_z$ [$\mu$m]'); +axis equal +xlim([-3, 3]); ylim([-0.6, 0.6]); +xticks([-3:1:3]); +yticks([-3:0.3:3]); +leg = legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; + +ax2 = nexttile(); +hold on; +plot(1e9*exp_tomo_cl_m0_Wz180.y.y.Data(2e3:end), 1e9*exp_tomo_cl_m0_Wz180.y.z.Data(2e3:end), 'color', colors(2,:), 'DisplayName', 'CL') +theta = linspace(0, 2*pi, 500); % Angle to plot the circle [rad] +plot(100*cos(theta), 50*sin(theta), 'k--', 'DisplayName', 'Beam size') +hold off; +xlabel('$D_y$ [nm]'); ylabel('$D_z$ [nm]'); +axis equal +xlim([-300, 300]); ylim([-100, 100]); +% xticks([-3:1:3]); +% yticks([-3:1:3]); +leg = legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; + +% Robustness estimation with simulation of Tomography scans +% <> + +% To verify the robustness to the change of payload mass, four simulations of tomography experiments were performed with payloads as shown Figure ref:fig:test_id31_picture_masses (i.e. $0\,kg$, $13\,kg$, $26\,kg$ and $39\,kg$). +% This time, the rotational velocity was set at $6\,\text{deg/s}$, as it is the typical rotational velocity for heavy samples. + +% The closed-loop systems were stable for all payload conditions, indicating good control robustness. +% However, the positioning errors are getting worse as the payload mass increases, especially in the lateral $D_y$ direction, as shown in Figure ref:fig:test_id31_hac_tomography_Wz36_simulation. +% Yet it was decided that this controller will be tested experimentally, and improved if necessary. + + +%% Simulation of tomography experiments at 1RPM with all payloads +% Configuration +open(mdl); +set_param(mdl, 'StopTime', '2'); % 30 degrees at 1rpm +initializeLoggingConfiguration('log', 'all', 'Ts', 1e-3); +initializeController('type', 'hac-iff'); +initializeReferences(... + 'Rz_type', 'rotating', ... + 'Rz_period', 360/6, ... % 6deg/s, 1 rpm + 'Dh_pos', [P_micro_hexapod; 0; 0; 0]); + +% Perform the simulations +initializeSample('type', '0'); +sim(mdl); +exp_tomo_cl_m0_1rpm = simout; +initializeSample('type', '1'); +sim(mdl); +exp_tomo_cl_m1_1rpm = simout; +initializeSample('type', '2'); +sim(mdl); +exp_tomo_cl_m2_1rpm = simout; +initializeSample('type', '3'); +sim(mdl); +exp_tomo_cl_m3_1rpm = simout; + +% Save the simulation results +save('./mat/test_id31_exp_tomo_cl_1rpm_sim.mat', 'exp_tomo_cl_m0_1rpm', 'exp_tomo_cl_m1_1rpm', 'exp_tomo_cl_m2_1rpm', 'exp_tomo_cl_m3_1rpm'); + +%% Positioning errors in the Y-Z plane during tomography experiments simulated using the multi-body model +figure; +tiledlayout(2, 2, 'TileSpacing', 'compact', 'Padding', 'None'); + +ax1 = nexttile; +hold on; +plot(1e9*exp_tomo_cl_m0_1rpm.y.y.Data(1e3:end), 1e9*exp_tomo_cl_m0_1rpm.y.z.Data(1e3:end), 'color', colors(1,:), 'DisplayName', '$m = 0$ kg') +theta = linspace(0, 2*pi, 500); % Angle to plot the circle [rad] +plot(100*cos(theta), 50*sin(theta), 'k--', 'DisplayName', 'Beam size') +axis equal +xticks([-400:100:400]); yticks([-100:100:100]); +xlabel('$D_y$ [nm]'); ylabel('$D_z$ [nm]'); +leg = legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; + +ax2 = nexttile; +hold on; +plot(1e9*exp_tomo_cl_m1_1rpm.y.y.Data(1e3:end), 1e9*exp_tomo_cl_m1_1rpm.y.z.Data(1e3:end), 'color', colors(2,:), 'DisplayName', '$m = 13$ kg') +theta = linspace(0, 2*pi, 500); % Angle to plot the circle [rad] +plot(100*cos(theta), 50*sin(theta), 'k--', 'HandleVisibility', 'off') +axis equal +xticks([-400:100:400]); yticks([-100:100:100]); +xlabel('$D_y$ [nm]'); ylabel('$D_z$ [nm]'); +leg = legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; + +ax3 = nexttile; +hold on; +plot(1e9*exp_tomo_cl_m2_1rpm.y.y.Data(1e3:end), 1e9*exp_tomo_cl_m2_1rpm.y.z.Data(1e3:end), 'color', colors(3,:), 'DisplayName', '$m = 26$ kg') +theta = linspace(0, 2*pi, 500); % Angle to plot the circle [rad] +plot(100*cos(theta), 50*sin(theta), 'k--', 'HandleVisibility', 'off') +axis equal +xticks([-400:100:400]); yticks([-100:100:100]); +xlabel('$D_y$ [nm]'); ylabel('$D_z$ [nm]'); +leg = legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; + +ax4 = nexttile; +hold on; +plot(1e9*exp_tomo_cl_m3_1rpm.y.y.Data(1e3:end), 1e9*exp_tomo_cl_m3_1rpm.y.z.Data(1e3:end), 'color', colors(4,:), 'DisplayName', '$m = 39$ kg') +theta = linspace(0, 2*pi, 500); % Angle to plot the circle [rad] +plot(100*cos(theta), 50*sin(theta), 'k--', 'HandleVisibility', 'off') +axis equal +xticks([-400:100:400]); yticks([-100:100:100]); +xlabel('$D_y$ [nm]'); ylabel('$D_z$ [nm]'); +leg = legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; + +linkaxes([ax1,ax2,ax3, ax4],'xy'); +xlim([-450, 450]); ylim([-100, 100]); diff --git a/matlab/test_id31_5_experiments.m b/matlab/test_id31_5_experiments.m new file mode 100644 index 0000000..dc9b5a2 --- /dev/null +++ b/matlab/test_id31_5_experiments.m @@ -0,0 +1,1222 @@ +%% Clear Workspace and Close figures +clear; close all; clc; + +%% Intialize Laplace variable +s = zpk('s'); + +%% Path for functions, data and scripts +addpath('./mat/'); % Path for Data +addpath('./src/'); % Path for functions +addpath('./STEPS/'); % Path for STEPS +addpath('./subsystems/'); % Path for Subsystems Simulink files + +%% Data directory +data_dir = './mat/'; + +%% Colors for the figures +colors = colororder; + +%% Frequency Vector +freqs = logspace(log10(1), log10(2e3), 1000); + +%% Sampling Time +Ts = 1e-4; + +%% Specifications for Experiments +specs_dz_peak = 50; % [nm] +specs_dy_peak = 100; % [nm] +specs_ry_peak = 0.85; % [urad] +specs_dz_rms = 15; % [nm RMS] +specs_dy_rms = 30; % [nm RMS] +specs_ry_rms = 0.25; % [urad RMS] + +% Slow Tomography scans + +% First, tomography scans are performed with a rotational velocity of $6\,\text{deg/s}$ for all considered payload masses (shown in Figure ref:fig:test_id31_picture_masses). +% Each experimental sequence consisted of two complete spindle rotations: an initial open-loop rotation followed by a closed-loop rotation. +% The experimental results for the $26\,\text{kg}$ payload are presented in Figure ref:fig:test_id31_tomo_m2_1rpm_robust_hac_iff_fit. + +% Due to static deformation of the micro-station stages under payload loading, a significant eccentricity was observed between the point of interest and the spindle rotation axis. +% To establish a theoretical lower bound for open-loop errors, an ideal scenario was assumed where the point of interest perfectly aligns with the spindle rotation axis. +% This idealized case was simulated by first calculating the eccentricity through circular fitting (represented by the dashed black circle in Figure ref:fig:test_id31_tomo_m2_1rpm_robust_hac_iff_fit), and then subtracting it from the measured data, as shown in Figure ref:fig:test_id31_tomo_m2_1rpm_robust_hac_iff_fit_removed. +% While this approach likely underestimates actual open-loop errors, as perfect alignment is practically unattainable, it enables a more balanced comparison with closed-loop performance. + + +%% Load Tomography scans with robust controller +data_tomo_m0_Wz6 = load("2023-08-11_11-37_tomography_1rpm_m0.mat"); +data_tomo_m0_Wz6.time = Ts*[0:length(data_tomo_m0_Wz6.Rz)-1]; + +data_tomo_m1_Wz6 = load("2023-08-11_11-15_tomography_1rpm_m1.mat"); +data_tomo_m1_Wz6.time = Ts*[0:length(data_tomo_m1_Wz6.Rz)-1]; + +data_tomo_m2_Wz6 = load("2023-08-11_10-59_tomography_1rpm_m2.mat"); +data_tomo_m2_Wz6.time = Ts*[0:length(data_tomo_m2_Wz6.Rz)-1]; + +data_tomo_m3_Wz6 = load("2023-08-11_10-24_tomography_1rpm_m3.mat"); +data_tomo_m3_Wz6.time = Ts*[0:length(data_tomo_m3_Wz6.Rz)-1]; + +%% Find best circle fit for all experiments +[~, i_m0] = find(data_tomo_m0_Wz6.hac_status == 1); +[x_m0, y_m0, R_m0] = circlefit(data_tomo_m0_Wz6.Dx_int(1:i_m0), data_tomo_m0_Wz6.Dy_int(1:i_m0)); +fun = @(theta)rms((data_tomo_m0_Wz6.Dx_int(1:i_m0) - (x_m0 + R_m0*cos(data_tomo_m0_Wz6.Rz(1:i_m0)+theta(1)))).^2 + ... + (data_tomo_m0_Wz6.Dy_int(1:i_m0) - (y_m0 + R_m0*sin(data_tomo_m0_Wz6.Rz(1:i_m0)+theta(1)))).^2); +delta_theta_m0 = fminsearch(fun, 0); + +[~, i_m1] = find(data_tomo_m1_Wz6.hac_status == 1); +[x_m1, y_m1, R_m1] = circlefit(data_tomo_m1_Wz6.Dx_int(1:i_m1), data_tomo_m1_Wz6.Dy_int(1:i_m1)); +fun = @(theta)rms((data_tomo_m1_Wz6.Dx_int(1:i_m1) - (x_m1 + R_m1*cos(data_tomo_m1_Wz6.Rz(1:i_m1)+theta(1)))).^2 + ... + (data_tomo_m1_Wz6.Dy_int(1:i_m1) - (y_m1 + R_m1*sin(data_tomo_m1_Wz6.Rz(1:i_m1)+theta(1)))).^2); +delta_theta_m1 = fminsearch(fun, 0); + +[~, i_m2] = find(data_tomo_m2_Wz6.hac_status == 1); +[x_m2, y_m2, R_m2] = circlefit(data_tomo_m2_Wz6.Dx_int(1:i_m2), data_tomo_m2_Wz6.Dy_int(1:i_m2)); +fun = @(theta)rms((data_tomo_m2_Wz6.Dx_int(1:i_m2) - (x_m2 + R_m2*cos(data_tomo_m2_Wz6.Rz(1:i_m2)+theta(1)))).^2 + ... + (data_tomo_m2_Wz6.Dy_int(1:i_m2) - (y_m2 + R_m2*sin(data_tomo_m2_Wz6.Rz(1:i_m2)+theta(1)))).^2); +delta_theta_m2 = fminsearch(fun, 0); + +[~, i_m3] = find(data_tomo_m3_Wz6.hac_status == 1); +[x_m3, y_m3, R_m3] = circlefit(data_tomo_m3_Wz6.Dx_int(1:i_m3), data_tomo_m3_Wz6.Dy_int(1:i_m3)); +fun = @(theta)rms((data_tomo_m3_Wz6.Dx_int(1:i_m3) - (x_m3 + R_m3*cos(data_tomo_m3_Wz6.Rz(1:i_m3)+theta(1)))).^2 + ... + (data_tomo_m3_Wz6.Dy_int(1:i_m3) - (y_m3 + R_m3*sin(data_tomo_m3_Wz6.Rz(1:i_m3)+theta(1)))).^2); +delta_theta_m3 = fminsearch(fun, 0); + +%% Tomography experiment at 1rpm with 26kg payload +figure; +hold on; +plot(1e6*data_tomo_m2_Wz6.Dx_int(1:10:i_m2), 1e6*data_tomo_m2_Wz6.Dy_int(1:10:i_m2), 'DisplayName', '$m = 26$ kg (OL)') +plot(1e6*data_tomo_m2_Wz6.Dx_int(i_m2:10:i_m2+1e4), 1e6*data_tomo_m2_Wz6.Dy_int(i_m2:10:i_m2+1e4), 'color', colors(3,:), 'HandleVisibility', 'off') +plot(1e6*data_tomo_m2_Wz6.Dx_int(i_m2+1e4:10:end), 1e6*data_tomo_m2_Wz6.Dy_int(i_m2+1e4:10:end), 'color', colors(2,:), 'DisplayName', '$m = 26$ kg (CL)') +theta = linspace(0, 2*pi, 500); % Angle to plot the circle [rad] +plot(1e6*(x_m2 + R_m2*cos(theta)), 1e6*(y_m2 + R_m2*sin(theta)), 'k--', 'DisplayName', 'Best Circular Fit') +hold off; +xlabel('$D_x$ [$\mu$m]'); ylabel('$D_y$ [$\mu$m]'); +axis equal +xlim([-20, 100]); ylim([-20, 100]); +xticks([-20:20:100]); +yticks([-20:20:100]); +leg = legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; + +%% Measured radial errors of the Spindle +figure; +hold on; +plot(1e6*(data_tomo_m2_Wz6.Dx_int(1:10:i_m2) - (x_m2 + R_m2*cos(data_tomo_m2_Wz6.Rz(1:10:i_m2)+delta_theta_m2))), ... + 1e6*(data_tomo_m2_Wz6.Dy_int(1:10:i_m2) - (y_m2 + R_m2*sin(data_tomo_m2_Wz6.Rz(1:10:i_m2)+delta_theta_m2))), 'color', colors(1,:), 'DisplayName', '$m = 26$ kg (OL)') +plot(1e6*detrend(data_tomo_m2_Wz6.Dx_int(i_m2+1e4:10:end), 0), 1e6*detrend(data_tomo_m2_Wz6.Dy_int(i_m2+1e4:10:end), 0), 'color', colors(2,:), 'DisplayName', '$m = 26$ kg (CL)') +hold off; +xlabel('$D_x$ [$\mu$m]'); ylabel('$D_y$ [$\mu$m]'); +axis equal +xlim([-0.6, 0.4]); ylim([-0.4, 0.6]); +xticks([-0.6:0.2:0.4]); +yticks([-0.4:0.2:0.6]); +leg = legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; + + + +% #+name: fig:test_id31_tomo_m2_1rpm_robust_hac_iff +% #+caption: Tomography experiment with rotation velocity of $6\,\text{deg/s}$, and payload mass of 26kg. Errors in the $(x,y)$ plane are shown in (\subref{fig:test_id31_tomo_m2_1rpm_robust_hac_iff_fit}). The estimated eccentricity is displayed by the black dashed circle. Errors with subtracted eccentricity are shown in (\subref{fig:test_id31_tomo_m2_1rpm_robust_hac_iff_fit_removed}). +% #+attr_latex: :options [htbp] +% #+begin_figure +% #+attr_latex: :caption \subcaption{\label{fig:test_id31_tomo_m2_1rpm_robust_hac_iff_fit}Errors in $(x,y)$ plane} +% #+attr_latex: :options {0.49\textwidth} +% #+begin_subfigure +% #+attr_latex: :scale 0.9 +% [[file:figs/test_id31_tomo_m2_1rpm_robust_hac_iff_fit.png]] +% #+end_subfigure +% #+attr_latex: :caption \subcaption{\label{fig:test_id31_tomo_m2_1rpm_robust_hac_iff_fit_removed}Removed eccentricity} +% #+attr_latex: :options {0.49\textwidth} +% #+begin_subfigure +% #+attr_latex: :scale 0.9 +% [[file:figs/test_id31_tomo_m2_1rpm_robust_hac_iff_fit_removed.png]] +% #+end_subfigure +% #+end_figure + +% After eccentricity compensation for each experiment, the residual motion in the $Y-Z$ is compared against the minimum beam size, as illustrated in Figure ref:fig:test_id31_tomo_Wz36_results. +% Results are indicating the NASS succeeds in keeping the sample's point of interests on the beam, except for the highest mass of $39\,\text{kg}$ for which the lateral motion is a bit too high. +% These experimental findings align with the predictions from the tomography simulations presented in Section ref:ssec:test_id31_iff_hac_robustness. + + +%% Tomography experiment at 1rpm - Results in the YZ - All masses tested +figure; +tiledlayout(2, 2, 'TileSpacing', 'compact', 'Padding', 'None'); + +ax1 = nexttile; +hold on; +plot(1e9*detrend(data_tomo_m0_Wz6.Dy_int(1:10:i_m0) - y_m0 - R_m0*sin(data_tomo_m0_Wz6.Rz(1:10:i_m0)+delta_theta_m0), 0), 1e9*detrend(data_tomo_m0_Wz6.Dz_int(1:10:i_m0), 0), 'DisplayName', 'OL') +plot(1e9*detrend(data_tomo_m0_Wz6.Dy_int(i_m0+1e4:10:end), 0), 1e9*detrend(data_tomo_m0_Wz6.Dz_int(i_m0+1e4:10:end), 0), 'DisplayName', 'CL') +theta = linspace(0, 2*pi, 500); % Angle to plot the circle [rad] +plot(100*cos(theta), 50*sin(theta), 'k--', 'DisplayName', 'Beam') +text(-430, 90, '$m = 0$ kg', 'Horiz','left', 'Vert','top', 'FontWeight', 'bold') +hold off; +xlabel('$D_y$ [nm]'); ylabel('$D_z$ [nm]'); +axis equal +xlim([-450, 450]); ylim([-100, 100]); +xticks([-400:100:400]); yticks([-50:50:50]); +leg = legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; + +ax2 = nexttile; +hold on; +plot(1e9*detrend(data_tomo_m1_Wz6.Dy_int(1:10:i_m1) - y_m1 - R_m1*sin(data_tomo_m1_Wz6.Rz(1:10:i_m1)+delta_theta_m1), 0), 1e9*detrend(data_tomo_m1_Wz6.Dz_int(1:10:i_m1), 0), 'DisplayName', 'OL') +plot(1e9*detrend(data_tomo_m1_Wz6.Dy_int(i_m1+1e4:10:end), 0), 1e9*detrend(data_tomo_m1_Wz6.Dz_int(i_m1+1e4:10:end), 0), 'DisplayName', 'CL') +theta = linspace(0, 2*pi, 500); % Angle to plot the circle [rad] +plot(100*cos(theta), 50*sin(theta), 'k--', 'HandleVisibility', 'off') +text(-430, 90, '$m = 13$ kg', 'Horiz','left', 'Vert','top', 'FontWeight', 'bold') +hold off; +xlabel('$D_y$ [nm]'); ylabel('$D_z$ [nm]'); +axis equal +xlim([-450, 450]); ylim([-100, 100]); +xticks([-400:100:400]); yticks([-50:50:50]); +leg = legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; + +ax3 = nexttile; +hold on; +plot(1e9*detrend(data_tomo_m2_Wz6.Dy_int(1:10:i_m2) - y_m2 - R_m2*sin(data_tomo_m2_Wz6.Rz(1:10:i_m2)+delta_theta_m2), 0), 1e9*detrend(data_tomo_m2_Wz6.Dz_int(1:10:i_m2), 0), 'DisplayName', 'OL') +plot(1e9*detrend(data_tomo_m2_Wz6.Dy_int(i_m2+1e4:10:end), 0), 1e9*detrend(data_tomo_m2_Wz6.Dz_int(i_m2+1e4:10:end), 0), 'DisplayName', 'CL') +theta = linspace(0, 2*pi, 500); % Angle to plot the circle [rad] +plot(100*cos(theta), 50*sin(theta), 'k--', 'HandleVisibility', 'off') +text(-430, 90, '$m = 26$ kg', 'Horiz','left', 'Vert','top', 'FontWeight', 'bold') +hold off; +xlabel('$D_y$ [nm]'); ylabel('$D_z$ [nm]'); +axis equal +xlim([-450, 450]); ylim([-100, 100]); +xticks([-400:100:400]); yticks([-50:50:50]); +leg = legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; + +ax4 = nexttile; +hold on; +plot(1e9*detrend(data_tomo_m3_Wz6.Dy_int(1:10:i_m3) - y_m3 - R_m3*sin(data_tomo_m3_Wz6.Rz(1:10:i_m3)+delta_theta_m3), 0), 1e9*detrend(data_tomo_m3_Wz6.Dz_int(1:10:i_m3), 0), 'DisplayName', 'OL') +plot(1e9*detrend(data_tomo_m3_Wz6.Dy_int(i_m3+1e4:10:end), 0), 1e9*detrend(data_tomo_m3_Wz6.Dz_int(i_m3+1e4:10:end), 0), 'DisplayName', 'CL') +theta = linspace(0, 2*pi, 500); % Angle to plot the circle [rad] +plot(100*cos(theta), 50*sin(theta), 'k--', 'HandleVisibility', 'off') +text(-860, 180, '$m = 39$ kg', 'Horiz','left', 'Vert','top', 'FontWeight', 'bold') +hold off; +xlabel('$D_y$ [nm]'); ylabel('$D_z$ [nm]'); +axis equal +xlim([-900, 900]); ylim([-200, 200]); +xticks([-800:200:800]); yticks([-100:100:100]); +leg = legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; + + + +% #+name: fig:test_id31_tomo_Wz36_results +% #+caption: Measured errors in the $Y-Z$ plane during tomography experiments at $6\,\text{deg/s}$ for all considered payloads. In the open-loop case, the effect of eccentricity is removed from the data. +% #+RESULTS: +% [[file:figs/test_id31_tomo_Wz36_results.png]] + + +%% Estimate RMS of the errors while in closed-loop and open-loop - Tomography at 6deg/s +% No mass +data_tomo_m0_Wz6.Dy_rms_cl = rms(detrend(data_tomo_m0_Wz6.Dy_int(i_m0+1e4:end), 0)); +data_tomo_m0_Wz6.Dz_rms_cl = rms(detrend(data_tomo_m0_Wz6.Dz_int(i_m0+1e4:end), 0)); +data_tomo_m0_Wz6.Ry_rms_cl = rms(detrend(data_tomo_m0_Wz6.Ry_int(i_m0+1e4:end), 0)); + +% Remove eccentricity for OL errors +data_tomo_m0_Wz6.Dy_rms_ol = rms(data_tomo_m0_Wz6.Dy_int(1:i_m0) - (y_m0 + R_m0*sin(data_tomo_m0_Wz6.Rz(1:i_m0)+delta_theta_m0))); +data_tomo_m0_Wz6.Dz_rms_ol = rms(detrend(data_tomo_m0_Wz6.Dz_int(1:i_m0), 0)); +[x0, y0, R] = circlefit(data_tomo_m0_Wz6.Rx_int(1:i_m0), data_tomo_m0_Wz6.Ry_int(1:i_m0)); +fun = @(theta)rms((data_tomo_m0_Wz6.Rx_int(1:i_m0) - (x0 + R*cos(data_tomo_m0_Wz6.Rz(1:i_m0)+theta(1)))).^2 + ... + (data_tomo_m0_Wz6.Ry_int(1:i_m0) - (y0 + R*sin(data_tomo_m0_Wz6.Rz(1:i_m0)+theta(1)))).^2); +delta_theta = fminsearch(fun, 0); +data_tomo_m0_Wz6.Ry_rms_ol = rms(data_tomo_m0_Wz6.Ry_int(1:i_m0) - (y0 + R*sin(data_tomo_m0_Wz6.Rz(1:i_m0)+delta_theta))); + +% 1 "layer mass" +data_tomo_m1_Wz6.Dy_rms_cl = rms(detrend(data_tomo_m1_Wz6.Dy_int(i_m1+1e4:end), 0)); +data_tomo_m1_Wz6.Dz_rms_cl = rms(detrend(data_tomo_m1_Wz6.Dz_int(i_m1+1e4:end), 0)); +data_tomo_m1_Wz6.Ry_rms_cl = rms(detrend(data_tomo_m1_Wz6.Ry_int(i_m1+1e4:end), 0)); + +% Remove eccentricity for OL errors +data_tomo_m1_Wz6.Dy_rms_ol = rms(data_tomo_m1_Wz6.Dy_int(1:i_m1) - (y_m1 + R_m1*sin(data_tomo_m1_Wz6.Rz(1:i_m1)+delta_theta_m1))); +data_tomo_m1_Wz6.Dz_rms_ol = rms(detrend(data_tomo_m1_Wz6.Dz_int(1:i_m1), 0)); +[x0, y0, R] = circlefit(data_tomo_m1_Wz6.Rx_int(1:i_m1), data_tomo_m1_Wz6.Ry_int(1:i_m1)); +fun = @(theta)rms((data_tomo_m1_Wz6.Rx_int(1:i_m1) - (x0 + R*cos(data_tomo_m1_Wz6.Rz(1:i_m1)+theta(1)))).^2 + ... + (data_tomo_m1_Wz6.Ry_int(1:i_m1) - (y0 + R*sin(data_tomo_m1_Wz6.Rz(1:i_m1)+theta(1)))).^2); +delta_theta = fminsearch(fun, 0); +data_tomo_m1_Wz6.Ry_rms_ol = rms(data_tomo_m1_Wz6.Ry_int(1:i_m1) - (y0 + R*sin(data_tomo_m1_Wz6.Rz(1:i_m1)+delta_theta))); + +% 2 "layer masses" +data_tomo_m2_Wz6.Dy_rms_cl = rms(detrend(data_tomo_m2_Wz6.Dy_int(i_m2+1e4:end), 0)); +data_tomo_m2_Wz6.Dz_rms_cl = rms(detrend(data_tomo_m2_Wz6.Dz_int(i_m2+1e4:end), 0)); +data_tomo_m2_Wz6.Ry_rms_cl = rms(detrend(data_tomo_m2_Wz6.Ry_int(i_m2+1e4:end), 0)); + +% Remove eccentricity for OL errors +data_tomo_m2_Wz6.Dy_rms_ol = rms(data_tomo_m2_Wz6.Dy_int(1:i_m2) - (y_m2 + R_m2*sin(data_tomo_m2_Wz6.Rz(1:i_m2)+delta_theta_m2))); +data_tomo_m2_Wz6.Dz_rms_ol = rms(detrend(data_tomo_m2_Wz6.Dz_int(1:i_m2), 0)); +[x0, y0, R] = circlefit(data_tomo_m2_Wz6.Rx_int(1:i_m2), data_tomo_m2_Wz6.Ry_int(1:i_m2)); +fun = @(theta)rms((data_tomo_m2_Wz6.Rx_int(1:i_m2) - (x0 + R*cos(data_tomo_m2_Wz6.Rz(1:i_m2)+theta(1)))).^2 + ... + (data_tomo_m2_Wz6.Ry_int(1:i_m2) - (y0 + R*sin(data_tomo_m2_Wz6.Rz(1:i_m2)+theta(1)))).^2); +delta_theta = fminsearch(fun, 0); +data_tomo_m2_Wz6.Ry_rms_ol = rms(data_tomo_m2_Wz6.Ry_int(1:i_m2) - (y0 + R*sin(data_tomo_m2_Wz6.Rz(1:i_m2)+delta_theta))); + +% 3 "layer masses" +data_tomo_m3_Wz6.Dy_rms_cl = rms(detrend(data_tomo_m3_Wz6.Dy_int(i_m3+1e4:end), 0)); +data_tomo_m3_Wz6.Dz_rms_cl = rms(detrend(data_tomo_m3_Wz6.Dz_int(i_m3+1e4:end), 0)); +data_tomo_m3_Wz6.Ry_rms_cl = rms(detrend(data_tomo_m3_Wz6.Ry_int(i_m3+1e4:end), 0)); + +% Remove eccentricity for OL errors +data_tomo_m3_Wz6.Dy_rms_ol = rms(data_tomo_m3_Wz6.Dy_int(1:i_m3) - (y_m3 + R_m3*sin(data_tomo_m3_Wz6.Rz(1:i_m3)+delta_theta_m3))); +data_tomo_m3_Wz6.Dz_rms_ol = rms(detrend(data_tomo_m3_Wz6.Dz_int(1:i_m3), 0)); +[x0, y0, R] = circlefit(data_tomo_m3_Wz6.Rx_int(1:i_m3), data_tomo_m3_Wz6.Ry_int(1:i_m3)); +fun = @(theta)rms((data_tomo_m3_Wz6.Rx_int(1:i_m3) - (x0 + R*cos(data_tomo_m3_Wz6.Rz(1:i_m3)+theta(1)))).^2 + ... + (data_tomo_m3_Wz6.Ry_int(1:i_m3) - (y0 + R*sin(data_tomo_m3_Wz6.Rz(1:i_m3)+theta(1)))).^2); +delta_theta = fminsearch(fun, 0); +data_tomo_m3_Wz6.Ry_rms_ol = rms(data_tomo_m3_Wz6.Ry_int(1:i_m3) - (y0 + R*sin(data_tomo_m3_Wz6.Rz(1:i_m3)+delta_theta))); + +% Fast Tomography scans + +% A tomography experiment was then performed with the highest rotational velocity of the Spindle: $180\,\text{deg/s}$[fn:7]. +% The trajectory of the point of interest during this fast tomography scan is shown in Figure ref:fig:test_id31_tomo_m0_30rpm_robust_hac_iff_exp. +% While the experimental results closely mirror the simulation results (Figure ref:fig:test_id31_tomo_m0_30rpm_robust_hac_iff_sim), the actual performance are slightly lower than predicted. +% Nevertheless, even with this robust (conservative) HAC implementation, the system performance approaches the specified requirements. + + +%% Experimental Results for Tomography at 180deg/s, no payload +data_tomo_m0_Wz180 = load('2023-08-17_15-26_tomography_30rpm_m0_robust.mat'); + +[~, i_m0] = find(data_tomo_m0_Wz180.hac_status == 1); +[x_m0, y_m0, R_m0] = circlefit(data_tomo_m0_Wz180.Dx_int(1:i_m0), data_tomo_m0_Wz180.Dy_int(1:i_m0)); +fun = @(theta)rms((data_tomo_m0_Wz180.Dx_int(1:i_m0) - (x_m0 + R_m0*cos(data_tomo_m0_Wz180.Rz(1:i_m0)+theta(1)))).^2 + ... + (data_tomo_m0_Wz180.Dy_int(1:i_m0) - (y_m0 + R_m0*sin(data_tomo_m0_Wz180.Rz(1:i_m0)+theta(1)))).^2); +delta_theta_m0 = fminsearch(fun, 0); + +%% Tomography at 180deg/s - Errors in the X/Y plane +figure; +hold on; +plot(1e6*data_tomo_m0_Wz180.Dx_int(1:i_m0), 1e6*data_tomo_m0_Wz180.Dy_int(1:i_m0), 'DisplayName', 'OL') +plot(1e6*data_tomo_m0_Wz180.Dx_int(i_m0:i_m0+1e4), 1e6*data_tomo_m0_Wz180.Dy_int(i_m0:i_m0+1e4), 'color', colors(3,:), 'HandleVisibility', 'off') +plot(1e6*data_tomo_m0_Wz180.Dx_int(i_m0+1e4:end), 1e6*data_tomo_m0_Wz180.Dy_int(i_m0+1e4:end), 'color', colors(2,:), 'DisplayName', 'CL') +theta = linspace(0, 2*pi, 500); % Angle to plot the circle [rad] +plot(1e6*(x_m0 + R_m0*cos(theta)), 1e6*(y_m0 + R_m0*sin(theta)), 'k--', 'DisplayName', 'Circ. Fit') +hold off; +xlabel('$D_x$ [$\mu$m]'); ylabel('$D_y$ [$\mu$m]'); +axis equal +xlim([-3, 3]); ylim([-3, 3]); +xticks([-3:1:3]); +yticks([-3:1:3]); +leg = legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; + +%% Tomography at 180deg/s - Errors in the Y/Z plane +figure; +tiledlayout(2, 1, 'TileSpacing', 'compact', 'Padding', 'None'); + +ax1 = nexttile(); +hold on; +plot(1e6*data_tomo_m0_Wz180.Dy_int(1:i_m0), 1e6*data_tomo_m0_Wz180.Dz_int(1:i_m0), 'DisplayName', 'OL') +plot(1e6*data_tomo_m0_Wz180.Dy_int(i_m0:i_m0+1e4), 1e6*data_tomo_m0_Wz180.Dz_int(i_m0:i_m0+1e4), 'color', colors(3,:), 'HandleVisibility', 'off') +plot(1e6*data_tomo_m0_Wz180.Dy_int(i_m0+1e4:end), 1e6*data_tomo_m0_Wz180.Dz_int(i_m0+1e4:end), 'color', colors(2,:), 'DisplayName', 'CL') +hold off; +xlabel('$D_y$ [$\mu$m]'); ylabel('$D_z$ [$\mu$m]'); +axis equal +xlim([-3, 3]); ylim([-0.6, 0.6]); +xticks([-3:1:3]); +yticks([-3:0.3:3]); +leg = legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; + +ax2 = nexttile(); +hold on; +plot(1e9*data_tomo_m0_Wz180.Dy_int(i_m0+1e4:end), 1e9*data_tomo_m0_Wz180.Dz_int(i_m0+1e4:end), 'color', colors(2,:), 'DisplayName', 'CL') +theta = linspace(0, 2*pi, 500); % Angle to plot the circle [rad] +plot(100*cos(theta), 50*sin(theta), 'k--', 'DisplayName', 'Beam size') +hold off; +xlabel('$D_y$ [nm]'); ylabel('$D_z$ [nm]'); +axis equal +xlim([-300, 300]); ylim([-100, 100]); +leg = legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; + + + +% #+name: fig:test_id31_tomo_m0_30rpm_robust_hac_iff_exp +% #+caption: Experimental results of a tomography experiment at 180 deg/s without payload. Position error of the sample is shown in the XY (\subref{fig:test_id31_tomo_m0_30rpm_robust_hac_iff_exp_xy}) and YZ (\subref{fig:test_id31_tomo_m0_30rpm_robust_hac_iff_exp_yz}) planes. +% #+attr_latex: :options [htbp] +% #+begin_figure +% #+attr_latex: :caption \subcaption{\label{fig:test_id31_tomo_m0_30rpm_robust_hac_iff_exp_xy}XY plane} +% #+attr_latex: :options {0.49\textwidth} +% #+begin_subfigure +% #+attr_latex: :scale 0.9 +% [[file:figs/test_id31_tomo_m0_30rpm_robust_hac_iff_exp_xy.png]] +% #+end_subfigure +% #+attr_latex: :caption \subcaption{\label{fig:test_id31_tomo_m0_30rpm_robust_hac_iff_exp_yz}YZ plane} +% #+attr_latex: :options {0.49\textwidth} +% #+begin_subfigure +% #+attr_latex: :scale 0.9 +% [[file:figs/test_id31_tomo_m0_30rpm_robust_hac_iff_exp_yz.png]] +% #+end_subfigure +% #+end_figure + + +%% Estimate RMS of the errors while in closed-loop and open-loop - Tomography at 180deg/s +% No mass +data_tomo_m0_Wz180.Dy_rms_cl = rms(detrend(data_tomo_m0_Wz180.Dy_int(i_m0+1e4:end), 0)); +data_tomo_m0_Wz180.Dz_rms_cl = rms(detrend(data_tomo_m0_Wz180.Dz_int(i_m0+1e4:end), 0)); +data_tomo_m0_Wz180.Ry_rms_cl = rms(detrend(data_tomo_m0_Wz180.Ry_int(i_m0+1e4:end), 0)); + +% Remove eccentricity for OL errors +data_tomo_m0_Wz180.Dy_rms_ol = rms(data_tomo_m0_Wz180.Dy_int(1:i_m0) - (y_m0 + R_m0*sin(data_tomo_m0_Wz180.Rz(1:i_m0)+delta_theta_m0))); +data_tomo_m0_Wz180.Dz_rms_ol = rms(detrend(data_tomo_m0_Wz180.Dz_int(1:i_m0), 0)); +[x0, y0, R] = circlefit(data_tomo_m0_Wz180.Rx_int(1:i_m0), data_tomo_m0_Wz180.Ry_int(1:i_m0)); +fun = @(theta)rms((data_tomo_m0_Wz180.Rx_int(1:i_m0) - (x0 + R*cos(data_tomo_m0_Wz180.Rz(1:i_m0)+theta(1)))).^2 + ... + (data_tomo_m0_Wz180.Ry_int(1:i_m0) - (y0 + R*sin(data_tomo_m0_Wz180.Rz(1:i_m0)+theta(1)))).^2); +delta_theta = fminsearch(fun, 0); +data_tomo_m0_Wz180.Ry_rms_ol = rms(data_tomo_m0_Wz180.Ry_int(1:i_m0) - (y0 + R*sin(data_tomo_m0_Wz180.Rz(1:i_m0)+delta_theta))); + +% Cumulative Amplitude Spectra + +% A comparative analysis was conducted using three tomography scans at $180,\text{deg/s}$ to evaluate the effectiveness of the HAC-LAC strategy in reducing positioning errors. +% The scans were performed under three conditions: open-loop, with decentralized IFF control, and with the complete HAC-LAC strategy. +% For these specific measurements, an enhanced high authority controller was optimized for low payload masses to meet performance requirements. + +% Figure ref:fig:test_id31_hac_cas_cl presents the cumulative amplitude spectra of the position errors for all three cases. +% The results reveal two distinct control contributions: the decentralized IFF effectively attenuates vibrations near the nano-hexapod suspension modes (an achievement not possible with HAC alone), while the high authority controller suppresses low-frequency vibrations primarily arising from Spindle guiding errors. +% Notably, the spectral patterns in Figure ref:fig:test_id31_hac_cas_cl closely resemble the cumulative amplitude spectra computed in the project's early stages. + +% This experiment also illustrates that when needed, performance can be enhanced by designing controllers for specific experimental conditions, rather than relying solely on robust controllers that accommodate all payload ranges. + + +%% Jacobian to compute the motion in the X-Y-Z-Rx-Ry directions +J_int_to_X = [ 0 0 -0.787401574803149 -0.212598425196851 0; + 0.78740157480315 0.21259842519685 0 0 0; + 0 0 0 0 -1; + -13.1233595800525 13.1233595800525 0 0 0; + 0 0 -13.1233595800525 13.1233595800525 0]; + +%% Parameters for frequency analysis computation +Nfft = floor(20.0/Ts); +win = hanning(Nfft); +Noverlap = floor(Nfft/2); + +%% Open-Loop measurement +data_ol_Wz180 = load('2023-08-11_16-51_m0_lac_off.mat'); % no rotation + +a = J_int_to_X*[data_ol_Wz180.d1; data_ol_Wz180.d2; data_ol_Wz180.d3; data_ol_Wz180.d4; data_ol_Wz180.d5]; +data_ol_Wz180.Dx_int = a(1,:); +data_ol_Wz180.Dy_int = a(2,:); +data_ol_Wz180.Dz_int = a(3,:); +data_ol_Wz180.Rx_int = a(4,:); +data_ol_Wz180.Ry_int = a(5,:); + +[data_ol_Wz180.pxx_Dx, data_ol_Wz180.f] = pwelch(detrend(data_ol_Wz180.Dx_int, 0), win, Noverlap, Nfft, 1/Ts); +[data_ol_Wz180.pxx_Dy, ~ ] = pwelch(detrend(data_ol_Wz180.Dy_int, 0), win, Noverlap, Nfft, 1/Ts); +[data_ol_Wz180.pxx_Dz, ~ ] = pwelch(detrend(data_ol_Wz180.Dz_int, 0), win, Noverlap, Nfft, 1/Ts); +[data_ol_Wz180.pxx_Rx, ~ ] = pwelch(detrend(data_ol_Wz180.Rx_int, 0), win, Noverlap, Nfft, 1/Ts); +[data_ol_Wz180.pxx_Ry, ~ ] = pwelch(detrend(data_ol_Wz180.Ry_int, 0), win, Noverlap, Nfft, 1/Ts); + +%% Effect of LAC - 180 deg/s +data_lac_Wz180 = load('2023-08-11_17-36_m0_lac_on_30rpm.mat'); + +a = J_int_to_X*[data_lac_Wz180.d1; data_lac_Wz180.d2; data_lac_Wz180.d3; data_lac_Wz180.d4; data_lac_Wz180.d5]; +data_lac_Wz180.Dx_int = a(1,:); +data_lac_Wz180.Dy_int = a(2,:); +data_lac_Wz180.Dz_int = a(3,:); +data_lac_Wz180.Rx_int = a(4,:); +data_lac_Wz180.Ry_int = a(5,:); + +[data_lac_Wz180.pxx_Dx, data_lac_Wz180.f] = pwelch(detrend(data_lac_Wz180.Dx_int, 0), win, Noverlap, Nfft, 1/Ts); +[data_lac_Wz180.pxx_Dy, ~ ] = pwelch(detrend(data_lac_Wz180.Dy_int, 0), win, Noverlap, Nfft, 1/Ts); +[data_lac_Wz180.pxx_Dz, ~ ] = pwelch(detrend(data_lac_Wz180.Dz_int, 0), win, Noverlap, Nfft, 1/Ts); +[data_lac_Wz180.pxx_Rx, ~ ] = pwelch(detrend(data_lac_Wz180.Rx_int, 0), win, Noverlap, Nfft, 1/Ts); +[data_lac_Wz180.pxx_Ry, ~ ] = pwelch(detrend(data_lac_Wz180.Ry_int, 0), win, Noverlap, Nfft, 1/Ts); + +%% Effect of HAC - 180 deg/s +data_hac_Wz180 = load('2023-08-11_16-49_m0_hac_on.mat'); + +a = J_int_to_X*[data_hac_Wz180.d1; data_hac_Wz180.d2; data_hac_Wz180.d3; data_hac_Wz180.d4; data_hac_Wz180.d5]; +data_hac_Wz180.Dx_int = a(1,:); +data_hac_Wz180.Dy_int = a(2,:); +data_hac_Wz180.Dz_int = a(3,:); +data_hac_Wz180.Rx_int = a(4,:); +data_hac_Wz180.Ry_int = a(5,:); + +[data_hac_Wz180.pxx_Dx, data_hac_Wz180.f] = pwelch(detrend(data_hac_Wz180.Dx_int, 0), win, Noverlap, Nfft, 1/Ts); +[data_hac_Wz180.pxx_Dy, ~ ] = pwelch(detrend(data_hac_Wz180.Dy_int, 0), win, Noverlap, Nfft, 1/Ts); +[data_hac_Wz180.pxx_Dz, ~ ] = pwelch(detrend(data_hac_Wz180.Dz_int, 0), win, Noverlap, Nfft, 1/Ts); +[data_hac_Wz180.pxx_Rx, ~ ] = pwelch(detrend(data_hac_Wz180.Rx_int, 0), win, Noverlap, Nfft, 1/Ts); +[data_hac_Wz180.pxx_Ry, ~ ] = pwelch(detrend(data_hac_Wz180.Ry_int, 0), win, Noverlap, Nfft, 1/Ts); + +% Compute closed-loop RMS errors +data_hac_Wz180.Dy_rms_cl = rms(detrend(data_hac_Wz180.Dy_int(1e4:end), 0)); +data_hac_Wz180.Dz_rms_cl = rms(detrend(data_hac_Wz180.Dz_int(1e4:end), 0)); +data_hac_Wz180.Ry_rms_cl = rms(detrend(data_hac_Wz180.Ry_int(1e4:end), 0)); + +%% Cumulative Amplitude Spectrum - Closed-Loop - Dy +figure; +tiledlayout(1, 1, 'TileSpacing', 'compact', 'Padding', 'None'); +hold on; +plot(data_ol_Wz180.f, sqrt(flip(-cumtrapz(flip(data_ol_Wz180.f), flip(data_ol_Wz180.pxx_Dy)))), 'DisplayName', sprintf('OL $%.1f \\mu m$', 1e6*rms(detrend(data_ol_Wz180.Dy_int, 0)))); +plot(data_lac_Wz180.f, sqrt(flip(-cumtrapz(flip(data_lac_Wz180.f), flip(data_lac_Wz180.pxx_Dy)))), 'DisplayName', sprintf('LAC $%.1f \\mu m$', 1e6*rms(detrend(data_lac_Wz180.Dy_int, 0)))); +plot(data_hac_Wz180.f, sqrt(flip(-cumtrapz(flip(data_hac_Wz180.f), flip(data_hac_Wz180.pxx_Dy)))), 'DisplayName', sprintf('HAC $%.0f nm$', 1e9*rms(detrend(data_hac_Wz180.Dy_int, 0)))); +plot([1e-2, 1e4], 1e-9*[specs_dy_rms, specs_dy_rms], 'k--', 'DisplayName', sprintf('Spec: $%.0f$nm', specs_dy_rms)) +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log'); +xlabel('Frequency [Hz]'); ylabel('CAS [m]'); +leg = legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; +xticks([1e0, 1e1, 1e2]); yticks([1e-9, 1e-8, 1e-7, 1e-6, 1e-5]); +xlim([0.1, 5e2]); ylim([1e-10, 2e-5]); + +%% Cumulative Amplitude Spectrum - Closed-Loop - Dz +figure; +tiledlayout(1, 1, 'TileSpacing', 'compact', 'Padding', 'None'); +hold on; +plot(data_ol_Wz180.f, sqrt(flip(-cumtrapz(flip(data_ol_Wz180.f), flip(data_ol_Wz180.pxx_Dz)))), 'DisplayName', sprintf('OL $%.0f nm$', 1e9*rms(detrend(data_ol_Wz180.Dz_int, 0)))); +plot(data_lac_Wz180.f, sqrt(flip(-cumtrapz(flip(data_lac_Wz180.f), flip(data_lac_Wz180.pxx_Dz)))), 'DisplayName', sprintf('LAC $%.0f nm$', 1e9*rms(detrend(data_lac_Wz180.Dz_int, 0)))); +plot(data_hac_Wz180.f, sqrt(flip(-cumtrapz(flip(data_hac_Wz180.f), flip(data_hac_Wz180.pxx_Dz)))), 'DisplayName', sprintf('HAC $%.0f nm$', 1e9*rms(detrend(data_hac_Wz180.Dz_int, 0)))); +plot([1e-2, 1e4], 1e-9*[specs_dz_rms, specs_dz_rms], 'k--', 'DisplayName', sprintf('Spec: $%.0f$nm', specs_dz_rms)) +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log'); +xlabel('Frequency [Hz]'); ylabel('CAS [m]'); +leg = legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; +xticks([1e0, 1e1, 1e2]); yticks([1e-9, 1e-8, 1e-7, 1e-6, 1e-5]); +xlim([0.1, 5e2]); ylim([1e-10, 2e-5]); + +%% Cumulative Amplitude Spectrum - Closed-Loop - Ry +figure; +tiledlayout(1, 1, 'TileSpacing', 'compact', 'Padding', 'None'); +hold on; +plot(data_ol_Wz180.f, sqrt(flip(-cumtrapz(flip(data_ol_Wz180.f), flip(data_ol_Wz180.pxx_Ry)))), 'DisplayName', sprintf('OL $%.0f \\mu$rad', 1e6*rms(detrend(data_ol_Wz180.Ry_int, 0)))); +plot(data_lac_Wz180.f, sqrt(flip(-cumtrapz(flip(data_lac_Wz180.f), flip(data_lac_Wz180.pxx_Ry)))), 'DisplayName', sprintf('LAC $%.0f \\mu$rad', 1e6*rms(detrend(data_lac_Wz180.Ry_int, 0)))); +plot(data_hac_Wz180.f, sqrt(flip(-cumtrapz(flip(data_hac_Wz180.f), flip(data_hac_Wz180.pxx_Ry)))), 'DisplayName', sprintf('HAC $%.2f \\mu$rad', 1e6*rms(detrend(data_hac_Wz180.Ry_int, 0)))); +plot([1e-2, 1e4], 1e-6*[specs_ry_rms, specs_ry_rms], 'k--', 'DisplayName', sprintf('Spec: $%.2f \\mu$rad', specs_ry_rms)) +hold off; +set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log'); +xlabel('Frequency [Hz]'); ylabel('CAS [rad]'); +leg = legend('location', 'southwest', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; +xticks([1e0, 1e1, 1e2]); yticks([1e-9, 1e-8, 1e-7, 1e-6, 1e-5]); +xlim([0.1, 5e2]); ylim([1e-10, 2e-5]); + +% Reflectivity Scans +% <> + +% X-ray reflectivity measurements involve scanning thin structures, particularly solid/liquid interfaces, through the beam by varying the $R_y$ angle. +% In this experiment, a $R_y$ scan was executed at a rotational velocity of $100,\mu rad/s$, and the closed-loop positioning errors were monitored (Figure ref:fig:test_id31_reflectivity). +% The results confirm that the NASS successfully maintains the point of interest within the specified beam parameters throughout the scanning process. + + +%% Load data for the reflectivity scan +data_ry = load("2023-08-18_15-24_first_reflectivity_m0.mat"); +data_ry.time = Ts*[0:length(data_ry.Ry_int)-1]; + +% Compute closed-loop errors +data_ry.Dy_rms_cl = rms(detrend(data_ry.e_dy,0)); % [m RMS] +data_ry.Dz_rms_cl = rms(detrend(data_ry.e_dz,0)); % [m RMS] +data_ry.Ry_rms_cl = rms(detrend(data_ry.e_ry,0)); % [rad RMS] + +%% Ry reflectivity scan - Lateral error +figure; +hold on; +plot(data_ry.time, 1e9*data_ry.e_dy, 'DisplayName', sprintf('$\\epsilon D_y = %.0f$ nm RMS', 1e9*rms(data_ry.e_dy))) +plot([0, 6.2], [specs_dy_peak, specs_dy_peak], '--', 'color', colors(1,:), 'HandleVisibility', 'off'); +plot([0, 6.2], [-specs_dy_peak, -specs_dy_peak], '--', 'color', colors(1,:), 'HandleVisibility', 'off'); +hold off; +xlabel('Time [s]'); +ylabel('$D_y$ error [nm]') +% legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 1); +xlim([0, 6.2]); +ylim([-150, 150]); +xticks([0:2:6]); + +%% Ry reflectivity scan - Vertical error +figure; +hold on; +plot(data_ry.time, 1e9*data_ry.e_dz, 'DisplayName', sprintf('$\\epsilon D_z = %.0f$ nm RMS', 1e9*rms(data_ry.e_dz))) +plot([0, 6.2], [specs_dz_peak, specs_dz_peak], '--', 'color', colors(1,:), 'HandleVisibility', 'off'); +plot([0, 6.2], [-specs_dz_peak, -specs_dz_peak], '--', 'color', colors(1,:), 'HandleVisibility', 'off'); +hold off; +xlabel('Time [s]'); +ylabel('$D_z$ error [nm]') +% legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 1); +xlim([0, 6.2]); +ylim([-100, 100]); +xticks([0:2:6]); + +%% Ry reflectivity scan - Setpoint and Error +figure; +yyaxis left +hold on; +plot(data_ry.time, 1e6*data_ry.e_ry, 'DisplayName', '$\epsilon_{R_y}$') +plot([0, 6.2], [specs_ry_peak, specs_ry_peak], '--', 'HandleVisibility', 'off'); +plot([0, 6.2], [-specs_ry_peak, -specs_ry_peak], '--', 'HandleVisibility', 'off'); +hold off; +ylim([-2, 2]) +ylabel('$R_y$ error [$\mu$rad]') +yyaxis right +hold on; +plot(data_ry.time, 1e6*data_ry.Ry_int, 'DisplayName', '$R_y$') +plot(data_ry.time, 1e6*data_ry.m_hexa_ry, 'k--', 'DisplayName', 'Setpoint') +hold off; +xlabel('Time [s]'); +ylabel('$R_y$ motion [$\mu$rad]') +leg = legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; +xlim([0, 6.2]); +ylim([-310, 310]); +xticks([0:2:6]); + +% Step by Step $D_z$ motion + +% The vertical step motion is performed exclusively with the nano-hexapod. +% Testing was conducted across step sizes ranging from $10,nm$ to $1,\mu m$, with results presented in Figure ref:fig:test_id31_dz_mim_steps. The system successfully resolves 10nm steps when detectors integrate over a 50ms period (illustrated by the red curve in Figure ref:fig:test_id31_dz_mim_10nm_steps), which is compatible with many experimental requirements. + +% In step-by-step scanning procedures, settling time is a critical parameter as it significantly impacts the total experiment duration. +% The system achieves a response time of approximately $70,ms$ to reach the target position (within $\pm 20,nm$), as demonstrated by the $1,\mu m$ step response in Figure ref:fig:test_id31_dz_mim_1000nm_steps. +% This settling duration typically decreases for smaller step sizes. + + +%% Load Dz steps data +data_dz_steps_10nm = load("2023-08-18_14-57_dz_mim_10_nm.mat"); +data_dz_steps_10nm.time = Ts*[0:length(data_dz_steps_10nm.Dz_int)-1]; + +data_dz_steps_100nm = load("2023-08-18_14-57_dz_mim_100_nm.mat"); +data_dz_steps_100nm.time = Ts*[0:length(data_dz_steps_100nm.Dz_int)-1]; + +data_dz_steps_1000nm = load("2023-08-18_14-57_dz_mim_1000_nm.mat"); +data_dz_steps_1000nm.time = Ts*[0:length(data_dz_steps_1000nm.Dz_int)-1]; + +%% Dz MIM test with 10nm steps +figure; +hold on; +plot(data_dz_steps_10nm.time, 1e9*(data_dz_steps_10nm.Dz_int - mean(data_dz_steps_10nm.Dz_int(1:1000))), 'DisplayName', '$D_z$') +plot(data_dz_steps_10nm.time, 1e9*lsim(1/(1 + s/2/pi/20), data_dz_steps_10nm.Dz_int - mean(data_dz_steps_10nm.Dz_int(1:1000)), data_dz_steps_10nm.time), 'DisplayName', '$D_z$ (LPF)') +plot(data_dz_steps_10nm.time, 1e9*(data_dz_steps_10nm.m_hexa_dz-data_dz_steps_10nm.m_hexa_dz(1)), 'k--', 'DisplayName', 'Setpoint') +hold off; +xlabel('Time [s]'); +ylabel('$D_z$ Motion [nm]'); +legend('location', 'northwest', 'FontSize', 8, 'NumColumns', 1); +xlim([0, 0.6]); +ylim([-10, 40]); +xticks([0:0.2:0.6]); +yticks([-10:10:50]); + +%% Dz MIM test with 100nm steps +figure; +hold on; +plot(data_dz_steps_100nm.time, 1e9*(data_dz_steps_100nm.Dz_int - mean(data_dz_steps_100nm.Dz_int(1:1000))), 'DisplayName', '$D_z$') +plot(data_dz_steps_100nm.time, 1e9*(data_dz_steps_100nm.m_hexa_dz-data_dz_steps_100nm.m_hexa_dz(1)), 'k--', 'DisplayName', 'Setpoint') +hold off; +xlabel('Time [s]'); +ylabel('$D_z$ Motion [nm]'); +legend('location', 'northwest', 'FontSize', 8, 'NumColumns', 1); +xlim([0, 0.6]); +% ylim([-10, 40]); +xticks([0:0.2:0.6]); +yticks([-0:100:300]); + +%% Dz step response - Stabilization time is around 70ms +figure; +[~, i] = find(data_dz_steps_1000nm.m_hexa_dz>data_dz_steps_1000nm.m_hexa_dz(1)); +i0 = i(1); + +figure; +hold on; +plot(1e3*(data_dz_steps_1000nm.time-data_dz_steps_1000nm.time(i0)), 1e6*(data_dz_steps_1000nm.Dz_int - mean(data_dz_steps_1000nm.Dz_int(1:1000)))) +plot(1e3*[-1, 1], 1e-3*[1000-20, 1000-20], 'k--') +plot(1e3*[-1, 1], 1e-3*[1000+20, 1000+20], 'k--') +xline(0, 'k--', 'LineWidth', 1.5) +xline(70, 'k--', 'LineWidth', 1.5) +hold off; +xlabel('Time [ms]'); +ylabel('$D_z$ Motion [$\mu$m]'); +xlim([-10, 140]); +ylim([-0.1, 1.6]) +xticks([0, 70]) +yticks([0, 1]) + +% Continuous $D_z$ motion: Dirty Layer Scans + +% For these and subsequent experiments, the NASS performs "ramp scans" (constant velocity scans). +% To eliminate tracking errors, the feedback controller incorporates two integrators, compensating for the plant's lack of integral action at low frequencies. + +% Initial testing at $10,\mu m/s$ demonstrates positioning errors well within specifications (indicated by dashed lines in Figure ref:fig:test_id31_dz_scan_10ums). + + +%% Dirty layer scans - 10um/s +data_dz_10ums = load("2023-08-18_15-33_dirty_layer_m0_small.mat"); +data_dz_10ums.time = Ts*[0:length(data_dz_10ums.Dz_int)-1]; + +%% Dirty layer scans - 100um/s +data_dz_100ums = load("2023-08-18_15-32_dirty_layer_m0.mat"); +data_dz_100ums.time = Ts*[0:length(data_dz_100ums.Dz_int)-1]; + +%% Performances for Dz scans - 10 um/s +% Determine when the motion starts and stops +i_dz_10ums = abs(diff(data_dz_10ums.m_hexa_dz)/Ts-10e-6) < 10*eps; + +% RMS error +data_dz_10ums.Dy_rms_cl = rms(detrend(data_dz_10ums.e_dy(i_dz_10ums), 0)); +data_dz_10ums.Dz_rms_cl = rms(detrend(data_dz_10ums.e_dz(i_dz_10ums), 0)); +data_dz_10ums.Ry_rms_cl = rms(detrend(data_dz_10ums.e_ry(i_dz_10ums), 0)); + +%% Performances for Dz scans - 100 um/s +i_dz_100ums = abs(diff(data_dz_100ums.m_hexa_dz)/Ts-100e-6) < 10*eps; + +% RMS error +data_dz_100ums.Dy_rms_cl = rms(detrend(data_dz_100ums.e_dy(i_dz_100ums), 0)); +data_dz_100ums.Dz_rms_cl = rms(detrend(data_dz_100ums.e_dz(i_dz_100ums), 0)); +data_dz_100ums.Ry_rms_cl = rms(detrend(data_dz_100ums.e_ry(i_dz_100ums), 0)); + +%% Dz scan at 10um/s - Lateral error +figure; +hold on; +plot(data_dz_10ums.time, 1e9*data_dz_10ums.e_dy, 'DisplayName', sprintf('$\\epsilon D_y: %.0f$ nm RMS', 1e9*rms(data_dz_10ums.e_dy))) +plot([0, 2.2], [specs_dy_peak, specs_dy_peak], '--', 'color', colors(1,:), 'HandleVisibility', 'off'); +plot([0, 2.2], [-specs_dy_peak, -specs_dy_peak], '--', 'color', colors(1,:), 'HandleVisibility', 'off'); +hold off; +xlabel('Time [s]'); +ylabel('$D_y$ error [nm]') +% leg = legend('location', 'northwest', 'FontSize', 8, 'NumColumns', 1); +% leg.ItemTokenSize(1) = 15; +xlim([0, 2.2]); +ylim([-150, 150]) + +%% Dz scan at 10um/s - Vertical error +figure; +yyaxis left +hold on; +plot(data_dz_10ums.time, 1e9*data_dz_10ums.e_dz, 'DisplayName', '$\epsilon_{D_z}$') +plot([0, 2.2], [specs_dz_peak, specs_dz_peak], '--', 'HandleVisibility', 'off'); +plot([0, 2.2], [-specs_dz_peak, -specs_dz_peak], '--', 'HandleVisibility', 'off'); +hold off; +ylabel('$D_z$ error [nm]'); +ylim([-100, 100]); +yticks([-50:50:50]); +yyaxis right +hold on; +plot(data_dz_10ums.time, 1e6*(data_dz_10ums.Dz_int), 'DisplayName', '$D_z$') +plot(data_dz_10ums.time, 1e6*(data_dz_10ums.m_hexa_dz), 'k--', 'DisplayName', 'Setpoint') +hold off; +xlabel('Time [s]'); +ylabel('$D_z$ Motion [$\mu$m]'); +leg = legend('location', 'northwest', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; +xlim([0, 2.2]); +ylim([-10, 10]); + +%% Dz scan at 10um/s - Ry error +figure; +hold on; +plot(data_dz_10ums.time, 1e6*data_dz_10ums.e_ry, 'DisplayName', sprintf('$\\epsilon R_y: %.2f \\mu$rad RMS', 1e6*rms(data_dz_10ums.e_ry))) +plot([0, 2.2], [specs_ry_peak, specs_ry_peak], '--', 'color', colors(1,:), 'HandleVisibility', 'off'); +plot([0, 2.2], [-specs_ry_peak, -specs_ry_peak], '--', 'color', colors(1,:), 'HandleVisibility', 'off'); +hold off; +xlabel('Time [s]'); +ylabel('$R_y$ error [$\mu$rad]') +% leg = legend('location', 'north', 'FontSize', 8, 'NumColumns', 1); +% leg.ItemTokenSize(1) = 15; +xlim([0, 2.2]); +ylim([-2, 2]); + + + +% #+name: fig:test_id31_dz_scan_10ums +% #+caption: $D_z$ scan with a velocity of $10\,\mu m/s$. $D_z$ setpoint, measured position and error are shown in (\subref{fig:test_id31_dz_scan_10ums_dz}). Errors in $D_y$ and $R_y$ are respectively shown in (\subref{fig:test_id31_dz_scan_10ums_dy}) and (\subref{fig:test_id31_dz_scan_10ums_ry}) +% #+attr_latex: :options [htbp] +% #+begin_figure +% #+attr_latex: :caption \subcaption{\label{fig:test_id31_dz_scan_10ums_dy}$D_y$} +% #+attr_latex: :options {0.33\textwidth} +% #+begin_subfigure +% #+attr_latex: :scale 1 +% [[file:figs/test_id31_dz_scan_10ums_dy.png]] +% #+end_subfigure +% #+attr_latex: :caption \subcaption{\label{fig:test_id31_dz_scan_10ums_dz}$D_z$} +% #+attr_latex: :options {0.33\textwidth} +% #+begin_subfigure +% #+attr_latex: :scale 1 +% [[file:figs/test_id31_dz_scan_10ums_dz.png]] +% #+end_subfigure +% #+attr_latex: :caption \subcaption{\label{fig:test_id31_dz_scan_10ums_ry}$R_y$} +% #+attr_latex: :options {0.33\textwidth} +% #+begin_subfigure +% #+attr_latex: :scale 1 +% [[file:figs/test_id31_dz_scan_10ums_ry.png]] +% #+end_subfigure +% #+end_figure + +% A subsequent scan at $100,\mu m/s$ - the maximum velocity for high-precision $D_z$ scans[fn:8] - maintains positioning errors within specifications during the constant velocity phase, with deviations occurring only during acceleration and deceleration phases (Figure ref:fig:test_id31_dz_scan_100ums). +% Since detectors typically operate only during the constant velocity phase, these transient deviations do not compromise measurement quality. +% Yet, performance during acceleration phases could potentially be enhanced through the implementation of feedforward control. + + +%% Dz scan at 100um/s - Lateral error +figure; +hold on; +plot(data_dz_100ums.time, 1e9*data_dz_100ums.e_dy, 'DisplayName', sprintf('$\\epsilon D_y = %.0f$ nm RMS', 1e9*rms(data_dz_100ums.e_dy))) +plot([0, 2.2], [specs_dy_peak, specs_dy_peak], '--', 'color', colors(1,:), 'HandleVisibility', 'off'); +plot([0, 2.2], [-specs_dy_peak, -specs_dy_peak], '--', 'color', colors(1,:), 'HandleVisibility', 'off'); +hold off; +xlabel('Time [s]'); +ylabel('$D_y$ error [nm]') +% legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 1); +xlim([0, 2.2]); +ylim([-150, 150]) + +%% Dz scan at 100um/s - Vertical error +figure; +yyaxis left +hold on; +plot(data_dz_100ums.time, 1e9*data_dz_100ums.e_dz, 'DisplayName', '$\epsilon_{d_z}$') +plot([0, 2.2], [specs_dz_peak, specs_dz_peak], '--', 'HandleVisibility', 'off'); +plot([0, 2.2], [-specs_dz_peak, -specs_dz_peak], '--', 'HandleVisibility', 'off'); +hold off; +ylabel('$D_z$ error [nm]'); +ylim([-100, 100]); +yticks([-50:50:50]); +yyaxis right +hold on; +plot(data_dz_100ums.time, 1e6*(data_dz_100ums.Dz_int), 'DisplayName', '$D_z$') +plot(data_dz_100ums.time, 1e6*(data_dz_100ums.m_hexa_dz), 'k--', 'DisplayName', 'Setpoint') +hold off; +xlabel('Time [s]'); +ylabel('$D_z$ Motion [$\mu$m]'); +leg = legend('location', 'northwest', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; +xlim([0, 2.2]); +ylim([-100, 100]); + +%% Dz scan at 100um/s - Tilt error +figure; +hold on; +plot(data_dz_100ums.time, 1e6*data_dz_100ums.e_ry, 'DisplayName', sprintf('$\\epsilon R_y = %.2f \\mu$rad RMS', 1e6*rms(data_dz_100ums.e_ry))) +plot([0, 2.2], [specs_ry_peak, specs_ry_peak], '--', 'color', colors(1,:), 'HandleVisibility', 'off'); +plot([0, 2.2], [-specs_ry_peak, -specs_ry_peak], '--', 'color', colors(1,:), 'HandleVisibility', 'off'); +hold off; +xlabel('Time [s]'); +ylabel('$R_y$ error [$\mu$rad]') +% legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 1); +xlim([0, 2.2]); +ylim([-2, 2]) + +% Slow scan + +% Initial testing utilized a scanning velocity of $10,\mu m/s$, which is typical for these experiments. +% Figure ref:fig:test_id31_dy_10ums compares the positioning errors between open-loop (without NASS) and closed-loop operation. +% In the scanning direction, open-loop measurements reveal periodic errors (Figure ref:fig:test_id31_dy_10ums_dy) attributable to the $T_y$ stage's stepper motor. +% These micro-stepping errors, inherent to stepper motor operation, occur 200 times per motor rotation with approximately $1\,\text{mrad}$ angular error amplitude. +% Given the $T_y$ stage's lead screw pitch of $2\,mm$, these errors manifest as $10\,\mu m$ periodic oscillations with $\approx 300\,nm$ amplitude, which can indeed be seen in the open-loop measurements (Figure ref:fig:test_id31_dy_10ums_dy). + +% In the vertical direction (Figure ref:fig:test_id31_dy_10ums_dz), open-loop errors likely stem from metrology measurement error due to the fact that the top interferometer points at a spherical target surface (see Figure ref:fig:test_id31_xy_map_sphere). +% Under closed-loop control, positioning errors remain within specifications across all directions. + + +%% Slow Ty scan (10um/s) - OL +data_ty_ol_10ums = load("2023-08-21_20-05_ty_scan_m1_open_loop_slow.mat"); +data_ty_ol_10ums.time = Ts*[0:length(data_ty_ol_10ums.Dy_int)-1]; + +%% Slow Ty scan (10um/s) - CL +data_ty_cl_10ums = load("2023-08-21_20-07_ty_scan_m1_cf_closed_loop_slow.mat"); +data_ty_cl_10ums.time = Ts*[0:length(data_ty_cl_10ums.Dy_int)-1]; + +%% Ty scan (at 10um/s) - Dy errors +figure; +hold on; +plot(1e6*data_ty_ol_10ums.Ty, 1e6*detrend(data_ty_ol_10ums.e_dy, 0), ... + 'DisplayName', 'Open-loop') +plot(1e6*data_ty_cl_10ums.Ty, 1e6*detrend(data_ty_cl_10ums.e_dy, 0), ... + 'DisplayName', 'Closed-loop') +plot([-100, 100], 1e-3*[specs_dy_peak, specs_dy_peak], 'k--', 'DisplayName', 'Specifications'); +plot([-100, 100], 1e-3*[-specs_dy_peak, -specs_dy_peak], 'k--', 'HandleVisibility', 'off'); +hold off; +xlabel('Ty position [$\mu$m]'); +ylabel('$D_y$ error [$\mu$m]'); +xlim([-100, 100]) +leg = legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; + +%% Ty scan (at 10um/s) - Dz and Ry errors +figure; +hold on; +plot(1e6*data_ty_ol_10ums.Ty, 1e6*detrend(data_ty_ol_10ums.e_dz, 0), ... + 'DisplayName', 'Open-loop') +plot(1e6*data_ty_cl_10ums.Ty, 1e6*detrend(data_ty_cl_10ums.e_dz, 0), ... + 'DisplayName', 'Closed-loop') +plot([-100, 100], 1e-3*[specs_dz_peak, specs_dz_peak], 'k--', 'DisplayName', 'Specifications'); +plot([-100, 100], 1e-3*[-specs_dz_peak, -specs_dz_peak], 'k--', 'HandleVisibility', 'off'); +hold off; +xlabel('Ty position [$\mu$m]'); +ylabel('$D_z$ error [$\mu$m]'); +xlim([-100, 100]) +ylim([-0.4, 0.4]) +leg = legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; + +figure; +hold on; +plot(1e6*data_ty_ol_10ums.Ty, 1e6*data_ty_ol_10ums.e_ry, ... + 'DisplayName', 'Open-loop') +plot(1e6*data_ty_cl_10ums.Ty, 1e6*data_ty_cl_10ums.e_ry, ... + 'DisplayName', 'Closed-loop') +plot([-100, 100], [specs_ry_peak, specs_ry_peak], 'k--', 'DisplayName', 'Specifications'); +plot([-100, 100], [-specs_ry_peak, -specs_ry_peak], 'k--', 'HandleVisibility', 'off'); +hold off; +xlabel('Ty position [$\mu$m]'); +ylabel('$R_y$ error [$\mu$rad]'); +xlim([-100, 100]); +ylim([-10, 10]) +leg = legend('location', 'northwest', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; + +% Fast Scan + +% System performance was evaluated at an increased scanning velocity of $100\,\mu m/s$, with results presented in Figure ref:fig:test_id31_dy_100ums. +% At this velocity, the micro-stepping errors generate $10\,\text{Hz}$ vibrations, which are further amplified by micro-station resonances. +% These vibrations exceed the NASS feedback controller bandwidth, resulting in limited attenuation under closed-loop control. +% This limitation exemplifies why stepper motors are suboptimal for "long-stroke/short-stroke" systems requiring precise scanning performance [[cite:&dehaeze22_fastj_uhv]]. + +% Two potential solutions exist for improving high-velocity scanning performance. +% First, the $T_y$ stage's stepper motor could be replaced with a three-phase torque motor. +% Alternatively, since closed-loop errors in $D_z$ and $R_y$ directions remain within specifications (Figures ref:fig:test_id31_dy_100ums_dz and ref:fig:test_id31_dy_100ums_ry), detector triggering could be based on measured $D_y$ position rather than time or $T_y$ setpoint, reducing sensitivity to $D_y$ vibrations. +% For applications requiring small $D_y$ scans, the nano-hexapod can be used exclusively, though with limited stroke capability. + + +%% Fast Ty scan (100um/s) - OL +data_ty_ol_100ums = load("2023-08-21_20-05_ty_scan_m1_open_loop.mat"); +data_ty_ol_100ums.time = Ts*[0:length(data_ty_ol_100ums.Dy_int)-1]; + +%% Fast Ty scan (100um/s) - CL +data_ty_cl_100ums = load("2023-08-21_20-07_ty_scan_m1_cf_closed_loop.mat"); +data_ty_cl_100ums.time = Ts*[0:length(data_ty_cl_100ums.Dy_int)-1]; + +%% Ty scan (at 100um/s) - Dy errors +figure; +hold on; +plot(1e6*data_ty_ol_100ums.Ty, 1e6*detrend(data_ty_ol_100ums.e_dy, 0), ... + 'DisplayName', 'Open-loop') +plot(1e6*data_ty_cl_100ums.Ty, 1e6*detrend(data_ty_cl_100ums.e_dy, 0), ... + 'DisplayName', 'Closed-loop') +plot([-100, 100], 1e-3*[specs_dy_peak, specs_dy_peak], 'k--', 'DisplayName', 'Specifications'); +plot([-100, 100], 1e-3*[-specs_dy_peak, -specs_dy_peak], 'k--', 'HandleVisibility', 'off'); +hold off; +xlabel('Ty position [$\mu$m]'); +ylabel('$D_y$ error [$\mu$m]'); +xlim([-100, 100]); +ylim([-3, 3]); +leg = legend('location', 'southwest', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; + +%% Ty scan (at 100um/s) - Dz and Ry errors +figure; +hold on; +plot(1e6*data_ty_ol_100ums.Ty, 1e6*detrend(data_ty_ol_100ums.e_dz, 0), ... + 'DisplayName', 'Open-loop') +plot(1e6*data_ty_cl_100ums.Ty, 1e6*detrend(data_ty_cl_100ums.e_dz, 0), ... + 'DisplayName', 'Closed-loop') +plot([-100, 100], 1e-3*[specs_dz_peak, specs_dz_peak], 'k--', 'DisplayName', 'Specifications'); +plot([-100, 100], 1e-3*[-specs_dz_peak, -specs_dz_peak], 'k--', 'HandleVisibility', 'off'); +hold off; +xlabel('Ty position [$\mu$m]'); +ylabel('$D_z$ error [$\mu$m]'); +xlim([-100, 100]); +ylim([-0.4, 0.4]); +leg = legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; + +figure; +hold on; +plot(1e6*data_ty_ol_100ums.Ty, 1e6*data_ty_ol_100ums.e_ry, ... + 'DisplayName', 'Open-loop') +plot(1e6*data_ty_cl_100ums.Ty, 1e6*data_ty_cl_100ums.e_ry, ... + 'DisplayName', 'Closed-loop') +plot([-100, 100], [specs_ry_peak, specs_ry_peak], 'k--', 'DisplayName', 'Specifications'); +plot([-100, 100], [-specs_ry_peak, -specs_ry_peak], 'k--', 'HandleVisibility', 'off'); +hold off; +xlabel('Ty position [$\mu$m]'); +ylabel('$R_y$ error [$\mu$rad]'); +xlim([-100, 100]); +ylim([-10, 10]) +leg = legend('location', 'northwest', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; + + + +% #+name: fig:test_id31_dy_100ums +% #+caption: Open-Loop (in blue) and Closed-loop (i.e. using the NASS, in red) during a $100\,\mu m/s$ scan with the $T_y$ stage. Errors in $D_y$ is shown in (\subref{fig:test_id31_dy_100ums_dy}). +% #+attr_latex: :options [htbp] +% #+begin_figure +% #+attr_latex: :caption \subcaption{\label{fig:test_id31_dy_100ums_dy} $D_y$} +% #+attr_latex: :options {0.33\textwidth} +% #+begin_subfigure +% #+attr_latex: :scale 1 +% [[file:figs/test_id31_dy_100ums_dy.png]] +% #+end_subfigure +% #+attr_latex: :caption \subcaption{\label{fig:test_id31_dy_100ums_dz} $D_z$} +% #+attr_latex: :options {0.33\textwidth} +% #+begin_subfigure +% #+attr_latex: :scale 1 +% [[file:figs/test_id31_dy_100ums_dz.png]] +% #+end_subfigure +% #+attr_latex: :caption \subcaption{\label{fig:test_id31_dy_100ums_ry} $R_y$} +% #+attr_latex: :options {0.33\textwidth} +% #+begin_subfigure +% #+attr_latex: :scale 1 +% [[file:figs/test_id31_dy_100ums_ry.png]] +% #+end_subfigure +% #+end_figure + + +%% Compute errors for Dy scans +i_ty_ol_10ums = data_ty_ol_10ums.Ty > data_ty_ol_10ums.Ty(1) & data_ty_ol_10ums.Ty < data_ty_ol_10ums.Ty(end); +i_ty_cl_10ums = data_ty_cl_10ums.Ty > data_ty_cl_10ums.Ty(1) & data_ty_cl_10ums.Ty < data_ty_cl_10ums.Ty(end); +i_ty_ol_100ums = data_ty_ol_100ums.Ty > data_ty_ol_100ums.Ty(1) & data_ty_ol_100ums.Ty < data_ty_ol_100ums.Ty(end); +i_ty_cl_100ums = data_ty_cl_100ums.Ty > data_ty_cl_100ums.Ty(1) & data_ty_cl_100ums.Ty < data_ty_cl_100ums.Ty(end); + +% RMS error +data_ty_ol_10ums.Dy_rms = rms(detrend(data_ty_ol_10ums.e_dy(i_ty_ol_10ums), 0)); +data_ty_ol_10ums.Dz_rms = rms(detrend(data_ty_ol_10ums.e_dz(i_ty_ol_10ums), 0)); +data_ty_ol_10ums.Ry_rms = rms(detrend(data_ty_ol_10ums.e_ry(i_ty_ol_10ums), 0)); + +data_ty_cl_10ums.Dy_rms = rms(detrend(data_ty_cl_10ums.e_dy(i_ty_cl_10ums), 0)); +data_ty_cl_10ums.Dz_rms = rms(detrend(data_ty_cl_10ums.e_dz(i_ty_cl_10ums), 0)); +data_ty_cl_10ums.Ry_rms = rms(detrend(data_ty_cl_10ums.e_ry(i_ty_cl_10ums), 0)); + +data_ty_ol_100ums.Dy_rms = rms(detrend(data_ty_ol_100ums.e_dy(i_ty_ol_100ums), 0)); +data_ty_ol_100ums.Dz_rms = rms(detrend(data_ty_ol_100ums.e_dz(i_ty_ol_100ums), 0)); +data_ty_ol_100ums.Ry_rms = rms(detrend(data_ty_ol_100ums.e_ry(i_ty_ol_100ums), 0)); + +data_ty_cl_100ums.Dy_rms = rms(detrend(data_ty_cl_100ums.e_dy(i_ty_cl_100ums), 0)); +data_ty_cl_100ums.Dz_rms = rms(detrend(data_ty_cl_100ums.e_dz(i_ty_cl_100ums), 0)); +data_ty_cl_100ums.Ry_rms = rms(detrend(data_ty_cl_100ums.e_ry(i_ty_cl_100ums), 0)); + +% Diffraction Tomography +% <> +% In diffraction tomography experiments, the micro-station executes combined motions: continuous rotation around the $R_z$ axis while performing lateral scans along $D_y$. +% For this validation, the spindle maintained a constant rotational velocity of $6\,\text{deg/s}$ while the nano-hexapod executed the lateral scanning motion. +% To avoid high-frequency vibrations typically induced by the stepper motor, the $T_y$ stage was not utilized, which constrained the scanning range to approximately $\pm 100\,\mu m/s$. +% The system's performance was evaluated at three lateral scanning velocities: $0.1\,mm/s$, $0.5\,mm/s$, and $1\,mm/s$. Figure ref:fig:test_id31_diffraction_tomo_setpoint presents both the $D_y$ position setpoints and the corresponding measured $D_y$ positions for all tested velocities. + + +%% 100um/s - Robust controller +data_dt_100ums = load("2023-08-18_17-12_diffraction_tomo_m0.mat"); +t = Ts*[0:length(data_dt_100ums.Dy_int)-1]; +data_dt_100ums = structfun(@(field) field(t>1.0861),data_dt_100ums, 'UniformOutput', false); +data_dt_100ums.time = Ts*[0:length(data_dt_100ums.Dy_int)-1]; + +%% 500um/s - Complementary filters +data_dt_500ums = load("2023-08-21_15-15_diffraction_tomo_m0_fast_cf.mat"); +t = Ts*[0:length(data_dt_500ums.Dy_int)-1]; +data_dt_500ums = structfun(@(field) field(t>0.275),data_dt_500ums, 'UniformOutput', false); +data_dt_500ums.time = Ts*[0:length(data_dt_500ums.Dy_int)-1]; + +%% 1mm/s - Complementary filters +data_dt_1000ums = load("2023-08-21_15-16_diffraction_tomo_m0_fast_cf.mat"); +t = Ts*[0:length(data_dt_1000ums.Dy_int)-1]; +data_dt_1000ums = structfun(@(field) field(t>0.19),data_dt_1000ums, 'UniformOutput', false); +data_dt_1000ums.time = Ts*[0:length(data_dt_1000ums.Dy_int)-1]; + +%% Dy motion for several configured velocities +figure; +hold on; +plot(data_dt_1000ums.time, 1e6*data_dt_1000ums.Dy_int, 'color', colors(1,:), ... + 'DisplayName', '$1 mm/s$') +plot(data_dt_1000ums.time, 1e6*data_dt_1000ums.m_hexa_dy, 'k--', ... + 'HandleVisibility', 'off') +plot(data_dt_500ums.time, 1e6*data_dt_500ums.Dy_int, 'color', colors(2,:), ... + 'DisplayName', '$0.5 mm/s$') +plot(data_dt_500ums.time, 1e6*data_dt_500ums.m_hexa_dy, 'k--', ... + 'HandleVisibility', 'off') +plot(data_dt_100ums.time, 1e6*data_dt_100ums.Dy_int, 'color', colors(3,:), ... + 'DisplayName', '$0.1 mm/s$') +plot(data_dt_100ums.time, 1e6*data_dt_100ums.m_hexa_dy, 'k--', ... + 'DisplayName', 'Setpoint') +hold off; +xlim([0, 4]); +ylim([-110, 110]); +xlabel('Time [s]'); +ylabel('$D_y$ position [$\mu$m]') +legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 1); + + + +% #+name: fig:test_id31_diffraction_tomo_setpoint +% #+caption: Dy motion for several configured velocities +% #+RESULTS: +% [[file:figs/test_id31_diffraction_tomo_setpoint.png]] + +% The positioning errors measured along $D_y$, $D_z$, and $R_y$ directions are displayed in Figure ref:fig:test_id31_diffraction_tomo. +% The system maintained positioning errors within specifications for both $D_z$ and $R_y$ (Figures ref:fig:test_id31_diffraction_tomo_dz and ref:fig:test_id31_diffraction_tomo_ry). +% However, lateral positioning errors exceeded specifications during acceleration and deceleration phases (Figure ref:fig:test_id31_diffraction_tomo_dy). +% Since these large errors occurred only during $\approx 20\,ms$ intervals, the issue could be addressed by implementing a corresponding delay in detector integration. +% Alternatively, developing a feedforward controller could improve lateral positioning accuracy during these transient phases. + + +%% Diffraction Tomography - Dy errors for several configured velocities +figure; +hold on; +plot(data_dt_1000ums.time, 1e9*(data_dt_1000ums.Dy_int - data_dt_1000ums.m_hexa_dy), ... + 'DisplayName', '$1 mm/s$') +plot(data_dt_500ums.time, 1e9*(data_dt_500ums.Dy_int - data_dt_500ums.m_hexa_dy), ... + 'DisplayName', '$0.5 mm/s$') +plot(data_dt_100ums.time, 1e9*(data_dt_100ums.Dy_int - data_dt_100ums.m_hexa_dy), ... + 'DisplayName', '$0.1 mm/s$') +plot([0, 6.2], [specs_dy_peak, specs_dy_peak], 'k--', 'DisplayName', 'Specs'); +plot([0, 6.2], [-specs_dy_peak, -specs_dy_peak], 'k--', 'HandleVisibility', 'off'); +hold off; +xlim([0, 3]); +xlabel('Time [s]'); +ylabel('$D_y$ error [nm]') +leg = legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; + +%% Diffraction Tomography - Dz errors for several configured velocities +figure; +hold on; +plot(data_dt_1000ums.time, 1e9*data_dt_1000ums.Dz_int, ... + 'DisplayName', '$1 mm/s$') +plot(data_dt_500ums.time, 1e9*data_dt_500ums.Dz_int, ... + 'DisplayName', '$0.5 mm/s$') +plot(data_dt_100ums.time, 1e9*data_dt_100ums.Dz_int, ... + 'DisplayName', '$0.1 mm/s$') +plot([0, 6.2], [specs_dz_peak, specs_dz_peak], 'k--', 'DisplayName', 'Specs'); +plot([0, 6.2], [-specs_dz_peak, -specs_dz_peak], 'k--', 'HandleVisibility', 'off'); +hold off; +xlim([0, 4]); +ylim([-100, 100]) +xlabel('Time [s]'); +ylabel('$D_z$ position [nm]') +leg = legend('location', 'southwest', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; + +%% Diffraction Tomography - Ry errors for several configured velocities +figure; +hold on; +plot(data_dt_1000ums.time, 1e6*data_dt_1000ums.Ry_int, ... + 'DisplayName', '$1 mm/s$') +plot(data_dt_500ums.time, 1e6*data_dt_500ums.Ry_int, ... + 'DisplayName', '$0.5 mm/s$') +plot(data_dt_100ums.time, 1e6*data_dt_100ums.Ry_int, ... + 'DisplayName', '$0.1 mm/s$') +plot([0, 6.2], [specs_ry_peak, specs_ry_peak], 'k--', 'DisplayName', 'Specs'); +plot([0, 6.2], [-specs_ry_peak, -specs_ry_peak], 'k--', 'HandleVisibility', 'off'); +hold off; +xlim([0, 4]); +ylim([-1.5, 1.5]) +xlabel('Time [s]'); +ylabel('$R_y$ position [$\mu$rad]') +leg = legend('location', 'southwest', 'FontSize', 8, 'NumColumns', 1); +leg.ItemTokenSize(1) = 15; + + + +% #+name: fig:test_id31_diffraction_tomo +% #+caption: Diffraction tomography scans (combined $R_z$ and $D_y$ motions) at several $D_y$ velocities ($R_z$ rotational velocity is $6\,\text{deg/s}$). +% #+attr_latex: :options [htbp] +% #+begin_figure +% #+attr_latex: :caption \subcaption{\label{fig:test_id31_diffraction_tomo_dy} $D_y$} +% #+attr_latex: :options {0.33\textwidth} +% #+begin_subfigure +% #+attr_latex: :scale 1 +% [[file:figs/test_id31_diffraction_tomo_dy.png]] +% #+end_subfigure +% #+attr_latex: :caption \subcaption{\label{fig:test_id31_diffraction_tomo_dz} $D_z$} +% #+attr_latex: :options {0.33\textwidth} +% #+begin_subfigure +% #+attr_latex: :scale 1 +% [[file:figs/test_id31_diffraction_tomo_dz.png]] +% #+end_subfigure +% #+attr_latex: :caption \subcaption{\label{fig:test_id31_diffraction_tomo_ry} $R_y$} +% #+attr_latex: :options {0.33\textwidth} +% #+begin_subfigure +% #+attr_latex: :scale 1 +% [[file:figs/test_id31_diffraction_tomo_ry.png]] +% #+end_subfigure +% #+end_figure + + +%% Computation of errors during diffraction tomography experiments +% Ignore acceleration and deceleration phases +acc_dt = 20e-3; % Acceleration phase to remove [s] +acc_n = acc_dt/Ts; % Number of points to delete + +% Determine when the motion starts and stops +i_dt_100ums = data_dt_100ums.m_hexa_dy>data_dt_100ums.m_hexa_dy(1) & data_dt_100ums.m_hexa_dy0); % Acceleration phases +for i = i_acc + i_dt_100ums(i:i+acc_n) = 0; +end +[~, i_dec] = find(diff(i_dt_100ums)<0); % Deceleration phases +for i = i_dec(2:2:end) + i_dt_100ums(i-acc_n:i) = 0; +end + +i_dt_500ums = data_dt_500ums.m_hexa_dy>data_dt_500ums.m_hexa_dy(1) & data_dt_500ums.m_hexa_dy0); % Acceleration phases +for i = i_acc + i_dt_500ums(i:i+acc_n) = 0; +end +[~, i_dec] = find(diff(i_dt_500ums)<0); % Deceleration phases +for i = i_dec(2:2:end) + i_dt_500ums(i-acc_n:i) = 0; +end + +i_dt_1000ums = data_dt_1000ums.m_hexa_dy>data_dt_1000ums.m_hexa_dy(1) & data_dt_1000ums.m_hexa_dy0); % Acceleration phases +for i = i_acc + i_dt_1000ums(i:i+acc_n) = 0; +end +[~, i_dec] = find(diff(i_dt_1000ums)<0); % Deceleration phases +for i = i_dec(2:2:end) + i_dt_1000ums(i-acc_n:i) = 0; +end + +% RMS error +data_dt_100ums.Dy_rms_cl = rms(detrend(data_dt_100ums.Dy_int(i_dt_100ums)-data_dt_100ums.m_hexa_dy(i_dt_100ums), 0)); +data_dt_100ums.Dz_rms_cl = rms(detrend(data_dt_100ums.Dz_int(i_dt_100ums), 0)); +data_dt_100ums.Ry_rms_cl = rms(detrend(data_dt_100ums.Ry_int(i_dt_100ums), 0)); + +data_dt_500ums.Dy_rms_cl = rms(detrend(data_dt_500ums.Dy_int(i_dt_500ums)-data_dt_500ums.m_hexa_dy(i_dt_500ums), 0)); +data_dt_500ums.Dz_rms_cl = rms(detrend(data_dt_500ums.Dz_int(i_dt_500ums), 0)); +data_dt_500ums.Ry_rms_cl = rms(detrend(data_dt_500ums.Ry_int(i_dt_500ums), 0)); + +data_dt_1000ums.Dy_rms_cl = rms(detrend(data_dt_1000ums.Dy_int(i_dt_1000ums)-data_dt_1000ums.m_hexa_dy(i_dt_1000ums), 0)); +data_dt_1000ums.Dz_rms_cl = rms(detrend(data_dt_1000ums.Dz_int(i_dt_1000ums), 0)); +data_dt_1000ums.Ry_rms_cl = rms(detrend(data_dt_1000ums.Ry_int(i_dt_1000ums), 0)); + +% Conclusion +% <> + +% A comprehensive series of experimental validations was conducted to evaluate the NASS performance across a wide range of typical scientific experiments. +% The system demonstrated robust performance in most scenarios, with positioning errors generally remaining within specified tolerances (30 nm RMS in $D_y$, 15 nm RMS in $D_z$, and 250 nrad RMS in $R_y$). + +% For tomography experiments, the NASS successfully maintained positioning accuracy at rotational velocities up to $180\,\text{deg/s}$ with light payloads, though performance degraded somewhat with heavier masses. +% The HAC-LAC control architecture proved particularly effective, with the decentralized IFF providing damping of nano-hexapod suspension modes while the high authority controller addressed low-frequency disturbances. + +% Vertical scanning capabilities were validated in both step-by-step and continuous motion modes. +% The system successfully resolved 10 nm steps with 50 ms detector integration time, while maintaining positioning accuracy during continuous scans at speeds up to $100\,\mu m/s$. + +% For lateral scanning, the system performed well at moderate speeds ($10\,\mu m/s$) but showed limitations at higher velocities ($100\,\mu m/s$) due to stepper motor-induced vibrations in the $T_y$ stage. + +% The most challenging test case - diffraction tomography combining rotation and lateral scanning - demonstrated the system's ability to maintain vertical and angular stability while highlighting some limitations in lateral positioning during rapid accelerations. +% These limitations could potentially be addressed through feedforward control or alternative detector triggering strategies. + +% Overall, the experimental results validate the effectiveness of the developed control architecture and demonstrate that the NASS meets most design specifications across a wide range of operating conditions (summarized in Table ref:tab:test_id31_experiments_results_summary). +% The identified limitations, primarily related to high-speed lateral scanning and heavy payload handling, provide clear directions for future improvements. + + +%% Summary of results +% 1e9*data_tomo_m0_Wz6.Dy_rms_ol, 1e9*data_tomo_m0_Wz6.Dz_rms_ol, 1e6*data_tomo_m0_Wz6.Ry_rms_ol; % Tomo - OL - 6deg/s - 0kg +% 1e9*data_tomo_m0_Wz6.Dy_rms_cl, 1e9*data_tomo_m0_Wz6.Dz_rms_cl, 1e6*data_tomo_m0_Wz6.Ry_rms_cl; % Tomo - CL - 6deg/s - 0kg +% 1e9*data_tomo_m1_Wz6.Dy_rms_ol, 1e9*data_tomo_m1_Wz6.Dz_rms_ol, 1e6*data_tomo_m1_Wz6.Ry_rms_ol; % Tomo - OL - 6deg/s - 13kg +% 1e9*data_tomo_m1_Wz6.Dy_rms_cl, 1e9*data_tomo_m1_Wz6.Dz_rms_cl, 1e6*data_tomo_m1_Wz6.Ry_rms_cl; % Tomo - CL - 6deg/s - 13kg +% 1e9*data_tomo_m2_Wz6.Dy_rms_ol, 1e9*data_tomo_m2_Wz6.Dz_rms_ol, 1e6*data_tomo_m2_Wz6.Ry_rms_ol; % Tomo - OL - 6deg/s - 26kg +% 1e9*data_tomo_m2_Wz6.Dy_rms_cl, 1e9*data_tomo_m2_Wz6.Dz_rms_cl, 1e6*data_tomo_m2_Wz6.Ry_rms_cl; % Tomo - CL - 6deg/s - 26kg +% 1e9*data_tomo_m3_Wz6.Dy_rms_ol, 1e9*data_tomo_m3_Wz6.Dz_rms_ol, 1e6*data_tomo_m3_Wz6.Ry_rms_ol; % Tomo - OL - 6deg/s - 39kg +% 1e9*data_tomo_m3_Wz6.Dy_rms_cl, 1e9*data_tomo_m3_Wz6.Dz_rms_cl, 1e6*data_tomo_m3_Wz6.Ry_rms_cl; % Tomo - CL - 6deg/s - 39kg +% 1e9*data_tomo_m0_Wz180.Dy_rms_ol, 1e9*data_tomo_m0_Wz180.Dz_rms_ol, 1e6*data_tomo_m0_Wz180.Ry_rms_ol; % Tomo - OL - 180deg/s - 0kg +% 1e9*data_tomo_m0_Wz180.Dy_rms_cl, 1e9*data_tomo_m0_Wz180.Dz_rms_cl, 1e6*data_tomo_m0_Wz180.Ry_rms_cl; % Tomo - CL - 180deg/s - 0kg +% 1e9*data_hac_Wz180.Dy_rms_cl, 1e9*data_hac_Wz180.Dz_rms_cl, 1e6*data_hac_Wz180.Ry_rms_cl; % Tomo - CL (high performance HAC) - 180deg/s - 0kg +% 1e9*data_ry.Dy_rms_cl, 1e9*data_ry.Dz_rms_cl, 1e6*data_ry.Ry_rms_cl; % Ry 100urad/s +% 1e9*data_dz_10ums.Dy_rms_cl, 1e9*data_dz_10ums.Dz_rms_cl, 1e6*data_dz_10ums.Ry_rms_cl; % Dz 10um/s +% 1e9*data_dz_100ums.Dy_rms_cl, 1e9*data_dz_100ums.Dz_rms_cl, 1e6*data_dz_100ums.Ry_rms_cl; % Dz 100um/s +% 1e9*data_ty_ol_10ums.Dy_rms, 1e9*data_ty_ol_10ums.Dz_rms, 1e6*data_ty_ol_10ums.Ry_rms; % Ty - OL - 10um/s +% 1e9*data_ty_cl_10ums.Dy_rms, 1e9*data_ty_cl_10ums.Dz_rms, 1e6*data_ty_cl_10ums.Ry_rms; % Ty - CL - 10um/s +% 1e9*data_ty_ol_100ums.Dy_rms, 1e9*data_ty_ol_100ums.Dz_rms, 1e6*data_ty_ol_100ums.Ry_rms; % Ty - OL - 100um/s +% 1e9*data_ty_cl_100ums.Dy_rms, 1e9*data_ty_cl_100ums.Dz_rms, 1e6*data_ty_cl_100ums.Ry_rms; % Ty - CL - 100um/s +% 1e9*data_dt_100ums.Dy_rms_cl, 1e9*data_dt_100ums.Dz_rms_cl, 1e6*data_dt_100ums.Ry_rms_cl; % Diffraction Tomo - CL - 6deg/s, 100um/s +% 1e9*data_dt_500ums.Dy_rms_cl, 1e9*data_dt_500ums.Dz_rms_cl, 1e6*data_dt_500ums.Ry_rms_cl; % Diffraction Tomo - CL - 6deg/s, 500um/s +% 1e9*data_dt_1000ums.Dy_rms_cl, 1e9*data_dt_1000ums.Dz_rms_cl, 1e6*data_dt_1000ums.Ry_rms_cl; % Diffraction Tomo - CL - 6deg/s, 1000um/s diff --git a/test-bench-id31.org b/test-bench-id31.org index 00e0c46..7999458 100644 --- a/test-bench-id31.org +++ b/test-bench-id31.org @@ -302,6 +302,7 @@ Simulation: 30rpm experiment with large off-axis errors (to see if it converges % This is done by offsetfing the micro-hexapod by 0.9um P_micro_hexapod = [10e-6; 0; 0]; % [m] +open(mdl) set_param(mdl, 'StopTime', '3'); initializeMicroHexapod('AP', P_micro_hexapod); @@ -2474,10 +2475,9 @@ Gm_hac_m2_Wz0 = feedback(Gm_m2_Wz0, Kiff, 'name', +1); Gm_hac_m3_Wz0 = feedback(Gm_m3_Wz0, Kiff, 'name', +1); % Check Stability -isstable(Gm_hac_m0_Wz0) -isstable(Gm_hac_m1_Wz0) -isstable(Gm_hac_m2_Wz0) -isstable(Gm_hac_m3_Wz0) +if not(isstable(Gm_hac_m0_Wz0) && isstable(Gm_hac_m1_Wz0) && isstable(Gm_hac_m2_Wz0) && isstable(Gm_hac_m3_Wz0)) + warning("One of the damped system with decentralized IFF is not stable"); +end #+end_src #+begin_src matlab :exports none :tangle no @@ -3094,10 +3094,10 @@ tiledlayout(3, 1, 'TileSpacing', 'Compact', 'Padding', 'None'); ax1 = nexttile([2,1]); hold on; -plot(f, abs(G_hac_m0_Wz0(:,1, 1).*squeeze(freqresp(Khac(1,1), f, 'Hz'))), 'color', colors(1,:), 'DisplayName', '$0$ kg'); -plot(f, abs(G_hac_m1_Wz0(:,1, 1).*squeeze(freqresp(Khac(1,1), f, 'Hz'))), 'color', colors(2,:), 'DisplayName', '$13$ kg'); -plot(f, abs(G_hac_m2_Wz0(:,1, 1).*squeeze(freqresp(Khac(1,1), f, 'Hz'))), 'color', colors(3,:), 'DisplayName', '$26$ kg'); -plot(f, abs(G_hac_m3_Wz0(:,1, 1).*squeeze(freqresp(Khac(1,1), f, 'Hz'))), 'color', colors(4,:), 'DisplayName', '$39$ kg'); +plot(f(2:end), abs(G_hac_m0_Wz0(:,1, 1).*squeeze(freqresp(Khac(1,1), f(2:end), 'Hz'))), 'color', colors(1,:), 'DisplayName', '$0$ kg'); +plot(f(2:end), abs(G_hac_m1_Wz0(:,1, 1).*squeeze(freqresp(Khac(1,1), f(2:end), 'Hz'))), 'color', colors(2,:), 'DisplayName', '$13$ kg'); +plot(f(2:end), abs(G_hac_m2_Wz0(:,1, 1).*squeeze(freqresp(Khac(1,1), f(2:end), 'Hz'))), 'color', colors(3,:), 'DisplayName', '$26$ kg'); +plot(f(2:end), abs(G_hac_m3_Wz0(:,1, 1).*squeeze(freqresp(Khac(1,1), f(2:end), 'Hz'))), 'color', colors(4,:), 'DisplayName', '$39$ kg'); xline(5, '--', 'linewidth', 1, 'color', [0,0,0,0.2], 'HandleVisibility', 'off') hold off; set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log'); @@ -3108,10 +3108,10 @@ leg.ItemTokenSize(1) = 15; ax2 = nexttile; hold on; -plot(f, 180/pi*angle(G_hac_m0_Wz0(:,1, 1).*squeeze(freqresp(Khac(1,1), f, 'Hz'))), 'color', colors(1,:)); -plot(f, 180/pi*angle(G_hac_m1_Wz0(:,1, 1).*squeeze(freqresp(Khac(1,1), f, 'Hz'))), 'color', colors(2,:)); -plot(f, 180/pi*angle(G_hac_m2_Wz0(:,1, 1).*squeeze(freqresp(Khac(1,1), f, 'Hz'))), 'color', colors(3,:)); -plot(f, 180/pi*angle(G_hac_m3_Wz0(:,1, 1).*squeeze(freqresp(Khac(1,1), f, 'Hz'))), 'color', colors(4,:)); +plot(f(2:end), 180/pi*angle(G_hac_m0_Wz0(:,1, 1).*squeeze(freqresp(Khac(1,1), f(2:end), 'Hz'))), 'color', colors(1,:)); +plot(f(2:end), 180/pi*angle(G_hac_m1_Wz0(:,1, 1).*squeeze(freqresp(Khac(1,1), f(2:end), 'Hz'))), 'color', colors(2,:)); +plot(f(2:end), 180/pi*angle(G_hac_m2_Wz0(:,1, 1).*squeeze(freqresp(Khac(1,1), f(2:end), 'Hz'))), 'color', colors(3,:)); +plot(f(2:end), 180/pi*angle(G_hac_m3_Wz0(:,1, 1).*squeeze(freqresp(Khac(1,1), f(2:end), 'Hz'))), 'color', colors(4,:)); xline(5, '--', 'linewidth', 1, 'color', [0,0,0,0.2]) hold off; set(gca, 'XScale', 'log'); set(gca, 'YScale', 'lin'); @@ -3229,6 +3229,7 @@ The obtained closed-loop positioning accuracy was found to comply with the requi % This is done by offsetfing the micro-hexapod by 0.9um P_micro_hexapod = [2.5e-6; 0; -0.3e-6]; % [m] +open(mdl); set_param(mdl, 'StopTime', '3'); % 6 turns at 180deg/s (30rpm) initializeGround(); @@ -3380,6 +3381,7 @@ Yet it was decided that this controller will be tested experimentally, and impro #+begin_src matlab %% Simulation of tomography experiments at 1RPM with all payloads % Configuration +open(mdl); set_param(mdl, 'StopTime', '2'); % 30 degrees at 1rpm initializeLoggingConfiguration('log', 'all', 'Ts', 1e-3); initializeController('type', 'hac-iff'); @@ -4343,12 +4345,6 @@ data_dz_100ums.time = Ts*[0:length(data_dz_100ums.Dz_int)-1]; %% Performances for Dz scans - 10 um/s % Determine when the motion starts and stops i_dz_10ums = abs(diff(data_dz_10ums.m_hexa_dz)/Ts-10e-6) < 10*eps; -% i_dz_10ums = data_dz_10ums.m_hexa_dz>data_dz_10ums.m_hexa_dz(1) & data_dz_10ums.m_hexa_dzdata_dz_100ums.m_hexa_dz(1) & data_dz_100ums.m_hexa_dz data_ty_cl_10ums.Ty(1) & data_ty_cl_10ums. i_ty_ol_100ums = data_ty_ol_100ums.Ty > data_ty_ol_100ums.Ty(1) & data_ty_ol_100ums.Ty < data_ty_ol_100ums.Ty(end); i_ty_cl_100ums = data_ty_cl_100ums.Ty > data_ty_cl_100ums.Ty(1) & data_ty_cl_100ums.Ty < data_ty_cl_100ums.Ty(end); -% Peak to Peak errors -ty_ol_10ums_dy_peak = (max(detrend(data_ty_ol_10ums.e_dy(i_ty_ol_10ums), 0))-min(detrend(data_ty_ol_10ums.e_dy(i_ty_ol_10ums), 0)))/2; -ty_ol_10ums_dz_peak = (max(detrend(data_ty_ol_10ums.e_dz(i_ty_ol_10ums), 0))-min(detrend(data_ty_ol_10ums.e_dz(i_ty_ol_10ums), 0)))/2; -ty_ol_10ums_ry_peak = (max(detrend(data_ty_ol_10ums.e_ry(i_ty_ol_10ums), 0))-min(detrend(data_ty_ol_10ums.e_ry(i_ty_ol_10ums), 0)))/2; - -ty_cl_10ums_dy_peak = (max(detrend(data_ty_cl_10ums.e_dy(i_ty_cl_10ums), 0))-min(detrend(data_ty_cl_10ums.e_dy(i_ty_cl_10ums), 0)))/2; -ty_cl_10ums_dz_peak = (max(detrend(data_ty_cl_10ums.e_dz(i_ty_cl_10ums), 0))-min(detrend(data_ty_cl_10ums.e_dz(i_ty_cl_10ums), 0)))/2; -ty_cl_10ums_ry_peak = (max(detrend(data_ty_cl_10ums.e_ry(i_ty_cl_10ums), 0))-min(detrend(data_ty_cl_10ums.e_ry(i_ty_cl_10ums), 0)))/2; - -ty_ol_100ums_dy_peak = (max(detrend(data_ty_ol_100ums.e_dy(i_ty_ol_100ums), 0))-min(detrend(data_ty_ol_100ums.e_dy(i_ty_ol_100ums), 0)))/2; -ty_ol_100ums_dz_peak = (max(detrend(data_ty_ol_100ums.e_dz(i_ty_ol_100ums), 0))-min(detrend(data_ty_ol_100ums.e_dz(i_ty_ol_100ums), 0)))/2; -ty_ol_100ums_ry_peak = (max(detrend(data_ty_ol_100ums.e_ry(i_ty_ol_100ums), 0))-min(detrend(data_ty_ol_100ums.e_ry(i_ty_ol_100ums), 0)))/2; - -ty_cl_100ums_dy_peak = (max(detrend(data_ty_cl_100ums.e_dy(i_ty_cl_100ums), 0))-min(detrend(data_ty_cl_100ums.e_dy(i_ty_cl_100ums), 0)))/2; -ty_cl_100ums_dz_peak = (max(detrend(data_ty_cl_100ums.e_dz(i_ty_cl_100ums), 0))-min(detrend(data_ty_cl_100ums.e_dz(i_ty_cl_100ums), 0)))/2; -ty_cl_100ums_ry_peak = (max(detrend(data_ty_cl_100ums.e_ry(i_ty_cl_100ums), 0))-min(detrend(data_ty_cl_100ums.e_ry(i_ty_cl_100ums), 0)))/2; - % RMS error data_ty_ol_10ums.Dy_rms = rms(detrend(data_ty_ol_10ums.e_dy(i_ty_ol_10ums), 0)); data_ty_ol_10ums.Dz_rms = rms(detrend(data_ty_ol_10ums.e_dz(i_ty_ol_10ums), 0)); @@ -5038,19 +5011,6 @@ for i = i_dec(2:2:end) i_dt_1000ums(i-acc_n:i) = 0; end -% Peak to Peak errors -dt_100ums_dy_peak = (max(detrend(data_dt_100ums.Dy_int(i_dt_100ums)-data_dt_100ums.m_hexa_dy(i_dt_100ums), 0))-min(detrend(data_dt_100ums.Dy_int(i_dt_100ums)-data_dt_100ums.m_hexa_dy(i_dt_100ums), 0)))/2; -dt_100ums_dz_peak = (max(detrend(data_dt_100ums.Dz_int(i_dt_100ums), 0))-min(detrend(data_dt_100ums.Dz_int(i_dt_100ums), 0)))/2; -dt_100ums_ry_peak = (max(detrend(data_dt_100ums.Ry_int(i_dt_100ums), 0))-min(detrend(data_dt_100ums.Ry_int(i_dt_100ums), 0)))/2; - -dt_500ums_dy_peak = (max(detrend(data_dt_500ums.Dy_int(i_dt_500ums)-data_dt_500ums.m_hexa_dy(i_dt_500ums), 0))-min(detrend(data_dt_500ums.Dy_int(i_dt_500ums)-data_dt_500ums.m_hexa_dy(i_dt_500ums), 0)))/2; -dt_500ums_dz_peak = (max(detrend(data_dt_500ums.Dz_int(i_dt_500ums), 0))-min(detrend(data_dt_500ums.Dz_int(i_dt_500ums), 0)))/2; -dt_500ums_ry_peak = (max(detrend(data_dt_500ums.Ry_int(i_dt_500ums), 0))-min(detrend(data_dt_500ums.Ry_int(i_dt_500ums), 0)))/2; - -dt_1000ums_dy_peak = (max(detrend(data_dt_1000ums.Dy_int(i_dt_1000ums)-data_dt_1000ums.m_hexa_dy(i_dt_1000ums), 0))-min(detrend(data_dt_1000ums.Dy_int(i_dt_1000ums)-data_dt_1000ums.m_hexa_dy(i_dt_1000ums), 0)))/2; -dt_1000ums_dz_peak = (max(detrend(data_dt_1000ums.Dz_int(i_dt_1000ums), 0))-min(detrend(data_dt_1000ums.Dz_int(i_dt_1000ums), 0)))/2; -dt_1000ums_ry_peak = (max(detrend(data_dt_1000ums.Ry_int(i_dt_1000ums), 0))-min(detrend(data_dt_1000ums.Ry_int(i_dt_1000ums), 0)))/2; - % RMS error data_dt_100ums.Dy_rms_cl = rms(detrend(data_dt_100ums.Dy_int(i_dt_100ums)-data_dt_100ums.m_hexa_dy(i_dt_100ums), 0)); data_dt_100ums.Dz_rms_cl = rms(detrend(data_dt_100ums.Dz_int(i_dt_100ums), 0)); @@ -5087,27 +5047,27 @@ The identified limitations, primarily related to high-speed lateral scanning and #+begin_src matlab %% Summary of results -1e9*data_tomo_m0_Wz6.Dy_rms_ol, 1e9*data_tomo_m0_Wz6.Dz_rms_ol, 1e6*data_tomo_m0_Wz6.Ry_rms_ol % Tomo - OL - 6deg/s - 0kg -1e9*data_tomo_m0_Wz6.Dy_rms_cl, 1e9*data_tomo_m0_Wz6.Dz_rms_cl, 1e6*data_tomo_m0_Wz6.Ry_rms_cl % Tomo - CL - 6deg/s - 0kg -1e9*data_tomo_m1_Wz6.Dy_rms_ol, 1e9*data_tomo_m1_Wz6.Dz_rms_ol, 1e6*data_tomo_m1_Wz6.Ry_rms_ol % Tomo - OL - 6deg/s - 13kg -1e9*data_tomo_m1_Wz6.Dy_rms_cl, 1e9*data_tomo_m1_Wz6.Dz_rms_cl, 1e6*data_tomo_m1_Wz6.Ry_rms_cl % Tomo - CL - 6deg/s - 13kg -1e9*data_tomo_m2_Wz6.Dy_rms_ol, 1e9*data_tomo_m2_Wz6.Dz_rms_ol, 1e6*data_tomo_m2_Wz6.Ry_rms_ol % Tomo - OL - 6deg/s - 26kg -1e9*data_tomo_m2_Wz6.Dy_rms_cl, 1e9*data_tomo_m2_Wz6.Dz_rms_cl, 1e6*data_tomo_m2_Wz6.Ry_rms_cl % Tomo - CL - 6deg/s - 26kg -1e9*data_tomo_m3_Wz6.Dy_rms_ol, 1e9*data_tomo_m3_Wz6.Dz_rms_ol, 1e6*data_tomo_m3_Wz6.Ry_rms_ol % Tomo - OL - 6deg/s - 39kg -1e9*data_tomo_m3_Wz6.Dy_rms_cl, 1e9*data_tomo_m3_Wz6.Dz_rms_cl, 1e6*data_tomo_m3_Wz6.Ry_rms_cl % Tomo - CL - 6deg/s - 39kg -1e9*data_tomo_m0_Wz180.Dy_rms_ol, 1e9*data_tomo_m0_Wz180.Dz_rms_ol, 1e6*data_tomo_m0_Wz180.Ry_rms_ol % Tomo - OL - 180deg/s - 0kg -1e9*data_tomo_m0_Wz180.Dy_rms_cl, 1e9*data_tomo_m0_Wz180.Dz_rms_cl, 1e6*data_tomo_m0_Wz180.Ry_rms_cl % Tomo - CL - 180deg/s - 0kg -1e9*data_hac_Wz180.Dy_rms_cl, 1e9*data_hac_Wz180.Dz_rms_cl, 1e6*data_hac_Wz180.Ry_rms_cl % Tomo - CL (high performance HAC) - 180deg/s - 0kg -1e9*data_ry.Dy_rms_cl, 1e9*data_ry.Dz_rms_cl, 1e6*data_ry.Ry_rms_cl % Ry 100urad/s -1e9*data_dz_10ums.Dy_rms_cl, 1e9*data_dz_10ums.Dz_rms_cl, 1e6*data_dz_10ums.Ry_rms_cl % Dz 10um/s -1e9*data_dz_100ums.Dy_rms_cl, 1e9*data_dz_100ums.Dz_rms_cl, 1e6*data_dz_100ums.Ry_rms_cl % Dz 100um/s -1e9*data_ty_ol_10ums.Dy_rms, 1e9*data_ty_ol_10ums.Dz_rms, 1e6*data_ty_ol_10ums.Ry_rms % Ty - OL - 10um/s -1e9*data_ty_cl_10ums.Dy_rms, 1e9*data_ty_cl_10ums.Dz_rms, 1e6*data_ty_cl_10ums.Ry_rms % Ty - CL - 10um/s -1e9*data_ty_ol_100ums.Dy_rms, 1e9*data_ty_ol_100ums.Dz_rms, 1e6*data_ty_ol_100ums.Ry_rms % Ty - OL - 100um/s -1e9*data_ty_cl_100ums.Dy_rms, 1e9*data_ty_cl_100ums.Dz_rms, 1e6*data_ty_cl_100ums.Ry_rms % Ty - CL - 100um/s -1e9*data_dt_100ums.Dy_rms_cl, 1e9*data_dt_100ums.Dz_rms_cl, 1e6*data_dt_100ums.Ry_rms_cl % Diffraction Tomo - CL - 6deg/s, 100um/s -1e9*data_dt_500ums.Dy_rms_cl, 1e9*data_dt_500ums.Dz_rms_cl, 1e6*data_dt_500ums.Ry_rms_cl % Diffraction Tomo - CL - 6deg/s, 500um/s -1e9*data_dt_1000ums.Dy_rms_cl, 1e9*data_dt_1000ums.Dz_rms_cl, 1e6*data_dt_1000ums.Ry_rms_cl % Diffraction Tomo - CL - 6deg/s, 1000um/s +% 1e9*data_tomo_m0_Wz6.Dy_rms_ol, 1e9*data_tomo_m0_Wz6.Dz_rms_ol, 1e6*data_tomo_m0_Wz6.Ry_rms_ol; % Tomo - OL - 6deg/s - 0kg +% 1e9*data_tomo_m0_Wz6.Dy_rms_cl, 1e9*data_tomo_m0_Wz6.Dz_rms_cl, 1e6*data_tomo_m0_Wz6.Ry_rms_cl; % Tomo - CL - 6deg/s - 0kg +% 1e9*data_tomo_m1_Wz6.Dy_rms_ol, 1e9*data_tomo_m1_Wz6.Dz_rms_ol, 1e6*data_tomo_m1_Wz6.Ry_rms_ol; % Tomo - OL - 6deg/s - 13kg +% 1e9*data_tomo_m1_Wz6.Dy_rms_cl, 1e9*data_tomo_m1_Wz6.Dz_rms_cl, 1e6*data_tomo_m1_Wz6.Ry_rms_cl; % Tomo - CL - 6deg/s - 13kg +% 1e9*data_tomo_m2_Wz6.Dy_rms_ol, 1e9*data_tomo_m2_Wz6.Dz_rms_ol, 1e6*data_tomo_m2_Wz6.Ry_rms_ol; % Tomo - OL - 6deg/s - 26kg +% 1e9*data_tomo_m2_Wz6.Dy_rms_cl, 1e9*data_tomo_m2_Wz6.Dz_rms_cl, 1e6*data_tomo_m2_Wz6.Ry_rms_cl; % Tomo - CL - 6deg/s - 26kg +% 1e9*data_tomo_m3_Wz6.Dy_rms_ol, 1e9*data_tomo_m3_Wz6.Dz_rms_ol, 1e6*data_tomo_m3_Wz6.Ry_rms_ol; % Tomo - OL - 6deg/s - 39kg +% 1e9*data_tomo_m3_Wz6.Dy_rms_cl, 1e9*data_tomo_m3_Wz6.Dz_rms_cl, 1e6*data_tomo_m3_Wz6.Ry_rms_cl; % Tomo - CL - 6deg/s - 39kg +% 1e9*data_tomo_m0_Wz180.Dy_rms_ol, 1e9*data_tomo_m0_Wz180.Dz_rms_ol, 1e6*data_tomo_m0_Wz180.Ry_rms_ol; % Tomo - OL - 180deg/s - 0kg +% 1e9*data_tomo_m0_Wz180.Dy_rms_cl, 1e9*data_tomo_m0_Wz180.Dz_rms_cl, 1e6*data_tomo_m0_Wz180.Ry_rms_cl; % Tomo - CL - 180deg/s - 0kg +% 1e9*data_hac_Wz180.Dy_rms_cl, 1e9*data_hac_Wz180.Dz_rms_cl, 1e6*data_hac_Wz180.Ry_rms_cl; % Tomo - CL (high performance HAC) - 180deg/s - 0kg +% 1e9*data_ry.Dy_rms_cl, 1e9*data_ry.Dz_rms_cl, 1e6*data_ry.Ry_rms_cl; % Ry 100urad/s +% 1e9*data_dz_10ums.Dy_rms_cl, 1e9*data_dz_10ums.Dz_rms_cl, 1e6*data_dz_10ums.Ry_rms_cl; % Dz 10um/s +% 1e9*data_dz_100ums.Dy_rms_cl, 1e9*data_dz_100ums.Dz_rms_cl, 1e6*data_dz_100ums.Ry_rms_cl; % Dz 100um/s +% 1e9*data_ty_ol_10ums.Dy_rms, 1e9*data_ty_ol_10ums.Dz_rms, 1e6*data_ty_ol_10ums.Ry_rms; % Ty - OL - 10um/s +% 1e9*data_ty_cl_10ums.Dy_rms, 1e9*data_ty_cl_10ums.Dz_rms, 1e6*data_ty_cl_10ums.Ry_rms; % Ty - CL - 10um/s +% 1e9*data_ty_ol_100ums.Dy_rms, 1e9*data_ty_ol_100ums.Dz_rms, 1e6*data_ty_ol_100ums.Ry_rms; % Ty - OL - 100um/s +% 1e9*data_ty_cl_100ums.Dy_rms, 1e9*data_ty_cl_100ums.Dz_rms, 1e6*data_ty_cl_100ums.Ry_rms; % Ty - CL - 100um/s +% 1e9*data_dt_100ums.Dy_rms_cl, 1e9*data_dt_100ums.Dz_rms_cl, 1e6*data_dt_100ums.Ry_rms_cl; % Diffraction Tomo - CL - 6deg/s, 100um/s +% 1e9*data_dt_500ums.Dy_rms_cl, 1e9*data_dt_500ums.Dz_rms_cl, 1e6*data_dt_500ums.Ry_rms_cl; % Diffraction Tomo - CL - 6deg/s, 500um/s +% 1e9*data_dt_1000ums.Dy_rms_cl, 1e9*data_dt_1000ums.Dz_rms_cl, 1e6*data_dt_1000ums.Ry_rms_cl; % Diffraction Tomo - CL - 6deg/s, 1000um/s #+end_src #+name: tab:test_id31_experiments_results_summary @@ -5190,7 +5150,7 @@ addpath('./STEPS/'); % Path for STEPS addpath('./subsystems/'); % Path for Subsystems Simulink files %% Data directory -data_dir = './mat/' +data_dir = './mat/'; #+END_SRC ** Initialize Simscape Model