#+TITLE: Nano-Hexapod on top of a Spindle - Test Bench
:DRAWER:
#+LANGUAGE: en
#+EMAIL: dehaeze.thomas@gmail.com
#+AUTHOR: Dehaeze Thomas

#+HTML_LINK_HOME: ../index.html
#+HTML_LINK_UP:   ../index.html

#+HTML_HEAD: <link rel="stylesheet" type="text/css" href="https://research.tdehaeze.xyz/css/style.css"/>
#+HTML_HEAD: <script type="text/javascript" src="https://research.tdehaeze.xyz/js/script.js"></script>

#+BIND: org-latex-image-default-option "scale=1"
#+BIND: org-latex-image-default-width ""

#+LaTeX_CLASS: scrreprt
#+LaTeX_CLASS_OPTIONS: [a4paper, 10pt, DIV=12, parskip=full, bibliography=totoc]
#+LATEX_HEADER: \input{preamble.tex}
#+LATEX_HEADER_EXTRA: \input{preamble_extra.tex}
#+LATEX_HEADER_EXTRA: \bibliography{test-bench-nass-spindle.bib}

#+BIND: org-latex-bib-compiler "biber"

#+PROPERTY: header-args:matlab  :session *MATLAB*
#+PROPERTY: header-args:matlab+ :comments org
#+PROPERTY: header-args:matlab+ :exports none
#+PROPERTY: header-args:matlab+ :results none
#+PROPERTY: header-args:matlab+ :eval no-export
#+PROPERTY: header-args:matlab+ :noweb yes
#+PROPERTY: header-args:matlab+ :mkdirp yes
#+PROPERTY: header-args:matlab+ :output-dir figs
#+PROPERTY: header-args:matlab+ :tangle no

#+PROPERTY: header-args:latex  :headers '("\\usepackage{tikz}" "\\usepackage{import}" "\\import{$HOME/Cloud/tikz/org/}{config.tex}")
#+PROPERTY: header-args:latex+ :imagemagick t :fit yes
#+PROPERTY: header-args:latex+ :iminoptions -scale 100% -density 150
#+PROPERTY: header-args:latex+ :imoutoptions -quality 100
#+PROPERTY: header-args:latex+ :results file raw replace
#+PROPERTY: header-args:latex+ :buffer no
#+PROPERTY: header-args:latex+ :tangle no
#+PROPERTY: header-args:latex+ :eval no-export
#+PROPERTY: header-args:latex+ :exports results
#+PROPERTY: header-args:latex+ :mkdirp yes
#+PROPERTY: header-args:latex+ :output-dir figs
#+PROPERTY: header-args:latex+ :post pdf2svg(file=*this*, ext="png")
:END:

#+latex: \clearpage

* Build                                                             :noexport:
#+NAME: startblock
#+BEGIN_SRC emacs-lisp :results none :tangle no
(add-to-list 'org-latex-classes
             '("scrreprt"
               "\\documentclass{scrreprt}"
               ("\\chapter{%s}" . "\\chapter*{%s}")
               ("\\section{%s}" . "\\section*{%s}")
               ("\\subsection{%s}" . "\\subsection*{%s}")
               ("\\paragraph{%s}" . "\\paragraph*{%s}")
               ))


;; Remove automatic org heading labels
(defun my-latex-filter-removeOrgAutoLabels (text backend info)
  "Org-mode automatically generates labels for headings despite explicit use of `#+LABEL`. This filter forcibly removes all automatically generated org-labels in headings."
  (when (org-export-derived-backend-p backend 'latex)
    (replace-regexp-in-string "\\\\label{sec:org[a-f0-9]+}\n" "" text)))
(add-to-list 'org-export-filter-headline-functions
             'my-latex-filter-removeOrgAutoLabels)

;; Remove all org comments in the output LaTeX file
(defun delete-org-comments (backend)
  (loop for comment in (reverse (org-element-map (org-element-parse-buffer)
                                    'comment 'identity))
        do
        (setf (buffer-substring (org-element-property :begin comment)
                                (org-element-property :end comment))
              "")))
(add-hook 'org-export-before-processing-hook 'delete-org-comments)

;; Use no package by default
(setq org-latex-packages-alist nil)
(setq org-latex-default-packages-alist nil)

;; Do not include the subtitle inside the title
(setq org-latex-subtitle-separate t)
(setq org-latex-subtitle-format "\\subtitle{%s}")

(setq org-export-before-parsing-hook '(org-ref-glossary-before-parsing
                                       org-ref-acronyms-before-parsing))
#+END_SRC

* Notes                                                             :noexport:
** Notes
Prefix is =test_rot=

*Goals*:
- Short stroke metrology with Interferometers to have more stroke
- Validation of IFF
- Validation of control architecture with rotation (kinematics)
  - How to estimate Rz? (useful for kinematics): what happens if Rz is ignored (i.e. supposed to be zero)?
- Effect of rotation on the plant dynamics

*Notes*:
- Robustness will be checked on ID31
- Tests were performed in December 2022 and January/February 2023
- All that can be explained here before the ID31 tests is nice
- 20 pages maximum should be good enough
- Should I speak here about the Simscape model? (Maybe not useful)

- [ ] Explain that beam time is complex to have, so first tests are made in the lab.
- [ ] talk about the metrology system.
  Comparison with LION precision system.
- [ ] Find pictures with additional mass with the metrology? (I don't find them)

** Alignment

Optimal distance for top Attocube: 179.67 (distance between reference surface and bottom surface of LION sphere)

Nouvelle mesure après alignement

aligné à mieux que 0.1mm en XYZ:
- XY tête du haut / axe LION
- XZ tête face / axe lion
- YZ tête droite / axe lion

* Introduction                                                        :ignore:

As the different beamlines are running 24/7
It is difficult to have access to the micro station to perform tests.
The only slot available is 3 weeks during the summer.

Before the tests on ID31:
- Development of a 5DoF metrology system
- Make sure all the kinematic is working properly


#+name: fig:picture_setup
#+caption: Setup with the Spindle, nano-hexapod and metrology
#+attr_latex: :width \linewidth
[[file:figs/IMG_20221220_152429.jpg]]

* Test-Bench Description
** Introduction                                                      :ignore:
#+begin_note
Here are the documentation of the equipment used for this test bench:
- Voltage Amplifier: PiezoDrive [[file:doc/PD200-V7-R1.pdf][PD200]]
- Amplified Piezoelectric Actuator: Cedrat [[file:doc/APA300ML.pdf][APA300ML]]
- DAC/ADC: Speedgoat [[file:doc/IO131-OEM-Datasheet.pdf][IO131]]
- Encoder: Renishaw [[file:doc/L-9517-9678-05-A_Data_sheet_VIONiC_series_en.pdf][Vionic]] and used [[file:doc/L-9517-9862-01-C_Data_sheet_RKLC_EN.pdf][Ruler]]
- LION Precision [[file:doc/Catalog-CPL190290.pdf][CPL290]]
- Spindle: Lab Motion [[file:doc/RS250S.pdf][RT250S]] with [[file:doc/DB36_connections_english_V2.pdf][Drivebox 3.6]] controller
#+end_note

** Matlab Init                                            :noexport:ignore:
#+begin_src matlab :tangle no :exports none :results silent :noweb yes :var current_dir=(file-name-directory buffer-file-name)
<<matlab-dir>>
#+end_src

#+begin_src matlab :exports none :results silent :noweb yes
<<matlab-init>>
#+end_src

#+begin_src matlab :tangle no :noweb yes
<<m-init-path>>
#+end_src

#+begin_src matlab :eval no :noweb yes
<<m-init-path-tangle>>
#+end_src

#+begin_src matlab :noweb yes
<<m-init-other>>
#+end_src

** Alignment

Procedure:
1. Align bottom sphere with the spindle rotation axis (~ 10um)
2. Align top sphere with the spindle rotation axis (~ 10um)

** Short Range metrology system

There are 5 interferometers pointing at 2 spheres as shown in Figure ref:fig:LION_metrology_interferometers.

#+name: fig:picture_metrology
#+caption: Metrology system with LION sphere (1 inch diameter) and 5 interferometers fixed to their individual tip-tilts
#+attr_latex: :width 0.5\linewidth
[[file:figs/IMG_20221216_181305.jpg]]

|                              | Value  |
|------------------------------+--------|
| Sphere Diameter              | 25.4mm |
| Distance between the spheres | 76.2mm |

#+name: fig:LION_metrology_interferometers
#+caption: Schematic of the measurement system
#+attr_latex: :width 0.5\linewidth
[[file:figs/LION_metrology_interferometers.png]]

*Assumptions*:
- Interferometers are perfectly positioned / oriented
- Sphere is perfect

*Compute the Jacobian matrix*:
- From pure X-Y-Z-Rx-Ry small motions, compute the effect on the 5 measured distances
- Compute the matrix
- Inverse the matrix
- Verify that it is working with simple example (for example using Solidworkds)

We have the following set of equations:
\begin{align}
d_1 &= -D_y + l_2 R_x \\
d_2 &= -D_y - l_1 R_x \\
d_3 &= -D_x - l_2 R_y \\
d_4 &= -D_x + l_1 R_y \\
d_5 &= -D_z
\end{align}

That can be written as a linear transformation:
\begin{equation}
\begin{bmatrix}
  d_1 \\ d_2 \\ d_3 \\ d_4 \\ d_5
\end{bmatrix} = \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} \cdot \begin{bmatrix}
  D_x \\ D_y \\ D_z \\ R_x \\ R_y
\end{bmatrix}
\end{equation}

By inverting the matrix, we obtain the Jacobian relation:
\begin{equation}
\begin{bmatrix}
  D_x \\ D_y \\ D_z \\ R_x \\ R_y
\end{bmatrix} = \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}^{-1} \cdot \begin{bmatrix}
  d_1 \\ d_2 \\ d_3 \\ d_4 \\ d_5
\end{bmatrix}
\end{equation}

#+begin_src matlab
%% Parameters
H = 150e-3;
l1 = (150-38-52)*1e-3;
l2 = (52+38+76.2-150)*1e-3;
#+end_src

#+begin_src matlab
%% 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];
#+end_src

#+begin_src matlab :exports results :results value table replace :tangle no :post addhdr(*this*)
data2orgtable(pinv(Hm), {'*Dx*', '*Dy*', '*Dz*', '*Rx*', '*Ry*'}, {'*d1*', '*d2*', '*d3*', '*d4*', '*d5*'}, ' %.2f ');
#+end_src

#+name: tab:jacobian_metrology
#+caption: Jacobian matrix for the metrology system
#+attr_latex: :environment tabularx :width \linewidth :align cXXXXX
#+attr_latex: :center t :booktabs t
#+RESULTS:
|      |  *d1* |   *d2* |   *d3* |  *d4* | *d5* |
|------+-------+--------+--------+-------+------|
| *Dx* |   0.0 |    0.0 |  -0.79 | -0.21 |  0.0 |
| *Dy* | -0.79 |  -0.21 |   -0.0 |  -0.0 |  0.0 |
| *Dz* |   0.0 |    0.0 |    0.0 |   0.0 | -1.0 |
| *Rx* | 13.12 | -13.12 |    0.0 |  -0.0 |  0.0 |
| *Ry* |   0.0 |    0.0 | -13.12 | 13.12 |  0.0 |

** Spindle errors
*** Introduction                                                    :ignore:

The spindle is rotated at 60rpm during 10 turns.
The signal of all 5 interferometers are recorded.

#+begin_src matlab
data = load(sprintf('%s/spindle/mat/2022-12-20_15-47_sec_test.mat', data_dir));
#+end_src

*** Errors in $D_x$ and $D_y$

Because of the eccentricity of the reference surfaces (the spheres), we expect the motion in the X-Y plane to be a circle as a first approximation.
We can first see that in Figure ref:fig:dx_dy_motion_rotation that shows the measured $D_x$ and $D_y$ motion as a function of the $R_z$ angle.

#+begin_src matlab :exports none :results none
%% Dz motion during the rotation
figure;
hold on;
plot(1/2/pi*unwrap(data.Rz - data.Rz(1)), 1e6*detrend(data.Dx_int, 0), 'DisplayName', '$D_x$')
plot(1/2/pi*unwrap(data.Rz - data.Rz(1)), 1e6*detrend(data.Dy_int, 0), 'DisplayName', '$D_y$')
hold off;
xlim([0, 10])
xlabel('Rotation [turn]'); ylabel('$D_z$ motion [$\mu$m]');
legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1);
#+end_src

#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/dx_dy_motion_rotation.pdf', 'width', 'wide', 'height', 'normal');
#+end_src

#+name: fig:dx_dy_motion_rotation
#+caption: Dx and Dy motion during the rotation
#+RESULTS:
[[file:figs/dx_dy_motion_rotation.png]]

#+begin_src matlab
%% Circle Fit
[xc,yc,R,a] = circlefit(data.Dx_int, data.Dy_int);
#+end_src

A circle is fit, and the obtained radius of the circle (i.e. the excentricity) is estimated to be:

#+begin_src matlab :results value replace :exports results :tangle no
sprintf('Error linked to excentricity = %.0f um', 1e6*R);
#+end_src

#+RESULTS:
: Error linked to excentricity = 19 um

The motion in the X-Y plane as well as the circle fit and the residual motion (circle fit subtracted from the measured motion) are shown in Figure ref:fig:dx_dy_spindle_rotation.

#+begin_src matlab :exports none :results none
%% Dx and Dy motion during the spindle rotation
theta = linspace(0, 2*pi, 1000);
figure;
tiledlayout(1, 2, 'TileSpacing', 'Compact', 'Padding', 'None');

ax1 = nexttile();
hold on;
plot(1e6*detrend(data.Dx_int, 0), 1e6*detrend(data.Dy_int, 0), 'DisplayName', 'Raw data')
plot(1e6*detrend(xc + R*cos(theta), 0), 1e6*detrend(yc + R*sin(theta), 0), '--', 'DisplayName', 'fit')
plot(1e6*(data.Dx_int - (xc + R*cos(data.Rz-0.07))), 1e6*(data.Dy_int - (yc + R*sin(data.Rz-0.07))), 'k.', 'DisplayName', 'Residual')
hold off;
xlabel('$D_x$ [$\mu$m]')
ylabel('$D_y$ [$\mu$m]')
axis equal
legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1);

ax2 = nexttile();
plot(1e6*(data.Dx_int - (xc + R*cos(data.Rz-0.07))), 1e6*(data.Dy_int - (yc + R*sin(data.Rz-0.07))), 'k.')
xlabel('$D_x$ [$\mu$m]')
ylabel('$D_y$ [$\mu$m]')
axis equal
#+end_src

#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/dx_dy_spindle_rotation.pdf', 'width', 'full', 'height', 'tall');
#+end_src

#+name: fig:dx_dy_spindle_rotation
#+caption: Dx and Dy motion during the spindle rotation
#+RESULTS:
[[file:figs/dx_dy_spindle_rotation.png]]

Let's now analyse the frequency content in the signal.

#+begin_src matlab :exports none
%% Hanning window used for pwelch function
Ts = 1e-4;
win = hanning(5/Ts);

%% Compute PSD of initial motion error
[S_Dx, f] = pwelch(detrend(data.Dx_int, 0), win, 0, [], 1/Ts);
[S_Dy, ~] = pwelch(detrend(data.Dy_int, 0), win, 0, [], 1/Ts);
#+end_src

#+begin_src matlab :exports none :results none
%% Amplitude Spectral Density of the measured Dx and Dy motion
figure;
hold on;
plot(f, sqrt(S_Dx), 'DisplayName', '$D_x$');
plot(f, sqrt(S_Dy), 'DisplayName', '$D_y$');
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log');
xlabel('Frequency [Hz]'); ylabel('ASD [$m/\sqrt{Hz}$]');
legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1);
xlim([0.1, 1e3]); ylim([1e-11, 1e-4]);
#+end_src

#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/dx_dy_spindle_rotation_asd.pdf', 'width', 'wide', 'height', 'normal');
#+end_src

#+name: fig:dx_dy_spindle_rotation_asd
#+caption: Amplitude Spectral Density of the measured Dx and Dy motion
#+RESULTS:
[[file:figs/dx_dy_spindle_rotation_asd.png]]

#+begin_src matlab :exports none :results none
%% Cumulative Amplitude Spectrum of the measured Dx and Dy motion
figure;
hold on;
plot(f, sqrt(flip(-cumtrapz(flip(f), flip(S_Dx)))), 'DisplayName', '$D_x$');
plot(f, sqrt(flip(-cumtrapz(flip(f), flip(S_Dy)))), 'DisplayName', '$D_y$');
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log');
xlabel('Frequency [Hz]'); ylabel('CAS [$m$]');
legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1);
xlim([0.1, 1e3]); ylim([1e-9, 2e-5]);
#+end_src

#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/dx_dy_spindle_rotation_cas.pdf', 'width', 'wide', 'height', 'normal');
#+end_src

#+name: fig:dx_dy_spindle_rotation_cas
#+caption: Cumulative Amplitude Spectrum of the measured Dx and Dy motion
#+RESULTS:
[[file:figs/dx_dy_spindle_rotation_cas.png]]

*** Errors in vertical motion $D_z$

The top interferometer is measuring the vertical motion of the sphere.

However, if the top sphere is not perfectly aligned with the spindle axis, there will also measure some vertical motion due to this excentricity.

#+begin_src matlab :exports none :results none
%% Dz motion during the rotation
figure;
tiledlayout(1, 2, 'TileSpacing', 'Compact', 'Padding', 'None');
ax1 = nexttile();
plot(1/2/pi*unwrap(data.Rz - data.Rz(1)), 1e6*detrend(data.Dz_int, 0), '-')
xlim([0, 10])
xlabel('Rotation [turn]'); ylabel('$D_z$ motion [$\mu$m]');

ax1 = nexttile();
plot(180/pi*unwrap(data.Rz - data.Rz(1)), 1e6*detrend(data.Dz_int, 0), '-')
xlim([0, 90])
xlabel('Rotation [deg]'); ylabel('$D_z$ motion [$\mu$m]');
#+end_src

#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/dz_motion_rotation.pdf', 'width', 'full', 'height', 'tall');
#+end_src

#+name: fig:dz_motion_rotation
#+caption: Dz motion during the rotation
#+RESULTS:
[[file:figs/dz_motion_rotation.png]]

Let's fit a sinus with a period of one turn.

#+begin_src matlab
%% Fit Sinus with period of one turn
x1 = data.Rz(data.t<1);
y1 = data.Dz_int(data.t<1);

fit = @(b,x)  b(1).*sin(x + b(2)) + b(3);    % Function to fit
fcn = @(b) sum((fit(b,x1) - y1).^2);                             % Least-Squares cost function

s1 = fminsearch(fcn, [1e-6;  30;  1.5e-6])                      % Minimise Least-Squares
#+end_src

#+begin_src matlab :results value replace :exports results :tangle no
sprintf('Errors linked to excentricity = %.0f [nm]', 1e9*s1(1));
#+end_src

#+RESULTS:
: Errors linked to excentricity = 410 [nm]

#+begin_src matlab :exports none :results none
%% Effect of the excentricity and remaining Dz motion
figure;
tiledlayout(1, 2, 'TileSpacing', 'Compact', 'Padding', 'None');

ax1 = nexttile();
hold on;
plot(180/pi*x1, 1e6*y1, '.', 'DisplayName', 'Raw')
plot(180/pi*x1, 1e6*fit(s1, x1), '.', 'DisplayName', 'Fit')
hold off;
xlabel('$R_z$ [deg]')
ylabel('$D_z$ [$\mu$m]')
legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1);
xlim([0, 360]);

ax2 = nexttile();
plot(180/pi*x1, 1e9*(y1 - fit(s1, x1)), 'k.')
xlabel('$R_z$ [deg]')
ylabel('$D_z$ [$\mu$m]')
xlim([0, 360]);
#+end_src

#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/dz_motion_rotation_excentricity.pdf', 'width', 'full', 'height', 'tall');
#+end_src

#+name: fig:dz_motion_rotation_excentricity
#+caption: Effect of the excentricity and remaining Dz motion
#+RESULTS:
[[file:figs/dz_motion_rotation_excentricity.png]]

If we look at the remaining motion after removing the effect of the eccentricity (Figure ref:fig:dz_motion_rotation_excentricity, right), we can see a signal with 20 periods every turn.
Let's fit this.

#+begin_src matlab
%% Fit Sinus with period of 18 degrees (one electrical period)
x2 = data.Rz(data.t<1)*20;
y2 = y1 - fit(s1, x1);

fit = @(b,x)  b(1).*sin(x + b(2)) + b(3);    % Function to fit
fcn = @(b) sum((fit(b,x2) - y2).^2);                             % Least-Squares cost function

s2 = fminsearch(fcn, [50e-9;  30;  0])                      % Minimise Least-Squares
#+end_src

#+begin_src matlab :results value replace :exports results :tangle no
sprintf('Errors linked to spindle motor = %.0f [nm]', 1e9*s2(1));
#+end_src

#+RESULTS:
: Errors linked to spindle motor = 58 [nm]

#+begin_src matlab :exports none :results none
%% Effect of the poles and remaining Dz motion
figure;
tiledlayout(1, 2, 'TileSpacing', 'Compact', 'Padding', 'None');

ax1 = nexttile();
hold on;
plot(180/pi*x1, 1e6*y2, '.', 'DisplayName', 'Raw')
plot(180/pi*x1, 1e6*fit(s2, x2), '.', 'DisplayName', 'Fit')
hold off;
xlabel('$R_z$ [deg]')
ylabel('$D_z$ [$\mu$m]')
legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1);
xlim([0, 360]);

ax2 = nexttile();
plot(180/pi*x1, 1e9*(y2 - fit(s2, x2)), 'k.')
xlabel('$R_z$ [deg]')
ylabel('$D_z$ [$\mu$m]')
xlim([0, 360]);
#+end_src

#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/dz_motion_rotation_poles.pdf', 'width', 'full', 'height', 'tall');
#+end_src

#+name: fig:dz_motion_rotation_poles
#+caption: Effect of the magnetic pole pairs and remaining Dz motion
#+RESULTS:
[[file:figs/dz_motion_rotation_poles.png]]

Let's look at the signal in the frequency domain.

On top of the peak at 1Hz (excentricity) and at 20Hz (number of pole pairs), we can observe a frequency of 126Hz (i.e. 126 periods per turn, approx 2.85 deg).

#+begin_quote
Could this be related to the air bearing system?
#+end_quote

#+begin_src matlab :exports none
%% Hanning window used for pwelch function
Ts = 1e-4;
win = hanning(5/Ts);

%% Compute PSD of initial motion error
[S_Dz, f] = pwelch(detrend(data.Dz_int, 0), win, 0, [], 1/Ts);
#+end_src

#+begin_src matlab :exports none :results none
%% Amplitude Spectral Density of the measured Dz motion
figure;
hold on;
plot(f, sqrt(S_Dz));
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log');
xlabel('Frequency [Hz]'); ylabel('ASD of $D_z$ [$m/\sqrt{Hz}$]');
xlim([0.1, 1e3]); ylim([1e-11, 1e-4]);
#+end_src

#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/dz_spindle_rotation_asd.pdf', 'width', 'wide', 'height', 'normal');
#+end_src

#+name: fig:dz_spindle_rotation_asd
#+caption: Amplitude Spectral Density of the measured Dz motion
#+RESULTS:
[[file:figs/dz_spindle_rotation_asd.png]]

#+begin_src matlab :exports none :results none
%% Cumulative Amplitude Spectrum of the measured Dx and Dy motion
figure;
hold on;
plot(f, sqrt(flip(-cumtrapz(flip(f), flip(S_Dx)))));
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log');
xlabel('Frequency [Hz]'); ylabel('CAS of $D_z$ [$m$]');
xlim([0.1, 1e3]); ylim([1e-9, 2e-5]);
#+end_src

#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/dz_spindle_rotation_cas.pdf', 'width', 'wide', 'height', 'normal');
#+end_src

#+name: fig:dz_spindle_rotation_cas
#+caption: Cumulative Amplitude Spectrum of the measured Dz motion
#+RESULTS:
[[file:figs/dz_spindle_rotation_cas.png]]

*** Angle errors in $R_x$ and $R_y$

#+begin_src matlab
[xc,yc,R,a] = circlefit(data.Rx_int, data.Ry_int);
#+end_src

#+begin_src matlab :results value replace :exports results :tangle no
sprintf('amplitude = %.0f urad', 1e6*R);
#+end_src

#+RESULTS:
: amplitude = 281 urad

#+begin_src matlab :exports none :results none
%% Rx and Ry motion during the spindle rotation
theta = linspace(0, 2*pi, 1000);
figure;
tiledlayout(1, 2, 'TileSpacing', 'Compact', 'Padding', 'None');

ax1 = nexttile();
hold on;
plot(1e6*detrend(data.Rx_int, 0), 1e6*detrend(data.Ry_int, 0), 'DisplayName', 'Raw data')
plot(1e6*detrend(xc + R*cos(theta), 0), 1e6*detrend(yc + R*sin(theta), 0), '--', 'DisplayName', 'fit')
plot(1e6*(data.Rx_int - (xc + R*cos(data.Rz+2.97))), 1e6*(data.Ry_int - (yc + R*sin(data.Rz+2.97))), 'k.', 'DisplayName', 'Residual')
hold off;
xlabel('$R_x$ [$\mu$rad]')
ylabel('$R_y$ [$\mu$rad]')
axis equal
legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1);

ax2 = nexttile();
plot(1e6*(data.Rx_int - (xc + R*cos(data.Rz+2.97))), 1e6*(data.Ry_int - (yc + R*sin(data.Rz+2.97))), 'k.')
xlabel('$R_x$ [$\mu$rad]')
ylabel('$R_y$ [$\mu$rad]')
axis equal
#+end_src

#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/rx_ry_spindle_rotation.pdf', 'width', 'full', 'height', 'tall');
#+end_src

#+name: fig:rx_ry_spindle_rotation
#+caption: Rx and Ry motion during the spindle rotation
#+RESULTS:
[[file:figs/rx_ry_spindle_rotation.png]]

Let's now analyse the frequency content in the signal.

#+begin_src matlab :exports none
%% Hanning window used for pwelch function
Ts = 1e-4;
win = hanning(5/Ts);

%% Compute PSD of initial motion error
[S_Rx, f] = pwelch(detrend(data.Rx_int, 0), win, 0, [], 1/Ts);
[S_Ry, ~] = pwelch(detrend(data.Ry_int, 0), win, 0, [], 1/Ts);
#+end_src

#+begin_src matlab :exports none :results none
%% Amplitude Spectral Density of the measured Rx and Ry motion
figure;
hold on;
plot(f, sqrt(S_Rx), 'DisplayName', '$D_x$');
plot(f, sqrt(S_Ry), 'DisplayName', '$D_y$');
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log');
xlabel('Frequency [Hz]'); ylabel('ASD [rad$/\sqrt{Hz}$]');
legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1);
xlim([0.1, 1e3]); ylim([1e-10, 1e-3]);
#+end_src

#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/rx_ry_spindle_rotation_asd.pdf', 'width', 'wide', 'height', 'normal');
#+end_src

#+name: fig:rx_ry_spindle_rotation_asd
#+caption: Amplitude Spectral Density of the measured Rx and Ry motion
#+RESULTS:
[[file:figs/rx_ry_spindle_rotation_asd.png]]

#+begin_src matlab :exports none :results none
%% Cumulative Amplitude Spectrum of the measured Rx and Ry motion
figure;
hold on;
plot(f, sqrt(flip(-cumtrapz(flip(f), flip(S_Rx)))), 'DisplayName', '$D_x$');
plot(f, sqrt(flip(-cumtrapz(flip(f), flip(S_Ry)))), 'DisplayName', '$D_y$');
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log');
xlabel('Frequency [Hz]'); ylabel('CAS [rad]');
legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1);
xlim([0.1, 1e3]); ylim([5e-8, 1e-3]);
#+end_src

#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/rx_ry_spindle_rotation_cas.pdf', 'width', 'wide', 'height', 'normal');
#+end_src

#+name: fig:rx_ry_spindle_rotation_cas
#+caption: Cumulative Amplitude Spectrum of the measured Rx and Ry motion
#+RESULTS:
[[file:figs/rx_ry_spindle_rotation_cas.png]]


* Simscape Model
** Introduction                                                      :ignore:

A 3D view of the Simscape model is shown in Figure ref:fig:simscape_model_spindle_bench.
The Spindle is represented by a /Bushing joint/.
Axial, radial and tilt stiffnesses are taken from the Spindle datasheet (see Table).

#+name: tab:spindle_stiffnesses
#+caption: Spindle stiffnesses
#+attr_latex: :environment tabularx :width \linewidth :align lXX
#+attr_latex: :center t :booktabs t
| *Stiffness* | *Value* | *Unit*    |
|-------------+---------+-----------|
| Axial       |     402 | $N/\mu m$ |
| Radial      |     226 | $N/\mu m$ |
| Tilt        |    2380 | $Nm/mrad$ |

The metrology system consists of 5 distance measurements (represented by the red lines in Figure ref:fig:simscape_model_spindle_bench).

#+name: fig:simscape_model_spindle_bench
#+caption: Screenshot of the 3D view of the Simscape model
#+attr_latex: :width 0.5\linewidth
[[file:figs/simscape_model_spindle_bench.jpg]]

** Matlab Init                                            :noexport:ignore:
#+begin_src matlab :tangle no :exports none :results silent :noweb yes :var current_dir=(file-name-directory buffer-file-name)
<<matlab-dir>>
#+end_src

#+begin_src matlab :exports none :results silent :noweb yes
<<matlab-init>>
#+end_src

#+begin_src matlab :tangle no :noweb yes
<<m-init-path>>
#+end_src

#+begin_src matlab :eval no :noweb yes
<<m-init-path-tangle>>
#+end_src

#+begin_src matlab :noweb yes
<<m-init-other>>
#+end_src

#+begin_src matlab
%% Frequency vector for further use
freqs = logspace(0,3,1000);

%% Start angle of the Spindle
start_angle = 0;
#+end_src

#+begin_src matlab :tangle no
%% Add path for Nano-Hexapod Simscape model
addpath('./matlab/nass-simscape/STEPS/nano_hexapod')
addpath('./matlab/nass-simscape/matlab/nano_hexapod')
addpath('./matlab/nass-simscape/src')
addpath('./matlab/nass-simscape/mat')
#+end_src

#+begin_src matlab :eval no
%% Add path for Nano-Hexapod Simscape model
addpath('./nass-simscape/STEPS/nano_hexapod')
addpath('./nass-simscape/matlab/nano_hexapod')
addpath('./nass-simscape/src')
addpath('./nass-simscape/mat')
#+end_src

** Simscape model parameters
The nano-hexapod is initialized.

#+begin_src matlab
%% Initialize Nano-Hexapod
n_hexapod = initializeNanoHexapodFinal('MO_B', 150e-3, ...
                                       'actuator_type', '2dof', ...
                                       'motion_sensor_type', 'plates');
#+end_src

#+begin_src matlab
%% Initialize Spindle
spindle = struct();

%% Stiffnesses
spindle.kx = 226e6; % Radial stiffness in [N/m]
spindle.ky = 226e6; % Radial stiffness in [N/m]
spindle.kz = 402e6; % Axial stiffness in [N/m]
spindle.krx = 2.38e6; % Tilt stiffness in [Nm/rad]
spindle.kry = 2.38e6; % Tilt stiffness in [Nm/rad]
spindle.krz = 0; % Rotation stiffness in [Nm/rad]

%% Damping
spindle.cx = 1/(2*0.01)/(sqrt(spindle.kx*50)); % Radial damping in [N/(m/s)]
spindle.cy = 1/(2*0.01)/(sqrt(spindle.ky*50)); % Radial damping in [N/(m/s)]
spindle.cz = 1/(2*0.01)/(sqrt(spindle.kz*50)); % Axial damping in [N/(m/s)]
spindle.crx = 1/(2*0.01)/(sqrt(spindle.krx*50)); % Tilt damping in [Nm/(rad/s)]
spindle.cry = 1/(2*0.01)/(sqrt(spindle.kry*50)); % Tilt damping in [Nm/(rad/s)]
spindle.crz = 0; % Rotation damping in [Nm/(rad/s)]
#+end_src

The Jacobian matrix that computes the $[x, y, z, R_x, R_y]$ motion of the sample from the 5 interferometers is defined below.

#+begin_src matlab
% Sensor Jacobian (Interferometers to Cartesian motion)
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];
#+end_src

#+begin_src matlab :exports none
%% Open the Simscape model
mdl = 'spindle_test_bench'
open(mdl)
#+end_src

** Control Architecture

Let's note:
- $d\mathcal{L}_m = [d_{\mathcal{L}_1},\ d_{\mathcal{L}_2},\ d_{\mathcal{L}_3},\ d_{\mathcal{L}_4},\ d_{\mathcal{L}_5},\ d_{\mathcal{L}_6}]$ the measurement of the 6 encoders fixed to the nano-hexapod
- $\bm{\tau}_m = [\tau_{m_1},\ \tau_{m_2},\ \tau_{m_3},\ \tau_{m_4},\ \tau_{m_5},\ \tau_{m_6}]$ the voltages measured by the 6 force sensors
- $\bm{u} = [u_1,\ u_2,\ u_3,\ u_4,\ u_5,\ u_6]$ the voltages send to the voltage amplifiers for the 6 piezoelectric actuators
- $R_z$ the spindle measured angle (encoder)
- $\bm{d}_m = [d_1,\ d_2,\ d_3,\ d_4,\ d_5]$ the distances measured by the 5 interferometers (see Figure ref:fig:LION_metrology_interferometers_bis)

#+name: fig:LION_metrology_interferometers_bis
#+caption: Schematic of the measurement system
#+attr_latex: :width 0.5\linewidth
[[file:figs/LION_metrology_interferometers.png]]

** Computation of the strut errors from the external metrology

The following frames are defined:
- $\{ W \}$: the frame that represents the wanted pose of the sample
- $\{ M \}$: the frame that represents the measured pose of the sample (estimated from the 5 interferometers and the spindle encoder)
- $\{ G \}$: the frame fixed to the granite and positioned at the sample's center
- $\{ H \}$: the frame fixed to the the spindle rotor, and positioned at the sample's center

We can express several homogeneous transformation matrices.

Frame fixed to the spindle rotor (centered on the sample's position), expressed in the frame of the granite:
\begin{equation}
{}^{G}\bm{T}_H = \begin{bmatrix}
cos(R_z) & -sin(R_z) & 0 & 0 \\
sin(R_z) &  cos(R_z) & 0 & 0 \\
0        &  0        & 1 & 0 \\
0        &  0        & 0 & 1
\end{bmatrix}
\end{equation}
with $R_z$ the spindle encoder.


Wanted position expressed in the frame of the granite:
\begin{equation}
{}^{G}\bm{T}_W = \begin{bmatrix}
 &                                 & & r_{D_x} \\
 & \bm{R}_x(r_{R_x}) \bm{R}_y(r_{R_y}) \bm{R}_z(r_{R_z}) & & r_{D_y} \\
 &                                 & & r_{D_z} \\
0        &  0                      & 0 & 1
\end{bmatrix}
\end{equation}
with $\bm{R}(r_{R_x}, r_{R_y}, r_{R_z})$ representing the wanted orientation of the sample with respect to the granite.
Typically, $r_{R_x} = 0$, $r_{R_y} = 0$ and $r_{R_z}$ corresponds to the spindle encoder $R_z$.


Measured position of the sample with respect to the granite:
\begin{equation}
{}^{G}\bm{T}_M = \begin{bmatrix}
 &                                 & & y_{D_x} \\
 & \bm{R}_x(y_{R_x}) \bm{R}_y(y_{R_y}) \bm{R}_z(R_z) & & y_{D_y} \\
 &                                 & & y_{D_z} \\
0        &  0                      & 0 & 1
\end{bmatrix}
\end{equation}
with $R_z$ the spindle encoder, and $[y_{D_x},\ y_{D_y},\ y_{D_z},\ y_{R_x},\ y_{R_y}]$ are obtained from the 5 interferometers:
\begin{equation}
\begin{bmatrix}
  y_{D_x} \\ y_{D_y} \\ y_{D_z} \\ y_{R_x} \\ y_{R_y}
\end{bmatrix} = \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}^{-1} \cdot \begin{bmatrix}
  d_1 \\ d_2 \\ d_3 \\ d_4 \\ d_5
\end{bmatrix}
\end{equation}


In order to have the *position error in the frame of the nano-hexapod*, we have to compute ${}^M\bm{T}_W$:
\begin{align}
{}^M\bm{T}_W &= {}^M\bm{T}_G \cdot {}^G\bm{T}_W \\
             &= {{}^G\bm{T}_M}^{-1} \cdot {}^G\bm{T}_W
\end{align}

The *inverse of the transformation matrix* can be obtained by
\begin{equation}
  {}^B\bm{T}_A = {}^A\bm{T}_B^{-1} =
  \left[ \begin{array}{ccc|c}
      &   &   & \\
      & {}^A\bm{R}_B^T  &   & -{}^A \bm{R}_B^T {}^A\bm{P}_{O_B} \\
      &   &   & \cr
    \hline
    0 & 0 & 0 & 1 \\
  \end{array} \right]
\end{equation}

The position errors $\bm{\epsilon}_{\mathcal{X}} = [\epsilon_{D_x},\ \epsilon_{D_y},\ \epsilon_{D_z},\ \epsilon_{R_x},\ \epsilon_{R_y},\ \epsilon_{R_z}]$ expressed in a frame fixed to the nano-hexapod can be extracted from ${}^W\bm{T}_M$:
- $\epsilon_{D_x} = {}^M\bm{T}_W(1,4)$
- $\epsilon_{D_y} = {}^M\bm{T}_W(2,4)$
- $\epsilon_{D_z} = {}^M\bm{T}_W(3,4)$
- $\epsilon_{R_y} = \text{atan2}({}^M\bm{T}_W(1,3), \sqrt{{}^M\bm{T}_W(1,1)^2 + {}^M\bm{T}_W(1,2)^2})$
- $\epsilon_{R_x} = \text{atan2}(\frac{-{}^M\bm{T}_W(2,3)}{\cos(\epsilon_{R_y})}, \frac{{}^M\bm{T}_W(3,3)}{\cos(\epsilon_{R_y})})$
- $\epsilon_{R_z} = \text{atan2}(\frac{-{}^M\bm{T}_W(1,2)}{\cos(\epsilon_{R_y})}, \frac{{}^M\bm{T}_W(1,1)}{\cos(\epsilon_{R_y})})$

Finally, the strut errors $\bm{\epsilon}_{\mathcal{L}} = [\epsilon_{\matcal{L}_1},\ \epsilon_{\matcal{L}_2},\ \epsilon_{\matcal{L}_3},\ \epsilon_{\matcal{L}_4},\ \epsilon_{\matcal{L}_5},\ \epsilon_{\matcal{L}_6}]$ can be computed from:
\begin{equation}
\bm{\epsilon}_\mathcal{L} = \bm{J} \cdot \bm{\epsilon}_\mathcal{X}
\end{equation}

** IFF Plant
#+begin_src matlab
start_angle = 0; % [deg]
#+end_src

#+begin_src matlab
options = linearizeOptions;
options.SampleTime = 0;

%% Identify the transfer function from u to taum
clear io; io_i = 1;
io(io_i) = linio([mdl, '/F'],  1, 'openinput');   io_i = io_i + 1; % Actuator Inputs
io(io_i) = linio([mdl, '/Fm'], 1, 'openoutput'); io_i = io_i + 1;  % Force Sensors

%% Perform the model extraction
G_iff = linearize(mdl, io, 0.0, options);
#+end_src

#+begin_src matlab :exports none :results none
%% IFF plant obtained on the Simscape model
figure;
tiledlayout(3, 1, 'TileSpacing', 'Compact', 'Padding', 'None');

ax1 = nexttile([2,1]);
hold on;
for i = 1:5
    for j = i+1:6
        plot(freqs, abs(squeeze(freqresp(G_iff(i, j), freqs, 'Hz'))), 'color', [0, 0, 0, 0.2], ...
             'HandleVisibility', 'off');
    end
end
for i =1:6
    set(gca,'ColorOrderIndex',i)
    plot(freqs, abs(squeeze(freqresp(G_iff(i, i), freqs, 'Hz'))), ...
         'DisplayName', sprintf('$\\tau_{m,%i}/u_%i$', i, i));
end
plot(freqs, abs(squeeze(freqresp(G_iff(1, 2), freqs, 'Hz'))), 'color', [0, 0, 0, 0.2], ...
     'DisplayName', '$\\tau_{m,i}/u_j$');
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log');
ylabel('Amplitude [m/V]'); set(gca, 'XTickLabel',[]);
% ylim([1e-9, 1e-3]);
legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 3);

ax2 = nexttile;
hold on;
for i =1:6
    set(gca,'ColorOrderIndex',i)
    plot(freqs, 180/pi*angle(squeeze(freqresp(G_iff(i, i), freqs, 'Hz'))));
end
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'lin');
xlabel('Frequency [Hz]'); ylabel('Phase [deg]');
hold off;
yticks(-360:90:360);

linkaxes([ax1,ax2],'x');
xlim([freqs(1), freqs(end)]);
#+end_src

** DVF Plant
#+begin_src matlab
start_angle = 0; % [deg]
#+end_src

#+begin_src matlab
options = linearizeOptions;
options.SampleTime = 0;

%% Identify the transfer function from u to taum
clear io; io_i = 1;
io(io_i) = linio([mdl, '/F'],  1, 'openinput');   io_i = io_i + 1; % Actuator Inputs
io(io_i) = linio([mdl, '/dL'], 1, 'openoutput'); io_i = io_i + 1;  % Encoders

%% Perform the model extraction
G_dvf = linearize(mdl, io, 0.0, options);
#+end_src

#+begin_src matlab :exports none :results none
%% DVF plant obtained on the Simscape model
figure;
tiledlayout(3, 1, 'TileSpacing', 'Compact', 'Padding', 'None');

ax1 = nexttile([2,1]);
hold on;
for i = 1:5
    for j = i+1:6
        plot(freqs, abs(squeeze(freqresp(G_dvf(i, j), freqs, 'Hz'))), 'color', [0, 0, 0, 0.2], ...
             'HandleVisibility', 'off');
    end
end
for i =1:6
    set(gca,'ColorOrderIndex',i)
    plot(freqs, abs(squeeze(freqresp(G_dvf(i, i), freqs, 'Hz'))), ...
         'DisplayName', sprintf('$d\\mathcal{L}_{m,%i}/u_%i$', i, i));
end
plot(freqs, abs(squeeze(freqresp(G_dvf(1, 2), freqs, 'Hz'))), 'color', [0, 0, 0, 0.2], ...
     'DisplayName', '$d\mathcal{L}_{m,i}/u_j$');
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log');
ylabel('Amplitude [m/V]'); set(gca, 'XTickLabel',[]);
% ylim([1e-9, 1e-3]);
legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 3);

ax2 = nexttile;
hold on;
for i =1:6
    set(gca,'ColorOrderIndex',i)
    plot(freqs, 180/pi*angle(squeeze(freqresp(G_dvf(i, i), freqs, 'Hz'))));
end
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'lin');
xlabel('Frequency [Hz]'); ylabel('Phase [deg]');
hold off;
yticks(-360:90:360);

linkaxes([ax1,ax2],'x');
xlim([freqs(1), freqs(end)]);
#+end_src

** HAC Plant
The transfer functions from the 6 actuator inputs to the 6 estimated strut errors are extracted from the Simscape model.

#+begin_src matlab
start_angle = 0; % [deg]
#+end_src

#+begin_src matlab
options = linearizeOptions;
options.SampleTime = 0;

%% Identify the transfer function from u to taum
clear io; io_i = 1;
io(io_i) = linio([mdl, '/F'],  1, 'openinput');   io_i = io_i + 1; % Actuator Inputs
io(io_i) = linio([mdl, '/J_X_to_L'],  1, 'openoutput'); io_i = io_i + 1; % Estimated Strut Error

%% Perform the model extraction
G = linearize(mdl, io, 0.0, options);
#+end_src

The obtained transfer functions are shown in Figure ref:fig:simscape_model_hac_plant.

We can see that the system is well decoupled at low frequency (i.e. below the first resonance of the Nano-Hexapod).

#+begin_src matlab :exports none :results none
%% HAC plant obtained on the Simscape model
figure;
tiledlayout(3, 1, 'TileSpacing', 'Compact', 'Padding', 'None');

ax1 = nexttile([2,1]);
hold on;
for i = 1:5
    for j = i+1:6
        plot(freqs, abs(squeeze(freqresp(G(i, j), freqs, 'Hz'))), 'color', [0, 0, 0, 0.2], ...
             'HandleVisibility', 'off');
    end
end
for i =1:6
    set(gca,'ColorOrderIndex',i)
    plot(freqs, abs(squeeze(freqresp(G(i, i), freqs, 'Hz'))), ...
         'DisplayName', sprintf('$d\\mathcal{L}_%i/u_%i$', i, i));
end
plot(freqs, abs(squeeze(freqresp(G(1, 2), freqs, 'Hz'))), 'color', [0, 0, 0, 0.2], ...
     'DisplayName', '$d\mathcal{L}_i/u_j$');
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log');
ylabel('Amplitude [m/V]'); set(gca, 'XTickLabel',[]);
% ylim([1e-9, 1e-3]);
legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 3);

ax2 = nexttile;
hold on;
for i =1:6
    set(gca,'ColorOrderIndex',i)
    plot(freqs, 180/pi*angle(squeeze(freqresp(G(i, i), freqs, 'Hz'))));
end
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'lin');
xlabel('Frequency [Hz]'); ylabel('Phase [deg]');
hold off;
yticks(-360:90:360);

linkaxes([ax1,ax2],'x');
xlim([1e1, 1e3]);
#+end_src

#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/simscape_model_hac_plant.pdf', 'width', 'wide', 'height', 'tall');
#+end_src

#+name: fig:simscape_model_hac_plant
#+caption: HAC plant obtained on the Simscape model
#+RESULTS:
[[file:figs/simscape_model_hac_plant.png]]

** Save Plants                                                     :noexport:
#+begin_src matlab :tangle no
save('matlab/mat/simscape_plants.mat', 'G_iff', 'G_dvf', 'G');
#+end_src

* Control Experiment
** Matlab Init                                            :noexport:ignore:
#+begin_src matlab :tangle no :exports none :results silent :noweb yes :var current_dir=(file-name-directory buffer-file-name)
<<matlab-dir>>
#+end_src

#+begin_src matlab :exports none :results silent :noweb yes
<<matlab-init>>
#+end_src

#+begin_src matlab :tangle no :noweb yes
<<m-init-path>>
#+end_src

#+begin_src matlab :eval no :noweb yes
<<m-init-path-tangle>>
#+end_src

#+begin_src matlab :noweb yes
<<m-init-other>>
#+end_src

#+begin_src matlab
%% Frequency vector for further use
freqs = logspace(0,3,1000);
#+end_src

#+begin_src matlab
data_path = '/home/thomas/mnt/data_mel/NASS';
#+end_src

** IFF Plant
#+begin_src matlab
%% Load identification data
data = load(sprintf('%s/dynamics/2023-02-01_15-21_identification_new_matrices_long_bis.mat', data_path));
#+end_src

#+begin_src matlab :exports none
%% Load extracted plants from Simscape
model = load('simscape_plants.mat', 'G_iff', 'G_dvf', 'G');
#+end_src

#+begin_src matlab :exports none
% Sampling Time [s]
Ts = 1e-4;

% Hannning Windows
win = hanning(ceil(1/Ts));
#+end_src

#+begin_src matlab :exports none
% And we get the frequency vector
[~, f] = tfestimate(data.uL1.id_plant, data.uL1.e_L1, win, [], [], 1/Ts);
#+end_src

#+begin_src matlab :exports none
%% IFF Plant (transfer function from u to taum)
G_iff = zeros(length(f), 6, 6);

for i_strut = 1:6
    eL = [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]';

    G_iff(:,:,i_strut) = tfestimate(data.(sprintf("uL%i", i_strut)).id_plant, eL, win, [], [], 1/Ts);
end
#+end_src

#+begin_src matlab :exports none :results none
%% 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_iff(:, i, j)), 'color', [0, 0, 0, 0.2], ...
             'HandleVisibility', 'off');
    end
end
for i = 1:6
    plot(f, abs(G_iff(:,i, i)), 'color', colors(i,:), ...
        'DisplayName', sprintf('$\\tau_{m,%i}/u_%i$', i, i));
end
plot(f, abs(G_iff(:, 1, 2)), 'color', [0, 0, 0, 0.2], ...
     'DisplayName', '$\tau_{m,i}/u_j$');
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log');
ylabel('Amplitude [V/V]'); set(gca, 'XTickLabel',[]);
% ylim([1e-8, 1e-3]);
legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 3);

ax2 = nexttile;
hold on;
for i =1:6
    plot(f, 180/pi*angle(G_iff(:,i, i)));
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]);
#+end_src

#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/G_iif_exp_no_rotation.pdf', 'width', 'full', 'height', 'tall');
#+end_src

#+name: fig:G_iif_exp_no_rotation
#+caption: Obtained transfer function from generated voltages to measured voltages on the piezoelectric force sensor
#+RESULTS:
[[file:figs/G_iif_exp_no_rotation.png]]

#+begin_src matlab :exports none
%% Comparison with the model
figure;
tiledlayout(3, 1, 'TileSpacing', 'Compact', 'Padding', 'None');

ax1 = nexttile([2,1]);
hold on;
for i = 1:6
    plot(f, abs(G_iff(:,i, i)), '-', 'color', colors(i,:), ...
        'DisplayName', sprintf('$\\tau_{m,%i}/u_%i$', i, i));
end
for i = 1:6
    plot(f, abs(squeeze(freqresp(model.G_iff(i, i), f, 'Hz'))), '--', 'color', colors(i,:), ...
        'DisplayName', sprintf('$\\tau_{m,%i}/u_%i$', i, i));
end
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log');
ylabel('Amplitude [V/V]'); set(gca, 'XTickLabel',[]);
% ylim([1e-8, 1e-3]);
% legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 3);

ax2 = nexttile;
hold on;
for i =1:6
    plot(f, 180/pi*angle(G_iff(:,i, i)), '-', 'color', colors(i,:));
end
for i =1:6
    plot(f, 180/pi*angle(squeeze(freqresp(model.G_iff(i, i), f, 'Hz'))), '--', 'color', colors(i,:));
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]);
#+end_src

#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/G_iif_exp_comp_no_rotation.pdf', 'width', 'full', 'height', 'tall');
#+end_src

#+name: fig:G_iif_exp_comp_no_rotation
#+caption: Comparison with the model
#+RESULTS:
[[file:figs/G_iif_exp_comp_no_rotation.png]]

** IFF Controller
#+begin_src matlab
%% IFF Controller
Kiff_g1 = -(1/(s + 2*pi*40))*...    % LPF: provides integral action above 40Hz
           (s/(s + 2*pi*30))*...    % HPF: limit low frequency gain
           (1/(1 + s/2/pi/500))*... % LPF: more robust to high frequency resonances
           eye(6);                  % Diagonal 6x6 controller
#+end_src

#+begin_src matlab :exports none :results none
%% Root Locus for IFF
gains = logspace(1, 4, 100);

figure;

hold on;
plot(real(pole(model.G_iff)),  imag(pole(model.G_iff)),  'x', 'color', colors(1,:), ...
    'DisplayName', '$g = 0$');
plot(real(tzero(model.G_iff)), imag(tzero(model.G_iff)), 'o', 'color', colors(1,:), ...
    'HandleVisibility', 'off');

for g = gains
    clpoles = pole(feedback(model.G_iff, g*Kiff_g1*eye(6), +1));
    plot(real(clpoles), imag(clpoles), '.', 'color', colors(1,:), ...
        'HandleVisibility', 'off');
end

% Optimal gain
g = 4e2;
clpoles = pole(feedback(model.G_iff, g*Kiff_g1*eye(6), +1));
plot(real(clpoles), imag(clpoles), 'x', 'color', colors(2,:), ...
    'DisplayName', sprintf('$g=%.0f$', g));
hold off;
xlim([-1500, 0]); ylim([0, 1500]);
axis square;
xlabel('Real Part'); ylabel('Imaginary Part');
legend('location', 'northwest');
#+end_src

#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/root_locus_iff_no_payload.pdf', 'width', 'normal', 'height', 'tall');
#+end_src

#+name: fig:root_locus_iff_no_payload
#+caption: Root Locus for IFF
#+RESULTS:
[[file:figs/root_locus_iff_no_payload.png]]


#+begin_src matlab :exports none
K = g*Kiff_g1(1,1);
K_order = order(K(1,1));

Kz = c2d(K(1,1)*(1 + s/2/pi/2e3)^(9-K_order)/(1 + s/2/pi/2e3)^(9-K_order), 1e-4);
[num, den] = tfdata(Kz, 'v');

formatSpec = '%.18e %.18e %.18e %.18e %.18e %.18e %.18e %.18e %.18e %.18e\n';
fileID = fopen('matlab/mat/K_iff_first_test.dat', 'w');
fprintf(fileID, formatSpec, [num; den]');
fclose(fileID);
#+end_src

** Open Loop Plant
Here the $R_z$ motion of the Hexapod is estimated from the encoders.

#+begin_src matlab :exports none
%% IFF Plant (transfer function from u to taum)
G_hac = zeros(length(f), 6, 6);

for i_strut = 1:6
    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]';

    G_hac(:,:,i_strut) = tfestimate(data.(sprintf("uL%i", i_strut)).id_plant, eL, win, [], [], 1/Ts);
end
#+end_src

#+begin_src matlab :exports none
%% Bode plot for the transfer function from u to dLm
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_hac(:, i, j)), 'color', [0, 0, 0, 0.2], ...
             'HandleVisibility', 'off');
    end
end
for i = 1:6
    plot(f, abs(G_hac(:,i, i)), 'color', colors(i,:), ...
        'DisplayName', sprintf('$\\epsilon_{m,%i}/u_%i$', i, i));
end
plot(f, abs(G_hac(:, 1, 2)), 'color', [0, 0, 0, 0.2], ...
     'DisplayName', '$\epsilon_{m,i}/u_j$');
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log');
ylabel('Amplitude [m/V]'); set(gca, 'XTickLabel',[]);
ylim([1e-8, 1e-3]);
legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 3);

ax2 = nexttile;
hold on;
for i =1:6
    plot(f, 180/pi*angle(G_hac(:,i, i)));
end
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]);
#+end_src

#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/G_damp_exp_no_rotation.pdf', 'width', 'full', 'height', 'tall');
#+end_src

#+name: fig:G_damp_exp_no_rotation
#+caption: Obtained transfer function from generated voltages to estimated strut motion
#+RESULTS:
[[file:figs/G_damp_exp_no_rotation.png]]

#+begin_src matlab :exports none :results none
%% Comparison of the open-loop plant measured experimentally and extracted from Simscape
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_hac(:, i, j)), 'color', [colors(1,:), 0.2], ...
             'HandleVisibility', 'off');
    end
end
plot(f, abs(G_hac(:,1, 1)), 'color', colors(1,:), ...
     'DisplayName', 'Model');
for i = 1:6
    plot(f, abs(G_hac(:,i, i)), 'color', colors(1,:), ...
         'HandleVisibility', 'off');
end
for i = 1:5
    for j = i+1:6
        plot(f, abs(squeeze(freqresp(model.G(i, j), f, 'Hz'))), 'color', [colors(2,:), 0.2], ...
             'HandleVisibility', 'off');
    end
end
plot(f, abs(squeeze(freqresp(model.G(1, 1), f, 'Hz'))), 'color', colors(2,:), ...
     'DisplayName', 'Experiment');
for i = 1:6
    plot(f, abs(squeeze(freqresp(model.G(i, i), f, 'Hz'))), 'color', colors(2,:), ...
         'HandleVisibility', 'off');
end
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log');
ylabel('Amplitude [m/V]'); set(gca, 'XTickLabel',[]);
ylim([1e-8, 1e-3]);
legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 1);

ax2 = nexttile;
hold on;
for i =1:6
    plot(f, 180/pi*angle(G_hac(:,i, i)), 'color', colors(1,:));
end
for i =1:6
    plot(f, 180/pi*angle(squeeze(freqresp(model.G(i, i), f, '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([-180, 180])

linkaxes([ax1,ax2],'x');
xlim([1, 1e3]);
#+end_src

#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/comp_hac_plant_exp_simscape.pdf', 'width', 'full', 'height', 'tall');
#+end_src

#+name: fig:comp_hac_plant_exp_simscape
#+caption: Comparison of the open-loop plant measured experimentally and extracted from Simscape
#+RESULTS:
[[file:figs/comp_hac_plant_exp_simscape.png]]


** Damped Plant
#+begin_src matlab
%% Load identification data for the damped plant
data = load(sprintf('%s/dynamics/2023-02-01_16-04_identification_damped_iff_long.mat', data_path));
#+end_src

#+begin_src matlab :exports none
%% IFF Plant (transfer function from u to taum)
G_damp = zeros(length(f), 6, 6);

for i_strut = 1:6
    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]';

    G_damp(:,:,i_strut) = tfestimate(data.(sprintf("uL%i", i_strut)).id_plant, eL, win, [], [], 1/Ts);
end
#+end_src

#+begin_src matlab :exports none
%% Bode plot for the transfer function from u to dLm
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_damp(:, i, j)), 'color', [0, 0, 0, 0.2], ...
             'HandleVisibility', 'off');
    end
end
for i = 1:6
    plot(f, abs(G_damp(:,i, i)), 'color', colors(i,:), ...
        'DisplayName', sprintf('$\\epsilon_{m,%i}/u_%i$', i, i));
end
plot(f, abs(G_damp(:, 1, 2)), 'color', [0, 0, 0, 0.2], ...
     'DisplayName', '$\epsilon_{m,i}/u_j$');
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log');
ylabel('Amplitude [m/V]'); set(gca, 'XTickLabel',[]);
ylim([1e-8, 1e-3]);
legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 3);

ax2 = nexttile;
hold on;
for i =1:6
    plot(f, 180/pi*angle(G_damp(:,i, i)));
end
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]);
#+end_src

#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/G_damp_damped_exp_no_rotation.pdf', 'width', 'full', 'height', 'tall');
#+end_src

#+name: fig:G_damp_damped_exp_no_rotation
#+caption: Obtained transfer function from generated voltages to estimated strut motion
#+RESULTS:
[[file:figs/G_damp_damped_exp_no_rotation.png]]

#+begin_src matlab :exports none :results none
%% Comparison of the undamped and damped plant with IFF
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_hac(:, i, j)), 'color', [colors(1,:), 0.2], ...
             'HandleVisibility', 'off');
    end
end
plot(f, abs(G_hac(:, 1, 1)), 'color', colors(1,:), ...
     'DisplayName', 'Undamped');
for i = 2:6
    plot(f, abs(G_hac(:,i, i)), 'color', colors(1,:), ...
             'HandleVisibility', 'off');
end
for i = 1:5
    for j = i+1:6
        plot(f, abs(G_damp(:, i, j)), 'color', [colors(2,:), 0.2], ...
             'HandleVisibility', 'off');
    end
end
plot(f, abs(G_damp(:, 1, 1)), 'color', colors(2,:), ...
     'DisplayName', 'Damped');
for i = 2:6
    plot(f, abs(G_damp(:,i, i)), 'color', colors(2,:), ...
             'HandleVisibility', 'off');
end
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log');
ylabel('Amplitude [m/V]'); set(gca, 'XTickLabel',[]);
ylim([1e-8, 1e-3]);
legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 1);

ax2 = nexttile;
hold on;
for i =1:6
    plot(f, 180/pi*angle(G_hac(:,i, i)), 'color', colors(1,:));
end
for i =1:6
    plot(f, 180/pi*angle(G_damp(:,i, i)), '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([-180, 180])

linkaxes([ax1,ax2],'x');
xlim([1, 1e3]);
#+end_src

#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/comp_damped_undamped_plant.pdf', 'width', 'full', 'height', 'tall');
#+end_src

#+name: fig:comp_damped_undamped_plant
#+caption: Comparison of the undamped and damped plant with IFF
#+RESULTS:
[[file:figs/comp_damped_undamped_plant.png]]

** HAC Controller
#+begin_src matlab
%% Lead to increase phase margin
a  = 4;  % Amount of phase lead / width of the phase lead / high frequency gain
wc = 2*pi*15; % Frequency with the maximum phase lead [rad/s]

H_lead = (1 + s/(wc/sqrt(a)))/(1 + s/(wc*sqrt(a)));

%% Low Pass filter to increase robustness
H_lpf = 1/(1 + s/2/pi/60);

%% Notch at the top-plate resonance
gm = 0.02;
xi = 0.3;
wn = 2*pi*665;

H_notch = (s^2 + 2*gm*xi*wn*s + wn^2)/(s^2 + 2*xi*wn*s + wn^2);

%% Decentralized HAC
Khac_iff_struts = (8e3) * ... % Gain
                  H_notch * ...      % Notch
                  H_lpf * ...      % LPF
                  (2*pi*100/s) * ... % Integrator
                  eye(6);            % 6x6 Diagonal
#+end_src

#+begin_src matlab :exports none
%% Loop Gain
figure;
tiledlayout(3, 1, 'TileSpacing', 'Compact', 'Padding', 'None');

ax1 = nexttile([2,1]);
hold on;
for i = 1:6
    plot(f, abs(G_damp(:,i, i).*squeeze(freqresp(Khac_iff_struts(i,i), f, 'Hz'))));
end
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log');
ylabel('Loop Gain'); set(gca, 'XTickLabel',[]);
% ylim([1e-8, 1e-3]);

ax2 = nexttile;
hold on;
for i =1:6
    plot(f, 180/pi*angle(G_damp(:,i, i).*squeeze(freqresp(Khac_iff_struts(i,i), f, 'Hz'))));
end
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]);
#+end_src

#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/first_hac_K_exp_loop_gain.pdf', 'width', 'full', 'height', 'tall');
#+end_src

#+name: fig:first_hac_K_exp_loop_gain
#+caption: Loop gain for the HAC
#+RESULTS:
[[file:figs/first_hac_K_exp_loop_gain.png]]

#+begin_src matlab :exports none
%% Compute the Eigenvalues of the loop gain
Ldet = zeros(6, length(f));

Lmimo = pagemtimes(permute(G_damp, [2,3,1]),squeeze(freqresp(Khac_iff_struts, f, 'Hz')));
for i_f = 2:length(f)
    Ldet(:, i_f) = eig(squeeze(Lmimo(:,:,i_f)));
end
#+end_src

#+begin_src matlab :exports none
%% Plot of the eigenvalues of L in the complex plane
figure;
hold on;
for i = 1:6
    plot(real(squeeze(Ldet(i,:))),  imag(squeeze(Ldet(i,:))), 'k.');
    plot(real(squeeze(Ldet(i,:))), -imag(squeeze(Ldet(i,:))), 'k.');
end
plot(-1, 0, 'kx', 'HandleVisibility', 'off');
hold off;
set(gca, 'XScale', 'lin'); set(gca, 'YScale', 'lin');
xlabel('Real'); ylabel('Imag');
xlim([-3, 1]); ylim([-2, 2]);
#+end_src

#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/first_hac_K_exp_root_locus.pdf', 'width', 'full', 'height', 'tall');
#+end_src

#+name: fig:first_hac_K_exp_root_locus
#+caption: Obtained Root Locus
#+RESULTS:
[[file:figs/first_hac_K_exp_root_locus.png]]

#+begin_src matlab :exports none
%% Export to Bliss format
K = - Khac_iff_struts(1,1);
K_order = order(K(1,1));

Kz = c2d(K(1,1)*(1 + s/2/pi/2e3)^(9-K_order)/(1 + s/2/pi/2e3)^(9-K_order), 1e-4);
[num, den] = tfdata(Kz, 'v');

formatSpec = '%.18e %.18e %.18e %.18e %.18e %.18e %.18e %.18e %.18e %.18e\n';
fileID = fopen('matlab/mat/K_hac_first_test.dat', 'w');
fprintf(fileID, formatSpec, [num; den]');
fclose(fileID);
#+end_src

** Compare dynamics seen by interferometers and by encoders
#+begin_src matlab :exports none
%% IFF Plant (transfer function from u to taum)
G_dl = zeros(length(f), 6, 6);

for i_strut = 1:6
    eL = -[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_dl(:,:,i_strut) = tfestimate(data.(sprintf("uL%i", i_strut)).id_plant, eL, win, [], [], 1/Ts);
end
#+end_src

#+begin_src matlab :exports none :results none
%% Comparison of the identified dynamic by the internal metrology (encoders) and by the external metrology (interferometers)
figure;
tiledlayout(3, 1, 'TileSpacing', 'Compact', 'Padding', 'None');

ax1 = nexttile([2,1]);
hold on;
plot(f, abs(G_hac(:,1, 1)), '-', ...
     'DisplayName', 'External Metrology');
plot(f, abs(G_dl(:,1, 1)), '-', ...
     'DisplayName', 'Internal Metrology');
for i = 1:5
    for j = i+1:6
        plot(f, abs(G_hac(:, i, j)), 'color', [colors(1,:), 0.2], ...
             'HandleVisibility', 'off');
        plot(f, abs(G_dl(:, i, j)), 'color', [colors(2,:), 0.2], ...
             'HandleVisibility', 'off');
    end
end
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log');
ylabel('Amplitude [m/V]'); set(gca, 'XTickLabel',[]);
ylim([1e-8, 1e-3]);
legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 1);

ax2 = nexttile;
hold on;
plot(f, 180/pi*angle(G_hac(:,1, 1)), '-');
plot(f, 180/pi*angle(G_dl(:,1, 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]);
#+end_src

#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/comp_dynamics_int_ext_metrology.pdf', 'width', 'full', 'height', 'tall');
#+end_src

#+name: fig:comp_dynamics_int_ext_metrology
#+caption: Comparison of the identified dynamic by the internal metrology (encoders) and by the external metrology (interferometers)
#+RESULTS:
[[file:figs/comp_dynamics_int_ext_metrology.png]]


** Compare dynamics obtained with different Rz estimations
#+begin_src matlab :exports none
%% Load identification data
data = load(sprintf('%s/dynamics/2023-02-01_15-18_identification_new_matrices_long_Va.mat', data_path));
#+end_src

#+begin_src matlab :exports none
%% IFF Plant (transfer function from u to taum)
G_hac_Va = zeros(length(f), 6, 6);

for i_strut = 1:6
    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]';

    G_hac_Va(:,:,i_strut) = tfestimate(data.(sprintf("uL%i", i_strut)).id_plant, eL, win, [], [], 1/Ts);
end
#+end_src

#+begin_src matlab :exports none :results none
%% Comparison of the obtained plant using the Encoders or using the output Voltages to estimate Rz
figure;
tiledlayout(3, 1, 'TileSpacing', 'Compact', 'Padding', 'None');

ax1 = nexttile([2,1]);
hold on;
plot(f, abs(G_hac(:,1, 1)), '-', ...
     'DisplayName', 'Encoders');
plot(f, abs(G_hac_Va(:,1, 1)), '-', ...
     'DisplayName', 'Voltage');
for i = 1:5
    for j = i+1:6
        plot(f, abs(G_hac(:, i, j)), 'color', [colors(1,:), 0.2], ...
             'HandleVisibility', 'off');
        plot(f, abs(G_hac_Va(:, i, j)), 'color', [colors(2,:), 0.2], ...
             'HandleVisibility', 'off');
    end
end
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log');
ylabel('Amplitude [m/V]'); set(gca, 'XTickLabel',[]);
ylim([1e-8, 1e-3]);
legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 3);

ax2 = nexttile;
hold on;
plot(f, 180/pi*angle(G_hac(:,1, 1)), '-');
plot(f, 180/pi*angle(G_hac_Va(:,1, 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]);
#+end_src

#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/comp_plant_encoders_Va.pdf', 'width', 'full', 'height', 'tall');
#+end_src

#+name: fig:comp_plant_encoders_Va
#+caption: Comparison of the obtained plant using the Encoders or using the output Voltages to estimate Rz
#+RESULTS:
[[file:figs/comp_plant_encoders_Va.png]]


* Closed-Loop Results
** Matlab Init                                            :noexport:ignore:
#+begin_src matlab :tangle no :exports none :results silent :noweb yes :var current_dir=(file-name-directory buffer-file-name)
<<matlab-dir>>
#+end_src

#+begin_src matlab :exports none :results silent :noweb yes
<<matlab-init>>
#+end_src

#+begin_src matlab :tangle no :noweb yes
<<m-init-path>>
#+end_src

#+begin_src matlab :eval no :noweb yes
<<m-init-path-tangle>>
#+end_src

#+begin_src matlab :noweb yes
<<m-init-other>>
#+end_src

#+begin_src matlab
%% Frequency vector for further use
freqs = logspace(0,3,1000);
#+end_src

#+begin_src matlab
data_path = '/home/thomas/mnt/data_mel/NASS';
#+end_src

** Open and Closed loop results
#+begin_src matlab
data_ol = load(sprintf('%s/spindle/2022-12-20_15-43_sec_test.mat', data_path));
data_cl = load(sprintf('%s/spindle/2023-02-01_16-57_closed_loop.mat', data_path));
#+end_src

#+begin_src matlab :exports none :results none
%% Comparison of the Open-Loop and Closed-Loop spindle errors
figure;
hold on;
plot(detrend(1e6*data_ol.Dx_int, 0), detrend(1e6*data_ol.Dy_int, 0), 'DisplayName', sprintf('OL $\\epsilon_d = %.1f$ [$\\mu$m RMS]', 1e6*rms(sqrt(detrend(data_ol.Dx_int, 0).^2 + detrend(data_ol.Dy_int, 0).^2))))
hold off;
xlabel('$x$ motion [$\mu$m]');
ylabel('$y$ motion [$\mu$m]');
legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1);
axis square
#+end_src

#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/spindle_errors_1rpm_ol.pdf', 'width', 'wide', 'height', 'tall');
#+end_src

#+RESULTS:
[[file:figs/spindle_errors_1rpm_ol.png]]

#+begin_src matlab :exports none :results none
%% Comparison of the Open-Loop and Closed-Loop spindle errors
figure;
hold on;
plot(detrend(1e6*data_ol.Dx_int, 0), detrend(1e6*data_ol.Dy_int, 0), 'DisplayName', sprintf('OL $\\epsilon_d = %.1f$ [$\\mu$m RMS]', 1e6*rms(sqrt(detrend(data_ol.Dx_int, 0).^2 + detrend(data_ol.Dy_int, 0).^2))))
plot(1e6*data_cl.Dx_int, 1e6*data_cl.Dy_int, 'DisplayName', sprintf('CL $\\epsilon_d = %.1f$ [nm RMS]', 1e9*rms(sqrt(data_cl.Dx_int.^2 + data_cl.Dy_int.^2))))
hold off;
xlabel('$x$ motion [$\mu$m]');
ylabel('$y$ motion [$\mu$m]');
legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1);
axis square
#+end_src

#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/spindle_errors_1rpm_op_cl.pdf', 'width', 'wide', 'height', 'tall');
#+end_src

#+name: fig:spindle_errors_1rpm_op_cl
#+caption: Comparison of the Open-Loop and Closed-Loop spindle errors
#+RESULTS:
[[file:figs/spindle_errors_1rpm_op_cl.png]]

#+begin_src matlab :exports none :results none
%% Comparison of the Open-Loop and Closed-Loop spindle errors - Rotation
figure;
hold on;
plot(detrend(1e6*data_ol.Rx_int, 0), detrend(1e6*data_ol.Ry_int, 0), 'DisplayName', 'Open-Loop')
plot(1e6*data_cl.Rx_int, 1e6*data_cl.Ry_int, 'DisplayName', 'Closed-Loop')
hold off;
xlabel('$R_x$ motion [$\mu$rad]');
ylabel('$R_y$ motion [$\mu$rad]');
legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1);
axis square
#+end_src

#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/spindle_errors_1rpm_op_cl_rot.pdf', 'width', 'wide', 'height', 'tall');
#+end_src

#+name: fig:spindle_errors_1rpm_op_cl_rot
#+caption: Comparison of the Open-Loop and Closed-Loop spindle errors - Rotation
#+RESULTS:
[[file:figs/spindle_errors_1rpm_op_cl_rot.png]]

* Nano-Hexapod fixed on the Spindle                                 :noexport:
<<sec:nano_hexapod_spindle>>
** Introduction                                                      :ignore:

The Spindle is now fixed on top of a Spindle: the RS250S from LAB Motion Systems ([[file:doc/RS250S.pdf][doc]]).
All electrical connections between the nano-hexapod and the control electronics are passing trough a Slip-Ring.
A picture of the nano-hexapod on top of the Spindle is shown in Figure ref:fig:hexapod_spindle_picture.

#+name: fig:hexapod_spindle_picture
#+caption: Nano-Hexapod fixed on top of the Spindle
#+attr_latex: :width \linewidth
[[file:figs/hexapod_spindle_picture.jpg]]

** Change of dynamics when fixed on the Spindle
*** Matlab Init                                            :noexport:ignore:
#+begin_src matlab :tangle no :exports none :results silent :noweb yes :var current_dir=(file-name-directory buffer-file-name)
<<matlab-dir>>
#+end_src

#+begin_src matlab :exports none :results silent :noweb yes
<<matlab-init>>
#+end_src

#+begin_src matlab :tangle no :noweb yes
<<m-init-path>>
#+end_src

#+begin_src matlab :eval no :noweb yes
<<m-init-path-tangle>>
#+end_src

#+begin_src matlab :noweb yes
<<m-init-other>>
#+end_src

*** Measured Frequency Response Functions
The identification only performed without any payload.

The following data are loaded:
- =Va=: the excitation voltage (corresponding to $u_i$)
- =Vs=: the generated voltage by the 6 force sensors (corresponding to $\bm{\tau}_m$)
- =de=: the measured motion by the 6 encoders (corresponding to $d\bm{\mathcal{L}}_m$)
#+begin_src matlab
%% Load Identification Data
meas_added_mass = {};

for i_strut = 1:6
    meas_added_mass(i_strut) = {load(sprintf('frf_data_exc_strut_%i_spindle_0m.mat', i_strut), 't', 'Va', 'Vs', 'de')};
end
#+end_src

The window =win= and the frequency vector =f= are defined.
#+begin_src matlab
% Sampling Time [s]
Ts = (meas_added_mass{1}.t(end) - (meas_added_mass{1}.t(1)))/(length(meas_added_mass{1}.t)-1);

% Hannning Windows
win = hanning(ceil(1/Ts));

% And we get the frequency vector
[~, f] = tfestimate(meas_added_mass{1}.Va, meas_added_mass{1}.de, win, [], [], 1/Ts);
#+end_src

Finally the $6 \times 6$ transfer function matrices from $\bm{u}$ to $d\bm{\mathcal{L}}_m$ and from $\bm{u}$ to $\bm{\tau}_m$ are identified:
#+begin_src matlab
%% DVF Plant (transfer function from u to dLm)
G_dL = zeros(length(f), 6, 6);

for i_strut = 1:6
    G_dL(:,:,i_strut) = tfestimate(meas_added_mass{i_strut}.Va, meas_added_mass{i_strut}.de, win, [], [], 1/Ts);
end

%% IFF Plant (transfer function from u to taum)
G_tau = zeros(length(f), 6, 6);

for i_strut = 1:6
    G_tau(:,:,i_strut) = tfestimate(meas_added_mass{i_strut}.Va, meas_added_mass{i_strut}.Vs, win, [], [], 1/Ts);
end
#+end_src

The identified dynamics are then saved for further use.
#+begin_src matlab :exports none :tangle no
save('matlab/data_frf/frf_spindle_m.mat', 'f', 'Ts', 'G_tau', 'G_dL')
#+end_src

#+begin_src matlab :eval no
save('data_frf/frf_spindle_m.mat', 'f', 'Ts', 'G_tau', 'G_dL')
#+end_src

#+begin_src matlab :exports none
frf_ol = load('frf_spindle_m.mat', 'f', 'Ts', 'G_tau', 'G_dL');
frf_vib_tab = load('frf_vib_table_m.mat', 'f', 'Ts', 'G_tau', 'G_dL');
#+end_src

*** Transfer function from Actuator to Encoder
The transfer functions from $u_i$ to $d\mathcal{L}_{m,i}$ are shown in Figure ref:fig:frf_GdL_spindle_0m.

#+begin_src matlab :exports none
%% Bode plot for the transfer function from u to dLm
figure;
tiledlayout(3, 1, 'TileSpacing', 'None', 'Padding', 'None');

ax1 = nexttile([2,1]);
hold on;
for i = 1:5
    for j = i+1:6
        plot(frf_ol.f, abs(frf_ol.G_dL(:, i, j)), 'color', [0, 0, 0, 0.2], ...
             'HandleVisibility', 'off');
    end
end
for i = 1:6
    set(gca,'ColorOrderIndex',i)
    plot(frf_ol.f, abs(frf_ol.G_dL(:,i, i)), ...
     'DisplayName', sprintf('$d\\mathcal{L}_{m,%i}/u_%i$', i, i));
end
plot(frf_ol.f, abs(frf_ol.G_dL(:, 1, 2)), 'color', [0, 0, 0, 0.2], ...
     'DisplayName', '$d\mathcal{L}_{m,i}/u_j$');
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log');
ylabel('Amplitude [m/V]'); set(gca, 'XTickLabel',[]);
ylim([1e-8, 1e-3]);
legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 3);

ax2 = nexttile;
hold on;
for i =1:6
    plot(frf_ol.f, 180/pi*angle(frf_ol.G_dL(:,i, i)));
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([10, 1e3]);
#+end_src

#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/frf_GdL_spindle_0m.pdf', 'width', 'wide', 'height', 'tall');
#+end_src

#+name: fig:frf_GdL_spindle_0m
#+caption: Measured Frequency Response Functions from $u_i$ to $d\mathcal{L}_{m,i}$ when the nano-hexapod is fixed to the Spindle
#+RESULTS:
[[file:figs/frf_GdL_spindle_0m.png]]

The dynamics of the nano-hexapod when fixed on the Spindle is compared with the dynamics when the nano-hexapod is fixed on the "vibration table" in Figure ref:fig:frf_GdL_comp_spindle_vib_table_0m.

#+begin_question
Why are the frequency of the suspension modes are *decreased* when the nano-hexapod is fixed to the Spindle?
#+end_question

#+begin_src matlab :exports none
%% Bode plot for the transfer function from u to dLm
figure;
tiledlayout(3, 1, 'TileSpacing', 'None', 'Padding', 'None');

ax1 = nexttile([2,1]);
hold on;
for i = 1:5
    for j = i+1:6
        plot(frf_ol.f, abs(frf_ol.G_dL(:,i,j)), 'color', [colors(1,:), 0.2], ...
             'HandleVisibility', 'off');
        plot(frf_vib_tab.f, abs(frf_vib_tab.G_dL{1}(:,i,j)), 'color', [colors(2,:), 0.2], ...
             'HandleVisibility', 'off');
    end
end
plot(frf_ol.f, abs(frf_ol.G_dL(:,1,1)), 'color', colors(1,:), ...
     'DisplayName', 'Spindle');
plot(frf_vib_tab.f, abs(frf_vib_tab.G_dL{1}(:,1,1)), 'color', colors(2,:), ...
     'DisplayName', 'Vib. Table');
for i = 2:6
    plot(frf_ol.f, abs(frf_ol.G_dL(:,i, i)), 'color', colors(1,:), ...
         'HandleVisibility', 'off');
    plot(frf_vib_tab.f, abs(frf_vib_tab.G_dL{1}(:,i, i)), 'color', colors(2,:), ...
         'HandleVisibility', 'off');
end
plot(frf_ol.f, abs(frf_ol.G_dL(:,1,2)), 'color', [colors(1,:), 0.2], ...
     'DisplayName', 'Spindle - Coupling');
plot(frf_vib_tab.f, abs(frf_vib_tab.G_dL{1}(:,1,2)), 'color', [colors(2,:), 0.2], ...
     'DisplayName', 'Vib. Table - Coupling');
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log');
ylabel('Amplitude [m/V]'); set(gca, 'XTickLabel',[]);
ylim([1e-8, 1e-3]);
legend('location', 'southwest', 'FontSize', 8, 'NumColumns', 2);

ax2 = nexttile;
hold on;
for i =1:6
    set(gca,'ColorOrderIndex',1)
    plot(frf_ol.f, 180/pi*angle(frf_ol.G_dL(:,i, i)));
    set(gca,'ColorOrderIndex',2)
    plot(frf_vib_tab.f, 180/pi*angle(frf_vib_tab.G_dL{1}(:,i, i)));
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([10, 1e3]);
#+end_src

#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/frf_GdL_comp_spindle_vib_table_0m.pdf', 'width', 'wide', 'height', 'tall');
#+end_src

#+name: fig:frf_GdL_comp_spindle_vib_table_0m
#+caption: Comparison of the dynamics from $u$ to $d\mathcal{L}$ when the nano-hexapod is fixed on top of the Spindle and when it is fixed on top of the "Vibration Table".
#+RESULTS:
[[file:figs/frf_GdL_comp_spindle_vib_table_0m.png]]

*** Transfer function from Actuator to Force Sensor
The transfer functions from $u_i$ to $\tau_m$ are shown in Figure ref:fig:frf_Gtau_spindle_0m.

#+begin_src matlab :exports none
%% Bode plot for the transfer function from u to dLm
figure;
tiledlayout(3, 1, 'TileSpacing', 'None', 'Padding', 'None');

ax1 = nexttile([2,1]);
hold on;
for i = 1:5
    for j = i+1:6
        plot(frf_ol.f, abs(frf_ol.G_tau(:, i, j)), 'color', [0, 0, 0, 0.2], ...
             'HandleVisibility', 'off');
    end
end
for i = 1:6
    set(gca,'ColorOrderIndex',i)
    plot(frf_ol.f, abs(frf_ol.G_tau(:,i, i)), ...
         'DisplayName', sprintf('$\\tau_{m,%i}/u_%i$', i, i));
end
plot(frf_ol.f, abs(frf_ol.G_tau(:, 1, 2)), 'color', [0, 0, 0, 0.2], ...
     'DisplayName', '$\\tau_{m,i}/u_j$');
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log');
ylabel('Amplitude [V/V]'); set(gca, 'XTickLabel',[]);
ylim([1e-2, 1e2]);
legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 3);

ax2 = nexttile;
hold on;
for i =1:6
    plot(frf_ol.f, 180/pi*angle(frf_ol.G_tau(:,i, i)));
end
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([20, 2e3]);
#+end_src

#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/frf_Gtau_spindle_0m.pdf', 'width', 'wide', 'height', 'tall');
#+end_src

#+name: fig:frf_Gtau_spindle_0m
#+caption: Measured Frequency Response Functions from $u_i$ to $\tau_{m,i}$ when the nano-hexapod is fixed to the Spindle
#+RESULTS:
[[file:figs/frf_Gtau_spindle_0m.png]]

The dynamics of the nano-hexapod when fixed on the Spindle is compared with the dynamics when the nano-hexapod is fixed on the "vibration table" in Figure ref:fig:frf_Gtau_comp_spindle_vib_table_0m.

#+begin_src matlab :exports none
%% Bode plot for the transfer function from u to taum
figure;
tiledlayout(3, 1, 'TileSpacing', 'None', 'Padding', 'None');

ax1 = nexttile([2,1]);
hold on;
for i = 1:5
    for j = i+1:6
        plot(frf_ol.f, abs(frf_ol.G_tau(:,i,j)), 'color', [colors(1,:), 0.2], ...
             'HandleVisibility', 'off');
        plot(frf_vib_tab.f, abs(frf_vib_tab.G_tau{1}(:,i,j)), 'color', [colors(2,:), 0.2], ...
             'HandleVisibility', 'off');
    end
end
plot(frf_ol.f, abs(frf_ol.G_tau(:,1,1)), 'color', colors(1,:), ...
     'DisplayName', 'Spindle');
plot(frf_vib_tab.f, abs(frf_vib_tab.G_tau{1}(:,1,1)), 'color', colors(2,:), ...
     'DisplayName', 'Vib. Table');
for i = 2:6
    plot(frf_ol.f, abs(frf_ol.G_tau(:,i, i)), 'color', colors(1,:), ...
         'HandleVisibility', 'off');
    plot(frf_vib_tab.f, abs(frf_vib_tab.G_tau{1}(:,i, i)), 'color', colors(2,:), ...
         'HandleVisibility', 'off');
end
plot(frf_ol.f, abs(frf_ol.G_tau(:,1,2)), 'color', [colors(1,:), 0.2], ...
     'DisplayName', 'Spindle - Coupling');
plot(frf_vib_tab.f, abs(frf_vib_tab.G_tau{1}(:,1,2)), 'color', [colors(2,:), 0.2], ...
     'DisplayName', 'Vib. Table - Coupling');
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log');
ylabel('Amplitude [-]'); set(gca, 'XTickLabel',[]);
ylim([1e-3, 1e2]);
legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 2);

ax2 = nexttile;
hold on;
for i =1:6
    set(gca,'ColorOrderIndex',1)
    plot(frf_ol.f, 180/pi*angle(frf_ol.G_tau(:,i, i)));
    set(gca,'ColorOrderIndex',2)
    plot(frf_vib_tab.f, 180/pi*angle(frf_vib_tab.G_tau{1}(:,i, i)));
end
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([10, 1e3]);
#+end_src

#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/frf_Gtau_comp_spindle_vib_table_0m.pdf', 'width', 'wide', 'height', 'tall');
#+end_src

#+name: fig:frf_Gtau_comp_spindle_vib_table_0m
#+caption: Comparison of the dynamics from $u$ to $d\mathcal{L}$ when the nano-hexapod is fixed on top of the Spindle and when it is fixed on top of the "Vibration Table".
#+RESULTS:
[[file:figs/frf_Gtau_comp_spindle_vib_table_0m.png]]

*** Conclusion
#+begin_important
The dynamics of the nano-hexapod does not change a lot when it is fixed to the Spindle.
The "suspension" modes are just increased a little bit due to the added stiffness of the spindle as compared to the vibration table.
#+end_important

** Dynamics of the Damped plant
*** Introduction                                                    :ignore:
As the dynamics is not much changed when the nano-hexapod is fixed on top of the Spindle, the same IFF controller is used to damp the plant.

*** Matlab Init                                            :noexport:ignore:
#+begin_src matlab :tangle no :exports none :results silent :noweb yes :var current_dir=(file-name-directory buffer-file-name)
<<matlab-dir>>
#+end_src

#+begin_src matlab :exports none :results silent :noweb yes
<<matlab-init>>
#+end_src

#+begin_src matlab :tangle no :noweb yes
<<m-init-path>>
#+end_src

#+begin_src matlab :eval no :noweb yes
<<m-init-path-tangle>>
#+end_src

#+begin_src matlab :noweb yes
<<m-init-other>>
#+end_src

*** Measured Frequency Response Functions
The identification is performed without added mass, and with one, two and three layers of added cylinders.
#+begin_src matlab
i_masses = 0:3;
#+end_src

The following data are loaded:
- =Va=: the excitation voltage (corresponding to $u_i$)
- =Vs=: the generated voltage by the 6 force sensors (corresponding to $\bm{\tau}_m$)
- =de=: the measured motion by the 6 encoders (corresponding to $d\bm{\mathcal{L}}_m$)
#+begin_src matlab
%% Load Identification Data
meas_added_mass = {};

for i_mass = i_masses
    for i_strut = 1:6
        meas_added_mass(i_strut, i_mass+1) = {load(sprintf('frf_data_exc_strut_%i_spindle_%im_iff.mat', i_strut, i_mass), 't', 'Va', 'Vs', 'de')};
    end
end
#+end_src

The window =win= and the frequency vector =f= are defined.
#+begin_src matlab
% Sampling Time [s]
Ts = (meas_added_mass{1,1}.t(end) - (meas_added_mass{1,1}.t(1)))/(length(meas_added_mass{1,1}.t)-1);

% Hannning Windows
win = hanning(ceil(1/Ts));

% And we get the frequency vector
[~, f] = tfestimate(meas_added_mass{1,1}.Va, meas_added_mass{1,1}.de, win, [], [], 1/Ts);
#+end_src

Finally the $6 \times 6$ transfer function matrices from $\bm{u}$ to $d\bm{\mathcal{L}}_m$ and from $\bm{u}$ to $\bm{\tau}_m$ are identified:
#+begin_src matlab
%% DVF Plant (transfer function from u to dLm)
G_dL = {};

for i_mass = i_masses
    G_dL(i_mass+1) = {zeros(length(f), 6, 6)};
    for i_strut = 1:6
        G_dL{i_mass+1}(:,:,i_strut) = tfestimate(meas_added_mass{i_strut, i_mass+1}.Va, meas_added_mass{i_strut, i_mass+1}.de, win, [], [], 1/Ts);
    end
end
#+end_src

The identified dynamics are then saved for further use.
#+begin_src matlab :exports none :tangle no
save('matlab/data_frf/frf_spindle_iff_m.mat', 'f', 'Ts', 'G_dL')
#+end_src

#+begin_src matlab :eval no
save('data_frf/frf_spindle_iff_m.mat', 'f', 'Ts', 'G_dL')
#+end_src

#+begin_src matlab :exports none
frf_ol  = load('frf_spindle_m.mat', 'f', 'Ts', 'G_tau', 'G_dL');
frf_iff = load('frf_spindle_iff_m.mat', 'f', 'Ts', 'G_dL');
#+end_src

*** Effect of Integral Force Feedback
#+begin_src matlab :exports none
%% Bode plot for the transfer function from u to dLm
figure;
tiledlayout(3, 1, 'TileSpacing', 'None', 'Padding', 'None');

ax1 = nexttile([2,1]);
hold on;
for i = 1:5
    for j = i+1:6
        plot(frf_ol.f, abs(frf_ol.G_dL(:, i, j)), 'color', [colors(1,:), 0.2], ...
             'HandleVisibility', 'off');
        plot(frf_ol.f, abs(frf_iff.G_dL{1}(:, i, j)), 'color', [colors(2,:), 0.2], ...
             'HandleVisibility', 'off');
    end
end
plot(frf_ol.f, abs(frf_ol.G_dL(:,1,1)), 'color', colors(1,:), ...
     'DisplayName', '$d\mathcal{L}_{m,i}/u_j$');
plot(frf_iff.f, abs(frf_iff.G_dL{1}(:,1,1)), 'color', colors(2,:), ...
     'DisplayName', '$d\mathcal{L}_{m,i}/u_j^\prime$');
for i = 2:6
    plot(frf_ol.f, abs(frf_ol.G_dL(:,i, i)), 'color', colors(1,:), ...
         'HandleVisibility', 'off');
    plot(frf_iff.f, abs(frf_iff.G_dL{1}(:,i, i)), 'color', colors(2,:), ...
         'HandleVisibility', 'off');
end
plot(frf_ol.f, abs(frf_ol.G_dL(:, 1, 2)), 'color', [colors(1,:), 0.2], ...
     'DisplayName', '$d\mathcal{L}_{m,i}/u_j$');
plot(frf_iff.f, abs(frf_iff.G_dL{1}(:, 1, 2)), 'color', [colors(2,:), 0.2], ...
     'DisplayName', '$d\mathcal{L}_{m,i}/u_j^\prime$');
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log');
ylabel('Amplitude [m/V]'); set(gca, 'XTickLabel',[]);
ylim([1e-8, 1e-3]);
legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 3);

ax2 = nexttile;
hold on;
for i =1:6
    set(gca,'ColorOrderIndex',1)
    plot(frf_ol.f, 180/pi*angle(frf_ol.G_dL(:,i, i)));
    set(gca,'ColorOrderIndex',2)
    plot(frf_iff.f, 180/pi*angle(frf_iff.G_dL{1}(:,i, i)));
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([10, 1e3]);
#+end_src

#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/frf_spindle_comp_ol_iff.pdf', 'width', 'full', 'height', 'tall');
#+end_src

#+name: fig:frf_spindle_comp_ol_iff
#+caption: Effect of Integral Force Feedback on the transfer function from $u_i$ to $d\mathcal{L}_i$
#+RESULTS:
[[file:figs/frf_spindle_comp_ol_iff.png]]

*** Effect of the payload
#+begin_important
From Figure ref:fig:frf_spindle_iff_effect_payload we can see that the coupling is quite large when payloads are added to the nano-hexapod.
This was not the case when the nano-hexapod was fixed to the vibration table.
#+end_important

#+begin_question
What is causing the resonances at 20Hz, 25Hz and 30Hz when there is some added payload?
Why the coupling is much larger than when the nano-hexapod was on top of the isolation table?
#+end_question

#+begin_src matlab :exports none
%% Bode plot for the transfer function from u to dLm
figure;
tiledlayout(3, 1, 'TileSpacing', 'None', 'Padding', 'None');

ax1 = nexttile([2,1]);
hold on;
for i_mass = i_masses
    for i = 1:5
        for j = i+1:6
            plot(frf_iff.f, abs(frf_iff.G_dL{i_mass+1}(:, i, j)), 'color', [colors(i_mass+1,:), 0.2], ...
                 'HandleVisibility', 'off');
        end
    end
    plot(frf_iff.f, abs(frf_iff.G_dL{i_mass+1}(:,1, 1)), 'color', colors(i_mass+1,:), ...
         'DisplayName', sprintf('$d\\mathcal{L}_{m,i}/u_i$ - %i', i_mass));
    for i = 2:6
        plot(frf_iff.f, abs(frf_iff.G_dL{i_mass+1}(:,i, i)), 'color', colors(i_mass+1,:), ...
             'HandleVisibility', 'off');
    end
end
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log');
ylabel('Amplitude [m/V]'); set(gca, 'XTickLabel',[]);
ylim([1e-8, 1e-3]);
legend('location', 'southwest', 'FontSize', 8, 'NumColumns', 2);

ax2 = nexttile;
hold on;
for i_mass = i_masses
    for i =1:6
        plot(frf_iff.f, 180/pi*angle(frf_iff.G_dL{i_mass+1}(:,i, i)), 'color', colors(i_mass+1,:));
    end
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([10, 1e3]);
#+end_src

#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/frf_spindle_iff_effect_payload.pdf', 'width', 'wide', 'height', 'tall');
#+end_src

#+name: fig:frf_spindle_iff_effect_payload
#+caption: Effect of the payload on the transfer functions from $u^\prime_i$ to $d\mathcal{L}_i$
#+RESULTS:
[[file:figs/frf_spindle_iff_effect_payload.png]]

*** Effect of rotation
#+begin_src matlab :exports none
%% Load Identification Data
meas_0rpm = {};
meas_60rpm = {};

for i_strut = 1:6
    meas_0rpm(i_strut) = {load(sprintf('frf_data_exc_strut_%i_spindle_3m_iff.mat', i_strut), 't', 'Va', 'Vs', 'de')};
    meas_60rpm(i_strut) = {load(sprintf('frf_data_exc_strut_%i_spindle_3m_iff_60rpm.mat', i_strut), 't', 'Va', 'Vs', 'de')};
end
#+end_src

#+begin_src matlab :exports none
% Sampling Time [s]
Ts = (meas_0rpm{1}.t(end) - (meas_0rpm{1}.t(1)))/(length(meas_0rpm{1}.t)-1);

% Hannning Windows
win = hanning(ceil(1/Ts));

% And we get the frequency vector
[~, f] = tfestimate(meas_0rpm{1}.Va, meas_0rpm{1}.de, win, [], [], 1/Ts);
#+end_src

#+begin_src matlab :exports none
%% DVF Plant (transfer function from u to dLm)
G_dL_0rpm = zeros(length(f), 6, 6);
G_dL_60rpm = zeros(length(f), 6, 6);

for i_strut = 1:6
    G_dL_0rpm(:,:,i_strut)  = tfestimate(meas_0rpm{i_strut}.Va,  meas_0rpm{i_strut}.de,  win, [], [], 1/Ts);
    G_dL_60rpm(:,:,i_strut) = tfestimate(meas_60rpm{i_strut}.Va, meas_60rpm{i_strut}.de, win, [], [], 1/Ts);
end
#+end_src

#+begin_important
The identified plants with and without spindle's rotation are compared in Figure ref:fig:frf_comp_spindle_0rpm_60rpm_3m.
It is shown that the rotational speed as little effect on the plant dynamics.
#+end_important

#+begin_src matlab :exports none
%% Bode plot for the transfer function from u to dLm
figure;
tiledlayout(3, 1, 'TileSpacing', 'None', 'Padding', 'None');

ax1 = nexttile([2,1]);
hold on;
for i = 1:5
    for j = i+1:6
        plot(f, abs(G_dL_0rpm(:, i, j)), 'color', [colors(1,:), 0.2], ...
             'HandleVisibility', 'off');
        plot(f, abs(G_dL_60rpm(:, i, j)), 'color', [colors(2,:), 0.2], ...
             'HandleVisibility', 'off');
    end
end
plot(f, abs(G_dL_0rpm(:,1,1)), 'color', colors(1,:), ...
     'DisplayName', '$d\mathcal{L}_{m,i}/u_j$ - 0rpm');
plot(f, abs(G_dL_60rpm(:,1,1)), 'color', colors(2,:), ...
     'DisplayName', '$d\mathcal{L}_{m,i}/u_j^\prime$ - 60rpm');
for i = 2:6
    plot(f, abs(G_dL_0rpm(:,i, i)), 'color', colors(1,:), ...
         'HandleVisibility', 'off');
    plot(f, abs(G_dL_60rpm(:,i, i)), 'color', colors(2,:), ...
         'HandleVisibility', 'off');
end
plot(f, abs(G_dL_0rpm(:, 1, 2)), 'color', [colors(1,:), 0.2], ...
     'DisplayName', '$d\mathcal{L}_{m,i}/u_j$ - 0rpm');
plot(f, abs(G_dL_60rpm(:, 1, 2)), 'color', [colors(2,:), 0.2], ...
     'DisplayName', '$d\mathcal{L}_{m,i}/u_j^\prime$ - 60rpm');
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log');
ylabel('Amplitude [m/V]'); set(gca, 'XTickLabel',[]);
ylim([1e-8, 1e-4]);
legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 3);

ax2 = nexttile;
hold on;
for i =1:6
    set(gca,'ColorOrderIndex',1)
    plot(f, 180/pi*angle(G_dL_0rpm(:,i, i)));
    set(gca,'ColorOrderIndex',2)
    plot(f, 180/pi*angle(G_dL_60rpm(:,i, i)));
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([10, 1e3]);
#+end_src

#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/frf_comp_spindle_0rpm_60rpm_3m.pdf', 'width', 'wide', 'height', 'tall');
#+end_src

#+name: fig:frf_comp_spindle_0rpm_60rpm_3m
#+caption: Comparison of the damped plant when the spindle is not rotating and when it is rotating at 60RPM
#+RESULTS:
[[file:figs/frf_comp_spindle_0rpm_60rpm_3m.png]]

* Helping Functions                                                 :noexport:
** Initialize Path
#+NAME: m-init-path
#+BEGIN_SRC matlab
%% Path for functions, data and scripts
addpath('./matlab/STEPS/'); % Path for STEPS files
addpath('./matlab/subsystems/'); % Path for Simulink subsystems
addpath('./matlab/mat/'); % Path for data
addpath('./matlab/src/'); % Path for functions
addpath('./matlab/');     % Path for scripts
#+END_SRC

#+NAME: m-init-path-tangle
#+BEGIN_SRC matlab
%% Path for functions, data and scripts
addpath('./STEPS/'); % Path for STEPS files
addpath('./subsystems/'); % Path for Simulink subsystems
addpath('./mat/'); % Path for data
addpath('./src/'); % Path for functions
#+END_SRC

** Initialize other elements
#+NAME: m-init-other
#+BEGIN_SRC matlab
%% Colors for the figures
colors = colororder;

Ts = 1e-4;
data_dir = "/home/thomas/mnt/data_easy/id00/inhouse/MEL/NASS"
#+END_SRC

** =circlefit=
:PROPERTIES:
:header-args:matlab+: :tangle matlab/src/circlefit.m
:header-args:matlab+: :comments none :mkdirp yes :eval no
:END:
<<sec:circlefit>>

#+begin_src matlab
function   [xc,yc,R,a] = circfit(x,y)
%
%   [xc yx R] = circfit(x,y)
%
%   fits a circle  in x,y plane in a more accurate
%   (less prone to ill condition )
%  procedure than circfit2 but using more memory
%  x,y are column vector where (x(i),y(i)) is a measured point
%
%  result is center point (yc,xc) and radius R
%  an optional output is the vector of coeficient a
% describing the circle's equation
%
%   x^2+y^2+a(1)*x+a(2)*y+a(3)=0
%
%  By:  Izhak bucher 25/oct /1991,
x=x(:); y=y(:);
a=[x y ones(size(x))]\[-(x.^2+y.^2)];
xc = -.5*a(1);
yc = -.5*a(2);
R  =  sqrt((a(1)^2+a(2)^2)/4-a(3));
#+end_src
