Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.
Comment: updated the AST diagram

This page is a repository of notes for the redesign of our WCS and Transform classes. Do not take anything here as final, and do assume that anything here can be added, deleted, or changed at a moment's notice.

Strawman class diagram

 updated version from RO 

View file
nameTransforms Strawman 2.pdf
height250

For comparison, here is a class diagram for similar bits of AST:

View file
nameAST.pdf
height250

 

We choose the name TransformGraph because it really is a graph: it has nodes (frames) and edges (transforms), and could be quite complicated. The minimum TranformGraph consists of one Frame, and no Maps. TransformGraphs are mutable, though their component frames and mappings may not be. Must any Frame in a TransformGraph have a path to any other frame? It would simplify the design if this was true.

A Frame (AST: Frame, GWCS: CoordinateFrame) is an immutable coordinate set that a transformation could go from or to. Frames are uniquely identified by a set of tags (e.g. {"meanPixel", "ccd4", "raft6", "Exposure153"}). We will have to standardize on a list of valid tag names.

A Map (AST: Mapping, GWCS: astropy.model) is a transformation that takes some numbers and returns some numbers, and that knows its domain and range (as numbers, but not what those numbers represent). Maps are functors (callable classes), with an inverse method to (if available) perform the inverse transformation. Maps may be either mutable or immutable: both have valid use-cases, and we will likely need immutable versions of many maps to ease parallelization. We need to decide how we will get the inverse map: map.inverse_map()? InverseMap(map)? while being aware that this is often non-trivial and likely non-analytic.

A possible method for composing mappings is to overload operator() to take a Map and return the composition f(g()) (possibly of type ComposedMap). This solves the question posed by e.g. ComposedMap(f,g,h): is that h(g(f())) or f(g(h)))?

A Transform is like an immutable TransformGraph with a start and end frame (it could be an immutable view, which might make the initial implementation easier). This makes it clear where you're going from and to, and is the thing one would generally operate on when working with a TransformGraph (which could be arbitrarily complicated). If you want to add or modify the transforms or frames, you work with the TransformGraph; If you want to use it, you connect() your desired start and end points to make an immutable Transform and use that. We're not totally happy with this name choice: we couldn't come up with a better one but liked this better than AbsoluteTransform.

FITS_WCS is a subclass of Transform that knows how to be persisted as a FITS file, and can be approximated as a SIP (or TPV or other?) FITS distortion. The *FITS methods are the only place in this whole structure were we think about the FITS WCS standard at all. Russell is uncertain if we really do want this.

connect(TransformGraph1,Frame1,TransformGraph2,Frame2,commonFrame,collapse=True): a function to "merge" two TransformGraphs at commonFrame and return a Transform with start=Frame1 and end=Frame2. Raises exception if there is no valid path.

Details about the methods on TransformGraph and Transform:

  • approximate: find a (polynomial, affine, spline, etc.) transform that best approximates this absoluteTransform to a given level of precision.
  • simplify: remove loops, identity maps, etc. and eliminate unnecessary frames between start and end. Both the TransformGraph method and function connect() always simplify() the before returning the resulting Transform.
  • collapse: combine as many internal transforms as possible (either by composition or other mathematical combination), to have the fewest transforms to get from start to end. Both versions of connect() default to collapsing the resulting maps, but can be told not to for debugging purposes.
  • as_transformGraph: returns a transformGraph from a Transform, so that you can manipulate it again. Would this be better as a TransformGraph constructor that takes a Transform?

Old notes

Jim: The goal of the design of AbsoluteTransform is to hide whether it is implemented as a view into a TransformGraph with a specific start and end point, or a copy of just the nodes and edges in the graph that connect its start and end point.  If we start with an AST implementation, I imagine we'd start with the former, but we'd be free to switch to the latter if we move away from it.

Jim didn't like "Model", as it sounds too much like its related to a fitter. Mapping or Transform is better, and I don't think we have a particular preference there.

Russell: I would prefer to adopt standard terminology if we can. The standards I know of are AstroPy and AST. The former uses Model, the latter Mapping. Given Jim's objection to Model, I suggest Mapping. I can also see an argument for Transform since we already use it, but we have XYTransform and Transform and it could be hard to root out old usage if we totally change what Transform means.

Jim: Since we talked, I've been wonder if we should just move simplify into AbsoluteTransform's constructor.  I can't think of a context in which one would want an unsimplified AbsoluteTransform.

Russell: I think we need to somehow support transforming between any two frames using the unsimplified path (e.g. a sequence of transforms from connecting frame to frame), as well as using an optimized transformation. The latter is usually preferred, but the former is wanted for testing and might also be preferred for one-off computations if simplification is expensive.

Transform and its subclasses may or may not be mutable, we weren't sure, and weren't sure it should be specified. I'm not wedded to the SphToSph, CartToSph subclasses, but I feel like they might be useful (I don't remember if you and I came to a decision about those).

WCS is a very particular subclass of AbsoluteTransform, going from pixels to sky. The *FITS methods are the only place in this whole structure were we think about the FITS WCS standard at all.

Russell: I am not convinced we need or want a special class for WCS. I originally thought it likely, but if we have a good API for TransformGraph then I hope it will suffice.

I think the "combine transforms and get a simplified result?" question in your notes would look like this:

graph3 = graph1.merge(graph2)
destPixelToSrcPixel = graph3.connect("pixel1", "pixel2").simplify().collapse()

Or, if you start with two AbsoluteTransforms instead of two graphs, you could do one of these two:

transform1.compose(transform2.invert()).simplify().collapse()
transform1.compose(transform2, invert=True).simplify().collapse()

I don't know that we need separate simplify() and collapse(), but they are rather different operations.

Jim: Thinking about it a bit further, if we don't move simplify() into the AbsoluteTransform constructor, then collapse() should first simplify(), so we'd never have to call them both.

Russell: I agree; I doubt it makes sense to have both simplify and collapse. I'm not even sure how they differ.

As I think about this, I don't know that Jim and I discussed what happens if connect(start,end) can find 2 paths between them. I guess it chooses the route with min(len(frames)+len(transforms))? But what to do there is not obviously clear with forward/reverse either, I don't think.

Jim: There are a ton of well-known algorithms for shortest paths in a graph (check out the Boost.Graph documentation for a list; I'm not sure if we want to use Boost.Graph, but it might be worth considering).  Many of those allow you to define a metric for length other than just the number of edges/nodes in between, but I suspect that's still the metric we want (and using that metric implies that one never has to call simplify() after connect(), even if we don't put simplify() in the AbsoluteTransform constructor).

Jim: I think I'd recommend that we conceptually consider all edges in the graph to be bidirectional, though the graph edge implementation might not actually hold both directions until they're actually needed.  In other words, we assume that any inverse transform that doesn't exist can be computed once we decide it's needed, and compute distances in the graph without concern for direction.

Russell: I am not convinced we need to worry about this. If AST already does it then we can use what AST does. I also think in most cases our graphs will be fairly simple with only one path connecting any two frames. So I'd hate to get hung up on this.

Design notes from Russell

AbsoluteTransform is an interesting idea; however, I have some uneasiness about it:
  • It seems a hassle to make users extract an AbsoluteTransform before they can transform anything.
  • If we have TransformGraph itself support transformation, then do we need it? A transform with two frames is just a simple graph, so we could offer a method that returns a simplified graph connecting two points:
             simpleGraph = transformGraph.getSimple(frame1, frame2)
That said, if supporting transformation in TransformGraph really does clutter up the API, then it may be worthwhile offering AbsoluteTransform. Similarly, if we gain enough benefit from immutability then it may also help, though in that case, we could just have FrozenTransformGraph.
I think we must support the ability to transform between two frames by going through all connecting transformations (unsimplified). This is not the preferred technique most of the time, but it useful for testing. It might also be preferred to transform one or two points if simplifying transforms is expensive. If TransformGraph supports transformation and we don't have AbsoluteTransform then this becomes trivial, but it can be made to work in any case even with AbsoluteTransform.
I am very very reluctant to have Transform subclasses CartesianToCartesian, etc. I would rather keep that information in Frame and keep Transform as a simple object that transforms N floats to M floats. Note that general libraries such as AST support other types of frames, e.g. for spectra. LSST need not support this, but we may want some other kind of frame some day and if so, the number of Transform subclasses will multiply far too quickly.
What does TransformGraph.merge(transformGraph) do? I think it will be very difficult to merge two transform graphs due to possible frame collisions and I’m not convinced it is worth the bother. As I said in my notes earlier, I think it far more likely that we’ll want to make a new transform by combining two  transforms from different transform graphs; this is a much simpler problem and avoids name collisions. Simpler is not trivial, however. The obvious implementation is 3 lines and I’d prefer a clean one-liner if we can think of one.

Transformations we need

See:

Jira
serverJIRA
serverId9da94fb6-5771-303d-a785-1b6c5ab0f2d2
keyDM-5918

Frames we need

See:

Jira
serverJIRA
columnskey,summary,type,created,updated,due,assignee,reporter,priority,status,resolution
serverId9da94fb6-5771-303d-a785-1b6c5ab0f2d2
keyDM-5919