GalSim-developers/GalSim — pull request #495 by rmjarvis (2013-12-30 23:22:52+00:00)
Title: #364a API changes to Image class

As I have been progressing on my efforts to add WCS capabilities into GalSim, I realized that the number of lines changed has become rather staggering. The lion's share of them though are in some preliminary API changes I made before really getting into the meat of the WCS functionality. While these constitute a large number of lines of code changed, they should actually be relatively straightforward to code review, since there are mostly making the unit tests and such conform to the new API.

So in the interest of making the later code review for #364 easier, I think it makes sense to do a PR on just these initial API changes, which I forked off into a branch named #364a. I already posted a description of this on the issue page for #364 and incorporated some comments I got there. But here it is again for the sake of those who weren't following that page.

The main change I made is that I now have a single python layer `Image` class that has an `image` attribute and a `wcs` attribute. The former is one of the C++ image classes, and the latter is the WCS, which now encapsulates the functionality that had been the `scale` parameter.

This simplifies the use of `Image` objects considerably, since there is now just a single type for people to use. The data type can be specified as `Image(..., dtype = numpy.float64)` for example, or the old letter notation will also still work: `ImageD(...)`. The latter is just an alias for the former now. The returned object is an `Image` instance.

All of the `scale` stuff in C++ has been ripped out and moved to the python layer. Specifically the functionality has been put into an `image.wcs` attribute. The `draw` command uses this to convert a profile from world (aka sky) coordinates to image coordinates before passing it on to the C++ `draw` command. For more complicated WCS types (which will be in the next PR for branch #364), the transformation is relatively straightforward using the local Jacobian of the transformation (implemented via an `AffineTransform` WCS class).

Here is a summary of the API changes:

* Changed the name of the `dx` parameter in the `draw`, `drawShoot`, `drawK` methods of `GSObject` and the constructors of `InterpolatedImage` and `CorrelatedNoise` to the name `scale`.
* Changed the `xw` and `yw` parameters of the `Pixel` constructor to a single `scale` parameter.
* `pix = Pixel(xw=scale)` should now be either `pix = Pixel(scale=scale)` or simply `pix = Pixel(scale)`.
* Added new `Box` class to take up the functionality that had been `Pixel` with unequal values of `xw` and `yw`.
* `box = Pixel(xw=width, yw=height)` should now be either `box = Box(width=width, height=height)` or `box = Box(width, height)`.
* Changed the `dx_cosmos` parameter of `getCOSMOSNoise` to `cosmos_scale`.
* Combined the old `Image`, `ImageView` and `ConstImageView` arrays of class names into a single python layer `Image` class that automatically constructs the appropriate C++ image class as an attribute.
* `im = Image[type](...)` should now be `Image(..., dtype=type)`
* `im = ImageView[type](numpy_array.astype(type))` should now be `im = Image(numpy_array.astype(type))`. i.e. The data type inherits from the numpy_array argument when appropriate. If it is already the correct type, you do not need the `astype(type)` part.
* `im = ConstImageView[type](numpy_array.astype(type))` should now be `im = Image(numpy_array.astype(type), make_const=True)`
* `im = ImageF(...)` and similar is still valid.
* `im = ImageViewF(...)` and similar should now be `im = ImageF(...)` (preserving the same type letter S, I, F or D).
* `im = ConstImageViewF(...)` and similar should now be `im = ImageF(..., make_const=True)` (again preserving the type letter).
* `im = ImageF(...)` _may_ now be written as `im = Image(...)`. That is, the numpy.float32 type is the default data type if you do not specify something else either through the type letter or the `dtype` parameter.
* Changed the handling of the `scale` and `init_value` parameters of the `Image` constructor, so that now they have to be named keyword arguments rather than a positional arguments.
* `im = ImageF(nx, ny, scale, init_val)` should now be `im = ImageF(nx, ny, scale=scale, init_value=init_val)`.
* Removed the `im.at(x,y)` syntax. This had been equivalent to `im(x,y)`, so any such code should now be switched to that.

For this PR, the only WCS class is `PixelScale`, which implements a simple conversion from pixels to arcsec. I've tried to convert all uses of `scale` to use the WCS functions instead. e.g. when you really want the pixel area, I call `wcs.pixelArea()` instead of writing `scale**2`.

Note that most of the time, one does not need to explicitly construct a `PixelScale` object. If you aren't going to use a more complicated WCS type, then the syntax `im.scale=scale` for instance still works through some python decorator magic.

Here are some of the methods of WCS classes that are now used when appropriate:

* `world_pos = wcs.toWorld(image_pos)` converts a position in image coordinates to world coordinates.
* `image_pos = wcs.toImage(world_pos)` converts a position in world coordinates to image coordinates.
* `world_profile = wcs.toWorld(image_profile)` converts the given profile (a GSObject) from world coordinates to image coordinates.
* `image_profile = wcs.toImage(world_profile)` converts the given profile (a GSObject) from image coordinates to world coordinates.
* `area = wcs.pixelArea()` returns the area (in arcsec**2) of a pixel.
* `local_wcs = wcs.local(image_pos=image_pos)` returns the local linear approximation of the WCS at a given point. Alternatively, `world_pos` may be provided if that is more convenient.
* `local_affine = wcs.localAffine(image_pos=image_pos)` returns the local linear approximation of the WCS at a given point when you need that returned value to be an AffineTransform type. For variable WCS, this will be the same as `wcs.local(...)`, but `local` is allowed to return any constant WCS type, which might be more efficient in some cases. e.g. `PixelScale` can return itself.

If the wcs is not constant, you need to provide either an `image_pos` or `world_pos` argument to many of the above functions to tell it where you want the pixel area or the profile drawn or whatever. e.g. the following commands are equivalent:
* `area = wcs.pixelArea(image_pos = image_pos)`
* `area = wcs.local(image_pos = image_pos).pixelArea()`
* `area = wcs.localAffine(image_pos = image_pos).pixelArea()`