Skip to content

Commit 781dbf9

Browse files
realmischGuyStenpaulromano
authored
Multi-group capability for kinetics parameter calculations with Iterated Fission Probability (#3425)
Co-authored-by: GuySten <[email protected]> Co-authored-by: Paul Romano <[email protected]>
1 parent 767db7e commit 781dbf9

File tree

14 files changed

+272
-8
lines changed

14 files changed

+272
-8
lines changed

docs/source/usersguide/kinetics.rst

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,23 @@ are needed to compute kinetics parameters in OpenMC:
6767
Obtaining kinetics parameters
6868
-----------------------------
6969

70+
The ``Model`` class can be used to automatically generate all IFP tallies using
71+
the Python API with :attr:`openmc.Settings.ifp_n_generation` greater than 0 and
72+
the :meth:`openmc.Model.add_ifp_kinetics_tallies` method::
73+
74+
model = openmc.Model(geometry, settings=settings)
75+
model.add_kinetics_parameters_tallies(num_groups=6) # Add 6 precursor groups
76+
77+
Alternatively, each of the tallies can be manually defined using group-wise or
78+
total :math:`\beta_{\text{eff}}` specified by providing a 6-group
79+
:class:`openmc.DelayedGroupFilter`::
80+
81+
beta_tally = openmc.Tally(name="group-beta-score")
82+
beta_tally.scores = ["ifp-beta-numerator"]
83+
84+
# Add DelayedGroupFilter to enable group-wise tallies
85+
beta_tally.filters = [openmc.DelayedGroupFilter(list(range(1, 7)))]
86+
7087
Here is an example showing how to declare the three available IFP scores in a
7188
single tally::
7289

@@ -95,6 +112,12 @@ for ``ifp-denominator``:
95112
96113
\beta_{\text{eff}} = \frac{S_{\text{ifp-beta-numerator}}}{S_{\text{ifp-denominator}}}
97114
115+
The kinetics parameters can be retrieved directly from a statepoint file using
116+
the :meth:`openmc.StatePoint.ifp_results` method::
117+
118+
with openmc.StatePoint(output_path) as sp:
119+
generation_time, beta_eff = sp.get_kinetics_parameters()
120+
98121
.. only:: html
99122

100123
.. rubric:: References
@@ -107,4 +130,4 @@ for ``ifp-denominator``:
107130
of the Iterated Fission Probability Method in OpenMC to Compute Adjoint-Weighted
108131
Kinetics Parameters", International Conference on Mathematics and Computational
109132
Methods Applied to Nuclear Science and Engineering (M&C 2025), Denver, April 27-30,
110-
2025 (to be presented).
133+
2025.

openmc/model/model.py

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22
from collections.abc import Iterable, Sequence
33
import copy
4-
from functools import lru_cache
4+
from functools import cache
55
from pathlib import Path
66
import math
77
from numbers import Integral, Real
@@ -160,7 +160,7 @@ def is_initialized(self) -> bool:
160160
return False
161161

162162
@property
163-
@lru_cache(maxsize=None)
163+
@cache
164164
def _materials_by_id(self) -> dict:
165165
"""Dictionary mapping material ID --> material"""
166166
if self.materials:
@@ -170,14 +170,14 @@ def _materials_by_id(self) -> dict:
170170
return {mat.id: mat for mat in mats}
171171

172172
@property
173-
@lru_cache(maxsize=None)
173+
@cache
174174
def _cells_by_id(self) -> dict:
175175
"""Dictionary mapping cell ID --> cell"""
176176
cells = self.geometry.get_all_cells()
177177
return {cell.id: cell for cell in cells.values()}
178178

179179
@property
180-
@lru_cache(maxsize=None)
180+
@cache
181181
def _cells_by_name(self) -> dict[int, openmc.Cell]:
182182
# Get the names maps, but since names are not unique, store a set for
183183
# each name key. In this way when the user requests a change by a name,
@@ -190,7 +190,7 @@ def _cells_by_name(self) -> dict[int, openmc.Cell]:
190190
return result
191191

192192
@property
193-
@lru_cache(maxsize=None)
193+
@cache
194194
def _materials_by_name(self) -> dict[int, openmc.Material]:
195195
if self.materials is None:
196196
mats = self.geometry.get_all_materials().values()
@@ -203,6 +203,37 @@ def _materials_by_name(self) -> dict[int, openmc.Material]:
203203
result[mat.name].add(mat)
204204
return result
205205

206+
def add_kinetics_parameters_tallies(self, num_groups: int | None = None):
207+
"""Add tallies for calculating kinetics parameters using the IFP method.
208+
209+
This method adds tallies to the model for calculating two kinetics
210+
parameters, the generation time and the effective delayed neutron
211+
fraction (beta effective). After a model is run, these parameters can be
212+
determined through the :meth:`openmc.StatePoint.ifp_results` method.
213+
214+
Parameters
215+
----------
216+
num_groups : int, optional
217+
Number of precursor groups to filter the delayed neutron fraction.
218+
If None, only the total effective delayed neutron fraction is
219+
tallied.
220+
221+
"""
222+
if not any('ifp-time-numerator' in t.scores for t in self.tallies):
223+
gen_time_tally = openmc.Tally(name='IFP time numerator')
224+
gen_time_tally.scores = ['ifp-time-numerator']
225+
self.tallies.append(gen_time_tally)
226+
if not any('ifp-beta-numerator' in t.scores for t in self.tallies):
227+
beta_tally = openmc.Tally(name='IFP beta numerator')
228+
beta_tally.scores = ['ifp-beta-numerator']
229+
if num_groups is not None:
230+
beta_tally.filters = [openmc.DelayedGroupFilter(list(range(1, num_groups + 1)))]
231+
self.tallies.append(beta_tally)
232+
if not any('ifp-denominator' in t.scores for t in self.tallies):
233+
denom_tally = openmc.Tally(name='IFP denominator')
234+
denom_tally.scores = ['ifp-denominator']
235+
self.tallies.append(denom_tally)
236+
206237
@classmethod
207238
def from_xml(
208239
cls,

openmc/statepoint.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from datetime import datetime
2+
from collections import namedtuple
23
import glob
34
import re
45
import os
@@ -8,13 +9,17 @@
89
import numpy as np
910
from pathlib import Path
1011
from uncertainties import ufloat
12+
from uncertainties.unumpy import uarray
1113

1214
import openmc
1315
import openmc.checkvalue as cv
1416

1517
_VERSION_STATEPOINT = 18
1618

1719

20+
KineticsParameters = namedtuple("KineticsParameters", ["generation_time", "beta_effective"])
21+
22+
1823
class StatePoint:
1924
"""State information on a simulation at a certain point in time (at the end
2025
of a given batch). Statepoints can be used to analyze tally results as well
@@ -710,3 +715,56 @@ def link_with_summary(self, summary):
710715
tally_filter.paths = cell.paths
711716

712717
self._summary = summary
718+
719+
def get_kinetics_parameters(self) -> KineticsParameters:
720+
"""Get kinetics parameters from IFP tallies.
721+
722+
This method searches the tallies in the statepoint for the tallies
723+
required to compute kinetics parameters using the Iterated Fission
724+
Probability (IFP) method.
725+
726+
Returns
727+
-------
728+
KineticsParameters
729+
A named tuple containing the generation time and effective delayed
730+
neutron fraction. If the necessary tallies for one or both
731+
parameters are not found, that parameter is returned as None.
732+
733+
"""
734+
735+
denom_tally = None
736+
gen_time_tally = None
737+
beta_tally = None
738+
for tally in self.tallies.values():
739+
if 'ifp-denominator' in tally.scores:
740+
denom_tally = self.get_tally(scores=['ifp-denominator'])
741+
if 'ifp-time-numerator' in tally.scores:
742+
gen_time_tally = self.get_tally(scores=['ifp-time-numerator'])
743+
if 'ifp-beta-numerator' in tally.scores:
744+
beta_tally = self.get_tally(scores=['ifp-beta-numerator'])
745+
746+
if denom_tally is None:
747+
return KineticsParameters(None, None)
748+
749+
def get_ufloat(tally, score):
750+
return uarray(tally.get_values(scores=[score]),
751+
tally.get_values(scores=[score], value='std_dev'))
752+
753+
denom_values = get_ufloat(denom_tally, 'ifp-denominator')
754+
if gen_time_tally is None:
755+
generation_time = None
756+
else:
757+
gen_time_values = get_ufloat(gen_time_tally, 'ifp-time-numerator')
758+
gen_time_values /= denom_values*self.keff
759+
generation_time = gen_time_values.flatten()[0]
760+
761+
if beta_tally is None:
762+
beta_effective = None
763+
else:
764+
beta_values = get_ufloat(beta_tally, 'ifp-beta-numerator')
765+
beta_values /= denom_values
766+
beta_effective = beta_values.flatten()
767+
if beta_effective.size == 1:
768+
beta_effective = beta_effective[0]
769+
770+
return KineticsParameters(generation_time, beta_effective)

src/tallies/tally.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -560,7 +560,8 @@ void Tally::set_scores(const vector<std::string>& scores)
560560
// Make sure a delayed group filter wasn't used with an incompatible
561561
// score.
562562
if (delayedgroup_filter_ != C_NONE) {
563-
if (score_str != "delayed-nu-fission" && score_str != "decay-rate")
563+
if (score_str != "delayed-nu-fission" && score_str != "decay-rate" &&
564+
score_str != "ifp-beta-numerator")
564565
fatal_error("Cannot tally " + score_str + "with a delayedgroup filter");
565566
}
566567

src/tallies/tally_scoring.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -964,6 +964,15 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index,
964964
if (delayed_groups.size() == settings::ifp_n_generation) {
965965
if (delayed_groups[0] > 0) {
966966
score = p.wgt_last();
967+
if (tally.delayedgroup_filter_ != C_NONE) {
968+
auto i_dg_filt = tally.filters()[tally.delayedgroup_filter_];
969+
const DelayedGroupFilter& filt {
970+
*dynamic_cast<DelayedGroupFilter*>(
971+
model::tally_filters[i_dg_filt].get())};
972+
score_fission_delayed_dg(i_tally, delayed_groups[0] - 1,
973+
score, score_index, p.filter_matches());
974+
continue;
975+
}
967976
}
968977
}
969978
}

tests/regression_tests/ifp/groupwise/__init__.py

Whitespace-only changes.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?xml version='1.0' encoding='utf-8'?>
2+
<model>
3+
<materials>
4+
<material id="1" name="core" depletable="true">
5+
<density value="16.0" units="g/cm3"/>
6+
<nuclide name="U235" ao="1.0"/>
7+
</material>
8+
</materials>
9+
<geometry>
10+
<cell id="1" material="1" region="-1" universe="1"/>
11+
<surface id="1" type="sphere" boundary="vacuum" coeffs="0.0 0.0 0.0 10.0"/>
12+
</geometry>
13+
<settings>
14+
<run_mode>eigenvalue</run_mode>
15+
<particles>1000</particles>
16+
<batches>20</batches>
17+
<inactive>5</inactive>
18+
<source type="independent" strength="1.0" particle="neutron">
19+
<space type="box">
20+
<parameters>-10.0 -10.0 -10.0 10.0 10.0 10.0</parameters>
21+
</space>
22+
<constraints>
23+
<fissionable>true</fissionable>
24+
</constraints>
25+
</source>
26+
<ifp_n_generation>5</ifp_n_generation>
27+
</settings>
28+
<tallies>
29+
<filter id="1" type="delayedgroup">
30+
<bins>1 2 3 4 5 6</bins>
31+
</filter>
32+
<tally id="1" name="IFP time numerator">
33+
<scores>ifp-time-numerator</scores>
34+
</tally>
35+
<tally id="2" name="IFP beta numerator">
36+
<filters>1</filters>
37+
<scores>ifp-beta-numerator</scores>
38+
</tally>
39+
<tally id="3" name="IFP denominator">
40+
<scores>ifp-denominator</scores>
41+
</tally>
42+
</tallies>
43+
</model>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
k-combined:
2+
1.006559E+00 5.389391E-03
3+
tally 1:
4+
9.109384E-08
5+
5.667165E-16
6+
tally 2:
7+
3.000000E-03
8+
9.000000E-06
9+
0.000000E+00
10+
0.000000E+00
11+
2.100000E-02
12+
1.370000E-04
13+
2.800000E-02
14+
2.220000E-04
15+
0.000000E+00
16+
0.000000E+00
17+
0.000000E+00
18+
0.000000E+00
19+
tally 3:
20+
1.489000E+01
21+
1.480036E+01
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Test the Iterated Fission Probability (IFP) method to compute adjoint-weighted
2+
kinetics parameters using dedicated tallies."""
3+
4+
import openmc
5+
import pytest
6+
7+
from tests.testing_harness import PyAPITestHarness
8+
9+
@pytest.fixture()
10+
def ifp_model():
11+
# Material
12+
material = openmc.Material(name="core")
13+
material.add_nuclide("U235", 1.0)
14+
material.set_density('g/cm3', 16.0)
15+
16+
# Geometry
17+
radius = 10.0
18+
sphere = openmc.Sphere(r=radius, boundary_type="vacuum")
19+
cell = openmc.Cell(region=-sphere, fill=material)
20+
geometry = openmc.Geometry([cell])
21+
22+
# Settings
23+
settings = openmc.Settings()
24+
settings.particles = 1000
25+
settings.batches = 20
26+
settings.inactive = 5
27+
settings.ifp_n_generation = 5
28+
29+
model = openmc.Model(settings=settings, geometry=geometry)
30+
31+
space = openmc.stats.Box(*cell.bounding_box)
32+
model.settings.source = openmc.IndependentSource(
33+
space=space, constraints={'fissionable': True})
34+
model.add_kinetics_parameters_tallies(num_groups=6)
35+
return model
36+
37+
38+
def test_iterated_fission_probability(ifp_model):
39+
harness = PyAPITestHarness("statepoint.20.h5", model=ifp_model)
40+
harness.main()

tests/regression_tests/ifp/total/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)