From 3079052171694c214654156a6f835d6b930c593d Mon Sep 17 00:00:00 2001 From: Angel Ezquerra Date: Sun, 24 Mar 2024 22:57:29 +0100 Subject: [PATCH] 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 --- impulse/lfsr/lfsr.nim | 190 ++++++++++++++++++++++++++++++++++++++++++ tests/test_lfsr.nim | 61 ++++++++++++++ 2 files changed, 251 insertions(+) create mode 100644 impulse/lfsr/lfsr.nim create mode 100644 tests/test_lfsr.nim diff --git a/impulse/lfsr/lfsr.nim b/impulse/lfsr/lfsr.nim new file mode 100644 index 0000000..4c10947 --- /dev/null +++ b/impulse/lfsr/lfsr.nim @@ -0,0 +1,190 @@ +## LFSR module which implements a Linear Feedback Shift Register that can be +## used to generate pseudo-random boolean sequences. It supports both Fibonacci +## and Galois LFSRs. +## +## LFSRs 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 +## +## Note that this code is heavily based on Nikesh Bajaj's pylfsr, which can be +## found in https://pylfsr.github.io + +import arraymancer +import std/strformat + +type LFSR_TYPE* = enum + ## LFSR types (see https://simple.wikipedia.org/wiki/Linear-feedback_shift_register) + fibonacci, galois + +type LFSR_INIT_STATE_TYPE* = enum + ## Common LFSR Initial States + ## While it is possible to set any LFSR initial `state` by passing a + ## Tensor[bool] to `initLFSR`, for convenience it is also possible + ## to pass one of these enum values as the initial `state`: + ## - `all_true`: all bits set to `true` + ## - `single_true`: all bits set to `false` except the last one + all_true, single_true + +type LFSR* = object + ## LFSR object used to generate fibonacci or galois pseudo random sequences + ## To use it first create the object using `initLFSR` and then call either + ## the `next` procedure to get the sequence values one by one or `generate` + ## to generate multiple values in one go. + fpoly*: Tensor[int] + conf*: LFSR_TYPE + state*: Tensor[bool] + init_state: Tensor[bool] + verbose*: bool + counter_starts_at_zero*: bool + outbit*: bool + count*: int + seq_bit_index: int + sequence: Tensor[bool] + feedbackbit: bool + +proc initLFSR*(fpoly: typeof(LFSR.fpoly), + conf = fibonacci, + state: Tensor[bool], + verbose = false, + counter_starts_at_zero = true): LFSR = + ## Initialize LFSR with given feedback polynomial and initial state + ## + ## Inputs: + ## - fpoly: Feedback polynomial as a Tensor[int] of exponents in descending + ## order. Note that exponent 0 can be omitted, since it is always + ## implicitly added. For example, to use the `x^5 + x^2 + 1` + ## polinomial you can set fpoly to `[5, 2].toTensor` or to + ## `[5, 2, 0].toTensor`. + ## - state: Initial state of the LFSR as a Tensor[bool] of size equal to the + ## highest exponent in fpoly (which must be its first element) + ## - conf: LFSR type as an enum (`fibonacci` or `galois`) + ## - verbose: Enable it to print additonal logs + ## - counter_starts_at_zero: Start the count from 0 or 1 + ## + ## Return: + ## - Ready to use LFSR object + # Remove the last value from fpoly if it is a zero + let fpoly = if fpoly[fpoly.size - 1] == 0: fpoly[_..^2] else: fpoly + if fpoly.size > 1 and fpoly[0] < fpoly[fpoly.size - 1]: + raise newException(ValueError, + &"The LFSR polinomial must be ordered in descending exponent order, but it is not:\n{fpoly=}") + if state.size != fpoly.max(): + raise newException(ValueError, + &"The LFSR state size is {state.size} but must be {fpoly.max()} because that is the highest fpoly exponent ({fpoly=})") + result = LFSR( + fpoly: fpoly, + conf: conf, + state: state, + init_state: state, + verbose: verbose, + counter_starts_at_zero: counter_starts_at_zero, + outbit: false, + count: 0, + seq_bit_index: state.size - 1, + sequence: newTensor[bool](0), + feedbackbit: false + ) + +proc initLFSR*(fpoly: typeof(LFSR.fpoly), + conf = fibonacci, + state = all_true, + verbose = false, counter_starts_at_zero = true): LFSR = + ## Overload of initLFSR that takes an enum for the initial state + ## + ## Inputs: + ## - fpoly: Feedback polynomial as a Tensor[int] of exponents in descending + ## order. Note that exponent 0 can be omitted, since it is always + ## implicitly added. For example, to use the `x^5 + x^2 + 1` + ## polinomial you can set fpoly to `[5, 2].toTensor` or to + ## `[5, 2, 0].toTensor`. + ## - state: Initial state of the LFSR as a Tensor[bool] of size equal to the + ## highest exponent in fpoly (which must be its first element) + ## - verbose: Enable it to print additonal logs + ## - counter_starts_at_zero: Start the count from 0 or 1 + ## + ## Return: + ## - Ready to use LFSR object + let init_state = if state == all_true: + arraymancer.ones[bool](fpoly.max()) + else: + zeros[bool](fpoly.max() - 1).append(true) + initLFSR(fpoly, conf = conf, state = init_state, + verbose = verbose, counter_starts_at_zero = counter_starts_at_zero) + +proc reset*(self: var LFSR) = + ## Reset the LFSR `state` and `count` to their initial values + self.state = self.init_state + self.count = 0 + +proc next*(self: var LFSR, verbose = false, + store_sequence: static bool = false) : bool = + ## Run one cycle on LFSR with given feedback polynomial and + ## update the count, state, feedback bit, output bit and sequence + ## + ## Inputs: + ## - Preconfigured LFSR object + ## - verbose: Print additonal logs even if LSRF.verbose is disabled + ## - store_sequence: static bool that enables saving the generated sequence + ## + ## Return: + ## - bool output bit + if self.verbose or verbose: + echo "State: ", self.state + + if self.counter_starts_at_zero: + result = self.state[self.seq_bit_index] + when store_sequence: + self.sequence = self.sequence.append(result) + + if self.conf == fibonacci: + var b = self.state[self.fpoly[0] - 1] xor self.state[self.fpoly[1] - 1] + if self.fpoly.size > 2: + for coeff in self.fpoly[2.._]: + b = self.state[coeff - 1] xor b + + self.state = self.state.roll(1) + self.feedbackbit = b + self.state[0] = self.feedbackbit + else: # galois + self.feedbackbit = self.state[0] + self.state = self.state.roll(-1) + for k in self.fpoly[1.._]: + self.state[k-1] = self.state[k-1] xor self.feedbackbit + + if not self.counter_starts_at_zero: + result = self.state[self.seq_bit_index] + when store_sequence: + self.sequence = self.sequence.append(result) + + self.count += 1 + self.outbit = result + + return result + +iterator generator*(lfsr: var LFSR,n: int, + store_sequence: static bool = false): bool = + ## Generator that will generate a random sequence of length `n` + ## + ## Inputs: + ## - Preconfigured LFSR object + ## - n: Number of random values to generate + ## - store_sequence: static bool that enables saving the generated sequence + ## + ## Return: + ## - Generated boolean values + yield lfsr.next(store_sequence = store_sequence) + +proc generate*(lfsr: var LFSR, n: int, + store_sequence: static bool = false): Tensor[bool] {.noinit.} = + ## Generate a random sequence of length `n` + ## + ## Inputs: + ## - Preconfigured LFSR object + ## - n: Number of random values to generate + ## - store_sequence: static bool that enables saving the generated sequence + ## + ## Return: + ## - `Tensor[bool]` of size `n` containing the generated values + result = newTensor[bool](n) + for i in 0 ..< n: + result[i] = lfsr.next(store_sequence = store_sequence) diff --git a/tests/test_lfsr.nim b/tests/test_lfsr.nim new file mode 100644 index 0000000..6dc9820 --- /dev/null +++ b/tests/test_lfsr.nim @@ -0,0 +1,61 @@ +import ../impulse/lfsr/lfsr +import arraymancer / tensor + +proc test_fibonacci(): bool = + var lfsr = initLFSR( + fpoly = [5, 2].toTensor, + state = all_true, + conf = fibonacci + ) + + let sequence = lfsr.generate(31).asType(int) + let expected_sequence = [1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, + 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0].toTensor + return sequence == expected_sequence + +proc test_galois(): bool = + var lfsr = initLFSR( + fpoly = [5, 2].toTensor, + state = all_true, + conf = galois + ) + + let sequence = lfsr.generate(31).asType(int) + let expected_sequence = [1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, + 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1].toTensor + return sequence == expected_sequence + +proc test_init(): bool = + var lfsr1 = initLFSR( + fpoly = [5, 2].toTensor, + state = all_true + ) + var lfsr2 = initLFSR( + fpoly = [5, 2].toTensor, + state = all_true, + conf = fibonacci + ) + var lfsr3 = initLFSR( + fpoly = [5, 2].toTensor, + state = ones[bool](5), + conf = fibonacci + ) + let s1 = lfsr1.generate(31) + let s2 = lfsr2.generate(31) + let s3 = lfsr3.generate(31) + return s1 == s2 and s2 == s3 + +proc test_reset(): bool = + var lfsr = initLFSR( + fpoly = [5, 2].toTensor, + state = all_true + ) + let s1 = lfsr.generate(31) + lfsr.reset() + let s2 = lfsr.generate(31) + return s1 == s2 + +doAssert test_fibonacci() +doAssert test_galois() +doAssert test_init() +doAssert test_reset()