Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Attention: The newest changes should be on top -->

- ENH: ENH: Refactor flight.py latitude/longitude to use inverted_haversine [#1055](https://github.com/RocketPy-Team/RocketPy/pull/1055)
- ENH: TST: Add unit tests for evaluate_reduced_mass and remove TODO [#1051](https://github.com/RocketPy-Team/RocketPy/pull/1051)
- ENH: seed sensor measurement noise per instance [#1052](https://github.com/RocketPy-Team/RocketPy/pull/1052)
- ENH: FIX: pre release hardening [#1047](https://github.com/RocketPy-Team/RocketPy/pull/1047)
- ENH: DEV: Claude Code ruff hooks (auto-format on edit + pre-push lint guard) [#1046](https://github.com/RocketPy-Team/RocketPy/pull/1046)
- ENH: CI: create a CI for testing docs updates + solve different docs issues [#1045](https://github.com/RocketPy-Team/RocketPy/pull/1045)
Expand Down
8 changes: 8 additions & 0 deletions rocketpy/sensors/accelerometer.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def __init__(
cross_axis_sensitivity=0,
consider_gravity=False,
name="Accelerometer",
seed=None,
):
"""
Initialize the accelerometer sensor
Expand Down Expand Up @@ -169,6 +170,11 @@ def __init__(
acceleration. Default is False.
name : str, optional
The name of the sensor. Default is "Accelerometer".
seed : int, optional
Seed for the random number generator that draws the measurement
noise. If given, the noise becomes reproducible and independent of
the process-global NumPy RNG. Default is None, meaning the noise is
seeded from fresh entropy per instance.

Returns
-------
Expand All @@ -193,6 +199,7 @@ def __init__(
temperature_scale_factor=temperature_scale_factor,
cross_axis_sensitivity=cross_axis_sensitivity,
name=name,
seed=seed,
)
self.consider_gravity = consider_gravity
self.prints = _InertialSensorPrints(self)
Expand Down Expand Up @@ -299,4 +306,5 @@ def from_dict(cls, data):
cross_axis_sensitivity=data["cross_axis_sensitivity"],
consider_gravity=data["consider_gravity"],
name=data["name"],
seed=data.get("seed"),
)
8 changes: 8 additions & 0 deletions rocketpy/sensors/barometer.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def __init__(
temperature_bias=0,
temperature_scale_factor=0,
name="Barometer",
seed=None,
):
"""
Initialize the barometer sensor
Expand Down Expand Up @@ -110,6 +111,11 @@ def __init__(
meaning no temperature scale factor is applied.
name : str, optional
The name of the sensor. Default is "Barometer".
seed : int, optional
Seed for the random number generator that draws the measurement
noise. If given, the noise becomes reproducible and independent of
the process-global NumPy RNG. Default is None, meaning the noise is
seeded from fresh entropy per instance.

Returns
-------
Expand All @@ -132,6 +138,7 @@ def __init__(
temperature_bias=temperature_bias,
temperature_scale_factor=temperature_scale_factor,
name=name,
seed=seed,
)
self.prints = _SensorPrints(self)

Expand Down Expand Up @@ -206,4 +213,5 @@ def from_dict(cls, data):
temperature_bias=data["temperature_bias"],
temperature_scale_factor=data["temperature_scale_factor"],
name=data["name"],
seed=data.get("seed"),
)
18 changes: 12 additions & 6 deletions rocketpy/sensors/gnss_receiver.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import math

import numpy as np

from rocketpy.tools import inverted_haversine

from ..mathutils.vector_matrix import Matrix, Vector
Expand Down Expand Up @@ -40,6 +38,7 @@ def __init__(
position_accuracy=0,
altitude_accuracy=0,
name="GnssReceiver",
seed=None,
):
"""Initialize the Gnss Receiver sensor.

Expand All @@ -55,8 +54,13 @@ def __init__(
position in meters. Default is 0.
name : str
The name of the sensor. Default is "GnssReceiver".
seed : int, optional
Seed for the random number generator that draws the measurement
noise. If given, the noise becomes reproducible and independent of
the process-global NumPy RNG. Default is None, meaning the noise is
seeded from fresh entropy per instance.
"""
super().__init__(sampling_rate=sampling_rate, name=name)
super().__init__(sampling_rate=sampling_rate, name=name, seed=seed)
self.position_accuracy = position_accuracy
self.altitude_accuracy = altitude_accuracy

Expand Down Expand Up @@ -90,9 +94,9 @@ def measure(self, time, **kwargs):
# Get from state u and add relative position
x, y, z = (Matrix.transformation(u[6:10]) @ relative_position) + Vector(u[0:3])
# Apply accuracy to the position
x = np.random.normal(x, self.position_accuracy)
y = np.random.normal(y, self.position_accuracy)
altitude = np.random.normal(z, self.altitude_accuracy)
x = self._rng.normal(x, self.position_accuracy)
y = self._rng.normal(y, self.position_accuracy)
altitude = self._rng.normal(z, self.altitude_accuracy)

# Convert x and y to latitude and longitude
drift = (x**2 + y**2) ** 0.5
Expand Down Expand Up @@ -131,6 +135,7 @@ def to_dict(self, **kwargs):
"position_accuracy": self.position_accuracy,
"altitude_accuracy": self.altitude_accuracy,
"name": self.name,
"seed": self._seed,
}

@classmethod
Expand All @@ -140,4 +145,5 @@ def from_dict(cls, data):
position_accuracy=data["position_accuracy"],
altitude_accuracy=data["altitude_accuracy"],
name=data["name"],
seed=data.get("seed"),
)
10 changes: 10 additions & 0 deletions rocketpy/sensors/gyroscope.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def __init__(
cross_axis_sensitivity=0,
acceleration_sensitivity=0,
name="Gyroscope",
seed=None,
):
"""
Initialize the gyroscope sensor
Expand Down Expand Up @@ -169,6 +170,13 @@ def __init__(
float or int is given, the same sensitivity is applied to all axes.
The values of each axis can be set individually by passing a list of
length 3.
name : str, optional
The name of the sensor. Default is "Gyroscope".
seed : int, optional
Seed for the random number generator that draws the measurement
noise. If given, the noise becomes reproducible and independent of
the process-global NumPy RNG. Default is None, meaning the noise is
seeded from fresh entropy per instance.

Returns
-------
Expand All @@ -193,6 +201,7 @@ def __init__(
temperature_scale_factor=temperature_scale_factor,
cross_axis_sensitivity=cross_axis_sensitivity,
name=name,
seed=seed,
)
self.acceleration_sensitivity = self._vectorize_input(
acceleration_sensitivity, "acceleration_sensitivity"
Expand Down Expand Up @@ -320,4 +329,5 @@ def from_dict(cls, data):
cross_axis_sensitivity=data["cross_axis_sensitivity"],
acceleration_sensitivity=data["acceleration_sensitivity"],
name=data["name"],
seed=data.get("seed"),
)
37 changes: 33 additions & 4 deletions rocketpy/sensors/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def __init__(
temperature_bias=0,
temperature_scale_factor=0,
name="Sensor",
seed=None,
):
"""
Initialize the accelerometer sensor
Expand Down Expand Up @@ -111,6 +112,11 @@ def __init__(
meaning no temperature scale factor is applied.
name : str, optional
The name of the sensor. Default is "Sensor".
seed : int, optional
Seed for the random number generator that draws the measurement
noise. If given, the noise becomes reproducible and independent of
the process-global NumPy RNG. Default is None, meaning the noise is
seeded from fresh entropy per instance.

Returns
-------
Expand Down Expand Up @@ -145,6 +151,14 @@ def __init__(
self._random_walk_drift = 0
self.normal_vector = Vector([0, 0, 0])

# Per-instance RNG, seeded deterministically when a seed is given, so
# the measurement noise is reproducible and independent of the
# process-global NumPy RNG (and therefore safe under parallel or
# forked evaluation). seed=None keeps the noise random but still
# drawn from this instance's generator.
self._seed = seed
self._rng = np.random.default_rng(seed)

# handle measurement range
if isinstance(measurement_range, (tuple, list)):
if len(measurement_range) != 2:
Expand Down Expand Up @@ -291,6 +305,7 @@ def to_dict(self, **kwargs):
"temperature_bias": self.temperature_bias,
"temperature_scale_factor": self.temperature_scale_factor,
"name": self.name,
"seed": self._seed,
}


Expand Down Expand Up @@ -358,6 +373,7 @@ def __init__(
temperature_scale_factor=0,
cross_axis_sensitivity=0,
name="Sensor",
seed=None,
):
"""
Initialize the accelerometer sensor
Expand Down Expand Up @@ -444,6 +460,11 @@ def __init__(
no cross-axis sensitivity is applied.
name : str, optional
The name of the sensor. Default is "Sensor".
seed : int, optional
Seed for the random number generator that draws the measurement
noise. If given, the noise becomes reproducible and independent of
the process-global NumPy RNG. Default is None, meaning the noise is
seeded from fresh entropy per instance.

Returns
-------
Expand Down Expand Up @@ -474,6 +495,7 @@ def __init__(
temperature_scale_factor, "temperature_scale_factor"
),
name=name,
seed=seed,
)

self.orientation = orientation
Expand Down Expand Up @@ -558,12 +580,12 @@ def apply_noise(self, value):
"""
# white noise
white_noise = Vector(
[np.random.normal(0, self.noise_variance[i] ** 0.5) for i in range(3)]
[self._rng.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._rng.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
Expand Down Expand Up @@ -660,6 +682,7 @@ def __init__(
temperature_bias=0,
temperature_scale_factor=0,
name="Sensor",
seed=None,
):
"""
Initialize the accelerometer sensor
Expand Down Expand Up @@ -709,6 +732,11 @@ def __init__(
meaning no temperature scale factor is applied.
name : str, optional
The name of the sensor. Default is "Sensor".
seed : int, optional
Seed for the random number generator that draws the measurement
noise. If given, the noise becomes reproducible and independent of
the process-global NumPy RNG. Default is None, meaning the noise is
seeded from fresh entropy per instance.

Returns
-------
Expand All @@ -731,6 +759,7 @@ def __init__(
temperature_bias=temperature_bias,
temperature_scale_factor=temperature_scale_factor,
name=name,
seed=seed,
)

def quantize(self, value):
Expand Down Expand Up @@ -768,15 +797,15 @@ def apply_noise(self, value):
"""
# white noise
white_noise = (
np.random.normal(0, self.noise_variance**0.5)
self._rng.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._rng.normal(0, self.random_walk_variance**0.5)
* self.random_walk_density
/ self.sampling_rate**0.5
)
Expand Down
4 changes: 4 additions & 0 deletions tests/fixtures/sensors/sensors_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def noisy_rotated_accelerometer():
cross_axis_sensitivity=0.5,
consider_gravity=True,
name="Accelerometer",
seed=42,
)


Expand All @@ -46,6 +47,7 @@ def noisy_rotated_gyroscope():
cross_axis_sensitivity=0.5,
acceleration_sensitivity=[0, 0.0008, 0.0017],
name="Gyroscope",
seed=42,
)


Expand All @@ -62,6 +64,7 @@ def noisy_barometer():
operating_temperature=25 + 273.15,
temperature_bias=0.02,
temperature_scale_factor=0.02,
seed=42,
)


Expand All @@ -71,6 +74,7 @@ def noisy_gnss():
sampling_rate=1,
position_accuracy=1,
altitude_accuracy=1,
seed=42,
)


Expand Down
9 changes: 0 additions & 9 deletions tests/unit/sensors/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,6 @@
from rocketpy.tools import euler313_to_quaternions


@pytest.fixture(autouse=True)
def _seed_rng():
"""Seed NumPy's global RNG before each test so that the noise-based
sensor measurements are deterministic. Without this, tests such as
``test_noisy_barometer`` are flaky because the random white noise can
occasionally exceed the assertion tolerance."""
np.random.seed(42)


# calisto standard simulation no wind solution index 200
TIME = 3.338513236767685
U = [
Expand Down
Loading
Loading