Source code for rocketpy.sensors.sensor

import json
import warnings
from abc import ABC, abstractmethod

import numpy as np

from rocketpy.mathutils.vector_matrix import Matrix, Vector


# pylint: disable=too-many-statements
[docs] class Sensor(ABC): """Abstract class for sensors Attributes ---------- sampling_rate : float Sample rate of the sensor in Hz. measurement_range : float, tuple The measurement range of the sensor in the sensor units. resolution : float The resolution of the sensor in sensor units/LSB. noise_density : float, list The noise density of the sensor in sensor units/√Hz. noise_variance : float, list The variance of the noise of the sensor in sensor units^2. random_walk_density : float, list The random walk density of the sensor in sensor units/√Hz. random_walk_variance : float, list The variance of the random walk of the sensor in sensor units^2. constant_bias : float, list The constant bias of the sensor in sensor units. operating_temperature : float The operating temperature of the sensor in Kelvin. temperature_bias : float, list The temperature bias of the sensor in sensor units/K. temperature_scale_factor : float, list The temperature scale factor of the sensor in %/K. name : str The name of the sensor. measurement : float The measurement of the sensor after quantization, noise and temperature drift. measured_data : list The stored measured data of the sensor after quantization, noise and temperature drift. """
[docs] def __init__( self, sampling_rate, measurement_range=np.inf, resolution=0, noise_density=0, noise_variance=1, random_walk_density=0, random_walk_variance=1, constant_bias=0, operating_temperature=25, temperature_bias=0, temperature_scale_factor=0, name="Sensor", ): """ Initialize the accelerometer sensor Parameters ---------- sampling_rate : float Sample rate of the sensor measurement_range : float, tuple, optional The measurement range of the sensor in the sensor units. If a float, the same range is applied both for positive and negative values. If a tuple, the first value is the positive range and the second value is the negative range. Default is np.inf. resolution : float, optional The resolution of the sensor in sensor units/LSB. Default is 0, meaning no quantization is applied. noise_density : float, list, optional The noise density of the sensor for a Gaussian white noise in sensor units/√Hz. Sometimes called "white noise drift", "angular random walk" for gyroscopes, "velocity random walk" for accelerometers or "(rate) noise density". Default is 0, meaning no noise is applied. noise_variance : float, list, optional The noise variance of the sensor for a Gaussian white noise in sensor units^2. Default is 1, meaning the noise is normally distributed with a standard deviation of 1 unit. random_walk_density : float, list, optional The random walk density of the sensor for a Gaussian random walk in sensor units/√Hz. Sometimes called "bias (in)stability" or "bias drift". Default is 0, meaning no random walk is applied. random_walk_variance : float, list, optional The random walk variance of the sensor for a Gaussian random walk in sensor units^2. Default is 1, meaning the noise is normally distributed with a standard deviation of 1 unit. constant_bias : float, list, optional The constant bias of the sensor in sensor units. Default is 0, meaning no constant bias is applied. operating_temperature : float, optional The operating temperature of the sensor in Kelvin. At 298.15 K (25 °C), the sensor is assumed to operate ideally, no temperature related noise is applied. Default is 298.15. temperature_bias : float, list, optional The temperature bias of the sensor in sensor units/K. Default is 0, meaning no temperature bias is applied. temperature_scale_factor : float, list, optional The temperature scale factor of the sensor in %/K. Default is 0, meaning no temperature scale factor is applied. name : str, optional The name of the sensor. Default is "Sensor". Returns ------- None See Also -------- TODO link to documentation on noise model """ warnings.warn( "The Sensor class (and all its subclasses) is still under " "experimental development. Some features may be changed in future " "versions, although we will try to keep the changes to a minimum.", UserWarning, ) self.sampling_rate = sampling_rate self.resolution = resolution self.operating_temperature = operating_temperature self.noise_density = noise_density self.noise_variance = noise_variance self.random_walk_density = random_walk_density self.random_walk_variance = random_walk_variance self.constant_bias = constant_bias self.temperature_bias = temperature_bias self.temperature_scale_factor = temperature_scale_factor self.name = name self.measurement = None self.measured_data = [] self._counter = 0 self._save_data = self._save_data_single self._random_walk_drift = 0 self.normal_vector = Vector([0, 0, 0]) # handle measurement range if isinstance(measurement_range, (tuple, list)): if len(measurement_range) != 2: raise ValueError("Invalid measurement range format") self.measurement_range = measurement_range elif isinstance(measurement_range, (int, float)): self.measurement_range = (-measurement_range, measurement_range) else: raise ValueError("Invalid measurement range format") # map which rocket(s) the sensor is attached to and how many times self._attached_rockets = {}
def __repr__(self): return f"{self.name}" def __call__(self, *args, **kwargs): return self.measure(*args, **kwargs)
[docs] def _reset(self, simulated_rocket): """Reset the sensor data for a new simulation.""" self._random_walk_drift = ( Vector([0, 0, 0]) if isinstance(self._random_walk_drift, Vector) else 0 ) self.measured_data = [] if self._attached_rockets[simulated_rocket] > 1: self.measured_data = [ [] for _ in range(self._attached_rockets[simulated_rocket]) ] self._save_data = self._save_data_multiple else: self._save_data = self._save_data_single
[docs] def _save_data_single(self, data): """Save the measured data to the sensor data list for a sensor that is added only once to the simulated rocket.""" self.measured_data.append(data)
[docs] def _save_data_multiple(self, data): """Save the measured data to the sensor data list for a sensor that is added multiple times to the simulated rocket.""" self.measured_data[self._counter].append(data) # counter for cases where the sensor is added multiple times in a rocket self._counter += 1 if self._counter == len(self.measured_data): self._counter = 0
[docs] @abstractmethod def measure(self, time, **kwargs): """Measure the sensor data at a given time"""
[docs] @abstractmethod def quantize(self, value): """Quantize the sensor measurement"""
[docs] @abstractmethod def apply_noise(self, value): """Add noise to the sensor measurement"""
[docs] @abstractmethod def apply_temperature_drift(self, value): """Apply temperature drift to the sensor measurement"""
[docs] @abstractmethod def export_measured_data(self, filename, file_format="csv"): """Export the measured values to a file"""
[docs] def _generic_export_measured_data(self, filename, file_format, data_labels): """Export the measured values to a file given the data labels of each sensor. Parameters ---------- sensor : Sensor Sensor object to export the measured values from. filename : str Name of the file to export the values to file_format : str file_format of the file to export the values to. Options are "csv" and "json". Default is "csv". data_labels : tuple Tuple of strings representing the labels for the data columns Returns ------- None """ if file_format.lower() not in ["json", "csv"]: raise ValueError("Invalid file_format") if file_format.lower() == "csv": # if sensor has been added multiple times to the simulated rocket if isinstance(self.measured_data[0], list): print("Data saved to", end=" ") for i, data in enumerate(self.measured_data): with open(filename + f"_{i + 1}", "w") as f: f.write(",".join(data_labels) + "\n") for entry in data: f.write(",".join(map(str, entry)) + "\n") print(filename + f"_{i + 1},", end=" ") else: with open(filename, "w") as f: f.write(",".join(data_labels) + "\n") for entry in self.measured_data: f.write(",".join(map(str, entry)) + "\n") print(f"Data saved to {filename}") return if file_format.lower() == "json": if isinstance(self.measured_data[0], list): print("Data saved to", end=" ") for i, data in enumerate(self.measured_data): data_dict = {label: [] for label in data_labels} for entry in data: for label, value in zip(data_labels, entry): data_dict[label].append(value) with open(filename + f"_{i + 1}", "w") as f: json.dump(data_dict, f) print(filename + f"_{i + 1},", end=" ") else: data_dict = {label: [] for label in data_labels} for entry in self.measured_data: for label, value in zip(data_labels, entry): data_dict[label].append(value) with open(filename, "w") as f: json.dump(data_dict, f) print(f"Data saved to {filename}") return
[docs] class InertialSensor(Sensor): """Model of an inertial sensor (accelerometer, gyroscope, magnetometer). Inertial sensors measurements are handled as vectors. The measurements are affected by the sensor's orientation in the rocket. Attributes ---------- sampling_rate : float Sample rate of the sensor in Hz. orientation : tuple, list Orientation of the sensor in the rocket. measurement_range : float, tuple The measurement range of the sensor in the sensor units. resolution : float The resolution of the sensor in sensor units/LSB. noise_density : float, list The noise density of the sensor in sensor units/√Hz. noise_variance : float, list The variance of the noise of the sensor in sensor units^2. random_walk_density : float, list The random walk density of the sensor in sensor units/√Hz. random_walk_variance : float, list The variance of the random walk of the sensor in sensor units^2. constant_bias : float, list The constant bias of the sensor in sensor units. operating_temperature : float The operating temperature of the sensor in Kelvin. temperature_bias : float, list The temperature bias of the sensor in sensor units/K. temperature_scale_factor : float, list The temperature scale factor of the sensor in %/K. cross_axis_sensitivity : float The cross axis sensitivity of the sensor in percentage. name : str The name of the sensor. rotation_matrix : Matrix The rotation matrix of the sensor from the rocket frame to the sensor frame of reference. normal_vector : Vector The normal vector of the sensor in the rocket frame of reference. measurement : float The measurement of the sensor after quantization, noise and temperature drift. measured_data : list The stored measured data of the sensor after quantization, noise and temperature drift. """
[docs] def __init__( self, sampling_rate, orientation=(0, 0, 0), measurement_range=np.inf, resolution=0, noise_density=0, noise_variance=1, random_walk_density=0, random_walk_variance=1, constant_bias=0, operating_temperature=298.15, temperature_bias=0, temperature_scale_factor=0, cross_axis_sensitivity=0, name="Sensor", ): """ Initialize the accelerometer sensor Parameters ---------- sampling_rate : float Sample rate of the sensor orientation : tuple, list, optional Orientation of the sensor in relation to the rocket frame of reference (Body Axes Coordinate System). See :ref:`rocket_axes` for more information. If orientation is not given, the sensor axes will be aligned with the rocket axis. The orientation can be given as either: - A list or tuple of length 3, where the elements are the intrinsic rotation angles in radians. The rotation sequence z-x-z (3-1-3) is used, meaning the sensor is first around the z axis (roll), then around the new x axis (pitch) and finally around the new z axis (roll). - A list of lists (matrix) of shape 3x3, representing the rotation matrix from the sensor frame to the rocket frame. The sensor frame of reference is defined as being initially aligned with the rocket frame of reference. measurement_range : float, tuple, optional The measurement range of the sensor in the sensor units. If a float, the same range is applied both for positive and negative values. If a tuple, the first value is the positive range and the second value is the negative range. Default is np.inf. resolution : float, optional The resolution of the sensor in sensor units/LSB. Default is 0, meaning no quantization is applied. noise_density : float, list, optional The noise density of the sensor for a Gaussian white noise in sensor units/√Hz. Sometimes called "white noise drift", "angular random walk" for gyroscopes, "velocity random walk" for accelerometers or "(rate) noise density". Default is 0, meaning no noise is applied. If a float or int is given, the same noise density is applied to all axes. The values of each axis can be set individually by passing a list of length 3. noise_variance : float, list, optional The noise variance of the sensor for a Gaussian white noise in sensor units^2. Default is 1, meaning the noise is normally distributed with a standard deviation of 1 unit. If a float or int is given, the same noise variance is applied to all axes. The values of each axis can be set individually by passing a list of length 3. random_walk_density : float, list, optional The random walk density of the sensor for a Gaussian random walk in sensor units/√Hz. Sometimes called "bias (in)stability" or "bias drift". Default is 0, meaning no random walk is applied. If a float or int is given, the same random walk is applied to all axes. The values of each axis can be set individually by passing a list of length 3. random_walk_variance : float, list, optional The random walk variance of the sensor for a Gaussian random walk in sensor units^2. Default is 1, meaning the noise is normally distributed with a standard deviation of 1 unit. If a float or int is given, the same random walk variance is applied to all axes. The values of each axis can be set individually by passing a list of length 3. constant_bias : float, list, optional The constant bias of the sensor in sensor units. Default is 0, meaning no constant bias is applied. If a float or int is given, the same constant bias is applied to all axes. The values of each axis can be set individually by passing a list of length 3. operating_temperature : float, optional The operating temperature of the sensor in Kelvin. At 298.15 K (25 °C), the sensor is assumed to operate ideally, no temperature related noise is applied. Default is 298.15. temperature_bias : float, list, optional The temperature bias of the sensor in sensor units/K. Default is 0, meaning no temperature bias is applied. If a float or int is given, the same temperature bias is applied to all axes. The values of each axis can be set individually by passing a list of length 3. temperature_scale_factor : float, list, optional The temperature scale factor of the sensor in %/K. Default is 0, meaning no temperature scale factor is applied. If a float or int is given, the same temperature scale factor is applied to all axes. The values of each axis can be set individually by passing a list of length 3. cross_axis_sensitivity : float, optional Skewness of the sensor's axes in percentage. Default is 0, meaning no cross-axis sensitivity is applied. name : str, optional The name of the sensor. Default is "Sensor". Returns ------- None See Also -------- TODO link to documentation on noise model """ super().__init__( sampling_rate=sampling_rate, measurement_range=measurement_range, resolution=resolution, noise_density=self._vectorize_input(noise_density, "noise_density"), noise_variance=self._vectorize_input(noise_variance, "noise_variance"), random_walk_density=self._vectorize_input( random_walk_density, "random_walk_density" ), random_walk_variance=self._vectorize_input( random_walk_variance, "random_walk_variance" ), constant_bias=self._vectorize_input(constant_bias, "constant_bias"), operating_temperature=operating_temperature, temperature_bias=self._vectorize_input( temperature_bias, "temperature_bias" ), temperature_scale_factor=self._vectorize_input( temperature_scale_factor, "temperature_scale_factor" ), name=name, ) self.orientation = orientation self.cross_axis_sensitivity = cross_axis_sensitivity self._random_walk_drift = Vector([0, 0, 0]) # rotation matrix and normal vector if any(isinstance(row, (tuple, list)) for row in orientation): # matrix self.rotation_matrix = Matrix(orientation) elif len(orientation) == 3: # euler angles self.rotation_matrix = Matrix.transformation_euler_angles( *np.deg2rad(orientation) ).round(12) else: raise ValueError("Invalid orientation format") self.normal_vector = Vector( [ self.rotation_matrix[0][2], self.rotation_matrix[1][2], self.rotation_matrix[2][2], ] ).unit_vector # cross axis sensitivity matrix _cross_axis_matrix = 0.01 * Matrix( [ [100, self.cross_axis_sensitivity, self.cross_axis_sensitivity], [self.cross_axis_sensitivity, 100, self.cross_axis_sensitivity], [self.cross_axis_sensitivity, self.cross_axis_sensitivity, 100], ] ) # compute total rotation matrix given cross axis sensitivity self._total_rotation_matrix = self.rotation_matrix @ _cross_axis_matrix
def _vectorize_input(self, value, name): if isinstance(value, (int, float)): return Vector([value, value, value]) elif isinstance(value, (tuple, list)): return Vector(value) else: raise ValueError(f"Invalid {name} format")
[docs] def quantize(self, value): """ Quantize the sensor measurement Parameters ---------- value : float The value to quantize Returns ------- float The quantized value """ x = min(max(value.x, self.measurement_range[0]), self.measurement_range[1]) y = min(max(value.y, self.measurement_range[0]), self.measurement_range[1]) z = min(max(value.z, self.measurement_range[0]), self.measurement_range[1]) if self.resolution != 0: x = round(x / self.resolution) * self.resolution y = round(y / self.resolution) * self.resolution z = round(z / self.resolution) * self.resolution return Vector([x, y, z])
[docs] def apply_noise(self, value): """ Add noise to the sensor measurement Parameters ---------- value : float The value to add noise to Returns ------- float The value with added noise """ # white noise white_noise = Vector( [np.random.normal(0, self.noise_variance[i] ** 0.5) for i in range(3)] ) & (self.noise_density * self.sampling_rate**0.5) # random walk self._random_walk_drift = self._random_walk_drift + Vector( [np.random.normal(0, self.random_walk_variance[i] ** 0.5) for i in range(3)] ) & (self.random_walk_density / self.sampling_rate**0.5) # add noise value += white_noise + self._random_walk_drift + self.constant_bias return value
[docs] def apply_temperature_drift(self, value): """ Apply temperature drift to the sensor measurement Parameters ---------- value : float The value to apply temperature drift to Returns ------- float The value with applied temperature drift """ # temperature drift value += (self.operating_temperature - 298.15) * self.temperature_bias # temperature scale factor scale_factor = ( Vector([1, 1, 1]) + (self.operating_temperature - 298.15) / 100 * self.temperature_scale_factor ) return value & scale_factor
[docs] class ScalarSensor(Sensor): """Model of a scalar sensor (e.g. Barometer). Scalar sensors are used to measure a single scalar value. The measurements are not affected by the sensor's orientation in the rocket. Attributes ---------- sampling_rate : float Sample rate of the sensor in Hz. measurement_range : float, tuple The measurement range of the sensor in the sensor units. resolution : float The resolution of the sensor in sensor units/LSB. noise_density : float The noise density of the sensor in sensor units/√Hz. noise_variance : float The variance of the noise of the sensor in sensor units^2. random_walk_density : float The random walk density of the sensor in sensor units/√Hz. random_walk_variance : float The variance of the random walk of the sensor in sensor units^2. constant_bias : float The constant bias of the sensor in sensor units. operating_temperature : float The operating temperature of the sensor in Kelvin. temperature_bias : float The temperature bias of the sensor in sensor units/K. temperature_scale_factor : float The temperature scale factor of the sensor in %/K. name : str The name of the sensor. measurement : float The measurement of the sensor after quantization, noise and temperature drift. measured_data : list The stored measured data of the sensor after quantization, noise and temperature drift. """
[docs] def __init__( self, sampling_rate, measurement_range=np.inf, resolution=0, noise_density=0, noise_variance=1, random_walk_density=0, random_walk_variance=1, constant_bias=0, operating_temperature=25, temperature_bias=0, temperature_scale_factor=0, name="Sensor", ): """ Initialize the accelerometer sensor Parameters ---------- sampling_rate : float Sample rate of the sensor measurement_range : float, tuple, optional The measurement range of the sensor in the sensor units. If a float, the same range is applied both for positive and negative values. If a tuple, the first value is the positive range and the second value is the negative range. Default is np.inf. resolution : float, optional The resolution of the sensor in sensor units/LSB. Default is 0, meaning no quantization is applied. noise_density : float, list, optional The noise density of the sensor for a Gaussian white noise in sensor units/√Hz. Sometimes called "white noise drift", "angular random walk" for gyroscopes, "velocity random walk" for accelerometers or "(rate) noise density". Default is 0, meaning no noise is applied. noise_variance : float, list, optional The noise variance of the sensor for a Gaussian white noise in sensor units^2. Default is 1, meaning the noise is normally distributed with a standard deviation of 1 unit. random_walk_density : float, list, optional The random walk density of the sensor for a Gaussian random walk in sensor units/√Hz. Sometimes called "bias (in)stability" or "bias drift". Default is 0, meaning no random walk is applied. random_walk_variance : float, list, optional The random walk variance of the sensor for a Gaussian random walk in sensor units^2. Default is 1, meaning the noise is normally distributed with a standard deviation of 1 unit. constant_bias : float, list, optional The constant bias of the sensor in sensor units. Default is 0, meaning no constant bias is applied. operating_temperature : float, optional The operating temperature of the sensor in Kelvin. At 298.15 K (25 °C), the sensor is assumed to operate ideally, no temperature related noise is applied. Default is 298.15. temperature_bias : float, list, optional The temperature bias of the sensor in sensor units/K. Default is 0, meaning no temperature bias is applied. temperature_scale_factor : float, list, optional The temperature scale factor of the sensor in %/K. Default is 0, meaning no temperature scale factor is applied. name : str, optional The name of the sensor. Default is "Sensor". Returns ------- None See Also -------- TODO link to documentation on noise model """ super().__init__( sampling_rate=sampling_rate, measurement_range=measurement_range, resolution=resolution, noise_density=noise_density, noise_variance=noise_variance, random_walk_density=random_walk_density, random_walk_variance=random_walk_variance, constant_bias=constant_bias, operating_temperature=operating_temperature, temperature_bias=temperature_bias, temperature_scale_factor=temperature_scale_factor, name=name, )
[docs] def quantize(self, value): """ Quantize the sensor measurement Parameters ---------- value : float The value to quantize Returns ------- float The quantized value """ value = min(max(value, self.measurement_range[0]), self.measurement_range[1]) if self.resolution != 0: value = round(value / self.resolution) * self.resolution return value
[docs] def apply_noise(self, value): """ Add noise to the sensor measurement Parameters ---------- value : float The value to add noise to Returns ------- float The value with added noise """ # white noise white_noise = ( np.random.normal(0, self.noise_variance**0.5) * self.noise_density * self.sampling_rate**0.5 ) # random walk self._random_walk_drift = ( self._random_walk_drift + np.random.normal(0, self.random_walk_variance**0.5) * self.random_walk_density / self.sampling_rate**0.5 ) # add noise value += white_noise + self._random_walk_drift + self.constant_bias return value
[docs] def apply_temperature_drift(self, value): """ Apply temperature drift to the sensor measurement Parameters ---------- value : float The value to apply temperature drift to Returns ------- float The value with applied temperature drift """ # temperature drift value += (self.operating_temperature - 298.15) * self.temperature_bias # temperature scale factor scale_factor = ( 1 + (self.operating_temperature - 298.15) / 100 * self.temperature_scale_factor ) value = value * scale_factor return value