Skip to content

Commit 111eb77

Browse files
Implement SphericalMesh.get_indices_at_coords (#3919)
Co-authored-by: Paul Romano <paul.k.romano@gmail.com>
1 parent 219d827 commit 111eb77

2 files changed

Lines changed: 184 additions & 25 deletions

File tree

openmc/mesh.py

Lines changed: 94 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,18 @@
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+
import math
77
from numbers import Integral, Real
88
from pathlib import Path
99
from typing import Protocol
1010

1111
import h5py
1212
import lxml.etree as ET
1313
import numpy as np
14-
from pathlib import Path
1514

1615
import openmc
1716
import openmc.checkvalue as cv
1817
from openmc.checkvalue import PathLike
19-
from openmc.utility_funcs import change_directory
2018
from .bounding_box import BoundingBox
2119
from ._xml import get_elem_list, get_text
2220
from .mixin import IDManagerMixin
@@ -291,7 +289,7 @@ def from_hdf5(cls, group: h5py.Group):
291289
"""
292290
mesh_type = 'regular' if 'type' not in group.keys() else group['type'][()].decode()
293291
mesh_id = int(group.name.split('/')[-1].lstrip('mesh '))
294-
mesh_name = '' if not 'name' in group else group['name'][()].decode()
292+
mesh_name = '' if 'name' not in group else group['name'][()].decode()
295293

296294
if mesh_type == 'regular':
297295
return RegularMesh.from_hdf5(group, mesh_id, mesh_name)
@@ -1903,7 +1901,7 @@ def __init__(
19031901
self,
19041902
r_grid: Sequence[float],
19051903
z_grid: Sequence[float],
1906-
phi_grid: Sequence[float] = (0, 2*pi),
1904+
phi_grid: Sequence[float] = (0, 2*math.pi),
19071905
origin: Sequence[float] = (0., 0., 0.),
19081906
mesh_id: int | None = None,
19091907
name: str = '',
@@ -1960,7 +1958,7 @@ def phi_grid(self, grid):
19601958
cv.check_length('mesh phi_grid', grid, 2)
19611959
cv.check_increasing('mesh phi_grid', grid)
19621960
grid = np.asarray(grid, dtype=float)
1963-
if np.any((grid < 0.0) | (grid > 2*pi)):
1961+
if np.any((grid < 0.0) | (grid > 2*math.pi)):
19641962
raise ValueError("phi_grid values must be in [0, 2π].")
19651963
self._phi_grid = grid
19661964

@@ -2028,9 +2026,9 @@ def __repr__(self):
20282026
return string
20292027

20302028
def get_indices_at_coords(
2031-
self,
2032-
coords: Sequence[float]
2033-
) -> tuple[int, int, int]:
2029+
self,
2030+
coords: Sequence[float]
2031+
) -> tuple[int, int, int]:
20342032
"""Finds the index of the mesh element at the specified coordinates.
20352033
20362034
.. versionadded:: 0.15.0
@@ -2046,7 +2044,7 @@ def get_indices_at_coords(
20462044
The r, phi, z indices
20472045
20482046
"""
2049-
r_value_from_origin = sqrt((coords[0]-self.origin[0])**2 + (coords[1]-self.origin[1])**2)
2047+
r_value_from_origin = math.hypot(coords[0]-self.origin[0], coords[1]-self.origin[1])
20502048

20512049
if r_value_from_origin < self.r_grid[0] or r_value_from_origin > self.r_grid[-1]:
20522050
raise ValueError(
@@ -2070,13 +2068,13 @@ def get_indices_at_coords(
20702068
delta_x = coords[0] - self.origin[0]
20712069
delta_y = coords[1] - self.origin[1]
20722070
# atan2 returns values in -pi to +pi range
2073-
phi_value = atan2(delta_y, delta_x)
2071+
phi_value = math.atan2(delta_y, delta_x)
20742072
if delta_x < 0 and delta_y < 0:
20752073
# returned phi_value anticlockwise and negative
2076-
phi_value += 2 * pi
2074+
phi_value += 2 * math.pi
20772075
if delta_x > 0 and delta_y < 0:
20782076
# returned phi_value anticlockwise and negative
2079-
phi_value += 2 * pi
2077+
phi_value += 2 * math.pi
20802078

20812079
phi_grid_values = np.array(self.phi_grid)
20822080

@@ -2111,7 +2109,7 @@ def from_bounding_box(
21112109
dimension: Sequence[int] = (10, 10, 10),
21122110
mesh_id: int | None = None,
21132111
name: str = '',
2114-
phi_grid_bounds: Sequence[float] = (0.0, 2*pi),
2112+
phi_grid_bounds: Sequence[float] = (0.0, 2*math.pi),
21152113
enclose_domain: bool = False,
21162114
) -> CylindricalMesh:
21172115
"""Create CylindricalMesh from a bounding box.
@@ -2339,8 +2337,8 @@ class SphericalMesh(StructuredMesh):
23392337
def __init__(
23402338
self,
23412339
r_grid: Sequence[float],
2342-
phi_grid: Sequence[float] = (0, 2*pi),
2343-
theta_grid: Sequence[float] = (0, pi),
2340+
phi_grid: Sequence[float] = (0, 2*math.pi),
2341+
theta_grid: Sequence[float] = (0, math.pi),
23442342
origin: Sequence[float] = (0., 0., 0.),
23452343
mesh_id: int | None = None,
23462344
name: str = '',
@@ -2397,7 +2395,7 @@ def theta_grid(self, grid):
23972395
cv.check_length('mesh theta_grid', grid, 2)
23982396
cv.check_increasing('mesh theta_grid', grid)
23992397
grid = np.asarray(grid, dtype=float)
2400-
if np.any((grid < 0.0) | (grid > pi)):
2398+
if np.any((grid < 0.0) | (grid > math.pi)):
24012399
raise ValueError("theta_grid values must be in [0, π].")
24022400
self._theta_grid = grid
24032401

@@ -2411,7 +2409,7 @@ def phi_grid(self, grid):
24112409
cv.check_length('mesh phi_grid', grid, 2)
24122410
cv.check_increasing('mesh phi_grid', grid)
24132411
grid = np.asarray(grid, dtype=float)
2414-
if np.any((grid < 0.0) | (grid > 2*pi)):
2412+
if np.any((grid < 0.0) | (grid > 2*math.pi)):
24152413
raise ValueError("phi_grid values must be in [0, 2π].")
24162414
self._phi_grid = grid
24172415

@@ -2483,8 +2481,8 @@ def from_bounding_box(
24832481
dimension: Sequence[int] = (10, 10, 10),
24842482
mesh_id: int | None = None,
24852483
name: str = '',
2486-
phi_grid_bounds: Sequence[float] = (0.0, 2*pi),
2487-
theta_grid_bounds: Sequence[float] = (0.0, pi),
2484+
phi_grid_bounds: Sequence[float] = (0.0, 2*math.pi),
2485+
theta_grid_bounds: Sequence[float] = (0.0, math.pi),
24882486
enclose_domain: bool = False,
24892487
) -> SphericalMesh:
24902488
"""Create SphericalMesh from a bounding box.
@@ -2645,10 +2643,82 @@ def _convert_to_cartesian(arr, origin: Sequence[float]):
26452643
arr[..., 2] = z + origin[2]
26462644
return arr
26472645

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

26532723

26542724
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
@@ -1065,3 +1065,92 @@ def test_rectilinear_mesh_get_indices_at_coords():
10651065
mesh.get_indices_at_coords([0.5, -0.5, 110.])
10661066
with pytest.raises(ValueError):
10671067
mesh.get_indices_at_coords([0.5, -20., 110.])
1068+
1069+
1070+
def test_SphericalMesh_get_indices_at_coords():
1071+
"""Test get_indices_at_coords method for SphericalMesh"""
1072+
1073+
# Basic mesh with default phi and theta grids (single angular bin)
1074+
mesh = openmc.SphericalMesh(r_grid=(0, 5, 10))
1075+
1076+
assert mesh.get_indices_at_coords([3, 0, 0]) == (0, 0, 0)
1077+
assert mesh.get_indices_at_coords([0, 0, 3]) == (0, 0, 0)
1078+
assert mesh.get_indices_at_coords([0, 0, -3]) == (0, 0, 0)
1079+
assert mesh.get_indices_at_coords([7, 0, 0]) == (1, 0, 0)
1080+
assert mesh.get_indices_at_coords([10, 0, 0]) == (1, 0, 0)
1081+
1082+
# Out-of-bounds r
1083+
with pytest.raises(ValueError):
1084+
mesh.get_indices_at_coords([11, 0, 0])
1085+
1086+
mesh2 = openmc.SphericalMesh(r_grid=(2, 5, 10))
1087+
with pytest.raises(ValueError):
1088+
mesh2.get_indices_at_coords([1, 0, 0])
1089+
1090+
# Multi-bin angular grids: use points clearly inside bins
1091+
mesh3 = openmc.SphericalMesh(
1092+
r_grid=(0, 5, 10),
1093+
theta_grid=(0, pi/4, pi/2, pi),
1094+
phi_grid=(0, pi/2, pi, 3*pi/2, 2*pi)
1095+
)
1096+
1097+
# Near z-axis: theta~0 -> bin 0
1098+
assert mesh3.get_indices_at_coords([0.01, 0, 3]) == (0, 0, 0)
1099+
1100+
# theta in (0, pi/4) -> bin 0: [1, 0, 2] theta=arccos(2/sqrt(5))~0.46
1101+
assert mesh3.get_indices_at_coords([1, 0, 2]) == (0, 0, 0)
1102+
1103+
# theta in (pi/4, pi/2) -> bin 1: [2, 0, 1] theta=arccos(1/sqrt(5))~1.107
1104+
assert mesh3.get_indices_at_coords([2, 0, 1]) == (0, 1, 0)
1105+
1106+
# theta in (pi/2, pi) -> bin 2: [1, 0, -2] theta=arccos(-2/sqrt(5))~2.034
1107+
assert mesh3.get_indices_at_coords([1, 0, -2]) == (0, 2, 0)
1108+
1109+
# phi in (pi/2, pi) -> bin 1: [-1, 1, 0.5]
1110+
assert mesh3.get_indices_at_coords([-1, 1, 0.5]) == (0, 1, 1)
1111+
1112+
# phi in (pi, 3*pi/2) -> bin 2: [-1, -1, 0.5]
1113+
assert mesh3.get_indices_at_coords([-1, -1, 0.5]) == (0, 1, 2)
1114+
1115+
# phi in (3*pi/2, 2*pi) -> bin 3: [1, -1, 0.5]
1116+
assert mesh3.get_indices_at_coords([1, -1, 0.5]) == (0, 1, 3)
1117+
1118+
# Non-default origin
1119+
mesh4 = openmc.SphericalMesh(
1120+
r_grid=(0, 5, 10),
1121+
origin=(100, 200, 300)
1122+
)
1123+
1124+
assert mesh4.get_indices_at_coords([103, 200, 300]) == (0, 0, 0)
1125+
assert mesh4.get_indices_at_coords([100, 200, 307]) == (1, 0, 0)
1126+
1127+
with pytest.raises(ValueError):
1128+
mesh4.get_indices_at_coords([111, 200, 300])
1129+
1130+
# Degenerate case: point at origin with r_grid starting at 0
1131+
mesh5 = openmc.SphericalMesh(r_grid=(0, 5))
1132+
assert mesh5.get_indices_at_coords([0, 0, 0]) == (0, 0, 0)
1133+
1134+
# Out-of-bounds theta: restricted theta grid
1135+
mesh6 = openmc.SphericalMesh(
1136+
r_grid=(0, 10),
1137+
theta_grid=(0, pi/4)
1138+
)
1139+
with pytest.raises(ValueError):
1140+
mesh6.get_indices_at_coords([5, 0, 0]) # theta=pi/2 > pi/4
1141+
1142+
# Out-of-bounds phi: restricted phi grid
1143+
mesh7 = openmc.SphericalMesh(
1144+
r_grid=(0, 10),
1145+
phi_grid=(0, pi/2)
1146+
)
1147+
with pytest.raises(ValueError):
1148+
mesh7.get_indices_at_coords([-5, 0, 0]) # phi=pi > pi/2
1149+
1150+
# Diagonal point: verify r, theta, phi all computed correctly
1151+
r = 6.0
1152+
val = r / sqrt(3)
1153+
result = mesh3.get_indices_at_coords([val, val, val])
1154+
assert result[0] == 1 # r=6 in second bin [5, 10]
1155+
assert result[1] == 1 # theta=arccos(1/sqrt(3))~0.955, in (pi/4, pi/2)
1156+
assert result[2] == 0 # phi=pi/4, in [0, pi/2)

0 commit comments

Comments
 (0)