docs for muutils v0.6.17
View Source on GitHub

# muutils.interval

represents a mathematical `Interval` over the real numbers

View Source
View on GitHub

      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:
View Source
View on GitHub

     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)
View Source
View on GitHub

     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
View Source
View on GitHub

    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
View Source
View on GitHub

    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
View Source
View on GitHub

    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
View Source
View on GitHub

    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
View Source
View on GitHub

    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
View Source
View on GitHub

    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]
View Source
View on GitHub

    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:
View Source
View on GitHub

    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:
View Source
View on GitHub

    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:
View Source
View on GitHub

    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:
View Source
View on GitHub

    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:
View Source
View on GitHub

    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:
View Source
View on GitHub

    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:
View Source
View on GitHub

    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:
View Source
View on GitHub

    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]:
View Source
View on GitHub

    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:
View Source
View on GitHub

    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):
View Source
View on GitHub

    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)
View Source
View on GitHub

    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)

##### Inherited Members

Interval
  lower
  upper
  closed_L
  closed_R
  singleton_set
  is_closed
  is_open
  is_half_open
  is_singleton
  is_empty
  is_finite
  singleton
  get_empty
  get_singleton
  numerical_contained
  interval_contained
  from_str
  copy
  size
  clamp
  intersection
  union

class OpenInterval(Interval):
View Source
View on GitHub

    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)
View Source
View on GitHub

    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)

##### Inherited Members

Interval
  lower
  upper
  closed_L
  closed_R
  singleton_set
  is_closed
  is_open
  is_half_open
  is_singleton
  is_empty
  is_finite
  singleton
  get_empty
  get_singleton
  numerical_contained
  interval_contained
  from_str
  copy
  size
  clamp
  intersection
  union