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

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