.. currentmodule:: rayoptics

#############################
ThorLabs catalog lens example
#############################

This notebook shows the steps to follow to open a Zemax zmx file of a ThorLabs aspheric singlet

.. code:: ipython3

    %matplotlib inline

.. code:: ipython3

    # initialization
    from rayoptics.environment import *

Use the object oriented filesystem interface from Python 3
----------------------------------------------------------

.. code:: ipython3

    root_pth = Path(rayoptics.__file__).resolve().parent

Read Zemax .zmx file for ThorLabs part 354710-C
-----------------------------------------------

Use the :func:`~.gui.appcmds.open_model` function to read Zemax **.zmx** files, CODE V **.seq** files and the native rayoptics JSON files, **.roa**

It returns an instance of :class:`~.opticalmodel.OpticalModel` that contains all of the model data.

Use the info keyword argument to get additional information about the details of the imported file.

The webpage for the 354710-C aspheric lens is `here <https://www.thorlabs.com/thorproduct.cfm?partnumber=354710-C>`_ 

.. code:: ipython3

    opm, info = open_model(root_pth/"zemax/tests/354710-C-Zemax(ZMX).zmx", info=True)

info is a tuple of the following items:

    - the track_contents dict 
    - glasses not found dict

.. code:: ipython3

    info




.. parsed-literal::

    ({'VERS': '140124 258 36214',
      'pupil': ('aperture', 'object', 'pupil'),
      'FTYP': '0 0 1 1 0 0 0',
      'STANDARD': 5,
      'EVENASPH': 1,
      'CONI': 1,
      'conj type': 'infinite',
      '# surfs': 6,
      '# wvls': 1,
      'fov': ('field', 'object', 'angle'),
      '# fields': 1,
      'GCAT': ['LIGHTPATH', 'SCHOTT'],
      'glass not found': 1,
      'glass substituted': 1,
      'encoding': 'utf-16'},
     {'D-ZK3M': 1, 'BK7': "create_glass('N-BK7','Schott')"})



The second dict indicates that there were two glasses in the model file that are not in the glass catalog set that ray-optics uses from the opticalglass package. There is a file called 354710-C-Zemax(ZMX)_tmpl.smx that contains the glasses not found dict dict.

One is a substitute material. One of the glasses was named BK7, and that's not an exact match in the current Schott catalog. But there is a similar glass N-BK7, and what the import process did was substitute that glass for the BK7 glass. 

The other glass here isn't in any of the ray-optics catalogs, and it apparently is from the LightPath catalog, as indicated by the 'GCAT' key in the track contents dict. A web search leads to this `D-ZK3M datasheet <https://www.amstechnologies-webshop.com/media/pdf/e9/04/26/D-ZK3.pdf>`_. 

One thing of importance is what wavelength this material is going to be used. The usual convenient way of specifying an optical glass with the index and V number is really only adequate in the visible wavelengths. In this case, the lens file is going to be used in the infrared, at 1550 nanometers. Fortunately there are adequate data points for the material definition in the IR.

The best approach to using the available data is to interpolate refractive index values from the refractive index table in the datasheet. There is a material model in the :mod:`~.seq.medium` package called :class:`~.seq.medium.InterpolatedGlass` that takes a glass name, a catalog name, and a list of wavelength/refractive index pairs. The template .smx file is edited with new InterpolatedGlass definition for D-ZK3M and saved with the same name as the .zmx file but with a .smx extender. The template file can be deleted, because it's no longer needed.

The name of this file should be the same as the name of the original Zemax file. But instead of an extender of zmx use an extender of smx, so when we do that, we now have an smx file right next to it. We can delete the template file, because that's no longer needed.

Open the model again.

.. code:: ipython3

    opm, info = open_model(root_pth/"zemax/tests/354710-C-Zemax(ZMX).zmx", info=True)

.. code:: ipython3

    info




.. parsed-literal::

    ({'VERS': '140124 258 36214',
      'pupil': ('aperture', 'object', 'pupil'),
      'FTYP': '0 0 1 1 0 0 0',
      'STANDARD': 5,
      'EVENASPH': 1,
      'CONI': 1,
      'conj type': 'infinite',
      '# surfs': 6,
      '# wvls': 1,
      'fov': ('field', 'object', 'angle'),
      '# fields': 1,
      'GCAT': ['LIGHTPATH', 'SCHOTT'],
      'encoding': 'utf-16'},
     {'BK7': "create_glass('N-BK7','Schott')",
      'D-ZK3M': "InterpolatedGlass('D-ZK3M', cat='LightPath', pairs=[(2352.4, 1.555),(1970.1, 1.561),(1529.6, 1.568),(1128.6, 1.573),(1014.0, 1.575),(852.1, 1.578),(706.5, 1.582),(656.3, 1.583),(643.8, 1.584),(632.8, 1.584),(589.3, 1.586),(587.6,1.586),(546.1, 1.589),(486.1, 1.593),(480.0, 1.594),(435.8, 1.598),(404.7, 1.602),(365.0, 1.610)])"})



Examining the info, The InterpolatedGlass definition for D-ZK3M is being used as well as the N-BK7 substitution for BK7. All of the flags for glass not found or glass substituted have been resolved.

Setup convenient aliases for using rayoptics functions

.. code:: ipython3

    sm = opm['seq_model']
    osp = opm['optical_spec']
    pm = opm['parax_model']

.. code:: ipython3

    osp['wvls'].wavelengths




.. parsed-literal::

    [1550.0]



.. code:: ipython3

    sm.list_model()


.. parsed-literal::

                 c            t        medium     mode   zdr      sd
     Obj:     0.000000  1.00000e+10       air             1      0.0000
    Stop:     1.182174     0.862527    D-ZK3M             1     0.75000
       2:     0.000000     0.523243       air             1     0.57417
       3:     0.000000     0.250000     N-BK7             1     0.24917
       4:     0.000000     0.249999       air             1     0.15527
     Img:     0.000000      0.00000                       1  8.9134e-06


Display first order properties of the model
-------------------------------------------

The calculated first order data is in the :class:`~.firstorder.FirstOrderData` class.
An instance of :class:`~.FirstOrderData`, :attr:`~.opticalspec.OpticalSpecs.parax_data`, is managed by the :class:`~.opticalspec.OpticalSpecs` class.

Other essential optical specification data is also managed by the :class:`~.opticalspec.OpticalSpecs` class:

    - spectral_region (:class:`~.opticalspec.WvlSpec`)
    - pupil (:class:`~.opticalspec.PupilSpec`)
    - field_of_view (:class:`~.opticalspec.FieldSpec`)
    - defocus (:class:`~.opticalspec.FocusRange`)

A convenience method in :class:`~.ParaxialModel`, :meth:`~.paraxialdesign.ParaxialModel.first_order_data`, can be used to display the first order properties of the model.

.. code:: ipython3

    pm.first_order_data()


.. parsed-literal::

    efl                1.49
    ffl               -1.49
    pp1                  -0
    bfl              0.2499
    ppk                1.24
    f/#              0.9933
    m             -1.49e-10
    red          -6.712e+09
    obj_dist          1e+10
    obj_ang               1
    enp_dist             -0
    enp_radius         0.75
    na obj          7.5e-11
    n obj                 1
    img_dist         0.2499
    img_ht          0.02601
    exp_dist          -1.24
    exp_radius         0.75
    na img          -0.4496
    n img                 1
    optical invariant      0.01309


Generate a lens picture
-----------------------

This is done using the :mod:`.interactivelayout` module.
All graphics in rayoptics are based on matplotlib.

.. code:: ipython3

    layout_plt = plt.figure(FigureClass=InteractiveLayout, opt_model=opm,
                            do_draw_rays=True, do_paraxial_layout=False).plot()



.. image:: output_21_0.png


Draw a transverse ray aberration plot
-------------------------------------

This is done using the :mod:`.axisarrayfigure` module.

.. code:: ipython3

    abr_plt = plt.figure(FigureClass=RayFanFigure, opt_model=opm, data_type='Ray',
                         scale_type=Fit.All_Same).plot()



.. image:: output_23_0.png


The model from ThorLabs only had 1 wavelength defined. Use the :class:`~.opticalspec.OpticalSpecs` instance, `osp`, to modify the :attr:`~.opticalspec.OpticalSpecs.spectral_region` in the optical subpackage to add wavelengths in the red and blue. The wavelenghts can be specified directly in nm or by using spectral line designations.

.. code:: ipython3

    osp['wvls'].set_from_list([[1050., 1], [1550., 2], [1700., 1]])
    osp['wvls'].reference_wvl = 1

.. code:: ipython3

    osp['wvls'].wavelengths




.. parsed-literal::

    [1050.0, 1550.0, 1700.0]



After changing the wavelengths, update the optical model using :meth:`~.OpticalModel.update_model` to ensure all of the data is consistent.
The :class:`~.opticalmodel.OpticalModel` class is in the :mod:`~.opticalmodel` module in the :mod:`~.optical` subpackage.

.. code:: ipython3

    opm.update_model()

The aberration plot can be updated by calling :meth:`~.axisarrayfigure.AxisArrayFigure.refresh` on abr_plt

.. code:: ipython3

    abr_plt.refresh()




.. image:: output_30_0.png



List the model, and use an alternate model listing command, list_sg, for "list surface and gap". This is useful because it will allow us to look at element decenters or surface decenters in our model.

.. code:: ipython3

    sm.list_model()


.. parsed-literal::

                 c            t        medium     mode   zdr      sd
     Obj:     0.000000  1.00000e+10       air             1      0.0000
    Stop:     1.182174     0.862527    D-ZK3M             1     0.75000
       2:     0.000000     0.523243       air             1     0.57417
       3:     0.000000     0.250000     N-BK7             1     0.24917
       4:     0.000000     0.249999       air             1     0.15527
     Img:     0.000000      0.00000                       1  8.9134e-06


.. code:: ipython3

    sm.list_sg()


.. parsed-literal::

                  c               mode        type        y       alpha
                          t           medium
     Obj:      0.00000                 
                    1.00000e+10          air
    Stop:      1.18217                 
                       0.862527       D-ZK3M
       2:      0.00000                 
                       0.523243          air
       3:      0.00000                 
                       0.250000        N-BK7
       4:      0.00000                 
                       0.249999          air
     Img:      0.00000                 


So we just take a quick look at what the actual surface definition is. We see the first surface has an even polynomial: a sphere with a conic constant, and aspheric coefficients, up to 12th order, and it's a transmitting surface.

.. code:: ipython3

    repr(sm.ifcs[1])




.. parsed-literal::

    'Surface(profile=EvenPolynomial(c=1.1821736792829385, cc=-0.4776343430417, coefs=[0.0, -0.006313587842251, -0.009394960901464, -0.01707674864971, 0.008070222726967, -0.02139444912229, 0.0, 0.0]), interact_mode=transmit)'



Now add decentered surfaces to both the first and the second surface of the lens element. These surfaces are defined as a local decenter type and a reverse decenter. A local decenter applies a translation first, and then a rotation, wheras the reverse decenter applies the reverse: the inverse of the rotation matrix, and then subtracting the translation. 

.. code:: ipython3

    sm.ifcs[1].decenter = srf.DecenterData(DecenterType.LOCAL)
    sm.ifcs[2].decenter = srf.DecenterData(DecenterType.REV)

Listing the model shows the decentered parameters.

.. code:: ipython3

    sm.list_sg()


.. parsed-literal::

                  c               mode        type        y       alpha
                          t           medium
     Obj:      0.00000                 
                    1.00000e+10          air
    Stop:      1.18217                       LOCAL     0.0000     0.0000
                       0.862527       D-ZK3M
       2:      0.00000                         REV     0.0000     0.0000
                       0.523243          air
       3:      0.00000                 
                       0.250000        N-BK7
       4:      0.00000                 
                       0.249999          air
     Img:      0.00000                 


The decenter is defined by a translation, the tilt is defined by alpha, beta, and gamma euler angles. The euler angles will be converted into a rotation matrix. So, this information gives you a translation and a rotation matrix.

.. code:: ipython3

    sm.ifcs[1].decenter




.. parsed-literal::

    'LOCAL': Decenter: array([0., 0., 0.]), Tilt: array([0., 0., 0.])



To laterally offset the lens element in the y direction, say by 0.2 millimeters, set the second element of the translation vector of each of the surfaces to 0.2. And because the second surface is a reverse decenter we'll see that the optical axis comes back to the original position. So now we've decentered the surfaces we see that the y component value is 0.2. 

.. code:: ipython3

    sm.ifcs[1].decenter.dec[1] = .2
    sm.ifcs[2].decenter.dec[1] = .2

.. code:: ipython3

    sm.list_sg()


.. parsed-literal::

                  c               mode        type        y       alpha
                          t           medium
     Obj:      0.00000                 
                    1.00000e+10          air
    Stop:      1.18217                       LOCAL    0.20000     0.0000
                       0.862527       D-ZK3M
       2:      0.00000                         REV    0.20000     0.0000
                       0.523243          air
       3:      0.00000                 
                       0.250000        N-BK7
       4:      0.00000                 
                       0.249999          air
     Img:      0.00000                 


When perturbing the lens position, we want the ray bundle to stay directed along the axis. So we want to separate the definition of the stop surface from the first surface, and put the stop on a dummy plane in front of the lens element. So when the lens element is laterally shifted, the stop plane, which is what the rays are aimed at, will stay the same.

Add a dummy plane, following the object surface. I'll list the model, and we see here, the dummy plane, and the current surface should be the surface that we did the insert at so that's surface one, and we want to set the stop to the current surface. And when we redo the listing, we see the stop is moved to the dummy plane. We have laterally offset the second and third surfaces by an amount of 0.2, 

.. code:: ipython3

    opm.add_dummy_plane(idx=0)

.. code:: ipython3

    sm.list_sg()


.. parsed-literal::

                  c               mode        type        y       alpha
                          t           medium
     Obj:      0.00000                 
                    1.00000e+10          air
       1:      0.00000                 
                        0.00000          air
    Stop:      1.18217                       LOCAL    0.20000     0.0000
                       0.862527       D-ZK3M
       3:      0.00000                         REV    0.20000     0.0000
                       0.523243          air
       4:      0.00000                 
                       0.250000        N-BK7
       5:      0.00000                 
                       0.249999          air
     Img:      0.00000                 


.. code:: ipython3

    sm.cur_surface




.. parsed-literal::

    1



.. code:: ipython3

    sm.set_stop()




.. parsed-literal::

    1



.. code:: ipython3

    sm.list_sg()


.. parsed-literal::

                  c               mode        type        y       alpha
                          t           medium
     Obj:      0.00000                 
                    1.00000e+10          air
    Stop:      0.00000                 
                        0.00000          air
       2:      1.18217                       LOCAL    0.20000     0.0000
                       0.862527       D-ZK3M
       3:      0.00000                         REV    0.20000     0.0000
                       0.523243          air
       4:      0.00000                 
                       0.250000        N-BK7
       5:      0.00000                 
                       0.249999          air
     Img:      0.00000                 


Now update the model and redraw the layout. Note that the lens element has shifted upward by 0.2 millimeters. The original ray fan is is unperturbed in object space but note that it gets deflected off axis, and in fact the aberration is large enough that flare is visible, even on the lens layout.

.. code:: ipython3

    opm.update_model()

.. code:: ipython3

    layout_plt.refresh()




.. image:: output_53_0.png



Now refresh the aberration plot to see what the aberrations look like now. Notice the scale size is now about two and a half times larger than it was just previously. Across the center of the aperture you're pretty well corrected but there is some flare occurring at the lower edge of the aperture. 

.. code:: ipython3

    abr_plt.refresh()




.. image:: output_55_0.png



