Source code for rpscripts.lib.partition

'''This module provides classes and functions for rhythmic partitioning analysis. For further information, see Gentil-Nunes 2009.

Gentil-Nunes, Pauxy. 2009. "Análise Particional: uma Mediação entre Composição Musical e a Teoria das Partições." Ph.D. Dissertation, Universidade Federal do Estado do Rio de Janeiro.

Gentil-Nunes, Pauxy. 2017. "Nestings and Intersections Between Partitional Complexes." MusMat - Brazilian Journal of Music and Mathematics I (2): 93--108. Available at https://musmat.org/wp-content/uploads/2018/06/09-Pauxy.pdf.
'''

from collections import Counter
from copy import deepcopy
import itertools
import json
import re


from ..config import ENCODING
from .base import get_number_combinations_pairs


[docs] def make_subseq(partition, n) -> list: '''Return a subset array from a given partition from 0 to `n` element.''' return [partition[i] for i in range(n)]
[docs] def get_partitions(number: int) -> list: '''Return the partitions of a given `number` as a list of lists.''' partitions_list = [] partition = [0] * number last_pos = 0 partition[last_pos] = number while True: partitions_list.append(make_subseq(partition, last_pos + 1)) rem_val = 0 while last_pos >= 0 and partition[last_pos] == 1: rem_val += partition[last_pos] last_pos -= 1 if last_pos < 0: return partitions_list partition[last_pos] -= 1 rem_val += 1 while rem_val > partition[last_pos]: partition[last_pos + 1] = partition[last_pos] rem_val -= partition[last_pos] last_pos += 1 partition[last_pos + 1] = rem_val last_pos += 1
[docs] def get_lexset(number: int) -> list: '''Return the lexical set of a given `number`. The lexical set is the list of all partitions from 1 to number.''' partitions = [] for n in range(1, number + 1): partitions.extend(get_partitions(n)) return partitions
[docs] def make_ryp_map(higher_cardinality: int) -> dict: '''Make a rhythmic partitioning Young lattice of a given cardinality.''' partitions = get_lexset(higher_cardinality) dic = {} for p in partitions: obj = Partition(p) dispersion = obj.get_dispersion_index() dn = obj.get_density_number() if dn not in dic.keys(): dic[dn] = [] if dispersion not in dic[dn]: dic[dn].append(dispersion) return {k: sorted(v) for k, v in dic.items()}
[docs] class Partition(object): '''Main partition class.''' def __init__(self, parts=None) -> None: self.parts = None self.parts_map = None if isinstance(parts, list): self.parts = sorted(parts) elif isinstance(parts, str): self.parts = [] for el in parts.split('.'): if '^' in el: part, n = el.split('^') self.parts.extend([int(part)] * int(n)) else: self.parts.append(int(el)) self.counter = Counter(self.parts) def __eq__(self, __value: object) -> bool: self.parts == __value.parts def __repr__(self) -> str: return '<P {}>'.format(self.as_string())
[docs] def as_string(self) -> str: '''Return the partition as a string such as "1^3.2".''' if not self.parts: return '0' dic = {} for el in self.parts: if el not in dic.keys(): dic[el] = 0 dic[el] += 1 str_repr = '.'.join([str(k) if v < 2 else '{}^{}'.format(k, v) for k, v in sorted(dic.items()) ]) return str_repr
[docs] def get_parts_size(self) -> int: '''Count the partition's number of parts.''' return len(self.parts)
[docs] def get_density_number(self) -> int: '''Return the partition's density number.''' return int(sum(self.parts))
[docs] def count_binary_relations(self): '''Count binary relations of partition's parts.''' density_number = self.get_density_number() return get_number_combinations_pairs(density_number)
[docs] def get_agglomeration_index(self) -> int: '''Return the partition's agglomeration index.''' if self.parts == []: return None return int(sum([get_number_combinations_pairs(n) for n in self.parts]))
[docs] def get_dispersion_index(self) -> int: '''Return the partition's dispersion index.''' if self.parts == []: return None return int(self.count_binary_relations() - self.get_agglomeration_index())
[docs] def resize(self) -> list: '''Return the list of current's resized partitions.''' # Resizing (m) a part means to change its thickness. The positive # resizing implies the inclusion of more sounding components to a # block, making it "fatter"; the negative resizing is, on the contrary, # the thickening of a part, subtracting a sounding component from a # block. (Gentil-Nunes, 2017) resized = [] for part in self.counter.keys(): _parts = deepcopy(self.parts) ind = _parts.index(part) _parts[ind] += 1 _parts.sort() resized.append(_parts) return list(map(Partition, resized))
[docs] def revariate(self) -> list: '''Return the list of current's revariated partitions.''' # Revariance (v) is the changing of variety (number of parts) inside a # textural configuration. Positive revariance implies adding an unitary # part to the partition and negative revariance means subtracting an # unitary part from it. (Gentil-Nunes, 2017) return Partition(self.parts + [1])
[docs] def transfer(self, default=True) -> list: '''Return the list of current's transfered partitions.''' # Transference (t) arises when resizing and revariance are applied # together, but with opposite signals (positive resizing with negative # revariance, and vice-versa). The consequence is that one sounding # component is displaced from a part to another, without affecting the # overall density- number. This kind of operation is very common in # traditional concert music. (Gentil-Nunes, 2017) def swap(part_1, part_2): _parts = deepcopy(self.parts) i1 = _parts.index(part_1) _parts.pop(i1) i2 = _parts.index(part_2) _parts.pop(i2) if part_1 > 1: _parts.append(part_1 - 1) _parts.append(part_2 + 1) return Partition(_parts) def subtract(part): if part > 1: _parts = deepcopy(self.parts) ind = _parts.index(part) _parts[ind] -= 1 _parts.append(1) return Partition(_parts) base_disp_ind = self.get_dispersion_index() low_pos = 10 ** 10 high_neg = -1 def update_pos_neg(p: Partition, low_pos, high_neg) -> tuple: p_disp_ind = p.get_dispersion_index() if p_disp_ind > base_disp_ind and p_disp_ind < low_pos: low_pos = p_disp_ind if p_disp_ind < base_disp_ind and p_disp_ind > high_neg: high_neg = p_disp_ind return low_pos, high_neg partitions = [] for part, quantity in self.counter.items(): # subtract from an existing part and move to a new part p = subtract(part) if p: low_pos, high_neg = update_pos_neg(p, low_pos, high_neg) partitions.append(p) # transfer between parts with equal values if quantity > 1: p = swap(part, part) if p: low_pos, high_neg = update_pos_neg(p, low_pos, high_neg) partitions.append(p) # transfer/swap between parts with different values distinct = list(self.counter.keys()) for part_1, part_2 in itertools.combinations(distinct, 2): p = swap(part_1, part_2) if p.parts != self.parts and p not in partitions: low_pos, high_neg = update_pos_neg(p, low_pos, high_neg) partitions.append(p) p = swap(part_2, part_1) if p.parts != self.parts and p not in partitions: low_pos, high_neg = update_pos_neg(p, low_pos, high_neg) partitions.append(p) result = {'positive': [], 'negative': []} partitions = sorted(partitions, key=lambda p: p.get_agglomeration_index()) for p in partitions: p_disp_ind = p.get_dispersion_index() if default: if p_disp_ind == low_pos: result['positive'].append(p) elif p_disp_ind == high_neg: result['negative'].append(p) else: if p_disp_ind > base_disp_ind: result['positive'].append(p) elif p_disp_ind < base_disp_ind: result['negative'].append(p) return result
[docs] class PartitionLattice(object): '''Partition Lattice class. It helps in lattice creating and saving.''' def __init__(self, cardinality=10) -> None: print('Creating lattice map with cardinality {}.'.format(cardinality)) self.filename = 'lattice_map.json' self.data = {} self.data = make_ryp_map(cardinality)
[docs] def save_file(self): '''Save lattice map into file.''' print('Saving lattice map to file {}.'.format(self.filename)) with open(self.filename, 'w', encoding=ENCODING) as fp: json.dump(self.data, fp)