Skip to content

Commit a7a05e6

Browse files
committed
Add an LFSR module
This module, which is heavily based on Nikesh Bajaj's pylsfr (https://pylfsr.github.io), implements a Linear Feedback Shift Register that can be used to generate pseudo-random boolean sequences. It supports both Fibonacci and Galois LFSRs. LFSRs are used in many digital communication systems (including, for example LTE and 5GNR). For more information see https://simple.wikipedia.org/wiki/Linear-feedback_shift_register
1 parent db4bec2 commit a7a05e6

File tree

3 files changed

+406
-1
lines changed

3 files changed

+406
-1
lines changed

README.md

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ block Complex:
6868
# @[(4.5, 0.0), (2.081559480312316, -1.651098762732523), (-1.831559480312316, 1.608220406444071), (-1.831559480312316, -1.608220406444071), (2.081559480312316, 1.651098762732523)]
6969
```
7070

71-
7271
### C++ example
7372

7473
When compiling on the C++ backend, the API is a bit different:
@@ -99,6 +98,96 @@ echo dOut
9998
# @[(4.5, 0.0), (2.081559480312316, -1.651098762732523), (-1.831559480312316, 1.608220406444071), (0.0, 0.0), (0.0, 0.0)]
10099
```
101100

101+
## LFSR
102+
103+
LFSR module which implements a Linear Feedback Shift Register that can be
104+
used to generate pseudo-random boolean sequences. It supports both Fibonacci
105+
and Galois LFSRs.
106+
107+
LFSRs used in many digital communication systems (including, for example LTE
108+
and 5GNR). For more information see:
109+
110+
https://simple.wikipedia.org/wiki/Linear-feedback_shift_register
111+
112+
Note that this module uses Arraymancer under the hood, so it depends on it.
113+
Also note that this code is heavily based on Nikesh Bajaj's pylfsr, which can
114+
be found in https://pylfsr.github.io
115+
116+
### LFSR examples
117+
118+
The following example creates a Fibonacci-style LFSR with polynomial
119+
`x^5 + x^3 + 1` and uses it to generate a pseudo-random sequence of 31 values.
120+
Note how the `taps` argument is an integer tensor with values `[5, 3]`,
121+
corresponding to the exponents of the coefficients of the polynomial, in
122+
descending order. Exponent 0 was skipped because it is implicitly included
123+
(if it is included it will be ignored). If the exponents are not in
124+
descending order a ValueError exception will be raised.
125+
126+
```nim
127+
import impulse/lfsr
128+
import arraymancer
129+
130+
var fibonacci_lfsr = initLFSR(
131+
taps = [5, 3], # descending order and 0 can be omitted
132+
# The following 2 lines can be skipped in this case since they are the defaults
133+
# state = single_true,
134+
# conf = fibonacci
135+
)
136+
137+
let sequence1 = fibonacci_lfsr.generate(31)
138+
139+
# Print the first few elements
140+
# Note that sequence1 will be a Tensor[bool] but it can be easily converted to
141+
# Tensor[int] for more concise printing
142+
echo sequence1.asType(int)[_..11]
143+
# Tensor[system.int] of shape "[12]" on backend "Cpu"
144+
# 1 0 0 0 0 1 0 0 1 0 1 1
145+
146+
# The generator can be reset to start over
147+
fibonacci_lfsr.reset()
148+
let sequence2 = fibonacci_lfsr.generate(31)
149+
doAssert sequence1 == sequence2
150+
```
151+
152+
Galois style LFSRs are also supported and it is also possible to set a custom
153+
start state as a Tensor[bool]:
154+
155+
```nim
156+
var galois_lfsr = initLFSR(
157+
# note how the 0 exponent can be included and taps can be a Tensor as well
158+
taps = [5, 3, 0].toTensor,
159+
state = [true, true, true, true, true].toTensor, # this is equivalent to `all_true`
160+
conf = galois
161+
)
162+
163+
# Generate the first 8 values
164+
let sequence3a = galois_lfsr.generate(8)
165+
echo sequence3a.asType(int)
166+
# Tensor[system.int] of shape "[8]" on backend "Cpu"
167+
# 1 1 1 1 0 0 0 1
168+
169+
# Generate a few more values
170+
let sequence3b = galois_lfsr.generate(10)
171+
echo sequence3b.asType(int)
172+
# Tensor[system.int] of shape "[10]" on backend "Cpu"
173+
# 1 0 1 1 1 0 1 0 1 0
174+
175+
galois_lfsr.reset()
176+
echo galois_lfsr.generate(18)
177+
# Tensor[system.int] of shape "[18]" on backend "Cpu"
178+
# 1 1 1 1 0 0 0 1 1 0 1 1 1 0 1 0 1 0
179+
```
180+
181+
### Maximal LFSR tap examples
182+
183+
As a convenience, a `tap_examples` function is provided. This function takes a
184+
`size` and returns one example (out of many) sequence of taps that generates a
185+
"maximal" LSFR sequence.
186+
187+
### LFSR efficiency
188+
189+
The LFSR module implementation is favors simplicity over speed. As of 2024, it
190+
is able to generate 2^24 values in less than 1 minute on a mid-range laptop.
102191

103192
## License
104193

impulse/lfsr.nim

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
## LFSR module which implements a Linear Feedback Shift Register that can be
2+
## used to generate pseudo-random boolean sequences. It supports both Fibonacci
3+
## and Galois LFSRs.
4+
##
5+
## LFSRs used in many digital communication systems (including, for example LTE
6+
## and 5GNR). For more information see:
7+
## https://simple.wikipedia.org/wiki/Linear-feedback_shift_register
8+
##
9+
## Notes:
10+
## - This code is heavily based on Nikesh Bajaj's pylfsr, which can be
11+
## found in https://pylfsr.github.io
12+
## - This implementation is not optimized for performance, but for simplicity.
13+
## It would be relatively trivial to implement a much faster version by
14+
## operating on the bits directly, rather than using tensors.
15+
16+
import arraymancer
17+
import std / [algorithm, strformat]
18+
19+
type LFSR_TYPE* = enum
20+
## LFSR types (see https://simple.wikipedia.org/wiki/Linear-feedback_shift_register)
21+
fibonacci, galois
22+
23+
type LFSR_INIT_STATE_TYPE* = enum
24+
## Common LFSR Initial States
25+
## While it is possible to set any LFSR initial `state` by passing a
26+
## Tensor[bool] to `initLFSR`, for convenience it is also possible
27+
## to pass one of these enum values as the initial `state`:
28+
## - `single_true`: all bits set to `false` except the LSB (i.e. the last one)
29+
## - `all_true`: all bits set to `true`
30+
single_true, all_true
31+
32+
type LFSR* = object
33+
## LFSR object used to generate fibonacci or galois pseudo random sequences
34+
## To use it first create the object using `initLFSR` and then call either
35+
## the `next` procedure to get the sequence values one by one or `generate`
36+
## to generate multiple values in one go.
37+
taps*: Tensor[int]
38+
conf*: LFSR_TYPE
39+
state*: Tensor[bool]
40+
init_state: Tensor[bool]
41+
verbose*: bool
42+
counter_starts_at_zero*: bool
43+
outbit*: bool
44+
count*: int
45+
seq_bit_index: int
46+
sequence: Tensor[bool]
47+
feedbackbit: bool
48+
49+
proc initLFSR*(taps: Tensor[int] | seq[int],
50+
conf = fibonacci,
51+
state: Tensor[bool],
52+
verbose = false,
53+
counter_starts_at_zero = true): LFSR =
54+
## Initialize LFSR with given feedback polynomial and initial state
55+
##
56+
## Inputs:
57+
## - taps: Feedback polynomial taps as a Tensor[int] or seq[int] of exponents
58+
## in descending order. Exponent 0 can be omitted, since it is always
59+
## implicitly added. For example, to use the `x^5 + x^3 + 1`
60+
## polynomial you can set taps to `[5, 3]` or to `[5, 3, 0]` (or to
61+
## `[5, 3].toTensor` or `[5, 3, 0].toTensor`).
62+
## - state: Initial state of the LFSR as a Tensor[bool] of size equal to the
63+
## highest exponent in taps (which must be its first element)
64+
## - conf: LFSR type as an enum (`fibonacci` or `galois`)
65+
## - verbose: Enable it to print additional logs
66+
## - counter_starts_at_zero: Start the count from 0 or 1 (defaults to `true`)
67+
##
68+
## Return:
69+
## - Ready to use LFSR object
70+
71+
# Remove the last value from taps if it is a zero
72+
when typeof(taps) is Tensor:
73+
let taps = if taps[taps.size - 1] == 0: taps[_..^2] else: taps
74+
else:
75+
let taps = if taps[taps.size - 1] == 0: taps.toTensor[_..^2] else: taps.toTensor
76+
if taps.size > 1 and not taps.toSeq1D.isSorted(order = SortOrder.Descending):
77+
raise newException(ValueError,
78+
&"The LFSR polynomial must be ordered in descending exponent order, but it is not:\n{taps=}")
79+
if state.size != taps.max():
80+
raise newException(ValueError,
81+
&"The LFSR state size is {state.size} but must be {taps.max()} because that is the highest taps exponent ({taps=})")
82+
result = LFSR(
83+
taps: taps,
84+
conf: conf,
85+
state: state,
86+
init_state: state,
87+
verbose: verbose,
88+
counter_starts_at_zero: counter_starts_at_zero,
89+
outbit: false,
90+
count: 0,
91+
seq_bit_index: state.size - 1,
92+
sequence: newTensor[bool](0),
93+
feedbackbit: false
94+
)
95+
96+
proc initLFSR*(taps: Tensor[int] | seq[int],
97+
conf = fibonacci,
98+
state = single_true,
99+
verbose = false, counter_starts_at_zero = true): LFSR =
100+
## Overload of initLFSR that takes an enum for the initial state
101+
##
102+
## Inputs:
103+
## - taps: Feedback polynomial taps as a Tensor[int] or seq[int] of exponents
104+
## in descending order. Exponent 0 can be omitted, since it is always
105+
## implicitly added. For example, to use the `x^5 + x^3 + 1`
106+
## polynomial you can set taps to `[5, 3]` or to `[5, 3, 0]` (or to
107+
## `[5, 3].toTensor` or `[5, 3, 0].toTensor`).
108+
## - state: Initial state of the LFSR as an enum value (`single_true` or
109+
## `all_true`). Defaults to `single_true`.
110+
## - verbose: Enable it to print additional logs
111+
## - counter_starts_at_zero: Start the count from 0 or 1 (defaults to `true`)
112+
##
113+
## Return:
114+
## - Ready to use LFSR object
115+
when typeof(taps) is not Tensor:
116+
let taps = taps.toTensor
117+
let init_state = if state == all_true:
118+
arraymancer.ones[bool](taps.max())
119+
else:
120+
zeros[bool](taps.max() - 1).append(true)
121+
initLFSR(taps, conf = conf, state = init_state,
122+
verbose = verbose, counter_starts_at_zero = counter_starts_at_zero)
123+
124+
proc reset*(self: var LFSR) =
125+
## Reset the LFSR `state` and `count` to their initial values
126+
self.state = self.init_state
127+
self.count = 0
128+
129+
proc next*(self: var LFSR, verbose = false,
130+
store_sequence: static bool = false) : bool =
131+
## Run one cycle on LFSR with given feedback polynomial and
132+
## update the count, state, feedback bit, output bit and sequence
133+
##
134+
## Inputs:
135+
## - Preconfigured LFSR object
136+
## - verbose: Print additional logs even if LSRF.verbose is disabled
137+
## - store_sequence: static bool that enables saving the generated sequence
138+
##
139+
## Return:
140+
## - bool output bit
141+
if self.verbose or verbose:
142+
echo "State: ", self.state
143+
144+
if self.counter_starts_at_zero:
145+
result = self.state[self.seq_bit_index]
146+
when store_sequence:
147+
self.sequence = self.sequence.append(result)
148+
149+
if self.conf == fibonacci:
150+
var b = self.state[self.taps[0] - 1] xor self.state[self.taps[1] - 1]
151+
if self.taps.size > 2:
152+
for coeff in self.taps[2.._]:
153+
b = self.state[coeff - 1] xor b
154+
155+
self.state = self.state.roll(1)
156+
self.feedbackbit = b
157+
self.state[0] = self.feedbackbit
158+
else: # galois
159+
self.feedbackbit = self.state[0]
160+
self.state = self.state.roll(-1)
161+
for k in self.taps[1.._]:
162+
self.state[k-1] = self.state[k-1] xor self.feedbackbit
163+
164+
if not self.counter_starts_at_zero:
165+
result = self.state[self.seq_bit_index]
166+
when store_sequence:
167+
self.sequence = self.sequence.append(result)
168+
169+
self.count += 1
170+
self.outbit = result
171+
172+
return result
173+
174+
iterator generator*(lfsr: var LFSR,n: int,
175+
store_sequence: static bool = false): bool =
176+
## Generator that will generate a random sequence of length `n`
177+
##
178+
## Inputs:
179+
## - Preconfigured LFSR object
180+
## - n: Number of random values to generate
181+
## - store_sequence: static bool that enables saving the generated sequence
182+
##
183+
## Return:
184+
## - Generated boolean values
185+
yield lfsr.next(store_sequence = store_sequence)
186+
187+
proc generate*(lfsr: var LFSR, n: int,
188+
store_sequence: static bool = false): Tensor[bool] {.noinit.} =
189+
## Generate a random sequence of length `n`
190+
##
191+
## Inputs:
192+
## - Preconfigured LFSR object
193+
## - n: Number of random values to generate
194+
## - store_sequence: static bool that enables saving the generated sequence
195+
##
196+
## Return:
197+
## - `Tensor[bool]` of size `n` containing the generated values
198+
result = newTensor[bool](n)
199+
for i in 0 ..< n:
200+
result[i] = lfsr.next(store_sequence = store_sequence)
201+
202+
func lfsr_tap_example*(size: int): seq[int] =
203+
## Get an example "maximal" LSFR tap sequence for a given size (up to 24)
204+
##
205+
## This is a convenience function which can be used to select a set of taps
206+
## that generates a "maximal" sequence of the given size.
207+
## Note that there are many more tap sequences that generate maximal
208+
## sequences and that these examples are have been taken from wikipedia.
209+
doAssert size >= 2 and size <= 24,
210+
"LSFR tap examples are only available for sizes between 2 and 24"
211+
let examples = @[
212+
@[2, 1],
213+
@[3, 2],
214+
@[4, 3],
215+
@[5, 3],
216+
@[6, 5],
217+
@[7, 6],
218+
@[8, 6, 5, 4],
219+
@[9, 5],
220+
@[10, 7],
221+
@[11, 9],
222+
@[12, 11, 10, 4],
223+
@[13, 12, 11, 8],
224+
@[14, 13, 12, 2],
225+
@[15, 14],
226+
@[16, 15, 13, 4],
227+
@[17, 14],
228+
@[18, 11],
229+
@[19, 18, 17, 14],
230+
@[20, 17],
231+
@[21, 19],
232+
@[22, 21],
233+
@[23, 18],
234+
@[24, 23, 22, 17]
235+
]
236+
examples[size-2]

0 commit comments

Comments
 (0)