pythonic-peano-arithmetic 0.3.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.
peano/__init__.py ADDED
@@ -0,0 +1,66 @@
1
+ """Public API from Peano arithmetic through algebraic-root approximation."""
2
+
3
+ from .algebraic_root import (
4
+ AlgebraicRoot,
5
+ RationalInterval,
6
+ algebraic_root,
7
+ )
8
+ from .integer import Z_MINUS_ONE, Z_ONE, Z_ZERO, Integer, integer, n2z
9
+ from .natural_number import N_ONE, N_ZERO, NaturalNumber, natural_number, successor
10
+ from .polynomial import (
11
+ P_ONE,
12
+ P_ZERO,
13
+ Polynomial,
14
+ count_real_roots,
15
+ n2p,
16
+ polynomial,
17
+ r2p,
18
+ sign_variations,
19
+ sturm_sequence,
20
+ z2p,
21
+ )
22
+ from .rational import (
23
+ Q_MINUS_ONE,
24
+ Q_ONE,
25
+ Q_ZERO,
26
+ Rational,
27
+ n2r,
28
+ rational,
29
+ z2r,
30
+ )
31
+ from .utils import config_log
32
+
33
+ __all__ = [
34
+ "NaturalNumber",
35
+ "natural_number",
36
+ "successor",
37
+ "N_ZERO",
38
+ "N_ONE",
39
+ "Integer",
40
+ "integer",
41
+ "n2z",
42
+ "Z_ZERO",
43
+ "Z_ONE",
44
+ "Z_MINUS_ONE",
45
+ "Rational",
46
+ "rational",
47
+ "n2r",
48
+ "z2r",
49
+ "Q_ZERO",
50
+ "Q_ONE",
51
+ "Q_MINUS_ONE",
52
+ "Polynomial",
53
+ "polynomial",
54
+ "n2p",
55
+ "z2p",
56
+ "r2p",
57
+ "P_ZERO",
58
+ "P_ONE",
59
+ "sturm_sequence",
60
+ "sign_variations",
61
+ "count_real_roots",
62
+ "RationalInterval",
63
+ "AlgebraicRoot",
64
+ "algebraic_root",
65
+ "config_log",
66
+ ]
@@ -0,0 +1,180 @@
1
+ """Observe non-rational roots through shrinking rational intervals."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from fractions import Fraction
7
+
8
+ from .natural_number import NaturalNumber
9
+ from .polynomial import Polynomial, count_real_roots
10
+ from .rational import Rational, rational
11
+ from .utils import LogMessage, localized, log
12
+
13
+
14
+ @dataclass(frozen=True, slots=True)
15
+ class RationalInterval:
16
+ """A closed interval with rational endpoints.
17
+
18
+ Equal endpoints are allowed and represent one exact rational point.
19
+ """
20
+
21
+ lower: Rational
22
+ upper: Rational
23
+
24
+ def __post_init__(self) -> None:
25
+ if not isinstance(self.lower, Rational) or not isinstance(self.upper, Rational):
26
+ raise TypeError("interval endpoints must be Rational values")
27
+ lower_fraction = _as_fraction(self.lower)
28
+ upper_fraction = _as_fraction(self.upper)
29
+ if lower_fraction > upper_fraction:
30
+ raise ValueError("lower must be less than or equal to upper")
31
+ object.__setattr__(self, "lower", _from_fraction(lower_fraction))
32
+ object.__setattr__(self, "upper", _from_fraction(upper_fraction))
33
+
34
+ @property
35
+ def width(self) -> Rational:
36
+ return _from_fraction(_as_fraction(self.upper) - _as_fraction(self.lower))
37
+
38
+ @property
39
+ def midpoint(self) -> Rational:
40
+ return _from_fraction((_as_fraction(self.lower) + _as_fraction(self.upper)) / 2)
41
+
42
+ @property
43
+ def is_point(self) -> bool:
44
+ return _as_fraction(self.lower) == _as_fraction(self.upper)
45
+
46
+ def contains(self, value: Rational) -> bool:
47
+ point = _as_fraction(value)
48
+ return _as_fraction(self.lower) <= point <= _as_fraction(self.upper)
49
+
50
+ def __str__(self) -> str:
51
+ return f"[{self.lower}, {self.upper}]"
52
+
53
+
54
+ @dataclass(frozen=True, slots=True, eq=False)
55
+ class AlgebraicRoot:
56
+ """Identify one real polynomial root by an isolating interval.
57
+
58
+ This educational object is not a complete real-number type. It deliberately
59
+ omits arithmetic and general equality and focuses on shrinking rational
60
+ intervals while preserving one root.
61
+ """
62
+
63
+ polynomial: Polynomial
64
+ interval: RationalInterval
65
+
66
+ def __post_init__(self) -> None:
67
+ if not isinstance(self.polynomial, Polynomial):
68
+ raise TypeError("polynomial must be a Polynomial")
69
+ if not isinstance(self.interval, RationalInterval):
70
+ raise TypeError("interval must be a RationalInterval")
71
+ if self.polynomial.degree <= 0:
72
+ raise ValueError("a root-defining polynomial must have positive degree")
73
+ if self.interval.is_point:
74
+ raise ValueError("the initial interval must have positive width")
75
+
76
+ lower_sign = self.polynomial.sign_at(self.interval.lower)
77
+ upper_sign = self.polynomial.sign_at(self.interval.upper)
78
+ if lower_sign == 0 or upper_sign == 0:
79
+ raise ValueError("initial interval endpoints cannot be roots")
80
+ if lower_sign == upper_sign:
81
+ raise ValueError("the polynomial must change sign across the endpoints")
82
+
83
+ number_of_roots = count_real_roots(
84
+ self.polynomial,
85
+ self.interval.lower,
86
+ self.interval.upper,
87
+ )
88
+ if number_of_roots != 1:
89
+ raise ValueError(
90
+ "the initial interval must contain exactly one distinct real root "
91
+ f"(found {number_of_roots})"
92
+ )
93
+
94
+ def approximate(self, steps: int | NaturalNumber) -> RationalInterval:
95
+ """Bisect ``steps`` times and return a closed interval containing the root."""
96
+
97
+ count = _step_count(steps)
98
+ interval = self.interval
99
+ for _ in range(count):
100
+ if interval.is_point:
101
+ break
102
+ interval = _bisect(self.polynomial, interval)
103
+ return interval
104
+
105
+ def trace(self, steps: int | NaturalNumber) -> tuple[RationalInterval, ...]:
106
+ """Return every bisection interval, including the initial interval."""
107
+
108
+ count = _step_count(steps)
109
+ intervals = [self.interval]
110
+ for _ in range(count):
111
+ if intervals[-1].is_point:
112
+ break
113
+ intervals.append(_bisect(self.polynomial, intervals[-1]))
114
+ return tuple(intervals)
115
+
116
+ def __repr__(self) -> str:
117
+ return f"<AlgebraicRoot({self.polynomial!r}, {self.interval})>"
118
+
119
+ def __str__(self) -> str:
120
+ return f"root of {self.polynomial} in {self.interval}"
121
+
122
+
123
+ @log(log_level=41)
124
+ def _bisect(
125
+ polynomial_value: Polynomial,
126
+ interval: RationalInterval,
127
+ ) -> tuple[RationalInterval, LogMessage]:
128
+ midpoint = interval.midpoint
129
+ midpoint_sign = polynomial_value.sign_at(midpoint)
130
+
131
+ if midpoint_sign == 0:
132
+ result = RationalInterval(midpoint, midpoint)
133
+ return (
134
+ result,
135
+ lambda: localized(
136
+ f"{polynomial_value!r}: midpoint {midpoint!r} is a root",
137
+ f"{polynomial_value!r}: 中点 {midpoint!r} は根",
138
+ ),
139
+ )
140
+
141
+ lower_sign = polynomial_value.sign_at(interval.lower)
142
+ if lower_sign != midpoint_sign:
143
+ result = RationalInterval(interval.lower, midpoint)
144
+ else:
145
+ result = RationalInterval(midpoint, interval.upper)
146
+ return (
147
+ result,
148
+ lambda: f"{polynomial_value!r}: {interval} -> {result}",
149
+ )
150
+
151
+
152
+ def algebraic_root(
153
+ polynomial_value: Polynomial,
154
+ lower: tuple[int, int],
155
+ upper: tuple[int, int],
156
+ ) -> AlgebraicRoot:
157
+ """Construct an algebraic root from Python integer endpoint pairs."""
158
+
159
+ return AlgebraicRoot(
160
+ polynomial_value,
161
+ RationalInterval(rational(*lower), rational(*upper)),
162
+ )
163
+
164
+
165
+ def _step_count(value: int | NaturalNumber) -> int:
166
+ if isinstance(value, NaturalNumber):
167
+ return int(value)
168
+ if isinstance(value, bool) or not isinstance(value, int):
169
+ raise TypeError("steps must be an int or NaturalNumber")
170
+ if value < 0:
171
+ raise ValueError("steps must be non-negative")
172
+ return value
173
+
174
+
175
+ def _as_fraction(value: Rational) -> Fraction:
176
+ return Fraction(int(value.p), int(value.q))
177
+
178
+
179
+ def _from_fraction(value: Fraction) -> Rational:
180
+ return rational(value.numerator, value.denominator)
peano/integer.py ADDED
@@ -0,0 +1,345 @@
1
+ """Construct integers as equivalence classes of natural-number differences."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from functools import total_ordering
7
+ from typing import TYPE_CHECKING, cast
8
+
9
+ from .natural_number import N_ONE, N_ZERO, NaturalNumber, natural_number
10
+ from .utils import LogMessage, log
11
+
12
+ if TYPE_CHECKING:
13
+ from .rational import Rational
14
+
15
+
16
+ @total_ordering
17
+ @dataclass(frozen=True, slots=True, eq=False, repr=False)
18
+ class Integer:
19
+ """Represent the difference ``a - b`` by a pair of natural numbers.
20
+
21
+ ``(a, b) ~ (c, d)`` is defined by ``a + d = b + c``. Representations are
22
+ deliberately not normalized automatically, so equivalent representatives
23
+ remain observable.
24
+ """
25
+
26
+ a: NaturalNumber
27
+ b: NaturalNumber
28
+
29
+ def __post_init__(self) -> None:
30
+ if not isinstance(self.a, NaturalNumber) or not isinstance(
31
+ self.b, NaturalNumber
32
+ ):
33
+ raise TypeError("Integer.a and Integer.b must be NaturalNumber values")
34
+
35
+ def __repr__(self) -> str:
36
+ return f"<Z({int(self.a)},{int(self.b)})>"
37
+
38
+ def __str__(self) -> str:
39
+ return str(int(self))
40
+
41
+ def __int__(self) -> int:
42
+ return int(self.a) - int(self.b)
43
+
44
+ def __abs__(self) -> NaturalNumber:
45
+ if self.a >= self.b:
46
+ return self.a - self.b
47
+ return self.b - self.a
48
+
49
+ @log(log_level=11)
50
+ def __eq__(self, other: object) -> tuple[bool, LogMessage]:
51
+ converted = _coerce_integer(other)
52
+ if converted is None:
53
+ return (
54
+ cast(bool, NotImplemented),
55
+ lambda: f"{self!r} == {other!r} = NotImplemented",
56
+ )
57
+ result = self.a + converted.b == self.b + converted.a
58
+ return (
59
+ result,
60
+ lambda: (
61
+ f"{self!r} == {converted!r} ⇔ "
62
+ f"{self.a!r} + {converted.b!r} == "
63
+ f"{self.b!r} + {converted.a!r}"
64
+ ),
65
+ )
66
+
67
+ @log(log_level=12)
68
+ def __lt__(self, other: object) -> tuple[bool, LogMessage]:
69
+ converted = _coerce_integer(other)
70
+ if converted is None:
71
+ return (
72
+ cast(bool, NotImplemented),
73
+ lambda: f"{self!r} < {other!r} = NotImplemented",
74
+ )
75
+ result = self.a + converted.b < self.b + converted.a
76
+ return (
77
+ result,
78
+ lambda: (
79
+ f"{self!r} < {converted!r} ⇔ "
80
+ f"{self.a!r} + {converted.b!r} < "
81
+ f"{self.b!r} + {converted.a!r}"
82
+ ),
83
+ )
84
+
85
+ @log(log_level=12)
86
+ def __le__(self, other: object) -> tuple[bool, LogMessage]:
87
+ converted = _coerce_integer(other)
88
+ if converted is None:
89
+ return (
90
+ cast(bool, NotImplemented),
91
+ lambda: f"{self!r} <= {other!r} = NotImplemented",
92
+ )
93
+ result = self.a + converted.b <= self.b + converted.a
94
+ return (
95
+ result,
96
+ lambda: (
97
+ f"{self!r} <= {converted!r} ⇔ "
98
+ f"{self.a!r} + {converted.b!r} <= "
99
+ f"{self.b!r} + {converted.a!r}"
100
+ ),
101
+ )
102
+
103
+ @log(log_level=14)
104
+ def __add__(self, other: object) -> tuple[Integer, LogMessage]:
105
+ converted = _coerce_integer(other)
106
+ if converted is None:
107
+ return (
108
+ cast(Integer, NotImplemented),
109
+ lambda: f"{self!r} + {other!r} = NotImplemented",
110
+ )
111
+ result = Integer(self.a + converted.a, self.b + converted.b)
112
+ return (
113
+ result,
114
+ lambda: (
115
+ f"{self!r} + {converted!r} = "
116
+ f"({self.a!r} + {converted.a!r}, "
117
+ f"{self.b!r} + {converted.b!r})"
118
+ ),
119
+ )
120
+
121
+ def __radd__(self, other: object) -> Integer:
122
+ return self + other
123
+
124
+ @log(log_level=14)
125
+ def __neg__(self) -> tuple[Integer, LogMessage]:
126
+ return (
127
+ Integer(self.b, self.a),
128
+ lambda: f"-{self!r} = ({self.b!r}, {self.a!r})",
129
+ )
130
+
131
+ @log(log_level=14)
132
+ def __sub__(self, other: object) -> tuple[Integer, LogMessage]:
133
+ converted = _coerce_integer(other)
134
+ if converted is None:
135
+ return (
136
+ cast(Integer, NotImplemented),
137
+ lambda: f"{self!r} - {other!r} = NotImplemented",
138
+ )
139
+ return (
140
+ self + -converted,
141
+ lambda: f"{self!r} - {converted!r} = {self!r} + (-{converted!r})",
142
+ )
143
+
144
+ def __rsub__(self, other: object) -> Integer:
145
+ converted = _coerce_integer(other)
146
+ if converted is None:
147
+ return cast(Integer, NotImplemented)
148
+ return converted - self
149
+
150
+ @log(log_level=15)
151
+ def __mul__(self, other: object) -> tuple[Integer, LogMessage]:
152
+ converted = _coerce_integer(other)
153
+ if converted is None:
154
+ return (
155
+ cast(Integer, NotImplemented),
156
+ lambda: f"{self!r} * {other!r} = NotImplemented",
157
+ )
158
+ result = Integer(
159
+ self.a * converted.a + self.b * converted.b,
160
+ self.a * converted.b + self.b * converted.a,
161
+ )
162
+ return (
163
+ result,
164
+ lambda: (
165
+ f"{self!r} * {converted!r} = "
166
+ f"({self.a!r} * {converted.a!r} + "
167
+ f"{self.b!r} * {converted.b!r}, "
168
+ f"{self.a!r} * {converted.b!r} + "
169
+ f"{self.b!r} * {converted.a!r})"
170
+ ),
171
+ )
172
+
173
+ def __rmul__(self, other: object) -> Integer:
174
+ return self * other
175
+
176
+ @log(log_level=15)
177
+ def __truediv__(self, other: object) -> tuple[Rational, LogMessage]:
178
+ converted = _coerce_integer(other)
179
+ if converted is None:
180
+ return (
181
+ cast("Rational", NotImplemented),
182
+ lambda: f"{self!r} / {other!r} = NotImplemented",
183
+ )
184
+ from .rational import Rational
185
+
186
+ result = Rational(self, converted)
187
+ return result, lambda: f"{self!r} / {converted!r} = {result!r}"
188
+
189
+ def __rtruediv__(self, other: object) -> Rational:
190
+ converted = _coerce_integer(other)
191
+ if converted is None:
192
+ return cast("Rational", NotImplemented)
193
+ return converted / self
194
+
195
+ def _divmod(self, divisor: Integer) -> tuple[Integer, Integer]:
196
+ if not divisor:
197
+ raise ZeroDivisionError("division by zero")
198
+
199
+ quotient_magnitude, remainder_magnitude = divmod(abs(self), abs(divisor))
200
+ same_sign = (self < Z_ZERO) == (divisor < Z_ZERO)
201
+
202
+ if same_sign:
203
+ quotient = Integer(quotient_magnitude, N_ZERO)
204
+ remainder = Integer(remainder_magnitude, N_ZERO)
205
+ elif not remainder_magnitude:
206
+ quotient = Integer(N_ZERO, quotient_magnitude)
207
+ remainder = Z_ZERO
208
+ else:
209
+ quotient = Integer(N_ZERO, quotient_magnitude + N_ONE)
210
+ remainder = Integer(abs(divisor) - remainder_magnitude, N_ZERO)
211
+
212
+ if divisor < Z_ZERO:
213
+ remainder = -remainder
214
+ return quotient, remainder
215
+
216
+ @log(log_level=15)
217
+ def __floordiv__(self, other: object) -> tuple[Integer, LogMessage]:
218
+ converted = _coerce_integer(other)
219
+ if converted is None:
220
+ return (
221
+ cast(Integer, NotImplemented),
222
+ lambda: f"{self!r} // {other!r} = NotImplemented",
223
+ )
224
+ quotient, _ = self._divmod(converted)
225
+ return (
226
+ quotient,
227
+ lambda: (
228
+ f"{self!r} = ({quotient!r}) * {converted!r} + "
229
+ f"({self!r} % {converted!r})"
230
+ ),
231
+ )
232
+
233
+ def __rfloordiv__(self, other: object) -> Integer:
234
+ converted = _coerce_integer(other)
235
+ if converted is None:
236
+ return cast(Integer, NotImplemented)
237
+ return converted // self
238
+
239
+ @log(log_level=15)
240
+ def __mod__(self, other: object) -> tuple[Integer, LogMessage]:
241
+ converted = _coerce_integer(other)
242
+ if converted is None:
243
+ return (
244
+ cast(Integer, NotImplemented),
245
+ lambda: f"{self!r} % {other!r} = NotImplemented",
246
+ )
247
+ _, remainder = self._divmod(converted)
248
+ return (
249
+ remainder,
250
+ lambda: (
251
+ f"{self!r} % {converted!r} = {remainder!r}, "
252
+ f"sign(remainder) = sign({converted!r})"
253
+ ),
254
+ )
255
+
256
+ def __rmod__(self, other: object) -> Integer:
257
+ converted = _coerce_integer(other)
258
+ if converted is None:
259
+ return cast(Integer, NotImplemented)
260
+ return converted % self
261
+
262
+ def __divmod__(self, other: object) -> tuple[Integer, Integer]:
263
+ converted = _coerce_integer(other)
264
+ if converted is None:
265
+ return cast(tuple[Integer, Integer], NotImplemented)
266
+ return self._divmod(converted)
267
+
268
+ def __rdivmod__(self, other: object) -> tuple[Integer, Integer]:
269
+ converted = _coerce_integer(other)
270
+ if converted is None:
271
+ return cast(tuple[Integer, Integer], NotImplemented)
272
+ return converted._divmod(self)
273
+
274
+ @log(log_level=16)
275
+ def __pow__(self, exponent: object) -> tuple[Integer, LogMessage]:
276
+ if not isinstance(exponent, NaturalNumber):
277
+ return (
278
+ cast(Integer, NotImplemented),
279
+ lambda: f"{self!r} ** {exponent!r} = NotImplemented",
280
+ )
281
+ if exponent == N_ZERO:
282
+ return Z_ONE, lambda: f"{self!r} ** {exponent!r} = {Z_ONE!r}"
283
+ return (
284
+ (self ** (exponent - N_ONE)) * self,
285
+ lambda: (
286
+ f"{self!r} ** {exponent!r} = "
287
+ f"({self!r} ** ({exponent!r} - {N_ONE!r})) * {self!r}"
288
+ ),
289
+ )
290
+
291
+ def __bool__(self) -> bool:
292
+ return self.a != self.b
293
+
294
+ def __hash__(self) -> int:
295
+ return hash(int(self))
296
+
297
+ def __pos__(self) -> Integer:
298
+ return self
299
+
300
+ def normalize(self) -> Integer:
301
+ """Return the representative ``(n, 0)`` or ``(0, n)``."""
302
+
303
+ if self.a >= self.b:
304
+ return Integer(self.a - self.b, N_ZERO)
305
+ return Integer(N_ZERO, self.b - self.a)
306
+
307
+
308
+ def integer(value: int) -> Integer:
309
+ """Construct the canonical difference representation of a Python integer."""
310
+
311
+ if isinstance(value, bool) or not isinstance(value, int):
312
+ raise TypeError("only int values can be converted to Integer")
313
+ if value >= 0:
314
+ return Integer(natural_number(value), N_ZERO)
315
+ return Integer(N_ZERO, natural_number(-value))
316
+
317
+
318
+ def n2z(value: NaturalNumber) -> Integer:
319
+ """Embed a natural number into the integers."""
320
+
321
+ if not isinstance(value, NaturalNumber):
322
+ raise TypeError("n2z expects a NaturalNumber")
323
+ return Integer(value, N_ZERO)
324
+
325
+
326
+ def _coerce_integer(value: object) -> Integer | None:
327
+ if isinstance(value, NaturalNumber):
328
+ return n2z(value)
329
+ if isinstance(value, Integer):
330
+ return value
331
+ return None
332
+
333
+
334
+ def cast2z(value: object) -> Integer:
335
+ """Coerce a ``NaturalNumber`` or ``Integer`` to ``Integer``."""
336
+
337
+ converted = _coerce_integer(value)
338
+ if converted is None:
339
+ raise TypeError(f"{value!r} is not an Integer")
340
+ return converted
341
+
342
+
343
+ Z_ZERO = Integer(N_ZERO, N_ZERO)
344
+ Z_ONE = Integer(N_ONE, N_ZERO)
345
+ Z_MINUS_ONE = Integer(N_ZERO, N_ONE)