Source code for mirdata.annotations

"""mirdata annotation data types"""

import logging
import re
from typing import List, Optional, Tuple

from deprecated.sphinx import deprecated
import librosa
import numpy as np
import scipy

# Regex pattern needed to validate chords and keys
KEY_MODE_PATTERN = r"^N|([A-G][b#]?)(:(major|minor|ionian|dorian|phrygian|lydian|mixolydian|aeolian|locrian))?$"
HARTE_CHORD_PATTERN = r"^((N)|(([A-G][b#]*)((:(maj|min|dim|aug|maj7|min7|7|dim7|hdim7|minmaj7|maj6|min6|9|maj9|min9|sus4)(\((\*?([b#]*([1-9]|1[0-3]?))(,\*?([b#]*([1-9]|1[0-3]?)))*)\))?)|(:\((\*?([b#]*([1-9]|1[0-3]?))(,\*?([b#]*([1-9]|1[0-3]?)))*)\)))?((/([b#]*([1-9]|1[0-3]?)))?)?))$"
JAMS_CHORD_PATTERN = r"^((N|X)|(([A-G](b*|#*))((:(maj|min|dim|aug|1|5|sus2|sus4|maj6|min6|7|maj7|min7|dim7|hdim7|minmaj7|aug7|9|maj9|min9|11|maj11|min11|13|maj13|min13)(\((\*?((b*|#*)([1-9]|1[0-3]?))(,\*?((b*|#*)([1-9]|1[0-3]?)))*)\))?)|(:\((\*?((b*|#*)([1-9]|1[0-3]?))(,\*?((b*|#*)([1-9]|1[0-3]?)))*)\)))?((/((b*|#*)([1-9]|1[0-3]?)))?)?))$"

#: Beat position units
BEAT_POSITION_UNITS = {
    "bar_index": "beat index within a bar, 1-indexed",
    "global_index": "beat index within full track, 1-indexed",
    "bar_fraction": "beat position as fractions of bars, e.g. 0.25",
    "global_fraction": "bar_frac, but where the integer part indicates the bar. e.g. 4.25",
}

#: Chord units
CHORD_UNITS = {
    "harte": "chords in harte format, e.g. Ab:maj7",
    "jams": "chords in jams 'chord' format",
    "open": "no strict schema or units",
}

#: Amplitude/voicing units
AMPLITUDE_UNITS = {
    "likelihood": "score between 0 and 1",
    "velocity": "MIDI velocity between 0 and 127",
    "binary": "0 or 1",
    "energy": "energy value, measured as the sum of a squared signal",
}

#: Event units
EVENT_UNITS = {"open": "no scrict schema or units"}

#: Key units
KEY_UNITS = {"key_mode": "key labels in key-mode format, e.g. G#:minor"}

#: Lyric units
LYRIC_UNITS = {
    "words": "lyrics as words or phrases",
    "syllable_open": "lyrics segmented by syllable, no strict schema",
    "pronunciations_open": "lyric pronunciations, no strict schema",
}

#: Pitch units
PITCH_UNITS = {
    "hz": "hertz",
    "midi": "MIDI note number",
    "pc": "pitch class, e.g. G#",
    "note_name": "pc with octave, e.g. Ab4",
}

#: Section units
SECTION_UNITS = {"open": "no scrict schema or units"}

#: Tempo units
TEMPO_UNITS = {"bpm": "beats per minute"}

#: Time units
TIME_UNITS = {"s": "seconds", "ms": "miliseconds", "ticks": "MIDI ticks"}

#: Voicing units
VOICING_UNITS = {k: AMPLITUDE_UNITS[k] for k in ["binary", "likelihood"]}


[docs] class Annotation(object): """Annotation base class""" def __repr__(self): attributes = [v for v in dir(self) if not v.startswith("_")] repr_str = f"{self.__class__.__name__}({', '.join(attributes)})" return repr_str
[docs] class MultiAnnotator(object): """Multiple annotator class. This class should be used for datasets with multiple annotators (e.g. multiple annotators per track). Attributes: annotators (list): list with annotator ids annotations (list): list of annotations (e.g. [beat_data1, beat_data2] each with type BeatData or [chord_data1, chord_data2] each with type chord data """ def __init__(self, annotators, annotations, dtype) -> None: validate_array_like(annotators, list, str, none_allowed=True) validate_array_like(annotations, list, dtype, none_allowed=True) validate_lengths_equal([annotators, annotations]) self.annotators = annotators self.annotations = annotations
[docs] class BeatData(Annotation): """BeatData class Attributes: times (np.ndarray): array of time stamps with positive, strictly increasing values time_unit (str): time unit, one of TIME_UNITS positions (np.ndarray): array of beat positions in the format of position_unit. For all units, values of 0 indicate beats which fall outside of a measure. position_unit (str): beat position unit, one of BEAT_POSITION_UNITS confidence (np.ndarray): array of confidence values confidence_unit (str): confidence unit, one of AMPLITUDE_UNITS """ def __init__( self, times, time_unit, positions, position_unit, confidence=None, confidence_unit=None, ): validate_array_like(times, np.ndarray, float) validate_lengths_equal([times, positions]) validate_times(times, time_unit) validate_beat_positions(positions, position_unit) validate_confidence(confidence, confidence_unit) self.times = times self.time_unit = time_unit self.positions = positions self.position_unit = position_unit self.confidence = confidence self.confidence_unit = confidence_unit
[docs] class SectionData(Annotation): """SectionData class Attributes: intervals (np.ndarray): (n x 2) array of intervals in the form [start_time, end_time]. Times should be positive and intervals should have non-negative duration interval_unit (str): unit of the time values in intervals. One of TIME_UNITS. labels (list or None): list of section labels label_unit (str or None): label unit, one of SECTION_UNITS """ def __init__(self, intervals, interval_unit, labels=None, label_unit=None): validate_array_like(intervals, np.ndarray, float) validate_array_like(labels, list, str, none_allowed=True) validate_lengths_equal([intervals, labels]) validate_intervals(intervals, interval_unit) validate_unit(label_unit, SECTION_UNITS, allow_none=True) self.intervals = intervals self.interval_unit = interval_unit self.labels = labels self.label_unit = label_unit
[docs] class ChordData(Annotation): """ChordData class Attributes: intervals (np.ndarray): (n x 2) array of intervals in the form [start_time, end_time]. Times should be positive and intervals should have non-negative duration interval_unit (str): unit of the time values in intervals. One of TIME_UNITS. labels (list): list chord labels (as strings) label_unit (str): chord label schema confidence (np.ndarray or None): array of confidence values confidence_unit (str or None): confidence unit, one of AMPLITUDE_UNITS """ def __init__( self, intervals, interval_unit, labels, label_unit, confidence=None, confidence_unit=None, ): validate_array_like(intervals, np.ndarray, float) validate_array_like(labels, list, str) validate_array_like(confidence, np.ndarray, float, none_allowed=True) validate_lengths_equal([intervals, labels, confidence]) validate_intervals(intervals, interval_unit) validate_unit(label_unit, CHORD_UNITS) validate_chord_labels(labels, label_unit) validate_confidence(confidence, confidence_unit) self.intervals = intervals self.labels = labels self.confidence = confidence
[docs] class GestureData(Annotation): """GestureData Class Attributes: keypoints (np.ndarray): scores (np.ndarray): """ def __init__(self, keypoints, scores): validate_array_like(keypoints, np.ndarray, np.float32) validate_array_like(scores, np.ndarray, np.float32) self.keypoints = keypoints self.scores = scores
[docs] class F0Data(Annotation): """F0Data class Attributes: times (np.ndarray): array of time stamps (as floats) with positive, strictly increasing values time_unit (str): time unit, one of TIME_UNITS frequencies (np.ndarray): array of frequency values (as floats) frequency_unit (str): frequency unit, one of PITCH_UNITS voicing (np.ndarray): array of voicing values, indicating whether or not a time frame has an active pitch voicing_unit (str): voicing unit, one of VOICING_UNITS confidence (np.ndarray or None): array of confidence values confidence_unit (str or None): confidence unit, one of AMPLITUDE_UNITS """ def __init__( self, times, time_unit, frequencies, frequency_unit, voicing, voicing_unit, confidence=None, confidence_unit=None, ): validate_array_like(times, np.ndarray, float) if frequency_unit in ["note_name", "pc"]: validate_array_like(frequencies, np.ndarray, None) else: validate_array_like(frequencies, np.ndarray, float) validate_array_like(voicing, np.ndarray, float) validate_array_like(confidence, np.ndarray, float, none_allowed=True) validate_lengths_equal([times, frequencies, voicing, confidence]) validate_times(times, time_unit) validate_uniform_times(times) validate_pitches(frequencies, frequency_unit) validate_voicing(voicing, voicing_unit) validate_confidence(confidence, confidence_unit) if any(voicing[frequencies == 0] != 0): raise ValueError("Found frequencies with value 0, but a nonzero voicing.") self.times = times self.time_unit = time_unit self.frequencies = frequencies self.frequency_unit = frequency_unit self.voicing = voicing self.voicing_unit = voicing_unit self._confidence = confidence self.confidence_unit = confidence_unit @property def confidence(self): logging.warning( "Warning: the API for annotations.F0Data.confidence has changed. " + "For most datasets, confidence will now be None, and " + "F0Data.voicing should be used instead." ) return self._confidence
[docs] def resample(self, times_new, times_new_unit): """Resample the annotation to a new time scale. This function is adapted from: https://github.com/craffel/mir_eval/blob/master/mir_eval/melody.py#L212 Args: times_new (np.ndarray): new time base, in units of times_new_unit times_new_unit (str): time unit, one of TIME_UNITS Returns: F0Data: F0 data sampled at new time scale """ times = convert_time_units(self.times, self.time_unit, times_new_unit) if self.frequency_unit not in ["hz", "midi"]: raise NotImplementedError( "resampling is not supported for {}".format(self.frequency_unit) ) frequencies = self.frequencies voicing = self.voicing confidence = self._confidence # We need to fix zero transitions # Fill in zero values with the last reported frequency # to avoid erroneous values when resampling frequencies_held = np.array(frequencies) for n, frequency in enumerate(frequencies[1:]): if frequency == 0: frequencies_held[n + 1] = frequencies_held[n] # Linearly interpolate frequencies frequencies_resampled = scipy.interpolate.interp1d( times, frequencies_held, "linear", bounds_error=False, fill_value=0.0 )(times_new) # Retain zeros frequency_mask = scipy.interpolate.interp1d( times, frequencies, "zero", bounds_error=False, fill_value=0 )(times_new) frequencies_resampled *= frequency_mask != 0 # Use nearest-neighbor for voicing if it was used for frequencies # if voicing is not binary, use linear interpolation if self.voicing_unit != "binary": voicing_resampled = scipy.interpolate.interp1d( times, voicing, "linear", bounds_error=False, fill_value=0 )(times_new) else: voicing_resampled = scipy.interpolate.interp1d( times, voicing, "nearest", bounds_error=False, fill_value=0 )(times_new) voicing_resampled[frequencies_resampled == 0] = 0 if confidence is None: confidence_resampled = None # binary confidence elif self.confidence_unit == "binary": confidence_resampled = scipy.interpolate.interp1d( times, confidence, "nearest", bounds_error=False, fill_value=0 )(times_new) # nonbinary confidence else: confidence_resampled = scipy.interpolate.interp1d( times, confidence, "linear", bounds_error=False, fill_value=0 )(times_new) return F0Data( times_new, times_new_unit, frequencies_resampled, self.frequency_unit, voicing_resampled, self.voicing_unit, confidence_resampled, self.confidence_unit, )
[docs] def to_sparse_index( self, time_scale, time_scale_unit, frequency_scale, frequency_scale_unit, amplitude_unit="binary", ): """ Convert F0 annotation to sparse matrix indices for a time-frequency matrix. Args: time_scale (np.array): times in units time_unit time_scale_unit (str): time scale units, one of TIME_UNITS frequency_scale (np.array): frequencies in frequency_unit frequency_scale_unit (str): frequency scale units, one of PITCH_UNITS amplitude_unit (str): amplitude units, one of AMPLITUDE_UNITS Defaults to "binary". Returns: * sparse_index (np.ndarray): Array of sparce indices [(time_index, frequency_index)] * amplitude (np.ndarray): Array of amplitude values for each index """ f0dat = self.resample(time_scale, time_scale_unit) frequencies = convert_pitch_units( f0dat.frequencies, self.frequency_unit, frequency_scale_unit ) # get indexes in matrix nonzero_freqs = frequencies > 0 # find indexes for frequencies not equal to 0 frequencies[frequencies == 0] = 1 # change zero frequency value to avoid NaN time_indexes = np.arange(len(time_scale)) freq_indexes = closest_index( np.log(frequencies)[:, np.newaxis], np.log(frequency_scale)[:, np.newaxis] ) # create sparse index index = [ (t, f) for t, f in zip(time_indexes[nonzero_freqs], freq_indexes[nonzero_freqs]) if t != -1 and f != -1 ] voicing = np.array( [ v for (v, t, f) in zip( f0dat.voicing[nonzero_freqs], time_indexes[nonzero_freqs], freq_indexes[nonzero_freqs], ) if t != -1 and f != -1 ] ) return ( np.array(index), convert_amplitude_units(voicing, self.voicing_unit, amplitude_unit), )
[docs] def to_matrix( self, time_scale, time_scale_unit, frequency_scale, frequency_scale_unit, amplitude_unit="binary", ): """Convert f0 data to a matrix (piano roll) defined by a time and frequency scale Args: time_scale (np.array): times in units time_unit time_scale_unit (str): time scale units, one of TIME_UNITS frequency_scale (np.array): frequencies in frequency_unit frequency_scale_unit (str): frequency scale units, one of PITCH_UNITS amplitude_unit (str): amplitude units, one of AMPLITUDE_UNITS Defaults to "binary". Returns: np.ndarray: 2D matrix of shape len(time_scale) x len(frequency_scale) """ index, voicing = self.to_sparse_index( time_scale, time_scale_unit, frequency_scale, frequency_scale_unit, amplitude_unit, ) matrix = np.zeros((len(time_scale), len(frequency_scale))) matrix[index[:, 0], index[:, 1]] = voicing return matrix
[docs] def to_multif0(self): """Convert annotation to multif0 format Returns: MultiF0Data: data in multif0 format """ frequency_list = [[f] if f > 0 else [] for f in self.frequencies] confidence_list = ( None if self._confidence is None else [ [c] if f > 0 else [] for c, f in zip(self._confidence, self.frequencies) ] ) return MultiF0Data( self.times, self.time_unit, frequency_list, self.frequency_unit, confidence_list, self.confidence_unit, )
[docs] def to_mir_eval(self): """Convert units and format to what is expected by mir_eval.melody.evaluate Returns: * times (np.ndarray) - uniformly spaced times in seconds * frequencies (np.ndarray) - frequency values in hz * voicing (np.ndarray) - voicings, as likelihood values """ times = convert_time_units(self.times, self.time_unit, "s") frequencies = convert_pitch_units(self.frequencies, self.frequency_unit, "hz") voicing = convert_amplitude_units(self.voicing, self.voicing_unit, "likelihood") return times, frequencies, voicing
[docs] class MultiF0Data(Annotation): """MultiF0Data class Attributes: times (np.ndarray): array of time stamps (as floats) with positive, strictly increasing values time_unit (str): time unit, one of TIME_UNITS frequency_list (list): list of lists of frequency values (as floats) frequency_unit (str): frequency unit, one of PITCH_UNITS confidence_list (np.ndarray or None): list of lists of confidence values confidence_unit (str or None): confidence unit, one of AMPLITUDE_UNITS """ def __init__( self, times, time_unit, frequency_list, frequency_unit, confidence_list=None, confidence_unit=None, ): validate_array_like(times, np.ndarray, float) validate_array_like(frequency_list, list, list) validate_array_like(confidence_list, list, list, none_allowed=True) validate_lengths_equal([times, frequency_list, confidence_list]) validate_times(times, time_unit) validate_uniform_times(times) validate_pitches(frequency_list, frequency_unit) validate_confidence(confidence_list, confidence_unit) self.times = times self.time_unit = time_unit self.frequency_list = frequency_list self.frequency_unit = frequency_unit self.confidence_list = confidence_list self.confidence_unit = confidence_unit self._remove_duplicates() def _remove_duplicates(self): new_frequency_list = [] new_confidence_list = [] confidence_list = ( [[0 for _ in flist] for flist in self.frequency_list] if self.confidence_list is None else self.confidence_list ) for flist, clist in zip(self.frequency_list, confidence_list): tmp_flist = [] tmp_clist = [] for f, c in zip(flist, clist): if f in tmp_flist: continue tmp_flist.append(f) tmp_clist.append(c) new_frequency_list.append(tmp_flist) new_confidence_list.append(tmp_clist) self.frequency_list = new_frequency_list self.confidence_list = ( None if self.confidence_list is None else new_confidence_list ) def __add__(self, other): if other is None: return self if isinstance(other, F0Data): other = other.to_multif0() if not isinstance(other, MultiF0Data): raise TypeError("Unable to add type {} to MultiF0 data".format(type(other))) other_times = convert_time_units(other.times, other.time_unit, self.time_unit) if np.max(other_times) > np.max(self.times): data_resamp = self.resample(other_times, self.time_unit) times = other_times this_data = data_resamp other_data = other else: other_resamp = other.resample(self.times, self.time_unit) times = self.times this_data = self other_data = other_resamp this_frequency_list = [[f for f in flist] for flist in this_data.frequency_list] other_frequency_list = convert_pitch_units( other_data.frequency_list, other.frequency_unit, self.frequency_unit ) for i, flist in enumerate(other_frequency_list): this_frequency_list[i].extend(flist) this_has_confidence = this_data.confidence_list is not None other_has_confidence = other_data.confidence_unit is not None this_confidence_unit = this_data.confidence_unit if this_has_confidence and other_has_confidence: this_confidence_list = [ [c for c in clist] for clist in this_data.confidence_list ] other_confidence_list = convert_amplitude_units( other_data.confidence_list, other.confidence_unit, self.confidence_unit ) for i, clist in enumerate(other_confidence_list): this_confidence_list[i].extend(clist) elif not this_has_confidence and not other_has_confidence: this_confidence_list = None else: logging.warning( "Adding two MultiF0Data where one has confidence=None " + "and the other does not. The sum will have confidence=None." ) this_confidence_list = None this_confidence_unit = None return MultiF0Data( times, self.time_unit, this_frequency_list, self.frequency_unit, this_confidence_list, this_confidence_unit, )
[docs] def resample(self, times_new, times_new_unit): """Resample annotation to a new time scale. This function is adapted from: https://github.com/craffel/mir_eval/blob/master/mir_eval/multipitch.py#L104 Args: times_new (np.array): array of new time scale values times_new_unit (str): units for new time scale, one of TIME_UNITS Returns: MultiF0Data: the resampled annotation """ times = convert_time_units(self.times, self.time_unit, times_new_unit) n_times = len(self.times) # scipy's interpolate doesn't handle ragged arrays. Instead, we interpolate # the frequency index and then map back to the frequency values. # This only works because we're using a nearest neighbor interpolator! frequency_index = np.arange(0, n_times) # times are already ordered so assume_sorted=True for efficiency # since we're interpolating the index, fill_value is set to the first index # that is out of range. We handle this in the next line. new_frequency_index = scipy.interpolate.interp1d( times, frequency_index, kind="nearest", bounds_error=False, assume_sorted=True, fill_value=n_times, )(times_new) # create array of frequencies plus additional empty element at the end for # target time stamps that are out of the interpolation range freq_vals = self.frequency_list + [[]] # map interpolated indices back to frequency values frequencies_resampled = [freq_vals[i] for i in new_frequency_index.astype(int)] if self.confidence_list is not None: confidence_vals = self.confidence_list + [[]] confidence_resampled = [ confidence_vals[i] for i in new_frequency_index.astype(int) ] else: confidence_resampled = None return MultiF0Data( times_new, times_new_unit, frequencies_resampled, self.frequency_unit, confidence_resampled, self.confidence_unit, )
[docs] def to_sparse_index( self, time_scale, time_scale_unit, frequency_scale, frequency_scale_unit, amplitude_unit="binary", ): """ Convert MultiF0 annotation to sparse matrix indices for a time-frequency matrix. Args: time_scale (np.array): times in units time_unit time_scale_unit (str): time scale units, one of TIME_UNITS frequency_scale (np.array): frequencies in frequency_unit frequency_scale_unit (str): frequency scale units, one of PITCH_UNITS amplitude_unit (str): amplitude units, one of AMPLITUDE_UNITS Defaults to "binary". Returns: * sparse_index (np.ndarray): Array of sparce indices [(time_index, frequency_index)] * amplitude (np.ndarray): Array of amplitude values for each index """ multif0dat = self.resample(time_scale, time_scale_unit) time_indexes = np.arange(len(time_scale)) frequencies_flattened = convert_pitch_units( np.array([f for f_list in multif0dat.frequency_list for f in f_list]), self.frequency_unit, frequency_scale_unit, ) time_indexes_flattened = np.array( [ t for (t, f_list) in zip(time_indexes, multif0dat.frequency_list) for f in f_list ] ) if multif0dat.confidence_list is None: confidence_flattened = np.ones((len(time_indexes_flattened),)) conf_unit = "binary" else: confidence_flattened = np.array( [c for c_list in multif0dat.confidence_list for c in c_list] ) conf_unit = self.confidence_unit # get frequency indexes in matrix nonzero_freqs = ( frequencies_flattened > 0 ) # find indexes for frequencies not equal to 0 frequencies_flattened[frequencies_flattened == 0] = ( 1 # change zero frequency value to avoid NaN ) freq_indexes = closest_index( np.log(frequencies_flattened)[:, np.newaxis], np.log(frequency_scale)[:, np.newaxis], ) # create sparse index index = [ (t, f) for t, f in zip( time_indexes_flattened[nonzero_freqs], freq_indexes[nonzero_freqs] ) if t != -1 and f != -1 ] confidence_out = np.array( [ c for c, t, f in zip( confidence_flattened[nonzero_freqs], time_indexes_flattened[nonzero_freqs], freq_indexes[nonzero_freqs], ) if t != -1 and f != -1 ] ) return ( np.array(index), convert_amplitude_units(confidence_out, conf_unit, amplitude_unit), )
[docs] def to_matrix( self, time_scale, time_scale_unit, frequency_scale, frequency_scale_unit, amplitude_unit="binary", ): """Convert f0 data to a matrix (piano roll) defined by a time and frequency scale Args: time_scale (np.array): times in units time_unit time_scale_unit (str): time scale units, one of TIME_UNITS frequency_scale (np.array): frequencies in frequency_unit frequency_scale_unit (str): frequency scale units, one of PITCH_UNITS amplitude_unit (str): amplitude units, one of AMPLITUDE_UNITS Defaults to "binary". Returns: np.ndarray: 2D matrix of shape len(time_scale) x len(frequency_scale) """ index, voicing = self.to_sparse_index( time_scale, time_scale_unit, frequency_scale, frequency_scale_unit, amplitude_unit, ) matrix = np.zeros((len(time_scale), len(frequency_scale))) matrix[index[:, 0], index[:, 1]] = voicing return matrix
[docs] def to_mir_eval(self): """Convert annotation into the format expected by mir_eval.multipitch.evaluate Returns: * times (np.ndarray): array of uniformly spaced time stamps in seconds * frequency_list (list): list of np.array of frequency values in Hz """ times = convert_time_units(self.times, self.time_unit, "s") frequency_list = [ convert_pitch_units(np.array(flist), self.frequency_unit, "hz") for flist in self.frequency_list ] return times, frequency_list
[docs] class NoteData(Annotation): """NoteData class Attributes: intervals (np.ndarray): (n x 2) array of intervals in the form [start_time, end_time]. Times should be positive and intervals should have non-negative duration interval_unit (str): unit of the time values in intervals. One of TIME_UNITS. pitches (np.ndarray): array of pitches pitch_unit (str): note unit, one of PITCH_UNITS confidence (np.ndarray or None): array of confidence values confidence_unit (str or None): confidence unit, one of AMPLITUDE_UNITS """ def __init__( self, intervals: np.ndarray, interval_unit: str, pitches: np.ndarray, pitch_unit: str, confidence: Optional[np.ndarray] = None, confidence_unit: Optional[str] = None, ): validate_array_like(intervals, np.ndarray, float) validate_array_like(pitches, np.ndarray, float) validate_array_like(confidence, np.ndarray, float, none_allowed=True) validate_lengths_equal([intervals, pitches, confidence]) validate_intervals(intervals, interval_unit) validate_pitches(pitches, pitch_unit) validate_confidence(confidence, confidence_unit) self.intervals = intervals self.interval_unit = interval_unit self.pitches = pitches self.pitch_unit = pitch_unit self.confidence = confidence self.confidence_unit = confidence_unit self._remove_duplicates() @property def notes(self) -> np.ndarray: logging.warning( "NoteData.notes is deprecated as of 0.3.4 and will be removed in a future version. Use" " NoteData.pitches." ) return self.pitches def _remove_duplicates(self): # deduplicate if matching interval and pitch unq, unq_idx = np.unique( np.hstack([self.intervals, self.pitches[:, np.newaxis]]), axis=0, return_index=True, ) self.intervals = unq[:, :2] self.pitches = unq[:, 2] if self.confidence is not None: self.confidence = self.confidence[unq_idx] def __add__(self, other): if other is None: return self if not isinstance(other, NoteData): raise TypeError("Unable to add type {} to NoteData".format(type(other))) # convert to the current units intervals = convert_time_units( other.intervals, other.interval_unit, self.interval_unit ) pitches = convert_pitch_units(other.pitches, other.pitch_unit, self.pitch_unit) if other.confidence is None and self.confidence is None: new_confidence = None new_confidence_unit = None elif other.confidence is not None and self.confidence is not None: new_confidence = np.concatenate( [ self.confidence, convert_amplitude_units( other.confidence, other.confidence_unit, self.confidence_unit ), ] ) new_confidence_unit = self.confidence_unit else: logging.warning( "Adding two NoteData objects but one has confidence=None and " + "the other does not. The resulting confidence will be None" ) new_confidence = None new_confidence_unit = None return NoteData( np.vstack([self.intervals, intervals]), self.interval_unit, np.concatenate([self.pitches, pitches]), self.pitch_unit, new_confidence, new_confidence_unit, )
[docs] def to_sparse_index( self, time_scale: np.ndarray, time_scale_unit: str, frequency_scale: np.ndarray, frequency_scale_unit: str, amplitude_unit: str = "binary", onsets_only: bool = False, ) -> Tuple[np.ndarray, np.ndarray]: """Convert note annotations to indexes of a sparse matrix (piano roll) Args: time_scale (np.array): array of matrix time stamps in seconds time_scale_unit (str): units for time scale values, one of TIME_UNITS frequency_scale (np.array): array of matrix frequency values in seconds frequency_scale_unit (str): units for frequency scale values, one of PITCH_UNITS amplitude_unit (str): units for amplitude values, one of AMPLITUDE_UNITS. Defaults to "binary". onsets_only (bool, optional): If True, returns an onset piano roll. Defaults to False. Returns: * sparse_index (np.ndarray): Array of sparce indices [(time_index, frequency_index)] * amplitude (np.ndarray): Array of amplitude values for each index """ intervals = convert_time_units( self.intervals, self.interval_unit, time_scale_unit ) freqs_hz = convert_pitch_units( self.pitches, self.pitch_unit, frequency_scale_unit ) if self.confidence is not None: confidence = convert_amplitude_units( self.confidence, self.confidence_unit, amplitude_unit ) else: confidence = convert_amplitude_units( np.ones((freqs_hz.shape)), "binary", amplitude_unit ) time_index_0 = closest_index( intervals[:, 0, np.newaxis], time_scale[:, np.newaxis] ) freq_indexes = closest_index( np.log(freqs_hz)[:, np.newaxis], np.log(frequency_scale)[:, np.newaxis] ) if onsets_only: onset_index = [] confidences = [] for t0, f, c in zip(time_index_0, freq_indexes, confidence): if t0 == -1 or f == -1: continue onset_index.append([t0, f]) confidences.append(c) return np.array(onset_index), np.array(confidences) time_index_1 = closest_index( intervals[:, 1, np.newaxis], time_scale[:, np.newaxis] ) max_idx = len(time_scale) - 1 sparse_index = [] confidences = [] for t0, t1, f, c in zip(time_index_0, time_index_1, freq_indexes, confidence): if f == -1 or (t0 == -1 and t1 == -1): continue t_start = max([t0, 0]) t_end = (t1 if t1 != -1 else max_idx) + 1 sparse_index.extend([[t, f] for t in range(t_start, t_end)]) confidences.extend([c for _ in range(t_start, t_end)]) return np.array(sparse_index), np.array(confidences)
[docs] def to_matrix( self, time_scale: np.ndarray, time_scale_unit: str, frequency_scale: np.ndarray, frequency_scale_unit: str, amplitude_unit: str = "binary", onsets_only: bool = False, ) -> np.ndarray: """Convert f0 data to a matrix (piano roll) defined by a time and frequency scale Args: time_scale (np.ndarray): array of matrix time stamps in seconds time_scale_unit (str): units for time scale values, one of TIME_UNITS frequency_scale (np.ndarray): array of matrix frequency values in seconds frequency_scale_unit (str): units for frequency scale values, one of PITCH_UNITS onsets_only (bool, optional): If True, returns an onset piano roll. Defaults to False. Returns: np.ndarray: 2D matrix of shape len(time_scale) x len(frequency_scale) """ index, voicing = self.to_sparse_index( time_scale, time_scale_unit, frequency_scale, frequency_scale_unit, amplitude_unit, onsets_only, ) matrix = np.zeros((len(time_scale), len(frequency_scale))) matrix[index[:, 0], index[:, 1]] = voicing return matrix
[docs] def to_multif0( self, time_hop: float, time_hop_unit: str, max_time: Optional[float] = None ) -> MultiF0Data: """Convert note annotation to multiple f0 format. Args: time_hop (float): time between time stamps in multif0 annotation time_hop_unit (str): unit for time_hop, and resulting multif0 data. One of TIME_UNITS max_time (float, optional): Maximum time stamp in time_hop units. Defaults to None, in which case the maximum note interval time is used. Returns: MultiF0Data: multif0 annotation """ intervals = convert_time_units( self.intervals, self.interval_unit, time_hop_unit ) note_time_max = np.max(intervals[:, 1]) max_time = note_time_max if not max_time else max_time if max_time < note_time_max: raise ValueError( "max_time = {} cannot be smaller than the last note interval = {}".format( max_time, note_time_max ) ) times = np.arange(0, max_time + time_hop, time_hop) frequency_list: List[List[float]] = [[] for _ in times] confidence_list: List[List[float]] = [[] for _ in times] if self.confidence is not None: for t0, t1, pch, conf in zip( intervals[:, 0], intervals[:, 1], self.pitches, self.confidence ): for i in range( int(np.round(t0 / time_hop)), int(np.round(t1 / time_hop)) + 1 ): frequency_list[i].append(pch) confidence_list[i].append(conf) else: for t0, t1, pch in zip(intervals[:, 0], intervals[:, 1], self.pitches): for i in range( int(np.round(t0 / time_hop)), int(np.round(t1 / time_hop)) + 1 ): frequency_list[i].append(pch) return MultiF0Data( times, time_hop_unit, frequency_list, self.pitch_unit, None if self.confidence is None else confidence_list, self.confidence_unit, )
[docs] def to_mir_eval(self): """Convert data to the format expected by mir_eval.transcription.evaluate and mir_eval.transcription_velocity.evaluate Returns: * intervals (np.ndarray) - (n x 2) array of intervals of start time, end time in seconds * pitches (np.ndarray) - array of pitch values in hz * velocity (optional, np.ndarray) - array of velocity values between 0 and 127 """ intervals = convert_time_units(self.intervals, self.interval_unit, "s") pitches = convert_pitch_units(self.pitches, self.pitch_unit, "hz") velocity = ( None if self.confidence is None else convert_amplitude_units( self.confidence, self.confidence_unit, "velocity" ) ) return intervals, pitches, velocity
[docs] class KeyData(Annotation): """KeyData class Attributes: intervals (np.ndarray): (n x 2) array of intervals in the form [start_time, end_time]. Times should be positive and intervals should have non-negative duration interval_unit (str): unit of the time values in intervals. One of TIME_UNITS. keys (list): list key labels (as strings) key_unit (str): key unit, one of KEY_UNITS """ def __init__(self, intervals, interval_unit, keys, key_unit): validate_array_like(intervals, np.ndarray, float) validate_array_like(keys, list, str) validate_lengths_equal([intervals, keys]) validate_intervals(intervals, interval_unit) validate_key_labels(keys, key_unit) self.intervals = intervals self.interval_unit = interval_unit self.keys = keys self.key_unit = key_unit
[docs] class LyricData(Annotation): """LyricData class Attributes: intervals (np.ndarray): (n x 2) array of intervals in the form [start_time, end_time]. Times should be positive and intervals should have non-negative duration interval_unit (str): unit of the time values in intervals. One of TIME_UNITS. lyrics (list): list of lyrics (as strings) lyric_unit (str): lyric unit, one of LYRIC_UNITS """ def __init__(self, intervals, interval_unit, lyrics, lyric_unit): validate_array_like(intervals, np.ndarray, float) validate_array_like(lyrics, list, str) validate_lengths_equal([intervals, lyrics]) validate_intervals(intervals, interval_unit) validate_unit(lyric_unit, LYRIC_UNITS) self.intervals = intervals self.interval_unit = interval_unit self.lyrics = lyrics self.lyric_unit = lyric_unit @property def pronunciations(self): logging.warning( "LyricData.pronunciations is deprecated as of 0.3.4 and will be removed in a future" " version. Use LyricData.lyrics." ) return self.lyrics
[docs] class TempoData(Annotation): """TempoData class Attributes: intervals (np.ndarray): (n x 2) array of intervals in the form [start_time, end_time]. Times should be positive and intervals should have non-negative duration interval_unit (str): unit of the time values in intervals. One of TIME_UNITS. tempos (list): array of tempo values (as floats) tempo_unit (str): tempo unit, one of TEMPO_UNITS confidence (np.ndarray or None): array of confidence values confidence_unit (str or None): confidence unit, one of AMPLITUDE_UNITS """ def __init__( self, intervals, interval_unit, tempos, tempo_unit, confidence=None, confidence_unit=None, ): validate_array_like(intervals, np.ndarray, float) validate_array_like(tempos, np.ndarray, float) validate_array_like(confidence, np.ndarray, float, none_allowed=True) validate_lengths_equal([intervals, tempos, confidence]) validate_intervals(intervals, interval_unit) validate_tempos(tempos, tempo_unit) validate_confidence(confidence, confidence_unit) self.intervals = intervals self.interval_unit = interval_unit self.tempos = tempos self.tempo_unit = tempo_unit self.confidence = confidence self.confidence_unit = confidence_unit @property def value(self): logging.warning( "TempoData.value is deprecated as of 0.3.4 and will be removed in a future version. Use" " TempoData.tempos." ) return self.tempos
[docs] class EventData(Annotation): """EventData class Attributes: intervals (np.ndarray): (n x 2) array of intervals in the form [start_time, end_time]. Times should be positive and intervals should have non-negative duration interval_unit (str): unit of the time values in intervals. One of TIME_UNITS. interval_unit (str): interval units, one of TIME_UNITS events (list): list of event labels (as strings) event_unit (str): event units, one of EVENT_UNITS """ def __init__(self, intervals, interval_unit, events, event_unit): validate_array_like(intervals, np.ndarray, float) validate_array_like(events, list, str) validate_lengths_equal([intervals, events]) validate_intervals(intervals, interval_unit) validate_unit(event_unit, EVENT_UNITS) self.intervals = intervals self.interval_unit = interval_unit self.events = events self.event_unit = event_unit
def convert_time_units(times, time_unit, target_time_unit): """Convert a time array from time_unit to target_time_unit Args: times (np.ndarray): array of time values in units time_unit time_unit (str): time unit, one of TIME_UNITS target_time_unit (str): new time unit, one of TIME_UNITS Raises: ValueError: If time units are not convertable Returns: np.ndarray: times in units target_time_unit """ if time_unit == "ticks" and target_time_unit == "ticks": return times def _to_seconds(times, time_unit): """Convert times in time_unit to seconds""" if time_unit == "s": return times if time_unit == "ms": return times / 1000.0 raise NotImplementedError def _from_seconds(times_sec, target_time_unit): """Convert times in seconds to target_time_unit""" if target_time_unit == "s": return times_sec if target_time_unit == "ms": return times_sec * 1000.0 raise NotImplementedError try: return _from_seconds(_to_seconds(times, time_unit), target_time_unit) except NotImplementedError: raise NotImplementedError( "Conversion of time in units {} to {} is not supported".format( time_unit, target_time_unit ) )
[docs] def convert_pitch_units(pitches, pitch_unit, target_pitch_unit): """Convert pitch values from pitch_unit to target_pitch_unit Args: pitches (np.array): array of pitch values pitch_unit (str): unit of pitch, one of PITCH_UNITS target_pitch_unit (str): target unit of pitch, one of PITCH_UNITS Raises: NotImplementedError: If conversion between given units is not supported Returns: np.array: array of pitch values in target_pitch_unit """ # if input is a nested list, call this function recursively if isinstance(pitches, list) and isinstance(pitches[0], list): return [ ( [] if len(plist) == 0 else list(convert_pitch_units(plist, pitch_unit, target_pitch_unit)) ) for plist in pitches ] if pitch_unit == "pc" and target_pitch_unit == "pc": return pitches def _to_hz(pitches, pitch_unit): """Convert pitches in pitch_unit to Hz""" if pitch_unit == "hz": return pitches if pitch_unit == "midi": zero_idx = pitches == 0 pitches_hz = librosa.midi_to_hz(pitches) pitches_hz[zero_idx] = 0 return pitches_hz if pitch_unit == "note_name": return librosa.note_to_hz(pitches) raise NotImplementedError def _from_hz(pitches_hz, target_pitch_unit): """Convert pitches int Hz to target_pitch_unit""" if target_pitch_unit == "hz": return pitches_hz if target_pitch_unit == "midi": zero_idx = pitches_hz == 0 pitches_midi = librosa.hz_to_midi(pitches_hz) pitches_midi[zero_idx] = 0 return pitches_midi if target_pitch_unit == "note_name": # cast to np.array for compatibility with legacy python3.6 and # librosa 0.9.2. It is redundant for librosa 0.10 return np.array(librosa.hz_to_note(pitches_hz)) raise NotImplementedError try: return _from_hz(_to_hz(pitches, pitch_unit), target_pitch_unit) except NotImplementedError: raise NotImplementedError( "Conversion of pitch in units {} to {} is not supported".format( pitch_unit, target_pitch_unit ) )
[docs] def convert_amplitude_units(amplitude, amplitude_unit, target_amplitude_unit): """Convert amplitude values to likelihoods Args: amplitude (np.array): array of amplitude values amplitude_unit (str): unit of amplitude, one of AMPLITUDE_UNITS target_amplitude_unit (str): target unit of amplitude, one of AMPLITUDE_UNITS Raises: NotImplementedError: If conversion is not supported Returns: np.array: array of amplitude values as in target amplitude unit """ # if input is a nested list, call this function recursively if isinstance(amplitude, list) and isinstance(amplitude[0], list): return [ ( [] if len(alist) == 0 else list( convert_amplitude_units( np.array(alist), amplitude_unit, target_amplitude_unit ) ) ) for alist in amplitude ] def _to_likelihood(amplitude, amplitude_unit): if amplitude_unit in ["likelihood", "binary"]: return amplitude if amplitude_unit == "velocity": return amplitude / 127.0 raise NotImplementedError def _from_likelihood(amplitude, target_amplitude_unit): if target_amplitude_unit == "likelihood": return amplitude if target_amplitude_unit == "binary": return np.ceil(amplitude) if target_amplitude_unit == "velocity": return amplitude * 127.0 raise NotImplementedError try: return _from_likelihood( _to_likelihood(amplitude, amplitude_unit), target_amplitude_unit ) except NotImplementedError: raise NotImplementedError( "Conversion of amplitude in units {} to {} is not supported".format( amplitude_unit, target_amplitude_unit ) )
[docs] def closest_index(input_array, target_array): """Get array of indices of target_array that are closest to the input_array Args: input_array (np.ndarray): (n x 2) array of input values target_array (np.ndarray): (m x 2) array of target values) Returns: np.ndarray: array of shape (n x 1) of indexes into target_array """ indexes = np.argmin(scipy.spatial.distance.cdist(input_array, target_array), axis=1) indexes[input_array[:, 0] > np.max(target_array[:, 0])] = -1 indexes[input_array[:, 0] < np.min(target_array[:, 0])] = -1 return indexes
[docs] def validate_array_like( array_like, expected_type, expected_dtype, check_child=False, none_allowed=False ): """Validate that array-like object is well formed If array_like is None, validation passes automatically. Args: array_like (array-like): object to validate expected_type (type): expected type, either list or np.ndarray expected_dtype (type): expected dtype check_child (bool): if True, checks if all elements of array are children of expected_dtype none_allowed (bool): if True, allows array to be None Raises: TypeError: if type/dtype does not match expected_type/expected_dtype ValueError: if array is empty but it shouldn't be """ if array_like is None: if none_allowed: return else: raise ValueError("array_like cannot be None") assert expected_type in [ list, np.ndarray, ], "expected type must be a list or np.ndarray" if not isinstance(array_like, expected_type): raise TypeError( f"Object should be a {expected_type}, but is a {type(array_like)}" ) if expected_type == list and not all( isinstance(n, expected_dtype) for n in array_like if not ((n is None) and none_allowed) ): raise TypeError(f"List elements should all have type {expected_dtype}") if ( expected_type == np.ndarray and array_like.dtype != expected_dtype and expected_dtype is not None ): raise TypeError( f"Array should have dtype {expected_dtype} but has {array_like.dtype}" ) if np.asarray(array_like, dtype=object).size == 0: raise ValueError("Object should not be empty, use None instead")
[docs] def validate_lengths_equal(array_list): """Validate that arrays in list are equal in length Some arrays may be None, and the validation for these are skipped. Args: array_list (list): list of array-like objects Raises: ValueError: if arrays are not equal in length """ if len(array_list) == 1: return for att1, att2 in zip(array_list[:-1], array_list[1:]): if att1 is None or att2 is None: continue if not len(att1) == len(att2): raise ValueError("Arrays have unequal length")
[docs] def validate_tempos(tempo, tempo_unit): """Validate if tempos are well-formed Args: tempo (list): list of tempo values tempo_unit (str): tempo unit, one of TEMPO_UNITS Raises: ValueError: if tempos are not well-formed """ validate_unit(tempo_unit, TEMPO_UNITS) if (tempo < 0).any(): raise ValueError("tempos must be positive")
[docs] def validate_beat_positions(positions, position_unit): """Validate if positions is well-formed. Args: positions (np.ndarray): an array of positions values positions_unit (str): one of BEAT_POSITION_UNITS Raises: ValueError: if positions values are incompatible with the unit """ if positions is None: return validate_unit(position_unit, BEAT_POSITION_UNITS) position_shape = np.shape(positions) if len(position_shape) != 1: raise ValueError( f"positions should be 1d, but array has shape {position_shape}" ) if (positions < 0).any(): raise ValueError("beat positions must be positive. Found values below 0.") if position_unit in ["bar_index", "global_index"] and not np.array_equal( np.floor(positions), positions ): raise ValueError( "measure index or global indexes should be integers. " + "Found fractional values." ) # we expect no more than 32 beats per bar - this can be changed if a need arises! if position_unit == "bar_index" and np.max(positions) > 32: raise ValueError( "beats with bar_index units should have indexes " + "which start from 1 at the beginning of every measure. " + "Found values > 16." ) if position_unit == "bar_fraction" and np.max(positions) > 1: raise ValueError( "beats with bar_fraction units should be between 0 and 1. " + "Found values above 1." )
[docs] def validate_confidence(confidence, confidence_unit): """Validate if confidence is well-formed. If confidence is None, validation passes automatically Args: confidence (np.ndarray): an array of confidence values confidence_unit (str): one of AMPLITUDE_UNITS Raises: ValueError: if confidence values are incompatible with the unit """ if confidence is None: return validate_unit(confidence_unit, AMPLITUDE_UNITS) if isinstance(confidence[0], list): confidence_flat = [c for subconf in confidence for c in subconf] else: confidence_flat = confidence if confidence_unit == "likelihood" and ( any([c < 0 for c in confidence_flat]) or any([c > 1 for c in confidence_flat]) ): raise ValueError( "confidence with unit 'likelihood' should be between 0 and 1. " + "Found values outside [0, 1]." ) if confidence_unit == "energy" and any([c < 0 for c in confidence_flat]): raise ValueError( "confidence with unit 'energy' should be nonnegative. " + "Found negative values." ) if confidence_unit == "binary" and any([c not in [0, 1] for c in confidence_flat]): raise ValueError( "confidence with unit 'binary' should only have values of 0 or 1. " + "Found non-binary values." ) if confidence_unit == "velocity" and ( any([c < 0 for c in confidence_flat]) or any([c > 127 for c in confidence_flat]) ): raise ValueError( "confidence with unit 'velocity' should be between 0 and 127. " + "Found values outside [0, 127]." )
[docs] def validate_voicing(voicing, voicing_unit): """Validate if voicing is well-formed. Args: voicing (np.ndarray): an array of voicing values voicing_unit (str): one of VOICING_UNITS Raises: ValueError: if voicing values are incompatible with the unit """ validate_unit(voicing_unit, VOICING_UNITS) voicing_shape = np.shape(voicing) if len(voicing_shape) != 1: raise ValueError(f"voicings should be 1d, but array has shape {voicing_shape}") if voicing_unit == "likelihood" and ( any([c < 0 for c in voicing]) or any([c > 1 for c in voicing]) ): raise ValueError( "voicing with unit 'likelihood' should be between 0 and 1. " + "Found values outside [0, 1]." ) if voicing_unit == "binary" and any([c not in [0, 1] for c in voicing]): raise ValueError( "voicing with unit 'binary' should only have values of 0 or 1. " + "Found non-binary values." )
[docs] def validate_pitches(pitches, pitch_unit): """Validate if pitches are well-formed. Args: pitches (np.ndarray): an array of pitch values pitch_unit (str): pitch unit, one of PITCH_UNITS Raises: ValueError: if pitches do not correspond to the unit """ validate_unit(pitch_unit, PITCH_UNITS) if pitch_unit in ["hz", "midi"] and np.any( [np.any(np.array(p) < 0) for p in pitches] ): raise ValueError( "pitches should be positive numbers. " + "Unvoiced frames should be indicated using the confidence field, " + "rather than negative pitch values." ) if pitch_unit == "midi" and np.any([np.any(np.array(p) > 127) for p in pitches]): raise ValueError("pitches in midi format cannot be larger than 127. ") if pitch_unit in ["pc", "note_name"]: try: librosa.note_to_midi(pitches) except: raise ValueError("invalid format for unit pc or note_name")
[docs] def validate_chord_labels(chords, chord_unit): """Validate that chord labels conform to chord_unit namespace Args: chords (list): list of chord labels as strings chord_unit (str): chord namespace, e.g. "harte" Raises: ValueError: If chords don't conform to namespace """ validate_unit(chord_unit, CHORD_UNITS) if chord_unit in ["harte", "jams"]: if chord_unit == "harte": pattern = HARTE_CHORD_PATTERN elif chord_unit == "jams": pattern = JAMS_CHORD_PATTERN matches = [re.match(pattern, c) for c in chords] if not all(matches): non_matches = [c for c, m in zip(chords, matches) if not m] raise ValueError( "chords {} don't conform to chord_unit {}".format( non_matches, chord_unit ) )
[docs] def validate_key_labels(keys, key_unit): """Validate that key labels conform to key_unit namespace Args: keys (list): list of key labels as strings key_unit (str): key namespace, e.g. "harte" Raises: ValueError: If keys don't conform to namespace """ validate_unit(key_unit, KEY_UNITS) if key_unit == "key_mode": pattern = KEY_MODE_PATTERN matches = [re.match(pattern, c) for c in keys] if not all(matches): non_matches = [k for k, m in zip(keys, matches) if not m] raise ValueError( "keys {} don't conform to key_unit key-mode".format(non_matches) )
[docs] def validate_times(times, time_unit): """Validate if times are well-formed. If times is None, validation passes automatically Args: times (np.ndarray): an array of time stamps time_unit (str): one of TIME_UNITS Raises: ValueError: if times have negative values or are non-increasing """ if times is None: return validate_unit(time_unit, TIME_UNITS) time_shape = np.shape(times) if len(time_shape) != 1: raise ValueError(f"Times should be 1d, but array has shape {time_shape}") if (times < 0).any(): raise ValueError("times should be positive numbers") if (times[1:] - times[:-1] <= 0).any(): raise ValueError("times should be strictly increasing")
[docs] def validate_intervals(intervals, interval_unit): """Validate if intervals are well-formed. If intervals is None, validation passes automatically Args: intervals (np.ndarray): (n x 2) array interval_unit (str): interval unit, one of TIME_UNITS Raises: ValueError: if intervals have an invalid shape, have negative values or if end times are smaller than start times. """ if intervals is None: return validate_unit(interval_unit, TIME_UNITS) # validate that intervals have the correct shape interval_shape = np.shape(intervals) if len(interval_shape) != 2 or interval_shape[1] != 2: raise ValueError( f"Intervals should be arrays with two columns, but array has {interval_shape}" ) # validate that time stamps are all positive numbers if (intervals < 0).any(): raise ValueError(f"Interval values should be nonnegative numbers") # validate that end times are bigger than start times elif (intervals[:, 1] - intervals[:, 0] < 0).any(): raise ValueError(f"Interval start times must be smaller than end times")
[docs] def validate_unit(unit, unit_values, allow_none=False): """Validate that the given unit is one of the allowed unit values. Args: unit (str): the unit name unit_values (dict): dictionary of possible unit values allow_none (bool): if true, allows unit=None to pass validation Raises: ValueError: If the given unit is not one of the allowed unit valuess """ if allow_none and not unit: return if unit not in unit_values: raise ValueError("unit={} is not one of {}".format(unit, unit_values))
[docs] def validate_uniform_times(times): time_diffs = np.diff(times) median_diff = np.median(time_diffs) if any(np.abs(time_diffs - median_diff) > 0.01): raise ValueError( "time stamps should be uniformly spaced, but found non-uniform spacing" )