How best to structure a physics (wave optics) simulator?

I am a physicist working on a project involving wave optics, which I would like to use Julia for. Being relatively new to this language (and not a programmer by trade), I’m not sure of the best way to structure the problem in Julia, and I’m seeking advice on good practices, with an eye to performance and flexibility. In particular, I’m wondering if I can leverage Julia’s type system in an advantageous way.

The basic idea of what I want to do is this (in optics lingo, I’m modeling a 2f imaging system with various non-ideal features). To zeroth order, I have a function propagator that takes as input an array fieldIn and returns a modified Fourier transform fieldOut, which models the effect of passing light through an ideal lens. There are then various non-ideal features that one might like to incorporate: defocusing, aberrations, shot noise, camera saturation, etc. However, I might not want to have all these features present every time I run this function, either because I haven’t implemented them all yet, or because some are not relevant to a particular setup. Note also that some of these features are largely decoupled from others (e.g. camera saturation is applied after all the other operations), while others (such as aberrations) require modifications to the zeroth order propagator function.

If I were coding this in something like MATLAB, I’d either just duplicate the function propagator for every set of parameters, getting something like propagator_aberration, or simply keep adding optional parameters to the original function propagator. Now already Julia gives me a better option by letting me create a type for each non-ideal feature and dispatch on which features are supplied to a function.

I can see three ways to structure the problem from here:

  1. Leave propagator as a function which dispatches on the features supplied to it.
  2. Make an overarching type opticalSystem that includes all the features for a given setup. Supply this type to propagator instead of the individual features.
  3. Make propagator itself a callable struct, with all the relevant features for a given setup as attributes.

My question is basically whether any of these is preferred, or perhaps whether there is an entirely different way that is better. It seems to me that the first one provides the greatest flexibility to add new methods, e.g. as I’m developing and implementing new features. However, it has a potential downside of being cumbersome when passing lots of different arguments for complicated setups.

What are good design principles for such a problem?

Hi ! When I was a master student, I wrote a small library to deal with aberrations, and I implemented something that looked like option 2 (see Example of using Wavefronts.jl · Wavefronts.jl documentation). I liked this approach because it lets you build a system and separates the specification of the system from the physical investigation.

You also might want to have a look at these :

2 Likes

There is an implication of order – first surface, second, and so on – which would be an issue in a comprehensive simulator that allows for scattering and back reflection. Each element of the system will generate a wavefront that then propagates and generates new ones. For a single surface, the simplest approach of a propagator function accepting an input wavefront, a specification of the surface, and returning an output wavefront(s) would be a building block without the constraint of knowing all the surfaces beforehand. OpticSim is a ray-tracing code, perhaps no longer being maintained. Another interesting approach is PyOptica which has an impressive roadmap but is also not actively developed. If polarization is included the propagator will need electromagnetics too, and will depend on surface materials. An example is the behavior of blazed gratings where the shape of the grating groove and its conductivity presents a challenge to predict on first principles.

1 Like