File size: 4,444 Bytes
767a051 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | """Closed exact rational intervals and outward-exact arithmetic."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Iterable
from .rational import Q
@dataclass(frozen=True, slots=True)
class ClosedInterval:
lower: Q
upper: Q
def __post_init__(self) -> None:
lo = Q.from_value(self.lower)
hi = Q.from_value(self.upper)
if hi < lo:
raise ValueError(f"interval upper {hi} is below lower {lo}")
object.__setattr__(self, "lower", lo)
object.__setattr__(self, "upper", hi)
@classmethod
def point(cls, value: Any) -> "ClosedInterval":
q = Q.from_value(value)
return cls(q, q)
@classmethod
def from_value(cls, value: Any) -> "ClosedInterval":
if isinstance(value, cls):
return value
if isinstance(value, dict):
return cls(Q.from_value(value["lower"]), Q.from_value(value["upper"]))
if isinstance(value, (list, tuple)) and len(value) == 2:
return cls(Q.from_value(value[0]), Q.from_value(value[1]))
return cls.point(value)
@classmethod
def hull(cls, values: Iterable[Any]) -> "ClosedInterval":
items = [Q.from_value(v) for v in values]
if not items:
raise ValueError("cannot form interval hull of an empty sequence")
return cls(min(items), max(items))
def to_record(self) -> dict[str, dict[str, int]]:
return {"lower": self.lower.to_record(), "upper": self.upper.to_record()}
def to_text(self) -> str:
return f"[{self.lower.to_text()},{self.upper.to_text()}]"
def width(self) -> Q:
return self.upper - self.lower
def midpoint(self) -> Q:
return (self.lower + self.upper) / 2
def contains(self, value: Any) -> bool:
q = Q.from_value(value)
return self.lower <= q <= self.upper
def contains_zero(self) -> bool:
return self.contains(0)
def interior_contains_zero(self) -> bool:
return self.lower < 0 < self.upper
def is_point(self) -> bool:
return self.lower == self.upper
def sign_class(self) -> str:
if self.upper < 0:
return "negative"
if self.lower > 0:
return "positive"
zero = Q(0)
if self.lower == zero and self.upper == zero:
return "zero"
if self.lower == zero:
return "nonnegative"
if self.upper == zero:
return "nonpositive"
return "straddles-zero"
def excludes_zero(self) -> bool:
return self.upper < 0 or self.lower > 0
def intersection(self, other: Any) -> "ClosedInterval | None":
b = ClosedInterval.from_value(other)
lo = max(self.lower, b.lower)
hi = min(self.upper, b.upper)
return None if hi < lo else ClosedInterval(lo, hi)
def subset_of(self, other: Any) -> bool:
b = ClosedInterval.from_value(other)
return b.lower <= self.lower and self.upper <= b.upper
def __neg__(self) -> "ClosedInterval":
return ClosedInterval(-self.upper, -self.lower)
def __add__(self, other: Any) -> "ClosedInterval":
b = ClosedInterval.from_value(other)
return ClosedInterval(self.lower + b.lower, self.upper + b.upper)
def __radd__(self, other: Any) -> "ClosedInterval":
return self + other
def __sub__(self, other: Any) -> "ClosedInterval":
return self + (-ClosedInterval.from_value(other))
def __rsub__(self, other: Any) -> "ClosedInterval":
return ClosedInterval.from_value(other) - self
def __mul__(self, other: Any) -> "ClosedInterval":
b = ClosedInterval.from_value(other)
products = (
self.lower * b.lower,
self.lower * b.upper,
self.upper * b.lower,
self.upper * b.upper,
)
return ClosedInterval(min(products), max(products))
def __rmul__(self, other: Any) -> "ClosedInterval":
return self * other
def reciprocal(self) -> "ClosedInterval":
if self.contains_zero():
raise ZeroDivisionError("interval reciprocal is undefined across zero")
return ClosedInterval(1 / self.upper, 1 / self.lower)
def __truediv__(self, other: Any) -> "ClosedInterval":
return self * ClosedInterval.from_value(other).reciprocal()
def __str__(self) -> str:
return self.to_text()
|