From 4bf026650f4dd643b89cbc7d94fe8157ae4694ce Mon Sep 17 00:00:00 2001 From: Legend101Zz <96632943+Legend101Zz@users.noreply.github.com> Date: Wed, 17 Dec 2025 18:53:20 +0530 Subject: [PATCH 01/16] add: Extract RandomGenerator class into a standalone header file --- .../cpp_standalone/brianlib/randomgenerator.h | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 brian2/devices/cpp_standalone/brianlib/randomgenerator.h diff --git a/brian2/devices/cpp_standalone/brianlib/randomgenerator.h b/brian2/devices/cpp_standalone/brianlib/randomgenerator.h new file mode 100644 index 000000000..d75107e69 --- /dev/null +++ b/brian2/devices/cpp_standalone/brianlib/randomgenerator.h @@ -0,0 +1,132 @@ +#ifndef _BRIAN_RANDOMGENERATOR_H +#define _BRIAN_RANDOMGENERATOR_H + +#include +#include +#include + +/** + * @brief Random number generator class that provides reproducible + * random sequences across different Brian2 backends. + * + * Uses std::mt19937 (Mersenne Twister) with the Jean-Sebastien Roy + * algorithm for converting to uniform doubles, ensuring identical + * sequences when seeded with the same value. + * + * This class is used by both C++ standalone mode and Cython runtime mode + * to ensure cross-backend reproducibility. + */ +class RandomGenerator +{ +private: + std::mt19937 gen; + double stored_gauss; + bool has_stored_gauss; + +public: + RandomGenerator() : has_stored_gauss(false) + { + seed(); + } + + /** + * @brief Seed with a random value from the system. + */ + void seed() + { + std::random_device rd; + gen.seed(rd()); + has_stored_gauss = false; + } + + /** + * @brief Seed with a specific value for reproducibility. + * @param seed The seed value. + */ + void seed(unsigned long seed_value) + { + gen.seed(seed_value); + has_stored_gauss = false; + } + + /** + * @brief Generate a uniform random double in [0, 1) + * + * Uses the Jean-Sebastien Roy algorithm to extract 53 bits + * of randomness from two consecutive MT19937 outputs. + * This ensures reproducibility with older Brian2 versions + * and across different backends. + * + * The algorithm: + * - Takes two 32-bit outputs from MT19937 + * - Extracts 27 bits from the first (shift right 5) + * - Extracts 26 bits from the second (shift right 6) + * - Combines them into a 53-bit integer (full double precision mantissa) + * - Divides by 2^53 to get a value in [0, 1) + */ + double rand() + { + /* shifts : 67108864 = 0x4000000 = 2^26 + * 9007199254740992 = 0x20000000000000 = 2^53 */ + const long a = gen() >> 5; // Upper 27 bits + const long b = gen() >> 6; // Upper 26 bits + return (a * 67108864.0 + b) / 9007199254740992.0; + } + + /** + * @brief Generate a standard normal random double (mean=0, std=1) + * + * Uses the Box-Muller transform with rejection sampling. + * Generates two values at once and caches one for the next call, + * making every other call essentially free. + */ + double randn() + { + if (has_stored_gauss) + { + const double tmp = stored_gauss; + has_stored_gauss = false; + return tmp; + } + else + { + double f, x1, x2, r2; + + do + { + x1 = 2.0 * rand() - 1.0; + x2 = 2.0 * rand() - 1.0; + r2 = x1 * x1 + x2 * x2; + } while (r2 >= 1.0 || r2 == 0.0); + + /* Box-Muller transform */ + f = sqrt(-2.0 * log(r2) / r2); + /* Keep for next call */ + stored_gauss = f * x1; + has_stored_gauss = true; + return f * x2; + } + } + + // Allow exporting/setting the internal state of the random generator + friend std::ostream &operator<<(std::ostream &out, const RandomGenerator &rng); + friend std::istream &operator>>(std::istream &in, RandomGenerator &rng); +}; + +/** + * @brief Stream output operator for serializing generator state. + */ +inline std::ostream &operator<<(std::ostream &out, const RandomGenerator &rng) +{ + return out << rng.gen; +} + +/** + * @brief Stream input operator for deserializing generator state. + */ +inline std::istream &operator>>(std::istream &in, RandomGenerator &rng) +{ + return in >> rng.gen; +} + +#endif // _BRIAN_RANDOMGENERATOR_H From 26b2eb6f31f44ce81f540f7b0ac009c46345b080 Mon Sep 17 00:00:00 2001 From: Legend101Zz <96632943+Legend101Zz@users.noreply.github.com> Date: Wed, 17 Dec 2025 18:56:51 +0530 Subject: [PATCH 02/16] fix: modify object.cpp template to use new rng header --- .../cpp_standalone/templates/objects.cpp | 70 +------------------ 1 file changed, 2 insertions(+), 68 deletions(-) diff --git a/brian2/devices/cpp_standalone/templates/objects.cpp b/brian2/devices/cpp_standalone/templates/objects.cpp index 1762089e9..1a78230f9 100644 --- a/brian2/devices/cpp_standalone/templates/objects.cpp +++ b/brian2/devices/cpp_standalone/templates/objects.cpp @@ -19,6 +19,7 @@ set_variable_from_value(name, {{array_name}}, var_size, (char)atoi(s_value.c_str #include "brianlib/clocks.h" #include "brianlib/dynamic_array.h" #include "brianlib/stdint_compat.h" +#include "brianlib/randomgenerator.h" #include "network.h" #include #include @@ -37,16 +38,6 @@ std::string results_dir = "results/"; // can be overwritten by --results_dir co // For multhreading, we need one generator for each thread. std::vector< RandomGenerator > _random_generators; -std::ostream& operator<<(std::ostream& out, const RandomGenerator& rng) -{ - return out << rng.gen; -} - -std::istream& operator>>(std::istream& in, RandomGenerator& rng) -{ - return in >> rng.gen; -} - //////////////// networks ///////////////// {% for net in networks | sort(attribute='name') %} Network {{net.name}}; @@ -413,6 +404,7 @@ void _dealloc_arrays() #include "brianlib/clocks.h" #include "brianlib/dynamic_array.h" #include "brianlib/stdint_compat.h" +#include "brianlib/randomgenerator.h" #include "network.h" #include #include @@ -423,64 +415,6 @@ namespace brian { extern std::string results_dir; -class RandomGenerator { - private: - std::mt19937 gen; - double stored_gauss; - bool has_stored_gauss = false; - public: - RandomGenerator() { - seed(); - } - void seed() { - std::random_device rd; - gen.seed(rd()); - has_stored_gauss = false; - } - void seed(unsigned long seed) { - gen.seed(seed); - has_stored_gauss = false; - } - // Allow exporting/setting the internal state of the random generator - friend std::ostream& operator<<(std::ostream& out, const RandomGenerator& rng); - friend std::istream& operator>>(std::istream& in, RandomGenerator& rng); - - double rand() { - /* shifts : 67108864 = 0x4000000, 9007199254740992 = 0x20000000000000 */ - const long a = gen() >> 5; - const long b = gen() >> 6; - return (a * 67108864.0 + b) / 9007199254740992.0; - } - - double randn() { - if (has_stored_gauss) { - const double tmp = stored_gauss; - has_stored_gauss = false; - return tmp; - } - else { - double f, x1, x2, r2; - - do { - x1 = 2.0*rand() - 1.0; - x2 = 2.0*rand() - 1.0; - r2 = x1*x1 + x2*x2; - } - while (r2 >= 1.0 || r2 == 0.0); - - /* Box-Muller transform */ - f = sqrt(-2.0*log(r2)/r2); - /* Keep for next call */ - stored_gauss = f*x1; - has_stored_gauss = true; - return f*x2; - } - } -}; - -extern std::ostream& operator<<(std::ostream& out, const RandomGenerator& rng); -extern std::istream& operator>>(std::istream& in, RandomGenerator& rng); - // In OpenMP we need one state per thread extern std::vector< RandomGenerator > _random_generators; From 45d0d2f969aebf9a2a1809a0a66007d4c897a8cd Mon Sep 17 00:00:00 2001 From: Legend101Zz <96632943+Legend101Zz@users.noreply.github.com> Date: Wed, 17 Dec 2025 20:06:54 +0530 Subject: [PATCH 03/16] fix: modify common_group.pyx --- .../codegen/runtime/cython_rt/templates/common_group.pyx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/brian2/codegen/runtime/cython_rt/templates/common_group.pyx b/brian2/codegen/runtime/cython_rt/templates/common_group.pyx index b9ea2572e..7fac14b57 100644 --- a/brian2/codegen/runtime/cython_rt/templates/common_group.pyx +++ b/brian2/codegen/runtime/cython_rt/templates/common_group.pyx @@ -69,6 +69,15 @@ cdef extern from "dynamic_array.h": size_t rows() size_t cols() size_t stride() + +# RandomGenerator C++ interface declaration for random number generation +cdef extern from "randomgenerator.h": + cdef cppclass RandomGenerator: + RandomGenerator() except + + void seed() except + + void seed(unsigned long) except + + double rand() nogil + double randn() nogil {% endmacro %} {% macro before_run() %} From 07a800b5815766e99f94e1579bdb476c79b9e44f Mon Sep 17 00:00:00 2001 From: Legend101Zz <96632943+Legend101Zz@users.noreply.github.com> Date: Wed, 17 Dec 2025 20:12:30 +0530 Subject: [PATCH 04/16] fix: modify common_group.pyx minor change --- brian2/codegen/runtime/cython_rt/templates/common_group.pyx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/brian2/codegen/runtime/cython_rt/templates/common_group.pyx b/brian2/codegen/runtime/cython_rt/templates/common_group.pyx index 7fac14b57..218d2801a 100644 --- a/brian2/codegen/runtime/cython_rt/templates/common_group.pyx +++ b/brian2/codegen/runtime/cython_rt/templates/common_group.pyx @@ -17,6 +17,7 @@ cdef extern from "math.h": from libc.stdlib cimport abs # For integers from libc.math cimport abs # For floating point values from libc.limits cimport INT_MIN, INT_MAX +from libc.stdint cimport uintptr_t from libcpp cimport bool from libcpp.set cimport set from cython.operator cimport dereference as _deref, preincrement as _preinc @@ -78,6 +79,7 @@ cdef extern from "randomgenerator.h": void seed(unsigned long) except + double rand() nogil double randn() nogil + {% endmacro %} {% macro before_run() %} From 4d9b9bc4ef0d1491adeefca461b0ed3a41f14a31 Mon Sep 17 00:00:00 2001 From: Legend101Zz <96632943+Legend101Zz@users.noreply.github.com> Date: Wed, 17 Dec 2025 20:13:01 +0530 Subject: [PATCH 05/16] refactor: add cpp rng to cython runtime --- brian2/codegen/generators/cython_generator.py | 64 ++++++++++--------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/brian2/codegen/generators/cython_generator.py b/brian2/codegen/generators/cython_generator.py index baf6000b3..bddeab6b4 100644 --- a/brian2/codegen/generators/cython_generator.py +++ b/brian2/codegen/generators/cython_generator.py @@ -555,30 +555,40 @@ def determine_keywords(self): name="_exprel", availability_check=C99Check("exprel"), ) -_BUFFER_SIZE = 20000 + +# ============================================================================== +# Random Number Generation +# ============================================================================== +# We use the C++ RandomGenerator class (from randomgenerator.h) to ensure +# identical random sequences between Cython runtime and C++ standalone modes. +# The RandomGenerator is instantiated once per code object and seeded via +# the namespace mechanism. rand_code = """ -cdef double _rand(int _idx): - cdef double **buffer_pointer = _namespace_rand_buffer - cdef double *buffer = buffer_pointer[0] - cdef _numpy.ndarray _new_rand - - if(_namespace_rand_buffer_index[0] == 0): - if buffer != NULL: - free(buffer) - _new_rand = _numpy.random.rand(_BUFFER_SIZE) - buffer = _numpy.PyArray_DATA(_new_rand) - PyArray_CLEARFLAGS(<_numpy.PyArrayObject*>_new_rand, _numpy.NPY_ARRAY_OWNDATA) - buffer_pointer[0] = buffer - - cdef double val = buffer[_namespace_rand_buffer_index[0]] - _namespace_rand_buffer_index[0] += 1 - if _namespace_rand_buffer_index[0] == _BUFFER_SIZE: - _namespace_rand_buffer_index[0] = 0 - return val -""".replace("_BUFFER_SIZE", str(_BUFFER_SIZE)) - -randn_code = rand_code.replace("rand", "randn").replace("randnom", "random") +# Module-level RandomGenerator instance +# This will be created once when the module is first imported +cdef RandomGenerator* _brian_random_generator = new RandomGenerator() + +cdef double _rand(int _idx) nogil: + return _brian_random_generator.rand() + +def _seed_random_generator(seed=None): + '''Seed the random generator. Called from Python.''' + global _brian_random_generator + if seed is None: + _brian_random_generator.seed() + else: + _brian_random_generator.seed(seed) + +def _get_random_generator_ptr(): + '''Return the address of the random generator for state management.''' + return _brian_random_generator +""" + +randn_code = """ +cdef double _randn(int _idx) nogil: + return _brian_random_generator.randn() +""" poisson_code = """ cdef double _loggam(double x): @@ -665,20 +675,14 @@ def determine_keywords(self): CythonCodeGenerator, code=rand_code, name="_rand", - namespace={ - "_rand_buffer": device.rand_buffer, - "_rand_buffer_index": device.rand_buffer_index, - }, + namespace={}, ) DEFAULT_FUNCTIONS["randn"].implementations.add_implementation( CythonCodeGenerator, code=randn_code, name="_randn", - namespace={ - "_randn_buffer": device.randn_buffer, - "_randn_buffer_index": device.randn_buffer_index, - }, + namespace={}, ) DEFAULT_FUNCTIONS["poisson"].implementations.add_implementation( CythonCodeGenerator, From e5df9fe3a69b7b9158e52a9653fd88fe370e5434 Mon Sep 17 00:00:00 2001 From: Legend101Zz <96632943+Legend101Zz@users.noreply.github.com> Date: Wed, 17 Dec 2025 20:19:58 +0530 Subject: [PATCH 06/16] refactor: Update RuntimeDevice to handle seeding for the new approach --- brian2/devices/device.py | 45 ++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/brian2/devices/device.py b/brian2/devices/device.py index ea6298757..23cfba50a 100644 --- a/brian2/devices/device.py +++ b/brian2/devices/device.py @@ -479,23 +479,24 @@ def __init__(self): #: objects). Arrays in this dictionary will disappear as soon as the #: last reference to the `Variable` object used as a key is gone self.arrays = WeakKeyDictionary() - # Note that the buffers only store a pointer to the actual random - # numbers -- the buffer will be filled in Cython code - self.randn_buffer = np.zeros(1, dtype=np.intp) - self.rand_buffer = np.zeros(1, dtype=np.intp) - self.randn_buffer_index = np.zeros(1, dtype=np.int32) - self.rand_buffer_index = np.zeros(1, dtype=np.int32) + # Store the seed value for seeding newly compiled code object + self._seed_value = None + # Track compiled modules that have been seeded + self._seeded_modules = WeakKeyDictionary() def __getstate__(self): state = dict(self.__dict__) # Python's pickle module cannot pickle a WeakKeyDictionary, we therefore # convert it to a standard dictionary state["arrays"] = dict(self.arrays) + # Don't try to pickle the seeded modules + state["_seeded_modules"] = {} return state def __setstate__(self, state): self.__dict__ = state self.__dict__["arrays"] = WeakKeyDictionary(self.__dict__["arrays"]) + self.__dict__["_seeded_modules"] = WeakKeyDictionary() def get_array_name(self, var, access_data=True): # if no owner is set, this is a temporary object (e.g. the array @@ -594,25 +595,37 @@ def seed(self, seed=None): The seed value for the random number generator, or ``None`` (the default) to set a random seed. """ + # Store the seed value - it will be used to seed any RandomGenerator + # instances in compiled Cython code + self._seed_value = seed + # Also seed numpy for any code that might still use it directly np.random.seed(seed) - self.rand_buffer_index[:] = 0 - self.randn_buffer_index[:] = 0 + # Clear the seeded modules so they get re-seeded on next use + self._seeded_modules = WeakKeyDictionary() def get_random_state(self): + """ + Return a representation of the current random number generator state. + + Note: With the RandomGenerator, full state restoration requires + access to the compiled modules. This method returns what can be saved, + but full restoration may not be possible if modules have been recompiled. + """ return { "numpy_state": np.random.get_state(), - "rand_buffer_index": np.array(self.rand_buffer_index), - "rand_buffer": np.array(self.rand_buffer), - "randn_buffer_index": np.array(self.randn_buffer_index), - "randn_buffer": np.array(self.randn_buffer), + "seed_value": self._seed_value, } def set_random_state(self, state): + """ + Reset the random number generator state to a previously stored state. + + Note: This restores the seed value and numpy state. The actual + RandomGenerator states in compiled modules may not be fully restorable. + """ np.random.set_state(state["numpy_state"]) - self.rand_buffer_index[:] = state["rand_buffer_index"] - self.rand_buffer[:] = state["rand_buffer"] - self.randn_buffer_index[:] = state["randn_buffer_index"] - self.randn_buffer[:] = state["randn_buffer"] + self._seed_value = state.get("seed_value") + self._seeded_modules = WeakKeyDictionary() class Dummy: From 8801d40fc4e9f556e4aee70d79346096c69563f1 Mon Sep 17 00:00:00 2001 From: Legend101Zz <96632943+Legend101Zz@users.noreply.github.com> Date: Wed, 17 Dec 2025 20:25:59 +0530 Subject: [PATCH 07/16] feat: run_block to add seeding logic in codeobject if needed --- brian2/codegen/runtime/cython_rt/cython_rt.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/brian2/codegen/runtime/cython_rt/cython_rt.py b/brian2/codegen/runtime/cython_rt/cython_rt.py index a48642c9a..f91c16c86 100644 --- a/brian2/codegen/runtime/cython_rt/cython_rt.py +++ b/brian2/codegen/runtime/cython_rt/cython_rt.py @@ -195,6 +195,8 @@ def compile_block(self, block): def run_block(self, block): compiled_code = self.compiled_code[block] if compiled_code: + # Seed the random generator if this module uses it + self._seed_if_needed(compiled_code) try: return compiled_code.main(self.namespace) except Exception as exc: @@ -213,6 +215,28 @@ def _insert_func_namespace(self, func): for dep in impl.dependencies.values(): self._insert_func_namespace(dep) + def _seed_if_needed(self, compiled_code): + """Seed the random generator in the compiled module if needed.""" + from brian2.devices.device import get_device + + device = get_device() + + # Check if this module has the seeding function and hasn't been seeded yet + if hasattr(compiled_code, "_seed_random_generator"): + # Use id() to track which modules have been seeded + # with the current seed value + module_id = id(compiled_code) + seed_value = device._seed_value + + # Check if we need to seed (new module or seed changed) + if not hasattr(device, "_seeded_modules"): + device._seeded_modules = {} + + current_seed_marker = (seed_value, id(device)) + if device._seeded_modules.get(module_id) != current_seed_marker: + compiled_code._seed_random_generator(seed_value) + device._seeded_modules[module_id] = current_seed_marker + def variables_to_namespace(self): # Variables can refer to values that are either constant (e.g. dt) # or change every timestep (e.g. t). We add the values of the From a875c0985be31906a94aa2965cf96ff79f9c23d6 Mon Sep 17 00:00:00 2001 From: Legend101Zz <96632943+Legend101Zz@users.noreply.github.com> Date: Wed, 17 Dec 2025 22:50:52 +0530 Subject: [PATCH 08/16] fix: deploy --- brian2/devices/device.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/brian2/devices/device.py b/brian2/devices/device.py index 23cfba50a..b199a5890 100644 --- a/brian2/devices/device.py +++ b/brian2/devices/device.py @@ -482,7 +482,7 @@ def __init__(self): # Store the seed value for seeding newly compiled code object self._seed_value = None # Track compiled modules that have been seeded - self._seeded_modules = WeakKeyDictionary() + self._seeded_modules = {} def __getstate__(self): state = dict(self.__dict__) @@ -496,7 +496,7 @@ def __getstate__(self): def __setstate__(self, state): self.__dict__ = state self.__dict__["arrays"] = WeakKeyDictionary(self.__dict__["arrays"]) - self.__dict__["_seeded_modules"] = WeakKeyDictionary() + self.__dict__["_seeded_modules"] = {} def get_array_name(self, var, access_data=True): # if no owner is set, this is a temporary object (e.g. the array @@ -601,7 +601,7 @@ def seed(self, seed=None): # Also seed numpy for any code that might still use it directly np.random.seed(seed) # Clear the seeded modules so they get re-seeded on next use - self._seeded_modules = WeakKeyDictionary() + self._seeded_modules = {} def get_random_state(self): """ @@ -625,7 +625,7 @@ def set_random_state(self, state): """ np.random.set_state(state["numpy_state"]) self._seed_value = state.get("seed_value") - self._seeded_modules = WeakKeyDictionary() + self._seeded_modules = {} class Dummy: From eed093fdbfb204427fba1048fde212bddb88f11d Mon Sep 17 00:00:00 2001 From: Legend101Zz <96632943+Legend101Zz@users.noreply.github.com> Date: Wed, 21 Jan 2026 18:59:37 +0530 Subject: [PATCH 09/16] feat: add cython wrapper for RNG --- .../cpp_standalone/brianlib/randomgenerator.h | 29 +++++- brian2/random/cythonrng.pxd | 5 + brian2/random/cythonrng.pyx | 94 +++++++++++++++++++ 3 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 brian2/random/cythonrng.pxd create mode 100644 brian2/random/cythonrng.pyx diff --git a/brian2/devices/cpp_standalone/brianlib/randomgenerator.h b/brian2/devices/cpp_standalone/brianlib/randomgenerator.h index d75107e69..54d928492 100644 --- a/brian2/devices/cpp_standalone/brianlib/randomgenerator.h +++ b/brian2/devices/cpp_standalone/brianlib/randomgenerator.h @@ -1,9 +1,12 @@ #ifndef _BRIAN_RANDOMGENERATOR_H #define _BRIAN_RANDOMGENERATOR_H +#include #include #include #include +#include +#include /** * @brief Random number generator class that provides reproducible @@ -108,6 +111,28 @@ class RandomGenerator } } + /** + * @brief Get the complete internal state as a string + * + * This captures the MT19937 state plus the Box-Muller cache, + * allowing exact restoration of the generator state. + */ + std::string get_state() const { + std::ostringstream oss; + oss << gen << " " << stored_gauss << " " << has_stored_gauss; + return oss.str(); + } + + /** + * @brief Set the complete internal state from a string. + * + * Restores the exact state previously captured by get_state(). + */ + void set_state(const std::string& state){ + std::istringstream iss(state); + iss >> gen >> stored_gauss >> has_stored_gauss; + } + // Allow exporting/setting the internal state of the random generator friend std::ostream &operator<<(std::ostream &out, const RandomGenerator &rng); friend std::istream &operator>>(std::istream &in, RandomGenerator &rng); @@ -118,7 +143,7 @@ class RandomGenerator */ inline std::ostream &operator<<(std::ostream &out, const RandomGenerator &rng) { - return out << rng.gen; + return out << rng.gen << " " << rng.stored_gauss << " " << rng.has_stored_gauss; } /** @@ -126,7 +151,7 @@ inline std::ostream &operator<<(std::ostream &out, const RandomGenerator &rng) */ inline std::istream &operator>>(std::istream &in, RandomGenerator &rng) { - return in >> rng.gen; + return in >> rng.gen >> rng.stored_gauss >> rng.has_stored_gauss; } #endif // _BRIAN_RANDOMGENERATOR_H diff --git a/brian2/random/cythonrng.pxd b/brian2/random/cythonrng.pxd new file mode 100644 index 000000000..16a6db826 --- /dev/null +++ b/brian2/random/cythonrng.pxd @@ -0,0 +1,5 @@ +# Cython declaration file for the global random number generator +# Other Cython modules can use: from brian2.random.cythonrng cimport _rand, _randn + +cdef double _rand() noexcept nogil +cdef double _randn() noexcept nogil diff --git a/brian2/random/cythonrng.pyx b/brian2/random/cythonrng.pyx new file mode 100644 index 000000000..ae82ebfcb --- /dev/null +++ b/brian2/random/cythonrng.pyx @@ -0,0 +1,94 @@ +# cython: language_level=3 +# distutils: language = c++ +# distutils: include_dirs = brian2/devices/cpp_standalone/brianlib + +""" +Global random number generator for Brian2's Cython runtime. + +This module provides a single global RandomGenerator instance that is shared +by all generated Cython code. This ensures: +1. Consistent random number sequences across all code objects +2. Identical behavior to C++ standalone mode (which also uses a global generator) +3. Proper state save/restore functionality +""" + +from libcpp.string cimport string + +cdef extern from "randomgenerator.h" + cdef cppclass RandomGenerator: + Randomgenerator() except + + void seed() except + + void seed(unsigned long) except + + double rand() noexcept nogil + double randn() noexcept nogil + string get_state() + void set_state(string) except + + +# The ONE global random generator instance +# This is created when the module is first imported and lives for the entire process +cdef RandomGenerator _global_rng + + +##### C-level functions (for use by generated Cython code via cimport) ##### +cdef double _rand() noexcept nogil: + return _global_rng.rand() + +cdef double _randn() noexcept nogil: + return _global_rng.randn() + + + +##### Python-level functions (for use by Brian2's Python code) ##### +def seed(seed_value=None): + """ + Seed the global random number generator. + + Parameters + ---------- + seed_value : int or None + If None, seed with system entropy (non-deterministic). + If an integer, seed with that value (deterministic/reproducible). + """ + if seed_value is None: + _global_rng.seed() + else: + _global_rng.seed(seed_value) + +def get_state(): + """ + Get the complete internal state of the random generator. + + Returns a string that captures: + - The full MT19937 internal state (624 x 32-bit words) + - The Box-Muller cached value (for randn) + - Whether there's a cached value + + This allows exact restoration to this point in the sequence. + + Returns + ------- + str + The serialized state that can be passed to set_state(). + """ + cdef string state = _global_rng.get_state() + return state.decode('utf-8') + +def set_state(state_str): + """ + Restore the random generator to a previously saved state. + + Parameters + ---------- + state_str : str + A state string previously returned by get_state(). + """ + cdef bytes state_bytes = state_str.encode('utf-8') + cdef string state = state_bytes + _global_rng.set_state(state) + + +def rand(): + return _global_rng.rand() + +def randn(): + return _global_rng.randn() From c2e80bfac6127753926baadd58a78fe002b633fd Mon Sep 17 00:00:00 2001 From: Legend101Zz <96632943+Legend101Zz@users.noreply.github.com> Date: Wed, 21 Jan 2026 19:19:39 +0530 Subject: [PATCH 10/16] fix: changes for making the cythonRNG work --- brian2/codegen/generators/cython_generator.py | 36 ++++--------- brian2/codegen/runtime/cython_rt/cython_rt.py | 2 - brian2/devices/device.py | 52 +++++++++++-------- 3 files changed, 40 insertions(+), 50 deletions(-) diff --git a/brian2/codegen/generators/cython_generator.py b/brian2/codegen/generators/cython_generator.py index bddeab6b4..be5e99cb9 100644 --- a/brian2/codegen/generators/cython_generator.py +++ b/brian2/codegen/generators/cython_generator.py @@ -559,36 +559,20 @@ def determine_keywords(self): # ============================================================================== # Random Number Generation # ============================================================================== -# We use the C++ RandomGenerator class (from randomgenerator.h) to ensure -# identical random sequences between Cython runtime and C++ standalone modes. -# The RandomGenerator is instantiated once per code object and seeded via -# the namespace mechanism. +# We use a pre-compiled global RandomGenerator via cythonrng module. +# This ensures: +# 1. All code objects share the same RNG (like C++ standalone) +# 2. Proper state save/restore is possible +# 3. Identical sequences between Cython and C++ standalone backends rand_code = """ -# Module-level RandomGenerator instance -# This will be created once when the module is first imported -cdef RandomGenerator* _brian_random_generator = new RandomGenerator() - -cdef double _rand(int _idx) nogil: - return _brian_random_generator.rand() - -def _seed_random_generator(seed=None): - '''Seed the random generator. Called from Python.''' - global _brian_random_generator - if seed is None: - _brian_random_generator.seed() - else: - _brian_random_generator.seed(seed) - -def _get_random_generator_ptr(): - '''Return the address of the random generator for state management.''' - return _brian_random_generator +# Import the global random number generator functions +from brian2.codegen.runtime.cython_rt.cythonrng cimport _rand, _randn """ -randn_code = """ -cdef double _randn(int _idx) nogil: - return _brian_random_generator.randn() -""" +# randn is included in the same import, but we keep a separate code string +# for the dependency system +randn_code = "" # Already imported via rand_code poisson_code = """ cdef double _loggam(double x): diff --git a/brian2/codegen/runtime/cython_rt/cython_rt.py b/brian2/codegen/runtime/cython_rt/cython_rt.py index f91c16c86..9dde765ee 100644 --- a/brian2/codegen/runtime/cython_rt/cython_rt.py +++ b/brian2/codegen/runtime/cython_rt/cython_rt.py @@ -195,8 +195,6 @@ def compile_block(self, block): def run_block(self, block): compiled_code = self.compiled_code[block] if compiled_code: - # Seed the random generator if this module uses it - self._seed_if_needed(compiled_code) try: return compiled_code.main(self.namespace) except Exception as exc: diff --git a/brian2/devices/device.py b/brian2/devices/device.py index b199a5890..e89dfefc9 100644 --- a/brian2/devices/device.py +++ b/brian2/devices/device.py @@ -479,24 +479,17 @@ def __init__(self): #: objects). Arrays in this dictionary will disappear as soon as the #: last reference to the `Variable` object used as a key is gone self.arrays = WeakKeyDictionary() - # Store the seed value for seeding newly compiled code object - self._seed_value = None - # Track compiled modules that have been seeded - self._seeded_modules = {} def __getstate__(self): state = dict(self.__dict__) # Python's pickle module cannot pickle a WeakKeyDictionary, we therefore # convert it to a standard dictionary state["arrays"] = dict(self.arrays) - # Don't try to pickle the seeded modules - state["_seeded_modules"] = {} return state def __setstate__(self, state): self.__dict__ = state self.__dict__["arrays"] = WeakKeyDictionary(self.__dict__["arrays"]) - self.__dict__["_seeded_modules"] = {} def get_array_name(self, var, access_data=True): # if no owner is set, this is a temporary object (e.g. the array @@ -595,37 +588,52 @@ def seed(self, seed=None): The seed value for the random number generator, or ``None`` (the default) to set a random seed. """ - # Store the seed value - it will be used to seed any RandomGenerator - # instances in compiled Cython code - self._seed_value = seed - # Also seed numpy for any code that might still use it directly + # Seed the global Cython RandomGenerator + from brian2.codegen.runtime.cython_rt.cythonrng import seed as rng_seed + + rng_seed(seed) + + # Also seed numpy for any code that might use it directly np.random.seed(seed) - # Clear the seeded modules so they get re-seeded on next use - self._seeded_modules = {} def get_random_state(self): """ Return a representation of the current random number generator state. - Note: With the RandomGenerator, full state restoration requires - access to the compiled modules. This method returns what can be saved, - but full restoration may not be possible if modules have been recompiled. + This captures the exact internal state of the RandomGenerator, + allowing precise restoration to this point in the random sequence. + + Returns + ------- + dict + A dictionary containing: + - 'rng_state': The internal state of the RandomGenerator + - 'numpy_state': The state of NumPy's random generator """ + from brian2.codegen.runtime.cython_rt.cythonrng import ( + get_state as rng_get_state, + ) + return { + "rng_state": rng_get_state(), "numpy_state": np.random.get_state(), - "seed_value": self._seed_value, } def set_random_state(self, state): """ - Reset the random number generator state to a previously stored state. + Restore the random number generator to a previously saved state. - Note: This restores the seed value and numpy state. The actual - RandomGenerator states in compiled modules may not be fully restorable. + Parameters + ---------- + state : dict + A state dictionary previously returned by get_random_state(). """ + from brian2.codegen.runtime.cython_rt.cythonrng import ( + set_state as rng_set_state, + ) + + rng_set_state(state["rng_state"]) np.random.set_state(state["numpy_state"]) - self._seed_value = state.get("seed_value") - self._seeded_modules = {} class Dummy: From 6820c296708df124bb1409eb078f829daaac197e Mon Sep 17 00:00:00 2001 From: Legend101Zz <96632943+Legend101Zz@users.noreply.github.com> Date: Wed, 21 Jan 2026 19:20:39 +0530 Subject: [PATCH 11/16] fix: syntax and setup.py changes to prebuilt the cythonRNG --- brian2/random/cythonrng.pyx | 2 +- setup.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/brian2/random/cythonrng.pyx b/brian2/random/cythonrng.pyx index ae82ebfcb..1f8a06ee0 100644 --- a/brian2/random/cythonrng.pyx +++ b/brian2/random/cythonrng.pyx @@ -14,7 +14,7 @@ by all generated Cython code. This ensures: from libcpp.string cimport string -cdef extern from "randomgenerator.h" +cdef extern from "randomgenerator.h": cdef cppclass RandomGenerator: Randomgenerator() except + void seed() except + diff --git a/setup.py b/setup.py index 816e1b92e..dea6eb9ab 100644 --- a/setup.py +++ b/setup.py @@ -41,6 +41,7 @@ def require_cython_extension(module_path, module_name,extra_include_dirs=None): extensions.append(spike_queue_ext) +# Dynamic Array extension dynamic_array_ext = require_cython_extension( module_path=["brian2", "memory"], module_name="cythondynamicarray", @@ -49,5 +50,13 @@ def require_cython_extension(module_path, module_name,extra_include_dirs=None): extensions.append(dynamic_array_ext) +# Random number generator extension +rng_ext = require_cython_extension( + module_path=["brian2", "random"], + module_name="cythonrng", + extra_include_dirs=["brian2/devices/cpp_standalone/brianlib"] +) +extensions.append(rng_ext) + setup(ext_modules=extensions) From 977a9ce074d4c30ffbe67719d3b604a2f74f2409 Mon Sep 17 00:00:00 2001 From: Legend101Zz <96632943+Legend101Zz@users.noreply.github.com> Date: Wed, 21 Jan 2026 19:26:10 +0530 Subject: [PATCH 12/16] fix: remove unneeded code --- brian2/codegen/runtime/cython_rt/cython_rt.py | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/brian2/codegen/runtime/cython_rt/cython_rt.py b/brian2/codegen/runtime/cython_rt/cython_rt.py index 9dde765ee..a48642c9a 100644 --- a/brian2/codegen/runtime/cython_rt/cython_rt.py +++ b/brian2/codegen/runtime/cython_rt/cython_rt.py @@ -213,28 +213,6 @@ def _insert_func_namespace(self, func): for dep in impl.dependencies.values(): self._insert_func_namespace(dep) - def _seed_if_needed(self, compiled_code): - """Seed the random generator in the compiled module if needed.""" - from brian2.devices.device import get_device - - device = get_device() - - # Check if this module has the seeding function and hasn't been seeded yet - if hasattr(compiled_code, "_seed_random_generator"): - # Use id() to track which modules have been seeded - # with the current seed value - module_id = id(compiled_code) - seed_value = device._seed_value - - # Check if we need to seed (new module or seed changed) - if not hasattr(device, "_seeded_modules"): - device._seeded_modules = {} - - current_seed_marker = (seed_value, id(device)) - if device._seeded_modules.get(module_id) != current_seed_marker: - compiled_code._seed_random_generator(seed_value) - device._seeded_modules[module_id] = current_seed_marker - def variables_to_namespace(self): # Variables can refer to values that are either constant (e.g. dt) # or change every timestep (e.g. t). We add the values of the From 14fbbdb42ba9a09b323500d7a32746ea5deb5a64 Mon Sep 17 00:00:00 2001 From: Legend101Zz <96632943+Legend101Zz@users.noreply.github.com> Date: Wed, 21 Jan 2026 19:28:59 +0530 Subject: [PATCH 13/16] fix: common.pyx template --- .../runtime/cython_rt/templates/common_group.pyx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/brian2/codegen/runtime/cython_rt/templates/common_group.pyx b/brian2/codegen/runtime/cython_rt/templates/common_group.pyx index 218d2801a..6db51f9c0 100644 --- a/brian2/codegen/runtime/cython_rt/templates/common_group.pyx +++ b/brian2/codegen/runtime/cython_rt/templates/common_group.pyx @@ -17,7 +17,6 @@ cdef extern from "math.h": from libc.stdlib cimport abs # For integers from libc.math cimport abs # For floating point values from libc.limits cimport INT_MIN, INT_MAX -from libc.stdint cimport uintptr_t from libcpp cimport bool from libcpp.set cimport set from cython.operator cimport dereference as _deref, preincrement as _preinc @@ -71,15 +70,6 @@ cdef extern from "dynamic_array.h": size_t cols() size_t stride() -# RandomGenerator C++ interface declaration for random number generation -cdef extern from "randomgenerator.h": - cdef cppclass RandomGenerator: - RandomGenerator() except + - void seed() except + - void seed(unsigned long) except + - double rand() nogil - double randn() nogil - {% endmacro %} {% macro before_run() %} From 9682d074fc9863aa21c57ca174c3a6a77f2b8a6a Mon Sep 17 00:00:00 2001 From: Legend101Zz <96632943+Legend101Zz@users.noreply.github.com> Date: Wed, 21 Jan 2026 19:39:49 +0530 Subject: [PATCH 14/16] fix: pxd imports --- brian2/codegen/generators/cython_generator.py | 2 +- brian2/devices/device.py | 6 +++--- brian2/random/__init__.py | 11 +++++++++++ setup.py | 10 +++++++++- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/brian2/codegen/generators/cython_generator.py b/brian2/codegen/generators/cython_generator.py index be5e99cb9..e7cb3aa06 100644 --- a/brian2/codegen/generators/cython_generator.py +++ b/brian2/codegen/generators/cython_generator.py @@ -567,7 +567,7 @@ def determine_keywords(self): rand_code = """ # Import the global random number generator functions -from brian2.codegen.runtime.cython_rt.cythonrng cimport _rand, _randn +from brian2.random.cythonrng cimport _rand, _randn """ # randn is included in the same import, but we keep a separate code string diff --git a/brian2/devices/device.py b/brian2/devices/device.py index e89dfefc9..d115d9ce1 100644 --- a/brian2/devices/device.py +++ b/brian2/devices/device.py @@ -589,7 +589,7 @@ def seed(self, seed=None): default) to set a random seed. """ # Seed the global Cython RandomGenerator - from brian2.codegen.runtime.cython_rt.cythonrng import seed as rng_seed + from brian2.random.cythonrng import seed as rng_seed rng_seed(seed) @@ -610,7 +610,7 @@ def get_random_state(self): - 'rng_state': The internal state of the RandomGenerator - 'numpy_state': The state of NumPy's random generator """ - from brian2.codegen.runtime.cython_rt.cythonrng import ( + from brian2.random.cythonrng import ( get_state as rng_get_state, ) @@ -628,7 +628,7 @@ def set_random_state(self, state): state : dict A state dictionary previously returned by get_random_state(). """ - from brian2.codegen.runtime.cython_rt.cythonrng import ( + from brian2.random.cythonrng import ( set_state as rng_set_state, ) diff --git a/brian2/random/__init__.py b/brian2/random/__init__.py index e69de29bb..c914ddf39 100644 --- a/brian2/random/__init__.py +++ b/brian2/random/__init__.py @@ -0,0 +1,11 @@ +# brian2/random/__init__.py +""" +Random number generation for Brian2. + +This module provides a unified random number generator that produces +identical sequences in both Cython runtime and C++ standalone modes. +""" + +from .cythonrng import seed, get_state, set_state, rand, randn + +__all__ = ["seed", "get_state", "set_state", "rand", "randn"] diff --git a/setup.py b/setup.py index dea6eb9ab..6fb7d240c 100644 --- a/setup.py +++ b/setup.py @@ -59,4 +59,12 @@ def require_cython_extension(module_path, module_name,extra_include_dirs=None): extensions.append(rng_ext) -setup(ext_modules=extensions) +setup( + ext_modules=extensions, + # Include .pxd files so they get installed + package_data={ + 'brian2.random': ['*.pxd'], + }, + # Make sure package data is included + include_package_data=True, +) From 3cde84761d6b8cd6949f2d006f70aea8ae6c1011 Mon Sep 17 00:00:00 2001 From: Legend101Zz <96632943+Legend101Zz@users.noreply.github.com> Date: Fri, 23 Jan 2026 07:13:06 +0530 Subject: [PATCH 15/16] fix: add _idx parameter for backwards compatibility --- brian2/codegen/generators/cython_generator.py | 5 ++--- brian2/random/cythonrng.pxd | 5 +++-- brian2/random/cythonrng.pyx | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/brian2/codegen/generators/cython_generator.py b/brian2/codegen/generators/cython_generator.py index e7cb3aa06..6fd6ce1a0 100644 --- a/brian2/codegen/generators/cython_generator.py +++ b/brian2/codegen/generators/cython_generator.py @@ -570,9 +570,8 @@ def determine_keywords(self): from brian2.random.cythonrng cimport _rand, _randn """ -# randn is included in the same import, but we keep a separate code string -# for the dependency system -randn_code = "" # Already imported via rand_code +# randn is included in the same import +randn_code = rand_code poisson_code = """ cdef double _loggam(double x): diff --git a/brian2/random/cythonrng.pxd b/brian2/random/cythonrng.pxd index 16a6db826..c6ae1bfdd 100644 --- a/brian2/random/cythonrng.pxd +++ b/brian2/random/cythonrng.pxd @@ -1,5 +1,6 @@ # Cython declaration file for the global random number generator # Other Cython modules can use: from brian2.random.cythonrng cimport _rand, _randn -cdef double _rand() noexcept nogil -cdef double _randn() noexcept nogil + # Note we accept (and ignore) the _idx parameter for backwards compatibility +cdef double _rand(int _idx) noexcept nogil +cdef double _randn(int _idx) noexcept nogil diff --git a/brian2/random/cythonrng.pyx b/brian2/random/cythonrng.pyx index 1f8a06ee0..4682c5c37 100644 --- a/brian2/random/cythonrng.pyx +++ b/brian2/random/cythonrng.pyx @@ -30,11 +30,11 @@ cdef RandomGenerator _global_rng ##### C-level functions (for use by generated Cython code via cimport) ##### -cdef double _rand() noexcept nogil: - return _global_rng.rand() +cdef double _rand(int _idx) noexcept nogil: + return _global_rng.rand() # Note we accept (and ignore) the _idx parameter for backwards compatibility -cdef double _randn() noexcept nogil: - return _global_rng.randn() +cdef double _randn(int _idx) noexcept nogil: + return _global_rng.randn() # Note we accept (and ignore) the _idx parameter for backwards compatibility From 62829a7da47a462395468e9a19ab44609f734f19 Mon Sep 17 00:00:00 2001 From: Marcel Stimberg Date: Fri, 23 Jan 2026 14:35:56 +0000 Subject: [PATCH 16/16] Change expected values in Cython RNG test --- brian2/tests/test_neurongroup.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/brian2/tests/test_neurongroup.py b/brian2/tests/test_neurongroup.py index d9de59192..f80ce1c9c 100644 --- a/brian2/tests/test_neurongroup.py +++ b/brian2/tests/test_neurongroup.py @@ -2017,9 +2017,7 @@ def test_random_values_fixed_seed(): ), ("RuntimeDevice", "cython", None): ( [0.1636023, 0.76229608, 0.74945305, 0.82121212, 0.82669968], - # Cython uses a buffer for the random values that it gets from numpy, the - # values for the second call are therefore different - [-0.24349748, 1.1164414, -1.97421849, 1.58092889, -0.06444478], + [-0.7758696, 0.13295831, 0.87360834, -1.21879122, 0.62980314], ), ("CPPStandaloneDevice", None, 1): ( [0.1636023, 0.76229608, 0.74945305, 0.82121212, 0.82669968],