From 1529c6fcba0e100f92caff76689bf66d893256c1 Mon Sep 17 00:00:00 2001 From: Brian McFee Date: Sat, 13 May 2017 12:13:03 -0400 Subject: [PATCH 1/7] deprecating jamsframe to_interval_values --- jams/core.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++ jams/util.py | 1 + tests/jams_test.py | 27 +++++++++++++++++++++++--- 3 files changed, 73 insertions(+), 3 deletions(-) diff --git a/jams/core.py b/jams/core.py index 3a73e3e0..a5467e0d 100644 --- a/jams/core.py +++ b/jams/core.py @@ -46,6 +46,9 @@ import copy import sys +from decorator import decorator + + from .version import version as __VERSION__ from . import schema from .exceptions import JamsError, SchemaError, ParameterError @@ -57,6 +60,29 @@ 'FileMetadata', 'AnnotationArray', 'JAMS'] +def deprecated(version, version_removed): + '''This is a decorator which can be used to mark functions + as deprecated. + + It will result in a warning being emitted when the function is used.''' + + def __wrapper(func, *args, **kwargs): + '''Warn the user, and then proceed.''' + code = six.get_function_code(func) + warnings.warn_explicit( + "{:s}.{:s}\n\tDeprecated as of JAMS version {:s}." + "\n\tIt will be removed in JAMS version {:s}." + .format(func.__module__, func.__name__, + version, version_removed), + category=DeprecationWarning, + filename=code.co_filename, + lineno=code.co_firstlineno + 1 + ) + return func(*args, **kwargs) + + return decorator(__wrapper) + + @contextlib.contextmanager def _open(name_or_fdesc, mode='r', fmt='auto'): '''An intelligent wrapper for ``open``. @@ -711,6 +737,7 @@ def add_observation(self, time=None, duration=None, self.drop(n, inplace=True, errors='ignore') six.reraise(SchemaError, SchemaError(str(exc)), sys.exc_info()[2]) + @deprecated('0.2.3', '0.3.0') def to_interval_values(self): '''Extract observation data in a `mir_eval`-friendly format. @@ -1168,6 +1195,25 @@ def slice(self, start_time, end_time, strict=False): return sliced_ann + def to_interval_values(self): + '''Extract observation data in a `mir_eval`-friendly format. + + Returns + ------- + intervals : np.ndarray [shape=(n, 2), dtype=float] + Start- and end-times of all valued intervals + + `intervals[i, :] = [time[i], time[i] + duration[i]]` + + labels : list + List view of value field. + ''' + + times = timedelta_to_float(self.data.time.values) + duration = timedelta_to_float(self.data.duration.values) + + return np.vstack([times, times + duration]).T, list(self.data.value) + class Curator(JObject): """Curator @@ -1881,3 +1927,5 @@ def serialize_obj(obj): return [serialize_obj(x) for x in obj] return obj + + diff --git a/jams/util.py b/jams/util.py index eb3cc0bc..47b4d940 100644 --- a/jams/util.py +++ b/jams/util.py @@ -12,6 +12,7 @@ smkdirs filebase find_with_extension + _deprecated """ import os diff --git a/tests/jams_test.py b/tests/jams_test.py index 1ef19f1c..2a4c4f42 100644 --- a/tests/jams_test.py +++ b/tests/jams_test.py @@ -211,10 +211,16 @@ def test_jamsframe_interval_values(): jf = jams.JamsFrame.from_dataframe(df) - intervals, values = jf.to_interval_values() + warnings.resetwarnings() + warnings.simplefilter('always') + with warnings.catch_warnings(record=True) as out: + intervals, values = jf.to_interval_values() + assert len(out) > 0 + assert out[0].category is DeprecationWarning + assert 'deprecated' in str(out[0].message).lower() - assert np.allclose(intervals, np.array([[0.0, 1.0], [1.0, 3.0]])) - eq_(values, ['a', 'b']) + assert np.allclose(intervals, np.array([[0.0, 1.0], [1.0, 3.0]])) + eq_(values, ['a', 'b']) def test_jamsframe_serialize(): @@ -369,6 +375,21 @@ def test_annotation_eq(): assert not (ann1 == ann2) + +def test_annotation_interval_values(): + + data = dict(time=[0.0, 1.0], + duration=[1., 2.0], + value=['a', 'b'], + confidence=[0.9, 0.9]) + + ann = jams.Annotation(namespace='tag_open', data=data) + + intervals, values = ann.to_interval_values() + + assert np.allclose(intervals, np.array([[0.0, 1.0], [1.0, 3.0]])) + eq_(values, ['a', 'b']) + # FileMetadata def test_filemetadata(): From a7303f4507e7357683057e3975e417d3d7bd97ff Mon Sep 17 00:00:00 2001 From: Brian McFee Date: Sat, 13 May 2017 12:15:25 -0400 Subject: [PATCH 2/7] continuing deprecation march --- jams/display.py | 10 +++++----- jams/eval.py | 28 ++++++++++++++-------------- jams/sonify.py | 10 +++++----- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/jams/display.py b/jams/display.py index 0ada9a45..c8161860 100644 --- a/jams/display.py +++ b/jams/display.py @@ -63,7 +63,7 @@ def pprint_jobject(obj, **kwargs): def intervals(annotation, **kwargs): '''Plotting wrapper for labeled intervals''' - times, labels = annotation.data.to_interval_values() + times, labels = annotation.to_interval_values() return mir_eval.display.labeled_intervals(times, labels, **kwargs) @@ -83,7 +83,7 @@ def pitch_contour(annotation, **kwargs): # If the annotation is empty, we need to construct a new axes ax = mir_eval.display.__get_axes(ax=ax)[0] - times, values = annotation.data.to_interval_values() + times, values = annotation.to_interval_values() indices = np.unique([v['index'] for v in values]) @@ -102,7 +102,7 @@ def pitch_contour(annotation, **kwargs): def event(annotation, **kwargs): '''Plotting wrapper for events''' - times, values = annotation.data.to_interval_values() + times, values = annotation.to_interval_values() if any(values): labels = values @@ -115,7 +115,7 @@ def event(annotation, **kwargs): def beat_position(annotation, **kwargs): '''Plotting wrapper for beat-position data''' - times, values = annotation.data.to_interval_values() + times, values = annotation.to_interval_values() labels = [_['position'] for _ in values] @@ -125,7 +125,7 @@ def beat_position(annotation, **kwargs): def piano_roll(annotation, **kwargs): '''Plotting wrapper for piano rolls''' - times, midi = annotation.data.to_interval_values() + times, midi = annotation.to_interval_values() return mir_eval.display.piano_roll(times, midi=midi, **kwargs) diff --git a/jams/eval.py b/jams/eval.py index 8f9ac8fe..7871e0b1 100644 --- a/jams/eval.py +++ b/jams/eval.py @@ -104,8 +104,8 @@ def beat(ref, est, **kwargs): namespace = 'beat' ref = coerce_annotation(ref, namespace) est = coerce_annotation(est, namespace) - ref_interval, _ = ref.data.to_interval_values() - est_interval, _ = est.data.to_interval_values() + ref_interval, _ = ref.to_interval_values() + est_interval, _ = est.to_interval_values() return mir_eval.beat.evaluate(ref_interval[:, 0], est_interval[:, 0], **kwargs) @@ -145,8 +145,8 @@ def onset(ref, est, **kwargs): namespace = 'onset' ref = coerce_annotation(ref, namespace) est = coerce_annotation(est, namespace) - ref_interval, _ = ref.data.to_interval_values() - est_interval, _ = est.data.to_interval_values() + ref_interval, _ = ref.to_interval_values() + est_interval, _ = est.to_interval_values() return mir_eval.onset.evaluate(ref_interval[:, 0], est_interval[:, 0], **kwargs) @@ -187,8 +187,8 @@ def chord(ref, est, **kwargs): namespace = 'chord' ref = coerce_annotation(ref, namespace) est = coerce_annotation(est, namespace) - ref_interval, ref_value = ref.data.to_interval_values() - est_interval, est_value = est.data.to_interval_values() + ref_interval, ref_value = ref.to_interval_values() + est_interval, est_value = est.to_interval_values() return mir_eval.chord.evaluate(ref_interval, ref_value, est_interval, est_value, **kwargs) @@ -229,8 +229,8 @@ def segment(ref, est, **kwargs): namespace = 'segment_open' ref = coerce_annotation(ref, namespace) est = coerce_annotation(est, namespace) - ref_interval, ref_value = ref.data.to_interval_values() - est_interval, est_value = est.data.to_interval_values() + ref_interval, ref_value = ref.to_interval_values() + est_interval, est_value = est.to_interval_values() return mir_eval.segment.evaluate(ref_interval, ref_value, est_interval, est_value, **kwargs) @@ -253,7 +253,7 @@ def hierarchy_flatten(annotation): A list of lists of labels, ordered by increasing specificity. ''' - intervals, values = annotation.data.to_interval_values() + intervals, values = annotation.to_interval_values() ordering = dict() @@ -394,8 +394,8 @@ def melody(ref, est, **kwargs): namespace = 'pitch_contour' ref = coerce_annotation(ref, namespace) est = coerce_annotation(est, namespace) - ref_interval, ref_p = ref.data.to_interval_values() - est_interval, est_p = est.data.to_interval_values() + ref_interval, ref_p = ref.to_interval_values() + est_interval, est_p = est.to_interval_values() ref_freq = np.asarray([p['frequency'] * (-1)**(~p['voiced']) for p in ref_p]) est_freq = np.asarray([p['frequency'] * (-1)**(~p['voiced']) for p in est_p]) @@ -432,7 +432,7 @@ def pattern_to_mireval(ann): patterns = defaultdict(lambda: defaultdict(list)) # Iterate over the data in interval-value format - for interval, observation in zip(*ann.data.to_interval_values()): + for interval, observation in zip(*ann.to_interval_values()): pattern_id = observation['pattern_id'] occurrence_id = observation['occurrence_id'] @@ -525,8 +525,8 @@ def transcription(ref, est, **kwargs): namespace = 'pitch_contour' ref = coerce_annotation(ref, namespace) est = coerce_annotation(est, namespace) - ref_intervals, ref_p = ref.data.to_interval_values() - est_intervals, est_p = est.data.to_interval_values() + ref_intervals, ref_p = ref.to_interval_values() + est_intervals, est_p = est.to_interval_values() ref_pitches = np.asarray([p['frequency'] * (-1)**(~p['voiced']) for p in ref_p]) est_pitches = np.asarray([p['frequency'] * (-1)**(~p['voiced']) for p in est_p]) diff --git a/jams/sonify.py b/jams/sonify.py index 530eaee4..a2697c67 100644 --- a/jams/sonify.py +++ b/jams/sonify.py @@ -43,7 +43,7 @@ def clicks(annotation, sr=22050, length=None, **kwargs): events such as beats or segment boundaries. ''' - interval, _ = annotation.data.to_interval_values() + interval, _ = annotation.to_interval_values() return filter_kwargs(mir_eval.sonify.clicks, interval[:, 0], fs=sr, length=length, **kwargs) @@ -56,7 +56,7 @@ def downbeat(annotation, sr=22050, length=None, **kwargs): beat_click = mkclick(440 * 2, sr=sr) downbeat_click = mkclick(440 * 3, sr=sr) - intervals, values = annotation.data.to_interval_values() + intervals, values = annotation.to_interval_values() beats, downbeats = [], [] @@ -109,7 +109,7 @@ def chord(annotation, sr=22050, length=None, **kwargs): This uses mir_eval.sonify.chords. ''' - intervals, chords = annotation.data.to_interval_values() + intervals, chords = annotation.to_interval_values() return filter_kwargs(mir_eval.sonify.chords, chords, intervals, @@ -127,7 +127,7 @@ def pitch_contour(annotation, sr=22050, length=None, **kwargs): are summed together. ''' - times, values = annotation.data.to_interval_values() + times, values = annotation.to_interval_values() indices = np.unique([v['index'] for v in values]) @@ -159,7 +159,7 @@ def piano_roll(annotation, sr=22050, length=None, **kwargs): namespace. ''' - intervals, pitches = annotation.data.to_interval_values() + intervals, pitches = annotation.to_interval_values() # Construct the pitchogram pitch_map = {f: idx for idx, f in enumerate(np.unique(pitches))} From 44e768a281c028ef92d4cc3db65b3a6744669ec6 Mon Sep 17 00:00:00 2001 From: Brian McFee Date: Sat, 13 May 2017 12:16:12 -0400 Subject: [PATCH 3/7] continuing deprecation march --- scripts/jams_to_lab.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/jams_to_lab.py b/scripts/jams_to_lab.py index b88e4095..b211b9db 100755 --- a/scripts/jams_to_lab.py +++ b/scripts/jams_to_lab.py @@ -6,7 +6,6 @@ import sys import os import json -import six import pandas as pd import jams @@ -56,6 +55,7 @@ def get_comments(jam, ann): 'annotation metadata': ann_comments}, indent=2) + def lab_dump(ann, comment, filename, sep, comment_char): '''Save an annotation as a lab/csv. @@ -77,11 +77,11 @@ def lab_dump(ann, comment, filename, sep, comment_char): The character used to denote comments ''' - intervals, values = ann.data.to_interval_values() + intervals, values = ann.to_interval_values() frame = pd.DataFrame(columns=['Time', 'End Time', 'Label'], data={'Time': intervals[:, 0], - 'End Time': intervals[:, 1], + 'End Time': intervals[:, 1], 'Label': values}) with open(filename, 'w') as fdesc: From 5b9fa66dfce4a7e4f107d3d940557162c6c9b6c3 Mon Sep 17 00:00:00 2001 From: Brian McFee Date: Sat, 13 May 2017 12:29:22 -0400 Subject: [PATCH 4/7] added observation and observation iterator --- jams/core.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/jams/core.py b/jams/core.py index a5467e0d..289266ba 100644 --- a/jams/core.py +++ b/jams/core.py @@ -29,7 +29,7 @@ JamsFrame Sandbox JObject - + Observation """ import json @@ -45,6 +45,7 @@ import gzip import copy import sys +from collections import namedtuple from decorator import decorator @@ -57,7 +58,8 @@ __all__ = ['load', 'JObject', 'Sandbox', 'JamsFrame', 'Annotation', 'Curator', 'AnnotationMetadata', - 'FileMetadata', 'AnnotationArray', 'JAMS'] + 'FileMetadata', 'AnnotationArray', 'JAMS', + 'Observation'] def deprecated(version, version_removed): @@ -515,6 +517,11 @@ def validate(self, strict=True): return valid +Observation = namedtuple('Observation', + ['time', 'duration', 'value', 'confidence']) +'''Core observation type: (time, duration, value, confidence).''' + + class Sandbox(JObject): """Sandbox (unconstrained) @@ -1214,6 +1221,15 @@ def to_interval_values(self): return np.vstack([times, times + duration]).T, list(self.data.value) + def __iter_obs__(self): + for _, (t, d, v, c) in self.data.iterrows(): + yield Observation(time=t.total_seconds(), + duration=d.total_seconds(), + value=v, confidence=c) + + def __iter__(self): + return self.__iter_obs__() + class Curator(JObject): """Curator From 3b315cd0690064cf0fdae5ff86f0cf4448f533c3 Mon Sep 17 00:00:00 2001 From: Brian McFee Date: Sat, 13 May 2017 12:33:38 -0400 Subject: [PATCH 5/7] test coverage for observation iterator --- tests/jams_test.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/jams_test.py b/tests/jams_test.py index 2a4c4f42..9d116a9f 100644 --- a/tests/jams_test.py +++ b/tests/jams_test.py @@ -376,6 +376,20 @@ def test_annotation_eq(): assert not (ann1 == ann2) +def test_annotation_iterator(): + + data = [dict(time=0, duration=0.5, value='one', confidence=0.2), + dict(time=1, duration=1, value='two', confidence=0.5)] + + namespace = 'tag_open' + + ann = jams.Annotation(namespace, data=data) + + for obs, obs_raw in zip(ann, data): + assert isinstance(obs, jams.Observation) + assert obs._asdict() == obs_raw, (obs, obs_raw) + + def test_annotation_interval_values(): data = dict(time=[0.0, 1.0], From 3aba29a45cb0aca9b4629d782b8cb522c2769085 Mon Sep 17 00:00:00 2001 From: Brian McFee Date: Sat, 13 May 2017 13:16:36 -0400 Subject: [PATCH 6/7] documentation and version updates --- docs/changes.rst | 47 ++++++++++++++++++++++++++++++++++------------- jams/version.py | 2 +- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index abd9c19a..e7a8fe0b 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -1,34 +1,55 @@ Changes ======= +v0.2.3 +------ + +- Deprecated the `JamsFrame` class + (`PR #153 `_): + + - Moved `JamsFrame.to_interval_values()` to `Annotation.to_interval_values()` + + - Any code that uses `pandas.DataFrame` methods on `Annotation.data` will cease to work + starting in 0.3.0. + +- Forward compatibility with 0.3.0 + (`PR #153 `_): + + - Added the `jams.Observation` type + + - Added iteration support to `Annotation` objects + +- added type safety check in regexp search (`PR #146 `_). +- added support for `pandas=0.20` (`PR #150 `_). + v0.2.2 ------ - added ``__contains__`` method to ``JObject`` - (`PR #139 `_). - Implemented ``JAMS.trim()`` method - (`PR #136 `_). - Updates to the SALAMI tag namespaces - (`PR #134 `_). - added `infer_duration` flag to ``import_lab`` - (`PR #125 `_). - namespace conversion validates input - (`PR #123 `_). - Refactored the ``pitch`` namespaces - (`PR #121 `_). - Fancy indexing for annotation arrays - (`PR #120 `_). - ``jams.schema.values`` function to access enumerated types - (`PR #119 `_). - ``jams.display`` submodule - (`PR #115 `_). - support for `mir_eval >= 0.3` - (`PR #106 `_). - Automatic conversion between namespaces - (`PR #105 `_). - Fixed a type error in ``jams_to_lab`` - (`PR #94 `_). - ``jams.sonify`` module for sonification - (`PR #91 `_). v0.2.1 ------ diff --git a/jams/version.py b/jams/version.py index 8c0da96f..dc2ba652 100644 --- a/jams/version.py +++ b/jams/version.py @@ -3,4 +3,4 @@ """Version info""" short_version = '0.2' -version = '0.2.2' +version = '0.2.3' From a54a0402eff6c43f4ec60823ba45c06ec4d937e4 Mon Sep 17 00:00:00 2001 From: Brian McFee Date: Sat, 13 May 2017 13:38:08 -0400 Subject: [PATCH 7/7] added python 3.6 to build matrix --- .travis.yml | 1 + setup.py | 1 + 2 files changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 7e5b21b3..4a537368 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,7 @@ python: - "2.7" - "3.4" - "3.5" + - "3.6" before_install: - bash .travis_dependencies.sh diff --git a/setup.py b/setup.py index bcdc1f7e..8ee80d38 100644 --- a/setup.py +++ b/setup.py @@ -27,6 +27,7 @@ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", ], keywords='audio music json', license='ISC',