-
Notifications
You must be signed in to change notification settings - Fork 308
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
Add validation skipping to @validate_quantities #1089
base: main
Are you sure you want to change the base?
Conversation
…date_quantities decorators
…nto the decorated functions argument list
…d in validate_quantities
…g to python's docs)
…get grumpy when input arguments do not have nits
Codecov Report
@@ Coverage Diff @@
## master #1089 +/- ##
==========================================
- Coverage 96.91% 96.82% -0.09%
==========================================
Files 70 70
Lines 6928 6961 +33
==========================================
+ Hits 6714 6740 +26
- Misses 214 221 +7
Continue to review full report at Codecov.
|
@rocco8773 Thank you for trying this! It is good to get some concrete numbers on how much this could improve performance. However, I am still concerned that this current implementation doesn't go far enough. I ran my own execution time tests and replicated your findings: The problem is that the simplest possible version of this function executes in only 1 us! import numpy as np
from astropy.constants import k_B
k_B = k_B.si.value
_coefficients = {
1: {"most_probable": 0, "rms": 1, "mean_magnitude": 2 / np.pi},
2: {"most_probable": 1, "rms": 2, "mean_magnitude": np.pi / 2},
3: {"most_probable": 2, "rms": 3, "mean_magnitude": 8 / np.pi},
}
def fast_thermal_speed(T, m, ndim, method):
coeff = _coefficients[ndim][method]
return np.sqrt(coeff * T / m) The same is true for Here's some Timer unit: 1e-07 s
Line # Hits Time Per Hit % Time Line Contents
==============================================================
668 1000 44700.0 44.7 2.3 m = mass if np.isfinite(mass) else particles.particle_mass(particle)
669
670 # different methods, as per https://en.wikipedia.org/wiki/Thermal_velocity
671 1000 4999.0 5.0 0.3 try:
672 1000 5941.0 5.9 0.3 coef = _coefficients[ndim]
673 except KeyError:
674 raise ValueError(
675 f"Got unsupported value for argument 'ndim' ({ndim}), expect one of "
676 f"{list(_coefficients.keys())}."
677 )
678 1000 3948.0 3.9 0.2 try:
679 1000 5550.0 5.5 0.3 coef = coef[method]
680 except KeyError:
681 raise ValueError(
682 f"Got unsupported value for argument 'method' ({method}), expected one "
683 f"of {list(coef.keys())}."
684 )
685
686 1000 1045985.0 1046.0 54.1 speed = (k_B * T / m).value
687 1000 813817.0 813.8 42.1 speed = np.sqrt(coef * speed) * u.m / u.s
688
689 1000 6783.0 6.8 0.4 return speed In this case I was supplying dimensionless values ( The biggest issue is with the math with
Incidentally, while working on this I found a package for neatly running |
Peter, I agree with your point. the sky's (or rather the microsecond range) is definitely the limit here for optimization, but I still think making this PR a small incremental improvement is the best way to go. It's a speed-up we can take; it'd be silly not to do it. The more I think of your idea (outsourcing all unit IO checks to the skippable decorator), the more I'm starting to like it. So I'll cut your comment on that into a separate issue, if that's ok :) I think your second point is essentially #918, btw! And damn, spyder-line-profiler looks great! Makes me wish I wasn't living the terminal life... just a bit :D |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good changes in the right direction! They definitely still need tests, but I like the implementation.
raise ValueError( | ||
fsig = inspect.signature(fn) | ||
if "to_hz" in fsig.parameters: | ||
raise TypeError( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to codecov, it doesn't look like this case is actually being checked anywhere. Do we know it doesn't actually clash with to_hz?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I realized this when I was updating ValidateQuantities
, so I'll add the appropriate tests to angular_freq_to_hz
as well as to ValidateQuantities
.
plasmapy/formulary/parameters.py
Outdated
) | ||
|
||
speed = (k_B * T / m).value | ||
speed = np.sqrt(coef * speed) * u.m / u.s |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can squeeze out a tiny bit more performance out of this by preallocating the unit somewhere in the module:
[ins] In [1]: %timeit 1 * u.m / u.s
17.9 µs ± 89 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
[ins] In [2]: unit = u.m / u.s
[ins] In [3]: %timeit 1 * unit
5.69 µs ± 126 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
[ins] In [4]: %timeit u.Quantity(1, unit)
3.99 µs ± 94.7 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
But with #1090 in the works we can skip that if you'd prefer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this will work since you still need to set unit = u.m/u.s
within thermal_speed
and that is not being timed in your last two cases. So, it looks faster since you're not timing something that still need to be apart of thermal_speed
.
A work around, we could create a utility package, e.g. plasmapy.utils.unit_defaults
, where we define all of our default units. For example,
>>> import plasmapy.utils.unit_defaults as ud
>>> ud.speed == u.m / u.s
True
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think @StanczakDominik was suggesting that you could define unit
outside of the function (the way some of the constants are defined)? Then it would be evaluated once when the module is imported?
(Default `False`) When setting `True` NO value or unit validations will | ||
be performed on input arguments or returned objects by | ||
`~plasmapy.utils.decorators.validators.validate_quantities`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Default `False`) When setting `True` NO value or unit validations will | |
be performed on input arguments or returned objects by | |
`~plasmapy.utils.decorators.validators.validate_quantities`. | |
(Default `False`) When setting `True` NO value or unit validations will | |
be performed on the decorated function's input arguments or returned objects by | |
the `~plasmapy.utils.decorators.validators.validate_quantities` decorator. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm avoiding the use of "function" because the decorator works for more then functions, i.e. methods too.
@StanczakDominik I absolutely agree - this is a good first step in the right direction! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I looked through the addition and it looked good! I agree that the main thing remaining is just tests.
) | ||
bound_args.arguments[arg_name] = arg | ||
if self.allow_skipping and skip_validations: | ||
validations = {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Obviously needs a test
You have to remember the formulary was not originally written with large scale computation in mind, it was written to be a formulary. For convenience and explicitly it was decided to incorporate units and take the time penalty. Right now is really the first time the formulary is being used as a computation tool, which it originally wasn't designed to be. I absolutely agree we should put energy into speeding up the functionality, but there will always be a trade-off between the convenience and explicit of units with the speed for computation. It looks like the formulary is heading the way of being dual purpose so there will always be a trade-off.
I fundamentally like the idea of having the
I agree with this and is why I proposed the
@StanczakDominik Yep! |
I understand, and the initial decision to use units makes sense in that context. My point is just that, unless the time penalty is very small, it will remain impractical to use for computation. That said, this PR is definitely progress in the right direction! I just wanted to make sure we were all on the same page about the ultimate goal/requirement.
I see the problem: that sort of situation was why I originally proposed separating each function into a fast core and a slower wrapper. That approach does allow for more customization per function. Another option would be to refactor the offending functions so that their behavior is determined explicitly by keywords instead of implicitly by argument units? So it is: I guess count this as my vote for that kind of functionality, then. |
… calculation on an astropy Quantity
Good news and not so good news. I was able to get the execution time down to ~20 us for Now, slowly reapplying the points I mentioned above results in the following penalties
Removing the beneficial changes mentioned above results in... So, my thoughts at the moment...
|
Oh oh oh, I just got a great idea for a way to systematically test this flag. Hypothesis. I'll explain and throw in a pr to this branch later. |
Sounds like great progress! |
Unit and value validations (by
@validate_quantities
) can really slow down the performance of the decorated formulary functionality. This is not a big issue when a formulary item is being calculated once, but can be a huge detriment when the calculation needs to be made numerous times. This PR adds the validator setting keywordallow_skipping
tovalidate_quantities
(andValidateQuantities
) which then injects theskip_validations
keyword into the decorated functions argument list. The doctorate function's docstring is appended to reflect this addition.As test,
thermal_speed
is decorated such that skipping is allowed. This does not change the base behavior and is implemented like...The way
thermal_speed
is calculated is also modified such that the returned value always has units ofu.m / u.s
, regardless of if the input arguments have (correct) units or no units. I decided to do this since: (1) whenskip_validations==False
we are guaranteed input arguments are in SI and nothing will be affected and (2) for performance inputs arguments may be unitless whenskip_validations==True
and without this modification@check_relativistic
would be grumpy.At least for
thermal_speed
, this appears to reduce execution time to about 40% of the original execution time. Here is an example of how theskip_validations
keyword works and timing comparisons.