Skip to content

Commit 862d9f9

Browse files
Add complex fraction video
1 parent 16faeb4 commit 862d9f9

File tree

3 files changed

+292
-0
lines changed

3 files changed

+292
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ James and his team are available for consulting, contracting, code reviews, and
1212

1313
| N | Code | Video |
1414
| -- | --- |--- |
15+
| 116 | [src](videos/116_complex_fraction) | [Complex (Gaussian) Rationals - Extending Python's Number hierarchy](https://youtu.be/lcm4tYGmAig) |
1516
| 115 | [src](videos/115_fast_pow) | [Fast pow](https://youtu.be/GrNJE6ogyQU) |
1617
| 114 | [src](videos/114_copy_or_no_copy) | [Python Iterators! COPY or NO COPY?](https://youtu.be/hVFKy9Gw95c) |
1718
| 113 | [src](videos/113_getting_rid_of_recursion) | [Getting around the recursion limit](https://youtu.be/1dUpHL5Yg8E) |

videos/116_complex_fraction/cfrac.py

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
from __future__ import annotations
2+
3+
import operator
4+
import sys
5+
import typing
6+
from decimal import Decimal
7+
from fractions import Fraction
8+
from numbers import Complex, Rational
9+
10+
_HASH_M = 2 ** (sys.hash_info.width - 1)
11+
12+
13+
def _fast_pow(x, n: int):
14+
if n == 1: # assume n >= 1
15+
return x
16+
half_n, remainder = divmod(n, 2)
17+
result = _fast_pow(x, half_n)
18+
result *= result
19+
return x * result if remainder else result
20+
21+
22+
def _operator_fallbacks(monomorphic_operator, fallback_operator):
23+
# See https://docs.python.org/3/library/numbers.html
24+
def forward(a, b):
25+
if isinstance(b, (Rational, ComplexFraction)):
26+
return monomorphic_operator(a, b)
27+
elif isinstance(b, (float, complex)):
28+
return fallback_operator(complex(a), b)
29+
else:
30+
return NotImplemented
31+
32+
forward.__name__ = f'__{fallback_operator.__name__}__'
33+
forward.__doc__ = monomorphic_operator.__doc__
34+
35+
def reverse(b, a):
36+
if isinstance(a, (Rational, ComplexFraction)):
37+
return monomorphic_operator(a, b)
38+
elif isinstance(a, Complex):
39+
return fallback_operator(complex(a), complex(b))
40+
else:
41+
return NotImplemented
42+
43+
reverse.__name__ = f'__r{fallback_operator.__name__}__'
44+
reverse.__doc__ = monomorphic_operator.__doc__
45+
46+
return forward, reverse
47+
48+
49+
SupportsFrac = typing.Union[Rational, float, str, Decimal]
50+
51+
52+
class ComplexFraction(Complex):
53+
"""Complex numbers of the form p + qi, where p and q are rational.
54+
55+
Also called Gaussian rationals.
56+
"""
57+
58+
__slots__ = ("_real", "_imag")
59+
60+
def __new__(cls,
61+
real: SupportsFrac = Fraction(0),
62+
imag: SupportsFrac = Fraction(0)):
63+
self = super().__new__(cls)
64+
self._real = Fraction(real)
65+
self._imag = Fraction(imag)
66+
return self
67+
68+
@property
69+
def real(self):
70+
return self._real
71+
72+
@property
73+
def imag(self):
74+
return self._imag
75+
76+
@classmethod
77+
def from_complex(cls, z):
78+
return cls(Fraction.from_float(z.real), Fraction.from_float(z.imag))
79+
80+
def as_fraction_pair(self):
81+
return self.real, self.imag
82+
83+
def __complex__(self):
84+
"""complex(self)"""
85+
return float(self.real) + 1j * float(self.imag)
86+
87+
def __repr__(self):
88+
"""repr(self)"""
89+
return f'{self.__class__.__name__}({self.real!r}, {self.imag!r})'
90+
91+
def __str__(self):
92+
"""str(self)"""
93+
return f'({self.real} + {self.imag}j)'
94+
95+
def _add(self, other):
96+
"""self + other"""
97+
return ComplexFraction(self.real + other.real, self.imag + other.imag)
98+
99+
__add__, __radd__ = _operator_fallbacks(_add, operator.add)
100+
101+
def _sub(self, other):
102+
"""self - other"""
103+
return ComplexFraction(self.real - other.real, self.imag - other.imag)
104+
105+
__sub__, __rsub__ = _operator_fallbacks(_sub, operator.sub)
106+
107+
def _mul(self, other):
108+
"""self * other"""
109+
return ComplexFraction(self.real * other.real - self.imag * other.imag,
110+
self.imag * other.real + self.real * other.imag)
111+
112+
__mul__, __rmul__ = _operator_fallbacks(_mul, operator.mul)
113+
114+
def _truediv(self, other):
115+
"""self / other"""
116+
denominator = other.real * other.real + other.imag * other.imag
117+
return ComplexFraction(
118+
(self.real * other.real + self.imag * other.imag) / denominator,
119+
(self.imag * other.real - self.real * other.imag) / denominator
120+
)
121+
122+
__truediv__, __rtruediv__ = _operator_fallbacks(_truediv, operator.truediv)
123+
124+
def __pow__(self, exponent):
125+
"""self ** exponent"""
126+
if not isinstance(exponent, Rational):
127+
return complex(self) ** exponent
128+
if exponent.denominator != 1: # not an integer exponent
129+
return complex(self) ** complex(exponent)
130+
exponent = exponent.numerator
131+
if exponent == 0:
132+
if self == 0:
133+
raise ValueError("math domain error")
134+
else:
135+
return ComplexFraction(1)
136+
if exponent < 0:
137+
return 1 / (self ** (-exponent))
138+
return _fast_pow(self, exponent)
139+
140+
def __rpow__(self, base):
141+
"""base ** self"""
142+
if self.imag == 0:
143+
return base ** self.real
144+
145+
return base ** complex(self)
146+
147+
def __pos__(self):
148+
"""+self"""
149+
return self
150+
151+
def __neg__(self):
152+
"""-self"""
153+
return ComplexFraction(-self.real, -self.imag)
154+
155+
def __abs__(self):
156+
"""abs(self)"""
157+
if self.imag == 0:
158+
return abs(self.real)
159+
elif self.real == 0:
160+
return abs(self.imag)
161+
return self.norm_squared() ** .5
162+
163+
def norm_squared(self):
164+
"""Square of Euclidean norm"""
165+
return self.real * self.real + self.imag * self.imag
166+
167+
def conjugate(self):
168+
"""p + qi -> p - qi"""
169+
return ComplexFraction(self.real, -self.imag)
170+
171+
def __hash__(self):
172+
"""hash(self)"""
173+
# See https://docs.python.org/3/library/stdtypes.html
174+
hash_value = hash(self.real) + sys.hash_info.imag * hash(self.imag)
175+
hash_value = (hash_value & (_HASH_M - 1)) - (hash_value & _HASH_M)
176+
if hash_value == -1:
177+
hash_value = -2
178+
return hash_value
179+
180+
def __eq__(self, other):
181+
"""self == other"""
182+
if not isinstance(other, Complex):
183+
return NotImplemented
184+
185+
return self.real == other.real and self.imag == other.imag
186+
187+
def __bool__(self):
188+
"""bool(self)"""
189+
return self.real != 0 or self.imag != 0
190+
191+
def __reduce__(self):
192+
return self.__class__, (self._real, self._imag)
193+
194+
def __copy__(self):
195+
if type(self) == ComplexFraction:
196+
return self # immutable
197+
return self.__class__(self._real, self._imag)
198+
199+
def __deepcopy__(self, memo):
200+
if type(self) == ComplexFraction:
201+
return self # immutable components
202+
return self.__class__(self._real, self._imag)
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import math
2+
3+
import pytest
4+
5+
from cfrac import ComplexFraction
6+
from fractions import Fraction
7+
8+
9+
def test_construction():
10+
assert ComplexFraction() == ComplexFraction(0)
11+
assert ComplexFraction() == 0
12+
assert ComplexFraction() == 0.0
13+
assert ComplexFraction(1, 1) == 1 + 1j
14+
assert ComplexFraction.from_complex(1 + 1j) == ComplexFraction(1, 1)
15+
16+
17+
def test_conversions():
18+
assert complex(ComplexFraction(0)) == 0
19+
assert abs(complex(ComplexFraction(1, 2)) - (1 + 2j)) < 1e-6
20+
assert ComplexFraction(1, 2).as_fraction_pair() == (1, 2)
21+
22+
23+
def test_arithmetic():
24+
assert ComplexFraction(1) + ComplexFraction(2) == ComplexFraction(3)
25+
assert ComplexFraction(1, 2) + ComplexFraction(3, 4) == ComplexFraction(4, 6)
26+
assert ComplexFraction(1, 2) - ComplexFraction(3, 4) == ComplexFraction(-2, -2)
27+
assert ComplexFraction(1, 2) * ComplexFraction(3, 4) == ComplexFraction(-5, 10)
28+
assert ComplexFraction(1, 2) / ComplexFraction(3, 4) == ComplexFraction(Fraction(11, 25), Fraction(2, 25))
29+
assert +ComplexFraction(1, 2) == ComplexFraction(1, 2)
30+
assert -ComplexFraction(1, 2) == ComplexFraction(-1, -2)
31+
assert ComplexFraction(1, 2).conjugate() == ComplexFraction(1, -2)
32+
assert ComplexFraction(1, 2).norm_squared() == 5
33+
assert math.isclose(abs(ComplexFraction(1, 2)), math.sqrt(5))
34+
z = ComplexFraction(1)
35+
z += 1j
36+
assert z == ComplexFraction(1, 1)
37+
38+
39+
def test_operator_fallbacks():
40+
assert ComplexFraction(1) + 1 == 2
41+
assert type(ComplexFraction(1) + 1) == ComplexFraction
42+
43+
assert 1 + ComplexFraction(1) == 2
44+
assert type(1 + ComplexFraction(1)) == ComplexFraction
45+
46+
assert abs(ComplexFraction(1) + 1.5 - 2.5) < 1e-6
47+
assert type(ComplexFraction(1) + 1.5) == complex
48+
49+
50+
def test_pow():
51+
assert ComplexFraction(1, 2) ** 0 == 1
52+
assert ComplexFraction(1, 2) ** 5 == ComplexFraction(41, -38)
53+
assert ComplexFraction(1, 2) ** -5 == ComplexFraction(Fraction(41, 3125), Fraction(38, 3125))
54+
assert ComplexFraction(0, 1) ** 1000 == 1
55+
56+
with pytest.raises(ValueError):
57+
ComplexFraction(0) ** 0
58+
59+
60+
def test_hash():
61+
assert hash(ComplexFraction(0)) == hash(0)
62+
assert hash(ComplexFraction(42)) == hash(42)
63+
assert hash(ComplexFraction(Fraction(1, 2))) == hash(Fraction(1, 2))
64+
assert hash(ComplexFraction(0, 1)) == hash(1j)
65+
66+
d = {ComplexFraction(0, 1): "a", ComplexFraction(1, 0): "b", ComplexFraction(1, 1): "c"}
67+
assert d[1j] == "a"
68+
assert d[1] == "b"
69+
assert d[1 + 1j] == "c"
70+
71+
72+
def test_bool():
73+
assert not bool(ComplexFraction(0))
74+
assert bool(ComplexFraction(1))
75+
assert bool(ComplexFraction(0, 1))
76+
77+
78+
def test_comparisons_raise():
79+
with pytest.raises(TypeError):
80+
ComplexFraction() < ComplexFraction()
81+
82+
with pytest.raises(TypeError):
83+
ComplexFraction() <= ComplexFraction()
84+
85+
with pytest.raises(TypeError):
86+
ComplexFraction() > ComplexFraction()
87+
88+
with pytest.raises(TypeError):
89+
ComplexFraction() >= ComplexFraction()

0 commit comments

Comments
 (0)