Pattern Generators

Introduction

We know that Patterns are of fixed length and can be generated based on a function. However, sometimes it is useful to have Patterns of infinite length, such as when generating random numbers. This is where Pattern Generators come in. Similar to Python generators where not all the values are kept in memory at once, except when Python generators usually have an end – FoxDot Pattern Generators do not! Let’s look at the PRand Pattern Generator to begin with, then you’ll find a list of all the available Pattern Generators with some examples code below.

The PRand Generator gives us an infinite supply of random numbers between a given low and high value or from a set of values given in a list. We can’t just print the object like we do with Patterns, because we can’t print an infinite list of values that hasn’t even been calculated yet:

>>> my_gen = PRand(0, 10)
>>> print(my_gen)
PRand(0, 10)

To see what values are in my_gen we need to access it using indexing or slicing.

>>> my_gen[0]
4
>>> my_gen[1]
6
>>> my_gen[:10]
P[4, 6, 1, 8, 3, 8, 8, 8, 0, 1]

Indexing the PRand gives us single values and slicing returns a Pattern of values. Notice how the first two values are 4 and 6? A Generator Pattern will (almost) always return the same value for a given index. You can apply any Pattern Method to the Pattern object you get back from slicing.

You can also perform basic mathematical operations on Pattern Generators:

>>> my_gen2 = my_gen * 2
>>> print(my_gen2)
PRand(Mul 2)
>>> my_gen2[:10]
P[8, 12, 2, 16, 6, 16, 16, 16, 0, 2]

Printing the new PRand tells us a little bit about it, but not much – just that it’s an existing PRand object and has been multiplied by 2. You can also use your own custom functions and apply them to Pattern Generators in the same way you can apply them to Patterns; through the transform method. Let’s create a function that returns 5 when an input is odd and 3 when the input is even:

>>> def odd_test(num):
...    return 5 if num % 2 == 1 else 3
>>> my_odd_gen = odd_test(PRand(0, 10))
>>> print(my_odd_gen)
3

The num % 2 returns a new PRand and not a number, so when it is asked “is this equal to 1?” it will always return False, and consequently, 3. To actually apply the function to the values in our Pattern Generator, we need to use the transform method and supply it with the function.

>>> my_odd_gen = PRand(0, 10).transform(odd_test)
>>> print(my_odd_gen)
PRand(<lambda> None)
>>> print(my_odd_gen[:10])
P[3, 3, 3, 5, 3, 3, 5, 3, 5, 3]

Printing the new Generator Pattern shows us we have a new PRand that has been transformed (the “lambda”) with no input value. Slicing it returns a Pattern of 3s and 5s just as we expect.

What happens if we’re not storing our Pattern Generators in a variable and we print them though?

>>> print(PRand(0, 10)[:10])
P[8, 5, 6, 0, 2, 2, 7, 3, 4, 4]
>>> print(PRand(0, 10)[:10])
P[1, 0, 0, 8, 0, 3, 4, 4, 2, 9]

We get a different list of values back. That’s because we are initialising a new PRand each time we run the code. This get’s a bit annoying, especially when using FoxDot as you are often supplying Pattern objects with Pattern Generators in this way and not storing them in a variable first. If you want to make sure you get the same values from your Pattern Generator, you can specify the seed, which – when set to the same number – forces a computer’s random number generator to generate the same set of numbers.

>>> print(PRand(0, 10, seed=1)[:10])
P[2, 9, 1, 4, 1, 7, 7, 7, 10, 6]
>>> print(PRand(0, 10, seed=1)[:10])
P[2, 9, 1, 4, 1, 7, 7, 7, 10, 6]

Types of Pattern Generators

PRand(lo, hi, seed=None) / PRand([values])

Returns a series of random integers between lo and hi inclusive. If hi is omitted then the range is between 0 and lo. A list of values can be supplied in place of the range and PRand will return a series of values randomly picked from the list.

>>> PRand(5, 10)[:10]
P[9, 10, 7, 10, 5, 10, 6, 5, 5, 7]
>>> PRand(5)[:10]
P[5, 4, 5, 5, 4, 1, 5, 5, 2, 0]
>>> PRand([1, 2, 3])[:10]
P[3, 2, 2, 2, 1, 3, 3, 2, 2, 1]

PxRand(lo, hi) / PxRand([values])

Identical to PRand but no elements are repeated.

>>> PxRand(5, 10)[:10]
P[6, 10, 8, 6, 9, 6, 9, 6, 10, 7]
>>> PxRand(5)[:10]
P[4, 3, 2, 1, 4, 1, 0, 5, 2, 3]
>>> PxRand([1, 2, 3])[:10]
P[2, 3, 2, 1, 3, 2, 3, 1, 3, 2]

PwRand([values], [weights])

Uses a list of weights to denote how frequently items with the same index from the list of values are picked. A weight of 2 means it is twice as likely to be picked as an item with a weight of 1.

>>> PwRand([0, 1], [1, 2])[:10]
P[1, 0, 0, 1, 1, 1, 1, 0, 1, 1]
>>> PwRand([0, 1, 2], [1, 2, 3])[:10]
P[1, 2, 2, 2, 2, 2, 0, 2, 2, 1]

PWhite(lo, hi)

Returns random floating point numbers between lo and hi.

>>> PWhite(0,5)[:10]
P[4.19058560392058, 2.776022501076935, 1.0643923909231003, 1.638195673989835, 0.8591848958936567, 2.461472231869311, 3.3546237751457335, 1.1854918335554943, 1.8310777165408907, 1.1767145868610336]

PChain(mapping_dictionary)

Based on a simple Markov Chain of using equal probabilities. Takes a dictionary of items; states and possible future states. Each future state has an equal probability of being chosen. If a possible future state is not valid, a KeyError will be raised.

>>> markov_chain = {
...    0: [3, 4],
...    3: [0, 4, 5],
...    4: [3, 0],
...    5: [3, 4],
... }
>>> PChain(markov_chain)[:10]
P[3, 0, 4, 0, 4, 3, 5, 4, 3, 5]

PWalk(max = 7, step = 1, start = 0)

Returns a series of integers where each element is step size away from each other and value are in the range +/- the max. The first element can be chosen using start.

>>> PWalk()[:10]
P[0, 1, 2, 1, 2, 3, 2, 3, 4, 5]
>>> PWalk(20, 5, 5)[:10]
P[5, 10, 15, 20, 15, 20, 15, 20, 15, 20]

PDelta(deltas, start = 0)

Takes a list of deltas (small increments/decrements) and returns the series of numbers generated by adding these values together in sequence. Using a single positive number will create an infinitely increasing series. Set the starting value using start.

>>> PDelta([0.1, 0.5, -0.3])[:10]
P[0, 0.1, 0.6, 0.3, 0.4, 0.9, 0.6, 0.7, 1.2, 0.9]
>>> PDelta([0.5])[:10]
P[0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5]
>>> PDelta([0.5, -0.5], start=1)[:10]
P[1, 1.5, 1.0, 1.5, 1.0, 1.5, 1.0, 1.5, 1.0, 1.5]

PSquare()

Returns the series of square numbers.

>>> PSquare()[:10]
P[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> PSquare()[10:20] % 7
P[2, 2, 4, 1, 0, 1, 4, 2, 2, 4]

PIndex()

Returns the series of whole numbers.

>>> PIndex()[:10]
P[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

PFibMod()

Returns the Fibonacci sequence.

>>> PFibMod()[:10]
P[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

PZ12(tokens = [0, 1], p = [1, 0.5])

Simple implementation of the Z12 algorithm for pre-determined random numbers. Using an irrational value for p, however, results in a non-determined order of values. Experimental – only works with 2 values.

>>> PZ12([0, 1], [1, 0.5])[:15]
P[0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1]
>>> PZ12([0, 1], [1, math.sqrt(2)])[:15]
P[1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1]