Source code for test_stochasticNoiseGenerator

#
#  ISC License
#
#  Copyright (c) 2026, Autonomous Vehicle Systems Lab, University of Colorado at Boulder
#
#  Permission to use, copy, modify, and/or distribute this software for any
#  purpose with or without fee is hereby granted, provided that the above
#  copyright notice and this permission notice appear in all copies.
#
#  THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
#  WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
#  MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
#  ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
#  WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
#  ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
#  OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
"""
Focused unit tests for the pluggable Gaussian noise generators used by the native
stochastic integrators (``stochasticNoiseGenerator.h``). These exercise the public
behaviors directly, rather than only through an integrator's happy path:

- ``PrescribedGaussianNoiseGenerator``: cursor-based FIFO replay, ``remaining()``/``clear()``,
  omitted-``dZ`` zero-filling, and the error paths (queue exhaustion, mismatched ``dW``
  length, undersized explicit ``dZ``).
- ``RandomGaussianNoiseGenerator``: seed repeatability, Wiener-only draws, and
  ``sqrt(h)`` scaling.
"""
import numpy as np
import numpy.testing as npt
import pytest

from Basilisk.simulation import svIntegrators


[docs] def test_native_output_buffer_methods_are_not_python_apis(): """Expose allocation-bearing return APIs instead of ineffective buffer copies.""" generator = svIntegrators.RandomGaussianNoiseGenerator() assert not hasattr(generator, "generateInto") assert not hasattr(generator, "generateWienerInto") assert len(generator.generateWiener(2, 0.1)) == 2
def _flat(vec): """svIntegrators returns Eigen column vectors as a list of 1-element lists.""" return np.asarray(vec, dtype=float).reshape(-1) def test_prescribed_replaysInFifoOrderWithRemaining(): gen = svIntegrators.PrescribedGaussianNoiseGenerator() assert gen.remaining() == 0 gen.pushStep([0.1, 0.2], [0.3, 0.4]) gen.pushStep([1.1, 1.2], [1.3, 1.4]) assert gen.remaining() == 2 s0 = gen.generate(2, 0.01) npt.assert_allclose(_flat(s0.dW), [0.1, 0.2]) npt.assert_allclose(_flat(s0.dZ), [0.3, 0.4]) assert gen.remaining() == 1 s1 = gen.generate(2, 0.01) npt.assert_allclose(_flat(s1.dW), [1.1, 1.2]) npt.assert_allclose(_flat(s1.dZ), [1.3, 1.4]) assert gen.remaining() == 0 def test_prescribed_omittedDzZeroFills(): gen = svIntegrators.PrescribedGaussianNoiseGenerator() gen.pushStep([1.0, 2.0]) # dZ omitted s = gen.generate(2, 0.01) npt.assert_allclose(_flat(s.dW), [1.0, 2.0]) npt.assert_allclose(_flat(s.dZ), [0.0, 0.0]) # zero-filled to m def test_prescribed_clear(): gen = svIntegrators.PrescribedGaussianNoiseGenerator() gen.pushStep([1.0]) gen.pushStep([2.0]) assert gen.remaining() == 2 gen.clear() assert gen.remaining() == 0 def test_prescribed_queueExhaustionRaises(): gen = svIntegrators.PrescribedGaussianNoiseGenerator() gen.pushStep([1.0]) gen.generate(1, 0.01) # Reading past the end raises rather than crashing the interpreter. with pytest.raises(RuntimeError): gen.generate(1, 0.01) def test_prescribed_mismatchedDwLengthRaises(): gen = svIntegrators.PrescribedGaussianNoiseGenerator() gen.pushStep([1.0, 2.0]) # 2 sources queued with pytest.raises(RuntimeError): gen.generate(1, 0.01) # integrator asks for 1 def test_prescribed_undersizedDzRaises(): gen = svIntegrators.PrescribedGaussianNoiseGenerator() gen.pushStep([1.0, 2.0], [0.5]) # explicit dZ shorter than m = 2 with pytest.raises(RuntimeError): gen.generate(2, 0.01) def test_random_seedRepeatable(): a = svIntegrators.RandomGaussianNoiseGenerator() b = svIntegrators.RandomGaussianNoiseGenerator() a.setSeed(12345) b.setSeed(12345) sa = a.generate(3, 0.04) sb = b.generate(3, 0.04) # Same seed => identical dW and dZ streams. npt.assert_array_equal(_flat(sa.dW), _flat(sb.dW)) npt.assert_array_equal(_flat(sa.dZ), _flat(sb.dZ)) # Different seed => different draw (overwhelmingly likely). b.setSeed(54321) sc = b.generate(3, 0.04) assert not np.allclose(_flat(sa.dW), _flat(sc.dW))
[docs] def test_random_incrementsScaleWithSqrtH(): """Each increment is drawn as sqrt(h)*N(0,1); with a fixed seed, halving h scales the draw by sqrt(1/2) exactly (same underlying standard-normal sequence).""" g = svIntegrators.RandomGaussianNoiseGenerator() g.setSeed(7) big = _flat(g.generate(4, 1.0).dW) g.setSeed(7) small = _flat(g.generate(4, 0.25).dW) # sqrt(0.25) = 0.5 npt.assert_allclose(small, 0.5 * big, rtol=1e-12)
def test_random_wienerOnlySkipsAuxiliaryDraws(): first = svIntegrators.RandomGaussianNoiseGenerator() second = svIntegrators.RandomGaussianNoiseGenerator() first.setSeed(4321) second.setSeed(4321) first_step = _flat(first.generateWiener(3, 0.25)) second_step = _flat(first.generateWiener(3, 0.25)) npt.assert_array_equal( first_step, _flat(second.generateWiener(3, 0.25)) ) npt.assert_array_equal( second_step, _flat(second.generateWiener(3, 0.25)) ) def test_random_reseedResetsCachedNormalVariate(): generator = svIntegrators.RandomGaussianNoiseGenerator() reference = svIntegrators.RandomGaussianNoiseGenerator() generator.setSeed(2468) reference.setSeed(2468) first = _flat(generator.generateWiener(1, 0.25)) generator.setSeed(2468) repeated = _flat(generator.generateWiener(1, 0.25)) expected = _flat(reference.generateWiener(1, 0.25)) npt.assert_array_equal(first, expected) npt.assert_array_equal(repeated, expected) if __name__ == "__main__": pytest.main([__file__, "-v"])