Skip to content

Commit 65ab0a2

Browse files
Feat/optimization algorithms (#62)
* fix: improve formatting in optimization.py * feat: add SHGO optimizer to optimization module * feat: implement SHGO optimizer in optimization module * feat: add BasinHopping optimizer to optimization module * feat: add BasinHopping optimizer to optimization module * fix: optimize update method to conditionally call update_paraxial * docs: update tutorial to include notes on SHGO and BasinHopping optimizers * docs: update tutorial to include SHGO and basin-hopping in global optimization examples * docs: improve formatting of docstring in EvenAsphere class * docs: add SHGO optimization example to gallery documentation * docs: update optimization gallery to include basin-hopping and improve descriptions * docs: update optimization framework documentation to include global and local optimizers
1 parent 191deb6 commit 65ab0a2

File tree

10 files changed

+754
-16
lines changed

10 files changed

+754
-16
lines changed

docs/developers_guide/optimization_framework.rst

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,12 @@ Components Explained
3030
- A base `Optimizer` class wraps `scipy.optimize.minimize` and provides a unified interface.
3131
- Built-in optimizers include:
3232

33-
- **Dual Annealing**
34-
- **Differential Evolution**
35-
- **Least Squares** (and more)
33+
- **Dual Annealing** (global)
34+
- **Differential Evolution** (global)
35+
- **Basin Hopping** (global)
36+
- **SHGO** (global)
37+
- **Least Squares** (local)
38+
- **Nelder-Mead**, **Powell**, **BFGS**, **L-BFGS-B**, **COBYLA**, etc. (local optimization, from `scipy.optimize.minimize`)
3639
- Users can subclass the base optimizer for custom methods.
3740

3841
3. **Operands and Variables**:

docs/examples/Tutorial_5b_Advanced_Optimization.ipynb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"\n",
2727
"- Various operand types (paraxial, real ray-based, aberrations)\n",
2828
"- Various variable types (radii, thickness, conic)\n",
29-
"- Global optimization (e.g. dual annealing or differential evolution)"
29+
"- Global optimization (e.g. dual annealing, differential evolution, SHGO, basin-hopping)"
3030
]
3131
},
3232
{
@@ -224,6 +224,9 @@
224224
"outputs": [],
225225
"source": [
226226
"optimizer = optimization.DifferentialEvolution(problem)\n",
227+
"# optimizer = optimization.SHGO(problem) # note SHGO requires bounds\n",
228+
"# optimizer = optimization.BasinHopping(problem) # note BasinHopping requires no bounds\n",
229+
"\n",
227230
"res = optimizer.optimize(maxiter=256, disp=False, workers=-1)"
228231
]
229232
},

docs/gallery/optimization.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ These examples demonstrate the optimization capabilities of optical systems usin
1313
optimization/constrained
1414
optimization/bounded_operands
1515
optimization/global
16+
optimization/basin_hopping
17+
optimization/shgo
1618
optimization/asphere
1719
optimization/beam_expander
1820
optimization/freeform

docs/gallery/optimization/basin_hopping.ipynb

Lines changed: 307 additions & 0 deletions
Large diffs are not rendered by default.

docs/gallery/optimization/global.ipynb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"cell_type": "markdown",
55
"metadata": {},
66
"source": [
7-
"# Global Optimization"
7+
"# Global Optimization (Differential Evolution)"
88
]
99
},
1010
{
@@ -14,6 +14,8 @@
1414
"The following global optimizers are implemented in Optiland:\n",
1515
"1. Differential Evolution\n",
1616
"2. Dual Annealing\n",
17+
"3. SHGO\n",
18+
"4. Basin-hopping\n",
1719
"\n",
1820
"These optimizers wrap the scipy.optimize implementations."
1921
]

docs/gallery/optimization/shgo.ipynb

Lines changed: 310 additions & 0 deletions
Large diffs are not rendered by default.

optiland/geometries/even_asphere.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
- k is the conic constant
1212
- Ci are the aspheric coefficients
1313
14-
Even-order aspheric surfaces are commonly used to correct specific aberrations
15-
while maintaining rotational symmetry. These surfaces are defined by polynomial
16-
terms with even exponents, ensuring symmetry about the optical axis.
14+
Even-order aspheric surfaces are commonly used to correct specific aberrations
15+
while maintaining rotational symmetry. These surfaces are defined by polynomial
16+
terms with even exponents, ensuring symmetry about the optical axis.
1717
1818
Kramer Harrison, 2024
1919
"""
@@ -34,9 +34,10 @@ class EvenAsphere(NewtonRaphsonGeometry):
3434
- k is the conic constant
3535
- Ci are the aspheric coefficients
3636
37-
Even-order aspheric surfaces are commonly used to correct specific aberrations
38-
while maintaining rotational symmetry. These surfaces are defined by polynomial
39-
terms with even exponents, ensuring symmetry about the optical axis.
37+
Even-order aspheric surfaces are commonly used to correct specific
38+
aberrations while maintaining rotational symmetry. These surfaces are
39+
defined by polynomial terms with even exponents, ensuring symmetry about
40+
the optical axis.
4041
4142
Args:
4243
coordinate_system (str): The coordinate system used for the geometry.

optiland/optic.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -417,13 +417,16 @@ def update_normalization(self, surface) -> None:
417417
if surface.surface_type == 'zernike':
418418
surface.geometry.norm_radius = surface.semi_aperture*1.1
419419

420-
def update(self)->None:
420+
def update(self) -> None:
421421
"""
422422
Update the surfaces based on the pickup operations.
423423
"""
424424
self.pickups.apply()
425425
self.solves.apply()
426-
self.update_paraxial()
426+
427+
if any(surface.surface_type in ['chebyshev', 'zernike']
428+
for surface in self.surface_group.surfaces):
429+
self.update_paraxial()
427430

428431
def image_solve(self):
429432
"""Update the image position such that the marginal ray crosses the

optiland/optimization/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,7 @@
2222
OptimizerGeneric,
2323
LeastSquares,
2424
DualAnnealing,
25-
DifferentialEvolution
25+
DifferentialEvolution,
26+
SHGO,
27+
BasinHopping
2628
)

optiland/optimization/optimization.py

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ def undo(self):
212212
var.update(x0[idvar])
213213
self._x.pop(-1)
214214

215-
def _fun(self, x)->float:
215+
def _fun(self, x) -> float:
216216
"""
217217
Internal function to evaluate the objective function.
218218
@@ -231,7 +231,7 @@ def _fun(self, x)->float:
231231
self.problem.update_optics()
232232

233233
# Compute merit function value
234-
try:
234+
try:
235235
rss = self.problem.sum_squared()
236236
if np.isnan(rss):
237237
return 1e10
@@ -405,3 +405,108 @@ def optimize(self, maxiter=1000, disp=True, workers=-1):
405405
updating=updating,
406406
workers=workers)
407407
return result
408+
409+
410+
class SHGO(OptimizerGeneric):
411+
"""
412+
Simplicity Homology Global Optimization (SHGO).
413+
414+
Args:
415+
problem (OptimizationProblem): The optimization problem to be solved.
416+
417+
Methods:
418+
optimize(workers=-1, *args, **kwargs): Runs the SHGO algorithm.
419+
"""
420+
421+
def __init__(self, problem: OptimizationProblem):
422+
"""
423+
Initializes a new instance of the SHGO class.
424+
425+
Args:
426+
problem (OptimizationProblem): The optimization problem to be
427+
solved.
428+
"""
429+
super().__init__(problem)
430+
431+
def optimize(self, workers=-1, *args, **kwargs):
432+
"""
433+
Runs the SHGO algorithm. Note that the SHGO algorithm accepts the same
434+
arguments as the scipy.optimize.shgo function.
435+
436+
Args:
437+
workers (int): Number of parallel workers to use. Set to -1 to use
438+
all available CPU processors. Default is -1.
439+
*args: Variable length argument list.
440+
**kwargs: Arbitrary keyword arguments.
441+
442+
Returns:
443+
result (OptimizeResult): The optimization result.
444+
445+
Raises:
446+
ValueError: If any variable in the problem does not have bounds.
447+
"""
448+
x0 = [var.value for var in self.problem.variables]
449+
self._x.append(x0)
450+
bounds = tuple([var.bounds for var in self.problem.variables])
451+
if any(None in bound for bound in bounds):
452+
raise ValueError('SHGO requires all variables have bounds.')
453+
454+
with warnings.catch_warnings():
455+
warnings.simplefilter("ignore", category=RuntimeWarning)
456+
457+
result = optimize.shgo(self._fun, bounds=bounds,
458+
workers=workers, *args, **kwargs)
459+
return result
460+
461+
462+
class BasinHopping(OptimizerGeneric):
463+
"""
464+
Basin-hopping optimizer for solving optimization problems.
465+
466+
Args:
467+
problem (OptimizationProblem): The optimization problem to be solved.
468+
469+
Methods:
470+
optimize(maxiter=1000, disp=True, workers=-1): Runs the basin-hopping
471+
optimization algorithm.
472+
"""
473+
474+
def __init__(self, problem: OptimizationProblem):
475+
"""
476+
Initializes a new instance of the BasinHopping class.
477+
478+
Args:
479+
problem (OptimizationProblem): The optimization problem to be
480+
solved.
481+
"""
482+
super().__init__(problem)
483+
484+
def optimize(self, niter=100, *args, **kwargs):
485+
"""
486+
Runs the basin-hopping algorithm. Note that the basin-hopping
487+
algorithm accepts the same arguments as the
488+
scipy.optimize.basinhopping function.
489+
490+
Args:
491+
niter (int): Number of iterations to perform. Default is 100.
492+
*args: Variable length argument list.
493+
**kwargs: Arbitrary keyword arguments.
494+
495+
Returns:
496+
result (OptimizeResult): The optimization result.
497+
498+
Raises:
499+
ValueError: If any variable in the problem does not have bounds.
500+
"""
501+
x0 = [var.value for var in self.problem.variables]
502+
self._x.append(x0)
503+
bounds = tuple([var.bounds for var in self.problem.variables])
504+
if not all(x is None for pair in bounds for x in pair):
505+
raise ValueError('Basin-hopping does not accept bounds.')
506+
507+
with warnings.catch_warnings():
508+
warnings.simplefilter("ignore", category=RuntimeWarning)
509+
510+
result = optimize.basinhopping(self._fun, x0=x0,
511+
niter=niter, *args, **kwargs)
512+
return result

0 commit comments

Comments
 (0)