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:
- Leave
propagator
as a function which dispatches on the features supplied to it. - Make an overarching type
opticalSystem
that includes all the features for a given setup. Supply this type topropagator
instead of the individual features. - 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?