Source code for rpscripts.tcontour

'''This module generates complexity map and adds level and sublevel complexity to given csv rhythmic partitioning data.

For further information, see Daniel Moreira (2015 and 2019).

Moreira, Daniel. 2015. "Perspectivas para a análise textural a partir da mediação entre a teoria dos contornos e a análise particional." Dissertação de mestrado, Universidade Federal do Rio de Janeiro.

Moreira, Daniel. 2019. "Textural Design: A Compositional Theory for the Organization of Musical Texture." Ph.D. Thesis, Universidade Federal do Rio de Janeiro.
'''


import statsmodels.nonparametric.smoothers_lowess

from rpscripts.config import LATTICE_MAP_PATH
from rpscripts.plotter import AbstractTimePlotter

from .lib.base import CustomException, GeneralSubparser, RPData, file_rename, load_json_file
from .lib.partition import Partition


[docs] class ExtendedPartition(Partition): '''Extend Partition class to handle textural contour.'''
[docs] def get_complexity_level(self, complexity_map: dict) -> int: '''Return partition's complexity level.''' density_number = self.get_density_number() if density_number == 0: return 0 try: m = complexity_map[str(density_number)] except: raise CustomException('The lattice map has not the density number {}'.format(density_number)) ind = m.index(int(self.get_dispersion_index())) return density_number + ind
[docs] class ContourPoint(object): '''Contour point class. It handles complexity level and sublevel''' def __init__(self, level=None, sublevel=None, partition_str=None) -> None: self.level = level self.sublevel = sublevel self.partition_str = partition_str def __eq__(self, other: object) -> bool: if isinstance(other, ContourPoint): if other.level == None: return False return self.level == other.level and self.sublevel == other.sublevel return False def __gt__(self, other: object) -> bool: if isinstance(other, ContourPoint): if other.level == None: return True if self.level == None: return False diff_level = self.level > other.level same_level = self.level == other.level and self.sublevel > other.sublevel return diff_level or same_level def __lt__(self, other: object) -> bool: if isinstance(other, ContourPoint): if other.level == None: return True if self.level == None: return False diff_level = self.level < other.level same_level = self.level == other.level and self.sublevel < other.sublevel return diff_level or same_level def __ge__(self, other: object) -> bool: if isinstance(other, ContourPoint): equal = self == other diff_level = self.level > other.level same_level = self.level == other.level and self.sublevel > other.sublevel return any([equal, diff_level, same_level]) def __repr__(self) -> str: if self.sublevel == 0: label = str(self.level) else: label = '{}-{}'.format(self.level, self.sublevel) return '<CP {} ({})>'.format(label, self.partition_str)
[docs] class Contour(object): '''Contour class. It handles the contour points.''' def __init__(self, str_partitions, complexity_map, locations, indexes) -> None: self.str_partitions = str_partitions self.str_partitions_set = list(set(self.str_partitions)) self.partitions_set = [ExtendedPartition(p) for p in self.str_partitions_set] self.partitions_info = {} self.indexes = indexes self.locations = locations self.contour_map = {} self.levels_map = {} self.levels_keys = [] self.levels_seq = [] self.sublevels_seq = [] self.sublevels_max = None self.cseg = [] self.complexity_map = complexity_map self.complexity_data = [] self.level_sublevel_seq = [] self.reduction_check = [] self.make_maps(complexity_map) self.make_contour() self.make_complexity_data() def __repr__(self) -> str: return '<C {}>'.format(', '.join(map(lambda el: '{}-{} ({})'.format(*el), self.cseg)))
[docs] def make_maps(self, complexity_map: dict) -> None: '''Make and set levels map and partitions info about complexity level and number of parts.''' self.partitions_info = {} self.levels_map = {} for partition in self.partitions_set: n_parts = len(partition.parts) str_partition = partition.as_string() complexity_level = partition.get_complexity_level(complexity_map) if complexity_level not in self.levels_map.keys(): self.levels_map[complexity_level] = set([]) self.levels_map[complexity_level].add(n_parts) self.partitions_info.update({ str_partition: (complexity_level, n_parts) }) self.levels_map = {k: self.levels_map[k] for k in sorted(self.levels_map.keys())} self.levels_keys = list(self.levels_map.keys())
[docs] def make_contour(self) -> None: '''Make the contour from the available complexity levels and partitions information.''' self.contour_map = {} for str_partition, (level, n_parts) in self.partitions_info.items(): n_parts_set = self.levels_map[level] contour_level = self.levels_keys.index(level) contour_sublevel = 0 if len(n_parts_set) > 1: contour_sublevel = list(n_parts_set).index(n_parts) + 1 self.contour_map.update({ str_partition: (contour_level, contour_sublevel) }) self.cseg = [] for str_partition in self.str_partitions: (contour_level, contour_sublevel) = self.contour_map[str_partition] cp = ContourPoint(contour_level, contour_sublevel, str_partition) self.cseg.append(cp) self.levels_seq.append(contour_level) self.sublevels_seq.append(contour_sublevel) self.sublevels_max = max(self.sublevels_seq) self.reduction_check = [True] * len(self.cseg)
[docs] def make_complexity_data(self) -> None: '''Make the complexity data from the available sequences of complexity levels and sublevels.''' self.complexity_data = [] for level, sublevel in zip(self.levels_seq, self.sublevels_seq): contour_repr = str(level) if int(sublevel) == 0: contour_repr = '{}-{}'.format(level, sublevel) self.level_sublevel_seq.append(contour_repr) self.complexity_data.append([ level, sublevel, contour_repr ])
[docs] class ExtendedRPData(RPData): '''Extend RPData class to add complexity (contour) data.'''
[docs] def add_complexity_data(self, contour: Contour): rows = contour.level_sublevel_seq self.tcontour = rows self.save_to_file()
[docs] class ContourPlot(AbstractTimePlotter): '''Contour Plot class.''' def __init__(self, contour: Contour, rpdata: RPData, image_format='svg', show_labels=False, run_lowess=False, lowess_degree=0.05, as_step=False) -> None: self.name = 'complexity' self.contour = contour self.run_lowess = run_lowess self.lowess_degree = lowess_degree self.as_step = as_step super().__init__(rpdata, image_format, show_labels)
[docs] def plot(self): level_seq = self.contour.levels_seq sublevel_seq = self.contour.sublevels_seq sublevel_max = self.contour.sublevels_max if sublevel_max > 0: y_values = [(level + sublevel) / (sublevel_max * 2) for level, sublevel in zip(level_seq, sublevel_seq)] else: y_values = level_seq # plot or step function if self.as_step: self.axis.step(self.x_values[:-1], y_values, where='post') else: self.axis.plot(self.x_values[:-1], y_values) if self.run_lowess: lowess = statsmodels.nonparametric.smoothers_lowess.lowess(y_values, self.x_values[:-1], frac=self.lowess_degree)[:, 1] self.axis.plot(self.x_values[:-1], lowess) self.axis.legend(['Contour', 'Lowess {}'.format(self.lowess_degree)]) # Show only integer yticks ypositions = [] ylabels = [] for tobj in self.axis.get_yticklabels(): yval = tobj._y ypositions.append(yval) ytick = '' if int(abs(int(yval))) == abs(yval): ytick = str(int(yval)) ylabels.append(ytick) self.axis.set_yticks(ypositions, ylabels) self.axis.set_ylabel('Contour points') self.make_xticks() return super().plot()
[docs] def make_contour_object(rpdata: RPData, complexity_map: dict) -> Contour: '''Return a contour object.''' partitions = rpdata.partitions locations = rpdata.data['Global offset'] indexes = rpdata.data['Index'] return Contour(partitions, complexity_map, locations, indexes)
[docs] class Subparser(GeneralSubparser): '''Implements argparser.'''
[docs] def setup(self) -> None: self.program_name = 'tcontour' self.program_help = 'Textural contour calculator and plotter'
[docs] def add_arguments(self) -> None: self.parser.add_argument("-fl", "--show_form_labels", help = "Draw vertical lines to display given form labels. It demands a previous labeled file. Check rpscripts labels -h' column", default=False, action='store_true') self.parser.add_argument("-np", "--no_plot", help='No Plot chart', default=False, action='store_true') self.parser.add_argument("-o", "--lowess", help='Plot LOWESS', default=False, action='store_true') self.parser.add_argument("--lowess_degree", help='Lowess degree', default=0.05, type=float) self.parser.add_argument("-s", "--as_step", help='Step chart', default=False, action='store_true')
[docs] def handle(self, args): json_filename = args.filename cdic_filename = LATTICE_MAP_PATH print('Loading complexity_map in {}...'.format(cdic_filename)) try: complexity_map = load_json_file(cdic_filename) except: raise CustomException('Failure on complexity map loading...') print('Generating texture contour...') rpdata = ExtendedRPData(args.filename) contour = make_contour_object(rpdata, complexity_map) rpdata.add_complexity_data(contour) if not args.no_plot: figname = file_rename(json_filename, 'svg', 'complexity') print('Saving texture contour plot in {}...'.format(figname)) contour_plot = ContourPlot(contour, rpdata, 'svg', args.show_form_labels, args.lowess, lowess_degree=args.lowess_degree, as_step=args.as_step) contour_plot.plot() contour_plot.save()