From 9b26b5357678848673b5a5b7f8b6beaaccce0604 Mon Sep 17 00:00:00 2001 From: MohamedElkamash Date: Sun, 15 Feb 2026 12:21:25 -0600 Subject: [PATCH 01/21] preliminary implementation --- include/openmc/settings.h | 5 + openmc/settings.py | 52 ++++--- src/finalize.cpp | 1 - src/particle.cpp | 135 ++++++++++-------- src/settings.cpp | 26 +++- .../surface_source_write/_visualize.py | 6 +- .../case-22/inputs_true.dat | 42 ++++++ .../case-22/results_true.dat | 0 .../case-23/inputs_true.dat | 42 ++++++ .../case-23/results_true.dat | 0 .../case-24/inputs_true.dat | 42 ++++++ .../case-24/results_true.dat | 0 .../surface_source_write/test.py | 119 +++++++++++---- tests/unit_tests/test_surface_source_write.py | 30 ++-- 14 files changed, 370 insertions(+), 130 deletions(-) create mode 100644 tests/regression_tests/surface_source_write/case-22/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-22/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-23/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-23/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-24/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-24/results_true.dat diff --git a/include/openmc/settings.h b/include/openmc/settings.h index 19ef6e5d2aa..964c713432b 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -183,6 +183,11 @@ extern SSWCellType ssw_cell_type; //!< Type of option for the cell //!< argument of surface source write extern double surface_grazing_cutoff; //!< surface flux cosine cutoff extern double surface_grazing_ratio; //!< surface flux substitution ratio + +extern vector ssw_cell_ids; //!< Cell ids for the surface source + //!< write setting +extern SSWCellType ssw_cell_type; //!< Type of option for the cell + //!< argument of surface source write extern TemperatureMethod temperature_method; //!< method for choosing temperatures extern double diff --git a/openmc/settings.py b/openmc/settings.py index 342fd5e53ae..6c204efa155 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -280,13 +280,13 @@ class Settings: process (int) :max_source_files: Maximum number of surface source files to be created (int) :mcpl: Output in the form of an MCPL-file (bool) - :cell: Cell ID used to determine if particles crossing identified + :cell: List of cell IDs used to determine if particles crossing identified surfaces are to be banked. Particles coming from or going to this declared cell will be banked (int) - :cellfrom: Cell ID used to determine if particles crossing identified + :cellfrom: List of cell IDs used to determine if particles crossing identified surfaces are to be banked. Particles coming from this declared cell will be banked (int) - :cellto: Cell ID used to determine if particles crossing identified + :cellto: List of cell IDs used to determine if particles crossing identified surfaces are to be banked. Particles going to this declared cell will be banked (int) surface_grazing_cutoff : float @@ -881,23 +881,24 @@ def surf_source_write(self, surf_source_write: dict): ("surface_ids", "max_particles", "max_source_files", "mcpl", "cell", "cellfrom", "cellto"), ) - if key == "surface_ids": - cv.check_type( - "surface ids for source banking", value, Iterable, Integral - ) - for surf_id in value: - cv.check_greater_than( - "surface id for source banking", surf_id, 0) + if key in ("surface_ids", "cell", "cellfrom", "cellto"): + name = { + "surface_ids": "surface id(s) for source banking", + "cell": "Cell ID(s) for source banking (from or to)", + "cellfrom": "Cell ID(s) for source banking (from only)", + "cellto": "Cell ID(s) for source banking (to only)", + }[key] + + cv.check_type(name, value, Iterable, Integral) + for x in value: + cv.check_greater_than(name, x, 0) elif key == "mcpl": cv.check_type("write to an MCPL-format file", value, bool) - elif key in ("max_particles", "max_source_files", "cell", "cellfrom", "cellto"): + elif key in ("max_particles", "max_source_files"): name = { "max_particles": "maximum particle banks on surfaces per process", "max_source_files": "maximun surface source files to be written", - "cell": "Cell ID for source banking (from or to)", - "cellfrom": "Cell ID for source banking (from only)", - "cellto": "Cell ID for source banking (to only)", }[key] cv.check_type(name, value, Integral) cv.check_greater_than(name, value, 0) @@ -1569,19 +1570,30 @@ def _create_surf_source_read_subelement(self, root): def _create_surf_source_write_subelement(self, root): if self._surf_source_write: element = ET.SubElement(root, "surf_source_write") + if "surface_ids" in self._surf_source_write: subelement = ET.SubElement(element, "surface_ids") subelement.text = " ".join( str(x) for x in self._surf_source_write["surface_ids"] ) - if "mcpl" in self._surf_source_write: - subelement = ET.SubElement(element, "mcpl") - subelement.text = str(self._surf_source_write["mcpl"]).lower() - for key in ("max_particles", "max_source_files", "cell", "cellfrom", "cellto"): + + for key in ("max_particles", "max_source_files"): if key in self._surf_source_write: subelement = ET.SubElement(element, key) subelement.text = str(self._surf_source_write[key]) + for key in ("cell", "cellfrom", "cellto"): + if key in self._surf_source_write: + subelement = ET.SubElement(element, key) + subelement.text = " ".join( + str(x) for x in self._surf_source_write[key] + ) + + if "mcpl" in self._surf_source_write: + subelement = ET.SubElement(element, "mcpl") + subelement.text = str(self._surf_source_write["mcpl"]).lower() + + def _create_collision_track_subelement(self, root): if self._collision_track: element = ET.SubElement(root, "collision_track") @@ -2089,14 +2101,14 @@ def _surf_source_write_from_xml_element(self, root): if elem is None: return for key in ('surface_ids', 'max_particles', 'max_source_files', 'mcpl', 'cell', 'cellto', 'cellfrom'): - if key == 'surface_ids': + if key in ('surface_ids', 'cell', 'cellto', 'cellfrom'): value = get_elem_list(elem, key, int) else: value = get_text(elem, key) if value is not None: if key == 'mcpl': value = value in ('true', '1') - elif key in ('max_particles', 'max_source_files', 'cell', 'cellfrom', 'cellto'): + elif key in ('max_particles', 'max_source_files'): value = int(value) self.surf_source_write[key] = value diff --git a/src/finalize.cpp b/src/finalize.cpp index e82e00b4cca..9f4f8343d47 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -130,7 +130,6 @@ int openmc_finalize() settings::source_rejection_fraction = 0.05; settings::source_separate = false; settings::source_write = true; - settings::ssw_cell_id = C_NONE; settings::ssw_cell_type = SSWCellType::None; settings::ssw_max_particles = 0; settings::ssw_max_files = 1; diff --git a/src/particle.cpp b/src/particle.cpp index 0a063559824..a2e1b5c30fb 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -895,83 +895,98 @@ void add_surf_source_to_bank(Particle& p, const Surface& surf) return; } + bool add_site = true; // If a cell/cellfrom/cellto parameter is defined - if (settings::ssw_cell_id != C_NONE) { - - // Retrieve cell index and storage type - int cell_idx = model::cell_map[settings::ssw_cell_id]; + if (!settings::ssw_cell_ids.empty()) { + for (auto& b : settings::ssw_cell_ids) { + add_site = true; + // Retrieve cell index and storage type + int cell_idx = model::cell_map[b]; + + if (surf.bc_) { + // Leave if cellto with vacuum boundary condition + if (surf.bc_->type() == "vacuum" && + settings::ssw_cell_type == SSWCellType::To) { + add_site = false; + continue; + } - if (surf.bc_) { - // Leave if cellto with vacuum boundary condition - if (surf.bc_->type() == "vacuum" && - settings::ssw_cell_type == SSWCellType::To) { - return; + // Leave if other boundary condition than vacuum + if (surf.bc_->type() != "vacuum") { + add_site = false; + continue; + } } - // Leave if other boundary condition than vacuum - if (surf.bc_->type() != "vacuum") { - return; + // Check if the cell of interest has been exited + bool exited = false; + for (int i = 0; i < p.n_coord_last(); ++i) { + if (p.cell_last(i) == cell_idx) { + exited = true; + } } - } - // Check if the cell of interest has been exited - bool exited = false; - for (int i = 0; i < p.n_coord_last(); ++i) { - if (p.cell_last(i) == cell_idx) { - exited = true; + // Check if the cell of interest has been entered + bool entered = false; + for (int i = 0; i < p.n_coord(); ++i) { + if (p.coord(i).cell() == cell_idx) { + entered = true; + } } - } - // Check if the cell of interest has been entered - bool entered = false; - for (int i = 0; i < p.n_coord(); ++i) { - if (p.coord(i).cell() == cell_idx) { - entered = true; - } - } + // Vacuum boundary conditions: return if cell is not exited + if (surf.bc_) { + if (surf.bc_->type() == "vacuum" && !exited) { + add_site = false; + continue; + } + } else { - // Vacuum boundary conditions: return if cell is not exited - if (surf.bc_) { - if (surf.bc_->type() == "vacuum" && !exited) { - return; - } - } else { + // If we both enter and exit the cell of interest + if (entered && exited) { + add_site = false; + continue; + } - // If we both enter and exit the cell of interest - if (entered && exited) { - return; - } + // If we did not enter nor exit the cell of interest + if (!entered && !exited) { + add_site = false; + continue; + } - // If we did not enter nor exit the cell of interest - if (!entered && !exited) { - return; - } + // If cellfrom and the cell before crossing is not the cell of + // interest + if (settings::ssw_cell_type == SSWCellType::From && !exited) { + add_site = false; + continue; + } - // If cellfrom and the cell before crossing is not the cell of - // interest - if (settings::ssw_cell_type == SSWCellType::From && !exited) { - return; + // If cellto and the cell after crossing is not the cell of interest + if (settings::ssw_cell_type == SSWCellType::To && !entered) { + add_site = false; + continue; + } } - - // If cellto and the cell after crossing is not the cell of interest - if (settings::ssw_cell_type == SSWCellType::To && !entered) { - return; + if (add_site) { + break; } } } - SourceSite site; - site.r = p.r(); - site.u = p.u(); - site.E = p.E(); - site.time = p.time(); - site.wgt = p.wgt(); - site.delayed_group = p.delayed_group(); - site.surf_id = surf.id_; - site.particle = p.type(); - site.parent_id = p.id(); - site.progeny_id = p.n_progeny(); - int64_t idx = simulation::surf_source_bank.thread_safe_append(site); + if (add_site) { + SourceSite site; + site.r = p.r(); + site.u = p.u(); + site.E = p.E(); + site.time = p.time(); + site.wgt = p.wgt(); + site.delayed_group = p.delayed_group(); + site.surf_id = surf.id_; + site.particle = p.type(); + site.parent_id = p.id(); + site.progeny_id = p.n_progeny(); + int64_t idx = simulation::surf_source_bank.thread_safe_append(site); + } } } // namespace openmc diff --git a/src/settings.cpp b/src/settings.cpp index 6b159f6e901..7eb25920eb5 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -134,7 +134,7 @@ std::unordered_set source_write_surf_id; CollisionTrackConfig collision_track_config {}; int64_t ssw_max_particles; int64_t ssw_max_files; -int64_t ssw_cell_id {C_NONE}; +vector ssw_cell_ids; SSWCellType ssw_cell_type {SSWCellType::None}; double surface_grazing_cutoff {0.001}; double surface_grazing_ratio {0.5}; @@ -936,23 +936,36 @@ void read_settings_xml(pugi::xml_node root) } // Get cell information if (check_for_node(node_ssw, "cell")) { - ssw_cell_id = std::stoll(get_node_value(node_ssw, "cell")); + if (!ssw_cell_ids.empty()) { + fatal_error( + "'cell', 'cellfrom' and 'cellto' cannot be used at the same time."); + } + auto temp = get_node_array(node_ssw, "cell"); + for (const auto& cell_id : temp) { + ssw_cell_ids.push_back(cell_id); + } ssw_cell_type = SSWCellType::Both; } if (check_for_node(node_ssw, "cellfrom")) { - if (ssw_cell_id != C_NONE) { + if (!ssw_cell_ids.empty()) { fatal_error( "'cell', 'cellfrom' and 'cellto' cannot be used at the same time."); } - ssw_cell_id = std::stoll(get_node_value(node_ssw, "cellfrom")); + auto temp = get_node_array(node_ssw, "cellfrom"); + for (const auto& cell_id : temp) { + ssw_cell_ids.push_back(cell_id); + } ssw_cell_type = SSWCellType::From; } if (check_for_node(node_ssw, "cellto")) { - if (ssw_cell_id != C_NONE) { + if (!ssw_cell_ids.empty()) { fatal_error( "'cell', 'cellfrom' and 'cellto' cannot be used at the same time."); } - ssw_cell_id = std::stoll(get_node_value(node_ssw, "cellto")); + auto temp = get_node_array(node_ssw, "cellto"); + for (const auto& cell_id : temp) { + ssw_cell_ids.push_back(cell_id); + } ssw_cell_type = SSWCellType::To; } } @@ -1294,6 +1307,7 @@ void free_memory_settings() settings::statepoint_batch.clear(); settings::sourcepoint_batch.clear(); settings::source_write_surf_id.clear(); + settings::ssw_cell_ids.clear(); settings::res_scat_nuclides.clear(); } diff --git a/tests/regression_tests/surface_source_write/_visualize.py b/tests/regression_tests/surface_source_write/_visualize.py index 73340cae06f..21cb88e5f4a 100644 --- a/tests/regression_tests/surface_source_write/_visualize.py +++ b/tests/regression_tests/surface_source_write/_visualize.py @@ -10,11 +10,11 @@ # Select an option # "show": 3D visualization using matplotlib # "savefig": 2D representation using matplotlib and storing the fig under plot_2d.png - option = "show" - # option = "savefig" + #option = "show" + option = "savefig" # Select the case from its folder name - folder = "case-20" + folder = "case-24" # Reading the surface source file with h5py.File(f"{folder}/surface_source_true.h5", "r") as fp: diff --git a/tests/regression_tests/surface_source_write/case-22/inputs_true.dat b/tests/regression_tests/surface_source_write/case-22/inputs_true.dat new file mode 100644 index 00000000000..801f17191b9 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-22/inputs_true.dat @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + fixed source + 20 + 5 + + + 0.5 0.5 1.5 + + + + + + 1.5 0.5 1.5 + + + + + 7 + 300 + 1 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-22/results_true.dat b/tests/regression_tests/surface_source_write/case-22/results_true.dat new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regression_tests/surface_source_write/case-23/inputs_true.dat b/tests/regression_tests/surface_source_write/case-23/inputs_true.dat new file mode 100644 index 00000000000..e898fb31977 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-23/inputs_true.dat @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + fixed source + 20 + 5 + + + 0.5 0.5 1.5 + + + + + + 1.5 0.5 1.5 + + + + + 7 + 300 + 2 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-23/results_true.dat b/tests/regression_tests/surface_source_write/case-23/results_true.dat new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regression_tests/surface_source_write/case-24/inputs_true.dat b/tests/regression_tests/surface_source_write/case-24/inputs_true.dat new file mode 100644 index 00000000000..99fb72c651b --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-24/inputs_true.dat @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + fixed source + 20 + 5 + + + 0.5 0.5 1.5 + + + + + + 1.5 0.5 1.5 + + + + + 7 + 300 + 1 2 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-24/results_true.dat b/tests/regression_tests/surface_source_write/case-24/results_true.dat new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regression_tests/surface_source_write/test.py b/tests/regression_tests/surface_source_write/test.py index 094df1f8b84..2f74d3fa51d 100644 --- a/tests/regression_tests/surface_source_write/test.py +++ b/tests/regression_tests/surface_source_write/test.py @@ -25,6 +25,7 @@ - model_2: cylindrical core in 1 box (vacuum BC), - model_3: cylindrical core in 1 box (reflective BC), - model_4: cylindrical core in 1 box (periodic BC). +- model_5: 2*2 array of boxes Two models including DAGMC geometries are also used, based on the mesh file 'dagmc.h5m' available from tests/regression_tests/dagmc/legacy: @@ -79,6 +80,9 @@ case-20 model_4 1 No P+R Particles crossing the declared periodic surface case-21 model_4 1 cell (root universe) P+R None +case-22 model_5 1 cellfrom (multiple) T particles crossing the declared + surface that come from multiple + cells ======== ======= ========= ========================= ===== =================================== *: BC stands for Boundary Conditions, T for Transmission, R for Reflective, and V for Vacuum. @@ -603,6 +607,56 @@ def model_4(): return model +@pytest.fixture +def model_5(): + """2*1*2 array of boxes""" + openmc.reset_auto_ids() + model = openmc.Model() + + # ============================================================================= + # Materials + # ============================================================================= + + # ============================================================================= + # Geometry + # ============================================================================= + + x1 = openmc.XPlane(x0=0, boundary_type='vacuum') + x2 = openmc.XPlane(x0=1, boundary_type='vacuum') + x3 = openmc.XPlane(x0=2, boundary_type='vacuum') + y1 = openmc.YPlane(y0=0, boundary_type='vacuum') + y2 = openmc.YPlane(y0=1, boundary_type='vacuum') + z1 = openmc.ZPlane(z0=0, boundary_type='vacuum') + z2 = openmc.ZPlane(z0=1, boundary_type='transmission') + z3 = openmc.ZPlane(z0=2, boundary_type='vacuum') + + box_11 = openmc.Cell(region = +x1 & -x2 & +y1 & -y2 & +z1 & -z2) + box_12 = openmc.Cell(region = +x2 & -x3 & +y1 & -y2 & +z1 & -z2) + box_21 = openmc.Cell(region = +x1 & -x2 & +y1 & -y2 & +z2 & -z3) + box_22 = openmc.Cell(region = +x2 & -x3 & +y1 & -y2 & +z2 & -z3) + + root = openmc.Universe(cells=(box_11, box_12, box_21, box_22)) + model.geometry = openmc.Geometry(root) + + # ============================================================================= + # Settings + # ============================================================================= + + model.settings = openmc.Settings() + model.settings.run_mode = 'fixed source' + model.settings.particles = 20 + model.settings.batches = 5 + model.settings.seed = 1 + + point_1 = openmc.stats.Point((0.5,0.5,1.5)) + point_2 = openmc.stats.Point((1.5,0.5,1.5)) + direction = openmc.stats.Monodirectional((0,0,-1)) + source_1 = openmc.IndependentSource(space=point_1, angle=direction, strength=0.8) + source_2 = openmc.IndependentSource(space=point_2, angle=direction, strength=0.2) + model.settings.source = [source_1, source_2] + + return model + def return_surface_source_data(filepath): """Read a surface source file and return a sorted array composed @@ -754,19 +808,19 @@ def _cleanup(self): ( "case-04", "model_1", - {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cell": 2}, + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cell": [2]}, ), ( "case-05", "model_1", - {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cell": 3}, + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cell": [3]}, ), - ("case-06", "model_1", {"max_particles": 300, "cell": 2}), - ("case-07", "model_1", {"max_particles": 300, "cell": 3}), - ("case-08", "model_1", {"max_particles": 300, "cellfrom": 2}), - ("case-09", "model_1", {"max_particles": 300, "cellto": 2}), - ("case-10", "model_1", {"max_particles": 300, "cellfrom": 3}), - ("case-11", "model_1", {"max_particles": 300, "cellto": 3}), + ("case-06", "model_1", {"max_particles": 300, "cell": [2]}), + ("case-07", "model_1", {"max_particles": 300, "cell": [3]}), + ("case-08", "model_1", {"max_particles": 300, "cellfrom": [2]}), + ("case-09", "model_1", {"max_particles": 300, "cellto": [2]}), + ("case-10", "model_1", {"max_particles": 300, "cellfrom": [3]}), + ("case-11", "model_1", {"max_particles": 300, "cellto": [3]}), ( "case-12", "model_2", @@ -775,17 +829,17 @@ def _cleanup(self): ( "case-13", "model_2", - {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cell": 3}, + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cell": [3]}, ), ( "case-14", "model_2", - {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cellfrom": 3}, + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cellfrom": [3]}, ), ( "case-15", "model_2", - {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cellto": 3}, + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cellto": [3]}, ), ( "case-16", @@ -795,17 +849,17 @@ def _cleanup(self): ( "case-17", "model_3", - {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cell": 3}, + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cell": [3]}, ), ( "case-18", "model_3", - {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cellfrom": 3}, + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cellfrom": [3]}, ), ( "case-19", "model_3", - {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cellto": 3}, + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cellto": [3]}, ), ( "case-20", @@ -815,7 +869,22 @@ def _cleanup(self): ( "case-21", "model_4", - {"max_particles": 300, "surface_ids": [4], "cell": 3}, + {"max_particles": 300, "surface_ids": [4], "cell": [3]}, + ), + ( + "case-22", + "model_5", + {"max_particles": 300, "surface_ids": [7], "cellto": [1]}, + ), + ( + "case-23", + "model_5", + {"max_particles": 300, "surface_ids": [7], "cellto": [2]}, + ), + ( + "case-24", + "model_5", + {"max_particles": 300, "surface_ids": [7], "cellto": [1, 2]}, ), ], ) @@ -848,7 +917,7 @@ def test_consistency_low_realization_number(model_1, two_threads, single_process model_1.settings.surf_source_write = { "max_particles": 200, "surface_ids": [1, 2, 3], - "cellfrom": 2, + "cellfrom": [2], } harness = SurfaceSourceWriteTestHarness( "statepoint.5.h5", model=model_1, workdir="case-a01" @@ -863,13 +932,13 @@ def test_consistency_low_realization_number(model_1, two_threads, single_process ( "case-e01", "model_1", - {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cell": 2}, + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cell": [2]}, ), - ("case-e02", "model_1", {"max_particles": 300, "cell": 3}), + ("case-e02", "model_1", {"max_particles": 300, "cell": [3]}), ( "case-e03", "model_2", - {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cell": 3}, + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cell": [3]}, ), ], ) @@ -1083,21 +1152,21 @@ def model_dagmc_2(): [ ("case-d01", "model_dagmc_1", {"max_particles": 300}), ("case-d02", "model_dagmc_1", {"max_particles": 300, "surface_ids": [1]}), - ("case-d03", "model_dagmc_1", {"max_particles": 300, "cell": 2}), + ("case-d03", "model_dagmc_1", {"max_particles": 300, "cell": [2]}), ( "case-d04", "model_dagmc_1", - {"max_particles": 300, "surface_ids": [1], "cell": 2}, + {"max_particles": 300, "surface_ids": [1], "cell": [2]}, ), - ("case-d05", "model_dagmc_1", {"max_particles": 300, "cellfrom": 2}), - ("case-d06", "model_dagmc_1", {"max_particles": 300, "cellto": 2}), + ("case-d05", "model_dagmc_1", {"max_particles": 300, "cellfrom": [2]}), + ("case-d06", "model_dagmc_1", {"max_particles": 300, "cellto": [2]}), ( "case-d07", "model_dagmc_2", { "max_particles": 300, "surface_ids": [101, 102, 103, 104, 105, 106], - "cell": 7, + "cell": [7], }, ), ( @@ -1106,7 +1175,7 @@ def model_dagmc_2(): { "max_particles": 300, "surface_ids": [101, 102, 103, 104, 105, 106], - "cell": 8, + "cell": [8], }, ), ], diff --git a/tests/unit_tests/test_surface_source_write.py b/tests/unit_tests/test_surface_source_write.py index 6f18d32b718..05923386b55 100644 --- a/tests/unit_tests/test_surface_source_write.py +++ b/tests/unit_tests/test_surface_source_write.py @@ -25,13 +25,13 @@ def geometry(): "parameter", [ {"max_particles": 200}, - {"max_particles": 200, "cell": 1}, - {"max_particles": 200, "cellto": 1}, - {"max_particles": 200, "cellfrom": 1}, + {"max_particles": 200, "cell": [1]}, + {"max_particles": 200, "cellto": [1]}, + {"max_particles": 200, "cellfrom": [1]}, {"max_particles": 200, "surface_ids": [2]}, - {"max_particles": 200, "surface_ids": [2], "cell": 1}, - {"max_particles": 200, "surface_ids": [2], "cellto": 1}, - {"max_particles": 200, "surface_ids": [2], "cellfrom": 1}, + {"max_particles": 200, "surface_ids": [2], "cell": [1]}, + {"max_particles": 200, "surface_ids": [2], "cellto": [1]}, + {"max_particles": 200, "surface_ids": [2], "cellfrom": [1]}, {"max_particles": 200, "surface_ids": [2], "max_source_files": 1}, ], ) @@ -108,11 +108,11 @@ def test_number_surface_source_file_created(max_particles, max_source_files, @pytest.mark.parametrize( "parameter, error", [ - ({"cell": 1}, ERROR_MSG_1), - ({"max_particles": 200, "cell": 1, "cellto": 1}, ERROR_MSG_2), - ({"max_particles": 200, "cell": 1, "cellfrom": 1}, ERROR_MSG_2), - ({"max_particles": 200, "cellto": 1, "cellfrom": 1}, ERROR_MSG_2), - ({"max_particles": 200, "cell": 1, "cellto": 1, "cellfrom": 1}, ERROR_MSG_2), + ({"cell": [1]}, ERROR_MSG_1), + ({"max_particles": 200, "cell": [1], "cellto": [1]}, ERROR_MSG_2), + ({"max_particles": 200, "cell": [1], "cellfrom": [1]}, ERROR_MSG_2), + ({"max_particles": 200, "cellto": [1], "cellfrom": [1]}, ERROR_MSG_2), + ({"max_particles": 200, "cell": [1], "cellto": [1], "cellfrom": [1]}, ERROR_MSG_2), ], ) def test_exceptions(parameter, error, run_in_tmpdir, geometry): @@ -161,8 +161,8 @@ def model(): @pytest.mark.parametrize( "parameter", [ - {"max_particles": 200, "cellto": 2, "surface_ids": [2]}, - {"max_particles": 200, "cellfrom": 2, "surface_ids": [2]}, + {"max_particles": 200, "cellto": [2], "surface_ids": [2]}, + {"max_particles": 200, "cellfrom": [2], "surface_ids": [2]}, ], ) def test_particle_direction(parameter, run_in_tmpdir, model): @@ -249,8 +249,8 @@ def model_dagmc(request): @pytest.mark.parametrize( "parameter", [ - {"max_particles": 200, "cellto": 1}, - {"max_particles": 200, "cellfrom": 1}, + {"max_particles": 200, "cellto": [1]}, + {"max_particles": 200, "cellfrom": [1]}, ], ) def test_particle_direction_dagmc(parameter, run_in_tmpdir, model_dagmc): From df2f44c02bbe2db35daab7317bb60a67fd96ca01 Mon Sep 17 00:00:00 2001 From: MohamedElkamash Date: Sun, 15 Feb 2026 23:01:33 -0600 Subject: [PATCH 02/21] added direction parameter instead of cellto and cellfrom --- include/openmc/settings.h | 7 +++++ src/finalize.cpp | 1 - src/particle.cpp | 15 +++++----- src/settings.cpp | 60 ++++++++++++++++++--------------------- 4 files changed, 42 insertions(+), 41 deletions(-) diff --git a/include/openmc/settings.h b/include/openmc/settings.h index 964c713432b..7d191941bcd 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -6,6 +6,7 @@ #include #include +#include #include #include "pugixml.hpp" @@ -188,6 +189,10 @@ extern vector ssw_cell_ids; //!< Cell ids for the surface source //!< write setting extern SSWCellType ssw_cell_type; //!< Type of option for the cell //!< argument of surface source write +extern std::unordered_map + ssw_cells; //!< Cell ids and directions + //!< for the surface source write setting + extern TemperatureMethod temperature_method; //!< method for choosing temperatures extern double @@ -220,6 +225,8 @@ void read_settings_xml(pugi::xml_node root); void free_memory_settings(); +SSWCellType ssw_cell_type_from_string(std::string_view s); + } // namespace openmc #endif // OPENMC_SETTINGS_H diff --git a/src/finalize.cpp b/src/finalize.cpp index 9f4f8343d47..8d6b405e72d 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -130,7 +130,6 @@ int openmc_finalize() settings::source_rejection_fraction = 0.05; settings::source_separate = false; settings::source_write = true; - settings::ssw_cell_type = SSWCellType::None; settings::ssw_max_particles = 0; settings::ssw_max_files = 1; settings::survival_biasing = false; diff --git a/src/particle.cpp b/src/particle.cpp index a2e1b5c30fb..19ed35e8d5d 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -897,16 +897,15 @@ void add_surf_source_to_bank(Particle& p, const Surface& surf) bool add_site = true; // If a cell/cellfrom/cellto parameter is defined - if (!settings::ssw_cell_ids.empty()) { - for (auto& b : settings::ssw_cell_ids) { + if (!settings::ssw_cells.empty()) { + for (auto& cell : settings::ssw_cells) { add_site = true; // Retrieve cell index and storage type - int cell_idx = model::cell_map[b]; - + int cell_idx = model::cell_map[cell.first]; + SSWCellType direction = cell.second; if (surf.bc_) { // Leave if cellto with vacuum boundary condition - if (surf.bc_->type() == "vacuum" && - settings::ssw_cell_type == SSWCellType::To) { + if (surf.bc_->type() == "vacuum" && direction == SSWCellType::To) { add_site = false; continue; } @@ -956,13 +955,13 @@ void add_surf_source_to_bank(Particle& p, const Surface& surf) // If cellfrom and the cell before crossing is not the cell of // interest - if (settings::ssw_cell_type == SSWCellType::From && !exited) { + if (direction == SSWCellType::From && !exited) { add_site = false; continue; } // If cellto and the cell after crossing is not the cell of interest - if (settings::ssw_cell_type == SSWCellType::To && !entered) { + if (direction == SSWCellType::To && !entered) { add_site = false; continue; } diff --git a/src/settings.cpp b/src/settings.cpp index 7eb25920eb5..a17fc187d4e 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -138,6 +138,7 @@ vector ssw_cell_ids; SSWCellType ssw_cell_type {SSWCellType::None}; double surface_grazing_cutoff {0.001}; double surface_grazing_ratio {0.5}; +std::unordered_map ssw_cells; TemperatureMethod temperature_method {TemperatureMethod::NEAREST}; double temperature_tolerance {10.0}; double temperature_default {293.6}; @@ -935,38 +936,22 @@ void read_settings_xml(pugi::xml_node root) surf_mcpl_write = get_node_value_bool(node_ssw, "mcpl"); } // Get cell information - if (check_for_node(node_ssw, "cell")) { - if (!ssw_cell_ids.empty()) { - fatal_error( - "'cell', 'cellfrom' and 'cellto' cannot be used at the same time."); - } - auto temp = get_node_array(node_ssw, "cell"); - for (const auto& cell_id : temp) { - ssw_cell_ids.push_back(cell_id); - } - ssw_cell_type = SSWCellType::Both; - } - if (check_for_node(node_ssw, "cellfrom")) { - if (!ssw_cell_ids.empty()) { - fatal_error( - "'cell', 'cellfrom' and 'cellto' cannot be used at the same time."); - } - auto temp = get_node_array(node_ssw, "cellfrom"); - for (const auto& cell_id : temp) { - ssw_cell_ids.push_back(cell_id); - } - ssw_cell_type = SSWCellType::From; - } - if (check_for_node(node_ssw, "cellto")) { - if (!ssw_cell_ids.empty()) { - fatal_error( - "'cell', 'cellfrom' and 'cellto' cannot be used at the same time."); - } - auto temp = get_node_array(node_ssw, "cellto"); - for (const auto& cell_id : temp) { - ssw_cell_ids.push_back(cell_id); + if (check_for_node(node_ssw, "cells")) { + auto ids = get_node_array(node_ssw, "cells"); + if (check_for_node(node_ssw, "directions")) { + auto directions = get_node_array(node_ssw, "directions"); + if (directions.size() != ids.size()) { + fatal_error("'directions' must have the same length as 'cells'"); + } + for (std::size_t i {0}; i < ids.size(); ++i) { + SSWCellType direction = ssw_cell_type_from_string(directions[i]); + ssw_cells.emplace(ids[i], direction); + } + } else { + for (std::size_t i {0}; i < ids.size(); ++i) { + ssw_cells.emplace(ids[i], SSWCellType::Both); + } } - ssw_cell_type = SSWCellType::To; } } @@ -1307,10 +1292,21 @@ void free_memory_settings() settings::statepoint_batch.clear(); settings::sourcepoint_batch.clear(); settings::source_write_surf_id.clear(); - settings::ssw_cell_ids.clear(); + settings::ssw_cells.clear(); settings::res_scat_nuclides.clear(); } +SSWCellType ssw_cell_type_from_string(std::string_view s) +{ + if (s == "from") + return SSWCellType::From; + if (s == "to") + return SSWCellType::To; + if (s == "both") + return SSWCellType::Both; + throw std::invalid_argument("direction must be 'from', 'to', or 'both'"); +} + //============================================================================== // C API functions //============================================================================== From 80f2274456fa8efd898dfe1a7ff76a8c5f23e97f Mon Sep 17 00:00:00 2001 From: MohamedElkamash Date: Sun, 15 Feb 2026 23:50:31 -0600 Subject: [PATCH 03/21] added directions parameter to python api changed input files in surface_source_write tests cases to the new syntax updated test_surface_source_write.py reverted tests back to original state to allow testing with older syntax added directions parameter to python api --- openmc/settings.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/openmc/settings.py b/openmc/settings.py index 6c204efa155..e24b966ca1e 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -280,7 +280,7 @@ class Settings: process (int) :max_source_files: Maximum number of surface source files to be created (int) :mcpl: Output in the form of an MCPL-file (bool) - :cell: List of cell IDs used to determine if particles crossing identified + :cells: List of cell IDs used to determine if particles crossing identified surfaces are to be banked. Particles coming from or going to this declared cell will be banked (int) :cellfrom: List of cell IDs used to determine if particles crossing identified @@ -297,6 +297,8 @@ class Settings: Surface flux cosine substitution ratio. If not specified, the default value is 0.5. For more information, see the surface tally section in the theory manual. + :directions: List of directions corresponding to cells. + Acceptable entries are: "from", "to", or "both" (str) survival_biasing : bool Indicate whether survival biasing is to be used tabular_legendre : dict @@ -879,20 +881,22 @@ def surf_source_write(self, surf_source_write: dict): "surface source writing key", key, ("surface_ids", "max_particles", "max_source_files", - "mcpl", "cell", "cellfrom", "cellto"), + "mcpl", "cells", "directions"), ) - if key in ("surface_ids", "cell", "cellfrom", "cellto"): + if key in ("surface_ids", "cells"): name = { "surface_ids": "surface id(s) for source banking", - "cell": "Cell ID(s) for source banking (from or to)", - "cellfrom": "Cell ID(s) for source banking (from only)", - "cellto": "Cell ID(s) for source banking (to only)", + "cells": "Cell ID(s) for source banking", }[key] - cv.check_type(name, value, Iterable, Integral) for x in value: cv.check_greater_than(name, x, 0) - + elif key == "directions": + cv.check_type("directions corresponding to cells (from, to or both)", value, Iterable, str) + for direction in value: + if (direction not in ["from", "to", "both"]): + msg = "allowed values for direction: 'from', 'to', 'both' " + raise ValueError(msg) elif key == "mcpl": cv.check_type("write to an MCPL-format file", value, bool) elif key in ("max_particles", "max_source_files"): @@ -1582,7 +1586,7 @@ def _create_surf_source_write_subelement(self, root): subelement = ET.SubElement(element, key) subelement.text = str(self._surf_source_write[key]) - for key in ("cell", "cellfrom", "cellto"): + for key in ("cells", "directions"): if key in self._surf_source_write: subelement = ET.SubElement(element, key) subelement.text = " ".join( @@ -2100,9 +2104,11 @@ def _surf_source_write_from_xml_element(self, root): elem = root.find('surf_source_write') if elem is None: return - for key in ('surface_ids', 'max_particles', 'max_source_files', 'mcpl', 'cell', 'cellto', 'cellfrom'): - if key in ('surface_ids', 'cell', 'cellto', 'cellfrom'): + for key in ('surface_ids', 'max_particles', 'max_source_files', 'mcpl', 'cells', 'directions'): + if key in ['surface_ids', 'cells']: value = get_elem_list(elem, key, int) + elif key == 'directions': + value = get_elem_list(elem, key, str) else: value = get_text(elem, key) if value is not None: @@ -2112,6 +2118,7 @@ def _surf_source_write_from_xml_element(self, root): value = int(value) self.surf_source_write[key] = value + def _collision_track_from_xml_element(self, root): elem = root.find('collision_track') if elem is not None: From b6a4a33df2f56c74d4854d25e78b407ac2bb8cb0 Mon Sep 17 00:00:00 2001 From: MohamedElkamash Date: Thu, 19 Feb 2026 15:09:15 -0600 Subject: [PATCH 04/21] allowed the old syntax --- include/openmc/settings.h | 1 + openmc/settings.py | 13 +++--- src/particle.cpp | 11 +++-- src/settings.cpp | 41 +++++++++++++++++- .../case-24/inputs_true.dat | 3 +- .../surface_source_write/test.py | 42 +++++++++---------- tests/unit_tests/test_surface_source_write.py | 30 ++++++------- 7 files changed, 95 insertions(+), 46 deletions(-) diff --git a/include/openmc/settings.h b/include/openmc/settings.h index 7d191941bcd..914ee10a1f0 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -189,6 +189,7 @@ extern vector ssw_cell_ids; //!< Cell ids for the surface source //!< write setting extern SSWCellType ssw_cell_type; //!< Type of option for the cell //!< argument of surface source write + extern std::unordered_map ssw_cells; //!< Cell ids and directions //!< for the surface source write setting diff --git a/openmc/settings.py b/openmc/settings.py index e24b966ca1e..bf9fb7a97ba 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -881,7 +881,7 @@ def surf_source_write(self, surf_source_write: dict): "surface source writing key", key, ("surface_ids", "max_particles", "max_source_files", - "mcpl", "cells", "directions"), + "mcpl", "cells", "directions", "cell", "cellfrom", "cellto"), ) if key in ("surface_ids", "cells"): name = { @@ -899,10 +899,13 @@ def surf_source_write(self, surf_source_write: dict): raise ValueError(msg) elif key == "mcpl": cv.check_type("write to an MCPL-format file", value, bool) - elif key in ("max_particles", "max_source_files"): + elif key in ("max_particles", "max_source_files", "cell", "cellfrom", "cellto"): name = { "max_particles": "maximum particle banks on surfaces per process", "max_source_files": "maximun surface source files to be written", + "cell": "Cell ID for source banking (from or to)", + "cellfrom": "Cell ID for source banking (from only)", + "cellto": "Cell ID for source banking (to only)", }[key] cv.check_type(name, value, Integral) cv.check_greater_than(name, value, 0) @@ -1581,7 +1584,7 @@ def _create_surf_source_write_subelement(self, root): str(x) for x in self._surf_source_write["surface_ids"] ) - for key in ("max_particles", "max_source_files"): + for key in ("max_particles", "max_source_files", "cell", "cellfrom", "cellto"): if key in self._surf_source_write: subelement = ET.SubElement(element, key) subelement.text = str(self._surf_source_write[key]) @@ -2104,7 +2107,7 @@ def _surf_source_write_from_xml_element(self, root): elem = root.find('surf_source_write') if elem is None: return - for key in ('surface_ids', 'max_particles', 'max_source_files', 'mcpl', 'cells', 'directions'): + for key in ('surface_ids', 'max_particles', 'max_source_files', 'mcpl', 'cells', 'directions', 'cell', 'cellto', 'cellfrom'): if key in ['surface_ids', 'cells']: value = get_elem_list(elem, key, int) elif key == 'directions': @@ -2114,7 +2117,7 @@ def _surf_source_write_from_xml_element(self, root): if value is not None: if key == 'mcpl': value = value in ('true', '1') - elif key in ('max_particles', 'max_source_files'): + elif key in ('max_particles', 'max_source_files', 'cell', 'cellfrom', 'cellto'): value = int(value) self.surf_source_write[key] = value diff --git a/src/particle.cpp b/src/particle.cpp index 19ed35e8d5d..5cc23911ded 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -895,11 +895,14 @@ void add_surf_source_to_bank(Particle& p, const Surface& surf) return; } - bool add_site = true; - // If a cell/cellfrom/cellto parameter is defined + bool add_site = + true; // this insures that a site is added if 'cells' is not defined + + // If 'cells' is defined if (!settings::ssw_cells.empty()) { for (auto& cell : settings::ssw_cells) { - add_site = true; + add_site = true; // we assume the cell-direction pair is valid until it + // gets rejected // Retrieve cell index and storage type int cell_idx = model::cell_map[cell.first]; SSWCellType direction = cell.second; @@ -966,6 +969,8 @@ void add_surf_source_to_bank(Particle& p, const Surface& surf) continue; } } + // if any cell-direction pair survived all the checks we terminate the + // loop if (add_site) { break; } diff --git a/src/settings.cpp b/src/settings.cpp index a17fc187d4e..8fe16421341 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -138,6 +138,7 @@ vector ssw_cell_ids; SSWCellType ssw_cell_type {SSWCellType::None}; double surface_grazing_cutoff {0.001}; double surface_grazing_ratio {0.5}; +int64_t ssw_cell_id {C_NONE}; std::unordered_map ssw_cells; TemperatureMethod temperature_method {TemperatureMethod::NEAREST}; double temperature_tolerance {10.0}; @@ -937,6 +938,13 @@ void read_settings_xml(pugi::xml_node root) } // Get cell information if (check_for_node(node_ssw, "cells")) { + // raise an error if the new syntax is mixed with the old syntax + if (check_for_node(node_ssw, "cell") || + check_for_node(node_ssw, "cellfrom") || + check_for_node(node_ssw, "cellto")) { + fatal_error("'cells' cannot be used at the same time with 'cell', " + "'cellfrom' or 'cellto'."); + } auto ids = get_node_array(node_ssw, "cells"); if (check_for_node(node_ssw, "directions")) { auto directions = get_node_array(node_ssw, "directions"); @@ -947,11 +955,42 @@ void read_settings_xml(pugi::xml_node root) SSWCellType direction = ssw_cell_type_from_string(directions[i]); ssw_cells.emplace(ids[i], direction); } - } else { + } else { // default behavior if 'directions' is not defined for (std::size_t i {0}; i < ids.size(); ++i) { ssw_cells.emplace(ids[i], SSWCellType::Both); } } + } else { + if (check_for_node(node_ssw, "directions")) { + fatal_error("'directions' cannot be used if 'cells' is not defined."); + } + // old syntax will be deprecated in the future + if (check_for_node(node_ssw, "cell")) { + warning("'cell' is deprecated and will be removed in the future. Use " + "'cells' and 'directions' instead."); + ssw_cell_id = std::stoll(get_node_value(node_ssw, "cell")); + ssw_cells.emplace(ssw_cell_id, SSWCellType::Both); + } + if (check_for_node(node_ssw, "cellfrom")) { + warning("'cellfrom' is deprecated and will be removed in the future. " + "Use 'cells' and 'directions' instead."); + if (ssw_cell_id != C_NONE) { + fatal_error( + "'cell', 'cellfrom' and 'cellto' cannot be used at the same time."); + } + ssw_cell_id = std::stoll(get_node_value(node_ssw, "cellfrom")); + ssw_cells.emplace(ssw_cell_id, SSWCellType::From); + } + if (check_for_node(node_ssw, "cellto")) { + warning("'cellto' is deprecated and will be removed in the future. Use " + "'cells' and 'directions' instead."); + if (ssw_cell_id != C_NONE) { + fatal_error( + "'cell', 'cellfrom' and 'cellto' cannot be used at the same time."); + } + ssw_cell_id = std::stoll(get_node_value(node_ssw, "cellto")); + ssw_cells.emplace(ssw_cell_id, SSWCellType::To); + } } } diff --git a/tests/regression_tests/surface_source_write/case-24/inputs_true.dat b/tests/regression_tests/surface_source_write/case-24/inputs_true.dat index 99fb72c651b..fc5c109864a 100644 --- a/tests/regression_tests/surface_source_write/case-24/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-24/inputs_true.dat @@ -35,7 +35,8 @@ 7 300 - 1 2 + 1 2 + to to 1 diff --git a/tests/regression_tests/surface_source_write/test.py b/tests/regression_tests/surface_source_write/test.py index 2f74d3fa51d..933a708bd31 100644 --- a/tests/regression_tests/surface_source_write/test.py +++ b/tests/regression_tests/surface_source_write/test.py @@ -808,19 +808,19 @@ def _cleanup(self): ( "case-04", "model_1", - {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cell": [2]}, + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cell": 2}, ), ( "case-05", "model_1", - {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cell": [3]}, + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cell": 3}, ), - ("case-06", "model_1", {"max_particles": 300, "cell": [2]}), - ("case-07", "model_1", {"max_particles": 300, "cell": [3]}), - ("case-08", "model_1", {"max_particles": 300, "cellfrom": [2]}), - ("case-09", "model_1", {"max_particles": 300, "cellto": [2]}), - ("case-10", "model_1", {"max_particles": 300, "cellfrom": [3]}), - ("case-11", "model_1", {"max_particles": 300, "cellto": [3]}), + ("case-06", "model_1", {"max_particles": 300, "cell": 2}), + ("case-07", "model_1", {"max_particles": 300, "cell": 3}), + ("case-08", "model_1", {"max_particles": 300, "cellfrom": 2}), + ("case-09", "model_1", {"max_particles": 300, "cellto": 2}), + ("case-10", "model_1", {"max_particles": 300, "cellfrom": 3}), + ("case-11", "model_1", {"max_particles": 300, "cellto": 3}), ( "case-12", "model_2", @@ -829,17 +829,17 @@ def _cleanup(self): ( "case-13", "model_2", - {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cell": [3]}, + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cell": 3}, ), ( "case-14", "model_2", - {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cellfrom": [3]}, + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cellfrom": 3}, ), ( "case-15", "model_2", - {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cellto": [3]}, + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cellto": 3}, ), ( "case-16", @@ -849,17 +849,17 @@ def _cleanup(self): ( "case-17", "model_3", - {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cell": [3]}, + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cell": 3}, ), ( "case-18", "model_3", - {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cellfrom": [3]}, + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cellfrom": 3}, ), ( "case-19", "model_3", - {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cellto": [3]}, + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cellto": 3}, ), ( "case-20", @@ -869,22 +869,22 @@ def _cleanup(self): ( "case-21", "model_4", - {"max_particles": 300, "surface_ids": [4], "cell": [3]}, + {"max_particles": 300, "surface_ids": [4], "cell": 3}, ), ( "case-22", "model_5", - {"max_particles": 300, "surface_ids": [7], "cellto": [1]}, + {"max_particles": 300, "surface_ids": [7], "cellto": 1}, ), ( "case-23", "model_5", - {"max_particles": 300, "surface_ids": [7], "cellto": [2]}, + {"max_particles": 300, "surface_ids": [7], "cellto": 2}, ), ( "case-24", "model_5", - {"max_particles": 300, "surface_ids": [7], "cellto": [1, 2]}, + {"max_particles": 300, "surface_ids": [7], "cells": [1, 2], "directions": ["to", "to"]}, ), ], ) @@ -917,7 +917,7 @@ def test_consistency_low_realization_number(model_1, two_threads, single_process model_1.settings.surf_source_write = { "max_particles": 200, "surface_ids": [1, 2, 3], - "cellfrom": [2], + "cellfrom": 2, } harness = SurfaceSourceWriteTestHarness( "statepoint.5.h5", model=model_1, workdir="case-a01" @@ -932,13 +932,13 @@ def test_consistency_low_realization_number(model_1, two_threads, single_process ( "case-e01", "model_1", - {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cell": [2]}, + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cell": 2}, ), ("case-e02", "model_1", {"max_particles": 300, "cell": [3]}), ( "case-e03", "model_2", - {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cell": [3]}, + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cell": 3}, ), ], ) diff --git a/tests/unit_tests/test_surface_source_write.py b/tests/unit_tests/test_surface_source_write.py index 05923386b55..6f18d32b718 100644 --- a/tests/unit_tests/test_surface_source_write.py +++ b/tests/unit_tests/test_surface_source_write.py @@ -25,13 +25,13 @@ def geometry(): "parameter", [ {"max_particles": 200}, - {"max_particles": 200, "cell": [1]}, - {"max_particles": 200, "cellto": [1]}, - {"max_particles": 200, "cellfrom": [1]}, + {"max_particles": 200, "cell": 1}, + {"max_particles": 200, "cellto": 1}, + {"max_particles": 200, "cellfrom": 1}, {"max_particles": 200, "surface_ids": [2]}, - {"max_particles": 200, "surface_ids": [2], "cell": [1]}, - {"max_particles": 200, "surface_ids": [2], "cellto": [1]}, - {"max_particles": 200, "surface_ids": [2], "cellfrom": [1]}, + {"max_particles": 200, "surface_ids": [2], "cell": 1}, + {"max_particles": 200, "surface_ids": [2], "cellto": 1}, + {"max_particles": 200, "surface_ids": [2], "cellfrom": 1}, {"max_particles": 200, "surface_ids": [2], "max_source_files": 1}, ], ) @@ -108,11 +108,11 @@ def test_number_surface_source_file_created(max_particles, max_source_files, @pytest.mark.parametrize( "parameter, error", [ - ({"cell": [1]}, ERROR_MSG_1), - ({"max_particles": 200, "cell": [1], "cellto": [1]}, ERROR_MSG_2), - ({"max_particles": 200, "cell": [1], "cellfrom": [1]}, ERROR_MSG_2), - ({"max_particles": 200, "cellto": [1], "cellfrom": [1]}, ERROR_MSG_2), - ({"max_particles": 200, "cell": [1], "cellto": [1], "cellfrom": [1]}, ERROR_MSG_2), + ({"cell": 1}, ERROR_MSG_1), + ({"max_particles": 200, "cell": 1, "cellto": 1}, ERROR_MSG_2), + ({"max_particles": 200, "cell": 1, "cellfrom": 1}, ERROR_MSG_2), + ({"max_particles": 200, "cellto": 1, "cellfrom": 1}, ERROR_MSG_2), + ({"max_particles": 200, "cell": 1, "cellto": 1, "cellfrom": 1}, ERROR_MSG_2), ], ) def test_exceptions(parameter, error, run_in_tmpdir, geometry): @@ -161,8 +161,8 @@ def model(): @pytest.mark.parametrize( "parameter", [ - {"max_particles": 200, "cellto": [2], "surface_ids": [2]}, - {"max_particles": 200, "cellfrom": [2], "surface_ids": [2]}, + {"max_particles": 200, "cellto": 2, "surface_ids": [2]}, + {"max_particles": 200, "cellfrom": 2, "surface_ids": [2]}, ], ) def test_particle_direction(parameter, run_in_tmpdir, model): @@ -249,8 +249,8 @@ def model_dagmc(request): @pytest.mark.parametrize( "parameter", [ - {"max_particles": 200, "cellto": [1]}, - {"max_particles": 200, "cellfrom": [1]}, + {"max_particles": 200, "cellto": 1}, + {"max_particles": 200, "cellfrom": 1}, ], ) def test_particle_direction_dagmc(parameter, run_in_tmpdir, model_dagmc): From 1038551722e308e4fa4bf9597d7b014d8194d1fd Mon Sep 17 00:00:00 2001 From: MohamedElkamash Date: Thu, 19 Feb 2026 15:15:25 -0600 Subject: [PATCH 05/21] added ssw_cell_id back to finalize --- src/finalize.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/finalize.cpp b/src/finalize.cpp index 8d6b405e72d..d7ff69611c3 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -130,6 +130,7 @@ int openmc_finalize() settings::source_rejection_fraction = 0.05; settings::source_separate = false; settings::source_write = true; + settings::ssw_cell_id = C_NONE; settings::ssw_max_particles = 0; settings::ssw_max_files = 1; settings::survival_biasing = false; From cc4c88f66c7562dfcdc17078750da372ce1c043d Mon Sep 17 00:00:00 2001 From: MohamedElkamash Date: Thu, 19 Feb 2026 16:08:29 -0600 Subject: [PATCH 06/21] changed the condition of adding a surface to bank from particle.cpp to surface.cpp --- src/particle.cpp | 3 +-- src/surface.cpp | 5 +++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/particle.cpp b/src/particle.cpp index 5cc23911ded..b2251a3dc96 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -890,8 +890,7 @@ void Particle::update_neutron_xs( //============================================================================== void add_surf_source_to_bank(Particle& p, const Surface& surf) { - if (simulation::current_batch <= settings::n_inactive || - simulation::surf_source_bank.full()) { + if (simulation::current_batch <= settings::n_inactive) { return; } diff --git a/src/surface.cpp b/src/surface.cpp index 81b756deae7..25ae5091efe 100644 --- a/src/surface.cpp +++ b/src/surface.cpp @@ -63,8 +63,9 @@ Surface::Surface(pugi::xml_node surf_node) { if (check_for_node(surf_node, "id")) { id_ = std::stoi(get_node_value(surf_node, "id")); - if (contains(settings::source_write_surf_id, id_) || - settings::source_write_surf_id.empty()) { + if (settings::surf_source_write && + (contains(settings::source_write_surf_id, id_) || + settings::source_write_surf_id.empty())) { surf_source_ = true; } } else { From e40c7a683aa13ce7bd482a634db6a6b97492f520 Mon Sep 17 00:00:00 2001 From: MohamedElkamash Date: Fri, 20 Feb 2026 06:51:11 -0600 Subject: [PATCH 07/21] simplified the add site loop logic --- src/particle.cpp | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/src/particle.cpp b/src/particle.cpp index b2251a3dc96..240c9d66dbe 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -894,27 +894,24 @@ void add_surf_source_to_bank(Particle& p, const Surface& surf) return; } - bool add_site = - true; // this insures that a site is added if 'cells' is not defined + bool add_site = true; // add the site if 'cells' is not defined // If 'cells' is defined if (!settings::ssw_cells.empty()) { for (auto& cell : settings::ssw_cells) { - add_site = true; // we assume the cell-direction pair is valid until it - // gets rejected + add_site = false; // we assume the cell-direction pair is invalid till it + // passes all the tests // Retrieve cell index and storage type int cell_idx = model::cell_map[cell.first]; SSWCellType direction = cell.second; if (surf.bc_) { // Leave if cellto with vacuum boundary condition if (surf.bc_->type() == "vacuum" && direction == SSWCellType::To) { - add_site = false; continue; } // Leave if other boundary condition than vacuum if (surf.bc_->type() != "vacuum") { - add_site = false; continue; } } @@ -938,41 +935,35 @@ void add_surf_source_to_bank(Particle& p, const Surface& surf) // Vacuum boundary conditions: return if cell is not exited if (surf.bc_) { if (surf.bc_->type() == "vacuum" && !exited) { - add_site = false; continue; } } else { // If we both enter and exit the cell of interest if (entered && exited) { - add_site = false; continue; } // If we did not enter nor exit the cell of interest if (!entered && !exited) { - add_site = false; continue; } // If cellfrom and the cell before crossing is not the cell of // interest if (direction == SSWCellType::From && !exited) { - add_site = false; continue; } // If cellto and the cell after crossing is not the cell of interest if (direction == SSWCellType::To && !entered) { - add_site = false; continue; } } - // if any cell-direction pair survived all the checks we terminate the - // loop - if (add_site) { - break; - } + // if the cell-direction pair survived all the checks we add the site and + // terminate the loop + add_site = true; + break; } } From 2e4f8fe6f53348a1d5e9f0157c12305ff5fed265 Mon Sep 17 00:00:00 2001 From: MohamedElkamash Date: Sat, 21 Feb 2026 10:58:22 -0600 Subject: [PATCH 08/21] handled duplicated cells --- src/settings.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/settings.cpp b/src/settings.cpp index 8fe16421341..53b89c12596 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -953,7 +953,12 @@ void read_settings_xml(pugi::xml_node root) } for (std::size_t i {0}; i < ids.size(); ++i) { SSWCellType direction = ssw_cell_type_from_string(directions[i]); - ssw_cells.emplace(ids[i], direction); + auto [it, inserted] = ssw_cells.emplace(ids[i], direction); + //check for duplicate keys with different values + if (!inserted && it->second != direction) { + // the union of different values will always be 'Both' + it->second = SSWCellType::Both; + } } } else { // default behavior if 'directions' is not defined for (std::size_t i {0}; i < ids.size(); ++i) { From c3c9fc0ffcf5197c67239ff6a21cd3d408af59ad Mon Sep 17 00:00:00 2001 From: MohamedElkamash Date: Sat, 21 Feb 2026 11:02:44 -0600 Subject: [PATCH 09/21] added descriptions for the parameters in the openmc api --- openmc/settings.py | 9 +++++++++ src/settings.cpp | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/openmc/settings.py b/openmc/settings.py index bf9fb7a97ba..efe072999a3 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -298,6 +298,15 @@ class Settings: value is 0.5. For more information, see the surface tally section in the theory manual. :directions: List of directions corresponding to cells. + :cell: Cell ID used to determine if particles crossing identified + surfaces are to be banked. Particles coming from or going to this + declared cell will be banked (int) + :cellfrom: Cell ID used to determine if particles crossing identified + surfaces are to be banked. Particles coming from this + declared cell will be banked (int) + :cellto: Cell ID used to determine if particles crossing identified + surfaces are to be banked. Particles going to this declared + cell will be banked (int) Acceptable entries are: "from", "to", or "both" (str) survival_biasing : bool Indicate whether survival biasing is to be used diff --git a/src/settings.cpp b/src/settings.cpp index 53b89c12596..762e060f86f 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -954,7 +954,7 @@ void read_settings_xml(pugi::xml_node root) for (std::size_t i {0}; i < ids.size(); ++i) { SSWCellType direction = ssw_cell_type_from_string(directions[i]); auto [it, inserted] = ssw_cells.emplace(ids[i], direction); - //check for duplicate keys with different values + // check for duplicate keys with different values if (!inserted && it->second != direction) { // the union of different values will always be 'Both' it->second = SSWCellType::Both; From 643047fcc92aa17fbdffd7bc54e62ed77f2682bb Mon Sep 17 00:00:00 2001 From: MohamedElkamash Date: Sat, 21 Feb 2026 11:46:08 -0600 Subject: [PATCH 10/21] refactored the site adding loop logic --- src/particle.cpp | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/particle.cpp b/src/particle.cpp index 240c9d66dbe..672901ae452 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -898,24 +898,21 @@ void add_surf_source_to_bank(Particle& p, const Surface& surf) // If 'cells' is defined if (!settings::ssw_cells.empty()) { + if (surf.bc_ && surf.bc_->type() != "vacuum") { + // Leave if other boundary condition than vacuum + return; + } + add_site = false; // we assume all cell-direction pairs are invalid till one of + // them passes all the tests for (auto& cell : settings::ssw_cells) { - add_site = false; // we assume the cell-direction pair is invalid till it - // passes all the tests // Retrieve cell index and storage type int cell_idx = model::cell_map[cell.first]; SSWCellType direction = cell.second; - if (surf.bc_) { - // Leave if cellto with vacuum boundary condition - if (surf.bc_->type() == "vacuum" && direction == SSWCellType::To) { + if (surf.bc_ && surf.bc_->type() == "vacuum" && direction == SSWCellType::To) { + // skip if cellto with vacuum boundary condition continue; } - // Leave if other boundary condition than vacuum - if (surf.bc_->type() != "vacuum") { - continue; - } - } - // Check if the cell of interest has been exited bool exited = false; for (int i = 0; i < p.n_coord_last(); ++i) { From 7e466eb5aa499eed8bf057962cb7044eab158543 Mon Sep 17 00:00:00 2001 From: MohamedElkamash Date: Sun, 22 Feb 2026 12:28:53 -0600 Subject: [PATCH 11/21] added regression tests added regression tests --- src/particle.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/particle.cpp b/src/particle.cpp index 672901ae452..31b97e4bc12 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -902,16 +902,17 @@ void add_surf_source_to_bank(Particle& p, const Surface& surf) // Leave if other boundary condition than vacuum return; } - add_site = false; // we assume all cell-direction pairs are invalid till one of - // them passes all the tests + add_site = false; // we assume all cell-direction pairs are invalid till one + // of them passes all the tests for (auto& cell : settings::ssw_cells) { // Retrieve cell index and storage type int cell_idx = model::cell_map[cell.first]; SSWCellType direction = cell.second; - if (surf.bc_ && surf.bc_->type() == "vacuum" && direction == SSWCellType::To) { + if (surf.bc_ && surf.bc_->type() == "vacuum" && + direction == SSWCellType::To) { // skip if cellto with vacuum boundary condition - continue; - } + continue; + } // Check if the cell of interest has been exited bool exited = false; From e1b37348ba7d9715ccecf3e6ac3bd1ea8ded2e38 Mon Sep 17 00:00:00 2001 From: MohamedElkamash Date: Sun, 22 Feb 2026 12:28:53 -0600 Subject: [PATCH 12/21] added regression tests --- openmc/settings.py | 6 +- src/particle.cpp | 2 +- .../case-22/inputs_true.dat | 70 +++++-- .../case-22/surface_source_true.h5 | Bin 0 -> 15068 bytes .../case-23/inputs_true.dat | 70 +++++-- .../case-23/surface_source_true.h5 | Bin 0 -> 14564 bytes .../case-24/inputs_true.dat | 69 +++++-- .../case-24/surface_source_true.h5 | Bin 0 -> 27416 bytes .../surface_source_write/test.py | 171 ++++++++++++++---- 9 files changed, 302 insertions(+), 86 deletions(-) create mode 100644 tests/regression_tests/surface_source_write/case-22/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-23/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-24/surface_source_true.h5 diff --git a/openmc/settings.py b/openmc/settings.py index efe072999a3..993d43def48 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -300,13 +300,13 @@ class Settings: :directions: List of directions corresponding to cells. :cell: Cell ID used to determine if particles crossing identified surfaces are to be banked. Particles coming from or going to this - declared cell will be banked (int) + declared cell will be banked (int) ("cell" will be deprecated in the future, use "cells" instead.) :cellfrom: Cell ID used to determine if particles crossing identified surfaces are to be banked. Particles coming from this - declared cell will be banked (int) + declared cell will be banked (int) ("cellfrom" will be deprecated in the future, use "cells" and "directions" instead.) :cellto: Cell ID used to determine if particles crossing identified surfaces are to be banked. Particles going to this declared - cell will be banked (int) + cell will be banked (int) ("cellto" will be deprecated in the future, use "cells" and "directions" instead.) Acceptable entries are: "from", "to", or "both" (str) survival_biasing : bool Indicate whether survival biasing is to be used diff --git a/src/particle.cpp b/src/particle.cpp index 31b97e4bc12..00cd338f645 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -958,7 +958,7 @@ void add_surf_source_to_bank(Particle& p, const Surface& surf) continue; } } - // if the cell-direction pair survived all the checks we add the site and + // if a cell-direction pair survived all the checks we add the site and // terminate the loop add_site = true; break; diff --git a/tests/regression_tests/surface_source_write/case-22/inputs_true.dat b/tests/regression_tests/surface_source_write/case-22/inputs_true.dat index 801f17191b9..a39d905ce90 100644 --- a/tests/regression_tests/surface_source_write/case-22/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-22/inputs_true.dat @@ -3,39 +3,75 @@ - - - - + + + + + + + + + + + + - - - - - - - + + + + + + + + + + fixed source - 20 + 60 5 - + + + 1.5 0.5 0.5 + + + + + + 2.5 0.5 0.5 + + + + 0.5 0.5 1.5 + + + + + 3.5 0.5 1.5 + + + + + + 1.5 0.5 2.5 + - + - 1.5 0.5 1.5 + 2.5 0.5 2.5 - 7 + 2 4 9 10 300 - 1 + 6 + to 1 diff --git a/tests/regression_tests/surface_source_write/case-22/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-22/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..27bca94959af53ff6a72ddde88fa80370ebd6056 GIT binary patch literal 15068 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 n;J4T>D(cDfH#{`wkx$z^doPm>yFbkY - - - - + + + + + + + + + + + + - - - - - - - + + + + + + + + + + fixed source - 20 + 60 5 - + + + 1.5 0.5 0.5 + + + + + + 2.5 0.5 0.5 + + + + 0.5 0.5 1.5 + + + + + 3.5 0.5 1.5 + + + + + + 1.5 0.5 2.5 + - + - 1.5 0.5 1.5 + 2.5 0.5 2.5 - 7 + 2 4 9 10 300 - 2 + 7 + to 1 diff --git a/tests/regression_tests/surface_source_write/case-23/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-23/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..432adb6a30c7e60505b5e11aaf9b3b58f3500f91 GIT binary patch literal 14564 zcmeHMdpuR!7T%)lB$V<8+$g?7nlu8~&Z>k%i(o-Heb>vj^PEV?% zRN^=(Nl0`$DoF|@DR;B?zSr2tT4!0m{m=c~v-@SPJ=dJy9%Fppm~*VTVy0VI4VD-o z!RY!hnG7+8O!p=KPbcs5(tz8;|IWYf&ASlcwb8uxSU>KC2nVt2o&v95hKH{z%I!$x z^*hhDv}ABb^6~jC?>56*oO{LOcl^`-UqxWLB{+0B?8ft&ucv!Fo$-SavLYBP7{K|@|9!4q42C3w z-91EvGe?q>6|4M$LjqPTXUK4bE!KTUm3v==JCpAoza7fC+jT9%=--W<$jc)UUWSSA z=I!Z5VKDfuD1#;1g(%1Q9cNA(%)9@0jT{HRE7swEH;{LKi0U+sXI&pQLvIj6uah%B zf_IPG&->C#)7#uNym5F{`s?K0=7#Zx@wM{TS=^rgWgj_b0Z`yc#&7Y zLdJ}q1A4jcIVWgAaA<&U;D2({p_lPJyL!&;&rx$%-t_C&uR9@`ygU;XImVOoR22H5 zJoR@C_PCfB;AB6`LWG0B*ZyT*zV`gRf3B%UfXU{LteM7b+jwo$4MBv{s;bq~_UghW z>Db}Vnp;h{_nW%McdfH*J|PV1nBP|?Nb0^a0>)2Ye;uClkpxqW&HX*Tot(w6*H6=p z%6X@ymdhAJ=^G!H$0spnS5fv~JE>zf-YKjd_0bk0gWfK$imFF;(#0&+Hg*23Wg?s} z4gF=xmoFrk)Z-Z)q3t$)q&{SpJ#1G$-hwdca>Sl6aedP)cbKIs-Y?7d520W#|M_&Z z*dl8vRX+VBBB((qm|+&qujQ85!JG)MpvLK#Bka2%?_3T4RZdOQY6qP1t}8Ku#(*f_ zjkmCt1|ls{=ji50R!m#w9GD&XcAI^rto5R*8M zqEE4;#KR4szkPYCXL>WT6KkHj@0XX!?tUiDg0|xZj}Ji`!lcX5mR%)>cRA=lxqFqT z@wrb3liYV=-Zjo&sYx<0habJPCC%o&7Yb(kz5Uk}@iU)563WauYi| zr^VoW$97WJlIrZr^7v@)>OI0~x*g5!mQc`0D2>*QH1NUy8NUnVJ>JSy2yFe1-u99Y+VAG-9xSL8e9oq9a= z-6d4!S!zNRbIh0>pd_|BdFvLePrAIn2$!!~C*=bp zFMI8IXHkQEr^|b=irNH^{WG9>e5MKeI+>l+HD6SGUnJJr0?f_Xt%Wg-$WC(KiFv=h za{kUS+nu0o_zv3~i8_Qym-m>RRyz)8DMLWAVWVA}NFOv$Y-i&t{p^U-YzVI@uDbcC zNho$+nyE43@&kQ1d5rn#y^C<_`IKErl0^m@AR?oXS*H38*-6e3@tWN9ub{o{Pi&#) zY3th4JB70@)i-D@$*G(KECoM(b2YNPgStLVby_Tx*erOvF6z^6dExAvXXeO7O@3?% z61Bb;^H&I`UJ5*OG*G!(9$X(rIC)p&+6L!4_4Tt*Bv3tRz9U>QS?e)&KCVOP=F#gw z8TpH%X0S1FvL($LLRYmW zH_s%iKLs&~?bP0)8ys(J4X5R}chf6^ z)9vBKtE_be)vpmIwv&1uSy&A_VbnPdp1*Zo-mHc9ce=c16-~OQ9yK0L1bO>Lv4m59 zp4xn3LO>uJv`bmd6|GoHCZ3UuT4+~nCo#Z;Z739q|1@?b*Hq?zyi^dxx01b z@VbOKqOMQxdBdJeaT5iLZz(acdeUf*ql+1|AvHs5`UF@!rE%)Y0AKJyv?kQh`_jhuRh~wE5r+d71zcOSV*OiJb$F&W)oy3?$OHx*^d^#Om zSo&j&58`-YOzIpNL`oN!R7*mrMZ4&O*nvo&#C;D{Jgwcfm6P_ zhkf=+ldkk5(L8m`=Ze`eCsQ0j=J7$5(UWmqM;=dNeOhEDeJqKUhdV9u#;pu7WTynp zb8q__n`0dG-N8U1cXPlpviAsryc650RB5AD3wCg)G}~Tlu5g|^UfAmzjVv~Rh5;p7 z)1t`Ef&}d(*3#Mg`?EA|*uqV{r;Uxt*iNh^>X?=5q(07ZvxlIo+O*H4T|JvXmq8O4lpvV^jPBWHfPho24U`u@r9rz;!G zXTkO%#*b6e`ocSLJW~`+3?|2T!RSrWhpsFX&K_sBNuo`%-T_9ebJo~#60b{GOVsr_ zL&VUNRpA2XZ+Qf{_~CbFbTM=GonALw(-gid!teXf;%6DUm`8p!IL=O)0U1gT2U4yH zr&hGdf9vhvXbmYNJC-a^!8HTcr(isZd5^RCly)M@3j90Dq{P0HVN%!piJ8Isqcsg- zu%CJUp?Nqr>1saD{I}0}sWy;(@}gOxws7jY(h$!xZRrl+UiQXKqZ;dzJf6hsXU0AA zjuZDSLF--d#L87T-^sN^%+b}@OS$EmDscT}gV3ec3yRHQ!=%H{`%?hOpCRu}+&R_MI+A=?A@YO{bc{{a1dLwu<SJ^ZYVH7~e+68paLf~Ju_(;DP2i@M%Aj@M7@J9XYi>*~rsh~k{B znQyyv*a7Pk+esah^=->jwW%}VK)a%)Z5ZB<=wdpxI?pe>ZUXPiv=`Tukl861PvW)C zx!up_6;mB9_`jTJ6OL;|%)1~aF-LN;w{FBOv;pI(P|LE%dk0;PcGnJ9%uv^bFJFC( zKj@O>CUuT}Sk_?m{EZq!?NpE*xEuS9ITG}pIG)!x?sjz7p8yJj_x%|2urKFz#F(AB z`jYwkri1(OO{E)d;CPaAM2snOn=lZfP5y!JS z+P`+F)i^j>UbI#5IoVk{b>A-^^$R)3c7&H2@!7V)xL%^`dq~uvI##?clq4t1mwd(? zVa-#IXG3Mq-m(B22wY}kInNI36JrYcP8?5K=S< zgL$WpIXgT(VeJzZ - - - - + + + + + + + + + + + + - - - - - - - + + + + + + + + + + fixed source - 20 + 60 5 - + + + 1.5 0.5 0.5 + + + + + + 2.5 0.5 0.5 + + + + 0.5 0.5 1.5 + + + + + 3.5 0.5 1.5 + + + + + + 1.5 0.5 2.5 + - + - 1.5 0.5 1.5 + 2.5 0.5 2.5 - 7 + 2 4 9 10 300 - 1 2 + 6 7 to to 1 diff --git a/tests/regression_tests/surface_source_write/case-24/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-24/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..5fd380aba2df6506bd02ba99539d9a7aa1686593 GIT binary patch literal 27416 zcmeHPd00*B+is#H4Tc7h2AXAS7S(>5=ae)WA|XN(QpQdkP39pL;TK0`CNkzAQ*lH} zN{UQTBB4azrrvLRcGj}&>-_Uw-!B(?_qNu(?&rDh=Y5~`uAKx28@oQ@1H}cteTj() zhzLl3e@p)9==zeM-8n+OCqEDDdL!8Ny1MIipHSx;K@Q@#pUHQPOLyTb3wI8rc8z;^ z+S&?mjwJZxwd=;cr8-oa=aTUL!bvnS9A|@W16cn#sYOV`P*qIgx$)Vhb4d7BKAK zoWJP%_nhga>y_%|XXnOr9gJtC?g&3ScYfDl#4Fv;>KysM93yAa{QlMH#;$LE^76C% zM1{=x?pM>F9RK6c&(2*qBkKDh>>m&O$@`z3J1sbL%A(+DlV?QCn;*tG$KfaE{W$bv zZA8wGm_9jVnt;oX2mIvykLQFO9Q%{we;oSp+#Vd)eybZHA))UTCDv7E z!h-v{>>M`r$8-C3x%}I~UMEsR#<{uM2yzgJ_w&2z^~ZDm`<`hAK3R4iS=05})b+}H zev@VPf%TuXPr5@&&xrl^BVL$y9?^MxFU~h?B8UJlCTHUM=~q)hP>63Y-zQEGon7-- zA+a7|eruq(m||TRgeSR1QvomZA!X%i%S{HbTqeMf^S!o!m~_R5KeB`z|LQ)^V)^I+h!5;JHFi3?hG3D-Nn8eJH~O8AsJ9z0jP z`qQte4)MgX6JEqiBo}0y>-nbJn!d= zO~SQ_c{11CCDnOO&#g{yX_npI>OZW*SW9Jn!}0QB>;AhJ<%V75*dgmIjmPOJXP2Amd!!cO26i zadDmoyr>8ev6|S9FtP5;=cW8P{Wd%lWx=Ox(|bGM6T-xG#9a6IbtSvjjn#+yepLaZ zkA6Uy^tyMCnbgtM=+=W%(ygG*e}jWg4P70P{E>W8>t0mE%0D#31fCe1Wa%1W&(nK} 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 Date: Sun, 22 Feb 2026 13:51:13 -0600 Subject: [PATCH 13/21] added documentation fixed typos fixed formatting error added documentation --- docs/source/io_formats/settings.rst | 29 ++++++++++++++--- docs/source/usersguide/settings.rst | 32 ++++++++++++++++--- openmc/settings.py | 28 ++++++++-------- .../surface_source_write/test.py | 16 ++++------ 4 files changed, 72 insertions(+), 33 deletions(-) diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index e4ab0e169a1..e4e5db89e14 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -1166,11 +1166,12 @@ attributes/sub-elements: The ```` element triggers OpenMC to bank particles crossing certain surfaces and write out the source bank in a separate file called -``surface_source.h5``. One or multiple surface IDs and one cell ID can be used -to select the surfaces of interest. If no surface IDs are declared, every surface -of the model is eligible to bank particles. In that case, a cell ID (using -either the ``cell``, ``cellfrom`` or ``cellto`` attributes) can be used to select -every surface of a specific cell. This element has the following +``surface_source.h5``. One or multiple surface IDs and one or multiple cell IDs can be used +to select the surfaces of interest. The cell IDs can have direction flags assosciated with them +to further filter the banked particles. Allowed directions are ``to``, ``from`` or ``both``. +If only one cell ID is used in banking, the ``cell``, ``cellfrom`` or ``cellto`` attributes +can be used instead of the ``cells`` and ``directions`` attributes. If no surface IDs are declared, +every surface of the model is eligible to bank particles. This element has the following attributes/sub-elements: :surface_ids: @@ -1206,6 +1207,18 @@ attributes/sub-elements: .. _MCPL: https://mctools.github.io/mcpl/mcpl.pdf + :cells: + A list of integers representing the cell IDs used to determine if particles crossing + identified surfaces are to be banked. + + *Default*: None + + :directions: + A list of strings representing the directions corresponding to the cell IDs. Allowed values are + ``to``, ``from`` or ``both``. Must have the same length as ``cells``. + + *Default*: None + :cell: An integer representing the cell ID used to determine if particles crossing identified surfaces are to be banked. Particles coming from or going to this @@ -1227,9 +1240,15 @@ attributes/sub-elements: *Default*: None +.. note:: The ``cell``, ``cellfrom`` or ``cellto`` attributes cannot be + used simultaneously with ``cells`` attribute. + .. note:: The ``cell``, ``cellfrom`` and ``cellto`` attributes cannot be used simultaneously. +.. note:: If ``cells`` attribute is defined and ``directions`` attribute is not, + all the directions of in ``cells`` will default to ``both``. + .. note:: Surfaces with boundary conditions that are not "transmission" or "vacuum" are not eligible to store any particles when using ``cell``, ``cellfrom`` or ``cellto`` attributes. It is recommended to use surface IDs instead. diff --git a/docs/source/usersguide/settings.rst b/docs/source/usersguide/settings.rst index 5a04fedd70a..309a6e1b582 100644 --- a/docs/source/usersguide/settings.rst +++ b/docs/source/usersguide/settings.rst @@ -323,8 +323,32 @@ crossing any surface of the model will be banked:: settings.surf_source_write = {'max_particles': 10000} -A cell ID can also be used to bank particles that are crossing any surface of -a cell that particles are either coming from or going to:: +A list of cell IDs can also be used to bank particles that are crossing surfaces of +cells that particles are either coming from or going to:: + + settings.surf_source_write = { + 'surfaces_ids': [1, 2, 3], + 'cells': [1, 2] + 'max_particles': 10000 + } + +In this example, particles that are crossing surfaces with IDs of 1, 2, or 3 and +entering or exiting cells with IDs 1 or 2 are banked. + +To account specifically for particles leaving or entering a given cell, +a list of directions can be used to further filter particles banked:: + + settings.surf_source_write = { + 'surfaces_ids': [1, 2, 3], + 'cells': [1, 2] + 'directions': '"to", "from"' + 'max_particles': 10000 + } + +In this example, particles that are crossing surfaces with IDs of 1, 2, or 3 and +entering cell with ID 1 or exiting cell with ID 2 are banked. + +If only one cell is used to filter particles, an alternative syntax can be used to bank particles:: settings.surf_source_write = {'cell': 1, 'max_particles': 10000} @@ -333,10 +357,10 @@ be banked excluding any surface that does not use a 'transmission' or 'vacuum' boundary condition. .. note:: Surfaces with boundary conditions that are not "transmission" or "vacuum" - are not eligible to store any particles when using ``cell``, ``cellfrom`` + are not eligible to store any particles when using ``cells``, ``cell``, ``cellfrom`` or ``cellto`` attributes. It is recommended to use surface IDs instead. -Surface IDs can be used in combination with a cell ID:: +Another example that combines surface IDs with a cell ID:: settings.surf_source_write = { 'cell': 1, diff --git a/openmc/settings.py b/openmc/settings.py index 993d43def48..c811ae1d8ab 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -283,12 +283,20 @@ class Settings: :cells: List of cell IDs used to determine if particles crossing identified surfaces are to be banked. Particles coming from or going to this declared cell will be banked (int) - :cellfrom: List of cell IDs used to determine if particles crossing identified + :directions: List of directions corresponding to cells. Acceptable entries are: + "from", "to", or "both" (str) + :cell: Cell ID used to determine if particles crossing identified + surfaces are to be banked. Particles coming from or going to this + declared cell will be banked (int) ("cell" will be deprecated in the future, + use "cells" instead.) + :cellfrom: Cell ID used to determine if particles crossing identified surfaces are to be banked. Particles coming from this - declared cell will be banked (int) - :cellto: List of cell IDs used to determine if particles crossing identified + declared cell will be banked (int) ("cellfrom" will be deprecated in the future, + use "cells" and "directions" instead.) + :cellto: Cell ID used to determine if particles crossing identified surfaces are to be banked. Particles going to this declared - cell will be banked (int) + cell will be banked (int) ("cellto" will be deprecated in the future, + use "cells" and "directions" instead.) surface_grazing_cutoff : float Surface flux cosine cutoff. If not specified, the default value is 0.001. For more information, see the surface tally section in the theory @@ -297,17 +305,7 @@ class Settings: Surface flux cosine substitution ratio. If not specified, the default value is 0.5. For more information, see the surface tally section in the theory manual. - :directions: List of directions corresponding to cells. - :cell: Cell ID used to determine if particles crossing identified - surfaces are to be banked. Particles coming from or going to this - declared cell will be banked (int) ("cell" will be deprecated in the future, use "cells" instead.) - :cellfrom: Cell ID used to determine if particles crossing identified - surfaces are to be banked. Particles coming from this - declared cell will be banked (int) ("cellfrom" will be deprecated in the future, use "cells" and "directions" instead.) - :cellto: Cell ID used to determine if particles crossing identified - surfaces are to be banked. Particles going to this declared - cell will be banked (int) ("cellto" will be deprecated in the future, use "cells" and "directions" instead.) - Acceptable entries are: "from", "to", or "both" (str) + survival_biasing : bool Indicate whether survival biasing is to be used tabular_legendre : dict diff --git a/tests/regression_tests/surface_source_write/test.py b/tests/regression_tests/surface_source_write/test.py index 96503e123f3..55d118eec71 100644 --- a/tests/regression_tests/surface_source_write/test.py +++ b/tests/regression_tests/surface_source_write/test.py @@ -961,7 +961,7 @@ def test_consistency_low_realization_number(model_1, two_threads, single_process "model_1", {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cell": 2}, ), - ("case-e02", "model_1", {"max_particles": 300, "cell": [3]}), + ("case-e02", "model_1", {"max_particles": 300, "cell": 3}), ( "case-e03", "model_2", @@ -1179,21 +1179,21 @@ def model_dagmc_2(): [ ("case-d01", "model_dagmc_1", {"max_particles": 300}), ("case-d02", "model_dagmc_1", {"max_particles": 300, "surface_ids": [1]}), - ("case-d03", "model_dagmc_1", {"max_particles": 300, "cell": [2]}), + ("case-d03", "model_dagmc_1", {"max_particles": 300, "cell": 2}), ( "case-d04", "model_dagmc_1", - {"max_particles": 300, "surface_ids": [1], "cell": [2]}, + {"max_particles": 300, "surface_ids": [1], "cell": 2}, ), - ("case-d05", "model_dagmc_1", {"max_particles": 300, "cellfrom": [2]}), - ("case-d06", "model_dagmc_1", {"max_particles": 300, "cellto": [2]}), + ("case-d05", "model_dagmc_1", {"max_particles": 300, "cellfrom": 2}), + ("case-d06", "model_dagmc_1", {"max_particles": 300, "cellto": 2}), ( "case-d07", "model_dagmc_2", { "max_particles": 300, "surface_ids": [101, 102, 103, 104, 105, 106], - "cell": [7], + "cell": 7, }, ), ( @@ -1202,7 +1202,7 @@ def model_dagmc_2(): { "max_particles": 300, "surface_ids": [101, 102, 103, 104, 105, 106], - "cell": [8], + "cell": 8, }, ), ], @@ -1238,8 +1238,6 @@ def run_and_count(model, folder, parameter): harness._compare_inputs() harness._run_openmc() harness._test_output_created() - - # count banked particles in surface_source.h5 return len(return_surface_source_data("surface_source.h5")) finally: harness._cleanup() From 31cddb0b1c6a12d5752406525645bcbcdfe4d537 Mon Sep 17 00:00:00 2001 From: MohamedElkamash Date: Sun, 22 Feb 2026 22:24:20 -0600 Subject: [PATCH 14/21] fixed order in creating surface_source_write xml triggering CI fixed order in creating surface_source_write xml --- openmc/settings.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/openmc/settings.py b/openmc/settings.py index c811ae1d8ab..687a276f220 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -1591,6 +1591,10 @@ def _create_surf_source_write_subelement(self, root): str(x) for x in self._surf_source_write["surface_ids"] ) + if "mcpl" in self._surf_source_write: + subelement = ET.SubElement(element, "mcpl") + subelement.text = str(self._surf_source_write["mcpl"]).lower() + for key in ("max_particles", "max_source_files", "cell", "cellfrom", "cellto"): if key in self._surf_source_write: subelement = ET.SubElement(element, key) @@ -1603,11 +1607,6 @@ def _create_surf_source_write_subelement(self, root): str(x) for x in self._surf_source_write[key] ) - if "mcpl" in self._surf_source_write: - subelement = ET.SubElement(element, "mcpl") - subelement.text = str(self._surf_source_write["mcpl"]).lower() - - def _create_collision_track_subelement(self, root): if self._collision_track: element = ET.SubElement(root, "collision_track") From 631d4aec2bc750a02e949605e546fd1d63370116 Mon Sep 17 00:00:00 2001 From: Joffrey Dorville Date: Wed, 11 Mar 2026 16:57:05 -0500 Subject: [PATCH 15/21] Update error messages + unit tests for surface_source_write --- src/settings.cpp | 55 +++++++++++++------ tests/unit_tests/test_surface_source_write.py | 27 ++++++++- 2 files changed, 64 insertions(+), 18 deletions(-) diff --git a/src/settings.cpp b/src/settings.cpp index 762e060f86f..1320bede901 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -933,24 +933,35 @@ void read_settings_xml(pugi::xml_node root) ssw_max_files = 1; } + // Determine if sites are to be stored in MCPL format if (check_for_node(node_ssw, "mcpl")) { surf_mcpl_write = get_node_value_bool(node_ssw, "mcpl"); } - // Get cell information + + // Get cells information from 'cells' if (check_for_node(node_ssw, "cells")) { - // raise an error if the new syntax is mixed with the old syntax + // Error if the new syntax is mixed with the previous syntax if (check_for_node(node_ssw, "cell") || check_for_node(node_ssw, "cellfrom") || check_for_node(node_ssw, "cellto")) { - fatal_error("'cells' cannot be used at the same time with 'cell', " + fatal_error("In the element, 'cells' cannot be " + "used at the same time with 'cell', " "'cellfrom' or 'cellto'."); } + + // Read 'cells' information auto ids = get_node_array(node_ssw, "cells"); + // If 'directions' is declared, retrieve directions if (check_for_node(node_ssw, "directions")) { auto directions = get_node_array(node_ssw, "directions"); + + // Check that 'cells' and 'directions' have the same length if (directions.size() != ids.size()) { - fatal_error("'directions' must have the same length as 'cells'"); + fatal_error("In the element, 'directions' must " + "have the same length as 'cells'."); } + + // Store directions for (std::size_t i {0}; i < ids.size(); ++i) { SSWCellType direction = ssw_cell_type_from_string(directions[i]); auto [it, inserted] = ssw_cells.emplace(ids[i], direction); @@ -960,38 +971,50 @@ void read_settings_xml(pugi::xml_node root) it->second = SSWCellType::Both; } } - } else { // default behavior if 'directions' is not defined + } else { + // If 'directions' is not declared, assume 'both' for every cells for (std::size_t i {0}; i < ids.size(); ++i) { ssw_cells.emplace(ids[i], SSWCellType::Both); } } } else { + // If 'cells' is not declared, get cells information from 'cell', 'cellto' + // or 'cellfrom' instead - will be deprecated in the future + + // Error if 'directions' is set without 'cells' if (check_for_node(node_ssw, "directions")) { - fatal_error("'directions' cannot be used if 'cells' is not defined."); + fatal_error("In the element, 'directions' cannot " + "be used if 'cells' is not defined."); } - // old syntax will be deprecated in the future + + // Cell if (check_for_node(node_ssw, "cell")) { - warning("'cell' is deprecated and will be removed in the future. Use " + warning("In the element, 'cell' is deprecated and " + "will be removed in the future. Please use " "'cells' and 'directions' instead."); ssw_cell_id = std::stoll(get_node_value(node_ssw, "cell")); ssw_cells.emplace(ssw_cell_id, SSWCellType::Both); } + // Cellfrom if (check_for_node(node_ssw, "cellfrom")) { - warning("'cellfrom' is deprecated and will be removed in the future. " - "Use 'cells' and 'directions' instead."); + warning("In the element, 'cellfrom' is deprecated " + "and will be removed in the future. " + "Please use 'cells' and 'directions' instead."); if (ssw_cell_id != C_NONE) { - fatal_error( - "'cell', 'cellfrom' and 'cellto' cannot be used at the same time."); + fatal_error("In the element, 'cell', 'cellfrom' " + "and 'cellto' cannot be used at the same time."); } ssw_cell_id = std::stoll(get_node_value(node_ssw, "cellfrom")); ssw_cells.emplace(ssw_cell_id, SSWCellType::From); } + // Cellto if (check_for_node(node_ssw, "cellto")) { - warning("'cellto' is deprecated and will be removed in the future. Use " + warning("In the element, 'cellto' is deprecated " + "and will be removed in the future. Please use " "'cells' and 'directions' instead."); if (ssw_cell_id != C_NONE) { - fatal_error( - "'cell', 'cellfrom' and 'cellto' cannot be used at the same time."); + fatal_error("In the element, 'cell', 'cellfrom' " + "and 'cellto' cannot be used at the same time."); } ssw_cell_id = std::stoll(get_node_value(node_ssw, "cellto")); ssw_cells.emplace(ssw_cell_id, SSWCellType::To); @@ -1348,7 +1371,7 @@ SSWCellType ssw_cell_type_from_string(std::string_view s) return SSWCellType::To; if (s == "both") return SSWCellType::Both; - throw std::invalid_argument("direction must be 'from', 'to', or 'both'"); + fatal_error("Direction must be 'from', 'to', or 'both'"); } //============================================================================== diff --git a/tests/unit_tests/test_surface_source_write.py b/tests/unit_tests/test_surface_source_write.py index 6f18d32b718..63fa7fbf0c0 100644 --- a/tests/unit_tests/test_surface_source_write.py +++ b/tests/unit_tests/test_surface_source_write.py @@ -33,6 +33,10 @@ def geometry(): {"max_particles": 200, "surface_ids": [2], "cellto": 1}, {"max_particles": 200, "surface_ids": [2], "cellfrom": 1}, {"max_particles": 200, "surface_ids": [2], "max_source_files": 1}, + {"max_particles": 200, "cells": [1]}, + {"max_particles": 200, "surface_ids": [2], "cells": [1]}, + {"max_particles": 200, "cells": [1], "directions": ["to"]}, + {"max_particles": 200, "surface_ids": [2], "cells": [1], "directions": ["to"]}, ], ) def test_xml_serialization(parameter, run_in_tmpdir): @@ -102,8 +106,22 @@ def test_number_surface_source_file_created(max_particles, max_source_files, "using the 'max_particles' parameter to store surface " "source points." ) -ERROR_MSG_2 = "'cell', 'cellfrom' and 'cellto' cannot be used at the same time." - +ERROR_MSG_2 = ( + "In the element, 'cell', 'cellfrom' " + "and 'cellto' cannot be used at the same time." +) +ERROR_MSG_3 = ( + "In the element, 'cells' cannot be used " + "at the same time with 'cell', 'cellfrom' or 'cellto'." +) +ERROR_MSG_4 = ( + "In the element, 'directions' cannot be " + "used if 'cells' is not defined." +) +ERROR_MSG_5 = ( + "In the element, 'directions' must have " + "the same length as 'cells'." +) @pytest.mark.parametrize( "parameter, error", @@ -113,6 +131,11 @@ def test_number_surface_source_file_created(max_particles, max_source_files, ({"max_particles": 200, "cell": 1, "cellfrom": 1}, ERROR_MSG_2), ({"max_particles": 200, "cellto": 1, "cellfrom": 1}, ERROR_MSG_2), ({"max_particles": 200, "cell": 1, "cellto": 1, "cellfrom": 1}, ERROR_MSG_2), + ({"max_particles": 200, "cells": [1], "cell": 1}, ERROR_MSG_3), + ({"max_particles": 200, "cells": [1], "cellto": 1}, ERROR_MSG_3), + ({"max_particles": 200, "cells": [1], "cellfrom": 1}, ERROR_MSG_3), + ({"max_particles": 200, "directions": ["to"]}, ERROR_MSG_4), + ({"max_particles": 200, "cells": [1], "directions": ["to", "from"]}, ERROR_MSG_5), ], ) def test_exceptions(parameter, error, run_in_tmpdir, geometry): From 73bca1867b56b25b71a72dd8736c2c021c779f3c Mon Sep 17 00:00:00 2001 From: Joffrey Dorville Date: Thu, 12 Mar 2026 10:41:51 -0500 Subject: [PATCH 16/21] Explicit dependencies --- src/settings.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/settings.cpp b/src/settings.cpp index 1320bede901..bad91651bc0 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -4,6 +4,8 @@ #include // for ceil, pow #include // for numeric_limits #include +#include +#include #include #ifdef _OPENMP From 19284a7e26490278469a662eae488a255b8c45d9 Mon Sep 17 00:00:00 2001 From: Joffrey Dorville Date: Thu, 12 Mar 2026 10:56:13 -0500 Subject: [PATCH 17/21] Remove duplicate settings declaration and reorder settings --- include/openmc/settings.h | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/include/openmc/settings.h b/include/openmc/settings.h index 914ee10a1f0..d29574dccef 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -182,17 +182,13 @@ extern int64_t ssw_cell_id; //!< Cell id for the surface source //!< write setting extern SSWCellType ssw_cell_type; //!< Type of option for the cell //!< argument of surface source write -extern double surface_grazing_cutoff; //!< surface flux cosine cutoff -extern double surface_grazing_ratio; //!< surface flux substitution ratio - extern vector ssw_cell_ids; //!< Cell ids for the surface source //!< write setting -extern SSWCellType ssw_cell_type; //!< Type of option for the cell - //!< argument of surface source write - extern std::unordered_map - ssw_cells; //!< Cell ids and directions - //!< for the surface source write setting + ssw_cells; //!< Cell ids and directions for the surface source write setting + +extern double surface_grazing_cutoff; //!< surface flux cosine cutoff +extern double surface_grazing_ratio; //!< surface flux substitution ratio extern TemperatureMethod temperature_method; //!< method for choosing temperatures From 8d64ed8a1cd21bb02b04ce0d75fa2998f80bb22d Mon Sep 17 00:00:00 2001 From: Joffrey Dorville Date: Thu, 12 Mar 2026 11:18:51 -0500 Subject: [PATCH 18/21] Remove unused settings --- include/openmc/settings.h | 7 +------ src/finalize.cpp | 1 - src/settings.cpp | 7 +++---- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/include/openmc/settings.h b/include/openmc/settings.h index d29574dccef..9d7204d3c8c 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -178,12 +179,6 @@ extern int64_t ssw_max_particles; //!< maximum number of particles to be //!< banked on surfaces per process extern int64_t ssw_max_files; //!< maximum number of surface source files //!< to be created -extern int64_t ssw_cell_id; //!< Cell id for the surface source - //!< write setting -extern SSWCellType ssw_cell_type; //!< Type of option for the cell - //!< argument of surface source write -extern vector ssw_cell_ids; //!< Cell ids for the surface source - //!< write setting extern std::unordered_map ssw_cells; //!< Cell ids and directions for the surface source write setting diff --git a/src/finalize.cpp b/src/finalize.cpp index d7ff69611c3..8d6b405e72d 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -130,7 +130,6 @@ int openmc_finalize() settings::source_rejection_fraction = 0.05; settings::source_separate = false; settings::source_write = true; - settings::ssw_cell_id = C_NONE; settings::ssw_max_particles = 0; settings::ssw_max_files = 1; settings::survival_biasing = false; diff --git a/src/settings.cpp b/src/settings.cpp index bad91651bc0..1ec1bea40b8 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -4,6 +4,7 @@ #include // for ceil, pow #include // for numeric_limits #include +#include #include #include @@ -136,12 +137,9 @@ std::unordered_set source_write_surf_id; CollisionTrackConfig collision_track_config {}; int64_t ssw_max_particles; int64_t ssw_max_files; -vector ssw_cell_ids; -SSWCellType ssw_cell_type {SSWCellType::None}; +std::unordered_map ssw_cells; double surface_grazing_cutoff {0.001}; double surface_grazing_ratio {0.5}; -int64_t ssw_cell_id {C_NONE}; -std::unordered_map ssw_cells; TemperatureMethod temperature_method {TemperatureMethod::NEAREST}; double temperature_tolerance {10.0}; double temperature_default {293.6}; @@ -982,6 +980,7 @@ void read_settings_xml(pugi::xml_node root) } else { // If 'cells' is not declared, get cells information from 'cell', 'cellto' // or 'cellfrom' instead - will be deprecated in the future + int64_t ssw_cell_id {C_NONE}; // Error if 'directions' is set without 'cells' if (check_for_node(node_ssw, "directions")) { From 61be8bdc67140810dc49368adaf25849ade976c5 Mon Sep 17 00:00:00 2001 From: Joffrey Dorville Date: Thu, 12 Mar 2026 13:41:09 -0500 Subject: [PATCH 19/21] Update documentation --- docs/source/io_formats/settings.rst | 24 +++++++++++------- docs/source/usersguide/settings.rst | 39 ++++++++++++++++++----------- openmc/settings.py | 22 ++++++++-------- 3 files changed, 51 insertions(+), 34 deletions(-) diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index e4e5db89e14..9752c4c5324 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -1168,7 +1168,7 @@ The ```` element triggers OpenMC to bank particles crossing certain surfaces and write out the source bank in a separate file called ``surface_source.h5``. One or multiple surface IDs and one or multiple cell IDs can be used to select the surfaces of interest. The cell IDs can have direction flags assosciated with them -to further filter the banked particles. Allowed directions are ``to``, ``from`` or ``both``. +to further filter the banked particles. Allowed directions are "to", "from" or "both". If only one cell ID is used in banking, the ``cell``, ``cellfrom`` or ``cellto`` attributes can be used instead of the ``cells`` and ``directions`` attributes. If no surface IDs are declared, every surface of the model is eligible to bank particles. This element has the following @@ -1215,7 +1215,7 @@ attributes/sub-elements: :directions: A list of strings representing the directions corresponding to the cell IDs. Allowed values are - ``to``, ``from`` or ``both``. Must have the same length as ``cells``. + "to", "from" or "both". Must have the same length as ``cells``. *Default*: None @@ -1240,18 +1240,24 @@ attributes/sub-elements: *Default*: None -.. note:: The ``cell``, ``cellfrom`` or ``cellto`` attributes cannot be - used simultaneously with ``cells`` attribute. +.. note:: If the ``cells`` attribute is used and the ``directions`` attribute is not, + all directions associated with the cells declared in the ``cells`` atttribute + are set to ``both`` by default. -.. note:: The ``cell``, ``cellfrom`` and ``cellto`` attributes cannot be +.. note:: If only one cell is needed to filter particles, an alternative syntax can be + used to bank particles with the ``cell``, ``cellfrom`` and ``cellto`` attributes. + However, this syntax will be deprecated in the future. + +.. note:: The ``cells``, ``cell``, ``cellfrom`` and ``cellto`` attributes cannot be used simultaneously. -.. note:: If ``cells`` attribute is defined and ``directions`` attribute is not, - all the directions of in ``cells`` will default to ``both``. +.. note:: The ``directions`` attribute cannot be used with any of the ``cell``, + ``cellfrom`` and ``cellto`` attributes. .. note:: Surfaces with boundary conditions that are not "transmission" or "vacuum" - are not eligible to store any particles when using ``cell``, ``cellfrom`` - or ``cellto`` attributes. It is recommended to use surface IDs instead. + are not eligible to store any particles when using ``cells``, ``cell``, + ``cellfrom`` or ``cellto`` attributes. It is recommended to use surface + IDs instead. ------------------------------------ ```` Element diff --git a/docs/source/usersguide/settings.rst b/docs/source/usersguide/settings.rst index 309a6e1b582..20889c654e1 100644 --- a/docs/source/usersguide/settings.rst +++ b/docs/source/usersguide/settings.rst @@ -328,37 +328,48 @@ cells that particles are either coming from or going to:: settings.surf_source_write = { 'surfaces_ids': [1, 2, 3], - 'cells': [1, 2] + 'cells': [1, 2], 'max_particles': 10000 } In this example, particles that are crossing surfaces with IDs of 1, 2, or 3 and -entering or exiting cells with IDs 1 or 2 are banked. +entering or exiting cells with IDs 1 or 2 will be banked excluding any surface +that does not use a 'transmission' or 'vacuum' boundary condition. + +.. note:: Surfaces with boundary conditions that are not "transmission" or "vacuum" + are not eligible to store any particles when using ``cells``, ``cell``, ``cellfrom`` + or ``cellto`` attributes. It is recommended to use surface IDs instead. + +The constraint declared through the ``cells`` attribute is purely disjunctive. +This means that a particle crossing a target surface will be banked if at least one of +the clauses associated with all declared cells is true. In other terms, the logic inside +the ``cells`` attribute corresponds to an "OR" relationship, and not an "AND". To account specifically for particles leaving or entering a given cell, -a list of directions can be used to further filter particles banked:: +a list of directions can also be declared:: settings.surf_source_write = { 'surfaces_ids': [1, 2, 3], - 'cells': [1, 2] - 'directions': '"to", "from"' + 'cells': [1, 2], + 'directions': ["to", "from"], 'max_particles': 10000 } In this example, particles that are crossing surfaces with IDs of 1, 2, or 3 and -entering cell with ID 1 or exiting cell with ID 2 are banked. +entering cell with ID 1 or exiting cell with ID 2 will be banked. + +.. note:: + + If only one cell is needed to filter particles, an alternative syntax can be used to bank particles + with the ``cell``, ``cellfrom`` and ``cellto`` attributes. However, this syntax will be deprecated + in the future. -If only one cell is used to filter particles, an alternative syntax can be used to bank particles:: +For a single cell, particles can be banked using the ``cell`` attribute:: settings.surf_source_write = {'cell': 1, 'max_particles': 10000} In this example, particles that are crossing any surface that bounds cell 1 will -be banked excluding any surface that does not use a 'transmission' or 'vacuum' -boundary condition. - -.. note:: Surfaces with boundary conditions that are not "transmission" or "vacuum" - are not eligible to store any particles when using ``cells``, ``cell``, ``cellfrom`` - or ``cellto`` attributes. It is recommended to use surface IDs instead. +be banked. Another example that combines surface IDs with a cell ID:: @@ -385,7 +396,7 @@ or particles going to a cell:: 'max_particles': 10000 } -.. note:: The ``cell``, ``cellfrom`` and ``cellto`` attributes cannot be +.. note:: The ``cells``, ``cell``, ``cellfrom`` and ``cellto`` attributes cannot be used simultaneously. To generate more than one surface source files when the maximum number of stored diff --git a/openmc/settings.py b/openmc/settings.py index 687a276f220..116c174bc81 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -281,22 +281,23 @@ class Settings: :max_source_files: Maximum number of surface source files to be created (int) :mcpl: Output in the form of an MCPL-file (bool) :cells: List of cell IDs used to determine if particles crossing identified - surfaces are to be banked. Particles coming from or going to this - declared cell will be banked (int) + surfaces are to be banked. Particles coming from or going to these + declared cells will be banked depending on the requested direction + via the 'directions' keyword or both directions by default (int) :directions: List of directions corresponding to cells. Acceptable entries are: - "from", "to", or "both" (str) + "from", "to", or "both" (str) :cell: Cell ID used to determine if particles crossing identified surfaces are to be banked. Particles coming from or going to this declared cell will be banked (int) ("cell" will be deprecated in the future, - use "cells" instead.) + please use "cells" instead.) :cellfrom: Cell ID used to determine if particles crossing identified surfaces are to be banked. Particles coming from this declared cell will be banked (int) ("cellfrom" will be deprecated in the future, - use "cells" and "directions" instead.) + please use "cells" and "directions" instead.) :cellto: Cell ID used to determine if particles crossing identified surfaces are to be banked. Particles going to this declared cell will be banked (int) ("cellto" will be deprecated in the future, - use "cells" and "directions" instead.) + please use "cells" and "directions" instead.) surface_grazing_cutoff : float Surface flux cosine cutoff. If not specified, the default value is 0.001. For more information, see the surface tally section in the theory @@ -902,8 +903,7 @@ def surf_source_write(self, surf_source_write: dict): cv.check_type("directions corresponding to cells (from, to or both)", value, Iterable, str) for direction in value: if (direction not in ["from", "to", "both"]): - msg = "allowed values for direction: 'from', 'to', 'both' " - raise ValueError(msg) + raise ValueError("Allowed values for directions are: 'from', 'to', or 'both'.") elif key == "mcpl": cv.check_type("write to an MCPL-format file", value, bool) elif key in ("max_particles", "max_source_files", "cell", "cellfrom", "cellto"): @@ -2113,8 +2113,9 @@ def _surf_source_write_from_xml_element(self, root): elem = root.find('surf_source_write') if elem is None: return - for key in ('surface_ids', 'max_particles', 'max_source_files', 'mcpl', 'cells', 'directions', 'cell', 'cellto', 'cellfrom'): - if key in ['surface_ids', 'cells']: + for key in ('surface_ids', 'max_particles', 'max_source_files', 'mcpl', 'cells', + 'directions', 'cell', 'cellto', 'cellfrom'): + if key in ('surface_ids', 'cells'): value = get_elem_list(elem, key, int) elif key == 'directions': value = get_elem_list(elem, key, str) @@ -2127,7 +2128,6 @@ def _surf_source_write_from_xml_element(self, root): value = int(value) self.surf_source_write[key] = value - def _collision_track_from_xml_element(self, root): elem = root.find('collision_track') if elem is not None: From f27bd33c743c1d0a9850070b55209cde885e3500 Mon Sep 17 00:00:00 2001 From: Joffrey Dorville Date: Thu, 12 Mar 2026 15:39:50 -0500 Subject: [PATCH 20/21] Check that the bank is not full early in add_surf_source_to_bank() --- src/particle.cpp | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/particle.cpp b/src/particle.cpp index 00cd338f645..22eb2e9898d 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -890,27 +890,35 @@ void Particle::update_neutron_xs( //============================================================================== void add_surf_source_to_bank(Particle& p, const Surface& surf) { - if (simulation::current_batch <= settings::n_inactive) { + if (simulation::current_batch <= settings::n_inactive || + simulation::surf_source_bank.full()) { return; } - bool add_site = true; // add the site if 'cells' is not defined + // Add the site if no cells have been requested (via 'cells', 'cell', + // 'cellfrom' or 'cellto') + bool add_site = true; - // If 'cells' is defined + // If cells have been requested (via 'cells', 'cell', 'cellfrom' or 'cellto') if (!settings::ssw_cells.empty()) { + + // Leave if other boundary condition than vacuum if (surf.bc_ && surf.bc_->type() != "vacuum") { - // Leave if other boundary condition than vacuum return; } - add_site = false; // we assume all cell-direction pairs are invalid till one - // of them passes all the tests + + // The site will only be added if at least one cell-direction pair is valid + add_site = false; + + // Looping through all cell-direction pairs for (auto& cell : settings::ssw_cells) { // Retrieve cell index and storage type int cell_idx = model::cell_map[cell.first]; SSWCellType direction = cell.second; + + // Skip if cellto with vacuum boundary condition if (surf.bc_ && surf.bc_->type() == "vacuum" && direction == SSWCellType::To) { - // skip if cellto with vacuum boundary condition continue; } @@ -958,7 +966,7 @@ void add_surf_source_to_bank(Particle& p, const Surface& surf) continue; } } - // if a cell-direction pair survived all the checks we add the site and + // If a cell-direction pair survived all the checks we add the site and // terminate the loop add_site = true; break; From 6da8232af81b45794ff5d5c9c6e8015b90082c9d Mon Sep 17 00:00:00 2001 From: Joffrey Dorville Date: Fri, 13 Mar 2026 16:12:42 -0500 Subject: [PATCH 21/21] Formatting --- .../surface_source_write/test.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/tests/regression_tests/surface_source_write/test.py b/tests/regression_tests/surface_source_write/test.py index 55d118eec71..dffb1dc9c27 100644 --- a/tests/regression_tests/surface_source_write/test.py +++ b/tests/regression_tests/surface_source_write/test.py @@ -24,8 +24,8 @@ - model_1: cylindrical core in 2 boxes (vacuum and transmission BC), - model_2: cylindrical core in 1 box (vacuum BC), - model_3: cylindrical core in 1 box (reflective BC), -- model_4: cylindrical core in 1 box (periodic BC). -- model_5: 4*1*3 array of boxes (transmission BC) +- model_4: cylindrical core in 1 box (periodic BC), +- model_5: 4*1*3 array of boxes (transmission BC). Two models including DAGMC geometries are also used, based on the mesh file 'dagmc.h5m' available from tests/regression_tests/dagmc/legacy: @@ -616,10 +616,6 @@ def model_5(): openmc.reset_auto_ids() model = openmc.Model() - # ============================================================================= - # Materials - # ============================================================================= - # ============================================================================= # Geometry # ============================================================================= @@ -646,7 +642,8 @@ def model_5(): cells = [[None]*nz for _ in range(nx)] for j in range(nz): for i in range(nx): - cells[i][j] = openmc.Cell(region = +x_planes[i] & -x_planes[i+1] & +y_planes[0] & -y_planes[-1] & +z_planes[j] & -z_planes[j+1]) + cells[i][j] = (openmc.Cell(region = +x_planes[i] & -x_planes[i+1] + & +y_planes[0] & -y_planes[-1] & +z_planes[j] & -z_planes[j+1])) cells_1D = [cells[i][j] for j in range(len(cells[0])) for i in range(len(cells))] root = openmc.Universe(cells=cells_1D) @@ -1221,7 +1218,10 @@ def test_surface_source_cell_dagmc( harness.main() def test_surface_source_multiple_cells(model_5, single_thread, single_process): - """Test that the number of particles entering two cells equal the sum of the number of particles entering each individual cell""" + """Test that the number of particles entering two cells equal the sum of + the number of particles entering each individual cell + + """ assert os.environ["OMP_NUM_THREADS"] == "1" assert config["mpi_np"] == "1" @@ -1279,7 +1279,8 @@ def run_and_read(parameter, subdir): model = model_5 model.settings.surf_source_write = parameter - harness = SurfaceSourceWriteTestHarness("statepoint.5.h5", model=model, workdir=str(run_dir)) + harness = SurfaceSourceWriteTestHarness( + "statepoint.5.h5", model=model, workdir=str(run_dir)) base = os.getcwd() try: @@ -1298,4 +1299,4 @@ def run_and_read(parameter, subdir): for k, p in enumerate(params[1:], start=1): out = run_and_read(p, f"run{k}") assert out.shape == ref.shape - assert np.array_equal(out, ref) \ No newline at end of file + assert np.array_equal(out, ref)