Reflections on prysm, part 2: unused features
This is the second in a series of posts that take a look at my python library, prysm. See The first post for more introduction.
The previous post focused on the overcomplexity of setting up pupils – the 99% beneath the iceberg that users see. This one will focus on features of prysm that no one really uses. I have no usage stats to back this up – the code isn’t phoning home with metrics or anything – but judging by the lack of inqueries I get about them, I assume it to be so.
Somewhat more recently, prysm met astropy and areas of the API moved from a system of “preferred units,” where variables tend to have a value somewhat close to 1 or that is a “natural” unit of length for a person to think in, to one in which users can use any unit in some places. This, too, added tremendous complexity because waves - increments of $\lambda$ - are common in optics and are not something astropy does (or ever will) support. This is great when users express their phases as something that will take on a value of ~1 when plotting, or for toy examples, but those are not cases where performance is important, so they could just modify the phase themselves to scale it for display in those cases.
This would remove a lot of complexity in how prysm deals with wavelengths, which are all converted to new units within astropy (surely this slows down astropy’s machinery??) at runtime. There isn’t all that much code to deal with units, but it is complicated and inconsistent due to the incomplete conversion.
Probably the worst addition recently made is the
Labels class, which is used to define axis labels on plots. Labels is very complicated and, in an effort to not slow down performance sensitive code that carries it as deadweight – is a shared copy among all instances of a class.
This leads to frequent user experience, even by myself, of mutating the labels object botching an attempt to update it, and essentially having to recycle the entire session to fix it.
This is part of prysm’s universal plotting engine, which provides
.slices().plot(...) on essentially all types in the library.
Doing away with this would make the
Labels class serve no purpose, so it could be deleted. It’s not clear what to replace this with. It’s a given that we only really want to plot real numbers, and that we want to plot essentially everything. It’s likely that a plotting API that places less baggage on everything would exist independent of a types system.
About 200 lines of code exist to implement the labels system, which no one really modifies.
Too many convenience properties
Units and labels were big time sinks that didn’t really pay off. There are many more small things that add API surface without much benefit.
As an example, here’s the method and field set for a FringeZernike instance:
from prysm import FringeZernike methods = [s for s in dir(FringeZernike) if not s.startswith('_'] len(methods), methods 39, ['Sa', 'astype', 'barplot', 'barplot_magnitudes', 'barplot_topn', 'build', 'center_x', 'center_y', 'change_xy_unit', 'change_z_unit', 'copy', 'diameter', 'diameter_x', 'diameter_y', 'exact_polar', 'exact_x', 'exact_xy', 'exact_y', 'fcn', 'from_interferogram', 'interferogram', 'magnitudes', 'names', 'phase', 'plot2d', 'pv', 'rms', 'sample_spacing', 'samples_x', 'samples_y', 'semidiameter', 'shape', 'size', 'slices', 'std', 'strehl', 'top_n', 'truncate', 'truncate_topn']
In actuality, we don’t need 39 methods for a glorified phase screen. I won’t go through what all of these are. I’m just going to highlight the ones that go unused:
The center fields are computed lazily and are the center indices in the array, offset as is usual in “FFT aligned” arrays, which do contain a zero bin.
They’re used internally a little bit, but I’ve yet to ever use them externally. The same goes for the sample counts, which are really just the elements of shape.
Shape and size are passthroughs to the phase data to avoid a “.phase.” The directional diameters I have used exceptionally rarely, always using
.diameter. All of the
_y exist to support
MxN instead of just
NxN data. Semidiameter spares you a
/2 and is actually more keystrokes. It is clearer, though.
Supporting nonsquare data has always been a very useful feature, it should not go away. Except for shape, the metadata for nonsquare data is rarely used and should go away.
Too Many Polygons
Early in the development of prysm, I figured out how to use qhull to generate binary N-sided polygons. Half the length of geometry.py (the file is ~650 lines) is dedicated to macros for N=3..13, or something like that on the upper bound. I thought I might model camera lenses with N sided apertures, but I don’t ever use prysm for that. I should have let the user call
regular_polygon instead of providing the macros.
The library has a special config file and class, which is used everywhere. It allows you to set the defaults for plotting, labels, units, and simulation parameters like Q. I don’t use any of it, and there were bugs in the implementation (and I’m sure are still more) because of the rules for when python evaluates things when defining functions.