praatio.data_classes.point_tier

A PointTier is a tier containing an array of points -- data that exists at a specific point in time

  1"""
  2A PointTier is a tier containing an array of points -- data that exists at a specific point in time
  3"""
  4from typing import List, Tuple, Optional, Any, Sequence
  5
  6from typing_extensions import Literal
  7
  8from praatio.utilities.constants import (
  9    Point,
 10    POINT_TIER,
 11)
 12from praatio.utilities import constants
 13from praatio.utilities import errors
 14from praatio.utilities import utils
 15from praatio.utilities import my_math
 16
 17from praatio.data_classes import textgrid_tier
 18
 19
 20def _homogenizeEntries(entries):
 21    """
 22    Enforces consistency in points
 23
 24    - converts all entries to points
 25    - removes whitespace in labels
 26    - sorts values by time
 27    """
 28    processedEntries = [Point(float(time), label.strip()) for time, label in entries]
 29    processedEntries.sort()
 30    return processedEntries
 31
 32
 33def _calculateMinAndMaxTime(entries: Sequence[Point], minT=None, maxT=None):
 34    timeList = [time for time, label in entries]
 35    if minT is not None:
 36        timeList.append(float(minT))
 37    if maxT is not None:
 38        timeList.append(float(maxT))
 39
 40    try:
 41        calculatedMinT = min(timeList)
 42        calculatedMaxT = max(timeList)
 43    except ValueError:
 44        raise errors.TimelessTextgridTierException()
 45
 46    return (calculatedMinT, calculatedMaxT)
 47
 48
 49class PointTier(textgrid_tier.TextgridTier):
 50    tierType = POINT_TIER
 51    entryType = Point
 52
 53    def __init__(
 54        self,
 55        name: str,
 56        entries: List[Point],
 57        minT: Optional[float] = None,
 58        maxT: Optional[float] = None,
 59    ):
 60        """A point tier is for annotating instaneous events
 61
 62        The entries is of the form:
 63        [(timeVal1, label1), (timeVal2, label2), ]
 64
 65        The data stored in the labels can be anything but will
 66        be interpreted as text by praatio (the label could be descriptive
 67        text e.g. ('peak point here') or numerical data e.g. (pitch values
 68        like '132'))
 69        """
 70        entries = _homogenizeEntries(entries)
 71        calculatedMinT, calculatedMaxT = _calculateMinAndMaxTime(entries, minT, maxT)
 72
 73        super(PointTier, self).__init__(name, entries, calculatedMinT, calculatedMaxT)
 74
 75    @property
 76    def timestamps(self) -> List[float]:
 77        """All unique timestamps used in this tier"""
 78        tmpTimestamps = [time for time, _ in self.entries]
 79
 80        uniqueTimestamps = list(set(tmpTimestamps))
 81        uniqueTimestamps.sort()
 82
 83        return uniqueTimestamps
 84
 85    def crop(
 86        self,
 87        cropStart: float,
 88        cropEnd: float,
 89        mode: Literal["strict", "lax", "truncated"] = "lax",
 90        rebaseToZero: bool = True,
 91    ) -> "PointTier":
 92        """Creates a new tier containing all entries inside the new interval
 93
 94        Args:
 95            cropStart:
 96            cropEnd:
 97            mode: Mode is ignored.  This parameter is kept for
 98                compatibility with IntervalTier.crop()
 99            rebaseToZero: if True, all entries will have their
100                timestamps subtracted by *cropStart*.
101
102        Returns:
103            the modified version of the current tier
104        """
105        if cropStart >= cropEnd:
106            raise errors.ArgumentError(
107                f"Crop error: start time ({cropStart}) must occur before end time ({cropEnd})"
108            )
109
110        newEntries = []
111
112        for entry in self.entries:
113            timestamp = entry.time
114
115            if timestamp >= cropStart and timestamp <= cropEnd:
116                newEntries.append(entry)
117
118        if rebaseToZero is True:
119            newEntries = [
120                Point(timeV - cropStart, label) for timeV, label in newEntries
121            ]
122            minT = 0.0
123            maxT = cropEnd - cropStart
124        else:
125            minT = cropStart
126            maxT = cropEnd
127
128        return PointTier(self.name, newEntries, minT, maxT)
129
130    def deleteEntry(self, entry: Point) -> None:
131        """Removes an entry from the entries"""
132        self._entries.pop(self._entries.index(entry))
133
134    def dejitter(
135        self, referenceTier: textgrid_tier.TextgridTier, maxDifference: float = 0.001
136    ) -> "PointTier":
137        """
138        Set timestamps in this tier to be the same as values in the reference tier
139
140        Timestamps will only be moved if they are less than maxDifference away from the
141        reference time.
142
143        This can be used to correct minor alignment errors between tiers, as made when
144        annotating files manually, etc.
145
146        Args:
147            referenceTier: the IntervalTier or PointTier to use as a reference
148            maxDifference: the maximum amount to allow timestamps to be moved by
149
150        Returns:
151            the modified version of the current tier
152        """
153        referenceTimestamps = referenceTier.timestamps
154
155        newEntries = []
156        for time, label in self.entries:
157            timeCompare = min(referenceTimestamps, key=lambda x: abs(x - time))
158
159            if my_math.lessThanOrEqual(abs(time - timeCompare), maxDifference):
160                time = timeCompare
161            newEntries.append((time, label))
162
163        return self.new(entries=newEntries)
164
165    def editTimestamps(
166        self,
167        offset: float,
168        reportingMode: Literal["silence", "warning", "error"] = "warning",
169    ) -> "PointTier":
170        """Modifies all timestamps by a constant amount
171
172        Args:
173            offset:
174            reportingMode: one of "silence", "warning", or "error". This flag
175                determines the behavior if an entries moves outside of minTimestamp
176                or maxTimestamp after being edited
177
178        Returns:
179            the modified version of the current tier
180        """
181        utils.validateOption(
182            "reportingMode", reportingMode, constants.ErrorReportingMode
183        )
184        errorReporter = utils.getErrorReporter(reportingMode)
185
186        newEntries: List[Point] = []
187        for timestamp, label in self.entries:
188            newTimestamp = timestamp + offset
189            utils.checkIsUndershoot(newTimestamp, self.minTimestamp, errorReporter)
190            utils.checkIsOvershoot(newTimestamp, self.maxTimestamp, errorReporter)
191
192            if newTimestamp < 0:
193                continue
194
195            newEntries.append(Point(newTimestamp, label))
196
197        # Determine new min and max timestamps
198        timeList = [float(point.time) for point in newEntries]
199        newMin = min(timeList)
200        newMax = max(timeList)
201
202        if newMin > self.minTimestamp:
203            newMin = self.minTimestamp
204
205        if newMax < self.maxTimestamp:
206            newMax = self.maxTimestamp
207
208        return PointTier(self.name, newEntries, newMin, newMax)
209
210    def getValuesAtPoints(
211        self,
212        dataTupleList: List[Tuple[float, ...]],
213        fuzzyMatching: bool = False,
214    ) -> List[Tuple[Any, ...]]:
215        """Get the values that occur at points in the point tier
216
217        The procedure assumes that all data is ordered in time.
218        dataTupleList should be in the form
219        [(t1, v1a, v1b, ..), (t2, v2a, v2b, ..), ..]
220
221        It returns the data in the form of
222        [(t1, v1a, v1b, ..), (t2, v2a, v2b), ..]
223
224        The procedure makes one pass through dataTupleList and one
225        pass through self.entries.  If the data is not sequentially
226        ordered, the incorrect response will be returned.
227
228        Args:
229            dataTupleList:
230            fuzzyMatching: if True, if there is not a feature value
231                at a point, the nearest feature value will be taken.
232
233        Returns:
234            A list of values that exist at the given timepoints
235        """
236
237        currentIndex = 0
238        retList = []
239
240        sortedDataTupleList = sorted(dataTupleList)
241        for timestamp, label in self.entries:
242            retTuple = utils.getValueAtTime(
243                timestamp,
244                sortedDataTupleList,
245                fuzzyMatching=fuzzyMatching,
246                startI=currentIndex,
247            )
248            retRow, currentIndex = retTuple
249            retList.append(retRow)
250
251        return retList
252
253    def eraseRegion(
254        self,
255        start: float,
256        end: float,
257        collisionMode: Literal["truncate", "categorical", "error"] = "error",
258        doShrink: bool = True,
259    ) -> "PointTier":
260        """Makes a region in a tier blank (removes all contained entries)
261
262        Args:
263            start: the start of the deletion interval
264            end: the end of the deletion interval
265            collisionMode: Ignored for the moment (added for compatibility with
266                eraseRegion() for Interval Tiers)
267            doShrink: if True, moves leftward by (/end/ - /start/) all points
268                to the right of /end/
269
270        Returns:
271            The modified version of the current tier
272        """
273
274        newTier = self.new()
275        croppedTier = newTier.crop(start, end, constants.CropCollision.TRUNCATED, False)
276        matchList = croppedTier.entries
277
278        if len(matchList) > 0:
279            # Remove all the matches from the entries
280            # Go in reverse order because we're destructively altering
281            # the order of the list (messes up index order)
282            for point in matchList[::-1]:
283                newTier.deleteEntry(point)
284
285        if doShrink is True:
286            newEntries = []
287            diff = end - start
288            for point in newTier.entries:
289                if point.time < start:
290                    newEntries.append(point)
291                elif point.time > end:
292                    newEntries.append(Point(point.time - diff, point.label))
293
294            newMax = newTier.maxTimestamp - diff
295            newTier = newTier.new(entries=newEntries, maxTimestamp=newMax)
296
297        return newTier
298
299    def insertEntry(
300        self,
301        entry: Point,
302        collisionMode: Literal["replace", "merge", "error"] = "error",
303        collisionReportingMode: Literal["silence", "warning"] = "warning",
304    ) -> None:
305        """Inserts an interval into the tier
306
307        Args:
308            entry: the entry to insert
309            collisionMode: determines the behavior if intervals exist in
310                the insertion area.
311                - 'replace', existing items will be removed
312                - 'merge', inserting item will be fused with existing items
313                - 'error', will throw TextgridCollisionException
314            collisionReportingMode:
315
316        Returns:
317            None
318        """
319
320        utils.validateOption(
321            "collisionMode", collisionMode, constants.IntervalCollision
322        )
323        utils.validateOption(
324            "collisionReportingMode",
325            collisionReportingMode,
326            constants.ErrorReportingMode,
327        )
328        collisionReporter = utils.getErrorReporter(collisionReportingMode)
329
330        if not isinstance(entry, Point):
331            newPoint = Point(entry[0], entry[1])
332        else:
333            newPoint = entry
334
335        matchList = []
336        i = None
337        for i, point in enumerate(self.entries):
338            if point.time == newPoint.time:
339                matchList.append(point)
340                break
341
342        if len(matchList) == 0:
343            self._entries.append(newPoint)
344
345        elif collisionMode == constants.IntervalCollision.REPLACE:
346            self.deleteEntry(self.entries[i])
347            self._entries.append(newPoint)
348
349        elif collisionMode == constants.IntervalCollision.MERGE:
350            oldPoint = self.entries[i]
351            mergedPoint = Point(
352                newPoint.time, "-".join([oldPoint.label, newPoint.label])
353            )
354            self.deleteEntry(self._entries[i])
355            self._entries.append(mergedPoint)
356
357        else:
358            raise errors.CollisionError(
359                f"Attempted to insert interval {point} into tier {self.name} "
360                "of textgrid but overlapping entries "
361                f"{[tuple(interval) for interval in matchList]} "
362                "already exist"
363            )
364
365        self.sort()
366
367        if len(matchList) != 0:
368            collisionReporter(
369                errors.CollisionError,
370                f"Collision warning for ({point}) with items ({matchList}) of tier '{self.name}'",
371            )
372
373    def insertSpace(
374        self,
375        start: float,
376        duration: float,
377        _collisionMode: Literal["stretch", "split", "no_change", "error"] = "error",
378    ) -> "PointTier":
379        """Inserts a region into the tier
380
381        Args:
382            start: the start time to insert a space at
383            duration: the duration of the space to insert
384            collisionMode: Ignored for the moment (added for compatibility
385                with insertSpace() for Interval Tiers)
386
387        Returns:
388            PointTier: the modified version of the current tier
389        """
390
391        newEntries = []
392        for point in self.entries:
393            if point.time <= start:
394                newEntries.append(point)
395            elif point.time > start:
396                newEntries.append(Point(point.time + duration, point.label))
397
398        newTier = self.new(
399            entries=newEntries, maxTimestamp=self.maxTimestamp + duration
400        )
401
402        return newTier
403
404    def validate(
405        self, reportingMode: Literal["silence", "warning", "error"] = "warning"
406    ) -> bool:
407        """Validate this tier
408
409        Returns whether the tier is valid or not. If reportingMode is "warning"
410        or "error" this will also print on error or stop execution, respectively.
411
412        Args:
413            reportingMode: Determines the behavior if there is a size difference
414                between the maxTimestamp in the tier and the current textgrid.
415
416        Returns:
417            True if this tier is valid; False otherwise
418        """
419        utils.validateOption(
420            "reportingMode", reportingMode, constants.ErrorReportingMode
421        )
422        errorReporter = utils.getErrorReporter(reportingMode)
423
424        isValid = True
425        previousPoint = None
426        for point in self.entries:
427            if previousPoint and previousPoint.time > point.time:
428                isValid = False
429                errorReporter(
430                    errors.TextgridStateError,
431                    f"Points are not sorted in time: "
432                    f"[({previousPoint}), ({point})]",
433                )
434
435            if utils.checkIsUndershoot(point.time, self.minTimestamp, errorReporter):
436                isValid = False
437
438            if utils.checkIsOvershoot(point.time, self.maxTimestamp, errorReporter):
439                isValid = False
440
441            previousPoint = point
442
443        return isValid
class PointTier(praatio.data_classes.textgrid_tier.TextgridTier):
 50class PointTier(textgrid_tier.TextgridTier):
 51    tierType = POINT_TIER
 52    entryType = Point
 53
 54    def __init__(
 55        self,
 56        name: str,
 57        entries: List[Point],
 58        minT: Optional[float] = None,
 59        maxT: Optional[float] = None,
 60    ):
 61        """A point tier is for annotating instaneous events
 62
 63        The entries is of the form:
 64        [(timeVal1, label1), (timeVal2, label2), ]
 65
 66        The data stored in the labels can be anything but will
 67        be interpreted as text by praatio (the label could be descriptive
 68        text e.g. ('peak point here') or numerical data e.g. (pitch values
 69        like '132'))
 70        """
 71        entries = _homogenizeEntries(entries)
 72        calculatedMinT, calculatedMaxT = _calculateMinAndMaxTime(entries, minT, maxT)
 73
 74        super(PointTier, self).__init__(name, entries, calculatedMinT, calculatedMaxT)
 75
 76    @property
 77    def timestamps(self) -> List[float]:
 78        """All unique timestamps used in this tier"""
 79        tmpTimestamps = [time for time, _ in self.entries]
 80
 81        uniqueTimestamps = list(set(tmpTimestamps))
 82        uniqueTimestamps.sort()
 83
 84        return uniqueTimestamps
 85
 86    def crop(
 87        self,
 88        cropStart: float,
 89        cropEnd: float,
 90        mode: Literal["strict", "lax", "truncated"] = "lax",
 91        rebaseToZero: bool = True,
 92    ) -> "PointTier":
 93        """Creates a new tier containing all entries inside the new interval
 94
 95        Args:
 96            cropStart:
 97            cropEnd:
 98            mode: Mode is ignored.  This parameter is kept for
 99                compatibility with IntervalTier.crop()
100            rebaseToZero: if True, all entries will have their
101                timestamps subtracted by *cropStart*.
102
103        Returns:
104            the modified version of the current tier
105        """
106        if cropStart >= cropEnd:
107            raise errors.ArgumentError(
108                f"Crop error: start time ({cropStart}) must occur before end time ({cropEnd})"
109            )
110
111        newEntries = []
112
113        for entry in self.entries:
114            timestamp = entry.time
115
116            if timestamp >= cropStart and timestamp <= cropEnd:
117                newEntries.append(entry)
118
119        if rebaseToZero is True:
120            newEntries = [
121                Point(timeV - cropStart, label) for timeV, label in newEntries
122            ]
123            minT = 0.0
124            maxT = cropEnd - cropStart
125        else:
126            minT = cropStart
127            maxT = cropEnd
128
129        return PointTier(self.name, newEntries, minT, maxT)
130
131    def deleteEntry(self, entry: Point) -> None:
132        """Removes an entry from the entries"""
133        self._entries.pop(self._entries.index(entry))
134
135    def dejitter(
136        self, referenceTier: textgrid_tier.TextgridTier, maxDifference: float = 0.001
137    ) -> "PointTier":
138        """
139        Set timestamps in this tier to be the same as values in the reference tier
140
141        Timestamps will only be moved if they are less than maxDifference away from the
142        reference time.
143
144        This can be used to correct minor alignment errors between tiers, as made when
145        annotating files manually, etc.
146
147        Args:
148            referenceTier: the IntervalTier or PointTier to use as a reference
149            maxDifference: the maximum amount to allow timestamps to be moved by
150
151        Returns:
152            the modified version of the current tier
153        """
154        referenceTimestamps = referenceTier.timestamps
155
156        newEntries = []
157        for time, label in self.entries:
158            timeCompare = min(referenceTimestamps, key=lambda x: abs(x - time))
159
160            if my_math.lessThanOrEqual(abs(time - timeCompare), maxDifference):
161                time = timeCompare
162            newEntries.append((time, label))
163
164        return self.new(entries=newEntries)
165
166    def editTimestamps(
167        self,
168        offset: float,
169        reportingMode: Literal["silence", "warning", "error"] = "warning",
170    ) -> "PointTier":
171        """Modifies all timestamps by a constant amount
172
173        Args:
174            offset:
175            reportingMode: one of "silence", "warning", or "error". This flag
176                determines the behavior if an entries moves outside of minTimestamp
177                or maxTimestamp after being edited
178
179        Returns:
180            the modified version of the current tier
181        """
182        utils.validateOption(
183            "reportingMode", reportingMode, constants.ErrorReportingMode
184        )
185        errorReporter = utils.getErrorReporter(reportingMode)
186
187        newEntries: List[Point] = []
188        for timestamp, label in self.entries:
189            newTimestamp = timestamp + offset
190            utils.checkIsUndershoot(newTimestamp, self.minTimestamp, errorReporter)
191            utils.checkIsOvershoot(newTimestamp, self.maxTimestamp, errorReporter)
192
193            if newTimestamp < 0:
194                continue
195
196            newEntries.append(Point(newTimestamp, label))
197
198        # Determine new min and max timestamps
199        timeList = [float(point.time) for point in newEntries]
200        newMin = min(timeList)
201        newMax = max(timeList)
202
203        if newMin > self.minTimestamp:
204            newMin = self.minTimestamp
205
206        if newMax < self.maxTimestamp:
207            newMax = self.maxTimestamp
208
209        return PointTier(self.name, newEntries, newMin, newMax)
210
211    def getValuesAtPoints(
212        self,
213        dataTupleList: List[Tuple[float, ...]],
214        fuzzyMatching: bool = False,
215    ) -> List[Tuple[Any, ...]]:
216        """Get the values that occur at points in the point tier
217
218        The procedure assumes that all data is ordered in time.
219        dataTupleList should be in the form
220        [(t1, v1a, v1b, ..), (t2, v2a, v2b, ..), ..]
221
222        It returns the data in the form of
223        [(t1, v1a, v1b, ..), (t2, v2a, v2b), ..]
224
225        The procedure makes one pass through dataTupleList and one
226        pass through self.entries.  If the data is not sequentially
227        ordered, the incorrect response will be returned.
228
229        Args:
230            dataTupleList:
231            fuzzyMatching: if True, if there is not a feature value
232                at a point, the nearest feature value will be taken.
233
234        Returns:
235            A list of values that exist at the given timepoints
236        """
237
238        currentIndex = 0
239        retList = []
240
241        sortedDataTupleList = sorted(dataTupleList)
242        for timestamp, label in self.entries:
243            retTuple = utils.getValueAtTime(
244                timestamp,
245                sortedDataTupleList,
246                fuzzyMatching=fuzzyMatching,
247                startI=currentIndex,
248            )
249            retRow, currentIndex = retTuple
250            retList.append(retRow)
251
252        return retList
253
254    def eraseRegion(
255        self,
256        start: float,
257        end: float,
258        collisionMode: Literal["truncate", "categorical", "error"] = "error",
259        doShrink: bool = True,
260    ) -> "PointTier":
261        """Makes a region in a tier blank (removes all contained entries)
262
263        Args:
264            start: the start of the deletion interval
265            end: the end of the deletion interval
266            collisionMode: Ignored for the moment (added for compatibility with
267                eraseRegion() for Interval Tiers)
268            doShrink: if True, moves leftward by (/end/ - /start/) all points
269                to the right of /end/
270
271        Returns:
272            The modified version of the current tier
273        """
274
275        newTier = self.new()
276        croppedTier = newTier.crop(start, end, constants.CropCollision.TRUNCATED, False)
277        matchList = croppedTier.entries
278
279        if len(matchList) > 0:
280            # Remove all the matches from the entries
281            # Go in reverse order because we're destructively altering
282            # the order of the list (messes up index order)
283            for point in matchList[::-1]:
284                newTier.deleteEntry(point)
285
286        if doShrink is True:
287            newEntries = []
288            diff = end - start
289            for point in newTier.entries:
290                if point.time < start:
291                    newEntries.append(point)
292                elif point.time > end:
293                    newEntries.append(Point(point.time - diff, point.label))
294
295            newMax = newTier.maxTimestamp - diff
296            newTier = newTier.new(entries=newEntries, maxTimestamp=newMax)
297
298        return newTier
299
300    def insertEntry(
301        self,
302        entry: Point,
303        collisionMode: Literal["replace", "merge", "error"] = "error",
304        collisionReportingMode: Literal["silence", "warning"] = "warning",
305    ) -> None:
306        """Inserts an interval into the tier
307
308        Args:
309            entry: the entry to insert
310            collisionMode: determines the behavior if intervals exist in
311                the insertion area.
312                - 'replace', existing items will be removed
313                - 'merge', inserting item will be fused with existing items
314                - 'error', will throw TextgridCollisionException
315            collisionReportingMode:
316
317        Returns:
318            None
319        """
320
321        utils.validateOption(
322            "collisionMode", collisionMode, constants.IntervalCollision
323        )
324        utils.validateOption(
325            "collisionReportingMode",
326            collisionReportingMode,
327            constants.ErrorReportingMode,
328        )
329        collisionReporter = utils.getErrorReporter(collisionReportingMode)
330
331        if not isinstance(entry, Point):
332            newPoint = Point(entry[0], entry[1])
333        else:
334            newPoint = entry
335
336        matchList = []
337        i = None
338        for i, point in enumerate(self.entries):
339            if point.time == newPoint.time:
340                matchList.append(point)
341                break
342
343        if len(matchList) == 0:
344            self._entries.append(newPoint)
345
346        elif collisionMode == constants.IntervalCollision.REPLACE:
347            self.deleteEntry(self.entries[i])
348            self._entries.append(newPoint)
349
350        elif collisionMode == constants.IntervalCollision.MERGE:
351            oldPoint = self.entries[i]
352            mergedPoint = Point(
353                newPoint.time, "-".join([oldPoint.label, newPoint.label])
354            )
355            self.deleteEntry(self._entries[i])
356            self._entries.append(mergedPoint)
357
358        else:
359            raise errors.CollisionError(
360                f"Attempted to insert interval {point} into tier {self.name} "
361                "of textgrid but overlapping entries "
362                f"{[tuple(interval) for interval in matchList]} "
363                "already exist"
364            )
365
366        self.sort()
367
368        if len(matchList) != 0:
369            collisionReporter(
370                errors.CollisionError,
371                f"Collision warning for ({point}) with items ({matchList}) of tier '{self.name}'",
372            )
373
374    def insertSpace(
375        self,
376        start: float,
377        duration: float,
378        _collisionMode: Literal["stretch", "split", "no_change", "error"] = "error",
379    ) -> "PointTier":
380        """Inserts a region into the tier
381
382        Args:
383            start: the start time to insert a space at
384            duration: the duration of the space to insert
385            collisionMode: Ignored for the moment (added for compatibility
386                with insertSpace() for Interval Tiers)
387
388        Returns:
389            PointTier: the modified version of the current tier
390        """
391
392        newEntries = []
393        for point in self.entries:
394            if point.time <= start:
395                newEntries.append(point)
396            elif point.time > start:
397                newEntries.append(Point(point.time + duration, point.label))
398
399        newTier = self.new(
400            entries=newEntries, maxTimestamp=self.maxTimestamp + duration
401        )
402
403        return newTier
404
405    def validate(
406        self, reportingMode: Literal["silence", "warning", "error"] = "warning"
407    ) -> bool:
408        """Validate this tier
409
410        Returns whether the tier is valid or not. If reportingMode is "warning"
411        or "error" this will also print on error or stop execution, respectively.
412
413        Args:
414            reportingMode: Determines the behavior if there is a size difference
415                between the maxTimestamp in the tier and the current textgrid.
416
417        Returns:
418            True if this tier is valid; False otherwise
419        """
420        utils.validateOption(
421            "reportingMode", reportingMode, constants.ErrorReportingMode
422        )
423        errorReporter = utils.getErrorReporter(reportingMode)
424
425        isValid = True
426        previousPoint = None
427        for point in self.entries:
428            if previousPoint and previousPoint.time > point.time:
429                isValid = False
430                errorReporter(
431                    errors.TextgridStateError,
432                    f"Points are not sorted in time: "
433                    f"[({previousPoint}), ({point})]",
434                )
435
436            if utils.checkIsUndershoot(point.time, self.minTimestamp, errorReporter):
437                isValid = False
438
439            if utils.checkIsOvershoot(point.time, self.maxTimestamp, errorReporter):
440                isValid = False
441
442            previousPoint = point
443
444        return isValid

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

PointTier( name: str, entries: List[praatio.utilities.constants.Point], minT: Optional[float] = None, maxT: Optional[float] = None)
54    def __init__(
55        self,
56        name: str,
57        entries: List[Point],
58        minT: Optional[float] = None,
59        maxT: Optional[float] = None,
60    ):
61        """A point tier is for annotating instaneous events
62
63        The entries is of the form:
64        [(timeVal1, label1), (timeVal2, label2), ]
65
66        The data stored in the labels can be anything but will
67        be interpreted as text by praatio (the label could be descriptive
68        text e.g. ('peak point here') or numerical data e.g. (pitch values
69        like '132'))
70        """
71        entries = _homogenizeEntries(entries)
72        calculatedMinT, calculatedMaxT = _calculateMinAndMaxTime(entries, minT, maxT)
73
74        super(PointTier, self).__init__(name, entries, calculatedMinT, calculatedMaxT)

A point tier is for annotating instaneous events

The entries is of the form: [(timeVal1, label1), (timeVal2, 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. ('peak point here') or numerical data e.g. (pitch values like '132'))

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

All unique timestamps used in this tier

def crop( self, cropStart: float, cropEnd: float, mode: Literal['strict', 'lax', 'truncated'] = 'lax', rebaseToZero: bool = True) -> PointTier:
 86    def crop(
 87        self,
 88        cropStart: float,
 89        cropEnd: float,
 90        mode: Literal["strict", "lax", "truncated"] = "lax",
 91        rebaseToZero: bool = True,
 92    ) -> "PointTier":
 93        """Creates a new tier containing all entries inside the new interval
 94
 95        Args:
 96            cropStart:
 97            cropEnd:
 98            mode: Mode is ignored.  This parameter is kept for
 99                compatibility with IntervalTier.crop()
100            rebaseToZero: if True, all entries will have their
101                timestamps subtracted by *cropStart*.
102
103        Returns:
104            the modified version of the current tier
105        """
106        if cropStart >= cropEnd:
107            raise errors.ArgumentError(
108                f"Crop error: start time ({cropStart}) must occur before end time ({cropEnd})"
109            )
110
111        newEntries = []
112
113        for entry in self.entries:
114            timestamp = entry.time
115
116            if timestamp >= cropStart and timestamp <= cropEnd:
117                newEntries.append(entry)
118
119        if rebaseToZero is True:
120            newEntries = [
121                Point(timeV - cropStart, label) for timeV, label in newEntries
122            ]
123            minT = 0.0
124            maxT = cropEnd - cropStart
125        else:
126            minT = cropStart
127            maxT = cropEnd
128
129        return PointTier(self.name, newEntries, minT, maxT)

Creates a new tier containing all entries inside the new interval

Arguments:
  • cropStart:
  • cropEnd:
  • mode: Mode is ignored. This parameter is kept for compatibility with IntervalTier.crop()
  • rebaseToZero: if True, all entries will have their timestamps subtracted by cropStart.
Returns:

the modified version of the current tier

def deleteEntry(self, entry: praatio.utilities.constants.Point) -> None:
131    def deleteEntry(self, entry: Point) -> None:
132        """Removes an entry from the entries"""
133        self._entries.pop(self._entries.index(entry))

Removes an entry from the entries

def dejitter( self, referenceTier: praatio.data_classes.textgrid_tier.TextgridTier, maxDifference: float = 0.001) -> PointTier:
135    def dejitter(
136        self, referenceTier: textgrid_tier.TextgridTier, maxDifference: float = 0.001
137    ) -> "PointTier":
138        """
139        Set timestamps in this tier to be the same as values in the reference tier
140
141        Timestamps will only be moved if they are less than maxDifference away from the
142        reference time.
143
144        This can be used to correct minor alignment errors between tiers, as made when
145        annotating files manually, etc.
146
147        Args:
148            referenceTier: the IntervalTier or PointTier to use as a reference
149            maxDifference: the maximum amount to allow timestamps to be moved by
150
151        Returns:
152            the modified version of the current tier
153        """
154        referenceTimestamps = referenceTier.timestamps
155
156        newEntries = []
157        for time, label in self.entries:
158            timeCompare = min(referenceTimestamps, key=lambda x: abs(x - time))
159
160            if my_math.lessThanOrEqual(abs(time - timeCompare), maxDifference):
161                time = timeCompare
162            newEntries.append((time, label))
163
164        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 editTimestamps( self, offset: float, reportingMode: Literal['silence', 'warning', 'error'] = 'warning') -> PointTier:
166    def editTimestamps(
167        self,
168        offset: float,
169        reportingMode: Literal["silence", "warning", "error"] = "warning",
170    ) -> "PointTier":
171        """Modifies all timestamps by a constant amount
172
173        Args:
174            offset:
175            reportingMode: one of "silence", "warning", or "error". This flag
176                determines the behavior if an entries moves outside of minTimestamp
177                or maxTimestamp after being edited
178
179        Returns:
180            the modified version of the current tier
181        """
182        utils.validateOption(
183            "reportingMode", reportingMode, constants.ErrorReportingMode
184        )
185        errorReporter = utils.getErrorReporter(reportingMode)
186
187        newEntries: List[Point] = []
188        for timestamp, label in self.entries:
189            newTimestamp = timestamp + offset
190            utils.checkIsUndershoot(newTimestamp, self.minTimestamp, errorReporter)
191            utils.checkIsOvershoot(newTimestamp, self.maxTimestamp, errorReporter)
192
193            if newTimestamp < 0:
194                continue
195
196            newEntries.append(Point(newTimestamp, label))
197
198        # Determine new min and max timestamps
199        timeList = [float(point.time) for point in newEntries]
200        newMin = min(timeList)
201        newMax = max(timeList)
202
203        if newMin > self.minTimestamp:
204            newMin = self.minTimestamp
205
206        if newMax < self.maxTimestamp:
207            newMax = self.maxTimestamp
208
209        return PointTier(self.name, newEntries, newMin, newMax)

Modifies all timestamps by a constant amount

Arguments:
  • offset:
  • reportingMode: one of "silence", "warning", or "error". This flag determines the behavior if an entries moves outside of minTimestamp or maxTimestamp after being edited
Returns:

the modified version of the current tier

def getValuesAtPoints( self, dataTupleList: List[Tuple[float, ...]], fuzzyMatching: bool = False) -> List[Tuple[Any, ...]]:
211    def getValuesAtPoints(
212        self,
213        dataTupleList: List[Tuple[float, ...]],
214        fuzzyMatching: bool = False,
215    ) -> List[Tuple[Any, ...]]:
216        """Get the values that occur at points in the point tier
217
218        The procedure assumes that all data is ordered in time.
219        dataTupleList should be in the form
220        [(t1, v1a, v1b, ..), (t2, v2a, v2b, ..), ..]
221
222        It returns the data in the form of
223        [(t1, v1a, v1b, ..), (t2, v2a, v2b), ..]
224
225        The procedure makes one pass through dataTupleList and one
226        pass through self.entries.  If the data is not sequentially
227        ordered, the incorrect response will be returned.
228
229        Args:
230            dataTupleList:
231            fuzzyMatching: if True, if there is not a feature value
232                at a point, the nearest feature value will be taken.
233
234        Returns:
235            A list of values that exist at the given timepoints
236        """
237
238        currentIndex = 0
239        retList = []
240
241        sortedDataTupleList = sorted(dataTupleList)
242        for timestamp, label in self.entries:
243            retTuple = utils.getValueAtTime(
244                timestamp,
245                sortedDataTupleList,
246                fuzzyMatching=fuzzyMatching,
247                startI=currentIndex,
248            )
249            retRow, currentIndex = retTuple
250            retList.append(retRow)
251
252        return retList

Get the values that occur at points in the point tier

The procedure assumes that all data is ordered in time. dataTupleList should be in the form [(t1, v1a, v1b, ..), (t2, v2a, v2b, ..), ..]

It returns the data in the form of [(t1, v1a, v1b, ..), (t2, v2a, v2b), ..]

The procedure makes one pass through dataTupleList and one pass through self.entries. If the data is not sequentially ordered, the incorrect response will be returned.

Arguments:
  • dataTupleList:
  • fuzzyMatching: if True, if there is not a feature value at a point, the nearest feature value will be taken.
Returns:

A list of values that exist at the given timepoints

def eraseRegion( self, start: float, end: float, collisionMode: Literal['truncate', 'categorical', 'error'] = 'error', doShrink: bool = True) -> PointTier:
254    def eraseRegion(
255        self,
256        start: float,
257        end: float,
258        collisionMode: Literal["truncate", "categorical", "error"] = "error",
259        doShrink: bool = True,
260    ) -> "PointTier":
261        """Makes a region in a tier blank (removes all contained entries)
262
263        Args:
264            start: the start of the deletion interval
265            end: the end of the deletion interval
266            collisionMode: Ignored for the moment (added for compatibility with
267                eraseRegion() for Interval Tiers)
268            doShrink: if True, moves leftward by (/end/ - /start/) all points
269                to the right of /end/
270
271        Returns:
272            The modified version of the current tier
273        """
274
275        newTier = self.new()
276        croppedTier = newTier.crop(start, end, constants.CropCollision.TRUNCATED, False)
277        matchList = croppedTier.entries
278
279        if len(matchList) > 0:
280            # Remove all the matches from the entries
281            # Go in reverse order because we're destructively altering
282            # the order of the list (messes up index order)
283            for point in matchList[::-1]:
284                newTier.deleteEntry(point)
285
286        if doShrink is True:
287            newEntries = []
288            diff = end - start
289            for point in newTier.entries:
290                if point.time < start:
291                    newEntries.append(point)
292                elif point.time > end:
293                    newEntries.append(Point(point.time - diff, point.label))
294
295            newMax = newTier.maxTimestamp - diff
296            newTier = newTier.new(entries=newEntries, maxTimestamp=newMax)
297
298        return newTier

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

Arguments:
  • start: the start of the deletion interval
  • end: the end of the deletion interval
  • collisionMode: Ignored for the moment (added for compatibility with eraseRegion() for Interval Tiers)
  • doShrink: if True, moves leftward by (/end/ - /start/) all points to the right of /end/
Returns:

The modified version of the current tier

def insertEntry( self, entry: praatio.utilities.constants.Point, collisionMode: Literal['replace', 'merge', 'error'] = 'error', collisionReportingMode: Literal['silence', 'warning'] = 'warning') -> None:
300    def insertEntry(
301        self,
302        entry: Point,
303        collisionMode: Literal["replace", "merge", "error"] = "error",
304        collisionReportingMode: Literal["silence", "warning"] = "warning",
305    ) -> None:
306        """Inserts an interval into the tier
307
308        Args:
309            entry: the entry to insert
310            collisionMode: determines the behavior if intervals exist in
311                the insertion area.
312                - 'replace', existing items will be removed
313                - 'merge', inserting item will be fused with existing items
314                - 'error', will throw TextgridCollisionException
315            collisionReportingMode:
316
317        Returns:
318            None
319        """
320
321        utils.validateOption(
322            "collisionMode", collisionMode, constants.IntervalCollision
323        )
324        utils.validateOption(
325            "collisionReportingMode",
326            collisionReportingMode,
327            constants.ErrorReportingMode,
328        )
329        collisionReporter = utils.getErrorReporter(collisionReportingMode)
330
331        if not isinstance(entry, Point):
332            newPoint = Point(entry[0], entry[1])
333        else:
334            newPoint = entry
335
336        matchList = []
337        i = None
338        for i, point in enumerate(self.entries):
339            if point.time == newPoint.time:
340                matchList.append(point)
341                break
342
343        if len(matchList) == 0:
344            self._entries.append(newPoint)
345
346        elif collisionMode == constants.IntervalCollision.REPLACE:
347            self.deleteEntry(self.entries[i])
348            self._entries.append(newPoint)
349
350        elif collisionMode == constants.IntervalCollision.MERGE:
351            oldPoint = self.entries[i]
352            mergedPoint = Point(
353                newPoint.time, "-".join([oldPoint.label, newPoint.label])
354            )
355            self.deleteEntry(self._entries[i])
356            self._entries.append(mergedPoint)
357
358        else:
359            raise errors.CollisionError(
360                f"Attempted to insert interval {point} into tier {self.name} "
361                "of textgrid but overlapping entries "
362                f"{[tuple(interval) for interval in matchList]} "
363                "already exist"
364            )
365
366        self.sort()
367
368        if len(matchList) != 0:
369            collisionReporter(
370                errors.CollisionError,
371                f"Collision warning for ({point}) with items ({matchList}) of tier '{self.name}'",
372            )

Inserts an interval into the tier

Arguments:
  • entry: the entry to insert
  • collisionMode: determines the behavior if intervals exist in the insertion area.
    • 'replace', existing items will be removed
    • 'merge', inserting item will be fused with existing items
    • 'error', will throw TextgridCollisionException
  • collisionReportingMode:
Returns:

None

def insertSpace( self, start: float, duration: float, _collisionMode: Literal['stretch', 'split', 'no_change', 'error'] = 'error') -> PointTier:
374    def insertSpace(
375        self,
376        start: float,
377        duration: float,
378        _collisionMode: Literal["stretch", "split", "no_change", "error"] = "error",
379    ) -> "PointTier":
380        """Inserts a region into the tier
381
382        Args:
383            start: the start time to insert a space at
384            duration: the duration of the space to insert
385            collisionMode: Ignored for the moment (added for compatibility
386                with insertSpace() for Interval Tiers)
387
388        Returns:
389            PointTier: the modified version of the current tier
390        """
391
392        newEntries = []
393        for point in self.entries:
394            if point.time <= start:
395                newEntries.append(point)
396            elif point.time > start:
397                newEntries.append(Point(point.time + duration, point.label))
398
399        newTier = self.new(
400            entries=newEntries, maxTimestamp=self.maxTimestamp + duration
401        )
402
403        return newTier

Inserts a region into the tier

Arguments:
  • start: the start time to insert a space at
  • duration: the duration of the space to insert
  • collisionMode: Ignored for the moment (added for compatibility with insertSpace() for Interval Tiers)
Returns:

PointTier: the modified version of the current tier

def validate( self, reportingMode: Literal['silence', 'warning', 'error'] = 'warning') -> bool:
405    def validate(
406        self, reportingMode: Literal["silence", "warning", "error"] = "warning"
407    ) -> bool:
408        """Validate this tier
409
410        Returns whether the tier is valid or not. If reportingMode is "warning"
411        or "error" this will also print on error or stop execution, respectively.
412
413        Args:
414            reportingMode: Determines the behavior if there is a size difference
415                between the maxTimestamp in the tier and the current textgrid.
416
417        Returns:
418            True if this tier is valid; False otherwise
419        """
420        utils.validateOption(
421            "reportingMode", reportingMode, constants.ErrorReportingMode
422        )
423        errorReporter = utils.getErrorReporter(reportingMode)
424
425        isValid = True
426        previousPoint = None
427        for point in self.entries:
428            if previousPoint and previousPoint.time > point.time:
429                isValid = False
430                errorReporter(
431                    errors.TextgridStateError,
432                    f"Points are not sorted in time: "
433                    f"[({previousPoint}), ({point})]",
434                )
435
436            if utils.checkIsUndershoot(point.time, self.minTimestamp, errorReporter):
437                isValid = False
438
439            if utils.checkIsOvershoot(point.time, self.maxTimestamp, errorReporter):
440                isValid = False
441
442            previousPoint = point
443
444        return isValid

Validate this tier

Returns whether the tier is valid or not. If reportingMode is "warning" or "error" this will also print on error or stop execution, respectively.

Arguments:
  • reportingMode: Determines the behavior if there is a size difference between the maxTimestamp in the tier and the current textgrid.
Returns:

True if this tier is valid; False otherwise