Visitar URL original
Resolve numerical instability in `chemical_potential` by JaydenR2305 · Pull Request #1726 · PlasmaPy/PlasmaPy · GitHub
Skip to content

Resolve numerical instability in chemical_potential - #1726

Merged
rocco8773 merged 22 commits into
PlasmaPy:mainfrom
JaydenR2305:chemical-potential-stability
Sep 9, 2022
Merged

rocco8773 merged 22 commits into
PlasmaPy:mainfrom
JaydenR2305:chemical-potential-stability

Conversation

@JaydenR2305

@JaydenR2305 JaydenR2305 commented Sep 3, 2022 •

Copy link
Copy Markdown
Member

Currently, for reasonable electron densities (~1e10 cm^-3 - 1e20 cm^-3), chemical_potential returns a number extremely close to zero (~1e-16). This behavior is reasonable and expected. However, there are some occasions where the current least squares regression method will return an abnormally large value, demonstrating numerical instability. This instability can sometimes cause the docstring test to fail

I propose we use some alternative to a least squares method. The method tested in the figures is the Broyden–Fletcher–Goldfarb–Shanno algorithm. The figures were generated at 11000 Kelvin using the two different methods to calculate the chemical potential, and the resulting residual.

For densities greater than 1e20 cm^-3, the graphs produce identical results

The first regime is where the chemical potential is ~0. The algorithms generally agree on zero but sometimes the least squares method produces unexpected results
Figure_1

The residuals for both functions are the same suggesting they have an approximately equal error
Figure_2

Both algorithms produce the same results for electron densities greater than 1e20
Figure_3

  • I have added a changelog entry for this pull request.
  • If adding new functionality, I have added tests and
    docstrings.
  • I have fixed any newly failing tests.

@github-actions github-actions Bot added plasmapy.formulary Related to the plasmapy.formulary subpackage plasmapy.formulary.quantum Related to the plasmapy.formulary.quantum module labels Sep 3, 2022
@codecov

codecov Bot commented Sep 3, 2022 •

Copy link
Copy Markdown

Codecov Report

Merging #1726 (5896625) into main (fe2216b) will decrease coverage by 0.00%.
The diff coverage is 100.00%.

@@            Coverage Diff             @@
##             main    #1726      +/-   ##
==========================================
- Coverage   98.09%   98.09%   -0.01%     
==========================================
  Files          87       87              
  Lines        8100     8099       -1     
==========================================
- Hits         7946     7945       -1     
  Misses        154      154              
Impacted Files Coverage Δ
plasmapy/formulary/quantum.py 100.00% <100.00%> (ø)

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

@JaydenR2305
JaydenR2305 marked this pull request as ready for review September 4, 2022 13:30
@rocco8773

Copy link
Copy Markdown
Member

My suspicion is that you're running into the same issue discussed in astropy/astropy#9721.

I'd suggest exploring something like...

@validate_quantities(
    n_e={"can_be_negative": False, "pass_equivalent_units": True},
    T={"can_be_negative": False, "equivalencies": u.temperature_energy()},
)
def chemical_potential(n_e: u.m**-3, T: u.K) -> u.dimensionless_unscaled:
    # deBroglie wavelength
    lambdaDB = thermal_deBroglie_wavelength(T)
    # degeneracy parameter
    degen = (np.cbrt(n_e) * lambdaDB).to(u.dimensionless_unscaled)**3

    ...

Using "pass_equivalent_units" will allow n_e to NOT be converted to u.m, n_e will stay in the units it originally was passed with. This means, once you get to the degen line and multiply np.cbrt(n_e) * lambdaDB the .to() method only has to convert one ratio of u.cm/u.m instead of a power of three. This should reduce the floating point arithmetic error and make the calculation more stable.

@rocco8773

rocco8773 commented Sep 6, 2022 •

Copy link
Copy Markdown
Member

This needs to be merged before #1727

I need to think about the order of merger a little more...

@JaydenR2305 Can you add the same changes you made in #1727, sans change log entry, to this PR? If all the checks pass, then I'd be comfortable merging #1727 without all of its checks passing.

@JaydenR2305

Copy link
Copy Markdown
Member Author

Looks like 5.0 still uses asscalar somewhere..

@JaydenR2305

Copy link
Copy Markdown
Member Author

My suspicion is that you're running into the same issue discussed in astropy/astropy#9721.

I'd suggest exploring something like...

@validate_quantities(
    n_e={"can_be_negative": False, "pass_equivalent_units": True},
    T={"can_be_negative": False, "equivalencies": u.temperature_energy()},
)
def chemical_potential(n_e: u.m**-3, T: u.K) -> u.dimensionless_unscaled:
    # deBroglie wavelength
    lambdaDB = thermal_deBroglie_wavelength(T)
    # degeneracy parameter
    degen = (np.cbrt(n_e) * lambdaDB).to(u.dimensionless_unscaled)**3

    ...

Using "pass_equivalent_units" will allow n_e to NOT be converted to u.m, n_e will stay in the units it originally was passed with. This means, once you get to the degen line and multiply np.cbrt(n_e) * lambdaDB the .to() method only has to convert one ratio of u.cm/u.m instead of a power of three. This should reduce the floating point arithmetic error and make the calculation more stable.

This seems to be producing identical results
Figure_4

@rocco8773

Copy link
Copy Markdown
Member

Looks like 5.0 still uses asscalar somewhere..

You are correct, see my comment #1727 (comment).

@rocco8773 rocco8773 left a comment

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.

In the docstring, can you make the executive decision to either consistently use the subscript $_a$ or drop it all together? I think either approach is valid. I just want it to be consistent across the docstring, since switching back-and-forth and lead to confusion for users. If you choose to keep the subscript, then can you update the sentence explaining the equation to read something like:

"... where $\chi_a$ is the degeneracy parameter of particle 'a'..."

If we keep the subscript, then we at least need to explain what it means.

@rocco8773 rocco8773 left a comment

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.

Do you know which section in Bontiz discusses the chemical potential? If so, can you included in the first line of the docstring Notes section?

@JaydenR2305

Copy link
Copy Markdown
Member Author

Do you know which section in Bontiz discusses the chemical potential? If so, can you included in the first line of the docstring Notes section?

Good call! I added the specific equation number from Bonitz and improved the overall explanation. The documentation was somewhat vague on how the calculation is performed so I tried to make things more explicit

@namurphy namurphy left a comment

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.

LGTM! It looks like you addressed all of @rocco8773's comments, and I only have a very minor suggestion for the changelog entry. I'm happy to get this merged soon. Thank you for doing this!

Comment thread changelog/1726.trivial.rst Resolved
@lemmatum

lemmatum commented Sep 8, 2022

Copy link
Copy Markdown
Contributor

Cool, I've never heard of the BFGS algorithm. I have some reading up to do!
My only comment is it'd be good to add a test where the chemical potential is non-zero. Otherwise I think the changes look good.

Thanks for digging into this!

self.n_e_fail = 1e23 * u.cm**-3
self.T = 11604 * u.K
self.True1 = 6.133671348607095e-11
self.True1 = 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Add another test where the result is non-zero.

@namurphy
namurphy requested a review from rocco8773 September 8, 2022 17:08
Comment thread changelog/1726.trivial.rst Outdated
Comment on lines +1 to +2
Changed `~plasmapy.formulary.quantum.chemical_potential` to use the
Broyden-Fletcher-Goldfarb-Shanno algorithm to implicitly solve for beta.

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.

Suggested change
Changed `~plasmapy.formulary.quantum.chemical_potential` to use the
Broyden-Fletcher-Goldfarb-Shanno algorithm to implicitly solve for beta.
Changed `~plasmapy.formulary.quantum.chemical_potential` to use the
Broyden-Fletcher-Goldfarb-Shanno algorithm to implicitly solve for
the β(\ :math:`= 1/(k_B T)`\ ) parameter.

Only so beta is not mistaken for the plasma beta, which would likely be common in this community.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Whoops, that shouldn't say beta anyways. It should be the dimensionless chemical potential

Comment on lines 450 to +456
The definition for the ideal chemical potential is implicit, so it must
be obtained numerically by solving for the Fermi integral for values
of chemical potential approaching the degeneracy parameter. Since values
returned from the `~plasmapy.formulary.mathematics.Fermi_integral`
are complex, a nonlinear Levenberg-Marquardt least squares method is
used to iteratively approach a value of :math:`μ` which minimizes
:math:`I_{1/2}(β μ_a^{ideal}) - χ_a`
are complex, the Broyden–Fletcher–Goldfarb–Shanno algorithm is
used to iteratively approach a value of :math:`μ^{ideal}` which minimizes
:math:`\lvert I_{1/2}(β μ^{ideal}) - χ \rvert`.

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'm thinking as a follow up PR, it would be good to break out the code for the ideal chemical potential into its own function. @JaydenR2305 What do you think about this and would you be willing to do it? Again, this would be a followup PR and I'm not suggesting to do it in this PR.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Sure. That means this function would just be a wrapper for that function multiplied by beta?

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.

Yeah, in the end that would be the case.

Comment on lines 169 to 178
def test_knowns(n_e, T, expected_beta):
"""
Tests Fermi_integral for expected value.
Tests chemical_potential for expected value.
"""
methodVal = chemical_potential(self.n_e, self.T)
testTrue = u.isclose(methodVal, self.True1, rtol=1e-16, atol=0.0)
errStr = f"Chemical potential value should be {self.True1} and not {methodVal}."
methodVal = chemical_potential(n_e, T).value
testTrue = np.isclose(methodVal, expected_beta, rtol=1e-16, atol=0.0)
errStr = (
f"Chemical potential value should be {expected_beta} and not {methodVal}."
)
assert testTrue, errStr

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.

Can you add another assert to ensure the returned unit is dimensionless_unscaled? It'll likely look something like...

assert testTrue, errStr
assert chemical_potential.unit == u.dimensionless_unscaled

Also, could you please refactor this to meet PEP8 naming standards? e.g. methodVal -> method_val

@rocco8773 rocco8773 left a comment

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.

Looks good to me! Thank you for working on this.

@rocco8773
rocco8773 merged commit 62dd840 into PlasmaPy:main Sep 9, 2022
@JaydenR2305
JaydenR2305 deleted the chemical-potential-stability branch September 9, 2022 23:57
namurphy added a commit to namurphy/PlasmaPy that referenced this pull request Oct 25, 2022
This one attempts to use the :wikipedia: role, though I'll need to check that it works.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

plasmapy.formulary.quantum Related to the plasmapy.formulary.quantum module plasmapy.formulary Related to the plasmapy.formulary subpackage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants