Skip to content

Commit e2d6fcf

Browse files
authored
Add many tests and fix a few bugs (#48)
1 parent f3e9b6c commit e2d6fcf

6 files changed

Lines changed: 525 additions & 14 deletions

File tree

src/openmc_mcnp_adapter/openmc_conversion.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,8 @@ def flip_sense(surf):
334334
# decide if we want the up or down part of the
335335
# cone since one sheet is used
336336
up = grad >= 0
337-
surf = cls_cone(z0=offset, r2=angle, up=up)
337+
kwargs = {f"{s['mnemonic']}0": offset, "r2": angle, "up": up}
338+
surf = cls_cone(**kwargs)
338339
else:
339340
raise NotImplementedError(f"{s['mnemonic']} surface with {len(coeffs)} parameters")
340341
elif s['mnemonic'] == 'rcc':
@@ -919,13 +920,13 @@ def mcnp_to_model(filename, merge_surfaces: bool = True, expand_elements: bool =
919920
return openmc.Model(geometry, materials, settings)
920921

921922

922-
def mcnp_str_to_model(text: str, merge_surfaces: bool = True):
923+
def mcnp_str_to_model(text: str, **kwargs):
923924
# Write string to a temporary file
924925
with tempfile.NamedTemporaryFile('w', delete=False) as fp:
925926
fp.write(text)
926927

927928
# Parse model from file
928-
model = mcnp_to_model(fp.name, merge_surfaces)
929+
model = mcnp_to_model(fp.name, **kwargs)
929930

930931
# Remove temporary file and return model
931932
os.remove(fp.name)

src/openmc_mcnp_adapter/parse.py

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
_KEYWORDS = [
1414
r'\*?trcl', r'\*?fill', 'tmp', 'u', 'lat',
1515
'imp:.', 'vol', 'pwt', 'ext:.', 'fcl', 'wwn', 'dxc', 'nonu', 'pd',
16-
'elpt', 'cosy', 'bflcl', 'unc',
16+
'elpt', 'cosy', 'bflcl', 'unc', 'mat', 'rho',
1717
'pmt' # D1SUNED-specific
1818
]
1919
_ANY_KEYWORD = '|'.join(f'(?:{k})' for k in _KEYWORDS)
@@ -44,9 +44,29 @@
4444
_SAB_RE = re.compile(r'\s*[Mm][Tt](\d+)((?:\s+\S+)+)')
4545
_MODE_RE = re.compile(r'\s*mode(?:\s+\S+)*')
4646
_COMPLEMENT_RE = re.compile(r'(#)[ ]*(\d+)')
47-
_REPEAT_RE = re.compile(r'(\d+)\s+(\d+)[rR]')
4847
_NUM_RE = re.compile(r'(\d)([+-])(\d)')
4948

49+
_HAS_REPEAT_RE = re.compile(r'\b\d+[rR]\b')
50+
51+
_REPEAT_RE = re.compile(r"""
52+
(?P<value> # The numeric value to be repeated
53+
[+-]? # Optional sign
54+
(?: # Mantissa
55+
\d+(?:\.\d*)? # Digits with optional fractional part (e.g., 3 or 3. or 3.0)
56+
| # or
57+
\.\d+ # Leading-dot form (e.g., .25)
58+
)
59+
(?: # Optional exponent
60+
[eEdD][+-]?\d+ # E/D exponent with optional sign (e.g., 1e-3, 2D+3)
61+
| # or MCNP "bare" exponent without E/D
62+
[+-]\d+ # appended sign+digits (e.g., 1.0-3 -> 1.0e-3)
63+
)?
64+
)
65+
\s+ # One or more spaces between value and count
66+
(?P<count>\d+) # The repeat count
67+
[rR] # The 'R' or 'r' suffix
68+
""", re.VERBOSE)
69+
5070

5171
def float_(val):
5272
"""Convert scientific notation literals that don't have an 'e' in them to float"""
@@ -346,11 +366,12 @@ def sanitize(section: str) -> str:
346366
section = re.sub('\n {5}', ' ', section)
347367

348368
# Expand repeated numbers
349-
m = _REPEAT_RE.search(section)
350-
while m is not None:
351-
section = _REPEAT_RE.sub(' '.join((int(m.group(2)) + 1)*[m.group(1)]),
352-
section, 1)
353-
m = _REPEAT_RE.search(section)
369+
if _HAS_REPEAT_RE.search(section):
370+
section = _REPEAT_RE.sub(
371+
lambda m: ' '.join([m.group('value')] * (int(m.group('count')) + 1)),
372+
section,
373+
)
374+
354375
return section
355376

356377

tests/test_geometry.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
from textwrap import dedent
2+
3+
from pytest import mark, approx, param
4+
from openmc_mcnp_adapter import mcnp_str_to_model
5+
6+
7+
@mark.parametrize("whitespace", ["", " ", "\t"])
8+
def test_cell_complement(whitespace):
9+
# Cell 2 corresponds to r < 2 intersected with z > 0
10+
mcnp_str = dedent(f"""
11+
title
12+
100 1 1.0 +1 : -2
13+
2 1 1.0 #{whitespace}100
14+
15+
1 so 2.0
16+
2 pz 0.0
17+
18+
m1 1001.80c 1.0
19+
""")
20+
model = mcnp_str_to_model(mcnp_str)
21+
cell = model.geometry.get_all_cells()[2]
22+
23+
# Check various points
24+
assert (0., 0., 0.1) in cell.region
25+
assert (0., 0., -0.1) not in cell.region
26+
assert (0., 0., 1.99) in cell.region
27+
assert (0., 0., 2.01) not in cell.region
28+
assert (1., 1., 1.) in cell.region
29+
assert (2., 0., 1.) not in cell.region
30+
31+
32+
def test_likenbut():
33+
mcnp_str = dedent("""
34+
title
35+
1 1 -1.0 -1
36+
2 LIKE 1 BUT MAT=2 RHO=-2.0 TRCL=(2.0 0.0 0.0)
37+
38+
1 so 1.0
39+
40+
m1 1001.80c 1.0
41+
m2 1002.80c 1.0
42+
""")
43+
model = mcnp_str_to_model(mcnp_str)
44+
cell = model.geometry.get_all_cells()[2]
45+
46+
# Material should be changed to m2
47+
mat = cell.fill
48+
assert 'H2' in mat.get_nuclide_densities()
49+
50+
# Density should be 2.0 g/cm3
51+
assert mat.get_mass_density() == approx(2.0)
52+
53+
# Points should correspond to sphere of r=1 centered at (2, 0, 0)
54+
assert (2.0, 0.0, 0.0) in cell.region
55+
assert (0.0, 0.0, 0.0) not in cell.region
56+
assert (2.0, 0.9, 0.0) in cell.region
57+
assert (2.0, 1.1, 0.0) not in cell.region
58+
59+
60+
@mark.parametrize(
61+
"cell_card, surface_cards, points_inside, points_outside",
62+
[
63+
(
64+
"1 1 -1.0 -1 TRCL=(2.0 0.0 0.0)",
65+
("1 so 1.0",),
66+
[
67+
(2.0, 0.0, 0.0),
68+
(2.0, 0.9, 0.0),
69+
(2.0, 0.0, 0.9),
70+
],
71+
[
72+
(0.9, 0.0, 0.0),
73+
(2.0, 1.1, 0.0),
74+
(2.0, 0.0, 1.1),
75+
],
76+
),
77+
(
78+
"1 0 -1 TRCL=(1.0 0.0 0.0 0.0 -1.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0)",
79+
("1 rpp -0.5 0.5 -0.25 0.25 -0.1 0.1",),
80+
[
81+
(1.0, 0.0, 0.0),
82+
(1.2, 0.0, 0.0),
83+
(1.0, 0.4, 0.0),
84+
(1.0, -0.4, 0.0),
85+
],
86+
[
87+
(0.7, 0.0, 0.0),
88+
(1.3, 0.0, 0.0),
89+
(1.0, 0.6, 0.0),
90+
(1.0, 0.0, 0.2),
91+
],
92+
),
93+
(
94+
"1 0 -1 *TRCL=(1.0 0.0 0.0 90.0 180.0 90.0 0.0 90.0 90.0 90.0 90.0 0.0)",
95+
("1 rpp -0.5 0.5 -0.25 0.25 -0.1 0.1",),
96+
[
97+
(1.0, 0.0, 0.0),
98+
(1.2, 0.0, 0.0),
99+
(1.0, 0.4, 0.0),
100+
(1.0, -0.4, 0.0),
101+
],
102+
[
103+
(0.7, 0.0, 0.0),
104+
(1.3, 0.0, 0.0),
105+
(1.0, 0.6, 0.0),
106+
(1.0, 0.0, 0.2),
107+
],
108+
),
109+
param(
110+
"1 0 -1 TRCL=(0.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0 -1)",
111+
("1 rpp -0.5 0.5 -0.25 0.25 -0.1 0.1",),
112+
[],
113+
[],
114+
marks=mark.xfail(reason="13-parameter TRCL not yet supported"),
115+
),
116+
],
117+
)
118+
def test_trcl(cell_card, surface_cards, points_inside, points_outside):
119+
surface_block = "\n".join(surface_cards)
120+
mcnp_str = dedent(f"""
121+
title
122+
{cell_card}
123+
124+
{surface_block}
125+
126+
m1 1001.80c 1.0
127+
""")
128+
model = mcnp_str_to_model(mcnp_str)
129+
cell = model.geometry.get_all_cells()[1]
130+
131+
for point in points_inside:
132+
assert point in cell.region
133+
134+
for point in points_outside:
135+
assert point not in cell.region
136+
137+
138+
def test_trcl_fill():
139+
mcnp_str = dedent("""
140+
title
141+
1 0 -1 FILL=10 TRCL=(2.0 0.0 0.0)
142+
2 0 -2 U=10
143+
3 0 +2 U=10
144+
145+
1 so 10.0
146+
2 so 1.0
147+
148+
m1 1001.80c 1.0
149+
""")
150+
model = mcnp_str_to_model(mcnp_str)
151+
geometry = model.geometry
152+
cells = geometry.get_all_cells()
153+
154+
# Make sure that the cells in universe 10 were shifted
155+
assert geometry.find((2.0, 0.0, 0.0))[-1] is cells[2]
156+
assert geometry.find((4.0, 0.0, 0.0))[-1] is cells[3]
157+
assert geometry.find((0.0, 0.0, 0.0))[-1] is cells[3]

tests/test_material.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,24 @@
11
import textwrap
22

3+
import openmc
34
from openmc_mcnp_adapter import mcnp_str_to_model
45
from pytest import approx
56

67

8+
def convert_material(mat_card: str, density: float, thermal_card: str = "", **kwargs) -> openmc.Material:
9+
mcnp_model = textwrap.dedent(f"""
10+
title
11+
1 1 {density} 1
12+
13+
1 px 0.0
14+
15+
{mat_card}
16+
{thermal_card}
17+
""")
18+
model = mcnp_str_to_model(mcnp_model, **kwargs)
19+
return model.materials[0]
20+
21+
722
def test_material_clones():
823
mcnp_model = textwrap.dedent("""
924
title
@@ -24,3 +39,73 @@ def test_material_clones():
2439
assert cells[2].fill.id == cells[3].fill.id != 1
2540
assert cells[1].fill.get_mass_density() == approx(1.0)
2641
assert cells[2].fill.get_mass_density() == approx(2.0)
42+
43+
44+
def test_material_suffixes():
45+
# H1 with XS suffix; O16 without suffix
46+
mat_card = "m1 1001.80c 2.0 8016 1.0"
47+
thermal_card = "mt1 lwtr grph.10t"
48+
m = convert_material(mat_card, -2.0, thermal_card)
49+
nd = m.get_nuclide_densities()
50+
assert set(nd.keys()) == {'H1', 'O16'}
51+
assert nd['H1'].percent == approx(2.0)
52+
assert nd['H1'].percent_type == 'ao'
53+
assert nd['O16'].percent == approx(1.0)
54+
assert nd['O16'].percent_type == 'ao'
55+
56+
# Check S(a,b) tables mapped via get_thermal_name
57+
# Access private field because there is no public accessor
58+
sab_names = {name for (name, _) in getattr(m, '_sab', [])}
59+
assert 'c_H_in_H2O' in sab_names
60+
assert 'c_Graphite' in sab_names
61+
62+
63+
def test_weight_fractions():
64+
# 6000 -> natural C, 5010/5011 -> B-10/B-11; negative => weight
65+
mat_card = "m1 6000 -0.12 5010 -0.2 5011 -0.8"
66+
m = convert_material(mat_card, -1.0)
67+
nd = m.get_nuclide_densities()
68+
69+
# Natural carbon represented as C0 in OpenMC
70+
assert 'C0' in nd and nd['C0'].percent_type == 'wo' and nd['C0'].percent == approx(0.12)
71+
assert 'B10' in nd and nd['B10'].percent_type == 'wo' and nd['B10'].percent == approx(0.2)
72+
assert 'B11' in nd and nd['B11'].percent_type == 'wo' and nd['B11'].percent == approx(0.8)
73+
74+
75+
def test_no_expand_elements():
76+
# With expand_elements=False, natural oxygen should be added as O0 directly
77+
mat_card = "m1 47000 1.0"
78+
m = convert_material(mat_card, -1.0, expand_elements=False)
79+
nd = m.get_nuclide_densities()
80+
assert 'Ag0' in nd
81+
assert nd['Ag0'].percent_type == 'ao'
82+
assert nd['Ag0'].percent == approx(1.0)
83+
84+
85+
def test_mass_density():
86+
mat_card = "m1 3006.80c 0.5 3007.80c 0.5"
87+
m = convert_material(mat_card, -3.0)
88+
assert m.get_mass_density() == approx(3.0)
89+
90+
91+
def test_atom_density():
92+
mat_card = "m1 3006.80c 0.5 3007.80c 0.5"
93+
m = convert_material(mat_card, 0.02)
94+
nuclide_densities = m.get_nuclide_atom_densities()
95+
assert sum(nuclide_densities.values()) == approx(0.02)
96+
assert nuclide_densities['Li6'] == approx(0.01)
97+
assert nuclide_densities['Li7'] == approx(0.01)
98+
99+
100+
def test_density_no_whitespace():
101+
mcnp_model = textwrap.dedent("""
102+
title
103+
1 1 -4.5( -1 )
104+
105+
1 px 0.0
106+
107+
m1 1001.80c 2.0
108+
""")
109+
model = mcnp_str_to_model(mcnp_model)
110+
m = model.materials[0]
111+
assert m.get_mass_density() == approx(4.5)

0 commit comments

Comments
 (0)