docs for muutils v0.8.3
View Source on GitHub

muutils.interval

represents a mathematical Interval over the real numbers


  1"represents a mathematical `Interval` over the real numbers"
  2
  3from __future__ import annotations
  4
  5import math
  6import typing
  7from typing import Optional, Iterable, Sequence, Union, Any
  8
  9from muutils.misc import str_to_numeric
 10
 11_EPSILON: float = 1e-10
 12
 13Number = Union[float, int]
 14# TODO: make this also work with decimals, fractions, numpy types, etc.
 15# except we must somehow avoid importing them? idk
 16
 17_EMPTY_INTERVAL_ARGS: tuple[Number, Number, bool, bool, set[Number]] = (
 18    math.nan,
 19    math.nan,
 20    False,
 21    False,
 22    set(),
 23)
 24
 25
 26class Interval:
 27    """
 28    Represents a mathematical interval, open by default.
 29
 30    The Interval class can represent both open and closed intervals, as well as half-open intervals.
 31    It supports various initialization methods and provides containment checks.
 32
 33    Examples:
 34
 35        >>> i1 = Interval(1, 5)  # Default open interval (1, 5)
 36        >>> 3 in i1
 37        True
 38        >>> 1 in i1
 39        False
 40        >>> i2 = Interval([1, 5])  # Closed interval [1, 5]
 41        >>> 1 in i2
 42        True
 43        >>> i3 = Interval(1, 5, closed_L=True)  # Half-open interval [1, 5)
 44        >>> str(i3)
 45        '[1, 5)'
 46        >>> i4 = ClosedInterval(1, 5)  # Closed interval [1, 5]
 47        >>> i5 = OpenInterval(1, 5)  # Open interval (1, 5)
 48
 49    """
 50
 51    def __init__(
 52        self,
 53        *args: Union[Sequence[Number], Number],
 54        is_closed: Optional[bool] = None,
 55        closed_L: Optional[bool] = None,
 56        closed_R: Optional[bool] = None,
 57    ):
 58        self.lower: Number
 59        self.upper: Number
 60        self.closed_L: bool
 61        self.closed_R: bool
 62        self.singleton_set: Optional[set[Number]] = None
 63        try:
 64            if len(args) == 0:
 65                (
 66                    self.lower,
 67                    self.upper,
 68                    self.closed_L,
 69                    self.closed_R,
 70                    self.singleton_set,
 71                ) = _EMPTY_INTERVAL_ARGS
 72                return
 73            # Handle different types of input arguments
 74            if len(args) == 1 and isinstance(
 75                args[0], (list, tuple, Sequence, Iterable)
 76            ):
 77                assert (
 78                    len(args[0]) == 2
 79                ), "if arg is a list or tuple, it must have length 2"
 80                self.lower = args[0][0]
 81                self.upper = args[0][1]
 82                # Determine closure type based on the container type
 83                default_closed = isinstance(args[0], list)
 84            elif len(args) == 1 and isinstance(
 85                args[0], (int, float, typing.SupportsFloat, typing.SupportsInt)
 86            ):
 87                # a singleton, but this will be handled later
 88                self.lower = args[0]
 89                self.upper = args[0]
 90                default_closed = False
 91            elif len(args) == 2:
 92                self.lower, self.upper = args  # type: ignore[assignment]
 93                default_closed = False  # Default to open interval if two args
 94            else:
 95                raise ValueError(f"Invalid input arguments: {args}")
 96
 97            # if both of the bounds are NaN or None, return an empty interval
 98            if any(x is None for x in (self.lower, self.upper)) or any(
 99                math.isnan(x) for x in (self.lower, self.upper)
100            ):
101                if (self.lower is None and self.upper is None) or (
102                    math.isnan(self.lower) and math.isnan(self.upper)
103                ):
104                    (
105                        self.lower,
106                        self.upper,
107                        self.closed_L,
108                        self.closed_R,
109                        self.singleton_set,
110                    ) = _EMPTY_INTERVAL_ARGS
111                    return
112                else:
113                    raise ValueError(
114                        "Both bounds must be NaN or None to create an empty interval. Also, just use `Interval.get_empty()` instead."
115                    )
116
117            # Ensure lower bound is less than upper bound
118            if self.lower > self.upper:
119                raise ValueError("Lower bound must be less than upper bound")
120
121            if math.isnan(self.lower) or math.isnan(self.upper):
122                raise ValueError("NaN is not allowed as an interval bound")
123
124            # Determine closure properties
125            if is_closed is not None:
126                # can't specify both is_closed and closed_L/R
127                if (closed_L is not None) or (closed_R is not None):
128                    raise ValueError("Cannot specify both is_closed and closed_L/R")
129                self.closed_L = is_closed
130                self.closed_R = is_closed
131            else:
132                self.closed_L = closed_L if closed_L is not None else default_closed
133                self.closed_R = closed_R if closed_R is not None else default_closed
134
135            # handle singleton/empty case
136            if self.lower == self.upper and not (self.closed_L or self.closed_R):
137                (
138                    self.lower,
139                    self.upper,
140                    self.closed_L,
141                    self.closed_R,
142                    self.singleton_set,
143                ) = _EMPTY_INTERVAL_ARGS
144                return
145
146            elif self.lower == self.upper and (self.closed_L or self.closed_R):
147                self.singleton_set = {self.lower}  # Singleton interval
148                self.closed_L = True
149                self.closed_R = True
150                return
151            # otherwise `singleton_set` is `None`
152
153        except (AssertionError, ValueError) as e:
154            raise ValueError(
155                f"Invalid input arguments to Interval: {args = }, {is_closed = }, {closed_L = }, {closed_R = }\n{e}\nUsage:\n{self.__doc__}"
156            ) from e
157
158    @property
159    def is_closed(self) -> bool:
160        if self.is_empty:
161            return True
162        if self.is_singleton:
163            return True
164        return self.closed_L and self.closed_R
165
166    @property
167    def is_open(self) -> bool:
168        if self.is_empty:
169            return True
170        if self.is_singleton:
171            return False
172        return not self.closed_L and not self.closed_R
173
174    @property
175    def is_half_open(self) -> bool:
176        return (self.closed_L and not self.closed_R) or (
177            not self.closed_L and self.closed_R
178        )
179
180    @property
181    def is_singleton(self) -> bool:
182        return self.singleton_set is not None and len(self.singleton_set) == 1
183
184    @property
185    def is_empty(self) -> bool:
186        return self.singleton_set is not None and len(self.singleton_set) == 0
187
188    @property
189    def is_finite(self) -> bool:
190        return not math.isinf(self.lower) and not math.isinf(self.upper)
191
192    @property
193    def singleton(self) -> Number:
194        if not self.is_singleton:
195            raise ValueError("Interval is not a singleton")
196        return next(iter(self.singleton_set))  # type: ignore[arg-type]
197
198    @staticmethod
199    def get_empty() -> Interval:
200        return Interval(math.nan, math.nan, closed_L=None, closed_R=None)
201
202    @staticmethod
203    def get_singleton(value: Number) -> Interval:
204        if math.isnan(value) or value is None:
205            return Interval.get_empty()
206        return Interval(value, value, closed_L=True, closed_R=True)
207
208    def numerical_contained(self, item: Number) -> bool:
209        if self.is_empty:
210            return False
211        if math.isnan(item):
212            raise ValueError("NaN cannot be checked for containment in an interval")
213        if self.is_singleton:
214            return item in self.singleton_set  # type: ignore[operator]
215        return ((self.closed_L and item >= self.lower) or item > self.lower) and (
216            (self.closed_R and item <= self.upper) or item < self.upper
217        )
218
219    def interval_contained(self, item: Interval) -> bool:
220        if item.is_empty:
221            return True
222        if self.is_empty:
223            return False
224        if item.is_singleton:
225            return self.numerical_contained(item.singleton)
226        if self.is_singleton:
227            if not item.is_singleton:
228                return False
229            return self.singleton == item.singleton
230
231        lower_contained: bool = (
232            # either strictly wider bound
233            self.lower < item.lower
234            # if same, then self must be closed if item is open
235            or (self.lower == item.lower and self.closed_L >= item.closed_L)
236        )
237
238        upper_contained: bool = (
239            # either strictly wider bound
240            self.upper > item.upper
241            # if same, then self must be closed if item is open
242            or (self.upper == item.upper and self.closed_R >= item.closed_R)
243        )
244
245        return lower_contained and upper_contained
246
247    def __contains__(self, item: Any) -> bool:
248        if isinstance(item, Interval):
249            return self.interval_contained(item)
250        else:
251            return self.numerical_contained(item)
252
253    def __repr__(self) -> str:
254        if self.is_empty:
255            return r"∅"
256        if self.is_singleton:
257            return "{" + str(self.singleton) + "}"
258        left: str = "[" if self.closed_L else "("
259        right: str = "]" if self.closed_R else ")"
260        return f"{left}{self.lower}, {self.upper}{right}"
261
262    def __str__(self) -> str:
263        return repr(self)
264
265    @classmethod
266    def from_str(cls, input_str: str) -> Interval:
267        input_str = input_str.strip()
268        # empty and singleton
269        if input_str.count(",") == 0:
270            # empty set
271            if input_str == "∅":
272                return cls.get_empty()
273            assert input_str.startswith("{") and input_str.endswith(
274                "}"
275            ), "Invalid input string"
276            input_str_set_interior: str = input_str.strip("{}").strip()
277            if len(input_str_set_interior) == 0:
278                return cls.get_empty()
279            # singleton set
280            return cls.get_singleton(str_to_numeric(input_str_set_interior))
281
282        # expect commas
283        if not input_str.count(",") == 1:
284            raise ValueError("Invalid input string")
285
286        # get bounds
287        lower: str
288        upper: str
289        lower, upper = input_str.strip("[]()").split(",")
290        lower = lower.strip()
291        upper = upper.strip()
292
293        lower_num: Number = str_to_numeric(lower)
294        upper_num: Number = str_to_numeric(upper)
295
296        # figure out closure
297        closed_L: bool
298        closed_R: bool
299        if input_str[0] == "[":
300            closed_L = True
301        elif input_str[0] == "(":
302            closed_L = False
303        else:
304            raise ValueError("Invalid input string")
305
306        if input_str[-1] == "]":
307            closed_R = True
308        elif input_str[-1] == ")":
309            closed_R = False
310        else:
311            raise ValueError("Invalid input string")
312
313        return cls(lower_num, upper_num, closed_L=closed_L, closed_R=closed_R)
314
315    def __eq__(self, other: object) -> bool:
316        if not isinstance(other, Interval):
317            return False
318        if self.is_empty and other.is_empty:
319            return True
320        if self.is_singleton and other.is_singleton:
321            return self.singleton == other.singleton
322        return (self.lower, self.upper, self.closed_L, self.closed_R) == (
323            other.lower,
324            other.upper,
325            other.closed_L,
326            other.closed_R,
327        )
328
329    def __iter__(self):
330        if self.is_empty:
331            return
332        elif self.is_singleton:
333            yield self.singleton
334            return
335        else:
336            yield self.lower
337            yield self.upper
338
339    def __getitem__(self, index: int) -> float:
340        if self.is_empty:
341            raise IndexError("Empty interval has no bounds")
342        if self.is_singleton:
343            if index == 0:
344                return self.singleton
345            else:
346                raise IndexError("Singleton interval has only one bound")
347        if index == 0:
348            return self.lower
349        elif index == 1:
350            return self.upper
351        else:
352            raise IndexError("Interval index out of range")
353
354    def __len__(self) -> int:
355        return 0 if self.is_empty else 1 if self.is_singleton else 2
356
357    def copy(self) -> Interval:
358        if self.is_empty:
359            return Interval.get_empty()
360        if self.is_singleton:
361            return Interval.get_singleton(self.singleton)
362        return Interval(
363            self.lower, self.upper, closed_L=self.closed_L, closed_R=self.closed_R
364        )
365
366    def size(self) -> float:
367        """
368        Returns the size of the interval.
369
370        # Returns:
371
372         - `float`
373            the size of the interval
374        """
375        if self.is_empty or self.is_singleton:
376            return 0
377        else:
378            return self.upper - self.lower
379
380    def clamp(self, value: Union[int, float], epsilon: float = _EPSILON) -> float:
381        """
382        Clamp the given value to the interval bounds.
383
384        For open bounds, the clamped value will be slightly inside the interval (by epsilon).
385
386        # Parameters:
387
388         - `value : Union[int, float]`
389           the value to clamp.
390         - `epsilon : float`
391           margin for open bounds
392           (defaults to `_EPSILON`)
393
394        # Returns:
395
396         - `float`
397            the clamped value
398
399        # Raises:
400
401         - `ValueError` : If the input value is NaN.
402        """
403
404        if math.isnan(value):
405            raise ValueError("Cannot clamp NaN value")
406
407        if math.isnan(epsilon):
408            raise ValueError("Epsilon cannot be NaN")
409
410        if epsilon < 0:
411            raise ValueError(f"Epsilon must be non-negative: {epsilon = }")
412
413        if self.is_empty:
414            raise ValueError("Cannot clamp to an empty interval")
415
416        if self.is_singleton:
417            return self.singleton
418
419        if epsilon > self.size():
420            raise ValueError(
421                f"epsilon is greater than the size of the interval: {epsilon = }, {self.size() = }, {self = }"
422            )
423
424        # make type work with decimals and stuff
425        if not isinstance(value, (int, float)):
426            epsilon = value.__class__(epsilon)
427
428        clamped_min: Number
429        if self.closed_L:
430            clamped_min = self.lower
431        else:
432            clamped_min = self.lower + epsilon
433
434        clamped_max: Number
435        if self.closed_R:
436            clamped_max = self.upper
437        else:
438            clamped_max = self.upper - epsilon
439
440        return max(clamped_min, min(value, clamped_max))
441
442    def intersection(self, other: Interval) -> Interval:
443        if not isinstance(other, Interval):
444            raise TypeError("Can only intersect with another Interval")
445
446        if self.is_empty or other.is_empty:
447            return Interval.get_empty()
448
449        if self.is_singleton:
450            if other.numerical_contained(self.singleton):
451                return self.copy()
452            else:
453                return Interval.get_empty()
454
455        if other.is_singleton:
456            if self.numerical_contained(other.singleton):
457                return other.copy()
458            else:
459                return Interval.get_empty()
460
461        if self.upper < other.lower or other.upper < self.lower:
462            return Interval.get_empty()
463
464        lower: Number = max(self.lower, other.lower)
465        upper: Number = min(self.upper, other.upper)
466        closed_L: bool = self.closed_L if self.lower > other.lower else other.closed_L
467        closed_R: bool = self.closed_R if self.upper < other.upper else other.closed_R
468
469        return Interval(lower, upper, closed_L=closed_L, closed_R=closed_R)
470
471    def union(self, other: Interval) -> Interval:
472        if not isinstance(other, Interval):
473            raise TypeError("Can only union with another Interval")
474
475        # empty set case
476        if self.is_empty:
477            return other.copy()
478        if other.is_empty:
479            return self.copy()
480
481        # special case where the intersection is empty but the intervals are contiguous
482        if self.upper == other.lower:
483            if self.closed_R or other.closed_L:
484                return Interval(
485                    self.lower,
486                    other.upper,
487                    closed_L=self.closed_L,
488                    closed_R=other.closed_R,
489                )
490        elif other.upper == self.lower:
491            if other.closed_R or self.closed_L:
492                return Interval(
493                    other.lower,
494                    self.upper,
495                    closed_L=other.closed_L,
496                    closed_R=self.closed_R,
497                )
498
499        # non-intersecting nonempty and non-contiguous intervals
500        if self.intersection(other) == Interval.get_empty():
501            raise NotImplementedError(
502                "Union of non-intersecting nonempty non-contiguous intervals is not implemented "
503                + f"{self = }, {other = }, {self.intersection(other) = }"
504            )
505
506        # singleton case
507        if self.is_singleton:
508            return other.copy()
509        if other.is_singleton:
510            return self.copy()
511
512        # regular case
513        lower: Number = min(self.lower, other.lower)
514        upper: Number = max(self.upper, other.upper)
515        closed_L: bool = self.closed_L if self.lower < other.lower else other.closed_L
516        closed_R: bool = self.closed_R if self.upper > other.upper else other.closed_R
517
518        return Interval(lower, upper, closed_L=closed_L, closed_R=closed_R)
519
520
521class ClosedInterval(Interval):
522    def __init__(self, *args: Union[Sequence[float], float], **kwargs: Any):
523        if any(key in kwargs for key in ("is_closed", "closed_L", "closed_R")):
524            raise ValueError("Cannot specify closure properties for ClosedInterval")
525        super().__init__(*args, is_closed=True)
526
527
528class OpenInterval(Interval):
529    def __init__(self, *args: Union[Sequence[float], float], **kwargs: Any):
530        if any(key in kwargs for key in ("is_closed", "closed_L", "closed_R")):
531            raise ValueError("Cannot specify closure properties for OpenInterval")
532        super().__init__(*args, is_closed=False)

Number = typing.Union[float, int]
class Interval:
 27class Interval:
 28    """
 29    Represents a mathematical interval, open by default.
 30
 31    The Interval class can represent both open and closed intervals, as well as half-open intervals.
 32    It supports various initialization methods and provides containment checks.
 33
 34    Examples:
 35
 36        >>> i1 = Interval(1, 5)  # Default open interval (1, 5)
 37        >>> 3 in i1
 38        True
 39        >>> 1 in i1
 40        False
 41        >>> i2 = Interval([1, 5])  # Closed interval [1, 5]
 42        >>> 1 in i2
 43        True
 44        >>> i3 = Interval(1, 5, closed_L=True)  # Half-open interval [1, 5)
 45        >>> str(i3)
 46        '[1, 5)'
 47        >>> i4 = ClosedInterval(1, 5)  # Closed interval [1, 5]
 48        >>> i5 = OpenInterval(1, 5)  # Open interval (1, 5)
 49
 50    """
 51
 52    def __init__(
 53        self,
 54        *args: Union[Sequence[Number], Number],
 55        is_closed: Optional[bool] = None,
 56        closed_L: Optional[bool] = None,
 57        closed_R: Optional[bool] = None,
 58    ):
 59        self.lower: Number
 60        self.upper: Number
 61        self.closed_L: bool
 62        self.closed_R: bool
 63        self.singleton_set: Optional[set[Number]] = None
 64        try:
 65            if len(args) == 0:
 66                (
 67                    self.lower,
 68                    self.upper,
 69                    self.closed_L,
 70                    self.closed_R,
 71                    self.singleton_set,
 72                ) = _EMPTY_INTERVAL_ARGS
 73                return
 74            # Handle different types of input arguments
 75            if len(args) == 1 and isinstance(
 76                args[0], (list, tuple, Sequence, Iterable)
 77            ):
 78                assert (
 79                    len(args[0]) == 2
 80                ), "if arg is a list or tuple, it must have length 2"
 81                self.lower = args[0][0]
 82                self.upper = args[0][1]
 83                # Determine closure type based on the container type
 84                default_closed = isinstance(args[0], list)
 85            elif len(args) == 1 and isinstance(
 86                args[0], (int, float, typing.SupportsFloat, typing.SupportsInt)
 87            ):
 88                # a singleton, but this will be handled later
 89                self.lower = args[0]
 90                self.upper = args[0]
 91                default_closed = False
 92            elif len(args) == 2:
 93                self.lower, self.upper = args  # type: ignore[assignment]
 94                default_closed = False  # Default to open interval if two args
 95            else:
 96                raise ValueError(f"Invalid input arguments: {args}")
 97
 98            # if both of the bounds are NaN or None, return an empty interval
 99            if any(x is None for x in (self.lower, self.upper)) or any(
100                math.isnan(x) for x in (self.lower, self.upper)
101            ):
102                if (self.lower is None and self.upper is None) or (
103                    math.isnan(self.lower) and math.isnan(self.upper)
104                ):
105                    (
106                        self.lower,
107                        self.upper,
108                        self.closed_L,
109                        self.closed_R,
110                        self.singleton_set,
111                    ) = _EMPTY_INTERVAL_ARGS
112                    return
113                else:
114                    raise ValueError(
115                        "Both bounds must be NaN or None to create an empty interval. Also, just use `Interval.get_empty()` instead."
116                    )
117
118            # Ensure lower bound is less than upper bound
119            if self.lower > self.upper:
120                raise ValueError("Lower bound must be less than upper bound")
121
122            if math.isnan(self.lower) or math.isnan(self.upper):
123                raise ValueError("NaN is not allowed as an interval bound")
124
125            # Determine closure properties
126            if is_closed is not None:
127                # can't specify both is_closed and closed_L/R
128                if (closed_L is not None) or (closed_R is not None):
129                    raise ValueError("Cannot specify both is_closed and closed_L/R")
130                self.closed_L = is_closed
131                self.closed_R = is_closed
132            else:
133                self.closed_L = closed_L if closed_L is not None else default_closed
134                self.closed_R = closed_R if closed_R is not None else default_closed
135
136            # handle singleton/empty case
137            if self.lower == self.upper and not (self.closed_L or self.closed_R):
138                (
139                    self.lower,
140                    self.upper,
141                    self.closed_L,
142                    self.closed_R,
143                    self.singleton_set,
144                ) = _EMPTY_INTERVAL_ARGS
145                return
146
147            elif self.lower == self.upper and (self.closed_L or self.closed_R):
148                self.singleton_set = {self.lower}  # Singleton interval
149                self.closed_L = True
150                self.closed_R = True
151                return
152            # otherwise `singleton_set` is `None`
153
154        except (AssertionError, ValueError) as e:
155            raise ValueError(
156                f"Invalid input arguments to Interval: {args = }, {is_closed = }, {closed_L = }, {closed_R = }\n{e}\nUsage:\n{self.__doc__}"
157            ) from e
158
159    @property
160    def is_closed(self) -> bool:
161        if self.is_empty:
162            return True
163        if self.is_singleton:
164            return True
165        return self.closed_L and self.closed_R
166
167    @property
168    def is_open(self) -> bool:
169        if self.is_empty:
170            return True
171        if self.is_singleton:
172            return False
173        return not self.closed_L and not self.closed_R
174
175    @property
176    def is_half_open(self) -> bool:
177        return (self.closed_L and not self.closed_R) or (
178            not self.closed_L and self.closed_R
179        )
180
181    @property
182    def is_singleton(self) -> bool:
183        return self.singleton_set is not None and len(self.singleton_set) == 1
184
185    @property
186    def is_empty(self) -> bool:
187        return self.singleton_set is not None and len(self.singleton_set) == 0
188
189    @property
190    def is_finite(self) -> bool:
191        return not math.isinf(self.lower) and not math.isinf(self.upper)
192
193    @property
194    def singleton(self) -> Number:
195        if not self.is_singleton:
196            raise ValueError("Interval is not a singleton")
197        return next(iter(self.singleton_set))  # type: ignore[arg-type]
198
199    @staticmethod
200    def get_empty() -> Interval:
201        return Interval(math.nan, math.nan, closed_L=None, closed_R=None)
202
203    @staticmethod
204    def get_singleton(value: Number) -> Interval:
205        if math.isnan(value) or value is None:
206            return Interval.get_empty()
207        return Interval(value, value, closed_L=True, closed_R=True)
208
209    def numerical_contained(self, item: Number) -> bool:
210        if self.is_empty:
211            return False
212        if math.isnan(item):
213            raise ValueError("NaN cannot be checked for containment in an interval")
214        if self.is_singleton:
215            return item in self.singleton_set  # type: ignore[operator]
216        return ((self.closed_L and item >= self.lower) or item > self.lower) and (
217            (self.closed_R and item <= self.upper) or item < self.upper
218        )
219
220    def interval_contained(self, item: Interval) -> bool:
221        if item.is_empty:
222            return True
223        if self.is_empty:
224            return False
225        if item.is_singleton:
226            return self.numerical_contained(item.singleton)
227        if self.is_singleton:
228            if not item.is_singleton:
229                return False
230            return self.singleton == item.singleton
231
232        lower_contained: bool = (
233            # either strictly wider bound
234            self.lower < item.lower
235            # if same, then self must be closed if item is open
236            or (self.lower == item.lower and self.closed_L >= item.closed_L)
237        )
238
239        upper_contained: bool = (
240            # either strictly wider bound
241            self.upper > item.upper
242            # if same, then self must be closed if item is open
243            or (self.upper == item.upper and self.closed_R >= item.closed_R)
244        )
245
246        return lower_contained and upper_contained
247
248    def __contains__(self, item: Any) -> bool:
249        if isinstance(item, Interval):
250            return self.interval_contained(item)
251        else:
252            return self.numerical_contained(item)
253
254    def __repr__(self) -> str:
255        if self.is_empty:
256            return r"∅"
257        if self.is_singleton:
258            return "{" + str(self.singleton) + "}"
259        left: str = "[" if self.closed_L else "("
260        right: str = "]" if self.closed_R else ")"
261        return f"{left}{self.lower}, {self.upper}{right}"
262
263    def __str__(self) -> str:
264        return repr(self)
265
266    @classmethod
267    def from_str(cls, input_str: str) -> Interval:
268        input_str = input_str.strip()
269        # empty and singleton
270        if input_str.count(",") == 0:
271            # empty set
272            if input_str == "∅":
273                return cls.get_empty()
274            assert input_str.startswith("{") and input_str.endswith(
275                "}"
276            ), "Invalid input string"
277            input_str_set_interior: str = input_str.strip("{}").strip()
278            if len(input_str_set_interior) == 0:
279                return cls.get_empty()
280            # singleton set
281            return cls.get_singleton(str_to_numeric(input_str_set_interior))
282
283        # expect commas
284        if not input_str.count(",") == 1:
285            raise ValueError("Invalid input string")
286
287        # get bounds
288        lower: str
289        upper: str
290        lower, upper = input_str.strip("[]()").split(",")
291        lower = lower.strip()
292        upper = upper.strip()
293
294        lower_num: Number = str_to_numeric(lower)
295        upper_num: Number = str_to_numeric(upper)
296
297        # figure out closure
298        closed_L: bool
299        closed_R: bool
300        if input_str[0] == "[":
301            closed_L = True
302        elif input_str[0] == "(":
303            closed_L = False
304        else:
305            raise ValueError("Invalid input string")
306
307        if input_str[-1] == "]":
308            closed_R = True
309        elif input_str[-1] == ")":
310            closed_R = False
311        else:
312            raise ValueError("Invalid input string")
313
314        return cls(lower_num, upper_num, closed_L=closed_L, closed_R=closed_R)
315
316    def __eq__(self, other: object) -> bool:
317        if not isinstance(other, Interval):
318            return False
319        if self.is_empty and other.is_empty:
320            return True
321        if self.is_singleton and other.is_singleton:
322            return self.singleton == other.singleton
323        return (self.lower, self.upper, self.closed_L, self.closed_R) == (
324            other.lower,
325            other.upper,
326            other.closed_L,
327            other.closed_R,
328        )
329
330    def __iter__(self):
331        if self.is_empty:
332            return
333        elif self.is_singleton:
334            yield self.singleton
335            return
336        else:
337            yield self.lower
338            yield self.upper
339
340    def __getitem__(self, index: int) -> float:
341        if self.is_empty:
342            raise IndexError("Empty interval has no bounds")
343        if self.is_singleton:
344            if index == 0:
345                return self.singleton
346            else:
347                raise IndexError("Singleton interval has only one bound")
348        if index == 0:
349            return self.lower
350        elif index == 1:
351            return self.upper
352        else:
353            raise IndexError("Interval index out of range")
354
355    def __len__(self) -> int:
356        return 0 if self.is_empty else 1 if self.is_singleton else 2
357
358    def copy(self) -> Interval:
359        if self.is_empty:
360            return Interval.get_empty()
361        if self.is_singleton:
362            return Interval.get_singleton(self.singleton)
363        return Interval(
364            self.lower, self.upper, closed_L=self.closed_L, closed_R=self.closed_R
365        )
366
367    def size(self) -> float:
368        """
369        Returns the size of the interval.
370
371        # Returns:
372
373         - `float`
374            the size of the interval
375        """
376        if self.is_empty or self.is_singleton:
377            return 0
378        else:
379            return self.upper - self.lower
380
381    def clamp(self, value: Union[int, float], epsilon: float = _EPSILON) -> float:
382        """
383        Clamp the given value to the interval bounds.
384
385        For open bounds, the clamped value will be slightly inside the interval (by epsilon).
386
387        # Parameters:
388
389         - `value : Union[int, float]`
390           the value to clamp.
391         - `epsilon : float`
392           margin for open bounds
393           (defaults to `_EPSILON`)
394
395        # Returns:
396
397         - `float`
398            the clamped value
399
400        # Raises:
401
402         - `ValueError` : If the input value is NaN.
403        """
404
405        if math.isnan(value):
406            raise ValueError("Cannot clamp NaN value")
407
408        if math.isnan(epsilon):
409            raise ValueError("Epsilon cannot be NaN")
410
411        if epsilon < 0:
412            raise ValueError(f"Epsilon must be non-negative: {epsilon = }")
413
414        if self.is_empty:
415            raise ValueError("Cannot clamp to an empty interval")
416
417        if self.is_singleton:
418            return self.singleton
419
420        if epsilon > self.size():
421            raise ValueError(
422                f"epsilon is greater than the size of the interval: {epsilon = }, {self.size() = }, {self = }"
423            )
424
425        # make type work with decimals and stuff
426        if not isinstance(value, (int, float)):
427            epsilon = value.__class__(epsilon)
428
429        clamped_min: Number
430        if self.closed_L:
431            clamped_min = self.lower
432        else:
433            clamped_min = self.lower + epsilon
434
435        clamped_max: Number
436        if self.closed_R:
437            clamped_max = self.upper
438        else:
439            clamped_max = self.upper - epsilon
440
441        return max(clamped_min, min(value, clamped_max))
442
443    def intersection(self, other: Interval) -> Interval:
444        if not isinstance(other, Interval):
445            raise TypeError("Can only intersect with another Interval")
446
447        if self.is_empty or other.is_empty:
448            return Interval.get_empty()
449
450        if self.is_singleton:
451            if other.numerical_contained(self.singleton):
452                return self.copy()
453            else:
454                return Interval.get_empty()
455
456        if other.is_singleton:
457            if self.numerical_contained(other.singleton):
458                return other.copy()
459            else:
460                return Interval.get_empty()
461
462        if self.upper < other.lower or other.upper < self.lower:
463            return Interval.get_empty()
464
465        lower: Number = max(self.lower, other.lower)
466        upper: Number = min(self.upper, other.upper)
467        closed_L: bool = self.closed_L if self.lower > other.lower else other.closed_L
468        closed_R: bool = self.closed_R if self.upper < other.upper else other.closed_R
469
470        return Interval(lower, upper, closed_L=closed_L, closed_R=closed_R)
471
472    def union(self, other: Interval) -> Interval:
473        if not isinstance(other, Interval):
474            raise TypeError("Can only union with another Interval")
475
476        # empty set case
477        if self.is_empty:
478            return other.copy()
479        if other.is_empty:
480            return self.copy()
481
482        # special case where the intersection is empty but the intervals are contiguous
483        if self.upper == other.lower:
484            if self.closed_R or other.closed_L:
485                return Interval(
486                    self.lower,
487                    other.upper,
488                    closed_L=self.closed_L,
489                    closed_R=other.closed_R,
490                )
491        elif other.upper == self.lower:
492            if other.closed_R or self.closed_L:
493                return Interval(
494                    other.lower,
495                    self.upper,
496                    closed_L=other.closed_L,
497                    closed_R=self.closed_R,
498                )
499
500        # non-intersecting nonempty and non-contiguous intervals
501        if self.intersection(other) == Interval.get_empty():
502            raise NotImplementedError(
503                "Union of non-intersecting nonempty non-contiguous intervals is not implemented "
504                + f"{self = }, {other = }, {self.intersection(other) = }"
505            )
506
507        # singleton case
508        if self.is_singleton:
509            return other.copy()
510        if other.is_singleton:
511            return self.copy()
512
513        # regular case
514        lower: Number = min(self.lower, other.lower)
515        upper: Number = max(self.upper, other.upper)
516        closed_L: bool = self.closed_L if self.lower < other.lower else other.closed_L
517        closed_R: bool = self.closed_R if self.upper > other.upper else other.closed_R
518
519        return Interval(lower, upper, closed_L=closed_L, closed_R=closed_R)

Represents a mathematical interval, open by default.

The Interval class can represent both open and closed intervals, as well as half-open intervals. It supports various initialization methods and provides containment checks.

Examples:

>>> i1 = Interval(1, 5)  # Default open interval (1, 5)
>>> 3 in i1
True
>>> 1 in i1
False
>>> i2 = Interval([1, 5])  # Closed interval [1, 5]
>>> 1 in i2
True
>>> i3 = Interval(1, 5, closed_L=True)  # Half-open interval [1, 5)
>>> str(i3)
'[1, 5)'
>>> i4 = ClosedInterval(1, 5)  # Closed interval [1, 5]
>>> i5 = OpenInterval(1, 5)  # Open interval (1, 5)
Interval( *args: Union[Sequence[Union[float, int]], float, int], is_closed: Optional[bool] = None, closed_L: Optional[bool] = None, closed_R: Optional[bool] = None)
 52    def __init__(
 53        self,
 54        *args: Union[Sequence[Number], Number],
 55        is_closed: Optional[bool] = None,
 56        closed_L: Optional[bool] = None,
 57        closed_R: Optional[bool] = None,
 58    ):
 59        self.lower: Number
 60        self.upper: Number
 61        self.closed_L: bool
 62        self.closed_R: bool
 63        self.singleton_set: Optional[set[Number]] = None
 64        try:
 65            if len(args) == 0:
 66                (
 67                    self.lower,
 68                    self.upper,
 69                    self.closed_L,
 70                    self.closed_R,
 71                    self.singleton_set,
 72                ) = _EMPTY_INTERVAL_ARGS
 73                return
 74            # Handle different types of input arguments
 75            if len(args) == 1 and isinstance(
 76                args[0], (list, tuple, Sequence, Iterable)
 77            ):
 78                assert (
 79                    len(args[0]) == 2
 80                ), "if arg is a list or tuple, it must have length 2"
 81                self.lower = args[0][0]
 82                self.upper = args[0][1]
 83                # Determine closure type based on the container type
 84                default_closed = isinstance(args[0], list)
 85            elif len(args) == 1 and isinstance(
 86                args[0], (int, float, typing.SupportsFloat, typing.SupportsInt)
 87            ):
 88                # a singleton, but this will be handled later
 89                self.lower = args[0]
 90                self.upper = args[0]
 91                default_closed = False
 92            elif len(args) == 2:
 93                self.lower, self.upper = args  # type: ignore[assignment]
 94                default_closed = False  # Default to open interval if two args
 95            else:
 96                raise ValueError(f"Invalid input arguments: {args}")
 97
 98            # if both of the bounds are NaN or None, return an empty interval
 99            if any(x is None for x in (self.lower, self.upper)) or any(
100                math.isnan(x) for x in (self.lower, self.upper)
101            ):
102                if (self.lower is None and self.upper is None) or (
103                    math.isnan(self.lower) and math.isnan(self.upper)
104                ):
105                    (
106                        self.lower,
107                        self.upper,
108                        self.closed_L,
109                        self.closed_R,
110                        self.singleton_set,
111                    ) = _EMPTY_INTERVAL_ARGS
112                    return
113                else:
114                    raise ValueError(
115                        "Both bounds must be NaN or None to create an empty interval. Also, just use `Interval.get_empty()` instead."
116                    )
117
118            # Ensure lower bound is less than upper bound
119            if self.lower > self.upper:
120                raise ValueError("Lower bound must be less than upper bound")
121
122            if math.isnan(self.lower) or math.isnan(self.upper):
123                raise ValueError("NaN is not allowed as an interval bound")
124
125            # Determine closure properties
126            if is_closed is not None:
127                # can't specify both is_closed and closed_L/R
128                if (closed_L is not None) or (closed_R is not None):
129                    raise ValueError("Cannot specify both is_closed and closed_L/R")
130                self.closed_L = is_closed
131                self.closed_R = is_closed
132            else:
133                self.closed_L = closed_L if closed_L is not None else default_closed
134                self.closed_R = closed_R if closed_R is not None else default_closed
135
136            # handle singleton/empty case
137            if self.lower == self.upper and not (self.closed_L or self.closed_R):
138                (
139                    self.lower,
140                    self.upper,
141                    self.closed_L,
142                    self.closed_R,
143                    self.singleton_set,
144                ) = _EMPTY_INTERVAL_ARGS
145                return
146
147            elif self.lower == self.upper and (self.closed_L or self.closed_R):
148                self.singleton_set = {self.lower}  # Singleton interval
149                self.closed_L = True
150                self.closed_R = True
151                return
152            # otherwise `singleton_set` is `None`
153
154        except (AssertionError, ValueError) as e:
155            raise ValueError(
156                f"Invalid input arguments to Interval: {args = }, {is_closed = }, {closed_L = }, {closed_R = }\n{e}\nUsage:\n{self.__doc__}"
157            ) from e
lower: Union[float, int]
upper: Union[float, int]
closed_L: bool
closed_R: bool
singleton_set: Optional[set[Union[float, int]]]
is_closed: bool
159    @property
160    def is_closed(self) -> bool:
161        if self.is_empty:
162            return True
163        if self.is_singleton:
164            return True
165        return self.closed_L and self.closed_R
is_open: bool
167    @property
168    def is_open(self) -> bool:
169        if self.is_empty:
170            return True
171        if self.is_singleton:
172            return False
173        return not self.closed_L and not self.closed_R
is_half_open: bool
175    @property
176    def is_half_open(self) -> bool:
177        return (self.closed_L and not self.closed_R) or (
178            not self.closed_L and self.closed_R
179        )
is_singleton: bool
181    @property
182    def is_singleton(self) -> bool:
183        return self.singleton_set is not None and len(self.singleton_set) == 1
is_empty: bool
185    @property
186    def is_empty(self) -> bool:
187        return self.singleton_set is not None and len(self.singleton_set) == 0
is_finite: bool
189    @property
190    def is_finite(self) -> bool:
191        return not math.isinf(self.lower) and not math.isinf(self.upper)
singleton: Union[float, int]
193    @property
194    def singleton(self) -> Number:
195        if not self.is_singleton:
196            raise ValueError("Interval is not a singleton")
197        return next(iter(self.singleton_set))  # type: ignore[arg-type]
@staticmethod
def get_empty() -> Interval:
199    @staticmethod
200    def get_empty() -> Interval:
201        return Interval(math.nan, math.nan, closed_L=None, closed_R=None)
@staticmethod
def get_singleton(value: Union[float, int]) -> Interval:
203    @staticmethod
204    def get_singleton(value: Number) -> Interval:
205        if math.isnan(value) or value is None:
206            return Interval.get_empty()
207        return Interval(value, value, closed_L=True, closed_R=True)
def numerical_contained(self, item: Union[float, int]) -> bool:
209    def numerical_contained(self, item: Number) -> bool:
210        if self.is_empty:
211            return False
212        if math.isnan(item):
213            raise ValueError("NaN cannot be checked for containment in an interval")
214        if self.is_singleton:
215            return item in self.singleton_set  # type: ignore[operator]
216        return ((self.closed_L and item >= self.lower) or item > self.lower) and (
217            (self.closed_R and item <= self.upper) or item < self.upper
218        )
def interval_contained(self, item: Interval) -> bool:
220    def interval_contained(self, item: Interval) -> bool:
221        if item.is_empty:
222            return True
223        if self.is_empty:
224            return False
225        if item.is_singleton:
226            return self.numerical_contained(item.singleton)
227        if self.is_singleton:
228            if not item.is_singleton:
229                return False
230            return self.singleton == item.singleton
231
232        lower_contained: bool = (
233            # either strictly wider bound
234            self.lower < item.lower
235            # if same, then self must be closed if item is open
236            or (self.lower == item.lower and self.closed_L >= item.closed_L)
237        )
238
239        upper_contained: bool = (
240            # either strictly wider bound
241            self.upper > item.upper
242            # if same, then self must be closed if item is open
243            or (self.upper == item.upper and self.closed_R >= item.closed_R)
244        )
245
246        return lower_contained and upper_contained
@classmethod
def from_str(cls, input_str: str) -> Interval:
266    @classmethod
267    def from_str(cls, input_str: str) -> Interval:
268        input_str = input_str.strip()
269        # empty and singleton
270        if input_str.count(",") == 0:
271            # empty set
272            if input_str == "∅":
273                return cls.get_empty()
274            assert input_str.startswith("{") and input_str.endswith(
275                "}"
276            ), "Invalid input string"
277            input_str_set_interior: str = input_str.strip("{}").strip()
278            if len(input_str_set_interior) == 0:
279                return cls.get_empty()
280            # singleton set
281            return cls.get_singleton(str_to_numeric(input_str_set_interior))
282
283        # expect commas
284        if not input_str.count(",") == 1:
285            raise ValueError("Invalid input string")
286
287        # get bounds
288        lower: str
289        upper: str
290        lower, upper = input_str.strip("[]()").split(",")
291        lower = lower.strip()
292        upper = upper.strip()
293
294        lower_num: Number = str_to_numeric(lower)
295        upper_num: Number = str_to_numeric(upper)
296
297        # figure out closure
298        closed_L: bool
299        closed_R: bool
300        if input_str[0] == "[":
301            closed_L = True
302        elif input_str[0] == "(":
303            closed_L = False
304        else:
305            raise ValueError("Invalid input string")
306
307        if input_str[-1] == "]":
308            closed_R = True
309        elif input_str[-1] == ")":
310            closed_R = False
311        else:
312            raise ValueError("Invalid input string")
313
314        return cls(lower_num, upper_num, closed_L=closed_L, closed_R=closed_R)
def copy(self) -> Interval:
358    def copy(self) -> Interval:
359        if self.is_empty:
360            return Interval.get_empty()
361        if self.is_singleton:
362            return Interval.get_singleton(self.singleton)
363        return Interval(
364            self.lower, self.upper, closed_L=self.closed_L, closed_R=self.closed_R
365        )
def size(self) -> float:
367    def size(self) -> float:
368        """
369        Returns the size of the interval.
370
371        # Returns:
372
373         - `float`
374            the size of the interval
375        """
376        if self.is_empty or self.is_singleton:
377            return 0
378        else:
379            return self.upper - self.lower

Returns the size of the interval.

Returns:

  • float the size of the interval
def clamp(self, value: Union[int, float], epsilon: float = 1e-10) -> float:
381    def clamp(self, value: Union[int, float], epsilon: float = _EPSILON) -> float:
382        """
383        Clamp the given value to the interval bounds.
384
385        For open bounds, the clamped value will be slightly inside the interval (by epsilon).
386
387        # Parameters:
388
389         - `value : Union[int, float]`
390           the value to clamp.
391         - `epsilon : float`
392           margin for open bounds
393           (defaults to `_EPSILON`)
394
395        # Returns:
396
397         - `float`
398            the clamped value
399
400        # Raises:
401
402         - `ValueError` : If the input value is NaN.
403        """
404
405        if math.isnan(value):
406            raise ValueError("Cannot clamp NaN value")
407
408        if math.isnan(epsilon):
409            raise ValueError("Epsilon cannot be NaN")
410
411        if epsilon < 0:
412            raise ValueError(f"Epsilon must be non-negative: {epsilon = }")
413
414        if self.is_empty:
415            raise ValueError("Cannot clamp to an empty interval")
416
417        if self.is_singleton:
418            return self.singleton
419
420        if epsilon > self.size():
421            raise ValueError(
422                f"epsilon is greater than the size of the interval: {epsilon = }, {self.size() = }, {self = }"
423            )
424
425        # make type work with decimals and stuff
426        if not isinstance(value, (int, float)):
427            epsilon = value.__class__(epsilon)
428
429        clamped_min: Number
430        if self.closed_L:
431            clamped_min = self.lower
432        else:
433            clamped_min = self.lower + epsilon
434
435        clamped_max: Number
436        if self.closed_R:
437            clamped_max = self.upper
438        else:
439            clamped_max = self.upper - epsilon
440
441        return max(clamped_min, min(value, clamped_max))

Clamp the given value to the interval bounds.

For open bounds, the clamped value will be slightly inside the interval (by epsilon).

Parameters:

  • value : Union[int, float] the value to clamp.
  • epsilon : float margin for open bounds (defaults to _EPSILON)

Returns:

  • float the clamped value

Raises:

  • ValueError : If the input value is NaN.
def intersection(self, other: Interval) -> Interval:
443    def intersection(self, other: Interval) -> Interval:
444        if not isinstance(other, Interval):
445            raise TypeError("Can only intersect with another Interval")
446
447        if self.is_empty or other.is_empty:
448            return Interval.get_empty()
449
450        if self.is_singleton:
451            if other.numerical_contained(self.singleton):
452                return self.copy()
453            else:
454                return Interval.get_empty()
455
456        if other.is_singleton:
457            if self.numerical_contained(other.singleton):
458                return other.copy()
459            else:
460                return Interval.get_empty()
461
462        if self.upper < other.lower or other.upper < self.lower:
463            return Interval.get_empty()
464
465        lower: Number = max(self.lower, other.lower)
466        upper: Number = min(self.upper, other.upper)
467        closed_L: bool = self.closed_L if self.lower > other.lower else other.closed_L
468        closed_R: bool = self.closed_R if self.upper < other.upper else other.closed_R
469
470        return Interval(lower, upper, closed_L=closed_L, closed_R=closed_R)
def union(self, other: Interval) -> Interval:
472    def union(self, other: Interval) -> Interval:
473        if not isinstance(other, Interval):
474            raise TypeError("Can only union with another Interval")
475
476        # empty set case
477        if self.is_empty:
478            return other.copy()
479        if other.is_empty:
480            return self.copy()
481
482        # special case where the intersection is empty but the intervals are contiguous
483        if self.upper == other.lower:
484            if self.closed_R or other.closed_L:
485                return Interval(
486                    self.lower,
487                    other.upper,
488                    closed_L=self.closed_L,
489                    closed_R=other.closed_R,
490                )
491        elif other.upper == self.lower:
492            if other.closed_R or self.closed_L:
493                return Interval(
494                    other.lower,
495                    self.upper,
496                    closed_L=other.closed_L,
497                    closed_R=self.closed_R,
498                )
499
500        # non-intersecting nonempty and non-contiguous intervals
501        if self.intersection(other) == Interval.get_empty():
502            raise NotImplementedError(
503                "Union of non-intersecting nonempty non-contiguous intervals is not implemented "
504                + f"{self = }, {other = }, {self.intersection(other) = }"
505            )
506
507        # singleton case
508        if self.is_singleton:
509            return other.copy()
510        if other.is_singleton:
511            return self.copy()
512
513        # regular case
514        lower: Number = min(self.lower, other.lower)
515        upper: Number = max(self.upper, other.upper)
516        closed_L: bool = self.closed_L if self.lower < other.lower else other.closed_L
517        closed_R: bool = self.closed_R if self.upper > other.upper else other.closed_R
518
519        return Interval(lower, upper, closed_L=closed_L, closed_R=closed_R)
class ClosedInterval(Interval):
522class ClosedInterval(Interval):
523    def __init__(self, *args: Union[Sequence[float], float], **kwargs: Any):
524        if any(key in kwargs for key in ("is_closed", "closed_L", "closed_R")):
525            raise ValueError("Cannot specify closure properties for ClosedInterval")
526        super().__init__(*args, is_closed=True)

Represents a mathematical interval, open by default.

The Interval class can represent both open and closed intervals, as well as half-open intervals. It supports various initialization methods and provides containment checks.

Examples:

>>> i1 = Interval(1, 5)  # Default open interval (1, 5)
>>> 3 in i1
True
>>> 1 in i1
False
>>> i2 = Interval([1, 5])  # Closed interval [1, 5]
>>> 1 in i2
True
>>> i3 = Interval(1, 5, closed_L=True)  # Half-open interval [1, 5)
>>> str(i3)
'[1, 5)'
>>> i4 = ClosedInterval(1, 5)  # Closed interval [1, 5]
>>> i5 = OpenInterval(1, 5)  # Open interval (1, 5)
ClosedInterval(*args: Union[Sequence[float], float], **kwargs: Any)
523    def __init__(self, *args: Union[Sequence[float], float], **kwargs: Any):
524        if any(key in kwargs for key in ("is_closed", "closed_L", "closed_R")):
525            raise ValueError("Cannot specify closure properties for ClosedInterval")
526        super().__init__(*args, is_closed=True)
class OpenInterval(Interval):
529class OpenInterval(Interval):
530    def __init__(self, *args: Union[Sequence[float], float], **kwargs: Any):
531        if any(key in kwargs for key in ("is_closed", "closed_L", "closed_R")):
532            raise ValueError("Cannot specify closure properties for OpenInterval")
533        super().__init__(*args, is_closed=False)

Represents a mathematical interval, open by default.

The Interval class can represent both open and closed intervals, as well as half-open intervals. It supports various initialization methods and provides containment checks.

Examples:

>>> i1 = Interval(1, 5)  # Default open interval (1, 5)
>>> 3 in i1
True
>>> 1 in i1
False
>>> i2 = Interval([1, 5])  # Closed interval [1, 5]
>>> 1 in i2
True
>>> i3 = Interval(1, 5, closed_L=True)  # Half-open interval [1, 5)
>>> str(i3)
'[1, 5)'
>>> i4 = ClosedInterval(1, 5)  # Closed interval [1, 5]
>>> i5 = OpenInterval(1, 5)  # Open interval (1, 5)
OpenInterval(*args: Union[Sequence[float], float], **kwargs: Any)
530    def __init__(self, *args: Union[Sequence[float], float], **kwargs: Any):
531        if any(key in kwargs for key in ("is_closed", "closed_L", "closed_R")):
532            raise ValueError("Cannot specify closure properties for OpenInterval")
533        super().__init__(*args, is_closed=False)