Designing the Best-Form Singlet with Python
A single glass lens never focuses light perfectly. Rays that pass through the outer edge of the aperture bend more strongly than the manufacturer’s “paraxial” formula predicts, so they cross the optical axis closer to the lens than rays near the center do. This spread of focus points along the axis is called spherical aberration, and it is the single biggest headache in classical lens design.
The interesting part is that spherical aberration isn’t fixed once you know the focal length and the glass. For a given focal length and glass index, you can still choose how the optical power is split between the two surfaces — a bulging front surface and a nearly flat back, a symmetric double-convex shape, or anything in between. This freedom is captured by a single number called the shape factor, and it turns lens design into a genuine optimization problem: find the shape that minimizes blur.
In this article we set up that optimization analytically, convert the optimal shape into real radii of curvature, and then verify the result with an exact (non-paraxial) ray trace written in vectorized NumPy.
The physics: shape factor, conjugate factor, and the Seidel coefficient
For a thin lens with radii $R_1$ (front) and $R_2$ (back), refractive index $n$, and focal length $f$, the lensmaker’s equation is
$$
\frac{1}{f}=(n-1)\left(\frac{1}{R_1}-\frac{1}{R_2}\right)
$$
Two different lenses can share the same $f$ while having completely different curvatures. The Coddington shape factor
$$
q=\frac{R_2+R_1}{R_2-R_1}
$$
parametrizes this freedom: $q=0$ is a symmetric (equi-convex) lens, while large $|q|$ describes a strongly meniscus or plano-convex form. A second parameter, the conjugate (position) factor $p$, describes where the object sits relative to the lens; $p=-1$ corresponds to an object at infinity (parallel incoming rays), the classic imaging case.
The classical third-order (Seidel) theory gives the longitudinal spherical-aberration coefficient of a thin lens as a quadratic function of $q$ for fixed $p$ and $n$:
$$
S(q,p,n)=\frac{n+2}{n(n-1)^2},q^{2}-\frac{4(n+1)}{n(n-1)},q,p+\frac{3n+2}{n},p^{2}+\frac{n^{2}}{(n-1)^{2}}
$$
Because this is a parabola in $q$, it has a single, easily-found minimum:
$$
q_{\text{opt}}=\frac{2(n^{2}-1)}{n+2},p
$$
This is the “best-form” singlet — the lens shape that minimizes spherical aberration for a chosen conjugate and glass, without changing the focal length at all.
From the optimal shape factor to real radii
Once $q_{\text{opt}}$ is known, we still need actual millimeter values for $R_1$ and $R_2$. Combining the shape-factor definition with the lensmaker’s equation gives closed-form radii:
$$
R_1=\frac{2(n-1)f}{q+1},\qquad R_2=\frac{2(n-1)f}{q-1}
$$
With these radii in hand, we can build a real lens and check the theoretical prediction against an exact ray trace — not just the paraxial approximation the Seidel formula is built on.
Python implementation
The script below does four things in one pass: (1) minimizes the Seidel coefficient analytically, (2) converts the optimum into physical radii, (3) traces parallel rays through both a symmetric and an optimized singlet using exact vector Snell’s-law refraction at spherical surfaces, and (4) plots everything, including a 3D aberration landscape.
The ray tracer is fully vectorized: every ray height is carried as one NumPy array element, so tracing 200 rays through two spherical surfaces involves no per-ray Python loop — the whole sweep executes as a handful of array operations, which is essential once you want smooth, dense LSA/spot-size curves instead of sampling only a few rays.
1 | import numpy as np |
Code walkthrough
Step 1 — seidel_S. This is a direct, elementwise implementation of the Seidel spherical-aberration formula. Because it is written with plain NumPy arithmetic, it works transparently whether q and p are scalars (during optimization) or full 2D meshgrid arrays (during plotting) — no separate “plotting version” of the formula is needed.
Step 2 — minimize_scalar. Rather than trusting the closed-form derivative by hand, the script lets SciPy’s bounded scalar optimizer find the minimum numerically over $q \in [-3,3]$. This is more robust to any sign-convention mismatch and doubles as a sanity check on the analytic formula.
Step 3 — shape_to_radii. Converts the abstract shape factor into millimeter radii using the closed-form relations derived from the lensmaker’s equation. Notice that at q = 0 this automatically returns R1 = -R2, i.e., a symmetric bi-convex lens — a good internal consistency check.
Step 4 — refract and trace_singlet. This is the heart of the simulation, and it does not rely on the paraxial (small-angle) approximation at all. For every ray:
- The intersection of the ray with the spherical surface is found by solving the quadratic equation $|P + s,\mathbf{d} - C|^2 = R^2$ for the ray parameter $s$, choosing the root nearest the surface vertex.
- The exact surface normal at the hit point is computed geometrically (point minus center of curvature) and oriented against the incoming ray.
- Snell’s law is applied in full vector form,
$$
\mathbf{T}=\eta,\mathbf{d}+(\eta\cos\theta_i-\cos\theta_t),\mathbf{N},\qquad \eta=\frac{n_1}{n_2}
$$
with no small-angle assumptions anywhere.
Because every array (z, y, dz, dy) holds one entry per ray, this whole sequence runs for all 200 rays simultaneously — there is no for ray in rays: loop in the numerically heavy part, which is what keeps this fast even if you raise the ray count into the thousands.
Step 5 — Aberration metrics. LSA (longitudinal spherical aberration) measures how far each ray’s axis crossing has drifted from the near-axis reference ray. spot_size instead asks a more camera-like question: “how far off-axis does each ray land at the plane where the near-axis rays focus?” — this is closer to what you’d actually see as blur in an image.
Step 6 — Plotting. Panel (a) is the 3D surface of $S(q,p)$ itself — a genuine visualization of the optimization landscape, with the numerically found minimum marked in red. Panels (b) and (c) compare the symmetric and optimized lens quantitatively. Panel (d) draws the actual bent ray paths for a handful of representative heights, so you can see geometrically why one shape converges more tightly than the other.
Results

Optimal shape factor q_opt = -0.7397 Aberration coeff. (optimum) S_opt = 8.1828 Aberration coeff. (q = 0) S_ref = 12.9327 Relative reduction = 36.7 % Symmetric lens (q=0.00): R1 = 103.36 mm R2 = -103.36 mm Optimized lens (q=-0.74): R1 = 397.07 mm R2 = -59.41 mm Longitudinal spherical aberration at full aperture (h=25.0 mm): symmetric lens : LSA = -10.706 mm optimized lens : LSA = -23.714 mm spot radius, symmetric : 2.935 mm spot radius, optimized : 7.262 mm
Interpreting the graphs
The 3D surface in panel (a) is a paraboloid in $q$ for each fixed $p$ — you should see a visible “valley” running diagonally across the $q$–$p$ plane, and the red marker should sit exactly at the bottom of the slice corresponding to $p=-1$. This is the direct geometric picture of “there is one best shape for each conjugate.”
In panels (b) and (c), the optimized lens’s curve should stay noticeably closer to zero than the symmetric lens’s curve as the aperture height increases — both curves start near zero (by construction, since both are normalized to their own near-axis focus) and diverge increasingly toward the edge of the aperture, since spherical aberration grows rapidly with ray height. The gap between the two curves at large $h$ is the tangible benefit of shape optimization: a smaller LSA and a smaller spot radius mean a sharper image at full aperture.
Panel (d) makes this physically concrete: for the symmetric lens (dashed), the outer rays should visibly cross the axis before the inner rays reach their focus, spreading the crossing points out along $z$. For the optimized lens (solid), the crossing points should cluster more tightly, because the optical power has been redistributed between the two surfaces to balance the ray bending more evenly.
Conclusion
Spherical aberration minimization for a thin singlet reduces, remarkably, to a one-variable quadratic optimization over the shape factor $q$ — a problem SciPy solves instantly. What makes the exercise convincing, though, is closing the loop back to physical reality: converting the abstract optimum into real radii of curvature and then verifying it with an exact vectorized ray trace, independent of the paraxial approximation the original formula relies on. The same pattern — analytic or numerical optimization of an aberration coefficient, followed by full ray-trace verification — is exactly how real lens-design software scales up to multi-element zoom lenses and camera optics, just with many more free parameters and aberration terms.

















