Source code for optimization.operand.ray

"""Ray Operands Module

This module provides a class that calculates various ray tracing values for an
optical system. It is used in conjunction with the optimization module to
optimize optical systems.

Kramer Harrison, 2024
"""

from __future__ import annotations

import optiland.backend as be
from optiland import wavefront
from optiland.distribution import GaussianQuadrature


[docs] class RayOperand: """A class that provides static methods for performing ray tracing calculations on an optic. Methods: x_intercept: Calculates the x-coordinate of the intercept point on a specific surface. y_intercept: Calculates the y-coordinate of the intercept point on a specific surface. z_intercept: Calculates the z-coordinate of the intercept point on a specific surface. L: Calculates the direction cosine L of the ray on a specific surface. M: Calculates the direction cosine M of the ray on a specific surface. N: Calculates the direction cosine N of the ray on a specific surface. rms_spot_size: Calculates the root mean square (RMS) spot size on a specific surface. OPD_difference: Calculates the optical path difference (OPD) difference for a given ray distribution. """
[docs] @staticmethod def x_intercept(optic, surface_number, Hx, Hy, Px, Py, wavelength): """Calculates the x-coordinate of the intercept point on a specific surface. Args: optic: The optic object. surface_number: The number of the surface. Hx: The normalized x field coordinate. Hy: The normalized y field coordinate. Px: The normalized x pupil coordinate. Py: The normalized y pupil coordinate. wavelength: The wavelength of the ray. Returns: The x-coordinate of the intercept point. """ optic.trace_generic(Hx, Hy, Px, Py, wavelength) return optic.surfaces.x[surface_number, 0]
[docs] @staticmethod def y_intercept(optic, surface_number, Hx, Hy, Px, Py, wavelength): """Calculates the y-coordinate of the intercept point on a specific surface. Args: optic: The optic object. surface_number: The number of the surface. Hx: The normalized x field coordinate. Hy: The normalized y field coordinate. Px: The normalized x pupil coordinate. Py: The normalized y pupil coordinate. wavelength: The wavelength of the ray. Returns: The y-coordinate of the intercept point. """ optic.trace_generic(Hx, Hy, Px, Py, wavelength) return optic.surfaces.y[surface_number, 0]
[docs] @staticmethod def z_intercept(optic, surface_number, Hx, Hy, Px, Py, wavelength): """Calculates the z-coordinate of the intercept point on a specific surface. Args: optic: The optic object. surface_number: The number of the surface. Hx: The normalized x field coordinate. Hy: The normalized y field coordinate. Px: The normalized x pupil coordinate. Py: The normalized y pupil coordinate. wavelength: The wavelength of the ray. Returns: The z-coordinate of the intercept point. """ optic.trace_generic(Hx, Hy, Px, Py, wavelength) return optic.surfaces.z[surface_number, 0]
[docs] @staticmethod def x_intercept_lcs(optic, surface_number, Hx, Hy, Px, Py, wavelength): """Calculates the x-coordinate of the intercept point on a specific surface in its lcs, ie wrt to its vertex. Args: optic: The optic object. surface_number: The number of the surface. Hx: The normalized x field coordinate. Hy: The normalized y field coordinate. Px: The normalized x pupil coordinate. Py: The normalized y pupil coordinate. wavelength: The wavelength of the ray. Returns: The x-coordinate of the intercept point. """ optic.trace_generic(Hx, Hy, Px, Py, wavelength) intercept = optic.surfaces.x[surface_number, 0] decenter = optic.surfaces[surface_number].geometry.cs.x return intercept - decenter
[docs] @staticmethod def y_intercept_lcs(optic, surface_number, Hx, Hy, Px, Py, wavelength): """Calculates the y-coordinate of the intercept point on a specific surface in its lcs, ie wrt to its vertex. Args: optic: The optic object. surface_number: The number of the surface. Hx: The normalized x field coordinate. Hy: The normalized y field coordinate. Px: The normalized x pupil coordinate. Py: The normalized y pupil coordinate. wavelength: The wavelength of the ray. Returns: The y-coordinate of the intercept point. """ optic.trace_generic(Hx, Hy, Px, Py, wavelength) intercept = optic.surfaces.y[surface_number, 0] decenter = optic.surfaces[surface_number].geometry.cs.y return intercept - decenter
[docs] @staticmethod def z_intercept_lcs(optic, surface_number, Hx, Hy, Px, Py, wavelength): """Calculates the z-coordinate of the intercept point on a specific surface in its lcs, ie wrt to its vertex. Args: optic: The optic object. surface_number: The number of the surface. Hx: The normalized x field coordinate. Hy: The normalized y field coordinate. Px: The normalized x pupil coordinate. Py: The normalized y pupil coordinate. wavelength: The wavelength of the ray. Returns: The z-coordinate of the intercept point. """ optic.trace_generic(Hx, Hy, Px, Py, wavelength) intercept = optic.surfaces.z[surface_number, 0] decenter = optic.surfaces[surface_number].geometry.cs.z # For some reason decenter can sometimes be a single-element array. # In that case, retreive the float inside. # This is a workaround until a solution is found. if be.is_array_like(decenter): decenter = decenter.item() return intercept - decenter
[docs] @staticmethod def L(optic, surface_number, Hx, Hy, Px, Py, wavelength): """Calculates the direction cosine L of the ray on a specific surface. Args: optic: The optic object. surface_number: The number of the surface. Hx: The normalized x field coordinate. Hy: The normalized y field coordinate. Px: The normalized x pupil coordinate. Py: The normalized y pupil coordinate. wavelength: The wavelength of the ray. Returns: The direction cosine L of the ray. """ optic.trace_generic(Hx, Hy, Px, Py, wavelength) return optic.surfaces.L[surface_number, 0]
[docs] @staticmethod def M(optic, surface_number, Hx, Hy, Px, Py, wavelength): """Calculates the direction cosine M of the ray on a specific surface. Args: optic: The optic object. surface_number: The number of the surface. Hx: The normalized x field coordinate. Hy: The normalized y field coordinate. Px: The normalized x pupil coordinate. Py: The normalized y pupil coordinate. wavelength: The wavelength of the ray. Returns: The direction cosine M of the ray. """ optic.trace_generic(Hx, Hy, Px, Py, wavelength) return optic.surfaces.M[surface_number, 0]
[docs] @staticmethod def N(optic, surface_number, Hx, Hy, Px, Py, wavelength): """Calculates the direction cosine N of the ray on a specific surface. Args: optic: The optic object. surface_number: The number of the surface. Hx: The normalized x field coordinate. Hy: The normalized y field coordinate. Px: The normalized x pupil coordinate. Py: The normalized y pupil coordinate. wavelength: The wavelength of the ray. Returns: The direction cosine N of the ray. """ optic.trace_generic(Hx, Hy, Px, Py, wavelength) return optic.surfaces.N[surface_number, 0]
[docs] @staticmethod def AOI(optic, surface_number, Hx, Hy, Px, Py, wavelength): """ Calculates the real ray angle of incidence in degrees at a specific surface. This angle is always positive, and it is the angle between the incident ray and the surface normal. Args: optic: The optic object. surface_number: The number of the surface. Hx: The normalized x field coordinate. Hy: The normalized y field coordinate. Px: The normalized x pupil coordinate. Py: The normalized y pupil coordinate. wavelength: The wavelength of the ray. Returns: The angle of incidence in degrees (always positive as in zemax). """ optic.trace_generic(Hx, Hy, Px, Py, wavelength) surface = optic.surfaces[surface_number] geometry = surface.geometry L_inc = optic.surfaces.L[surface_number - 1, 0] M_inc = optic.surfaces.M[surface_number - 1, 0] N_inc = optic.surfaces.N[surface_number - 1, 0] from optiland.rays import RealRays rays_at_surface = RealRays( x=optic.surfaces.x[surface_number, 0], y=optic.surfaces.y[surface_number, 0], z=optic.surfaces.z[surface_number, 0], L=L_inc, M=M_inc, N=N_inc, intensity=1.0, # irrelevant for AOI wavelength=wavelength, ) # public surface_normal method nx, ny, nz = geometry.surface_normal(rays=rays_at_surface) # dot product between incident ray and surface normal dot_product = be.abs(L_inc * nx + M_inc * ny + N_inc * nz) # handle potential floating point errors where dot_product > 1.0 dot_product_clip = be.minimum(dot_product, be.array(1.0)) angle_rad = be.arccos(dot_product_clip) angle_deg = be.rad2deg(angle_rad) # For some reason angle_deg can sometimes be a single-element array. # In that case, retreive the float inside. # This is a workaround until a solution is found. if be.is_array_like(angle_deg): angle_deg = angle_deg.item() return angle_deg
[docs] @staticmethod def rms_spot_size( optic, surface_number, Hx, Hy, num_rays, wavelength, distribution="hexapolar", ): """Calculates the root mean square (RMS) spot size on a specific surface. Args: optic: The optic object. surface_number: The number of the surface. Hx: The normalized x field coordinate. Hy: The normalized y field coordinate. num_rays: The number of rays to trace. wavelength: The wavelength of the rays. distribution: The distribution of the rays. Default is 'hexapolar'. Returns: The RMS spot size on the specified surface. """ if wavelength == "all": x = [] y = [] for wave in optic.wavelengths.get_wavelengths(): optic.trace(Hx, Hy, wave, num_rays, distribution) x.append(optic.surfaces.x[surface_number, :].flatten()) y.append(optic.surfaces.y[surface_number, :].flatten()) wave_idx = optic.wavelengths.primary_index mean_x = be.mean(x[wave_idx]) mean_y = be.mean(y[wave_idx]) r2 = [(x[i] - mean_x) ** 2 + (y[i] - mean_y) ** 2 for i in range(len(x))] return be.sqrt(be.mean(be.concatenate(r2))) optic.trace(Hx, Hy, wavelength, num_rays, distribution) x = optic.surfaces.x[surface_number, :].flatten() y = optic.surfaces.y[surface_number, :].flatten() r2 = (x - be.mean(x)) ** 2 + (y - be.mean(y)) ** 2 return be.sqrt(be.mean(r2))
[docs] @staticmethod def OPD_difference( optic, Hx, Hy, num_rays, wavelength, distribution="gaussian_quad", ): """Calculates the mean optical path difference (OPD) difference for a given ray distribution. Args: optic: The optic object. Hx: The normalized x field coordinate. Hy: The normalized y field coordinate. num_rays: The number of rays to trace. wavelength: The wavelength of the rays. distribution: The distribution of the rays. Default is 'gaussian_quad'. Returns: The OPD difference for the given ray distribution. """ weights = None if distribution == "gaussian_quad": distribution = GaussianQuadrature() distribution.generate_points(num_rays) weights = distribution.weights wf = wavefront.Wavefront( optic, [(Hx, Hy)], [wavelength], num_rays, distribution, ) wavefront_data = wf.get_data((Hx, Hy), wavelength) opd = wavefront_data.opd if weights is None: weights = 1.0 / len(wf.distribution.x) delta = be.abs(opd - be.mean(opd)) * weights return be.sum(delta)
[docs] @staticmethod def clearance( optic, line_ray_surface_idx, line_ray_field_coords, line_ray_pupil_coords, point_ray_surface_idx, point_ray_field_coords, point_ray_pupil_coords, wavelength, ): """Computes the signed perpendicular distance in the YZ plane from a reference line (Line A) to a reference point (Point B). Line A is defined by a ray (RA) traced at field FA, after it leaves surface SA. Point B is the intersection of a ray (RB) traced at field FB with surface SB. This operand is useful for creating clearance or interference constraints, particularly in off-axis reflective systems. The sign convention is such that for Line A propagating generally in the +Z direction (N direction cosine > 0), the signed distance is positive if Point B is on the +Y side of Line A. If Line A propagates generally in the -Z direction (N direction cosine < 0), this sign is flipped. Args: optic: The optical system model. line_ray_surface_idx: The index of the surface (SA) from which Line A originates (i.e., ray data is taken *after* this surface). line_ray_field_coords: A tuple (Hx, Hy) representing the normalized field coordinates for the ray defining Line A (FA). line_ray_pupil_coords: A tuple (Px, Py) representing the normalized pupil coordinates for the ray defining Line A (FA). point_ray_surface_idx: The index of the surface (SB) with which the ray defining Point B intersects. point_ray_field_coords: A tuple (Hx, Hy) representing the normalized field coordinates for the ray defining Point B (FB). point_ray_pupil_coords: A tuple (Px, Py) representing the normalized pupil coordinates for the ray defining Point B (FB). wavelength: The wavelength at which to trace the rays. Returns: float: The signed perpendicular distance in the YZ plane from Line A to Point B. Returns 0.0 if Line A has zero length in the YZ plane (i.e., mA and nA are both zero). """ FA_Hx, FA_Hy = line_ray_field_coords FA_Px, FA_Py = line_ray_pupil_coords optic.trace_generic(FA_Hx, FA_Hy, FA_Px, FA_Py, wavelength) yA = optic.surfaces.y[line_ray_surface_idx, 0] zA = optic.surfaces.z[line_ray_surface_idx, 0] mA = optic.surfaces.M[line_ray_surface_idx, 0] nA = optic.surfaces.N[line_ray_surface_idx, 0] FB_Hx, FB_Hy = point_ray_field_coords FB_Px, FB_Py = point_ray_pupil_coords optic.trace_generic(FB_Hx, FB_Hy, FB_Px, FB_Py, wavelength) yB = optic.surfaces.y[point_ray_surface_idx, 0] zB = optic.surfaces.z[point_ray_surface_idx, 0] denominator = be.sqrt(mA**2 + nA**2) epsilon = 1e-9 if be.abs(denominator) < epsilon: d = 0.0 else: numerator = nA * (yB - yA) - mA * (zB - zA) d = numerator / denominator if nA < 0: d = -d return d