Coverage for /usr/lib/python3/dist-packages/sympy/external/pythonmpq.py: 32%
184 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1"""
2PythonMPQ: Rational number type based on Python integers.
4This class is intended as a pure Python fallback for when gmpy2 is not
5installed. If gmpy2 is installed then its mpq type will be used instead. The
6mpq type is around 20x faster. We could just use the stdlib Fraction class
7here but that is slower:
9 from fractions import Fraction
10 from sympy.external.pythonmpq import PythonMPQ
11 nums = range(1000)
12 dens = range(5, 1005)
13 rats = [Fraction(n, d) for n, d in zip(nums, dens)]
14 sum(rats) # <--- 24 milliseconds
15 rats = [PythonMPQ(n, d) for n, d in zip(nums, dens)]
16 sum(rats) # <--- 7 milliseconds
18Both mpq and Fraction have some awkward features like the behaviour of
19division with // and %:
21 >>> from fractions import Fraction
22 >>> Fraction(2, 3) % Fraction(1, 4)
23 1/6
25For the QQ domain we do not want this behaviour because there should be no
26remainder when dividing rational numbers. SymPy does not make use of this
27aspect of mpq when gmpy2 is installed. Since this class is a fallback for that
28case we do not bother implementing e.g. __mod__ so that we can be sure we
29are not using it when gmpy2 is installed either.
30"""
33import operator
34from math import gcd
35from decimal import Decimal
36from fractions import Fraction
37import sys
38from typing import Tuple as tTuple, Type
41# Used for __hash__
42_PyHASH_MODULUS = sys.hash_info.modulus
43_PyHASH_INF = sys.hash_info.inf
46class PythonMPQ:
47 """Rational number implementation that is intended to be compatible with
48 gmpy2's mpq.
50 Also slightly faster than fractions.Fraction.
52 PythonMPQ should be treated as immutable although no effort is made to
53 prevent mutation (since that might slow down calculations).
54 """
55 __slots__ = ('numerator', 'denominator')
57 def __new__(cls, numerator, denominator=None):
58 """Construct PythonMPQ with gcd computation and checks"""
59 if denominator is not None:
60 #
61 # PythonMPQ(n, d): require n and d to be int and d != 0
62 #
63 if isinstance(numerator, int) and isinstance(denominator, int):
64 # This is the slow part:
65 divisor = gcd(numerator, denominator)
66 numerator //= divisor
67 denominator //= divisor
68 return cls._new_check(numerator, denominator)
69 else:
70 #
71 # PythonMPQ(q)
72 #
73 # Here q can be PythonMPQ, int, Decimal, float, Fraction or str
74 #
75 if isinstance(numerator, int):
76 return cls._new(numerator, 1)
77 elif isinstance(numerator, PythonMPQ):
78 return cls._new(numerator.numerator, numerator.denominator)
80 # Let Fraction handle Decimal/float conversion and str parsing
81 if isinstance(numerator, (Decimal, float, str)):
82 numerator = Fraction(numerator)
83 if isinstance(numerator, Fraction):
84 return cls._new(numerator.numerator, numerator.denominator)
85 #
86 # Reject everything else. This is more strict than mpq which allows
87 # things like mpq(Fraction, Fraction) or mpq(Decimal, any). The mpq
88 # behaviour is somewhat inconsistent so we choose to accept only a
89 # more strict subset of what mpq allows.
90 #
91 raise TypeError("PythonMPQ() requires numeric or string argument")
93 @classmethod
94 def _new_check(cls, numerator, denominator):
95 """Construct PythonMPQ, check divide by zero and canonicalize signs"""
96 if not denominator:
97 raise ZeroDivisionError(f'Zero divisor {numerator}/{denominator}')
98 elif denominator < 0:
99 numerator = -numerator
100 denominator = -denominator
101 return cls._new(numerator, denominator)
103 @classmethod
104 def _new(cls, numerator, denominator):
105 """Construct PythonMPQ efficiently (no checks)"""
106 obj = super().__new__(cls)
107 obj.numerator = numerator
108 obj.denominator = denominator
109 return obj
111 def __int__(self):
112 """Convert to int (truncates towards zero)"""
113 p, q = self.numerator, self.denominator
114 if p < 0:
115 return -(-p//q)
116 return p//q
118 def __float__(self):
119 """Convert to float (approximately)"""
120 return self.numerator / self.denominator
122 def __bool__(self):
123 """True/False if nonzero/zero"""
124 return bool(self.numerator)
126 def __eq__(self, other):
127 """Compare equal with PythonMPQ, int, float, Decimal or Fraction"""
128 if isinstance(other, PythonMPQ):
129 return (self.numerator == other.numerator
130 and self.denominator == other.denominator)
131 elif isinstance(other, self._compatible_types):
132 return self.__eq__(PythonMPQ(other))
133 else:
134 return NotImplemented
136 def __hash__(self):
137 """hash - same as mpq/Fraction"""
138 try:
139 dinv = pow(self.denominator, -1, _PyHASH_MODULUS)
140 except ValueError:
141 hash_ = _PyHASH_INF
142 else:
143 hash_ = hash(hash(abs(self.numerator)) * dinv)
144 result = hash_ if self.numerator >= 0 else -hash_
145 return -2 if result == -1 else result
147 def __reduce__(self):
148 """Deconstruct for pickling"""
149 return type(self), (self.numerator, self.denominator)
151 def __str__(self):
152 """Convert to string"""
153 if self.denominator != 1:
154 return f"{self.numerator}/{self.denominator}"
155 else:
156 return f"{self.numerator}"
158 def __repr__(self):
159 """Convert to string"""
160 return f"MPQ({self.numerator},{self.denominator})"
162 def _cmp(self, other, op):
163 """Helper for lt/le/gt/ge"""
164 if not isinstance(other, self._compatible_types):
165 return NotImplemented
166 lhs = self.numerator * other.denominator
167 rhs = other.numerator * self.denominator
168 return op(lhs, rhs)
170 def __lt__(self, other):
171 """self < other"""
172 return self._cmp(other, operator.lt)
174 def __le__(self, other):
175 """self <= other"""
176 return self._cmp(other, operator.le)
178 def __gt__(self, other):
179 """self > other"""
180 return self._cmp(other, operator.gt)
182 def __ge__(self, other):
183 """self >= other"""
184 return self._cmp(other, operator.ge)
186 def __abs__(self):
187 """abs(q)"""
188 return self._new(abs(self.numerator), self.denominator)
190 def __pos__(self):
191 """+q"""
192 return self
194 def __neg__(self):
195 """-q"""
196 return self._new(-self.numerator, self.denominator)
198 def __add__(self, other):
199 """q1 + q2"""
200 if isinstance(other, PythonMPQ):
201 #
202 # This is much faster than the naive method used in the stdlib
203 # fractions module. Not sure where this method comes from
204 # though...
205 #
206 # Compare timings for something like:
207 # nums = range(1000)
208 # rats = [PythonMPQ(n, d) for n, d in zip(nums[:-5], nums[5:])]
209 # sum(rats) # <-- time this
210 #
211 ap, aq = self.numerator, self.denominator
212 bp, bq = other.numerator, other.denominator
213 g = gcd(aq, bq)
214 if g == 1:
215 p = ap*bq + aq*bp
216 q = bq*aq
217 else:
218 q1, q2 = aq//g, bq//g
219 p, q = ap*q2 + bp*q1, q1*q2
220 g2 = gcd(p, g)
221 p, q = (p // g2), q * (g // g2)
223 elif isinstance(other, int):
224 p = self.numerator + self.denominator * other
225 q = self.denominator
226 else:
227 return NotImplemented
229 return self._new(p, q)
231 def __radd__(self, other):
232 """z1 + q2"""
233 if isinstance(other, int):
234 p = self.numerator + self.denominator * other
235 q = self.denominator
236 return self._new(p, q)
237 else:
238 return NotImplemented
240 def __sub__(self ,other):
241 """q1 - q2"""
242 if isinstance(other, PythonMPQ):
243 ap, aq = self.numerator, self.denominator
244 bp, bq = other.numerator, other.denominator
245 g = gcd(aq, bq)
246 if g == 1:
247 p = ap*bq - aq*bp
248 q = bq*aq
249 else:
250 q1, q2 = aq//g, bq//g
251 p, q = ap*q2 - bp*q1, q1*q2
252 g2 = gcd(p, g)
253 p, q = (p // g2), q * (g // g2)
254 elif isinstance(other, int):
255 p = self.numerator - self.denominator*other
256 q = self.denominator
257 else:
258 return NotImplemented
260 return self._new(p, q)
262 def __rsub__(self, other):
263 """z1 - q2"""
264 if isinstance(other, int):
265 p = self.denominator * other - self.numerator
266 q = self.denominator
267 return self._new(p, q)
268 else:
269 return NotImplemented
271 def __mul__(self, other):
272 """q1 * q2"""
273 if isinstance(other, PythonMPQ):
274 ap, aq = self.numerator, self.denominator
275 bp, bq = other.numerator, other.denominator
276 x1 = gcd(ap, bq)
277 x2 = gcd(bp, aq)
278 p, q = ((ap//x1)*(bp//x2), (aq//x2)*(bq//x1))
279 elif isinstance(other, int):
280 x = gcd(other, self.denominator)
281 p = self.numerator*(other//x)
282 q = self.denominator//x
283 else:
284 return NotImplemented
286 return self._new(p, q)
288 def __rmul__(self, other):
289 """z1 * q2"""
290 if isinstance(other, int):
291 x = gcd(self.denominator, other)
292 p = self.numerator*(other//x)
293 q = self.denominator//x
294 return self._new(p, q)
295 else:
296 return NotImplemented
298 def __pow__(self, exp):
299 """q ** z"""
300 p, q = self.numerator, self.denominator
302 if exp < 0:
303 p, q, exp = q, p, -exp
305 return self._new_check(p**exp, q**exp)
307 def __truediv__(self, other):
308 """q1 / q2"""
309 if isinstance(other, PythonMPQ):
310 ap, aq = self.numerator, self.denominator
311 bp, bq = other.numerator, other.denominator
312 x1 = gcd(ap, bp)
313 x2 = gcd(bq, aq)
314 p, q = ((ap//x1)*(bq//x2), (aq//x2)*(bp//x1))
315 elif isinstance(other, int):
316 x = gcd(other, self.numerator)
317 p = self.numerator//x
318 q = self.denominator*(other//x)
319 else:
320 return NotImplemented
322 return self._new_check(p, q)
324 def __rtruediv__(self, other):
325 """z / q"""
326 if isinstance(other, int):
327 x = gcd(self.numerator, other)
328 p = self.denominator*(other//x)
329 q = self.numerator//x
330 return self._new_check(p, q)
331 else:
332 return NotImplemented
334 _compatible_types: tTuple[Type, ...] = ()
336#
337# These are the types that PythonMPQ will interoperate with for operations
338# and comparisons such as ==, + etc. We define this down here so that we can
339# include PythonMPQ in the list as well.
340#
341PythonMPQ._compatible_types = (PythonMPQ, int, Decimal, Fraction)