Glass substitution and steepest descent optimizer
I've read the documentation and to me it looks like features like glass substituion (semi-automatic optimization for chromatism) and a fast (but inaccurate) optimizer (akin to the "Orthogonal Descent in Zemax") could be useful.
Is there any need for something like that? Or are there any plans to implement any of these features? My idea is mostly to start with;
-
Adding an
add_wavelengthsfunctionality adding multiple wavelengths at the same time for specified range (which should be useful for apochromats and designs for relatively unusual parts of the spectrum - my idea would be to use geometrically spaced Chebyshev nodes (by which I mean nodes in which logarithms of wavelengths are the Chebyshev nodes) - however an issue like that is limited to an odd number of wavelengths (as an even number there will be no 'primary' wavelength). Would an approach like that suffice or would you rather suggest omitting an functionality like that or choosing a different approach for that? -
Implementing a simple 'steepest descent' optimizer - that could also be done relatively easily. I guess using binary search to optimize the parameters one at a time in the loop would be ok-ish - something like that would be useful especially for quick parameter tweaking (like directly after the glass substitution to quickly compare different glass combinations without major design changes).
-
Implementing the glass substitution itself as another type of optimization - for now I think either substituting the materials one at a time (like in the 'steepest descent') optimizer and a brute-force optimizer comparing all possible combinations and choosing the most performant one (for systems with limited degrees of freedom) would work good enough - something more advanced (like reordering based on partial dispersion indices without testing the whole system) could be implemented later.
Is there any need for functionalities like these? I could submit a pull request for the add_wavelengths option already today if that would be of any use, as I've written some code (which I hope matches all of the recomendations) for my own use. Also if that went smoothly I could implement the two remaining features some time later, although I'm not sure if my code will be enough to justify expanding the codebase.
Thanks for reading the message and implementing what I think is a first actually useful open-source optical design software - I've been looking for something like that for quite some time already and honestly that one has easily exceeded my expectations.
Hi @kkrutkowski,
Thanks a lot for opening this issue! And thanks for the kind words about Optiland. I am glad you're finding it useful.
On your suggestions for features:
- The
add_wavelengthsfunctionality is probably too specific to include directly in theOpticclass. I definitely see the utility, but I want to keep that class as lean as possible. Nonethless, it would make sense as a standalone utility function, perhaps in a new or existing module likewavelength.py. Users could then choose to use it when relevant, and it could be extended later to support other wavelength sampling strategies if we want. - A steepest descent optimizer sounds very useful. I would happily accept a PR if you want to build this.
- The glass substitution feature would also be a great addition. It's something I've wanted to support for a while but hadn't settled on the right way to approach it. Your proposed method sounds like a good starting point, and I’d definitely be open to including it. Depending on how large the scope turns out to be, I’d be happy to collaborate or help with parts of it.
Thanks again for the thoughtful ideas. Looking forward to seeing what you come up with.
Regards, Kramer
- The
add_wavelengthsfunctionality is probably too specific to include directly in theOpticclass. I definitely see the utility, but I want to keep that class as lean as possible. Nonethless, it would make sense as a standalone utility function, perhaps in a new or existing module likewavelength.py. Users could then choose to use it when relevant, and it could be extended later to support other wavelength sampling strategies if we want.
Thank you for yours reply.
Do you see the add_wavelengths() function rather as a member function of the WavelengthGroup class (which would be likely the least complex way to implement it) or as a separate child class imported separately (which in my opinion yould be significantly more messy, but I just may just lack the skills to do so)? I guess that would also be required to add it as a separate file instead of appending it to wavelengths.py? I'll open the first pull request as soon, as I figure that out, but right now I have no idea what would be the most convenient way of implementing it.
For now, I suggest we keep the wavelength sampling functionality separate from core classes like Optic and WavelengthGroup. I recognize this is a bit more complex, but I think it leads to a cleaner and more modular design.
I think a simple function within wavelengths.py should suffice, then you could call, for example:
from optiland.wavelengths import chebyshev_nodes
wavelengths = chebyshev_nodes(min_value=0.4, max_value=0.7, num_wavelengths=7)
for w in wavelengths:
optic.wavelengths.add_wavelength(w) # this ignores setting a primary wavelength
Or we could provide a higher-level helper to directly apply sampled wavelengths to an optic or its wavelength group:
add_wavelengths(optic.wavelengths, strategy="chebyshev", min_value=0.4, max_value=0.7, num_wavelengths=7)
The strategy argument is forward-compatible: we can later support other options like "uniform", "logarithmic", or even "from_file" without touching the core classes.
This approach keeps the core classes focused & small while making more advanced tools available for the users who need them. I'm open to feedback on naming and structure, but the core idea is to decouple sampling logic from application logic (i.e., adding wavelengths), and to expose this functionality in an optional, modular way.
Thank you for yours suggestions. I'll soon start testing different configurations of the strategy/sampling parameter - from From what I've just read there should be at least 6 options in total (Chebyshev in the terms of wavelength/wavelenght ratios/frequency, uniform (wavelength and drequency) and exponential (only relative to wavelength, would be uniform for ratios)), at least if slightly larger function is not an issue. And I think the higher-level helper would probably be the best idea here (definitely the most user-friendly imho).
What do you think about the even numbers of num_wavelength? Should the function default to the nearest odd number larger than the specified value or just omit setting the primary frequency? There are also some modified Chebyshev nodes with double node at 0, but generally here they would be nearly useless (as they would just shift the effective points towards edges of the interval), so I think the third option can be rejected at this point.
EDIT; Or we could just add a fourth, optional parameter stating the power of the wavelength in which the nodes should be applied. That way 1 would apply the nodes in the wavelength domain, while -1 would be equivalent to applying them in the frequency domain, while keeping the logic less convoluted.
Also just now I've had an idea the -1 could be a reasonable default option, as under the assumption that the refraction index follows the two-term form of the Cautchy's equation (which doesn't lie that far from truth for most applications) should minimize the maximum chromatic error within given range for a selected number of nodes.
EDIT2; Or the standard Chebyshev nodes could be limited to the -1 only, as it's expected to be near-linear spacing. Also in case like that the linear spacing would be limited to linear in frequency (and not in wavelength?). For exponential or exp-chebyshev it would make no difference here. And only 4 options instead of 5 or 6 (and probably the least confusing) from the user's perspective.
Thanks, these are great insights. I like the direction.
I agree that a higher-level helper is the right approach, and we can expand it to support multiple strategies. We don't necessarily need to add all the sampling options in a single PR. That might make things more manageable, but I leave that up to you.
I’m not a huge fan of using a power parameter. It’s somewhat ambiguous, especially given its established meaning in optics. I'd rather use something like domain="wavelength" or "frequency" to make intent explicit.
For the num_wavelengths, I think we should raise an error if it’s even and the strategy expects an odd number. That’s simpler and avoids any hidden logic or unexpected results.
If this grows much further, we might want to move it into its own subpackage (e.g. optiland/wavelength/) to keep things modular and clean. That refactor, if it's needed, can be another PR.
Thanks again - this is a very useful and thoughtful addition.
Hi @kkrutkowski, just to let you know the glass substitution algorithm has been implemented in PR #211. Please take a look at Tutorial 7e. Best, drpaprika