Add optika.zernikes and ZernikeSag#172
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## fix/polygonal-aperture-bounds #172 +/- ##
=================================================================
+ Coverage 99.35% 99.37% +0.02%
=================================================================
Files 116 120 +4
Lines 6003 6277 +274
=================================================================
+ Hits 5964 6238 +274
Misses 39 39
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| plt.legend() | ||
| """ | ||
|
|
||
| sag: None | AbstractSag = None |
There was a problem hiding this comment.
Kinda unfortunate this is called sag as well, if you refer to it, it has stutter: sag.sag. Is there a better word like base or sag_base or something that captures it without using the exact same word?
There was a problem hiding this comment.
Good call — renamed the attribute to base, so it now reads sag.base. Done in 8455e8d.
| plt.figure() | ||
| na.plt.plot(position.y, z, axis="y", label="perturbed") | ||
| na.plt.plot(position.y, z_base, axis="y", label="base") | ||
| plt.legend() |
There was a problem hiding this comment.
I don't see any difference between the two curves in this plot.
There was a problem hiding this comment.
You're right, and it was a real bug in the example: the perturbation is coma (Noll index 8), which varies as cos(phi) and is therefore identically zero along the y axis — exactly the slice the plot used, so the curves landed on top of each other. Switched the slice to vary along x, where the coma term is nonzero (~2.8 mm peak difference). Fixed in 8455e8d.
|
|
||
| def zernike( | ||
| j: int, | ||
| position: na.AbstractCartesian2dVectorArray, |
There was a problem hiding this comment.
I feel like position should be first, is it standard for this function to put j first?
There was a problem hiding this comment.
Agreed — there's no strong convention for j-first, and putting position first matches the other field-evaluation functions. Flipped both zernike() and zernike_gradient() to (position, j) and updated the call sites. Done in 8455e8d.
1759e2b to
9479771
Compare
4f496ec to
8455e8d
Compare
9479771 to
9fd2c2e
Compare
AbstractPolygonalAperture.bound_lower/bound_upper were computed from the raw vertices, while __call__ and wire() apply the aperture's internal transformation. For apertures with a nonzero internal transformation (e.g. a decentered grating aperture), any consumer of the bounding box (stratified wavefield sampling, sensor pixel grids, pupil normalization) saw the wrong region of the surface. EllipticalAperture had the related defect of transforming only the bounding-box corner points, which is incorrect under rotation; its bounds are now derived from the wire, matching CircularSectorAperture. RectangularAperture.__call__ expressed the rectangle in terms of bound_lower/bound_upper while also inverse-transforming the position, which double-counted the internal transformation once the bounds included it; it now tests against half_width directly. The generic bound tests now assert that the bounds enclose the wire and that the wire centroid is accepted by active, non-inverted apertures (nominal values, with a small tolerance for interpolation round-off, since uncertain parameters may be redrawn between evaluations). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Address review feedback: deriving EllipticalAperture.bound_lower/upper from wire().min()/max() sampled the boundary at a finite number of points, so the true extent between samples was underestimated whenever the internal transformation rotated the ellipse off the coordinate axes. Replace the wire sampling with a closed form. A boundary point is p(t) = c + a*e_a*cos(t) + b*e_b*sin(t), where c is the transformed center and e_a, e_b are the transformed local axes, so the extent along any world component is sqrt((a*e_a)^2 + (b*e_b)^2). This is exact under rotation and reduces to the axis-aligned (radius.x, radius.y) box when untransformed. CircularAperture bounds are already analytic and rotation-invariant, so they are unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…adients Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…profile Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Rename the ZernikeSag base-profile attribute from `sag` to `base` to avoid the `sag.sag` stutter when referring to it. - Fix the ZernikeSag docstring example: the coma term (Noll index 8) varies as cos(phi) and is identically zero along the y axis, so the original y-slice showed the perturbed and base curves on top of each other. Slice along x instead, where the perturbation is visible. - Put `position` first in the signatures of `zernike()` and `zernike_gradient()`, matching the convention of the other field-evaluation functions; update call sites and tests accordingly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
8455e8d to
ed9a198
Compare
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Summary
Stacked on #171 (will retarget to
mainonce it merges; the branches are independent in content).optika.zernikes: Noll-indexed, RMS-normalized Zernike polynomials (noll(),zernike()) and their analytic gradients (zernike_gradient()), matching the convention of :cite:t:Noll1976(reference added torefs.bib). The gradient uses an origin-safe series forR_n^m(rho)/rho, so it is exact at the center of the pupil.optika.sags.ZernikeSag: a sag profile that perturbs a base sag with a sum of Zernike polynomials, for modeling measured or analytic figure error. Elementiofcoefficientsis the Noll-j = i + 1coefficient over the disk of the givenradius. The analyticnormal()combines the base gradient with the Zernike gradients; the ray intercept falls back to the inherited numeric solver.Because the figure error perturbs the surface, the same object is seen consistently by both the geometric raytrace and (in an upcoming PR) the physical-optics propagation - a single source of truth for tolerancing.
Tests
noll()against the published index table; closed forms for j = 1, 2, 3, 4, 11.ZernikeSagthrough the standardAbstractTestAbstractSagbattery, plus a geometric check that Noll Z4 shifts the focus of a paraboloid by the analytic amount.🤖 Generated with Claude Code