Repository navigation
Implement bounding_box model property and render_model function - #3909
Conversation
|
I will review this later today. |
There was a problem hiding this comment.
@patti You committed a change of the astropy_helpers git submodule pointer in
patti@dd95943
I guess this was accidental and needs to be removed?
Maybe @embray or @astrofrog can advise how to get rid of this change in this branch?
Is simply making a new commit that changes it back acceptable?
Or should @patti modify the previous commit to not include that change? (this is quite tricky, no?)
There was a problem hiding this comment.
I remember having a similar issue and I think doing git submodule update fixed it for me. But I don't know about the details...
There was a problem hiding this comment.
Yes, do git submodule update then add the change to the branch. Then later on we can always squash the commits if we want.
There was a problem hiding this comment.
Just revert the submodule to the previous version and then commit and do a fixup rebase:
$ cd astropy_helpers
$ git checkout 1ecac98d547729393ca54bcbe4f03cd4e1bfcd9a
$ cd ..
$ git add astropy_helpers
$ git commit -m "Fixup"
$ git rebase -i master
then a text editor will open with:
1 pick dd95943 Begin building functions to calculate bounding box
2 pick 8c766b9 Outline functionality of cal_flux_bounds helper functions.
3 pick 3e192d1 Minor edits to helper function. Squash this later
4 pick 6a13e45 Add bounding box parameters and default bounds for some models.
5 pick 16e81f9 Implement bounding box attributes and properties, in progress.
6 pick 4bef961 Define _bound property for models where appropriate.
7 pick 8755039 Rework bounding box property.
8 pick 2b1fc2e Add render_models2sky function.
9 pick 123574b Implement render_model2sky function.
10 pick 6bfe498 Fixup
11
12 # Rebase 3f6de97..6bfe498 onto 3f6de97
13 #
14 # Commands:
15 # p, pick = use commit
16 # r, reword = use commit, but edit the commit message
17 # e, edit = use commit, but stop for amending
18 # s, squash = use commit, but meld into previous commit
19 # f, fixup = like "squash", but discard this commit's log message
20 # x, exec = run command (the rest of the line) using shell
21 #
22 # These lines can be re-ordered; they are executed from top to bottom.
23 #
24 # If you remove a line here THAT COMMIT WILL BE LOST.
25 #
26 # However, if you remove everything, the rebase will be aborted.
27 #
28 # Note that empty commits are commented out
(where the line numbers above are not part of the actual text)
Change line 10 pick 6bfe498 Fixup to read fixup pick 6bfe498 Fixup. Then move it to just below line 1, so that the fixup is applied to the commit where the error was made:
1 pick dd95943 Begin building functions to calculate bounding box
2 fixup pick 6bfe498 Fixup
3 pick 8c766b9 Outline functionality of cal_flux_bounds helper functions.
4 pick 3e192d1 Minor edits to helper function. Squash this later
5 pick 6a13e45 Add bounding box parameters and default bounds for some models.
6 pick 16e81f9 Implement bounding box attributes and properties, in progress.
...
Then:
$ git push origin --force
On newer versions of git there should be less risk of this happening--it's better about automatically updating submodules when switching between branches.
There was a problem hiding this comment.
Thanks for the instructions @embray. Should be fixed now.
|
@patti – Thanks! Can't wait to use this for my analyses! I'll let @adonath do the review of the API / implementation. @embray @adonath For high-level docs, what do you think is best?
|
|
@patti – Sorry, I forgot to ask you explicitly ... what do you think would be best for the high-level docs for this PR (ideally so that they can grow to cover what you'll implement in the coming weeks)? Concerning tests: start with one simple test that proves that this works at all (once it does). Then maybe parametrise it and include cases where the source is very small and very large and overlaps with the image boundaries. Gaussians and box models are great for tests, because they have analytical solutions on how much flux is within rectangular boxes, i.e. it's possible to verify the reference values. |
There was a problem hiding this comment.
Why are the x_key and y_key arguments necessary? Is it because the Gaussian2D and Gaussian1D have different parameter names then x_0 and y_0 (as the other models)? In that case, I'd just assume the parameters are called x_0 and y_0 and handle an exception for the Gaussian case (In fact one should maybe think of renaming the Gaussian parameters, because mean and stddev are terms only used in statistics... but that would be a major discussion and I don't think want to deal with that... )
There was a problem hiding this comment.
I'd lean toward re-naming the Gaussian parameters for consistency among the models. It might be useful to still have x_key and y_key to handle user-defined custom models, but I could go either way on that.
There was a problem hiding this comment.
Like the bounding box, the location is something the models should declare, so that you don't have to guess parameter names.
|
Thanks for working on this! I might make sense to add 'WIP: ' to the title of the PR, just to make clear that it's work in progress. And could you add a to do list in your first post? We can work that list out later during the hangout... |
|
I'll review all this in a bit, but I'll just add that it's fine if it's only working, for now, on a handful of models. I think it's expected that these features will be unimplemented by default, and would be implemented on individual models on a case-by-case basis. |
|
Thanks for providing the notebook! I hope large parts of it will transform into nice plots and usage examples for the docs:-) I noticed, that the difference image doesn't look correct. It seems like the positions with and without the bounding box are shifted with respect to each other. We'll have to check that in detail. Could you add a very simple test, that checks that the evaluation with and without the bounding box gives the same result for let's say a Gaussian or a box? At best this is checked for a few |
|
@embray In particular I'd be interesting to know, how you think the implementation of the bounding box feature would fit best in the current framework, concerning e.g. n-dimensional models and compound models. |
bbox model property and render_model2sky function bbox model property and render_model2sky function
|
@adonath regarding "It seems like the positions with and without the bounding box are shifted with respect to each other." It just looks that way because I didn't add 1 to the upper bounds to make it inclusive. I'll fix that. |
|
I'm having a hard time trying to diagnose a bug where I get a row of zeros in the Gaussian2D bounded image for certain values of theta. It's reproduced in a notebook, here. Any ideas? |
|
I'll go over this in more detail in a sec, but there's something in your example notebook that got me thinking, where you created parameters for 1000 sources, and then created 1000 Gaussian2D instances to model them: imshape = (800,1250)
sky_image = np.ones(imshape)
nsrc = 1000
data = {}
data['x_mean'] = np.random.rand(nsrc)*imshape[1]
data['y_mean'] = np.random.rand(nsrc)*imshape[0]
data['r_amp'] = np.abs(np.random.randn(nsrc)+10.)
data['g_amp'] = np.abs(np.random.randn(nsrc)+10.)
data['b_amp'] = np.abs(np.random.randn(nsrc)+10.)
data['x_stddev'] = np.abs(np.random.rand(nsrc)+3.)
data['y_stddev'] = data['x_stddev']*(np.random.rand(nsrc)+1)
data['theta'] = np.random.rand(nsrc)*np.pi*2.
mod_list = [Gaussian2D(x_mean=data['x_mean'][i], y_mean=data['y_mean'][i],
x_stddev=data['x_stddev'][i], y_stddev=data['y_stddev'][i],
amplitude=data['g_amp'][i], theta=data['theta'][i])
for i in range(nsrc)]Ideally this should be unnecessary. The Model interface has a useful, but poorly understood feature called "model sets" (introduced here and described in more detail here. It is for exactly this purpose--when you have many different parameterizations of the same model. You could make a model set of 1000 Gaussian2Ds like: models = Gaussian2D(x_mean=data['x_mean'], y_mean=data['y_mean'],
x_stddev=data['x_stddev'], y_stddev=data['y_stddev'],
amplitude=data['g_amp'], theta=data['theta'], n_models=nsrc)Then you can evaluate all of these models simultaneously against the image like: sources = models(x, y, model_set_axis=False)where You can then combine them into a single image just be calling image = np.zeros(imgsize, dtype='float')
sources = models(x, y, model_set_axis=False) # Returns bounded results
for bbox, source in zip(models.bbox, sources):
image[bbox] += sourceI think being able to do something akin to that should be able to work, but what do you think? (I don't personally claim to know exactly what the priorities are for this work myself :) |
There was a problem hiding this comment.
This function doesn't really belong in here. modeling.utils is mostly for utilities for implementation details of the modeling package itself, and not functions that users would use.
Instead, what we will ultimately want to do (as I've referenced elsewhere) is bake in evaluation of models using their boundaries into Model.__call__.
There was a problem hiding this comment.
@embray - I don't think it's a good idea to have all model rendering code live on model classes.
At the moment Model.__call__ gets x, y passed in ... here the x and y grids are computed on the fly from the input image and bounding box.
Of course it would be possible to introduce several cases and options into Model.__call__, but I think it would pretty soon lead to a very complex API.
Butter to implement some things as standalone functions for now, and to re-expose it via Model methods where appropriate. This also makes it easier to re-use the method @patti implements here (and more will come in the coming weeks) if the computations are separated from the framework at the low level.
If you don't like the function in this file, maybe a new render.py files in astropy/modeling would be OK with you?
There was a problem hiding this comment.
I wasn't going to suggest that this add a different interface to __call__. My thinking was that you would compute the x/y grids manually (which is easy using np.mgrid in conjunction with model.bbox, which I'm also proposing could return a slice that can be passed directly to np.mgrid. I think it would be good to add a flag to Model.__call__ to enable evaluation within the boundary as well.
However, I can also see the value in adding a higher-level shortcut to that, basically doing exactly what I described above (or, in other words, essentially what this function already does), but as a Model method (maybe Model.render_to), or Model.rasterize_to? Maybe something like this (very rough sketch):
def raster(self, to=None, copy=False):
if to is None:
img = np.zeros((self.bbox[0][1], self.bbox[1][1])) # i.e. make an empty image
# Alternatively, fill the background with NaNs?
else:
img = to # Add to an existing image that was passed in
# Also verify the size of the image and that the model is within its bounds
if copy:
img = img.copy()
y, x = np.mgrid[...]
output = self(x, y, bounded=True)
img[self.bbox] += output
# Optionally: Could support different blending modes as well as just add
# if copy=False the image is modified in-place, but is still returned also, for consistency
return img
I haven't fully investigated this yet, but I think there are still some rounding issues with the bounding boxes. They are not correctly rounded up or down. For example, when you call I also noticed you were using and similarly use the bbox itself and slicing to index the image, instead of using large array indices. |
|
@embray Thanks for looking into this! Confirmed on the rounding issue. The plan is to use Just to make sure I understand what you mean by a Regarding model sets, this was discussed and there were reasons we opted against using them that I don't recall exactly. @adonath or @cdeil? In general, I agree it would make sense to implement this in |
There was a problem hiding this comment.
I think this rounding / edge handling should be split out into a separate function.
These two lines are pretty hard to read and could be where the off-by-one bug you've seen for some cases hides.
Maybe you can find it by making this code more readable (e.g. by splitting it across 2x as many lines) or by implementing tests for it (e.g. bounding box edge exactly on the pixel edge, or shifted by 0.05 or 0.45 or 0.5 or 0.55 or 0.95)?
There was a problem hiding this comment.
Maybe this function could be called bounding_box_to_slice?
It would take float bounding box positions as input and return integer array slice indices, so that the bounding box is fully inside the pixels covered by the slice.
So it should include the astype(int) rounding computation that you do at the moment further down, in the meshgrid call.
|
Sorry, I haven't been following this. I'll try to review it very soon (unless @embray beats me to it). |
|
I can use this kind of functionality in my work as well. A natural extension would be to have this integrated in the |
Implement `bounding_box` model property and `render_model` function
|
@nden – Thank you! |
|
Sorry, I fell behind on being able to review this--I think would have preferred not to merge as-is, unless there was some agreement to make changes in a follow-up PR. But I'll try to catch up on what the latest conversation was. Update: Ah, I understand now GSoC was coming to an end and we wanted to get this merged first. In that case that's fine, sorry for whining. And sorry especially for falling behind on code review. (Also please don't merge PRs without first assigning a milestone). |
There was a problem hiding this comment.
This could still use _ instead of i since i isn't actually used, but no matter (I think it's clearer since it emphasises that the loop variable is not actually used)
|
Some nitpicks above, but otherwise this is looking great. Love the example image. |
|
@adonath @patti Thanks for the explanation of the argument against using slices. This makes sense to me and is nice and more generalizable. I do think it would be nice to be able to get the bounding box as a slice/tuple of slices where applicable though (such as when the coordinates are pixel coordinates). This, in addition to some other issues, is leading me to think that there should be some kind of tuple subclass for representing bounding boxes. This subclass could include a Not a high priority but something to consider. |
This was my first thought as well. Though after some (much) earlier discussion (now buried by the terrible GitHub discussion interface), there was some agreement that maybe it would be too confusing to lump this functionality into the normal I could go either way to be honest, but there were some good points (that don't recall exactly) made in favor of using a separate method. (Edit: Ah right, that discussion was here: #3909 (diff))
Yes, that's what I was thinking as well. I think for a compound model the |
There was a problem hiding this comment.
This should be shortened like:
`Gaussian2D.bounding_box_default <astropy.modeling.Gaussian2D.bounding_box_default>`
That reminds me also, this is another good reason to have a default (NotImplementedError) implementation of bounding_box_default on the base class, so that there is always a docstring for it.
|
What @adonath said. I could probably try to address a few of my own comments too if it will help move things along (since most of them were minor nitpicks). |
Implementation of a
bounding_boxproperty for theModelclass, andrender_modelfunction to efficiently evaluate models on large images or other arrays, e.g. a large sky image containing many unique sources. Each source model needs only to be evaluated within relevant limits."Effficient Modeling Rendering with Bounding Boxes"
Tests have been written for 1D, 2D, and 3D models, but this functionality can presumably extend to N-dimensions.
Remaining Tasks:
astropy/docs/modeling/bounding_boxes.rstdemoing a large sky-model use-case.