Source code for pathsim.blocks.noise
#########################################################################################
##
## TIME DOMAIN NOISE SOURCES
## (blocks/noise.py)
##
#########################################################################################
# IMPORTS ===============================================================================
import numpy as np
from ._block import Block
from ..events.schedule import Schedule
# NOISE SOURCE BLOCKS ===================================================================
[docs]
class WhiteNoise(Block):
"""White noise source with uniform spectral density. Samples from
distribution with 'sampling_rate' and holds noise values constant
for time bins (zero-order-hold).
If no 'sampling_rate' (None) is specified, every simulation timestep
gets a new noise value. This is the default setting.
Parameters
----------
spectral_density : float
noise spectral density
noise : float
internal noise value
sampling_rate : float, None
frequency with which the noise is sampled
Attributes
----------
events : list[Schedule]
scheduled event for periodic sampling
"""
#max number of ports
_n_in_max = 0
_n_out_max = 1
#maps for input and output port labels
_port_map_out = {"out": 0}
def __init__(self, spectral_density=1, sampling_rate=None):
super().__init__()
self.spectral_density = spectral_density
self.sampling_rate = sampling_rate
#sampling produces discrete time behavior
if sampling_rate is None:
#initial sample for non-discrete block
self._sample = np.random.rand()
else:
#internal scheduled list event
def _set(t):
self.outputs[0] = self._random(self.sampling_rate)
self.events = [
Schedule(
t_start=0,
t_period=sampling_rate,
func_act=_set
)
]
def __len__(self):
return 0
def _random(self, dt):
"""Generate random sample from scaled normal distribution"""
return np.random.normal(0, 1) * np.sqrt(self.spectral_density/dt)
[docs]
def sample(self, t, dt):
"""Sample from a normal distribution after successful timestep
Parameters
----------
t : float
evaluation time for sampling
dt : float
integration timestep
"""
if self.sampling_rate is None:
self.outputs[0] = self._random(dt)
[docs]
def update(self, t):
"""update system equation for fixed point loop,
here just setting the outputs
Parameters
----------
t : float
evaluation time
"""
pass
[docs]
class PinkNoise(Block):
"""Pink noise (1/f) source using the Voss-McCartney algorithm.
Generates noise with power spectral density inversely proportional to
frequency. Samples from distribution with 'sampling_rate' and holds
noise values constant for time bins (zero-order-hold).
The Voss-McCartney algorithm maintains ``num_octaves`` independent
random values. At each sample n, octaves are selectively updated based
on the binary representation of n:
- Octave 0: updated every sample (when n & 1 == 1)
- Octave 1: updated every 2nd sample (when n & 2 == 2)
- Octave 2: updated every 4th sample (when n & 4 == 4)
- Octave k: updated every :math:`2^k` samples
The pink noise output is the sum of all octaves, scaled to achieve the
desired spectral density:
.. math::
y[n] = \\sqrt{\\frac{S_0}{N \\cdot dt}} \\sum_{k=0}^{N-1} x_k[n]
where :math:`S_0` is the spectral density, :math:`N` is ``num_octaves``,
:math:`dt` is the sampling timestep, and :math:`x_k[n]` are the octave
values (each drawn from :math:`\\mathcal{N}(0, 1)` when updated).
Note
----
If no 'sampling_rate' (None) is specified, every simulation timestep
gets a new noise value. This is the default setting.
Parameters
----------
spectral_density : float
noise spectral density :math:`S_0`
num_octaves : int
number of octaves (levels of randomness), default is 16
sampling_rate : float, None
frequency with which the noise is sampled
Attributes
----------
n_samples : int
internal sample counter
octave_values : array[float]
internal random numbers for octaves in the Voss-McCartney algorithm
events : list[Schedule]
scheduled event for periodic sampling
References
----------
.. [1] Voss, R. F., & Clarke, J. (1978). "1/f noise" in music: Music from
1/f noise. The Journal of the Acoustical Society of America, 63(1),
258-263.
"""
#max number of ports
_n_in_max = 0
_n_out_max = 1
#maps for input and output port labels
_port_map_out = {"out": 0}
def __init__(self, spectral_density=1, num_octaves=16, sampling_rate=None):
super().__init__()
self.spectral_density = spectral_density
self.num_octaves = num_octaves
self.sampling_rate = sampling_rate
self.n_samples = 0
# Initialize the random values for each octave
self.octave_values = np.random.normal(0, 1, self.num_octaves)
#sampling produces discrete time behavior
if sampling_rate is not None:
#internal scheduled list event
def _set(t):
self.outputs[0] = self._random(self.sampling_rate)
self.events = [
Schedule(
t_start=0,
t_period=sampling_rate,
func_act=_set
)
]
def __len__(self):
return 0
[docs]
def reset(self):
super().reset()
#reset counters and octave values
self.n_samples = 0
self.octave_values = np.random.normal(0, 1, self.num_octaves)
def _random(self, dt):
"""Generate a pink noise sample using the Voss-McCartney algorithm.
Uses bitwise operations on the sample counter to determine which
octaves to update. Octave k is updated every :math:`2^k` samples,
creating the characteristic 1/f power spectrum when all octaves
are summed.
Parameters
----------
dt : float
timestep for scaling spectral density
Returns
-------
float
Pink noise sample scaled as :math:`\\sqrt{S_0 / (N \\cdot dt)}`
where :math:`S_0` is spectral density and :math:`N` is num_octaves
"""
# Increment the counter
self.n_samples += 1
# Use bitwise operations to determine which octaves to update
# Find the rightmost zero bit in the binary representation
mask, idx = self.n_samples, 0
while mask & 1 == 0 and idx < self.num_octaves:
mask >>= 1
idx += 1
# Update the selected octave with a new random value
if idx < self.num_octaves:
self.octave_values[idx] = np.random.normal(0, 1)
# Sum the octave values to produce the pink noise sample
pink_sample = np.sum(self.octave_values)
# Scale by sqrt(spectral_density / num_octaves / dt) to maintain
# consistent spectral density across different timesteps
return pink_sample * np.sqrt(self.spectral_density/self.num_octaves/dt)
[docs]
def sample(self, t, dt):
"""Sample pink noise after successful timestep.
Parameters
----------
t : float
evaluation time for sampling
dt : float
integration timestep
"""
if self.sampling_rate is None:
self.outputs[0] = self._random(dt)
[docs]
def update(self, t):
"""update system equation for fixed point loop,
here just setting the outputs
Parameters
----------
t : float
evaluation time
"""
pass