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

[PROTO] Framework for lite "fast" formulary functionality #1098

Draft
wants to merge 24 commits into
base: main
Choose a base branch
from

Conversation

rocco8773
Copy link
Member

This is a prototype for developing a framework to implement lite ("lightweight") formulary functionality whose design intent is for computation. The current formulary functionality is constructed around Astropy's units for user convenience and to reduce calculation errors from improper unit use. This is great, but yields very slow executing code. The goal here is to develop a framework that still has our traditional formulary functionality but also an intuitive lite version for computation.

This prototype is developed for the thermal_speed function. The approach this prototype takes is...

  1. A lightweight thermal speed function is developed as thermal_speed_lite. The formula this function has is based on SI units, but there is no argument validations (values or units). The assumption here is the user knows what is going and can insure proper values are passed to the function. The function is also decorated with numba.njit.
  2. The previous thermal_speed function exists as it always has, but now utilizes thermal_speed_lite within the code for calculation.
  3. A decorator add_lite is developed to bind thermal_speed_lite to thermal_speed as thermal_speed.lite, as well as update thermal_speed's docstrings to indicate the addition of lite.

This all works something like...

@preserve_signature  # this is needed since njit destroys the signature
@numba.njit
def thermal_speed_lite(...):
    ...

@add_lite(thermal_speed_lite)
@validate_quantities(...)
@particle_inpute
def thermal_speed(...):
    ...

The use now looks like...

>>> T = 5 * u.K
>>> par = Particle("He+)
>>> thermal_speed(T, par)
<Quantity 144.13705752 m / s>

>>> %timeit thermal_speed(T, par)
717 µs ± 34.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

>>> # now using lite
>>> coef = thermal_speed.coefficients[3]["most_probable"]  # coefficients is also bound using add_lite
>>> thermal_speed.lite(T.value, par.mass.value, coef)
144.13705751725402

>>> %timeit thermal_speed.lite(T.value, par.mass.value, coef)
71.7 µs ± 3.16 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

>>> # better performance can be achieved if units don't have to be used at all
>>> T_unitless, mass_unitless = T.value, par.mass.value
>>> %timeit thermal_speed.lite(T_unitless, mass_unitless, coef)
397 ns ± 5.09 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

See further discussions in #1090

@github-actions github-actions bot added docs PlasmaPy Docs at http:https://docs.plasmapy.org plasmapy.formulary Related to the plasmapy.formulary subpackage plasmapy.utils Related to the plasmapy.utils subpackage labels Apr 2, 2021
@rocco8773 rocco8773 self-assigned this Apr 2, 2021
@rocco8773 rocco8773 added this to In Progress | Draft in Formulary Development via automation Apr 2, 2021
@StanczakDominik
Copy link
Member

posted a minute ago

Good timing on my part! 😉

@codecov
Copy link

codecov bot commented Apr 2, 2021

Codecov Report

Merging #1098 (013b613) into master (5dd2547) will decrease coverage by 0.03%.
The diff coverage is 91.42%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1098      +/-   ##
==========================================
- Coverage   96.91%   96.88%   -0.04%     
==========================================
  Files          70       70              
  Lines        6928     6956      +28     
==========================================
+ Hits         6714     6739      +25     
- Misses        214      217       +3     
Impacted Files Coverage Δ
plasmapy/utils/decorators/helpers.py 96.72% <90.00%> (-3.28%) ⬇️
plasmapy/formulary/parameters.py 98.68% <92.85%> (-0.41%) ⬇️
plasmapy/utils/decorators/__init__.py 100.00% <100.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 5dd2547...013b613. Read the comment docs.

StanczakDominik
StanczakDominik previously approved these changes Apr 2, 2021
Copy link
Member

@StanczakDominik StanczakDominik left a comment

Choose a reason for hiding this comment

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

The goal here is to develop a framework that still has our traditional formulary functionality but also an intuitive lite version for computation.

I personally hate the fact that we're still enjoying zero-ver, but already have traditions around functionality 😉

Looks great! A bunch of minor text fix suggestions, but I have no complaints about the implementation!

Comment on lines +549 to +552
"""
Dictionary of various coefficients used in calculating the
`~plasmapy.formulary.parameters.thermal_speed`.
"""
Copy link
Member

Choose a reason for hiding this comment

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

Minor rewrite:

Suggested change
"""
Dictionary of various coefficients used in calculating the
`~plasmapy.formulary.parameters.thermal_speed`.
"""
"""
Dictionary of various coefficients used for
`~plasmapy.formulary.parameters.thermal_speed`, with the dimensionality
as the key.
"""

Comment on lines +559 to +560
A lite weight version of `thermal_speed` intended for computational used
and, thus, does not do any argument validation/conditioning.
Copy link
Member

Choose a reason for hiding this comment

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

Minor rewrite:

Suggested change
A lite weight version of `thermal_speed` intended for computational used
and, thus, does not do any argument validation/conditioning.
A lightweight version of `thermal_speed` intended for computational use
which does not do any argument validation/conditioning.

**Aliases:** `vth_`
**Aliases:** `~plasmapy.formulary.parameters.vth_`

**Lightwieght Version:** `~plasmapy.formulary.parameters.thermal_speed_lite`
Copy link
Member

@StanczakDominik StanczakDominik Apr 2, 2021

Choose a reason for hiding this comment

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

Typo fix

Suggested change
**Lightwieght Version:** `~plasmapy.formulary.parameters.thermal_speed_lite`
**Lightweight Version:** `~plasmapy.formulary.parameters.thermal_speed_lite`

and wow, with the suggestions interface and the stars around it, this really jumps out at you like a coke ad.


# different methods, as per https://en.wikipedia.org/wiki/Thermal_velocity
try:
coef = _coefficients[ndim]
coeff = thermal_speed_coefficients[ndim]
Copy link
Member

Choose a reason for hiding this comment

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

Just taking the opportunity to avoid overwriting the var name later with a minor refactor

Suggested change
coeff = thermal_speed_coefficients[ndim]
coeffs_for_dim = thermal_speed_coefficients[ndim]

except KeyError:
raise ValueError("{ndim} is not a supported value for ndim in thermal_speed")
try:
coef = coef[method]
coeff = coeff[method]
Copy link
Member

Choose a reason for hiding this comment

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

continues minor refactor

Suggested change
coeff = coeff[method]
coeff = coeffs_for_dim[method]



vth_ = thermal_speed
""" Alias to :func:`thermal_speed`. """
""" Alias to :func:`~plasmapy.formulary.parameters.thermal_speed`. """
Copy link
Member

Choose a reason for hiding this comment

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

Works nicely with #1099, too! 😄

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I ran across this same thing when working on this PR. I actually thought I already made this change. I think we need to get in the practice of using the explicit link since `thermal_speed` would just look for the variable in the current global() scope.

@@ -142,3 +142,51 @@ def wrapper(*args, **kwargs):
wrapper.__signature__ = inspect.signature(f)

return wrapper


def add_lite(flite, attrs=None, scope=None):
Copy link
Member

Choose a reason for hiding this comment

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

Maybe "register_lite", come to think of it? I think "register" has a pretty common meaning for decorators.

Copy link
Member Author

Choose a reason for hiding this comment

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

When I think "register" I'm think it's registering the functionality with some kind of table/database. How about bind_lite? That's closer to what it is actually doing.

Copy link
Member

Choose a reason for hiding this comment

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

That sounds great!

def wrapper(*args, **kwargs):
return f(*args, **kwargs)

wrapper_doc = inspect.cleandoc(getattr(wrapper, "__doc__", """"""))
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
wrapper_doc = inspect.cleandoc(getattr(wrapper, "__doc__", """"""))
wrapper_doc = inspect.cleandoc(getattr(wrapper, "__doc__", ""))

since

> """""" == ""
True

plasmapy/utils/decorators/helpers.py Show resolved Hide resolved
@@ -5,6 +5,7 @@ h5py >= 2.8
lmfit >= 1.0.1
matplotlib >= 2.0
mpmath >= 1.0.0
numba
Copy link
Member

Choose a reason for hiding this comment

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

Once you start adding tests for this, tox-minimal-37 will complain since it has its own list of dependencies. I'm adding that in tox.ini for you.

Copy link
Member Author

Choose a reason for hiding this comment

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

I thought we already had numba for some reason, so I was surprised when all the tests started failing because of this. Do you have any restrictions related to which numba version we should take?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, we experimented with adding it in #953; at the time, Py3.9 was released, numba completely broke on it with its bytecode changes, and we didn't want to add version-dependent numba-or-nonumba logic. 0.53 is out now; things should be fine, as py39 will just (I think...) default to that as the only compatible dependency. Though we should look at the CI logs and check which runners get which version, just in case.

I don't have a clean lower bound on which numba versions are fine, though.

Formulary Development automation moved this from In Progress | Draft to In Progress | Review Apr 2, 2021
Copy link
Member

@pheuer pheuer left a comment

Choose a reason for hiding this comment

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

This looks great @rocco8773 !! I'm so excited about this. I've suggested a few changes - let me know what you think.

The documentation looks great: the only part I don't love is that the lite versions of the functions appear alongside the main functions and the aliases in the list of functions. But, I think we should just accept that for now. When we figure out a solution for the aliases, hopefully that will improve this situation too.

thermal_speed_lite,
attrs=[("coefficients", "thermal_speed_coefficients")],
scope=globals(),
)
@check_relativistic
@validate_quantities(
T={"can_be_negative": False, "equivalencies": u.temperature_energy()},
mass={"can_be_negative": False, "can_be_nan": True},
)
@particles.particle_input
def thermal_speed(
Copy link
Member

Choose a reason for hiding this comment

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

The docstring for thermal_speed includes a table of the coefficients: maybe we should add a link next to that to the thermal_speed_coefficients variable to make more clear that these contain the same information.

Copy link
Member Author

Choose a reason for hiding this comment

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

Agreed! I'll get to this. I haven't refined and the docstrings because I didn't want to spend the time on it until the framework was approved.

@@ -142,3 +142,51 @@ def wrapper(*args, **kwargs):
wrapper.__signature__ = inspect.signature(f)

return wrapper


def add_lite(flite, attrs=None, scope=None):
Copy link
Member

Choose a reason for hiding this comment

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

I think this might be better split into two functions, eg. register_lite and register_attrs. Or, maybe flite should also be a kwarg? I'm just thinking that currently attaching an attribute without also attaching a lite function would not be supported, but that seems like it should be possible.

Copy link
Member

Choose a reason for hiding this comment

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

Hmm, what use case would you see for that?

Copy link
Member

Choose a reason for hiding this comment

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

I don't see one now: just following the "one function one job" principle.

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 is doing "one job" I added the ability to add other attributes than "lite", because sometimes you need additional attributes to properly use "lite". For example, for thermal_speed.lite to properly work you also need the coefficients. So, think of add_lite as binding all required "lite" functionality (not just the lite function) to get <func>.lite to work.

Copy link
Member Author

Choose a reason for hiding this comment

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

Additionally, to get the documentation to come out correctly and cleanly then the lite function and required attributes needs to be added at the same time.

Copy link
Member

Choose a reason for hiding this comment

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

I see that. I guess I'm fine with this implementation now: if we need to add attributes separately in the future I guess we can always break this function apart then.

"""
if attrs is None:
attrs = []
elif scope is None:
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason to not just set scope=globals() by default?

Copy link
Member

@StanczakDominik StanczakDominik Apr 2, 2021

Choose a reason for hiding this comment

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

I suspect this part could be optimized out given some tinkering. Maybe sys.modules(__module__) combined with importlib and inspect.getmembers in some funny way...

But at this point, if you just bluntly set globals as a default from helpers.py, it's going to "inherit the scope of helpers.py" rather than the local environment of the function that's being registered.

This party is trickier than it seems, I think!

Copy link
Member

Choose a reason for hiding this comment

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

Got it - that makes sense (as much as anything regarding scopes does)

Copy link
Member Author

Choose a reason for hiding this comment

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

But at this point, if you just bluntly set globals as a default from helpers.py, it's going to "inherit the scope of helpers.py" rather than the local environment of the function that's being registered.

Yes, this is what was happening when globals() was the default value.

@preserve_signature
@njit
def thermal_speed_lite(T, mass, coeff):
"""
Copy link
Member

Choose a reason for hiding this comment

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

This docstring should still provide information on the parameters and returns so those appear in the documentation. Right now in the docs there's no indication that this function doesn't take units, or where one should look to determine coeff

@pheuer
Copy link
Member

pheuer commented Apr 2, 2021

@StanczakDominik Let's continue the conversation from #1099 about separately documenting lite functions here. It sounds like we have three possible solutions in mind so far:

  1. Leave lite functions alone: let them appear right next to the parent functions in the docs.

    • Pros: Less work, no additional code
    • Cons: Potentially doubles the number of visible functions. Could be confusing when some functions have lite versions but others don't yet.
  2. Use "semi-dynamically generated module" wizardry to sort "_lite" functions into a separate module under the hood (like Semi-dynamically generated aliases submodule  #1099 does for aliases).

    • Pros: Separates the lite functions visually in the docs.
    • Cons: Possible unintended consequences? No one understands sphinx.
  3. Remove lite functions from all, document them manually in the docstrings of their main functions (possibly with a decorator that automatically inserts the lite function docstring at the end of the main function).

    • Pros: Cleaner documentation (for the main functions)
    • Cons: Harder to find documentation on lite functions? Sphinx might complain about non-private functions being left out of __all__?

Obligatory tag @namurphy because trade-off analysis 😃

@StanczakDominik
Copy link
Member

StanczakDominik commented Apr 2, 2021

See, Nick? We're learning! 😸

The con for 1 is pretty heavy. 2 - hard to tell; Sphinx is a riddle. 3 would take a quick prototype that I would absolutely do right now if I were less conscious of my current procrastination driven development binge.

I would say 3 is acceptable if the lite functions appear in the index. Since they are actually separate objects, I suspect they might. But again, (:egypt: :bird: :lion: :woman:) is a riddle.

@rocco8773
Copy link
Member Author

The goal here is to develop a framework that still has our traditional formulary functionality but also an intuitive lite version for computation.

I personally hate the fact that we're still enjoying zero-ver, but already have traditions around functionality 😉

I think this is why I've become more in favor of the date base versioning. I think plasmapy will always have a combination of mature and development functionality. That's why I consider it perpetual beta.

Looks great! A bunch of minor text fix suggestions, but I have no complaints about the implementation!

Great! I wanted to get everyone's opinion on the framework before spending the time to put the bells and whistles on all the functionality and docstrings. I can start refining everything now.

@rocco8773
Copy link
Member Author

rocco8773 commented Apr 2, 2021

  1. Leave lite functions alone: let them appear right next to the parent functions in the docs.

    • Pros: Less work, no additional code
    • Cons: Potentially doubles the number of visible functions. Could be confusing when some functions have lite versions but others don't yet.

I don't see that as a con. <func>_lite is part of the public api and thus should be documented. Hiding it from the documentation significantly reduces discoverability, which is a major pain for users. Since <func>_lite is apart of the public api, a use would see it, go look for it in the documentation, and then not be able to find it. This is BAD! I'd rather have everything listed than not have something discoverable.

  1. Use "semi-dynamically generated module" wizardry to sort "_lite" functions into a separate module under the hood (like Semi-dynamically generated aliases submodule  #1099 does for aliases).
  • Pros: Separates the lite functions visually in the docs.
  • Cons: Possible unintended consequences? No one understands sphinx.

This could be done "simply" for aliases, but I don't like it for <func>_lite stuff. We'd need to develop something along the lines of automodsumm to accomplish this, see my #1099 (comment). This if feels is the most viable route.

  1. Remove lite functions from all, document them manually in the docstrings of their main functions (possibly with a decorator that automatically inserts the lite function docstring at the end of the main function).
  • Pros: Cleaner documentation (for the main functions)
  • Cons: Harder to find documentation on lite functions? Sphinx might complain about non-private functions being left out of all?

As I stated here, this is not an option. Not including <func>_lite in __all__ means <func>_lite would never be documented and, thus, the lite functionality would never be documented. It is absolutely necessary to include it.

@StanczakDominik
Copy link
Member

Well, given #1100, let's definitely go with the automodsumm one! I would like to try my hand at adapting the solution from there to the _lite functions case - I guess the whole of #1099 resulted from me not knowing enough sphinx and I'd like to fix that! So I'd be completely fine with leaving them in the main table for now, with the understanding that I'll fix them later.

@StanczakDominik StanczakDominik added the prototype 🏗️ Trying out an implementation on a trial basis label Apr 3, 2021
@pheuer
Copy link
Member

pheuer commented Apr 3, 2021

<func>_lite is part of the public api and thus should be documented.

I definitely agree with this! My only concern is that I think having one very long list of functions in the documentation makes the package less approachable, especially to students new to plasma physics.

"Who are these crazy people and why do they have SO MANY DIFFERENT FREQUENCIES? Maybe I should transfer to a condensed matter group..."

I agree the automodsum implementation a la #1100 is the ideal way of handling this. I'd like to see a list of "Functions", followed by "Lite Functions" and "Aliases" (in whatever order). Then the first thing the user sees is a more manageable list of the main functions, and they have to scroll down to see the different versions of those functions.

@rocco8773
Copy link
Member Author

As I stated in #1100 (comment), I think I have a solution for grouping lite functions, so stay tuned for a prototype.

As I stated before, I think the lite functions should be document like everything else and I do agree they cause clutter on the documentation page. I like the ability to group them in a separate section that explains what they are.

@rocco8773 rocco8773 added the plasmapy.diagnostics Related to the plasmapy.diagnostics subpackage label Jun 30, 2021
@namurphy namurphy added the status: dormant PRs that are stalled label May 26, 2023
@namurphy namurphy added this to the Future milestone Apr 9, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
docs PlasmaPy Docs at http:https://docs.plasmapy.org plasmapy.diagnostics Related to the plasmapy.diagnostics subpackage plasmapy.formulary Related to the plasmapy.formulary subpackage plasmapy.utils Related to the plasmapy.utils subpackage prototype 🏗️ Trying out an implementation on a trial basis status: dormant PRs that are stalled
Projects
Formulary Development
  
In Progress | Review
Development

Successfully merging this pull request may close these issues.

None yet

4 participants