Repository navigation
Implementation of errors and masks - #202
wkerzendorf wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
It looks like the add/sub/mul/div methods are the same? Is this intended?
There was a problem hiding this comment.
Well I for a bool mask yes. It means values that were masked (in either of the operands) stay masked. But you can define your own mask which can have different behaviors for the different arithmetic operations.
There was a problem hiding this comment.
To reiterate a discussion that I know realize occurred in specutils and not here: I see this is very non-intuitive behavior, and would lead to a lot of confusion. I agree this how to organize masks is an issue, but I think it's much more confusing to implement this at a mask level than it is at the level of a specific type of NDData (e.g. a Spectrum1D)
There was a problem hiding this comment.
@eteq could you give an example of where masks (specifically boolean) could be used in any other logical sense? In my mind you mask regions of the data which are defunct. If you're adding two (for example sake) images together and there are masked regions then in my mind it only makes sense that the resultant image has regions which was neither masked by image1 or image2 - hence, a logical and for the add/sub/mul/div methods.
Perhaps people use these masks in other ways which I've never needed to.
There was a problem hiding this comment.
Here's an example of the sort of thing that worries me. Lets suppose we have two NDData images a and b we want to combine: a uses a boolean mask like the one here where False is "good pixel", and True is "bad pixel". b, on the other hand, is a (slightly) more complicated integer masking scheme where 0 is "good pixel", 1 is "cosmetic defect", and 2 is "saturated pixel". If I then want to add these two images, I naturally expect False and 0 to combine to give 0, but all other combinations should give whatever the integer value is (e.g. False w/ 1 -> 1 and True w/ 1-> 1, False w/ 2-> 2, etc.). But that's not what happens - instead I get all kinds of weirdness like False w/ 2 -> False.
The key point is that you can't count on boolean masks combining with boolean masks all the time, and given that, it seems dangerous/confusing to override the standard numpy behavior.
There was a problem hiding this comment.
@wkerzendorf - yes, but note that one could consider having each error type having a method that validates whether its shape is consistent with a given data shape. For example, a spatially varying PDF would need to be 4-d if the data is 3-d, and the first three dimensions should match, or an SDError's shape should match (unless it's a scalar), etc. So I wonder whether the error classes should have a method e.g. validate_shape that checks for this? Even though there might be a variety of error types, a specific error type still has a set of criteria that determines if it is consistent with the data.
There was a problem hiding this comment.
I like the idea of a validate_shape method that defaults to just doing nothing in the base class, but raises some error if it decides the shape is wrong (probably a ValueError to match other numpy shape mismatches). Then in BoolMask or SDError that you have here you can just check if the shapes match.
There was a problem hiding this comment.
@astrofrog probably good idea. I think as release for 0.1 is getting closer, we should put this on the 0.2 milestone. What do you think?
There was a problem hiding this comment.
I'm fine with it not being in this PR, though it should be pretty simple to implement for SDError, e.g. (untested):
def validate_shape(self, shape):
if np.isscalar(self.values):
return True
else:
return self.values.shape == shape
where self.values are the standard deviations. Then in NDData, you just need to call validate_shape rather than just checking the shapes directly. But feel free to postpone this if you want.
There was a problem hiding this comment.
I think if we follow the approach I described above of having base (abstract) classes, it should be in this PR because it'll be a fundamental part of the implementation. Just have the base class have:
def validate_shape(self, shape):
pass
And just be sure to call it when the object is created. Then the subclasses implement as-needed.
|
@eteq I think for errors and masks (and I would focus on the errors first as it is more obvious there) are not specific to spectrum1d. They are a base level NDData problem which is the same ranging from 1d spectra, images, cubes, to n-dimensonal simulations. There are a couple of ways to incorporate them in nddata (that I thought about) and this ones seems to be the most straight forward. I think implementing them in Spectrum1d would inadvertently lead to a lot of code duplication. |
|
We should try and make progress with this. First I think that first we need to separate the issue of masks and errors, as they are quite different, so I propose splitting the pull request. In the case of errors, I have several issues:
I guess what I'm trying to say is that we're going to have to put a lot of thought into how to deal with the errors properly. I suggest that we create a wiki page with a list of requirements, code examples, how we want things to behave, then work on the implementation. For masks, I think that we also want to think about this more, in particular, I kind of agree with @eteq that the arithmetic cannot be done at the level of the mask objects, because they don't know what the user wants (for errors, things are well formulated mathematically, but not for masks). |
|
@astrofrog I agree that this is one of the most important things and we should put some effort into this.
I agree we need to put some thought into this, but I think one good thing is to have a pull request that shows one implementation (here's one I prepared earlier ;-) ). I have thought about this problem in a bit of detail as I was trying to implement it for Spectrum1D. I thought of another approach using functions pointers hidden in the meta data, which seemed inferior. Last but not least: masks. Of course they know what the user wants: If the user wants a BoolMask then it will behave like masked arrays in numpy. Ignoring values in sums and so on. If the user specifies other masks then it will do what is written in these masks. Masks are the same for Spectrum1D, Images, IFUs, simulation data ,.... the same as errors. |
|
A couple of quick notes (need to think about the rest more)
|
|
First, regarding splitting: I agree with @astrofrog - they aren't the same thing, if nothing else to be evidenced by the fact that very different topics are coming up here. However, I also agree with @wkerzendorf that we should treat them the same in the sense of how they interact with
(see my example above for a case where we don't know how the user wants to combine masks without making them decide. People use mask flags for all kinds of wacky things!) |
|
@eteq regarding the arithmetic of nddata objects with different errors and masks: I believe for now it is easiest to throw an exception if they do not match. It is very similar to Anyways, the whole point is that this implementation allows for such magic to happen in the background. Var-errors can be converted to sd errors on the fly if that is what we want to do. For now, I would throw an exception and worry about making it automagical later. (4) ...screw you github, I can make my own numbering scheme and there's nothing you can do about it ;-). That is fine. However this reconfirms me in my assumption that error_add and mask_add should have access to the data, masks and errors of both operand and self nddata. I think in general for the first release we want bool masks and sd errors to lay a foundation that others can build upon. |
|
Just a couple of comments:
|
|
@wkerzendorf - as I said above, I've come around to your point regarding masks, but I'm not quite following what you are saying about errors... I agree that the point is to have the magic happen in the background, when it's unambiguous - (4) You raise a good point that there is a need for the masks to have access to the data, though... this is why I was saying before that it might be better to implement this at the e.g. But I still agree with @astrofrog that if we are going to allow error-conversion, there should be at least two to make it clear who the conversion is supposed to work. I also still think it would be better if you split this into two pull requests, especially if you are going to split it into a base class for each (mask and errors), which I think you should. Then it will be much easier to review the PRs in detail without having tons of confusing threads in the diffs. |
|
@eteq's comments: Second: sorry you misunderstood me. I meant the error_add (and friends....) function needs to have access to all the data and not the error object. This way we go around the problematic garbage collection (but good point, which we should think about in this interconnected hell). I believe that the masks and errors do exactley the same no matter what kind of object it is (a spectrum, an ifu, a bird, a plane, ...) so we need to solve this here at ground zero. Third: Agreed, we need to check if in this framework we can make the conversion-magic happen. This is the last thing on my list (next ones are two pull requests and subclass from object (probably)). Irregardless of the pull requests, when thinking about masks and errors there are many similarities that we should keep in mind (they are both tag-alongs of nddata, they need to be taken care of in the right way when doing arithmetic, ...). Finally, I think that when talking about masks, it often seems to be more like flags (@eteq mentioned, saturated, defect, ...) maybe we should call them flags. What do people think? In the end we'll probably inherit the new NA value from numpy 2.0 anyways (which might make boolmasks obsolete; only boolmasks). |
|
@wkerzendorf second: I don't understand what you're saying here, still. And you are absolutely right that there are similarities we should keep in mind - I promise we will be looking at both PRs if we split them :) But I think when we have the API ironed out and are getting into the detailed code-review stage, it will be far less confusing if there are separate, smaller PRs. As for masks/flags, this is a hopeless ambiguity - I've heard both terms used to mean the other one many times. I think mask makes slightly more sense because "mask" is generally more commonly used for pixel-like data (usually there's some agreed definition of a "bad" pixel), and "flags" is more common in catalog settings. But it's really just semantics at this point. But you can't count on using the numpy 2.0 NA value for a long time yet - as you say, we may want to internally use that eventually if numpy 2.x is installed, but we have to continue supporting older versions of numpy (and probably will for many years, based on how slowly astronomers can be to upgrade). |
|
It seems we agree at least on the basics. Which is a huge leap forward in astropy progress (doesn't happen to often ;-) ).
|
|
I have left this open to see if there's a few things missing, I guess no one complained that there's fundamental differences. So I close this one. |
Fix a bug that caused WCSAxes to crash when trying to transform arrays with zero coordinates
…alse upon _any_ exception, whereas on Python 3 it returns False only when AttributeError is raised, and allows any other exception to bubble up. By casting a wider net this seems to get the job done for both Python 2 and 3.
DistutilsClassError - astropy_helpers.setup_helpers.test must provide user_options
Propagate update of specutils maintainers/description
To implement arithmetic operation, convolution and interpolation for masks and errors @andycasey and I implemented ndarray subclasses for them. If nothing is specified when instantiating a nddata object it will automatically convert ndarrays to the appropriate mask and error types.