Source code for thebeat.core.sequence

# Copyright (C) 2022-2023  Jelle van der Werff
#
# This file is part of thebeat.
#
# thebeat is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# thebeat is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with thebeat.  If not, see <https://www.gnu.org/licenses/>.

from __future__ import annotations

import copy
import pathlib
from fractions import Fraction
from typing import Optional, Union
import matplotlib.pyplot as plt
import numpy as np
import numpy.typing as npt
import thebeat.helpers


[docs]class BaseSequence: """This is the most basic of classes that the :py:class:`~thebeat.core.Sequence`, :py:class:`~thebeat.music.Rhythm`, and :py:class:`~thebeat.core.SoundSequence` classes inherit from. It cannot do many things, apart from holding a number of inter-onset intervals (IOIs). The BaseSequence class dictates that a sequence can either end with an interval or not. The default is to end with an event, meaning that if there are *n* onset values (i.e. *t* values), there are *n*-1 IOIs. This is what people will need in most cases. Sequences that end with an interval have an IOI at the end (so they end with a gap of silence). This is what you will need in cases with e.g. rhythmical/musical sequences. The BaseSequence class protects against impossible values for the IOIs, as well as for the event onsets (*t* values). Attributes ---------- iois : NumPy 1-D array Contains the inter-onset intervals (IOIs). This is the bread and butter of the BaseSequence class. Sequences that end with an event have *n* onsets and *n*-1 IOIs. Sequences that end with an interval have an equal number of IOIs and onsets. end_with_interval : bool If ``False``, sequence has *n*-1 inter-onset intervals (IOIs) for *n* event onsets. If ``True``, sequence has an equal number of IOIs and event onsets. name : str If desired, one can give the object a name. This is for instance used when printing the sequence, or when plotting the sequence. It can always be retrieved and changed via this attribute. """
[docs] def __init__(self, iois: npt.ArrayLike[float], first_onset: float = 0.0, end_with_interval: Optional[bool] = False, name: Optional[str] = None): """Initialization of BaseSequence class.""" if end_with_interval is True and first_onset != 0: raise ValueError("First onset must be 0 for sequences that end with an interval.") # Save attributes self.iois = iois self._first_onset = first_onset self.end_with_interval = end_with_interval # Additionally save the provided name (may be None) self.name = name
[docs] def copy(self, deep: bool = False): """Returns a copy of itself. See :py:func:`copy.copy` for more information. Parameters ---------- deep If ``True``, a deep copy is returned. If ``False``, a shallow copy is returned. """ if deep is True: return copy.deepcopy(self) else: return copy.copy(self)
@property def iois(self) -> np.ndarray: """The inter-onset intervals (IOIs) of the Sequence object. These are the intervals in milliseconds between the onset of an event, and the onset of the next event. This is the most important attribute of the Sequence class and is used throughout. This getter returns a copy of the IOIs instead of the actual attribute. """ return np.array(self._iois, dtype=np.float64, copy=True) @iois.setter def iois(self, values: npt.ArrayLike[float]): """IOI setter. Checks against negative (or zero) IOIs.""" # We always want a NumPy array iois = np.array(values, dtype=np.float64, copy=True) if np.any(iois <= 0): raise ValueError("Inter-onset intervals (IOIs) cannot be zero or negative.") self._iois = iois @property def onsets(self) -> np.ndarray: """Returns the event onsets (t values) on the basis of the sequence objects' inter-onset intervals (IOIs). """ if self.end_with_interval is True: return np.cumsum(np.append(self._first_onset, self.iois[:-1])) else: return np.cumsum(np.append(self._first_onset, self.iois)) @onsets.setter def onsets(self, values): """Setter for the event onsets. Onsets must be in order, and there cannot be two simultaneous onsets that occur simultaneously. """ # Set the IOIs if self.end_with_interval is True: raise ValueError("Cannot change onsets of sequences that end with an interval. This is because we need to " "know the final IOI for such sequences. Either reconstruct the sequence, or change the " "IOIs.") values = np.array(values, dtype=np.float64) if np.any(values[:-1] >= values[1:]): raise ValueError("Onsets are not ordered strictly monotonically.") self._iois = np.array(np.diff(values), dtype=np.float64) self._first_onset = float(values[0]) @property def mean_ioi(self) -> np.float64: """The average inter-onset interval (IOI).""" return np.float64(np.mean(self.iois)) @property def duration(self) -> np.float64: """Property that returns the summed total of the inter-onset intervals.""" return np.float64(np.sum(self.iois))
[docs]class Sequence(BaseSequence): """ Arguably, the :py:class:`~thebeat.core.Sequence` class is the most important class in this package. It is used as the basis for many functions as it contains timing information in the form of inter-onset intervals (IOIs; the times between the onset of an event, and the onset of the next event) and event onsets (i.e. *t* values). IOIs are what we use to construct :py:class:`Sequence` objects. The most basic way of constructing a :py:class:`Sequence` object is by passing it a list or array of IOIs (see under :py:meth:`~thebeat.core.Sequence.__init__`). However, the different class methods (e.g. :py:meth:`Sequence.generate_isochronous`) may also be used. For the :py:class:`Sequence` class it does not matter whether the provided IOIs are in seconds or milliseconds. However, it does matter when passing the :py:class:`Sequence` object to e,g, a :py:class:`SoundSequence` object (see :py:meth:`SoundSequence.__init__`). This class additionally contains methods and attributes to, for instance, change the tempo, add Gaussian noise, or to plot the :py:class:`Sequence` object using matplotlib. For more info, check out the :py:meth:`~thebeat.core.Sequence.__init__` method, and the different methods below. """
[docs] def __init__(self, iois: npt.ArrayLike[float], first_onset: float = 0.0, end_with_interval: bool = False, name: Optional[str] = None): """Construct a Sequence class on the basis of inter-onset intervals (IOIs). When ``end_with_interval`` is ``False`` (the default), the sequence contains *n* event onsets, but *n*-1 IOIs. If ``True``, the sequence contains an equal number of event onsets and IOIs. Parameters ---------- iois An iterable of inter-onset intervals (IOIs). For instance: ``[500, 500, 400, 200]``. end_with_interval Indicates whether sequence has an extra final inter-onset interval; this is useful for musical/rhythmical sequences. name Optionally, you can give the Sequence object a name. This is used when printing, plotting, or writing the Sequence object. It can always be retrieved and changed via :py:attr:`BaseSequence.name`. Examples -------- >>> iois = [500, 400, 600, 400] >>> seq = Sequence(iois) >>> print(seq.onsets) [ 0. 500. 900. 1500. 1900.] """ # Call super init method super().__init__(iois=iois, first_onset=first_onset, end_with_interval=end_with_interval, name=name)
def __add__(self, other): if isinstance(other, Sequence) and self.end_with_interval: return Sequence(iois=np.concatenate([self.iois, other.iois]), end_with_interval=other.end_with_interval) elif isinstance(other, (int, float, np.integer, np.float)) and not self.end_with_interval: return Sequence(iois=np.append(self.iois, other), end_with_interval=True, name=self.name) elif isinstance(other, (int, float, np.integer, np.float)) and self.end_with_interval: iois = self.iois iois[-1] += other return Sequence(iois=iois, end_with_interval=True, name=self.name) else: raise ValueError("Can only concatenate sequences that end with an interval, or a sequence and a number." "For instance: 'seq1 + seq2' or 'seq1 + 100'.") def __mul__(self, other: int): return self._repeat(times=other) def __str__(self): name = self.name if self.name else "Not provided" end_with_intervality = "(ends with interval)" if self.end_with_interval else "(ends with event)" return (f"Object of type Sequence {end_with_intervality}\n" f"{len(self.onsets)} events\n" f"IOIs: {self.iois}\n" f"Onsets: {self.onsets}\n" f"Sequence name: {name}\n") def __repr__(self): if self.name: f"Sequence(name={self.name}, iois={np.array2string(self.iois, threshold=8, precision=2)})" return f"Sequence(iois={np.array2string(self.iois, threshold=8, precision=2)})"
[docs] @classmethod def from_integer_ratios(cls, numerators: npt.ArrayLike[float], value_of_one: float, **kwargs) -> Sequence: """ This class method can be used to construct a new :py:class:`Sequence` object on the basis of integer ratios. See :py:attr:`Sequence.integer_ratios` for explanation. Parameters ---------- numerators The numerators of the integer ratios. For instance: ``[1, 2, 4]`` value_of_one This represents the duration of the 1, multiples of this value are used. For instance, a sequence of ``[2, 4]`` using ``value_of_one=500`` would be a :py:class:`Sequence` with IOIs: ``[1000 2000]``. **kwargs Additional keyword arguments are passed to the :py:class:`Sequence` constructor (see :py:meth:`thebeat.core.Sequence.__init__`. Examples -------- >>> seq = Sequence.from_integer_ratios(numerators=[1, 2, 4], value_of_one=500) >>> print(seq.iois) [ 500. 1000. 2000.] """ numerators = np.array(numerators) return cls(numerators * value_of_one, **kwargs)
[docs] @classmethod def from_onsets(cls, onsets: Union[np.ndarray[float], list[float]], **kwargs) -> Sequence: """ Class method that can be used to generate a new :py:class:`Sequence` object on the basis of event onsets. Here, the onsets do not have to start with zero. Parameters ---------- onsets An array or list containg event onsets, for instance: ``[0, 500, 1000]``. The onsets do not have to start with zero. **kwargs Additional keyword arguments are passed to the :py:class:`Sequence` constructor (excluding ``first_onset`` and ``end_with_interval``, which are set by this method). Examples -------- >>> seq = Sequence.from_onsets([0, 500, 1000]) >>> print(seq.iois) [500. 500.] """ iois = np.diff(onsets) return cls(iois, first_onset=onsets[0], end_with_interval=False, **kwargs)
[docs] @classmethod def from_txt(cls, filepath: Union[str, pathlib.Path], type: str = "iois", end_with_interval: bool = False, **kwargs) -> Sequence: """ Class method that can be used to generate a new :py:class:`Sequence` object from a text file. The text file is assumed to contain one IOI/onset per line. Parameters ---------- filepath The path to the text file. type The type of the sequence. Can be either ``iois`` or ``onsets``. end_with_interval Indicates whether the sequence should end with an event (``False``) or an interval (``True``). **kwargs Additional keyword arguments are passed to the :py:class:`Sequence` constructor. """ with open(filepath, "r") as f: data = f.readlines() if type == "iois": return cls(iois=np.array(data, dtype=np.float64), end_with_interval=end_with_interval, **kwargs) elif type == "onsets": return cls.from_onsets(onsets=np.array(data, dtype=np.float64), **kwargs) else: raise ValueError("type can only be 'iois' or 'onsets'.")
[docs] @classmethod def generate_isochronous(cls, n_events: int, ioi: float, end_with_interval: bool = False, **kwargs) -> Sequence: """ Class method that generates a sequence of isochronous (i.e. equidistant) inter-onset intervals. Note that there will be *n*-1 IOIs in a sequence. IOIs are rounded off to integers. Parameters ---------- n_events The desired number of events in the sequence. ioi The inter-onset interval to be used between all events. end_with_interval Indicates whether the sequence should end with an event (``False``) or an interval (``True``). **kwargs Additional keyword arguments are passed to the :py:class:`Sequence` constructor. Examples -------- >>> seq = Sequence.generate_isochronous(n_events=5,ioi=500) >>> print(seq.iois) [500. 500. 500. 500.] >>> print(len(seq.onsets)) 5 >>> print(len(seq.iois)) 4 >>> seq = Sequence.generate_isochronous(n_events=5,ioi=500,end_with_interval=True) >>> print(len(seq.onsets)) 5 >>> print(len(seq.iois)) 5 """ # Number of IOIs depends on end_with_interval argument n_iois = n_events if end_with_interval else n_events - 1 return cls([ioi] * n_iois, end_with_interval=end_with_interval, **kwargs)
[docs] @classmethod def generate_random_normal(cls, n_events: int, mu: float, sigma: float, rng: Optional[np.random.Generator] = None, end_with_interval: bool = False, **kwargs) -> Sequence: """ Class method that generates a :py:class:`Sequence` object with random inter-onset intervals (IOIs) based on the normal distribution. Parameters ---------- n_events The desired number of events in the sequence. mu The mean of the normal distribution. sigma The standard deviation of the normal distribution. rng A :class:`numpy.random.Generator` object. If not supplied :func:`numpy.random.default_rng` is used. end_with_interval Indicates whether the sequence should end with an event (``False``) or an interval (``True``). **kwargs Additional keyword arguments are passed to the :py:class:`Sequence` constructor. Examples -------- >>> generator = np.random.default_rng(seed=123) >>> seq = Sequence.generate_random_normal(n_events=5,mu=500,sigma=50,rng=generator) >>> print(seq.iois) [450.54393248 481.61066743 564.39626306 509.69872096] >>> seq = Sequence.generate_random_normal(n_events=5,mu=500,sigma=50,end_with_interval=True) >>> len(seq.onsets) == len(seq.iois) True """ if rng is None: rng = np.random.default_rng() # Number of IOIs depends on end_with_intervality n_iois = n_events if end_with_interval else n_events - 1 return cls(rng.normal(loc=mu, scale=sigma, size=n_iois), end_with_interval=end_with_interval, **kwargs)
[docs] @classmethod def generate_random_uniform(cls, n_events: int, a: float, b: float, rng: Optional[np.random.Generator] = None, end_with_interval: bool = False, **kwargs) -> Sequence: """ Class method that generates a :py:class:`Sequence` object with random inter-onset intervals (IOIs) based on a uniform distribution. Parameters ---------- n_events The desired number of events in the sequence. a The left bound of the uniform distribution. b The right bound of the uniform distribution. rng A :class:`numpy.random.Generator` object. If not supplied :func:`numpy.random.default_rng` is used. end_with_interval Indicates whether the sequence should end with an event (``False``) or an interval (``True``). **kwargs Additional keyword arguments are passed to the :py:class:`Sequence` constructor. Examples -------- >>> generator = np.random.default_rng(seed=123) >>> seq = Sequence.generate_random_uniform(n_events=5,a=400,b=600,rng=generator) >>> print(seq.iois) [536.47037265 410.76420376 444.07197455 436.87436214] >>> seq = Sequence.generate_random_uniform(n_events=5,a=400,b=600,end_with_interval=True) >>> len(seq.onsets) == len(seq.iois) True """ if rng is None: rng = np.random.default_rng() # Number of IOIs depends on end_with_interval argument n_iois = n_events if end_with_interval else n_events - 1 iois = rng.uniform(low=a, high=b, size=n_iois) return cls(iois, end_with_interval=end_with_interval, **kwargs)
[docs] @classmethod def generate_random_poisson(cls, n_events: int, lam: float, rng: Optional[np.random.Generator] = None, end_with_interval: bool = False, **kwargs) -> Sequence: """ Class method that generates a :py:class:`Sequence` object with random inter-onset intervals (IOIs) based on a Poisson distribution. Parameters ---------- n_events The desired number of events in the sequence. lam The desired value for lambda. rng A :class:`numpy.random.Generator` object. If not supplied :func:`numpy.random.default_rng` is used. end_with_interval Indicates whether the sequence should end with an event (``False``) or an interval (``True``). **kwargs Additional keyword arguments are passed to the :py:class:`Sequence` constructor. Examples -------- >>> generator = np.random.default_rng(123) >>> seq = Sequence.generate_random_poisson(n_events=5,lam=500,rng=generator) >>> print(seq.iois) [512. 480. 476. 539.] """ if rng is None: rng = np.random.default_rng() # Number of IOIs depends on end_with_interval argument n_iois = n_events if end_with_interval else n_events - 1 return cls(rng.poisson(lam=lam, size=n_iois), end_with_interval=end_with_interval, **kwargs)
[docs] @classmethod def generate_random_exponential(cls, n_events: int, lam: float, rng: Optional[np.random.Generator] = None, end_with_interval: bool = False, **kwargs) -> Sequence: """Class method that generates a :py:class:`Sequence` object with random inter-onset intervals (IOIs) based on an exponential distribution. Parameters ---------- n_events The desired number of events in the sequence. lam The desired value for lambda. rng A :class:`numpy.random.Generator` object. If not supplied NumPy's :func:`numpy.random.default_rng` is used. end_with_interval Indicates whether the sequence should end with an event (``False``) or an interval (``True``). **kwargs Additional keyword arguments are passed to the :py:class:`Sequence` constructor. Examples -------- >>> generator = np.random.default_rng(seed=123) >>> seq = Sequence.generate_random_exponential(n_events=5,lam=500,rng=generator) >>> print(seq.iois) [298.48624756 58.51553052 125.89734975 153.98272273] """ if rng is None: rng = np.random.default_rng() n_iois = n_events if end_with_interval else n_events - 1 return cls(rng.exponential(scale=lam, size=n_iois), end_with_interval=end_with_interval, **kwargs)
[docs] def merge(self, other: Union[thebeat.core.Sequence, list[thebeat.core.Sequence]]): """ Merge this :py:class:`Sequence` object with one or multiple other :py:class:`Sequence` objects. Returns a new :py:class:`Sequence` object. Parameters ---------- other A :py:class:`Sequence` object, or a list of :py:class:`Sequence` objects. Returns ------- object A :py:class:`Sequence` object. """ if isinstance(other, thebeat.Sequence): return thebeat.utils.merge_sequences([self, other]) return thebeat.utils.merge_sequences([self, *other])
# Manipulation methods
[docs] def add_noise_gaussian(self, noise_sd: float, rng: Optional[np.random.Generator] = None): """This method can be used to add some Gaussian noise to the inter-onset intervals (IOIs) of the Sequence object. It uses a normal distribution with mean 0, and a standard deviation of ``noise_sd``. Parameters ---------- noise_sd The standard deviation of the normal distribution used for adding in noise. rng A Numpy Generator object. If none is supplied, :func:`numpy.random.default_rng` is used. Examples -------- >>> gen = np.random.default_rng(seed=123) >>> seq = Sequence.generate_isochronous(n_events=5,ioi=500) >>> print(seq.iois) [500. 500. 500. 500.] >>> seq.add_noise_gaussian(noise_sd=50, rng=gen) >>> print(seq.iois) [450.54393248 481.61066743 564.39626306 509.69872096] >>> print(seq.onsets) [ 0. 450.54393248 932.15459991 1496.55086297 2006.24958393] """ if rng is None: rng = np.random.default_rng() self.iois += rng.normal(loc=0, scale=noise_sd, size=len(self.iois))
[docs] def change_tempo(self, factor: float) -> None: """Change the tempo of the `Sequence` object, where a factor of 1 or bigger increases the tempo (resulting in smaller inter-onset intervals). A factor between 0 and 1 decreases the tempo (resulting in larger inter-onset intervals). Parameters ---------- factor Tempo change factor. E.g. 2 means twice as fast. 0.5 means twice as slow. Examples -------- >>> seq = Sequence.generate_isochronous(n_events=5, ioi=500) >>> print(seq.onsets) [ 0. 500. 1000. 1500. 2000.] >>> seq.change_tempo(2) >>> print(seq.onsets) [ 0. 250. 500. 750. 1000.] """ if factor > 0: self.iois /= factor else: raise ValueError("Please provide a factor larger than 0.")
[docs] def change_tempo_linearly(self, total_change: float): """Create a ritardando or accelerando effect in the inter-onset intervals (IOIs). It divides the IOIs by a vector linearly spaced between 1 and ``total_change``. Parameters ---------- total_change Total tempo change at the end of the :py:class:`Sequence` compared to the beginning. So, a total change of 2 (accelerando) results in a final IOI that is twice as short as the first IOI. A total change of 0.5 (ritardando) results in a final IOI that is twice as long as the first IOI. Examples -------- >>> seq = Sequence.generate_isochronous(n_events=5,ioi=500) >>> print(seq.iois) [500. 500. 500. 500.] >>> seq.change_tempo_linearly(total_change=2) >>> print(seq.iois) [500. 375. 300. 250.] """ self.iois /= np.linspace(start=1, stop=total_change, num=len(self.iois))
[docs] def round_onsets(self, decimals: int = 0): """Use this function to round off the :py:class:`Sequence` object's onsets (i.e. *t* values). This can, for instance, be useful to get rid of warnings that are the result of frame rounding. See e.g. :py:class:`SoundSequence`. Note that this function does not return anything. The onsets of the sequence object from which this method is called are rounded. Parameters ---------- decimals The number of decimals desired. """ self.onsets = np.round(self.onsets, decimals=decimals)
[docs] def quantize(self, to: float): """Quantize the Sequence object's onsets (i.e. *t* values) to a certain bin size. Note ---- This function does not return anything. Instead, the onsets of the sequence object itself are changed. Parameters ---------- to The value to be quantized to. E.g. a value of ``100`` means that the onsets will be quantized to the nearest multiple of 100. Examples -------- >>> seq = Sequence(iois=[235, 510, 420, 99]) >>> print(seq.onsets) [ 0. 235. 745. 1165. 1264.] >>> seq.quantize(to=100) >>> print(seq.onsets) [ 0. 200. 700. 1200. 1300.] """ self.onsets = np.round(self.onsets / to) * to
# Visualization
[docs] def plot_sequence(self, linewidth: Optional[Union[npt.ArrayLike[float], float]] = None, **kwargs) -> tuple[plt.Figure, plt.Axes]: """ Plot the :py:class:`Sequence` object as an event plot on the basis of the event onsets. The lines' left boundaries are placed at the event onsets. Parameters ---------- linewidth The desired width of the bars (events). Defaults to 1/10th of the smallest inter-onset interval (IOI). Can be a single value that will be used for each onset, or a list or array of values (i.e with a value for each respective onsets). **kwargs Additional parameters (e.g. 'title', 'dpi' etc.) are passed to :py:func:`thebeat.helpers.plot_single_sequence`. Examples -------- >>> seq = Sequence.generate_isochronous(n_events=5,ioi=500) >>> seq.plot_sequence() # doctest: +SKIP In this example, we plot onto an existing :class:`~matplotlib.pyplot.Axes` object. >>> import matplotlib.pyplot as plt >>> seq = Sequence([500, 200, 1000]) >>> fig, axs = plt.subplots(nrows=1, ncols=2) >>> seq.plot_sequence(ax=axs[0]) # doctest: +SKIP """ # For the title, use the Sequence name if it has one. Otherwise use the title parameter, # which may be None. if self.name and kwargs.get('title') is None: kwargs.get('title', self.name) # Linewidths if linewidth is None: linewidths = np.repeat(np.min(self.iois) / 10, len(self.onsets)) elif isinstance(linewidth, (int, float, np.integer, np.float)): linewidths = np.repeat(linewidth, len(self.onsets)) else: linewidths = np.array(linewidth) # If the sequence is end_with_interval we also want to plot the final ioi final_ioi = self.iois[-1] if self.end_with_interval else None # Plot the sequence fig, ax = thebeat.helpers.plot_single_sequence(onsets=self.onsets, end_with_interval=self.end_with_interval, final_ioi=final_ioi, linewidths=linewidths, **kwargs) return fig, ax
@property def integer_ratios(self) -> np.ndarray: r"""Calculate how to describe a sequence of IOIs in integer ratio numerators from the total duration of the sequence by finding the least common multiplier. Example ------- A sequence of IOIs ``[250, 500, 1000, 250]`` has a total duration of 2000. This can be described using the least common multiplier as :math:`\frac{1}{8}, \frac{2}{8}, \frac{4}{8}, \frac{1}{8}`, so this method returns the numerators ``[1, 2, 4, 1]``. Notes ----- The method for calculating the integer ratios is based on :cite:t:`jacobyIntegerRatioPriors2017`. Examples -------- >>> seq = Sequence([250, 500, 1000, 250]) >>> print(seq.integer_ratios) [1 2 4 1] """ fractions = [Fraction(int(ioi), int(self.duration)) for ioi in self.iois] lcm = np.lcm.reduce([fr.denominator for fr in fractions]) vals = [int(fr.numerator * lcm / fr.denominator) for fr in fractions] return np.array(vals) @property def interval_ratios_from_dyads(self) -> np.ndarray: r"""Return sequential interval ratios, calculated as: :math:`\textrm{ratio}_k = \frac{\textrm{IOI}_k}{\textrm{IOI}_k + \textrm{IOI}_{k+1}}`. Note that for *n* IOIs this property returns *n*-1 ratios. Notes ----- The used method is based on the methodology from :cite:t:`roeskeCategoricalRhythmsAre2020`. Examples -------- >>> seq = Sequence.from_integer_ratios([2, 2, 1, 1], value_of_one=500) >>> print(seq.iois) [1000. 1000. 500. 500.] >>> print(seq.interval_ratios_from_dyads) [0.5 0.66666667 0.5 ] """ iois = self.iois return thebeat.utils.get_interval_ratios_from_dyads(iois) def _repeat(self, times: int) -> Sequence: """ Repeat the inter-onset intervals (IOIs) ``times`` times. Returns a new Sequence instance. Only works for Sequences that end with an interval! Otherwise, we do not know what the IOI is between the offset of the final event of the original sequence, and the onset of the first sound in the repeated sequence. Parameters ---------- times The number of times the inter-onset intervals should be repeated. """ if not isinstance(times, int): raise TypeError("You can only multiply Sequence objects by integers.") if not self.end_with_interval or not self.onsets[0] == 0: raise ValueError( "You can only repeat Sequences that end with an interval that additionally have first_onset == 0.0. " "Try adding the end_with_interval=True flag when creating this object.") new_iois = np.tile(self.iois, reps=times) return Sequence(iois=new_iois, first_onset=0.0, end_with_interval=True, name=self.name)