decoint 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
decoint/interval.py ADDED
@@ -0,0 +1,546 @@
1
+ from dataclasses import dataclass
2
+ from gmpy2 import mpfr, RoundUp, RoundDown, context, get_context, is_nan, is_infinite, next_below
3
+ import re
4
+ from typing import Tuple
5
+
6
+ ctx = get_context()
7
+ ctx.precision = 53
8
+ ctx.emin = -1074
9
+ ctx.emax = 1024
10
+
11
+ BOX_REGEX = re.compile(r"^\[\s*([^,;\]]+?)\s*(?:[,;]\s*([^,;\]]*?))?\s*\]$")
12
+ UNC_REGEX = re.compile(r"^([^?]+)\?(.*)$")
13
+
14
+ Number = mpfr
15
+ @dataclass(frozen = True)
16
+ class Interval:
17
+ lo: Number = Number('-inf')
18
+ hi: Number = Number('inf')
19
+
20
+ def __post_init__(self):
21
+
22
+ if isinstance(self.lo, str):
23
+ parsed_interval = Interval.from_string(self.lo)
24
+ if not (is_nan(parsed_interval.lo) or is_nan(parsed_interval.hi)):
25
+ object.__setattr__(self, "lo", parsed_interval.lo)
26
+ object.__setattr__(self, "hi", parsed_interval.hi)
27
+ else:
28
+ pass
29
+
30
+
31
+ with context(get_context(), round=RoundDown):
32
+ lo = Number(self.lo)
33
+ with context(get_context(), round=RoundUp):
34
+ hi = Number(self.hi)
35
+
36
+ if lo > hi:
37
+ object.__setattr__(self, "lo", Number('inf'))
38
+ object.__setattr__(self, "hi", Number('-inf'))
39
+
40
+ if is_nan(lo) or is_nan(hi):
41
+ object.__setattr__(self, "lo", Number('NaN'))
42
+ object.__setattr__(self, "hi", Number('NaN'))
43
+
44
+ elif lo == Number('-inf') and hi == Number('-inf'):
45
+ object.__setattr__(self, "lo", Number('NaN'))
46
+ object.__setattr__(self, "hi", Number('NaN'))
47
+
48
+ elif lo == Number('inf') and hi == Number('inf'):
49
+ object.__setattr__(self, "lo", Number('NaN'))
50
+ object.__setattr__(self, "hi", Number('NaN'))
51
+
52
+ else:
53
+ object.__setattr__(self, "lo", lo)
54
+ object.__setattr__(self, "hi", hi)
55
+
56
+ @classmethod
57
+ def empty(cls):
58
+ return cls(Number('inf'), Number('-inf'))
59
+ @classmethod
60
+ def entire(cls):
61
+ return cls(Number('-inf'), Number('inf'))
62
+
63
+ @classmethod
64
+ def _coerce(cls, value):
65
+ if isinstance(value, cls):
66
+ return value
67
+ if isinstance(value, str):
68
+ return cls.from_string(value)
69
+ if hasattr(value, "interval"):
70
+ return value.interval
71
+ return cls(value, value)
72
+
73
+ @classmethod
74
+ def from_string(cls, s: str):
75
+ content = s.strip()
76
+ content_compact = "".join(content.split()).lower()
77
+
78
+ if content_compact in ("[]", "[empty]"):
79
+ return cls.empty()
80
+ if content_compact in ("[entire]", "[,]"):
81
+ return cls(Number('-inf'), Number('inf'))
82
+
83
+ box_match = BOX_REGEX.match(content)
84
+ if box_match:
85
+ left, right = box_match.groups()
86
+ if right is None:
87
+ lo = cls._parse_bound(left, round_up = False)
88
+ hi = cls._parse_bound(left, round_up = True)
89
+ else:
90
+ lo = cls._parse_bound(left, round_up = False)
91
+ hi = cls._parse_bound(right, round_up = True)
92
+ return cls(lo, hi)
93
+
94
+ unc_match = UNC_REGEX.match(content)
95
+ if unc_match:
96
+ lo, hi = cls._parse_uncertainty(content)
97
+ return cls(lo, hi)
98
+
99
+ return Interval(Number('nan'), Number('nan'))
100
+
101
+ @classmethod
102
+ def _parse_bound(cls, s: str, round_up: bool):
103
+ s_clean = s.strip() if s else ""
104
+ s_lower = s_clean.lower()
105
+ if not s_clean:
106
+ return Number('inf') if round_up else Number('-inf')
107
+ if s_lower in ('inf', '+inf', 'infinity', '+infinity'):
108
+ return Number('inf')
109
+ if s_lower in ('-inf', '-infinity'):
110
+ return Number('-inf')
111
+
112
+ target_round = RoundUp if round_up else RoundDown
113
+
114
+ with context(get_context(), round = target_round):
115
+ if '/' in s_clean:
116
+ parts = s_clean.split('/')
117
+ if len(parts) == 2:
118
+ return Number(parts[0]) / Number(parts[1])
119
+
120
+ return Number(s_clean)
121
+
122
+ @classmethod
123
+ def _parse_uncertainty(cls, text: str):
124
+ if "??" in text:
125
+ if text.lower().endswith("??u"):
126
+ center_str = text.split("??")[0]
127
+ lo = cls._parse_bound(center_str, round_up = False)
128
+ return lo, Number('inf')
129
+ elif text.lower().endswith("??d"):
130
+ center_str = text.split("??")[0]
131
+ hi = cls._parse_bound(center_str, round_up = True)
132
+ return Number('-inf'), hi
133
+ else:
134
+ return Number('-inf'), Number('inf')
135
+
136
+ match = UNC_REGEX.match(text)
137
+ if not match:
138
+ raise ValueError("Malformed Uncertainty Syntax")
139
+
140
+ center_str, unc_str = match.groups()
141
+ center_str = center_str.strip()
142
+ unc_str = unc_str.strip() if unc_str else ""
143
+ unc_str_lower = unc_str.lower()
144
+
145
+ dec_digits = 0
146
+ check_str = center_str.lower().lstrip('+-')
147
+ if '.' in center_str and not check_str.startswith('0x'):
148
+ dec_digits = len(center_str.split('.')[1])
149
+
150
+ # DIRECTION FIX 1: Detect 'u' and 'd' flags, then strip them so
151
+ # "5ue-5" becomes "5e-5" for clean exponent splitting.
152
+ is_upper_only = 'u' in unc_str_lower
153
+ is_lower_only = 'd' in unc_str_lower
154
+ if is_upper_only:
155
+ unc_str_lower = unc_str_lower.replace('u', '')
156
+ if is_lower_only:
157
+ unc_str_lower = unc_str_lower.replace('d', '')
158
+
159
+ unc_str_lower = unc_str_lower.strip()
160
+
161
+ # --- UPPER BOUND (hi) ---
162
+ if is_lower_only:
163
+ # DIRECTION FIX 2: If 'd' was present, upper bound is locked to the center value
164
+ # scaled by the exponent (e.g., 2.5 * 10^-5). No tolerance is added.
165
+ with context(get_context(), round = RoundUp):
166
+ hi = Number(center_str)
167
+ if 'e' in unc_str_lower:
168
+ _, exp_part = unc_str_lower.split('e')
169
+ scale_factor = Number(10) ** int(exp_part)
170
+ hi = hi * scale_factor
171
+ else:
172
+ # Standard upper bound (adds tolerance + center scaled by exponent)
173
+ with context(get_context(), round = RoundUp):
174
+ mid_hi = Number(center_str)
175
+ if not unc_str_lower:
176
+ tol_hi = Number('0.5') * (Number(10) ** (-dec_digits))
177
+ elif 'e' in unc_str_lower:
178
+ unc_digits, exp_part = unc_str_lower.split('e')
179
+ u_val = Number(unc_digits) if unc_digits else Number('0.5')
180
+ base_tol = u_val * (Number(10) ** (-dec_digits))
181
+ scale_factor = Number(10) ** int(exp_part)
182
+ mid_hi = mid_hi * scale_factor
183
+ tol_hi = base_tol * scale_factor
184
+ else:
185
+ tol_hi = Number(unc_str_lower) * (Number(10) ** (-dec_digits))
186
+
187
+ hi = mid_hi + tol_hi
188
+
189
+ # --- LOWER BOUND (lo) ---
190
+ if is_upper_only:
191
+ # DIRECTION FIX 3: If 'u' was present, lower bound is locked to the center value
192
+ # scaled by the exponent (e.g., 2.5 * 10^-5). No tolerance is subtracted.
193
+ with context(get_context(), round = RoundDown):
194
+ mid_lo = Number(center_str)
195
+ if 'e' in unc_str_lower:
196
+ _, exp_part = unc_str_lower.split('e')
197
+ scale_factor = (Number(10) ** int(exp_part))
198
+ mid_lo = mid_lo * scale_factor
199
+ lo = mid_lo
200
+ else:
201
+ # Standard lower bound (subtracts tolerance)
202
+ with context(get_context(), round = RoundUp):
203
+ if not unc_str_lower:
204
+ tol_lo = Number('0.5') * (Number(10) ** (-dec_digits))
205
+ elif 'e' in unc_str_lower:
206
+ unc_digits, exp_part = unc_str_lower.split('e')
207
+ u_val = Number(unc_digits) if unc_digits else Number('0.5')
208
+ base_tol = u_val * (Number(10) ** (-dec_digits))
209
+ scale_factor = Number(10) ** int(exp_part)
210
+ tol_lo = base_tol * scale_factor
211
+ else:
212
+ tol_lo = Number(unc_str_lower) * (Number(10) ** (-dec_digits))
213
+
214
+ with context(get_context(), round = RoundDown):
215
+ mid_lo = Number(center_str)
216
+ if 'e' in unc_str_lower:
217
+ _, exp_part = unc_str_lower.split('e')
218
+ scale_factor = Number(10) ** int(exp_part)
219
+ mid_lo = mid_lo * scale_factor
220
+
221
+ lo = mid_lo - tol_lo
222
+
223
+ return lo, hi
224
+
225
+
226
+
227
+ @property
228
+ def is_common(self):
229
+ if not is_nan(self.lo) and self.is_bounded and not self.is_empty:
230
+ return True
231
+ return False
232
+
233
+ @property
234
+ def is_empty(self):
235
+ return self.lo > self.hi
236
+
237
+ @property
238
+ def is_entire(self):
239
+ return not self.is_empty and self.lo == Number('-inf') and self.hi == Number('inf')
240
+
241
+ @property
242
+ def is_bounded(self):
243
+ return not self.is_empty and self.lo != Number('-inf') and self.hi != Number('inf')
244
+
245
+ @property
246
+ def is_singleton(self):
247
+ return not self.is_empty and self.lo == self.hi
248
+
249
+ @property
250
+ def width(self):
251
+ if self.is_empty:
252
+ return Number('nan')
253
+ if not self.is_bounded:
254
+ return Number('inf')
255
+ with context(get_context()) as ctx:
256
+ ctx.round = RoundUp
257
+ return self.hi - self.lo
258
+
259
+ @property
260
+ def radius(self):
261
+ if self.is_empty:
262
+ return Number('nan')
263
+ if not self.is_bounded:
264
+ return Number('inf')
265
+
266
+ m = self.midpoint
267
+ with context(get_context(), round = RoundUp):
268
+ d_lo = m - self.lo
269
+ d_hi = self.hi - m
270
+ return max(d_lo, d_hi)
271
+
272
+ @property
273
+ def midpoint(self):
274
+ if self.is_empty:
275
+ return Number("nan")
276
+ lo, hi = self.lo, self.hi
277
+ if self.is_entire or (is_infinite(lo) and is_infinite(hi)):
278
+ return Number(0)
279
+ if is_infinite(lo):
280
+ return Number(next_below(Number("-inf")))
281
+ if is_infinite(hi):
282
+ return Number(next_below(Number("inf")))
283
+ return (lo / 2) + (hi / 2)
284
+
285
+ @property
286
+ def magnitude(self):
287
+ if self.is_empty:
288
+ return Number("nan")
289
+ return max(abs(self.lo), abs(self.hi))
290
+ @property
291
+ def mignitude(self):
292
+ if self.is_empty:
293
+ return Number("nan")
294
+ if self.contains(0):
295
+ return Number(0)
296
+
297
+ return min(abs(self.lo), abs(self.hi))
298
+
299
+ def contains(self, x):
300
+ try:
301
+ x = Number(x)
302
+ except Exception:
303
+ return False
304
+ if self.is_empty:
305
+ return False
306
+ if is_infinite(x):
307
+ return False
308
+ return self.lo <= x <= self.hi
309
+
310
+ def subset(self, other):
311
+ if not isinstance(other, Interval):
312
+ return False
313
+ if self.is_empty:
314
+ return True
315
+ if other.is_empty:
316
+ return False
317
+ return other.lo <= self.lo and other.hi >= self.hi
318
+
319
+ def proper_subset(self, other):
320
+ if not isinstance(other, Interval):
321
+ return False
322
+ return self.subset(other) and self != other
323
+
324
+ def overlaps(self, other):
325
+ if not isinstance(other, Interval):
326
+ return False
327
+ if self.is_empty or other.is_empty:
328
+ return False
329
+ return max(other.lo, self.lo) <= min(self.hi, other.hi)
330
+
331
+ def intersection(self, other):
332
+ if not isinstance(other, Interval):
333
+ return Interval(Number('nan'), Number('nan'))
334
+ if self.is_empty or other.is_empty:
335
+ return Interval.empty()
336
+ return Interval(max(self.lo, other.lo), min(self.hi, other.hi))
337
+
338
+ def hull(self, other):
339
+ if not isinstance(other, Interval):
340
+ return Interval(Number('nan'), Number('nan'))
341
+ if self.is_empty:
342
+ return other
343
+ if other.is_empty:
344
+ return self
345
+ return Interval(min(self.lo, other.lo), max(self.hi, other.hi))
346
+
347
+ def __repr__(self):
348
+ if self.is_empty:
349
+ return "Interval.empty()"
350
+ if self.is_entire:
351
+ return "Interval.entire()"
352
+ return f"Interval({self.lo}, {self.hi})"
353
+
354
+ def __str__(self):
355
+ if self.is_empty:
356
+ return "[empty]"
357
+ if self.is_entire:
358
+ return "[entire]"
359
+ return f"[{self.lo}, {self.hi}]"
360
+
361
+ def disjoint(self, other):
362
+ if not isinstance(other, Interval):
363
+ return False
364
+ return not self.overlaps(other)
365
+
366
+ def interior_contains(self, x):
367
+ try:
368
+ x = Number(x)
369
+ except:
370
+ return False
371
+ if self.is_empty:
372
+ return False
373
+ return self.lo < x < self.hi
374
+
375
+ def interior(self, other):
376
+ if not isinstance(other, Interval):
377
+ return False
378
+ if self.is_empty:
379
+ return True
380
+ if other.is_empty:
381
+ return False
382
+ if self.is_entire and other.is_entire:
383
+ return True
384
+
385
+ return other.lo < self.lo and self.hi < other.hi
386
+
387
+ def precedes(self, other):
388
+ other = self._coerce(other)
389
+ if self.is_empty or other.is_empty:
390
+ return True
391
+ return self.hi <= other.lo
392
+
393
+ def meets(self, other):
394
+ if not isinstance(other, Interval):
395
+ return False
396
+ if self.is_empty or other.is_empty:
397
+ return False
398
+ return self.hi == other.lo or other.hi == self.lo
399
+
400
+ def __add__(self, other):
401
+ from .arithmetic import add
402
+ other = self._coerce(other)
403
+ return add(self, other)
404
+
405
+ def __sub__(self, other):
406
+ from .arithmetic import sub
407
+ other = self._coerce(other)
408
+ return sub(self, other)
409
+
410
+ def __mul__(self, other):
411
+ from .arithmetic import mul
412
+ other = self._coerce(other)
413
+ return mul(self, other)
414
+
415
+ def __truediv__(self, other):
416
+ from .arithmetic import div
417
+ other = self._coerce(other)
418
+ return div(self, other)
419
+
420
+ def __neg__(self):
421
+ if self.is_empty:
422
+ return Interval.empty()
423
+ return Interval(-self.hi, -self.lo)
424
+
425
+ def __abs__(self):
426
+ if self.is_empty:
427
+ return Interval.empty()
428
+
429
+ if self.lo >= 0:
430
+ return self
431
+ if self.hi <= 0:
432
+ return Interval(-self.hi, -self.lo)
433
+ return Interval(Number(0), Number(max(-self.lo, self.hi)))
434
+
435
+ def __radd__(self, other):
436
+ from .arithmetic import add
437
+ other = self._coerce(other)
438
+ return add(other, self)
439
+
440
+ def __rsub__(self, other):
441
+ from .arithmetic import sub
442
+ other = self._coerce(other)
443
+ return sub(other, self)
444
+
445
+ def __rmul__(self, other):
446
+ from .arithmetic import mul
447
+ other = self._coerce(other)
448
+ return mul(other, self)
449
+
450
+ def __rtruediv__(self, other):
451
+ from .arithmetic import div
452
+ other = self._coerce(other)
453
+ return div(other, self)
454
+
455
+ @property
456
+ def is_strictly_positive(self):
457
+ return not self.is_empty and self.lo > 0
458
+
459
+ @property
460
+ def is_strictly_negative(self):
461
+ return not self.is_empty and self.hi < 0
462
+
463
+ @property
464
+ def is_nonnegative(self):
465
+ return not self.is_empty and self.lo >= 0
466
+
467
+ @property
468
+ def is_nonpositive(self):
469
+ return not self.is_empty and self.hi <= 0
470
+
471
+ @property
472
+ def contains_zero(self):
473
+ return self.contains(0)
474
+
475
+ def __eq__(self, other):
476
+ other = self._coerce(other)
477
+ if self.is_empty and other.is_empty:
478
+ return True
479
+ return self.lo == other.lo and self.hi == other.hi
480
+
481
+ def __lt__(self, other):
482
+ other = self._coerce(other)
483
+ if self.is_empty and other.is_empty:
484
+ return True
485
+ if self.is_empty or other.is_empty:
486
+ return False
487
+ return self.lo <= other.lo and self.hi <= other.hi
488
+
489
+ def __gt__(self, other):
490
+ if not isinstance(other, Interval):
491
+ other = self._coerce(other)
492
+ if self.is_empty or other.is_empty:
493
+ return False
494
+ return self.lo >= other.lo and self.hi >= other.hi
495
+
496
+ def strictly_less_than(self, other):
497
+ other = self._coerce(other)
498
+ if self.is_empty and other.is_empty:
499
+ return True
500
+ if self.is_entire and other.is_entire:
501
+ return True
502
+ if other.is_empty or self.is_empty:
503
+ return False
504
+ return self.lo < other.lo and self.hi < other.hi
505
+
506
+ def bisect(self):
507
+ if self.is_empty:
508
+ return (Interval.empty(), Interval.empty())
509
+
510
+ m = self.midpoint
511
+ return (Interval(self.lo, m), Interval(m, self.hi))
512
+
513
+ def inf_sub(self, other):
514
+ other = self._coerce(other)
515
+ if self.is_empty or other.is_empty:
516
+ return Number('nan')
517
+ with context(get_context()) as ctx:
518
+ ctx.round = RoundDown
519
+ return self.lo - other.lo
520
+
521
+ def sup_sub(self, other):
522
+ other = self._coerce(other)
523
+ if self.is_empty or other.is_empty:
524
+ return Number('nan')
525
+ with context(get_context()) as ctx:
526
+ ctx.round = RoundUp
527
+ return self.hi - other.hi
528
+
529
+ def strictly_precedes(self, other):
530
+ other = self._coerce(other)
531
+ if self.is_empty or other.is_empty:
532
+ return True
533
+ return self.hi < other.lo
534
+
535
+ def __neg__(self):
536
+ if self.is_empty:
537
+ return self
538
+ if self.is_entire:
539
+ return self
540
+ return Interval(-self.hi, -self.lo)
541
+
542
+ def inf(self):
543
+ return self.lo
544
+
545
+ def sup(self):
546
+ return self.hi