Source code for skZemax.skZemax_subfunctions._LDE_functions

from __future__ import annotations


import numpy as np
import xarray as xr
from box import Box

from skZemax.skZemax_subfunctions._c_print import c_print as cp
from skZemax.skZemax_subfunctions._field_functions import Field_GetNormalization
from skZemax.skZemax_subfunctions._wavelength_functions import (
    ZOSAPI_SystemData_IWavelength,
)
from skZemax.skZemax_subfunctions._ZOSAPI_interface_functions import (
    __LowLevelZemaxStringCheck__,
    _CheckIfStringValidInDir_,
    _convert_raw_input_worker_,
    _ctype_to_numpy_,
)


type ZOSAPI_Editors_LDE_ILDERow = object  # <- ZOSAPI.Editors.LDE.ILDERow # The actual module is referenced by the base PythonStandaloneApplication class.
type ZOSAPI_Editors_LDE_ISurfaceApertureType = object  # <- ZOSAPI.Editors.LDE.ISurfaceApertureType # The actual module is referenced by the base PythonStandaloneApplication class.
type ZOSAPI_Editors_LDE_SurfaceColumn = object  # <- ZOSAPI.Editors.LDE.SurfaceColumn # The actual module is referenced by the base PythonStandaloneApplication class.
type ZOSAPI_Tools_RayTrace_IBatchRayTrace = object  # <- ZOSAPI.Tools.RayTrace.IBatchRayTrace # The actual module is referenced by the base PythonStandaloneApplication class.
type CLR_MethodBinding = object  # <- CLR.MethodBinding # The actual module is referenced by the base PythonStandaloneApplication class.


def _convert_raw_surface_input_(
    self, in_surface: int | ZOSAPI_Editors_LDE_ILDERow, return_index: bool = True
) -> int | ZOSAPI_Editors_LDE_ILDERow:
    return _convert_raw_input_worker_(
        self,
        in_value=in_surface,
        object_type=self.ZOSAPI.Editors.LDE.ILDERow,
        return_index=return_index,
    )


[docs] def LDE_GetNumberOfSurfaces(self) -> int: """ Gets the number of surfaces in the LDE :return: The total number of (sequential) surfaces in the lens data editor (LDE) :rtype: int """ return int(self.TheSystem.LDE.get_NumberOfSurfaces())
[docs] def LDE_GetSurface(self, SurfaceNum: int) -> ZOSAPI_Editors_LDE_ILDERow: """ Returns a surface object at the given index. :param SurfaceNum: Surface index number :type SurfaceNum: int :return: The object of the surface at index. :rtype: ZOSAPI_Editors_LDE_ILDERow """ SurfaceNum = int(SurfaceNum) if SurfaceNum < self.LDE_GetNumberOfSurfaces() and SurfaceNum >= 0: return self.TheSystem.LDE.GetSurfaceAt(int(SurfaceNum)) if self._verbose: cp( f"!@ly!@LDE_GetSurface :: Asked for Surface [!@lm!@{SurfaceNum}!@ly!@] but there are only !@lm!@{self.LDE_GetNumberOfSurfaces()}!@ly!@ surfaces built up to index !@lm!@{self.LDE_GetNumberOfSurfaces() - 1}!@ly!@." ) return None
[docs] def LDE_InsertNewSurface( self, insertSurface: int | ZOSAPI_Editors_LDE_ILDERow ) -> ZOSAPI_Editors_LDE_ILDERow: """ Inserts a new surface at specified location in the LDE. :param insertSurface: The location to insert the surface. Specified by either an index or a surface object. :type insertSurface: Union[int, ZOSAPI_Editors_LDE_ILDERow] :return: The surface object of the newly inserted surface. :rtype: ZOSAPI_Editors_LDE_ILDERow """ return self.TheSystem.LDE.InsertNewSurfaceAt( self._convert_raw_surface_input_(insertSurface, return_index=True) )
[docs] def LDE_AddNewSurface(self) -> ZOSAPI_Editors_LDE_ILDERow: """ Adds a new surface to the end of the system :return: The surface object of the newly made surface. :rtype: ZOSAPI_Editors_LDE_ILDERow """ return self.TheSystem.LDE.AddSurface()
[docs] def LDE_RemoveSurface(self, delSurface: int | ZOSAPI_Editors_LDE_ILDERow) -> None: """ Removes the surface at specified location in the LDE. :param delSurface: The location to delete the surface. Specified by either an index or a surface object. :type delSurface: Union[int, ZOSAPI_Editors_LDE_ILDERow] """ self.TheSystem.LDE.RemoveSurfaceAt( self._convert_raw_surface_input_(delSurface, return_index=True) )
[docs] def LDE_CopyAndInsertSurfacesFromFile( self, in_Surface: int | ZOSAPI_Editors_LDE_ILDERow, path_to_file: str, first_surface_to_copy: int, number_surfaces_to_copy: int, ) -> None: """ Copies surfaces from another .zmx file and insert them into the current one. Surfaces of first_surface_to_copy to (first_surface_to_copy + number_surfaces_to_copy) are inserted in the current file beginning at in_Surface. :param in_Surface: Location in the active file to begin inserting surfaces. :type in_Surface: Union[int, ZOSAPI_Editors_LDE_ILDERow] :param path_to_file: Path to another Zemax file from which to copy some surface elements. :type path_to_file: str :param first_surface_to_copy: The first surface in the external Zemax file to copy over into the currently active one. :type first_surface_to_copy: int :param number_surfaces_to_copy: The number of surfaces after first_surface_to_copy to copy over along with it. :type number_surfaces_to_copy: int """ SurfaceLDE = self._convert_raw_surface_input_(in_Surface, return_index=True) new_system = self.TheApplication.CreateNewSystem(self.ZOSAPI.SystemType.Sequential) new_system.LoadFile(path_to_file, False) self.TheSystem.LDE.CopySurfacesFrom( new_system.LDE, first_surface_to_copy, number_surfaces_to_copy, SurfaceLDE ) self.TheApplication.CloseSystemAt( self.TheApplication.get_NumberOfOpticalSystems() - 1, False ) del new_system
[docs] def LDE_GetNamesOfAllSurfaceTypes(self, print_to_console: bool = False) -> list: """ This function is simply for user convenance to look up the ZOS-API names of all surface types. This can be useful to look up what one may want to code as input to functions like :func:`LDE_ChangeSurfaceType`. :param print_to_console: If True will print to console, defaults to False :type print_to_console: bool, optional :return: A list of the names of all surface types the ZOS-API knows. :rtype: list """ surface_types = __LowLevelZemaxStringCheck__( self, in_obj=self.ZOSAPI.Editors.LDE.SurfaceType ) if print_to_console: cp("\n!@lg!@LDE_GetListOfAllSurfaceTypes :: Names of Surface Types:") [cp(" !@lm!@" + str(x)) for x in surface_types] cp("\n") return surface_types
[docs] def LDE_ChangeSurfaceType( self, in_Surface: int | ZOSAPI_Editors_LDE_ILDERow, surface_type: str ) -> ZOSAPI_Editors_LDE_ILDERow: """ This function changes the surface type of a (sequential) surface element in the lens data editor. Surface types are described in Zemax documentation, and can be named by the :func:`LDE_GetNamesOfAllSurfaceTypes` function. :param in_Surface: The surface to change as an object or as an index. :type in_Surface: Union[int, ZOSAPI_Editors_LDE_ILDERow] :param surface_type: A string identifying the surface type to change it to. :type surface_type: str :return: The surface object being operated on. :rtype: ZOSAPI_Editors_LDE_ILDERow """ SurfaceLDE = self._convert_raw_surface_input_(in_Surface, return_index=False) surfacetype = self._CheckIfStringValidInDir_( self.ZOSAPI.Editors.LDE.SurfaceType, str(surface_type) ) if surfacetype is not None: SurfaceLDE.ChangeType(SurfaceLDE.GetSurfaceTypeSettings(surfacetype)) elif self._verbose: cp("!@ly!@LDE_ChangeSurfaceType :: Did not change surface type") return SurfaceLDE
[docs] def LDE_GetNamesOfAllApertureTypes(self, print_to_console: bool = False) -> list: """ This function is simply for user convenance to look up the ZOS-API names of all aperture types. This can be useful to look up what one may want to code as input to functions like :func:`LDE_GetApertureTypeSettings`. :param print_to_console: If True will print to console, defaults to False :type print_to_console: bool, optional :return: A list of the names of all aperture types the ZOS-API knows. :rtype: list """ aperture_types = __LowLevelZemaxStringCheck__( self, in_obj=self.ZOSAPI.Editors.LDE.SurfaceApertureTypes ) if print_to_console: cp("\n!@lg!@LDE_GetNamesOfAllApertureTypes :: Names of Aperture Types:") [cp(" !@lm!@" + str(x)) for x in aperture_types] cp("\n") return aperture_types
[docs] def LDE_GetSurfaceApertureType( self, in_Surface: int | ZOSAPI_Editors_LDE_ILDERow ) -> str: """ This function returns the type of aperture the given surface is. :param in_Surface: The surface to change as an object or as an index. :type in_Surface: Union[int, ZOSAPI_Editors_LDE_ILDERow] :return: The name of the aperture type :rtype: str """ return str( self._convert_raw_surface_input_( in_Surface, return_index=False ).ApertureData.CurrentType )
[docs] def LDE_GetApertureTypeSettings( self, in_Surface: int | ZOSAPI_Editors_LDE_ILDERow, aperture_type: str ) -> ZOSAPI_Editors_LDE_ISurfaceApertureType: """ Note that while this is available to a user for customized implantation, it is recommended to go through a pre-defined function like :func:`LDE_ChangeApertureToRectangular`/:func:`LDE_GetApertureAsRectangularType` or :func:`LDE_ChangeApertureToCircular`/:func:`LDE_GetApertureAsCircularType` to change/get the aperture settings. This function gets the aperture type settings of the surface. Generally this is to change a LDE surface's aperture. If you want to get the aperture type of the surface use :func:`LDE_GetSurfaceApertureType`. :param in_Surface: The surface to change as an object or as an index. :type in_Surface: Union[int, ZOSAPI_Editors_LDE_ILDERow] :param aperture_type: A string identifying the aperture type to change it to. :type aperture_type: str :return: The settings of the aperture to change. :rtype: ZOSAPI_Editors_LDE_ISurfaceApertureType """ SurfaceLDE = self._convert_raw_surface_input_(in_Surface, return_index=False) aperturetype = self._CheckIfStringValidInDir_( self.ZOSAPI.Editors.LDE.SurfaceApertureTypes, str(aperture_type) ) settings = None if aperturetype is not None: settings = SurfaceLDE.ApertureData.CreateApertureTypeSettings(aperturetype) elif self._verbose: cp( f"!@ly!@LDE_GetApertureTypeSettings :: Did not find {aperture_type!s} aperture type settings" ) return None return settings
[docs] def LDE_ChangeApertureToRectangular( self, in_Surface: int | ZOSAPI_Editors_LDE_ILDERow, XHalfWidth: float = 10.0, YHalfWidth: float = 10.0, ApertureXDecenter: float = 0.0, ApertureYDecenter: float = 0.0, ) -> ZOSAPI_Editors_LDE_ILDERow: """ Changes the aperture of the surface to a rectangular one with specified widths Rays are vignetted which intercept the surface outside the rectangular region defined by the half widths in x and y. :param in_Surface: The surface to change as an object or as an index. :type in_Surface: Union[int, ZOSAPI_Editors_LDE_ILDERow] :param XHalfWidth: The half-width in the x-direction of the aperture, defaults to 10.0 :type XHalfWidth: float, optional :param YHalfWidth: The half-width in the y-direction of the aperture, defaults to 10.0 :type YHalfWidth: float, optional :param ApertureXDecenter: The decenter in the x-direction of the aperture, defaults to 0.0 :type ApertureXDecenter: float, optional :param ApertureYDecenter: The decenter in the y-direction of the aperture, defaults to 0.0 :type ApertureYDecenter: float, optional :return: The surface object. :rtype: ZOSAPI_Editors_LDE_ILDERow """ SurfaceLDE = self._convert_raw_surface_input_(in_Surface, return_index=False) settings = self.LDE_GetApertureTypeSettings( in_Surface=SurfaceLDE, aperture_type="RectangularAperture" ) settings._S_RectangularAperture.XHalfWidth = XHalfWidth settings._S_RectangularAperture.YHalfWidth = YHalfWidth settings._S_RectangularAperture.ApertureXDecenter = ApertureXDecenter settings._S_RectangularAperture.ApertureYDecenter = ApertureYDecenter SurfaceLDE.ApertureData.ChangeApertureTypeSettings(settings) return SurfaceLDE
[docs] def LDE_GetApertureAsRectangularType( self, in_Surface: int | ZOSAPI_Editors_LDE_ILDERow ) -> Box: """ This function returns the aperture settings of this object interpreted as a Rectangular aperture by Zemax. Note that this does not nesscarly mean the aperture is currently set to be Rectangular, but rather these are the current settings it would have if it were. If the surface is this type of aperture, then these are the settings it has. :param in_Surface: The surface to change as an object or as an index. :type in_Surface: Union[int, ZOSAPI_Editors_LDE_ILDERow] :return: A dict of the Rectangular aperture settings :rtype: Box """ SurfaceLDE = self._convert_raw_surface_input_(in_Surface, return_index=False) settings = self.LDE_GetApertureTypeSettings( in_Surface=SurfaceLDE, aperture_type="RectangularAperture" ) out = Box({}) out["XHalfWidth"] = settings._S_RectangularAperture.XHalfWidth out["YHalfWidth"] = settings._S_RectangularAperture.YHalfWidth out["ApertureXDecenter"] = settings._S_RectangularAperture.ApertureXDecenter out["ApertureYDecenter"] = settings._S_RectangularAperture.ApertureYDecenter return out
[docs] def LDE_ChangeApertureToCircular( self, in_Surface: int | ZOSAPI_Editors_LDE_ILDERow, MinimumRadius: float = 0.0, MaximumRadius: float = 10.0, ApertureXDecenter: float = 0.0, ApertureYDecenter: float = 0.0, ) -> ZOSAPI_Editors_LDE_ILDERow: """ Changes the aperture of the surface to a circular one with specified radii. A Circular Aperture defines an annular region which vignettes all rays which strike the surface inside of the minimum radius, and outside of the maximum radius. If the ray is between the minimum and maximum radii, then the ray will be allowed to proceed. :param in_Surface: The surface to change as an object or as an index. :type in_Surface: Union[int, ZOSAPI_Editors_LDE_ILDERow] :param MinimumRadius: The minimum radius of the aperture, defaults to 0.0 :type MinimumRadius: float, optional :param MaximumRadius: The maximum radius of the aperture, defaults to 10.0 :type MaximumRadius: float, optional :param ApertureXDecenter: The decenter in the x-direction of the aperture, defaults to 0.0 :type ApertureXDecenter: float, optional :param ApertureYDecenter: The decenter in the y-direction of the aperture, defaults to 0.0 :type ApertureYDecenter: float, optional :return: The surface object. :rtype: ZOSAPI_Editors_LDE_ILDERow """ SurfaceLDE = self._convert_raw_surface_input_(in_Surface, return_index=False) settings = self.LDE_GetApertureTypeSettings( in_Surface=SurfaceLDE, aperture_type="CircularAperture" ) settings._S_CircularAperture.MinimumRadius = MinimumRadius settings._S_CircularAperture.MaximumRadius = MaximumRadius settings._S_CircularAperture.ApertureXDecenter = ApertureXDecenter settings._S_CircularAperture.ApertureYDecenter = ApertureYDecenter SurfaceLDE.ApertureData.ChangeApertureTypeSettings(settings) return SurfaceLDE
[docs] def LDE_GetApertureAsCircularType( self, in_Surface: int | ZOSAPI_Editors_LDE_ILDERow ) -> Box: """ This function returns the aperture settings of this object interpreted as a Circular aperture by Zemax. Note that this does not nesscarly mean the aperture is currently set to be Circular, but rather these are the current settings it would have if it were. If the surface is this type of aperture, then these are the settings it has. :param in_Surface: The surface to change as an object or as an index. :type in_Surface: Union[int, ZOSAPI_Editors_LDE_ILDERow] :return: A dict of the Circular aperture settings :rtype: Box """ SurfaceLDE = self._convert_raw_surface_input_(in_Surface, return_index=False) settings = self.LDE_GetApertureTypeSettings( in_Surface=SurfaceLDE, aperture_type="CircularAperture" ) out = Box({}) out["MinimumRadius"] = settings._S_CircularAperture.MinimumRadius out["MaximumRadius"] = settings._S_CircularAperture.MaximumRadius out["ApertureXDecenter"] = settings._S_CircularAperture.ApertureXDecenter out["ApertureYDecenter"] = settings._S_CircularAperture.ApertureYDecenter return out
[docs] def LDE_ChangeApertureToCircularObscuration( self, in_Surface: int | ZOSAPI_Editors_LDE_ILDERow, MinimumRadius: float = 0.0, MaximumRadius: float = 10.0, ApertureXDecenter: float = 0.0, ApertureYDecenter: float = 0.0, ) -> ZOSAPI_Editors_LDE_ILDERow: """ Changes the aperture of the surface to a circular *obscuration* one with specified radii. The Circular Obscuration is the complement of the Circular Aperture. If the ray is NOT between the minimum and maximum radii, then the ray will be allowed to proceed. :param in_Surface: The surface to change as an object or as an index. :type in_Surface: Union[int, ZOSAPI_Editors_LDE_ILDERow] :param MinimumRadius: The minimum radius of the aperture, defaults to 0.0 :type MinimumRadius: float, optional :param MaximumRadius: The maximum radius of the aperture, defaults to 10.0 :type MaximumRadius: float, optional :param ApertureXDecenter: The decenter in the x-direction of the aperture, defaults to 0.0 :type ApertureXDecenter: float, optional :param ApertureYDecenter: The decenter in the y-direction of the aperture, defaults to 0.0 :type ApertureYDecenter: float, optional :return: The surface object. :rtype: ZOSAPI_Editors_LDE_ILDERow """ SurfaceLDE = self._convert_raw_surface_input_(in_Surface, return_index=False) settings = self.LDE_GetApertureTypeSettings( in_Surface=SurfaceLDE, aperture_type="CircularObscuration" ) settings._S_CircularObscuration.MinimumRadius = MinimumRadius settings._S_CircularObscuration.MaximumRadius = MaximumRadius settings._S_CircularObscuration.ApertureXDecenter = ApertureXDecenter settings._S_CircularObscuration.ApertureYDecenter = ApertureYDecenter SurfaceLDE.ApertureData.ChangeApertureTypeSettings(settings) return SurfaceLDE
[docs] def LDE_GetApertureAsCircularObscurationType( self, in_Surface: int | ZOSAPI_Editors_LDE_ILDERow ) -> Box: """ This function returns the aperture settings of this object interpreted as a CircularObscuration aperture by Zemax. Note that this does not nesscarly mean the aperture is currently set to be CircularObscuration, but rather these are the current settings it would have if it were. If the surface is this type of aperture, then these are the settings it has. :param in_Surface: The surface to change as an object or as an index. :type in_Surface: Union[int, ZOSAPI_Editors_LDE_ILDERow] :return: A dict of the CircularObscuration aperture settings :rtype: Box """ SurfaceLDE = self._convert_raw_surface_input_(in_Surface, return_index=False) settings = self.LDE_GetApertureTypeSettings( in_Surface=SurfaceLDE, aperture_type="CircularObscuration" ) out = Box({}) out["MinimumRadius"] = settings._S_CircularObscuration.MinimumRadius out["MaximumRadius"] = settings._S_CircularObscuration.MaximumRadius out["ApertureXDecenter"] = settings._S_CircularObscuration.ApertureXDecenter out["ApertureYDecenter"] = settings._S_CircularObscuration.ApertureYDecenter return out
[docs] def LDE_ChangeApertureToFloating( self, in_Surface: int | ZOSAPI_Editors_LDE_ILDERow ) -> ZOSAPI_Editors_LDE_ILDERow: """ Changes the aperture of the surface to a Floating one. A floating aperture is very similar to the circular aperture, except the minimum radius is always zero, and the maximum radius is always equal to the clear semi-diameter or semi-diameter of the surface. Since the clear semi-diameter or semi-diameter value may be adjusted by OpticStudio (when in automatic mode) the aperture value "floats" as the clear semi-diameter or semi-diameter value. The floating aperture is useful when macros or external programs use OpticStudio to trace rays that may lie outside of the default clear semi-diameters or semi-diameters, and these rays are to be vignetted. :param in_Surface: The surface to change as an object or as an index. :type in_Surface: Union[int, ZOSAPI_Editors_LDE_ILDERow] :return: The surface object. :rtype: ZOSAPI_Editors_LDE_ILDERow """ SurfaceLDE = self._convert_raw_surface_input_(in_Surface, return_index=False) settings = self.LDE_GetApertureTypeSettings( in_Surface=SurfaceLDE, aperture_type="FloatingAperture" ) SurfaceLDE.ApertureData.ChangeApertureTypeSettings(settings) return SurfaceLDE
[docs] def LDE_SetSurfaceAsStop(self, in_Surface: int | ZOSAPI_Editors_LDE_ILDERow) -> None: """ Sets the surface as the system stop. :param in_Surface: The surface to change as an object or as an index. :type in_Surface: Union[int, ZOSAPI_Editors_LDE_ILDERow] """ in_Surface = self._convert_raw_surface_input_(in_Surface, return_index=False) in_Surface.IsStop = True
[docs] def LDE_GetStopSurface(self) -> ZOSAPI_Editors_LDE_ILDERow: """ Finds and returns the stop surface of the system :return: The surface object of the stop. :rtype: ZOSAPI_Editors_LDE_ILDERow """ return self.LDE_GetSurface( int( np.argmax( [ self.LDE_CheckIfSurfaceIsStop(x) for x in range(self.LDE_GetNumberOfSurfaces()) ] ) ) )
[docs] def LDE_CheckIfSurfaceIsStop( self, in_Surface: int | ZOSAPI_Editors_LDE_ILDERow ) -> bool: """ Checks if the given surface is the stop of the system. :param in_Surface: The surface to change as an object or as an index. :type in_Surface: Union[int, ZOSAPI_Editors_LDE_ILDERow] :return: True if the surface is the system stop. :rtype: bool """ in_Surface = self._convert_raw_surface_input_(in_Surface, return_index=False) return in_Surface.IsStop
def _LDE_GetSurfaceColumns_(self) -> tuple: """ Worker function which builds a surface's column names and underlying attributes. :return: the surface column call to access the enumerators, and the sting names of the columns :rtype: tuple """ all_surface_columns = __LowLevelZemaxStringCheck__( self, self.ZOSAPI.Editors.LDE.SurfaceColumn ) # # surface_columns = [x for x in all_surface_columns if 'Par' not in x] # This is just making a nicer order (to match Zemax GUI) of the above surface_columns = [ "Comment", "Radius", "Thickness", "Material", "Coating", "SemiDiameter", "ChipZone", "MechanicalSemiDiameter", "Conic", "TCE", ] par_columns = [x for x in all_surface_columns if "Par" in x] # Ensure 'ParXX' names are sorted by int in XX surface_columns = surface_columns + [ y for x, y in sorted( zip([int(x.strip("Par")) for x in par_columns], par_columns, strict=False) ) ] surface_column_calls = [ getattr(self.ZOSAPI.Editors.LDE.SurfaceColumn, x) for x in surface_columns ] return surface_column_calls, surface_columns def _LDE_GetSurfaceCalls_(self, SurfaceLDE: ZOSAPI_Editors_LDE_ILDERow) -> tuple: """ Worker function which (invoking :func:`_LDE_GetSurfaceColumns_`) will return the base underlying column calls of a surface. :param SurfaceLDE: A surface object (not expected to be the index). :type SurfaceLDE: ZOSAPI_Editors_LDE_ILDERow :return: The column calls, and the column names as strings. :rtype: tuple """ surface_column_calls, surface_columns = self._LDE_GetSurfaceColumns_() surfacecolumn_calls = [SurfaceLDE.GetSurfaceCell(x) for x in surface_column_calls] return surfacecolumn_calls, surface_columns
[docs] def LDE_GetSurfaceColumnEnum( self, in_str: str, in_Surface: None | int | ZOSAPI_Editors_LDE_ILDERow = None ) -> ZOSAPI_Editors_LDE_SurfaceColumn: """ Returns the underlying ZOI-API enumerators of a LDE column matching the input string. If in_Surface is None, then assuming only searching for a default/base property or one named by 'par#'. If in_Surface is not None, then will search for the property in the specific surface by name and return the right enum (since different surface types have different names for their columns). If the given string is able to be matched to more than one column name, then the first column within the names will be given. :param in_str: The column, by name, to get the ZOI-API enumerator of. :type in_str: str :param in_Surface: The surface to change as an object or as an index, defaults to None :type in_Surface: Union[None, int, ZOSAPI_Editors_LDE_ILDERow], optional :return: the ZOI-API enumerator :rtype: ZOSAPI_Editors_LDE_SurfaceColumn """ if in_Surface is None or "par" in in_str.lower(): return self._CheckIfStringValidInDir_( self.ZOSAPI.Editors.LDE.SurfaceColumn, in_str ) surface_column_calls, surface_columns = self._LDE_GetSurfaceCalls_( self._convert_raw_surface_input_(in_Surface, return_index=False) ) bool_mask = [in_str.lower() in x.Header.lower() for x in surface_column_calls] if np.any(bool_mask): return self._CheckIfStringValidInDir_( self.ZOSAPI.Editors.LDE.SurfaceColumn, surface_columns[int(np.where(bool_mask)[0][0])], ) if self._verbose: cp( f"!@ly!@LDE_GetSurfaceColumnEnum :: Did not find [!@lm!@{in_str}!@ly!@] in surface properties." ) return None
[docs] def LDE_GetAllColumnDataOfSurface( self, in_Surface: int | ZOSAPI_Editors_LDE_ILDERow ) -> Box: """ Gets all column data of a Sequential surface and returns it as a dict. This was made to reflect the functions of :func:`NCE_GetAllColumnDataOfObject` and :func:`NCE_SetAllColumnDataOfObjectFromDict` which are for robustness in Non-sequential mode. However, unlike Non-sequential mode, Sequential mode seems to work as the API intended so you don't need to use these lower level dict functions to adjust surface properties. However, it is still very useful to set properties through this mechanism in python code. :param in_Surface: The surface to change as an object or as an index. :type in_Surface: Union[int, ZOSAPI_Editors_LDE_ILDERow] :return: dict of the surface's column data. :rtype: Box """ surfacecolumn_calls, _surface_columns = self._LDE_GetSurfaceCalls_( self._convert_raw_surface_input_(in_Surface, return_index=False) ) out = Box({}) for scall in surfacecolumn_calls: if "(unused)" in scall.Header and "Par 0" not in scall.Header: break # Everything after this should be empty if "(unused)" in scall.Header: pass else: out[scall.Header] = scall.Value return out
[docs] def LDE_SetAllColumnDataOfSurfaceFromDict( self, in_Surface: int | ZOSAPI_Editors_LDE_ILDERow, SurfaceLDE_dict: dict | Box ) -> None: """ Sets all column data of a Sequential surface from a dict - usually the dict produced by :func:`LDE_GetAllColumnDataOfSurface` after being altered. This was made to reflect the functions of :func:`NCE_GetAllColumnDataOfObject` and :func:`NCE_SetAllColumnDataOfObjectFromDict` which are for robustness in Non-sequential mode. However, unlike Non-sequential mode, Sequential mode seems to work as the API intended so you don't need to use these lower level dict functions to adjust surface properties. However, it is still very useful to set properties through this mechanism in python code. :param in_Surface: The surface to change as an object or as an index. :type in_Surface: Union[int, ZOSAPI_Editors_LDE_ILDERow] :param SurfaceLDE_dict: Column properties and values to set for the surface. :type SurfaceLDE_dict: dict|Box """ surfacecolumn_calls, _surface_columns = self._LDE_GetSurfaceCalls_( self._convert_raw_surface_input_(in_Surface, return_index=False) ) for scall in surfacecolumn_calls: if "(unused)" in scall.Header and "Par 0" not in scall.Header: break # Everything after this should be empty if "(unused)" in scall.Header: pass elif SurfaceLDE_dict[scall.Header] != scall.Value: if isinstance(SurfaceLDE_dict[scall.Header], int): try: scall.IntegerValue = SurfaceLDE_dict[scall.Header] except Exception: scall.Value = str( SurfaceLDE_dict[scall.Header] ) # If fail, fall back to string input. else: if SurfaceLDE_dict[scall.Header] is not None: scall.Value = str(SurfaceLDE_dict[scall.Header])
[docs] def LDE_SetTiltDecenterOfSurface( self, in_Surface: int | ZOSAPI_Editors_LDE_ILDERow, BeforeSurfaceDecenterX: float = 0.0, BeforeSurfaceDecenterY=0.0, BeforeSurfaceTiltX: float = 0.0, BeforeSurfaceTiltY: float = 0.0, BeforeSurfaceTiltZ: float = 0.0, AfterSurfaceDecenterX: float = 0.0, AfterSurfaceDecenterY=0.0, AfterSurfaceTiltX: float = 0.0, AfterSurfaceTiltY: float = 0.0, AfterSurfaceTiltZ: float = 0.0, BeforeSurfaceOrder: str = "Decenter_Tilt", AfterSurfaceOrder: str = "Decenter_Tilt", ) -> ZOSAPI_Editors_LDE_ILDERow: """ Sets the tilt and decenter properties of a surface. Surface tilts and decenters allow a change in the coordinate system to be implemented both before and after ray tracing to the surface. Surface tilts and decenters are redundant with and very similar to coordinate breaks. A surface tilt/decenter can be thought of as a coordinate break, followed by the surface, followed by another coordinate break. The advantage to using surface tilts and decenters is the elimination of "dummy" coordinate break surfaces in the Lens Data Editor. This allows for a somewhat less cluttered display some users prefer. The disadvantage of using the surface tilts and decenters is the current implementation does not support optimization of surface tilt and decenter data. :param in_Surface: The surface to change as an object or as an index. :type in_Surface: Union[int, ZOSAPI_Editors_LDE_ILDERow] :param BeforeSurfaceDecenterX: Decenter in X before surface, defaults to 0.0 :type BeforeSurfaceDecenterX: float, optional :param BeforeSurfaceDecenterY: Decenter in Y before surface, defaults to 0.0 :type BeforeSurfaceDecenterY: float, optional :param BeforeSurfaceTiltX: Tilt in X before surface, defaults to 0.0 :type BeforeSurfaceTiltX: float, optional :param BeforeSurfaceTiltY: Tilt in Y before surface, defaults to 0.0 :type BeforeSurfaceTiltY: float, optional :param BeforeSurfaceTiltZ: Tilt in Z before surface, defaults to 0.0 :type BeforeSurfaceTiltZ: float, optional :param AfterSurfaceDecenterX: Decenter in X after surface, defaults to 0.0 :type AfterSurfaceDecenterX: float, optional :param AfterSurfaceDecenterY: Decenter in Y after surface, defaults to 0.0 :type AfterSurfaceDecenterY: float, optional :param AfterSurfaceTiltX: Tilt in X after surface, defaults to 0.0 :type AfterSurfaceTiltX: float, optional :param AfterSurfaceTiltY: Tilt in Y after surface, defaults to 0.0 :type AfterSurfaceTiltY: float, optional :param AfterSurfaceTiltZ: Tilt in Z after surface, defaults to 0.0 :type AfterSurfaceTiltZ: float, optional :param BeforeSurfaceOrder: Before surface order of decenters and tilts, defaults to 'Decenter_Tilt' :type BeforeSurfaceOrder: str, optional :param AfterSurfaceOrder: After surface order of decenters and tilts, defaults to 'Decenter_Tilt' :type AfterSurfaceOrder: str, optional :return: The object of the surface. :rtype: ZOSAPI_Editors_LDE_ILDERow """ SurfaceLDE = self._convert_raw_surface_input_(in_Surface, return_index=False) SurfaceLDE.TiltDecenterData.BeforeSurfaceOrder = self._CheckIfStringValidInDir_( self.ZOSAPI.Editors.LDE.TiltDecenterOrderType, BeforeSurfaceOrder ) SurfaceLDE.TiltDecenterData.AfterSurfaceOrder = self._CheckIfStringValidInDir_( self.ZOSAPI.Editors.LDE.TiltDecenterOrderType, AfterSurfaceOrder ) SurfaceLDE.TiltDecenterData.BeforeSurfaceDecenterX = BeforeSurfaceDecenterX SurfaceLDE.TiltDecenterData.BeforeSurfaceDecenterY = BeforeSurfaceDecenterY SurfaceLDE.TiltDecenterData.BeforeSurfaceTiltX = BeforeSurfaceTiltX SurfaceLDE.TiltDecenterData.BeforeSurfaceTiltY = BeforeSurfaceTiltY SurfaceLDE.TiltDecenterData.BeforeSurfaceTiltZ = BeforeSurfaceTiltZ SurfaceLDE.TiltDecenterData.AfterSurfaceDecenterX = AfterSurfaceDecenterX SurfaceLDE.TiltDecenterData.AfterSurfaceDecenterY = AfterSurfaceDecenterY SurfaceLDE.TiltDecenterData.AfterSurfaceTiltX = AfterSurfaceTiltX SurfaceLDE.TiltDecenterData.AfterSurfaceTiltY = AfterSurfaceTiltY SurfaceLDE.TiltDecenterData.AfterSurfaceTiltZ = AfterSurfaceTiltZ return SurfaceLDE
[docs] def LDE_SetTiltDecenterAfterSurfaceMode( self, in_Surface: int | ZOSAPI_Editors_LDE_ILDERow, mode: str | tuple[str, int] ) -> ZOSAPI_Editors_LDE_ILDERow: """ This function is a compliment function to :func:`LDE_SetTiltDecenterOfSurface` and sets the "After surface mode". After the surface, the same set of operations may be performed in either order. The values for the before and after decenters and tilts, and the order in which they are done, may be independent. However, it is frequently useful to have the after tilts and decenter values be related to previous before values. - "explicit" - "pickup" - "reverse" - ('pickup', surface[int, ZOSAPI_Editors_LDE_ILDERow]) - ('reverse', surface[int, ZOSAPI_Editors_LDE_ILDERow]) Reversing values from a prior surface involves changing the order and picking up the tilt and decenter values from the target surface and reversing the sign. The coordinate system resulting from the before and after tilt and decenters will define the coordinate system for the next surface. The thickness of a surface is the thickness in the new coordinate system after all the tilt and decenters are applied, measured along the resulting Z axis. :param in_Surface: The surface to change as an object or as an index. :type in_Surface: Union[int, ZOSAPI_Editors_LDE_ILDERow] :param mode: One of the modes listed above. :type mode: Union[str, tuple[str, Union[int, ZOSAPI_Editors_LDE_ILDERow]]] :return: The surface object :rtype: ZOSAPI_Editors_LDE_ILDERow """ SurfaceLDE = self._convert_raw_surface_input_(in_Surface, return_index=False) if isinstance(mode, str): if "explicit" in mode.lower(): SurfaceLDE.TiltDecenterData.SetAfterSurfaceModeExplicit() elif "pickup" in mode.lower(): SurfaceLDE.TiltDecenterData.SetAfterSurfaceModePickupThis() elif "reverse" in mode.lower(): SurfaceLDE.TiltDecenterData.SetAfterSurfaceModeReverseThis() else: cp( f"!@ly!@LDE_SetTiltDecenterAfterSurfaceMode :: mode [!@lm!@{mode!s}!@ly!@] not understood." ) elif isinstance(mode, tuple): try: if "pickup" in str(mode[0]).lower(): SurfaceLDE.TiltDecenterData.SetAfterSurfaceModePickup( self._convert_raw_surface_input_(mode[1], return_index=True) ) elif "reverse" in str(mode[0]).lower(): SurfaceLDE.TiltDecenterData.SetAfterSurfaceModeReverse( self._convert_raw_surface_input_(mode[1], return_index=True) ) else: cp( f"!@ly!@LDE_SetTiltDecenterAfterSurfaceMode :: mode [!@lm!@{mode[0]!s}!@ly!@] not understood." ) except Exception: cp( f"!@ly!@LDE_SetTiltDecenterAfterSurfaceMode :: Problem setting input [!@lm!@{mode!s}!@ly!@]." ) else: cp( f"!@ly!@LDE_SetTiltDecenterAfterSurfaceMode :: Expected [!@lm!@string!@ly!@] or [!@lm!@tuple!@ly!@] as input. Got [!@lm!@{type(mode)!s}!@ly!@]." )
[docs] def LDE_GetObjectRotationAndPositionMatrices( self, in_Surface: int | ZOSAPI_Editors_LDE_ILDERow ) -> tuple[np.ndarray, np.ndarray]: """ Looks up an LDE object and returns the object's rotation matrix and position information w/r to the global coordiante system. See Example 07. :param in_Surface: The surface to change as an object or as an index. :type in_Surface: Union[int, ZOSAPI_Editors_LDE_ILDERow] :return: tuple of object's ([x, y, z], R matrix) as np.ndarrays :rtype: tuple[np.ndarray, np.ndarray] """ in_Surface = self._convert_raw_surface_input_(in_Surface, return_index=True) global_transofmrations = self.TheSystem.LDE.GetGlobalMatrix(in_Surface) # After comapring with Zemax itself, GetGlobalMatrix() seems to return bad R coefficent values. Offsets seem okay still. # This is done through the operand 'GLCR' which only uses two input parameters: # the surface number, and the rotation matrix entry number. # The API call to get the operand needs 8 inputs, so we will use zeros as the dummies that don't matter. # The 3 x 3 R matrix has 9 components. If Data is 1, GLCR returns R[1][1], if Data is 2, GLCR returns R[1][2], etc... through Data = 9 returning R[3][3]. R = np.array( self.MFE_GetOperandValues( "GLCR", np.array([[in_Surface, x + 1, 0, 0, 0, 0, 0, 0] for x in range(9)]) ).reshape(3, 3) ) return np.array( [ global_transofmrations[-3], global_transofmrations[-2], global_transofmrations[-1], ] ), R
[docs] def LDE_RunRayTrace(self, ray_trace_rays: xr.Dataset = None) -> xr.Dataset: """ This funcion executes a sequential ray trace. There are differnt definitions for a sequential ray trace: - Normalized Unpolarized (NormUnpol) - Direct Unpolarized (DirectUnpol) - Normalized Polarized (NormPol) - Direct Polarized (DirectPol) The type of sequential ray trace which will be selected is determined by the :param ray_trace_rays: Infromation of rays which should be traced, defaults to None (will use :func:`LDE_BuildRayTraceNormalizedUnpolarizedRays` as default) :type ray_trace_rays: xr.Dataset, optional """ if ray_trace_rays is None: ray_trace_rays = self.LDE_BuildRayTraceNormalizedUnpolarizedRays() opened_batch_ray_trace = self.TheSystem.Tools.OpenBatchRayTrace() desired_ray_trace_call = _CheckIfStringValidInDir_( self, opened_batch_ray_trace, ray_trace_rays.attrs["ray_trace_type"], extra_include_filter=["Create"], extra_exclude_filter=["NSC"], ) if "CreateNormUnpol" in str(desired_ray_trace_call): ray_trace_rays = self._run_NormUnPol_raytrace_( opened_batch_ray_trace, desired_ray_trace_call, ray_trace_rays ) opened_batch_ray_trace.Close() return ray_trace_rays
[docs] def LDE_BuildRayTraceNormalizedUnpolarizedRays( self, Hx: np.ndarray = np.array([0]), Hy: np.ndarray = np.array([0]), Px: np.ndarray = np.cos(np.linspace(0, 2 * np.pi, 25, endpoint=False)), Py: np.ndarray = np.sin(np.linspace(0, 2 * np.pi, 25, endpoint=False)), ending_surface: int | ZOSAPI_Editors_LDE_ILDERow = None, do_all_surfaces_to_ending: bool = True, primary_wavelength: int | float | ZOSAPI_SystemData_IWavelength = None, wavelengths: int | float | ZOSAPI_SystemData_IWavelength | list[int, float, ZOSAPI_SystemData_IWavelength] | np.ndarray[int, float, ZOSAPI_SystemData_IWavelength] = None, ray_type: str = "Real", OPD_mode: str = "None", should_take_rays_one_to_one:bool=False, should_meshgrid_Hxy: bool = False, should_meshgrid_Pxy: bool = True, ) -> xr.Dataset: """ This function sets up custom `unpolarized` rays in Zemax's `normalized` coordiante system. These rays are intended to be used in an skZemax sequential ray trace executed with :func:`LDE_RunRayTrace`. In this system a single ray is defined by a starting position at the front of the optical system `(Hx, Hy)`, and a point on the entrence pupil to travel through `(Px, Py)`. A grid of rays - defined by `Hx` and `Hy` - are built in a normalized coordianate as input to the optical system. There are two normalized systems - set by :func:`Field_SetNormalization`: Radial: If the field normalization is radial, then the normalized field coordinates represent points on a unit circle. The radius of this unit circle, called the maximum radial field, is given by the radius of the field point farthest from the origin in field coordinates. The maximum radial field magnitude is then used to scale all fields to normalized field coordinates. Real field coordinates can be determined by multiplying the normalized coordinates, `Hx` and `Hy`, by the maximum radial field magnitude: :math:`f_x = (Hx)(F_r)` :math:`f_y = (Hy)(F_r)` where :math:`F_r` is the maximum radial field magnitude and :math:`f_x` and :math:`f_y` are the field coordinates in field units. Rectangular: If the field normalization is rectangular, then the normalized field coordinates represent points on a unit rectangle. The x and y direction widths of this unit rectangle, called the maximum x field and maximum y field, are defined by the largest absolute magnitudes of all the x and y field coordinates. The maximum x and y field magnitudes are then used to scale all fields to normalized field coordinates. Real field coordinates can be determined by multiplying the normalized coordinates :math:`f_x = (Hx)(F_x)` :math:`f_y = (Hy)(F_y)` where :math:`F_x` and :math:`F_x` are the x and y field magnitudes, and :math:`f_x` and :math:`f_y` are the field coordinates in field units. This function builds rays such that each - and every - ray defined in the `(Hx, Hy) grid` at the front is then traced through a positon of the entrance pupil defined also by a normalized system `(Px, Py)`. Unlike the `(Hx, Hy)` coordiantes, the entrnce pupil is always treated as radial: The normalized pupil coordinate (0.0, 1.0) is always at the top of the pupil, and therefore defines a marginal ray. The normalized pupil coordinate (0.0, 0.0) always goes through the center of the pupil, and therefore defines a chief ray. The radial size of the pupil is defined by the radius of the paraxial entrance pupil, unless ray aiming is turned on, in which case the radial size of the pupil is given by the radial size of the stop. This function will build rays for you in this system, based on the inputs of the `(Hx, Hy)` and `(Px, Py)` grids. In building both grids the radial constraints (if applicable) are handled for you and any input with a magnitude greater than 1 are filtered. A Final note on calulation of optical path difference (`OPD_mode`) - which can only be calculated when tracing rays all the way to the image surface: Computing the OPD takes additional time beyond that for regular ray tracing, and OpticStudio only performs this additional computation if requested to do so. Computing the OPD is also more complicated for the client program, which means two rays must be traced rather than just one. To compute OPD for an arbitrary ray, OpticStudio must trace the chief ray, then the arbitrary ray, then subtract the phase of the two to get the OPD. Rather than trace the same chief ray over and over, which is slow, usually the chief ray is traced once, and then the phase of the chief ray is subtracted from each subsequent ray. `None`: no OPD calculation will be performed. It will return the optical path as defined in the Single Ray Trace analysis. That value can differ from other analyses at infinite conjugates. `Current`: calculate the OPD based on the current ray and the previously computed chief ray. `CurrentAndChief`: will first compute the chief ray, and then calculate the OPD for the current ray. NOTE: Since Zemax does not support more than 24 wavelengths at once, the primary wavelength will be assigned as the first wavelength of the system and this function - along with running the ray trace itself (:func:`_run_NormUnPol_raytrace_`) - will alter the wavelengths of the system file. All wavelengths given here will be traced in batches as large as 24 (inlcuding the primary wavelength). The iniital wavelength settings of the file will be reset for you afterwards (within :func:`_run_NormUnPol_raytrace_`). :param Hx: An array of Hx points, defaults to np.array([0]) :type Hx: np.ndarray, optional :param Hy: An array of Hy points, defaults to np.array([0]) :type Hy: np.ndarray, optional :param Px: An array of Px points, defaults to np.cos(np.linspace(0, 2 * np.pi, 25, endpoint=False)) :type Px: np.ndarray, optional :param Py: An array of Py points, defaults to np.sin(np.linspace(0, 2 * np.pi, 25, endpoint=False)) :type Py: np.ndarray, optional :param ending_surface: The surface to trace the rays up to (object or as an index), defaults to None (takes the last surface) :type ending_surface: Union[int, ZOSAPI_Editors_LDE_ILDERow], optional :param do_all_surfaces_to_ending: If True will do ray trace for all surfaces up-to the ending one, else will only do the ending, defaults to True :type do_all_surfaces_to_ending: bool, optional :param primary_wavelength: Primary wavelength of the system to trace (object, index, or micrometers), defaults to None (takes whatever the current primary wavelength of the system is.) :type primary_wavelength: Union[int, float, ZOSAPI_SystemData_IWavelength], optional :param wavelengths: Wavelength(s) of the system to trace (object, index, or micrometers), defaults to None (takes primary wavelength). :type wavelengths: Union[int, float, ZOSAPI_SystemData_IWavelength, list[int, float, ZOSAPI_SystemData_IWavelength], np.ndarray[int, float, ZOSAPI_SystemData_IWavelength]], optional :param ray_type: Type of ray tracing to do. Options are "Real" or "Paraxial". "Paraxial" applies small angle approximations to snell's law for speed where "Real" does not, defaults to "Real" :type ray_type: str, optional :param OPD_mode: The type of OPD scheme to apply (see desciprtion above), defaults to 'None' :type OPD_mode: str, optional :param should_take_rays_one_to_one: If True the supplied Hx, Hy, Px, and Py must be the same length and each element will define a single ray. All H and P meshgridding will be disabled. If False, every Hx/Hy pair will be used with every Px/Py pair (multipule rays may be built from the same Hx/Hy position) and meshgridding may be used, defaults to False. :type should_take_rays_one_to_one: bool, optional :param should_meshgrid_Hxy: Ignored if should_take_rays_one_to_one==True. If True the two arrays of Hx and Hy will be used with np.meshgrid, else the two arrays will be taken as is to define the Hx and Hy points, defaults to False. :type should_meshgrid_Hxy: bool, optional :param should_meshgrid_Pxy: Ignored if should_take_rays_one_to_one==True. If True the two arrays of Px and Py will be used with np.meshgrid, else the two arrays will be taken as is to define the Px and Py points, defaults to True :type should_meshgrid_Pxy: bool, optional :return: An xarray of rays ready to be traced by :func:`LDE_RunRayTrace` :rtype: xr.Dataset """ if should_take_rays_one_to_one: should_meshgrid_Hxy=False should_meshgrid_Pxy=False if not np.all([len(x) == len(Hx) for x in [Hy, Px, Py]]): cp('!@lr!@LDE_BuildRayTraceNormalizedUnpolarizedRays :: Expecting Hx, Hy, Px, and Py inputs to expcitly define rays, but they are not the same length.') return None if ending_surface is None: ending_surface = self.LDE_GetNumberOfSurfaces() - 1 else: ending_surface = self._convert_raw_surface_input_( ending_surface, return_index=True ) # Configure the wavelnegths in batches of up to 24. the primary wavelength needs to be in each batch. if primary_wavelength is None: primary_wavelength = self.Wavelength_GetPrimaryWavelengthAsMicrometers() # Store initial wavelengths initial_system_wavelengths_um = ( self.Wavelength_GetAllSystemWavelengthsAsMicrometers() ) initial_system_weights = self.Wavelength_GetAllSystemWavelengthsWeights() initial_system_primary_wavelength_um = ( self.Wavelength_GetPrimaryWavelengthAsMicrometers() ) # Enforce new primary in the first index. self.Wavelength_RemoveAllButPrimaryWavelength() self.Wavelength_AddWavelength(primary_wavelength, 1.0) self.Wavelength_SetPrimaryWavelength(2) self.Wavelength_RemoveAllButPrimaryWavelength() # Sort out the wavelengths and their indices in batches. The primary wavelength will always be the first wavelength in the # wavelenth array. The wavelengths will be broken into batches with lens of [24, <=23, ..., <=23]. # This is because the primary wavelength must always be in the system and will be the 24th wavelenegth of all chuncks following the first. # However, we will not store the same information of the primary wavelength multipule times. if wavelengths is not None: if not isinstance(wavelengths, np.ndarray) and not isinstance( wavelengths, list ): wavelengths = np.array([wavelengths]) wavelengths_sorted = np.concat( [ np.array([primary_wavelength]), np.sort(wavelengths[wavelengths != primary_wavelength]), ] ) else: wavelengths_sorted = np.array([primary_wavelength]) chuncked_wavelengths_um = [wavelengths_sorted[0:24]] if len(wavelengths_sorted) > 24: chuncked_wavelengths_um += [ wavelengths_sorted[i : i + 23] for i in range(24, len(wavelengths_sorted), 23) ] # chuncked_wavelengths_idx = [np.arange(2, len(x)+2, 1) for x in chuncked_wavelengths_um] # chuncked_wavelengths_idx[0] = np.insert(chuncked_wavelengths_idx[0], 0, 1)[0:-1] # incorperate the primary wavelength in the first batch chunck_idx = [ np.ones_like(x) * idx for idx, x in enumerate(chuncked_wavelengths_um) ] def _check_bounds_(in_hx, in_hy, in_px, in_py): in_hx = in_hx[np.abs(in_hx) <= 1] in_hy = in_hy[np.abs(in_hy) <= 1] if "Rect" not in Field_GetNormalization(self): radius = np.sqrt(in_hx**2 + in_hy**2) in_hx = in_hx[radius <= 1] in_hy = in_hy[radius <= 1] # Normazlied pupil is always radial in_px = in_px[np.abs(in_px) <= 1] in_py = in_py[np.abs(in_py) <= 1] radius = np.sqrt(in_px**2 + in_py**2) in_px = in_px[radius <= 1] in_py = in_py[radius <= 1] return in_hx, in_hy, in_px, in_py if should_meshgrid_Hxy: HX, HY = np.meshgrid(Hx, Hy) else: HX = np.atleast_2d(Hx) HY = np.atleast_2d(Hy) if should_meshgrid_Pxy: PX, PY = np.meshgrid(Px, Py) else: PX = np.atleast_2d(Px) PY = np.atleast_2d(Py) HX, HY, PX, PY = _check_bounds_(HX, HY, PX, PY) if not should_take_rays_one_to_one: HXarray = np.repeat(HX, PX.shape[0]).astype(float) HYarray = np.repeat(HY, PX.shape[0]).astype(float) PXarray = np.tile(PX, HX.shape[0]).astype(float) PYarray = np.tile(PY, HX.shape[0]).astype(float) else: HXarray = HX HYarray = HY PXarray = PX PYarray = PY return xr.Dataset( { "Hx": ("ray", HXarray), "Hy": ("ray", HYarray), "Px": ("ray", PXarray), "Py": ("ray", PYarray), }, coords={ "wavelengths": ( "wvln", np.hstack(chuncked_wavelengths_um).astype(float), {"units": "microns"}, ), "ray_traceing_chunk_idx": ("wvln", np.hstack(chunck_idx).astype(int)), }, attrs={ "ray_trace_type": "NormUnpol", "ending_surface": int(ending_surface), "do_all_surfaces_to_ending": int(do_all_surfaces_to_ending), "initial_system_wavelengths_um": initial_system_wavelengths_um.astype( float ), "initial_system_weights": initial_system_weights.astype(float), "initial_system_primary_wavelength_um": float( initial_system_primary_wavelength_um ), "ray_traced_primary_wavelength_um": float(primary_wavelength), "ray_type": str( _CheckIfStringValidInDir_( self, self.ZOSAPI.Tools.RayTrace.RaysType, ray_type ) ), "OPD_mode": str( _CheckIfStringValidInDir_( self, self.ZOSAPI.Tools.RayTrace.OPDMode, OPD_mode ) ), }, )
def _run_NormUnPol_raytrace_( self, opened_batch_ray_trace: ZOSAPI_Tools_RayTrace_IBatchRayTrace, desired_ray_trace_call: CLR_MethodBinding, ray_trace_rays: xr.Dataset, ): """ Executes a Normalized Un-polarized Raytrace. This function is expected to be called only by :func:`LDE_RunRayTrace`. This particular worker function should be selected by the 'ray_trace_type' property in the xarray of rays to be traced. TODO: I think there is a bug in what Zemax returns through the RayTrae.dll. The X/Y/Z normals of the very last (image) surface are wrong and should probably be the same of the surface before(?). This is a probalem if only traceing the last surface and not the whole system. Solution TBD. :param opened_batch_ray_trace: The opened batch ray trace tool (output of self.TheSystem.Tools.OpenBatchRayTrace()) :type opened_batch_ray_trace: ZOSAPI_Tools_RayTrace_IBatchRayTrace :param desired_ray_trace_call: A callback to the ray interface object, in this case this should be the CreateNormUnpol() function. :type desired_ray_trace_call: CLR_MethodBinding :param ray_trace_rays: Infromation of rays which should be traced. In this case, an xarray formatted as :func:`LDE_BuildRayTraceNormalizedUnpolarizedRays` does. :type ray_trace_rays: xr.Dataset """ units = self.Utilities_GetAllSystemUnits() # Append properties to the ray_trace_rays xarray. if bool(int(ray_trace_rays.attrs["do_all_surfaces_to_ending"])): surfaces_to_trace = np.arange( 0, int(ray_trace_rays.attrs["ending_surface"]) + 1 ) else: surfaces_to_trace = np.array([int(ray_trace_rays.attrs["ending_surface"])]) dims = "('wvln', 'surf', 'ray')" blank_rays = np.zeros( ( ray_trace_rays.wavelengths.shape[0], surfaces_to_trace.shape[0], ray_trace_rays.ray.shape[0], ) ) ray_trace_rays = ray_trace_rays.assign_coords( {"surf": (("surf"), surfaces_to_trace.astype(int))} ) ray_trace_rays = ray_trace_rays.assign( {"error": (eval(dims), blank_rays.astype(bool))} ) ray_trace_rays = ray_trace_rays.assign( {"vignette": (eval(dims), blank_rays.astype(int))} ) ray_trace_rays = ray_trace_rays.assign( {"X": (eval(dims), blank_rays.astype(float), {"units": units["LensUnits"]})} ) ray_trace_rays = ray_trace_rays.assign( {"Y": (eval(dims), blank_rays.astype(float), {"units": units["LensUnits"]})} ) ray_trace_rays = ray_trace_rays.assign( {"Z": (eval(dims), blank_rays.astype(float), {"units": units["LensUnits"]})} ) ray_trace_rays = ray_trace_rays.assign( {"Xcosine": (eval(dims), blank_rays.astype(float))} ) ray_trace_rays = ray_trace_rays.assign( {"Ycosine": (eval(dims), blank_rays.astype(float))} ) ray_trace_rays = ray_trace_rays.assign( {"Zcosine": (eval(dims), blank_rays.astype(float))} ) ray_trace_rays = ray_trace_rays.assign( {"Xnormal": (eval(dims), blank_rays.astype(float))} ) ray_trace_rays = ray_trace_rays.assign( {"Ynormal": (eval(dims), blank_rays.astype(float))} ) ray_trace_rays = ray_trace_rays.assign( {"Znormal": (eval(dims), blank_rays.astype(float))} ) ray_trace_rays = ray_trace_rays.assign( {"angle_in": (eval(dims), blank_rays.astype(float), {"units": "degrees"})} ) ray_trace_rays = ray_trace_rays.assign( {"OPD": (eval(dims), blank_rays.astype(float), {"units": units["LensUnits"]})} ) ray_trace_rays = ray_trace_rays.assign( {"intensity": (eval(dims), blank_rays.astype(float))} ) ray_trace_rays = ray_trace_rays.assign( { "surface_comment": ( ("surf"), np.array( [ str(self.LDE_GetSurface(x).Comment) for x in range(surfaces_to_trace.shape[0]) ] ), ) } ) ray_trace_rays = ray_trace_rays.assign( { "pupil_apodization": ( ("ray"), np.array( [ float(self.TheSystem.LDE.GetApodization(x, y)) for x, y in zip( ray_trace_rays.Px.values, ray_trace_rays.Py.values, strict=False, ) ] ), ) } ) # The public class ReadNormUnpolData within BatchRayTrace.dll seems a little buggy after inspection. # One line in InitializeOutput does: maxSegments = (maxSegments) * (maxSegments); # I see absoltly no reason for this and think it is just a stright bug. # Also, within ReadNextBlock there is this: # int maxSeg = output.MaxSegments; # int incrementer = 0; # bool full = false; # while (!full) # { # if (incrementer > maxSeg) # { # break; # } # and incrementer indexes things like output.X[incrementer] = X; - which are indexed from zero. # So if incrementer == maxSeg then it will try to index outside of the buffer. # To account for all this I allocated (number of rays)*(number of wavelengths)*(number of surfaces)*BUFFER FACTOR and sqrt the value, and round up. BUFFER = int( np.ceil( np.sqrt( int(ray_trace_rays.ending_surface) * ray_trace_rays.wvln.shape[0] * ray_trace_rays.ray.shape[0] * 1.2 ) ) ) # Ensure primary wavelength is configured in the system. This should be the same um as the very first wavelength in ray_trace_rays. self.Wavelength_RemoveAllButPrimaryWavelength() self.Wavelength_AddWavelength( float(ray_trace_rays.ray_traced_primary_wavelength_um) ) self.Wavelength_SetPrimaryWavelength(2) for chunk_idx in list(set(ray_trace_rays.ray_traceing_chunk_idx.values)): # Now set the wavelengths of the ray trace by chunk (since Zemax can only support 23 + the primary) self.Wavelength_RemoveAllButPrimaryWavelength() if ray_trace_rays.wavelengths.shape[0] > 1: if chunk_idx == 0: [ self.Wavelength_AddWavelength(x) for x in ray_trace_rays.sel( ray_traceing_chunk_idx=chunk_idx ).wavelengths[1::] ] else: [ self.Wavelength_AddWavelength(x) for x in ray_trace_rays.sel( ray_traceing_chunk_idx=chunk_idx ).wavelengths ] wi = ray_trace_rays.ray_traceing_chunk_idx.values == chunk_idx number_of_wavelengths_in_chunk = self.Wavelength_GetNumberOfWavelengths() for surf_idx, surf in enumerate(surfaces_to_trace): ray_tracer = desired_ray_trace_call( ray_trace_rays.ray.shape[0] * number_of_wavelengths_in_chunk, _CheckIfStringValidInDir_( self, self.ZOSAPI.Tools.RayTrace.RaysType, ray_trace_rays.attrs["ray_type"], ), int(surf), ) dataReader = self.BatchRayTrace.ReadNormUnpolData( opened_batch_ray_trace, ray_tracer ) dataReader.ClearData() for wvlenidx in range(self.Wavelength_GetNumberOfWavelengths()): dataReader.AddRay( int(wvlenidx + 1), ray_trace_rays.Hx.values, ray_trace_rays.Hy.values, ray_trace_rays.Px.values, ray_trace_rays.Py.values, _CheckIfStringValidInDir_( self, self.ZOSAPI.Tools.RayTrace.OPDMode, ray_trace_rays.attrs["OPD_mode"], ), ) rayData = dataReader.InitializeOutput(BUFFER) isFinished = False totalSegRead = 0 def _reshape_chunk_(in_chunk_values: np.ndarray): if chunk_idx == 0: return in_chunk_values.reshape(number_of_wavelengths_in_chunk, -1) else: # don't return the primary wavelength return in_chunk_values.reshape(number_of_wavelengths_in_chunk, -1)[ 1::, : ] while not isFinished and rayData is not None: readSegments = dataReader.ReadNextBlock(rayData) if readSegments == 0: isFinished = True else: totalSegRead = totalSegRead + readSegments ray_trace_rays.error.values[wi, surf_idx, :] = _reshape_chunk_( _ctype_to_numpy_( self, rayData.errorCode, data_length=readSegments, data_type=np.int32, ).astype(bool) ) ray_trace_rays.vignette.values[wi, surf_idx, :] = _reshape_chunk_( _ctype_to_numpy_( self, rayData.vignetteCode, data_length=readSegments, data_type=np.int32, ).astype(int) ) ray_trace_rays.X.values[wi, surf_idx, :] = _reshape_chunk_( _ctype_to_numpy_( self, rayData.X, data_length=readSegments, data_type=np.double, ) ) ray_trace_rays.Y.values[wi, surf_idx, :] = _reshape_chunk_( _ctype_to_numpy_( self, rayData.Y, data_length=readSegments, data_type=np.double, ) ) ray_trace_rays.Z.values[wi, surf_idx, :] = _reshape_chunk_( _ctype_to_numpy_( self, rayData.Z, data_length=readSegments, data_type=np.double, ) ) ray_trace_rays.Xcosine.values[wi, surf_idx, :] = _reshape_chunk_( _ctype_to_numpy_( self, rayData.L, data_length=readSegments, data_type=np.double, ) ) ray_trace_rays.Ycosine.values[wi, surf_idx, :] = _reshape_chunk_( _ctype_to_numpy_( self, rayData.M, data_length=readSegments, data_type=np.double, ) ) ray_trace_rays.Zcosine.values[wi, surf_idx, :] = _reshape_chunk_( _ctype_to_numpy_( self, rayData.N, data_length=readSegments, data_type=np.double, ) ) ray_trace_rays.Xnormal.values[wi, surf_idx, :] = _reshape_chunk_( _ctype_to_numpy_( self, rayData.l2, data_length=readSegments, data_type=np.double, ) ) ray_trace_rays.Ynormal.values[wi, surf_idx, :] = _reshape_chunk_( _ctype_to_numpy_( self, rayData.m2, data_length=readSegments, data_type=np.double, ) ) ray_trace_rays.Znormal.values[wi, surf_idx, :] = _reshape_chunk_( _ctype_to_numpy_( self, rayData.n2, data_length=readSegments, data_type=np.double, ) ) ray_trace_rays.OPD.values[wi, surf_idx, :] = _reshape_chunk_( _ctype_to_numpy_( self, rayData.opd, data_length=readSegments, data_type=np.double, ) ) ray_trace_rays.intensity.values[wi, surf_idx, :] = _reshape_chunk_( _ctype_to_numpy_( self, rayData.intensity, data_length=readSegments, data_type=np.double, ) ) dataReader.ClearData() del ray_tracer ray_tracer = None del dataReader dataReader = None # Include pupile apodization for intensity ray_trace_rays.intensity.values = ( ray_trace_rays.intensity / ray_trace_rays.pupil_apodization ).values # Add global system variables global_transofmrations = np.array( [self.TheSystem.LDE.GetGlobalMatrix(int(x)) for x in ray_trace_rays.surf.values] ) # After comapring with Zemax itself, GetGlobalMatrix() seems to return bad R coefficent values. Offsets seem okay still. # Could go through self.LDE_GetObjectRotationAndPositionMatrices() instead, but this is direct. # This is done through the operand 'GLCR' which only uses two input parameters: # the surface number, and the rotation matrix entry number. # The API call to get the operand needs 8 inputs, so we will use zeros as the dummies that don't matter. # The 3 x 3 R matrix has 9 components. If Data is 1, GLCR returns R[1][1], if Data is 2, GLCR returns R[1][2], etc... through Data = 9 returning R[3][3]. R = np.array( [ self.MFE_GetOperandValues( "GLCR", np.array([[s, x + 1, 0, 0, 0, 0, 0, 0] for x in range(9)]) ).reshape(3, 3) for s in ray_trace_rays.surf.values ] ) transformer = xr.Dataset( { "Xo": ( "surf", global_transofmrations[:, -3].astype(float), {"units": units["LensUnits"]}, ), "Yo": ( "surf", global_transofmrations[:, -2].astype(float), {"units": units["LensUnits"]}, ), "Zo": ( "surf", global_transofmrations[:, -1].astype(float), {"units": units["LensUnits"]}, ), "R": (("surf", "row", "col"), R), "Vcosine": ( eval("('col'," + dims.strip("(")), np.array( [ ray_trace_rays.Xcosine.values, ray_trace_rays.Ycosine.values, ray_trace_rays.Zcosine.values, ] ), ), "Vnormal": ( eval("('col'," + dims.strip("(")), np.array( [ ray_trace_rays.Xnormal.values, ray_trace_rays.Ynormal.values, ray_trace_rays.Znormal.values, ] ), ), } ) cosine_glo = transformer.R.dot(transformer.Vcosine, dim="col") normal_glo = transformer.R.dot(transformer.Vnormal, dim="col") ray_trace_rays = ray_trace_rays.assign( { "X_global": ( eval(dims), (ray_trace_rays.X + transformer.Xo).values, {"units": units["LensUnits"]}, ) } ) ray_trace_rays = ray_trace_rays.assign( { "Y_global": ( eval(dims), (ray_trace_rays.Y + transformer.Yo).values, {"units": units["LensUnits"]}, ) } ) ray_trace_rays = ray_trace_rays.assign( { "Z_global": ( eval(dims), (ray_trace_rays.Z + transformer.Zo).values, {"units": units["LensUnits"]}, ) } ) ray_trace_rays = ray_trace_rays.assign( { "Xcosine_global": ( eval(dims), np.swapaxes(cosine_glo[:, 0, :, :].values, 0, 1), ) } ) ray_trace_rays = ray_trace_rays.assign( { "Ycosine_global": ( eval(dims), np.swapaxes(cosine_glo[:, 1, :, :].values, 0, 1), ) } ) ray_trace_rays = ray_trace_rays.assign( { "Zcosine_global": ( eval(dims), np.swapaxes(cosine_glo[:, 2, :, :].values, 0, 1), ) } ) ray_trace_rays = ray_trace_rays.assign( { "Xnormal_global": ( eval(dims), np.swapaxes(normal_glo[:, 0, :, :].values, 0, 1), ) } ) ray_trace_rays = ray_trace_rays.assign( { "Ynormal_global": ( eval(dims), np.swapaxes(normal_glo[:, 1, :, :].values, 0, 1), ) } ) ray_trace_rays = ray_trace_rays.assign( { "Znormal_global": ( eval(dims), np.swapaxes(normal_glo[:, 2, :, :].values, 0, 1), ) } ) # Find "Angle in". ray_trace_rays.angle_in.values = np.rad2deg( np.arccos( np.abs( ray_trace_rays.Xcosine.roll(surf=1) * ray_trace_rays.Xnormal + ray_trace_rays.Ycosine.roll(surf=1) * ray_trace_rays.Ynormal + ray_trace_rays.Zcosine.roll(surf=1) * ray_trace_rays.Znormal ) ) ).values ray_trace_rays.angle_in.values[np.isnan(ray_trace_rays.angle_in.values)] = 0.0 ray_trace_rays.angle_in.values[:, 0, :] = 0.0 # Reset the file's wavelengths and return self.Wavelength_RemoveAllButPrimaryWavelength() self.Wavelength_AddWavelength( float(ray_trace_rays.initial_system_primary_wavelength_um) ) self.Wavelength_SetPrimaryWavelength(2) self.Wavelength_RemoveAllButPrimaryWavelength() [ self.Wavelength_AddWavelength(float(x), float(y)) for x, y in zip( ray_trace_rays.initial_system_wavelengths_um, ray_trace_rays.initial_system_weights, ) if x != float(ray_trace_rays.initial_system_primary_wavelength_um) ] return ray_trace_rays.drop_vars("ray_traceing_chunk_idx") # raytrace = TheSystem.Tools.OpenBatchRayTrace(); # % GetDirectFieldCoordinates # % Result is the Boolean output, "X, Y, Z, L, M, N" are the "out double" variables as defined in the syntax guide # [result,X,Y,Z,L,M,N] = raytrace.GetDirectFieldCoordinates(wavenumber, ZOSAPI.Tools.RayTrace.RaysType.Real, hx, hy, px, py); # % GetPhase # % Result is the Boolean output, "exr, exi, eyr, eyi, ezr, ezi" are the "out double" variables as defined in the syntax guide # [exr,exi,eyr,eyi,ezr,ezi] = raytrace.GetPhase(L, M, N, jx, jy, xPhaseDeg, yPhaseDeg, intensity); # raytrace.Close();