mjhoptics / ray-optics

geometric ray tracing for optical systems
BSD 3-Clause "New" or "Revised" License
263 stars 54 forks source link

A lens system with aspherical elements #23

Closed dibyendumajumdar closed 3 years ago

dibyendumajumdar commented 3 years ago

Hi

Is it possible to now model lens systems that have aspheric surfaces? Is there an example available?

Thanks and Regards Dibyendu

mjhoptics commented 3 years ago

Hello @dibyendumajumdar, Yes you can use conic, even polynomials or even+odd polynomials. The mjhoptics/ray-optics-notebooks repo has a couple of examples. Cell Phone Lens.ipynb is a lens from a patent that has several high order aspherics. The Two Mirror Telescopes.ipynb file compares several telescope forms with different conic mirrors. The classes used are in rayoptics/elem/profiles.py Hope this helps.

dibyendumajumdar commented 3 years ago

Thank you.

dibyendumajumdar commented 3 years ago

Can glass types be specified just using refractive index / abbe number as they are in patent data? I have a hobby of converting photo lenses from patent data to tools like yours - it is more difficult to do so if you require a glass catalog to be pre-defined.

mjhoptics commented 3 years ago

@dibyendumajumdar, yes, you can. In the package rayoptics.seq.medium, there are several classes for modeling glasses. Try the class Glass; it takes index and v-number. HTH, MIke

dibyendumajumdar commented 3 years ago

That's great, thank you. Bill Claff maintains a lot of photo lens patent data at https://www.photonstophotos.net/GeneralTopics/Lenses/OpticalBench/OpticalBenchHub.htm, and I have a utility that converts from his data to various tools. I will try to add support for ray-optics, and share results I get.

mjhoptics commented 3 years ago

That sounds interesting, if some variation on existing inputs would really help, let me know. There is a website, http://www.lens-designs.com, that has many patent lenses, in zemax format. I've used this site as the source of files to test the zemax importer. I have a notebook that can walk the different categories on the site and generate a list of (hundreds) of zmx files. I did things like opening every 20th file and ploting the results. Quite a variety of lens forms. I need to clean it up and check it in.

dibyendumajumdar commented 3 years ago

According to Bill that site has lots of errors.. Bill maintains a list that matches production lenses.

mjhoptics commented 3 years ago

Patent descriptions aren't necessarily production prescriptions. If Bill has validated the designs on his site are production prescriptions, that's good to know.

dibyendumajumdar commented 3 years ago

Yes, the validation is basically matching the patent to schematic given by manufacturer - Bill tends to overlay the two.

dibyendumajumdar commented 3 years ago

Hi - I am doing one example manually - but I am kind of stuck. I can see when I layout the model that it doesn't look right.

This is my Jupyter Notebook: https://github.com/cameragossip/cameragossip.github.io/blob/master/lensdesigns/mjh-ray-optics-Nikkor-58mm-f0.95.ipynb

The patent I am trying to model is from https://patents.google.com/patent/WO2019229849A1/en

Bill Claff's version: https://www.photonstophotos.net/GeneralTopics/Lenses/OpticalBench/OpticalBench.htm#Data/WO2019-229849_Example01P.txt,figureOpacity=0.25,AxisO,OffAxis

I wasn't sure if I am entering the aspheric data correctly - the conic constant in the patent is described thus:

x = (h**2 / r) / [1+ {1− (1 + κ) · (h / r)**2 } 1/2 ]
+ A4h**4 + A6h**6 + A8h**8 + A10h**10 + A12h**12 + A14h**14
Here, h is the height in the direction perpendicular to the optical axis, 
x is the distance along the optical axis direction from the tangent 
plane of the apex of the aspheric surface at the height h to the 
aspheric surface, 
and κ is the conic constant. 

I think I need to use EvenPolynomial for this, is my understanding correct?

mjhoptics commented 3 years ago

It looks like the surface normal of the first aspheric is wrong. This may take a while to track down. Otherwise, your setup looks good.

dibyendumajumdar commented 3 years ago

Also the concave surfaces are not displaying properly - I am not sure if I am missing something.

mjhoptics commented 3 years ago

Because Ray failure occurs at the first element, estimates for clear apertures default to 1, so most of the elements are drawn as short squat elements. I’ll check the concavity in detail.

mjhoptics commented 3 years ago

Here are a few things to try to get a better lens layout. 1) Set paraxial apertures in the sequential model: for i, ifc in enumerate(sm.ifcs): sd = abs(pm.ax[i][0]) + abs(pm.pr[i][0]) ifc.set_max_aperture(sd)

2) Transfer the max aperture to the corresponding lens element: em = opm.ele_model for i, e in enumerate(em.elements): if isinstance(e, elements.Element): sd1 = sm.ifcs[e.s1_indx].max_aperture sd2 = sm.ifcs[e.s2_indx].max_aperture e.sd = max(sd1, sd2)

3) Draw the lens layout without the rays (do_draw_rays=False): layout_plt = plt.figure(FigureClass=InteractiveLayout, opt_model=opm, do_draw_rays=False, do_paraxial_layout=False, is_dark=isdark).plot()

output_18_0

Still looking at the actual problem...

dibyendumajumdar commented 3 years ago

Thank you. I was doing thus but not sure why it didn't work? The diameters below were computed by Bill Claff - I divided by 2 as I thought ray-optics expects semi-diameter - don't know if my understanding is correct.

elmn = [e for e in opm.ele_model.elements if isinstance(e, Element)]
elmn[0].sd = 66.8/2
elmn[1].sd = 65.82/2
#elmn[1].flat2 = 57.94/2
elmn[2].sd = 58.28/2
#elmn[2].flat1 = 58.28/2
elmn[3].sd = 65.32/2
elmn[4].sd = 70.90/2
elmn[5].sd = 65.0/2
elmn[6].sd = 61.06/2
elmn[7].sd = 59.42/2
elmn[8].sd = 50.24/2
elmn[9].sd = 49.92/2
elmn[10].sd = 49.92/2
elmn[11].sd = 51.12/2
elmn[12].sd = 46.8/2
elmn[13].sd = 45.36/2
elmn[14].sd = 39.46/2
elmn[15].sd = 39.46/2
elmn[16].sd = 38.94/2
elmn[17].sd = 44.3/2

image

dibyendumajumdar commented 3 years ago

Adding this helped:

for e in elmn: 
    sm.ifcs[e.s1_indx].max_aperture = e.sd
    sm.ifcs[e.s2_indx].max_aperture = e.sd

image

dibyendumajumdar commented 3 years ago

btw love that the type of glass is indicated by the color coding - is it just based on refractive index?

mjhoptics commented 3 years ago

Here are a few things to try to get a better lens layout. 1) Set paraxial apertures in the sequential model: for i, ifc in enumerate(sm.ifcs): sd = abs(pm.ax[i][0]) + abs(pm.pr[i][0]) ifc.set_max_aperture(sd)

2) Transfer the max aperture to the corresponding lens element: em = opm.ele_model for i, e in enumerate(em.elements): if isinstance(e, elements.Element): sd1 = sm.ifcs[e.s1_indx].max_aperture sd2 = sm.ifcs[e.s2_indx].max_aperture e.sd = max(sd1, sd2)

3) Draw the lens layout without the rays (do_draw_rays=False): layout_plt = plt.figure(FigureClass=InteractiveLayout, opt_model=opm, do_draw_rays=False, do_paraxial_layout=False, is_dark=isdark).plot()

output_18_0

Still looking at the actual problem...

mjhoptics commented 3 years ago

The elements have a mounting semi-diameter and the interfaces/surfaces have a clear aperture semi-diameter. If the surface is concave, I try to draw a flat between the surface semi-diameter and the element semi-diameter. For most of the surfaces, the ray trace data wasn't available because of the ray failures so the surface semi-diameters were set to 1. So the layout was drawing enormous flats on the elements with concave surfaces. Setting the surface semi-diameters as well as the elements avoided the flats being drawn. The color for the glass type is determined by where it is in the glass map, so both index and v-number. You can get a glass map figure as follows: import opticalglass.glassmap as gm gmf = plt.figure(FigureClass=gm.GlassMapFigure, glass_db=gm.GlassMapDB()).plot()

dibyendumajumdar commented 3 years ago

Is it possible to add an optional parameter to add_surface to set the max_aperture value?

dibyendumajumdar commented 3 years ago

Another question: is it essential to set PupilSpec? If I only have the F# as given by the patent then how can I set this? I tried doing it the way I saw in the example you mentioned before but maybe I did something wrong as it broke the system.

osp.pupil = PupilSpec(osp, key=['object', 'f/#'], value=0.98)
mjhoptics commented 3 years ago

Specify the f/# in image space, not object space, e.g. osp.pupil = PupilSpec(osp, key=['image', 'f/#'], value=0.98)

Adding the max_aperture to the add_surface fct is doable. Currently it takes a list for each surface. The list can contain 4 items: [radius, thickness, index, v-number] or, if air is the material, contains 2 items: [radius, thickness] I could add sd/max_aperture to the end of these as an option: [radius, thickness, index, v-number, max_aperture] [radius, thickness, max_aperture] Or, I could add a keyword argument, sd, so that the lists are unchanged. What do you think?

dibyendumajumdar commented 3 years ago

Specify the f/# in image space, not object space, e.g. osp.pupil = PupilSpec(osp, key=['image', 'f/#'], value=0.98)

Okay thank you

Adding the max_aperture to the add_surface fct is doable. Currently it takes a list for each surface. The list can contain 4 items: [radius, thickness, index, v-number] or, if air is the material, contains 2 items: [radius, thickness] I could add sd/max_aperture to the end of these as an option: [radius, thickness, index, v-number, max_aperture] [radius, thickness, max_aperture] Or, I could add a keyword argument, sd, so that the lists are unchanged. What do you think?

I guess keyword arg will be better, as it is optional I presume.

mjhoptics commented 3 years ago

There is code that automatically tries to set apertures based on ray tracing when update_model() is called. I need to make sure that that doesn't wipe out any user entered values. Hopefully I can do that without breaking something else.

dibyendumajumdar commented 3 years ago

Here is another lens - Leica 50mm f2 Summmicron: Auto generated from my utility - from Bill Claff's data.

Patent US 4,123,144

%matplotlib inline
isdark = False
from rayoptics.environment import *
from rayoptics.elem.elements import Element
opm = OpticalModel()
sm  = opm.seq_model
osp = opm.optical_spec
pm = opm.parax_model
osp.pupil = PupilSpec(osp, key=['image', 'f/#'], value=2)
osp.field_of_view = FieldSpec(osp, key=['object', 'angle'], flds=[0., 22.5])
osp.spectral_region = WvlSpec([(486.1327, 0.5), (587.5618, 1.0), (656.2725, 0.5)], ref_wl=1)
opm.system_spec.title = 'US 4,123,144'
opm.system_spec.dimensions = 'MM'
opm.radius_mode = True
sm.gaps[0].thi=1e10
sm.add_surface([42.71,3.99,1.7343,28.19])
sm.ifcs[-1].max_aperture = 14.47
sm.add_surface([195.38,0.2])
sm.ifcs[-1].max_aperture = 13.53
sm.add_surface([20.5,7.18,1.67133,41.64])
sm.ifcs[-1].max_aperture = 12.01
sm.add_surface([0,1.29,1.7919,25.55])
sm.ifcs[-1].max_aperture = 10.745
sm.add_surface([14.94,5.35])
sm.ifcs[-1].max_aperture = 9.195
sm.add_surface([0,7.61])
sm.set_stop()
sm.ifcs[-1].max_aperture = 9.0295
sm.add_surface([-14.94,1,1.65222,33.6])
sm.ifcs[-1].max_aperture = 8.75
sm.add_surface([0,5.22,1.79227,47.15])
sm.ifcs[-1].max_aperture = 9.635
sm.add_surface([-20.5,0.2])
sm.ifcs[-1].max_aperture = 10.19
sm.add_surface([0,3.69,1.79227,47.15])
sm.ifcs[-1].max_aperture = 11.48
sm.add_surface([-42.71,37.32])
sm.ifcs[-1].max_aperture = 11.985
sm.list_surfaces()
sm.list_gaps()
opm.update_model()
sm.list_model()
pm.first_order_data()
layout_plt = plt.figure(FigureClass=InteractiveLayout, opt_model=opm, do_draw_rays=True, do_paraxial_layout=False,
                        is_dark=isdark).plot()

image

dibyendumajumdar commented 3 years ago

I think the skew rays are going out of the bounds with this:

Nikkor 50mm f1.4 US 4448497

%matplotlib inline
isdark = False
from rayoptics.environment import *
from rayoptics.elem.elements import Element
opm = OpticalModel()
sm  = opm.seq_model
osp = opm.optical_spec
pm = opm.parax_model
osp.pupil = PupilSpec(osp, key=['image', 'f/#'], value=1.4)
osp.field_of_view = FieldSpec(osp, key=['object', 'angle'], flds=[0., 23])
osp.spectral_region = WvlSpec([(486.1327, 0.5), (587.5618, 1.0), (656.2725, 0.5)], ref_wl=1)
opm.system_spec.title = 'US 4448497'
opm.system_spec.dimensions = 'MM'
opm.radius_mode = True
sm.gaps[0].thi=1e10
sm.add_surface([86.771,9.8837,1.80218,44.7])
sm.ifcs[-1].max_aperture = 38
sm.add_surface([592.843,0.1938])
sm.ifcs[-1].max_aperture = 38
sm.add_surface([50.839,9.6899,1.78797,47.5])
sm.ifcs[-1].max_aperture = 32
sm.add_surface([75.826,2.907])
sm.ifcs[-1].max_aperture = 31
sm.add_surface([141.066,2.7132,1.6727,32.2])
sm.ifcs[-1].max_aperture = 30
sm.add_surface([34.981,16.6512])
sm.ifcs[-1].max_aperture = 25.5
sm.add_surface([0,13])
sm.set_stop()
sm.ifcs[-1].max_aperture = 24.709
sm.add_surface([-34.459,1.938,1.74,28.3])
sm.ifcs[-1].max_aperture = 25.4
sm.add_surface([9689.92,12.9845,1.77279,49.4])
sm.ifcs[-1].max_aperture = 29
sm.add_surface([-56.876,0.3876])
sm.ifcs[-1].max_aperture = 30.5
sm.add_surface([-179.516,8.1395,1.78797,47.5])
sm.ifcs[-1].max_aperture = 33.4
sm.add_surface([-60.444,0.1938])
sm.ifcs[-1].max_aperture = 33.9
sm.add_surface([277.19,5.4264,1.78797,47.5])
sm.ifcs[-1].max_aperture = 33
sm.add_surface([-331.492,74.1])
sm.ifcs[-1].max_aperture = 33
sm.list_surfaces()
sm.list_gaps()
opm.update_model()
sm.list_model()
pm.first_order_data()
layout_plt = plt.figure(FigureClass=InteractiveLayout, opt_model=opm, do_draw_rays=True, do_paraxial_layout=False,
                        is_dark=isdark).plot()

image

mjhoptics commented 3 years ago

You can apply vignetting to each field point with the function apply_paraxial_vignetting, e.g. from rayoptics.raytr.trace import apply_paraxial_vignetting apply_paraxial_vignetting(opm) Looks like you're making good progress with the lens import.

dibyendumajumdar commented 3 years ago

Cool, that works. Well yes the import is easy as to do - as long as I can find a way to specify everything. Which seems to be the case so far.

Please let me know if you are interested in having these in your notebook repo. I can submit PRs.

Is there a way to generate spot diagrams - both on axis and for skew rays?

mjhoptics commented 3 years ago

The simple way to do a spot diagram is: spot_plt = plt.figure(FigureClass=SpotDiagramFigure, opt_model=opm, scale_type=Fit.User_Scale, user_scale_value=0.1, is_dark=isdark).plot() I added a way to build up dashboards with more control over the constituent parts. You can see these in the "focus tilt notebook.ipynb" and "wavefront notebook.ipynb".

If you'd like to checkin your notebooks, that would be good. We could put them in a subdirectory for Bill Claff models.

dibyendumajumdar commented 3 years ago

The simple way to do a spot diagram is: spot_plt = plt.figure(FigureClass=SpotDiagramFigure, opt_model=opm, scale_type=Fit.User_Scale, user_scale_value=0.1, is_dark=isdark).plot()

Cool.

I added a way to build up dashboards with more control over the constituent parts. You can see these in the "focus tilt notebook.ipynb" and "wavefront notebook.ipynb".

Very nice - its amazing!

If you'd like to checkin your notebooks, that would be good. We could put them in a subdirectory for Bill Claff models.

They are really patent data - but Bill extracts them and puts them into a text format - the bit that he adds is standardized data, and he adds the diameters. Of course he has his own website / tool to show the lens schema, but his app is limited as it only does 2-d ray tracing based on refractive index - so it is not really meant to evaluate the lens design in any way.

I am interested in photo lenses that map to real production lenses, and these are a subset of the data maintained by Bill.

Happy to submit PRs. Most newer lenses have aspherics so I guess I should wait for the aspheric issue to be resolved before submitting those.

I don't mind what we call the subdirectory - you could call it 'PhotoLensPatents' and put a README that says where it is sourced from.

mjhoptics commented 3 years ago

Updated a fix for the aspheric ray trace problem. Just a note on the patent prescription. It uses the conic constant, rather than the eccentricity. Use the cc keyword for this, not the ec one.

dibyendumajumdar commented 3 years ago

I noticed the fix, so cloned the repo, and also used cc instead of ec. Looks better but still some issues:

I will submit a PR to your notebook repo - it may be easier to play around with it.

image

dibyendumajumdar commented 3 years ago

Btw I noticed when auto generating the notebook that if I set the aspherical profiles while adding surfaces, they are lost? So I need to add the profiles only after adding all the surfaces?

mjhoptics commented 3 years ago

It should work after the add_surface call (famous last words...)

I'm seeing the cover plate when using the data you uploaded initially. I'll look into the vignetting setting - doesn't work all the time for me either.

dibyendumajumdar commented 3 years ago

I was seeing the cover glass before too - but not in the latest above ...

dibyendumajumdar commented 3 years ago

@mjhoptics thank you for fixing the 58mm f0.95 notebook.

I submitted a PR for a few more examples - generally speaking I am still facing issues with the skew rays going outside of the apertures, and also the layout issues I mentioned before.

I saw a do_apertures flag - tried it but I got an error on setting this to False.

dibyendumajumdar commented 3 years ago

Hi @mjhoptics
I updated my clone of the repo with above, but I do not see a difference. For example:

image

mjhoptics commented 3 years ago

Add the line sm.do_apertures = False before the line opm.update_model()

dibyendumajumdar commented 3 years ago

Cool. thank you