Custom Variable Scaling
While default variable scaling is often sufficient for many applications, users may wish to customize this for their optimization problems. This entails explicitly specifying the scaling behavior of a variable during optimization. Recall that scaling is applied to variables during optimization in order to bring the variables into approximately common range in order to improve convergence and optimizer performance.
[1]:
import numpy as np
from optiland import optic, optimization
from optiland.optimization.scaling import LinearScaler, ReciprocalScaler
Define a starting lens:
[2]:
lens = optic.Optic()
# add surfaces
lens.surfaces.add(index=0, thickness=np.inf)
lens.surfaces.add(index=1, thickness=7, radius=1000, material="N-SF11", is_stop=True)
lens.surfaces.add(index=2, thickness=30, radius=-1000, surface_type='even_asphere') # Second lens surface is an even asphere
lens.surfaces.add(index=3)
# set aperture
lens.set_aperture(aperture_type="EPD", value=15)
# add field
lens.fields.set_type(field_type="angle")
lens.fields.add(y=0)
# add wavelength
lens.wavelengths.add(value=0.55, is_primary=True)
# draw lens
_ = lens.draw(num_rays=5)
Define optimization problem:
[3]:
problem = optimization.OptimizationProblem()
Add operands (targets for optimization):
[4]:
"""
Define RMS spot size properties for the optimization:
1. Surface number = -1, implying last surface (image surface)
2. Normalized field coordinates (Hx, Hy) = (0, 0)
3. Number of rays = 5, corresponds to number of rings in hexapolar distribution
(see distribution documentation)
4. Wavelength = 0.55 µm
5. Pupil distribution = hexapolar
"""
input_data = {
"optic": lens,
"surface_number": -1,
"Hx": 0,
"Hy": 0,
"num_rays": 5,
"wavelength": 0.55,
"distribution": "hexapolar",
}
# add RMS spot size operand
problem.add_operand(
operand_type="rms_spot_size",
target=0,
weight=1,
input_data=input_data,
)
Define Variables with Custom Scaling
Here, we will define three variables for the optimizer. Instead of using the default scaling for all of them, we will specify custom scaling strategies for each variable. Note that the default scaling for each variable type will suffice for most problems, but customization may be helpful for certain problems.
Surface 1 - Reciprocal Radius: We use the
radiusvariable type with theReciprocalScalerscaler. This is often superior to a standard radius for optimization because it handles the transition from a convex to a concave surface (passing through infinity, or a flat surface) smoothly. For the optimizer, the variable value simply passes through zero, which is much more stable. Note that thereciprocal_radiusvariable itself also implements this strategy, but we build it manually to demonstrate the functionality.Surface 2 - Custom Linear Scaled Radius: For the second surface, we’ll still vary the radius, but we’ll override the default
LinearScalerwith our own parameters. This demonstrates how you can tune the scaling to a specific range you expect the variable to occupy.Surface 2 - Aspheric Coefficient: Aspheric surfaces provide more degrees of freedom to correct aberrations. However, their coefficients are often tiny (e.g.,
1e-3). We will vary the 2nd-order coefficient (A_2), which uses thePowerScalerby default to bring its value into a range the optimizer can handle effectively.
[5]:
# 1. Add the radius of surface 1 as a reciprocal variable
reciprocal_scaler = ReciprocalScaler() # Use the ReciprocalScaler for this variable
problem.add_variable(lens, "radius", surface_number=1, scaler=reciprocal_scaler, min_val=50.0)
# 2. Add the radius of surface 2, overriding the default linear scaling
linear_scaler = LinearScaler(factor=1/200.0, offset=0) # Scale relative to 200mm
problem.add_variable(optic=lens, variable_type="radius", surface_number=2, scaler=linear_scaler)
# 3. Add the aspheric coefficient on surface 2.
# This will use its own default, a PowerScaler.
problem.add_variable(
optic=lens,
variable_type="asphere_coeff",
surface_number=2,
coeff_number=0,
)
Check initial merit function value and system properties:
[6]:
problem.info()
╒════╤════════════════════════╤═══════════════════╕
│ │ Merit Function Value │ Improvement (%) │
╞════╪════════════════════════╪═══════════════════╡
│ 0 │ 30.0924 │ 0 │
╘════╧════════════════════════╧═══════════════════╛
╒════╤════════════════╤══════════╤══════════════╤══════════════╤══════════╤═════════╤═════════╤════════════════╕
│ │ Operand Type │ Target │ Min. Bound │ Max. Bound │ Weight │ Value │ Delta │ Contrib. [%] │
╞════╪════════════════╪══════════╪══════════════╪══════════════╪══════════╪═════════╪═════════╪════════════════╡
│ 0 │ rms spot size │ 0 │ │ │ 1 │ 5.486 │ 5.486 │ 100 │
╘════╧════════════════╧══════════╧══════════════╧══════════════╧══════════╧═════════╧═════════╧════════════════╛
╒════╤═════════════════╤═══════════╤═════════╤══════════════╤══════════════╕
│ │ Variable Type │ Surface │ Value │ Min. Bound │ Max. Bound │
╞════╪═════════════════╪═══════════╪═════════╪══════════════╪══════════════╡
│ 0 │ radius │ 1 │ 1000 │ 50 │ │
│ 1 │ radius │ 2 │ -1000 │ nan │ │
│ 2 │ asphere_coeff │ 2 │ 0 │ nan │ │
╘════╧═════════════════╧═══════════╧═════════╧══════════════╧══════════════╛
Define optimizer:
[7]:
optimizer = optimization.OptimizerGeneric(problem)
Run optimization:
[8]:
optimizer.optimize(tol=1e-9)
[8]:
message: CONVERGENCE: RELATIVE REDUCTION OF F <= FACTR*EPSMCH
success: True
status: 0
fun: 0.007266126782113237
x: [ 2.000e-02 -2.786e-01 -4.714e+00]
nit: 6
jac: [-7.485e-01 3.044e-05 3.128e-05]
nfev: 84
njev: 21
hess_inv: <3x3 LbfgsInvHessProduct with dtype=float64>
Print merit function value and system properties after optimization:
[9]:
problem.info()
╒════╤════════════════════════╤═══════════════════╕
│ │ Merit Function Value │ Improvement (%) │
╞════╪════════════════════════╪═══════════════════╡
│ 0 │ 0.00726613 │ 99.9759 │
╘════╧════════════════════════╧═══════════════════╛
╒════╤════════════════╤══════════╤══════════════╤══════════════╤══════════╤═════════╤═════════╤════════════════╕
│ │ Operand Type │ Target │ Min. Bound │ Max. Bound │ Weight │ Value │ Delta │ Contrib. [%] │
╞════╪════════════════╪══════════╪══════════════╪══════════════╪══════════╪═════════╪═════════╪════════════════╡
│ 0 │ rms spot size │ 0 │ │ │ 1 │ 0.085 │ 0.085 │ 100 │
╘════╧════════════════╧══════════╧══════════════╧══════════════╧══════════╧═════════╧═════════╧════════════════╛
╒════╤═════════════════╤═══════════╤═══════════════╤══════════════╤══════════════╕
│ │ Variable Type │ Surface │ Value │ Min. Bound │ Max. Bound │
╞════╪═════════════════╪═══════════╪═══════════════╪══════════════╪══════════════╡
│ 0 │ radius │ 1 │ 50 │ 50 │ │
│ 1 │ radius │ 2 │ -55.7112 │ nan │ │
│ 2 │ asphere_coeff │ 2 │ -0.000471431 │ nan │ │
╘════╧═════════════════╧═══════════╧═══════════════╧══════════════╧══════════════╛
Draw final lens:
[10]:
_ = lens.draw(num_rays=12)