TenneySelector

class auxjad.TenneySelector(contents: list, *, weights: Optional[list] = None, curvature: float = 1.0)[source]

An implementation of the Dissonant Counterpoint Algorithm by James Tenney. This class can be used to randomly select elements from an input list, giving more weight to elements which have not been selected in recent iterations. In other words, Tenney’s algorithm uses feedback in order to lower the weight of recently selected elements.

This implementation is based on the paper: Polansky, L., A. Barnett, and M. Winter (2011). ‘A Few More Words About James Tenney: Dissonant Counterpoint and Statistical Feedback’. In: Journal of Mathematics and Music 5(2). pp. 63–82.

Basic usage:

The selector should be initialised with a list. The elements of this list can be of any type.

>>> selector = auxjad.TenneySelector(['A', 'B', 'C', 'D', 'E', 'F'])
>>> selector.contents
['A', 'B', 'C', 'D', 'E', 'F']

Applying the len() function to the selector will return the length of the input list.

>>> len(selector)
6

When no other keyword arguments are used, the default probabilities of each element in the list is 1.0. Probabilities are not normalised.

>>> selector.probabilities
[1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
>>> selector.previous_index
None

Calling the selector will output one of its elements, selected according to the current probability values.

>>> selector()
C

Alternatively, use the next() function or __next__() method to get the next result.

>>> selector.__next__()
A
>>> next(selector)
D

After each call, the object updates all probability values, setting the previously selected element’s probability at 0.0 and raising all other probabilities according to a growth function (more on this below).

>>> result = ''
>>> for _ in range(30):
...     result += selector()
>>> result
EDFACEABAFDCEDAFADCBFEDABEDFEC

From the result above it is possible to see that there are no immediate repetitions of elements (since once selected, their probability is always set to 0.0 and will take at least one iteration to grow to a non-zero value). Checking the probabilities and previous_index properties will return us their current values.

>>> selector.probabilities
[6.0, 5.0, 0.0, 3.0, 1.0, 2.0]
>>> selector.previous_index
2
previous_result and previous_index:

Use the read-only properties previous_result and previous_index to output the previous result and its index. Default values for both is None.

>>> selector = auxjad.TenneySelector(['A', 'B', 'C', 'D', 'E', 'F'])
>>> selector.previous_index
None
>>> selector.previous_result
None
>>> selector()
C
>>> selector.previous_index
2
>>> selector.previous_result
C
Arguments and properties:

This class can take two optional keywords argument during its instantiation, namely weights and curvature. weights takes a list of float with the individual weights of each element; by default, all weights are set to 1.0. These weights affects the effective probability of each element. The other argument, curvature, is the exponent of the growth function for all elements. The growth function takes as input the number of iterations since an element has been last selected, and raise this number by the curvature value. If curvature is set to 1.0 (which is its default value), the growth is linear with each iteration. If set to a value larger than 0.0 and less than 1.0, the growth is negative (or concave), so that the chances of an element which is not being selected will grow at ever smaller rates as the number of iterations it has not been selected increase. If the curvature is set to 1.0, the growth is linear with the number of iterations. If the curvature is larger than 1.0, the curvature is positive (or convex) and the growth will accelerate as the number of iterations an element has not been selected grows. Setting the curvature to 0.0 will result in an static probability vector with all values set to 1.0, except for the previously selected one which will be set to 0.0; this will result in a uniformly random selection without repetition.

With linear curvature (default value of 1.0):

>>> selector = auxjad.TenneySelector(['A', 'B', 'C', 'D', 'E', 'F'])
>>> selector.curvature
1.0
>>> selector.weights
[1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
>>> selector.probabilities
[1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
>>> selector()
'B'
>>> selector.curvature
1.0
>>> selector.weights
[1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
>>> selector.probabilities
[2.0, 0.0, 2.0, 2.0, 2.0, 2.0]
Convex curvature:

Using a convex curvature (i.e. greater than 0.0 and less than 1.0):

>>> selector = auxjad.TenneySelector(['A', 'B', 'C', 'D', 'E', 'F'],
...                                  curvature=0.2,
...                                  )
>>> selector.curvature
0.2
>>> selector.weights
[1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
>>> selector.probabilities
[1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
>>> selector()
'C'
>>> selector.curvature
0.2
>>> selector.weights
[1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
>>> selector.probabilities
[1.148698354997035, 1.148698354997035, 0.0, 1.148698354997035,
1.148698354997035, 1.148698354997035]

With a convex curvature, the growth of the probability of each non-selected term gets smaller as the number of times it is not selected increases. The smaller the curvature is, the less difference there will be between any non-previously selected elements. This results in sequences which have more chances of a same element being near each other. In the sequence below, note how there are many cases of a same element being separated only by a single other one, such as 'ACA' in index 6.

>>> result = ''
>>> for _ in range(30):
...     result += selector()
>>> result
DACBEDFACABDACECBEFAEDBAFBABFD

Checking the probability values at this point outputs:

>>> selector.probabilities
[1.2457309396155174, 1.148698354997035, 1.6952182030724354, 0.0,
1.5518455739153598, 1.0]

As we can see, all non-zero values are relatively close to each other, which is why there is a high chance of an element being selected again just two iterations apart.

Concave curvature:

Using a concave curvature (i.e. greater than 1.0):

>>> selector = auxjad.TenneySelector(['A', 'B', 'C', 'D', 'E', 'F'],
...                                  curvature=15.2,
...                                  )
>>> selector.curvature
0.2
>>> selector.weights
[1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
>>> selector.probabilities
[1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
>>> selector()
'C'
>>> selector.curvature
0.2
>>> selector.weights
[1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
>>> selector.probabilities
[37640.547696542824, 37640.547696542824, 37640.547696542824, 0.0,
37640.547696542824, 37640.547696542824]

With a concave curvature, the growth of the probability of each non-selected term gets larger as the number of times it is not selected increases. The larger the curvature is, the larger difference there will be between any non-previously selected elements. This results in sequences which have less chances of a same element being near each other. In the sequence below, with a curvature of 15.2, note how the elements are as far apart from each other, resulting in a repeating string of 'DFAECB'.

>>> result = ''
>>> for _ in range(30):
...     result += selector()
>>> result
DFAECBDFAECBDFAECBDFAECBDFAECB

Checking the probability values at this point outputs:

>>> selector.probabilities
[17874877.39956566, 0.0, 1.0, 42106007735.02238,
37640.547696542824, 1416810830.8957152]

As we can see, the non-zero values vary wildly. The higher the curvature, the higher the difference between these values, making some of them much more likely to be selected.

curvature property:

To change the curvature value at any point, simply set the property curvature to a different value.

>>> selector = auxjad.TenneySelector(['A', 'B', 'C', 'D', 'E', 'F'])
>>> selector.curvature
1.0
>>> selector.curvature = 0.25
>>> selector.curvature
0.25
weights:

Each element can also have a fixed weight to themselves. This will affect the probability calculation. The example below uses the default linear curvature.

>>> selector = auxjad.TenneySelector(
...     ['A', 'B', 'C', 'D', 'E', 'F'],
...     weights=[1.0, 1.0, 5.0, 5.0, 10.0, 20.0],
... )
>>> selector.weights
[1.0, 1.0, 5.0, 5.0, 10.0, 20.0]
>>> selector.probabilities
[1.0, 1.0, 5.0, 5.0, 10.0, 20.0]
>>> result = ''
>>> for _ in range(30):
...     result += selector()
>>> result
FBEFECFDEADFEDFEDBFECDAFCEDCFE
>>> selector.weights
[1.0, 1.0, 5.0, 5.0, 10.0, 20.0]
>>> selector.probabilities
[7.0, 12.0, 10.0, 15.0, 0.0, 20.0]

Set weights to None to reset it to a uniform distribution.

>>> selector = auxjad.TenneySelector(
...     ['A', 'B', 'C', 'D', 'E', 'F'],
...     weights=[1.0, 1.0, 5.0, 5.0, 10.0, 20.0],
... )
>>> selector.weights
[1.0, 1.0, 5.0, 5.0, 10.0, 20.0]
>>> selector.weights = None
>>> selector.weights
[1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
reset_probabilities():

To reset the probability distribution of all elements to its initial value (an uniform distribution), use the method reset_probabilities().

>>> selector = auxjad.TenneySelector(['A', 'B', 'C', 'D', 'E', 'F'])
>>> for _ in range(30):
...     selector()
>>> selector.probabilities
[4.0, 3.0, 1.0, 0.0, 5.0, 2.0]
>>> selector.reset_probabilities()
>>> selector.probabilities
[1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
contents:

The instances of this class can be indexed and sliced. This allows reading, assigning, or deleting values from contents. Replacing elements by assignment will not affect the probabilities property, and the new elements will have the same probability as the ones it replaced. Deleting element will delete the probability of that index.

>>> selector = auxjad.TenneySelector(['A', 'B', 'C', 'D', 'E', 'F'])
>>> for _ in range(30):
...     selector()
>>> selector.probabilities
[3.0, 2.0, 1.0, 7.0, 5.0, 0.0]
>>> selector[2]
'C'
>>> selector[1:4]
['B', 'C', 'D']
>>> selector[2] = 'foo'
>>> selector.contents
['A', 'B', 'foo', 'D', 'E', 'F']
>>> selector[:] = ['foo', 'bar', 'X', 'Y', 'Z', '...']
>>> selector.contents
['foo', 'bar', 'X', 'Y', 'Z', '...']
>>> selector.probabilities
[3.0, 2.0, 1.0, 7.0, 5.0, 0.0]
>>> del selector[0:2]
>>> selector.contents
['X', 'Y', 'Z', '...']
>>> selector.probabilities
[1.0, 7.0, 5.0, 0.0]

You can also check if the instance contains a specific element. In the case of the selector above, we have:

>>> 'X' in selector
True
>>> 'A' in selector
False
Changing contents resets probabilities and weights:

A new list of an arbitrary length can be set at any point using the property contents. Do notice that both probabilities and weights will be reset at that point.

>>> selector = auxjad.TenneySelector(
...     ['A', 'B', 'C', 'D', 'E', 'F'],
...     weights=[1.0, 1.0, 5.0, 5.0, 10.0, 20.0],
... )
>>> for _ in range(30):
...     selector()
>>> len(selector)
6
>>> selector.contents
['A', 'B', 'C', 'D', 'E', 'F']
>>> selector.weights
[1.0, 1.0, 5.0, 5.0, 10.0, 20.0]
>>> selector.probabilities
[8.0, 2.0, 5.0, 15.0, 50.0, 0.0]
>>> selector.contents = [2, 4, 6, 8]
>>> len(selector)
4
>>> selector.contents
[2, 4, 6, 8]
>>> selector.weights
[1.0, 1.0, 1.0, 1.0]
>>> selector.probabilities
[1.0, 1.0, 1.0, 1.0]

Methods

__call__()

Calls the selection process and outputs one element of contents.

__delitem__(key)

Deletes one or more elements of contents through indexing or slicing.

__getitem__(key)

Returns one or more elements of contents through indexing or slicing.

__init__(contents, *[, weights, curvature])

Initialises self.

__len__()

Returns the length of contents.

__next__()

Calls the selection process and outputs one element of contents.

__repr__()

Returns interpreter representation of contents.

__setitem__(key, value)

Assigns values to one or more elements of contents through indexing or slicing.

reset_probabilities()

Resets the probability distribution of all elements to an uniform distribution.

Attributes

contents

The list from which the selector picks elements.

curvature

The exponent of the growth function.

previous_index

Read-only property, returns the index of the previously output element.

previous_result

Read-only property, returns the previously output element.

probabilities

Read-only property, returns the probabilities vector.

weights

The list with weights for each element of contents.

__call__()Any[source]

Calls the selection process and outputs one element of contents.

__delitem__(key: int)None[source]

Deletes one or more elements of contents through indexing or slicing.

__getitem__(key: int)Any[source]

Returns one or more elements of contents through indexing or slicing.

__init__(contents: list, *, weights: Optional[list] = None, curvature: float = 1.0)None[source]

Initialises self.

__len__()int[source]

Returns the length of contents.

__next__()Any[source]

Calls the selection process and outputs one element of contents.

__repr__()str[source]

Returns interpreter representation of contents.

__setitem__(key: int, value: Any)None[source]

Assigns values to one or more elements of contents through indexing or slicing.

property contents: list

The list from which the selector picks elements.

property curvature: float

The exponent of the growth function.

property previous_index: Optional[int]

Read-only property, returns the index of the previously output element.

property previous_result: Any

Read-only property, returns the previously output element.

property probabilities: list

Read-only property, returns the probabilities vector.

reset_probabilities()None[source]

Resets the probability distribution of all elements to an uniform distribution.

property weights: list

The list with weights for each element of contents.