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

Added/modified 1D and 3D Maxwellian speed/velocity distributions #152

Merged
merged 17 commits into from
Nov 5, 2017

Conversation

lemmatum
Copy link
Contributor

Added and modified 1D and 3D Maxwellian speed and velocity distribution functions. These functions also include optional drift velocities/speeds. I've included tests for these new functions, but the 3D tests fail because scipy's QUADPACK based triple integral doesn't play nice with units. I'm open to suggestions on how to work around this!

Contributions towards #101 and #141

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.

Nice work overall! There are a few minor fixes remaining, and we have to figure out the problem with integration, but overall this smells of a quick merge :)


Returns
-------
f : Quantity
probability in Velocity^-1, normized so that:
probability in Velocity^-1, normalized so that:
Copy link
Member

Choose a reason for hiding this comment

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

Oh. Yeaaaah. ^^'

@@ -60,6 +65,8 @@ def Maxwellian_1D(v: units.m/units.s,

.. math::
f = \sqrt{\frac{m}{2 \pi k_B T}} \exp(-\frac{m}{2 k_B T} (v-V)^2)
f = (\pi * v_Th^2)^{-1/2} \exp(-(v - v_{drift})^2 / v_Th^2)
Copy link
Member

Choose a reason for hiding this comment

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

Could we add something like "or equivalently," or "two equivalent forms:" here?

That might break with the .. math:: .rst tag. We'll have to keep an eye on that.

Copy link
Contributor Author

@lemmatum lemmatum Oct 30, 2017

Choose a reason for hiding this comment

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

Oh, I was intending to remove the first form listed there. I think it makes more sense to look at the function as a comparison of velocities to obtain a probability with respect to the most likely velocity rather than having temperatures, and masses floating around. We can always add the thermal velocity function under "see also"

Copy link
Member

Choose a reason for hiding this comment

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

It probably wouldn't hurt too much to have both of those available, but it's not critical to have both here, I suppose!

# accounting for thermal velocity in 3D
vTh3D = np.sqrt(3) * vTh
vThSq = vTh3D ** 2
# Get particle velocity squared
Copy link
Member

Choose a reason for hiding this comment

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

This isn't a big deal, but, "relative" particle velocity, maybe, given it's shifted by the drift one?


Example
-------

Copy link
Member

Choose a reason for hiding this comment

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

Could definitely use some doctests here :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How do you guys run doctests?

Copy link
Member

Choose a reason for hiding this comment

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

That documentation update really can't come soon enough, can it? :D I'll try to automate and simplify the process a bit while I'm at it.

If you clone the repo to a directory called PlasmaPy (so you have a PlasmaPy/plasmapy directory where the actual repository is located, you should be able to run pytest or more specifically pytest plasmapy/physics/distribution.py from the PlasmaPy directory and that should, due to seeing the setup.cgg file, include doctests.


Example
-------

Copy link
Member

Choose a reason for hiding this comment

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

As previously, a doctest would be nice! We could go with a single particle for simplicity.

integ = spint.trapz(y=yData1D, x=xData1D)
print("1D integral {0}".format(integ))
exceptStr = "Integral of distribution function should be 1."
assert np.isclose(integ.value, 1), exceptStr
Copy link
Member

Choose a reason for hiding this comment

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

How about this, using the power of python 3.6 (though a .format would of course work as well, except for being less awesome):

# cut the print
exceptStr = f"Integral of distribution function should be 1 and not {integ}."
assert # as previously except it returns the value now

Copy link
Contributor Author

Choose a reason for hiding this comment

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

oops, I'll delete the print line, that was mainly for my own testing. I like the idea of including the value in the exception string though!

particle=particle)
return velDist
# setting up integration from -10*vTh to 10*vTh, which is close to Inf
infApprox = 10 * vTh
Copy link
Member

Choose a reason for hiding this comment

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

I'm trying to think of a way we could check whether this is a good approximation (aside from calculating this again for 20 vTh) and raise a warning if it turns out not to be one.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this ends up being something like 50 or 100 e-foldings of decay, since we have roughly exp(-v**2 / vTh**2) => exp(-(10*vTh)**2 / vTh**2) => exp(-100), times some coefficients. In any case, this test works for the 1D case and passes the isclose to 1 test with default errors.

Copy link
Member

Choose a reason for hiding this comment

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

... 50 e-foldings? Okay. Nevermind the warning. 😆

# if I recall correctly, astropy.units may have trouble with
# accounting for units in integrals, so this step currently fails when
# tplquad tries to convert a quantity with units to a scalar.
# integrating, this should be close to 1
Copy link
Member

Choose a reason for hiding this comment

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

Turn this into a simple float via velDist3D.si.value, then multiply the result by the proper units? infApprox would also have to be changed.

lambda z, y: -infApprox,
lambda z, y: infApprox,
epsabs=1.49e-08*u.m/u.m,
epsrel=1.49e-08*u.m/u.m)
Copy link
Member

Choose a reason for hiding this comment

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

I'm curious, why this number in particular? Also, the units seem off, could this perhaps be why tplquad failed here? If we're integrating velocities, don't we need u.m/u.s?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Those are just the default values in tplquad 😉 , I put them in explicitly to check if adding dimensionless units would help the test behave correctly. Sometimes this works according to the "known issues" with astropy.units pages I linked below.

The final results from integration should be a probability, which is dimensionless, therefore the errors on the final value are dimensionless. u.m/u.m was just a shortcut for adding a dimensionless unit.

Here are some references for why the integration doesn't behave well with astropy.units. Here and here.

Copy link
Member

Choose a reason for hiding this comment

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

Oh right, it's an error on the final value! I forgot about that, you're quite right.

Well, we can still go for floats on the internal side!

@lemmatum
Copy link
Contributor Author

I've fixed the issues with astropy.units and tplquad not playing nicely. The fix was relatively simple, but pytest now runs really slowly or stalls. When it does work, just one of the 3D tests takes about 150 seconds. I've tried profiling with cProfile, but it also stalls. I'm attaching a partial cProfile which may give some insights on how to optimize.
test_plasmaPy_cProfile.txt

epsrel=1.49e-08*u.m/u.m)
exceptStr = "Integral of distribution function should be 1."
assert np.isclose(integ.value, 1), exceptStr
epsabs=0.0,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

probably should change this from 0.0 to some larger value as I'm not sure how QUADPACK handles 0.0 absolute error (whether infinitely small or just ignored).

@lemmatum
Copy link
Contributor Author

I just tested out a version of Maxwellian_velocity_3D() with all the unit checks and conversions stripped out and passing additional parameters under args=() in tplquad rather than creating a partially-applied function. It runs in 1.6 seconds.... and gives 1.0000000181399045 as the results of the triple integral, which is pretty damn close.

So perhaps the speed of unit conversions, mentioned in #131 , is a valid concern. Need to dig in and figure out what was killing the optimizations in tplquad.

assert np.isclose(integ.value, 1), exceptStr
epsabs=0.0,
epsrel=5.0e-1)
# value returned from tplquad is (integral, error), we just need the 1st
Copy link
Member

Choose a reason for hiding this comment

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

Or we could use the returned error in line 102-105, as atol? Does that make sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure. I don't think the error here is the error on the final value, but rather the error in how the function slices up the bounds into individual units to be calculated, or the error on these local approximate integrals.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In any case using epabs=5.0e-1 gave me quite a reasonable value on the final integral value when I stripped the Maxwellian function of units. This was the aforementioned 1.0000000181399045.

@StanczakDominik
Copy link
Member

I'd have to bet (70% of my confidence) on unit conversions. @Cadair mentioned them being expensive operations, to be used sparingly. Still... it might be the partial function application.

@namurphy namurphy added the plasmapy.formulary Related to the plasmapy.formulary subpackage label Oct 31, 2017
@StanczakDominik StanczakDominik added this to In Progress in Plasma parameters Oct 31, 2017
@lemmatum
Copy link
Contributor Author

lemmatum commented Nov 1, 2017

Here are some ideas on how to resolve this

  • Write the Maxwellian without units
  • Write two Maxwellian functions, one with units and the other without
  • Write the Maxwellian with a flag to disable unit checks, and move unit checks and conversions to an if statement inside the function
  • Write a function that takes the proposed inputs (with units), and returns a scaled version of the Maxwellian function but without units.

Some further optimization could be done by passing vThermal as an argument. For any task, such as integration, where the temperature is the same, vThermal will be the same. The way the function is written right now it needlessly calculates vThermal on each call.

Some of this will depend on what part of the code is actually slowing down tplquad. My inclination is towards option 3.

@StanczakDominik
Copy link
Member

StanczakDominik commented Nov 1, 2017 via email

…on functions. These function also include optional drift velocities/speeds. I've included tests for these new functions, but the 3D tests fails because scipy's QUADPACK based triple integral doesn't play nice with units.
…dded new functions to __init__.py. Noted in comments that velocities are actually relative particle velocities.
…ng nicely with astropy.units. This was done by applying units to inputs and stripping units of output from partially applied distribution function within the test.
…ng unit and unitless options, and an option to pass thermal velocity to avoid redundant calculations.
…0*u.m/m.s was passed as an argument. This made the units squared, which then caused failed unit conversions. Also fixed docstring values.
@lemmatum
Copy link
Contributor Author

lemmatum commented Nov 2, 2017

Apologies for all the pushes. I was learning how to rebase since upstream's distribution.py was updated with edits to the docstrings.
Anyway this version works now! Tests pass in about 1 second when using "unitless" flag in distribution functions.

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.

A few lines are not getting tested. If you could go through my notes or the Coveralls report, that'd be handy in solving that!

coeff = (vThSq * np.pi) ** (-1 / 2)
expTerm = np.exp(-vSq / vThSq)
distFunc = coeff * expTerm
except Exception:
Copy link
Member

Choose a reason for hiding this comment

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

Code coverage whines about this one, so either add some test with pytest.raises(ValueError) or simply plot down a # coveralls: ignore after the Exception: to skip testing this one!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Or just remove the try-except block? I don't think it's doing much here.

Copy link
Member

Choose a reason for hiding this comment

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

We could do that as well. I'm not 100% sure why they pop up everywhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just started by imitating other code I've seen in this package, but it seems like these try-except blocks are mostly useless for such simple functions.

# need to be assigned units
if Vx_drift == 0:
if not isinstance(Vx_drift, astropy.units.quantity.Quantity):
Vx_drift = Vx_drift * u.m/u.s
Copy link
Member

Choose a reason for hiding this comment

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

The three Vdirection_drift = ... lines don't get covered by test. Adding another small one with a floating point value would solve that!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This refers to tests in test_distribution.py, correct?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, that it does.

V_drift = V_drift.to(u.m/u.s)
# convert temperature to Kelvins
T = T.to(u.K, equivalencies=u.temperature_energy())
if vTh == -1:
Copy link
Member

Choose a reason for hiding this comment

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

This is a neat idea! Would adding np.nan instead of -1 make sense here, to signify that it's a nonsensical value?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure. Just tested it and it does work. Just had to switch from == to np.isnan(vTh) as the Truthiness test.

vTh = vTh.to(u.m/u.s)
elif vTh == -1 and units == "unitless":
# assuming unitless temperature is in Kelvins
vTh = (thermal_speed(T*u.K, particle=particle, method="most_probable"))
Copy link
Member

Choose a reason for hiding this comment

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

This one doesn't get tested :(

vTh = (thermal_speed(T, particle=particle, method="most_probable"))
elif vTh != -1:
# check units of thermal velocity
vTh = vTh.to(u.m/u.s)
Copy link
Member

Choose a reason for hiding this comment

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

If the velocity variable can be used as a velocity (as the tests are passing later), is this check (elif block from line 338) necessary? Not 100% sure.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you mean that if vTh gets added or multiplied to some other value that has units then astropy will throw an error? I was just trying to be explicit, but that might be a better way to go, less clutter.

Copy link
Member

Choose a reason for hiding this comment

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

Well, if it's convertible to u.m/u.s, it's not going to throw any errors later even if you don't do the explicit check here. But explicit is neat, I was just asking out of curiosity. Let's keep it this way :)

expTerm = np.exp(-vSq / vThSq)
distFunc = coeff1 * coeff2 * expTerm
except Exception:
raise ValueError("Unable to get distribution function.")
Copy link
Member

Choose a reason for hiding this comment

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

This block doesn't get tested either. Could ignore it.

# need to be assigned units
if Vx_drift == 0:
if not isinstance(Vx_drift, astropy.units.quantity.Quantity):
Vx_drift = Vx_drift * u.m/u.s
Copy link
Member

Choose a reason for hiding this comment

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

These 3 lines don't get tested either. Same situation as before, it seems!

@StanczakDominik
Copy link
Member

Oh yeah, I forgot to mention. Mad props on getting that test down to 1s! That's really amazing.

@lemmatum
Copy link
Contributor Author

lemmatum commented Nov 2, 2017

It wasn't too difficult to optimize! Thank those poor FORTRAN programmers who wrote QUADPACK 😜

@StanczakDominik
Copy link
Member

Their sacrifice shan't be forgotten. [*]

@lemmatum
Copy link
Contributor Author

lemmatum commented Nov 3, 2017

I added a bunch of tests under Test_Maxwellian_speed_3D, if coveralls is happy then I'll add similar tests for the other distribution functions.

@lemmatum
Copy link
Contributor Author

lemmatum commented Nov 5, 2017

Looks like coveralls are happy with the 3D Maxwellian speed function tests. I'll make similar tests for the other functions.

…s input permutations (vTh, units, drift) for 1D velocity, 1D speed, and 3D velocity distribution functions. These are similar to tests from previous commit for 3D speed distribution function.
@lemmatum
Copy link
Contributor Author

lemmatum commented Nov 5, 2017

Added tests for other functions 🤞

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.

Solid testing! Merging this one!

T=self.T_e,
particle=self.particle,
units="units")
errStr = (f"Distribution function should be {self.distFuncTrue} "
Copy link
Member

Choose a reason for hiding this comment

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

Bonus points for f-string usage

Copy link
Contributor Author

Choose a reason for hiding this comment

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

😸

@StanczakDominik StanczakDominik merged commit 93ac5af into PlasmaPy:master Nov 5, 2017
Plasma parameters automation moved this from In Progress to Done Nov 5, 2017
@lemmatum lemmatum deleted the distributionFunctions branch November 5, 2017 09:44
@rocco8773 rocco8773 added this to Complete in Formulary Development Jul 9, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
plasmapy.formulary Related to the plasmapy.formulary subpackage
Projects
Development

Successfully merging this pull request may close these issues.

None yet

3 participants