Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
41653f0
first commit to new branch
church89 Aug 23, 2023
1d3f0ca
batchwise first commits
church89 Sep 4, 2023
f561da1
update unit test
church89 Sep 5, 2023
652df2b
add test unit
church89 Sep 5, 2023
c08b762
unit test update
church89 Sep 6, 2023
7b27002
further improvements to the module
church89 Sep 12, 2023
0910bf2
add unit and regression tests
church89 Sep 12, 2023
2cd774e
Merge pull request #10 from openmc-dev/develop
church89 Sep 12, 2023
562baad
add missing docstrings
church89 Sep 13, 2023
7643aaf
remove unused import
church89 Sep 13, 2023
1777cee
add missing returns
church89 Sep 13, 2023
6eb6651
fix typo
church89 Sep 13, 2023
42b8e3d
Merge pull request #11 from openmc-dev/develop
church89 Sep 14, 2023
2d48f17
prevent to openmc batchwise_root when to there
church89 Sep 14, 2023
d56fb98
Apply suggestions from code review
church89 Feb 5, 2024
5e2023a
address comments by @drewejohnson
church89 Feb 5, 2024
4a2788e
fix some docstrings
church89 Feb 6, 2024
3110e45
reformatted with black
church89 Feb 6, 2024
a4f1f16
refactoring of _search_for_keff method
church89 Feb 6, 2024
0c804dd
fix some spelling typos
church89 Feb 6, 2024
38ed171
first commit to new branch
church89 Aug 23, 2023
cea8801
resolve conflicts
church89 Feb 7, 2024
7138d2d
update unit test
church89 Sep 5, 2023
872fa81
add test unit
church89 Sep 5, 2023
c16bece
unit test update
church89 Sep 6, 2023
643a75f
further improvements to the module
church89 Sep 12, 2023
94abad5
add unit and regression tests
church89 Sep 12, 2023
688631c
add missing docstrings
church89 Sep 13, 2023
325969f
remove unused import
church89 Sep 13, 2023
5d6e991
add missing returns
church89 Sep 13, 2023
85a1f35
fix typo
church89 Sep 13, 2023
d69a8a8
prevent to openmc batchwise_root when to there
church89 Sep 14, 2023
13ee2e3
Apply suggestions from code review
church89 Feb 5, 2024
1731fda
address comments by @drewejohnson
church89 Feb 5, 2024
e8c36f6
fix some docstrings
church89 Feb 6, 2024
e4c0ad2
reformatted with black
church89 Feb 6, 2024
f1213a4
refactoring of _search_for_keff method
church89 Feb 6, 2024
12a988d
fix some spelling typos
church89 Feb 6, 2024
ad8c7ed
fix conflicts
church89 Feb 7, 2024
ef4fe68
missing underscore
church89 Feb 7, 2024
1aad455
add batchwise get method and update unit test
church89 Feb 12, 2024
f3dfdf8
Apply suggestions from code review
church89 Mar 14, 2024
9d6c5be
address comments by @drewejohnson
church89 Mar 14, 2024
a4b44ed
address more comments by @drewejohnson
church89 Mar 15, 2024
0a2933c
replaced batchwise nomenclature with reactivity_control one
church89 Mar 15, 2024
0ccc75a
update regression reference result files
church89 Mar 15, 2024
da184bb
few fixes
church89 Mar 18, 2024
64543ff
fix unit test failing
church89 Mar 18, 2024
9328e5b
explicit path argument in SIIntegrator step result save method, since…
church89 Mar 19, 2024
a4731ab
add reactivity control restarting condition to integrator class
church89 Mar 19, 2024
49191d4
Merge branch 'develop' into batchwise_pr
church89 Mar 20, 2024
ee13d16
remove unused index argumnet in restart method to comply with latest …
church89 Mar 20, 2024
12a1e70
make sure cell attribute vector maintain initial coordinates
church89 Mar 21, 2024
f67b231
make sure bracket type is an array and apply black code style
church89 Mar 21, 2024
06366b5
fix failing regression test
church89 Mar 21, 2024
fbf91c4
update reference result files
church89 Mar 21, 2024
21e1ac6
change warning message
church89 Mar 26, 2024
6ad6ea0
restore check conditions in adaptive search_for_keff routine
church89 Apr 8, 2024
efc1f61
made ReactivityControllers settable through the integrator
church89 Apr 16, 2024
75ca1f2
Merge pull request #31 from openmsr/reactivity_control
church89 Apr 16, 2024
d2fa7a0
extend use of paramteric geometry to lattice and not only universe
church89 May 7, 2024
7583f78
implement suggested changes to reactivity control classes
church89 Jun 5, 2024
fa13a81
update tests
church89 Jun 6, 2024
c3a6705
Merge pull request #32 from openmsr/reactivity_control_abstrac
church89 Jun 6, 2024
e7c15cf
add forgotten argument to StepResult save method
church89 Jun 6, 2024
4f988cf
Merge remote-tracking branch 'openmsr/add_path_to_step_res' into batc…
church89 Jun 6, 2024
f576e87
Merge branch 'develop' into batchwise_pr
church89 Jun 6, 2024
3ed3071
Merge pull request #44 from openmc-dev/develop
church89 Mar 12, 2025
8d7ac63
Merge branch 'develop' of https://github.com/openmc-dev/openmc into o…
church89 Jan 13, 2026
47f7a12
Merge branch 'openmc-dev-develop' into batchwise_pr
church89 Jan 13, 2026
dc40d18
add lost docstring
church89 Jan 13, 2026
3e9733b
refactoring of reactivity control
church89 Jan 19, 2026
17f10c4
remove obsolete bits and update unit test
church89 Jan 19, 2026
5737c39
minor fix
church89 Jan 19, 2026
22b1fe2
minor fix
church89 Jan 19, 2026
1258e26
Merge pull request #54 from openmsr/batchwise_pr_keff_search_update
church89 Jan 19, 2026
4c512ad
addressing Paul's comments
church89 Feb 20, 2026
76b5ab4
add renamed files
church89 Feb 20, 2026
c2c0a6e
add updated result files
church89 Feb 20, 2026
e5188d2
add forgotten docstring
church89 Feb 20, 2026
e9ea747
fix update vec logic
church89 Feb 26, 2026
1fd1ad7
make keff_search control class private
church89 Feb 26, 2026
48dc319
update unit test
church89 Feb 26, 2026
fcb0d3d
update test results
church89 Feb 26, 2026
eb9dcac
update regression tests
church89 Feb 26, 2026
0a10fe3
fix bug
church89 Feb 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 146 additions & 1 deletion openmc/deplete/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from .pool import deplete
from .reaction_rates import ReactionRates
from .transfer_rates import TransferRates, ExternalSourceRates
from .keff_search_control import _KeffSearchControl


__all__ = [
Expand Down Expand Up @@ -617,6 +618,11 @@ class Integrator(ABC):
External source rates for the depletion system.

.. versionadded:: 0.15.3
keff_search_control : openmc.deplete._KeffSearchControl
Instance of _KeffSearchControl class to perform keff search during
transport-depletion simulation.

.. versionadded:: 0.15.4

""")

Expand Down Expand Up @@ -684,6 +690,7 @@ def __init__(

self.transfer_rates = None
self.external_source_rates = None
self._keff_search_control = None

if isinstance(solver, str):
# Delay importing of cram module, which requires this file
Expand Down Expand Up @@ -729,6 +736,15 @@ def solver(self, func):

self._solver = func

@property
def keff_search_control(self):
return self._keff_search_control

@keff_search_control.setter
def keff_search_control(self, keff_search_control):
check_type('keff search control', keff_search_control, _KeffSearchControl)
self._keff_search_control = keff_search_control

def _timed_deplete(self, n, rates, dt, i=None, matrix_func=None):
start = time.time()
results = deplete(
Expand Down Expand Up @@ -837,6 +853,13 @@ def _get_start_data(self) -> tuple[float, int]:
return (self.operator.prev_res[-1].time[0],
len(self.operator.prev_res) - 1)

def _get_bos_from_keff_search_control(self, step_index, bos_conc):
"""Get BOS from keff search control."""
x = deepcopy(bos_conc)
# Get new vector after keff criticality control
x, keff_search_root = self._keff_search_control.search_for_keff(x, step_index)
return x, keff_search_root

def integrate(
self,
final_step: bool = True,
Expand Down Expand Up @@ -877,10 +900,19 @@ def integrate(

# Solve transport equation (or obtain result from restart)
if i > 0 or self.operator.prev_res is None:
# Update geometry/material according to keff search control
if self._keff_search_control is not None and source_rate != 0.0:
n, keff_search_root = self._get_bos_from_keff_search_control(i, n)
else:
keff_search_root = None
n, res = self._get_bos_data_from_operator(i, source_rate, n)
else:
n, res = self._get_bos_data_from_restart(source_rate, n)

# Get keff search root from keff search control
if self._keff_search_control:
n, keff_search_root = self._get_bos_from_keff_search_control(i, n)
else:
keff_search_root = None
# Solve Bateman equations over time interval
proc_time, n_end = self(n, res.rates, dt, source_rate, i)

Expand All @@ -893,6 +925,7 @@ def integrate(
self._i_res + i,
proc_time,
write_rates=write_rates,
keff_search_root=keff_search_root,
path=path
)

Expand All @@ -906,6 +939,10 @@ def integrate(
# solve)
if output and final_step and comm.rank == 0:
print(f"[openmc.deplete] t={t} (final operator evaluation)")
if self._keff_search_control is not None and source_rate != 0.0:
n, keff_search_root = self._get_bos_from_keff_search_control(i+1, n)
else:
keff_search_root = None
res_final = self.operator(n, source_rate if final_step else 0.0)
StepResult.save(
self.operator,
Expand All @@ -916,6 +953,7 @@ def integrate(
self._i_res + len(self),
proc_time,
write_rates=write_rates,
keff_search_root=keff_search_root,
path=path
)
self.operator.write_bos_data(len(self) + self._i_res)
Expand Down Expand Up @@ -1048,6 +1086,113 @@ def add_redox(self, material, buffer, oxidation_states, timesteps=None):

self.transfer_rates.set_redox(material, buffer, oxidation_states, timesteps)

def add_keff_search_control(
self,
function: Callable,
x0: float,
x1: float,
bracket: list[float],
**search_kwargs
):
"""Add keff search to the integrator scheme.

This method creates a :class:`openmc.deplete._KeffSearchControl` that
performs keff searches during depletion to maintain a target keff
by adjusting a model parameter through the provided function.

.. important::
The function **must** modify the model through ``openmc.lib`` (e.g.,
``openmc.lib.cells``, ``openmc.lib.materials``) and **NOT** through
``openmc.model``. The function is called within a
:class:`openmc.lib.TemporarySession` context where only the C API
(``openmc.lib``) is available for modifications.

Parameters
----------
function : Callable
Function that modifies the model through ``openmc.lib`` based on a
parameter value. The function should take a single float parameter
and modify ``openmc.lib`` objects accordingly (e.g., adjust control
rod position via ``openmc.lib.cells[...].translation``, material
density via ``openmc.lib.materials[...].set_densities(...)``, etc.).

**Important**: The function must modify ``openmc.lib`` objects, not
``openmc.model`` objects.
x0: float
Initial lower bound for the keff search.
x1: float
Initial upper bound for the keff search.
bracket : list[float]
Bracket interval [x_min, x_max] that constrains the allowed parameter
values during the keff search. This is a required parameter
that defines the absolute bounds for the search. The bracket must contain
exactly 2 elements with bracket[0] < bracket[1]. These values are passed
directly to the ``x_min`` and ``x_max`` optional arguments in
:meth:`openmc.Model.keff_search`, which enforce hard limits on the
parameter range. If the keff search converges to a value outside this
bracket, it will be clamped to the nearest bracket bound with a warning.
**search_kwargs
Additional keyword arguments passed to
:meth:`openmc.Model.keff_search`. Common options include:

* ``target`` : float, optional
Target keff value to search for. Defaults to 1.0.
* ``k_tol`` : float, optional
Stopping criterion on the function value. Defaults to 1e-4.
* ``sigma_final`` : float, optional
Maximum accepted k-effective uncertainty. Defaults to 3e-4.
* ``maxiter`` : int, optional
Maximum number of iterations. Defaults to 50.

See :meth:`openmc.Model.keff_search` for a complete list of
available options.

Examples
--------
Add keff search that adjusts a control rod position:

>>> def adjust_rod_position(position):
... openmc.lib.cells[rod_cell.id].translation = [0, 0, position]
>>> integrator.add_keff_search_control(
... adjust_rod_position,
... x0=0.0,
... x1=5.0,
... bracket=[-10,10],
... target=1.0,
... k_tol=1e-4
... )

Add keff search that adjusts material density:

>>> def adjust_material_density(density_factor):
... # Get the material from openmc.lib
... lib_mat = openmc.lib.materials[material_id]
... # Get current nuclides and densities
... nuclides = lib_mat.nuclides
... current_densities = lib_mat.densities
... # Scale all densities by the factor
... new_densities = densities * density_factor
... # Update the material densities
... lib_mat.set_densities(nuclides, new_densities)
>>> integrator.add_keff_search_control(
... adjust_material_density,
... x0=0.8,
... x1=1.2,
... bracket=[0.2, 1.5],
... target=1.0
... )

.. versionadded:: 0.15.4

"""
self._keff_search_control = _KeffSearchControl(
self.operator,
function,
x0,
x1,
bracket,
**search_kwargs)

@add_params
class SIIntegrator(Integrator):
r"""Abstract class for the Stochastic Implicit Euler integrators
Expand Down
146 changes: 146 additions & 0 deletions openmc/deplete/keff_search_control.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import openmc.lib
from openmc.mpi import comm
from typing import Callable
from warnings import warn

class _KeffSearchControl:
"""Controller for keff search during depletion calculations.

This class performs keff searches to maintain a target keff by adjusting
a model parameter through a provided function.

Parameters
----------
operator : openmc.deplete.Operator
Depletion operator instance
function : Callable
Function that modifies the model based on a parameter value
x0 : float
Initial lower bound for the keff search
x1 : float
Initial upper bound for the keff search
bracket : list[float]
Absolute bracketing interval lower and upper
if keff search solution lies off these limits the closest
limit will be set as new result.
search_kwargs : dict, optional
Additional keyword arguments to pass to `model.keff_search`
"""
def __init__(self, operator, function: Callable, x0: float, x1: float, bracket: list[float], **search_kwargs):
if len(bracket) != 2:
raise ValueError(f"bracket must have exactly 2 elements, got {len(bracket)}")
if bracket[0] >= bracket[1]:
raise ValueError(f"bracket[0] must be < bracket[1], got {bracket}")
self.x0 = x0
self.x1 = x1
self.operator = operator
self.function = function
self.search_kwargs = search_kwargs
self.search_kwargs['x_min'] = bracket[0]
self.search_kwargs['x_max'] = bracket[1]

def search_for_keff(self, x, step_index):
"""Perform keff search and update the atom density vector.

Parameters
----------
x : list of numpy.ndarray
Current atom density vector (atoms per material)
step_index : int
Current depletion step index

Returns
-------
x : list of numpy.ndarray
Updated atom density vector
root : float
Parameter value that achieves target keff
"""
root = self._search_for_keff()
x = self._update_vec(x)
return x, root

def _search_for_keff(self) -> float:
"""Perform the keff search using the model's keff_search method.

Returns
-------
float
Parameter value that achieves target keff

Raises
------
ValueError
If the keff search fails to converge
"""
with openmc.lib.TemporarySession(model=self.operator.model) as session:
# Only pass the first 3 required args plus explicitly provided kwargs
result = self.operator.model.keff_search(
self.function,
self.x0,
self.x1,
**self.search_kwargs
)
if not result.converged:
raise ValueError(
f"Search for keff failed to converge. "
f"Termination reason: {result.flag}"
)

root = result.root

# Check if root is outside the bracket bounds and give a warning
if root < self.search_kwargs['x_min']:
warn(
f"keff search result ({root:.6f}) is below the lower bracket bound "
f"({self.search_kwargs['x_min']:.6f}). Clamping to bracket lower bound.",
UserWarning
)

elif root > self.search_kwargs['x_max']:
warn(
f"keff search result ({root:.6f}) is above the upper bracket bound "
f"({self.search_kwargs['x_max']:.6f}). Clamping to bracket upper bound.",
UserWarning
)

#restore the number of initial batches
openmc.lib.settings.set_batches(self.operator.model.settings.batches)

return root

def _update_vec(self, x):
"""Update the atom density vector from openmc.lib.materials and AtomNumber object.

This method synchronizes the atom densities across all MPI ranks by
broadcasting the number object from each rank and updating the x vector
with the current atom densities from openmc.lib.materials or AtomNumber object
if the nuclide is not in openmc.lib.materials.

Parameters
----------
x : list of numpy.ndarray
Atom density vector to update (atoms per material)

Returns
-------
list of numpy.ndarray
Updated atom density vector synchronized across all ranks
"""
for rank in range(comm.size):
number_i = comm.bcast(self.operator.number, root=rank)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the density change is being made through openmc.lib, wouldn't this end up resetting the densities to what they were before since operator.number doesn't get updated?


for mat_idx, mat in enumerate(number_i.materials):
for nuc in number_i.nuclides:
if nuc in number_i.burnable_nuclides:
nuc_idx = number_i.burnable_nuclides.index(nuc)
volume = number_i.get_mat_volume(mat) # cm^3
if nuc in openmc.lib.materials[int(mat)].nuclides:
_nuc_idx = openmc.lib.materials[int(mat)].nuclides.index(nuc)
val = 1.0e24 * openmc.lib.materials[int(mat)].densities[_nuc_idx] # atom/cm^3
else:
val = number_i.get_atom_density(mat, nuc) # atom/cm^3
x[mat_idx][nuc_idx] = val * volume

x = comm.bcast(x, root=rank)
return x
Loading
Loading