Align the centers of rotation of Ry, Hexa, NASS with the sample
This commit is contained in:
@@ -9,3 +9,7 @@ runSimulation('vc', 'light', 'cl', 'none');
|
|||||||
runSimulation('pz', 'light', 'cl', 'none');
|
runSimulation('pz', 'light', 'cl', 'none');
|
||||||
% runSimulation('vc', 'heavy', 'cl', 'none');
|
% runSimulation('vc', 'heavy', 'cl', 'none');
|
||||||
% runSimulation('pz', 'heavy', 'cl', 'none');
|
% runSimulation('pz', 'heavy', 'cl', 'none');
|
||||||
|
|
||||||
|
%%
|
||||||
|
opts_inputs = struct('Dw', true);
|
||||||
|
initializeInputs(opts_inputs);
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ initializeSample(struct('mass', 1));
|
|||||||
|
|
||||||
initializeNanoHexapod(struct('actuator', 'lorentz'));
|
initializeNanoHexapod(struct('actuator', 'lorentz'));
|
||||||
K = K_light_vc; %#ok
|
K = K_light_vc; %#ok
|
||||||
save('./mat/controller.mat', 'K');
|
save('./mat/controllers.mat', 'K');
|
||||||
Gd_cl_light_vc = identifyPlant();
|
Gd_cl_light_vc = identifyPlant();
|
||||||
|
|
||||||
initializeNanoHexapod(struct('actuator', 'piezo'));
|
initializeNanoHexapod(struct('actuator', 'piezo'));
|
||||||
K = K_light_pz; %#ok
|
K = K_light_pz; %#ok
|
||||||
save('./mat/controller.mat', 'K');
|
save('./mat/controllers.mat', 'K');
|
||||||
Gd_cl_light_pz = identifyPlant();
|
Gd_cl_light_pz = identifyPlant();
|
||||||
|
|
||||||
%% Closed Loop - Heavy Sample
|
%% Closed Loop - Heavy Sample
|
||||||
@@ -22,12 +22,12 @@ initializeSample(struct('mass', 50));
|
|||||||
|
|
||||||
initializeNanoHexapod(struct('actuator', 'lorentz'));
|
initializeNanoHexapod(struct('actuator', 'lorentz'));
|
||||||
K = K_heavy_vc; %#ok
|
K = K_heavy_vc; %#ok
|
||||||
save('./mat/controller.mat', 'K');
|
save('./mat/controllers.mat', 'K');
|
||||||
G_cl_heavy_vc = identifyPlant();
|
G_cl_heavy_vc = identifyPlant();
|
||||||
|
|
||||||
initializeNanoHexapod(struct('actuator', 'piezo'));
|
initializeNanoHexapod(struct('actuator', 'piezo'));
|
||||||
K = K_heavy_pz;
|
K = K_heavy_pz;
|
||||||
save('./mat/controller.mat', 'K');
|
save('./mat/controllers.mat', 'K');
|
||||||
G_cl_heavy_pz = identifyPlant();
|
G_cl_heavy_pz = identifyPlant();
|
||||||
|
|
||||||
%% Save the identified transfer functions
|
%% Save the identified transfer functions
|
||||||
|
|||||||
@@ -5,13 +5,53 @@ clear; close all; clc;
|
|||||||
load('./mat/G.mat', 'G_light_vc', 'G_light_pz', 'G_heavy_vc', 'G_heavy_pz');
|
load('./mat/G.mat', 'G_light_vc', 'G_light_pz', 'G_heavy_vc', 'G_heavy_pz');
|
||||||
|
|
||||||
%%
|
%%
|
||||||
fs = 10;
|
s = tf('s');
|
||||||
|
|
||||||
K_light_vc = generateDiagPidControl(G_light_vc.G_cart, fs);
|
%%
|
||||||
K_light_pz = generateDiagPidControl(G_light_pz.G_cart, fs);
|
% bodeFig({minreal(G_light_vc.G_cart(1, 1))}, struct('phase', true));
|
||||||
|
|
||||||
K_heavy_vc = generateDiagPidControl(G_heavy_vc.G_cart, fs);
|
%%
|
||||||
K_heavy_pz = generateDiagPidControl(G_heavy_pz.G_cart, fs);
|
% sisotool(minreal(G_light_vc.G_cart(6, 6)))
|
||||||
|
|
||||||
|
K_light_vc = tf(zeros(6));
|
||||||
|
K_light_vc(1, 1) = 3.4802e06*(s+0.6983)*(s+131.6)/((s+5.306e-06)*(s+577.4));
|
||||||
|
K_light_vc(2, 2) = 5.3292e06*(s+0.6722)*(s+58.23)/((s+3.628e-06)*(s+734.6));
|
||||||
|
K_light_vc(3, 3) = 2.1727e06*(s+66.94)*(s+3.522)/((s+530.6)*(s+0.0006859));
|
||||||
|
K_light_vc(4, 4) = 6.4807e06*(s+2.095)*(s+111.1)/((s+0.0001246)*(s+577.4));
|
||||||
|
K_light_vc(5, 5) = 2.5184e06*(s+5.582)*(s+48.76)/((s+0.01558)*(s+453.8));
|
||||||
|
K_light_vc(6, 6) = 1.022e06*(s+6.152)*(s+40.15)/((s+0.001945)*(s+589.7));
|
||||||
|
|
||||||
|
%%
|
||||||
|
i = 6;
|
||||||
|
bodeFig({-G_light_vc.G_plant(i, i)*K_light_vc(i, i)}, struct('phase', true))
|
||||||
|
|
||||||
|
%%
|
||||||
|
% sisotool(minreal(G_light_pz.G_plant(1, 1)))
|
||||||
|
|
||||||
|
K_light_pz = tf(zeros(6));
|
||||||
|
% K_light_pz(1, 1) = 3.4802e06*(s+0.6983)*(s+131.6)/((s+5.306e-06)*(s+577.4));
|
||||||
|
% K_light_pz(2, 2) = 5.3292e06*(s+0.6722)*(s+58.23)/((s+3.628e-06)*(s+734.6));
|
||||||
|
% K_light_pz(3, 3) = 2.1727e06*(s+66.94)*(s+3.522)/((s+530.6)*(s+0.0006859));
|
||||||
|
% K_light_pz(4, 4) = 6.4807e06*(s+2.095)*(s+111.1)/((s+0.0001246)*(s+577.4));
|
||||||
|
% K_light_pz(5, 5) = 2.5184e06*(s+5.582)*(s+48.76)/((s+0.01558)*(s+453.8));
|
||||||
|
% K_light_pz(6, 6) = 1.022e06*(s+6.152)*(s+40.15)/((s+0.001945)*(s+589.7));
|
||||||
|
|
||||||
|
%%
|
||||||
|
K_heavy_vc = tf(zeros(6));
|
||||||
|
K_heavy_pz = tf(zeros(6));
|
||||||
|
|
||||||
|
|
||||||
|
%%
|
||||||
|
save('./mat/K_fb.mat', 'K_light_vc', 'K_light_pz', 'K_heavy_vc', 'K_heavy_pz');
|
||||||
|
|
||||||
|
%% Automatic generation of controllers
|
||||||
|
% fs = 100;
|
||||||
|
%
|
||||||
|
% K_light_vc = generateDiagPidControl(G_light_vc.G_cart, fs);
|
||||||
|
% K_light_pz = generateDiagPidControl(G_light_pz.G_cart, fs);
|
||||||
|
%
|
||||||
|
% K_heavy_vc = generateDiagPidControl(G_heavy_vc.G_cart, fs);
|
||||||
|
% K_heavy_pz = generateDiagPidControl(G_heavy_pz.G_cart, fs);
|
||||||
|
|
||||||
%% Save the MIMO control
|
%% Save the MIMO control
|
||||||
save('./mat/K_fb.mat', 'K_light_vc', 'K_light_pz', 'K_heavy_vc', 'K_heavy_pz');
|
% save('./mat/K_fb.mat', 'K_light_vc', 'K_light_pz', 'K_heavy_vc', 'K_heavy_pz');
|
||||||
|
|||||||
@@ -58,11 +58,11 @@ mass((T_mass_start+3)/sim_conf.Ts:(T_mass_start+5)/sim_conf.Ts, 1) = mass((T_mas
|
|||||||
mass((T_mass_start+3)/sim_conf.Ts:(T_mass_start+5)/sim_conf.Ts, 2) = mass((T_mass_start+2)/sim_conf.Ts, 2)-2*pi*(-10/360)*(time_vector((T_mass_start+3)/sim_conf.Ts:(T_mass_start+5)/sim_conf.Ts)-time_vector((T_mass_start+3)/sim_conf.Ts));
|
mass((T_mass_start+3)/sim_conf.Ts:(T_mass_start+5)/sim_conf.Ts, 2) = mass((T_mass_start+2)/sim_conf.Ts, 2)-2*pi*(-10/360)*(time_vector((T_mass_start+3)/sim_conf.Ts:(T_mass_start+5)/sim_conf.Ts)-time_vector((T_mass_start+3)/sim_conf.Ts));
|
||||||
|
|
||||||
opts_inputs = struct(...
|
opts_inputs = struct(...
|
||||||
'ty', ty, ...
|
'Dy', ty, ...
|
||||||
'ry', ry, ...
|
'Ry', ry, ...
|
||||||
'rz', rz, ...
|
'Rz', rz, ...
|
||||||
'u_hexa', u_hexa, ...
|
'Dh', u_hexa, ...
|
||||||
'mass', mass ...
|
'Rm', mass ...
|
||||||
);
|
);
|
||||||
|
|
||||||
initializeInputs(opts_inputs);
|
initializeInputs(opts_inputs);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ clear; close all; clc;
|
|||||||
|
|
||||||
%% Initialize simulation configuration
|
%% Initialize simulation configuration
|
||||||
opts_sim = struct(...
|
opts_sim = struct(...
|
||||||
'Tsim', 0.1 ...
|
'Tsim', 0.001 ...
|
||||||
);
|
);
|
||||||
|
|
||||||
initializeSimConf(opts_sim);
|
initializeSimConf(opts_sim);
|
||||||
@@ -14,14 +14,14 @@ load('./mat/sim_conf.mat', 'sim_conf')
|
|||||||
time_vector = 0:sim_conf.Ts:sim_conf.Tsim;
|
time_vector = 0:sim_conf.Ts:sim_conf.Tsim;
|
||||||
|
|
||||||
% Translation Stage
|
% Translation Stage
|
||||||
Ty = 0.20*ones(length(time_vector), 1);
|
Ty = 0*ones(length(time_vector), 1);
|
||||||
|
|
||||||
% Tilt Stage
|
% Tilt Stage
|
||||||
Ry = 2*pi*(3/360)*ones(length(time_vector), 1);
|
Ry = 2*pi*(0/360)*ones(length(time_vector), 1);
|
||||||
% Ry = 2*pi*(3/360)*sin(2*pi*time_vector);
|
% Ry = 2*pi*(3/360)*sin(2*pi*time_vector);
|
||||||
|
|
||||||
% Spindle
|
% Spindle
|
||||||
Rz = 2*pi*3*(time_vector);
|
Rz = 2*pi*0*(time_vector);
|
||||||
% Rz = 2*pi*(190/360)*ones(length(time_vector), 1);
|
% Rz = 2*pi*(190/360)*ones(length(time_vector), 1);
|
||||||
|
|
||||||
% Micro Hexapod
|
% Micro Hexapod
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -8,18 +8,20 @@ function [] = initializeExperiment(exp_name, sys_mass)
|
|||||||
|
|
||||||
if strcmp(sys_mass, 'light')
|
if strcmp(sys_mass, 'light')
|
||||||
opts_inputs = struct(...
|
opts_inputs = struct(...
|
||||||
'ground_motion', true, ...
|
'Dw', true, ...
|
||||||
'rz', 60 ... % rpm
|
'Rz', 60 ... % rpm
|
||||||
);
|
);
|
||||||
elseif strcpm(sys_mass, 'heavy')
|
elseif strcpm(sys_mass, 'heavy')
|
||||||
opts_inputs = struct(...
|
opts_inputs = struct(...
|
||||||
'ground_motion', true, ...
|
'Dw', true, ...
|
||||||
'rz', 1 ... % rpm
|
'Rz', 1 ... % rpm
|
||||||
);
|
);
|
||||||
else
|
else
|
||||||
error('sys_mass should be light or heavy');
|
error('sys_mass should be light or heavy');
|
||||||
end
|
end
|
||||||
|
|
||||||
initializeInputs(opts_inputs);
|
initializeInputs(opts_inputs);
|
||||||
elseif
|
else
|
||||||
|
error('exp_name is only configured for tomography');
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ function [granite] = initializeGranite()
|
|||||||
granite.k.z = 1e8; % [N/m]
|
granite.k.z = 1e8; % [N/m]
|
||||||
granite.c.z = 1e4; % [N/(m/s)]
|
granite.c.z = 1e4; % [N/(m/s)]
|
||||||
|
|
||||||
|
%% Positioning parameters
|
||||||
|
granite.sample_pos = 0.8; % Z-offset for the initial position of the sample [m]
|
||||||
|
|
||||||
%% Save
|
%% Save
|
||||||
save('./mat/stages.mat', 'granite', '-append');
|
save('./mat/stages.mat', 'granite', '-append');
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ function [inputs] = initializeInputs(opts_param)
|
|||||||
elseif islogical(opts.Rz) && opts.Rz == false
|
elseif islogical(opts.Rz) && opts.Rz == false
|
||||||
Rz = zeros(length(t), 1);
|
Rz = zeros(length(t), 1);
|
||||||
elseif isnumeric(opts.Rz) && length(opts.Rz) == 1
|
elseif isnumeric(opts.Rz) && length(opts.Rz) == 1
|
||||||
Rz = opts.Rz*(2*pi/360)*ones(length(t), 1);
|
Rz = opts.Rz*(2*pi/60)*t;
|
||||||
else
|
else
|
||||||
Rz = opts.Rz;
|
Rz = opts.Rz;
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ function [micro_hexapod] = initializeMicroHexapod(opts_param)
|
|||||||
%% Stewart Object
|
%% Stewart Object
|
||||||
micro_hexapod = struct();
|
micro_hexapod = struct();
|
||||||
micro_hexapod.h = 350; % Total height of the platform [mm]
|
micro_hexapod.h = 350; % Total height of the platform [mm]
|
||||||
micro_hexapod.jacobian = 265; % Distance from the top platform to the Jacobian point [mm]
|
% micro_hexapod.jacobian = 269.26; % Distance from the top platform to the Jacobian point [mm]
|
||||||
|
micro_hexapod.jacobian = 270; % Distance from the top platform to the Jacobian point [mm]
|
||||||
|
|
||||||
%% Bottom Plate - Mechanical Design
|
%% Bottom Plate - Mechanical Design
|
||||||
BP = struct();
|
BP = struct();
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ function [nano_hexapod] = initializeNanoHexapod(opts_param)
|
|||||||
nano_hexapod = struct();
|
nano_hexapod = struct();
|
||||||
nano_hexapod.h = 90; % Total height of the platform [mm]
|
nano_hexapod.h = 90; % Total height of the platform [mm]
|
||||||
nano_hexapod.jacobian = 175; % Point where the Jacobian is computed => Center of rotation [mm]
|
nano_hexapod.jacobian = 175; % Point where the Jacobian is computed => Center of rotation [mm]
|
||||||
|
% nano_hexapod.jacobian = 174.26; % Point where the Jacobian is computed => Center of rotation [mm]
|
||||||
|
|
||||||
%% Bottom Plate
|
%% Bottom Plate
|
||||||
BP = struct();
|
BP = struct();
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ function [ry] = initializeRy(opts_param)
|
|||||||
ry.c.rad = 10*(1/5)*sqrt(ry.k.rad/ry.m);
|
ry.c.rad = 10*(1/5)*sqrt(ry.k.rad/ry.m);
|
||||||
ry.c.rrad = 10*(1/5)*sqrt(ry.k.rrad/ry.m);
|
ry.c.rrad = 10*(1/5)*sqrt(ry.k.rrad/ry.m);
|
||||||
ry.c.tilt = 10*(1/1)*sqrt(ry.k.tilt/ry.m);
|
ry.c.tilt = 10*(1/1)*sqrt(ry.k.tilt/ry.m);
|
||||||
|
|
||||||
|
%% Positioning parameters
|
||||||
|
ry.z_offset = 0.58178; % Z-Offset so that the center of rotation matches the sample center [m]
|
||||||
|
|
||||||
%% Save
|
%% Save
|
||||||
save('./mat/stages.mat', 'ry', '-append');
|
save('./mat/stages.mat', 'ry', '-append');
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -7,7 +7,7 @@ function [K] = generateDiagPidControl(G, fs)
|
|||||||
%%
|
%%
|
||||||
K = tf(zeros(6));
|
K = tf(zeros(6));
|
||||||
|
|
||||||
for i = 1:5
|
for i = 1:6
|
||||||
input_name = G.InputName(i);
|
input_name = G.InputName(i);
|
||||||
output_name = G.OutputName(i);
|
output_name = G.OutputName(i);
|
||||||
K(i, i) = tf(pidtune(minreal(G(output_name, input_name)), 'PIDF', 2*pi*fs, pid_opts));
|
K(i, i) = tf(pidtune(minreal(G(output_name, input_name)), 'PIDF', 2*pi*fs, pid_opts));
|
||||||
|
|||||||
+5
-1
@@ -24,6 +24,7 @@ function [sys] = identifyPlant(opts_param)
|
|||||||
io(5) = linio([mdl, '/Dsm'], 1, 'output'); % Displacement of the sample
|
io(5) = linio([mdl, '/Dsm'], 1, 'output'); % Displacement of the sample
|
||||||
io(6) = linio([mdl, '/Fnlm'], 1, 'output'); % Force sensor in NASS's legs
|
io(6) = linio([mdl, '/Fnlm'], 1, 'output'); % Force sensor in NASS's legs
|
||||||
io(7) = linio([mdl, '/Dnlm'], 1, 'output'); % Displacement of NASS's legs
|
io(7) = linio([mdl, '/Dnlm'], 1, 'output'); % Displacement of NASS's legs
|
||||||
|
io(8) = linio([mdl, '/Es'], 1, 'output'); % Position Error w.r.t. NASS base
|
||||||
|
|
||||||
%% Run the linearization
|
%% Run the linearization
|
||||||
G = linearize(mdl, io, 0);
|
G = linearize(mdl, io, 0);
|
||||||
@@ -33,7 +34,8 @@ function [sys] = identifyPlant(opts_param)
|
|||||||
'F1', 'F2', 'F3', 'F4', 'F5', 'F6'};
|
'F1', 'F2', 'F3', 'F4', 'F5', 'F6'};
|
||||||
G.OutputName = {'Dx', 'Dy', 'Dz', 'Rx', 'Ry', 'Rz', ...
|
G.OutputName = {'Dx', 'Dy', 'Dz', 'Rx', 'Ry', 'Rz', ...
|
||||||
'Fm1', 'Fm2', 'Fm3', 'Fm4', 'Fm5', 'Fm6', ...
|
'Fm1', 'Fm2', 'Fm3', 'Fm4', 'Fm5', 'Fm6', ...
|
||||||
'Dm1', 'Dm2', 'Dm3', 'Dm4', 'Dm5', 'Dm6'};
|
'Dm1', 'Dm2', 'Dm3', 'Dm4', 'Dm5', 'Dm6', ...
|
||||||
|
'Edx', 'Rdy', 'Edz', 'Erx', 'Ery', 'Erz'};
|
||||||
|
|
||||||
%% Create the sub transfer functions
|
%% Create the sub transfer functions
|
||||||
% From forces applied in the cartesian frame to displacement of the sample in the cartesian frame
|
% From forces applied in the cartesian frame to displacement of the sample in the cartesian frame
|
||||||
@@ -46,4 +48,6 @@ function [sys] = identifyPlant(opts_param)
|
|||||||
sys.G_iff = minreal(G({'Fm1', 'Fm2', 'Fm3', 'Fm4', 'Fm5', 'Fm6'}, {'F1', 'F2', 'F3', 'F4', 'F5', 'F6'}));
|
sys.G_iff = minreal(G({'Fm1', 'Fm2', 'Fm3', 'Fm4', 'Fm5', 'Fm6'}, {'F1', 'F2', 'F3', 'F4', 'F5', 'F6'}));
|
||||||
% From forces applied on NASS's legs to displacement of each leg
|
% From forces applied on NASS's legs to displacement of each leg
|
||||||
sys.G_dleg = minreal(G({'Dm1', 'Dm2', 'Dm3', 'Dm4', 'Dm5', 'Dm6'}, {'F1', 'F2', 'F3', 'F4', 'F5', 'F6'}));
|
sys.G_dleg = minreal(G({'Dm1', 'Dm2', 'Dm3', 'Dm4', 'Dm5', 'Dm6'}, {'F1', 'F2', 'F3', 'F4', 'F5', 'F6'}));
|
||||||
|
% From forces applied on NASS's legs to displacement of each leg
|
||||||
|
sys.G_plant = minreal(G({'Edx', 'Rdy', 'Edz', 'Erx', 'Ery', 'Erz'}, {'Fnx', 'Fny', 'Fnz', 'Mnx', 'Mny', 'Mnz'}));
|
||||||
end
|
end
|
||||||
|
|||||||
+4
-4
@@ -3,14 +3,14 @@ function [] = runSimulation(sys_name, sys_mass, ctrl_type, act_damp)
|
|||||||
if strcmp(ctrl_type, 'cl') && strcmp(act_damp, 'none')
|
if strcmp(ctrl_type, 'cl') && strcmp(act_damp, 'none')
|
||||||
K_obj = load('./mat/K_fb.mat');
|
K_obj = load('./mat/K_fb.mat');
|
||||||
K = K_obj.(sprintf('K_%s_%s', sys_mass, sys_name)); %#ok
|
K = K_obj.(sprintf('K_%s_%s', sys_mass, sys_name)); %#ok
|
||||||
save('./mat/controller.mat', 'K');
|
save('./mat/controllers.mat', 'K');
|
||||||
elseif strcmp(ctrl_type, 'cl') && strcmp(act_damp, 'iff')
|
elseif strcmp(ctrl_type, 'cl') && strcmp(act_damp, 'iff')
|
||||||
K_obj = load('./mat/K_fb_iff.mat');
|
K_obj = load('./mat/K_fb_iff.mat');
|
||||||
K = K_obj.(sprintf('K_%s_%s_iff', sys_mass, sys_name)); %#ok
|
K = K_obj.(sprintf('K_%s_%s_iff', sys_mass, sys_name)); %#ok
|
||||||
save('./mat/controller.mat', 'K');
|
save('./mat/controllers.mat', 'K');
|
||||||
elseif strcmp(ctrl_type, 'ol')
|
elseif strcmp(ctrl_type, 'ol')
|
||||||
K = tf(zeros(6)); %#ok
|
K = tf(zeros(6)); %#ok
|
||||||
save('./mat/controller.mat', 'K');
|
save('./mat/controllers.mat', 'K');
|
||||||
else
|
else
|
||||||
error('ctrl_type should be cl or ol');
|
error('ctrl_type should be cl or ol');
|
||||||
end
|
end
|
||||||
@@ -46,7 +46,7 @@ function [] = runSimulation(sys_name, sys_mass, ctrl_type, act_damp)
|
|||||||
sim('sim_nano_station_ctrl.slx');
|
sim('sim_nano_station_ctrl.slx');
|
||||||
|
|
||||||
%% Split the Dsample matrix into vectors
|
%% Split the Dsample matrix into vectors
|
||||||
[Dx, Dy, Dz, Rx, Ry, Rz] = matSplit(Dsample.Data, 1); %#ok
|
[Dx, Dy, Dz, Rx, Ry, Rz] = matSplit(Es.Data, 1); %#ok
|
||||||
time = Dsample.Time; %#ok
|
time = Dsample.Time; %#ok
|
||||||
|
|
||||||
%% Save the result
|
%% Save the result
|
||||||
|
|||||||
Reference in New Issue
Block a user