Master Thesis Code
by Simon Moser
Loading...
Searching...
No Matches
JumpSensor.m
Go to the documentation of this file.
1% =========================================================================== %
2%> @brief JumpSensor is a simulation of the JUMP sensor of the ZurichMove Project.
3%>
4%> JumpSensor simulates one JUMP sensor of the ZurichMove Project. The JUMP
5%> sensor is a wearable sensor which can be used time synchronized with other
6%> JUMP sensors to track the movement of a person.
7%>
8%> This class is a simulation of the JUMP sensor. It can be configured equally
9%> to the real JUMP sensor. It contains the models of the actual sensors of the
10%> JUMP sensor according to the datasheets of the sensors. The JUMP sensor
11%> simulation can be used to test the algorithms of the ZurichMove Project.
12%>
13%> The JUMP sensor is equipped with the following sensors:
14%> - MPU9250 9-axis IMU containing :
15%> - 3-axis accelerometer
16%> - 3-axis gyroscope
17%> - 3-axis magnetometer
18%> - ADXL375 3-axis accelerometer (high-g)
19%> - MS5611 barometer and temperature sensor
20%>
21%> @code
22%> % example code
23%> % create a JUMP sensor with default settings
24%> jump = JumpSensor();
25%>
26%> % generate true data (examples, use the \ref trajectoryGenerationApp.m for more realistic data)
27%> acc_true = zeros(1000,3); % laying still
28%> gyr_true = zeros(1000,3); % not moving
29%> ori_true = quaternion.ones(1000,1); % no rotation
30%>
31%> % generate sensor data
32%> data = jump.generate(acc_true, gyr_true, ori_true);
33%> @endcode
34%>
35%> see also the more detailed example in @ref jumpSensorExample.m
36%>
37%> @copyright see the file @ref LICENSE in the root directory of the repository
38% =========================================================================== %
39
41
42 properties (Access = private)
43 % default settings from JUMPconnect
44 accRange = 16 * 9.81; %> default acceleration range (m/s^2)
45 gyroRange = 2000; %> default gyroscope range (°/s)
46 sampleRate = 200; %> default sample rate of the MPU (Hz)
47 sampleRatePressure = 5; %> default sample rate of the pressure and temperature sensor (Hz)
48 sampleRateMagnetometer = 10; %> default sample rate of the magnetometer (Hz)
50 % sensors
51 mpu {mustBeA(mpu, 'imuSensor')} = imuSensor; %> model of the MPU9250 9-axis IMU
52 hig {mustBeA(hig, 'imuSensor')} = imuSensor; %> model of the ADXL375 3-axis accelerometer
54 %> @note the barometer is implemented directly in the generate function
56 end
57
58 methods (Access = public)
60 % ====================================================================== %
61 %> @brief JumpSensor is the constructor of the JUMP sensor.
62 %>
63 %> JumpSensor creates a JUMP sensor with the given settings. The
64 %> settings are the same as in the JUMPconnect software. The JUMP sensor
65 %> can be used to generate sensor data from true data.
66 %>
67 %> @param accRange (optional) - the acceleration range of the MPU9250 in g
68 %> @param gyroRange (optional) - the gyroscope range of the MPU9250 in °/s
69 %> @param sampleRate (optional) - the sample rate of the MPU9250 in Hz
70 %> @param sampleRatePressure (optional) - the sample rate of the pressure and temperature sensor in Hz
71 %>
72 %> @retval obj - the created JUMP sensor
73 % ====================================================================== %
74 function obj = JumpSensor(args)
75 arguments
76 args.accRange (1,1) double {mustBeMember(args.accRange, [2,4,8,16])} = 16;
77 args.gyroRange (1,1) double {mustBeMember(args.gyroRange, [250,500,1000,2000])} = 2000;
78 args.sampleRate (1,1) double {mustBeMember(args.sampleRate, [50,200])} = 200;
79 args.sampleRatePressure (1,1) double {mustBeMember(args.sampleRatePressure, [1,5])} = 5;
80 end
81
82 obj.accRange = args.accRange * 9.81; % convert to m/s^2
83 obj.gyroRange = args.gyroRange;
84 obj.sampleRate = args.sampleRate;
85 obj.sampleRatePressure = args.sampleRatePressure;
86
87 % make checks on the settings
88 if obj.gyroRange ~= 2000
89 warning("The JUMPconnect software only allows the setting 2000 °/s.");
90 end
91 if (obj.sampleRate == 50 && obj.sampleRatePressure == 5) || (obj.sampleRate == 200 && obj.sampleRatePressure == 1)
92 warning("The JUMPconnect software does not allow the combination of %i Hz sample rate and %i Hz pressure sensor sample rate.", obj.sampleRate, obj.sampleRatePressure);
93 end
94
95 % create the sensors
96 obj.createMpu();
97 obj.createHig();
98
99
100 end
101
102 % ====================================================================== %
103 %> @brief generate generates sensor data from true data.
104 %>
105 %> generate generates sensor data from true data with the defined sensor
106 %> models. The data format is identical to the data format of the parsing
107 %> function of the function JUMPREADDATA.
108 %>
109 %> @param acc_true (nx3 double) - the true accelerometer data in m/s^2
110 %> @param gyr_true (nx3 double) - the true gyroscope data in deg/s
111 %> @param ori_true (nx1 quaternion) - the true orientation data in quaternions
112 %> @param options.alt_true (nx1 double, optional) - the true altitude in m (default: 500 m)
113 %> @param options.tmp_true (nx1 double, optional) - the true temperature in °C (default: 25°C)
114 %> @param options.t_start (datetime, optional) - the start time of the sensor data (default: now)
115 %> @param options.simulate_mpu (logical, optional) - simulate the MPU9250 sensor (default: true)
116 %> @param options.simulate_hig (logical, optional) - simulate the ADXL375 sensor (default: true)
117 %> @param options.simulate_prs (logical, optional) - simulate the MS5611 sensor (default: true)
118 %> @param options.simulate_tmp (logical, optional) - simulate the temperature sensor (default: true)
119 %>
120 %> @retval data (struct) - the sensor data in the format of the parsing function of the function JUMPREADDATA
121 %> - data.tt (timetable) - timetable containing the sensor data
122 %> - data.ID (char) - sensor ID
123 %> - data.UID (char) - sensor UID
124 %> - data.name (char) - sensor name (fixed to 'JUMP model')
125 %> - data.sw_version (char) - sensor software version
126 %> - data.hw_version (char) - sensor hardware version
127 %> - data.acc_range (double) - accelerometer range
128 %> - data.gyro_range (double) - gyroscope range
129 %> - data.mpu_cal (double) - mpu calibration
130 %> - data.act_thrs (double) - activity threshold
131 %> - data.inact_dur (double) - inactivity duration
132 %> - data.mpu_rate (double) - mpu rate
133 %> - data.adxl_rate (double) - adxl rate
134 %> - data.pres_rate (double) - pressure rate
135 %> - data.magn_en (double) - magnetometer enabled
136 %> - data.sync (double) - sync mode
137 %> - data.ble_adress (char) - ble adress
138 %>
139 %> @note for whatever reason the MATLAB imuSensor class negates the
140 %> accelerometer data. I guess, because they missunderstand how the
141 %> gravitational force works. According to their algorithm description,
142 %> they negate the input acceleration and then add the gravity vector.
143 %> This is not the correct way to do it. The correct way is to negate the
144 %> gravity vector. Therefore, the input is negated and the gravity vector
145 %> is added two times before the imuSensor class is called. This is
146 %> a bug in the MATLAB imuSensor class. In ither words, we have the
147 %> actual acceleration \f$ a \f$. to patch the MATLAB bevavior, we do:
148 %> \f[ a_{patched} = -a + 2 \cdot [0,0,g], \f] before we call the
149 %> imuSensor class.then the imuSensor class does
150 %> \f[ totalAcc = -a_{input} + [0,0, g] = -(-a + 2 \cdot [0,0,g]) +
151 %> [0,0, +g] = a - [0,0,g], \f] which brings the correct result.
152 %>
153 %> @todo check the covariances of the sensors
154 %>
155 %> @todo check the axis misalignment and sensitivity over time.
156 % ====================================================================== %
157 function data = generate(obj, acc_true, gyr_true, ori_true, options)
158
159 % check input arguments
160 arguments
161 obj;
162 acc_true (:,3) double;
163 gyr_true (:,3) double;
164 ori_true (:,1) quaternion;
165 options.alt_true (:,1) double = repmat(500, length(acc_true), 1);
166 options.tmp_true (:,1) double = repmat(25, length(acc_true), 1);
167 options.t_start (1,1) datetime = datetime("now");
168 options.simulate_mpu (1,1) logical = true;
169 options.simulate_hig (1,1) logical = true;
170 options.simulate_prs (1,1) logical = true;
171 options.simulate_tmp (1,1) logical = true;
172 end
173
174 % unpack options
175 alt_true = options.alt_true;
176 tmp_true = options.tmp_true;
177 t_start = options.t_start;
178
179 % check if the input vectors have the same length
180 input_length = length(acc_true);
181 assert(length(gyr_true) == input_length, "The input vectors must have the same length.");
182 assert(length(ori_true) == input_length, "The input vectors must have the same length.");
183 assert(length(tmp_true) == input_length, "The input vectors must have the same length.");
184
185 % modify the acceleration because of a MATLAB bug
186 acc_true = -acc_true + 2*[0,0,9.81]; % negate the acceleration and add 2x the gravity vector
187
188 if options.simulate_mpu
189
190 % generate the sensor data
191 gyr_true = gyr_true * pi / 180; % convert to rad/s
192
193 % mpu
194 [acc_mpu, gyr_mpu, mag_mpu] = obj.mpu(acc_true, gyr_true, ori_true);
195 gyr_mpu = gyr_mpu * 180 / pi; % convert to deg/s
196
197 % nownsample and then upsample with previous value to get the same output as the real sensor
198 mag_mpu_ds = downsample(mag_mpu, obj.sampleRate / obj.sampleRateMagnetometer); % NOTE: the magnetometer is sampled at 10 Hz
199 mag_mpu = nan(input_length,3);
200 mag_mpu(1:obj.sampleRate/obj.sampleRateMagnetometer:end,:) = mag_mpu_ds;
201 mag_mpu = fillmissing(mag_mpu, 'previous');
202
203 else
204 acc_mpu = nan(input_length,3);
205 gyr_mpu = nan(input_length,3);
206 mag_mpu = nan(input_length,3);
207 end
208
209 if options.simulate_hig
210 % adxl
211 acc_adxl = obj.hig(acc_true, zeros(input_length,3), ori_true);
212 else
213 acc_adxl = nan(input_length,3);
214 end
215
216 if options.simulate_prs
217 % pressure
218 prs = obj.getPressureFromAltitude(alt_true, tmp_true);
219 else
220 prs = repmat(1013.25, input_length, 1);
221 end
222
223
224 if options.simulate_tmp
225 % temperature
226 tmp = obj.getSensorTemperature(tmp_true);
227 else
228 tmp = tmp_true;
229 end
230
231 % create timetable
232 t = repmat(t_start, input_length, 1) + ((0:input_length-1) .* seconds(1/obj.sampleRate))';
233 data.tt = timetable(t, acc_mpu, gyr_mpu, acc_adxl, mag_mpu, prs, tmp);
234 data.tt.Properties.VariableNames = {'acc', 'gyr', 'hig', 'mag', 'prs', 'tmp'};
235
236 % add sensor information
237 data.t_start = t_start;
238 data.ID = 'S-24-A-SIMUL';
239 data.UID = 'simulation';
240 data.name = 'JUMP simulation';
241 data.sw_version = '132000(s)';
242 data.hw_version = '1627390748(à)';
243 data.acc_range = round(obj.accRange / 9.81); % convert to g
244 data.gyro_range = round(obj.gyroRange * 180 / pi); % convert to deg/s
245 data.mpu_cal = 2;
246 data.act_thrs = 0;
247 data.inact_dur = 0;
248 data.mpu_rate = obj.sampleRate;
249 data.adxl_rate = obj.sampleRate;
250 data.pres_rate = obj.sampleRatePressure;
251 data.magn_en = 255;
252 data.sync = 1;
253 data.ble_address = '001122334455';
254
255 end
256
257 % ====================================================================== %
258 %> @brief generates data from a @ref Trajectory object.
259 %>
260 %> generateFromTrajectory generates sensor data from a @ref Trajectory
261 %> object. The sensor data is generated with the defined sensor models.
262 %>
263 %> @param obj (JumpSensor) - the JUMP sensor
264 %> @param traj (Trajectory) - the trajectory object
265 %>
266 %> @retval data (struct) - the sensor data in the format defined in @ref generate
267 % ====================================================================== %
268 function data = generateFromTrajectory(obj, traj)
269
270 % check input arguments
271 arguments
272 obj;
273 traj (1,1) Trajectory;
274 end
275
276 traj = traj.getTrajectory;
277
278 % generate the sensor data
279 data = obj.generate(traj.acc, traj.gyr, traj.ori);
280 end
281
282 % ====================================================================== %
283 %> @brief returns the parameters of the JUMP sensor.
284 %>
285 %> getParams returns the parameters of the JUMP sensor. The parameters
286 %> are used to generate the erroneous sensor data.
287 %>
288 %> @param obj (JumpSensor) - the JUMP sensor
289 %>
290 %> @retval params (struct) - the parameters of the JUMP sensor
291 %> - params.mpu.acc (struct) - the parameters of the MPU9250 accelerometer
292 %> - params.mpu.gyr (struct) - the parameters of the MPU9250 gyroscope
293 %> - params.mpu.mag (struct) - the parameters of the MPU9250 magnetometer
294 %> - params.hig.acc (struct) - the parameters of the ADXL375 accelerometer
295 % ====================================================================== %
296 function params = getParams(obj)
297
298 params.mpu.acc = obj.mpu.Accelerometer;
299 params.mpu.gyr = obj.mpu.Gyroscope;
300 params.mpu.mag = obj.mpu.Magnetometer;
301 params.hig.acc = obj.hig.Accelerometer;
302
303 end
304 end
305
306 methods (Access = private)
307
308 % ====================================================================== %
309 %> @brief createMpu creates the MPU9250 sensor.
310 %>
311 %> createMpu creates the MPU9250 sensor with the settings specified in
312 %> the properties of the JUMP sensor model.
313 %>
314 %> @param obj (JumpSensor) - the JUMP sensor
315 %>
316 %> @retval none
317 %>
318 %> @note in here the actual sensor is modeled with noise and bias. See the
319 %> actual code for the parameters and explanations.
320 % ====================================================================== %
321 function createMpu(obj)
322
323 obj.mpu = imuSensor('accel-gyro-mag', ... % sensor type
324 "ReferenceFrame", "NED", ... % reference frame for JUMP sensors
325 "SampleRate", obj.sampleRate, ... % sample rate
326 "Temperature", 25, ... % temperature (tunable)
327 "MagneticField", [21.8719 1.2697, 42.8197] ... % magnetic field at 46.8N, 8.23E, 500m (GPS) (NED, according to Magbnetic Field Estimated Values of NOAA)
328 );
329
330 % accelerometer parameters
331 obj.mpu.Accelerometer.MeasurementRange = obj.accRange * 9.81; % m/s^2
332 obj.mpu.Accelerometer.Resolution = obj.accRange / 32768; % (m/s^2) / LSB
333 obj.mpu.Accelerometer.ConstantBias = [obj.rant(0.06), obj.rant(0.06), obj.rant(0.08)] * 9.81; % (m/s^2) (datasheet: 60,60,80 mg)
334 % sensitivity is modeled into the misalignment matrix
335 obj.mpu.Accelerometer.AxesMisalignment = obj.getMisalignmentMatrix(2,2,2,3); % (percent cross-axis sensitivity, percent overall sensitivity error)
336 obj.mpu.Accelerometer.NoiseDensity = [obj.rant(0.0025,0.0035,2),obj.rant(0.0025,0.0035,2),obj.rant(0.0055,0.0065,2)]; % (m/s^2) / sqrt(Hz) (datasheet: 300 µg/rtHz, value from allan variance experiments)
337 obj.mpu.Accelerometer.BiasInstability = 6e-3; % (m/s^2) (missing in datasheet, value from allan variance experiments)
338 obj.mpu.Accelerometer.RandomWalk = 3.9e-5; % (m/s^2) * sqrt(Hz) (missing in datasheet, value from allan variance experiments)
339 obj.mpu.Accelerometer.TemperatureBias = [obj.rant(0.0015), obj.rant(0.0015), obj.rant(0.0015)] * 9.81; % (m/s^2) / °C (datasheet: 1.5 mg/°C)
340 obj.mpu.Accelerometer.TemperatureScaleFactor = abs(obj.rant(0.026)); % (%/°C) (datasheet: 0.026 %/°C)
341 obj.mpu.Accelerometer.NoiseType = "single-sided";
342
343 % gyroscope parameters
344 obj.mpu.Gyroscope.MeasurementRange = obj.gyroRange * pi/180; % rad/s
345 obj.mpu.Gyroscope.Resolution = (obj.gyroRange * pi/180) / 32768; % (rad/s) / LSB
346 obj.mpu.Gyroscope.ConstantBias = [obj.rant(5), obj.rant(5), obj.rant(5)] * pi/180; % (rad/s) (datasheet: +-5 dps)
347 % sensitivity is modeled into the misalignment matrix
348 obj.mpu.Gyroscope.AxesMisalignment = obj.getMisalignmentMatrix(2,2,2,3); % (percent cross-axis sensitivity)
349 obj.mpu.Gyroscope.NoiseDensity = [obj.rant(4.7e-3,6e-3,2),obj.rant(4.7e-3,6e-3,2),obj.rant(4.7e-3,6e-3,2)] * pi/180; % (rad/s) / sqrt(Hz) (datasheet: 10 mdps/rtHz, value from allan variance experiments)
350 obj.mpu.Gyroscope.BiasInstability = 8.3e-3 * pi/180; % (rad/s) (missing in datasheet, value from allan variance experiments)
351 obj.mpu.Gyroscope.RandomWalk = 155.9e-6 * pi/180; % (rad/s) * sqrt(Hz) (missing in datasheet, value from allan variance experiments)
352 obj.mpu.Gyroscope.TemperatureBias = [obj.rant(0.48), obj.rant(0.48), obj.rant(0.48)] * pi / 180; % (rad/s) / °C (datasheet: (60 °/s) / 125 °C)
353 obj.mpu.Gyroscope.TemperatureScaleFactor = abs(obj.rant(3)); % (%/°C) (datasheet: 3 %/°C)
354 obj.mpu.Gyroscope.AccelerationBias = 0.1 * pi/180; % (rad/s) / (m/s^2) (NOTE: missing in datasheet, this is an estimation from https://www.analog.com/en/resources/technical-articles/gyro-mechanical-performance.html)
355 obj.mpu.Gyroscope.NoiseType = "single-sided";
356
357 % magnetometer parameters
358 obj.mpu.Magnetometer.MeasurementRange = 4800; % µT
359 obj.mpu.Magnetometer.Resolution = 9600 / 2^14; % µT / LSB
360 obj.mpu.Magnetometer.ConstantBias = obj.mpu.Magnetometer.Resolution * randn() * 500; % µT (datasheet: +-500 LSB)
361 obj.mpu.Magnetometer.AxesMisalignment = diag([100,100,100]); % (percent cross-axis sensitivity) (NOTE: missing in datasheet)
362 obj.mpu.Magnetometer.NoiseDensity = obj.rant(0.65,0.75,2); % µT / sqrt(Hz) (missing in datasheet, value from allan variance experiments)
363 obj.mpu.Magnetometer.BiasInstability = 0.3673; % µT (missing in datasheet, value from allan variance experiments)
364 obj.mpu.Magnetometer.RandomWalk = 0.01216; % µT * sqrt(Hz) (missing in datasheet, value from allan variance experiments)
365 % NOTE: temperature bias and scale factor are missing in the datasheet
366 obj.mpu.Magnetometer.NoiseType = "single-sided";
367 end
368
369 % ====================================================================== %
370 %> @brief createHig creates the ADXL375 sensor.
371 %>
372 %> createHig creates the ADXL375 sensor with the settings specified in
373 %> the properties of the JUMP sensor model.
374 %>
375 %> @param obj (JumpSensor) - the JUMP sensor
376 %>
377 %> @retval none
378 %>
379 %> @note in here the actual sensor is modeled with noise and bias. See the
380 %> actual code for the parameters and explanations.
381 % ====================================================================== %
382 function obj = createHig(obj)
383
384 obj.hig = imuSensor('accel-gyro', ... % sensor type (NOTE: there is no actual gyro, but the imuSensor class requires it)
385 "ReferenceFrame", "NED", ... % reference frame for JUMP sensors
386 "SampleRate", obj.sampleRate, ... % sample rate
387 "Temperature", 25 ... % temperature (tunable)
388 );
389
390 % accelerometer parameters
391 obj.hig.Accelerometer.MeasurementRange = 200 * 9.81; % m/s^2 (datasheet: min 180g, typ: 200g)
392 obj.hig.Accelerometer.Resolution = 0.049 * 9.81; % (m/s^2) / LSB (datasheet: 49 mg/LSB)
393 obj.hig.Accelerometer.ConstantBias = [obj.rant(-0.4,0.4,1),obj.rant(-0.4,0.4,1),obj.rant(-0.4,0.4,1)] * 9.81; % (m/s^2) (datasheet: +- 0.4g 1 sigma)
394 obj.hig.Accelerometer.AxesMisalignment = obj.getMisalignmentMatrix(2.5); % (percent cross-axis sensitivity) (datasheet: 2.5%)
395 obj.hig.Accelerometer.NoiseDensity = obj.rant(0.0264,0.0381,2); % (m/s^2) / sqrt(Hz) (datasheet: 5 mg/rtHz, value from allan variance experiments)
396 obj.hig.Accelerometer.BiasInstability = 0.0038; % m/s^2 (missing in datasheet, value from allan variance experiments)
397 obj.hig.Accelerometer.RandomWalk = 174.95e-6; % (m/s^2) * sqrt(Hz) (missing in datasheet, value from allan variance experiments)
398 obj.hig.Accelerometer.TemperatureBias = abs(obj.rant(-0.01,0.01,1)) * 9.81; % (m/s^2) / °C (datasheet: 10 mg/°C)
399 obj.hig.Accelerometer.TemperatureScaleFactor = abs(obj.rant(-0.02,0.02,1)); % (%/°C) (datasheet: 0.02 %/°C)
400 obj.hig.Accelerometer.NoiseType = "single-sided";
401
402 end
403
404 % ====================================================================== %
405 %> @brief getPressureFromAltitude returns the pressure from the altitude.
406 %>
407 %> getPressureFromAltitude returns the pressure from the altitude. The
408 %> pressure is modeled with the barometric formula and the actual sensor
409 %> is modeled with noise and bias.
410 %>
411 %> @param obj (JumpSensor) - the JUMP sensor
412 %> @param alt (nx1 double) - the altitude in m
413 %> @param tmp (nx1 double, optional) - the temperature in °C (default: 25°C)
414 %> @param prs_sealvl (1x1 double, optional) - the pressure at sea level in mbar (default: 1013.25 mbar)
415 %>
416 %> @retval mbar (nx1 double) - the pressure in mbar
417 %>
418 %> @note the experiment 240213_pressure_membrane suggested a mean raise
419 %> time of 0.5s. Since this is approx the free fall duration (=0.49 s),
420 %> we assume that the pressure sensor is able to measure the pressure with
421 %> almost no delay. Therefore, we do not need to model the delay of the
422 %> sensor.
423 %>
424 %> @note formula from: https://www.omnicalculator.com/physics/air-pressure-at-altitude, last accessed: 2024-02
425 % ====================================================================== %
426 function mbar = getPressureFromAltitude(obj, alt, tmp, prs_sealvl)
427
428 % input validation
429 arguments
430 obj;
431 alt (:,1) double {mustBeInRange(alt, 0, 2000)}
432 tmp (:,1) double {mustBeInRange(tmp, -20, 40)} = repmat(25, numel(alt),1); % temperature in °C
433 prs_sealvl (1,1) double = 1013.25; % pressure at sea level in mbar
434 end
435
436 % downsample the altitude to the sample rate of the pressure sensor
437 downsample_ratio = obj.sampleRate / obj.sampleRatePressure;
438 alt_ds = downsample(alt, downsample_ratio);
439 tmp = downsample(tmp, downsample_ratio);
440
441 % calculate the pressure from the altitude (source: https://www.omnicalculator.com/physics/air-pressure-at-altitude)
443 % Formula: P = P0 * exp(-g * M * (h - h0) / (R * T))
444 %
445 % h : altitude in m
446 % P : pressure at altitude h in Pascal
447 % P0: pressure at sea level in Pascal
448 % T : temperature at h in Kelvin
449 % g : acceleration due to gravity in m/s^2
450 % M : molar mass of Earth's air in kg/mol
451 % R: universal gas constant in J/(mol*K)
452 h = alt_ds;
453 P0 = prs_sealvl * 100; % convert to Pascal
454 T = tmp + 273.15; % convert to Kelvin
455 g = 9.80665; % m/s^2
456 M = 0.0289644; % kg/mol
457 R = 8.31432; % J/(mol*K)
458
459 P = P0 * exp(-g * M .* h ./ (R * T));
460 mbar = P / 100; % convert to mbar
461
462 % the experiment 240213_pressure_membrane suggested a mean raise time of 0.5s.
463 % since this is approx the free fall duration (=0.49 s), we assume that the
464 % pressure sensor is able to measure the pressure with almost no delay.
465 % Therefore, we do not need to model the delay of the sensor.
466
467 % model the actual sensor
468 resolution = 0.027; % mbar / LSB
469 noiseDensity = 0.0109; % mbar / sqrt(Hz) (from allan variance experiments)
470 biasInstability = 0.013; % mbar (from allan variance experiments)
471 randomWalk = 0.003; % mbar * sqrt(Hz) (from allan variance experiments)
472
473 % generate noise
474 noise = obj.generateNoise(length(mbar), noiseDensity, randomWalk, biasInstability, obj.sampleRatePressure);
475
476 bias = obj.rant(-1.5,1.5); % mbar
477 mbar = mbar + bias + noise; % add noise and bias
478 mbar = mbar / resolution; % convert to LSB
479 mbar = round(mbar); % round to integer
480 mbar = mbar * resolution; % convert back to mbar
481
482 % upsample the pressure to the sample rate of the MPU (filling with the last value)
483 mbarOut = nan(length(alt),1);
484 mbarOut(1:downsample_ratio:end) = mbar;
485 mbar = mbarOut;
486 % mbar = fillmissing(mbarOut, 'previous'); % the new jumpReadData function handles this better, therefore not needed anymore.
487 end
488
489 % ====================================================================== %
490 %> @brief getSensorTemperature returns the sensor temperature.
491 %>
492 %> getSensorTemperature returns the sensor temperature with the actual
493 %> sensor noise and bias.
494 %>
495 %> @param obj (JumpSensor) - the JUMP sensor
496 %> @param tmp_true (nx1 double) - the true temperature in °C
497 %>
498 %> @retval degC (nx1 double) - the sensor temperature in °C
499 % ====================================================================== %
500 function degC = getSensorTemperature(obj, tmp_true)
501
502 % input validation
503 arguments
504 obj;
505 tmp_true (:,1) double {mustBeInRange(tmp_true, -20, 40)} = repmat(25, numel(alt),1); % temperature in °C
506 end
507
508 % downsample the temperature to the sample rate of the pressure sensor
509 downsample_ratio = obj.sampleRate / obj.sampleRatePressure;
510 tmp_true_ds = downsample(tmp_true, downsample_ratio);
511
512 % model the actual sensor
513 resolution = 0.01; % °C / LSB
514 noiseDensity = 0.0013; % °C / sqrt(Hz) (from allan variance experiments)
515 biasInstability = 0.00158; % °C (from allan variance experiments)
516 randomWalk = 0.0005; % °C * sqrt(Hz) (from allan variance experiments)
518 % generate noise
519 noise = obj.generateNoise(length(tmp_true_ds), noiseDensity, randomWalk, biasInstability, obj.sampleRatePressure);
520
521 bias = obj.rant(-0.8,0.8); % °C
522 degC = tmp_true_ds + bias + noise; % add noise and bias
523
524 % quantize the temperature
525 degC = degC / resolution; % convert to LSB
526 degC = round(degC); % round to integer
527 degC = degC * resolution; % convert back to °C
528
529 % upsample the temperature to the sample rate of the MPU (filling with the last value)
530 degCOut = nan(length(tmp_true),1);
531 degCOut(1:downsample_ratio:end) = degC;
532 degC = degCOut;
533 %degC = fillmissing(degCOut, 'previous'); % the new jumpReadData function handles this better, therefore not needed anymore.
534
535 end
536
537 end
538
539
540 methods (Access = private, Static)
541
542 % ====================================================================== %
543 %> @brief getMisalignmentMatrix returns a misalignment matrix for the
544 %> accelerometer or gyroscope.
545 %>
546 %> getMisalignmentMatrix returns a misalignment matrix for the
547 %> accelerometer or gyroscope. The misalignment matrix is a 3x3 matrix
548 %> which is used to rotate the sensor axes to the reference frame. The
549 %> misalignment matrix is generated randomly.
550 %>
551 %> @param x (1x1 double) - the typical misalignment of the x-axis in percent
552 %> @param y (1x1 double) - the typical misalignment of the y-axis in percent
553 %> @param z (1x1 double) - the typical misalignment of the z-axis in percent
554 %> @param sensitivity (1x1 double) - the overall sensitivity error in percent
555 %>
556 %> @retval M (3x3 double) - the misalignment matrix in percent
557 %>
558 %> @note the misalignment matrix is generated with the following steps:
559 %> 1. generate the three axis in perfect alignment, but with sensitivity errors
560 %> 2. generate the misalignment angles
561 %> 3. generate the misalignment rotation
562 %> 4. rotate the axes (attention, the rotx function works with degrees!)
563 %> 5. create the misalignment matrix
564 % ====================================================================== %
565 function M = getMisalignmentMatrix(x, y, z, sensitivity)
566
567 % check input arguments
568 if nargin == 1
569 y = x;
570 z = x;
571 sensitivity = 0;
572 end
573
574 % scale to scalars
575 x = x / 100;
576 y = y / 100;
577 z = z / 100;
578 sensitivity = sensitivity / 100;
579
580 % define the three axis in perfect alignment, but with sensitivity errors
581 ax_x = [1;0;0] * JumpSensor.rant(1-sensitivity, 1+sensitivity);
582 ax_y = [0;1;0] * JumpSensor.rant(1-sensitivity, 1+sensitivity);
583 ax_z = [0;0;1] * JumpSensor.rant(1-sensitivity, 1+sensitivity);
584
585 % generate the misalignment angles
586 theta_x = JumpSensor.rant(0,asin(x) * 180/pi);
587 theta_y = JumpSensor.rant(0,asin(y) * 180/pi);
588 theta_z = JumpSensor.rant(0,asin(z) * 180/pi);
589
590 % generate the misalignment rotation
591 rot_x = rand() * 360;
592 rot_y = rand() * 360;
593 rot_z = rand() * 360;
594
595 % rotate the axes (attention, the rotx function works with degrees!)
596 ax_x = rotx(rot_x) * ( roty(theta_x) * ax_x );
597 ax_y = roty(rot_y) * ( rotz(theta_y) * ax_y );
598 ax_z = rotz(rot_z) * ( rotx(theta_z) * ax_z );
599
600 % create the misalignment matrix
601 M = [ax_x'; ax_y'; ax_z'] * 100;
602
603 end
604
605 % ====================================================================== %
606 %> @brief rant returns a typical value in the sense of datasheet typical
607 %> values.
608 %>
609 %> rant(lower) returns a typical random number with a symmetric
610 %> distribution around zero, where the range is defined by the lower
611 %> bound. The random number is gaussian distributed with lower been the
612 %> two standard deviation range.
613 %>
614 %> rant(lower, upper) returns a typical random number with a gaussian
615 %> distribution with the given range.
616 %>
617 %> rant(lower, upper, nstd) returns a typical random number with a
618 %> gaussian distribution with nstd standard deviations.
619 %>
620 %> @param lower (1x1 double) - the lower bound of the distribution
621 %> @param upper (1x1 double) - the upper bound of the distribution
622 %> @param nstd (1x1 double) - the number of standard deviations
623 %>
624 %> @retval number (1x1 double) - the random number
625 function number = rant(lower, upper, nstd)
626
627 % check input arguments
628 arguments
629 lower (1,1) double;
630 upper (1,1) double = -lower;
631 nstd (1,1) double = 2;
632 end
633
634 if lower > upper
635 [lower, upper] = deal(upper, lower);
636 end
637
638 % gaussian distribution
639 mu = (lower + upper) / 2;
640 sigma = (upper - lower) / (2 * nstd);
641 number = normrnd(mu, sigma);
642
643 end
645 % ====================================================================== %
646 %> @brief generateNoise generates noise with the given parameters.
647 %>
648 %> generateNoise generates noise with the given parameters. The noise is
649 %> generated with the following model:
650 %> - white noise with the noise density N
651 %> - random walk with the coefficient K
652 %> - bias instability with the coefficient B
653 %> The noise is generated with the sample rate fs.
654 %>
655 %> @param n (1x1 double) - the number of samples
656 %> @param N (1x1 double) - the noise density
657 %> @param K (1x1 double) - the random walk coefficient
658 %> @param B (1x1 double) - the bias instability coefficient
659 %> @param fs (1x1 double) - the sample rate
660 %>
661 %> @retval noise (nx1 double) - the generated noise
662 % ====================================================================== %
663 function noise = generateNoise(n,N,K,B,fs)
664
665 % check input arguments
666 arguments
667 n (1,1) double {mustBeInteger, mustBePositive}; % number of samples
668 N (1,1) double {mustBePositive}; % noise density
669 K (1,1) double {mustBePositive}; % random walk
670 B (1,1) double {mustBePositive}; % bias instability
671 fs (1,1) double {mustBeInteger, mustBePositive}; % sample rate
672 end
673
674 % scale the coefficients
675 N = N * sqrt(2); % single-sided noise
676 K = K * sqrt(2); % single-sided noise
677 B = B * sqrt(2); % single-sided noise
678
679 % define the PSD of the Noise
680 epsilon = 1e-6; % to avoid division by zero
681 PSD = @(f) N^2 + B^2 ./ (2*pi*f + epsilon) + K^2 ./ (2*pi*f + epsilon).^2;
682
683 % frequency vector
684 f = (0:(n-1)/2) * fs / n;
685
686 % generate the noise
687 white_noise = randn(n,1);
688
689 % one-sided fft of the white noise
690 Y = fft(white_noise);
691
692 % set the DC component to zero
693 Y(1) = 0;
694
695 % adjust the amplitude according to the PSD
696 Y(1:length(f)) = Y(1:length(f)) .* sqrt(PSD(f))';
697
698 % if the signal length is even, the Nyquist frequency component is real
699 if mod(n,2) == 0
700 Y (n/2+1) = real(Y(n/2+1));
701 end
702
703 % inverse fft
704 noise = ifft(Y, 'symmetric');
705
706 end
707
708 end
709end
710
JumpSensor is a simulation of the JUMP sensor of the ZurichMove Project.
Definition JumpSensor.m:41
Property sampleRateMagnetometer
Definition JumpSensor.m:55
Property sampleRatePressure
Definition JumpSensor.m:53
function generateFromTrajectory(in obj, in traj)
generates data from a Trajectory object.
static function generateNoise(in n, in N, in K, in B, in fs)
generateNoise generates noise with the given parameters.
static function getMisalignmentMatrix(in x, in y, in z, in sensitivity)
getMisalignmentMatrix returns a misalignment matrix for the accelerometer or gyroscope.
function getSensorTemperature(in obj, in tmp_true)
getSensorTemperature returns the sensor temperature.
function getParams(in obj)
returns the parameters of the JUMP sensor.
Property sampleRate
Definition JumpSensor.m:51
function createMpu(in obj)
createMpu creates the MPU9250 sensor.
Property gyroRange
Definition JumpSensor.m:49
function JumpSensor(in args)
JumpSensor is the constructor of the JUMP sensor.
function generate(in obj, in acc_true, in gyr_true, in ori_true, in options)
generate generates sensor data from true data.
function getPressureFromAltitude(in obj, in alt, in tmp, in prs_sealvl)
getPressureFromAltitude returns the pressure from the altitude.
function createHig(in obj)
createHig creates the ADXL375 sensor.
static function rant(in lower, in upper, in nstd)
rant returns a typical value in the sense of datasheet typical values.
Property mpu
Definition JumpSensor.m:59
Property hig
Definition JumpSensor.m:61
Property accRange
Definition JumpSensor.m:47
Trajectory Class for generating and manipulating trajectories from waypoints.
Definition Trajectory.m:37
handle class from MATLAB.
app that helps to generate a trajectory.
function jumpReadData(in filePath, in options)