Skip to content

Commit 058e995

Browse files
authored
Merge pull request #55 from paulromano/keff-search-fixes
Fixes for PR 2693
2 parents 0a10fe3 + 84e7422 commit 058e995

5 files changed

Lines changed: 163 additions & 197 deletions

File tree

openmc/deplete/abc.py

Lines changed: 56 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -618,11 +618,6 @@ class Integrator(ABC):
618618
External source rates for the depletion system.
619619
620620
.. versionadded:: 0.15.3
621-
keff_search_control : openmc.deplete._KeffSearchControl
622-
Instance of _KeffSearchControl class to perform keff search during
623-
transport-depletion simulation.
624-
625-
.. versionadded:: 0.15.4
626621
627622
""")
628623

@@ -736,15 +731,6 @@ def solver(self, func):
736731

737732
self._solver = func
738733

739-
@property
740-
def keff_search_control(self):
741-
return self._keff_search_control
742-
743-
@keff_search_control.setter
744-
def keff_search_control(self, keff_search_control):
745-
check_type('keff search control', keff_search_control, _KeffSearchControl)
746-
self._keff_search_control = keff_search_control
747-
748734
def _timed_deplete(self, n, rates, dt, i=None, matrix_func=None):
749735
start = time.time()
750736
results = deplete(
@@ -853,12 +839,36 @@ def _get_start_data(self) -> tuple[float, int]:
853839
return (self.operator.prev_res[-1].time[0],
854840
len(self.operator.prev_res) - 1)
855841

856-
def _get_bos_from_keff_search_control(self, step_index, bos_conc):
857-
"""Get BOS from keff search control."""
858-
x = deepcopy(bos_conc)
859-
# Get new vector after keff criticality control
860-
x, keff_search_root = self._keff_search_control.search_for_keff(x, step_index)
861-
return x, keff_search_root
842+
def _restore_keff_search_control(self, res):
843+
"""Restore keff search control from restart results."""
844+
keff_search_root = res.keff_search_root
845+
if keff_search_root is None:
846+
raise ValueError(
847+
"Cannot restore keff search control from restart "
848+
"results because no stored keff_search_root is "
849+
"available."
850+
)
851+
self._keff_search_control.function(keff_search_root)
852+
return keff_search_root
853+
854+
def _get_bos_data(self, step_index, source_rate, bos_conc):
855+
"""Get beginning-of-step concentrations, rates, and control state."""
856+
if step_index > 0 or self.operator.prev_res is None:
857+
if self._keff_search_control is not None and source_rate != 0.0:
858+
keff_search_root = self._keff_search_control.run(bos_conc)
859+
else:
860+
keff_search_root = None
861+
bos_conc, res = self._get_bos_data_from_operator(
862+
step_index, source_rate, bos_conc)
863+
else:
864+
bos_conc, res = self._get_bos_data_from_restart(
865+
source_rate, bos_conc)
866+
if self._keff_search_control is not None and source_rate != 0.0:
867+
keff_search_root = self._restore_keff_search_control(res)
868+
else:
869+
keff_search_root = None
870+
871+
return bos_conc, res, keff_search_root
862872

863873
def integrate(
864874
self,
@@ -898,21 +908,9 @@ def integrate(
898908
if output and comm.rank == 0:
899909
print(f"[openmc.deplete] t={t} s, dt={dt} s, source={source_rate}")
900910

901-
# Solve transport equation (or obtain result from restart)
902-
if i > 0 or self.operator.prev_res is None:
903-
# Update geometry/material according to keff search control
904-
if self._keff_search_control is not None and source_rate != 0.0:
905-
n, keff_search_root = self._get_bos_from_keff_search_control(i, n)
906-
else:
907-
keff_search_root = None
908-
n, res = self._get_bos_data_from_operator(i, source_rate, n)
909-
else:
910-
n, res = self._get_bos_data_from_restart(source_rate, n)
911-
# Get keff search root from keff search control
912-
if self._keff_search_control:
913-
n, keff_search_root = self._get_bos_from_keff_search_control(i, n)
914-
else:
915-
keff_search_root = None
911+
# Get beginning-of-step data from operator or restart results
912+
n, res, keff_search_root = self._get_bos_data(i, source_rate, n)
913+
916914
# Solve Bateman equations over time interval
917915
proc_time, n_end = self(n, res.rates, dt, source_rate, i)
918916

@@ -940,7 +938,7 @@ def integrate(
940938
if output and final_step and comm.rank == 0:
941939
print(f"[openmc.deplete] t={t} (final operator evaluation)")
942940
if self._keff_search_control is not None and source_rate != 0.0:
943-
n, keff_search_root = self._get_bos_from_keff_search_control(i+1, n)
941+
keff_search_root = self._keff_search_control.run(n)
944942
else:
945943
keff_search_root = None
946944
res_final = self.operator(n, source_rate if final_step else 0.0)
@@ -1091,40 +1089,34 @@ def add_keff_search_control(
10911089
function: Callable,
10921090
x0: float,
10931091
x1: float,
1094-
bracket: list[float],
1092+
bracket: Sequence[float],
10951093
**search_kwargs
10961094
):
10971095
"""Add keff search to the integrator scheme.
10981096
1099-
This method creates a :class:`openmc.deplete._KeffSearchControl` that
1100-
performs keff searches during depletion to maintain a target keff
1101-
by adjusting a model parameter through the provided function.
1097+
This method causes OpenMC to perform a keff search during depletion to
1098+
maintain a target keff by adjusting a model parameter through the
1099+
provided function.
11021100
11031101
.. important::
11041102
The function **must** modify the model through ``openmc.lib`` (e.g.,
11051103
``openmc.lib.cells``, ``openmc.lib.materials``) and **NOT** through
1106-
``openmc.model``. The function is called within a
1104+
``openmc.Model``. The function is called within a
11071105
:class:`openmc.lib.TemporarySession` context where only the C API
11081106
(``openmc.lib``) is available for modifications.
11091107
11101108
Parameters
11111109
----------
11121110
function : Callable
1113-
Function that modifies the model through ``openmc.lib`` based on a
1114-
parameter value. The function should take a single float parameter
1115-
and modify ``openmc.lib`` objects accordingly (e.g., adjust control
1116-
rod position via ``openmc.lib.cells[...].translation``, material
1117-
density via ``openmc.lib.materials[...].set_densities(...)``, etc.).
1118-
1119-
**Important**: The function must modify ``openmc.lib`` objects, not
1120-
``openmc.model`` objects.
1121-
x0: float
1111+
Function that takes a single float argument and modifies the model
1112+
through :mod:`openmc.lib`.
1113+
x0 : float
11221114
Initial lower bound for the keff search.
1123-
x1: float
1115+
x1 : float
11241116
Initial upper bound for the keff search.
1125-
bracket : list[float]
1117+
bracket : sequence of float
11261118
Bracket interval [x_min, x_max] that constrains the allowed parameter
1127-
values during the keff search. This is a required parameter
1119+
values during the keff search. This is a required parameter
11281120
that defines the absolute bounds for the search. The bracket must contain
11291121
exactly 2 elements with bracket[0] < bracket[1]. These values are passed
11301122
directly to the ``x_min`` and ``x_max`` optional arguments in
@@ -1162,36 +1154,30 @@ def add_keff_search_control(
11621154
... k_tol=1e-4
11631155
... )
11641156
1165-
Add keff search that adjusts material density:
1157+
Add keff search that adjusts the U235 density:
11661158
1167-
>>> def adjust_material_density(density_factor):
1159+
>>> def set_u235_density(u235_density):
11681160
... # Get the material from openmc.lib
11691161
... lib_mat = openmc.lib.materials[material_id]
11701162
... # Get current nuclides and densities
11711163
... nuclides = lib_mat.nuclides
1172-
... current_densities = lib_mat.densities
1173-
... # Scale all densities by the factor
1174-
... new_densities = densities * density_factor
1175-
... # Update the material densities
1176-
... lib_mat.set_densities(nuclides, new_densities)
1164+
... densities = lib_mat.densities
1165+
... u235_idx = nuclides.index('U235')
1166+
... densities[u235_idx] = u235_density
1167+
... lib_mat.set_densities(nuclides, densities)
11771168
>>> integrator.add_keff_search_control(
1178-
... adjust_material_density,
1179-
... x0=0.8,
1180-
... x1=1.2,
1181-
... bracket=[0.2, 1.5],
1169+
... set_u235_density,
1170+
... x0=5.0e-4,
1171+
... x1=1.0e-3,
1172+
... bracket=[1.0e-4, 2.0e-3],
11821173
... target=1.0
11831174
... )
11841175
11851176
.. versionadded:: 0.15.4
11861177
11871178
"""
11881179
self._keff_search_control = _KeffSearchControl(
1189-
self.operator,
1190-
function,
1191-
x0,
1192-
x1,
1193-
bracket,
1194-
**search_kwargs)
1180+
self.operator, function, x0, x1, bracket, **search_kwargs)
11951181

11961182
@add_params
11971183
class SIIntegrator(Integrator):
Lines changed: 53 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
import openmc.lib
2-
from openmc.mpi import comm
31
from typing import Callable
42
from warnings import warn
53

4+
import openmc.lib
5+
6+
67
class _KeffSearchControl:
78
"""Controller for keff search during depletion calculations.
8-
9-
This class performs keff searches to maintain a target keff by adjusting
10-
a model parameter through a provided function.
11-
9+
10+
This class performs keff searches to maintain a target keff by adjusting a
11+
model parameter through a provided function.
12+
1213
Parameters
1314
----------
1415
operator : openmc.deplete.Operator
@@ -20,11 +21,12 @@ class _KeffSearchControl:
2021
x1 : float
2122
Initial upper bound for the keff search
2223
bracket : list[float]
23-
Absolute bracketing interval lower and upper
24-
if keff search solution lies off these limits the closest
25-
limit will be set as new result.
26-
search_kwargs : dict, optional
27-
Additional keyword arguments to pass to `model.keff_search`
24+
Absolute bracketing interval lower and upper. If the keff search
25+
solution lies off these limits the closest limit will be set as new
26+
result.
27+
**search_kwargs : dict, optional
28+
Additional keyword arguments to pass to :meth:`openmc.Model.keff_search`
29+
2830
"""
2931
def __init__(self, operator, function: Callable, x0: float, x1: float, bracket: list[float], **search_kwargs):
3032
if len(bracket) != 2:
@@ -39,108 +41,88 @@ def __init__(self, operator, function: Callable, x0: float, x1: float, bracket:
3941
self.search_kwargs['x_min'] = bracket[0]
4042
self.search_kwargs['x_max'] = bracket[1]
4143

42-
def search_for_keff(self, x, step_index):
44+
def run(self, x):
4345
"""Perform keff search and update the atom density vector.
44-
46+
4547
Parameters
4648
----------
4749
x : list of numpy.ndarray
4850
Current atom density vector (atoms per material)
49-
step_index : int
50-
Current depletion step index
51-
51+
5252
Returns
5353
-------
54-
x : list of numpy.ndarray
55-
Updated atom density vector
5654
root : float
5755
Parameter value that achieves target keff
5856
"""
5957
root = self._search_for_keff()
60-
x = self._update_vec(x)
61-
return x, root
62-
58+
self._update_vec(x)
59+
return root
60+
6361
def _search_for_keff(self) -> float:
6462
"""Perform the keff search using the model's keff_search method.
65-
63+
6664
Returns
6765
-------
6866
float
6967
Parameter value that achieves target keff
70-
68+
7169
Raises
7270
------
7371
ValueError
7472
If the keff search fails to converge
7573
"""
76-
with openmc.lib.TemporarySession(model=self.operator.model) as session:
74+
with openmc.lib.TemporarySession(self.operator.model):
7775
# Only pass the first 3 required args plus explicitly provided kwargs
7876
result = self.operator.model.keff_search(
79-
self.function,
80-
self.x0,
81-
self.x1,
82-
**self.search_kwargs
77+
self.function, self.x0, self.x1, **self.search_kwargs
8378
)
8479
if not result.converged:
8580
raise ValueError(
8681
f"Search for keff failed to converge. "
8782
f"Termination reason: {result.flag}"
8883
)
89-
84+
9085
root = result.root
91-
86+
9287
# Check if root is outside the bracket bounds and give a warning
9388
if root < self.search_kwargs['x_min']:
94-
warn(
95-
f"keff search result ({root:.6f}) is below the lower bracket bound "
96-
f"({self.search_kwargs['x_min']:.6f}). Clamping to bracket lower bound.",
97-
UserWarning
98-
)
99-
89+
warn(f"keff search result ({root:.6f}) is below the lower bracket "
90+
f"bound ({self.search_kwargs['x_min']:.6f}).", UserWarning)
10091
elif root > self.search_kwargs['x_max']:
101-
warn(
102-
f"keff search result ({root:.6f}) is above the upper bracket bound "
103-
f"({self.search_kwargs['x_max']:.6f}). Clamping to bracket upper bound.",
104-
UserWarning
105-
)
106-
107-
#restore the number of initial batches
92+
warn(f"keff search result ({root:.6f}) is above the upper bracket "
93+
f"bound ({self.search_kwargs['x_max']:.6f}).", UserWarning)
94+
95+
# Restore the number of initial batches
10896
openmc.lib.settings.set_batches(self.operator.model.settings.batches)
10997

11098
return root
11199

112100
def _update_vec(self, x):
113101
"""Update the atom density vector from openmc.lib.materials and AtomNumber object.
114-
115-
This method synchronizes the atom densities across all MPI ranks by
116-
broadcasting the number object from each rank and updating the x vector
117-
with the current atom densities from openmc.lib.materials or AtomNumber object
118-
if the nuclide is not in openmc.lib.materials.
119-
102+
103+
The depletion vector ``x`` is rank-local, matching the materials owned
104+
by ``self.operator.number`` on the current MPI rank. We therefore only
105+
update entries for locally owned materials using the compositions
106+
currently stored in ``openmc.lib.materials``.
107+
120108
Parameters
121109
----------
122110
x : list of numpy.ndarray
123111
Atom density vector to update (atoms per material)
124-
125-
Returns
126-
-------
127-
list of numpy.ndarray
128-
Updated atom density vector synchronized across all ranks
112+
129113
"""
130-
for rank in range(comm.size):
131-
number_i = comm.bcast(self.operator.number, root=rank)
132-
133-
for mat_idx, mat in enumerate(number_i.materials):
134-
for nuc in number_i.nuclides:
135-
if nuc in number_i.burnable_nuclides:
136-
nuc_idx = number_i.burnable_nuclides.index(nuc)
137-
volume = number_i.get_mat_volume(mat) # cm^3
138-
if nuc in openmc.lib.materials[int(mat)].nuclides:
139-
_nuc_idx = openmc.lib.materials[int(mat)].nuclides.index(nuc)
140-
val = 1.0e24 * openmc.lib.materials[int(mat)].densities[_nuc_idx] # atom/cm^3
141-
else:
142-
val = number_i.get_atom_density(mat, nuc) # atom/cm^3
143-
x[mat_idx][nuc_idx] = val * volume
144-
145-
x = comm.bcast(x, root=rank)
146-
return x
114+
number = self.operator.number
115+
116+
for mat_idx, mat in enumerate(number.materials):
117+
lib_material = openmc.lib.materials[int(mat)]
118+
nuclides = lib_material.nuclides
119+
densities = 1e24 * lib_material.densities
120+
volume = number.get_mat_volume(mat)
121+
122+
for nuc_idx, nuc in enumerate(number.burnable_nuclides):
123+
if nuc in nuclides:
124+
lib_nuc_idx = nuclides.index(nuc)
125+
atom_density = densities[lib_nuc_idx]
126+
else:
127+
atom_density = number.get_atom_density(mat, nuc)
128+
x[mat_idx][nuc_idx] = atom_density * volume

0 commit comments

Comments
 (0)