Repository: openmc-dev/openmc

Issue:
Expose a `max_events` setting. 
## Description
For deep shielding problems and/or problems using weight windows, particles may exceed the maximum number of allowed events. This value is currently hard-coded in https://github.com/openmc-dev/openmc/blob/87e00f76ed6fd30b0dc10e8c86daba1f4ff7dff5/include/openmc/particle_data.h#L31-L32. 

## Compatibility
Exposing this setting should be an additive change and won't break backward compatibility.


Discussion:

Resolving patch:
diff --git a/openmc/settings.py b/openmc/settings.py
--- a/openmc/settings.py
+++ b/openmc/settings.py
@@ -109,6 +109,10 @@ class Settings:
         parallelism.
 
         .. versionadded:: 0.12
+    max_particle_events : int
+        Maximum number of allowed particle events per source particle.
+
+        .. versionadded:: 0.14.1
     max_order : None or int
         Maximum scattering order to apply globally when in multi-group mode.
     max_splits : int
@@ -334,6 +338,7 @@ def __init__(self, **kwargs):
 
         self._event_based = None
         self._max_particles_in_flight = None
+        self._max_particle_events = None
         self._write_initial_source = None
         self._weight_windows = cv.CheckedList(WeightWindows, 'weight windows')
         self._weight_window_generators = cv.CheckedList(WeightWindowGenerator, 'weight window generators')
@@ -938,6 +943,16 @@ def max_particles_in_flight(self, value: int):
         cv.check_greater_than('max particles in flight', value, 0)
         self._max_particles_in_flight = value
 
+    @property
+    def max_particle_events(self) -> int:
+        return self._max_particle_events
+
+    @max_particle_events.setter
+    def max_particle_events(self, value: int):
+        cv.check_type('max particle events', value, Integral)
+        cv.check_greater_than('max particle events', value, 0)
+        self._max_particle_events = value
+
     @property
     def write_initial_source(self) -> bool:
         return self._write_initial_source
@@ -1341,6 +1356,11 @@ def _create_max_particles_in_flight_subelement(self, root):
             elem = ET.SubElement(root, "max_particles_in_flight")
             elem.text = str(self._max_particles_in_flight).lower()
 
+    def _create_max_events_subelement(self, root):
+        if self._max_particle_events is not None:
+            elem = ET.SubElement(root, "max_particle_events")
+            elem.text = str(self._max_particle_events).lower()
+
     def _create_material_cell_offsets_subelement(self, root):
         if self._material_cell_offsets is not None:
             elem = ET.SubElement(root, "material_cell_offsets")
@@ -1719,6 +1739,11 @@ def _max_particles_in_flight_from_xml_element(self, root):
         if text is not None:
             self.max_particles_in_flight = int(text)
 
+    def _max_particle_events_from_xml_element(self, root):
+        text = get_text(root, 'max_particle_events')
+        if text is not None:
+            self.max_particle_events = int(text)
+
     def _material_cell_offsets_from_xml_element(self, root):
         text = get_text(root, 'material_cell_offsets')
         if text is not None:
@@ -1820,6 +1845,7 @@ def to_xml_element(self, mesh_memo=None):
         self._create_delayed_photon_scaling_subelement(element)
         self._create_event_based_subelement(element)
         self._create_max_particles_in_flight_subelement(element)
+        self._create_max_events_subelement(element)
         self._create_material_cell_offsets_subelement(element)
         self._create_log_grid_bins_subelement(element)
         self._create_write_initial_source_subelement(element)
@@ -1923,6 +1949,7 @@ def from_xml_element(cls, elem, meshes=None):
         settings._delayed_photon_scaling_from_xml_element(elem)
         settings._event_based_from_xml_element(elem)
         settings._max_particles_in_flight_from_xml_element(elem)
+        settings._max_particle_events_from_xml_element(elem)
         settings._material_cell_offsets_from_xml_element(elem)
         settings._log_grid_bins_from_xml_element(elem)
         settings._write_initial_source_from_xml_element(elem)


Test patch:
diff --git a/tests/unit_tests/test_settings.py b/tests/unit_tests/test_settings.py
--- a/tests/unit_tests/test_settings.py
+++ b/tests/unit_tests/test_settings.py
@@ -27,8 +27,8 @@ def test_export_to_xml(run_in_tmpdir):
     s.survival_biasing = True
     s.cutoff = {'weight': 0.25, 'weight_avg': 0.5, 'energy_neutron': 1.0e-5,
                 'energy_photon': 1000.0, 'energy_electron': 1.0e-5,
-                'energy_positron': 1.0e-5, 'time_neutron': 1.0e-5, 
-                'time_photon': 1.0e-5, 'time_electron': 1.0e-5, 
+                'energy_positron': 1.0e-5, 'time_neutron': 1.0e-5,
+                'time_photon': 1.0e-5, 'time_electron': 1.0e-5,
                 'time_positron': 1.0e-5}
     mesh = openmc.RegularMesh()
     mesh.lower_left = (-10., -10., -10.)
@@ -59,6 +59,8 @@ def test_export_to_xml(run_in_tmpdir):
     s.write_initial_source = True
     s.weight_window_checkpoints = {'surface': True, 'collision': False}
 
+    s.max_particle_events = 100
+
     # Make sure exporting XML works
     s.export_to_xml()
 
@@ -92,7 +94,7 @@ def test_export_to_xml(run_in_tmpdir):
     assert s.cutoff == {'weight': 0.25, 'weight_avg': 0.5,
                         'energy_neutron': 1.0e-5, 'energy_photon': 1000.0,
                         'energy_electron': 1.0e-5, 'energy_positron': 1.0e-5,
-                        'time_neutron': 1.0e-5, 'time_photon': 1.0e-5, 
+                        'time_neutron': 1.0e-5, 'time_photon': 1.0e-5,
                         'time_electron': 1.0e-5, 'time_positron': 1.0e-5}
     assert isinstance(s.entropy_mesh, openmc.RegularMesh)
     assert s.entropy_mesh.lower_left == [-10., -10., -10.]
@@ -128,3 +130,4 @@ def test_export_to_xml(run_in_tmpdir):
     assert vol.lower_left == (-10., -10., -10.)
     assert vol.upper_right == (10., 10., 10.)
     assert s.weight_window_checkpoints == {'surface': True, 'collision': False}
+    assert s.max_particle_events == 100