Skip to content

Commit ab5bf6f

Browse files
committed
[math] implement PRBS generator
Fixes #8199 [math] move to free-standing functions in namespace [nfc] spell out return types in tutorial as suggested by guitargeek [math] LFSR: allow setting output typename, default to uchar also remove const qualifiers from signature since they are copied by value and apply clang-formatting [nfc] fix typo in docu
1 parent 8ea9f59 commit ab5bf6f

File tree

7 files changed

+251
-9
lines changed

7 files changed

+251
-9
lines changed

documentation/users-guide/MathLibraries.md

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,15 @@ which are provided in the `ROOT::Math` namespace are:
3434
for evaluating one-dimensional (`ROOT::Math::IBaseFunctiononeDim`) and multi-dimensional functions
3535
(`ROOT::Math::IBaseFunctionMultiDim`) and parametric function interfaces for evaluating functions with parameters in one
3636
(`ROOT::Math::IParametricFunctionOneDim`) or multi dimensions (`ROOT::Math::IParametricFunctionMultiDim`).
37-
A set of user convenient wrapper classes, such as `ROOT::Math::Functor` is provided for wrapping user-classes in the needed interface,
38-
required to use the algorithms of the `ROOT` Mathematical libraries.
37+
A set of user convenient wrapper classes, such as `ROOT::Math::Functor` is provided for wrapping user-classes in the needed interface,
38+
required to use the algorithms of the `ROOT` Mathematical libraries.
3939

4040
- Numerical algorithms interfaces and in same cases default implementations for:
4141
- numerical integration;
4242
- numerical differentiation;
43-
- one dimensional root-finding;
44-
- one-dimensional minimization;
45-
- multi-dimensional minimization (only the `ROOT::Math::Minimizer` interface)
43+
- one dimensional root-finding;
44+
- one-dimensional minimization;
45+
- multi-dimensional minimization (only the `ROOT::Math::Minimizer` interface)
4646

4747
- Fitting classes: set of classes for fitting generic data sets. These classes are provided in the namespace `ROOT::Fit`.
4848
They are describing separately in the Fitting chapter.
@@ -268,6 +268,7 @@ classes. 4 different types exist: **`TRandom`**, **`TRandom1`**,
268268
of random generators. **`TRandom`** is the base class used by others. It
269269
implements methods for generating random numbers according to
270270
pre-defined distributions, such as Gaussian or Poisson.
271+
For random bit sequence generators, see **`ROOT::Math::LFSR`**.
271272

272273
### TRandom
273274

@@ -592,6 +593,11 @@ Here are the CPU times obtained using the four random classes on an
592593
| `UNURAN` | | | | |
593594
+--------------------+---------------+----------------+----------------+----------------+
594595

596+
### ROOT::Math::LFSR
597+
598+
This namespace contains free functions to generate pseudo-random binary sequences,
599+
to match those often implemented in electronic chips, based on linear feedback shift
600+
registers (LFSR). A usage example can be found in $ROOTSYS/tutorials/math/PRBS.C.
595601

596602
## Mathematical Functions
597603

@@ -1469,10 +1475,10 @@ iteration the subinterval with the largest estimated error is bisected. It is po
14691475
* `Integration::kGAUSS41` : 41 points Gauss-Konrod rule (value = 4)
14701476
* `Integration::kGAUSS51` : 51 points Gauss-Konrod rule (value = 5)
14711477
* `Integration::kGAUSS61` : 61 points Gauss-Konrod rule (value = 6)
1472-
The higher-order rules give better accuracy for smooth functions, while lower-order rules save time when the function contains local difficulties, such as discontinuities. If no integration rule
1473-
is passed, the 31 points rule is used as default.
1478+
The higher-order rules give better accuracy for smooth functions, while lower-order rules save time when the function contains local difficulties, such as discontinuities. If no integration rule
1479+
is passed, the 31 points rule is used as default.
14741480

1475-
* `ROOT::Math::Integration::kADAPTIVESINGULAR`: based on `gsl_integration_qags`. It is an integration type which can be used in the case of the presence of singularities.It uses the
1481+
* `ROOT::Math::Integration::kADAPTIVESINGULAR`: based on `gsl_integration_qags`. It is an integration type which can be used in the case of the presence of singularities.It uses the
14761482
Gauss-Kronrod 21-point integration rule. This is the default algorithm
14771483

14781484
Note that when using the common `ROOT::Math::IntegratorOneDIm` class the enumeration type defining the algorithm must be defined in the namespace `ROOT::Math::IntegrationOneDim` (to distinguish from

math/mathcore/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ set(HEADERS
5757
Math/IntegratorOptions.h
5858
Math/KDTree.h
5959
Math/LCGEngine.h
60+
Math/LFSR.h
6061
Math/Math.h
6162
Math/MersenneTwisterEngine.h
6263
Math/MinimTransformFunction.h

math/mathcore/inc/Math/LFSR.h

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// @(#)root/mathcore:$Id$
2+
// Author: Fernando Hueso-González 04/08/2021
3+
4+
#ifndef ROOT_Math_LFSR
5+
#define ROOT_Math_LFSR
6+
7+
#include <array>
8+
#include <bitset>
9+
#include <vector>
10+
#include <set>
11+
#include <cmath>
12+
#include "TError.h"
13+
14+
/// Pseudo Random Binary Sequence (PRBS) generator namespace with functions based
15+
/// on linear feedback shift registers (LFSR) with a periodicity of 2^n-1
16+
///
17+
/// @note It should NOT be used for general-purpose random number generation or any
18+
/// statistical study, for those cases see e.g. std::mt19937 instead.
19+
///
20+
/// The goal is to generate binary bit sequences with the same algorithm as the ones usually implemented
21+
/// in electronic chips, so that the theoretically expected ones can be compared with the acquired sequences.
22+
///
23+
/// The main ingredients of a PRBS generator are a monic polynomial of maximum degree \f$n\f$, with coefficients
24+
/// either 0 or 1, and a <a href="https://www.nayuki.io/page/galois-linear-feedback-shift-register">Galois</a>
25+
/// linear-feedback shift register with a non-zero seed. When the monic polynomial exponents are chosen appropriately,
26+
/// the period of the resulting bit sequence (0s and 1s) yields \f$2^n - 1\f$.
27+
///
28+
/// @sa https://gist.github.com/mattbierner/d6d989bf26a7e54e7135, https://root.cern/doc/master/civetweb_8c_source.html#l06030,
29+
/// https://cryptography.fandom.com/wiki/Linear_feedback_shift_register, https://www3.advantest.com/documents/11348/33b24c8a-c8cb-40b8-a2a7-37515ba4abc8,
30+
/// https://www.reddit.com/r/askscience/comments/63a10q/for_prbs3_with_clock_input_on_each_gate_how_can/, https://es.mathworks.com/help/serdes/ref/prbs.html,
31+
/// https://metacpan.org/pod/Math::PRBS, https://ez.analog.com/data_converters/high-speed_adcs/f/q-a/545335/ad9689-pn9-and-pn23
32+
33+
34+
35+
namespace ROOT::Math::LFSR {
36+
37+
/**
38+
* @brief Generate the next pseudo-random bit using the current state of a linear feedback shift register (LFSR) and update it
39+
* @tparam k the length of the LFSR, usually also the order of the monic polynomial PRBS-k (last exponent)
40+
* @tparam nTaps the number of taps
41+
* @param lfsr the current value of the LFSR. Passed by reference, it will be updated with the next value
42+
* @param taps the taps that will be XOR-ed to calculate the new bit. They are the exponents of the monic polynomial. Ordering is unimportant. Note that an exponent E in the polynom maps to bit index E-1 in the LFSR.
43+
* @param left if true, the direction of the register shift is to the left <<, the newBit is set on lfsr at bit position 0 (right). If false, shift is to the right and the newBit is stored at bit position (k-1)
44+
* @return the new random bit
45+
* @throw an exception is thrown if taps are out of the range [1, k]
46+
* @see https://en.wikipedia.org/wiki/Monic_polynomial, https://en.wikipedia.org/wiki/Linear-feedback_shift_register, https://en.wikipedia.org/wiki/Pseudorandom_binary_sequence
47+
*/
48+
template <size_t k, size_t nTaps>
49+
static bool NextLFSR(std::bitset<k> &lfsr, std::array<uint16_t, nTaps> taps, bool left = true)
50+
{
51+
static_assert(k <= 32, "For the moment, only supported until k == 32.");
52+
static_assert(k > 0, "Non-zero degree is needed for the LFSR.");
53+
static_assert(nTaps > 0, "At least one tap is needed for the LFSR.");
54+
static_assert(nTaps <= k, "Cannot use more taps than polynomial order");
55+
for (uint16_t j = 0; j < nTaps; ++j) {
56+
static_assert((taps[j] - 1) <= k && (taps[j] - 1) > 0, "Tap value is out of range [1,k]");
57+
}
58+
59+
// First, calculate the XOR (^) of all selected bits (marked by the taps)
60+
bool newBit = lfsr[taps[0] - 1]; // the exponent E of the polynomial correspond to index E - 1 in the bitset
61+
for (uint16_t j = 1; j < nTaps; ++j) {
62+
newBit ^= lfsr[taps[j] - 1];
63+
}
64+
65+
// Apply the shift to the register in the right direction, and overwrite the empty one with newBit
66+
if (left) {
67+
lfsr <<= 1;
68+
lfsr[0] = newBit;
69+
} else {
70+
lfsr >>= 1;
71+
lfsr[k - 1] = newBit;
72+
}
73+
74+
return newBit;
75+
}
76+
77+
/**
78+
* @brief Generation of a sequence of pseudo-random bits using a linear feedback shift register (LFSR), until a
79+
* register value is repeated (or maxPeriod is reached)
80+
* @tparam k the length of the LFSR, usually also the order of the monic polynomial PRBS-k (last exponent)
81+
* @tparam nTaps the number of taps
82+
* @tparam Output the type of the container where the bit result (0 or 1) is stored (e.g. char, bool). It's unsigned
83+
* char by default, use bool instead if you want to save memory
84+
* @param start the start value (seed) of the LFSR
85+
* @param taps the taps that will be XOR-ed to calculate the new bit. They are the exponents of the monic polynomial.
86+
* Ordering is unimportant. Note that an exponent E in the polynom maps to bit index E-1 in the LFSR.
87+
* @param[out] iterator container storing the array of pseudo random bits (iterator is left untouched if input
88+
* parameters were incorrect) Pass for example an iterator to std::vector<unsigned char> (or to bool if memory saving
89+
* is important)
90+
* @param left if true, the direction of the register shift is to the left <<, the newBit is set on lfsr at bit
91+
* position 0 (right). If false, shift is to the right and the newBit is stored at bit position (k-1)
92+
* @param wrapping if true, allow repetition of values in the LFSRhistory, until maxPeriod is reached or the repeated
93+
* value == start. Enabling this option saves memory as no history is kept
94+
* @param oppositeBit if true, use the high/low bit of the LFSR to store output (for left=true/false, respectively)
95+
* instead of the newBit returned by ::NextLFSR
96+
* @return the array of pseudo random bits, or an empty array if input was incorrect
97+
* @see https://en.wikipedia.org/wiki/Monic_polynomial, https://en.wikipedia.org/wiki/Linear-feedback_shift_register,
98+
* https://en.wikipedia.org/wiki/Pseudorandom_binary_sequence
99+
*/
100+
template <size_t k, size_t nTaps, typename Output = unsigned char>
101+
static std::vector<Output> GenerateSequence(std::bitset<k> start, std::array<uint16_t, nTaps> taps, bool left = true,
102+
bool wrapping = false, bool oppositeBit = false)
103+
{
104+
std::vector<bool> result; // Store result here
105+
106+
// Sanity-checks
107+
static_assert(k <= 32, "For the moment, only supported until k == 32.");
108+
static_assert(k > 0, "Non-zero degree is needed for the LFSR.");
109+
static_assert(nTaps >= 2, "At least two taps are needed for a proper sequence");
110+
static_assert(nTaps <= k, "Cannot use more taps than polynomial order");
111+
for (auto tap : taps) {
112+
if (tap > k || tap == 0) {
113+
Error("ROOT::Math::LFSR", "Tap %u is out of range [1,%lu]", tap, k);
114+
return result;
115+
}
116+
}
117+
if (start.none()) {
118+
Error("ROOT::Math::LFSR", "A non-zero start value is needed");
119+
return result;
120+
}
121+
122+
// Calculate maximum period and pre-allocate space in result
123+
const uint32_t maxPeriod = pow(2, k) - 1;
124+
result.reserve(maxPeriod);
125+
126+
std::set<uint32_t> lfsrHistory; // a placeholder to store the history of all different values of the LFSR
127+
std::bitset<k> lfsr(start); // a variable storing the current value of the LFSR
128+
uint32_t i = 0; // a loop counter
129+
if (oppositeBit) // if oppositeBit enabled, first value is already started with the seed
130+
result.emplace_back(left ? lfsr[k - 1] : lfsr[0]);
131+
132+
// Loop now until maxPeriod or a lfsr value is repeated. If wrapping enabled, allow repeated values if not equal
133+
// to seed
134+
do {
135+
bool newBit = NextLFSR(lfsr, taps, left);
136+
137+
if (!oppositeBit)
138+
result.emplace_back(newBit);
139+
else
140+
result.emplace_back(left ? lfsr[k - 1] : lfsr[0]);
141+
142+
++i;
143+
144+
if (!wrapping) // If wrapping not allowed, break the loop once a repeated value is encountered
145+
{
146+
if (lfsrHistory.count(lfsr.to_ulong()))
147+
break;
148+
149+
lfsrHistory.insert(lfsr.to_ulong()); // Add to the history
150+
}
151+
} while (lfsr != start && i < maxPeriod);
152+
153+
if (oppositeBit)
154+
result.pop_back(); // remove last element, as we already pushed the one from the seed above the while loop
155+
156+
result.shrink_to_fit(); // only some special taps will lead to the maxPeriod, others will stop earlier
157+
158+
return;
159+
}
160+
}
161+
162+
#endif

math/mathcore/src/TRandom2.cxx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
66
\class TRandom2
77
8-
Random number generator class based on the maximally quidistributed combined
8+
Random number generator class based on the maximally equidistributed combined
99
Tausworthe generator by L'Ecuyer.
1010
1111
The period of the generator is 2**88 (about 10**26) and it uses only 3 words

math/mathcore/test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,4 @@ if(clad)
102102
endif()
103103

104104
ROOT_ADD_GTEST(testFitter testFitter.cxx LIBRARIES Core MathCore)
105+
ROOT_ADD_GTEST(testLFSR testLFSR.cxx LIBRARIES Core MathCore)

math/mathcore/test/testLFSR.cxx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
2+
#include <Math/LFSR.h>
3+
#include "gtest/gtest.h"
4+
5+
TEST(LFSR, GenerateSequence)
6+
{
7+
// PRBS3
8+
std::array<uint16_t, 2> taps3 = {2, 3}; // Exponents of the monic polynomial
9+
auto prbs3 = ROOT::Math::LFSR::GenerateSequence<3, 2, bool>(std::bitset<3>().flip(), taps3); // Start value all high
10+
EXPECT_EQ(prbs3, std::vector<bool>({false, false, true, false, true, true, true}));
11+
12+
// PRBS4
13+
std::array<uint16_t, 2> taps4 = {3, 4}; // Exponents of the monic polynomial
14+
auto prbs4 = ROOT::Math::LFSR::GenerateSequence<4, 2, bool>(std::bitset<4>().flip(), taps4); // Start value all high
15+
EXPECT_EQ(prbs4, std::vector<bool>({false, false, false, true, false, false, true, true, false, true, false, true,
16+
true, true, true}));
17+
18+
// PRBS7
19+
std::array<uint16_t, 2> taps5 = {5, 3}; // Exponents of the monic polynomial
20+
auto prbs5 = ROOT::Math::LFSR::GenerateSequence<5, 2, bool>(std::bitset<5>().flip(), taps5); // Start value all high
21+
EXPECT_EQ(prbs5, std::vector<bool>({false, false, false, true, true, false, true, true, true, false, true,
22+
false, true, false, false, false, false, true, false, false, true, false,
23+
true, true, false, false, true, true, true, true, true}));
24+
}

tutorials/math/PRBS.C

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/// \file
2+
/// \ingroup tutorial_math
3+
/// \notebook -nodraw
4+
/// Tutorial illustrating the use of ROOT::Math::LFSR::GenerateSequence
5+
/// can be run with:
6+
///
7+
/// ~~~{.cpp}
8+
/// root > .x PRBS.C
9+
/// root > .x PRBS.C+ with ACLIC
10+
/// ~~~
11+
///
12+
/// These values match the PRBS sequences generated by a Keysight waveform generator
13+
/// except for the N first bits that appear at the end rather than at the beginning
14+
/// \see https://www.sciencedirect.com/science/article/pii/S0168900222002431#fig6
15+
///
16+
/// \macro_output
17+
/// \macro_code
18+
///
19+
/// \author Fernando Hueso-González
20+
21+
#include <Math/LFSR.h>
22+
#include <iostream>
23+
24+
void PRBS()
25+
{
26+
printf("\nLFSR::GenerateSequence PRBS3, PRBS4 and PRBS5 tests\n");
27+
printf("==========================\n");
28+
29+
// PRBS3
30+
std::array<uint16_t, 2> taps3 = {2, 3}; // Exponents of the monic polynomial
31+
auto prbs3 = ROOT::Math::LFSR::GenerateSequence(std::bitset<3>().flip(), taps3); // Start value all high
32+
33+
// PRBS4
34+
std::array<uint16_t, 2> taps4 = {3, 4}; // Exponents of the monic polynomial
35+
auto prbs4 = ROOT::Math::LFSR::GenerateSequence(std::bitset<4>().flip(), taps4); // Start value all high
36+
37+
// PRBS7
38+
std::array<uint16_t, 2> taps5 = {5, 3}; // Exponents of the monic polynomial
39+
auto prbs5 = ROOT::Math::LFSR::GenerateSequence(std::bitset<5>().flip(), taps5); // Start value all high
40+
41+
for (auto prbs : {prbs3, prbs4, prbs5}) {
42+
std::cout << "PRBS period " << prbs.size() << ":\t";
43+
for (auto p : prbs) {
44+
std::cout << static_cast<unsigned short>(p) << " ";
45+
}
46+
std::cout << std::endl;
47+
}
48+
}

0 commit comments

Comments
 (0)