Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cosmo: allow broadcasting in z_at_value #11778

Merged
merged 4 commits into from
Jul 16, 2021

Conversation

nstarman
Copy link
Member

@nstarman nstarman commented May 24, 2021

Signed-off-by: Nathaniel Starkman (@nstarman) nstarkman@protonmail.com

Description

z_at_value only works on scalars. numpy.vectorize cannot be used for broadcasting because it does not preserve units.
This PR renames the current z_at_value to scalar_z_at_value and the new z_at_value correctly broadcasts inputs, calling scalar_z_at_value for each element.

Edit: this does NOT obviate the need for #11361. There is definitely good reason to have an interpolated approach for large arrays. This PR just enables broadcasting on the current function.

Fixes: #11949

@github-actions
Copy link

👋 Thank you for your draft pull request! Do you know that you can use [ci skip] or [skip ci] in your commit messages to skip running continuous integration tests until you are ready?

@nstarman nstarman force-pushed the cosmo_vectorize_z_at_value branch 3 times, most recently from 2e22891 to f95450d Compare May 24, 2021 18:18
@nstarman nstarman marked this pull request as ready for review May 24, 2021 18:34
@dhomeier
Copy link
Contributor

I won't have time to provide an actual code review this week, but can you describe how this would be placed in the context of #11361? Specifically, in the scheme of #11361 (comment)
this could constitute the branch taken for moderate input lengths; with larger arrays switched to the interpolation scheme.

Also there is probably some overhead in repeatedly calling scalar_z_at_value that might be removed when using identical method etc., but I am not sure if it has measurable impact.

@nstarman
Copy link
Member Author

I won't have time to provide an actual code review this week, but can you describe how this would be placed in the context of #11361? Specifically, in the scheme of #11361 (comment)
this could constitute the branch taken for moderate input lengths; with larger arrays switched to the interpolation scheme.

I think it's more in the vein of #11361 (comment) and #11361 (comment), where it was suggested that the implementation in #11361 is sufficiently different that it should be in a separate function "z_at_array", and that "z_at_value"� should just be vectorized to a fancy for-loop. No speed increase. Just a lot more convenient.
So #11361 can and should go ahead for doing fast calculations on large arrays. This just makes "z_at_value"� broadcast as expected.

Also there is probably some overhead in repeatedly calling scalar_z_at_value that might be removed when using identical method etc., but I am not sure if it has measurable impact.

I clocked ``z_at_value` as 0.5 ms slower for a single calculation and 4 ms slower for an array of 50
Screen Shot 2021-05-25 at 14 52 56. Surely there are further optimizations to be found, but this seems like it scales reasonably well.

@dhomeier
Copy link
Contributor

I think it's more in the vein of #11361 (comment) and #11361 (comment), where it was suggested that the implementation in #11361 is sufficiently different that it should be in a separate function "z_at_array", and that "z_at_value"� should just be vectorized to a fancy for-loop. No speed increase. Just a lot more convenient.
So #11361 can and should go ahead for doing fast calculations on large arrays. This just makes "z_at_value"� broadcast as expected.

Ah, I think I had missed that part of the discussion or did not realise that it was still pertinent for wrapping up #11361.
But I agree, since the interpolation method makes it functionally different and it will indeed produce slightly different results, it is entirely sensible to put that one into a separate function.

Also there is probably some overhead in repeatedly calling scalar_z_at_value that might be removed when using identical method etc., but I am not sure if it has measurable impact.

I clocked ``z_at_value` as 0.5 ms slower for a single calculation and 4 ms slower for an array of 50

That's actually not measuring the overhead I had in mind – I was rather thinking about doing the setup for the minimiser method only once (when using the same method for all values) and/or vectorising the fval_zmin, fval_zmax, bracket calculation etc. and basically only looping over the minimize_scalar itself.
But when allowing even method as an array input as you are doing here (which I am still unsure if it is a good idea) that setup would become rather tricky, and from my timings for #11080 I still expect by far the most time to be spent in minimize_scalar, so it's probably not worth the effort optimising it here further.

@nstarman
Copy link
Member Author

nstarman commented Jun 1, 2021

@dhomeier. The more I think about it, the more I agree that 'method' should not be broadcasted. I've excluded it from the broadcast.

I also add a test to check whether numpy.vectorize works on quantities and this whole PR can be replaced. 🤞 some time in the future it will fail and we can just use numpy.vectorize.

@nstarman nstarman force-pushed the cosmo_vectorize_z_at_value branch from 6ca0935 to f3958d8 Compare June 6, 2021 19:37
@nstarman
Copy link
Member Author

This PR is premised on the assumption that numpy.vectorize won't soon work on Quantities AND that we should not subclass numpy.vectorize and make the fix ourselves (presumably to be upstreamed). I'm looking at a number of other functions in astropy/cosmology that are not vectorized and I'm wondering about this two assumptions.

@mhvk as a resident expert on numpy...

@mhvk
Copy link
Contributor

mhvk commented Jun 24, 2021

@nstarman - it would definitely be good to get np.vectorize to work - just never has risen to high enough priority to think what is blocking it. Probably best, though, to discuss in a separate issue: Could you make a minimal example that shows that a function that itself works with Quantity cannot be used with np.vectorize?

@nstarman nstarman marked this pull request as draft June 24, 2021 18:57
@nstarman
Copy link
Member Author

nstarman commented Jun 26, 2021

👍 See #11893 for a semi-functional example implementation.

astropy/cosmology/funcs.py Outdated Show resolved Hide resolved
@nstarman nstarman marked this pull request as ready for review July 13, 2021 14:47
@nstarman nstarman requested a review from mhvk July 13, 2021 14:47
@nstarman nstarman force-pushed the cosmo_vectorize_z_at_value branch 2 times, most recently from 6920943 to f4173a6 Compare July 13, 2021 16:40
@nstarman
Copy link
Member Author

Ok, if all this passes, I still need to do a full docstring for z_at_value. I think it now merits documentation. The bracket rules in particular need explication.

Copy link
Contributor

@mhvk mhvk left a comment

Choose a reason for hiding this comment

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

Somewhat random comments. Does look good generally.

astropy/cosmology/funcs.py Show resolved Hide resolved
astropy/cosmology/funcs.py Outdated Show resolved Hide resolved
astropy/cosmology/funcs.py Outdated Show resolved Hide resolved
astropy/cosmology/funcs.py Outdated Show resolved Hide resolved
@nstarman nstarman marked this pull request as draft July 13, 2021 21:08
@nstarman nstarman added this to Review in progress in Cosmology, the Expansion Jul 14, 2021
@nstarman nstarman moved this from Review in progress to In progress in Cosmology, the Expansion Jul 14, 2021
@astropy astropy deleted a comment from pep8speaks Jul 15, 2021
@nstarman nstarman marked this pull request as ready for review July 15, 2021 21:43
@nstarman nstarman added this to the v5.0 milestone Jul 15, 2021
@nstarman nstarman force-pushed the cosmo_vectorize_z_at_value branch 4 times, most recently from b9519a3 to 7daf75e Compare July 16, 2021 01:32
@nstarman nstarman requested a review from mhvk July 16, 2021 01:53
@nstarman
Copy link
Member Author

If it's approved, I'm going to squash some of my commit history. So don't merge, thanks!

@pllim
Copy link
Member

pllim commented Jul 16, 2021

So don't merge, thanks!

Might be safer to turn this into draft then, but up to you. Thanks!

Copy link
Contributor

@mhvk mhvk left a comment

Choose a reason for hiding this comment

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

Looks good! Two very small comments that are easy to implement, and a question whether you even want to expose z_at_scalar_value.

raise TypeError(f"`bracket` has dtype {bracket.dtype}, not 'O'")

# make multi-dimensional iterator for all but `method`, `verbose`
# TODO: figure out 'reduce_ok', so scalar returns scalar
Copy link
Contributor

Choose a reason for hiding this comment

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

I thought this was now OK!?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's fixed in that I manually check for a scalar and correct the output. I think there's some way to get 'reduce_ok' to do this automatically, but I can't get the examples in the numpy documentation to work here.

Copy link
Member Author

@nstarman nstarman Jul 16, 2021

Choose a reason for hiding this comment

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

Hence the note for myself or an enterprising contributor. Low priority, but it would be nice.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, I thought it was the problem that the code returned a 1-D array rather than an array scalar. I think reduce_ok is really meant for dimensions that are reduced over (i.e., it tells the iterator that it is OK for a dimension to be missing or be unity in the output).

Copy link
Member Author

Choose a reason for hiding this comment

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

In the first example in https://numpy.org/doc/stable/reference/arrays.nditer.html#reduction-iteration it seems to work for degrading to a scalar.

Copy link
Contributor

Choose a reason for hiding this comment

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

Indeed, and I did not know that. But I think it is not directly relevant, since you are not actually reducing over any dimension..

astropy/cosmology/funcs.py Outdated Show resolved Hide resolved
astropy/cosmology/tests/test_funcs.py Outdated Show resolved Hide resolved
@@ -0,0 +1,5 @@
Rename ``z_at_value`` to ``z_at_scalar_value`` and add a wrapper function
Copy link
Contributor

Choose a reason for hiding this comment

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

The overhead from setting up the iterator is likely small compared to the actual function call, so might it make sense to just state that z_at_value can now handle arrays? I.e., not mention z_at_scalar_value at all?

Copy link
Contributor

Choose a reason for hiding this comment

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

The overhead from setting up the iterator is likely small compared to the actual function call,

That was the one clocked in #11778 (comment), right?

Copy link
Member Author

Choose a reason for hiding this comment

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

That was a previous implementation. I'll reclock.

Copy link
Member Author

@nstarman nstarman Jul 16, 2021

Choose a reason for hiding this comment

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

It's a pretty negligible difference. And scales as we'd expect.

Screen Shot 2021-07-16 at 12 16 45

Copy link
Contributor

Choose a reason for hiding this comment

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

The extra overhead from calling z_at_value on an array is expected? Not a serious difference, just wondering where that additional time comes in...

Copy link
Member Author

Choose a reason for hiding this comment

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

Looking at the above, I'm inclined to agree.
The point of z_at_scalar_value public is that it's more performant. Except it basically isn't. I'm going to make it a private function. The interpolated case (#11361) should introduce a new function, perhaps z_at_interp_value. If I can make z_at_scalar_value more performant relative to z_at_value, I'll make it public.

Copy link
Contributor

Choose a reason for hiding this comment

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

@dhomeier - list comprehension is really quite fast, hard to np.nditer to match, with all the broadcasting etc.

Copy link
Contributor

@dhomeier dhomeier Jul 16, 2021

Choose a reason for hiding this comment

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

The point of z_at_scalar_value public is that it's more performant. Except it basically isn't. I'm going to

Well, 12-15 % faster is still faster, even if perhaps not enough to justify keeping it public. What was more surprising to me was that even looping over it 100 times is still 15 % faster than calling z_at_value directly on the value. Unless you are seeing some caching artefacts in timeit, but I can't really see where they'd come in.

Copy link
Contributor

Choose a reason for hiding this comment

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

@dhomeier - list comprehension is really quite fast, hard to np.nditer to match, with all the broadcasting etc.

Sounds sensible – I had not looked at the internals since my early incomplete review, so I thought there was still some code that the vectorised version did not execute on every single element of fval; but seeing now that there is also a bunch of extras included for accepting sequences of bracket etc.

Copy link
Member Author

Choose a reason for hiding this comment

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

That's probably from the fact that z_at_value needs to consider how to broadcast all the values. I think if I were to do array nesting and so forth we would see that the manual implementation is equivalent to z_at_value. z_at_value just doesn't know when it can simplify. If python allowed for multiple dispatch..

Cosmology, the Expansion automation moved this from In progress to Reviewer approved Jul 16, 2021
@nstarman nstarman force-pushed the cosmo_vectorize_z_at_value branch 2 times, most recently from aa1badb to 1a1515b Compare July 16, 2021 17:17
@nstarman
Copy link
Member Author

nstarman commented Jul 16, 2021

Ok. Tests should pass and I've cleaned the commit history.
Edit: and some spacing in the docstrings.

@nstarman nstarman requested a review from dhomeier July 16, 2021 17:18
Signed-off-by: Nathaniel Starkman (@nstarman) <nstarkman@protonmail.com>
Signed-off-by: Nathaniel Starkman (@nstarman) <nstarkman@protonmail.com>
Signed-off-by: Nathaniel Starkman (@nstarman) <nstarkman@protonmail.com>
Signed-off-by: Nathaniel Starkman (@nstarman) <nstarkman@protonmail.com>
@astropy astropy deleted a comment from pep8speaks Jul 16, 2021
@mhvk mhvk merged commit ee1577d into astropy:main Jul 16, 2021
Cosmology, the Expansion automation moved this from Reviewer approved to Done Jul 16, 2021
@mhvk
Copy link
Contributor

mhvk commented Jul 16, 2021

Nice!

@nstarman nstarman deleted the cosmo_vectorize_z_at_value branch July 16, 2021 18:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

Coordinates: Distance.z does not work with vectors
4 participants