praatio.data_classes.interval_tier API documentation

# praatio.data_classes.interval_tier

An IntervalTier is a tier containing an array of intervals -- data that spans a period of time

View Source

      1"""
      2An IntervalTier is a tier containing an array of intervals -- data that spans a period of time
      3"""
      4from typing import Callable, List, Optional, Tuple, Sequence
      5
      6from typing_extensions import Literal
      7
      8
      9from praatio.utilities.constants import (
     10    Interval,
     11    INTERVAL_TIER,
     12    CropCollision,
     13)
     14
     15from praatio.utilities import errors
     16from praatio.utilities import utils
     17from praatio.utilities import my_math
     18from praatio.utilities import constants
     19
     20from praatio.data_classes import textgrid_tier
     21
     22
     23def _homogenizeEntries(entries):
     24    """
     25    Enforces consistency in intervals
     26
     27    - converts all entries to intervals
     28    - removes whitespace in labels
     29    - sorts values by time
     30    """
     31    processedEntries = [
     32        Interval(float(start), float(end), label.strip())
     33        for start, end, label in entries
     34    ]
     35    processedEntries.sort()
     36    return processedEntries
     37
     38
     39def _calculateMinAndMaxTime(entries: Sequence[Interval], minT=None, maxT=None):
     40    minTimeList = [interval.start for interval in entries]
     41    maxTimeList = [interval.end for interval in entries]
     42
     43    if minT is not None:
     44        minTimeList.append(float(minT))
     45    if maxT is not None:
     46        maxTimeList.append(float(maxT))
     47
     48    try:
     49        resolvedMinT = min(minTimeList)
     50        resolvedMaxT = max(maxTimeList)
     51    except ValueError:
     52        raise errors.TimelessTextgridTierException()
     53
     54    return (resolvedMinT, resolvedMaxT)
     55
     56
     57class IntervalTier(textgrid_tier.TextgridTier):
     58    tierType = INTERVAL_TIER
     59    entryType = Interval
     60
     61    def __init__(
     62        self,
     63        name: str,
     64        entries: List[Interval],
     65        minT: Optional[float] = None,
     66        maxT: Optional[float] = None,
     67    ):
     68        """An interval tier is for annotating events that have duration
     69
     70        The entries is of the form:
     71        [(startTime1, endTime1, label1), (startTime2, endTime2, label2), ]
     72
     73        The data stored in the labels can be anything but will
     74        be interpreted as text by praatio (the label could be descriptive
     75        text e.g. ('erase this region') or numerical data e.g. (average pitch
     76        values like '132'))
     77        """
     78        entries = _homogenizeEntries(entries)
     79        calculatedMinT, calculatedMaxT = _calculateMinAndMaxTime(entries, minT, maxT)
     80
     81        super(IntervalTier, self).__init__(
     82            name, entries, calculatedMinT, calculatedMaxT
     83        )
     84        self._validate()
     85
     86    def _validate(self):
     87        """An interval tier is invalid if the entries are out of order or overlap with each other"""
     88        for entry in self.entries:
     89            if entry.start >= entry.end:
     90                raise errors.TextgridStateError(
     91                    f"The start time of an interval ({entry.start}) "
     92                    f"cannot occur after its end time ({entry.end})"
     93                )
     94
     95        for entry, nextEntry in zip(self.entries[0::], self.entries[1::]):
     96            if entry.end > nextEntry.start:
     97                raise errors.TextgridStateError(
     98                    "Two intervals in the same tier overlap in time:\n"
     99                    f"({entry.start}, {entry.end}, {entry.label}) and "
    100                    f"({nextEntry.start}, {nextEntry.end}, {nextEntry.label})"
    101                )
    102
    103    @property
    104    def timestamps(self) -> List[float]:
    105        """All unique timestamps used in this tier"""
    106        tmpTimestamps = [
    107            time
    108            for start, stop, _ in self.entries
    109            for time in [
    110                start,
    111                stop,
    112            ]
    113        ]
    114
    115        uniqueTimestamps = list(set(tmpTimestamps))
    116        uniqueTimestamps.sort()
    117
    118        return uniqueTimestamps
    119
    120    def crop(
    121        self,
    122        cropStart: float,
    123        cropEnd: float,
    124        mode: Literal["strict", "lax", "truncated"],
    125        rebaseToZero: bool,
    126    ) -> "IntervalTier":
    127        """Creates a new tier with all entries that fit inside the new interval
    128
    129        Args:
    130            cropStart:
    131            cropEnd:
    132            mode: determines cropping behavior
    133                - 'strict', only intervals wholly contained by the crop
    134                    interval will be kept
    135                - 'lax', partially contained intervals will be kept
    136                - 'truncated', partially contained intervals will be
    137                    truncated to fit within the crop region.
    138            rebaseToZero: if True, the cropped textgrid values
    139                will be subtracted by the cropStart
    140
    141        Returns:
    142            the modified version of the current tier
    143        """
    144
    145        utils.validateOption("mode", mode, CropCollision)
    146
    147        if cropStart >= cropEnd:
    148            raise errors.ArgumentError(
    149                f"Crop error: start time ({cropStart}) must occur before end time ({cropEnd})"
    150            )
    151
    152        newEntryList = utils.getIntervalsInInterval(
    153            cropStart, cropEnd, self.entries, mode
    154        )
    155
    156        if rebaseToZero is True:
    157            newSmallestValue = newEntryList[0][0]
    158            if newSmallestValue < cropStart:
    159                timeDiff = newSmallestValue
    160            else:
    161                timeDiff = cropStart
    162            newEntryList = [
    163                Interval(start - timeDiff, end - timeDiff, label)
    164                for start, end, label in newEntryList
    165            ]
    166            minT = 0.0
    167            maxT = cropEnd - cropStart
    168        else:
    169            minT = cropStart
    170            maxT = cropEnd
    171
    172        croppedTier = IntervalTier(self.name, newEntryList, minT, maxT)
    173
    174        return croppedTier
    175
    176    def dejitter(
    177        self,
    178        referenceTier: textgrid_tier.TextgridTier,
    179        maxDifference: float = 0.001,
    180    ) -> textgrid_tier.TextgridTier:
    181        """
    182        Set timestamps in this tier to be the same as values in the reference tier
    183
    184        Timestamps will only be moved if they are less than maxDifference away from the
    185        reference time.
    186
    187        This can be used to correct minor alignment errors between tiers, as made when
    188        annotating files manually, etc.
    189
    190        Args:
    191            referenceTier: the IntervalTier or PointTier to use as a reference
    192            maxDifference: the maximum amount to allow timestamps to be moved by
    193
    194        Returns:
    195            the modified version of the current tier
    196        """
    197        referenceTimestamps = referenceTier.timestamps
    198
    199        newEntries = []
    200        for start, stop, label in self.entries:
    201            startCompare = min(referenceTimestamps, key=lambda x: abs(x - start))
    202            stopCompare = min(referenceTimestamps, key=lambda x: abs(x - stop))
    203
    204            if my_math.lessThanOrEqual(abs(start - startCompare), maxDifference):
    205                start = startCompare
    206            if my_math.lessThanOrEqual(abs(stop - stopCompare), maxDifference):
    207                stop = stopCompare
    208            newEntries.append((start, stop, label))
    209
    210        return self.new(entries=newEntries)
    211
    212    def deleteEntry(self, entry: Interval) -> None:
    213        """Removes an entry from the entries"""
    214        self._entries.pop(self._entries.index(entry))
    215
    216    def difference(self, tier: "IntervalTier") -> "IntervalTier":
    217        """Takes the set difference of this tier and the given one
    218
    219        Any overlapping portions of entries with entries in this textgrid
    220        will be removed from the returned tier.
    221
    222        Args:
    223            tier: the tier to subtract from this one
    224
    225        Returns:
    226            the modified version of the current tier
    227        """
    228        retTier = self.new()
    229
    230        for entry in tier.entries:
    231            retTier = retTier.eraseRegion(
    232                entry.start,
    233                entry.end,
    234                collisionMode=constants.EraseCollision.TRUNCATE,
    235                doShrink=False,
    236            )
    237
    238        return retTier
    239
    240    def editTimestamps(
    241        self,
    242        offset: float,
    243        reportingMode: Literal["silence", "warning", "error"] = "warning",
    244    ) -> "IntervalTier":
    245        """Modifies all timestamps by a constant amount
    246
    247        Args:
    248            offset: the amount to shift all intervals
    249            reportingMode: Determines the behavior if an entries moves outside
    250                of minTimestamp or maxTimestamp after being edited
    251
    252        Returns:
    253            the modified version of the current tier
    254        """
    255        utils.validateOption(
    256            "reportingMode", reportingMode, constants.ErrorReportingMode
    257        )
    258        errorReporter = utils.getErrorReporter(reportingMode)
    259
    260        newEntryList = []
    261        for interval in self.entries:
    262            newStart = offset + interval.start
    263            newEnd = offset + interval.end
    264
    265            utils.checkIsUndershoot(newStart, self.minTimestamp, errorReporter)
    266            utils.checkIsOvershoot(newEnd, self.maxTimestamp, errorReporter)
    267
    268            if newEnd <= 0:
    269                continue
    270            if newStart < 0:
    271                newStart = 0
    272
    273            newEntryList.append(Interval(newStart, newEnd, interval.label))
    274
    275        # Determine new min and max timestamps
    276        newMin = min([interval.start for interval in newEntryList])
    277        newMax = max([interval.end for interval in newEntryList])
    278
    279        if newMin > self.minTimestamp:
    280            newMin = self.minTimestamp
    281
    282        if newMax < self.maxTimestamp:
    283            newMax = self.maxTimestamp
    284
    285        return IntervalTier(self.name, newEntryList, newMin, newMax)
    286
    287    def eraseRegion(
    288        self,
    289        start: float,
    290        end: float,
    291        collisionMode: Literal["truncate", "categorical", "error"] = "error",
    292        doShrink: bool = True,
    293    ) -> "IntervalTier":
    294        """Makes a region in a tier blank (removes all contained entries)
    295
    296        Args:
    297            start:
    298            end:
    299            collisionMode: Determines the behavior when the region to erase
    300                overlaps with existing intervals.
    301                - 'truncate' partially contained entries will have the portion
    302                    removed that overlaps with the target entry
    303                - 'categorical' all entries that overlap, even partially, with
    304                    the target entry will be completely removed
    305                - None or any other value throws IntervalCollision
    306            doShrink: If True, moves leftward by (/end/ - /start/)
    307                amount, each item that occurs after /end/
    308
    309        Returns:
    310            The modified version of the current tier
    311
    312        Raises:
    313            CollisionError
    314        """
    315        utils.validateOption("collisionMode", collisionMode, constants.EraseCollision)
    316
    317        matchList = self.crop(start, end, CropCollision.LAX, False).entries
    318        newTier = self.new()
    319
    320        if len(matchList) == 0:
    321            pass
    322        else:
    323            if collisionMode == constants.EraseCollision.ERROR:
    324                raise errors.CollisionError(
    325                    f"Erase region ({start}, {end})overlapped with an interval. "
    326                    "If this was expected, consider setting the collisionMode"
    327                )
    328
    329            # Remove all the matches from the entries
    330            # Go in reverse order because we're destructively altering
    331            # the order of the list (messes up index order)
    332            for interval in matchList[::-1]:
    333                newTier.deleteEntry(interval)
    334
    335            # If we're only truncating, reinsert entries on the left and
    336            # right edges
    337            # if categorical, it doesn't make it into the list at all
    338            if collisionMode == constants.EraseCollision.TRUNCATE:
    339                # Check left edge
    340                if matchList[0].start < start:
    341                    newEntry = Interval(matchList[0].start, start, matchList[0].label)
    342                    newTier.insertEntry(newEntry)
    343
    344                # Check right edge
    345                if matchList[-1].end > end:
    346                    newEntry = Interval(end, matchList[-1].end, matchList[-1].label)
    347                    newTier.insertEntry(newEntry)
    348
    349        if doShrink is True:
    350            diff = end - start
    351            newEntryList = []
    352            for interval in newTier.entries:
    353                if interval.end <= start:
    354                    newEntryList.append(interval)
    355                elif interval.start >= end:
    356                    newEntryList.append(
    357                        Interval(
    358                            interval.start - diff, interval.end - diff, interval.label
    359                        )
    360                    )
    361
    362            # Special case: an interval that spanned the deleted
    363            # section
    364            for i in range(0, len(newEntryList) - 1):
    365                rightEdge = newEntryList[i].end == start
    366                leftEdge = newEntryList[i + 1].start == start
    367                sameLabel = newEntryList[i].label == newEntryList[i + 1].label
    368                if rightEdge and leftEdge and sameLabel:
    369                    newInterval = Interval(
    370                        newEntryList[i].start,
    371                        newEntryList[i + 1].end,
    372                        newEntryList[i].label,
    373                    )
    374
    375                    newEntryList.pop(i + 1)
    376                    newEntryList.pop(i)
    377                    newEntryList.insert(i, newInterval)
    378
    379                    # Only one interval can span the deleted section,
    380                    # so if we've found it, move on
    381                    break
    382
    383            newMax = newTier.maxTimestamp - diff
    384            newTier = newTier.new(entries=newEntryList, maxTimestamp=newMax)
    385
    386        return newTier
    387
    388    def getValuesInIntervals(self, dataTupleList: List) -> List[Tuple[Interval, List]]:
    389        """Returns data from dataTupleList contained in labeled intervals
    390
    391        Each labeled interval will get its own list of data values.
    392
    393        dataTupleList should be of the form:
    394        [(time1, value1a, value1b,...), (time2, value2a, value2b...), ...]
    395        """
    396
    397        returnList = []
    398
    399        for interval in self.entries:
    400            intervalDataList = utils.getValuesInInterval(
    401                dataTupleList, interval.start, interval.end
    402            )
    403            returnList.append((interval, intervalDataList))
    404
    405        return returnList
    406
    407    def getNonEntries(self) -> List[Interval]:
    408        """Returns the regions of the textgrid without labels
    409
    410        This can include unlabeled segments and regions marked as silent.
    411        """
    412        entries = self.entries
    413        invertedEntryList = [
    414            Interval(entries[i].end, entries[i + 1].start, "")
    415            for i in range(len(entries) - 1)
    416        ]
    417
    418        # Remove entries that have no duration (ie lie between two entries
    419        # that share a border)
    420        invertedEntryList = [
    421            interval for interval in invertedEntryList if interval.start < interval.end
    422        ]
    423
    424        if entries[0].start > 0:
    425            invertedEntryList.insert(0, Interval(0, entries[0].start, ""))
    426
    427        if entries[-1].end < self.maxTimestamp:
    428            invertedEntryList.append(Interval(entries[-1].end, self.maxTimestamp, ""))
    429
    430        invertedEntryList = [
    431            interval if isinstance(interval, Interval) else Interval(*interval)
    432            for interval in invertedEntryList
    433        ]
    434
    435        return invertedEntryList
    436
    437    def insertEntry(
    438        self,
    439        entry: Interval,
    440        collisionMode: Literal["replace", "merge", "error"] = "error",
    441        collisionReportingMode: Literal["silence", "warning"] = "warning",
    442    ) -> None:
    443        """Inserts an interval into the tier
    444
    445        Args:
    446            entry: the Interval to insert
    447            collisionMode: determines the behavior in the event that intervals
    448                exist in the insertion area.
    449                - 'replace' will remove existing items
    450                - 'merge' will fuse the inserting item with existing items
    451                - None or any other value will throw a CollisionError
    452            collisionReportingMode: Determines the behavior if the new entry
    453                overlaps with an existing one
    454
    455        Returns:
    456            the modified version of the current tier
    457        """
    458        utils.validateOption(
    459            "collisionMode", collisionMode, constants.IntervalCollision
    460        )
    461        utils.validateOption(
    462            "collisionReportingMode",
    463            collisionReportingMode,
    464            constants.ErrorReportingMode,
    465        )
    466        collisionReporter = utils.getErrorReporter(collisionReportingMode)
    467
    468        if not isinstance(entry, Interval):
    469            interval = Interval(*entry)
    470        else:
    471            interval = entry
    472
    473        matchList = self.crop(
    474            interval.start, interval.end, CropCollision.LAX, False
    475        )._entries
    476
    477        if len(matchList) == 0:
    478            self._entries.append(interval)
    479
    480        elif collisionMode == constants.IntervalCollision.REPLACE:
    481            for matchEntry in matchList:
    482                self.deleteEntry(matchEntry)
    483            self._entries.append(interval)
    484
    485        elif collisionMode == constants.IntervalCollision.MERGE:
    486            for matchEntry in matchList:
    487                self.deleteEntry(matchEntry)
    488            matchList.append(interval)
    489            matchList.sort()  # By starting time
    490
    491            newInterval = Interval(
    492                min([tmpInterval.start for tmpInterval in matchList]),
    493                max([tmpInterval.end for tmpInterval in matchList]),
    494                "-".join([tmpInterval.label for tmpInterval in matchList]),
    495            )
    496            self._entries.append(newInterval)
    497
    498        else:
    499            raise errors.CollisionError(
    500                "Attempted to insert interval "
    501                f"({interval.start}, {interval.end}, '{interval.label}') into tier {self.name} "
    502                "of textgrid but overlapping entries "
    503                f"{[tuple(interval) for interval in matchList]} "
    504                "already exist"
    505            )
    506
    507        self.sort()
    508
    509        if self._entries[0][0] < self.minTimestamp:
    510            self.minTimestamp = self._entries[0][0]
    511
    512        if self._entries[-1][1] > self.maxTimestamp:
    513            self.maxTimestamp = self._entries[-1][1]
    514
    515        if len(matchList) != 0:
    516            collisionReporter(
    517                errors.CollisionError,
    518                f"Collision warning for ({interval}) with items "
    519                f"({matchList}) of tier '{self.name}'",
    520            )
    521
    522    def insertSpace(
    523        self,
    524        start: float,
    525        duration: float,
    526        collisionMode: Literal["stretch", "split", "no_change", "error"],
    527    ) -> "IntervalTier":
    528        """Inserts a blank region into the tier
    529
    530        Args:
    531            start:
    532            duration:
    533            collisionMode: Determines the behavior that occurs if
    534                an interval stradles the starting point
    535                - 'stretch' stretches the interval by /duration/ amount
    536                - 'split' splits the interval into two--everything to the
    537                    right of 'start' will be advanced by 'duration' seconds
    538                - 'no change' leaves the interval as is with no change
    539                - 'error' will stop execution and raise an error
    540
    541        Returns:
    542            the modified version of the current tier
    543        """
    544        utils.validateOption(
    545            "collisionMode", collisionMode, constants.WhitespaceCollision
    546        )
    547
    548        newEntryList = []
    549        for interval in self.entries:
    550            # Entry exists before the insertion point
    551            if interval.end <= start:
    552                newEntryList.append(interval)
    553            # Entry exists after the insertion point
    554            elif interval.start >= start:
    555                newEntryList.append(
    556                    Interval(
    557                        interval.start + duration,
    558                        interval.end + duration,
    559                        interval.label,
    560                    )
    561                )
    562            # Entry straddles the insertion point
    563            elif interval.start <= start and interval.end > start:
    564                if collisionMode == constants.WhitespaceCollision.STRETCH:
    565                    newEntryList.append(
    566                        Interval(
    567                            interval.start, interval.end + duration, interval.label
    568                        )
    569                    )
    570                elif collisionMode == constants.WhitespaceCollision.SPLIT:
    571                    # Left side of the split
    572                    newEntryList.append(Interval(interval.start, start, interval.label))
    573                    # Right side of the split
    574                    newEntryList.append(
    575                        (
    576                            start + duration,
    577                            start + duration + (interval.end - start),
    578                            interval.label,
    579                        )
    580                    )
    581                elif collisionMode == constants.WhitespaceCollision.NO_CHANGE:
    582                    newEntryList.append(interval)
    583                else:
    584                    raise errors.ArgumentError(
    585                        f"Collision occured during insertSpace() for interval '{interval}' "
    586                        f"and given white space insertion interval ({start}, {start + duration})"
    587                    )
    588
    589        newTier = self.new(
    590            entries=newEntryList, maxTimestamp=self.maxTimestamp + duration
    591        )
    592
    593        return newTier
    594
    595    def intersection(self, tier: "IntervalTier", demarcator="-") -> "IntervalTier":
    596        """Takes the set intersection of this tier and the given one
    597
    598        - The output will contain one interval for each overlapping pair
    599          e.g. [(1, 2, 'foo')] and [(1, 1.3, 'bang'), (1.7, 2, 'wizz')]
    600                -> [(1, 1.3, 'foo-bang'), (1.7, 2, 'foo-wizz')]
    601        - Only intervals that exist in both tiers will remain in the returned tier.
    602          e.g. [(1, 2, 'foo'), (3, 4, 'bar')] and [(1, 2, 'bang'), (2, 3, 'wizz')]
    603                -> [(1, 2, 'foo-bang')]
    604        - If intervals partially overlap, only the overlapping portion will be returned.
    605          e.g. [(1, 2, 'foo')] and [(0.5, 1.5, 'bang')]
    606                -> [(1, 1.5, 'foo-bang')]
    607
    608        Compare with IntervalTier.mergeLabels
    609
    610        Args:
    611            tier: the tier to intersect with
    612            demarcator: the character to separate the labels of the overlapping intervals
    613
    614        Returns:
    615            IntervalTier: the modified version of the current tier
    616        """
    617        retEntryList = []
    618        for interval in tier.entries:
    619            subTier = self.crop(
    620                interval.start, interval.end, CropCollision.TRUNCATED, False
    621            )
    622
    623            # Combine the labels in the two tiers
    624            subEntryList = [
    625                (
    626                    subInterval.start,
    627                    subInterval.end,
    628                    f"{subInterval.label}{demarcator}{interval.label}",
    629                )
    630                for subInterval in subTier.entries
    631            ]
    632
    633            retEntryList.extend(subEntryList)
    634
    635        newName = f"{self.name}-{tier.name}"
    636
    637        retTier = self.new(newName, retEntryList)
    638
    639        return retTier
    640
    641    def mergeLabels(
    642        self, tier: "IntervalTier", demarcator: str = ","
    643    ) -> "IntervalTier":
    644        """Merges labels of overlapping tiers into this tier
    645
    646        - All intervals in this tier will appear in the output; for the given tier, only intervals
    647          that overlap with content in this tier will appear in the output
    648          e.g. [(1, 2, 'foo'), (3, 4, 'bar')] and [(1, 2, 'bang'), (2, 3, 'wizz')]
    649                -> [(1, 2, 'foo(bang)'), (3, 4, 'bar()')]
    650        - If multiple entries exist in a subinterval, their labels will be concatenated
    651          e.g. [(1, 2, 'hi')] and [(1, 1.5, 'h'), (1.5, 2, 'ai')] -> [(1, 2, 'hi(h,ai)')]
    652
    653        compare with IntervalTier.intersection
    654
    655        Args:
    656            tier: the tier to intersect with
    657            demarcator: the string to separate items that fall in the same subinterval
    658
    659        Returns:
    660            IntervalTier: the modified version of the current tier
    661        """
    662        retEntryList = []
    663        for interval in self.entries:
    664            subTier = tier.crop(
    665                interval.start, interval.end, CropCollision.TRUNCATED, False
    666            )
    667            if len(subTier._entries) == 0:
    668                continue
    669
    670            subLabel = demarcator.join([entry.label for entry in subTier.entries])
    671            label = f"{interval.label}({subLabel})"
    672
    673            start = min(interval.start, subTier._entries[0].start)
    674            end = max(interval.end, subTier._entries[-1].end)
    675
    676            intersectedInterval = (
    677                start,
    678                end,
    679                label,
    680            )
    681
    682            retEntryList.append(intersectedInterval)
    683
    684        newName = f"{self.name}-{tier.name}"
    685
    686        retTier = self.new(newName, retEntryList)
    687
    688        return retTier
    689
    690    def morph(
    691        self,
    692        targetTier: "IntervalTier",
    693        filterFunc: Optional[Callable[[str], bool]] = None,
    694    ) -> "IntervalTier":
    695        """Morphs the duration of segments in this tier to those in another
    696
    697        This preserves the labels and the duration of silence in
    698        this tier while changing the duration of labeled segments.
    699
    700        Args:
    701            targetTier:
    702            filterFunc: if specified, filters entries. The
    703                functor takes one argument, an Interval. It returns true
    704                if the Interval should be modified and false if not.
    705
    706        Returns:
    707            The modified version of the current tier
    708        """
    709        cumulativeAdjustAmount = 0
    710        newEntryList = []
    711        allIntervals = [self.entries, targetTier.entries]
    712        for sourceInterval, targetInterval in utils.safeZip(allIntervals, True):
    713            # sourceInterval.start - lastFromEnd -> was this interval and the
    714            # last one adjacent?
    715            newStart = sourceInterval.start + cumulativeAdjustAmount
    716
    717            currIntervalDuration = sourceInterval.end - sourceInterval.start
    718            if filterFunc is None or filterFunc(sourceInterval.label):
    719                newIntervalDuration = targetInterval.end - targetInterval.start
    720                cumulativeAdjustAmount += newIntervalDuration - currIntervalDuration
    721                newEnd = newStart + newIntervalDuration
    722            else:
    723                newEnd = newStart + currIntervalDuration
    724
    725            newEntryList.append(Interval(newStart, newEnd, sourceInterval.label))
    726
    727        newMin = self.minTimestamp
    728        cumulativeDifference = newEntryList[-1].end - self.entries[-1].end
    729        newMax = self.maxTimestamp + cumulativeDifference
    730
    731        return IntervalTier(self.name, newEntryList, newMin, newMax)
    732
    733    def validate(
    734        self, reportingMode: Literal["silence", "warning", "error"] = "warning"
    735    ) -> bool:
    736        """Validate this tier
    737
    738        Args:
    739            reportingMode (str): Determines the behavior if validation fails.
    740
    741        Returns:
    742            True if the tier is valid; False if not
    743        """
    744        utils.validateOption(
    745            "reportingMode", reportingMode, constants.ErrorReportingMode
    746        )
    747        errorReporter = utils.getErrorReporter(reportingMode)
    748
    749        isValid = True
    750        previousInterval = None
    751        for interval in self.entries:
    752            if interval.start >= interval.end:
    753                isValid = False
    754                errorReporter(
    755                    errors.TextgridStateError,
    756                    f"Invalid interval. End time occurs before or on the start time({interval}).",
    757                )
    758
    759            if previousInterval and previousInterval.end > interval.start:
    760                isValid = False
    761                errorReporter(
    762                    errors.TextgridStateError,
    763                    f"Intervals are not sorted in time: "
    764                    f"[({previousInterval}), ({interval})]",
    765                )
    766
    767            if utils.checkIsUndershoot(
    768                interval.start, self.minTimestamp, errorReporter
    769            ):
    770                isValid = False
    771
    772            if utils.checkIsOvershoot(interval.end, self.maxTimestamp, errorReporter):
    773                isValid = False
    774
    775            previousInterval = interval
    776
    777        return isValid

class IntervalTier(praatio.data_classes.textgrid_tier.TextgridTier): View Source

     58class IntervalTier(textgrid_tier.TextgridTier):
     59    tierType = INTERVAL_TIER
     60    entryType = Interval
     61
     62    def __init__(
     63        self,
     64        name: str,
     65        entries: List[Interval],
     66        minT: Optional[float] = None,
     67        maxT: Optional[float] = None,
     68    ):
     69        """An interval tier is for annotating events that have duration
     70
     71        The entries is of the form:
     72        [(startTime1, endTime1, label1), (startTime2, endTime2, label2), ]
     73
     74        The data stored in the labels can be anything but will
     75        be interpreted as text by praatio (the label could be descriptive
     76        text e.g. ('erase this region') or numerical data e.g. (average pitch
     77        values like '132'))
     78        """
     79        entries = _homogenizeEntries(entries)
     80        calculatedMinT, calculatedMaxT = _calculateMinAndMaxTime(entries, minT, maxT)
     81
     82        super(IntervalTier, self).__init__(
     83            name, entries, calculatedMinT, calculatedMaxT
     84        )
     85        self._validate()
     86
     87    def _validate(self):
     88        """An interval tier is invalid if the entries are out of order or overlap with each other"""
     89        for entry in self.entries:
     90            if entry.start >= entry.end:
     91                raise errors.TextgridStateError(
     92                    f"The start time of an interval ({entry.start}) "
     93                    f"cannot occur after its end time ({entry.end})"
     94                )
     95
     96        for entry, nextEntry in zip(self.entries[0::], self.entries[1::]):
     97            if entry.end > nextEntry.start:
     98                raise errors.TextgridStateError(
     99                    "Two intervals in the same tier overlap in time:\n"
    100                    f"({entry.start}, {entry.end}, {entry.label}) and "
    101                    f"({nextEntry.start}, {nextEntry.end}, {nextEntry.label})"
    102                )
    103
    104    @property
    105    def timestamps(self) -> List[float]:
    106        """All unique timestamps used in this tier"""
    107        tmpTimestamps = [
    108            time
    109            for start, stop, _ in self.entries
    110            for time in [
    111                start,
    112                stop,
    113            ]
    114        ]
    115
    116        uniqueTimestamps = list(set(tmpTimestamps))
    117        uniqueTimestamps.sort()
    118
    119        return uniqueTimestamps
    120
    121    def crop(
    122        self,
    123        cropStart: float,
    124        cropEnd: float,
    125        mode: Literal["strict", "lax", "truncated"],
    126        rebaseToZero: bool,
    127    ) -> "IntervalTier":
    128        """Creates a new tier with all entries that fit inside the new interval
    129
    130        Args:
    131            cropStart:
    132            cropEnd:
    133            mode: determines cropping behavior
    134                - 'strict', only intervals wholly contained by the crop
    135                    interval will be kept
    136                - 'lax', partially contained intervals will be kept
    137                - 'truncated', partially contained intervals will be
    138                    truncated to fit within the crop region.
    139            rebaseToZero: if True, the cropped textgrid values
    140                will be subtracted by the cropStart
    141
    142        Returns:
    143            the modified version of the current tier
    144        """
    145
    146        utils.validateOption("mode", mode, CropCollision)
    147
    148        if cropStart >= cropEnd:
    149            raise errors.ArgumentError(
    150                f"Crop error: start time ({cropStart}) must occur before end time ({cropEnd})"
    151            )
    152
    153        newEntryList = utils.getIntervalsInInterval(
    154            cropStart, cropEnd, self.entries, mode
    155        )
    156
    157        if rebaseToZero is True:
    158            newSmallestValue = newEntryList[0][0]
    159            if newSmallestValue < cropStart:
    160                timeDiff = newSmallestValue
    161            else:
    162                timeDiff = cropStart
    163            newEntryList = [
    164                Interval(start - timeDiff, end - timeDiff, label)
    165                for start, end, label in newEntryList
    166            ]
    167            minT = 0.0
    168            maxT = cropEnd - cropStart
    169        else:
    170            minT = cropStart
    171            maxT = cropEnd
    172
    173        croppedTier = IntervalTier(self.name, newEntryList, minT, maxT)
    174
    175        return croppedTier
    176
    177    def dejitter(
    178        self,
    179        referenceTier: textgrid_tier.TextgridTier,
    180        maxDifference: float = 0.001,
    181    ) -> textgrid_tier.TextgridTier:
    182        """
    183        Set timestamps in this tier to be the same as values in the reference tier
    184
    185        Timestamps will only be moved if they are less than maxDifference away from the
    186        reference time.
    187
    188        This can be used to correct minor alignment errors between tiers, as made when
    189        annotating files manually, etc.
    190
    191        Args:
    192            referenceTier: the IntervalTier or PointTier to use as a reference
    193            maxDifference: the maximum amount to allow timestamps to be moved by
    194
    195        Returns:
    196            the modified version of the current tier
    197        """
    198        referenceTimestamps = referenceTier.timestamps
    199
    200        newEntries = []
    201        for start, stop, label in self.entries:
    202            startCompare = min(referenceTimestamps, key=lambda x: abs(x - start))
    203            stopCompare = min(referenceTimestamps, key=lambda x: abs(x - stop))
    204
    205            if my_math.lessThanOrEqual(abs(start - startCompare), maxDifference):
    206                start = startCompare
    207            if my_math.lessThanOrEqual(abs(stop - stopCompare), maxDifference):
    208                stop = stopCompare
    209            newEntries.append((start, stop, label))
    210
    211        return self.new(entries=newEntries)
    212
    213    def deleteEntry(self, entry: Interval) -> None:
    214        """Removes an entry from the entries"""
    215        self._entries.pop(self._entries.index(entry))
    216
    217    def difference(self, tier: "IntervalTier") -> "IntervalTier":
    218        """Takes the set difference of this tier and the given one
    219
    220        Any overlapping portions of entries with entries in this textgrid
    221        will be removed from the returned tier.
    222
    223        Args:
    224            tier: the tier to subtract from this one
    225
    226        Returns:
    227            the modified version of the current tier
    228        """
    229        retTier = self.new()
    230
    231        for entry in tier.entries:
    232            retTier = retTier.eraseRegion(
    233                entry.start,
    234                entry.end,
    235                collisionMode=constants.EraseCollision.TRUNCATE,
    236                doShrink=False,
    237            )
    238
    239        return retTier
    240
    241    def editTimestamps(
    242        self,
    243        offset: float,
    244        reportingMode: Literal["silence", "warning", "error"] = "warning",
    245    ) -> "IntervalTier":
    246        """Modifies all timestamps by a constant amount
    247
    248        Args:
    249            offset: the amount to shift all intervals
    250            reportingMode: Determines the behavior if an entries moves outside
    251                of minTimestamp or maxTimestamp after being edited
    252
    253        Returns:
    254            the modified version of the current tier
    255        """
    256        utils.validateOption(
    257            "reportingMode", reportingMode, constants.ErrorReportingMode
    258        )
    259        errorReporter = utils.getErrorReporter(reportingMode)
    260
    261        newEntryList = []
    262        for interval in self.entries:
    263            newStart = offset + interval.start
    264            newEnd = offset + interval.end
    265
    266            utils.checkIsUndershoot(newStart, self.minTimestamp, errorReporter)
    267            utils.checkIsOvershoot(newEnd, self.maxTimestamp, errorReporter)
    268
    269            if newEnd <= 0:
    270                continue
    271            if newStart < 0:
    272                newStart = 0
    273
    274            newEntryList.append(Interval(newStart, newEnd, interval.label))
    275
    276        # Determine new min and max timestamps
    277        newMin = min([interval.start for interval in newEntryList])
    278        newMax = max([interval.end for interval in newEntryList])
    279
    280        if newMin > self.minTimestamp:
    281            newMin = self.minTimestamp
    282
    283        if newMax < self.maxTimestamp:
    284            newMax = self.maxTimestamp
    285
    286        return IntervalTier(self.name, newEntryList, newMin, newMax)
    287
    288    def eraseRegion(
    289        self,
    290        start: float,
    291        end: float,
    292        collisionMode: Literal["truncate", "categorical", "error"] = "error",
    293        doShrink: bool = True,
    294    ) -> "IntervalTier":
    295        """Makes a region in a tier blank (removes all contained entries)
    296
    297        Args:
    298            start:
    299            end:
    300            collisionMode: Determines the behavior when the region to erase
    301                overlaps with existing intervals.
    302                - 'truncate' partially contained entries will have the portion
    303                    removed that overlaps with the target entry
    304                - 'categorical' all entries that overlap, even partially, with
    305                    the target entry will be completely removed
    306                - None or any other value throws IntervalCollision
    307            doShrink: If True, moves leftward by (/end/ - /start/)
    308                amount, each item that occurs after /end/
    309
    310        Returns:
    311            The modified version of the current tier
    312
    313        Raises:
    314            CollisionError
    315        """
    316        utils.validateOption("collisionMode", collisionMode, constants.EraseCollision)
    317
    318        matchList = self.crop(start, end, CropCollision.LAX, False).entries
    319        newTier = self.new()
    320
    321        if len(matchList) == 0:
    322            pass
    323        else:
    324            if collisionMode == constants.EraseCollision.ERROR:
    325                raise errors.CollisionError(
    326                    f"Erase region ({start}, {end})overlapped with an interval. "
    327                    "If this was expected, consider setting the collisionMode"
    328                )
    329
    330            # Remove all the matches from the entries
    331            # Go in reverse order because we're destructively altering
    332            # the order of the list (messes up index order)
    333            for interval in matchList[::-1]:
    334                newTier.deleteEntry(interval)
    335
    336            # If we're only truncating, reinsert entries on the left and
    337            # right edges
    338            # if categorical, it doesn't make it into the list at all
    339            if collisionMode == constants.EraseCollision.TRUNCATE:
    340                # Check left edge
    341                if matchList[0].start < start:
    342                    newEntry = Interval(matchList[0].start, start, matchList[0].label)
    343                    newTier.insertEntry(newEntry)
    344
    345                # Check right edge
    346                if matchList[-1].end > end:
    347                    newEntry = Interval(end, matchList[-1].end, matchList[-1].label)
    348                    newTier.insertEntry(newEntry)
    349
    350        if doShrink is True:
    351            diff = end - start
    352            newEntryList = []
    353            for interval in newTier.entries:
    354                if interval.end <= start:
    355                    newEntryList.append(interval)
    356                elif interval.start >= end:
    357                    newEntryList.append(
    358                        Interval(
    359                            interval.start - diff, interval.end - diff, interval.label
    360                        )
    361                    )
    362
    363            # Special case: an interval that spanned the deleted
    364            # section
    365            for i in range(0, len(newEntryList) - 1):
    366                rightEdge = newEntryList[i].end == start
    367                leftEdge = newEntryList[i + 1].start == start
    368                sameLabel = newEntryList[i].label == newEntryList[i + 1].label
    369                if rightEdge and leftEdge and sameLabel:
    370                    newInterval = Interval(
    371                        newEntryList[i].start,
    372                        newEntryList[i + 1].end,
    373                        newEntryList[i].label,
    374                    )
    375
    376                    newEntryList.pop(i + 1)
    377                    newEntryList.pop(i)
    378                    newEntryList.insert(i, newInterval)
    379
    380                    # Only one interval can span the deleted section,
    381                    # so if we've found it, move on
    382                    break
    383
    384            newMax = newTier.maxTimestamp - diff
    385            newTier = newTier.new(entries=newEntryList, maxTimestamp=newMax)
    386
    387        return newTier
    388
    389    def getValuesInIntervals(self, dataTupleList: List) -> List[Tuple[Interval, List]]:
    390        """Returns data from dataTupleList contained in labeled intervals
    391
    392        Each labeled interval will get its own list of data values.
    393
    394        dataTupleList should be of the form:
    395        [(time1, value1a, value1b,...), (time2, value2a, value2b...), ...]
    396        """
    397
    398        returnList = []
    399
    400        for interval in self.entries:
    401            intervalDataList = utils.getValuesInInterval(
    402                dataTupleList, interval.start, interval.end
    403            )
    404            returnList.append((interval, intervalDataList))
    405
    406        return returnList
    407
    408    def getNonEntries(self) -> List[Interval]:
    409        """Returns the regions of the textgrid without labels
    410
    411        This can include unlabeled segments and regions marked as silent.
    412        """
    413        entries = self.entries
    414        invertedEntryList = [
    415            Interval(entries[i].end, entries[i + 1].start, "")
    416            for i in range(len(entries) - 1)
    417        ]
    418
    419        # Remove entries that have no duration (ie lie between two entries
    420        # that share a border)
    421        invertedEntryList = [
    422            interval for interval in invertedEntryList if interval.start < interval.end
    423        ]
    424
    425        if entries[0].start > 0:
    426            invertedEntryList.insert(0, Interval(0, entries[0].start, ""))
    427
    428        if entries[-1].end < self.maxTimestamp:
    429            invertedEntryList.append(Interval(entries[-1].end, self.maxTimestamp, ""))
    430
    431        invertedEntryList = [
    432            interval if isinstance(interval, Interval) else Interval(*interval)
    433            for interval in invertedEntryList
    434        ]
    435
    436        return invertedEntryList
    437
    438    def insertEntry(
    439        self,
    440        entry: Interval,
    441        collisionMode: Literal["replace", "merge", "error"] = "error",
    442        collisionReportingMode: Literal["silence", "warning"] = "warning",
    443    ) -> None:
    444        """Inserts an interval into the tier
    445
    446        Args:
    447            entry: the Interval to insert
    448            collisionMode: determines the behavior in the event that intervals
    449                exist in the insertion area.
    450                - 'replace' will remove existing items
    451                - 'merge' will fuse the inserting item with existing items
    452                - None or any other value will throw a CollisionError
    453            collisionReportingMode: Determines the behavior if the new entry
    454                overlaps with an existing one
    455
    456        Returns:
    457            the modified version of the current tier
    458        """
    459        utils.validateOption(
    460            "collisionMode", collisionMode, constants.IntervalCollision
    461        )
    462        utils.validateOption(
    463            "collisionReportingMode",
    464            collisionReportingMode,
    465            constants.ErrorReportingMode,
    466        )
    467        collisionReporter = utils.getErrorReporter(collisionReportingMode)
    468
    469        if not isinstance(entry, Interval):
    470            interval = Interval(*entry)
    471        else:
    472            interval = entry
    473
    474        matchList = self.crop(
    475            interval.start, interval.end, CropCollision.LAX, False
    476        )._entries
    477
    478        if len(matchList) == 0:
    479            self._entries.append(interval)
    480
    481        elif collisionMode == constants.IntervalCollision.REPLACE:
    482            for matchEntry in matchList:
    483                self.deleteEntry(matchEntry)
    484            self._entries.append(interval)
    485
    486        elif collisionMode == constants.IntervalCollision.MERGE:
    487            for matchEntry in matchList:
    488                self.deleteEntry(matchEntry)
    489            matchList.append(interval)
    490            matchList.sort()  # By starting time
    491
    492            newInterval = Interval(
    493                min([tmpInterval.start for tmpInterval in matchList]),
    494                max([tmpInterval.end for tmpInterval in matchList]),
    495                "-".join([tmpInterval.label for tmpInterval in matchList]),
    496            )
    497            self._entries.append(newInterval)
    498
    499        else:
    500            raise errors.CollisionError(
    501                "Attempted to insert interval "
    502                f"({interval.start}, {interval.end}, '{interval.label}') into tier {self.name} "
    503                "of textgrid but overlapping entries "
    504                f"{[tuple(interval) for interval in matchList]} "
    505                "already exist"
    506            )
    507
    508        self.sort()
    509
    510        if self._entries[0][0] < self.minTimestamp:
    511            self.minTimestamp = self._entries[0][0]
    512
    513        if self._entries[-1][1] > self.maxTimestamp:
    514            self.maxTimestamp = self._entries[-1][1]
    515
    516        if len(matchList) != 0:
    517            collisionReporter(
    518                errors.CollisionError,
    519                f"Collision warning for ({interval}) with items "
    520                f"({matchList}) of tier '{self.name}'",
    521            )
    522
    523    def insertSpace(
    524        self,
    525        start: float,
    526        duration: float,
    527        collisionMode: Literal["stretch", "split", "no_change", "error"],
    528    ) -> "IntervalTier":
    529        """Inserts a blank region into the tier
    530
    531        Args:
    532            start:
    533            duration:
    534            collisionMode: Determines the behavior that occurs if
    535                an interval stradles the starting point
    536                - 'stretch' stretches the interval by /duration/ amount
    537                - 'split' splits the interval into two--everything to the
    538                    right of 'start' will be advanced by 'duration' seconds
    539                - 'no change' leaves the interval as is with no change
    540                - 'error' will stop execution and raise an error
    541
    542        Returns:
    543            the modified version of the current tier
    544        """
    545        utils.validateOption(
    546            "collisionMode", collisionMode, constants.WhitespaceCollision
    547        )
    548
    549        newEntryList = []
    550        for interval in self.entries:
    551            # Entry exists before the insertion point
    552            if interval.end <= start:
    553                newEntryList.append(interval)
    554            # Entry exists after the insertion point
    555            elif interval.start >= start:
    556                newEntryList.append(
    557                    Interval(
    558                        interval.start + duration,
    559                        interval.end + duration,
    560                        interval.label,
    561                    )
    562                )
    563            # Entry straddles the insertion point
    564            elif interval.start <= start and interval.end > start:
    565                if collisionMode == constants.WhitespaceCollision.STRETCH:
    566                    newEntryList.append(
    567                        Interval(
    568                            interval.start, interval.end + duration, interval.label
    569                        )
    570                    )
    571                elif collisionMode == constants.WhitespaceCollision.SPLIT:
    572                    # Left side of the split
    573                    newEntryList.append(Interval(interval.start, start, interval.label))
    574                    # Right side of the split
    575                    newEntryList.append(
    576                        (
    577                            start + duration,
    578                            start + duration + (interval.end - start),
    579                            interval.label,
    580                        )
    581                    )
    582                elif collisionMode == constants.WhitespaceCollision.NO_CHANGE:
    583                    newEntryList.append(interval)
    584                else:
    585                    raise errors.ArgumentError(
    586                        f"Collision occured during insertSpace() for interval '{interval}' "
    587                        f"and given white space insertion interval ({start}, {start + duration})"
    588                    )
    589
    590        newTier = self.new(
    591            entries=newEntryList, maxTimestamp=self.maxTimestamp + duration
    592        )
    593
    594        return newTier
    595
    596    def intersection(self, tier: "IntervalTier", demarcator="-") -> "IntervalTier":
    597        """Takes the set intersection of this tier and the given one
    598
    599        - The output will contain one interval for each overlapping pair
    600          e.g. [(1, 2, 'foo')] and [(1, 1.3, 'bang'), (1.7, 2, 'wizz')]
    601                -> [(1, 1.3, 'foo-bang'), (1.7, 2, 'foo-wizz')]
    602        - Only intervals that exist in both tiers will remain in the returned tier.
    603          e.g. [(1, 2, 'foo'), (3, 4, 'bar')] and [(1, 2, 'bang'), (2, 3, 'wizz')]
    604                -> [(1, 2, 'foo-bang')]
    605        - If intervals partially overlap, only the overlapping portion will be returned.
    606          e.g. [(1, 2, 'foo')] and [(0.5, 1.5, 'bang')]
    607                -> [(1, 1.5, 'foo-bang')]
    608
    609        Compare with IntervalTier.mergeLabels
    610
    611        Args:
    612            tier: the tier to intersect with
    613            demarcator: the character to separate the labels of the overlapping intervals
    614
    615        Returns:
    616            IntervalTier: the modified version of the current tier
    617        """
    618        retEntryList = []
    619        for interval in tier.entries:
    620            subTier = self.crop(
    621                interval.start, interval.end, CropCollision.TRUNCATED, False
    622            )
    623
    624            # Combine the labels in the two tiers
    625            subEntryList = [
    626                (
    627                    subInterval.start,
    628                    subInterval.end,
    629                    f"{subInterval.label}{demarcator}{interval.label}",
    630                )
    631                for subInterval in subTier.entries
    632            ]
    633
    634            retEntryList.extend(subEntryList)
    635
    636        newName = f"{self.name}-{tier.name}"
    637
    638        retTier = self.new(newName, retEntryList)
    639
    640        return retTier
    641
    642    def mergeLabels(
    643        self, tier: "IntervalTier", demarcator: str = ","
    644    ) -> "IntervalTier":
    645        """Merges labels of overlapping tiers into this tier
    646
    647        - All intervals in this tier will appear in the output; for the given tier, only intervals
    648          that overlap with content in this tier will appear in the output
    649          e.g. [(1, 2, 'foo'), (3, 4, 'bar')] and [(1, 2, 'bang'), (2, 3, 'wizz')]
    650                -> [(1, 2, 'foo(bang)'), (3, 4, 'bar()')]
    651        - If multiple entries exist in a subinterval, their labels will be concatenated
    652          e.g. [(1, 2, 'hi')] and [(1, 1.5, 'h'), (1.5, 2, 'ai')] -> [(1, 2, 'hi(h,ai)')]
    653
    654        compare with IntervalTier.intersection
    655
    656        Args:
    657            tier: the tier to intersect with
    658            demarcator: the string to separate items that fall in the same subinterval
    659
    660        Returns:
    661            IntervalTier: the modified version of the current tier
    662        """
    663        retEntryList = []
    664        for interval in self.entries:
    665            subTier = tier.crop(
    666                interval.start, interval.end, CropCollision.TRUNCATED, False
    667            )
    668            if len(subTier._entries) == 0:
    669                continue
    670
    671            subLabel = demarcator.join([entry.label for entry in subTier.entries])
    672            label = f"{interval.label}({subLabel})"
    673
    674            start = min(interval.start, subTier._entries[0].start)
    675            end = max(interval.end, subTier._entries[-1].end)
    676
    677            intersectedInterval = (
    678                start,
    679                end,
    680                label,
    681            )
    682
    683            retEntryList.append(intersectedInterval)
    684
    685        newName = f"{self.name}-{tier.name}"
    686
    687        retTier = self.new(newName, retEntryList)
    688
    689        return retTier
    690
    691    def morph(
    692        self,
    693        targetTier: "IntervalTier",
    694        filterFunc: Optional[Callable[[str], bool]] = None,
    695    ) -> "IntervalTier":
    696        """Morphs the duration of segments in this tier to those in another
    697
    698        This preserves the labels and the duration of silence in
    699        this tier while changing the duration of labeled segments.
    700
    701        Args:
    702            targetTier:
    703            filterFunc: if specified, filters entries. The
    704                functor takes one argument, an Interval. It returns true
    705                if the Interval should be modified and false if not.
    706
    707        Returns:
    708            The modified version of the current tier
    709        """
    710        cumulativeAdjustAmount = 0
    711        newEntryList = []
    712        allIntervals = [self.entries, targetTier.entries]
    713        for sourceInterval, targetInterval in utils.safeZip(allIntervals, True):
    714            # sourceInterval.start - lastFromEnd -> was this interval and the
    715            # last one adjacent?
    716            newStart = sourceInterval.start + cumulativeAdjustAmount
    717
    718            currIntervalDuration = sourceInterval.end - sourceInterval.start
    719            if filterFunc is None or filterFunc(sourceInterval.label):
    720                newIntervalDuration = targetInterval.end - targetInterval.start
    721                cumulativeAdjustAmount += newIntervalDuration - currIntervalDuration
    722                newEnd = newStart + newIntervalDuration
    723            else:
    724                newEnd = newStart + currIntervalDuration
    725
    726            newEntryList.append(Interval(newStart, newEnd, sourceInterval.label))
    727
    728        newMin = self.minTimestamp
    729        cumulativeDifference = newEntryList[-1].end - self.entries[-1].end
    730        newMax = self.maxTimestamp + cumulativeDifference
    731
    732        return IntervalTier(self.name, newEntryList, newMin, newMax)
    733
    734    def validate(
    735        self, reportingMode: Literal["silence", "warning", "error"] = "warning"
    736    ) -> bool:
    737        """Validate this tier
    738
    739        Args:
    740            reportingMode (str): Determines the behavior if validation fails.
    741
    742        Returns:
    743            True if the tier is valid; False if not
    744        """
    745        utils.validateOption(
    746            "reportingMode", reportingMode, constants.ErrorReportingMode
    747        )
    748        errorReporter = utils.getErrorReporter(reportingMode)
    749
    750        isValid = True
    751        previousInterval = None
    752        for interval in self.entries:
    753            if interval.start >= interval.end:
    754                isValid = False
    755                errorReporter(
    756                    errors.TextgridStateError,
    757                    f"Invalid interval. End time occurs before or on the start time({interval}).",
    758                )
    759
    760            if previousInterval and previousInterval.end > interval.start:
    761                isValid = False
    762                errorReporter(
    763                    errors.TextgridStateError,
    764                    f"Intervals are not sorted in time: "
    765                    f"[({previousInterval}), ({interval})]",
    766                )
    767
    768            if utils.checkIsUndershoot(
    769                interval.start, self.minTimestamp, errorReporter
    770            ):
    771                isValid = False
    772
    773            if utils.checkIsOvershoot(interval.end, self.maxTimestamp, errorReporter):
    774                isValid = False
    775
    776            previousInterval = interval
    777
    778        return isValid

Helper class that provides a standard way to create an ABC using inheritance.

IntervalTier( name: str, entries: List[praatio.utilities.constants.Interval], minT: Optional[float] = None, maxT: Optional[float] = None) View Source

    62    def __init__(
    63        self,
    64        name: str,
    65        entries: List[Interval],
    66        minT: Optional[float] = None,
    67        maxT: Optional[float] = None,
    68    ):
    69        """An interval tier is for annotating events that have duration
    70
    71        The entries is of the form:
    72        [(startTime1, endTime1, label1), (startTime2, endTime2, label2), ]
    73
    74        The data stored in the labels can be anything but will
    75        be interpreted as text by praatio (the label could be descriptive
    76        text e.g. ('erase this region') or numerical data e.g. (average pitch
    77        values like '132'))
    78        """
    79        entries = _homogenizeEntries(entries)
    80        calculatedMinT, calculatedMaxT = _calculateMinAndMaxTime(entries, minT, maxT)
    81
    82        super(IntervalTier, self).__init__(
    83            name, entries, calculatedMinT, calculatedMaxT
    84        )
    85        self._validate()

An interval tier is for annotating events that have duration

The entries is of the form: [(startTime1, endTime1, label1), (startTime2, endTime2, label2), ]

The data stored in the labels can be anything but will be interpreted as text by praatio (the label could be descriptive text e.g. ('erase this region') or numerical data e.g. (average pitch values like '132'))

tierType = 'IntervalTier'
entryType = <class 'praatio.utilities.constants.Interval'>
timestamps: List[float]

All unique timestamps used in this tier

def crop( self, cropStart: float, cropEnd: float, mode: Literal['strict', 'lax', 'truncated'], rebaseToZero: bool) -> IntervalTier: View Source

    121    def crop(
    122        self,
    123        cropStart: float,
    124        cropEnd: float,
    125        mode: Literal["strict", "lax", "truncated"],
    126        rebaseToZero: bool,
    127    ) -> "IntervalTier":
    128        """Creates a new tier with all entries that fit inside the new interval
    129
    130        Args:
    131            cropStart:
    132            cropEnd:
    133            mode: determines cropping behavior
    134                - 'strict', only intervals wholly contained by the crop
    135                    interval will be kept
    136                - 'lax', partially contained intervals will be kept
    137                - 'truncated', partially contained intervals will be
    138                    truncated to fit within the crop region.
    139            rebaseToZero: if True, the cropped textgrid values
    140                will be subtracted by the cropStart
    141
    142        Returns:
    143            the modified version of the current tier
    144        """
    145
    146        utils.validateOption("mode", mode, CropCollision)
    147
    148        if cropStart >= cropEnd:
    149            raise errors.ArgumentError(
    150                f"Crop error: start time ({cropStart}) must occur before end time ({cropEnd})"
    151            )
    152
    153        newEntryList = utils.getIntervalsInInterval(
    154            cropStart, cropEnd, self.entries, mode
    155        )
    156
    157        if rebaseToZero is True:
    158            newSmallestValue = newEntryList[0][0]
    159            if newSmallestValue < cropStart:
    160                timeDiff = newSmallestValue
    161            else:
    162                timeDiff = cropStart
    163            newEntryList = [
    164                Interval(start - timeDiff, end - timeDiff, label)
    165                for start, end, label in newEntryList
    166            ]
    167            minT = 0.0
    168            maxT = cropEnd - cropStart
    169        else:
    170            minT = cropStart
    171            maxT = cropEnd
    172
    173        croppedTier = IntervalTier(self.name, newEntryList, minT, maxT)
    174
    175        return croppedTier

Creates a new tier with all entries that fit inside the new interval

###### Arguments:

- cropStart:
- cropEnd:
- mode: determines cropping behavior
  - 'strict', only intervals wholly contained by the crop interval will be kept
  - 'lax', partially contained intervals will be kept
  - 'truncated', partially contained intervals will be truncated to fit within the crop region.
- rebaseToZero: if True, the cropped textgrid values will be subtracted by the cropStart

###### Returns:

> the modified version of the current tier

def dejitter( self, referenceTier: praatio.data_classes.textgrid_tier.TextgridTier, maxDifference: float = 0.001) -> praatio.data_classes.textgrid_tier.TextgridTier: View Source

    177    def dejitter(
    178        self,
    179        referenceTier: textgrid_tier.TextgridTier,
    180        maxDifference: float = 0.001,
    181    ) -> textgrid_tier.TextgridTier:
    182        """
    183        Set timestamps in this tier to be the same as values in the reference tier
    184
    185        Timestamps will only be moved if they are less than maxDifference away from the
    186        reference time.
    187
    188        This can be used to correct minor alignment errors between tiers, as made when
    189        annotating files manually, etc.
    190
    191        Args:
    192            referenceTier: the IntervalTier or PointTier to use as a reference
    193            maxDifference: the maximum amount to allow timestamps to be moved by
    194
    195        Returns:
    196            the modified version of the current tier
    197        """
    198        referenceTimestamps = referenceTier.timestamps
    199
    200        newEntries = []
    201        for start, stop, label in self.entries:
    202            startCompare = min(referenceTimestamps, key=lambda x: abs(x - start))
    203            stopCompare = min(referenceTimestamps, key=lambda x: abs(x - stop))
    204
    205            if my_math.lessThanOrEqual(abs(start - startCompare), maxDifference):
    206                start = startCompare
    207            if my_math.lessThanOrEqual(abs(stop - stopCompare), maxDifference):
    208                stop = stopCompare
    209            newEntries.append((start, stop, label))
    210
    211        return self.new(entries=newEntries)

Set timestamps in this tier to be the same as values in the reference tier

Timestamps will only be moved if they are less than maxDifference away from the reference time.

This can be used to correct minor alignment errors between tiers, as made when annotating files manually, etc.

###### Arguments:

- referenceTier: the IntervalTier or PointTier to use as a reference
- maxDifference: the maximum amount to allow timestamps to be moved by

###### Returns:

> the modified version of the current tier

def deleteEntry(self, entry: praatio.utilities.constants.Interval) -> None: View Source

    213    def deleteEntry(self, entry: Interval) -> None:
    214        """Removes an entry from the entries"""
    215        self._entries.pop(self._entries.index(entry))

Removes an entry from the entries

def difference( self, tier: IntervalTier) -> IntervalTier: View Source

    217    def difference(self, tier: "IntervalTier") -> "IntervalTier":
    218        """Takes the set difference of this tier and the given one
    219
    220        Any overlapping portions of entries with entries in this textgrid
    221        will be removed from the returned tier.
    222
    223        Args:
    224            tier: the tier to subtract from this one
    225
    226        Returns:
    227            the modified version of the current tier
    228        """
    229        retTier = self.new()
    230
    231        for entry in tier.entries:
    232            retTier = retTier.eraseRegion(
    233                entry.start,
    234                entry.end,
    235                collisionMode=constants.EraseCollision.TRUNCATE,
    236                doShrink=False,
    237            )
    238
    239        return retTier

Takes the set difference of this tier and the given one

Any overlapping portions of entries with entries in this textgrid will be removed from the returned tier.

###### Arguments:

- tier: the tier to subtract from this one

###### Returns:

> the modified version of the current tier

def editTimestamps( self, offset: float, reportingMode: Literal['silence', 'warning', 'error'] = 'warning') -> IntervalTier: View Source

    241    def editTimestamps(
    242        self,
    243        offset: float,
    244        reportingMode: Literal["silence", "warning", "error"] = "warning",
    245    ) -> "IntervalTier":
    246        """Modifies all timestamps by a constant amount
    247
    248        Args:
    249            offset: the amount to shift all intervals
    250            reportingMode: Determines the behavior if an entries moves outside
    251                of minTimestamp or maxTimestamp after being edited
    252
    253        Returns:
    254            the modified version of the current tier
    255        """
    256        utils.validateOption(
    257            "reportingMode", reportingMode, constants.ErrorReportingMode
    258        )
    259        errorReporter = utils.getErrorReporter(reportingMode)
    260
    261        newEntryList = []
    262        for interval in self.entries:
    263            newStart = offset + interval.start
    264            newEnd = offset + interval.end
    265
    266            utils.checkIsUndershoot(newStart, self.minTimestamp, errorReporter)
    267            utils.checkIsOvershoot(newEnd, self.maxTimestamp, errorReporter)
    268
    269            if newEnd <= 0:
    270                continue
    271            if newStart < 0:
    272                newStart = 0
    273
    274            newEntryList.append(Interval(newStart, newEnd, interval.label))
    275
    276        # Determine new min and max timestamps
    277        newMin = min([interval.start for interval in newEntryList])
    278        newMax = max([interval.end for interval in newEntryList])
    279
    280        if newMin > self.minTimestamp:
    281            newMin = self.minTimestamp
    282
    283        if newMax < self.maxTimestamp:
    284            newMax = self.maxTimestamp
    285
    286        return IntervalTier(self.name, newEntryList, newMin, newMax)

Modifies all timestamps by a constant amount

###### Arguments:

- offset: the amount to shift all intervals
- reportingMode: Determines the behavior if an entries moves outside of minTimestamp or maxTimestamp after being edited

###### Returns:

> the modified version of the current tier

def eraseRegion( self, start: float, end: float, collisionMode: Literal['truncate', 'categorical', 'error'] = 'error', doShrink: bool = True) -> IntervalTier: View Source

    288    def eraseRegion(
    289        self,
    290        start: float,
    291        end: float,
    292        collisionMode: Literal["truncate", "categorical", "error"] = "error",
    293        doShrink: bool = True,
    294    ) -> "IntervalTier":
    295        """Makes a region in a tier blank (removes all contained entries)
    296
    297        Args:
    298            start:
    299            end:
    300            collisionMode: Determines the behavior when the region to erase
    301                overlaps with existing intervals.
    302                - 'truncate' partially contained entries will have the portion
    303                    removed that overlaps with the target entry
    304                - 'categorical' all entries that overlap, even partially, with
    305                    the target entry will be completely removed
    306                - None or any other value throws IntervalCollision
    307            doShrink: If True, moves leftward by (/end/ - /start/)
    308                amount, each item that occurs after /end/
    309
    310        Returns:
    311            The modified version of the current tier
    312
    313        Raises:
    314            CollisionError
    315        """
    316        utils.validateOption("collisionMode", collisionMode, constants.EraseCollision)
    317
    318        matchList = self.crop(start, end, CropCollision.LAX, False).entries
    319        newTier = self.new()
    320
    321        if len(matchList) == 0:
    322            pass
    323        else:
    324            if collisionMode == constants.EraseCollision.ERROR:
    325                raise errors.CollisionError(
    326                    f"Erase region ({start}, {end})overlapped with an interval. "
    327                    "If this was expected, consider setting the collisionMode"
    328                )
    329
    330            # Remove all the matches from the entries
    331            # Go in reverse order because we're destructively altering
    332            # the order of the list (messes up index order)
    333            for interval in matchList[::-1]:
    334                newTier.deleteEntry(interval)
    335
    336            # If we're only truncating, reinsert entries on the left and
    337            # right edges
    338            # if categorical, it doesn't make it into the list at all
    339            if collisionMode == constants.EraseCollision.TRUNCATE:
    340                # Check left edge
    341                if matchList[0].start < start:
    342                    newEntry = Interval(matchList[0].start, start, matchList[0].label)
    343                    newTier.insertEntry(newEntry)
    344
    345                # Check right edge
    346                if matchList[-1].end > end:
    347                    newEntry = Interval(end, matchList[-1].end, matchList[-1].label)
    348                    newTier.insertEntry(newEntry)
    349
    350        if doShrink is True:
    351            diff = end - start
    352            newEntryList = []
    353            for interval in newTier.entries:
    354                if interval.end <= start:
    355                    newEntryList.append(interval)
    356                elif interval.start >= end:
    357                    newEntryList.append(
    358                        Interval(
    359                            interval.start - diff, interval.end - diff, interval.label
    360                        )
    361                    )
    362
    363            # Special case: an interval that spanned the deleted
    364            # section
    365            for i in range(0, len(newEntryList) - 1):
    366                rightEdge = newEntryList[i].end == start
    367                leftEdge = newEntryList[i + 1].start == start
    368                sameLabel = newEntryList[i].label == newEntryList[i + 1].label
    369                if rightEdge and leftEdge and sameLabel:
    370                    newInterval = Interval(
    371                        newEntryList[i].start,
    372                        newEntryList[i + 1].end,
    373                        newEntryList[i].label,
    374                    )
    375
    376                    newEntryList.pop(i + 1)
    377                    newEntryList.pop(i)
    378                    newEntryList.insert(i, newInterval)
    379
    380                    # Only one interval can span the deleted section,
    381                    # so if we've found it, move on
    382                    break
    383
    384            newMax = newTier.maxTimestamp - diff
    385            newTier = newTier.new(entries=newEntryList, maxTimestamp=newMax)
    386
    387        return newTier

Makes a region in a tier blank (removes all contained entries)

###### Arguments:

- start:
- end:
- collisionMode: Determines the behavior when the region to erase overlaps with existing intervals.
  - 'truncate' partially contained entries will have the portion removed that overlaps with the target entry
  - 'categorical' all entries that overlap, even partially, with the target entry will be completely removed
  - None or any other value throws IntervalCollision
- doShrink: If True, moves leftward by (/end/ - /start/) amount, each item that occurs after /end/

###### Returns:

> The modified version of the current tier

###### Raises:

- CollisionError

def getValuesInIntervals( self, dataTupleList: List) -> List[Tuple[praatio.utilities.constants.Interval, List]]: View Source

    389    def getValuesInIntervals(self, dataTupleList: List) -> List[Tuple[Interval, List]]:
    390        """Returns data from dataTupleList contained in labeled intervals
    391
    392        Each labeled interval will get its own list of data values.
    393
    394        dataTupleList should be of the form:
    395        [(time1, value1a, value1b,...), (time2, value2a, value2b...), ...]
    396        """
    397
    398        returnList = []
    399
    400        for interval in self.entries:
    401            intervalDataList = utils.getValuesInInterval(
    402                dataTupleList, interval.start, interval.end
    403            )
    404            returnList.append((interval, intervalDataList))
    405
    406        return returnList

Returns data from dataTupleList contained in labeled intervals

Each labeled interval will get its own list of data values.

dataTupleList should be of the form: [(time1, value1a, value1b,...), (time2, value2a, value2b...), ...]

def getNonEntries(self) -> List[praatio.utilities.constants.Interval]: View Source

    408    def getNonEntries(self) -> List[Interval]:
    409        """Returns the regions of the textgrid without labels
    410
    411        This can include unlabeled segments and regions marked as silent.
    412        """
    413        entries = self.entries
    414        invertedEntryList = [
    415            Interval(entries[i].end, entries[i + 1].start, "")
    416            for i in range(len(entries) - 1)
    417        ]
    418
    419        # Remove entries that have no duration (ie lie between two entries
    420        # that share a border)
    421        invertedEntryList = [
    422            interval for interval in invertedEntryList if interval.start < interval.end
    423        ]
    424
    425        if entries[0].start > 0:
    426            invertedEntryList.insert(0, Interval(0, entries[0].start, ""))
    427
    428        if entries[-1].end < self.maxTimestamp:
    429            invertedEntryList.append(Interval(entries[-1].end, self.maxTimestamp, ""))
    430
    431        invertedEntryList = [
    432            interval if isinstance(interval, Interval) else Interval(*interval)
    433            for interval in invertedEntryList
    434        ]
    435
    436        return invertedEntryList

Returns the regions of the textgrid without labels

This can include unlabeled segments and regions marked as silent.

def insertEntry( self, entry: praatio.utilities.constants.Interval, collisionMode: Literal['replace', 'merge', 'error'] = 'error', collisionReportingMode: Literal['silence', 'warning'] = 'warning') -> None: View Source

    438    def insertEntry(
    439        self,
    440        entry: Interval,
    441        collisionMode: Literal["replace", "merge", "error"] = "error",
    442        collisionReportingMode: Literal["silence", "warning"] = "warning",
    443    ) -> None:
    444        """Inserts an interval into the tier
    445
    446        Args:
    447            entry: the Interval to insert
    448            collisionMode: determines the behavior in the event that intervals
    449                exist in the insertion area.
    450                - 'replace' will remove existing items
    451                - 'merge' will fuse the inserting item with existing items
    452                - None or any other value will throw a CollisionError
    453            collisionReportingMode: Determines the behavior if the new entry
    454                overlaps with an existing one
    455
    456        Returns:
    457            the modified version of the current tier
    458        """
    459        utils.validateOption(
    460            "collisionMode", collisionMode, constants.IntervalCollision
    461        )
    462        utils.validateOption(
    463            "collisionReportingMode",
    464            collisionReportingMode,
    465            constants.ErrorReportingMode,
    466        )
    467        collisionReporter = utils.getErrorReporter(collisionReportingMode)
    468
    469        if not isinstance(entry, Interval):
    470            interval = Interval(*entry)
    471        else:
    472            interval = entry
    473
    474        matchList = self.crop(
    475            interval.start, interval.end, CropCollision.LAX, False
    476        )._entries
    477
    478        if len(matchList) == 0:
    479            self._entries.append(interval)
    480
    481        elif collisionMode == constants.IntervalCollision.REPLACE:
    482            for matchEntry in matchList:
    483                self.deleteEntry(matchEntry)
    484            self._entries.append(interval)
    485
    486        elif collisionMode == constants.IntervalCollision.MERGE:
    487            for matchEntry in matchList:
    488                self.deleteEntry(matchEntry)
    489            matchList.append(interval)
    490            matchList.sort()  # By starting time
    491
    492            newInterval = Interval(
    493                min([tmpInterval.start for tmpInterval in matchList]),
    494                max([tmpInterval.end for tmpInterval in matchList]),
    495                "-".join([tmpInterval.label for tmpInterval in matchList]),
    496            )
    497            self._entries.append(newInterval)
    498
    499        else:
    500            raise errors.CollisionError(
    501                "Attempted to insert interval "
    502                f"({interval.start}, {interval.end}, '{interval.label}') into tier {self.name} "
    503                "of textgrid but overlapping entries "
    504                f"{[tuple(interval) for interval in matchList]} "
    505                "already exist"
    506            )
    507
    508        self.sort()
    509
    510        if self._entries[0][0] < self.minTimestamp:
    511            self.minTimestamp = self._entries[0][0]
    512
    513        if self._entries[-1][1] > self.maxTimestamp:
    514            self.maxTimestamp = self._entries[-1][1]
    515
    516        if len(matchList) != 0:
    517            collisionReporter(
    518                errors.CollisionError,
    519                f"Collision warning for ({interval}) with items "
    520                f"({matchList}) of tier '{self.name}'",
    521            )

Inserts an interval into the tier

###### Arguments:

- entry: the Interval to insert
- collisionMode: determines the behavior in the event that intervals exist in the insertion area.
  - 'replace' will remove existing items
  - 'merge' will fuse the inserting item with existing items
  - None or any other value will throw a CollisionError
- collisionReportingMode: Determines the behavior if the new entry overlaps with an existing one

###### Returns:

> the modified version of the current tier

def insertSpace( self, start: float, duration: float, collisionMode: Literal['stretch', 'split', 'no_change', 'error']) -> IntervalTier: View Source

    523    def insertSpace(
    524        self,
    525        start: float,
    526        duration: float,
    527        collisionMode: Literal["stretch", "split", "no_change", "error"],
    528    ) -> "IntervalTier":
    529        """Inserts a blank region into the tier
    530
    531        Args:
    532            start:
    533            duration:
    534            collisionMode: Determines the behavior that occurs if
    535                an interval stradles the starting point
    536                - 'stretch' stretches the interval by /duration/ amount
    537                - 'split' splits the interval into two--everything to the
    538                    right of 'start' will be advanced by 'duration' seconds
    539                - 'no change' leaves the interval as is with no change
    540                - 'error' will stop execution and raise an error
    541
    542        Returns:
    543            the modified version of the current tier
    544        """
    545        utils.validateOption(
    546            "collisionMode", collisionMode, constants.WhitespaceCollision
    547        )
    548
    549        newEntryList = []
    550        for interval in self.entries:
    551            # Entry exists before the insertion point
    552            if interval.end <= start:
    553                newEntryList.append(interval)
    554            # Entry exists after the insertion point
    555            elif interval.start >= start:
    556                newEntryList.append(
    557                    Interval(
    558                        interval.start + duration,
    559                        interval.end + duration,
    560                        interval.label,
    561                    )
    562                )
    563            # Entry straddles the insertion point
    564            elif interval.start <= start and interval.end > start:
    565                if collisionMode == constants.WhitespaceCollision.STRETCH:
    566                    newEntryList.append(
    567                        Interval(
    568                            interval.start, interval.end + duration, interval.label
    569                        )
    570                    )
    571                elif collisionMode == constants.WhitespaceCollision.SPLIT:
    572                    # Left side of the split
    573                    newEntryList.append(Interval(interval.start, start, interval.label))
    574                    # Right side of the split
    575                    newEntryList.append(
    576                        (
    577                            start + duration,
    578                            start + duration + (interval.end - start),
    579                            interval.label,
    580                        )
    581                    )
    582                elif collisionMode == constants.WhitespaceCollision.NO_CHANGE:
    583                    newEntryList.append(interval)
    584                else:
    585                    raise errors.ArgumentError(
    586                        f"Collision occured during insertSpace() for interval '{interval}' "
    587                        f"and given white space insertion interval ({start}, {start + duration})"
    588                    )
    589
    590        newTier = self.new(
    591            entries=newEntryList, maxTimestamp=self.maxTimestamp + duration
    592        )
    593
    594        return newTier

Inserts a blank region into the tier

###### Arguments:

- start:
- duration:
- collisionMode: Determines the behavior that occurs if an interval stradles the starting point
  - 'stretch' stretches the interval by /duration/ amount
  - 'split' splits the interval into two--everything to the right of 'start' will be advanced by 'duration' seconds
  - 'no change' leaves the interval as is with no change
  - 'error' will stop execution and raise an error

###### Returns:

> the modified version of the current tier

def intersection( self, tier: IntervalTier, demarcator='-') -> IntervalTier: View Source

    596    def intersection(self, tier: "IntervalTier", demarcator="-") -> "IntervalTier":
    597        """Takes the set intersection of this tier and the given one
    598
    599        - The output will contain one interval for each overlapping pair
    600          e.g. [(1, 2, 'foo')] and [(1, 1.3, 'bang'), (1.7, 2, 'wizz')]
    601                -> [(1, 1.3, 'foo-bang'), (1.7, 2, 'foo-wizz')]
    602        - Only intervals that exist in both tiers will remain in the returned tier.
    603          e.g. [(1, 2, 'foo'), (3, 4, 'bar')] and [(1, 2, 'bang'), (2, 3, 'wizz')]
    604                -> [(1, 2, 'foo-bang')]
    605        - If intervals partially overlap, only the overlapping portion will be returned.
    606          e.g. [(1, 2, 'foo')] and [(0.5, 1.5, 'bang')]
    607                -> [(1, 1.5, 'foo-bang')]
    608
    609        Compare with IntervalTier.mergeLabels
    610
    611        Args:
    612            tier: the tier to intersect with
    613            demarcator: the character to separate the labels of the overlapping intervals
    614
    615        Returns:
    616            IntervalTier: the modified version of the current tier
    617        """
    618        retEntryList = []
    619        for interval in tier.entries:
    620            subTier = self.crop(
    621                interval.start, interval.end, CropCollision.TRUNCATED, False
    622            )
    623
    624            # Combine the labels in the two tiers
    625            subEntryList = [
    626                (
    627                    subInterval.start,
    628                    subInterval.end,
    629                    f"{subInterval.label}{demarcator}{interval.label}",
    630                )
    631                for subInterval in subTier.entries
    632            ]
    633
    634            retEntryList.extend(subEntryList)
    635
    636        newName = f"{self.name}-{tier.name}"
    637
    638        retTier = self.new(newName, retEntryList)
    639
    640        return retTier

Takes the set intersection of this tier and the given one

- The output will contain one interval for each overlapping pair e.g. [(1, 2, 'foo')] and [(1, 1.3, 'bang'), (1.7, 2, 'wizz')] -> [(1, 1.3, 'foo-bang'), (1.7, 2, 'foo-wizz')]
- Only intervals that exist in both tiers will remain in the returned tier. e.g. [(1, 2, 'foo'), (3, 4, 'bar')] and [(1, 2, 'bang'), (2, 3, 'wizz')] -> [(1, 2, 'foo-bang')]
- If intervals partially overlap, only the overlapping portion will be returned. e.g. [(1, 2, 'foo')] and [(0.5, 1.5, 'bang')] -> [(1, 1.5, 'foo-bang')]

Compare with IntervalTier.mergeLabels

###### Arguments:

- tier: the tier to intersect with
- demarcator: the character to separate the labels of the overlapping intervals

###### Returns:

> IntervalTier: the modified version of the current tier

def mergeLabels( self, tier: IntervalTier, demarcator: str = ',') -> IntervalTier: View Source

    642    def mergeLabels(
    643        self, tier: "IntervalTier", demarcator: str = ","
    644    ) -> "IntervalTier":
    645        """Merges labels of overlapping tiers into this tier
    646
    647        - All intervals in this tier will appear in the output; for the given tier, only intervals
    648          that overlap with content in this tier will appear in the output
    649          e.g. [(1, 2, 'foo'), (3, 4, 'bar')] and [(1, 2, 'bang'), (2, 3, 'wizz')]
    650                -> [(1, 2, 'foo(bang)'), (3, 4, 'bar()')]
    651        - If multiple entries exist in a subinterval, their labels will be concatenated
    652          e.g. [(1, 2, 'hi')] and [(1, 1.5, 'h'), (1.5, 2, 'ai')] -> [(1, 2, 'hi(h,ai)')]
    653
    654        compare with IntervalTier.intersection
    655
    656        Args:
    657            tier: the tier to intersect with
    658            demarcator: the string to separate items that fall in the same subinterval
    659
    660        Returns:
    661            IntervalTier: the modified version of the current tier
    662        """
    663        retEntryList = []
    664        for interval in self.entries:
    665            subTier = tier.crop(
    666                interval.start, interval.end, CropCollision.TRUNCATED, False
    667            )
    668            if len(subTier._entries) == 0:
    669                continue
    670
    671            subLabel = demarcator.join([entry.label for entry in subTier.entries])
    672            label = f"{interval.label}({subLabel})"
    673
    674            start = min(interval.start, subTier._entries[0].start)
    675            end = max(interval.end, subTier._entries[-1].end)
    676
    677            intersectedInterval = (
    678                start,
    679                end,
    680                label,
    681            )
    682
    683            retEntryList.append(intersectedInterval)
    684
    685        newName = f"{self.name}-{tier.name}"
    686
    687        retTier = self.new(newName, retEntryList)
    688
    689        return retTier

Merges labels of overlapping tiers into this tier

- All intervals in this tier will appear in the output; for the given tier, only intervals that overlap with content in this tier will appear in the output e.g. [(1, 2, 'foo'), (3, 4, 'bar')] and [(1, 2, 'bang'), (2, 3, 'wizz')] -> [(1, 2, 'foo(bang)'), (3, 4, 'bar()')]
- If multiple entries exist in a subinterval, their labels will be concatenated e.g. [(1, 2, 'hi')] and [(1, 1.5, 'h'), (1.5, 2, 'ai')] -> [(1, 2, 'hi(h,ai)')]

compare with IntervalTier.intersection

###### Arguments:

- tier: the tier to intersect with
- demarcator: the string to separate items that fall in the same subinterval

###### Returns:

> IntervalTier: the modified version of the current tier

def morph( self, targetTier: IntervalTier, filterFunc: Optional[Callable[[str], bool]] = None) -> IntervalTier: View Source

    691    def morph(
    692        self,
    693        targetTier: "IntervalTier",
    694        filterFunc: Optional[Callable[[str], bool]] = None,
    695    ) -> "IntervalTier":
    696        """Morphs the duration of segments in this tier to those in another
    697
    698        This preserves the labels and the duration of silence in
    699        this tier while changing the duration of labeled segments.
    700
    701        Args:
    702            targetTier:
    703            filterFunc: if specified, filters entries. The
    704                functor takes one argument, an Interval. It returns true
    705                if the Interval should be modified and false if not.
    706
    707        Returns:
    708            The modified version of the current tier
    709        """
    710        cumulativeAdjustAmount = 0
    711        newEntryList = []
    712        allIntervals = [self.entries, targetTier.entries]
    713        for sourceInterval, targetInterval in utils.safeZip(allIntervals, True):
    714            # sourceInterval.start - lastFromEnd -> was this interval and the
    715            # last one adjacent?
    716            newStart = sourceInterval.start + cumulativeAdjustAmount
    717
    718            currIntervalDuration = sourceInterval.end - sourceInterval.start
    719            if filterFunc is None or filterFunc(sourceInterval.label):
    720                newIntervalDuration = targetInterval.end - targetInterval.start
    721                cumulativeAdjustAmount += newIntervalDuration - currIntervalDuration
    722                newEnd = newStart + newIntervalDuration
    723            else:
    724                newEnd = newStart + currIntervalDuration
    725
    726            newEntryList.append(Interval(newStart, newEnd, sourceInterval.label))
    727
    728        newMin = self.minTimestamp
    729        cumulativeDifference = newEntryList[-1].end - self.entries[-1].end
    730        newMax = self.maxTimestamp + cumulativeDifference
    731
    732        return IntervalTier(self.name, newEntryList, newMin, newMax)

Morphs the duration of segments in this tier to those in another

This preserves the labels and the duration of silence in this tier while changing the duration of labeled segments.

###### Arguments:

- targetTier:
- filterFunc: if specified, filters entries. The functor takes one argument, an Interval. It returns true if the Interval should be modified and false if not.

###### Returns:

> The modified version of the current tier

def validate( self, reportingMode: Literal['silence', 'warning', 'error'] = 'warning') -> bool: View Source

    734    def validate(
    735        self, reportingMode: Literal["silence", "warning", "error"] = "warning"
    736    ) -> bool:
    737        """Validate this tier
    738
    739        Args:
    740            reportingMode (str): Determines the behavior if validation fails.
    741
    742        Returns:
    743            True if the tier is valid; False if not
    744        """
    745        utils.validateOption(
    746            "reportingMode", reportingMode, constants.ErrorReportingMode
    747        )
    748        errorReporter = utils.getErrorReporter(reportingMode)
    749
    750        isValid = True
    751        previousInterval = None
    752        for interval in self.entries:
    753            if interval.start >= interval.end:
    754                isValid = False
    755                errorReporter(
    756                    errors.TextgridStateError,
    757                    f"Invalid interval. End time occurs before or on the start time({interval}).",
    758                )
    759
    760            if previousInterval and previousInterval.end > interval.start:
    761                isValid = False
    762                errorReporter(
    763                    errors.TextgridStateError,
    764                    f"Intervals are not sorted in time: "
    765                    f"[({previousInterval}), ({interval})]",
    766                )
    767
    768            if utils.checkIsUndershoot(
    769                interval.start, self.minTimestamp, errorReporter
    770            ):
    771                isValid = False
    772
    773            if utils.checkIsOvershoot(interval.end, self.maxTimestamp, errorReporter):
    774                isValid = False
    775
    776            previousInterval = interval
    777
    778        return isValid

Validate this tier

###### Arguments:

- reportingMode (str): Determines the behavior if validation fails.

###### Returns:

> True if the tier is valid; False if not

##### Inherited Members

praatio.data_classes.textgrid_tier.TextgridTier
  name
  minTimestamp
  maxTimestamp
  errorReporter
  entries
  appendTier
  find
  new
  sort
  union