-
Notifications
You must be signed in to change notification settings - Fork 36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[NUMERICAL] VS Algorithm causes lots of overflow / underflow for high degrees #263
Comments
I am using arch Linux and installed it as:
|
Thanks for filing! It is making me think about the trade-offs of using the (faster) VS Algorithm vs. using the (standard) de Casteljau algorithm. (A related issue cropped up in #156, fixed in 5768824 by using a The primary issue with the implementation (the VS Algorithm) is that it is computing large powers of the input ( I'm still digging into the specifics of how / where this fails, but I'll likely implement a fix that switches from the VS Algorithm to the de Casteljau algorithm if the degree exceeds a fixed number (probably 54). The implementation computes binomial coefficients iteratively via: binom_val = (binom_val * (degree - index + 1)) / index However, this approach has 4 types of accuracy issues:
Using the script below, the following binomial coefficients are the earliest such instances of a problem for a 64-bit floating point value (
Script to determine these values: import fractions
import numpy as np
def computed_exactly(n):
b_np = np.float64(1.0)
b_fr = fractions.Fraction(1)
for k in range(1, n):
b_np = (b_np * (n + 1 - k)) / k
b_fr = (b_fr * (n + 1 - k)) / k
if b_fr.denominator != 1:
raise ValueError("Non-integer", b_fr)
if b_np != b_fr:
return n, k, b_np, b_fr.numerator
return None
def represented_exactly(n):
b_fr = fractions.Fraction(1)
for k in range(1, n):
b_fr = (b_fr * (n + 1 - k)) / k
if b_fr.denominator != 1:
raise ValueError("Non-integer", b_fr)
b_np = np.float64(b_fr.numerator)
if b_np != b_fr:
return n, k, b_np, b_fr.numerator
return None
def computed_overflow_int32(n):
b_np = np.int32(1)
b_fr = fractions.Fraction(1)
for k in range(1, n):
b_fr = (b_fr * (n + 1 - k)) / k
if b_fr.denominator != 1:
raise ValueError("Non-integer", b_fr)
try:
b_np = (b_np * np.int32(n + 1 - k)) // np.int32(k)
except FloatingPointError as exc:
if exc.args != ("overflow encountered in int_scalars",):
raise
machine_limits_int32 = np.iinfo(np.int32)
work_too_hard = np.int64(b_np) * np.int64(n + 1 - k)
above = work_too_hard - machine_limits_int32.max - 1
if not (0 < above < machine_limits_int32.max):
raise ValueError("Overflow too large")
overflow_numerator = above + machine_limits_int32.min
overflow_value = overflow_numerator // np.int64(k)
return n, k, overflow_value, b_fr.numerator
if not isinstance(b_np, np.int32):
raise TypeError("Non-int32", b_np)
if b_np != b_fr:
return n, k, b_np, b_fr.numerator
return None
def represented_overflow_int32(n):
b_fr = fractions.Fraction(1)
for k in range(1, n):
b_fr = (b_fr * (n + 1 - k)) / k
if b_fr.denominator != 1:
raise ValueError("Non-integer", b_fr)
b_np = np.int32(b_fr.numerator)
if b_np != b_fr:
return n, k, b_np, b_fr.numerator
return None
def computed_overflow(n):
b_np = np.float64(1.0)
for k in range(1, n):
try:
b_np = (b_np * (n + 1 - k)) / k
except FloatingPointError as exc:
if exc.args != ("overflow encountered in double_scalars",):
raise
return n, k
if np.isinf(b_np):
return n, k
return None
def represented_overflow(n):
b_fr = fractions.Fraction(1)
for k in range(1, n):
b_fr = (b_fr * (n + 1 - k)) / k
if b_fr.denominator != 1:
raise ValueError("Non-integer", b_fr)
try:
b_np = np.float64(b_fr.numerator)
except OverflowError as exc:
if exc.args != ("int too large to convert to float",):
raise
return n, k
if np.isinf(b_np):
return n, k
return None
def main():
np.seterr(all="raise")
# Find the first instance where `n C k` cannot be computed exactly as a
# float64.
for n in range(1, 128):
mismatch = computed_exactly(n)
if mismatch is None:
continue
n, k, numerical, exact = mismatch
print(
"Inexact computation (float64): "
f"{n} C {k} = {exact} != {numerical}"
)
break
# Find the first instance where `n C k` cannot be represented exactly as a
# float64.
for n in range(1, 128):
mismatch = represented_exactly(n)
if mismatch is None:
continue
n, k, numerical, exact = mismatch
print(
"Inexact representation (float64): "
f"{n} C {k} = {exact} != {numerical}"
)
break
# Find the first instance where computing `n C k` overflows as an int32.
for n in range(1, 128):
mismatch = computed_overflow_int32(n)
if mismatch is None:
continue
n, k, numerical, exact = mismatch
print(
"Overflow in computation (int32): "
f"{n} C {k} = {exact} != {numerical}"
)
break
# Find the first instance where `n C k` overflows as an int32.
for n in range(1, 128):
mismatch = represented_overflow_int32(n)
if mismatch is None:
continue
n, k, numerical, exact = mismatch
print(
"Overflow in representation (int32): "
f"{n} C {k} = {exact} != {numerical}"
)
break
# Find the first instance where computing `n C k` overflows as a float64.
for n in range(1, 2048):
mismatch = computed_overflow(n)
if mismatch is None:
continue
n, k = mismatch
print(f"Overflow in computation (float64): {n} C {k}")
break
# Find the first instance where `n C k` overflows as a float64.
for n in range(1, 2048):
mismatch = represented_overflow(n)
if mismatch is None:
continue
n, k = mismatch
print(f"Overflow in representation (float64): {n} C {k}")
break
if __name__ == "__main__":
main() |
As I dig a wee bit more, note that the 4 warnings you mentioned only show up on the FIRST instance of the warning, but if we convert all NumPy warnings to exceptions via
UPDATES: (I'll post the instrumented code at some point).
|
@ravipra Another note (unrelated to my findings above), you can vectorize and massively speed up the computation. Instead of def f(ind):
s_val = ind/(len(inds)-1)
return curve.evaluate(s_val).tolist()[1][0]
return list(map(f, inds)) create an array of the s_values = np.linspace(0.0, 1.0, len(inds))
evaluated = curve.evaluate_multi(s_values) |
This is the implementation I'll use. It ports quite nicely to Fortran and attempts to "optimize" the computation by using contiguous memory. def de_casteljau(nodes, s_values):
dimension, columns = nodes.shape
(num_s,) = s_values.shape
degree = columns - 1
# TODO: Determine if it's possible to do broadcast multiply in Fortran
# order below to avoid allocating all of `lambda{1,2}`.
lambda1 = np.empty((dimension, num_s, columns - 1), order="F")
lambda2 = np.empty((dimension, num_s, columns - 1), order="F")
workspace = np.empty((dimension, num_s, columns - 1), order="F")
lambda1_1d = 1.0 - s_values # Probably not worth allocating this
for i in range(num_s):
lambda1[:, i, :] = lambda1_1d[i]
lambda2[:, i, :] = s_values[i]
workspace[:, i, :] = (
lambda1_1d[i] * nodes[:, :degree] + s_values[i] * nodes[:, 1:]
)
for i in range(degree - 1, 0, -1):
workspace[:, :, :i] = (
lambda1[:, :, :i] * workspace[:, :, :i]
+ lambda2[:, :, :i] * workspace[:, :, 1 : (i + 1)]
)
# NOTE: This returns an array with `evaluated.flags.owndata` false, though
# it is Fortran contiguous.
return workspace[:, :, 0] |
@ravipra Here is the "fix" in action with your inputs: In [1]: import bezier
In [2]: import numpy as np
In [3]: with open("./issue-263/data.txt", "r") as file_obj:
...: raw_data = file_obj.read()
...:
In [4]: data_lines = raw_data.strip().split("\n")
In [5]: values = list(map(int, data_lines))
In [6]: indicies = list(range(len(values)))
In [7]: nodes = np.asfortranarray([indicies, values])
In [8]: curve = bezier.Curve.from_nodes(nodes)
In [9]: curve
Out[9]: <Curve (degree=1021, dimension=2)>
In [10]: s_values = np.linspace(0.0, 1.0, len(indicies))
In [11]: evaluated = curve.evaluate_multi(s_values)
In [12]: evaluated.max()
Out[12]: 6650.24031866094
In [13]: evaluated.min()
Out[13]: -2374.7425879401285
In [14]: evaluated
Out[14]:
array([[ 0.00000000e+00, 1.00000000e+00, 2.00000000e+00, ...,
1.01900000e+03, 1.02000000e+03, 1.02100000e+03],
[-3.00000000e+00, 3.72447859e+03, 5.40495424e+03, ...,
4.41543898e+02, 4.33333153e+02, 4.32000000e+02]])
In [15]: bezier.__version__
Out[15]: '2021.2.13.dev1' but also note that the de Casteljau algorithm is much less efficient here In [16]: %time _ = bezier.hazmat.curve_helpers.evaluate_multi_vs(nodes, 1.0 - s_values, s_values)
.../site-packages/bezier/hazmat/curve_helpers.py:259: RuntimeWarning: overflow encountered in multiply
result += binom_val * lambda2_pow * nodes[:, [index]]
.../site-packages/bezier/hazmat/curve_helpers.py:260: RuntimeWarning: invalid value encountered in multiply
result *= lambda1
.../site-packages/bezier/hazmat/curve_helpers.py:259: RuntimeWarning: invalid value encountered in multiply
result += binom_val * lambda2_pow * nodes[:, [index]]
.../site-packages/bezier/hazmat/curve_helpers.py:259: RuntimeWarning: invalid value encountered in add
result += binom_val * lambda2_pow * nodes[:, [index]]
CPU times: user 37.7 ms, sys: 2.68 ms, total: 40.4 ms
Wall time: 38.7 ms
In [17]: %time _ = bezier.hazmat.curve_helpers.evaluate_multi_de_casteljau(nodes, 1.0 - s_values, s_values)
CPU times: user 5.17 s, sys: 1.51 s, total: 6.69 s
Wall time: 6.7 s |
Hi,
Thank you for the very useful package.
Been able to use it for the past one year successfully. But getting "invalid value encountered" runtime warnings today.
The errors are:
The code I am using is:
Attaching the data file (data.txt) that I am using.
Could you please let me know what I am doing wrong?
Thank you.
Ravi
data.txt
The text was updated successfully, but these errors were encountered: