Skip to content

Commit f311d54

Browse files
Implement SphericalMesh.get_indices_at_coords
Resolves the remaining work from #3867. Converts Cartesian (x, y, z) input to spherical (r, theta, phi) relative to the mesh origin, validates against grid bounds, and returns bin indices via np.searchsorted. Handles the degenerate r=0 case where angular coordinates are undefined. Adds comprehensive unit tests covering basic lookups, multi-bin angular grids, non-default origin, boundary values, r=0, and out-of-bounds errors for each axis.
1 parent 23e8a11 commit f311d54

2 files changed

Lines changed: 167 additions & 6 deletions

File tree

openmc/mesh.py

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from abc import ABC, abstractmethod
44
from collections.abc import Iterable, Sequence, Mapping
55
from functools import wraps
6-
from math import pi, sqrt, atan2
6+
from math import acos, atan2, pi, sqrt
77
from numbers import Integral, Real
88
from pathlib import Path
99
from typing import Protocol
@@ -2644,10 +2644,82 @@ def _convert_to_cartesian(arr, origin: Sequence[float]):
26442644
arr[..., 2] = z + origin[2]
26452645
return arr
26462646

2647-
def get_indices_at_coords(self, coords: Sequence[float]) -> tuple:
2648-
raise NotImplementedError(
2649-
"get_indices_at_coords is not yet implemented for SphericalMesh"
2650-
)
2647+
def get_indices_at_coords(
2648+
self,
2649+
coords: Sequence[float]
2650+
) -> tuple[int, int, int]:
2651+
"""Find the mesh cell indices containing the specified coordinates.
2652+
2653+
.. versionadded:: 0.15.4
2654+
2655+
Parameters
2656+
----------
2657+
coords : Sequence[float]
2658+
Cartesian coordinates of the point as (x, y, z).
2659+
2660+
Returns
2661+
-------
2662+
tuple[int, int, int]
2663+
The r, theta, phi indices.
2664+
2665+
Raises
2666+
------
2667+
ValueError
2668+
If the coordinates fall outside the mesh grid boundaries.
2669+
2670+
"""
2671+
dx = coords[0] - self.origin[0]
2672+
dy = coords[1] - self.origin[1]
2673+
dz = coords[2] - self.origin[2]
2674+
2675+
r_value = sqrt(dx**2 + dy**2 + dz**2)
2676+
2677+
if r_value < self.r_grid[0] or r_value > self.r_grid[-1]:
2678+
raise ValueError(
2679+
f'The r value {r_value} computed from the specified '
2680+
f'coordinates is outside the r grid range '
2681+
f'[{self.r_grid[0]}, {self.r_grid[-1]}].'
2682+
)
2683+
2684+
r_index = int(min(
2685+
np.searchsorted(self.r_grid, r_value, side='right') - 1,
2686+
len(self.r_grid) - 2
2687+
))
2688+
2689+
if r_value == 0.0:
2690+
theta_value = 0.0
2691+
phi_value = 0.0
2692+
else:
2693+
theta_value = acos(dz / r_value)
2694+
phi_value = atan2(dy, dx)
2695+
if phi_value < 0:
2696+
phi_value += 2 * pi
2697+
2698+
if theta_value < self.theta_grid[0] or theta_value > self.theta_grid[-1]:
2699+
raise ValueError(
2700+
f'The theta value {theta_value} computed from the specified '
2701+
f'coordinates is outside the theta grid range '
2702+
f'[{self.theta_grid[0]}, {self.theta_grid[-1]}].'
2703+
)
2704+
2705+
theta_index = int(min(
2706+
np.searchsorted(self.theta_grid, theta_value, side='right') - 1,
2707+
len(self.theta_grid) - 2
2708+
))
2709+
2710+
if phi_value < self.phi_grid[0] or phi_value > self.phi_grid[-1]:
2711+
raise ValueError(
2712+
f'The phi value {phi_value} computed from the specified '
2713+
f'coordinates is outside the phi grid range '
2714+
f'[{self.phi_grid[0]}, {self.phi_grid[-1]}].'
2715+
)
2716+
2717+
phi_index = int(min(
2718+
np.searchsorted(self.phi_grid, phi_value, side='right') - 1,
2719+
len(self.phi_grid) - 2
2720+
))
2721+
2722+
return (r_index, theta_index, phi_index)
26512723

26522724

26532725
def require_statepoint_data(func):

tests/unit_tests/test_mesh.py

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from math import pi
1+
from math import pi, sqrt
22
from tempfile import TemporaryDirectory
33
from pathlib import Path
44
import itertools
@@ -1028,3 +1028,92 @@ def test_rectilinear_mesh_get_indices_at_coords():
10281028
mesh.get_indices_at_coords([0.5, -0.5, 110.])
10291029
with pytest.raises(ValueError):
10301030
mesh.get_indices_at_coords([0.5, -20., 110.])
1031+
1032+
1033+
def test_SphericalMesh_get_indices_at_coords():
1034+
"""Test get_indices_at_coords method for SphericalMesh"""
1035+
1036+
# Basic mesh with default phi and theta grids (single angular bin)
1037+
mesh = openmc.SphericalMesh(r_grid=(0, 5, 10))
1038+
1039+
assert mesh.get_indices_at_coords([3, 0, 0]) == (0, 0, 0)
1040+
assert mesh.get_indices_at_coords([0, 0, 3]) == (0, 0, 0)
1041+
assert mesh.get_indices_at_coords([0, 0, -3]) == (0, 0, 0)
1042+
assert mesh.get_indices_at_coords([7, 0, 0]) == (1, 0, 0)
1043+
assert mesh.get_indices_at_coords([10, 0, 0]) == (1, 0, 0)
1044+
1045+
# Out-of-bounds r
1046+
with pytest.raises(ValueError):
1047+
mesh.get_indices_at_coords([11, 0, 0])
1048+
1049+
mesh2 = openmc.SphericalMesh(r_grid=(2, 5, 10))
1050+
with pytest.raises(ValueError):
1051+
mesh2.get_indices_at_coords([1, 0, 0])
1052+
1053+
# Multi-bin angular grids: use points clearly inside bins
1054+
mesh3 = openmc.SphericalMesh(
1055+
r_grid=(0, 5, 10),
1056+
theta_grid=(0, pi/4, pi/2, pi),
1057+
phi_grid=(0, pi/2, pi, 3*pi/2, 2*pi)
1058+
)
1059+
1060+
# Near z-axis: theta~0 -> bin 0
1061+
assert mesh3.get_indices_at_coords([0.01, 0, 3]) == (0, 0, 0)
1062+
1063+
# theta in (0, pi/4) -> bin 0: [1, 0, 2] theta=arccos(2/sqrt(5))~0.46
1064+
assert mesh3.get_indices_at_coords([1, 0, 2]) == (0, 0, 0)
1065+
1066+
# theta in (pi/4, pi/2) -> bin 1: [2, 0, 1] theta=arccos(1/sqrt(5))~1.107
1067+
assert mesh3.get_indices_at_coords([2, 0, 1]) == (0, 1, 0)
1068+
1069+
# theta in (pi/2, pi) -> bin 2: [1, 0, -2] theta=arccos(-2/sqrt(5))~2.034
1070+
assert mesh3.get_indices_at_coords([1, 0, -2]) == (0, 2, 0)
1071+
1072+
# phi in (pi/2, pi) -> bin 1: [-1, 1, 0.5]
1073+
assert mesh3.get_indices_at_coords([-1, 1, 0.5]) == (0, 1, 1)
1074+
1075+
# phi in (pi, 3*pi/2) -> bin 2: [-1, -1, 0.5]
1076+
assert mesh3.get_indices_at_coords([-1, -1, 0.5]) == (0, 1, 2)
1077+
1078+
# phi in (3*pi/2, 2*pi) -> bin 3: [1, -1, 0.5]
1079+
assert mesh3.get_indices_at_coords([1, -1, 0.5]) == (0, 1, 3)
1080+
1081+
# Non-default origin
1082+
mesh4 = openmc.SphericalMesh(
1083+
r_grid=(0, 5, 10),
1084+
origin=(100, 200, 300)
1085+
)
1086+
1087+
assert mesh4.get_indices_at_coords([103, 200, 300]) == (0, 0, 0)
1088+
assert mesh4.get_indices_at_coords([100, 200, 307]) == (1, 0, 0)
1089+
1090+
with pytest.raises(ValueError):
1091+
mesh4.get_indices_at_coords([111, 200, 300])
1092+
1093+
# Degenerate case: point at origin with r_grid starting at 0
1094+
mesh5 = openmc.SphericalMesh(r_grid=(0, 5))
1095+
assert mesh5.get_indices_at_coords([0, 0, 0]) == (0, 0, 0)
1096+
1097+
# Out-of-bounds theta: restricted theta grid
1098+
mesh6 = openmc.SphericalMesh(
1099+
r_grid=(0, 10),
1100+
theta_grid=(0, pi/4)
1101+
)
1102+
with pytest.raises(ValueError):
1103+
mesh6.get_indices_at_coords([5, 0, 0]) # theta=pi/2 > pi/4
1104+
1105+
# Out-of-bounds phi: restricted phi grid
1106+
mesh7 = openmc.SphericalMesh(
1107+
r_grid=(0, 10),
1108+
phi_grid=(0, pi/2)
1109+
)
1110+
with pytest.raises(ValueError):
1111+
mesh7.get_indices_at_coords([-5, 0, 0]) # phi=pi > pi/2
1112+
1113+
# Diagonal point: verify r, theta, phi all computed correctly
1114+
r = 6.0
1115+
val = r / sqrt(3)
1116+
result = mesh3.get_indices_at_coords([val, val, val])
1117+
assert result[0] == 1 # r=6 in second bin [5, 10]
1118+
assert result[1] == 1 # theta=arccos(1/sqrt(3))~0.955, in (pi/4, pi/2)
1119+
assert result[2] == 0 # phi=pi/4, in [0, pi/2)

0 commit comments

Comments
 (0)