One of the challenges discussed in the Simultaneous Astrometry RFD is the unification of
afw.geom.XYTransform with the
Gtransfo object introduced in
meas_simastrom. Both of these are polymorphic base classes that represent arbitrary coordinate transforms, but they represent two different views:
XYTransform is the interface for consumers of the already-fit model, while
Gtransfo is the interface for fitters. I think meeting the needs of those two different users is a generic problem in scientific software architecture, and we’ll need to deal with it in several more cases in the not-too-distant future:
- To get simultaneous photometric calibration working in
meas_simastrom, we’ll need a fitter object (analog of
Gtransfo) that works well with our existing consumer object (
Kernel both currently suffer from trying to meet the needs of both consumers and fitters, and don’t do either as elegantly as they could. And we’ll have to make major changes to both of them anyway to support chromatic dependence.
- The galaxy model classes in
meas_modelfit could also benefit greatly from some refactoring.
- The background subtraction objects are also due for an interface overhaul.
Here’s what I think makes a consumer interface different from a fitter interface:
A fitter interface has parameters that can be varied to change the model. It may hold these parameters as instance state, or it simply operate on external arrays that hold them, but it needs to be able to update the model given those parameters, provide mechanisms to guess initial parameters, and probably compute derivatives of the model with respect to those parameters. A consumer interface, in contrast, doesn’t care about parameters at all, and probably needs to support model implementation that were never actually fit, or were fit externally in a way that doesn’t even allow us to know what the free parameters were. It’s also important to note that a different parametrization of the same model (or a different choice as to what parameters are free and which are held fixed) may be best implemented as different derived classes for fitters, but only a single derived class for consumers, so we shouldn’t assume that there is a one-to-one relationship between fitter and consumer classes.
Fitter classes must be mutable if they include their parameter vector as instance state, and even if they don’t, there’s no generic advantage to making them immutable. It is very helpful to make consumer classes immutable, however, because this allows these complex objects to be shared (via
shared_ptr or Python reference counting) by multiple parts of the codebase without needing to worry about the multiple holders of the same object invalidating each other by unintentionally modifying each other’s state. Without immutability, we frequently have to do a deep-copy of a complex object to guard against this possibility, even if the actual probability of the object being modified is low. As an example, note how much implicit deep-copying of
Calib objects (both mutable) happens in
ExposureInfo) compared to
Psf (which is immutable).
A fitter class should be optimized for evaluating its model for a set of arguments that is largely fixed, as they’re the arguments of the data that is being fit. This means it’s often a good idea for a fitter object to include whatever state it can to make repeated evaluation on those particular data points more efficient, even if that makes the object larger in memory or requires a bit more work up front. In contrast, consumer objects generally aren’t evaluated repeatedly at the same point (since their parameters are fixed, it’d be more efficient just to save the result, after all), and instead should probably prefer a more lightweight memory footprint.
It’s rarely necessary for a fitter object to be persistable; usually we just need to persist consumer objects. This doesn’t just refer to general persistence to files - it may be necessary to represent some consumer objects (e.g. galaxy models) in a single record in an output catalog.
Consumer interfaces may need to support summary statistic calculations (e.g. FWHM for a
Psf, or flux for a galaxy model) that aren’t needed while fitting.
There are, of course, many more things that fitter and consumer objects for the same model will have in common, so the challenge is to meet the differing needs of the users without a lot of code duplication. And if we have an interface that does have distinct fitter and consumer classes, we clearly need to be able to efficiently convert one into the other (or at least create consumer objects from fitter objects).
I can imagine a lot of design patterns that could potentially do this (multiple classes with some sort of composition relationship, a single class with multiple inheritance, two class hierarchies that share a pimpl), but I haven’t really thought through any in detail. I’ll try to find some time to do that later, but I wanted to post this here now to explain why I think unifying
Gtransfo doesn’t necessarily mean merging them into one class, and to try to spur some more thought on the generic design patterns for this more general problem.