Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ Modules
./notebooks/quickstart
One-Dimensional Grids <./notebooks/one_dimensional_grids>
Angular Grids <./notebooks/angular_grid>
Max-Det Integration <./notebooks/max_det_integration>
Atom Grid Construction <./notebooks/atom_grid_construction>
Atom Grid Application <./notebooks/atom_grid>
Molecular Grid Construction <./notebooks/molecular_grid_construction>
Expand Down
3 changes: 3 additions & 0 deletions doc/notebooks/max_det_integration.nblink
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"path": "../../examples/max_det_integration.ipynb"
}
95 changes: 95 additions & 0 deletions examples/max_det_integration.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Spherical Integration with Maximum Determinant Points\n",
"\n",
"This example demonstrates how to use the `MaxDeterminantGrid` class for numerical integration on the unit sphere.\n",
"\n",
"## 1. Introduction\n",
"Maximum determinant points (Fekete points) provide a stable set of points for integration on the sphere with positive weights. \n",
"For a given degree $t$, there are $N = (t+1)^2$ points."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"from grid.max_det import MaxDeterminantGrid\n",
"from grid.utils import generate_real_spherical_harmonics\n",
"\n",
"# Create a Max-Det grid of degree 5\n",
"grid = MaxDeterminantGrid(degree=5)\n",
"print(f\"Grid Size: {grid.size}\")\n",
"print(f\"Degrees Supported: {grid.degree}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 2. Integration of a simple function\n",
"Let's integrate $f(x, y, z) = x^2 + y^2 + z^2$ over the unit sphere. \n",
"Since $x^2 + y^2 + z^2 = 1$ on the sphere, the integral should be $4\\pi$."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def func(p):\n",
" return p[:, 0]**2 + p[:, 1]**2 + p[:, 2]**2\n",
"\n",
"integral = grid.integrate(func)\n",
"print(f\"Calculated Integral: {integral}\")\n",
"print(f\"Expected Integral: {4 * np.pi}\")\n",
"print(f\"Error: {np.abs(integral - 4 * np.pi)}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 3. Integration of Spherical Harmonics\n",
"Spherical harmonics of degree $l > 0$ should integrate to 0."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"r = np.linalg.norm(grid.points, axis=1)\n",
"phi = np.arccos(grid.points[:, 2] / r)\n",
"theta = np.arctan2(grid.points[:, 1], grid.points[:, 0])\n",
"\n",
"l = 2; m = 0\n",
"sph_harm = generate_real_spherical_harmonics(l, theta, phi)\n",
"Y_lm = sph_harm[l**2, :] # Horton 2 index for (l=2, m=0)\n",
"\n",
"integral_Y = grid.integrate(Y_lm)\n",
"print(f\"Integral of Y_{l}{m}: {integral_Y}\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
Binary file added output.txt
Binary file not shown.
59 changes: 59 additions & 0 deletions results.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
============================= test session starts =============================
platform win32 -- Python 3.13.1, pytest-9.0.2, pluggy-1.6.0 -- C:\Python313\python.exe
cachedir: .pytest_cache
rootdir: C:\Users\valok\.gemini\antigravity\scratch\grid
configfile: pyproject.toml
plugins: anyio-4.9.0
collecting ... collected 10 items

src/grid/tests/test_maxdet.py::test_maxdet_constant PASSED [ 10%]
src/grid/tests/test_maxdet.py::test_maxdet_weights_positive PASSED [ 20%]
src/grid/tests/test_maxdet.py::test_maxdet_sum_weights PASSED [ 30%]
src/grid/tests/test_maxdet.py::test_maxdet_integration_spherical_harmonic[1] PASSED [ 40%]
src/grid/tests/test_maxdet.py::test_maxdet_integration_spherical_harmonic[2] PASSED [ 50%]
src/grid/tests/test_maxdet.py::test_maxdet_integration_spherical_harmonic[3] PASSED [ 60%]
src/grid/tests/test_maxdet.py::test_maxdet_integration_spherical_harmonic[4] PASSED [ 70%]
src/grid/tests/test_maxdet.py::test_maxdet_integration_spherical_harmonic[5] PASSED [ 80%]
src/grid/tests/test_maxdet.py::test_maxdet_orthonormality PASSED [ 90%]
src/grid/tests/test_maxdet.py::test_integrate_method_interface FAILED [100%]

================================== FAILURES ===================================
_______________________ test_integrate_method_interface _______________________

def test_integrate_method_interface():
# Test that integrate(function) and Grid.integrate(values) are consistent
grid = MaxDeterminantGrid(degree=2)
# R^2 = 1 on unit sphere
func = lambda p: p[:, 0]**2 + p[:, 1]**2 + p[:, 2]**2

val1 = grid.integrate(func)
> val2 = grid.integrate(np.ones(grid.size))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

src\grid\tests\test_maxdet.py:74:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <grid.max_det.MaxDeterminantGrid object at 0x0000013422BCC4B0>
function = array([1., 1., 1., 1., 1., 1., 1., 1., 1.])

def integrate(self, function):
r"""Integrate a function over the sphere.

Parameters
----------
function : callable
A function that accepts (N, 3) points and returns (N,) values.

Returns
-------
float:
The integral of the function over the unit sphere.
"""
> return np.sum(function(self.points) * self.weights)
^^^^^^^^^^^^^^^^^^^^^
E TypeError: 'numpy.ndarray' object is not callable

src\grid\max_det.py:86: TypeError
=========================== short test summary info ===========================
FAILED src/grid/tests/test_maxdet.py::test_integrate_method_interface - TypeError: 'numpy.ndarray' object is not callable
========================= 1 failed, 9 passed in 1.23s =========================
43 changes: 43 additions & 0 deletions scripts/fetch_maxdet_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import urllib.request
import os
import numpy as np

def download_maxdet(degree, npts):
filename = f"md{degree:03d}.{npts:05d}"
url = f"https://web.maths.unsw.edu.au/~rsw/Sphere/S2Pts/MD/{filename}"
print(f"Downloading {url}...")
try:
with urllib.request.urlopen(url) as f:
data = f.read().decode().splitlines()
points_weights = []
for line in data:
if line.strip():
points_weights.append([float(x) for x in line.split()])
pw = np.array(points_weights)
points = pw[:, :3]
# Sum of weights in the file is already 4*pi
# Let's normalize to 1 to stay consistent with other npz files
weights = pw[:, 3] / (4 * np.pi)
return points, weights
except Exception as e:
print(f"Error downloading {url}: {e}")
return None, None

def save_as_npz(degree, npts, points, weights, target_dir):
filename = f"maxdet_{degree}_{npts}.npz"
filepath = os.path.join(target_dir, filename)
print(f"Saving to {filepath}...")
np.savez(filepath, degree=degree, size=npts, points=points, weights=weights)

if __name__ == "__main__":
target_dir = r"C:\Users\valok\.gemini\antigravity\scratch\grid\src\grid\data\max_det"
if not os.path.exists(target_dir):
os.makedirs(target_dir)

# Degrees and corresponding number of points
degrees = [1, 2, 3, 4, 5, 10, 20, 30]
for d in degrees:
npts = (d + 1) ** 2
p, w = download_maxdet(d, npts)
if p is not None:
save_as_npz(d, npts, p, w, target_dir)
2 changes: 2 additions & 0 deletions src/grid/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,6 @@
from grid.onedgrid import *
from grid.periodicgrid import *
from grid.rtransform import *
from grid.max_det import *
from grid.ngrid import *

34 changes: 34 additions & 0 deletions src/grid/_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# file generated by setuptools-scm
# don't change, don't track in version control

__all__ = [
"__version__",
"__version_tuple__",
"version",
"version_tuple",
"__commit_id__",
"commit_id",
]

TYPE_CHECKING = False
if TYPE_CHECKING:
from typing import Tuple
from typing import Union

VERSION_TUPLE = Tuple[Union[int, str], ...]
COMMIT_ID = Union[str, None]
else:
VERSION_TUPLE = object
COMMIT_ID = object

version: str
__version__: str
__version_tuple__: VERSION_TUPLE
version_tuple: VERSION_TUPLE
commit_id: COMMIT_ID
__commit_id__: COMMIT_ID

__version__ = version = '0.1.dev812+g0b68e7c36'
__version_tuple__ = version_tuple = (0, 1, 'dev812', 'g0b68e7c36')

__commit_id__ = commit_id = 'g0b68e7c36'
41 changes: 27 additions & 14 deletions src/grid/angular.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@
# Cache is used to store the angular grid
LEBEDEV_CACHE = {}
SPHERICAL_CACHE = {}
MAXDET_CACHE = {}


class AngularGrid(Grid):
Expand All @@ -300,8 +301,9 @@ class AngularGrid(Grid):
:math:`4\pi` normalization factor present in the original quadrature scheme
is included in the integration weights.

Two types of angular grids are supported: Lebedev-Laikov grid and symmetric
spherical t-design. Specifically, for spherical t-design, the weights are constant
Two types of angular grids are supported: Lebedev-Laikov grid, symmetric
spherical t-design, and maximum determinant points.
Specifically, for spherical t-design, the weights are constant
value of :math:`4 \pi / N`\, where :math:`N` is the number of points in the grid.
The weights are chosen so that the spherical harmonics are normalized.

Expand Down Expand Up @@ -332,8 +334,8 @@ def __init__(
If True, then store the points and weights of the AngularGrid in cache
to avoid duplicate grids that have the same `degree`\.
method: str, optional, keyword-only
Method for constructing the angular grid. Options are "lebedev" (for Lebedev-Laikov)
and "spherical" (for symmetric spherical t-design).
Method for constructing the angular grid. Options are "lebedev" (for Lebedev-Laikov),
"spherical" (for symmetric spherical t-design), and "maxdet" (for maximum determinant).

Returns
-------
Expand All @@ -353,8 +355,10 @@ def __init__(
cache_dict = LEBEDEV_CACHE
elif method == "spherical":
cache_dict = SPHERICAL_CACHE
elif method == "maxdet":
cache_dict = MAXDET_CACHE
else:
raise ValueError(f"Method {method} is not supported, choose 'lebedev' or 'spherical'")
raise ValueError(f"Method {method} is not supported, choose 'lebedev', 'spherical' or 'maxdet'")

# allow only one of degree or size to be given
if size is not None:
Expand Down Expand Up @@ -406,15 +410,15 @@ def method(self):
@staticmethod
def convert_angular_sizes_to_degrees(sizes: np.ndarray, method: str):
"""
Convert given Lebedev/Spherical design grid sizes to degrees.
Convert given Lebedev/Spherical/Max-Det design grid sizes to degrees.

Parameters
----------
sizes : ndarray[int]
Sequence of angular grid sizes (e.g., number of points for each atomic shell).
method: str
Method for constructing the angular grid. Options are "lebedev" (for Lebedev-Laikov)
and "spherical" (for symmetric spherical t-design).
Method for constructing the angular grid. Options are "lebedev" (for Lebedev-Laikov),
"spherical" (for symmetric spherical t-design), and "maxdet" (for maximum determinant).

Returns
-------
Expand Down Expand Up @@ -448,8 +452,8 @@ def _get_degree_and_size(degree: int | None, size: int | None, method: str):
degree is used for constructing the grid. Use None, if `degree` is given. If both
`degree` and `size` are given `degree` is used for constructing the grid.
method: str
Method for constructing the angular grid. Options are "lebedev" (for Lebedev-Laikov)
and "spherical" (for symmetric spherical t-design).
Method for constructing the angular grid. Options are "lebedev" (for Lebedev-Laikov),
"spherical" (for symmetric spherical t-design), and "maxdet" (for maximum determinant).

Returns
-------
Expand All @@ -464,8 +468,12 @@ def _get_degree_and_size(degree: int | None, size: int | None, method: str):
dict_degrees, dict_npoints = LEBEDEV_DEGREES, LEBEDEV_NPOINTS
elif method == "spherical":
dict_degrees, dict_npoints = SPHERICAL_DEGREES, SPHERICAL_NPOINTS
elif method == "maxdet":
# For maxdet, N = (t+1)^2
from grid.max_det import MaxDeterminantGrid
return MaxDeterminantGrid._get_degree_and_size(degree, size)
else:
raise ValueError(f"Method {method} is not supported, choose 'lebedev' or 'spherical'")
raise ValueError(f"Method {method} is not supported, choose 'lebedev', 'spherical' or 'maxdet'")

# check whether degree and size are valid
if not (degree is None or (isinstance(degree, (int, np.integer)) and degree >= 0)):
Expand Down Expand Up @@ -518,8 +526,8 @@ def _load_precomputed_angular_grid(degree: int, size: int, method: str):
size : int
Number of angular grid points.
method: str
Method for constructing the angular grid. Options are "lebedev" (for Lebedev-Laikov)
and "spherical" (for symmetric spherical t-design).
Method for constructing the angular grid. Options are "lebedev" (for Lebedev-Laikov),
"spherical" (for symmetric spherical t-design), and "maxdet" (for maximum determinant).

Returns
-------
Expand All @@ -535,8 +543,13 @@ def _load_precomputed_angular_grid(degree: int, size: int, method: str):
elif method == "spherical":
dict_degrees, dict_npoints = SPHERICAL_DEGREES, SPHERICAL_NPOINTS
file_path = "grid.data.spherical_design"
elif method == "maxdet":
from grid.max_det import MaxDeterminantGrid
# Reuse logic from MaxDeterminantGrid but ensure it follows the AngularGrid interface
grid = MaxDeterminantGrid(degree=degree, size=size, cache=False)
return grid.points, grid.weights / (4 * np.pi) # Undo the 4pi because it's added back in caller
else:
raise ValueError(f"Method {method} is not supported, choose 'lebedev' or 'spherical'")
raise ValueError(f"Method {method} is not supported, choose 'lebedev', 'spherical' or 'maxdet'")

# check whether degree and size are valid
if not (degree is None or (isinstance(degree, (int, np.integer)) and degree >= 0)):
Expand Down
12 changes: 6 additions & 6 deletions src/grid/atomgrid.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ def __init__(
spherical grids at each radial grid point. If the integer is zero, then no rotate
is used.
method: str, optional, keyword-only
Method for constructing the angular grid. Options are "lebedev" (for Lebedev-Laikov)
and "spherical" (for symmetric spherical t-design).
Method for constructing the angular grid. Options are "lebedev" (for Lebedev-Laikov),
"spherical" (for symmetric spherical t-design), and "maxdet" (for maximum determinant).

"""
# check stage, if center is None, set to (0., 0., 0.)
Expand Down Expand Up @@ -172,8 +172,8 @@ def from_preset(
Integer used as a seed for generating random rotation matrices to rotate the angular
spherical grids at each radial grid point. If 0, then no rotate is made.
method: str, optional
Method for constructing the angular grid. Options are "lebedev" (for Lebedev-Laikov)
and "spherical" (for symmetric spherical t-design).
Method for constructing the angular grid. Options are "lebedev" (for Lebedev-Laikov),
"spherical" (for symmetric spherical t-design), and "maxdet" (for maximum determinant).

Notes
-----
Expand Down Expand Up @@ -279,8 +279,8 @@ def from_pruned(
spherical grids at each radial grid point. If the integer is zero, then no rotate
is used.
method: str, optional
Method for constructing the angular grid. Options are "lebedev" (for Lebedev-Laikov)
and "spherical" (for symmetric spherical t-design).
Method for constructing the angular grid. Options are "lebedev" (for Lebedev-Laikov),
"spherical" (for symmetric spherical t-design), and "maxdet" (for maximum determinant).

Returns
-------
Expand Down
Loading