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.
- boris/__init__.py +26 -0
- boris/__main__.py +25 -0
- boris/about.py +143 -0
- boris/add_modifier.py +635 -0
- boris/add_modifier_ui.py +303 -0
- boris/advanced_event_filtering.py +455 -0
- boris/analysis_plugins/__init__.py +0 -0
- boris/analysis_plugins/_latency.py +59 -0
- boris/analysis_plugins/irr_cohen_kappa.py +109 -0
- boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
- boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
- boris/analysis_plugins/number_of_occurences.py +22 -0
- boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
- boris/analysis_plugins/time_budget.py +61 -0
- boris/behav_coding_map_creator.py +1110 -0
- boris/behavior_binary_table.py +305 -0
- boris/behaviors_coding_map.py +239 -0
- boris/boris_cli.py +340 -0
- boris/cmd_arguments.py +49 -0
- boris/coding_pad.py +280 -0
- boris/config.py +785 -0
- boris/config_file.py +356 -0
- boris/connections.py +409 -0
- boris/converters.py +333 -0
- boris/converters_ui.py +225 -0
- boris/cooccurence.py +250 -0
- boris/core.py +5901 -0
- boris/core_qrc.py +15958 -0
- boris/core_ui.py +1107 -0
- boris/db_functions.py +324 -0
- boris/dev.py +134 -0
- boris/dialog.py +1108 -0
- boris/duration_widget.py +238 -0
- boris/edit_event.py +245 -0
- boris/edit_event_ui.py +233 -0
- boris/event_operations.py +1040 -0
- boris/events_cursor.py +61 -0
- boris/events_snapshots.py +596 -0
- boris/exclusion_matrix.py +141 -0
- boris/export_events.py +1006 -0
- boris/export_observation.py +1203 -0
- boris/external_processes.py +332 -0
- boris/geometric_measurement.py +941 -0
- boris/gui_utilities.py +135 -0
- boris/image_overlay.py +72 -0
- boris/import_observations.py +242 -0
- boris/ipc_mpv.py +325 -0
- boris/irr.py +634 -0
- boris/latency.py +244 -0
- boris/measurement_widget.py +161 -0
- boris/media_file.py +115 -0
- boris/menu_options.py +213 -0
- boris/modifier_coding_map_creator.py +1013 -0
- boris/modifiers_coding_map.py +157 -0
- boris/mpv.py +2016 -0
- boris/mpv2.py +2193 -0
- boris/observation.py +1453 -0
- boris/observation_operations.py +2538 -0
- boris/observation_ui.py +679 -0
- boris/observations_list.py +337 -0
- boris/otx_parser.py +442 -0
- boris/param_panel.py +201 -0
- boris/param_panel_ui.py +305 -0
- boris/player_dock_widget.py +198 -0
- boris/plot_data_module.py +536 -0
- boris/plot_events.py +634 -0
- boris/plot_events_rt.py +237 -0
- boris/plot_spectrogram_rt.py +316 -0
- boris/plot_waveform_rt.py +230 -0
- boris/plugins.py +431 -0
- boris/portion/__init__.py +31 -0
- boris/portion/const.py +95 -0
- boris/portion/dict.py +365 -0
- boris/portion/func.py +52 -0
- boris/portion/interval.py +581 -0
- boris/portion/io.py +181 -0
- boris/preferences.py +510 -0
- boris/preferences_ui.py +770 -0
- boris/project.py +2007 -0
- boris/project_functions.py +2041 -0
- boris/project_import_export.py +1096 -0
- boris/project_ui.py +794 -0
- boris/qrc_boris.py +10389 -0
- boris/qrc_boris5.py +2579 -0
- boris/select_modifiers.py +312 -0
- boris/select_observations.py +210 -0
- boris/select_subj_behav.py +286 -0
- boris/state_events.py +197 -0
- boris/subjects_pad.py +106 -0
- boris/synthetic_time_budget.py +290 -0
- boris/time_budget_functions.py +1136 -0
- boris/time_budget_widget.py +1039 -0
- boris/transitions.py +365 -0
- boris/utilities.py +1810 -0
- boris/version.py +24 -0
- boris/video_equalizer.py +159 -0
- boris/video_equalizer_ui.py +248 -0
- boris/video_operations.py +310 -0
- boris/view_df.py +104 -0
- boris/view_df_ui.py +75 -0
- boris/write_event.py +538 -0
- boris_behav_obs-9.7.7.dist-info/METADATA +139 -0
- boris_behav_obs-9.7.7.dist-info/RECORD +109 -0
- boris_behav_obs-9.7.7.dist-info/WHEEL +5 -0
- boris_behav_obs-9.7.7.dist-info/entry_points.txt +2 -0
- boris_behav_obs-9.7.7.dist-info/licenses/LICENSE.TXT +674 -0
- 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)
|