Source code for juggling.pattern

from collections import Iterable
from math import floor

try:
    # Python3
    from collections import UserList  # noqa
except ImportError:
    # Python2
    from UserList import UserList  # noqa

from juggling.utils import CacheProperties


__all__ = ['Pattern']


VANILLA_PATTERN = 'VSS'
MULTIPLEX_PATTERN = 'MSS'
SYNCHRONOUS_PATTERN = 'SSS'


def flatten_pattern_list(l):
    """ Flattens a pattern list into a list of tuples where each tuple is (beat, throw).

    >>> flatten_pattern_list([4,4,1])
        [(0,4),(1,4),(2,1)]
    >>> flatten_pattern_list([[53],3,1])
        [(0,5),(0,3),(1,3),(1,1)]
    >>>
    """
    # type: list -> list
    flattened = []
    for beat, el in enumerate(l):
        if isinstance(el, Iterable):
            for sub in flatten_pattern_list(el):
                flattened.append((beat, sub[1]))
        else:
            flattened.append((beat, el))
    return flattened


def convert_sss_to_mss(pattern_list):
    """ Flattens a SSS to a MSS, returns a Pattern list """
    data = []

    def mod_sync(t, mod=0):
        if isinstance(t, float):
            return floor(t) + mod
        return t

    for _ in pattern_list:
        if isinstance(_, tuple):
            d = []
            if isinstance(_[0], list):
                d.append([mod_sync(n, 1) for n in _[0]])
            else:
                d.append(mod_sync(_[0], 1))

            if isinstance(_[1], Iterable):
                d.append([mod_sync(n, -1) for n in _[1]])
            else:
                d.append(mod_sync(_[1], -1))
            data += d
        else:
            data.append(_)
    return data


def generate_state(pattern, starting_throw=0):
    # type: (Pattern, int) -> list
    """ Generates a state for a given :class:`Pattern` starting at `starting_throw` which is an
    offset into the :class:`Pattern` """

    state = [None] * (pattern.max_throw * 2)  # principal that the highest throw can go through the pattern twice
    # offset the pattern to the `starting_throw`
    starting_throw %= len(pattern.converted_to_mss)
    converted_pattern = pattern.converted_to_mss[starting_throw:] + pattern.converted_to_mss[:starting_throw]

    i = current_beat = current_prop = 0
    flattened = flatten_pattern_list(converted_pattern)
    while current_prop < pattern.num_objects:
        if flattened[i][1] == 0:
            state[current_beat] = 1
        else:
            if state[current_beat] is None:
                state[current_beat] = 1
            else:
                state[current_beat] += 1

            if state[current_beat] > 0:
                current_prop += 1

            d = current_beat + flattened[i][1]
            if state[d] is None:
                state[d] = -1
            else:
                state[d] -= 1

            i += 1
            if i == len(flattened):
                i = 0
                current_beat += 1
            elif flattened[i][0] != flattened[i-1][0]:
                current_beat += 1

    # remove trailing info
    while state and state[-1] is None or state[-1] < 0:
        del state[-1]

    if -1 in state or None in state:
        return []

    return state


[docs]class Pattern(CacheProperties, UserList): """ The base representation of any juggling pattern, regardless of notation. A :class:`Pattern` looks and acts like a list, but has some specific constraints about how data can be stored in it. >>> p = Pattern([4, 4, 1]) >>> p.is_symmetric == True >>> p.period == 3 >>> p.is_excited == False """ def __init__(self, pattern): # TODO: this is a stop-gap, follow the discussion on the group to figure out how we're actually super(Pattern, self).__init__() UserList.__init__(self, initlist=pattern) def __setattr__(self, key, value): self._clear_cache() super(Pattern, self).__setattr__(key, value) def __setitem__(self, key, value): self._clear_cache() super(Pattern, self).__setitem__(key, value) @property def type(self): if isinstance(self[0], tuple): return SYNCHRONOUS_PATTERN for throw in self: if isinstance(throw, list): return MULTIPLEX_PATTERN return VANILLA_PATTERN @property def converted_to_mss(self): return convert_sss_to_mss(self.data) @property def throws_with_beats(self): return flatten_pattern_list(self.converted_to_mss) @property def throw_destinations(self): total = 0 destination = [] for throw in self.throws_with_beats: if throw[1] == 0: destination.append(throw[0]) else: destination.append((throw[0] + throw[1]) % self.period) total += throw[1] return destination @property def incoming(self): incoming = [0] * self.period for beat, dest in enumerate(self.throw_destinations): if self.throws_with_beats[beat][1] != 0: incoming[dest] += 1 return incoming @property def outgoing(self): outgoing = [0] * self.period for beat in range(len(self.throw_destinations)): if self.throws_with_beats[beat][1] != 0: outgoing[self.throws_with_beats[beat][0]] += 1 return outgoing @property def is_valid(self): return self.incoming == self.outgoing
[docs] def starting_with(self, throw): """ return the pattern if started with throw at index `throw` """ throw %= (self.period//2) if self.type == SYNCHRONOUS_PATTERN else self.period return self.data[throw:] + self.data[:throw]
@property def states(self): if self.is_valid: period = (self.period//2) if self.type == SYNCHRONOUS_PATTERN else self.period return tuple(generate_state(self, i) for i in range(0, period)) return [] @property def num_objects(self): def get_sum(_): s = 0 for i in _: if isinstance(i, (tuple, list)): i = get_sum(i) s += i return s return floor(get_sum(self.data) / self.period) @property def period(self): return len(self.converted_to_mss) @property def max_throw(self): def get_max(_, highest=0): for i in _: if isinstance(i, (tuple, list)): i = get_max(i, highest) highest = max(i, highest) return highest return floor(get_max(self.data)) # floor in case of sync crossing throws with .5 @property def ground_states(self): return [self.starting_with(i) for i, state in enumerate(self.states) if len(set(state)) == 1] @property def is_ground_state(self): if self.states: return len(set(self.states[0])) == 1 return False @property def current_state(self): if self.states: return self.states[0] return [] @property def entry_transitions(self): if not self.is_excited: return [] transition = [] current_throw = self.num_objects for beat in self.current_state: if beat > 0: transition.append(current_throw) else: current_throw += 1 # remove any base pattern throws from the beginning while transition and transition[0] == self.num_objects: del transition[0] return transition @property def exit_transitions(self): if not self.is_excited: return [] transition = [] current_zero_loc = 0 for i in range(0, self.num_objects): try: next_zero = self.current_state[current_zero_loc:].index(0) current_zero_loc = next_zero + 1 transition.append(next_zero - i) except ValueError: transition.append(self.num_objects) # remove any base pattern throws from the beginning while transition and transition[-1] == self.num_objects: del transition[-1] return transition @property def is_symmetric(self): """ Returns True if this pattern is symmetric, the pattern repeats with the opposite hand """ return bool(self.period & 0x1) @property def is_asymmetric(self): """ Returns True if this pattern is asymmetric, the pattern repeats with the same hand """ return not self.is_symmetric @property def is_excited(self): return len(set(self.current_state)) != 1