Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #530 from qutech/issues/529_from_float
Issues/529 from float
- Loading branch information
Showing
5 changed files
with
233 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
from typing import Tuple, Type | ||
from numbers import Rational | ||
from math import gcd | ||
|
||
|
||
def lcm(a: int, b: int): | ||
"""least common multiple""" | ||
return a * b // gcd(a, b) | ||
|
||
|
||
def _approximate_int(alpha_num: int, d_num: int, den: int) -> Tuple[int, int]: | ||
"""Find the best fraction approximation of alpha_num / den with an error smaller d_num / den. Best means the | ||
fraction with the smallest denominator. | ||
Algorithm from https://link.springer.com/content/pdf/10.1007%2F978-3-540-72914-3.pdf | ||
Args:s | ||
alpha_num: Numerator of number to approximate. 0 < alpha_num < den | ||
d_num: Numerator of allowed absolute error. | ||
den: Denominator of both numbers above. | ||
Returns: | ||
(numerator, denominator) | ||
""" | ||
assert 0 < alpha_num < den | ||
|
||
lower_num = alpha_num - d_num | ||
upper_num = alpha_num + d_num | ||
|
||
p_a, q_a = 0, 1 | ||
p_b, q_b = 1, 1 | ||
|
||
p_full, q_full = p_b, q_b | ||
|
||
to_left = True | ||
|
||
while True: | ||
|
||
# compute the number of steps to the left | ||
x_num = den * p_b - alpha_num * q_b | ||
x_den = -den * p_a + alpha_num * q_a | ||
x = (x_num + x_den - 1) // x_den # ceiling division | ||
|
||
p_full += x * p_a | ||
q_full += x * q_a | ||
|
||
p_prev = p_full - p_a | ||
q_prev = q_full - q_a | ||
|
||
# check whether we have a valid approximation | ||
if (q_full * lower_num < p_full * den < q_full * upper_num or | ||
q_prev * lower_num < p_prev * den < q_prev * upper_num): | ||
bound_num = upper_num if to_left else lower_num | ||
|
||
k_num = den * p_b - bound_num * q_b | ||
k_den = bound_num * q_a - den * p_a | ||
k = (k_num // k_den) + 1 | ||
|
||
return p_b + k * p_a, q_b + k * q_a | ||
|
||
# update the interval | ||
p_a = p_prev | ||
q_a = q_prev | ||
|
||
p_b = p_full | ||
q_b = q_full | ||
|
||
to_left = not to_left | ||
|
||
|
||
def approximate_rational(x: Rational, abs_err: Rational, fraction_type: Type[Rational]) -> Rational: | ||
"""Return the fraction with the smallest denominator in (x - abs_err, x + abs_err)""" | ||
if abs_err <= 0: | ||
raise ValueError('abs_err must be > 0') | ||
|
||
xp, xq = x.numerator, x.denominator | ||
if xq == 1: | ||
return x | ||
|
||
dp, dq = abs_err.numerator, abs_err.denominator | ||
|
||
# separate integer part. alpha_num is guaranteed to be < xq | ||
n, alpha_num = divmod(xp, xq) | ||
|
||
# find common denominator of alpha_num / xq and dp / dq | ||
den = lcm(xq, dq) | ||
alpha_num = alpha_num * den // xq | ||
d_num = dp * den // dq | ||
|
||
if alpha_num < d_num: | ||
p, q = 0, 1 | ||
else: | ||
p, q = _approximate_int(alpha_num, d_num, den) | ||
|
||
return fraction_type(p + n * q, q) | ||
|
||
|
||
def approximate_double(x: float, abs_err: float, fraction_type: Type[Rational]) -> Rational: | ||
"""Return the fraction with the smallest denominator in (x - abs_err, x + abs_err).""" | ||
return approximate_rational(fraction_type(x), fraction_type(abs_err), fraction_type=fraction_type) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import unittest | ||
from typing import Callable, List, Iterator, Tuple | ||
import random | ||
from fractions import Fraction | ||
from collections import deque | ||
from itertools import islice | ||
|
||
from qupulse.utils.numeric import approximate_rational, approximate_double | ||
|
||
|
||
def stern_brocot_sequence() -> Iterator[int]: | ||
sb = deque([1, 1]) | ||
while True: | ||
sb += [sb[0] + sb[1], sb[1]] | ||
yield sb.popleft() | ||
|
||
|
||
def stern_brocot_tree(depth: int) -> List[Fraction]: | ||
"""see wikipedia article""" | ||
fractions = [Fraction(0), Fraction(1)] | ||
|
||
seq = stern_brocot_sequence() | ||
next(seq) | ||
|
||
for n in range(depth): | ||
for _ in range(n + 1): | ||
p = next(seq) | ||
q = next(seq) | ||
fractions.append(Fraction(p, q)) | ||
|
||
return sorted(fractions) | ||
|
||
|
||
def window(iterable, n): | ||
assert n > 0 | ||
it = iter(iterable) | ||
state = deque(islice(it, 0, n - 1), maxlen=n) | ||
for new_element in it: | ||
state.append(new_element) | ||
yield tuple(state) | ||
state.popleft() | ||
|
||
|
||
def uniform_without_bounds(rng, a, b): | ||
result = a | ||
while not a < result < b: | ||
result = rng.uniform(a, b) | ||
return result | ||
|
||
|
||
def generate_test_pairs(depth: int, seed) -> Iterator[Tuple[Tuple[Fraction, Fraction], Fraction]]: | ||
rng = random.Random(seed) | ||
tree = stern_brocot_tree(depth) | ||
extended_tree = [float('-inf')] + tree + [float('inf')] | ||
|
||
# values map to themselves | ||
for a, b, c in window(tree, 3): | ||
err = min(b - a, c - b) | ||
yield (b, err), b | ||
|
||
for prev, a, b, upcom in zip(extended_tree, extended_tree[1:], extended_tree[2:], extended_tree[3:]): | ||
mid = (a + b) / 2 | ||
|
||
low = Fraction(uniform_without_bounds(rng, a, mid)) | ||
err = min(mid - a, low - prev) | ||
yield (low, err), a | ||
|
||
high = Fraction(uniform_without_bounds(rng, mid, b)) | ||
err = min(b - mid, upcom - high) | ||
yield (high, err), b | ||
|
||
|
||
class ApproximationTests(unittest.TestCase): | ||
def test_approximate_rational(self): | ||
"""Use Stern-Brocot tree and rng to generate test cases where we know the result""" | ||
depth = 70 # equivalent to 7457 test cases | ||
test_pairs = list(generate_test_pairs(depth, seed=42)) | ||
|
||
for offset in (-2, -1, 0, 1, 2): | ||
for (x, abs_err), result in test_pairs: | ||
expected = result + offset | ||
result = approximate_rational(x + offset, abs_err, Fraction) | ||
self.assertEqual(expected, result) | ||
|
||
with self.assertRaises(ValueError): | ||
approximate_rational(Fraction(3, 1), Fraction(0, 100), Fraction) | ||
|
||
with self.assertRaises(ValueError): | ||
approximate_rational(Fraction(3, 1), Fraction(-1, 100), Fraction) | ||
|
||
x = Fraction(3, 1) | ||
abs_err = Fraction(1, 100) | ||
self.assertIs(x, approximate_rational(x, abs_err, Fraction)) | ||
|
||
def test_approximate_double(self): | ||
test_values = [ | ||
((.1, .05), Fraction(1, 7)), | ||
((.12, .005), Fraction(2, 17)), | ||
((.15, .005), Fraction(2, 13)), | ||
# .111_111_12, 0.000_000_005 | ||
((.11111112, 0.000000005), Fraction(888890, 8000009)), | ||
((.111125, 0.0000005), Fraction(859, 7730)), | ||
((2.50000000008, .1), Fraction(5, 2)) | ||
] | ||
|
||
for (x, err), expected in test_values: | ||
result = approximate_double(x, err, Fraction) | ||
self.assertEqual(expected, result, msg='{x} ± {err} results in {result} ' | ||
'which is not the expected {expected}'.format(x=x, err=err, | ||
result=result, | ||
expected=expected)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters