Skip to content

Sensor noise uses the global NumPy RNG and can't be seeded (not reproducible) #1042

Description

@thc1006

I ran into this while driving the IMU/GNSS sensors in a seeded run, and wanted to check whether it's a known limitation before proposing anything.

Current behaviour

The measurement noise for the sensors (white noise and random-walk bias drift in Sensor/InertialSensor/ScalarSensor, and the position/altitude accuracy in GnssReceiver) is drawn from the process-global NumPy RNG: rocketpy/sensors/sensor.py lines 556 and 561 (InertialSensor.apply_noise) and 766/774 (ScalarSensor.apply_noise), and rocketpy/sensors/gnss_receiver.py lines 93-95 (GnssReceiver.measure).

None of the sensor constructors take a seed, so there's no way to make the noise reproducible without reseeding the global numpy.random state by hand. That has two consequences. First, a simulation that's otherwise reproducible still produces different sensor measurements on every run; the rest of the stochastic stack already seeds explicitly (StochasticModel builds np.random.default_rng(seed) and MonteCarlo derives its streams from a SeedSequence), so the sensors are the one place that escapes it. Second, under multiprocess the child workers inherit a copy of the global RNG state, so sensor noise can come out correlated (or seed-independent) across parallel runs.

Minimal example

import numpy as np
from rocketpy.mathutils.vector_matrix import Vector
from rocketpy import Accelerometer

acc = Accelerometer(sampling_rate=100, noise_density=0.1, noise_variance=1.0)
print(acc.apply_noise(Vector([0, 0, 0])))  # changes every run; no `seed` to pin it

The only way to make this repeatable today is np.random.seed(...) on the global state, which also perturbs every other consumer of the global RNG and isn't safe across processes.

Desired behaviour

Each sensor draws its noise from its own numpy.random.Generator, seeded deterministically, so the noise is reproducible for a given seed and independent of the global RNG. This matches what StochasticModel and MonteCarlo already do.

Proposed solution

Add a seed=None argument to the sensor constructors, store self._rng = np.random.default_rng(seed), and replace the np.random.normal(...) calls with self._rng.normal(...). seed=None keeps the current "random by default" behaviour, but per instance, so it no longer reaches into the global RNG. Reproducibility then lives at construction, since a freshly-seeded sensor replays the same sequence. That matches how StochasticModel and MonteCarlo are already seeded (seed once, draw sequentially).

I've prototyped this with unit tests (reproducible from a seed, decorrelated across sensors, independent of the global RNG, and unchanged for the zero-noise ideal case) and would be glad to open a PR if this direction sounds reasonable. Happy to discuss the API first, for example whether the seed should live on the sensor or be threaded down from add_sensor/the rocket.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Fields

    No fields configured for Feature.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions