-
Notifications
You must be signed in to change notification settings - Fork 107
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
Zernike enhancements #1327
base: main
Are you sure you want to change the base?
Zernike enhancements #1327
Changes from 3 commits
1553081
9dff874
65f072f
b3f8aba
24f9bcc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -876,6 +876,21 @@ def test_dz_val(): | |||||
atol=2e-13, rtol=0 | ||||||
) | ||||||
|
||||||
zk_coefs = dz.xycoef(*uv_vector) | ||||||
for zk, c in zip(zk_list, zk_coefs): | ||||||
# Zk may have trailing zeros... | ||||||
ncoef = len(c) | ||||||
np.testing.assert_allclose( | ||||||
zk.coef[:ncoef], | ||||||
c, | ||||||
atol=2e-13, rtol=0 | ||||||
) | ||||||
for i, (u, v) in enumerate(zip(*uv_vector)): | ||||||
np.testing.assert_equal( | ||||||
zk_coefs[i], | ||||||
dz.xycoef(u, v) | ||||||
) | ||||||
|
||||||
# Check asserts | ||||||
with assert_raises(AssertionError): | ||||||
dz(0.0, [1.0]) | ||||||
|
@@ -1553,5 +1568,56 @@ def test_dz_mean(): | |||||
) | ||||||
|
||||||
|
||||||
def test_large_j(run_slow): | ||||||
# The analytic form for an annular Zernike of the form (n, m) = (n, n) or (n, -n) | ||||||
# is: | ||||||
# r^n sincos(n theta) sqrt((2n+2) / sum_i=0^n-1 eps^(2i)) | ||||||
# where sincos is sin if n is even and cos if n is odd, and eps = R_inner/R_outer. | ||||||
|
||||||
rng = galsim.BaseDeviate(1234).as_numpy_generator() | ||||||
x = rng.uniform(-1.0, 1.0, size=100) | ||||||
y = rng.uniform(-1.0, 1.0, size=100) | ||||||
|
||||||
R_outer = 1.0 | ||||||
R_inner = 0.5 | ||||||
eps = R_inner/R_outer | ||||||
|
||||||
test_vals = [ | ||||||
(10, 1e-12), # Z66 | ||||||
(20, 1e-12), # Z231 | ||||||
(40, 1e-9), # Z861 | ||||||
] | ||||||
if run_slow: | ||||||
test_vals += [ | ||||||
(60, 1e-6), # Z1891 | ||||||
(80, 1e-3), # Z3321 | ||||||
# (100, 10), # Z5151 # This one is catastrophic failure! | ||||||
] | ||||||
|
||||||
print() | ||||||
for n, tol in test_vals: | ||||||
j = np.sum(np.arange(n+2)) | ||||||
n, m = galsim.zernike.noll_to_zern(j) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Somewhat confusing reuse of variable |
||||||
print(f"Z{j} => (n, m) = ({n}, {n})") | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Probably followed by |
||||||
coefs = np.zeros(j+1) | ||||||
coefs[j] = 1.0 | ||||||
zk = Zernike(coefs, R_outer=R_outer, R_inner=R_inner) | ||||||
|
||||||
def analytic_zk(x, y): | ||||||
r = np.hypot(x, y) | ||||||
theta = np.arctan2(y, x) | ||||||
factor = np.sqrt((2*n+2) / np.sum([eps**(2*i) for i in range(n+1)])) | ||||||
if m > 0: | ||||||
return r**n * np.cos(n*theta) * factor | ||||||
else: | ||||||
return r**n * np.sin(n*theta) * factor | ||||||
|
||||||
np.testing.assert_allclose( | ||||||
zk(x, y), | ||||||
analytic_zk(x, y), | ||||||
atol=tol, rtol=tol | ||||||
) | ||||||
|
||||||
|
||||||
if __name__ == "__main__": | ||||||
runtests(__file__) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we expect this to converge particularly well when r>1, right? So should probably limit this to have
x**2 + y**2 < 1
.When I tried it, I was able to use significantly lower tolerances:
(10, 1.e-12)
(20, 1.e-12)
(40, 1.e-10)
(60, 1.e-8)
(80, 1.e-6)
(100, 2.e-3)
So it is still becoming less accurate as j get very large. But not as catastrophically bad as this test implies.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ones that are least accurate are always ones with r close to 1. Like 0.95 or so. This is probably a clue about where the recursion is going unstable. I might play around with the recursion formula to see if I can harden it up at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, limiting to 0.5 < r < 1.0 might even be appropriate. Though, OTOH, these are just 2d polynomials. They're orthogonal over an annulus but defined everywhere.
The particular polynomials here are proportional to r^n, so that might explain why the large r are trickier. Other values of j will presumably produce polynomials with larger amplitudes near r=0.
In case it's helpful while you're looking at recursions, here's the closed-form table I got out of Mathematica up to Z28:
![Screenshot 2025-01-24 at 6 26 12 AM](https://private-user-images.githubusercontent.com/3650485/406473002-6cfeab7b-9b92-4908-aaa7-d651e8599c4b.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzkyNjQ2NzgsIm5iZiI6MTczOTI2NDM3OCwicGF0aCI6Ii8zNjUwNDg1LzQwNjQ3MzAwMi02Y2ZlYWI3Yi05YjkyLTQ5MDgtYWFhNy1kNjUxZTg1OTljNGIucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI1MDIxMSUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNTAyMTFUMDg1OTM4WiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9YTExOGE3MzM5ZmMxMmRmM2M0NTdlN2M5NTA0OGUyYTk2ZWZhMmY2ZjI0YTk3Y2NiNDY4YWZmNjk0N2YwZTNlZSZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.1bJlrtjoZ_vF1rSkG1nhX2TFFJDWF92RAEQq9mQ9f50)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the problem is the conversion of the coef array from rrsq format to xy format. The xy version has lots of very large coefficients with alternating sign. The native rho/rhosq version is much more stable numerically.
If I change evalCartesian to:
then all the tests pass at 1.e-12 tolerance, except Z5151, which needed 1.e-11. (It's also massively faster than the current implementation -- I guess the rrsq_to_xy function must be a slow step?)
I don't know if this means we should actually make the above change though. There might be other downsides (e.g. not getting to use the C++ layer horner2d implementation, at least without a little more work). But I suppose we could at least provide another method that would evaluate this way for users who want more accuracy at very high Zernike order.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting. I sped up rrsq_to_xy significantly by rewriting it as a cached recursive function. Now the tradeoff seems to be roughly:
The xy array is about 2x the effort to compute as the rrsq array, which makes sense since the xy is derived from the rrsq. However, most of this effort is cached so I don't think there's a significant difference after the initial Zernike constructions.
The xy approach (so c++ horner) is about 10x faster when there are ~100 evaluation points. However, on my machine (M3 Pro) the complex-python approach actually runs faster when the number of evaluation points is ~10_000 or more (!). Maybe this is because the rrsq array is ~2x smaller than the xy array?
Given the improved accuracy and the above, I tentatively vote make the rrsq approach the default. I want to benchmark some donut fitting first though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm.... Maybe the horner step depends on jmax too. I just ran some donut image forward models (jmax ~65 and npoints ~10000) and they were ~4x faster using the xy approach. So maybe need to leave that as an expert option.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Either way is fine with me. As long as there is a public API way to get the calculation that is accurate for high order.
I think most use cases won't care about the accuracy as much as the speed. But it seems important to have a way to get the accurate one.