Skip to content
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

Setting spherical object and image plane as optimisation target #29

Open
kevin-axai opened this issue Dec 28, 2024 · 4 comments
Open

Setting spherical object and image plane as optimisation target #29

kevin-axai opened this issue Dec 28, 2024 · 4 comments
Labels

Comments

@kevin-axai
Copy link

Hi,
Another that question that popped up in my so far very enjoyable exploring of Optiland is how I might go about setting up a spherical object surface with rays launched into a object-side NA, and then having the lens system optimised such that the target image surface is also spherical, with a given RoC. Essentially three questions:

  • How could one define field points on a curved object plane, is that possible?
  • Setting up rays emitting into the NA cone should be simple enough by setting suitable direction cosines
  • I'm not quite sure about how to optimising e.g. RMS spot size on a curved image plane, is that possible? In ZOS one might use e.g. Target Sag (TGTY) to enforce this.

Appreciate any inputs here!

Thanks,
Kevin

@HarrisonKramer
Copy link
Owner

HarrisonKramer commented Dec 28, 2024

Hi Kevin,

Thanks for the question!

You can define field points on a curved object surface by setting the object radius of curvature via the radius parameter when you add the first surface. The same can be done on the image surface. As an example:

from optiland import optic

lens = optic.Optic()

lens.add_surface(index=0, thickness=100, radius=50.0)
lens.add_surface(index=1, thickness=7, radius=40.0, is_stop=True, material='N-SF2',
                 surface_type='even_asphere', coefficients=[0, 0, 0])
lens.add_surface(index=2, thickness=30.0, radius=-40.0)
lens.add_surface(index=3, radius=-50.0)

lens.set_aperture(aperture_type='objectNA', value=0.1)

lens.set_field_type(field_type='object_height')
lens.add_field(y=0)
lens.add_field(y=25)

lens.add_wavelength(value=0.55, is_primary=True)

lens.draw(num_rays=10)

This results in the following image:
image

Optimization can then be performed as follows:

from optiland import optimization

problem = optimization.OptimizationProblem()

for field in lens.fields.get_field_coords():
    input_data = {'optic': lens, 'surface_number': -1, 'Hx': field[0], 'Hy': field[1],
                  'num_rays': 5, 'wavelength': 0.55, 'distribution': 'hexapolar'}

    problem.add_operand(operand_type='rms_spot_size', target=0, weight=1, input_data=input_data)

problem.add_variable(lens, 'radius', surface_number=1)  # first surface
problem.add_variable(lens, 'radius', surface_number=2)  # second surface
problem.add_variable(lens, 'radius', surface_number=3)  # image surface
problem.add_variable(lens, 'asphere_coeff', surface_number=1, coeff_number=0)
problem.add_variable(lens, 'asphere_coeff', surface_number=1, coeff_number=1)
problem.add_variable(lens, 'asphere_coeff', surface_number=1, coeff_number=2)
problem.add_variable(lens, 'thickness', surface_number=2)  # distance to image plane

optimizer = optimization.OptimizerGeneric(problem)

res = optimizer.optimize(tol=1e-9)

And the resulting lens diagram:
image

I recognize your remark about optimizing the RMS spot size on a curved surface. As it stands, the RMS spot size calculation (in optiland.optimization.operand.ray) only takes into account the (x, y) points of the ray intersection. This would ignore the projection onto a non-planar or tilted surface, so you may want to create a new optimization operand that takes into account the (x, y, z) points instead. One of the tutorials describes how to set up user-defined operands.

If you have another operand that you find useful, consider adding it and submitting a pull request. I'd be happy to have it added to the operand list. I could also put something together if you have an idea of the algorithm. New operands are quite easy to add to the package.

Regards,
Kramer

Edit: typo, change object "plane" to "surface"

@kevin-axai
Copy link
Author

kevin-axai commented Dec 29, 2024 via email

@kevin-axai
Copy link
Author

Hi Kramer,

one question that remains is how I might define a ray bundle that is launched from the curved object surface but in a direction perpendicular to the surface - I'm sure this can be done using the ray class and ray generator and defining the direction cosines. However, it's not quite clear to me how one can trace a custom ray bundle through the system - is it possible to do this and then optimise on e.g. three custom ray bundles for three positions on the curved object surface, emitting into a cone defined by object NA but at a user-defined central angle? Basically, I want to simulate imaging emission from a fibre tip, which is fixed such that the emitting tip moves along a curved trajectory, and emission is along the axis of the fibre. Thanks again!

@HarrisonKramer
Copy link
Owner

Hi Kevin,

You can trace any arbitrary ray bundle through the optical surface. All you need to do is manually define the rays (positions, direction cosines, relative intensities, wavelengths), create an instance of RealRays, then trace them through the system via rays_out = lens.surface_group.trace(rays_in).

Here is a rough (untested) starting point for you. You'll have to fill in some of the logic.

import numpy as np
from optiland.rays import RealRays
from optiland.optimization.operand import operand_registry
from optiland import optimization


def surface_normal(surf, x, y):
    """
    Compute the surface normal at a given point (x, y) on the surface.

    Workaround helper function (not most efficient way to do this...)
    """
    z = surf.geometry.sag(x, y)

    i = np.ones_like(x)  # these values are not used, so we can set them to anything
    rays_tmp = RealRays(x, y, z, i, i, i, i, i)

    nx, ny, nz = surf.geometry.surface_normal(rays_tmp)
    return -nx, -ny, -nz  # return the normal pointing to the right


def generate_rays(nx, ny, nz, theta):
    """
    Define a cone of rays centered around the normal vector
    and into a cone with half-angle theta.
    """
    # insert logic here
    # All components are 1D numpy arrays

    return RealRays(x, y, z, L, M, N, intensity, wavelength)


def new_metric(lens, x, y, theta):
    """
    Compute the metric at a given point (x, y) on the surface.
    """
    nx, ny, nz = surface_normal(surf, x, y)
    rays_in = generate_rays(nx, ny, nz, theta)

    # trace
    rays_out = lens.surface_group.trace(rays_in)

    # get x, y, z on image plane
    x = rays_out.x
    y = rays_out.y
    z = rays_out.z

    # insert logic here e.g., compute the RMS spot size

    return metric  # must return a scalar


operand_registry.register('my_new_metric', new_metric, overwrite=True)

problem = optimization.OptimizationProblem()

input_data = {'lens': lens, 'x': 0, 'y': 0, 'theta': 5.0}
problem.add_operand(operand_type='my_new_metric', target=0, weight=1, input_data=input_data)
# add one operand for each of your (x, y) points

# add variables, or more operands, etc.

# run optimizer
optimizer = optimization.OptimizerGeneric(problem)
res = optimizer.optimize(tol=1e-6)

For reference, I also perform raytracing with custom rays in this example notebook.

Hope that helps.

Regards,
Kramer

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants