Source code for anndata.ann.annlocation.intervalcompare

# -*- coding: UTF-8 -*-
"""
:filename: sppas.src.anndata.annlocation.intervalcompare.py
:author: Brigitte Bigi
:contact: develop@sppas.org
:summary: Comparison methods of 2 intervals, used by the filter system.

.. _This file is part of SPPAS: http://www.sppas.org/
..
    -------------------------------------------------------------------------

     ___   __    __    __    ___
    /     |  \  |  \  |  \  /              the automatic
    \__   |__/  |__/  |___| \__             annotation and
       \  |     |     |   |    \             analysis
    ___/  |     |     |   | ___/              of speech

    Copyright (C) 2011-2021  Brigitte Bigi
    Laboratoire Parole et Langage, Aix-en-Provence, France

    Use of this software is governed by the GNU Public License, version 3.

    SPPAS 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.

    SPPAS 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 SPPAS. If not, see <http://www.gnu.org/licenses/>.

    This banner notice must not be removed.

    -------------------------------------------------------------------------

This class is inspired by both the "Allen's Interval Algebra" and INDU.
James Allen, in 1983, proposed an algebraic framework named Interval
Algebra (IA), for qualitative reasoning with time intervals where the
binary relationship between a pair of intervals is represented  by a
subset of 13 atomic relation, that are:

  - distinct because no pair of definite intervals can be related
  by more than one of the relationships;

  - exhaustive because any pair of definite intervals are described
  by one of the relations;

  - qualitative (rather than quantitative) because no numeric time
  spans are considered.

These relations and the operations on them form the "Allen's Interval Algebra".

Using this calculus, given facts can be formalized and then used for
automatic reasoning. Relations are: before, after, meets, met by,
overlaps, overlapped by, starts, started by, finishes, finished by,
contains, during and equals.

Pujari, Kumari and Sattar proposed INDU in 1999: an Interval & Duration
network. They extended the IA to model qualitative information about
intervals and durations in a single binary constraint network. Duration
relations are: greater, lower and equal.
INDU comprises of 25 basic relations between a pair of two intervals.

For convenience reasons, and because this class will be used to filter
annotated data (and not reasoning), it implements the following methods:

        'before'
        'before_equal'
        'before_greater'
        'before_lower'
        'after'
        'after_equal'
        'after_greater'
        'after_lower'
        'meets'
        'meets_equal'
        'meets_greater'
        'meets_lower'
        'metby'
        'metby_equal'
        'metby_greater'
        'metby_lower'
        'overlaps'
        'overlaps_equal'
        'overlaps_greater'
        'overlaps_lower'
        'overlappedby'
        'overlappedby_equal'
        'overlappedby_greater'
        'overlappedby_lower'
        'starts'
        'startedby'
        'finishes'
        'finishedby'
        'contains'
        'during'
        'equals'

So that they are not distinct. Some of them accept parameters so they
are not exhaustive too.

"""

from sppas.src.structs.basecompare import sppasBaseCompare

from ...anndataexc import AnnDataTypeError
from ...anndataexc import AnnDataValueError

from .point import sppasPoint
from .interval import sppasInterval
from .disjoint import sppasDisjoint
from .duration import sppasDuration

# ---------------------------------------------------------------------------


[docs]class sppasIntervalCompare(sppasBaseCompare): """SPPAS implementation of interval'comparisons. Includes "Allen's Interval Algebra" and INDU, with several options. This class can be used to compare any of the localization-derived classes: - sppasInterval(): begin and end points are used, - sppasDisjoint(): the first and the last points are used and then it\ is considered a full interval. - sppasPoint(): considered like a degenerated interval. """
[docs] def __init__(self): """Create a sppasIntervalCompare instance.""" super(sppasIntervalCompare, self).__init__() # Allen self.methods['before'] = sppasIntervalCompare.before self.methods['after'] = sppasIntervalCompare.after self.methods['meets'] = sppasIntervalCompare.meets self.methods['metby'] = sppasIntervalCompare.metby self.methods['overlaps'] = sppasIntervalCompare.overlaps self.methods['overlappedby'] = sppasIntervalCompare.overlappedby self.methods['starts'] = sppasIntervalCompare.starts self.methods['startedby'] = sppasIntervalCompare.startedby self.methods['finishes'] = sppasIntervalCompare.finishes self.methods['finishedby'] = sppasIntervalCompare.finishedby self.methods['during'] = sppasIntervalCompare.during self.methods['contains'] = sppasIntervalCompare.contains self.methods['equals'] = sppasIntervalCompare.equals # INDU self.methods['before_equal'] = sppasIntervalCompare.before_equal self.methods['before_greater'] = sppasIntervalCompare.before_greater self.methods['before_lower'] = sppasIntervalCompare.before_lower self.methods['after_equal'] = sppasIntervalCompare.after_equal self.methods['after_greater'] = sppasIntervalCompare.after_greater self.methods['after_lower'] = sppasIntervalCompare.after_lower self.methods['meets_equal'] = sppasIntervalCompare.meets_equal self.methods['meets_greater'] = sppasIntervalCompare.meets_greater self.methods['meets_lower'] = sppasIntervalCompare.meets_lower self.methods['metby_equal'] = sppasIntervalCompare.metby_equal self.methods['metby_greater'] = sppasIntervalCompare.metby_greater self.methods['metby_lower'] = sppasIntervalCompare.metby_lower self.methods['overlaps_equal'] = sppasIntervalCompare.overlaps_equal self.methods['overlaps_greater'] = sppasIntervalCompare.overlaps_greater self.methods['overlaps_lower'] = sppasIntervalCompare.overlaps_lower self.methods['overlappedby_equal'] = sppasIntervalCompare.overlappedby_equal self.methods['overlappedby_greater'] = sppasIntervalCompare.overlappedby_greater self.methods['overlappedby_lower'] = sppasIntervalCompare.overlappedby_lower
# ---------------------------------------------------------------------------
[docs] @staticmethod def before(i1, i2, max_delay=None, **kwargs): """Return True if i1 precedes i2. This is part of the Allen algebra. :param i1: |-------| :param i2: |-------| :param max_delay: (int/float/sppasDuration) Maximum delay between the \ end of i1 and the beginning of i2. :param **kwargs: un-used. """ x1, x2 = sppasIntervalCompare._unpack(i1) y1, y2 = sppasIntervalCompare._unpack(i2) is_before = x2 < y1 if is_before is True and max_delay is not None: delay = sppasInterval(x2, y1) return delay.duration() < max_delay return is_before
# ---------------------------------------------------------------------------
[docs] @staticmethod def before_equal(i1, i2, *args): """Return True if i1 precedes i2 and the durations are equals. This is part of the INDU algebra. :param i1: |-------| :param i2: |-------| :param max_delay: (int/float/sppasDuration) Maximum delay between the \ end of i1 and the beginning of i2. """ return sppasIntervalCompare.before(i1, i2, *args) and \ i1.duration() == i2.duration()
# ---------------------------------------------------------------------------
[docs] @staticmethod def before_greater(i1, i2, *args): """Return True if i1 precedes i2 and the duration of i1 is greater. This is part of the INDU algebra. :param i1: |-----------| :param i2: |-----| :param max_delay: (int/float/sppasDuration) Maximum delay between the \ end of i1 and the beginning of i2. """ return sppasIntervalCompare.before(i1, i2, *args) and \ i1.duration() > i2.duration()
# ---------------------------------------------------------------------------
[docs] @staticmethod def before_lower(i1, i2, *args): """Return True if i1 precedes i2 and the duration of i1 is lower. This is part of the INDU algebra. :param i1: |-----| :param i2: |------------| :param max_delay: (int/float/sppasDuration) Maximum delay between the \ end of i1 and the beginning of i2. """ return sppasIntervalCompare.before(i1, i2, *args) and \ i1.duration() < i2.duration()
# ---------------------------------------------------------------------------
[docs] @staticmethod def after(i1, i2, max_delay=None, **kwargs): """Return True if i1 follows i2. This is part of the Allen algebra. :param i1: |--------| :param i2: |-------| :param max_delay: (int/float/sppasDuration) Maximum delay between \ the end of i2 and the beginning of i1. :param **kwargs: unused. """ x1, x2 = sppasIntervalCompare._unpack(i1) y1, y2 = sppasIntervalCompare._unpack(i2) is_after = y2 < x1 if is_after and max_delay is not None: interval = sppasInterval(y2, x1) return interval.duration() < max_delay return is_after
# ---------------------------------------------------------------------------
[docs] @staticmethod def after_equal(i1, i2, *args): return sppasIntervalCompare.after(i1, i2, *args) and \ i1.duration() == i2.duration()
# ---------------------------------------------------------------------------
[docs] @staticmethod def after_greater(i1, i2, *args): return sppasIntervalCompare.after(i1, i2, *args) and \ i1.duration() > i2.duration()
# ---------------------------------------------------------------------------
[docs] @staticmethod def after_lower(i1, i2, *args): return sppasIntervalCompare.after(i1, i2, *args) and \ i1.duration() < i2.duration()
# ---------------------------------------------------------------------------
[docs] @staticmethod def meets(i1, i2, **kwargs): """Return True if i1 meets i2. :param i1: |-------| :param i2: |-------| :param **kwargs: unused. """ x1, x2 = sppasIntervalCompare._unpack(i1) y1, y2 = sppasIntervalCompare._unpack(i2) return not sppasIntervalCompare.equals(i1, i2) and x2 == y1
# ---------------------------------------------------------------------------
[docs] @staticmethod def meets_equal(i1, i2, **kwargs): return sppasIntervalCompare.meets(i1, i2) and \ i1.duration() == i2.duration()
# ---------------------------------------------------------------------------
[docs] @staticmethod def meets_greater(i1, i2, **kwargs): return sppasIntervalCompare.meets(i1, i2) and \ i1.duration() > i2.duration()
# ---------------------------------------------------------------------------
[docs] @staticmethod def meets_lower(i1, i2, **kwargs): return sppasIntervalCompare.meets(i1, i2) and \ i1.duration() < i2.duration()
# ---------------------------------------------------------------------------
[docs] @staticmethod def metby(i1, i2, **kwargs): """Return True if i1 is met by i2. :param i1: |-------| :param i2: |-------| :param **kwargs: unused. """ x1, x2 = sppasIntervalCompare._unpack(i1) y1, y2 = sppasIntervalCompare._unpack(i2) return not sppasIntervalCompare.equals(i1, i2) and x1 == y2
# ---------------------------------------------------------------------------
[docs] @staticmethod def metby_equal(i1, i2, **kwargs): return sppasIntervalCompare.metby(i1, i2) and \ i1.duration() == i2.duration()
# ---------------------------------------------------------------------------
[docs] @staticmethod def metby_greater(i1, i2, **kwargs): return sppasIntervalCompare.metby(i1, i2) and \ i1.duration() > i2.duration()
# ---------------------------------------------------------------------------
[docs] @staticmethod def metby_lower(i1, i2, **kwargs): return sppasIntervalCompare.metby(i1, i2) and \ i1.duration() < i2.duration()
# ---------------------------------------------------------------------------
[docs] @staticmethod def overlaps(i1, i2, overlap_min=None, percent=False, **kwargs): """Return True if i1 overlaps with i2. :param i1: |-------| :param i2: |------| :param overlap_min: (int/float/sppasDuration) Minimum duration of the \ overlap between i1 and i2. :param percent: (bool) The min_dur parameter is a percentage of i1, \ instead of an absolute duration. :param **kwargs: unused. """ x1, x2 = sppasIntervalCompare._unpack(i1) y1, y2 = sppasIntervalCompare._unpack(i2) is_overlap = x1 < y1 < x2 < y2 if is_overlap and overlap_min is not None: overlap_interval = sppasInterval(y1, x2) if percent is True: # relative duration (min_dur parameter represents a percentage of i1) if overlap_min < 0. or overlap_min > 100.: raise AnnDataValueError("min_dur/percentage", overlap_min) # relative duration (min_dur parameter represents a percentage of i1) v, m = i1.duration().get_value(), i1.duration().get_margin() duration = sppasDuration(v * float(overlap_min) / 100., m) else: # absolute duration duration = overlap_min return overlap_interval.duration() >= duration return is_overlap
# ---------------------------------------------------------------------------
[docs] @staticmethod def overlaps_equal(i1, i2, overlap_min=None, percent=False, **kwargs): return sppasIntervalCompare.overlaps(i1, i2, overlap_min, percent) and \ i1.duration() == i2.duration()
# ---------------------------------------------------------------------------
[docs] @staticmethod def overlaps_greater(i1, i2, overlap_min=None, percent=False, **kwargs): return sppasIntervalCompare.overlaps(i1, i2, overlap_min, percent) and \ i1.duration() > i2.duration()
# ---------------------------------------------------------------------------
[docs] @staticmethod def overlaps_lower(i1, i2, overlap_min=None, percent=False, **kwargs): return sppasIntervalCompare.overlaps(i1, i2, overlap_min, percent) and \ i1.duration() < i2.duration()
# ---------------------------------------------------------------------------
[docs] @staticmethod def overlappedby(i1, i2, overlapped_min=None, percent=False, **kwargs): """Return True if i1 overlapped by i2. :param i1: |-------| :param i2: |-------| :param overlapped_min: (int/float/sppasDuration) Minimum duration of the overlap between i1 and i2. :param percent: (bool) The min_dur parameter is a percentage of i1, instead of an absolute duration. :param **kwargs: unused. """ x1, x2 = sppasIntervalCompare._unpack(i1) y1, y2 = sppasIntervalCompare._unpack(i2) is_overlap = y1 < x1 < y2 < x2 if is_overlap and overlapped_min is not None: # create an interval of the overlap part. overlap_interval = sppasInterval(i1.get_begin(), i2.get_end()) if percent is True: if overlapped_min < 0. or overlapped_min > 100.: raise AnnDataValueError("min_dur/percentage", overlapped_min) # relative duration (min_dur parameter represents a percentage of i1) v, m = i1.duration().get_value(), i1.duration().get_margin() duration = sppasDuration(v * float(overlapped_min) / 100., m) else: # absolute duration # (min_dur parameter represents the minimum duration) duration = overlapped_min return overlap_interval.duration() >= duration return is_overlap
# ---------------------------------------------------------------------------
[docs] @staticmethod def overlappedby_equal(i1, i2, overlapped_min=None, percent=False, **kwargs): return sppasIntervalCompare.overlappedby(i1, i2, overlapped_min, percent) and \ i1.duration() == i2.duration()
# ---------------------------------------------------------------------------
[docs] @staticmethod def overlappedby_greater(i1, i2, overlapped_min=None, percent=False, **kwargs): return sppasIntervalCompare.overlappedby(i1, i2, overlapped_min, percent) and \ i1.duration() > i2.duration()
# ---------------------------------------------------------------------------
[docs] @staticmethod def overlappedby_lower(i1, i2, overlapped_min=None, percent=False, **kwargs): return sppasIntervalCompare.overlappedby(i1, i2, overlapped_min, percent) and \ i1.duration() < i2.duration()
# ---------------------------------------------------------------------------
[docs] @staticmethod def starts(i1, i2, **kwargs): """Return True if i1 starts at the start of i2 and finishes within it. :param i1: |----| :param i2: |----------| :param **kwargs: unused. """ x1, x2 = sppasIntervalCompare._unpack(i1) y1, y2 = sppasIntervalCompare._unpack(i2) return x1 == y1 and x2 < y2
# ---------------------------------------------------------------------------
[docs] @staticmethod def startedby(i1, i2, **kwargs): """Return True if i1 is started at the start of i2 interval. :param i1: |----------| :param i2: |----| :param **kwargs: unused. """ x1, x2 = sppasIntervalCompare._unpack(i1) y1, y2 = sppasIntervalCompare._unpack(i2) return x1 == y1 and y2 < x2
# ---------------------------------------------------------------------------
[docs] @staticmethod def finishes(i1, i2, **kwargs): """Return True if i1 finishes the same and starts within of i2. :param i1: |----| :param i2: |---------| :param **kwargs: unused. """ x1, x2 = sppasIntervalCompare._unpack(i1) y1, y2 = sppasIntervalCompare._unpack(i2) return y1 < x1 and x2 == y2
# ---------------------------------------------------------------------------
[docs] @staticmethod def finishedby(i1, i2, **kwargs): """Return True if i1 finishes the same and starts before of i2. :param i1: |---------| :param i2: |----| :param **kwargs: unused. """ x1, x2 = sppasIntervalCompare._unpack(i1) y1, y2 = sppasIntervalCompare._unpack(i2) return x1 < y1 and x2 == y2
# ---------------------------------------------------------------------------
[docs] @staticmethod def during(i1, i2, **kwargs): """Return True if i1 is located during i2. :param i1: |----| :param i2: |------------| :param **kwargs: unused. """ x1, x2 = sppasIntervalCompare._unpack(i1) y1, y2 = sppasIntervalCompare._unpack(i2) return y1 < x1 and x2 < y2
# ---------------------------------------------------------------------------
[docs] @staticmethod def contains(i1, i2, **kwargs): """Return True if i1 contains i2. :param i1: |------------| :param i2: |----| :param **kwargs: unused. """ x1, x2 = sppasIntervalCompare._unpack(i1) y1, y2 = sppasIntervalCompare._unpack(i2) return x1 < y1 and y2 < x2
# ---------------------------------------------------------------------------
[docs] @staticmethod def equals(i1, i2, **kwargs): """Return True if i1 equals i2. :param i1: |-------| :param i2: |-------| :param **kwargs: unused. """ x1, x2 = sppasIntervalCompare._unpack(i1) y1, y2 = sppasIntervalCompare._unpack(i2) return x1 == y1 and x2 == y2
# --------------------------------------------------------------------------- # Private # --------------------------------------------------------------------------- @staticmethod def _unpack(localization): """Return the 2 extremities of a localization.""" if isinstance(localization, (sppasInterval, sppasDisjoint)): return localization.get_begin(), localization.get_end() elif isinstance(localization, sppasPoint): return localization, localization raise AnnDataTypeError(localization, "sppasBaseLocalization")