예제 #1
0
def test_mathtools_Ratio___eq___01():

    ratio_1 = mathtools.Ratio((1, 2, 1))
    ratio_2 = mathtools.Ratio((1, 2, 1))
    ratio_3 = mathtools.Ratio((2, 3, 3))

    assert ratio_1 == ratio_1
    assert ratio_1 == ratio_2
    assert not ratio_1 == ratio_3
    assert ratio_2 == ratio_1
    assert ratio_2 == ratio_2
    assert not ratio_2 == ratio_3
    assert not ratio_3 == ratio_1
    assert not ratio_3 == ratio_2
    assert ratio_3 == ratio_3
 def _get_ratios(self):
     if self.ratios:
         ratios = self.ratios
     else:
         ratios = (mathtools.Ratio([1]), )
     ratios = datastructuretools.CyclicTuple(ratios)
     return ratios
예제 #3
0
def test_systemtools_StorageFormatAgent_get_import_statements_04():
    subject = rhythmmakertools.IncisedRhythmMaker(
        incise_specifier=rhythmmakertools.InciseSpecifier(
            prefix_talea=(1, ),
            prefix_counts=(0, ),
            suffix_talea=(1, ),
            suffix_counts=(1, ),
            talea_denominator=16,
            body_ratio=mathtools.Ratio((1, )),
            outer_divisions_only=True,
        ),
        beam_specifier=rhythmmakertools.BeamSpecifier(
            beam_each_division=False,
            beam_divisions_together=False,
        ),
        duration_spelling_specifier=rhythmmakertools.DurationSpellingSpecifier(
            decrease_durations_monotonically=True,
            forbidden_written_duration=durationtools.Duration(1, 2),
        ),
        tuplet_spelling_specifier=rhythmmakertools.TupletSpellingSpecifier(
            avoid_dots=True,
            is_diminution=True,
            simplify_redundant_tuplets=True,
        ),
    )
    agent = systemtools.StorageFormatAgent(subject)
    assert agent.get_import_statements() == (
        'from abjad.tools import durationtools',
        'from abjad.tools import mathtools',
        'from abjad.tools import rhythmmakertools',
    )
 def __init__(
     self,
     ratios=None,
 ):
     if ratios is not None:
         ratios = ratios or ()
         ratios = [mathtools.Ratio(_) for _ in ratios]
         ratios = tuple(ratios)
     self._ratios = ratios
예제 #5
0
def partition_sequence_by_ratio_of_lengths(sequence, ratio):
    '''Partitions `sequence` by `ratio` of lengths.

    ..  container:: example

        **Example 1.** Partitions sequence by ``1:1:1`` ratio:

        ::

            >>> sequence = list(range(10))
            >>> sequencetools.partition_sequence_by_ratio_of_lengths(
            ...     sequence,
            ...     [1, 1, 1],
            ...     )
            [[0, 1, 2], [3, 4, 5, 6], [7, 8, 9]]

        Returns list of lists.

    ..  container:: example

        **Example 2.** Partitions sequence by ``1:1:2`` ratio:

        ::

            >>> sequence = tuple(range(10))
            >>> sequencetools.partition_sequence_by_ratio_of_lengths(
            ...     sequence,
            ...     [1, 1, 2],
            ...     )
            [(0, 1, 2), (3, 4), (5, 6, 7, 8, 9)]

        Returns list of tuples.

    Uses the rounding magic implemented in
    ``mathtools.partition_integer_by_ratio()`` to avoid fractional part
    lengths.

    Returns list of `sequence` objects.
    '''
    from abjad.tools import sequencetools
    if sequence is None:
        callback = sequencetools.PartitionByRatioOfLengthsCallback(
            ratio=ratio, )
        return callback
    ratio = mathtools.Ratio(ratio)
    counts = mathtools.partition_integer_by_ratio(len(sequence), ratio)
    return sequencetools.partition_sequence_by_counts(
        sequence,
        counts,
        cyclic=False,
        overhang=Exact,
    )
예제 #6
0
 def __init__(
     self,
     prefix_talea=None,
     prefix_counts=None,
     suffix_talea=None,
     suffix_counts=None,
     talea_denominator=None,
     body_ratio=None,
     fill_with_notes=True,
     outer_divisions_only=None,
 ):
     prefix_talea = prefix_talea or ()
     prefix_talea = tuple(prefix_talea)
     assert self._is_integer_tuple(prefix_talea)
     self._prefix_talea = prefix_talea
     prefix_counts = prefix_counts or ()
     prefix_counts = tuple(prefix_counts)
     assert self._is_length_tuple(prefix_counts)
     self._prefix_counts = prefix_counts
     if prefix_counts and prefix_counts != (0, ):
         assert prefix_talea
     if prefix_talea:
         assert prefix_counts
     suffix_talea = suffix_talea or ()
     suffix_talea = tuple(suffix_talea)
     assert self._is_integer_tuple(suffix_talea)
     self._suffix_talea = suffix_talea
     assert self._is_length_tuple(suffix_counts)
     suffix_counts = suffix_counts or ()
     suffix_counts = tuple(suffix_counts)
     self._suffix_counts = suffix_counts
     if suffix_counts and suffix_counts != (0, ):
         assert suffix_talea
     if suffix_talea:
         assert suffix_counts
     if talea_denominator is not None:
         if not mathtools.is_nonnegative_integer_power_of_two(
                 talea_denominator):
             message = 'talea denominator {!r} must be nonnegative'
             message += ' integer power of 2.'
             message = message.format(talea_denominator)
             raise Exception(message)
     self._talea_denominator = talea_denominator
     if prefix_talea or suffix_talea:
         assert talea_denominator is not None
     if body_ratio is not None:
         body_ratio = mathtools.Ratio(body_ratio)
     self._body_ratio = body_ratio
     assert isinstance(fill_with_notes, bool)
     self._fill_with_notes = fill_with_notes
     assert isinstance(outer_divisions_only, (bool, type(None)))
     self._outer_divisions_only = outer_divisions_only
예제 #7
0
    def partition_by_ratio_of_durations(self, ratio):
        r'''Partition payload by `ratio` of durations.

        Returns tuple of newly constructed expressions with callbacks.
        '''
        result = []
        ratio = mathtools.Ratio(ratio)
        for part in range(len(ratio)):
            callback = \
                'result = self._partition_by_ratio_of_durations(payload_expression, {!r}, {!r})'
            callback = callback.format(ratio, part)
            result.append(self._copy_and_append_callback(callback))
        return tuple(result)
예제 #8
0
 def __init__(
     self,
     prefix_talea=None,
     prefix_counts=None,
     suffix_talea=None,
     suffix_counts=None,
     talea_denominator=None,
     body_ratio=None,
     fill_with_notes=True,
     outer_divisions_only=False,
 ):
     prefix_talea = prefix_talea or ()
     prefix_talea = tuple(prefix_talea)
     assert self._is_integer_tuple(prefix_talea)
     self._prefix_talea = prefix_talea
     prefix_counts = prefix_counts or ()
     prefix_counts = tuple(prefix_counts)
     assert self._is_length_tuple(prefix_counts)
     self._prefix_counts = prefix_counts
     if prefix_counts and prefix_counts != (0, ):
         assert prefix_talea
     if prefix_talea:
         assert prefix_counts
     suffix_talea = suffix_talea or ()
     suffix_talea = tuple(suffix_talea)
     assert self._is_integer_tuple(suffix_talea)
     self._suffix_talea = suffix_talea
     assert self._is_length_tuple(suffix_counts)
     suffix_counts = suffix_counts or ()
     suffix_counts = tuple(suffix_counts)
     self._suffix_counts = suffix_counts
     if suffix_counts and suffix_counts != (0, ):
         assert suffix_talea
     if suffix_talea:
         assert suffix_counts
     if talea_denominator is not None:
         assert mathtools.is_nonnegative_integer_power_of_two(
             talea_denominator)
     self._talea_denominator = talea_denominator
     if prefix_talea or suffix_talea:
         assert talea_denominator is not None
     if body_ratio is not None:
         body_ratio = mathtools.Ratio(body_ratio)
     self._body_ratio = body_ratio
     assert isinstance(fill_with_notes, bool)
     self._fill_with_notes = fill_with_notes
     assert isinstance(outer_divisions_only, bool)
     self._outer_divisions_only = outer_divisions_only
예제 #9
0
def divide_number_by_ratio(number, ratio):
    r'''Divides integer by `ratio`.

    ::

        >>> mathtools.divide_number_by_ratio(1, [1, 1, 3])
        [Fraction(1, 5), Fraction(1, 5), Fraction(3, 5)]

    Divides fraction by `ratio`:

    ::

        >>> mathtools.divide_number_by_ratio(Fraction(1), [1, 1, 3])
        [Fraction(1, 5), Fraction(1, 5), Fraction(3, 5)]

    Divides float by ratio:

    ::

        >>> mathtools.divide_number_by_ratio(1.0, [1, 1, 3]) # doctest: +SKIP
        [0.20000000000000001, 0.20000000000000001, 0.60000000000000009]

    Raises type error on nonnumeric `number`.

    Raises type error on noninteger in `ratio`.

    Returns list of fractions or list of floats.
    '''
    from abjad.tools import mathtools

    # check input
    assert isinstance(number, numbers.Number)
    ratio = mathtools.Ratio(ratio)

    # find factors and multiply by factors
    factors = [
        fractions.Fraction(p, sum(ratio.numbers)) for p in ratio.numbers
    ]
    result = [factor * number for factor in factors]

    # return result
    return result
예제 #10
0
    def ratio(self):
        r'''Gets ratio of metric modulation.

        ..  container:: example

            ::

                >>> metric_modulation = indicatortools.MetricModulation(
                ...     left_rhythm=Tuplet((2, 3), [Note("c'4")]),
                ...     right_rhythm=Note("c'4"),
                ...     )
                >>> metric_modulation.ratio
                Ratio((2, 3))

        Returns ratio.
        '''
        left_duration = self.left_rhythm.get_duration()
        right_duration = self.right_rhythm.get_duration()
        duration = left_duration / right_duration
        ratio = mathtools.Ratio(duration.pair)
        return ratio
예제 #11
0
 def _to_tuplet_with_ratio(self, proportions, is_diminution=True):
     from abjad.tools import scoretools
     # check input
     proportions = mathtools.Ratio(proportions)
     # find target duration of fixed-duration tuplet
     target_duration = self.written_duration
     # find basic duration of note in tuplet
     basic_prolated_duration = target_duration / sum(proportions.numbers)
     # find basic written duration of note in tuplet
     basic_written_duration = \
         basic_prolated_duration.equal_or_greater_assignable
     # find written duration of each note in tuplet
     written_durations = [
         _ * basic_written_duration for _ in proportions.numbers
     ]
     # make tuplet notes
     try:
         notes = [scoretools.Note(0, x) for x in written_durations]
     except AssignabilityError:
         denominator = target_duration._denominator
         note_durations = [
             durationtools.Duration(_, denominator)
             for _ in proportions.numbers
         ]
         notes = scoretools.make_notes(0, note_durations)
     # make tuplet
     tuplet = scoretools.FixedDurationTuplet(target_duration, notes)
     # fix tuplet contents if necessary
     tuplet._fix()
     # change prolation if necessary
     if not tuplet.multiplier == 1:
         if is_diminution:
             if not tuplet.is_diminution:
                 tuplet.toggle_prolation()
         else:
             if tuplet.is_diminution:
                 tuplet.toggle_prolation()
     # return tuplet
     return tuplet
예제 #12
0
    def tessalate_by_ratio(
        self,
        ratio,
        invert_on_negative=False,
        reflect_on_negative=False,
        y_center=None,
    ):
        r'''Concatenate copies of a BreakPointFunction, stretched by
        the weights in `ratio`:

        ::

            >>> bpf = interpolationtools.BreakPointFunction(
            ...     {0.: 0., 0.25: 0.9, 1.: 1.})

        ::

            >>> bpf.tessalate_by_ratio((1, 2, 3))
            BreakPointFunction({
                0.0: (0.0,),
                0.25: (0.9,),
                1.0: (1.0, 0.0),
                1.5: (0.9,),
                3.0: (1.0, 0.0),
                3.75: (0.9,),
                6.0: (1.0,)
            })

        Negative ratio values are still treated as weights.

        If `invert_on_negative` is True, copies corresponding to
        negative ratio values will be inverted:

        ::

            >>> bpf.tessalate_by_ratio((1, -2, 3), invert_on_negative=True)
            BreakPointFunction({
                0.0: (0.0,),
                0.25: (0.9,),
                1.0: (1.0,),
                1.5: (0.09999999999999998,),
                3.0: (0.0,),
                3.75: (0.9,),
                6.0: (1.0,)
            })

        If `y_center` is not None, inversion will take `y_center` as
        the axis of inversion:

        ::

            >>> bpf.tessalate_by_ratio((1, -2, 3),
            ...     invert_on_negative=True, y_center=0)
            BreakPointFunction({
                0.0: (0.0,),
                0.25: (0.9,),
                1.0: (1.0, 0.0),
                1.5: (-0.9,),
                3.0: (-1.0, 0.0),
                3.75: (0.9,),
                6.0: (1.0,)
            })

        If `reflect_on_negative` is True, copies corresponding to
        negative ratio values will be reflected:

        ::

            >>> bpf.tessalate_by_ratio((1, -2, 3), reflect_on_negative=True)
            BreakPointFunction({
                0.0: (0.0,),
                0.25: (0.9,),
                1.0: (1.0,),
                2.5: (0.9,),
                3.0: (0.0,),
                3.75: (0.9,),
                6.0: (1.0,)
            })

        Inversion may be combined reflecting.

        Emit new `BreakPointFunction` instance.
        '''
        ratio = mathtools.Ratio(ratio)
        tessalated_bpf = None
        for i, pair in enumerate(
                mathtools.cumulative_sums_pairwise(
                    [abs(x) for x in ratio.numbers])):
            sign = mathtools.sign(ratio.numbers[i])
            start, stop = pair
            bpf = self.scale_x_axis(start, stop)
            if sign < 0:
                if invert_on_negative:
                    bpf = bpf.invert(y_center)
                if reflect_on_negative:
                    bpf = bpf.reflect()
            if i == 0:
                tessalated_bpf = bpf
            else:
                tessalated_bpf = tessalated_bpf.concatenate(bpf)
        return tessalated_bpf
예제 #13
0
def test_mathtools_Ratio___eq___02():
    r'''Comparison works with tuples.
    '''

    assert mathtools.Ratio((1, 2, 1)) == (1, 2, 1)
def partition_integer_by_ratio(n, ratio):
    r'''Partitions positive integer-equivalent `n` by `ratio`.

    ..  container:: example

        >>> abjad.mathtools.partition_integer_by_ratio(10, [1, 2])
        [3, 7]

    ..  container:: example

        Partitions positive integer-equivalent `n` by `ratio` with negative
        parts:

        >>> abjad.mathtools.partition_integer_by_ratio(10, [1, -2])
        [3, -7]

    ..  container:: example

        Partitions negative integer-equivalent `n` by `ratio`:

        >>> abjad.mathtools.partition_integer_by_ratio(-10, [1, 2])
        [-3, -7]

    ..  container:: example

        Partitions negative integer-equivalent `n` by `ratio` with negative
        parts:

        >>> abjad.mathtools.partition_integer_by_ratio(-10, [1, -2])
        [-3, 7]

    ..  container:: example

        More examples:

        >>> abjad.mathtools.partition_integer_by_ratio(10, [1])
        [10]

        >>> abjad.mathtools.partition_integer_by_ratio(10, [1, 1])
        [5, 5]

        >>> abjad.mathtools.partition_integer_by_ratio(10, [1, -1, -1])
        [3, -4, -3]

        >>> abjad.mathtools.partition_integer_by_ratio(-10, [1, 1, 1, 1])
        [-3, -2, -3, -2]

        >>> abjad.mathtools.partition_integer_by_ratio(-10, [1, 1, 1, 1, 1])
        [-2, -2, -2, -2, -2]

    Returns result with weight equal to absolute value of `n`.

    Returns list of integers.
    '''
    from abjad.tools import mathtools
    if not mathtools.is_integer_equivalent_number(n):
        message = 'is not integer-equivalent number: {!r}.'
        message = message.format(n)
        raise TypeError(message)
    ratio = mathtools.Ratio(ratio).numbers
    if not all(mathtools.is_integer_equivalent_number(part) for part in ratio):
        message = 'some parts in {!r} not integer-equivalent numbers.'
        message = message.format(ratio)
        raise TypeError(message)
    result = [0]
    divisions = [
        float(abs(n)) * abs(part) / mathtools.weight(ratio) for part in ratio
    ]
    cumulative_divisions = mathtools.cumulative_sums(divisions, start=None)
    for division in cumulative_divisions:
        rounded_division = int(round(division)) - sum(result)
        if division - round(division) == 0.5:
            rounded_division += 1
        result.append(rounded_division)
    result = result[1:]
    if mathtools.sign(n) == -1:
        result = [-x for x in result]
    ratio_signs = [mathtools.sign(x) for x in ratio]
    result = [pair[0] * pair[1] for pair in zip(ratio_signs, result)]
    return result
 def __init__(self, ratio=None):
     ratio = ratio or mathtools.Ratio((1, ))
     ratio = mathtools.Ratio(ratio)
     self._ratio = ratio
예제 #16
0
    def to_tuplet(
        self,
        proportions,
        dotted=False,
        is_diminution=True,
        ):
        r'''Change logical tie to tuplet.

        ..  container:: example

            **Example 1.** Change logical tie to diminished tuplet:

            ::

                >>> staff = Staff(r"c'8 ~ c'16 cqs''4")
                >>> crescendo = spannertools.Hairpin(descriptor='p < f')
                >>> attach(crescendo, staff[:])
                >>> override(staff).dynamic_line_spanner.staff_padding = 3
                >>> time_signature = TimeSignature((7, 16))
                >>> attach(time_signature, staff)

            ..  doctest::

                >>> print(format(staff))
                \new Staff \with {
                    \override DynamicLineSpanner #'staff-padding = #3
                } {
                    \time 7/16
                    c'8 ~ \< \p
                    c'16
                    cqs''4 \f
                }

            ::

                >>> show(staff) # doctest: +SKIP

            ::

                >>> logical_tie = inspect_(staff[0]).get_logical_tie()
                >>> logical_tie.to_tuplet([2, 1, 1, 1], is_diminution=True)
                FixedDurationTuplet(Duration(3, 16), "c'8 c'16 c'16 c'16")

            ..  doctest::

                >>> print(format(staff))
                \new Staff \with {
                    \override DynamicLineSpanner #'staff-padding = #3
                } {
                    \time 7/16
                    \tweak #'text #tuplet-number::calc-fraction-text
                    \times 3/5 {
                        c'8 \< \p
                        c'16
                        c'16
                        c'16
                    }
                    cqs''4 \f
                }

            ::

                >>> show(staff) # doctest: +SKIP

        ..  container:: example

            **Example 2.** Change logical tie to augmented tuplet:

            ::

                >>> staff = Staff(r"c'8 ~ c'16 cqs''4")
                >>> crescendo = spannertools.Hairpin(descriptor='p < f')
                >>> attach(crescendo, staff[:])
                >>> override(staff).dynamic_line_spanner.staff_padding = 3
                >>> time_signature = TimeSignature((7, 16))
                >>> attach(time_signature, staff)

            ..  doctest::

                >>> print(format(staff))
                \new Staff \with {
                    \override DynamicLineSpanner #'staff-padding = #3
                } {
                    \time 7/16
                    c'8 ~ \< \p
                    c'16
                    cqs''4 \f
                }

            ::

                >>> show(staff) # doctest: +SKIP

            ::

                >>> logical_tie = inspect_(staff[0]).get_logical_tie()
                >>> logical_tie.to_tuplet([2, 1, 1, 1], is_diminution=False)
                FixedDurationTuplet(Duration(3, 16), "c'16 c'32 c'32 c'32")

            ..  doctest::

                >>> print(format(staff))
                \new Staff \with {
                    \override DynamicLineSpanner #'staff-padding = #3
                } {
                    \time 7/16
                    \tweak #'text #tuplet-number::calc-fraction-text
                    \times 6/5 {
                        c'16 \< \p
                        c'32
                        c'32
                        c'32
                    }
                    cqs''4 \f
                }

            ::

                >>> show(staff) # doctest: +SKIP

        Returns tuplet.
        '''
        from abjad.tools import scoretools
        from abjad.tools import mathtools
        from abjad.tools import agenttools
        from abjad.tools import scoretools
        from abjad.tools import spannertools
        from abjad.tools import scoretools

        # coerce input
        proportions = mathtools.Ratio(proportions)

        # find target duration of fixed-duration tuplet
        target_duration = self._preprolated_duration

        # find duration of each note in tuplet
        prolated_duration = target_duration / sum(proportions)

        # find written duration of each notes in tuplet
        if is_diminution:
            if dotted:
                basic_written_duration = \
                    prolated_duration.equal_or_greater_assignable
            else:
                basic_written_duration = \
                    prolated_duration.equal_or_greater_power_of_two
        else:
            if dotted:
                basic_written_duration = \
                    prolated_duration.equal_or_lesser_assignable
            else:
                basic_written_duration = \
                    prolated_duration.equal_or_lesser_power_of_two

        # find written duration of each note in tuplet
        written_durations = [x * basic_written_duration for x in proportions]

        # make tuplet notes
        try:
            notes = [scoretools.Note(0, x) for x in written_durations]
        except AssignabilityError:
            denominator = target_duration._denominator
            note_durations = [durationtools.Duration(x, denominator)
                for x in proportions]
            notes = scoretools.make_notes(0, note_durations)

        # make tuplet
        tuplet = scoretools.FixedDurationTuplet(target_duration, notes)

        # replace logical tie with tuplet
        mutate(self).replace(tuplet)

        # untie tuplet
        for spanner in tuplet._get_spanners(spannertools.Tie):
            spanner._sever_all_components()
        #detach(spannertools.Tie, tuplet)

        # return tuplet
        return tuplet
예제 #17
0
def partition_integer_by_ratio(n, ratio):
    r'''Partitions positive integer-equivalent `n` by `ratio`.

    ::

        >>> mathtools.partition_integer_by_ratio(10, [1, 2])
        [3, 7]

    Partitions positive integer-equivalent `n` by `ratio` with negative parts:

    ::

        >>> mathtools.partition_integer_by_ratio(10, [1, -2])
        [3, -7]

    Partitions negative integer-equivalent `n` by `ratio`:

    ::

        >>> mathtools.partition_integer_by_ratio(-10, [1, 2])
        [-3, -7]

    Partitions negative integer-equivalent `n` by `ratio` with negative parts:

    ::

        >>> mathtools.partition_integer_by_ratio(-10, [1, -2])
        [-3, 7]

    Returns result with weight equal to absolute value of `n`.

    Raises type error on noninteger `n`.

    Returns list of integers.
    '''
    from abjad.tools import mathtools

    if not mathtools.is_integer_equivalent_number(n):
        message = 'is not integer-equivalent number: {!r}.'
        message = message.format(n)
        raise TypeError(message)

    ratio = mathtools.Ratio(ratio).numbers

    if not all(mathtools.is_integer_equivalent_number(part) for part in ratio):
        message = 'some parts in {!r} not integer-equivalent numbers.'
        message = message.format(ratio)
        raise TypeError(message)

    result = [0]

    divisions = [
        float(abs(n)) * abs(part) / mathtools.weight(ratio) for part in ratio
    ]
    cumulative_divisions = mathtools.cumulative_sums(divisions, start=None)

    for division in cumulative_divisions:
        rounded_division = int(round(division)) - sum(result)
        #This makes rounding behave like python 2. Would be good to remove
        # in the long run
        if sys.version_info[0] == 3:
            if division - round(division) == 0.5:
                rounded_division += 1
        result.append(rounded_division)

    result = result[1:]

    # adjust signs of output elements
    if mathtools.sign(n) == -1:
        result = [-x for x in result]
    ratio_signs = [mathtools.sign(x) for x in ratio]
    result = [pair[0] * pair[1] for pair in zip(ratio_signs, result)]

    # return result
    return result
예제 #18
0
    def list_related_tempos(
        self,
        maximum_numerator=None,
        maximum_denominator=None,
        ):
        r'''Lists tempos related to this tempo.

        Returns list of tempo / ratio pairs.

        Each new tempo equals not less than half of this tempo
        and not more than twice this tempo.

        ..  container:: example

            Rewrites tempo ``58`` MM by ratios of the form ``n:d`` such that
            ``1 <= n <= 8`` and ``1 <= d <= 8``:
            ...

            ::

                >>> tempo = Tempo(Duration(1, 4), 58)
                >>> pairs = tempo.list_related_tempos(
                ...     maximum_numerator=8,
                ...     maximum_denominator=8,
                ...     )

            ::

                >>> for tempo, ratio in pairs:
                ...     string = '{!s}\t{!s}'.format(tempo, ratio)
                ...     print(string)
                4=29    1:2
                4=58    1:1
                4=87    3:2
                4=116   2:1

        ..  container:: example

            Rewrites tempo ``58`` MM by ratios of the form ``n:d`` such that
            ``1 <= n <= 30`` and ``1 <= d <= 30``:

            ::

                >>> tempo = Tempo(Duration(1, 4), 58)
                >>> pairs = tempo.list_related_tempos(
                ...     maximum_numerator=30,
                ...     maximum_denominator=30,
                ...     )

            ::

                >>> for tempo, ratio in pairs:
                ...     string = '{!s}\t{!s}'.format(tempo, ratio)
                ...     print(string)
                ... 
                4=30    15:29
                4=32    16:29
                4=34    17:29
                4=36    18:29
                4=38    19:29
                4=40    20:29
                4=42    21:29
                4=44    22:29
                4=46    23:29
                4=48    24:29
                4=50    25:29
                4=52    26:29
                4=54    27:29
                4=56    28:29
                4=58    1:1
                4=60    30:29

        Returns list.
        '''
        # assert integer tempo
        assert isinstance(self.units_per_minute, int), repr(self)
        # find divisors
        divisors = mathtools.divisors(self.units_per_minute)
        if maximum_denominator is not None:
            divisors = [x for x in divisors if x <= maximum_denominator]
        # make pairs
        pairs = []
        for divisor in divisors:
            start = int(math.ceil(divisor / 2.0))
            stop = 2 * divisor
            numerators = range(start, stop + 1)
            if maximum_numerator is not None:
                    numerators = [
                        x for x in numerators
                        if x <= maximum_numerator
                        ]
        for numerator in numerators:
                ratio = mathtools.Ratio(numerator, divisor)
                multiplier = durationtools.Multiplier(*ratio)
                new_units_per_minute = multiplier * self.units_per_minute
                assert mathtools.is_integer_equivalent_expr(
                    new_units_per_minute)
                new_units_per_minute = int(new_units_per_minute)
                new_tempo = type(self)(
                    duration=self.duration, 
                    units_per_minute=new_units_per_minute,
                    )
                pair = (new_tempo, ratio)
                if pair not in pairs:
                    pairs.append(pair)
        # sort pairs
        pairs.sort()
        # return pairs
        return pairs