Skip to content

Implement bounding_box model property and render_model function - #3909

Merged
nden merged 2 commits into
astropy:masterfrom
patti:boundingboxes
Aug 17, 2015
Merged

nden merged 2 commits into
astropy:masterfrom
patti:boundingboxes

Conversation

@patti

@patti patti commented Jul 2, 2015

Copy link
Copy Markdown
Contributor

Implementation of a bounding_box property for the Model class, and render_model function 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:

  • Write high level docs for render_model. Add subpage astropy/docs/modeling/bounding_boxes.rst demoing a large sky-model use-case.
  • Add changelog entry
  • Revert autopep8 changes to unrelated lines of code.

@adonath

adonath commented Jul 2, 2015

Copy link
Copy Markdown
Member

I will review this later today.

Comment thread astropy_helpers Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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?)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, do git submodule update then add the change to the branch. Then later on we can always squash the commits if we want.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@embray - Thanks for explaining in detail how to do the fixup commit ... very helpful!!!
@patti – If you haven't done this before, I'd suggest making a backup branch or just backup copy of the folder with your repo before you try it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the instructions @embray. Should be fixed now.

@cdeil

cdeil commented Jul 2, 2015

Copy link
Copy Markdown
Member

@patti – Thanks! Can't wait to use this for my analyses!

I'll let @adonath do the review of the API / implementation.
One thing you could do is translate your example notebook into tests and docs.

@embray @adonath For high-level docs, what do you think is best?

@cdeil

cdeil commented Jul 2, 2015

Copy link
Copy Markdown
Member

@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.

Comment thread astropy/modeling/utils.py Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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... )

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like the bounding box, the location is something the models should declare, so that you don't have to guess parameter names.

@adonath

adonath commented Jul 2, 2015

Copy link
Copy Markdown
Member

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...

@embray

embray commented Jul 2, 2015

Copy link
Copy Markdown
Member

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.

@adonath

adonath commented Jul 2, 2015

Copy link
Copy Markdown
Member

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
positions and shapes of the bbox.

@adonath

adonath commented Jul 2, 2015

Copy link
Copy Markdown
Member

@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.

@patti patti changed the title Implement bbox model property and render_model2sky function WIP: Implement bbox model property and render_model2sky function Jul 2, 2015
@patti

patti commented Jul 2, 2015

Copy link
Copy Markdown
Contributor Author

@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.

@patti

patti commented Jul 4, 2015

Copy link
Copy Markdown
Contributor Author

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?

@embray

embray commented Jul 5, 2015

Copy link
Copy Markdown
Member

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 model_set_axis=False can be read as "evaluate each model in the set on the same input".

You can then combine them into a single image just be calling image = np.sum(sources, axis=0). Unfortunately, for a case like this, this is extremely prohibitive on a typical desktop system, because you end up with 1000 800x1250 images, one for each source. However, if each model has a bounding box, we could change the behavior of model set evaluation to evaluate each model on its bounding box (in this case the .bbox property would probably return an array, with one entry for each model in the set). Since each bbox isn't the same size I guess it would end up having to be an array of different-sized arrays, but that would still be a lot better. Then one could build up the image like:

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] += source

I 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 :)

Comment thread astropy/modeling/utils.py Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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__.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@embray

embray commented Jul 5, 2015

Copy link
Copy Markdown
Member

I'm having a hard time trying to diagnose a bug where I get a row of zeros in the Gaussian2D bounded image

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 np.meshgrid you just call it with arguments like: np.arange(bbox[0],bbox[1]).astype(int). But astype(int) effectively takes the floor, where in this case it looks like you might be wanting to round to the closest integer instead.

I also noticed you were using np.meshgrid directly, but you can more simply use

y, x = np.mgrid[bbox[2]:bbox[3], bbox[0]:bbox[1]]

and similarly use the bbox itself and slicing to index the image, instead of using large array indices.
In fact, it might be nice if the bbox attribute just returned a pair of slice objects instead of a 4-tuple, so it can be used to directly slice an array.

@patti

patti commented Jul 5, 2015

Copy link
Copy Markdown
Contributor Author

@embray Thanks for looking into this! Confirmed on the rounding issue. The plan is to use extract_array and add_array in nddata.utils to save myself some hassle down the line anyway. I just haven't got that working quite yet.

Just to make sure I understand what you mean by a slice object, this would look like a tuple of arrays bbox = (x_slice, y_slice) corresponding to the coordinate indices of the bbox? Effectively, y_slice, x_slice = np.mgrid[bbox[2]:bbox[3], bbox[0]:bbox[1]], or do you mean something different?

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 model.call. @adonath didn't think it belonged in the super class- maybe because it's just more involved? Would this call some version of render_models2sky() that does live in the modeling.utils?

Comment thread astropy/modeling/utils.py Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@mwcraig

mwcraig commented Aug 14, 2015

Copy link
Copy Markdown
Member

@embray @nden -- friendly bump on this...any chance you could approve and merge in the next few days before @patti's GSOC ends? I'd offer to review but know nothing about the modeling package :)

@nden

nden commented Aug 14, 2015

Copy link
Copy Markdown
Contributor

Sorry, I haven't been following this. I'll try to review it very soon (unless @embray beats me to it).

@nden

nden commented Aug 17, 2015

Copy link
Copy Markdown
Contributor

I can use this kind of functionality in my work as well. A natural extension would be to have this integrated in the __call__ method, and also working with fitters and compound models. I guess for compound models the default would be the intersection of the all default bbox ranges but something similar to the custom inverse would be useful as well. This is a very good start on this topic. Thanks @patti !

nden added a commit that referenced this pull request Aug 17, 2015
Implement `bounding_box` model property and `render_model` function
@nden
nden merged commit 02dea69 into astropy:master Aug 17, 2015
@cdeil

cdeil commented Aug 17, 2015

Copy link
Copy Markdown
Member

@nden – Thank you!

@embray

embray commented Aug 19, 2015

Copy link
Copy Markdown
Member

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).

@embray embray added this to the v1.1.0 milestone Aug 19, 2015

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

@embray

embray commented Aug 19, 2015

Copy link
Copy Markdown
Member

Some nitpicks above, but otherwise this is looking great. Love the example image.
Sorry I didn't get to review sooner. I'll try to catch up on the past discussion now.

@embray

embray commented Aug 19, 2015

Copy link
Copy Markdown
Member

@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 .slice attribute or something of the sort that automatically converts to a slice when possible. Users wouldn't have to use this subclass directly since the implementation can automatically convert user-supplied tuples to this special class.

Not a high priority but something to consider.

@embray

embray commented Aug 19, 2015

Copy link
Copy Markdown
Member

@nden

I can use this kind of functionality in my work as well. A natural extension would be to have this integrated in the __call__ method

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 __call__ interface. Instead the render_model method would be added to models for this purpose.

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))

I guess for compound models the default would be the intersection of the all default bbox ranges but something similar to the custom inverse would be useful as well.

Yes, that's what I was thinking as well. I think for a compound model the default_bounding_box would be the intersection. But @patti's API should also already allow for setting a custom bounding box on a compound model.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@adonath

adonath commented Aug 19, 2015

Copy link
Copy Markdown
Member

@embray Thanks for the review!
@patti I think it's best to address these comments in the follow up PR #4040

@embray

embray commented Aug 19, 2015

Copy link
Copy Markdown
Member

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants