Skip to content

Sensors API

Sensors extract a physical response at one system coordinate, then apply their measurement effects in declaration order. The clean and measured channels are both retained in simulation results.

from dynaresp import sensors

sensor = sensors.Accelerometer(
    position=2.0,
    effects=[sensors.GaussianNoise(standard_deviation=0.01)],
)

See the sensors guide for coordinate behavior, effect ordering, and reproducible noise.

Shared interfaces

Sensor

Sensor(*, position, noise=None, effects=())

Measure one kinematic quantity at one physical location or DOF.

Parameters:

Name Type Description Default
position float

Physical coordinate for a continuous system, or an integer-valued DOF index for a discrete system.

required
noise MeasurementEffect | None

Optional noise effect appended after every item in effects. This is a convenience argument; noise may also be placed explicitly in effects when another ordering is required.

None
effects Sequence[MeasurementEffect]

Measurement effects applied left-to-right after the clean structural response has been sampled.

()

Effects alter only the reported measurement, never the solved structural response.

Source code in src/dynaresp/sensors/base.py
def __init__(
    self,
    *,
    position: float,
    noise: MeasurementEffect | None = None,
    effects: Sequence[MeasurementEffect] = (),
) -> None:
    resolved_position = float(position)
    if not np.isfinite(resolved_position):
        raise ValueError("position must be finite")
    resolved = tuple(effects)
    if noise is not None:
        resolved = (*resolved, noise)
    if any(not isinstance(effect, MeasurementEffect) for effect in resolved):
        raise TypeError("effects must contain MeasurementEffect instances")
    self.position = resolved_position
    self.effects = resolved

measure_clean

measure_clean(system, response)

Map a generalized response to one clean physical signal.

response must have shape (system.ndof, n_time). The returned signal has shape (n_time,) and is not passed through measurement effects.

Source code in src/dynaresp/sensors/base.py
def measure_clean(
    self,
    system: ObservationModel,
    response: FloatArray,
) -> FloatArray:
    """Map a generalized response to one clean physical signal.

    ``response`` must have shape ``(system.ndof, n_time)``. The returned
    signal has shape ``(n_time,)`` and is not passed through measurement
    effects.
    """
    vector = system.observation_vector(self.position)
    return np.asarray(vector @ response, dtype=np.float64)

apply_effects

apply_effects(clean_signal, rng)

Apply configured effects in order to a copy of clean_signal.

One deterministic child stream is derived from rng for each effect, so stochastic effects do not share generator state. The supplied generator advances, while the input signal remains unchanged.

Source code in src/dynaresp/sensors/base.py
def apply_effects(
    self,
    clean_signal: FloatArray,
    rng: np.random.Generator,
) -> FloatArray:
    """Apply configured effects in order to a copy of ``clean_signal``.

    One deterministic child stream is derived from ``rng`` for each effect,
    so stochastic effects do not share generator state. The supplied
    generator advances, while the input signal remains unchanged.
    """
    measured = clean_signal.copy()
    effect_seeds = rng.integers(
        0,
        np.iinfo(np.uint64).max,
        len(self.effects),
        dtype=np.uint64,
    )
    for effect, seed in zip(self.effects, effect_seeds, strict=True):
        measured = effect.apply(measured, np.random.default_rng(int(seed)))
    return measured

to_config

to_config()

Return a JSON-compatible sensor and effect description.

Source code in src/dynaresp/sensors/base.py
def to_config(self) -> dict[str, object]:
    """Return a JSON-compatible sensor and effect description."""
    return {
        "type": type(self).__name__,
        "quantity": self.quantity,
        "position": self.position,
        "effects": [effect.to_config() for effect in self.effects],
    }

MeasurementEffect

Bases: ABC

Transform a clean sensor signal without altering structural response.

Implementations receive a one-dimensional (n_time,) signal and a dedicated NumPy generator, and must return a signal of the same shape. Effects should not mutate their input. A :class:~dynaresp.sensors.Sensor composes them in declaration order.

apply abstractmethod

apply(signal, rng)

Return the transformed signal, using rng if stochastic.

Source code in src/dynaresp/sensors/effects.py
@abstractmethod
def apply(
    self,
    signal: FloatArray,
    rng: np.random.Generator,
) -> FloatArray:
    """Return the transformed signal, using ``rng`` if stochastic."""

to_config

to_config()

Return a JSON-compatible effect description.

Source code in src/dynaresp/sensors/effects.py
def to_config(self) -> dict[str, object]:
    """Return a JSON-compatible effect description."""
    if not is_dataclass(self):
        return {"type": type(self).__name__}
    return {
        "type": type(self).__name__,
        **asdict(self),
    }

Sensor types

Accelerometer

Accelerometer(*, position, noise=None, effects=())

Bases: Sensor

Measure physical acceleration in system position units per second².

Source code in src/dynaresp/sensors/base.py
def __init__(
    self,
    *,
    position: float,
    noise: MeasurementEffect | None = None,
    effects: Sequence[MeasurementEffect] = (),
) -> None:
    resolved_position = float(position)
    if not np.isfinite(resolved_position):
        raise ValueError("position must be finite")
    resolved = tuple(effects)
    if noise is not None:
        resolved = (*resolved, noise)
    if any(not isinstance(effect, MeasurementEffect) for effect in resolved):
        raise TypeError("effects must contain MeasurementEffect instances")
    self.position = resolved_position
    self.effects = resolved

VelocitySensor

VelocitySensor(*, position, noise=None, effects=())

Bases: Sensor

Measure physical velocity in system position units per second.

Source code in src/dynaresp/sensors/base.py
def __init__(
    self,
    *,
    position: float,
    noise: MeasurementEffect | None = None,
    effects: Sequence[MeasurementEffect] = (),
) -> None:
    resolved_position = float(position)
    if not np.isfinite(resolved_position):
        raise ValueError("position must be finite")
    resolved = tuple(effects)
    if noise is not None:
        resolved = (*resolved, noise)
    if any(not isinstance(effect, MeasurementEffect) for effect in resolved):
        raise TypeError("effects must contain MeasurementEffect instances")
    self.position = resolved_position
    self.effects = resolved

DisplacementSensor

DisplacementSensor(*, position, noise=None, effects=())

Bases: Sensor

Measure physical displacement in the system's position units.

Source code in src/dynaresp/sensors/base.py
def __init__(
    self,
    *,
    position: float,
    noise: MeasurementEffect | None = None,
    effects: Sequence[MeasurementEffect] = (),
) -> None:
    resolved_position = float(position)
    if not np.isfinite(resolved_position):
        raise ValueError("position must be finite")
    resolved = tuple(effects)
    if noise is not None:
        resolved = (*resolved, noise)
    if any(not isinstance(effect, MeasurementEffect) for effect in resolved):
        raise TypeError("effects must contain MeasurementEffect instances")
    self.position = resolved_position
    self.effects = resolved

Measurement effects

GaussianNoise dataclass

GaussianNoise(snr_db=None, standard_deviation=None)

Bases: MeasurementEffect

Add independent zero-mean Gaussian measurement noise.

Parameters:

Name Type Description Default
snr_db float | None

Desired signal-to-noise amplitude ratio in decibels. Noise standard deviation is rms(signal) / 10**(snr_db / 20).

None
standard_deviation float | None

Absolute noise standard deviation in the signal's units.

None

Exactly one argument is required. SNR mode adapts to each clean signal; a zero signal consequently receives zero noise. Random samples come from the generator supplied by the sensor.

apply

apply(signal, rng)

Return a noisy copy using absolute or signal-relative scale.

Source code in src/dynaresp/sensors/effects.py
def apply(self, signal: FloatArray, rng: np.random.Generator) -> FloatArray:
    """Return a noisy copy using absolute or signal-relative scale."""
    if self.standard_deviation is None:
        rms = float(np.sqrt(np.mean(np.square(signal))))
        assert self.snr_db is not None
        standard_deviation = rms / (10.0 ** (self.snr_db / 20.0))
    else:
        standard_deviation = self.standard_deviation
    return np.asarray(
        signal + rng.normal(0.0, standard_deviation, signal.shape),
        dtype=np.float64,
    )

Bias dataclass

Bias(value)

Bases: MeasurementEffect

Add a constant offset in the measured signal's units.

Parameters:

Name Type Description Default
value float

Offset added to every sample.

required

apply

apply(signal, rng)

Return a copy with value added to every sample.

Source code in src/dynaresp/sensors/effects.py
def apply(self, signal: FloatArray, rng: np.random.Generator) -> FloatArray:
    """Return a copy with ``value`` added to every sample."""
    del rng
    return np.asarray(signal + self.value, dtype=np.float64)

Sensitivity dataclass

Sensitivity(factor)

Bases: MeasurementEffect

Apply a dimensionless multiplicative calibration factor.

Parameters:

Name Type Description Default
factor float

Value by which every sample is multiplied.

required

apply

apply(signal, rng)

Return a copy scaled by the calibration factor.

Source code in src/dynaresp/sensors/effects.py
def apply(self, signal: FloatArray, rng: np.random.Generator) -> FloatArray:
    """Return a copy scaled by the calibration ``factor``."""
    del rng
    return np.asarray(signal * self.factor, dtype=np.float64)

Clipping dataclass

Clipping(minimum, maximum)

Bases: MeasurementEffect

Clip measurements to a closed sensor range.

Parameters:

Name Type Description Default
minimum float

Lower limit in the measured signal's units.

required
maximum float

Upper limit in the measured signal's units; it must be greater than minimum.

required

apply

apply(signal, rng)

Return a copy limited to [minimum, maximum].

Source code in src/dynaresp/sensors/effects.py
def apply(self, signal: FloatArray, rng: np.random.Generator) -> FloatArray:
    """Return a copy limited to ``[minimum, maximum]``."""
    del rng
    return np.asarray(np.clip(signal, self.minimum, self.maximum), dtype=np.float64)

Quantization dataclass

Quantization(bits, full_scale)

Bases: MeasurementEffect

Uniformly quantize a clipped, symmetric full-scale range.

Parameters:

Name Type Description Default
bits int

Positive converter bit depth, producing 2**bits levels.

required
full_scale float

Positive magnitude of the symmetric range [-full_scale, full_scale] in signal units.

required

apply

apply(signal, rng)

Return the uniformly quantized, full-scale-clipped signal.

Source code in src/dynaresp/sensors/effects.py
def apply(self, signal: FloatArray, rng: np.random.Generator) -> FloatArray:
    """Return the uniformly quantized, full-scale-clipped signal."""
    del rng
    levels = 2**self.bits - 1
    clipped = np.clip(signal, -self.full_scale, self.full_scale)
    normalized = (clipped + self.full_scale) / (2.0 * self.full_scale)
    quantized = np.round(normalized * levels) / levels
    return np.asarray(
        quantized * (2.0 * self.full_scale) - self.full_scale,
        dtype=np.float64,
    )

Dropout dataclass

Dropout(probability, value=0.0)

Bases: MeasurementEffect

Replace independently selected samples by a configured value.

Parameters:

Name Type Description Default
probability float

Per-sample replacement probability in [0, 1].

required
value float

Replacement value in signal units. NaN is accepted to mark missing data; infinities are rejected.

0.0

Selection uses the generator assigned to this effect by the sensor.

apply

apply(signal, rng)

Return a copy with independently selected samples replaced.

Source code in src/dynaresp/sensors/effects.py
def apply(self, signal: FloatArray, rng: np.random.Generator) -> FloatArray:
    """Return a copy with independently selected samples replaced."""
    measured = signal.copy()
    measured[rng.random(signal.shape) < self.probability] = self.value
    return measured