diff --git a/PyRandLib/__init__.py b/PyRandLib/__init__.py index 38f0b50..4e2d8a6 100644 --- a/PyRandLib/__init__.py +++ b/PyRandLib/__init__.py @@ -6,11 +6,15 @@ Copyright (c) 2016-2025 Philippe Schmouker, schmouk (at) gmail.com """ +from .basecwg import BaseCWG from .baselcg import BaseLCG from .baselfib64 import BaseLFib64 from .basemrg import BaseMRG from .baserandom import BaseRandom from .basewell import BaseWELL +from .cwg64 import Cwg64 +from .cwg128_64 import Cwg128_64 +from .cwg128 import Cwg128 from .fastrand32 import FastRand32 from .fastrand63 import FastRand63 from .lfib78 import LFib78 diff --git a/PyRandLib/annotation_types.py b/PyRandLib/annotation_types.py index b1a6f8b..06f3b88 100644 --- a/PyRandLib/annotation_types.py +++ b/PyRandLib/annotation_types.py @@ -23,10 +23,11 @@ #============================================================================= from typing import List, Tuple, Union -Numerical = Union[ int, float ] -StatesList = Union[ Tuple[int], List[int] ] -StateType = Union[ StatesList, Tuple[StatesList, int] ] -SeedStateType = Union[ Numerical, StateType ] +Numerical = Union[ int, float ] +StatesList = Union[ Tuple[int], List[int] ] +StatesListAndState = Tuple[ StatesList, int ] +StateType = Union[ StatesList, StatesListAndState ] +SeedStateType = Union[ Numerical, StateType ] #===== end of PyRandLib.annotation_types =============================== diff --git a/PyRandLib/basecwg.py b/PyRandLib/basecwg.py new file mode 100644 index 0000000..560b735 --- /dev/null +++ b/PyRandLib/basecwg.py @@ -0,0 +1,137 @@ +""" +Copyright (c) 2025 Philippe Schmouker, schmouk (at) gmail.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +#============================================================================= +from .baserandom import BaseRandom +from .annotation_types import SeedStateType, StatesListAndState + + +#============================================================================= +class BaseCWG( BaseRandom ): + """Definition of the base class for all Collatz-Weyl pseudo-random Generators. + + This module is part of library PyRandLib. + + Copyright (c) 2025 Philippe Schmouker + + CWG models are chaotic generators that are combined with Weyl sequences to + eliminate the risk of short cycles. They have a large period, a uniform + distribution, and the ability to generate multiple independent streams by + changing their internal parameters (Weyl increment). CWGs owe their + exceptional quality to the arithmetical dynamics of noninvertible, + generalized, Collatz mappings based on the wellknown Collatz conjecture. + There is no jump function, but each odd number of the Weyl increment + initiates a new unique period, which enables quick initialization of + independent streams (this text is extracted from [8], see README.md). + + The internal implementation of the CWG algorithm varies according to its + implemented version. See implementation classes to get their formal + description. + + See Cwg64 for a minimum 2^70 (i.e. about 1.18e+21) period CW-Generator + with low computation time, medium period, 64- bits output values and very + good randomness characteristics. + See Cwg128_64 for a minimum 2^71 (i.e. about 2.36e+21) period CW-Generator + with very low computation time, medium period, 64-bits output values and + very good randomness characteristics. + See Cwg128 for a minimum 2^135 (i.e. about 4.36e+40) period CW-generator + with very low computation time, medium period, 64- bits output values and + very good randomness characteristics. + + Furthermore this class is callable: + rand = BaseCWG() # Caution: this is just used as illustrative. This base class cannot be instantiated + print( rand() ) # prints a pseudo-random value within [0.0, 1.0) + print( rand(a) ) # prints a pseudo-random value within [0, a) or [0.0, a) depending on the type of a + print( rand(a, n) ) # prints a list of n pseudo-random values each within [0, a) + + Reminder: + We give you here below a copy of the table of tests for the CWGs that have + been implemented in PyRandLib, as presented in paper [8] - see file README.md. + + | PyRandLib class | [8] generator name | Memory Usage | Period | time-32bits | time-64 bits | SmallCrush fails | Crush fails | BigCrush fails | + | --------------- | ------------------ | ------------- | -------- | ----------- | ------------ | ---------------- | ----------- | -------------- | + | Cwg64 | CWG64 | 8 x 4-bytes | >= 2^70 | n.a. | n.a. | 0 | 0 | 0 | + | Cwg128_64 | CWG128_64 | 10 x 4-bytes | >= 2^71 | n.a. | n.a. | 0 | 0 | 0 |_ + | Cwg128 | CWG128 | 16 x 4-bytes | >= 2^135 | n.a. | n.a. | 0 | 0 | 0 | + + * _small crush_ is a small set of simple tests that quickly tests some of + the expected characteristics for a pretty good PRG; + * _crush_ is a bigger set of tests that test more deeply expected random + characteristics; + * _big crush_ is the ultimate set of difficult tests that any GOOD PRG + should definitively pass. + """ + + #------------------------------------------------------------------------- + def __init__(self, _seedState: SeedStateType = None) -> None: + """Constructor. + + Should _seedState be None then the local time is used as a seed (with + its shuffled value). + Notice: method setstate() is not implemented in base class BaseRandom. + So, it must be implemented in classes inheriting BaseLCG and it must + initialize attribute self._state. + """ + super().__init__( _seedState ) # this internally calls 'setstate()' which + # MUST be implemented in inheriting classes + + + #------------------------------------------------------------------------- + def getstate(self) -> StatesListAndState: + """Returns an object capturing the current internal state of the generator. + + This object can be passed to setstate() to restore the state. + For CWG, this state is defined by a list of control values + (a, weyl and s - or a list of 4 coeffs) and an internal state + value, which are used in methods 'next() and 'setstate() of + every inheriting class. + + All inheriting classes MUST IMPLEMENT this method. + """ + raise NotImplementedError() + + +#============================================================================= +class SplitMix: + """The splitting and mixing algorithm used to intiialize CWGs states. + """ + #------------------------------------------------------------------------- + def __init__(self, _seed: int) -> None: + """Constructor. + """ + self.state = _seed & 0xffff_ffff_ffff_ffff + + #------------------------------------------------------------------------- + def __call__(self, _mask: int = 0xffff_ffff_ffff_ffff) -> int: + """The shuffle algorithm. + """ + self.state += 0x9e37_79b9_7f4a_7c15 + self.state &= 0xffff_ffff_ffff_ffff + + z = self.state & _mask + z = ((z ^ (z >> 30)) * 0xbf58476d1ce4e5b9) & _mask + z = ((z ^ (z >> 27)) * 0x94d049bb133111eb) & _mask + + return z ^ (z >> 31) + + +#===== end of module basecwg.py ======================================== diff --git a/PyRandLib/baselcg.py b/PyRandLib/baselcg.py index 25291e3..88d7bf9 100644 --- a/PyRandLib/baselcg.py +++ b/PyRandLib/baselcg.py @@ -95,8 +95,8 @@ def getstate(self) -> int: """Returns an object capturing the current internal state of the generator. This object can be passed to setstate() to restore the state. - For LCG, the state is defined with a single integer, 'self._value', - which has to be used in methods 'random() and 'setstate() of every + For LCG, the state is defined with a single integer, 'self._state', + which has to be used in methods 'next() and 'setstate() of every inheriting class. """ return self._state diff --git a/PyRandLib/baserandom.py b/PyRandLib/baserandom.py index c81aca0..879fdd1 100644 --- a/PyRandLib/baserandom.py +++ b/PyRandLib/baserandom.py @@ -249,8 +249,12 @@ class BaseRandom( Random ): def __init__(self, _seed: SeedStateType = None) -> None: """Constructor. - Should _seed be None or not an integer then the local - time is used (with its shuffled value) as a seed. + Should _seed be None or not a number then the local time is used + (with its shuffled value) as a seed. + + Notice: the Python built-in base class random.Random internally + calls method setstate() which MUST be overridden in classes that + inherit from class BaseRandom. """ super().__init__( _seed ) @@ -326,7 +330,7 @@ def seed(self, _seed: SeedStateType = None) -> None: def __call__(self, _max : Union[Numerical, Tuple[Numerical], List[Numerical]] = 1.0, - times: int = 1 ) -> Numerical: + times: int = 1 ) -> Union[Numerical | List[Numerical]]: """This class's instances are callable. The returned value is uniformly contained within the diff --git a/PyRandLib/basesquares.py b/PyRandLib/basesquares.py new file mode 100644 index 0000000..194ff9f --- /dev/null +++ b/PyRandLib/basesquares.py @@ -0,0 +1,104 @@ +""" +Copyright (c) 2025 Philippe Schmouker, schmouk (at) gmail.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +#============================================================================= +from .baserandom import BaseRandom +from .annotation_types import SeedStateType, StatesList + + +#============================================================================= +class BaseSquares( BaseRandom ): + """Definition of the base class for the Squares counter-based pseudo-random Generator. + + This module is part of library PyRandLib. + + Copyright (c) 2025 Philippe Schmouker + + Squares models are based on an incremented counter and a key. The + algorithm squares a combination of the counter and the key values, + and exchanges the upper and lower bits of the combination, the + whole repeated a number of times (4 to 5 rounds). Output values + are provided on 32-bits or on 64-bits according to the model. See + [9] in README.md. + + See Squares32 for a 2^64 (i.e. about 1.84e+19) period PRNG with + low computation time, medium period, 32-bits output values and + very good randomness characteristics. + + See Squares64 for a 2^64 (i.e. about 1.84e+19) period PRNG with + low computation time, medium period, 64-bits output values and + very good randomness characteristics. + + Furthermore this class is callable: + rand = BaseSquares()# Caution: this is just used as illustrative. This base class cannot be instantiated + print( rand() ) # prints a pseudo-random value within [0.0, 1.0) + print( rand(a) ) # prints a pseudo-random value within [0, a) or [0.0, a) depending on the type of a + print( rand(a, n) ) # prints a list of n pseudo-random values each within [0, a) + + Reminder: + We give you here below a copy of the table of tests for the Squares + that have been implemented in PyRandLib, as presented in paper [9] + - see file README.md. + + | PyRandLib class | [9] generator name | Memory Usage | Period | time-32bits | time-64 bits | SmallCrush fails | Crush fails | BigCrush fails | + | --------------- | ------------------ | ------------- | -------- | ----------- | ------------ | ---------------- | ----------- | -------------- | + | Squares32 | squares32 | 4 x 4-bytes | 2^64 | n.a. | n.a. | 0 | 0 | 0 | + | Squares64 | squares64 | 4 x 4-bytes | 2^64 | n.a. | n.a. | 0 | 0 | 0 |_ + + * _small crush_ is a small set of simple tests that quickly tests some of + the expected characteristics for a pretty good PRG; + * _crush_ is a bigger set of tests that test more deeply expected random + characteristics; + * _big crush_ is the ultimate set of difficult tests that any GOOD PRG + should definitively pass. + """ + + #------------------------------------------------------------------------- + def __init__(self, _seedState: SeedStateType = None) -> None: + """Constructor. + + Should _seedState be None then the local time is used as a seed (with + its shuffled value). + Notice: method setstate() is not implemented in base class BaseRandom. + So, it must be implemented in classes inheriting BaseLCG and it must + initialize attribute self._state. + """ + super().__init__( _seedState ) # this internally calls 'setstate()' which + # MUST be implemented in inheriting classes + + + #------------------------------------------------------------------------- + def getstate(self) -> StatesList: + """Returns an object capturing the current internal state of the generator. + + This object can be passed to setstate() to restore the state. + For CWG, this state is defined by a list of control values + (a, weyl and s - or a list of 4 coeffs) and an internal state + value, which are used in methods 'next() and 'setstate() of + every inheriting class. + + All inheriting classes MUST IMPLEMENT this method. + """ + raise NotImplementedError() + + +#===== end of module basesquares.py ==================================== diff --git a/PyRandLib/cwg128.py b/PyRandLib/cwg128.py new file mode 100644 index 0000000..76b59f6 --- /dev/null +++ b/PyRandLib/cwg128.py @@ -0,0 +1,175 @@ +""" +Copyright (c) 2025 Philippe Schmouker, schmouk (at) gmail.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +#============================================================================= +from typing import Tuple + +from .basecwg import BaseCWG, SplitMix +from .fastrand32 import FastRand32 +from .annotation_types import SeedStateType + + +#============================================================================= +class Cwg128( BaseCWG ): + """ + Pseudo-random numbers generator - Collatz-Weyl pseudo-random Generators + dedicated to 128-bits calculations and 128-bits output values with large + period (min 2^135, i.e. 4.36e+40) but short computation time. All CWG + algorithms offer multi streams features, by simply using different initial + settings for control value 's' - see below. + + This module is part of library PyRandLib. + + Copyright (c) 2025 Philippe Schmouker + + This CWG model evaluates pseudo-random numbers suites x(i) as a simple + mathematical function of + + x(i+1) = (x(i) >> 1) * ((a += x(i)) | 1) ^ (weyl += s) + + and returns as the output value the xored shifted: a >> 96 ^ x(i+1) + + where a, weyl and s are the control values and x the internal state of the + PRNG. 's' must be initally odd. 'a', 'weyl' and initial state 'x' may be + initialized each with any 64-bits value. + + Notice: in the original paper, four control value c[0] to c[3] are used. + It appears that these value are used just are 's' for c[0], 'x' for c[1], + 'a' for c[2] and 'weyl' for c[3] in the other versions of the algorithm. + + See Cwg64 for a minimum 2^70 (i.e. about 1.18e+21) period CW-Generator + with very low computation time, medium period, 64- bits output values and + very good randomness characteristics. + See Cwg128_64 for a minimum 2^71 (i.e. about 2.36e+21) period CW-Generator + with very low computation time, medium period, 64-bits output values and + very good randomness characteristics. + + Furthermore this class is callable: + rand = Cwg128() + print( rand() ) # prints a pseudo-random value within [0.0, 1.0) + print( rand(a) ) # prints a pseudo-random value within [0, a) or [0.0, a) depending on the type of a + print( rand(a, n) ) # prints a list of n pseudo-random values each within [0, a) + + Reminder: + We give you here below a copy of the table of tests for the LCGs that have + been implemented in PyRandLib, as presented in paper [8] - see file README.md. + + | PyRandLib class | [8] generator name | Memory Usage | Period | time-32bits | time-64 bits | SmallCrush fails | Crush fails | BigCrush fails | + | --------------- | ------------------ | ------------- | -------- | ----------- | ------------ | ---------------- | ----------- | -------------- | + | Cwg64 | CWG64 | 8 x 4-bytes | >= 2^70 | n.a. | n.a. | 0 | 0 | 0 | + | Cwg128_64 | CWG128_64 | 10 x 4-bytes | >= 2^71 | n.a. | n.a. | 0 | 0 | 0 |_ + | Cwg128 | CWG128 | 16 x 4-bytes | >= 2^135 | n.a. | n.a. | 0 | 0 | 0 | + + * _small crush_ is a small set of simple tests that quickly tests some of + the expected characteristics for a pretty good PRG; + * _crush_ is a bigger set of tests that test more deeply expected random + characteristics; + * _big crush_ is the ultimate set of difficult tests that any GOOD PRG + should definitively pass. + """ + + + #------------------------------------------------------------------------- + _NORMALIZE: float = 2.938_735_877_055_718_769_921_8e-39 # i.e. 1.0 / (1 << 128) + """The value of this class attribute MUST BE OVERRIDDEN in inheriting + classes if returned random integer values are coded on anything else + than 32 bits. It is THE multiplier constant value to be applied to + pseudo-random number for them to be normalized in interval [0.0, 1.0). + """ + + _OUT_BITS: int = 128 + """The value of this class attribute MUST BE OVERRIDDEN in inheriting + classes if returned random integer values are coded on anything else + than 32 bits. + """ + + + #------------------------------------------------------------------------- + def __init__(self, _seedState: SeedStateType = None) -> None: + """Constructor. + + Should _seedState be None then the local time is used as a seed (with + its shuffled value). + """ + super().__init__( _seedState ) # this internally calls 'setstate()' which + # MUST be implemented in inheriting classes + + + #------------------------------------------------------------------------- + def next(self) -> int: + """This is the core of the pseudo-random generator. + """ + # evaluates next internal state + self._a = (self._a + self._state) & 0xffff_ffff_ffff_ffff_ffff_ffff_ffff_ffff + self._weyl = (self._weyl + self._s) & 0xffff_ffff_ffff_ffff_ffff_ffff_ffff_ffff + self._state = (((self._state >> 1) * (self._a | 1)) ^ self._weyl) & 0xffff_ffff_ffff_ffff_ffff_ffff_ffff_ffff + # returns the xored-shifted output value + return self._state ^ (self._a >> 96) + + + #------------------------------------------------------------------------- + def getstate(self) -> Tuple[int]: + """Returns an object capturing the current internal state of the generator. + + This object can be passed to setstate() to restore the state. + """ + return (self._a, self._weyl, self._s, self._state) + + + #------------------------------------------------------------------------- + def setstate(self, _state: SeedStateType) -> None: + """Restores the internal state of the generator. + + _state should have been obtained from a previous call + to getstate(), and setstate() restores the internal + state of the generator to what it was at the time + setstate() was called. + """ + if isinstance( _state, int ): + # passed initial seed is an integer, just uses it + splitMix = SplitMix( _state ) + self._a = self._weyl = 0 + self._state = (splitMix() << 64) | splitMix() # Notice: in the original paper, this seems to be erroneously initialized on sole 64 lowest bits + self._s = (splitMix() << 64) | (splitMix(0x7fff_ffff_ffff_ffff) << 1) | 1 + + elif isinstance( _state, float ): + # transforms passed initial seed from float to integer + if _state < 0.0 : + _state = -_state + if _state >= 1.0: + self.setstate( int(_state + 0.5) & 0xffff_ffff_ffff_ffff_ffff_ffff_ffff_ffff ) + else: + self.setstate( int(_state * 0x1_0000_0000_0000_0000_0000_0000_0000_0000) & 0xffff_ffff_ffff_ffff_ffff_ffff_ffff_ffff ) + + else: + try: + self._a = _state[0] & 0xffff_ffff_ffff_ffff_ffff_ffff_ffff_ffff + self._weyl = _state[1] & 0xffff_ffff_ffff_ffff_ffff_ffff_ffff_ffff + self._s = (_state[2] & 0xffff_ffff_ffff_ffff_ffff_ffff_ffff_ffff) | 1 # notice: s must be odd + self._state = _state[3] & 0xffff_ffff_ffff_ffff_ffff_ffff_ffff_ffff + + except: + # uses local time as initial seed + init_rand = FastRand32() + self.setstate( init_rand.next() | (init_rand.next() << 32) | (init_rand.next() << 64) | (init_rand.next() << 96) ) + +#===== end of module cwg128.py ========================================= diff --git a/PyRandLib/cwg128_64.py b/PyRandLib/cwg128_64.py new file mode 100644 index 0000000..c23489e --- /dev/null +++ b/PyRandLib/cwg128_64.py @@ -0,0 +1,172 @@ +""" +Copyright (c) 2025 Philippe Schmouker, schmouk (at) gmail.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +#============================================================================= +from typing import Tuple + +from .basecwg import BaseCWG, SplitMix +from .fastrand32 import FastRand32 +from .annotation_types import SeedStateType + + +#============================================================================= +class Cwg128_64( BaseCWG ): + """ + Pseudo-random numbers generator - Collatz-Weyl pseudo-random Generators + dedicated to 128-bits calculations and 64-bits output values with small + period (min 2^71, i.e. 2.36e+21) but short computation time. All CWG + algorithms offer multi streams features, by simply using different + initial settings for control value 's' - see below. + + This module is part of library PyRandLib. + + Copyright (c) 2025 Philippe Schmouker + + This CWG model evaluates pseudo-random numbers suites x(i) as a simple + mathematical function of + + x(i+1) = (x(i) | 1) * ((a += x(i)) >> 1) ^ (weyl += s) + + and returns as the output value the xored shifted: a >> 48 ^ x(i+1) + + where a, weyl and s are the control values and x the internal state of the + PRNG. 's' must be initally odd. 'a', 'weyl' and initial state 'x' may be + initialized each with any 64-bits value. + + See Cwg64 for a minimum 2^70 (i.e. about 1.18e+21) period CW-Generator + with very low computation time, medium period, 64- bits output values and + very good randomness characteristics. + See Cwg128 for a minimum 2^135 (i.e. about 4.36e+40) period CW-generator + with very low computation time, medium period, 64- bits output values and + very good randomness characteristics. + + Furthermore this class is callable: + rand = Cwg128_64() + print( rand() ) # prints a pseudo-random value within [0.0, 1.0) + print( rand(a) ) # prints a pseudo-random value within [0, a) or [0.0, a) depending on the type of a + print( rand(a, n) ) # prints a list of n pseudo-random values each within [0, a) + + Reminder: + We give you here below a copy of the table of tests for the LCGs that have + been implemented in PyRandLib, as presented in paper [8] - see file README.md. + + | PyRandLib class | [8] generator name | Memory Usage | Period | time-32bits | time-64 bits | SmallCrush fails | Crush fails | BigCrush fails | + | --------------- | ------------------ | ------------- | -------- | ----------- | ------------ | ---------------- | ----------- | -------------- | + | Cwg64 | CWG64 | 8 x 4-bytes | >= 2^70 | n.a. | n.a. | 0 | 0 | 0 | + | Cwg128_64 | CWG128_64 | 10 x 4-bytes | >= 2^71 | n.a. | n.a. | 0 | 0 | 0 |_ + | Cwg128 | CWG128 | 16 x 4-bytes | >= 2^135 | n.a. | n.a. | 0 | 0 | 0 | + + * _small crush_ is a small set of simple tests that quickly tests some of + the expected characteristics for a pretty good PRG; + * _crush_ is a bigger set of tests that test more deeply expected random + characteristics; + * _big crush_ is the ultimate set of difficult tests that any GOOD PRG + should definitively pass. + """ + + + #------------------------------------------------------------------------- + _NORMALIZE: float = 5.421_010_862_427_522_170_037_3e-20 # i.e. 1.0 / (1 << 64) + """The value of this class attribute MUST BE OVERRIDDEN in inheriting + classes if returned random integer values are coded on anything else + than 32 bits. It is THE multiplier constant value to be applied to + pseudo-random number for them to be normalized in interval [0.0, 1.0). + """ + + _OUT_BITS: int = 64 + """The value of this class attribute MUST BE OVERRIDDEN in inheriting + classes if returned random integer values are coded on anything else + than 32 bits. + """ + + + #------------------------------------------------------------------------- + def __init__(self, _seedState: SeedStateType = None) -> None: + """Constructor. + + Should _seedState be None then the local time is used as a seed (with + its shuffled value). + """ + super().__init__( _seedState ) # this internally calls 'setstate()' which + # MUST be implemented in inheriting classes + + + #------------------------------------------------------------------------- + def next(self) -> int: + """This is the core of the pseudo-random generator. + """ + # evaluates next internal state + self._a = (self._a + self._state) & 0xffff_ffff_ffff_ffff + self._weyl = (self._weyl + self._s) & 0xffff_ffff_ffff_ffff + self._state = (((self._state | 1) * (self._a >> 1)) ^ self._weyl) & 0xffff_ffff_ffff_ffff_ffff_ffff_ffff_ffff + # returns the xored-shifted output value + return (self._state ^ (self._a >> 48)) & 0xffff_ffff_ffff_ffff + + + #------------------------------------------------------------------------- + def getstate(self) -> Tuple[int]: + """Returns an object capturing the current internal state of the generator. + + This object can be passed to setstate() to restore the state. + """ + return (self._a, self._weyl, self._s, self._state) + + + #------------------------------------------------------------------------- + def setstate(self, _state: SeedStateType) -> None: + """Restores the internal state of the generator. + + _state should have been obtained from a previous call + to getstate(), and setstate() restores the internal + state of the generator to what it was at the time + setstate() was called. + """ + if isinstance( _state, int ): + # passed initial seed is an integer, just uses it + splitMix = SplitMix( _state ) + self._a = self._weyl = 0 + self._state = (splitMix() << 64) | splitMix() + self._s = (splitMix(0x7fff_ffff_ffff_ffff) << 1) | 1; + + elif isinstance( _state, float ): + # transforms passed initial seed from float to integer + if _state < 0.0 : + _state = -_state + if _state >= 1.0: + self.setstate( int(_state + 0.5) & 0xffff_ffff_ffff_ffff ) + else: + self.setstate( int(_state * 0x1_0000_0000_0000_0000) & 0xffff_ffff_ffff_ffff ) + + else: + try: + self._a = _state[0] & 0xffff_ffff_ffff_ffff + self._weyl = _state[1] & 0xffff_ffff_ffff_ffff + self._s = (_state[2] & 0xffff_ffff_ffff_ffff) | 1 # notice: s must be odd + self._state = _state[3] & 0xffff_ffff_ffff_ffff_ffff_ffff_ffff_ffff + + except: + # uses local time as initial seed + init_rand = FastRand32() + self.setstate( init_rand.next() | (init_rand.next() << 32) | (init_rand.next() << 64) | (init_rand.next() << 96) ) + + +#===== end of module cwg64.py ========================================== diff --git a/PyRandLib/cwg64.py b/PyRandLib/cwg64.py new file mode 100644 index 0000000..b154c10 --- /dev/null +++ b/PyRandLib/cwg64.py @@ -0,0 +1,172 @@ +""" +Copyright (c) 2025 Philippe Schmouker, schmouk (at) gmail.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +#============================================================================= +from typing import Tuple + +from .basecwg import BaseCWG, SplitMix +from .fastrand32 import FastRand32 +from .annotation_types import SeedStateType + + +#============================================================================= +class Cwg64( BaseCWG ): + """ + Pseudo-random numbers generator - Collatz-Weyl pseudo-random Generators + dedicated to 64-bits calculations and 64-bits output values with small + period (min 2^70, i.e. 1.18e+21) but short computation time. All CWG + algorithms offer multi streams features, by simply using different + initial settings for control value 's' - see below. + + This module is part of library PyRandLib. + + Copyright (c) 2025 Philippe Schmouker + + This CWG model evaluates pseudo-random numbers suites x(i) as a simple + mathematical function of + + x(i+1) = (x(i) >> 1) * ((a += x(i)) | 1) ^ (weyl += s) + + and returns as the output value the xored shifted: a >> 48 ^ x(i+1) + + where a, weyl and s are the control values and x the internal state of the + PRNG. 's' must be initally odd. 'a', 'weyl' and initial state 'x' may be + initialized each with any 64-bits value. + + See Cwg128_64 for a minimum 2^71 (i.e. about 2.36e+21) period CW-Generator + with very low computation time, medium period, 64-bits output values and + very good randomness characteristics. + See Cwg128 for a minimum 2^135 (i.e. about 4.36e+40) period CW-generator + with very low computation time, medium period, 64- bits output values and + very good randomness characteristics. + + Furthermore this class is callable: + rand = Cwg64() + print( rand() ) # prints a pseudo-random value within [0.0, 1.0) + print( rand(a) ) # prints a pseudo-random value within [0, a) or [0.0, a) depending on the type of a + print( rand(a, n) ) # prints a list of n pseudo-random values each within [0, a) + + Reminder: + We give you here below a copy of the table of tests for the LCGs that have + been implemented in PyRandLib, as presented in paper [8] - see file README.md. + + | PyRandLib class | [8] generator name | Memory Usage | Period | time-32bits | time-64 bits | SmallCrush fails | Crush fails | BigCrush fails | + | --------------- | ------------------ | ------------- | -------- | ----------- | ------------ | ---------------- | ----------- | -------------- | + | Cwg64 | CWG64 | 8 x 4-bytes | >= 2^70 | n.a. | n.a. | 0 | 0 | 0 | + | Cwg128_64 | CWG128_64 | 10 x 4-bytes | >= 2^71 | n.a. | n.a. | 0 | 0 | 0 |_ + | Cwg128 | CWG128 | 16 x 4-bytes | >= 2^135 | n.a. | n.a. | 0 | 0 | 0 | + + * _small crush_ is a small set of simple tests that quickly tests some of + the expected characteristics for a pretty good PRG; + * _crush_ is a bigger set of tests that test more deeply expected random + characteristics; + * _big crush_ is the ultimate set of difficult tests that any GOOD PRG + should definitively pass. + """ + + + #------------------------------------------------------------------------- + _NORMALIZE: float = 5.421_010_862_427_522_170_037_3e-20 # i.e. 1.0 / (1 << 64) + """The value of this class attribute MUST BE OVERRIDDEN in inheriting + classes if returned random integer values are coded on anything else + than 32 bits. It is THE multiplier constant value to be applied to + pseudo-random number for them to be normalized in interval [0.0, 1.0). + """ + + _OUT_BITS: int = 64 + """The value of this class attribute MUST BE OVERRIDDEN in inheriting + classes if returned random integer values are coded on anything else + than 32 bits. + """ + + + #------------------------------------------------------------------------- + def __init__(self, _seedState: SeedStateType = None) -> None: + """Constructor. + + Should _seedState be None then the local time is used as a seed (with + its shuffled value). + """ + super().__init__( _seedState ) # this internally calls 'setstate()' which + # MUST be implemented in inheriting classes + + + #------------------------------------------------------------------------- + def next(self) -> int: + """This is the core of the pseudo-random generator. + """ + # evaluates next internal state + self._a = (self._a + self._state) & 0xffff_ffff_ffff_ffff + self._weyl = (self._weyl + self._s) & 0xffff_ffff_ffff_ffff + self._state = (((self._state >> 1) * (self._a | 1)) ^ self._weyl) & 0xffff_ffff_ffff_ffff + # returns the xored-shifted output value + return self._state ^ (self._a >> 48) + + + #------------------------------------------------------------------------- + def getstate(self) -> Tuple[int]: + """Returns an object capturing the current internal state of the generator. + + This object can be passed to setstate() to restore the state. + """ + return (self._a, self._weyl, self._s, self._state) + + + #------------------------------------------------------------------------- + def setstate(self, _state: SeedStateType) -> None: + """Restores the internal state of the generator. + + _state should have been obtained from a previous call + to getstate(), and setstate() restores the internal + state of the generator to what it was at the time + setstate() was called. + """ + if isinstance( _state, int ): + # passed initial seed is an integer, just uses it + splitMix = SplitMix( _state ) + self._a = self._weyl = 0 + self._state = splitMix() + self._s = (splitMix(0x7fff_ffff_ffff_ffff) << 1) | 1 + + elif isinstance( _state, float ): + # transforms passed initial seed from float to integer + if _state < 0.0 : + _state = -_state + if _state >= 1.0: + self.setstate( int(_state + 0.5) & 0xffff_ffff_ffff_ffff ) + else: + self.setstate( int(_state * 0x1_0000_0000_0000_0000) & 0xffff_ffff_ffff_ffff ) + + else: + try: + self._a = _state[0] & 0xffff_ffff_ffff_ffff + self._weyl = _state[1] & 0xffff_ffff_ffff_ffff + self._s = (_state[2] & 0xffff_ffff_ffff_ffff) | 1 # notice: s must be odd + self._state = _state[3] & 0xffff_ffff_ffff_ffff + + except: + # uses local time as initial seed + init_rand = FastRand32() + self.setstate( init_rand.next() | (init_rand.next() << 32) ) + + +#===== end of module cwg64.py ========================================== diff --git a/PyRandLib/pcg1024_32.py b/PyRandLib/pcg1024_32.py index 82a8754..880e2ae 100644 --- a/PyRandLib/pcg1024_32.py +++ b/PyRandLib/pcg1024_32.py @@ -136,6 +136,7 @@ def next(self) -> int: # then xor's it with the next 32-bits value evaluated with the internal state return super().next() ^ extendedValue + #------------------------------------------------------------------------- def getstate(self) -> StateType: """Returns an object capturing the current internal state of the generator. @@ -209,8 +210,11 @@ def _advancetable(self) -> None: #------------------------------------------------------------------------- def _extendedstep(self, value: int, i: int) -> bool: """Evaluates new extended state indexed value in the extended state table. + + Returns True when the evaluated extended value is set to zero on all bits + but its two lowest ones - these two bits never change with MCGs. """ - state = (0xacb8_6d69 * (value ^ (value >> 22))) & 0xffff_ffff ##self._invxrs( value, 32, 22 ) + state = (0xacb8_6d69 * (value ^ (value >> 22))) & 0xffff_ffff state = self._invxrs( state, 32, 4 + (state >> 28) & 0x0f ) state = (0x108e_f2d9 * state + 2 * (i + 1)) & 0xffff_ffff diff --git a/PyRandLib/pcg128_64.py b/PyRandLib/pcg128_64.py index 6a81cf3..e62d9f0 100644 --- a/PyRandLib/pcg128_64.py +++ b/PyRandLib/pcg128_64.py @@ -161,7 +161,7 @@ def setstate(self, _state: Numerical) -> None: if _state < 0.0 : _state = -_state if _state >= 1.0: - self._state = int( _state + 0.5 ) & 0xffff_ffff_ffff_ffffffff_ffff_ffff_ffff + self._state = int( _state + 0.5 ) & 0xffff_ffff_ffff_ffff_ffff_ffff_ffff_ffff else: self._state = int( _state * 0x1_0000_0000_0000_0000_0000_0000_0000_0000) & 0xffff_ffff_ffff_ffff_ffff_ffff_ffff_ffff diff --git a/PyRandLib/squares32.py b/PyRandLib/squares32.py new file mode 100644 index 0000000..07b0b17 --- /dev/null +++ b/PyRandLib/squares32.py @@ -0,0 +1,167 @@ +""" +Copyright (c) 2025 Philippe Schmouker, schmouk (at) gmail.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +#============================================================================= +from .basesquares import BaseSquares +from .fastrand32 import FastRand32 +from .annotation_types import SeedStateType, StatesList + + +#============================================================================= +class Squares32( BaseSquares ): + """ + Pseudo-random numbers generator - Squares pseudo-random Generators + dedicated to 64-bits calculations and 32-bits output values with + small period (min 2^64, i.e. 1.84e+19) but short computation time. + All Squares algorithms offer multi streams features, by simply + using different initial settings for control value 'key'. + + This module is part of library PyRandLib. + + Copyright (c) 2025 Philippe Schmouker + + This Squares models is based on a four rounds of squaring and + exchanging of upper and lower bits of the successive combinations. + Output values are provided on 32-bits or on 64-bits according to + the model. See [9] in README.md. + + See Squares64 for a 2^64 (i.e. about 1.84e+19) period PRNG with + low computation time, medium period, 64-bits output values and + very good randomness characteristics. + + Furthermore this class is callable: + rand = Squares32() + print( rand() ) # prints a pseudo-random value within [0.0, 1.0) + print( rand(a) ) # prints a pseudo-random value within [0, a) or [0.0, a) depending on the type of a + print( rand(a, n) ) # prints a list of n pseudo-random values each within [0, a) + + Reminder: + We give you here below a copy of the table of tests for the Squares + that have been implemented in PyRandLib, as presented in paper [9] + - see file README.md. + + | PyRandLib class | [9] generator name | Memory Usage | Period | time-32bits | time-64 bits | SmallCrush fails | Crush fails | BigCrush fails | + | --------------- | ------------------ | ------------- | -------- | ----------- | ------------ | ---------------- | ----------- | -------------- | + | Squares32 | squares32 | 4 x 4-bytes | 2^64 | n.a. | n.a. | 0 | 0 | 0 | + | Squares64 | squares64 | 4 x 4-bytes | 2^64 | n.a. | n.a. | 0 | 0 | 0 |_ + + * _small crush_ is a small set of simple tests that quickly tests some of + the expected characteristics for a pretty good PRG; + * _crush_ is a bigger set of tests that test more deeply expected random + characteristics; + * _big crush_ is the ultimate set of difficult tests that any GOOD PRG + should definitively pass. + """ + + #------------------------------------------------------------------------- + def __init__(self, _seedState: SeedStateType = None) -> None: + """Constructor. + + Should _seedState be None then the local time is used as a seed (with + its shuffled value). + Notice: method setstate() is not implemented in base class BaseRandom. + So, it must be implemented in classes inheriting BaseLCG and it must + initialize attribute self._state. + """ + super().__init__( _seedState ) # this internally calls 'setstate()' which + # MUST be implemented in inheriting classes + + + #------------------------------------------------------------------------- + def next(self) -> int: + """This is the core of the pseudo-random generator. + """ + ''' + inline static uint32_t squares32(uint64_t ctr, uint64_t key) { + uint64_t x, y, z; + y = x = ctr * key; z = y + key; + x = x*x + y; x = (x>>32) | (x<<32); /* round 1 */ + x = x*x + z; x = (x>>32) | (x<<32); /* round 2 */ + x = x*x + y; x = (x>>32) | (x<<32); /* round 3 */ + return (x*x + z) >> 32; /* round 4 */ + ''' + self._counter += 1 + self._counter &= 0xffff_ffff_ffff_ffff + y = x = (self._counter * self._key) & 0xffff_ffff_ffff_ffff + z = (y + self._key) & 0xffff_ffff_ffff_ffff + # round 1 + x = (x * x + y) & 0xffff_ffff_ffff_ffff + x = (x >> 32) | ((x & 0xffff_ffff) << 32) + # round 2 + x = (x * x + z) & 0xffff_ffff_ffff_ffff + x = (x >> 32) | ((x & 0xffff_ffff) << 32) + # round 3 + x = (x * x + y) & 0xffff_ffff_ffff_ffff + x = (x >> 32) | ((x & 0xffff_ffff) << 32) + # round 4 + return ((x * x + z) & 0xffff_ffff_ffff_ffff) >> 32 + + + #------------------------------------------------------------------------- + def getstate(self) -> StatesList: + """Returns an object capturing the current internal state of the generator. + + This object can be passed to setstate() to restore the state. + For CWG, this state is defined by a list of control values + (a, weyl and s - or a list of 4 coeffs) and an internal state + value, which are used in methods 'next() and 'setstate() of + every inheriting class. + + All inheriting classes MUST IMPLEMENT this method. + """ + return (self._counter, self._key) + + + #------------------------------------------------------------------------- + def setstate(self, _state: SeedStateType) -> None: + """Restores the internal state of the generator. + + _state should have been obtained from a previous call + to getstate(), and setstate() restores the internal + state of the generator to what it was at the time + setstate() was called. + """ + if isinstance( _state, int ): + # passed initial seed is an integer, just uses it + self._counter = 0 + self._key = self._initKey( _state ) + + elif isinstance( _state, float ): + # transforms passed initial seed from float to integer + self._counter = 0 + if _state < 0.0 : + _state = -_state + if _state >= 1.0: + self._key = self._initKey( FastRand32(int(_state + 0.5) & 0xffff_ffff_ffff_ffff) ) + else: + self._key = self._initKey( FastRand32(int(_state * 0x1_0000_0000_0000_0000) & 0xffff_ffff_ffff_ffff) ) + + else: + try: + self._counter = _state[0] & 0xffff_ffff_ffff_ffff + self._key = _state[1] & 0xffff_ffff_ffff_ffff + except: + # uses local time as initial seed + self._counter = 0 + self._key = self._initKey( FastRand32() ) + +#===== end of module squares32.py ====================================== diff --git a/README.md b/README.md index 4cf49cf..0f012a9 100644 --- a/README.md +++ b/README.md @@ -63,8 +63,11 @@ In [1], every known PRNG at the time of the editing has been tested according to We give you here below a copy of the resulting table for the PRNGs that have been implemented in **PyRandLib**, as provided in [1], plus the Mersenne twister one which is not implemented in **PyRandLib**. We add in this table the evaluations provided by the authors of every new PRNGs that have been described after the publication of [1]. Fields may be missing then for them. A comparison of the computation times for all implemented PRNGs in **PyRandLib** is provided in an another belowing table. - | PyRabndLib class | TU01 generator name | Memory Usage | Period | time-32bits | time-64 bits | SmallCrush fails | Crush fails | BigCrush fails | + | PyRabndLib class | TU01 generator name (1) | Memory Usage | Period | time-32bits | time-64 bits | SmallCrush fails | Crush fails | BigCrush fails | | ---------------- | ---------------------------------- | --------------- | -------- | ----------- | ------------ | ---------------- | ----------- | -------------- | + | Cwg64 | *CWG64* | 8 x 4-bytes | >= 2^70 | n.a. | n.a. | 0 | 0 | 0 | + | Cwg128_64 | *CWG128-64* | 10 x 4-bytes | >= 2^71 | n.a. | n.a. | 0 | 0 | 0 | + | Cwg128 | *CWG128* | 16 x 4-bytes | >= 2^135 | n.a. | n.a. | 0 | 0 | 0 | | FastRand32 | LCG(2^32, 69069, 1) | 1 x 4-bytes | 2^32 | 3.20 | 0.67 | 11 | 106 | *too many* | | FastRand63 | LCG(2^63, 9219741426499971445, 1) | 2 x 4-bytes | 2^63 | 4.20 | 0.75 | 0 | 5 | 7 | | LFib78 | LFib(2^64, 17, 5, +) | 34 x 4-bytes | 2^78 | n.a. | 1.1 | 0 | 0 | 0 | @@ -74,16 +77,17 @@ We add in this table the evaluations provided by the authors of every new PRNGs | MRGRand287 | Marsa-LFIB4 | 256 x 4-bytes | 2^287 | 3.40 | 0.8 | 0 | 0 | 0 | | MRGRand1457 | DX-47-3 | 47 x 4-bytes | 2^1,457 | n.a. | 1.4 | 0 | 0 | 0 | | MRGRand49507 | DX-1597-2-7 | 1,597 x 4-bytes | 2^49,507 | n.a. | 1.4 | 0 | 0 | 0 | - | Pcg64_32 | not available | 2 x 4 bytes | 2^64 | n.a. | n.a. | 0 | 0 | 0 | - | Pcg128_64 | not available | 4 x 4 bytes | 2^128 | n.a. | n.a. | 0 | 0 | 0 | - | Pcg1024_32 | not available | 1,026 x 4 bytes | 2^32,830 | n.a. | n.a. | 0 | 0 | 0 | + | Pcg64_32 | *PCG XSH RS 64/32 (LCG)* | 2 x 4 bytes | 2^64 | n.a. | n.a. | 0 | 0 | 0 | + | Pcg128_64 | *PCG XSL RR 128/64 (LCG)* | 4 x 4 bytes | 2^128 | n.a. | n.a. | 0 | 0 | 0 | + | Pcg1024_32 | *PCG XSH RS 64/32 (EXT 1024)* | 1,026 x 4 bytes | 2^32,830 | n.a. | n.a. | 0 | 0 | 0 | | Well512a | not available | 16 x 4-bytes | 2^512 | n.a. | n.a. | n.a. | n.a. | n.a. | | Well1024a | WELL1024a | 32 x 4-bytes | 2^1,024 | 4.0 | 1.1 | 0 | 4 | 4 | - | Well19937b (1) | WELL19937a | 624 x 4-bytes | 2^19,937 | 4.3 | 1.3 | 0 | 2 | 2 | + | Well19937b (2) | WELL19937a | 624 x 4-bytes | 2^19,937 | 4.3 | 1.3 | 0 | 2 | 2 | | Well44497c | not available | 1,391 x 4-bytes | 2^44,497 | n.a. | n.a. | n.a. | n.a. | n.a. | | Mersenne twister | MT19937 | 6 x 4-bytes | 2^19,937 | 4.30 | 1.6 | 0 | 2 | 2 | -(1)The Well19937b generator provided with library PyRandLib implements the Well19937a algorithm augmented with an associated *tempering* algorithm. +(1)*or generator original name in related paper* +(2)The Well19937b generator provided with library PyRandLib implements the Well19937a algorithm augmented with an associated *tempering* algorithm. @@ -100,21 +104,24 @@ Up to now, it has only been run with a Python 3.9.13 (64-bits) virtual environme **PyRandLib** time-64 bits: | PyRabndLib class | Python 3.9 | Python 3.10 | Python 3.11 | Python 3.12 | Python 3.13 | SmallCrush fails | Crush fails | BigCrush fails | | ---------------- | ---------- | ----------- | ----------- | ----------- | ----------- | ---------------- | ----------- | -------------- | + | Cwg64 | 0.60 | | | | | 0 | 0 | 0 | + | Cwg128_64 | 0.60 | | | | | 0 | 0 | 0 | + | Cwg128 | 0.63 | | | | | 0 | 0 | 0 | | FastRand32 | 0.20 | | | | | 11 | 106 | *too many* | - | FastRand63 | 0.22 | | | | | 0 | 5 | 7 | - | LFib78 | 0.37 | | | | | 0 | 0 | 0 | - | LFib116 | 0.39 | | | | | 0 | 0 | 0 | - | LFib668 | 0.40 | | | | | 0 | 0 | 0 | - | LFib1340 | 0.41 | | | | | 0 | 0 | 0 | - | MRGRand287 | 0.60 | | | | | 0 | 0 | 0 | - | MRGRand1457 | 0.61 | | | | | 0 | 0 | 0 | - | MRGRand49507 | 0.58 | | | | | 0 | 0 | 0 | - | Pcg64_32 | 0.41 | | | | | 0 | 0 | 0 | - | Pcg128_64 | 0.59 | | | | | 0 | 0 | 0 | - | Pcg1024_32 | 0.82 | | | | | 0 | 0 | 0 | + | FastRand63 | 0.21 | | | | | 0 | 5 | 7 | + | LFib78 | 0.35 | | | | | 0 | 0 | 0 | + | LFib116 | 0.35 | | | | | 0 | 0 | 0 | + | LFib668 | 0.37 | | | | | 0 | 0 | 0 | + | LFib1340 | 0.39 | | | | | 0 | 0 | 0 | + | MRGRand287 | 0.57 | | | | | 0 | 0 | 0 | + | MRGRand1457 | 0.58 | | | | | 0 | 0 | 0 | + | MRGRand49507 | 0.54 | | | | | 0 | 0 | 0 | + | Pcg64_32 | 0.39 | | | | | 0 | 0 | 0 | + | Pcg128_64 | 0.57 | | | | | 0 | 0 | 0 | + | Pcg1024_32 | 0.80 | | | | | 0 | 0 | 0 | | Well512a | 1.95 | | | | | n.a. | n.a. | n.a. | | Well1024a | 1.80 | | | | | 0 | 4 | 4 | - | Well19937b (1) | 2.44 | | | | | 0 | 2 | 2 | + | Well19937b (1) | 2.43 | | | | | 0 | 2 | 2 | | Well44497c | 2.82 | | | | | n.a. | n.a. | n.a. | (1)The Well19937b generator provided with library PyRandLib implements the Well19937a algorithm augmented with an associated *tempering* algorithm. @@ -170,19 +177,22 @@ The call operator (i.e., '()') gets a new signature which is still backward com Version 2.0 of **PyRandLib** implements some new other "recent" PRNGs - see them listed below. It also provides two test scripts, enhanced documentation and some other internal development features: 1. The WELL algorithm (Well-Equilibrated Long-period Linear, see [6], 2006) is now implemented in **PyRandLib**. This algorithm has proven to very quickly escape from the zeroland (up to 1,000 times faster than the Mersenne-Twister algorithm, for instance) while providing large to very large periods and rather small computation time. -In **PyRandLib**, the WELL algorithm is provided in next forms: Well512a, Well1024a, Well19937c and Well44497b. +In **PyRandLib**, the WELL algorithm is provided in next forms: Well512a, Well1024a, Well19937c and Well44497b which all generate output values coded on 32-bits. -1. The PCG (Permuted Congruential Generator, see [7], 2014) is now implemented in **PyRandLib**. This algorithm is a very fast and enhanced on randomness quality version of Linear Congruential Generators. It is based on solid Mathematics foundation and clearly explained in technical report [7]. It offers jumping, hard to discover internal state and multi-streams featured. It passes all crush and big crush tests of TestU01. -**PyRandLib** implements its 3 major versions with resp. 2^32, 2^64 and 2^128 periodicities. The original library (C and C++) can be downloaded here: [https://www.pcg-random.org/downloads/pcg-cpp-0.98.zip](https://www.pcg-random.org/downloads/pcg-cpp-0.98.zip) as well as can code be cloned from here: [https://github.com/imneme/pcg-cpp](https://github.com/imneme/pcg-cpp). +1. The PCG algorithm (Permuted Congruential Generator, see [7], 2014) is now implemented in **PyRandLib**. This algorithm is a very fast and enhanced on randomness quality version of Linear Congruential Generators. It is based on solid Mathematics foundation and clearly explained in technical report [7]. It offers jumping, hard to discover internal state and multi-streams featured. It passes all crush and big crush tests of TestU01. +**PyRandLib** implements its 3 major versions with resp. 2^32, 2^64 and 2^128 periodicities: Pcg64_32, Pcg128-64 and Pcg1024_32 classes which generate output values coded on resp. 32-, 64- and 32- bits. The original library (C and C++) can be downloaded here: [https://www.pcg-random.org/downloads/pcg-cpp-0.98.zip](https://www.pcg-random.org/downloads/pcg-cpp-0.98.zip) as well as can code be cloned from here: [https://github.com/imneme/pcg-cpp](https://github.com/imneme/pcg-cpp). -1. A short script `testED.py` is now avalibale at root directory. It checks the equi-distribution of every PRNG implemented in **PyRandLib** in a simple way and is used to test for their maybe bad implementation within the library. Since release 2.0 this test is run on all PRNGs. +1. The CWG algorithm (Collatz-Weyl Generator, see [8], 2024) is now implemented in **PyRandLib**. This algorithm is fast, uses four integers as its internal state and generates chaos via multiplication and xored-shifted instructions. Periods are medium to large and the generated randomness is of up quality. It does not offer jump ahead but multi-streams feature is available via the simple modification of well specified one of the four integers. +2. In **PyRandLib**, the CWG algorithm is provided in next forms: Cwg64, Cwg64-128 and Cwg128 which generate output values coded on resp. 64-, 64- and 128- bits . + +3. A short script `testED.py` is now avalibale at root directory. It checks the equi-distribution of every PRNG implemented in **PyRandLib** in a simple way and is used to test for their maybe bad implementation within the library. Since release 2.0 this test is run on all PRNGs. It is now **highly recommended** to not use previous releases (aka. 1.x) of **PyRandLib**. 1. Another short script `testCPUPerfs.py` is now avaliable for testing CPU performance of the different implemented algorithms. It has been used to enhance this documentation by providing a new *times evaluation* table. -1. Documentation has been enhanced, with typos and erroneous docstrings fixed also. +2. Documentation has been enhanced, with typos and erroneous docstrings fixed also. -1. All developments are now done under a newly created branch named `dev`. This development branch may be derived into sub-branches for the development of new features. Merges from `dev` to branch `main` only happen when creating new releases. +3. All developments are now done under a newly created branch named `dev`. This development branch may be derived into sub-branches for the development of new features. Merges from `dev` to branch `main` only happen when creating new releases. So, if you want to see what is currently going on for next release, just check-out branch `dev`. 1. A Github project dedicated to **PyRandLib** has been created: the [pyrandlib](https://github.com/users/schmouk/projects/14) project. @@ -216,13 +226,60 @@ Notice: Since PyRandLib 2.0, class `BaseRandom` implements the new method `next( Since version 2.0 of PyRandLib also, the newly implemented method `getrandbits()` overrides the same method of Python built-in base class `random.Random`. + +### Cwg64 - minimum 2^70 period + +**Cwg64** implements the full 64 bits version of the Collatz-Weyl Generator algorithm: computations are done on 64-bits, the output generated value is coded on 64-bits also. It provides a medium period which is at minimum 2^70 (i.e. about 1.18e+21), short computation time and a four 64-bits integers internal state (x, a, weyl, s). + +This version of the CGW algorithm evaluates pseudo-random suites *output(i)* as the combination of the next instructions applied to *state(i-1)*: + + a(i) = a(i-1) + x(i-1) + weyl(i) = weyl(i-1) + s // s is constant over time and must be odd, this is the value to modify to get multi-streams + x(i) = ((x(i-1) >> 1) * ((a(i)) | 1)) ^ (weyl(i))) + output(i) = (a(i) >> 48) ^ x(i) + +See Cwg128_64 for a (minimum) 2^71 period (i.e. about 2.36e+21) and one 128-bits plus three 64-bits integers internal state. +See Cwg128 for a (minimum) 2^135 (i.e. about 4.36e+40) and a four 128-bits integers internal state. + + +### Cwg128_64 - minimum 2^71 period + +**Cwg128_64** implements the mixed 128/64 bits version of the Collatz-Weyl Generator algorithm: computations are done on 128- and 64-bits, the output generated value is coded on 64-bits also. It provides a medium period which is at minimum 2^71 (i.e. about 2.36e+21), short computation time and a three 64-bits (a, weyl, s) plus one 128-bits integer internal state (x). + +This version of the CGW algorithm evaluates pseudo-random suites *output(i)* as the combination of the next instructions applied to *state(i-1)*: + + a(i) = a(i-1) + x(i-1) + weyl(i) = weyl(i+1) + s // s is constant over time and must be odd, this is the value to modify to get multi-streams + x(i) = ((x(i-1) | 1) * (a(i) >> 1)) ^ (weyl(i)) + output(i) = (a(i) >> 48) ^ x(i) + +See Cwg64 for a (minimum) 2^70 period (i.e. about 1.18e+21) and four 64-bits integers internal state. +See Cwg128 for a (minimum) 2^135 (i.e. about 4.36e+40) and a four 128-bits integers internal state. + + + +### Cwg128 - minimum 2^135 period + +**Cwg128** implements the full 128 bits version of the Collatz-Weyl Generator algorithm: computations are done on 128-bits, the output generated value is coded on 128-bits also. It provides a medium period which is at minimum 2^135 (i.e. about 4.36e+40), short computation time and a four 128-bits integers internal state (x, a, weyl, s). + +This version of the CGW algorithm evaluates pseudo-random suites *output(i)* as the combination of the next instructions applied to *state(i-1)*: + + a(i) = a(i-1) + x(i-1) + weyl(i) = weyl(i-1) + s // s is constant over time and must be odd, this is the value to modify to get multi-streams + x(i) = ((x(i-1) >> 1) * ((a(i)) | 1)) ^ (weyl(i))) + output(i) = (a(i) >> 96) ^ x(i) + +See Cwg64 for a (minimum) 2^70 period (i.e. about 1.18e+21) and four 64-bits integers internal state. +See Cwg128_64 for a (minimum) 2^71 period (i.e. about 2.36e+21) and one 128-bits plus three 64-bits integers internal state. + + + ### FastRand32 - 2^32 periodicity **FastRand32** implements a Linear Congruential Generator dedicated to 32-bits calculations with very short period (about 4.3e+09) but very short time computation. -LCG models evaluate pseudo-random numbers suites *x(i)* as a simple -mathematical function of *x(i-1)*: +LCG models evaluate pseudo-random numbers suites *x(i)* as a simple mathematical function of *x(i-1)*: x(i) = ( a * x(i-1) + c ) mod m @@ -230,14 +287,13 @@ The implementation of **FastRand32** is based on (*a*=69069, *c*=1) since thes Results are nevertheless considered to be poor as stated in the evaluation done by Pierre L'Ecuyer and Richard Simard. Therefore, it is not recommended to use such pseudo-random numbers generators for serious simulation applications. -See FastRand63 for a 2^63 (i.e. about 9.2e+18) period LC-Generator with low computation time and *better* randomness characteristics. +See FastRand63 for a 2^63 (i.e. about 9.2e+18) period LC-Generator with low computation time and *better* randomness characteristics. ### FastRand63 - 2^63 periodicity -**FastRand63** implements a Linear Congruential Generator dedicated to 63-bits calculations with a short period (about 9.2e+18) and very short -time computation. +**FastRand63** implements a Linear Congruential Generator dedicated to 63-bits calculations with a short period (about 9.2e+18) and very short time computation. LCG model evaluate pseudo-random numbers suites *x(i)* as a simple mathematical function of *x(i-1)*: @@ -247,15 +303,13 @@ The implementation of this LCG 63-bits model is based on (*a*=921974142649997144 Results are nevertheless considered to be poor as stated in the evaluation done by Pierre L'Ecuyer and Richard Simard. Therefore, it is not recommended to use this pseudo-random numbers generatorsfor serious simulation applications, even if FastRandom63 fails on very far less tests than does FastRandom32. -See FastRand32 for a 2^32 period (i.e. about 4.3e+09) LC-Generator with 25% -lower computation time. +See FastRand32 for a 2^32 period (i.e. about 4.3e+09) LC-Generator with 25% lower computation time. ### LFibRand78 - 2^78 periodicity -**LFibRand78** implements a fast 64-bits Lagged Fibonacci generator (LFib). -Lagged Fibonacci generators *LFib( m, r, k, op)* use the recurrence +**LFibRand78** implements a fast 64-bits Lagged Fibonacci generator (LFib). Lagged Fibonacci generators *LFib( m, r, k, op)* use the recurrence x(i) = ( x(i-r) op (x(i-k) ) mod m @@ -271,11 +325,9 @@ The implementation of **LFibRand78** is based on a Lagged Fibonacci generator ( x(i) = ( x(i-5) + x(i-17) ) mod 2^64 -It offers a period of about 2^78 - i.e. 3.0e+23 - with low computation time -due to the use of a 2^64 modulo (less than twice the computation time of LCGs) and low memory consumption (17 integers 32-bits coded). +It offers a period of about 2^78 - i.e. 3.0e+23 - with low computation time due to the use of a 2^64 modulo (less than twice the computation time of LCGs) and low memory consumption (17 integers 32-bits coded). -Please notice that the TestUO1 article states that the operator should be '*' while George Marsaglia in its original article [4] used the operator -'+'. We've implemented in **PyRandLib** the original operator '+'. +Please notice that the TestUO1 article states that the operator should be '*' while George Marsaglia in its original article [4] used the operator '+'. We've implemented in **PyRandLib** the original operator '+'. @@ -687,3 +739,17 @@ Finally: Harvey Mudd College Computer Science Department Technical Report, HMC-C xurl = "https://www.cs.hmc.edu/tr/hmc-cs-2014-0905.pdf", } see also [https://www.pcg-random.org/pdf/hmc-cs-2014-0905.pdf](https://www.pcg-random.org/pdf/hmc-cs-2014-0905.pdf). + + +**[8]** Tomasz R. Dziala. 2023. +*Collatz-Weyl Generators: High Quality and High Throughput Parameterized Pseudorandom Number Generators*. +Published at arXiv, December 2023 (11 pages) +Last reference: arXiv:2312.17043v4 [cs.CE], 2 Dec 2024, +see [https://arxiv.org/abs/2312.17043](https://arxiv.org/abs/2312.17043). + + +**[9]** Bernard Widynski. March 2022. +*Squares: A Fast Counter-Based RNG*. +Published at arXiv, March 2022 (5 pages) +Last reference: arXiv:2004.06278v7 [cs.DS] 13 Mar 2022 +see [https://arxiv.org/pdf/2004.06278](https://arxiv.org/pdf/2004.06278). diff --git a/testCPUPerfs.py b/testCPUPerfs.py index 8420e67..b098d15 100644 --- a/testCPUPerfs.py +++ b/testCPUPerfs.py @@ -49,22 +49,25 @@ def test_perf(prng_class_name: str, seed_value: int, n_loops: int, n_repeats: in N = 15 - test_perf("FastRand32" , 0x3ca5_8796 , 2_000_000, N) - test_perf("FastRand63" , 0x3ca5_8796_1f2e_b45a, 2_000_000, N) - test_perf("LFib78" , 0x3ca5_8796_1f2e_b45a, 2_000_000, N) - test_perf("LFib116" , 0x3ca5_8796_1f2e_b45a, 2_000_000, N) - test_perf("LFib668" , 0x3ca5_8796_1f2e_b45a, 2_000_000, N) - test_perf("LFib1340" , 0x3ca5_8796_1f2e_b45a, 2_000_000, N) - test_perf("MRGRand287" , 0x3ca5_8796 , 2_000_000, N) - test_perf("MRGRand1457" , 0x3ca5_8796 , 2_000_000, N) - test_perf("MRGRand49507", 0x3ca5_8796 , 2_000_000, N) - test_perf("Pcg64_32" , 0x3ca5_8796_1f2e_b45a, 2_000_000, N) - test_perf("Pcg128_64" , 0x3ca5_8796_1f2e_b45a_3ca5_8796_1f2e_b45a, 2_000_000, N) - test_perf("Pcg1024_32" , 0x3ca5_8796_1f2e_b45a, 2_000_000, N) - test_perf("Well512a" , 0x3ca5_8796 , 1_000_000, N) - test_perf("Well1024a" , 0x3ca5_8796 , 1_000_000, N) - test_perf("Well19937c" , 0x3ca5_8796 , 1_000_000, N) - test_perf("Well44497b" , 0x3ca5_8796 , 1_000_000, N) + test_perf("Cwg64" , 0x3ca5_8796 , 100_000, N) + test_perf("Cwg128_64" , 0x3ca5_8796_1f2e_b45a, 100_000, N) + test_perf("Cwg128" , 0x3ca5_8796_1f2e_b45a_3ca5_8796_1f2e_b45a, 100_000, N) + #test_perf("FastRand32" , 0x3ca5_8796 , 100_000, N) + #test_perf("FastRand63" , 0x3ca5_8796_1f2e_b45a, 100_000, N) + #test_perf("LFib78" , 0x3ca5_8796_1f2e_b45a, 100_000, N) + #test_perf("LFib116" , 0x3ca5_8796_1f2e_b45a, 100_000, N) + #test_perf("LFib668" , 0x3ca5_8796_1f2e_b45a, 100_000, N) + #test_perf("LFib1340" , 0x3ca5_8796_1f2e_b45a, 100_000, N) + #test_perf("MRGRand287" , 0x3ca5_8796 , 100_000, N) + #test_perf("MRGRand1457" , 0x3ca5_8796 , 100_000, N) + #test_perf("MRGRand49507", 0x3ca5_8796 , 100_000, N) + #test_perf("Pcg64_32" , 0x3ca5_8796_1f2e_b45a, 100_000, N) + #test_perf("Pcg128_64" , 0x3ca5_8796_1f2e_b45a_3ca5_8796_1f2e_b45a, 100_000, N) + #test_perf("Pcg1024_32" , 0x3ca5_8796_1f2e_b45a, 100_000, N) + #test_perf("Well512a" , 0x3ca5_8796 , 100_000, N) + #test_perf("Well1024a" , 0x3ca5_8796 , 100_000, N) + #test_perf("Well19937c" , 0x3ca5_8796 , 100_000, N) + #test_perf("Well44497b" , 0x3ca5_8796 , 100_000, N) #===== end of module testCPUPerfs.py =================================== diff --git a/testED.py b/testED.py index cb458a9..0e193cc 100644 --- a/testED.py +++ b/testED.py @@ -99,7 +99,8 @@ def test_algo(rnd_algo, nb_entries: int = 1_000, nb_loops: int = 1_000_000): elif variance > max_variance: max_variance = variance - print(f" variances are in range [{min_variance:,.3f} ; {'+' if max_variance > 0.0 else ''}{max_variance:,.3f}]") + print(f" variances are in range [{min_variance:,.3f} ; {'+' if max_variance > 0.0 else ''}{max_variance:,.3f}]", end='') + print(f", min: {min(hist)}, max: {max(hist)}") if (not err): print(" Test OK.") @@ -108,22 +109,25 @@ def test_algo(rnd_algo, nb_entries: int = 1_000, nb_loops: int = 1_000_000): #============================================================================= if __name__ == "__main__": - test_algo(FastRand32(), 3217, nb_loops = 2_000_000) # notice: 3217 is a prime number - test_algo(FastRand63(), 3217, nb_loops = 2_000_000) - test_algo(LFib78(), 3217, nb_loops = 2_000_000) - test_algo(LFib116(), 3217, nb_loops = 2_000_000) - test_algo(LFib668(), 3217, nb_loops = 2_000_000) - test_algo(LFib1340(), 3217, nb_loops = 2_000_000) - test_algo(MRGRand287(), 3217, nb_loops = 2_000_000) - test_algo(MRGRand1457(), 3217, nb_loops = 2_000_000) - test_algo(MRGRand49507(), 3217, nb_loops = 2_000_000) - test_algo(Pcg64_32(), 3217, nb_loops = 2_000_000) - test_algo(Pcg128_64(), 3217, nb_loops = 2_000_000) - test_algo(Pcg1024_32(), 3217, nb_loops = 2_000_000) - test_algo(Well512a(), 3217, nb_loops = 1_500_000) - test_algo(Well1024a(), 3217, nb_loops = 1_500_000) - test_algo(Well19937c(), nb_entries = 2029) # notice: 2029 is a prime number - test_algo(Well44497b(), nb_entries = 2029) + test_algo(Cwg64(), 3217, nb_loops = 2_000_000) # notice: 3217 is a prime number + test_algo(Cwg128_64(), 3217, nb_loops = 2_000_000) + test_algo(Cwg128(), 3217, nb_loops = 2_000_000) + #test_algo(FastRand32(), 3217, nb_loops = 2_000_000) + #test_algo(FastRand63(), 3217, nb_loops = 2_000_000) + #test_algo(LFib78(), 3217, nb_loops = 2_000_000) + #test_algo(LFib116(), 3217, nb_loops = 2_000_000) + #test_algo(LFib668(), 3217, nb_loops = 2_000_000) + #test_algo(LFib1340(), 3217, nb_loops = 2_000_000) + #test_algo(MRGRand287(), 3217, nb_loops = 2_000_000) + #test_algo(MRGRand1457(), 3217, nb_loops = 2_000_000) + #test_algo(MRGRand49507(), 3217, nb_loops = 2_000_000) + #test_algo(Pcg64_32(), 3217, nb_loops = 2_000_000) + #test_algo(Pcg128_64(), 3217, nb_loops = 2_000_000) + #test_algo(Pcg1024_32(), 3217, nb_loops = 2_000_000) + #test_algo(Well512a(), 3217, nb_loops = 1_500_000) + #test_algo(Well1024a(), 3217, nb_loops = 1_500_000) + #test_algo(Well19937c(), nb_entries = 2029) # notice: 2029 is a prime number + #test_algo(Well44497b(), nb_entries = 2029) #===== end of module testED.py =========================================