Source code for auxjad.core.CartographySelector

import random
from typing import Any, Union


[docs]class CartographySelector(): r"""A selector used to store, manipulate, and select objects using a weighted function constructed with a fixed decay rate. The decay rate represents the ratio of probabilities of any index given the probability of the preceding one. For instance, if the decay rate is set to ``0.75`` (which is its default value), the probability of the element in index ``1`` of the input :obj:`list` being selected is :math:`75\%`` the probability of the element in index ``0``, and the probability of the element in index ``2`` is :math:`56.25\%`` (i.e. :math:`0.75^2`) the probability of the element in index ``0``. The probability :math:`P(n)`` of the :math:`n`-th element can thus be expressed as a relation to the probability of another element :math:`k` indexes apart using: .. math:: P(n) = (3/4)^k \times P(n-k) This is the selector used in my *Cartography* series of compositions. Basic usage: The selector should be initialised with a :obj:`list` of objects. The elements of this :obj:`list` can be of any type. >>> selector = auxjad.CartographySelector([0, 1, 2, 3, 4]) >>> selector.contents [0, 1, 2, 3, 4] Calling the selector will output one of its elements, selected according to its weight function. >>> selector() 2 The default decay rate is ``0.75``; that is, the weight of any given elements is the weight of the previous one multiplied by ``0.75``. The :attr:`weights` are associated with the index position, not the elements themselves. >>> selector.weights [1.0, 0.75, 0.5625, 0.421875, 0.31640625] By default, only the weight function (defined by the decay rate) is taken into consideration when selecting an element. This means that repeated elements can appear, as shown below. >>> result = '' >>> for _ in range(30): ... result += str(selector()) >>> result 203001402200011111101400310140 :func:`len()` function: Applying the :func:`len()` function to the selector will return the length of the input :obj:`list`. >>> selector = auxjad.CartographySelector([0, 1, 2, 3, 4]) >>> len(selector) 5 :func:`next()` function: Alternatively, use the :func:`next()` function or :meth:`__next__()` method to get the next result. >>> selector = auxjad.CartographySelector([0, 1, 2, 3, 4]) >>> selector.__next__() 1 >>> next(selector) 0 :meth:`__call__` and argument ``no_repeat``: Calling the selector with the optional keyword argument ``no_repeat`` set to ``True`` will forbid immediate repetitions among consecutive calls. >>> selector = auxjad.CartographySelector([0, 1, 2, 3, 4]) >>> result = '' >>> for _ in range(30): ... result += str(selector(no_repeat=True)) >>> result 210421021020304024230120241202 :attr:`decay_rate`: The keyword argument :attr:`decay_rate` can be used to set a different decay rate when creating a selector. >>> selector = auxjad.CartographySelector([0, 1, 2, 3, 4], ... decay_rate=0.5, ... ) >>> selector.weights [1.0, 0.5, 0.25, 0.125, 0.0625] The decay rate can also be set after the creation of a selector using, the property :attr:`decay_rate`. >>> selector = auxjad.CartographySelector([0, 1, 2, 3, 4]) >>> selector.decay_rate = 0.2 >>> selector.weights [1.0, 0.2, 0.04000000000000001, 0.008000000000000002, 0.0016000000000000003] >>> result = '' >>> for _ in range(30): ... result += str(selector()) >>> result '000001002100000201001030000100' :meth:`drop_first_and_append`: This is a type of content transformation, it drops the first element of :attr:`contents`, shifts all others leftwards, and appends the new element to the last index. >>> selector = auxjad.CartographySelector([0, 1, 2, 3, 4]) >>> selector.contents [0, 1, 2, 3, 4] >>> selector.drop_first_and_append(5) >>> selector.contents [1, 2, 3, 4, 5] >>> selector.drop_first_and_append(42) >>> selector.contents [2, 3, 4, 5, 42] :meth:`drop_n_and_append`: This is a type of content transformation similar to :meth:`drop_first_and_append`, it drops the element at index ``n`` of :attr:`contents`, shifts all the next elements one position lefwards, and appends the new element at the last index. >>> selector = auxjad.CartographySelector([10, 7, 14, 31, 98]) >>> selector.contents [10, 7, 14, 31, 98] >>> selector.drop_n_and_append(100, 2) >>> selector.contents [10, 7, 31, 98, 100] :meth:`drop_last_and_prepend`: A type of content transformation, it drops the last element of :attr:`contents`, shifts all others rightwards, and then prepends the new element to the first index. >>> selector = auxjad.CartographySelector([0, 1, 2, 3, 4]) >>> selector.contents [0, 1, 2, 3, 4] >>> selector.drop_last_and_prepend(-1) >>> selector.contents [-1, 0, 1, 2, 3] >>> selector.drop_last_and_prepend(71) >>> selector.contents [71, -1, 0, 1, 2] :meth:`rotate`: Rotation is another type of content transformation. It rotates all elements rightwards, moving the last element to the first index. If the optional keyword argument ``anticlockwise`` is set to ``True``, the rotation will be in the opposite direction. >>> selector = auxjad.CartographySelector([0, 1, 2, 3, 4]) >>> selector.contents [0, 1, 2, 3, 4] >>> selector.rotate() >>> selector.contents [1, 2, 3, 4, 0] >>> selector.rotate(anticlockwise=True) >>> selector.contents [0, 1, 2, 3, 4] >>> selector.rotate(anticlockwise=True) >>> selector.contents [1, 2, 3, 4, 0] :meth:`mirror_swap`: The mirror swap transformation swaps takes an input index and swaps the element at tit with its complementary element. Complementary elements are defined as the pair of elements which share the same distance from the centre of the :attr:`contents` (in terms of number of indeces), and are located at either side of this centre. >>> selector = auxjad.CartographySelector([0, 1, 2, 3, 4]) >>> selector.contents [0, 1, 2, 3, 4] >>> selector.mirror_swap(0) >>> selector.contents [4, 1, 2, 3, 0] >>> selector.mirror_swap(0) >>> selector.contents [0, 1, 2, 3, 4] >>> selector.mirror_swap(3) >>> selector.contents [0, 3, 2, 1, 4] >>> selector.mirror_swap(2) >>> selector.contents [0, 3, 2, 1, 4] :meth:`mirror_random_swap`: A type of content transformation which will apply :meth:`mirror_swap` to a random pair of complementary elements. In case of a selector with an odd number of elements, this method will never pick the element at the central index since that is the pivot point of the operation and it would not result in any changes. >>> selector = auxjad.CartographySelector([0, 1, 2, 3, 4]) >>> selector.contents [0, 1, 2, 3, 4] >>> selector.mirror_random_swap() >>> selector.contents [4, 1, 2, 3, 0] >>> selector.mirror_random_swap() >>> selector.contents [4, 3, 2, 1, 0] >>> selector.mirror_random_swap() >>> selector.contents [4, 1, 2, 3, 0] :meth:`shuffle`: The method :meth:`shuffle` will shuffle the position of the elements of the selector's :attr:`contents`. >>> selector = auxjad.CartographySelector([0, 1, 2, 3, 4]) >>> selector.contents [0, 1, 2, 3, 4] >>> selector.shuffle() >>> selector.contents [1, 4, 3, 0, 2] :attr:`contents`: The contents of a selector can also be altered after it has been initialised using the :attr:`contents` property. The length of the contents can change as well. >>> selector = auxjad.CartographySelector([0, 1, 2, 3, 4], ... decay_rate=0.5, ... ) >>> len(selector) 5 >>> selector.weights [1.0, 0.5, 0.25, 0.125, 0.0625] >>> selector.contents = [10, 7, 14, 31, 98, 47, 32] >>> selector.contents [10, 7, 14, 31, 98, 47, 32] >>> len(selector) 7 >>> selector.weights [1.0, 0.5, 0.25, 0.125, 0.0625, 0.03125, 0.015625] :attr:`previous_result` and :attr:`previous_index`: Use the read-only properties :attr:`previous_result` and :attr:`previous_index` to output the previous result and its index. >>> selector = auxjad.CartographySelector([10, 7, 14, 31, 98]) >>> selector() 14 >>> previous_index = selector.previous_index >>> previous_index 2 >>> selector.previous_result 14 Slicing and indexing: Instances of this class can be indexed and sliced. This allows reading, assigning, or deleting values from :attr:`contents`. >>> selector = auxjad.CartographySelector([10, 7, 14, 31, 98]) >>> selector[1] 7 >>> selector[1:4] [7, 14, 31] >>> selector[:] [10, 7, 14, 31, 98] >>> selector() 31 >>> previous_index = selector.previous_index >>> previous_index 3 >>> selector[previous_index] 31 >>> selector.contents [10, 7, 14, 31, 98] >>> selector[2] = 100 >>> selector.contents [10, 7, 100, 31, 98] >>> del selector[2:4] >>> selector.contents [10, 7, 98] """ ### CLASS VARIABLES ### __slots__ = ('_contents', '_decay_rate', '_previous_index', '_weights', ) ### INITIALISER ###
[docs] def __init__(self, contents: list[Any], *, decay_rate: float = 0.75, ) -> None: r'Initialises self.' if not isinstance(contents, list): raise TypeError("'contents' must be 'list'") if not isinstance(decay_rate, float): raise TypeError("'decay_rate' must be 'float'") if decay_rate <= 0.0 or decay_rate > 1.0: raise ValueError("'decay_rate' must be larger than 0.0 and " "less than or equal to 1.0") self._contents = contents[:] self._previous_index = None self._decay_rate = decay_rate self._generate_weights()
### SPECIAL METHODS ###
[docs] def __repr__(self) -> str: r'Returns interpreter representation of :attr:`contents`.' return str(self._contents)
[docs] def __len__(self) -> int: r'Returns the length of :attr:`contents`.' return len(self._contents)
[docs] def __call__(self, *, no_repeat: bool = False, ) -> Any: r"""Calls the selection process and outputs one element of :attr:`contents`. """ if not isinstance(no_repeat, bool): raise TypeError("'no_repeat' must be 'bool") if not no_repeat: new_index = random.choices( [n for n in range(self.__len__())], weights=self._weights, )[0] else: new_index = self._previous_index while new_index == self._previous_index: new_index = random.choices( [n for n in range(self.__len__())], weights=self._weights, )[0] self._previous_index = new_index return self._contents[self._previous_index]
[docs] def __next__(self) -> Any: r"""Calls the selection process and outputs one element of :attr:`contents`. """ return self.__call__()
[docs] def __getitem__(self, key: int, ) -> Any: r"""Returns one or more elements of :attr:`contents` through indexing or slicing. """ return self._contents[key]
[docs] def __setitem__(self, key: int, value: Any, ) -> None: r"""Assigns values to one or more elements of :attr:`contents` through indexing or slicing. """ self._contents[key] = value
[docs] def __delitem__(self, key: int, ) -> None: r"""Deletes one or more elements of :attr:`contents` through indexing or slicing. """ del self._contents[key] self._generate_weights()
### PUBLIC METHODS ###
[docs] def drop_first_and_append(self, new_element: Any, ) -> None: r"""A type of content transformation, it drops the first element of :attr:`contents`, shifts all others leftwards, and appends the new element to the last index. """ self._contents = self._contents[1:] + [new_element]
[docs] def drop_n_and_append(self, new_element: Any, n: int, ) -> None: r"""A type of content transformation similar to :meth:`drop_first_and_append`, it drops the element at index ``n`` of :attr:`contents`, shifts all the next elements one position lefwards, and appends the new element at the last index. """ self._contents = (self._contents[:n] + self._contents[n + 1:] + [new_element])
[docs] def drop_last_and_prepend(self, new_element: Any, ) -> None: r"""A type of content transformation, it drops the last element of :attr:`contents`, shifts all others rightwards, and then prepends the new element to the first index. """ self._contents = [new_element] + self._contents[:-1]
[docs] def rotate(self, *, anticlockwise=False, ) -> None: r"""A type of content transformation, it rotates all elements rightwards, moving the last element to the first index. If the optional keyword argument ``anticlockwise`` is set to ``True``, the rotation will be in the opposite direction. """ if not anticlockwise: self._contents = self._contents[1:] + self._contents[:1] else: self._contents = self._contents[-1:] + self._contents[:-1]
[docs] def mirror_swap(self, index: int, ) -> None: r"""A type of content transformation which swaps takes an input index and swaps the element at tit with its complementary element. Complementary elements are defined as the pair of elements which share the same distance from the centre of the :attr:`contents` (in terms of number of indeces), and are located at either side of this centre. """ aux = self._contents[index] self._contents[index] = self._contents[-1 - index] self._contents[-1 - index] = aux
[docs] def mirror_random_swap(self) -> None: r"""A type of content transformation which will apply :meth:`mirror_swap` to a random pair of complementary elements. In case of a selector with an odd number of elements, this method will never pick the element at the central index since that is the pivot point of the operation and it would not result in any changes. """ max_index = self.__len__() // 2 - 1 self.mirror_swap(random.randint(0, max_index))
[docs] def shuffle(self) -> None: r'Shuffles the position of the elements of :attr:`contents`.' random.shuffle(self._contents)
### PRIVATE METHODS ### def _generate_weights(self) -> None: r"""Given a decay rate, this method generates the :attr:`weights` of individual indeces. """ self._weights = [] for n in range(self.__len__()): self._weights.append(self._decay_rate ** n) ### PUBLIC PROPERTIES ### @property def contents(self) -> list[Any]: r'The :obj:`list` from which the selector picks elements.' return self._contents @contents.setter def contents(self, contents: list[Any], ) -> None: if not isinstance(contents, list): raise TypeError("'contents' must be 'list") self._contents = contents[:] self._generate_weights() @property def decay_rate(self) -> float: r"""The decay rate represents the ratio of probabilities of any index given the probability of the preceding one. For instance, if the decay rate is set to ``0.75`` (which is its default value), the probability of the element in index ``1`` of the input :obj:`list` being selected is ``0.75`` the probability of the element in index ``0``, and the probability of the element in index ``2`` is ``0.5625`` (i.e. ``0.75`` squared) the probability of the element in index ``0``. """ return self._decay_rate @decay_rate.setter def decay_rate(self, decay_rate: float, ) -> None: if not isinstance(decay_rate, float): raise TypeError("'decay_rate' must be float") if decay_rate <= 0.0 or decay_rate > 1.0: raise ValueError("'decay_rate' must be larger than 0.0 and less " "than or equal to 1.0") self._decay_rate = decay_rate self._generate_weights() @property def previous_index(self) -> Union[int, None]: r"""Read-only property, returns the index of the previously output element. """ return self._previous_index @property def previous_result(self) -> Any: r'Read-only property, returns the previously output element.' if self._previous_index is not None: return self._contents[self._previous_index] else: return self._previous_index @property def weights(self) -> list[float]: r'Read-only property, returns the weight vector.' return self._weights