2024-03-19 15:30:11 +01:00
#+TITLE : Nano-Hexapod on the micro-station
: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]
2024-11-13 10:27:34 +01:00
#+LATEX_HEADER : \input{preamble.tex}
#+LATEX_HEADER_EXTRA : \input{preamble_extra.tex}
2024-03-19 15:30:11 +01:00
#+LATEX_HEADER_EXTRA : \bibliography{test-bench-id31.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:
#+begin_export html
<hr >
<p >This report is also available as a <a href="./test-bench-id31.pdf" >pdf</a >.</p >
<hr >
#+end_export
#+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}")
))
2024-11-13 10:27:34 +01:00
2024-03-19 15:30:11 +01:00
;; 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:
2024-11-13 10:27:34 +01:00
** Notes
Prefix is =test_id31=
2024-11-13 17:11:40 +01:00
data_dir = "/home/thomas/mnt/data_id31/id31nass/id31/20230801/RAW_DATA"
mat_dir = "/home/thomas/mnt/data_id31/nass"
2024-11-13 10:27:34 +01:00
*Goals* :
- Short stroke metrology
- Complete validation of the concept + nano-hexapod + instrumentation + control system
- Experimental validation of complementary filter control
*Outline* :
- Short stroke metrology system
- Objective: 5DoF measurement while rotation
- Estimation of angular acceptance
- Do not care too much about accuracy here
- Mounting and alignment
- Jacobian etc...
- Plant identification:
- Comparison with Simscape
- Better Rz alignment: use of the model
- Effect of payload mass
- Robust IFF
- High Authority Control
- Classical Loop Shaping:
- Control in the frame of the struts
- Control in the cartesian frame
- Complementary Filter Control
- S/T, noise budget
- Scientific experiments
- Tomography
- Lateral Ty scans
- Dirty layer scans
- Reflectivity
- Diffraction tomography
2025-01-31 16:33:25 +01:00
** DONE [#A] Make detailed outline
CLOSED: [2025-01-30 Thu 11:34]
2024-11-13 17:11:40 +01:00
2025-01-31 16:33:25 +01:00
- [ ] Where to put *noise budget* ?
- Separated (OL, IFF, HAC-IFF)?
- Or all at once?
- It was made specifically for tomography experiments
- Each time, 3 figures (Dy, Dz, Ry)
- [ ] Separate sections for different control strategies?
2024-11-15 18:15:13 +01:00
2025-01-31 16:33:25 +01:00
*Outline* :
- Short stroke metrology
- Open-Loop plant
- Effect of poor Rz alignment
- Effect of payload mass, effect of rotation
- IFF
- Controller design
- Check robustness
- Estimated damped plant?
- Robust HAC (frame of the struts)
- Damped plants
- Loop Shaping
- Check stability
- Noise budget
2024-11-15 18:15:13 +01:00
2025-01-31 16:33:25 +01:00
** DONE [#A] Find specifications for each experiment
CLOSED: [2025-01-30 Thu 11:16]
2024-11-15 18:15:13 +01:00
2025-01-31 16:33:25 +01:00
*Specifications for different directions* (Check this [[file:~/Cloud/work-projects/ID31-NASS/documents/work-package-1/work-package-1.pdf ][document ]].)
- [ ] Add these specifications in CAS plots (RMS values).
- [ ] Add these specifications in Y-Z plots (show beam size: 200nm x 100nm)
2024-11-15 18:15:13 +01:00
2025-01-31 16:33:25 +01:00
| | Dx | Dy | Dz | Rx | Ry | Rz |
|-------------+----+-------+-------+----+----------+-------|
| RMS | | 30nm | 15nm | | 250 nrad | |
| peak 2 peak | | 200nm | 100nm | | 1.7 urad | |
|-------------+----+-------+-------+----+----------+-------|
| MIM | | 20nm | 10nm | | 1urad | 2urad |
2024-11-15 18:15:13 +01:00
2025-01-31 16:33:25 +01:00
- [ ] Also check what was used for the uniaxial model and NASS Simscape model
*20nmRMS* from the uniaxial model
*Tomography* :
- Beam size: 200nm x 100nm
- Keep the PoI in the beam: peak to peak errors of 200nm in Dy and 100nm in Dz
- RMS errors (/ by 6.6) gives 30nmRMS in Dy and 15nmRMS in Dz.
- Ry error <1.7urad, 250nrad RMS
2025-02-01 09:45:01 +01:00
** TODO [#A] Maybe should only put experimental results in last section
Some Tomography experiments are presented in ref:sec:test_id31_iff_hac
Maybe it should only be simulations, and put everything in the last "experimental" section.
2025-01-31 16:33:25 +01:00
** TODO [#A] Ask Veijo to give me a short summary (5 lines) for each experiment type with references
- Tomography
- Lateral scans
- "dirty layer scan"
- Reflectivity
- Diffraction tomography
2024-11-15 18:15:13 +01:00
** TODO [#A] Check why simulation gives worst performances than reality
- [X] Check if low pass filtering the disturbances resolves the issue
No but it makes the simulation faster!
- [ ] Maybe because the disturbances where estimated at 60RPM and not at 1RPM !
** TODO [#A] Where to discuss the necessity of estimated Rz?
One big advantage of doing the control in the cartesian plane, is that we don't need the estimation of nano-hexapod Rz, therefore we don't need the encoders anymore!
Maybe this should be done in A6 (simscape-nass).
Here it can be reminded when doing the control in the cartesian frame.
2025-02-01 10:37:12 +01:00
** DONE [#A] Add same specifications for all the curves
CLOSED: [2025-02-01 Sat 10:22]
2024-11-15 18:15:13 +01:00
2025-01-31 16:33:25 +01:00
Peak to peak errors:
- Dz < +/- 50nm
- Dy < +/- 100nm
- Ry < +/- 0.85urad
2024-11-15 18:15:13 +01:00
2025-01-31 16:33:25 +01:00
RMS errors:
- Dz: < 15nm RMS
- Dy: < 30nm RMS
- Ry: < 0.25urad RMS
2024-11-15 18:15:13 +01:00
2025-01-31 16:33:25 +01:00
** TODO [#C] Make sure the display order to directions is always the same
2024-11-15 18:15:13 +01:00
2025-01-31 16:33:25 +01:00
*Dy, Dz, Ry*
2024-11-15 18:15:13 +01:00
2025-01-31 16:33:25 +01:00
** TODO [#C] Should I speak about higher bandwidth controllers even though they give lower performances?
2024-11-15 18:15:13 +01:00
2025-01-31 16:33:25 +01:00
Probably not
[[file:backup.org::*High Performance HAC ][High Performance HAC ]]
** DONE [#A] Copy all necessary .mat files
CLOSED: [2024-11-13 Wed 21:20]
** DONE [#B] Change the name of the I/O in the Simscape model to match the notations
CLOSED: [2024-11-14 Thu 11:32]
Fn => Vs
** TODO [#C] Talk about experimental conditions
- Air Conditioning: +/-0.1 deg?
- Experimental hutch is closed for a long time to have best thermal stability
** TODO [#B] Coherent notation/description of spindle rotation
- RPM
- rpm
2025-02-01 10:37:12 +01:00
- *Wz (deg/s)*
Maybe deg/s is the most adequate
2025-01-31 16:33:25 +01:00
Make a choice, and then adapt all notations.
Also, change the =initializeReferences= to accept the chosen description instead of =period= .
** TODO [#B] Explain that RMS values are not filtered
Maybe say that the detectors are integrating the signals (maybe 10ms?).
Then update some RMS values to show that it can be considered better than the values given.
Maybe give RMS value at 10kHz, and at 100Hz (LPF 1st order)
2024-11-15 18:15:13 +01:00
2025-01-31 16:33:25 +01:00
** DONE [#B] Analyze the observed modes
CLOSED: [2025-01-30 Thu 11:12]
2024-11-15 18:15:13 +01:00
Added mode compared to Nano-Hexapod test bench:
- Modes of the metrology spheres (between 600 and 700Hz)
- *Sphere modes*
To identify this mode, look at the transfer function from actuator to individual interferometers.
It may be possible that this mode does not appear on the bottom interferometers but only on the top one (because of limited stiffness of the top sphere support).
Look at =2023-08-10_18-32_identify_spurious_modes.mat=
2025-01-31 16:33:25 +01:00
*Conclusion is not clear*
** TODO [#B] Explain why position error does not converges to zero during tomography experiments
2024-11-15 18:15:13 +01:00
In closed-loop, the position does not converges to zeros but to a stable position.
This is due to the limited loop-gain at a frequency corresponding to the eccentricity.
Even though there is an integrator in the controller, this integrator is in the frame of the struts, and not in the cartesian frame.
The eccentricity is seen as DC disturbance in the cartesian frame, but at "Wz" frequency in the frame of the strut, hence the non-convergence to zero position.
2025-01-31 16:33:25 +01:00
Or the opposite?
2024-11-15 18:15:13 +01:00
- *Before 2023-08-11_11-41_tomography_30rpm_m0.mat* : Static errors
- *After 2023-08-11_14-16_m0_1rpm.mat* : No more static errors
Simulation: 30rpm experiment with large off-axis errors (to see if it converges to zero)
#+begin_src matlab
%% Tomography experiment
% Sample is not centered with the rotation axis
% This is done by offsetfing the micro-hexapod by 0.9um
P_micro_hexapod = [10e-6; 0; 0]; % [m]
set_param(mdl, 'StopTime', '3');
initializeMicroHexapod('AP', P_micro_hexapod);
initializeSample('type', '0');
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]);
% Closed-Loop Simulation
load('test_id31_K_iff.mat', 'Kiff');
load('test_id31_K_hac_robust.mat', 'Khac');
initializeController('type', 'hac-iff');
sim(mdl);
exp_tomo_cl_m0_Wz180_offset = simout;
#+end_src
#+begin_src matlab :exports none :results none
%% Measured radial errors of the Spindle
figure;
hold on;
plot(1e6*exp_tomo_cl_m0_Wz180_offset.y.x.Data, 1e6*exp_tomo_cl_m0_Wz180_offset.y.y.Data, 'DisplayName', 'Simulation')
hold off;
xlabel('X displacement [$\mu$m]'); ylabel('Y displacement [$\mu$m]');
axis equal
% xlim([-1, 1]); ylim([-1, 1]);
% xticks([-1, -0.5, 0, 0.5, 1]);
% yticks([-1, -0.5, 0, 0.5, 1]);
leg = legend('location', 'none', 'FontSize', 8, 'NumColumns', 1);
leg.ItemTokenSize(1) = 15;
#+end_src
Yes it converges to zero.
2025-01-31 16:33:25 +01:00
- [ ] Maybe just show results where the error converges to zero?
2024-11-15 18:15:13 +01:00
2024-11-15 18:41:18 +01:00
** DONE [#C] Talk about additional delay observed in the plant from u to d (interf)
CLOSED: [2024-11-15 Fri 18:22]
2024-11-15 18:15:13 +01:00
Explain that this is due to digital conversions using additional electronics, but this is inducing additional delays.
This was latter resolved by directly decoding the correct digital protocol in the Speedgoat.
** TODO [#B] Verify sign of plants
Should be inverse compare to encoder output
** DONE [#A] Make a nice schematic with all the signal names
CLOSED: [2024-11-15 Fri 15:03]
Should probably be similar than the one used in A6 report (Simscape with NASS).
- [X] Make a first schematic with all the signals going in and out of the Speedgoat.
In this figure, there is nothing about control, kinematics, etc...
But it would be nice to represent the micro-station + metrology
- [X] Make other figures for LAC, HAC-LAC, but this time really bloc diagrams
- Force sensors: $\bm{V}_s = [V_ {s1},\ V_{s2},\ V_ {s3},\ V_{s4},\ V_ {s5},\ V_{s6}]$
- Encoders: $\bm{d}_e = [d_ {e1},\ d_{e2},\ d_ {e3},\ d_{e4},\ d_ {e5},\ d_{e6}]$
- Interferometers: $\bm{d} = [d_{1},\ d_ {2},\ d_{3},\ d_ {4},\ d_{5}]$
- Command signal: $\bm{u} = [u_1,\ u_2,\ u_3,\ u_4,\ u_5,\ u_6]$
- Voltage across the piezoelectric stack actuator: $\bm{V}_a = [V_ {a1},\ V_{a2},\ V_ {a3},\ V_{a4},\ V_ {a5},\ V_{a6}]$
- Motion of the sample measured by external metrology: $\bm{\mathcal{X}} = [D_x,\,D_y,\,D_z,\,R_x,\,R_y,\,R_z]$
- Motion of the struts measured by external metrology: $\bm{\mathcal{L}} = [\mathcal{L}_1,\,\mathcal{L}_2,\,\mathcal{L}_3,\,\mathcal{L}_4,\,\mathcal{L}_5,\,\mathcal{L}_6]$
- Error of the sample measured by external metrology: $\bm{\epsilon}_{\mathcal{X}} = [\epsilon_ {D_x},\,\epsilon_ {D_y},\,\epsilon_ {D_z},\,\epsilon_ {R_x},\,\epsilon_ {R_y},\,\epsilon_ {R_z}]$
- Error of the struts measured by external metrology: $\bm{\epsilon}_{\mathcal{L}} = [\epsilon_ {\mathcal{L}_1},\,\epsilon_ {\mathcal{L}_2},\,\epsilon_ {\mathcal{L}_3},\,\epsilon_ {\mathcal{L}_4},\,\epsilon_ {\mathcal{L}_5},\,\epsilon_ {\mathcal{L}_6}]$
- Spindle angle setpoint (or encoder):
- Translation stage setpoint:
2024-11-13 17:11:40 +01:00
** DONE [#A] Resolve height issue in Simscape model
CLOSED: [2024-11-13 Wed 11:53]
For the current (ID31) model, the beam is 175mm above the nano-hexapod top platform.
It should be 150mm (25mm offset).
Check the micro-station model.
Height granite <= > micro-heapod = 530mm (OK Model/Solidworks)
Height of nano-heaxapod = 95mm
Height granite <= > beam = 800 mm
This means that height of nano-hexapod <= > beam is 800 - 530 - 95 = *175mm and not 150mm* .
- [X] *I need to know what was used during the experiments!*
*150mm* was used during the experiments
- [X] It should be compatible for the Jacobian used for the short stroke metrology
it seems 150mm was used for the metrology jacobian!
- [X] If something is change, update the previous Simscape models
2025-02-01 10:37:12 +01:00
** TODO [#C] Verify notations
$\bm{\epsilon\mathcal{L}}$ and not $\bm{e\mathcal{L}}$
$\bm{\epsilon\mathcal{X}}$ and not $\bm{e\mathcal{L}}$
2024-11-15 18:15:13 +01:00
** CANC [#B] Should the micro-hexapod position be adjusted to match the experiment
CLOSED: [2024-11-13 Wed 18:05]
2024-11-13 17:11:40 +01:00
2024-11-15 18:15:13 +01:00
- State "CANC" from "TODO" [2024-11-13 Wed 18:05]
2024-11-13 17:11:40 +01:00
After alignment, the micro-hexapod position was *h1tz = -17.72101mm* .
I suppose compared to the initial height of 350mm
2024-11-15 18:15:13 +01:00
** DONE [#A] Maybe just need one mass for the first identification
CLOSED: [2024-11-13 Wed 18:05]
2024-11-13 17:11:40 +01:00
First identification:
- compare with Simscape
- High coupling
- Check Rz alignment
- Correct Rz alignment
- New identification for all masses
- Better match with Simscape model!
2025-02-01 10:37:12 +01:00
** QUES [#C] Why now we have minimum phase zero for IFF Plant?
2024-11-13 10:27:34 +01:00
** CANC [#C] Find identification where Rz was not taken into account
CLOSED: [2024-11-12 Tue 16:03]
- State "CANC" from "TODO" [2024-11-12 Tue 16:03]
- Much larger coupling than estimated from the model
- In the model, suppose Rz = 0?
- Then how to estimate Rz?
- From voltages
- From encoders
- Results and comparison with model
*Maybe not talk about this here as it is not too interesting*
2024-03-19 15:30:11 +01:00
2025-02-01 18:35:04 +01:00
* Glossary and Acronyms - Tables :ignore:
#+name : acronyms
| key | abbreviation | full form |
|------+--------------+----------------------------------|
| nass | NASS | Nano Active Stabilization System |
| fem | FEM | Finite Element Model |
| apa | APA | Amplified Piezoelectric Actuator |
| dac | DAC | Digital to Analog Converter |
| rga | RGA | Relative Gain Array |
2024-03-19 15:30:11 +01:00
* Introduction :ignore:
2025-02-01 09:45:01 +01:00
Now that the nano-hexapod is mounted and that the the multi-body model of the nano-hexapod could be validated based on dynamics measurements, the complete NASS is mounted as shown in Figure ref:fig:test_id31_micro_station_nano_hexapod and the performances are evaluated on the ID31 beamline.
2024-11-13 10:27:34 +01:00
At the beginning of the project, it was planned to develop a long stroke 5-DoF metrology system to measure the pose of the sample with respect to the granite.
The development of such system was complex, and was not completed at the time of the experimental tests on ID31.
2025-02-01 09:45:01 +01:00
To still be able to validate the developed nano active platform and the associated instrumentation and control architecture, a 5-DoF short stroke metrology system is developed and presented in Section ref:sec:test_id31_metrology.
2024-11-13 10:27:34 +01:00
2025-02-01 09:45:01 +01:00
The identify dynamics of the nano-hexapod fixed on top of the micro-station is identified for different experimental conditions (payload masses, rotational velocities) and compared with the multi-body model in Section ref:sec:test_id31_open_loop_plant.
2024-11-13 10:27:34 +01:00
2025-02-01 09:45:01 +01:00
In order to apply the developed HAC-LAC architecture, decentralized Integral Force Feedback is first applied to actively damp the plant in a robust way (Section ref:sec:test_id31_iff), and the high authority controller is then implemented (Section ref:sec:test_id31_hac).
2024-11-13 10:27:34 +01:00
2025-02-01 09:45:01 +01:00
Finally, the positioning accuracy of the NASS is evaluated by performing scans corresponding to several scientific experiments (Section ref:sec:test_id31_experiments)
2024-11-13 10:27:34 +01:00
#+name : fig:test_id31_micro_station_nano_hexapod
#+caption : Picture of the micro-station without the nano-hexapod (\subref{fig:test_id31_micro_station_cables}) and with the nano-hexapod (\subref{fig:test_id31_fixed_nano_hexapod})
#+attr_latex : :options [htbp]
#+begin_figure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_micro_station_cables}Micro-station and nano-hexapod cables}
#+attr_latex : :options {0.49\textwidth}
#+begin_subfigure
#+attr_latex : :width 0.95\linewidth
[[file:figs/test_id31_micro_station_cables.jpg ]]
#+end_subfigure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_fixed_nano_hexapod}Nano-hexapod fixed on top of the micro-station}
#+attr_latex : :options {0.49\textwidth}
#+begin_subfigure
#+attr_latex : :width 0.95\linewidth
[[file:figs/test_id31_fixed_nano_hexapod.jpg ]]
#+end_subfigure
#+end_figure
2024-03-19 15:30:11 +01:00
* Short Stroke Metrology System
2024-11-13 17:11:40 +01:00
:PROPERTIES:
:header-args:matlab+: :tangle matlab/test_id31_1_metrology.m
:END:
2024-11-13 10:27:34 +01:00
<<sec:test_id31_metrology >>
2024-03-19 15:30:11 +01:00
** Introduction :ignore:
The control of the nano-hexapod requires an external metrology system measuring the relative position of the nano-hexapod top platform with respect to the granite.
2024-11-13 10:27:34 +01:00
As the long-stroke ($\approx 1 \,cm^3$) metrology system was not developed yet, a stroke stroke ($> 100\,\mu m^3$) was used instead to validate the nano-hexapod control.
A first considered option was to use the "Spindle error analyzer" shown in Figure ref:fig:test_id31_lion.
This system comprises 5 capacitive sensors which are facing two reference spheres.
2025-02-01 09:45:01 +01:00
But as the gap between the capacitive sensors and the spheres is very small[fn: 1 ] , the risk of damaging the spheres and the capacitive sensors is too high.
2024-11-13 10:27:34 +01:00
#+name : fig:test_id31_short_stroke_metrology
#+caption : Short stroke metrology system used to measure the sample position with respect to the granite in 5DoF. The system is based on a "Spindle error analyzer" (\subref{fig:test_id31_lion}), but the capacitive sensors are replaced with fibered interferometers (\subref{fig:test_id31_interf}). Interferometer heads are shown in (\subref{fig:test_id31_interf_head})
#+attr_latex : :options [htbp]
#+begin_figure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_lion}Capacitive Sensors}
#+attr_latex : :options {0.33\textwidth}
#+begin_subfigure
#+attr_latex : :width 0.9\linewidth
[[file:figs/test_id31_lion.jpg ]]
#+end_subfigure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_interf}Short-Stroke metrology}
#+attr_latex : :options {0.33\textwidth}
#+begin_subfigure
#+attr_latex : :width 0.9\linewidth
[[file:figs/test_id31_interf.jpg ]]
#+end_subfigure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_interf_head}Interferometer head}
#+attr_latex : :options {0.33\textwidth}
#+begin_subfigure
#+attr_latex : :width 0.9\linewidth
[[file:figs/test_id31_interf_head.jpg ]]
#+end_subfigure
#+end_figure
Instead of using capacitive sensors, 5 fibered interferometers were used in a similar way (Figure ref:fig:test_id31_interf).
At the end of each fiber, a sensor head[fn: 2 ] (Figure ref:fig:test_id31_interf_head) is used, which consists of a lens precisely positioned with respect to the fiber's end.
2025-02-01 09:45:01 +01:00
The lens is focusing the light on the surface of the sphere, such that the reflected light comes back into the fiber and produces an interference.
This way, the gap between the head and the reference sphere is much larger (here around $40\,mm$), removing the risk of collision.
2024-11-13 10:27:34 +01:00
2025-02-01 09:45:01 +01:00
Nevertheless, the metrology system still has limited measurement range due to limited angular acceptance of the fibered interferometers.
Indeed, when the spheres are moving perpendicularly to the beam axis, the reflected light does not coincide with the incident light, and above some perpendicular displacement, the reflected light does not comes back into the fiber, and no interference is produced.
2024-03-19 15:30:11 +01:00
** 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
2024-11-13 10:27:34 +01:00
** Metrology Kinematics
<<ssec:test_id31_metrology_kinematics >>
2024-03-19 15:30:11 +01:00
2024-11-13 10:27:34 +01:00
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$.
2025-02-01 09:45:01 +01:00
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.
2024-03-19 15:30:11 +01:00
2024-11-13 10:27:34 +01:00
\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
2024-03-19 15:30:11 +01:00
\end{equation}
2024-11-13 10:27:34 +01:00
#+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: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
2025-02-01 09:45:01 +01:00
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.
2024-11-13 10:27:34 +01:00
\begin{equation}\label{eq:test_id31_metrology_kinematics_inverse}
2024-03-19 15:30:11 +01:00
\begin{bmatrix}
2024-11-13 10:27:34 +01:00
D_x \\ D_y \\ D_z \\ R_x \\ R_y
2025-02-01 09:45:01 +01:00
\end{bmatrix} = {\underbrace{\begin{bmatrix}
2024-03-19 15:30:11 +01:00
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
2025-02-01 09:45:01 +01:00
\end{bmatrix}}_{\bm{J_d}}}^{-1} \cdot \begin{bmatrix}
2024-03-19 15:30:11 +01:00
d_1 \\ d_2 \\ d_3 \\ d_4 \\ d_5
\end{bmatrix}
\end{equation}
#+begin_src matlab
2024-11-13 10:27:34 +01:00
%% Geometrical parameters of the metrology system
2024-03-19 15:30:11 +01:00
H = 150e-3;
l1 = (150-48-42)*1e-3;
l2 = (76.2+48+42-150)*1e-3;
2024-11-13 10:27:34 +01:00
% Computation of the Transformation matrix
2024-03-19 15:30:11 +01:00
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
2024-11-13 10:27:34 +01:00
** Rough alignment of the reference spheres
<<ssec:test_id31_metrology_sphere_rought_alignment >>
2024-03-19 15:30:11 +01:00
2024-11-13 10:27:34 +01:00
The two reference spheres are aligned with the rotation axis of the spindle.
To do so, two measuring probes are used as shown in Figure ref:fig:align_top_sphere_comparators.
2024-03-19 15:30:11 +01:00
2024-11-13 10:27:34 +01:00
To not damage the sensitive sphere surface, the probes are instead positioned on the cylinder on which the sphere is mounted.
2025-02-01 09:45:01 +01:00
First, the probes are fixed to the bottom (fixed) cylinder to align the first sphere with the spindle axis.
2024-11-13 10:27:34 +01:00
Then, the probes are fixed to the top (adjustable) cylinder, and the same alignment is performed.
2024-03-19 15:30:11 +01:00
2025-02-01 09:45:01 +01:00
With this setup, the alignment accuracy of both spheres with the spindle axis is expected to around $10\,\mu m$.
The accuracy is probably limited due to the poor coaxiality between the cylinders and the spheres.
However, this first alignment should permit to position the two sphere within the acceptance of the interferometers.
2024-03-19 15:30:11 +01:00
2024-11-13 10:27:34 +01:00
** Tip-Tilt adjustment of the interferometers
<<ssec:test_id31_metrology_alignment >>
2025-02-01 09:45:01 +01:00
The short-stroke metrology system is placed on top of the main granite using a gantry made of granite blocs (Figure ref:fig:short_stroke_metrology_overview).
Granite is used to have good vibration and thermal stability.
2024-11-13 10:27:34 +01:00
#+name : fig:short_stroke_metrology_overview
#+caption : Granite gantry used to fix the short-stroke metrology system
#+attr_latex : :width 0.8\linewidth
[[file:figs/test_id31_short_stroke_metrology_overview.jpg ]]
2025-02-01 09:45:01 +01:00
The interferometer beams need to be position with respect to the two reference spheres as close as possible to the ideal case shown in Figure ref:fig:test_id31_metrology_kinematics.
This means that their positions and angles needs to be well adjusted with respect to the two spheres.
First, the vertical position of the spheres is adjusted using the micro-hexapod to match the height of the interferometers.
2024-11-13 10:27:34 +01:00
Then, the horizontal position of the gantry is adjusted such that the coupling efficiency (i.e. the intensity of the light reflected back in the fiber) of the top interferometer is maximized.
2025-02-01 09:45:01 +01:00
This is equivalent as to optimize the perpendicularity between the interferometer beam and the sphere surface (i.e. the concentricity between the top beam and the sphere center).
The lateral sensor heads (i.e. all except the top one) are each fixed to a custom tip-tilt adjustment mechanism.
This allow to individually orient them such that they all point to the spheres' center (i.e. perpendicular to the sphere surface).
This is done by maximizing the coupling efficiency of each interferometer.
2024-11-13 10:27:34 +01:00
2025-02-01 09:45:01 +01:00
After the alignment procedure, the top interferometer should coincide with with spindle axis, and the lateral interferometers should all be in the horizontal plane and intersect the spheres' center.
2024-11-13 10:27:34 +01:00
** Fine Alignment of reference spheres using interferometers
<<ssec:test_id31_metrology_sphere_fine_alignment >>
2025-02-01 09:45:01 +01:00
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.
2024-11-13 10:27:34 +01:00
The alignment process is made by few iterations.
First, the spindle is scanned and the alignment errors are recorded.
2025-02-01 09:45:01 +01:00
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.
2024-11-13 10:27:34 +01:00
Then, the spindle is scanned again, and the new alignment errors are recorded.
2025-02-01 09:45:01 +01:00
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.
2024-03-19 15:30:11 +01:00
#+begin_src matlab
2024-11-13 10:27:34 +01:00
%% Angular alignment
% Load Data
2024-03-19 15:30:11 +01:00
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);
2024-11-13 10:27:34 +01:00
% Offset wrong points
2024-03-19 15:30:11 +01:00
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);
2024-11-13 10:27:34 +01:00
% Compute circle fit and get radius
2024-03-19 15:30:11 +01:00
[~, ~ , 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);
#+end_src
#+begin_src matlab :exports none :results none
%% 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, '-', ...
2024-11-13 10:27:34 +01:00
'DisplayName', sprintf('$R_0 = %.0f \\mu$rad', R_it0))
2024-03-19 15:30:11 +01:00
plot(1e6*data_it1.Rx_int_filtered, 1e6*data_it1.Ry_int_filtered, '-', ...
2024-11-13 10:27:34 +01:00
'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))
2024-03-19 15:30:11 +01:00
hold off;
xlabel('$R_x$ [$\mu$rad]'); ylabel('$R_y$ [$\mu$rad]');
axis equal
legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1);
2024-11-13 10:27:34 +01:00
xlim([-600, 300]);
ylim([-100, 800]);
2024-03-19 15:30:11 +01:00
#+end_src
2024-11-13 10:27:34 +01:00
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_metrology_align_rx_ry.pdf', 'width', 'half', 'height', 'normal');
2024-03-19 15:30:11 +01:00
#+end_src
#+begin_src matlab
2024-11-13 10:27:34 +01:00
%% Eccentricity alignment
% Load Data
2024-03-19 15:30:11 +01:00
data_it0 = h5scan(data_dir, 'alignment', 'h1rx_h1ry_0002', 5);
data_it1 = h5scan(data_dir, 'alignment', 'h1dx_h1dy', 1);
2024-11-13 10:27:34 +01:00
% Offset wrong points
2024-03-19 15:30:11 +01:00
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);
2024-11-13 10:27:34 +01:00
% Compute circle fit and get radius
2024-03-19 15:30:11 +01:00
[~, ~ , 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);
#+end_src
#+begin_src matlab :exports none :results none
%% 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, '-', ...
2024-11-13 10:27:34 +01:00
'DisplayName', sprintf('$R_0 = %.0f \\mu$m', R_it0))
2024-03-19 15:30:11 +01:00
plot(1e6*data_it1.Dx_int_filtered, 1e6*data_it1.Dy_int_filtered, '-', ...
2024-11-13 10:27:34 +01:00
'DisplayName', sprintf('$R_1 = %.0f \\mu$m', R_it1))
2024-03-19 15:30:11 +01:00
hold off;
xlabel('$D_x$ [$\mu$m]'); ylabel('$D_y$ [$\mu$m]');
axis equal
legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1);
2024-11-13 10:27:34 +01:00
xlim([-1, 21]);
ylim([-8, 14]);
2024-03-19 15:30:11 +01:00
#+end_src
#+begin_src matlab :tangle no :exports results :results file replace
2024-11-13 10:27:34 +01:00
exportFig('figs/test_id31_metrology_align_dx_dy.pdf', 'width', 'half', 'height', 'normal');
2024-03-19 15:30:11 +01:00
#+end_src
2024-11-13 10:27:34 +01:00
#+name : fig:test_id31_metrology_align
#+caption : Measured angular (\subref{fig:test_id31_metrology_align_rx_ry}) and lateral (\subref{fig:test_id31_metrology_align_dx_dy}) errors during a full spindle rotation. Between two rotations, the micro-hexapod is adjusted to better align the two spheres with the rotation axis.
#+attr_latex : :options [htbp]
#+begin_figure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_metrology_align_rx_ry}Angular alignment}
#+attr_latex : :options {0.49\textwidth}
#+begin_subfigure
#+attr_latex : :scale 1
[[file:figs/test_id31_metrology_align_rx_ry.png ]]
#+end_subfigure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_metrology_align_dx_dy}Lateral alignment}
#+attr_latex : :options {0.49\textwidth}
#+begin_subfigure
#+attr_latex : :scale 1
[[file:figs/test_id31_metrology_align_dx_dy.png ]]
#+end_subfigure
#+end_figure
2024-03-19 15:30:11 +01:00
2024-11-13 10:27:34 +01:00
** Estimated measurement volume
<<ssec:test_id31_metrology_acceptance >>
2024-03-19 15:30:11 +01:00
Because the interferometers are pointing to spheres and not flat surfaces, the lateral acceptance is limited.
2024-11-13 10:27:34 +01:00
In order to estimate the metrology acceptance, the micro-hexapod is used to perform three accurate scans of $\pm 1\,mm$, respectively along the the $x$, $y$ and $z$ axes.
2025-02-01 09:45:01 +01:00
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.
2024-11-13 10:27:34 +01:00
Results are summarized in Table ref:tab:test_id31_metrology_acceptance.
2025-02-01 09:45:01 +01:00
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.
2024-03-19 15:30:11 +01:00
#+begin_src matlab
2024-11-13 10:27:34 +01:00
%% Estimated acceptance of the metrology
% This is estimated by moving the spheres using the micro-hexapod
2024-03-19 15:30:11 +01:00
2024-11-13 10:27:34 +01:00
% Dx
data_dx = h5scan(data_dir, 'metrology_acceptance_new_align', 'dx', 1);
2024-03-19 15:30:11 +01:00
2024-11-13 10:27:34 +01:00
dx_acceptance = zeros(5,1);
2024-03-19 15:30:11 +01:00
2024-11-13 10:27:34 +01:00
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
2024-03-19 15:30:11 +01:00
2024-11-13 10:27:34 +01:00
% Dy
data_dy = h5scan(data_dir, 'metrology_acceptance_new_align', 'dy', 1);
2024-03-19 15:30:11 +01:00
2024-11-13 10:27:34 +01:00
dy_acceptance = zeros(5,1);
2024-03-19 15:30:11 +01:00
2024-11-13 10:27:34 +01:00
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
2024-03-19 15:30:11 +01:00
2024-11-13 10:27:34 +01:00
% Dz
data_dz = h5scan(data_dir, 'metrology_acceptance_new_align', 'dz', 1);
2024-03-19 15:30:11 +01:00
2024-11-13 10:27:34 +01:00
dz_acceptance = zeros(5,1);
2024-03-19 15:30:11 +01:00
2024-11-13 10:27:34 +01:00
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
2024-03-19 15:30:11 +01:00
#+end_src
2024-11-13 10:27:34 +01:00
#+begin_src matlab :exports results :results value table replace :tangle no :post addhdr(*this* )
% data2orgtable([dx_acceptance, dy_acceptance, dz_acceptance], {'$d_1$ (y)', '$d_2$ (y)', '$d_3$ (x)', '$d_4$ (x)', '$d_5$ (z)'}, {'$D_x$', '$D_y$', '$D_z$'}, ' %.2f ');
2024-03-19 15:30:11 +01:00
#+end_src
2024-11-13 10:27:34 +01:00
#+name : tab:test_id31_metrology_acceptance
#+caption : Estimated measurement range for each interferometer, and for three different directions.
#+attr_latex : :environment tabularx :width 0.45\linewidth :align Xccc
#+attr_latex : :center t :booktabs t
#+RESULTS :
| | $D_x$ | $D_y$ | $D_z$ |
|-----------+-------------+------------+-------|
| $d_1$ (y) | $1.0\,mm$ | $>2\,mm$ | $1.35\,mm$ |
| $d_2$ (y) | $0.8\,mm$ | $>2\,mm$ | $1.01\,mm$ |
| $d_3$ (x) | $>2\,mm$ | $1.06\,mm$ | $1.38\,mm$ |
| $d_4$ (x) | $>2\,mm$ | $0.99\,mm$ | $0.94\,mm$ |
| $d_5$ (z) | $1.33\, mm$ | $1.06\,mm$ | $>2\,mm$ |
2024-03-19 15:30:11 +01:00
2024-11-13 10:27:34 +01:00
** Estimated measurement errors
<<ssec:test_id31_metrology_errors >>
2024-03-19 15:30:11 +01:00
2025-02-01 09:45:01 +01:00
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.
2024-11-13 10:27:34 +01:00
Only the bandwidth and noise characteristics of the external metrology are important.
Yet, some elements effecting the accuracy of the metrology are discussed here.
2024-03-19 15:30:11 +01:00
2024-11-13 10:27:34 +01:00
First, the "metrology kinematics" (discussed in Section ref:ssec:test_id31_metrology_kinematics) is only approximate (i.e. valid for very small displacements).
2025-02-01 09:45:01 +01:00
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).
2024-11-13 10:27:34 +01:00
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.
2024-03-19 15:30:11 +01:00
2025-02-01 09:45:01 +01:00
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.
2024-03-19 15:30:11 +01:00
2025-02-01 09:45:01 +01:00
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.
2024-11-13 10:27:34 +01:00
Therefore, any variation of air temperature, pressure or humidity will induce measurement errors.
2025-02-01 09:45:01 +01:00
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$.
2024-03-19 15:30:11 +01:00
2025-02-01 09:45:01 +01:00
Interferometers are also affected by noise [[cite:&watchi18_review_compac_inter ]].
2024-11-13 10:27:34 +01:00
The effect of the noise on the translation and rotation measurements is estimated in Figure ref:fig:test_id31_interf_noise.
2024-03-19 15:30:11 +01:00
2024-11-13 10:27:34 +01:00
#+begin_src matlab
%% Interferometer noise estimation
data = load("test_id31_interf_noise.mat");
2024-03-19 15:30:11 +01:00
2024-11-13 10:27:34 +01:00
Ts = 1e-4;
Nfft = floor(5/Ts);
win = hanning(Nfft);
Noverlap = floor(Nfft/2);
2024-03-19 15:30:11 +01:00
2024-11-13 10:27:34 +01:00
[pxx_int, f] = pwelch(detrend(data.d, 0), win, Noverlap, Nfft, 1/Ts);
2024-03-19 15:30:11 +01:00
2024-11-13 10:27:34 +01:00
% Uncorrelated noise: square root of the sum of the squares
pxx_cart = pxx_int*sum(inv(Hm).^2, 2)';
2024-03-19 15:30:11 +01:00
2024-11-13 10:27:34 +01:00
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
2024-03-19 15:30:11 +01:00
#+end_src
#+begin_src matlab
figure;
hold on;
2024-11-13 10:27:34 +01:00
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;
2024-03-19 15:30:11 +01:00
#+end_src
2024-11-13 10:27:34 +01:00
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_interf_noise.pdf', 'width', 'half', 'height', 'normal');
2024-03-19 15:30:11 +01:00
#+end_src
2024-11-13 10:27:34 +01:00
#+begin_src matlab
%% X-Y scan with the micro-hexapod, and record of the vertical interferometer
data = h5scan(data_dir, 'metrology_acceptance', 'after_int_align_meshXY', 1);
2024-03-19 15:30:11 +01:00
2024-11-13 10:27:34 +01:00
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]
2024-03-19 15:30:11 +01:00
2024-11-13 10:27:34 +01:00
mdl = scatteredInterpolant(x, y, z);
[xg, yg] = meshgrid(unique(x), unique(y));
zg = mdl(xg, yg);
2024-03-19 15:30:11 +01:00
2024-11-13 10:27:34 +01:00
% Fit a sphere to the data
[sphere_center,sphere_radius] = sphereFit(1e-3*[x, y, z]);
2024-03-19 15:30:11 +01:00
#+end_src
#+begin_src matlab :exports none :results none
2024-11-13 10:27:34 +01:00
%% XY mapping of the Z measurement by the interferometer
2024-03-19 15:30:11 +01:00
figure;
2024-11-13 10:27:34 +01:00
[~,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);
#+end_src
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_xy_map_sphere.pdf', 'width', 'half', 'height', 'normal');
#+end_src
#+name : fig:test_id31_metrology_errors
#+caption : Estimated measurement errors of the metrology. Cross-coupling between lateral motion and vertical measurement is shown in (\subref{fig:test_id31_xy_map_sphere}). Effect of interferometer noise on the measured translations and rotations is shown in (\subref{fig:test_id31_interf_noise}).
#+attr_latex : :options [htbp]
#+begin_figure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_xy_map_sphere}Z measurement during an XY mapping}
#+attr_latex : :options {0.49\textwidth}
#+begin_subfigure
#+attr_latex : :width 0.95\linewidth
[[file:figs/test_id31_xy_map_sphere.png ]]
#+end_subfigure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_interf_noise}Interferometer noise}
#+attr_latex : :options {0.49\textwidth}
#+begin_subfigure
#+attr_latex : :width 0.95\linewidth
[[file:figs/test_id31_interf_noise.png ]]
#+end_subfigure
#+end_figure
2024-03-19 15:30:11 +01:00
2025-02-01 10:37:12 +01:00
* Open Loop Plant
2024-11-13 17:11:40 +01:00
:PROPERTIES:
:header-args:matlab+: :tangle matlab/test_id31_2_open_loop_plant.m
:END:
<<sec:test_id31_open_loop_plant >>
** Introduction :ignore:
2024-03-19 15:30:11 +01:00
2025-02-01 10:37:12 +01:00
The NASS plant is schematically shown in Figure ref:fig:test_id31_block_schematic_plant.
The input $\bm{u} = [u_1,\ u_2,\ u_3,\ u_4,\ u_5,\ u_6]$ is the command signal and corresponds to the voltages generated for each piezoelectric actuator.
After amplification, the voltages across the piezoelectric stack actuators are $\bm{V}_a = [V_ {a1},\ V_{a2},\ V_ {a3},\ V_{a4},\ V_ {a5},\ V_{a6}]$.
2025-02-01 09:45:01 +01:00
2025-02-01 10:37:12 +01:00
From the setpoint of micro-station stages ($r_{D_y}$ for the translation stage, $r_ {R_y}$ for the tilt stage and $r_ {R_z}$ for the spindle), the reference pose of the sample $\bm{r}_ {\mathcal{X}}$ is computed using the micro-station's kinematics.
The sample's position $\bm{y}_\mathcal{X} = [D_x,\,D_y,\,D_z,\,R_x,\,R_y,\,R_z]$ is measured using multiple sensors.
First, the five interferometers $\bm{d} = [d_{1},\ d_ {2},\ d_{3},\ d_ {4},\ d_{5}]$ are used to measure the $[D_x,\,D_y,\,D_z,\,R_x,\,R_y]$ degrees of freedom of the sample.
The $R_z$ position of the sample is computed from the spindle's setpoint $r_ {R_z}$ and from the 6 encoders $\bm{d}_e$ integrated in the nano-hexapod.
2025-02-01 09:45:01 +01:00
2025-02-01 10:37:12 +01:00
The sample's position $\bm{y}_{\mathcal{X}}$ is compared to the reference position $\bm{r}_ {\mathcal{X}}$ to compute the position error in the frame of the (rotating) nano-hexapod $\bm{\epsilon\mathcal{X}} = [\epsilon_{D_x},\,\epsilon_ {D_y},\,\epsilon_ {D_z},\,\epsilon_ {R_x},\,\epsilon_ {R_y},\,\epsilon_ {R_z}]$.
Finally, the Jacobian matrix $\bm{J}$ of the nano-hexapod is used to map $\bm{\epsilon\mathcal{X}}$ in the frame of the nano-hexapod struts $\bm{\epsilon\mathcal{L}} = [\epsilon_{\mathcal{L}_1},\,\epsilon_ {\mathcal{L}_2},\,\epsilon_ {\mathcal{L}_3},\,\epsilon_ {\mathcal{L}_4},\,\epsilon_ {\mathcal{L}_5},\,\epsilon_ {\mathcal{L}_6}]$.
Voltages generated by the force sensor piezoelectric stacks $\bm{V}_s = [V_ {s1},\ V_{s2},\ V_ {s3},\ V_{s4},\ V_ {s5},\ V_{s6}]$ will be used for active damping.
2024-11-15 18:15:13 +01:00
#+begin_src latex :file test_id31_block_schematic_plant.pdf
\begin{tikzpicture}
% Blocs
\node[block={2.0cm}{1.0cm}] (metrology) {Metrology};
\node[block={2.0cm}{2.0cm}, below=0.1 of metrology, align=center] (nhexa) {Nano\\Hexapod};
\node[block={4.0cm}{1.5cm}, below=0.1 of nhexa, align=center] (ustation) {Micro\\Station};
\coordinate[] (inputVa) at ($(nhexa.south west)!0.5!(nhexa.north west)$);
\coordinate[] (outputVs) at ($(nhexa.south east)!0.3!(nhexa.north east)$);
\coordinate[] (outputde) at ($(nhexa.south east)!0.7!(nhexa.north east)$);
\coordinate[] (outputDy) at ($(ustation.south east)!0.1!(ustation.north east)$);
\coordinate[] (outputRy) at ($(ustation.south east)!0.5!(ustation.north east)$);
\coordinate[] (outputRz) at ($(ustation.south east)!0.9!(ustation.north east)$);
\node[block={1.0cm}{1.0cm}, left=0.8 of inputVa] (pd200) {PD200};
\node[block={1.0cm}{1.0cm}, right=0.8 of outputde] (Rz_kinematics) {$\bm{J}_ {R_z}^{-1}$};
\node[block={2.0cm}{2.0cm}, right=2.2 of ustation, align=center] (ustation_kinematics) {Compute\\Reference\\Position};
\node[block={2.0cm}{2.0cm}, right=0.8 of ustation_kinematics, align=center] (compute_error) {Compute\\Error\\Position};
\node[block={2.0cm}{2.0cm}, above=0.8 of compute_error, align=center] (compute_pos) {Compute\\Sample\\Position};
\node[block={1.0cm}{1.0cm}, right=0.8 of compute_error] (hexa_jacobian) {$\bm{J}$};
\node[block={1.0cm}{1.0cm}, right=0.8 of metrology] (metrology_kinematics) {$\bm{J}_d^{-1}$};
\coordinate[] (inputMetrology) at ($(compute_error.north east)!0.3!(compute_error.north west)$);
\coordinate[] (inputRz) at ($(compute_error.north east)!0.7!(compute_error.north west)$);
\node[addb={+}{}{}{}{}, right=0.4 of Rz_kinematics] (addRz) {};
\draw[->] (Rz_kinematics.east) -- (addRz.west);
\draw[->] (outputRz-|addRz)node[branch]{} -- (addRz.south);
\draw[->] (outputDy) node[above right]{$r_{D_y}$} -- (outputDy-|ustation_kinematics.west);
\draw[->] (outputRy) node[above right]{$r_{R_y}$} -- (outputRy-|ustation_kinematics.west);
\draw[->] (outputRz) node[above right]{$r_{R_z}$} -- (outputRz-|ustation_kinematics.west);
2025-02-01 12:11:25 +01:00
% \draw[->] (outputVs) -- ++(0.8, 0) node[above left]{$\bm{V}_s$};
2024-11-15 18:15:13 +01:00
\draw[->] (metrology.east) -- (metrology_kinematics.west) node[above left]{$\bm{d}$};
\draw[->] (metrology_kinematics.east)node[above right]{$[D_x,\,D_y,\,D_z,\,R_x,\,R_y]$} -- (compute_pos.west|-metrology_kinematics);
\draw[->] (addRz.east)node[above right]{$R_z$} -- (compute_pos.west|-addRz);
\draw[->] (compute_pos.south)node -- (compute_error.north)node[above right]{$\bm{y}_ {\mathcal{X}}$};
\draw[->] (outputde) -- (Rz_kinematics.west) node[above left]{$\bm{d}_ {e}$};
\draw[->] (ustation_kinematics.east) -- (compute_error.west) node[above left]{$\bm{r}_ {\mathcal{X}}$};
\draw[->] (compute_error.east) -- (hexa_jacobian.west) node[above left]{$\bm{\epsilon\mathcal{X}}$};
2025-02-01 12:11:25 +01:00
\draw[->] (hexa_jacobian.east) -- ++(0.8, 0)coordinate(eL) node[above left]{$\bm{\epsilon\mathcal{L}}$};
2024-11-15 18:15:13 +01:00
\draw[->] (pd200.east) -- (inputVa) node[above left]{$\bm{V}_a$};
\draw[<-] (pd200.west) -- ++(-0.8, 0) node[above right]{$\bm{u}$};
2025-02-01 12:11:25 +01:00
\draw[->] (outputVs) -- (outputVs-|eL) node[above left]{$\bm{V}_s$};
\begin{scope}[on background layer]
\node[fit={(metrology.north-|pd200.west) (hexa_jacobian.east|-compute_error.south)}, fill=black!20!white, draw, dashed, inner sep=4pt] (plant) {};
\node[anchor={north west}] at (plant.north west){$\text{Plant}$};
\end{scope}
2024-11-15 18:15:13 +01:00
\end{tikzpicture}
#+end_src
#+name : fig:test_id31_block_schematic_plant
2025-02-01 10:37:12 +01:00
#+caption : Schematic of the NASS plant
2024-11-15 18:15:13 +01:00
#+RESULTS :
[[file:figs/test_id31_block_schematic_plant.png ]]
2024-11-13 17:11:40 +01:00
** Matlab Init :noexport:ignore:
#+begin_src matlab
%% test_id31_2_open_loop_plant.m
#+end_src
2024-03-19 15:30:11 +01:00
2024-11-13 17:11:40 +01:00
#+begin_src matlab :tangle no :exports none :results silent :noweb yes :var current_dir=(file-name-directory buffer-file-name)
<<matlab-dir >>
#+end_src
2024-03-19 15:30:11 +01:00
2024-11-13 17:11:40 +01:00
#+begin_src matlab :exports none :results silent :noweb yes
<<matlab-init >>
#+end_src
2024-03-19 15:30:11 +01:00
2024-11-13 17:11:40 +01:00
#+begin_src matlab :tangle no :noweb yes
<<m-init-path >>
#+end_src
2024-03-19 15:30:11 +01:00
2024-11-13 17:11:40 +01:00
#+begin_src matlab :eval no :noweb yes
<<m-init-path-tangle >>
#+end_src
2024-03-19 15:30:11 +01:00
2024-11-13 17:11:40 +01:00
#+begin_src matlab :noweb yes
<<m-init-simscape >>
#+end_src
2024-03-19 15:30:11 +01:00
2024-11-13 17:11:40 +01:00
#+begin_src matlab :noweb yes
<<m-init-other >>
#+end_src
2025-02-01 10:37:12 +01:00
** Open-Loop Plant Identification
2024-11-13 17:11:40 +01:00
<<ssec:test_id31_open_loop_plant_first_id >>
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).
2025-02-01 10:37:12 +01:00
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).
2024-11-15 18:15:13 +01:00
The experimental time delay estimated from the FRF (Figure ref:fig:test_id31_first_id_int) is larger than expected.
2025-02-01 10:37:12 +01:00
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.
2024-11-15 18:15:13 +01:00
This issue was later solved.
2024-03-19 15:30:11 +01:00
#+begin_src matlab
2024-11-13 17:11:40 +01:00
%% 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;
2024-11-15 18:15:13 +01:00
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]
2024-11-13 17:11:40 +01:00
% With no payload
2024-11-15 18:15:13 +01:00
Gm = exp(-1e-4*s)*linearize(mdl, io);
2024-11-13 17:11:40 +01:00
Gm.InputName = {'u1', 'u2', 'u3', 'u4', 'u5', 'u6'};
2024-11-15 18:15:13 +01:00
Gm.OutputName = {'Vs1', 'Vs2', 'Vs3', 'Vs4', 'Vs5', 'Vs6', ...
2024-11-13 17:11:40 +01:00
'eL1', 'eL2', 'eL3', 'eL4', 'eL5', 'eL6'};
#+end_src
2024-03-19 15:30:11 +01:00
2024-11-13 17:11:40 +01:00
#+begin_src matlab
%% Identify the plant from experimental data
2024-03-19 15:30:11 +01:00
2024-11-13 17:11:40 +01:00
% Load identification data
data = load('2023-08-08_16-17_ol_plant_m0_Wz0.mat');
2024-03-19 15:30:11 +01:00
2024-11-15 18:15:13 +01:00
% Frequency analysis parameters
Ts = 1e-4; % Sampling Time [s]
2024-11-13 17:11:40 +01:00
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);
2024-03-19 15:30:11 +01:00
2024-11-15 18:15:13 +01:00
G_iff = zeros(length(f), 6, 6); % Force sensor outputs
G_int = zeros(length(f), 6, 6); % Estimated strut errors
2024-11-13 17:11:40 +01:00
for i_strut = 1:6
2024-11-15 18:15:13 +01:00
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]';
2024-11-13 17:11:40 +01:00
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]';
2024-11-15 18:15:13 +01:00
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]';
2024-03-19 15:30:11 +01:00
2024-11-15 18:15:13 +01:00
G_iff(:,:,i_strut) = tfestimate(data.(sprintf("uL%i", i_strut)).id_plant, Vs, win, Noverlap, Nfft, 1/Ts);
2024-11-13 17:11:40 +01:00
G_int(:,:,i_strut) = tfestimate(data.(sprintf("uL%i", i_strut)).id_plant, eL, win, Noverlap, Nfft, 1/Ts);
end
2024-03-19 15:30:11 +01:00
#+end_src
2024-11-13 17:11:40 +01:00
#+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_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,:)], ...
2024-11-15 18:15:13 +01:00
'DisplayName', '$-e\mathcal{L}_i/u_i$ meas');
2024-11-13 17:11:40 +01:00
plot(freqs, abs(squeeze(freqresp(Gm(sprintf('eL%i', 1), sprintf('u%i', 1)), freqs, 'Hz'))), 'color', [colors(2,:)], ...
2024-11-15 18:15:13 +01:00
'DisplayName', '$-e\mathcal{L}_i/u_i$ model');
2024-11-13 17:11:40 +01:00
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], ...
2024-11-15 18:15:13 +01:00
'DisplayName', '$-e\mathcal{L}_i/u_j$ meas');
2024-11-13 17:11:40 +01:00
plot(freqs, abs(squeeze(freqresp(Gm(sprintf('eL%i', 1), sprintf('u%i', 2)), freqs, 'Hz'))), 'color', [colors(2,:), 0.2], ...
2024-11-15 18:15:13 +01:00
'DisplayName', '$-e\mathcal{L}_i/u_j$ model');
2024-11-13 17:11:40 +01:00
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]);
#+end_src
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_first_id_int.pdf', 'width', 'half', 'height', 600);
#+end_src
#+begin_src matlab :exports none :results none
%% 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');
2024-11-15 18:15:13 +01:00
plot(freqs, abs(squeeze(freqresp(Gm(sprintf('Vs%i', i), sprintf('u%i', j)), freqs, 'Hz'))), 'color', [colors(2,:), 0.2], ...
2024-11-13 17:11:40 +01:00
'HandleVisibility', 'off');
end
end
plot(f, abs(G_iff(:,1, 1)), 'color', [colors(1,:)], ...
2024-11-15 18:15:13 +01:00
'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');
2024-11-13 17:11:40 +01:00
for i = 2:6
plot(f, abs(G_iff(:,i, i)), 'color', [colors(1,:)], ...
'HandleVisibility', 'off');
2024-11-15 18:15:13 +01:00
plot(freqs, abs(squeeze(freqresp(Gm(sprintf('Vs%i', i), sprintf('u%i', i)), freqs, 'Hz'))), 'color', [colors(2,:)], ...
2024-11-13 17:11:40 +01:00
'HandleVisibility', 'off');
end
plot(f, abs(G_iff(:, 1, 2)), 'color', [colors(1,:), 0.2], ...
2024-11-15 18:15:13 +01:00
'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');
2024-11-13 17:11:40 +01:00
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log');
ylabel('Amplitude [V/V]'); set(gca, 'XTickLabel',[]);
2024-11-15 18:15:13 +01:00
ylim([5e-5, 4e1]);
2024-11-13 17:11:40 +01:00
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
2024-11-15 18:15:13 +01:00
plot(freqs, 180/pi*angle(squeeze(freqresp(Gm(sprintf('Vs%i', i), sprintf('u%i', i)), freqs, 'Hz'))), 'color', [colors(2,:)]);
2024-11-13 17:11:40 +01:00
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 none
exportFig('figs/test_id31_first_id_iff.pdf', 'width', 'half', 'height', 600);
#+end_src
#+name : fig:test_id31_first_id
2025-02-01 10:37:12 +01:00
#+caption : Comparison between the measured dynamics and the multi-body model dynamics. Both for the external metrology (\subref{fig:test_id31_first_id_int}) and force sensors (\subref{fig:test_id31_first_id_iff}). Direct terms are displayed with solid lines while off-diagonal (i.e. coupling) terms are shown with shaded lines.
2024-11-13 17:11:40 +01:00
#+attr_latex : :options [htbp]
#+begin_figure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_first_id_int}External Metrology}
#+attr_latex : :options {0.49\textwidth}
#+begin_subfigure
#+attr_latex : :width 0.95\linewidth
[[file:figs/test_id31_first_id_int.png ]]
#+end_subfigure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_first_id_iff}Force Sensors}
#+attr_latex : :options {0.49\textwidth}
#+begin_subfigure
#+attr_latex : :width 0.95\linewidth
[[file:figs/test_id31_first_id_iff.png ]]
#+end_subfigure
#+end_figure
** Better Angular Alignment
<<ssec:test_id31_open_loop_plant_rz_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.
2025-02-01 10:37:12 +01:00
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.
2024-11-13 17:11:40 +01:00
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.
2025-02-01 10:37:12 +01:00
After alignment, the same movement was performed using the nano-hexapod while recording the signal of the external metrology.
2024-11-13 17:11:40 +01:00
Results shown in Figure ref:fig:test_id31_Rz_align_correct are indeed indicating much better alignment.
2024-11-13 10:27:34 +01:00
#+begin_src matlab
2024-11-13 17:11:40 +01:00
%% 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);
#+end_src
#+begin_src matlab
% 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
#+end_src
#+begin_src matlab
%% 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')
2024-11-15 18:15:13 +01:00
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))
2024-11-13 17:11:40 +01:00
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;
#+end_src
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_Rz_align_error.pdf', 'width', 'half', 'height', 'normal');
#+end_src
#+begin_src matlab
% 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
#+end_src
#+begin_src matlab
%% 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')
2024-11-15 18:15:13 +01:00
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))
2024-11-13 17:11:40 +01:00
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;
#+end_src
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_Rz_align_correct.pdf', 'width', 'half', 'height', 'normal');
#+end_src
#+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
2025-02-01 10:37:12 +01:00
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.
2024-11-15 18:15:13 +01:00
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).
2025-01-31 16:33:25 +01:00
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.
2024-11-13 17:11:40 +01:00
#+begin_src matlab
%% 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
#+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(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', '$e\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', '$e\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', '$e\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', '$e\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;
#+end_src
#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/test_id31_first_id_int_better_rz_align.pdf', 'width', 'wide', 'height', 'normal');
#+end_src
#+name : fig:test_id31_first_id_int_better_rz_align
#+caption : Decrease of the coupling with better Rz alignment
#+RESULTS :
[[file:figs/test_id31_first_id_int_better_rz_align.png ]]
** Effect of Payload Mass
<<ssec:test_id31_open_loop_plant_mass >>
2025-02-01 10:37:12 +01:00
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.
2024-11-13 17:11:40 +01:00
The obtained direct terms are compared with the model dynamics in Figure ref:fig:test_nhexa_comp_simscape_diag_masses.
2025-02-01 10:37:12 +01:00
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.
2024-11-13 17:11:40 +01:00
2024-11-15 18:15:13 +01:00
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).
2024-11-13 17:11:40 +01:00
#+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
2024-11-15 18:15:13 +01:00
#+begin_src matlab :exports none
%% 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'};
#+end_src
2024-11-13 17:11:40 +01:00
#+begin_src matlab
%% 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
#+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;
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])
#+end_src
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_comp_simscape_int_diag_masses.pdf', 'width', 'half', 'height', 600);
#+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;
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
2024-11-15 18:15:13 +01:00
plot(freqs, abs(squeeze(freqresp(Gm_m0_Wz0('Vs1', 'u1'), freqs, 'Hz'))), '--', 'color', colors(1,:), ...
2024-11-13 17:11:40 +01:00
'DisplayName', 'Model (0kg)');
2024-11-15 18:15:13 +01:00
plot(freqs, abs(squeeze(freqresp(Gm_m1_Wz0('Vs1', 'u1'), freqs, 'Hz'))), '--', 'color', colors(2,:), ...
2024-11-13 17:11:40 +01:00
'DisplayName', 'Model (13kg)');
2024-11-15 18:15:13 +01:00
plot(freqs, abs(squeeze(freqresp(Gm_m2_Wz0('Vs1', 'u1'), freqs, 'Hz'))), '--', 'color', colors(3,:), ...
2024-11-13 17:11:40 +01:00
'DisplayName', 'Model (26kg)');
2024-11-15 18:15:13 +01:00
plot(freqs, abs(squeeze(freqresp(Gm_m3_Wz0('Vs1', 'u1'), freqs, 'Hz'))), '--', 'color', colors(4,:), ...
2024-11-13 17:11:40 +01:00
'DisplayName', 'Model (39kg)');
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log');
ylabel('Amplitude [V/V]'); set(gca, 'XTickLabel',[]);
2024-11-15 18:15:13 +01:00
ylim([1e-2, 4e1]);
2024-11-13 17:11:40 +01:00
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
2024-11-15 18:15:13 +01:00
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,:))
2024-11-13 17:11:40 +01:00
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])
#+end_src
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_comp_simscape_iff_diag_masses.pdf', 'width', 'half', 'height', 600);
#+end_src
#+name : fig:test_nhexa_comp_simscape_diag_masses
2025-01-31 16:33:25 +01:00
#+caption : Comparison of the diagonal elements (i.e. "direct" terms) of the measured FRF matrix and the dynamics identified from the multi-body model. Both for the dynamics from $u$ to $e\mathcal{L}$ (\subref{fig:test_id31_comp_simscape_int_diag_masses}) and from $u$ to $V_s$ (\subref{fig:test_id31_comp_simscape_iff_diag_masses})
2024-11-13 17:11:40 +01:00
#+attr_latex : :options [htbp]
#+begin_figure
2024-11-15 18:15:13 +01:00
#+attr_latex : :caption \subcaption{\label{fig:test_id31_comp_simscape_int_diag_masses}from $u$ to $e\mathcal{L}$}
2024-11-13 17:11:40 +01:00
#+attr_latex : :options {0.49\textwidth}
#+begin_subfigure
#+attr_latex : :width 0.95\linewidth
[[file:figs/test_id31_comp_simscape_int_diag_masses.png ]]
#+end_subfigure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_comp_simscape_iff_diag_masses}from $u$ to $V_s$}
#+attr_latex : :options {0.49\textwidth}
#+begin_subfigure
#+attr_latex : :width 0.95\linewidth
[[file:figs/test_id31_comp_simscape_iff_diag_masses.png ]]
#+end_subfigure
#+end_figure
** Effect of Spindle Rotation
<<ssec:test_id31_open_loop_plant_rotation >>
2025-02-01 10:37:12 +01:00
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$.
2024-11-13 17:11:40 +01:00
The comparison of the obtained dynamics from command signal $u$ to estimated strut error $e\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.
#+begin_src matlab
%% Identify the model dynamics with Spindle rotation
initializeSample('type', '0');
initializeReferences(...
'Rz_type', 'rotating', ...
2024-11-15 18:15:13 +01:00
'Rz_period', 360/36); % 36 deg/s, 6rpm
2024-11-13 17:11:40 +01:00
Gm_m0_Wz36 = linearize(mdl, io, 0.1);
Gm_m0_Wz36.InputName = {'u1', 'u2', 'u3', 'u4', 'u5', 'u6'};
2024-11-15 18:15:13 +01:00
Gm_m0_Wz36.OutputName = {'Vs1', 'Vs2', 'Vs3', 'Vs4', 'Vs5', 'Vs6', ...
2024-11-13 17:11:40 +01:00
'eL1', 'eL2', 'eL3', 'eL4', 'eL5', 'eL6'};
initializeReferences(...
'Rz_type', 'rotating', ...
2024-11-15 18:15:13 +01:00
'Rz_period', 360/180); % 180 deg/s, 30rpm
2024-11-13 17:11:40 +01:00
Gm_m0_Wz180 = linearize(mdl, io, 0.1);
Gm_m0_Wz180.InputName = {'u1', 'u2', 'u3', 'u4', 'u5', 'u6'};
2024-11-15 18:15:13 +01:00
Gm_m0_Wz180.OutputName = {'Vs1', 'Vs2', 'Vs3', 'Vs4', 'Vs5', 'Vs6', ...
2024-11-13 17:11:40 +01:00
'eL1', 'eL2', 'eL3', 'eL4', 'eL5', 'eL6'};
#+end_src
#+begin_src matlab
%% 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
#+end_src
2024-11-15 18:15:13 +01:00
#+begin_src matlab :exports none :tangle no
% The identified dynamics are then saved for further use.
save('./matlab/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('./matlab/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');
#+end_src
#+begin_src matlab :eval no
% 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');
#+end_src
2024-11-13 17:11:40 +01:00
#+begin_src matlab :exports none :results none
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])
#+end_src
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_effect_rotation_direct.pdf', 'width', 'half', 'height', 'short');
#+end_src
#+begin_src matlab :exports none :results none
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])
#+end_src
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_effect_rotation_coupling.pdf', 'width', 'half', 'height', 'short');
#+end_src
#+name : fig:test_id31_effect_rotation
#+caption : Effect of the spindle rotation on the plant dynamics from $u$ to $e\mathcal{L}$. Three rotational velocities are tested ($0\,\text{deg}/s$, $36\,\text{deg}/s$ and $180\,\text{deg}/s$). Both direct terms (\subref{fig:test_id31_effect_rotation_direct}) and coupling terms (\subref{fig:test_id31_effect_rotation_coupling}) are displayed.
#+attr_latex : :options [htbp]
#+begin_figure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_effect_rotation_direct}Direct terms}
#+attr_latex : :options {0.49\textwidth}
#+begin_subfigure
#+attr_latex : :width 0.95\linewidth
[[file:figs/test_id31_effect_rotation_direct.png ]]
#+end_subfigure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_effect_rotation_coupling}Coupling terms}
#+attr_latex : :options {0.49\textwidth}
#+begin_subfigure
#+attr_latex : :width 0.95\linewidth
[[file:figs/test_id31_effect_rotation_coupling.png ]]
#+end_subfigure
#+end_figure
** Conclusion
:PROPERTIES:
:UNNUMBERED: t
:END:
2025-02-01 10:37:12 +01:00
The identified frequency response functions from command signals $\bm{u}$ to the force sensors $\bm{V}_s$ and to the estimated strut errors $\bm{\epsilon\mathcal{L}}$ are well matching the developed multi-body model.
Effect of payload mass is shown to be well predicted by the model, which can be useful if robust model based control is to be used.
The spindle rotation has no visible effect on the measured dynamics, indicating that controllers should be robust to the spindle rotation.
2024-11-13 17:11:40 +01:00
* Decentralized Integral Force Feedback
:PROPERTIES:
:header-args:matlab+: :tangle matlab/test_id31_3_iff.m
:END:
<<sec:test_id31_iff >>
** Introduction :ignore:
2025-02-01 12:11:25 +01:00
The HAC-LAC strategy that was previously developed and validated using the multi-body model is now experimentally implemented.
In this section, the low authority control part is first validated.
2024-11-15 18:15:13 +01:00
It consisted of a decentralized Integral Force Feedback controller $\bm{K}_{\text{IFF}}$, with all the diagonal terms being equal eqref:eq:test_id31_Kiff.
2024-11-13 17:11:40 +01:00
2024-11-15 18:15:13 +01:00
\begin{equation}\label{eq:test_id31_iff_diagonal}
\bm{K}_{\text{IFF}} = K_ {\text{IFF}} \cdot \bm{I}_6 = \begin{bmatrix}
K_{\text{IFF}} & & 0 \\
& \ddots & \\
0 & & K_{\text{IFF}}
\end{bmatrix}
\end{equation}
2025-02-01 12:11:25 +01:00
And it is implemented as shown in the block diagram of Figure ref:fig:test_id31_iff_block_diagram.
2024-11-15 18:15:13 +01:00
#+begin_src latex :file test_id31_iff_schematic.pdf
\begin{tikzpicture}
% Blocs
\node[block={2.0cm}{2.0cm}] (P) {Plant};
\coordinate[] (input) at ($(P.south west)!0.5!(P.north west)$);
\coordinate[] (outputH) at ($(P.south east)!0.2!(P.north east)$);
\coordinate[] (outputL) at ($(P.south east)!0.8!(P.north east)$);
\node[block, above=0.6 of P] (Klac) {$\bm{K}_\text{IFF}$};
\node[addb, left= of input] (addF) {};
% Connections and labels
\draw[->] (outputL) -- ++(0.6, 0) coordinate(eastlac) |- (Klac.east);
\node[above right] at (outputL){$\bm{V}_s$};
\draw[->] (Klac.west) -| (addF.north);
\draw[->] (addF.east) -- (input) node[above left]{$\bm{u}$};
2025-02-01 12:11:25 +01:00
\draw[->] (outputH) -- ++(1.6, 0) node[above left]{$\bm{\epsilon\mathcal{L}}$};
\draw[<-] (addF.west) -- ++(-1.0, 0) node[above right]{$\bm{u^{\prime}}$};
2024-11-15 18:15:13 +01:00
\begin{scope}[on background layer]
\node[fit={(Klac.north-|eastlac) (addF.west|-P.south)}, fill=black!20!white, draw, dashed, inner sep=10pt] (Pi) {};
\node[anchor={north west}] at (Pi.north west){$\text{Damped Plant}$};
\end{scope}
\end{tikzpicture}
#+end_src
#+name : fig:test_id31_iff_block_diagram
#+caption : Block diagram of the implemented decentralized IFF controller. The controller $\bm{K}_{\text{IFF}}$ is a diagonal controller with the same elements on every diagonal term $K_{\text{IFF}}$.
#+RESULTS :
[[file:figs/test_id31_iff_schematic.png ]]
** Matlab Init :noexport:ignore:
#+begin_src matlab
%% test_id31_3_iff.m
#+end_src
#+begin_src matlab :tangle no :exports none :results silent :noweb yes :var current_dir=(file-name-directory buffer-file-name)
<<matlab-dir >>
#+end_src
2024-11-13 17:11:40 +01:00
#+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-simscape >>
#+end_src
#+begin_src matlab :noweb yes
<<m-init-other >>
#+end_src
2024-11-15 18:15:13 +01:00
** IFF Plant
<<ssec:test_id31_iff_plant >>
2024-11-13 17:11:40 +01:00
2025-02-01 12:11:25 +01:00
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.
2024-11-13 17:11:40 +01:00
2025-02-01 12:11:25 +01:00
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.
2024-11-13 17:11:40 +01:00
2024-11-15 18:15:13 +01:00
#+begin_src matlab
% 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');
2024-11-13 17:11:40 +01:00
#+end_src
#+begin_src matlab :exports none
figure;
2024-11-15 18:15:13 +01:00
tiledlayout(2, 3, 'TileSpacing', 'tight', 'Padding', 'tight');
2024-11-13 17:11:40 +01:00
2024-11-15 18:15:13 +01:00
ax1 = nexttile();
2024-11-13 17:11:40 +01:00
hold on;
2024-11-15 18:15:13 +01:00
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')
2024-11-13 17:11:40 +01:00
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log');
2024-11-15 18:15:13 +01:00
set(gca, 'XTickLabel',[]); ylabel('Amplitude [m/V]');
yticks([1e-2, 1e-1, 1e0, 1e1]);
2024-11-13 17:11:40 +01:00
2024-11-15 18:15:13 +01:00
ax2 = nexttile();
2024-11-13 17:11:40 +01:00
hold on;
2024-11-15 18:15:13 +01:00
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')
2024-11-13 17:11:40 +01:00
hold off;
2024-11-15 18:15:13 +01:00
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log');
set(gca, 'XTickLabel',[]); set(gca, 'YTickLabel',[]);
2024-11-13 17:11:40 +01:00
2024-11-15 18:15:13 +01:00
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;
2024-11-13 17:11:40 +01:00
2024-11-15 18:15:13 +01:00
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]);
2024-11-13 17:11:40 +01:00
2024-11-15 18:15:13 +01:00
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])
2024-11-13 17:11:40 +01:00
2024-11-15 18:15:13 +01:00
ax6 = nexttile();
2024-11-13 17:11:40 +01:00
hold on;
2024-11-15 18:15:13 +01:00
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')
2024-11-13 17:11:40 +01:00
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log');
2024-11-15 18:15:13 +01:00
xlabel('Frequency [Hz]'); set(gca, 'YTickLabel',[]);
xticks([10, 20, 50, 100, 200])
2024-11-13 17:11:40 +01:00
2024-11-15 18:15:13 +01:00
linkaxes([ax1,ax2,ax3,ax4,ax5,ax6],'xy');
xlim([10, 5e2]); ylim([1e-2, 5e1]);
2024-11-13 17:11:40 +01:00
#+end_src
2024-11-15 18:15:13 +01:00
#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/test_id31_comp_simscape_Vs.pdf', 'width', 'full', 'height', 700);
2024-11-13 17:11:40 +01:00
#+end_src
2024-11-15 18:15:13 +01:00
#+name : fig:test_id31_comp_simscape_Vs
#+caption : Comparison of the measured (in blue) and modeled (in red) frequency transfer functions from the first control signal $u_1$ to the six force sensor voltages $V_{s1}$ to $V_{s6}$
#+RESULTS :
[[file:figs/test_id31_comp_simscape_Vs.png ]]
2024-11-13 17:11:40 +01:00
2024-11-15 18:15:13 +01:00
** IFF Controller
<<ssec:test_id31_iff_controller >>
2024-11-13 17:11:40 +01:00
2025-02-01 12:11:25 +01:00
A decentralized IFF controller was designed such that it adds damping to the suspension modes of the nano-hexapod for all considered payloads.
2024-11-15 18:15:13 +01:00
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.
2025-02-01 12:11:25 +01:00
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.
2024-11-13 17:11:40 +01:00
2024-11-15 18:15:13 +01:00
\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}
2024-11-13 17:11:40 +01:00
2025-02-01 12:11:25 +01:00
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.
2024-11-15 18:15:13 +01:00
#+begin_src matlab
%% 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);
2024-11-13 17:11:40 +01:00
2024-11-15 18:15:13 +01:00
% 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)
2024-11-13 17:11:40 +01:00
2024-11-15 18:15:13 +01:00
Kiff.InputName = {'Vs1', 'Vs2', 'Vs3', 'Vs4', 'Vs5', 'Vs6'};
Kiff.OutputName = {'u1', 'u2', 'u3', 'u4', 'u5', 'u6'};
2024-11-13 17:11:40 +01:00
#+end_src
2024-11-15 18:15:13 +01:00
#+begin_src matlab :exports none :tangle no
% The designed IFF controller is saved
save('./matlab/mat/test_id31_K_iff.mat', 'Kiff');
2024-11-13 17:11:40 +01:00
#+end_src
2024-11-15 18:15:13 +01:00
#+begin_src matlab :eval no
% The designed IFF controller is saved
save('./mat/test_id31_K_iff.mat', 'Kiff');
2024-11-13 17:11:40 +01:00
#+end_src
#+begin_src matlab :exports none :results none
2024-11-15 18:15:13 +01:00
%% Bode plot of the designed decentralized IFF controller
2024-11-13 17:11:40 +01:00
figure;
tiledlayout(3, 1, 'TileSpacing', 'compact', 'Padding', 'None');
ax1 = nexttile([2,1]);
hold on;
2024-11-15 18:15:13 +01:00
plot(f, abs(squeeze(freqresp(Kiff(1,1), f, 'Hz'))), 'color', colors(1,:));
2024-11-13 17:11:40 +01:00
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log');
2024-11-15 18:15:13 +01:00
ylabel('Amplitude'); set(gca, 'XTickLabel',[]);
ylim([1e-2, 1e1]);
2024-11-13 17:11:40 +01:00
ax2 = nexttile;
hold on;
2024-11-15 18:15:13 +01:00
plot(f, 180/pi*angle(squeeze(freqresp(Kiff(1,1), f, 'Hz'))), 'color', colors(1,:));
2024-11-13 17:11:40 +01:00
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'lin');
xlabel('Frequency [Hz]'); ylabel('Phase [deg]');
hold off;
yticks(-360:90:360);
2024-11-15 18:15:13 +01:00
ylim([-180, 180])
2024-11-13 17:11:40 +01:00
linkaxes([ax1,ax2],'x');
xlim([1, 1e3]);
#+end_src
2024-11-15 18:15:13 +01:00
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_Kiff_bode_plot.pdf', 'width', 'half', 'height', 600);
2024-11-13 17:11:40 +01:00
#+end_src
#+begin_src matlab :exports none :results none
2024-11-15 18:15:13 +01:00
%% Loop gain for the decentralized IFF controller
Kiff_frf = squeeze(freqresp(Kiff(1,1), f, 'Hz'));
2024-11-13 17:11:40 +01:00
figure;
tiledlayout(3, 1, 'TileSpacing', 'compact', 'Padding', 'None');
ax1 = nexttile([2,1]);
hold on;
2024-11-15 18:15:13 +01:00
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');
2024-11-13 17:11:40 +01:00
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'log');
2024-11-15 18:15:13 +01:00
ylabel('Loop Gain'); set(gca, 'XTickLabel',[]);
ylim([1e-2, 1e1]);
leg = legend('location', 'northwest', 'FontSize', 8, 'NumColumns', 1);
leg.ItemTokenSize(1) = 15;
2024-11-13 17:11:40 +01:00
ax2 = nexttile;
hold on;
2024-11-15 18:15:13 +01:00
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,:));
2024-11-13 17:11:40 +01:00
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'lin');
xlabel('Frequency [Hz]'); ylabel('Phase [deg]');
hold off;
yticks(-360:90:360);
2024-11-15 18:15:13 +01:00
ylim([-180, 180])
2024-11-13 17:11:40 +01:00
linkaxes([ax1,ax2],'x');
xlim([1, 1e3]);
#+end_src
#+begin_src matlab :tangle no :exports results :results file replace
2024-11-15 18:15:13 +01:00
exportFig('figs/test_id31_Kiff_loop_gain.pdf', 'width', 'half', 'height', 600);
2024-11-13 17:11:40 +01:00
#+end_src
2024-11-15 18:15:13 +01:00
#+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
2024-11-13 17:11:40 +01:00
2024-11-15 18:15:13 +01:00
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.
2025-02-01 12:11:25 +01:00
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.
2024-11-15 18:15:13 +01:00
However, in this study, it was chosen to implement a fix (i.e. non-adaptive) decentralized IFF controller.
#+begin_src matlab :exports none :results none
%% 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'});
2024-11-13 17:11:40 +01:00
#+end_src
#+begin_src matlab :exports none :results none
figure;
2024-11-15 18:15:13 +01:00
tiledlayout(1, 1, 'TileSpacing', 'compact', 'Padding', 'None');
nexttile();
2024-11-13 17:11:40 +01:00
hold on;
2024-11-15 18:15:13 +01:00
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');
2024-11-13 17:11:40 +01:00
end
2024-11-15 18:15:13 +01:00
% Optimal gain
clpoles = pole(feedback(Gm_iff_m0, Kiff, +1));
plot(real(clpoles), imag(clpoles), 'kx', ...
'DisplayName', '$g_{opt}$');
2024-11-13 17:11:40 +01:00
hold off;
2024-11-15 18:15:13 +01:00
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');
#+end_src
2024-11-13 17:11:40 +01:00
2024-11-15 18:15:13 +01:00
#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/test_id31_iff_root_locus_m0.pdf', 'width', 'third', 'height', 'normal');
#+end_src
#+begin_src matlab :exports none :results none
%% description
figure;
tiledlayout(1, 1, 'TileSpacing', 'compact', 'Padding', 'None');
nexttile();
2024-11-13 17:11:40 +01:00
hold on;
2024-11-15 18:15:13 +01:00
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');
2024-11-13 17:11:40 +01:00
end
2024-11-15 18:15:13 +01:00
% 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');
#+end_src
#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/test_id31_iff_root_locus_m1.pdf', 'width', 'third', 'height', 'normal');
#+end_src
#+begin_src matlab :exports none :results none
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');
2024-11-13 17:11:40 +01:00
end
2024-11-15 18:15:13 +01:00
% 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');
#+end_src
#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/test_id31_iff_root_locus_m2.pdf', 'width', 'third', 'height', 'normal');
#+end_src
#+begin_src matlab :exports none :results none
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');
2024-11-13 17:11:40 +01:00
end
2024-11-15 18:15:13 +01:00
% 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');
#+end_src
#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/test_id31_iff_root_locus_m3.pdf', 'width', 'third', 'height', 'normal');
#+end_src
#+name : fig:test_id31_iff_root_locus_m0
#+caption : Root Locus plots for the designed decentralized IFF controller and using the multy-body model. Black crosses indicate the closed-loop poles for the choosen value of the gain.
#+attr_latex : :options [htbp]
#+begin_figure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_iff_root_locus_m0}$m = 0\,\text{kg}$}
#+attr_latex : :options {0.24\textwidth}
#+begin_subfigure
#+attr_latex : :width 0.9\linewidth
[[file:figs/test_id31_iff_root_locus_m0.png ]]
#+end_subfigure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_iff_root_locus_m1}$m = 13\,\text{kg}$}
#+attr_latex : :options {0.24\textwidth}
#+begin_subfigure
#+attr_latex : :width 0.9\linewidth
[[file:figs/test_id31_iff_root_locus_m1.png ]]
#+end_subfigure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_iff_root_locus_m2}$m = 26\,\text{kg}$}
#+attr_latex : :options {0.24\textwidth}
#+begin_subfigure
#+attr_latex : :width 0.9\linewidth
[[file:figs/test_id31_iff_root_locus_m2.png ]]
#+end_subfigure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_iff_root_locus_m3}$m = 39\,\text{kg}$}
#+attr_latex : :options {0.24\textwidth}
#+begin_subfigure
#+attr_latex : :width 0.9\linewidth
[[file:figs/test_id31_iff_root_locus_m3.png ]]
#+end_subfigure
#+end_figure
2025-02-01 12:11:25 +01:00
** Damped Plant
2024-11-15 18:15:13 +01:00
<<ssec:test_id31_iff_perf >>
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}}$.
2025-02-01 12:11:25 +01:00
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.
2024-11-15 18:15:13 +01:00
#+begin_src matlab
%% 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
isstable(Gm_hac_m0_Wz0)
isstable(Gm_hac_m1_Wz0)
isstable(Gm_hac_m2_Wz0)
isstable(Gm_hac_m3_Wz0)
#+end_src
#+begin_src matlab :exports none :tangle no
% The estimated damped plants from the multi-body model are saved
save('./matlab/mat/test_id31_simscape_damped_plants.mat', 'Gm_hac_m0_Wz0', 'Gm_hac_m1_Wz0', 'Gm_hac_m2_Wz0', 'Gm_hac_m3_Wz0');
#+end_src
#+begin_src matlab :eval no
% 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');
#+end_src
#+begin_src matlab :exports none :results none
%% 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', '$-e\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', '$-e\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', '$-e\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', '$-e\mathcal{L}_{1}/u_1$ - 39 kg');
plot(freqs, abs(squeeze(freqresp(Gm_hac_m0_Wz0('eL1', 'u1'), freqs, 'Hz'))), 'color', colors(1,:), ...
'DisplayName', '$-e\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', '$-e\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', '$-e\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', '$-e\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,:));
2024-11-13 17:11:40 +01:00
hold off;
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'lin');
xlabel('Frequency [Hz]'); ylabel('Phase [deg]');
hold off;
yticks(-360:90:360);
2024-11-15 18:15:13 +01:00
ylim([-20, 200])
2024-11-13 17:11:40 +01:00
linkaxes([ax1,ax2],'x');
xlim([1, 1e3]);
#+end_src
2025-02-01 12:11:25 +01:00
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_comp_ol_iff_plant_model.pdf', 'width', 'half', 'height', 600);
2024-11-13 17:11:40 +01:00
#+end_src
2025-02-01 12:11:25 +01:00
#+begin_src matlab
%% 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
#+end_src
#+begin_src matlab :exports none :tangle no
% The identified dynamics are then saved for further use.
save('./matlab/mat/test_id31_identified_damped_plants.mat', 'G_hac_m0_Wz0', 'G_hac_m1_Wz0', 'G_hac_m2_Wz0', 'G_hac_m3_Wz0', 'f');
#+end_src
#+begin_src matlab :eval no
% 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');
#+end_src
#+begin_src matlab :exports none :results none
%% 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]);
#+end_src
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_hac_plant_effect_mass.pdf', 'width', 'half', 'height', 600);
#+end_src
#+name : fig:test_id31_hac_plant_effect_mass_comp_model
#+caption : Comparison of the open-loop plants and the damped plant with Decentralized IFF, estimated from the multi-body model (\subref{fig:test_id31_comp_ol_iff_plant_model}). Comparison of the measured damped plants and modeled plants for all considered payloads (\subref{fig:test_id31_hac_plant_effect_mass}). Only "direct" terms ($\epsilon\mathcal{L}_i/u_i^\prime$) are displayed for simplificty
#+attr_latex : :options [htbp]
#+begin_figure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_comp_ol_iff_plant_model}Effect of IFF on the plant}
#+attr_latex : :options {0.49\textwidth}
#+begin_subfigure
#+attr_latex : :width 0.95\linewidth
2024-11-15 18:15:13 +01:00
[[file:figs/test_id31_comp_ol_iff_plant_model.png ]]
2025-02-01 12:11:25 +01:00
#+end_subfigure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_hac_plant_effect_mass}Comparison of model and experimental results}
#+attr_latex : :options {0.49\textwidth}
#+begin_subfigure
#+attr_latex : :width 0.95\linewidth
[[file:figs/test_id31_hac_plant_effect_mass.png ]]
#+end_subfigure
#+end_figure
2024-11-15 18:15:13 +01:00
** Conclusion
:PROPERTIES:
:UNNUMBERED: t
:END:
2025-02-01 12:11:25 +01:00
The implementation of a decentralized Integral Force Feedback controller has been successfully demonstrated.
Using the multi-body model, the controller was designed and optimized to ensure stability across all payload conditions while providing significant damping of suspension modes.
The experimental results validated the model predictions, showing a reduction of peak amplitudes by approximately a factor of 10 across the full payload range (0-39 kg).
While higher gains could potentially achieve better damping performance for lighter payloads, the chosen fixed-gain configuration represents a robust compromise that maintains stability and performance across all operating conditions.
The good correlation between the modeled and measured damped plants confirms the effectiveness of using the multi-body model for both controller design and performance prediction.
2024-11-15 18:15:13 +01:00
* High Authority Control in the frame of the struts
:PROPERTIES:
:header-args:matlab+: :tangle matlab/test_id31_4_hac.m
:END:
2025-02-01 09:45:01 +01:00
<<sec:test_id31_hac >>
2024-11-15 18:15:13 +01:00
** Introduction :ignore:
2024-11-13 17:11:40 +01:00
2025-02-01 18:35:04 +01:00
In this section, a High-Authority-Controller is developed to actively stabilize the sample's position.
The corresponding control architecture is shown in Figure ref:fig:test_id31_iff_hac_schematic.
As the diagonal terms of the damped plants were found to be all equal (thanks to the system's symmetry and manufacturing and mounting uniformity, see Figure ref:fig:test_id31_hac_plant_effect_mass), a diagonal high authority controller $\bm{K}_ {\text{HAC}}$ is implemented with all diagonal terms being equal eqref:eq:eq:test_id31_hac_diagonal.
2024-11-13 17:11:40 +01:00
2024-11-15 18:15:13 +01:00
\begin{equation}\label{eq:eq:test_id31_hac_diagonal}
\bm{K}_{\text{HAC}} = K_ {\text{HAC}} \cdot \bm{I}_6 = \begin{bmatrix}
K_{\text{HAC}} & & 0 \\
& \ddots & \\
0 & & K_{\text{HAC}}
\end{bmatrix}
\end{equation}
#+begin_src latex :file test_id31_iff_hac_schematic.pdf
\begin{tikzpicture}
% Blocs
\node[block={2.0cm}{2.0cm}] (P) {Plant};
\coordinate[] (input) at ($(P.south west)!0.5!(P.north west)$);
\coordinate[] (outputH) at ($(P.south east)!0.2!(P.north east)$);
\coordinate[] (outputL) at ($(P.south east)!0.8!(P.north east)$);
\node[block, above=0.6 of P] (Klac) {$\bm{K}_\text{IFF}$};
\node[addb, left= of input] (addF) {};
\node[block, left= of addF] (Khac) {$\bm{K}_\text{HAC}$};
% Connections and labels
\draw[->] (outputL) -- ++(0.6, 0) coordinate(eastlac) |- (Klac.east);
\node[above right] at (outputL){$\bm{V}_s$};
\draw[->] (Klac.west) -| (addF.north);
\draw[->] (addF.east) -- (input) node[above left]{$\bm{u}$};
\draw[->] (outputH) -- ++(1.6, 0) node[above left]{$\bm{\epsilon\mathcal{L}}$};
\draw[->] (Khac.east) node[above right]{$\bm{u}^{\prime}$} -- (addF.west);
\draw[->] ($(outputH) + (1.2, 0)$)node[branch]{} |- ($(Khac.west)+ (-0.6, -1.6)$) |- (Khac.west);
\begin{scope}[on background layer]
\node[fit={(Klac.north-|eastlac) (addF.west|-P.south)}, fill=black!20!white, draw, dashed, inner sep=10pt] (Pi) {};
\node[anchor={north west}] at (Pi.north west){$\text{Damped Plant}$};
\end{scope}
\end{tikzpicture}
#+end_src
#+name : fig:test_id31_iff_hac_schematic
#+caption : Block diagram of the implemented HAC-IFF controllers. The controller $\bm{K}_{\text{HAC}}$ is a diagonal controller with the same elements on every diagonal term $K_{\text{HAC}}$.
#+RESULTS :
[[file:figs/test_id31_iff_hac_schematic.png ]]
** Matlab Init :noexport:ignore:
2024-11-13 17:11:40 +01:00
#+begin_src matlab
2024-11-15 18:15:13 +01:00
%% test_id31_4_hac.m
#+end_src
#+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-simscape >>
#+end_src
#+begin_src matlab :noweb yes
<<m-init-other >>
#+end_src
** Damped Plant
<<ssec:test_id31_iff_hac_plant >>
2025-02-01 18:35:04 +01:00
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.
2024-11-15 18:15:13 +01:00
#+begin_src matlab
% 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');
2025-02-01 12:11:25 +01:00
% 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');
2024-11-15 18:15:13 +01:00
% 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');
#+end_src
#+begin_src matlab :exports none
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]);
#+end_src
#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/test_id31_comp_simscape_hac.pdf', 'width', 'full', 'height', 700);
#+end_src
#+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 ]]
2025-02-01 18:35:04 +01:00
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.
2024-11-15 18:15:13 +01:00
This is one of the key benefit of using the HAC-LAC strategy.
#+begin_src matlab :exports none :results none
%% 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]);
#+end_src
2025-02-01 18:35:04 +01:00
#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/test_id31_comp_all_undamped_damped_plants.pdf', 'width', 'wide', 'height', 600);
2024-11-15 18:15:13 +01:00
#+end_src
2025-02-01 18:35:04 +01:00
#+name : fig:test_id31_comp_all_undamped_damped_plants
#+caption : Comparison of the (six) direct terms for all (four) payload conditions in the undamped case (in blue) and the damped case (i.e. with the decentralized IFF being implemented, in red).
#+RESULTS :
2024-11-15 18:15:13 +01:00
[[file:figs/test_id31_comp_all_undamped_damped_plants.png ]]
2025-01-31 16:33:25 +01:00
** Interaction Analysis
2025-02-01 18:35:04 +01:00
<<sec:test_id31_hac_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.
2025-01-31 16:33:25 +01:00
#+begin_src matlab
2025-02-01 18:35:04 +01:00
%% 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))));
2025-01-31 16:33:25 +01:00
end
2025-02-01 18:35:04 +01:00
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))));
2025-01-31 16:33:25 +01:00
end
2025-02-01 18:35:04 +01:00
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))));
2025-01-31 16:33:25 +01:00
end
2025-02-01 18:35:04 +01:00
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))));
2025-01-31 16:33:25 +01:00
end
#+end_src
2025-02-01 18:35:04 +01:00
#+begin_src matlab :exports none :results none
%% RGA-number for the damped plants - Comparison of all the payload conditions
2025-01-31 16:33:25 +01:00
figure;
hold on;
2025-02-01 18:35:04 +01:00
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');
2025-01-31 16:33:25 +01:00
set(gca, 'XScale', 'log'); set(gca, 'YScale', 'lin');
2025-02-01 18:35:04 +01:00
xlim([1, 1e2]); ylim([0, 10]);
leg = legend('location', 'northwest', 'FontSize', 8, 'NumColumns', 2);
leg.ItemTokenSize(1) = 15;
#+end_src
#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/test_id31_hac_rga_number.pdf', 'width', 'wide', 'height', 'normal');
2025-01-31 16:33:25 +01:00
#+end_src
2025-02-01 18:35:04 +01:00
#+name : fig:test_id31_hac_rga_number
#+caption : RGA-number for the damped plants - Comparison of all the payload conditions
#+RESULTS :
[[file:figs/test_id31_hac_rga_number.png ]]
2024-11-15 18:15:13 +01:00
** Robust Controller Design
<<ssec:test_id31_iff_hac_controller >>
2025-02-01 18:35:04 +01:00
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.
2024-11-15 18:15:13 +01:00
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.
2025-02-01 18:35:04 +01:00
The controller transfer function is shown in eqref:eq:test_id31_robust_hac.
2024-11-15 18:15:13 +01:00
\begin{equation}\label{eq:test_id31_robust_hac}
2025-02-01 18:35:04 +01:00
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)
2024-11-15 18:15:13 +01:00
\end{equation}
2025-02-01 18:35:04 +01:00
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.
2024-11-15 18:15:13 +01:00
#+begin_src matlab
%% 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
#+end_src
#+begin_src matlab :exports none :tangle no
% The designed HAC controller is saved
save('./matlab/mat/test_id31_K_hac_robust.mat', 'Khac');
#+end_src
#+begin_src matlab :eval no
% The designed HAC controller is saved
save('./mat/test_id31_K_hac_robust.mat', 'Khac');
#+end_src
#+begin_src matlab :exports none :results none
%% Decentralized Loop gain for the High Authority Controller
figure;
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');
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, 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,:));
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]);
#+end_src
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_hac_loop_gain.pdf', 'width', 'half', 'height', 600);
#+end_src
#+begin_src matlab :exports none
%% Compute the Eigenvalues of the loop gain
Ldet = zeros(4, 6, length(f));
% Loop gain
2025-02-01 18:35:04 +01:00
Lmimo = pagemtimes(permute(G_hac_m0_Wz0, [2,3,1]),squeeze(freqresp(Khac, f, 'Hz')));
2024-11-15 18:15:13 +01:00
for i_f = 2:length(f)
Ldet(1,:, i_f) = eig(squeeze(Lmimo(:,:,i_f)));
end
2025-02-01 18:35:04 +01:00
Lmimo = pagemtimes(permute(G_hac_m1_Wz0, [2,3,1]),squeeze(freqresp(Khac, f, 'Hz')));
2024-11-15 18:15:13 +01:00
for i_f = 2:length(f)
Ldet(2,:, i_f) = eig(squeeze(Lmimo(:,:,i_f)));
end
2025-02-01 18:35:04 +01:00
Lmimo = pagemtimes(permute(G_hac_m2_Wz0, [2,3,1]),squeeze(freqresp(Khac, f, 'Hz')));
2024-11-15 18:15:13 +01:00
for i_f = 2:length(f)
Ldet(3,:, i_f) = eig(squeeze(Lmimo(:,:,i_f)));
end
2025-02-01 18:35:04 +01:00
Lmimo = pagemtimes(permute(G_hac_m3_Wz0, [2,3,1]),squeeze(freqresp(Khac, f, 'Hz')));
2024-11-15 18:15:13 +01:00
for i_f = 2:length(f)
Ldet(4,:, 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;
2025-02-01 18:35:04 +01:00
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');
2024-11-15 18:15:13 +01:00
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');
2025-02-01 18:35:04 +01:00
leg = legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 2);
leg.ItemTokenSize(1) = 15;
2024-11-15 18:15:13 +01:00
axis square
xlim([-1.5, 0.5]); ylim([-1, 1]);
#+end_src
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_hac_characteristic_loci.pdf', 'width', 'half', 'height', 600);
#+end_src
#+name : fig:test_id31_hac_loop_gain_loci
#+caption : Robust High Authority Controller. "Decentralized loop-gains" are shown in (\subref{fig:test_id31_hac_loop_gain}) and characteristic loci are shown in (\subref{fig:test_id31_hac_characteristic_loci})
#+attr_latex : :options [htbp]
#+begin_figure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_hac_loop_gain}Loop Gains}
#+attr_latex : :options {0.49\textwidth}
#+begin_subfigure
#+attr_latex : :width 0.95\linewidth
[[file:figs/test_id31_hac_loop_gain.png ]]
#+end_subfigure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_hac_characteristic_loci}Characteristic Loci}
#+attr_latex : :options {0.49\textwidth}
#+begin_subfigure
#+attr_latex : :width 0.95\linewidth
[[file:figs/test_id31_hac_characteristic_loci.png ]]
#+end_subfigure
#+end_figure
2025-02-01 18:35:04 +01:00
** Performance estimation with simulation of Tomography scans
2024-11-15 18:15:13 +01:00
<<ssec:test_id31_iff_hac_perf >>
2025-02-01 18:35:04 +01:00
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 ] .
2024-11-15 18:15:13 +01:00
The rotational velocity was set to 30rpm, 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.
2025-02-01 18:35:04 +01:00
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).
2024-11-15 18:15:13 +01:00
#+begin_src matlab
%% 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]
set_param(mdl, 'StopTime', '3'); % 6 turns at 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;
#+end_src
#+begin_src matlab :exports none :tangle no
% Save the simulation results
save('./matlab/mat/test_id31_exp_tomo_ol_cl_30rpm_sim.mat', 'exp_tomo_ol_m0_Wz180', 'exp_tomo_cl_m0_Wz180');
#+end_src
#+begin_src matlab :eval no
% 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');
#+end_src
2025-02-01 18:35:04 +01:00
#+begin_src matlab :tangle no
% Load the simulation results
load('test_id31_exp_tomo_ol_cl_30rpm_sim.mat', 'exp_tomo_ol_m0_Wz180', 'exp_tomo_cl_m0_Wz180');
#+end_src
2024-11-15 18:15:13 +01:00
#+begin_src matlab :exports none :results none
%% 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;
#+end_src
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_tomo_m0_30rpm_robust_hac_iff_sim_xy.pdf', 'width', 'half', 'height', 'normal');
#+end_src
#+begin_src matlab :exports none :results none
%% Simulation of tomography experiment - no payload, 30rpm - YZ errors
figure;
2025-02-01 18:35:04 +01:00
tiledlayout(2, 1, 'TileSpacing', 'compact', 'Padding', 'None');
ax1 = nexttile();
2024-11-15 18:15:13 +01:00
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
2025-02-01 18:35:04 +01:00
xlim([-3, 3]); ylim([-0.6, 0.6]);
2024-11-15 18:15:13 +01:00
xticks([-3:1:3]);
2025-02-01 18:35:04 +01:00
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]);
2024-11-15 18:15:13 +01:00
leg = legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1);
leg.ItemTokenSize(1) = 15;
#+end_src
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_tomo_m0_30rpm_robust_hac_iff_sim_yz.pdf', 'width', 'half', 'height', 'normal');
#+end_src
#+name : fig:test_id31_tomo_m0_30rpm_robust_hac_iff_sim
#+caption : Position error of the sample in the XY (\subref{fig:test_id31_tomo_m0_30rpm_robust_hac_iff_sim_xy}) and YZ (\subref{fig:test_id31_tomo_m0_30rpm_robust_hac_iff_sim_yz}) planes during a simulation of a tomography experiment at 30RPM. No payload is placed on top of the nano-hexapod.
#+attr_latex : :options [htbp]
#+begin_figure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_tomo_m0_30rpm_robust_hac_iff_sim_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_sim_xy.png ]]
#+end_subfigure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_tomo_m0_30rpm_robust_hac_iff_sim_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_sim_yz.png ]]
#+end_subfigure
#+end_figure
2025-02-01 18:35:04 +01:00
** Robustness estimation with simulation of Tomography scans
<<ssec:test_id31_iff_hac_robustness >>
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 1rpm (i.e. 6deg/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.
#+begin_src matlab
%% Simulation of tomography experiments at 1RPM with all payloads
% Configuration
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;
#+end_src
#+begin_src matlab :exports none :tangle no
% Save the simulation results
save('./matlab/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');
#+end_src
#+begin_src matlab :eval no
% 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');
#+end_src
#+begin_src matlab :tangle no
% Load the simulation results
load('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');
#+end_src
#+begin_src matlab :exports none :results none
%% 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([-50:50:50]);
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([-50:50:50]);
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([-50:50:50]);
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([-50:50:50]);
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]);
#+end_src
#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/test_id31_hac_tomography_Wz36_simulation.pdf', 'width', 'full', 'height', 'normal');
#+end_src
#+name : fig:test_id31_hac_tomography_Wz36_simulation
#+caption : Positioning errors in the Y-Z plane during tomography experiments simulated using the multi-body model (in closed-loop)
#+RESULTS :
[[file:figs/test_id31_hac_tomography_Wz36_simulation.png ]]
** Conclusion
:PROPERTIES:
:UNNUMBERED: t
:END:
In this section, a High-Authority-Controller was developed to actively stabilize the sample's position.
The multi-body model was first validated by comparing it with measured frequency responses of the damped plant, showing good agreement for both direct terms and coupling terms.
This validation confirmed that the model could be reliably used to tune the feedback controller and evaluate its performances.
An interaction analysis using the RGA-number was then performed, revealing that higher payload masses lead to increased coupling when implementing control in the strut reference frame.
Based on this analysis, a diagonal controller with a crossover frequency of 5 Hz was designed, incorporating an integrator, a lead compensator, and a first order low-pass filter.
Finally, simulations of tomography experiments were performed to validate the HAC-LAC architecture.
The closed-loop system remained stable for all tested payload conditions (0 to 39 kg).
With no payload at 30 rpm, the NASS successfully kept the sample point of interested on the beam, which fulfills the specifications.
At 1 rpm, while positioning errors increased with the payload mass (particularly in the lateral direction), the system maintained stable.
These results demonstrate both the effectiveness and limitations of implementing control in the frame of the struts.
* Validation with Scientific experiments
:PROPERTIES:
:header-args:matlab+: :tangle matlab/test_id31_5_experiments.m
:END:
<<sec:test_id31_experiments >>
** Introduction :ignore:
# - [ ] Where are different experiment explained?
# - [ ] Explain which controller was used here: robust one that works for all payloads!
# - [ ] All results are not filtered (i.e. 10kHz)
# - [ ] For ramp scans, a higher performance controller was used with two integrators
The online metrology prototype does not allow samples to be placed on top of the nano-hexapod while being illuminated by the x-ray beam.
However, in order to fully validate the NASS, typical motion performed during scientific experiments can be mimicked, and the positioning performances can be evaluated.
For tomography scans, performances were already evaluated in Section ref:ssec:test_id31_iff_hac_perf.
Here, other typical experiments are performed:
- /Lateral scans/ : the $T_y$ translations stage performs $D_y$ scans and the errors are corrected by the NASS in real time (Section ref:ssec:test_id31_scans_dy)
- /Vertical layer scans/ : the nano-hexapod is used to perform $D_z$ step motion or ramp scans (Section ref:ssec:test_id31_scans_dz)
- /Reflectivity scans/ : the tilt stage is doing $R_y$ rotations and the errors are corrected by the NASS in real time (Section ref:ssec:test_id31_scans_reflectivity)
- /Diffraction Tomography/ : the Spindle is performing continuous $R_z$ rotation while the translation stage is performing lateral $D_y$ scans at the same time.
This is the experiment with the most stringent requirements (Section ref:ssec:test_id31_scans_diffraction_tomo)
** 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
** $R_z$ scans: Tomography
<<ssec:test_id31_scans_tomography >>
**** Introduction :ignore:
*Issue with this control architecture (or controller?)* :
- The position is not converging to zero
*Compare* :
- 1rpm, 6rpm, 30rpm
- at 1rpm: m0, m1, m2, m3 (same robust controller!)
**** Previous results at 30rpm
2024-11-15 18:15:13 +01:00
Then the same tomography experiment (i.e. constant spindle rotation at 30rpm, and no payload) was performed experimentally.
The measured position of the "point of interest" during the experiment are shown in Figure ref:fig:test_id31_tomo_m0_30rpm_robust_hac_iff_exp.
#+begin_src matlab
%% Experimental Results for Tomography at 30RPM, no payload
% Load measured noise
data_tomo_m0_Wz180 = load('2023-08-17_15-26_tomography_30rpm_m0_robust.mat');
[~, i_cl] = find(data_tomo_m0_Wz180.hac_status = = 1);
#+end_src
#+begin_src matlab :exports none :results none
%% Measured radial errors of the Spindle
figure;
hold on;
plot(1e6*data_tomo_m0_Wz180.Dx_int(1:i_cl), 1e6*data_tomo_m0_Wz180.Dy_int(1:i_cl), 'DisplayName', 'OL')
plot(1e6*data_tomo_m0_Wz180.Dx_int(i_cl:i_cl+1e4), 1e6*data_tomo_m0_Wz180.Dy_int(i_cl:i_cl+1e4), 'color', colors(3,:), 'HandleVisibility', 'off')
plot(1e6*data_tomo_m0_Wz180.Dx_int(i_cl+1e4:end), 1e6*data_tomo_m0_Wz180.Dy_int(i_cl+1e4: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;
#+end_src
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_tomo_m0_30rpm_robust_hac_iff_exp_xy.pdf', 'width', 'half', 'height', 'normal');
#+end_src
#+begin_src matlab :exports none :results none
%% Measured radial errors of the Spindle
figure;
2025-01-31 16:33:25 +01:00
tiledlayout(2, 1, 'TileSpacing', 'compact', 'Padding', 'None');
ax1 = nexttile();
2024-11-15 18:15:13 +01:00
hold on;
plot(1e6*data_tomo_m0_Wz180.Dy_int(1:i_cl), 1e6*data_tomo_m0_Wz180.Dz_int(1:i_cl), 'DisplayName', 'OL')
plot(1e6*data_tomo_m0_Wz180.Dy_int(i_cl:i_cl+1e4), 1e6*data_tomo_m0_Wz180.Dz_int(i_cl:i_cl+1e4), 'color', colors(3,:), 'HandleVisibility', 'off')
plot(1e6*data_tomo_m0_Wz180.Dy_int(i_cl+1e4:end), 1e6*data_tomo_m0_Wz180.Dz_int(i_cl+1e4:end), 'color', colors(2,:), 'DisplayName', 'CL')
2025-01-31 16:33:25 +01:00
% theta = linspace(0, 2*pi, 500); % Angle to plot the circle [rad]
% plot(0.1*cos(theta), 0.05*sin(theta), 'k-', 'DisplayName', 'Best Fit')
2024-11-15 18:15:13 +01:00
hold off;
xlabel('$D_y$ [$\mu$m]'); ylabel('$D_z$ [$\mu$m]');
axis equal
2025-01-31 16:33:25 +01:00
xlim([-3, 3]); ylim([-0.6, 0.6]);
2024-11-15 18:15:13 +01:00
xticks([-3:1:3]);
2025-01-31 16:33:25 +01:00
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_cl+1e4:end), 1e9*data_tomo_m0_Wz180.Dz_int(i_cl+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]);
% xticks([-3:1:3]);
% yticks([-3:1:3]);
2024-11-15 18:15:13 +01:00
leg = legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1);
leg.ItemTokenSize(1) = 15;
#+end_src
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_tomo_m0_30rpm_robust_hac_iff_exp_yz.pdf', 'width', 'half', 'height', 'normal');
#+end_src
#+name : fig:test_id31_tomo_m0_30rpm_robust_hac_iff_exp
#+caption : Experimental results of a tomography experiment at 30RPM 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
Even though the simulation (Figure ref:fig:test_id31_tomo_m0_30rpm_robust_hac_iff_sim) and the experimental results (Figure ref:fig:test_id31_tomo_m0_30rpm_robust_hac_iff_exp) are looking similar, the most important metric to compare is the RMS values of the positioning errors in closed-loop.
These are computed for both the simulation and the experimental results and are compared in Table ref:tab:test_id31_tomo_m0_30rpm_robust_hac_iff_rms.
The lateral and vertical errors are similar, however the tilt ($R_y$) errors are underestimated by the model, which is reasonable as disturbances in $R_y$ were not modeled.
Results obtained with this conservative HAC are already close to the specifications.
#+begin_src matlab
%% Compute RMS of errors
% Simulation - OL
rms_Dy_m0_Wz180_ol_sim = rms(detrend(exp_tomo_ol_m0_Wz180.y.y.Data, 0));
rms_Dz_m0_Wz180_ol_sim = rms(detrend(exp_tomo_ol_m0_Wz180.y.z.Data, 0));
rms_Ry_m0_Wz180_ol_sim = rms(detrend(squeeze(atan2(exp_tomo_ol_m0_Wz180.y.R.Data(1,3,:), sqrt(exp_tomo_ol_m0_Wz180.y.R.Data(1,1,:).^2+exp_tomo_ol_m0_Wz180.y.R.Data(1,2,:).^2))), 0));
i_stab = 1000; % Remove the transient that is irrelevant here
% Simulation - CL
rms_Dy_m0_Wz180_cl_sim = rms(detrend(exp_tomo_cl_m0_Wz180.y.y.Data(i_stab:end), 0));
rms_Dz_m0_Wz180_cl_sim = rms(detrend(exp_tomo_cl_m0_Wz180.y.z.Data(i_stab:end), 0));
rms_Ry_m0_Wz180_cl_sim = rms(detrend(squeeze(atan2(exp_tomo_cl_m0_Wz180.y.R.Data(1,3,i_stab:end), sqrt(exp_tomo_cl_m0_Wz180.y.R.Data(1,1,i_stab:end).^2+exp_tomo_cl_m0_Wz180.y.R.Data(1,2,i_stab:end).^2))), 0));
% Experimental - OL
[~, i_cl] = find(data_tomo_m0_Wz180.hac_status = = 1);
rms_Dy_m0_Wz180_ol_exp = rms(detrend(data_tomo_m0_Wz180.Dy_int(1:i_cl), 0));
rms_Dz_m0_Wz180_ol_exp = rms(detrend(data_tomo_m0_Wz180.Dz_int(1:i_cl), 0));
rms_Ry_m0_Wz180_ol_exp = rms(detrend(data_tomo_m0_Wz180.Ry_int(1:i_cl), 0));
% Experimental - CL
rms_Dy_m0_Wz180_cl_exp = rms(detrend(data_tomo_m0_Wz180.Dy_int(i_cl+10000:end), 0));
rms_Dz_m0_Wz180_cl_exp = rms(detrend(data_tomo_m0_Wz180.Dz_int(i_cl+10000:end), 0));
rms_Ry_m0_Wz180_cl_exp = rms(detrend(data_tomo_m0_Wz180.Ry_int(i_cl+10000:end), 0));
#+end_src
#+name : tab:test_id31_tomo_m0_30rpm_robust_hac_iff_rms
#+caption : RMS values of the errors for a tomography experiment at 30RPM and without payload. Experimental results and simulation are compared.
#+attr_latex : :environment tabularx :width 0.7\linewidth :align Xccc
#+attr_latex : :center t :booktabs t
2025-01-31 16:33:25 +01:00
| | $D_y$ | $D_z$ | $R_y$ |
|-----------------+-----------------------+--------------------+------------------------|
| Experiment (OL) | $1.8\,\mu\text{mRMS}$ | $24\,\text{nmRMS}$ | $10\,\mu\text{radRMS}$ |
|-----------------+-----------------------+--------------------+------------------------|
| Simulation (CL) | $30\,\text{nmRMS}$ | $8\,\text{nmRMS}$ | $73\,\text{nradRMS}$ |
| Experiment (CL) | $39\,\text{nmRMS}$ | $11\,\text{nmRMS}$ | $130\,\text{nradRMS}$ |
|-----------------+-----------------------+--------------------+------------------------|
| Specifications | $30\,\text{nmRMS}$ | $15\,\text{nmRMS}$ | $250\,\text{nradRMS}$ |
2024-11-15 18:15:13 +01:00
2025-02-01 18:35:04 +01:00
**** Previous results at 1rpm
2025-01-31 16:33:25 +01:00
2025-02-01 18:35:04 +01:00
The tomography experiments that were simulated were then experimentally conducted.
For each payload, a spindle rotating was first performed in open-loop, and then the loop was closed during another full spindle rotation.
An example with the $26\,\text{kg}$ payload is shown in Figure ref:fig:test_id31_tomo_m2_1rpm_robust_hac_iff_fit.
The eccentricity between the "point of interest" and the spindle rotation axis is quite large as the added payload mass statically deforms the micro-station stages.
To estimate the open-loop errors, it is supposed that the "point of interest" can be perfectly aligned with the spindle rotation axis.
Therefore, the eccentricity is first estimated by performing a circular fit (dashed black circle in Figure ref:fig:test_id31_tomo_m2_1rpm_robust_hac_iff_fit), and then subtracted from the data in Figure ref:fig:test_id31_tomo_m2_1rpm_robust_hac_iff_fit_removed.
This underestimate the real condition open-loop errors as it is difficult to obtain a perfect alignment in practice.
2024-11-15 18:15:13 +01:00
#+begin_src matlab
2025-02-01 18:35:04 +01:00
%% 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];
2024-11-15 18:15:13 +01:00
2025-02-01 18:35:04 +01:00
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];
2024-11-15 18:15:13 +01:00
2025-02-01 18:35:04 +01:00
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];
2024-11-15 18:15:13 +01:00
2025-02-01 18:35:04 +01:00
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];
2024-11-15 18:15:13 +01:00
#+end_src
2025-02-01 18:35:04 +01:00
#+begin_src matlab
[~, i_m0] = find(data_tomo_m0_Wz6.hac_status = = 1);
% Find best circle
[x_m0, y_m0, R_m0] = circlefit(data_tomo_m2_Wz6.Dx_int(1:i_m2), data_tomo_m2_Wz6.Dy_int(1:i_m2));
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);
[~, i_m2] = find(data_tomo_m2_Wz6.hac_status = = 1);
[~, i_m3] = find(data_tomo_m3_Wz6.hac_status = = 1);
2024-11-15 18:15:13 +01:00
#+end_src
2025-02-01 18:35:04 +01:00
#+begin_src matlab :exports none :results none
%% Tomography experiment at 1rpm with 26kg payload
2024-11-15 18:15:13 +01:00
figure;
hold on;
2025-02-01 18:35:04 +01:00
plot(1e9*data_tomo_m0_Wz6.Dy_int(i_cl+1e4:end), 1e9*data_tomo_m0_Wz6.Dz_int(i_cl+1e4:end))
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
xticks([-400:100:400]);
yticks([-50:50:50])
% xlim([-20, 100]); ylim([-20, 100]);
% xticks([-20:20:100]);
% yticks([-20:20:100]);
% leg = legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 1);
% leg.ItemTokenSize(1) = 15;
2024-11-15 18:15:13 +01:00
#+end_src
#+begin_src matlab
2025-02-01 18:35:04 +01:00
figure;
tiledlayout(2, 2, 'TileSpacing', 'compact', 'Padding', 'None');
2024-11-15 18:15:13 +01:00
2025-02-01 18:35:04 +01:00
ax1 = nexttile;
hold on;
plot(1e9*detrend(data_tomo_m0_Wz6.Dy_int(i_cl+1e4:end), 0), 1e9*detrend(data_tomo_m0_Wz6.Dz_int(i_cl+1e4:end), 0), 'color', colors(1,:))
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([-50:50:50])
2024-11-15 18:15:13 +01:00
2025-02-01 18:35:04 +01:00
ax2 = nexttile;
hold on;
plot(1e9*detrend(data_tomo_m1_Wz6.Dy_int(i_cl+1e4:end), 0), 1e9*detrend(data_tomo_m1_Wz6.Dz_int(i_cl+1e4:end), 0), 'color', colors(2,:))
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([-50:50:50])
2024-11-15 18:15:13 +01:00
2025-02-01 18:35:04 +01:00
ax3 = nexttile;
hold on;
plot(1e9*detrend(data_tomo_m2_Wz6.Dy_int(i_cl+1e4:end), 0), 1e9*detrend(data_tomo_m2_Wz6.Dz_int(i_cl+1e4:end), 0), 'color', colors(3,:))
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([-50:50:50])
2024-11-15 18:15:13 +01:00
2025-02-01 18:35:04 +01:00
ax4 = nexttile;
hold on;
plot(1e9*detrend(data_tomo_m3_Wz6.Dy_int(i_cl+1e4:end), 0), 1e9*detrend(data_tomo_m3_Wz6.Dz_int(i_cl+1e4:end), 0), 'color', colors(4,:))
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([-50:50:50])
2024-11-15 18:15:13 +01:00
2025-02-01 18:35:04 +01:00
linkaxes([ax1,ax2,ax3, ax4],'xy');
xlim([-450, 450]); ylim([-100, 100]);
#+end_src
2024-11-15 18:15:13 +01:00
2025-02-01 18:35:04 +01:00
#+begin_src matlab :exports none :results none
%% Tomography experiment at 1rpm with 26kg payload
figure;
hold on;
plot(1e6*data_tomo_m0_Wz6.Dx_int(1:i_cl), 1e6*data_tomo_m0_Wz6.Dy_int(1:i_cl), 'DisplayName', 'OL')
plot(1e6*data_tomo_m0_Wz6.Dx_int(i_cl:i_cl+1e4), 1e6*data_tomo_m0_Wz6.Dy_int(i_cl:i_cl+1e4), 'color', colors(3,:), 'HandleVisibility', 'off')
plot(1e6*data_tomo_m0_Wz6.Dx_int(i_cl+1e4:end), 1e6*data_tomo_m0_Wz6.Dy_int(i_cl+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', 'Best 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', 'southeast', 'FontSize', 8, 'NumColumns', 1);
leg.ItemTokenSize(1) = 15;
2024-11-15 18:15:13 +01:00
#+end_src
2025-02-01 18:35:04 +01:00
2024-11-15 18:15:13 +01:00
#+begin_src matlab :exports none :results none
%% Compute best circle fit for the displayed tomography experiment
[~, i_cl] = find(data_tomo_m0_Wz6.hac_status = = 1);
% Find best circle
[x0, y0, R] = 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) - (x0 + R*cos(data_tomo_m2_Wz6.Rz(1:i_m2)+theta(1)))).^2 + ...
(data_tomo_m2_Wz6.Dy_int(1:i_m2) - (y0 + R*sin(data_tomo_m2_Wz6.Rz(1:i_m2)+theta(1)))).^2);
delta_theta = fminsearch(fun, 0);
#+end_src
2025-01-31 16:33:25 +01:00
- [ ] Maybe show in the YZ plane?
- [ ] Add the beam size?
2024-11-15 18:15:13 +01:00
#+begin_src matlab :exports none :results none
%% Tomography experiment at 1rpm with 26kg payload
figure;
hold on;
plot(1e6*data_tomo_m2_Wz6.Dx_int(1:i_cl), 1e6*data_tomo_m2_Wz6.Dy_int(1:i_cl), 'DisplayName', 'OL')
plot(1e6*data_tomo_m2_Wz6.Dx_int(i_cl:i_cl+1e4), 1e6*data_tomo_m2_Wz6.Dy_int(i_cl:i_cl+1e4), 'color', colors(3,:), 'HandleVisibility', 'off')
plot(1e6*data_tomo_m2_Wz6.Dx_int(i_cl+1e4:end), 1e6*data_tomo_m2_Wz6.Dy_int(i_cl+1e4:end), 'color', colors(2,:), 'DisplayName', 'CL')
theta = linspace(0, 2*pi, 500); % Angle to plot the circle [rad]
plot(1e6*(x0 + R*cos(theta)), 1e6*(y0 + R*sin(theta)), 'k--', 'DisplayName', 'Best 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', 'southeast', 'FontSize', 8, 'NumColumns', 1);
leg.ItemTokenSize(1) = 15;
#+end_src
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_tomo_m2_1rpm_robust_hac_iff_fit.pdf', 'width', 'half', 'height', 'normal');
#+end_src
#+begin_src matlab :exports none :results none
%% Measured radial errors of the Spindle
figure;
hold on;
plot(1e6*(data_tomo_m2_Wz6.Dx_int(1:100:i_m2) - (x0 + R*cos(data_tomo_m2_Wz6.Rz(1:100:i_m2)+delta_theta))), ...
1e6*(data_tomo_m2_Wz6.Dy_int(1:100:i_m2) - (y0 + R*sin(data_tomo_m2_Wz6.Rz(1:100:i_m2)+delta_theta))), 'color', colors(1,:), 'DisplayName', 'OL')
plot(1e6*detrend(data_tomo_m2_Wz6.Dx_int(i_cl+1e4:100:end), 0), 1e6*detrend(data_tomo_m2_Wz6.Dy_int(i_cl+1e4:100:end), 0), 'color', colors(2,:), 'DisplayName', '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;
#+end_src
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_tomo_m2_1rpm_robust_hac_iff_fit_removed.pdf', 'width', 'half', 'height', 'normal');
#+end_src
#+name : fig:test_id31_tomo_m2_1rpm_robust_hac_iff
#+caption : Tomography experiment with rotation velocity of 1rpm, 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
The RMS values of the open-loop and closed-loop errors for all masses are summarized in Table ref:tab:test_id31_tomo_1rpm_robust_ol_cl_errors.
The obtained closed-loop errors are fulfilling the requirements, except for the $39\,\text{kg}$ payload in the lateral ($D_y$) direction.
#+begin_src matlab
%% Estimate RMS of the errors while in closed-loop and open-loop
% No mass
[~, i_m0] = find(data_tomo_m0_Wz6.hac_status = = 1);
data_tomo_m0_Wz6.Dy_rms_cl = rms(detrend(data_tomo_m0_Wz6.Dy_int(i_m0+50000:end), 0));
data_tomo_m0_Wz6.Dz_rms_cl = rms(detrend(data_tomo_m0_Wz6.Dz_int(i_m0+50000:end), 0));
data_tomo_m0_Wz6.Ry_rms_cl = rms(detrend(data_tomo_m0_Wz6.Ry_int(i_m0+50000:end), 0));
% Remove eccentricity for OL errors
[x0, y0, R] = 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) - (x0 + R*cos(data_tomo_m0_Wz6.Rz(1:i_m0)+theta(1)))).^2 + ...
(data_tomo_m0_Wz6.Dy_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.Dy_rms_ol = rms(data_tomo_m0_Wz6.Dy_int(1:i_m0) - (y0 + R*sin(data_tomo_m0_Wz6.Rz(1:i_m0)+delta_theta)));
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"
[~, i_m1] = find(data_tomo_m1_Wz6.hac_status = = 1);
data_tomo_m1_Wz6.Dy_rms_cl = rms(detrend(data_tomo_m1_Wz6.Dy_int(i_m1+50000:end), 0));
data_tomo_m1_Wz6.Dz_rms_cl = rms(detrend(data_tomo_m1_Wz6.Dz_int(i_m1+50000:end), 0));
data_tomo_m1_Wz6.Ry_rms_cl = rms(detrend(data_tomo_m1_Wz6.Ry_int(i_m1+50000:end), 0));
% Remove eccentricity for OL errors
[x0, y0, R] = 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) - (x0 + R*cos(data_tomo_m1_Wz6.Rz(1:i_m1)+theta(1)))).^2 + ...
(data_tomo_m1_Wz6.Dy_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.Dy_rms_ol = rms(data_tomo_m1_Wz6.Dy_int(1:i_m1) - (y0 + R*sin(data_tomo_m1_Wz6.Rz(1:i_m1)+delta_theta)));
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"
[~, i_m2] = find(data_tomo_m2_Wz6.hac_status = = 1);
data_tomo_m2_Wz6.Dy_rms_cl = rms(detrend(data_tomo_m2_Wz6.Dy_int(i_m2+50000:end), 0));
data_tomo_m2_Wz6.Dz_rms_cl = rms(detrend(data_tomo_m2_Wz6.Dz_int(i_m2+50000:end), 0));
data_tomo_m2_Wz6.Ry_rms_cl = rms(detrend(data_tomo_m2_Wz6.Ry_int(i_m2+50000:end), 0));
% Remove eccentricity for OL errors
[x0, y0, R] = 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) - (x0 + R*cos(data_tomo_m2_Wz6.Rz(1:i_m2)+theta(1)))).^2 + ...
(data_tomo_m2_Wz6.Dy_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.Dy_rms_ol = rms(data_tomo_m2_Wz6.Dy_int(1:i_m2) - (y0 + R*sin(data_tomo_m2_Wz6.Rz(1:i_m2)+delta_theta)));
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"
[~, i_m3] = find(data_tomo_m3_Wz6.hac_status = = 1);
data_tomo_m3_Wz6.Dy_rms_cl = rms(detrend(data_tomo_m3_Wz6.Dy_int(i_m3+50000:end), 0));
data_tomo_m3_Wz6.Dz_rms_cl = rms(detrend(data_tomo_m3_Wz6.Dz_int(i_m3+50000:end), 0));
data_tomo_m3_Wz6.Ry_rms_cl = rms(detrend(data_tomo_m3_Wz6.Ry_int(i_m3+50000:end), 0));
% Remove eccentricity for OL errors
[x0, y0, R] = 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) - (x0 + R*cos(data_tomo_m3_Wz6.Rz(1:i_m3)+theta(1)))).^2 + ...
(data_tomo_m3_Wz6.Dy_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.Dy_rms_ol = rms(data_tomo_m3_Wz6.Dy_int(1:i_m3) - (y0 + R*sin(data_tomo_m3_Wz6.Rz(1:i_m3)+delta_theta)));
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)));
#+end_src
#+name : tab:test_id31_tomo_1rpm_robust_ol_cl_errors
#+caption : RMS values of the measured errors during open-loop and closed-loop tomography scans (1rpm) for all considered payloads. Measured closed-Loop errors are indicated by "bold" font.
#+attr_latex : :environment tabularx :width 0.9\linewidth :align Xccc
#+attr_latex : :center t :booktabs t
| | $D_y$ | $D_z$ | $R_y$ |
|------------------+----------------------------------------------+--------------------------------------------+--------------------------------------------------|
| $0\,kg$ | $142 \Longrightarrow \bm{15}\,\text{nm RMS}$ | $32 \Longrightarrow \bm{5}\,\text{nm RMS}$ | $460 \Longrightarrow \bm{55}\,\text{nrad RMS}$ |
| $13\,kg$ | $149 \Longrightarrow \bm{25}\,\text{nm RMS}$ | $26 \Longrightarrow \bm{6}\,\text{nm RMS}$ | $470 \Longrightarrow \bm{55}\,\text{nrad RMS}$ |
| $26\,kg$ | $202 \Longrightarrow \bm{25}\,\text{nm RMS}$ | $36 \Longrightarrow \bm{7}\,\text{nm RMS}$ | $1700 \Longrightarrow \bm{103}\,\text{nrad RMS}$ |
| $39\,kg$ | $297 \Longrightarrow \bm{53}\,\text{nm RMS}$ | $38 \Longrightarrow \bm{9}\,\text{nm RMS}$ | $1700 \Longrightarrow \bm{169}\,\text{nrad RMS}$ |
|------------------+----------------------------------------------+--------------------------------------------+--------------------------------------------------|
| *Specifications* | $30\,\text{nmRMS}$ | $15\,\text{nmRMS}$ | $250\,\text{nradRMS}$ |
#+begin_src matlab :exports results :results value table replace :tangle no :post addhdr(*this* )
data2orgtable([1e9*data_tomo_m0_Wz6.Dy_rms_ol, 1e9*data_tomo_m0_Wz6.Dz_rms_ol, 1e9*data_tomo_m0_Wz6.Ry_rms_ol; ...
1e9*data_tomo_m1_Wz6.Dy_rms_ol, 1e9*data_tomo_m1_Wz6.Dz_rms_ol, 1e9*data_tomo_m1_Wz6.Ry_rms_ol; ...
1e9*data_tomo_m2_Wz6.Dy_rms_ol, 1e9*data_tomo_m2_Wz6.Dz_rms_ol, 1e9*data_tomo_m2_Wz6.Ry_rms_ol; ...
1e9*data_tomo_m3_Wz6.Dy_rms_ol, 1e9*data_tomo_m3_Wz6.Dz_rms_ol, 1e9*data_tomo_m3_Wz6.Ry_rms_ol], ...
{'$m_0$', '$m_1$', '$m_2$', '$m_3$'}, {'$D_y$ [$\mu m$]', '$D_z$ [$nm$]', '$R_y$ [$\mu\text{rad}$]'}, ' %.0f ');
#+end_src
#+begin_src matlab :exports results :results value table replace :tangle no :post addhdr(*this* )
data2orgtable([1e9*data_tomo_m0_Wz6.Dx_rms_cl, 1e9*data_tomo_m0_Wz6.Dy_rms_cl, 1e9*data_tomo_m0_Wz6.Dz_rms_cl, 1e9*data_tomo_m0_Wz6.Rx_rms_cl, 1e9*data_tomo_m0_Wz6.Ry_rms_cl; ...
1e9*data_tomo_m1_Wz6.Dx_rms_cl, 1e9*data_tomo_m1_Wz6.Dy_rms_cl, 1e9*data_tomo_m1_Wz6.Dz_rms_cl, 1e9*data_tomo_m1_Wz6.Rx_rms_cl, 1e9*data_tomo_m1_Wz6.Ry_rms_cl; ...
1e9*data_tomo_m2_Wz6.Dx_rms_cl, 1e9*data_tomo_m2_Wz6.Dy_rms_cl, 1e9*data_tomo_m2_Wz6.Dz_rms_cl, 1e9*data_tomo_m2_Wz6.Rx_rms_cl, 1e9*data_tomo_m2_Wz6.Ry_rms_cl; ...
1e9*data_tomo_m3_Wz6.Dx_rms_cl, 1e9*data_tomo_m3_Wz6.Dy_rms_cl, 1e9*data_tomo_m3_Wz6.Dz_rms_cl, 1e9*data_tomo_m3_Wz6.Rx_rms_cl, 1e9*data_tomo_m3_Wz6.Ry_rms_cl], ...
{'$m_0$', '$m_1$', '$m_2$', '$m_3$'}, {'$D_x$ [nm]', '$D_y$ [nm]', '$D_z$ [nm]', '$R_x$ [nrad]', '$R_y$ [nrad]'}, ' %.0f ');
#+end_src
2025-01-31 16:33:25 +01:00
** $D_y$ - Lateral Scans
<<ssec:test_id31_scans_dy >>
**** Introduction :ignore:
Lateral scans are performed with the $T_y$ stage.
The stepper motor controller[fn: 5 ] outputs the setpoint which is received by the Speedgoat.
In the Speedgoat, the setpoint is compared with the measured $D_y$ position of the top-platform, and the Nano-Hexapod is used to correct positioning errors induced by the scanning of the $T_y$ stage.
The stroke is here limited to $\pm 100\,\mu m$ due to the limited acceptance of the metrology system.
2024-11-15 18:15:13 +01:00
2025-01-31 16:33:25 +01:00
**** Slow scan
2024-11-15 18:15:13 +01:00
2025-01-31 16:33:25 +01:00
The $T_y$ stage is first scanned with a velocity of $10\,\mu m/s$ which is typical for such experiments.
The errors in open-loop (i.e. without using the NASS) and in closed-loop are compared in Figure ref:fig:test_id31_dy_10ums.
2024-11-15 18:15:13 +01:00
2025-01-31 16:33:25 +01:00
In the direction of motion, periodic errors can be observed in the open-loop case (Figure ref:fig:test_id31_dy_10ums_dy).
These errors are induced by the stepper motor being used in the $T_y$ stage.
Indeed, stepper motors inherently have "micro-stepping errors" which are periodic errors happening 200 times per motor rotation with an amplitude approximately equal to $1\,\text{mrad}$.
As the lead screw for the $T_y$ stage has a pitch of $2\,mm$, this means that the micro-stepping errors have a period of $10\,\mu m$ and an amplitude of $\approx 300\,nm$ which can indeed be seen in open-loop.
2024-11-15 18:15:13 +01:00
2025-01-31 16:33:25 +01:00
In the vertical direction (Figure ref:fig:test_id31_dy_10ums_dz), open-loop errors are most likely due to measurement errors of the metrology itself as the top interferometer point at a sphere (see Figure ref:fig:test_id31_xy_map_sphere).
In closed-loop, the errors are within the specifications in all directions.
2024-11-15 18:15:13 +01:00
#+begin_src matlab
2025-01-31 16:33:25 +01:00
%% Slow Ty scan (10um/s) - OL
data_ty_ol_slow = load("2023-08-21_20-05_ty_scan_m1_open_loop_slow.mat");
data_ty_ol_slow.time = Ts*[0:length(data_ty_ol_slow.Dy_int)-1];
%% Slow Ty scan (10um/s) - CL
data_ty_cl_slow = load("2023-08-21_20-07_ty_scan_m1_cf_closed_loop_slow.mat");
data_ty_cl_slow.time = Ts*[0:length(data_ty_cl_slow.Dy_int)-1];
2024-11-15 18:15:13 +01:00
#+end_src
2025-01-31 16:33:25 +01:00
#+begin_src matlab :exports none :results none
%% Ty scan (at 10um/s) - Dy errors
figure;
hold on;
plot(1e6*data_ty_ol_slow.Ty, 1e6*detrend(data_ty_ol_slow.e_dy, 0), ...
'DisplayName', 'Open-loop')
plot(1e6*data_ty_cl_slow.Ty, 1e6*detrend(data_ty_cl_slow.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;
2024-11-15 18:15:13 +01:00
#+end_src
2025-01-31 16:33:25 +01:00
#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/test_id31_dy_10ums_dy.pdf', 'width', 'third', 'height', 'normal');
2024-11-15 18:15:13 +01:00
#+end_src
2025-01-31 16:33:25 +01:00
#+begin_src matlab :exports none :results none
%% Ty scan (at 10um/s) - Dz and Ry errors
2024-11-15 18:15:13 +01:00
figure;
2025-01-31 16:33:25 +01:00
hold on;
plot(1e6*data_ty_ol_slow.Ty, 1e6*detrend(data_ty_ol_slow.e_dz, 0), ...
'DisplayName', 'Open-loop')
plot(1e6*data_ty_cl_slow.Ty, 1e6*detrend(data_ty_cl_slow.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;
#+end_src
#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/test_id31_dy_10ums_dz.pdf', 'width', 'third', 'height', 'normal');
#+end_src
2024-11-15 18:15:13 +01:00
2025-01-31 16:33:25 +01:00
#+begin_src matlab :exports none :results none
figure;
2024-11-15 18:15:13 +01:00
hold on;
2025-01-31 16:33:25 +01:00
plot(1e6*data_ty_ol_slow.Ty, 1e6*data_ty_ol_slow.e_ry, ...
'DisplayName', 'Open-loop')
plot(1e6*data_ty_cl_slow.Ty, 1e6*data_ty_cl_slow.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');
2024-11-15 18:15:13 +01:00
hold off;
2025-01-31 16:33:25 +01:00
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;
2024-11-15 18:15:13 +01:00
#+end_src
2025-01-31 16:33:25 +01:00
#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/test_id31_dy_10ums_ry.pdf', 'width', 'third', 'height', 'normal');
#+end_src
2024-11-15 18:15:13 +01:00
2025-01-31 16:33:25 +01:00
#+name : fig:test_id31_dy_10ums
#+caption : Open-Loop (in blue) and Closed-loop (i.e. using the NASS, in red) during a $10\,\mu m/s$ scan with the $T_y$ stage. Errors in $D_y$ is shown in (\subref{fig:test_id31_dy_10ums_dy}).
#+attr_latex : :options [htbp]
#+begin_figure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_dy_10ums_dy} $D_y$}
#+attr_latex : :options {0.33\textwidth}
#+begin_subfigure
#+attr_latex : :scale 1
[[file:figs/test_id31_dy_10ums_dy.png ]]
#+end_subfigure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_dy_10ums_dz} $D_z$}
#+attr_latex : :options {0.33\textwidth}
#+begin_subfigure
#+attr_latex : :scale 1
[[file:figs/test_id31_dy_10ums_dz.png ]]
#+end_subfigure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_dy_10ums_ry} $R_y$}
#+attr_latex : :options {0.33\textwidth}
#+begin_subfigure
#+attr_latex : :scale 1
[[file:figs/test_id31_dy_10ums_ry.png ]]
#+end_subfigure
#+end_figure
2024-11-15 18:15:13 +01:00
2025-01-31 16:33:25 +01:00
**** Faster Scan
2024-11-15 18:15:13 +01:00
2025-01-31 16:33:25 +01:00
The performance of the NASS is then tested for a scanning velocity of $100\,\mu m/s$ and the results are shown in Figure ref:fig:test_id31_dy_100ums.
At this velocity, the micro-stepping errors have a frequency of $10\,\text{Hz}$ and are inducing lot's of vibrations which are even amplified by some resonances of the micro-station.
These vibrations are outside the bandwidth of the NASS feedback controller and therefore not well reduced in closed-loop.
2024-11-15 18:15:13 +01:00
2025-01-31 16:33:25 +01:00
This is the main reason why stepper motors should be not be used for "long-stroke / short-stroke" systems when good scanning performances are wanted [[cite:&dehaeze22_fastj_uhv ]].
In order to improve the scanning performances at high velocity, the stepper motor of the $T_y$ stage could be replaced by a three-phase torque motor for instance.
As the closed-loop errors in $D_z$ and $R_y$ directions are within specifications (see Figures ref:fig:test_id31_dy_100ums_dz and ref:fig:test_id31_dy_100ums_ry), another option would be to trigger the detectors based on the measured $D_y$ position instead of based on time or on the $T_y$ setpoint.
This would make the experiment less sensitive to $D_y$ vibrations.
For small $D_y$ scans, the nano-hexapod alone can be used for the scans, but with limited strokes.
2024-11-15 18:15:13 +01:00
#+begin_src matlab
2025-01-31 16:33:25 +01:00
%% Fast Ty scan (100um/s) - OL
data_ty_ol_fast = load("2023-08-21_20-05_ty_scan_m1_open_loop.mat");
data_ty_ol_fast.time = Ts*[0:length(data_ty_ol_fast.Dy_int)-1];
2024-11-15 18:15:13 +01:00
2025-01-31 16:33:25 +01:00
%% Fast Ty scan (10um/s) - CL
data_ty_cl_fast = load("2023-08-21_20-07_ty_scan_m1_cf_closed_loop.mat");
data_ty_cl_fast.time = Ts*[0:length(data_ty_cl_fast.Dy_int)-1];
2024-11-15 18:15:13 +01:00
#+end_src
2025-01-31 16:33:25 +01:00
#+begin_src matlab :exports none :results none
%% Ty scan (at 100um/s) - Dy errors
2024-11-15 18:15:13 +01:00
figure;
hold on;
2025-01-31 16:33:25 +01:00
plot(1e6*data_ty_ol_fast.Ty, 1e6*detrend(data_ty_ol_fast.e_dy, 0), ...
'DisplayName', 'Open-loop')
plot(1e6*data_ty_cl_fast.Ty, 1e6*detrend(data_ty_cl_fast.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');
2024-11-15 18:15:13 +01:00
hold off;
2025-01-31 16:33:25 +01:00
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;
2024-11-15 18:15:13 +01:00
#+end_src
2025-01-31 16:33:25 +01:00
#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/test_id31_dy_100ums_dy.pdf', 'width', 'third', 'height', 'normal');
2024-11-15 18:15:13 +01:00
#+end_src
2025-01-31 16:33:25 +01:00
#+begin_src matlab :exports none :results none
%% Ty scan (at 100um/s) - Dz and Ry errors
figure;
hold on;
plot(1e6*data_ty_ol_fast.Ty, 1e6*detrend(data_ty_ol_fast.e_dz, 0), ...
'DisplayName', 'Open-loop')
plot(1e6*data_ty_cl_fast.Ty, 1e6*detrend(data_ty_cl_fast.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;
2024-11-15 18:15:13 +01:00
#+end_src
2025-01-31 16:33:25 +01:00
#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/test_id31_dy_100ums_dz.pdf', 'width', 'third', 'height', 'normal');
2024-11-15 18:15:13 +01:00
#+end_src
2025-01-31 16:33:25 +01:00
#+begin_src matlab :exports none :results none
2024-11-15 18:15:13 +01:00
figure;
hold on;
2025-01-31 16:33:25 +01:00
plot(1e6*data_ty_ol_fast.Ty, 1e6*data_ty_ol_fast.e_ry, ...
'DisplayName', 'Open-loop')
plot(1e6*data_ty_cl_fast.Ty, 1e6*data_ty_cl_fast.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;
2024-11-15 18:15:13 +01:00
#+end_src
2025-01-31 16:33:25 +01:00
#+begin_src matlab :tangle no :exports results :results file replace
exportFig('figs/test_id31_dy_100ums_ry.pdf', 'width', 'third', 'height', 'normal');
2024-11-15 18:15:13 +01:00
#+end_src
2025-01-31 16:33:25 +01:00
#+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
2024-11-15 18:15:13 +01:00
2025-01-31 16:33:25 +01:00
**** Conclusion
2024-11-15 18:15:13 +01:00
#+begin_src matlab
2025-01-31 16:33:25 +01:00
%% Compute errors for Dy scans
i_ty_ol_slow = data_ty_ol_slow.Ty > data_ty_ol_slow.Ty(1) & data_ty_ol_slow.Ty < data_ty_ol_slow.Ty(end);
i_ty_cl_slow = data_ty_cl_slow.Ty > data_ty_cl_slow.Ty(1) & data_ty_cl_slow.Ty < data_ty_cl_slow.Ty(end);
i_ty_ol_fast = data_ty_ol_fast.Ty > data_ty_ol_fast.Ty(1) & data_ty_ol_fast.Ty < data_ty_ol_fast.Ty(end);
i_ty_cl_fast = data_ty_cl_fast.Ty > data_ty_cl_fast.Ty(1) & data_ty_cl_fast.Ty < data_ty_cl_fast.Ty(end);
2024-11-15 18:15:13 +01:00
2025-01-31 16:33:25 +01:00
% Peak to Peak errors
ty_ol_slow_dy_peak = 1e9*(max(detrend(data_ty_ol_slow.e_dy(i_ty_ol_slow), 0))-min(detrend(data_ty_ol_slow.e_dy(i_ty_ol_slow), 0)))/2;
ty_ol_slow_dz_peak = 1e9*(max(detrend(data_ty_ol_slow.e_dz(i_ty_ol_slow), 0))-min(detrend(data_ty_ol_slow.e_dz(i_ty_ol_slow), 0)))/2;
ty_ol_slow_ry_peak = 1e6*(max(detrend(data_ty_ol_slow.e_ry(i_ty_ol_slow), 0))-min(detrend(data_ty_ol_slow.e_ry(i_ty_ol_slow), 0)))/2;
2024-11-15 18:15:13 +01:00
2025-01-31 16:33:25 +01:00
ty_cl_slow_dy_peak = 1e9*(max(detrend(data_ty_cl_slow.e_dy(i_ty_cl_slow), 0))-min(detrend(data_ty_cl_slow.e_dy(i_ty_cl_slow), 0)))/2;
ty_cl_slow_dz_peak = 1e9*(max(detrend(data_ty_cl_slow.e_dz(i_ty_cl_slow), 0))-min(detrend(data_ty_cl_slow.e_dz(i_ty_cl_slow), 0)))/2;
ty_cl_slow_ry_peak = 1e6*(max(detrend(data_ty_cl_slow.e_ry(i_ty_cl_slow), 0))-min(detrend(data_ty_cl_slow.e_ry(i_ty_cl_slow), 0)))/2;
2024-11-15 18:15:13 +01:00
2025-01-31 16:33:25 +01:00
ty_ol_fast_dy_peak = 1e9*(max(detrend(data_ty_ol_fast.e_dy(i_ty_ol_fast), 0))-min(detrend(data_ty_ol_fast.e_dy(i_ty_ol_fast), 0)))/2;
ty_ol_fast_dz_peak = 1e9*(max(detrend(data_ty_ol_fast.e_dz(i_ty_ol_fast), 0))-min(detrend(data_ty_ol_fast.e_dz(i_ty_ol_fast), 0)))/2;
ty_ol_fast_ry_peak = 1e6*(max(detrend(data_ty_ol_fast.e_ry(i_ty_ol_fast), 0))-min(detrend(data_ty_ol_fast.e_ry(i_ty_ol_fast), 0)))/2;
2024-11-15 18:15:13 +01:00
2025-01-31 16:33:25 +01:00
ty_cl_fast_dy_peak = 1e9*(max(detrend(data_ty_cl_fast.e_dy(i_ty_cl_fast), 0))-min(detrend(data_ty_cl_fast.e_dy(i_ty_cl_fast), 0)))/2;
ty_cl_fast_dz_peak = 1e9*(max(detrend(data_ty_cl_fast.e_dz(i_ty_cl_fast), 0))-min(detrend(data_ty_cl_fast.e_dz(i_ty_cl_fast), 0)))/2;
ty_cl_fast_ry_peak = 1e6*(max(detrend(data_ty_cl_fast.e_ry(i_ty_cl_fast), 0))-min(detrend(data_ty_cl_fast.e_ry(i_ty_cl_fast), 0)))/2;
2024-11-15 18:15:13 +01:00
2025-01-31 16:33:25 +01:00
% RMS error
ty_ol_slow_dy_rms = 1e9*rms(detrend(data_ty_ol_slow.e_dy(i_ty_ol_slow), 0));
ty_ol_slow_dz_rms = 1e9*rms(detrend(data_ty_ol_slow.e_dz(i_ty_ol_slow), 0));
ty_ol_slow_ry_rms = 1e6*rms(detrend(data_ty_ol_slow.e_ry(i_ty_ol_slow), 0));
2024-11-15 18:15:13 +01:00
2025-01-31 16:33:25 +01:00
ty_cl_slow_dy_rms = 1e9*rms(detrend(data_ty_cl_slow.e_dy(i_ty_cl_slow), 0));
ty_cl_slow_dz_rms = 1e9*rms(detrend(data_ty_cl_slow.e_dz(i_ty_cl_slow), 0));
ty_cl_slow_ry_rms = 1e6*rms(detrend(data_ty_cl_slow.e_ry(i_ty_cl_slow), 0));
2024-11-15 18:15:13 +01:00
2025-01-31 16:33:25 +01:00
ty_ol_fast_dy_rms = 1e9*rms(detrend(data_ty_ol_fast.e_dy(i_ty_ol_fast), 0));
ty_ol_fast_dz_rms = 1e9*rms(detrend(data_ty_ol_fast.e_dz(i_ty_ol_fast), 0));
ty_ol_fast_ry_rms = 1e6*rms(detrend(data_ty_ol_fast.e_ry(i_ty_ol_fast), 0));
2024-11-15 18:15:13 +01:00
2025-01-31 16:33:25 +01:00
ty_cl_fast_dy_rms = 1e9*rms(detrend(data_ty_cl_fast.e_dy(i_ty_cl_fast), 0));
ty_cl_fast_dz_rms = 1e9*rms(detrend(data_ty_cl_fast.e_dz(i_ty_cl_fast), 0));
ty_cl_fast_ry_rms = 1e6*rms(detrend(data_ty_cl_fast.e_ry(i_ty_cl_fast), 0));
2024-11-15 18:15:13 +01:00
#+end_src
#+begin_src matlab :exports results :results value table replace :tangle no :post addhdr(*this* )
2025-01-31 16:33:25 +01:00
data2orgtable([
specs_dy_rms, specs_dz_rms, specs_ry_rms; ... % Specifications
ty_ol_slow_dy_rms, ty_ol_slow_dz_rms, ty_ol_slow_ry_rms;
ty_cl_slow_dy_rms, ty_cl_slow_dz_rms, ty_cl_slow_ry_rms;
ty_ol_fast_dy_rms, ty_ol_fast_dz_rms, ty_ol_fast_ry_rms;
ty_cl_fast_dy_rms, ty_cl_fast_dz_rms, ty_cl_fast_ry_rms], {'Specs', '10um/s (OL)', '10um/s (CL)', '100um/s (OL)', '100um/s (CL)'}, {'$D_y$', '$D_z$', '$R_y$'}, ' %.2f ');
2024-11-15 18:15:13 +01:00
#+end_src
#+RESULTS :
2025-01-31 16:33:25 +01:00
| | $D_y$ | $D_z$ | $R_y$ |
|--------------+---------+--------+-------|
| Specs | 30.0 | 15.0 | 0.25 |
|--------------+---------+--------+-------|
| 10um/s (OL) | 585.43 | 154.51 | 6.3 |
| 10um/s (CL) | 20.64 | 9.67 | 0.06 |
|--------------+---------+--------+-------|
| 100um/s (OL) | 1063.58 | 166.85 | 6.44 |
| 100um/s (CL) | 731.63 | 19.91 | 0.36 |
2024-11-15 18:15:13 +01:00
#+begin_src matlab :exports results :results value table replace :tangle no :post addhdr(*this* )
2025-01-31 16:33:25 +01:00
data2orgtable([
specs_dy_peak, specs_dz_peak, specs_ry_peak; ... % Specifications
ty_ol_slow_dy_peak, ty_ol_slow_dz_peak, ty_ol_slow_ry_peak;
ty_cl_slow_dy_peak, ty_cl_slow_dz_peak, ty_cl_slow_ry_peak;
ty_ol_fast_dy_peak, ty_ol_fast_dz_peak, ty_ol_fast_ry_peak;
ty_cl_fast_dy_peak, ty_cl_fast_dz_peak, ty_cl_fast_ry_peak], {'Specs', '10um/s (OL)', '10um/s (CL)', '100um/s (OL)', '100um/s (CL)'}, {'$D_y$', '$D_z$', '$R_y$'}, ' %.2f ');
2024-11-15 18:15:13 +01:00
#+end_src
#+RESULTS :
2025-01-31 16:33:25 +01:00
| | $D_y$ | $D_z$ | $R_y$ |
|--------------+---------+--------+-------|
| Specs | 100.0 | 50.0 | 0.85 |
| 10um/s (OL) | 1167.8 | 308.35 | 11.06 |
| 10um/s (CL) | 86.36 | 41.6 | 0.28 |
| 100um/s (OL) | 2687.67 | 328.45 | 11.26 |
| 100um/s (CL) | 1339.31 | 69.5 | 0.91 |
2024-11-15 18:15:13 +01:00
** $D_z$ scans: Dirty Layer Scans
<<ssec:test_id31_scans_dz >>
2025-01-31 16:33:25 +01:00
**** Introduction :ignore:
In some cases, samples are composed of several atomic "layers" that are first aligned in the horizontal plane with precise $R_y$ positioning and that are then scanned vertically with precise $D_z$ motion.
The vertical scan can be performed continuously of using step-by-step motion.
**** Step by Step $D_z$ motion
2024-11-15 18:15:13 +01:00
2025-01-31 16:33:25 +01:00
Vertical steps are here performed using the nano-hexapod only.
Step sizes from $10\,nm$ to $1\,\mu m$ are tested, and the results are shown in Figure ref:fig:test_id31_dz_mim_steps.
10nm steps can be resolved if detectors are integrating over 50ms (see red curve in Figure ref:fig:test_id31_dz_mim_10nm_steps), which is reasonable for many experiments.
When doing step-by-step scans, the time to reach the next value is quite critical as long settling time can render the total experiment excessively long.
The response time to reach the wanted value (to within $\pm 20\,nm$) is around $70\,ms$ as shown with the $1\,\mu m$ step response in Figure ref:fig:test_id31_dz_mim_1000nm_steps.
#+begin_src matlab
%% Load Dz steps data
2024-11-15 18:15:13 +01:00
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];
#+end_src
#+begin_src matlab :exports none :results none
%% Dz MIM test with 10nm steps
figure;
hold on;
2025-01-31 16:33:25 +01:00
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')
2024-11-15 18:15:13 +01:00
hold off;
xlabel('Time [s]');
ylabel('$D_z$ Motion [nm]');
2025-01-31 16:33:25 +01:00
legend('location', 'northwest', 'FontSize', 8, 'NumColumns', 1);
xlim([0, 0.6]);
ylim([-10, 40]);
xticks([0:0.2:0.6]);
yticks([-10:10:50]);
2024-11-15 18:15:13 +01:00
#+end_src
2025-01-31 16:33:25 +01:00
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_dz_mim_10nm_steps.pdf', 'width', 'third', 'height', 'normal');
2024-11-15 18:15:13 +01:00
#+end_src
#+begin_src matlab :exports none :results none
2025-01-31 16:33:25 +01:00
%% Dz MIM test with 100nm steps
2024-11-15 18:15:13 +01:00
figure;
hold on;
2025-01-31 16:33:25 +01:00
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')
2024-11-15 18:15:13 +01:00
hold off;
xlabel('Time [s]');
ylabel('$D_z$ Motion [nm]');
2025-01-31 16:33:25 +01:00
legend('location', 'northwest', 'FontSize', 8, 'NumColumns', 1);
xlim([0, 0.6]);
% ylim([-10, 40]);
xticks([0:0.2:0.6]);
yticks([-0:100:300]);
2024-11-15 18:15:13 +01:00
#+end_src
2025-01-31 16:33:25 +01:00
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_dz_mim_100nm_steps.pdf', 'width', 'third', 'height', 'normal');
2024-11-15 18:15:13 +01:00
#+end_src
#+begin_src matlab :exports none :results none
%% 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;
2025-01-31 16:33:25 +01:00
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--')
2024-11-15 18:15:13 +01:00
xline(0, 'k--', 'LineWidth', 1.5)
2025-01-31 16:33:25 +01:00
xline(70, 'k--', 'LineWidth', 1.5)
2024-11-15 18:15:13 +01:00
hold off;
2025-01-31 16:33:25 +01:00
xlabel('Time [ms]');
ylabel('$D_z$ Motion [$\mu$m]');
xlim([-10, 140]);
ylim([-0.1, 1.6])
xticks([0, 70])
yticks([0, 1])
2024-11-15 18:15:13 +01:00
#+end_src
2025-01-31 16:33:25 +01:00
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_dz_mim_1000nm_steps.pdf', 'width', 'third', 'height', 'normal');
2024-11-15 18:15:13 +01:00
#+end_src
2025-01-31 16:33:25 +01:00
#+name : fig:test_id31_dz_mim_steps
#+caption : Vertical steps performed with the nano-hexapod. 10nm steps are shown in (\subref{fig:test_id31_dz_mim_10nm_steps}) with the low pass filtered data corresponding to an integration time of $50\,ms$. 100nm steps are shown in (\subref{fig:test_id31_dz_mim_100nm_steps}). The response time to reach a peak to peak error of $\pm 20\,nm$ is $\approx 70\,ms$ as shown in (\subref{fig:test_id31_dz_mim_1000nm_steps}) for a $1\,\mu m$ step.
#+attr_latex : :options [htbp]
#+begin_figure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_dz_mim_10nm_steps}10nm steps}
#+attr_latex : :options {0.33\textwidth}
#+begin_subfigure
#+attr_latex : :scale 1
[[file:figs/test_id31_dz_mim_10nm_steps.png ]]
#+end_subfigure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_dz_mim_100nm_steps}100nm steps}
#+attr_latex : :options {0.33\textwidth}
#+begin_subfigure
#+attr_latex : :scale 1
[[file:figs/test_id31_dz_mim_100nm_steps.png ]]
#+end_subfigure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_dz_mim_1000nm_steps}$1\,\mu$m step}
#+attr_latex : :options {0.33\textwidth}
#+begin_subfigure
#+attr_latex : :scale 1
[[file:figs/test_id31_dz_mim_1000nm_steps.png ]]
#+end_subfigure
#+end_figure
**** Continuous $D_z$ motion: Dirty Layer Scans
2024-11-15 18:15:13 +01:00
2025-01-31 16:33:25 +01:00
Instead of performing "step-by-step" scans, continuous scans can also be performed in the vertical direction.
At $10\,\mu m/s$, the errors are well within the specifications (see Figure ref:fig:test_id31_dz_scan_10ums).
2024-11-15 18:15:13 +01:00
#+begin_src matlab
2025-01-31 16:33:25 +01:00
%% Dirty layer scans - 10um/s
2024-11-15 18:15:13 +01:00
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];
2025-01-31 16:33:25 +01:00
%% Dirty layer scans - 100um/s
2024-11-15 18:15:13 +01:00
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];
#+end_src
#+begin_src matlab :exports none :results none
2025-01-31 16:33:25 +01:00
%% Dz scan at 10um/s - Lateral error
2024-11-15 18:15:13 +01:00
figure;
hold on;
2025-01-31 16:33:25 +01:00
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');
2024-11-15 18:15:13 +01:00
hold off;
xlabel('Time [s]');
2025-01-31 16:33:25 +01:00
ylabel('$D_y$ error [nm]')
% leg = legend('location', 'northwest', 'FontSize', 8, 'NumColumns', 1);
% leg.ItemTokenSize(1) = 15;
xlim([0, 2.2]);
ylim([-150, 150])
2024-11-15 18:15:13 +01:00
#+end_src
2025-01-31 16:33:25 +01:00
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_dz_scan_10ums_dy.pdf', 'width', 'third', 'height', 'normal');
2024-11-15 18:15:13 +01:00
#+end_src
#+begin_src matlab :exports none :results none
2025-01-31 16:33:25 +01:00
%% Dz scan at 10um/s - Vertical error
2024-11-15 18:15:13 +01:00
figure;
2025-01-31 16:33:25 +01:00
yyaxis left
2024-11-15 18:15:13 +01:00
hold on;
2025-01-31 16:33:25 +01:00
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');
2024-11-15 18:15:13 +01:00
hold off;
2025-01-31 16:33:25 +01:00
ylabel('$D_z$ error [nm]');
ylim([-100, 100]);
yticks([-50:50:50]);
yyaxis right
2024-11-15 18:15:13 +01:00
hold on;
2025-01-31 16:33:25 +01:00
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')
2024-11-15 18:15:13 +01:00
hold off;
xlabel('Time [s]');
2025-01-31 16:33:25 +01:00
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]);
2024-11-15 18:15:13 +01:00
#+end_src
2025-01-31 16:33:25 +01:00
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_dz_scan_10ums_dz.pdf', 'width', 'third', 'height', 'normal');
2024-11-15 18:15:13 +01:00
#+end_src
#+begin_src matlab :exports none :results none
2025-01-31 16:33:25 +01:00
%% Dz scan at 10um/s - Ry error
2024-11-15 18:15:13 +01:00
figure;
hold on;
2025-01-31 16:33:25 +01:00
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');
2024-11-15 18:15:13 +01:00
hold off;
2025-01-31 16:33:25 +01:00
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]);
2024-11-15 18:15:13 +01:00
#+end_src
2025-01-31 16:33:25 +01:00
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_dz_scan_10ums_ry.pdf', 'width', 'third', 'height', 'normal');
2024-11-15 18:15:13 +01:00
#+end_src
2025-01-31 16:33:25 +01:00
#+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
The second tested velocity is $100\,\mu m/s$, which is the fastest velocity for $D_z$ scans when the ultimate performances is wanted (corresponding to a 1ms integration time and 100nm "resolution").
At this velocity, the positioning errors are also within the specifications except for the very start and very end of the motion (i.e. during acceleration/deceleration phases, see Figure ref:fig:test_id31_dz_scan_100ums).
However, the detectors are usually triggered only during the constant velocity phase, so this is not not an issue.
The performances during acceleration phase may also be improved by using a feedforward controller.
2024-11-15 18:15:13 +01:00
#+begin_src matlab :exports none :results none
2025-01-31 16:33:25 +01:00
%% Ry reflectivity scan - Vertical error
2024-11-15 18:15:13 +01:00
figure;
hold on;
2025-01-31 16:33:25 +01:00
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');
2024-11-15 18:15:13 +01:00
hold off;
2025-01-31 16:33:25 +01:00
xlabel('Time [s]');
ylabel('$D_y$ error [nm]')
% legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 1);
xlim([0, 2.2]);
ylim([-150, 150])
2024-11-13 17:11:40 +01:00
#+end_src
2025-01-31 16:33:25 +01:00
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_dz_scan_100ums_dy.pdf', 'width', 'third', 'height', 'normal');
2024-11-13 17:11:40 +01:00
#+end_src
2024-11-15 18:15:13 +01:00
#+begin_src matlab :exports none :results none
2024-11-13 17:11:40 +01:00
figure;
2025-01-31 16:33:25 +01:00
yyaxis left
2024-11-13 17:11:40 +01:00
hold on;
2025-01-31 16:33:25 +01:00
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');
2024-11-13 17:11:40 +01:00
hold off;
2025-01-31 16:33:25 +01:00
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]);
#+end_src
2024-11-13 17:11:40 +01:00
2025-01-31 16:33:25 +01:00
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_dz_scan_100ums_dz.pdf', 'width', 'third', 'height', 'normal');
#+end_src
#+begin_src matlab :exports none :results none
%% Ry reflectivity scan - Vertical error
figure;
2024-11-13 17:11:40 +01:00
hold on;
2025-01-31 16:33:25 +01:00
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');
2024-11-13 17:11:40 +01:00
hold off;
2025-01-31 16:33:25 +01:00
xlabel('Time [s]');
ylabel('$R_y$ error [$\mu$rad]')
% legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 1);
xlim([0, 2.2]);
ylim([-2, 2])
2024-11-13 17:11:40 +01:00
#+end_src
2025-01-31 16:33:25 +01:00
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_dz_scan_100ums_ry.pdf', 'width', 'third', 'height', 'normal');
2024-11-13 17:11:40 +01:00
#+end_src
2025-01-31 16:33:25 +01:00
#+name : fig:test_id31_dz_scan_100ums
#+caption : $D_z$ scan with a velocity of $100\,\mu m/s$. $D_z$ setpoint, measured position and error are shown in (\subref{fig:test_id31_dz_scan_100ums_dz}). Errors in $D_y$ and $R_y$ are respectively shown in (\subref{fig:test_id31_dz_scan_100ums_dy}) and (\subref{fig:test_id31_dz_scan_100ums_ry})
#+attr_latex : :options [htbp]
#+begin_figure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_dz_scan_100ums_dy}$D_y$}
#+attr_latex : :options {0.33\textwidth}
#+begin_subfigure
#+attr_latex : :scale 1
[[file:figs/test_id31_dz_scan_100ums_dy.png ]]
#+end_subfigure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_dz_scan_100ums_dz}$D_z$}
#+attr_latex : :options {0.33\textwidth}
#+begin_subfigure
#+attr_latex : :scale 1
[[file:figs/test_id31_dz_scan_100ums_dz.png ]]
#+end_subfigure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_dz_scan_100ums_ry}$R_y$}
#+attr_latex : :options {0.33\textwidth}
#+begin_subfigure
#+attr_latex : :scale 1
[[file:figs/test_id31_dz_scan_100ums_ry.png ]]
#+end_subfigure
#+end_figure
**** Summary
2024-11-13 17:11:40 +01:00
2024-11-15 18:15:13 +01:00
#+begin_src matlab
2025-01-31 16:33:25 +01:00
%% Performances for Dz scans
% 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_100ums = abs(diff(data_dz_100ums.m_hexa_dz)/Ts-100e-6) < 10*eps;
% i_dz_10ums = data_dz_10ums.m_hexa_dz>data_dz_10ums.m_hexa_dz(1) & data_dz_10ums.m_hexa_dz<data_dz_10ums.m_hexa_dz(end);
% i_dz_100ums = data_dz_100ums.m_hexa_dz>data_dz_100ums.m_hexa_dz(1) & data_dz_100ums.m_hexa_dz<data_dz_100ums.m_hexa_dz(end);
% Peak to Peak errors
dz_10ums_dy_peak = 1e9*(max(detrend(data_dz_10ums.e_dy(i_dz_10ums), 0))-min(detrend(data_dz_10ums.e_dy(i_dz_10ums), 0)))/2;
dz_10ums_dz_peak = 1e9*(max(detrend(data_dz_10ums.e_dz(i_dz_10ums), 0))-min(detrend(data_dz_10ums.e_dz(i_dz_10ums), 0)))/2;
dz_10ums_ry_peak = 1e6*(max(detrend(data_dz_10ums.e_ry(i_dz_10ums), 0))-min(detrend(data_dz_10ums.e_ry(i_dz_10ums), 0)))/2;
dz_100ums_dy_peak = 1e9*(max(detrend(data_dz_100ums.e_dy(i_dz_100ums), 0))-min(detrend(data_dz_100ums.e_dy(i_dz_100ums), 0)))/2;
dz_100ums_dz_peak = 1e9*(max(detrend(data_dz_100ums.e_dz(i_dz_100ums), 0))-min(detrend(data_dz_100ums.e_dz(i_dz_100ums), 0)))/2;
dz_100ums_ry_peak = 1e6*(max(detrend(data_dz_100ums.e_ry(i_dz_100ums), 0))-min(detrend(data_dz_100ums.e_ry(i_dz_100ums), 0)))/2;
% RMS error
dz_10ums_dy_rms = 1e9*rms(detrend(data_dz_10ums.e_dy(i_dz_10ums), 0));
dz_10ums_dz_rms = 1e9*rms(detrend(data_dz_10ums.e_dz(i_dz_10ums), 0));
dz_10ums_ry_rms = 1e6*rms(detrend(data_dz_10ums.e_ry(i_dz_10ums), 0));
dz_100ums_dy_rms = 1e9*rms(detrend(data_dz_100ums.e_dy(i_dz_100ums), 0));
dz_100ums_dz_rms = 1e9*rms(detrend(data_dz_100ums.e_dz(i_dz_100ums), 0));
dz_100ums_ry_rms = 1e6*rms(detrend(data_dz_100ums.e_ry(i_dz_100ums), 0));
2024-11-15 18:15:13 +01:00
#+end_src
2024-11-13 17:11:40 +01:00
2025-01-31 16:33:25 +01:00
#+begin_src matlab :exports results :results value table replace :tangle no :post addhdr(*this* )
data2orgtable([
specs_dy_peak, specs_dz_peak, specs_ry_peak; ... % Specifications
dz_10ums_dy_peak, dz_10ums_dz_peak, dz_10ums_ry_peak;
dz_100ums_dy_peak, dz_100ums_dz_peak, dz_100ums_ry_peak], {'Specs', '10um/s', '100um/s'}, {'$D_y$', '$D_z$', '$R_y$'}, ' %.2f ');
#+end_src
#+RESULTS :
| | $D_y$ | $D_z$ | $R_y$ |
|---------+-------+-------+-------|
| Specs | 100.0 | 50.0 | 0.85 |
| 10um/s | 82.35 | 17.94 | 0.41 |
| 100um/s | 98.72 | 41.45 | 0.48 |
#+begin_src matlab :exports results :results value table replace :tangle no :post addhdr(*this* )
data2orgtable([
specs_dy_rms, specs_dz_rms, specs_ry_rms; ... % Specifications
dz_10ums_dy_rms, dz_10ums_dz_rms, dz_10ums_ry_rms;
dz_100ums_dy_rms, dz_100ums_dz_rms, dz_100ums_ry_rms], {'Specs', '10um/s', '100um/s'}, {'$D_y$', '$D_z$', '$R_y$'}, ' %.2f ');
2024-11-15 18:15:13 +01:00
#+end_src
2024-11-13 17:11:40 +01:00
2025-01-31 16:33:25 +01:00
#+RESULTS :
| | $D_y$ | $D_z$ | $R_y$ |
|---------+-------+-------+-------|
| Specs | 30.0 | 15.0 | 0.25 |
| 10um/s | 25.11 | 5.04 | 0.11 |
| 100um/s | 34.84 | 9.08 | 0.13 |
** $R_y$ scans: Reflectivity
<<ssec:test_id31_scans_reflectivity >>
2024-11-13 17:11:40 +01:00
2025-01-31 16:33:25 +01:00
X-ray reflectivity consists of scanning the $R_y$ angle of thin structures (typically solid/liquid interfaces) through the beam.
Here, a $R_y$ scan is performed with a rotational velocity of $100\,\mu rad/s$ and the positioning errors in closed-loop are recorded (Figure ref:fig:test_id31_reflectivity).
It is shown that the NASS is able to keep the point of interest in the beam within specifications.
2024-11-13 17:11:40 +01:00
2025-01-31 16:33:25 +01:00
#+begin_src matlab
%% 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];
#+end_src
2024-11-13 17:11:40 +01:00
2024-11-15 18:15:13 +01:00
#+begin_src matlab :exports none :results none
2025-01-31 16:33:25 +01:00
%% Ry reflectivity scan - Lateral error
2024-11-15 18:15:13 +01:00
figure;
2024-11-13 17:11:40 +01:00
hold on;
2025-01-31 16:33:25 +01:00
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');
2024-11-13 17:11:40 +01:00
hold off;
2025-01-31 16:33:25 +01:00
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]);
2024-11-15 18:15:13 +01:00
#+end_src
2024-11-13 17:11:40 +01:00
2025-01-31 16:33:25 +01:00
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_reflectivity_dy.pdf', 'width', 'third', 'height', 'normal');
2024-11-15 18:15:13 +01:00
#+end_src
2024-11-13 17:11:40 +01:00
2024-11-15 18:15:13 +01:00
#+begin_src matlab :exports none :results none
2025-01-31 16:33:25 +01:00
%% Ry reflectivity scan - Vertical error
2024-11-15 18:15:13 +01:00
figure;
2024-11-13 17:11:40 +01:00
hold on;
2025-01-31 16:33:25 +01:00
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');
2024-11-15 18:15:13 +01:00
hold off;
2025-01-31 16:33:25 +01:00
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]);
#+end_src
2024-11-13 17:11:40 +01:00
2025-01-31 16:33:25 +01:00
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_reflectivity_dz.pdf', 'width', 'third', 'height', 'normal');
#+end_src
#+begin_src matlab :exports none :results none
%% Ry reflectivity scan - Setpoint and Error
figure;
yyaxis left
2024-11-15 18:15:13 +01:00
hold on;
2025-01-31 16:33:25 +01:00
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');
2024-11-13 17:11:40 +01:00
hold off;
2025-01-31 16:33:25 +01:00
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]);
2024-11-13 17:11:40 +01:00
#+end_src
2025-01-31 16:33:25 +01:00
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_reflectivity_ry.pdf', 'width', 'third', 'height', 'normal');
2024-11-13 17:11:40 +01:00
#+end_src
2025-01-31 16:33:25 +01:00
#+name : fig:test_id31_reflectivity
#+caption : Reflectivity scan ($R_y$) with a rotational velocity of $100\,\mu \text{rad}/s$.
#+attr_latex : :options [htbp]
#+begin_figure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_reflectivity_dy}$D_y$}
#+attr_latex : :options {0.33\textwidth}
#+begin_subfigure
#+attr_latex : :scale 1
[[file:figs/test_id31_reflectivity_dy.png ]]
#+end_subfigure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_reflectivity_dz}$D_z$}
#+attr_latex : :options {0.33\textwidth}
#+begin_subfigure
#+attr_latex : :scale 1
[[file:figs/test_id31_reflectivity_dz.png ]]
#+end_subfigure
#+attr_latex : :caption \subcaption{\label{fig:test_id31_reflectivity_ry}$R_y$}
#+attr_latex : :options {0.33\textwidth}
#+begin_subfigure
#+attr_latex : :scale 1
[[file:figs/test_id31_reflectivity_ry.png ]]
#+end_subfigure
#+end_figure
2024-11-13 17:11:40 +01:00
2024-11-15 18:15:13 +01:00
** Combined $R_z$ and $D_y$: Diffraction Tomography
<<ssec:test_id31_scans_diffraction_tomo >>
2025-01-31 16:33:25 +01:00
In diffraction tomography, the micro-station performs combined $R_z$ rotation and $D_y$ lateral scans.
Here the spindle is performing a continuous 1rpm rotation while the nano-hexapod is used to perform fast $D_y$ scans.
The $T_y$ stage is here not used as the stepper motor would induce high frequency vibrations, therefore the stroke is here limited to $\approx \pm 100\,\mu m/s$.
Several $D_y$ velocities are tested: $0.1\,mm/s$, $0.5\,mm/s$ and $1\,mm/s$.
The $D_y$ setpoint and the measured positions are shown for all tested velocities in Figure ref:fig:test_id31_diffraction_tomo_setpoint.
2024-11-13 17:11:40 +01:00
2024-11-15 18:15:13 +01:00
#+begin_src matlab
%% 100um/s - Robust controller
data_dt_100ums = load("2023-08-18_17-12_diffraction_tomo_m0.mat");
2025-01-31 16:33:25 +01:00
t = Ts*[0:length(data_dt_100ums.Dy_int)-1];
data_dt_100ums = structfun(@(field) field(t>1.0861),data_dt_100ums, 'UniformOutput', false)
2024-11-15 18:15:13 +01:00
data_dt_100ums.time = Ts*[0:length(data_dt_100ums.Dy_int)-1];
2024-11-13 17:11:40 +01:00
2024-11-15 18:15:13 +01:00
%% 500um/s - Complementary filters
data_dt_500ums = load("2023-08-21_15-15_diffraction_tomo_m0_fast_cf.mat");
2025-01-31 16:33:25 +01:00
t = Ts*[0:length(data_dt_500ums.Dy_int)-1];
data_dt_500ums = structfun(@(field) field(t>0.275),data_dt_500ums, 'UniformOutput', false)
2024-11-15 18:15:13 +01:00
data_dt_500ums.time = Ts*[0:length(data_dt_500ums.Dy_int)-1];
2024-11-13 17:11:40 +01:00
2024-11-15 18:15:13 +01:00
%% 1mm/s - Complementary filters
data_dt_1000ums = load("2023-08-21_15-16_diffraction_tomo_m0_fast_cf.mat");
2025-01-31 16:33:25 +01:00
t = Ts*[0:length(data_dt_1000ums.Dy_int)-1];
data_dt_1000ums = structfun(@(field) field(t>0.19),data_dt_1000ums, 'UniformOutput', false)
2024-11-15 18:15:13 +01:00
data_dt_1000ums.time = Ts*[0:length(data_dt_1000ums.Dy_int)-1];
2024-11-13 17:11:40 +01:00
#+end_src
2024-11-15 18:15:13 +01:00
#+begin_src matlab :exports none :results none
%% Dy motion for several configured velocities
2024-11-13 17:11:40 +01:00
figure;
hold on;
2025-01-31 16:33:25 +01:00
plot(data_dt_1000ums.time, 1e6*data_dt_1000ums.Dy_int, 'color', colors(1,:), ...
2024-11-15 18:15:13 +01:00
'DisplayName', '$1 mm/s$')
2025-01-31 16:33:25 +01:00
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,:), ...
2024-11-15 18:15:13 +01:00
'DisplayName', '$0.5 mm/s$')
2025-01-31 16:33:25 +01:00
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,:), ...
2024-11-15 18:15:13 +01:00
'DisplayName', '$0.1 mm/s$')
2025-01-31 16:33:25 +01:00
plot(data_dt_100ums.time, 1e6*data_dt_100ums.m_hexa_dy, 'k--', ...
'DisplayName', 'Setpoint')
2024-11-13 17:11:40 +01:00
hold off;
2024-11-15 18:15:13 +01:00
xlim([0, 4]);
2025-01-31 16:33:25 +01:00
ylim([-110, 110]);
2024-11-15 18:15:13 +01:00
xlabel('Time [s]');
ylabel('$D_y$ position [$\mu$m]')
legend('location', 'southeast', 'FontSize', 8, 'NumColumns', 1);
2024-11-13 17:11:40 +01:00
#+end_src
2024-11-15 18:15:13 +01:00
#+begin_src matlab :tangle no :exports results :results file replace
2025-01-31 16:33:25 +01:00
exportFig('figs/test_id31_diffraction_tomo_setpoint.pdf', 'width', 'wide', 'height', 'normal');
2024-11-15 18:15:13 +01:00
#+end_src
2025-01-31 16:33:25 +01:00
#+name : fig:test_id31_diffraction_tomo_setpoint
2024-11-15 18:15:13 +01:00
#+caption : Dy motion for several configured velocities
#+RESULTS :
2025-01-31 16:33:25 +01:00
[[file:figs/test_id31_diffraction_tomo_setpoint.png ]]
2024-11-13 17:11:40 +01:00
2025-01-31 16:33:25 +01:00
The measured errors in $D_y$, $D_z$ and $R_y$ directions are shown in Figure ref:fig:test_id31_diffraction_tomo.
While the $D_z$ and $R_y$ errors are within specifications (see Figures ref:fig:test_id31_diffraction_tomo_dz and ref:fig:test_id31_diffraction_tomo_ry), the lateral error goes outside of specifications during acceleration and deceleration phases (Figure ref:fig:test_id31_diffraction_tomo_dy).
However, it goes out of specifications during only during $\approx 20\,ms$, and this could be optimized using feedforward and more appropriate setpoint signals.
2024-11-13 17:11:40 +01:00
2025-01-31 16:33:25 +01:00
#+begin_src matlab :exports none :results none
%% 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;
2025-02-01 18:35:04 +01:00
xlim([0, 3]);
2025-01-31 16:33:25 +01:00
xlabel('Time [s]');
ylabel('$D_y$ error [nm]')
2025-02-01 18:35:04 +01:00
leg = legend('location', 'northeast', 'FontSize', 8, 'NumColumns', 1);
2025-01-31 16:33:25 +01:00
leg.ItemTokenSize(1) = 15;
#+end_src
2024-11-13 17:11:40 +01:00
2025-01-31 16:33:25 +01:00
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_diffraction_tomo_dy.pdf', 'width', 'third', 'height', 'normal');
#+end_src
2024-11-13 17:11:40 +01:00
2025-01-31 16:33:25 +01:00
#+begin_src matlab :exports none :results none
%% 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;
#+end_src
2024-11-13 17:11:40 +01:00
2025-01-31 16:33:25 +01:00
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_diffraction_tomo_dz.pdf', 'width', 'third', 'height', 'normal');
#+end_src
2024-11-13 17:11:40 +01:00
#+begin_src matlab :exports none :results none
2025-01-31 16:33:25 +01:00
%% Diffraction Tomography - Ry errors for several configured velocities
2024-11-13 17:11:40 +01:00
figure;
hold on;
2024-11-15 18:15:13 +01:00
plot(data_dt_1000ums.time, 1e6*data_dt_1000ums.Ry_int, ...
2025-01-31 16:33:25 +01:00
'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');
2024-11-13 17:11:40 +01:00
hold off;
2025-01-31 16:33:25 +01:00
xlim([0, 4]);
ylim([-1.5, 1.5])
2024-11-15 18:15:13 +01:00
xlabel('Time [s]');
2025-01-31 16:33:25 +01:00
ylabel('$R_y$ position [$\mu$rad]')
leg = legend('location', 'southwest', 'FontSize', 8, 'NumColumns', 1);
leg.ItemTokenSize(1) = 15;
2024-11-13 17:11:40 +01:00
#+end_src
2025-01-31 16:33:25 +01:00
#+begin_src matlab :tangle no :exports results :results file none
exportFig('figs/test_id31_diffraction_tomo_ry.pdf', 'width', 'third', 'height', 'normal');
#+end_src
#+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 1rpm).
#+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
#+begin_src matlab
%% 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_dy<data_dt_100ums.m_hexa_dy(end);
[~, i_acc] = find(diff(i_dt_100ums)>0); % 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_dy<data_dt_500ums.m_hexa_dy(end);
[~, i_acc] = find(diff(i_dt_500ums)>0); % 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_dy<data_dt_1000ums.m_hexa_dy(end);
[~, i_acc] = find(diff(i_dt_1000ums)>0); % 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
% Peak to Peak errors
dt_100ums_dy_peak = 1e9*(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 = 1e9*(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 = 1e6*(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 = 1e9*(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 = 1e9*(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 = 1e6*(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 = 1e9*(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 = 1e9*(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 = 1e6*(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
dt_100ums_dy_rms = 1e9*(rms(detrend(data_dt_100ums.Dy_int(i_dt_100ums)-data_dt_100ums.m_hexa_dy(i_dt_100ums), 0)));
dt_100ums_dz_rms = 1e9*(rms(detrend(data_dt_100ums.Dz_int(i_dt_100ums), 0)));
dt_100ums_ry_rms = 1e6*(rms(detrend(data_dt_100ums.Ry_int(i_dt_100ums), 0)));
dt_500ums_dy_rms = 1e9*(rms(detrend(data_dt_500ums.Dy_int(i_dt_500ums)-data_dt_500ums.m_hexa_dy(i_dt_500ums), 0)));
dt_500ums_dz_rms = 1e9*(rms(detrend(data_dt_500ums.Dz_int(i_dt_500ums), 0)));
dt_500ums_ry_rms = 1e6*(rms(detrend(data_dt_500ums.Ry_int(i_dt_500ums), 0)));
dt_1000ums_dy_rms = 1e9*(rms(detrend(data_dt_1000ums.Dy_int(i_dt_1000ums)-data_dt_1000ums.m_hexa_dy(i_dt_1000ums), 0)));
dt_1000ums_dz_rms = 1e9*(rms(detrend(data_dt_1000ums.Dz_int(i_dt_1000ums), 0)));
dt_1000ums_ry_rms = 1e6*(rms(detrend(data_dt_1000ums.Ry_int(i_dt_1000ums), 0)));
#+end_src
#+begin_src matlab :exports results :results value table replace :tangle no :post addhdr(*this* )
data2orgtable([
specs_dy_peak, specs_dz_peak, specs_ry_peak; ... % Specifications
dt_100ums_dy_peak, dt_100ums_dz_peak, dt_100ums_ry_peak;
dt_500ums_dy_peak, dt_500ums_dz_peak, dt_500ums_ry_peak;
dt_1000ums_dy_peak, dt_1000ums_dz_peak, dt_1000ums_ry_peak
], {'Specs', '0.1 mm/s', '0.5 mm/s', '1 mm/s'}, {'Velocity', '$D_y$ [nmRMS]', '$D_z$ [nmRMS]', '$R_y$ [$\mu\text{radRMS}$]'}, ' %.2f ');
2024-11-15 18:15:13 +01:00
#+end_src
#+RESULTS :
2025-01-31 16:33:25 +01:00
| Velocity | $D_y$ [nmRMS] | $D_z$ [nmRMS] | $R_y$ [$\mu\text{radRMS}$] |
|----------+---------------+---------------+----------------------------|
| Specs | 100.0 | 50.0 | 0.85 |
| 0.1 mm/s | 208.25 | 35.33 | 0.73 |
| 0.5 mm/s | 117.94 | 28.03 | 0.27 |
| 1 mm/s | 186.88 | 33.02 | 0.53 |
2024-11-15 18:15:13 +01:00
#+begin_src matlab :exports results :results value table replace :tangle no :post addhdr(*this* )
2025-01-31 16:33:25 +01:00
data2orgtable([
specs_dy_rms, specs_dz_rms, specs_ry_rms; ... % Specifications
dt_100ums_dy_rms, dt_100ums_dz_rms, dt_100ums_ry_rms;
dt_500ums_dy_rms, dt_500ums_dz_rms, dt_500ums_ry_rms;
dt_1000ums_dy_rms, dt_1000ums_dz_rms, dt_1000ums_ry_rms
], {'Specs', '0.1 mm/s', '0.5 mm/s', '1 mm/s'}, {'Velocity', '$D_y$ [nmRMS]', '$D_z$ [nmRMS]', '$R_y$ [$\mu\text{radRMS}$]'}, ' %.2f ');
2024-11-13 17:11:40 +01:00
#+end_src
2025-01-31 16:33:25 +01:00
#+RESULTS :
| Velocity | $D_y$ [nmRMS] | $D_z$ [nmRMS] | $R_y$ [$\mu\text{radRMS}$] |
|----------+---------------+---------------+----------------------------|
| Specs | 30.0 | 15.0 | 0.25 |
| 0.1 mm/s | 36.18 | 7.35 | 0.11 |
| 0.5 mm/s | 28.58 | 7.52 | 0.08 |
| 1 mm/s | 53.05 | 9.84 | 0.14 |
2024-11-15 18:15:13 +01:00
#+name : tab:id31_diffraction_tomo_results
2025-01-31 16:33:25 +01:00
#+caption : Obtained errors during diffraction tomography experiments for several $D_y$ velocities
2024-11-15 18:15:13 +01:00
#+attr_latex : :environment tabularx :width \linewidth :align lXX
#+attr_latex : :center t :booktabs t
2024-11-13 17:11:40 +01:00
#+RESULTS :
2024-11-15 18:15:13 +01:00
** Conclusion
:PROPERTIES:
:UNNUMBERED: t
:END:
<<ssec:test_id31_scans_conclusion >>
For each conducted experiments, the $D_y$, $D_z$ and $R_y$ errors are computed and summarized in Table ref:tab:id31_experiments_results_summary.
#+begin_src matlab
%% Summary of results
data_results = [...
2025-01-31 16:33:25 +01:00
specs_dy_rms, specs_dz_rms, 1e3*specs_ry_rms ; ... % Specifications
2024-11-15 18:15:13 +01:00
1e9*data_tomo_1rpm_m0.Dy_rms_cl, 1e9*data_tomo_1rpm_m0.Dz_rms_cl, 1e9*data_tomo_1rpm_m0.Ry_rms_cl ; ... % Tomo 1rpm
1e9*data_tomo_6rpm_m0.Dy_rms_cl, 1e9*data_tomo_6rpm_m0.Dz_rms_cl, 1e9*data_tomo_6rpm_m0.Ry_rms_cl ; ... % Tomo 6rpm
1e9*data_tomo_30rpm_m0.Dy_rms_cl, 1e9*data_tomo_30rpm_m0.Dz_rms_cl, 1e9*data_tomo_30rpm_m0.Ry_rms_cl ; ... % Tomo 30rpm
1e9*rms(detrend(data_dz_10ums.e_dy, 0)), 1e9*rms(detrend(data_dz_10ums.e_dz, 0)), 1e9*rms(detrend(data_dz_10ums.e_ry, 0)) ; ... % Dz 10um/s
1e9*rms(detrend(data_dz_100ums.e_dy,0)), 1e9*rms(detrend(data_dz_100ums.e_dz,0)), 1e9*rms(detrend(data_dz_100ums.e_ry,0)) ; ... % Dz 100um/s
1e9*rms(detrend(data_ry.e_dy,0)), 1e9*rms(detrend(data_ry.e_dz,0)), 1e9*rms(detrend(data_ry.e_ry,0)) ; ... % Ry 100urad/s
2025-01-31 16:33:25 +01:00
1e9*rms(detrend(data_ty_cl_slow.e_dy, 0)), 1e9*rms(detrend(data_ty_cl_slow.e_dz, 0)), 1e9*rms(detrend(data_ty_cl_slow.e_ry, 0)) ; ... % Dy 10 um/s
2024-11-15 18:15:13 +01:00
1e9*rms(detrend(data_dt_100ums.Dy_int-data_dt_100ums.m_hexa_dy, 0)), 1e9*rms(detrend(data_dt_100ums.Dz_int, 0)), 1e9*rms(detrend(data_dt_100ums.Ry_int, 0)); ... % Diffraction tomo 0.1mm/s
1e9*rms(detrend(data_dt_1000ums.Dy_int-data_dt_1000ums.m_hexa_dy,0)), 1e9*rms(detrend(data_dt_1000ums.Dz_int,0)), 1e9*rms(detrend(data_dt_1000ums.Ry_int,0)) ... % Diffraction tomo 1mm/s
];
#+end_src
2024-11-13 17:11:40 +01:00
2024-11-15 18:15:13 +01:00
#+begin_src matlab :exports results :results value table replace :tangle no :post addhdr(*this* )
2025-01-31 16:33:25 +01:00
data2orgtable(data_results, {'Specifications', 'Tomography ($R_z$ 1rpm)', 'Tomography ($R_z$ 6rpm)', 'Tomography ($R_z$ 30rpm)', 'Dirty Layer ($D_z$ $10\,\mu m/s$)', 'Dirty Layer ($D_z$ $100\,\mu m/s$)', 'Reflectivity ($R_y$ $100\,\mu\text{rad}/s$)', 'Lateral Scan ($D_y$ $10\,\mu m/s$)', 'Diffraction Tomography ($R_z$ 1rpm, $D_y$ 0.1mm/s)', 'Diffraction Tomography ($R_z$ 1rpm, $D_y$ 1mm/s)'}, {'$D_y$ [nmRMS]', '$D_z$ [nmRMS]', '$R_y$ [nradRMS]'}, ' %.0f ');
2024-11-15 18:15:13 +01:00
#+end_src
2024-11-13 17:11:40 +01:00
2024-11-15 18:15:13 +01:00
#+name : tab:id31_experiments_results_summary
#+caption : Table caption
#+attr_latex : :environment tabularx :width \linewidth :align Xccc
#+attr_latex : :center t :booktabs t
#+RESULTS :
| | $D_y$ [nmRMS] | $D_z$ [nmRMS] | $R_y$ [nradRMS] |
|----------------------------------------------------+---------------+---------------+-----------------|
2025-01-31 16:33:25 +01:00
| Specifications | | | |
|----------------------------------------------------+---------------+---------------+-----------------|
2024-11-15 18:15:13 +01:00
| Tomography ($R_z$ 1rpm) | 15 | 5 | 55 |
| Tomography ($R_z$ 6rpm) | 19 | 5 | 73 |
| Tomography ($R_z$ 30rpm) | 38 | 10 | 129 |
2025-01-31 16:33:25 +01:00
|----------------------------------------------------+---------------+---------------+-----------------|
2024-11-15 18:15:13 +01:00
| Dirty Layer ($D_z$ $10\,\mu m/s$) | 25 | 5 | 114 |
| Dirty Layer ($D_z$ $100\,\mu m/s$) | 34 | 15 | 130 |
2025-01-31 16:33:25 +01:00
|----------------------------------------------------+---------------+---------------+-----------------|
2024-11-15 18:15:13 +01:00
| Reflectivity ($R_y$ $100\,\mu\text{rad}/s$) | 28 | 6 | 118 |
2025-01-31 16:33:25 +01:00
|----------------------------------------------------+---------------+---------------+-----------------|
2024-11-15 18:15:13 +01:00
| Lateral Scan ($D_y$ $10\,\mu m/s$) | 21 | 10 | 37 |
2025-01-31 16:33:25 +01:00
|----------------------------------------------------+---------------+---------------+-----------------|
2024-11-15 18:15:13 +01:00
| Diffraction Tomography ($R_z$ 1rpm, $D_y$ 0.1mm/s) | 75 | 9 | 118 |
| Diffraction Tomography ($R_z$ 1rpm, $D_y$ 1mm/s) | 428 | 11 | 169 |
2024-11-13 17:11:40 +01:00
2025-01-31 16:33:25 +01:00
* Conclusion
:PROPERTIES:
:UNNUMBERED: t
:END:
<<ssec:test_id31_conclusion >>
2024-11-13 17:11:40 +01:00
* Bibliography :ignore:
#+latex : \printbibliography[heading=bibintoc,title={Bibliography}]
* Helping Functions :noexport:
** Initialize Path
#+NAME : m-init-path
#+BEGIN_SRC matlab
addpath('./matlab/ '); % Path for scripts
%% Path for functions, data and scripts
addpath('./matlab/mat/ '); % Path for Computed FRF
addpath('./matlab/src/ '); % Path for functions
addpath('./matlab/STEPS/ '); % Path for STEPS
addpath('./matlab/subsystems/ '); % Path for Subsystems Simulink files
%% Data directory
data_dir = './matlab/mat/ '
#+END_SRC
#+NAME : m-init-path-tangle
#+BEGIN_SRC matlab
%% 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/ '
#+END_SRC
** Initialize Simscape Model
#+NAME : m-init-simscape
#+begin_src matlab
% Simulink Model name
mdl = 'nass_model_id31';
#+end_src
** Initialize other elements
#+NAME : m-init-other
#+BEGIN_SRC matlab
%% Colors for the figures
colors = colororder;
%% Frequency Vector
freqs = logspace(log10(1), log10(2e3), 1000);
%% Sampling Time
Ts = 1e-4;
2025-01-31 16:33:25 +01:00
%% 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]
2024-11-13 17:11:40 +01:00
#+END_SRC
* Matlab Functions :noexport:
** Utility Functions
*** =h5scan= - Easily load h5 files
:PROPERTIES:
:header-args:matlab+: :tangle matlab/src/h5scan.m
:header-args:matlab+: :comments none :mkdirp yes :eval no
:END:
#+begin_src matlab
function [cntrs,tp] = h5scan(pth,smp,ds,sn,varargin)
i = cellfun(@(x) isa(x,'detector'),varargin);
if any(i), det = varargin{i}; varargin = varargin(~i); else, det = []; end;
if ~isstr(ds), ds = sprintf('%.4d',ds); end;
f = sprintf('%s/%s/ %s_%s/%s_ %s.h5',pth,smp,smp,ds,smp,ds);
h = h5info(f,sprintf('/%d.1/measurement',sn));
fid = H5F.open(f);
for i = 1:length(h.Links),
nm = h.Links(i).Name;
try,
id = H5D.open(fid,h.Links(i).Value{1});
cntrs.(nm) = H5D.read(id);
H5D.close(id);
if ~isempty(det) & strcmp(nm,det.name), cntrs.(nm) = integrate(det,double(cntrs.(nm))); end;
catch,
warning('solving problem with %s\n',nm);
cntrs.(nm) = vrtlds(sprintf('%s/%s/ %s_%s/scan%.4d/ ',pth,smp,smp,ds,sn),nm,det);
end;
[~,tp.(nm)] = fileparts(h.Links(i).Value{1});
end;
try,
h = h5info(f,sprintf('/%d.2/measurement',sn));
catch,
h = [];
end;
if ~isempty(h),
for i = 1:length(h.Links),
nm = h.Links(i).Name;
try,
id = H5D.open(fid,h.Links(i).Value{1});
cntrs.part2.(nm) = H5D.read(id);
H5D.close(id);
catch,
warning('solving problem with %s\n',nm);
cntrs.part2.(nm) = vrtlds(sprintf('%s/%s/ %s_%s/scan%.4d/ ',pth,smp,smp,ds,sn),nm,det);
end;
[~,tp.part2.(nm)] = fileparts(h.Links(i).Value{1});
end;
end;
if length(varargin),
fn = sprintf('/%d.1/instrument/positioners/ ',sn);
h = h5info(f,fn);
[~,k,m] = intersect({h.Datasets.Name},varargin,'stable');
h.Datasets = h.Datasets(k);
for i = 1:length(h.Datasets),
id = H5D.open(fid,[fn h.Datasets(i).Name]);
cntrs.(h.Datasets(i).Name) = H5D.read(id);
H5D.close(id);
end;
end;
H5F.close(fid);
%%%%%%%%%%%%%%%%%%%%%%%%%%
function A = vrtlds(f,nm,det)
%try,
n = 0; A = [];
fn = sprintf('%s/%s_%.4d.h5',f,nm,n);
while exist(fn) == 2,
fid = H5F.open(fn); n = n+1;
id = H5D.open(fid,sprintf('/entry_0000/ESRF-ID31/ %s/data',nm));
if 2 < nargin & strcmp(nm,'p3') & ~isempty(det),
fprintf('integrating %s\n',fn);
if isempty(A),
A = integrate(det,double(H5D.read(id)),1);
else,
tmp = integrate(det,double(H5D.read(id)),1); A.y = cat(2,A.y,tmp.y); A.y0 = cat(2,A.y0,tmp.y0);
end;
else,
fprintf('loading %s\n',fn);
A = cat(3,A,H5D.read(id));
end;
H5D.close(id); H5F.close(fid);
fn = sprintf('%s/%s_%.4d.h5',f,nm,n);
end;
%catch,
% A = [];
%end;
% fid = H5F.open...
% id = H5D.open...
% sid = H5D.get_space(id);
% [ndims,h5_dims]=H5S.get_simple_extent_dims(sid)
% Read a 2x3 hyperslab of data from a dataset, starting in the 4th row and 5th column of the example dataset.
% Create a property list identifier, then open the HDF5 file and the dataset /g1/g1.1/dset1.1.1.
% fid = H5F.open('example.h5');
% id = H5D.open(fid,'/g1/g1.1/dset1.1.1');
% dims = ([500 1679 1475];
% msid = H5S.create_simple(3,dims,[]);
% sid = H5D.get_space(id);
% offset = [n*500 0 0];
% block = dims; % d1: 500 or min(d1tot-n*500,500)
% H5S.select_hyperslab(sid,'H5S_SELECT_SET',offset,[],[],block);
% data = H5D.read(id,'H5ML_DEFAULT',msid,sid,'H5P_DEFAULT');
% H5D.close(id);
% H5F.close(fid);
#+end_src
*** =sphereFit= - Fit sphere from x,y,z points
:PROPERTIES:
:header-args:matlab+: :tangle matlab/src/sphereFit.m
:header-args:matlab+: :comments none :mkdirp yes :eval no
:END:
#+begin_src matlab
function [Center,Radius] = sphereFit(X)
% this fits a sphere to a collection of data using a closed form for the
% solution (opposed to using an array the size of the data set).
% Minimizes Sum((x-xc)^2+(y-yc)^2+ (z-zc)^2-r^2)^2
% x,y,z are the data, xc,yc,zc are the sphere's center, and r is the radius
% Assumes that points are not in a singular configuration, real numbers, ...
% if you have coplanar data, use a circle fit with svd for determining the
% plane, recommended Circle Fit (Pratt method), by Nikolai Chernov
% http://www.mathworks.com/matlabcentral/fileexchange/22643
% Input:
% X: n x 3 matrix of cartesian data
% Outputs:
% Center: Center of sphere
% Radius: Radius of sphere
% Author:
% Alan Jennings, University of Dayton
A=[mean(X(:,1).*(X(:,1)-mean(X(:,1)))), ...
2024-11-13 10:27:34 +01:00
2*mean(X(:,1).* (X(:,2)-mean(X(:,2)))), ...
2*mean(X(:,1).* (X(:,3)-mean(X(:,3)))); ...
0, ...
mean(X(:,2).*(X(:,2)-mean(X(:,2)))), ...
2*mean(X(:,2).* (X(:,3)-mean(X(:,3)))); ...
0, ...
0, ...
mean(X(:,3).*(X(:,3)-mean(X(:,3))))];
A=A+A.';
B=[mean((X(:,1).^2+X(:,2).^2+X(:,3).^2).*(X(:,1)-mean(X(:,1))));...
mean((X(:,1).^2+X(:,2).^2+X(:,3).^2).*(X(:,2)-mean(X(:,2))));...
mean((X(:,1).^2+X(:,2).^2+X(:,3).^2).*(X(:,3)-mean(X(:,3))))];
Center=(A\B).';
Radius=sqrt(mean(sum([X(:,1)-Center(1),X(:,2)-Center(2),X(:,3)-Center(3)].^2,2)));
#+end_src
2024-11-15 18:15:13 +01:00
*** =unwrapphase= - Unwrap phase for FRF data
:PROPERTIES:
:header-args:matlab+: :tangle matlab/src/unwrapphase.m
:header-args:matlab+: :comments none :mkdirp yes :eval no
:END:
#+begin_src matlab
function [unwraped_phase] = unwrapphase(frf, f, args)
#+end_src
#+begin_src matlab
arguments
frf
f
args.f0 (1,1) double {mustBeNumeric} = 1
end
#+end_src
#+begin_src matlab
unwraped_phase = unwrap(frf);
[~,i] = min(abs(f - args.f0));
unwraped_phase = unwraped_phase - 2*pi*round(unwraped_phase(i)./(2*pi));
#+end_src
*** =circlefit=
:PROPERTIES:
:header-args:matlab+: :tangle matlab/src/circlefit.m
:header-args:matlab+: :comments none :mkdirp yes :eval no
:END:
#+begin_src matlab
function [xc,yc,R,a] = circlefit(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
2024-11-13 17:11:40 +01:00
** Initialize Simscape Model
*** =initializeSimscapeConfiguration=: Simscape Configuration
#+begin_src matlab :tangle matlab/src/initializeSimscapeConfiguration.m :comments none :mkdirp yes :eval no
function [] = initializeSimscapeConfiguration(args)
arguments
args.gravity logical {mustBeNumericOrLogical} = true
end
conf_simscape = struct();
if args.gravity
conf_simscape.type = 1;
else
conf_simscape.type = 2;
end
if exist('./mat', 'dir')
if exist('./mat/nass_model_conf_simscape.mat', 'file')
save('mat/nass_model_conf_simscape.mat', 'conf_simscape', '-append');
else
save('mat/nass_model_conf_simscape.mat', 'conf_simscape');
end
elseif exist('./matlab', 'dir')
if exist('./matlab/mat/nass_model_conf_simscape.mat', 'file')
save('matlab/mat/nass_model_conf_simscape.mat', 'conf_simscape', '-append');
else
save('matlab/mat/nass_model_conf_simscape.mat', 'conf_simscape');
end
end
end
#+end_src
*** =initializeLoggingConfiguration=: Logging Configuration
#+begin_src matlab :tangle matlab/src/initializeLoggingConfiguration.m :comments none :mkdirp yes :eval no
function [] = initializeLoggingConfiguration(args)
arguments
args.log char {mustBeMember(args.log,{'none', 'all', 'forces'})} = 'none'
args.Ts (1,1) double {mustBeNumeric, mustBePositive} = 1e-3
end
conf_log = struct();
switch args.log
case 'none'
conf_log.type = 0;
case 'all'
conf_log.type = 1;
case 'forces'
conf_log.type = 2;
end
conf_log.Ts = args.Ts;
if exist('./mat', 'dir')
if exist('./mat/nass_model_conf_log.mat', 'file')
save('mat/nass_model_conf_log.mat', 'conf_log', '-append');
else
save('mat/nass_model_conf_log.mat', 'conf_log');
end
elseif exist('./matlab', 'dir')
if exist('./matlab/mat/nass_model_conf_log.mat', 'file')
save('matlab/mat/nass_model_conf_log.mat', 'conf_log', '-append');
else
save('matlab/mat/nass_model_conf_log.mat', 'conf_log');
end
end
end
#+end_src
*** =initializeReferences=: Generate Reference Signals
#+begin_src matlab :tangle matlab/src/initializeReferences.m :comments none :mkdirp yes :eval no
function [ref] = initializeReferences(args)
arguments
% Sampling Frequency [s]
args.Ts (1,1) double {mustBeNumeric, mustBePositive} = 1e-3
% Maximum simulation time [s]
args.Tmax (1,1) double {mustBeNumeric, mustBePositive} = 100
% Either "constant" / "triangular" / "sinusoidal"
args.Dy_type char {mustBeMember(args.Dy_type,{'constant', 'triangular', 'sinusoidal'})} = 'constant'
% Amplitude of the displacement [m]
args.Dy_amplitude (1,1) double {mustBeNumeric} = 0
% Period of the displacement [s]
args.Dy_period (1,1) double {mustBeNumeric, mustBePositive} = 1
% Either "constant" / "triangular" / "sinusoidal"
args.Ry_type char {mustBeMember(args.Ry_type,{'constant', 'triangular', 'sinusoidal'})} = 'constant'
% Amplitude [rad]
args.Ry_amplitude (1,1) double {mustBeNumeric} = 0
% Period of the displacement [s]
args.Ry_period (1,1) double {mustBeNumeric, mustBePositive} = 1
% Either "constant" / "rotating"
args.Rz_type char {mustBeMember(args.Rz_type,{'constant', 'rotating', 'rotating-not-filtered'})} = 'constant'
% Initial angle [rad]
args.Rz_amplitude (1,1) double {mustBeNumeric} = 0
% Period of the rotating [s]
args.Rz_period (1,1) double {mustBeNumeric, mustBePositive} = 1
% For now, only constant is implemented
args.Dh_type char {mustBeMember(args.Dh_type,{'constant'})} = 'constant'
% Initial position [m,m,m,rad,rad,rad] of the top platform (Pitch-Roll-Yaw Euler angles)
args.Dh_pos (6,1) double {mustBeNumeric} = zeros(6, 1), ...
% For now, only constant is implemented
args.Rm_type char {mustBeMember(args.Rm_type,{'constant'})} = 'constant'
% Initial position of the two masses
args.Rm_pos (2,1) double {mustBeNumeric} = [0; pi]
% For now, only constant is implemented
args.Dn_type char {mustBeMember(args.Dn_type,{'constant'})} = 'constant'
% Initial position [m,m,m,rad,rad,rad] of the top platform
args.Dn_pos (6,1) double {mustBeNumeric} = zeros(6,1)
end
%% Set Sampling Time
Ts = args.Ts;
Tmax = args.Tmax;
%% Low Pass Filter to filter out the references
s = zpk('s');
w0 = 2*pi*10;
xi = 1;
H_lpf = 1/(1 + 2*xi/w0*s + s^2/w0^2);
%% Translation stage - Dy
t = 0:Ts:Tmax; % Time Vector [s]
Dy = zeros(length(t), 1);
Dyd = zeros(length(t), 1);
Dydd = zeros(length(t), 1);
switch args.Dy_type
case 'constant'
Dy(:) = args.Dy_amplitude;
Dyd(:) = 0;
Dydd(:) = 0;
case 'triangular'
% This is done to unsure that we start with no displacement
Dy_raw = args.Dy_amplitude*sawtooth(2*pi*t/args.Dy_period,1/2);
i0 = find(t>=args.Dy_period/4,1);
Dy(1:end-i0+1) = Dy_raw(i0:end);
Dy(end-i0+2:end) = Dy_raw(end); % we fix the last value
% The signal is filtered out
Dy = lsim(H_lpf, Dy, t);
Dyd = lsim(H_lpf*s, Dy, t);
Dydd = lsim(H_lpf*s^2, Dy, t);
case 'sinusoidal'
Dy(:) = args.Dy_amplitude*sin(2*pi/args.Dy_period*t);
Dyd = args.Dy_amplitude*2*pi/args.Dy_period*cos(2*pi/args.Dy_period*t);
Dydd = -args.Dy_amplitude*(2*pi/args.Dy_period)^2*sin(2*pi/args.Dy_period*t);
otherwise
warning('Dy_type is not set correctly');
end
Dy = struct('time', t, 'signals', struct('values', Dy), 'deriv', Dyd, 'dderiv', Dydd);
%% Tilt Stage - Ry
t = 0:Ts:Tmax; % Time Vector [s]
Ry = zeros(length(t), 1);
Ryd = zeros(length(t), 1);
Rydd = zeros(length(t), 1);
switch args.Ry_type
case 'constant'
Ry(:) = args.Ry_amplitude;
Ryd(:) = 0;
Rydd(:) = 0;
case 'triangular'
Ry_raw = args.Ry_amplitude*sawtooth(2*pi*t/args.Ry_period,1/2);
i0 = find(t>=args.Ry_period/4,1);
Ry(1:end-i0+1) = Ry_raw(i0:end);
Ry(end-i0+2:end) = Ry_raw(end); % we fix the last value
% The signal is filtered out
Ry = lsim(H_lpf, Ry, t);
Ryd = lsim(H_lpf*s, Ry, t);
Rydd = lsim(H_lpf*s^2, Ry, t);
case 'sinusoidal'
Ry(:) = args.Ry_amplitude*sin(2*pi/args.Ry_period*t);
Ryd = args.Ry_amplitude*2*pi/args.Ry_period*cos(2*pi/args.Ry_period*t);
Rydd = -args.Ry_amplitude*(2*pi/args.Ry_period)^2*sin(2*pi/args.Ry_period*t);
otherwise
warning('Ry_type is not set correctly');
end
Ry = struct('time', t, 'signals', struct('values', Ry), 'deriv', Ryd, 'dderiv', Rydd);
%% Spindle - Rz
t = 0:Ts:Tmax; % Time Vector [s]
Rz = zeros(length(t), 1);
Rzd = zeros(length(t), 1);
Rzdd = zeros(length(t), 1);
switch args.Rz_type
case 'constant'
Rz(:) = args.Rz_amplitude;
Rzd(:) = 0;
Rzdd(:) = 0;
case 'rotating-not-filtered'
Rz(:) = 2*pi/args.Rz_period*t;
% The signal is filtered out
Rz(:) = 2*pi/args.Rz_period*t;
Rzd(:) = 2*pi/args.Rz_period;
Rzdd(:) = 0;
% We add the angle offset
Rz = Rz + args.Rz_amplitude;
case 'rotating'
Rz(:) = 2*pi/args.Rz_period*t;
% The signal is filtered out
Rz = lsim(H_lpf, Rz, t);
Rzd = lsim(H_lpf*s, Rz, t);
Rzdd = lsim(H_lpf*s^2, Rz, t);
% We add the angle offset
Rz = Rz + args.Rz_amplitude;
otherwise
warning('Rz_type is not set correctly');
end
Rz = struct('time', t, 'signals', struct('values', Rz), 'deriv', Rzd, 'dderiv', Rzdd);
%% Micro-Hexapod
t = [0, Ts];
Dh = zeros(length(t), 6);
Dhl = zeros(length(t), 6);
switch args.Dh_type
case 'constant'
Dh = [args.Dh_pos, args.Dh_pos];
load('nass_model_stages.mat', 'micro_hexapod');
AP = [args.Dh_pos(1) ; args.Dh_pos(2) ; args.Dh_pos(3)];
tx = args.Dh_pos(4);
ty = args.Dh_pos(5);
tz = args.Dh_pos(6);
ARB = [cos(tz) -sin(tz) 0;
sin(tz) cos(tz) 0;
0 0 1]*...
[ cos(ty) 0 sin(ty);
0 1 0;
-sin(ty) 0 cos(ty)]*...
[1 0 0;
0 cos(tx) -sin(tx);
0 sin(tx) cos(tx)];
[~, Dhl] = inverseKinematics(micro_hexapod, 'AP', AP, 'ARB', ARB);
Dhl = [Dhl, Dhl];
otherwise
warning('Dh_type is not set correctly');
end
Dh = struct('time', t, 'signals', struct('values', Dh));
Dhl = struct('time', t, 'signals', struct('values', Dhl));
if exist('./mat', 'dir')
if exist('./mat/nass_model_references.mat', 'file')
save('mat/nass_model_references.mat', 'Dy', 'Ry', 'Rz', 'Dh', 'Dhl', 'args', 'Ts', '-append');
else
save('mat/nass_model_references.mat', 'Dy', 'Ry', 'Rz', 'Dh', 'Dhl', 'args', 'Ts');
end
elseif exist('./matlab', 'dir')
if exist('./matlab/mat/nass_model_references.mat', 'file')
save('matlab/mat/nass_model_references.mat', 'Dy', 'Ry', 'Rz', 'Dh', 'Dhl', 'args', 'Ts', '-append');
else
save('matlab/mat/nass_model_references.mat', 'Dy', 'Ry', 'Rz', 'Dh', 'Dhl', 'args', 'Ts');
end
end
end
#+end_src
*** =initializeDisturbances=: Initialize Disturbances
#+begin_src matlab :tangle matlab/src/initializeDisturbances.m :comments none :mkdirp yes :eval no
function [] = initializeDisturbances(args)
% initializeDisturbances - Initialize the disturbances
%
% Syntax: [] = initializeDisturbances(args)
%
% Inputs:
% - args -
arguments
% Global parameter to enable or disable the disturbances
args.enable logical {mustBeNumericOrLogical} = true
% Ground Motion - X direction
args.Dw_x logical {mustBeNumericOrLogical} = true
% Ground Motion - Y direction
args.Dw_y logical {mustBeNumericOrLogical} = true
% Ground Motion - Z direction
args.Dw_z logical {mustBeNumericOrLogical} = true
% Translation Stage - X direction
args.Fdy_x logical {mustBeNumericOrLogical} = true
% Translation Stage - Z direction
args.Fdy_z logical {mustBeNumericOrLogical} = true
% Spindle - X direction
args.Frz_x logical {mustBeNumericOrLogical} = true
% Spindle - Y direction
args.Frz_y logical {mustBeNumericOrLogical} = true
% Spindle - Z direction
args.Frz_z logical {mustBeNumericOrLogical} = true
end
% Initialization of random numbers
rng("shuffle");
%% Ground Motion
if args.enable
% Load the PSD of disturbance
load('ustation_disturbance_psd.mat', 'gm_dist')
% Frequency Data
Dw.f = gm_dist.f;
Dw.psd_x = gm_dist.pxx_x;
Dw.psd_y = gm_dist.pxx_y;
Dw.psd_z = gm_dist.pxx_z;
% Time data
Fs = 2*Dw.f(end); % Sampling Frequency of data is twice the maximum frequency of the PSD vector [Hz]
N = 2*length(Dw.f); % Number of Samples match the one of the wanted PSD
T0 = N/Fs; % Signal Duration [s]
Dw.t = linspace(0, T0, N+1)'; % Time Vector [s]
% ASD representation of the ground motion
C = zeros(N/2,1);
for i = 1:N/2
C(i) = sqrt(Dw.psd_x(i)/T0);
end
if args.Dw_x
theta = 2*pi*rand(N/2,1); % Generate random phase [rad]
Cx = [0 ; C.*complex(cos(theta),sin(theta))];
Cx = [Cx; flipud(conj(Cx(2:end)))];;
Dw.x = N/sqrt(2)*ifft(Cx); % Ground Motion - x direction [m]
else
Dw.x = zeros(length(Dw.t), 1);
end
if args.Dw_y
theta = 2*pi*rand(N/2,1); % Generate random phase [rad]
Cx = [0 ; C.*complex(cos(theta),sin(theta))];
Cx = [Cx; flipud(conj(Cx(2:end)))];;
Dw.y = N/sqrt(2)*ifft(Cx); % Ground Motion - y direction [m]
else
Dw.y = zeros(length(Dw.t), 1);
end
if args.Dw_y
theta = 2*pi*rand(N/2,1); % Generate random phase [rad]
Cx = [0 ; C.*complex(cos(theta),sin(theta))];
Cx = [Cx; flipud(conj(Cx(2:end)))];;
Dw.z = N/sqrt(2)*ifft(Cx); % Ground Motion - z direction [m]
else
Dw.z = zeros(length(Dw.t), 1);
end
else
Dw.t = [0,1]; % Time Vector [s]
Dw.x = [0,0]; % Ground Motion - X [m]
Dw.y = [0,0]; % Ground Motion - Y [m]
Dw.z = [0,0]; % Ground Motion - Z [m]
end
%% Translation stage
if args.enable
% Load the PSD of disturbance
load('ustation_disturbance_psd.mat', 'dy_dist')
% Frequency Data
Dy.f = dy_dist.f;
Dy.psd_x = dy_dist.pxx_fx;
Dy.psd_z = dy_dist.pxx_fz;
% Time data
Fs = 2*Dy.f(end); % Sampling Frequency of data is twice the maximum frequency of the PSD vector [Hz]
N = 2*length(Dy.f); % Number of Samples match the one of the wanted PSD
T0 = N/Fs; % Signal Duration [s]
Dy.t = linspace(0, T0, N+1)'; % Time Vector [s]
% ASD representation of the disturbance voice
C = zeros(N/2,1);
for i = 1:N/2
C(i) = sqrt(Dy.psd_x(i)/T0);
end
if args.Fdy_x
theta = 2*pi*rand(N/2,1); % Generate random phase [rad]
Cx = [0 ; C.*complex(cos(theta),sin(theta))];
Cx = [Cx; flipud(conj(Cx(2:end)))];;
Dy.x = N/sqrt(2)*ifft(Cx); % Translation stage disturbances - X direction [N]
else
Dy.x = zeros(length(Dy.t), 1);
end
if args.Fdy_z
theta = 2*pi*rand(N/2,1); % Generate random phase [rad]
Cx = [0 ; C.*complex(cos(theta),sin(theta))];
Cx = [Cx; flipud(conj(Cx(2:end)))];;
Dy.z = N/sqrt(2)*ifft(Cx); % Translation stage disturbances - Z direction [N]
else
Dy.z = zeros(length(Dy.t), 1);
end
else
Dy.t = [0,1]; % Time Vector [s]
Dy.x = [0,0]; % Translation Stage disturbances - X [N]
Dy.z = [0,0]; % Translation Stage disturbances - Z [N]
end
%% Spindle
if args.enable
% Load the PSD of disturbance
load('ustation_disturbance_psd.mat', 'rz_dist')
% Frequency Data
Rz.f = rz_dist.f;
Rz.psd_x = rz_dist.pxx_fx;
Rz.psd_y = rz_dist.pxx_fy;
Rz.psd_z = rz_dist.pxx_fz;
% Time data
Fs = 2*Rz.f(end); % Sampling Frequency of data is twice the maximum frequency of the PSD vector [Hz]
N = 2*length(Rz.f); % Number of Samples match the one of the wanted PSD
T0 = N/Fs; % Signal Duration [s]
Rz.t = linspace(0, T0, N+1)'; % Time Vector [s]
% ASD representation of the disturbance voice
C = zeros(N/2,1);
for i = 1:N/2
C(i) = sqrt(Rz.psd_x(i)/T0);
end
if args.Frz_x
theta = 2*pi*rand(N/2,1); % Generate random phase [rad]
Cx = [0 ; C.*complex(cos(theta),sin(theta))];
Cx = [Cx; flipud(conj(Cx(2:end)))];;
Rz.x = N/sqrt(2)*ifft(Cx); % spindle disturbances - X direction [N]
else
Rz.x = zeros(length(Rz.t), 1);
end
if args.Frz_y
theta = 2*pi*rand(N/2,1); % Generate random phase [rad]
Cx = [0 ; C.*complex(cos(theta),sin(theta))];
Cx = [Cx; flipud(conj(Cx(2:end)))];;
Rz.y = N/sqrt(2)*ifft(Cx); % spindle disturbances - Y direction [N]
else
Rz.y = zeros(length(Rz.t), 1);
end
if args.Frz_z
theta = 2*pi*rand(N/2,1); % Generate random phase [rad]
Cx = [0 ; C.*complex(cos(theta),sin(theta))];
Cx = [Cx; flipud(conj(Cx(2:end)))];;
Rz.z = N/sqrt(2)*ifft(Cx); % spindle disturbances - Z direction [N]
else
Rz.z = zeros(length(Rz.t), 1);
end
else
Rz.t = [0,1]; % Time Vector [s]
Rz.x = [0,0]; % Spindle disturbances - X [N]
Rz.y = [0,0]; % Spindle disturbances - X [N]
Rz.z = [0,0]; % Spindle disturbances - Z [N]
end
u = zeros(100, 6);
Fd = u;
Dw.x = Dw.x - Dw.x(1);
Dw.y = Dw.y - Dw.y(1);
Dw.z = Dw.z - Dw.z(1);
Dy.x = Dy.x - Dy.x(1);
Dy.z = Dy.z - Dy.z(1);
Rz.x = Rz.x - Rz.x(1);
Rz.y = Rz.y - Rz.y(1);
Rz.z = Rz.z - Rz.z(1);
if exist('./mat', 'dir')
save('mat/nass_model_disturbances.mat', 'Dw', 'Dy', 'Rz', 'Fd', 'args');
elseif exist('./matlab', 'dir')
save('matlab/mat/nass_model_disturbances.mat', 'Dw', 'Dy', 'Rz', 'Fd', 'args');
end
end
#+end_src
*** =initializeController=: Initialize Controller
#+begin_src matlab :tangle matlab/src/initializeController.m :comments none :mkdirp yes :eval no
function [] = initializeController(args)
arguments
args.type char {mustBeMember(args.type,{'open-loop', 'iff', 'dvf', 'hac-dvf', 'ref-track-L', 'ref-track-iff-L', 'cascade-hac-lac', 'hac-iff', 'stabilizing'})} = 'open-loop'
end
controller = struct();
switch args.type
case 'open-loop'
controller.type = 1;
controller.name = 'Open-Loop';
case 'dvf'
controller.type = 2;
controller.name = 'Decentralized Direct Velocity Feedback';
case 'iff'
controller.type = 3;
controller.name = 'Decentralized Integral Force Feedback';
case 'hac-dvf'
controller.type = 4;
controller.name = 'HAC-DVF';
case 'ref-track-L'
controller.type = 5;
controller.name = 'Reference Tracking in the frame of the legs';
case 'ref-track-iff-L'
controller.type = 6;
controller.name = 'Reference Tracking in the frame of the legs + IFF';
case 'cascade-hac-lac'
controller.type = 7;
controller.name = 'Cascade Control + HAC-LAC';
case 'hac-iff'
controller.type = 8;
controller.name = 'HAC-IFF';
case 'stabilizing'
controller.type = 9;
controller.name = 'Stabilizing Controller';
end
if exist('./mat', 'dir')
save('mat/nass_model_controller.mat', 'controller');
elseif exist('./matlab', 'dir')
save('matlab/mat/nass_model_controller.mat', 'controller');
end
end
#+end_src
*** =computeReferencePose=
#+begin_src matlab :tangle matlab/src/computeReferencePose.m :comments none :mkdirp yes :eval no
function [WTr] = computeReferencePose(Dy, Ry, Rz, Dh, Dn)
% computeReferencePose - Compute the homogeneous transformation matrix corresponding to the wanted pose of the sample
%
% Syntax: [WTr] = computeReferencePose(Dy, Ry, Rz, Dh, Dn)
%
% Inputs:
% - Dy - Reference of the Translation Stage [m]
% - Ry - Reference of the Tilt Stage [rad]
% - Rz - Reference of the Spindle [rad]
% - Dh - Reference of the Micro Hexapod (Pitch, Roll, Yaw angles) [m, m, m, rad, rad, rad]
% - Dn - Reference of the Nano Hexapod [m, m, m, rad, rad, rad]
%
% Outputs:
% - WTr -
%% Translation Stage
Rty = [1 0 0 0;
0 1 0 Dy;
0 0 1 0;
0 0 0 1];
%% Tilt Stage - Pure rotating aligned with Ob
Rry = [ cos(Ry) 0 sin(Ry) 0;
0 1 0 0;
-sin(Ry) 0 cos(Ry) 0;
0 0 0 1];
%% Spindle - Rotation along the Z axis
Rrz = [cos(Rz) -sin(Rz) 0 0 ;
sin(Rz) cos(Rz) 0 0 ;
0 0 1 0 ;
0 0 0 1 ];
%% Micro-Hexapod
Rhx = [1 0 0;
0 cos(Dh(4)) -sin(Dh(4));
0 sin(Dh(4)) cos(Dh(4))];
Rhy = [ cos(Dh(5)) 0 sin(Dh(5));
0 1 0;
-sin(Dh(5)) 0 cos(Dh(5))];
Rhz = [cos(Dh(6)) -sin(Dh(6)) 0;
sin(Dh(6)) cos(Dh(6)) 0;
0 0 1];
Rh = [1 0 0 Dh(1) ;
0 1 0 Dh(2) ;
0 0 1 Dh(3) ;
0 0 0 1 ];
Rh(1:3, 1:3) = Rhz*Rhy*Rhx;
%% Nano-Hexapod
Rnx = [1 0 0;
0 cos(Dn(4)) -sin(Dn(4));
0 sin(Dn(4)) cos(Dn(4))];
Rny = [ cos(Dn(5)) 0 sin(Dn(5));
0 1 0;
-sin(Dn(5)) 0 cos(Dn(5))];
Rnz = [cos(Dn(6)) -sin(Dn(6)) 0;
sin(Dn(6)) cos(Dn(6)) 0;
0 0 1];
Rn = [1 0 0 Dn(1) ;
0 1 0 Dn(2) ;
0 0 1 Dn(3) ;
0 0 0 1 ];
Rn(1:3, 1:3) = Rnz*Rny*Rnx;
%% Total Homogeneous transformation
WTr = Rty*Rry*Rrz*Rh*Rn;
end
#+end_src
*** =extractNodes=
#+begin_src matlab :tangle matlab/src/extractNodes.m :comments none :mkdirp yes :eval no
function [int_xyz, int_i, n_xyz, n_i, nodes] = extractNodes(filename)
% extractNodes -
%
% Syntax: [n_xyz, nodes] = extractNodes(filename)
%
% Inputs:
% - filename - relative or absolute path of the file that contains the Matrix
%
% Outputs:
% - n_xyz -
% - nodes - table containing the node numbers and corresponding dof of the interfaced DoFs
arguments
filename
end
fid = fopen(filename,'rt');
if fid == -1
error('Error opening the file');
end
n_xyz = []; % Contains nodes coordinates
n_i = []; % Contains nodes indices
n_num = []; % Contains node numbers
n_dof = {}; % Contains node directions
while 1
% Read a line
nextline = fgetl(fid);
% End of the file
if ~isstr(nextline), break, end
% Line just before the list of nodes coordinates
if contains(nextline, 'NODE') && ...
contains(nextline, 'X') && ...
contains(nextline, 'Y') && ...
contains(nextline, 'Z')
while 1
nextline = fgetl(fid);
if nextline < 0, break, end
c = sscanf(nextline, ' %f');
if isempty(c), break, end
n_xyz = [n_xyz; c(2:4)'];
n_i = [n_i; c(1)];
end
end
if nextline < 0, break, end
% Line just before the list of node DOF
if contains(nextline, 'NODE LABEL')
while 1
nextline = fgetl(fid);
if nextline < 0, break, end
c = sscanf(nextline, ' %d %s');
if isempty(c), break, end
n_num = [n_num; c(1)];
n_dof{length(n_dof)+1} = char(c(2:end)');
end
nodes = table(n_num, string(n_dof'), 'VariableNames', {'node_i', 'node_dof'});
end
if nextline < 0, break, end
end
fclose(fid);
int_i = unique(nodes.('node_i')); % indices of interface nodes
% Extract XYZ coordinates of only the interface nodes
if length(n_xyz) > 0 && length(n_i) > 0
int_xyz = n_xyz(logical(sum(n_i.*ones(1, length(int_i)) = = int_i', 2)), :);
else
int_xyz = n_xyz;
end
#+end_src
** Initialize Micro-Station Stages
*** =initializeGround=: Ground
#+begin_src matlab :tangle matlab/src/initializeGround.m :comments none :mkdirp yes :eval no
function [ground] = initializeGround(args)
arguments
args.type char {mustBeMember(args.type,{'none', 'rigid'})} = 'rigid'
args.rot_point (3,1) double {mustBeNumeric} = zeros(3,1) % Rotation point for the ground motion [m]
end
ground = struct();
switch args.type
case 'none'
ground.type = 0;
case 'rigid'
ground.type = 1;
end
ground.shape = [2, 2, 0.5]; % [m]
ground.density = 2800; % [kg/m3]
ground.rot_point = args.rot_point;
if exist('./mat', 'dir')
if exist('./mat/nass_model_stages.mat', 'file')
save('mat/nass_model_stages.mat', 'ground', '-append');
else
save('mat/nass_model_stages.mat', 'ground');
end
elseif exist('./matlab', 'dir')
if exist('./matlab/mat/nass_model_stages.mat', 'file')
save('matlab/mat/nass_model_stages.mat', 'ground', '-append');
else
save('matlab/mat/nass_model_stages.mat', 'ground');
end
end
end
#+end_src
*** =initializeGranite=: Granite
#+begin_src matlab :tangle matlab/src/initializeGranite.m :comments none :mkdirp yes :eval no
function [granite] = initializeGranite(args)
arguments
args.type char {mustBeMember(args.type,{'rigid', 'flexible', 'none'})} = 'flexible'
args.density (1,1) double {mustBeNumeric, mustBeNonnegative} = 2800 % Density [kg/m3]
args.K (6,1) double {mustBeNumeric, mustBeNonnegative} = [5e9; 5e9; 5e9; 2.5e7; 2.5e7; 1e7] % [N/m]
args.C (6,1) double {mustBeNumeric, mustBeNonnegative} = [4.0e5; 1.1e5; 9.0e5; 2e4; 2e4; 1e4] % [N/(m/s)]
args.x0 (1,1) double {mustBeNumeric} = 0 % Rest position of the Joint in the X direction [m]
args.y0 (1,1) double {mustBeNumeric} = 0 % Rest position of the Joint in the Y direction [m]
args.z0 (1,1) double {mustBeNumeric} = 0 % Rest position of the Joint in the Z direction [m]
args.sample_pos (1,1) double {mustBeNumeric} = 0.775 % Height of the measurment point [m]
end
granite = struct();
switch args.type
case 'none'
granite.type = 0;
case 'rigid'
granite.type = 1;
case 'flexible'
granite.type = 2;
end
granite.density = args.density; % [kg/m3]
granite.STEP = 'granite.STEP';
% Z-offset for the initial position of the sample with respect to the granite top surface.
granite.sample_pos = args.sample_pos; % [m]
granite.K = args.K; % [N/m]
granite.C = args.C; % [N/(m/s)]
if exist('./mat', 'dir')
if exist('./mat/nass_model_stages.mat', 'file')
save('mat/nass_model_stages.mat', 'granite', '-append');
else
save('mat/nass_model_stages.mat', 'granite');
end
elseif exist('./matlab', 'dir')
if exist('./matlab/mat/nass_model_stages.mat', 'file')
save('matlab/mat/nass_model_stages.mat', 'granite', '-append');
else
save('matlab/mat/nass_model_stages.mat', 'granite');
end
end
end
#+end_src
*** =initializeTy=: Translation Stage
#+begin_src matlab :tangle matlab/src/initializeTy.m :comments none :mkdirp yes :eval no
function [ty] = initializeTy(args)
arguments
args.type char {mustBeMember(args.type,{'none', 'rigid', 'flexible'})} = 'flexible'
end
ty = struct();
switch args.type
case 'none'
ty.type = 0;
case 'rigid'
ty.type = 1;
case 'flexible'
ty.type = 2;
end
% Ty Granite frame
ty.granite_frame.density = 7800; % [kg/m3] = > 43kg
ty.granite_frame.STEP = 'Ty_Granite_Frame.STEP';
% Guide Translation Ty
ty.guide.density = 7800; % [kg/m3] = > 76kg
ty.guide.STEP = 'Ty_Guide.STEP';
% Ty - Guide_Translation12
ty.guide12.density = 7800; % [kg/m3]
ty.guide12.STEP = 'Ty_Guide_12.STEP';
% Ty - Guide_Translation11
ty.guide11.density = 7800; % [kg/m3]
ty.guide11.STEP = 'Ty_Guide_11.STEP';
% Ty - Guide_Translation22
ty.guide22.density = 7800; % [kg/m3]
ty.guide22.STEP = 'Ty_Guide_22.STEP';
% Ty - Guide_Translation21
ty.guide21.density = 7800; % [kg/m3]
ty.guide21.STEP = 'Ty_Guide_21.STEP';
% Ty - Plateau translation
ty.frame.density = 7800; % [kg/m3]
ty.frame.STEP = 'Ty_Stage.STEP';
% Ty Stator Part
ty.stator.density = 5400; % [kg/m3]
ty.stator.STEP = 'Ty_Motor_Stator.STEP';
% Ty Rotor Part
ty.rotor.density = 5400; % [kg/m3]
ty.rotor.STEP = 'Ty_Motor_Rotor.STEP';
ty.K = [2e8; 1e8; 2e8; 6e7; 9e7; 6e7]; % [N/m, N*m/rad]
ty.C = [8e4; 5e4; 8e4; 2e4; 3e4; 1e4]; % [N/(m/s), N*m/ (rad/s)]
if exist('./mat', 'dir')
if exist('./mat/nass_model_stages.mat', 'file')
save('mat/nass_model_stages.mat', 'ty', '-append');
else
save('mat/nass_model_stages.mat', 'ty');
end
elseif exist('./matlab', 'dir')
if exist('./matlab/mat/nass_model_stages.mat', 'file')
save('matlab/mat/nass_model_stages.mat', 'ty', '-append');
else
save('matlab/mat/nass_model_stages.mat', 'ty');
end
end
end
#+end_src
*** =initializeRy=: Tilt Stage
#+begin_src matlab :tangle matlab/src/initializeRy.m :comments none :mkdirp yes :eval no
function [ry] = initializeRy(args)
arguments
args.type char {mustBeMember(args.type,{'none', 'rigid', 'flexible'})} = 'flexible'
args.Ry_init (1,1) double {mustBeNumeric} = 0
end
ry = struct();
switch args.type
case 'none'
ry.type = 0;
case 'rigid'
ry.type = 1;
case 'flexible'
ry.type = 2;
end
% Ry - Guide for the tilt stage
ry.guide.density = 7800; % [kg/m3]
ry.guide.STEP = 'Tilt_Guide.STEP';
% Ry - Rotor of the motor
ry.rotor.density = 2400; % [kg/m3]
ry.rotor.STEP = 'Tilt_Motor_Axis.STEP';
% Ry - Motor
ry.motor.density = 3200; % [kg/m3]
ry.motor.STEP = 'Tilt_Motor.STEP';
% Ry - Plateau Tilt
ry.stage.density = 7800; % [kg/m3]
ry.stage.STEP = 'Tilt_Stage.STEP';
% Z-Offset so that the center of rotation matches the sample center;
ry.z_offset = 0.58178; % [m]
ry.Ry_init = args.Ry_init; % [rad]
ry.K = [3.8e8; 4e8; 3.8e8; 1.2e8; 6e4; 1.2e8];
ry.C = [1e5; 1e5; 1e5; 3e4; 1e3; 3e4];
if exist('./mat', 'dir')
if exist('./mat/nass_model_stages.mat', 'file')
save('mat/nass_model_stages.mat', 'ry', '-append');
else
save('mat/nass_model_stages.mat', 'ry');
end
elseif exist('./matlab', 'dir')
if exist('./matlab/mat/nass_model_stages.mat', 'file')
save('matlab/mat/nass_model_stages.mat', 'ry', '-append');
else
save('matlab/mat/nass_model_stages.mat', 'ry');
end
end
end
#+end_src
*** =initializeRz=: Spindle
#+begin_src matlab :tangle matlab/src/initializeRz.m :comments none :mkdirp yes :eval no
function [rz] = initializeRz(args)
arguments
args.type char {mustBeMember(args.type,{'none', 'rigid', 'flexible'})} = 'flexible'
end
rz = struct();
switch args.type
case 'none'
rz.type = 0;
case 'rigid'
rz.type = 1;
case 'flexible'
rz.type = 2;
end
% Spindle - Slip Ring
rz.slipring.density = 7800; % [kg/m3]
rz.slipring.STEP = 'Spindle_Slip_Ring.STEP';
% Spindle - Rotor
rz.rotor.density = 7800; % [kg/m3]
rz.rotor.STEP = 'Spindle_Rotor.STEP';
% Spindle - Stator
rz.stator.density = 7800; % [kg/m3]
rz.stator.STEP = 'Spindle_Stator.STEP';
rz.K = [7e8; 7e8; 2e9; 1e7; 1e7; 1e7];
rz.C = [4e4; 4e4; 7e4; 1e4; 1e4; 1e4];
if exist('./mat', 'dir')
if exist('./mat/nass_model_stages.mat', 'file')
save('mat/nass_model_stages.mat', 'rz', '-append');
else
save('mat/nass_model_stages.mat', 'rz');
end
elseif exist('./matlab', 'dir')
if exist('./matlab/mat/nass_model_stages.mat', 'file')
save('matlab/mat/nass_model_stages.mat', 'rz', '-append');
else
save('matlab/mat/nass_model_stages.mat', 'rz');
end
end
end
#+end_src
*** =initializeMicroHexapod=: Micro Hexapod
#+begin_src matlab :tangle matlab/src/initializeMicroHexapod.m :comments none :mkdirp yes :eval no
function [micro_hexapod] = initializeMicroHexapod(args)
arguments
args.type char {mustBeMember(args.type,{'none', 'rigid', 'flexible'})} = 'flexible'
% initializeFramesPositions
args.H (1,1) double {mustBeNumeric, mustBePositive} = 350e-3
args.MO_B (1,1) double {mustBeNumeric} = 270e-3
% generateGeneralConfiguration
args.FH (1,1) double {mustBeNumeric, mustBePositive} = 50e-3
args.FR (1,1) double {mustBeNumeric, mustBePositive} = 175.5e-3
args.FTh (6,1) double {mustBeNumeric} = [-10, 10, 120-10, 120+10, 240-10, 240+10]*(pi/180)
args.MH (1,1) double {mustBeNumeric, mustBePositive} = 45e-3
args.MR (1,1) double {mustBeNumeric, mustBePositive} = 118e-3
args.MTh (6,1) double {mustBeNumeric} = [-60+10, 60-10, 60+10, 180-10, 180+10, -60-10]*(pi/180)
% initializeStrutDynamics
args.Ki (6,1) double {mustBeNumeric, mustBeNonnegative} = 2e7*ones(6,1)
args.Ci (6,1) double {mustBeNumeric, mustBeNonnegative} = 1.4e3*ones(6,1)
% initializeCylindricalPlatforms
args.Fpm (1,1) double {mustBeNumeric, mustBePositive} = 10
args.Fph (1,1) double {mustBeNumeric, mustBePositive} = 26e-3
args.Fpr (1,1) double {mustBeNumeric, mustBePositive} = 207.5e-3
args.Mpm (1,1) double {mustBeNumeric, mustBePositive} = 10
args.Mph (1,1) double {mustBeNumeric, mustBePositive} = 26e-3
args.Mpr (1,1) double {mustBeNumeric, mustBePositive} = 150e-3
% initializeCylindricalStruts
args.Fsm (1,1) double {mustBeNumeric, mustBePositive} = 1
args.Fsh (1,1) double {mustBeNumeric, mustBePositive} = 100e-3
args.Fsr (1,1) double {mustBeNumeric, mustBePositive} = 25e-3
args.Msm (1,1) double {mustBeNumeric, mustBePositive} = 1
args.Msh (1,1) double {mustBeNumeric, mustBePositive} = 100e-3
args.Msr (1,1) double {mustBeNumeric, mustBePositive} = 25e-3
% inverseKinematics
args.AP (3,1) double {mustBeNumeric} = zeros(3,1)
args.ARB (3,3) double {mustBeNumeric} = eye(3)
end
stewart = initializeStewartPlatform();
stewart = initializeFramesPositions(stewart, ...
'H', args.H, ...
'MO_B', args.MO_B);
stewart = generateGeneralConfiguration(stewart, ...
'FH', args.FH, ...
'FR', args.FR, ...
'FTh', args.FTh, ...
'MH', args.MH, ...
'MR', args.MR, ...
'MTh', args.MTh);
stewart = computeJointsPose(stewart);
stewart = initializeStrutDynamics(stewart, ...
'K', args.Ki, ...
'C', args.Ci);
stewart = initializeJointDynamics(stewart, ...
'type_F', 'universal_p', ...
'type_M', 'spherical_p');
stewart = initializeCylindricalPlatforms(stewart, ...
'Fpm', args.Fpm, ...
'Fph', args.Fph, ...
'Fpr', args.Fpr, ...
'Mpm', args.Mpm, ...
'Mph', args.Mph, ...
'Mpr', args.Mpr);
stewart = initializeCylindricalStruts(stewart, ...
'Fsm', args.Fsm, ...
'Fsh', args.Fsh, ...
'Fsr', args.Fsr, ...
'Msm', args.Msm, ...
'Msh', args.Msh, ...
'Msr', args.Msr);
stewart = computeJacobian(stewart);
stewart = initializeStewartPose(stewart, ...
'AP', args.AP, ...
'ARB', args.ARB);
stewart = initializeInertialSensor(stewart, 'type', 'none');
switch args.type
case 'none'
stewart.type = 0;
case 'rigid'
stewart.type = 1;
case 'flexible'
stewart.type = 2;
end
micro_hexapod = stewart;
if exist('./mat', 'dir')
if exist('./mat/nass_model_stages.mat', 'file')
save('mat/nass_model_stages.mat', 'micro_hexapod', '-append');
else
save('mat/nass_model_stages.mat', 'micro_hexapod');
end
elseif exist('./matlab', 'dir')
if exist('./matlab/mat/nass_model_stages.mat', 'file')
save('matlab/mat/nass_model_stages.mat', 'micro_hexapod', '-append');
else
save('matlab/mat/nass_model_stages.mat', 'micro_hexapod');
end
end
end
#+end_src
*** =initializeNanoHexapod=: Nano-Hexapod
#+begin_src matlab :tangle matlab/src/initializeNanoHexapod.m :comments none :mkdirp yes :eval no
function [nano_hexapod] = initializeNanoHexapod(args)
arguments
%% Bottom Flexible Joints
args.flex_bot_type char {mustBeMember(args.flex_bot_type,{'2dof', '3dof', '4dof', 'flexible'})} = '4dof'
args.flex_bot_kRx (6,1) double {mustBeNumeric} = ones(6,1)*5 % X bending stiffness [Nm/rad]
args.flex_bot_kRy (6,1) double {mustBeNumeric} = ones(6,1)*5 % Y bending stiffness [Nm/rad]
args.flex_bot_kRz (6,1) double {mustBeNumeric} = ones(6,1)*260 % Torsionnal stiffness [Nm/rad]
args.flex_bot_kz (6,1) double {mustBeNumeric} = ones(6,1)*7e7 % Axial Stiffness [N/m]
args.flex_bot_cRx (6,1) double {mustBeNumeric} = ones(6,1)*0.001 % X bending Damping [Nm/(rad/s)]
args.flex_bot_cRy (6,1) double {mustBeNumeric} = ones(6,1)*0.001 % Y bending Damping [Nm/(rad/s)]
args.flex_bot_cRz (6,1) double {mustBeNumeric} = ones(6,1)*0.001 % Torsionnal Damping [Nm/(rad/s)]
args.flex_bot_cz (6,1) double {mustBeNumeric} = ones(6,1)*0.001 % Axial Damping [N/(m/s)]
%% Top Flexible Joints
args.flex_top_type char {mustBeMember(args.flex_top_type,{'2dof', '3dof', '4dof', 'flexible'})} = '4dof'
args.flex_top_kRx (6,1) double {mustBeNumeric} = ones(6,1)*5 % X bending stiffness [Nm/rad]
args.flex_top_kRy (6,1) double {mustBeNumeric} = ones(6,1)*5 % Y bending stiffness [Nm/rad]
args.flex_top_kRz (6,1) double {mustBeNumeric} = ones(6,1)*260 % Torsionnal stiffness [Nm/rad]
args.flex_top_kz (6,1) double {mustBeNumeric} = ones(6,1)*7e7 % Axial Stiffness [N/m]
args.flex_top_cRx (6,1) double {mustBeNumeric} = ones(6,1)*0.001 % X bending Damping [Nm/(rad/s)]
args.flex_top_cRy (6,1) double {mustBeNumeric} = ones(6,1)*0.001 % Y bending Damping [Nm/(rad/s)]
args.flex_top_cRz (6,1) double {mustBeNumeric} = ones(6,1)*0.001 % Torsionnal Damping [Nm/(rad/s)]
args.flex_top_cz (6,1) double {mustBeNumeric} = ones(6,1)*0.001 % Axial Damping [N/(m/s)]
%% Jacobian - Location of frame {A} and {B}
args.MO_B (1,1) double {mustBeNumeric} = 150e-3 % Height of {B} w.r.t. {M} [m]
%% Relative Motion Sensor
args.motion_sensor_type char {mustBeMember(args.motion_sensor_type,{'struts', 'plates'})} = 'struts'
%% Top Plate
args.top_plate_type char {mustBeMember(args.top_plate_type,{'rigid', 'flexible'})} = 'rigid'
args.top_plate_xi (1,1) double {mustBeNumeric} = 0.01 % Damping Ratio
%% Actuators
args.actuator_type char {mustBeMember(args.actuator_type,{'2dof', 'flexible frame', 'flexible'})} = 'flexible'
args.actuator_Ga (6,1) double {mustBeNumeric} = zeros(6,1) % Actuator gain [N/V]
args.actuator_Gs (6,1) double {mustBeNumeric} = zeros(6,1) % Sensor gain [V/m]
% For 2DoF
args.actuator_k (6,1) double {mustBeNumeric, mustBePositive} = ones(6,1)*380000
args.actuator_ke (6,1) double {mustBeNumeric, mustBePositive} = ones(6,1)*4952605
args.actuator_ka (6,1) double {mustBeNumeric, mustBePositive} = ones(6,1)*2476302
args.actuator_c (6,1) double {mustBeNumeric, mustBePositive} = ones(6,1)*5
args.actuator_ce (6,1) double {mustBeNumeric, mustBePositive} = ones(6,1)*100
args.actuator_ca (6,1) double {mustBeNumeric, mustBePositive} = ones(6,1)*50
args.actuator_Leq (6,1) double {mustBeNumeric} = ones(6,1)*0.056 % [m]
% For Flexible Frame
args.actuator_ks (6,1) double {mustBeNumeric} = ones(6,1)*235e6 % Stiffness of one stack [N/m]
args.actuator_cs (6,1) double {mustBeNumeric} = ones(6,1)*1e1 % Stiffness of one stack [N/m]
% Misalignment
args.actuator_d_align (6,3) double {mustBeNumeric} = zeros(6,3) % [m]
args.actuator_xi (1,1) double {mustBeNumeric} = 0.01 % Damping Ratio
%% Controller
args.controller_type char {mustBeMember(args.controller_type,{'none', 'iff', 'dvf', 'hac-iff-struts'})} = 'none'
end
nano_hexapod = struct();
nano_hexapod.flex_bot = struct();
switch args.flex_bot_type
case '2dof'
nano_hexapod.flex_bot.type = 1;
case '3dof'
nano_hexapod.flex_bot.type = 2;
case '4dof'
nano_hexapod.flex_bot.type = 3;
case 'flexible'
nano_hexapod.flex_bot.type = 4;
end
nano_hexapod.flex_bot.kRx = args.flex_bot_kRx; % X bending stiffness [Nm/rad]
nano_hexapod.flex_bot.kRy = args.flex_bot_kRy; % Y bending stiffness [Nm/rad]
nano_hexapod.flex_bot.kRz = args.flex_bot_kRz; % Torsionnal stiffness [Nm/rad]
nano_hexapod.flex_bot.kz = args.flex_bot_kz; % Axial stiffness [N/m]
nano_hexapod.flex_bot.cRx = args.flex_bot_cRx; % [Nm/(rad/s)]
nano_hexapod.flex_bot.cRy = args.flex_bot_cRy; % [Nm/(rad/s)]
nano_hexapod.flex_bot.cRz = args.flex_bot_cRz; % [Nm/(rad/s)]
nano_hexapod.flex_bot.cz = args.flex_bot_cz; %[N/(m/s)]
nano_hexapod.flex_top = struct();
switch args.flex_top_type
case '2dof'
nano_hexapod.flex_top.type = 1;
case '3dof'
nano_hexapod.flex_top.type = 2;
case '4dof'
nano_hexapod.flex_top.type = 3;
case 'flexible'
nano_hexapod.flex_top.type = 4;
end
nano_hexapod.flex_top.kRx = args.flex_top_kRx; % X bending stiffness [Nm/rad]
nano_hexapod.flex_top.kRy = args.flex_top_kRy; % Y bending stiffness [Nm/rad]
nano_hexapod.flex_top.kRz = args.flex_top_kRz; % Torsionnal stiffness [Nm/rad]
nano_hexapod.flex_top.kz = args.flex_top_kz; % Axial stiffness [N/m]
nano_hexapod.flex_top.cRx = args.flex_top_cRx; % [Nm/(rad/s)]
nano_hexapod.flex_top.cRy = args.flex_top_cRy; % [Nm/(rad/s)]
nano_hexapod.flex_top.cRz = args.flex_top_cRz; % [Nm/(rad/s)]
nano_hexapod.flex_top.cz = args.flex_top_cz; %[N/(m/s)]
nano_hexapod.motion_sensor = struct();
switch args.motion_sensor_type
case 'struts'
nano_hexapod.motion_sensor.type = 1;
case 'plates'
nano_hexapod.motion_sensor.type = 2;
end
nano_hexapod.actuator = struct();
switch args.actuator_type
case '2dof'
nano_hexapod.actuator.type = 1;
case 'flexible frame'
nano_hexapod.actuator.type = 2;
case 'flexible'
nano_hexapod.actuator.type = 3;
end
%% Actuator gain [N/V]
if all(args.actuator_Ga == 0)
switch args.actuator_type
case '2dof'
nano_hexapod.actuator.Ga = ones(6,1)*(-2.5796);
case 'flexible frame'
nano_hexapod.actuator.Ga = ones(6,1); % TODO
case 'flexible'
nano_hexapod.actuator.Ga = ones(6,1)*23.2;
end
else
nano_hexapod.actuator.Ga = args.actuator_Ga; % Actuator gain [N/V]
end
%% Sensor gain [V/m]
if all(args.actuator_Gs == 0)
switch args.actuator_type
case '2dof'
nano_hexapod.actuator.Gs = ones(6,1)*466664;
case 'flexible frame'
nano_hexapod.actuator.Gs = ones(6,1); % TODO
case 'flexible'
nano_hexapod.actuator.Gs = ones(6,1)*(-4898341);
end
else
nano_hexapod.actuator.Gs = args.actuator_Gs; % Sensor gain [V/m]
end
switch args.actuator_type
case '2dof'
nano_hexapod.actuator.k = args.actuator_k; % [N/m]
nano_hexapod.actuator.ke = args.actuator_ke; % [N/m]
nano_hexapod.actuator.ka = args.actuator_ka; % [N/m]
nano_hexapod.actuator.c = args.actuator_c; % [N/(m/s)]
nano_hexapod.actuator.ce = args.actuator_ce; % [N/(m/s)]
nano_hexapod.actuator.ca = args.actuator_ca; % [N/(m/s)]
nano_hexapod.actuator.Leq = args.actuator_Leq; % [m]
case 'flexible frame'
nano_hexapod.actuator.K = readmatrix('APA300ML_b_mat_K.CSV'); % Stiffness Matrix
nano_hexapod.actuator.M = readmatrix('APA300ML_b_mat_M.CSV'); % Mass Matrix
nano_hexapod.actuator.P = extractNodes('APA300ML_b_out_nodes_3D.txt'); % Node coordinates [m]
nano_hexapod.actuator.ks = args.actuator_ks; % Stiffness of one stack [N/m]
nano_hexapod.actuator.cs = args.actuator_cs; % Damping of one stack [N/m]
nano_hexapod.actuator.xi = args.actuator_xi; % Damping ratio
case 'flexible'
nano_hexapod.actuator.K = readmatrix('full_APA300ML_K.CSV'); % Stiffness Matrix
nano_hexapod.actuator.M = readmatrix('full_APA300ML_M.CSV'); % Mass Matrix
nano_hexapod.actuator.P = extractNodes('full_APA300ML_out_nodes_3D.txt'); % Node coordiantes [m]
nano_hexapod.actuator.d_align = args.actuator_d_align; % Misalignment
nano_hexapod.actuator.xi = args.actuator_xi; % Damping ratio
end
nano_hexapod.geometry = struct();
Fa = [[-86.05, -74.78, 22.49],
[ 86.05, -74.78, 22.49],
[ 107.79, -37.13, 22.49],
[ 21.74, 111.91, 22.49],
[-21.74, 111.91, 22.49],
[-107.79, -37.13, 22.49]]'*1e-3; % Ai w.r.t. {F} [m]
Mb = [[-28.47, -106.25, -22.50],
[ 28.47, -106.25, -22.50],
[ 106.25, 28.47, -22.50],
[ 77.78, 77.78, -22.50],
[-77.78, 77.78, -22.50],
[-106.25, 28.47, -22.50]]'*1e-3; % Bi w.r.t. {M} [m]
Fb = Mb + [0; 0; 95e-3]; % Bi w.r.t. {F} [m]
si = Fb - Fa;
si = si./vecnorm(si); % Normalize
Fc = [[-29.362, -105.765, 52.605]
[ 29.362, -105.765, 52.605]
[ 106.276, 27.454, 52.605]
[ 76.914, 78.31, 52.605]
[-76.914, 78.31, 52.605]
[-106.276, 27.454, 52.605]]'*1e-3; % Meas pos w.r.t. {F}
Mc = Fc - [0; 0; 95e-3]; % Meas pos w.r.t. {M}
nano_hexapod.geometry.Fa = Fa;
nano_hexapod.geometry.Fb = Fb;
nano_hexapod.geometry.Fc = Fc;
nano_hexapod.geometry.Mb = Mb;
nano_hexapod.geometry.Mc = Mc;
nano_hexapod.geometry.si = si;
nano_hexapod.geometry.MO_B = args.MO_B;
Bb = Mb - [0; 0; args.MO_B];
nano_hexapod.geometry.J = [nano_hexapod.geometry.si', cross(Bb, nano_hexapod.geometry.si)'];
switch args.motion_sensor_type
case 'struts'
nano_hexapod.geometry.Js = nano_hexapod.geometry.J;
case 'plates'
Bc = Mc - [0; 0; args.MO_B];
nano_hexapod.geometry.Js = [nano_hexapod.geometry.si', cross(Bc, nano_hexapod.geometry.si)'];
end
nano_hexapod.top_plate = struct();
switch args.top_plate_type
case 'rigid'
nano_hexapod.top_plate.type = 1;
case 'flexible'
nano_hexapod.top_plate.type = 2;
nano_hexapod.top_plate.R_flex = ...
{[ 0.53191886726305 0.4795690716524 0.69790817745892
-0.29070157897799 0.8775041341865 -0.38141720787774
-0.79533320729697 0 0.60617249143351 ],
[ 0.53191886726305 -0.4795690716524 -0.69790817745892
0.29070157897799 0.8775041341865 -0.38141720787774
0.79533320729697 0 0.60617249143351 ],
[-0.01420448131633 -0.9997254079576 -0.01863709726680
0.60600604129104 -0.0234330681729 0.79511481512719
-0.79533320729697 0 0.60617249143351 ],
[-0.51771438594672 -0.5201563363051 0.67927108019212
0.31530446231304 -0.8540710660135 -0.41369760724945
0.79533320729697 0 0.60617249143351 ],
[-0.51771438594671 0.5201563363052 -0.67927108019211
-0.31530446231304 -0.8540710660135 -0.41369760724945
-0.79533320729697 0 0.60617249143351 ],
[-0.01420448131632 0.9997254079576 0.01863709726679
-0.60600604129104 -0.0234330681729 0.79511481512719
0.79533320729697 0 0.60617249143351 ] };
nano_hexapod.top_plate.R_enc = ...
{ [-0.877504134186525 -0.479569071652412 0
0.479569071652412 -0.877504134186525 0
0 0 1 ],
[ 0.877504134186525 -0.479569071652413 0
0.479569071652413 0.877504134186525 0
0 0 1 ],
[ 0.023433068172945 0.999725407957606 0
-0.999725407957606 0.023433068172945 0
0 0 1 ],
[-0.854071066013566 -0.520156336305202 0
0.520156336305202 -0.854071066013566 0
0 0 1 ],
[ 0.854071066013574 -0.520156336305191 0
0.520156336305191 0.854071066013574 0
0 0 1 ],
[-0.023433068172958 0.999725407957606 0
-0.999725407957606 -0.023433068172958 0
0 0 1 ] };
nano_hexapod.top_plate.K = readmatrix('top_plate_K_6.CSV'); % Stiffness Matrix
nano_hexapod.top_plate.M = readmatrix('top_plate_M_6.CSV'); % Mass Matrix
nano_hexapod.top_plate.P = extractNodes('top_plate_out_nodes_3D_qua.txt'); % Node coordiantes [m]
nano_hexapod.top_plate.xi = args.top_plate_xi; % Damping ratio
end
if exist('./mat', 'dir')
if exist('./mat/nass_model_stages.mat', 'file')
save('mat/nass_model_stages.mat', 'nano_hexapod', '-append');
else
save('mat/nass_model_stages.mat', 'nano_hexapod');
end
elseif exist('./matlab', 'dir')
if exist('./matlab/mat/nass_model_stages.mat', 'file')
save('matlab/mat/nass_model_stages.mat', 'nano_hexapod', '-append');
else
save('matlab/mat/nass_model_stages.mat', 'nano_hexapod');
end
end
end
#+end_src
*** =initializeSample=: Sample
#+begin_src matlab :tangle matlab/src/initializeSample.m :comments none :mkdirp yes :eval no
function [sample] = initializeSample(args)
arguments
args.type char {mustBeMember(args.type,{'0', '1', '2', '3'})} = '0'
end
sample = struct();
switch args.type
case '0'
sample.type = 0;
case '1'
sample.type = 1;
case '2'
sample.type = 2;
case '3'
sample.type = 3;
end
if exist('./mat', 'dir')
if exist('./mat/nass_model_stages.mat', 'file')
save('mat/nass_model_stages.mat', 'sample', '-append');
else
save('mat/nass_model_stages.mat', 'sample');
end
elseif exist('./matlab', 'dir')
if exist('./matlab/mat/nass_model_stages.mat', 'file')
save('matlab/mat/nass_model_stages.mat', 'sample', '-append');
else
save('matlab/mat/nass_model_stages.mat', 'sample');
end
end
end
#+end_src
** Stewart platform
*** =initializeStewartPlatform=: Initialize the Stewart Platform structure
:PROPERTIES:
:header-args:matlab+: :tangle matlab/src/initializeStewartPlatform.m
:header-args:matlab+: :comments none :mkdirp yes :eval no
:END:
**** Documentation
#+name : fig:stewart-frames-position
#+caption : Definition of the position of the frames
[[file:figs/stewart-frames-position.png ]]
**** Function description
#+begin_src matlab
function [stewart] = initializeStewartPlatform()
% initializeStewartPlatform - Initialize the stewart structure
%
% Syntax: [stewart] = initializeStewartPlatform(args)
%
% Outputs:
% - stewart - A structure with the following sub-structures:
% - platform_F -
% - platform_M -
% - joints_F -
% - joints_M -
% - struts_F -
% - struts_M -
% - actuators -
% - geometry -
% - properties -
#+end_src
**** Initialize the Stewart structure
#+begin_src matlab
stewart = struct();
stewart.platform_F = struct();
stewart.platform_M = struct();
stewart.joints_F = struct();
stewart.joints_M = struct();
stewart.struts_F = struct();
stewart.struts_M = struct();
stewart.actuators = struct();
stewart.sensors = struct();
stewart.sensors.inertial = struct();
stewart.sensors.force = struct();
stewart.sensors.relative = struct();
stewart.geometry = struct();
stewart.kinematics = struct();
#+end_src
*** =initializeFramesPositions=: Initialize the positions of frames {A}, {B}, {F} and {M}
:PROPERTIES:
:header-args:matlab+: :tangle matlab/src/initializeFramesPositions.m
:header-args:matlab+: :comments none :mkdirp yes :eval no
:END:
**** Documentation
#+name : fig:stewart-frames-position
#+caption : Definition of the position of the frames
[[file:figs/stewart-frames-position.png ]]
**** Function description
#+begin_src matlab
function [stewart] = initializeFramesPositions(stewart, args)
% initializeFramesPositions - Initialize the positions of frames {A}, {B}, {F} and {M}
%
% Syntax: [stewart] = initializeFramesPositions(stewart, args)
%
% Inputs:
% - args - Can have the following fields:
% - H [1x1] - Total Height of the Stewart Platform (height from {F} to {M}) [m]
% - MO_B [1x1] - Height of the frame {B} with respect to {M} [m]
%
% Outputs:
% - stewart - A structure with the following fields:
% - geometry.H [1x1] - Total Height of the Stewart Platform [m]
% - geometry.FO_M [3x1] - Position of {M} with respect to {F} [m]
% - platform_M.MO_B [3x1] - Position of {B} with respect to {M} [m]
% - platform_F.FO_A [3x1] - Position of {A} with respect to {F} [m]
#+end_src
**** Optional Parameters
#+begin_src matlab
arguments
stewart
args.H (1,1) double {mustBeNumeric, mustBePositive} = 90e-3
args.MO_B (1,1) double {mustBeNumeric} = 50e-3
end
#+end_src
**** Compute the position of each frame
#+begin_src matlab
H = args.H; % Total Height of the Stewart Platform [m]
FO_M = [0; 0; H]; % Position of {M} with respect to {F} [m]
MO_B = [0; 0; args.MO_B]; % Position of {B} with respect to {M} [m]
FO_A = MO_B + FO_M; % Position of {A} with respect to {F} [m]
#+end_src
**** Populate the =stewart= structure
#+begin_src matlab
stewart.geometry.H = H;
stewart.geometry.FO_M = FO_M;
stewart.platform_M.MO_B = MO_B;
stewart.platform_F.FO_A = FO_A;
#+end_src
*** =generateGeneralConfiguration=: Generate a Very General Configuration
:PROPERTIES:
:header-args:matlab+: :tangle matlab/src/generateGeneralConfiguration.m
:header-args:matlab+: :comments none :mkdirp yes :eval no
:END:
**** Documentation
#+begin_src latex :file stewart_bottom_plate.pdf :tangle no
\begin{tikzpicture}
% Internal and external limit
\draw[fill=white!80!black] (0, 0) circle [radius=3];
% Circle where the joints are located
\draw[dashed] (0, 0) circle [radius=2.5];
% Bullets for the positions of the joints
\node[] (J1) at ( 80:2.5){$\bullet$};
\node[] (J2) at (100:2.5){$\bullet$};
\node[] (J3) at (200:2.5){$\bullet$};
\node[] (J4) at (220:2.5){$\bullet$};
\node[] (J5) at (320:2.5){$\bullet$};
\node[] (J6) at (340:2.5){$\bullet$};
% Name of the points
\node[above right] at (J1) {$a_{1}$};
\node[above left] at (J2) {$a_{2}$};
\node[above left] at (J3) {$a_{3}$};
\node[right ] at (J4) {$a_{4}$};
\node[left ] at (J5) {$a_{5}$};
\node[above right] at (J6) {$a_{6}$};
% First 2 angles
\draw[dashed, ->] (0:1) arc [start angle=0, end angle=80, radius=1] node[below right]{$\theta_{1}$};
\draw[dashed, ->] (0:1.5) arc [start angle=0, end angle=100, radius=1.5] node[left ]{$\theta_{2}$};
% Division of 360 degrees by 3
\draw[dashed] (0, 0) -- ( 80:3.2);
\draw[dashed] (0, 0) -- (100:3.2);
\draw[dashed] (0, 0) -- (200:3.2);
\draw[dashed] (0, 0) -- (220:3.2);
\draw[dashed] (0, 0) -- (320:3.2);
\draw[dashed] (0, 0) -- (340:3.2);
% Radius for the position of the joints
\draw[<- >] (0, 0) --node[near end, above]{$R$} (180:2.5);
\draw[->] (0, 0) -- ++(3.4, 0) node[above]{$x$};
\draw[->] (0, 0) -- ++(0, 3.4) node[left]{$y$};
\end{tikzpicture}
#+end_src
**** Function description
#+begin_src matlab
function [stewart] = generateGeneralConfiguration(stewart, args)
% generateGeneralConfiguration - Generate a Very General Configuration
%
% Syntax: [stewart] = generateGeneralConfiguration(stewart, args)
%
% Inputs:
% - args - Can have the following fields:
% - FH [1x1] - Height of the position of the fixed joints with respect to the frame {F} [m]
% - FR [1x1] - Radius of the position of the fixed joints in the X-Y [m]
% - FTh [6x1] - Angles of the fixed joints in the X-Y plane with respect to the X axis [rad]
% - MH [1x1] - Height of the position of the mobile joints with respect to the frame {M} [m]
% - FR [1x1] - Radius of the position of the mobile joints in the X-Y [m]
% - MTh [6x1] - Angles of the mobile joints in the X-Y plane with respect to the X axis [rad]
%
% Outputs:
% - stewart - updated Stewart structure with the added fields:
% - platform_F.Fa [3x6] - Its i'th column is the position vector of joint ai with respect to {F}
% - platform_M.Mb [3x6] - Its i'th column is the position vector of joint bi with respect to {M}
#+end_src
**** Optional Parameters
#+begin_src matlab
arguments
stewart
args.FH (1,1) double {mustBeNumeric, mustBePositive} = 15e-3
args.FR (1,1) double {mustBeNumeric, mustBePositive} = 115e-3;
args.FTh (6,1) double {mustBeNumeric} = [-10, 10, 120-10, 120+10, 240-10, 240+10]*(pi/180);
args.MH (1,1) double {mustBeNumeric, mustBePositive} = 15e-3
args.MR (1,1) double {mustBeNumeric, mustBePositive} = 90e-3;
args.MTh (6,1) double {mustBeNumeric} = [-60+10, 60-10, 60+10, 180-10, 180+10, -60-10]*(pi/180);
end
#+end_src
**** Compute the pose
#+begin_src matlab
Fa = zeros(3,6);
Mb = zeros(3,6);
#+end_src
#+begin_src matlab
for i = 1:6
Fa(:,i) = [args.FR*cos(args.FTh(i)); args.FR*sin(args.FTh(i)); args.FH];
Mb(:,i) = [args.MR*cos(args.MTh(i)); args.MR*sin(args.MTh(i)); -args.MH];
end
#+end_src
**** Populate the =stewart= structure
#+begin_src matlab
stewart.platform_F.Fa = Fa;
stewart.platform_M.Mb = Mb;
#+end_src
*** =computeJointsPose=: Compute the Pose of the Joints
:PROPERTIES:
:header-args:matlab+: :tangle matlab/src/computeJointsPose.m
:header-args:matlab+: :comments none :mkdirp yes :eval no
:END:
**** Documentation
#+name : fig:stewart-struts
#+caption : Position and orientation of the struts
[[file:figs/stewart-struts.png ]]
**** Function description
#+begin_src matlab
function [stewart] = computeJointsPose(stewart)
% computeJointsPose -
%
% Syntax: [stewart] = computeJointsPose(stewart)
%
% Inputs:
% - stewart - A structure with the following fields
% - platform_F.Fa [3x6] - Its i'th column is the position vector of joint ai with respect to {F}
% - platform_M.Mb [3x6] - Its i'th column is the position vector of joint bi with respect to {M}
% - platform_F.FO_A [3x1] - Position of {A} with respect to {F}
% - platform_M.MO_B [3x1] - Position of {B} with respect to {M}
% - geometry.FO_M [3x1] - Position of {M} with respect to {F}
%
% Outputs:
% - stewart - A structure with the following added fields
% - geometry.Aa [3x6] - The i'th column is the position of ai with respect to {A}
% - geometry.Ab [3x6] - The i'th column is the position of bi with respect to {A}
% - geometry.Ba [3x6] - The i'th column is the position of ai with respect to {B}
% - geometry.Bb [3x6] - The i'th column is the position of bi with respect to {B}
% - geometry.l [6x1] - The i'th element is the initial length of strut i
% - geometry.As [3x6] - The i'th column is the unit vector of strut i expressed in {A}
% - geometry.Bs [3x6] - The i'th column is the unit vector of strut i expressed in {B}
% - struts_F.l [6x1] - Length of the Fixed part of the i'th strut
% - struts_M.l [6x1] - Length of the Mobile part of the i'th strut
% - platform_F.FRa [3x3x6] - The i'th 3x3 array is the rotation matrix to orientate the bottom of the i'th strut from {F}
% - platform_M.MRb [3x3x6] - The i'th 3x3 array is the rotation matrix to orientate the top of the i'th strut from {M}
#+end_src
**** Check the =stewart= structure elements
#+begin_src matlab
assert(isfield(stewart.platform_F, 'Fa'), 'stewart.platform_F should have attribute Fa')
Fa = stewart.platform_F.Fa;
assert(isfield(stewart.platform_M, 'Mb'), 'stewart.platform_M should have attribute Mb')
Mb = stewart.platform_M.Mb;
assert(isfield(stewart.platform_F, 'FO_A'), 'stewart.platform_F should have attribute FO_A')
FO_A = stewart.platform_F.FO_A;
assert(isfield(stewart.platform_M, 'MO_B'), 'stewart.platform_M should have attribute MO_B')
MO_B = stewart.platform_M.MO_B;
assert(isfield(stewart.geometry, 'FO_M'), 'stewart.geometry should have attribute FO_M')
FO_M = stewart.geometry.FO_M;
#+end_src
**** Compute the position of the Joints
#+begin_src matlab
Aa = Fa - repmat(FO_A, [1, 6]);
Bb = Mb - repmat(MO_B, [1, 6]);
Ab = Bb - repmat(-MO_B-FO_M+FO_A, [1, 6]);
Ba = Aa - repmat( MO_B+FO_M-FO_A, [1, 6]);
#+end_src
**** Compute the strut length and orientation
#+begin_src matlab
As = (Ab - Aa)./vecnorm(Ab - Aa); % As_i is the i'th vector of As
l = vecnorm(Ab - Aa)';
#+end_src
#+begin_src matlab
Bs = (Bb - Ba)./vecnorm(Bb - Ba);
#+end_src
**** Compute the orientation of the Joints
#+begin_src matlab
FRa = zeros(3,3,6);
MRb = zeros(3,3,6);
for i = 1:6
FRa(:,:,i) = [cross([0;1;0], As(:,i)) , cross(As(:,i), cross([0;1;0], As(:,i))) , As(:,i)];
FRa(:,:,i) = FRa(:,:,i)./vecnorm(FRa(:,:,i));
MRb(:,:,i) = [cross([0;1;0], Bs(:,i)) , cross(Bs(:,i), cross([0;1;0], Bs(:,i))) , Bs(:,i)];
MRb(:,:,i) = MRb(:,:,i)./vecnorm(MRb(:,:,i));
end
#+end_src
**** Populate the =stewart= structure
#+begin_src matlab
stewart.geometry.Aa = Aa;
stewart.geometry.Ab = Ab;
stewart.geometry.Ba = Ba;
stewart.geometry.Bb = Bb;
stewart.geometry.As = As;
stewart.geometry.Bs = Bs;
stewart.geometry.l = l;
stewart.struts_F.l = l/2;
stewart.struts_M.l = l/2;
stewart.platform_F.FRa = FRa;
stewart.platform_M.MRb = MRb;
#+end_src
*** =initializeCylindricalPlatforms=: Initialize the geometry of the Fixed and Mobile Platforms
:PROPERTIES:
:header-args:matlab+: :tangle matlab/src/initializeCylindricalPlatforms.m
:header-args:matlab+: :comments none :mkdirp yes :eval no
:END:
**** Function description
#+begin_src matlab
function [stewart] = initializeCylindricalPlatforms(stewart, args)
% initializeCylindricalPlatforms - Initialize the geometry of the Fixed and Mobile Platforms
%
% Syntax: [stewart] = initializeCylindricalPlatforms(args)
%
% Inputs:
% - args - Structure with the following fields:
% - Fpm [1x1] - Fixed Platform Mass [kg]
% - Fph [1x1] - Fixed Platform Height [m]
% - Fpr [1x1] - Fixed Platform Radius [m]
% - Mpm [1x1] - Mobile Platform Mass [kg]
% - Mph [1x1] - Mobile Platform Height [m]
% - Mpr [1x1] - Mobile Platform Radius [m]
%
% Outputs:
% - stewart - updated Stewart structure with the added fields:
% - platform_F [struct] - structure with the following fields:
% - type = 1
% - M [1x1] - Fixed Platform Mass [kg]
% - I [3x3] - Fixed Platform Inertia matrix [kg*m^2]
% - H [1x1] - Fixed Platform Height [m]
% - R [1x1] - Fixed Platform Radius [m]
% - platform_M [struct] - structure with the following fields:
% - M [1x1] - Mobile Platform Mass [kg]
% - I [3x3] - Mobile Platform Inertia matrix [kg*m^2]
% - H [1x1] - Mobile Platform Height [m]
% - R [1x1] - Mobile Platform Radius [m]
#+end_src
**** Optional Parameters
#+begin_src matlab
arguments
stewart
args.Fpm (1,1) double {mustBeNumeric, mustBePositive} = 1
args.Fph (1,1) double {mustBeNumeric, mustBePositive} = 10e-3
args.Fpr (1,1) double {mustBeNumeric, mustBePositive} = 125e-3
args.Mpm (1,1) double {mustBeNumeric, mustBePositive} = 1
args.Mph (1,1) double {mustBeNumeric, mustBePositive} = 10e-3
args.Mpr (1,1) double {mustBeNumeric, mustBePositive} = 100e-3
end
#+end_src
**** Compute the Inertia matrices of platforms
#+begin_src matlab
I_F = diag([1/12*args.Fpm * (3*args.Fpr^2 + args.Fph^2), ...
1/12*args.Fpm * (3*args.Fpr^2 + args.Fph^2), ...
1/2 *args.Fpm * args.Fpr^2]);
#+end_src
#+begin_src matlab
I_M = diag([1/12*args.Mpm * (3*args.Mpr^2 + args.Mph^2), ...
1/12*args.Mpm * (3*args.Mpr^2 + args.Mph^2), ...
1/2 *args.Mpm * args.Mpr^2]);
#+end_src
**** Populate the =stewart= structure
#+begin_src matlab
stewart.platform_F.type = 1;
stewart.platform_F.I = I_F;
stewart.platform_F.M = args.Fpm;
stewart.platform_F.R = args.Fpr;
stewart.platform_F.H = args.Fph;
#+end_src
#+begin_src matlab
stewart.platform_M.type = 1;
stewart.platform_M.I = I_M;
stewart.platform_M.M = args.Mpm;
stewart.platform_M.R = args.Mpr;
stewart.platform_M.H = args.Mph;
#+end_src
*** =initializeCylindricalStruts=: Define the inertia of cylindrical struts
:PROPERTIES:
:header-args:matlab+: :tangle matlab/src/initializeCylindricalStruts.m
:header-args:matlab+: :comments none :mkdirp yes :eval no
:END:
**** Function description
#+begin_src matlab
function [stewart] = initializeCylindricalStruts(stewart, args)
% initializeCylindricalStruts - Define the mass and moment of inertia of cylindrical struts
%
% Syntax: [stewart] = initializeCylindricalStruts(args)
%
% Inputs:
% - args - Structure with the following fields:
% - Fsm [1x1] - Mass of the Fixed part of the struts [kg]
% - Fsh [1x1] - Height of cylinder for the Fixed part of the struts [m]
% - Fsr [1x1] - Radius of cylinder for the Fixed part of the struts [m]
% - Msm [1x1] - Mass of the Mobile part of the struts [kg]
% - Msh [1x1] - Height of cylinder for the Mobile part of the struts [m]
% - Msr [1x1] - Radius of cylinder for the Mobile part of the struts [m]
%
% Outputs:
% - stewart - updated Stewart structure with the added fields:
% - struts_F [struct] - structure with the following fields:
% - M [6x1] - Mass of the Fixed part of the struts [kg]
% - I [3x3x6] - Moment of Inertia for the Fixed part of the struts [kg*m^2]
% - H [6x1] - Height of cylinder for the Fixed part of the struts [m]
% - R [6x1] - Radius of cylinder for the Fixed part of the struts [m]
% - struts_M [struct] - structure with the following fields:
% - M [6x1] - Mass of the Mobile part of the struts [kg]
% - I [3x3x6] - Moment of Inertia for the Mobile part of the struts [kg*m^2]
% - H [6x1] - Height of cylinder for the Mobile part of the struts [m]
% - R [6x1] - Radius of cylinder for the Mobile part of the struts [m]
#+end_src
**** Optional Parameters
#+begin_src matlab
arguments
stewart
args.Fsm (1,1) double {mustBeNumeric, mustBePositive} = 0.1
args.Fsh (1,1) double {mustBeNumeric, mustBePositive} = 50e-3
args.Fsr (1,1) double {mustBeNumeric, mustBePositive} = 5e-3
args.Msm (1,1) double {mustBeNumeric, mustBePositive} = 0.1
args.Msh (1,1) double {mustBeNumeric, mustBePositive} = 50e-3
args.Msr (1,1) double {mustBeNumeric, mustBePositive} = 5e-3
end
#+end_src
**** Compute the properties of the cylindrical struts
#+begin_src matlab
Fsm = ones(6,1).*args.Fsm;
Fsh = ones(6,1).*args.Fsh;
Fsr = ones(6,1).*args.Fsr;
Msm = ones(6,1).*args.Msm;
Msh = ones(6,1).*args.Msh;
Msr = ones(6,1).*args.Msr;
#+end_src
#+begin_src matlab
I_F = zeros(3, 3, 6); % Inertia of the "fixed" part of the strut
I_M = zeros(3, 3, 6); % Inertia of the "mobile" part of the strut
for i = 1:6
I_F(:,:,i) = diag([1/12 * Fsm(i) * (3*Fsr(i)^2 + Fsh(i)^2), ...
1/12 * Fsm(i) * (3*Fsr(i)^2 + Fsh(i)^2), ...
1/2 * Fsm(i) * Fsr(i)^2]);
I_M(:,:,i) = diag([1/12 * Msm(i) * (3*Msr(i)^2 + Msh(i)^2), ...
1/12 * Msm(i) * (3*Msr(i)^2 + Msh(i)^2), ...
1/2 * Msm(i) * Msr(i)^2]);
end
#+end_src
**** Populate the =stewart= structure
#+begin_src matlab
stewart.struts_M.type = 1;
stewart.struts_M.I = I_M;
stewart.struts_M.M = Msm;
stewart.struts_M.R = Msr;
stewart.struts_M.H = Msh;
#+end_src
#+begin_src matlab
stewart.struts_F.type = 1;
stewart.struts_F.I = I_F;
stewart.struts_F.M = Fsm;
stewart.struts_F.R = Fsr;
stewart.struts_F.H = Fsh;
#+end_src
*** =initializeStrutDynamics=: Add Stiffness and Damping properties of each strut
:PROPERTIES:
:header-args:matlab+: :tangle matlab/src/initializeStrutDynamics.m
:header-args:matlab+: :comments none :mkdirp yes :eval no
:END:
**** Documentation
#+name : fig:piezoelectric_stack
#+attr_html : :width 500px
#+caption : Example of a piezoelectric stach actuator (PI)
[[file:figs/piezoelectric_stack.jpg ]]
A simplistic model of such amplified actuator is shown in Figure ref:fig:actuator_model_simple where:
- $K$ represent the vertical stiffness of the actuator
- $C$ represent the vertical damping of the actuator
- $F$ represents the force applied by the actuator
- $F_{m}$ represents the total measured force
- $v_{m}$ represents the absolute velocity of the top part of the actuator
- $d_{m}$ represents the total relative displacement of the actuator
#+begin_src latex :file actuator_model_simple.pdf :tangle no
\begin{tikzpicture}
\draw (-1, 0) -- (1, 0);
% Spring, Damper, and Actuator
\draw[spring] (-1, 0) -- (-1, 1.5) node[midway, left=0.1]{$K$};
\draw[damper] ( 0, 0) -- ( 0, 1.5) node[midway, left=0.2]{$C$};
\draw[actuator] ( 1, 0) -- ( 1, 1.5) node[midway, left=0.1](F){$F$};
\node[forcesensor={2}{0.2}] (fsens) at (0, 1.5){};
\node[left] at (fsens.west) {$F_{m}$};
\draw[dashed] (1, 0) -- ++(0.4, 0);
\draw[dashed] (1, 1.7) -- ++(0.4, 0);
\draw[->] (0, 1.7)node[]{$\bullet$} -- ++(0, 0.5) node[right]{$v_{m}$};
\draw[<- >] (1.4, 0) -- ++(0, 1.7) node[midway, right]{$d_{m}$};
\end{tikzpicture}
#+end_src
#+name : fig:actuator_model_simple
#+caption : Simple model of an Actuator
#+RESULTS :
[[file:figs/actuator_model_simple.png ]]
**** Function description
#+begin_src matlab
function [stewart] = initializeStrutDynamics(stewart, args)
% initializeStrutDynamics - Add Stiffness and Damping properties of each strut
%
% Syntax: [stewart] = initializeStrutDynamics(args)
%
% Inputs:
% - args - Structure with the following fields:
% - K [6x1] - Stiffness of each strut [N/m]
% - C [6x1] - Damping of each strut [N/(m/s)]
%
% Outputs:
% - stewart - updated Stewart structure with the added fields:
% - actuators.type = 1
% - actuators.K [6x1] - Stiffness of each strut [N/m]
% - actuators.C [6x1] - Damping of each strut [N/(m/s)]
#+end_src
**** Optional Parameters
#+begin_src matlab
arguments
stewart
args.type char {mustBeMember(args.type,{'classical', 'amplified'})} = 'classical'
args.K (6,1) double {mustBeNumeric, mustBeNonnegative} = 20e6*ones(6,1)
args.C (6,1) double {mustBeNumeric, mustBeNonnegative} = 2e1*ones(6,1)
args.k1 (6,1) double {mustBeNumeric} = 1e6*ones(6,1)
args.ke (6,1) double {mustBeNumeric} = 5e6*ones(6,1)
args.ka (6,1) double {mustBeNumeric} = 60e6*ones(6,1)
args.c1 (6,1) double {mustBeNumeric} = 10*ones(6,1)
args.F_gain (6,1) double {mustBeNumeric} = 1*ones(6,1)
args.me (6,1) double {mustBeNumeric} = 0.01*ones(6,1)
args.ma (6,1) double {mustBeNumeric} = 0.01*ones(6,1)
end
#+end_src
**** Add Stiffness and Damping properties of each strut
#+begin_src matlab
if strcmp(args.type, 'classical')
stewart.actuators.type = 1;
elseif strcmp(args.type, 'amplified')
stewart.actuators.type = 2;
end
stewart.actuators.K = args.K;
stewart.actuators.C = args.C;
stewart.actuators.k1 = args.k1;
stewart.actuators.c1 = args.c1;
stewart.actuators.ka = args.ka;
stewart.actuators.ke = args.ke;
stewart.actuators.F_gain = args.F_gain;
stewart.actuators.ma = args.ma;
stewart.actuators.me = args.me;
#+end_src
*** =initializeJointDynamics=: Add Stiffness and Damping properties for spherical joints
:PROPERTIES:
:header-args:matlab+: :tangle matlab/src/initializeJointDynamics.m
:header-args:matlab+: :comments none :mkdirp yes :eval no
:END:
**** Function description
#+begin_src matlab
function [stewart] = initializeJointDynamics(stewart, args)
% initializeJointDynamics - Add Stiffness and Damping properties for the spherical joints
%
% Syntax: [stewart] = initializeJointDynamics(args)
%
% Inputs:
% - args - Structure with the following fields:
% - type_F - 'universal', 'spherical', 'universal_p', 'spherical_p'
% - type_M - 'universal', 'spherical', 'universal_p', 'spherical_p'
% - Kf_M [6x1] - Bending (Rx, Ry) Stiffness for each top joints [(N.m)/rad]
% - Kt_M [6x1] - Torsion (Rz) Stiffness for each top joints [(N.m)/rad]
% - Cf_M [6x1] - Bending (Rx, Ry) Damping of each top joint [(N.m)/(rad/s)]
% - Ct_M [6x1] - Torsion (Rz) Damping of each top joint [(N.m)/(rad/s)]
% - Kf_F [6x1] - Bending (Rx, Ry) Stiffness for each bottom joints [(N.m)/rad]
% - Kt_F [6x1] - Torsion (Rz) Stiffness for each bottom joints [(N.m)/rad]
% - Cf_F [6x1] - Bending (Rx, Ry) Damping of each bottom joint [(N.m)/(rad/s)]
% - Cf_F [6x1] - Torsion (Rz) Damping of each bottom joint [(N.m)/(rad/s)]
%
% Outputs:
% - stewart - updated Stewart structure with the added fields:
% - stewart.joints_F and stewart.joints_M:
% - type - 1 (universal), 2 (spherical), 3 (universal perfect), 4 (spherical perfect)
% - Kx, Ky, Kz [6x1] - Translation (Tx, Ty, Tz) Stiffness [N/m]
% - Kf [6x1] - Flexion (Rx, Ry) Stiffness [(N.m)/rad]
% - Kt [6x1] - Torsion (Rz) Stiffness [(N.m)/rad]
% - Cx, Cy, Cz [6x1] - Translation (Rx, Ry) Damping [N/(m/s)]
% - Cf [6x1] - Flexion (Rx, Ry) Damping [(N.m)/(rad/s)]
% - Cb [6x1] - Torsion (Rz) Damping [(N.m)/(rad/s)]
#+end_src
**** Optional Parameters
#+begin_src matlab
arguments
stewart
args.type_F char {mustBeMember(args.type_F,{'universal', 'spherical', 'universal_p', 'spherical_p', 'universal_3dof', 'spherical_3dof', 'flexible'})} = 'universal'
args.type_M char {mustBeMember(args.type_M,{'universal', 'spherical', 'universal_p', 'spherical_p', 'universal_3dof', 'spherical_3dof', 'flexible'})} = 'spherical'
args.Kf_M (6,1) double {mustBeNumeric, mustBeNonnegative} = 33*ones(6,1)
args.Cf_M (6,1) double {mustBeNumeric, mustBeNonnegative} = 1e-4*ones(6,1)
args.Kt_M (6,1) double {mustBeNumeric, mustBeNonnegative} = 236*ones(6,1)
args.Ct_M (6,1) double {mustBeNumeric, mustBeNonnegative} = 1e-3*ones(6,1)
args.Kf_F (6,1) double {mustBeNumeric, mustBeNonnegative} = 33*ones(6,1)
args.Cf_F (6,1) double {mustBeNumeric, mustBeNonnegative} = 1e-4*ones(6,1)
args.Kt_F (6,1) double {mustBeNumeric, mustBeNonnegative} = 236*ones(6,1)
args.Ct_F (6,1) double {mustBeNumeric, mustBeNonnegative} = 1e-3*ones(6,1)
args.Ka_F (6,1) double {mustBeNumeric, mustBeNonnegative} = 1.2e8*ones(6,1)
args.Ca_F (6,1) double {mustBeNumeric, mustBeNonnegative} = 1e1*ones(6,1)
args.Kr_F (6,1) double {mustBeNumeric, mustBeNonnegative} = 1.1e7*ones(6,1)
args.Cr_F (6,1) double {mustBeNumeric, mustBeNonnegative} = 1e1*ones(6,1)
args.Ka_M (6,1) double {mustBeNumeric, mustBeNonnegative} = 1.2e8*ones(6,1)
args.Ca_M (6,1) double {mustBeNumeric, mustBeNonnegative} = 1e1*ones(6,1)
args.Kr_M (6,1) double {mustBeNumeric, mustBeNonnegative} = 1.1e7*ones(6,1)
args.Cr_M (6,1) double {mustBeNumeric, mustBeNonnegative} = 1e1*ones(6,1)
args.K_M double {mustBeNumeric} = zeros(6,6)
args.M_M double {mustBeNumeric} = zeros(6,6)
args.n_xyz_M double {mustBeNumeric} = zeros(2,3)
args.xi_M double {mustBeNumeric} = 0.1
args.step_file_M char {} = ''
args.K_F double {mustBeNumeric} = zeros(6,6)
args.M_F double {mustBeNumeric} = zeros(6,6)
args.n_xyz_F double {mustBeNumeric} = zeros(2,3)
args.xi_F double {mustBeNumeric} = 0.1
args.step_file_F char {} = ''
end
#+end_src
**** Add Actuator Type
#+begin_src matlab
switch args.type_F
case 'universal'
stewart.joints_F.type = 1;
case 'spherical'
stewart.joints_F.type = 2;
case 'universal_p'
stewart.joints_F.type = 3;
case 'spherical_p'
stewart.joints_F.type = 4;
case 'flexible'
stewart.joints_F.type = 5;
case 'universal_3dof'
stewart.joints_F.type = 6;
case 'spherical_3dof'
stewart.joints_F.type = 7;
end
switch args.type_M
case 'universal'
stewart.joints_M.type = 1;
case 'spherical'
stewart.joints_M.type = 2;
case 'universal_p'
stewart.joints_M.type = 3;
case 'spherical_p'
stewart.joints_M.type = 4;
case 'flexible'
stewart.joints_M.type = 5;
case 'universal_3dof'
stewart.joints_M.type = 6;
case 'spherical_3dof'
stewart.joints_M.type = 7;
end
#+end_src
**** Add Stiffness and Damping in Translation of each strut
Axial and Radial (shear) Stiffness
#+begin_src matlab
stewart.joints_M.Ka = args.Ka_M;
stewart.joints_M.Kr = args.Kr_M;
stewart.joints_F.Ka = args.Ka_F;
stewart.joints_F.Kr = args.Kr_F;
#+end_src
Translation Damping
#+begin_src matlab
stewart.joints_M.Ca = args.Ca_M;
stewart.joints_M.Cr = args.Cr_M;
stewart.joints_F.Ca = args.Ca_F;
stewart.joints_F.Cr = args.Cr_F;
#+end_src
**** Add Stiffness and Damping in Rotation of each strut
Rotational Stiffness
#+begin_src matlab
stewart.joints_M.Kf = args.Kf_M;
stewart.joints_M.Kt = args.Kt_M;
stewart.joints_F.Kf = args.Kf_F;
stewart.joints_F.Kt = args.Kt_F;
#+end_src
Rotational Damping
#+begin_src matlab
stewart.joints_M.Cf = args.Cf_M;
stewart.joints_M.Ct = args.Ct_M;
stewart.joints_F.Cf = args.Cf_F;
stewart.joints_F.Ct = args.Ct_F;
#+end_src
**** Stiffness and Mass matrices for flexible joint
#+begin_src matlab
stewart.joints_F.M = args.M_F;
stewart.joints_F.K = args.K_F;
stewart.joints_F.n_xyz = args.n_xyz_F;
stewart.joints_F.xi = args.xi_F;
stewart.joints_F.xi = args.xi_F;
stewart.joints_F.step_file = args.step_file_F;
stewart.joints_M.M = args.M_M;
stewart.joints_M.K = args.K_M;
stewart.joints_M.n_xyz = args.n_xyz_M;
stewart.joints_M.xi = args.xi_M;
stewart.joints_M.step_file = args.step_file_M;
#+end_src
*** =initializeStewartPose=: Determine the initial stroke in each leg to have the wanted pose
:PROPERTIES:
:header-args:matlab+: :tangle matlab/src/initializeStewartPose.m
:header-args:matlab+: :comments none :mkdirp yes :eval no
:END:
**** Function description
#+begin_src matlab
function [stewart] = initializeStewartPose(stewart, args)
% initializeStewartPose - Determine the initial stroke in each leg to have the wanted pose
% It uses the inverse kinematic
%
% Syntax: [stewart] = initializeStewartPose(stewart, args)
%
% Inputs:
% - stewart - A structure with the following fields
% - Aa [3x6] - The positions ai expressed in {A}
% - Bb [3x6] - The positions bi expressed in {B}
% - args - Can have the following fields:
% - AP [3x1] - The wanted position of {B} with respect to {A}
% - ARB [3x3] - The rotation matrix that gives the wanted orientation of {B} with respect to {A}
%
% Outputs:
% - stewart - updated Stewart structure with the added fields:
% - actuators.Leq [6x1] - The 6 needed displacement of the struts from the initial position in [m] to have the wanted pose of {B} w.r.t. {A}
#+end_src
**** Optional Parameters
#+begin_src matlab
arguments
stewart
args.AP (3,1) double {mustBeNumeric} = zeros(3,1)
args.ARB (3,3) double {mustBeNumeric} = eye(3)
end
#+end_src
**** Use the Inverse Kinematic function
#+begin_src matlab
[Li, dLi] = inverseKinematics(stewart, 'AP', args.AP, 'ARB', args.ARB);
#+end_src
**** Populate the =stewart= structure
#+begin_src matlab
stewart.actuators.Leq = dLi;
#+end_src
*** =computeJacobian=: Compute the Jacobian Matrix
:PROPERTIES:
:header-args:matlab+: :tangle matlab/src/computeJacobian.m
:header-args:matlab+: :comments none :mkdirp yes :eval no
:END:
**** Function description
#+begin_src matlab
function [stewart] = computeJacobian(stewart)
% computeJacobian -
%
% Syntax: [stewart] = computeJacobian(stewart)
%
% Inputs:
% - stewart - With at least the following fields:
% - geometry.As [3x6] - The 6 unit vectors for each strut expressed in {A}
% - geometry.Ab [3x6] - The 6 position of the joints bi expressed in {A}
% - actuators.K [6x1] - Total stiffness of the actuators
%
% Outputs:
% - stewart - With the 3 added field:
% - kinematics.J [6x6] - The Jacobian Matrix
% - kinematics.K [6x6] - The Stiffness Matrix
% - kinematics.C [6x6] - The Compliance Matrix
#+end_src
**** Check the =stewart= structure elements
#+begin_src matlab
assert(isfield(stewart.geometry, 'As'), 'stewart.geometry should have attribute As')
As = stewart.geometry.As;
assert(isfield(stewart.geometry, 'Ab'), 'stewart.geometry should have attribute Ab')
Ab = stewart.geometry.Ab;
assert(isfield(stewart.actuators, 'K'), 'stewart.actuators should have attribute K')
Ki = stewart.actuators.K;
#+end_src
**** Compute Jacobian Matrix
#+begin_src matlab
J = [As' , cross(Ab, As)'];
#+end_src
**** Compute Stiffness Matrix
#+begin_src matlab
K = J'*diag(Ki)*J;
#+end_src
**** Compute Compliance Matrix
#+begin_src matlab
C = inv(K);
#+end_src
**** Populate the =stewart= structure
#+begin_src matlab
stewart.kinematics.J = J;
stewart.kinematics.K = K;
stewart.kinematics.C = C;
#+end_src
*** =initializeInertialSensor=: Initialize the inertial sensor in each strut
:PROPERTIES:
:header-args:matlab+: :tangle matlab/src/initializeInertialSensor.m
:header-args:matlab+: :comments none :mkdirp yes :eval no
:END:
**** Geophone - Working Principle
From the schematic of the Z-axis geophone shown in Figure ref:fig:z_axis_geophone, we can write the transfer function from the support velocity $\dot{w}$ to the relative velocity of the inertial mass $\dot{d}$:
\[ \frac{\dot{d}}{\dot{w}} = \frac{-\frac{s^2}{{\omega_0}^2}}{\frac{s^2}{{\omega_0}^2} + 2 \xi \frac{s}{\omega_0} + 1} \]
with:
- $\omega_0 = \sqrt{\frac{k}{m}}$
- $\xi = \frac{1}{2} \sqrt{\frac{m}{k}}$
#+name : fig:z_axis_geophone
#+caption : Schematic of a Z-Axis geophone
[[file:figs/inertial_sensor.png ]]
We see that at frequencies above $\omega_0$:
\[ \frac{\dot{d}}{\dot{w}} \approx -1 \]
And thus, the measurement of the relative velocity of the mass with respect to its support gives the absolute velocity of the support.
We generally want to have the smallest resonant frequency $\omega_0$ to measure low frequency absolute velocity, however there is a trade-off between $\omega_0$ and the mass of the inertial mass.
**** Accelerometer - Working Principle
From the schematic of the Z-axis accelerometer shown in Figure ref:fig:z_axis_accelerometer, we can write the transfer function from the support acceleration $\ddot{w}$ to the relative position of the inertial mass $d$:
\[ \frac{d}{\ddot{w}} = \frac{-\frac{1}{{\omega_0}^2}}{\frac{s^2}{{\omega_0}^2} + 2 \xi \frac{s}{\omega_0} + 1} \]
with:
- $\omega_0 = \sqrt{\frac{k}{m}}$
- $\xi = \frac{1}{2} \sqrt{\frac{m}{k}}$
#+name : fig:z_axis_accelerometer
#+caption : Schematic of a Z-Axis geophone
[[file:figs/inertial_sensor.png ]]
We see that at frequencies below $\omega_0$:
\[ \frac{d}{\ddot{w}} \approx -\frac{1}{{\omega_0}^2} \]
And thus, the measurement of the relative displacement of the mass with respect to its support gives the absolute acceleration of the support.
Note that there is trade-off between:
- the highest measurable acceleration $\omega_0$
- the sensitivity of the accelerometer which is equal to $-\frac{1}{{\omega_0}^2}$
**** Function description
#+begin_src matlab
function [stewart] = initializeInertialSensor(stewart, args)
% initializeInertialSensor - Initialize the inertial sensor in each strut
%
% Syntax: [stewart] = initializeInertialSensor(args)
%
% Inputs:
% - args - Structure with the following fields:
% - type - 'geophone', 'accelerometer', 'none'
% - mass [1x1] - Weight of the inertial mass [kg]
% - freq [1x1] - Cutoff frequency [Hz]
%
% Outputs:
% - stewart - updated Stewart structure with the added fields:
% - stewart.sensors.inertial
% - type - 1 (geophone), 2 (accelerometer), 3 (none)
% - K [1x1] - Stiffness [N/m]
% - C [1x1] - Damping [N/(m/s)]
% - M [1x1] - Inertial Mass [kg]
% - G [1x1] - Gain
#+end_src
**** Optional Parameters
#+begin_src matlab
arguments
stewart
args.type char {mustBeMember(args.type,{'geophone', 'accelerometer', 'none'})} = 'none'
args.mass (1,1) double {mustBeNumeric, mustBeNonnegative} = 1e-2
args.freq (1,1) double {mustBeNumeric, mustBeNonnegative} = 1e3
end
#+end_src
**** Compute the properties of the sensor
#+begin_src matlab
sensor = struct();
switch args.type
case 'geophone'
sensor.type = 1;
sensor.M = args.mass;
sensor.K = sensor.M * (2*pi*args.freq)^2;
sensor.C = 2*sqrt(sensor.M * sensor.K);
case 'accelerometer'
sensor.type = 2;
sensor.M = args.mass;
sensor.K = sensor.M * (2*pi*args.freq)^2;
sensor.C = 2*sqrt(sensor.M * sensor.K);
sensor.G = -sensor.K/sensor.M;
case 'none'
sensor.type = 3;
end
#+end_src
**** Populate the =stewart= structure
#+begin_src matlab
stewart.sensors.inertial = sensor;
#+end_src
*** =inverseKinematics=: Compute Inverse Kinematics
:PROPERTIES:
:header-args:matlab+: :tangle matlab/src/inverseKinematics.m
:header-args:matlab+: :comments none :mkdirp yes :eval no
:END:
**** Theory
2024-11-15 18:15:13 +01:00
For inverse kinematic analysis, it is assumed that the position ${}^A\bm{P}$ and orientation of the moving platform ${}^A\bm{R}_B$ are given and the problem is to obtain the joint variables, namely, $\bm{L} = [l_1, l_2, \dots, l_6]^T$.
2024-11-13 17:11:40 +01:00
From the geometry of the manipulator, the loop closure for each limb, $i = 1, 2, \dots, 6$ can be written as
\begin{align*}
2024-11-15 18:15:13 +01:00
l_i {}^A\hat{\bm{s}}_i &= {}^A\bm{A} + {}^A\bm{b}_i - {}^A\bm{a}_i \\
&= {}^A\bm{A} + {}^A\bm{R}_b {}^B\bm{b}_i - {}^A\bm{a}_i
2024-11-13 17:11:40 +01:00
\end{align*}
2024-11-15 18:15:13 +01:00
To obtain the length of each actuator and eliminate $\hat{\bm{s}}_i$, it is sufficient to dot multiply each side by itself:
2024-11-13 17:11:40 +01:00
\begin{equation}
2024-11-15 18:15:13 +01:00
l_i^2 \left[ {}^A\hat{\bm{s}}_i^T {}^A\hat{\bm{s}}_i \right] = \left[ {}^A\bm{P} + {}^A\bm{R}_B {}^B\bm{b}_i - {}^A\bm{a}_i \right]^T \left[ {}^A\bm{P} + {}^A\bm{R}_B {}^B\bm{b}_i - {}^A\bm{a}_i \right]
2024-11-13 17:11:40 +01:00
\end{equation}
Hence, for $i = 1, 2, \dots, 6$, each limb length can be uniquely determined by:
\begin{equation}
2024-11-15 18:15:13 +01:00
l_i = \sqrt{{}^A\bm{P}^T {}^A\bm{P} + {}^B\bm{b}_i^T {}^B\bm{b}_i + {}^A\bm{a}_i^T {}^A\bm{a}_i - 2 {}^A\bm{P}^T {}^A\bm{a}_i + 2 {}^A\bm{P}^T \left[{}^A\bm{R}_B {}^B\bm{b}_i\right] - 2 \left[{}^A\bm{R}_B {}^B\bm{b}_i\right]^T {}^A\bm{a}_i}
2024-11-13 17:11:40 +01:00
\end{equation}
If the position and orientation of the moving platform lie in the feasible workspace of the manipulator, one unique solution to the limb length is determined by the above equation.
Otherwise, when the limbs' lengths derived yield complex numbers, then the position or orientation of the moving platform is not reachable.
**** Function description
#+begin_src matlab
function [Li, dLi] = inverseKinematics(stewart, args)
% inverseKinematics - Compute the needed length of each strut to have the wanted position and orientation of {B} with respect to {A}
%
% Syntax: [stewart] = inverseKinematics(stewart)
%
% Inputs:
% - stewart - A structure with the following fields
% - geometry.Aa [3x6] - The positions ai expressed in {A}
% - geometry.Bb [3x6] - The positions bi expressed in {B}
% - geometry.l [6x1] - Length of each strut
% - args - Can have the following fields:
% - AP [3x1] - The wanted position of {B} with respect to {A}
% - ARB [3x3] - The rotation matrix that gives the wanted orientation of {B} with respect to {A}
%
% Outputs:
% - Li [6x1] - The 6 needed length of the struts in [m] to have the wanted pose of {B} w.r.t. {A}
% - dLi [6x1] - The 6 needed displacement of the struts from the initial position in [m] to have the wanted pose of {B} w.r.t. {A}
#+end_src
**** Optional Parameters
#+begin_src matlab
arguments
stewart
args.AP (3,1) double {mustBeNumeric} = zeros(3,1)
args.ARB (3,3) double {mustBeNumeric} = eye(3)
end
#+end_src
**** Check the =stewart= structure elements
#+begin_src matlab
assert(isfield(stewart.geometry, 'Aa'), 'stewart.geometry should have attribute Aa')
Aa = stewart.geometry.Aa;
assert(isfield(stewart.geometry, 'Bb'), 'stewart.geometry should have attribute Bb')
Bb = stewart.geometry.Bb;
assert(isfield(stewart.geometry, 'l'), 'stewart.geometry should have attribute l')
l = stewart.geometry.l;
#+end_src
**** Compute
#+begin_src matlab
Li = sqrt(args.AP'*args.AP + diag(Bb'*Bb) + diag(Aa'*Aa) - (2*args.AP'*Aa)' + (2*args.AP'* (args.ARB*Bb))' - diag(2* (args.ARB*Bb)'*Aa));
#+end_src
#+begin_src matlab
dLi = Li-l;
#+end_src
2024-11-13 10:27:34 +01:00
* Footnotes
2025-02-01 09:45:01 +01:00
[fn:6]The roundness of the spheres is specified at $50\,nm$
2025-01-31 16:33:25 +01:00
[fn:5]The "IcePAP" [[cite:&janvier13_icepap ]] which is developed at the ESRF
2025-02-01 18:35:04 +01:00
[fn:4]Note that the eccentricity of the "point of interest" with respect to the Spindle rotation axis has been tuned based on measurements.
2024-11-15 18:15:13 +01:00
[fn:3]The "PEPU" [[cite:&hino18_posit_encod_proces_unit ]] was used for digital protocol conversion between the interferometers and the Speedgoat
2024-11-13 10:27:34 +01:00
[fn:2]M12/F40 model from Attocube
[fn:1]Depending on the measuring range, gap can range from $\approx 1\,\mu m$ to $\approx 100\,\mu m$