boris-behav-obs 9.7.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of boris-behav-obs might be problematic. Click here for more details.

Files changed (109) hide show
  1. boris/__init__.py +26 -0
  2. boris/__main__.py +25 -0
  3. boris/about.py +143 -0
  4. boris/add_modifier.py +635 -0
  5. boris/add_modifier_ui.py +303 -0
  6. boris/advanced_event_filtering.py +455 -0
  7. boris/analysis_plugins/__init__.py +0 -0
  8. boris/analysis_plugins/_latency.py +59 -0
  9. boris/analysis_plugins/irr_cohen_kappa.py +109 -0
  10. boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
  11. boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
  12. boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
  13. boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
  14. boris/analysis_plugins/number_of_occurences.py +22 -0
  15. boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
  16. boris/analysis_plugins/time_budget.py +61 -0
  17. boris/behav_coding_map_creator.py +1110 -0
  18. boris/behavior_binary_table.py +305 -0
  19. boris/behaviors_coding_map.py +239 -0
  20. boris/boris_cli.py +340 -0
  21. boris/cmd_arguments.py +49 -0
  22. boris/coding_pad.py +280 -0
  23. boris/config.py +785 -0
  24. boris/config_file.py +356 -0
  25. boris/connections.py +409 -0
  26. boris/converters.py +333 -0
  27. boris/converters_ui.py +225 -0
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +5901 -0
  30. boris/core_qrc.py +15958 -0
  31. boris/core_ui.py +1107 -0
  32. boris/db_functions.py +324 -0
  33. boris/dev.py +134 -0
  34. boris/dialog.py +1108 -0
  35. boris/duration_widget.py +238 -0
  36. boris/edit_event.py +245 -0
  37. boris/edit_event_ui.py +233 -0
  38. boris/event_operations.py +1040 -0
  39. boris/events_cursor.py +61 -0
  40. boris/events_snapshots.py +596 -0
  41. boris/exclusion_matrix.py +141 -0
  42. boris/export_events.py +1006 -0
  43. boris/export_observation.py +1203 -0
  44. boris/external_processes.py +332 -0
  45. boris/geometric_measurement.py +941 -0
  46. boris/gui_utilities.py +135 -0
  47. boris/image_overlay.py +72 -0
  48. boris/import_observations.py +242 -0
  49. boris/ipc_mpv.py +325 -0
  50. boris/irr.py +634 -0
  51. boris/latency.py +244 -0
  52. boris/measurement_widget.py +161 -0
  53. boris/media_file.py +115 -0
  54. boris/menu_options.py +213 -0
  55. boris/modifier_coding_map_creator.py +1013 -0
  56. boris/modifiers_coding_map.py +157 -0
  57. boris/mpv.py +2016 -0
  58. boris/mpv2.py +2193 -0
  59. boris/observation.py +1453 -0
  60. boris/observation_operations.py +2538 -0
  61. boris/observation_ui.py +679 -0
  62. boris/observations_list.py +337 -0
  63. boris/otx_parser.py +442 -0
  64. boris/param_panel.py +201 -0
  65. boris/param_panel_ui.py +305 -0
  66. boris/player_dock_widget.py +198 -0
  67. boris/plot_data_module.py +536 -0
  68. boris/plot_events.py +634 -0
  69. boris/plot_events_rt.py +237 -0
  70. boris/plot_spectrogram_rt.py +316 -0
  71. boris/plot_waveform_rt.py +230 -0
  72. boris/plugins.py +431 -0
  73. boris/portion/__init__.py +31 -0
  74. boris/portion/const.py +95 -0
  75. boris/portion/dict.py +365 -0
  76. boris/portion/func.py +52 -0
  77. boris/portion/interval.py +581 -0
  78. boris/portion/io.py +181 -0
  79. boris/preferences.py +510 -0
  80. boris/preferences_ui.py +770 -0
  81. boris/project.py +2007 -0
  82. boris/project_functions.py +2041 -0
  83. boris/project_import_export.py +1096 -0
  84. boris/project_ui.py +794 -0
  85. boris/qrc_boris.py +10389 -0
  86. boris/qrc_boris5.py +2579 -0
  87. boris/select_modifiers.py +312 -0
  88. boris/select_observations.py +210 -0
  89. boris/select_subj_behav.py +286 -0
  90. boris/state_events.py +197 -0
  91. boris/subjects_pad.py +106 -0
  92. boris/synthetic_time_budget.py +290 -0
  93. boris/time_budget_functions.py +1136 -0
  94. boris/time_budget_widget.py +1039 -0
  95. boris/transitions.py +365 -0
  96. boris/utilities.py +1810 -0
  97. boris/version.py +24 -0
  98. boris/video_equalizer.py +159 -0
  99. boris/video_equalizer_ui.py +248 -0
  100. boris/video_operations.py +310 -0
  101. boris/view_df.py +104 -0
  102. boris/view_df_ui.py +75 -0
  103. boris/write_event.py +538 -0
  104. boris_behav_obs-9.7.7.dist-info/METADATA +139 -0
  105. boris_behav_obs-9.7.7.dist-info/RECORD +109 -0
  106. boris_behav_obs-9.7.7.dist-info/WHEEL +5 -0
  107. boris_behav_obs-9.7.7.dist-info/entry_points.txt +2 -0
  108. boris_behav_obs-9.7.7.dist-info/licenses/LICENSE.TXT +674 -0
  109. boris_behav_obs-9.7.7.dist-info/top_level.txt +1 -0
@@ -0,0 +1,581 @@
1
+ from collections import namedtuple
2
+ from .const import Bound, inf
3
+
4
+
5
+ Atomic = namedtuple("Atomic", ["left", "lower", "upper", "right"])
6
+
7
+
8
+ def mergeable(a, b):
9
+ """
10
+ Tester whether two atomic intervals can be merged (i.e. they overlap or are adjacent).
11
+
12
+ :param a: an atomic interval.
13
+ :param b: an atomic interval.
14
+ :return: True if mergeable, False otherwise.
15
+ """
16
+ if a.lower < b.lower or (a.lower == b.lower and a.left == Bound.CLOSED):
17
+ first, second = a, b
18
+ else:
19
+ first, second = b, a
20
+
21
+ if first.upper == second.lower:
22
+ return first.right == Bound.CLOSED or second.left == Bound.CLOSED
23
+
24
+ return first.upper > second.lower
25
+
26
+
27
+ def open(lower, upper):
28
+ """
29
+ Create an open interval with given bounds.
30
+
31
+ :param lower: value of the lower bound.
32
+ :param upper: value of the upper bound.
33
+ :return: an interval.
34
+ """
35
+ return Interval.from_atomic(Bound.OPEN, lower, upper, Bound.OPEN)
36
+
37
+
38
+ def closed(lower, upper):
39
+ """
40
+ Create a closed interval with given bounds.
41
+
42
+ :param lower: value of the lower bound.
43
+ :param upper: value of the upper bound.
44
+ :return: an interval.
45
+ """
46
+ return Interval.from_atomic(Bound.CLOSED, lower, upper, Bound.CLOSED)
47
+
48
+
49
+ def openclosed(lower, upper):
50
+ """
51
+ Create a left-open interval with given bounds.
52
+
53
+ :param lower: value of the lower bound.
54
+ :param upper: value of the upper bound.
55
+ :return: an interval.
56
+ """
57
+ return Interval.from_atomic(Bound.OPEN, lower, upper, Bound.CLOSED)
58
+
59
+
60
+ def closedopen(lower, upper):
61
+ """
62
+ Create a right-open interval with given bounds.
63
+
64
+ :param lower: value of the lower bound.
65
+ :param upper: value of the upper bound.
66
+ :return: an interval.
67
+ """
68
+ return Interval.from_atomic(Bound.CLOSED, lower, upper, Bound.OPEN)
69
+
70
+
71
+ def singleton(value):
72
+ """
73
+ Create a singleton interval.
74
+
75
+ :param value: value of the lower and upper bounds.
76
+ :return: an interval.
77
+ """
78
+ return Interval.from_atomic(Bound.CLOSED, value, value, Bound.CLOSED)
79
+
80
+
81
+ def empty():
82
+ """
83
+ Create an empty interval.
84
+
85
+ :return: an interval.
86
+ """
87
+ return Interval.from_atomic(Bound.OPEN, inf, -inf, Bound.OPEN)
88
+
89
+
90
+ class Interval:
91
+ """
92
+ This class represents an interval.
93
+
94
+ An interval is an (automatically simplified) union of atomic intervals.
95
+ It can be created with Interval.from_atomic(), by passing intervals to __init__, or by using
96
+ one of the helpers provided in this module (open, closed, openclosed, etc.)
97
+ """
98
+
99
+ __slots__ = ("_intervals",)
100
+
101
+ def __init__(self, *intervals):
102
+ """
103
+ Create a disjunction of zero, one or more intervals.
104
+
105
+ :param intervals: zero, one or more intervals.
106
+ """
107
+ self._intervals = list()
108
+
109
+ for interval in intervals:
110
+ if isinstance(interval, Interval):
111
+ if not interval.empty:
112
+ self._intervals.extend(interval._intervals)
113
+ else:
114
+ raise TypeError("Parameters must be Interval instances")
115
+
116
+ if len(self._intervals) == 0:
117
+ # So we have at least one (empty) interval
118
+ self._intervals.append(Atomic(Bound.OPEN, inf, -inf, Bound.OPEN))
119
+ else:
120
+ # Sort intervals by lower bound, closed first.
121
+ self._intervals.sort(key=lambda i: (i.lower, i.left is Bound.OPEN))
122
+
123
+ i = 0
124
+ # Try to merge consecutive intervals
125
+ while i < len(self._intervals) - 1:
126
+ current = self._intervals[i]
127
+ successor = self._intervals[i + 1]
128
+
129
+ if mergeable(current, successor):
130
+ if current.lower == successor.lower:
131
+ lower = current.lower
132
+ left = current.left if current.left == Bound.CLOSED else successor.left
133
+ else:
134
+ lower = min(current.lower, successor.lower)
135
+ left = current.left if lower == current.lower else successor.left
136
+
137
+ if current.upper == successor.upper:
138
+ upper = current.upper
139
+ right = current.right if current.right == Bound.CLOSED else successor.right
140
+ else:
141
+ upper = max(current.upper, successor.upper)
142
+ right = current.right if upper == current.upper else successor.right
143
+
144
+ union = Atomic(left, lower, upper, right)
145
+ self._intervals.pop(i) # pop current
146
+ self._intervals.pop(i) # pop successor
147
+ self._intervals.insert(i, union)
148
+ else:
149
+ i = i + 1
150
+
151
+ @property
152
+ def left(self):
153
+ """
154
+ Lowest left boundary is either CLOSED or OPEN.
155
+ """
156
+ return self._intervals[0].left
157
+
158
+ @property
159
+ def lower(self):
160
+ """
161
+ Lowest lower bound value.
162
+ """
163
+ return self._intervals[0].lower
164
+
165
+ @property
166
+ def upper(self):
167
+ """
168
+ Highest upper bound value.
169
+ """
170
+ return self._intervals[-1].upper
171
+
172
+ @property
173
+ def right(self):
174
+ """
175
+ Highest right boundary is either CLOSED or OPEN.
176
+ """
177
+ return self._intervals[-1].right
178
+
179
+ @property
180
+ def empty(self):
181
+ """
182
+ True if interval is empty, False otherwise.
183
+ """
184
+ return self.lower > self.upper or (self.lower == self.upper and (self.left == Bound.OPEN or self.right == Bound.OPEN))
185
+
186
+ @property
187
+ def atomic(self):
188
+ """
189
+ True if this interval is atomic, False otherwise.
190
+ An interval is atomic if it is composed of a single (possibly empty) atomic interval.
191
+ """
192
+ return len(self._intervals) == 1
193
+
194
+ @staticmethod
195
+ def from_atomic(left, lower, upper, right):
196
+ """
197
+ Create an Interval instance containing a single atomic interval.
198
+
199
+ :param left: either CLOSED or OPEN.
200
+ :param lower: value of the lower bound.
201
+ :param upper: value of the upper bound.
202
+ :param right: either CLOSED or OPEN.
203
+ """
204
+ instance = Interval()
205
+ left = left if lower not in [inf, -inf] else Bound.OPEN
206
+ right = right if upper not in [inf, -inf] else Bound.OPEN
207
+
208
+ instance._intervals = [Atomic(left, lower, upper, right)]
209
+ if instance.empty:
210
+ return Interval()
211
+
212
+ return instance
213
+
214
+ @property
215
+ def enclosure(self):
216
+ """
217
+ Return the smallest interval composed of a single atomic interval that encloses
218
+ the current interval.
219
+
220
+ :return: an Interval instance.
221
+ """
222
+ return Interval.from_atomic(self.left, self.lower, self.upper, self.right)
223
+
224
+ def replace(self, left=None, lower=None, upper=None, right=None, *, ignore_inf=True):
225
+ """
226
+ Create a new interval based on the current one and the provided values.
227
+
228
+ If current interval is not atomic, it is extended or restricted such that
229
+ its enclosure satisfies the new bounds. In other words, its new enclosure
230
+ will be equal to self.enclosure.replace(left, lower, upper, right).
231
+
232
+ Callable can be passed instead of values. In that case, it is called with the current
233
+ corresponding value except if ignore_inf if set (default) and the corresponding
234
+ bound is an infinity.
235
+
236
+ :param left: (a function of) left boundary.
237
+ :param lower: (a function of) value of the lower bound.
238
+ :param upper: (a function of) value of the upper bound.
239
+ :param right: (a function of) right boundary.
240
+ :param ignore_inf: ignore infinities if functions are provided (default is True).
241
+ :return: an Interval instance
242
+ """
243
+ enclosure = self.enclosure
244
+
245
+ if callable(left):
246
+ left = left(enclosure.left)
247
+ else:
248
+ left = enclosure.left if left is None else left
249
+
250
+ if callable(lower):
251
+ if ignore_inf and enclosure.lower in [-inf, inf]:
252
+ lower = enclosure.lower
253
+ else:
254
+ lower = lower(enclosure.lower)
255
+ else:
256
+ lower = enclosure.lower if lower is None else lower
257
+
258
+ if callable(upper):
259
+ if ignore_inf and enclosure.upper in [-inf, inf]:
260
+ upper = enclosure.upper
261
+ else:
262
+ upper = upper(enclosure.upper)
263
+ else:
264
+ upper = enclosure.upper if upper is None else upper
265
+
266
+ if callable(right):
267
+ right = right(enclosure.right)
268
+ else:
269
+ right = enclosure.right if right is None else right
270
+
271
+ if self.atomic:
272
+ return Interval.from_atomic(left, lower, upper, right)
273
+
274
+ n_interval = self & Interval.from_atomic(left, lower, upper, right)
275
+
276
+ if n_interval.atomic:
277
+ return n_interval.replace(left, lower, upper, right)
278
+ else:
279
+ lowest = n_interval[0].replace(left=left, lower=lower)
280
+ highest = n_interval[-1].replace(upper=upper, right=right)
281
+ return Interval(lowest, *n_interval[1:-1], highest)
282
+
283
+ def apply(self, func):
284
+ """
285
+ Apply a function on each of the underlying atomic intervals and return their union
286
+ as a new interval instance
287
+
288
+ Given function is expected to return an interval (possibly empty or not atomic) or
289
+ a 4-uple (left, lower, upper, right) whose values correspond to the parameters of
290
+ Interval.from_atomic(left, lower, upper, right).
291
+
292
+ This method is merely a shortcut for Interval(*list(map(func, self))).
293
+
294
+ :param func: function to apply on each underlying atomic interval.
295
+ :return: an Interval instance.
296
+ """
297
+ intervals = []
298
+
299
+ for i in self:
300
+ value = func(i)
301
+
302
+ if isinstance(value, Interval):
303
+ intervals.append(value)
304
+ elif isinstance(value, tuple):
305
+ intervals.append(Interval.from_atomic(*value))
306
+ else:
307
+ raise TypeError("Unsupported return type {} for {}".format(type(value), value))
308
+
309
+ return Interval(*intervals)
310
+
311
+ def adjacent(self, other):
312
+ """
313
+ Test if two intervals are adjacent.
314
+
315
+ Two intervals are adjacent if they do not overlap and their union form a
316
+ single atomic interval.
317
+
318
+ While this definition corresponds to the usual notion of adjacency for atomic
319
+ intervals, it has stronger requirements for non-atomic ones since it requires
320
+ all underlying atomic intervals to be adjacent (i.e. that one
321
+ interval fills the gaps between the atomic intervals of the other one).
322
+
323
+ :param other: an interval.
324
+ :return: True if intervals are adjacent, False otherwise.
325
+ """
326
+ return (self & other).empty and (self | other).atomic
327
+
328
+ def overlaps(self, other):
329
+ """
330
+ Test if two intervals overlap (i.e. if their intersection is non-empty).
331
+
332
+ :param other: an interval.
333
+ :return: True if intervals overlap, False otherwise.
334
+ """
335
+ if isinstance(other, Interval):
336
+ i_iter = iter(self)
337
+ o_iter = iter(other)
338
+ i_current = next(i_iter)
339
+ o_current = next(o_iter)
340
+
341
+ while i_current is not None and o_current is not None:
342
+ if i_current < o_current:
343
+ i_current = next(i_iter, None)
344
+ elif o_current < i_current:
345
+ o_current = next(o_iter, None)
346
+ else:
347
+ return True
348
+ return False
349
+ else:
350
+ raise TypeError("Unsupported type {} for {}".format(type(other), other))
351
+
352
+ def intersection(self, other):
353
+ """
354
+ Return the intersection of two intervals.
355
+
356
+ :param other: an interval.
357
+ :return: the intersection of the intervals.
358
+ """
359
+ return self & other
360
+
361
+ def union(self, other):
362
+ """
363
+ Return the union of two intervals.
364
+
365
+ :param other: an interval.
366
+ :return: the union of the intervals.
367
+ """
368
+ return self | other
369
+
370
+ def contains(self, item):
371
+ """
372
+ Test if given item is contained in this interval.
373
+ This method accepts intervals and arbitrary comparable values.
374
+
375
+ :param item: an interval or any arbitrary comparable value.
376
+ :return: True if given item is contained, False otherwise.
377
+ """
378
+ return item in self
379
+
380
+ def complement(self):
381
+ """
382
+ Return the complement of this interval.
383
+
384
+ :return: the complement of this interval.
385
+ """
386
+ return ~self
387
+
388
+ def difference(self, other):
389
+ """
390
+ Return the difference of two intervals.
391
+
392
+ :param other: an interval.
393
+ :return: the difference of the intervals.
394
+ """
395
+ return self - other
396
+
397
+ def __len__(self):
398
+ return len(self._intervals)
399
+
400
+ def __iter__(self):
401
+ return iter([Interval.from_atomic(*i) for i in self._intervals])
402
+
403
+ def __getitem__(self, item):
404
+ if isinstance(item, slice):
405
+ return [Interval.from_atomic(*i) for i in self._intervals[item]]
406
+ else:
407
+ return Interval.from_atomic(*self._intervals[item])
408
+
409
+ def __and__(self, other):
410
+ if not isinstance(other, Interval):
411
+ return NotImplemented
412
+
413
+ if self.atomic and other.atomic:
414
+ if self.lower == other.lower:
415
+ lower = self.lower
416
+ left = self.left if self.left == Bound.OPEN else other.left
417
+ else:
418
+ lower = max(self.lower, other.lower)
419
+ left = self.left if lower == self.lower else other.left
420
+
421
+ if self.upper == other.upper:
422
+ upper = self.upper
423
+ right = self.right if self.right == Bound.OPEN else other.right
424
+ else:
425
+ upper = min(self.upper, other.upper)
426
+ right = self.right if upper == self.upper else other.right
427
+
428
+ return Interval.from_atomic(left, lower, upper, right)
429
+ else:
430
+ intersections = []
431
+
432
+ i_iter = iter(self)
433
+ o_iter = iter(other)
434
+ i_current = next(i_iter)
435
+ o_current = next(o_iter)
436
+
437
+ while i_current is not None and o_current is not None:
438
+ if i_current < o_current:
439
+ i_current = next(i_iter, None)
440
+ elif o_current < i_current:
441
+ o_current = next(o_iter, None)
442
+ else:
443
+ # i_current and o_current have an overlap
444
+ intersections.append(i_current & o_current)
445
+
446
+ if i_current <= o_current:
447
+ # o_current can still intersect next i
448
+ i_current = next(i_iter, None)
449
+ elif o_current <= i_current:
450
+ # i_current can still intersect next o
451
+ o_current = next(o_iter, None)
452
+ else:
453
+ assert False
454
+
455
+ return Interval(*intersections)
456
+
457
+ def __or__(self, other):
458
+ if isinstance(other, Interval):
459
+ return Interval(self, other)
460
+ else:
461
+ return NotImplemented
462
+
463
+ def __contains__(self, item):
464
+ if isinstance(item, Interval):
465
+ if item.empty:
466
+ return True
467
+ elif self.atomic:
468
+ left = item.lower > self.lower or (item.lower == self.lower and (item.left == self.left or self.left == Bound.CLOSED))
469
+ right = item.upper < self.upper or (item.upper == self.upper and (item.right == self.right or self.right == Bound.CLOSED))
470
+ return left and right
471
+ else:
472
+ selfiter = iter(self)
473
+ current = next(selfiter)
474
+
475
+ for other in item:
476
+ while current < other:
477
+ try:
478
+ current = next(selfiter)
479
+ except StopIteration:
480
+ return False
481
+
482
+ # here current and other could have an overlap
483
+ if other not in current:
484
+ return False
485
+ return True
486
+ else:
487
+ # Item is a value
488
+ for i in self._intervals:
489
+ left = (item >= i.lower) if i.left == Bound.CLOSED else (item > i.lower)
490
+ right = (item <= i.upper) if i.right == Bound.CLOSED else (item < i.upper)
491
+ if left and right:
492
+ return True
493
+ return False
494
+
495
+ def __invert__(self):
496
+ complements = [
497
+ Interval.from_atomic(Bound.OPEN, -inf, self.lower, ~self.left),
498
+ Interval.from_atomic(~self.right, self.upper, inf, Bound.OPEN),
499
+ ]
500
+
501
+ for i, j in zip(self._intervals[:-1], self._intervals[1:]):
502
+ complements.append(Interval.from_atomic(~i.right, i.upper, j.lower, ~j.left))
503
+
504
+ return Interval(*complements)
505
+
506
+ def __sub__(self, other):
507
+ if isinstance(other, Interval):
508
+ return self & ~other
509
+ else:
510
+ return NotImplemented
511
+
512
+ def __eq__(self, other):
513
+ if isinstance(other, Interval):
514
+ if len(other._intervals) != len(self._intervals):
515
+ return False
516
+
517
+ for a, b in zip(self._intervals, other._intervals):
518
+ eq = a.left == b.left and a.lower == b.lower and a.upper == b.upper and a.right == b.right
519
+ if not eq:
520
+ return False
521
+ return True
522
+ else:
523
+ return NotImplemented
524
+
525
+ def __lt__(self, other):
526
+ if isinstance(other, Interval):
527
+ if self.right == Bound.OPEN:
528
+ return self.upper <= other.lower
529
+ else:
530
+ return self.upper < other.lower or (self.upper == other.lower and other.left == Bound.OPEN)
531
+ else:
532
+ return self.upper < other or (self.right == Bound.OPEN and self.upper == other)
533
+
534
+ def __gt__(self, other):
535
+ if isinstance(other, Interval):
536
+ if self.left == Bound.OPEN:
537
+ return self.lower >= other.upper
538
+ else:
539
+ return self.lower > other.upper or (self.lower == other.upper and other.right == Bound.OPEN)
540
+ else:
541
+ return self.lower > other or (self.left == Bound.OPEN and self.lower == other)
542
+
543
+ def __le__(self, other):
544
+ if isinstance(other, Interval):
545
+ if self.right == Bound.OPEN:
546
+ return self.upper <= other.upper
547
+ else:
548
+ return self.upper < other.upper or (self.upper == other.upper and other.right == Bound.CLOSED)
549
+ else:
550
+ return self.lower < other or (self.left == Bound.CLOSED and self.lower == other)
551
+
552
+ def __ge__(self, other):
553
+ if isinstance(other, Interval):
554
+ if self.left == Bound.OPEN:
555
+ return self.lower >= other.lower
556
+ else:
557
+ return self.lower > other.lower or (self.lower == other.lower and other.left == Bound.CLOSED)
558
+ else:
559
+ return self.upper > other or (self.right == Bound.CLOSED and self.upper == other)
560
+
561
+ def __hash__(self):
562
+ return hash(tuple([self.lower, self.upper]))
563
+
564
+ def __repr__(self):
565
+ intervals = []
566
+
567
+ for interval in self:
568
+ if interval.empty:
569
+ intervals.append("()")
570
+ elif interval.lower == interval.upper:
571
+ intervals.append("[{}]".format(repr(interval.lower)))
572
+ else:
573
+ intervals.append(
574
+ "{}{},{}{}".format(
575
+ "[" if interval.left == Bound.CLOSED else "(",
576
+ repr(interval.lower),
577
+ repr(interval.upper),
578
+ "]" if interval.right == Bound.CLOSED else ")",
579
+ )
580
+ )
581
+ return " | ".join(intervals)