Implement SphericalMesh.get_indices_at_coords (#3919)
Some checks failed
Tests and Coverage / filter-changes (push) Has been cancelled
Tests and Coverage / Python 3.13 (omp=n, mpi=n, dagmc=, libmesh=, event= (push) Has been cancelled
Tests and Coverage / Python 3.14 (omp=n, mpi=n, dagmc=, libmesh=, event= (push) Has been cancelled
Tests and Coverage / Python 3.14t (omp=n, mpi=n, dagmc=, libmesh=, event= (push) Has been cancelled
Tests and Coverage / Python 3.12 (omp=n, mpi=n, dagmc=n, libmesh=n, event=n (push) Has been cancelled
Tests and Coverage / Python 3.12 (omp=y, mpi=n, dagmc=n, libmesh=n, event=n (push) Has been cancelled
Tests and Coverage / Python 3.12 (omp=n, mpi=y, dagmc=n, libmesh=n, event=n (push) Has been cancelled
Tests and Coverage / Python 3.12 (omp=y, mpi=y, dagmc=n, libmesh=n, event=n (push) Has been cancelled
Tests and Coverage / Python 3.12 (omp=y, mpi=n, dagmc=, libmesh=y, event= (push) Has been cancelled
Tests and Coverage / Python 3.12 (omp=y, mpi=n, dagmc=, libmesh=, event=y (push) Has been cancelled
Tests and Coverage / Python 3.12 (omp=y, mpi=y, dagmc=y, libmesh=, event= (push) Has been cancelled
Tests and Coverage / Python 3.12 (omp=y, mpi=y, dagmc=, libmesh=y, event= (push) Has been cancelled
Tests and Coverage / coverage (push) Has been cancelled
Tests and Coverage / Check CI status (push) Has been cancelled
dockerhub-publish-develop / main (push) Has been cancelled
dockerhub-publish-develop-dagmc-libmesh / main (push) Has been cancelled
dockerhub-publish-develop-dagmc / main (push) Has been cancelled
dockerhub-publish-develop-libmesh / main (push) Has been cancelled

Co-authored-by: Paul Romano <paul.k.romano@gmail.com>
This commit is contained in:
EdenRochmanSharabi 2026-05-30 08:18:09 +02:00 committed by GitHub
parent 219d82726f
commit 111eb77066
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 184 additions and 25 deletions

View file

@ -3,7 +3,7 @@ import warnings
from abc import ABC, abstractmethod
from collections.abc import Iterable, Sequence, Mapping
from functools import wraps
from math import pi, sqrt, atan2
import math
from numbers import Integral, Real
from pathlib import Path
from typing import Protocol
@ -11,12 +11,10 @@ from typing import Protocol
import h5py
import lxml.etree as ET
import numpy as np
from pathlib import Path
import openmc
import openmc.checkvalue as cv
from openmc.checkvalue import PathLike
from openmc.utility_funcs import change_directory
from .bounding_box import BoundingBox
from ._xml import get_elem_list, get_text
from .mixin import IDManagerMixin
@ -291,7 +289,7 @@ class MeshBase(IDManagerMixin, ABC):
"""
mesh_type = 'regular' if 'type' not in group.keys() else group['type'][()].decode()
mesh_id = int(group.name.split('/')[-1].lstrip('mesh '))
mesh_name = '' if not 'name' in group else group['name'][()].decode()
mesh_name = '' if 'name' not in group else group['name'][()].decode()
if mesh_type == 'regular':
return RegularMesh.from_hdf5(group, mesh_id, mesh_name)
@ -1903,7 +1901,7 @@ class CylindricalMesh(StructuredMesh):
self,
r_grid: Sequence[float],
z_grid: Sequence[float],
phi_grid: Sequence[float] = (0, 2*pi),
phi_grid: Sequence[float] = (0, 2*math.pi),
origin: Sequence[float] = (0., 0., 0.),
mesh_id: int | None = None,
name: str = '',
@ -1960,7 +1958,7 @@ class CylindricalMesh(StructuredMesh):
cv.check_length('mesh phi_grid', grid, 2)
cv.check_increasing('mesh phi_grid', grid)
grid = np.asarray(grid, dtype=float)
if np.any((grid < 0.0) | (grid > 2*pi)):
if np.any((grid < 0.0) | (grid > 2*math.pi)):
raise ValueError("phi_grid values must be in [0, 2π].")
self._phi_grid = grid
@ -2046,7 +2044,7 @@ class CylindricalMesh(StructuredMesh):
The r, phi, z indices
"""
r_value_from_origin = sqrt((coords[0]-self.origin[0])**2 + (coords[1]-self.origin[1])**2)
r_value_from_origin = math.hypot(coords[0]-self.origin[0], coords[1]-self.origin[1])
if r_value_from_origin < self.r_grid[0] or r_value_from_origin > self.r_grid[-1]:
raise ValueError(
@ -2070,13 +2068,13 @@ class CylindricalMesh(StructuredMesh):
delta_x = coords[0] - self.origin[0]
delta_y = coords[1] - self.origin[1]
# atan2 returns values in -pi to +pi range
phi_value = atan2(delta_y, delta_x)
phi_value = math.atan2(delta_y, delta_x)
if delta_x < 0 and delta_y < 0:
# returned phi_value anticlockwise and negative
phi_value += 2 * pi
phi_value += 2 * math.pi
if delta_x > 0 and delta_y < 0:
# returned phi_value anticlockwise and negative
phi_value += 2 * pi
phi_value += 2 * math.pi
phi_grid_values = np.array(self.phi_grid)
@ -2111,7 +2109,7 @@ class CylindricalMesh(StructuredMesh):
dimension: Sequence[int] = (10, 10, 10),
mesh_id: int | None = None,
name: str = '',
phi_grid_bounds: Sequence[float] = (0.0, 2*pi),
phi_grid_bounds: Sequence[float] = (0.0, 2*math.pi),
enclose_domain: bool = False,
) -> CylindricalMesh:
"""Create CylindricalMesh from a bounding box.
@ -2339,8 +2337,8 @@ class SphericalMesh(StructuredMesh):
def __init__(
self,
r_grid: Sequence[float],
phi_grid: Sequence[float] = (0, 2*pi),
theta_grid: Sequence[float] = (0, pi),
phi_grid: Sequence[float] = (0, 2*math.pi),
theta_grid: Sequence[float] = (0, math.pi),
origin: Sequence[float] = (0., 0., 0.),
mesh_id: int | None = None,
name: str = '',
@ -2397,7 +2395,7 @@ class SphericalMesh(StructuredMesh):
cv.check_length('mesh theta_grid', grid, 2)
cv.check_increasing('mesh theta_grid', grid)
grid = np.asarray(grid, dtype=float)
if np.any((grid < 0.0) | (grid > pi)):
if np.any((grid < 0.0) | (grid > math.pi)):
raise ValueError("theta_grid values must be in [0, π].")
self._theta_grid = grid
@ -2411,7 +2409,7 @@ class SphericalMesh(StructuredMesh):
cv.check_length('mesh phi_grid', grid, 2)
cv.check_increasing('mesh phi_grid', grid)
grid = np.asarray(grid, dtype=float)
if np.any((grid < 0.0) | (grid > 2*pi)):
if np.any((grid < 0.0) | (grid > 2*math.pi)):
raise ValueError("phi_grid values must be in [0, 2π].")
self._phi_grid = grid
@ -2483,8 +2481,8 @@ class SphericalMesh(StructuredMesh):
dimension: Sequence[int] = (10, 10, 10),
mesh_id: int | None = None,
name: str = '',
phi_grid_bounds: Sequence[float] = (0.0, 2*pi),
theta_grid_bounds: Sequence[float] = (0.0, pi),
phi_grid_bounds: Sequence[float] = (0.0, 2*math.pi),
theta_grid_bounds: Sequence[float] = (0.0, math.pi),
enclose_domain: bool = False,
) -> SphericalMesh:
"""Create SphericalMesh from a bounding box.
@ -2645,11 +2643,83 @@ class SphericalMesh(StructuredMesh):
arr[..., 2] = z + origin[2]
return arr
def get_indices_at_coords(self, coords: Sequence[float]) -> tuple:
raise NotImplementedError(
"get_indices_at_coords is not yet implemented for SphericalMesh"
def get_indices_at_coords(
self,
coords: Sequence[float]
) -> tuple[int, int, int]:
"""Find the mesh cell indices containing the specified coordinates.
.. versionadded:: 0.15.4
Parameters
----------
coords : Sequence[float]
Cartesian coordinates of the point as (x, y, z).
Returns
-------
tuple[int, int, int]
The r, theta, phi indices.
Raises
------
ValueError
If the coordinates fall outside the mesh grid boundaries.
"""
dx = coords[0] - self.origin[0]
dy = coords[1] - self.origin[1]
dz = coords[2] - self.origin[2]
r_value = math.hypot(dx, dy, dz)
if r_value < self.r_grid[0] or r_value > self.r_grid[-1]:
raise ValueError(
f'The r value {r_value} computed from the specified '
f'coordinates is outside the r grid range '
f'[{self.r_grid[0]}, {self.r_grid[-1]}].'
)
r_index = int(min(
np.searchsorted(self.r_grid, r_value, side='right') - 1,
len(self.r_grid) - 2
))
if r_value == 0.0:
theta_value = 0.0
phi_value = 0.0
else:
theta_value = math.acos(dz / r_value)
phi_value = math.atan2(dy, dx)
if phi_value < 0:
phi_value += 2 * math.pi
if theta_value < self.theta_grid[0] or theta_value > self.theta_grid[-1]:
raise ValueError(
f'The theta value {theta_value} computed from the specified '
f'coordinates is outside the theta grid range '
f'[{self.theta_grid[0]}, {self.theta_grid[-1]}].'
)
theta_index = int(min(
np.searchsorted(self.theta_grid, theta_value, side='right') - 1,
len(self.theta_grid) - 2
))
if phi_value < self.phi_grid[0] or phi_value > self.phi_grid[-1]:
raise ValueError(
f'The phi value {phi_value} computed from the specified '
f'coordinates is outside the phi grid range '
f'[{self.phi_grid[0]}, {self.phi_grid[-1]}].'
)
phi_index = int(min(
np.searchsorted(self.phi_grid, phi_value, side='right') - 1,
len(self.phi_grid) - 2
))
return (r_index, theta_index, phi_index)
def require_statepoint_data(func):
@wraps(func)

View file

@ -1,4 +1,4 @@
from math import pi
from math import pi, sqrt
from tempfile import TemporaryDirectory
from pathlib import Path
import itertools
@ -1065,3 +1065,92 @@ def test_rectilinear_mesh_get_indices_at_coords():
mesh.get_indices_at_coords([0.5, -0.5, 110.])
with pytest.raises(ValueError):
mesh.get_indices_at_coords([0.5, -20., 110.])
def test_SphericalMesh_get_indices_at_coords():
"""Test get_indices_at_coords method for SphericalMesh"""
# Basic mesh with default phi and theta grids (single angular bin)
mesh = openmc.SphericalMesh(r_grid=(0, 5, 10))
assert mesh.get_indices_at_coords([3, 0, 0]) == (0, 0, 0)
assert mesh.get_indices_at_coords([0, 0, 3]) == (0, 0, 0)
assert mesh.get_indices_at_coords([0, 0, -3]) == (0, 0, 0)
assert mesh.get_indices_at_coords([7, 0, 0]) == (1, 0, 0)
assert mesh.get_indices_at_coords([10, 0, 0]) == (1, 0, 0)
# Out-of-bounds r
with pytest.raises(ValueError):
mesh.get_indices_at_coords([11, 0, 0])
mesh2 = openmc.SphericalMesh(r_grid=(2, 5, 10))
with pytest.raises(ValueError):
mesh2.get_indices_at_coords([1, 0, 0])
# Multi-bin angular grids: use points clearly inside bins
mesh3 = openmc.SphericalMesh(
r_grid=(0, 5, 10),
theta_grid=(0, pi/4, pi/2, pi),
phi_grid=(0, pi/2, pi, 3*pi/2, 2*pi)
)
# Near z-axis: theta~0 -> bin 0
assert mesh3.get_indices_at_coords([0.01, 0, 3]) == (0, 0, 0)
# theta in (0, pi/4) -> bin 0: [1, 0, 2] theta=arccos(2/sqrt(5))~0.46
assert mesh3.get_indices_at_coords([1, 0, 2]) == (0, 0, 0)
# theta in (pi/4, pi/2) -> bin 1: [2, 0, 1] theta=arccos(1/sqrt(5))~1.107
assert mesh3.get_indices_at_coords([2, 0, 1]) == (0, 1, 0)
# theta in (pi/2, pi) -> bin 2: [1, 0, -2] theta=arccos(-2/sqrt(5))~2.034
assert mesh3.get_indices_at_coords([1, 0, -2]) == (0, 2, 0)
# phi in (pi/2, pi) -> bin 1: [-1, 1, 0.5]
assert mesh3.get_indices_at_coords([-1, 1, 0.5]) == (0, 1, 1)
# phi in (pi, 3*pi/2) -> bin 2: [-1, -1, 0.5]
assert mesh3.get_indices_at_coords([-1, -1, 0.5]) == (0, 1, 2)
# phi in (3*pi/2, 2*pi) -> bin 3: [1, -1, 0.5]
assert mesh3.get_indices_at_coords([1, -1, 0.5]) == (0, 1, 3)
# Non-default origin
mesh4 = openmc.SphericalMesh(
r_grid=(0, 5, 10),
origin=(100, 200, 300)
)
assert mesh4.get_indices_at_coords([103, 200, 300]) == (0, 0, 0)
assert mesh4.get_indices_at_coords([100, 200, 307]) == (1, 0, 0)
with pytest.raises(ValueError):
mesh4.get_indices_at_coords([111, 200, 300])
# Degenerate case: point at origin with r_grid starting at 0
mesh5 = openmc.SphericalMesh(r_grid=(0, 5))
assert mesh5.get_indices_at_coords([0, 0, 0]) == (0, 0, 0)
# Out-of-bounds theta: restricted theta grid
mesh6 = openmc.SphericalMesh(
r_grid=(0, 10),
theta_grid=(0, pi/4)
)
with pytest.raises(ValueError):
mesh6.get_indices_at_coords([5, 0, 0]) # theta=pi/2 > pi/4
# Out-of-bounds phi: restricted phi grid
mesh7 = openmc.SphericalMesh(
r_grid=(0, 10),
phi_grid=(0, pi/2)
)
with pytest.raises(ValueError):
mesh7.get_indices_at_coords([-5, 0, 0]) # phi=pi > pi/2
# Diagonal point: verify r, theta, phi all computed correctly
r = 6.0
val = r / sqrt(3)
result = mesh3.get_indices_at_coords([val, val, val])
assert result[0] == 1 # r=6 in second bin [5, 10]
assert result[1] == 1 # theta=arccos(1/sqrt(3))~0.955, in (pi/4, pi/2)
assert result[2] == 0 # phi=pi/4, in [0, pi/2)