Skip to content

PR1 : implement analytical Coulomb potential for Gaussian densities#304

Open
Ao-chuba wants to merge 2 commits into
theochem:masterfrom
Ao-chuba:feature/coulomb-analytical-potentials
Open

PR1 : implement analytical Coulomb potential for Gaussian densities#304
Ao-chuba wants to merge 2 commits into
theochem:masterfrom
Ao-chuba:feature/coulomb-analytical-potentials

Conversation

@Ao-chuba
Copy link
Copy Markdown
Member

First inital PR for Improvement of the robustness of poisson equation .
This PR implements the core mathematical engine for evaluating the exact analytical electrostatic (Coulomb) potential of $s$-type and $p$-type radial Gaussian charge densities. The math is derived from the exact formulations and $r \to 0$ limits outlined in the course project reference (https://qchem.qc-edu.org/problems/HF_dft.html). This PR contains the standalone coulomb.py module and an isolated unit test suite validating math correctness across various distances and normalization schemes, establishing the foundation for the subsequent robust Poisson solver scaffolding.
related to issue -> #288

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new standalone coulomb module providing exact analytical Coulomb potential evaluations for s-type and p-type radial Gaussian charge densities, together with a higher-level coulomb_potential aggregator that sums contributions from a set of centered Gaussians. This is the first building block towards a more robust Poisson solver (issue #288).

Changes:

  • New src/grid/coulomb.py with coulomb_gaussian_s, coulomb_gaussian_p, and coulomb_potential, including correct handling of the r → 0 limit for both normalized and unnormalized conventions.
  • New src/grid/tests/test_coulomb.py exercising the analytical formulas at r=0 and at non-zero radii for both normalizations, plus an aggregate s-type-only consistency test against a hand-rolled reference.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
src/grid/coulomb.py New module implementing analytical Coulomb potentials of s- and p-type Gaussian densities and a multi-Gaussian aggregator.
src/grid/tests/test_coulomb.py New unit tests validating the s/p formulas at the origin and at non-zero radii, and an aggregate s-only check.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/grid/coulomb.py Outdated
Comment thread src/grid/coulomb.py
Comment thread src/grid/coulomb.py
Comment thread src/grid/coulomb.py Outdated
@Ao-chuba
Copy link
Copy Markdown
Member Author

as per copilot suggestions i took into consideration for some of its inputs:

  • Added validation inside coulomb_potential ensuring that coeffs_p, expons_p, and centers_p are either all provided or all omitted to prevent silent dropping of p-type density.
  • Added checks to ensure that the coefficients, exponents, and centers have the same length. This prevents silent truncation of Gaussians due to Python's zip behavior.
  • Added checks to ensure alpha > 0 and beta > 0. This prevents invalid math operations (like taking np.sqrt of negative numbers or division by zero).
  • changed number 1e-12 to a named module-level constant (_R_ZERO_THRESHOLD) to avoid duplication and improve readability.

Comment thread src/grid/coulomb.py
out[mask_nonzero] = erf(sqrt_a * r_nonzero) / r_nonzero
out[mask_zero] = 2.0 * sqrt_a / np.sqrt(np.pi)
else:
prefactor = (np.pi / alpha) ** 1.5
Copy link
Copy Markdown
Collaborator

@marco-2023 marco-2023 May 28, 2026

Choose a reason for hiding this comment

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

The code can and should be shorter and simpler (read):

out = erf(sqrt_a * r) / r
out[r < _R_ZERO_THRESHOLD] = 2.0 * sqrt_a / np.sqrt(np.pi)

Code is repeated inside the if-else. It is better to compute out and if not normalizes multiply by the prefactor at the end (you can even do it with return)

# Apply normalization factor only if needed
if normalized:
    return out

return prefactor * out

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.

prefactor = 1.0 if normalized else (np.pi / alpha) ** 1.5
out[mask_nonzero] = prefactor * erf(sqrt_a * r_nonzero) / r_nonzero
out[mask_zero]    = prefactor * 2.0 * sqrt_a / np.sqrt(np.pi)

Would be better if its structured like this instead?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I don't think we should have two masks just for one or two points. My idea was something like:

out = erf(sqrt_a * r) / r
out[r < _R_ZERO_THRESHOLD] = 2.0 * sqrt_a / np.sqrt(np.pi)

if normalized:
    return out

return prefactor * out

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This pattern is even better

np.divide(erf(sqrt_a * r), r, out = out, where=r >= _R_ZERO_THRESHOLD)
out[r < _R_ZERO_THRESHOLD] = 2.0 * sqrt_a / np.sqrt(np.pi)

if normalized:
    return out

return prefactor * out

Comment thread src/grid/coulomb.py
out = np.empty_like(r)
mask_zero = r < 1e-12
mask_nonzero = ~mask_zero

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This function can also be simplified, similar to the previous one.
Compute term 1 and term 2, and use if-else for prefactors at return.
We can also get rid of masks.

def test_coulomb_gaussian_s():
# Test r=0 limit
assert_allclose(
coulomb_gaussian_s(0.0, 2.0, normalized=True), 2.0 * np.sqrt(2.0) / np.sqrt(np.pi)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It is a better practice to store the reference value in one variable, the result in another, and compare them at the end. And being consistent with this across tests, similar to what you did for test_coulomb_gaussian_p

Copy link
Copy Markdown
Collaborator

@marco-2023 marco-2023 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! I think this is a good progress.
Please simplify the functions.
Homogenize the tests

I think this module should not be in the root of the package, but we can leave that for another moment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants