optiland icon indicating copy to clipboard operation
optiland copied to clipboard

`norm_radius` is overwritten by `OpticUpdater`

Open crnh opened this issue 4 months ago • 7 comments

For polynomial geometries, the normalization radius can be specified using the norm_radius or norm_x and norm_y parameters. For some geometries, e.g. the Zernike geometry, it makes sense to specify this radius manually. However, when OpticUpdater performs an update, this value is set to 1.25 times the surface aperture. This happens when calling Optic.info; Optic.info calls OpticUpdater.update_paraxial which in turn calls update_normalization. In the case of Zernike surfaces, this results in a norm_radius that is often too small, causing calculations to fail after printing the system information.

Questions:

  • [ ] Why is the normalization radius always updated? Only to make sure it is set, or are there other reasons?
  • [ ] Can the update behavior be modified so manually specified normalization radii are respected?

Checklist

  • [x] I have searched the existing issues and discussions for a similar question or problem.
  • [x] I have read the documentation and tried to find an answer there.
  • [x] I am using the latest version of Optiland (if not, please update and retry).
  • [x] I have tried to reproduce or debug the issue myself before opening this.
  • [x] I have included all necessary context, such as version info, error messages, or minimal reproducible examples.

Thanks for taking the time to go through this — it really helps us help you!

Environment

  • Optiland Version: 0.5.5
  • Python Version: 3.12
  • OS: [Windows]
  • Additional Info:

crnh avatar Aug 12 '25 11:08 crnh

Hi @crnh,

This was a design decision by @drpaprika. Essentially, the normalization is set like this because it's more robust for optimization. It's a common approach from the literature - see references that drpaprika shared below (from here). Regarding you second question, I think it makes sense to do this, especially if a user requires a fixed normalization radius for a surface. I'd expect a relatively simple condition could be added so user-defined normalization radii are not updated. Or, perhaps a flag could be added to enable or disable norm radii updates for a surface. Do you want to implement this? We can also add it to the to do list and get to it before long.

Here are a two publications that discuss this:

  1. On-the-fly surface manufacturability constraints for freeform optical design by Kevin P. Thompson (2019) DOI

For all designs, the normalization radius of each freeform surface was set to enclose the size of the effective aperture during optimization.

  1. Exit pupil quality analysis and optimization in freeform afocal telescope systems by Aaron Bauer (2023) DOI

One critical aspect of optimizing with orthogonal polynomials is handling the normalization radius. Our preferred method is to let the normalization radius vary in optimization and constrain its value always to be 5% larger than the semi-diameter of the optical surface. This technique allows the system to continuously change form without encountering scenarios where the surface semi-diameters become larger than the normalization radius, which can cause raytrace errors.

That's why I included a call to update_paraxial() at each optimization cycle. I hope that helps, drapaprika

Regards, Kramer

HarrisonKramer avatar Aug 13 '25 13:08 HarrisonKramer

Btw, on this topic,

Since I will be using the Forbes surface type a lot, I want its normalization radius to be updated and varied through optimization.

My approach was the following (coming in the Forbes PR #258):

class NormalizationRadiusVariable(VariableBehavior):
    """
    Represents a variable for the normalization radius of a surface.
    """

    def __init__(self, optic, surface_number, apply_scaling=True, **kwargs):
        super().__init__(optic, surface_number, apply_scaling, **kwargs)
        self.optic.surface_group.surfaces[
            self.surface_number
        ].is_norm_radius_variable = True

    def get_value(self):
        """Returns the current value of the normalization radius."""
        surf = self._surfaces.surfaces[self.surface_number]
        if hasattr(surf.geometry, "norm_radius"):
            return surf.geometry.norm_radius
        else:
            raise AttributeError(
                f"Geometry for surface {self.surface_number} "
                "does not have a 'norm_radius' attribute."
            )

    def update_value(self, new_value):
        """Updates the value of the normalization radius."""
        self.optic.set_norm_radius(new_value, self.surface_number)

    def __str__(self):
        return f"Normalization Radius, Surface {self.surface_number}"


I just set a flag to check whether or not the normalization radius is to be considered as variable, and go from there.

manuelFragata avatar Aug 13 '25 13:08 manuelFragata

Essentially, the normalization is set like this because it's more robust for optimization.

Thanks for the explanation. I read @drpaprika's explanation as well and understand why this is necessary. However, it causes a parameter to be silently overwritten even if explicitly specified. This results in an unreliable interface. In this specific case, it also breaks the surface sag viewer; plotting the surface sag doesn't work after printing the system information.

Furthermore, in some optimization scenarios I don't want the normalization radius to be updated. For example, there are ophthalmic measurement devices that measure the topography of the cornea of the eye and report it as Zernike coefficients over a certain radius. If I am using this output in an optical eye model, I don't want the normalization radius to scale with the aperture during optimization.

I have encountered similar issues when working on other libraries. If the user chooses to explicitly specify the default argument, it is basically impossible to know whether the normalization radius was explicitly specified or not. Disabling this behavior when the normalization is explicitly specified would therefore require more than a simple check. Furthermore, we would need to implement a mechanism to re-enable this behavior when desired.

I currently do not have the time to investigate this, but I think it would be good to

  • Not update the normalization radius as part of printing the system information and other actions that should not modify the system;
  • Only vary the normalization radius during optimization;
  • And make the latter optional.

crnh avatar Aug 13 '25 13:08 crnh

Agreed. We can use this issue to track progress on this.

I will think about a more robust solution here. We should just be explicit, so it's crystal clear to users the behavior to expect. I think requiring users to explicitly choose to allow the norm radius to vary during optimization makes sense.

Regards, Kramer

HarrisonKramer avatar Aug 13 '25 14:08 HarrisonKramer

Hi @crnh,

After considering options here, I think there are two reasonable approaches:

1. Make norm_radius a new optimization "variable"

This forces the user to explicitly set norm_radius as variable prior to optimization. Any other updates to norm_radius outside of optimization would then be removed, i.e., Optic.info would no longer change the norm_radius. This would operate slightly differently than a typical variable in the sense that it only allows updates based on the semi-aperture diameter during optimization. We might need to have two "modes" here - one for a traditional variable type (updates based on optimizer) and one based simply on the semi-aperture diameter - TBD.

2. Set up a state-based configuration for norm_radius

Here, we could create a state for norm_radius that the user must configure. For example, we could have two states, AUTO and FIXED. The AUTO state is essentially the current behavior, wherein the value is automatically updated depending on the semi-aperture diameter. The FIXED state simply fixes the value to a user-defined value. We could enforce additional logic:

  • AUTO is the default.
  • If a user manually specifies norm_radius during geometry creation, the state is set to FIXED.
  • If you attempt to modify norm_radius while the state is FIXED, an error is raised.

I think it's clear that option 1 is the easiest option, but both are possible. Let me know what you think.

Regards, Kramer

HarrisonKramer avatar Sep 14 '25 09:09 HarrisonKramer

Hi @HarrisonKramer,

Thanks for looking into this. I have not yet used Optiland's optimization features, so I am not sure how much I can say about this. Option 1 looks the most transparent to me. It may make sense to (by default) automatically add this variable to optimization problems that contain a polynomial surface, because optimization may fail if the normalization radius is not properly updated.

crnh avatar Sep 16 '25 09:09 crnh

Hi all,

Thank you @HarrisonKramer for the suggested approaches. I am also aligned with both of you - the option 1 seems the easiest and most transparent.

Thus, regarding the option 1, I have a few comments, coming from my experience with optimization with the Forbes surface (which has a norm_radius:

  • I would argue that contrary to what @crnh suggested, the default of the norm_radius should be equal to semi-aperture (or even the aperture diameter which I have found gives better results in some optimization trials I have tested) and not added directly as an optimization variable which can hinder the optimizer's path to a minimum. I can provide actual testing results in a few days, as now I am busy with some other stuff, but that's it: imo, default norm_radius=aperture of the lens.

  • I would just reinforce that we really must have two "modes" as you suggested - eg. if a norm_radius variable was explicitly added by the user to the problem then it is treated as such - controlled as a variable by the optimizer; if the problem does not have a norm_radius variable, but it is MODIFIED by the optimizer - for example in the custom DLS I have introduced in another issue, we raise a "warning", and we make it clear in the documentation that this is the behaviour of some algorithms. We might even add a smarter logic that really locks the norm_radius as that set by the user, and no optimization algorithm or anything can change it - it becomes fixed.

Looking forward to your feedback.

Best, ~Manuel

manuelFragata avatar Sep 18 '25 15:09 manuelFragata