# -*- coding: UTF-8 -*-
"""
:filename: sppas.src.anndata.annlocation.location.py
:author: Brigitte Bigi
:contact: develop@sppas.org
:summary: Represent the list of possible localizations of an annotation.
.. _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.
-------------------------------------------------------------------------
"""
import copy
from ...anndataexc import AnnDataTypeError
from .localization import sppasBaseLocalization
# ---------------------------------------------------------------------------
[docs]class sppasLocation(object):
"""Location of an annotation of a tier.
sppasLocation allows to store a set of localizations with their scores.
This class is using a list of lists, i.e. a list of pairs (localization,
score). This is the best compromise between memory usage, speed and
readability.
"""
[docs] def __init__(self, localization=None, score=None):
"""Create a new sppasLocation instance and add the entry.
:param localization: (Localization or list of localizations)
:param score: (float or list of float)
If a list of alternative localizations are given, the same score
is assigned to all items.
"""
self.__localizations = list()
if localization is not None:
if isinstance(localization, list):
if isinstance(score, list) and len(localization) == len(score):
for l, s in zip(localization, score):
self.append(l, s)
else:
for loc in localization:
self.append(loc, 1./len(localization))
else:
self.append(localization, score)
# -----------------------------------------------------------------------
[docs] def append(self, localization, score=None):
"""Add a localization into the list.
:param localization: (Localization) the localization to append
:param score: (float)
"""
if isinstance(localization, sppasBaseLocalization) is False:
raise AnnDataTypeError(localization, "sppasBaseLocalization")
if localization not in self.__localizations:
# check types consistency.
if len(self.__localizations) > 0:
if self.is_point() != localization.is_point():
raise AnnDataTypeError(localization, "sppasPoint")
if self.is_interval() != localization.is_interval():
raise AnnDataTypeError(localization, "sppasInterval")
if self.is_disjoint() != localization.is_disjoint():
raise AnnDataTypeError(localization, "sppasDisjoint")
self.__localizations.append([localization, score])
# -----------------------------------------------------------------------
[docs] def remove(self, localization):
"""Remove a localization of the list.
:param localization: (sppasLocalization) the loc to be removed
"""
if isinstance(localization, sppasBaseLocalization) is False:
raise AnnDataTypeError(localization, "sppasBaseLocalization")
if len(self.__localizations) == 1:
raise IndexError("A location must contain at least one "
"localization. The single one can't be removed.")
# self.__localizations = list()
else:
for l in self.__localizations:
if l[0] == localization:
self.__localizations.remove(l)
# -----------------------------------------------------------------------
[docs] def get_score(self, loc):
"""Return the score of a localization or None if it is not in.
:param loc: (sppasLocalization)
:returns: score: (float)
"""
if not isinstance(loc, sppasBaseLocalization):
raise AnnDataTypeError(loc, "sppasLocalization")
for l in self.__localizations:
if l[0] == loc:
return l[1]
return None
# -----------------------------------------------------------------------
[docs] def set_score(self, loc, score):
"""Set a score to a given localization.
:param loc: (sppasLocalization)
:param score: (float)
"""
if not isinstance(loc, sppasBaseLocalization):
raise AnnDataTypeError(loc, "sppasLocalization")
if self.__localizations is not None:
for i, l in enumerate(self.__localizations):
if l[0] == loc:
self.__localizations[i][1] = score
# -----------------------------------------------------------------------
[docs] def get_best(self):
"""Return a copy of the best localization.
:returns: (sppasLocalization) localization with the highest score.
"""
if len(self.__localizations) == 1:
return self.__localizations[0][0]
_max_t = self.__localizations[0][0]
_max_score = self.__localizations[0][1]
for (t, s) in reversed(self.__localizations):
if _max_score is None or (s is not None and s > _max_score):
_max_score = s
_max_t = t
return _max_t.copy()
# -----------------------------------------------------------------------
[docs] def is_point(self):
"""Return True if the location is made of sppasPoint localizations."""
return self.__localizations[0][0].is_point()
# -----------------------------------------------------------------------
[docs] def is_interval(self):
"""Return True if the location is made of sppasInterval locs."""
return self.__localizations[0][0].is_interval()
# -----------------------------------------------------------------------
[docs] def is_disjoint(self):
"""Return True if the location is made of sppasDisjoint locs."""
return self.__localizations[0][0].is_disjoint()
# -----------------------------------------------------------------------
[docs] def contains(self, point):
"""Return True if the localization point is in the list."""
if self.is_point():
return any([point == l[0] for l in self.__localizations])
else:
return any([l[0].is_bound(point) for l in self.__localizations])
# -----------------------------------------------------------------------
[docs] def copy(self):
"""Return a deep copy of the location."""
return copy.deepcopy(self)
# -----------------------------------------------------------------------
[docs] def get_lowest_localization(self):
"""Return a copy of the sppasPoint with the lowest localization."""
if self.is_point():
min_localization = min([l[0] for l in self.__localizations])
else:
min_localization = min([l[0].get_begin() for l in self.__localizations])
# We return a copy to be sure the original loc won't be modified
return min_localization.copy()
# -----------------------------------------------------------------------
[docs] def get_highest_localization(self):
"""Return a copy of the sppasPoint with the highest loc."""
if self.is_point():
max_localization = max([l[0] for l in self.__localizations])
else:
max_localization = max([l[0].get_end() for l in self.__localizations])
# We return a copy to ensure the original loc won't be modified
return max_localization.copy()
# -----------------------------------------------------------------------
[docs] def match_duration(self, dur_functions, logic_bool="and"):
"""Return True if a duration matches all or any of the functions.
:param dur_functions: list of (function, value, logical_not)
:param logic_bool: (str) Apply a logical "and" or "or"
:returns: (bool)
- function: a function in python with 2 arguments: dur/value
- value: the expected value for the duration (int/float/sppasDuration)
- logical_not: boolean
:Example: Search if a duration is exactly 30ms
>>> d.match([(eq, 0.03, False)])
:Example: Search if a duration is not 30ms
>>> d.match([(eq, 0.03, True)])
>>> d.match([(ne, 0.03, False)])
:Example: Search if a duration is comprised between 0.3 and 0.7
>>> l.match([(ge, 0.03, False),
>>> (le, 0.07, False)], logic_bool="and")
See sppasDurationCompare() to get a list of functions.
"""
is_matching = False
# any localization can match
for loc, score in self.__localizations:
dur = loc.duration()
matches = list()
for func, value, logical_not in dur_functions:
if logical_not is True:
matches.append(not func(dur, value))
else:
matches.append(func(dur, value))
if logic_bool == "and":
is_matching = all(matches)
else:
is_matching = any(matches)
# no need to test the next locs if the current one is matching.
if is_matching is True:
return True
return is_matching
# -----------------------------------------------------------------------
[docs] def match_localization(self, loc_functions, logic_bool="and"):
"""Return True if a localization matches all or any of the functions.
:param loc_functions: list of (function, value, logical_not)
:param logic_bool: (str) Apply a logical "and" or a logical "or"
between the functions.
:returns: (bool)
- function: a function in python with 2 arguments: loc/value
- value: the expected value for the localization (int/float/sppasPoint)
- logical_not: boolean
:Example: Search if a localization is after (or starts at) 1 minutes
>>> l.match([(rangefrom, 60., False)])
:Example: Search if a localization is before (or ends at) 3 minutes
>>> l.match([(rangeto, 180., True)])
:Example: Search if a localization is between 1 min and 3 min
>>> l.match([(rangefrom, 60., False),
>>> (rangeto, 180., False)], logic_bool="and")
See sppasLocalizationCompare() to get a list of functions.
"""
is_matching = False
# any localization can match
for loc, score in self.__localizations:
matches = list()
for func, value, logical_not in loc_functions:
if logical_not is True:
matches.append(not func(loc, value))
else:
matches.append(func(loc, value))
if logic_bool == "and":
is_matching = all(matches)
else:
is_matching = any(matches)
# no need to test the next locs if the current one is matching.
if is_matching is True:
return True
return is_matching
# -----------------------------------------------------------------------
[docs] def set_radius(self, radius):
"""Set a radius value to all localizations.
:param radius: (int, float) New radius value
:raise: AnnDataTypeError, AnnDataNegValueError
"""
for t, s in self.__localizations:
t.set_radius(radius)
# -----------------------------------------------------------------------
[docs] def shift(self, delay):
"""Shift the location to a given delay.
:param delay: (int, float) delay to shift all localizations
:raise: AnnDataTypeError
"""
for loc, score in self.__localizations:
loc.shift(delay)
# -----------------------------------------------------------------------
# Overloads
# -----------------------------------------------------------------------
def __format__(self, fmt):
return str(self).__format__(fmt)
# -----------------------------------------------------------------------
def __repr__(self, *args, **kwargs):
st = ""
for t, s in self.__localizations:
st += "sppasLocalization({!s:s}, score={:s}), ".format(t, s)
return st
# ------------------------------------------------------------------------
def __str__(self, *args, **kwargs):
st = ""
for t, s in self.__localizations:
st += "{!s:s}, {!s:s} ; ".format(t, s)
return st
# -----------------------------------------------------------------------
def __iter__(self):
for loc in self.__localizations:
yield loc
# -----------------------------------------------------------------------
def __getitem__(self, i):
return self.__localizations[i]
# -----------------------------------------------------------------------
def __len__(self):
return len(self.__localizations)
# -----------------------------------------------------------------------
def __eq__(self, other):
if len(self.__localizations) != len(other):
return False
for (l1, l2) in zip(self.__localizations, other):
if l1[0] != l2[0]:
return False
if l1[1] != l2[1]:
return False
return True
# -----------------------------------------------------------------------
def __ne__(self, other):
return not self == other