From e6c5f56ee284530f447e4578fc97f8f899298cac Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Mon, 10 Jun 2024 18:18:04 +0100 Subject: [PATCH 001/184] Fixing plot xs for when plotting element string reaction (#3029) --- openmc/plotter.py | 13 +++++++++---- tests/unit_tests/test_plotter.py | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/openmc/plotter.py b/openmc/plotter.py index 97ca5f92975..aa3825f5e1a 100644 --- a/openmc/plotter.py +++ b/openmc/plotter.py @@ -59,10 +59,15 @@ def _get_legend_label(this, type): """Gets a label for the element or nuclide or material and reaction plotted""" if isinstance(this, str): if type in openmc.data.DADZ: - z, a, m = openmc.data.zam(this) - da, dz = openmc.data.DADZ[type] - gnds_name = openmc.data.gnds_name(z + dz, a + da, m) - return f'{this} {type} {gnds_name}' + if this in ELEMENT_NAMES: + return f'{this} {type}' + else: # this is a nuclide so the legend can contain more information + z, a, m = openmc.data.zam(this) + da, dz = openmc.data.DADZ[type] + gnds_name = openmc.data.gnds_name(z + dz, a + da, m) + # makes a string with nuclide reaction and new nuclide + # For example "Be9 (n,2n) Be8" + return f'{this} {type} {gnds_name}' return f'{this} {type}' elif this.name == '': return f'Material {this.id} {type}' diff --git a/tests/unit_tests/test_plotter.py b/tests/unit_tests/test_plotter.py index 2c195c5e120..0220cec3e71 100644 --- a/tests/unit_tests/test_plotter.py +++ b/tests/unit_tests/test_plotter.py @@ -75,7 +75,7 @@ def test_calculate_cexs_with_materials(test_mat): @pytest.mark.parametrize("this", ["Be", "Be9"]) def test_plot_xs(this): from matplotlib.figure import Figure - assert isinstance(openmc.plot_xs({this: ['total', 'elastic']}), Figure) + assert isinstance(openmc.plot_xs({this: ['total', 'elastic', 16, '(n,2n)']}), Figure) def test_plot_xs_mat(test_mat): From 88f3e4d21f3c86ba76858cca05cb4a36d453d0ca Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 10 Jun 2024 12:19:46 -0500 Subject: [PATCH 002/184] Allow mesh material homogenization to include or exclude voids (#3000) --- openmc/mesh.py | 7 +++++++ tests/unit_tests/test_mesh.py | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/openmc/mesh.py b/openmc/mesh.py index 48263c2055c..f0c0d7414fe 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -152,6 +152,7 @@ def get_homogenized_materials( model: openmc.Model, n_samples: int = 10_000, prn_seed: Optional[int] = None, + include_void: bool = True, **kwargs ) -> List[openmc.Material]: """Generate homogenized materials over each element in a mesh. @@ -168,6 +169,8 @@ def get_homogenized_materials( prn_seed : int, optional Pseudorandom number generator (PRNG) seed; if None, one will be generated randomly. + include_void : bool, optional + Whether homogenization should include voids. **kwargs Keyword-arguments passed to :func:`openmc.lib.init`. @@ -222,6 +225,10 @@ def get_homogenized_materials( material_ids.pop(index_void) volumes.pop(index_void) + # If void should be excluded, adjust total volume + if not include_void: + total_volume = sum(volumes) + # Compute volume fractions volume_fracs = np.array(volumes) / total_volume diff --git a/tests/unit_tests/test_mesh.py b/tests/unit_tests/test_mesh.py index 5bacd5fc693..0f7f71ceae1 100644 --- a/tests/unit_tests/test_mesh.py +++ b/tests/unit_tests/test_mesh.py @@ -386,3 +386,9 @@ def test_mesh_get_homogenized_materials(): # Mesh element that overlaps void should have half density assert m4.get_mass_density('H1') == pytest.approx(0.5, rel=1e-2) + + # If not including void, density of homogenized material should be same as + # original material + m5, = mesh_void.get_homogenized_materials( + model, n_samples=1000, include_void=False) + assert m5.get_mass_density('H1') == pytest.approx(1.0) From 12a278b1acc6c3ba85c6ec07ff6657ca476c4cb6 Mon Sep 17 00:00:00 2001 From: kmeag <143649668+kmeag@users.noreply.github.com> Date: Mon, 10 Jun 2024 15:54:59 -0400 Subject: [PATCH 003/184] Enforce lower_left in lattice geometry (#2982) Co-authored-by: Paul Romano --- openmc/lattice.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openmc/lattice.py b/openmc/lattice.py index f7f9da8ea8e..51806856051 100644 --- a/openmc/lattice.py +++ b/openmc/lattice.py @@ -884,6 +884,10 @@ def create_xml_subelement(self, xml_element, memo=None): dimension = ET.SubElement(lattice_subelement, "dimension") dimension.text = ' '.join(map(str, self.shape)) + # Make sure lower_left has been specified + if self.lower_left is None: + raise ValueError(f"Lattice {self.id} does not have lower_left specified.") + # Export Lattice lower left lower_left = ET.SubElement(lattice_subelement, "lower_left") lower_left.text = ' '.join(map(str, self._lower_left)) From e971bd121313b1e9c7113385751ffd9c793e4a95 Mon Sep 17 00:00:00 2001 From: hsameer481 <165707775+hsameer481@users.noreply.github.com> Date: Mon, 10 Jun 2024 17:31:00 -0400 Subject: [PATCH 004/184] added error checking on cylindrical mesh (#2977) Co-authored-by: Paul Romano --- openmc/mesh.py | 21 +++++++++-- tests/unit_tests/mesh_to_vtk/test_vtk_dims.py | 2 +- tests/unit_tests/test_mesh.py | 36 +++++++++++++++++++ 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/openmc/mesh.py b/openmc/mesh.py index f0c0d7414fe..215c9192336 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -1388,6 +1388,8 @@ def r_grid(self): @r_grid.setter def r_grid(self, grid): cv.check_type('mesh r_grid', grid, Iterable, Real) + cv.check_length('mesh r_grid', grid, 2) + cv.check_increasing('mesh r_grid', grid) self._r_grid = np.asarray(grid, dtype=float) @property @@ -1397,7 +1399,12 @@ def phi_grid(self): @phi_grid.setter def phi_grid(self, grid): cv.check_type('mesh phi_grid', grid, Iterable, Real) - self._phi_grid = np.asarray(grid, dtype=float) + cv.check_length('mesh phi_grid', grid, 2) + cv.check_increasing('mesh phi_grid', grid) + grid = np.asarray(grid, dtype=float) + if np.any((grid < 0.0) | (grid > 2*pi)): + raise ValueError("phi_grid values must be in [0, 2π].") + self._phi_grid = grid @property def z_grid(self): @@ -1406,6 +1413,8 @@ def z_grid(self): @z_grid.setter def z_grid(self, grid): cv.check_type('mesh z_grid', grid, Iterable, Real) + cv.check_length('mesh z_grid', grid, 2) + cv.check_increasing('mesh z_grid', grid) self._z_grid = np.asarray(grid, dtype=float) @property @@ -1840,7 +1849,10 @@ def theta_grid(self, grid): cv.check_type('mesh theta_grid', grid, Iterable, Real) cv.check_length('mesh theta_grid', grid, 2) cv.check_increasing('mesh theta_grid', grid) - self._theta_grid = np.asarray(grid, dtype=float) + grid = np.asarray(grid, dtype=float) + if np.any((grid < 0.0) | (grid > pi)): + raise ValueError("theta_grid values must be in [0, π].") + self._theta_grid = grid @property def phi_grid(self): @@ -1851,7 +1863,10 @@ def phi_grid(self, grid): cv.check_type('mesh phi_grid', grid, Iterable, Real) cv.check_length('mesh phi_grid', grid, 2) cv.check_increasing('mesh phi_grid', grid) - self._phi_grid = np.asarray(grid, dtype=float) + grid = np.asarray(grid, dtype=float) + if np.any((grid < 0.0) | (grid > 2*pi)): + raise ValueError("phi_grid values must be in [0, 2π].") + self._phi_grid = grid @property def _grids(self): diff --git a/tests/unit_tests/mesh_to_vtk/test_vtk_dims.py b/tests/unit_tests/mesh_to_vtk/test_vtk_dims.py index baa54498438..0b6acf5aabf 100644 --- a/tests/unit_tests/mesh_to_vtk/test_vtk_dims.py +++ b/tests/unit_tests/mesh_to_vtk/test_vtk_dims.py @@ -153,7 +153,7 @@ def test_write_data_to_vtk_round_trip(run_in_tmpdir): smesh = openmc.SphericalMesh( r_grid=(0.0, 1.0, 2.0), - theta_grid=(0.0, 2.0, 4.0, 5.0), + theta_grid=(0.0, 0.5, 1.0, 2.0), phi_grid=(0.0, 3.0, 6.0), ) rmesh = openmc.RegularMesh() diff --git a/tests/unit_tests/test_mesh.py b/tests/unit_tests/test_mesh.py index 0f7f71ceae1..ed08be816f5 100644 --- a/tests/unit_tests/test_mesh.py +++ b/tests/unit_tests/test_mesh.py @@ -189,6 +189,42 @@ def test_CylindricalMesh_initiation(): openmc.SphericalMesh(('🧇', '🥞')) +def test_invalid_cylindrical_mesh_errors(): + # Test invalid r_grid values + with pytest.raises(ValueError): + openmc.CylindricalMesh(r_grid=[5, 1], phi_grid=[0, pi], z_grid=[0, 10]) + + with pytest.raises(ValueError): + openmc.CylindricalMesh(r_grid=[1, 2, 4, 3], phi_grid=[0, pi], z_grid=[0, 10]) + + with pytest.raises(ValueError): + openmc.CylindricalMesh(r_grid=[1], phi_grid=[0, pi], z_grid=[0, 10]) + + # Test invalid phi_grid values + with pytest.raises(ValueError): + openmc.CylindricalMesh(r_grid=[0, 1, 2], phi_grid=[-1, 3], z_grid=[0, 10]) + + with pytest.raises(ValueError): + openmc.CylindricalMesh( + r_grid=[0, 1, 2], + phi_grid=[0, 2*pi + 0.1], + z_grid=[0, 10] + ) + + with pytest.raises(ValueError): + openmc.CylindricalMesh(r_grid=[0, 1, 2], phi_grid=[pi], z_grid=[0, 10]) + + # Test invalid z_grid values + with pytest.raises(ValueError): + openmc.CylindricalMesh(r_grid=[0, 1, 2], phi_grid=[0, pi], z_grid=[5]) + + with pytest.raises(ValueError): + openmc.CylindricalMesh(r_grid=[0, 1, 2], phi_grid=[0, pi], z_grid=[5, 1]) + + with pytest.raises(ValueError): + openmc.CylindricalMesh(r_grid=[1, 2, 4, 3], phi_grid=[0, pi], z_grid=[0, 10, 5]) + + def test_centroids(): # regular mesh mesh = openmc.RegularMesh() From 8b33615ac2d016ffde636f2a7f918c883094a3f0 Mon Sep 17 00:00:00 2001 From: Isaac Meyer Date: Tue, 11 Jun 2024 22:37:28 -0400 Subject: [PATCH 005/184] fix shannon entropy broken link (#3034) --- docs/source/methods/eigenvalue.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/methods/eigenvalue.rst b/docs/source/methods/eigenvalue.rst index 41bf8654920..b63723c0264 100644 --- a/docs/source/methods/eigenvalue.rst +++ b/docs/source/methods/eigenvalue.rst @@ -142,7 +142,7 @@ than unity. By ensuring that the expected number of fission sites in each mesh cell is constant, the collision density across all cells, and hence the variance of tallies, is more uniform than it would be otherwise. -.. _Shannon entropy: https://laws.lanl.gov/vhosts/mcnp.lanl.gov/pdf_files/la-ur-06-3737.pdf +.. _Shannon entropy: https://mcnp.lanl.gov/pdf_files/TechReport_2006_LANL_LA-UR-06-3737_Brown.pdf .. [Lieberoth] J. Lieberoth, "A Monte Carlo Technique to Solve the Static Eigenvalue Problem of the Boltzmann Transport Equation," *Nukleonik*, **11**, From 1f3280461fa41e22dbc563324c98fcf53179d61b Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 11 Jun 2024 21:49:01 -0500 Subject: [PATCH 006/184] Make sure skewed dataset is cast to bool properly (#3001) --- openmc/data/njoy.py | 2 +- openmc/data/thermal_angle_energy.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openmc/data/njoy.py b/openmc/data/njoy.py index c1dcbab17f5..ac1b5e345ea 100644 --- a/openmc/data/njoy.py +++ b/openmc/data/njoy.py @@ -453,7 +453,7 @@ def make_ace_thermal(filename, filename_thermal, temperatures=None, error : float, optional Fractional error tolerance for NJOY processing iwt : int - `iwt` parameter used in NJOR/ACER card 9 + `iwt` parameter used in NJOY/ACER card 9 evaluation : openmc.data.endf.Evaluation, optional If the ENDF neutron sublibrary file contains multiple material evaluations, this argument indicates which evaluation to use. diff --git a/openmc/data/thermal_angle_energy.py b/openmc/data/thermal_angle_energy.py index 0dd2a0b7a51..4789ebcc6f2 100644 --- a/openmc/data/thermal_angle_energy.py +++ b/openmc/data/thermal_angle_energy.py @@ -225,7 +225,7 @@ def from_hdf5(cls, group): """ energy_out = group['energy_out'][()] mu_out = group['mu_out'][()] - skewed = bool(group['skewed']) + skewed = bool(group['skewed'][()]) return cls(energy_out, mu_out, skewed) From dcb80338c5794490a99db58bbcdc7c25a9dd0439 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 11 Jun 2024 21:56:31 -0500 Subject: [PATCH 007/184] Eliminate deprecation warnings from scipy and pandas (#2951) --- openmc/data/photon.py | 12 +++++++----- openmc/mgxs_library.py | 18 ++++++++++++------ tests/unit_tests/test_data_photon.py | 7 ++++--- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/openmc/data/photon.py b/openmc/data/photon.py index c9d51f06238..0d0419f0724 100644 --- a/openmc/data/photon.py +++ b/openmc/data/photon.py @@ -124,7 +124,7 @@ class AtomicRelaxation(EqualityMixin): Dictionary indicating the number of electrons in a subshell when neutral (values) for given subshells (keys). The subshells should be given as strings, e.g., 'K', 'L1', 'L2', etc. - transitions : pandas.DataFrame + transitions : dict of str to pandas.DataFrame Dictionary indicating allowed transitions and their probabilities (values) for given subshells (keys). The subshells should be given as strings, e.g., 'K', 'L1', 'L2', etc. The transitions are represented as @@ -363,8 +363,9 @@ def from_hdf5(cls, group): df = pd.DataFrame(sub_group['transitions'][()], columns=columns) # Replace float indexes back to subshell strings - df[columns[:2]] = df[columns[:2]].replace( - np.arange(float(len(_SUBSHELLS))), _SUBSHELLS) + with pd.option_context('future.no_silent_downcasting', True): + df[columns[:2]] = df[columns[:2]].replace( + np.arange(float(len(_SUBSHELLS))), _SUBSHELLS) transitions[shell] = df return cls(binding_energy, num_electrons, transitions) @@ -387,8 +388,9 @@ def to_hdf5(self, group, shell): # Write transition data with replacements if shell in self.transitions: - df = self.transitions[shell].replace( - _SUBSHELLS, range(len(_SUBSHELLS))) + with pd.option_context('future.no_silent_downcasting', True): + df = self.transitions[shell].replace( + _SUBSHELLS, range(len(_SUBSHELLS))) group.create_dataset('transitions', data=df.values.astype(float)) diff --git a/openmc/mgxs_library.py b/openmc/mgxs_library.py index 642da83d170..d1b96289081 100644 --- a/openmc/mgxs_library.py +++ b/openmc/mgxs_library.py @@ -3,7 +3,7 @@ import h5py import numpy as np -from scipy.integrate import simps +import scipy.integrate from scipy.interpolate import interp1d from scipy.special import eval_legendre @@ -1823,6 +1823,12 @@ def convert_scatter_format(self, target_format, target_order=None): # Reset and re-generate XSdata.xs_shapes with the new scattering format xsdata._xs_shapes = None + # scipy 1.11+ prefers 'simpson', whereas older versions use 'simps' + if hasattr(scipy.integrate, 'simpson'): + integrate = scipy.integrate.simpson + else: + integrate = scipy.integrate.simps + for i, temp in enumerate(xsdata.temperatures): orig_data = self._scatter_matrix[i] new_shape = orig_data.shape[:-1] + (xsdata.num_orders,) @@ -1860,7 +1866,7 @@ def convert_scatter_format(self, target_format, target_order=None): table_fine[..., imu] += ((l + 0.5) * eval_legendre(l, mu_fine[imu]) * orig_data[..., l]) - new_data[..., h_bin] = simps(table_fine, mu_fine) + new_data[..., h_bin] = integrate(table_fine, mu_fine) elif self.scatter_format == SCATTER_TABULAR: # Calculate the mu points of the current data @@ -1874,7 +1880,7 @@ def convert_scatter_format(self, target_format, target_order=None): for l in range(xsdata.num_orders): y = (interp1d(mu_self, orig_data)(mu_fine) * eval_legendre(l, mu_fine)) - new_data[..., l] = simps(y, mu_fine) + new_data[..., l] = integrate(y, mu_fine) elif target_format == SCATTER_TABULAR: # Simply use an interpolating function to get the new data @@ -1893,7 +1899,7 @@ def convert_scatter_format(self, target_format, target_order=None): interp = interp1d(mu_self, orig_data) for h_bin in range(xsdata.num_orders): mu_fine = np.linspace(mu[h_bin], mu[h_bin + 1], _NMU) - new_data[..., h_bin] = simps(interp(mu_fine), mu_fine) + new_data[..., h_bin] = integrate(interp(mu_fine), mu_fine) elif self.scatter_format == SCATTER_HISTOGRAM: # The histogram format does not have enough information to @@ -1919,7 +1925,7 @@ def convert_scatter_format(self, target_format, target_order=None): mu_fine = np.linspace(-1, 1, _NMU) for l in range(xsdata.num_orders): y = interp(mu_fine) * norm * eval_legendre(l, mu_fine) - new_data[..., l] = simps(y, mu_fine) + new_data[..., l] = integrate(y, mu_fine) elif target_format == SCATTER_TABULAR: # Simply use an interpolating function to get the new data @@ -1938,7 +1944,7 @@ def convert_scatter_format(self, target_format, target_order=None): for h_bin in range(xsdata.num_orders): mu_fine = np.linspace(mu[h_bin], mu[h_bin + 1], _NMU) new_data[..., h_bin] = \ - norm * simps(interp(mu_fine), mu_fine) + norm * integrate(interp(mu_fine), mu_fine) # Remove small values resulting from numerical precision issues new_data[..., np.abs(new_data) < 1.E-10] = 0. diff --git a/tests/unit_tests/test_data_photon.py b/tests/unit_tests/test_data_photon.py index f7274e9c195..171dd52f3a2 100644 --- a/tests/unit_tests/test_data_photon.py +++ b/tests/unit_tests/test_data_photon.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python - from collections.abc import Mapping, Callable import os from pathlib import Path @@ -123,6 +121,8 @@ def test_reactions(element, reaction): reactions[18] +# TODO: Remove skip when support is Python 3.9+ +@pytest.mark.skipif(not hasattr(pd.options, 'future'), reason='pandas version too old') @pytest.mark.parametrize('element', ['Pu'], indirect=True) def test_export_to_hdf5(tmpdir, element): filename = str(tmpdir.join('tmp.h5')) @@ -146,8 +146,9 @@ def test_export_to_hdf5(tmpdir, element): # Export to hdf5 again element2.export_to_hdf5(filename, 'w') + def test_photodat_only(run_in_tmpdir): endf_dir = Path(os.environ['OPENMC_ENDF_DATA']) photoatomic_file = endf_dir / 'photoat' / 'photoat-001_H_000.endf' data = openmc.data.IncidentPhoton.from_endf(photoatomic_file) - data.export_to_hdf5('tmp.h5', 'w') \ No newline at end of file + data.export_to_hdf5('tmp.h5', 'w') From 3e16c038fee8ee8699c290f9e89c338ec4c5cd9c Mon Sep 17 00:00:00 2001 From: Gavin Ridley Date: Wed, 12 Jun 2024 17:33:55 -0500 Subject: [PATCH 008/184] only add png or h5 extension if not present in plots.py (#3036) Co-authored-by: Paul Romano --- openmc/plots.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/openmc/plots.py b/openmc/plots.py index df4a7663310..02a44449bed 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -171,8 +171,14 @@ def _get_plot_image(plot, cwd): from IPython.display import Image # Make sure .png file was created - stem = plot.filename if plot.filename is not None else f'plot_{plot.id}' - png_file = Path(cwd) / f'{stem}.png' + png_filename = plot.filename if plot.filename is not None else f'plot_{plot.id}' + + # Add file extension if not already present. The C++ code added it + # automatically if it wasn't present. + if Path(png_filename).suffix != ".png": + png_filename += ".png" + + png_file = Path(cwd) / png_filename if not png_file.exists(): raise FileNotFoundError( f"Could not find .png image for plot {plot.id}. Your version of " @@ -630,7 +636,7 @@ def meshlines(self, meshlines): cv.check_type('plot meshlines', meshlines, dict) if 'type' not in meshlines: msg = f'Unable to set the meshlines to "{meshlines}" which ' \ - 'does not have a "type" key' + 'does not have a "type" key' raise ValueError(msg) elif meshlines['type'] not in ['tally', 'entropy', 'ufs', 'cmfd']: @@ -968,8 +974,13 @@ def to_vtk(self, output: Optional[PathLike] = None, # Run OpenMC in geometry plotting mode and produces a h5 file openmc.plot_geometry(False, openmc_exec, cwd) - stem = self.filename if self.filename is not None else f'plot_{self.id}' - h5_voxel_file = Path(cwd) / f'{stem}.h5' + h5_voxel_filename = self.filename if self.filename is not None else f'plot_{self.id}' + + # Add file extension if not already present + if Path(h5_voxel_filename).suffix != ".h5": + h5_voxel_filename += ".h5" + + h5_voxel_file = Path(cwd) / h5_voxel_filename if output is None: output = h5_voxel_file.with_suffix('.vti') From b1b8a4c32834f82b0687600efbffa0e3181ef4c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigfrid=20Stj=C3=A4rnholm?= Date: Thu, 13 Jun 2024 05:46:18 +0200 Subject: [PATCH 009/184] Set OpenMCOperator materials to the model materials when diff_burnable_mats = True (#2877) --- openmc/deplete/openmc_operator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openmc/deplete/openmc_operator.py b/openmc/deplete/openmc_operator.py index ad7a3924b7e..75f0925c4e7 100644 --- a/openmc/deplete/openmc_operator.py +++ b/openmc/deplete/openmc_operator.py @@ -142,6 +142,7 @@ def __init__( if diff_burnable_mats: self._differentiate_burnable_mats() + self.materials = self.model.materials # Determine which nuclides have cross section data # This nuclides variables contains every nuclides From 5222b343a4e7f21fa64d0d6be92292878e1f6ae3 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Mon, 17 Jun 2024 11:02:20 -0500 Subject: [PATCH 010/184] Fixed Source Random Ray (#2988) Co-authored-by: Gavin Ridley Co-authored-by: Paul Romano --- docs/source/methods/random_ray.rst | 48 ++- docs/source/usersguide/random_ray.rst | 162 ++++++++- include/openmc/mgxs_interface.h | 3 + .../openmc/random_ray/flat_source_domain.h | 131 +++++-- .../openmc/random_ray/random_ray_simulation.h | 3 +- include/openmc/source.h | 9 +- src/mgxs_interface.cpp | 10 + src/random_ray/flat_source_domain.cpp | 296 +++++++++++++-- src/random_ray/random_ray_simulation.cpp | 95 +++-- src/settings.cpp | 8 +- src/simulation.cpp | 14 +- .../random_ray_basic/results_true.dat | 338 ++++++++--------- .../random_ray_fixed_source/__init__.py | 0 .../cell/inputs_true.dat | 204 +++++++++++ .../cell/results_true.dat | 47 +++ .../material/inputs_true.dat | 204 +++++++++++ .../material/results_true.dat | 47 +++ .../random_ray_fixed_source/test.py | 339 ++++++++++++++++++ .../universe/inputs_true.dat | 204 +++++++++++ .../universe/results_true.dat | 47 +++ .../random_ray_vacuum/results_true.dat | 336 ++++++++--------- 21 files changed, 2120 insertions(+), 425 deletions(-) create mode 100644 tests/regression_tests/random_ray_fixed_source/__init__.py create mode 100644 tests/regression_tests/random_ray_fixed_source/cell/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source/cell/results_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source/material/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source/material/results_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source/test.py create mode 100644 tests/regression_tests/random_ray_fixed_source/universe/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source/universe/results_true.dat diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst index aa436784766..e0fff792e04 100644 --- a/docs/source/methods/random_ray.rst +++ b/docs/source/methods/random_ray.rst @@ -94,8 +94,8 @@ Method of Characteristics The Boltzmann neutron transport equation is a partial differential equation (PDE) that describes the angular flux within a system. It is a balance equation, with the streaming and absorption terms typically appearing on the left hand -side, which are balanced by the scattering source and fission source terms on -the right hand side. +side, which are balanced by the scattering source, fission, and fixed source +terms on the right hand side. .. math:: :label: transport @@ -522,8 +522,8 @@ make their traversals, and summing these contributions up as in Equation improve the estimate of the source and scalar flux over many iterations, given that our initial starting source will just be a guess? -The source :math:`Q^{n}` for iteration :math:`n` can be inferred -from the scalar flux from the previous iteration :math:`n-1` as: +In an eigenvalue simulation, the source :math:`Q^{n}` for iteration :math:`n` +can be inferred from the scalar flux from the previous iteration :math:`n-1` as: .. math:: :label: source_update @@ -535,7 +535,7 @@ where :math:`Q^{n}(i, g)` is the total source (fission + scattering) in region :math:`g` must be computed by summing over the contributions from all groups :math:`g' \in G`. -In a similar manner, the eigenvalue for iteration :math:`n` can be computed as: +The eigenvalue for iteration :math:`n` can be computed as: .. math:: :label: eigenvalue_update @@ -576,6 +576,18 @@ and a similar substitution can be made to update Equation estimate is used, such that the total fission source from the previous iteration (:math:`n-1`) is also recomputed each iteration. +In a fixed source simulation, the fission source is replaced by a user specified +fixed source term :math:`Q_\text{fixed}(i,E)`, which is defined for each FSR and +energy group. This additional source term is applied at this stage for +generating the next iteration's source estimate as: + +.. math:: + :label: fixed_source_update + + Q^{n}(i, g) = Q_\text{fixed}(i,g) + \sum\limits^{G}_{g'} \Sigma_{s}(i,g,g') \phi^{n-1}(g') + +and no eigenvalue is computed. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Ray Starting Conditions and Inactive Length ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -742,6 +754,32 @@ behavior if a single simulation cell is able to score to multiple filter mesh cells. In the future, the capability to fully support mesh tallies may be added to OpenMC, but for now this restriction needs to be respected. +.. _usersguide_fixed_source_methods: + +------------ +Fixed Source +------------ + +The random ray solver in OpenMC can be used for both eigenvalue and fixed source +problems. There are a few key differences between fixed source transport with +random ray and Monte Carlo, however. + +- **Source definition:** In Monte Carlo, it is relatively easy to define various + source distributions, including point sources, surface sources, volume + sources, and even custom user sources -- all with varying angular and spatial + statistical distributions. In random ray, the natural way to include a fixed + source term is by adding a fixed (flat) contribution to specific flat source + regions. Thus, in the OpenMC implementation of random ray, particle sources + are restricted to being volumetric and isotropic, although different energy + spectrums are supported. Fixed sources can be applied to specific materials, + cells, or universes. + +- **Inactive batches:** In Monte Carlo, use of a fixed source implies that all + batches are active batches, as there is no longer a need to develop a fission + source distribution. However, in random ray mode, there is still a need to + develop the scattering source by way of inactive batches before beginning + active batches. + --------------------------- Fundamental Sources of Bias --------------------------- diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index dad86c826e0..61f0746500c 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -40,13 +40,15 @@ Carlo, **inactive batches are required for both eigenvalue and fixed source solves in random ray mode** due to this additional need to converge the scattering source. +.. warning:: + Unlike Monte Carlo, the random ray solver still requires usage of inactive + batches when in fixed source mode so as to develop the scattering source. + The additional burden of converging the scattering source generally results in a higher requirement for the number of inactive batches---often by an order of magnitude or more. For instance, it may be reasonable to only use 50 inactive batches for a light water reactor simulation with Monte Carlo, but random ray -might require 500 or more inactive batches. Similar to Monte Carlo, -:ref:`Shannon entropy ` can be used to gauge whether the -combined scattering and fission source has fully developed. +might require 500 or more inactive batches. Similar to Monte Carlo, active batches are used in the random ray solver mode to accumulate and converge statistics on unknown quantities (i.e., the random ray @@ -248,6 +250,8 @@ a larger value until the "low ray density" messages go away. ray lengths are sufficiently long to allow for transport to occur between source and target regions of interest. +.. _usersguide_ray_source: + ---------- Ray Source ---------- @@ -261,7 +265,7 @@ that the source must not be limited to only fissionable regions. Additionally, the source box must cover the entire simulation domain. In the case of a simulation domain that is not box shaped, a box source should still be used to bound the domain but with the source limited to rejection sampling the actual -simulation universe (which can be specified via the ``domains`` field of the +simulation universe (which can be specified via the ``domains`` constraint of the :class:`openmc.IndependentSource` Python class). Similar to Monte Carlo sources, for two-dimensional problems (e.g., a 2D pincell) it is desirable to make the source bounded near the origin of the infinite dimension. An example of an @@ -411,11 +415,78 @@ in the `OpenMC Jupyter notebook collection separate materials can be defined each with a separate multigroup dataset corresponding to a given temperature. +--------------------------------- +Fixed Source and Eigenvalue Modes +--------------------------------- + +Both fixed source and eigenvalue modes are supported with the random ray solver +in OpenMC. Modes can be selected as described in the :ref:`run modes section +`. In both modes, a ray source must be provided to let +OpenMC know where to sample ray starting locations from, as discussed in the +:ref:`ray source section `. In fixed source mode, at +least one regular source must be provided as well that represents the physical +particle fixed source. As discussed in the :ref:`fixed source methodology +section `, the types of fixed sources supported +in the random ray solver mode are limited compared to what is possible with the +Monte Carlo solver. + +Currently, all of the following conditions must be met for the particle source +to be valid in random ray mode: + +- One or more domain ids must be specified that indicate which cells, universes, + or materials the source applies to. This implicitly limits the source type to + being volumetric. This is specified via the ``domains`` constraint placed on the + :class:`openmc.IndependentSource` Python class. +- The source must be isotropic (default for a source) +- The source must use a discrete (i.e., multigroup) energy distribution. The + discrete energy distribution is input by defining a + :class:`openmc.stats.Discrete` Python class, and passed as the ``energy`` + field of the :class:`openmc.IndependentSource` Python class. + +Any other spatial distribution information contained in a particle source will +be ignored. Only the specified cell, material, or universe domains will be used +to define the spatial location of the source, as the source will be applied +during a pre-processing stage of OpenMC to all source regions that are contained +within the specified domains for the source. + +When defining a :class:`openmc.stats.Discrete` object, note that the ``x`` field +will correspond to the discrete energy points, and the ``p`` field will +correspond to the discrete probabilities. It is recommended to select energy +points that fall within energy groups rather than on boundaries between the +groups. That is, if the problem contains two energy groups (with bin edges of +1.0e-5, 1.0e-1, 1.0e7), then a good selection for the ``x`` field might be +points of 1.0e-2 and 1.0e1. + +:: + + # Define geometry, etc. + ... + source_cell = openmc.Cell(fill=source_mat, name='cell where fixed source will be') + ... + # Define physical neutron fixed source + energy_points = [1.0e-2, 1.0e1] + strengths = [0.25, 0.75] + energy_distribution = openmc.stats.Discrete(x=energy_points, p=strengths) + neutron_source = openmc.IndependentSource( + energy=energy_distribution, + constraints={'domains': [source_cell]} + ) + + # Add fixed source and ray sampling source to settings file + settings.source = [neutron_source] + --------------------------------------- Putting it All Together: Example Inputs --------------------------------------- -An example of a settings definition for random ray is given below:: +~~~~~~~~~~~~~~~~~~ +Eigenvalue Example +~~~~~~~~~~~~~~~~~~ + +An example of a settings definition for an eigenvalue random ray simulation is +given below: + +:: # Geometry and MGXS material definition of 2x2 lattice (not shown) pitch = 1.26 @@ -478,3 +549,84 @@ Monte Carlo run (see the :ref:`geometry ` and There is also a complete example of a pincell available in the ``openmc/examples/pincell_random_ray`` folder. + +~~~~~~~~~~~~~~~~~~~~ +Fixed Source Example +~~~~~~~~~~~~~~~~~~~~ + +An example of a settings definition for a fixed source random ray simulation is +given below: + +:: + + # Geometry and MGXS material definition of 2x2 lattice (not shown) + pitch = 1.26 + source_cell = openmc.Cell(fill=source_mat, name='cell where fixed source will be') + ebins = [1e-5, 1e-1, 20.0e6] + ... + + # Instantiate a settings object for a random ray solve + settings = openmc.Settings() + settings.energy_mode = "multi-group" + settings.batches = 1200 + settings.inactive = 600 + settings.particles = 2000 + settings.run_mode = 'fixed source' + settings.random_ray['distance_inactive'] = 40.0 + settings.random_ray['distance_active'] = 400.0 + + # Create an initial uniform spatial source distribution for sampling rays + lower_left = (-pitch, -pitch, -pitch) + upper_right = ( pitch, pitch, pitch) + uniform_dist = openmc.stats.Box(lower_left, upper_right) + settings.random_ray['ray_source'] = openmc.IndependentSource(space=uniform_dist) + + # Define physical neutron fixed source + energy_points = [1.0e-2, 1.0e1] + strengths = [0.25, 0.75] + energy_distribution = openmc.stats.Discrete(x=energy_points, p=strengths) + neutron_source = openmc.IndependentSource( + energy=energy_distribution, + constraints={'domains': [source_cell]} + ) + + # Add fixed source and ray sampling source to settings file + settings.source = [neutron_source] + + settings.export_to_xml() + + # Define tallies + + # Create a mesh filter + mesh = openmc.RegularMesh() + mesh.dimension = (2, 2) + mesh.lower_left = (-pitch/2, -pitch/2) + mesh.upper_right = (pitch/2, pitch/2) + mesh_filter = openmc.MeshFilter(mesh) + + # Create a multigroup energy filter + energy_filter = openmc.EnergyFilter(ebins) + + # Create tally using our two filters and add scores + tally = openmc.Tally() + tally.filters = [mesh_filter, energy_filter] + tally.scores = ['flux'] + + # Instantiate a Tallies collection and export to XML + tallies = openmc.Tallies([tally]) + tallies.export_to_xml() + + # Create voxel plot + plot = openmc.Plot() + plot.origin = [0, 0, 0] + plot.width = [2*pitch, 2*pitch, 1] + plot.pixels = [1000, 1000, 1] + plot.type = 'voxel' + + # Instantiate a Plots collection and export to XML + plots = openmc.Plots([plot]) + plots.export_to_xml() + +All other inputs (e.g., geometry, material) will be unchanged from a typical +Monte Carlo run (see the :ref:`geometry ` and +:ref:`multigroup materials ` user guides for more information). diff --git a/include/openmc/mgxs_interface.h b/include/openmc/mgxs_interface.h index 8bcdf6dc608..da074f825ee 100644 --- a/include/openmc/mgxs_interface.h +++ b/include/openmc/mgxs_interface.h @@ -46,6 +46,9 @@ class MgxsInterface { // Get the kT values which are used in the OpenMC model vector> get_mat_kTs(); + // Get the group index corresponding to a continuous energy + int get_group_index(double E); + int num_energy_groups_; int num_delayed_groups_; vector xs_names_; // available names in HDF5 file diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index 5f64914edb9..badbb25fc2c 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -3,9 +3,83 @@ #include "openmc/openmp_interface.h" #include "openmc/position.h" +#include "openmc/source.h" namespace openmc { +//---------------------------------------------------------------------------- +// Helper Functions + +// The hash_combine function is the standard hash combine function from boost +// that is typically used for combining multiple hash values into a single hash +// as is needed for larger objects being stored in a hash map. The function is +// taken from: +// https://www.boost.org/doc/libs/1_55_0/doc/html/hash/reference.html#boost.hash_combine +// which carries the following license: +// +// Boost Software License - Version 1.0 - August 17th, 2003 +// Permission is hereby granted, free of charge, to any person or organization +// obtaining a copy of the software and accompanying documentation covered by +// this license (the "Software") to use, reproduce, display, distribute, +// execute, and transmit the Software, and to prepare derivative works of the +// Software, and to permit third-parties to whom the Software is furnished to +// do so, all subject to the following: +// The copyright notices in the Software and this entire statement, including +// the above license grant, this restriction and the following disclaimer, +// must be included in all copies of the Software, in whole or in part, and +// all derivative works of the Software, unless such copies or derivative +// works are solely in the form of machine-executable object code generated by +// a source language processor. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +// SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +// FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +inline void hash_combine(size_t& seed, const size_t v) +{ + seed ^= (v + 0x9e3779b9 + (seed << 6) + (seed >> 2)); +} + +//---------------------------------------------------------------------------- +// Helper Structs + +// A mapping object that is used to map between a specific random ray +// source region and an OpenMC native tally bin that it should score to +// every iteration. +struct TallyTask { + int tally_idx; + int filter_idx; + int score_idx; + int score_type; + TallyTask(int tally_idx, int filter_idx, int score_idx, int score_type) + : tally_idx(tally_idx), filter_idx(filter_idx), score_idx(score_idx), + score_type(score_type) + {} + TallyTask() = default; + + // Comparison and Hash operators are defined to allow usage of the + // TallyTask struct as a key in an unordered_set + bool operator==(const TallyTask& other) const + { + return tally_idx == other.tally_idx && filter_idx == other.filter_idx && + score_idx == other.score_idx && score_type == other.score_type; + } + + struct HashFunctor { + size_t operator()(const TallyTask& task) const + { + size_t seed = 0; + hash_combine(seed, task.tally_idx); + hash_combine(seed, task.filter_idx); + hash_combine(seed, task.score_idx); + hash_combine(seed, task.score_type); + return seed; + } + }; +}; + /* * The FlatSourceDomain class encompasses data and methods for storing * scalar flux and source region for all flat source regions in a @@ -14,23 +88,6 @@ namespace openmc { class FlatSourceDomain { public: - //---------------------------------------------------------------------------- - // Helper Structs - - // A mapping object that is used to map between a specific random ray - // source region and an OpenMC native tally bin that it should score to - // every iteration. - struct TallyTask { - int tally_idx; - int filter_idx; - int score_idx; - int score_type; - TallyTask(int tally_idx, int filter_idx, int score_idx, int score_type) - : tally_idx(tally_idx), filter_idx(filter_idx), score_idx(score_idx), - score_type(score_type) - {} - }; - //---------------------------------------------------------------------------- // Constructors FlatSourceDomain(); @@ -44,10 +101,13 @@ class FlatSourceDomain { int64_t add_source_to_scalar_flux(); void batch_reset(); void convert_source_regions_to_tallies(); - void random_ray_tally() const; + void reset_tally_volumes(); + void random_ray_tally(); void accumulate_iteration_flux(); void output_to_vtk() const; void all_reduce_replicated_source_regions(); + void convert_external_sources(); + void count_external_source_regions(); //---------------------------------------------------------------------------- // Public Data members @@ -55,6 +115,8 @@ class FlatSourceDomain { bool mapped_all_tallies_ {false}; // If all source regions have been visited int64_t n_source_regions_ {0}; // Total number of source regions in the model + int64_t n_external_source_regions_ {0}; // Total number of source regions with + // non-zero external source terms // 1D array representing source region starting offset for each OpenMC Cell // in model::cells @@ -72,18 +134,39 @@ class FlatSourceDomain { vector scalar_flux_old_; vector scalar_flux_new_; vector source_; + vector external_source_; + +private: + //---------------------------------------------------------------------------- + // Methods + void apply_external_source_to_source_region( + Discrete* discrete, double strength_factor, int64_t source_region); + void apply_external_source_to_cell_instances(int32_t i_cell, + Discrete* discrete, double strength_factor, int target_material_id, + const vector& instances); + void apply_external_source_to_cell_and_children(int32_t i_cell, + Discrete* discrete, double strength_factor, int32_t target_material_id); //---------------------------------------------------------------------------- // Private data members -private: int negroups_; // Number of energy groups in simulation int64_t n_source_elements_ {0}; // Total number of source regions in the model // times the number of energy groups - // 2D array representing values for all source regions x energy groups x tally + double + simulation_volume_; // Total physical volume of the simulation domain, as + // defined by the 3D box of the random ray source + + // 2D array representing values for all source elements x tally // tasks vector> tally_task_; + // 1D array representing values for all source regions, with each region + // containing a set of volume tally tasks. This more complicated data + // structure is convenient for ensuring that volumes are only tallied once per + // source region, regardless of how many energy groups are used for tallying. + vector> volume_task_; + // 1D arrays representing values for all source regions vector material_; vector volume_t_; @@ -92,6 +175,14 @@ class FlatSourceDomain { // groups vector scalar_flux_final_; + // Volumes for each tally and bin/score combination. This intermediate data + // structure is used when tallying quantities that must be normalized by + // volume (i.e., flux). The vector is index by tally index, while the inner 2D + // xtensor is indexed by bin index and score index in a similar manner to the + // results tensor in the Tally class, though without the third dimension, as + // SUM and SUM_SQ do not need to be tracked. + vector> tally_volumes_; + }; // class FlatSourceDomain //============================================================================ diff --git a/include/openmc/random_ray/random_ray_simulation.h b/include/openmc/random_ray/random_ray_simulation.h index cde0d27dff2..b439574bfca 100644 --- a/include/openmc/random_ray/random_ray_simulation.h +++ b/include/openmc/random_ray/random_ray_simulation.h @@ -24,7 +24,8 @@ class RandomRaySimulation { void instability_check( int64_t n_hits, double k_eff, double& avg_miss_rate) const; void print_results_random_ray(uint64_t total_geometric_intersections, - double avg_miss_rate, int negroups, int64_t n_source_regions) const; + double avg_miss_rate, int negroups, int64_t n_source_regions, + int64_t n_external_source_regions) const; //---------------------------------------------------------------------------- // Data members diff --git a/include/openmc/source.h b/include/openmc/source.h index 9ef8b925127..5cc0356e3e9 100644 --- a/include/openmc/source.h +++ b/include/openmc/source.h @@ -52,6 +52,8 @@ extern vector> external_sources; class Source { public: + // Domain types + enum class DomainType { UNIVERSE, MATERIAL, CELL }; // Constructors, destructors Source() = default; explicit Source(pugi::xml_node node); @@ -76,9 +78,6 @@ class Source { static unique_ptr create(pugi::xml_node node); protected: - // Domain types - enum class DomainType { UNIVERSE, MATERIAL, CELL }; - // Strategy used for rejecting sites when constraints are applied. KILL means // that sites are always accepted but if they don't satisfy constraints, they // are given weight 0. RESAMPLE means that a new source site will be sampled @@ -134,6 +133,10 @@ class IndependentSource : public Source { Distribution* energy() const { return energy_.get(); } Distribution* time() const { return time_.get(); } + // Make domain type and ids available + DomainType domain_type() const { return domain_type_; } + const std::unordered_set& domain_ids() const { return domain_ids_; } + protected: // Indicates whether derived class already handles constraints bool constraints_applied() const override { return true; } diff --git a/src/mgxs_interface.cpp b/src/mgxs_interface.cpp index d54211ecaf1..0febc612bf0 100644 --- a/src/mgxs_interface.cpp +++ b/src/mgxs_interface.cpp @@ -15,6 +15,7 @@ #include "openmc/material.h" #include "openmc/math_functions.h" #include "openmc/nuclide.h" +#include "openmc/search.h" #include "openmc/settings.h" namespace openmc { @@ -183,6 +184,15 @@ vector> MgxsInterface::get_mat_kTs() //============================================================================== +int MgxsInterface::get_group_index(double E) +{ + int g = + lower_bound_index(rev_energy_bins_.begin(), rev_energy_bins_.end(), E); + return num_energy_groups_ - g - 1.; +} + +//============================================================================== + void MgxsInterface::read_header(const std::string& path_cross_sections) { // Save name of HDF5 file to be read to struct data diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 5e1194aa92a..7f5d76943e5 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -2,6 +2,7 @@ #include "openmc/cell.h" #include "openmc/geometry.h" +#include "openmc/material.h" #include "openmc/message_passing.h" #include "openmc/mgxs_interface.h" #include "openmc/output.h" @@ -48,10 +49,19 @@ FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) // Initialize element-wise arrays scalar_flux_new_.assign(n_source_elements_, 0.0); - scalar_flux_old_.assign(n_source_elements_, 1.0); scalar_flux_final_.assign(n_source_elements_, 0.0); source_.resize(n_source_elements_); + external_source_.assign(n_source_elements_, 0.0); tally_task_.resize(n_source_elements_); + volume_task_.resize(n_source_regions_); + + if (settings::run_mode == RunMode::EIGENVALUE) { + // If in eigenvalue mode, set starting flux to guess of unity + scalar_flux_old_.assign(n_source_elements_, 1.0); + } else { + // If in fixed source mode, set starting flux to guess of zero + scalar_flux_old_.assign(n_source_elements_, 0.0); + } // Initialize material array int64_t source_region_id = 0; @@ -68,6 +78,25 @@ FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) if (source_region_id != n_source_regions_) { fatal_error("Unexpected number of source regions"); } + + // Initialize tally volumes + tally_volumes_.resize(model::tallies.size()); + for (int i = 0; i < model::tallies.size(); i++) { + // Get the shape of the 3D result tensor + auto shape = model::tallies[i]->results().shape(); + + // Create a new 2D tensor with the same size as the first + // two dimensions of the 3D tensor + tally_volumes_[i] = + xt::xtensor::from_shape({shape[0], shape[1]}); + } + + // Compute simulation domain volume based on ray source + auto* is = dynamic_cast(RandomRay::ray_source_.get()); + SpatialDistribution* space_dist = is->space(); + SpatialBox* sb = dynamic_cast(space_dist); + Position dims = sb->upper_right() - sb->lower_left(); + simulation_volume_ = dims.x * dims.y * dims.z; } void FlatSourceDomain::batch_reset() @@ -97,11 +126,11 @@ void FlatSourceDomain::update_neutron_source(double k_eff) // Temperature and angle indices, if using multiple temperature // data sets and/or anisotropic data sets. - // TODO: Currently assumes we are only using single temp/single - // angle data. + // TODO: Currently assumes we are only using single temp/single angle data. const int t = 0; const int a = 0; + // Add scattering source #pragma omp parallel for for (int sr = 0; sr < n_source_regions_; sr++) { int material = material_[sr]; @@ -110,23 +139,47 @@ void FlatSourceDomain::update_neutron_source(double k_eff) float sigma_t = data::mg.macro_xs_[material].get_xs( MgxsType::TOTAL, e_out, nullptr, nullptr, nullptr, t, a); float scatter_source = 0.0f; - float fission_source = 0.0f; for (int e_in = 0; e_in < negroups_; e_in++) { float scalar_flux = scalar_flux_old_[sr * negroups_ + e_in]; + float sigma_s = data::mg.macro_xs_[material].get_xs( MgxsType::NU_SCATTER, e_in, &e_out, nullptr, nullptr, t, a); - float nu_sigma_f = data::mg.macro_xs_[material].get_xs( - MgxsType::NU_FISSION, e_in, nullptr, nullptr, nullptr, t, a); - float chi = data::mg.macro_xs_[material].get_xs( - MgxsType::CHI_PROMPT, e_in, &e_out, nullptr, nullptr, t, a); scatter_source += sigma_s * scalar_flux; - fission_source += nu_sigma_f * scalar_flux * chi; } - fission_source *= inverse_k_eff; - float new_isotropic_source = (scatter_source + fission_source) / sigma_t; - source_[sr * negroups_ + e_out] = new_isotropic_source; + source_[sr * negroups_ + e_out] = scatter_source / sigma_t; + } + } + + if (settings::run_mode == RunMode::EIGENVALUE) { + // Add fission source if in eigenvalue mode +#pragma omp parallel for + for (int sr = 0; sr < n_source_regions_; sr++) { + int material = material_[sr]; + + for (int e_out = 0; e_out < negroups_; e_out++) { + float sigma_t = data::mg.macro_xs_[material].get_xs( + MgxsType::TOTAL, e_out, nullptr, nullptr, nullptr, t, a); + float fission_source = 0.0f; + + for (int e_in = 0; e_in < negroups_; e_in++) { + float scalar_flux = scalar_flux_old_[sr * negroups_ + e_in]; + float nu_sigma_f = data::mg.macro_xs_[material].get_xs( + MgxsType::NU_FISSION, e_in, nullptr, nullptr, nullptr, t, a); + float chi = data::mg.macro_xs_[material].get_xs( + MgxsType::CHI_PROMPT, e_in, &e_out, nullptr, nullptr, t, a); + fission_source += nu_sigma_f * scalar_flux * chi; + } + source_[sr * negroups_ + e_out] += + fission_source * inverse_k_eff / sigma_t; + } + } + } else { +// Add external source if in fixed source mode +#pragma omp parallel for + for (int se = 0; se < n_source_elements_; se++) { + source_[se] += external_source_[se]; } } @@ -355,10 +408,14 @@ void FlatSourceDomain::convert_source_regions_to_tallies() for (auto score_index = 0; score_index < tally.scores_.size(); score_index++) { auto score_bin = tally.scores_[score_index]; - // If a valid tally, filter, and score cobination has been found, + // If a valid tally, filter, and score combination has been found, // then add it to the list of tally tasks for this source element. - tally_task_[source_element].emplace_back( - i_tally, filter_index, score_index, score_bin); + TallyTask task(i_tally, filter_index, score_index, score_bin); + tally_task_[source_element].push_back(task); + + // Also add this task to the list of volume tasks for this source + // region. + volume_task_[sr].insert(task); } } } @@ -372,6 +429,16 @@ void FlatSourceDomain::convert_source_regions_to_tallies() mapped_all_tallies_ = all_source_regions_mapped; } +// Set the volume accumulators to zero for all tallies +void FlatSourceDomain::reset_tally_volumes() +{ +#pragma omp parallel for + for (int i = 0; i < tally_volumes_.size(); i++) { + auto& tensor = tally_volumes_[i]; + tensor.fill(0.0); // Set all elements of the tensor to 0.0 + } +} + // Tallying in random ray is not done directly during transport, rather, // it is done only once after each power iteration. This is made possible // by way of a mapping data structure that relates spatial source regions @@ -381,10 +448,13 @@ void FlatSourceDomain::convert_source_regions_to_tallies() // tally function simply traverses the mapping data structure and executes // the scoring operations to OpenMC's native tally result arrays. -void FlatSourceDomain::random_ray_tally() const +void FlatSourceDomain::random_ray_tally() { openmc::simulation::time_tallies.start(); + // Reset our tally volumes to zero + reset_tally_volumes(); + // Temperature and angle indices, if using multiple temperature // data sets and/or anisotropic data sets. // TODO: Currently assumes we are only using single temp/single @@ -397,32 +467,46 @@ void FlatSourceDomain::random_ray_tally() const // them. #pragma omp parallel for for (int sr = 0; sr < n_source_regions_; sr++) { - double volume = volume_[sr]; + // The fsr.volume_ is the unitless fractional simulation averaged volume + // (i.e., it is the FSR's fraction of the overall simulation volume). The + // simulation_volume_ is the total 3D physical volume in cm^3 of the entire + // global simulation domain (as defined by the ray source box). Thus, the + // FSR's true 3D spatial volume in cm^3 is found by multiplying its fraction + // of the total volume by the total volume. Not important in eigenvalue + // solves, but useful in fixed source solves for returning the flux shape + // with a magnitude that makes sense relative to the fixed source strength. + double volume = volume_[sr] * simulation_volume_; + double material = material_[sr]; for (int g = 0; g < negroups_; g++) { int idx = sr * negroups_ + g; - double flux = scalar_flux_new_[idx] * volume; + double flux = scalar_flux_new_[idx]; + + // Determine numerical score value for (auto& task : tally_task_[idx]) { double score; switch (task.score_type) { case SCORE_FLUX: - score = flux; + score = flux * volume; break; case SCORE_TOTAL: - score = flux * data::mg.macro_xs_[material].get_xs( - MgxsType::TOTAL, g, NULL, NULL, NULL, t, a); + score = flux * volume * + data::mg.macro_xs_[material].get_xs( + MgxsType::TOTAL, g, NULL, NULL, NULL, t, a); break; case SCORE_FISSION: - score = flux * data::mg.macro_xs_[material].get_xs( - MgxsType::FISSION, g, NULL, NULL, NULL, t, a); + score = flux * volume * + data::mg.macro_xs_[material].get_xs( + MgxsType::FISSION, g, NULL, NULL, NULL, t, a); break; case SCORE_NU_FISSION: - score = flux * data::mg.macro_xs_[material].get_xs( - MgxsType::NU_FISSION, g, NULL, NULL, NULL, t, a); + score = flux * volume * + data::mg.macro_xs_[material].get_xs( + MgxsType::NU_FISSION, g, NULL, NULL, NULL, t, a); break; case SCORE_EVENTS: @@ -435,13 +519,49 @@ void FlatSourceDomain::random_ray_tally() const "random ray mode."); break; } + + // Apply score to the appropriate tally bin Tally& tally {*model::tallies[task.tally_idx]}; #pragma omp atomic tally.results_(task.filter_idx, task.score_idx, TallyResult::VALUE) += score; + } // end tally task loop + } // end energy group loop + + // For flux tallies, the total volume of the spatial region is needed + // for normalizing the flux. We store this volume in a separate tensor. + // We only contribute to each volume tally bin once per FSR. + for (const auto& task : volume_task_[sr]) { + if (task.score_type == SCORE_FLUX) { +#pragma omp atomic + tally_volumes_[task.tally_idx](task.filter_idx, task.score_idx) += + volume; + } + } + } // end FSR loop + + // Normalize any flux scores by the total volume of the FSRs scoring to that + // bin. To do this, we loop over all tallies, and then all filter bins, + // and then scores. For each score, we check the tally data structure to + // see what index that score corresponds to. If that score is a flux score, + // then we divide it by volume. + for (int i = 0; i < model::tallies.size(); i++) { + Tally& tally {*model::tallies[i]}; +#pragma omp parallel for + for (int bin = 0; bin < tally.n_filter_bins(); bin++) { + for (int score_idx = 0; score_idx < tally.n_scores(); score_idx++) { + auto score_type = tally.scores_[score_idx]; + if (score_type == SCORE_FLUX) { + double vol = tally_volumes_[i](bin, score_idx); + if (vol > 0.0) { + tally.results_(bin, score_idx, TallyResult::VALUE) /= vol; + } + } } } } + + openmc::simulation::time_tallies.stop(); } void FlatSourceDomain::all_reduce_replicated_source_regions() @@ -683,4 +803,130 @@ void FlatSourceDomain::output_to_vtk() const } } +void FlatSourceDomain::apply_external_source_to_source_region( + Discrete* discrete, double strength_factor, int64_t source_region) +{ + const auto& discrete_energies = discrete->x(); + const auto& discrete_probs = discrete->prob(); + + for (int e = 0; e < discrete_energies.size(); e++) { + int g = data::mg.get_group_index(discrete_energies[e]); + external_source_[source_region * negroups_ + g] += + discrete_probs[e] * strength_factor; + } +} + +void FlatSourceDomain::apply_external_source_to_cell_instances(int32_t i_cell, + Discrete* discrete, double strength_factor, int target_material_id, + const vector& instances) +{ + Cell& cell = *model::cells[i_cell]; + + if (cell.type_ != Fill::MATERIAL) + return; + + for (int j : instances) { + int cell_material_idx = cell.material(j); + int cell_material_id = model::materials[cell_material_idx]->id(); + if (target_material_id == C_NONE || + cell_material_id == target_material_id) { + int64_t source_region = source_region_offsets_[i_cell] + j; + apply_external_source_to_source_region( + discrete, strength_factor, source_region); + } + } +} + +void FlatSourceDomain::apply_external_source_to_cell_and_children( + int32_t i_cell, Discrete* discrete, double strength_factor, + int32_t target_material_id) +{ + Cell& cell = *model::cells[i_cell]; + + if (cell.type_ == Fill::MATERIAL) { + vector instances(cell.n_instances_); + std::iota(instances.begin(), instances.end(), 0); + apply_external_source_to_cell_instances( + i_cell, discrete, strength_factor, target_material_id, instances); + } else if (target_material_id == C_NONE) { + std::unordered_map> cell_instance_list = + cell.get_contained_cells(0, nullptr); + for (const auto& pair : cell_instance_list) { + int32_t i_child_cell = pair.first; + apply_external_source_to_cell_instances(i_child_cell, discrete, + strength_factor, target_material_id, pair.second); + } + } +} + +void FlatSourceDomain::count_external_source_regions() +{ +#pragma omp parallel for reduction(+ : n_external_source_regions_) + for (int sr = 0; sr < n_source_regions_; sr++) { + float total = 0.f; + for (int e = 0; e < negroups_; e++) { + int64_t se = sr * negroups_ + e; + total += external_source_[se]; + } + if (total != 0.f) { + n_external_source_regions_++; + } + } +} + +void FlatSourceDomain::convert_external_sources() +{ + // Loop over external sources + for (int es = 0; es < model::external_sources.size(); es++) { + Source* s = model::external_sources[es].get(); + IndependentSource* is = dynamic_cast(s); + Discrete* energy = dynamic_cast(is->energy()); + const std::unordered_set& domain_ids = is->domain_ids(); + + double strength_factor = is->strength(); + + if (is->domain_type() == Source::DomainType::MATERIAL) { + for (int32_t material_id : domain_ids) { + for (int i_cell = 0; i_cell < model::cells.size(); i_cell++) { + apply_external_source_to_cell_and_children( + i_cell, energy, strength_factor, material_id); + } + } + } else if (is->domain_type() == Source::DomainType::CELL) { + for (int32_t cell_id : domain_ids) { + int32_t i_cell = model::cell_map[cell_id]; + apply_external_source_to_cell_and_children( + i_cell, energy, strength_factor, C_NONE); + } + } else if (is->domain_type() == Source::DomainType::UNIVERSE) { + for (int32_t universe_id : domain_ids) { + int32_t i_universe = model::universe_map[universe_id]; + Universe& universe = *model::universes[i_universe]; + for (int32_t i_cell : universe.cells_) { + apply_external_source_to_cell_and_children( + i_cell, energy, strength_factor, C_NONE); + } + } + } + } // End loop over external sources + + // Temperature and angle indices, if using multiple temperature + // data sets and/or anisotropic data sets. + // TODO: Currently assumes we are only using single temp/single angle data. + const int t = 0; + const int a = 0; + +// Divide the fixed source term by sigma t (to save time when applying each +// iteration) +#pragma omp parallel for + for (int sr = 0; sr < n_source_regions_; sr++) { + int material = material_[sr]; + for (int e = 0; e < negroups_; e++) { + float sigma_t = data::mg.macro_xs_[material].get_xs( + MgxsType::TOTAL, e, nullptr, nullptr, nullptr, t, a); + external_source_[sr * negroups_ + e] /= sigma_t; + } + } +} + } // namespace openmc diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index b9fd93a48f6..17866c30208 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -111,13 +111,6 @@ void validate_random_ray_inputs() } } - // Validate solver mode - /////////////////////////////////////////////////////////////////// - if (settings::run_mode == RunMode::FIXED_SOURCE) { - fatal_error( - "Invalid run mode. Fixed source not yet supported in random ray mode."); - } - // Validate ray source /////////////////////////////////////////////////////////////////// @@ -125,8 +118,8 @@ void validate_random_ray_inputs() IndependentSource* is = dynamic_cast(RandomRay::ray_source_.get()); if (!is) { - fatal_error( - "Invalid ray source definition. Ray source must be IndependentSource."); + fatal_error("Invalid ray source definition. Ray source must provided and " + "be of type IndependentSource."); } // Check for box source @@ -134,24 +127,68 @@ void validate_random_ray_inputs() SpatialBox* sb = dynamic_cast(space_dist); if (!sb) { fatal_error( - "Invalid source definition -- only box sources are allowed in random " - "ray " - "mode. If no source is specified, OpenMC default is an isotropic point " - "source at the origin, which is invalid in random ray mode."); + "Invalid ray source definition -- only box sources are allowed."); } // Check that box source is not restricted to fissionable areas if (sb->only_fissionable()) { - fatal_error("Invalid source definition -- fissionable spatial distribution " - "not allowed for random ray source."); + fatal_error( + "Invalid ray source definition -- fissionable spatial distribution " + "not allowed."); } // Check for isotropic source UnitSphereDistribution* angle_dist = is->angle(); Isotropic* id = dynamic_cast(angle_dist); if (!id) { - fatal_error("Invalid source definition -- only isotropic sources are " - "allowed for random ray source."); + fatal_error("Invalid ray source definition -- only isotropic sources are " + "allowed."); + } + + // Validate external sources + /////////////////////////////////////////////////////////////////// + if (settings::run_mode == RunMode::FIXED_SOURCE) { + if (model::external_sources.size() < 1) { + fatal_error("Must provide a particle source (in addition to ray source) " + "in fixed source random ray mode."); + } + + for (int i = 0; i < model::external_sources.size(); i++) { + Source* s = model::external_sources[i].get(); + + // Check for independent source + IndependentSource* is = dynamic_cast(s); + + if (!is) { + fatal_error( + "Only IndependentSource external source types are allowed in " + "random ray mode"); + } + + // Check for isotropic source + UnitSphereDistribution* angle_dist = is->angle(); + Isotropic* id = dynamic_cast(angle_dist); + if (!id) { + fatal_error( + "Invalid source definition -- only isotropic external sources are " + "allowed in random ray mode."); + } + + // Validate that a domain ID was specified + if (is->domain_ids().size() == 0) { + fatal_error("Fixed sources must be specified by domain " + "id (cell, material, or universe) in random ray mode."); + } + + // Check that a discrete energy distribution was used + Distribution* d = is->energy(); + Discrete* dd = dynamic_cast(d); + if (!dd) { + fatal_error( + "Only discrete (multigroup) energy distributions are allowed for " + "external sources in random ray mode."); + } + } } // Validate plotting files @@ -208,6 +245,12 @@ RandomRaySimulation::RandomRaySimulation() void RandomRaySimulation::simulate() { + if (settings::run_mode == RunMode::FIXED_SOURCE) { + // Transfer external source user inputs onto random ray source regions + domain_.convert_external_sources(); + domain_.count_external_source_regions(); + } + // Random ray power iteration loop while (simulation::current_batch < settings::n_batches) { @@ -249,11 +292,13 @@ void RandomRaySimulation::simulate() // Add source to scalar flux, compute number of FSR hits int64_t n_hits = domain_.add_source_to_scalar_flux(); - // Compute random ray k-eff - k_eff_ = domain_.compute_k_eff(k_eff_); + if (settings::run_mode == RunMode::EIGENVALUE) { + // Compute random ray k-eff + k_eff_ = domain_.compute_k_eff(k_eff_); - // Store random ray k-eff into OpenMC's native k-eff variable - global_tally_tracklength = k_eff_; + // Store random ray k-eff into OpenMC's native k-eff variable + global_tally_tracklength = k_eff_; + } // Execute all tallying tasks, if this is an active batch if (simulation::current_batch > settings::n_inactive && mpi::master) { @@ -302,7 +347,7 @@ void RandomRaySimulation::output_simulation_results() const if (mpi::master) { print_results_random_ray(total_geometric_intersections_, avg_miss_rate_ / settings::n_batches, negroups_, - domain_.n_source_regions_); + domain_.n_source_regions_, domain_.n_external_source_regions_); if (model::plots.size() > 0) { domain_.output_to_vtk(); } @@ -339,7 +384,7 @@ void RandomRaySimulation::instability_check( // Print random ray simulation results void RandomRaySimulation::print_results_random_ray( uint64_t total_geometric_intersections, double avg_miss_rate, int negroups, - int64_t n_source_regions) const + int64_t n_source_regions, int64_t n_external_source_regions) const { using namespace simulation; @@ -355,6 +400,8 @@ void RandomRaySimulation::print_results_random_ray( fmt::print( " Total Iterations = {}\n", settings::n_batches); fmt::print(" Flat Source Regions (FSRs) = {}\n", n_source_regions); + fmt::print( + " FSRs Containing External Sources = {}\n", n_external_source_regions); fmt::print(" Total Geometric Intersections = {:.4e}\n", static_cast(total_geometric_intersections)); fmt::print(" Avg per Iteration = {:.4e}\n", @@ -387,7 +434,7 @@ void RandomRaySimulation::print_results_random_ray( show_time("Time per integration", time_per_integration); } - if (settings::verbosity >= 4) { + if (settings::verbosity >= 4 && settings::run_mode == RunMode::EIGENVALUE) { header("Results", 4); fmt::print(" k-effective = {:.5f} +/- {:.5f}\n", simulation::keff, simulation::keff_std); diff --git a/src/settings.cpp b/src/settings.cpp index 6790f2cc0f7..136020d4a18 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -191,7 +191,8 @@ void get_run_parameters(pugi::xml_node node_base) } // Get number of inactive batches - if (run_mode == RunMode::EIGENVALUE) { + if (run_mode == RunMode::EIGENVALUE || + solver_type == SolverType::RANDOM_RAY) { if (check_for_node(node_base, "inactive")) { n_inactive = std::stoi(get_node_value(node_base, "inactive")); } @@ -524,8 +525,9 @@ void read_settings_xml(pugi::xml_node root) } // If no source specified, default to isotropic point source at origin with - // Watt spectrum - if (model::external_sources.empty()) { + // Watt spectrum. No default source is needed in random ray mode. + if (model::external_sources.empty() && + settings::solver_type != SolverType::RANDOM_RAY) { double T[] {0.0}; double p[] {1.0}; model::external_sources.push_back(make_unique( diff --git a/src/simulation.cpp b/src/simulation.cpp index 527d98db4f1..e218cdb2928 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -137,7 +137,11 @@ int openmc_simulation_init() // Display header if (mpi::master) { if (settings::run_mode == RunMode::FIXED_SOURCE) { - header("FIXED SOURCE TRANSPORT SIMULATION", 3); + if (settings::solver_type == SolverType::MONTE_CARLO) { + header("FIXED SOURCE TRANSPORT SIMULATION", 3); + } else if (settings::solver_type == SolverType::RANDOM_RAY) { + header("FIXED SOURCE TRANSPORT SIMULATION (RANDOM RAY SOLVER)", 3); + } } else if (settings::run_mode == RunMode::EIGENVALUE) { if (settings::solver_type == SolverType::MONTE_CARLO) { header("K EIGENVALUE SIMULATION", 3); @@ -343,7 +347,13 @@ void initialize_batch() ++simulation::current_batch; if (settings::run_mode == RunMode::FIXED_SOURCE) { - write_message(6, "Simulating batch {}", simulation::current_batch); + if (settings::solver_type == SolverType::RANDOM_RAY && + simulation::current_batch < settings::n_inactive + 1) { + write_message( + 6, "Simulating batch {:<4} (inactive)", simulation::current_batch); + } else { + write_message(6, "Simulating batch {}", simulation::current_batch); + } } // Reset total starting particle weight used for normalizing tallies diff --git a/tests/regression_tests/random_ray_basic/results_true.dat b/tests/regression_tests/random_ray_basic/results_true.dat index 802e78a828b..ee2e4000de7 100644 --- a/tests/regression_tests/random_ray_basic/results_true.dat +++ b/tests/regression_tests/random_ray_basic/results_true.dat @@ -1,171 +1,171 @@ k-combined: -8.400322E-01 8.023349E-03 +8.400322E-01 8.023350E-03 tally 1: -1.260220E+00 -3.179889E-01 -1.484289E-01 -4.411066E-03 -3.612463E-01 -2.612843E-02 -7.086707E-01 -1.006119E-01 -3.342483E-02 -2.238499E-04 -8.134936E-02 -1.325949E-03 -4.194328E-01 -3.558669E-02 -4.287776E-03 -3.717447E-06 -1.043559E-02 -2.201986E-05 -5.878720E-01 -7.045887E-02 -6.147757E-03 -7.701173E-06 -1.496241E-02 -4.561699E-05 -1.768113E+00 -6.356917E-01 -6.513486E-03 -8.628535E-06 -1.585272E-02 -5.111136E-05 -5.063704E+00 -5.152401E+00 -2.440293E-03 -1.196869E-06 -6.038334E-03 -7.328193E-06 -3.253717E+00 -2.117655E+00 -1.389120E-02 -3.859385E-05 -3.863767E-02 -2.985798E-04 -1.876994E+00 -7.046366E-01 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -8.390875E-01 -1.408791E-01 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -4.513839E-01 -4.139640E-02 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -6.682186E-01 -9.116003E-02 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.849034E+00 -6.944337E-01 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -4.523425E+00 -4.112118E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.821432E+00 -1.592568E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.159618E+00 -2.694138E-01 -1.354028E-01 -3.672094E-03 -3.295432E-01 -2.175122E-02 -6.880334E-01 -9.491215E-02 -3.234611E-02 -2.097782E-04 -7.872396E-02 -1.242596E-03 -4.184841E-01 -3.536436E-02 -4.274305E-03 -3.687209E-06 -1.040280E-02 -2.184074E-05 -5.810180E-01 -6.872944E-02 -6.060273E-03 -7.476500E-06 -1.474949E-02 -4.428617E-05 -1.782580E+00 -6.457892E-01 -6.552384E-03 -8.730345E-06 -1.594739E-02 -5.171444E-05 -5.278155E+00 -5.596601E+00 -2.546878E-03 -1.303010E-06 -6.302072E-03 -7.978072E-06 -3.420419E+00 -2.340454E+00 -1.465798E-02 -4.299061E-05 -4.077042E-02 -3.325951E-04 -1.279417E+00 -3.278133E-01 -1.509073E-01 -4.561836E-03 -3.672782E-01 -2.702150E-02 -7.212777E-01 -1.042487E-01 -3.411552E-02 -2.332877E-04 -8.303035E-02 -1.381852E-03 -4.269473E-01 -3.685202E-02 -4.378540E-03 -3.872997E-06 -1.065649E-02 -2.294124E-05 -5.973530E-01 -7.266946E-02 -6.260881E-03 -7.976490E-06 -1.523773E-02 -4.724780E-05 -1.795373E+00 -6.547440E-01 -6.635941E-03 -8.945067E-06 -1.615075E-02 -5.298634E-05 -5.161876E+00 -5.353441E+00 -2.505311E-03 -1.261399E-06 -6.199218E-03 -7.723296E-06 -3.344042E+00 -2.236603E+00 -1.443089E-02 -4.166228E-05 -4.013879E-02 -3.223186E-04 +5.086560E+00 +5.180937E+00 +1.885166E+00 +7.115505E-01 +4.588117E+00 +4.214785E+00 +2.860401E+00 +1.639329E+00 +4.245221E-01 +3.610930E-02 +1.033202E+00 +2.138892E-01 +1.692631E+00 +5.793967E-01 +5.445818E-02 +5.996625E-04 +1.325403E-01 +3.552030E-03 +2.372249E+00 +1.146944E+00 +7.808143E-02 +1.242279E-03 +1.900346E-01 +7.358492E-03 +7.134949E+00 +1.034824E+01 +8.272648E-02 +1.391872E-03 +2.013422E-01 +8.244790E-03 +2.043539E+01 +8.389902E+01 +3.099367E-02 +1.930673E-04 +7.669167E-02 +1.182113E-03 +1.313212E+01 +3.449537E+01 +1.764293E-01 +6.225587E-03 +4.907293E-01 +4.816401E-02 +7.567717E+00 +1.145439E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +3.383194E+00 +2.290469E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.819673E+00 +6.726159E-01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.693683E+00 +1.480961E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +7.453759E+00 +1.128171E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.823561E+01 +6.681652E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.137517E+01 +2.588512E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.601917E+00 +4.242626E+00 +1.719723E+00 +5.923467E-01 +4.185463E+00 +3.508696E+00 +2.730305E+00 +1.494324E+00 +4.108214E-01 +3.383938E-02 +9.998572E-01 +2.004436E-01 +1.660852E+00 +5.570433E-01 +5.428709E-02 +5.947848E-04 +1.321239E-01 +3.523137E-03 +2.306069E+00 +1.082856E+00 +7.697032E-02 +1.206037E-03 +1.873304E-01 +7.143816E-03 +7.075194E+00 +1.017519E+01 +8.322052E-02 +1.408295E-03 +2.025446E-01 +8.342072E-03 +2.094832E+01 +8.816716E+01 +3.234739E-02 +2.101889E-04 +8.004135E-02 +1.286945E-03 +1.357413E+01 +3.685984E+01 +1.861680E-01 +6.934828E-03 +5.178169E-01 +5.365103E-02 +5.072150E+00 +5.151431E+00 +1.916644E+00 +7.358713E-01 +4.664727E+00 +4.358847E+00 +2.859465E+00 +1.638250E+00 +4.332944E-01 +3.763171E-02 +1.054552E+00 +2.229070E-01 +1.693008E+00 +5.796672E-01 +5.561096E-02 +6.247543E-04 +1.353459E-01 +3.700658E-03 +2.368860E+00 +1.143296E+00 +7.951820E-02 +1.286690E-03 +1.935314E-01 +7.621558E-03 +7.119587E+00 +1.030023E+01 +8.428176E-02 +1.442932E-03 +2.051275E-01 +8.547244E-03 +2.046758E+01 +8.418768E+01 +3.181946E-02 +2.034766E-04 +7.873502E-02 +1.245847E-03 +1.325834E+01 +3.515919E+01 +1.832838E-01 +6.720556E-03 +5.097947E-01 +5.199331E-02 diff --git a/tests/regression_tests/random_ray_fixed_source/__init__.py b/tests/regression_tests/random_ray_fixed_source/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regression_tests/random_ray_fixed_source/cell/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source/cell/inputs_true.dat new file mode 100644 index 00000000000..99e67745a26 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source/cell/inputs_true.dat @@ -0,0 +1,204 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + 5.0 5.0 5.0 + 2 2 2 + -5.0 -5.0 -5.0 + +1 1 +1 1 + +1 1 +1 1 + + + 5.0 5.0 5.0 + 2 2 2 + -5.0 -5.0 -5.0 + +2 2 +2 2 + +2 2 +2 2 + + + 5.0 5.0 5.0 + 2 2 2 + -5.0 -5.0 -5.0 + +3 3 +3 3 + +3 3 +3 3 + + + 10.0 10.0 10.0 + 6 10 6 + 0.0 0.0 0.0 + +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +8 8 8 8 9 9 +8 9 9 9 9 9 +8 9 9 9 9 9 +8 9 9 9 9 9 +8 9 9 9 9 9 +7 9 9 9 9 9 + +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 8 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 + +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 8 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 + +9 9 9 8 9 9 +9 9 9 8 9 9 +9 9 9 8 9 9 +9 9 9 8 9 9 +9 9 9 8 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 + +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 + +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 + + + + + + + + + + fixed source + 1000 + 10 + 5 + + + 100.0 1.0 + + + cell + 4 + + + multi-group + + 400.0 + 100.0 + + + 0.0 0.0 0.0 60.0 100.0 60.0 + + + + + + + 1 10 1 + 0.0 0.0 0.0 + 10.0 100.0 10.0 + + + 6 1 1 + 0.0 50.0 0.0 + 60.0 60.0 10.0 + + + 6 1 1 + 0.0 90.0 30.0 + 60.0 100.0 40.0 + + + 1 + + + 2 + + + 3 + + + 1 + flux + analog + + + 2 + flux + analog + + + 3 + flux + analog + + + diff --git a/tests/regression_tests/random_ray_fixed_source/cell/results_true.dat b/tests/regression_tests/random_ray_fixed_source/cell/results_true.dat new file mode 100644 index 00000000000..df923ab236a --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source/cell/results_true.dat @@ -0,0 +1,47 @@ +tally 1: +3.751048E+01 +2.841797E+02 +9.930788E+00 +1.988180E+01 +3.781121E+00 +3.005165E+00 +2.383139E+00 +1.232560E+00 +1.561884E+00 +6.440577E-01 +1.089787E+00 +3.724896E-01 +6.608456E-01 +1.285592E-01 +2.372611E-01 +1.601299E-02 +7.814803E-02 +1.765829E-03 +2.862108E-02 +2.460129E-04 +tally 2: +1.089787E+00 +3.724896E-01 +3.767926E-01 +3.724399E-02 +8.614121E-02 +1.526889E-03 +3.610725E-02 +2.629885E-04 +1.466261E-02 +4.536997E-05 +4.653106E-03 +4.381672E-06 +tally 3: +1.617918E-03 +6.317049E-07 +1.161473E-03 +2.789553E-07 +1.198879E-03 +3.189531E-07 +1.031737E-03 +2.207381E-07 +5.466329E-04 +6.166808E-08 +2.146062E-04 +9.937520E-09 diff --git a/tests/regression_tests/random_ray_fixed_source/material/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source/material/inputs_true.dat new file mode 100644 index 00000000000..7fde0c419b5 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source/material/inputs_true.dat @@ -0,0 +1,204 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + 5.0 5.0 5.0 + 2 2 2 + -5.0 -5.0 -5.0 + +1 1 +1 1 + +1 1 +1 1 + + + 5.0 5.0 5.0 + 2 2 2 + -5.0 -5.0 -5.0 + +2 2 +2 2 + +2 2 +2 2 + + + 5.0 5.0 5.0 + 2 2 2 + -5.0 -5.0 -5.0 + +3 3 +3 3 + +3 3 +3 3 + + + 10.0 10.0 10.0 + 6 10 6 + 0.0 0.0 0.0 + +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +8 8 8 8 9 9 +8 9 9 9 9 9 +8 9 9 9 9 9 +8 9 9 9 9 9 +8 9 9 9 9 9 +7 9 9 9 9 9 + +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 8 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 + +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 8 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 + +9 9 9 8 9 9 +9 9 9 8 9 9 +9 9 9 8 9 9 +9 9 9 8 9 9 +9 9 9 8 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 + +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 + +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 + + + + + + + + + + fixed source + 1000 + 10 + 5 + + + 100.0 1.0 + + + material + 1 + + + multi-group + + 400.0 + 100.0 + + + 0.0 0.0 0.0 60.0 100.0 60.0 + + + + + + + 1 10 1 + 0.0 0.0 0.0 + 10.0 100.0 10.0 + + + 6 1 1 + 0.0 50.0 0.0 + 60.0 60.0 10.0 + + + 6 1 1 + 0.0 90.0 30.0 + 60.0 100.0 40.0 + + + 1 + + + 2 + + + 3 + + + 1 + flux + analog + + + 2 + flux + analog + + + 3 + flux + analog + + + diff --git a/tests/regression_tests/random_ray_fixed_source/material/results_true.dat b/tests/regression_tests/random_ray_fixed_source/material/results_true.dat new file mode 100644 index 00000000000..e3d52890919 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source/material/results_true.dat @@ -0,0 +1,47 @@ +tally 1: +3.751047E+01 +2.841797E+02 +9.930788E+00 +1.988181E+01 +3.781121E+00 +3.005165E+00 +2.383139E+00 +1.232560E+00 +1.561884E+00 +6.440577E-01 +1.089787E+00 +3.724896E-01 +6.608456E-01 +1.285592E-01 +2.372611E-01 +1.601299E-02 +7.814803E-02 +1.765829E-03 +2.862108E-02 +2.460129E-04 +tally 2: +1.089787E+00 +3.724896E-01 +3.767925E-01 +3.724398E-02 +8.614120E-02 +1.526889E-03 +3.610725E-02 +2.629885E-04 +1.466261E-02 +4.536997E-05 +4.653106E-03 +4.381672E-06 +tally 3: +1.617918E-03 +6.317049E-07 +1.161473E-03 +2.789553E-07 +1.198879E-03 +3.189531E-07 +1.031737E-03 +2.207381E-07 +5.466329E-04 +6.166809E-08 +2.146062E-04 +9.937520E-09 diff --git a/tests/regression_tests/random_ray_fixed_source/test.py b/tests/regression_tests/random_ray_fixed_source/test.py new file mode 100644 index 00000000000..41a6c35bb03 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source/test.py @@ -0,0 +1,339 @@ +import os + +import numpy as np +import openmc +from openmc.utility_funcs import change_directory +import pytest + +from tests.testing_harness import TolerantPyAPITestHarness + +def fill_3d_list(n, val): + """ + Generates a 3D list of dimensions nxnxn filled with copies of val. + + Parameters: + n (int): The dimension of the 3D list. + val (any): The value to fill the 3D list with. + + Returns: + list: A 3D list of dimensions nxnxn filled with val. + """ + return [[[val for _ in range(n)] for _ in range(n)] for _ in range(n)] + + +class MGXSTestHarness(TolerantPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + +def create_random_ray_model(domain_type): + openmc.reset_auto_ids() + ############################################################################### + # Create multigroup data + + # Instantiate the energy group data + ebins = [1e-5, 20.0e6] + groups = openmc.mgxs.EnergyGroups(group_edges=ebins) + + # High scattering ratio means system is all scattering + # Low means fully absorbing + scattering_ratio = 0.5 + + source_total_xs = 0.1 + source_mat_data = openmc.XSdata('source', groups) + source_mat_data.order = 0 + source_mat_data.set_total([source_total_xs]) + source_mat_data.set_absorption([source_total_xs * (1.0 - scattering_ratio)]) + source_mat_data.set_scatter_matrix(np.rollaxis(np.array([[[source_total_xs * scattering_ratio]]]),0,3)) + + void_total_xs = 1.0e-4 + void_mat_data = openmc.XSdata('void', groups) + void_mat_data.order = 0 + void_mat_data.set_total([void_total_xs]) + void_mat_data.set_absorption([void_total_xs * (1.0 - scattering_ratio)]) + void_mat_data.set_scatter_matrix(np.rollaxis(np.array([[[void_total_xs * scattering_ratio]]]),0,3)) + + shield_total_xs = 0.1 + shield_mat_data = openmc.XSdata('shield', groups) + shield_mat_data.order = 0 + shield_mat_data.set_total([shield_total_xs]) + shield_mat_data.set_absorption([shield_total_xs * (1.0 - scattering_ratio)]) + shield_mat_data.set_scatter_matrix(np.rollaxis(np.array([[[shield_total_xs * scattering_ratio]]]),0,3)) + + mg_cross_sections_file = openmc.MGXSLibrary(groups) + mg_cross_sections_file.add_xsdatas([source_mat_data, void_mat_data, shield_mat_data]) + mg_cross_sections_file.export_to_hdf5() + + ############################################################################### + # Create materials for the problem + + # Instantiate some Macroscopic Data + source_data = openmc.Macroscopic('source') + void_data = openmc.Macroscopic('void') + shield_data = openmc.Macroscopic('shield') + + # Instantiate some Materials and register the appropriate Macroscopic objects + source_mat = openmc.Material(name='source') + source_mat.set_density('macro', 1.0) + source_mat.add_macroscopic(source_data) + + void_mat = openmc.Material(name='void') + void_mat.set_density('macro', 1.0) + void_mat.add_macroscopic(void_data) + + shield_mat = openmc.Material(name='shield') + shield_mat.set_density('macro', 1.0) + shield_mat.add_macroscopic(shield_data) + + # Instantiate a Materials collection and export to XML + materials_file = openmc.Materials([source_mat, void_mat, shield_mat]) + materials_file.cross_sections = "mgxs.h5" + + ############################################################################### + # Define problem geometry + + source_cell = openmc.Cell(fill=source_mat, name='infinite source region') + void_cell = openmc.Cell(fill=void_mat, name='infinite void region') + shield_cell = openmc.Cell(fill=shield_mat, name='infinite shield region') + + sub = openmc.Universe() + sub.add_cells([source_cell]) + + vub = openmc.Universe() + vub.add_cells([void_cell]) + + aub = openmc.Universe() + aub.add_cells([shield_cell]) + + # n controls the dimension of subdivision within each outer lattice element + # E.g., n = 10 results in 1cm cubic FSRs + n = 2 + delta = 10.0 / n + ll = [-5.0, -5.0, -5.0] + pitch = [delta, delta, delta] + + source_lattice = openmc.RectLattice() + source_lattice.lower_left = ll + source_lattice.pitch = pitch + source_lattice.universes = fill_3d_list(n, sub) + + void_lattice = openmc.RectLattice() + void_lattice.lower_left = ll + void_lattice.pitch = pitch + void_lattice.universes = fill_3d_list(n, vub) + + shield_lattice = openmc.RectLattice() + shield_lattice.lower_left = ll + shield_lattice.pitch = pitch + shield_lattice.universes = fill_3d_list(n, aub) + + source_lattice_cell = openmc.Cell(fill=source_lattice, name='source lattice cell') + su = openmc.Universe() + su.add_cells([source_lattice_cell]) + + void_lattice_cell = openmc.Cell(fill=void_lattice, name='void lattice cell') + vu = openmc.Universe() + vu.add_cells([void_lattice_cell]) + + shield_lattice_cell = openmc.Cell(fill=shield_lattice, name='shield lattice cell') + au = openmc.Universe() + au.add_cells([shield_lattice_cell]) + + z_base = [ + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [vu, vu, vu, vu, au, au], + [vu, au, au, au, au, au], + [vu, au, au, au, au, au], + [vu, au, au, au, au, au], + [vu, au, au, au, au, au], + [su, au, au, au, au, au] + ] + + z_col = [ + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, vu, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au] + ] + + z_high = [ + [au, au, au, vu, au, au], + [au, au, au, vu, au, au], + [au, au, au, vu, au, au], + [au, au, au, vu, au, au], + [au, au, au, vu, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au] + ] + + z_cap = [ + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au], + [au, au, au, au, au, au] + ] + + dogleg_pattern = [ + z_base, + z_col, + z_col, + z_high, + z_cap, + z_cap + ] + + x = 60.0 + x_dim = 6 + + y = 100.0 + y_dim = 10 + + z = 60.0 + z_dim = 6 + + lattice = openmc.RectLattice() + lattice.lower_left = [0.0, 0.0, 0.0] + lattice.pitch = [x/x_dim, y/y_dim, z/z_dim] + lattice.universes = dogleg_pattern + + lattice_cell = openmc.Cell(fill=lattice, name='dogleg lattice cell') + + lattice_uni = openmc.Universe() + lattice_uni.add_cells([lattice_cell]) + + x_low = openmc.XPlane(x0=0.0,boundary_type='reflective') + x_high = openmc.XPlane(x0=x,boundary_type='vacuum') + + y_low = openmc.YPlane(y0=0.0,boundary_type='reflective') + y_high = openmc.YPlane(y0=y,boundary_type='vacuum') + + z_low = openmc.ZPlane(z0=0.0,boundary_type='reflective') + z_high = openmc.ZPlane(z0=z,boundary_type='vacuum') + + full_domain = openmc.Cell(fill=lattice_uni, region=+x_low & -x_high & +y_low & -y_high & +z_low & -z_high, name='full domain') + + root = openmc.Universe(name='root universe') + root.add_cell(full_domain) + + # Create a geometry with the two cells and export to XML + geometry = openmc.Geometry(root) + + ############################################################################### + # Define problem settings + + # Instantiate a Settings object, set all runtime parameters, and export to XML + settings = openmc.Settings() + settings.energy_mode = "multi-group" + settings.batches = 10 + settings.inactive = 5 + settings.particles = 1000 + settings.run_mode = 'fixed source' + + settings.random_ray['distance_active'] = 400.0 + settings.random_ray['distance_inactive'] = 100.0 + + # Create an initial uniform spatial source for ray integration + lower_left = (0.0, 0.0, 0.0) + upper_right = (x, y, z) + uniform_dist = openmc.stats.Box(lower_left, upper_right) + settings.random_ray['ray_source']= openmc.IndependentSource(space=uniform_dist) + + # Create the neutron source in the bottom right of the moderator + strengths = [1.0] + midpoints = [100.0] + energy_distribution = openmc.stats.Discrete(x=midpoints,p=strengths) + if domain_type == 'cell': + domain = source_lattice_cell + elif domain_type == 'material': + domain = source_mat + elif domain_type == 'universe': + domain = sub + source = openmc.IndependentSource( + energy=energy_distribution, + constraints={'domains': [domain]} + ) + settings.source = [source] + + ############################################################################### + # Define tallies + + estimator = 'analog' + + # Case 3A + mesh_3A = openmc.RegularMesh() + mesh_3A.dimension = (1, y_dim, 1) + mesh_3A.lower_left = (0.0, 0.0, 0.0) + mesh_3A.upper_right = (10.0, y, 10.0) + mesh_filter_3A = openmc.MeshFilter(mesh_3A) + + tally_3A = openmc.Tally(name="Case 3A") + tally_3A.filters = [mesh_filter_3A] + tally_3A.scores = ['flux'] + tally_3A.estimator = estimator + + # Case 3B + mesh_3B = openmc.RegularMesh() + mesh_3B.dimension = (x_dim, 1, 1) + mesh_3B.lower_left = (0.0, 50.0, 0.0) + mesh_3B.upper_right = (x, 60.0, 10.0) + mesh_filter_3B = openmc.MeshFilter(mesh_3B) + + tally_3B = openmc.Tally(name="Case 3B") + tally_3B.filters = [mesh_filter_3B] + tally_3B.scores = ['flux'] + tally_3B.estimator = estimator + + # Case 3C + mesh_3C = openmc.RegularMesh() + mesh_3C.dimension = (x_dim, 1, 1) + mesh_3C.lower_left = (0.0, 90.0, 30.0) + mesh_3C.upper_right = (x, 100.0, 40.0) + mesh_filter_3C = openmc.MeshFilter(mesh_3C) + + tally_3C = openmc.Tally(name="Case 3C") + tally_3C.filters = [mesh_filter_3C] + tally_3C.scores = ['flux'] + tally_3C.estimator = estimator + + # Instantiate a Tallies collection and export to XML + tallies = openmc.Tallies([tally_3A, tally_3B, tally_3C]) + + ############################################################################### + # Assmble Model + + model = openmc.model.Model() + model.geometry = geometry + model.materials = materials_file + model.settings = settings + model.xs_data = mg_cross_sections_file + model.tallies = tallies + + return model + +@pytest.mark.parametrize("domain_type", ["cell", "material", "universe"]) +def test_random_ray_fixed_source(domain_type): + with change_directory(domain_type): + model = create_random_ray_model(domain_type) + + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/regression_tests/random_ray_fixed_source/universe/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source/universe/inputs_true.dat new file mode 100644 index 00000000000..349917fee72 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source/universe/inputs_true.dat @@ -0,0 +1,204 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + 5.0 5.0 5.0 + 2 2 2 + -5.0 -5.0 -5.0 + +1 1 +1 1 + +1 1 +1 1 + + + 5.0 5.0 5.0 + 2 2 2 + -5.0 -5.0 -5.0 + +2 2 +2 2 + +2 2 +2 2 + + + 5.0 5.0 5.0 + 2 2 2 + -5.0 -5.0 -5.0 + +3 3 +3 3 + +3 3 +3 3 + + + 10.0 10.0 10.0 + 6 10 6 + 0.0 0.0 0.0 + +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +8 8 8 8 9 9 +8 9 9 9 9 9 +8 9 9 9 9 9 +8 9 9 9 9 9 +8 9 9 9 9 9 +7 9 9 9 9 9 + +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 8 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 + +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 8 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 + +9 9 9 8 9 9 +9 9 9 8 9 9 +9 9 9 8 9 9 +9 9 9 8 9 9 +9 9 9 8 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 + +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 + +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 +9 9 9 9 9 9 + + + + + + + + + + fixed source + 1000 + 10 + 5 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 400.0 + 100.0 + + + 0.0 0.0 0.0 60.0 100.0 60.0 + + + + + + + 1 10 1 + 0.0 0.0 0.0 + 10.0 100.0 10.0 + + + 6 1 1 + 0.0 50.0 0.0 + 60.0 60.0 10.0 + + + 6 1 1 + 0.0 90.0 30.0 + 60.0 100.0 40.0 + + + 1 + + + 2 + + + 3 + + + 1 + flux + analog + + + 2 + flux + analog + + + 3 + flux + analog + + + diff --git a/tests/regression_tests/random_ray_fixed_source/universe/results_true.dat b/tests/regression_tests/random_ray_fixed_source/universe/results_true.dat new file mode 100644 index 00000000000..36d24e939af --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source/universe/results_true.dat @@ -0,0 +1,47 @@ +tally 1: +3.751047E+01 +2.841797E+02 +9.930788E+00 +1.988180E+01 +3.781121E+00 +3.005165E+00 +2.383139E+00 +1.232560E+00 +1.561884E+00 +6.440577E-01 +1.089787E+00 +3.724896E-01 +6.608456E-01 +1.285592E-01 +2.372611E-01 +1.601299E-02 +7.814803E-02 +1.765829E-03 +2.862107E-02 +2.460129E-04 +tally 2: +1.089787E+00 +3.724896E-01 +3.767926E-01 +3.724399E-02 +8.614120E-02 +1.526889E-03 +3.610725E-02 +2.629885E-04 +1.466261E-02 +4.536997E-05 +4.653106E-03 +4.381672E-06 +tally 3: +1.617918E-03 +6.317049E-07 +1.161473E-03 +2.789553E-07 +1.198879E-03 +3.189531E-07 +1.031737E-03 +2.207381E-07 +5.466329E-04 +6.166808E-08 +2.146062E-04 +9.937520E-09 diff --git a/tests/regression_tests/random_ray_vacuum/results_true.dat b/tests/regression_tests/random_ray_vacuum/results_true.dat index 744ce6cef28..c25999aad5a 100644 --- a/tests/regression_tests/random_ray_vacuum/results_true.dat +++ b/tests/regression_tests/random_ray_vacuum/results_true.dat @@ -1,171 +1,171 @@ k-combined: 1.010455E-01 1.585558E-02 tally 1: -1.849176E-01 -7.634332E-03 -2.181815E-02 -1.062861E-04 -5.310100E-02 -6.295730E-04 -4.048251E-02 -3.851890E-04 -1.893676E-03 -8.448769E-07 -4.608828E-03 -5.004529E-06 -4.063643E-03 -4.022442E-06 -4.112970E-05 -4.186661E-10 -1.001015E-04 -2.479919E-09 -7.467029E-03 -1.178864E-05 -7.688748E-05 -1.266903E-09 -1.871288E-04 -7.504350E-09 -3.870644E-02 -3.010745E-04 -1.375240E-04 -3.807356E-09 -3.347099E-04 -2.255298E-08 -4.524967E-01 -4.098857E-02 -2.437418E-04 -1.190325E-08 -6.031220E-04 -7.288126E-08 -4.989226E-01 -4.993728E-02 -2.374296E-03 -1.135824E-06 -6.603983E-03 -8.787258E-06 -3.899991E-01 -3.308783E-02 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -7.108982E-02 -1.144390E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -5.295259E-03 -6.352159E-06 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -9.852001E-03 -1.984406E-05 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -4.414391E-02 -3.905201E-04 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.571668E-01 -1.323140E-02 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.752932E-01 -1.517930E-02 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.465446E-01 -4.901884E-03 -1.700791E-02 -6.587674E-05 -4.139385E-02 -3.902131E-04 -3.424032E-02 -2.841338E-04 -1.598985E-03 -6.216312E-07 -3.891610E-03 -3.682159E-06 -4.067582E-03 -4.209468E-06 -4.152829E-05 -4.494649E-10 -1.010715E-04 -2.662352E-09 -7.526712E-03 -1.225032E-05 -7.769969E-05 -1.328443E-09 -1.891055E-04 -7.868877E-09 -4.008649E-02 -3.246821E-04 -1.417944E-04 -4.070719E-09 -3.451035E-04 -2.411301E-08 -4.859902E-01 -4.747592E-02 -2.606214E-04 -1.369749E-08 -6.448895E-04 -8.386705E-08 -5.475198E-01 -6.061269E-02 -2.625477E-03 -1.405458E-06 -7.302631E-03 -1.087327E-05 -1.909660E-01 -8.147906E-03 -2.269063E-02 -1.149570E-04 -5.522446E-02 -6.809342E-04 -4.196583E-02 -4.141620E-04 -1.980406E-03 -9.227119E-07 -4.819913E-03 -5.465576E-06 -4.247004E-03 -4.420116E-06 -4.341806E-05 -4.691518E-10 -1.056709E-04 -2.778965E-09 -7.742814E-03 -1.272112E-05 -8.039606E-05 -1.389209E-09 -1.956679E-04 -8.228817E-09 -3.982370E-02 -3.190931E-04 -1.427171E-04 -4.103942E-09 -3.473492E-04 -2.430981E-08 -4.849535E-01 -4.707014E-02 -2.678327E-04 -1.438540E-08 -6.627333E-04 -8.807897E-08 -5.493457E-01 -6.069440E-02 -2.717400E-03 -1.501450E-06 -7.558312E-03 -1.161591E-05 +7.466984E-01 +1.245920E-01 +2.771079E-01 +1.714504E-02 +6.744252E-01 +1.015566E-01 +1.634870E-01 +6.288701E-03 +2.405120E-02 +1.362874E-04 +5.853580E-02 +8.072823E-04 +1.641162E-02 +6.568765E-05 +5.223801E-04 +6.753516E-08 +1.271369E-03 +4.000365E-07 +3.014736E-02 +1.922973E-04 +9.765325E-04 +2.043645E-07 +2.376685E-03 +1.210529E-06 +1.562379E-01 +4.906526E-03 +1.746664E-03 +6.141658E-07 +4.251084E-03 +3.638028E-06 +1.826385E+00 +6.678141E-01 +3.095716E-03 +1.920117E-06 +7.660132E-03 +1.175650E-05 +2.013809E+00 +8.136726E-01 +3.015545E-02 +1.832201E-04 +8.387586E-02 +1.417475E-03 +1.573069E+00 +5.388374E-01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.867849E-01 +1.864798E-02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.136372E-02 +1.035499E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +3.973323E-02 +3.229766E-04 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.779974E-01 +6.350613E-03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.036886E+00 +2.151147E-01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.109991E+00 +2.468005E-01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.813217E-01 +7.704274E-02 +2.160141E-01 +1.062660E-02 +5.257350E-01 +6.294540E-02 +1.358032E-01 +4.462239E-03 +2.030839E-02 +1.002755E-04 +4.942655E-02 +5.939703E-04 +1.612888E-02 +6.604109E-05 +5.274425E-04 +7.250333E-08 +1.283689E-03 +4.294649E-07 +2.985723E-02 +1.925418E-04 +9.868482E-04 +2.142916E-07 +2.401791E-03 +1.269331E-06 +1.590678E-01 +5.110940E-03 +1.800903E-03 +6.566490E-07 +4.383091E-03 +3.889678E-06 +1.928611E+00 +7.475889E-01 +3.310101E-03 +2.209547E-06 +8.190613E-03 +1.352862E-05 +2.172777E+00 +9.544513E-01 +3.334566E-02 +2.267149E-04 +9.274926E-02 +1.753971E-03 +7.566911E-01 +1.278105E-01 +2.881892E-01 +1.854375E-02 +7.013949E-01 +1.098417E-01 +1.662732E-01 +6.495387E-03 +2.515274E-02 +1.488430E-04 +6.121675E-02 +8.816538E-04 +1.682747E-02 +6.933017E-05 +5.514441E-04 +7.567901E-08 +1.342104E-03 +4.482757E-07 +3.068773E-02 +1.997062E-04 +1.021094E-03 +2.240938E-07 +2.485139E-03 +1.327393E-06 +1.578722E-01 +5.013656E-03 +1.812622E-03 +6.620083E-07 +4.411613E-03 +3.921424E-06 +1.922798E+00 +7.400474E-01 +3.401690E-03 +2.320513E-06 +8.417243E-03 +1.420805E-05 +2.178318E+00 +9.546106E-01 +3.451316E-02 +2.421994E-04 +9.599661E-02 +1.873766E-03 From 8be35cd7b5c9e761cacfb49c258157cfaa850211 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 17 Jun 2024 14:25:05 -0500 Subject: [PATCH 011/184] Fix bug with invalidated iterators when enforcing precedence in region expressions (#2950) --- include/openmc/cell.h | 3 +-- src/cell.cpp | 47 +++++++++++++++++++++---------------------- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/include/openmc/cell.h b/include/openmc/cell.h index e68443a32fd..d78614f057e 100644 --- a/include/openmc/cell.h +++ b/include/openmc/cell.h @@ -128,8 +128,7 @@ class Region { void add_precedence(); //! Add parenthesis to enforce precedence - std::vector::iterator add_parentheses( - std::vector::iterator start); + gsl::index add_parentheses(gsl::index start); //! Remove complement operators from the expression void remove_complement_ops(); diff --git a/src/cell.cpp b/src/cell.cpp index a46f05687eb..88876678706 100644 --- a/src/cell.cpp +++ b/src/cell.cpp @@ -578,17 +578,14 @@ void Region::apply_demorgan( //! precedence than unions using parentheses. //============================================================================== -std::vector::iterator Region::add_parentheses( - std::vector::iterator start) +gsl::index Region::add_parentheses(gsl::index start) { - int32_t start_token = *start; - // Add left parenthesis - if (start_token == OP_INTERSECTION) { - start = expression_.insert(start - 1, OP_LEFT_PAREN); - } else { - start = expression_.insert(start + 1, OP_LEFT_PAREN); + int32_t start_token = expression_[start]; + // Add left parenthesis and set new position to be after parenthesis + if (start_token == OP_UNION) { + start += 2; } - start++; + expression_.insert(expression_.begin() + start - 1, OP_LEFT_PAREN); // Keep track of return iterator distance. If we don't encounter a left // parenthesis, we return an iterator corresponding to wherever the right @@ -600,23 +597,23 @@ std::vector::iterator Region::add_parentheses( // Add right parenthesis // While the start iterator is within the bounds of infix - while (start < expression_.end()) { + while (start + 1 < expression_.size()) { start++; // If the current token is an operator and is different than the start token - if (*start >= OP_UNION && *start != start_token) { + if (expression_[start] >= OP_UNION && expression_[start] != start_token) { // Skip wrapped regions but save iterator position to check precedence and // add right parenthesis, right parenthesis position depends on the // operator, when the operator is a union then do not include the operator // in the region, when the operator is an intersection then include the // operator and next surface - if (*start == OP_LEFT_PAREN) { - return_it_dist = std::distance(expression_.begin(), start); + if (expression_[start] == OP_LEFT_PAREN) { + return_it_dist = start; int depth = 1; do { start++; - if (*start > OP_COMPLEMENT) { - if (*start == OP_RIGHT_PAREN) { + if (expression_[start] > OP_COMPLEMENT) { + if (expression_[start] == OP_RIGHT_PAREN) { depth--; } else { depth++; @@ -624,10 +621,12 @@ std::vector::iterator Region::add_parentheses( } } while (depth > 0); } else { - start = expression_.insert( - start_token == OP_UNION ? start - 1 : start, OP_RIGHT_PAREN); + if (start_token == OP_UNION) { + --start; + } + expression_.insert(expression_.begin() + start, OP_RIGHT_PAREN); if (return_it_dist > 0) { - return expression_.begin() + return_it_dist; + return return_it_dist; } else { return start - 1; } @@ -638,7 +637,7 @@ std::vector::iterator Region::add_parentheses( // return iterator expression_.push_back(OP_RIGHT_PAREN); if (return_it_dist > 0) { - return expression_.begin() + return_it_dist; + return return_it_dist; } else { return start - 1; } @@ -651,21 +650,21 @@ void Region::add_precedence() int32_t current_op = 0; std::size_t current_dist = 0; - for (auto it = expression_.begin(); it != expression_.end(); it++) { - int32_t token = *it; + for (gsl::index i = 0; i < expression_.size(); i++) { + int32_t token = expression_[i]; if (token == OP_UNION || token == OP_INTERSECTION) { if (current_op == 0) { // Set the current operator if is hasn't been set current_op = token; - current_dist = std::distance(expression_.begin(), it); + current_dist = i; } else if (token != current_op) { // If the current operator doesn't match the token, add parenthesis to // assert precedence if (current_op == OP_INTERSECTION) { - it = add_parentheses(expression_.begin() + current_dist); + i = add_parentheses(current_dist); } else { - it = add_parentheses(it); + i = add_parentheses(i); } current_op = 0; current_dist = 0; From 89d4dafa5ab95503a423b6f18a58f1cea23e68f3 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 17 Jun 2024 16:00:45 -0500 Subject: [PATCH 012/184] Implement policy for Python, C++, and CMake versions (#3035) --- .github/workflows/ci.yml | 6 ---- CMakeLists.txt | 18 +++------- docs/source/devguide/index.rst | 1 + docs/source/devguide/policies.rst | 35 +++++++++++++++++++ docs/source/devguide/styleguide.rst | 2 +- examples/custom_source/CMakeLists.txt | 2 +- .../CMakeLists.txt | 2 +- setup.py | 4 +-- .../deplete_with_transfer_rates/test.py | 1 - tests/unit_tests/test_data_photon.py | 2 -- 10 files changed, 44 insertions(+), 29 deletions(-) create mode 100644 docs/source/devguide/policies.rst diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7aaaf1da4b0..9293e319b42 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,12 +35,6 @@ jobs: vectfit: [n] include: - - python-version: "3.8" - omp: n - mpi: n - - python-version: "3.9" - omp: n - mpi: n - python-version: "3.11" omp: n mpi: n diff --git a/CMakeLists.txt b/CMakeLists.txt index 1841251ff5d..98473e492c4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.10 FATAL_ERROR) +cmake_minimum_required(VERSION 3.16 FATAL_ERROR) project(openmc C CXX) # Set version numbers @@ -16,11 +16,6 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) # Set module path set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/Modules) -# Allow user to specify _ROOT variables -if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.12) - cmake_policy(SET CMP0074 NEW) -endif() - # Enable correct usage of CXX_EXTENSIONS if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.22) cmake_policy(SET CMP0128 NEW) @@ -272,11 +267,6 @@ endif() # xtensor header-only library #=============================================================================== -# CMake 3.13+ will complain about policy CMP0079 unless it is set explicitly -if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.13) - cmake_policy(SET CMP0079 NEW) -endif() - find_package_write_status(xtensor) if (NOT xtensor_FOUND) add_subdirectory(vendor/xtl) @@ -580,9 +570,9 @@ target_compile_options(openmc PRIVATE ${cxxflags}) target_include_directories(openmc PRIVATE ${CMAKE_BINARY_DIR}/include) target_link_libraries(openmc libopenmc) -# Ensure C++14 standard is used and turn off GNU extensions -target_compile_features(openmc PUBLIC cxx_std_14) -target_compile_features(libopenmc PUBLIC cxx_std_14) +# Ensure C++17 standard is used and turn off GNU extensions +target_compile_features(openmc PUBLIC cxx_std_17) +target_compile_features(libopenmc PUBLIC cxx_std_17) set_target_properties(openmc libopenmc PROPERTIES CXX_EXTENSIONS OFF) #=============================================================================== diff --git a/docs/source/devguide/index.rst b/docs/source/devguide/index.rst index d100fdcdfd9..2e131e09490 100644 --- a/docs/source/devguide/index.rst +++ b/docs/source/devguide/index.rst @@ -15,6 +15,7 @@ other related topics. contributing workflow styleguide + policies tests user-input docbuild diff --git a/docs/source/devguide/policies.rst b/docs/source/devguide/policies.rst new file mode 100644 index 00000000000..2cf31998764 --- /dev/null +++ b/docs/source/devguide/policies.rst @@ -0,0 +1,35 @@ +.. _devguide_policies: + +======== +Policies +======== + +--------------------- +Python Version Policy +--------------------- + +OpenMC follows the Scientific Python Ecosystem Coordination guidelines `SPEC 0 +`_ on minimum supported +versions, which recommends that support for Python versions be dropped 3 years +after their initial release. + +------------------- +C++ Standard Policy +------------------- + +C++ code in OpenMC must conform to the most recent C++ standard that is fully +supported in the `version of the gcc compiler +`_ that is distributed with the +oldest version of Ubuntu that is still within its `standard support period +`_. Ubuntu 20.04 LTS will be supported +through April 2025 and is distributed with gcc 9.3.0, which fully supports the +C++17 standard. + +-------------------- +CMake Version Policy +-------------------- + +Similar to the C++ standard policy, the minimum supported version of CMake +corresponds to whatever version is distributed with the oldest version of Ubuntu +still within its standard support period. Ubuntu 20.04 LTS is distributed with +CMake 3.16. diff --git a/docs/source/devguide/styleguide.rst b/docs/source/devguide/styleguide.rst index 3c71d14ad92..b266f5f0222 100644 --- a/docs/source/devguide/styleguide.rst +++ b/docs/source/devguide/styleguide.rst @@ -40,7 +40,7 @@ Follow the `C++ Core Guidelines`_ except when they conflict with another guideline listed here. For convenience, many important guidelines from that list are repeated here. -Conform to the C++14 standard. +Conform to the C++17 standard. Always use C++-style comments (``//``) as opposed to C-style (``/**/``). (It is more difficult to comment out a large section of code that uses C-style diff --git a/examples/custom_source/CMakeLists.txt b/examples/custom_source/CMakeLists.txt index 21463ed51ed..ba5ae94adc6 100644 --- a/examples/custom_source/CMakeLists.txt +++ b/examples/custom_source/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.10 FATAL_ERROR) +cmake_minimum_required(VERSION 3.16 FATAL_ERROR) project(openmc_sources CXX) add_library(source SHARED source_ring.cpp) find_package(OpenMC REQUIRED) diff --git a/examples/parameterized_custom_source/CMakeLists.txt b/examples/parameterized_custom_source/CMakeLists.txt index 8232f3b546a..20dac4d8f40 100644 --- a/examples/parameterized_custom_source/CMakeLists.txt +++ b/examples/parameterized_custom_source/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.10 FATAL_ERROR) +cmake_minimum_required(VERSION 3.16 FATAL_ERROR) project(openmc_sources CXX) add_library(parameterized_source SHARED parameterized_source_ring.cpp) find_package(OpenMC REQUIRED) diff --git a/setup.py b/setup.py index a33037ad3e6..de87e1490ad 100755 --- a/setup.py +++ b/setup.py @@ -53,15 +53,13 @@ 'Topic :: Scientific/Engineering' 'Programming Language :: C++', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', ], # Dependencies - 'python_requires': '>=3.8', + 'python_requires': '>=3.10', 'install_requires': [ 'numpy>=1.9', 'h5py', 'scipy', 'ipython', 'matplotlib', 'pandas', 'lxml', 'uncertainties', 'setuptools' diff --git a/tests/regression_tests/deplete_with_transfer_rates/test.py b/tests/regression_tests/deplete_with_transfer_rates/test.py index 669fd4bea01..9bd5a052b5d 100644 --- a/tests/regression_tests/deplete_with_transfer_rates/test.py +++ b/tests/regression_tests/deplete_with_transfer_rates/test.py @@ -46,7 +46,6 @@ def model(): return openmc.Model(geometry, materials, settings) -@pytest.mark.skipif(sys.version_info < (3, 9), reason="Requires Python 3.9+") @pytest.mark.parametrize("rate, dest_mat, power, ref_result", [ (1e-5, None, 0.0, 'no_depletion_only_removal'), (-1e-5, None, 0.0, 'no_depletion_only_feed'), diff --git a/tests/unit_tests/test_data_photon.py b/tests/unit_tests/test_data_photon.py index 171dd52f3a2..010ac1f353e 100644 --- a/tests/unit_tests/test_data_photon.py +++ b/tests/unit_tests/test_data_photon.py @@ -121,8 +121,6 @@ def test_reactions(element, reaction): reactions[18] -# TODO: Remove skip when support is Python 3.9+ -@pytest.mark.skipif(not hasattr(pd.options, 'future'), reason='pandas version too old') @pytest.mark.parametrize('element', ['Pu'], indirect=True) def test_export_to_hdf5(tmpdir, element): filename = str(tmpdir.join('tmp.h5')) From ce4176deb5b8f64a45c46ad87063c76140ce77dd Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Mon, 17 Jun 2024 16:14:12 -0500 Subject: [PATCH 013/184] Hexagonal lattice iterators (#2921) Co-authored-by: Paul Romano --- include/openmc/lattice.h | 35 +++--- src/geometry_aux.cpp | 3 +- src/lattice.cpp | 46 +++++-- .../cell_instances/test_hex_multilattice.py | 119 ++++++++++++++++++ .../test_rect_multilattice.py} | 22 ++-- 5 files changed, 192 insertions(+), 33 deletions(-) create mode 100644 tests/unit_tests/cell_instances/test_hex_multilattice.py rename tests/unit_tests/{test_cell_instance.py => cell_instances/test_rect_multilattice.py} (84%) diff --git a/include/openmc/lattice.h b/include/openmc/lattice.h index 11dda4a6b85..429d81ddda8 100644 --- a/include/openmc/lattice.h +++ b/include/openmc/lattice.h @@ -56,13 +56,14 @@ class Lattice { virtual ~Lattice() {} - virtual int32_t const& operator[](array const& i_xyz) = 0; + virtual const int32_t& operator[](const array& i_xyz) = 0; virtual LatticeIter begin(); - LatticeIter end(); + virtual LatticeIter end(); + virtual int32_t& back(); virtual ReverseLatticeIter rbegin(); - ReverseLatticeIter rend(); + virtual ReverseLatticeIter rend(); //! Convert internal universe values from IDs to indices using universe_map. void adjust_indices(); @@ -81,7 +82,7 @@ class Lattice { //! \param i_xyz[3] The indices for a lattice tile. //! \return true if the given indices fit within the lattice bounds. False //! otherwise. - virtual bool are_valid_indices(array const& i_xyz) const = 0; + virtual bool are_valid_indices(const array& i_xyz) const = 0; //! \brief Find the next lattice surface crossing //! \param r A 3D Cartesian coordinate. @@ -125,7 +126,7 @@ class Lattice { //! \param i_xyz[3] The indices for a lattice tile. //! \return Distribcell offset i.e. the largest instance number for the target //! cell found in the geometry tree under this lattice tile. - virtual int32_t& offset(int map, array const& i_xyz) = 0; + virtual int32_t& offset(int map, const array& i_xyz) = 0; //! \brief Get the distribcell offset for a lattice tile. //! \param The map index for the target cell. @@ -167,12 +168,12 @@ class LatticeIter { LatticeIter& operator++() { - while (indx_ < lat_.universes_.size()) { + while (indx_ < lat_.end().indx_) { ++indx_; if (lat_.is_valid_index(indx_)) return *this; } - indx_ = lat_.universes_.size(); + indx_ = lat_.end().indx_; return *this; } @@ -190,7 +191,7 @@ class ReverseLatticeIter : public LatticeIter { ReverseLatticeIter& operator++() { - while (indx_ > -1) { + while (indx_ > lat_.begin().indx_ - 1) { --indx_; if (lat_.is_valid_index(indx_)) return *this; @@ -206,9 +207,9 @@ class RectLattice : public Lattice { public: explicit RectLattice(pugi::xml_node lat_node); - int32_t const& operator[](array const& i_xyz) override; + const int32_t& operator[](const array& i_xyz) override; - bool are_valid_indices(array const& i_xyz) const override; + bool are_valid_indices(const array& i_xyz) const override; std::pair> distance( Position r, Direction u, const array& i_xyz) const override; @@ -221,7 +222,7 @@ class RectLattice : public Lattice { Position get_local_position( Position r, const array& i_xyz) const override; - int32_t& offset(int map, array const& i_xyz) override; + int32_t& offset(int map, const array& i_xyz) override; int32_t offset(int map, int indx) const override; @@ -241,13 +242,19 @@ class HexLattice : public Lattice { public: explicit HexLattice(pugi::xml_node lat_node); - int32_t const& operator[](array const& i_xyz) override; + const int32_t& operator[](const array& i_xyz) override; LatticeIter begin() override; ReverseLatticeIter rbegin() override; - bool are_valid_indices(array const& i_xyz) const override; + LatticeIter end() override; + + int32_t& back() override; + + ReverseLatticeIter rend() override; + + bool are_valid_indices(const array& i_xyz) const override; std::pair> distance( Position r, Direction u, const array& i_xyz) const override; @@ -262,7 +269,7 @@ class HexLattice : public Lattice { bool is_valid_index(int indx) const override; - int32_t& offset(int map, array const& i_xyz) override; + int32_t& offset(int map, const array& i_xyz) override; int32_t offset(int map, int indx) const override; diff --git a/src/geometry_aux.cpp b/src/geometry_aux.cpp index 10b239be838..050d4db968c 100644 --- a/src/geometry_aux.cpp +++ b/src/geometry_aux.cpp @@ -530,7 +530,8 @@ std::string distribcell_path_inner(int32_t target_cell, int32_t map, if (c.type_ != Fill::MATERIAL) { int32_t temp_offset; if (c.type_ == Fill::UNIVERSE) { - temp_offset = offset + c.offset_[map]; + temp_offset = + offset + c.offset_[map]; // TODO: should also apply to lattice fills? } else { Lattice& lat = *model::lattices[c.fill_]; int32_t indx = lat.universes_.size() * map + lat.begin().indx_; diff --git a/src/lattice.cpp b/src/lattice.cpp index fa2e2828ecf..73005ad096c 100644 --- a/src/lattice.cpp +++ b/src/lattice.cpp @@ -58,6 +58,11 @@ LatticeIter Lattice::end() return LatticeIter(*this, universes_.size()); } +int32_t& Lattice::back() +{ + return universes_.back(); +} + ReverseLatticeIter Lattice::rbegin() { return ReverseLatticeIter(*this, universes_.size() - 1); @@ -106,9 +111,10 @@ int32_t Lattice::fill_offset_table(int32_t offset, int32_t target_univ_id, // offsets_ array doesn't actually include the offset accounting for the last // universe, so we get the before-last offset for the given map and then // explicitly add the count for the last universe. - if (offsets_[map * universes_.size()] != C_NONE) { - int last_offset = offsets_[(map + 1) * universes_.size() - 1]; - int last_univ = universes_.back(); + if (offsets_[map * universes_.size() + this->begin().indx_] != C_NONE) { + int last_offset = + offsets_[(map + 1) * universes_.size() - this->begin().indx_ - 1]; + int last_univ = this->back(); return last_offset + count_universe_instances(last_univ, target_univ_id, univ_count_memo); } @@ -117,6 +123,7 @@ int32_t Lattice::fill_offset_table(int32_t offset, int32_t target_univ_id, offsets_[map * universes_.size() + it.indx_] = offset; offset += count_universe_instances(*it, target_univ_id, univ_count_memo); } + return offset; } @@ -225,14 +232,14 @@ RectLattice::RectLattice(pugi::xml_node lat_node) : Lattice {lat_node} //============================================================================== -int32_t const& RectLattice::operator[](array const& i_xyz) +const int32_t& RectLattice::operator[](const array& i_xyz) { return universes_[get_flat_index(i_xyz)]; } //============================================================================== -bool RectLattice::are_valid_indices(array const& i_xyz) const +bool RectLattice::are_valid_indices(const array& i_xyz) const { return ((i_xyz[0] >= 0) && (i_xyz[0] < n_cells_[0]) && (i_xyz[1] >= 0) && (i_xyz[1] < n_cells_[1]) && (i_xyz[2] >= 0) && @@ -354,7 +361,7 @@ Position RectLattice::get_local_position( //============================================================================== -int32_t& RectLattice::offset(int map, array const& i_xyz) +int32_t& RectLattice::offset(int map, const array& i_xyz) { return offsets_[n_cells_[0] * n_cells_[1] * n_cells_[2] * map + n_cells_[0] * n_cells_[1] * i_xyz[2] + @@ -676,13 +683,19 @@ void HexLattice::fill_lattice_y(const vector& univ_words) //============================================================================== -int32_t const& HexLattice::operator[](array const& i_xyz) +const int32_t& HexLattice::operator[](const array& i_xyz) { return universes_[get_flat_index(i_xyz)]; } //============================================================================== +// The HexLattice iterators need their own versions b/c the universes array is +// "square", meaning that it is allocated with entries that are intentionally +// left empty. As such, the iterator indices need to skip the empty entries to +// get cell instances and geometry paths correct. See the image in the Theory +// and Methodology section on "Hexagonal Lattice Indexing" for a visual of where +// the empty positions are. LatticeIter HexLattice::begin() { return LatticeIter(*this, n_rings_ - 1); @@ -693,9 +706,24 @@ ReverseLatticeIter HexLattice::rbegin() return ReverseLatticeIter(*this, universes_.size() - n_rings_); } +int32_t& HexLattice::back() +{ + return universes_[universes_.size() - n_rings_]; +} + +LatticeIter HexLattice::end() +{ + return LatticeIter(*this, universes_.size() - n_rings_ + 1); +} + +ReverseLatticeIter HexLattice::rend() +{ + return ReverseLatticeIter(*this, n_rings_ - 2); +} + //============================================================================== -bool HexLattice::are_valid_indices(array const& i_xyz) const +bool HexLattice::are_valid_indices(const array& i_xyz) const { // Check if (x, alpha, z) indices are valid, accounting for number of rings return ((i_xyz[0] >= 0) && (i_xyz[1] >= 0) && (i_xyz[2] >= 0) && @@ -992,7 +1020,7 @@ bool HexLattice::is_valid_index(int indx) const //============================================================================== -int32_t& HexLattice::offset(int map, array const& i_xyz) +int32_t& HexLattice::offset(int map, const array& i_xyz) { int nx {2 * n_rings_ - 1}; int ny {2 * n_rings_ - 1}; diff --git a/tests/unit_tests/cell_instances/test_hex_multilattice.py b/tests/unit_tests/cell_instances/test_hex_multilattice.py new file mode 100644 index 00000000000..3f503fe9b84 --- /dev/null +++ b/tests/unit_tests/cell_instances/test_hex_multilattice.py @@ -0,0 +1,119 @@ +from math import sqrt + +import pytest +import numpy as np +import openmc +import openmc.lib + +from tests import cdtemp + + +@pytest.fixture(scope='module', autouse=True) +def double_hex_lattice_model(): + openmc.reset_auto_ids() + radius = 0.9 + pin_lattice_pitch = 2.0 + # make the hex prism a little larger to make sure test + # locations are definitively in the model + hex_prism_edge = 1.2 * pin_lattice_pitch + + model = openmc.Model() + + # materials + nat_u = openmc.Material() + nat_u.set_density('g/cm3', 12.0) + nat_u.add_element('U', 1.0) + + graphite = openmc.Material() + graphite.set_density('g/cm3', 1.1995) + graphite.add_element('C', 1.0) + + # zplanes to define lower and upper region + z_low = openmc.ZPlane(-10, boundary_type='vacuum') + z_mid = openmc.ZPlane(0) + z_high = openmc.ZPlane(10, boundary_type='vacuum') + hex_prism = openmc.model.HexagonalPrism( + edge_length=hex_prism_edge, boundary_type='reflective') + + # geometry + cyl = openmc.ZCylinder(r=radius) + univ = openmc.model.pin([cyl], [nat_u, graphite]) + + # create a hexagonal lattice of compacts + hex_lattice = openmc.HexLattice() + hex_lattice.orientation = 'y' + hex_lattice.pitch = (pin_lattice_pitch,) + hex_lattice.center = (0., 0.) + center = [univ] + ring = [univ, univ, univ, univ, univ, univ] + hex_lattice.universes = [ring, center] + lower_hex_cell = openmc.Cell(fill=hex_lattice, region=-hex_prism & +z_low & -z_mid) + upper_hex_cell = openmc.Cell(fill=hex_lattice, region=-hex_prism & +z_mid & -z_high) + hex_cells = [lower_hex_cell, upper_hex_cell] + model.geometry = openmc.Geometry(hex_cells) + + # moderator + cell = next(iter(univ.get_all_cells().values())) + tally = openmc.Tally(tally_id=1) + filter = openmc.DistribcellFilter(cell) + tally.filters = [filter] + tally.scores = ['flux'] + model.tallies = [tally] + + # settings + # source definition. fission source given bounding box of graphite active region + system_LL = (-pin_lattice_pitch*sqrt(3)/2, -pin_lattice_pitch, -5) + system_UR = (pin_lattice_pitch*sqrt(3)/2, pin_lattice_pitch, 5) + source_dist = openmc.stats.Box(system_LL, system_UR) + model.settings.source = openmc.IndependentSource(space=source_dist) + model.settings.particles = 100 + model.settings.inactive = 2 + model.settings.batches = 10 + + with cdtemp(): + model.export_to_xml() + openmc.lib.init() + yield + openmc.lib.finalize() + + +# Lower cell instances +# 6 +# 5 4 +# 3 +# 2 1 +# 0 +# Upper cell instances +# 13 +# 12 11 +# 10 +# 9 8 +# 7 +hex_expected_results = [ + ((0.0, -2.0, -5.0), 0), + ((1.732, -1.0, -5.0), 1), + ((-1.732, -1.0, -5.0), 2), + ((0.0, 0.0, -0.1), 3), + ((1.732, 1.0, -5.0), 4), + ((-1.732, 1.0, -5.0), 5), + ((0.0, 2.0, -0.1), 6), + ((0.0, -2.0, 5.0), 7), + ((1.732, -1.0, 5.0), 8), + ((-1.732, -1.0, 5.0), 9), + ((0.0, 0.0, 5.0), 10), + ((1.732, 1.0, 5.0), 11), + ((-1.732, 1.0, 5.0), 12), + ((0.0, 2.0, 5.0), 13), +] + + +@pytest.mark.parametrize("r,expected_cell_instance", hex_expected_results, ids=str) +def test_cell_instance_hex_multilattice(r, expected_cell_instance): + _, cell_instance = openmc.lib.find_cell(r) + assert cell_instance == expected_cell_instance + + +def test_cell_instance_multilattice_results(): + openmc.lib.run() + tally_results = openmc.lib.tallies[1].mean + assert (tally_results != 0.0).all() diff --git a/tests/unit_tests/test_cell_instance.py b/tests/unit_tests/cell_instances/test_rect_multilattice.py similarity index 84% rename from tests/unit_tests/test_cell_instance.py rename to tests/unit_tests/cell_instances/test_rect_multilattice.py index 25c20cfef22..aaecb3bdac5 100644 --- a/tests/unit_tests/test_cell_instance.py +++ b/tests/unit_tests/cell_instances/test_rect_multilattice.py @@ -1,5 +1,5 @@ -import numpy as np import pytest +import numpy as np import openmc import openmc.lib @@ -8,7 +8,7 @@ @pytest.fixture(scope='module', autouse=True) -def double_lattice_model(): +def double_rect_lattice_model(): openmc.reset_auto_ids() model = openmc.Model() @@ -40,8 +40,9 @@ def double_lattice_model(): cell_with_lattice2.translation = (2., 0., 0.) model.geometry = openmc.Geometry([cell_with_lattice1, cell_with_lattice2]) - tally = openmc.Tally() - tally.filters = [openmc.DistribcellFilter(c)] + tally = openmc.Tally(tally_id=1) + dcell_filter = openmc.DistribcellFilter(c) + tally.filters = [dcell_filter] tally.scores = ['flux'] model.tallies = [tally] @@ -50,7 +51,8 @@ def double_lattice_model(): bbox[0][2] = -0.5 bbox[1][2] = 0.5 space = openmc.stats.Box(*bbox) - model.settings.source = openmc.IndependentSource(space=space) + source = openmc.IndependentSource(space=space) + model.settings.source = source # Add necessary settings and export model.settings.batches = 10 @@ -63,14 +65,13 @@ def double_lattice_model(): yield openmc.lib.finalize() - # This shows the expected cell instance numbers for each lattice position: # ┌─┬─┬─┬─┐ # │2│3│6│7│ # ├─┼─┼─┼─┤ # │0│1│4│5│ # └─┴─┴─┴─┘ -expected_results = [ +rect_expected_results = [ ((0.5, 0.5, 0.0), 0), ((1.5, 0.5, 0.0), 1), ((0.5, 1.5, 0.0), 2), @@ -80,13 +81,16 @@ def double_lattice_model(): ((2.5, 1.5, 0.0), 6), ((3.5, 1.5, 0.0), 7), ] -@pytest.mark.parametrize("r,expected_cell_instance", expected_results) -def test_cell_instance_multilattice(r, expected_cell_instance): + + +@pytest.mark.parametrize("r,expected_cell_instance", rect_expected_results, ids=lambda p : f'{p}') +def test_cell_instance_rect_multilattice(r, expected_cell_instance): _, cell_instance = openmc.lib.find_cell(r) assert cell_instance == expected_cell_instance def test_cell_instance_multilattice_results(): + openmc.run() openmc.lib.run() tally_results = openmc.lib.tallies[1].mean assert (tally_results != 0.0).all() From 97537d5e9ae9284262bc1d54cc3e51227627f397 Mon Sep 17 00:00:00 2001 From: vanessalulla Date: Mon, 17 Jun 2024 23:31:34 -0400 Subject: [PATCH 014/184] Rename max_splits to max_history_splits, set default value to 1.0e7 (#2954) Co-authored-by: Patrick Shriwise Co-authored-by: Paul Romano --- docs/source/io_formats/settings.rst | 4 +-- include/openmc/settings.h | 3 +- openmc/settings.py | 36 ++++++++++--------- src/finalize.cpp | 4 +-- src/settings.cpp | 7 ++-- src/weight_windows.cpp | 2 +- .../weightwindows/generators/test.py | 2 +- .../weightwindows/inputs_true.dat | 2 +- tests/regression_tests/weightwindows/test.py | 2 +- tests/unit_tests/weightwindows/test.py | 2 +- tests/unit_tests/weightwindows/test_ww_gen.py | 2 +- 11 files changed, 36 insertions(+), 30 deletions(-) diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index ae3266dabed..6e9a7f2abc0 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -285,10 +285,10 @@ then, OpenMC will only use up to the :math:`P_1` data. :ref:`energy_mode`. ------------------------ -```` Element +```` Element ------------------------ -The ```` element indicates the number of times a particle can split during a history. +The ```` element indicates the number of times a particle can split during a history. *Default*: 1000 diff --git a/include/openmc/settings.h b/include/openmc/settings.h index b71ec364931..df4a54a6f6d 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -121,7 +121,8 @@ extern std::unordered_set statepoint_batch; //!< Batches when state should be written extern std::unordered_set source_write_surf_id; //!< Surface ids where sources will be written -extern int max_splits; //!< maximum number of particle splits for weight windows +extern int + max_history_splits; //!< maximum number of particle splits for weight windows extern int64_t max_surface_particles; //!< maximum number of particles to be //!< banked on surfaces per process extern TemperatureMethod diff --git a/openmc/settings.py b/openmc/settings.py index 63124602908..9565c2b7863 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -116,7 +116,7 @@ class Settings: .. versionadded:: 0.14.1 max_order : None or int Maximum scattering order to apply globally when in multi-group mode. - max_splits : int + max_history_splits : int Maximum number of times a particle can split during a history .. versionadded:: 0.13 @@ -358,7 +358,7 @@ def __init__(self, **kwargs): self._weight_windows_on = None self._weight_windows_file = None self._weight_window_checkpoints = {} - self._max_splits = None + self._max_history_splits = None self._max_tracks = None self._random_ray = {} @@ -1007,14 +1007,18 @@ def weight_window_checkpoints(self, weight_window_checkpoints: dict): self._weight_window_checkpoints = weight_window_checkpoints @property - def max_splits(self) -> int: - return self._max_splits + def max_splits(self): + raise AttributeError('max_splits has been deprecated. Please use max_history_splits instead') - @max_splits.setter - def max_splits(self, value: int): + @property + def max_history_splits(self) -> int: + return self._max_history_splits + + @max_history_splits.setter + def max_history_splits(self, value: int): cv.check_type('maximum particle splits', value, Integral) cv.check_greater_than('max particle splits', value, 0) - self._max_splits = value + self._max_history_splits = value @property def max_tracks(self) -> int: @@ -1472,10 +1476,10 @@ def _create_weight_window_checkpoints_subelement(self, root): subelement = ET.SubElement(element, "surface") subelement.text = str(self._weight_window_checkpoints['surface']).lower() - def _create_max_splits_subelement(self, root): - if self._max_splits is not None: - elem = ET.SubElement(root, "max_splits") - elem.text = str(self._max_splits) + def _create_max_history_splits_subelement(self, root): + if self._max_history_splits is not None: + elem = ET.SubElement(root, "max_history_splits") + elem.text = str(self._max_history_splits) def _create_max_tracks_subelement(self, root): if self._max_tracks is not None: @@ -1834,10 +1838,10 @@ def _weight_window_checkpoints_from_xml_element(self, root): value = value in ('true', '1') self.weight_window_checkpoints[key] = value - def _max_splits_from_xml_element(self, root): - text = get_text(root, 'max_splits') + def _max_history_splits_from_xml_element(self, root): + text = get_text(root, 'max_history_splits') if text is not None: - self.max_splits = int(text) + self.max_history_splits = int(text) def _max_tracks_from_xml_element(self, root): text = get_text(root, 'max_tracks') @@ -1915,7 +1919,7 @@ def to_xml_element(self, mesh_memo=None): self._create_weight_window_generators_subelement(element, mesh_memo) self._create_weight_windows_file_element(element) self._create_weight_window_checkpoints_subelement(element) - self._create_max_splits_subelement(element) + self._create_max_history_splits_subelement(element) self._create_max_tracks_subelement(element) self._create_random_ray_subelement(element) @@ -2019,7 +2023,7 @@ def from_xml_element(cls, elem, meshes=None): settings._weight_windows_from_xml_element(elem, meshes) settings._weight_window_generators_from_xml_element(elem, meshes) settings._weight_window_checkpoints_from_xml_element(elem) - settings._max_splits_from_xml_element(elem) + settings._max_history_splits_from_xml_element(elem) settings._max_tracks_from_xml_element(elem) settings._random_ray_from_xml_element(elem) diff --git a/src/finalize.cpp b/src/finalize.cpp index 26efc9723a5..2bde0e3387c 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -91,8 +91,8 @@ int openmc_finalize() settings::max_lost_particles = 10; settings::max_order = 0; settings::max_particles_in_flight = 100000; - settings::max_particle_events = 1000000; - settings::max_splits = 1000; + settings::max_particle_events = 1'000'000; + settings::max_history_splits = 10'000'000; settings::max_tracks = 1000; settings::max_write_lost_particles = -1; settings::n_log_bins = 8000; diff --git a/src/settings.cpp b/src/settings.cpp index 136020d4a18..f5cd1b7e917 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -107,7 +107,7 @@ int max_order {0}; int n_log_bins {8000}; int n_batches; int n_max_batches; -int max_splits {1000}; +int max_history_splits {10'000'000}; int max_tracks {1000}; ResScatMethod res_scat_method {ResScatMethod::rvs}; double res_scat_energy_min {0.01}; @@ -981,8 +981,9 @@ void read_settings_xml(pugi::xml_node root) weight_windows_on = get_node_value_bool(root, "weight_windows_on"); } - if (check_for_node(root, "max_splits")) { - settings::max_splits = std::stoi(get_node_value(root, "max_splits")); + if (check_for_node(root, "max_history_splits")) { + settings::max_history_splits = + std::stoi(get_node_value(root, "max_history_splits")); } if (check_for_node(root, "max_tracks")) { diff --git a/src/weight_windows.cpp b/src/weight_windows.cpp index 1ca83a2bd79..2eb930965b3 100644 --- a/src/weight_windows.cpp +++ b/src/weight_windows.cpp @@ -102,7 +102,7 @@ void apply_weight_windows(Particle& p) // the window if (weight > weight_window.upper_weight) { // do not further split the particle if above the limit - if (p.n_split() >= settings::max_splits) + if (p.n_split() >= settings::max_history_splits) return; double n_split = std::ceil(weight / weight_window.upper_weight); diff --git a/tests/regression_tests/weightwindows/generators/test.py b/tests/regression_tests/weightwindows/generators/test.py index 1d516db8dde..d6a40b44f01 100644 --- a/tests/regression_tests/weightwindows/generators/test.py +++ b/tests/regression_tests/weightwindows/generators/test.py @@ -22,7 +22,7 @@ def test_ww_generator(run_in_tmpdir): model.settings.particles = 500 model.settings.batches = 5 model.settings.run_mode = 'fixed source' - model.settings.max_splits = 100 + model.settings.max_history_splits = 100 mesh = openmc.RegularMesh.from_domain(model.geometry.root_universe) energy_bounds = np.linspace(0.0, 1e6, 70) diff --git a/tests/regression_tests/weightwindows/inputs_true.dat b/tests/regression_tests/weightwindows/inputs_true.dat index c40f4b7615e..dcc41ae8417 100644 --- a/tests/regression_tests/weightwindows/inputs_true.dat +++ b/tests/regression_tests/weightwindows/inputs_true.dat @@ -70,7 +70,7 @@ 10 1e-38 - 200 + 200 diff --git a/tests/regression_tests/weightwindows/test.py b/tests/regression_tests/weightwindows/test.py index 9ed8559760f..864f4f062dc 100644 --- a/tests/regression_tests/weightwindows/test.py +++ b/tests/regression_tests/weightwindows/test.py @@ -48,7 +48,7 @@ def model(): settings.run_mode = 'fixed source' settings.particles = 200 settings.batches = 2 - settings.max_splits = 200 + settings.max_history_splits = 200 settings.photon_transport = True space = Point((0.001, 0.001, 0.001)) energy = Discrete([14E6], [1.0]) diff --git a/tests/unit_tests/weightwindows/test.py b/tests/unit_tests/weightwindows/test.py index 8af843cbdc0..d3da0cd1176 100644 --- a/tests/unit_tests/weightwindows/test.py +++ b/tests/unit_tests/weightwindows/test.py @@ -94,7 +94,7 @@ def model(): settings.run_mode = 'fixed source' settings.particles = 500 settings.batches = 2 - settings.max_splits = 100 + settings.max_history_splits = 100 settings.photon_transport = True space = Point((0.001, 0.001, 0.001)) energy = Discrete([14E6], [1.0]) diff --git a/tests/unit_tests/weightwindows/test_ww_gen.py b/tests/unit_tests/weightwindows/test_ww_gen.py index 072e332cb75..6fc3747b1da 100644 --- a/tests/unit_tests/weightwindows/test_ww_gen.py +++ b/tests/unit_tests/weightwindows/test_ww_gen.py @@ -55,7 +55,7 @@ def model(): run_mode='fixed source', particles=100, batches=10, - max_splits=10, + max_history_splits=10, survival_biasing=False ) From b0732cb6b3754a2718b7c6c028f151e317a5174f Mon Sep 17 00:00:00 2001 From: Lorenzo Chierici Date: Tue, 18 Jun 2024 13:31:07 +0200 Subject: [PATCH 015/184] add dagmc fill materials to homogenized materials method (#3026) Co-authored-by: Paul Romano --- openmc/mesh.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/openmc/mesh.py b/openmc/mesh.py index 215c9192336..5456abd1aed 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -211,6 +211,18 @@ def get_homogenized_materials( # Create homogenized material for each element materials = model.geometry.get_all_materials() + + # Account for materials in DAGMC universes + # TODO: This should really get incorporated in lower-level calls to + # get_all_materials, but right now it requires information from the + # Model object + for cell in model.geometry.get_all_cells().values(): + if isinstance(cell.fill, openmc.DAGMCUniverse): + names = cell.fill.material_names + materials.update({ + mat.id: mat for mat in model.materials if mat.name in names + }) + homogenized_materials = [] for mat_volume_list in mat_volume_by_element: material_ids, volumes = [list(x) for x in zip(*mat_volume_list)] From e33e66aa888453efaf43afcc94e7d6b2c74b1e7c Mon Sep 17 00:00:00 2001 From: pitkajuh Date: Tue, 18 Jun 2024 11:56:51 +0000 Subject: [PATCH 016/184] Fix #2994, non-existent path causes segmentation fault when saving plot (#3038) Co-authored-by: Patrick Shriwise Co-authored-by: Paul Romano --- src/plot.cpp | 5 +++++ tests/unit_tests/test_plots.py | 25 ++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/plot.cpp b/src/plot.cpp index bf733cff48f..f29653f8033 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -315,6 +315,11 @@ void Plot::set_output_path(pugi::xml_node plot_node) } else { filename = fmt::format("plot_{}", id()); } + const std::string dir_if_present = + filename.substr(0, filename.find_last_of("/") + 1); + if (dir_if_present.size() > 0 && !dir_exists(dir_if_present)) { + fatal_error(fmt::format("Directory '{}' does not exist!", dir_if_present)); + } // add appropriate file extension to name switch (type_) { case PlotType::slice: diff --git a/tests/unit_tests/test_plots.py b/tests/unit_tests/test_plots.py index 922e4d9dc95..a1e15016a2e 100644 --- a/tests/unit_tests/test_plots.py +++ b/tests/unit_tests/test_plots.py @@ -11,7 +11,7 @@ def myplot(): plot.width = (100., 100.) plot.origin = (2., 3., -10.) plot.pixels = (500, 500) - plot.filename = 'myplot' + plot.filename = './not-a-dir/myplot' plot.type = 'slice' plot.basis = 'yz' plot.background = 'black' @@ -221,3 +221,26 @@ def test_voxel_plot_roundtrip(): assert new_plot.origin == plot.origin assert new_plot.width == plot.width assert new_plot.color_by == plot.color_by + + +def test_plot_directory(run_in_tmpdir): + pwr_pin = openmc.examples.pwr_pin_cell() + + # create a standard plot, expected to work + plot = openmc.Plot() + plot.filename = 'plot_1' + plot.type = 'slice' + plot.pixels = (10, 10) + plot.color_by = 'material' + plot.width = (100., 100.) + pwr_pin.plots = [plot] + pwr_pin.plot_geometry() + + # use current directory, also expected to work + plot.filename = './plot_1' + pwr_pin.plot_geometry() + + # use a non-existent directory, should raise an error + plot.filename = './not-a-dir/plot_1' + with pytest.raises(RuntimeError, match='does not exist'): + pwr_pin.plot_geometry() From f6d3ee7a26b1c3eeb616425890259c5645afa718 Mon Sep 17 00:00:00 2001 From: ybadr16 <104090877+ybadr16@users.noreply.github.com> Date: Wed, 19 Jun 2024 01:56:08 +0300 Subject: [PATCH 017/184] Update IsogonalOctagon to use xz basis and update tests (#3045) --- openmc/model/surface_composite.py | 46 ++++++++++++++-------- tests/unit_tests/test_surface_composite.py | 2 +- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index 1fa4234fbf7..7c134e84302 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -233,7 +233,7 @@ class IsogonalOctagon(CompositeSurface): r"""Infinite isogonal octagon composite surface An isogonal octagon is composed of eight planar surfaces. The prism is - parallel to the x, y, or z axis. The remaining two axes (y and z, z and x, + parallel to the x, y, or z axis. The remaining two axes (y and z, x and z, or x and y) serve as a basis for constructing the surfaces. Two surfaces are parallel to the first basis axis, two surfaces are parallel to the second basis axis, and the remaining four surfaces intersect both @@ -241,7 +241,7 @@ class IsogonalOctagon(CompositeSurface): This class acts as a proper surface, meaning that unary `+` and `-` operators applied to it will produce a half-space. The negative side is - defined to be the region inside of the octogonal prism. + defined to be the region inside of the octagonal prism. .. versionadded:: 0.13.1 @@ -249,7 +249,7 @@ class IsogonalOctagon(CompositeSurface): ---------- center : iterable of float Coordinate for the central axis of the octagon in the - (y, z), (z, x), or (x, y) basis. + (y, z), (x, z), or (x, y) basis depending on the axis parameter. r1 : float Half-width of octagon across its basis axis-parallel sides in units of cm. Must be less than :math:`r_2\sqrt{2}`. @@ -290,7 +290,7 @@ class IsogonalOctagon(CompositeSurface): def __init__(self, center, r1, r2, axis='z', **kwargs): c1, c2 = center - # Coords for axis-perpendicular planes + # Coordinates for axis-perpendicular planes cright = c1 + r1 cleft = c1 - r1 @@ -307,7 +307,7 @@ def __init__(self, center, r1, r2, axis='z', **kwargs): L_basis_ax = (r2 * sqrt(2) - r1) - # Coords for quadrant planes + # Coordinates for quadrant planes p1_ur = np.array([L_basis_ax, r1, 0.]) p2_ur = np.array([r1, L_basis_ax, 0.]) p3_ur = np.array([r1, L_basis_ax, 1.]) @@ -335,17 +335,18 @@ def __init__(self, center, r1, r2, axis='z', **kwargs): self.right = openmc.XPlane(cright, **kwargs) self.left = openmc.XPlane(cleft, **kwargs) elif axis == 'y': - coord_map = [1, 2, 0] - self.top = openmc.XPlane(ctop, **kwargs) - self.bottom = openmc.XPlane(cbottom, **kwargs) - self.right = openmc.ZPlane(cright, **kwargs) - self.left = openmc.ZPlane(cleft, **kwargs) + coord_map = [0, 2, 1] + self.top = openmc.ZPlane(ctop, **kwargs) + self.bottom = openmc.ZPlane(cbottom, **kwargs) + self.right = openmc.XPlane(cright, **kwargs) + self.left = openmc.XPlane(cleft, **kwargs) elif axis == 'x': coord_map = [2, 0, 1] self.top = openmc.ZPlane(ctop, **kwargs) self.bottom = openmc.ZPlane(cbottom, **kwargs) self.right = openmc.YPlane(cright, **kwargs) self.left = openmc.YPlane(cleft, **kwargs) + self.axis = axis # Put our coordinates in (x,y,z) order and add the offset for p in points: @@ -363,14 +364,27 @@ def __init__(self, center, r1, r2, axis='z', **kwargs): **kwargs) def __neg__(self): - return -self.top & +self.bottom & -self.right & +self.left & \ - +self.upper_right & +self.lower_right & -self.lower_left & \ - -self.upper_left + if self.axis == 'y': + region = -self.top & +self.bottom & -self.right & +self.left & \ + -self.upper_right & -self.lower_right & +self.lower_left & \ + +self.upper_left + else: + region = -self.top & +self.bottom & -self.right & +self.left & \ + +self.upper_right & +self.lower_right & -self.lower_left & \ + -self.upper_left + + return region def __pos__(self): - return +self.top | -self.bottom | +self.right | -self.left | \ - -self.upper_right | -self.lower_right | +self.lower_left | \ - +self.upper_left + if self.axis == 'y': + region = +self.top | -self.bottom | +self.right | -self.left | \ + +self.upper_right | +self.lower_right | -self.lower_left | \ + -self.upper_left + else: + region = +self.top | -self.bottom | +self.right | -self.left | \ + -self.upper_right | -self.lower_right | +self.lower_left | \ + +self.upper_left + return region class RightCircularCylinder(CompositeSurface): diff --git a/tests/unit_tests/test_surface_composite.py b/tests/unit_tests/test_surface_composite.py index 62ec18b3261..01c827223fd 100644 --- a/tests/unit_tests/test_surface_composite.py +++ b/tests/unit_tests/test_surface_composite.py @@ -255,7 +255,7 @@ def test_cylinder_sector_from_theta_alpha(): @pytest.mark.parametrize( "axis, plane_tb, plane_lr, axis_idx", [ ("x", "Z", "Y", 0), - ("y", "X", "Z", 1), + ("y", "Z", "X", 1), ("z", "Y", "X", 2), ] ) From 84f561c1ed561a85cbfb036bf791dafc72744a5c Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Wed, 19 Jun 2024 15:48:34 +0100 Subject: [PATCH 018/184] sets used instead of lists when membership testing (#3021) --- openmc/arithmetic.py | 4 ++-- openmc/data/photon.py | 4 ++-- openmc/mgxs_library.py | 12 ++++-------- openmc/plots.py | 2 +- openmc/plotter.py | 8 ++++---- openmc/search.py | 2 +- openmc/settings.py | 2 +- openmc/stats/univariate.py | 4 ++-- openmc/surface.py | 4 ++-- 9 files changed, 19 insertions(+), 23 deletions(-) diff --git a/openmc/arithmetic.py b/openmc/arithmetic.py index 4520553dafc..821014e36df 100644 --- a/openmc/arithmetic.py +++ b/openmc/arithmetic.py @@ -10,10 +10,10 @@ # Acceptable tally arithmetic binary operations -_TALLY_ARITHMETIC_OPS = ['+', '-', '*', '/', '^'] +_TALLY_ARITHMETIC_OPS = {'+', '-', '*', '/', '^'} # Acceptable tally aggregation operations -_TALLY_AGGREGATE_OPS = ['sum', 'avg'] +_TALLY_AGGREGATE_OPS = {'sum', 'avg'} class CrossScore: diff --git a/openmc/data/photon.py b/openmc/data/photon.py index 0d0419f0724..25ded24cbe3 100644 --- a/openmc/data/photon.py +++ b/openmc/data/photon.py @@ -28,10 +28,10 @@ R0 = CM_PER_ANGSTROM * PLANCK_C / (2.0 * pi * FINE_STRUCTURE * MASS_ELECTRON_EV) # Electron subshell labels -_SUBSHELLS = [None, 'K', 'L1', 'L2', 'L3', 'M1', 'M2', 'M3', 'M4', 'M5', +_SUBSHELLS = (None, 'K', 'L1', 'L2', 'L3', 'M1', 'M2', 'M3', 'M4', 'M5', 'N1', 'N2', 'N3', 'N4', 'N5', 'N6', 'N7', 'O1', 'O2', 'O3', 'O4', 'O5', 'O6', 'O7', 'O8', 'O9', 'P1', 'P2', 'P3', 'P4', - 'P5', 'P6', 'P7', 'P8', 'P9', 'P10', 'P11', 'Q1', 'Q2', 'Q3'] + 'P5', 'P6', 'P7', 'P8', 'P9', 'P10', 'P11', 'Q1', 'Q2', 'Q3') _REACTION_NAME = { 501: ('Total photon interaction', 'total'), diff --git a/openmc/mgxs_library.py b/openmc/mgxs_library.py index d1b96289081..3381c3abb6e 100644 --- a/openmc/mgxs_library.py +++ b/openmc/mgxs_library.py @@ -18,21 +18,17 @@ # Supported incoming particle MGXS angular treatment representations REPRESENTATION_ISOTROPIC = 'isotropic' REPRESENTATION_ANGLE = 'angle' -_REPRESENTATIONS = [ +_REPRESENTATIONS = { REPRESENTATION_ISOTROPIC, REPRESENTATION_ANGLE -] +} # Supported scattering angular distribution representations -_SCATTER_TYPES = [ +_SCATTER_TYPES = { SCATTER_TABULAR, SCATTER_LEGENDRE, SCATTER_HISTOGRAM -] - -# List of MGXS indexing schemes -_XS_SHAPES = ["[G][G'][Order]", "[G]", "[G']", "[G][G']", "[DG]", "[DG][G]", - "[DG][G']", "[DG][G][G']"] +} # Number of mu points for conversion between scattering formats _NMU = 257 diff --git a/openmc/plots.py b/openmc/plots.py index 02a44449bed..0d9dca30e79 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -14,7 +14,7 @@ from ._xml import clean_indentation, get_elem_tuple, reorder_attributes, get_text from .mixin import IDManagerMixin -_BASES = ['xy', 'xz', 'yz'] +_BASES = {'xy', 'xz', 'yz'} _SVG_COLORS = { 'aliceblue': (240, 248, 255), diff --git a/openmc/plotter.py b/openmc/plotter.py index aa3825f5e1a..8a9a331d804 100644 --- a/openmc/plotter.py +++ b/openmc/plotter.py @@ -7,15 +7,15 @@ import openmc.data # Supported keywords for continuous-energy cross section plotting -PLOT_TYPES = ['total', 'scatter', 'elastic', 'inelastic', 'fission', +PLOT_TYPES = {'total', 'scatter', 'elastic', 'inelastic', 'fission', 'absorption', 'capture', 'nu-fission', 'nu-scatter', 'unity', - 'slowing-down power', 'damage'] + 'slowing-down power', 'damage'} # Supported keywords for multi-group cross section plotting -PLOT_TYPES_MGXS = ['total', 'absorption', 'scatter', 'fission', +PLOT_TYPES_MGXS = {'total', 'absorption', 'scatter', 'fission', 'kappa-fission', 'nu-fission', 'prompt-nu-fission', 'deleyed-nu-fission', 'chi', 'chi-prompt', 'chi-delayed', - 'inverse-velocity', 'beta', 'decay-rate', 'unity'] + 'inverse-velocity', 'beta', 'decay-rate', 'unity'} # Create a dictionary which can be used to convert PLOT_TYPES_MGXS to the # openmc.XSdata attribute name needed to access the data _PLOT_MGXS_ATTR = {line: line.replace(' ', '_').replace('-', '_') diff --git a/openmc/search.py b/openmc/search.py index 7f6b3b5b9a4..70ce011b634 100644 --- a/openmc/search.py +++ b/openmc/search.py @@ -8,7 +8,7 @@ import openmc.checkvalue as cv -_SCALAR_BRACKETED_METHODS = ['brentq', 'brenth', 'ridder', 'bisect'] +_SCALAR_BRACKETED_METHODS = {'brentq', 'brenth', 'ridder', 'bisect'} def _search_keff(guess, target, model_builder, model_args, print_iterations, diff --git a/openmc/settings.py b/openmc/settings.py index 9565c2b7863..43f763a194d 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -26,7 +26,7 @@ class RunMode(Enum): PARTICLE_RESTART = 'particle restart' -_RES_SCAT_METHODS = ['dbrc', 'rvs'] +_RES_SCAT_METHODS = {'dbrc', 'rvs'} class Settings: diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index f44bce67cee..4069c5a9e11 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -16,13 +16,13 @@ from .._xml import get_text from ..mixin import EqualityMixin -_INTERPOLATION_SCHEMES = [ +_INTERPOLATION_SCHEMES = { 'histogram', 'linear-linear', 'linear-log', 'log-linear', 'log-log' -] +} class Univariate(EqualityMixin, ABC): diff --git a/openmc/surface.py b/openmc/surface.py index 806331024e2..0fa2ae43949 100644 --- a/openmc/surface.py +++ b/openmc/surface.py @@ -14,8 +14,8 @@ from .bounding_box import BoundingBox -_BOUNDARY_TYPES = ['transmission', 'vacuum', 'reflective', 'periodic', 'white'] -_ALBEDO_BOUNDARIES = ['reflective', 'periodic', 'white'] +_BOUNDARY_TYPES = {'transmission', 'vacuum', 'reflective', 'periodic', 'white'} +_ALBEDO_BOUNDARIES = {'reflective', 'periodic', 'white'} _WARNING_UPPER = """\ "{}(...) accepts an argument named '{}', not '{}'. Future versions of OpenMC \ From ddc9526966103a4dc28780f9cbc38dc942932445 Mon Sep 17 00:00:00 2001 From: Joffrey Dorville <54550047+JoffreyDorville@users.noreply.github.com> Date: Wed, 19 Jun 2024 10:11:58 -0500 Subject: [PATCH 019/184] Storing surface source points using a cell ID (#2888) Co-authored-by: Paul Romano --- docs/source/io_formats/settings.rst | 35 +- docs/source/usersguide/settings.rst | 49 +- include/openmc/particle.h | 4 +- include/openmc/particle_data.h | 8 +- include/openmc/settings.h | 12 + openmc/settings.py | 93 +- src/dagmc.cpp | 4 +- src/particle.cpp | 163 ++- src/settings.cpp | 32 +- src/surface.cpp | 3 +- .../filter_cellfrom/__init__.py | 0 .../filter_cellfrom/inputs_true.dat | 151 +++ .../filter_cellfrom/results_true.dat | 53 + .../regression_tests/filter_cellfrom/test.py | 315 +++++ tests/regression_tests/surface_source/test.py | 6 +- .../surface_source_write/__init__.py | 0 .../surface_source_write/_visualize.py | 66 + .../case-01/inputs_true.dat | 54 + .../case-01/results_true.dat | 2 + .../case-01/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-02/inputs_true.dat | 55 + .../case-02/results_true.dat | 2 + .../case-02/surface_source_true.h5 | Bin 0 -> 12752 bytes .../case-03/inputs_true.dat | 55 + .../case-03/results_true.dat | 2 + .../case-03/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-04/inputs_true.dat | 56 + .../case-04/results_true.dat | 2 + .../case-04/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-05/inputs_true.dat | 56 + .../case-05/results_true.dat | 2 + .../case-05/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-06/inputs_true.dat | 55 + .../case-06/results_true.dat | 2 + .../case-06/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-07/inputs_true.dat | 55 + .../case-07/results_true.dat | 2 + .../case-07/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-08/inputs_true.dat | 55 + .../case-08/results_true.dat | 2 + .../case-08/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-09/inputs_true.dat | 55 + .../case-09/results_true.dat | 2 + .../case-09/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-10/inputs_true.dat | 55 + .../case-10/results_true.dat | 2 + .../case-10/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-11/inputs_true.dat | 55 + .../case-11/results_true.dat | 2 + .../case-11/surface_source_true.h5 | Bin 0 -> 6408 bytes .../case-12/inputs_true.dat | 48 + .../case-12/results_true.dat | 2 + .../case-12/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-13/inputs_true.dat | 49 + .../case-13/results_true.dat | 2 + .../case-13/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-14/inputs_true.dat | 49 + .../case-14/results_true.dat | 2 + .../case-14/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-15/inputs_true.dat | 49 + .../case-15/results_true.dat | 2 + .../case-15/surface_source_true.h5 | Bin 0 -> 2144 bytes .../case-16/inputs_true.dat | 48 + .../case-16/results_true.dat | 2 + .../case-16/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-17/inputs_true.dat | 49 + .../case-17/results_true.dat | 2 + .../case-17/surface_source_true.h5 | Bin 0 -> 2144 bytes .../case-18/inputs_true.dat | 49 + .../case-18/results_true.dat | 2 + .../case-18/surface_source_true.h5 | Bin 0 -> 2144 bytes .../case-19/inputs_true.dat | 49 + .../case-19/results_true.dat | 2 + .../case-19/surface_source_true.h5 | Bin 0 -> 2144 bytes .../case-20/inputs_true.dat | 48 + .../case-20/results_true.dat | 2 + .../case-20/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-21/inputs_true.dat | 49 + .../case-21/results_true.dat | 2 + .../case-21/surface_source_true.h5 | Bin 0 -> 2144 bytes .../case-a01/inputs_true.dat | 56 + .../case-a01/results_true.dat | 2 + .../case-a01/surface_source_true.h5 | Bin 0 -> 6616 bytes .../case-d01/inputs_true.dat | 33 + .../case-d01/results_true.dat | 2 + .../case-d01/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-d02/inputs_true.dat | 34 + .../case-d02/results_true.dat | 2 + .../case-d02/surface_source_true.h5 | Bin 0 -> 31472 bytes .../case-d03/inputs_true.dat | 34 + .../case-d03/results_true.dat | 2 + .../case-d03/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-d04/inputs_true.dat | 35 + .../case-d04/results_true.dat | 2 + .../case-d04/surface_source_true.h5 | Bin 0 -> 31472 bytes .../case-d05/inputs_true.dat | 34 + .../case-d05/results_true.dat | 2 + .../case-d05/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-d06/inputs_true.dat | 34 + .../case-d06/results_true.dat | 2 + .../case-d06/surface_source_true.h5 | Bin 0 -> 29496 bytes .../case-d07/inputs_true.dat | 49 + .../case-d07/results_true.dat | 2 + .../case-d07/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-d08/inputs_true.dat | 49 + .../case-d08/results_true.dat | 2 + .../case-d08/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-e01/inputs_true.dat | 56 + .../case-e01/results_true.dat | 2 + .../case-e01/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-e02/inputs_true.dat | 55 + .../case-e02/results_true.dat | 2 + .../case-e02/surface_source_true.h5 | Bin 0 -> 33344 bytes .../case-e03/inputs_true.dat | 49 + .../case-e03/results_true.dat | 2 + .../case-e03/surface_source_true.h5 | Bin 0 -> 33344 bytes .../surface_source_write/test.py | 1126 +++++++++++++++++ tests/unit_tests/test_surface_source_write.py | 248 ++++ .../unit_tests/test_tally_multiply_density.py | 2 +- tests/unit_tests/weightwindows/test_ww_gen.py | 2 +- 120 files changed, 3963 insertions(+), 86 deletions(-) create mode 100644 tests/regression_tests/filter_cellfrom/__init__.py create mode 100644 tests/regression_tests/filter_cellfrom/inputs_true.dat create mode 100644 tests/regression_tests/filter_cellfrom/results_true.dat create mode 100644 tests/regression_tests/filter_cellfrom/test.py create mode 100644 tests/regression_tests/surface_source_write/__init__.py create mode 100644 tests/regression_tests/surface_source_write/_visualize.py create mode 100644 tests/regression_tests/surface_source_write/case-01/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-01/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-01/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-02/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-02/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-02/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-03/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-03/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-03/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-04/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-04/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-04/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-05/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-05/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-05/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-06/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-06/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-06/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-07/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-07/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-07/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-08/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-08/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-08/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-09/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-09/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-09/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-10/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-10/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-10/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-11/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-11/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-11/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-12/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-12/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-12/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-13/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-13/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-13/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-14/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-14/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-14/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-15/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-15/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-15/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-16/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-16/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-16/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-17/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-17/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-17/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-18/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-18/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-18/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-19/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-19/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-19/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-20/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-20/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-20/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-21/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-21/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-21/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-a01/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-a01/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-a01/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-d01/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-d01/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-d01/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-d02/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-d02/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-d02/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-d03/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-d03/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-d03/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-d04/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-d04/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-d04/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-d05/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-d05/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-d05/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-d06/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-d06/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-d06/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-d07/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-d07/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-d07/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-d08/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-d08/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-d08/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-e01/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-e01/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-e01/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-e02/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-e02/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-e02/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/case-e03/inputs_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-e03/results_true.dat create mode 100644 tests/regression_tests/surface_source_write/case-e03/surface_source_true.h5 create mode 100644 tests/regression_tests/surface_source_write/test.py create mode 100644 tests/unit_tests/test_surface_source_write.py diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index 6e9a7f2abc0..915f698b60f 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -902,7 +902,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``. This element has the following attributes/sub-elements: +``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 +attributes/sub-elements: :surface_ids: A list of integers separated by spaces indicating the unique IDs of surfaces @@ -928,6 +933,34 @@ certain surfaces and write out the source bank in a separate file called .. _MCPL: https://mctools.github.io/mcpl/mcpl.pdf + :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 + declared cell will be banked if they cross the identified surfaces. + + *Default*: None + + :cellfrom: + An integer representing the cell ID used to determine if particles crossing + identified surfaces are to be banked. Particles coming from this declared + cell will be banked if they cross the identified surfaces. + + *Default*: None + + :cellto: + An integer representing the cell ID used to determine if particles crossing + identified surfaces are to be banked. Particles going to this declared cell + will be banked if they cross the identified surfaces. + + *Default*: None + +.. note:: The ``cell``, ``cellfrom`` and ``cellto`` attributes cannot be + used simultaneously. + +.. 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. + ------------------------------ ```` Element ------------------------------ diff --git a/docs/source/usersguide/settings.rst b/docs/source/usersguide/settings.rst index eb02f654c30..d73d90cbb3a 100644 --- a/docs/source/usersguide/settings.rst +++ b/docs/source/usersguide/settings.rst @@ -277,6 +277,9 @@ source file can be manually generated with the :func:`openmc.write_source_file` function. This is particularly useful for coupling OpenMC with another program that generates a source to be used in OpenMC. +Surface Sources ++++++++++++++++ + A source file based on particles that cross one or more surfaces can be generated during a simulation using the :attr:`Settings.surf_source_write` attribute:: @@ -287,7 +290,51 @@ attribute:: } In this example, at most 10,000 source particles are stored when particles cross -surfaces with IDs of 1, 2, or 3. +surfaces with IDs of 1, 2, or 3. If no surface IDs are declared, particles +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:: + + 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 ``cell``, ``cellfrom`` + or ``cellto`` attributes. It is recommended to use surface IDs instead. + +Surface IDs can be used in combination with a cell ID:: + + settings.surf_source_write = { + 'cell': 1, + 'surfaces_ids': [1, 2, 3], + 'max_particles': 10000 + } + +In that case, only particles that are crossing the declared surfaces coming from +cell 1 or going to cell 1 will be banked. To account specifically for particles +leaving or entering a given cell, ``cellfrom`` and ``cellto`` are also available +to respectively account for particles coming from a cell:: + + settings.surf_source_write = { + 'cellfrom': 1, + 'max_particles': 10000 + } + +or particles going to a cell:: + + settings.surf_source_write = { + 'cellto': 1, + 'max_particles': 10000 + } + +.. note:: The ``cell``, ``cellfrom`` and ``cellto`` attributes cannot be + used simultaneously. .. _compiled_source: diff --git a/include/openmc/particle.h b/include/openmc/particle.h index e423880f87c..6a2e67049fd 100644 --- a/include/openmc/particle.h +++ b/include/openmc/particle.h @@ -74,7 +74,7 @@ class Particle : public ParticleData { void pht_secondary_particles(); //! Cross a surface and handle boundary conditions - void cross_surface(); + void cross_surface(const Surface& surf); //! Cross a vacuum boundary condition. // @@ -127,6 +127,8 @@ std::string particle_type_to_str(ParticleType type); ParticleType str_to_particle_type(std::string str); +void add_surf_source_to_bank(Particle& p, const Surface& surf); + } // namespace openmc #endif // OPENMC_PARTICLE_H diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index 5c765e2e605..164148cce10 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -211,9 +211,15 @@ class GeometryState { // resets all coordinate levels for the particle void clear() { - for (auto& level : coord_) + for (auto& level : coord_) { level.reset(); + } n_coord_ = 1; + + for (auto& cell : cell_last_) { + cell = C_NONE; + } + n_coord_last_ = 1; } // Initialize all internal state from position and direction diff --git a/include/openmc/settings.h b/include/openmc/settings.h index df4a54a6f6d..a95f1ced9f1 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -16,6 +16,14 @@ namespace openmc { +// Type of surface source write +enum class SSWCellType { + None, + Both, + From, + To, +}; + //============================================================================== // Global variable declarations //============================================================================== @@ -125,6 +133,10 @@ extern int max_history_splits; //!< maximum number of particle splits for weight windows extern int64_t max_surface_particles; //!< maximum number of particles to be //!< banked on surfaces per process +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 TemperatureMethod temperature_method; //!< method for choosing temperatures extern double diff --git a/openmc/settings.py b/openmc/settings.py index 43f763a194d..80d766ab149 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -201,6 +201,15 @@ class Settings: :max_particles: Maximum number of particles to be banked on surfaces per process (int) :mcpl: Output in the form of an MCPL-file (bool) + :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) survival_biasing : bool Indicate whether survival biasing is to be used tabular_legendre : dict @@ -693,23 +702,30 @@ def surf_source_write(self) -> dict: @surf_source_write.setter def surf_source_write(self, surf_source_write: dict): - cv.check_type('surface source writing options', surf_source_write, Mapping) + cv.check_type("surface source writing options", surf_source_write, Mapping) for key, value in surf_source_write.items(): - cv.check_value('surface source writing key', key, - ('surface_ids', 'max_particles', 'mcpl')) - if key == 'surface_ids': - cv.check_type('surface ids for source banking', value, - Iterable, Integral) + cv.check_value( + "surface source writing key", + key, + ("surface_ids", "max_particles", "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) - elif key == 'max_particles': - cv.check_type('maximum particle banks on surfaces per process', - value, Integral) - cv.check_greater_than('maximum particle banks on surfaces per process', - value, 0) - elif key == 'mcpl': - cv.check_type('write to an MCPL-format file', value, bool) + cv.check_greater_than("surface id for source banking", surf_id, 0) + elif key == "mcpl": + cv.check_type("write to an MCPL-format file", value, bool) + elif key in ("max_particles", "cell", "cellfrom", "cellto"): + name = { + "max_particles": "maximum particle banks on surfaces per process", + "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) self._surf_source_write = surf_source_write @@ -1207,16 +1223,18 @@ 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: + 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 'max_particles' in self._surf_source_write: - subelement = ET.SubElement(element, "max_particles") - subelement.text = str(self._surf_source_write['max_particles']) - if 'mcpl' in self._surf_source_write: + 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() + subelement.text = str(self._surf_source_write["mcpl"]).lower() + for key in ("max_particles", "cell", "cellfrom", "cellto"): + if key in self._surf_source_write: + subelement = ET.SubElement(element, key) + subelement.text = str(self._surf_source_write[key]) def _create_confidence_intervals(self, root): if self._confidence_intervals is not None: @@ -1381,9 +1399,9 @@ def _create_create_fission_neutrons_subelement(self, root): elem.text = str(self._create_fission_neutrons).lower() def _create_create_delayed_neutrons_subelement(self, root): - if self._create_delayed_neutrons is not None: - elem = ET.SubElement(root, "create_delayed_neutrons") - elem.text = str(self._create_delayed_neutrons).lower() + if self._create_delayed_neutrons is not None: + elem = ET.SubElement(root, "create_delayed_neutrons") + elem.text = str(self._create_delayed_neutrons).lower() def _create_delayed_photon_scaling_subelement(self, root): if self._delayed_photon_scaling is not None: @@ -1610,17 +1628,18 @@ def _surf_source_read_from_xml_element(self, root): def _surf_source_write_from_xml_element(self, root): elem = root.find('surf_source_write') - if elem is not None: - for key in ('surface_ids', 'max_particles','mcpl'): - value = get_text(elem, key) - if value is not None: - if key == 'surface_ids': - value = [int(x) for x in value.split()] - elif key in ('max_particles'): - value = int(value) - elif key == 'mcpl': - value = value in ('true', '1') - self.surf_source_write[key] = value + if elem is None: + return + for key in ('surface_ids', 'max_particles', 'mcpl', 'cell', 'cellto', 'cellfrom'): + value = get_text(elem, key) + if value is not None: + if key == 'surface_ids': + value = [int(x) for x in value.split()] + elif key == 'mcpl': + value = value in ('true', '1') + elif key in ('max_particles', 'cell', 'cellfrom', 'cellto'): + value = int(value) + self.surf_source_write[key] = value def _confidence_intervals_from_xml_element(self, root): text = get_text(root, 'confidence_intervals') diff --git a/src/dagmc.cpp b/src/dagmc.cpp index 2f2502f6ea1..a29a2589f0b 100644 --- a/src/dagmc.cpp +++ b/src/dagmc.cpp @@ -271,8 +271,10 @@ void DAGUniverse::init_geometry() : dagmc_instance_->id_by_index(2, i + 1); // set surface source attribute if needed - if (contains(settings::source_write_surf_id, s->id_)) + if (contains(settings::source_write_surf_id, s->id_) || + settings::source_write_surf_id.empty()) { s->surf_source_ = true; + } // set BCs std::string bc_value = diff --git a/src/particle.cpp b/src/particle.cpp index a91113c61ad..0c6a33b78bc 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -168,6 +168,12 @@ void Particle::event_calculate_xs() // Set birth cell attribute if (cell_born() == C_NONE) cell_born() = lowest_coord().cell; + + // Initialize last cells from current cell + for (int j = 0; j < n_coord(); ++j) { + cell_last(j) = coord(j).cell; + } + n_coord_last() = n_coord(); } // Write particle track. @@ -264,16 +270,16 @@ void Particle::event_advance() void Particle::event_cross_surface() { - // Set surface that particle is on and adjust coordinate levels - surface() = boundary().surface_index; - n_coord() = boundary().coord_level; - // Saving previous cell data for (int j = 0; j < n_coord(); ++j) { cell_last(j) = coord(j).cell; } n_coord_last() = n_coord(); + // Set surface that particle is on and adjust coordinate levels + surface() = boundary().surface_index; + n_coord() = boundary().coord_level; + if (boundary().lattice_translation[0] != 0 || boundary().lattice_translation[1] != 0 || boundary().lattice_translation[2] != 0) { @@ -284,7 +290,17 @@ void Particle::event_cross_surface() event() = TallyEvent::LATTICE; } else { // Particle crosses surface - cross_surface(); + // TODO: off-by-one + const auto& surf {model::surfaces[std::abs(surface()) - 1].get()}; + // If BC, add particle to surface source before crossing surface + if (surf->surf_source_ && surf->bc_) { + add_surf_source_to_bank(*this, *surf); + } + cross_surface(*surf); + // If no BC, add particle to surface source after crossing surface + if (surf->surf_source_ && !surf->bc_) { + add_surf_source_to_bank(*this, *surf); + } if (settings::weight_window_checkpoint_surface) { apply_weight_windows(*this); } @@ -418,6 +434,12 @@ void Particle::event_revive_from_secondary() // Set birth cell attribute if (cell_born() == C_NONE) cell_born() = lowest_coord().cell; + + // Initialize last cells from current cell + for (int j = 0; j < n_coord(); ++j) { + cell_last(j) = coord(j).cell; + } + n_coord_last() = n_coord(); } pht_secondary_particles(); } @@ -502,40 +524,22 @@ void Particle::pht_secondary_particles() } } -void Particle::cross_surface() +void Particle::cross_surface(const Surface& surf) { - int i_surface = std::abs(surface()); - // TODO: off-by-one - const auto& surf {model::surfaces[i_surface - 1].get()}; - if (settings::verbosity >= 10 || trace()) { - write_message(1, " Crossing surface {}", surf->id_); - } - if (surf->surf_source_ && simulation::current_batch > settings::n_inactive && - !simulation::surf_source_bank.full()) { - SourceSite site; - site.r = r(); - site.u = u(); - site.E = E(); - site.time = time(); - site.wgt = wgt(); - site.delayed_group = delayed_group(); - site.surf_id = surf->id_; - site.particle = type(); - site.parent_id = id(); - site.progeny_id = n_progeny(); - int64_t idx = simulation::surf_source_bank.thread_safe_append(site); + if (settings::verbosity >= 10 || trace()) { + write_message(1, " Crossing surface {}", surf.id_); } // if we're crossing a CSG surface, make sure the DAG history is reset #ifdef DAGMC - if (surf->geom_type_ == GeometryType::CSG) + if (surf.geom_type_ == GeometryType::CSG) history().reset(); #endif // Handle any applicable boundary conditions. - if (surf->bc_ && settings::run_mode != RunMode::PLOTTING) { - surf->bc_->handle_particle(*this, *surf); + if (surf.bc_ && settings::run_mode != RunMode::PLOTTING) { + surf.bc_->handle_particle(*this, surf); return; } @@ -544,10 +548,10 @@ void Particle::cross_surface() #ifdef DAGMC // in DAGMC, we know what the next cell should be - if (surf->geom_type_ == GeometryType::DAG) { - int32_t i_cell = - next_cell(i_surface, cell_last(n_coord() - 1), lowest_coord().universe) - - 1; + if (surf.geom_type_ == GeometryType::DAG) { + int32_t i_cell = next_cell(std::abs(surface()), cell_last(n_coord() - 1), + lowest_coord().universe) - + 1; // save material and temp material_last() = material(); sqrtkT_last() = sqrtkT(); @@ -561,8 +565,9 @@ void Particle::cross_surface() #endif bool verbose = settings::verbosity >= 10 || trace(); - if (neighbor_list_find_cell(*this, verbose)) + if (neighbor_list_find_cell(*this, verbose)) { return; + } // ========================================================================== // COULDN'T FIND PARTICLE IN NEIGHBORING CELLS, SEARCH ALL CELLS @@ -586,7 +591,7 @@ void Particle::cross_surface() if (!exhaustive_find_cell(*this, verbose)) { mark_as_lost("After particle " + std::to_string(id()) + - " crossed surface " + std::to_string(surf->id_) + + " crossed surface " + std::to_string(surf.id_) + " it could not be located in any cell and it did not leak."); return; } @@ -650,7 +655,7 @@ void Particle::cross_reflective_bc(const Surface& surf, Direction new_u) u() = new_u; // Reassign particle's cell and surface - coord(0).cell = cell_last(n_coord_last() - 1); + coord(0).cell = cell_last(0); surface() = -surface(); // If a reflective surface is coincident with a lattice or universe @@ -873,4 +878,90 @@ ParticleType str_to_particle_type(std::string str) } } +void add_surf_source_to_bank(Particle& p, const Surface& surf) +{ + if (simulation::current_batch <= settings::n_inactive || + simulation::surf_source_bank.full()) { + return; + } + + // 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 (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") { + 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 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) { + return; + } + } else { + + // 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) { + return; + } + + // 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) { + return; + } + } + } + + 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 f5cd1b7e917..6c3226b04a6 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -119,6 +119,8 @@ std::unordered_set sourcepoint_batch; std::unordered_set statepoint_batch; std::unordered_set source_write_surf_id; int64_t max_surface_particles; +int64_t ssw_cell_id {C_NONE}; +SSWCellType ssw_cell_type {SSWCellType::None}; TemperatureMethod temperature_method {TemperatureMethod::NEAREST}; double temperature_tolerance {10.0}; double temperature_default {293.6}; @@ -747,7 +749,9 @@ void read_settings_xml(pugi::xml_node root) // Get surface source write node xml_node node_ssw = root.child("surf_source_write"); - // Determine surface ids at which crossing particles are to be banked + // Determine surface ids at which crossing particles are to be banked. + // If no surfaces are specified, all surfaces in the model will be used + // to bank source points. if (check_for_node(node_ssw, "surface_ids")) { auto temp = get_node_array(node_ssw, "surface_ids"); for (const auto& b : temp) { @@ -759,7 +763,12 @@ void read_settings_xml(pugi::xml_node root) if (check_for_node(node_ssw, "max_particles")) { max_surface_particles = std::stoll(get_node_value(node_ssw, "max_particles")); + } else { + fatal_error("A maximum number of particles needs to be specified " + "using the 'max_particles' parameter to store surface " + "source points."); } + if (check_for_node(node_ssw, "mcpl")) { surf_mcpl_write = get_node_value_bool(node_ssw, "mcpl"); @@ -769,6 +778,27 @@ void read_settings_xml(pugi::xml_node root) "surface source files."); } } + // Get cell information + if (check_for_node(node_ssw, "cell")) { + ssw_cell_id = std::stoll(get_node_value(node_ssw, "cell")); + ssw_cell_type = SSWCellType::Both; + } + if (check_for_node(node_ssw, "cellfrom")) { + 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_cell_type = SSWCellType::From; + } + if (check_for_node(node_ssw, "cellto")) { + 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_cell_type = SSWCellType::To; + } } // If source is not separate and is to be written out in the statepoint file, diff --git a/src/surface.cpp b/src/surface.cpp index 12fef070e18..50ef2a12830 100644 --- a/src/surface.cpp +++ b/src/surface.cpp @@ -63,7 +63,8 @@ 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_)) { + if (contains(settings::source_write_surf_id, id_) || + settings::source_write_surf_id.empty()) { surf_source_ = true; } } else { diff --git a/tests/regression_tests/filter_cellfrom/__init__.py b/tests/regression_tests/filter_cellfrom/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regression_tests/filter_cellfrom/inputs_true.dat b/tests/regression_tests/filter_cellfrom/inputs_true.dat new file mode 100644 index 00000000000..8a37e8286f0 --- /dev/null +++ b/tests/regression_tests/filter_cellfrom/inputs_true.dat @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 2000 + 15 + 5 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + 1 + + + + 1 + + + 1 + + + 2 + + + 3 + + + 4 + + + 2 + + + 3 + + + 4 + + + 5 1 + total + + + 5 2 + total + + + 5 3 + total + + + 5 4 + total + + + 6 1 + total + + + 6 2 + total + + + 6 3 + total + + + 6 4 + total + + + 7 1 + total + + + 7 2 + total + + + 7 3 + total + + + 7 4 + total + + + 8 1 + total + + + 8 2 + total + + + 8 3 + total + + + 8 4 + total + + + total + + + diff --git a/tests/regression_tests/filter_cellfrom/results_true.dat b/tests/regression_tests/filter_cellfrom/results_true.dat new file mode 100644 index 00000000000..5dd43e1f33b --- /dev/null +++ b/tests/regression_tests/filter_cellfrom/results_true.dat @@ -0,0 +1,53 @@ +k-combined: +9.640806E-02 2.655206E-03 +tally 1: +6.025999E+00 +3.635317E+00 +tally 2: +4.523606E-04 +2.051522E-08 +tally 3: +6.026452E+00 +3.635862E+00 +tally 4: +0.000000E+00 +0.000000E+00 +tally 5: +1.530903E+00 +2.357048E-01 +tally 6: +4.992646E-05 +2.600391E-10 +tally 7: +1.530953E+00 +2.357201E-01 +tally 8: +1.889115E+01 +3.574727E+01 +tally 9: +7.556902E+00 +5.717680E+00 +tally 10: +5.022871E-04 +2.531352E-08 +tally 11: +7.557405E+00 +5.718440E+00 +tally 12: +1.889115E+01 +3.574727E+01 +tally 13: +0.000000E+00 +0.000000E+00 +tally 14: +2.663407E-04 +7.161725E-09 +tally 15: +2.663407E-04 +7.161725E-09 +tally 16: +8.025710E+01 +6.459305E+02 +tally 17: +1.067059E+02 +1.140939E+03 diff --git a/tests/regression_tests/filter_cellfrom/test.py b/tests/regression_tests/filter_cellfrom/test.py new file mode 100644 index 00000000000..8fb7509cbe8 --- /dev/null +++ b/tests/regression_tests/filter_cellfrom/test.py @@ -0,0 +1,315 @@ +"""This test ensures that the CellFromFilter works correctly even if the level of +coordinates (number of encapsulated universes) is different in the cell from +where the particle originates compared to the cell where the particle is going. + +A matrix of reaction rates based on where the particle is coming from and +where it goes to is calculated and compared to the total reaction rate of the problem. +The components of this matrix are also compared to other components using symmetric +properties. + +TODO: + +- Test with a lattice, +- Test with mesh, +- Test with reflective boundary conditions, +- Test with periodic boundary conditions. + +""" + +from numpy.testing import assert_allclose, assert_equal +import numpy as np +import openmc +import pytest + +from tests.testing_harness import PyAPITestHarness + + +RTOL = 1.0e-7 +ATOL = 0.0 + + +@pytest.fixture +def model(): + """Cylindrical core contained in a first box which is contained in a larger box. + A lower universe is used to describe the interior of the first box which + contains the core and its surrounding space.""" + openmc.reset_auto_ids() + model = openmc.Model() + + # ============================================================================= + # Materials + # ============================================================================= + + fuel = openmc.Material() + fuel.add_nuclide("U234", 0.0004524) + fuel.add_nuclide("U235", 0.0506068) + fuel.add_nuclide("U238", 0.9487090) + fuel.add_nuclide("U236", 0.0002318) + fuel.add_nuclide("O16", 2.0) + fuel.set_density("g/cm3", 10.97) + + water = openmc.Material() + water.add_nuclide("H1", 2.0) + water.add_nuclide("O16", 1.0) + water.set_density("g/cm3", 1.0) + + air = openmc.Material() + air.add_element("O", 0.2) + air.add_element("N", 0.8) + air.set_density("g/cm3", 0.001225) + + # ============================================================================= + # Geometry + # ============================================================================= + + # ----------------------------------------------------------------------------- + # Cylindrical core + # ----------------------------------------------------------------------------- + + # Parameters + core_radius = 2.0 + core_height = 4.0 + + # Surfaces + core_cylinder = openmc.ZCylinder(r=core_radius) + core_lower_plane = openmc.ZPlane(z0=-core_height / 2.0) + core_upper_plane = openmc.ZPlane(z0=core_height / 2.0) + + # Region + core_region = -core_cylinder & +core_lower_plane & -core_upper_plane + + # Cells + core = openmc.Cell(fill=fuel, region=core_region) + outside_core_region = +core_cylinder | -core_lower_plane | +core_upper_plane + outside_core = openmc.Cell(fill=air, region=outside_core_region) + + # Universe + inside_box1_universe = openmc.Universe(cells=[core, outside_core]) + + # ----------------------------------------------------------------------------- + # Box 1 + # ----------------------------------------------------------------------------- + + # Parameters + box1_size = 4.1 + + # Surfaces + box1_rpp = openmc.model.RectangularParallelepiped( + -box1_size / 2.0, box1_size / 2.0, + -box1_size / 2.0, box1_size / 2.0, + -box1_size / 2.0, box1_size / 2.0, + ) + + # Cell + box1 = openmc.Cell(fill=inside_box1_universe, region=-box1_rpp) + + # ----------------------------------------------------------------------------- + # Box 2 + # ----------------------------------------------------------------------------- + + # Parameters + box2_size = 12 + + # Surfaces + box2_rpp = openmc.model.RectangularParallelepiped( + -box2_size / 2.0, box2_size / 2.0, + -box2_size / 2.0, box2_size / 2.0, + -box2_size / 2.0, box2_size / 2.0, + boundary_type="vacuum" + ) + + # Cell + box2 = openmc.Cell(fill=water, region=-box2_rpp & +box1_rpp) + + # Register geometry + model.geometry = openmc.Geometry([box1, box2]) + + # ============================================================================= + # Settings + # ============================================================================= + + model.settings = openmc.Settings() + model.settings.particles = 2000 + model.settings.batches = 15 + model.settings.inactive = 5 + model.settings.seed = 1 + + bounds = [ + -core_radius, + -core_radius, + -core_height / 2.0, + core_radius, + core_radius, + core_height / 2.0, + ] + distribution = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True) + model.settings.source = openmc.IndependentSource(space=distribution) + + # ============================================================================= + # Tallies + # ============================================================================= + + in_core_filter = openmc.CellFilter([core]) + in_outside_core_filter = openmc.CellFilter([outside_core]) + in_box1_filter = openmc.CellFilter([box1]) + in_box2_filter = openmc.CellFilter([box2]) + + from_core_filter = openmc.CellFromFilter([core]) + from_outside_core_filter = openmc.CellFromFilter([outside_core]) + from_box1_filter = openmc.CellFromFilter([box1]) + from_box2_filter = openmc.CellFromFilter([box2]) + + t1_1 = openmc.Tally(name="total from 1 in 1") + t1_1.filters = [from_core_filter, in_core_filter] + t1_1.scores = ["total"] + + t1_2 = openmc.Tally(name="total from 1 in 2") + t1_2.filters = [from_core_filter, in_outside_core_filter] + t1_2.scores = ["total"] + + t1_3 = openmc.Tally(name="total from 1 in 3") + t1_3.filters = [from_core_filter, in_box1_filter] + t1_3.scores = ["total"] + + t1_4 = openmc.Tally(name="total from 1 in 4") + t1_4.filters = [from_core_filter, in_box2_filter] + t1_4.scores = ["total"] + + t2_1 = openmc.Tally(name="total from 2 in 1") + t2_1.filters = [from_outside_core_filter, in_core_filter] + t2_1.scores = ["total"] + + t2_2 = openmc.Tally(name="total from 2 in 2") + t2_2.filters = [from_outside_core_filter, in_outside_core_filter] + t2_2.scores = ["total"] + + t2_3 = openmc.Tally(name="total from 2 in 3") + t2_3.filters = [from_outside_core_filter, in_box1_filter] + t2_3.scores = ["total"] + + t2_4 = openmc.Tally(name="total from 2 in 4") + t2_4.filters = [from_outside_core_filter, in_box2_filter] + t2_4.scores = ["total"] + + t3_1 = openmc.Tally(name="total from 3 in 1") + t3_1.filters = [from_box1_filter, in_core_filter] + t3_1.scores = ["total"] + + t3_2 = openmc.Tally(name="total from 3 in 2") + t3_2.filters = [from_box1_filter, in_outside_core_filter] + t3_2.scores = ["total"] + + t3_3 = openmc.Tally(name="total from 3 in 3") + t3_3.filters = [from_box1_filter, in_box1_filter] + t3_3.scores = ["total"] + + t3_4 = openmc.Tally(name="total from 3 in 4") + t3_4.filters = [from_box1_filter, in_box2_filter] + t3_4.scores = ["total"] + + t4_1 = openmc.Tally(name="total from 4 in 1") + t4_1.filters = [from_box2_filter, in_core_filter] + t4_1.scores = ["total"] + + t4_2 = openmc.Tally(name="total from 4 in 2") + t4_2.filters = [from_box2_filter, in_outside_core_filter] + t4_2.scores = ["total"] + + t4_3 = openmc.Tally(name="total from 4 in 3") + t4_3.filters = [from_box2_filter, in_box1_filter] + t4_3.scores = ["total"] + + t4_4 = openmc.Tally(name="total from 4 in 4") + t4_4.filters = [from_box2_filter, in_box2_filter] + t4_4.scores = ["total"] + + tglobal = openmc.Tally(name="total") + tglobal.scores = ["total"] + + model.tallies += [ + t1_1, + t1_2, + t1_3, + t1_4, + t2_1, + t2_2, + t2_3, + t2_4, + t3_1, + t3_2, + t3_3, + t3_4, + t4_1, + t4_2, + t4_3, + t4_4, + tglobal, + ] + return model + + +class CellFromFilterTest(PyAPITestHarness): + + def _compare_results(self): + """Additional unit tests on the tally results to check + consistency of CellFromFilter.""" + with openmc.StatePoint(self.statepoint_name) as sp: + + t1_1 = sp.get_tally(name="total from 1 in 1").mean + t1_2 = sp.get_tally(name="total from 1 in 2").mean + t1_3 = sp.get_tally(name="total from 1 in 3").mean + t1_4 = sp.get_tally(name="total from 1 in 4").mean + + t2_1 = sp.get_tally(name="total from 2 in 1").mean + t2_2 = sp.get_tally(name="total from 2 in 2").mean + t2_3 = sp.get_tally(name="total from 2 in 3").mean + t2_4 = sp.get_tally(name="total from 2 in 4").mean + + t3_1 = sp.get_tally(name="total from 3 in 1").mean + t3_2 = sp.get_tally(name="total from 3 in 2").mean + t3_3 = sp.get_tally(name="total from 3 in 3").mean + t3_4 = sp.get_tally(name="total from 3 in 4").mean + + t4_1 = sp.get_tally(name="total from 4 in 1").mean + t4_2 = sp.get_tally(name="total from 4 in 2").mean + t4_3 = sp.get_tally(name="total from 4 in 3").mean + t4_4 = sp.get_tally(name="total from 4 in 4").mean + + tglobal = sp.get_tally(name="total").mean + + # From 1 and 2 is equivalent to from 3 + assert_allclose(t1_1 + t2_1, t3_1, rtol=RTOL, atol=ATOL) + assert_allclose(t1_2 + t2_2, t3_2, rtol=RTOL, atol=ATOL) + assert_allclose(t1_3 + t2_3, t3_3, rtol=RTOL, atol=ATOL) + assert_allclose(t1_4 + t2_4, t3_4, rtol=RTOL, atol=ATOL) + + # In 1 and 2 equivalent to in 3 + assert_allclose(t1_1 + t1_2, t1_3, rtol=RTOL, atol=ATOL) + assert_allclose(t2_1 + t2_2, t2_3, rtol=RTOL, atol=ATOL) + assert_allclose(t3_1 + t3_2, t3_3, rtol=RTOL, atol=ATOL) + assert_allclose(t4_1 + t4_2, t4_3, rtol=RTOL, atol=ATOL) + + # Comparison to global from 3 + assert_allclose(t3_3 + t3_4 + t4_3 + t4_4, tglobal, rtol=RTOL, atol=ATOL) + + # Comparison to global from 1 and 2 + t_from_1_wo_3 = t1_1 + t1_2 + t1_4 + t_from_2_wo_3 = t2_1 + t2_2 + t2_4 + t_from_4_wo_3 = t4_1 + t4_2 + t4_4 + assert_allclose( + t_from_1_wo_3 + t_from_2_wo_3 + t_from_4_wo_3, + tglobal, + rtol=RTOL, + atol=ATOL, + ) + + # 1 cannot contribute to 4 and 4 cannot contribute to 1 by symmetry + assert_equal(t1_4, np.zeros_like(t1_4)) + assert_equal(t4_1, np.zeros_like(t4_1)) + + return super()._compare_results() + + +def test_filter_cellfrom(model): + harness = CellFromFilterTest("statepoint.15.h5", model) + harness.main() diff --git a/tests/regression_tests/surface_source/test.py b/tests/regression_tests/surface_source/test.py index a25b40064d8..81bba0c9463 100644 --- a/tests/regression_tests/surface_source/test.py +++ b/tests/regression_tests/surface_source/test.py @@ -118,15 +118,13 @@ def _cleanup(self): @pytest.mark.surf_source_op('write') -def test_surface_source_write(model): +def test_surface_source_write(model, monkeypatch): # Test result is based on 1 MPI process - np = config['mpi_np'] - config['mpi_np'] = '1' + monkeypatch.setitem(config, "mpi_np", "1") harness = SurfaceSourceTestHarness('statepoint.10.h5', model, 'inputs_true_write.dat') harness.main() - config['mpi_np'] = np @pytest.mark.surf_source_op('read') diff --git a/tests/regression_tests/surface_source_write/__init__.py b/tests/regression_tests/surface_source_write/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regression_tests/surface_source_write/_visualize.py b/tests/regression_tests/surface_source_write/_visualize.py new file mode 100644 index 00000000000..73340cae06f --- /dev/null +++ b/tests/regression_tests/surface_source_write/_visualize.py @@ -0,0 +1,66 @@ +"""Helper script to visualize the surface_source.h5 files created with this test. +""" + +import h5py +import matplotlib.pyplot as plt + + +if __name__ == "__main__": + + # 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" + + # Select the case from its folder name + folder = "case-20" + + # Reading the surface source file + with h5py.File(f"{folder}/surface_source_true.h5", "r") as fp: + source_bank = fp["source_bank"][()] + r_xs = source_bank['r']['x'] + r_ys = source_bank['r']['y'] + r_zs = source_bank['r']['z'] + + print("Size of the source bank: ", len(source_bank)) + + # Select data range to visualize + idx_1 = 0 + idx_2 = -1 + + # Show 3D representation + if option == "show": + + fig = plt.figure(figsize=(10, 10)) + ax1 = fig.add_subplot(projection="3d", proj_type="ortho") + ax1.scatter(r_xs[idx_1:idx_2], r_ys[idx_1:idx_2], r_zs[idx_1:idx_2], marker=".") + ax1.view_init(0, 0) + ax1.xaxis.set_ticklabels([]) + ax1.set_ylabel("y-axis [cm]") + ax1.set_zlabel("z-axis [cm]") + ax1.set_aspect("equal", "box") + + plt.show() + + # Save 2D representations + elif option == "savefig": + + fig = plt.figure(figsize=(14, 5)) + ax1 = fig.add_subplot(121, projection="3d", proj_type="ortho") + ax1.scatter(r_xs[idx_1:idx_2], r_ys[idx_1:idx_2], r_zs[idx_1:idx_2], marker=".") + ax1.view_init(0, 0) + ax1.xaxis.set_ticklabels([]) + ax1.set_ylabel("y-axis [cm]") + ax1.set_zlabel("z-axis [cm]") + ax1.set_aspect("equal", "box") + + ax2 = fig.add_subplot(122, projection="3d", proj_type="ortho") + ax2.scatter(r_xs[idx_1:idx_2], r_ys[idx_1:idx_2], r_zs[idx_1:idx_2], marker=".") + ax2.view_init(90, -90) + ax2.zaxis.set_ticklabels([]) + ax2.set_xlabel("x-axis [cm]") + ax2.set_ylabel("y-axis [cm]") + ax2.set_aspect("equal", "box") + + plt.savefig("plot_2d.png") diff --git a/tests/regression_tests/surface_source_write/case-01/inputs_true.dat b/tests/regression_tests/surface_source_write/case-01/inputs_true.dat new file mode 100644 index 00000000000..ecca3a9f86c --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-01/inputs_true.dat @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 300 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-01/results_true.dat b/tests/regression_tests/surface_source_write/case-01/results_true.dat new file mode 100644 index 00000000000..8979eb55477 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-01/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.169123E-02 9.144814E-03 diff --git a/tests/regression_tests/surface_source_write/case-01/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-01/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..82dca4d032eec7395fe8697247c7dc4fb01f51e1 GIT binary patch literal 33344 zcmeI52{=@5`1i-Y#?((GQPxnlWC`({kzHw}MT-cLN?Np`q>>g1ZAvAD7L`N`J);yQ zvZO58_kGL0y)-j^XP&>z@4c?3>w2&EKV8i^bDVQNbKl?Z^E~%CXXeal69c13ydu1W z(FZp-ft^77HA{OMramxl$`aaZ+TU%cGc44pFLfHpN||9Hun|W8CQMyUr0!pwjj~`l zb@}R*hK7WZHEH{&O{sq-2y;>9xM>Uicl!TT222b|zpS#B8a2&NsZ^iQlCfvkJ~uo2 z&9+;1ZvVB2=1YvW!vE!TGS#oBulWfo)cx}ixJUjuIJwyGJ?Lsr+xOlgN22%rUG>X+LVo;!Zx~S+KBPDjZED}{{NT9w2}Rf=IFqGS$Slg z$>LxniqQu~P~#)04UcSp@Ym~+Zh<=e-DJFN4^da9?@j*a9>&{tA9XdF#{Y9G%98(= zV;t%1emy8IqP`esGG2c6I_>!7vxVcVk6$$2w(gF5f31SyR~%=4yluDIyKFgVzjd>t z+pc}CBiopavmJgBal|<6zZQrS_Uv^zT(`LGb+U6AYg5a|Ss%X$ zzb(gzYe(A+D=X`-7R60%XKXAsR6Cc_?EPyOQ>ZQ-Mw=^%^yx&f`vpHfJqn=}j81$C3I9JA|*_Kxe`#4hge>~MB@hj%coU&Dnjzb$##^W?|?VA1i zd^ZsH-&5r9sUK1}+~l|<%hQItazNE(spe_CRe)pW{a0#m(y3SOJ~yW0P!1->=P;{3pm~DY?g^LbTLTUpYXWZx(UMUG9AxTpGNXRyr{evN z%W0x?@zC}?+D>WnG>&DZ;Zjv%2cW#>CPx|`sC#Y6&XDsq;cLRw*%EY|(Ry+8F&;;= zL+_T{^Dgj1dET{snLOlQD-yUFa#9~pnHuIIMaRMSJ7_$TyH9inMGb+jO_7uJKTM)> zhzvQwCd*Y0?2)762vFYSxbZN{2k0=(EFYj8X88c+Fv|xhhgm*AIn43_t*@h6kWjd~ zOu%Me74Ufy^dM;gC)L*k@^Ge1+8T`lxN2XqZ|AB~IQ$sQl~_;<*PWNnxnZP4QaPe& zwA6YQov&$aX0)B69M-~v4_0n28S4-Biz;V{-)CjW2^T&orrGrS{s84@FWtx4>P-gT z1feH^SNW*%a5LmwPkncTb?5Kv5R~(2i-KI*a1Ny8kw_*kj0iq>LVzLX^Qvck?XpUA zdE=+-^C?#zQZ$+PL=!TSK^*es8`A@;6k%m?p%SW?1_Sr~HIciGx&SMf5!zAt?l`JsZlWC{ffJD+uwmZoZ{yk16p7io9H1Z z!bTcMk2w5PgkB%~xPWpVTbTXiyf8Et&o)e;U;cmqnGq+-|8xE8BkFW{!;f<)$8lke zJjb>nkbIkENaQUSRUaZlj^f8IYfo1CdK5qYpd4oTjTRf#S4^uzP!6-kAC$wa@dxEF zYy3eu%<=*CbT!Qegel75We>h~jkTW>2YV_9-gm=*m`t^Y{Z(M5^dw^T{xbN|qxz8X z^I}->NuO=`cO6omZ`4)+B7MI@i(z#9K{-5e)nt2BPK*L3k`)Ti=BX1o$TK`sq}S9H z0bm}!@M%>FET}QasJqtyZ*f+Md7fNAn!L9ruEkxRuASohYm~#RywUm~TH=^hzo8sv zwI7tjtoDO)nALtz4zql~Lo)z%$}AtC9A^0dJLy3vwVPZ znB@bM!z>@59A^1o63qbA={&^?$M^F$f&eLV=ZeJ~Wa?tFduo{YY42dL@7&jC<>i^6 zw&;}nx58$a@a2A&=^Yi4nBu#jG`8Pg|3NwXQcX^YSyzLOk{_2k`v;*NQRUUuV_am@ z^LN}<^khNyCG4h4`5M9eL3OUVeED$cvYNvuC51^mnxXkV&h+&){=7fRsqcR{1ou>e zuO6l!8^88JN<7?TksWq5nwFW+qNek#ZGAKFo9VcT^V=O*JUdhsHhR2(a+21#2XU?Z2&7l7j*70W9y>mH@o?{KC{qMd&wO=meNztx?BWl2-pm2d z@{E<_9N0(+D+PqHY@PqiIsD>stfWykklpRsG4qdER5eV> zvC6ImFD_)6O`TNSq6$gROr%OW(_xrVDna$xe6o2LgGzYZZrfPboAyZkzQ; zrWSbKWRK~1od;jm%@{uRwG1mxxwlSOXDJJDpIrE1W&AE@_XKRa_?nYniq`yR&ez?UCKkcX(5T7! zr{uYR^TvpyKclMeg`hPZXCi$>%9QOp7PmvG+l7`}jrzb*-QoDP32fww5Vsdj8%v?h z<-A$;!F7-&aKG!-xnE(#PQ@5$K*Cm&>N=$|TiDtJaLZ_z{j{t-d9VpuRnI!VdMFoUYn5&7^pnCwFFEV3B!<#)CW^{8+Vvi{02B)xuqpk-zgOg1cA$ZwhsPs@M2|3LX0 z+}Cg88|QKf43`IZ?dknN$C*f=?nkO8zT>Htj1U+dr-*9eI+>9(_`v6Fcs_ccjX76Ikg&YZ8Gq<{-rT;6jZG{6tmgLQ{3 zBSFYnAB*ggN;=L&@dSR(Zf)As1E-#Dv5wtPN9%tWa+E(T?UnmdOUIdLoXeOZdwi}$ z7_59wcxafM2PoqLk(}YO^2x!i4PaGvw~7CaJlH58>bqd(HyC?9!1(-2H;^(ZU{j1k z4;=@8UKt%1m{sr4nAGu%u&YKf(pRzrv_B=@J#WTErY&L=8K!0?7b-D>Ky_@j@4%K zM6QG4ba}&{w?sJ~rhcrrlFg1$;vkYqxfqXH0T)BgNd-?rkN|yMMXNza$2pW^Ex4MM z=iC60pJd*U>|aIo8v*MQIqWLFW~819b9fmY_y&6KGB+|T$OfVQvpMWeXp+*23SZBV z=;NVX%N*qhQU#z*dPFLg9uph_S0zte&qYe>Bg2eT;)4 zr%|OS#ix_L9;F>Cj>ln^KIrFAnWYcPNt^%1ruy#K^8MQxbR60_#8Drh@#NVo&)O-#N`70gYEt?3+A+>*yNF49 zr#3(ZH6@K*+F#(KN804LCB?9Z_3_Esv7MNLiSm2dH2V5_A`bQaldrLb4jc+715qEs zxXT~1V~jXXifo?SH!h=#hjvYSym+YXhfJ`@ymQa<5$Mnj%zgcF7;~o_88U4Z9fx+#Z9ERM+RtQ40AtV0YCkB4st>07 zGigduYdhq1yfpHeP7X{s(pzi6;TrH`SjsZnqW~JVN1M#kEd$a}NJmv;0g0T$6{U5O z{_{xq{te|=Yd=@~#=(dA%|CUhW?k1YH0Ei;PIj2bY^0-S2e=9l&A zsqK`2xuh8^Cjd!ZxoVLC=_cUqJ^Ma!`v4`mMM%(A8uTjoX z(acr(8JnT-G}roPg!VCxT91J8U~dWhpbk6Ra|^)}iG!V$JvCratzPOHup$O@eE1NrEtg1KL zb~9Ft<_ks~=l+Iofr;95KA_bEN6{lVz>TksEN^#>@2!u<2~&iEYSAFp>%4zu(T z!izI9&n$gV4zu(bpY!MSK{?FQXX;@I;ttLecSRpcHVw)zl;%$Sr8`tI1qIs`z*yy~vIJ z`#<=)7v(4{P8h3q7;$nm_xjj>q37W19h5^joRP3?S0h+sP-W5T zo=xQtu@y&MUjeoPczKQucfWrw+$&OOuu-uA>{qu8lLR@KYtnZ;zi0H_`QD)6Ar6#DPw;EzjC4$TLxsrrdh7Odp{jYc+y zT3W!SumbHP-^#!#xg**$SLZ<0JMrJNVJ#%o)Fnx*8^99awZa=OwCIi(@cjYG0o+f* z(`&dfDvwN_<`Z}Da1a!_Wo6{tMxo{St5GHcTnqFS1FP z{`?O9d@srw{xa9@jTaA%KCm@oFHOGVQzzx!HOsv_iTDq<^vs z>e^BTWkXwD`L6f?KV^T2+3mNIr0(IW&Db zOWVDKzV|{U%d}??rgD(iO0O}E-I@o~uX0z56EXE$zLPN8{`dRs ze4`!3&yeHYxo17MA$|RZKb}K53Fa9htv!O6;QixodCK{z@emnuVm|x#i6R`@XLd%f zXQ3R`=?Po|azkLbjcK;0T@^ekx;pTD&=6cPr%zvVPZkaFb*vQ*j zZYm~oR{?GoJt4PyU*Y^`182n2s^F#|nW$U~6OxeKH2Qp(0pv*99TtGQAeg5>gJwE5p zkJ~7RS^CWROZuQ3X6Z9N=g;eda+sx$>|fFc~)_`(;^=ZT}ZON0eU=VH^cCZvDEk8>!8S^CKRC4Ep1 zv-BCCL&SJPcYX1;U?FcY`KYFLfSr70-@{JtoJJ5Bk zuWz$-9AYD@>n>TgHoOLCon5?K``QDz{?JF8AD45M{C%AxsSZ^^kJ)!XrK6S1x9u>l{$ zcpCRy{8)v=!!ZHphIfy~BDG1F_UZ3Btd&V)bt0p9A}>6TV?~Y^@O2^@53_tQ5Agw3 zFgrM?Uno4htyr3B;k`8|#IWP7tDlKNH2o?zK2veF~580a4q8Gf) z1t$(Kd;P*-9!Z-||CBJ2H%^>BC?|nrWnl89o5~@QJ-cRn5N;ZR6<5S(Xas(Re!GL+ zB^v8sr%{?x=#~;7Y5ygD)<1JerFkN2*CQPK^;4ANt)1*G=~4syu8ZWpJ3+Y*@5tN2 znr{UTXvrnQ>vQ|8R^3Pe2eVm;Qo1cbFv`Wb;h8MydhC97PlUsUi${SvPn$epQPL0`xk|fql=GN};+R;HyjAE8{8W zuoI6&)C8B%pAW%*4h4A}wLm>5ne8(gMvAFgsLOt+u>xEN{Bkm3Iws{(wD} zZHbq-tAR|nv#hRHIk+Yt!X*+mgA{k~xn}MJc|$o{5*i;^*%rdL--T6nyl2BGcp_F6 zv{-xhqB?l((wv1#RS+hg4(2~)PywPu=kxH}3zL?AKW)0s==XU;In45#5|X!ar>P2? zpN!lWx*@3V`CfltC`hnZS@x@_1WU>9P%*OqqofIUP2>$B;%}@I==}7c%5A77nl#`ZJfroE;kO` z^w?Ng5hO+W6!=)$7UAH>Ib~|zXv_LKOkYg9Q#aDT0>p z5RB(ld$IdiF<9KxrBcIH3La`L_?9eI160olhL^BSCn*wTKCeLhM*EKDcpTZv7n+{V z1K_rsvD~zsO`vg8x5Ty`*)TxuzT)$9Ah*OZoyZt!A!H?VHb4ISq*oB`+ z&S1mtZrnUnY{P~z;@p2tf*f<%t|l| z4q5N;y#7xju)8df5_GE>-285MKl17fQgB7$HFrcG{J1cmx+B_Tf#ThVm&H2Z#m{D& zw$#dLsXVe&Psf??I{tjD3ub%V4 z79LpS>adZXgZBX%&&_A*H?SBMa@;04@drW~fEp*+R@~oho^2M$-1}bMsI3f;zHN$5 z<*I|mil&e5t}?--9+#C3pQY!pQa2v;8_H2C&*M0vF$}+^nKrM``~fa4+cPD_i-mk4 z>%`Am(Rv_h9I`gHuNZWunX=y1{0cLcZm<#Z(#B#ATeY)=&~qk=CzGq_OFC~MyklUX z_iaTJH6CvA`nC3eN3;&i1K|&=q{wioziG3SO9eDrwbf%~fg)zl?sGk}fu4h}d(n9G zdp^9`uapdK%dXEq8AI!@iR4ptw=FqIU%{g(TIL&BiouziEmq!k^)RT`**TCS5!|~j zCh_WOryeC$l==lCHB?XVRn;r%u&eq`t=IO_gS7>NLPWm`##>5G1{iwTmK+)dyro z9C=>OO+OG0-Uld$g5$HE@EV&(`3?}G)D0FY;H%v-A0@N(ZzFwkLadySwK7d%O@i=Xze%fo$M2 zJLi>PaSEuZlKY}CsTu727zbWFmL=tSEid&(IQafr4ar!gSC@ank3Mi@!%%vqT^o2) z5_wi(GYiIu!{ykkcn;y<^+7p(bL9k+T86-$9y0L-$Jl)k7;=avX1*3#Z#G?WTRde&qKnX5@T$mzDw<#be~VZn6`?!5;^s9D--9l4{u?aM$3S6*F?5 zti1fsgMvw~fG^KJ&-(Mtz~1A%Y&OSBAiJ#aj$BI?sLv5BbXS-|dT`^JhbH0!eBLyX zyis0qW9ps`;wwYz!7L+>tMZ+N3_0niK1CcsIQafxe9nl!ScOA#Dw}}K8zn9GO*ITT zFV#3-y+kVQ>MZ(q<3SyG;{wyr4(BhJ>j?0%gH2j3r{oPAl@3qoFZ(%KKUYv1?J zTM~E}#^ddwV#tB`8b6+*oWKx==EE9&z)Gyuc`JbDNR-|wKDk13#? zXwU`7ann@e`@6uP%&p?(gKa?lnSzVj_G%C|xq-u|zZ~%Y^P?@mHXja0NQD%#Gc1Id`^#g9B4}!?xA`V;a=Unem3KRs~kSFj)W_i-GmBgG;9^ zD~CMt#VRvqNRn>OuwAHv#Di}?D96pF`-0k3TE4ik@_+V;@$gqM0Q#T=^Wu zDsj9YR?E0Pi(bYlu>aEko78!1+R zGe}lS0!i+rZYu$(jBR^$aiikEK9A%L3puyHmyma9x&wzq*HC+4(G!aUON^EN(*NNx~|+_ovH0VF&cXzR{xXUHL5(22Cl z{C(a~&VA?M^YMQDQ16A+(=xMG;4shh)1xQf;qkf)N)nS>Ah~riSyJ#S*zk>YH8`08 zKa{Ff{j&y;GJWQ8TCe_n-gK#Xqn~h-ua>+GtjcHwyz3W5V^viQIeSi*fBDS!`@D_M zA(E}lM88UFw1GW3(@7plO$<3Ahb|tq^7(zrdxxe9oWG8_IFz z34W)M(g``=&zSzxnRfn~Kz7ja_cSwl1!g)OJ8|$w5ttOQePfJhBixe4)hLm!LkblO zy)CeW{(E5f^Ayy=X6*iKTQ4^DvVJQE3+_b6&kG&`#1(OA8nttWrn{4->U)H4^AE&)#P(9>4F26(#T`RPTz9avIQ_>YdsB)a|$f4&Ok+_`+z ztxs-j|CoZQ?D5_#K{D%G(+3^3pF!;AvQ*Z!pTPOHk2=Z+nqgx7%!rZ`jo8Vf3#F_= z>3>fTUmu_xvuGiUss$X__n%@P+IhIC@emnuiWfz8&v{SJ!JijHIUXBc@085%1+Yfu z>Z+o4us30M(3+7t{;Z5gzkc@{P^{J`tLgU@EMzEb!)9_6UzH2Q|z9RgR{>tRv~?fM*%AqTTMHf^=H zGF{&A^)<>Fh&__#Gp`I>coGoJwr2oRvPr;xO#OEGRdY6o+;Gm>g)I*buL!ujU9=HA z+&8G6`ht&?KUYK3OPl`t!T94J5_Ly3ebP@E<;<9039=&}uMOEa1h*LmgjwjZGURym zZ~7#(jQ)Pncpsn~f`CPrRC@z3D*cB?FPw!;c^ljeIoh>#QwvvOba}(;gL1r|`Cr(w zp#vDy7Hjtja8Nl!h8(Wf3oq>FrvJV(ULTahtno*mst^6-&yPRjbN>AJgL1xH+}3pF zSwG;8Tp#rhM=MOw(>(}BzHe+Jsm*G<`wb8|AXfhFLNh#UFHy&{J`41p37KvzHHbOR ztqJjM)u;0T-fxSk38P&bjI!8(J?-rW8_w`2+?vt?l?bvylL|`#cfkw2BJ)(h$9nYP z;7FaSaL8_}*V`{(`6;eX`tseF%FIQP8oL+&XO7>!vndVcLtvZR35g}wDxp7@*4dog zYG6Rh$V}f@2vjAMPj@b?fr+nf`s@H@z*!|;Ijp1$%XqrV_R)6w`(R8IkG6)7@*`CO z=5sh`Gv`3>Sbw19CG*v1umJAl;nDMqsDyPkXY?y + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 8 + 300 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-02/results_true.dat b/tests/regression_tests/surface_source_write/case-02/results_true.dat new file mode 100644 index 00000000000..8979eb55477 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-02/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.169123E-02 9.144814E-03 diff --git a/tests/regression_tests/surface_source_write/case-02/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-02/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..872efa070701d0a052aa84f2f1980145fb59519b GIT binary patch literal 12752 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 fIbtUfR=hm1DWH4{y87gfrX|1|kOEHDC< literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-03/inputs_true.dat b/tests/regression_tests/surface_source_write/case-03/inputs_true.dat new file mode 100644 index 00000000000..38faa2a7844 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-03/inputs_true.dat @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 4 5 6 7 8 9 + 300 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-03/results_true.dat b/tests/regression_tests/surface_source_write/case-03/results_true.dat new file mode 100644 index 00000000000..8979eb55477 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-03/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.169123E-02 9.144814E-03 diff --git a/tests/regression_tests/surface_source_write/case-03/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-03/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..04a6ac9a387d5bd34e4d46fc533e90171d779238 GIT binary patch literal 33344 zcmeI52~-@6NAVtIfeCz()A- zA`%IV1n!@^hI~x^+=jG6N80+3wvqHxJ9GpF!jI4RN$0sq=?gPVo!CG+ zzsXoji!ga5Ieqe$^f7^-WonN|KJovz|4&(9ofi7jRV+xV$^Mi}@-wnA4)1q4WNW`| z$M$`Dex4%xB|^U7f4Su$`77S9XAxvc>9Z1ulm8qX_u4zV9k3@SKWB1>dGbG*KlQtQ z95ekqca!A*bfl6&M>lola?%&bTLywG!;e#Zla9Y@a(ig<|DU}0Ce#06qoaRv@#Hl; z!eCOxj~7BvU?(VipZvbt&*RCkK-x~X`KRw5BV9}zP5x&J|MXoK(q-g4{-58PI`ers!^EKr~q=SFh{F8m09i4vqnaV$0Pkrj2zU#Wv`R64F^@abi|4-kwv){Yj&E9U? z&O`fM4orS$-9LPf`V{x1<1qd_Axt>za>!wuqa8t?`htJhr~b}??T4HlZTC{j)P{e! zp86E^cbO)=_D8v)r>Fl}qKKq&#z1F7a_2^}d;d&gHp$C>Tx@i6mxGC+(OSC66v)1R zfK*=#P*{{(~s4%a!n`2wfDLBtZ$P0wrXF9mHk#QOQz+5ppnN3T9` zx5N5^Z+@LcGN{VI4fa}ck{VOj4}u?Ca$bBp^((GUeNG^}JktNvR+S!Eo7sCiMs|$C zY5gA0zBBkPH~{Bg5sdAGRRb5_x|+9wgNwfioHJj7HfB#uoKKhgW6l(hh=sFQHpmh% z;G&>ZH1{SG7PNfp)y%kl7_0hyOvE4&#GQT~u~(oNHrC7^eeAUuHOqj9IdZjVI8$YV zK=>y;rmb2A^4!Bebx$hCBDIcoC7Uv0hpNgOSIw#hyPDijm=07yFV(P3;_}__s*a=5 zhXW}3I^})JTp7U!EQRYMb(dY=)KM)*Az zEEQSV2{-$Tm)_BqMj!BdEmZECK7YtQF_jMlf|`K~^PmR?JP5o`uLiMG@@FPaaBjvO z`hC-vA3VoqL&BYf8DkVZS)Oz`p*nP!?PA$Aqgh?xd`0j%gKO3BjVW)>1+E&{bWXhg zZmJ5(xPQler{wANFp-6jx2Ch63aNevjh*x_Z!`V!w{@y}s>XAp;N`5150?&QfG}oZ zznzx{U=xGjF(|-*el3VN{!Bo6T7ScHVhz`SWWMx`qGuPv`DMvbPHZMlRKVNr4{6|2xnpC~&{ZlP)IM!J0?%OU(NyOCoM8T7a8hUfe(J*;FQr0w6* zgXjD$JyRVGzeN2lJ$TOF(!>5s%Fn&Or3cUXTY6^wM|$v_?k|tO!NaZK{Yl-t-uL5h z>Squ!0VmrIWz!;P*fDZ(M|VH)U9i)dxjYQEFVZfQWSNW3@3>=FeP+##3EAMG6u zoSt-Z*FYBIJRokeDJrJ3of79>K6Vy@i|at{h4;G$)4IV|+r*>p_e#O@N*!q_2L?37 zn3EqVS@p*p#Panb%~QL{=_42VY*!!5CSdapv@xlTHA3yPJBpty?gZ}l7~_VMD`8UC z{O@PpHzD;o;hXta8PIUZvGbqw97JAxjt%H0+Y5=?BPAg#_zlixS?40zRt`fdrR6#r zd*CH^7IolR2D6P5_wRO1LG}ond%deO|6|V8wbHZ! z2pD6|4Z2rS?H~s^;Htm42{v%I>6`S_!UL^tZ4UeyXS zYZ{LqSltP&d`6kt-erO}f(NAb=(C}jfqr}4bTs~$W51<9uP*chlsI(A*_~s8oKG`y z7Ds5DkFs7%!y(r>KkSCrQ)#oIcpoP{_PW}Hqj^s!g|o>vg2Q=k4_vMwt+Ze59enac z4NK6khhy}yUPAFB$nteES(5qd|CmGa1MC*V-LJBk^FG$Gd6eQ0GjY~ArE>Y((8f{f{K0e1 z+;%&8ac3LER=*m{T>piVKMG@XI2-T;s<^xoZ7l?P?E2_L%}^ayOv_j-DAToF}2K>u-l?DX-CZ#vGu97pF(!?B47 z8}2%M6}(jpJ=&elf@qnEem3%MgXcnZQ|ivP1L@0;26fjJ0I8J&z8ys?(U!cU1$BpL z`**0z51z9;wPbXUE(_8VFZECH$8qb-M_wW0`{Auhqn5&s8 zW=M~H*{W@GY+wLbhgPedC~pGZQYX|FY$}EFVTt8xuoDtGy3$29e??N@t(rS8Ri}+R z0yqvJK7CNoK}1NpFpSSL!R>e;pUt`qv-bhCA+M#LlXW=v)(X`C13)z}r_Z8=JHXU@> zfqE^Y#}xyBFIFcqGvhH-<({`wr#&9F#9p}W!(fcg67i19`weH&G@O51kI;X_5&n-j zBGYh28i!8tR(yuiruomK=Q3dy;^w;Xc9lRe$o)$8o=(V?Api&B8lnDX4U3y6^wE6l zt$tO%$sbW1$9jjsvjrk-h(mk#lL=`$Z0cj;j2tZBc0dn~L*0(YbMn?LR1ERqK*;GI z@!glE7By=|j>pJhOQII7UsJ~eJcpD%a$r#Us^e#RtiI3jX6S`4aHGVyrt;w?u+UZ` zAvm%d+F!od?FSP<i-R&L8oqAC7w`A_b0Znhfb(FmZJw@AD}3896<7 z#}gWH9BR8w$N9J8!D5^eQobPnHb>$=;z<5S9I5|^vjoTSbiACJx04ZRaGh`-I2uFo zYsJ<2Mivh`fa=9H8`N$^L(5}%Hs9Ra;K=r}3GuTi`Ur#-;_EqTzg~*tOdS#t=LWw( z`TL2`n)}0O>{@ngCQk3+D|v0d@oPLsl+5&2O0>>*ZP@Zoh{UKMss!FB8;HBecCU2TnIBwAg}w&yI4LmfwDDQQpb6tUF3D`2aEVQbfFlyB@K>%uH8 z>;bD_7gz=!JZ=-iph19czu4XuxZcF>q;d5M#GcVJxTuG=KBX={ayZV^ArTS0Y9ua- zFe1A!w(V6)^w^t)tM*thw*vkZX^UDtJAs4-tM#GS*YK8g+OGA_#nE*1RH5N-^vL5l zxiNdfdbud|DRPjQ ziPC?GL`no#wjC`2odln=kIZ79xUk?R5Lh|v`!Vb}+<8a@?ob~7o7*$mTJ@b z0d+e>5yv6hP0`&!*!XTYSfqV2Xvs(o@L_dv@AmHp_9wF>OPG>?q)|2|G081eGm{x1bDh0j)QNW$gGR}E7UldVu-x>4 z-4gaLVAl3|JkUuJwOh*QJA&g-+YLW9%B8qu#A?vKR89BTQYWlY0IMjOZoWB*X)o^-9 zcEe6qd-2XQ{Q&Gzd5QuYx}g&Poq}a5{cwom(gDXU5x_n-ebZRwdpLWE-<=#YfC^d- z`I@ONp}7yH?uT7P;*f1g;T+&h5E|$OSDIycjGTMm&fE>Q+^+pFftl;xsgL(zjGSGX zsdFjJm75>(c9Ii!-X>)kIz2~y8ctBd>#J>rgMiIaJqBrOgJ-)gNsI6dKx~i)6Xgm5 zTgvG-0k1-s-Kfwu#~h%C&o{q&%RW74HHkyJA!7eFhh$I+W*uFG<|lSWI$h6BKU%j_}yCLG~Vae)z?Cb)R7*J^exLFb_(TKh*UPsX$Vm z&xGALeP~>Yl0LFrSMu!VVlIsSwQlrq=UWiJttpq@;tlZskheUd2ywtdG z0DP9{v{LVtKnXj?H^+<8_QzAl9g<-tNBNRooQ9)fLp?ydaSp4-13GN#ED@=lTYlqJ ze+h`(;P8FkLAC-pS z%`cW0=0~=ID$|g=c2zAPG|7Kay?rMnsxiMm9+(544A^jE*Lcv5HE$ohY*YVVbKcEU zDA({{#N10chtH+wz)IkI%;`uMEK@o}*Opucnj&`;yU%KbAD4dDi)49%xUuz>8t_>B zF(<}v<(6+wly(R*bo9i8pa${|EZ3{4}3nN6!ln>J%eNB@5L(ZO++sf($~Z^?jDr-{fe|GXwG^Fhev zRWEpuAq9#dw!xvJJ>bc$q~6sV>tWwTChN>`S#+SNNPe`EHttZ{Z5@e&vm3J3Ao+*T5f0Nav_ds^T&Xshg?_ut^6_kkL+eN4d>DvMk{df z6|~CrxnzKTga#Y}1qnmFpsPA$^Sb0WAW5Zgu5eBT__n5H{uwq4^ojAf$g*?O+wGs{ z77+jS^0RRoJ^yz3G5(J@Cev`L(&BAc)1Ly*ZOP?FttVi1#hdp=X>B0rd*#zx%+)~D z>_SzVZwvI;*>f~oUITTrQ_`Lh|P=aQH%<7%yLvcDmcE^RS;kO{;VJ-W4V zC>OF;RK;Joe*r02ds6B4$aWeIb^hQv9A|d3kMj*t+969G?HkaQ=D-|`mnr&-R6!RO zvzz{6EwDAIk^rpQA*$(qK-j4Oc-aYk8(Nn9$DEks^$e`{e)WevzZmm=|E2FRq~c1H zqG&ZR8?_%ynb!yQemDp(jm1N8Y@5S;S3Pu$^>q4+@p;p7EOB z|G2sF)SDt_xGBCDadN&Cy>p4p4yfhn&|CZ!>mzl@jvGLEx)$H z>G@mb6wmovdba#Wdhi?{;=Gju=MpG>z2S06!@2A9n5xQpO^v87*jmrb6ZNznw#ap8F!FVYUHHOj{a`Cj&mGf`jot>7egmxk=wQG3>Jj+h2&S6tT?B4~ z@S{=f{jl?N>m2{OU{DbnecmNe8&!FgV9S0*l!ilHFIeL^e=C2s;W&TG-|(EjrN`z! z(u3#BjTYa1x~m^d;fYA~?S;MNe2mz;p&*+liwgi#j1fnKc`wME7i}EWS_Nlw&D*?< zUmxY&ZpRkhJ$-!JPSQj6jXl@pJWVw^!1Y-Q@8+S52#JTSS~2IraQu77zuA~IIoa1BRc|P2WD)LE?}%!;$KXm;mMZ60*`Go5jh84HF^`b-^W7 zAa33(g=Ya>;Bd;FRA2Er$e5unemhMPjkMLNcU(?8{+hb}!E+AyCOs%h`PB{)wpb#g z_Kq3b?j}iGT=*XBVZL;8fj~caS3IYEq~;~Oxri^W#8VI5FBBcZ>^GmL{7`dj$wElm zzvXXsIL_4JWP45eXheJLCt&cd>2{wX5z}~hF8xtoA1ri{UHI6e9KKz>DE_O_12Ehv zf4*|P4w`l(cYF4MObU;oH$?kpS%}{xM z;yEMj3Mfz1mQz12k8*yvHYZqlC5;|(S^3dVL66IywUx*7H+=jRuYG1=%Z`>YV~?Kg z3-A}{flZBA)(xK$xc9b2=K`NvXcm3S_|}7B#4W;2L&9?7v>XN;=W85u`p0v_fH&8F zT~9g@Bm2cnoE?jI*WFxgLc^iXA3P^f)Xt-^=nJ@VJ>kPEwqNr=C!m3{cS zhZ(P7@Yb>_!mR<2e6HC)sYDc&T5}rd8l#;TO71K8VK=hx(aIts-H(eGEcR}IZ{vD7 z=S7XcXUc-E#=OlyF6ekfdRsF9nw$0g4m87XF2)hT%yDF-b+^$PSK5Am>V72hKABVX z_=M%sJDs46a2h!E(^2eZB08v%aw-|<)+-3S+Fc6|iL8o4U3=k;ePa@D_&1@tpXI!v z8EtPhVbN1m=ly0x>xsl)V5?@Hc- zxMKa>x3NlSla|;4>!q~)Z)6QW>_+x+GVcKWE*WFiVjy57S`pVW0LXrDT``-rcy}wz zIw|?6YiT3Mx-4lfc(WO9-*q(7#6}C%Q(FG+cjH~i+Q@cGe7L~+{mRMq^d51e=Il2V zPJO`B0Oj?KaFGt3WY)b7$nAgRj-PQY7%gGj>n*H_3d?va>f@fDlGFa7hrCbbxPpoj z@1U=ctiwpdGGu8h1J-lq&KYyV4q#h%Q+Bm+Jrr~ImfjNE0DNYzzLPyHkLEMU-s{D2 z$Ts>X4k>+vvEuuS6DEm3!z7;5>(e;2a@ZCqw|=tUk~_z+X>TIr*!zkzT__irdAtw{ z$2eN^2EI1Y9GkbRu2=kjp0i^GF)uovbiuI6P%$hs0V-FK+pF9dHEmJWto=mJ?% z5;=Ebo8Xwo@fi0Da_BA<$ZLn=kZtut4;u+Uj&o&FISWeryGjSYG zy=FOtq`sQ6CSBBdJTuti(rYIlt56-fJSw zwdWJL{2<1mEHduVbVz(#Q)BR#D(L%o@*O@O~DC#K7^103j1X31Mr4$?&KjB=`t z(9{dR<17?<+wS1{4W1WCK1-MTYka^=9L`PdH6Ub^hC?nhKl}}!KdC|D%Kf#JdK;0* ze-)uiPeeFd-S^IYdJ85u&Ux6qwGRp=^1rsWu7V{OC8C`U2Z3h6bJq`_nxNtQPEWX5 zq}r;w576p)#@zUE7K-1^#Hr*-itX{B<@`<$z0fxY>!2_2Zj#Kc(<8sezv;hzz4eP$ z2eb^YeY)2y6dpWw4omKD1I$Oe&;E`>@@uRs%}F}u{0I<@ zmM%qx*%8eGYfA&eUYK`i6iOG@Kok0b9Z#aNV0)^|@>vQIK*5U1_uEDy4ToH({Zsys zIl&`gGAB(Np-Kh8qu@>w;2_?}yC>KU4~{WCt73|Pyvw)VTXe7)czSM~kejyz4Ow^l zo;GfL2sH;kZ&!(9TTrt4S8yz#vQf&GGQI}0{C?V>E9@OOwD+-pLt6*fJ4e_DrnkU{ zPVd`hD;lB#?@meQ;Ko~0$5HZg;3*vh!Xkztt=01xk(;&daj$Pv;wjcFA>7%WGui)^ z%tctEPyy)z7ac#bu^pi7opWc0F{7xj>lHOzeES{8Apfm^v(@Cdw`CoIn|yz9{620y zz&sEN)-8SHcjsXR7|O{~T$j)X1?HTb1BK~PqpS1fwf+|za{lB6j$)QZZ6KK`{YFO* z0b9K1jK7OaKU6%x^QKNd1Rm50vgVa2gTe<)#rt>=R00#s#&f9ihm&kD()K8Eo^pw5 z6D98ST^{F9JxYg|$eg=9dv62Way)>maU6FMTTS6g79XXoWmtuB~)cK^fS%5d=fgx3N^ zrj_U=Cr~4Z^Ec}D0e)U6DSd?Perr=I)(?2EzFaBV{tXO&9(c22a$MvKhf$TQ&!Pa4 z-TSEK;VN)W#os!uWDRQL_~Q2O>N))U##^nq=`~^W2${!OmucLnIRsvd+~Bk4YX%$h z?XJ|Ab%Tpfk}*wz$Kb`mx#bqsQfPAY^MmhyV>j~r#;J74??mL=b%s#ULM~)Ukld23 z2E#LQM4pAG&BfV`x}KYkgAVlN28n)!*n*9R1AY5I>d`~Nvg%DhIf|?O(s%)QX2DsF zw0FT2t|3h%{3=qAte;x2fwrEb=HTa1o<00U<|7;fS?8}saBxz}58^Q3xn<>GEs(@6 zByFHG0wqf_tWte@!OOPPZ1V>zQTdily^J_L)NvF)k5c(eb-7XEIP^?V`@&V*LW+w- zq(&6AieV{*A=f0f8QQisE=OE@eg~A^K0sII?en>O!$S*eR=*S z&`5|f@(iL!>gGtsAMWUbtF|vb>ASKFy4dF0WJhE0S*}%kqV?qbdXqVA#W)VN-T28x zhGt7dPKMpOX&dnwT;z}udN(#%Pq?LIHl;NI;J`mJ9MTPVddi=zr`rZRpRA3$QpSRE z7tt?biJ+bLPVLw8Y0@Vj5V6xAZutq-bpqj0{&{P$H&Ef!fg?rx2jB;jWh-aT%Ywmi zcX!V9P5_eLgcP1)1l{j_x>Yls_P&u?&;OX;I4kq$=S+;^&)muHX9w-;0AZX8VRXq& zQ1o@X!3Fnj;1Onl4K7Q7iA_O$Vd;#h&Y;snJ!QRV+f~!dZ^Q}<0;>D{OX0rS4OWBU z`OxIa{AJeLI$+)+$<95QZE%yJ_gl6Hv4H;iDm}|(9O%KPkp~lFY5RAm^-MFr5o;Yd zalvm_6AT!Aj@obk1Yc_A1%_Pd1zal5Oj`SE!FDDowTS0&0LfOoq8pTj807Q2^AM(A zFAdLOV9!&_+BQllH%}KfaS3m%0ozh7?)DiKfq?djHTES@?N%kY|e~v>^)@)!d=0$fUUJzc;zS5kn7 zzYgqg9jla!uK-7*^I_gb9rU`Fp4x;uZ9fuqdq;>y2l;@Aur^(JGe?JrFyG?(uzYl8 zPR5yUe%Cx`In;3n&(U|{Te{4j9-AT(G3Qp^Sk?u7;9Aq-1+cyv_J;U|YbSKV6|nso z@T>>(CBOS0sr`to)cHV2i2^isqmI|YWC^6Lo}AO#5Tyxn{v#1P`S$wVXoc$M&kpwU zv_exmo!}zTBJggv?EQhj4(NSM)ppJ9L1ee%=@I==H5v}J-9&JlsY4>>dB{}!Q8qhb zR$)H0Nrr&U#0kpo_Z)b-j)p_+2Y62NwT&Dp;hflIGmAYEhdB{4cP36~Q?TOc4I5}U z)P9ZUkkeh%={2877={Lp#(8xo`#=m~kDc4(E6y!kS$@UPzXTdhG+)!X9}7<0 zq2G}e&<~cM%`XbSE{3|t_#cXHr;XRt?E^fAtmh~z-}R#>s$u!Fb8r0FJ^;PSie<|= zd*C_|80#@q1R3>Q-zGk(10{Tp=CYMK=w3Z$R#a>HaqD6vCFFi=#(AF~5Y>LQ+jeE< zZ_(}h2Dv0Ia^F?S05`Em13I?tzh`v17zHw1K*iZC6qBObQQ(fS^S9w}>Uii~kOdd0bwx=l@z4$M&|`;n*+DP~owhddkaa zm|}lfZSZ*)uuX{QU8k}M)v#X{eK(GFoCCEVNZ>g6x_X;V$}?kQ*QD9AU;kR~$!^Z= z_LKa@@LMf!bz?ybcq`GtCLPuR)3OJ0UX)cMhO5IjI(pESQ|j^~iQ`Ni60yrZidNcv ztzUhJFf_ey7X5zZN;#M6hImqOg*FTxWW>XH~cs^A%amQH!i zKA^5w?RK5R2rXXV>3)i3`t^5~;5hCdYu_aXRKbC>$RqhiN_~e}zl)sMwE}`+JqtOF zM{U3(CR46RwFBDgD^JYQKv5IveV66LruS?7JlCkG8?D3Jt0+2DMNI4Fi4tezl(q2; znaj{}sQntx*>LE1LXzqim{^k&Bw$UCk^BN-7BW7TEszK6%{m9}%xi^@N?0t!1Uf+t zdwx_ox)#0TT(ESdGA)PNub1KM*5|f2z$@(=Sr2w&IAnihI_HcW=e6@fc38>N#B1vM z6wmo)zl>+g+hH;X`NX%oP12ek!T2UljlWKU%@1zT$1mvs6-PQ`vMSoZ?TZp`W(8@Y zSi|l!!dbNK18Tc1$LaZ79Q?f9#!b(=xaeO%Ntvw+GVgIAKNbz+IcAkRuIig?Cm7DEWvK+k7Ae(z?3v*4M4Z5PJ2|wX zMtwLbb^7&$@bi#shYBn_$NM3ddwZMr0?If9EUlo~%~iPo2#GShvAbz^W-?v16%msd_V-72sLSLv~iT0 zgP(^?(u0M_X*U<0d<)iE_A9e7cf<8(zNe%#biya|1Pj_tnt|&s<(H2li@;(2=+JOR zLzMr3w4?~j^!*{^dB|j2lI^zV+fqTo=g&a?vJR8y$?tHP!?>hwLk~zV;(xTOvK%;n z>w_Y<%HX+Dsg%1#IIm_4xMj z_rNk>LVDTcyaL`=t6nQ#>4Tcv-eu;bx+2W7!cH57?5FhuJcq1@z5TO>)#I0N^+fVm z^Sv&pX;2~leX{(hpW!*M>PQ0Ux6yVc?5+Z}>-K&XlIBA7w0DbL@}r#(NUcYiq=$Tt z%z2<|Clyj&15wMu$(1!4!S=D*0=DFCz>u)*&O`aWX{&(;{_|yd%@LEO^Km1W$>|x-qMq?y+Gb;TXlus4ag*a zAV0*UcJeuDscN2|HtJxRKTpAukH&7)990sByjj%kS0KeU4g^n!hCH29L5XieLM(nO zt@2=e+iR`&8M*Kp%W>5wupf90ELp{0xB|VO@qx`rpLRVv>iCAAhs-it+ zF<0-gxfErvLi$9y!1J7jQ^N@WFfS8+D@5ZKy zJ_;!-kcG5*s5$B+06BjW_4?Bvt!KcFoD`k)=_ETc_3`BRml-&@GW^@CkJ8?sQrivB zA*G9j8LCNtyTFN9I8?mkUCKTq=Uj)(VS(foG4F@R@eE6O}b#nf;@!Qv6 z0|U}jRezlQQ$Of)e7^MtLl)HNdcf@9nG2H4%w^bY1ktgVj!SpPOdk)(^Qy`11F{}Z ztLUaz*cfoqoPE>lViDY&5S?LB)()B`v8vIGfhRRbr9Jzv5u$e?o@GivPjP2cWCNCfhIy^-~Y=f&1Tpm%fT@v-J# z{*A8J4`C~B1H4{key-uwu<5zFdZua%kcdzSLU&mq``0*ozMbzw;|J9KhEkMF?dS+w z9JFm3g-hvs=Q8^JS}%G_XyiL7rev72U(ES*XB+gg8`&q!+X2t5NRx0~i=tbyH-@>1 z(Ate$*ZlA|O&q7lPi&Flqk7mMW_awb=C6KVj(839*u`G>pOOwr+H5 zT7c`-rU|Xu4nY5=Bq~U3DawEDuE3)^Yo@i^TAZG#Ln7j%u%}Q@wH=syJ1f?&Vnt@+ z$P%~r`j66bsN*%B^LXo`61a;VA@MN1P)3=;Qw&&Vt8VLX^ldAv`g^&Xw%(xS zQ2QI6lYiJo<@>8&s-rn+V61Mp<#hNW^RgFtav1;{_R z3YAdIRhyGb%c0I69g-e$JP7v?qqFxbB84L&X6K<-(vC79FLvfTe9nMSW!4-99&7;G z`4=CAOV5J~;`QTtuh`H}HGCDuakTX*HD?{pZc~RuWcIU7y&s=+!ZU2nBl5kULAF4w zM7UrF$l87?t#b>6hPu-Gb#mK4_1VYO0o-d)-cR0gBe!VV@zj2;i{q>u-xBgglyX0U z1o_(UTAwinm(9Jlx%}x{ki+L6=M>llzSrE9em%Lq>Tca4x^@R))O)dy&hkRq{wnHt zji2wlNTT+=%vJ(M*42FJtOVQSI%#Xtk8IJ|mjbyJ_`f|{-V6d-^w4ttZm|BI#+AyZ z*N6(Q_8OBbwD$|t96g*KQu>(9n%o!ZCs~o?S^L#_HVy*Y_~BjldIPXviB2cq{anD8 zSpP{eD;{R_)b6{bj3Rm3BF?MMoTVv0)b$*m)7)hqx@{e$d?Ip}p0wYU83c3I>N*uv zq=IT;H_u?Fc1WP_+;zvZ9qjkmc6zdHh8z)LZUnC`({TPo&sEi?T}Swtkl8A^>W|M8 zkW->p^1p>Pg706$mt^WRflnop!!pusfN;f6_({qH@RH1lkjZ{V!}*<_^`W7G=H-l( z{Mjrj$ACcw#QMzLj4PJy0PFI*mBKa2sW5ri>st?k1p>3Ut;fOq_K;%%J&81&-|4YS z*(s5uHUW2DuIDa>zsB>;#CgcE=h{9M+UI7}@c^Gc8y2vJJ?ZT*_)AnXh_Pf~FG*KMi2|T0;5=a?`dGsp|#& zx&$eSXQix!S}1ngbpFPQ0l)Vk|KW84`|I~`?wHt`k;!%6B17nT@+wn+q@2$SL%SvD z?#@PM(dOx&XKldg>CaO5W}cHv;f%kYU7{k(gqdErH;d^;6<8Xmn7A&y7^D{Q@}!RS zKrq|p;%6ULG@DsUXHgz){~LAw;MXMx6dH_*E(n1O+$$C4wov?SM$T)C2ZJ1*wCmMS zx2y1+$8#At?nN^r?3n;eZ_Q~SX*94z9F z<(d`YBj77Go|Bt845L?H*M6Ye2Rp9Xd$6XbfRdE;{?{`qVX2T}#;5y=sLL~+w+4;0 z<&?Vq!LPG`1oK#%pEyYIgESuoEdloyz??L@Ti>n^YG0=loYhqczDx5v>o+_D&I(#e z!u}T#`lTE8ZIs_iqlemV_;nV-&l()7+4Ov$KG7;_dneL z3l2Z}bf~@&^rG3m{a(9~OZcZ zT<_XTHJXvo0|o1|*KR6Ig4yLN-8mIokastX)@Pl#M8l!B8(z;xiMUmrzIcexrU$hGv`sM5_6B_^xZ^QLuC)an7yE0U3vzZkYeUw}JX)kU68@1iY>qd~< zy<|Ow+ZP_H;U5F96E0n2tsaDh7YKfDFZ95X*xtO^)_LIL+U+Y^zt_S20ctP!J_@5p zjWU=RC1~%rsr8uQ{5qv<_a05|F`yVK-Y&U{(vN^?TYlY^%8&^I1_({T-;2Pu_l5ED z2m9e(MR?Ys_!1I6Z?vOlkaoQ%>T-%-&*4G4vXAefbg=P-=w0PGl>1;T_~exp5^Wu@ zBV{~%{^A-~=TKm%Vc!T&yyq@CG{S=%t+w8^!|OIp{``)k7}{gHPl?h#z&_k(y3NKn z0y}Oiu0OQ85juOT(nXusLyv`b+UGHMfMt8Xx40%Yg227;ITmzqg!GJH1~NI+B&NlM0>d}rhnQFLWX-cck$JL6CTA{v(TGhV(fY%v}yZe zPOq~r`?sJNc!I3&+YH@sHtU~g~o~Ga*ByGoV%gnMZk=7J5GymltWK(!{WUO9boBJ?t*=j zv0sjTmnj`6Hdg>W&UH1Bv+m7qd-bL!0y_GUlZ$Mz%mBJ&Yuix14kYE=;My5D9(Qa)E&1QY7NE!lf(Ok4@dkt=QW{cSy9}t{v$@~^=zA#GhiI@ z&YD9$?;xX$e0NYrKYT6tRj}F-f}|v#H8Q&QLGKp+2V2J2|5y(>|BE?bd;=XDHs^f0 zm(o5uOeR@3Df3h#e163Ej#6_G&{lu%D96_gPTQuiEO1N&f!FBQglTfoaH!(}-meS4 z9}X&>oeQqs6X^=||K;E38nu>hu&sl~QVZP|#&y7_9~yQn+0_8&>uh@_xl|FosGD-e zAXbFNuc^x^c|8>RDIo;ucO#u8beWK>i`7a=H|+x(HWSB_;bD2jgT*u)YQM&F_H%DE_nzY+n6(M%p$$s*~$Io5;Fu9K1gi@uQYiJ#e^ibTP_-qU*zZuK+ z`84gg4QdX4J(LrRs;f3E?1a0cQT>G;M2xI!YxJ(0_k@Gt@Z+9Q)TaZ4Dw>DBe$)nK zd5kx0IGupp)h#g`v@xd9Lv1%aXYHmO|8&{Ozq9i5Ma1G_1%(P+iP%h> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 4 5 6 7 8 9 + 300 + 2 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-04/results_true.dat b/tests/regression_tests/surface_source_write/case-04/results_true.dat new file mode 100644 index 00000000000..8979eb55477 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-04/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.169123E-02 9.144814E-03 diff --git a/tests/regression_tests/surface_source_write/case-04/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-04/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..75c309eb4f43740be530493ea10863478d06a525 GIT binary patch literal 33344 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 GIsXqD9JFl! literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-05/inputs_true.dat b/tests/regression_tests/surface_source_write/case-05/inputs_true.dat new file mode 100644 index 00000000000..49eedb7cfb8 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-05/inputs_true.dat @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 4 5 6 7 8 9 + 300 + 3 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-05/results_true.dat b/tests/regression_tests/surface_source_write/case-05/results_true.dat new file mode 100644 index 00000000000..8979eb55477 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-05/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.169123E-02 9.144814E-03 diff --git a/tests/regression_tests/surface_source_write/case-05/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-05/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..1a2f54d765811bd10a2758e20a11b7ea1f242ae5 GIT binary patch literal 33344 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 F{{Rq!v~K_a literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-06/inputs_true.dat b/tests/regression_tests/surface_source_write/case-06/inputs_true.dat new file mode 100644 index 00000000000..cdb9726d65f --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-06/inputs_true.dat @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 300 + 2 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-06/results_true.dat b/tests/regression_tests/surface_source_write/case-06/results_true.dat new file mode 100644 index 00000000000..8979eb55477 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-06/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.169123E-02 9.144814E-03 diff --git a/tests/regression_tests/surface_source_write/case-06/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-06/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..bef135e5ace081b17fa1400cddb095fda62df652 GIT binary patch literal 33344 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 YCj~A4wCW)cI{o0XzX)Z2bf$d!KQh5CSO5S3 literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-07/inputs_true.dat b/tests/regression_tests/surface_source_write/case-07/inputs_true.dat new file mode 100644 index 00000000000..ff73358ca89 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-07/inputs_true.dat @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 300 + 3 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-07/results_true.dat b/tests/regression_tests/surface_source_write/case-07/results_true.dat new file mode 100644 index 00000000000..8979eb55477 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-07/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.169123E-02 9.144814E-03 diff --git a/tests/regression_tests/surface_source_write/case-07/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-07/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..f84a5c4c5b6d8cb4ad3f720e40580f6309998d57 GIT binary patch literal 33344 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 300 + 2 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-08/results_true.dat b/tests/regression_tests/surface_source_write/case-08/results_true.dat new file mode 100644 index 00000000000..8979eb55477 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-08/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.169123E-02 9.144814E-03 diff --git a/tests/regression_tests/surface_source_write/case-08/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-08/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..1fd302a7cd429beaee0a203ec8e5f1da09cb09d9 GIT binary patch literal 33344 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 ud%f^f literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-09/inputs_true.dat b/tests/regression_tests/surface_source_write/case-09/inputs_true.dat new file mode 100644 index 00000000000..4e893fffa82 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-09/inputs_true.dat @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 300 + 2 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-09/results_true.dat b/tests/regression_tests/surface_source_write/case-09/results_true.dat new file mode 100644 index 00000000000..8979eb55477 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-09/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.169123E-02 9.144814E-03 diff --git a/tests/regression_tests/surface_source_write/case-09/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-09/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..af832f060cbfbfd6f25fd6cd2049a5bfef55e307 GIT binary patch literal 33344 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 literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-10/inputs_true.dat b/tests/regression_tests/surface_source_write/case-10/inputs_true.dat new file mode 100644 index 00000000000..2d8f1ce6450 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-10/inputs_true.dat @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 300 + 3 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-10/results_true.dat b/tests/regression_tests/surface_source_write/case-10/results_true.dat new file mode 100644 index 00000000000..8979eb55477 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-10/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.169123E-02 9.144814E-03 diff --git a/tests/regression_tests/surface_source_write/case-10/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-10/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..1512d869298f2a8a0c86d1218646be2abe1ac77b GIT binary patch literal 33344 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 300 + 3 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-11/results_true.dat b/tests/regression_tests/surface_source_write/case-11/results_true.dat new file mode 100644 index 00000000000..8979eb55477 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-11/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.169123E-02 9.144814E-03 diff --git a/tests/regression_tests/surface_source_write/case-11/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-11/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..0edb06c88e9a4ba368a72bb0a16b698553dee244 GIT binary patch literal 6408 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 literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-12/inputs_true.dat b/tests/regression_tests/surface_source_write/case-12/inputs_true.dat new file mode 100644 index 00000000000..087024695b6 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-12/inputs_true.dat @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 4 5 6 7 8 9 + 300 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-12/results_true.dat b/tests/regression_tests/surface_source_write/case-12/results_true.dat new file mode 100644 index 00000000000..d793a7e4216 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-12/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +4.752377E-02 6.773395E-03 diff --git a/tests/regression_tests/surface_source_write/case-12/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-12/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..ea81a8250956ba2f3da07bde3332e13963cd26d7 GIT binary patch literal 33344 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 S)+dnMzTq}Zl@F~h?f(H)kZg?r literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-13/inputs_true.dat b/tests/regression_tests/surface_source_write/case-13/inputs_true.dat new file mode 100644 index 00000000000..a3421298b94 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-13/inputs_true.dat @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 4 5 6 7 8 9 + 300 + 3 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-13/results_true.dat b/tests/regression_tests/surface_source_write/case-13/results_true.dat new file mode 100644 index 00000000000..d793a7e4216 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-13/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +4.752377E-02 6.773395E-03 diff --git a/tests/regression_tests/surface_source_write/case-13/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-13/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..08bd809852152200e5449745616feef691f46778 GIT binary patch literal 33344 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 Q0?F+gZo^dh(CX6uAE<_K5&!@I literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-14/inputs_true.dat b/tests/regression_tests/surface_source_write/case-14/inputs_true.dat new file mode 100644 index 00000000000..b4c141b5d53 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-14/inputs_true.dat @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 4 5 6 7 8 9 + 300 + 3 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-14/results_true.dat b/tests/regression_tests/surface_source_write/case-14/results_true.dat new file mode 100644 index 00000000000..d793a7e4216 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-14/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +4.752377E-02 6.773395E-03 diff --git a/tests/regression_tests/surface_source_write/case-14/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-14/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..50cddcc70cd27df73f37a288d86a906d7e7694fe GIT binary patch literal 33344 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 TdaX|&xqZWJm?|GyUE2QxR|Ib8 literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-15/inputs_true.dat b/tests/regression_tests/surface_source_write/case-15/inputs_true.dat new file mode 100644 index 00000000000..9244d839d13 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-15/inputs_true.dat @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 4 5 6 7 8 9 + 300 + 3 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-15/results_true.dat b/tests/regression_tests/surface_source_write/case-15/results_true.dat new file mode 100644 index 00000000000..d793a7e4216 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-15/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +4.752377E-02 6.773395E-03 diff --git a/tests/regression_tests/surface_source_write/case-15/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-15/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..3e15017a16626a9a66d903daf1721a97dc3c4058 GIT binary patch literal 2144 zcmeHI!A`kb>e1ZM}pWvtXS2(*n zV-{FqJQ`U7GyCSvv^#xM-kbG{!`w*@nD9$mAib{Y`T#0ae-AwT^utk*OrwCyAEUE_U%tM+FS*YuY)<+YG#NrL zlPhTJLdBJlA_{+qHeL>I+!e9D?9uB+#&V!gKEqSURfFn2R2P^(C5pyrV==be8S$3i zChN(z+>~hijMuX?@|VwuFFUogifCgow*DmHlhWC#g>OQ(+?PS3C^eI4@ol-iVAPog zJ%137(+G2#mOK;6qi`|`D8nR;`+nGiOOs&n&77zcCt-K=JyR_U--OKEEUH<~jpI0K nusoVGSFHZC=n6-qeo6M{!~X5nbzNWu)_z3uYF=;a|J;Euu@!3E literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-16/inputs_true.dat b/tests/regression_tests/surface_source_write/case-16/inputs_true.dat new file mode 100644 index 00000000000..b3139928aa6 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-16/inputs_true.dat @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 4 5 6 7 8 9 + 300 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-16/results_true.dat b/tests/regression_tests/surface_source_write/case-16/results_true.dat new file mode 100644 index 00000000000..a97be70caec --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-16/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +1.496403E+00 3.891283E-02 diff --git a/tests/regression_tests/surface_source_write/case-16/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-16/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..49f67e8205608e848f0130ed65b472ff98eee046 GIT binary patch literal 33344 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 NWw4>&FGk7_{y#M + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 4 5 6 7 8 9 + 300 + 3 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-17/results_true.dat b/tests/regression_tests/surface_source_write/case-17/results_true.dat new file mode 100644 index 00000000000..a97be70caec --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-17/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +1.496403E+00 3.891283E-02 diff --git a/tests/regression_tests/surface_source_write/case-17/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-17/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..35878eecae7d7fcf08cbd53e5db8b2d63e3177d3 GIT binary patch literal 2144 zcmeHI%}&BV5T0#8D{2Co5aNkP;K0F~n1JcQC{YjI_$|FTVm3ulb>*LW9kI~t|FJE8Zm)vI+HYfcHnhYVB z$rZG9q2kI&5rscO8!v}9?uuAn_UL6JV>wVLpW!LwszG%hste4Y5k=#)u^3zKEAf`! zChN(z+>~hijMuX?@|VwuFFUogifCgow*DmHqte-_g>OQ(+~;AUC^eI4@ol;NVBDDn z{njv^q!H%$mOK;6qi`|`D8n?32d%IVmnOmDn>kS@PQu>!d#0Kez6qJRSyZ!}8^>|f oV0koWu2}tN(G`wJ{gT|Bj|R6_*L8svSo;ypt9iYx|8obv0JQmP-v9sr literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-18/inputs_true.dat b/tests/regression_tests/surface_source_write/case-18/inputs_true.dat new file mode 100644 index 00000000000..80d6253a16c --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-18/inputs_true.dat @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 4 5 6 7 8 9 + 300 + 3 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-18/results_true.dat b/tests/regression_tests/surface_source_write/case-18/results_true.dat new file mode 100644 index 00000000000..a97be70caec --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-18/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +1.496403E+00 3.891283E-02 diff --git a/tests/regression_tests/surface_source_write/case-18/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-18/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..36da9e7a7f7307e5af5221f380b58750742fcc41 GIT binary patch literal 2144 zcmeHI!A`kb>e1ZM}pWvtXS2(*n zV-{FqJQ`U7GyCSvv^#xM-kbG{!`w*@4&2rnMMJZKSpN + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 4 5 6 7 8 9 + 300 + 3 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-19/results_true.dat b/tests/regression_tests/surface_source_write/case-19/results_true.dat new file mode 100644 index 00000000000..a97be70caec --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-19/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +1.496403E+00 3.891283E-02 diff --git a/tests/regression_tests/surface_source_write/case-19/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-19/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..f4261451dd4832b3ebf2e4f014543c850f8f35de GIT binary patch literal 2144 zcmeHI!A`kb>e1ZM}pWvtXS2(*n zV-{FqJQ`U7GyCSvv^#xM-kbG{!`w*@I+!e9D?9uB+#&V!gKEqSURfFn2R2P^(C5pyrV==be8S$3i zChN(z+>~hijMuX?@|VwuFFUogifCgow*DmHlhWC#g>OQ(+?PS3C^eI4@ol+%f7G4& z{q`UprxE5fEqNxCN8w}?P=-kw2ko#AmnOmDn>kT8PQu>kd!||zz6qJRSyZ!}8^>|f nV0koWu2}tN(G`wJ{gNEahr#XDbzNWu)_z3uYF=;a|J;Eux+!Yo literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-20/inputs_true.dat b/tests/regression_tests/surface_source_write/case-20/inputs_true.dat new file mode 100644 index 00000000000..44f0d72c732 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-20/inputs_true.dat @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 4 + 300 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-20/results_true.dat b/tests/regression_tests/surface_source_write/case-20/results_true.dat new file mode 100644 index 00000000000..7ccce7cc3a6 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-20/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +1.149925E+00 2.542255E-01 diff --git a/tests/regression_tests/surface_source_write/case-20/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-20/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..fa8a4e628728deb4c4d75b03cd31f0aefe6d132c GIT binary patch literal 33344 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

l11eivG9^A2FF@o(kK8lbM?n}(nd+t7C#e>`?H zC6C)gdGK=|xNVf6k)K$De&^fTD6DwFa;qE2&u7ybxseT<>-PJ~OtzUXh^T>XbIBxI74S-f2E^v>Q+dKd( z-#@rNdTB^j%8i$t=S}3$pUa_fFekOBW2V6=uy}!eJX0wPR=U(7 z-GrqAvaX#KlQAn0?px1zPh6=DK8ntz%zLklo>gGUN;pEc1HA7~%7f?O-XZe%u0Ozo*!suD$6BDRzI&-Os)jl?)epW= zAajWQhgQ*4brXM0B%Lphy-S|1Um@@I;ti|1A@|)`%!c%ldU<||3O|?Dx6d>(8*WqOWqHTV=3xy;CZ86jVurT+b7$HKF`5_`Nw|s zCnLDt5RY-ogE`f+u!SD1bE?i(0mBEu@)5VH!NR)jch+;&L*ETsZ=K&Ijb366otJAp zb3et;mrOSht=${GHkjXWR}q0>(fFnYN|~IQ2FaS-E9c_x}14*rw?z z_1rEB$ff2O)h%oXo<3{uJkAtEAAkAf(63CM{}BBFeofv_r(&0SeRb0`8L_nrx=$i| zi{LBIGqWl-cfcFT`Jh@L8Jz8i@!WGa8zu~T9*`UCN028cn+1oINp?V#ho7u4+8q@u zi7rN4EcyV0Yowc!Tou%aKSXt(h}%aSBCI7>rGYt>#|0UmPt5=8Uwkt9)`+BzTOMvS zA?J@0IrzU%f$Ob9HMHmZi3T90J;=TA(nS8|KQAgGAE_vFtM3g+IWAM#ZJ7#&+HWVu z?d$}WT8|d08uTK_je-4dT1`mu;JTW$H~f4FSu83N+s@z9HnINcka^L@?#mc>*_ib@ z+^G{BU1{I+=*bs&@`H#?^>`dG`D`|nsW=-ov*9{p`C#V!H$3kNkAXg9AK=U0HogCY z%<1VCe%Cn);+gElTQ}vwRK3W`+wyH7*3UF4+PVPTDhl8%PZmS>MO>C*yGV}Th~v+0 zvLF;@CU5F&CPqwkyPK#R7b7zDHWlfIF&8u@^8D|+RPs}BorJmj|~wGUAq`g5W9?{BFuj6gZNL9d#G^VNu9$kVCl znUv551WJRnm@Wl?TgJBTL#$;$&y;OVjVw?lj4xaTEwQ7Pa-G};$tU1xtWt+FkYLL%o5&bC6 z#O<1|`jN3jei4lPaNm*7@b@}}&%47F(Zc0$SZZC|ouLxom1ckZ7;iHaI(nINEWsaP z1)N^|ZYN24BXa1^N#J^06Y|wtazO(~q(q;UIm3cgItU!fJU4NUp!(y^c$2rl$vyL8 z0bdfB_h8)m!2A~omUr^Uu=P8|47r?AMd+3C+7vGp5-?N7n!YiZCu5S8N01^ z+PSU`AmV1ej`d}*)3x|03)e(`-0}?^xtuM?Xg@{MD2F`$5IMp$4qiCizU8~mb2@&* zGPjn#mv}!4mWfFK_7kxncbA*=tNv_Q&A_^X9}xQw-u}2ec)f#x0}F=@`r+ly^RdB;s^O*TOII%1wS%Z#2lMwb zyn+L_zDu7dErLud9+YH7uR)U^-@i5cEqR@fs5g8)jE;jjGOoN3S;>Ow$aZcOIMNMI z^hc*3C}@MthxXQnX6HkPeH8*#!r8F)e)oY+e}FD)LNa(Zk>}fZn@{Qu*B8!NaOwJm zScB>F=-5|?2DNql1krQ#mKR@3g=pAuYpPl&%*ruecKOr?sM+yqVPmcw>Ma%akohoq z9hTTW!~b=-c=JgcN z!aP`ZTCW_J2gCZp`^%LaL03V$$jjm{V8y9JjBdf5P;p}8r2m2(TDrw5?4<~KzCh%N z;{aNDCZaV0p;&S}6o$Tpo@z5LHUfGpNxulVi;;Mmynf?c2iyzKWiRID;V zZ&Me~MeG57{;! edE5?HrP?A#ixAW#=z`r^N%DD3qCE8bS^o!D&vB6e literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/test.py b/tests/regression_tests/surface_source_write/test.py new file mode 100644 index 00000000000..f949d808503 --- /dev/null +++ b/tests/regression_tests/surface_source_write/test.py @@ -0,0 +1,1126 @@ +"""Test the 'surface_source_write' setting. + +Results +------- + +All results are generated using only 1 MPI process. + +All results are generated using 1 thread except for "test_consistency_low_realization_number". +This specific test verifies that when the number of realization (i.e., point being candidate +to be stored) is lower than the capacity, results are reproducible even with multiple +threads (i.e., there is no potential thread competition that would produce different +results in that case). + +All results are generated using the history-based mode except for cases e01 to e03. + +All results are visually verified using the '_visualize.py' script in the regression test folder. + +OpenMC models +------------- + +Four OpenMC models with CSG-only geometries are used to cover the transmission, vacuum, +reflective and periodic Boundary Conditions (BC): + +- 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). + +Two models including DAGMC geometries are also used, based on the mesh file 'dagmc.h5m' +available from tests/regression_tests/dagmc/legacy: + +- model_dagmc_1: model adapted from tests/regression_tests/dagmc/legacy, +- model_dagmc_2: model_dagmc_1 contained in two CSG boxes to introduce multiple level of coordinates. + +Test cases +---------- + +Test cases using CSG-only geometries: + +======== ======= ========= ========================= ===== =================================== +Folder Model Surface Cell BC* Expected particles +======== ======= ========= ========================= ===== =================================== +case-01 model_1 No No T+V Particles crossing any surface in + the model +case-02 model_1 1 No T Particles crossing this surface + only +case-03 model_1 Multiple No T Particles crossing the declared + surfaces +case-04 model_1 Multiple cell (lower universe) T Particles crossing the declared + surfaces that come from or are + coming to the cell +case-05 model_1 Multiple cell (root universe) T Particles crossing the declared + surfaces that come from or are + coming to the cell +case-06 model_1 No cell (lower universe) T Particles crossing any surface that + come from or are coming to the cell +case-07 model_1 No cell (root universe) T Particles crossing any surface that + come from or are coming to the cell +case-08 model_1 No cellfrom (lower universe) T Particles crossing any surface that + come from the cell +case-09 model_1 No cellto (lower universe) T Particles crossing any surface that + are coming to the cell +case-10 model_1 No cellfrom (root universe) T Particles crossing any surface that + come from the cell +case-11 model_1 No cellto (root universe) T Particles crossing any surface that + are coming to the cell +case-12 model_2 Multiple No V Particles crossing the declared + surfaces +case-13 model_2 Multiple cell (root universe) V Particles crossing any surface that + come from or are coming to the cell +case-14 model_2 Multiple cellfrom (root universe) V Particles crossing any surface that + are coming to the cell +case-15 model_2 Multiple cellto (root universe) V None +case-16 model_3 Multiple No R Particles crossing the declared + surfaces +case-17 model_3 Multiple cell (root universe) R None +case-18 model_3 Multiple cellfrom (root universe) R None +case-19 model_3 Multiple cellto (root universe) R None +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 +======== ======= ========= ========================= ===== =================================== + +*: BC stands for Boundary Conditions, T for Transmission, R for Reflective, and V for Vacuum. + +An additional case, called 'case-a01', is used to check that the results are comparable when +the number of threads is set to 2 if the number of realization is lower than the capacity. + +Cases e01 to e03 are the event-based cases corresponding to the history-based cases 04, 07 and 13, +respectively. + +Test cases using DAGMC geometries: + +======== ============= ========= ===================== ===== =================================== +Folder Model Surface Cell BC* Expected particles +======== ============= ========= ===================== ===== =================================== +case-d01 model_dagmc_1 No No T+V Particles crossing any surface in + the model +case-d02 model_dagmc_1 1 No T Particles crossing this surface + only +case-d03 model_dagmc_1 No cell T Particles crossing any surface that + come from or are coming to the cell +case-d04 model_dagmc_1 1 cell T Particles crossing the declared + surface that come from or are + coming to the cell +case-d05 model_dagmc_1 No cellfrom T Particles crossing any surface that + come from the cell +case-d06 model_dagmc_1 No cellto T Particles crossing any surface that + are coming to the cell +case-d07 model_dagmc_2 Multiple cell (lower universe) T Particles crossing the declared + surfaces that come from or are + coming to the cell +case-d08 model_dagmc_2 Multiple cell (root universe) T Particles crossing the declared + surfaces that come from or are + coming to the cell +======== ============= ========= ===================== ===== =================================== + +*: BC stands for Boundary Conditions, T for Transmission, and V for Vacuum. + +Notes: + +- The test cases list is non-exhaustive compared to the number of possible combinations. + Test cases have been selected based on use and internal code logic. +- Cases 08 to 11 are testing that the feature still works even if the level of coordinates + before and after crossing a surface is different, +- Tests on boundary conditions are not performed on DAGMC models as the logic is shared + with CSG-only models, +- Cases that should return an error are tested in the 'test_exceptions' unit test + from 'unit_tests/surface_source_write/test.py'. + +TODO: + +- Test with a lattice. + +""" + +import os +import shutil +from pathlib import Path + +import h5py +import numpy as np +import openmc +import openmc.lib +import pytest + +from tests.testing_harness import PyAPITestHarness +from tests.regression_tests import config + + +@pytest.fixture(scope="function") +def single_thread(monkeypatch): + """Set the number of OMP threads to 1 for the test.""" + monkeypatch.setenv("OMP_NUM_THREADS", "1") + + +@pytest.fixture(scope="function") +def two_threads(monkeypatch): + """Set the number of OMP threads to 2 for the test.""" + monkeypatch.setenv("OMP_NUM_THREADS", "2") + + +@pytest.fixture(scope="function") +def single_process(monkeypatch): + """Set the number of MPI process to 1 for the test.""" + monkeypatch.setitem(config, "mpi_np", "1") + + +@pytest.fixture(scope="module") +def model_1(): + """Cylindrical core contained in a first box which is contained in a larger box. + A lower universe is used to describe the interior of the first box which + contains the core and its surrounding space. + + """ + openmc.reset_auto_ids() + model = openmc.Model() + + # ============================================================================= + # Materials + # ============================================================================= + + fuel = openmc.Material() + fuel.add_nuclide("U234", 0.0004524) + fuel.add_nuclide("U235", 0.0506068) + fuel.add_nuclide("U238", 0.9487090) + fuel.add_nuclide("U236", 0.0002318) + fuel.add_nuclide("O16", 2.0) + fuel.set_density("g/cm3", 11.0) + + water = openmc.Material() + water.add_nuclide("H1", 2.0) + water.add_nuclide("O16", 1.0) + water.set_density("g/cm3", 1.0) + + # ============================================================================= + # Geometry + # ============================================================================= + + # ----------------------------------------------------------------------------- + # Cylindrical core + # ----------------------------------------------------------------------------- + + # Parameters + core_radius = 2.0 + core_height = 4.0 + + # Surfaces + core_cylinder = openmc.ZCylinder(r=core_radius) + core_lower_plane = openmc.ZPlane(-core_height / 2.0) + core_upper_plane = openmc.ZPlane(core_height / 2.0) + + # Region + core_region = -core_cylinder & +core_lower_plane & -core_upper_plane + + # Cells + core = openmc.Cell(fill=fuel, region=core_region) + outside_core_region = +core_cylinder | -core_lower_plane | +core_upper_plane + outside_core = openmc.Cell(fill=water, region=outside_core_region) + + # Universe + inside_box1_universe = openmc.Universe(cells=[core, outside_core]) + + # ----------------------------------------------------------------------------- + # Box 1 + # ----------------------------------------------------------------------------- + + # Parameters + box1_size = 6.0 + + # Surfaces + box1_rpp = openmc.model.RectangularParallelepiped( + -box1_size / 2.0, box1_size / 2.0, + -box1_size / 2.0, box1_size / 2.0, + -box1_size / 2.0, box1_size / 2.0, + ) + + # Cell + box1 = openmc.Cell(fill=inside_box1_universe, region=-box1_rpp) + + # ----------------------------------------------------------------------------- + # Box 2 + # ----------------------------------------------------------------------------- + + # Parameters + box2_size = 8 + + # Surfaces + box2_rpp = openmc.model.RectangularParallelepiped( + -box2_size / 2.0, box2_size / 2.0, + -box2_size / 2.0, box2_size / 2.0, + -box2_size / 2.0, box2_size / 2.0, + boundary_type="vacuum" + ) + + # Cell + box2 = openmc.Cell(fill=water, region=-box2_rpp & +box1_rpp) + + # Register geometry + model.geometry = openmc.Geometry([box1, box2]) + + # ============================================================================= + # Settings + # ============================================================================= + + model.settings = openmc.Settings() + model.settings.particles = 100 + model.settings.batches = 5 + model.settings.inactive = 1 + model.settings.seed = 1 + + bounds = [ + -core_radius, + -core_radius, + -core_height / 2.0, + core_radius, + core_radius, + core_height / 2.0, + ] + distribution = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True) + model.settings.source = openmc.IndependentSource(space=distribution) + + return model + + +@pytest.fixture +def model_2(): + """Cylindrical core contained in a box. + A lower universe is used to describe the interior of the box which + contains the core and its surrounding space. + + The box is defined with vacuum boundary conditions. + + """ + openmc.reset_auto_ids() + model = openmc.Model() + + # ============================================================================= + # Materials + # ============================================================================= + + fuel = openmc.Material() + fuel.add_nuclide("U234", 0.0004524) + fuel.add_nuclide("U235", 0.0506068) + fuel.add_nuclide("U238", 0.9487090) + fuel.add_nuclide("U236", 0.0002318) + fuel.add_nuclide("O16", 2.0) + fuel.set_density("g/cm3", 11.0) + + water = openmc.Material() + water.add_nuclide("H1", 2.0) + water.add_nuclide("O16", 1.0) + water.set_density("g/cm3", 1.0) + + # ============================================================================= + # Geometry + # ============================================================================= + + # ----------------------------------------------------------------------------- + # Cylindrical core + # ----------------------------------------------------------------------------- + + # Parameters + core_radius = 2.0 + core_height = 4.0 + + # Surfaces + core_cylinder = openmc.ZCylinder(r=core_radius) + core_lower_plane = openmc.ZPlane(-core_height / 2.0) + core_upper_plane = openmc.ZPlane(core_height / 2.0) + + # Region + core_region = -core_cylinder & +core_lower_plane & -core_upper_plane + + # Cells + core = openmc.Cell(fill=fuel, region=core_region) + outside_core_region = +core_cylinder | -core_lower_plane | +core_upper_plane + outside_core = openmc.Cell(fill=water, region=outside_core_region) + + # Universe + inside_box1_universe = openmc.Universe(cells=[core, outside_core]) + + # ----------------------------------------------------------------------------- + # Box 1 + # ----------------------------------------------------------------------------- + + # Parameters + box1_size = 6.0 + + # Surfaces + box1_rpp = openmc.model.RectangularParallelepiped( + -box1_size / 2.0, box1_size / 2.0, + -box1_size / 2.0, box1_size / 2.0, + -box1_size / 2.0, box1_size / 2.0, + boundary_type="vacuum" + ) + + # Cell + box1 = openmc.Cell(fill=inside_box1_universe, region=-box1_rpp) + + # Register geometry + model.geometry = openmc.Geometry([box1]) + + # ============================================================================= + # Settings + # ============================================================================= + + model.settings = openmc.Settings() + model.settings.particles = 100 + model.settings.batches = 5 + model.settings.inactive = 1 + model.settings.seed = 1 + + bounds = [ + -core_radius, + -core_radius, + -core_height / 2.0, + core_radius, + core_radius, + core_height / 2.0, + ] + distribution = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True) + model.settings.source = openmc.IndependentSource(space=distribution) + + return model + + +@pytest.fixture +def model_3(): + """Cylindrical core contained in a box. + A lower universe is used to describe the interior of the box which + contains the core and its surrounding space. + + The box is defined with reflective boundary conditions. + + """ + openmc.reset_auto_ids() + model = openmc.Model() + + # ============================================================================= + # Materials + # ============================================================================= + + fuel = openmc.Material() + fuel.add_nuclide("U234", 0.0004524) + fuel.add_nuclide("U235", 0.0506068) + fuel.add_nuclide("U238", 0.9487090) + fuel.add_nuclide("U236", 0.0002318) + fuel.add_nuclide("O16", 2.0) + fuel.set_density("g/cm3", 11.0) + + water = openmc.Material() + water.add_nuclide("H1", 2.0) + water.add_nuclide("O16", 1.0) + water.set_density("g/cm3", 1.0) + + # ============================================================================= + # Geometry + # ============================================================================= + + # ----------------------------------------------------------------------------- + # Cylindrical core + # ----------------------------------------------------------------------------- + + # Parameters + core_radius = 2.0 + core_height = 4.0 + + # Surfaces + core_cylinder = openmc.ZCylinder(r=core_radius) + core_lower_plane = openmc.ZPlane(-core_height / 2.0) + core_upper_plane = openmc.ZPlane(core_height / 2.0) + + # Region + core_region = -core_cylinder & +core_lower_plane & -core_upper_plane + + # Cells + core = openmc.Cell(fill=fuel, region=core_region) + outside_core_region = +core_cylinder | -core_lower_plane | +core_upper_plane + outside_core = openmc.Cell(fill=water, region=outside_core_region) + + # Universe + inside_box1_universe = openmc.Universe(cells=[core, outside_core]) + + # ----------------------------------------------------------------------------- + # Box 1 + # ----------------------------------------------------------------------------- + + # Parameters + box1_size = 6.0 + + # Surfaces + box1_rpp = openmc.model.RectangularParallelepiped( + -box1_size / 2.0, box1_size / 2.0, + -box1_size / 2.0, box1_size / 2.0, + -box1_size / 2.0, box1_size / 2.0, + boundary_type="reflective" + ) + + # Cell + box1 = openmc.Cell(fill=inside_box1_universe, region=-box1_rpp) + + # Register geometry + model.geometry = openmc.Geometry([box1]) + + # ============================================================================= + # Settings + # ============================================================================= + + model.settings = openmc.Settings() + model.settings.particles = 100 + model.settings.batches = 5 + model.settings.inactive = 1 + model.settings.seed = 1 + + bounds = [ + -core_radius, + -core_radius, + -core_height / 2.0, + core_radius, + core_radius, + core_height / 2.0, + ] + distribution = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True) + model.settings.source = openmc.IndependentSource(space=distribution) + + return model + + +@pytest.fixture +def model_4(): + """Cylindrical core contained in a box. + A lower universe is used to describe the interior of the box which + contains the core and its surrounding space. + + The box is defined with a pair of periodic boundary with reflective + boundaries. + + """ + openmc.reset_auto_ids() + model = openmc.Model() + + # ============================================================================= + # Materials + # ============================================================================= + + fuel = openmc.Material() + fuel.add_nuclide("U234", 0.0004524) + fuel.add_nuclide("U235", 0.0506068) + fuel.add_nuclide("U238", 0.9487090) + fuel.add_nuclide("U236", 0.0002318) + fuel.add_nuclide("O16", 2.0) + fuel.set_density("g/cm3", 11.0) + + water = openmc.Material() + water.add_nuclide("H1", 2.0) + water.add_nuclide("O16", 1.0) + water.set_density("g/cm3", 1.0) + + # ============================================================================= + # Geometry + # ============================================================================= + + # ----------------------------------------------------------------------------- + # Cylindrical core + # ----------------------------------------------------------------------------- + + # Parameters + core_radius = 2.0 + core_height = 4.0 + + # Surfaces + core_cylinder = openmc.ZCylinder(r=core_radius) + core_lower_plane = openmc.ZPlane(-core_height / 2.0) + core_upper_plane = openmc.ZPlane(core_height / 2.0) + + # Region + core_region = -core_cylinder & +core_lower_plane & -core_upper_plane + + # Cells + core = openmc.Cell(fill=fuel, region=core_region) + outside_core_region = +core_cylinder | -core_lower_plane | +core_upper_plane + outside_core = openmc.Cell(fill=water, region=outside_core_region) + + # Universe + inside_box1_universe = openmc.Universe(cells=[core, outside_core]) + + # ----------------------------------------------------------------------------- + # Box 1 + # ----------------------------------------------------------------------------- + + # Parameters + box1_size = 6.0 + + # Surfaces + box1_lower_plane = openmc.ZPlane(-box1_size / 2.0, boundary_type="periodic") + box1_upper_plane = openmc.ZPlane(box1_size / 2.0, boundary_type="periodic") + box1_left_plane = openmc.XPlane(-box1_size / 2.0, boundary_type="reflective") + box1_right_plane = openmc.XPlane(box1_size / 2.0, boundary_type="reflective") + box1_rear_plane = openmc.YPlane(-box1_size / 2.0, boundary_type="reflective") + box1_front_plane = openmc.YPlane(box1_size / 2.0, boundary_type="reflective") + + # Region + box1_region = ( + +box1_lower_plane + & -box1_upper_plane + & +box1_left_plane + & -box1_right_plane + & +box1_rear_plane + & -box1_front_plane + ) + + # Cell + box1 = openmc.Cell(fill=inside_box1_universe, region=box1_region) + + # Register geometry + model.geometry = openmc.Geometry([box1]) + + # ============================================================================= + # Settings + # ============================================================================= + + model.settings = openmc.Settings() + model.settings.particles = 100 + model.settings.batches = 5 + model.settings.inactive = 1 + model.settings.seed = 1 + + bounds = [ + -core_radius, + -core_radius, + -core_height / 2.0, + core_radius, + core_radius, + core_height / 2.0, + ] + distribution = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True) + model.settings.source = openmc.IndependentSource(space=distribution) + + return model + + +def return_surface_source_data(filepath): + """Read a surface source file and return a sorted array composed + of flatten arrays of source data for each surface source point. + + TODO: + + - use read_source_file from source.py instead. Or a dedicated function + to produce sorted list of source points for a given file. + + Parameters + ---------- + filepath : str + Path to the surface source file + + Returns + ------- + data : np.array + Sorted array composed of flatten arrays of source data for + each surface source point + + """ + data = [] + keys = [] + + # Read source file + with h5py.File(filepath, "r") as f: + for point in f["source_bank"]: + r = point["r"] + u = point["u"] + e = point["E"] + time = point["time"] + wgt = point["wgt"] + delayed_group = point["delayed_group"] + surf_id = point["surf_id"] + particle = point["particle"] + + key = ( + f"{r[0]:.10e} {r[1]:.10e} {r[2]:.10e} {u[0]:.10e} {u[1]:.10e} {u[2]:.10e}" + f"{e:.10e} {time:.10e} {wgt:.10e} {delayed_group} {surf_id} {particle}" + ) + + keys.append(key) + + values = [*r, *u, e, time, wgt, delayed_group, surf_id, particle] + assert len(values) == 12 + data.append(values) + + data = np.array(data) + keys = np.array(keys) + sorted_idx = np.argsort(keys) + + return data[sorted_idx] + + +class SurfaceSourceWriteTestHarness(PyAPITestHarness): + def __init__(self, statepoint_name, model=None, inputs_true=None, workdir=None): + super().__init__(statepoint_name, model, inputs_true) + self.workdir = workdir + + def _test_output_created(self): + """Make sure surface_source.h5 has also been created.""" + super()._test_output_created() + if self._model.settings.surf_source_write: + assert os.path.exists( + "surface_source.h5" + ), "Surface source file has not been created." + + def _compare_output(self): + """Compare surface_source.h5 files.""" + if self._model.settings.surf_source_write: + source_true = return_surface_source_data("surface_source_true.h5") + source_test = return_surface_source_data("surface_source.h5") + np.testing.assert_allclose(source_true, source_test, rtol=1e-07) + + def main(self): + """Accept commandline arguments and either run or update tests.""" + if config["build_inputs"]: + self.build_inputs() + elif config["update"]: + self.update_results() + else: + self.execute_test() + + def build_inputs(self): + """Build inputs.""" + base_dir = os.getcwd() + try: + os.chdir(self.workdir) + self._build_inputs() + finally: + os.chdir(base_dir) + + def execute_test(self): + """Build inputs, run OpenMC, and verify correct results.""" + base_dir = os.getcwd() + try: + os.chdir(self.workdir) + self._build_inputs() + inputs = self._get_inputs() + self._write_inputs(inputs) + self._compare_inputs() + self._run_openmc() + self._test_output_created() + self._compare_output() + results = self._get_results() + self._write_results(results) + self._compare_results() + finally: + self._cleanup() + os.chdir(base_dir) + + def update_results(self): + """Update results_true.dat and inputs_true.dat""" + base_dir = os.getcwd() + try: + os.chdir(self.workdir) + self._build_inputs() + inputs = self._get_inputs() + self._write_inputs(inputs) + self._overwrite_inputs() + self._run_openmc() + self._test_output_created() + results = self._get_results() + self._write_results(results) + self._overwrite_results() + finally: + self._cleanup() + os.chdir(base_dir) + + def _overwrite_results(self): + """Also add the 'surface_source.h5' file during overwriting.""" + super()._overwrite_results() + if os.path.exists("surface_source.h5"): + shutil.copyfile("surface_source.h5", "surface_source_true.h5") + + def _cleanup(self): + """Also remove the 'surface_source.h5' file while cleaning.""" + super()._cleanup() + fs = "surface_source.h5" + if os.path.exists(fs): + os.remove(fs) + + +@pytest.mark.skipif(config["event"] is True, reason="Results from history-based mode.") +@pytest.mark.parametrize( + "folder, model_name, parameter", + [ + ("case-01", "model_1", {"max_particles": 300}), + ("case-02", "model_1", {"max_particles": 300, "surface_ids": [8]}), + ( + "case-03", + "model_1", + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9]}, + ), + ( + "case-04", + "model_1", + {"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}, + ), + ("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", + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9]}, + ), + ( + "case-13", + "model_2", + {"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}, + ), + ( + "case-15", + "model_2", + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cellto": 3}, + ), + ( + "case-16", + "model_3", + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9]}, + ), + ( + "case-17", + "model_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}, + ), + ( + "case-19", + "model_3", + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cellto": 3}, + ), + ( + "case-20", + "model_4", + {"max_particles": 300, "surface_ids": [4]}, + ), + ( + "case-21", + "model_4", + {"max_particles": 300, "surface_ids": [4], "cell": 3}, + ), + ], +) +def test_surface_source_cell_history_based( + folder, model_name, parameter, single_thread, single_process, request +): + """Test on history-based results for CSG-only geometries.""" + assert os.environ["OMP_NUM_THREADS"] == "1" + assert config["mpi_np"] == "1" + model = request.getfixturevalue(model_name) + model.settings.surf_source_write = parameter + harness = SurfaceSourceWriteTestHarness( + "statepoint.5.h5", model=model, workdir=folder + ) + harness.main() + + +@pytest.mark.skipif(config["event"] is True, reason="Results from history-based mode.") +def test_consistency_low_realization_number(model_1, two_threads, single_process): + """The objective is to test that the results produced, in a case where + the number of potential realization (particle storage) is low + compared to the capacity of storage, are still consistent. + + This configuration ensures that the competition between threads does not + occur and that the content of the source file created can be compared. + + """ + assert os.environ["OMP_NUM_THREADS"] == "2" + assert config["mpi_np"] == "1" + model_1.settings.surf_source_write = { + "max_particles": 200, + "surface_ids": [1, 2, 3], + "cellfrom": 2, + } + harness = SurfaceSourceWriteTestHarness( + "statepoint.5.h5", model=model_1, workdir="case-a01" + ) + harness.main() + + +@pytest.mark.skipif(config["event"] is False, reason="Results from event-based mode.") +@pytest.mark.parametrize( + "folder, model_name, parameter", + [ + ( + "case-e01", + "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-e03", + "model_2", + {"max_particles": 300, "surface_ids": [4, 5, 6, 7, 8, 9], "cell": 3}, + ), + ], +) +def test_surface_source_cell_event_based( + folder, model_name, parameter, single_thread, single_process, request +): + """Test on event-based results for CSG-only geometries.""" + assert os.environ["OMP_NUM_THREADS"] == "1" + assert config["mpi_np"] == "1" + model = request.getfixturevalue(model_name) + model.settings.surf_source_write = parameter + harness = SurfaceSourceWriteTestHarness( + "statepoint.5.h5", model=model, workdir=folder + ) + harness.main() + + +@pytest.fixture(scope="module") +def model_dagmc_1(): + """Model based on the mesh file 'dagmc.h5m' available from + tests/regression_tests/dagmc/legacy. + + """ + openmc.reset_auto_ids() + model = openmc.Model() + + # ============================================================================= + # Materials + # ============================================================================= + + u235 = openmc.Material(name="no-void fuel") + u235.add_nuclide("U235", 1.0, "ao") + u235.set_density("g/cc", 11) + u235.id = 40 + + water = openmc.Material(name="water") + water.add_nuclide("H1", 2.0, "ao") + water.add_nuclide("O16", 1.0, "ao") + water.set_density("g/cc", 1.0) + water.add_s_alpha_beta("c_H_in_H2O") + water.id = 41 + + materials = openmc.Materials([u235, water]) + model.materials = materials + + # ============================================================================= + # Geometry + # ============================================================================= + + dagmc_univ = openmc.DAGMCUniverse(Path("../../dagmc/legacy/dagmc.h5m")) + model.geometry = openmc.Geometry(dagmc_univ) + + # ============================================================================= + # Settings + # ============================================================================= + + model.settings = openmc.Settings() + model.settings.particles = 100 + model.settings.batches = 5 + model.settings.inactive = 1 + model.settings.seed = 1 + + source_box = openmc.stats.Box([-4, -4, -20], [4, 4, 20], only_fissionable=True) + model.settings.source = openmc.IndependentSource(space=source_box) + + return model + + +@pytest.fixture(scope="module") +def model_dagmc_2(): + """Model based on the mesh file 'dagmc.h5m' available from + tests/regression_tests/dagmc/legacy. + + This model corresponds to the model_dagmc_1 contained in two boxes to introduce + multiple level of coordinates from CSG geometry. + + """ + openmc.reset_auto_ids() + model = openmc.Model() + + # ============================================================================= + # Materials + # ============================================================================= + + u235 = openmc.Material(name="no-void fuel") + u235.add_nuclide("U235", 1.0, "ao") + u235.set_density("g/cc", 11) + u235.id = 40 + + water = openmc.Material(name="water") + water.add_nuclide("H1", 2.0, "ao") + water.add_nuclide("O16", 1.0, "ao") + water.set_density("g/cc", 1.0) + water.add_s_alpha_beta("c_H_in_H2O") + water.id = 41 + + materials = openmc.Materials([u235, water]) + model.materials = materials + + # ============================================================================= + # Geometry + # ============================================================================= + + dagmc_univ = openmc.DAGMCUniverse(Path("../../dagmc/legacy/dagmc.h5m")) + + # ----------------------------------------------------------------------------- + # Box 1 + # ----------------------------------------------------------------------------- + + # Parameters + box1_size = 44 + + # Surfaces + box1_lower_plane = openmc.ZPlane(-box1_size / 2.0, surface_id=101) + box1_upper_plane = openmc.ZPlane(box1_size / 2.0, surface_id=102) + box1_left_plane = openmc.XPlane(-box1_size / 2.0, surface_id=103) + box1_right_plane = openmc.XPlane(box1_size / 2.0, surface_id=104) + box1_rear_plane = openmc.YPlane(-box1_size / 2.0, surface_id=105) + box1_front_plane = openmc.YPlane(box1_size / 2.0, surface_id=106) + + # Region + box1_region = ( + +box1_lower_plane + & -box1_upper_plane + & +box1_left_plane + & -box1_right_plane + & +box1_rear_plane + & -box1_front_plane + ) + + # Cell + box1 = openmc.Cell(fill=dagmc_univ, region=box1_region, cell_id=8) + + # ----------------------------------------------------------------------------- + # Box 2 + # ----------------------------------------------------------------------------- + + # Parameters + box2_size = 48 + + # Surfaces + box2_lower_plane = openmc.ZPlane( + -box2_size / 2.0, boundary_type="vacuum", surface_id=107 + ) + box2_upper_plane = openmc.ZPlane( + box2_size / 2.0, boundary_type="vacuum", surface_id=108 + ) + box2_left_plane = openmc.XPlane( + -box2_size / 2.0, boundary_type="vacuum", surface_id=109 + ) + box2_right_plane = openmc.XPlane( + box2_size / 2.0, boundary_type="vacuum", surface_id=110 + ) + box2_rear_plane = openmc.YPlane( + -box2_size / 2.0, boundary_type="vacuum", surface_id=111 + ) + box2_front_plane = openmc.YPlane( + box2_size / 2.0, boundary_type="vacuum", surface_id=112 + ) + + # Region + inside_box2 = ( + +box2_lower_plane + & -box2_upper_plane + & +box2_left_plane + & -box2_right_plane + & +box2_rear_plane + & -box2_front_plane + ) + outside_box1 = ( + -box1_lower_plane + | +box1_upper_plane + | -box1_left_plane + | +box1_right_plane + | -box1_rear_plane + | +box1_front_plane + ) + + box2_region = inside_box2 & outside_box1 + + # Cell + box2 = openmc.Cell(fill=water, region=box2_region, cell_id=9) + + # Register geometry + model.geometry = openmc.Geometry([box1, box2]) + + # ============================================================================= + # Settings + # ============================================================================= + + model.settings = openmc.Settings() + model.settings.particles = 100 + model.settings.batches = 5 + model.settings.inactive = 1 + model.settings.seed = 1 + + source_box = openmc.stats.Box([-4, -4, -20], [4, 4, 20], only_fissionable=True) + model.settings.source = openmc.IndependentSource(space=source_box) + + return model + + +@pytest.mark.skipif( + not openmc.lib._dagmc_enabled(), reason="DAGMC CAD geometry is not enabled." +) +@pytest.mark.skipif(config["event"] is True, reason="Results from history-based mode.") +@pytest.mark.parametrize( + "folder, model_name, parameter", + [ + ("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-d04", + "model_dagmc_1", + {"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-d07", + "model_dagmc_2", + { + "max_particles": 300, + "surface_ids": [101, 102, 103, 104, 105, 106], + "cell": 7, + }, + ), + ( + "case-d08", + "model_dagmc_2", + { + "max_particles": 300, + "surface_ids": [101, 102, 103, 104, 105, 106], + "cell": 8, + }, + ), + ], +) +def test_surface_source_cell_dagmc( + folder, model_name, parameter, single_thread, single_process, request +): + """Test on models with DAGMC geometries.""" + assert os.environ["OMP_NUM_THREADS"] == "1" + assert config["mpi_np"] == "1" + model = request.getfixturevalue(model_name) + model.settings.surf_source_write = parameter + harness = SurfaceSourceWriteTestHarness( + "statepoint.5.h5", model=model, workdir=folder + ) + harness.main() diff --git a/tests/unit_tests/test_surface_source_write.py b/tests/unit_tests/test_surface_source_write.py new file mode 100644 index 00000000000..47236307330 --- /dev/null +++ b/tests/unit_tests/test_surface_source_write.py @@ -0,0 +1,248 @@ +"""Test the 'surf_source_write' setting used to store particles that cross +surfaces in a file for a given simulation.""" + +from pathlib import Path + +import openmc +import openmc.lib +import pytest +import h5py +import numpy as np + + +@pytest.fixture(scope="module") +def geometry(): + """Simple hydrogen sphere geometry""" + openmc.reset_auto_ids() + material = openmc.Material(name="H1") + material.add_element("H", 1.0) + sphere = openmc.Sphere(r=1.0, boundary_type="vacuum") + cell = openmc.Cell(region=-sphere, fill=material) + return openmc.Geometry([cell]) + + +@pytest.mark.parametrize( + "parameter", + [ + {"max_particles": 200}, + {"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}, + ], +) +def test_xml_serialization(parameter, run_in_tmpdir): + """Check that the different use cases can be written and read in XML.""" + settings = openmc.Settings() + settings.surf_source_write = parameter + settings.export_to_xml() + + read_settings = openmc.Settings.from_xml() + assert read_settings.surf_source_write == parameter + + +ERROR_MSG_1 = ( + "A maximum number of particles needs to be specified " + "using the 'max_particles' parameter to store surface " + "source points." +) +ERROR_MSG_2 = "'cell', 'cellfrom' and 'cellto' cannot be used at the same time." + + +@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), + ], +) +def test_exceptions(parameter, error, run_in_tmpdir, geometry): + """Test parameters configuration that should return an error.""" + settings = openmc.Settings(run_mode="fixed source", batches=5, particles=100) + settings.surf_source_write = parameter + model = openmc.Model(geometry=geometry, settings=settings) + with pytest.raises(RuntimeError, match=error): + model.run() + + +@pytest.fixture(scope="module") +def model(): + """Simple hydrogen sphere divided in two hemispheres + by a z-plane to form 2 cells.""" + openmc.reset_auto_ids() + model = openmc.Model() + + # Material + material = openmc.Material(name="H1") + material.add_element("H", 1.0) + + # Geometry + radius = 1.0 + sphere = openmc.Sphere(r=radius, boundary_type="reflective") + plane = openmc.ZPlane(0.0) + cell_1 = openmc.Cell(region=-sphere & -plane, fill=material) + cell_2 = openmc.Cell(region=-sphere & +plane, fill=material) + root = openmc.Universe(cells=[cell_1, cell_2]) + model.geometry = openmc.Geometry(root) + + # Settings + model.settings = openmc.Settings() + model.settings.run_mode = "fixed source" + model.settings.particles = 100 + model.settings.batches = 3 + model.settings.seed = 1 + + bounds = [-radius, -radius, -radius, radius, radius, radius] + distribution = openmc.stats.Box(bounds[:3], bounds[3:]) + model.settings.source = openmc.IndependentSource(space=distribution) + + return model + + +@pytest.mark.parametrize( + "parameter", + [ + {"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): + """Test the direction of particles with the 'cellfrom' and 'cellto' parameters + on a simple model with only one surface of interest. + + Cell 2 is the upper hemisphere and surface 2 is the plane dividing the sphere + into two hemispheres. + + """ + model.settings.surf_source_write = parameter + model.run() + with h5py.File("surface_source.h5", "r") as f: + source = f["source_bank"] + + assert len(source) == 200 + + # We want to verify that the dot product of the surface's normal vector + # and the direction of the particle is either positive or negative + # depending on cellfrom or cellto. In this case, it is equivalent + # to just compare the z component of the direction of the particle. + for point in source: + if "cellto" in parameter.keys(): + assert point["u"]["z"] > 0.0 + elif "cellfrom" in parameter.keys(): + assert point["u"]["z"] < 0.0 + else: + assert False + + +@pytest.fixture +def model_dagmc(request): + """Model based on the mesh file 'dagmc.h5m' available from + tests/regression_tests/dagmc/legacy. + + """ + openmc.reset_auto_ids() + model = openmc.Model() + + # ============================================================================= + # Materials + # ============================================================================= + + u235 = openmc.Material(name="no-void fuel") + u235.add_nuclide("U235", 1.0, "ao") + u235.set_density("g/cc", 11) + u235.id = 40 + + water = openmc.Material(name="water") + water.add_nuclide("H1", 2.0, "ao") + water.add_nuclide("O16", 1.0, "ao") + water.set_density("g/cc", 1.0) + water.add_s_alpha_beta("c_H_in_H2O") + water.id = 41 + + model.materials = openmc.Materials([u235, water]) + + # ============================================================================= + # Geometry + # ============================================================================= + dagmc_path = Path(request.fspath).parent / "../regression_tests/dagmc/legacy/dagmc.h5m" + dagmc_univ = openmc.DAGMCUniverse(dagmc_path) + model.geometry = openmc.Geometry(dagmc_univ) + + # ============================================================================= + # Settings + # ============================================================================= + + model.settings = openmc.Settings() + model.settings.particles = 300 + model.settings.batches = 5 + model.settings.inactive = 1 + model.settings.seed = 1 + + source_box = openmc.stats.Box([-4, -4, -20], [4, 4, 20]) + model.settings.source = openmc.IndependentSource(space=source_box) + + return model + + +@pytest.mark.skipif( + not openmc.lib._dagmc_enabled(), reason="DAGMC CAD geometry is not enabled." +) +@pytest.mark.parametrize( + "parameter", + [ + {"max_particles": 200, "cellto": 1}, + {"max_particles": 200, "cellfrom": 1}, + ], +) +def test_particle_direction_dagmc(parameter, run_in_tmpdir, model_dagmc): + """Test the direction of particles with the 'cellfrom' and 'cellto' parameters + on a DAGMC model.""" + model_dagmc.settings.surf_source_write = parameter + model_dagmc.run() + + r = 7.0 + h = 20.0 + + with h5py.File("surface_source.h5", "r") as f: + source = f["source_bank"] + + assert len(source) == 200 + + for point in source: + + x, y, z = point["r"] + ux, uy, uz = point["u"] + + # If the point is on the upper or lower circle + if np.allclose(abs(z), h): + # If the point is also on the cylindrical surface + if np.allclose(np.sqrt(x**2 + y**2), r): + if "cellfrom" in parameter.keys(): + assert (uz * z > 0) or (ux * x + uy * y > 0) + elif "cellto" in parameter.keys(): + assert (uz * z < 0) or (ux * x + uy * y < 0) + else: + assert False + # If the point is not on the cylindrical surface + else: + if "cellfrom" in parameter.keys(): + assert uz * z > 0 + elif "cellto" in parameter.keys(): + assert uz * z < 0 + else: + assert False + # If the point is not on the upper or lower circle, + # meaning it is on the cylindrical surface + else: + if "cellfrom" in parameter.keys(): + assert ux * x + uy * y > 0 + elif "cellto" in parameter.keys(): + assert ux * x + uy * y < 0 + else: + assert False diff --git a/tests/unit_tests/test_tally_multiply_density.py b/tests/unit_tests/test_tally_multiply_density.py index 66ef41e0dac..552d76bd52f 100644 --- a/tests/unit_tests/test_tally_multiply_density.py +++ b/tests/unit_tests/test_tally_multiply_density.py @@ -3,7 +3,7 @@ import pytest -def test_micro_macro_compare(): +def test_micro_macro_compare(run_in_tmpdir): # Create simple sphere model with H1 and H2 mat = openmc.Material() mat.add_components({'H1': 1.0, 'H2': 1.0}) diff --git a/tests/unit_tests/weightwindows/test_ww_gen.py b/tests/unit_tests/weightwindows/test_ww_gen.py index 6fc3747b1da..555421461b7 100644 --- a/tests/unit_tests/weightwindows/test_ww_gen.py +++ b/tests/unit_tests/weightwindows/test_ww_gen.py @@ -101,7 +101,7 @@ def labels(params): @pytest.mark.parametrize("filters", test_cases, ids=labels) -def test_ww_gen(filters, model): +def test_ww_gen(filters, run_in_tmpdir, model): tally = openmc.Tally() tally.filters = list(filters) From 3bedd043d0026490412018685901c9409635713b Mon Sep 17 00:00:00 2001 From: lhchg Date: Wed, 19 Jun 2024 23:56:02 +0800 Subject: [PATCH 020/184] update math function unit test with catch2 (#2955) Co-authored-by: Paul Romano --- openmc/lib/math.py | 266 +-------------------- tests/cpp_unit_tests/CMakeLists.txt | 1 + tests/cpp_unit_tests/test_math.cpp | 357 ++++++++++++++++++++++++++++ tests/unit_tests/test_math.py | 247 ------------------- 4 files changed, 359 insertions(+), 512 deletions(-) create mode 100644 tests/cpp_unit_tests/test_math.cpp delete mode 100644 tests/unit_tests/test_math.py diff --git a/openmc/lib/math.py b/openmc/lib/math.py index 029f1c4c9ff..8c62f241624 100644 --- a/openmc/lib/math.py +++ b/openmc/lib/math.py @@ -1,5 +1,4 @@ -from ctypes import c_int, c_double, POINTER, c_uint64 -from random import getrandbits +from ctypes import c_int, c_double import numpy as np from numpy.ctypeslib import ndpointer @@ -7,137 +6,18 @@ from . import _dll -_dll.t_percentile.restype = c_double -_dll.t_percentile.argtypes = [c_double, c_int] - -_dll.calc_pn_c.restype = None -_dll.calc_pn_c.argtypes = [c_int, c_double, ndpointer(c_double)] - -_dll.evaluate_legendre.restype = c_double -_dll.evaluate_legendre.argtypes = [c_int, POINTER(c_double), c_double] - -_dll.calc_rn_c.restype = None -_dll.calc_rn_c.argtypes = [c_int, ndpointer(c_double), ndpointer(c_double)] - _dll.calc_zn.restype = None _dll.calc_zn.argtypes = [c_int, c_double, c_double, ndpointer(c_double)] _dll.calc_zn_rad.restype = None _dll.calc_zn_rad.argtypes = [c_int, c_double, ndpointer(c_double)] -_dll.rotate_angle_c.restype = None -_dll.rotate_angle_c.argtypes = [ndpointer(c_double), c_double, - POINTER(c_double), POINTER(c_uint64)] -_dll.maxwell_spectrum.restype = c_double -_dll.maxwell_spectrum.argtypes = [c_double, POINTER(c_uint64)] - -_dll.watt_spectrum.restype = c_double -_dll.watt_spectrum.argtypes = [c_double, c_double, POINTER(c_uint64)] - -_dll.broaden_wmp_polynomials.restype = None -_dll.broaden_wmp_polynomials.argtypes = [c_double, c_double, c_int, - ndpointer(c_double)] - -_dll.normal_variate.restype = c_double -_dll.normal_variate.argtypes = [c_double, c_double, POINTER(c_uint64)] - -def t_percentile(p, df): - """ Calculate the percentile of the Student's t distribution with a - specified probability level and number of degrees of freedom - - Parameters - ---------- - p : float - Probability level - df : int - Degrees of freedom - - Returns - ------- - float - Corresponding t-value - - """ - - return _dll.t_percentile(p, df) - - -def calc_pn(n, x): - """ Calculate the n-th order Legendre polynomial at the value of x. - - Parameters - ---------- - n : int - Legendre order - x : float - Independent variable to evaluate the Legendre at - - Returns - ------- - float - Corresponding Legendre polynomial result - - """ - - pnx = np.empty(n + 1, dtype=np.float64) - _dll.calc_pn_c(n, x, pnx) - return pnx - - -def evaluate_legendre(data, x): - """ Finds the value of f(x) given a set of Legendre coefficients - and the value of x. - - Parameters - ---------- - data : iterable of float - Legendre coefficients - x : float - Independent variable to evaluate the Legendre at - - Returns - ------- - float - Corresponding Legendre expansion result - - """ - - data_arr = np.array(data, dtype=np.float64) - return _dll.evaluate_legendre(len(data)-1, - data_arr.ctypes.data_as(POINTER(c_double)), x) - - -def calc_rn(n, uvw): - """ Calculate the n-th order real Spherical Harmonics for a given angle; - all Rn,m values are provided for all n (where -n <= m <= n). - - Parameters - ---------- - n : int - Harmonics order - uvw : iterable of float - Independent variable to evaluate the Legendre at - - Returns - ------- - numpy.ndarray - Corresponding real harmonics value - - """ - - num_nm = (n + 1) * (n + 1) - rn = np.empty(num_nm, dtype=np.float64) - uvw_arr = np.array(uvw, dtype=np.float64) - _dll.calc_rn_c(n, uvw_arr, rn) - return rn - def calc_zn(n, rho, phi): """ Calculate the n-th order modified Zernike polynomial moment for a given angle (rho, theta) location in the unit disk. The normalization of the polynomials is such that the integral of Z_pq*Z_pq over the unit disk is exactly pi - Parameters ---------- n : int @@ -146,12 +26,10 @@ def calc_zn(n, rho, phi): Radial location in the unit disk phi : float Theta (radians) location in the unit disk - Returns ------- numpy.ndarray Corresponding resulting list of coefficients - """ num_bins = ((n + 1) * (n + 2)) // 2 @@ -165,161 +43,19 @@ def calc_zn_rad(n, rho): moment with no azimuthal dependency (m=0) for a given radial location in the unit disk. The normalization of the polynomials is such that the integral of Z_pq*Z_pq over the unit disk is exactly pi. - Parameters ---------- n : int Maximum order rho : float Radial location in the unit disk - Returns ------- numpy.ndarray Corresponding resulting list of coefficients - """ num_bins = n // 2 + 1 zn_rad = np.zeros(num_bins, dtype=np.float64) _dll.calc_zn_rad(n, rho, zn_rad) return zn_rad - - -def rotate_angle(uvw0, mu, phi, prn_seed=None): - """ Rotates direction cosines through a polar angle whose cosine is - mu and through an azimuthal angle sampled uniformly. - - Parameters - ---------- - uvw0 : iterable of float - Original direction cosine - mu : float - Polar angle cosine to rotate - phi : float - Azimuthal angle; if None, one will be sampled uniformly - prn_seed : int - Pseudorandom number generator (PRNG) seed; if None, one will be - generated randomly. - - Returns - ------- - numpy.ndarray - Rotated direction cosine - - """ - - if prn_seed is None: - prn_seed = getrandbits(63) - - uvw0_arr = np.array(uvw0, dtype=np.float64) - if phi is None: - _dll.rotate_angle_c(uvw0_arr, mu, None, c_uint64(prn_seed)) - else: - _dll.rotate_angle_c(uvw0_arr, mu, c_double(phi), c_uint64(prn_seed)) - - uvw = uvw0_arr - - return uvw - - -def maxwell_spectrum(T, prn_seed=None): - """ Samples an energy from the Maxwell fission distribution based - on a direct sampling scheme. - - Parameters - ---------- - T : float - Spectrum parameter - prn_seed : int - Pseudorandom number generator (PRNG) seed; if None, one will be - generated randomly. - - Returns - ------- - float - Sampled outgoing energy - - """ - - if prn_seed is None: - prn_seed = getrandbits(63) - - return _dll.maxwell_spectrum(T, c_uint64(prn_seed)) - - -def watt_spectrum(a, b, prn_seed=None): - """ Samples an energy from the Watt energy-dependent fission spectrum. - - Parameters - ---------- - a : float - Spectrum parameter a - b : float - Spectrum parameter b - prn_seed : int - Pseudorandom number generator (PRNG) seed; if None, one will be - generated randomly. - - Returns - ------- - float - Sampled outgoing energy - - """ - - if prn_seed is None: - prn_seed = getrandbits(63) - - return _dll.watt_spectrum(a, b, c_uint64(prn_seed)) - - -def normal_variate(mean_value, std_dev, prn_seed=None): - """ Samples an energy from the Normal distribution. - - Parameters - ---------- - mean_value : float - Mean of the Normal distribution - std_dev : float - Standard deviation of the normal distribution - prn_seed : int - Pseudorandom number generator (PRNG) seed; if None, one will be - generated randomly. - - Returns - ------- - float - Sampled outgoing normally distributed value - - """ - - if prn_seed is None: - prn_seed = getrandbits(63) - - return _dll.normal_variate(mean_value, std_dev, c_uint64(prn_seed)) - - -def broaden_wmp_polynomials(E, dopp, n): - """ Doppler broadens the windowed multipole curvefit. The curvefit is a - polynomial of the form a/E + b/sqrt(E) + c + d sqrt(E) ... - - Parameters - ---------- - E : float - Energy to evaluate at - dopp : float - sqrt(atomic weight ratio / kT), with kT given in eV - n : int - Number of components to the polynomial - - Returns - ------- - numpy.ndarray - Resultant leading coefficients - - """ - - factors = np.zeros(n, dtype=np.float64) - _dll.broaden_wmp_polynomials(E, dopp, n, factors) - return factors diff --git a/tests/cpp_unit_tests/CMakeLists.txt b/tests/cpp_unit_tests/CMakeLists.txt index 24ec1b7a90e..f0f5f2853ad 100644 --- a/tests/cpp_unit_tests/CMakeLists.txt +++ b/tests/cpp_unit_tests/CMakeLists.txt @@ -3,6 +3,7 @@ set(TEST_NAMES test_file_utils test_tally test_interpolate + test_math # Add additional unit test files here ) diff --git a/tests/cpp_unit_tests/test_math.cpp b/tests/cpp_unit_tests/test_math.cpp new file mode 100644 index 00000000000..1ad7c4b7097 --- /dev/null +++ b/tests/cpp_unit_tests/test_math.cpp @@ -0,0 +1,357 @@ +#include +#include + +#include +#include +#include + +#include "openmc/math_functions.h" +#include "openmc/random_dist.h" +#include "openmc/random_lcg.h" +#include "openmc/wmp.h" + +TEST_CASE("Test t_percentile") +{ + // The reference solutions come from scipy.stats.t.ppf + std::vector> ref_ts { + {-15.894544844102773, -0.32491969623407446, 0.000000000000000, + 0.32491969623407446, 15.894544844102759}, + {-4.848732214442601, -0.2886751346880066, 0.000000000000000, + 0.2886751346880066, 4.848732214442598}, + {-2.756508521909475, -0.2671808657039658, 0.000000000000000, + 0.2671808657039658, 2.7565085219094745}}; + + // Permutations include 1 DoF, 2 DoF, and > 2 DoF + // We will test 5 p-values at 3-DoF values + std::vector test_ps {0.02, 0.4, 0.5, 0.6, 0.98}; + std::vector test_dfs {1, 2, 5}; + + for (int i = 0; i < test_dfs.size(); i++) { + int df = test_dfs[i]; + + std::vector test_ts; + + for (double p : test_ps) { + double test_t = openmc::t_percentile(p, df); + test_ts.push_back(test_t); + } + + // The 5 DoF approximation in openmc.lib.math.t_percentile is off by up to + // 8e-3 from the scipy solution, so test that one separately with looser + // tolerance + double tolerance = (df > 2) ? 1e-2 : 1e-6; + + REQUIRE_THAT( + ref_ts[i], Catch::Matchers::Approx(test_ts).epsilon(tolerance)); + } +} + +TEST_CASE("Test calc_pn") +{ + // The reference solutions come from scipy.special.eval_legendre + std::vector> ref_vals { + {1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1}, + {1, -0.5, -0.125, 0.4375, -0.289062, -0.0898438, 0.323242, -0.223145, + -0.0736389, 0.267899, -0.188229}, + {1, 0, -0.5, -0, 0.375, 0, -0.3125, -0, 0.273438, 0, -0.246094}, + {1, 0.5, -0.125, -0.4375, -0.289062, 0.0898438, 0.323242, 0.223145, + -0.0736389, -0.267899, -0.188229}, + {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}}; + + int max_order = 10; + std::vector test_xs = {-1.0, -0.5, 0.0, 0.5, 1.0}; + + std::vector> test_vals; + for (double x : test_xs) { + std::vector test_val(max_order + 1); + openmc::calc_pn_c(max_order, x, test_val.data()); + test_vals.push_back(test_val); + } + + for (int i = 0; i < ref_vals.size(); i++) { + REQUIRE_THAT(ref_vals[i], Catch::Matchers::Approx(test_vals[i])); + } +} + +TEST_CASE("Test evaluate_legendre") +{ + // The reference solutions come from numpy.polynomial.legendre.legval + std::vector ref_vals { + 5.5, -0.45597649, -1.35351562, -2.7730999, 60.5}; + + int max_order = 10; + std::vector test_xs = {-1.0, -0.5, 0.0, 0.5, 1.0}; + + // Set the coefficients back to 1s for the test values since + // evaluate legendre incorporates the (2l+1)/2 term on its own + std::vector test_coeffs(max_order + 1, 1.0); + + std::vector test_vals; + for (double x : test_xs) { + test_vals.push_back( + openmc::evaluate_legendre(test_coeffs.size() - 1, test_coeffs.data(), x)); + } + + REQUIRE_THAT(ref_vals, Catch::Matchers::Approx(test_vals)); +} + +TEST_CASE("Test calc_rn") +{ + std::vector ref_vals {1.000000000000000, -0.019833838076210, + 0.980066577841242, -0.197676811654084, 0.006790834062088, + -0.033668438114859, 0.940795745502164, -0.335561350977312, + 0.033500236162691, -0.001831975566765, 0.014882082223994, + -0.046185860057145, 0.883359726009014, -0.460318044571973, + 0.073415616482180, -0.005922278973373, 0.000448625292461, + -0.004750335422039, 0.025089695062177, -0.057224052171859, + 0.809468042300133, -0.570331780454957, 0.123771351522967, + -0.015356543011155, 0.001061098599927, -0.000104097571795, + 0.001319047965347, -0.009263463267120, 0.037043163155191, + -0.066518621473934, 0.721310852552881, -0.662967447756079, + 0.182739660926192, -0.029946258412359, 0.003119841820746, + -0.000190549327031, 0.000023320052630, -0.000338370521658, + 0.002878809439524, -0.015562587450914, 0.050271226423217, + -0.073829294593737, 0.621486505922182, -0.735830327235834, + 0.247995745731425, -0.050309614442385, 0.006809024629381, + -0.000619383085285, 0.000034086826414, -0.000005093626712, + 0.000082405610567, -0.000809532012556, 0.005358034016708, + -0.023740240859138, 0.064242405926477, -0.078969918083157, + 0.512915839160049, -0.787065093668736, 0.316917738015632, + -0.076745744765114, 0.012672942183651, -0.001481838409317, + 0.000120451946983, -0.000006047366709, 0.000001091052697, + -0.000019334294214, 0.000213051604838, -0.001640234119608, + 0.008982263900105, -0.033788039035668, 0.078388909900756, + -0.081820779415058, 0.398746190829636, -0.815478614863816, + 0.386704633068855, -0.109227544713261, 0.021245051959237, + -0.003002428416676, 0.000311416667310, -0.000022954482885, + 0.000001059646310, -0.000000230023931, 0.000004408854505, + -0.000053457925526, 0.000464152759861, -0.002976305522860, + 0.013958017448970, -0.045594791382625, 0.092128969315914, + -0.082334538374971, 0.282248459574595, -0.820599067736528, + 0.454486474163594, -0.147395565311743, 0.033013815809602, + -0.005448090715661, 0.000678450207914, -0.000063467485444, + 0.000004281943868, -0.000000182535754, 0.000000047847775, + -0.000000982664801, 0.000012933320414, -0.000124076425457, + 0.000901739739837, -0.004982323311961, 0.020457776068931, + -0.058948376674391, 0.104888993733747, -0.080538298991650, + 0.166710763818175, -0.802696588503912, 0.517433650833039, + -0.190564076304612, 0.048387190622376, -0.009120081648146, + 0.001318069323039, -0.000147308722683, 0.000012561029621, + -0.000000779794781, 0.000000030722703}; + + int max_order = 10; + + double azi = 0.1; // Longitude + double pol = 0.2; // Latitude + double mu = std::cos(pol); + + std::vector test_uvw {std::sin(pol) * std::cos(azi), + std::sin(pol) * std::sin(azi), std::cos(pol)}; + + std::vector test_vals((max_order + 1) * (max_order + 1), 0); + openmc::calc_rn_c(max_order, test_uvw.data(), test_vals.data()); + + REQUIRE_THAT(ref_vals, Catch::Matchers::Approx(test_vals)); +} + +TEST_CASE("Test calc_zn") +{ + std::vector ref_vals {1.00000000e+00, 2.39712769e-01, 4.38791281e-01, + 2.10367746e-01, -5.00000000e-01, 1.35075576e-01, 1.24686873e-01, + -2.99640962e-01, -5.48489101e-01, 8.84215021e-03, 5.68310892e-02, + -4.20735492e-01, -1.25000000e-01, -2.70151153e-01, -2.60091773e-02, + 1.87022545e-02, -3.42888902e-01, 1.49820481e-01, 2.74244551e-01, + -2.43159131e-02, -2.50357380e-02, 2.20500013e-03, -1.98908812e-01, + 4.07587508e-01, 4.37500000e-01, 2.61708929e-01, 9.10321205e-02, + -1.54686328e-02, -2.74049397e-03, -7.94845816e-02, 4.75368705e-01, + 7.11647284e-02, 1.30266162e-01, 3.37106977e-02, 1.06401886e-01, + -7.31606787e-03, -2.95625975e-03, -1.10250006e-02, 3.55194307e-01, + -1.44627826e-01, -2.89062500e-01, -9.28644588e-02, -1.62557358e-01, + 7.73431638e-02, -2.55329539e-03, -1.90923851e-03, 1.57578403e-02, + 1.72995854e-01, -3.66267690e-01, -1.81657333e-01, -3.32521518e-01, + -2.59738162e-02, -2.31580576e-01, 4.20673902e-02, -4.11710546e-04, + -9.36449487e-04, 1.92156884e-02, 2.82515641e-02, -3.90713738e-01, + -1.69280296e-01, -8.98437500e-02, -1.08693628e-01, 1.78813094e-01, + -1.98191857e-01, 1.65964201e-02, 2.77013853e-04}; + + int n = 10; + double rho = 0.5; + double phi = 0.5; + + int nums = ((n + 1) * (n + 2)) / 2; + + std::vector test_vals(nums, 0); + openmc::calc_zn(n, rho, phi, test_vals.data()); + + REQUIRE_THAT(ref_vals, Catch::Matchers::Approx(test_vals)); +} + +TEST_CASE("Test calc_zn_rad") +{ + std::vector ref_vals {1.00000000e+00, -5.00000000e-01, + -1.25000000e-01, 4.37500000e-01, -2.89062500e-01, -8.98437500e-02}; + + int n = 10; + double rho = 0.5; + + int nums = n / 2 + 1; + std::vector test_vals(nums, 0); + openmc::calc_zn_rad(n, rho, test_vals.data()); + + REQUIRE_THAT(ref_vals, Catch::Matchers::Approx(test_vals)); +} + +TEST_CASE("Test rotate_angle") +{ + std::vector uvw0 {1.0, 0.0, 0.0}; + double phi = 0.0; + + uint64_t prn_seed = 1; + openmc::prn(&prn_seed); + + SECTION("Test rotate_angle mu is 0") + { + std::vector ref_uvw {0.0, 0.0, -1.0}; + + double mu = 0.0; + + std::vector test_uvw(uvw0); + openmc::rotate_angle_c(test_uvw.data(), mu, &phi, &prn_seed); + + REQUIRE_THAT(ref_uvw, Catch::Matchers::Approx(test_uvw)); + } + + SECTION("Test rotate_angle mu is 1") + { + std::vector ref_uvw = {1.0, 0.0, 0.0}; + + double mu = 1.0; + + std::vector test_uvw(uvw0); + openmc::rotate_angle_c(test_uvw.data(), mu, &phi, &prn_seed); + + REQUIRE_THAT(ref_uvw, Catch::Matchers::Approx(test_uvw)); + } + + // Now to test phi is None + SECTION("Test rotate_angle no phi") + { + // When seed = 1, phi will be sampled as 1.9116495709698769 + // The resultant reference is from hand-calculations given the above + std::vector ref_uvw = { + 0.9, -0.422746750548505, 0.10623175090659095}; + + double mu = 0.9; + prn_seed = 1; + + std::vector test_uvw(uvw0); + openmc::rotate_angle_c(test_uvw.data(), mu, NULL, &prn_seed); + + REQUIRE_THAT(ref_uvw, Catch::Matchers::Approx(test_uvw)); + } +} + +TEST_CASE("Test maxwell_spectrum") +{ + double ref_val = 0.27767406743161277; + + double T = 0.5; + uint64_t prn_seed = 1; + + double test_val = openmc::maxwell_spectrum(T, &prn_seed); + + REQUIRE(ref_val == test_val); +} + +TEST_CASE("Test watt_spectrum") +{ + double ref_val = 0.30957476387766697; + + double a = 0.5; + double b = 0.75; + uint64_t prn_seed = 1; + + double test_val = openmc::watt_spectrum(a, b, &prn_seed); + + REQUIRE(ref_val == test_val); +} + +TEST_CASE("Test normal_variate") +{ + + // Generate a series of normally distributed random numbers and test + // whether their mean and standard deviation are close to the expected value + SECTION("Test with non-zero standard deviation") + { + uint64_t seed = 1; + + double mean = 0.0; + double standard_deviation = 1.0; + + int num_samples = 10000; + double sum = 0.0; + double sum_squared_difference = 0.0; + + for (int i = 0; i < num_samples; ++i) { + double sample = openmc::normal_variate(mean, standard_deviation, &seed); + sum += sample; + sum_squared_difference += (sample - mean) * (sample - mean); + } + + double actual_mean = sum / num_samples; + double actual_standard_deviation = + std::sqrt(sum_squared_difference / num_samples); + + REQUIRE_THAT(mean, Catch::Matchers::WithinAbs(actual_mean, 0.1)); + REQUIRE_THAT(standard_deviation, + Catch::Matchers::WithinAbs(actual_standard_deviation, 0.1)); + } + + // When the standard deviation is zero + // the generated random number should always be equal to the mean + SECTION("Test with zero standard deviation") + { + uint64_t seed = 1; + double mean = 5.0; + double standard_deviation = 0.0; + + for (int i = 0; i < 10; ++i) { + double sample = openmc::normal_variate(mean, standard_deviation, &seed); + REQUIRE(sample == mean); + } + } +} + +TEST_CASE("Test broaden_wmp_polynomials") +{ + double test_E = 0.5; + int n = 6; + + // Two branches of the code to worry about, beta > 6 and otherwise + // beta = sqrtE * dopp + SECTION("Test broaden_wmp_polynomials beta > 6") + { + std::vector ref_val { + 2., 1.41421356, 1.0001, 0.70731891, 0.50030001, 0.353907}; + + double test_dopp = 100.0; // approximately U235 at room temperature + + std::vector test_val(n, 0); + openmc::broaden_wmp_polynomials(test_E, test_dopp, n, test_val.data()); + + REQUIRE_THAT(ref_val, Catch::Matchers::Approx(test_val)); + } + + SECTION("Test broaden_wmp_polynomials beta < 6") + { + std::vector ref_val = { + 1.99999885, 1.41421356, 1.04, 0.79195959, 0.6224, 0.50346003}; + + double test_dopp = 5.0; + + std::vector test_val(n, 0); + openmc::broaden_wmp_polynomials(test_E, test_dopp, n, test_val.data()); + + REQUIRE_THAT(ref_val, Catch::Matchers::Approx(test_val)); + } +} diff --git a/tests/unit_tests/test_math.py b/tests/unit_tests/test_math.py deleted file mode 100644 index c50057fcd6f..00000000000 --- a/tests/unit_tests/test_math.py +++ /dev/null @@ -1,247 +0,0 @@ -import numpy as np -import pytest -import scipy as sp -from scipy.stats import shapiro - -import openmc -import openmc.lib - - -def test_t_percentile(): - # Permutations include 1 DoF, 2 DoF, and > 2 DoF - # We will test 5 p-values at 3-DoF values - test_ps = [0.02, 0.4, 0.5, 0.6, 0.98] - test_dfs = [1, 2, 5] - - # The reference solutions come from Scipy - ref_ts = [[sp.stats.t.ppf(p, df) for p in test_ps] for df in test_dfs] - - test_ts = [[openmc.lib.math.t_percentile(p, df) for p in test_ps] - for df in test_dfs] - - # The 5 DoF approximation in openmc.lib.math.t_percentile is off by up to - # 8e-3 from the scipy solution, so test that one separately with looser - # tolerance - assert np.allclose(ref_ts[:-1], test_ts[:-1]) - assert np.allclose(ref_ts[-1], test_ts[-1], atol=1e-2) - - -def test_calc_pn(): - max_order = 10 - test_xs = np.linspace(-1., 1., num=5, endpoint=True) - - # Reference solutions from scipy - ref_vals = np.array([sp.special.eval_legendre(n, test_xs) - for n in range(0, max_order + 1)]) - - test_vals = [] - for x in test_xs: - test_vals.append(openmc.lib.math.calc_pn(max_order, x).tolist()) - - test_vals = np.swapaxes(np.array(test_vals), 0, 1) - - assert np.allclose(ref_vals, test_vals) - - -def test_evaluate_legendre(): - max_order = 10 - # Coefficients are set to 1, but will incorporate the (2l+1)/2 norm factor - # for the reference solution - test_coeffs = [0.5 * (2. * l + 1.) for l in range(max_order + 1)] - test_xs = np.linspace(-1., 1., num=5, endpoint=True) - - ref_vals = np.polynomial.legendre.legval(test_xs, test_coeffs) - - # Set the coefficients back to 1s for the test values since - # evaluate legendre incorporates the (2l+1)/2 term on its own - test_coeffs = [1. for l in range(max_order + 1)] - - test_vals = np.array([openmc.lib.math.evaluate_legendre(test_coeffs, x) - for x in test_xs]) - - assert np.allclose(ref_vals, test_vals) - - -def test_calc_rn(): - max_order = 10 - test_ns = np.array([i for i in range(0, max_order + 1)]) - azi = 0.1 # Longitude - pol = 0.2 # Latitude - test_uvw = np.array([np.sin(pol) * np.cos(azi), - np.sin(pol) * np.sin(azi), - np.cos(pol)]) - - # Reference solutions from the equations - ref_vals = [] - - def coeff(n, m): - return np.sqrt((2. * n + 1) * sp.special.factorial(n - m) / - (sp.special.factorial(n + m))) - - def pnm_bar(n, m, mu): - val = coeff(n, m) - if m != 0: - val *= np.sqrt(2.) - val *= sp.special.lpmv([m], [n], [mu]) - return val[0] - - ref_vals = [] - for n in test_ns: - for m in range(-n, n + 1): - if m < 0: - ylm = pnm_bar(n, np.abs(m), np.cos(pol)) * \ - np.sin(np.abs(m) * azi) - else: - ylm = pnm_bar(n, m, np.cos(pol)) * np.cos(m * azi) - - # Un-normalize for comparison - ylm /= np.sqrt(2. * n + 1.) - ref_vals.append(ylm) - - test_vals = [] - test_vals = openmc.lib.math.calc_rn(max_order, test_uvw) - - assert np.allclose(ref_vals, test_vals) - - -def test_calc_zn(): - n = 10 - rho = 0.5 - phi = 0.5 - - # Reference solution from running the C++ implementation - ref_vals = np.array([ - 1.00000000e+00, 2.39712769e-01, 4.38791281e-01, - 2.10367746e-01, -5.00000000e-01, 1.35075576e-01, - 1.24686873e-01, -2.99640962e-01, -5.48489101e-01, - 8.84215021e-03, 5.68310892e-02, -4.20735492e-01, - -1.25000000e-01, -2.70151153e-01, -2.60091773e-02, - 1.87022545e-02, -3.42888902e-01, 1.49820481e-01, - 2.74244551e-01, -2.43159131e-02, -2.50357380e-02, - 2.20500013e-03, -1.98908812e-01, 4.07587508e-01, - 4.37500000e-01, 2.61708929e-01, 9.10321205e-02, - -1.54686328e-02, -2.74049397e-03, -7.94845816e-02, - 4.75368705e-01, 7.11647284e-02, 1.30266162e-01, - 3.37106977e-02, 1.06401886e-01, -7.31606787e-03, - -2.95625975e-03, -1.10250006e-02, 3.55194307e-01, - -1.44627826e-01, -2.89062500e-01, -9.28644588e-02, - -1.62557358e-01, 7.73431638e-02, -2.55329539e-03, - -1.90923851e-03, 1.57578403e-02, 1.72995854e-01, - -3.66267690e-01, -1.81657333e-01, -3.32521518e-01, - -2.59738162e-02, -2.31580576e-01, 4.20673902e-02, - -4.11710546e-04, -9.36449487e-04, 1.92156884e-02, - 2.82515641e-02, -3.90713738e-01, -1.69280296e-01, - -8.98437500e-02, -1.08693628e-01, 1.78813094e-01, - -1.98191857e-01, 1.65964201e-02, 2.77013853e-04]) - - test_vals = openmc.lib.math.calc_zn(n, rho, phi) - - assert np.allclose(ref_vals, test_vals) - - -def test_calc_zn_rad(): - n = 10 - rho = 0.5 - - # Reference solution from running the C++ implementation - ref_vals = np.array([ - 1.00000000e+00, -5.00000000e-01, -1.25000000e-01, - 4.37500000e-01, -2.89062500e-01,-8.98437500e-02]) - - test_vals = openmc.lib.math.calc_zn_rad(n, rho) - - assert np.allclose(ref_vals, test_vals) - - -def test_rotate_angle(): - uvw0 = np.array([1., 0., 0.]) - phi = 0. - mu = 0. - - # reference: mu of 0 pulls the vector the bottom, so: - ref_uvw = np.array([0., 0., -1.]) - - test_uvw = openmc.lib.math.rotate_angle(uvw0, mu, phi) - - assert np.array_equal(ref_uvw, test_uvw) - - # Repeat for mu = 1 (no change) - mu = 1. - ref_uvw = np.array([1., 0., 0.]) - - test_uvw = openmc.lib.math.rotate_angle(uvw0, mu, phi) - - assert np.array_equal(ref_uvw, test_uvw) - - # Now to test phi is None - mu = 0.9 - phi = None - prn_seed = 1 - - # When seed = 1, phi will be sampled as 1.9116495709698769 - # The resultant reference is from hand-calculations given the above - ref_uvw = [0.9, -0.422746750548505, 0.10623175090659095] - test_uvw = openmc.lib.math.rotate_angle(uvw0, mu, phi, prn_seed) - - assert np.allclose(ref_uvw, test_uvw) - - -def test_maxwell_spectrum(): - prn_seed = 1 - T = 0.5 - ref_val = 0.27767406743161277 - test_val = openmc.lib.math.maxwell_spectrum(T, prn_seed) - - assert ref_val == test_val - - -def test_watt_spectrum(): - prn_seed = 1 - a = 0.5 - b = 0.75 - ref_val = 0.30957476387766697 - test_val = openmc.lib.math.watt_spectrum(a, b, prn_seed) - - assert ref_val == test_val - - -def test_normal_dist(): - # When standard deviation is zero, sampled value should be mean - prn_seed = 1 - mean = 14.08 - stdev = 0.0 - ref_val = 14.08 - test_val = openmc.lib.math.normal_variate(mean, stdev, prn_seed) - assert ref_val == pytest.approx(test_val) - - # Use Shapiro-Wilk test to ensure normality of sampled vairates - stdev = 1.0 - samples = [] - num_samples = 10000 - for _ in range(num_samples): - # sample the normal distribution from openmc - samples.append(openmc.lib.math.normal_variate(mean, stdev, prn_seed)) - prn_seed += 1 - stat, p = shapiro(samples) - assert p > 0.05 - - -def test_broaden_wmp_polynomials(): - # Two branches of the code to worry about, beta > 6 and otherwise - # beta = sqrtE * dopp - # First lets do beta > 6 - test_E = 0.5 - test_dopp = 100. # approximately U235 at room temperature - n = 6 - - ref_val = [2., 1.41421356, 1.0001, 0.70731891, 0.50030001, 0.353907] - test_val = openmc.lib.math.broaden_wmp_polynomials(test_E, test_dopp, n) - - assert np.allclose(ref_val, test_val) - - # now beta < 6 - test_dopp = 5. - ref_val = [1.99999885, 1.41421356, 1.04, 0.79195959, 0.6224, 0.50346003] - test_val = openmc.lib.math.broaden_wmp_polynomials(test_E, test_dopp, n) - - assert np.allclose(ref_val, test_val) From 3dff03f8249223751a34f701cd89e7959ba57fe8 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 19 Jun 2024 15:47:08 -0500 Subject: [PATCH 021/184] Resolve warnings related to numpy 2.0 (#3044) --- openmc/mgxs_library.py | 10 +++++----- openmc/statepoint.py | 2 +- tests/regression_tests/mg_temperature/build_2g.py | 5 +++-- tests/unit_tests/test_stats.py | 9 --------- 4 files changed, 9 insertions(+), 17 deletions(-) diff --git a/openmc/mgxs_library.py b/openmc/mgxs_library.py index 3381c3abb6e..b840563cffc 100644 --- a/openmc/mgxs_library.py +++ b/openmc/mgxs_library.py @@ -1862,7 +1862,7 @@ def convert_scatter_format(self, target_format, target_order=None): table_fine[..., imu] += ((l + 0.5) * eval_legendre(l, mu_fine[imu]) * orig_data[..., l]) - new_data[..., h_bin] = integrate(table_fine, mu_fine) + new_data[..., h_bin] = integrate(table_fine, x=mu_fine) elif self.scatter_format == SCATTER_TABULAR: # Calculate the mu points of the current data @@ -1876,7 +1876,7 @@ def convert_scatter_format(self, target_format, target_order=None): for l in range(xsdata.num_orders): y = (interp1d(mu_self, orig_data)(mu_fine) * eval_legendre(l, mu_fine)) - new_data[..., l] = integrate(y, mu_fine) + new_data[..., l] = integrate(y, x=mu_fine) elif target_format == SCATTER_TABULAR: # Simply use an interpolating function to get the new data @@ -1895,7 +1895,7 @@ def convert_scatter_format(self, target_format, target_order=None): interp = interp1d(mu_self, orig_data) for h_bin in range(xsdata.num_orders): mu_fine = np.linspace(mu[h_bin], mu[h_bin + 1], _NMU) - new_data[..., h_bin] = integrate(interp(mu_fine), mu_fine) + new_data[..., h_bin] = integrate(interp(mu_fine), x=mu_fine) elif self.scatter_format == SCATTER_HISTOGRAM: # The histogram format does not have enough information to @@ -1921,7 +1921,7 @@ def convert_scatter_format(self, target_format, target_order=None): mu_fine = np.linspace(-1, 1, _NMU) for l in range(xsdata.num_orders): y = interp(mu_fine) * norm * eval_legendre(l, mu_fine) - new_data[..., l] = integrate(y, mu_fine) + new_data[..., l] = integrate(y, x=mu_fine) elif target_format == SCATTER_TABULAR: # Simply use an interpolating function to get the new data @@ -1940,7 +1940,7 @@ def convert_scatter_format(self, target_format, target_order=None): for h_bin in range(xsdata.num_orders): mu_fine = np.linspace(mu[h_bin], mu[h_bin + 1], _NMU) new_data[..., h_bin] = \ - norm * integrate(interp(mu_fine), mu_fine) + norm * integrate(interp(mu_fine), x=mu_fine) # Remove small values resulting from numerical precision issues new_data[..., np.abs(new_data) < 1.E-10] = 0. diff --git a/openmc/statepoint.py b/openmc/statepoint.py index f6df01bd4f1..13aac0caf7e 100644 --- a/openmc/statepoint.py +++ b/openmc/statepoint.py @@ -235,7 +235,7 @@ def global_tallies(self): if self._global_tallies is None: data = self._f['global_tallies'][()] gt = np.zeros(data.shape[0], dtype=[ - ('name', 'a14'), ('sum', 'f8'), ('sum_sq', 'f8'), + ('name', 'S14'), ('sum', 'f8'), ('sum_sq', 'f8'), ('mean', 'f8'), ('std_dev', 'f8')]) gt['name'] = ['k-collision', 'k-absorption', 'k-tracklength', 'leakage'] diff --git a/tests/regression_tests/mg_temperature/build_2g.py b/tests/regression_tests/mg_temperature/build_2g.py index f236031a727..1fb7234499b 100644 --- a/tests/regression_tests/mg_temperature/build_2g.py +++ b/tests/regression_tests/mg_temperature/build_2g.py @@ -289,8 +289,9 @@ def build_inf_model(xsnames, xslibname, temperature, tempmethod='nearest'): settings_file.output = {'summary': False} # Create an initial uniform spatial source distribution over fissionable zones bounds = [-INF, -INF, -INF, INF, INF, INF] - uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True) + uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:]) settings_file.temperature = {'method': tempmethod} - settings_file.source = openmc.IndependentSource(space=uniform_dist) + settings_file.source = openmc.IndependentSource( + space=uniform_dist, constraints={'fissionable': True}) model.settings = settings_file model.export_to_model_xml() diff --git a/tests/unit_tests/test_stats.py b/tests/unit_tests/test_stats.py index f5b467d0380..5451c0a2cac 100644 --- a/tests/unit_tests/test_stats.py +++ b/tests/unit_tests/test_stats.py @@ -376,15 +376,6 @@ def test_box(): d = openmc.stats.Box.from_xml_element(elem) assert d.lower_left == pytest.approx(lower_left) assert d.upper_right == pytest.approx(upper_right) - assert not d.only_fissionable - - # only fissionable parameter - d2 = openmc.stats.Box(lower_left, upper_right, True) - assert d2.only_fissionable - elem = d2.to_xml_element() - assert elem.attrib['type'] == 'fission' - d = openmc.stats.Spatial.from_xml_element(elem) - assert isinstance(d, openmc.stats.Box) def test_point(): From 390005e8411f689f8529c73e35709ac581b42e44 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 20 Jun 2024 09:26:53 -0500 Subject: [PATCH 022/184] Make sure time boundary doesn't prevent secondary particle creation (#3043) --- src/simulation.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/simulation.cpp b/src/simulation.cpp index e218cdb2928..2d5d75aa477 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -762,13 +762,15 @@ void transport_history_based_single_particle(Particle& p) { while (p.alive()) { p.event_calculate_xs(); - if (!p.alive()) - break; - p.event_advance(); - if (p.collision_distance() > p.boundary().distance) { - p.event_cross_surface(); - } else { - p.event_collide(); + if (p.alive()) { + p.event_advance(); + } + if (p.alive()) { + if (p.collision_distance() > p.boundary().distance) { + p.event_cross_surface(); + } else if (p.alive()) { + p.event_collide(); + } } p.event_revive_from_secondary(); } From 78ee851990bd63608324b630d960ef007a5c2a3f Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 20 Jun 2024 09:27:49 -0500 Subject: [PATCH 023/184] Determine whether nuclides are fissionable in volume calc mode (#3047) Co-authored-by: Patrick Shriwise --- src/nuclide.cpp | 13 +++++++++++++ tests/unit_tests/test_lib.py | 8 +++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/nuclide.cpp b/src/nuclide.cpp index 91adc077799..04ec110738c 100644 --- a/src/nuclide.cpp +++ b/src/nuclide.cpp @@ -63,6 +63,19 @@ Nuclide::Nuclide(hid_t group, const vector& temperature) read_attribute(group, "atomic_weight_ratio", awr_); if (settings::run_mode == RunMode::VOLUME) { + // Determine whether nuclide is fissionable and then exit + int mt; + hid_t rxs_group = open_group(group, "reactions"); + for (auto name : group_names(rxs_group)) { + if (starts_with(name, "reaction_")) { + hid_t rx_group = open_group(rxs_group, name.c_str()); + read_attribute(rx_group, "mt", mt); + if (is_fission(mt)) { + fissionable_ = true; + break; + } + } + } return; } diff --git a/tests/unit_tests/test_lib.py b/tests/unit_tests/test_lib.py index 009a9096831..e3c7ce3b70d 100644 --- a/tests/unit_tests/test_lib.py +++ b/tests/unit_tests/test_lib.py @@ -963,7 +963,8 @@ def test_sample_external_source(run_in_tmpdir, mpi_intracomm): model.settings.source = openmc.IndependentSource( space=openmc.stats.Box([-5., -5., -5.], [5., 5., 5.]), angle=openmc.stats.Monodirectional((0., 0., 1.)), - energy=openmc.stats.Discrete([1.0e5], [1.0]) + energy=openmc.stats.Discrete([1.0e5], [1.0]), + constraints={'fissionable': True} ) model.settings.particles = 1000 model.settings.batches = 10 @@ -993,3 +994,8 @@ def test_sample_external_source(run_in_tmpdir, mpi_intracomm): assert p1.wgt == p2.wgt openmc.lib.finalize() + + # Make sure sampling works in volume calculation mode + openmc.lib.init(["-c"]) + openmc.lib.sample_external_source(100) + openmc.lib.finalize() From 4bd0b09e608ed4b27e7bf5e158e44d4147457f2e Mon Sep 17 00:00:00 2001 From: Ethan Peterson Date: Thu, 20 Jun 2024 11:14:55 -0700 Subject: [PATCH 024/184] Avoiding more numpy 2.0 deprecation warnings (#3049) --- openmc/model/surface_composite.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index 7c134e84302..2945f73b71e 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -972,19 +972,19 @@ def _validate_points(self, points): # Check if polygon is self-intersecting by comparing edges pairwise n = len(points) for i in range(n): - p0 = points[i, :] - p1 = points[(i + 1) % n, :] + p0 = np.append(points[i, :], 0) + p1 = np.append(points[(i + 1) % n, :], 0) for j in range(i + 1, n): - p2 = points[j, :] - p3 = points[(j + 1) % n, :] + p2 = np.append(points[j, :], 0) + p3 = np.append(points[(j + 1) % n, :], 0) # Compute orientation of p0 wrt p2->p3 line segment - cp0 = np.cross(p3-p0, p2-p0) + cp0 = np.cross(p3-p0, p2-p0)[-1] # Compute orientation of p1 wrt p2->p3 line segment - cp1 = np.cross(p3-p1, p2-p1) + cp1 = np.cross(p3-p1, p2-p1)[-1] # Compute orientation of p2 wrt p0->p1 line segment - cp2 = np.cross(p1-p2, p0-p2) + cp2 = np.cross(p1-p2, p0-p2)[-1] # Compute orientation of p3 wrt p0->p1 line segment - cp3 = np.cross(p1-p3, p0-p3) + cp3 = np.cross(p1-p3, p0-p3)[-1] # Group cross products in an array and find out how many are 0 cross_products = np.array([[cp0, cp1], [cp2, cp3]]) From 00faa7d698251be80970ce50460c860dbfdc34cb Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Fri, 21 Jun 2024 18:21:13 -0500 Subject: [PATCH 025/184] Set DAGMC cell instances on surface crossing (#3052) --- src/particle.cpp | 9 +++++++-- .../dagmc/universes/inputs_true.dat | 13 +++++++++++-- .../dagmc/universes/results_true.dat | 13 ++++++++++++- tests/regression_tests/dagmc/universes/test.py | 11 +++++++++-- 4 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/particle.cpp b/src/particle.cpp index 0c6a33b78bc..7d30e26bdf8 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -557,9 +557,14 @@ void Particle::cross_surface(const Surface& surf) sqrtkT_last() = sqrtkT(); // set new cell value lowest_coord().cell = i_cell; + auto& cell = model::cells[i_cell]; + cell_instance() = 0; - material() = model::cells[i_cell]->material_[0]; - sqrtkT() = model::cells[i_cell]->sqrtkT_[0]; + if (cell->distribcell_index_ >= 0) + cell_instance() = cell_instance_at_level(*this, n_coord() - 1); + + material() = cell->material(cell_instance()); + sqrtkT() = cell->sqrtkT(cell_instance()); return; } #endif diff --git a/tests/regression_tests/dagmc/universes/inputs_true.dat b/tests/regression_tests/dagmc/universes/inputs_true.dat index babf43ff1d8..4b5be3612bd 100644 --- a/tests/regression_tests/dagmc/universes/inputs_true.dat +++ b/tests/regression_tests/dagmc/universes/inputs_true.dat @@ -47,9 +47,18 @@ eigenvalue 100 10 - 2 + 5

false - + + + 4 0 4 1 4 2 4 3 4 4 + + + 1 + scatter + + + diff --git a/tests/regression_tests/dagmc/universes/results_true.dat b/tests/regression_tests/dagmc/universes/results_true.dat index f7bd016c155..76cbc9db0cf 100644 --- a/tests/regression_tests/dagmc/universes/results_true.dat +++ b/tests/regression_tests/dagmc/universes/results_true.dat @@ -1,2 +1,13 @@ k-combined: -9.156561E-01 4.398617E-02 +9.887663E-01 1.510336E-02 +tally 1: +4.340758E+00 +4.265459E+00 +4.712319E+00 +4.654778E+00 +4.151897E+00 +3.588090E+00 +2.965925E+00 +1.852746E+00 +0.000000E+00 +0.000000E+00 diff --git a/tests/regression_tests/dagmc/universes/test.py b/tests/regression_tests/dagmc/universes/test.py index a318275cd01..057a7b0d477 100644 --- a/tests/regression_tests/dagmc/universes/test.py +++ b/tests/regression_tests/dagmc/universes/test.py @@ -72,13 +72,20 @@ def __init__(self, *args, **kwargs): self._model.geometry = openmc.Geometry([bounding_cell]) + # add a cell instance tally + tally = openmc.Tally(name='cell instance tally') + # using scattering + cell_instance_filter = openmc.CellInstanceFilter(((4, 0), (4, 1), (4, 2), (4, 3), (4, 4))) + tally.filters = [cell_instance_filter] + tally.scores = ['scatter'] + self._model.tallies = [tally] + # settings self._model.settings.particles = 100 self._model.settings.batches = 10 - self._model.settings.inactive = 2 + self._model.settings.inactive = 5 self._model.settings.output = {'summary' : False} - def test_univ(): harness = DAGMCUniverseTest('statepoint.10.h5', model=openmc.Model()) harness.main() From 55b52b7ef3c9415ce045712132bf31c2a013d8c8 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 21 Jun 2024 20:28:56 -0500 Subject: [PATCH 026/184] Release of version 0.15.0 (#3050) --- CMakeLists.txt | 4 +- docs/source/conf.py | 4 +- docs/source/io_formats/settings.rst | 4 +- docs/source/methods/random_ray.rst | 4 +- docs/source/releasenotes/0.15.0.rst | 262 ++++++++++++++++++ docs/source/releasenotes/index.rst | 1 + examples/lattice/hexagonal/build_xml.py | 5 +- examples/lattice/nested/build_xml.py | 5 +- examples/lattice/simple/build_xml.py | 5 +- examples/pincell/build_xml.py | 5 +- examples/pincell_multigroup/build_xml.py | 5 +- include/openmc/version.h.in | 2 +- openmc/__init__.py | 2 +- openmc/deplete/abc.py | 4 +- openmc/deplete/microxs.py | 2 +- openmc/lib/mesh.py | 4 +- openmc/mesh.py | 4 +- openmc/plotter.py | 2 +- openmc/region.py | 2 +- openmc/settings.py | 4 +- openmc/source.py | 6 +- openmc/stats/multivariate.py | 4 +- openmc/trigger.py | 2 +- .../filter_cellfrom/inputs_true.dat | 5 +- .../regression_tests/filter_cellfrom/test.py | 5 +- .../case-01/inputs_true.dat | 5 +- .../case-02/inputs_true.dat | 5 +- .../case-03/inputs_true.dat | 5 +- .../case-04/inputs_true.dat | 5 +- .../case-05/inputs_true.dat | 5 +- .../case-06/inputs_true.dat | 5 +- .../case-07/inputs_true.dat | 5 +- .../case-08/inputs_true.dat | 5 +- .../case-09/inputs_true.dat | 5 +- .../case-10/inputs_true.dat | 5 +- .../case-11/inputs_true.dat | 5 +- .../case-12/inputs_true.dat | 5 +- .../case-13/inputs_true.dat | 5 +- .../case-14/inputs_true.dat | 5 +- .../case-15/inputs_true.dat | 5 +- .../case-16/inputs_true.dat | 5 +- .../case-17/inputs_true.dat | 5 +- .../case-18/inputs_true.dat | 5 +- .../case-19/inputs_true.dat | 5 +- .../case-20/inputs_true.dat | 5 +- .../case-21/inputs_true.dat | 5 +- .../case-a01/inputs_true.dat | 5 +- .../case-d01/inputs_true.dat | 5 +- .../case-d02/inputs_true.dat | 5 +- .../case-d03/inputs_true.dat | 5 +- .../case-d04/inputs_true.dat | 5 +- .../case-d05/inputs_true.dat | 5 +- .../case-d06/inputs_true.dat | 5 +- .../case-d07/inputs_true.dat | 5 +- .../case-d08/inputs_true.dat | 5 +- .../case-e01/inputs_true.dat | 5 +- .../case-e02/inputs_true.dat | 5 +- .../case-e03/inputs_true.dat | 5 +- .../surface_source_write/test.py | 30 +- tests/unit_tests/test_tracks.py | 2 +- 60 files changed, 464 insertions(+), 85 deletions(-) create mode 100644 docs/source/releasenotes/0.15.0.rst diff --git a/CMakeLists.txt b/CMakeLists.txt index 98473e492c4..8a000c9b9e4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,8 +3,8 @@ project(openmc C CXX) # Set version numbers set(OPENMC_VERSION_MAJOR 0) -set(OPENMC_VERSION_MINOR 14) -set(OPENMC_VERSION_RELEASE 1) +set(OPENMC_VERSION_MINOR 15) +set(OPENMC_VERSION_RELEASE 0) set(OPENMC_VERSION ${OPENMC_VERSION_MAJOR}.${OPENMC_VERSION_MINOR}.${OPENMC_VERSION_RELEASE}) configure_file(include/openmc/version.h.in "${CMAKE_BINARY_DIR}/include/openmc/version.h" @ONLY) diff --git a/docs/source/conf.py b/docs/source/conf.py index 53d529f3dc0..19c04d98af3 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -69,9 +69,9 @@ # built documents. # # The short X.Y version. -version = "0.14" +version = "0.15" # The full version, including alpha/beta/rc tags. -release = "0.14.1-dev" +release = "0.15.0" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index 915f698b60f..e395ada6826 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -284,9 +284,9 @@ then, OpenMC will only use up to the :math:`P_1` data. .. note:: This element is not used in the continuous-energy :ref:`energy_mode`. ------------------------- +-------------------------------- ```` Element ------------------------- +-------------------------------- The ```` element indicates the number of times a particle can split during a history. diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst index e0fff792e04..427ba11489f 100644 --- a/docs/source/methods/random_ray.rst +++ b/docs/source/methods/random_ray.rst @@ -100,11 +100,11 @@ terms on the right hand side. .. math:: :label: transport - \begin{align*} + \begin{aligned} \mathbf{\Omega} \cdot \mathbf{\nabla} \psi(\mathbf{r},\mathbf{\Omega},E) & + \Sigma_t(\mathbf{r},E) \psi(\mathbf{r},\mathbf{\Omega},E) = \\ & \int_0^\infty d E^\prime \int_{4\pi} d \Omega^{\prime} \Sigma_s(\mathbf{r},\mathbf{\Omega}^\prime \rightarrow \mathbf{\Omega}, E^\prime \rightarrow E) \psi(\mathbf{r},\mathbf{\Omega}^\prime, E^\prime) \\ & + \frac{\chi(\mathbf{r}, E)}{4\pi k_{eff}} \int_0^\infty dE^\prime \nu \Sigma_f(\mathbf{r},E^\prime) \int_{4\pi}d \Omega^\prime \psi(\mathbf{r},\mathbf{\Omega}^\prime,E^\prime) - \end{align*} + \end{aligned} In Equation :eq:`transport`, :math:`\psi` is the angular neutron flux. This parameter represents the total distance traveled by all neutrons in a particular diff --git a/docs/source/releasenotes/0.15.0.rst b/docs/source/releasenotes/0.15.0.rst new file mode 100644 index 00000000000..069bfd78368 --- /dev/null +++ b/docs/source/releasenotes/0.15.0.rst @@ -0,0 +1,262 @@ +==================== +What's New in 0.15.0 +==================== + +.. currentmodule:: openmc + +------- +Summary +------- + +This release of OpenMC includes many bug fixes, performance improvements, and +several notable new features. The major highlight of this release is the +introduction of a new transport solver based on the random ray method, which is +fully described in the :ref:`user's guide `. Other notable additions +include a mesh-based source class (:class:`openmc.MeshSource`), a generalization +of source domain rejection through the notion of "constraints", and new methods +on mesh-based classes for computing material volume fractions and homogenized +materials. + +------------------------------------ +Compatibility Notes and Deprecations +------------------------------------ + +Previously, specifying domain rejection for a source was only possible on the +:class:`~openmc.IndependentSoure` class and worked by specifying a `domains` +argument. This capability has been generalized to all source classes and +expanded as well; specifying a domain to reject on should now be done with the +`constraints` argument as follows:: + + source = openmc.IndependentSource(..., constraints={'domains': [cell]}) + +The `domains` argument is deprecated and will be removed in a future version of +OpenMC. Similarly, the ``only_fissionable`` argument to +:class:`openmc.stats.Box` has been replaced by a `'fissionable'` constraint. +That is, instead of specifying:: + + space = openmc.stats.Box(lower_left, upper_right, only_fissionable=True) + source = openmc.IndependentSource(space=space) + +You should now provide the constraint as:: + + space = openmc.stats.Box(lower_left, upper_right) + source = openmc.IndependentSource(space=space, constraints={'fissionable': True}) + +The :attr:`openmc.Settings.max_splits` attribute was renamed to +``max_history_splits`` and its default value has been changed to 1e7 (`#2954 +`_). + +------------ +New Features +------------ + +- When running OpenMC in volume calculation mode, only atomic weight ratio data + is loaded from data files which reduces initialization time. (`#2741 + `_) +- Introduced a ``GeometryState`` class in C++ to better separate particle and + geometry data. (`#2744 `_)) +- A new :class:`openmc.MaterialFromFilter` class allows filtering tallies by + which material a particle came from. (`#2750 + `_) +- Implemented a :meth:`openmc.deplete.MicroXS.from_multigroup_flux` method that + generates microscopic cross sections for depletion from a predetermined + multigroup flux. (`#2755 `_) +- A new :class:`openmc.MeshSource` class enables the specification of a source + distribution over a mesh, where each mesh element has a different + energy/angle/time distribution. (`#2759 + `_) +- Improve performance of depletion solver by utilizing CSR sparse matrix + representation. (`#2764 `_, + `#2771 `_) +- Added a :meth:`openmc.CylindricalMesh.get_indices_at_coords` method that + provides the mesh element index corresponding to a given point in space. + (`#2782 `_) +- Added a `path` argument to the :meth:`openmc.deplete.Integrator.integrate` + method. (`#2784 `_) +- Added a :meth:`openmc.Geometry.get_all_nuclides` method. (`#2796 + `_) +- A new capability to compute material volume fractions over mesh elements was + added in the :meth:`openmc.lib.Mesh.material_volumes` method. (`#2802 + `_) +- A new transport solver was added based on the `random ray + `_ method. (`#2823 + `_, `#2988 + `_) +- Added a :attr:`openmc.lib.Material.depletable` attribute. (`#2843 + `_) +- Added a :meth:`openmc.lib.Mesh.get_plot_bins` method and corresponding + ``openmc_mesh_get_plot_bins`` C API function that can be utilized to generate + mesh tally visualizations in the plotter application. (`#2854 + `_) +- Introduced a :func:`openmc.read_source_file` function that enables reading a + source file from the Python API. (`#2858 + `_) +- Added a ``bounding_box`` property on the :class:`openmc.RectilinearMesh` and + :class:`openmc.UnstructuredMesh` classes. (`#2861 + `_) +- Added a ``openmc_mesh_get_volumes`` C API function. (`#2869 + `_) +- The :attr:`openmc.Settings.surf_source_write` dictionary now accepts `cell`, + `cellfrom`, or `cellto` keys that limit surface source sites to those entering + or leaving specific cells. (`#2888 + `_) +- Added a :meth:`openmc.Region.plot` method that allows regions to be plotted + directly. (`#2895 `_) +- Implemented "contains" operator for the :class:`openmc.BoundingBox` class. + (`#2906 `_) +- Generalized source rejection via a new ``constraints`` argument to all source + classes. (`#2916 `_) +- Added a new :class:`openmc.MeshBornFilter` class that filters tally events + based on which mesh element a particle was born in. (`#2925 + `_) +- The :class:`openmc.Trigger` class now has a ``ignore_zeros`` argument that + results in any bins with zero score to be ignored when checking the trigger. + (`#2928 `_) +- Introduced a :attr:`openmc.Settings.max_events` attribute that controls the + maximum number of events a particle can undergo. (`#2945 + `_) +- Added support for :class:`openmc.UnstructuredMesh` in the + :class:`openmc.MeshSource` class. (`#2949 + `_) +- Added a :meth:`openmc.MeshBase.get_homogenized_materials` method that computes + homogenized materials over mesh elements. (`#2971 + `_) +- Add an ``options`` argument to :class:`openmc.UnstructuredMesh` that allows + configuring underlying data structures in MOAB. (`#2976 + `_) +- Type hints were added to several classes in the :mod:`openmc.deplete` module. + (`#2866 `_) + +--------- +Bug Fixes +--------- + +- Fix unit conversion in openmc.deplete.Results.get_mass (`#2761 `_) +- Fix Lagrangian interpolation (`#2775 `_) +- Depletion restart with MPI (`#2778 `_) +- Modify depletion transfer rates test to be more robust (`#2779 `_) +- Call simulation_finalize if needed when finalizing OpenMC (`#2790 `_) +- F90_NONE Removal (MGMC tallying optimization) (`#2785 `_) +- Correctly apply volumes to materials when using DAGMC geometries (`#2787 `_) +- Add inline to openmc::interpolate (`#2789 `_) +- Use huge_tree=True in lxml parsing (`#2791 `_) +- OpenMPMutex "Copying" (`#2794 `_) +- Do not link against several transitive dependencies of HDF5 (`#2797 `_) +- Added check to length of input arguments for IndependantOperator (`#2799 `_) +- Pytest Update Documentation (`#2801 `_) +- Move 'import lxml' to third-party block of imports (`#2803 `_) +- Fix creation of meshes when from loading settings from XML (`#2805 `_) +- Avoid high memory use when writing unstructured mesh VTK files (`#2806 `_) +- Consolidating thread information into the openmp interface header (`#2809 `_) +- Prevent underflow in calculation of speed (`#2811 `_) +- Provide error message if a cell path can't be determined (`#2812 `_) +- Fix distribcell labels for lattices used as fill in multiple cells (`#2813 `_) +- Make creation of spatial trees based on usage for unstructured mesh. (`#2815 `_) +- Ensure particle direction is normalized for plotting / volume calculations (`#2816 `_) +- Added missing meshes to documentation (`#2820 `_) +- Reset timers at correct place in deplete (`#2821 `_) +- Fix config change not propagating through to decay energies (`#2825 `_) +- Ensure that implicit complement cells appear last in DAGMC universes (`#2838 `_) +- Export model.tallies to XML in CoupledOperator (`#2840 `_) +- Fix locating h5m files references in DAGMC universes (`#2842 `_) +- Prepare for NumPy 2.0 (`#2845 `_) +- Added missing functions and classes to openmc.lib docs (`#2847 `_) +- Fix compilation on CentOS 7 (missing link to libdl) (`#2849 `_) +- Adding resulting nuclide to cross section plot legend (`#2851 `_) +- Updating file extension for Excel files when exporting MGXS data (`#2852 `_) +- Removed error raising when calling warn (`#2853 `_) +- Setting ``surf_source_`` attribute for DAGMC surfaces. (`#2857 `_) +- Changing y axis label for heating plots (`#2859 `_) +- Removed unused step_index arg from restart (`#2867 `_) +- Fix issue with Cell::get_contained_cells() utility function (`#2873 `_) +- Adding energy axis units to plot xs (`#2876 `_) +- Set OpenMCOperator materials when diff_burnable_mats = True (`#2877 `_) +- Fix expansion filter merging (`#2882 `_) +- Added checks that tolerance value is between 0 and 1 (`#2884 `_) +- Statepoint file loading refactor and CAPI function (`#2886 `_) +- Added check for length of value passed into EnergyFilter (`#2887 `_) +- Ensure that Model.run() works when specifying a custom XML path (`#2889 `_) +- Updating docker file base to bookworm (`#2890 `_) +- Clarifying documentation for cones (`#2892 `_) +- Abort on cmake config if openmp requested but not found (`#2893 `_) +- Tiny updates from experience building on Mac (`#2894 `_) +- Added damage-energy as optional reaction for micro (`#2903 `_) +- docs: add missing max_splits in settings specification (`#2910 `_) +- Changed CI to use latest actions to get away from the Node 16 deprecation. (`#2912 `_) +- Mkdir to always allow parents and exist ok (`#2914 `_) +- Fixed small sphinx typo (`#2915 `_) +- Hexagonal lattice iterators (`#2921 `_) +- Fix Chain.form_matrix to work with scipy 1.12 (`#2922 `_) +- Allow get_microxs_and_flux to use OPENMC_CHAIN_FILE environment variable (`#2934 `_) +- Polygon fix to better handle colinear points (`#2935 `_) +- Fix CMFD to work with scipy 1.13 (`#2936 `_) +- Print warning if no natural isotopes when using add_element (`#2938 `_) +- Update xtl and xtensor submodules (`#2941 `_) +- Ensure two surfaces with different boundary type are not considered redundant (`#2942 `_) +- Updated package versions in Dockerfile (`#2946 `_) +- Add MPI calls to DAGMC external test (`#2948 `_) +- Eliminate deprecation warnings from scipy and pandas (`#2951 `_) +- Update math function unit test with catch2 (`#2955 `_) +- Support track file writing for particle restart runs. (`#2957 `_) +- Make UWUW optional (`#2965 `_) +- Allow pure decay IndependentOperator (`#2966 `_) +- Added fix to cfloat_endf for length 11 endf floats (`#2967 `_) +- Moved apt get to optional CI parts (`#2970 `_) +- Update bounding_box docstrings (`#2972 `_) +- Added extra error checking on spherical mesh creation (`#2973 `_) +- Update CODEOWNERS file (`#2974 `_) +- Added error checking on cylindrical mesh (`#2977 `_) +- Correction for histogram interpolation of Tabular distributions (`#2981 `_) +- Enforce lower_left in lattice geometry (`#2982 `_) +- Update random_dist.h comment to be less specific (`#2991 `_) +- Apply memoization in get_all_universes (`#2995 `_) +- Make sure skewed dataset is cast to bool properly (`#3001 `_) +- Hexagonal lattice roundtrip (`#3003 `_) +- Fix CylinderSector and IsogonalOctagon translations (`#3018 `_) +- Sets used instead of lists when membership testing (`#3021 `_) +- Fixing plot xs for when plotting element string reaction (`#3029 `_) +- Fix shannon entropy broken link (`#3034 `_) +- Only add png or h5 extension if not present in plots.py (`#3036 `_) +- Fix non-existent path causing segmentation fault when saving plot (`#3038 `_) +- Resolve warnings related to numpy 2.0 (`#3044 `_) +- Update IsogonalOctagon to use xz basis (`#3045 `_) +- Determine whether nuclides are fissionable in volume calc mode (`#3047 `_) +- Avoiding more numpy 2.0 deprecation warnings (`#3049 `_) +- Set DAGMC cell instances on surface crossing (`#3052 `_) + +------------ +Contributors +------------ + +- `Aidan Crilly `_ +- `April Novak `_ +- `Davide Mancusi `_ +- `Baptiste Mouginot `_ +- `Chris Wagner `_ +- `Lorenzo Chierici `_ +- `Catherine Yu `_ +- `Erik Knudsen `_ +- `Ethan Peterson `_ +- `Gavin Ridley `_ +- `hsameer481 `_ +- `Hunter Belanger `_ +- `Isaac Meyer `_ +- `Jin Whan Bae `_ +- `Joffrey Dorville `_ +- `John Tramm `_ +- `Yue Jin `_ +- `Sigfrid Stjärnholm `_ +- `Kimberly Meagher `_ +- `lhchg `_ +- `Luke Labrie-Cleary `_ +- `Micah Gale `_ +- `Nicholas Linden `_ +- `pitkajuh `_ +- `Rosie Barker `_ +- `Paul Romano `_ +- `Patrick Shriwise `_ +- `Jonathan Shimwell `_ +- `Travis Labossiere-Hickman `_ +- `Vanessa Lulla `_ +- `Olek Yardas `_ +- `Perry Young `_ diff --git a/docs/source/releasenotes/index.rst b/docs/source/releasenotes/index.rst index 6923101cc43..bde1205891e 100644 --- a/docs/source/releasenotes/index.rst +++ b/docs/source/releasenotes/index.rst @@ -7,6 +7,7 @@ Release Notes .. toctree:: :maxdepth: 1 + 0.15.0 0.14.0 0.13.3 0.13.2 diff --git a/examples/lattice/hexagonal/build_xml.py b/examples/lattice/hexagonal/build_xml.py index 96ff3f34a11..9485d0aa45f 100644 --- a/examples/lattice/hexagonal/build_xml.py +++ b/examples/lattice/hexagonal/build_xml.py @@ -114,8 +114,9 @@ # Create an initial uniform spatial source distribution over fissionable zones bounds = [-1, -1, -1, 1, 1, 1] -uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True) -settings_file.source = openmc.IndependentSource(space=uniform_dist) +uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:]) +settings_file.source = openmc.IndependentSource( + space=uniform_dist, constraints={'fissionable': True}) settings_file.keff_trigger = {'type' : 'std_dev', 'threshold' : 5E-4} settings_file.trigger_active = True diff --git a/examples/lattice/nested/build_xml.py b/examples/lattice/nested/build_xml.py index 407a53a2327..2db23a46b33 100644 --- a/examples/lattice/nested/build_xml.py +++ b/examples/lattice/nested/build_xml.py @@ -124,8 +124,9 @@ # Create an initial uniform spatial source distribution over fissionable zones bounds = [-1, -1, -1, 1, 1, 1] -uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True) -settings_file.source = openmc.IndependentSource(space=uniform_dist) +uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:]) +settings_file.source = openmc.IndependentSource( + space=uniform_dist, constraints={'fissionable': True}) settings_file.export_to_xml() diff --git a/examples/lattice/simple/build_xml.py b/examples/lattice/simple/build_xml.py index 0da8d8cf086..56c46612169 100644 --- a/examples/lattice/simple/build_xml.py +++ b/examples/lattice/simple/build_xml.py @@ -115,8 +115,9 @@ # Create an initial uniform spatial source distribution over fissionable zones bounds = [-1, -1, -1, 1, 1, 1] -uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True) -settings_file.source = openmc.IndependentSource(space=uniform_dist) +uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:]) +settings_file.source = openmc.IndependentSource( + space=uniform_dist, constraints={'fissionable': True}) settings_file.trigger_active = True settings_file.trigger_max_batches = 100 diff --git a/examples/pincell/build_xml.py b/examples/pincell/build_xml.py index d008c882237..aed0c790f10 100644 --- a/examples/pincell/build_xml.py +++ b/examples/pincell/build_xml.py @@ -67,8 +67,9 @@ # Create an initial uniform spatial source distribution over fissionable zones lower_left = (-pitch/2, -pitch/2, -1) upper_right = (pitch/2, pitch/2, 1) -uniform_dist = openmc.stats.Box(lower_left, upper_right, only_fissionable=True) -settings.source = openmc.IndependentSource(space=uniform_dist) +uniform_dist = openmc.stats.Box(lower_left, upper_right) +settings.source = openmc.IndependentSource( + space=uniform_dist, constraints={'fissionable': True}) # For source convergence checks, add a mesh that can be used to calculate the # Shannon entropy diff --git a/examples/pincell_multigroup/build_xml.py b/examples/pincell_multigroup/build_xml.py index 019ae6a113f..0971e5de639 100644 --- a/examples/pincell_multigroup/build_xml.py +++ b/examples/pincell_multigroup/build_xml.py @@ -113,8 +113,9 @@ # Create an initial uniform spatial source distribution over fissionable zones lower_left = (-pitch/2, -pitch/2, -1) upper_right = (pitch/2, pitch/2, 1) -uniform_dist = openmc.stats.Box(lower_left, upper_right, only_fissionable=True) -settings.source = openmc.IndependentSource(space=uniform_dist) +uniform_dist = openmc.stats.Box(lower_left, upper_right) +settings.source = openmc.IndependentSource( + space=uniform_dist, constraints={'fissionable': True}) settings.export_to_xml() ############################################################################### diff --git a/include/openmc/version.h.in b/include/openmc/version.h.in index e1c2b0541a5..a518e0d63b0 100644 --- a/include/openmc/version.h.in +++ b/include/openmc/version.h.in @@ -10,7 +10,7 @@ namespace openmc { constexpr int VERSION_MAJOR {@OPENMC_VERSION_MAJOR@}; constexpr int VERSION_MINOR {@OPENMC_VERSION_MINOR@}; constexpr int VERSION_RELEASE {@OPENMC_VERSION_RELEASE@}; -constexpr bool VERSION_DEV {true}; +constexpr bool VERSION_DEV {false}; constexpr std::array VERSION {VERSION_MAJOR, VERSION_MINOR, VERSION_RELEASE}; // clang-format on diff --git a/openmc/__init__.py b/openmc/__init__.py index 17b0abe2f69..12a3c87d381 100644 --- a/openmc/__init__.py +++ b/openmc/__init__.py @@ -39,4 +39,4 @@ from openmc.model import Model -__version__ = '0.14.1-dev' +__version__ = '0.15.0' diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 5d9b8a2403b..7bf7f1086a7 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -786,7 +786,7 @@ def integrate( path : PathLike Path to file to write. Defaults to 'depletion_results.h5'. - .. versionadded:: 0.14.1 + .. versionadded:: 0.15.0 """ with change_directory(self.operator.output_dir): n = self.operator.initial_condition() @@ -993,7 +993,7 @@ def integrate( path : PathLike Path to file to write. Defaults to 'depletion_results.h5'. - .. versionadded:: 0.14.1 + .. versionadded:: 0.15.0 """ with change_directory(self.operator.output_dir): n = self.operator.initial_condition() diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index 497a5284a1d..b2ae9f6c7f1 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -219,7 +219,7 @@ def from_multigroup_flux( sections available. MicroXS entry will be 0 if the nuclide cross section is not found. - .. versionadded:: 0.14.1 + .. versionadded:: 0.15.0 Parameters ---------- diff --git a/openmc/lib/mesh.py b/openmc/lib/mesh.py index 4da7baba771..3546112b54d 100644 --- a/openmc/lib/mesh.py +++ b/openmc/lib/mesh.py @@ -174,7 +174,7 @@ def material_volumes( ) -> List[List[Tuple[Material, float]]]: """Determine volume of materials in each mesh element - .. versionadded:: 0.14.1 + .. versionadded:: 0.15.0 Parameters ---------- @@ -231,7 +231,7 @@ def get_plot_bins( ) -> np.ndarray: """Get mesh bin indices for a rasterized plot. - .. versionadded:: 0.14.1 + .. versionadded:: 0.15.0 Parameters ---------- diff --git a/openmc/mesh.py b/openmc/mesh.py index 5456abd1aed..ee58ec3b8c3 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -157,7 +157,7 @@ def get_homogenized_materials( ) -> List[openmc.Material]: """Generate homogenized materials over each element in a mesh. - .. versionadded:: 0.14.1 + .. versionadded:: 0.15.0 Parameters ---------- @@ -1487,6 +1487,8 @@ def get_indices_at_coords( ) -> Tuple[int, int, int]: """Finds the index of the mesh voxel at the specified x,y,z coordinates. + .. versionadded:: 0.15.0 + Parameters ---------- coords : Sequence[float] diff --git a/openmc/plotter.py b/openmc/plotter.py index 8a9a331d804..8b205b79d8b 100644 --- a/openmc/plotter.py +++ b/openmc/plotter.py @@ -169,7 +169,7 @@ def plot_xs(reactions, divisor_types=None, temperature=294., axis=None, energy_axis_units : {'eV', 'keV', 'MeV'} Units used on the plot energy axis - .. versionadded:: 0.14.1 + .. versionadded:: 0.15.0 Returns ------- diff --git a/openmc/region.py b/openmc/region.py index f679129c1fc..e509b152805 100644 --- a/openmc/region.py +++ b/openmc/region.py @@ -344,7 +344,7 @@ def rotate(self, rotation, pivot=(0., 0., 0.), order='xyz', inplace=False, def plot(self, *args, **kwargs): """Display a slice plot of the region. - .. versionadded:: 0.14.1 + .. versionadded:: 0.15.0 Parameters ---------- diff --git a/openmc/settings.py b/openmc/settings.py index 80d766ab149..fae6f638364 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -113,7 +113,7 @@ class Settings: max_particle_events : int Maximum number of allowed particle events per source particle. - .. versionadded:: 0.14.1 + .. versionadded:: 0.15.0 max_order : None or int Maximum scattering order to apply globally when in multi-group mode. max_history_splits : int @@ -158,7 +158,7 @@ class Settings: Starting ray distribution (must be uniform in space and angle) as specified by a :class:`openmc.SourceBase` object. - .. versionadded:: 0.14.1 + .. versionadded:: 0.15.0 resonance_scattering : dict Settings for resonance elastic scattering. Accepted keys are 'enable' (bool), 'method' (str), 'energy_min' (float), 'energy_max' (float), and diff --git a/openmc/source.py b/openmc/source.py index 91318f8e9ad..51ed74de3c6 100644 --- a/openmc/source.py +++ b/openmc/source.py @@ -264,7 +264,7 @@ class IndependentSource(SourceBase): Domains to reject based on, i.e., if a sampled spatial location is not within one of these domains, it will be rejected. - .. deprecated:: 0.14.1 + .. deprecated:: 0.15.0 Use the `constraints` argument instead. constraints : dict Constraints on sampled source particles. Valid keys include 'domains', @@ -482,7 +482,7 @@ class MeshSource(SourceBase): strength of the mesh source as a whole is the sum of all source strengths applied to the elements. - .. versionadded:: 0.14.1 + .. versionadded:: 0.15.0 Parameters ---------- @@ -1049,7 +1049,7 @@ def write_source_file( def read_source_file(filename: PathLike) -> typing.List[SourceParticle]: """Read a source file and return a list of source particles. - .. versionadded:: 0.14.1 + .. versionadded:: 0.15.0 Parameters ---------- diff --git a/openmc/stats/multivariate.py b/openmc/stats/multivariate.py index 212083c3cff..ce9740ad212 100644 --- a/openmc/stats/multivariate.py +++ b/openmc/stats/multivariate.py @@ -769,7 +769,7 @@ class Box(Spatial): Whether spatial sites should only be accepted if they occur in fissionable materials - .. deprecated:: 0.14.1 + .. deprecated:: 0.15.0 Use the `constraints` argument when defining a source object instead. Attributes @@ -782,7 +782,7 @@ class Box(Spatial): Whether spatial sites should only be accepted if they occur in fissionable materials - .. deprecated:: 0.14.1 + .. deprecated:: 0.15.0 Use the `constraints` argument when defining a source object instead. """ diff --git a/openmc/trigger.py b/openmc/trigger.py index 79f5ea5af89..be2537e3ac8 100644 --- a/openmc/trigger.py +++ b/openmc/trigger.py @@ -22,7 +22,7 @@ class Trigger(EqualityMixin): can cause the trigger to fire prematurely if there are zero scores in any bin at the first evaluation. - .. versionadded:: 0.14.1 + .. versionadded:: 0.15.0 Attributes ---------- diff --git a/tests/regression_tests/filter_cellfrom/inputs_true.dat b/tests/regression_tests/filter_cellfrom/inputs_true.dat index 8a37e8286f0..b85f63c6e18 100644 --- a/tests/regression_tests/filter_cellfrom/inputs_true.dat +++ b/tests/regression_tests/filter_cellfrom/inputs_true.dat @@ -49,9 +49,12 @@ 15 5 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 1 diff --git a/tests/regression_tests/filter_cellfrom/test.py b/tests/regression_tests/filter_cellfrom/test.py index 8fb7509cbe8..5559b4c8171 100644 --- a/tests/regression_tests/filter_cellfrom/test.py +++ b/tests/regression_tests/filter_cellfrom/test.py @@ -142,8 +142,9 @@ def model(): core_radius, core_height / 2.0, ] - distribution = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True) - model.settings.source = openmc.IndependentSource(space=distribution) + distribution = openmc.stats.Box(bounds[:3], bounds[3:]) + model.settings.source = openmc.IndependentSource( + space=distribution, constraints={'fissionable': True}) # ============================================================================= # Tallies diff --git a/tests/regression_tests/surface_source_write/case-01/inputs_true.dat b/tests/regression_tests/surface_source_write/case-01/inputs_true.dat index ecca3a9f86c..7368debd5f7 100644 --- a/tests/regression_tests/surface_source_write/case-01/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-01/inputs_true.dat @@ -42,9 +42,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 300 diff --git a/tests/regression_tests/surface_source_write/case-02/inputs_true.dat b/tests/regression_tests/surface_source_write/case-02/inputs_true.dat index 8be841ad6d7..12f15499f20 100644 --- a/tests/regression_tests/surface_source_write/case-02/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-02/inputs_true.dat @@ -42,9 +42,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 8 diff --git a/tests/regression_tests/surface_source_write/case-03/inputs_true.dat b/tests/regression_tests/surface_source_write/case-03/inputs_true.dat index 38faa2a7844..cd0f7deda59 100644 --- a/tests/regression_tests/surface_source_write/case-03/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-03/inputs_true.dat @@ -42,9 +42,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 4 5 6 7 8 9 diff --git a/tests/regression_tests/surface_source_write/case-04/inputs_true.dat b/tests/regression_tests/surface_source_write/case-04/inputs_true.dat index 99a5a289929..ac3c03e37e0 100644 --- a/tests/regression_tests/surface_source_write/case-04/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-04/inputs_true.dat @@ -42,9 +42,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 4 5 6 7 8 9 diff --git a/tests/regression_tests/surface_source_write/case-05/inputs_true.dat b/tests/regression_tests/surface_source_write/case-05/inputs_true.dat index 49eedb7cfb8..143e8520074 100644 --- a/tests/regression_tests/surface_source_write/case-05/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-05/inputs_true.dat @@ -42,9 +42,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 4 5 6 7 8 9 diff --git a/tests/regression_tests/surface_source_write/case-06/inputs_true.dat b/tests/regression_tests/surface_source_write/case-06/inputs_true.dat index cdb9726d65f..f2dc7ea35d6 100644 --- a/tests/regression_tests/surface_source_write/case-06/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-06/inputs_true.dat @@ -42,9 +42,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 300 diff --git a/tests/regression_tests/surface_source_write/case-07/inputs_true.dat b/tests/regression_tests/surface_source_write/case-07/inputs_true.dat index ff73358ca89..a6107d16faa 100644 --- a/tests/regression_tests/surface_source_write/case-07/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-07/inputs_true.dat @@ -42,9 +42,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 300 diff --git a/tests/regression_tests/surface_source_write/case-08/inputs_true.dat b/tests/regression_tests/surface_source_write/case-08/inputs_true.dat index 8bda9795d39..b87e2b2076d 100644 --- a/tests/regression_tests/surface_source_write/case-08/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-08/inputs_true.dat @@ -42,9 +42,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 300 diff --git a/tests/regression_tests/surface_source_write/case-09/inputs_true.dat b/tests/regression_tests/surface_source_write/case-09/inputs_true.dat index 4e893fffa82..f4f6ebd87cf 100644 --- a/tests/regression_tests/surface_source_write/case-09/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-09/inputs_true.dat @@ -42,9 +42,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 300 diff --git a/tests/regression_tests/surface_source_write/case-10/inputs_true.dat b/tests/regression_tests/surface_source_write/case-10/inputs_true.dat index 2d8f1ce6450..372e5c0116f 100644 --- a/tests/regression_tests/surface_source_write/case-10/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-10/inputs_true.dat @@ -42,9 +42,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 300 diff --git a/tests/regression_tests/surface_source_write/case-11/inputs_true.dat b/tests/regression_tests/surface_source_write/case-11/inputs_true.dat index 307dec54789..d5401dc873f 100644 --- a/tests/regression_tests/surface_source_write/case-11/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-11/inputs_true.dat @@ -42,9 +42,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 300 diff --git a/tests/regression_tests/surface_source_write/case-12/inputs_true.dat b/tests/regression_tests/surface_source_write/case-12/inputs_true.dat index 087024695b6..cb8f877416c 100644 --- a/tests/regression_tests/surface_source_write/case-12/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-12/inputs_true.dat @@ -35,9 +35,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 4 5 6 7 8 9 diff --git a/tests/regression_tests/surface_source_write/case-13/inputs_true.dat b/tests/regression_tests/surface_source_write/case-13/inputs_true.dat index a3421298b94..23bbf0455eb 100644 --- a/tests/regression_tests/surface_source_write/case-13/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-13/inputs_true.dat @@ -35,9 +35,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 4 5 6 7 8 9 diff --git a/tests/regression_tests/surface_source_write/case-14/inputs_true.dat b/tests/regression_tests/surface_source_write/case-14/inputs_true.dat index b4c141b5d53..29acd94f250 100644 --- a/tests/regression_tests/surface_source_write/case-14/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-14/inputs_true.dat @@ -35,9 +35,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 4 5 6 7 8 9 diff --git a/tests/regression_tests/surface_source_write/case-15/inputs_true.dat b/tests/regression_tests/surface_source_write/case-15/inputs_true.dat index 9244d839d13..c775f2f5276 100644 --- a/tests/regression_tests/surface_source_write/case-15/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-15/inputs_true.dat @@ -35,9 +35,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 4 5 6 7 8 9 diff --git a/tests/regression_tests/surface_source_write/case-16/inputs_true.dat b/tests/regression_tests/surface_source_write/case-16/inputs_true.dat index b3139928aa6..969a8f31ea8 100644 --- a/tests/regression_tests/surface_source_write/case-16/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-16/inputs_true.dat @@ -35,9 +35,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 4 5 6 7 8 9 diff --git a/tests/regression_tests/surface_source_write/case-17/inputs_true.dat b/tests/regression_tests/surface_source_write/case-17/inputs_true.dat index 8fcb01e4502..0a65db51008 100644 --- a/tests/regression_tests/surface_source_write/case-17/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-17/inputs_true.dat @@ -35,9 +35,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 4 5 6 7 8 9 diff --git a/tests/regression_tests/surface_source_write/case-18/inputs_true.dat b/tests/regression_tests/surface_source_write/case-18/inputs_true.dat index 80d6253a16c..89df27b475b 100644 --- a/tests/regression_tests/surface_source_write/case-18/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-18/inputs_true.dat @@ -35,9 +35,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 4 5 6 7 8 9 diff --git a/tests/regression_tests/surface_source_write/case-19/inputs_true.dat b/tests/regression_tests/surface_source_write/case-19/inputs_true.dat index 5355675fec2..3ea7dbffe38 100644 --- a/tests/regression_tests/surface_source_write/case-19/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-19/inputs_true.dat @@ -35,9 +35,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 4 5 6 7 8 9 diff --git a/tests/regression_tests/surface_source_write/case-20/inputs_true.dat b/tests/regression_tests/surface_source_write/case-20/inputs_true.dat index 44f0d72c732..c3c768a3da9 100644 --- a/tests/regression_tests/surface_source_write/case-20/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-20/inputs_true.dat @@ -35,9 +35,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 4 diff --git a/tests/regression_tests/surface_source_write/case-21/inputs_true.dat b/tests/regression_tests/surface_source_write/case-21/inputs_true.dat index 893b8b5b975..5dae8374d27 100644 --- a/tests/regression_tests/surface_source_write/case-21/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-21/inputs_true.dat @@ -35,9 +35,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 4 diff --git a/tests/regression_tests/surface_source_write/case-a01/inputs_true.dat b/tests/regression_tests/surface_source_write/case-a01/inputs_true.dat index a65c9f770eb..5cec1b76f73 100644 --- a/tests/regression_tests/surface_source_write/case-a01/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-a01/inputs_true.dat @@ -42,9 +42,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 1 2 3 diff --git a/tests/regression_tests/surface_source_write/case-d01/inputs_true.dat b/tests/regression_tests/surface_source_write/case-d01/inputs_true.dat index b6157ba9b50..b5452080c23 100644 --- a/tests/regression_tests/surface_source_write/case-d01/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-d01/inputs_true.dat @@ -21,9 +21,12 @@ 5 1 - + -4 -4 -20 4 4 20 + + true + 300 diff --git a/tests/regression_tests/surface_source_write/case-d02/inputs_true.dat b/tests/regression_tests/surface_source_write/case-d02/inputs_true.dat index 6877634f12c..d6ca4143c62 100644 --- a/tests/regression_tests/surface_source_write/case-d02/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-d02/inputs_true.dat @@ -21,9 +21,12 @@ 5 1 - + -4 -4 -20 4 4 20 + + true + 1 diff --git a/tests/regression_tests/surface_source_write/case-d03/inputs_true.dat b/tests/regression_tests/surface_source_write/case-d03/inputs_true.dat index ddcde89e4d4..ba326c978fc 100644 --- a/tests/regression_tests/surface_source_write/case-d03/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-d03/inputs_true.dat @@ -21,9 +21,12 @@ 5 1 - + -4 -4 -20 4 4 20 + + true + 300 diff --git a/tests/regression_tests/surface_source_write/case-d04/inputs_true.dat b/tests/regression_tests/surface_source_write/case-d04/inputs_true.dat index 7695ecc7970..c309dd1552b 100644 --- a/tests/regression_tests/surface_source_write/case-d04/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-d04/inputs_true.dat @@ -21,9 +21,12 @@ 5 1 - + -4 -4 -20 4 4 20 + + true + 1 diff --git a/tests/regression_tests/surface_source_write/case-d05/inputs_true.dat b/tests/regression_tests/surface_source_write/case-d05/inputs_true.dat index 865633fbb8c..c49703f831a 100644 --- a/tests/regression_tests/surface_source_write/case-d05/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-d05/inputs_true.dat @@ -21,9 +21,12 @@ 5 1 - + -4 -4 -20 4 4 20 + + true + 300 diff --git a/tests/regression_tests/surface_source_write/case-d06/inputs_true.dat b/tests/regression_tests/surface_source_write/case-d06/inputs_true.dat index 4226707369d..48db2c30dd2 100644 --- a/tests/regression_tests/surface_source_write/case-d06/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-d06/inputs_true.dat @@ -21,9 +21,12 @@ 5 1 - + -4 -4 -20 4 4 20 + + true + 300 diff --git a/tests/regression_tests/surface_source_write/case-d07/inputs_true.dat b/tests/regression_tests/surface_source_write/case-d07/inputs_true.dat index 77498b64db3..e2897125b40 100644 --- a/tests/regression_tests/surface_source_write/case-d07/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-d07/inputs_true.dat @@ -35,9 +35,12 @@ 5 1 - + -4 -4 -20 4 4 20 + + true + 101 102 103 104 105 106 diff --git a/tests/regression_tests/surface_source_write/case-d08/inputs_true.dat b/tests/regression_tests/surface_source_write/case-d08/inputs_true.dat index 561a3833736..185b8629b90 100644 --- a/tests/regression_tests/surface_source_write/case-d08/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-d08/inputs_true.dat @@ -35,9 +35,12 @@ 5 1 - + -4 -4 -20 4 4 20 + + true + 101 102 103 104 105 106 diff --git a/tests/regression_tests/surface_source_write/case-e01/inputs_true.dat b/tests/regression_tests/surface_source_write/case-e01/inputs_true.dat index 99a5a289929..ac3c03e37e0 100644 --- a/tests/regression_tests/surface_source_write/case-e01/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-e01/inputs_true.dat @@ -42,9 +42,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 4 5 6 7 8 9 diff --git a/tests/regression_tests/surface_source_write/case-e02/inputs_true.dat b/tests/regression_tests/surface_source_write/case-e02/inputs_true.dat index ff73358ca89..a6107d16faa 100644 --- a/tests/regression_tests/surface_source_write/case-e02/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-e02/inputs_true.dat @@ -42,9 +42,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 300 diff --git a/tests/regression_tests/surface_source_write/case-e03/inputs_true.dat b/tests/regression_tests/surface_source_write/case-e03/inputs_true.dat index a3421298b94..23bbf0455eb 100644 --- a/tests/regression_tests/surface_source_write/case-e03/inputs_true.dat +++ b/tests/regression_tests/surface_source_write/case-e03/inputs_true.dat @@ -35,9 +35,12 @@ 5 1 - + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + true + 4 5 6 7 8 9 diff --git a/tests/regression_tests/surface_source_write/test.py b/tests/regression_tests/surface_source_write/test.py index f949d808503..38581736ec4 100644 --- a/tests/regression_tests/surface_source_write/test.py +++ b/tests/regression_tests/surface_source_write/test.py @@ -277,8 +277,9 @@ def model_1(): core_radius, core_height / 2.0, ] - distribution = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True) - model.settings.source = openmc.IndependentSource(space=distribution) + distribution = openmc.stats.Box(bounds[:3], bounds[3:]) + model.settings.source = openmc.IndependentSource( + space=distribution, constraints={'fissionable': True}) return model @@ -379,8 +380,9 @@ def model_2(): core_radius, core_height / 2.0, ] - distribution = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True) - model.settings.source = openmc.IndependentSource(space=distribution) + distribution = openmc.stats.Box(bounds[:3], bounds[3:]) + model.settings.source = openmc.IndependentSource( + space=distribution, constraints={'fissionable': True}) return model @@ -481,8 +483,9 @@ def model_3(): core_radius, core_height / 2.0, ] - distribution = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True) - model.settings.source = openmc.IndependentSource(space=distribution) + distribution = openmc.stats.Box(bounds[:3], bounds[3:]) + model.settings.source = openmc.IndependentSource( + space=distribution, constraints={'fissionable': True}) return model @@ -594,8 +597,9 @@ def model_4(): core_radius, core_height / 2.0, ] - distribution = openmc.stats.Box(bounds[:3], bounds[3:], only_fissionable=True) - model.settings.source = openmc.IndependentSource(space=distribution) + distribution = openmc.stats.Box(bounds[:3], bounds[3:]) + model.settings.source = openmc.IndependentSource( + space=distribution, constraints={'fissionable': True}) return model @@ -935,8 +939,9 @@ def model_dagmc_1(): model.settings.inactive = 1 model.settings.seed = 1 - source_box = openmc.stats.Box([-4, -4, -20], [4, 4, 20], only_fissionable=True) - model.settings.source = openmc.IndependentSource(space=source_box) + source_box = openmc.stats.Box([-4, -4, -20], [4, 4, 20]) + model.settings.source = openmc.IndependentSource( + space=source_box, constraints={'fissionable': True}) return model @@ -1069,8 +1074,9 @@ def model_dagmc_2(): model.settings.inactive = 1 model.settings.seed = 1 - source_box = openmc.stats.Box([-4, -4, -20], [4, 4, 20], only_fissionable=True) - model.settings.source = openmc.IndependentSource(space=source_box) + source_box = openmc.stats.Box([-4, -4, -20], [4, 4, 20]) + model.settings.source = openmc.IndependentSource( + space=source_box, constraints={'fissionable': True}) return model diff --git a/tests/unit_tests/test_tracks.py b/tests/unit_tests/test_tracks.py index 3951a72c63c..fa866415984 100644 --- a/tests/unit_tests/test_tracks.py +++ b/tests/unit_tests/test_tracks.py @@ -168,7 +168,7 @@ def test_restart_track(run_in_tmpdir, sphere_model): # generate lost particle files with pytest.raises(RuntimeError, match='Maximum number of lost particles has been reached.'): - sphere_model.run(output=False) + sphere_model.run(output=False, threads=1) lost_particle_files = list(Path.cwd().glob('particle_*.h5')) assert len(lost_particle_files) > 0 From 8ce81d132ade670b66483887955fc1182358273a Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 24 Jun 2024 11:29:51 -0500 Subject: [PATCH 027/184] Change version number to 0.15.1-dev (#3058) --- CMakeLists.txt | 2 +- docs/source/conf.py | 2 +- include/openmc/version.h.in | 2 +- openmc/__init__.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8a000c9b9e4..3cbeaa0241b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ project(openmc C CXX) # Set version numbers set(OPENMC_VERSION_MAJOR 0) set(OPENMC_VERSION_MINOR 15) -set(OPENMC_VERSION_RELEASE 0) +set(OPENMC_VERSION_RELEASE 1) set(OPENMC_VERSION ${OPENMC_VERSION_MAJOR}.${OPENMC_VERSION_MINOR}.${OPENMC_VERSION_RELEASE}) configure_file(include/openmc/version.h.in "${CMAKE_BINARY_DIR}/include/openmc/version.h" @ONLY) diff --git a/docs/source/conf.py b/docs/source/conf.py index 19c04d98af3..726269f0b7c 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -71,7 +71,7 @@ # The short X.Y version. version = "0.15" # The full version, including alpha/beta/rc tags. -release = "0.15.0" +release = "0.15.1-dev" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/include/openmc/version.h.in b/include/openmc/version.h.in index a518e0d63b0..e1c2b0541a5 100644 --- a/include/openmc/version.h.in +++ b/include/openmc/version.h.in @@ -10,7 +10,7 @@ namespace openmc { constexpr int VERSION_MAJOR {@OPENMC_VERSION_MAJOR@}; constexpr int VERSION_MINOR {@OPENMC_VERSION_MINOR@}; constexpr int VERSION_RELEASE {@OPENMC_VERSION_RELEASE@}; -constexpr bool VERSION_DEV {false}; +constexpr bool VERSION_DEV {true}; constexpr std::array VERSION {VERSION_MAJOR, VERSION_MINOR, VERSION_RELEASE}; // clang-format on diff --git a/openmc/__init__.py b/openmc/__init__.py index 12a3c87d381..d18e1188e2a 100644 --- a/openmc/__init__.py +++ b/openmc/__init__.py @@ -39,4 +39,4 @@ from openmc.model import Model -__version__ = '0.15.0' +__version__ = '0.15.1-dev' From a8171cbd4e9a1ee801a52f6f0c1426982b73774f Mon Sep 17 00:00:00 2001 From: Ethan Krammer <68677905+ethankrammer@users.noreply.github.com> Date: Thu, 27 Jun 2024 09:22:10 -0500 Subject: [PATCH 028/184] Implementation of Shannon Entropy for Random Ray (#3030) Co-authored-by: Ethan Krammer Co-authored-by: John Tramm --- docs/source/methods/eigenvalue.rst | 17 +++- docs/source/methods/random_ray.rst | 36 +++++++ docs/source/usersguide/random_ray.rst | 11 +++ src/eigenvalue.cpp | 2 +- src/random_ray/flat_source_domain.cpp | 38 +++++++- src/settings.cpp | 45 +++++---- src/simulation.cpp | 3 +- .../random_ray_entropy/__init__.py | 0 .../random_ray_entropy/geometry.xml | 88 ++++++++++++++++++ .../random_ray_entropy/materials.xml | 8 ++ .../random_ray_entropy/mgxs.h5 | Bin 0 -> 10664 bytes .../random_ray_entropy/results_true.dat | 13 +++ .../random_ray_entropy/settings.xml | 17 ++++ .../random_ray_entropy/test.py | 33 +++++++ 14 files changed, 282 insertions(+), 29 deletions(-) create mode 100644 tests/regression_tests/random_ray_entropy/__init__.py create mode 100644 tests/regression_tests/random_ray_entropy/geometry.xml create mode 100644 tests/regression_tests/random_ray_entropy/materials.xml create mode 100644 tests/regression_tests/random_ray_entropy/mgxs.h5 create mode 100644 tests/regression_tests/random_ray_entropy/results_true.dat create mode 100644 tests/regression_tests/random_ray_entropy/settings.xml create mode 100644 tests/regression_tests/random_ray_entropy/test.py diff --git a/docs/source/methods/eigenvalue.rst b/docs/source/methods/eigenvalue.rst index b63723c0264..8abcc09574b 100644 --- a/docs/source/methods/eigenvalue.rst +++ b/docs/source/methods/eigenvalue.rst @@ -55,15 +55,17 @@ in :ref:`fission-bank-algorithms`. Source Convergence Issues ------------------------- +.. _methods-shannon-entropy: + Diagnosing Convergence with Shannon Entropy ------------------------------------------- As discussed earlier, it is necessary to converge both :math:`k_{eff}` and the source distribution before any tallies can begin. Moreover, the convergence rate -of the source distribution is in general slower than that of -:math:`k_{eff}`. One should thus examine not only the convergence of -:math:`k_{eff}` but also the convergence of the source distribution in order to -make decisions on when to start active batches. +of the source distribution is in general slower than that of :math:`k_{eff}`. +One should thus examine not only the convergence of :math:`k_{eff}` but also the +convergence of the source distribution in order to make decisions on when to +start active batches. However, the representation of the source distribution makes it a bit more difficult to analyze its convergence. Since :math:`k_{eff}` is a scalar @@ -108,6 +110,13 @@ at plots of :math:`k_{eff}` and the Shannon entropy. A number of methods have been proposed (see e.g. [Romano]_, [Ueki]_), but each of these is not without problems. +Shannon entropy is calculated differently for the random ray solver, as +described :ref:`in the random ray theory section +`. Additionally, as the Shannon entropy only +serves as a diagnostic tool for convergence of the fission source distribution, +there is currently no diagnostic to determine if the scattering source +distribution in random ray is converged. + --------------------------- Uniform Fission Site Method --------------------------- diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst index 427ba11489f..d46d3ad4165 100644 --- a/docs/source/methods/random_ray.rst +++ b/docs/source/methods/random_ray.rst @@ -411,6 +411,8 @@ which when partially simplified becomes: Note that there are now four (seemingly identical) volume terms in this equation. +.. _methods-volume-dilemma: + ~~~~~~~~~~~~~~ Volume Dilemma ~~~~~~~~~~~~~~ @@ -745,6 +747,7 @@ How are Tallies Handled? Most tallies, filters, and scores that you would expect to work with a multigroup solver like random ray should work. For example, you can define 3D mesh tallies with energy filters and flux, fission, and nu-fission scores, etc. + There are some restrictions though. For starters, it is assumed that all filter mesh boundaries will conform to physical surface boundaries (or lattice boundaries) in the simulation geometry. It is acceptable for multiple cells @@ -754,6 +757,39 @@ behavior if a single simulation cell is able to score to multiple filter mesh cells. In the future, the capability to fully support mesh tallies may be added to OpenMC, but for now this restriction needs to be respected. +.. _methods-shannon-entropy-random-ray: + +----------------------------- +Shannon Entropy in Random Ray +----------------------------- + +As :math:`k_{eff}` is updated at each generation, the fission source at each FSR +is used to compute the Shannon entropy. This follows the :ref:`same procedure +for computing Shannon entropy in continuous-energy or multigroup Monte Carlo +simulations `, except that fission sources at FSRs are +considered, rather than fission sites of user-defined regular meshes. Thus, the +volume-weighted fission rate is considered instead, and the fraction of fission +sources is adjusted such that: + +.. math:: + :label: fraction-source-random-ray + + S_i = \frac{\text{Fission source in FSR $i \times$ Volume of FSR + $i$}}{\text{Total fission source}} = \frac{Q_{i} V_{i}}{\sum_{i=1}^{i=N} + Q_{i} V_{i}} + +The Shannon entropy is then computed normally as + +.. math:: + :label: shannon-entropy-random-ray + + H = - \sum_{i=1}^N S_i \log_2 S_i + +where :math:`N` is the number of FSRs. FSRs with no fission source (or, +occassionally, negative fission source, :ref:`due to the volume estimator +problem `) are skipped to avoid taking an undefined +logarithm in :eq:`shannon-entropy-random-ray`. + .. _usersguide_fixed_source_methods: ------------ diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index 61f0746500c..2a038b12257 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -62,6 +62,17 @@ solver:: settings.batches = 1200 settings.inactive = 600 +--------------- +Shannon Entropy +--------------- + +Similar to Monte Carlo, :ref:`Shannon entropy +` can be used to gauge whether the fission +source has fully developed. The Shannon entropy is calculated automatically +after each batch and is printed to the statepoint file. Unlike Monte Carlo, an +entropy mesh does not need to be defined, as the Shannon entropy is calculated +over FSRs using a volume-weighted approach. + ------------------------------- Inactive Ray Length (Dead Zone) ------------------------------- diff --git a/src/eigenvalue.cpp b/src/eigenvalue.cpp index d5410094b18..8669d76f947 100644 --- a/src/eigenvalue.cpp +++ b/src/eigenvalue.cpp @@ -556,7 +556,7 @@ void shannon_entropy() double H = 0.0; for (auto p_i : p) { if (p_i > 0.0) { - H -= p_i * std::log(p_i) / std::log(2.0); + H -= p_i * std::log2(p_i); } } diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 7f5d76943e5..792e97da0c1 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -1,6 +1,7 @@ #include "openmc/random_ray/flat_source_domain.h" #include "openmc/cell.h" +#include "openmc/eigenvalue.h" #include "openmc/geometry.h" #include "openmc/material.h" #include "openmc/message_passing.h" @@ -278,6 +279,9 @@ double FlatSourceDomain::compute_k_eff(double k_eff_old) const const int t = 0; const int a = 0; + // Vector for gathering fission source terms for Shannon entropy calculation + vector p(n_source_regions_, 0.0f); + #pragma omp parallel for reduction(+ : fission_rate_old, fission_rate_new) for (int sr = 0; sr < n_source_regions_; sr++) { @@ -300,12 +304,38 @@ double FlatSourceDomain::compute_k_eff(double k_eff_old) const sr_fission_source_new += nu_sigma_f * scalar_flux_new_[idx]; } - fission_rate_old += sr_fission_source_old * volume; - fission_rate_new += sr_fission_source_new * volume; + // Compute total fission rates in FSR + sr_fission_source_old *= volume; + sr_fission_source_new *= volume; + + // Accumulate totals + fission_rate_old += sr_fission_source_old; + fission_rate_new += sr_fission_source_new; + + // Store total fission rate in the FSR for Shannon calculation + p[sr] = sr_fission_source_new; } double k_eff_new = k_eff_old * (fission_rate_new / fission_rate_old); + double H = 0.0; + // defining an inverse sum for better performance + double inverse_sum = 1 / fission_rate_new; + +#pragma omp parallel for reduction(+ : H) + for (int sr = 0; sr < n_source_regions_; sr++) { + // Only if FSR has non-negative and non-zero fission source + if (p[sr] > 0.0f) { + // Normalize to total weight of bank sites. p_i for better performance + float p_i = p[sr] * inverse_sum; + // Sum values to obtain Shannon entropy. + H -= p_i * std::log2(p_i); + } + } + + // Adds entropy value to shared entropy vector in openmc namespace. + simulation::entropy.push_back(H); + return k_eff_new; } @@ -525,8 +555,8 @@ void FlatSourceDomain::random_ray_tally() #pragma omp atomic tally.results_(task.filter_idx, task.score_idx, TallyResult::VALUE) += score; - } // end tally task loop - } // end energy group loop + } + } // For flux tallies, the total volume of the spatial region is needed // for normalizing the flux. We store this volume in a separate tensor. diff --git a/src/settings.cpp b/src/settings.cpp index 6c3226b04a6..4ffa50cdf45 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -627,30 +627,37 @@ void read_settings_xml(pugi::xml_node root) } } - // Shannon Entropy mesh - if (check_for_node(root, "entropy_mesh")) { - int temp = std::stoi(get_node_value(root, "entropy_mesh")); - if (model::mesh_map.find(temp) == model::mesh_map.end()) { - fatal_error(fmt::format( - "Mesh {} specified for Shannon entropy does not exist.", temp)); + // Shannon entropy + if (solver_type == SolverType::RANDOM_RAY) { + if (check_for_node(root, "entropy_mesh")) { + fatal_error("Random ray uses FSRs to compute the Shannon entropy. " + "No user-defined entropy mesh is supported."); } + entropy_on = true; + } else if (solver_type == SolverType::MONTE_CARLO) { + if (check_for_node(root, "entropy_mesh")) { + int temp = std::stoi(get_node_value(root, "entropy_mesh")); + if (model::mesh_map.find(temp) == model::mesh_map.end()) { + fatal_error(fmt::format( + "Mesh {} specified for Shannon entropy does not exist.", temp)); + } - auto* m = - dynamic_cast(model::meshes[model::mesh_map.at(temp)].get()); - if (!m) - fatal_error("Only regular meshes can be used as an entropy mesh"); - simulation::entropy_mesh = m; + auto* m = dynamic_cast( + model::meshes[model::mesh_map.at(temp)].get()); + if (!m) + fatal_error("Only regular meshes can be used as an entropy mesh"); + simulation::entropy_mesh = m; - // Turn on Shannon entropy calculation - entropy_on = true; + // Turn on Shannon entropy calculation + entropy_on = true; - } else if (check_for_node(root, "entropy")) { - fatal_error( - "Specifying a Shannon entropy mesh via the element " - "is deprecated. Please create a mesh using and then reference " - "it by specifying its ID in an element."); + } else if (check_for_node(root, "entropy")) { + fatal_error( + "Specifying a Shannon entropy mesh via the element " + "is deprecated. Please create a mesh using and then reference " + "it by specifying its ID in an element."); + } } - // Uniform fission source weighting mesh if (check_for_node(root, "ufs_mesh")) { auto temp = std::stoi(get_node_value(root, "ufs_mesh")); diff --git a/src/simulation.cpp b/src/simulation.cpp index 2d5d75aa477..be03e48553c 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -531,7 +531,8 @@ void finalize_generation() if (settings::run_mode == RunMode::EIGENVALUE) { // Calculate shannon entropy - if (settings::entropy_on) + if (settings::entropy_on && + settings::solver_type == SolverType::MONTE_CARLO) shannon_entropy(); // Collect results and statistics diff --git a/tests/regression_tests/random_ray_entropy/__init__.py b/tests/regression_tests/random_ray_entropy/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regression_tests/random_ray_entropy/geometry.xml b/tests/regression_tests/random_ray_entropy/geometry.xml new file mode 100644 index 00000000000..4c87bbbfb9a --- /dev/null +++ b/tests/regression_tests/random_ray_entropy/geometry.xml @@ -0,0 +1,88 @@ + + + + + + 12.5 12.5 12.5 + 8 8 8 + 0.0 0.0 0.0 + +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 + +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 + +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 + +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 + +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 + +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 + +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 + +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 + + + + + + + + diff --git a/tests/regression_tests/random_ray_entropy/materials.xml b/tests/regression_tests/random_ray_entropy/materials.xml new file mode 100644 index 00000000000..5a6f93414bb --- /dev/null +++ b/tests/regression_tests/random_ray_entropy/materials.xml @@ -0,0 +1,8 @@ + + + mgxs.h5 + + + + + diff --git a/tests/regression_tests/random_ray_entropy/mgxs.h5 b/tests/regression_tests/random_ray_entropy/mgxs.h5 new file mode 100644 index 0000000000000000000000000000000000000000..6ce80bf713a8d5ff29700ba59e982e18f99b5403 GIT binary patch literal 10664 zcmeHN%}*0S6o1<)ELx=~2P1yeL;JB5M`%OA4UJMuu{yU;m}}jFYub= zpCp>Wc%w?`qzL4JLLk-KJs<`=jH9M_TERFYFm72kyJxt}HjSLnQCudeLWQygKO6SV(mu^{#vDtpJMgnd zH{3=&5sl;2%Eu!cjybM9GwIIQ3|eA|^>K9`h0dQi?=_$Ct!2L!-+%vhQ~3XQ!s+(o zk0k{bjN?Gc@yAL;^%%{l`++9`bj_V4+@MK6%Q@D1c^N!HbJV{c83gv_Q{XH7@ zLUFe!NP%DEcLnpS4N4!}|Be9huotfg$Yp`!8G*|jVO6cwN1#3`^AJf<+$ z;db}k2Yj;uLxT^6kOlNSNdC@?g5Pacu$ja1uHg#02Ep%5%2c{A9m}hQ&q)QJ zb5gOo1FX=yIu|vppSS1FZtb80ix}W%XQ$2Y2?A0_%E1fc90x*l}8~J3RX;`i{jvLfPglKxS;(OKWW1?go4)haasERZ?)r} zg}UG$wGRwK45412!0~}z?V16K?LHMyO*|C8v~|7xQd)UjRl3eE`ovRdPeNFIJeKMT Fk6-_XIk5l$ literal 0 HcmV?d00001 diff --git a/tests/regression_tests/random_ray_entropy/results_true.dat b/tests/regression_tests/random_ray_entropy/results_true.dat new file mode 100644 index 00000000000..25425453871 --- /dev/null +++ b/tests/regression_tests/random_ray_entropy/results_true.dat @@ -0,0 +1,13 @@ +k-combined: +1.000000E+00 0.000000E+00 +entropy: +8.863421E+00 +8.933584E+00 +8.960553E+00 +8.967921E+00 +8.976016E+00 +8.981856E+00 +8.983670E+00 +8.986584E+00 +8.987732E+00 +8.988186E+00 diff --git a/tests/regression_tests/random_ray_entropy/settings.xml b/tests/regression_tests/random_ray_entropy/settings.xml new file mode 100644 index 00000000000..81deaa7751d --- /dev/null +++ b/tests/regression_tests/random_ray_entropy/settings.xml @@ -0,0 +1,17 @@ + + + eigenvalue + 100 + 10 + 5 + multi-group + + + + 0.0 0.0 0.0 100.0 100.0 100.0 + + + 40.0 + 400.0 + + diff --git a/tests/regression_tests/random_ray_entropy/test.py b/tests/regression_tests/random_ray_entropy/test.py new file mode 100644 index 00000000000..a3cba65ad0b --- /dev/null +++ b/tests/regression_tests/random_ray_entropy/test.py @@ -0,0 +1,33 @@ +import glob +import os + +from openmc import StatePoint + +from tests.testing_harness import TestHarness + + +class EntropyTestHarness(TestHarness): + def _get_results(self): + """Digest info in the statepoint and return as a string.""" + # Read the statepoint file. + statepoint = glob.glob(os.path.join(os.getcwd(), self._sp_name))[0] + with StatePoint(statepoint) as sp: + # Write out k-combined. + outstr = 'k-combined:\n' + outstr += '{:12.6E} {:12.6E}\n'.format(sp.keff.n, sp.keff.s) + + # Write out entropy data. + outstr += 'entropy:\n' + results = ['{:12.6E}'.format(x) for x in sp.entropy] + outstr += '\n'.join(results) + '\n' + + return outstr + +''' +# This test is adapted from "Monte Carlo power iteration: Entropy and spatial correlations," +M. Nowak et al. The cross sections are defined explicitly so that the value for entropy +is exactly 9 and the eigenvalue is exactly 1. +''' +def test_entropy(): + harness = EntropyTestHarness('statepoint.10.h5') + harness.main() From ac0ad0bac2aaae0b71f625248b56933bc1138699 Mon Sep 17 00:00:00 2001 From: Olek <45364492+yardasol@users.noreply.github.com> Date: Fri, 28 Jun 2024 10:18:17 -0400 Subject: [PATCH 029/184] Fix hyperlinks in `random_ray.rst` (#3064) --- docs/source/methods/random_ray.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst index d46d3ad4165..7a29a187516 100644 --- a/docs/source/methods/random_ray.rst +++ b/docs/source/methods/random_ray.rst @@ -10,7 +10,7 @@ Random Ray What is Random Ray? ------------------- -`Random ray `_ is a stochastic transport method, closely related to +`Random ray `_ is a stochastic transport method, closely related to the deterministic Method of Characteristics (MOC) [Askew-1972]_. Rather than each ray representing a single neutron as in Monte Carlo, it represents a characteristic line through the simulation geometry upon which the transport @@ -82,7 +82,7 @@ Random Ray Numerical Derivation In this section, we will derive the numerical basis for the random ray solver mode in OpenMC. The derivation of random ray is also discussed in several papers -(`1 `_, `2 `_, `3 `_), and some of those +(`1 `_, `2 `_, `3 `_), and some of those derivations are reproduced here verbatim. Several extensions are also made to add clarity, particularly on the topic of OpenMC's treatment of cell volumes in the random ray solver. @@ -428,7 +428,7 @@ of terms. Mathematically, such cancellation allows us to arrive at the following This derivation appears mathematically sound at first glance but unfortunately raises a serious issue as discussed in more depth by `Tramm et al. -`_ and `Cosgrove and Tramm `_. Namely, the second +`_ and `Cosgrove and Tramm `_. Namely, the second term: .. math:: @@ -607,7 +607,7 @@ guess can be made by taking the isotropic source from the FSR the ray was sampled in, direct usage of this quantity would result in significant bias and error being imparted on the simulation. -Thus, an `on-the-fly approximation method `_ was developed (known +Thus, an `on-the-fly approximation method `_ was developed (known as the "dead zone"), where the first several mean free paths of a ray are considered to be "inactive" or "read only". In this sense, the angular flux is solved for using the MOC equation, but the ray does not "tally" any scalar flux From 391450ce01c4222c4a24466d749ec1dc5ac61b19 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Fri, 28 Jun 2024 11:41:20 -0500 Subject: [PATCH 030/184] Random Ray Normalization Improvements (#3051) Co-authored-by: Olek <45364492+yardasol@users.noreply.github.com> --- docs/source/methods/random_ray.rst | 14 + docs/source/usersguide/random_ray.rst | 21 + .../openmc/random_ray/flat_source_domain.h | 5 + openmc/settings.py | 12 + src/random_ray/flat_source_domain.cpp | 125 +- src/settings.cpp | 4 + .../random_ray_basic/inputs_true.dat | 1 + .../regression_tests/random_ray_basic/test.py | 1 + .../False_hybrid/inputs_true.dat | 1018 +++++++++++++++++ .../False_hybrid/results_true.dat | 9 + .../True_hybrid/inputs_true.dat | 1018 +++++++++++++++++ .../True_hybrid/results_true.dat | 9 + .../random_ray_cube/__init__.py | 0 .../regression_tests/random_ray_cube/test.py | 231 ++++ .../cell/inputs_true.dat | 1 + .../cell/results_true.dat | 86 +- .../material/inputs_true.dat | 1 + .../material/results_true.dat | 84 +- .../random_ray_fixed_source/test.py | 1 + .../universe/inputs_true.dat | 1 + .../universe/results_true.dat | 84 +- .../random_ray_vacuum/inputs_true.dat | 1 + .../random_ray_vacuum/test.py | 1 + 23 files changed, 2573 insertions(+), 155 deletions(-) create mode 100644 tests/regression_tests/random_ray_cube/False_hybrid/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_cube/False_hybrid/results_true.dat create mode 100644 tests/regression_tests/random_ray_cube/True_hybrid/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_cube/True_hybrid/results_true.dat create mode 100644 tests/regression_tests/random_ray_cube/__init__.py create mode 100644 tests/regression_tests/random_ray_cube/test.py diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst index 7a29a187516..45af2b0c894 100644 --- a/docs/source/methods/random_ray.rst +++ b/docs/source/methods/random_ray.rst @@ -740,6 +740,8 @@ scalar flux value for the FSR). global::volume[fsr] += s; } +.. _methods_random_tallies: + ------------------------ How are Tallies Handled? ------------------------ @@ -757,6 +759,18 @@ behavior if a single simulation cell is able to score to multiple filter mesh cells. In the future, the capability to fully support mesh tallies may be added to OpenMC, but for now this restriction needs to be respected. +Flux tallies are handled slightly differently than in Monte Carlo. By default, +in MC, flux tallies are reported in units of tracklength (cm), so must be +manually normalized by volume by the user to produce an estimate of flux in +units of cm\ :sup:`-2`\. Alternatively, MC flux tallies can be normalized via a +separated volume calculation process as discussed in the :ref:`Volume +Calculation Section`. In random ray, as the volumes are +computed on-the-fly as part of the transport process, the flux tallies can +easily be reported either in units of flux (cm\ :sup:`-2`\) or tracklength (cm). +By default, the unnormalized flux values (units of cm) will be reported. If the +user wishes to received volume normalized flux tallies, then an option for this +is available, as described in the :ref:`User Guide`. + .. _methods-shannon-entropy-random-ray: ----------------------------- diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index 2a038b12257..6df132859c7 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -330,6 +330,8 @@ of a two-dimensional 2x2 reflective pincell lattice: In the future, automated subdivision of FSRs via mesh overlay may be supported. +.. _usersguide_flux_norm: + ------- Tallies ------- @@ -367,6 +369,25 @@ Note that there is no difference between the analog, tracklength, and collision estimators in random ray mode as individual particles are not being simulated. Tracklength-style tally estimation is inherent to the random ray method. +As discussed in the random ray theory section on :ref:`Random Ray +Tallies`, by default flux tallies in the random ray mode +are not normalized by the spatial tally volumes such that flux tallies are in +units of cm. While the volume information is readily available as a byproduct of +random ray integration, the flux value is reported in unnormalized units of cm +so that the user will be able to compare "apples to apples" with the default +flux tallies from the Monte Carlo solver (also reported by default in units of +cm). If volume normalized flux tallies (in units of cm\ :sup:`-2`) are desired, +then the user can set the ``volume_normalized_flux_tallies`` field in the +:attr:`openmc.Settings.random_ray` dictionary to ``True``. An example is given +below: + +:: + + settings.random_ray['volume_normalized_flux_tallies'] = True + +Note that MC mode flux tallies can also be normalized by volume, as discussed in +the :ref:`Volume Calculation Section` of the user guide. + -------- Plotting -------- diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index badbb25fc2c..51938404037 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -108,6 +108,11 @@ class FlatSourceDomain { void all_reduce_replicated_source_regions(); void convert_external_sources(); void count_external_source_regions(); + double compute_fixed_source_normalization_factor() const; + + //---------------------------------------------------------------------------- + // Static Data members + static bool volume_normalized_flux_tallies_; //---------------------------------------------------------------------------- // Public Data members diff --git a/openmc/settings.py b/openmc/settings.py index fae6f638364..b048c3a8b34 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -157,6 +157,12 @@ class Settings: :ray_source: Starting ray distribution (must be uniform in space and angle) as specified by a :class:`openmc.SourceBase` object. + :volume_normalized_flux_tallies: + Whether to normalize flux tallies by volume (bool). The default + is 'False'. When enabled, flux tallies will be reported in units of + cm/cm^3. When disabled, flux tallies will be reported in units + of cm (i.e., total distance traveled by neutrons in the spatial + tally region). .. versionadded:: 0.15.0 resonance_scattering : dict @@ -1084,6 +1090,8 @@ def random_ray(self, random_ray: dict): random_ray[key], 0.0, True) elif key == 'ray_source': cv.check_type('random ray source', random_ray[key], SourceBase) + elif key == 'volume_normalized_flux_tallies': + cv.check_type('volume normalized flux tallies', random_ray[key], bool) else: raise ValueError(f'Unable to set random ray to "{key}" which is ' 'unsupported by OpenMC') @@ -1877,6 +1885,10 @@ def _random_ray_from_xml_element(self, root): elif child.tag == 'source': source = SourceBase.from_xml_element(child) self.random_ray['ray_source'] = source + elif child.tag == 'volume_normalized_flux_tallies': + self.random_ray['volume_normalized_flux_tallies'] = ( + child.text in ('true', '1') + ) def to_xml_element(self, mesh_memo=None): """Create a 'settings' element to be written to an XML file. diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 792e97da0c1..29b72c77bed 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -23,6 +23,9 @@ namespace openmc { // FlatSourceDomain implementation //============================================================================== +// Static Variable Declarations +bool FlatSourceDomain::volume_normalized_flux_tallies_ {false}; + FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) { // Count the number of source regions, compute the cell offset @@ -81,15 +84,17 @@ FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) } // Initialize tally volumes - tally_volumes_.resize(model::tallies.size()); - for (int i = 0; i < model::tallies.size(); i++) { - // Get the shape of the 3D result tensor - auto shape = model::tallies[i]->results().shape(); - - // Create a new 2D tensor with the same size as the first - // two dimensions of the 3D tensor - tally_volumes_[i] = - xt::xtensor::from_shape({shape[0], shape[1]}); + if (volume_normalized_flux_tallies_) { + tally_volumes_.resize(model::tallies.size()); + for (int i = 0; i < model::tallies.size(); i++) { + // Get the shape of the 3D result tensor + auto shape = model::tallies[i]->results().shape(); + + // Create a new 2D tensor with the same size as the first + // two dimensions of the 3D tensor + tally_volumes_[i] = + xt::xtensor::from_shape({shape[0], shape[1]}); + } } // Compute simulation domain volume based on ray source @@ -462,11 +467,63 @@ void FlatSourceDomain::convert_source_regions_to_tallies() // Set the volume accumulators to zero for all tallies void FlatSourceDomain::reset_tally_volumes() { + if (volume_normalized_flux_tallies_) { #pragma omp parallel for - for (int i = 0; i < tally_volumes_.size(); i++) { - auto& tensor = tally_volumes_[i]; - tensor.fill(0.0); // Set all elements of the tensor to 0.0 + for (int i = 0; i < tally_volumes_.size(); i++) { + auto& tensor = tally_volumes_[i]; + tensor.fill(0.0); // Set all elements of the tensor to 0.0 + } + } +} + +// In fixed source mode, due to the way that volumetric fixed sources are +// converted and applied as volumetric sources in one or more source regions, +// we need to perform an additional normalization step to ensure that the +// reported scalar fluxes are in units per source neutron. This allows for +// direct comparison of reported tallies to Monte Carlo flux results. +// This factor needs to be computed at each iteration, as it is based on the +// volume estimate of each FSR, which improves over the course of the simulation +double FlatSourceDomain::compute_fixed_source_normalization_factor() const +{ + // If we are not in fixed source mode, then there are no external sources + // so no normalization is needed. + if (settings::run_mode != RunMode::FIXED_SOURCE) { + return 1.0; + } + + // Step 1 is to sum over all source regions and energy groups to get the + // total external source strength in the simulation. + double simulation_external_source_strength = 0.0; +#pragma omp parallel for reduction(+ : simulation_external_source_strength) + for (int sr = 0; sr < n_source_regions_; sr++) { + int material = material_[sr]; + double volume = volume_[sr] * simulation_volume_; + for (int e = 0; e < negroups_; e++) { + // Temperature and angle indices, if using multiple temperature + // data sets and/or anisotropic data sets. + // TODO: Currently assumes we are only using single temp/single + // angle data. + const int t = 0; + const int a = 0; + float sigma_t = data::mg.macro_xs_[material].get_xs( + MgxsType::TOTAL, e, nullptr, nullptr, nullptr, t, a); + simulation_external_source_strength += + external_source_[sr * negroups_ + e] * sigma_t * volume; + } + } + + // Step 2 is to determine the total user-specified external source strength + double user_external_source_strength = 0.0; + for (auto& ext_source : model::external_sources) { + user_external_source_strength += ext_source->strength(); } + + // The correction factor is the ratio of the user-specified external source + // strength to the simulation external source strength. + double source_normalization_factor = + user_external_source_strength / simulation_external_source_strength; + + return source_normalization_factor; } // Tallying in random ray is not done directly during transport, rather, @@ -492,6 +549,9 @@ void FlatSourceDomain::random_ray_tally() const int t = 0; const int a = 0; + double source_normalization_factor = + compute_fixed_source_normalization_factor(); + // We loop over all source regions and energy groups. For each // element, we check if there are any scores needed and apply // them. @@ -510,7 +570,7 @@ void FlatSourceDomain::random_ray_tally() double material = material_[sr]; for (int g = 0; g < negroups_; g++) { int idx = sr * negroups_ + g; - double flux = scalar_flux_new_[idx]; + double flux = scalar_flux_new_[idx] * source_normalization_factor; // Determine numerical score value for (auto& task : tally_task_[idx]) { @@ -561,11 +621,13 @@ void FlatSourceDomain::random_ray_tally() // For flux tallies, the total volume of the spatial region is needed // for normalizing the flux. We store this volume in a separate tensor. // We only contribute to each volume tally bin once per FSR. - for (const auto& task : volume_task_[sr]) { - if (task.score_type == SCORE_FLUX) { + if (volume_normalized_flux_tallies_) { + for (const auto& task : volume_task_[sr]) { + if (task.score_type == SCORE_FLUX) { #pragma omp atomic - tally_volumes_[task.tally_idx](task.filter_idx, task.score_idx) += - volume; + tally_volumes_[task.tally_idx](task.filter_idx, task.score_idx) += + volume; + } } } } // end FSR loop @@ -575,16 +637,18 @@ void FlatSourceDomain::random_ray_tally() // and then scores. For each score, we check the tally data structure to // see what index that score corresponds to. If that score is a flux score, // then we divide it by volume. - for (int i = 0; i < model::tallies.size(); i++) { - Tally& tally {*model::tallies[i]}; + if (volume_normalized_flux_tallies_) { + for (int i = 0; i < model::tallies.size(); i++) { + Tally& tally {*model::tallies[i]}; #pragma omp parallel for - for (int bin = 0; bin < tally.n_filter_bins(); bin++) { - for (int score_idx = 0; score_idx < tally.n_scores(); score_idx++) { - auto score_type = tally.scores_[score_idx]; - if (score_type == SCORE_FLUX) { - double vol = tally_volumes_[i](bin, score_idx); - if (vol > 0.0) { - tally.results_(bin, score_idx, TallyResult::VALUE) /= vol; + for (int bin = 0; bin < tally.n_filter_bins(); bin++) { + for (int score_idx = 0; score_idx < tally.n_scores(); score_idx++) { + auto score_type = tally.scores_[score_idx]; + if (score_type == SCORE_FLUX) { + double vol = tally_volumes_[i](bin, score_idx); + if (vol > 0.0) { + tally.results_(bin, score_idx, TallyResult::VALUE) /= vol; + } } } } @@ -767,6 +831,9 @@ void FlatSourceDomain::output_to_vtk() const } } + double source_normalization_factor = + compute_fixed_source_normalization_factor(); + // Open file for writing std::FILE* plot = std::fopen(filename.c_str(), "wb"); @@ -786,7 +853,8 @@ void FlatSourceDomain::output_to_vtk() const std::fprintf(plot, "LOOKUP_TABLE default\n"); for (int fsr : voxel_indices) { int64_t source_element = fsr * negroups_ + g; - float flux = scalar_flux_final_[source_element]; + float flux = + scalar_flux_final_[source_element] * source_normalization_factor; flux /= (settings::n_batches - settings::n_inactive); flux = convert_to_big_endian(flux); std::fwrite(&flux, sizeof(float), 1, plot); @@ -819,7 +887,8 @@ void FlatSourceDomain::output_to_vtk() const int mat = material_[fsr]; for (int g = 0; g < negroups_; g++) { int64_t source_element = fsr * negroups_ + g; - float flux = scalar_flux_final_[source_element]; + float flux = + scalar_flux_final_[source_element] * source_normalization_factor; flux /= (settings::n_batches - settings::n_inactive); float Sigma_f = data::mg.macro_xs_[mat].get_xs( MgxsType::FISSION, g, nullptr, nullptr, nullptr, 0, 0); diff --git a/src/settings.cpp b/src/settings.cpp index 4ffa50cdf45..b92b77df937 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -269,6 +269,10 @@ void get_run_parameters(pugi::xml_node node_base) } else { fatal_error("Specify random ray source in settings XML"); } + if (check_for_node(random_ray_node, "volume_normalized_flux_tallies")) { + FlatSourceDomain::volume_normalized_flux_tallies_ = + get_node_value_bool(random_ray_node, "volume_normalized_flux_tallies"); + } } } diff --git a/tests/regression_tests/random_ray_basic/inputs_true.dat b/tests/regression_tests/random_ray_basic/inputs_true.dat index 97b7906f7b2..a2b058f2bdb 100644 --- a/tests/regression_tests/random_ray_basic/inputs_true.dat +++ b/tests/regression_tests/random_ray_basic/inputs_true.dat @@ -85,6 +85,7 @@ -1.26 -1.26 -1 1.26 1.26 1 + True diff --git a/tests/regression_tests/random_ray_basic/test.py b/tests/regression_tests/random_ray_basic/test.py index 1727a63716c..c00d25880c5 100644 --- a/tests/regression_tests/random_ray_basic/test.py +++ b/tests/regression_tests/random_ray_basic/test.py @@ -190,6 +190,7 @@ def random_ray_model() -> openmc.Model: settings.random_ray['distance_active'] = 100.0 settings.random_ray['distance_inactive'] = 20.0 settings.random_ray['ray_source'] = rr_source + settings.random_ray['volume_normalized_flux_tallies'] = True ############################################################################### # Define tallies diff --git a/tests/regression_tests/random_ray_cube/False_hybrid/inputs_true.dat b/tests/regression_tests/random_ray_cube/False_hybrid/inputs_true.dat new file mode 100644 index 00000000000..82c24291b30 --- /dev/null +++ b/tests/regression_tests/random_ray_cube/False_hybrid/inputs_true.dat @@ -0,0 +1,1018 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 1.0 1.0 1.0 + 30 30 30 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 10 + 5 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + False + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_cube/False_hybrid/results_true.dat b/tests/regression_tests/random_ray_cube/False_hybrid/results_true.dat new file mode 100644 index 00000000000..88718c5bfbc --- /dev/null +++ b/tests/regression_tests/random_ray_cube/False_hybrid/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +-5.920549E+04 +2.321317E+09 +tally 2: +4.761087E+02 +4.909570E+04 +tally 3: +2.206028E+01 +1.019164E+02 diff --git a/tests/regression_tests/random_ray_cube/True_hybrid/inputs_true.dat b/tests/regression_tests/random_ray_cube/True_hybrid/inputs_true.dat new file mode 100644 index 00000000000..464c3440493 --- /dev/null +++ b/tests/regression_tests/random_ray_cube/True_hybrid/inputs_true.dat @@ -0,0 +1,1018 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 1.0 1.0 1.0 + 30 30 30 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 10 + 5 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_cube/True_hybrid/results_true.dat b/tests/regression_tests/random_ray_cube/True_hybrid/results_true.dat new file mode 100644 index 00000000000..4a29967407d --- /dev/null +++ b/tests/regression_tests/random_ray_cube/True_hybrid/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +-5.230164E+02 +1.818988E+05 +tally 2: +3.067508E-02 +2.037701E-04 +tally 3: +1.941220E-03 +7.892297E-07 diff --git a/tests/regression_tests/random_ray_cube/__init__.py b/tests/regression_tests/random_ray_cube/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regression_tests/random_ray_cube/test.py b/tests/regression_tests/random_ray_cube/test.py new file mode 100644 index 00000000000..51d61ae797c --- /dev/null +++ b/tests/regression_tests/random_ray_cube/test.py @@ -0,0 +1,231 @@ +import os + +import numpy as np +import openmc +from openmc.utility_funcs import change_directory +import pytest + +from tests.testing_harness import TolerantPyAPITestHarness + +# Creates a 3 region cube with different fills in each region +def fill_cube(N, n_1, n_2, fill_1, fill_2, fill_3): + cube = [[[0 for _ in range(N)] for _ in range(N)] for _ in range(N)] + for i in range(N): + for j in range(N): + for k in range(N): + if i < n_1 and j >= (N-n_1) and k < n_1: + cube[i][j][k] = fill_1 + elif i < n_2 and j >= (N-n_2) and k < n_2: + cube[i][j][k] = fill_2 + else: + cube[i][j][k] = fill_3 + return cube + +class MGXSTestHarness(TolerantPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + +def create_random_ray_model(volume_normalize, estimator): + openmc.reset_auto_ids() + ############################################################################### + # Create multigroup data + + # Instantiate the energy group data + ebins = [1e-5, 20.0e6] + groups = openmc.mgxs.EnergyGroups(group_edges=ebins) + + void_sigma_a = 4.0e-6 + void_sigma_s = 3.0e-4 + void_mat_data = openmc.XSdata('void', groups) + void_mat_data.order = 0 + void_mat_data.set_total([void_sigma_a + void_sigma_s]) + void_mat_data.set_absorption([void_sigma_a]) + void_mat_data.set_scatter_matrix(np.rollaxis(np.array([[[void_sigma_s]]]),0,3)) + + absorber_sigma_a = 0.75 + absorber_sigma_s = 0.25 + absorber_mat_data = openmc.XSdata('absorber', groups) + absorber_mat_data.order = 0 + absorber_mat_data.set_total([absorber_sigma_a + absorber_sigma_s]) + absorber_mat_data.set_absorption([absorber_sigma_a]) + absorber_mat_data.set_scatter_matrix(np.rollaxis(np.array([[[absorber_sigma_s]]]),0,3)) + + multiplier = 0.1 + source_sigma_a = void_sigma_a * multiplier + source_sigma_s = void_sigma_s * multiplier + source_mat_data = openmc.XSdata('source', groups) + source_mat_data.order = 0 + print(source_sigma_a + source_sigma_s) + source_mat_data.set_total([source_sigma_a + source_sigma_s]) + source_mat_data.set_absorption([source_sigma_a]) + source_mat_data.set_scatter_matrix(np.rollaxis(np.array([[[source_sigma_s]]]),0,3)) + + mg_cross_sections_file = openmc.MGXSLibrary(groups) + mg_cross_sections_file.add_xsdatas([source_mat_data, void_mat_data, absorber_mat_data]) + mg_cross_sections_file.export_to_hdf5() + + ############################################################################### + # Create materials for the problem + + # Instantiate some Macroscopic Data + source_data = openmc.Macroscopic('source') + void_data = openmc.Macroscopic('void') + absorber_data = openmc.Macroscopic('absorber') + + # Instantiate some Materials and register the appropriate Macroscopic objects + source_mat = openmc.Material(name='source') + source_mat.set_density('macro', 1.0) + source_mat.add_macroscopic(source_data) + + void_mat = openmc.Material(name='void') + void_mat.set_density('macro', 1.0) + void_mat.add_macroscopic(void_data) + + absorber_mat = openmc.Material(name='absorber') + absorber_mat.set_density('macro', 1.0) + absorber_mat.add_macroscopic(absorber_data) + + # Instantiate a Materials collection and export to XML + materials_file = openmc.Materials([source_mat, void_mat, absorber_mat]) + materials_file.cross_sections = "mgxs.h5" + + ############################################################################### + # Define problem geometry + + source_cell = openmc.Cell(fill=source_mat, name='infinite source region') + void_cell = openmc.Cell(fill=void_mat, name='infinite void region') + absorber_cell = openmc.Cell(fill=absorber_mat, name='infinite absorber region') + + source_universe = openmc.Universe() + source_universe.add_cells([source_cell]) + + void_universe = openmc.Universe() + void_universe.add_cells([void_cell]) + + absorber_universe = openmc.Universe() + absorber_universe.add_cells([absorber_cell]) + + absorber_width = 30.0 + n_base = 6 + + # This variable can be increased above 1 to refine the FSR mesh resolution further + refinement_level = 5 + + n = n_base * refinement_level + pitch = absorber_width / n + + pattern = fill_cube(n, 1*refinement_level, 5*refinement_level, source_universe, void_universe, absorber_universe) + + lattice = openmc.RectLattice() + lattice.lower_left = [0.0, 0.0, 0.0] + lattice.pitch = [pitch, pitch, pitch] + lattice.universes = pattern + #print(lattice) + + lattice_cell = openmc.Cell(fill=lattice) + + lattice_uni = openmc.Universe() + lattice_uni.add_cells([lattice_cell]) + + x_low = openmc.XPlane(x0=0.0,boundary_type='reflective') + x_high = openmc.XPlane(x0=absorber_width,boundary_type='vacuum') + + y_low = openmc.YPlane(y0=0.0,boundary_type='reflective') + y_high = openmc.YPlane(y0=absorber_width,boundary_type='vacuum') + + z_low = openmc.ZPlane(z0=0.0,boundary_type='reflective') + z_high = openmc.ZPlane(z0=absorber_width,boundary_type='vacuum') + + full_domain = openmc.Cell(fill=lattice_uni, region=+x_low & -x_high & +y_low & -y_high & +z_low & -z_high, name='full domain') + + root = openmc.Universe(name='root universe') + root.add_cell(full_domain) + + # Create a geometry with the two cells and export to XML + geometry = openmc.Geometry(root) + + ############################################################################### + # Define problem settings + + # Instantiate a Settings object, set all runtime parameters, and export to XML + settings = openmc.Settings() + settings.energy_mode = "multi-group" + settings.inactive = 5 + settings.batches = 10 + settings.particles = 90 + settings.run_mode = 'fixed source' + + # Create an initial uniform spatial source for ray integration + lower_left_ray = [0.0, 0.0, 0.0] + upper_right_ray = [absorber_width, absorber_width, absorber_width] + uniform_dist_ray = openmc.stats.Box(lower_left_ray, upper_right_ray, only_fissionable=False) + rr_source = openmc.IndependentSource(space=uniform_dist_ray) + + settings.random_ray['distance_active'] = 500.0 + settings.random_ray['distance_inactive'] = 100.0 + settings.random_ray['ray_source'] = rr_source + settings.random_ray['volume_normalized_flux_tallies'] = volume_normalize + + #settings.random_ray['volume_estimator'] = estimator + + # Create the neutron source in the bottom right of the moderator + strengths = [1.0] # Good - fast group appears largest (besides most thermal) + midpoints = [100.0] + energy_distribution = openmc.stats.Discrete(x=midpoints,p=strengths) + + source = openmc.IndependentSource(energy=energy_distribution, constraints={'domains':[source_universe]}, strength=3.14) + + settings.source = [source] + + ############################################################################### + # Define tallies + + estimator = 'tracklength' + + absorber_filter = openmc.MaterialFilter(absorber_mat) + absorber_tally = openmc.Tally(name="Absorber Tally") + absorber_tally.filters = [absorber_filter] + absorber_tally.scores = ['flux'] + absorber_tally.estimator = estimator + + void_filter = openmc.MaterialFilter(void_mat) + void_tally = openmc.Tally(name="Void Tally") + void_tally.filters = [void_filter] + void_tally.scores = ['flux'] + void_tally.estimator = estimator + + source_filter = openmc.MaterialFilter(source_mat) + source_tally = openmc.Tally(name="Source Tally") + source_tally.filters = [source_filter] + source_tally.scores = ['flux'] + source_tally.estimator = estimator + + # Instantiate a Tallies collection and export to XML + tallies = openmc.Tallies([source_tally, void_tally, absorber_tally]) + + ############################################################################### + # Assmble Model + + model = openmc.model.Model() + model.geometry = geometry + model.materials = materials_file + model.settings = settings + model.xs_data = mg_cross_sections_file + model.tallies = tallies + + return model + +@pytest.mark.parametrize("volume_normalize, estimator", [ + (False, "hybrid"), + (True, "hybrid") +]) +def test_random_ray_cube(volume_normalize, estimator): + # Generating a unique directory name from the parameters + directory_name = f"{volume_normalize}_{estimator}" + with change_directory(directory_name): + model = create_random_ray_model(volume_normalize, estimator) + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() \ No newline at end of file diff --git a/tests/regression_tests/random_ray_fixed_source/cell/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source/cell/inputs_true.dat index 99e67745a26..a8eaa009661 100644 --- a/tests/regression_tests/random_ray_fixed_source/cell/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source/cell/inputs_true.dat @@ -153,6 +153,7 @@ 400.0 100.0 + False 0.0 0.0 0.0 60.0 100.0 60.0 diff --git a/tests/regression_tests/random_ray_fixed_source/cell/results_true.dat b/tests/regression_tests/random_ray_fixed_source/cell/results_true.dat index df923ab236a..ba1254d384d 100644 --- a/tests/regression_tests/random_ray_fixed_source/cell/results_true.dat +++ b/tests/regression_tests/random_ray_fixed_source/cell/results_true.dat @@ -1,47 +1,47 @@ tally 1: -3.751048E+01 +3.751047E+01 2.841797E+02 -9.930788E+00 -1.988180E+01 -3.781121E+00 -3.005165E+00 -2.383139E+00 -1.232560E+00 -1.561884E+00 -6.440577E-01 -1.089787E+00 -3.724896E-01 -6.608456E-01 -1.285592E-01 -2.372611E-01 -1.601299E-02 -7.814803E-02 -1.765829E-03 -2.862108E-02 -2.460129E-04 +1.000671E+01 +2.020214E+01 +3.979569E+00 +3.330838E+00 +2.552971E+00 +1.407542E+00 +1.736764E+00 +7.948004E-01 +1.178834E+00 +4.328006E-01 +6.953895E-01 +1.408391E-01 +2.389347E-01 +1.617450E-02 +8.128516E-02 +1.903860E-03 +2.764062E-02 +2.285458E-04 tally 2: -1.089787E+00 -3.724896E-01 -3.767926E-01 -3.724399E-02 -8.614121E-02 -1.526889E-03 -3.610725E-02 -2.629885E-04 -1.466261E-02 -4.536997E-05 -4.653106E-03 -4.381672E-06 +1.178834E+00 +4.328006E-01 +3.982124E-01 +4.138078E-02 +9.676374E-02 +1.919297E-03 +3.944531E-02 +3.133733E-04 +1.554518E-02 +5.084822E-05 +4.815079E-03 +4.681450E-06 tally 3: -1.617918E-03 -6.317049E-07 -1.161473E-03 -2.789553E-07 -1.198879E-03 -3.189531E-07 -1.031737E-03 -2.207381E-07 -5.466329E-04 -6.166808E-08 -2.146062E-04 -9.937520E-09 +1.635823E-03 +6.482611E-07 +1.248235E-03 +3.227247E-07 +1.248195E-03 +3.488233E-07 +1.030935E-03 +2.206418E-07 +5.670315E-04 +6.648507E-08 +2.393638E-04 +1.242517E-08 diff --git a/tests/regression_tests/random_ray_fixed_source/material/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source/material/inputs_true.dat index 7fde0c419b5..e3ad3703b6e 100644 --- a/tests/regression_tests/random_ray_fixed_source/material/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source/material/inputs_true.dat @@ -153,6 +153,7 @@ 400.0 100.0 + False 0.0 0.0 0.0 60.0 100.0 60.0 diff --git a/tests/regression_tests/random_ray_fixed_source/material/results_true.dat b/tests/regression_tests/random_ray_fixed_source/material/results_true.dat index e3d52890919..ba1254d384d 100644 --- a/tests/regression_tests/random_ray_fixed_source/material/results_true.dat +++ b/tests/regression_tests/random_ray_fixed_source/material/results_true.dat @@ -1,47 +1,47 @@ tally 1: 3.751047E+01 2.841797E+02 -9.930788E+00 -1.988181E+01 -3.781121E+00 -3.005165E+00 -2.383139E+00 -1.232560E+00 -1.561884E+00 -6.440577E-01 -1.089787E+00 -3.724896E-01 -6.608456E-01 -1.285592E-01 -2.372611E-01 -1.601299E-02 -7.814803E-02 -1.765829E-03 -2.862108E-02 -2.460129E-04 +1.000671E+01 +2.020214E+01 +3.979569E+00 +3.330838E+00 +2.552971E+00 +1.407542E+00 +1.736764E+00 +7.948004E-01 +1.178834E+00 +4.328006E-01 +6.953895E-01 +1.408391E-01 +2.389347E-01 +1.617450E-02 +8.128516E-02 +1.903860E-03 +2.764062E-02 +2.285458E-04 tally 2: -1.089787E+00 -3.724896E-01 -3.767925E-01 -3.724398E-02 -8.614120E-02 -1.526889E-03 -3.610725E-02 -2.629885E-04 -1.466261E-02 -4.536997E-05 -4.653106E-03 -4.381672E-06 +1.178834E+00 +4.328006E-01 +3.982124E-01 +4.138078E-02 +9.676374E-02 +1.919297E-03 +3.944531E-02 +3.133733E-04 +1.554518E-02 +5.084822E-05 +4.815079E-03 +4.681450E-06 tally 3: -1.617918E-03 -6.317049E-07 -1.161473E-03 -2.789553E-07 -1.198879E-03 -3.189531E-07 -1.031737E-03 -2.207381E-07 -5.466329E-04 -6.166809E-08 -2.146062E-04 -9.937520E-09 +1.635823E-03 +6.482611E-07 +1.248235E-03 +3.227247E-07 +1.248195E-03 +3.488233E-07 +1.030935E-03 +2.206418E-07 +5.670315E-04 +6.648507E-08 +2.393638E-04 +1.242517E-08 diff --git a/tests/regression_tests/random_ray_fixed_source/test.py b/tests/regression_tests/random_ray_fixed_source/test.py index 41a6c35bb03..7ceadca5b93 100644 --- a/tests/regression_tests/random_ray_fixed_source/test.py +++ b/tests/regression_tests/random_ray_fixed_source/test.py @@ -251,6 +251,7 @@ def create_random_ray_model(domain_type): settings.random_ray['distance_active'] = 400.0 settings.random_ray['distance_inactive'] = 100.0 + settings.random_ray['volume_normalized_flux_tallies'] = False # Create an initial uniform spatial source for ray integration lower_left = (0.0, 0.0, 0.0) diff --git a/tests/regression_tests/random_ray_fixed_source/universe/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source/universe/inputs_true.dat index 349917fee72..efbc03adf8c 100644 --- a/tests/regression_tests/random_ray_fixed_source/universe/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source/universe/inputs_true.dat @@ -153,6 +153,7 @@ 400.0 100.0 + False 0.0 0.0 0.0 60.0 100.0 60.0 diff --git a/tests/regression_tests/random_ray_fixed_source/universe/results_true.dat b/tests/regression_tests/random_ray_fixed_source/universe/results_true.dat index 36d24e939af..ba1254d384d 100644 --- a/tests/regression_tests/random_ray_fixed_source/universe/results_true.dat +++ b/tests/regression_tests/random_ray_fixed_source/universe/results_true.dat @@ -1,47 +1,47 @@ tally 1: 3.751047E+01 2.841797E+02 -9.930788E+00 -1.988180E+01 -3.781121E+00 -3.005165E+00 -2.383139E+00 -1.232560E+00 -1.561884E+00 -6.440577E-01 -1.089787E+00 -3.724896E-01 -6.608456E-01 -1.285592E-01 -2.372611E-01 -1.601299E-02 -7.814803E-02 -1.765829E-03 -2.862107E-02 -2.460129E-04 +1.000671E+01 +2.020214E+01 +3.979569E+00 +3.330838E+00 +2.552971E+00 +1.407542E+00 +1.736764E+00 +7.948004E-01 +1.178834E+00 +4.328006E-01 +6.953895E-01 +1.408391E-01 +2.389347E-01 +1.617450E-02 +8.128516E-02 +1.903860E-03 +2.764062E-02 +2.285458E-04 tally 2: -1.089787E+00 -3.724896E-01 -3.767926E-01 -3.724399E-02 -8.614120E-02 -1.526889E-03 -3.610725E-02 -2.629885E-04 -1.466261E-02 -4.536997E-05 -4.653106E-03 -4.381672E-06 +1.178834E+00 +4.328006E-01 +3.982124E-01 +4.138078E-02 +9.676374E-02 +1.919297E-03 +3.944531E-02 +3.133733E-04 +1.554518E-02 +5.084822E-05 +4.815079E-03 +4.681450E-06 tally 3: -1.617918E-03 -6.317049E-07 -1.161473E-03 -2.789553E-07 -1.198879E-03 -3.189531E-07 -1.031737E-03 -2.207381E-07 -5.466329E-04 -6.166808E-08 -2.146062E-04 -9.937520E-09 +1.635823E-03 +6.482611E-07 +1.248235E-03 +3.227247E-07 +1.248195E-03 +3.488233E-07 +1.030935E-03 +2.206418E-07 +5.670315E-04 +6.648507E-08 +2.393638E-04 +1.242517E-08 diff --git a/tests/regression_tests/random_ray_vacuum/inputs_true.dat b/tests/regression_tests/random_ray_vacuum/inputs_true.dat index 4ef10942005..5b2fa090596 100644 --- a/tests/regression_tests/random_ray_vacuum/inputs_true.dat +++ b/tests/regression_tests/random_ray_vacuum/inputs_true.dat @@ -85,6 +85,7 @@ -1.26 -1.26 -1 1.26 1.26 1 + True diff --git a/tests/regression_tests/random_ray_vacuum/test.py b/tests/regression_tests/random_ray_vacuum/test.py index e9ca2252144..3923b8a815f 100644 --- a/tests/regression_tests/random_ray_vacuum/test.py +++ b/tests/regression_tests/random_ray_vacuum/test.py @@ -193,6 +193,7 @@ def random_ray_model() -> openmc.Model: settings.random_ray['distance_active'] = 100.0 settings.random_ray['distance_inactive'] = 20.0 settings.random_ray['ray_source'] = rr_source + settings.random_ray['volume_normalized_flux_tallies'] = True ############################################################################### # Define tallies From 653d8835dcefb6d4f46fab9695feec168e62302a Mon Sep 17 00:00:00 2001 From: John Vincent Cauilan <64677361+johvincau@users.noreply.github.com> Date: Wed, 3 Jul 2024 12:44:20 -0500 Subject: [PATCH 031/184] Add missing show_overlaps option to plots.xml input file documentation (#3068) --- docs/source/io_formats/plots.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/source/io_formats/plots.rst b/docs/source/io_formats/plots.rst index e6b75eafcb6..04d5720c84f 100644 --- a/docs/source/io_formats/plots.rst +++ b/docs/source/io_formats/plots.rst @@ -151,6 +151,18 @@ attributes or sub-elements. These are not used in "voxel" plots: *Default*: 255 255 255 (white) + :show_overlaps: + Indicates whether overlapping regions of different cells are shown. + + *Default*: None + + :overlap_color: + Specifies the RGB color of overlapping regions of different cells. Does not + do anything if ``show_overlaps`` is "false" or not specified. Should be 3 + integers separated by spaces. + + *Default*: 255 0 0 (red) + :meshlines: The ``meshlines`` sub-element allows for plotting the boundaries of a regular mesh on top of a plot. Only one ``meshlines`` element is allowed per From 1b22dd28d40a9f248ffcc26deab9427c8921d497 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Wed, 3 Jul 2024 13:28:06 -0500 Subject: [PATCH 032/184] Random Ray Testing Simplification (#3061) Co-authored-by: Paul Romano --- openmc/__init__.py | 3 +- openmc/examples.py | 529 ++++++++- .../regression_tests/random_ray_basic/test.py | 233 ---- .../False_hybrid/inputs_true.dat | 1018 ----------------- .../False_hybrid/results_true.dat | 9 - .../True_hybrid/inputs_true.dat | 1018 ----------------- .../True_hybrid/results_true.dat | 9 - .../regression_tests/random_ray_cube/test.py | 231 ---- .../cell/inputs_true.dat | 205 ---- .../cell/results_true.dat | 47 - .../material/inputs_true.dat | 205 ---- .../material/results_true.dat | 47 - .../random_ray_fixed_source/test.py | 340 ------ .../universe/inputs_true.dat | 205 ---- .../universe/results_true.dat | 47 - .../__init__.py | 0 .../cell/inputs_true.dat | 244 ++++ .../cell/results_true.dat | 9 + .../material/inputs_true.dat | 244 ++++ .../material/results_true.dat | 9 + .../random_ray_fixed_source_domain/test.py | 51 + .../universe/inputs_true.dat | 244 ++++ .../universe/results_true.dat | 9 + .../False/inputs_true.dat | 244 ++++ .../False/results_true.dat | 9 + .../True/inputs_true.dat | 244 ++++ .../True/results_true.dat | 9 + .../__init__.py | 0 .../test.py | 27 + .../__init__.py | 0 .../inputs_true.dat | 0 .../results_true.dat | 0 .../regression_tests/random_ray_k_eff/test.py | 19 + .../random_ray_vacuum/__init__.py | 0 .../random_ray_vacuum/inputs_true.dat | 109 -- .../random_ray_vacuum/results_true.dat | 171 --- .../random_ray_vacuum/test.py | 236 ---- 37 files changed, 1869 insertions(+), 4155 deletions(-) delete mode 100644 tests/regression_tests/random_ray_basic/test.py delete mode 100644 tests/regression_tests/random_ray_cube/False_hybrid/inputs_true.dat delete mode 100644 tests/regression_tests/random_ray_cube/False_hybrid/results_true.dat delete mode 100644 tests/regression_tests/random_ray_cube/True_hybrid/inputs_true.dat delete mode 100644 tests/regression_tests/random_ray_cube/True_hybrid/results_true.dat delete mode 100644 tests/regression_tests/random_ray_cube/test.py delete mode 100644 tests/regression_tests/random_ray_fixed_source/cell/inputs_true.dat delete mode 100644 tests/regression_tests/random_ray_fixed_source/cell/results_true.dat delete mode 100644 tests/regression_tests/random_ray_fixed_source/material/inputs_true.dat delete mode 100644 tests/regression_tests/random_ray_fixed_source/material/results_true.dat delete mode 100644 tests/regression_tests/random_ray_fixed_source/test.py delete mode 100644 tests/regression_tests/random_ray_fixed_source/universe/inputs_true.dat delete mode 100644 tests/regression_tests/random_ray_fixed_source/universe/results_true.dat rename tests/regression_tests/{random_ray_basic => random_ray_fixed_source_domain}/__init__.py (100%) create mode 100644 tests/regression_tests/random_ray_fixed_source_domain/cell/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_domain/cell/results_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_domain/material/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_domain/material/results_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_domain/test.py create mode 100644 tests/regression_tests/random_ray_fixed_source_domain/universe/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_domain/universe/results_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_normalization/False/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_normalization/False/results_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_normalization/True/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_normalization/True/results_true.dat rename tests/regression_tests/{random_ray_cube => random_ray_fixed_source_normalization}/__init__.py (100%) create mode 100644 tests/regression_tests/random_ray_fixed_source_normalization/test.py rename tests/regression_tests/{random_ray_fixed_source => random_ray_k_eff}/__init__.py (100%) rename tests/regression_tests/{random_ray_basic => random_ray_k_eff}/inputs_true.dat (100%) rename tests/regression_tests/{random_ray_basic => random_ray_k_eff}/results_true.dat (100%) create mode 100644 tests/regression_tests/random_ray_k_eff/test.py delete mode 100644 tests/regression_tests/random_ray_vacuum/__init__.py delete mode 100644 tests/regression_tests/random_ray_vacuum/inputs_true.dat delete mode 100644 tests/regression_tests/random_ray_vacuum/results_true.dat delete mode 100644 tests/regression_tests/random_ray_vacuum/test.py diff --git a/openmc/__init__.py b/openmc/__init__.py index d18e1188e2a..e589d35efc9 100644 --- a/openmc/__init__.py +++ b/openmc/__init__.py @@ -32,11 +32,12 @@ from openmc.search import * from openmc.polynomial import * from openmc.tracks import * -from . import examples from .config import * # Import a few names from the model module from openmc.model import Model +from . import examples + __version__ = '0.15.1-dev' diff --git a/openmc/examples.py b/openmc/examples.py index fc94b8b528e..038c75ad214 100644 --- a/openmc/examples.py +++ b/openmc/examples.py @@ -3,10 +3,10 @@ import numpy as np import openmc -import openmc.model -def pwr_pin_cell(): + +def pwr_pin_cell() -> openmc.Model: """Create a PWR pin-cell model. This model is a single fuel pin with 2.4 w/o enriched UO2 corresponding to a @@ -16,11 +16,11 @@ def pwr_pin_cell(): Returns ------- - model : openmc.model.Model + model : openmc.Model A PWR pin-cell model """ - model = openmc.model.Model() + model = openmc.Model() # Define materials. fuel = openmc.Material(name='UO2 (2.4%)') @@ -77,7 +77,8 @@ def pwr_pin_cell(): model.settings.inactive = 5 model.settings.particles = 100 model.settings.source = openmc.IndependentSource( - space=openmc.stats.Box([-pitch/2, -pitch/2, -1], [pitch/2, pitch/2, 1]), + space=openmc.stats.Box([-pitch/2, -pitch/2, -1], + [pitch/2, pitch/2, 1]), constraints={'fissionable': True} ) @@ -89,7 +90,7 @@ def pwr_pin_cell(): return model -def pwr_core(): +def pwr_core() -> openmc.Model: """Create a PWR full-core model. This model is the OECD/NEA Monte Carlo Performance benchmark which is a @@ -99,11 +100,11 @@ def pwr_core(): Returns ------- - model : openmc.model.Model + model : openmc.Model Full-core PWR model """ - model = openmc.model.Model() + model = openmc.Model() # Define materials. fuel = openmc.Material(1, name='UOX fuel') @@ -166,7 +167,8 @@ def pwr_core(): lower_rad_ref.add_nuclide('Cr52', 0.145407678031, 'wo') lower_rad_ref.add_s_alpha_beta('c_H_in_H2O') - upper_rad_ref = openmc.Material(7, name='Upper radial reflector / Top plate region') + upper_rad_ref = openmc.Material( + 7, name='Upper radial reflector / Top plate region') upper_rad_ref.set_density('g/cm3', 4.28) upper_rad_ref.add_nuclide('H1', 0.0086117, 'wo') upper_rad_ref.add_nuclide('O16', 0.0683369, 'wo') @@ -313,13 +315,15 @@ def pwr_core(): 11, 11, 11, 11, 11, 13, 13, 14, 14, 14]) # Define fuel lattices. - l100 = openmc.RectLattice(name='Fuel assembly (lower half)', lattice_id=100) + l100 = openmc.RectLattice( + name='Fuel assembly (lower half)', lattice_id=100) l100.lower_left = (-10.71, -10.71) l100.pitch = (1.26, 1.26) l100.universes = np.tile(fuel_cold, (17, 17)) l100.universes[tube_x, tube_y] = tube_cold - l101 = openmc.RectLattice(name='Fuel assembly (upper half)', lattice_id=101) + l101 = openmc.RectLattice( + name='Fuel assembly (upper half)', lattice_id=101) l101.lower_left = (-10.71, -10.71) l101.pitch = (1.26, 1.26) l101.universes = np.tile(fuel_hot, (17, 17)) @@ -405,10 +409,14 @@ def pwr_core(): c6 = openmc.Cell(cell_id=6, fill=top_fa, region=-s5 & +s36 & -s37) c7 = openmc.Cell(cell_id=7, fill=top_nozzle, region=-s5 & +s37 & -s38) c8 = openmc.Cell(cell_id=8, fill=upper_rad_ref, region=-s7 & +s38 & -s39) - c9 = openmc.Cell(cell_id=9, fill=bot_nozzle, region=+s6 & -s7 & +s32 & -s38) - c10 = openmc.Cell(cell_id=10, fill=rpv_steel, region=+s7 & -s8 & +s31 & -s39) - c11 = openmc.Cell(cell_id=11, fill=lower_rad_ref, region=+s5 & -s6 & +s32 & -s34) - c12 = openmc.Cell(cell_id=12, fill=upper_rad_ref, region=+s5 & -s6 & +s36 & -s38) + c9 = openmc.Cell(cell_id=9, fill=bot_nozzle, + region=+s6 & -s7 & +s32 & -s38) + c10 = openmc.Cell(cell_id=10, fill=rpv_steel, + region=+s7 & -s8 & +s31 & -s39) + c11 = openmc.Cell(cell_id=11, fill=lower_rad_ref, + region=+s5 & -s6 & +s32 & -s34) + c12 = openmc.Cell(cell_id=12, fill=upper_rad_ref, + region=+s5 & -s6 & +s36 & -s38) root.add_cells((c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12)) # Assign root universe to geometry @@ -430,7 +438,7 @@ def pwr_core(): return model -def pwr_assembly(): +def pwr_assembly() -> openmc.Model: """Create a PWR assembly model. This model is a reflected 17x17 fuel assembly from the the `BEAVRS @@ -440,12 +448,12 @@ def pwr_assembly(): Returns ------- - model : openmc.model.Model + model : openmc.Model A PWR assembly model """ - model = openmc.model.Model() + model = openmc.Model() # Define materials. fuel = openmc.Material(name='Fuel') @@ -489,10 +497,10 @@ def pwr_assembly(): fuel_pin_universe = openmc.Universe(name='Fuel Pin') fuel_cell = openmc.Cell(name='fuel', fill=fuel, region=-fuel_or) clad_cell = openmc.Cell(name='clad', fill=clad, region=+fuel_or & -clad_or) - hot_water_cell = openmc.Cell(name='hot water', fill=hot_water, region=+clad_or) + hot_water_cell = openmc.Cell( + name='hot water', fill=hot_water, region=+clad_or) fuel_pin_universe.add_cells([fuel_cell, clad_cell, hot_water_cell]) - # Create a control rod guide tube universe guide_tube_universe = openmc.Universe(name='Guide Tube') gt_inner_cell = openmc.Cell(name='guide tube inner water', fill=hot_water, @@ -530,7 +538,8 @@ def pwr_assembly(): model.settings.inactive = 5 model.settings.particles = 100 model.settings.source = openmc.IndependentSource( - space=openmc.stats.Box([-pitch/2, -pitch/2, -1], [pitch/2, pitch/2, 1]), + space=openmc.stats.Box([-pitch/2, -pitch/2, -1], + [pitch/2, pitch/2, 1]), constraints={'fissionable': True} ) @@ -544,7 +553,7 @@ def pwr_assembly(): return model -def slab_mg(num_regions=1, mat_names=None, mgxslib_name='2g.h5'): +def slab_mg(num_regions=1, mat_names=None, mgxslib_name='2g.h5') -> openmc.Model: """Create a 1D slab model. Parameters @@ -561,7 +570,7 @@ def slab_mg(num_regions=1, mat_names=None, mgxslib_name='2g.h5'): Returns ------- - model : openmc.model.Model + model : openmc.Model One-group, 1D slab model """ @@ -637,10 +646,482 @@ def slab_mg(num_regions=1, mat_names=None, mgxslib_name='2g.h5'): settings_file.output = {'summary': False} - model = openmc.model.Model() + model = openmc.Model() model.geometry = geometry_file model.materials = materials_file model.settings = settings_file model.xs_data = macros return model + + +def random_ray_lattice() -> openmc.Model: + """Create a 2x2 PWR pincell asymmetrical lattic eexample. + + This model is a 2x2 reflective lattice of fuel pins with one of the lattice + locations having just moderator instead of a fuel pin. It uses 7 group + cross section data. + + Returns + ------- + model : openmc.Model + A PWR 2x2 lattice model + + """ + model = openmc.Model() + + ########################################################################### + # Create MGXS data for the problem + + # Instantiate the energy group data + group_edges = [1e-5, 0.0635, 10.0, 1.0e2, 1.0e3, 0.5e6, 1.0e6, 20.0e6] + groups = openmc.mgxs.EnergyGroups(group_edges) + + # Instantiate the 7-group (C5G7) cross section data + uo2_xsdata = openmc.XSdata('UO2', groups) + uo2_xsdata.order = 0 + uo2_xsdata.set_total( + [0.1779492, 0.3298048, 0.4803882, 0.5543674, 0.3118013, 0.3951678, + 0.5644058]) + uo2_xsdata.set_absorption([8.0248e-03, 3.7174e-03, 2.6769e-02, 9.6236e-02, + 3.0020e-02, 1.1126e-01, 2.8278e-01]) + scatter_matrix = np.array( + [[[0.1275370, 0.0423780, 0.0000094, 0.0000000, 0.0000000, 0.0000000, 0.0000000], + [0.0000000, 0.3244560, 0.0016314, 0.0000000, + 0.0000000, 0.0000000, 0.0000000], + [0.0000000, 0.0000000, 0.4509400, 0.0026792, + 0.0000000, 0.0000000, 0.0000000], + [0.0000000, 0.0000000, 0.0000000, 0.4525650, + 0.0055664, 0.0000000, 0.0000000], + [0.0000000, 0.0000000, 0.0000000, 0.0001253, + 0.2714010, 0.0102550, 0.0000000], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, + 0.0012968, 0.2658020, 0.0168090], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0085458, 0.2730800]]]) + scatter_matrix = np.rollaxis(scatter_matrix, 0, 3) + uo2_xsdata.set_scatter_matrix(scatter_matrix) + uo2_xsdata.set_fission([7.21206e-03, 8.19301e-04, 6.45320e-03, + 1.85648e-02, 1.78084e-02, 8.30348e-02, + 2.16004e-01]) + uo2_xsdata.set_nu_fission([2.005998e-02, 2.027303e-03, 1.570599e-02, + 4.518301e-02, 4.334208e-02, 2.020901e-01, + 5.257105e-01]) + uo2_xsdata.set_chi([5.8791e-01, 4.1176e-01, 3.3906e-04, 1.1761e-07, 0.0000e+00, + 0.0000e+00, 0.0000e+00]) + + h2o_xsdata = openmc.XSdata('LWTR', groups) + h2o_xsdata.order = 0 + h2o_xsdata.set_total([0.15920605, 0.412969593, 0.59030986, 0.58435, + 0.718, 1.2544497, 2.650379]) + h2o_xsdata.set_absorption([6.0105e-04, 1.5793e-05, 3.3716e-04, + 1.9406e-03, 5.7416e-03, 1.5001e-02, + 3.7239e-02]) + scatter_matrix = np.array( + [[[0.0444777, 0.1134000, 0.0007235, 0.0000037, 0.0000001, 0.0000000, 0.0000000], + [0.0000000, 0.2823340, 0.1299400, 0.0006234, + 0.0000480, 0.0000074, 0.0000010], + [0.0000000, 0.0000000, 0.3452560, 0.2245700, + 0.0169990, 0.0026443, 0.0005034], + [0.0000000, 0.0000000, 0.0000000, 0.0910284, + 0.4155100, 0.0637320, 0.0121390], + [0.0000000, 0.0000000, 0.0000000, 0.0000714, + 0.1391380, 0.5118200, 0.0612290], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, + 0.0022157, 0.6999130, 0.5373200], + [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.1324400, 2.4807000]]]) + scatter_matrix = np.rollaxis(scatter_matrix, 0, 3) + h2o_xsdata.set_scatter_matrix(scatter_matrix) + + mg_cross_sections = openmc.MGXSLibrary(groups) + mg_cross_sections.add_xsdatas([uo2_xsdata, h2o_xsdata]) + mg_cross_sections.export_to_hdf5('mgxs.h5') + + ########################################################################### + # Create materials for the problem + + # Instantiate some Materials and register the appropriate macroscopic data + uo2 = openmc.Material(name='UO2 fuel') + uo2.set_density('macro', 1.0) + uo2.add_macroscopic('UO2') + + water = openmc.Material(name='Water') + water.set_density('macro', 1.0) + water.add_macroscopic('LWTR') + + # Instantiate a Materials collection and export to XML + materials = openmc.Materials([uo2, water]) + materials.cross_sections = "mgxs.h5" + + ########################################################################### + # Define problem geometry + + ######################################## + # Define an unbounded pincell universe + + pitch = 1.26 + + # Create a surface for the fuel outer radius + fuel_or = openmc.ZCylinder(r=0.54, name='Fuel OR') + inner_ring_a = openmc.ZCylinder(r=0.33, name='inner ring a') + inner_ring_b = openmc.ZCylinder(r=0.45, name='inner ring b') + outer_ring_a = openmc.ZCylinder(r=0.60, name='outer ring a') + outer_ring_b = openmc.ZCylinder(r=0.69, name='outer ring b') + + # Instantiate Cells + fuel_a = openmc.Cell(fill=uo2, region=-inner_ring_a, name='fuel inner a') + fuel_b = openmc.Cell(fill=uo2, region=+inner_ring_a & - + inner_ring_b, name='fuel inner b') + fuel_c = openmc.Cell(fill=uo2, region=+inner_ring_b & - + fuel_or, name='fuel inner c') + moderator_a = openmc.Cell( + fill=water, region=+fuel_or & -outer_ring_a, name='moderator inner a') + moderator_b = openmc.Cell( + fill=water, region=+outer_ring_a & -outer_ring_b, name='moderator outer b') + moderator_c = openmc.Cell( + fill=water, region=+outer_ring_b, name='moderator outer c') + + # Create pincell universe + pincell_base = openmc.Universe() + + # Register Cells with Universe + pincell_base.add_cells( + [fuel_a, fuel_b, fuel_c, moderator_a, moderator_b, moderator_c]) + + # Create planes for azimuthal sectors + azimuthal_planes = [] + for i in range(8): + angle = 2 * i * openmc.pi / 8 + normal_vector = (-openmc.sin(angle), openmc.cos(angle), 0) + azimuthal_planes.append(openmc.Plane( + a=normal_vector[0], b=normal_vector[1], c=normal_vector[2], d=0)) + + # Create a cell for each azimuthal sector + azimuthal_cells = [] + for i in range(8): + azimuthal_cell = openmc.Cell(name=f'azimuthal_cell_{i}') + azimuthal_cell.fill = pincell_base + azimuthal_cell.region = +azimuthal_planes[i] & -azimuthal_planes[(i+1) % 8] + azimuthal_cells.append(azimuthal_cell) + + # Create a geometry with the azimuthal universes + pincell = openmc.Universe(cells=azimuthal_cells) + + ######################################## + # Define a moderator lattice universe + + moderator_infinite = openmc.Cell(fill=water, name='moderator infinite') + mu = openmc.Universe() + mu.add_cells([moderator_infinite]) + + lattice = openmc.RectLattice() + lattice.lower_left = [-pitch/2.0, -pitch/2.0] + lattice.pitch = [pitch/10.0, pitch/10.0] + lattice.universes = np.full((10, 10), mu) + + mod_lattice_cell = openmc.Cell(fill=lattice) + + mod_lattice_uni = openmc.Universe() + + mod_lattice_uni.add_cells([mod_lattice_cell]) + + ######################################## + # Define 2x2 outer lattice + lattice2x2 = openmc.RectLattice() + lattice2x2.lower_left = (-pitch, -pitch) + lattice2x2.pitch = (pitch, pitch) + lattice2x2.universes = [ + [pincell, pincell], + [pincell, mod_lattice_uni] + ] + + ######################################## + # Define cell containing lattice and other stuff + box = openmc.model.RectangularPrism( + pitch*2, pitch*2, boundary_type='reflective') + + assembly = openmc.Cell(fill=lattice2x2, region=-box, name='assembly') + + # Create a geometry with the top-level cell + geometry = openmc.Geometry([assembly]) + + ########################################################################### + # Define problem settings + + # Instantiate a Settings object, set all runtime parameters, and export to XML + settings = openmc.Settings() + settings.energy_mode = "multi-group" + settings.batches = 10 + settings.inactive = 5 + settings.particles = 100 + + # Create an initial uniform spatial source distribution over fissionable zones + lower_left = (-pitch, -pitch, -1) + upper_right = (pitch, pitch, 1) + uniform_dist = openmc.stats.Box(lower_left, upper_right) + rr_source = openmc.IndependentSource(space=uniform_dist) + + settings.random_ray['distance_active'] = 100.0 + settings.random_ray['distance_inactive'] = 20.0 + settings.random_ray['ray_source'] = rr_source + settings.random_ray['volume_normalized_flux_tallies'] = True + + ########################################################################### + # Define tallies + + # Create a mesh that will be used for tallying + mesh = openmc.RegularMesh() + mesh.dimension = (2, 2) + mesh.lower_left = (-pitch, -pitch) + mesh.upper_right = (pitch, pitch) + + # Create a mesh filter that can be used in a tally + mesh_filter = openmc.MeshFilter(mesh) + + # Create an energy group filter as well + group_edges = [1e-5, 0.0635, 10.0, 1.0e2, 1.0e3, 0.5e6, 1.0e6, 20.0e6] + energy_filter = openmc.EnergyFilter(group_edges) + + # Now use the mesh filter in a tally and indicate what scores are desired + tally = openmc.Tally(name="Mesh tally") + tally.filters = [mesh_filter, energy_filter] + tally.scores = ['flux', 'fission', 'nu-fission'] + tally.estimator = 'analog' + + # Instantiate a Tallies collection and export to XML + tallies = openmc.Tallies([tally]) + + ########################################################################### + # Exporting to OpenMC model + ########################################################################### + + model.geometry = geometry + model.materials = materials + model.settings = settings + model.tallies = tallies + return model + + +def random_ray_three_region_cube() -> openmc.Model: + """Create a three region cube model. + + This is a simple monoenergetic problem of a cube with three concentric cubic + regions. The innermost region is near void (with Sigma_t around 10^-5) and + contains an external isotropic source term, the middle region is void (with + Sigma_t around 10^-4), and the outer region of the cube is an absorber + (with Sigma_t around 1). + + Returns + ------- + model : openmc.Model + A three region cube model + + """ + + model = openmc.Model() + + ########################################################################### + # Helper function creates a 3 region cube with different fills in each region + def fill_cube(N, n_1, n_2, fill_1, fill_2, fill_3): + cube = [[[0 for _ in range(N)] for _ in range(N)] for _ in range(N)] + for i in range(N): + for j in range(N): + for k in range(N): + if i < n_1 and j >= (N-n_1) and k < n_1: + cube[i][j][k] = fill_1 + elif i < n_2 and j >= (N-n_2) and k < n_2: + cube[i][j][k] = fill_2 + else: + cube[i][j][k] = fill_3 + return cube + + ########################################################################### + # Create multigroup data + + # Instantiate the energy group data + ebins = [1e-5, 20.0e6] + groups = openmc.mgxs.EnergyGroups(group_edges=ebins) + + void_sigma_a = 4.0e-6 + void_sigma_s = 3.0e-4 + void_mat_data = openmc.XSdata('void', groups) + void_mat_data.order = 0 + void_mat_data.set_total([void_sigma_a + void_sigma_s]) + void_mat_data.set_absorption([void_sigma_a]) + void_mat_data.set_scatter_matrix( + np.rollaxis(np.array([[[void_sigma_s]]]), 0, 3)) + + absorber_sigma_a = 0.75 + absorber_sigma_s = 0.25 + absorber_mat_data = openmc.XSdata('absorber', groups) + absorber_mat_data.order = 0 + absorber_mat_data.set_total([absorber_sigma_a + absorber_sigma_s]) + absorber_mat_data.set_absorption([absorber_sigma_a]) + absorber_mat_data.set_scatter_matrix( + np.rollaxis(np.array([[[absorber_sigma_s]]]), 0, 3)) + + multiplier = 0.1 + source_sigma_a = void_sigma_a * multiplier + source_sigma_s = void_sigma_s * multiplier + source_mat_data = openmc.XSdata('source', groups) + source_mat_data.order = 0 + source_mat_data.set_total([source_sigma_a + source_sigma_s]) + source_mat_data.set_absorption([source_sigma_a]) + source_mat_data.set_scatter_matrix( + np.rollaxis(np.array([[[source_sigma_s]]]), 0, 3)) + + mg_cross_sections_file = openmc.MGXSLibrary(groups) + mg_cross_sections_file.add_xsdatas( + [source_mat_data, void_mat_data, absorber_mat_data]) + mg_cross_sections_file.export_to_hdf5() + + ########################################################################### + # Create materials for the problem + + # Instantiate some Macroscopic Data + source_data = openmc.Macroscopic('source') + void_data = openmc.Macroscopic('void') + absorber_data = openmc.Macroscopic('absorber') + + # Instantiate some Materials and register the appropriate Macroscopic objects + source_mat = openmc.Material(name='source') + source_mat.set_density('macro', 1.0) + source_mat.add_macroscopic(source_data) + + void_mat = openmc.Material(name='void') + void_mat.set_density('macro', 1.0) + void_mat.add_macroscopic(void_data) + + absorber_mat = openmc.Material(name='absorber') + absorber_mat.set_density('macro', 1.0) + absorber_mat.add_macroscopic(absorber_data) + + # Instantiate a Materials collection and export to XML + materials_file = openmc.Materials([source_mat, void_mat, absorber_mat]) + materials_file.cross_sections = "mgxs.h5" + + ########################################################################### + # Define problem geometry + + source_cell = openmc.Cell(fill=source_mat, name='infinite source region') + void_cell = openmc.Cell(fill=void_mat, name='infinite void region') + absorber_cell = openmc.Cell( + fill=absorber_mat, name='infinite absorber region') + + source_universe = openmc.Universe(name='source universe') + source_universe.add_cells([source_cell]) + + void_universe = openmc.Universe() + void_universe.add_cells([void_cell]) + + absorber_universe = openmc.Universe() + absorber_universe.add_cells([absorber_cell]) + + absorber_width = 30.0 + n_base = 6 + + # This variable can be increased above 1 to refine the FSR mesh resolution further + refinement_level = 2 + + n = n_base * refinement_level + pitch = absorber_width / n + + pattern = fill_cube(n, 1*refinement_level, 5*refinement_level, + source_universe, void_universe, absorber_universe) + + lattice = openmc.RectLattice() + lattice.lower_left = [0.0, 0.0, 0.0] + lattice.pitch = [pitch, pitch, pitch] + lattice.universes = pattern + + lattice_cell = openmc.Cell(fill=lattice) + + lattice_uni = openmc.Universe() + lattice_uni.add_cells([lattice_cell]) + + x_low = openmc.XPlane(x0=0.0, boundary_type='reflective') + x_high = openmc.XPlane(x0=absorber_width, boundary_type='vacuum') + + y_low = openmc.YPlane(y0=0.0, boundary_type='reflective') + y_high = openmc.YPlane(y0=absorber_width, boundary_type='vacuum') + + z_low = openmc.ZPlane(z0=0.0, boundary_type='reflective') + z_high = openmc.ZPlane(z0=absorber_width, boundary_type='vacuum') + + full_domain = openmc.Cell(fill=lattice_uni, region=+x_low & - + x_high & +y_low & -y_high & +z_low & -z_high, name='full domain') + + root = openmc.Universe(name='root universe') + root.add_cell(full_domain) + + # Create a geometry with the two cells and export to XML + geometry = openmc.Geometry(root) + + ########################################################################### + # Define problem settings + + # Instantiate a Settings object, set all runtime parameters, and export to XML + settings = openmc.Settings() + settings.energy_mode = "multi-group" + settings.inactive = 5 + settings.batches = 10 + settings.particles = 90 + settings.run_mode = 'fixed source' + + # Create an initial uniform spatial source for ray integration + lower_left_ray = [0.0, 0.0, 0.0] + upper_right_ray = [absorber_width, absorber_width, absorber_width] + uniform_dist_ray = openmc.stats.Box( + lower_left_ray, upper_right_ray, only_fissionable=False) + rr_source = openmc.IndependentSource(space=uniform_dist_ray) + + settings.random_ray['distance_active'] = 500.0 + settings.random_ray['distance_inactive'] = 100.0 + settings.random_ray['ray_source'] = rr_source + settings.random_ray['volume_normalized_flux_tallies'] = True + + # Create the neutron source in the bottom right of the moderator + # Good - fast group appears largest (besides most thermal) + strengths = [1.0] + midpoints = [100.0] + energy_distribution = openmc.stats.Discrete(x=midpoints, p=strengths) + + source = openmc.IndependentSource(energy=energy_distribution, constraints={ + 'domains': [source_universe]}, strength=3.14) + + settings.source = [source] + + ########################################################################### + # Define tallies + + estimator = 'tracklength' + + absorber_filter = openmc.MaterialFilter(absorber_mat) + absorber_tally = openmc.Tally(name="Absorber Tally") + absorber_tally.filters = [absorber_filter] + absorber_tally.scores = ['flux'] + absorber_tally.estimator = estimator + + void_filter = openmc.MaterialFilter(void_mat) + void_tally = openmc.Tally(name="Void Tally") + void_tally.filters = [void_filter] + void_tally.scores = ['flux'] + void_tally.estimator = estimator + + source_filter = openmc.MaterialFilter(source_mat) + source_tally = openmc.Tally(name="Source Tally") + source_tally.filters = [source_filter] + source_tally.scores = ['flux'] + source_tally.estimator = estimator + + # Instantiate a Tallies collection and export to XML + tallies = openmc.Tallies([source_tally, void_tally, absorber_tally]) + + ########################################################################### + # Assmble Model + + model.geometry = geometry + model.materials = materials_file + model.settings = settings + model.tallies = tallies + + return model diff --git a/tests/regression_tests/random_ray_basic/test.py b/tests/regression_tests/random_ray_basic/test.py deleted file mode 100644 index c00d25880c5..00000000000 --- a/tests/regression_tests/random_ray_basic/test.py +++ /dev/null @@ -1,233 +0,0 @@ -import os - -import numpy as np -import openmc - -from tests.testing_harness import TolerantPyAPITestHarness - - -class MGXSTestHarness(TolerantPyAPITestHarness): - def _cleanup(self): - super()._cleanup() - f = 'mgxs.h5' - if os.path.exists(f): - os.remove(f) - - -def random_ray_model() -> openmc.Model: - ############################################################################### - # Create multigroup data - - # Instantiate the energy group data - group_edges = [1e-5, 0.0635, 10.0, 1.0e2, 1.0e3, 0.5e6, 1.0e6, 20.0e6] - groups = openmc.mgxs.EnergyGroups(group_edges) - - # Instantiate the 7-group (C5G7) cross section data - uo2_xsdata = openmc.XSdata('UO2', groups) - uo2_xsdata.order = 0 - uo2_xsdata.set_total( - [0.1779492, 0.3298048, 0.4803882, 0.5543674, 0.3118013, 0.3951678, - 0.5644058]) - uo2_xsdata.set_absorption([8.0248e-03, 3.7174e-03, 2.6769e-02, 9.6236e-02, - 3.0020e-02, 1.1126e-01, 2.8278e-01]) - scatter_matrix = np.array( - [[[0.1275370, 0.0423780, 0.0000094, 0.0000000, 0.0000000, 0.0000000, 0.0000000], - [0.0000000, 0.3244560, 0.0016314, 0.0000000, 0.0000000, 0.0000000, 0.0000000], - [0.0000000, 0.0000000, 0.4509400, 0.0026792, 0.0000000, 0.0000000, 0.0000000], - [0.0000000, 0.0000000, 0.0000000, 0.4525650, 0.0055664, 0.0000000, 0.0000000], - [0.0000000, 0.0000000, 0.0000000, 0.0001253, 0.2714010, 0.0102550, 0.0000000], - [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0012968, 0.2658020, 0.0168090], - [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0085458, 0.2730800]]]) - scatter_matrix = np.rollaxis(scatter_matrix, 0, 3) - uo2_xsdata.set_scatter_matrix(scatter_matrix) - uo2_xsdata.set_fission([7.21206e-03, 8.19301e-04, 6.45320e-03, - 1.85648e-02, 1.78084e-02, 8.30348e-02, - 2.16004e-01]) - uo2_xsdata.set_nu_fission([2.005998e-02, 2.027303e-03, 1.570599e-02, - 4.518301e-02, 4.334208e-02, 2.020901e-01, - 5.257105e-01]) - uo2_xsdata.set_chi([5.8791e-01, 4.1176e-01, 3.3906e-04, 1.1761e-07, 0.0000e+00, - 0.0000e+00, 0.0000e+00]) - - h2o_xsdata = openmc.XSdata('LWTR', groups) - h2o_xsdata.order = 0 - h2o_xsdata.set_total([0.15920605, 0.412969593, 0.59030986, 0.58435, - 0.718, 1.2544497, 2.650379]) - h2o_xsdata.set_absorption([6.0105e-04, 1.5793e-05, 3.3716e-04, - 1.9406e-03, 5.7416e-03, 1.5001e-02, - 3.7239e-02]) - scatter_matrix = np.array( - [[[0.0444777, 0.1134000, 0.0007235, 0.0000037, 0.0000001, 0.0000000, 0.0000000], - [0.0000000, 0.2823340, 0.1299400, 0.0006234, 0.0000480, 0.0000074, 0.0000010], - [0.0000000, 0.0000000, 0.3452560, 0.2245700, 0.0169990, 0.0026443, 0.0005034], - [0.0000000, 0.0000000, 0.0000000, 0.0910284, 0.4155100, 0.0637320, 0.0121390], - [0.0000000, 0.0000000, 0.0000000, 0.0000714, 0.1391380, 0.5118200, 0.0612290], - [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0022157, 0.6999130, 0.5373200], - [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.1324400, 2.4807000]]]) - scatter_matrix = np.rollaxis(scatter_matrix, 0, 3) - h2o_xsdata.set_scatter_matrix(scatter_matrix) - - mg_cross_sections = openmc.MGXSLibrary(groups) - mg_cross_sections.add_xsdatas([uo2_xsdata, h2o_xsdata]) - mg_cross_sections.export_to_hdf5() - - ############################################################################### - # Create materials for the problem - - # Instantiate some Materials and register the appropriate macroscopic data - uo2 = openmc.Material(name='UO2 fuel') - uo2.set_density('macro', 1.0) - uo2.add_macroscopic('UO2') - - water = openmc.Material(name='Water') - water.set_density('macro', 1.0) - water.add_macroscopic('LWTR') - - # Instantiate a Materials collection and export to XML - materials = openmc.Materials([uo2, water]) - materials.cross_sections = "mgxs.h5" - - ############################################################################### - # Define problem geometry - - ######################################## - # Define an unbounded pincell universe - - pitch = 1.26 - - # Create a surface for the fuel outer radius - fuel_or = openmc.ZCylinder(r=0.54, name='Fuel OR') - inner_ring_a = openmc.ZCylinder(r=0.33, name='inner ring a') - inner_ring_b = openmc.ZCylinder(r=0.45, name='inner ring b') - outer_ring_a = openmc.ZCylinder(r=0.60, name='outer ring a') - outer_ring_b = openmc.ZCylinder(r=0.69, name='outer ring b') - - # Instantiate Cells - fuel_a = openmc.Cell(fill=uo2, region=-inner_ring_a, name='fuel inner a') - fuel_b = openmc.Cell(fill=uo2, region=+inner_ring_a & -inner_ring_b, name='fuel inner b') - fuel_c = openmc.Cell(fill=uo2, region=+inner_ring_b & -fuel_or, name='fuel inner c') - moderator_a = openmc.Cell(fill=water, region=+fuel_or & -outer_ring_a, name='moderator inner a') - moderator_b = openmc.Cell(fill=water, region=+outer_ring_a & -outer_ring_b, name='moderator outer b') - moderator_c = openmc.Cell(fill=water, region=+outer_ring_b, name='moderator outer c') - - # Create pincell universe - pincell_base = openmc.Universe() - - # Register Cells with Universe - pincell_base.add_cells([fuel_a, fuel_b, fuel_c, moderator_a, moderator_b, moderator_c]) - - # Create planes for azimuthal sectors - azimuthal_planes = [] - for i in range(8): - angle = 2 * i * openmc.pi / 8 - normal_vector = (-openmc.sin(angle), openmc.cos(angle), 0) - azimuthal_planes.append(openmc.Plane(a=normal_vector[0], b=normal_vector[1], c=normal_vector[2], d=0)) - - # Create a cell for each azimuthal sector - azimuthal_cells = [] - for i in range(8): - azimuthal_cell = openmc.Cell(name=f'azimuthal_cell_{i}') - azimuthal_cell.fill = pincell_base - azimuthal_cell.region = +azimuthal_planes[i] & -azimuthal_planes[(i+1) % 8] - azimuthal_cells.append(azimuthal_cell) - - # Create a geometry with the azimuthal universes - pincell = openmc.Universe(cells=azimuthal_cells) - - ######################################## - # Define a moderator lattice universe - - moderator_infinite = openmc.Cell(fill=water, name='moderator infinite') - mu = openmc.Universe() - mu.add_cells([moderator_infinite]) - - lattice = openmc.RectLattice() - lattice.lower_left = [-pitch/2.0, -pitch/2.0] - lattice.pitch = [pitch/10.0, pitch/10.0] - lattice.universes = np.full((10, 10), mu) - - mod_lattice_cell = openmc.Cell(fill=lattice) - - mod_lattice_uni = openmc.Universe() - - mod_lattice_uni.add_cells([mod_lattice_cell]) - - ######################################## - # Define 2x2 outer lattice - lattice2x2 = openmc.RectLattice() - lattice2x2.lower_left = (-pitch, -pitch) - lattice2x2.pitch = (pitch, pitch) - lattice2x2.universes = [ - [pincell, pincell], - [pincell, mod_lattice_uni] - ] - - ######################################## - # Define cell containing lattice and other stuff - box = openmc.model.RectangularPrism(pitch*2, pitch*2, boundary_type='reflective') - - assembly = openmc.Cell(fill=lattice2x2, region=-box, name='assembly') - - # Create a geometry with the top-level cell - geometry = openmc.Geometry([assembly]) - - ############################################################################### - # Define problem settings - - # Instantiate a Settings object, set all runtime parameters, and export to XML - settings = openmc.Settings() - settings.energy_mode = "multi-group" - settings.batches = 10 - settings.inactive = 5 - settings.particles = 100 - - # Create an initial uniform spatial source distribution over fissionable zones - lower_left = (-pitch, -pitch, -1) - upper_right = (pitch, pitch, 1) - uniform_dist = openmc.stats.Box(lower_left, upper_right) - rr_source = openmc.IndependentSource(space=uniform_dist) - - settings.random_ray['distance_active'] = 100.0 - settings.random_ray['distance_inactive'] = 20.0 - settings.random_ray['ray_source'] = rr_source - settings.random_ray['volume_normalized_flux_tallies'] = True - - ############################################################################### - # Define tallies - - # Create a mesh that will be used for tallying - mesh = openmc.RegularMesh() - mesh.dimension = (2, 2) - mesh.lower_left = (-pitch, -pitch) - mesh.upper_right = (pitch, pitch) - - # Create a mesh filter that can be used in a tally - mesh_filter = openmc.MeshFilter(mesh) - - # Create an energy group filter as well - energy_filter = openmc.EnergyFilter(group_edges) - - # Now use the mesh filter in a tally and indicate what scores are desired - tally = openmc.Tally(name="Mesh tally") - tally.filters = [mesh_filter, energy_filter] - tally.scores = ['flux', 'fission', 'nu-fission'] - tally.estimator = 'analog' - - # Instantiate a Tallies collection and export to XML - tallies = openmc.Tallies([tally]) - - ############################################################################### - # Exporting to OpenMC model - ############################################################################### - - model = openmc.Model() - model.geometry = geometry - model.materials = materials - model.settings = settings - model.tallies = tallies - return model - - -def test_random_ray_basic(): - harness = MGXSTestHarness('statepoint.10.h5', random_ray_model()) - harness.main() diff --git a/tests/regression_tests/random_ray_cube/False_hybrid/inputs_true.dat b/tests/regression_tests/random_ray_cube/False_hybrid/inputs_true.dat deleted file mode 100644 index 82c24291b30..00000000000 --- a/tests/regression_tests/random_ray_cube/False_hybrid/inputs_true.dat +++ /dev/null @@ -1,1018 +0,0 @@ - - - - mgxs.h5 - - - - - - - - - - - - - - - - - - - - - 1.0 1.0 1.0 - 30 30 30 - 0.0 0.0 0.0 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - - - - - - - - - - fixed source - 90 - 10 - 5 - - - 100.0 1.0 - - - universe - 1 - - - multi-group - - 500.0 - 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - - False - - - - - 1 - - - 2 - - - 3 - - - 3 - flux - tracklength - - - 2 - flux - tracklength - - - 1 - flux - tracklength - - - diff --git a/tests/regression_tests/random_ray_cube/False_hybrid/results_true.dat b/tests/regression_tests/random_ray_cube/False_hybrid/results_true.dat deleted file mode 100644 index 88718c5bfbc..00000000000 --- a/tests/regression_tests/random_ray_cube/False_hybrid/results_true.dat +++ /dev/null @@ -1,9 +0,0 @@ -tally 1: --5.920549E+04 -2.321317E+09 -tally 2: -4.761087E+02 -4.909570E+04 -tally 3: -2.206028E+01 -1.019164E+02 diff --git a/tests/regression_tests/random_ray_cube/True_hybrid/inputs_true.dat b/tests/regression_tests/random_ray_cube/True_hybrid/inputs_true.dat deleted file mode 100644 index 464c3440493..00000000000 --- a/tests/regression_tests/random_ray_cube/True_hybrid/inputs_true.dat +++ /dev/null @@ -1,1018 +0,0 @@ - - - - mgxs.h5 - - - - - - - - - - - - - - - - - - - - - 1.0 1.0 1.0 - 30 30 30 - 0.0 0.0 0.0 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 -2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 - - - - - - - - - - fixed source - 90 - 10 - 5 - - - 100.0 1.0 - - - universe - 1 - - - multi-group - - 500.0 - 100.0 - - - 0.0 0.0 0.0 30.0 30.0 30.0 - - - True - - - - - 1 - - - 2 - - - 3 - - - 3 - flux - tracklength - - - 2 - flux - tracklength - - - 1 - flux - tracklength - - - diff --git a/tests/regression_tests/random_ray_cube/True_hybrid/results_true.dat b/tests/regression_tests/random_ray_cube/True_hybrid/results_true.dat deleted file mode 100644 index 4a29967407d..00000000000 --- a/tests/regression_tests/random_ray_cube/True_hybrid/results_true.dat +++ /dev/null @@ -1,9 +0,0 @@ -tally 1: --5.230164E+02 -1.818988E+05 -tally 2: -3.067508E-02 -2.037701E-04 -tally 3: -1.941220E-03 -7.892297E-07 diff --git a/tests/regression_tests/random_ray_cube/test.py b/tests/regression_tests/random_ray_cube/test.py deleted file mode 100644 index 51d61ae797c..00000000000 --- a/tests/regression_tests/random_ray_cube/test.py +++ /dev/null @@ -1,231 +0,0 @@ -import os - -import numpy as np -import openmc -from openmc.utility_funcs import change_directory -import pytest - -from tests.testing_harness import TolerantPyAPITestHarness - -# Creates a 3 region cube with different fills in each region -def fill_cube(N, n_1, n_2, fill_1, fill_2, fill_3): - cube = [[[0 for _ in range(N)] for _ in range(N)] for _ in range(N)] - for i in range(N): - for j in range(N): - for k in range(N): - if i < n_1 and j >= (N-n_1) and k < n_1: - cube[i][j][k] = fill_1 - elif i < n_2 and j >= (N-n_2) and k < n_2: - cube[i][j][k] = fill_2 - else: - cube[i][j][k] = fill_3 - return cube - -class MGXSTestHarness(TolerantPyAPITestHarness): - def _cleanup(self): - super()._cleanup() - f = 'mgxs.h5' - if os.path.exists(f): - os.remove(f) - -def create_random_ray_model(volume_normalize, estimator): - openmc.reset_auto_ids() - ############################################################################### - # Create multigroup data - - # Instantiate the energy group data - ebins = [1e-5, 20.0e6] - groups = openmc.mgxs.EnergyGroups(group_edges=ebins) - - void_sigma_a = 4.0e-6 - void_sigma_s = 3.0e-4 - void_mat_data = openmc.XSdata('void', groups) - void_mat_data.order = 0 - void_mat_data.set_total([void_sigma_a + void_sigma_s]) - void_mat_data.set_absorption([void_sigma_a]) - void_mat_data.set_scatter_matrix(np.rollaxis(np.array([[[void_sigma_s]]]),0,3)) - - absorber_sigma_a = 0.75 - absorber_sigma_s = 0.25 - absorber_mat_data = openmc.XSdata('absorber', groups) - absorber_mat_data.order = 0 - absorber_mat_data.set_total([absorber_sigma_a + absorber_sigma_s]) - absorber_mat_data.set_absorption([absorber_sigma_a]) - absorber_mat_data.set_scatter_matrix(np.rollaxis(np.array([[[absorber_sigma_s]]]),0,3)) - - multiplier = 0.1 - source_sigma_a = void_sigma_a * multiplier - source_sigma_s = void_sigma_s * multiplier - source_mat_data = openmc.XSdata('source', groups) - source_mat_data.order = 0 - print(source_sigma_a + source_sigma_s) - source_mat_data.set_total([source_sigma_a + source_sigma_s]) - source_mat_data.set_absorption([source_sigma_a]) - source_mat_data.set_scatter_matrix(np.rollaxis(np.array([[[source_sigma_s]]]),0,3)) - - mg_cross_sections_file = openmc.MGXSLibrary(groups) - mg_cross_sections_file.add_xsdatas([source_mat_data, void_mat_data, absorber_mat_data]) - mg_cross_sections_file.export_to_hdf5() - - ############################################################################### - # Create materials for the problem - - # Instantiate some Macroscopic Data - source_data = openmc.Macroscopic('source') - void_data = openmc.Macroscopic('void') - absorber_data = openmc.Macroscopic('absorber') - - # Instantiate some Materials and register the appropriate Macroscopic objects - source_mat = openmc.Material(name='source') - source_mat.set_density('macro', 1.0) - source_mat.add_macroscopic(source_data) - - void_mat = openmc.Material(name='void') - void_mat.set_density('macro', 1.0) - void_mat.add_macroscopic(void_data) - - absorber_mat = openmc.Material(name='absorber') - absorber_mat.set_density('macro', 1.0) - absorber_mat.add_macroscopic(absorber_data) - - # Instantiate a Materials collection and export to XML - materials_file = openmc.Materials([source_mat, void_mat, absorber_mat]) - materials_file.cross_sections = "mgxs.h5" - - ############################################################################### - # Define problem geometry - - source_cell = openmc.Cell(fill=source_mat, name='infinite source region') - void_cell = openmc.Cell(fill=void_mat, name='infinite void region') - absorber_cell = openmc.Cell(fill=absorber_mat, name='infinite absorber region') - - source_universe = openmc.Universe() - source_universe.add_cells([source_cell]) - - void_universe = openmc.Universe() - void_universe.add_cells([void_cell]) - - absorber_universe = openmc.Universe() - absorber_universe.add_cells([absorber_cell]) - - absorber_width = 30.0 - n_base = 6 - - # This variable can be increased above 1 to refine the FSR mesh resolution further - refinement_level = 5 - - n = n_base * refinement_level - pitch = absorber_width / n - - pattern = fill_cube(n, 1*refinement_level, 5*refinement_level, source_universe, void_universe, absorber_universe) - - lattice = openmc.RectLattice() - lattice.lower_left = [0.0, 0.0, 0.0] - lattice.pitch = [pitch, pitch, pitch] - lattice.universes = pattern - #print(lattice) - - lattice_cell = openmc.Cell(fill=lattice) - - lattice_uni = openmc.Universe() - lattice_uni.add_cells([lattice_cell]) - - x_low = openmc.XPlane(x0=0.0,boundary_type='reflective') - x_high = openmc.XPlane(x0=absorber_width,boundary_type='vacuum') - - y_low = openmc.YPlane(y0=0.0,boundary_type='reflective') - y_high = openmc.YPlane(y0=absorber_width,boundary_type='vacuum') - - z_low = openmc.ZPlane(z0=0.0,boundary_type='reflective') - z_high = openmc.ZPlane(z0=absorber_width,boundary_type='vacuum') - - full_domain = openmc.Cell(fill=lattice_uni, region=+x_low & -x_high & +y_low & -y_high & +z_low & -z_high, name='full domain') - - root = openmc.Universe(name='root universe') - root.add_cell(full_domain) - - # Create a geometry with the two cells and export to XML - geometry = openmc.Geometry(root) - - ############################################################################### - # Define problem settings - - # Instantiate a Settings object, set all runtime parameters, and export to XML - settings = openmc.Settings() - settings.energy_mode = "multi-group" - settings.inactive = 5 - settings.batches = 10 - settings.particles = 90 - settings.run_mode = 'fixed source' - - # Create an initial uniform spatial source for ray integration - lower_left_ray = [0.0, 0.0, 0.0] - upper_right_ray = [absorber_width, absorber_width, absorber_width] - uniform_dist_ray = openmc.stats.Box(lower_left_ray, upper_right_ray, only_fissionable=False) - rr_source = openmc.IndependentSource(space=uniform_dist_ray) - - settings.random_ray['distance_active'] = 500.0 - settings.random_ray['distance_inactive'] = 100.0 - settings.random_ray['ray_source'] = rr_source - settings.random_ray['volume_normalized_flux_tallies'] = volume_normalize - - #settings.random_ray['volume_estimator'] = estimator - - # Create the neutron source in the bottom right of the moderator - strengths = [1.0] # Good - fast group appears largest (besides most thermal) - midpoints = [100.0] - energy_distribution = openmc.stats.Discrete(x=midpoints,p=strengths) - - source = openmc.IndependentSource(energy=energy_distribution, constraints={'domains':[source_universe]}, strength=3.14) - - settings.source = [source] - - ############################################################################### - # Define tallies - - estimator = 'tracklength' - - absorber_filter = openmc.MaterialFilter(absorber_mat) - absorber_tally = openmc.Tally(name="Absorber Tally") - absorber_tally.filters = [absorber_filter] - absorber_tally.scores = ['flux'] - absorber_tally.estimator = estimator - - void_filter = openmc.MaterialFilter(void_mat) - void_tally = openmc.Tally(name="Void Tally") - void_tally.filters = [void_filter] - void_tally.scores = ['flux'] - void_tally.estimator = estimator - - source_filter = openmc.MaterialFilter(source_mat) - source_tally = openmc.Tally(name="Source Tally") - source_tally.filters = [source_filter] - source_tally.scores = ['flux'] - source_tally.estimator = estimator - - # Instantiate a Tallies collection and export to XML - tallies = openmc.Tallies([source_tally, void_tally, absorber_tally]) - - ############################################################################### - # Assmble Model - - model = openmc.model.Model() - model.geometry = geometry - model.materials = materials_file - model.settings = settings - model.xs_data = mg_cross_sections_file - model.tallies = tallies - - return model - -@pytest.mark.parametrize("volume_normalize, estimator", [ - (False, "hybrid"), - (True, "hybrid") -]) -def test_random_ray_cube(volume_normalize, estimator): - # Generating a unique directory name from the parameters - directory_name = f"{volume_normalize}_{estimator}" - with change_directory(directory_name): - model = create_random_ray_model(volume_normalize, estimator) - harness = MGXSTestHarness('statepoint.10.h5', model) - harness.main() \ No newline at end of file diff --git a/tests/regression_tests/random_ray_fixed_source/cell/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source/cell/inputs_true.dat deleted file mode 100644 index a8eaa009661..00000000000 --- a/tests/regression_tests/random_ray_fixed_source/cell/inputs_true.dat +++ /dev/null @@ -1,205 +0,0 @@ - - - - mgxs.h5 - - - - - - - - - - - - - - - - - - - - - - - - 5.0 5.0 5.0 - 2 2 2 - -5.0 -5.0 -5.0 - -1 1 -1 1 - -1 1 -1 1 - - - 5.0 5.0 5.0 - 2 2 2 - -5.0 -5.0 -5.0 - -2 2 -2 2 - -2 2 -2 2 - - - 5.0 5.0 5.0 - 2 2 2 - -5.0 -5.0 -5.0 - -3 3 -3 3 - -3 3 -3 3 - - - 10.0 10.0 10.0 - 6 10 6 - 0.0 0.0 0.0 - -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -8 8 8 8 9 9 -8 9 9 9 9 9 -8 9 9 9 9 9 -8 9 9 9 9 9 -8 9 9 9 9 9 -7 9 9 9 9 9 - -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 8 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 - -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 8 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 - -9 9 9 8 9 9 -9 9 9 8 9 9 -9 9 9 8 9 9 -9 9 9 8 9 9 -9 9 9 8 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 - -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 - -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 - - - - - - - - - - fixed source - 1000 - 10 - 5 - - - 100.0 1.0 - - - cell - 4 - - - multi-group - - 400.0 - 100.0 - False - - - 0.0 0.0 0.0 60.0 100.0 60.0 - - - - - - - 1 10 1 - 0.0 0.0 0.0 - 10.0 100.0 10.0 - - - 6 1 1 - 0.0 50.0 0.0 - 60.0 60.0 10.0 - - - 6 1 1 - 0.0 90.0 30.0 - 60.0 100.0 40.0 - - - 1 - - - 2 - - - 3 - - - 1 - flux - analog - - - 2 - flux - analog - - - 3 - flux - analog - - - diff --git a/tests/regression_tests/random_ray_fixed_source/cell/results_true.dat b/tests/regression_tests/random_ray_fixed_source/cell/results_true.dat deleted file mode 100644 index ba1254d384d..00000000000 --- a/tests/regression_tests/random_ray_fixed_source/cell/results_true.dat +++ /dev/null @@ -1,47 +0,0 @@ -tally 1: -3.751047E+01 -2.841797E+02 -1.000671E+01 -2.020214E+01 -3.979569E+00 -3.330838E+00 -2.552971E+00 -1.407542E+00 -1.736764E+00 -7.948004E-01 -1.178834E+00 -4.328006E-01 -6.953895E-01 -1.408391E-01 -2.389347E-01 -1.617450E-02 -8.128516E-02 -1.903860E-03 -2.764062E-02 -2.285458E-04 -tally 2: -1.178834E+00 -4.328006E-01 -3.982124E-01 -4.138078E-02 -9.676374E-02 -1.919297E-03 -3.944531E-02 -3.133733E-04 -1.554518E-02 -5.084822E-05 -4.815079E-03 -4.681450E-06 -tally 3: -1.635823E-03 -6.482611E-07 -1.248235E-03 -3.227247E-07 -1.248195E-03 -3.488233E-07 -1.030935E-03 -2.206418E-07 -5.670315E-04 -6.648507E-08 -2.393638E-04 -1.242517E-08 diff --git a/tests/regression_tests/random_ray_fixed_source/material/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source/material/inputs_true.dat deleted file mode 100644 index e3ad3703b6e..00000000000 --- a/tests/regression_tests/random_ray_fixed_source/material/inputs_true.dat +++ /dev/null @@ -1,205 +0,0 @@ - - - - mgxs.h5 - - - - - - - - - - - - - - - - - - - - - - - - 5.0 5.0 5.0 - 2 2 2 - -5.0 -5.0 -5.0 - -1 1 -1 1 - -1 1 -1 1 - - - 5.0 5.0 5.0 - 2 2 2 - -5.0 -5.0 -5.0 - -2 2 -2 2 - -2 2 -2 2 - - - 5.0 5.0 5.0 - 2 2 2 - -5.0 -5.0 -5.0 - -3 3 -3 3 - -3 3 -3 3 - - - 10.0 10.0 10.0 - 6 10 6 - 0.0 0.0 0.0 - -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -8 8 8 8 9 9 -8 9 9 9 9 9 -8 9 9 9 9 9 -8 9 9 9 9 9 -8 9 9 9 9 9 -7 9 9 9 9 9 - -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 8 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 - -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 8 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 - -9 9 9 8 9 9 -9 9 9 8 9 9 -9 9 9 8 9 9 -9 9 9 8 9 9 -9 9 9 8 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 - -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 - -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 - - - - - - - - - - fixed source - 1000 - 10 - 5 - - - 100.0 1.0 - - - material - 1 - - - multi-group - - 400.0 - 100.0 - False - - - 0.0 0.0 0.0 60.0 100.0 60.0 - - - - - - - 1 10 1 - 0.0 0.0 0.0 - 10.0 100.0 10.0 - - - 6 1 1 - 0.0 50.0 0.0 - 60.0 60.0 10.0 - - - 6 1 1 - 0.0 90.0 30.0 - 60.0 100.0 40.0 - - - 1 - - - 2 - - - 3 - - - 1 - flux - analog - - - 2 - flux - analog - - - 3 - flux - analog - - - diff --git a/tests/regression_tests/random_ray_fixed_source/material/results_true.dat b/tests/regression_tests/random_ray_fixed_source/material/results_true.dat deleted file mode 100644 index ba1254d384d..00000000000 --- a/tests/regression_tests/random_ray_fixed_source/material/results_true.dat +++ /dev/null @@ -1,47 +0,0 @@ -tally 1: -3.751047E+01 -2.841797E+02 -1.000671E+01 -2.020214E+01 -3.979569E+00 -3.330838E+00 -2.552971E+00 -1.407542E+00 -1.736764E+00 -7.948004E-01 -1.178834E+00 -4.328006E-01 -6.953895E-01 -1.408391E-01 -2.389347E-01 -1.617450E-02 -8.128516E-02 -1.903860E-03 -2.764062E-02 -2.285458E-04 -tally 2: -1.178834E+00 -4.328006E-01 -3.982124E-01 -4.138078E-02 -9.676374E-02 -1.919297E-03 -3.944531E-02 -3.133733E-04 -1.554518E-02 -5.084822E-05 -4.815079E-03 -4.681450E-06 -tally 3: -1.635823E-03 -6.482611E-07 -1.248235E-03 -3.227247E-07 -1.248195E-03 -3.488233E-07 -1.030935E-03 -2.206418E-07 -5.670315E-04 -6.648507E-08 -2.393638E-04 -1.242517E-08 diff --git a/tests/regression_tests/random_ray_fixed_source/test.py b/tests/regression_tests/random_ray_fixed_source/test.py deleted file mode 100644 index 7ceadca5b93..00000000000 --- a/tests/regression_tests/random_ray_fixed_source/test.py +++ /dev/null @@ -1,340 +0,0 @@ -import os - -import numpy as np -import openmc -from openmc.utility_funcs import change_directory -import pytest - -from tests.testing_harness import TolerantPyAPITestHarness - -def fill_3d_list(n, val): - """ - Generates a 3D list of dimensions nxnxn filled with copies of val. - - Parameters: - n (int): The dimension of the 3D list. - val (any): The value to fill the 3D list with. - - Returns: - list: A 3D list of dimensions nxnxn filled with val. - """ - return [[[val for _ in range(n)] for _ in range(n)] for _ in range(n)] - - -class MGXSTestHarness(TolerantPyAPITestHarness): - def _cleanup(self): - super()._cleanup() - f = 'mgxs.h5' - if os.path.exists(f): - os.remove(f) - -def create_random_ray_model(domain_type): - openmc.reset_auto_ids() - ############################################################################### - # Create multigroup data - - # Instantiate the energy group data - ebins = [1e-5, 20.0e6] - groups = openmc.mgxs.EnergyGroups(group_edges=ebins) - - # High scattering ratio means system is all scattering - # Low means fully absorbing - scattering_ratio = 0.5 - - source_total_xs = 0.1 - source_mat_data = openmc.XSdata('source', groups) - source_mat_data.order = 0 - source_mat_data.set_total([source_total_xs]) - source_mat_data.set_absorption([source_total_xs * (1.0 - scattering_ratio)]) - source_mat_data.set_scatter_matrix(np.rollaxis(np.array([[[source_total_xs * scattering_ratio]]]),0,3)) - - void_total_xs = 1.0e-4 - void_mat_data = openmc.XSdata('void', groups) - void_mat_data.order = 0 - void_mat_data.set_total([void_total_xs]) - void_mat_data.set_absorption([void_total_xs * (1.0 - scattering_ratio)]) - void_mat_data.set_scatter_matrix(np.rollaxis(np.array([[[void_total_xs * scattering_ratio]]]),0,3)) - - shield_total_xs = 0.1 - shield_mat_data = openmc.XSdata('shield', groups) - shield_mat_data.order = 0 - shield_mat_data.set_total([shield_total_xs]) - shield_mat_data.set_absorption([shield_total_xs * (1.0 - scattering_ratio)]) - shield_mat_data.set_scatter_matrix(np.rollaxis(np.array([[[shield_total_xs * scattering_ratio]]]),0,3)) - - mg_cross_sections_file = openmc.MGXSLibrary(groups) - mg_cross_sections_file.add_xsdatas([source_mat_data, void_mat_data, shield_mat_data]) - mg_cross_sections_file.export_to_hdf5() - - ############################################################################### - # Create materials for the problem - - # Instantiate some Macroscopic Data - source_data = openmc.Macroscopic('source') - void_data = openmc.Macroscopic('void') - shield_data = openmc.Macroscopic('shield') - - # Instantiate some Materials and register the appropriate Macroscopic objects - source_mat = openmc.Material(name='source') - source_mat.set_density('macro', 1.0) - source_mat.add_macroscopic(source_data) - - void_mat = openmc.Material(name='void') - void_mat.set_density('macro', 1.0) - void_mat.add_macroscopic(void_data) - - shield_mat = openmc.Material(name='shield') - shield_mat.set_density('macro', 1.0) - shield_mat.add_macroscopic(shield_data) - - # Instantiate a Materials collection and export to XML - materials_file = openmc.Materials([source_mat, void_mat, shield_mat]) - materials_file.cross_sections = "mgxs.h5" - - ############################################################################### - # Define problem geometry - - source_cell = openmc.Cell(fill=source_mat, name='infinite source region') - void_cell = openmc.Cell(fill=void_mat, name='infinite void region') - shield_cell = openmc.Cell(fill=shield_mat, name='infinite shield region') - - sub = openmc.Universe() - sub.add_cells([source_cell]) - - vub = openmc.Universe() - vub.add_cells([void_cell]) - - aub = openmc.Universe() - aub.add_cells([shield_cell]) - - # n controls the dimension of subdivision within each outer lattice element - # E.g., n = 10 results in 1cm cubic FSRs - n = 2 - delta = 10.0 / n - ll = [-5.0, -5.0, -5.0] - pitch = [delta, delta, delta] - - source_lattice = openmc.RectLattice() - source_lattice.lower_left = ll - source_lattice.pitch = pitch - source_lattice.universes = fill_3d_list(n, sub) - - void_lattice = openmc.RectLattice() - void_lattice.lower_left = ll - void_lattice.pitch = pitch - void_lattice.universes = fill_3d_list(n, vub) - - shield_lattice = openmc.RectLattice() - shield_lattice.lower_left = ll - shield_lattice.pitch = pitch - shield_lattice.universes = fill_3d_list(n, aub) - - source_lattice_cell = openmc.Cell(fill=source_lattice, name='source lattice cell') - su = openmc.Universe() - su.add_cells([source_lattice_cell]) - - void_lattice_cell = openmc.Cell(fill=void_lattice, name='void lattice cell') - vu = openmc.Universe() - vu.add_cells([void_lattice_cell]) - - shield_lattice_cell = openmc.Cell(fill=shield_lattice, name='shield lattice cell') - au = openmc.Universe() - au.add_cells([shield_lattice_cell]) - - z_base = [ - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [vu, vu, vu, vu, au, au], - [vu, au, au, au, au, au], - [vu, au, au, au, au, au], - [vu, au, au, au, au, au], - [vu, au, au, au, au, au], - [su, au, au, au, au, au] - ] - - z_col = [ - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, vu, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au] - ] - - z_high = [ - [au, au, au, vu, au, au], - [au, au, au, vu, au, au], - [au, au, au, vu, au, au], - [au, au, au, vu, au, au], - [au, au, au, vu, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au] - ] - - z_cap = [ - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au], - [au, au, au, au, au, au] - ] - - dogleg_pattern = [ - z_base, - z_col, - z_col, - z_high, - z_cap, - z_cap - ] - - x = 60.0 - x_dim = 6 - - y = 100.0 - y_dim = 10 - - z = 60.0 - z_dim = 6 - - lattice = openmc.RectLattice() - lattice.lower_left = [0.0, 0.0, 0.0] - lattice.pitch = [x/x_dim, y/y_dim, z/z_dim] - lattice.universes = dogleg_pattern - - lattice_cell = openmc.Cell(fill=lattice, name='dogleg lattice cell') - - lattice_uni = openmc.Universe() - lattice_uni.add_cells([lattice_cell]) - - x_low = openmc.XPlane(x0=0.0,boundary_type='reflective') - x_high = openmc.XPlane(x0=x,boundary_type='vacuum') - - y_low = openmc.YPlane(y0=0.0,boundary_type='reflective') - y_high = openmc.YPlane(y0=y,boundary_type='vacuum') - - z_low = openmc.ZPlane(z0=0.0,boundary_type='reflective') - z_high = openmc.ZPlane(z0=z,boundary_type='vacuum') - - full_domain = openmc.Cell(fill=lattice_uni, region=+x_low & -x_high & +y_low & -y_high & +z_low & -z_high, name='full domain') - - root = openmc.Universe(name='root universe') - root.add_cell(full_domain) - - # Create a geometry with the two cells and export to XML - geometry = openmc.Geometry(root) - - ############################################################################### - # Define problem settings - - # Instantiate a Settings object, set all runtime parameters, and export to XML - settings = openmc.Settings() - settings.energy_mode = "multi-group" - settings.batches = 10 - settings.inactive = 5 - settings.particles = 1000 - settings.run_mode = 'fixed source' - - settings.random_ray['distance_active'] = 400.0 - settings.random_ray['distance_inactive'] = 100.0 - settings.random_ray['volume_normalized_flux_tallies'] = False - - # Create an initial uniform spatial source for ray integration - lower_left = (0.0, 0.0, 0.0) - upper_right = (x, y, z) - uniform_dist = openmc.stats.Box(lower_left, upper_right) - settings.random_ray['ray_source']= openmc.IndependentSource(space=uniform_dist) - - # Create the neutron source in the bottom right of the moderator - strengths = [1.0] - midpoints = [100.0] - energy_distribution = openmc.stats.Discrete(x=midpoints,p=strengths) - if domain_type == 'cell': - domain = source_lattice_cell - elif domain_type == 'material': - domain = source_mat - elif domain_type == 'universe': - domain = sub - source = openmc.IndependentSource( - energy=energy_distribution, - constraints={'domains': [domain]} - ) - settings.source = [source] - - ############################################################################### - # Define tallies - - estimator = 'analog' - - # Case 3A - mesh_3A = openmc.RegularMesh() - mesh_3A.dimension = (1, y_dim, 1) - mesh_3A.lower_left = (0.0, 0.0, 0.0) - mesh_3A.upper_right = (10.0, y, 10.0) - mesh_filter_3A = openmc.MeshFilter(mesh_3A) - - tally_3A = openmc.Tally(name="Case 3A") - tally_3A.filters = [mesh_filter_3A] - tally_3A.scores = ['flux'] - tally_3A.estimator = estimator - - # Case 3B - mesh_3B = openmc.RegularMesh() - mesh_3B.dimension = (x_dim, 1, 1) - mesh_3B.lower_left = (0.0, 50.0, 0.0) - mesh_3B.upper_right = (x, 60.0, 10.0) - mesh_filter_3B = openmc.MeshFilter(mesh_3B) - - tally_3B = openmc.Tally(name="Case 3B") - tally_3B.filters = [mesh_filter_3B] - tally_3B.scores = ['flux'] - tally_3B.estimator = estimator - - # Case 3C - mesh_3C = openmc.RegularMesh() - mesh_3C.dimension = (x_dim, 1, 1) - mesh_3C.lower_left = (0.0, 90.0, 30.0) - mesh_3C.upper_right = (x, 100.0, 40.0) - mesh_filter_3C = openmc.MeshFilter(mesh_3C) - - tally_3C = openmc.Tally(name="Case 3C") - tally_3C.filters = [mesh_filter_3C] - tally_3C.scores = ['flux'] - tally_3C.estimator = estimator - - # Instantiate a Tallies collection and export to XML - tallies = openmc.Tallies([tally_3A, tally_3B, tally_3C]) - - ############################################################################### - # Assmble Model - - model = openmc.model.Model() - model.geometry = geometry - model.materials = materials_file - model.settings = settings - model.xs_data = mg_cross_sections_file - model.tallies = tallies - - return model - -@pytest.mark.parametrize("domain_type", ["cell", "material", "universe"]) -def test_random_ray_fixed_source(domain_type): - with change_directory(domain_type): - model = create_random_ray_model(domain_type) - - harness = MGXSTestHarness('statepoint.10.h5', model) - harness.main() diff --git a/tests/regression_tests/random_ray_fixed_source/universe/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source/universe/inputs_true.dat deleted file mode 100644 index efbc03adf8c..00000000000 --- a/tests/regression_tests/random_ray_fixed_source/universe/inputs_true.dat +++ /dev/null @@ -1,205 +0,0 @@ - - - - mgxs.h5 - - - - - - - - - - - - - - - - - - - - - - - - 5.0 5.0 5.0 - 2 2 2 - -5.0 -5.0 -5.0 - -1 1 -1 1 - -1 1 -1 1 - - - 5.0 5.0 5.0 - 2 2 2 - -5.0 -5.0 -5.0 - -2 2 -2 2 - -2 2 -2 2 - - - 5.0 5.0 5.0 - 2 2 2 - -5.0 -5.0 -5.0 - -3 3 -3 3 - -3 3 -3 3 - - - 10.0 10.0 10.0 - 6 10 6 - 0.0 0.0 0.0 - -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -8 8 8 8 9 9 -8 9 9 9 9 9 -8 9 9 9 9 9 -8 9 9 9 9 9 -8 9 9 9 9 9 -7 9 9 9 9 9 - -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 8 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 - -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 8 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 - -9 9 9 8 9 9 -9 9 9 8 9 9 -9 9 9 8 9 9 -9 9 9 8 9 9 -9 9 9 8 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 - -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 - -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 -9 9 9 9 9 9 - - - - - - - - - - fixed source - 1000 - 10 - 5 - - - 100.0 1.0 - - - universe - 1 - - - multi-group - - 400.0 - 100.0 - False - - - 0.0 0.0 0.0 60.0 100.0 60.0 - - - - - - - 1 10 1 - 0.0 0.0 0.0 - 10.0 100.0 10.0 - - - 6 1 1 - 0.0 50.0 0.0 - 60.0 60.0 10.0 - - - 6 1 1 - 0.0 90.0 30.0 - 60.0 100.0 40.0 - - - 1 - - - 2 - - - 3 - - - 1 - flux - analog - - - 2 - flux - analog - - - 3 - flux - analog - - - diff --git a/tests/regression_tests/random_ray_fixed_source/universe/results_true.dat b/tests/regression_tests/random_ray_fixed_source/universe/results_true.dat deleted file mode 100644 index ba1254d384d..00000000000 --- a/tests/regression_tests/random_ray_fixed_source/universe/results_true.dat +++ /dev/null @@ -1,47 +0,0 @@ -tally 1: -3.751047E+01 -2.841797E+02 -1.000671E+01 -2.020214E+01 -3.979569E+00 -3.330838E+00 -2.552971E+00 -1.407542E+00 -1.736764E+00 -7.948004E-01 -1.178834E+00 -4.328006E-01 -6.953895E-01 -1.408391E-01 -2.389347E-01 -1.617450E-02 -8.128516E-02 -1.903860E-03 -2.764062E-02 -2.285458E-04 -tally 2: -1.178834E+00 -4.328006E-01 -3.982124E-01 -4.138078E-02 -9.676374E-02 -1.919297E-03 -3.944531E-02 -3.133733E-04 -1.554518E-02 -5.084822E-05 -4.815079E-03 -4.681450E-06 -tally 3: -1.635823E-03 -6.482611E-07 -1.248235E-03 -3.227247E-07 -1.248195E-03 -3.488233E-07 -1.030935E-03 -2.206418E-07 -5.670315E-04 -6.648507E-08 -2.393638E-04 -1.242517E-08 diff --git a/tests/regression_tests/random_ray_basic/__init__.py b/tests/regression_tests/random_ray_fixed_source_domain/__init__.py similarity index 100% rename from tests/regression_tests/random_ray_basic/__init__.py rename to tests/regression_tests/random_ray_fixed_source_domain/__init__.py diff --git a/tests/regression_tests/random_ray_fixed_source_domain/cell/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_domain/cell/inputs_true.dat new file mode 100644 index 00000000000..d6ca8918cd0 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_domain/cell/inputs_true.dat @@ -0,0 +1,244 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 10 + 5 + + + 100.0 1.0 + + + cell + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_fixed_source_domain/cell/results_true.dat b/tests/regression_tests/random_ray_fixed_source_domain/cell/results_true.dat new file mode 100644 index 00000000000..5f297586075 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_domain/cell/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +-5.745886E+02 +9.758367E+04 +tally 2: +2.971927E-02 +1.827222E-04 +tally 3: +1.978393E-03 +7.951531E-07 diff --git a/tests/regression_tests/random_ray_fixed_source_domain/material/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_domain/material/inputs_true.dat new file mode 100644 index 00000000000..7fae491aea4 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_domain/material/inputs_true.dat @@ -0,0 +1,244 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 10 + 5 + + + 100.0 1.0 + + + material + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_fixed_source_domain/material/results_true.dat b/tests/regression_tests/random_ray_fixed_source_domain/material/results_true.dat new file mode 100644 index 00000000000..5f297586075 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_domain/material/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +-5.745886E+02 +9.758367E+04 +tally 2: +2.971927E-02 +1.827222E-04 +tally 3: +1.978393E-03 +7.951531E-07 diff --git a/tests/regression_tests/random_ray_fixed_source_domain/test.py b/tests/regression_tests/random_ray_fixed_source_domain/test.py new file mode 100644 index 00000000000..5885a92009a --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_domain/test.py @@ -0,0 +1,51 @@ +import os + +import openmc +from openmc.utility_funcs import change_directory +from openmc.examples import random_ray_three_region_cube +import pytest + +from tests.testing_harness import TolerantPyAPITestHarness + + +class MGXSTestHarness(TolerantPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + + +@pytest.mark.parametrize("domain_type", ["cell", "material", "universe"]) +def test_random_ray_fixed_source(domain_type): + with change_directory(domain_type): + openmc.reset_auto_ids() + model = random_ray_three_region_cube() + + # Based on the parameter, we need to adjust + # the particle source constraints + source = model.settings.source[0] + constraints = source.constraints + + if domain_type == 'cell': + cells = model.geometry.get_all_cells() + for key, cell in cells.items(): + print(cell.name) + if cell.name == 'infinite source region': + constraints['domain_type'] = 'cell' + constraints['domain_ids'] = [cell.id] + elif domain_type == 'material': + materials = model.materials + for material in materials: + if material.name == 'source': + constraints['domain_type'] = 'material' + constraints['domain_ids'] = [material.id] + elif domain_type == 'universe': + universes = model.geometry.get_all_universes() + for key, universe in universes.items(): + if universe.name == 'source universe': + constraints['domain_type'] = 'universe' + constraints['domain_ids'] = [universe.id] + + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/regression_tests/random_ray_fixed_source_domain/universe/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_domain/universe/inputs_true.dat new file mode 100644 index 00000000000..75d84efe978 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_domain/universe/inputs_true.dat @@ -0,0 +1,244 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 10 + 5 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_fixed_source_domain/universe/results_true.dat b/tests/regression_tests/random_ray_fixed_source_domain/universe/results_true.dat new file mode 100644 index 00000000000..5f297586075 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_domain/universe/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +-5.745886E+02 +9.758367E+04 +tally 2: +2.971927E-02 +1.827222E-04 +tally 3: +1.978393E-03 +7.951531E-07 diff --git a/tests/regression_tests/random_ray_fixed_source_normalization/False/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_normalization/False/inputs_true.dat new file mode 100644 index 00000000000..559cff0f0df --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_normalization/False/inputs_true.dat @@ -0,0 +1,244 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 10 + 5 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + False + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_fixed_source_normalization/False/results_true.dat b/tests/regression_tests/random_ray_fixed_source_normalization/False/results_true.dat new file mode 100644 index 00000000000..dbe778df9bf --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_normalization/False/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +-6.614590E+04 +1.283943E+09 +tally 2: +4.612657E+02 +4.402080E+04 +tally 3: +2.248302E+01 +1.026884E+02 diff --git a/tests/regression_tests/random_ray_fixed_source_normalization/True/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_normalization/True/inputs_true.dat new file mode 100644 index 00000000000..75d84efe978 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_normalization/True/inputs_true.dat @@ -0,0 +1,244 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 10 + 5 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_fixed_source_normalization/True/results_true.dat b/tests/regression_tests/random_ray_fixed_source_normalization/True/results_true.dat new file mode 100644 index 00000000000..5f297586075 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_normalization/True/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +-5.745886E+02 +9.758367E+04 +tally 2: +2.971927E-02 +1.827222E-04 +tally 3: +1.978393E-03 +7.951531E-07 diff --git a/tests/regression_tests/random_ray_cube/__init__.py b/tests/regression_tests/random_ray_fixed_source_normalization/__init__.py similarity index 100% rename from tests/regression_tests/random_ray_cube/__init__.py rename to tests/regression_tests/random_ray_fixed_source_normalization/__init__.py diff --git a/tests/regression_tests/random_ray_fixed_source_normalization/test.py b/tests/regression_tests/random_ray_fixed_source_normalization/test.py new file mode 100644 index 00000000000..3fa4ba2a63f --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_normalization/test.py @@ -0,0 +1,27 @@ +import os + +import openmc +from openmc.utility_funcs import change_directory +from openmc.examples import random_ray_three_region_cube +import pytest + +from tests.testing_harness import TolerantPyAPITestHarness + + +class MGXSTestHarness(TolerantPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + + +@pytest.mark.parametrize("normalize", [True, False]) +def test_random_ray_fixed_source(normalize): + with change_directory(str(normalize)): + openmc.reset_auto_ids() + model = random_ray_three_region_cube() + model.settings.random_ray['volume_normalized_flux_tallies'] = normalize + + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/regression_tests/random_ray_fixed_source/__init__.py b/tests/regression_tests/random_ray_k_eff/__init__.py similarity index 100% rename from tests/regression_tests/random_ray_fixed_source/__init__.py rename to tests/regression_tests/random_ray_k_eff/__init__.py diff --git a/tests/regression_tests/random_ray_basic/inputs_true.dat b/tests/regression_tests/random_ray_k_eff/inputs_true.dat similarity index 100% rename from tests/regression_tests/random_ray_basic/inputs_true.dat rename to tests/regression_tests/random_ray_k_eff/inputs_true.dat diff --git a/tests/regression_tests/random_ray_basic/results_true.dat b/tests/regression_tests/random_ray_k_eff/results_true.dat similarity index 100% rename from tests/regression_tests/random_ray_basic/results_true.dat rename to tests/regression_tests/random_ray_k_eff/results_true.dat diff --git a/tests/regression_tests/random_ray_k_eff/test.py b/tests/regression_tests/random_ray_k_eff/test.py new file mode 100644 index 00000000000..8dd0dd9155c --- /dev/null +++ b/tests/regression_tests/random_ray_k_eff/test.py @@ -0,0 +1,19 @@ +import os + +from openmc.examples import random_ray_lattice + +from tests.testing_harness import TolerantPyAPITestHarness + + +class MGXSTestHarness(TolerantPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + + +def test_random_ray_basic(): + model = random_ray_lattice() + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/regression_tests/random_ray_vacuum/__init__.py b/tests/regression_tests/random_ray_vacuum/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/regression_tests/random_ray_vacuum/inputs_true.dat b/tests/regression_tests/random_ray_vacuum/inputs_true.dat deleted file mode 100644 index 5b2fa090596..00000000000 --- a/tests/regression_tests/random_ray_vacuum/inputs_true.dat +++ /dev/null @@ -1,109 +0,0 @@ - - - - mgxs.h5 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0.126 0.126 - 10 10 - -0.63 -0.63 - -3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 -3 3 3 3 3 3 3 3 3 3 - - - 1.26 1.26 - 2 2 - -1.26 -1.26 - -2 2 -2 5 - - - - - - - - - - - - - - - - - - - - - eigenvalue - 100 - 10 - 5 - multi-group - - 100.0 - 20.0 - - - -1.26 -1.26 -1 1.26 1.26 1 - - - True - - - - - 2 2 - -1.26 -1.26 - 1.26 1.26 - - - 1 - - - 1e-05 0.0635 10.0 100.0 1000.0 500000.0 1000000.0 20000000.0 - - - 1 2 - flux fission nu-fission - analog - - - diff --git a/tests/regression_tests/random_ray_vacuum/results_true.dat b/tests/regression_tests/random_ray_vacuum/results_true.dat deleted file mode 100644 index c25999aad5a..00000000000 --- a/tests/regression_tests/random_ray_vacuum/results_true.dat +++ /dev/null @@ -1,171 +0,0 @@ -k-combined: -1.010455E-01 1.585558E-02 -tally 1: -7.466984E-01 -1.245920E-01 -2.771079E-01 -1.714504E-02 -6.744252E-01 -1.015566E-01 -1.634870E-01 -6.288701E-03 -2.405120E-02 -1.362874E-04 -5.853580E-02 -8.072823E-04 -1.641162E-02 -6.568765E-05 -5.223801E-04 -6.753516E-08 -1.271369E-03 -4.000365E-07 -3.014736E-02 -1.922973E-04 -9.765325E-04 -2.043645E-07 -2.376685E-03 -1.210529E-06 -1.562379E-01 -4.906526E-03 -1.746664E-03 -6.141658E-07 -4.251084E-03 -3.638028E-06 -1.826385E+00 -6.678141E-01 -3.095716E-03 -1.920117E-06 -7.660132E-03 -1.175650E-05 -2.013809E+00 -8.136726E-01 -3.015545E-02 -1.832201E-04 -8.387586E-02 -1.417475E-03 -1.573069E+00 -5.388374E-01 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.867849E-01 -1.864798E-02 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.136372E-02 -1.035499E-04 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -3.973323E-02 -3.229766E-04 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.779974E-01 -6.350613E-03 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.036886E+00 -2.151147E-01 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.109991E+00 -2.468005E-01 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -5.813217E-01 -7.704274E-02 -2.160141E-01 -1.062660E-02 -5.257350E-01 -6.294540E-02 -1.358032E-01 -4.462239E-03 -2.030839E-02 -1.002755E-04 -4.942655E-02 -5.939703E-04 -1.612888E-02 -6.604109E-05 -5.274425E-04 -7.250333E-08 -1.283689E-03 -4.294649E-07 -2.985723E-02 -1.925418E-04 -9.868482E-04 -2.142916E-07 -2.401791E-03 -1.269331E-06 -1.590678E-01 -5.110940E-03 -1.800903E-03 -6.566490E-07 -4.383091E-03 -3.889678E-06 -1.928611E+00 -7.475889E-01 -3.310101E-03 -2.209547E-06 -8.190613E-03 -1.352862E-05 -2.172777E+00 -9.544513E-01 -3.334566E-02 -2.267149E-04 -9.274926E-02 -1.753971E-03 -7.566911E-01 -1.278105E-01 -2.881892E-01 -1.854375E-02 -7.013949E-01 -1.098417E-01 -1.662732E-01 -6.495387E-03 -2.515274E-02 -1.488430E-04 -6.121675E-02 -8.816538E-04 -1.682747E-02 -6.933017E-05 -5.514441E-04 -7.567901E-08 -1.342104E-03 -4.482757E-07 -3.068773E-02 -1.997062E-04 -1.021094E-03 -2.240938E-07 -2.485139E-03 -1.327393E-06 -1.578722E-01 -5.013656E-03 -1.812622E-03 -6.620083E-07 -4.411613E-03 -3.921424E-06 -1.922798E+00 -7.400474E-01 -3.401690E-03 -2.320513E-06 -8.417243E-03 -1.420805E-05 -2.178318E+00 -9.546106E-01 -3.451316E-02 -2.421994E-04 -9.599661E-02 -1.873766E-03 diff --git a/tests/regression_tests/random_ray_vacuum/test.py b/tests/regression_tests/random_ray_vacuum/test.py deleted file mode 100644 index 3923b8a815f..00000000000 --- a/tests/regression_tests/random_ray_vacuum/test.py +++ /dev/null @@ -1,236 +0,0 @@ -import os - -import numpy as np -import openmc - -from tests.testing_harness import TolerantPyAPITestHarness - - -class MGXSTestHarness(TolerantPyAPITestHarness): - def _cleanup(self): - super()._cleanup() - f = 'mgxs.h5' - if os.path.exists(f): - os.remove(f) - - -def random_ray_model() -> openmc.Model: - ############################################################################### - # Create multigroup data - - # Instantiate the energy group data - group_edges = [1e-5, 0.0635, 10.0, 1.0e2, 1.0e3, 0.5e6, 1.0e6, 20.0e6] - groups = openmc.mgxs.EnergyGroups(group_edges) - - # Instantiate the 7-group (C5G7) cross section data - uo2_xsdata = openmc.XSdata('UO2', groups) - uo2_xsdata.order = 0 - uo2_xsdata.set_total( - [0.1779492, 0.3298048, 0.4803882, 0.5543674, 0.3118013, 0.3951678, - 0.5644058]) - uo2_xsdata.set_absorption([8.0248e-03, 3.7174e-03, 2.6769e-02, 9.6236e-02, - 3.0020e-02, 1.1126e-01, 2.8278e-01]) - scatter_matrix = np.array( - [[[0.1275370, 0.0423780, 0.0000094, 0.0000000, 0.0000000, 0.0000000, 0.0000000], - [0.0000000, 0.3244560, 0.0016314, 0.0000000, 0.0000000, 0.0000000, 0.0000000], - [0.0000000, 0.0000000, 0.4509400, 0.0026792, 0.0000000, 0.0000000, 0.0000000], - [0.0000000, 0.0000000, 0.0000000, 0.4525650, 0.0055664, 0.0000000, 0.0000000], - [0.0000000, 0.0000000, 0.0000000, 0.0001253, 0.2714010, 0.0102550, 0.0000000], - [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0012968, 0.2658020, 0.0168090], - [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0085458, 0.2730800]]]) - scatter_matrix = np.rollaxis(scatter_matrix, 0, 3) - uo2_xsdata.set_scatter_matrix(scatter_matrix) - uo2_xsdata.set_fission([7.21206e-03, 8.19301e-04, 6.45320e-03, - 1.85648e-02, 1.78084e-02, 8.30348e-02, - 2.16004e-01]) - uo2_xsdata.set_nu_fission([2.005998e-02, 2.027303e-03, 1.570599e-02, - 4.518301e-02, 4.334208e-02, 2.020901e-01, - 5.257105e-01]) - uo2_xsdata.set_chi([5.8791e-01, 4.1176e-01, 3.3906e-04, 1.1761e-07, 0.0000e+00, - 0.0000e+00, 0.0000e+00]) - - h2o_xsdata = openmc.XSdata('LWTR', groups) - h2o_xsdata.order = 0 - h2o_xsdata.set_total([0.15920605, 0.412969593, 0.59030986, 0.58435, - 0.718, 1.2544497, 2.650379]) - h2o_xsdata.set_absorption([6.0105e-04, 1.5793e-05, 3.3716e-04, - 1.9406e-03, 5.7416e-03, 1.5001e-02, - 3.7239e-02]) - scatter_matrix = np.array( - [[[0.0444777, 0.1134000, 0.0007235, 0.0000037, 0.0000001, 0.0000000, 0.0000000], - [0.0000000, 0.2823340, 0.1299400, 0.0006234, 0.0000480, 0.0000074, 0.0000010], - [0.0000000, 0.0000000, 0.3452560, 0.2245700, 0.0169990, 0.0026443, 0.0005034], - [0.0000000, 0.0000000, 0.0000000, 0.0910284, 0.4155100, 0.0637320, 0.0121390], - [0.0000000, 0.0000000, 0.0000000, 0.0000714, 0.1391380, 0.5118200, 0.0612290], - [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0022157, 0.6999130, 0.5373200], - [0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.0000000, 0.1324400, 2.4807000]]]) - scatter_matrix = np.rollaxis(scatter_matrix, 0, 3) - h2o_xsdata.set_scatter_matrix(scatter_matrix) - - mg_cross_sections = openmc.MGXSLibrary(groups) - mg_cross_sections.add_xsdatas([uo2_xsdata, h2o_xsdata]) - mg_cross_sections.export_to_hdf5() - - ############################################################################### - # Create materials for the problem - - # Instantiate some Materials and register the appropriate Macroscopic objects - uo2 = openmc.Material(name='UO2 fuel') - uo2.set_density('macro', 1.0) - uo2.add_macroscopic('UO2') - - water = openmc.Material(name='Water') - water.set_density('macro', 1.0) - water.add_macroscopic('LWTR') - - # Instantiate a Materials collection and export to XML - materials = openmc.Materials([uo2, water]) - materials.cross_sections = "mgxs.h5" - - ############################################################################### - # Define problem geometry - - ######################################## - # Define an unbounded pincell universe - - pitch = 1.26 - - # Create a surface for the fuel outer radius - fuel_or = openmc.ZCylinder(r=0.54, name='Fuel OR') - inner_ring_a = openmc.ZCylinder(r=0.33, name='inner ring a') - inner_ring_b = openmc.ZCylinder(r=0.45, name='inner ring b') - outer_ring_a = openmc.ZCylinder(r=0.60, name='outer ring a') - outer_ring_b = openmc.ZCylinder(r=0.69, name='outer ring b') - - # Instantiate Cells - fuel_a = openmc.Cell(fill=uo2, region=-inner_ring_a, name='fuel inner a') - fuel_b = openmc.Cell(fill=uo2, region=+inner_ring_a & -inner_ring_b, name='fuel inner b') - fuel_c = openmc.Cell(fill=uo2, region=+inner_ring_b & -fuel_or, name='fuel inner c') - moderator_a = openmc.Cell(fill=water, region=+fuel_or & -outer_ring_a, name='moderator inner a') - moderator_b = openmc.Cell(fill=water, region=+outer_ring_a & -outer_ring_b, name='moderator outer b') - moderator_c = openmc.Cell(fill=water, region=+outer_ring_b, name='moderator outer c') - - # Create pincell universe - pincell_base = openmc.Universe() - - # Register Cells with Universe - pincell_base.add_cells([fuel_a, fuel_b, fuel_c, moderator_a, moderator_b, moderator_c]) - - # Create planes for azimuthal sectors - azimuthal_planes = [] - for i in range(8): - angle = 2 * i * openmc.pi / 8 - normal_vector = (-openmc.sin(angle), openmc.cos(angle), 0) - azimuthal_planes.append(openmc.Plane(a=normal_vector[0], b=normal_vector[1], c=normal_vector[2], d=0)) - - # Create a cell for each azimuthal sector - azimuthal_cells = [] - for i in range(8): - azimuthal_cell = openmc.Cell(name=f'azimuthal_cell_{i}') - azimuthal_cell.fill = pincell_base - azimuthal_cell.region = +azimuthal_planes[i] & -azimuthal_planes[(i+1) % 8] - azimuthal_cells.append(azimuthal_cell) - - # Create a geometry with the azimuthal universes - pincell = openmc.Universe(cells=azimuthal_cells) - - ######################################## - # Define a moderator lattice universe - - moderator_infinite = openmc.Cell(fill=water, name='moderator infinite') - mu = openmc.Universe() - mu.add_cells([moderator_infinite]) - - lattice = openmc.RectLattice() - lattice.lower_left = [-pitch/2.0, -pitch/2.0] - lattice.pitch = [pitch/10.0, pitch/10.0] - lattice.universes = np.full((10, 10), mu) - - mod_lattice_cell = openmc.Cell(fill=lattice) - - mod_lattice_uni = openmc.Universe() - - mod_lattice_uni.add_cells([mod_lattice_cell]) - - ######################################## - # Define 2x2 outer lattice - lattice2x2 = openmc.RectLattice() - lattice2x2.lower_left = [-pitch, -pitch] - lattice2x2.pitch = [pitch, pitch] - lattice2x2.universes = [ - [pincell, pincell], - [pincell, mod_lattice_uni] - ] - - ######################################## - # Define cell containing lattice and other stuff - box = openmc.model.RectangularPrism(pitch*2, pitch*2, boundary_type='vacuum') - - assembly = openmc.Cell(fill=lattice2x2, region=-box, name='assembly') - - root = openmc.Universe(name='root universe') - root.add_cell(assembly) - - # Create a geometry with the two cells and export to XML - geometry = openmc.Geometry(root) - - ############################################################################### - # Define problem settings - - # Instantiate a Settings object, set all runtime parameters, and export to XML - settings = openmc.Settings() - settings.energy_mode = "multi-group" - settings.batches = 10 - settings.inactive = 5 - settings.particles = 100 - - # Create an initial uniform spatial source distribution over fissionable zones - lower_left = (-pitch, -pitch, -1) - upper_right = (pitch, pitch, 1) - uniform_dist = openmc.stats.Box(lower_left, upper_right) - rr_source = openmc.IndependentSource(space=uniform_dist) - - settings.random_ray['distance_active'] = 100.0 - settings.random_ray['distance_inactive'] = 20.0 - settings.random_ray['ray_source'] = rr_source - settings.random_ray['volume_normalized_flux_tallies'] = True - - ############################################################################### - # Define tallies - - # Create a mesh that will be used for tallying - mesh = openmc.RegularMesh() - mesh.dimension = (2, 2) - mesh.lower_left = (-pitch, -pitch) - mesh.upper_right = (pitch, pitch) - - # Create a mesh filter that can be used in a tally - mesh_filter = openmc.MeshFilter(mesh) - - # Create an energy group filter as well - energy_filter = openmc.EnergyFilter(group_edges) - - # Now use the mesh filter in a tally and indicate what scores are desired - tally = openmc.Tally(name="Mesh tally") - tally.filters = [mesh_filter, energy_filter] - tally.scores = ['flux', 'fission', 'nu-fission'] - tally.estimator = 'analog' - - # Instantiate a Tallies collection and export to XML - tallies = openmc.Tallies([tally]) - - ############################################################################### - # Exporting to OpenMC model - ############################################################################### - - model = openmc.Model() - model.geometry = geometry - model.materials = materials - model.settings = settings - model.tallies = tallies - return model - - -def test_random_ray_vacuum(): - harness = MGXSTestHarness('statepoint.10.h5', random_ray_model()) - harness.main() From b11f8b73ca3a53913d75846562c4a4457076a1ef Mon Sep 17 00:00:00 2001 From: John Vincent Cauilan <64677361+johvincau@users.noreply.github.com> Date: Mon, 8 Jul 2024 09:37:17 -0500 Subject: [PATCH 033/184] Enforce sequence type when setting Setting.track (#3071) --- openmc/settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openmc/settings.py b/openmc/settings.py index b048c3a8b34..9731f54ecb1 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -1,4 +1,4 @@ -from collections.abc import Iterable, Mapping, MutableSequence +from collections.abc import Iterable, Mapping, MutableSequence, Sequence from enum import Enum import itertools from math import ceil @@ -819,7 +819,7 @@ def track(self) -> typing.Iterable[typing.Iterable[int]]: @track.setter def track(self, track: typing.Iterable[typing.Iterable[int]]): - cv.check_type('track', track, Iterable) + cv.check_type('track', track, Sequence) for t in track: if len(t) != 3: msg = f'Unable to set the track to "{t}" since its length is not 3' From e74dc5037ebfc575a72ed963c2ab148fc374b5ee Mon Sep 17 00:00:00 2001 From: John Vincent Cauilan <64677361+johvincau@users.noreply.github.com> Date: Mon, 8 Jul 2024 16:29:49 -0500 Subject: [PATCH 034/184] Enforce non-negative percents for material.add_nuclide to prevent unintended ao/wo flipping (#3075) --- openmc/material.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openmc/material.py b/openmc/material.py index 6edc372161c..4b871b77d60 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -519,6 +519,7 @@ def add_nuclide(self, nuclide: str, percent: float, percent_type: str = 'ao'): cv.check_type('nuclide', nuclide, str) cv.check_type('percent', percent, Real) cv.check_value('percent type', percent_type, {'ao', 'wo'}) + cv.check_greater_than('percent', percent, 0, equality=True) if self._macroscopic is not None: msg = 'Unable to add a Nuclide to Material ID="{}" as a ' \ @@ -727,6 +728,7 @@ def add_element(self, element: str, percent: float, percent_type: str = 'ao', cv.check_type('nuclide', element, str) cv.check_type('percent', percent, Real) + cv.check_greater_than('percent', percent, 0, equality=True) cv.check_value('percent type', percent_type, {'ao', 'wo'}) # Make sure element name is just that From a5a26bb749d08618ea88e160af955890e799afe0 Mon Sep 17 00:00:00 2001 From: Nicolas Linden <57541749+nplinden@users.noreply.github.com> Date: Tue, 9 Jul 2024 19:30:09 +0200 Subject: [PATCH 035/184] Add -DCMAKE_BUILD_TYPE=Release flag for MOAB in Dockerfile (#3077) Co-authored-by: Nicolas Linden --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 107df4afe76..35b9cf578d6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -111,7 +111,8 @@ RUN if [ "$build_dagmc" = "on" ]; then \ mkdir -p $HOME/MOAB && cd $HOME/MOAB \ && git clone --single-branch -b ${MOAB_TAG} --depth 1 ${MOAB_REPO} \ && mkdir build && cd build \ - && cmake ../moab -DENABLE_HDF5=ON \ + && cmake ../moab -DCMAKE_BUILD_TYPE=Release \ + -DENABLE_HDF5=ON \ -DENABLE_NETCDF=ON \ -DBUILD_SHARED_LIBS=OFF \ -DENABLE_FORTRAN=OFF \ From cbec1ab035a4b4aaca8a102889de59a864b16813 Mon Sep 17 00:00:00 2001 From: John Vincent Cauilan <64677361+johvincau@users.noreply.github.com> Date: Wed, 10 Jul 2024 17:04:09 -0500 Subject: [PATCH 036/184] Correct openmc.Geometry initializer to accept iterables of openmc.Cell (#3081) --- openmc/geometry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmc/geometry.py b/openmc/geometry.py index db927c46ec1..e37a69c73d2 100644 --- a/openmc/geometry.py +++ b/openmc/geometry.py @@ -41,7 +41,7 @@ class Geometry: def __init__( self, - root: typing.Optional[openmc.UniverseBase] = None, + root: openmc.UniverseBase | typing.Iterable[openmc.Cell] | None = None, merge_surfaces: bool = False, surface_precision: int = 10 ): From 2107af5e2fb279d4dc8842e7c6055e72ecd3da86 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Fri, 12 Jul 2024 20:17:25 +0200 Subject: [PATCH 037/184] Moving most of setup.py to pyproject.toml (#3074) Co-authored-by: Jon Shimwell Co-authored-by: Paul Romano --- .readthedocs.yaml | 7 ++-- docs/requirements-rtd.txt | 13 ------- openmc/__init__.py | 3 +- pyproject.toml | 72 +++++++++++++++++++++++++++++++++++++++ setup.py | 67 +----------------------------------- 5 files changed, 80 insertions(+), 82 deletions(-) delete mode 100644 docs/requirements-rtd.txt diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 98ca8b581da..b7b69f9fe59 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -3,11 +3,14 @@ version: 2 build: os: "ubuntu-20.04" tools: - python: "3.9" + python: "3.10" sphinx: configuration: docs/source/conf.py python: install: - - requirements: docs/requirements-rtd.txt + - method: pip + path: . + extra_requirements: + - docs diff --git a/docs/requirements-rtd.txt b/docs/requirements-rtd.txt deleted file mode 100644 index df033351705..00000000000 --- a/docs/requirements-rtd.txt +++ /dev/null @@ -1,13 +0,0 @@ -sphinx==5.0.2 -sphinx_rtd_theme==1.0.0 -sphinx-numfig -jupyter -sphinxcontrib-katex -sphinxcontrib-svg2pdfconverter -numpy -scipy -h5py -pandas -uncertainties -matplotlib -lxml diff --git a/openmc/__init__.py b/openmc/__init__.py index e589d35efc9..566d287068f 100644 --- a/openmc/__init__.py +++ b/openmc/__init__.py @@ -1,3 +1,4 @@ +import importlib.metadata from openmc.arithmetic import * from openmc.bounding_box import * from openmc.cell import * @@ -40,4 +41,4 @@ from . import examples -__version__ = '0.15.1-dev' +__version__ = importlib.metadata.version("openmc") diff --git a/pyproject.toml b/pyproject.toml index d5970617a74..ca646323b24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,74 @@ [build-system] requires = ["setuptools", "wheel", "numpy", "cython"] + +[project] +name = "openmc" +authors = [ + {name = "The OpenMC Development Team", email = "openmc@anl.gov"}, +] +description = "OpenMC" +version = "0.15.1-dev" +requires-python = ">=3.10" +license = {file = "LICENSE"} +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: End Users/Desktop", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Topic :: Scientific/Engineering", + "Programming Language :: C++", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +dependencies = [ + "numpy", + "h5py", + "scipy", + "ipython", + "matplotlib", + "pandas", + "lxml", + "uncertainties", + "setuptools", +] + +[project.optional-dependencies] +depletion-mpi = ["mpi4py"] +docs = [ + "sphinx==5.0.2", + "sphinxcontrib-katex", + "sphinx-numfig", + "jupyter", + "sphinxcontrib-svg2pdfconverter", + "sphinx-rtd-theme==1.0.0" +] +test = ["pytest", "pytest-cov", "colorama", "openpyxl"] +vtk = ["vtk"] + +[project.urls] +Homepage = "https://openmc.org" +Documentation = "https://docs.openmc.org" +Repository = "https://github.com/openmc-dev/openmc" +Issues = "https://github.com/openmc-dev/openmc/issues" + +[tool.setuptools.packages.find] +include = ['openmc*', 'scripts*'] +exclude = ['tests*'] + +[tool.setuptools.package-data] +"openmc.data.effective_dose" = ["*.txt"] +"openmc.data" = ["*.txt", "*.DAT", "*.json", "*.h5"] +"openmc.lib" = ["libopenmc.dylib", "libopenmc.so"] + +[project.scripts] +openmc-ace-to-hdf5 = "scripts.openmc_ace_to_hdf5:main" +openmc-plot-mesh-tally = "scripts.openmc_plot_mesh_tally:main" +openmc-track-combine = "scripts.openmc_track_combine:main" +openmc-track-to-vtk = "scripts.openmc_track_to_vtk:main" +openmc-update-inputs = "scripts.openmc_update_inputs:main" +openmc-update-mgxs = "scripts.openmc_update_mgxs:main" +openmc-voxel-to-vtk = "scripts.openmc_voxel_to_vtk:main" diff --git a/setup.py b/setup.py index de87e1490ad..4e24f48a123 100755 --- a/setup.py +++ b/setup.py @@ -1,76 +1,11 @@ #!/usr/bin/env python -import glob -import sys import numpy as np - -from setuptools import setup, find_packages +from setuptools import setup from Cython.Build import cythonize -# Determine shared library suffix -if sys.platform == 'darwin': - suffix = 'dylib' -else: - suffix = 'so' - -# Get version information from __init__.py. This is ugly, but more reliable than -# using an import. -with open('openmc/__init__.py', 'r') as f: - version = f.readlines()[-1].split()[-1].strip("'") - kwargs = { - 'name': 'openmc', - 'version': version, - 'packages': find_packages(exclude=['tests*']), - 'scripts': glob.glob('scripts/openmc-*'), - - # Data files and libraries - 'package_data': { - 'openmc.lib': ['libopenmc.{}'.format(suffix)], - 'openmc.data': ['mass_1.mas20.txt', 'BREMX.DAT', 'half_life.json', '*.h5'], - 'openmc.data.effective_dose': ['*.txt'] - }, - - # Metadata - 'author': 'The OpenMC Development Team', - 'author_email': 'openmc@anl.gov', - 'description': 'OpenMC', - 'url': 'https://openmc.org', - 'download_url': 'https://github.com/openmc-dev/openmc/releases', - 'project_urls': { - 'Issue Tracker': 'https://github.com/openmc-dev/openmc/issues', - 'Documentation': 'https://docs.openmc.org', - 'Source Code': 'https://github.com/openmc-dev/openmc', - }, - 'classifiers': [ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'Intended Audience :: End Users/Desktop', - 'Intended Audience :: Science/Research', - 'License :: OSI Approved :: MIT License', - 'Natural Language :: English', - 'Topic :: Scientific/Engineering' - 'Programming Language :: C++', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - ], - - # Dependencies - 'python_requires': '>=3.10', - 'install_requires': [ - 'numpy>=1.9', 'h5py', 'scipy', 'ipython', 'matplotlib', - 'pandas', 'lxml', 'uncertainties', 'setuptools' - ], - 'extras_require': { - 'depletion-mpi': ['mpi4py'], - 'docs': ['sphinx', 'sphinxcontrib-katex', 'sphinx-numfig', 'jupyter', - 'sphinxcontrib-svg2pdfconverter', 'sphinx-rtd-theme'], - 'test': ['pytest', 'pytest-cov', 'colorama', 'openpyxl'], - 'vtk': ['vtk'], - }, # Cython is used to add resonance reconstruction and fast float_endf 'ext_modules': cythonize('openmc/data/*.pyx'), 'include_dirs': [np.get_include()] From 58f9092a68ec758240252c226720014c252b3039 Mon Sep 17 00:00:00 2001 From: John Vincent Cauilan <64677361+johvincau@users.noreply.github.com> Date: Tue, 16 Jul 2024 16:59:16 -0500 Subject: [PATCH 038/184] Include batch statistics discussion in methodology introduction (#3076) Co-authored-by: Paul Romano --- docs/source/methods/tallies.rst | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/source/methods/tallies.rst b/docs/source/methods/tallies.rst index d833c157d5d..4b56d39559a 100644 --- a/docs/source/methods/tallies.rst +++ b/docs/source/methods/tallies.rst @@ -4,9 +4,9 @@ Tallies ======= -Note that the methods discussed in this section are written specifically for -continuous-energy mode but equivalent apply to the multi-group mode if the -particle's energy is replaced with the particle's group +The methods discussed in this section are written specifically for continuous- +energy mode. However, they can also apply to the multi-group mode if the +particle's energy is instead interpreted as the particle's group. ------------------ Filters and Scores @@ -207,6 +207,8 @@ the change-in-angle), we must use an analog estimator. .. TODO: Add description of surface current tallies +.. _tallies_statistics: + ---------- Statistics ---------- @@ -268,6 +270,14 @@ normal, log-normal, Weibull, etc. The central limit theorem states that as Estimating Statistics of a Random Variable ------------------------------------------ +After running OpenMC, each tallied quantity has a reported mean and standard +deviation. The below sections explain how these quantities are computed. Note +that OpenMC uses **batch statistics**, meaning that each observation for a tally +random variable corresponds to the aggregation of tally contributions from +multiple source particles that are grouped together into a single batch. See +:ref:`usersguide_particles` for more information on how the number of source +particles and statistical batches are specified. + Mean ++++ From fd47df4bb23f011f2d3ab2b59597efa21d0258f6 Mon Sep 17 00:00:00 2001 From: Rufus <123552806+RufusN@users.noreply.github.com> Date: Wed, 17 Jul 2024 01:53:40 +0100 Subject: [PATCH 039/184] Linear Source Random Ray (#3072) Co-authored-by: John Tramm Co-authored-by: Paul Romano --- CMakeLists.txt | 2 + docs/source/methods/random_ray.rst | 184 ++++++++++- docs/source/usersguide/random_ray.rst | 28 ++ include/openmc/constants.h | 2 + .../openmc/random_ray/flat_source_domain.h | 21 +- .../openmc/random_ray/linear_source_domain.h | 61 ++++ include/openmc/random_ray/moment_matrix.h | 90 ++++++ include/openmc/random_ray/random_ray.h | 13 +- .../openmc/random_ray/random_ray_simulation.h | 3 +- openmc/settings.py | 8 + src/random_ray/flat_source_domain.cpp | 28 +- src/random_ray/linear_source_domain.cpp | 269 ++++++++++++++++ src/random_ray/moment_matrix.cpp | 84 +++++ src/random_ray/random_ray.cpp | 291 +++++++++++++++++- src/random_ray/random_ray_simulation.cpp | 78 +++-- src/settings.cpp | 13 + .../__init__.py | 0 .../linear/inputs_true.dat | 245 +++++++++++++++ .../linear/results_true.dat | 9 + .../linear_xy/inputs_true.dat | 245 +++++++++++++++ .../linear_xy/results_true.dat | 9 + .../random_ray_fixed_source_linear/test.py | 27 ++ .../random_ray_linear/__init__.py | 0 .../random_ray_linear/linear/inputs_true.dat | 110 +++++++ .../random_ray_linear/linear/results_true.dat | 171 ++++++++++ .../linear_xy/inputs_true.dat | 110 +++++++ .../linear_xy/results_true.dat | 171 ++++++++++ .../random_ray_linear/test.py | 26 ++ 28 files changed, 2235 insertions(+), 63 deletions(-) create mode 100644 include/openmc/random_ray/linear_source_domain.h create mode 100644 include/openmc/random_ray/moment_matrix.h create mode 100644 src/random_ray/linear_source_domain.cpp create mode 100644 src/random_ray/moment_matrix.cpp create mode 100644 tests/regression_tests/random_ray_fixed_source_linear/__init__.py create mode 100644 tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_linear/linear/results_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_linear/linear_xy/results_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_linear/test.py create mode 100644 tests/regression_tests/random_ray_linear/__init__.py create mode 100644 tests/regression_tests/random_ray_linear/linear/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_linear/linear/results_true.dat create mode 100644 tests/regression_tests/random_ray_linear/linear_xy/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_linear/linear_xy/results_true.dat create mode 100644 tests/regression_tests/random_ray_linear/test.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 3cbeaa0241b..0f4cc1b527b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -380,6 +380,8 @@ list(APPEND libopenmc_SOURCES src/random_ray/random_ray_simulation.cpp src/random_ray/random_ray.cpp src/random_ray/flat_source_domain.cpp + src/random_ray/linear_source_domain.cpp + src/random_ray/moment_matrix.cpp src/reaction.cpp src/reaction_product.cpp src/scattdata.cpp diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst index 45af2b0c894..9f8eb84d80e 100644 --- a/docs/source/methods/random_ray.rst +++ b/docs/source/methods/random_ray.rst @@ -218,9 +218,9 @@ Following the multigroup discretization, another assumption made is that a large and complex problem can be broken up into small constant cross section regions, and that these regions have group dependent, flat, isotropic sources (fission and scattering), :math:`Q_g`. Anisotropic as well as higher order sources are -also possible with MOC-based methods but are not used yet in OpenMC for -simplicity. With these key assumptions, the multigroup MOC form of the neutron -transport equation can be written as in Equation :eq:`moc_final`. +also possible with MOC-based methods. With these key assumptions, the multigroup +MOC form of the neutron transport equation can be written as in Equation +:eq:`moc_final`. .. math:: :label: moc_final @@ -287,7 +287,7 @@ final expression for the average angular flux for a ray crossing a region as: .. math:: :label: average_psi_final - \overline{\psi}_{r,i,g} = \frac{Q_{i,g}}{\Sigma_{t,i,g}} + \frac{\Delta \psi_{r,g}}{\ell_r \Sigma_{t,i,g}} + \overline{\psi}_{r,i,g} = \frac{Q_{i,g}}{\Sigma_{t,i,g}} + \frac{\Delta \psi_{r,g}}{\ell_r \Sigma_{t,i,g}}. ~~~~~~~~~~~ Random Rays @@ -771,6 +771,170 @@ By default, the unnormalized flux values (units of cm) will be reported. If the user wishes to received volume normalized flux tallies, then an option for this is available, as described in the :ref:`User Guide`. +-------------- +Linear Sources +-------------- + +Instead of making a flat source approximation, as in the previous section, a +Linear Source (LS) approximation can be used. Different LS approximations have +been developed; the OpenMC implementation follows the MOC LS scheme described by +`Ferrer `_. The LS source along a characteristic is given by: + +.. math:: + :label: linear_source + + Q_{i,g}(s) = \bar{Q}_{r,i,g} + \hat{Q}_{r,i,g}(s-\ell_{r}/2), + +where the source, :math:`Q_{i,g}(s)`, varies linearly along the track and +:math:`\bar{Q}_{r,i,g}` and :math:`\hat{Q}_{r,i,g}` are track specific source +terms to define shortly. Integrating the source, as done in Equation +:eq:`moc_final`, leads to + +.. math:: + :label: lsr_attenuation + + \psi^{out}_{r,g}=\psi^{in}_{r,g} + \left(\frac{\bar{Q}_{r, i, g}}{\Sigma_{\mathrm{t}, i, g}}-\psi^{in}_{r,g}\right) + F_{1}\left(\tau_{i,g}\right)+\frac{\hat{Q}_{r, i, g}^{g}}{2\left(\Sigma_{\mathrm{t}, i,g}\right)^{2}} F_{2}\left(\tau_{i,g}\right), + +where for simplicity the term :math:`\tau_{i,g}` and the expoentials :math:`F_1` +and :math:`F_2` are introduced, given by: + +.. math:: + :label: tau + + \tau_{i,g} = \Sigma_{\mathrm{t,i,g}} \ell_{r} + +.. math:: + :label: f1 + + F_1(\tau) = 1 - e^{-\tau}, + +and + +.. math:: + :label: f2 + + F_{2}\left(\tau\right) = 2\left[\tau-F_{1}\left(\tau\right)\right]-\tau F_{1}\left(\tau\right). + + +To solve for the track specific source terms in Equation :eq:`linear_source` we +first define a local reference frame. If we now refer to :math:`\mathbf{r}` as +the global coordinate and introduce the source region specific coordinate +:math:`\mathbf{u}` such that, + +.. math:: + :label: local_coord + + \mathbf{u}_{r} = \mathbf{r}-\mathbf{r}_{\mathrm{c}}, + +where :math:`\mathbf{r}_{\mathrm{c}}` is the centroid of the source region of +interest. In turn :math:`\mathbf{u}_{r,\mathrm{c}}` and :math:`\mathbf{u}_{r,0}` +are the local centroid and entry positions of a ray. The computation of the +local and global centroids are described further by `Gunow `_. + +Using the local position, the source in a source region is given by: + +.. math:: + :label: region_source + + \tilde{Q}(\boldsymbol{x}) ={Q}_{i,g}+ \boldsymbol{\vec{Q}}_{i,g} \cdot \mathbf{u}_{r}\;\mathrm{,} + +This definition allows us to solve for our characteric source terms resulting in: + +.. math:: + :label: source_term_1 + + \bar{Q}_{r, i, g} = Q_{i,g} + \left[\mathbf{u}_{r,\mathrm{c}} \cdot \boldsymbol{\vec{Q}}_{i,g}\right], + +.. math:: + :label: source_term_2 + + \hat{Q}_{r, i, g} = \left[\boldsymbol{\Omega} \cdot \boldsymbol{\vec{Q}}_{i,g}\right]\;\mathrm{,} + +:math:`\boldsymbol{\Omega}` being the direction vector of the ray. The next step +is to solve for the LS source vector :math:`\boldsymbol{\vec{Q}}_{i,g}`. A +relationship between the LS source vector and the source moments, +:math:`\boldsymbol{\vec{q}}_{i,g}` can be derived, as in `Ferrer +`_ and `Gunow `_: + +.. math:: + :label: m_equation + + \mathbf{M}_{i} \boldsymbol{\vec{Q}}_{i,g} = \boldsymbol{\vec{q}}_{i,g} \;\mathrm{.} + +The spatial moments matrix :math:`M_i` in region :math:`i` represents the +spatial distribution of the 3D object composing the `source region +`_. This matrix is independent of the material of the source +region, fluxes, and any transport effects -- it is a purely geometric quantity. +It is a symmetric :math:`3\times3` matrix. While :math:`M_i` is not known +apriori to the simulation, similar to the source region volume, it can be +computed "on-the-fly" as a byproduct of the random ray integration process. Each +time a ray randomly crosses the region within its active length, an estimate of +the spatial moments matrix can be computed by using the midpoint of the ray as +an estimate of the centroid, and the distance and direction of the ray can be +used to inform the other spatial moments within the matrix. As this information +is purely geometric, the stochastic estimate of the centroid and spatial moments +matrix can be accumulated and improved over the entire duration of the +simulation, converging towards their true quantities. + +With an estimate of the spatial moments matrix :math:`M_i` resulting from the +ray tracing process naturally, the LS source vector +:math:`\boldsymbol{\vec{Q}}_{i,g}` can be obtained via a linear solve of +:eq:`m_equation`, or by the direct inversion of :math:`M_i`. However, to +accomplish this, we must first know the source moments +:math:`\boldsymbol{\vec{q}}_{i,g}`. Fortunately, the source moments are also +defined by the definition of the source: + +.. math:: + :label: source_moments + + q_{v, i, g}= \frac{\chi_{i,g}}{k_{eff}} \sum_{g^{\prime}=1}^{G} \nu + \Sigma_{\mathrm{f},i, g^{\prime}} \hat{\phi}_{v, i, g^{\prime}} + \sum_{g^{\prime}=1}^{G} + \Sigma_{\mathrm{s}, i, g^{\prime}\rightarrow g} \hat{\phi}_{v, i, g^{\prime}}\quad \forall v \in(x, y, z)\;\mathrm{,} + +where :math:`v` indicates the direction vector component, and we have introduced +the scalar flux moments :math:`\hat{\phi}`. The scalar flux moments can be +solved for by taking the `integral definition `_ of a spatial +moment, allowing us to derive a "simulation averaged" estimator for the scalar +moment, as in Equation :eq:`phi_sim`, + +.. math:: + :label: scalar_moments_sim + + \hat{\phi}_{v,i,g}^{simulation} = \frac{\sum\limits_{r=1}^{N_i} + \ell_{r} \left[\Omega_{v} \hat{\psi}_{r,i,g} + u_{r,v,0} \bar{\psi}_{r,i,g}\right]} + {\Sigma_{t,i,g} \frac{\sum\limits^{B}_{b}\sum\limits^{N_i}_{r} \ell_{b,r} }{B}} + \quad \forall v \in(x, y, z)\;\mathrm{,} + + +where the average angular flux is given by Equation :eq:`average_psi_final`, and +the angular flux spatial moments :math:`\hat{\psi}_{r,i,g}` by: + +.. math:: + :label: angular_moments + + \hat{\psi}_{r, i, g} = \frac{\ell_{r}\psi^{in}_{r,g}}{2} + + \left(\frac{\bar{Q}_{r,i, g}}{\Sigma_{\mathrm{t}, i, g}}-\psi^{in}_{r,g}\right) + \frac{G_{1}\left(\tau_{i,g}\right)}{\Sigma_{\mathrm{t}, i, g}} + \frac{\ell_{r}\hat{Q}_{r,i,g}} + {2\left(\Sigma_{\mathrm{t}, i, g}\right)^{2}}G_{2}\left(\tau_{i,g}\right)\;\mathrm{.} + + +The new exponentials introduced, again for simplicity, are simply: + +.. math:: + :label: G1 + + G_{1}(\tau) = 1+\frac{\tau}{2}-\left(1+\frac{1}{\tau}\right) F_{1}(\tau), + +.. math:: + :label: G2 + + G_{2}(\tau) = \frac{2}{3} \tau-\left(1+\frac{2}{\tau}\right) G_{1}(\tau) + +The contents of this section, alongside the equations for the flat source and +scalar flux, Equations :eq:`source_update` and :eq:`phi_sim` respectively, +completes the set of equations for LS. + .. _methods-shannon-entropy-random-ray: ----------------------------- @@ -789,7 +953,7 @@ sources is adjusted such that: :label: fraction-source-random-ray S_i = \frac{\text{Fission source in FSR $i \times$ Volume of FSR - $i$}}{\text{Total fission source}} = \frac{Q_{i} V_{i}}{\sum_{i=1}^{i=N} + $i$}}{\text{Total fission source}} = \frac{Q_{i} V_{i}}{\sum_{i=1}^{i=N} Q_{i} V_{i}} The Shannon entropy is then computed normally as @@ -852,13 +1016,13 @@ in random ray particle transport are: areas typically have solutions that are highly effective at mitigating bias, error stemming from multigroup energy discretization is much harder to remedy. - - **Flat Source Approximation:**. In OpenMC, a "flat" (0th order) source - approximation is made, wherein the scattering and fission sources within a + - **Source Approximation:**. In OpenMC, a "flat" (0th order) source + approximation is often made, wherein the scattering and fission sources within a cell are assumed to be spatially uniform. As the source in reality is a continuous function, this leads to bias, although the bias can be reduced to acceptable levels if the flat source regions are sufficiently small. - The bias can also be mitigated by assuming a higher-order source (e.g., - linear or quadratic), although OpenMC does not yet have this capability. + The bias can also be mitigated by assuming a higher-order source such as the + linear source approximation currently implemented into OpenMC. In practical terms, this source of bias can become very large if cells are large (with dimensions beyond that of a typical particle mean free path), but the subdivision of cells can often reduce this bias to trivial levels. @@ -882,6 +1046,8 @@ in random ray particle transport are: .. _Tramm-2018: https://dspace.mit.edu/handle/1721.1/119038 .. _Tramm-2020: https://doi.org/10.1051/EPJCONF/202124703021 .. _Cosgrove-2023: https://doi.org/10.1080/00295639.2023.2270618 +.. _Ferrer-2016: https://doi.org/10.13182/NSE15-6 +.. _Gunow-2018: https://dspace.mit.edu/handle/1721.1/119030 .. only:: html diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index 6df132859c7..117d5e23fb5 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -447,6 +447,34 @@ in the `OpenMC Jupyter notebook collection separate materials can be defined each with a separate multigroup dataset corresponding to a given temperature. +-------------- +Linear Sources +-------------- + +Linear Sources (LS), are supported with the eigenvalue and fixed source random +ray solvers. General 3D LS can be toggled by setting the ``source_shape`` field +in the :attr:`openmc.Settings.random_ray` dictionary to ``'linear'`` as:: + + settings.random_ray['source_shape'] = 'linear' + +LS enables the use of coarser mesh discretizations and lower ray populations, +offsetting the increased computation per ray. + +While OpenMC has no specific mode for 2D simulations, such simulations can be +performed implicitly by leaving one of the dimensions of the geometry unbounded +or by imposing reflective boundary conditions with no variation in between them +in that dimension. When 3D linear sources are used in a 2D random ray +simulation, the extremely long (or potentially infinite) spatial dimension along +one of the axes can cause the linear source to become noisy, leading to +potentially large increases in variance. To mitigate this, the user can force +the z-terms of the linear source to zero by setting the ``source_shape`` field +as:: + + settings.random_ray['source_shape'] = 'linear_xy' + +which will greatly improve the quality of the linear source term in 2D +simulations. + --------------------------------- Fixed Source and Eigenvalue Modes --------------------------------- diff --git a/include/openmc/constants.h b/include/openmc/constants.h index 1c683e5f50c..e502506c91e 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -342,6 +342,8 @@ enum class RunMode { enum class SolverType { MONTE_CARLO, RANDOM_RAY }; +enum class RandomRaySourceShape { FLAT, LINEAR, LINEAR_XY }; + //============================================================================== // Geometry Constants diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index 51938404037..33c5661dcd4 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -89,25 +89,28 @@ struct TallyTask { class FlatSourceDomain { public: //---------------------------------------------------------------------------- - // Constructors + // Constructors and Destructors FlatSourceDomain(); + virtual ~FlatSourceDomain() = default; //---------------------------------------------------------------------------- // Methods - void update_neutron_source(double k_eff); + virtual void update_neutron_source(double k_eff); double compute_k_eff(double k_eff_old) const; - void normalize_scalar_flux_and_volumes( + virtual void normalize_scalar_flux_and_volumes( double total_active_distance_per_iteration); - int64_t add_source_to_scalar_flux(); - void batch_reset(); + virtual int64_t add_source_to_scalar_flux(); + virtual void batch_reset(); void convert_source_regions_to_tallies(); void reset_tally_volumes(); void random_ray_tally(); - void accumulate_iteration_flux(); + virtual void accumulate_iteration_flux(); void output_to_vtk() const; - void all_reduce_replicated_source_regions(); + virtual void all_reduce_replicated_source_regions(); void convert_external_sources(); void count_external_source_regions(); + virtual void flux_swap(); + virtual double evaluate_flux_at_point(Position r, int64_t sr, int g) const; double compute_fixed_source_normalization_factor() const; //---------------------------------------------------------------------------- @@ -131,6 +134,7 @@ class FlatSourceDomain { vector lock_; vector was_hit_; vector volume_; + vector volume_t_; vector position_recorded_; vector position_; @@ -141,7 +145,7 @@ class FlatSourceDomain { vector source_; vector external_source_; -private: +protected: //---------------------------------------------------------------------------- // Methods void apply_external_source_to_source_region( @@ -174,7 +178,6 @@ class FlatSourceDomain { // 1D arrays representing values for all source regions vector material_; - vector volume_t_; // 2D arrays stored in 1D representing values for all source regions x energy // groups diff --git a/include/openmc/random_ray/linear_source_domain.h b/include/openmc/random_ray/linear_source_domain.h new file mode 100644 index 00000000000..5010ffddd6f --- /dev/null +++ b/include/openmc/random_ray/linear_source_domain.h @@ -0,0 +1,61 @@ +#ifndef OPENMC_RANDOM_RAY_LINEAR_SOURCE_DOMAIN_H +#define OPENMC_RANDOM_RAY_LINEAR_SOURCE_DOMAIN_H + +#include "openmc/random_ray/flat_source_domain.h" +#include "openmc/random_ray/moment_matrix.h" + +#include "openmc/openmp_interface.h" +#include "openmc/position.h" +#include "openmc/source.h" + +namespace openmc { + +/* + * The LinearSourceDomain class encompasses data and methods for storing + * scalar flux and source region for all linear source regions in a + * random ray simulation domain. + */ + +class LinearSourceDomain : public FlatSourceDomain { +public: + //---------------------------------------------------------------------------- + // Constructors + LinearSourceDomain(); + + //---------------------------------------------------------------------------- + // Methods + void update_neutron_source(double k_eff) override; + double compute_k_eff(double k_eff_old) const; + void normalize_scalar_flux_and_volumes( + double total_active_distance_per_iteration) override; + int64_t add_source_to_scalar_flux() override; + void batch_reset() override; + void convert_source_regions_to_tallies(); + void reset_tally_volumes(); + void random_ray_tally(); + void accumulate_iteration_flux() override; + void output_to_vtk() const; + void all_reduce_replicated_source_regions() override; + void convert_external_sources(); + void count_external_source_regions(); + void flux_swap() override; + double evaluate_flux_at_point(Position r, int64_t sr, int g) const override; + + //---------------------------------------------------------------------------- + // Public Data members + + vector source_gradients_; + vector flux_moments_old_; + vector flux_moments_new_; + vector flux_moments_t_; + vector centroid_; + vector centroid_iteration_; + vector centroid_t_; + vector mom_matrix_; + vector mom_matrix_t_; + +}; // class LinearSourceDomain + +} // namespace openmc + +#endif // OPENMC_RANDOM_RAY_LINEAR_SOURCE_DOMAIN_H diff --git a/include/openmc/random_ray/moment_matrix.h b/include/openmc/random_ray/moment_matrix.h new file mode 100644 index 00000000000..c95bb2c1286 --- /dev/null +++ b/include/openmc/random_ray/moment_matrix.h @@ -0,0 +1,90 @@ +#ifndef OPENMC_MOMENT_MATRIX_H +#define OPENMC_MOMENT_MATRIX_H + +#include + +#include "openmc/position.h" + +namespace openmc { + +// The MomentArray class is a 3-element array representing the x, y, and z +// moments. It is defined as an alias for the Position class to allow for +// dot products and other operations with Position objects. +// TODO: This class could in theory have 32-bit instead of 64-bit FP values. +using MomentArray = Position; + +// The MomentMatrix class is a sparse representation a 3x3 symmetric +// matrix, with elements labeled as follows: +// +// | a b c | +// | b d e | +// | c e f | +// +// This class uses FP64 values as objects that are accumulated to over many +// iterations. +class MomentMatrix { +public: + //---------------------------------------------------------------------------- + // Public data members + double a; + double b; + double c; + double d; + double e; + double f; + + //---------------------------------------------------------------------------- + // Constructors + MomentMatrix() = default; + MomentMatrix(double a, double b, double c, double d, double e, double f) + : a {a}, b {b}, c {c}, d {d}, e {e}, f {f} + {} + + //---------------------------------------------------------------------------- + // Methods + MomentMatrix inverse() const; + double determinant() const; + void compute_spatial_moments_matrix( + const Position& r, const Direction& u, const double& distance); + + inline void set_to_zero() { a = b = c = d = e = f = 0; } + + inline MomentMatrix& operator*=(double x) + { + a *= x; + b *= x; + c *= x; + d *= x; + e *= x; + f *= x; + return *this; + } + + inline MomentMatrix operator*(double x) const + { + MomentMatrix m_copy = *this; + m_copy *= x; + return m_copy; + } + + inline MomentMatrix& operator+=(const MomentMatrix& rhs) + { + a += rhs.a; + b += rhs.b; + c += rhs.c; + d += rhs.d; + e += rhs.e; + f += rhs.f; + return *this; + } + + MomentArray operator*(const MomentArray& rhs) const + { + return {a * rhs.x + b * rhs.y + c * rhs.z, + b * rhs.x + d * rhs.y + e * rhs.z, c * rhs.x + e * rhs.y + f * rhs.z}; + } +}; + +} // namespace openmc + +#endif // OPENMC_MOMENT_MATRIX_H diff --git a/include/openmc/random_ray/random_ray.h b/include/openmc/random_ray/random_ray.h index 5ee64574ec1..913a9af4a75 100644 --- a/include/openmc/random_ray/random_ray.h +++ b/include/openmc/random_ray/random_ray.h @@ -4,6 +4,7 @@ #include "openmc/memory.h" #include "openmc/particle.h" #include "openmc/random_ray/flat_source_domain.h" +#include "openmc/random_ray/moment_matrix.h" #include "openmc/source.h" namespace openmc { @@ -25,14 +26,18 @@ class RandomRay : public Particle { // Methods void event_advance_ray(); void attenuate_flux(double distance, bool is_active); + void attenuate_flux_flat_source(double distance, bool is_active); + void attenuate_flux_linear_source(double distance, bool is_active); + void initialize_ray(uint64_t ray_id, FlatSourceDomain* domain); uint64_t transport_history_based_single_ray(); //---------------------------------------------------------------------------- // Static data members - static double distance_inactive_; // Inactive (dead zone) ray length - static double distance_active_; // Active ray length - static unique_ptr ray_source_; // Starting source for ray sampling + static double distance_inactive_; // Inactive (dead zone) ray length + static double distance_active_; // Active ray length + static unique_ptr ray_source_; // Starting source for ray sampling + static RandomRaySourceShape source_shape_; // Flag for linear source //---------------------------------------------------------------------------- // Public data members @@ -42,6 +47,8 @@ class RandomRay : public Particle { //---------------------------------------------------------------------------- // Private data members vector delta_psi_; + vector delta_moments_; + int negroups_; FlatSourceDomain* domain_ {nullptr}; // pointer to domain that has flat source // data needed for ray transport diff --git a/include/openmc/random_ray/random_ray_simulation.h b/include/openmc/random_ray/random_ray_simulation.h index b439574bfca..c1d47821d7a 100644 --- a/include/openmc/random_ray/random_ray_simulation.h +++ b/include/openmc/random_ray/random_ray_simulation.h @@ -2,6 +2,7 @@ #define OPENMC_RANDOM_RAY_SIMULATION_H #include "openmc/random_ray/flat_source_domain.h" +#include "openmc/random_ray/linear_source_domain.h" namespace openmc { @@ -31,7 +32,7 @@ class RandomRaySimulation { // Data members private: // Contains all flat source region data - FlatSourceDomain domain_; + unique_ptr domain_; // Random ray eigenvalue double k_eff_ {1.0}; diff --git a/openmc/settings.py b/openmc/settings.py index 9731f54ecb1..8076625818a 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -157,6 +157,9 @@ class Settings: :ray_source: Starting ray distribution (must be uniform in space and angle) as specified by a :class:`openmc.SourceBase` object. + :source_shape: + Assumed shape of the source distribution within each source + region. Options are 'flat' (default), 'linear', or 'linear_xy'. :volume_normalized_flux_tallies: Whether to normalize flux tallies by volume (bool). The default is 'False'. When enabled, flux tallies will be reported in units of @@ -1090,6 +1093,9 @@ def random_ray(self, random_ray: dict): random_ray[key], 0.0, True) elif key == 'ray_source': cv.check_type('random ray source', random_ray[key], SourceBase) + elif key == 'source_shape': + cv.check_value('source shape', random_ray[key], + ('flat', 'linear', 'linear_xy')) elif key == 'volume_normalized_flux_tallies': cv.check_type('volume normalized flux tallies', random_ray[key], bool) else: @@ -1885,6 +1891,8 @@ def _random_ray_from_xml_element(self, root): elif child.tag == 'source': source = SourceBase.from_xml_element(child) self.random_ray['ray_source'] = source + elif child.tag == 'source_shape': + self.random_ray['source_shape'] = child.text elif child.tag == 'volume_normalized_flux_tallies': self.random_ray['volume_normalized_flux_tallies'] = ( child.text in ('true', '1') diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 29b72c77bed..8b6cda93f2a 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -751,6 +751,13 @@ void FlatSourceDomain::all_reduce_replicated_source_regions() #endif } +double FlatSourceDomain::evaluate_flux_at_point( + Position r, int64_t sr, int g) const +{ + return scalar_flux_final_[sr * negroups_ + g] / + (settings::n_batches - settings::n_inactive); +} + // Outputs all basic material, FSR ID, multigroup flux, and // fission source data to .vtk file that can be directly // loaded and displayed by Paraview. Note that .vtk binary @@ -811,6 +818,7 @@ void FlatSourceDomain::output_to_vtk() const // Relate voxel spatial locations to random ray source regions vector voxel_indices(Nx * Ny * Nz); + vector voxel_positions(Nx * Ny * Nz); #pragma omp parallel for collapse(3) for (int z = 0; z < Nz; z++) { @@ -827,6 +835,7 @@ void FlatSourceDomain::output_to_vtk() const int64_t source_region_idx = source_region_offsets_[i_cell] + p.cell_instance(); voxel_indices[z * Ny * Nx + y * Nx + x] = source_region_idx; + voxel_positions[z * Ny * Nx + y * Nx + x] = sample; } } } @@ -851,11 +860,10 @@ void FlatSourceDomain::output_to_vtk() const for (int g = 0; g < negroups_; g++) { std::fprintf(plot, "SCALARS flux_group_%d float\n", g); std::fprintf(plot, "LOOKUP_TABLE default\n"); - for (int fsr : voxel_indices) { + for (int i = 0; i < Nx * Ny * Nz; i++) { + int64_t fsr = voxel_indices[i]; int64_t source_element = fsr * negroups_ + g; - float flux = - scalar_flux_final_[source_element] * source_normalization_factor; - flux /= (settings::n_batches - settings::n_inactive); + float flux = evaluate_flux_at_point(voxel_positions[i], fsr, g); flux = convert_to_big_endian(flux); std::fwrite(&flux, sizeof(float), 1, plot); } @@ -882,14 +890,14 @@ void FlatSourceDomain::output_to_vtk() const // Plot fission source std::fprintf(plot, "SCALARS total_fission_source float\n"); std::fprintf(plot, "LOOKUP_TABLE default\n"); - for (int fsr : voxel_indices) { + for (int i = 0; i < Nx * Ny * Nz; i++) { + int64_t fsr = voxel_indices[i]; + float total_fission = 0.0; int mat = material_[fsr]; for (int g = 0; g < negroups_; g++) { int64_t source_element = fsr * negroups_ + g; - float flux = - scalar_flux_final_[source_element] * source_normalization_factor; - flux /= (settings::n_batches - settings::n_inactive); + float flux = evaluate_flux_at_point(voxel_positions[i], fsr, g); float Sigma_f = data::mg.macro_xs_[mat].get_xs( MgxsType::FISSION, g, nullptr, nullptr, nullptr, 0, 0); total_fission += Sigma_f * flux; @@ -1027,5 +1035,9 @@ void FlatSourceDomain::convert_external_sources() } } } +void FlatSourceDomain::flux_swap() +{ + scalar_flux_old_.swap(scalar_flux_new_); +} } // namespace openmc diff --git a/src/random_ray/linear_source_domain.cpp b/src/random_ray/linear_source_domain.cpp new file mode 100644 index 00000000000..1603ec24a4e --- /dev/null +++ b/src/random_ray/linear_source_domain.cpp @@ -0,0 +1,269 @@ +#include "openmc/random_ray/linear_source_domain.h" + +#include "openmc/cell.h" +#include "openmc/geometry.h" +#include "openmc/material.h" +#include "openmc/message_passing.h" +#include "openmc/mgxs_interface.h" +#include "openmc/output.h" +#include "openmc/plot.h" +#include "openmc/random_ray/random_ray.h" +#include "openmc/simulation.h" +#include "openmc/tallies/filter.h" +#include "openmc/tallies/tally.h" +#include "openmc/tallies/tally_scoring.h" +#include "openmc/timer.h" + +namespace openmc { + +//============================================================================== +// LinearSourceDomain implementation +//============================================================================== + +LinearSourceDomain::LinearSourceDomain() : FlatSourceDomain() +{ + // First order spatial moment of the scalar flux + flux_moments_old_.assign(n_source_elements_, {0.0, 0.0, 0.0}); + flux_moments_new_.assign(n_source_elements_, {0.0, 0.0, 0.0}); + flux_moments_t_.assign(n_source_elements_, {0.0, 0.0, 0.0}); + // Source gradients given by M inverse multiplied by source moments + source_gradients_.assign(n_source_elements_, {0.0, 0.0, 0.0}); + + centroid_.assign(n_source_regions_, {0.0, 0.0, 0.0}); + centroid_iteration_.assign(n_source_regions_, {0.0, 0.0, 0.0}); + centroid_t_.assign(n_source_regions_, {0.0, 0.0, 0.0}); + mom_matrix_.assign(n_source_regions_, {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}); + mom_matrix_t_.assign(n_source_regions_, {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}); +} + +void LinearSourceDomain::batch_reset() +{ + FlatSourceDomain::batch_reset(); +#pragma omp parallel for + for (int64_t se = 0; se < n_source_elements_; se++) { + flux_moments_new_[se] = {0.0, 0.0, 0.0}; + } +#pragma omp parallel for + for (int64_t sr = 0; sr < n_source_regions_; sr++) { + centroid_iteration_[sr] = {0.0, 0.0, 0.0}; + mom_matrix_[sr] = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; + } +} + +void LinearSourceDomain::update_neutron_source(double k_eff) +{ + simulation::time_update_src.start(); + + double inverse_k_eff = 1.0 / k_eff; + + // Temperature and angle indices, if using multiple temperature + // data sets and/or anisotropic data sets. + // TODO: Currently assumes we are only using single temp/single + // angle data. + const int t = 0; + const int a = 0; + +#pragma omp parallel for + for (int sr = 0; sr < n_source_regions_; sr++) { + + int material = material_[sr]; + MomentMatrix invM = mom_matrix_[sr].inverse(); + + for (int e_out = 0; e_out < negroups_; e_out++) { + float sigma_t = data::mg.macro_xs_[material].get_xs( + MgxsType::TOTAL, e_out, nullptr, nullptr, nullptr, t, a); + + float scatter_flat = 0.0f; + float fission_flat = 0.0f; + MomentArray scatter_linear = {0.0, 0.0, 0.0}; + MomentArray fission_linear = {0.0, 0.0, 0.0}; + + for (int e_in = 0; e_in < negroups_; e_in++) { + // Handles for the flat and linear components of the flux + float flux_flat = scalar_flux_old_[sr * negroups_ + e_in]; + MomentArray flux_linear = flux_moments_old_[sr * negroups_ + e_in]; + + // Handles for cross sections + float sigma_s = data::mg.macro_xs_[material].get_xs( + MgxsType::NU_SCATTER, e_in, &e_out, nullptr, nullptr, t, a); + float nu_sigma_f = data::mg.macro_xs_[material].get_xs( + MgxsType::NU_FISSION, e_in, nullptr, nullptr, nullptr, t, a); + float chi = data::mg.macro_xs_[material].get_xs( + MgxsType::CHI_PROMPT, e_in, &e_out, nullptr, nullptr, t, a); + + // Compute source terms for flat and linear components of the flux + scatter_flat += sigma_s * flux_flat; + fission_flat += nu_sigma_f * flux_flat * chi; + scatter_linear += sigma_s * flux_linear; + fission_linear += nu_sigma_f * flux_linear * chi; + } + + // Compute the flat source term + source_[sr * negroups_ + e_out] = + (scatter_flat + fission_flat * inverse_k_eff) / sigma_t; + + // Compute the linear source terms + if (simulation::current_batch > 2) { + source_gradients_[sr * negroups_ + e_out] = + invM * ((scatter_linear + fission_linear * inverse_k_eff) / sigma_t); + } + } + } + + if (settings::run_mode == RunMode::FIXED_SOURCE) { +// Add external source to flat source term if in fixed source mode +#pragma omp parallel for + for (int se = 0; se < n_source_elements_; se++) { + source_[se] += external_source_[se]; + } + } + + simulation::time_update_src.stop(); +} + +void LinearSourceDomain::normalize_scalar_flux_and_volumes( + double total_active_distance_per_iteration) +{ + float normalization_factor = 1.0 / total_active_distance_per_iteration; + double volume_normalization_factor = + 1.0 / (total_active_distance_per_iteration * simulation::current_batch); + +// Normalize flux to total distance travelled by all rays this iteration +#pragma omp parallel for + for (int64_t e = 0; e < scalar_flux_new_.size(); e++) { + scalar_flux_new_[e] *= normalization_factor; + flux_moments_new_[e] *= normalization_factor; + } + +// Accumulate cell-wise ray length tallies collected this iteration, then +// update the simulation-averaged cell-wise volume estimates +#pragma omp parallel for + for (int64_t sr = 0; sr < n_source_regions_; sr++) { + centroid_t_[sr] += centroid_iteration_[sr]; + mom_matrix_t_[sr] += mom_matrix_[sr]; + volume_t_[sr] += volume_[sr]; + volume_[sr] = volume_t_[sr] * volume_normalization_factor; + if (volume_t_[sr] > 0.0) { + double inv_volume = 1.0 / volume_t_[sr]; + centroid_[sr] = centroid_t_[sr]; + centroid_[sr] *= inv_volume; + mom_matrix_[sr] = mom_matrix_t_[sr]; + mom_matrix_[sr] *= inv_volume; + } + } +} + +int64_t LinearSourceDomain::add_source_to_scalar_flux() +{ + int64_t n_hits = 0; + + // Temperature and angle indices, if using multiple temperature + // data sets and/or anisotropic data sets. + // TODO: Currently assumes we are only using single temp/single + // angle data. + const int t = 0; + const int a = 0; + +#pragma omp parallel for reduction(+ : n_hits) + for (int sr = 0; sr < n_source_regions_; sr++) { + + double volume = volume_[sr]; + int material = material_[sr]; + + // Check if this cell was hit this iteration + int was_cell_hit = was_hit_[sr]; + if (was_cell_hit) { + n_hits++; + } + + for (int g = 0; g < negroups_; g++) { + int64_t idx = (sr * negroups_) + g; + // There are three scenarios we need to consider: + if (was_cell_hit) { + // 1. If the FSR was hit this iteration, then the new flux is equal to + // the flat source from the previous iteration plus the contributions + // from rays passing through the source region (computed during the + // transport sweep) + scalar_flux_new_[idx] /= volume; + scalar_flux_new_[idx] += source_[idx]; + flux_moments_new_[idx] *= (1.0 / volume); + } else if (volume > 0.0) { + // 2. If the FSR was not hit this iteration, but has been hit some + // previous iteration, then we simply set the new scalar flux to be + // equal to the contribution from the flat source alone. + scalar_flux_new_[idx] = source_[idx]; + } else { + // If the FSR was not hit this iteration, and it has never been hit in + // any iteration (i.e., volume is zero), then we want to set this to 0 + // to avoid dividing anything by a zero volume. + scalar_flux_new_[idx] = 0.0f; + flux_moments_new_[idx] *= 0.0; + } + } + } + + return n_hits; +} + +void LinearSourceDomain::flux_swap() +{ + FlatSourceDomain::flux_swap(); + flux_moments_old_.swap(flux_moments_new_); +} + +void LinearSourceDomain::accumulate_iteration_flux() +{ + // Accumulate scalar flux + FlatSourceDomain::accumulate_iteration_flux(); + + // Accumulate scalar flux moments +#pragma omp parallel for + for (int64_t se = 0; se < n_source_elements_; se++) { + flux_moments_t_[se] += flux_moments_new_[se]; + } +} + +void LinearSourceDomain::all_reduce_replicated_source_regions() +{ +#ifdef OPENMC_MPI + FlatSourceDomain::all_reduce_replicated_source_regions(); + simulation::time_bank_sendrecv.start(); + + // We are going to assume we can safely cast Position, MomentArray, + // and MomentMatrix to contiguous arrays of doubles for the MPI + // allreduce operation. This is a safe assumption as typically + // compilers will at most pad to 8 byte boundaries. If a new FP32 MomentArray + // type is introduced, then there will likely be padding, in which case this + // function will need to become more complex. + if (sizeof(MomentArray) != 3 * sizeof(double) || + sizeof(MomentMatrix) != 6 * sizeof(double)) { + fatal_error("Unexpected buffer padding in linear source domain reduction."); + } + + MPI_Allreduce(MPI_IN_PLACE, static_cast(flux_moments_new_.data()), + n_source_elements_ * 3, MPI_DOUBLE, MPI_SUM, mpi::intracomm); + MPI_Allreduce(MPI_IN_PLACE, static_cast(mom_matrix_.data()), + n_source_regions_ * 6, MPI_DOUBLE, MPI_SUM, mpi::intracomm); + MPI_Allreduce(MPI_IN_PLACE, static_cast(centroid_iteration_.data()), + n_source_regions_ * 3, MPI_DOUBLE, MPI_SUM, mpi::intracomm); + + simulation::time_bank_sendrecv.stop(); +#endif +} + +double LinearSourceDomain::evaluate_flux_at_point( + Position r, int64_t sr, int g) const +{ + float phi_flat = FlatSourceDomain::evaluate_flux_at_point(r, sr, g); + + Position local_r = r - centroid_[sr]; + MomentArray phi_linear = flux_moments_t_[sr * negroups_ + g]; + phi_linear *= 1.0 / (settings::n_batches - settings::n_inactive); + + MomentMatrix invM = mom_matrix_[sr].inverse(); + MomentArray phi_solved = invM * phi_linear; + + return phi_flat + phi_solved.dot(local_r); +} + +} // namespace openmc diff --git a/src/random_ray/moment_matrix.cpp b/src/random_ray/moment_matrix.cpp new file mode 100644 index 00000000000..0324a14943b --- /dev/null +++ b/src/random_ray/moment_matrix.cpp @@ -0,0 +1,84 @@ +#include "openmc/random_ray/moment_matrix.h" +#include "openmc/error.h" + +#include + +namespace openmc { + +//============================================================================== +// UpperTriangular implementation +//============================================================================== + +// Inverts a 3x3 smmetric matrix labeled as: +// +// | a b c | +// | b d e | +// | c e f | +// +// We first check the determinant to ensure it is non-zero before proceeding +// with the inversion. If the determinant is zero, we return a matrix of zeros. +// Inversion is calculated by computing the adjoint matrix first, and then the +// inverse can be computed as: A^-1 = 1/det(A) * adj(A) +MomentMatrix MomentMatrix::inverse() const +{ + MomentMatrix inv; + + // Check if the determinant is zero + double det = determinant(); + if (det < std::abs(1.0e-10)) { + // Set the inverse to zero. In effect, this will + // result in all the linear terms of the source becoming + // zero, leaving just the flat source. + inv.set_to_zero(); + return inv; + } + + // Compute the adjoint matrix + inv.a = d * f - e * e; + inv.b = c * e - b * f; + inv.c = b * e - c * d; + inv.d = a * f - c * c; + inv.e = b * c - a * e; + inv.f = a * d - b * b; + + // A^-1 = 1/det(A) * adj(A) + inv *= 1.0 / det; + + return inv; +} + +// Computes the determinant of a 3x3 symmetric +// matrix, with elements labeled as follows: +// +// | a b c | +// | b d e | +// | c e f | +double MomentMatrix::determinant() const +{ + return a * (d * f - e * e) - b * (b * f - c * e) + c * (b * e - c * d); +} + +// Compute a 3x3 spatial moment matrix based on a single ray crossing. +// The matrix is symmetric, and is defined as: +// +// | a b c | +// | b d e | +// | c e f | +// +// The estimate of the obect's spatial moments matrix is computed based on the +// midpoint of the ray's crossing, the direction of the ray, and the distance +// the ray traveled through the 3D object. +void MomentMatrix::compute_spatial_moments_matrix( + const Position& r, const Direction& u, const double& distance) +{ + constexpr double one_over_twelve = 1.0 / 12.0; + const double distance2_12 = distance * distance * one_over_twelve; + a = r[0] * r[0] + u[0] * u[0] * distance2_12; + b = r[0] * r[1] + u[0] * u[1] * distance2_12; + c = r[0] * r[2] + u[0] * u[2] * distance2_12; + d = r[1] * r[1] + u[1] * u[1] * distance2_12; + e = r[1] * r[2] + u[1] * u[2] * distance2_12; + f = r[2] * r[2] + u[2] * u[2] * distance2_12; +} + +} // namespace openmc \ No newline at end of file diff --git a/src/random_ray/random_ray.cpp b/src/random_ray/random_ray.cpp index 63d728cce8b..a5bf6ec1060 100644 --- a/src/random_ray/random_ray.cpp +++ b/src/random_ray/random_ray.cpp @@ -1,9 +1,11 @@ #include "openmc/random_ray/random_ray.h" +#include "openmc/constants.h" #include "openmc/geometry.h" #include "openmc/message_passing.h" #include "openmc/mgxs_interface.h" #include "openmc/random_ray/flat_source_domain.h" +#include "openmc/random_ray/linear_source_domain.h" #include "openmc/search.h" #include "openmc/settings.h" #include "openmc/simulation.h" @@ -60,6 +62,118 @@ float cjosey_exponential(float tau) return num / den; } +// The below two functions (exponentialG and exponentialG2) were developed +// by Colin Josey. The implementation of these functions is closely based +// on the OpenMOC versions of these functions. The OpenMOC license is given +// below: + +// Copyright (C) 2012-2023 Massachusetts Institute of Technology and OpenMOC +// contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// Computes y = 1/x-(1-exp(-x))/x**2 using a 5/6th order rational +// approximation. It is accurate to 2e-7 over [0, 1e5]. Developed by Colin +// Josey using Remez's algorithm, with original implementation in OpenMOC at: +// https://github.com/mit-crpg/OpenMOC/blob/develop/src/exponentials.h +float exponentialG(float tau) +{ + // Numerator coefficients in rational approximation for 1/x - (1 - exp(-x)) / + // x^2 + constexpr float d0n = 0.5f; + constexpr float d1n = 0.176558112351595f; + constexpr float d2n = 0.04041584305811143f; + constexpr float d3n = 0.006178333902037397f; + constexpr float d4n = 0.0006429894635552992f; + constexpr float d5n = 0.00006064409107557148f; + + // Denominator coefficients in rational approximation for 1/x - (1 - exp(-x)) + // / x^2 + constexpr float d0d = 1.0f; + constexpr float d1d = 0.6864462055546078f; + constexpr float d2d = 0.2263358514260129f; + constexpr float d3d = 0.04721469893686252f; + constexpr float d4d = 0.006883236664917246f; + constexpr float d5d = 0.0007036272419147752f; + constexpr float d6d = 0.00006064409107557148f; + + float x = tau; + + float num = d5n; + num = num * x + d4n; + num = num * x + d3n; + num = num * x + d2n; + num = num * x + d1n; + num = num * x + d0n; + + float den = d6d; + den = den * x + d5d; + den = den * x + d4d; + den = den * x + d3d; + den = den * x + d2d; + den = den * x + d1d; + den = den * x + d0d; + + return num / den; +} + +// Computes G2 : y = 2/3 - (1 + 2/x) * (1/x + 0.5 - (1 + 1/x) * (1-exp(-x)) / +// x) using a 5/5th order rational approximation. It is accurate to 1e-6 over +// [0, 1e6]. Developed by Colin Josey using Remez's algorithm, with original +// implementation in OpenMOC at: +// https://github.com/mit-crpg/OpenMOC/blob/develop/src/exponentials.h +float exponentialG2(float tau) +{ + + // Coefficients for numerator in rational approximation + constexpr float g1n = -0.08335775885589858f; + constexpr float g2n = -0.003603942303847604f; + constexpr float g3n = 0.0037673183263550827f; + constexpr float g4n = 0.00001124183494990467f; + constexpr float g5n = 0.00016837426505799449f; + + // Coefficients for denominator in rational approximation + constexpr float g1d = 0.7454048371823628f; + constexpr float g2d = 0.23794300531408347f; + constexpr float g3d = 0.05367250964303789f; + constexpr float g4d = 0.006125197988351906f; + constexpr float g5d = 0.0010102514456857377f; + + float x = tau; + + float num = g5n; + num = num * x + g4n; + num = num * x + g3n; + num = num * x + g2n; + num = num * x + g1n; + num = num * x; + + float den = g5d; + den = den * x + g4d; + den = den * x + g3d; + den = den * x + g2d; + den = den * x + g1d; + den = den * x + 1.0f; + + return num / den; +} + //============================================================================== // RandomRay implementation //============================================================================== @@ -68,12 +182,18 @@ float cjosey_exponential(float tau) double RandomRay::distance_inactive_; double RandomRay::distance_active_; unique_ptr RandomRay::ray_source_; +RandomRaySourceShape RandomRay::source_shape_ {RandomRaySourceShape::FLAT}; RandomRay::RandomRay() : angular_flux_(data::mg.num_energy_groups_), delta_psi_(data::mg.num_energy_groups_), negroups_(data::mg.num_energy_groups_) -{} +{ + if (source_shape_ == RandomRaySourceShape::LINEAR || + source_shape_ == RandomRaySourceShape::LINEAR_XY) { + delta_moments_.resize(negroups_); + } +} RandomRay::RandomRay(uint64_t ray_id, FlatSourceDomain* domain) : RandomRay() { @@ -152,6 +272,21 @@ void RandomRay::event_advance_ray() } } +void RandomRay::attenuate_flux(double distance, bool is_active) +{ + switch (source_shape_) { + case RandomRaySourceShape::FLAT: + attenuate_flux_flat_source(distance, is_active); + break; + case RandomRaySourceShape::LINEAR: + case RandomRaySourceShape::LINEAR_XY: + attenuate_flux_linear_source(distance, is_active); + break; + default: + fatal_error("Unknown source shape for random ray transport."); + } +} + // This function forms the inner loop of the random ray transport process. // It is responsible for several tasks. Based on the incoming angular flux // of the ray and the source term in the region, the outgoing angular flux @@ -165,7 +300,7 @@ void RandomRay::event_advance_ray() // than use of many atomic operations corresponding to each energy group // individually (at least on CPU). Several other bookkeeping tasks are also // performed when inside the lock. -void RandomRay::attenuate_flux(double distance, bool is_active) +void RandomRay::attenuate_flux_flat_source(double distance, bool is_active) { // The number of geometric intersections is counted for reporting purposes n_event()++; @@ -236,6 +371,158 @@ void RandomRay::attenuate_flux(double distance, bool is_active) } } +void RandomRay::attenuate_flux_linear_source(double distance, bool is_active) +{ + // Cast domain to LinearSourceDomain + LinearSourceDomain* domain = dynamic_cast(domain_); + if (!domain) { + fatal_error("RandomRay::attenuate_flux_linear_source() called with " + "non-LinearSourceDomain domain."); + } + + // The number of geometric intersections is counted for reporting purposes + n_event()++; + + // Determine source region index etc. + int i_cell = lowest_coord().cell; + + // The source region is the spatial region index + int64_t source_region = + domain_->source_region_offsets_[i_cell] + cell_instance(); + + // The source element is the energy-specific region index + int64_t source_element = source_region * negroups_; + int material = this->material(); + + // Temperature and angle indices, if using multiple temperature + // data sets and/or anisotropic data sets. + // TODO: Currently assumes we are only using single temp/single + // angle data. + const int t = 0; + const int a = 0; + + Position& centroid = domain->centroid_[source_region]; + Position midpoint = r() + u() * (distance / 2.0); + + // Determine the local position of the midpoint and the ray origin + // relative to the source region's centroid + Position rm_local; + Position r0_local; + + // In the first few iterations of the simulation, the source region + // may not yet have had any ray crossings, in which case there will + // be no estimate of its centroid. We detect this by checking if it has + // any accumulated volume. If its volume is zero, just use the midpoint + // of the ray as the region's centroid. + if (domain->volume_t_[source_region]) { + rm_local = midpoint - centroid; + r0_local = r() - centroid; + } else { + rm_local = {0.0, 0.0, 0.0}; + r0_local = -u() * 0.5 * distance; + } + double distance_2 = distance * distance; + + // Linear Source MOC incoming flux attenuation + source + // contribution/attenuation equation + for (int g = 0; g < negroups_; g++) { + + // Compute tau, the optical thickness of the ray segment + float sigma_t = data::mg.macro_xs_[material].get_xs( + MgxsType::TOTAL, g, NULL, NULL, NULL, t, a); + float tau = sigma_t * distance; + + // If tau is very small, set it to zero to avoid numerical issues. + // The following computations will still work with tau = 0. + if (tau < 1.0e-8f) { + tau = 0.0f; + } + + // Compute linear source terms, spatial and directional (dir), + // calculated from the source gradients dot product with local centroid + // and direction, respectively. + float spatial_source = + domain_->source_[source_element + g] + + rm_local.dot(domain->source_gradients_[source_element + g]); + float dir_source = u().dot(domain->source_gradients_[source_element + g]); + + float gn = exponentialG(tau); + float f1 = 1.0f - tau * gn; + float f2 = (2.0f * gn - f1) * distance_2; + float new_delta_psi = (angular_flux_[g] - spatial_source) * f1 * distance - + 0.5 * dir_source * f2; + + float h1 = f1 - gn; + float g1 = 0.5f - h1; + float g2 = exponentialG2(tau); + g1 = g1 * spatial_source; + g2 = g2 * dir_source * distance * 0.5f; + h1 = h1 * angular_flux_[g]; + h1 = (g1 + g2 + h1) * distance_2; + spatial_source = spatial_source * distance + new_delta_psi; + + // Store contributions for this group into arrays, so that they can + // be accumulated into the source region's estimates inside of the locked + // region. + delta_psi_[g] = new_delta_psi; + delta_moments_[g] = r0_local * spatial_source + u() * h1; + + // Update the angular flux for this group + angular_flux_[g] -= new_delta_psi * sigma_t; + + // If 2D mode is enabled, the z-component of the flux moments is forced + // to zero + if (source_shape_ == RandomRaySourceShape::LINEAR_XY) { + delta_moments_[g].z = 0.0; + } + } + + // If ray is in the active phase (not in dead zone), make contributions to + // source region bookkeeping + if (is_active) { + // Compute an estimate of the spatial moments matrix for the source + // region based on parameters from this ray's crossing + MomentMatrix moment_matrix_estimate; + moment_matrix_estimate.compute_spatial_moments_matrix( + rm_local, u(), distance); + + // Aquire lock for source region + domain_->lock_[source_region].lock(); + + // Accumulate deltas into the new estimate of source region flux for this + // iteration + for (int g = 0; g < negroups_; g++) { + domain_->scalar_flux_new_[source_element + g] += delta_psi_[g]; + domain->flux_moments_new_[source_element + g] += delta_moments_[g]; + } + + // Accumulate the volume (ray segment distance), centroid, and spatial + // momement estimates into the running totals for the iteration for this + // source region. The centroid and spatial momements estimates are scaled by + // the ray segment length as part of length averaging of the estimates. + domain_->volume_[source_region] += distance; + domain->centroid_iteration_[source_region] += midpoint * distance; + moment_matrix_estimate *= distance; + domain->mom_matrix_[source_region] += moment_matrix_estimate; + + // If the source region hasn't been hit yet this iteration, + // indicate that it now has + if (domain_->was_hit_[source_region] == 0) { + domain_->was_hit_[source_region] = 1; + } + + // Tally valid position inside the source region (e.g., midpoint of + // the ray) if not done already + if (!domain_->position_recorded_[source_region]) { + domain_->position_[source_region] = midpoint; + domain_->position_recorded_[source_region] = 1; + } + + // Release lock + domain_->lock_[source_region].unlock(); + } +} + void RandomRay::initialize_ray(uint64_t ray_id, FlatSourceDomain* domain) { domain_ = domain; diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index 17866c30208..4bc77645bcd 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -6,6 +6,7 @@ #include "openmc/mgxs_interface.h" #include "openmc/output.h" #include "openmc/plot.h" +#include "openmc/random_ray/flat_source_domain.h" #include "openmc/random_ray/random_ray.h" #include "openmc/simulation.h" #include "openmc/source.h" @@ -241,14 +242,26 @@ RandomRaySimulation::RandomRaySimulation() // Random ray mode does not have an inner loop over generations within a // batch, so set the current gen to 1 simulation::current_gen = 1; + + switch (RandomRay::source_shape_) { + case RandomRaySourceShape::FLAT: + domain_ = make_unique(); + break; + case RandomRaySourceShape::LINEAR: + case RandomRaySourceShape::LINEAR_XY: + domain_ = make_unique(); + break; + default: + fatal_error("Unknown random ray source shape"); + } } void RandomRaySimulation::simulate() { if (settings::run_mode == RunMode::FIXED_SOURCE) { // Transfer external source user inputs onto random ray source regions - domain_.convert_external_sources(); - domain_.count_external_source_regions(); + domain_->convert_external_sources(); + domain_->count_external_source_regions(); } // Random ray power iteration loop @@ -262,11 +275,11 @@ void RandomRaySimulation::simulate() simulation::total_weight = 1.0; // Update source term (scattering + fission) - domain_.update_neutron_source(k_eff_); + domain_->update_neutron_source(k_eff_); // Reset scalar fluxes, iteration volume tallies, and region hit flags to // zero - domain_.batch_reset(); + domain_->batch_reset(); // Start timer for transport simulation::time_transport.start(); @@ -275,7 +288,7 @@ void RandomRaySimulation::simulate() #pragma omp parallel for schedule(dynamic) \ reduction(+ : total_geometric_intersections_) for (int i = 0; i < simulation::work_per_rank; i++) { - RandomRay ray(i, &domain_); + RandomRay ray(i, domain_.get()); total_geometric_intersections_ += ray.transport_history_based_single_ray(); } @@ -283,18 +296,18 @@ void RandomRaySimulation::simulate() simulation::time_transport.stop(); // If using multiple MPI ranks, perform all reduce on all transport results - domain_.all_reduce_replicated_source_regions(); + domain_->all_reduce_replicated_source_regions(); // Normalize scalar flux and update volumes - domain_.normalize_scalar_flux_and_volumes( + domain_->normalize_scalar_flux_and_volumes( settings::n_particles * RandomRay::distance_active_); // Add source to scalar flux, compute number of FSR hits - int64_t n_hits = domain_.add_source_to_scalar_flux(); + int64_t n_hits = domain_->add_source_to_scalar_flux(); if (settings::run_mode == RunMode::EIGENVALUE) { // Compute random ray k-eff - k_eff_ = domain_.compute_k_eff(k_eff_); + k_eff_ = domain_->compute_k_eff(k_eff_); // Store random ray k-eff into OpenMC's native k-eff variable global_tally_tracklength = k_eff_; @@ -304,19 +317,19 @@ void RandomRaySimulation::simulate() if (simulation::current_batch > settings::n_inactive && mpi::master) { // Generate mapping between source regions and tallies - if (!domain_.mapped_all_tallies_) { - domain_.convert_source_regions_to_tallies(); + if (!domain_->mapped_all_tallies_) { + domain_->convert_source_regions_to_tallies(); } // Use above mapping to contribute FSR flux data to appropriate tallies - domain_.random_ray_tally(); + domain_->random_ray_tally(); // Add this iteration's scalar flux estimate to final accumulated estimate - domain_.accumulate_iteration_flux(); + domain_->accumulate_iteration_flux(); } // Set phi_old = phi_new - domain_.scalar_flux_old_.swap(domain_.scalar_flux_new_); + domain_->flux_swap(); // Check for any obvious insabilities/nans/infs instability_check(n_hits, k_eff_, avg_miss_rate_); @@ -347,9 +360,9 @@ void RandomRaySimulation::output_simulation_results() const if (mpi::master) { print_results_random_ray(total_geometric_intersections_, avg_miss_rate_ / settings::n_batches, negroups_, - domain_.n_source_regions_, domain_.n_external_source_regions_); + domain_->n_source_regions_, domain_->n_external_source_regions_); if (model::plots.size() > 0) { - domain_.output_to_vtk(); + domain_->output_to_vtk(); } } } @@ -359,25 +372,28 @@ void RandomRaySimulation::output_simulation_results() const void RandomRaySimulation::instability_check( int64_t n_hits, double k_eff, double& avg_miss_rate) const { - double percent_missed = ((domain_.n_source_regions_ - n_hits) / - static_cast(domain_.n_source_regions_)) * + double percent_missed = ((domain_->n_source_regions_ - n_hits) / + static_cast(domain_->n_source_regions_)) * 100.0; avg_miss_rate += percent_missed; - if (percent_missed > 10.0) { - warning(fmt::format( - "Very high FSR miss rate detected ({:.3f}%). Instability may occur. " - "Increase ray density by adding more rays and/or active distance.", - percent_missed)); - } else if (percent_missed > 0.01) { - warning(fmt::format("Elevated FSR miss rate detected ({:.3f}%). Increasing " - "ray density by adding more rays and/or active " - "distance may improve simulation efficiency.", - percent_missed)); - } + if (mpi::master) { + if (percent_missed > 10.0) { + warning(fmt::format( + "Very high FSR miss rate detected ({:.3f}%). Instability may occur. " + "Increase ray density by adding more rays and/or active distance.", + percent_missed)); + } else if (percent_missed > 0.01) { + warning( + fmt::format("Elevated FSR miss rate detected ({:.3f}%). Increasing " + "ray density by adding more rays and/or active " + "distance may improve simulation efficiency.", + percent_missed)); + } - if (k_eff > 10.0 || k_eff < 0.01 || !(std::isfinite(k_eff))) { - fatal_error("Instability detected"); + if (k_eff > 10.0 || k_eff < 0.01 || !(std::isfinite(k_eff))) { + fatal_error("Instability detected"); + } } } diff --git a/src/settings.cpp b/src/settings.cpp index b92b77df937..db956abf5ca 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -269,6 +269,19 @@ void get_run_parameters(pugi::xml_node node_base) } else { fatal_error("Specify random ray source in settings XML"); } + if (check_for_node(random_ray_node, "source_shape")) { + std::string temp_str = + get_node_value(random_ray_node, "source_shape", true, true); + if (temp_str == "flat") { + RandomRay::source_shape_ = RandomRaySourceShape::FLAT; + } else if (temp_str == "linear") { + RandomRay::source_shape_ = RandomRaySourceShape::LINEAR; + } else if (temp_str == "linear_xy") { + RandomRay::source_shape_ = RandomRaySourceShape::LINEAR_XY; + } else { + fatal_error("Unrecognized source shape: " + temp_str); + } + } if (check_for_node(random_ray_node, "volume_normalized_flux_tallies")) { FlatSourceDomain::volume_normalized_flux_tallies_ = get_node_value_bool(random_ray_node, "volume_normalized_flux_tallies"); diff --git a/tests/regression_tests/random_ray_fixed_source_linear/__init__.py b/tests/regression_tests/random_ray_fixed_source_linear/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat new file mode 100644 index 00000000000..e2972c5f20a --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat @@ -0,0 +1,245 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 10 + 5 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + linear + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_fixed_source_linear/linear/results_true.dat b/tests/regression_tests/random_ray_fixed_source_linear/linear/results_true.dat new file mode 100644 index 00000000000..7fb01a0ea11 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_linear/linear/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +-5.745718E+02 +9.757661E+04 +tally 2: +3.074428E-02 +1.952251E-04 +tally 3: +1.980876E-03 +7.970502E-07 diff --git a/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat new file mode 100644 index 00000000000..5b958d65cdb --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat @@ -0,0 +1,245 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 10 + 5 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + linear_xy + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/results_true.dat b/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/results_true.dat new file mode 100644 index 00000000000..22b39edcfd8 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +-5.745810E+02 +9.758220E+04 +tally 2: +3.022777E-02 +1.884091E-04 +tally 3: +1.980651E-03 +7.968779E-07 diff --git a/tests/regression_tests/random_ray_fixed_source_linear/test.py b/tests/regression_tests/random_ray_fixed_source_linear/test.py new file mode 100644 index 00000000000..25335dea602 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_linear/test.py @@ -0,0 +1,27 @@ +import os + +import numpy as np +import openmc +from openmc.utility_funcs import change_directory +from openmc.examples import random_ray_three_region_cube +import pytest + +from tests.testing_harness import TolerantPyAPITestHarness + + +class MGXSTestHarness(TolerantPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + + +@pytest.mark.parametrize("shape", ["linear", "linear_xy"]) +def test_random_ray_fixed_source_linear(shape): + with change_directory(shape): + openmc.reset_auto_ids() + model = random_ray_three_region_cube() + model.settings.random_ray['source_shape'] = shape + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/regression_tests/random_ray_linear/__init__.py b/tests/regression_tests/random_ray_linear/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regression_tests/random_ray_linear/linear/inputs_true.dat b/tests/regression_tests/random_ray_linear/linear/inputs_true.dat new file mode 100644 index 00000000000..c580be3d19e --- /dev/null +++ b/tests/regression_tests/random_ray_linear/linear/inputs_true.dat @@ -0,0 +1,110 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.126 0.126 + 10 10 + -0.63 -0.63 + +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 + + + 1.26 1.26 + 2 2 + -1.26 -1.26 + +2 2 +2 5 + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + multi-group + + 100.0 + 20.0 + + + -1.26 -1.26 -1 1.26 1.26 1 + + + True + linear + + + + + 2 2 + -1.26 -1.26 + 1.26 1.26 + + + 1 + + + 1e-05 0.0635 10.0 100.0 1000.0 500000.0 1000000.0 20000000.0 + + + 1 2 + flux fission nu-fission + analog + + + diff --git a/tests/regression_tests/random_ray_linear/linear/results_true.dat b/tests/regression_tests/random_ray_linear/linear/results_true.dat new file mode 100644 index 00000000000..a3e24dc75d8 --- /dev/null +++ b/tests/regression_tests/random_ray_linear/linear/results_true.dat @@ -0,0 +1,171 @@ +k-combined: +8.273022E-01 1.347623E-02 +tally 1: +5.004109E+00 +5.022655E+00 +1.844047E+00 +6.833376E-01 +4.488042E+00 +4.047669E+00 +2.824818E+00 +1.599927E+00 +4.182704E-01 +3.509796E-02 +1.017987E+00 +2.078987E-01 +1.676761E+00 +5.682729E-01 +5.385624E-02 +5.857594E-04 +1.310753E-01 +3.469677E-03 +2.353602E+00 +1.127695E+00 +7.721312E-02 +1.212202E-03 +1.879213E-01 +7.180339E-03 +7.082957E+00 +1.019023E+01 +8.203580E-02 +1.366996E-03 +1.996612E-01 +8.097436E-03 +2.034293E+01 +8.321967E+01 +3.099116E-02 +1.933713E-04 +7.668546E-02 +1.183975E-03 +1.311478E+01 +3.442575E+01 +1.778200E-01 +6.347187E-03 +4.945973E-01 +4.910476E-02 +7.576546E+00 +1.148094E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +3.386885E+00 +2.295318E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.823720E+00 +6.769694E-01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.703042E+00 +1.494228E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +7.465612E+00 +1.132769E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.820337E+01 +6.657534E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.127686E+01 +2.549122E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.561053E+00 +4.169936E+00 +1.674166E+00 +5.624648E-01 +4.074585E+00 +3.331694E+00 +2.723008E+00 +1.487311E+00 +4.059013E-01 +3.307772E-02 +9.878827E-01 +1.959320E-01 +1.663524E+00 +5.584734E-01 +5.399658E-02 +5.877094E-04 +1.314169E-01 +3.481227E-03 +2.310994E+00 +1.086435E+00 +7.627297E-02 +1.181877E-03 +1.856331E-01 +7.000707E-03 +7.100201E+00 +1.023633E+01 +8.285591E-02 +1.393520E-03 +2.016572E-01 +8.254554E-03 +2.119292E+01 +9.022917E+01 +3.281214E-02 +2.163583E-04 +8.119135E-02 +1.324719E-03 +1.380975E+01 +3.815787E+01 +1.919186E-01 +7.384267E-03 +5.338118E-01 +5.712809E-02 +5.024478E+00 +5.056216E+00 +1.888826E+00 +7.148091E-01 +4.597025E+00 +4.234087E+00 +2.839930E+00 +1.616473E+00 +4.294394E-01 +3.697612E-02 +1.045170E+00 +2.190237E-01 +1.688196E+00 +5.765397E-01 +5.539650E-02 +6.200086E-04 +1.348240E-01 +3.672547E-03 +2.361633E+00 +1.136540E+00 +7.901052E-02 +1.270359E-03 +1.922958E-01 +7.524822E-03 +7.100780E+00 +1.024481E+01 +8.389610E-02 +1.429594E-03 +2.041888E-01 +8.468240E-03 +2.049187E+01 +8.438141E+01 +3.195576E-02 +2.052140E-04 +7.907229E-02 +1.256485E-03 +1.327412E+01 +3.524466E+01 +1.850084E-01 +6.847675E-03 +5.145915E-01 +5.297677E-02 diff --git a/tests/regression_tests/random_ray_linear/linear_xy/inputs_true.dat b/tests/regression_tests/random_ray_linear/linear_xy/inputs_true.dat new file mode 100644 index 00000000000..5c4076de70f --- /dev/null +++ b/tests/regression_tests/random_ray_linear/linear_xy/inputs_true.dat @@ -0,0 +1,110 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.126 0.126 + 10 10 + -0.63 -0.63 + +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 + + + 1.26 1.26 + 2 2 + -1.26 -1.26 + +2 2 +2 5 + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + multi-group + + 100.0 + 20.0 + + + -1.26 -1.26 -1 1.26 1.26 1 + + + True + linear_xy + + + + + 2 2 + -1.26 -1.26 + 1.26 1.26 + + + 1 + + + 1e-05 0.0635 10.0 100.0 1000.0 500000.0 1000000.0 20000000.0 + + + 1 2 + flux fission nu-fission + analog + + + diff --git a/tests/regression_tests/random_ray_linear/linear_xy/results_true.dat b/tests/regression_tests/random_ray_linear/linear_xy/results_true.dat new file mode 100644 index 00000000000..f79b3fa787f --- /dev/null +++ b/tests/regression_tests/random_ray_linear/linear_xy/results_true.dat @@ -0,0 +1,171 @@ +k-combined: +8.368882E-01 8.107070E-03 +tally 1: +5.072700E+00 +5.152684E+00 +1.876680E+00 +7.051697E-01 +4.567463E+00 +4.176989E+00 +2.858196E+00 +1.636775E+00 +4.239946E-01 +3.601884E-02 +1.031918E+00 +2.133534E-01 +1.692006E+00 +5.789664E-01 +5.442292E-02 +5.988675E-04 +1.324545E-01 +3.547321E-03 +2.371801E+00 +1.146513E+00 +7.804414E-02 +1.241074E-03 +1.899438E-01 +7.351359E-03 +7.134037E+00 +1.034554E+01 +8.269855E-02 +1.390936E-03 +2.012742E-01 +8.239245E-03 +2.043174E+01 +8.386878E+01 +3.098171E-02 +1.929186E-04 +7.666208E-02 +1.181203E-03 +1.312800E+01 +3.447362E+01 +1.763141E-01 +6.217438E-03 +4.904088E-01 +4.810096E-02 +7.608165E+00 +1.157716E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +3.392187E+00 +2.302667E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.821339E+00 +6.737984E-01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.695742E+00 +1.483104E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +7.457904E+00 +1.129343E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.824331E+01 +6.687165E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.138096E+01 +2.591161E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.582534E+00 +4.206969E+00 +1.707129E+00 +5.837143E-01 +4.154811E+00 +3.457563E+00 +2.727459E+00 +1.491183E+00 +4.100516E-01 +3.371280E-02 +9.979836E-01 +1.996938E-01 +1.660765E+00 +5.570066E-01 +5.425719E-02 +5.941503E-04 +1.320511E-01 +3.519379E-03 +2.306630E+00 +1.083425E+00 +7.695103E-02 +1.205473E-03 +1.872834E-01 +7.140475E-03 +7.077631E+00 +1.018246E+01 +8.321109E-02 +1.408010E-03 +2.025216E-01 +8.340388E-03 +2.094896E+01 +8.817186E+01 +3.233088E-02 +2.099749E-04 +8.000050E-02 +1.285635E-03 +1.356905E+01 +3.683229E+01 +1.859858E-01 +6.921278E-03 +5.173102E-01 +5.354619E-02 +5.056029E+00 +5.118575E+00 +1.910007E+00 +7.308066E-01 +4.648575E+00 +4.328846E+00 +2.856363E+00 +1.634636E+00 +4.328679E-01 +3.755784E-02 +1.053514E+00 +2.224694E-01 +1.692444E+00 +5.793158E-01 +5.559366E-02 +6.243434E-04 +1.353038E-01 +3.698224E-03 +2.368251E+00 +1.142832E+00 +7.949453E-02 +1.285913E-03 +1.934738E-01 +7.616955E-03 +7.118354E+00 +1.029753E+01 +8.426801E-02 +1.442436E-03 +2.050940E-01 +8.544309E-03 +2.046889E+01 +8.419814E+01 +3.182500E-02 +2.035334E-04 +7.874874E-02 +1.246195E-03 +1.326056E+01 +3.517079E+01 +1.833225E-01 +6.723237E-03 +5.099023E-01 +5.201406E-02 diff --git a/tests/regression_tests/random_ray_linear/test.py b/tests/regression_tests/random_ray_linear/test.py new file mode 100644 index 00000000000..10262789deb --- /dev/null +++ b/tests/regression_tests/random_ray_linear/test.py @@ -0,0 +1,26 @@ +import os + +import openmc +from openmc.examples import random_ray_lattice +from openmc.utility_funcs import change_directory +import pytest + +from tests.testing_harness import TolerantPyAPITestHarness + + +class MGXSTestHarness(TolerantPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + + +@pytest.mark.parametrize("shape", ["linear", "linear_xy"]) +def test_random_ray_source(shape): + with change_directory(shape): + openmc.reset_auto_ids() + model = random_ray_lattice() + model.settings.random_ray['source_shape'] = shape + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() From 32440ad203116d13e893a5ac37ab848df684011b Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 18 Jul 2024 11:54:06 -0500 Subject: [PATCH 040/184] Remove use of pkg_resources package (#3069) --- openmc/lib/__init__.py | 8 +++----- pyproject.toml | 2 +- tests/regression_tests/conftest.py | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/openmc/lib/__init__.py b/openmc/lib/__init__.py index 42d77944146..9bb2efb38af 100644 --- a/openmc/lib/__init__.py +++ b/openmc/lib/__init__.py @@ -13,11 +13,10 @@ """ from ctypes import CDLL, c_bool, c_int +import importlib.resources import os import sys -import pkg_resources - # Determine shared-library suffix if sys.platform == 'darwin': @@ -27,9 +26,8 @@ if os.environ.get('READTHEDOCS', None) != 'True': # Open shared library - _filename = pkg_resources.resource_filename( - __name__, f'libopenmc.{_suffix}') - _dll = CDLL(_filename) + _filename = importlib.resources.files(__name__) / f'libopenmc.{_suffix}' + _dll = CDLL(str(_filename)) # TODO: Remove str() when Python 3.12+ else: # For documentation builds, we don't actually have the shared library # available. Instead, we create a mock object so that when the modules diff --git a/pyproject.toml b/pyproject.toml index ca646323b24..d1b2c335cd7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ docs = [ "sphinxcontrib-svg2pdfconverter", "sphinx-rtd-theme==1.0.0" ] -test = ["pytest", "pytest-cov", "colorama", "openpyxl"] +test = ["packaging", "pytest", "pytest-cov", "colorama", "openpyxl"] vtk = ["vtk"] [project.urls] diff --git a/tests/regression_tests/conftest.py b/tests/regression_tests/conftest.py index b4a7644762c..1cdf414e767 100644 --- a/tests/regression_tests/conftest.py +++ b/tests/regression_tests/conftest.py @@ -1,12 +1,12 @@ import numpy as np import openmc -from pkg_resources import parse_version +from packaging.version import parse import pytest @pytest.fixture(scope='module', autouse=True) def numpy_version_requirement(): - assert parse_version(np.__version__) >= parse_version("1.14"), \ + assert parse(np.__version__) >= parse("1.14"), \ "Regression tests require NumPy 1.14 or greater" From 4c0e08bae86c9d40c70740bc98e043b81d295a1f Mon Sep 17 00:00:00 2001 From: John Vincent Cauilan <64677361+johvincau@users.noreply.github.com> Date: Thu, 18 Jul 2024 12:40:42 -0500 Subject: [PATCH 041/184] Replace all deprecated Python typing imports and syntax with updated forms (#3085) Co-authored-by: Paul Romano --- openmc/bounding_box.py | 2 +- openmc/checkvalue.py | 3 +- openmc/data/data.py | 5 +- openmc/data/decay.py | 3 +- openmc/deplete/coupled_operator.py | 3 +- openmc/deplete/independent_operator.py | 3 +- openmc/deplete/microxs.py | 24 ++++---- openmc/deplete/openmc_operator.py | 3 +- openmc/deplete/reaction_rates.py | 7 +-- openmc/deplete/results.py | 35 ++++++----- openmc/geometry.py | 37 ++++++------ openmc/lib/mesh.py | 7 +-- openmc/material.py | 82 +++++++++++++------------- openmc/mesh.py | 56 +++++++++--------- openmc/model/model.py | 15 +++-- openmc/model/surface_composite.py | 3 +- openmc/plots.py | 3 +- openmc/settings.py | 26 ++++---- openmc/source.py | 48 ++++++++------- openmc/stats/multivariate.py | 18 +++--- openmc/stats/univariate.py | 25 ++++---- openmc/utility_funcs.py | 3 +- openmc/weight_windows.py | 20 +++---- 23 files changed, 204 insertions(+), 227 deletions(-) diff --git a/openmc/bounding_box.py b/openmc/bounding_box.py index 6e58ca8ba02..5f3d3a6cfdb 100644 --- a/openmc/bounding_box.py +++ b/openmc/bounding_box.py @@ -1,5 +1,5 @@ from __future__ import annotations -from typing import Iterable +from collections.abc import Iterable import numpy as np diff --git a/openmc/checkvalue.py b/openmc/checkvalue.py index 42df3e5efbe..4fa205b14f7 100644 --- a/openmc/checkvalue.py +++ b/openmc/checkvalue.py @@ -1,12 +1,11 @@ import copy import os -import typing # required to prevent typing.Union namespace overwriting Union from collections.abc import Iterable import numpy as np # Type for arguments that accept file paths -PathLike = typing.Union[str, os.PathLike] +PathLike = str | os.PathLike def check_type(name, value, expected_type, expected_iter_type=None, *, none_ok=False): diff --git a/openmc/data/data.py b/openmc/data/data.py index 7caf31e26ee..d94d6aaaa39 100644 --- a/openmc/data/data.py +++ b/openmc/data/data.py @@ -5,7 +5,6 @@ from pathlib import Path from math import sqrt, log from warnings import warn -from typing import Dict # Isotopic abundances from Meija J, Coplen T B, et al, "Isotopic compositions # of the elements 2013 (IUPAC Technical Report)", Pure. Appl. Chem. 88 (3), @@ -283,13 +282,13 @@ NEUTRON_MASS = 1.00866491595 # Used in atomic_mass function as a cache -_ATOMIC_MASS: Dict[str, float] = {} +_ATOMIC_MASS: dict[str, float] = {} # Regex for GNDS nuclide names (used in zam function) _GNDS_NAME_RE = re.compile(r'([A-Zn][a-z]*)(\d+)((?:_[em]\d+)?)') # Used in half_life function as a cache -_HALF_LIFE: Dict[str, float] = {} +_HALF_LIFE: dict[str, float] = {} _LOG_TWO = log(2.0) def atomic_mass(isotope): diff --git a/openmc/data/decay.py b/openmc/data/decay.py index be3dab77abf..66acb2212d8 100644 --- a/openmc/data/decay.py +++ b/openmc/data/decay.py @@ -2,7 +2,6 @@ from io import StringIO from math import log import re -from typing import Optional from warnings import warn import numpy as np @@ -579,7 +578,7 @@ def sources(self): _DECAY_PHOTON_ENERGY = {} -def decay_photon_energy(nuclide: str) -> Optional[Univariate]: +def decay_photon_energy(nuclide: str) -> Univariate | None: """Get photon energy distribution resulting from the decay of a nuclide This function relies on data stored in a depletion chain. Before calling it diff --git a/openmc/deplete/coupled_operator.py b/openmc/deplete/coupled_operator.py index acdcf467c57..17af935f4a2 100644 --- a/openmc/deplete/coupled_operator.py +++ b/openmc/deplete/coupled_operator.py @@ -10,7 +10,6 @@ import copy from warnings import warn -from typing import Optional import numpy as np from uncertainties import ufloat @@ -34,7 +33,7 @@ __all__ = ["CoupledOperator", "Operator", "OperatorResult"] -def _find_cross_sections(model: Optional[str] = None): +def _find_cross_sections(model: str | None = None): """Determine cross sections to use for depletion Parameters diff --git a/openmc/deplete/independent_operator.py b/openmc/deplete/independent_operator.py index 250b94deb42..759abde1308 100644 --- a/openmc/deplete/independent_operator.py +++ b/openmc/deplete/independent_operator.py @@ -8,7 +8,6 @@ from __future__ import annotations from collections.abc import Iterable import copy -from typing import List, Set import numpy as np from uncertainties import ufloat @@ -279,7 +278,7 @@ def _load_previous_results(self): new_res = res_obj.distribute(self.local_mats, mat_indexes) self.prev_res.append(new_res) - def _get_nuclides_with_data(self, cross_sections: List[MicroXS]) -> Set[str]: + def _get_nuclides_with_data(self, cross_sections: list[MicroXS]) -> set[str]: """Finds nuclides with cross section data""" return set(cross_sections[0].nuclides) diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index b2ae9f6c7f1..39753529b2f 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -5,8 +5,8 @@ """ from __future__ import annotations +from collections.abc import Iterable, Sequence from tempfile import TemporaryDirectory -from typing import List, Tuple, Iterable, Optional, Union, Sequence import pandas as pd import numpy as np @@ -27,7 +27,7 @@ _valid_rxns.append('damage-energy') -def _resolve_chain_file_path(chain_file: str): +def _resolve_chain_file_path(chain_file: str | None): if chain_file is None: chain_file = openmc.config.get('chain_file') if 'chain_file' not in openmc.config: @@ -41,12 +41,12 @@ def _resolve_chain_file_path(chain_file: str): def get_microxs_and_flux( model: openmc.Model, domains, - nuclides: Optional[Iterable[str]] = None, - reactions: Optional[Iterable[str]] = None, - energies: Optional[Union[Iterable[float], str]] = None, - chain_file: Optional[PathLike] = None, + nuclides: Iterable[str] | None = None, + reactions: Iterable[str] | None = None, + energies: Iterable[float] | str | None = None, + chain_file: PathLike | None = None, run_kwargs=None - ) -> Tuple[List[np.ndarray], List[MicroXS]]: + ) -> tuple[list[np.ndarray], list[MicroXS]]: """Generate a microscopic cross sections and flux from a Model .. versionadded:: 0.14.0 @@ -183,7 +183,7 @@ class MicroXS: :data:`openmc.deplete.chain.REACTIONS` """ - def __init__(self, data: np.ndarray, nuclides: List[str], reactions: List[str]): + def __init__(self, data: np.ndarray, nuclides: list[str], reactions: list[str]): # Validate inputs if data.shape[:2] != (len(nuclides), len(reactions)): raise ValueError( @@ -205,12 +205,12 @@ def __init__(self, data: np.ndarray, nuclides: List[str], reactions: List[str]): @classmethod def from_multigroup_flux( cls, - energies: Union[Sequence[float], str], + energies: Sequence[float] | str, multigroup_flux: Sequence[float], - chain_file: Optional[PathLike] = None, + chain_file: PathLike | None = None, temperature: float = 293.6, - nuclides: Optional[Sequence[str]] = None, - reactions: Optional[Sequence[str]] = None, + nuclides: Sequence[str] | None = None, + reactions: Sequence[str] | None = None, **init_kwargs: dict, ) -> MicroXS: """Generated microscopic cross sections from a known flux. diff --git a/openmc/deplete/openmc_operator.py b/openmc/deplete/openmc_operator.py index 75f0925c4e7..c0ff568bc27 100644 --- a/openmc/deplete/openmc_operator.py +++ b/openmc/deplete/openmc_operator.py @@ -7,7 +7,6 @@ from abc import abstractmethod from warnings import warn -from typing import List, Tuple, Dict import numpy as np @@ -185,7 +184,7 @@ def _differentiate_burnable_mats(self): """Assign distribmats for each burnable material""" pass - def _get_burnable_mats(self) -> Tuple[List[str], Dict[str, float], List[str]]: + def _get_burnable_mats(self) -> tuple[list[str], dict[str, float], list[str]]: """Determine depletable materials, volumes, and nuclides Returns diff --git a/openmc/deplete/reaction_rates.py b/openmc/deplete/reaction_rates.py index 4aaf829a9a5..714d9048b47 100644 --- a/openmc/deplete/reaction_rates.py +++ b/openmc/deplete/reaction_rates.py @@ -2,7 +2,6 @@ An ndarray to store reaction rates with string, integer, or slice indexing. """ -from typing import Dict import numpy as np @@ -53,9 +52,9 @@ class ReactionRates(np.ndarray): # the __array_finalize__ method (discussed here: # https://docs.scipy.org/doc/numpy/user/basics.subclassing.html) - index_mat: Dict[str, int] - index_nuc: Dict[str, int] - index_rx: Dict[str, int] + index_mat: dict[str, int] + index_nuc: dict[str, int] + index_rx: dict[str, int] def __new__(cls, local_mats, nuclides, reactions, from_results=False): # Create appropriately-sized zeroed-out ndarray diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index 2e537a1b735..f897a88422c 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -1,8 +1,7 @@ import numbers import bisect import math -import typing # required to prevent typing.Union namespace overwriting Union -from typing import Iterable, Optional, Tuple, List +from collections.abc import Iterable from warnings import warn import h5py @@ -97,11 +96,11 @@ def from_hdf5(cls, filename: PathLike): def get_activity( self, - mat: typing.Union[Material, str], + mat: Material | str, units: str = "Bq/cm3", by_nuclide: bool = False, - volume: Optional[float] = None - ) -> Tuple[np.ndarray, typing.Union[np.ndarray, List[dict]]]: + volume: float | None = None + ) -> tuple[np.ndarray, np.ndarray | list[dict]]: """Get activity of material over time. .. versionadded:: 0.14.0 @@ -152,11 +151,11 @@ def get_activity( def get_atoms( self, - mat: typing.Union[Material, str], + mat: Material | str, nuc: str, nuc_units: str = "atoms", time_units: str = "s" - ) -> Tuple[np.ndarray, np.ndarray]: + ) -> tuple[np.ndarray, np.ndarray]: """Get number of nuclides over time from a single material Parameters @@ -215,11 +214,11 @@ def get_atoms( def get_decay_heat( self, - mat: typing.Union[Material, str], + mat: Material | str, units: str = "W", by_nuclide: bool = False, - volume: Optional[float] = None - ) -> Tuple[np.ndarray, typing.Union[np.ndarray, List[dict]]]: + volume: float | None = None + ) -> tuple[np.ndarray, np.ndarray | list[dict]]: """Get decay heat of material over time. .. versionadded:: 0.14.0 @@ -242,7 +241,7 @@ def get_decay_heat( ------- times : numpy.ndarray Array of times in [s] - decay_heat : numpy.ndarray or List[dict] + decay_heat : numpy.ndarray or list[dict] Array of total decay heat values if by_nuclide = False (default) or list of dictionaries of decay heat values by nuclide if by_nuclide = True. @@ -270,11 +269,11 @@ def get_decay_heat( return times, decay_heat def get_mass(self, - mat: typing.Union[Material, str], + mat: Material | str, nuc: str, mass_units: str = "g", time_units: str = "s" - ) -> Tuple[np.ndarray, np.ndarray]: + ) -> tuple[np.ndarray, np.ndarray]: """Get mass of nuclides over time from a single material .. versionadded:: 0.14.0 @@ -324,10 +323,10 @@ def get_mass(self, def get_reaction_rate( self, - mat: typing.Union[Material, str], + mat: Material | str, nuc: str, rx: str - ) -> Tuple[np.ndarray, np.ndarray]: + ) -> tuple[np.ndarray, np.ndarray]: """Get reaction rate in a single material/nuclide over time Parameters @@ -364,7 +363,7 @@ def get_reaction_rate( return times, rates - def get_keff(self, time_units: str = 's') -> Tuple[np.ndarray, np.ndarray]: + def get_keff(self, time_units: str = 's') -> tuple[np.ndarray, np.ndarray]: """Evaluates the eigenvalue from a results list. .. versionadded:: 0.13.1 @@ -400,7 +399,7 @@ def get_keff(self, time_units: str = 's') -> Tuple[np.ndarray, np.ndarray]: times = _get_time_as(times, time_units) return times, eigenvalues - def get_eigenvalue(self, time_units: str = 's') -> Tuple[np.ndarray, np.ndarray]: + def get_eigenvalue(self, time_units: str = 's') -> tuple[np.ndarray, np.ndarray]: warn("The get_eigenvalue(...) function has been renamed get_keff and " "will be removed in a future version of OpenMC.", FutureWarning) return self.get_keff(time_units) @@ -526,7 +525,7 @@ def get_step_where( def export_to_materials( self, burnup_index: int, - nuc_with_data: Optional[Iterable[str]] = None, + nuc_with_data: Iterable[str] | None = None, path: PathLike = 'materials.xml' ) -> Materials: """Return openmc.Materials object based on results at a given step diff --git a/openmc/geometry.py b/openmc/geometry.py index e37a69c73d2..6cce4c18c70 100644 --- a/openmc/geometry.py +++ b/openmc/geometry.py @@ -1,6 +1,5 @@ from __future__ import annotations import os -import typing from collections import defaultdict from copy import deepcopy from collections.abc import Iterable @@ -41,7 +40,7 @@ class Geometry: def __init__( self, - root: openmc.UniverseBase | typing.Iterable[openmc.Cell] | None = None, + root: openmc.UniverseBase | Iterable[openmc.Cell] | None = None, merge_surfaces: bool = False, surface_precision: int = 10 ): @@ -267,7 +266,7 @@ def get_universe(univ_id): def from_xml( cls, path: PathLike = 'geometry.xml', - materials: typing.Optional[typing.Union[PathLike, 'openmc.Materials']] = 'materials.xml' + materials: PathLike | 'openmc.Materials' | None = 'materials.xml' ) -> Geometry: """Generate geometry from XML file @@ -316,7 +315,7 @@ def find(self, point) -> list: """ return self.root_universe.find(point) - def get_instances(self, paths) -> typing.Union[int, typing.List[int]]: + def get_instances(self, paths) -> int | list[int]: """Return the instance number(s) for a cell/material in a geometry path. The instance numbers are used as indices into distributed @@ -363,7 +362,7 @@ def get_instances(self, paths) -> typing.Union[int, typing.List[int]]: return indices if return_list else indices[0] - def get_all_cells(self) -> typing.Dict[int, openmc.Cell]: + def get_all_cells(self) -> dict[int, openmc.Cell]: """Return all cells in the geometry. Returns @@ -377,7 +376,7 @@ def get_all_cells(self) -> typing.Dict[int, openmc.Cell]: else: return {} - def get_all_universes(self) -> typing.Dict[int, openmc.Universe]: + def get_all_universes(self) -> dict[int, openmc.Universe]: """Return all universes in the geometry. Returns @@ -392,7 +391,7 @@ def get_all_universes(self) -> typing.Dict[int, openmc.Universe]: universes.update(self.root_universe.get_all_universes()) return universes - def get_all_nuclides(self) -> typing.List[str]: + def get_all_nuclides(self) -> list[str]: """Return all nuclides within the geometry. Returns @@ -406,7 +405,7 @@ def get_all_nuclides(self) -> typing.List[str]: all_nuclides |= set(material.get_nuclides()) return sorted(all_nuclides) - def get_all_materials(self) -> typing.Dict[int, openmc.Material]: + def get_all_materials(self) -> dict[int, openmc.Material]: """Return all materials within the geometry. Returns @@ -421,7 +420,7 @@ def get_all_materials(self) -> typing.Dict[int, openmc.Material]: else: return {} - def get_all_material_cells(self) -> typing.Dict[int, openmc.Cell]: + def get_all_material_cells(self) -> dict[int, openmc.Cell]: """Return all cells filled by a material Returns @@ -440,7 +439,7 @@ def get_all_material_cells(self) -> typing.Dict[int, openmc.Cell]: return material_cells - def get_all_material_universes(self) -> typing.Dict[int, openmc.Universe]: + def get_all_material_universes(self) -> dict[int, openmc.Universe]: """Return all universes having at least one material-filled cell. This method can be used to find universes that have at least one cell @@ -463,7 +462,7 @@ def get_all_material_universes(self) -> typing.Dict[int, openmc.Universe]: return material_universes - def get_all_lattices(self) -> typing.Dict[int, openmc.Lattice]: + def get_all_lattices(self) -> dict[int, openmc.Lattice]: """Return all lattices defined Returns @@ -481,7 +480,7 @@ def get_all_lattices(self) -> typing.Dict[int, openmc.Lattice]: return lattices - def get_all_surfaces(self) -> typing.Dict[int, openmc.Surface]: + def get_all_surfaces(self) -> dict[int, openmc.Surface]: """ Return all surfaces used in the geometry @@ -517,7 +516,7 @@ def _get_domains_by_name(self, name, case_sensitive, matching, domain_type) -> l def get_materials_by_name( self, name, case_sensitive=False, matching=False - ) -> typing.List[openmc.Material]: + ) -> list[openmc.Material]: """Return a list of materials with matching names. Parameters @@ -540,7 +539,7 @@ def get_materials_by_name( def get_cells_by_name( self, name, case_sensitive=False, matching=False - ) -> typing.List[openmc.Cell]: + ) -> list[openmc.Cell]: """Return a list of cells with matching names. Parameters @@ -563,7 +562,7 @@ def get_cells_by_name( def get_surfaces_by_name( self, name, case_sensitive=False, matching=False - ) -> typing.List[openmc.Surface]: + ) -> list[openmc.Surface]: """Return a list of surfaces with matching names. .. versionadded:: 0.13.3 @@ -588,7 +587,7 @@ def get_surfaces_by_name( def get_cells_by_fill_name( self, name, case_sensitive=False, matching=False - ) -> typing.List[openmc.Cell]: + ) -> list[openmc.Cell]: """Return a list of cells with fills with matching names. Parameters @@ -635,7 +634,7 @@ def get_cells_by_fill_name( def get_universes_by_name( self, name, case_sensitive=False, matching=False - ) -> typing.List[openmc.Universe]: + ) -> list[openmc.Universe]: """Return a list of universes with matching names. Parameters @@ -658,7 +657,7 @@ def get_universes_by_name( def get_lattices_by_name( self, name, case_sensitive=False, matching=False - ) -> typing.List[openmc.Lattice]: + ) -> list[openmc.Lattice]: """Return a list of lattices with matching names. Parameters @@ -679,7 +678,7 @@ def get_lattices_by_name( """ return self._get_domains_by_name(name, case_sensitive, matching, 'lattice') - def remove_redundant_surfaces(self) -> typing.Dict[int, openmc.Surface]: + def remove_redundant_surfaces(self) -> dict[int, openmc.Surface]: """Remove and return all of the redundant surfaces. Uses surface_precision attribute of Geometry instance for rounding and diff --git a/openmc/lib/mesh.py b/openmc/lib/mesh.py index 3546112b54d..78566d449a6 100644 --- a/openmc/lib/mesh.py +++ b/openmc/lib/mesh.py @@ -1,8 +1,7 @@ -from collections.abc import Mapping +from collections.abc import Mapping, Sequence from ctypes import (c_int, c_int32, c_char_p, c_double, POINTER, Structure, create_string_buffer, c_uint64, c_size_t) from random import getrandbits -from typing import Optional, List, Tuple, Sequence from weakref import WeakValueDictionary import numpy as np @@ -170,8 +169,8 @@ def volumes(self) -> np.ndarray: def material_volumes( self, n_samples: int = 10_000, - prn_seed: Optional[int] = None - ) -> List[List[Tuple[Material, float]]]: + prn_seed: int | None = None + ) -> list[list[tuple[Material, float]]]: """Determine volume of materials in each mesh element .. versionadded:: 0.15.0 diff --git a/openmc/material.py b/openmc/material.py index 4b871b77d60..5b958c75a68 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -5,9 +5,7 @@ from numbers import Real from pathlib import Path import re -import typing # imported separately as py3.8 requires typing.Iterable import warnings -from typing import Optional, List, Union, Dict import lxml.etree as ET import numpy as np @@ -161,11 +159,11 @@ def __repr__(self) -> str: return string @property - def name(self) -> Optional[str]: + def name(self) -> str | None: return self._name @name.setter - def name(self, name: Optional[str]): + def name(self, name: str | None): if name is not None: cv.check_type(f'name for Material ID="{self._id}"', name, str) @@ -174,17 +172,17 @@ def name(self, name: Optional[str]): self._name = '' @property - def temperature(self) -> Optional[float]: + def temperature(self) -> float | None: return self._temperature @temperature.setter - def temperature(self, temperature: Optional[Real]): + def temperature(self, temperature: Real | None): cv.check_type(f'Temperature for Material ID="{self._id}"', temperature, (Real, type(None))) self._temperature = temperature @property - def density(self) -> Optional[float]: + def density(self) -> float | None: return self._density @property @@ -202,7 +200,7 @@ def depletable(self, depletable: bool): self._depletable = depletable @property - def paths(self) -> List[str]: + def paths(self) -> list[str]: if self._paths is None: raise ValueError('Material instance paths have not been determined. ' 'Call the Geometry.determine_paths() method.') @@ -217,15 +215,15 @@ def num_instances(self) -> int: return self._num_instances @property - def nuclides(self) -> List[namedtuple]: + def nuclides(self) -> list[namedtuple]: return self._nuclides @property - def isotropic(self) -> List[str]: + def isotropic(self) -> list[str]: return self._isotropic @isotropic.setter - def isotropic(self, isotropic: typing.Iterable[str]): + def isotropic(self, isotropic: Iterable[str]): cv.check_iterable_type('Isotropic scattering nuclides', isotropic, str) self._isotropic = list(isotropic) @@ -248,7 +246,7 @@ def average_molar_mass(self) -> float: return mass / moles @property - def volume(self) -> Optional[float]: + def volume(self) -> float | None: return self._volume @volume.setter @@ -258,7 +256,7 @@ def volume(self, volume: Real): self._volume = volume @property - def ncrystal_cfg(self) -> Optional[str]: + def ncrystal_cfg(self) -> str | None: return self._ncrystal_cfg @property @@ -274,7 +272,7 @@ def fissionable_mass(self) -> float: return density*self.volume @property - def decay_photon_energy(self) -> Optional[Univariate]: + def decay_photon_energy(self) -> Univariate | None: warnings.warn( "The 'decay_photon_energy' property has been replaced by the " "get_decay_photon_energy() method and will be removed in a future " @@ -285,8 +283,8 @@ def get_decay_photon_energy( self, clip_tolerance: float = 1e-6, units: str = 'Bq', - volume: Optional[float] = None - ) -> Optional[Univariate]: + volume: float | None = None + ) -> Univariate | None: r"""Return energy distribution of decay photons from unstable nuclides. .. versionadded:: 0.14.0 @@ -471,7 +469,7 @@ def add_volume_information(self, volume_calc): else: raise ValueError(f'No volume information found for material ID={self.id}.') - def set_density(self, units: str, density: Optional[float] = None): + def set_density(self, units: str, density: float | None = None): """Set the density of the material Parameters @@ -685,10 +683,10 @@ def remove_macroscopic(self, macroscopic: str): self._macroscopic = None def add_element(self, element: str, percent: float, percent_type: str = 'ao', - enrichment: Optional[float] = None, - enrichment_target: Optional[str] = None, - enrichment_type: Optional[str] = None, - cross_sections: Optional[str] = None): + enrichment: float | None = None, + enrichment_target: str | None = None, + enrichment_type: str | None = None, + cross_sections: str | None = None): """Add a natural element to the material Parameters @@ -799,9 +797,9 @@ def add_element(self, element: str, percent: float, percent_type: str = 'ao', self.add_nuclide(*nuclide) def add_elements_from_formula(self, formula: str, percent_type: str = 'ao', - enrichment: Optional[float] = None, - enrichment_target: Optional[str] = None, - enrichment_type: Optional[str] = None): + enrichment: float | None = None, + enrichment_target: str | None = None, + enrichment_type: str | None = None): """Add a elements from a chemical formula to the material. .. versionadded:: 0.12 @@ -930,7 +928,7 @@ def add_s_alpha_beta(self, name: str, fraction: float = 1.0): def make_isotropic_in_lab(self): self.isotropic = [x.name for x in self._nuclides] - def get_elements(self) -> List[str]: + def get_elements(self) -> list[str]: """Returns all elements in the material .. versionadded:: 0.12 @@ -944,7 +942,7 @@ def get_elements(self) -> List[str]: return sorted({re.split(r'(\d+)', i)[0] for i in self.get_nuclides()}) - def get_nuclides(self, element: Optional[str] = None) -> List[str]: + def get_nuclides(self, element: str | None = None) -> list[str]: """Returns a list of all nuclides in the material, if the element argument is specified then just nuclides of that element are returned. @@ -974,7 +972,7 @@ def get_nuclides(self, element: Optional[str] = None) -> List[str]: return matching_nuclides - def get_nuclide_densities(self) -> Dict[str, tuple]: + def get_nuclide_densities(self) -> dict[str, tuple]: """Returns all nuclides in the material and their densities Returns @@ -992,7 +990,7 @@ def get_nuclide_densities(self) -> Dict[str, tuple]: return nuclides - def get_nuclide_atom_densities(self, nuclide: Optional[str] = None) -> Dict[str, float]: + def get_nuclide_atom_densities(self, nuclide: str | None = None) -> dict[str, float]: """Returns one or all nuclides in the material and their atomic densities in units of atom/b-cm @@ -1078,7 +1076,7 @@ def get_nuclide_atom_densities(self, nuclide: Optional[str] = None) -> Dict[str, return nuclides def get_activity(self, units: str = 'Bq/cm3', by_nuclide: bool = False, - volume: Optional[float] = None) -> Union[Dict[str, float], float]: + volume: float | None = None) -> dict[str, float] | float: """Returns the activity of the material or for each nuclide in the material in units of [Bq], [Bq/g] or [Bq/cm3]. @@ -1101,7 +1099,7 @@ def get_activity(self, units: str = 'Bq/cm3', by_nuclide: bool = False, Returns ------- - typing.Union[dict, float] + Union[dict, float] If by_nuclide is True then a dictionary whose keys are nuclide names and values are activity is returned. Otherwise the activity of the material is returned as a float. @@ -1125,7 +1123,7 @@ def get_activity(self, units: str = 'Bq/cm3', by_nuclide: bool = False, return activity if by_nuclide else sum(activity.values()) def get_decay_heat(self, units: str = 'W', by_nuclide: bool = False, - volume: Optional[float] = None) -> Union[Dict[str, float], float]: + volume: float | None = None) -> dict[str, float] | float: """Returns the decay heat of the material or for each nuclide in the material in units of [W], [W/g] or [W/cm3]. @@ -1173,7 +1171,7 @@ def get_decay_heat(self, units: str = 'W', by_nuclide: bool = False, return decayheat if by_nuclide else sum(decayheat.values()) - def get_nuclide_atoms(self, volume: Optional[float] = None) -> Dict[str, float]: + def get_nuclide_atoms(self, volume: float | None = None) -> dict[str, float]: """Return number of atoms of each nuclide in the material .. versionadded:: 0.13.1 @@ -1202,7 +1200,7 @@ def get_nuclide_atoms(self, volume: Optional[float] = None) -> Dict[str, float]: atoms[nuclide] = 1.0e24 * atom_per_bcm * volume return atoms - def get_mass_density(self, nuclide: Optional[str] = None) -> float: + def get_mass_density(self, nuclide: str | None = None) -> float: """Return mass density of one or all nuclides Parameters @@ -1224,7 +1222,7 @@ def get_mass_density(self, nuclide: Optional[str] = None) -> float: mass_density += density_i return mass_density - def get_mass(self, nuclide: Optional[str] = None, volume: Optional[float] = None) -> float: + def get_mass(self, nuclide: str | None = None, volume: float | None = None) -> float: """Return mass of one or all nuclides. Note that this method requires that the :attr:`Material.volume` has @@ -1254,7 +1252,7 @@ def get_mass(self, nuclide: Optional[str] = None, volume: Optional[float] = None raise ValueError("Volume must be set in order to determine mass.") return volume*self.get_mass_density(nuclide) - def clone(self, memo: Optional[dict] = None) -> Material: + def clone(self, memo: dict | None = None) -> Material: """Create a copy of this material with a new unique ID. Parameters @@ -1311,8 +1309,8 @@ def _get_macroscopic_xml(self, macroscopic: str) -> ET.Element: return xml_element def _get_nuclides_xml( - self, nuclides: typing.Iterable[NuclideTuple], - nuclides_to_ignore: Optional[typing.Iterable[str]] = None)-> List[ET.Element]: + self, nuclides: Iterable[NuclideTuple], + nuclides_to_ignore: Iterable[str] | None = None)-> list[ET.Element]: xml_elements = [] # Remove any nuclides to ignore from the XML export @@ -1324,7 +1322,7 @@ def _get_nuclides_xml( return xml_elements def to_xml_element( - self, nuclides_to_ignore: Optional[typing.Iterable[str]] = None) -> ET.Element: + self, nuclides_to_ignore: Iterable[str] | None = None) -> ET.Element: """Return XML representation of the material Parameters @@ -1398,8 +1396,8 @@ def to_xml_element( return element @classmethod - def mix_materials(cls, materials, fracs: typing.Iterable[float], - percent_type: str = 'ao', name: Optional[str] = None) -> Material: + def mix_materials(cls, materials, fracs: Iterable[float], + percent_type: str = 'ao', name: str | None = None) -> Material: """Mix materials together based on atom, weight, or volume fractions .. versionadded:: 0.12 @@ -1596,7 +1594,7 @@ def __init__(self, materials=None): self += materials @property - def cross_sections(self) -> Optional[Path]: + def cross_sections(self) -> Path | None: return self._cross_sections @cross_sections.setter @@ -1686,7 +1684,7 @@ def _write_xml(self, file, header=True, level=0, spaces_per_level=2, file.write(indentation) def export_to_xml(self, path: PathLike = 'materials.xml', - nuclides_to_ignore: Optional[typing.Iterable[str]] = None): + nuclides_to_ignore: Iterable[str] | None = None): """Export material collection to an XML file. Parameters diff --git a/openmc/mesh.py b/openmc/mesh.py index ee58ec3b8c3..85af6a009c9 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -1,14 +1,12 @@ from __future__ import annotations -import typing import warnings from abc import ABC, abstractmethod -from collections.abc import Iterable +from collections.abc import Iterable, Sequence from functools import wraps from math import pi, sqrt, atan2 from numbers import Integral, Real from pathlib import Path import tempfile -from typing import Optional, Sequence, Tuple, List import h5py import lxml.etree as ET @@ -49,7 +47,7 @@ class MeshBase(IDManagerMixin, ABC): next_id = 1 used_ids = set() - def __init__(self, mesh_id: Optional[int] = None, name: str = ''): + def __init__(self, mesh_id: int | None = None, name: str = ''): # Initialize Mesh class attributes self.id = mesh_id self.name = name @@ -151,10 +149,10 @@ def get_homogenized_materials( self, model: openmc.Model, n_samples: int = 10_000, - prn_seed: Optional[int] = None, + prn_seed: int | None = None, include_void: bool = True, **kwargs - ) -> List[openmc.Material]: + ) -> list[openmc.Material]: """Generate homogenized materials over each element in a mesh. .. versionadded:: 0.15.0 @@ -393,7 +391,7 @@ def num_mesh_cells(self): def write_data_to_vtk(self, filename: PathLike, - datasets: Optional[dict] = None, + datasets: dict | None = None, volume_normalization: bool = True, curvilinear: bool = False): """Creates a VTK object of the mesh @@ -642,7 +640,7 @@ class RegularMesh(StructuredMesh): """ - def __init__(self, mesh_id: Optional[int] = None, name: str = ''): + def __init__(self, mesh_id: int | None = None, name: str = ''): super().__init__(mesh_id, name) self._dimension = None @@ -655,7 +653,7 @@ def dimension(self): return tuple(self._dimension) @dimension.setter - def dimension(self, dimension: typing.Iterable[int]): + def dimension(self, dimension: Iterable[int]): cv.check_type('mesh dimension', dimension, Iterable, Integral) cv.check_length('mesh dimension', dimension, 1, 3) self._dimension = dimension @@ -672,7 +670,7 @@ def lower_left(self): return self._lower_left @lower_left.setter - def lower_left(self, lower_left: typing.Iterable[Real]): + def lower_left(self, lower_left: Iterable[Real]): cv.check_type('mesh lower_left', lower_left, Iterable, Real) cv.check_length('mesh lower_left', lower_left, 1, 3) self._lower_left = lower_left @@ -692,7 +690,7 @@ def upper_right(self): return [l + w * d for l, w, d in zip(ls, ws, dims)] @upper_right.setter - def upper_right(self, upper_right: typing.Iterable[Real]): + def upper_right(self, upper_right: Iterable[Real]): cv.check_type('mesh upper_right', upper_right, Iterable, Real) cv.check_length('mesh upper_right', upper_right, 1, 3) self._upper_right = upper_right @@ -716,7 +714,7 @@ def width(self): return [(u - l) / d for u, l, d in zip(us, ls, dims)] @width.setter - def width(self, width: typing.Iterable[Real]): + def width(self, width: Iterable[Real]): cv.check_type('mesh width', width, Iterable, Real) cv.check_length('mesh width', width, 1, 3) self._width = width @@ -815,7 +813,7 @@ def from_rect_lattice( cls, lattice: 'openmc.RectLattice', division: int = 1, - mesh_id: Optional[int] = None, + mesh_id: int | None = None, name: str = '' ): """Create mesh from an existing rectangular lattice @@ -853,9 +851,9 @@ def from_rect_lattice( @classmethod def from_domain( cls, - domain: typing.Union['openmc.Cell', 'openmc.Region', 'openmc.Universe', 'openmc.Geometry'], + domain: 'openmc.Cell' | 'openmc.Region' | 'openmc.Universe' | 'openmc.Geometry', dimension: Sequence[int] = (10, 10, 10), - mesh_id: Optional[int] = None, + mesh_id: int | None = None, name: str = '' ): """Create mesh from an existing openmc cell, region, universe or @@ -962,7 +960,7 @@ def from_xml_element(cls, elem: ET.Element): return mesh - def build_cells(self, bc: Optional[str] = None): + def build_cells(self, bc: str | None = None): """Generates a lattice of universes with the same dimensionality as the mesh object. The individual cells/universes produced will not have material definitions applied and so downstream code @@ -1363,7 +1361,7 @@ def __init__( z_grid: Sequence[float], phi_grid: Sequence[float] = (0, 2*pi), origin: Sequence[float] = (0., 0., 0.), - mesh_id: Optional[int] = None, + mesh_id: int | None = None, name: str = '', ): super().__init__(mesh_id, name) @@ -1484,7 +1482,7 @@ def __repr__(self): def get_indices_at_coords( self, coords: Sequence[float] - ) -> Tuple[int, int, int]: + ) -> tuple[int, int, int]: """Finds the index of the mesh voxel at the specified x,y,z coordinates. .. versionadded:: 0.15.0 @@ -1496,7 +1494,7 @@ def get_indices_at_coords( Returns ------- - Tuple[int, int, int] + tuple[int, int, int] The r, phi, z indices """ @@ -1562,9 +1560,9 @@ def from_hdf5(cls, group: h5py.Group): @classmethod def from_domain( cls, - domain: typing.Union['openmc.Cell', 'openmc.Region', 'openmc.Universe', 'openmc.Geometry'], + domain: 'openmc.Cell' | 'openmc.Region' | 'openmc.Universe' | 'openmc.Geometry', dimension: Sequence[int] = (10, 10, 10), - mesh_id: Optional[int] = None, + mesh_id: int | None = None, phi_grid_bounds: Sequence[float] = (0.0, 2*pi), name: str = '' ): @@ -1813,7 +1811,7 @@ def __init__( phi_grid: Sequence[float] = (0, 2*pi), theta_grid: Sequence[float] = (0, pi), origin: Sequence[float] = (0., 0., 0.), - mesh_id: Optional[int] = None, + mesh_id: int | None = None, name: str = '', ): super().__init__(mesh_id, name) @@ -2139,9 +2137,9 @@ class UnstructuredMesh(MeshBase): _LINEAR_TET = 0 _LINEAR_HEX = 1 - def __init__(self, filename: PathLike, library: str, mesh_id: Optional[int] = None, + def __init__(self, filename: PathLike, library: str, mesh_id: int | None = None, name: str = '', length_multiplier: float = 1.0, - options: Optional[str] = None): + options: str | None = None): super().__init__(mesh_id, name) self.filename = filename self._volumes = None @@ -2173,11 +2171,11 @@ def library(self, lib: str): self._library = lib @property - def options(self) -> Optional[str]: + def options(self) -> str | None: return self._options @options.setter - def options(self, options: Optional[str]): + def options(self, options: str | None): cv.check_type('options', options, (str, type(None))) self._options = options @@ -2215,7 +2213,7 @@ def volumes(self): return self._volumes @volumes.setter - def volumes(self, volumes: typing.Iterable[Real]): + def volumes(self, volumes: Iterable[Real]): cv.check_type("Unstructured mesh volumes", volumes, Iterable, Real) self._volumes = volumes @@ -2353,8 +2351,8 @@ def write_vtk_mesh(self, **kwargs): def write_data_to_vtk( self, - filename: Optional[PathLike] = None, - datasets: Optional[dict] = None, + filename: PathLike | None = None, + datasets: dict | None = None, volume_normalization: bool = True ): """Map data to unstructured VTK mesh elements. diff --git a/openmc/model/model.py b/openmc/model/model.py index 2160c97e6cc..2ea579ab7df 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -6,7 +6,6 @@ from numbers import Integral from tempfile import NamedTemporaryFile import warnings -from typing import Optional, Dict import h5py import lxml.etree as ET @@ -83,7 +82,7 @@ def __init__(self, geometry=None, materials=None, settings=None, self.plots = plots @property - def geometry(self) -> Optional[openmc.Geometry]: + def geometry(self) -> openmc.Geometry | None: return self._geometry @geometry.setter @@ -92,7 +91,7 @@ def geometry(self, geometry): self._geometry = geometry @property - def materials(self) -> Optional[openmc.Materials]: + def materials(self) -> openmc.Materials | None: return self._materials @materials.setter @@ -106,7 +105,7 @@ def materials(self, materials): self._materials.append(mat) @property - def settings(self) -> Optional[openmc.Settings]: + def settings(self) -> openmc.Settings | None: return self._settings @settings.setter @@ -115,7 +114,7 @@ def settings(self, settings): self._settings = settings @property - def tallies(self) -> Optional[openmc.Tallies]: + def tallies(self) -> openmc.Tallies | None: return self._tallies @tallies.setter @@ -129,7 +128,7 @@ def tallies(self, tallies): self._tallies.append(tally) @property - def plots(self) -> Optional[openmc.Plots]: + def plots(self) -> openmc.Plots | None: return self._plots @plots.setter @@ -169,7 +168,7 @@ def _cells_by_id(self) -> dict: @property @lru_cache(maxsize=None) - def _cells_by_name(self) -> Dict[int, openmc.Cell]: + def _cells_by_name(self) -> dict[int, openmc.Cell]: # Get the names maps, but since names are not unique, store a set for # each name key. In this way when the user requests a change by a name, # the change will be applied to all of the same name. @@ -182,7 +181,7 @@ def _cells_by_name(self) -> Dict[int, openmc.Cell]: @property @lru_cache(maxsize=None) - def _materials_by_name(self) -> Dict[int, openmc.Material]: + def _materials_by_name(self) -> dict[int, openmc.Material]: if self.materials is None: mats = self.geometry.get_all_materials().values() else: diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index 2945f73b71e..29411fe4608 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -1,12 +1,11 @@ from abc import ABC, abstractmethod -from collections.abc import Iterable +from collections.abc import Iterable, Sequence from copy import copy from functools import partial from math import sqrt, pi, sin, cos, isclose from numbers import Real import warnings import operator -from typing import Sequence import numpy as np from scipy.spatial import ConvexHull, Delaunay diff --git a/openmc/plots.py b/openmc/plots.py index 0d9dca30e79..7532d9d5cb1 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -1,7 +1,6 @@ from collections.abc import Iterable, Mapping from numbers import Integral, Real from pathlib import Path -from typing import Optional import h5py import lxml.etree as ET @@ -942,7 +941,7 @@ def to_ipython_image(self, openmc_exec='openmc', cwd='.'): # Return produced image return _get_plot_image(self, cwd) - def to_vtk(self, output: Optional[PathLike] = None, + def to_vtk(self, output: PathLike | None = None, openmc_exec: str = 'openmc', cwd: str = '.'): """Render plot as an voxel image diff --git a/openmc/settings.py b/openmc/settings.py index 8076625818a..ce97f138f24 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -4,8 +4,6 @@ from math import ceil from numbers import Integral, Real from pathlib import Path -import typing # required to prevent typing.Union namespace overwriting Union -from typing import Optional import lxml.etree as ET @@ -514,7 +512,7 @@ def max_order(self) -> int: return self._max_order @max_order.setter - def max_order(self, max_order: Optional[int]): + def max_order(self, max_order: int | None): if max_order is not None: cv.check_type('maximum scattering order', max_order, Integral) cv.check_greater_than('maximum scattering order', max_order, 0, @@ -522,11 +520,11 @@ def max_order(self, max_order: Optional[int]): self._max_order = max_order @property - def source(self) -> typing.List[SourceBase]: + def source(self) -> list[SourceBase]: return self._source @source.setter - def source(self, source: typing.Union[SourceBase, typing.Iterable[SourceBase]]): + def source(self, source: SourceBase | Iterable[SourceBase]): if not isinstance(source, MutableSequence): source = [source] self._source = cv.CheckedList(SourceBase, 'source distributions', source) @@ -804,7 +802,7 @@ def temperature(self, temperature: dict): self._temperature = temperature @property - def trace(self) -> typing.Iterable: + def trace(self) -> Iterable: return self._trace @trace.setter @@ -817,11 +815,11 @@ def trace(self, trace: Iterable): self._trace = trace @property - def track(self) -> typing.Iterable[typing.Iterable[int]]: + def track(self) -> Iterable[Iterable[int]]: return self._track @track.setter - def track(self, track: typing.Iterable[typing.Iterable[int]]): + def track(self, track: Iterable[Iterable[int]]): cv.check_type('track', track, Sequence) for t in track: if len(t) != 3: @@ -904,12 +902,12 @@ def resonance_scattering(self, res: dict): self._resonance_scattering = res @property - def volume_calculations(self) -> typing.List[VolumeCalculation]: + def volume_calculations(self) -> list[VolumeCalculation]: return self._volume_calculations @volume_calculations.setter def volume_calculations( - self, vol_calcs: typing.Union[VolumeCalculation, typing.Iterable[VolumeCalculation]] + self, vol_calcs: VolumeCalculation | Iterable[VolumeCalculation] ): if not isinstance(vol_calcs, MutableSequence): vol_calcs = [vol_calcs] @@ -1003,11 +1001,11 @@ def write_initial_source(self, value: bool): self._write_initial_source = value @property - def weight_windows(self) -> typing.List[WeightWindows]: + def weight_windows(self) -> list[WeightWindows]: return self._weight_windows @weight_windows.setter - def weight_windows(self, value: typing.Union[WeightWindows, typing.Iterable[WeightWindows]]): + def weight_windows(self, value: WeightWindows | Iterable[WeightWindows]): if not isinstance(value, MutableSequence): value = [value] self._weight_windows = cv.CheckedList(WeightWindows, 'weight windows', value) @@ -1056,7 +1054,7 @@ def max_tracks(self, value: int): self._max_tracks = value @property - def weight_windows_file(self) -> Optional[PathLike]: + def weight_windows_file(self) -> PathLike | None: return self._weight_windows_file @weight_windows_file.setter @@ -1065,7 +1063,7 @@ def weight_windows_file(self, value: PathLike): self._weight_windows_file = value @property - def weight_window_generators(self) -> typing.List[WeightWindowGenerator]: + def weight_window_generators(self) -> list[WeightWindowGenerator]: return self._weight_window_generators @weight_window_generators.setter diff --git a/openmc/source.py b/openmc/source.py index 51ed74de3c6..e35e62f5fca 100644 --- a/openmc/source.py +++ b/openmc/source.py @@ -1,12 +1,10 @@ from __future__ import annotations from abc import ABC, abstractmethod -from collections.abc import Iterable +from collections.abc import Iterable, Sequence from enum import IntEnum from numbers import Real import warnings -import typing # imported separately as py3.8 requires typing.Iterable -# also required to prevent typing.Union namespace overwriting Union -from typing import Optional, Sequence, Dict, Any +from typing import Any import lxml.etree as ET import numpy as np @@ -57,8 +55,8 @@ class SourceBase(ABC): def __init__( self, - strength: Optional[float] = 1.0, - constraints: Optional[Dict[str, Any]] = None + strength: float | None = 1.0, + constraints: dict[str, Any] | None = None ): self.strength = strength self.constraints = constraints @@ -75,11 +73,11 @@ def strength(self, strength): self._strength = strength @property - def constraints(self) -> Dict[str, Any]: + def constraints(self) -> dict[str, Any]: return self._constraints @constraints.setter - def constraints(self, constraints: Optional[Dict[str, Any]]): + def constraints(self, constraints: dict[str, Any] | None): self._constraints = {} if constraints is None: return @@ -200,7 +198,7 @@ def from_xml_element(cls, elem: ET.Element, meshes=None) -> SourceBase: raise ValueError(f'Source type {source_type} is not recognized') @staticmethod - def _get_constraints(elem: ET.Element) -> Dict[str, Any]: + def _get_constraints(elem: ET.Element) -> dict[str, Any]: # Find element containing constraints constraints_elem = elem.find("constraints") elem = constraints_elem if constraints_elem is not None else elem @@ -308,14 +306,14 @@ class IndependentSource(SourceBase): def __init__( self, - space: Optional[openmc.stats.Spatial] = None, - angle: Optional[openmc.stats.UnitSphere] = None, - energy: Optional[openmc.stats.Univariate] = None, - time: Optional[openmc.stats.Univariate] = None, + space: openmc.stats.Spatial | None = None, + angle: openmc.stats.UnitSphere | None = None, + energy: openmc.stats.Univariate | None = None, + time: openmc.stats.Univariate | None = None, strength: float = 1.0, particle: str = 'neutron', - domains: Optional[Sequence[typing.Union[openmc.Cell, openmc.Material, openmc.Universe]]] = None, - constraints: Optional[Dict[str, Any]] = None + domains: Sequence[openmc.Cell | openmc.Material | openmc.Universe] | None = None, + constraints: dict[str, Any] | None = None ): if domains is not None: warnings.warn("The 'domains' arguments has been replaced by the " @@ -528,7 +526,7 @@ def __init__( self, mesh: MeshBase, sources: Sequence[SourceBase], - constraints: Optional[Dict[str, Any]] = None, + constraints: dict[str, Any] | None = None, ): super().__init__(strength=None, constraints=constraints) self.mesh = mesh @@ -702,10 +700,10 @@ class CompiledSource(SourceBase): """ def __init__( self, - library: Optional[str] = None, - parameters: Optional[str] = None, + library: str | None = None, + parameters: str | None = None, strength: float = 1.0, - constraints: Optional[Dict[str, Any]] = None + constraints: dict[str, Any] | None = None ) -> None: super().__init__(strength=strength, constraints=constraints) @@ -829,9 +827,9 @@ class FileSource(SourceBase): def __init__( self, - path: Optional[PathLike] = None, + path: PathLike | None = None, strength: float = 1.0, - constraints: Optional[Dict[str, Any]] = None + constraints: dict[str, Any] | None = None ): super().__init__(strength=strength, constraints=constraints) self._path = None @@ -966,8 +964,8 @@ class SourceParticle: """ def __init__( self, - r: typing.Iterable[float] = (0., 0., 0.), - u: typing.Iterable[float] = (0., 0., 1.), + r: Iterable[float] = (0., 0., 0.), + u: Iterable[float] = (0., 0., 1.), E: float = 1.0e6, time: float = 0.0, wgt: float = 1.0, @@ -1003,7 +1001,7 @@ def to_tuple(self) -> tuple: def write_source_file( - source_particles: typing.Iterable[SourceParticle], + source_particles: Iterable[SourceParticle], filename: PathLike, **kwargs ): """Write a source file using a collection of source particles @@ -1046,7 +1044,7 @@ def write_source_file( fh.create_dataset('source_bank', data=arr, dtype=source_dtype) -def read_source_file(filename: PathLike) -> typing.List[SourceParticle]: +def read_source_file(filename: PathLike) -> list[SourceParticle]: """Read a source file and return a list of source particles. .. versionadded:: 0.15.0 diff --git a/openmc/stats/multivariate.py b/openmc/stats/multivariate.py index ce9740ad212..06c65896f49 100644 --- a/openmc/stats/multivariate.py +++ b/openmc/stats/multivariate.py @@ -1,7 +1,6 @@ from __future__ import annotations -import typing from abc import ABC, abstractmethod -from collections.abc import Iterable +from collections.abc import Iterable, Sequence from math import cos, pi from numbers import Real from warnings import warn @@ -9,6 +8,7 @@ import lxml.etree as ET import numpy as np +import openmc import openmc.checkvalue as cv from .._xml import get_text from ..mesh import MeshBase @@ -212,7 +212,7 @@ class Monodirectional(UnitSphere): """ - def __init__(self, reference_uvw: typing.Sequence[float] = [1., 0., 0.]): + def __init__(self, reference_uvw: Sequence[float] = [1., 0., 0.]): super().__init__(reference_uvw) def to_xml_element(self): @@ -789,8 +789,8 @@ class Box(Spatial): def __init__( self, - lower_left: typing.Sequence[float], - upper_right: typing.Sequence[float], + lower_left: Sequence[float], + upper_right: Sequence[float], only_fissionable: bool = False ): self.lower_left = lower_left @@ -889,7 +889,7 @@ class Point(Spatial): """ - def __init__(self, xyz: typing.Sequence[float] = (0., 0., 0.)): + def __init__(self, xyz: Sequence[float] = (0., 0., 0.)): self.xyz = xyz @property @@ -939,9 +939,9 @@ def from_xml_element(cls, elem: ET.Element): def spherical_uniform( r_outer: float, r_inner: float = 0.0, - thetas: typing.Sequence[float] = (0., pi), - phis: typing.Sequence[float] = (0., 2*pi), - origin: typing.Sequence[float] = (0., 0., 0.) + thetas: Sequence[float] = (0., pi), + phis: Sequence[float] = (0., 2*pi), + origin: Sequence[float] = (0., 0., 0.) ): """Return a uniform spatial distribution over a spherical shell. diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index 4069c5a9e11..2d93b1f1b5d 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -1,9 +1,8 @@ from __future__ import annotations import math -import typing from abc import ABC, abstractmethod from collections import defaultdict -from collections.abc import Iterable +from collections.abc import Iterable, Sequence from copy import deepcopy from numbers import Real from warnings import warn @@ -68,7 +67,7 @@ def from_xml_element(cls, elem): return Mixture.from_xml_element(elem) @abstractmethod - def sample(n_samples: int = 1, seed: typing.Optional[int] = None): + def sample(n_samples: int = 1, seed: int | None = None): """Sample the univariate distribution Parameters @@ -210,8 +209,8 @@ def from_xml_element(cls, elem: ET.Element): @classmethod def merge( cls, - dists: typing.Sequence[Discrete], - probs: typing.Sequence[int] + dists: Sequence[Discrete], + probs: Sequence[int] ): """Merge multiple discrete distributions into a single distribution @@ -859,8 +858,8 @@ class Tabular(Univariate): def __init__( self, - x: typing.Sequence[float], - p: typing.Sequence[float], + x: Sequence[float], + p: Sequence[float], interpolation: str = 'linear-linear', ignore_negative: bool = False ): @@ -958,7 +957,7 @@ def normalize(self): """Normalize the probabilities stored on the distribution""" self._p /= self.cdf().max() - def sample(self, n_samples: int = 1, seed: typing.Optional[int] = None): + def sample(self, n_samples: int = 1, seed: int | None = None): rng = np.random.RandomState(seed) xi = rng.random(n_samples) @@ -1100,7 +1099,7 @@ class Legendre(Univariate): """ - def __init__(self, coefficients: typing.Sequence[float]): + def __init__(self, coefficients: Sequence[float]): self.coefficients = coefficients self._legendre_poly = None @@ -1156,8 +1155,8 @@ class Mixture(Univariate): def __init__( self, - probability: typing.Sequence[float], - distribution: typing.Sequence[Univariate] + probability: Sequence[float], + distribution: Sequence[Univariate] ): self.probability = probability self.distribution = distribution @@ -1319,8 +1318,8 @@ def clip(self, tolerance: float = 1e-6, inplace: bool = False) -> Mixture: def combine_distributions( - dists: typing.Sequence[Univariate], - probs: typing.Sequence[float] + dists: Sequence[Univariate], + probs: Sequence[float] ): """Combine distributions with specified probabilities diff --git a/openmc/utility_funcs.py b/openmc/utility_funcs.py index 4eb307c9303..3dff45380c1 100644 --- a/openmc/utility_funcs.py +++ b/openmc/utility_funcs.py @@ -2,12 +2,11 @@ import os from pathlib import Path from tempfile import TemporaryDirectory -from typing import Optional from .checkvalue import PathLike @contextmanager -def change_directory(working_dir: Optional[PathLike] = None, *, tmpdir: bool = False): +def change_directory(working_dir: PathLike | None = None, *, tmpdir: bool = False): """Context manager for executing in a provided working directory Parameters diff --git a/openmc/weight_windows.py b/openmc/weight_windows.py index 96f1db89282..a10fd2a6510 100644 --- a/openmc/weight_windows.py +++ b/openmc/weight_windows.py @@ -1,6 +1,6 @@ from __future__ import annotations from numbers import Real, Integral -from typing import Iterable, List, Optional, Dict, Sequence +from collections.abc import Iterable, Sequence import warnings import lxml.etree as ET @@ -110,15 +110,15 @@ def __init__( self, mesh: MeshBase, lower_ww_bounds: Iterable[float], - upper_ww_bounds: Optional[Iterable[float]] = None, - upper_bound_ratio: Optional[float] = None, - energy_bounds: Optional[Iterable[Real]] = None, + upper_ww_bounds: Iterable[float] | None = None, + upper_bound_ratio: float | None = None, + energy_bounds: Iterable[Real] | None = None, particle_type: str = 'neutron', survival_ratio: float = 3, - max_lower_bound_ratio: Optional[float] = None, + max_lower_bound_ratio: float | None = None, max_split: int = 10, weight_cutoff: float = 1.e-38, - id: Optional[int] = None + id: int | None = None ): self.mesh = mesh self.id = id @@ -353,7 +353,7 @@ def to_xml_element(self) -> ET.Element: return element @classmethod - def from_xml_element(cls, elem: ET.Element, meshes: Dict[int, MeshBase]) -> WeightWindows: + def from_xml_element(cls, elem: ET.Element, meshes: dict[int, MeshBase]) -> WeightWindows: """Generate weight window settings from an XML element Parameters @@ -407,7 +407,7 @@ def from_xml_element(cls, elem: ET.Element, meshes: Dict[int, MeshBase]) -> Weig ) @classmethod - def from_hdf5(cls, group: h5py.Group, meshes: Dict[int, MeshBase]) -> WeightWindows: + def from_hdf5(cls, group: h5py.Group, meshes: dict[int, MeshBase]) -> WeightWindows: """Create weight windows from HDF5 group Parameters @@ -457,7 +457,7 @@ def from_hdf5(cls, group: h5py.Group, meshes: Dict[int, MeshBase]) -> WeightWind ) -def wwinp_to_wws(path: PathLike) -> List[WeightWindows]: +def wwinp_to_wws(path: PathLike) -> list[WeightWindows]: """Create WeightWindows instances from a wwinp file .. versionadded:: 0.13.1 @@ -698,7 +698,7 @@ class WeightWindowGenerator: def __init__( self, mesh: openmc.MeshBase, - energy_bounds: Optional[Sequence[float]] = None, + energy_bounds: Sequence[float] | None = None, particle_type: str = 'neutron', method: str = 'magic', max_realizations: int = 1, From e5cc925db3ce5685cfd5afeb3822050ff8720824 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Wed, 24 Jul 2024 03:42:03 +0100 Subject: [PATCH 042/184] removed unused which function in CI scripts (#3095) --- tools/ci/gha-install.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/tools/ci/gha-install.py b/tools/ci/gha-install.py index f4b2fbb3187..f046e863470 100644 --- a/tools/ci/gha-install.py +++ b/tools/ci/gha-install.py @@ -2,22 +2,6 @@ import shutil import subprocess -def which(program): - def is_exe(fpath): - return os.path.isfile(fpath) and os.access(fpath, os.X_OK) - - fpath, fname = os.path.split(program) - if fpath: - if is_exe(program): - return program - else: - for path in os.environ["PATH"].split(os.pathsep): - path = path.strip('"') - exe_file = os.path.join(path, program) - if is_exe(exe_file): - return exe_file - return None - def install(omp=False, mpi=False, phdf5=False, dagmc=False, libmesh=False, ncrystal=False): # Create build directory and change to it From 0ad003307a67e5eb6c161fed2f3c6b571649de79 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Wed, 24 Jul 2024 13:27:39 +0100 Subject: [PATCH 043/184] packages used for testing moved to tests section of pyprojects.tom (#3094) --- pyproject.toml | 1 + tools/ci/gha-install.sh | 8 +------- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d1b2c335cd7..2b2764cea69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,7 @@ docs = [ "sphinx-rtd-theme==1.0.0" ] test = ["packaging", "pytest", "pytest-cov", "colorama", "openpyxl"] +ci = ["cpp-coveralls", "coveralls"] vtk = ["vtk"] [project.urls] diff --git a/tools/ci/gha-install.sh b/tools/ci/gha-install.sh index 2cef974d346..87952fda9cb 100755 --- a/tools/ci/gha-install.sh +++ b/tools/ci/gha-install.sh @@ -48,10 +48,4 @@ fi python tools/ci/gha-install.py # Install Python API in editable mode -pip install -e .[test,vtk] - -# For coverage testing of the C++ source files -pip install cpp-coveralls - -# For coverage testing of the Python source files -pip install coveralls +pip install -e .[test,vtk,ci] From 467b8e99abc62d5361f628ea37e1bcd4afcf3245 Mon Sep 17 00:00:00 2001 From: John Vincent Cauilan <64677361+johvincau@users.noreply.github.com> Date: Tue, 30 Jul 2024 07:27:35 -0500 Subject: [PATCH 044/184] Fix ParticleFilter to work with set inputs (#3092) --- openmc/filter.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openmc/filter.py b/openmc/filter.py index e005140eeb3..0f884cfcd16 100644 --- a/openmc/filter.py +++ b/openmc/filter.py @@ -1,6 +1,6 @@ from __future__ import annotations from abc import ABCMeta -from collections.abc import Iterable +from collections.abc import Iterable, Sequence import hashlib from itertools import product from numbers import Real, Integral @@ -736,7 +736,7 @@ class ParticleFilter(Filter): Parameters ---------- - bins : str, or iterable of str + bins : str, or sequence of str The particles to tally represented as strings ('neutron', 'photon', 'electron', 'positron'). filter_id : int @@ -744,7 +744,7 @@ class ParticleFilter(Filter): Attributes ---------- - bins : iterable of str + bins : sequence of str The particles to tally id : int Unique identifier for the filter @@ -764,8 +764,8 @@ def __eq__(self, other): @Filter.bins.setter def bins(self, bins): - bins = np.atleast_1d(bins) - cv.check_iterable_type('filter bins', bins, str) + cv.check_type('bins', bins, Sequence, str) + bins = np.atleast_1d(bins) for edge in bins: cv.check_value('filter bin', edge, _PARTICLES) self._bins = bins From 9d9b2daceba82354f4720315ee2491b11d1d6e23 Mon Sep 17 00:00:00 2001 From: "Juan M. Valderrama" <119371594+valderrama-juan@users.noreply.github.com> Date: Tue, 30 Jul 2024 14:47:00 -0500 Subject: [PATCH 045/184] Improve description of probabilities for openmc.stats.Tabular class (#3099) Co-authored-by: Paul Romano --- openmc/stats/univariate.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index 2d93b1f1b5d..a47f4f32030 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -837,7 +837,8 @@ class Tabular(Univariate): Tabulated values of the random variable p : Iterable of float Tabulated probabilities. For histogram interpolation, if the length of - `p` is the same as `x`, the last value is ignored. + `p` is the same as `x`, the last value is ignored. Probabilities `p` are + given per unit of `x`. interpolation : {'histogram', 'linear-linear', 'linear-log', 'log-linear', 'log-log'}, optional Indicates how the density function is interpolated between tabulated points. Defaults to 'linear-linear'. @@ -854,6 +855,15 @@ class Tabular(Univariate): Indicates how the density function is interpolated between tabulated points. Defaults to 'linear-linear'. + Notes + ----- + The probabilities `p` are interpreted per unit of the corresponding + independent variable `x`. This follows the definition of a probability + density function (PDF) in probability theory, where the PDF represents the + relative likelihood of the random variable taking on a particular value per + unit of the variable. For example, if `x` represents energy in eV, then `p` + should represent probabilities per eV. + """ def __init__( From 346f751deb61f7f3b373855d1f4b2e7a27ae8228 Mon Sep 17 00:00:00 2001 From: John Vincent Cauilan <64677361+johvincau@users.noreply.github.com> Date: Tue, 13 Aug 2024 16:51:30 -0500 Subject: [PATCH 046/184] Adjust decay data reader to better handle non-normalized branching ratios (#3080) Co-authored-by: Paul Romano --- openmc/deplete/chain.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index 1d383498052..2e81250dcfb 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -386,24 +386,31 @@ def from_endf(cls, decay_files, fpy_files, neutron_files, if not data.nuclide['stable'] and data.half_life.nominal_value != 0.0: nuclide.half_life = data.half_life.nominal_value nuclide.decay_energy = data.decay_energy.nominal_value - sum_br = 0.0 - for i, mode in enumerate(data.modes): + branch_ratios = [] + branch_ids = [] + for mode in data.modes: type_ = ','.join(mode.modes) if mode.daughter in decay_data: target = mode.daughter else: print('missing {} {} {}'.format( - parent, ','.join(mode.modes), mode.daughter)) + parent, type_, mode.daughter)) target = replace_missing(mode.daughter, decay_data) - - # Write branching ratio, taking care to ensure sum is unity br = mode.branching_ratio.nominal_value - sum_br += br - if i == len(data.modes) - 1 and sum_br != 1.0: - br = 1.0 - sum(m.branching_ratio.nominal_value - for m in data.modes[:-1]) + branch_ratios.append(br) + branch_ids.append((type_, target)) + + if not math.isclose(sum(branch_ratios), 1.0): + max_br = max(branch_ratios) + max_index = branch_ratios.index(max_br) + + # Adjust maximum branching ratio so they sum to unity + new_br = max_br - sum(branch_ratios) + 1.0 + branch_ratios[max_index] = new_br + assert math.isclose(sum(branch_ratios), 1.0) - # Append decay mode + # Append decay modes + for br, (type_, target) in zip(branch_ratios, branch_ids): nuclide.add_decay_mode(type_, target, br) nuclide.sources = data.sources From 3ef54ec349ac7a847df8c0b2de02c2bd2cf3155f Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Wed, 14 Aug 2024 05:43:44 +0100 Subject: [PATCH 047/184] Replacing endf c functions with package (#3101) --- openmc/data/_endf.pyx | 8 ------ openmc/data/endf.c | 57 ------------------------------------------- openmc/data/endf.py | 10 +------- pyproject.toml | 1 + setup.py | 2 +- 5 files changed, 3 insertions(+), 75 deletions(-) delete mode 100644 openmc/data/_endf.pyx delete mode 100644 openmc/data/endf.c diff --git a/openmc/data/_endf.pyx b/openmc/data/_endf.pyx deleted file mode 100644 index 991ee015b29..00000000000 --- a/openmc/data/_endf.pyx +++ /dev/null @@ -1,8 +0,0 @@ -# cython: c_string_type=str, c_string_encoding=ascii - -cdef extern from "endf.c": - double cfloat_endf(const char* buffer, int n) - -def float_endf(s): - cdef const char* c_string = s - return cfloat_endf(c_string, len(s)) diff --git a/openmc/data/endf.c b/openmc/data/endf.c deleted file mode 100644 index dd5eee54184..00000000000 --- a/openmc/data/endf.c +++ /dev/null @@ -1,57 +0,0 @@ -#include - -//! Convert string representation of a floating point number into a double -// -//! This function handles converting floating point numbers from an ENDF 11 -//! character field into a double, covering all the corner cases. Floating point -//! numbers are allowed to contain whitespace (which is ignored). Also, in -//! exponential notation, it allows the 'e' to be omitted. A field containing -//! only whitespace is to be interpreted as a zero. -// -//! \param buffer character input from an ENDF file -//! \param n Length of character input -//! \return Floating point number - -double cfloat_endf(const char* buffer, int n) -{ - char arr[13]; // 11 characters plus e and a null terminator - int j = 0; // current position in arr - int found_significand = 0; - int found_exponent = 0; - - // limit n to 11 characters - n = n > 11 ? 11 : n; - - int i; - for (i = 0; i < n; ++i) { - char c = buffer[i]; - - // Skip whitespace characters - if (c == ' ') continue; - - if (found_significand) { - if (!found_exponent) { - if (c == '+' || c == '-') { - // In the case that we encounter +/- and we haven't yet encountered - // e/E, we manually add it - arr[j++] = 'e'; - found_exponent = 1; - - } else if (c == 'e' || c == 'E' || c == 'd' || c == 'D') { - arr[j++] = 'e'; - found_exponent = 1; - continue; - } - } - } else if (c == '.' || (c >= '0' && c <= '9')) { - found_significand = 1; - } - - // Copy character - arr[j++] = c; - } - - // Done copying. Add null terminator and convert to double - arr[j] = '\0'; - return atof(arr); -} diff --git a/openmc/data/endf.py b/openmc/data/endf.py index 73299723a8b..63cd092ebb1 100644 --- a/openmc/data/endf.py +++ b/openmc/data/endf.py @@ -14,11 +14,7 @@ from .data import gnds_name from .function import Tabulated1D -try: - from ._endf import float_endf - _CYTHON = True -except ImportError: - _CYTHON = False +from endf.records import float_endf _LIBRARY = {0: 'ENDF/B', 1: 'ENDF/A', 2: 'JEFF', 3: 'EFF', @@ -91,10 +87,6 @@ def py_float_endf(s): return float(ENDF_FLOAT_RE.sub(r'\1e\2\3', s)) -if not _CYTHON: - float_endf = py_float_endf - - def int_endf(s): """Convert string of integer number in ENDF to int. diff --git a/pyproject.toml b/pyproject.toml index 2b2764cea69..98b1d152fbc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ dependencies = [ "lxml", "uncertainties", "setuptools", + "endf", ] [project.optional-dependencies] diff --git a/setup.py b/setup.py index 4e24f48a123..88a45a36090 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ kwargs = { - # Cython is used to add resonance reconstruction and fast float_endf + # Cython is used to add resonance reconstruction 'ext_modules': cythonize('openmc/data/*.pyx'), 'include_dirs': [np.get_include()] } From ae245e0fb7b4dc20df389d42a1b31f956e05f024 Mon Sep 17 00:00:00 2001 From: pitkajuh Date: Wed, 14 Aug 2024 06:52:15 +0000 Subject: [PATCH 048/184] Ensure RegularMesh repr shows value for width of the mesh (#3100) Co-authored-by: Paul Romano --- openmc/mesh.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openmc/mesh.py b/openmc/mesh.py index 85af6a009c9..a706b8fa811 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -787,8 +787,8 @@ def __repr__(self): string += '{0: <16}{1}{2}\n'.format('\tDimensions', '=\t', self.n_dimension) string += '{0: <16}{1}{2}\n'.format('\tVoxels', '=\t', self._dimension) string += '{0: <16}{1}{2}\n'.format('\tLower left', '=\t', self._lower_left) - string += '{0: <16}{1}{2}\n'.format('\tUpper Right', '=\t', self._upper_right) - string += '{0: <16}{1}{2}\n'.format('\tWidth', '=\t', self._width) + string += '{0: <16}{1}{2}\n'.format('\tUpper Right', '=\t', self.upper_right) + string += '{0: <16}{1}{2}\n'.format('\tWidth', '=\t', self.width) return string @classmethod From 9483cce0bc334467748cd842cb122fefc6f3305d Mon Sep 17 00:00:00 2001 From: Jon Shimwell Date: Wed, 14 Aug 2024 15:34:17 +0100 Subject: [PATCH 049/184] Remove resonance reconstruction and Cython dependency (#3111) Co-authored-by: Paul Romano --- Dockerfile | 2 +- MANIFEST.in | 2 - docs/source/usersguide/install.rst | 4 - openmc/data/function.py | 22 -- openmc/data/neutron.py | 88 +---- openmc/data/njoy.py | 15 +- openmc/data/reconstruct.pyx | 522 -------------------------- pyproject.toml | 3 +- setup.py | 14 - tests/unit_tests/test_data_neutron.py | 27 +- tools/ci/gha-install.sh | 3 +- 11 files changed, 33 insertions(+), 669 deletions(-) delete mode 100644 openmc/data/reconstruct.pyx delete mode 100755 setup.py diff --git a/Dockerfile b/Dockerfile index 35b9cf578d6..4a94e3c0b11 100644 --- a/Dockerfile +++ b/Dockerfile @@ -95,7 +95,7 @@ RUN cd $HOME \ RUN if [ "$build_dagmc" = "on" ]; then \ # Install addition packages required for DAGMC apt-get -y install libeigen3-dev libnetcdf-dev libtbb-dev libglfw3-dev \ - && pip install --upgrade numpy "cython<3.0" \ + && pip install --upgrade numpy \ # Clone and install EMBREE && mkdir -p $HOME/EMBREE && cd $HOME/EMBREE \ && git clone --single-branch -b ${EMBREE_TAG} --depth 1 ${EMBREE_REPO} \ diff --git a/MANIFEST.in b/MANIFEST.in index afd016cb021..b73218af0dc 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -26,8 +26,6 @@ recursive-include include *.h recursive-include include *.h.in recursive-include include *.hh recursive-include man *.1 -recursive-include openmc *.pyx -recursive-include openmc *.c recursive-include src *.cc recursive-include src *.cpp recursive-include src *.rnc diff --git a/docs/source/usersguide/install.rst b/docs/source/usersguide/install.rst index 130e96c0ae6..1c0b7fa5b32 100644 --- a/docs/source/usersguide/install.rst +++ b/docs/source/usersguide/install.rst @@ -584,10 +584,6 @@ distributions. parallel runs. This package is needed if you plan on running depletion simulations in parallel using MPI. - `Cython `_ - Cython is used for resonance reconstruction for ENDF data converted to - :class:`openmc.data.IncidentNeutron`. - `vtk `_ The Python VTK bindings are needed to convert voxel and track files to VTK format. diff --git a/openmc/data/function.py b/openmc/data/function.py index 23fd5e9d4fa..c5914f513d5 100644 --- a/openmc/data/function.py +++ b/openmc/data/function.py @@ -708,28 +708,6 @@ def __init__(self, resonances, background, mt): self.background = background self.mt = mt - def __call__(self, x): - # Get background cross section - xs = self.background(x) - - for r in self.resonances: - if not isinstance(r, openmc.data.resonance._RESOLVED): - continue - - if isinstance(x, Iterable): - # Determine which energies are within resolved resonance range - within = (r.energy_min <= x) & (x <= r.energy_max) - - # Get resonance cross sections and add to background - resonant_xs = r.reconstruct(x[within]) - xs[within] += resonant_xs[self.mt] - else: - if r.energy_min <= x <= r.energy_max: - resonant_xs = r.reconstruct(x) - xs += resonant_xs[self.mt] - - return xs - @property def background(self): return self._background diff --git a/openmc/data/neutron.py b/openmc/data/neutron.py index 894be187178..95a3424ea4d 100644 --- a/openmc/data/neutron.py +++ b/openmc/data/neutron.py @@ -16,8 +16,7 @@ Evaluation, SUM_RULES, get_head_record, get_tab1_record, get_evaluations) from .fission_energy import FissionEnergyRelease from .function import Tabulated1D, Sum, ResonancesWithBackground -from .grid import linearize, thin -from .njoy import make_ace +from .njoy import make_ace, make_pendf from .product import Product from .reaction import Reaction, _get_photon_products_ace, FISSION_MTS from . import resonance as res @@ -286,7 +285,7 @@ def add_temperature_from_ace(self, ace_or_filename, metastable_scheme='nndc'): if strT in data.urr: self.urr[strT] = data.urr[strT] - def add_elastic_0K_from_endf(self, filename, overwrite=False): + def add_elastic_0K_from_endf(self, filename, overwrite=False, **kwargs): """Append 0K elastic scattering cross section from an ENDF file. Parameters @@ -297,6 +296,8 @@ def add_elastic_0K_from_endf(self, filename, overwrite=False): If existing 0 K data is present, this flag can be used to indicate that it should be overwritten. Otherwise, an exception will be thrown. + **kwargs + Keyword arguments passed to :func:`openmc.data.njoy.make_pendf` Raises ------ @@ -309,75 +310,22 @@ def add_elastic_0K_from_endf(self, filename, overwrite=False): if '0K' in self.energy and not overwrite: raise ValueError('0 K data already exists for this nuclide.') - data = type(self).from_endf(filename) - if data.resonances is not None: - x = [] - y = [] - for rr in data.resonances: - if isinstance(rr, res.RMatrixLimited): - raise TypeError('R-Matrix Limited not supported.') - elif isinstance(rr, res.Unresolved): - continue + with tempfile.TemporaryDirectory() as tmpdir: + # Set arguments for make_pendf + pendf_path = os.path.join(tmpdir, 'pendf') + kwargs.setdefault('output_dir', tmpdir) + kwargs.setdefault('pendf', pendf_path) - # Get energies/widths for resonances - e_peak = rr.parameters['energy'].values - if isinstance(rr, res.MultiLevelBreitWigner): - gamma = rr.parameters['totalWidth'].values - elif isinstance(rr, res.ReichMoore): - df = rr.parameters - gamma = (df['neutronWidth'] + - df['captureWidth'] + - abs(df['fissionWidthA']) + - abs(df['fissionWidthB'])).values - - # Determine peak energies and widths - e_min, e_max = rr.energy_min, rr.energy_max - in_range = (e_peak > e_min) & (e_peak < e_max) - e_peak = e_peak[in_range] - gamma = gamma[in_range] - - # Get midpoints between resonances (use min/max energy of - # resolved region as absolute lower/upper bound) - e_mid = np.concatenate( - ([e_min], (e_peak[1:] + e_peak[:-1])/2, [e_max])) - - # Add grid around each resonance that includes the peak +/- the - # width times each value in _RESONANCE_ENERGY_GRID. Values are - # constrained so that points around one resonance don't overlap - # with points around another. This algorithm is from Fudge - # (https://doi.org/10.1063/1.1945057). - energies = [] - for e, g, e_lower, e_upper in zip(e_peak, gamma, e_mid[:-1], - e_mid[1:]): - e_left = e - g*_RESONANCE_ENERGY_GRID - energies.append(e_left[e_left > e_lower][::-1]) - e_right = e + g*_RESONANCE_ENERGY_GRID[1:] - energies.append(e_right[e_right < e_upper]) - - # Concatenate all points - energies = np.concatenate(energies) - - # Create 1000 equal log-spaced energies over RRR, combine with - # resonance peaks and half-height energies - e_log = np.logspace(log10(e_min), log10(e_max), 1000) - energies = np.union1d(e_log, energies) - - # Linearize and thin cross section - xi, yi = linearize(energies, data[2].xs['0K']) - xi, yi = thin(xi, yi) - - # If there are multiple resolved resonance ranges (e.g. Pu239 in - # ENDF/B-VII.1), combine them - x = np.concatenate((x, xi)) - y = np.concatenate((y, yi)) - else: - energies = data[2].xs['0K'].x - x, y = linearize(energies, data[2].xs['0K']) - x, y = thin(x, y) + # Run NJOY to create a pointwise ENDF file + make_pendf(filename, **kwargs) - # Set 0K energy grid and elastic scattering cross section - self.energy['0K'] = x - self[2].xs['0K'] = Tabulated1D(x, y) + # Add 0K elastic scattering cross section + pendf = Evaluation(pendf_path) + file_obj = StringIO(pendf.section[3, 2]) + get_head_record(file_obj) + params, xs = get_tab1_record(file_obj) + self.energy['0K'] = xs.x + self[2].xs['0K'] = xs def get_reaction_components(self, mt): """Determine what reactions make up redundant reaction. diff --git a/openmc/data/njoy.py b/openmc/data/njoy.py index ac1b5e345ea..1bf44891ef4 100644 --- a/openmc/data/njoy.py +++ b/openmc/data/njoy.py @@ -221,7 +221,7 @@ def run(commands, tapein, tapeout, input_filename=None, stdout=False, shutil.move(tmpfilename, str(filename)) -def make_pendf(filename, pendf='pendf', error=0.001, stdout=False): +def make_pendf(filename, pendf='pendf', **kwargs): """Generate pointwise ENDF file from an ENDF file Parameters @@ -230,10 +230,9 @@ def make_pendf(filename, pendf='pendf', error=0.001, stdout=False): Path to ENDF file pendf : str, optional Path of pointwise ENDF file to write - error : float, optional - Fractional error tolerance for NJOY processing - stdout : bool - Whether to display NJOY standard output + **kwargs + Keyword arguments passed to :func:`openmc.data.njoy.make_ace`. All NJOY + module arguments other than pendf default to False. Raises ------ @@ -241,9 +240,9 @@ def make_pendf(filename, pendf='pendf', error=0.001, stdout=False): If the NJOY process returns with a non-zero status """ - - make_ace(filename, pendf=pendf, error=error, broadr=False, - heatr=False, purr=False, acer=False, stdout=stdout) + for key in ('broadr', 'heatr', 'gaspr', 'purr', 'acer'): + kwargs.setdefault(key, False) + make_ace(filename, pendf=pendf, **kwargs) def make_ace(filename, temperatures=None, acer=True, xsdir=None, diff --git a/openmc/data/reconstruct.pyx b/openmc/data/reconstruct.pyx deleted file mode 100644 index cd0bbc38b95..00000000000 --- a/openmc/data/reconstruct.pyx +++ /dev/null @@ -1,522 +0,0 @@ -from libc.stdlib cimport malloc, calloc, free -from libc.math cimport cos, sin, sqrt, atan, M_PI - -cimport numpy as np -import numpy as np -from numpy.linalg import inv -cimport cython - - -cdef extern from "complex.h": - double cabs(double complex) - double complex conj(double complex) - double creal(complex double) - double cimag(complex double) - double complex cexp(double complex) - -# Physical constants are from CODATA 2014 -cdef double NEUTRON_MASS_ENERGY = 939.5654133e6 # eV/c^2 -cdef double HBAR_C = 197.3269788e5 # eV-b^0.5 - - -@cython.cdivision(True) -def wave_number(double A, double E): - r"""Neutron wave number in center-of-mass system. - - ENDF-102 defines the neutron wave number in the center-of-mass system in - Equation D.10 as - - .. math:: - k = \frac{2m_n}{\hbar} \frac{A}{A + 1} \sqrt{|E|} - - Parameters - ---------- - A : double - Ratio of target mass to neutron mass - E : double - Energy in eV - - Returns - ------- - double - Neutron wave number in b^-0.5 - - """ - return A/(A + 1)*sqrt(2*NEUTRON_MASS_ENERGY*abs(E))/HBAR_C - -@cython.cdivision(True) -cdef double _wave_number(double A, double E): - return A/(A + 1)*sqrt(2*NEUTRON_MASS_ENERGY*abs(E))/HBAR_C - - -@cython.cdivision(True) -cdef double phaseshift(int l, double rho): - """Calculate hardsphere phase shift as given in ENDF-102, Equation D.13 - - Parameters - ---------- - l : int - Angular momentum quantum number - rho : float - Product of the wave number and the channel radius - - Returns - ------- - double - Hardsphere phase shift - - """ - if l == 0: - return rho - elif l == 1: - return rho - atan(rho) - elif l == 2: - return rho - atan(3*rho/(3 - rho**2)) - elif l == 3: - return rho - atan((15*rho - rho**3)/(15 - 6*rho**2)) - elif l == 4: - return rho - atan((105*rho - 10*rho**3)/(105 - 45*rho**2 + rho**4)) - - -@cython.cdivision(True) -def penetration_shift(int l, double rho): - r"""Calculate shift and penetration factors as given in ENDF-102, Equations D.11 - and D.12. - - Parameters - ---------- - l : int - Angular momentum quantum number - rho : float - Product of the wave number and the channel radius - - Returns - ------- - double - Penetration factor for given :math:`l` - double - Shift factor for given :math:`l` - - """ - cdef double den - - if l == 0: - return rho, 0. - elif l == 1: - den = 1 + rho**2 - return rho**3/den, -1/den - elif l == 2: - den = 9 + 3*rho**2 + rho**4 - return rho**5/den, -(18 + 3*rho**2)/den - elif l == 3: - den = 225 + 45*rho**2 + 6*rho**4 + rho**6 - return rho**7/den, -(675 + 90*rho**2 + 6*rho**4)/den - elif l == 4: - den = 11025 + 1575*rho**2 + 135*rho**4 + 10*rho**6 + rho**8 - return rho**9/den, -(44100 + 4725*rho**2 + 270*rho**4 + 10*rho**6)/den - - -@cython.boundscheck(False) -@cython.wraparound(False) -@cython.cdivision(True) -def reconstruct_mlbw(mlbw, double E): - """Evaluate cross section using MLBW data. - - Parameters - ---------- - mlbw : openmc.data.MultiLevelBreitWigner - Multi-level Breit-Wigner resonance parameters - E : double - Energy in eV at which to evaluate the cross section - - Returns - ------- - elastic : double - Elastic scattering cross section in barns - capture : double - Radiative capture cross section in barns - fission : double - Fission cross section in barns - - """ - cdef int i, nJ, ij, l, n_res, i_res - cdef double elastic, capture, fission - cdef double A, k, rho, rhohat, I - cdef double P, S, phi, cos2phi, sin2phi - cdef double Ex, Q, rhoc, rhochat, P_c, S_c - cdef double jmin, jmax, j, Dl - cdef double E_r, gt, gn, gg, gf, gx, P_r, S_r, P_rx - cdef double gnE, gtE, Eprime, x, f - cdef double *g - cdef double (*s)[2] - cdef double [:,:] params - - I = mlbw.target_spin - A = mlbw.atomic_weight_ratio - k = _wave_number(A, E) - - elastic = 0. - capture = 0. - fission = 0. - - for i, l in enumerate(mlbw._l_values): - params = mlbw._parameter_matrix[l] - - rho = k*mlbw.channel_radius[l](E) - rhohat = k*mlbw.scattering_radius[l](E) - P, S = penetration_shift(l, rho) - phi = phaseshift(l, rhohat) - cos2phi = cos(2*phi) - sin2phi = sin(2*phi) - - # Determine shift and penetration at modified energy - if mlbw._competitive[i]: - Ex = E + mlbw.q_value[l]*(A + 1)/A - rhoc = mlbw.channel_radius[l](Ex) - rhochat = mlbw.scattering_radius[l](Ex) - P_c, S_c = penetration_shift(l, rhoc) - if Ex < 0: - P_c = 0 - - # Determine range of total angular momentum values based on equation - # 41 in LA-UR-12-27079 - jmin = abs(abs(I - l) - 0.5) - jmax = I + l + 0.5 - nJ = int(jmax - jmin + 1) - - # Determine Dl factor using Equation 43 in LA-UR-12-27079 - Dl = 2*l + 1 - g = malloc(nJ*sizeof(double)) - for ij in range(nJ): - j = jmin + ij - g[ij] = (2*j + 1)/(4*I + 2) - Dl -= g[ij] - - s = calloc(2*nJ, sizeof(double)) - for i_res in range(params.shape[0]): - # Copy resonance parameters - E_r = params[i_res, 0] - j = params[i_res, 2] - ij = int(j - jmin) - gt = params[i_res, 3] - gn = params[i_res, 4] - gg = params[i_res, 5] - gf = params[i_res, 6] - gx = params[i_res, 7] - P_r = params[i_res, 8] - S_r = params[i_res, 9] - P_rx = params[i_res, 10] - - # Calculate neutron and total width at energy E - gnE = P*gn/P_r # ENDF-102, Equation D.7 - gtE = gnE + gg + gf - if gx > 0: - gtE += gx*P_c/P_rx - - Eprime = E_r + (S_r - S)/(2*P_r)*gn # ENDF-102, Equation D.9 - x = 2*(E - Eprime)/gtE # LA-UR-12-27079, Equation 26 - f = 2*gnE/(gtE*(1 + x*x)) # Common factor in Equation 40 - s[ij][0] += f # First sum in Equation 40 - s[ij][1] += f*x # Second sum in Equation 40 - capture += f*g[ij]*gg/gtE - if gf > 0: - fission += f*g[ij]*gf/gtE - - for ij in range(nJ): - # Add all but last term of LA-UR-12-27079, Equation 40 - elastic += g[ij]*((1 - cos2phi - s[ij][0])**2 + - (sin2phi + s[ij][1])**2) - - # Add final term with Dl from Equation 40 - elastic += 2*Dl*(1 - cos2phi) - - # Free memory - free(g) - free(s) - - capture *= 2*M_PI/(k*k) - fission *= 2*M_PI/(k*k) - elastic *= M_PI/(k*k) - - return (elastic, capture, fission) - - -@cython.boundscheck(False) -@cython.wraparound(False) -@cython.cdivision(True) -def reconstruct_slbw(slbw, double E): - """Evaluate cross section using SLBW data. - - Parameters - ---------- - slbw : openmc.data.SingleLevelBreitWigner - Single-level Breit-Wigner resonance parameters - E : double - Energy in eV at which to evaluate the cross section - - Returns - ------- - elastic : double - Elastic scattering cross section in barns - capture : double - Radiative capture cross section in barns - fission : double - Fission cross section in barns - - """ - cdef int i, l, i_res - cdef double elastic, capture, fission - cdef double A, k, rho, rhohat, I - cdef double P, S, phi, cos2phi, sin2phi, sinphi2 - cdef double Ex, rhoc, rhochat, P_c, S_c - cdef double E_r, J, gt, gn, gg, gf, gx, P_r, S_r, P_rx - cdef double gnE, gtE, Eprime, f - cdef double x, theta, psi, chi - cdef double [:,:] params - - I = slbw.target_spin - A = slbw.atomic_weight_ratio - k = _wave_number(A, E) - - elastic = 0. - capture = 0. - fission = 0. - - for i, l in enumerate(slbw._l_values): - params = slbw._parameter_matrix[l] - - rho = k*slbw.channel_radius[l](E) - rhohat = k*slbw.scattering_radius[l](E) - P, S = penetration_shift(l, rho) - phi = phaseshift(l, rhohat) - cos2phi = cos(2*phi) - sin2phi = sin(2*phi) - sinphi2 = sin(phi)**2 - - # Add potential scattering -- first term in ENDF-102, Equation D.2 - elastic += 4*M_PI/(k*k)*(2*l + 1)*sinphi2 - - # Determine shift and penetration at modified energy - if slbw._competitive[i]: - Ex = E + slbw.q_value[l]*(A + 1)/A - rhoc = k*slbw.channel_radius[l](Ex) - rhochat = k*slbw.scattering_radius[l](Ex) - P_c, S_c = penetration_shift(l, rhoc) - if Ex < 0: - P_c = 0 - - for i_res in range(params.shape[0]): - # Copy resonance parameters - E_r = params[i_res, 0] - J = params[i_res, 2] - gt = params[i_res, 3] - gn = params[i_res, 4] - gg = params[i_res, 5] - gf = params[i_res, 6] - gx = params[i_res, 7] - P_r = params[i_res, 8] - S_r = params[i_res, 9] - P_rx = params[i_res, 10] - - # Calculate neutron and total width at energy E - gnE = P*gn/P_r # Equation D.7 - gtE = gnE + gg + gf - if gx > 0: - gtE += gx*P_c/P_rx - - Eprime = E_r + (S_r - S)/(2*P_r)*gn # Equation D.9 - gJ = (2*J + 1)/(4*I + 2) # Mentioned in section D.1.1.4 - - # Calculate common factor for elastic, capture, and fission - # cross sections - f = M_PI/(k*k)*gJ*gnE/((E - Eprime)**2 + gtE**2/4) - - # Add contribution to elastic per Equation D.2 - elastic += f*(gnE*cos2phi - 2*(gg + gf)*sinphi2 - + 2*(E - Eprime)*sin2phi) - - # Add contribution to capture per Equation D.3 - capture += f*gg - - # Add contribution to fission per Equation D.6 - if gf > 0: - fission += f*gf - - return (elastic, capture, fission) - - -@cython.boundscheck(False) -@cython.wraparound(False) -@cython.cdivision(True) -def reconstruct_rm(rm, double E): - """Evaluate cross section using Reich-Moore data. - - Parameters - ---------- - rm : openmc.data.ReichMoore - Reich-Moore resonance parameters - E : double - Energy in eV at which to evaluate the cross section - - Returns - ------- - elastic : double - Elastic scattering cross section in barns - capture : double - Radiative capture cross section in barns - fission : double - Fission cross section in barns - - """ - cdef int i, l, m, n, i_res - cdef int i_s, num_s, i_J, num_J - cdef double elastic, capture, fission, total - cdef double A, k, rho, rhohat, I - cdef double P, S, phi - cdef double smin, smax, s, Jmin, Jmax, J, j - cdef double E_r, gn, gg, gfa, gfb, P_r - cdef double E_diff, abs_value, gJ - cdef double Kr, Ki, x - cdef double complex Ubar, U_, factor - cdef bint hasfission - cdef np.ndarray[double, ndim=2] one - cdef np.ndarray[double complex, ndim=2] K, Imat, U - cdef double [:,:] params - - # Get nuclear spin - I = rm.target_spin - - elastic = 0. - fission = 0. - total = 0. - A = rm.atomic_weight_ratio - k = _wave_number(A, E) - one = np.eye(3) - K = np.zeros((3,3), dtype=complex) - - for i, l in enumerate(rm._l_values): - # Check for l-dependent scattering radius - rho = k*rm.channel_radius[l](E) - rhohat = k*rm.scattering_radius[l](E) - - # Calculate shift and penetrability - P, S = penetration_shift(l, rho) - - # Calculate phase shift - phi = phaseshift(l, rhohat) - - # Calculate common factor on collision matrix terms (term outside curly - # braces in ENDF-102, Eq. D.27) - Ubar = cexp(-2j*phi) - - # The channel spin is the vector sum of the target spin, I, and the - # neutron spin, 1/2, so can take on values of |I - 1/2| < s < I + 1/2 - smin = abs(I - 0.5) - smax = I + 0.5 - num_s = int(smax - smin + 1) - - for i_s in range(num_s): - s = i_s + smin - - # Total angular momentum is the vector sum of l and s and can assume - # values between |l - s| < J < l + s - Jmin = abs(l - s) - Jmax = l + s - num_J = int(Jmax - Jmin + 1) - - for i_J in range(num_J): - J = i_J + Jmin - - # Initialize K matrix - for m in range(3): - for n in range(3): - K[m,n] = 0.0 - - hasfission = False - if (l, J) in rm._parameter_matrix: - params = rm._parameter_matrix[l, J] - - for i_res in range(params.shape[0]): - # Sometimes, the same (l, J) quantum numbers can occur - # for different values of the channel spin, s. In this - # case, the sign of the channel spin indicates which - # spin is to be used. If the spin is negative assume - # this resonance comes from the I - 1/2 channel and vice - # versa. - j = params[i_res, 2] - if l > 0: - if (j < 0 and s != smin) or (j > 0 and s != smax): - continue - - # Copy resonance parameters - E_r = params[i_res, 0] - gn = params[i_res, 3] - gg = params[i_res, 4] - gfa = params[i_res, 5] - gfb = params[i_res, 6] - P_r = params[i_res, 7] - - # Calculate neutron width at energy E - gn = sqrt(P*gn/P_r) - - # Calculate j/2 * inverse of denominator of K matrix terms - factor = 0.5j/(E_r - E - 0.5j*gg) - - # Upper triangular portion of K matrix -- see ENDF-102, - # Equation D.28 - K[0,0] = K[0,0] + gn*gn*factor - if gfa != 0.0 or gfb != 0.0: - # Negate fission widths if necessary - gfa = (-1 if gfa < 0 else 1)*sqrt(abs(gfa)) - gfb = (-1 if gfb < 0 else 1)*sqrt(abs(gfb)) - - K[0,1] = K[0,1] + gn*gfa*factor - K[0,2] = K[0,2] + gn*gfb*factor - K[1,1] = K[1,1] + gfa*gfa*factor - K[1,2] = K[1,2] + gfa*gfb*factor - K[2,2] = K[2,2] + gfb*gfb*factor - hasfission = True - - # Get collision matrix - gJ = (2*J + 1)/(4*I + 2) - if hasfission: - # Copy upper triangular portion of K to lower triangular - K[1,0] = K[0,1] - K[2,0] = K[0,2] - K[2,1] = K[1,2] - - Imat = inv(one - K) - U = Ubar*(2*Imat - one) # ENDF-102, Eq. D.27 - elastic += gJ*cabs(1 - U[0,0])**2 # ENDF-102, Eq. D.24 - total += 2*gJ*(1 - creal(U[0,0])) # ENDF-102, Eq. D.23 - - # Calculate fission from ENDF-102, Eq. D.26 - fission += 4*gJ*(cabs(Imat[1,0])**2 + cabs(Imat[2,0])**2) - else: - U_ = Ubar*(2/(1 - K[0,0]) - 1) - if abs(creal(K[0,0])) < 3e-4 and abs(phi) < 3e-4: - # If K and phi are both very small, the calculated cross - # sections can lose precision because the real part of U - # ends up very close to unity. To get around this, we - # use Euler's formula to express Ubar by real and - # imaginary parts, expand cos(2phi) = 1 - 2phi^2 + - # O(phi^4), and then simplify - Kr = creal(K[0,0]) - Ki = cimag(K[0,0]) - x = 2*(-Kr + (Kr*Kr + Ki*Ki)*(1 - phi*phi) + phi*phi - - sin(2*phi)*Ki)/((1 - Kr)*(1 - Kr) + Ki*Ki) - total += 2*gJ*x - elastic += gJ*(x*x + cimag(U_)**2) - else: - total += 2*gJ*(1 - creal(U_)) # ENDF-102, Eq. D.23 - elastic += gJ*cabs(1 - U_)**2 # ENDF-102, Eq. D.24 - - # Calculate capture as difference of other cross sections as per ENDF-102, - # Equation D.25 - capture = total - elastic - fission - - elastic *= M_PI/(k*k) - capture *= M_PI/(k*k) - fission *= M_PI/(k*k) - - return (elastic, capture, fission) diff --git a/pyproject.toml b/pyproject.toml index 98b1d152fbc..39aa261c1b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,6 @@ [build-system] -requires = ["setuptools", "wheel", "numpy", "cython"] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" [project] name = "openmc" diff --git a/setup.py b/setup.py deleted file mode 100755 index 88a45a36090..00000000000 --- a/setup.py +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env python - -import numpy as np -from setuptools import setup -from Cython.Build import cythonize - - -kwargs = { - # Cython is used to add resonance reconstruction - 'ext_modules': cythonize('openmc/data/*.pyx'), - 'include_dirs': [np.get_include()] -} - -setup(**kwargs) diff --git a/tests/unit_tests/test_data_neutron.py b/tests/unit_tests/test_data_neutron.py index c0d6a1f1547..db6ae1eb850 100644 --- a/tests/unit_tests/test_data_neutron.py +++ b/tests/unit_tests/test_data_neutron.py @@ -282,10 +282,6 @@ def test_slbw(xe135): s = resolved.parameters.iloc[0] assert s['energy'] == pytest.approx(0.084) - xs = resolved.reconstruct([10., 30., 100.]) - assert sorted(xs.keys()) == [2, 18, 102] - assert np.all(xs[18] == 0.0) - def test_mlbw(sm150): resolved = sm150.resonances.resolved @@ -294,10 +290,6 @@ def test_mlbw(sm150): assert resolved.energy_max == pytest.approx(1570.) assert resolved.target_spin == 0.0 - xs = resolved.reconstruct([10., 100., 1000.]) - assert sorted(xs.keys()) == [2, 18, 102] - assert np.all(xs[18] == 0.0) - def test_reichmoore(gd154): res = gd154.resonances @@ -319,7 +311,6 @@ def test_reichmoore(gd154): elastic = gd154.reactions[2].xs['0K'] assert isinstance(elastic, openmc.data.ResonancesWithBackground) - assert elastic(0.0253) == pytest.approx(5.7228949796394524) def test_rml(cl35): @@ -347,8 +338,6 @@ def test_mlbw_cov_lcomp0(cf252): assert not subset.parameters.empty assert (subset.file2res.parameters['energy'] < 100).all() samples = cov.sample(1) - xs = samples[0].reconstruct([10., 100., 1000.]) - assert sorted(xs.keys()) == [2, 18, 102] def test_mlbw_cov_lcomp1(ti50): @@ -365,9 +354,7 @@ def test_mlbw_cov_lcomp1(ti50): subset = cov.subset('L', [1, 1]) assert not subset.parameters.empty assert (subset.file2res.parameters['L'] == 1).all() - samples = cov.sample(1) - xs = samples[0].reconstruct([10., 100., 1000.]) - assert sorted(xs.keys()) == [2, 18, 102] + cov.sample(1) def test_mlbw_cov_lcomp2(na23): @@ -384,9 +371,7 @@ def test_mlbw_cov_lcomp2(na23): subset = cov.subset('L', [1, 1]) assert not subset.parameters.empty assert (subset.file2res.parameters['L'] == 1).all() - samples = cov.sample(1) - xs = samples[0].reconstruct([10., 100., 1000.]) - assert sorted(xs.keys()) == [2, 18, 102] + cov.sample(1) def test_rmcov_lcomp1(gd154): @@ -403,9 +388,7 @@ def test_rmcov_lcomp1(gd154): subset = cov.subset('energy', [0, 100]) assert not subset.parameters.empty assert (subset.file2res.parameters['energy'] < 100).all() - samples = cov.sample(1) - xs = samples[0].reconstruct([10., 100., 1000.]) - assert sorted(xs.keys()) == [2, 18, 102] + cov.sample(1) def test_rmcov_lcomp2(th232): @@ -422,9 +405,7 @@ def test_rmcov_lcomp2(th232): subset = cov.subset('energy', [0, 100]) assert not subset.parameters.empty assert (subset.file2res.parameters['energy'] < 100).all() - samples = cov.sample(1) - xs = samples[0].reconstruct([10., 100., 1000.]) - assert sorted(xs.keys()) == [2, 18, 102] + cov.sample(1) def test_madland_nix(am241): diff --git a/tools/ci/gha-install.sh b/tools/ci/gha-install.sh index 87952fda9cb..cff7dc834f5 100755 --- a/tools/ci/gha-install.sh +++ b/tools/ci/gha-install.sh @@ -40,8 +40,7 @@ if [[ $MPI == 'y' ]]; then export CC=mpicc export HDF5_MPI=ON export HDF5_DIR=/usr/lib/x86_64-linux-gnu/hdf5/mpich - pip install wheel "cython<3.0" - pip install --no-binary=h5py --no-build-isolation h5py + pip install --no-binary=h5py h5py fi # Build and install OpenMC executable From 10c511a0e24b8b0a0703441bb47ca4fd1012f23b Mon Sep 17 00:00:00 2001 From: Zoe Prieto <101403129+zoeprieto@users.noreply.github.com> Date: Thu, 15 Aug 2024 12:10:26 -0300 Subject: [PATCH 050/184] Nuclide temperatures - solution to issue #3102 (#3110) --- include/openmc/string_utils.h | 18 +++++++++++++++++- src/mgxs.cpp | 33 ++++++++++++++++++--------------- src/nuclide.cpp | 19 +++++++++++-------- src/string_utils.cpp | 3 +-- src/thermal.cpp | 24 ++++++++++++++---------- 5 files changed, 61 insertions(+), 36 deletions(-) diff --git a/include/openmc/string_utils.h b/include/openmc/string_utils.h index 6b4c69d48b7..2e8b0d14f39 100644 --- a/include/openmc/string_utils.h +++ b/include/openmc/string_utils.h @@ -1,6 +1,7 @@ #ifndef OPENMC_STRING_UTILS_H #define OPENMC_STRING_UTILS_H +#include #include #include "openmc/vector.h" @@ -15,7 +16,7 @@ std::string to_element(const std::string& name); void to_lower(std::string& str); -int word_count(std::string const& str); +int word_count(const std::string& str); vector split(const std::string& in); @@ -23,5 +24,20 @@ bool ends_with(const std::string& value, const std::string& ending); bool starts_with(const std::string& value, const std::string& beginning); +template +inline std::string concatenate(const T& values, const std::string& del = ", ") +{ + if (values.size() == 0) + return ""; + + std::stringstream oss; + auto it = values.begin(); + oss << *it++; + while (it != values.end()) { + oss << del << *it++; + } + return oss.str(); +} + } // namespace openmc #endif // OPENMC_STRING_UTILS_H diff --git a/src/mgxs.cpp b/src/mgxs.cpp index 9eae5a7da7f..eae3e5817ce 100644 --- a/src/mgxs.cpp +++ b/src/mgxs.cpp @@ -72,18 +72,18 @@ void Mgxs::metadata_from_hdf5(hid_t xs_id, const vector& temperature, } get_datasets(kT_group, dset_names); vector shape = {num_temps}; - xt::xarray available_temps(shape); + xt::xarray temps_available(shape); for (int i = 0; i < num_temps; i++) { - read_double(kT_group, dset_names[i], &available_temps[i], true); + read_double(kT_group, dset_names[i], &temps_available[i], true); // convert eV to Kelvin - available_temps[i] /= K_BOLTZMANN; + temps_available[i] = std::round(temps_available[i] / K_BOLTZMANN); // Done with dset_names, so delete it delete[] dset_names[i]; } delete[] dset_names; - std::sort(available_temps.begin(), available_temps.end()); + std::sort(temps_available.begin(), temps_available.end()); // If only one temperature is available, lets just use nearest temperature // interpolation @@ -99,17 +99,20 @@ void Mgxs::metadata_from_hdf5(hid_t xs_id, const vector& temperature, case TemperatureMethod::NEAREST: // Determine actual temperatures to read for (const auto& T : temperature) { - auto i_closest = xt::argmin(xt::abs(available_temps - T))[0]; - double temp_actual = available_temps[i_closest]; + auto i_closest = xt::argmin(xt::abs(temps_available - T))[0]; + double temp_actual = temps_available[i_closest]; if (std::fabs(temp_actual - T) < settings::temperature_tolerance) { if (std::find(temps_to_read.begin(), temps_to_read.end(), std::round(temp_actual)) == temps_to_read.end()) { temps_to_read.push_back(std::round(temp_actual)); } } else { - fatal_error(fmt::format("MGXS library does not contain cross sections " - "for {} at or near {} K.", - in_name, std::round(T))); + fatal_error(fmt::format( + "MGXS library does not contain cross sections " + "for {} at or near {} K. Available temperatures " + "are {} K. Consider making use of openmc.Settings.temperature " + "to specify how intermediate temperatures are treated.", + in_name, std::round(T), concatenate(temps_available))); } } break; @@ -122,16 +125,16 @@ void Mgxs::metadata_from_hdf5(hid_t xs_id, const vector& temperature, in_name + " at temperatures that bound " + std::to_string(std::round(temperature[i]))); } - if ((available_temps[j] <= temperature[i]) && - (temperature[i] < available_temps[j + 1])) { + if ((temps_available[j] <= temperature[i]) && + (temperature[i] < temps_available[j + 1])) { if (std::find(temps_to_read.begin(), temps_to_read.end(), - std::round(available_temps[j])) == temps_to_read.end()) { - temps_to_read.push_back(std::round((int)available_temps[j])); + temps_available[j]) == temps_to_read.end()) { + temps_to_read.push_back(temps_available[j]); } if (std::find(temps_to_read.begin(), temps_to_read.end(), - std::round(available_temps[j + 1])) == temps_to_read.end()) { - temps_to_read.push_back(std::round((int)available_temps[j + 1])); + temps_available[j + 1]) == temps_to_read.end()) { + temps_to_read.push_back(temps_available[j + 1]); } break; } diff --git a/src/nuclide.cpp b/src/nuclide.cpp index 04ec110738c..f720f848bc2 100644 --- a/src/nuclide.cpp +++ b/src/nuclide.cpp @@ -86,7 +86,7 @@ Nuclide::Nuclide(hid_t group, const vector& temperature) for (const auto& name : dset_names) { double T; read_dataset(kT_group, name.c_str(), T); - temps_available.push_back(T / K_BOLTZMANN); + temps_available.push_back(std::round(T / K_BOLTZMANN)); } std::sort(temps_available.begin(), temps_available.end()); @@ -158,9 +158,12 @@ Nuclide::Nuclide(hid_t group, const vector& temperature) } } } else { - fatal_error( - "Nuclear data library does not contain cross sections for " + name_ + - " at or near " + std::to_string(T_desired) + " K."); + fatal_error(fmt::format( + "Nuclear data library does not contain cross sections " + "for {} at or near {} K. Available temperatures " + "are {} K. Consider making use of openmc.Settings.temperature " + "to specify how intermediate temperatures are treated.", + name_, std::to_string(T_desired), concatenate(temps_available))); } } break; @@ -173,8 +176,8 @@ Nuclide::Nuclide(hid_t group, const vector& temperature) for (int j = 0; j < temps_available.size() - 1; ++j) { if (temps_available[j] <= T_desired && T_desired < temps_available[j + 1]) { - int T_j = std::round(temps_available[j]); - int T_j1 = std::round(temps_available[j + 1]); + int T_j = temps_available[j]; + int T_j1 = temps_available[j + 1]; if (!contains(temps_to_read, T_j)) { temps_to_read.push_back(T_j); } @@ -191,14 +194,14 @@ Nuclide::Nuclide(hid_t group, const vector& temperature) if (std::abs(T_desired - temps_available.front()) <= settings::temperature_tolerance) { if (!contains(temps_to_read, temps_available.front())) { - temps_to_read.push_back(std::round(temps_available.front())); + temps_to_read.push_back(temps_available.front()); } continue; } if (std::abs(T_desired - temps_available.back()) <= settings::temperature_tolerance) { if (!contains(temps_to_read, temps_available.back())) { - temps_to_read.push_back(std::round(temps_available.back())); + temps_to_read.push_back(temps_available.back()); } continue; } diff --git a/src/string_utils.cpp b/src/string_utils.cpp index 3b1c1b4a678..74f048e8d24 100644 --- a/src/string_utils.cpp +++ b/src/string_utils.cpp @@ -2,7 +2,6 @@ #include // for equal #include // for tolower, isspace -#include namespace openmc { @@ -36,7 +35,7 @@ void to_lower(std::string& str) str[i] = std::tolower(str[i]); } -int word_count(std::string const& str) +int word_count(const std::string& str) { std::stringstream stream(str); std::string dum; diff --git a/src/thermal.cpp b/src/thermal.cpp index 741f89ed104..cbe0983ed65 100644 --- a/src/thermal.cpp +++ b/src/thermal.cpp @@ -19,6 +19,7 @@ #include "openmc/secondary_correlated.h" #include "openmc/secondary_thermal.h" #include "openmc/settings.h" +#include "openmc/string_utils.h" namespace openmc { @@ -59,7 +60,7 @@ ThermalScattering::ThermalScattering( // Read temperature value double T; read_dataset(kT_group, dset_names[i].data(), T); - temps_available[i] = T / K_BOLTZMANN; + temps_available[i] = std::round(T / K_BOLTZMANN); } std::sort(temps_available.begin(), temps_available.end()); @@ -89,9 +90,12 @@ ThermalScattering::ThermalScattering( temps_to_read.push_back(std::round(temp_actual)); } } else { - fatal_error(fmt::format("Nuclear data library does not contain cross " - "sections for {} at or near {} K.", - name_, std::round(T))); + fatal_error(fmt::format( + "Nuclear data library does not contain cross sections " + "for {} at or near {} K. Available temperatures " + "are {} K. Consider making use of openmc.Settings.temperature " + "to specify how intermediate temperatures are treated.", + name_, std::round(T), concatenate(temps_available))); } } break; @@ -103,8 +107,8 @@ ThermalScattering::ThermalScattering( bool found = false; for (int j = 0; j < temps_available.size() - 1; ++j) { if (temps_available[j] <= T && T < temps_available[j + 1]) { - int T_j = std::round(temps_available[j]); - int T_j1 = std::round(temps_available[j + 1]); + int T_j = temps_available[j]; + int T_j1 = temps_available[j + 1]; if (std::find(temps_to_read.begin(), temps_to_read.end(), T_j) == temps_to_read.end()) { temps_to_read.push_back(T_j); @@ -122,14 +126,14 @@ ThermalScattering::ThermalScattering( if (std::abs(T - temps_available[0]) <= settings::temperature_tolerance) { if (std::find(temps_to_read.begin(), temps_to_read.end(), - std::round(temps_available[0])) == temps_to_read.end()) { - temps_to_read.push_back(std::round(temps_available[0])); + temps_available[0]) == temps_to_read.end()) { + temps_to_read.push_back(temps_available[0]); } } else if (std::abs(T - temps_available[n - 1]) <= settings::temperature_tolerance) { if (std::find(temps_to_read.begin(), temps_to_read.end(), - std::round(temps_available[n - 1])) == temps_to_read.end()) { - temps_to_read.push_back(std::round(temps_available[n - 1])); + temps_available[n - 1]) == temps_to_read.end()) { + temps_to_read.push_back(temps_available[n - 1]); } } else { fatal_error( From e7bc9ba23c896f758c3a4139133cd58eb2b9c1f9 Mon Sep 17 00:00:00 2001 From: Youssef Badr <104090877+ybadr16@users.noreply.github.com> Date: Fri, 16 Aug 2024 00:56:22 +0300 Subject: [PATCH 051/184] Added error if cross sections path is a folder (#3115) --- src/cross_sections.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/cross_sections.cpp b/src/cross_sections.cpp index a7bd86095b3..248ae9019d9 100644 --- a/src/cross_sections.cpp +++ b/src/cross_sections.cpp @@ -283,6 +283,10 @@ void read_ce_cross_sections_xml() { // Check if cross_sections.xml exists const auto& filename = settings::path_cross_sections; + if (dir_exists(filename)) { + fatal_error("OPENMC_CROSS_SECTIONS is set to a directory. " + "It should be set to an XML file."); + } if (!file_exists(filename)) { // Could not find cross_sections.xml file fatal_error("Cross sections XML file '" + filename + "' does not exist."); From b22656e57f9469d25f14a203831bcdb390cd84db Mon Sep 17 00:00:00 2001 From: John Tramm Date: Thu, 15 Aug 2024 20:47:05 -0500 Subject: [PATCH 052/184] Fix random ray solver to correctly simulate fixed source problems with fissionable materials (#3106) Co-authored-by: Paul Romano --- openmc/examples.py | 2 +- src/random_ray/flat_source_domain.cpp | 42 ++--- .../__init__.py | 0 .../flat/inputs_true.dat | 140 +++++++++++++++ .../flat/results_true.dat | 169 ++++++++++++++++++ .../linear_xy/inputs_true.dat | 140 +++++++++++++++ .../linear_xy/results_true.dat | 169 ++++++++++++++++++ .../test.py | 133 ++++++++++++++ 8 files changed, 773 insertions(+), 22 deletions(-) create mode 100644 tests/regression_tests/random_ray_fixed_source_subcritical/__init__.py create mode 100644 tests/regression_tests/random_ray_fixed_source_subcritical/flat/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_subcritical/flat/results_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/results_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_subcritical/test.py diff --git a/openmc/examples.py b/openmc/examples.py index 038c75ad214..538b6ea4ac7 100644 --- a/openmc/examples.py +++ b/openmc/examples.py @@ -804,7 +804,7 @@ def random_ray_lattice() -> openmc.Model: azimuthal_cells.append(azimuthal_cell) # Create a geometry with the azimuthal universes - pincell = openmc.Universe(cells=azimuthal_cells) + pincell = openmc.Universe(cells=azimuthal_cells, name='pincell') ######################################## # Define a moderator lattice universe diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 8b6cda93f2a..584b3a7edb4 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -158,31 +158,31 @@ void FlatSourceDomain::update_neutron_source(double k_eff) } } - if (settings::run_mode == RunMode::EIGENVALUE) { - // Add fission source if in eigenvalue mode + // Add fission source #pragma omp parallel for - for (int sr = 0; sr < n_source_regions_; sr++) { - int material = material_[sr]; + for (int sr = 0; sr < n_source_regions_; sr++) { + int material = material_[sr]; - for (int e_out = 0; e_out < negroups_; e_out++) { - float sigma_t = data::mg.macro_xs_[material].get_xs( - MgxsType::TOTAL, e_out, nullptr, nullptr, nullptr, t, a); - float fission_source = 0.0f; - - for (int e_in = 0; e_in < negroups_; e_in++) { - float scalar_flux = scalar_flux_old_[sr * negroups_ + e_in]; - float nu_sigma_f = data::mg.macro_xs_[material].get_xs( - MgxsType::NU_FISSION, e_in, nullptr, nullptr, nullptr, t, a); - float chi = data::mg.macro_xs_[material].get_xs( - MgxsType::CHI_PROMPT, e_in, &e_out, nullptr, nullptr, t, a); - fission_source += nu_sigma_f * scalar_flux * chi; - } - source_[sr * negroups_ + e_out] += - fission_source * inverse_k_eff / sigma_t; + for (int e_out = 0; e_out < negroups_; e_out++) { + float sigma_t = data::mg.macro_xs_[material].get_xs( + MgxsType::TOTAL, e_out, nullptr, nullptr, nullptr, t, a); + float fission_source = 0.0f; + + for (int e_in = 0; e_in < negroups_; e_in++) { + float scalar_flux = scalar_flux_old_[sr * negroups_ + e_in]; + float nu_sigma_f = data::mg.macro_xs_[material].get_xs( + MgxsType::NU_FISSION, e_in, nullptr, nullptr, nullptr, t, a); + float chi = data::mg.macro_xs_[material].get_xs( + MgxsType::CHI_PROMPT, e_in, &e_out, nullptr, nullptr, t, a); + fission_source += nu_sigma_f * scalar_flux * chi; } + source_[sr * negroups_ + e_out] += + fission_source * inverse_k_eff / sigma_t; } - } else { -// Add external source if in fixed source mode + } + + // Add external source if in fixed source mode + if (settings::run_mode == RunMode::FIXED_SOURCE) { #pragma omp parallel for for (int se = 0; se < n_source_elements_; se++) { source_[se] += external_source_[se]; diff --git a/tests/regression_tests/random_ray_fixed_source_subcritical/__init__.py b/tests/regression_tests/random_ray_fixed_source_subcritical/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regression_tests/random_ray_fixed_source_subcritical/flat/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_subcritical/flat/inputs_true.dat new file mode 100644 index 00000000000..0bf4cfdc90d --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_subcritical/flat/inputs_true.dat @@ -0,0 +1,140 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.126 0.126 + 10 10 + -0.63 -0.63 + +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 + + + 0.126 0.126 + 10 10 + -0.63 -0.63 + +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 9 + + + 1.26 1.26 + 2 2 + -1.26 -1.26 + +2 12 +12 13 + + + + + + + + + + + + + + + + + + + + + fixed source + 30 + 125 + 100 + + + 1.134 -1.26 -1.0 1.26 -1.134 1.0 + + + 2e-05 0.0735 20.0 200.0 2000.0 750000.0 2000000.0 0.14285714285714285 0.14285714285714285 0.14285714285714285 0.14285714285714285 0.14285714285714285 0.14285714285714285 0.14285714285714285 + + + universe + 9 + + + multi-group + + 40.0 + 40.0 + + + -1.26 -1.26 -1 1.26 1.26 1 + + + False + flat + + + + + 2 2 + -1.26 -1.26 + 1.26 1.26 + + + 1 + + + 1e-05 0.0635 10.0 100.0 1000.0 500000.0 1000000.0 20000000.0 + + + 1 2 + flux fission nu-fission + analog + + + diff --git a/tests/regression_tests/random_ray_fixed_source_subcritical/flat/results_true.dat b/tests/regression_tests/random_ray_fixed_source_subcritical/flat/results_true.dat new file mode 100644 index 00000000000..e9d2a733d43 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_subcritical/flat/results_true.dat @@ -0,0 +1,169 @@ +tally 1: +1.582116E+02 +1.005480E+03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.897563E+01 +1.396228E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.038901E+01 +1.667203E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.465551E+01 +2.438101E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.260230E+01 +1.110141E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +9.438906E+01 +3.579643E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.698287E+01 +1.314208E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.002152E+02 +1.608182E+03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +7.374897E+01 +2.181176E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.636086E+01 +2.787512E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +3.000395E+01 +3.611990E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.824715E+01 +1.360847E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +9.621963E+01 +3.714397E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.954480E+01 +1.436984E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.067903E+02 +4.585529E+02 +1.257560E+01 +6.364831E+00 +3.060650E+01 +3.770132E+01 +4.983480E+01 +9.972910E+01 +2.349541E+00 +2.217300E-01 +5.718312E+00 +1.313392E+00 +1.965462E+01 +1.548888E+01 +2.011494E-01 +1.622334E-03 +4.895573E-01 +9.609705E-03 +2.318671E+01 +2.155841E+01 +2.445032E-01 +2.397352E-03 +5.950720E-01 +1.420043E-02 +5.322867E+01 +1.136138E+02 +1.968847E-01 +1.554358E-03 +4.791838E-01 +9.207284E-03 +1.191175E+02 +5.692803E+02 +5.806982E-02 +1.354810E-04 +1.436897E-01 +8.295235E-04 +8.022963E+01 +2.589171E+02 +3.465139E-01 +4.882327E-03 +9.638109E-01 +3.777193E-02 +1.555305E+02 +9.711601E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.836421E+01 +1.367538E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.042089E+01 +1.673401E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.477708E+01 +2.463124E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.317851E+01 +1.134205E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +9.634473E+01 +3.724422E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.879813E+01 +1.396454E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 diff --git a/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/inputs_true.dat new file mode 100644 index 00000000000..f7572a07281 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/inputs_true.dat @@ -0,0 +1,140 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.126 0.126 + 10 10 + -0.63 -0.63 + +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 + + + 0.126 0.126 + 10 10 + -0.63 -0.63 + +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 8 +8 8 8 8 8 8 8 8 8 9 + + + 1.26 1.26 + 2 2 + -1.26 -1.26 + +2 12 +12 13 + + + + + + + + + + + + + + + + + + + + + fixed source + 30 + 125 + 100 + + + 1.134 -1.26 -1.0 1.26 -1.134 1.0 + + + 2e-05 0.0735 20.0 200.0 2000.0 750000.0 2000000.0 0.14285714285714285 0.14285714285714285 0.14285714285714285 0.14285714285714285 0.14285714285714285 0.14285714285714285 0.14285714285714285 + + + universe + 9 + + + multi-group + + 40.0 + 40.0 + + + -1.26 -1.26 -1 1.26 1.26 1 + + + False + linear_xy + + + + + 2 2 + -1.26 -1.26 + 1.26 1.26 + + + 1 + + + 1e-05 0.0635 10.0 100.0 1000.0 500000.0 1000000.0 20000000.0 + + + 1 2 + flux fission nu-fission + analog + + + diff --git a/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/results_true.dat b/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/results_true.dat new file mode 100644 index 00000000000..90be5c56921 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/results_true.dat @@ -0,0 +1,169 @@ +tally 1: +1.573422E+02 +9.945961E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.868035E+01 +1.382578E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.028997E+01 +1.651549E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.453137E+01 +2.414255E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.232506E+01 +1.098683E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +9.385720E+01 +3.539613E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.664229E+01 +1.298333E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.001324E+02 +1.606858E+03 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +7.358736E+01 +2.171786E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.625416E+01 +2.765491E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.987675E+01 +3.582038E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.797311E+01 +1.348247E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +9.573509E+01 +3.677339E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.926615E+01 +1.423707E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.060513E+02 +4.521166E+02 +1.247077E+01 +6.257701E+00 +3.035137E+01 +3.706675E+01 +4.962504E+01 +9.887387E+01 +2.338861E+00 +2.196709E-01 +5.692320E+00 +1.301195E+00 +1.957564E+01 +1.536441E+01 +2.003053E-01 +1.608650E-03 +4.875030E-01 +9.528647E-03 +2.309275E+01 +2.138375E+01 +2.434816E-01 +2.377213E-03 +5.925856E-01 +1.408114E-02 +5.300741E+01 +1.126617E+02 +1.960322E-01 +1.540737E-03 +4.771089E-01 +9.126597E-03 +1.184809E+02 +5.630856E+02 +5.774249E-02 +1.339082E-04 +1.428798E-01 +8.198938E-04 +7.971031E+01 +2.554943E+02 +3.441954E-01 +4.814135E-03 +9.573621E-01 +3.724437E-02 +1.549617E+02 +9.639834E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.808547E+01 +1.354629E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.029953E+01 +1.654123E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.462331E+01 +2.433425E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.283851E+01 +1.120036E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +9.572799E+01 +3.677288E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.842566E+01 +1.378719E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 diff --git a/tests/regression_tests/random_ray_fixed_source_subcritical/test.py b/tests/regression_tests/random_ray_fixed_source_subcritical/test.py new file mode 100644 index 00000000000..6c2c569c65e --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_subcritical/test.py @@ -0,0 +1,133 @@ +import os + +import openmc +from openmc.examples import random_ray_lattice +from openmc.utility_funcs import change_directory +import pytest + +from tests.testing_harness import TolerantPyAPITestHarness + + +class MGXSTestHarness(TolerantPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + + +@pytest.mark.parametrize("shape", ["flat", "linear_xy"]) +def test_random_ray_source(shape): + with change_directory(shape): + openmc.reset_auto_ids() + + # The general strategy is to reuse the random_ray_lattice model, + # but redfine some of the geometry to make it a good + # subcritical multiplication problem. We then also add in + # a fixed source term. + + model = random_ray_lattice() + + # Begin by updating the random ray settings for fixed source + settings = model.settings + settings.random_ray['source_shape'] = shape + settings.run_mode = 'fixed source' + settings.particles = 30 + settings.random_ray['distance_active'] = 40.0 + settings.random_ray['distance_inactive'] = 40.0 + settings.random_ray['volume_normalized_flux_tallies'] = False + + # This problem needs about 2k iterations to converge, + # but for regression testing we only need a few hundred + # to ensure things are working as expected. With + # only 100 inactive batches, tallies will still be off + # by 3x or more. For validation against MGMC, be sure + # to increase the batch counts. + settings.batches = 125 + settings.inactive = 100 + + ######################################## + # Define the alternative geometry + + pitch = 1.26 + + for material in model.materials: + if material.name == 'Water': + water = material + + # The new geometry replaces two of the fuel pins with + # moderator, reducing k-eff to around 0.84. We also + # add a special universe in the corner of one of the moderator + # regions to use as a domain constraint for the source + moderator_infinite = openmc.Cell(fill=water, name='moderator infinite') + mu = openmc.Universe(cells=[moderator_infinite]) + + moderator_infinite2 = openmc.Cell(fill=water, name='moderator infinite 2') + mu2 = openmc.Universe(cells=[moderator_infinite2]) + + n_sub = 10 + + lattice = openmc.RectLattice() + lattice.lower_left = [-pitch/2.0, -pitch/2.0] + lattice.pitch = [pitch/n_sub, pitch/n_sub] + lattice.universes = [[mu] * n_sub for _ in range(n_sub)] + + lattice2 = openmc.RectLattice() + lattice2.lower_left = [-pitch/2.0, -pitch/2.0] + lattice2.pitch = [pitch/n_sub, pitch/n_sub] + lattice2.universes = [[mu] * n_sub for _ in range(n_sub)] + lattice2.universes[n_sub-1][n_sub-1] = mu2 + + mod_lattice_cell = openmc.Cell(fill=lattice) + mod_lattice_uni = openmc.Universe(cells=[mod_lattice_cell]) + + mod_lattice_cell2 = openmc.Cell(fill=lattice2) + mod_lattice_uni2 = openmc.Universe(cells=[mod_lattice_cell2]) + + lattice2x2 = openmc.RectLattice() + lattice2x2.lower_left = [-pitch, -pitch] + lattice2x2.pitch = [pitch, pitch] + + universes = model.geometry.get_all_universes() + for universe in universes.values(): + if universe.name == 'pincell': + pincell = universe + + lattice2x2.universes = [ + [pincell, mod_lattice_uni], + [mod_lattice_uni, mod_lattice_uni2] + ] + + box = openmc.model.RectangularPrism( + pitch*2, pitch*2, boundary_type='reflective') + + assembly = openmc.Cell(fill=lattice2x2, region=-box, name='assembly') + + root = openmc.Universe(name='root universe', cells=[assembly]) + model.geometry = openmc.Geometry(root) + + ######################################## + # Define the fixed source term + + s = 1.0 / 7.0 + strengths = [s, s, s, s, s, s, s] + midpoints = [2.0e-5, 0.0735, 20.0, 2.0e2, 2.0e3, 0.75e6, 2.0e6] + energy_distribution = openmc.stats.Discrete(x=midpoints, p=strengths) + + lower_left_src = [pitch - pitch/10.0, -pitch, -1.0] + upper_right_src = [pitch, -pitch + pitch/10.0, 1.0] + spatial_distribution = openmc.stats.Box( + lower_left_src, upper_right_src, only_fissionable=False) + + settings.source = openmc.IndependentSource( + space=spatial_distribution, + energy=energy_distribution, + constraints={'domains': [mu2]}, + strength=1.0 + ) + + ######################################## + # Run test + + harness = MGXSTestHarness('statepoint.125.h5', model) + harness.main() From 54c28b7705519c8e769de52c10ff1db4c81fc8e6 Mon Sep 17 00:00:00 2001 From: Lorenzo Chierici Date: Fri, 16 Aug 2024 13:57:32 +0200 Subject: [PATCH 053/184] run microxs with mpi (#3028) --- openmc/deplete/microxs.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index 39753529b2f..5be9875f304 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -21,6 +21,7 @@ from .chain import Chain, REACTIONS from .coupled_operator import _find_cross_sections, _get_nuclides_with_data import openmc.lib +from openmc.mpi import comm _valid_rxns = list(REACTIONS) _valid_rxns.append('fission') @@ -124,6 +125,15 @@ def get_microxs_and_flux( flux_tally.scores = ['flux'] model.tallies = openmc.Tallies([rr_tally, flux_tally]) + if openmc.lib.is_initialized: + openmc.lib.finalize() + + if comm.rank == 0: + model.export_to_model_xml() + comm.barrier() + # Reinitialize with tallies + openmc.lib.init(intracomm=comm) + # create temporary run with TemporaryDirectory() as temp_dir: if run_kwargs is None: @@ -133,12 +143,15 @@ def get_microxs_and_flux( run_kwargs.setdefault('cwd', temp_dir) statepoint_path = model.run(**run_kwargs) - with StatePoint(statepoint_path) as sp: - rr_tally = sp.tallies[rr_tally.id] - rr_tally._read_results() - flux_tally = sp.tallies[flux_tally.id] - flux_tally._read_results() + if comm.rank == 0: + with StatePoint(statepoint_path) as sp: + rr_tally = sp.tallies[rr_tally.id] + rr_tally._read_results() + flux_tally = sp.tallies[flux_tally.id] + flux_tally._read_results() + rr_tally = comm.bcast(rr_tally) + flux_tally = comm.bcast(flux_tally) # Get reaction rates and flux values reaction_rates = rr_tally.get_reshaped_data() # (domains, groups, nuclides, reactions) flux = flux_tally.get_reshaped_data() # (domains, groups, 1, 1) @@ -371,4 +384,3 @@ def to_csv(self, *args, **kwargs): ) df = pd.DataFrame({'xs': self.data.flatten()}, index=multi_index) df.to_csv(*args, **kwargs) - From 4ef1faf766818a7c01569386d27451774406d542 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 16 Aug 2024 11:33:59 -0500 Subject: [PATCH 054/184] Add delta_function convenience function (#3090) --- docs/source/pythonapi/stats.rst | 1 + openmc/stats/univariate.py | 21 +++++++++++++++++++++ tests/unit_tests/test_stats.py | 7 +++++++ 3 files changed, 29 insertions(+) diff --git a/docs/source/pythonapi/stats.rst b/docs/source/pythonapi/stats.rst index ffb4fcda2ba..b72896c1860 100644 --- a/docs/source/pythonapi/stats.rst +++ b/docs/source/pythonapi/stats.rst @@ -28,6 +28,7 @@ Univariate Probability Distributions :nosignatures: :template: myfunction.rst + openmc.stats.delta_function openmc.stats.muir Angular Distributions diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index a47f4f32030..10822c06fa7 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -312,6 +312,27 @@ def clip(self, tolerance: float = 1e-6, inplace: bool = False) -> Discrete: return type(self)(new_x, new_p) +def delta_function(value: float, intensity: float = 1.0) -> Discrete: + """Return a discrete distribution with a single point. + + .. versionadded:: 0.15.1 + + Parameters + ---------- + value : float + Value of the random variable. + intensity : float, optional + When used for an energy distribution, this can be used to assign an + intensity. + + Returns + ------- + Discrete distribution with a single point + + """ + return Discrete([value], [intensity]) + + class Uniform(Univariate): """Distribution with constant probability over a finite interval [a,b] diff --git a/tests/unit_tests/test_stats.py b/tests/unit_tests/test_stats.py index 5451c0a2cac..0414fc22559 100644 --- a/tests/unit_tests/test_stats.py +++ b/tests/unit_tests/test_stats.py @@ -50,6 +50,13 @@ def test_discrete(): assert_sample_mean(samples, exp_mean) +def test_delta_function(): + d = openmc.stats.delta_function(14.1e6) + assert isinstance(d, openmc.stats.Discrete) + np.testing.assert_array_equal(d.x, [14.1e6]) + np.testing.assert_array_equal(d.p, [1.0]) + + def test_merge_discrete(): x1 = [0.0, 1.0, 10.0] p1 = [0.3, 0.2, 0.5] From 39a2d46e2636707d11f951fb4f206c2155c77198 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Sun, 18 Aug 2024 23:09:32 -0500 Subject: [PATCH 055/184] Implement bounding_box operation for meshes (#3119) --- cmake/Modules/FindLIBMESH.cmake | 2 +- include/openmc/bounding_box.h | 61 +++++++++++++++++++++++++++++++++ include/openmc/cell.h | 1 + include/openmc/mesh.h | 52 +++++++++++++++++++++++++--- include/openmc/surface.h | 50 +-------------------------- include/openmc/universe.h | 1 + openmc/bounding_box.py | 3 +- openmc/lib/mesh.py | 30 ++++++++++++++++ src/mesh.cpp | 60 +++++++++++++++++++++++++++++--- src/particle.cpp | 2 +- tests/unit_tests/test_lib.py | 21 ++++++++++++ 11 files changed, 223 insertions(+), 60 deletions(-) create mode 100644 include/openmc/bounding_box.h diff --git a/cmake/Modules/FindLIBMESH.cmake b/cmake/Modules/FindLIBMESH.cmake index df5b5b9d863..048dfc2a8b8 100644 --- a/cmake/Modules/FindLIBMESH.cmake +++ b/cmake/Modules/FindLIBMESH.cmake @@ -14,7 +14,7 @@ if(DEFINED ENV{METHOD}) message(STATUS "Using environment variable METHOD to determine libMesh build: ${LIBMESH_PC_FILE}") endif() -include(FindPkgConfig) +find_package(PkgConfig REQUIRED) set(ENV{PKG_CONFIG_PATH} "$ENV{PKG_CONFIG_PATH}:${LIBMESH_PC}") set(PKG_CONFIG_USE_CMAKE_PREFIX_PATH True) pkg_check_modules(LIBMESH REQUIRED ${LIBMESH_PC_FILE}>=1.7.0 IMPORTED_TARGET) diff --git a/include/openmc/bounding_box.h b/include/openmc/bounding_box.h new file mode 100644 index 00000000000..40f603583b4 --- /dev/null +++ b/include/openmc/bounding_box.h @@ -0,0 +1,61 @@ +#ifndef OPENMC_BOUNDING_BOX_H +#define OPENMC_BOUNDING_BOX_H + +#include // for min, max + +#include "openmc/constants.h" + +namespace openmc { + +//============================================================================== +//! Coordinates for an axis-aligned cuboid that bounds a geometric object. +//============================================================================== + +struct BoundingBox { + double xmin = -INFTY; + double xmax = INFTY; + double ymin = -INFTY; + double ymax = INFTY; + double zmin = -INFTY; + double zmax = INFTY; + + inline BoundingBox operator&(const BoundingBox& other) + { + BoundingBox result = *this; + return result &= other; + } + + inline BoundingBox operator|(const BoundingBox& other) + { + BoundingBox result = *this; + return result |= other; + } + + // intersect operator + inline BoundingBox& operator&=(const BoundingBox& other) + { + xmin = std::max(xmin, other.xmin); + xmax = std::min(xmax, other.xmax); + ymin = std::max(ymin, other.ymin); + ymax = std::min(ymax, other.ymax); + zmin = std::max(zmin, other.zmin); + zmax = std::min(zmax, other.zmax); + return *this; + } + + // union operator + inline BoundingBox& operator|=(const BoundingBox& other) + { + xmin = std::min(xmin, other.xmin); + xmax = std::max(xmax, other.xmax); + ymin = std::min(ymin, other.ymin); + ymax = std::max(ymax, other.ymax); + zmin = std::min(zmin, other.zmin); + zmax = std::max(zmax, other.zmax); + return *this; + } +}; + +} // namespace openmc + +#endif diff --git a/include/openmc/cell.h b/include/openmc/cell.h index d78614f057e..70843140bad 100644 --- a/include/openmc/cell.h +++ b/include/openmc/cell.h @@ -12,6 +12,7 @@ #include "pugixml.hpp" #include +#include "openmc/bounding_box.h" #include "openmc/constants.h" #include "openmc/memory.h" // for unique_ptr #include "openmc/neighbor_list.h" diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index 3917e6368a4..6f3f2b0a480 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -11,6 +11,7 @@ #include "xtensor/xtensor.hpp" #include +#include "openmc/bounding_box.h" #include "openmc/error.h" #include "openmc/memory.h" // for unique_ptr #include "openmc/particle.h" @@ -185,9 +186,24 @@ class Mesh { vector material_volumes( int n_sample, int bin, uint64_t* seed) const; + //! Determine bounding box of mesh + // + //! \return Bounding box of mesh + BoundingBox bounding_box() const + { + auto ll = this->lower_left(); + auto ur = this->upper_right(); + return {ll.x, ur.x, ll.y, ur.y, ll.z, ur.z}; + } + + virtual Position lower_left() const = 0; + virtual Position upper_right() const = 0; + // Data members - int id_ {-1}; //!< User-specified ID - int n_dimension_ {-1}; //!< Number of dimensions + xt::xtensor lower_left_; //!< Lower-left coordinates of mesh + xt::xtensor upper_right_; //!< Upper-right coordinates of mesh + int id_ {-1}; //!< User-specified ID + int n_dimension_ {-1}; //!< Number of dimensions }; class StructuredMesh : public Mesh { @@ -325,14 +341,30 @@ class StructuredMesh : public Mesh { return this->volume(get_indices_from_bin(bin)); } + Position lower_left() const override + { + int n = lower_left_.size(); + Position ll {lower_left_[0], 0.0, 0.0}; + ll.y = (n >= 2) ? lower_left_[1] : -INFTY; + ll.z = (n == 3) ? lower_left_[2] : -INFTY; + return ll; + }; + + Position upper_right() const override + { + int n = upper_right_.size(); + Position ur {upper_right_[0], 0.0, 0.0}; + ur.y = (n >= 2) ? upper_right_[1] : INFTY; + ur.z = (n == 3) ? upper_right_[2] : INFTY; + return ur; + }; + //! Get the volume of a specified element //! \param[in] ijk Mesh index to return the volume for //! \return Volume of the bin virtual double volume(const MeshIndex& ijk) const = 0; // Data members - xt::xtensor lower_left_; //!< Lower-left coordinates of mesh - xt::xtensor upper_right_; //!< Upper-right coordinates of mesh std::array shape_; //!< Number of mesh elements in each dimension protected: @@ -655,6 +687,15 @@ class UnstructuredMesh : public Mesh { ElementType element_type(int bin) const; + Position lower_left() const override + { + return {lower_left_[0], lower_left_[1], lower_left_[2]}; + } + Position upper_right() const override + { + return {upper_right_[0], upper_right_[1], upper_right_[2]}; + } + protected: //! Set the length multiplier to apply to each point in the mesh void set_length_multiplier(const double length_multiplier); @@ -672,6 +713,9 @@ class UnstructuredMesh : public Mesh { -1.0}; //!< Multiplicative factor applied to mesh coordinates std::string options_; //!< Options for search data structures + //! Determine lower-left and upper-right bounds of mesh + void determine_bounds(); + private: //! Setup method for the mesh. Builds data structures, //! sets up element mapping, creates bounding boxes, etc. diff --git a/include/openmc/surface.h b/include/openmc/surface.h index 350775123c1..af235301c14 100644 --- a/include/openmc/surface.h +++ b/include/openmc/surface.h @@ -9,6 +9,7 @@ #include "pugixml.hpp" #include "openmc/boundary_condition.h" +#include "openmc/bounding_box.h" #include "openmc/constants.h" #include "openmc/memory.h" // for unique_ptr #include "openmc/particle.h" @@ -28,55 +29,6 @@ extern std::unordered_map surface_map; extern vector> surfaces; } // namespace model -//============================================================================== -//! Coordinates for an axis-aligned cuboid that bounds a geometric object. -//============================================================================== - -struct BoundingBox { - double xmin = -INFTY; - double xmax = INFTY; - double ymin = -INFTY; - double ymax = INFTY; - double zmin = -INFTY; - double zmax = INFTY; - - inline BoundingBox operator&(const BoundingBox& other) - { - BoundingBox result = *this; - return result &= other; - } - - inline BoundingBox operator|(const BoundingBox& other) - { - BoundingBox result = *this; - return result |= other; - } - - // intersect operator - inline BoundingBox& operator&=(const BoundingBox& other) - { - xmin = std::max(xmin, other.xmin); - xmax = std::min(xmax, other.xmax); - ymin = std::max(ymin, other.ymin); - ymax = std::min(ymax, other.ymax); - zmin = std::max(zmin, other.zmin); - zmax = std::min(zmax, other.zmax); - return *this; - } - - // union operator - inline BoundingBox& operator|=(const BoundingBox& other) - { - xmin = std::min(xmin, other.xmin); - xmax = std::max(xmax, other.xmax); - ymin = std::min(ymin, other.ymin); - ymax = std::max(ymax, other.ymax); - zmin = std::min(zmin, other.zmin); - zmax = std::max(zmax, other.zmax); - return *this; - } -}; - //============================================================================== //! A geometry primitive used to define regions of 3D space. //============================================================================== diff --git a/include/openmc/universe.h b/include/openmc/universe.h index 26f33cb383d..9fea06bccba 100644 --- a/include/openmc/universe.h +++ b/include/openmc/universe.h @@ -1,6 +1,7 @@ #ifndef OPENMC_UNIVERSE_H #define OPENMC_UNIVERSE_H +#include "openmc/bounding_box.h" #include "openmc/cell.h" namespace openmc { diff --git a/openmc/bounding_box.py b/openmc/bounding_box.py index 5f3d3a6cfdb..f0dc06a4a04 100644 --- a/openmc/bounding_box.py +++ b/openmc/bounding_box.py @@ -42,7 +42,8 @@ def __init__(self, lower_left: Iterable[float], upper_right: Iterable[float]): def __repr__(self) -> str: return "BoundingBox(lower_left={}, upper_right={})".format( - tuple(self.lower_left), tuple(self.upper_right)) + tuple(float(x) for x in self.lower_left), + tuple(float(x) for x in self.upper_right)) def __getitem__(self, key) -> np.ndarray: return self._bounds[key] diff --git a/openmc/lib/mesh.py b/openmc/lib/mesh.py index 78566d449a6..16bec019863 100644 --- a/openmc/lib/mesh.py +++ b/openmc/lib/mesh.py @@ -2,6 +2,7 @@ from ctypes import (c_int, c_int32, c_char_p, c_double, POINTER, Structure, create_string_buffer, c_uint64, c_size_t) from random import getrandbits +import sys from weakref import WeakValueDictionary import numpy as np @@ -13,6 +14,7 @@ from .error import _error_handler from .material import Material from .plot import _Position +from ..bounding_box import BoundingBox __all__ = [ 'Mesh', 'RegularMesh', 'RectilinearMesh', 'CylindricalMesh', @@ -44,6 +46,10 @@ class _MaterialVolume(Structure): _dll.openmc_mesh_get_volumes.argtypes = [c_int32, POINTER(c_double)] _dll.openmc_mesh_get_volumes.restype = c_int _dll.openmc_mesh_get_volumes.errcheck = _error_handler +_dll.openmc_mesh_bounding_box.argtypes = [ + c_int32, POINTER(c_double), POINTER(c_double)] +_dll.openmc_mesh_bounding_box.restype = c_int +_dll.openmc_mesh_bounding_box.errcheck = _error_handler _dll.openmc_mesh_material_volumes.argtypes = [ c_int32, c_int, c_int, c_int, POINTER(_MaterialVolume), POINTER(c_int), POINTER(c_uint64)] @@ -166,6 +172,22 @@ def volumes(self) -> np.ndarray: self._index, volumes.ctypes.data_as(POINTER(c_double))) return volumes + @property + def bounding_box(self) -> BoundingBox: + inf = sys.float_info.max + ll = np.zeros(3) + ur = np.zeros(3) + _dll.openmc_mesh_bounding_box( + self._index, + ll.ctypes.data_as(POINTER(c_double)), + ur.ctypes.data_as(POINTER(c_double)) + ) + ll[ll == inf] = np.inf + ur[ur == inf] = np.inf + ll[ll == -inf] = -np.inf + ur[ur == -inf] = -np.inf + return BoundingBox(ll, ur) + def material_volumes( self, n_samples: int = 10_000, @@ -292,6 +314,8 @@ class RegularMesh(Mesh): Total number of mesh elements. volumes : numpy.ndarray Volume of each mesh element in [cm^3] + bounding_box : openmc.BoundingBox + Axis-aligned bounding box of the mesh """ mesh_type = 'regular' @@ -378,6 +402,8 @@ class RectilinearMesh(Mesh): Total number of mesh elements. volumes : numpy.ndarray Volume of each mesh element in [cm^3] + bounding_box : openmc.BoundingBox + Axis-aligned bounding box of the mesh """ mesh_type = 'rectilinear' @@ -481,6 +507,8 @@ class CylindricalMesh(Mesh): Total number of mesh elements. volumes : numpy.ndarray Volume of each mesh element in [cm^3] + bounding_box : openmc.BoundingBox + Axis-aligned bounding box of the mesh """ mesh_type = 'cylindrical' @@ -584,6 +612,8 @@ class SphericalMesh(Mesh): Total number of mesh elements. volumes : numpy.ndarray Volume of each mesh element in [cm^3] + bounding_box : openmc.BoundingBox + Axis-aligned bounding box of the mesh """ mesh_type = 'spherical' diff --git a/src/mesh.cpp b/src/mesh.cpp index c57dd5dcc31..0f8b4b14e84 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -319,6 +319,28 @@ UnstructuredMesh::UnstructuredMesh(pugi::xml_node node) : Mesh(node) } } +void UnstructuredMesh::determine_bounds() +{ + double xmin = INFTY; + double ymin = INFTY; + double zmin = INFTY; + double xmax = -INFTY; + double ymax = -INFTY; + double zmax = -INFTY; + int n = this->n_vertices(); + for (int i = 0; i < n; ++i) { + auto v = this->vertex(i); + xmin = std::min(v.x, xmin); + ymin = std::min(v.y, ymin); + zmin = std::min(v.z, zmin); + xmax = std::max(v.x, xmax); + ymax = std::max(v.y, ymax); + zmax = std::max(v.z, zmax); + } + lower_left_ = {xmin, ymin, zmin}; + upper_right_ = {xmax, ymax, zmax}; +} + Position UnstructuredMesh::sample_tet( std::array coords, uint64_t* seed) const { @@ -1372,8 +1394,10 @@ int CylindricalMesh::set_grid() full_phi_ = (grid_[1].front() == 0.0) && (grid_[1].back() == 2.0 * PI); - lower_left_ = {grid_[0].front(), grid_[1].front(), grid_[2].front()}; - upper_right_ = {grid_[0].back(), grid_[1].back(), grid_[2].back()}; + lower_left_ = {origin_[0] - grid_[0].back(), origin_[1] - grid_[0].back(), + origin_[2] + grid_[2].front()}; + upper_right_ = {origin_[0] + grid_[0].back(), origin_[1] + grid_[0].back(), + origin_[2] + grid_[2].back()}; return 0; } @@ -1687,8 +1711,9 @@ int SphericalMesh::set_grid() full_theta_ = (grid_[1].front() == 0.0) && (grid_[1].back() == PI); full_phi_ = (grid_[2].front() == 0.0) && (grid_[2].back() == 2 * PI); - lower_left_ = {grid_[0].front(), grid_[1].front(), grid_[2].front()}; - upper_right_ = {grid_[0].back(), grid_[1].back(), grid_[2].back()}; + double r = grid_[0].back(); + lower_left_ = {origin_[0] - r, origin_[1] - r, origin_[2] - r}; + upper_right_ = {origin_[0] + r, origin_[1] + r, origin_[2] + r}; return 0; } @@ -1899,6 +1924,26 @@ extern "C" int openmc_mesh_get_volumes(int32_t index, double* volumes) return 0; } +//! Get the bounding box of a mesh +extern "C" int openmc_mesh_bounding_box(int32_t index, double* ll, double* ur) +{ + if (int err = check_mesh(index)) + return err; + + BoundingBox bbox = model::meshes[index]->bounding_box(); + + // set lower left corner values + ll[0] = bbox.xmin; + ll[1] = bbox.ymin; + ll[2] = bbox.zmin; + + // set upper right corner values + ur[0] = bbox.xmax; + ur[1] = bbox.ymax; + ur[2] = bbox.zmax; + return 0; +} + extern "C" int openmc_mesh_material_volumes(int32_t index, int n_sample, int bin, int result_size, void* result, int* hits, uint64_t* seed) { @@ -2265,6 +2310,9 @@ void MOABMesh::initialize() } } } + + // Determine bounds of mesh + this->determine_bounds(); } void MOABMesh::prepare_for_tallies() @@ -2952,6 +3000,10 @@ void LibMesh::initialize() // bounding box for the mesh for quick rejection checks bbox_ = libMesh::MeshTools::create_bounding_box(*m_); + libMesh::Point ll = bbox_.min(); + libMesh::Point ur = bbox_.max(); + lower_left_ = {ll(0), ll(1), ll(2)}; + upper_right_ = {ur(0), ur(1), ur(2)}; } // Sample position within a tet for LibMesh type tets diff --git a/src/particle.cpp b/src/particle.cpp index 7d30e26bdf8..64c50c9438f 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -296,7 +296,7 @@ void Particle::event_cross_surface() if (surf->surf_source_ && surf->bc_) { add_surf_source_to_bank(*this, *surf); } - cross_surface(*surf); + this->cross_surface(*surf); // If no BC, add particle to surface source after crossing surface if (surf->surf_source_ && !surf->bc_) { add_surf_source_to_bank(*this, *surf); diff --git a/tests/unit_tests/test_lib.py b/tests/unit_tests/test_lib.py index e3c7ce3b70d..64c16c238ea 100644 --- a/tests/unit_tests/test_lib.py +++ b/tests/unit_tests/test_lib.py @@ -570,6 +570,12 @@ def test_regular_mesh(lib_init): np.testing.assert_allclose(mesh.volumes, 1.0) + # bounding box + mesh.set_parameters(lower_left=ll, upper_right=ur) + bbox = mesh.bounding_box + np.testing.assert_allclose(bbox.lower_left, ll) + np.testing.assert_allclose(bbox.upper_right, ur) + meshes = openmc.lib.meshes assert isinstance(meshes, Mapping) assert len(meshes) == 1 @@ -650,6 +656,11 @@ def test_rectilinear_mesh(lib_init): np.testing.assert_allclose(mesh.volumes, 1000.0) + # bounding box + bbox = mesh.bounding_box + np.testing.assert_allclose(bbox.lower_left, (-10., 0., 10.)) + np.testing.assert_allclose(bbox.upper_right, (10., 20., 30.)) + with pytest.raises(exc.AllocationError): mesh2 = openmc.lib.RectilinearMesh(mesh.id) @@ -697,6 +708,11 @@ def test_cylindrical_mesh(lib_init): np.testing.assert_allclose(mesh.volumes[::2], 10/360 * pi * 5**2 * 10) np.testing.assert_allclose(mesh.volumes[1::2], 10/360 * pi * (10**2 - 5**2) * 10) + # bounding box + bbox = mesh.bounding_box + np.testing.assert_allclose(bbox.lower_left, (-10., -10., 10.)) + np.testing.assert_allclose(bbox.upper_right, (10., 10., 30.)) + with pytest.raises(exc.AllocationError): mesh2 = openmc.lib.CylindricalMesh(mesh.id) @@ -750,6 +766,11 @@ def test_spherical_mesh(lib_init): np.testing.assert_allclose(mesh.volumes[2::4], f * 5**3 * dtheta(10., 20.)) np.testing.assert_allclose(mesh.volumes[3::4], f * (10**3 - 5**3) * dtheta(10., 20.)) + # bounding box + bbox = mesh.bounding_box + np.testing.assert_allclose(bbox.lower_left, (-10., -10., -10.)) + np.testing.assert_allclose(bbox.upper_right, (10., 10., 10.)) + with pytest.raises(exc.AllocationError): mesh2 = openmc.lib.SphericalMesh(mesh.id) From 86fc40a6456e789676eec3410afff38e340ea8f8 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 19 Aug 2024 10:07:28 -0500 Subject: [PATCH 056/184] Add OrthogonalBox composite surface (#3118) --- docs/source/pythonapi/model.rst | 1 + openmc/model/surface_composite.py | 80 ++++++++++++++++++++++ openmc/surface.py | 13 +++- tests/unit_tests/test_surface_composite.py | 54 +++++++++++++++ 4 files changed, 145 insertions(+), 3 deletions(-) diff --git a/docs/source/pythonapi/model.rst b/docs/source/pythonapi/model.rst index 99459f64d09..21944018e7d 100644 --- a/docs/source/pythonapi/model.rst +++ b/docs/source/pythonapi/model.rst @@ -26,6 +26,7 @@ Composite Surfaces openmc.model.CylinderSector openmc.model.HexagonalPrism openmc.model.IsogonalOctagon + openmc.model.OrthogonalBox openmc.model.Polygon openmc.model.RectangularParallelepiped openmc.model.RectangularPrism diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index 29411fe4608..a2cb0243849 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -645,6 +645,86 @@ def __pos__(self): return +self.xmax | -self.xmin | +self.ymax | -self.ymin | +self.zmax | -self.zmin +class OrthogonalBox(CompositeSurface): + """Arbitrarily oriented orthogonal box + + This composite surface is composed of four or six planar surfaces that form + an arbitrarily oriented orthogonal box when combined. + + Parameters + ---------- + v : iterable of float + (x,y,z) coordinates of a corner of the box + a1 : iterable of float + Vector of first side starting from ``v`` + a2 : iterable of float + Vector of second side starting from ``v`` + a3 : iterable of float, optional + Vector of third side starting from ``v``. When not specified, it is + assumed that the box will be infinite along the vector normal to the + plane specified by ``a1`` and ``a2``. + **kwargs + Keyword arguments passed to underlying plane classes + + Attributes + ---------- + ax1_min, ax1_max : openmc.Plane + Planes representing minimum and maximum along first axis + ax2_min, ax2_max : openmc.Plane + Planes representing minimum and maximum along second axis + ax3_min, ax3_max : openmc.Plane + Planes representing minimum and maximum along third axis + + """ + _surface_names = ('ax1_min', 'ax1_max', 'ax2_min', 'ax2_max', 'ax3_min', 'ax3_max') + + def __init__(self, v, a1, a2, a3=None, **kwargs): + v = np.array(v) + a1 = np.array(a1) + a2 = np.array(a2) + if has_a3 := a3 is not None: + a3 = np.array(a3) + else: + a3 = np.cross(a1, a2) # normal to plane specified by a1 and a2 + + # Generate corners of box + p1 = v + p2 = v + a1 + p3 = v + a2 + p4 = v + a3 + p5 = v + a1 + a2 + p6 = v + a2 + a3 + p7 = v + a1 + a3 + + # Generate 6 planes of box + self.ax1_min = openmc.Plane.from_points(p1, p3, p4, **kwargs) + self.ax1_max = openmc.Plane.from_points(p2, p5, p7, **kwargs) + self.ax2_min = openmc.Plane.from_points(p1, p4, p2, **kwargs) + self.ax2_max = openmc.Plane.from_points(p3, p6, p5, **kwargs) + if has_a3: + self.ax3_min = openmc.Plane.from_points(p1, p2, p3, **kwargs) + self.ax3_max = openmc.Plane.from_points(p4, p7, p6, **kwargs) + + # Make sure a point inside the box produces the correct senses. If not, + # flip the plane coefficients so it does. + mid_point = v + (a1 + a2 + a3)/2 + nums = (1, 2, 3) if has_a3 else (1, 2) + for num in nums: + min_surf = getattr(self, f'ax{num}_min') + max_surf = getattr(self, f'ax{num}_max') + if mid_point in -min_surf: + min_surf.flip_normal() + if mid_point in +max_surf: + max_surf.flip_normal() + + def __neg__(self): + region = (+self.ax1_min & -self.ax1_max & + +self.ax2_min & -self.ax2_max) + if hasattr(self, 'ax3_min'): + region &= (+self.ax3_min & -self.ax3_max) + return region + + class XConeOneSided(CompositeSurface): """One-sided cone parallel the x-axis diff --git a/openmc/surface.py b/openmc/surface.py index 0fa2ae43949..2f10750a89b 100644 --- a/openmc/surface.py +++ b/openmc/surface.py @@ -802,6 +802,13 @@ def from_points(cls, p1, p2, p3, **kwargs): d = np.dot(n, p1) return cls(a=a, b=b, c=c, d=d, **kwargs) + def flip_normal(self): + """Modify plane coefficients to reverse the normal vector.""" + self.a = -self.a + self.b = -self.b + self.c = -self.c + self.d = -self.d + class XPlane(PlaneMixin, Surface): """A plane perpendicular to the x axis of the form :math:`x - x_0 = 0` @@ -1762,7 +1769,7 @@ class Cone(QuadricMixin, Surface): z-coordinate of the apex in [cm]. Defaults to 0. r2 : float, optional Parameter related to the aperture [:math:`\\rm cm^2`]. - It can be interpreted as the increase in the radius squared per cm along + It can be interpreted as the increase in the radius squared per cm along the cone's axis of revolution. dx : float, optional x-component of the vector representing the axis of the cone. @@ -1920,7 +1927,7 @@ class XCone(QuadricMixin, Surface): z-coordinate of the apex in [cm]. Defaults to 0. r2 : float, optional Parameter related to the aperture [:math:`\\rm cm^2`]. - It can be interpreted as the increase in the radius squared per cm along + It can be interpreted as the increase in the radius squared per cm along the cone's axis of revolution. boundary_type : {'transmission, 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the @@ -2021,7 +2028,7 @@ class YCone(QuadricMixin, Surface): z-coordinate of the apex in [cm]. Defaults to 0. r2 : float, optional Parameter related to the aperture [:math:`\\rm cm^2`]. - It can be interpreted as the increase in the radius squared per cm along + It can be interpreted as the increase in the radius squared per cm along the cone's axis of revolution. boundary_type : {'transmission, 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the diff --git a/tests/unit_tests/test_surface_composite.py b/tests/unit_tests/test_surface_composite.py index 01c827223fd..d862ae6b0de 100644 --- a/tests/unit_tests/test_surface_composite.py +++ b/tests/unit_tests/test_surface_composite.py @@ -498,3 +498,57 @@ def test_cruciform_prism(axis): openmc.model.CruciformPrism([1.0, 0.5, 2.0, 3.0]) with pytest.raises(ValueError): openmc.model.CruciformPrism([3.0, 2.0, 0.5, 1.0]) + + +def test_box(): + v = (-1.0, -1.0, -2.5) + a1 = (2.0, -1.0, 0.0) + a2 = (1.0, 2.0, 0.0) + a3 = (0.0, 0.0, 5.0) + s = openmc.model.OrthogonalBox(v, a1, a2, a3) + for num in (1, 2, 3): + assert isinstance(getattr(s, f'ax{num}_min'), openmc.Plane) + assert isinstance(getattr(s, f'ax{num}_max'), openmc.Plane) + + # Make sure boundary condition propagates + s.boundary_type = 'reflective' + assert s.boundary_type == 'reflective' + for num in (1, 2, 3): + assert getattr(s, f'ax{num}_min').boundary_type == 'reflective' + assert getattr(s, f'ax{num}_max').boundary_type == 'reflective' + + # Check bounding box + ll, ur = (+s).bounding_box + assert np.all(np.isinf(ll)) + assert np.all(np.isinf(ur)) + ll, ur = (-s).bounding_box + assert ll[2] == pytest.approx(-2.5) + assert ur[2] == pytest.approx(2.5) + + # __contains__ on associated half-spaces + assert (0., 0., 0.) in -s + assert (-2., 0., 0.) not in -s + assert (0., 0.9, 0.) in -s + assert (0., 0., -3.) in +s + assert (0., 0., 3.) in +s + + # translate method + s_t = s.translate((1., 1., 0.)) + assert (-0.01, 0., 0.) in +s_t + assert (0.01, 0., 0.) in -s_t + + # Make sure repr works + repr(s) + + # Version with infinite 3rd dimension + s = openmc.model.OrthogonalBox(v, a1, a2) + assert not hasattr(s, 'ax3_min') + assert not hasattr(s, 'ax3_max') + ll, ur = (-s).bounding_box + assert np.all(np.isinf(ll)) + assert np.all(np.isinf(ur)) + assert (0., 0., 0.) in -s + assert (-2., 0., 0.) not in -s + assert (0., 0.9, 0.) in -s + assert (0., 0., -3.) not in +s + assert (0., 0., 3.) not in +s From 5bc04b5d78b83684685ccf53564498493e2b6a93 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Fri, 23 Aug 2024 11:56:26 -0500 Subject: [PATCH 057/184] Alternative Random Ray Volume Estimators (#3060) Co-authored-by: Olek <45364492+yardasol@users.noreply.github.com> --- docs/source/methods/random_ray.rst | 126 +++++-- docs/source/usersguide/random_ray.rst | 58 +++ include/openmc/constants.h | 1 + .../openmc/random_ray/flat_source_domain.h | 19 +- .../openmc/random_ray/linear_source_domain.h | 9 +- include/openmc/random_ray/random_ray.h | 2 + .../openmc/random_ray/random_ray_simulation.h | 1 + openmc/settings.py | 10 + src/random_ray/flat_source_domain.cpp | 169 ++++++--- src/random_ray/linear_source_domain.cpp | 82 ++--- src/random_ray/random_ray.cpp | 12 - src/random_ray/random_ray_simulation.cpp | 18 +- src/settings.cpp | 14 + .../cell/results_true.dat | 12 +- .../material/results_true.dat | 12 +- .../universe/results_true.dat | 12 +- .../linear/inputs_true.dat | 4 +- .../linear/results_true.dat | 12 +- .../linear_xy/inputs_true.dat | 4 +- .../linear_xy/results_true.dat | 12 +- .../random_ray_fixed_source_linear/test.py | 4 +- .../False/results_true.dat | 12 +- .../True/results_true.dat | 12 +- .../flat/results_true.dat | 168 ++++----- .../linear_xy/results_true.dat | 168 ++++----- .../random_ray_k_eff/results_true.dat | 124 +++---- .../random_ray_linear/linear/inputs_true.dat | 4 +- .../random_ray_linear/linear/results_true.dat | 338 +++++++++--------- .../linear_xy/inputs_true.dat | 4 +- .../linear_xy/results_true.dat | 338 +++++++++--------- .../random_ray_linear/test.py | 4 +- .../random_ray_volume_estimator/__init__.py | 0 .../hybrid/inputs_true.dat | 245 +++++++++++++ .../hybrid/results_true.dat | 9 + .../naive/inputs_true.dat | 245 +++++++++++++ .../naive/results_true.dat | 9 + .../simulation_averaged/inputs_true.dat | 245 +++++++++++++ .../simulation_averaged/results_true.dat | 9 + .../random_ray_volume_estimator/test.py | 30 ++ .../__init__.py | 0 .../hybrid/inputs_true.dat | 246 +++++++++++++ .../hybrid/results_true.dat | 9 + .../naive/inputs_true.dat | 246 +++++++++++++ .../naive/results_true.dat | 9 + .../simulation_averaged/inputs_true.dat | 246 +++++++++++++ .../simulation_averaged/results_true.dat | 9 + .../test.py | 32 ++ 47 files changed, 2572 insertions(+), 782 deletions(-) create mode 100644 tests/regression_tests/random_ray_volume_estimator/__init__.py create mode 100644 tests/regression_tests/random_ray_volume_estimator/hybrid/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator/hybrid/results_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator/naive/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator/naive/results_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator/simulation_averaged/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator/simulation_averaged/results_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator/test.py create mode 100644 tests/regression_tests/random_ray_volume_estimator_linear/__init__.py create mode 100644 tests/regression_tests/random_ray_volume_estimator_linear/hybrid/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator_linear/hybrid/results_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator_linear/naive/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator_linear/naive/results_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/results_true.dat create mode 100644 tests/regression_tests/random_ray_volume_estimator_linear/test.py diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst index 9f8eb84d80e..3d98747e4af 100644 --- a/docs/source/methods/random_ray.rst +++ b/docs/source/methods/random_ray.rst @@ -411,7 +411,7 @@ which when partially simplified becomes: Note that there are now four (seemingly identical) volume terms in this equation. -.. _methods-volume-dilemma: +.. _methods_random_ray_vol: ~~~~~~~~~~~~~~ Volume Dilemma @@ -440,9 +440,11 @@ features stochastic variables (the sums over random ray lengths and angular fluxes) in both the numerator and denominator, making it a stochastic ratio estimator, which is inherently biased. In practice, usage of the naive estimator does result in a biased, but "consistent" estimator (i.e., it is biased, but -the bias tends towards zero as the sample size increases). Experimentally, the -right answer can be obtained with this estimator, though a very fine ray density -is required to eliminate the bias. +the bias tends towards zero as the sample size increases). Empirically, this +bias tends to effect eigenvalue calculations much more significantly than in +fixed source simulations. Experimentally, the right answer can be obtained with +this estimator, though for eigenvalue simulations a very fine ray density is +required to eliminate the bias. How might we solve the biased ratio estimator problem? While there is no obvious way to alter the numerator term (which arises from the characteristic @@ -463,17 +465,17 @@ replace the actual tracklength that was accumulated inside that FSR each iteration with the expected value. If we know the analytical volumes, then those can be used to directly compute -the expected value of the tracklength in each cell. However, as the analytical -volumes are not typically known in OpenMC due to the usage of user-defined -constructive solid geometry, we need to source this quantity from elsewhere. An -obvious choice is to simply accumulate the total tracklength through each FSR -across all iterations (batches) and to use that sum to compute the expected -average length per iteration, as: +the expected value of the tracklength in each cell, :math:`L_{avg}`. However, as +the analytical volumes are not typically known in OpenMC due to the usage of +user-defined constructive solid geometry, we need to source this quantity from +elsewhere. An obvious choice is to simply accumulate the total tracklength +through each FSR across all iterations (batches) and to use that sum to compute +the expected average length per iteration, as: .. math:: - :label: sim_estimator + :label: L_avg - \sum\limits^{}_{i} \ell_i \approx \frac{\sum\limits^{B}_{b}\sum\limits^{N_i}_{r} \ell_{b,r} }{B} + \sum\limits^{}_{i} \ell_i \approx L_{avg} = \frac{\sum\limits^{B}_{b}\sum\limits^{N_i}_{r=1} \ell_{b,r} }{B} where :math:`b` is a single batch in :math:`B` total batches simulated so far. @@ -486,7 +488,7 @@ averaged" estimator is therefore: .. math:: :label: phi_sim - \phi_{i,g}^{simulation} = \frac{Q_{i,g} }{\Sigma_{t,i,g}} + \frac{\sum\limits_{r=1}^{N_i} \Delta \psi_{r,g}}{\Sigma_{t,i,g} \frac{\sum\limits^{B}_{b}\sum\limits^{N_i}_{r} \ell_{b,r} }{B}} + \phi_{i,g}^{simulation} = \frac{Q_{i,g} }{\Sigma_{t,i,g}} + \frac{\sum\limits_{r=1}^{N_i} \Delta \psi_{r,g}}{\Sigma_{t,i,g} L_{avg}} In practical terms, the "simulation averaged" estimator is virtually indistinguishable numerically from use of the true analytical volume to estimate @@ -500,17 +502,81 @@ in which case the denominator served as a normalization term for the numerator integral in Equation :eq:`integral`. Essentially, we have now used a different term for the volume in the numerator as compared to the normalizing volume in the denominator. The inevitable mismatch (due to noise) between these two -quantities results in a significant increase in variance. Notably, the same -problem occurs if using a tracklength estimate based on the analytical volume, -as again the numerator integral and the normalizing denominator integral no -longer match on a per-iteration basis. - -In practice, the simulation averaged method does completely remove the bias, -though at the cost of a notable increase in variance. Empirical testing reveals -that on most problems, the simulation averaged estimator does win out overall in -numerical performance, as a much coarser quadrature can be used resulting in -faster runtimes overall. Thus, OpenMC uses the simulation averaged estimator in -its random ray mode. +quantities results in a significant increase in variance, and can even result in +the generation of negative fluxes. Notably, the same problem occurs if using a +tracklength estimate based on the analytical volume, as again the numerator +integral and the normalizing denominator integral no longer match on a +per-iteration basis. + +In practice, the simulation averaged method does completely remove the bias seen +when using the naive estimator, though at the cost of a notable increase in +variance. Empirical testing reveals that on most eigenvalue problems, the +simulation averaged estimator does win out overall in numerical performance, as +a much coarser quadrature can be used resulting in faster runtimes overall. +Thus, OpenMC uses the simulation averaged estimator as default in its random ray +mode for eigenvalue solves. + +OpenMC also features a "hybrid" volume estimator that uses the naive estimator +for all regions containing an external (fixed) source term. For all other +source regions, the "simulation averaged" estimator is used. This typically achieves +a best of both worlds result, with the benefits of the low bias simulation averaged +estimator in most regions, while preventing instability and/or large biases in regions +with external source terms via use of the naive estimator. In general, it is +recommended to use the "hybrid" estimator, which is the default method used +in OpenMC. If instability is encountered despite high ray densities, then +the naive estimator may be preferable. + +A table that summarizes the pros and cons, as well as recommendations for +different use cases, is given in the :ref:`volume +estimators` section of the user guide. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +What Happens When a Source Region is Missed? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Given the stochastic nature of random ray, when low ray densities are used it is +common for small source regions to occasionally not be hit by any rays in a +particular power iteration :math:`n`. This naturally collapses the flux estimate +in that cell for the iteration from Equation :eq:`phi_naive` to: + +.. math:: + :label: phi_missed_one + + \phi_{i,g,n}^{missed} = \frac{Q_{i,g,n} }{\Sigma_{t,i,g}} + +as the streaming operator has gone to zero. While this is obviously innacurate +as it ignores transport, for most problems where the region is only occasionally +missed this estimator does not tend to introduce any significant bias. + +However, in cases where the total cross section in the region is very small +(e.g., a void-like material) and where a strong external fixed source has been +placed, then this treatment causes major issues. In this pathological case, the +lack of transport forces the entirety of the fixed source to effectively be +contained and collided within the cell, which for a low cross section region is +highly unphysical. The net effect is that a very high estimate of the flux +(often orders of magnitude higher than is expected) is generated that iteration, +which cannot be washed out even with hundreds or thousands of iterations. Thus, +huge biases are often seen in spatial tallies containing void-like regions with +external sources unless a high enough ray density is used such that all source +regions are always hit each iteration. This is particularly problematic as +external sources placed in void-like regions are very common in many types of +fixed source analysis. + +For regions where external sources are present, to eliminate this bias it is +therefore preferable to simply use the previous iteration's estimate of the flux +in that cell, as: + +.. math:: + :label: phi_missed_two + + \phi_{i,g,n}^{missed} = \phi_{i,g,n-1} . + +When linear sources are present, the flux moments from the previous iteration +are used in the same manner. While this introduces some small degree of +correlation to the simulation, for miss rates on the order of a few percent the +correlations are trivial and the bias is eliminated. Thus, in OpenMC the +previous iteration's scalar flux estimate is applied to cells that are missed +where there is an external source term present within the cell. ~~~~~~~~~~~~~~~ Power Iteration @@ -563,15 +629,15 @@ total spatial- and energy-integrated fission rate :math:`F^{n-1}` in iteration Notably, the volume term :math:`V_i` appears in the eigenvalue update equation. The same logic applies to the treatment of this term as was discussed earlier. -In OpenMC, we use the "simulation averaged" volume derived from summing over all -ray tracklength contributions to a FSR over all iterations and dividing by the -total integration tracklength to date. Thus, Equation :eq:`fission_source` -becomes: +In OpenMC, we use the "simulation averaged" volume (Equation :eq:`L_avg`) +derived from summing over all ray tracklength contributions to a FSR over all +iterations and dividing by the total integration tracklength to date. Thus, +Equation :eq:`fission_source` becomes: .. math:: :label: fission_source_volumed - F^n = \sum\limits^{M}_{i} \left( \frac{\sum\limits^{B}_{b}\sum\limits^{N_i}_{r} \ell_{b,r} }{B} \sum\limits^{G}_{g} \nu \Sigma_f(i, g) \phi^{n}(g) \right) + F^n = \sum\limits^{M}_{i} \left( L_{avg} \sum\limits^{G}_{g} \nu \Sigma_f(i, g) \phi^{n}(g) \right) and a similar substitution can be made to update Equation :eq:`fission_source_prev` . In OpenMC, the most up-to-date version of the volume @@ -965,7 +1031,7 @@ The Shannon entropy is then computed normally as where :math:`N` is the number of FSRs. FSRs with no fission source (or, occassionally, negative fission source, :ref:`due to the volume estimator -problem `) are skipped to avoid taking an undefined +problem `) are skipped to avoid taking an undefined logarithm in :eq:`shannon-entropy-random-ray`. .. _usersguide_fixed_source_methods: diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index 117d5e23fb5..5ca0ab6bed9 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -535,6 +535,64 @@ points of 1.0e-2 and 1.0e1. # Add fixed source and ray sampling source to settings file settings.source = [neutron_source] +.. _usersguide_vol_estimators: + +----------------------------- +Alternative Volume Estimators +----------------------------- + +As discussed in the random ray theory section on :ref:`volume estimators +`, there are several possible derivations for the scalar +flux estimate. These options deal with different ways of treating the +accumulation over ray lengths crossing each FSR (a quantity directly +proportional to volume), which can be computed using several methods. The +following methods are currently available in OpenMC: + +.. list-table:: Comparison of Estimators + :header-rows: 1 + :widths: 10 30 30 30 + + * - Estimator + - Description + - Pros + - Cons + * - ``simulation_averaged`` + - Accumulates total active ray lengths in each FSR over all iterations, + improving the estimate of the volume in each cell each iteration. + - * Virtually unbiased after several iterations + * Asymptotically approaches the true analytical volume + * Typically most efficient in terms of speed vs. accuracy + - * Higher variance + * Can lead to negative fluxes and numerical instability in pathological + cases + * - ``naive`` + - Treats the volume as composed only of the active ray length through each + FSR per iteration, being a biased but numerically consistent ratio + estimator. + - * Low variance + * Unlikely to result in negative fluxes + * Recommended in cases where the simulation averaged estimator is + unstable + - * Biased estimator + * Requires more rays or longer active ray length to mitigate bias + * - ``hybrid`` (default) + - Applies the naive estimator to all cells that contain an external (fixed) + source contribution. Applies the simulation averaged estimator to all + other cells. + - * High accuracy/low bias of the simulation averaged estimator in most + cells + * Stability of the naive estimator in cells with fixed sources + - * Can lead to slightly negative fluxes in cells where the simulation + averaged estimator is used + +These estimators can be selected by setting the ``volume_estimator`` field in the +:attr:`openmc.Settings.random_ray` dictionary. For example, to use the naive +estimator, the following code would be used: + +:: + + settings.random_ray['volume_estimator'] = 'naive' + --------------------------------------- Putting it All Together: Example Inputs --------------------------------------- diff --git a/include/openmc/constants.h b/include/openmc/constants.h index e502506c91e..605ae1839d8 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -342,6 +342,7 @@ enum class RunMode { enum class SolverType { MONTE_CARLO, RANDOM_RAY }; +enum class RandomRayVolumeEstimator { NAIVE, SIMULATION_AVERAGED, HYBRID }; enum class RandomRaySourceShape { FLAT, LINEAR, LINEAR_XY }; //============================================================================== diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index 33c5661dcd4..5c50f7fb0a0 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -1,6 +1,7 @@ #ifndef OPENMC_RANDOM_RAY_FLAT_SOURCE_DOMAIN_H #define OPENMC_RANDOM_RAY_FLAT_SOURCE_DOMAIN_H +#include "openmc/constants.h" #include "openmc/openmp_interface.h" #include "openmc/position.h" #include "openmc/source.h" @@ -99,7 +100,8 @@ class FlatSourceDomain { double compute_k_eff(double k_eff_old) const; virtual void normalize_scalar_flux_and_volumes( double total_active_distance_per_iteration); - virtual int64_t add_source_to_scalar_flux(); + + int64_t add_source_to_scalar_flux(); virtual void batch_reset(); void convert_source_regions_to_tallies(); void reset_tally_volumes(); @@ -117,6 +119,10 @@ class FlatSourceDomain { // Static Data members static bool volume_normalized_flux_tallies_; + //---------------------------------------------------------------------------- + // Static data members + static RandomRayVolumeEstimator volume_estimator_; + //---------------------------------------------------------------------------- // Public Data members @@ -132,7 +138,6 @@ class FlatSourceDomain { // 1D arrays representing values for all source regions vector lock_; - vector was_hit_; vector volume_; vector volume_t_; vector position_recorded_; @@ -140,10 +145,11 @@ class FlatSourceDomain { // 2D arrays stored in 1D representing values for all source regions x energy // groups - vector scalar_flux_old_; - vector scalar_flux_new_; + vector scalar_flux_old_; + vector scalar_flux_new_; vector source_; vector external_source_; + vector external_source_present_; protected: //---------------------------------------------------------------------------- @@ -155,6 +161,10 @@ class FlatSourceDomain { const vector& instances); void apply_external_source_to_cell_and_children(int32_t i_cell, Discrete* discrete, double strength_factor, int32_t target_material_id); + virtual void set_flux_to_flux_plus_source( + int64_t idx, double volume, int material, int g); + void set_flux_to_source(int64_t idx); + virtual void set_flux_to_old_flux(int64_t idx); //---------------------------------------------------------------------------- // Private data members @@ -178,6 +188,7 @@ class FlatSourceDomain { // 1D arrays representing values for all source regions vector material_; + vector volume_naive_; // 2D arrays stored in 1D representing values for all source regions x energy // groups diff --git a/include/openmc/random_ray/linear_source_domain.h b/include/openmc/random_ray/linear_source_domain.h index 5010ffddd6f..4812d14337c 100644 --- a/include/openmc/random_ray/linear_source_domain.h +++ b/include/openmc/random_ray/linear_source_domain.h @@ -28,7 +28,7 @@ class LinearSourceDomain : public FlatSourceDomain { double compute_k_eff(double k_eff_old) const; void normalize_scalar_flux_and_volumes( double total_active_distance_per_iteration) override; - int64_t add_source_to_scalar_flux() override; + void batch_reset() override; void convert_source_regions_to_tallies(); void reset_tally_volumes(); @@ -54,6 +54,13 @@ class LinearSourceDomain : public FlatSourceDomain { vector mom_matrix_; vector mom_matrix_t_; +protected: + //---------------------------------------------------------------------------- + // Methods + void set_flux_to_flux_plus_source( + int64_t idx, double volume, int material, int g) override; + void set_flux_to_old_flux(int64_t idx) override; + }; // class LinearSourceDomain } // namespace openmc diff --git a/include/openmc/random_ray/random_ray.h b/include/openmc/random_ray/random_ray.h index 913a9af4a75..96c38da7b1c 100644 --- a/include/openmc/random_ray/random_ray.h +++ b/include/openmc/random_ray/random_ray.h @@ -43,6 +43,8 @@ class RandomRay : public Particle { // Public data members vector angular_flux_; + bool ray_trace_only_ {false}; // If true, only perform geometry operations + private: //---------------------------------------------------------------------------- // Private data members diff --git a/include/openmc/random_ray/random_ray_simulation.h b/include/openmc/random_ray/random_ray_simulation.h index c1d47821d7a..55bac6905c6 100644 --- a/include/openmc/random_ray/random_ray_simulation.h +++ b/include/openmc/random_ray/random_ray_simulation.h @@ -19,6 +19,7 @@ class RandomRaySimulation { //---------------------------------------------------------------------------- // Methods + void compute_segment_correction_factors(); void simulate(); void reduce_simulation_statistics(); void output_simulation_results() const; diff --git a/openmc/settings.py b/openmc/settings.py index ce97f138f24..ddaac040191 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -155,6 +155,10 @@ class Settings: :ray_source: Starting ray distribution (must be uniform in space and angle) as specified by a :class:`openmc.SourceBase` object. + :volume_estimator: + Choice of volume estimator for the random ray solver. Options are + 'naive', 'simulation_averaged', or 'hybrid'. + The default is 'hybrid'. :source_shape: Assumed shape of the source distribution within each source region. Options are 'flat' (default), 'linear', or 'linear_xy'. @@ -1091,6 +1095,10 @@ def random_ray(self, random_ray: dict): random_ray[key], 0.0, True) elif key == 'ray_source': cv.check_type('random ray source', random_ray[key], SourceBase) + elif key == 'volume_estimator': + cv.check_value('volume estimator', random_ray[key], + ('naive', 'simulation_averaged', + 'hybrid')) elif key == 'source_shape': cv.check_value('source shape', random_ray[key], ('flat', 'linear', 'linear_xy')) @@ -1889,6 +1897,8 @@ def _random_ray_from_xml_element(self, root): elif child.tag == 'source': source = SourceBase.from_xml_element(child) self.random_ray['ray_source'] = source + elif child.tag == 'volume_estimator': + self.random_ray['volume_estimator'] = child.text elif child.tag == 'source_shape': self.random_ray['source_shape'] = child.text elif child.tag == 'volume_normalized_flux_tallies': diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 584b3a7edb4..62768b55f0f 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -24,6 +24,8 @@ namespace openmc { //============================================================================== // Static Variable Declarations +RandomRayVolumeEstimator FlatSourceDomain::volume_estimator_ { + RandomRayVolumeEstimator::HYBRID}; bool FlatSourceDomain::volume_normalized_flux_tallies_ {false}; FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) @@ -49,13 +51,13 @@ FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) position_.resize(n_source_regions_); volume_.assign(n_source_regions_, 0.0); volume_t_.assign(n_source_regions_, 0.0); - was_hit_.assign(n_source_regions_, 0); + volume_naive_.assign(n_source_regions_, 0.0); // Initialize element-wise arrays scalar_flux_new_.assign(n_source_elements_, 0.0); scalar_flux_final_.assign(n_source_elements_, 0.0); source_.resize(n_source_elements_); - external_source_.assign(n_source_elements_, 0.0); + tally_task_.resize(n_source_elements_); volume_task_.resize(n_source_regions_); @@ -64,7 +66,10 @@ FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) scalar_flux_old_.assign(n_source_elements_, 1.0); } else { // If in fixed source mode, set starting flux to guess of zero + // and initialize external source arrays scalar_flux_old_.assign(n_source_elements_, 0.0); + external_source_.assign(n_source_elements_, 0.0); + external_source_present_.assign(n_source_regions_, false); } // Initialize material array @@ -109,9 +114,8 @@ void FlatSourceDomain::batch_reset() { // Reset scalar fluxes, iteration volume tallies, and region hit flags to // zero - parallel_fill(scalar_flux_new_, 0.0f); + parallel_fill(scalar_flux_new_, 0.0); parallel_fill(volume_, 0.0); - parallel_fill(was_hit_, 0); } void FlatSourceDomain::accumulate_iteration_flux() @@ -142,14 +146,14 @@ void FlatSourceDomain::update_neutron_source(double k_eff) int material = material_[sr]; for (int e_out = 0; e_out < negroups_; e_out++) { - float sigma_t = data::mg.macro_xs_[material].get_xs( + double sigma_t = data::mg.macro_xs_[material].get_xs( MgxsType::TOTAL, e_out, nullptr, nullptr, nullptr, t, a); - float scatter_source = 0.0f; + double scatter_source = 0.0f; for (int e_in = 0; e_in < negroups_; e_in++) { - float scalar_flux = scalar_flux_old_[sr * negroups_ + e_in]; + double scalar_flux = scalar_flux_old_[sr * negroups_ + e_in]; - float sigma_s = data::mg.macro_xs_[material].get_xs( + double sigma_s = data::mg.macro_xs_[material].get_xs( MgxsType::NU_SCATTER, e_in, &e_out, nullptr, nullptr, t, a); scatter_source += sigma_s * scalar_flux; } @@ -164,15 +168,15 @@ void FlatSourceDomain::update_neutron_source(double k_eff) int material = material_[sr]; for (int e_out = 0; e_out < negroups_; e_out++) { - float sigma_t = data::mg.macro_xs_[material].get_xs( + double sigma_t = data::mg.macro_xs_[material].get_xs( MgxsType::TOTAL, e_out, nullptr, nullptr, nullptr, t, a); - float fission_source = 0.0f; + double fission_source = 0.0f; for (int e_in = 0; e_in < negroups_; e_in++) { - float scalar_flux = scalar_flux_old_[sr * negroups_ + e_in]; - float nu_sigma_f = data::mg.macro_xs_[material].get_xs( + double scalar_flux = scalar_flux_old_[sr * negroups_ + e_in]; + double nu_sigma_f = data::mg.macro_xs_[material].get_xs( MgxsType::NU_FISSION, e_in, nullptr, nullptr, nullptr, t, a); - float chi = data::mg.macro_xs_[material].get_xs( + double chi = data::mg.macro_xs_[material].get_xs( MgxsType::CHI_PROMPT, e_in, &e_out, nullptr, nullptr, t, a); fission_source += nu_sigma_f * scalar_flux * chi; } @@ -196,7 +200,7 @@ void FlatSourceDomain::update_neutron_source(double k_eff) void FlatSourceDomain::normalize_scalar_flux_and_volumes( double total_active_distance_per_iteration) { - float normalization_factor = 1.0 / total_active_distance_per_iteration; + double normalization_factor = 1.0 / total_active_distance_per_iteration; double volume_normalization_factor = 1.0 / (total_active_distance_per_iteration * simulation::current_batch); @@ -211,16 +215,14 @@ void FlatSourceDomain::normalize_scalar_flux_and_volumes( #pragma omp parallel for for (int64_t sr = 0; sr < n_source_regions_; sr++) { volume_t_[sr] += volume_[sr]; + volume_naive_[sr] = volume_[sr] * normalization_factor; volume_[sr] = volume_t_[sr] * volume_normalization_factor; } } -// Combine transport flux contributions and flat source contributions from the -// previous iteration to generate this iteration's estimate of scalar flux. -int64_t FlatSourceDomain::add_source_to_scalar_flux() +void FlatSourceDomain::set_flux_to_flux_plus_source( + int64_t idx, double volume, int material, int g) { - int64_t n_hits = 0; - // Temperature and angle indices, if using multiple temperature // data sets and/or anisotropic data sets. // TODO: Currently assumes we are only using single temp/single @@ -228,41 +230,99 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() const int t = 0; const int a = 0; + double sigma_t = data::mg.macro_xs_[material].get_xs( + MgxsType::TOTAL, g, nullptr, nullptr, nullptr, t, a); + + scalar_flux_new_[idx] /= (sigma_t * volume); + scalar_flux_new_[idx] += source_[idx]; +} + +void FlatSourceDomain::set_flux_to_old_flux(int64_t idx) +{ + scalar_flux_new_[idx] = scalar_flux_old_[idx]; +} + +void FlatSourceDomain::set_flux_to_source(int64_t idx) +{ + scalar_flux_new_[idx] = source_[idx]; +} + +// Combine transport flux contributions and flat source contributions from the +// previous iteration to generate this iteration's estimate of scalar flux. +int64_t FlatSourceDomain::add_source_to_scalar_flux() +{ + int64_t n_hits = 0; + #pragma omp parallel for reduction(+ : n_hits) for (int sr = 0; sr < n_source_regions_; sr++) { - // Check if this cell was hit this iteration - int was_cell_hit = was_hit_[sr]; - if (was_cell_hit) { + double volume_simulation_avg = volume_[sr]; + double volume_iteration = volume_naive_[sr]; + + // Increment the number of hits if cell was hit this iteration + if (volume_iteration) { n_hits++; } - double volume = volume_[sr]; + // Check if an external source is present in this source region + bool external_source_present = + external_source_present_.size() && external_source_present_[sr]; + + // The volume treatment depends on the volume estimator type + // and whether or not an external source is present in the cell. + double volume; + switch (volume_estimator_) { + case RandomRayVolumeEstimator::NAIVE: + volume = volume_iteration; + break; + case RandomRayVolumeEstimator::SIMULATION_AVERAGED: + volume = volume_simulation_avg; + break; + case RandomRayVolumeEstimator::HYBRID: + if (external_source_present) { + volume = volume_iteration; + } else { + volume = volume_simulation_avg; + } + break; + default: + fatal_error("Invalid volume estimator type"); + } + int material = material_[sr]; for (int g = 0; g < negroups_; g++) { int64_t idx = (sr * negroups_) + g; // There are three scenarios we need to consider: - if (was_cell_hit) { + if (volume_iteration > 0.0) { // 1. If the FSR was hit this iteration, then the new flux is equal to // the flat source from the previous iteration plus the contributions // from rays passing through the source region (computed during the // transport sweep) - float sigma_t = data::mg.macro_xs_[material].get_xs( - MgxsType::TOTAL, g, nullptr, nullptr, nullptr, t, a); - scalar_flux_new_[idx] /= (sigma_t * volume); - scalar_flux_new_[idx] += source_[idx]; - } else if (volume > 0.0) { + set_flux_to_flux_plus_source(idx, volume, material, g); + } else if (volume_simulation_avg > 0.0) { // 2. If the FSR was not hit this iteration, but has been hit some - // previous iteration, then we simply set the new scalar flux to be - // equal to the contribution from the flat source alone. - scalar_flux_new_[idx] = source_[idx]; - } else { - // If the FSR was not hit this iteration, and it has never been hit in - // any iteration (i.e., volume is zero), then we want to set this to 0 - // to avoid dividing anything by a zero volume. - scalar_flux_new_[idx] = 0.0f; + // previous iteration, then we need to make a choice about what + // to do. Naively we will usually want to set the flux to be equal + // to the reduced source. However, in fixed source problems where + // there is a strong external source present in the cell, and where + // the cell has a very low cross section, this approximation will + // cause a huge upward bias in the flux estimate of the cell (in these + // conditions, the flux estimate can be orders of magnitude too large). + // Thus, to avoid this bias, if any external source is present + // in the cell we will use the previous iteration's flux estimate. This + // injects a small degree of correlation into the simulation, but this + // is going to be trivial when the miss rate is a few percent or less. + if (external_source_present) { + set_flux_to_old_flux(idx); + } else { + set_flux_to_source(idx); + } } + // If the FSR was not hit this iteration, and it has never been hit in + // any iteration (i.e., volume is zero), then we want to set this to 0 + // to avoid dividing anything by a zero volume. This happens implicitly + // given that the new scalar flux arrays are set to zero each iteration. } } @@ -482,7 +542,8 @@ void FlatSourceDomain::reset_tally_volumes() // reported scalar fluxes are in units per source neutron. This allows for // direct comparison of reported tallies to Monte Carlo flux results. // This factor needs to be computed at each iteration, as it is based on the -// volume estimate of each FSR, which improves over the course of the simulation +// volume estimate of each FSR, which improves over the course of the +// simulation double FlatSourceDomain::compute_fixed_source_normalization_factor() const { // If we are not in fixed source mode, then there are no external sources @@ -505,7 +566,7 @@ double FlatSourceDomain::compute_fixed_source_normalization_factor() const // angle data. const int t = 0; const int a = 0; - float sigma_t = data::mg.macro_xs_[material].get_xs( + double sigma_t = data::mg.macro_xs_[material].get_xs( MgxsType::TOTAL, e, nullptr, nullptr, nullptr, t, a); simulation_external_source_strength += external_source_[sr * negroups_ + e] * sigma_t * volume; @@ -559,12 +620,13 @@ void FlatSourceDomain::random_ray_tally() for (int sr = 0; sr < n_source_regions_; sr++) { // The fsr.volume_ is the unitless fractional simulation averaged volume // (i.e., it is the FSR's fraction of the overall simulation volume). The - // simulation_volume_ is the total 3D physical volume in cm^3 of the entire - // global simulation domain (as defined by the ray source box). Thus, the - // FSR's true 3D spatial volume in cm^3 is found by multiplying its fraction - // of the total volume by the total volume. Not important in eigenvalue - // solves, but useful in fixed source solves for returning the flux shape - // with a magnitude that makes sense relative to the fixed source strength. + // simulation_volume_ is the total 3D physical volume in cm^3 of the + // entire global simulation domain (as defined by the ray source box). + // Thus, the FSR's true 3D spatial volume in cm^3 is found by multiplying + // its fraction of the total volume by the total volume. Not important in + // eigenvalue solves, but useful in fixed source solves for returning the + // flux shape with a magnitude that makes sense relative to the fixed + // source strength. double volume = volume_[sr] * simulation_volume_; double material = material_[sr]; @@ -741,11 +803,8 @@ void FlatSourceDomain::all_reduce_replicated_source_regions() MPI_Allreduce(MPI_IN_PLACE, volume_.data(), n_source_regions_, MPI_DOUBLE, MPI_SUM, mpi::intracomm); - MPI_Allreduce(MPI_IN_PLACE, was_hit_.data(), n_source_regions_, MPI_INT, - MPI_SUM, mpi::intracomm); - MPI_Allreduce(MPI_IN_PLACE, scalar_flux_new_.data(), n_source_elements_, - MPI_FLOAT, MPI_SUM, mpi::intracomm); + MPI_DOUBLE, MPI_SUM, mpi::intracomm); simulation::time_bank_sendrecv.stop(); #endif @@ -913,6 +972,8 @@ void FlatSourceDomain::output_to_vtk() const void FlatSourceDomain::apply_external_source_to_source_region( Discrete* discrete, double strength_factor, int64_t source_region) { + external_source_present_[source_region] = true; + const auto& discrete_energies = discrete->x(); const auto& discrete_probs = discrete->prob(); @@ -968,14 +1029,10 @@ void FlatSourceDomain::apply_external_source_to_cell_and_children( void FlatSourceDomain::count_external_source_regions() { + n_external_source_regions_ = 0; #pragma omp parallel for reduction(+ : n_external_source_regions_) for (int sr = 0; sr < n_source_regions_; sr++) { - float total = 0.f; - for (int e = 0; e < negroups_; e++) { - int64_t se = sr * negroups_ + e; - total += external_source_[se]; - } - if (total != 0.f) { + if (external_source_present_[sr]) { n_external_source_regions_++; } } @@ -1029,7 +1086,7 @@ void FlatSourceDomain::convert_external_sources() for (int sr = 0; sr < n_source_regions_; sr++) { int material = material_[sr]; for (int e = 0; e < negroups_; e++) { - float sigma_t = data::mg.macro_xs_[material].get_xs( + double sigma_t = data::mg.macro_xs_[material].get_xs( MgxsType::TOTAL, e, nullptr, nullptr, nullptr, t, a); external_source_[sr * negroups_ + e] /= sigma_t; } diff --git a/src/random_ray/linear_source_domain.cpp b/src/random_ray/linear_source_domain.cpp index 1603ec24a4e..5c3fa91c182 100644 --- a/src/random_ray/linear_source_domain.cpp +++ b/src/random_ray/linear_source_domain.cpp @@ -70,25 +70,25 @@ void LinearSourceDomain::update_neutron_source(double k_eff) MomentMatrix invM = mom_matrix_[sr].inverse(); for (int e_out = 0; e_out < negroups_; e_out++) { - float sigma_t = data::mg.macro_xs_[material].get_xs( + double sigma_t = data::mg.macro_xs_[material].get_xs( MgxsType::TOTAL, e_out, nullptr, nullptr, nullptr, t, a); - float scatter_flat = 0.0f; - float fission_flat = 0.0f; + double scatter_flat = 0.0f; + double fission_flat = 0.0f; MomentArray scatter_linear = {0.0, 0.0, 0.0}; MomentArray fission_linear = {0.0, 0.0, 0.0}; for (int e_in = 0; e_in < negroups_; e_in++) { // Handles for the flat and linear components of the flux - float flux_flat = scalar_flux_old_[sr * negroups_ + e_in]; + double flux_flat = scalar_flux_old_[sr * negroups_ + e_in]; MomentArray flux_linear = flux_moments_old_[sr * negroups_ + e_in]; // Handles for cross sections - float sigma_s = data::mg.macro_xs_[material].get_xs( + double sigma_s = data::mg.macro_xs_[material].get_xs( MgxsType::NU_SCATTER, e_in, &e_out, nullptr, nullptr, t, a); - float nu_sigma_f = data::mg.macro_xs_[material].get_xs( + double nu_sigma_f = data::mg.macro_xs_[material].get_xs( MgxsType::NU_FISSION, e_in, nullptr, nullptr, nullptr, t, a); - float chi = data::mg.macro_xs_[material].get_xs( + double chi = data::mg.macro_xs_[material].get_xs( MgxsType::CHI_PROMPT, e_in, &e_out, nullptr, nullptr, t, a); // Compute source terms for flat and linear components of the flux @@ -103,7 +103,10 @@ void LinearSourceDomain::update_neutron_source(double k_eff) (scatter_flat + fission_flat * inverse_k_eff) / sigma_t; // Compute the linear source terms - if (simulation::current_batch > 2) { + // In the first 10 iterations when the centroids and spatial moments + // are not well known, we will leave the source gradients as zero + // so as to avoid causing any numerical instability. + if (simulation::current_batch > 10) { source_gradients_[sr * negroups_ + e_out] = invM * ((scatter_linear + fission_linear * inverse_k_eff) / sigma_t); } @@ -124,7 +127,7 @@ void LinearSourceDomain::update_neutron_source(double k_eff) void LinearSourceDomain::normalize_scalar_flux_and_volumes( double total_active_distance_per_iteration) { - float normalization_factor = 1.0 / total_active_distance_per_iteration; + double normalization_factor = 1.0 / total_active_distance_per_iteration; double volume_normalization_factor = 1.0 / (total_active_distance_per_iteration * simulation::current_batch); @@ -142,6 +145,7 @@ void LinearSourceDomain::normalize_scalar_flux_and_volumes( centroid_t_[sr] += centroid_iteration_[sr]; mom_matrix_t_[sr] += mom_matrix_[sr]; volume_t_[sr] += volume_[sr]; + volume_naive_[sr] = volume_[sr] * normalization_factor; volume_[sr] = volume_t_[sr] * volume_normalization_factor; if (volume_t_[sr] > 0.0) { double inv_volume = 1.0 / volume_t_[sr]; @@ -153,56 +157,18 @@ void LinearSourceDomain::normalize_scalar_flux_and_volumes( } } -int64_t LinearSourceDomain::add_source_to_scalar_flux() +void LinearSourceDomain::set_flux_to_flux_plus_source( + int64_t idx, double volume, int material, int g) { - int64_t n_hits = 0; - - // Temperature and angle indices, if using multiple temperature - // data sets and/or anisotropic data sets. - // TODO: Currently assumes we are only using single temp/single - // angle data. - const int t = 0; - const int a = 0; - -#pragma omp parallel for reduction(+ : n_hits) - for (int sr = 0; sr < n_source_regions_; sr++) { - - double volume = volume_[sr]; - int material = material_[sr]; - - // Check if this cell was hit this iteration - int was_cell_hit = was_hit_[sr]; - if (was_cell_hit) { - n_hits++; - } - - for (int g = 0; g < negroups_; g++) { - int64_t idx = (sr * negroups_) + g; - // There are three scenarios we need to consider: - if (was_cell_hit) { - // 1. If the FSR was hit this iteration, then the new flux is equal to - // the flat source from the previous iteration plus the contributions - // from rays passing through the source region (computed during the - // transport sweep) - scalar_flux_new_[idx] /= volume; - scalar_flux_new_[idx] += source_[idx]; - flux_moments_new_[idx] *= (1.0 / volume); - } else if (volume > 0.0) { - // 2. If the FSR was not hit this iteration, but has been hit some - // previous iteration, then we simply set the new scalar flux to be - // equal to the contribution from the flat source alone. - scalar_flux_new_[idx] = source_[idx]; - } else { - // If the FSR was not hit this iteration, and it has never been hit in - // any iteration (i.e., volume is zero), then we want to set this to 0 - // to avoid dividing anything by a zero volume. - scalar_flux_new_[idx] = 0.0f; - flux_moments_new_[idx] *= 0.0; - } - } - } + scalar_flux_new_[idx] /= volume; + scalar_flux_new_[idx] += source_[idx]; + flux_moments_new_[idx] *= (1.0 / volume); +} - return n_hits; +void LinearSourceDomain::set_flux_to_old_flux(int64_t idx) +{ + scalar_flux_new_[idx] = scalar_flux_old_[idx]; + flux_moments_new_[idx] = flux_moments_old_[idx]; } void LinearSourceDomain::flux_swap() @@ -254,7 +220,7 @@ void LinearSourceDomain::all_reduce_replicated_source_regions() double LinearSourceDomain::evaluate_flux_at_point( Position r, int64_t sr, int g) const { - float phi_flat = FlatSourceDomain::evaluate_flux_at_point(r, sr, g); + double phi_flat = FlatSourceDomain::evaluate_flux_at_point(r, sr, g); Position local_r = r - centroid_[sr]; MomentArray phi_linear = flux_moments_t_[sr * negroups_ + g]; diff --git a/src/random_ray/random_ray.cpp b/src/random_ray/random_ray.cpp index a5bf6ec1060..eb64cb7d26e 100644 --- a/src/random_ray/random_ray.cpp +++ b/src/random_ray/random_ray.cpp @@ -348,12 +348,6 @@ void RandomRay::attenuate_flux_flat_source(double distance, bool is_active) domain_->scalar_flux_new_[source_element + g] += delta_psi_[g]; } - // If the source region hasn't been hit yet this iteration, - // indicate that it now has - if (domain_->was_hit_[source_region] == 0) { - domain_->was_hit_[source_region] = 1; - } - // Accomulate volume (ray distance) into this iteration's estimate // of the source region's volume domain_->volume_[source_region] += distance; @@ -505,12 +499,6 @@ void RandomRay::attenuate_flux_linear_source(double distance, bool is_active) moment_matrix_estimate *= distance; domain->mom_matrix_[source_region] += moment_matrix_estimate; - // If the source region hasn't been hit yet this iteration, - // indicate that it now has - if (domain_->was_hit_[source_region] == 0) { - domain_->was_hit_[source_region] = 1; - } - // Tally valid position inside the source region (e.g., midpoint of // the ray) if not done already if (!domain_->position_recorded_[source_region]) { diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index 4bc77645bcd..57959e80179 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -383,7 +383,7 @@ void RandomRaySimulation::instability_check( "Very high FSR miss rate detected ({:.3f}%). Instability may occur. " "Increase ray density by adding more rays and/or active distance.", percent_missed)); - } else if (percent_missed > 0.01) { + } else if (percent_missed > 1.0) { warning( fmt::format("Elevated FSR miss rate detected ({:.3f}%). Increasing " "ray density by adding more rays and/or active " @@ -432,6 +432,22 @@ void RandomRaySimulation::print_results_random_ray( fmt::print(" Avg per Iteration = {:.4e}\n", total_integrations / settings::n_batches); + std::string estimator; + switch (domain_->volume_estimator_) { + case RandomRayVolumeEstimator::SIMULATION_AVERAGED: + estimator = "Simulation Averaged"; + break; + case RandomRayVolumeEstimator::NAIVE: + estimator = "Naive"; + break; + case RandomRayVolumeEstimator::HYBRID: + estimator = "Hybrid"; + break; + default: + fatal_error("Invalid volume estimator type"); + } + fmt::print(" Volume Estimator Type = {}\n", estimator); + header("Timing Statistics", 4); show_time("Total time for initialization", time_initialize.elapsed()); show_time("Reading cross sections", time_read_xs.elapsed(), 1); diff --git a/src/settings.cpp b/src/settings.cpp index db956abf5ca..ba451e219c5 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -269,6 +269,20 @@ void get_run_parameters(pugi::xml_node node_base) } else { fatal_error("Specify random ray source in settings XML"); } + if (check_for_node(random_ray_node, "volume_estimator")) { + std::string temp_str = + get_node_value(random_ray_node, "volume_estimator", true, true); + if (temp_str == "simulation_averaged") { + FlatSourceDomain::volume_estimator_ = + RandomRayVolumeEstimator::SIMULATION_AVERAGED; + } else if (temp_str == "naive") { + FlatSourceDomain::volume_estimator_ = RandomRayVolumeEstimator::NAIVE; + } else if (temp_str == "hybrid") { + FlatSourceDomain::volume_estimator_ = RandomRayVolumeEstimator::HYBRID; + } else { + fatal_error("Unrecognized volume estimator: " + temp_str); + } + } if (check_for_node(random_ray_node, "source_shape")) { std::string temp_str = get_node_value(random_ray_node, "source_shape", true, true); diff --git a/tests/regression_tests/random_ray_fixed_source_domain/cell/results_true.dat b/tests/regression_tests/random_ray_fixed_source_domain/cell/results_true.dat index 5f297586075..6da51a711bf 100644 --- a/tests/regression_tests/random_ray_fixed_source_domain/cell/results_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_domain/cell/results_true.dat @@ -1,9 +1,9 @@ tally 1: --5.745886E+02 -9.758367E+04 +5.934460E-01 +7.058894E-02 tally 2: -2.971927E-02 -1.827222E-04 +3.206214E-02 +2.063370E-04 tally 3: -1.978393E-03 -7.951531E-07 +2.096411E-03 +8.804924E-07 diff --git a/tests/regression_tests/random_ray_fixed_source_domain/material/results_true.dat b/tests/regression_tests/random_ray_fixed_source_domain/material/results_true.dat index 5f297586075..6da51a711bf 100644 --- a/tests/regression_tests/random_ray_fixed_source_domain/material/results_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_domain/material/results_true.dat @@ -1,9 +1,9 @@ tally 1: --5.745886E+02 -9.758367E+04 +5.934460E-01 +7.058894E-02 tally 2: -2.971927E-02 -1.827222E-04 +3.206214E-02 +2.063370E-04 tally 3: -1.978393E-03 -7.951531E-07 +2.096411E-03 +8.804924E-07 diff --git a/tests/regression_tests/random_ray_fixed_source_domain/universe/results_true.dat b/tests/regression_tests/random_ray_fixed_source_domain/universe/results_true.dat index 5f297586075..6da51a711bf 100644 --- a/tests/regression_tests/random_ray_fixed_source_domain/universe/results_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_domain/universe/results_true.dat @@ -1,9 +1,9 @@ tally 1: --5.745886E+02 -9.758367E+04 +5.934460E-01 +7.058894E-02 tally 2: -2.971927E-02 -1.827222E-04 +3.206214E-02 +2.063370E-04 tally 3: -1.978393E-03 -7.951531E-07 +2.096411E-03 +8.804924E-07 diff --git a/tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat index e2972c5f20a..6ef3f08713b 100644 --- a/tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_linear/linear/inputs_true.dat @@ -192,8 +192,8 @@ fixed source 90 - 10 - 5 + 40 + 20 100.0 1.0 diff --git a/tests/regression_tests/random_ray_fixed_source_linear/linear/results_true.dat b/tests/regression_tests/random_ray_fixed_source_linear/linear/results_true.dat index 7fb01a0ea11..46200b19c8d 100644 --- a/tests/regression_tests/random_ray_fixed_source_linear/linear/results_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_linear/linear/results_true.dat @@ -1,9 +1,9 @@ tally 1: --5.745718E+02 -9.757661E+04 +2.339084E+00 +2.747299E-01 tally 2: -3.074428E-02 -1.952251E-04 +1.090051E-01 +6.073265E-04 tally 3: -1.980876E-03 -7.970502E-07 +7.300117E-03 +2.715425E-06 diff --git a/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat index 5b958d65cdb..805c53fe674 100644 --- a/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/inputs_true.dat @@ -192,8 +192,8 @@ fixed source 90 - 10 - 5 + 40 + 20 100.0 1.0 diff --git a/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/results_true.dat b/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/results_true.dat index 22b39edcfd8..ea711a92567 100644 --- a/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/results_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/results_true.dat @@ -1,9 +1,9 @@ tally 1: --5.745810E+02 -9.758220E+04 +2.335701E+00 +2.742860E-01 tally 2: -3.022777E-02 -1.884091E-04 +1.081600E-01 +5.980902E-04 tally 3: -1.980651E-03 -7.968779E-07 +7.295015E-03 +2.711589E-06 diff --git a/tests/regression_tests/random_ray_fixed_source_linear/test.py b/tests/regression_tests/random_ray_fixed_source_linear/test.py index 25335dea602..99211024e6e 100644 --- a/tests/regression_tests/random_ray_fixed_source_linear/test.py +++ b/tests/regression_tests/random_ray_fixed_source_linear/test.py @@ -23,5 +23,7 @@ def test_random_ray_fixed_source_linear(shape): openmc.reset_auto_ids() model = random_ray_three_region_cube() model.settings.random_ray['source_shape'] = shape - harness = MGXSTestHarness('statepoint.10.h5', model) + model.settings.inactive = 20 + model.settings.batches = 40 + harness = MGXSTestHarness('statepoint.40.h5', model) harness.main() diff --git a/tests/regression_tests/random_ray_fixed_source_normalization/False/results_true.dat b/tests/regression_tests/random_ray_fixed_source_normalization/False/results_true.dat index dbe778df9bf..d8f78493ce3 100644 --- a/tests/regression_tests/random_ray_fixed_source_normalization/False/results_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_normalization/False/results_true.dat @@ -1,9 +1,9 @@ tally 1: --6.614590E+04 -1.283943E+09 +6.840321E+01 +9.376316E+02 tally 2: -4.612657E+02 -4.402080E+04 +4.976182E+02 +4.970407E+04 tally 3: -2.248302E+01 -1.026884E+02 +2.382441E+01 +1.137148E+02 diff --git a/tests/regression_tests/random_ray_fixed_source_normalization/True/results_true.dat b/tests/regression_tests/random_ray_fixed_source_normalization/True/results_true.dat index 5f297586075..6da51a711bf 100644 --- a/tests/regression_tests/random_ray_fixed_source_normalization/True/results_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_normalization/True/results_true.dat @@ -1,9 +1,9 @@ tally 1: --5.745886E+02 -9.758367E+04 +5.934460E-01 +7.058894E-02 tally 2: -2.971927E-02 -1.827222E-04 +3.206214E-02 +2.063370E-04 tally 3: -1.978393E-03 -7.951531E-07 +2.096411E-03 +8.804924E-07 diff --git a/tests/regression_tests/random_ray_fixed_source_subcritical/flat/results_true.dat b/tests/regression_tests/random_ray_fixed_source_subcritical/flat/results_true.dat index e9d2a733d43..831eac5019d 100644 --- a/tests/regression_tests/random_ray_fixed_source_subcritical/flat/results_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_subcritical/flat/results_true.dat @@ -1,168 +1,168 @@ tally 1: -1.582116E+02 -1.005480E+03 +1.591301E+02 +1.016825E+03 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.897563E+01 -1.396228E+02 +5.916980E+01 +1.404518E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.038901E+01 -1.667203E+01 +2.036992E+01 +1.662864E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.465551E+01 -2.438101E+01 +2.464728E+01 +2.434669E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.260230E+01 -1.110141E+02 +5.260923E+01 +1.109616E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -9.438906E+01 -3.579643E+02 +9.443775E+01 +3.580658E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.698287E+01 -1.314208E+02 +5.708323E+01 +1.318058E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.002152E+02 -1.608182E+03 +2.004495E+02 +1.610586E+03 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -7.374897E+01 -2.181176E+02 +7.321594E+01 +2.147152E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.636086E+01 -2.787512E+01 +2.573858E+01 +2.655227E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -3.000395E+01 -3.611990E+01 +2.944079E+01 +3.474938E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.824715E+01 -1.360847E+02 +5.758548E+01 +1.328297E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -9.621963E+01 -3.714397E+02 +9.536577E+01 +3.644652E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.954480E+01 -1.436984E+02 +5.824884E+01 +1.368347E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.067903E+02 -4.585529E+02 -1.257560E+01 -6.364831E+00 -3.060650E+01 -3.770132E+01 -4.983480E+01 -9.972910E+01 -2.349541E+00 -2.217300E-01 -5.718312E+00 -1.313392E+00 -1.965462E+01 -1.548888E+01 -2.011494E-01 -1.622334E-03 -4.895573E-01 -9.609705E-03 -2.318671E+01 -2.155841E+01 -2.445032E-01 -2.397352E-03 -5.950720E-01 -1.420043E-02 -5.322867E+01 -1.136138E+02 -1.968847E-01 -1.554358E-03 -4.791838E-01 -9.207284E-03 -1.191175E+02 -5.692803E+02 -5.806982E-02 -1.354810E-04 -1.436897E-01 -8.295235E-04 -8.022963E+01 -2.589171E+02 -3.465139E-01 -4.882327E-03 -9.638109E-01 -3.777193E-02 -1.555305E+02 -9.711601E+02 +1.073356E+02 +4.630895E+02 +1.263975E+01 +6.427678E+00 +3.076263E+01 +3.807359E+01 +4.995302E+01 +1.001379E+02 +2.355060E+00 +2.226283E-01 +5.731746E+00 +1.318712E+00 +1.959843E+01 +1.538887E+01 +2.005479E-01 +1.611455E-03 +4.880935E-01 +9.545263E-03 +2.315398E+01 +2.148264E+01 +2.441408E-01 +2.388612E-03 +5.941898E-01 +1.414866E-02 +5.319555E+01 +1.134034E+02 +1.967517E-01 +1.551313E-03 +4.788601E-01 +9.189243E-03 +1.192041E+02 +5.698491E+02 +5.811654E-02 +1.356369E-04 +1.438053E-01 +8.304781E-04 +8.044764E+01 +2.602455E+02 +3.474828E-01 +4.908567E-03 +9.665057E-01 +3.797493E-02 +1.561720E+02 +9.788256E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.836421E+01 -1.367538E+02 +5.840869E+01 +1.368656E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.042089E+01 -1.673401E+01 +2.029506E+01 +1.650985E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.477708E+01 -2.463124E+01 +2.467632E+01 +2.440891E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.317851E+01 -1.134205E+02 +5.301033E+01 +1.126277E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -9.634473E+01 -3.724422E+02 +9.608414E+01 +3.702737E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.879813E+01 -1.396454E+02 +5.881578E+01 +1.396463E+02 0.000000E+00 0.000000E+00 0.000000E+00 diff --git a/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/results_true.dat b/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/results_true.dat index 90be5c56921..43a6d825816 100644 --- a/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/results_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/results_true.dat @@ -1,168 +1,168 @@ tally 1: -1.573422E+02 -9.945961E+02 +1.583461E+02 +1.007023E+03 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.868035E+01 -1.382578E+02 +5.891511E+01 +1.392800E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.028997E+01 -1.651549E+01 +2.028333E+01 +1.649217E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.453137E+01 -2.414255E+01 +2.453681E+01 +2.413486E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.232506E+01 -1.098683E+02 +5.235799E+01 +1.099237E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -9.385720E+01 -3.539613E+02 +9.394382E+01 +3.543494E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.664229E+01 -1.298333E+02 +5.676269E+01 +1.303097E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.001324E+02 -1.606858E+03 +2.007853E+02 +1.616208E+03 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -7.358736E+01 -2.171786E+02 +7.319692E+01 +2.146291E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.625416E+01 -2.765491E+01 +2.566725E+01 +2.640687E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.987675E+01 -3.582038E+01 +2.934697E+01 +3.453012E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.797311E+01 -1.348247E+02 +5.737202E+01 +1.318574E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -9.573509E+01 -3.677339E+02 +9.495251E+01 +3.613355E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.926615E+01 -1.423707E+02 +5.799236E+01 +1.356172E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.060513E+02 -4.521166E+02 -1.247077E+01 -6.257701E+00 -3.035137E+01 -3.706675E+01 -4.962504E+01 -9.887387E+01 -2.338861E+00 -2.196709E-01 -5.692320E+00 -1.301195E+00 -1.957564E+01 -1.536441E+01 -2.003053E-01 -1.608650E-03 -4.875030E-01 -9.528647E-03 -2.309275E+01 -2.138375E+01 -2.434816E-01 -2.377213E-03 -5.925856E-01 -1.408114E-02 -5.300741E+01 -1.126617E+02 -1.960322E-01 -1.540737E-03 -4.771089E-01 -9.126597E-03 -1.184809E+02 -5.630856E+02 -5.774249E-02 -1.339082E-04 -1.428798E-01 -8.198938E-04 -7.971031E+01 -2.554943E+02 -3.441954E-01 -4.814135E-03 -9.573621E-01 -3.724437E-02 -1.549617E+02 -9.639834E+02 +1.066411E+02 +4.570264E+02 +1.254011E+01 +6.325603E+00 +3.052011E+01 +3.746896E+01 +4.977289E+01 +9.940494E+01 +2.345784E+00 +2.208432E-01 +5.709169E+00 +1.308139E+00 +1.953078E+01 +1.528285E+01 +1.998229E-01 +1.599785E-03 +4.863290E-01 +9.476135E-03 +2.307190E+01 +2.133049E+01 +2.432475E-01 +2.371063E-03 +5.920158E-01 +1.404471E-02 +5.299849E+01 +1.125570E+02 +1.959909E-01 +1.539183E-03 +4.770085E-01 +9.117394E-03 +1.186130E+02 +5.641029E+02 +5.781214E-02 +1.341752E-04 +1.430521E-01 +8.215282E-04 +7.995712E+01 +2.570085E+02 +3.452925E-01 +4.844032E-03 +9.604136E-01 +3.747567E-02 +1.556790E+02 +9.726171E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.808547E+01 -1.354629E+02 +5.816710E+01 +1.357523E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.029953E+01 -1.654123E+01 +2.018475E+01 +1.633543E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.462331E+01 -2.433425E+01 +2.453470E+01 +2.413604E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.283851E+01 -1.120036E+02 +5.269366E+01 +1.113096E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -9.572799E+01 -3.677288E+02 +9.550190E+01 +3.658245E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.842566E+01 -1.378719E+02 +5.846305E+01 +1.379618E+02 0.000000E+00 0.000000E+00 0.000000E+00 diff --git a/tests/regression_tests/random_ray_k_eff/results_true.dat b/tests/regression_tests/random_ray_k_eff/results_true.dat index ee2e4000de7..a21929d53ba 100644 --- a/tests/regression_tests/random_ray_k_eff/results_true.dat +++ b/tests/regression_tests/random_ray_k_eff/results_true.dat @@ -1,36 +1,36 @@ k-combined: -8.400322E-01 8.023350E-03 +8.400321E-01 8.023358E-03 tally 1: -5.086560E+00 -5.180937E+00 +5.086559E+00 +5.180935E+00 1.885166E+00 -7.115505E-01 -4.588117E+00 -4.214785E+00 -2.860401E+00 -1.639329E+00 +7.115503E-01 +4.588116E+00 +4.214784E+00 +2.860400E+00 +1.639328E+00 4.245221E-01 -3.610930E-02 +3.610929E-02 1.033202E+00 2.138892E-01 1.692631E+00 -5.793967E-01 +5.793966E-01 5.445818E-02 5.996625E-04 1.325403E-01 3.552030E-03 -2.372249E+00 +2.372248E+00 1.146944E+00 -7.808143E-02 -1.242279E-03 +7.808142E-02 +1.242278E-03 1.900346E-01 -7.358492E-03 -7.134949E+00 +7.358490E-03 +7.134948E+00 1.034824E+01 -8.272648E-02 -1.391872E-03 -2.013422E-01 -8.244790E-03 +8.272647E-02 +1.391871E-03 +2.013421E-01 +8.244788E-03 2.043539E+01 8.389902E+01 3.099367E-02 @@ -40,23 +40,23 @@ tally 1: 1.313212E+01 3.449537E+01 1.764293E-01 -6.225587E-03 -4.907293E-01 -4.816401E-02 -7.567717E+00 +6.225586E-03 +4.907292E-01 +4.816400E-02 +7.567715E+00 1.145439E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 3.383194E+00 -2.290469E+00 +2.290468E+00 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.819673E+00 -6.726159E-01 +1.819672E+00 +6.726158E-01 0.000000E+00 0.000000E+00 0.000000E+00 @@ -67,7 +67,7 @@ tally 1: 0.000000E+00 0.000000E+00 0.000000E+00 -7.453759E+00 +7.453758E+00 1.128171E+01 0.000000E+00 0.000000E+00 @@ -85,12 +85,12 @@ tally 1: 0.000000E+00 0.000000E+00 0.000000E+00 -4.601917E+00 -4.242626E+00 +4.601916E+00 +4.242624E+00 1.719723E+00 -5.923467E-01 -4.185463E+00 -3.508696E+00 +5.923465E-01 +4.185462E+00 +3.508695E+00 2.730305E+00 1.494324E+00 4.108214E-01 @@ -98,67 +98,67 @@ tally 1: 9.998572E-01 2.004436E-01 1.660852E+00 -5.570433E-01 +5.570432E-01 5.428709E-02 5.947848E-04 1.321239E-01 3.523137E-03 2.306069E+00 -1.082856E+00 -7.697032E-02 -1.206037E-03 -1.873304E-01 -7.143816E-03 +1.082855E+00 +7.697031E-02 +1.206036E-03 +1.873303E-01 +7.143815E-03 7.075194E+00 1.017519E+01 8.322052E-02 -1.408295E-03 +1.408294E-03 2.025446E-01 -8.342072E-03 +8.342070E-03 2.094832E+01 -8.816716E+01 +8.816715E+01 3.234739E-02 2.101889E-04 8.004135E-02 1.286945E-03 1.357413E+01 -3.685984E+01 +3.685983E+01 1.861680E-01 -6.934828E-03 +6.934827E-03 5.178169E-01 -5.365103E-02 -5.072150E+00 -5.151431E+00 -1.916644E+00 -7.358713E-01 -4.664727E+00 -4.358847E+00 -2.859465E+00 +5.365102E-02 +5.072149E+00 +5.151429E+00 +1.916643E+00 +7.358710E-01 +4.664726E+00 +4.358845E+00 +2.859464E+00 1.638250E+00 4.332944E-01 -3.763171E-02 +3.763170E-02 1.054552E+00 2.229070E-01 1.693008E+00 -5.796672E-01 +5.796671E-01 5.561096E-02 6.247543E-04 1.353459E-01 3.700658E-03 2.368860E+00 1.143296E+00 -7.951820E-02 +7.951819E-02 1.286690E-03 1.935314E-01 -7.621558E-03 -7.119587E+00 +7.621556E-03 +7.119586E+00 1.030023E+01 -8.428176E-02 -1.442932E-03 -2.051275E-01 -8.547244E-03 +8.428175E-02 +1.442931E-03 +2.051274E-01 +8.547243E-03 2.046758E+01 -8.418768E+01 +8.418767E+01 3.181946E-02 2.034766E-04 7.873502E-02 @@ -166,6 +166,6 @@ tally 1: 1.325834E+01 3.515919E+01 1.832838E-01 -6.720556E-03 +6.720555E-03 5.097947E-01 5.199331E-02 diff --git a/tests/regression_tests/random_ray_linear/linear/inputs_true.dat b/tests/regression_tests/random_ray_linear/linear/inputs_true.dat index c580be3d19e..4df51bad5ba 100644 --- a/tests/regression_tests/random_ray_linear/linear/inputs_true.dat +++ b/tests/regression_tests/random_ray_linear/linear/inputs_true.dat @@ -74,8 +74,8 @@ eigenvalue 100 - 10 - 5 + 40 + 20 multi-group 100.0 diff --git a/tests/regression_tests/random_ray_linear/linear/results_true.dat b/tests/regression_tests/random_ray_linear/linear/results_true.dat index a3e24dc75d8..6617c782164 100644 --- a/tests/regression_tests/random_ray_linear/linear/results_true.dat +++ b/tests/regression_tests/random_ray_linear/linear/results_true.dat @@ -1,171 +1,171 @@ k-combined: -8.273022E-01 1.347623E-02 +1.095967E+00 1.543581E-02 tally 1: -5.004109E+00 -5.022655E+00 -1.844047E+00 -6.833376E-01 -4.488042E+00 -4.047669E+00 -2.824818E+00 -1.599927E+00 -4.182704E-01 -3.509796E-02 -1.017987E+00 -2.078987E-01 -1.676761E+00 -5.682729E-01 -5.385624E-02 -5.857594E-04 -1.310753E-01 -3.469677E-03 -2.353602E+00 -1.127695E+00 -7.721312E-02 -1.212202E-03 -1.879213E-01 -7.180339E-03 -7.082957E+00 -1.019023E+01 -8.203580E-02 -1.366996E-03 -1.996612E-01 -8.097436E-03 -2.034293E+01 -8.321967E+01 -3.099116E-02 -1.933713E-04 -7.668546E-02 -1.183975E-03 -1.311478E+01 -3.442575E+01 -1.778200E-01 -6.347187E-03 -4.945973E-01 -4.910476E-02 -7.576546E+00 -1.148094E+01 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -3.386885E+00 -2.295318E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.823720E+00 -6.769694E-01 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.703042E+00 -1.494228E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -7.465612E+00 -1.132769E+01 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.820337E+01 -6.657534E+01 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.127686E+01 -2.549122E+01 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -4.561053E+00 -4.169936E+00 -1.674166E+00 -5.624648E-01 -4.074585E+00 -3.331694E+00 -2.723008E+00 -1.487311E+00 -4.059013E-01 -3.307772E-02 -9.878827E-01 -1.959320E-01 -1.663524E+00 -5.584734E-01 -5.399658E-02 -5.877094E-04 -1.314169E-01 -3.481227E-03 -2.310994E+00 -1.086435E+00 -7.627297E-02 -1.181877E-03 -1.856331E-01 -7.000707E-03 -7.100201E+00 -1.023633E+01 -8.285591E-02 -1.393520E-03 -2.016572E-01 -8.254554E-03 -2.119292E+01 -9.022917E+01 -3.281214E-02 -2.163583E-04 -8.119135E-02 -1.324719E-03 -1.380975E+01 -3.815787E+01 -1.919186E-01 -7.384267E-03 -5.338118E-01 -5.712809E-02 -5.024478E+00 -5.056216E+00 -1.888826E+00 -7.148091E-01 -4.597025E+00 -4.234087E+00 -2.839930E+00 -1.616473E+00 -4.294394E-01 -3.697612E-02 -1.045170E+00 -2.190237E-01 -1.688196E+00 -5.765397E-01 -5.539650E-02 -6.200086E-04 -1.348240E-01 -3.672547E-03 -2.361633E+00 -1.136540E+00 -7.901052E-02 -1.270359E-03 -1.922958E-01 -7.524822E-03 -7.100780E+00 -1.024481E+01 -8.389610E-02 -1.429594E-03 -2.041888E-01 -8.468240E-03 -2.049187E+01 -8.438141E+01 -3.195576E-02 -2.052140E-04 -7.907229E-02 -1.256485E-03 -1.327412E+01 -3.524466E+01 -1.850084E-01 -6.847675E-03 -5.145915E-01 -5.297677E-02 +2.548108E+01 +3.269093E+01 +9.271804E+00 +4.327275E+00 +2.256572E+01 +2.563210E+01 +1.816107E+01 +1.653421E+01 +2.659203E+00 +3.544951E-01 +6.471969E+00 +2.099810E+00 +1.364193E+01 +9.308675E+00 +4.362828E-01 +9.521133E-03 +1.061825E+00 +5.639730E-02 +1.746102E+01 +1.524680E+01 +5.733016E-01 +1.643671E-02 +1.395301E+00 +9.736091E-02 +4.539598E+01 +1.030472E+02 +5.263055E-01 +1.385088E-02 +1.280938E+00 +8.204609E-02 +9.945736E+01 +4.946716E+02 +1.505228E-01 +1.133424E-03 +3.724582E-01 +6.939732E-03 +5.324914E+01 +1.418809E+02 +7.219589E-01 +2.614875E-02 +2.008092E+00 +2.022988E-01 +4.188246E+01 +8.843468E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.224363E+01 +2.481131E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.436097E+01 +1.031496E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.901248E+01 +1.807657E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.559337E+01 +1.039424E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +8.802992E+01 +3.874925E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.506602E+01 +1.016497E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.207833E+01 +2.455068E+01 +8.139772E+00 +3.337974E+00 +1.981058E+01 +1.977210E+01 +1.710407E+01 +1.466648E+01 +2.542287E+00 +3.240467E-01 +6.187418E+00 +1.919453E+00 +1.342690E+01 +9.017908E+00 +4.362263E-01 +9.518695E-03 +1.061688E+00 +5.638286E-02 +1.713924E+01 +1.469065E+01 +5.714028E-01 +1.632839E-02 +1.390680E+00 +9.671931E-02 +4.539193E+01 +1.030326E+02 +5.345253E-01 +1.428719E-02 +1.300944E+00 +8.463057E-02 +1.013270E+02 +5.134168E+02 +1.555517E-01 +1.210145E-03 +3.849017E-01 +7.409480E-03 +5.377836E+01 +1.446537E+02 +7.374053E-01 +2.722908E-02 +2.051056E+00 +2.106567E-01 +2.522726E+01 +3.202007E+01 +9.366659E+00 +4.412278E+00 +2.279657E+01 +2.613561E+01 +1.803368E+01 +1.629756E+01 +2.696490E+00 +3.643066E-01 +6.562716E+00 +2.157928E+00 +1.357447E+01 +9.216150E+00 +4.437305E-01 +9.847287E-03 +1.079951E+00 +5.832923E-02 +1.735848E+01 +1.506775E+01 +5.825173E-01 +1.696803E-02 +1.417731E+00 +1.005081E-01 +4.519297E+01 +1.021280E+02 +5.357712E-01 +1.435322E-02 +1.303976E+00 +8.502167E-02 +9.934508E+01 +4.935314E+02 +1.538761E-01 +1.184229E-03 +3.807556E-01 +7.250804E-03 +5.335839E+01 +1.424221E+02 +7.404823E-01 +2.747396E-02 +2.059614E+00 +2.125512E-01 diff --git a/tests/regression_tests/random_ray_linear/linear_xy/inputs_true.dat b/tests/regression_tests/random_ray_linear/linear_xy/inputs_true.dat index 5c4076de70f..113771438c4 100644 --- a/tests/regression_tests/random_ray_linear/linear_xy/inputs_true.dat +++ b/tests/regression_tests/random_ray_linear/linear_xy/inputs_true.dat @@ -74,8 +74,8 @@ eigenvalue 100 - 10 - 5 + 40 + 20 multi-group 100.0 diff --git a/tests/regression_tests/random_ray_linear/linear_xy/results_true.dat b/tests/regression_tests/random_ray_linear/linear_xy/results_true.dat index f79b3fa787f..abfd03c0678 100644 --- a/tests/regression_tests/random_ray_linear/linear_xy/results_true.dat +++ b/tests/regression_tests/random_ray_linear/linear_xy/results_true.dat @@ -1,171 +1,171 @@ k-combined: -8.368882E-01 8.107070E-03 +1.104727E+00 1.593303E-02 tally 1: -5.072700E+00 -5.152684E+00 -1.876680E+00 -7.051697E-01 -4.567463E+00 -4.176989E+00 -2.858196E+00 -1.636775E+00 -4.239946E-01 -3.601884E-02 -1.031918E+00 -2.133534E-01 -1.692006E+00 -5.789664E-01 -5.442292E-02 -5.988675E-04 -1.324545E-01 -3.547321E-03 -2.371801E+00 -1.146513E+00 -7.804414E-02 -1.241074E-03 -1.899438E-01 -7.351359E-03 -7.134037E+00 -1.034554E+01 -8.269855E-02 -1.390936E-03 -2.012742E-01 -8.239245E-03 -2.043174E+01 -8.386878E+01 -3.098171E-02 -1.929186E-04 -7.666208E-02 -1.181203E-03 -1.312800E+01 -3.447362E+01 -1.763141E-01 -6.217438E-03 -4.904088E-01 -4.810096E-02 -7.608165E+00 -1.157716E+01 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -3.392187E+00 -2.302667E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.821339E+00 -6.737984E-01 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -2.695742E+00 -1.483104E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -7.457904E+00 -1.129343E+01 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.824331E+01 -6.687165E+01 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -1.138096E+01 -2.591161E+01 -0.000000E+00 -0.000000E+00 -0.000000E+00 -0.000000E+00 -4.582534E+00 -4.206969E+00 -1.707129E+00 -5.837143E-01 -4.154811E+00 -3.457563E+00 -2.727459E+00 -1.491183E+00 -4.100516E-01 -3.371280E-02 -9.979836E-01 -1.996938E-01 -1.660765E+00 -5.570066E-01 -5.425719E-02 -5.941503E-04 -1.320511E-01 -3.519379E-03 -2.306630E+00 -1.083425E+00 -7.695103E-02 -1.205473E-03 -1.872834E-01 -7.140475E-03 -7.077631E+00 -1.018246E+01 -8.321109E-02 -1.408010E-03 -2.025216E-01 -8.340388E-03 -2.094896E+01 -8.817186E+01 -3.233088E-02 -2.099749E-04 -8.000050E-02 -1.285635E-03 -1.356905E+01 -3.683229E+01 -1.859858E-01 -6.921278E-03 -5.173102E-01 -5.354619E-02 -5.056029E+00 -5.118575E+00 -1.910007E+00 -7.308066E-01 -4.648575E+00 -4.328846E+00 -2.856363E+00 -1.634636E+00 -4.328679E-01 -3.755784E-02 -1.053514E+00 -2.224694E-01 -1.692444E+00 -5.793158E-01 -5.559366E-02 -6.243434E-04 -1.353038E-01 -3.698224E-03 -2.368251E+00 -1.142832E+00 -7.949453E-02 -1.285913E-03 -1.934738E-01 -7.616955E-03 -7.118354E+00 -1.029753E+01 -8.426801E-02 -1.442436E-03 -2.050940E-01 -8.544309E-03 -2.046889E+01 -8.419814E+01 -3.182500E-02 -2.035334E-04 -7.874874E-02 -1.246195E-03 -1.326056E+01 -3.517079E+01 -1.833225E-01 -6.723237E-03 -5.099023E-01 -5.201406E-02 +2.566934E+01 +3.317503E+01 +9.417202E+00 +4.465518E+00 +2.291958E+01 +2.645097E+01 +1.823903E+01 +1.667438E+01 +2.679931E+00 +3.600420E-01 +6.522415E+00 +2.132667E+00 +1.365623E+01 +9.327448E+00 +4.370682E-01 +9.554968E-03 +1.063736E+00 +5.659772E-02 +1.750634E+01 +1.532609E+01 +5.762870E-01 +1.660889E-02 +1.402567E+00 +9.838082E-02 +4.543609E+01 +1.032286E+02 +5.271287E-01 +1.389456E-02 +1.282941E+00 +8.230483E-02 +9.881678E+01 +4.882586E+02 +1.487616E-01 +1.106634E-03 +3.681003E-01 +6.775702E-03 +5.260781E+01 +1.384126E+02 +7.018594E-01 +2.464953E-02 +1.952187E+00 +1.907001E-01 +4.184779E+01 +8.826530E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.226932E+01 +2.486554E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.441107E+01 +1.038682E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.909194E+01 +1.822767E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.579319E+01 +1.048559E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +8.833276E+01 +3.901410E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.528911E+01 +1.025740E+02 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.218278E+01 +2.477434E+01 +8.203467E+00 +3.389915E+00 +1.996560E+01 +2.007976E+01 +1.714818E+01 +1.473927E+01 +2.551034E+00 +3.262450E-01 +6.208707E+00 +1.932474E+00 +1.343470E+01 +9.027502E+00 +4.363259E-01 +9.522121E-03 +1.061930E+00 +5.640315E-02 +1.717034E+01 +1.474381E+01 +5.729720E-01 +1.641841E-02 +1.394499E+00 +9.725251E-02 +4.542715E+01 +1.031896E+02 +5.348128E-01 +1.430232E-02 +1.301643E+00 +8.472019E-02 +1.009076E+02 +5.091264E+02 +1.543547E-01 +1.191320E-03 +3.819398E-01 +7.294216E-03 +5.342751E+01 +1.427427E+02 +7.255554E-01 +2.633014E-02 +2.018096E+00 +2.037021E-01 +2.540053E+01 +3.247261E+01 +9.483956E+00 +4.526477E+00 +2.308205E+01 +2.681205E+01 +1.812760E+01 +1.646855E+01 +2.717374E+00 +3.700301E-01 +6.613545E+00 +2.191830E+00 +1.362672E+01 +9.286907E+00 +4.458292E-01 +9.940508E-03 +1.085059E+00 +5.888142E-02 +1.744511E+01 +1.521876E+01 +5.866632E-01 +1.721091E-02 +1.427821E+00 +1.019468E-01 +4.538426E+01 +1.029941E+02 +5.385934E-01 +1.450495E-02 +1.310845E+00 +8.592045E-02 +9.921424E+01 +4.921945E+02 +1.532444E-01 +1.174272E-03 +3.791926E-01 +7.189835E-03 +5.301633E+01 +1.405571E+02 +7.285745E-01 +2.655093E-02 +2.026493E+00 +2.054102E-01 diff --git a/tests/regression_tests/random_ray_linear/test.py b/tests/regression_tests/random_ray_linear/test.py index 10262789deb..510c57de8c4 100644 --- a/tests/regression_tests/random_ray_linear/test.py +++ b/tests/regression_tests/random_ray_linear/test.py @@ -22,5 +22,7 @@ def test_random_ray_source(shape): openmc.reset_auto_ids() model = random_ray_lattice() model.settings.random_ray['source_shape'] = shape - harness = MGXSTestHarness('statepoint.10.h5', model) + model.settings.inactive = 20 + model.settings.batches = 40 + harness = MGXSTestHarness('statepoint.40.h5', model) harness.main() diff --git a/tests/regression_tests/random_ray_volume_estimator/__init__.py b/tests/regression_tests/random_ray_volume_estimator/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regression_tests/random_ray_volume_estimator/hybrid/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator/hybrid/inputs_true.dat new file mode 100644 index 00000000000..9c15ec97db0 --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator/hybrid/inputs_true.dat @@ -0,0 +1,245 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 10 + 5 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + hybrid + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_volume_estimator/hybrid/results_true.dat b/tests/regression_tests/random_ray_volume_estimator/hybrid/results_true.dat new file mode 100644 index 00000000000..6da51a711bf --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator/hybrid/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +5.934460E-01 +7.058894E-02 +tally 2: +3.206214E-02 +2.063370E-04 +tally 3: +2.096411E-03 +8.804924E-07 diff --git a/tests/regression_tests/random_ray_volume_estimator/naive/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator/naive/inputs_true.dat new file mode 100644 index 00000000000..7d05f0978da --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator/naive/inputs_true.dat @@ -0,0 +1,245 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 10 + 5 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + naive + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_volume_estimator/naive/results_true.dat b/tests/regression_tests/random_ray_volume_estimator/naive/results_true.dat new file mode 100644 index 00000000000..f8d6d10b001 --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator/naive/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +5.935538E-01 +7.061433E-02 +tally 2: +3.263210E-02 +2.134164E-04 +tally 3: +2.107977E-03 +8.905227E-07 diff --git a/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/inputs_true.dat new file mode 100644 index 00000000000..301671e6d45 --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/inputs_true.dat @@ -0,0 +1,245 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 10 + 5 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + simulation_averaged + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/results_true.dat b/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/results_true.dat new file mode 100644 index 00000000000..5f297586075 --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator/simulation_averaged/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +-5.745886E+02 +9.758367E+04 +tally 2: +2.971927E-02 +1.827222E-04 +tally 3: +1.978393E-03 +7.951531E-07 diff --git a/tests/regression_tests/random_ray_volume_estimator/test.py b/tests/regression_tests/random_ray_volume_estimator/test.py new file mode 100644 index 00000000000..fba4bbbbe6d --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator/test.py @@ -0,0 +1,30 @@ +import os + +import openmc +from openmc.utility_funcs import change_directory +from openmc.examples import random_ray_three_region_cube +import pytest + +from tests.testing_harness import TolerantPyAPITestHarness + + +class MGXSTestHarness(TolerantPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + + +@pytest.mark.parametrize("estimator", ["hybrid", + "simulation_averaged", + "naive" + ]) +def test_random_ray_volume_estimator(estimator): + with change_directory(estimator): + openmc.reset_auto_ids() + model = random_ray_three_region_cube() + model.settings.random_ray['volume_estimator'] = estimator + + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/__init__.py b/tests/regression_tests/random_ray_volume_estimator_linear/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/inputs_true.dat new file mode 100644 index 00000000000..6440cca04a0 --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/inputs_true.dat @@ -0,0 +1,246 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 40 + 20 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + linear + hybrid + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/results_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/results_true.dat new file mode 100644 index 00000000000..46200b19c8d --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +2.339084E+00 +2.747299E-01 +tally 2: +1.090051E-01 +6.073265E-04 +tally 3: +7.300117E-03 +2.715425E-06 diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/naive/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/naive/inputs_true.dat new file mode 100644 index 00000000000..fcf93b046d0 --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator_linear/naive/inputs_true.dat @@ -0,0 +1,246 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 40 + 20 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + linear + naive + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/naive/results_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/naive/results_true.dat new file mode 100644 index 00000000000..02bfc526626 --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator_linear/naive/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +2.339575E+00 +2.748441E-01 +tally 2: +1.086360E-01 +6.030557E-04 +tally 3: +7.298937E-03 +2.741185E-06 diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/inputs_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/inputs_true.dat new file mode 100644 index 00000000000..1df3204a370 --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/inputs_true.dat @@ -0,0 +1,246 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 40 + 20 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + linear + simulation_averaged + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/results_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/results_true.dat new file mode 100644 index 00000000000..93e54feea99 --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +2.670874E+02 +4.432876E+05 +tally 2: +1.117459E-01 +6.497788E-04 +tally 3: +7.563753E-03 +2.947210E-06 diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/test.py b/tests/regression_tests/random_ray_volume_estimator_linear/test.py new file mode 100644 index 00000000000..94a14f3ad3b --- /dev/null +++ b/tests/regression_tests/random_ray_volume_estimator_linear/test.py @@ -0,0 +1,32 @@ +import os + +import openmc +from openmc.utility_funcs import change_directory +from openmc.examples import random_ray_three_region_cube +import pytest + +from tests.testing_harness import TolerantPyAPITestHarness + + +class MGXSTestHarness(TolerantPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + + +@pytest.mark.parametrize("estimator", ["hybrid", + "simulation_averaged", + "naive" + ]) +def test_random_ray_volume_estimator_linear(estimator): + with change_directory(estimator): + openmc.reset_auto_ids() + model = random_ray_three_region_cube() + model.settings.random_ray['source_shape'] = 'linear' + model.settings.random_ray['volume_estimator'] = estimator + model.settings.inactive = 20 + model.settings.batches = 40 + harness = MGXSTestHarness('statepoint.40.h5', model) + harness.main() From bd43d751633126d98dee9b5d7b414882780d3a70 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Thu, 5 Sep 2024 14:00:08 -0500 Subject: [PATCH 058/184] Tweaking title of feature issue template (#3127) --- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index ddf49b89417..978f88d147a 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,5 +1,5 @@ --- -name: Feature request +name: Feature or ehancement request about: Suggest a new feature or enhancement to existing capabilities title: '' labels: '' From 57816e6b8cf23ed0e9b020b72752ed6aeb9501dd Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Sat, 7 Sep 2024 17:50:38 -0500 Subject: [PATCH 059/184] Fix a typo in feature request template (#3128) --- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 978f88d147a..3b13abdbcb7 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,5 +1,5 @@ --- -name: Feature or ehancement request +name: Feature or enhancement request about: Suggest a new feature or enhancement to existing capabilities title: '' labels: '' From 890cab524295b3455fa2ba8a50e2ed5c85b5e6ab Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Thu, 26 Sep 2024 06:45:13 -0500 Subject: [PATCH 060/184] Correct failure due to progress bar values (#3143) --- src/plot.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/plot.cpp b/src/plot.cpp index f29653f8033..348138570c1 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -971,10 +971,6 @@ void Plot::create_voxel() const ProgressBar pb; for (int z = 0; z < pixels_[2]; z++) { - // update progress bar - pb.set_value( - 100. * static_cast(z) / static_cast((pixels_[2] - 1))); - // update z coordinate pltbase.origin_.z = ll.z + z * vox[2]; @@ -989,6 +985,10 @@ void Plot::create_voxel() const // Write to HDF5 dataset voxel_write_slice(z, dspace, dset, memspace, data_flipped.data()); + + // update progress bar + pb.set_value( + 100. * static_cast(z + 1) / static_cast((pixels_[2]))); } voxel_finalize(dspace, dset, memspace); From 8b77a8dd3b3a1c65b1e08f7f032bfee7c86ee51d Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Fri, 27 Sep 2024 00:20:40 +0200 Subject: [PATCH 061/184] Adding source option to plot (#2863) Co-authored-by: Jon Shimwell Co-authored-by: Paul Romano --- openmc/geometry.py | 5 +- openmc/lib/core.py | 9 ++- openmc/model/model.py | 116 +++++++++++++++++++++++++++++++++ openmc/universe.py | 3 +- tests/unit_tests/test_model.py | 29 +++++++++ 5 files changed, 155 insertions(+), 7 deletions(-) diff --git a/openmc/geometry.py b/openmc/geometry.py index 6cce4c18c70..a52dc4418d2 100644 --- a/openmc/geometry.py +++ b/openmc/geometry.py @@ -7,8 +7,6 @@ import warnings import lxml.etree as ET -import numpy as np - import openmc import openmc._xml as xml from .checkvalue import check_type, check_less_than, check_greater_than, PathLike @@ -67,7 +65,7 @@ def root_universe(self, root_universe): self._root_universe = root_universe @property - def bounding_box(self) -> np.ndarray: + def bounding_box(self) -> openmc.BoundingBox: return self.root_universe.bounding_box @property @@ -800,6 +798,7 @@ def plot(self, *args, **kwargs): Units used on the plot axis **kwargs Keyword arguments passed to :func:`matplotlib.pyplot.imshow` + Returns ------- matplotlib.axes.Axes diff --git a/openmc/lib/core.py b/openmc/lib/core.py index a9a549fa05a..e646a9ae1df 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -474,8 +474,11 @@ def run(output=True): _dll.openmc_run() -def sample_external_source(n_samples=1, prn_seed=None): - """Sample external source +def sample_external_source( + n_samples: int = 1000, + prn_seed: int | None = None +) -> list[openmc.SourceParticle]: + """Sample external source and return source particles. .. versionadded:: 0.13.1 @@ -490,7 +493,7 @@ def sample_external_source(n_samples=1, prn_seed=None): Returns ------- list of openmc.SourceParticle - List of samples source particles + List of sampled source particles """ if n_samples <= 0: diff --git a/openmc/model/model.py b/openmc/model/model.py index 2ea579ab7df..78f743a03ed 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -9,6 +9,7 @@ import h5py import lxml.etree as ET +import numpy as np import openmc import openmc._xml as xml @@ -793,6 +794,121 @@ def calculate_volumes(self, threads=None, output=True, cwd='.', openmc.lib.materials[domain_id].volume = \ vol_calc.volumes[domain_id].n + def plot( + self, + n_samples: int | None = None, + plane_tolerance: float = 1., + source_kwargs: dict | None = None, + **kwargs, + ): + """Display a slice plot of the geometry. + + .. versionadded:: 0.15.1 + + Parameters + ---------- + n_samples : dict + The number of source particles to sample and add to plot. Defaults + to None which doesn't plot any particles on the plot. + plane_tolerance: float + When plotting a plane the source locations within the plane +/- + the plane_tolerance will be included and those outside of the + plane_tolerance will not be shown + source_kwargs : dict + Keyword arguments passed to :func:`matplotlib.pyplot.scatter`. + **kwargs + Keyword arguments passed to :func:`openmc.Universe.plot` + + Returns + ------- + matplotlib.axes.Axes + Axes containing resulting image + """ + + check_type('n_samples', n_samples, int | None) + check_type('plane_tolerance', plane_tolerance, float) + if source_kwargs is None: + source_kwargs = {} + source_kwargs.setdefault('marker', 'x') + + ax = self.geometry.plot(**kwargs) + if n_samples: + # Sample external source particles + particles = self.sample_external_source(n_samples) + + # Determine plotting parameters and bounding box of geometry + bbox = self.geometry.bounding_box + origin = kwargs.get('origin', None) + basis = kwargs.get('basis', 'xy') + indices = {'xy': (0, 1, 2), 'xz': (0, 2, 1), 'yz': (1, 2, 0)}[basis] + + # Infer origin if not provided + if np.isinf(bbox.extent[basis]).any(): + if origin is None: + origin = (0, 0, 0) + else: + if origin is None: + # if nan values in the bbox.center they get replaced with 0.0 + # this happens when the bounding_box contains inf values + with warnings.catch_warnings(): + warnings.simplefilter("ignore", RuntimeWarning) + origin = np.nan_to_num(bbox.center) + + slice_index = indices[2] + slice_value = origin[slice_index] + + xs = [] + ys = [] + tol = plane_tolerance + for particle in particles: + if (slice_value - tol < particle.r[slice_index] < slice_value + tol): + xs.append(particle.r[indices[0]]) + ys.append(particle.r[indices[1]]) + ax.scatter(xs, ys, **source_kwargs) + return ax + + def sample_external_source( + self, + n_samples: int = 1000, + prn_seed: int | None = None, + **init_kwargs + ) -> list[openmc.SourceParticle]: + """Sample external source and return source particles. + + .. versionadded:: 0.15.1 + + Parameters + ---------- + n_samples : int + Number of samples + prn_seed : int + Pseudorandom number generator (PRNG) seed; if None, one will be + generated randomly. + **init_kwargs + Keyword arguments passed to :func:`openmc.lib.init` + + Returns + ------- + list of openmc.SourceParticle + List of samples source particles + """ + import openmc.lib + + # Silence output by default. Also set arguments to start in volume + # calculation mode to avoid loading cross sections + init_kwargs.setdefault('output', False) + init_kwargs.setdefault('args', ['-c']) + + with change_directory(tmpdir=True): + # Export model within temporary directory + self.export_to_model_xml() + + # Sample external source sites + with openmc.lib.run_in_memory(**init_kwargs): + return openmc.lib.sample_external_source( + n_samples=n_samples, prn_seed=prn_seed + ) + def plot_geometry(self, output=True, cwd='.', openmc_exec='openmc'): """Creates plot images as specified by the Model.plots attribute diff --git a/openmc/universe.py b/openmc/universe.py index 9fab9ae51b6..d424c243bd4 100644 --- a/openmc/universe.py +++ b/openmc/universe.py @@ -1,3 +1,4 @@ +from __future__ import annotations import math from abc import ABC, abstractmethod from collections.abc import Iterable @@ -230,7 +231,7 @@ def cells(self): return self._cells @property - def bounding_box(self): + def bounding_box(self) -> openmc.BoundingBox: regions = [c.region for c in self.cells.values() if c.region is not None] if regions: diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 404235bef1f..4b567c56d62 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -591,3 +591,32 @@ def test_single_xml_exec(run_in_tmpdir): os.mkdir('subdir') pincell_model.run(path='subdir') + + +def test_model_plot(): + # plots the geometry with source location and checks the resulting + # matplotlib includes the correct coordinates for the scatter plot for all + # basis. + + surface = openmc.Sphere(r=600, boundary_type="vacuum") + cell = openmc.Cell(region=-surface) + geometry = openmc.Geometry([cell]) + source = openmc.IndependentSource(space=openmc.stats.Point((1, 2, 3))) + settings = openmc.Settings(particles=1, batches=1, source=source) + model = openmc.Model(geometry, settings=settings) + + plot = model.plot(n_samples=1, plane_tolerance=4.0, basis="xy") + coords = plot.axes.collections[0].get_offsets().data.flatten() + assert (coords == np.array([1.0, 2.0])).all() + + plot = model.plot(n_samples=1, plane_tolerance=4.0, basis="xz") + coords = plot.axes.collections[0].get_offsets().data.flatten() + assert (coords == np.array([1.0, 3.0])).all() + + plot = model.plot(n_samples=1, plane_tolerance=4.0, basis="yz") + coords = plot.axes.collections[0].get_offsets().data.flatten() + assert (coords == np.array([2.0, 3.0])).all() + + plot = model.plot(n_samples=1, plane_tolerance=0.1, basis="xy") + coords = plot.axes.collections[0].get_offsets().data.flatten() + assert (coords == np.array([])).all() From 1645e3bb8719e299a574bf4bac268691abab2f10 Mon Sep 17 00:00:00 2001 From: azimG Date: Fri, 27 Sep 2024 02:31:59 +0200 Subject: [PATCH 062/184] Mat ids reset (#3125) Co-authored-by: azimgivron Co-authored-by: azim_givron Co-authored-by: Paul Romano --- openmc/deplete/independent_operator.py | 1 - openmc/deplete/openmc_operator.py | 5 ---- openmc/mixin.py | 9 +++++-- tests/regression_tests/__init__.py | 11 +++++++++ .../deplete_no_transport/test.py | 24 ++++--------------- .../deplete_with_transfer_rates/test.py | 6 +++-- tests/unit_tests/test_deplete_decay.py | 7 +++--- .../unit_tests/test_deplete_transfer_rates.py | 1 + 8 files changed, 31 insertions(+), 33 deletions(-) diff --git a/openmc/deplete/independent_operator.py b/openmc/deplete/independent_operator.py index 759abde1308..cc1a05bd99f 100644 --- a/openmc/deplete/independent_operator.py +++ b/openmc/deplete/independent_operator.py @@ -244,7 +244,6 @@ def _consolidate_nuclides_to_material(nuclides, nuc_units, volume): """Puts nuclide list into an openmc.Materials object. """ - openmc.reset_auto_ids() mat = openmc.Material() if nuc_units == 'atom/b-cm': for nuc, conc in nuclides.items(): diff --git a/openmc/deplete/openmc_operator.py b/openmc/deplete/openmc_operator.py index c0ff568bc27..5837cd2c187 100644 --- a/openmc/deplete/openmc_operator.py +++ b/openmc/deplete/openmc_operator.py @@ -146,8 +146,6 @@ def __init__( # Determine which nuclides have cross section data # This nuclides variables contains every nuclides # for which there is an entry in the micro_xs parameter - openmc.reset_auto_ids() - self.nuclides_with_data = self._get_nuclides_with_data( self.cross_sections) @@ -396,9 +394,6 @@ def _update_materials_and_nuclides(self, vec): self.number.set_density(vec) self._update_materials() - # Prevent OpenMC from complaining about re-creating tallies - openmc.reset_auto_ids() - # Update tally nuclides data in preparation for transport solve nuclides = self._get_reaction_nuclides() self._rate_helper.nuclides = nuclides diff --git a/openmc/mixin.py b/openmc/mixin.py index 31c26ec7603..0bc4128b0bf 100644 --- a/openmc/mixin.py +++ b/openmc/mixin.py @@ -72,12 +72,17 @@ def id(self, uid): cls.used_ids.add(uid) self._id = uid + @classmethod + def reset_ids(cls): + """Reset counters""" + cls.used_ids.clear() + cls.next_id = 1 + def reset_auto_ids(): """Reset counters for all auto-generated IDs""" for cls in IDManagerMixin.__subclasses__(): - cls.used_ids.clear() - cls.next_id = 1 + cls.reset_ids() def reserve_ids(ids, cls=None): diff --git a/tests/regression_tests/__init__.py b/tests/regression_tests/__init__.py index ba48fc01e9e..e1cb56f1dd8 100644 --- a/tests/regression_tests/__init__.py +++ b/tests/regression_tests/__init__.py @@ -12,6 +12,17 @@ } +def assert_same_mats(res_ref, res_test): + for mat in res_ref[0].index_mat: + assert mat in res_test[0].index_mat, f"Material {mat} not in new results." + for nuc in res_ref[0].index_nuc: + assert nuc in res_test[0].index_nuc, f"Nuclide {nuc} not in new results." + for mat in res_test[0].index_mat: + assert mat in res_ref[0].index_mat, f"Material {mat} not in old results." + for nuc in res_test[0].index_nuc: + assert nuc in res_ref[0].index_nuc, f"Nuclide {nuc} not in old results." + + def assert_atoms_equal(res_ref, res_test, tol=1e-5): for mat in res_test[0].index_mat: for nuc in res_test[0].index_nuc: diff --git a/tests/regression_tests/deplete_no_transport/test.py b/tests/regression_tests/deplete_no_transport/test.py index 54c29cd5b70..63ae584e116 100644 --- a/tests/regression_tests/deplete_no_transport/test.py +++ b/tests/regression_tests/deplete_no_transport/test.py @@ -10,7 +10,7 @@ from openmc.deplete import IndependentOperator, MicroXS from tests.regression_tests import config, assert_atoms_equal, \ - assert_reaction_rates_equal + assert_reaction_rates_equal, assert_same_mats @pytest.fixture(scope="module") @@ -101,7 +101,7 @@ def test_against_self(run_in_tmpdir, res_ref = openmc.deplete.Results(path_reference) # Assert same mats - _assert_same_mats(res_test, res_ref) + assert_same_mats(res_ref, res_test) tol = 1.0e-14 assert_atoms_equal(res_ref, res_test, tol) @@ -155,7 +155,7 @@ def test_against_coupled(run_in_tmpdir, res_ref = openmc.deplete.Results(path_reference) # Assert same mats - _assert_same_mats(res_test, res_ref) + assert_same_mats(res_test, res_ref) assert_atoms_equal(res_ref, res_test, atom_tol) assert_reaction_rates_equal(res_ref, res_test, rx_tol) @@ -172,6 +172,7 @@ def _create_operator(from_nuclides, for nuc, dens in fuel.get_nuclide_atom_densities().items(): nuclides[nuc] = dens + openmc.reset_auto_ids() op = IndependentOperator.from_nuclides(fuel.volume, nuclides, flux, @@ -187,20 +188,3 @@ def _create_operator(from_nuclides, normalization_mode=normalization_mode) return op - - -def _assert_same_mats(res_ref, res_test): - for mat in res_ref[0].index_mat: - assert mat in res_test[0].index_mat, \ - f"Material {mat} not in new results." - for nuc in res_ref[0].index_nuc: - assert nuc in res_test[0].index_nuc, \ - f"Nuclide {nuc} not in new results." - - for mat in res_test[0].index_mat: - assert mat in res_ref[0].index_mat, \ - f"Material {mat} not in old results." - for nuc in res_test[0].index_nuc: - assert nuc in res_ref[0].index_nuc, \ - f"Nuclide {nuc} not in old results." - diff --git a/tests/regression_tests/deplete_with_transfer_rates/test.py b/tests/regression_tests/deplete_with_transfer_rates/test.py index 9bd5a052b5d..10d60866a69 100644 --- a/tests/regression_tests/deplete_with_transfer_rates/test.py +++ b/tests/regression_tests/deplete_with_transfer_rates/test.py @@ -11,11 +11,12 @@ from openmc.deplete import CoupledOperator from tests.regression_tests import config, assert_reaction_rates_equal, \ - assert_atoms_equal + assert_atoms_equal, assert_same_mats @pytest.fixture def model(): + openmc.reset_auto_ids() f = openmc.Material(name="f") f.add_element("U", 1, percent_type="ao", enrichment=4.25) f.add_element("O", 2) @@ -66,7 +67,7 @@ def test_transfer_rates(run_in_tmpdir, model, rate, dest_mat, power, ref_result) integrator = openmc.deplete.PredictorIntegrator( op, [1], power, timestep_units = 'd') integrator.add_transfer_rate('f', transfer_elements, rate, - destination_material=dest_mat) + destination_material=dest_mat) integrator.integrate() # Get path to test and reference results @@ -82,5 +83,6 @@ def test_transfer_rates(run_in_tmpdir, model, rate, dest_mat, power, ref_result) res_ref = openmc.deplete.Results(path_reference) res_test = openmc.deplete.Results(path_test) + assert_same_mats(res_ref, res_test) assert_atoms_equal(res_ref, res_test, 1e-6) assert_reaction_rates_equal(res_ref, res_test) diff --git a/tests/unit_tests/test_deplete_decay.py b/tests/unit_tests/test_deplete_decay.py index aca812560c7..cebbc16fa79 100644 --- a/tests/unit_tests/test_deplete_decay.py +++ b/tests/unit_tests/test_deplete_decay.py @@ -40,8 +40,9 @@ def test_deplete_decay_products(run_in_tmpdir): # Get concentration of H1 and He4 results = openmc.deplete.Results('depletion_results.h5') - _, h1 = results.get_atoms("1", "H1") - _, he4 = results.get_atoms("1", "He4") + mat_id = op.materials[0].id + _, h1 = results.get_atoms(f"{mat_id}", "H1") + _, he4 = results.get_atoms(f"{mat_id}", "He4") # Since we started with 1e24 atoms of Li5, we should have 1e24 atoms of both # H1 and He4 @@ -78,6 +79,6 @@ def test_deplete_decay_step_fissionable(run_in_tmpdir): # Get concentration of U238. It should be unchanged since this chain has no U238 decay. results = openmc.deplete.Results('depletion_results.h5') - _, u238 = results.get_atoms("1", "U238") + _, u238 = results.get_atoms(f"{mat.id}", "U238") assert u238[1] == pytest.approx(original_atoms) diff --git a/tests/unit_tests/test_deplete_transfer_rates.py b/tests/unit_tests/test_deplete_transfer_rates.py index 847606be435..140777cd6f9 100644 --- a/tests/unit_tests/test_deplete_transfer_rates.py +++ b/tests/unit_tests/test_deplete_transfer_rates.py @@ -71,6 +71,7 @@ def model(): def test_get_set(model, case_name, transfer_rates): """Tests the get/set methods""" + openmc.reset_auto_ids() op = CoupledOperator(model, CHAIN_PATH) transfer = TransferRates(op, model) From b54de4d76176c43ebb2edee3ddbc78e709b3c910 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 2 Oct 2024 12:12:44 -0500 Subject: [PATCH 063/184] Fix check for trigger score name (#3155) --- src/tallies/tally.cpp | 2 +- tests/unit_tests/test_triggers.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index 674987b8f13..ba899611c3b 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -714,7 +714,7 @@ void Tally::init_triggers(pugi::xml_node node) } else { int i_score = 0; for (; i_score < this->scores_.size(); ++i_score) { - if (reaction_name(this->scores_[i_score]) == score_str) + if (this->scores_[i_score] == reaction_type(score_str)) break; } if (i_score == this->scores_.size()) { diff --git a/tests/unit_tests/test_triggers.py b/tests/unit_tests/test_triggers.py index 6b9e54eeb72..14bda0cceb3 100644 --- a/tests/unit_tests/test_triggers.py +++ b/tests/unit_tests/test_triggers.py @@ -113,3 +113,34 @@ def test_tally_trigger_zero_ignored(run_in_tmpdir): total_batches = sp.n_realizations + sp.n_inactive assert total_batches < pincell.settings.trigger_max_batches + + +def test_trigger_he3_production(run_in_tmpdir): + li6 = openmc.Material() + li6.set_density('g/cm3', 1.0) + li6.add_nuclide('Li6', 1.0) + + sph = openmc.Sphere(r=20, boundary_type='vacuum') + outer_cell = openmc.Cell(fill=li6, region=-sph) + model = openmc.Model() + model.geometry = openmc.Geometry([outer_cell]) + model.settings.source = openmc.IndependentSource( + energy=openmc.stats.delta_function(14.1e6) + ) + model.settings.batches = 10 + model.settings.particles = 100 + model.settings.run_mode = 'fixed source' + model.settings.trigger_active = True + model.settings.trigger_batch_interval = 10 + model.settings.trigger_max_batches = 30 + + # Define tally with trigger + trigger = openmc.Trigger(trigger_type='rel_err', threshold=0.0001) + trigger.scores = ['He3-production'] + he3_production_tally = openmc.Tally() + he3_production_tally.scores = ['He3-production'] + he3_production_tally.triggers = [trigger] + model.tallies = openmc.Tallies([he3_production_tally]) + + # Run model to verify that trigger works + model.run() From 9686851e7a1bf1dc49ad313673829bbbabd3945e Mon Sep 17 00:00:00 2001 From: Zoe Prieto <101403129+zoeprieto@users.noreply.github.com> Date: Thu, 3 Oct 2024 19:32:03 -0300 Subject: [PATCH 064/184] Write surface source files per batch (#3124) Co-authored-by: Patrick Shriwise Co-authored-by: Paul Romano --- docs/source/io_formats/settings.rst | 9 +++ docs/source/usersguide/settings.rst | 11 ++++ include/openmc/settings.h | 15 +++-- include/openmc/simulation.h | 1 + include/openmc/state_point.h | 8 ++- openmc/settings.py | 13 ++-- src/finalize.cpp | 5 ++ src/settings.cpp | 13 +++- src/simulation.cpp | 60 +++++++++++-------- src/state_point.cpp | 20 ++++++- tests/regression_tests/dagmc/legacy/test.py | 3 +- tests/regression_tests/surface_source/test.py | 2 +- .../surface_source_write/test.py | 2 +- tests/unit_tests/test_surface_source_write.py | 53 ++++++++++++++++ 14 files changed, 167 insertions(+), 48 deletions(-) diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index e395ada6826..2c6c3265649 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -923,6 +923,15 @@ attributes/sub-elements: *Default*: None + :max_source_files: + An integer value indicating the number of surface source files to be written + containing the maximum number of particles each. The surface source bank + will be cleared in simulation memory each time a surface source file is + written. By default a ``surface_source.h5`` file will be created when the + maximum number of saved particles is reached. + + *Default*: 1 + :mcpl: An optional boolean which indicates if the banked particles should be written to a file in the MCPL_-format instead of the native HDF5-based diff --git a/docs/source/usersguide/settings.rst b/docs/source/usersguide/settings.rst index d73d90cbb3a..4111481a9ea 100644 --- a/docs/source/usersguide/settings.rst +++ b/docs/source/usersguide/settings.rst @@ -336,6 +336,17 @@ or particles going to a cell:: .. note:: The ``cell``, ``cellfrom`` and ``cellto`` attributes cannot be used simultaneously. +To generate more than one surface source files when the maximum number of stored +particles is reached, ``max_source_files`` is available. The surface source bank +will be cleared in simulation memory each time a surface source file is written. +As an example, to write a maximum of three surface source files::: + + settings.surf_source_write = { + 'surfaces_ids': [1, 2, 3], + 'max_particles': 10000, + 'max_source_files': 3 + } + .. _compiled_source: Compiled Sources diff --git a/include/openmc/settings.h b/include/openmc/settings.h index a95f1ced9f1..1e44c08801d 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -129,14 +129,17 @@ extern std::unordered_set statepoint_batch; //!< Batches when state should be written extern std::unordered_set source_write_surf_id; //!< Surface ids where sources will be written + extern int max_history_splits; //!< maximum number of particle splits for weight windows -extern int64_t max_surface_particles; //!< maximum number of particles to be - //!< banked on surfaces per process -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 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 TemperatureMethod temperature_method; //!< method for choosing temperatures extern double diff --git a/include/openmc/simulation.h b/include/openmc/simulation.h index dd8bb7cdfd7..3e4e24e1d06 100644 --- a/include/openmc/simulation.h +++ b/include/openmc/simulation.h @@ -37,6 +37,7 @@ extern "C" int n_lost_particles; //!< cumulative number of lost particles extern "C" bool need_depletion_rx; //!< need to calculate depletion rx? extern "C" int restart_batch; //!< batch at which a restart job resumed extern "C" bool satisfy_triggers; //!< have tally triggers been satisfied? +extern int ssw_current_file; //!< current surface source file extern "C" int total_gen; //!< total number of generations simulated extern double total_weight; //!< Total source weight in a batch extern int64_t work_per_rank; //!< number of particles per MPI rank diff --git a/include/openmc/state_point.h b/include/openmc/state_point.h index 95524f6b622..f0d41e1697f 100644 --- a/include/openmc/state_point.h +++ b/include/openmc/state_point.h @@ -2,6 +2,7 @@ #define OPENMC_STATE_POINT_H #include +#include #include @@ -33,8 +34,11 @@ void load_state_point(); // values on each rank, used to create global indexing. This vector // can be created by calling calculate_parallel_index_vector on // source_bank.size() if such a vector is not already available. -void write_source_point(const char* filename, gsl::span source_bank, - const vector& bank_index); +void write_h5_source_point(const char* filename, + gsl::span source_bank, const vector& bank_index); + +void write_source_point(std::string, gsl::span source_bank, + const vector& bank_index, bool use_mcpl); // This appends a source bank specification to an HDF5 file // that's already open. It is used internally by write_source_point. diff --git a/openmc/settings.py b/openmc/settings.py index ddaac040191..96e6368e4f3 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -211,6 +211,7 @@ class Settings: banked (int) :max_particles: Maximum number of particles to be banked on surfaces per 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 surfaces are to be banked. Particles coming from or going to this @@ -718,7 +719,7 @@ def surf_source_write(self, surf_source_write: dict): cv.check_value( "surface source writing key", key, - ("surface_ids", "max_particles", "mcpl", "cell", "cellfrom", "cellto"), + ("surface_ids", "max_particles", "max_source_files", "mcpl", "cell", "cellfrom", "cellto"), ) if key == "surface_ids": cv.check_type( @@ -726,11 +727,13 @@ def surf_source_write(self, surf_source_write: dict): ) for surf_id in value: cv.check_greater_than("surface id for source banking", surf_id, 0) + elif key == "mcpl": cv.check_type("write to an MCPL-format file", value, bool) - elif key in ("max_particles", "cell", "cellfrom", "cellto"): + 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)", @@ -1251,7 +1254,7 @@ def _create_surf_source_write_subelement(self, root): 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", "cell", "cellfrom", "cellto"): + 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]) @@ -1650,14 +1653,14 @@ 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', 'mcpl', 'cell', 'cellto', 'cellfrom'): + for key in ('surface_ids', 'max_particles', 'max_source_files', 'mcpl', 'cell', 'cellto', 'cellfrom'): value = get_text(elem, key) if value is not None: if key == 'surface_ids': value = [int(x) for x in value.split()] elif key == 'mcpl': value = value in ('true', '1') - elif key in ('max_particles', 'cell', 'cellfrom', 'cellto'): + elif key in ('max_particles', 'max_source_files', 'cell', 'cellfrom', 'cellto'): value = int(value) self.surf_source_write[key] = value diff --git a/src/finalize.cpp b/src/finalize.cpp index 2bde0e3387c..08c2fced308 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -120,6 +120,10 @@ int openmc_finalize() settings::source_latest = false; 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; settings::survival_biasing = false; settings::temperature_default = 293.6; settings::temperature_method = TemperatureMethod::NEAREST; @@ -141,6 +145,7 @@ int openmc_finalize() simulation::keff = 1.0; simulation::need_depletion_rx = false; + simulation::ssw_current_file = 1; simulation::total_gen = 0; simulation::entropy_mesh = nullptr; diff --git a/src/settings.cpp b/src/settings.cpp index ba451e219c5..3a905bdf961 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -118,7 +118,8 @@ SolverType solver_type {SolverType::MONTE_CARLO}; std::unordered_set sourcepoint_batch; std::unordered_set statepoint_batch; std::unordered_set source_write_surf_id; -int64_t max_surface_particles; +int64_t ssw_max_particles; +int64_t ssw_max_files; int64_t ssw_cell_id {C_NONE}; SSWCellType ssw_cell_type {SSWCellType::None}; TemperatureMethod temperature_method {TemperatureMethod::NEAREST}; @@ -799,14 +800,20 @@ void read_settings_xml(pugi::xml_node root) // Get maximum number of particles to be banked per surface if (check_for_node(node_ssw, "max_particles")) { - max_surface_particles = - std::stoll(get_node_value(node_ssw, "max_particles")); + ssw_max_particles = std::stoll(get_node_value(node_ssw, "max_particles")); } else { fatal_error("A maximum number of particles needs to be specified " "using the 'max_particles' parameter to store surface " "source points."); } + // Get maximum number of surface source files to be created + if (check_for_node(node_ssw, "max_source_files")) { + ssw_max_files = std::stoll(get_node_value(node_ssw, "max_source_files")); + } else { + ssw_max_files = 1; + } + if (check_for_node(node_ssw, "mcpl")) { surf_mcpl_write = get_node_value_bool(node_ssw, "mcpl"); diff --git a/src/simulation.cpp b/src/simulation.cpp index be03e48553c..09b3764732b 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -117,6 +117,7 @@ int openmc_simulation_init() // Reset global variables -- this is done before loading state point (as that // will potentially populate k_generation and entropy) simulation::current_batch = 0; + simulation::ssw_current_file = 1; simulation::k_generation.clear(); simulation::entropy.clear(); openmc_reset(); @@ -308,6 +309,7 @@ int n_lost_particles {0}; bool need_depletion_rx {false}; int restart_batch; bool satisfy_triggers {false}; +int ssw_current_file; int total_gen {0}; double total_weight; int64_t work_per_rank; @@ -337,7 +339,7 @@ void allocate_banks() if (settings::surf_source_write) { // Allocate surface source bank - simulation::surf_source_bank.reserve(settings::max_surface_particles); + simulation::surf_source_bank.reserve(settings::ssw_max_particles); } } @@ -345,7 +347,6 @@ void initialize_batch() { // Increment current batch ++simulation::current_batch; - if (settings::run_mode == RunMode::FIXED_SOURCE) { if (settings::solver_type == SolverType::RANDOM_RAY && simulation::current_batch < settings::n_inactive + 1) { @@ -439,42 +440,49 @@ void finalize_batch() std::string source_point_filename = fmt::format("{0}source.{1:0{2}}", settings::path_output, simulation::current_batch, w); gsl::span bankspan(simulation::source_bank); - if (settings::source_mcpl_write) { - write_mcpl_source_point( - source_point_filename.c_str(), bankspan, simulation::work_index); - } else { - write_source_point( - source_point_filename.c_str(), bankspan, simulation::work_index); - } + write_source_point(source_point_filename, bankspan, + simulation::work_index, settings::source_mcpl_write); } // Write a continously-overwritten source point if requested. if (settings::source_latest) { - // note: correct file extension appended automatically auto filename = settings::path_output + "source"; gsl::span bankspan(simulation::source_bank); - if (settings::source_mcpl_write) { - write_mcpl_source_point( - filename.c_str(), bankspan, simulation::work_index); - } else { - write_source_point(filename.c_str(), bankspan, simulation::work_index); - } + write_source_point(filename.c_str(), bankspan, simulation::work_index, + settings::source_mcpl_write); } } // Write out surface source if requested. if (settings::surf_source_write && - simulation::current_batch == settings::n_batches) { - auto filename = settings::path_output + "surface_source"; - auto surf_work_index = - mpi::calculate_parallel_index_vector(simulation::surf_source_bank.size()); - gsl::span surfbankspan(simulation::surf_source_bank.begin(), - simulation::surf_source_bank.size()); - if (settings::surf_mcpl_write) { - write_mcpl_source_point(filename.c_str(), surfbankspan, surf_work_index); - } else { - write_source_point(filename.c_str(), surfbankspan, surf_work_index); + simulation::ssw_current_file <= settings::ssw_max_files) { + bool last_batch = (simulation::current_batch == settings::n_batches); + if (simulation::surf_source_bank.full() || last_batch) { + // Determine appropriate filename + auto filename = fmt::format("{}surface_source.{}", settings::path_output, + simulation::current_batch); + if (settings::ssw_max_files == 1 || + (simulation::ssw_current_file == 1 && last_batch)) { + filename = settings::path_output + "surface_source"; + } + + // Get span of source bank and calculate parallel index vector + auto surf_work_index = mpi::calculate_parallel_index_vector( + simulation::surf_source_bank.size()); + gsl::span surfbankspan(simulation::surf_source_bank.begin(), + simulation::surf_source_bank.size()); + + // Write surface source file + write_source_point( + filename, surfbankspan, surf_work_index, settings::surf_mcpl_write); + + // Reset surface source bank and increment counter + simulation::surf_source_bank.clear(); + if (!last_batch && settings::ssw_max_files >= 1) { + simulation::surf_source_bank.reserve(settings::ssw_max_particles); + } + ++simulation::ssw_current_file; } } } diff --git a/src/state_point.cpp b/src/state_point.cpp index c7b7d6ad85c..adc026fa3c2 100644 --- a/src/state_point.cpp +++ b/src/state_point.cpp @@ -15,6 +15,7 @@ #include "openmc/error.h" #include "openmc/file_utils.h" #include "openmc/hdf5_interface.h" +#include "openmc/mcpl_interface.h" #include "openmc/mesh.h" #include "openmc/message_passing.h" #include "openmc/mgxs_interface.h" @@ -568,8 +569,23 @@ hid_t h5banktype() return banktype; } -void write_source_point(const char* filename, gsl::span source_bank, - const vector& bank_index) +void write_source_point(std::string filename, gsl::span source_bank, + const vector& bank_index, bool use_mcpl) +{ + std::string ext = use_mcpl ? "mcpl" : "h5"; + write_message("Creating source file {}.{} with {} particles ...", filename, + ext, source_bank.size(), 5); + + // Dispatch to appropriate function based on file type + if (use_mcpl) { + write_mcpl_source_point(filename.c_str(), source_bank, bank_index); + } else { + write_h5_source_point(filename.c_str(), source_bank, bank_index); + } +} + +void write_h5_source_point(const char* filename, + gsl::span source_bank, const vector& bank_index) { // When using parallel HDF5, the file is written to collectively by all // processes. With MPI-only, the file is opened and written by the master diff --git a/tests/regression_tests/dagmc/legacy/test.py b/tests/regression_tests/dagmc/legacy/test.py index 884264f30ef..b6b1e376b00 100644 --- a/tests/regression_tests/dagmc/legacy/test.py +++ b/tests/regression_tests/dagmc/legacy/test.py @@ -104,5 +104,4 @@ def test_surf_source(model): def test_dagmc(model): harness = PyAPITestHarness('statepoint.5.h5', model) - harness.main() - + harness.main() \ No newline at end of file diff --git a/tests/regression_tests/surface_source/test.py b/tests/regression_tests/surface_source/test.py index 81bba0c9463..251e9e9ad4a 100644 --- a/tests/regression_tests/surface_source/test.py +++ b/tests/regression_tests/surface_source/test.py @@ -132,4 +132,4 @@ def test_surface_source_read(model): harness = SurfaceSourceTestHarness('statepoint.10.h5', model, 'inputs_true_read.dat') - harness.main() + harness.main() \ No newline at end of file diff --git a/tests/regression_tests/surface_source_write/test.py b/tests/regression_tests/surface_source_write/test.py index 38581736ec4..fe11d68a6e4 100644 --- a/tests/regression_tests/surface_source_write/test.py +++ b/tests/regression_tests/surface_source_write/test.py @@ -1129,4 +1129,4 @@ def test_surface_source_cell_dagmc( harness = SurfaceSourceWriteTestHarness( "statepoint.5.h5", model=model, workdir=folder ) - harness.main() + harness.main() \ No newline at end of file diff --git a/tests/unit_tests/test_surface_source_write.py b/tests/unit_tests/test_surface_source_write.py index 47236307330..6f18d32b718 100644 --- a/tests/unit_tests/test_surface_source_write.py +++ b/tests/unit_tests/test_surface_source_write.py @@ -32,6 +32,7 @@ def geometry(): {"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}, ], ) def test_xml_serialization(parameter, run_in_tmpdir): @@ -44,6 +45,58 @@ def test_xml_serialization(parameter, run_in_tmpdir): assert read_settings.surf_source_write == parameter +@pytest.fixture(scope="module") +def model(): + """Simple hydrogen sphere geometry""" + openmc.reset_auto_ids() + model = openmc.Model() + + # Material + h1 = openmc.Material(name="H1") + h1.add_nuclide("H1", 1.0) + h1.set_density('g/cm3', 1e-7) + + # Geometry + radius = 1.0 + sphere = openmc.Sphere(r=radius, boundary_type="vacuum") + cell = openmc.Cell(region=-sphere, fill=h1) + model.geometry = openmc.Geometry([cell]) + + # Settings + model.settings = openmc.Settings() + model.settings.run_mode = "fixed source" + model.settings.particles = 100 + model.settings.batches = 3 + model.settings.seed = 1 + + distribution = openmc.stats.Point() + model.settings.source = openmc.IndependentSource(space=distribution) + return model + + +@pytest.mark.parametrize( + "max_particles, max_source_files", + [ + (100, 2), + (100, 3), + (100, 1), + ], +) +def test_number_surface_source_file_created(max_particles, max_source_files, + run_in_tmpdir, model): + """Check the number of surface source files written.""" + model.settings.surf_source_write = { + "max_particles": max_particles, + "max_source_files": max_source_files + } + model.run() + should_be_numbered = max_source_files > 1 + for i in range(1, max_source_files + 1): + if should_be_numbered: + assert Path(f"surface_source.{i}.h5").exists() + if not should_be_numbered: + assert Path("surface_source.h5").exists() + ERROR_MSG_1 = ( "A maximum number of particles needs to be specified " "using the 'max_particles' parameter to store surface " From 3a5b218728e09fe802d031d96c0350f3414ce671 Mon Sep 17 00:00:00 2001 From: Rayan HADDAD <103775910+rayanhaddad169@users.noreply.github.com> Date: Fri, 4 Oct 2024 01:13:18 +0200 Subject: [PATCH 065/184] adapt the openmc-update-inputs script for surfaces (#3131) Co-authored-by: r.haddad --- scripts/openmc-update-inputs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/scripts/openmc-update-inputs b/scripts/openmc-update-inputs index 2d49c0626e2..5ee1f1ca0bc 100755 --- a/scripts/openmc-update-inputs +++ b/scripts/openmc-update-inputs @@ -1,7 +1,5 @@ #!/usr/bin/env python3 -"""Update OpenMC's input XML files to the latest format. - -""" +"""Update OpenMC's input XML files to the latest format.""" import argparse from itertools import chain @@ -201,6 +199,16 @@ def update_geometry(geometry_root): was_updated = True + for surface in root.findall('surface'): + for attribute in ['type', 'coeffs', 'boundary']: + if surface.find(attribute) is not None: + value = surface.find(attribute).text + surface.attrib[attribute] = value + + child_element = surface.find(attribute) + surface.remove(child_element) + was_updated = True + # Remove 'type' from lattice definitions. for lat in root.iter('lattice'): elem = lat.find('type') @@ -296,4 +304,4 @@ if __name__ == '__main__': move(fname, fname + '.original') # Write a new geometry file. - tree.write(fname, xml_declaration=True) + tree.write(fname, xml_declaration=True) \ No newline at end of file From 836428666d5264343676bbb977b15bc67be45a3f Mon Sep 17 00:00:00 2001 From: rzehumat <49885819+rzehumat@users.noreply.github.com> Date: Fri, 4 Oct 2024 02:51:12 +0200 Subject: [PATCH 066/184] [docs] theory on PCG random number generator (#3134) Co-authored-by: Matej Rzehulka Co-authored-by: Paul Romano --- docs/source/methods/random_numbers.rst | 39 ++++++++++++++++++-------- src/random_lcg.cpp | 3 +- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/docs/source/methods/random_numbers.rst b/docs/source/methods/random_numbers.rst index 0cefc9156bc..4376bbdb0c5 100644 --- a/docs/source/methods/random_numbers.rst +++ b/docs/source/methods/random_numbers.rst @@ -7,7 +7,7 @@ Random Number Generation In order to sample probability distributions, one must be able to produce random numbers. The standard technique to do this is to generate numbers on the interval :math:`[0,1)` from a deterministic sequence that has properties that -make it appear to be random, e.g. being uniformly distributed and not exhibiting +make it appear to be random, e.g., being uniformly distributed and not exhibiting correlation between successive terms. Since the numbers produced this way are not truly "random" in a strict sense, they are typically referred to as pseudorandom numbers, and the techniques used to generate them are pseudorandom @@ -15,6 +15,11 @@ number generators (PRNGs). Numbers sampled on the unit interval can then be transformed for the purpose of sampling other continuous or discrete probability distributions. +There are many different algorithms for pseudorandom number generation. OpenMC +currently uses `permuted congruential generator`_ (PCG), which builds on top of +the simpler linear congruential generator (LCG). Both algorithms are described +below. + ------------------------------ Linear Congruential Generators ------------------------------ @@ -37,8 +42,8 @@ be generated with a method chosen at random. Some theory should be used." Typically, :math:`M` is chosen to be a power of two as this enables :math:`x \mod M` to be performed using the bitwise AND operator with a bit mask. The constants for the linear congruential generator used by default in OpenMC are -:math:`g = 2806196910506780709`, :math:`c = 1`, and :math:`M = 2^{63}` (see -`L'Ecuyer`_). +:math:`g = 2806196910506780709`, :math:`c = 1`, and :math:`M = 2^{63}` (from +`L'Ecuyer `_). Skip-ahead Capability --------------------- @@ -50,7 +55,8 @@ want to skip ahead :math:`N` random numbers and :math:`N` is large, the cost of sampling :math:`N` random numbers to get to that position may be prohibitively expensive. Fortunately, algorithms have been developed that allow us to skip ahead in :math:`O(\log_2 N)` operations instead of :math:`O(N)`. One algorithm -to do so is described in a paper by Brown_. This algorithm relies on the following +to do so is described in a `paper by Brown +`_. This algorithm relies on the following relationship: .. math:: @@ -58,15 +64,26 @@ relationship: \xi_{i+k} = g^k \xi_i + c \frac{g^k - 1}{g - 1} \mod M -Note that equation :eq:`lcg-skipahead` has the same general form as equation :eq:`lcg`, so -the idea is to determine the new multiplicative and additive constants in -:math:`O(\log_2 N)` operations. +Note that equation :eq:`lcg-skipahead` has the same general form as equation +:eq:`lcg`, so the idea is to determine the new multiplicative and additive +constants in :math:`O(\log_2 N)` operations. -.. only:: html - .. rubric:: References +-------------------------------- +Permuted Congruential Generators +-------------------------------- +The `permuted congruential generator`_ (PCG) algorithm aims to improve upon the +LCG algorithm by permuting the output. The algorithm works on the basic +principle of first advancing the generator state using the LCG algorithm and +then applying a permutation function on the LCG state to obtain the output. This +results in increased statistical quality as measured by common statistical tests +while exhibiting a very small performance overhead relative to the LCG algorithm +and an equivalent memory footprint. For further details, see the original +technical report by `O'Neill +`_. OpenMC uses the +PCG-RXS-M-XS variant with a 64-bit state and 64-bit output. -.. _L'Ecuyer: https://doi.org/10.1090/S0025-5718-99-00996-5 -.. _Brown: https://laws.lanl.gov/vhosts/mcnp.lanl.gov/pdf_files/anl-rn-arb-stride.pdf .. _linear congruential generator: https://en.wikipedia.org/wiki/Linear_congruential_generator + +.. _permuted congruential generator: https://en.wikipedia.org/wiki/Permuted_congruential_generator diff --git a/src/random_lcg.cpp b/src/random_lcg.cpp index 581d696176f..ca4467719c4 100644 --- a/src/random_lcg.cpp +++ b/src/random_lcg.cpp @@ -17,7 +17,8 @@ constexpr uint64_t prn_stride {152917LL}; // stride between particles //============================================================================== // 64 bit implementation of the PCG-RXS-M-XS 64-bit state / 64-bit output -// geneator Adapted from: https://github.com/imneme/pcg-c +// geneator Adapted from: https://github.com/imneme/pcg-c, in particular +// https://github.com/imneme/pcg-c/blob/83252d9c23df9c82ecb42210afed61a7b42402d7/include/pcg_variants.h#L188-L192 // @techreport{oneill:pcg2014, // title = "PCG: A Family of Simple Fast Space-Efficient Statistically Good // Algorithms for Random Number Generation", author = "Melissa E. O'Neill", From 1a520c9f4d598f96b4e6712fefa1d25b7d4da404 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Fri, 4 Oct 2024 03:37:51 +0200 Subject: [PATCH 067/184] Adding material.get_element_atom_densities (#3103) Co-authored-by: Paul Romano --- openmc/data/data.py | 20 +++++++++----- openmc/material.py | 43 +++++++++++++++++++++++++++++++ tests/unit_tests/test_material.py | 22 ++++++++++++++++ 3 files changed, 78 insertions(+), 7 deletions(-) diff --git a/openmc/data/data.py b/openmc/data/data.py index d94d6aaaa39..408adf2e429 100644 --- a/openmc/data/data.py +++ b/openmc/data/data.py @@ -549,7 +549,18 @@ def gnds_name(Z, A, m=0): return f'{ATOMIC_SYMBOL[Z]}{A}' -def isotopes(element): + +def _get_element_symbol(element: str) -> str: + if len(element) > 2: + symbol = ELEMENT_SYMBOL.get(element.lower()) + if symbol is None: + raise ValueError(f'Element name "{element}" not recognized') + return symbol + else: + return element + + +def isotopes(element: str) -> list[tuple[str, float]]: """Return naturally occurring isotopes and their abundances .. versionadded:: 0.12.1 @@ -570,12 +581,7 @@ def isotopes(element): If the element name is not recognized """ - # Convert name to symbol if needed - if len(element) > 2: - symbol = ELEMENT_SYMBOL.get(element.lower()) - if symbol is None: - raise ValueError(f'Element name "{element}" not recognised') - element = symbol + element = _get_element_symbol(element) # Get the nuclides present in nature result = [] diff --git a/openmc/material.py b/openmc/material.py index 5b958c75a68..f550fd64900 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -18,6 +18,7 @@ from .mixin import IDManagerMixin from openmc.checkvalue import PathLike from openmc.stats import Univariate, Discrete, Mixture +from openmc.data.data import _get_element_symbol # Units for density supported by OpenMC @@ -1075,6 +1076,48 @@ def get_nuclide_atom_densities(self, nuclide: str | None = None) -> dict[str, fl return nuclides + def get_element_atom_densities(self, element: str | None = None) -> dict[str, float]: + """Returns one or all elements in the material and their atomic + densities in units of atom/b-cm + + .. versionadded:: 0.15.1 + + Parameters + ---------- + element : str, optional + Element for which atom density is desired. If not specified, the + atom density for each element in the material is given. + + Returns + ------- + elements : dict + Dictionary whose keys are element names and values are densities in + [atom/b-cm] + + """ + if element is not None: + element = _get_element_symbol(element) + + nuc_densities = self.get_nuclide_atom_densities() + + # Initialize an empty dictionary for summed values + densities = {} + + # Accumulate densities for each nuclide + for nuclide, density in nuc_densities.items(): + nuc_element = openmc.data.ATOMIC_SYMBOL[openmc.data.zam(nuclide)[0]] + if element is None or element == nuc_element: + if nuc_element not in densities: + densities[nuc_element] = 0.0 + densities[nuc_element] += float(density) + + # If specific element was requested, make sure it is present + if element is not None and element not in densities: + raise ValueError(f'Element {element} not found in material.') + + return densities + + def get_activity(self, units: str = 'Bq/cm3', by_nuclide: bool = False, volume: float | None = None) -> dict[str, float] | float: """Returns the activity of the material or for each nuclide in the diff --git a/tests/unit_tests/test_material.py b/tests/unit_tests/test_material.py index 44c0730fbd2..94ba82571be 100644 --- a/tests/unit_tests/test_material.py +++ b/tests/unit_tests/test_material.py @@ -380,6 +380,28 @@ def test_get_nuclide_atom_densities_specific(uo2): assert all_nuc['O16'] == one_nuc['O16'] +def test_get_element_atom_densities(uo2): + for element, density in uo2.get_element_atom_densities().items(): + assert element in ('U', 'O') + assert density > 0 + + +def test_get_element_atom_densities_specific(uo2): + one_nuc = uo2.get_element_atom_densities('O') + assert list(one_nuc.keys()) == ['O'] + assert list(one_nuc.values())[0] > 0 + + one_nuc = uo2.get_element_atom_densities('uranium') + assert list(one_nuc.keys()) == ['U'] + assert list(one_nuc.values())[0] > 0 + + with pytest.raises(ValueError, match='not found'): + uo2.get_element_atom_densities('Li') + + with pytest.raises(ValueError, match='not recognized'): + uo2.get_element_atom_densities('proximium') + + def test_get_nuclide_atoms(): mat = openmc.Material() mat.add_nuclide('Li6', 1.0) From c285a2c4ce8a549a02f28acbce1836c11876b8ab Mon Sep 17 00:00:00 2001 From: Zoe Prieto <101403129+zoeprieto@users.noreply.github.com> Date: Fri, 4 Oct 2024 02:07:03 -0300 Subject: [PATCH 068/184] Implement filter for cosine of angle of surface crossing (#2768) Co-authored-by: Paul Romano --- CMakeLists.txt | 1 + docs/source/pythonapi/base.rst | 1 + include/openmc/tallies/filter.h | 1 + include/openmc/tallies/filter_mu.h | 2 +- include/openmc/tallies/filter_musurface.h | 34 +++++++++++++++ openmc/filter.py | 42 +++++++++++++++++- openmc/lib/filter.py | 11 +++-- src/tallies/filter.cpp | 3 ++ src/tallies/filter_musurface.cpp | 36 ++++++++++++++++ .../filter_musurface/__init__.py | 0 .../filter_musurface/inputs_true.dat | 37 ++++++++++++++++ .../filter_musurface/results_true.dat | 11 +++++ .../regression_tests/filter_musurface/test.py | 43 +++++++++++++++++++ tests/unit_tests/test_filter_musurface.py | 38 ++++++++++++++++ 14 files changed, 254 insertions(+), 6 deletions(-) create mode 100644 include/openmc/tallies/filter_musurface.h create mode 100644 src/tallies/filter_musurface.cpp create mode 100644 tests/regression_tests/filter_musurface/__init__.py create mode 100644 tests/regression_tests/filter_musurface/inputs_true.dat create mode 100644 tests/regression_tests/filter_musurface/results_true.dat create mode 100644 tests/regression_tests/filter_musurface/test.py create mode 100644 tests/unit_tests/test_filter_musurface.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 0f4cc1b527b..b4011434e78 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -416,6 +416,7 @@ list(APPEND libopenmc_SOURCES src/tallies/filter_meshborn.cpp src/tallies/filter_meshsurface.cpp src/tallies/filter_mu.cpp + src/tallies/filter_musurface.cpp src/tallies/filter_particle.cpp src/tallies/filter_polar.cpp src/tallies/filter_sph_harm.cpp diff --git a/docs/source/pythonapi/base.rst b/docs/source/pythonapi/base.rst index 5ae9f20edf4..611d2c216ee 100644 --- a/docs/source/pythonapi/base.rst +++ b/docs/source/pythonapi/base.rst @@ -133,6 +133,7 @@ Constructing Tallies openmc.EnergyFilter openmc.EnergyoutFilter openmc.MuFilter + openmc.MuSurfaceFilter openmc.PolarFilter openmc.AzimuthalFilter openmc.DistribcellFilter diff --git a/include/openmc/tallies/filter.h b/include/openmc/tallies/filter.h index 1166c0eee53..210ab284ba9 100644 --- a/include/openmc/tallies/filter.h +++ b/include/openmc/tallies/filter.h @@ -36,6 +36,7 @@ enum class FilterType { MESHBORN, MESH_SURFACE, MU, + MUSURFACE, PARTICLE, POLAR, SPHERICAL_HARMONICS, diff --git a/include/openmc/tallies/filter_mu.h b/include/openmc/tallies/filter_mu.h index 942ee60c225..b0ee40eac9f 100644 --- a/include/openmc/tallies/filter_mu.h +++ b/include/openmc/tallies/filter_mu.h @@ -40,7 +40,7 @@ class MuFilter : public Filter { void set_bins(gsl::span bins); -private: +protected: //---------------------------------------------------------------------------- // Data members diff --git a/include/openmc/tallies/filter_musurface.h b/include/openmc/tallies/filter_musurface.h new file mode 100644 index 00000000000..2ca19e3a259 --- /dev/null +++ b/include/openmc/tallies/filter_musurface.h @@ -0,0 +1,34 @@ +#ifndef OPENMC_TALLIES_FILTER_MU_SURFACE_H +#define OPENMC_TALLIES_FILTER_MU_SURFACE_H + +#include + +#include "openmc/tallies/filter_mu.h" +#include "openmc/vector.h" + +namespace openmc { + +//============================================================================== +//! Bins the incoming-outgoing direction cosine. This is only used for surface +//! crossings. +//============================================================================== + +class MuSurfaceFilter : public MuFilter { +public: + //---------------------------------------------------------------------------- + // Constructors, destructors + + ~MuSurfaceFilter() = default; + + //---------------------------------------------------------------------------- + // Methods + + std::string type_str() const override { return "musurface"; } + FilterType type() const override { return FilterType::MUSURFACE; } + + void get_all_bins(const Particle& p, TallyEstimator estimator, + FilterMatch& match) const override; +}; + +} // namespace openmc +#endif // OPENMC_TALLIES_FILTER_MU_SURFACE_H diff --git a/openmc/filter.py b/openmc/filter.py index 0f884cfcd16..b5926af5ad9 100644 --- a/openmc/filter.py +++ b/openmc/filter.py @@ -22,7 +22,7 @@ _FILTER_TYPES = ( 'universe', 'material', 'cell', 'cellborn', 'surface', 'mesh', 'energy', - 'energyout', 'mu', 'polar', 'azimuthal', 'distribcell', 'delayedgroup', + 'energyout', 'mu', 'musurface', 'polar', 'azimuthal', 'distribcell', 'delayedgroup', 'energyfunction', 'cellfrom', 'materialfrom', 'legendre', 'spatiallegendre', 'sphericalharmonics', 'zernike', 'zernikeradial', 'particle', 'cellinstance', 'collision', 'time' @@ -765,7 +765,7 @@ def __eq__(self, other): @Filter.bins.setter def bins(self, bins): cv.check_type('bins', bins, Sequence, str) - bins = np.atleast_1d(bins) + bins = np.atleast_1d(bins) for edge in bins: cv.check_value('filter bin', edge, _PARTICLES) self._bins = bins @@ -1850,6 +1850,44 @@ def check_bins(self, bins): cv.check_less_than('filter value', x, 1., equality=True) +class MuSurfaceFilter(MuFilter): + """Bins tally events based on the angle of surface crossing. + + This filter bins events based on the cosine of the angle between the + direction of the particle and the normal to the surface at the point it + crosses. Only used in conjunction with a SurfaceFilter and current score. + + .. versionadded:: 0.15.1 + + Parameters + ---------- + values : int or Iterable of Real + A grid of surface crossing angles which the events will be divided into. + Values represent the cosine of the angle between the direction of the + particle and the normal to the surface at the point it crosses. If an + iterable is given, the values will be used explicitly as grid points. If + a single int is given, the range [-1, 1] will be divided equally into + that number of bins. + filter_id : int + Unique identifier for the filter + + Attributes + ---------- + values : numpy.ndarray + An array of values for which each successive pair constitutes a range of + surface crossing angle cosines for a single bin. + id : int + Unique identifier for the filter + bins : numpy.ndarray + An array of shape (N, 2) where each row is a pair of cosines of surface + crossing angle for a single filter + num_bins : Integral + The number of filter bins + + """ + # Note: inherits implementation from MuFilter + + class PolarFilter(RealFilter): """Bins tally events based on the incident particle's direction. diff --git a/openmc/lib/filter.py b/openmc/lib/filter.py index 340c2fa3448..b30f5e66282 100644 --- a/openmc/lib/filter.py +++ b/openmc/lib/filter.py @@ -21,9 +21,9 @@ 'CellInstanceFilter', 'CollisionFilter', 'DistribcellFilter', 'DelayedGroupFilter', 'EnergyFilter', 'EnergyoutFilter', 'EnergyFunctionFilter', 'LegendreFilter', 'MaterialFilter', 'MaterialFromFilter', 'MeshFilter', 'MeshBornFilter', - 'MeshSurfaceFilter', 'MuFilter', 'ParticleFilter', - 'PolarFilter', 'SphericalHarmonicsFilter', 'SpatialLegendreFilter', 'SurfaceFilter', - 'UniverseFilter', 'ZernikeFilter', 'ZernikeRadialFilter', 'filters' + 'MeshSurfaceFilter', 'MuFilter', 'MuSurfaceFilter', 'ParticleFilter', 'PolarFilter', + 'SphericalHarmonicsFilter', 'SpatialLegendreFilter', 'SurfaceFilter', 'UniverseFilter', + 'ZernikeFilter', 'ZernikeRadialFilter', 'filters' ] # Tally functions @@ -540,6 +540,10 @@ class MuFilter(Filter): filter_type = 'mu' +class MuSurfaceFilter(Filter): + filter_type = 'musurface' + + class ParticleFilter(Filter): filter_type = 'particle' @@ -642,6 +646,7 @@ class ZernikeRadialFilter(ZernikeFilter): 'meshborn': MeshBornFilter, 'meshsurface': MeshSurfaceFilter, 'mu': MuFilter, + 'musurface': MuSurfaceFilter, 'particle': ParticleFilter, 'polar': PolarFilter, 'sphericalharmonics': SphericalHarmonicsFilter, diff --git a/src/tallies/filter.cpp b/src/tallies/filter.cpp index ff7a3416b90..074212db44d 100644 --- a/src/tallies/filter.cpp +++ b/src/tallies/filter.cpp @@ -26,6 +26,7 @@ #include "openmc/tallies/filter_meshborn.h" #include "openmc/tallies/filter_meshsurface.h" #include "openmc/tallies/filter_mu.h" +#include "openmc/tallies/filter_musurface.h" #include "openmc/tallies/filter_particle.h" #include "openmc/tallies/filter_polar.h" #include "openmc/tallies/filter_sph_harm.h" @@ -133,6 +134,8 @@ Filter* Filter::create(const std::string& type, int32_t id) return Filter::create(id); } else if (type == "mu") { return Filter::create(id); + } else if (type == "musurface") { + return Filter::create(id); } else if (type == "particle") { return Filter::create(id); } else if (type == "polar") { diff --git a/src/tallies/filter_musurface.cpp b/src/tallies/filter_musurface.cpp new file mode 100644 index 00000000000..58fe39b9113 --- /dev/null +++ b/src/tallies/filter_musurface.cpp @@ -0,0 +1,36 @@ +#include "openmc/tallies/filter_musurface.h" + +#include // for abs, copysign + +#include "openmc/search.h" +#include "openmc/surface.h" +#include "openmc/tallies/tally_scoring.h" + +namespace openmc { + +void MuSurfaceFilter::get_all_bins( + const Particle& p, TallyEstimator estimator, FilterMatch& match) const +{ + // Get surface normal (and make sure it is a unit vector) + const auto surf {model::surfaces[std::abs(p.surface()) - 1].get()}; + auto n = surf->normal(p.r()); + n /= n.norm(); + + // Determine whether normal should be pointing in or out + if (p.surface() < 0) + n *= -1; + + // Determine cosine of angle between normal and particle direction + double mu = p.u().dot(n); + if (std::abs(mu) > 1.0) + mu = std::copysign(1.0, mu); + + // Find matching bin + if (mu >= bins_.front() && mu <= bins_.back()) { + auto bin = lower_bound_index(bins_.begin(), bins_.end(), mu); + match.bins_.push_back(bin); + match.weights_.push_back(1.0); + } +} + +} // namespace openmc diff --git a/tests/regression_tests/filter_musurface/__init__.py b/tests/regression_tests/filter_musurface/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regression_tests/filter_musurface/inputs_true.dat b/tests/regression_tests/filter_musurface/inputs_true.dat new file mode 100644 index 00000000000..031f62159ab --- /dev/null +++ b/tests/regression_tests/filter_musurface/inputs_true.dat @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + eigenvalue + 1000 + 5 + 0 + + + + 1 + + + -1.0 -0.5 0.0 0.5 1.0 + + + 1 2 + current + + + diff --git a/tests/regression_tests/filter_musurface/results_true.dat b/tests/regression_tests/filter_musurface/results_true.dat new file mode 100644 index 00000000000..39c9f2b925a --- /dev/null +++ b/tests/regression_tests/filter_musurface/results_true.dat @@ -0,0 +1,11 @@ +k-combined: +1.157005E-01 7.587090E-03 +tally 1: +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +8.770000E-01 +1.608710E-01 +3.909000E+00 +3.063035E+00 diff --git a/tests/regression_tests/filter_musurface/test.py b/tests/regression_tests/filter_musurface/test.py new file mode 100644 index 00000000000..f2ec96b495d --- /dev/null +++ b/tests/regression_tests/filter_musurface/test.py @@ -0,0 +1,43 @@ +import numpy as np +from math import pi + +import openmc +import pytest + +from tests.testing_harness import PyAPITestHarness + + +@pytest.fixture +def model(): + model = openmc.Model() + fuel = openmc.Material() + fuel.set_density('g/cm3', 10.0) + fuel.add_nuclide('U235', 1.0) + zr = openmc.Material() + zr.set_density('g/cm3', 1.0) + zr.add_nuclide('Zr90', 1.0) + + cyl1 = openmc.ZCylinder(r=1.0) + cyl2 = openmc.ZCylinder(r=3.0, boundary_type='vacuum') + cell1 = openmc.Cell(fill=fuel, region=-cyl1) + cell2 = openmc.Cell(fill=zr, region=+cyl1 & -cyl2) + model.geometry = openmc.Geometry([cell1, cell2]) + + model.settings.batches = 5 + model.settings.inactive = 0 + model.settings.particles = 1000 + + # Create a tally for current through the first surface binned by mu + surf_filter = openmc.SurfaceFilter([cyl1]) + mu_filter = openmc.MuSurfaceFilter([-1.0, -0.5, 0.0, 0.5, 1.0]) + tally = openmc.Tally() + tally.filters = [surf_filter, mu_filter] + tally.scores = ['current'] + model.tallies.append(tally) + + return model + + +def test_filter_musurface(model): + harness = PyAPITestHarness('statepoint.5.h5', model) + harness.main() diff --git a/tests/unit_tests/test_filter_musurface.py b/tests/unit_tests/test_filter_musurface.py new file mode 100644 index 00000000000..ca0db71f0c6 --- /dev/null +++ b/tests/unit_tests/test_filter_musurface.py @@ -0,0 +1,38 @@ +import openmc + + +def test_musurface(run_in_tmpdir): + sphere = openmc.Sphere(r=1.0, boundary_type='vacuum') + cell = openmc.Cell(region=-sphere) + model = openmc.Model() + model.geometry = openmc.Geometry([cell]) + model.settings.particles = 1000 + model.settings.batches = 10 + E = 1.0 + model.settings.source = openmc.IndependentSource( + space=openmc.stats.Point(), + angle=openmc.stats.Isotropic(), + energy=openmc.stats.delta_function(E), + ) + model.settings.run_mode = "fixed source" + + filter1 = openmc.MuSurfaceFilter(200) + filter2 = openmc.SurfaceFilter(sphere) + tally = openmc.Tally() + tally.filters = [filter1, filter2] + tally.scores = ['current'] + model.tallies = openmc.Tallies([tally]) + + # Run OpenMC + sp_filename = model.run() + + # Get current binned by mu + with openmc.StatePoint(sp_filename) as sp: + current_mu = sp.tallies[tally.id].mean.ravel() + + # All contributions should show up in last bin + assert current_mu[-1] == 1.0 + for element in current_mu[:-1]: + assert element == 0.0 + + From 9070b8b220153e2b4d8c8e5238ffec99cfd06add Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Fri, 4 Oct 2024 22:59:41 -0500 Subject: [PATCH 069/184] Prepare point query data structures on meshes when applying Weight Windows (#3157) Co-authored-by: Paul Romano --- include/openmc/mesh.h | 6 +-- openmc/weight_windows.py | 5 ++- src/mesh.cpp | 5 ++- src/tallies/filter_mesh.cpp | 2 +- src/weight_windows.cpp | 7 ++-- tests/unit_tests/weightwindows/test.py | 38 ++++++++++++++++++- .../weightwindows/test_mesh_tets.exo | 1 + vendor/fmt | 2 +- 8 files changed, 53 insertions(+), 13 deletions(-) create mode 120000 tests/unit_tests/weightwindows/test_mesh_tets.exo diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index 6f3f2b0a480..f0ad5724706 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -83,8 +83,8 @@ class Mesh { virtual ~Mesh() = default; // Methods - //! Perform any preparation needed to support use in mesh filters - virtual void prepare_for_tallies() {}; + //! Perform any preparation needed to support point location within the mesh + virtual void prepare_for_point_location() {}; //! Update a position to the local coordinates of the mesh virtual void local_coords(Position& r) const {}; @@ -737,7 +737,7 @@ class MOABMesh : public UnstructuredMesh { // Overridden Methods //! Perform any preparation needed to support use in mesh filters - void prepare_for_tallies() override; + void prepare_for_point_location() override; Position sample_element(int32_t bin, uint64_t* seed) const override; diff --git a/openmc/weight_windows.py b/openmc/weight_windows.py index a10fd2a6510..5df9f71dcc7 100644 --- a/openmc/weight_windows.py +++ b/openmc/weight_windows.py @@ -328,8 +328,9 @@ def to_xml_element(self) -> ET.Element: subelement = ET.SubElement(element, 'particle_type') subelement.text = self.particle_type - subelement = ET.SubElement(element, 'energy_bounds') - subelement.text = ' '.join(str(e) for e in self.energy_bounds) + if self.energy_bounds is not None: + subelement = ET.SubElement(element, 'energy_bounds') + subelement.text = ' '.join(str(e) for e in self.energy_bounds) subelement = ET.SubElement(element, 'lower_ww_bounds') subelement.text = ' '.join(str(b) for b in self.lower_ww_bounds.ravel('F')) diff --git a/src/mesh.cpp b/src/mesh.cpp index 0f8b4b14e84..b90fead0339 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -2315,7 +2315,7 @@ void MOABMesh::initialize() this->determine_bounds(); } -void MOABMesh::prepare_for_tallies() +void MOABMesh::prepare_for_point_location() { // if the KDTree has already been constructed, do nothing if (kdtree_) @@ -2365,7 +2365,8 @@ void MOABMesh::build_kdtree(const moab::Range& all_tets) all_tets_and_tris.merge(all_tris); // create a kd-tree instance - write_message("Building adaptive k-d tree for tet mesh...", 7); + write_message( + 7, "Building adaptive k-d tree for tet mesh with ID {}...", id_); kdtree_ = make_unique(mbi_.get()); // Determine what options to use diff --git a/src/tallies/filter_mesh.cpp b/src/tallies/filter_mesh.cpp index 5b01da1f65a..03f7da97847 100644 --- a/src/tallies/filter_mesh.cpp +++ b/src/tallies/filter_mesh.cpp @@ -80,7 +80,7 @@ void MeshFilter::set_mesh(int32_t mesh) // perform any additional perparation for mesh tallies here mesh_ = mesh; n_bins_ = model::meshes[mesh_]->n_bins(); - model::meshes[mesh_]->prepare_for_tallies(); + model::meshes[mesh_]->prepare_for_point_location(); } void MeshFilter::set_translation(const Position& translation) diff --git a/src/weight_windows.cpp b/src/weight_windows.cpp index 2eb930965b3..68f7550ae5a 100644 --- a/src/weight_windows.cpp +++ b/src/weight_windows.cpp @@ -147,8 +147,8 @@ WeightWindows::WeightWindows(int32_t id) WeightWindows::WeightWindows(pugi::xml_node node) { // Make sure required elements are present - const vector required_elems {"id", "particle_type", - "energy_bounds", "lower_ww_bounds", "upper_ww_bounds"}; + const vector required_elems { + "id", "particle_type", "lower_ww_bounds", "upper_ww_bounds"}; for (const auto& elem : required_elems) { if (!check_for_node(node, elem.c_str())) { fatal_error(fmt::format("Must specify <{}> for weight windows.", elem)); @@ -165,7 +165,7 @@ WeightWindows::WeightWindows(pugi::xml_node node) // Determine associated mesh int32_t mesh_id = std::stoi(get_node_value(node, "mesh")); - mesh_idx_ = model::mesh_map.at(mesh_id); + set_mesh(model::mesh_map.at(mesh_id)); // energy bounds if (check_for_node(node, "energy_bounds")) @@ -340,6 +340,7 @@ void WeightWindows::set_mesh(int32_t mesh_idx) fatal_error(fmt::format("Could not find a mesh for index {}", mesh_idx)); mesh_idx_ = mesh_idx; + model::meshes[mesh_idx_]->prepare_for_point_location(); allocate_ww_bounds(); } diff --git a/tests/unit_tests/weightwindows/test.py b/tests/unit_tests/weightwindows/test.py index d3da0cd1176..79aadbef0de 100644 --- a/tests/unit_tests/weightwindows/test.py +++ b/tests/unit_tests/weightwindows/test.py @@ -296,4 +296,40 @@ def test_ww_attrs_capi(run_in_tmpdir, model): assert wws.id == 2 assert wws.particle == openmc.ParticleType.PHOTON - openmc.lib.finalize() \ No newline at end of file + openmc.lib.finalize() + + +@pytest.mark.parametrize('library', ('libmesh', 'moab')) +def test_unstructured_mesh_applied_wws(request, run_in_tmpdir, library): + """ + Ensure that weight windows on unstructured mesh work when + they aren't part of a tally or weight window generator + """ + + if library == 'libmesh' and not openmc.lib._libmesh_enabled(): + pytest.skip('LibMesh not enabled in this build.') + if library == 'moab' and not openmc.lib._dagmc_enabled(): + pytest.skip('DAGMC (and MOAB) mesh not enabled in this build.') + + water = openmc.Material(name='water') + water.add_nuclide('H1', 2.0) + water.add_nuclide('O16', 1.0) + water.set_density('g/cc', 1.0) + box = openmc.model.RectangularParallelepiped(*(3*[-10, 10]), boundary_type='vacuum') + cell = openmc.Cell(region=-box, fill=water) + + geometry = openmc.Geometry([cell]) + mesh_file = str(request.fspath.dirpath() / 'test_mesh_tets.exo') + mesh = openmc.UnstructuredMesh(mesh_file, library) + + dummy_wws = np.ones((12_000,)) + + wws = openmc.WeightWindows(mesh, dummy_wws, upper_bound_ratio=5.0) + + model = openmc.Model(geometry) + model.settings.weight_windows = wws + model.settings.weight_windows_on = True + model.settings.run_mode = 'fixed source' + model.settings.particles = 100 + model.settings.batches = 2 + model.run() diff --git a/tests/unit_tests/weightwindows/test_mesh_tets.exo b/tests/unit_tests/weightwindows/test_mesh_tets.exo new file mode 120000 index 00000000000..5bf23b369a3 --- /dev/null +++ b/tests/unit_tests/weightwindows/test_mesh_tets.exo @@ -0,0 +1 @@ +../../regression_tests/unstructured_mesh/test_mesh_tets.e \ No newline at end of file diff --git a/vendor/fmt b/vendor/fmt index d141cdbeb0f..65ac626c585 160000 --- a/vendor/fmt +++ b/vendor/fmt @@ -1 +1 @@ -Subproject commit d141cdbeb0fb422a3fb7173b285fd38e0d1772dc +Subproject commit 65ac626c5856f5aad1f1542e79407a6714357043 From 2450eef4246cae85c7ee8b895458729b7e6dc97c Mon Sep 17 00:00:00 2001 From: Zoe Prieto <101403129+zoeprieto@users.noreply.github.com> Date: Sat, 5 Oct 2024 03:51:56 -0300 Subject: [PATCH 070/184] Introduce ParticleList class for manipulating a list of source particles (#3148) Co-authored-by: Paul Romano --- docs/source/pythonapi/base.rst | 1 + openmc/lib/core.py | 9 +- openmc/source.py | 248 +++++++++++++++--- .../surface_source_write/test.py | 47 ++-- tests/unit_tests/test_source_file.py | 30 ++- 5 files changed, 260 insertions(+), 75 deletions(-) diff --git a/docs/source/pythonapi/base.rst b/docs/source/pythonapi/base.rst index 611d2c216ee..23df02f2ee7 100644 --- a/docs/source/pythonapi/base.rst +++ b/docs/source/pythonapi/base.rst @@ -191,6 +191,7 @@ Post-processing :template: myclass.rst openmc.Particle + openmc.ParticleList openmc.ParticleTrack openmc.StatePoint openmc.Summary diff --git a/openmc/lib/core.py b/openmc/lib/core.py index e646a9ae1df..8561602e670 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -477,7 +477,7 @@ def run(output=True): def sample_external_source( n_samples: int = 1000, prn_seed: int | None = None -) -> list[openmc.SourceParticle]: +) -> openmc.ParticleList: """Sample external source and return source particles. .. versionadded:: 0.13.1 @@ -492,7 +492,7 @@ def sample_external_source( Returns ------- - list of openmc.SourceParticle + openmc.ParticleList List of sampled source particles """ @@ -506,14 +506,13 @@ def sample_external_source( _dll.openmc_sample_external_source(c_size_t(n_samples), c_uint64(prn_seed), sites_array) # Convert to list of SourceParticle and return - return [ - openmc.SourceParticle( + return openmc.ParticleList([openmc.SourceParticle( r=site.r, u=site.u, E=site.E, time=site.time, wgt=site.wgt, delayed_group=site.delayed_group, surf_id=site.surf_id, particle=openmc.ParticleType(site.particle) ) for site in sites_array - ] + ]) def simulation_init(): diff --git a/openmc/source.py b/openmc/source.py index e35e62f5fca..7c8e6af8dab 100644 --- a/openmc/source.py +++ b/openmc/source.py @@ -5,10 +5,12 @@ from numbers import Real import warnings from typing import Any +from pathlib import Path import lxml.etree as ET import numpy as np import h5py +import pandas as pd import openmc import openmc.checkvalue as cv @@ -917,6 +919,34 @@ def from_string(cls, value: str): except KeyError: raise ValueError(f"Invalid string for creation of {cls.__name__}: {value}") + @classmethod + def from_pdg_number(cls, pdg_number: int) -> ParticleType: + """Constructs a ParticleType instance from a PDG number. + + The Particle Data Group at LBNL publishes a Monte Carlo particle + numbering scheme as part of the `Review of Particle Physics + <10.1103/PhysRevD.110.030001>`_. This method maps PDG numbers to the + corresponding :class:`ParticleType`. + + Parameters + ---------- + pdg_number : int + The PDG number of the particle type. + + Returns + ------- + The corresponding ParticleType instance. + """ + try: + return { + 2112: ParticleType.NEUTRON, + 22: ParticleType.PHOTON, + 11: ParticleType.ELECTRON, + -11: ParticleType.POSITRON, + }[pdg_number] + except KeyError: + raise ValueError(f"Unrecognized PDG number: {pdg_number}") + def __repr__(self) -> str: """ Returns a string representation of the ParticleType instance. @@ -930,11 +960,6 @@ def __repr__(self) -> str: def __str__(self) -> str: return self.__repr__() - # needed for <= 3.7, IntEnum will use the mixed-in type's `__format__` method otherwise - # this forces it to default to the standard object format, relying on __str__ under the hood - def __format__(self, spec): - return object.__format__(self, spec) - class SourceParticle: """Source particle @@ -1020,31 +1045,179 @@ def write_source_file( openmc.SourceParticle """ - # Create compound datatype for source particles - pos_dtype = np.dtype([('x', ' ParticleList: + """Create particle list from an HDF5 file. + Parameters + ---------- + filename : path-like + Path to source file to read. + + Returns + ------- + ParticleList instance -def read_source_file(filename: PathLike) -> list[SourceParticle]: + """ + with h5py.File(filename, 'r') as fh: + filetype = fh.attrs['filetype'] + arr = fh['source_bank'][...] + + if filetype != b'source': + raise ValueError(f'File {filename} is not a source file') + + source_particles = [ + SourceParticle(*params, ParticleType(particle)) + for *params, particle in arr + ] + return cls(source_particles) + + @classmethod + def from_mcpl(cls, filename: PathLike) -> ParticleList: + """Create particle list from an MCPL file. + + Parameters + ---------- + filename : path-like + Path to MCPL file to read. + + Returns + ------- + ParticleList instance + + """ + import mcpl + # Process .mcpl file + particles = [] + with mcpl.MCPLFile(filename) as f: + for particle in f.particles: + # Determine particle type based on the PDG number + try: + particle_type = ParticleType.from_pdg_number(particle.pdgcode) + except ValueError: + particle_type = "UNKNOWN" + + # Create a source particle instance. Note that MCPL stores + # energy in MeV and time in ms. + source_particle = SourceParticle( + r=tuple(particle.position), + u=tuple(particle.direction), + E=1.0e6*particle.ekin, + time=1.0e-3*particle.time, + wgt=particle.weight, + particle=particle_type + ) + particles.append(source_particle) + + return cls(particles) + + def __getitem__(self, index): + """ + Return a new ParticleList object containing the particle(s) + at the specified index or slice. + + Parameters + ---------- + index : int, slice or list + The index, slice or list to select from the list of particles + + Returns + ------- + openmc.ParticleList or openmc.SourceParticle + A new object with the selected particle(s) + """ + if isinstance(index, int): + # If it's a single integer, return the corresponding particle + return super().__getitem__(index) + elif isinstance(index, slice): + # If it's a slice, return a new ParticleList object with the + # sliced particles + return ParticleList(super().__getitem__(index)) + elif isinstance(index, list): + # If it's a list of integers, return a new ParticleList object with + # the selected particles. Note that Python 3.10 gets confused if you + # use super() here, so we call list.__getitem__ directly. + return ParticleList([list.__getitem__(self, i) for i in index]) + else: + raise TypeError(f"Invalid index type: {type(index)}. Must be int, " + "slice, or list of int.") + + def to_dataframe(self) -> pd.DataFrame: + """A dataframe representing the source particles + + Returns + ------- + pandas.DataFrame + DataFrame containing the source particles attributes. + """ + # Extract the attributes of the source particles into a list of tuples + data = [(sp.r[0], sp.r[1], sp.r[2], sp.u[0], sp.u[1], sp.u[2], + sp.E, sp.time, sp.wgt, sp.delayed_group, sp.surf_id, + sp.particle.name.lower()) for sp in self] + + # Define the column names for the DataFrame + columns = ['x', 'y', 'z', 'u_x', 'u_y', 'u_z', 'E', 'time', 'wgt', + 'delayed_group', 'surf_id', 'particle'] + + # Create the pandas DataFrame from the data + return pd.DataFrame(data, columns=columns) + + def export_to_hdf5(self, filename: PathLike, **kwargs): + """Export particle list to an HDF5 file. + + This method write out an .h5 file that can be used as a source file in + conjunction with the :class:`openmc.FileSource` class. + + Parameters + ---------- + filename : path-like + Path to source file to write + **kwargs + Keyword arguments to pass to :class:`h5py.File` + + See Also + -------- + openmc.FileSource + + """ + # Create compound datatype for source particles + pos_dtype = np.dtype([('x', ' ParticleList: """Read a source file and return a list of source particles. .. versionadded:: 0.15.0 @@ -1056,23 +1229,18 @@ def read_source_file(filename: PathLike) -> list[SourceParticle]: Returns ------- - list of SourceParticle - Source particles read from file + openmc.ParticleList See Also -------- openmc.SourceParticle """ - with h5py.File(filename, 'r') as fh: - filetype = fh.attrs['filetype'] - arr = fh['source_bank'][...] - - if filetype != b'source': - raise ValueError(f'File {filename} is not a source file') - - source_particles = [] - for *params, particle in arr: - source_particles.append(SourceParticle(*params, ParticleType(particle))) - - return source_particles + filename = Path(filename) + if filename.suffix not in ('.h5', '.mcpl'): + raise ValueError('Source file must have a .h5 or .mcpl extension.') + + if filename.suffix == '.h5': + return ParticleList.from_hdf5(filename) + else: + return ParticleList.from_mcpl(filename) diff --git a/tests/regression_tests/surface_source_write/test.py b/tests/regression_tests/surface_source_write/test.py index fe11d68a6e4..f144eb82a73 100644 --- a/tests/regression_tests/surface_source_write/test.py +++ b/tests/regression_tests/surface_source_write/test.py @@ -608,11 +608,6 @@ def return_surface_source_data(filepath): """Read a surface source file and return a sorted array composed of flatten arrays of source data for each surface source point. - TODO: - - - use read_source_file from source.py instead. Or a dedicated function - to produce sorted list of source points for a given file. - Parameters ---------- filepath : str @@ -629,27 +624,25 @@ def return_surface_source_data(filepath): keys = [] # Read source file - with h5py.File(filepath, "r") as f: - for point in f["source_bank"]: - r = point["r"] - u = point["u"] - e = point["E"] - time = point["time"] - wgt = point["wgt"] - delayed_group = point["delayed_group"] - surf_id = point["surf_id"] - particle = point["particle"] - - key = ( - f"{r[0]:.10e} {r[1]:.10e} {r[2]:.10e} {u[0]:.10e} {u[1]:.10e} {u[2]:.10e}" - f"{e:.10e} {time:.10e} {wgt:.10e} {delayed_group} {surf_id} {particle}" - ) - - keys.append(key) - - values = [*r, *u, e, time, wgt, delayed_group, surf_id, particle] - assert len(values) == 12 - data.append(values) + source = openmc.read_source_file(filepath) + + for point in source: + r = point.r + u = point.u + e = point.E + time = point.time + wgt = point.wgt + delayed_group = point.delayed_group + surf_id = point.surf_id + particle = point.particle + key = ( + f"{r[0]:.10e} {r[1]:.10e} {r[2]:.10e} {u[0]:.10e} {u[1]:.10e} {u[2]:.10e}" + f"{e:.10e} {time:.10e} {wgt:.10e} {delayed_group} {surf_id} {particle}" + ) + keys.append(key) + values = [*r, *u, e, time, wgt, delayed_group, surf_id, particle] + assert len(values) == 12 + data.append(values) data = np.array(data) keys = np.array(keys) @@ -1129,4 +1122,4 @@ def test_surface_source_cell_dagmc( harness = SurfaceSourceWriteTestHarness( "statepoint.5.h5", model=model, workdir=folder ) - harness.main() \ No newline at end of file + harness.main() diff --git a/tests/unit_tests/test_source_file.py b/tests/unit_tests/test_source_file.py index 1b5549b008e..41906c80f83 100644 --- a/tests/unit_tests/test_source_file.py +++ b/tests/unit_tests/test_source_file.py @@ -44,11 +44,9 @@ def test_source_file(run_in_tmpdir): assert np.all(arr['delayed_group'] == 0) assert np.all(arr['particle'] == 0) - # Ensure sites read in are consistent - sites = openmc.read_source_file('test_source.h5') + sites = openmc.ParticleList.from_hdf5('test_source.h5') - assert filetype == b'source' xs = np.array([site.r[0] for site in sites]) ys = np.array([site.r[1] for site in sites]) zs = np.array([site.r[2] for site in sites]) @@ -68,6 +66,32 @@ def test_source_file(run_in_tmpdir): p_types = np.array([s.particle for s in sites]) assert np.all(p_types == 0) + # Ensure a ParticleList item is a SourceParticle + site = sites[0] + assert isinstance(site, openmc.SourceParticle) + assert site.E == pytest.approx(n) + + # Ensure site slice read in and exported are consistent + sites_slice = sites[:10] + sites_slice.export_to_hdf5("test_source_slice.h5") + sites_slice = openmc.ParticleList.from_hdf5('test_source_slice.h5') + + assert isinstance(sites_slice, openmc.ParticleList) + assert len(sites_slice) == 10 + E = np.array([s.E for s in sites_slice]) + np.testing.assert_allclose(E, n - np.arange(10)) + + # Ensure site list read in and exported are consistent + df = sites.to_dataframe() + sites_filtered = sites[df[df.E <= 10.0].index.tolist()] + sites_filtered.export_to_hdf5("test_source_filtered.h5") + sites_filtered = openmc.read_source_file('test_source_filtered.h5') + + assert isinstance(sites_filtered, openmc.ParticleList) + assert len(sites_filtered) == 10 + E = np.array([s.E for s in sites_filtered]) + np.testing.assert_allclose(E, np.arange(10, 0, -1)) + def test_wrong_source_attributes(run_in_tmpdir): # Create a source file with animal attributes From 34f04267a5a6bfe17479c11bce98c1adbbfd4a29 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Sat, 5 Oct 2024 12:28:22 -0500 Subject: [PATCH 071/184] Update fmt submodule to version 11.0.2 (#3162) --- include/openmc/output.h | 2 +- include/openmc/position.h | 2 +- vendor/fmt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/include/openmc/output.h b/include/openmc/output.h index 1ece3de960c..549d2ae32ba 100644 --- a/include/openmc/output.h +++ b/include/openmc/output.h @@ -76,7 +76,7 @@ struct formatter> { } template - auto format(const std::array& arr, FormatContext& ctx) + auto format(const std::array& arr, FormatContext& ctx) const { return format_to(ctx.out(), "({}, {})", arr[0], arr[1]); } diff --git a/include/openmc/position.h b/include/openmc/position.h index 11ea3764792..e6939dc0e46 100644 --- a/include/openmc/position.h +++ b/include/openmc/position.h @@ -221,7 +221,7 @@ namespace fmt { template<> struct formatter : formatter { template - auto format(const openmc::Position& pos, FormatContext& ctx) + auto format(const openmc::Position& pos, FormatContext& ctx) const { return formatter::format( fmt::format("({}, {}, {})", pos.x, pos.y, pos.z), ctx); diff --git a/vendor/fmt b/vendor/fmt index 65ac626c585..0c9fce2ffef 160000 --- a/vendor/fmt +++ b/vendor/fmt @@ -1 +1 @@ -Subproject commit 65ac626c5856f5aad1f1542e79407a6714357043 +Subproject commit 0c9fce2ffefecfdce794e1859584e25877b7b592 From c0acc28038cc13c7d88b3eae17c86615c590f30f Mon Sep 17 00:00:00 2001 From: Matteo Zammataro <103496190+MatteoZammataro@users.noreply.github.com> Date: Tue, 8 Oct 2024 20:29:55 +0200 Subject: [PATCH 072/184] Add dose coefficients from ICRP 74 (#3020) Co-authored-by: matteo.zammataro Co-authored-by: Paul Romano --- openmc/data/effective_dose/dose.py | 110 +++++++++++------- .../{ => icrp116}/electrons.txt | 0 .../{ => icrp116}/helium_ions.txt | 0 .../{ => icrp116}/negative_muons.txt | 0 .../{ => icrp116}/negative_pions.txt | 0 .../effective_dose/{ => icrp116}/neutrons.txt | 0 .../effective_dose/{ => icrp116}/photons.txt | 0 .../{ => icrp116}/photons_kerma.txt | 0 .../{ => icrp116}/positive_muons.txt | 0 .../{ => icrp116}/positive_pions.txt | 0 .../{ => icrp116}/positrons.txt | 0 .../effective_dose/{ => icrp116}/protons.txt | 0 .../icrp74/generate_photon_effective_dose.py | 69 +++++++++++ .../data/effective_dose/icrp74/neutrons.txt | 50 ++++++++ openmc/data/effective_dose/icrp74/photons.txt | 26 +++++ tests/unit_tests/test_data_dose.py | 14 +++ 16 files changed, 228 insertions(+), 41 deletions(-) rename openmc/data/effective_dose/{ => icrp116}/electrons.txt (100%) rename openmc/data/effective_dose/{ => icrp116}/helium_ions.txt (100%) rename openmc/data/effective_dose/{ => icrp116}/negative_muons.txt (100%) rename openmc/data/effective_dose/{ => icrp116}/negative_pions.txt (100%) rename openmc/data/effective_dose/{ => icrp116}/neutrons.txt (100%) rename openmc/data/effective_dose/{ => icrp116}/photons.txt (100%) rename openmc/data/effective_dose/{ => icrp116}/photons_kerma.txt (100%) rename openmc/data/effective_dose/{ => icrp116}/positive_muons.txt (100%) rename openmc/data/effective_dose/{ => icrp116}/positive_pions.txt (100%) rename openmc/data/effective_dose/{ => icrp116}/positrons.txt (100%) rename openmc/data/effective_dose/{ => icrp116}/protons.txt (100%) create mode 100644 openmc/data/effective_dose/icrp74/generate_photon_effective_dose.py create mode 100644 openmc/data/effective_dose/icrp74/neutrons.txt create mode 100644 openmc/data/effective_dose/icrp74/photons.txt diff --git a/openmc/data/effective_dose/dose.py b/openmc/data/effective_dose/dose.py index ae981ee7dc8..c7f458d1c6c 100644 --- a/openmc/data/effective_dose/dose.py +++ b/openmc/data/effective_dose/dose.py @@ -2,40 +2,61 @@ import numpy as np -_FILES = ( - ('electron', 'electrons.txt'), - ('helium', 'helium_ions.txt'), - ('mu-', 'negative_muons.txt'), - ('pi-', 'negative_pions.txt'), - ('neutron', 'neutrons.txt'), - ('photon', 'photons.txt'), - ('photon kerma', 'photons_kerma.txt'), - ('mu+', 'positive_muons.txt'), - ('pi+', 'positive_pions.txt'), - ('positron', 'positrons.txt'), - ('proton', 'protons.txt') -) - -_DOSE_ICRP116 = {} - - -def _load_dose_icrp116(): - """Load effective dose tables from text files""" - for particle, filename in _FILES: - path = Path(__file__).parent / filename - data = np.loadtxt(path, skiprows=3, encoding='utf-8') - data[:, 0] *= 1e6 # Change energies to eV - _DOSE_ICRP116[particle] = data - - -def dose_coefficients(particle, geometry='AP'): - """Return effective dose conversion coefficients from ICRP-116 - - This function provides fluence (and air kerma) to effective dose conversion - coefficients for various types of external exposures based on values in - `ICRP Publication 116 `_. - Corrected values found in a correigendum are used rather than the values in - theoriginal report. +import openmc.checkvalue as cv + +_FILES = { + ('icrp74', 'neutron'): Path('icrp74') / 'neutrons.txt', + ('icrp74', 'photon'): Path('icrp74') / 'photons.txt', + ('icrp116', 'electron'): Path('icrp116') / 'electrons.txt', + ('icrp116', 'helium'): Path('icrp116') / 'helium_ions.txt', + ('icrp116', 'mu-'): Path('icrp116') / 'negative_muons.txt', + ('icrp116', 'pi-'): Path('icrp116') / 'negative_pions.txt', + ('icrp116', 'neutron'): Path('icrp116') / 'neutrons.txt', + ('icrp116', 'photon'): Path('icrp116') / 'photons.txt', + ('icrp116', 'photon kerma'): Path('icrp116') / 'photons_kerma.txt', + ('icrp116', 'mu+'): Path('icrp116') / 'positive_muons.txt', + ('icrp116', 'pi+'): Path('icrp116') / 'positive_pions.txt', + ('icrp116', 'positron'): Path('icrp116') / 'positrons.txt', + ('icrp116', 'proton'): Path('icrp116') / 'protons.txt', +} + +_DOSE_TABLES = {} + + +def _load_dose_icrp(data_source: str, particle: str): + """Load effective dose tables from text files. + + Parameters + ---------- + data_source : {'icrp74', 'icrp116'} + The dose conversion data source to use + particle : {'neutron', 'photon', 'photon kerma', 'electron', 'positron'} + Incident particle + + """ + path = Path(__file__).parent / _FILES[data_source, particle] + data = np.loadtxt(path, skiprows=3, encoding='utf-8') + data[:, 0] *= 1e6 # Change energies to eV + _DOSE_TABLES[data_source, particle] = data + + +def dose_coefficients(particle, geometry='AP', data_source='icrp116'): + """Return effective dose conversion coefficients. + + This function provides fluence (and air kerma) to effective or ambient dose + (H*(10)) conversion coefficients for various types of external exposures + based on values in ICRP publications. Corrected values found in a + corrigendum are used rather than the values in the original report. + Available libraries include `ICRP Publication 74 + ` and `ICRP Publication 116 + `. + + For ICRP 74 data, the photon effective dose per fluence is determined by + multiplying the air kerma per fluence values (Table A.1) by the effective + dose per air kerma (Table A.17). The neutron effective dose per fluence is + found in Table A.41. For ICRP 116 data, the photon effective dose per + fluence is found in Table A.1 and the neutron effective dose per fluence is + found in Table A.5. Parameters ---------- @@ -44,6 +65,8 @@ def dose_coefficients(particle, geometry='AP'): geometry : {'AP', 'PA', 'LLAT', 'RLAT', 'ROT', 'ISO'} Irradiation geometry assumed. Refer to ICRP-116 (Section 3.2) for the meaning of the options here. + data_source : {'icrp74', 'icrp116'} + The data source for the effective dose conversion coefficients. Returns ------- @@ -54,19 +77,24 @@ def dose_coefficients(particle, geometry='AP'): 'photon kerma', the coefficients are given in [Sv/Gy]. """ - if not _DOSE_ICRP116: - _load_dose_icrp116() + + cv.check_value('geometry', geometry, {'AP', 'PA', 'LLAT', 'RLAT', 'ROT', 'ISO'}) + cv.check_value('data_source', data_source, {'icrp74', 'icrp116'}) + + if (data_source, particle) not in _FILES: + raise ValueError(f"{particle} has no dose data in data source {data_source}.") + elif (data_source, particle) not in _DOSE_TABLES: + _load_dose_icrp(data_source, particle) # Get all data for selected particle - data = _DOSE_ICRP116.get(particle) - if data is None: - raise ValueError(f"{particle} has no effective dose data") + data = _DOSE_TABLES[data_source, particle] # Determine index for selected geometry if particle in ('neutron', 'photon', 'proton', 'photon kerma'): - index = ('AP', 'PA', 'LLAT', 'RLAT', 'ROT', 'ISO').index(geometry) + columns = ('AP', 'PA', 'LLAT', 'RLAT', 'ROT', 'ISO') else: - index = ('AP', 'PA', 'ISO').index(geometry) + columns = ('AP', 'PA', 'ISO') + index = columns.index(geometry) # Pull out energy and dose from table energy = data[:, 0].copy() diff --git a/openmc/data/effective_dose/electrons.txt b/openmc/data/effective_dose/icrp116/electrons.txt similarity index 100% rename from openmc/data/effective_dose/electrons.txt rename to openmc/data/effective_dose/icrp116/electrons.txt diff --git a/openmc/data/effective_dose/helium_ions.txt b/openmc/data/effective_dose/icrp116/helium_ions.txt similarity index 100% rename from openmc/data/effective_dose/helium_ions.txt rename to openmc/data/effective_dose/icrp116/helium_ions.txt diff --git a/openmc/data/effective_dose/negative_muons.txt b/openmc/data/effective_dose/icrp116/negative_muons.txt similarity index 100% rename from openmc/data/effective_dose/negative_muons.txt rename to openmc/data/effective_dose/icrp116/negative_muons.txt diff --git a/openmc/data/effective_dose/negative_pions.txt b/openmc/data/effective_dose/icrp116/negative_pions.txt similarity index 100% rename from openmc/data/effective_dose/negative_pions.txt rename to openmc/data/effective_dose/icrp116/negative_pions.txt diff --git a/openmc/data/effective_dose/neutrons.txt b/openmc/data/effective_dose/icrp116/neutrons.txt similarity index 100% rename from openmc/data/effective_dose/neutrons.txt rename to openmc/data/effective_dose/icrp116/neutrons.txt diff --git a/openmc/data/effective_dose/photons.txt b/openmc/data/effective_dose/icrp116/photons.txt similarity index 100% rename from openmc/data/effective_dose/photons.txt rename to openmc/data/effective_dose/icrp116/photons.txt diff --git a/openmc/data/effective_dose/photons_kerma.txt b/openmc/data/effective_dose/icrp116/photons_kerma.txt similarity index 100% rename from openmc/data/effective_dose/photons_kerma.txt rename to openmc/data/effective_dose/icrp116/photons_kerma.txt diff --git a/openmc/data/effective_dose/positive_muons.txt b/openmc/data/effective_dose/icrp116/positive_muons.txt similarity index 100% rename from openmc/data/effective_dose/positive_muons.txt rename to openmc/data/effective_dose/icrp116/positive_muons.txt diff --git a/openmc/data/effective_dose/positive_pions.txt b/openmc/data/effective_dose/icrp116/positive_pions.txt similarity index 100% rename from openmc/data/effective_dose/positive_pions.txt rename to openmc/data/effective_dose/icrp116/positive_pions.txt diff --git a/openmc/data/effective_dose/positrons.txt b/openmc/data/effective_dose/icrp116/positrons.txt similarity index 100% rename from openmc/data/effective_dose/positrons.txt rename to openmc/data/effective_dose/icrp116/positrons.txt diff --git a/openmc/data/effective_dose/protons.txt b/openmc/data/effective_dose/icrp116/protons.txt similarity index 100% rename from openmc/data/effective_dose/protons.txt rename to openmc/data/effective_dose/icrp116/protons.txt diff --git a/openmc/data/effective_dose/icrp74/generate_photon_effective_dose.py b/openmc/data/effective_dose/icrp74/generate_photon_effective_dose.py new file mode 100644 index 00000000000..f8e970137e2 --- /dev/null +++ b/openmc/data/effective_dose/icrp74/generate_photon_effective_dose.py @@ -0,0 +1,69 @@ +from prettytable import PrettyTable +import numpy as np + +# Data from Table A.1 (air kerma per fluence) +energy_a1 = np.array([ + 0.01, 0.015, 0.02, 0.03, 0.04, 0.05, 0.06, 0.08, 0.1, 0.15, 0.2, + 0.3, 0.4, 0.5, 0.6, 0.8, 1.0, 1.5, 2.0, 3.0, 4.0, 5.0, 6.0, 8.0, 10.0 +]) +air_kerma = np.array([7.43, 3.12, 1.68, 0.721, 0.429, 0.323, 0.289, 0.307, 0.371, 0.599, 0.856, 1.38, + 1.89, 2.38, 2.84, 3.69, 4.47, 6.14, 7.55, 9.96, 12.1, 14.1, 16.1, 20.1, 24.0]) + +# Data from Table A.17 (effective dose per air kerma) +energy_a17 = np.array([ + 0.01, 0.015, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08, 0.1, 0.15, 0.2, 0.3, + 0.4, 0.5, 0.6, 0.8, 1.0, 2.0, 4.0, 6.0, 8.0, 10.0 +]) +dose_per_airkerma = { + 'AP': np.array([ + 0.00653, 0.0402, 0.122, 0.416, 0.788, 1.106, 1.308, 1.407, 1.433, 1.394, + 1.256, 1.173, 1.093, 1.056, 1.036, 1.024, 1.010, 1.003, 0.992, 0.993, + 0.993, 0.991, 0.990 + ]), + 'PA': np.array([ + 0.00248, 0.00586, 0.0181, 0.128, 0.370, 0.640, 0.846, 0.966, 1.019, + 1.030, 0.959, 0.915, 0.880, 0.871, 0.869, 0.870, 0.875, 0.880, 0.901, + 0.918, 0.924, 0.927, 0.929 + ]), + 'RLAT': np.array([ + 0.00172, 0.00549, 0.0151, 0.0782, 0.205, 0.345, 0.455, 0.522, 0.554, + 0.571, 0.551, 0.549, 0.557, 0.570, 0.585, 0.600, 0.628, 0.651, 0.728, + 0.796, 0.827, 0.846, 0.860 + ]), + 'LLAT': np.array([ + 0.00172, 0.00549, 0.0155, 0.0904, 0.241, 0.405, 0.528, 0.598, 0.628, + 0.641, 0.620, 0.615, 0.615, 0.623, 0.635, 0.648, 0.670, 0.691, 0.757, + 0.813, 0.836, 0.850, 0.859 + ]), + 'ROT': np.array([ + 0.00326, 0.0153, 0.0462, 0.191, 0.426, 0.661, 0.828, 0.924, 0.961, + 0.960, 0.892, 0.854, 0.824, 0.814, 0.812, 0.814, 0.821, 0.831, 0.871, + 0.909, 0.925, 0.934, 0.941 + ]), + 'ISO': np.array([ + 0.00271, 0.0123, 0.0362, 0.143, 0.326, 0.511, 0.642, 0.720, 0.749, + 0.748, 0.700, 0.679, 0.664, 0.667, 0.675, 0.684, 0.703, 0.719, 0.774, + 0.824, 0.846, 0.859, 0.868 + ]) +} + +# Interpolate air kerma onto energy grid for Table A.17 +air_kerma = np.interp(energy_a17, energy_a1, air_kerma) + +# Compute effective dose per fluence +dose_per_fluence = { + geometry: air_kerma * dose_per_airkerma + for geometry, dose_per_airkerma in dose_per_airkerma.items() +} + +# Create table +table = PrettyTable() +table.field_names = ['Energy (MeV)', 'AP', 'PA', 'LLAT', 'RLAT', 'ROT', 'ISO'] +table.float_format = '.7' +for i, energy in enumerate(energy_a17): + row = [energy] + for geometry in table.field_names[1:]: + row.append(dose_per_fluence[geometry][i]) + table.add_row(row) +print('Photons: Effective dose per fluence, in units of pSv cm², for monoenergetic particles incident in various geometries.\n') +print(table.get_string(border=False)) diff --git a/openmc/data/effective_dose/icrp74/neutrons.txt b/openmc/data/effective_dose/icrp74/neutrons.txt new file mode 100644 index 00000000000..14aab48bd19 --- /dev/null +++ b/openmc/data/effective_dose/icrp74/neutrons.txt @@ -0,0 +1,50 @@ +Neutrons: Effective dose per fluence, in units of pSv cm², for monoenergetic particles incident in various geometries. + +Energy (MeV) AP PA LLAT RLAT ROT ISO +1.00E-09 5.24 3.52 1.68 1.36 2.99 2.4 +1.00E-08 6.55 4.39 2.04 1.7 3.72 2.89 +2.50E-08 7.6 5.16 2.31 1.99 4.4 3.3 +1.00E-07 9.95 6.77 2.86 2.58 5.75 4.13 +2.00E-07 11.2 7.63 3.21 2.92 6.43 4.59 +5.00E-07 12.8 8.76 3.72 3.35 7.27 5.2 +1.00E-06 13.8 9.55 4.12 3.67 7.84 5.63 +2.00E-06 14.5 10.2 4.39 3.89 8.31 5.96 +5.00E-06 15 10.7 4.66 4.08 8.72 6.28 +1.00E-05 15.1 11 4.8 4.16 8.9 6.44 +2.00E-05 15.1 11.1 4.89 4.2 8.92 6.51 +5.00E-05 14.8 11.1 4.95 4.19 8.82 6.51 +1.00E-04 14.6 11 4.95 4.15 8.69 6.45 +2.00E-04 14.4 10.9 4.92 4.1 8.56 6.32 +5.00E-04 14.2 10.7 4.86 4.03 8.4 6.14 +1.00E-03 14.2 10.7 4.84 4 8.34 6.04 +2.00E-03 14.4 10.8 4.87 4 8.39 6.05 +5.00E-03 15.7 11.6 5.25 4.29 9.06 6.52 +1.00E-02 18.3 13.5 6.14 5.02 10.6 7.7 +2.00E-02 23.8 17.3 7.95 6.48 13.8 10.2 +3.00E-02 29 21 9.74 7.93 16.9 12.7 +5.00E-02 38.5 27.6 13.1 10.6 22.7 17.3 +7.00E-02 47.2 33.5 16.1 13.1 27.8 21.5 +1.00E-01 59.8 41.3 20.1 16.4 34.8 27.2 +1.50E-01 80.2 52.2 25.5 21.2 45.4 35.2 +2.00E-01 99 61.5 30.3 25.6 54.8 42.4 +3.00E-01 133 77.1 38.6 33.4 71.6 54.7 +5.00E-01 188 103 53.2 46.8 99.4 75 +7.00E-01 231 124 66.6 58.3 123 92.8 +9.00E-01 267 144 79.6 69.1 144 108 +1 282 154 86 74.5 154 116 +1.2 310 175 99.8 85.8 173 130 +2 383 247 153 129 234 178 +3 432 308 195 171 283 220 +4 458 345 224 198 315 250 +5 474 366 244 217 335 272 +6 483 380 261 232 348 282 +7 490 391 274 244 358 290 +8 494 399 285 253 366 297 +9 497 406 294 261 373 303 +1.00E+01 499 412 302 268 378 309 +1.20E+01 499 422 315 278 385 322 +1.40E+01 496 429 324 286 390 333 +1.50E+01 494 431 328 290 391 338 +1.60E+01 491 433 331 293 393 342 +1.80E+01 486 435 335 299 394 345 +2.00E+01 480 436 338 305 395 343 diff --git a/openmc/data/effective_dose/icrp74/photons.txt b/openmc/data/effective_dose/icrp74/photons.txt new file mode 100644 index 00000000000..1ce3d67e03e --- /dev/null +++ b/openmc/data/effective_dose/icrp74/photons.txt @@ -0,0 +1,26 @@ +Photons: Effective dose per fluence, in units of pSv cm², for monoenergetic particles incident in various geometries. + + Energy (MeV) AP PA LLAT RLAT ROT ISO + 0.0100000 0.0485179 0.0184264 0.0127796 0.0127796 0.0242218 0.0201353 + 0.0150000 0.1254240 0.0182832 0.0171288 0.0171288 0.0477360 0.0383760 + 0.0200000 0.2049600 0.0304080 0.0260400 0.0253680 0.0776160 0.0608160 + 0.0300000 0.2999360 0.0922880 0.0651784 0.0563822 0.1377110 0.1031030 + 0.0400000 0.3380520 0.1587300 0.1033890 0.0879450 0.1827540 0.1398540 + 0.0500000 0.3572380 0.2067200 0.1308150 0.1114350 0.2135030 0.1650530 + 0.0600000 0.3780120 0.2444940 0.1525920 0.1314950 0.2392920 0.1855380 + 0.0700000 0.4192860 0.2878680 0.1782040 0.1555560 0.2753520 0.2145600 + 0.0800000 0.4399310 0.3128330 0.1927960 0.1700780 0.2950270 0.2299430 + 0.1000000 0.5171740 0.3821300 0.2378110 0.2118410 0.3561600 0.2775080 + 0.1500000 0.7523440 0.5744410 0.3713800 0.3300490 0.5343080 0.4193000 + 0.2000000 1.0040880 0.7832400 0.5264400 0.4699440 0.7310240 0.5812240 + 0.3000000 1.5083400 1.2144000 0.8487000 0.7686600 1.1371200 0.9163200 + 0.4000000 1.9958400 1.6461900 1.1774700 1.0773000 1.5384600 1.2606300 + 0.5000000 2.4656800 2.0682200 1.5113000 1.3923000 1.9325600 1.6065000 + 0.6000000 2.9081600 2.4708000 1.8403200 1.7040000 2.3117600 1.9425600 + 0.8000000 3.7269000 3.2287500 2.4723000 2.3173200 3.0294900 2.5940700 + 1.0000000 4.4834100 3.9336000 3.0887700 2.9099700 3.7145700 3.2139300 + 2.0000000 7.4896000 6.8025500 5.7153500 5.4964000 6.5760500 5.8437000 + 4.0000000 12.0153000 11.1078000 9.8373000 9.6316000 10.9989000 9.9704000 + 6.0000000 15.9873000 14.8764000 13.4596000 13.3147000 14.8925000 13.6206000 + 8.0000000 19.9191000 18.6327000 17.0850000 17.0046000 18.7734000 17.2659000 + 10.0000000 23.7600000 22.2960000 20.6160000 20.6400000 22.5840000 20.8320000 diff --git a/tests/unit_tests/test_data_dose.py b/tests/unit_tests/test_data_dose.py index 348143e0b0a..2d80cf8384f 100644 --- a/tests/unit_tests/test_data_dose.py +++ b/tests/unit_tests/test_data_dose.py @@ -22,8 +22,22 @@ def test_dose_coefficients(): assert energy[-1] == approx(10e9) assert dose[-1] == approx(699.0) + energy, dose = dose_coefficients('photon', data_source='icrp74') + assert energy[0] == approx(0.01e6) + assert dose[0] == approx(7.43*0.00653) + assert energy[-1] == approx(10.0e6) + assert dose[-1] == approx(24.0*0.990) + + energy, dose = dose_coefficients('neutron', 'LLAT', data_source='icrp74') + assert energy[0] == approx(1e-3) + assert dose[0] == approx(1.68) + assert energy[-1] == approx(20.0e6) + assert dose[-1] == approx(338.0) + # Invalid particle/geometry should raise an exception with raises(ValueError): dose_coefficients('slime', 'LAT') with raises(ValueError): dose_coefficients('neutron', 'ZZ') + with raises(ValueError): + dose_coefficients('neutron', data_source='icrp7000') From e0471388333b7cd85187e30ebb712c241611cd50 Mon Sep 17 00:00:00 2001 From: Ahnaf Tahmid Chowdhury Date: Wed, 9 Oct 2024 07:57:14 +0600 Subject: [PATCH 073/184] Fix for UWUW Macro Conflict (#3150) Co-authored-by: Patrick Shriwise --- .github/workflows/ci.yml | 5 +++++ CMakeLists.txt | 13 ++++++++----- cmake/OpenMCConfig.cmake.in | 6 +++--- src/dagmc.cpp | 34 ++++++++++++++++++---------------- src/output.cpp | 2 +- 5 files changed, 35 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9293e319b42..c67f1935977 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -131,6 +131,11 @@ jobs: echo "$HOME/NJOY2016/build" >> $GITHUB_PATH $GITHUB_WORKSPACE/tools/ci/gha-install.sh + - name: display-config + shell: bash + run: | + openmc -v + - name: cache-xs uses: actions/cache@v4 with: diff --git a/CMakeLists.txt b/CMakeLists.txt index b4011434e78..575e45373ae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -510,6 +510,14 @@ endif() if(OPENMC_USE_DAGMC) target_compile_definitions(libopenmc PRIVATE DAGMC) target_link_libraries(libopenmc dagmc-shared) + + if(OPENMC_USE_UWUW) + target_compile_definitions(libopenmc PRIVATE OPENMC_UWUW) + target_link_libraries(libopenmc uwuw-shared) + endif() +elseif(OPENMC_USE_UWUW) + set(OPENMC_USE_UWUW OFF) + message(FATAL_ERROR "DAGMC must be enabled when UWUW is enabled.") endif() if(OPENMC_USE_LIBMESH) @@ -546,11 +554,6 @@ if(OPENMC_USE_NCRYSTAL) target_link_libraries(libopenmc NCrystal::NCrystal) endif() -if (OPENMC_USE_UWUW) - target_compile_definitions(libopenmc PRIVATE UWUW) - target_link_libraries(libopenmc uwuw-shared) -endif() - #=============================================================================== # Log build info that this executable can report later #=============================================================================== diff --git a/cmake/OpenMCConfig.cmake.in b/cmake/OpenMCConfig.cmake.in index 44a5e0d5a3f..b3b901de427 100644 --- a/cmake/OpenMCConfig.cmake.in +++ b/cmake/OpenMCConfig.cmake.in @@ -39,6 +39,6 @@ if(@OPENMC_USE_MCPL@) find_package(MCPL REQUIRED) endif() -if(@OPENMC_USE_UWUW@) - find_package(UWUW REQUIRED) -endif() +if(@OPENMC_USE_UWUW@ AND NOT ${DAGMC_BUILD_UWUW}) + message(FATAL_ERROR "UWUW is enabled in OpenMC but the DAGMC installation discovered was not configured with UWUW.") +endif() \ No newline at end of file diff --git a/src/dagmc.cpp b/src/dagmc.cpp index a29a2589f0b..b79676c3626 100644 --- a/src/dagmc.cpp +++ b/src/dagmc.cpp @@ -11,7 +11,7 @@ #include "openmc/settings.h" #include "openmc/string_utils.h" -#ifdef UWUW +#ifdef OPENMC_UWUW #include "uwuw.hpp" #endif #include @@ -29,7 +29,7 @@ const bool DAGMC_ENABLED = true; const bool DAGMC_ENABLED = false; #endif -#ifdef UWUW +#ifdef OPENMC_UWUW const bool UWUW_ENABLED = true; #else const bool UWUW_ENABLED = false; @@ -112,6 +112,11 @@ void DAGUniverse::initialize() { geom_type() = GeometryType::DAG; +#ifdef OPENMC_UWUW + // read uwuw materials from the .h5m file if present + read_uwuw_materials(); +#endif + init_dagmc(); init_metadata(); @@ -431,16 +436,16 @@ void DAGUniverse::to_hdf5(hid_t universes_group) const bool DAGUniverse::uses_uwuw() const { -#ifdef UWUW +#ifdef OPENMC_UWUW return uwuw_ && !uwuw_->material_library.empty(); #else return false; -#endif // UWUW +#endif // OPENMC_UWUW } std::string DAGUniverse::get_uwuw_materials_xml() const { -#ifdef UWUW +#ifdef OPENMC_UWUW if (!uses_uwuw()) { throw std::runtime_error("This DAGMC Universe does not use UWUW materials"); } @@ -460,12 +465,12 @@ std::string DAGUniverse::get_uwuw_materials_xml() const return ss.str(); #else fatal_error("DAGMC was not configured with UWUW."); -#endif // UWUW +#endif // OPENMC_UWUW } void DAGUniverse::write_uwuw_materials_xml(const std::string& outfile) const { -#ifdef UWUW +#ifdef OPENMC_UWUW if (!uses_uwuw()) { throw std::runtime_error( "This DAGMC universe does not use UWUW materials."); @@ -478,7 +483,7 @@ void DAGUniverse::write_uwuw_materials_xml(const std::string& outfile) const mats_xml.close(); #else fatal_error("DAGMC was not configured with UWUW."); -#endif +#endif // OPENMC_UWUW } void DAGUniverse::legacy_assign_material( @@ -540,7 +545,7 @@ void DAGUniverse::legacy_assign_material( void DAGUniverse::read_uwuw_materials() { -#ifdef UWUW +#ifdef OPENMC_UWUW // If no filename was provided, don't read UWUW materials if (filename_ == "") return; @@ -580,16 +585,13 @@ void DAGUniverse::read_uwuw_materials() } #else fatal_error("DAGMC was not configured with UWUW."); -#endif +#endif // OPENMC_UWUW } void DAGUniverse::uwuw_assign_material( moab::EntityHandle vol_handle, std::unique_ptr& c) const { -#ifdef UWUW - // read materials from uwuw material file - read_uwuw_materials(); - +#ifdef OPENMC_UWUW // lookup material in uwuw if present std::string uwuw_mat = dmd_ptr->volume_material_property_data_eh[vol_handle]; if (uwuw_->material_library.count(uwuw_mat) != 0) { @@ -601,11 +603,11 @@ void DAGUniverse::uwuw_assign_material( } else { fatal_error(fmt::format("Material with value '{}' not found in the " "UWUW material library", - mat_str)); + uwuw_mat)); } #else fatal_error("DAGMC was not configured with UWUW."); -#endif +#endif // OPENMC_UWUW } //============================================================================== // DAGMC Cell implementation diff --git a/src/output.cpp b/src/output.cpp index 5fdbea1304e..a430fe9a6c6 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -347,7 +347,7 @@ void print_build_info() #ifdef COVERAGEBUILD coverage = y; #endif -#ifdef UWUW +#ifdef OPENMC_UWUW uwuw = y; #endif From fb3aaa46ac6dac05315c7fe84c3f105fbb74ae46 Mon Sep 17 00:00:00 2001 From: Ahnaf Tahmid Chowdhury Date: Thu, 10 Oct 2024 22:05:32 +0600 Subject: [PATCH 074/184] Improve Detection of libMesh Installation via `LIBMESH_ROOT` and CMake's PkgConfig (#3149) --- .github/workflows/ci.yml | 1 - cmake/Modules/FindLIBMESH.cmake | 4 ++-- tools/ci/gha-install-libmesh.sh | 1 - tools/ci/gha-install.py | 7 ++++--- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c67f1935977..5973c3cd48d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,7 +96,6 @@ jobs: - name: Environment Variables run: | - echo "DAGMC_ROOT=$HOME/DAGMC" echo "OPENMC_CROSS_SECTIONS=$HOME/nndc_hdf5/cross_sections.xml" >> $GITHUB_ENV echo "OPENMC_ENDF_DATA=$HOME/endf-b-vii.1" >> $GITHUB_ENV diff --git a/cmake/Modules/FindLIBMESH.cmake b/cmake/Modules/FindLIBMESH.cmake index 048dfc2a8b8..df9208c18b2 100644 --- a/cmake/Modules/FindLIBMESH.cmake +++ b/cmake/Modules/FindLIBMESH.cmake @@ -15,7 +15,7 @@ if(DEFINED ENV{METHOD}) endif() find_package(PkgConfig REQUIRED) -set(ENV{PKG_CONFIG_PATH} "$ENV{PKG_CONFIG_PATH}:${LIBMESH_PC}") -set(PKG_CONFIG_USE_CMAKE_PREFIX_PATH True) + +set(PKG_CONFIG_USE_CMAKE_PREFIX_PATH TRUE) pkg_check_modules(LIBMESH REQUIRED ${LIBMESH_PC_FILE}>=1.7.0 IMPORTED_TARGET) pkg_get_variable(LIBMESH_PREFIX ${LIBMESH_PC_FILE} prefix) diff --git a/tools/ci/gha-install-libmesh.sh b/tools/ci/gha-install-libmesh.sh index cb808ae5b3b..d4557d2d3a2 100755 --- a/tools/ci/gha-install-libmesh.sh +++ b/tools/ci/gha-install-libmesh.sh @@ -16,7 +16,6 @@ else ../libmesh/configure --prefix=$HOME/LIBMESH --enable-exodus --disable-netcdf-4 --disable-eigen --disable-lapack --disable-mpi fi make -j4 install -export LIBMESH_PC=$HOME/LIBMESH/lib/pkgconfig/ rm -rf $HOME/LIBMESH/build popd diff --git a/tools/ci/gha-install.py b/tools/ci/gha-install.py index f046e863470..282389a8f19 100644 --- a/tools/ci/gha-install.py +++ b/tools/ci/gha-install.py @@ -31,7 +31,8 @@ def install(omp=False, mpi=False, phdf5=False, dagmc=False, libmesh=False, ncrys if dagmc: cmake_cmd.append('-DOPENMC_USE_DAGMC=ON') - cmake_cmd.append('-DCMAKE_PREFIX_PATH=~/DAGMC') + dagmc_path = os.environ.get('HOME') + '/DAGMC' + cmake_cmd.append('-DCMAKE_PREFIX_PATH=' + dagmc_path) if libmesh: cmake_cmd.append('-DOPENMC_USE_LIBMESH=ON') @@ -40,8 +41,8 @@ def install(omp=False, mpi=False, phdf5=False, dagmc=False, libmesh=False, ncrys if ncrystal: cmake_cmd.append('-DOPENMC_USE_NCRYSTAL=ON') - ncrystal_cmake_path = os.environ.get('HOME') + '/ncrystal_inst/lib/cmake' - cmake_cmd.append(f'-DCMAKE_PREFIX_PATH={ncrystal_cmake_path}') + ncrystal_path = os.environ.get('HOME') + '/ncrystal_inst' + cmake_cmd.append(f'-DCMAKE_PREFIX_PATH={ncrystal_path}') # Build in coverage mode for coverage testing cmake_cmd.append('-DOPENMC_ENABLE_COVERAGE=on') From 579777a3e5f84ace43b19d26379dd4f85af5d30c Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 10 Oct 2024 12:17:40 -0500 Subject: [PATCH 075/184] Consistency in treatment of paths for files specified within the Model class (#3153) --- docs/source/usersguide/geometry.rst | 25 ++++++++---- openmc/config.py | 27 +++++++++++-- openmc/material.py | 3 +- openmc/mesh.py | 9 ++--- openmc/settings.py | 36 +++++++++++------- openmc/source.py | 38 +++++++++---------- openmc/universe.py | 14 +++---- openmc/utility_funcs.py | 22 +++++++++++ tests/conftest.py | 7 ++++ tests/regression_tests/source_dlopen/test.py | 3 +- .../source_parameterized_dlopen/test.py | 3 +- tests/unit_tests/test_config.py | 12 +++++- tests/unit_tests/test_settings.py | 2 +- tests/unit_tests/test_source.py | 8 ++-- tests/unit_tests/test_temp_interp.py | 4 +- 15 files changed, 142 insertions(+), 71 deletions(-) diff --git a/docs/source/usersguide/geometry.rst b/docs/source/usersguide/geometry.rst index 3a3d02231ad..6f14ebfa51c 100644 --- a/docs/source/usersguide/geometry.rst +++ b/docs/source/usersguide/geometry.rst @@ -474,7 +474,7 @@ applied as universes in the OpenMC geometry file. A geometry represented entirely by a DAGMC geometry will contain only the DAGMC universe. Using a :class:`openmc.DAGMCUniverse` looks like the following:: - dag_univ = openmc.DAGMCUniverse(filename='dagmc.h5m') + dag_univ = openmc.DAGMCUniverse('dagmc.h5m') geometry = openmc.Geometry(dag_univ) geometry.export_to_xml() @@ -495,13 +495,22 @@ It is important in these cases to understand the DAGMC model's position with respect to the CSG geometry. DAGMC geometries can be plotted with OpenMC to verify that the model matches one's expectations. -**Note:** DAGMC geometries used in OpenMC are currently required to be clean, -meaning that all surfaces have been `imprinted and merged -`_ successfully -and that the model is `watertight -`_. -Future implementations of DAGMC geometry will support small volume overlaps and -un-merged surfaces. +By default, when you specify a .h5m file for a :class:`~openmc.DAGMCUniverse` +instance, it will store the absolute path to the .h5m file. If you prefer to +store the relative path, you can set the ``'resolve_paths'`` configuration +variable:: + + openmc.config['resolve_paths'] = False + dag_univ = openmc.DAGMCUniverse('dagmc.h5m') + +.. note:: + DAGMC geometries used in OpenMC are currently required to be clean, + meaning that all surfaces have been `imprinted and merged + `_ successfully + and that the model is `watertight + `_. + Future implementations of DAGMC geometry will support small volume overlaps and + un-merged surfaces. Cell, Surface, and Material IDs ------------------------------- diff --git a/openmc/config.py b/openmc/config.py index b823d6b06b2..ab53ab61b5f 100644 --- a/openmc/config.py +++ b/openmc/config.py @@ -1,4 +1,5 @@ from collections.abc import MutableMapping +from contextlib import contextmanager import os from pathlib import Path import warnings @@ -11,7 +12,7 @@ class _Config(MutableMapping): def __init__(self, data=()): - self._mapping = {} + self._mapping = {'resolve_paths': True} self.update(data) def __getitem__(self, key): @@ -42,10 +43,12 @@ def __setitem__(self, key, value): # Reset photon source data since it relies on chain file _DECAY_PHOTON_ENERGY.clear() _DECAY_ENERGY.clear() + elif key == 'resolve_paths': + self._mapping[key] = value else: raise KeyError(f'Unrecognized config key: {key}. Acceptable keys ' - 'are "cross_sections", "mg_cross_sections" and ' - '"chain_file"') + 'are "cross_sections", "mg_cross_sections", ' + '"chain_file", and "resolve_paths".') def __iter__(self): return iter(self._mapping) @@ -61,6 +64,24 @@ def _set_path(self, key, value): if not p.exists(): warnings.warn(f"'{value}' does not exist.") + @contextmanager + def patch(self, key, value): + """Temporarily change a value in the configuration. + + Parameters + ---------- + key : str + Key to change + value : object + New value + """ + previous_value = self.get(key) + self[key] = value + yield + if previous_value is None: + del self[key] + else: + self[key] = previous_value def _default_config(): """Return default configuration""" diff --git a/openmc/material.py b/openmc/material.py index f550fd64900..74a403c1535 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -16,6 +16,7 @@ import openmc.checkvalue as cv from ._xml import clean_indentation, reorder_attributes from .mixin import IDManagerMixin +from .utility_funcs import input_path from openmc.checkvalue import PathLike from openmc.stats import Univariate, Discrete, Mixture from openmc.data.data import _get_element_symbol @@ -1643,7 +1644,7 @@ def cross_sections(self) -> Path | None: @cross_sections.setter def cross_sections(self, cross_sections): if cross_sections is not None: - self._cross_sections = Path(cross_sections) + self._cross_sections = input_path(cross_sections) def append(self, material): """Append material to collection diff --git a/openmc/mesh.py b/openmc/mesh.py index a706b8fa811..6afe5d36eea 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -5,8 +5,6 @@ from functools import wraps from math import pi, sqrt, atan2 from numbers import Integral, Real -from pathlib import Path -import tempfile import h5py import lxml.etree as ET @@ -19,6 +17,7 @@ from ._xml import get_text from .mixin import IDManagerMixin from .surface import _BOUNDARY_TYPES +from .utility_funcs import input_path class MeshBase(IDManagerMixin, ABC): @@ -2072,7 +2071,7 @@ class UnstructuredMesh(MeshBase): Parameters ---------- - filename : str or pathlib.Path + filename : path-like Location of the unstructured mesh file library : {'moab', 'libmesh'} Mesh library used for the unstructured mesh tally @@ -2158,8 +2157,8 @@ def filename(self): @filename.setter def filename(self, filename): - cv.check_type('Unstructured Mesh filename', filename, (str, Path)) - self._filename = filename + cv.check_type('Unstructured Mesh filename', filename, PathLike) + self._filename = input_path(filename) @property def library(self): diff --git a/openmc/settings.py b/openmc/settings.py index 96e6368e4f3..0a78fb564f8 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -8,12 +8,14 @@ import lxml.etree as ET import openmc.checkvalue as cv +from openmc.checkvalue import PathLike from openmc.stats.multivariate import MeshSpatial -from . import (RegularMesh, SourceBase, MeshSource, IndependentSource, - VolumeCalculation, WeightWindows, WeightWindowGenerator) from ._xml import clean_indentation, get_text, reorder_attributes -from openmc.checkvalue import PathLike -from .mesh import _read_meshes +from .mesh import _read_meshes, RegularMesh +from .source import SourceBase, MeshSource, IndependentSource +from .utility_funcs import input_path +from .volume import VolumeCalculation +from .weight_windows import WeightWindows, WeightWindowGenerator class RunMode(Enum): @@ -699,14 +701,18 @@ def surf_source_read(self) -> dict: return self._surf_source_read @surf_source_read.setter - def surf_source_read(self, surf_source_read: dict): - cv.check_type('surface source reading options', surf_source_read, Mapping) - for key, value in surf_source_read.items(): + def surf_source_read(self, ssr: dict): + cv.check_type('surface source reading options', ssr, Mapping) + for key, value in ssr.items(): cv.check_value('surface source reading key', key, ('path')) if key == 'path': - cv.check_type('path to surface source file', value, str) - self._surf_source_read = surf_source_read + cv.check_type('path to surface source file', value, PathLike) + self._surf_source_read = dict(ssr) + + # Resolve path to surface source file + if 'path' in ssr: + self._surf_source_read['path'] = input_path(ssr['path']) @property def surf_source_write(self) -> dict: @@ -1066,8 +1072,8 @@ def weight_windows_file(self) -> PathLike | None: @weight_windows_file.setter def weight_windows_file(self, value: PathLike): - cv.check_type('weight windows file', value, (str, Path)) - self._weight_windows_file = value + cv.check_type('weight windows file', value, PathLike) + self._weight_windows_file = input_path(value) @property def weight_window_generators(self) -> list[WeightWindowGenerator]: @@ -1241,7 +1247,7 @@ def _create_surf_source_read_subelement(self, root): element = ET.SubElement(root, "surf_source_read") if 'path' in self._surf_source_read: subelement = ET.SubElement(element, "path") - subelement.text = self._surf_source_read['path'] + subelement.text = str(self._surf_source_read['path']) def _create_surf_source_write_subelement(self, root): if self._surf_source_write: @@ -1501,7 +1507,7 @@ def _create_weight_window_generators_subelement(self, root, mesh_memo=None): def _create_weight_windows_file_element(self, root): if self.weight_windows_file is not None: element = ET.Element("weight_windows_file") - element.text = self.weight_windows_file + element.text = str(self.weight_windows_file) root.append(element) def _create_weight_window_checkpoints_subelement(self, root): @@ -1645,9 +1651,11 @@ def _sourcepoint_from_xml_element(self, root): def _surf_source_read_from_xml_element(self, root): elem = root.find('surf_source_read') if elem is not None: + ssr = {} value = get_text(elem, 'path') if value is not None: - self.surf_source_read['path'] = value + ssr['path'] = value + self.surf_source_read = ssr def _surf_source_write_from_xml_element(self, root): elem = root.find('surf_source_write') diff --git a/openmc/source.py b/openmc/source.py index 7c8e6af8dab..878f52c3b22 100644 --- a/openmc/source.py +++ b/openmc/source.py @@ -3,6 +3,7 @@ from collections.abc import Iterable, Sequence from enum import IntEnum from numbers import Real +from pathlib import Path import warnings from typing import Any from pathlib import Path @@ -19,6 +20,7 @@ from openmc.stats.univariate import Univariate from ._xml import get_text from .mesh import MeshBase, StructuredMesh, UnstructuredMesh +from .utility_funcs import input_path class SourceBase(ABC): @@ -664,7 +666,7 @@ class CompiledSource(SourceBase): Parameters ---------- - library : str or None + library : path-like Path to a compiled shared library parameters : str Parameters to be provided to the compiled shared library function @@ -686,7 +688,7 @@ class CompiledSource(SourceBase): Attributes ---------- - library : str or None + library : pathlib.Path Path to a compiled shared library parameters : str Parameters to be provided to the compiled shared library function @@ -702,17 +704,13 @@ class CompiledSource(SourceBase): """ def __init__( self, - library: str | None = None, + library: PathLike, parameters: str | None = None, strength: float = 1.0, constraints: dict[str, Any] | None = None ) -> None: super().__init__(strength=strength, constraints=constraints) - - self._library = None - if library is not None: - self.library = library - + self.library = library self._parameters = None if parameters is not None: self.parameters = parameters @@ -722,13 +720,13 @@ def type(self) -> str: return "compiled" @property - def library(self) -> str: + def library(self) -> Path: return self._library @library.setter - def library(self, library_name): - cv.check_type('library', library_name, str) - self._library = library_name + def library(self, library_name: PathLike): + cv.check_type('library', library_name, PathLike) + self._library = input_path(library_name) @property def parameters(self) -> str: @@ -748,7 +746,7 @@ def populate_xml_element(self, element): XML element containing source data """ - element.set("library", self.library) + element.set("library", str(self.library)) if self.parameters is not None: element.set("parameters", self.parameters) @@ -794,7 +792,7 @@ class FileSource(SourceBase): Parameters ---------- - path : str or pathlib.Path + path : path-like Path to the source file from which sites should be sampled strength : float Strength of the source (default is 1.0) @@ -829,14 +827,12 @@ class FileSource(SourceBase): def __init__( self, - path: PathLike | None = None, + path: PathLike, strength: float = 1.0, constraints: dict[str, Any] | None = None ): super().__init__(strength=strength, constraints=constraints) - self._path = None - if path is not None: - self.path = path + self.path = path @property def type(self) -> str: @@ -848,8 +844,8 @@ def path(self) -> PathLike: @path.setter def path(self, p: PathLike): - cv.check_type('source file', p, str) - self._path = p + cv.check_type('source file', p, PathLike) + self._path = input_path(p) def populate_xml_element(self, element): """Add necessary file source information to an XML element @@ -861,7 +857,7 @@ def populate_xml_element(self, element): """ if self.path is not None: - element.set("file", self.path) + element.set("file", str(self.path)) @classmethod def from_xml_element(cls, elem: ET.Element) -> openmc.FileSource: diff --git a/openmc/universe.py b/openmc/universe.py index d424c243bd4..648b773df6f 100644 --- a/openmc/universe.py +++ b/openmc/universe.py @@ -17,6 +17,7 @@ from .checkvalue import check_type, check_value from .mixin import IDManagerMixin from .surface import _BOUNDARY_TYPES +from .utility_funcs import input_path class UniverseBase(ABC, IDManagerMixin): @@ -766,7 +767,7 @@ class DAGMCUniverse(UniverseBase): Parameters ---------- - filename : str + filename : path-like Path to the DAGMC file used to represent this universe. universe_id : int, optional Unique identifier of the universe. If not specified, an identifier will @@ -820,7 +821,7 @@ class DAGMCUniverse(UniverseBase): """ def __init__(self, - filename, + filename: cv.PathLike, universe_id=None, name='', auto_geom_ids=False, @@ -850,9 +851,9 @@ def filename(self): return self._filename @filename.setter - def filename(self, val): - cv.check_type('DAGMC filename', val, (Path, str)) - self._filename = val + def filename(self, val: cv.PathLike): + cv.check_type('DAGMC filename', val, cv.PathLike) + self._filename = input_path(val) @property def auto_geom_ids(self): @@ -915,8 +916,7 @@ def _n_geom_elements(self, geom_type): def decode_str_tag(tag_val): return tag_val.tobytes().decode().replace('\x00', '') - dagmc_filepath = Path(self.filename).resolve() - with h5py.File(dagmc_filepath) as dagmc_file: + with h5py.File(self.filename) as dagmc_file: category_data = dagmc_file['tstt/tags/CATEGORY/values'] category_strs = map(decode_str_tag, category_data) n = sum([v == geom_type.capitalize() for v in category_strs]) diff --git a/openmc/utility_funcs.py b/openmc/utility_funcs.py index 3dff45380c1..da9f73b1651 100644 --- a/openmc/utility_funcs.py +++ b/openmc/utility_funcs.py @@ -3,8 +3,10 @@ from pathlib import Path from tempfile import TemporaryDirectory +import openmc from .checkvalue import PathLike + @contextmanager def change_directory(working_dir: PathLike | None = None, *, tmpdir: bool = False): """Context manager for executing in a provided working directory @@ -35,3 +37,23 @@ def change_directory(working_dir: PathLike | None = None, *, tmpdir: bool = Fals os.chdir(orig_dir) if tmpdir: tmp.cleanup() + + +def input_path(filename: PathLike) -> Path: + """Return a path object for an input file based on global configuration + + Parameters + ---------- + filename : PathLike + Path to input file + + Returns + ------- + pathlib.Path + Path object + + """ + if openmc.config['resolve_paths']: + return Path(filename).resolve() + else: + return Path(filename) diff --git a/tests/conftest.py b/tests/conftest.py index 639d669f3a8..cd86da53900 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ import pytest +import openmc from tests.regression_tests import config as regression_config @@ -27,3 +28,9 @@ def run_in_tmpdir(tmpdir): yield finally: orig.chdir() + + +@pytest.fixture(scope='session', autouse=True) +def resolve_paths(): + with openmc.config.patch('resolve_paths', False): + yield diff --git a/tests/regression_tests/source_dlopen/test.py b/tests/regression_tests/source_dlopen/test.py index 88ff9dd8509..0581d6deec4 100644 --- a/tests/regression_tests/source_dlopen/test.py +++ b/tests/regression_tests/source_dlopen/test.py @@ -72,8 +72,7 @@ def model(): model.tallies = openmc.Tallies([tally]) # custom source from shared library - source = openmc.CompiledSource() - source.library = 'build/libsource.so' + source = openmc.CompiledSource('build/libsource.so') model.settings.source = source return model diff --git a/tests/regression_tests/source_parameterized_dlopen/test.py b/tests/regression_tests/source_parameterized_dlopen/test.py index 1cc253528cb..151fb37356e 100644 --- a/tests/regression_tests/source_parameterized_dlopen/test.py +++ b/tests/regression_tests/source_parameterized_dlopen/test.py @@ -71,8 +71,7 @@ def model(): model.tallies = openmc.Tallies([tally]) # custom source from shared library - source = openmc.CompiledSource() - source.library = 'build/libsource.so' + source = openmc.CompiledSource('build/libsource.so') source.parameters = '1e3' model.settings.source = source diff --git a/tests/unit_tests/test_config.py b/tests/unit_tests/test_config.py index 1d3c0f173b0..9d3f53a7403 100644 --- a/tests/unit_tests/test_config.py +++ b/tests/unit_tests/test_config.py @@ -19,7 +19,10 @@ def test_config_basics(): assert isinstance(openmc.config, Mapping) for key, value in openmc.config.items(): assert isinstance(key, str) - assert isinstance(value, os.PathLike) + if key == 'resolve_paths': + assert isinstance(value, bool) + else: + assert isinstance(value, os.PathLike) # Set and delete openmc.config['cross_sections'] = '/path/to/cross_sections.xml' @@ -32,6 +35,13 @@ def test_config_basics(): openmc.config['🐖'] = '/like/to/eat/bacon' +def test_config_patch(): + openmc.config['cross_sections'] = '/path/to/cross_sections.xml' + with openmc.config.patch('cross_sections', '/path/to/other.xml'): + assert str(openmc.config['cross_sections']) == '/path/to/other.xml' + assert str(openmc.config['cross_sections']) == '/path/to/cross_sections.xml' + + def test_config_set_envvar(): openmc.config['cross_sections'] = '/path/to/cross_sections.xml' assert os.environ['OPENMC_CROSS_SECTIONS'] == '/path/to/cross_sections.xml' diff --git a/tests/unit_tests/test_settings.py b/tests/unit_tests/test_settings.py index 650bfd18680..02a47625162 100644 --- a/tests/unit_tests/test_settings.py +++ b/tests/unit_tests/test_settings.py @@ -91,7 +91,7 @@ def test_export_to_xml(run_in_tmpdir): assert s.sourcepoint == {'batches': [50, 150, 500, 1000], 'separate': True, 'write': True, 'overwrite': True, 'mcpl': True} assert s.statepoint == {'batches': [50, 150, 500, 1000]} - assert s.surf_source_read == {'path': 'surface_source_1.h5'} + assert s.surf_source_read['path'].name == 'surface_source_1.h5' assert s.surf_source_write == {'surface_ids': [2], 'max_particles': 200} assert s.confidence_intervals assert s.ptables diff --git a/tests/unit_tests/test_source.py b/tests/unit_tests/test_source.py index 9a19f6f24dd..32650d54936 100644 --- a/tests/unit_tests/test_source.py +++ b/tests/unit_tests/test_source.py @@ -53,7 +53,7 @@ def test_spherical_uniform(): def test_source_file(): filename = 'source.h5' src = openmc.FileSource(path=filename) - assert src.path == filename + assert src.path.name == filename elem = src.to_xml_element() assert 'strength' in elem.attrib @@ -61,9 +61,9 @@ def test_source_file(): def test_source_dlopen(): - library = './libsource.so' - src = openmc.CompiledSource(library=library) - assert src.library == library + library = 'libsource.so' + src = openmc.CompiledSource(library) + assert src.library.name == library elem = src.to_xml_element() assert 'library' in elem.attrib diff --git a/tests/unit_tests/test_temp_interp.py b/tests/unit_tests/test_temp_interp.py index 4c2882347b3..4566070cfd9 100644 --- a/tests/unit_tests/test_temp_interp.py +++ b/tests/unit_tests/test_temp_interp.py @@ -152,7 +152,7 @@ def model(tmp_path_factory): mat = openmc.Material() mat.add_nuclide('U235', 1.0) model.materials.append(mat) - model.materials.cross_sections = str(Path('cross_sections_fake.xml').resolve()) + model.materials.cross_sections = 'cross_sections_fake.xml' sph = openmc.Sphere(r=100.0, boundary_type='reflective') cell = openmc.Cell(fill=mat, region=-sph) @@ -257,7 +257,7 @@ def test_temperature_slightly_above(run_in_tmpdir): mat2.add_nuclide('U235', 1.0) mat2.temperature = 600.0 model.materials.extend([mat1, mat2]) - model.materials.cross_sections = str(Path('cross_sections_fake.xml').resolve()) + model.materials.cross_sections = 'cross_sections_fake.xml' sph1 = openmc.Sphere(r=1.0) sph2 = openmc.Sphere(r=4.0, boundary_type='reflective') From 91fd60be69caf42b615f79f3b75c52843009033e Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 10 Oct 2024 12:58:15 -0500 Subject: [PATCH 076/184] Immediately resolve complement operators for regions (#3145) --- openmc/model/surface_composite.py | 8 ----- openmc/region.py | 30 +++++++++---------- openmc/surface.py | 3 +- .../filter_mesh/inputs_true.dat | 2 +- .../filter_translations/inputs_true.dat | 2 +- .../mgxs_library_mesh/inputs_true.dat | 2 +- .../photon_production_inputs_true.dat | 2 +- .../photon_production/inputs_true.dat | 2 +- .../score_current/inputs_true.dat | 2 +- tests/unit_tests/test_region.py | 2 +- 10 files changed, 24 insertions(+), 31 deletions(-) diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index a2cb0243849..e2a5f49c4d0 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -778,12 +778,6 @@ def __init__(self, x0=0., y0=0., z0=0., r2=1., up=True, **kwargs): def __neg__(self): return -self.cone & (+self.plane if self.up else -self.plane) - def __pos__(self): - if self.up: - return (+self.cone & +self.plane) | -self.plane - else: - return (+self.cone & -self.plane) | +self.plane - class YConeOneSided(CompositeSurface): """One-sided cone parallel the y-axis @@ -836,7 +830,6 @@ def __init__(self, x0=0., y0=0., z0=0., r2=1., up=True, **kwargs): self.up = up __neg__ = XConeOneSided.__neg__ - __pos__ = XConeOneSided.__pos__ class ZConeOneSided(CompositeSurface): @@ -890,7 +883,6 @@ def __init__(self, x0=0., y0=0., z0=0., r2=1., up=True, **kwargs): self.up = up __neg__ = XConeOneSided.__neg__ - __pos__ = XConeOneSided.__pos__ class Polygon(CompositeSurface): diff --git a/openmc/region.py b/openmc/region.py index e509b152805..e1cb834757a 100644 --- a/openmc/region.py +++ b/openmc/region.py @@ -1,3 +1,4 @@ +from __future__ import annotations from abc import ABC, abstractmethod from collections.abc import MutableSequence from copy import deepcopy @@ -30,8 +31,9 @@ def __and__(self, other): def __or__(self, other): return Union((self, other)) - def __invert__(self): - return Complement(self) + @abstractmethod + def __invert__(self) -> Region: + pass @abstractmethod def __contains__(self, point): @@ -442,6 +444,9 @@ def __iand__(self, other): self.append(other) return self + def __invert__(self) -> Union: + return Union(~n for n in self) + # Implement mutable sequence protocol by delegating to list def __getitem__(self, key): return self._nodes[key] @@ -530,6 +535,9 @@ def __ior__(self, other): self.append(other) return self + def __invert__(self) -> Intersection: + return Intersection(~n for n in self) + # Implement mutable sequence protocol by delegating to list def __getitem__(self, key): return self._nodes[key] @@ -603,7 +611,7 @@ class Complement(Region): """ - def __init__(self, node): + def __init__(self, node: Region): self.node = node def __contains__(self, point): @@ -622,6 +630,9 @@ def __contains__(self, point): """ return point not in self.node + def __invert__(self) -> Region: + return self.node + def __str__(self): return '~' + str(self.node) @@ -637,18 +648,7 @@ def node(self, node): @property def bounding_box(self) -> BoundingBox: - # Use De Morgan's laws to distribute the complement operator so that it - # only applies to surface half-spaces, thus allowing us to calculate the - # bounding box in the usual recursive manner. - if isinstance(self.node, Union): - temp_region = Intersection(~n for n in self.node) - elif isinstance(self.node, Intersection): - temp_region = Union(~n for n in self.node) - elif isinstance(self.node, Complement): - temp_region = self.node.node - else: - temp_region = ~self.node - return temp_region.bounding_box + return (~self.node).bounding_box def get_surfaces(self, surfaces=None): """Recursively find and return all the surfaces referenced by the node diff --git a/openmc/surface.py b/openmc/surface.py index 2f10750a89b..537823ba16a 100644 --- a/openmc/surface.py +++ b/openmc/surface.py @@ -1,3 +1,4 @@ +from __future__ import annotations from abc import ABC, abstractmethod from collections.abc import Iterable from copy import deepcopy @@ -2631,7 +2632,7 @@ def __or__(self, other): else: return Union((self, other)) - def __invert__(self): + def __invert__(self) -> Halfspace: return -self.surface if self.side == '+' else +self.surface def __contains__(self, point): diff --git a/tests/regression_tests/filter_mesh/inputs_true.dat b/tests/regression_tests/filter_mesh/inputs_true.dat index a58d58bab95..0667c034148 100644 --- a/tests/regression_tests/filter_mesh/inputs_true.dat +++ b/tests/regression_tests/filter_mesh/inputs_true.dat @@ -12,7 +12,7 @@ - + diff --git a/tests/regression_tests/filter_translations/inputs_true.dat b/tests/regression_tests/filter_translations/inputs_true.dat index a80ccd87675..5004c3217ab 100644 --- a/tests/regression_tests/filter_translations/inputs_true.dat +++ b/tests/regression_tests/filter_translations/inputs_true.dat @@ -12,7 +12,7 @@ - + diff --git a/tests/regression_tests/mgxs_library_mesh/inputs_true.dat b/tests/regression_tests/mgxs_library_mesh/inputs_true.dat index bb9c99026d0..1ecb7a2d3b4 100644 --- a/tests/regression_tests/mgxs_library_mesh/inputs_true.dat +++ b/tests/regression_tests/mgxs_library_mesh/inputs_true.dat @@ -12,7 +12,7 @@ - + diff --git a/tests/regression_tests/model_xml/photon_production_inputs_true.dat b/tests/regression_tests/model_xml/photon_production_inputs_true.dat index ee6cb88622b..10f0bad9841 100644 --- a/tests/regression_tests/model_xml/photon_production_inputs_true.dat +++ b/tests/regression_tests/model_xml/photon_production_inputs_true.dat @@ -9,7 +9,7 @@ - + diff --git a/tests/regression_tests/photon_production/inputs_true.dat b/tests/regression_tests/photon_production/inputs_true.dat index ee6cb88622b..10f0bad9841 100644 --- a/tests/regression_tests/photon_production/inputs_true.dat +++ b/tests/regression_tests/photon_production/inputs_true.dat @@ -9,7 +9,7 @@ - + diff --git a/tests/regression_tests/score_current/inputs_true.dat b/tests/regression_tests/score_current/inputs_true.dat index 9ea8e5e4ae1..ddbd6c24b05 100644 --- a/tests/regression_tests/score_current/inputs_true.dat +++ b/tests/regression_tests/score_current/inputs_true.dat @@ -12,7 +12,7 @@ - + diff --git a/tests/unit_tests/test_region.py b/tests/unit_tests/test_region.py index 8c9e0afe4d9..cbcd1983125 100644 --- a/tests/unit_tests/test_region.py +++ b/tests/unit_tests/test_region.py @@ -106,7 +106,7 @@ def test_complement(reset): assert_unbounded(outside_equiv) # string represention - assert str(inside) == '~(1 | -2 | 3)' + assert str(inside) == '(-1 2 -3)' # evaluate method assert (0, 0, 0) in inside From b4a796e9b42022501798d297d5f4039a1a5dc246 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 10 Oct 2024 13:39:16 -0500 Subject: [PATCH 077/184] Avoid writing subnormal nuclide densities to XML (#3144) --- openmc/material.py | 14 ++++++++++++-- tests/unit_tests/test_material.py | 14 ++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/openmc/material.py b/openmc/material.py index 74a403c1535..63994b3055f 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -5,6 +5,7 @@ from numbers import Real from pathlib import Path import re +import sys import warnings import lxml.etree as ET @@ -26,6 +27,9 @@ DENSITY_UNITS = ('g/cm3', 'g/cc', 'kg/m3', 'atom/b-cm', 'atom/cm3', 'sum', 'macro') +# Smallest normalized floating point number +_SMALLEST_NORMAL = sys.float_info.min + NuclideTuple = namedtuple('NuclideTuple', ['name', 'percent', 'percent_type']) @@ -1339,10 +1343,16 @@ def _get_nuclide_xml(self, nuclide: NuclideTuple) -> ET.Element: xml_element = ET.Element("nuclide") xml_element.set("name", nuclide.name) + # Prevent subnormal numbers from being written to XML, which causes an + # exception on the C++ side when calling std::stod + val = nuclide.percent + if abs(val) < _SMALLEST_NORMAL: + val = 0.0 + if nuclide.percent_type == 'ao': - xml_element.set("ao", str(nuclide.percent)) + xml_element.set("ao", str(val)) else: - xml_element.set("wo", str(nuclide.percent)) + xml_element.set("wo", str(val)) return xml_element diff --git a/tests/unit_tests/test_material.py b/tests/unit_tests/test_material.py index 94ba82571be..c6a07cff977 100644 --- a/tests/unit_tests/test_material.py +++ b/tests/unit_tests/test_material.py @@ -683,3 +683,17 @@ def intensity(src): stable.add_nuclide('Gd156', 1.0) stable.volume = 1.0 assert stable.get_decay_photon_energy() is None + + +def test_avoid_subnormal(run_in_tmpdir): + # Write a materials.xml with a material that has a nuclide density that is + # represented as a subnormal floating point value + mat = openmc.Material() + mat.add_nuclide('H1', 1.0) + mat.add_nuclide('H2', 1.0e-315) + mats = openmc.Materials([mat]) + mats.export_to_xml() + + # When read back in, the density should be zero + mats = openmc.Materials.from_xml() + assert mats[0].get_nuclide_atom_densities()['H2'] == 0.0 From 04ecf5490796ce023192076e35e8ffe815da8f62 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 11 Oct 2024 06:13:10 -0500 Subject: [PATCH 078/184] Improve clipping of Mixture distributions (#3154) Co-authored-by: Ethan Peterson --- openmc/material.py | 7 +- openmc/stats/univariate.py | 126 ++++++++++++------ src/distribution.cpp | 3 + tests/regression_tests/source/inputs_true.dat | 18 +-- tests/unit_tests/test_stats.py | 18 ++- 5 files changed, 122 insertions(+), 50 deletions(-) diff --git a/openmc/material.py b/openmc/material.py index 63994b3055f..1213ea669d4 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -326,7 +326,7 @@ def get_decay_photon_energy( probs = [] for nuc, atoms_per_bcm in self.get_nuclide_atom_densities().items(): source_per_atom = openmc.data.decay_photon_energy(nuc) - if source_per_atom is not None: + if source_per_atom is not None and atoms_per_bcm > 0.0: dists.append(source_per_atom) probs.append(1e24 * atoms_per_bcm * multiplier) @@ -339,6 +339,11 @@ def get_decay_photon_energy( if isinstance(combined, (Discrete, Mixture)): combined.clip(clip_tolerance, inplace=True) + # If clipping resulted in a single distribution within a mixture, pick + # out that single distribution + if isinstance(combined, Mixture) and len(combined.distribution) == 1: + combined = combined.distribution[0] + return combined @classmethod diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index 10822c06fa7..0dc6f385685 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -97,6 +97,45 @@ def integral(self): return 1.0 +def _intensity_clip(intensity: Sequence[float], tolerance: float = 1e-6) -> np.ndarray: + """Clip low-importance points from an array of intensities. + + Given an array of intensities, this function returns an array of indices for + points that contribute non-negligibly to the total sum of intensities. + + Parameters + ---------- + intensity : sequence of float + Intensities in arbitrary units. + tolerance : float + Maximum fraction of intensities that will be discarded. + + Returns + ------- + Array of indices + + """ + # Get indices of intensities from largest to smallest + index_sort = np.argsort(intensity)[::-1] + + # Get intensities from largest to smallest + sorted_intensity = np.asarray(intensity)[index_sort] + + # Determine cumulative sum of probabilities + cumsum = np.cumsum(sorted_intensity) + cumsum /= cumsum[-1] + + # Find index that satisfies cutoff + index_cutoff = np.searchsorted(cumsum, 1.0 - tolerance) + + # Now get indices up to cutoff + new_indices = index_sort[:index_cutoff + 1] + + # Put back in the order of the original array and return + new_indices.sort() + return new_indices + + class Discrete(Univariate): """Distribution characterized by a probability mass function. @@ -283,32 +322,20 @@ def clip(self, tolerance: float = 1e-6, inplace: bool = False) -> Discrete: cv.check_less_than("tolerance", tolerance, 1.0, equality=True) cv.check_greater_than("tolerance", tolerance, 0.0, equality=True) - # Determine (reversed) sorted order of probabilities + # Compute intensities intensity = self.p * self.x - index_sort = np.argsort(intensity)[::-1] - # Get probabilities in above order - sorted_intensity = intensity[index_sort] - - # Determine cumulative sum of probabilities - cumsum = np.cumsum(sorted_intensity) - cumsum /= cumsum[-1] - - # Find index which satisfies cutoff - index_cutoff = np.searchsorted(cumsum, 1.0 - tolerance) - - # Now get indices up to cutoff - new_indices = index_sort[:index_cutoff + 1] - new_indices.sort() + # Get indices for intensities above threshold + indices = _intensity_clip(intensity, tolerance=tolerance) # Create new discrete distribution if inplace: - self.x = self.x[new_indices] - self.p = self.p[new_indices] + self.x = self.x[indices] + self.p = self.p[indices] return self else: - new_x = self.x[new_indices] - new_p = self.p[new_indices] + new_x = self.x[indices] + new_p = self.p[indices] return type(self)(new_x, new_p) @@ -1206,7 +1233,7 @@ def probability(self, probability): for p in probability: cv.check_greater_than('mixture distribution probabilities', p, 0.0, True) - self._probability = probability + self._probability = np.array(probability, dtype=float) @property def distribution(self): @@ -1312,40 +1339,63 @@ def integral(self): ]) def clip(self, tolerance: float = 1e-6, inplace: bool = False) -> Mixture: - r"""Remove low-importance points from contained discrete distributions. + r"""Remove low-importance points / distributions - Given a probability mass function :math:`p(x)` with :math:`\{x_1, x_2, - x_3, \dots\}` the possible values of the random variable with - corresponding probabilities :math:`\{p_1, p_2, p_3, \dots\}`, this - function will remove any low-importance points such that :math:`\sum_i - x_i p_i` is preserved to within some threshold. + Like :meth:`Discrete.clip`, this method will remove low-importance + points from discrete distributions contained within the mixture but it + will also clip any distributions that have negligible contributions to + the overall intensity. .. versionadded:: 0.14.0 Parameters ---------- tolerance : float - Maximum fraction of :math:`\sum_i x_i p_i` that will be discarded - for any discrete distributions within the mixture distribution. + Maximum fraction of intensities that will be discarded. inplace : bool Whether to modify the current object in-place or return a new one. Returns ------- - Discrete distribution with low-importance points removed + Distribution with low-importance points / distributions removed """ + # Determine integral of original distribution to compare later + original_integral = self.integral() + + # Determine indices for any distributions that contribute non-negligibly + # to overall intensity + intensities = [prob*dist.integral() for prob, dist in + zip(self.probability, self.distribution)] + indices = _intensity_clip(intensities, tolerance=tolerance) + + # Clip mixture of distributions + probability = self.probability[indices] + distribution = [self.distribution[i] for i in indices] + + # Clip points from Discrete distributions + distribution = [ + dist.clip(tolerance, inplace) if isinstance(dist, Discrete) else dist + for dist in distribution + ] + if inplace: - for dist in self.distribution: - if isinstance(dist, Discrete): - dist.clip(tolerance, inplace=True) - return self + # Set attributes of current object and return + self.probability = probability + self.distribution = distribution + new_dist = self else: - distribution = [ - dist.clip(tolerance) if isinstance(dist, Discrete) else dist - for dist in self.distribution - ] - return type(self)(self.probability, distribution) + # Create new distribution + new_dist = type(self)(probability, distribution) + + # Show warning if integral of new distribution is not within + # tolerance of original + diff = (original_integral - new_dist.integral())/original_integral + if diff > tolerance: + warn("Clipping mixture distribution resulted in an integral that is " + f"lower by a fraction of {diff} when tolerance={tolerance}.") + + return new_dist def combine_distributions( diff --git a/src/distribution.cpp b/src/distribution.cpp index 3026630b335..a6b4acd58b1 100644 --- a/src/distribution.cpp +++ b/src/distribution.cpp @@ -394,6 +394,9 @@ Mixture::Mixture(pugi::xml_node node) distribution_.push_back(std::make_pair(cumsum, std::move(dist))); } + // Save integral of distribution + integral_ = cumsum; + // Normalize cummulative probabilities to 1 for (auto& pair : distribution_) { pair.first /= cumsum; diff --git a/tests/regression_tests/source/inputs_true.dat b/tests/regression_tests/source/inputs_true.dat index c1a616dd109..e787f24d413 100644 --- a/tests/regression_tests/source/inputs_true.dat +++ b/tests/regression_tests/source/inputs_true.dat @@ -85,13 +85,13 @@ - + - + - + 1.0 1.3894954943731377 1.93069772888325 2.6826957952797255 3.72759372031494 5.17947467923121 7.196856730011519 10.0 13.894954943731374 19.306977288832496 26.826957952797247 37.2759372031494 51.7947467923121 71.96856730011518 100.0 138.94954943731375 193.06977288832496 268.26957952797244 372.7593720314938 517.9474679231207 719.6856730011514 1000.0 1389.4954943731375 1930.6977288832495 2682.6957952797247 3727.593720314938 5179.474679231207 7196.856730011514 10000.0 13894.95494373136 19306.977288832495 26826.95795279722 37275.93720314938 51794.74679231213 71968.56730011514 100000.0 138949.5494373136 193069.77288832495 268269.5795279722 372759.3720314938 517947.4679231202 719685.6730011514 1000000.0 1389495.494373136 1930697.7288832497 2682695.7952797217 3727593.720314938 5179474.679231202 7196856.730011513 10000000.0 0.0 2.9086439299358713e-08 5.80533561806147e-08 8.67817193689187e-08 1.1515347785771536e-07 1.4305204600565115e-07 1.7036278261198208e-07 1.9697346200185813e-07 2.227747351856934e-07 2.4766057919761985e-07 2.715287327665956e-07 2.9428111652990295e-07 3.1582423606228735e-07 3.360695660646056e-07 3.549339141332686e-07 3.723397626156721e-07 3.882155871468592e-07 4.024961505584776e-07 4.151227709522976e-07 4.260435628367196e-07 4.3521365033538783e-07 4.4259535159179273e-07 4.4815833361210174e-07 4.5187973690993757e-07 4.5374426944091084e-07 4.5374426944091084e-07 4.5187973690993757e-07 4.4815833361210174e-07 4.4259535159179273e-07 4.352136503353879e-07 4.2604356283671966e-07 4.1512277095229767e-07 4.0249615055847764e-07 3.8821558714685926e-07 3.723397626156722e-07 3.5493391413326864e-07 3.360695660646057e-07 3.158242360622874e-07 2.942811165299031e-07 2.715287327665957e-07 2.4766057919762e-07 2.2277473518569352e-07 1.9697346200185819e-07 1.7036278261198226e-07 1.4305204600565126e-07 1.1515347785771556e-07 8.678171936891881e-08 5.805335618061493e-08 2.9086439299358858e-08 5.559621115282002e-23 @@ -108,13 +108,13 @@ - + - + - + 1.0 1.3894954943731377 1.93069772888325 2.6826957952797255 3.72759372031494 5.17947467923121 7.196856730011519 10.0 13.894954943731374 19.306977288832496 26.826957952797247 37.2759372031494 51.7947467923121 71.96856730011518 100.0 138.94954943731375 193.06977288832496 268.26957952797244 372.7593720314938 517.9474679231207 719.6856730011514 1000.0 1389.4954943731375 1930.6977288832495 2682.6957952797247 3727.593720314938 5179.474679231207 7196.856730011514 10000.0 13894.95494373136 19306.977288832495 26826.95795279722 37275.93720314938 51794.74679231213 71968.56730011514 100000.0 138949.5494373136 193069.77288832495 268269.5795279722 372759.3720314938 517947.4679231202 719685.6730011514 1000000.0 1389495.494373136 1930697.7288832497 2682695.7952797217 3727593.720314938 5179474.679231202 7196856.730011513 10000000.0 0.0 2.9086439299358713e-08 5.80533561806147e-08 8.67817193689187e-08 1.1515347785771536e-07 1.4305204600565115e-07 1.7036278261198208e-07 1.9697346200185813e-07 2.227747351856934e-07 2.4766057919761985e-07 2.715287327665956e-07 2.9428111652990295e-07 3.1582423606228735e-07 3.360695660646056e-07 3.549339141332686e-07 3.723397626156721e-07 3.882155871468592e-07 4.024961505584776e-07 4.151227709522976e-07 4.260435628367196e-07 4.3521365033538783e-07 4.4259535159179273e-07 4.4815833361210174e-07 4.5187973690993757e-07 4.5374426944091084e-07 4.5374426944091084e-07 4.5187973690993757e-07 4.4815833361210174e-07 4.4259535159179273e-07 4.352136503353879e-07 4.2604356283671966e-07 4.1512277095229767e-07 4.0249615055847764e-07 3.8821558714685926e-07 3.723397626156722e-07 3.5493391413326864e-07 3.360695660646057e-07 3.158242360622874e-07 2.942811165299031e-07 2.715287327665957e-07 2.4766057919762e-07 2.2277473518569352e-07 1.9697346200185819e-07 1.7036278261198226e-07 1.4305204600565126e-07 1.1515347785771556e-07 8.678171936891881e-08 5.805335618061493e-08 2.9086439299358858e-08 5.559621115282002e-23 @@ -132,13 +132,13 @@ - + - + - + 1.0 1.3894954943731377 1.93069772888325 2.6826957952797255 3.72759372031494 5.17947467923121 7.196856730011519 10.0 13.894954943731374 19.306977288832496 26.826957952797247 37.2759372031494 51.7947467923121 71.96856730011518 100.0 138.94954943731375 193.06977288832496 268.26957952797244 372.7593720314938 517.9474679231207 719.6856730011514 1000.0 1389.4954943731375 1930.6977288832495 2682.6957952797247 3727.593720314938 5179.474679231207 7196.856730011514 10000.0 13894.95494373136 19306.977288832495 26826.95795279722 37275.93720314938 51794.74679231213 71968.56730011514 100000.0 138949.5494373136 193069.77288832495 268269.5795279722 372759.3720314938 517947.4679231202 719685.6730011514 1000000.0 1389495.494373136 1930697.7288832497 2682695.7952797217 3727593.720314938 5179474.679231202 7196856.730011513 10000000.0 0.0 2.9086439299358713e-08 5.80533561806147e-08 8.67817193689187e-08 1.1515347785771536e-07 1.4305204600565115e-07 1.7036278261198208e-07 1.9697346200185813e-07 2.227747351856934e-07 2.4766057919761985e-07 2.715287327665956e-07 2.9428111652990295e-07 3.1582423606228735e-07 3.360695660646056e-07 3.549339141332686e-07 3.723397626156721e-07 3.882155871468592e-07 4.024961505584776e-07 4.151227709522976e-07 4.260435628367196e-07 4.3521365033538783e-07 4.4259535159179273e-07 4.4815833361210174e-07 4.5187973690993757e-07 4.5374426944091084e-07 4.5374426944091084e-07 4.5187973690993757e-07 4.4815833361210174e-07 4.4259535159179273e-07 4.352136503353879e-07 4.2604356283671966e-07 4.1512277095229767e-07 4.0249615055847764e-07 3.8821558714685926e-07 3.723397626156722e-07 3.5493391413326864e-07 3.360695660646057e-07 3.158242360622874e-07 2.942811165299031e-07 2.715287327665957e-07 2.4766057919762e-07 2.2277473518569352e-07 1.9697346200185819e-07 1.7036278261198226e-07 1.4305204600565126e-07 1.1515347785771556e-07 8.678171936891881e-08 5.805335618061493e-08 2.9086439299358858e-08 5.559621115282002e-23 diff --git a/tests/unit_tests/test_stats.py b/tests/unit_tests/test_stats.py index 0414fc22559..643e115564b 100644 --- a/tests/unit_tests/test_stats.py +++ b/tests/unit_tests/test_stats.py @@ -262,7 +262,7 @@ def test_mixture(): d2 = openmc.stats.Uniform(3, 7) p = [0.5, 0.5] mix = openmc.stats.Mixture(p, [d1, d2]) - assert mix.probability == p + np.testing.assert_allclose(mix.probability, p) assert mix.distribution == [d1, d2] assert len(mix) == 4 @@ -274,7 +274,7 @@ def test_mixture(): elem = mix.to_xml_element('distribution') d = openmc.stats.Mixture.from_xml_element(elem) - assert d.probability == p + np.testing.assert_allclose(d.probability, p) assert d.distribution == [d1, d2] assert len(d) == 4 @@ -296,6 +296,20 @@ def test_mixture_clip(): mix_same = mix.clip(1e-6, inplace=True) assert mix_same is mix + # Make sure clip removes low probability distributions + d_small = openmc.stats.Uniform(0., 1.) + d_large = openmc.stats.Uniform(2., 5.) + mix = openmc.stats.Mixture([1e-10, 1.0], [d_small, d_large]) + mix_clip = mix.clip(1e-3) + assert mix_clip.distribution == [d_large] + + # Make sure warning is raised if tolerance is exceeded + d1 = openmc.stats.Discrete([1.0, 1.001], [1.0, 0.7e-6]) + d2 = openmc.stats.Tabular([0.0, 1.0], [0.7e-6], interpolation='histogram') + mix = openmc.stats.Mixture([1.0, 1.0], [d1, d2]) + with pytest.warns(UserWarning): + mix_clip = mix.clip(1e-6) + def test_polar_azimuthal(): # default polar-azimuthal should be uniform in mu and phi From fc3de1cbef87780fbdd5d591d80fa54484a1882a Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 11 Oct 2024 06:15:15 -0500 Subject: [PATCH 079/184] Add ConicalFrustum composite surface (#3151) Co-authored-by: Ethan Peterson --- docs/source/pythonapi/model.rst | 1 + openmc/model/surface_composite.py | 120 +++++++++++++++++++++ tests/unit_tests/test_surface_composite.py | 44 ++++++++ 3 files changed, 165 insertions(+) diff --git a/docs/source/pythonapi/model.rst b/docs/source/pythonapi/model.rst index 21944018e7d..e7d6d320f1e 100644 --- a/docs/source/pythonapi/model.rst +++ b/docs/source/pythonapi/model.rst @@ -22,6 +22,7 @@ Composite Surfaces :nosignatures: :template: myclass.rst + openmc.model.ConicalFrustum openmc.model.CruciformPrism openmc.model.CylinderSector openmc.model.HexagonalPrism diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index e2a5f49c4d0..df290329647 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -1717,3 +1717,123 @@ def __neg__(self) -> openmc.Region: prism &= ~corners return prism + + +def _rotation_matrix(v1, v2): + """Compute rotation matrix that would rotate v1 into v2. + + Parameters + ---------- + v1 : numpy.ndarray + Unrotated vector + v2 : numpy.ndarray + Rotated vector + + Returns + ------- + 3x3 rotation matrix + + """ + # Normalize vectors and compute cosine + u1 = v1 / np.linalg.norm(v1) + u2 = v2 / np.linalg.norm(v2) + cos_angle = np.dot(u1, u2) + + I = np.identity(3) + + # Handle special case where vectors are parallel or anti-parallel + if isclose(abs(cos_angle), 1.0, rel_tol=1e-8): + return np.sign(cos_angle)*I + else: + # Calculate rotation angle + sin_angle = np.sqrt(1 - cos_angle*cos_angle) + + # Calculate axis of rotation + axis = np.cross(u1, u2) + axis /= np.linalg.norm(axis) + + # Create cross-product matrix K + kx, ky, kz = axis + K = np.array([ + [0.0, -kz, ky], + [kz, 0.0, -kx], + [-ky, kx, 0.0] + ]) + + # Create rotation matrix using Rodrigues' rotation formula + return I + K * sin_angle + (K @ K) * (1 - cos_angle) + + +class ConicalFrustum(CompositeSurface): + """Conical frustum. + + A conical frustum, also known as a right truncated cone, is a cone that is + truncated by two parallel planes that are perpendicular to the axis of the + cone. The lower and upper base of the conical frustum are circular faces. + This surface is equivalent to the TRC macrobody in MCNP. + + .. versionadded:: 0.15.1 + + Parameters + ---------- + center_base : iterable of float + Cartesian coordinates of the center of the bottom planar face. + axis : iterable of float + Vector from the center of the bottom planar face to the center of the + top planar face that defines the axis of the cone. The length of this + vector is the height of the conical frustum. + r1 : float + Radius of the lower cone base + r2 : float + Radius of the upper cone base + **kwargs + Keyword arguments passed to underlying plane classes + + Attributes + ---------- + cone : openmc.Cone + Cone surface + plane_bottom : openmc.Plane + Plane surface defining the bottom of the frustum + plane_top : openmc.Plane + Plane surface defining the top of the frustum + + """ + _surface_names = ('cone', 'plane_bottom', 'plane_top') + + def __init__(self, center_base: Sequence[float], axis: Sequence[float], + r1: float, r2: float, **kwargs): + center_base = np.array(center_base) + axis = np.array(axis) + + # Determine length of axis height vector + h = np.linalg.norm(axis) + + # To create the frustum oriented with the correct axis, first we will + # create a cone along the z axis and then rotate it according to the + # given axis. Thus, we first need to determine the apex using the z axis + # as a reference. + x0, y0, z0 = center_base + if r1 != r2: + apex = z0 + r1*h/(r1 - r2) + r_sq = ((r1 - r2)/h)**2 + cone = openmc.ZCone(x0, y0, apex, r2=r_sq, **kwargs) + else: + # In the degenerate case r1 == r2, the cone becomes a cylinder + cone = openmc.ZCylinder(x0, y0, r1, **kwargs) + + # Create the parallel planes + plane_bottom = openmc.ZPlane(z0, **kwargs) + plane_top = openmc.ZPlane(z0 + h, **kwargs) + + # Determine rotation matrix corresponding to specified axis + u = np.array([0., 0., 1.]) + rotation = _rotation_matrix(u, axis) + + # Rotate the surfaces + self.cone = cone.rotate(rotation, pivot=center_base) + self.plane_bottom = plane_bottom.rotate(rotation, pivot=center_base) + self.plane_top = plane_top.rotate(rotation, pivot=center_base) + + def __neg__(self) -> openmc.Region: + return +self.plane_bottom & -self.plane_top & -self.cone diff --git a/tests/unit_tests/test_surface_composite.py b/tests/unit_tests/test_surface_composite.py index d862ae6b0de..963bbe00d19 100644 --- a/tests/unit_tests/test_surface_composite.py +++ b/tests/unit_tests/test_surface_composite.py @@ -552,3 +552,47 @@ def test_box(): assert (0., 0.9, 0.) in -s assert (0., 0., -3.) not in +s assert (0., 0., 3.) not in +s + + +def test_conical_frustum(): + center_base = (0.0, 0.0, -3) + axis = (0., 0., 3.) + r1 = 2.0 + r2 = 0.5 + s = openmc.model.ConicalFrustum(center_base, axis, r1, r2) + assert isinstance(s.cone, openmc.Cone) + assert isinstance(s.plane_bottom, openmc.Plane) + assert isinstance(s.plane_top, openmc.Plane) + + # Make sure boundary condition propagates + s.boundary_type = 'reflective' + assert s.boundary_type == 'reflective' + assert s.cone.boundary_type == 'reflective' + assert s.plane_bottom.boundary_type == 'reflective' + assert s.plane_top.boundary_type == 'reflective' + + # Check bounding box + ll, ur = (+s).bounding_box + assert np.all(np.isinf(ll)) + assert np.all(np.isinf(ur)) + ll, ur = (-s).bounding_box + assert ll[2] == pytest.approx(-3.0) + assert ur[2] == pytest.approx(0.0) + + # __contains__ on associated half-spaces + assert (0., 0., -1.) in -s + assert (0., 0., -4.) not in -s + assert (0., 0., 1.) not in -s + assert (1., 1., -2.99) in -s + assert (1., 1., -0.01) in +s + + # translate method + s_t = s.translate((1., 1., 0.)) + assert (1., 1., -0.01) in -s_t + + # Make sure repr works + repr(s) + + # Denegenerate case with r1 = r2 + s = openmc.model.ConicalFrustum(center_base, axis, r1, r1) + assert (1., 1., -0.01) in -s From 8263f05e7ed62290a1a3891ea9ff81f05b13f955 Mon Sep 17 00:00:00 2001 From: Soha <120593053+sohhae@users.noreply.github.com> Date: Fri, 11 Oct 2024 23:11:27 -0500 Subject: [PATCH 080/184] Update quickinstall instructions for macOS (#3130) Co-authored-by: Paul Romano --- docs/source/quickinstall.rst | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/source/quickinstall.rst b/docs/source/quickinstall.rst index 7f222f77cb8..323cd7fd48d 100644 --- a/docs/source/quickinstall.rst +++ b/docs/source/quickinstall.rst @@ -137,12 +137,11 @@ packages should be installed, for example in Homebrew via: The compiler provided by the above LLVM package should be used in place of the one provisioned by XCode, which does not support the multithreading library used -by OpenMC. Consequently, the C++ compiler should explicitly be set before -proceeding: - -.. code-block:: sh - - export CXX=/opt/homebrew/opt/llvm/bin/clang++ +by OpenMC. To ensure CMake picks up the correct compiler, make sure that either +the :envvar:`CXX` environment variable is set to the brew-installed ``clang++`` +or that the directory containing it is on your :envvar:`PATH` environment +variable. Common locations for the brew-installed compiler are +``/opt/homebrew/opt/llvm/bin`` and ``/usr/local/opt/llvm/bin``. After the packages have been installed, follow the instructions to build from source below. From dcb25575ca0691ddde36af5141376c29aab884bc Mon Sep 17 00:00:00 2001 From: Lorenzo Chierici Date: Mon, 14 Oct 2024 21:47:22 +0200 Subject: [PATCH 081/184] avoid zero division if source rate of previous result is zero (#3169) --- openmc/deplete/abc.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 7bf7f1086a7..784023f26ff 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -754,8 +754,9 @@ def _get_bos_data_from_restart(self, source_rate, bos_conc): rates = res.rates[0] k = ufloat(res.k[0, 0], res.k[0, 1]) - # Scale reaction rates by ratio of source rates - rates *= source_rate / res.source_rate + if res.source_rate != 0.0: + # Scale reaction rates by ratio of source rates + rates *= source_rate / res.source_rate return bos_conc, OperatorResult(k, rates) def _get_start_data(self): From c19b9b1beb41ba807e4b79a413b15209ef5127b6 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Sat, 19 Oct 2024 00:12:18 +0100 Subject: [PATCH 082/184] added subfolders to txt search command in pyproject (#3174) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 39aa261c1b4..d0419d550f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ include = ['openmc*', 'scripts*'] exclude = ['tests*'] [tool.setuptools.package-data] -"openmc.data.effective_dose" = ["*.txt"] +"openmc.data.effective_dose" = ["**/*.txt"] "openmc.data" = ["*.txt", "*.DAT", "*.json", "*.h5"] "openmc.lib" = ["libopenmc.dylib", "libopenmc.so"] From 82a6f9e40b66dccca225a97202f4e8fce1b193cd Mon Sep 17 00:00:00 2001 From: Ahnaf Tahmid Chowdhury Date: Sat, 19 Oct 2024 05:18:16 +0600 Subject: [PATCH 083/184] Update `fmt` Formatters for Compatibility with Versions below 11 (#3172) --- include/openmc/output.h | 10 +++++++--- include/openmc/position.h | 10 +++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/include/openmc/output.h b/include/openmc/output.h index 549d2ae32ba..940ea78ceb9 100644 --- a/include/openmc/output.h +++ b/include/openmc/output.h @@ -76,10 +76,14 @@ struct formatter> { } template - auto format(const std::array& arr, FormatContext& ctx) const +#if FMT_VERSION >= 110000 // Version 11.0.0 and above + auto format(const std::array& arr, FormatContext& ctx) const { +#else // For versions below 11.0.0 + auto format(const std::array& arr, FormatContext& ctx) { +#endif return format_to(ctx.out(), "({}, {})", arr[0], arr[1]); - } -}; +} +}; // namespace fmt } // namespace fmt diff --git a/include/openmc/position.h b/include/openmc/position.h index e6939dc0e46..dc60ab35a04 100644 --- a/include/openmc/position.h +++ b/include/openmc/position.h @@ -221,12 +221,16 @@ namespace fmt { template<> struct formatter : formatter { template - auto format(const openmc::Position& pos, FormatContext& ctx) const +#if FMT_VERSION >= 110000 // Version 11.0.0 and above + auto format(const openmc::Position& pos, FormatContext& ctx) const { +#else // For versions below 11.0.0 + auto format(const openmc::Position& pos, FormatContext& ctx) { +#endif return formatter::format( fmt::format("({}, {}, {})", pos.x, pos.y, pos.z), ctx); - } -}; +} +}; // namespace fmt } // namespace fmt From 9c9a13c48ea7dbb2984467b287314d849a1dca18 Mon Sep 17 00:00:00 2001 From: Rayan HADDAD <103775910+rayanhaddad169@users.noreply.github.com> Date: Mon, 28 Oct 2024 17:28:59 +0100 Subject: [PATCH 084/184] enable polymorphisme for mix_materials (#3180) Co-authored-by: r.haddad --- openmc/material.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmc/material.py b/openmc/material.py index 1213ea669d4..a6401216adb 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -1536,7 +1536,7 @@ def mix_materials(cls, materials, fracs: Iterable[float], if name is None: name = '-'.join([f'{m.name}({f})' for m, f in zip(materials, fracs)]) - new_mat = openmc.Material(name=name) + new_mat = cls(name=name) # Compute atom fractions of nuclides and add them to the new material tot_nuclides_per_cc = np.sum([dens for dens in nuclides_per_cc.values()]) From 7552123c762a13be49269bf3c5bf393635109766 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Tue, 29 Oct 2024 18:05:40 +0100 Subject: [PATCH 085/184] added list to doc string arg for plot_xs (#3178) --- openmc/plotter.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/openmc/plotter.py b/openmc/plotter.py index 8b205b79d8b..c3b951ad893 100644 --- a/openmc/plotter.py +++ b/openmc/plotter.py @@ -1,5 +1,10 @@ +# annotations package is required to enable postponed evaluation of type +# hints in conjugation with the | operator. This avoids TypeError: +# unsupported operand type(s) for |: 'str' and 'NoneType' error +from __future__ import annotations from itertools import chain from numbers import Integral, Real +from typing import Dict, Iterable, List import numpy as np @@ -120,18 +125,29 @@ def _get_title(reactions): return 'Cross Section Plot' -def plot_xs(reactions, divisor_types=None, temperature=294., axis=None, - sab_name=None, ce_cross_sections=None, mg_cross_sections=None, - enrichment=None, plot_CE=True, orders=None, divisor_orders=None, - energy_axis_units="eV", **kwargs): +def plot_xs( + reactions: Dict[str, openmc.Material | List[str]], + divisor_types: Iterable[str] | None = None, + temperature: float = 294.0, + axis: "plt.Axes" | None = None, + sab_name: str | None = None, + ce_cross_sections: str | None = None, + mg_cross_sections: str | None = None, + enrichment: float | None = None, + plot_CE: bool = True, + orders: Iterable[int] | None = None, + divisor_orders: Iterable[int] | None = None, + energy_axis_units: str = "eV", + **kwargs, +) -> "plt.Figure": """Creates a figure of continuous-energy cross sections for this item. Parameters ---------- reactions : dict keys can be either a nuclide or element in string form or an - openmc.Material object. Values are the type of cross sections to - include in the plot. + openmc.Material object. Values are a list of the types of + cross sections to include in the plot. divisor_types : Iterable of values of PLOT_TYPES, optional Cross section types which will divide those produced by types before plotting. A type of 'unity' can be used to effectively not From 339d78c5fae8ba3a815ae10097238d0099a142c4 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 6 Nov 2024 05:59:25 -0600 Subject: [PATCH 086/184] Fix plot_xs type hint (#3184) --- openmc/plotter.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/openmc/plotter.py b/openmc/plotter.py index c3b951ad893..85c4963a76f 100644 --- a/openmc/plotter.py +++ b/openmc/plotter.py @@ -1,6 +1,3 @@ -# annotations package is required to enable postponed evaluation of type -# hints in conjugation with the | operator. This avoids TypeError: -# unsupported operand type(s) for |: 'str' and 'NoneType' error from __future__ import annotations from itertools import chain from numbers import Integral, Real @@ -126,7 +123,7 @@ def _get_title(reactions): def plot_xs( - reactions: Dict[str, openmc.Material | List[str]], + reactions: Dict[str | openmc.Material, List[str]], divisor_types: Iterable[str] | None = None, temperature: float = 294.0, axis: "plt.Axes" | None = None, @@ -139,7 +136,7 @@ def plot_xs( divisor_orders: Iterable[int] | None = None, energy_axis_units: str = "eV", **kwargs, -) -> "plt.Figure": +) -> "plt.Figure" | None: """Creates a figure of continuous-energy cross sections for this item. Parameters @@ -291,7 +288,6 @@ def plot_xs( return fig - def calculate_cexs(this, types, temperature=294., sab_name=None, cross_sections=None, enrichment=None, ncrystal_cfg=None): """Calculates continuous-energy cross sections of a requested type. From 754f6fa44ab7c1bbb55cdaacc54aff642f1446f5 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Fri, 8 Nov 2024 15:18:15 -0600 Subject: [PATCH 087/184] Reset values of lattice offset tables when allocated (#3188) --- include/openmc/lattice.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/openmc/lattice.h b/include/openmc/lattice.h index 429d81ddda8..fb374ab75c3 100644 --- a/include/openmc/lattice.h +++ b/include/openmc/lattice.h @@ -71,7 +71,8 @@ class Lattice { //! Allocate offset table for distribcell. void allocate_offset_table(int n_maps) { - offsets_.resize(n_maps * universes_.size(), C_NONE); + offsets_.resize(n_maps * universes_.size()); + std::fill(offsets_.begin(), offsets_.end(), C_NONE); } //! Populate the distribcell offset tables. From 9983ee1a7eb9b95bd45123a9d44a021f789f0410 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Sat, 9 Nov 2024 00:52:40 +0100 Subject: [PATCH 088/184] allowing varible offsets for polygon.offset (#3120) Co-authored-by: Paul Romano --- openmc/model/surface_composite.py | 27 ++++++++++++++++++---- tests/unit_tests/test_surface_composite.py | 11 +++++---- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index df290329647..7a5ab8f1002 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -1,3 +1,4 @@ +from __future__ import annotations from abc import ABC, abstractmethod from collections.abc import Iterable, Sequence from copy import copy @@ -1316,25 +1317,43 @@ def _decompose_polygon_into_convex_sets(self): surfsets.append(surf_ops) return surfsets - def offset(self, distance): + def offset(self, distance: float | Sequence[float] | np.ndarray) -> Polygon: """Offset this polygon by a set distance Parameters ---------- - distance : float + distance : float or sequence of float or np.ndarray The distance to offset the polygon by. Positive is outward - (expanding) and negative is inward (shrinking). + (expanding) and negative is inward (shrinking). If a float is + provided, the same offset is applied to all vertices. If a list or + tuple is provided, each vertex gets a different offset. If an + iterable or numpy array is provided, each vertex gets a different + offset. Returns ------- offset_polygon : openmc.model.Polygon """ + + if isinstance(distance, float): + distance = np.full(len(self.points), distance) + elif isinstance(distance, Sequence): + distance = np.array(distance) + elif not isinstance(distance, np.ndarray): + raise TypeError("Distance must be a float or sequence of float.") + + if len(distance) != len(self.points): + raise ValueError( + f"Length of distance {len(distance)} array must " + f"match number of polygon points {len(self.points)}" + ) + normals = np.insert(self._normals, 0, self._normals[-1, :], axis=0) cos2theta = np.sum(normals[1:, :]*normals[:-1, :], axis=-1, keepdims=True) costheta = np.cos(np.arccos(cos2theta) / 2) nvec = (normals[1:, :] + normals[:-1, :]) unit_nvec = nvec / np.linalg.norm(nvec, axis=-1, keepdims=True) - disp_vec = distance / costheta * unit_nvec + disp_vec = distance[:, np.newaxis] / costheta * unit_nvec return type(self)(self.points + disp_vec, basis=self.basis) diff --git a/tests/unit_tests/test_surface_composite.py b/tests/unit_tests/test_surface_composite.py index 963bbe00d19..015ac667e45 100644 --- a/tests/unit_tests/test_surface_composite.py +++ b/tests/unit_tests/test_surface_composite.py @@ -347,10 +347,13 @@ def test_polygon(): assert any([points_in[i] in reg for reg in star_poly.regions]) assert points_in[i] not in +star_poly assert (0, 0, 0) not in -star_poly - if basis != 'rz': - offset_star = star_poly.offset(.6) - assert (0, 0, 0) in -offset_star - assert any([(0, 0, 0) in reg for reg in offset_star.regions]) + if basis != "rz": + for offsets in [0.6, np.array([0.6] * 10), [0.6] * 10]: + offset_star = star_poly.offset(offsets) + assert (0, 0, 0) in -offset_star + assert any([(0, 0, 0) in reg for reg in offset_star.regions]) + with pytest.raises(ValueError): + star_poly.offset([0.6, 0.6]) # check invalid Polygon input points # duplicate points not just at start and end From 70807b146fedecd47808a9541a3ab96ca529fd20 Mon Sep 17 00:00:00 2001 From: azimG Date: Sat, 9 Nov 2024 17:56:37 +0100 Subject: [PATCH 089/184] Update surface_composite.py (#3189) --- openmc/model/surface_composite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index 7a5ab8f1002..cc570cc56e5 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -27,7 +27,7 @@ def translate(self, vector, inplace=False): return surf def rotate(self, rotation, pivot=(0., 0., 0.), order='xyz', inplace=False): - surf = copy(self) + surf = self if inplace else copy(self) for name in self._surface_names: s = getattr(surf, name) setattr(surf, name, s.rotate(rotation, pivot, order, inplace)) From c05132cb0de27f9a5db39b7cb5a9882439c823d1 Mon Sep 17 00:00:00 2001 From: Nicolas Linden Date: Sat, 9 Nov 2024 20:33:14 +0100 Subject: [PATCH 090/184] add export_model_xml arguments to Model.plot_geometry and Model.calculate_volumes (#3190) Co-authored-by: Nicolas Linden Co-authored-by: Paul Romano --- openmc/model/model.py | 41 +++++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index 78f743a03ed..0ea6db8db88 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -719,7 +719,8 @@ def run(self, particles=None, threads=None, geometry_debug=False, def calculate_volumes(self, threads=None, output=True, cwd='.', openmc_exec='openmc', mpi_args=None, - apply_volumes=True): + apply_volumes=True, export_model_xml=True, + **export_kwargs): """Runs an OpenMC stochastic volume calculation and, if requested, applies volumes to the model @@ -748,6 +749,13 @@ def calculate_volumes(self, threads=None, output=True, cwd='.', apply_volumes : bool, optional Whether apply the volume calculation results from this calculation to the model. Defaults to applying the volumes. + export_model_xml : bool, optional + Exports a single model.xml file rather than separate files. Defaults + to True. + **export_kwargs + Keyword arguments passed to either :meth:`Model.export_to_model_xml` + or :meth:`Model.export_to_xml`. + """ if len(self.settings.volume_calculations) == 0: @@ -769,10 +777,15 @@ def calculate_volumes(self, threads=None, output=True, cwd='.', openmc.lib.calculate_volumes(output) else: - self.export_to_xml() - openmc.calculate_volumes(threads=threads, output=output, - openmc_exec=openmc_exec, - mpi_args=mpi_args) + if export_model_xml: + self.export_to_model_xml(**export_kwargs) + else: + self.export_to_xml(**export_kwargs) + path_input = export_kwargs.get("path", None) + openmc.calculate_volumes( + threads=threads, output=output, openmc_exec=openmc_exec, + mpi_args=mpi_args, path_input=path_input + ) # Now we apply the volumes if apply_volumes: @@ -909,7 +922,8 @@ def sample_external_source( n_samples=n_samples, prn_seed=prn_seed ) - def plot_geometry(self, output=True, cwd='.', openmc_exec='openmc'): + def plot_geometry(self, output=True, cwd='.', openmc_exec='openmc', + export_model_xml=True, **export_kwargs): """Creates plot images as specified by the Model.plots attribute .. versionadded:: 0.13.0 @@ -924,6 +938,12 @@ def plot_geometry(self, output=True, cwd='.', openmc_exec='openmc'): openmc_exec : str, optional Path to OpenMC executable. Defaults to 'openmc'. This only applies to the case when not using the C API. + export_model_xml : bool, optional + Exports a single model.xml file rather than separate files. Defaults + to True. + **export_kwargs + Keyword arguments passed to either :meth:`Model.export_to_model_xml` + or :meth:`Model.export_to_xml`. """ @@ -937,8 +957,13 @@ def plot_geometry(self, output=True, cwd='.', openmc_exec='openmc'): # Compute the volumes openmc.lib.plot_geometry(output) else: - self.export_to_xml() - openmc.plot_geometry(output=output, openmc_exec=openmc_exec) + if export_model_xml: + self.export_to_model_xml(**export_kwargs) + else: + self.export_to_xml(**export_kwargs) + path_input = export_kwargs.get("path", None) + openmc.plot_geometry(output=output, openmc_exec=openmc_exec, + path_input=path_input) def _change_py_lib_attribs(self, names_or_ids, value, obj_type, attrib_name, density_units='atom/b-cm'): From c9207795d87cf4407c92bf054e1930b8c592be76 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 11 Nov 2024 16:03:31 -0600 Subject: [PATCH 091/184] Fixes in MicroXS.from_multigroup_flux (#3192) --- openmc/deplete/independent_operator.py | 5 +++-- openmc/deplete/microxs.py | 10 ++++++---- tests/unit_tests/test_deplete_decay.py | 4 ++-- tests/unit_tests/test_deplete_independent_operator.py | 6 +++--- tests/unit_tests/test_deplete_microxs.py | 8 +++++--- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/openmc/deplete/independent_operator.py b/openmc/deplete/independent_operator.py index cc1a05bd99f..226682ffa53 100644 --- a/openmc/deplete/independent_operator.py +++ b/openmc/deplete/independent_operator.py @@ -44,7 +44,7 @@ class IndependentOperator(OpenMCOperator): Parameters ---------- - materials : openmc.Materials + materials : iterable of openmc.Material Materials to deplete. fluxes : list of numpy.ndarray Flux in each group in [n-cm/src] for each domain @@ -127,8 +127,9 @@ def __init__(self, reduce_chain_level=None, fission_yield_opts=None): # Validate micro-xs parameters - check_type('materials', materials, openmc.Materials) + check_type('materials', materials, Iterable, openmc.Material) check_type('micros', micros, Iterable, MicroXS) + materials = openmc.Materials(materials) if not (len(fluxes) == len(micros) == len(materials)): msg = (f'The length of fluxes ({len(fluxes)}) should be equal to ' diff --git a/openmc/deplete/microxs.py b/openmc/deplete/microxs.py index 5be9875f304..bbe9b2a4322 100644 --- a/openmc/deplete/microxs.py +++ b/openmc/deplete/microxs.py @@ -198,6 +198,8 @@ class MicroXS: """ def __init__(self, data: np.ndarray, nuclides: list[str], reactions: list[str]): # Validate inputs + if len(data.shape) != 3: + raise ValueError('Data array must be 3D.') if data.shape[:2] != (len(nuclides), len(reactions)): raise ValueError( f'Nuclides list of length {len(nuclides)} and ' @@ -291,11 +293,11 @@ def from_multigroup_flux( mts = [REACTION_MT[name] for name in reactions] # Normalize multigroup flux - multigroup_flux = np.asarray(multigroup_flux) + multigroup_flux = np.array(multigroup_flux) multigroup_flux /= multigroup_flux.sum() - # Create 2D array for microscopic cross sections - microxs_arr = np.zeros((len(nuclides), len(mts))) + # Create 3D array for microscopic cross sections + microxs_arr = np.zeros((len(nuclides), len(mts), 1)) # Create a material with all nuclides mat_all_nucs = openmc.Material() @@ -327,7 +329,7 @@ def from_multigroup_flux( xs = lib_nuc.collapse_rate( mt, temperature, energies, multigroup_flux ) - microxs_arr[nuc_index, mt_index] = xs + microxs_arr[nuc_index, mt_index, 0] = xs return cls(microxs_arr, nuclides, reactions) diff --git a/tests/unit_tests/test_deplete_decay.py b/tests/unit_tests/test_deplete_decay.py index cebbc16fa79..6e7b0b101ec 100644 --- a/tests/unit_tests/test_deplete_decay.py +++ b/tests/unit_tests/test_deplete_decay.py @@ -20,7 +20,7 @@ def test_deplete_decay_products(run_in_tmpdir): """) # Create MicroXS object with no cross sections - micro_xs = openmc.deplete.MicroXS(np.empty((0, 0)), [], []) + micro_xs = openmc.deplete.MicroXS(np.empty((0, 0, 0)), [], []) # Create depletion operator with no reactions op = openmc.deplete.IndependentOperator.from_nuclides( @@ -59,7 +59,7 @@ def test_deplete_decay_step_fissionable(run_in_tmpdir): """ # Set up a pure decay operator - micro_xs = openmc.deplete.MicroXS(np.empty((0, 0)), [], []) + micro_xs = openmc.deplete.MicroXS(np.empty((0, 0, 0)), [], []) mat = openmc.Material() mat.name = 'I do not decay.' mat.add_nuclide('U238', 1.0, 'ao') diff --git a/tests/unit_tests/test_deplete_independent_operator.py b/tests/unit_tests/test_deplete_independent_operator.py index 9129cf0642f..c765d065009 100644 --- a/tests/unit_tests/test_deplete_independent_operator.py +++ b/tests/unit_tests/test_deplete_independent_operator.py @@ -6,7 +6,7 @@ import pytest -from openmc import Material, Materials +from openmc import Material from openmc.deplete import IndependentOperator, MicroXS CHAIN_PATH = Path(__file__).parents[1] / "chain_simple.xml" @@ -34,7 +34,7 @@ def test_operator_init(): fuel.set_density("g/cc", 10.4) fuel.depletable = True fuel.volume = 1 - materials = Materials([fuel]) + materials = [fuel] fluxes = [1.0] micros = [micro_xs] IndependentOperator(materials, fluxes, micros, CHAIN_PATH) @@ -47,7 +47,7 @@ def test_error_handling(): fuel.set_density("g/cc", 1) fuel.depletable = True fuel.volume = 1 - materials = Materials([fuel]) + materials = [fuel] fluxes = [1.0, 2.0] micros = [micro_xs] with pytest.raises(ValueError, match=r"The length of fluxes \(2\)"): diff --git a/tests/unit_tests/test_deplete_microxs.py b/tests/unit_tests/test_deplete_microxs.py index ad54026f014..073b3f162d1 100644 --- a/tests/unit_tests/test_deplete_microxs.py +++ b/tests/unit_tests/test_deplete_microxs.py @@ -46,9 +46,7 @@ def test_from_array(): data.shape = (12, 2, 1) MicroXS(data, nuclides, reactions) - with pytest.raises(ValueError, match=r'Nuclides list of length \d* and ' - r'reactions array of length \d* do not ' - r'match dimensions of data array of shape \(\d*\, \d*\)'): + with pytest.raises(ValueError, match='Data array must be 3D'): MicroXS(data[:, 0], nuclides, reactions) @@ -96,9 +94,13 @@ def test_multigroup_flux_same(): energies = [0., 6.25e-1, 5.53e3, 8.21e5, 2.e7] flux_per_ev = [0.3, 0.3, 1.0, 1.0] flux = flux_per_ev * np.diff(energies) + flux_sum = flux.sum() microxs_4g = MicroXS.from_multigroup_flux( energies=energies, multigroup_flux=flux, chain_file=chain_file) + # from_multigroup_flux should not modify the flux + assert flux.sum() == flux_sum + # Generate micro XS based on 2-group flux, where the boundaries line up with # the 4 group flux and have the same flux per eV across the full energy # range From 3e2a042eb199bc49c0a9e21d06591d26134837cf Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Tue, 12 Nov 2024 14:58:15 -0600 Subject: [PATCH 092/184] Fix documentation typo in `boundary_type` (#3196) --- openmc/surface.py | 66 +++++++++++++++++++++++------------------------ 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/openmc/surface.py b/openmc/surface.py index 537823ba16a..c95025f949b 100644 --- a/openmc/surface.py +++ b/openmc/surface.py @@ -119,7 +119,7 @@ class Surface(IDManagerMixin, ABC): surface_id : int, optional Unique identifier for the surface. If not specified, an identifier will automatically be assigned. - boundary_type : {'transmission, 'vacuum', 'reflective', 'periodic', 'white'}, optional + boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. Note that periodic boundary conditions @@ -135,7 +135,7 @@ class Surface(IDManagerMixin, ABC): Attributes ---------- - boundary_type : {'transmission, 'vacuum', 'reflective', 'periodic', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float @@ -683,7 +683,7 @@ class Plane(PlaneMixin, Surface): The 'C' parameter for the plane. Defaults to 0. d : float, optional The 'D' parameter for the plane. Defaults to 0. - boundary_type : {'transmission, 'vacuum', 'reflective', 'periodic', 'white'}, optional + boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. @@ -707,7 +707,7 @@ class Plane(PlaneMixin, Surface): The 'C' parameter for the plane d : float The 'D' parameter for the plane - boundary_type : {'transmission, 'vacuum', 'reflective', 'periodic', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float @@ -818,7 +818,7 @@ class XPlane(PlaneMixin, Surface): ---------- x0 : float, optional Location of the plane in [cm]. Defaults to 0. - boundary_type : {'transmission, 'vacuum', 'reflective', 'periodic', 'white'}, optional + boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. Only axis-aligned periodicity is @@ -837,7 +837,7 @@ class XPlane(PlaneMixin, Surface): ---------- x0 : float Location of the plane in [cm] - boundary_type : {'transmission, 'vacuum', 'reflective', 'periodic', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float @@ -883,7 +883,7 @@ class YPlane(PlaneMixin, Surface): ---------- y0 : float, optional Location of the plane in [cm] - boundary_type : {'transmission, 'vacuum', 'reflective', 'periodic', 'white'}, optional + boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. Only axis-aligned periodicity is @@ -902,7 +902,7 @@ class YPlane(PlaneMixin, Surface): ---------- y0 : float Location of the plane in [cm] - boundary_type : {'transmission, 'vacuum', 'reflective', 'periodic', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float @@ -948,7 +948,7 @@ class ZPlane(PlaneMixin, Surface): ---------- z0 : float, optional Location of the plane in [cm]. Defaults to 0. - boundary_type : {'transmission, 'vacuum', 'reflective', 'periodic', 'white'}, optional + boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. Only axis-aligned periodicity is @@ -967,7 +967,7 @@ class ZPlane(PlaneMixin, Surface): ---------- z0 : float Location of the plane in [cm] - boundary_type : {'transmission, 'vacuum', 'reflective', 'periodic', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'periodic', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float @@ -1197,7 +1197,7 @@ class Cylinder(QuadricMixin, Surface): dz : float, optional z-component of the vector representing the axis of the cylinder. Defaults to 1. - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'}, optional + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. @@ -1228,7 +1228,7 @@ class Cylinder(QuadricMixin, Surface): y-component of the vector representing the axis of the cylinder dz : float z-component of the vector representing the axis of the cylinder - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float @@ -1372,7 +1372,7 @@ class XCylinder(QuadricMixin, Surface): z-coordinate for the origin of the Cylinder in [cm]. Defaults to 0 r : float, optional Radius of the cylinder in [cm]. Defaults to 1. - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'}, optional + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. @@ -1395,7 +1395,7 @@ class XCylinder(QuadricMixin, Surface): z-coordinate for the origin of the Cylinder in [cm] r : float Radius of the cylinder in [cm] - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float @@ -1470,7 +1470,7 @@ class YCylinder(QuadricMixin, Surface): z-coordinate for the origin of the Cylinder in [cm]. Defaults to 0 r : float, optional Radius of the cylinder in [cm]. Defaults to 1. - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'}, optional + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. @@ -1493,7 +1493,7 @@ class YCylinder(QuadricMixin, Surface): z-coordinate for the origin of the Cylinder in [cm] r : float Radius of the cylinder in [cm] - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float @@ -1568,7 +1568,7 @@ class ZCylinder(QuadricMixin, Surface): y-coordinate for the origin of the Cylinder in [cm]. Defaults to 0 r : float, optional Radius of the cylinder in [cm]. Defaults to 1. - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'}, optional + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. @@ -1591,7 +1591,7 @@ class ZCylinder(QuadricMixin, Surface): y-coordinate for the origin of the Cylinder in [cm] r : float Radius of the cylinder in [cm] - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float @@ -1667,7 +1667,7 @@ class Sphere(QuadricMixin, Surface): z-coordinate of the center of the sphere in [cm]. Defaults to 0. r : float, optional Radius of the sphere in [cm]. Defaults to 1. - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'}, optional + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. @@ -1691,7 +1691,7 @@ class Sphere(QuadricMixin, Surface): z-coordinate of the center of the sphere in [cm] r : float Radius of the sphere in [cm] - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float @@ -1784,7 +1784,7 @@ class Cone(QuadricMixin, Surface): surface_id : int, optional Unique identifier for the surface. If not specified, an identifier will automatically be assigned. - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'}, optional + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. @@ -1812,7 +1812,7 @@ class Cone(QuadricMixin, Surface): y-component of the vector representing the axis of the cone. dz : float z-component of the vector representing the axis of the cone. - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float @@ -1930,7 +1930,7 @@ class XCone(QuadricMixin, Surface): Parameter related to the aperture [:math:`\\rm cm^2`]. It can be interpreted as the increase in the radius squared per cm along the cone's axis of revolution. - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'}, optional + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. @@ -1954,7 +1954,7 @@ class XCone(QuadricMixin, Surface): z-coordinate of the apex in [cm] r2 : float Parameter related to the aperature - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float @@ -2031,7 +2031,7 @@ class YCone(QuadricMixin, Surface): Parameter related to the aperture [:math:`\\rm cm^2`]. It can be interpreted as the increase in the radius squared per cm along the cone's axis of revolution. - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'}, optional + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. @@ -2055,7 +2055,7 @@ class YCone(QuadricMixin, Surface): z-coordinate of the apex in [cm] r2 : float Parameter related to the aperature - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float @@ -2132,7 +2132,7 @@ class ZCone(QuadricMixin, Surface): Parameter related to the aperature [cm^2]. This is the square of the radius of the cone 1 cm from. This can also be treated as the square of the slope of the cone relative to its axis. - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'}, optional + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. @@ -2156,7 +2156,7 @@ class ZCone(QuadricMixin, Surface): z-coordinate of the apex in [cm] r2 : float Parameter related to the aperature - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float @@ -2221,7 +2221,7 @@ class Quadric(QuadricMixin, Surface): ---------- a, b, c, d, e, f, g, h, j, k : float, optional coefficients for the surface. All default to 0. - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'}, optional + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles freely pass through the surface. @@ -2239,7 +2239,7 @@ class Quadric(QuadricMixin, Surface): ---------- a, b, c, d, e, f, g, h, j, k : float coefficients for the surface - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float @@ -2391,7 +2391,7 @@ class XTorus(TorusMixin, Surface): Minor radius of the torus in [cm] (parallel to axis of revolution) c : float Minor radius of the torus in [cm] (perpendicular to axis of revolution) - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float @@ -2466,7 +2466,7 @@ class YTorus(TorusMixin, Surface): Minor radius of the torus in [cm] (parallel to axis of revolution) c : float Minor radius of the torus (perpendicular to axis of revolution) - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float @@ -2541,7 +2541,7 @@ class ZTorus(TorusMixin, Surface): Minor radius of the torus in [cm] (parallel to axis of revolution) c : float Minor radius of the torus in [cm] (perpendicular to axis of revolution) - boundary_type : {'transmission, 'vacuum', 'reflective', 'white'} + boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. albedo : float From 0ecd45c90651698518673368290fc58d90e8a5e8 Mon Sep 17 00:00:00 2001 From: Seda Yilmaz <133794379+nsedayilmaz@users.noreply.github.com> Date: Wed, 13 Nov 2024 02:11:59 +0300 Subject: [PATCH 093/184] Add a vessel composite surface with ellipsoids on top and bottom. (#3168) Co-authored-by: Patrick Shriwise Co-authored-by: Paul Romano --- docs/source/pythonapi/model.rst | 1 + openmc/model/surface_composite.py | 105 ++++++++++++++++++++- tests/unit_tests/test_surface_composite.py | 49 ++++++++++ 3 files changed, 150 insertions(+), 5 deletions(-) diff --git a/docs/source/pythonapi/model.rst b/docs/source/pythonapi/model.rst index e7d6d320f1e..3034826bddd 100644 --- a/docs/source/pythonapi/model.rst +++ b/docs/source/pythonapi/model.rst @@ -32,6 +32,7 @@ Composite Surfaces openmc.model.RectangularParallelepiped openmc.model.RectangularPrism openmc.model.RightCircularCylinder + openmc.model.Vessel openmc.model.XConeOneSided openmc.model.YConeOneSided openmc.model.ZConeOneSided diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index cc570cc56e5..1f25b9ca636 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -41,9 +41,11 @@ def boundary_type(self): def boundary_type(self, boundary_type): # Set boundary type on underlying surfaces, but not for ambiguity plane # on one-sided cones + classes = (XConeOneSided, YConeOneSided, ZConeOneSided, Vessel) for name in self._surface_names: - if name != 'plane': - getattr(self, name).boundary_type = boundary_type + if isinstance(self, classes) and name.startswith('plane'): + continue + getattr(self, name).boundary_type = boundary_type def __repr__(self): return f"<{type(self).__name__} at 0x{id(self):x}>" @@ -90,8 +92,8 @@ class CylinderSector(CompositeSurface): counterclockwise direction with respect to the first basis axis (+y, +z, or +x). Must be greater than :attr:`theta1`. center : iterable of float - Coordinate for central axes of cylinders in the (y, z), (x, z), or (x, y) - basis. Defaults to (0,0). + Coordinate for central axes of cylinders in the (y, z), (x, z), or (x, y) + basis. Defaults to (0,0). axis : {'x', 'y', 'z'} Central axis of the cylinders defining the inner and outer surfaces of the sector. Defaults to 'z'. @@ -119,7 +121,7 @@ def __init__(self, r2, theta1, theta2, - center=(0.,0.), + center=(0., 0.), axis='z', **kwargs): @@ -1856,3 +1858,96 @@ def __init__(self, center_base: Sequence[float], axis: Sequence[float], def __neg__(self) -> openmc.Region: return +self.plane_bottom & -self.plane_top & -self.cone + + +class Vessel(CompositeSurface): + """Vessel composed of cylinder with semi-ellipsoid top and bottom. + + This composite surface is represented by a finite cylinder with ellipsoidal + top and bottom surfaces. This surface is equivalent to the 'vesesl' surface + in Serpent. + + .. versionadded:: 0.15.1 + + Parameters + ---------- + r : float + Radius of vessel. + p1 : float + Minimum coordinate for cylindrical part of vessel. + p2 : float + Maximum coordinate for cylindrical part of vessel. + h1 : float + Height of bottom ellipsoidal part of vessel. + h2 : float + Height of top ellipsoidal part of vessel. + center : 2-tuple of float + Coordinate for central axis of the cylinder in the (y, z), (x, z), or + (x, y) basis. Defaults to (0,0). + axis : {'x', 'y', 'z'} + Central axis of the cylinder. + + """ + + _surface_names = ('cyl', 'plane_bottom', 'plane_top', 'bottom', 'top') + + def __init__(self, r: float, p1: float, p2: float, h1: float, h2: float, + center: Sequence[float] = (0., 0.), axis: str = 'z', **kwargs): + if p1 >= p2: + raise ValueError('p1 must be less than p2') + check_value('axis', axis, {'x', 'y', 'z'}) + + c1, c2 = center + cyl_class = getattr(openmc, f'{axis.upper()}Cylinder') + plane_class = getattr(openmc, f'{axis.upper()}Plane') + self.cyl = cyl_class(c1, c2, r, **kwargs) + self.plane_bottom = plane_class(p1) + self.plane_top = plane_class(p2) + + # General equation for an ellipsoid: + # (x-x₀)²/r² + (y-y₀)²/r² + (z-z₀)²/h² = 1 + # (x-x₀)² + (y-y₀)² + (z-z₀)²s² = r² + # Let s = r/h: + # (x² - 2x₀x + x₀²) + (y² - 2y₀y + y₀²) + (z² - 2z₀z + z₀²)s² = r² + # x² + y² + s²z² - 2x₀x - 2y₀y - 2s²z₀z + (x₀² + y₀² + z₀²s² - r²) = 0 + + sb = (r/h1) + st = (r/h2) + kwargs['a'] = kwargs['b'] = kwargs['c'] = 1.0 + kwargs_bottom = kwargs + kwargs_top = kwargs.copy() + + sb2 = sb*sb + st2 = st*st + kwargs_bottom['k'] = c1*c1 + c2*c2 + p1*p1*sb2 - r*r + kwargs_top['k'] = c1*c1 + c2*c2 + p2*p2*st2 - r*r + + if axis == 'x': + kwargs_bottom['a'] *= sb2 + kwargs_top['a'] *= st2 + kwargs_bottom['g'] = -2*p1*sb2 + kwargs_top['g'] = -2*p2*st2 + kwargs_top['h'] = kwargs_bottom['h'] = -2*c1 + kwargs_top['j'] = kwargs_bottom['j'] = -2*c2 + elif axis == 'y': + kwargs_bottom['b'] *= sb2 + kwargs_top['b'] *= st2 + kwargs_top['g'] = kwargs_bottom['g'] = -2*c1 + kwargs_bottom['h'] = -2*p1*sb2 + kwargs_top['h'] = -2*p2*st2 + kwargs_top['j'] = kwargs_bottom['j'] = -2*c2 + elif axis == 'z': + kwargs_bottom['c'] *= sb2 + kwargs_top['c'] *= st2 + kwargs_top['g'] = kwargs_bottom['g'] = -2*c1 + kwargs_top['h'] = kwargs_bottom['h'] = -2*c2 + kwargs_bottom['j'] = -2*p1*sb2 + kwargs_top['j'] = -2*p2*st2 + + self.bottom = openmc.Quadric(**kwargs_bottom) + self.top = openmc.Quadric(**kwargs_top) + + def __neg__(self): + return ((-self.cyl & +self.plane_bottom & -self.plane_top) | + (-self.bottom & -self.plane_bottom) | + (-self.top & +self.plane_top)) diff --git a/tests/unit_tests/test_surface_composite.py b/tests/unit_tests/test_surface_composite.py index 015ac667e45..da7ffbcc470 100644 --- a/tests/unit_tests/test_surface_composite.py +++ b/tests/unit_tests/test_surface_composite.py @@ -599,3 +599,52 @@ def test_conical_frustum(): # Denegenerate case with r1 = r2 s = openmc.model.ConicalFrustum(center_base, axis, r1, r1) assert (1., 1., -0.01) in -s + + +def test_vessel(): + center = (3.0, 2.0) + r = 1.0 + p1, p2 = -5.0, 5.0 + h1 = h2 = 1.0 + s = openmc.model.Vessel(r, p1, p2, h1, h2, center) + assert isinstance(s.cyl, openmc.Cylinder) + assert isinstance(s.plane_bottom, openmc.Plane) + assert isinstance(s.plane_top, openmc.Plane) + assert isinstance(s.bottom, openmc.Quadric) + assert isinstance(s.top, openmc.Quadric) + + # Make sure boundary condition propagates (but not for planes) + s.boundary_type = 'reflective' + assert s.boundary_type == 'reflective' + assert s.cyl.boundary_type == 'reflective' + assert s.bottom.boundary_type == 'reflective' + assert s.top.boundary_type == 'reflective' + assert s.plane_bottom.boundary_type == 'transmission' + assert s.plane_top.boundary_type == 'transmission' + + # Check bounding box + ll, ur = (+s).bounding_box + assert np.all(np.isinf(ll)) + assert np.all(np.isinf(ur)) + ll, ur = (-s).bounding_box + assert np.all(np.isinf(ll)) + assert np.all(np.isinf(ur)) + + # __contains__ on associated half-spaces + assert (3., 2., 0.) in -s + assert (3., 2., -5.0) in -s + assert (3., 2., 5.0) in -s + assert (3., 2., -5.9) in -s + assert (3., 2., 5.9) in -s + assert (3., 2., -6.1) not in -s + assert (3., 2., 6.1) not in -s + assert (4.5, 2., 0.) in +s + assert (3., 3.2, 0.) in +s + assert (3., 2., 7.) in +s + + # translate method + s_t = s.translate((0., 0., 1.)) + assert (3., 2., 6.1) in -s_t + + # Make sure repr works + repr(s) From 58400cbf1099fdaa73cc4ae17167cd0e8b52c97d Mon Sep 17 00:00:00 2001 From: Paul Wilson Date: Wed, 13 Nov 2024 15:39:41 -0600 Subject: [PATCH 094/184] Add PointCloud spatial distribution (#3161) Co-authored-by: Patrick Shriwise Co-authored-by: Paul Romano --- docs/source/io_formats/settings.rst | 56 ++++++++++--- docs/source/pythonapi/stats.rst | 1 + docs/source/usersguide/settings.rst | 4 +- include/openmc/distribution_spatial.h | 20 +++++ include/openmc/xml_interface.h | 3 + openmc/stats/multivariate.py | 114 ++++++++++++++++++++++++++ src/distribution_spatial.cpp | 45 ++++++++++ src/xml_interface.cpp | 19 +++++ tests/unit_tests/test_source.py | 56 +++++++++++++ 9 files changed, 306 insertions(+), 12 deletions(-) diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index 2c6c3265649..34b73fc55c5 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -579,24 +579,38 @@ attributes/sub-elements: :type: The type of spatial distribution. Valid options are "box", "fission", - "point", "cartesian", "cylindrical", and "spherical". A "box" spatial - distribution has coordinates sampled uniformly in a parallelepiped. A - "fission" spatial distribution samples locations from a "box" + "point", "cartesian", "cylindrical", "spherical", "mesh", and "cloud". + + A "box" spatial distribution has coordinates sampled uniformly in a + parallelepiped. + + A "fission" spatial distribution samples locations from a "box" distribution but only locations in fissionable materials are accepted. + A "point" spatial distribution has coordinates specified by a triplet. + A "cartesian" spatial distribution specifies independent distributions of - x-, y-, and z-coordinates. A "cylindrical" spatial distribution specifies - independent distributions of r-, phi-, and z-coordinates where phi is the - azimuthal angle and the origin for the cylindrical coordinate system is - specified by origin. A "spherical" spatial distribution specifies - independent distributions of r-, cos_theta-, and phi-coordinates where - cos_theta is the cosine of the angle with respect to the z-axis, phi is - the azimuthal angle, and the sphere is centered on the coordinate - (x0,y0,z0). A "mesh" spatial distribution samples source sites from a mesh element + x-, y-, and z-coordinates. + + A "cylindrical" spatial distribution specifies independent distributions + of r-, phi-, and z-coordinates where phi is the azimuthal angle and the + origin for the cylindrical coordinate system is specified by origin. + + A "spherical" spatial distribution specifies independent distributions of + r-, cos_theta-, and phi-coordinates where cos_theta is the cosine of the + angle with respect to the z-axis, phi is the azimuthal angle, and the + sphere is centered on the coordinate (x0,y0,z0). + + A "mesh" spatial distribution samples source sites from a mesh element based on the relative strengths provided in the node. Source locations within an element are sampled isotropically. If no strengths are provided, the space within the mesh is uniformly sampled. + A "cloud" spatial distribution samples source sites from a list of spatial + positions provided in the node, based on the relative strengths provided + in the node. If no strengths are provided, the positions are uniformly + sampled. + *Default*: None :parameters: @@ -662,6 +676,26 @@ attributes/sub-elements: For "cylindrical and "spherical" distributions, this element specifies the coordinates for the origin of the coordinate system. + :mesh_id: + For "mesh" spatial distributions, this element specifies which mesh ID to + use for the geometric description of the mesh. + + :coords: + For "cloud" distributions, this element specifies a list of coordinates + for each of the points in the cloud. + + :strengths: + For "mesh" and "cloud" spatial distributions, this element specifies the + relative source strength of each mesh element or each point in the cloud. + + :volume_normalized: + For "mesh" spatial distrubtions, this optional boolean element specifies + whether the vector of relative strengths should be multiplied by the mesh + element volume. This is most common if the strengths represent a source + per unit volume. + + *Default*: false + :angle: An element specifying the angular distribution of source sites. This element has the following attributes: diff --git a/docs/source/pythonapi/stats.rst b/docs/source/pythonapi/stats.rst index b72896c1860..c8318ba8620 100644 --- a/docs/source/pythonapi/stats.rst +++ b/docs/source/pythonapi/stats.rst @@ -59,6 +59,7 @@ Spatial Distributions openmc.stats.Box openmc.stats.Point openmc.stats.MeshSpatial + openmc.stats.PointCloud .. autosummary:: :toctree: generated diff --git a/docs/source/usersguide/settings.rst b/docs/source/usersguide/settings.rst index 4111481a9ea..349aa34d07a 100644 --- a/docs/source/usersguide/settings.rst +++ b/docs/source/usersguide/settings.rst @@ -192,7 +192,9 @@ distributions using spherical or cylindrical coordinates, you can use :class:`openmc.stats.SphericalIndependent` or :class:`openmc.stats.CylindricalIndependent`, respectively. Meshes can also be used to represent spatial distributions with :class:`openmc.stats.MeshSpatial` -by specifying a mesh and source strengths for each mesh element. +by specifying a mesh and source strengths for each mesh element. It is also +possible to define a "cloud" of source points, each with a different relative +probability, using :class:`openmc.stats.PointCloud`. The angular distribution can be set equal to a sub-class of :class:`openmc.stats.UnitSphere` such as :class:`openmc.stats.Isotropic`, diff --git a/include/openmc/distribution_spatial.h b/include/openmc/distribution_spatial.h index 28568c890d8..8ff766d1333 100644 --- a/include/openmc/distribution_spatial.h +++ b/include/openmc/distribution_spatial.h @@ -136,6 +136,26 @@ class MeshSpatial : public SpatialDistribution { //!< mesh element indices }; +//============================================================================== +//! Distribution of points +//============================================================================== + +class PointCloud : public SpatialDistribution { +public: + explicit PointCloud(pugi::xml_node node); + explicit PointCloud( + std::vector point_cloud, gsl::span strengths); + + //! Sample a position from the distribution + //! \param seed Pseudorandom number seed pointer + //! \return Sampled position + Position sample(uint64_t* seed) const override; + +private: + std::vector point_cloud_; + DiscreteIndex point_idx_dist_; //!< Distribution of Position indices +}; + //============================================================================== //! Uniform distribution of points over a box //============================================================================== diff --git a/include/openmc/xml_interface.h b/include/openmc/xml_interface.h index bd6554c134a..f49613ecde1 100644 --- a/include/openmc/xml_interface.h +++ b/include/openmc/xml_interface.h @@ -50,6 +50,9 @@ xt::xarray get_node_xarray( return xt::adapt(v, shape); } +std::vector get_node_position_array( + pugi::xml_node node, const char* name, bool lowercase = false); + Position get_node_position( pugi::xml_node node, const char* name, bool lowercase = false); diff --git a/openmc/stats/multivariate.py b/openmc/stats/multivariate.py index 06c65896f49..cd474fa9b28 100644 --- a/openmc/stats/multivariate.py +++ b/openmc/stats/multivariate.py @@ -279,6 +279,8 @@ def from_xml_element(cls, elem, meshes=None): return Point.from_xml_element(elem) elif distribution == 'mesh': return MeshSpatial.from_xml_element(elem, meshes) + elif distribution == 'cloud': + return PointCloud.from_xml_element(elem) class CartesianIndependent(Spatial): @@ -756,6 +758,118 @@ def from_xml_element(cls, elem, meshes): return cls(meshes[mesh_id], strengths, volume_normalized) +class PointCloud(Spatial): + """Spatial distribution from a point cloud. + + This distribution specifies a discrete list of points, with corresponding + relative probabilities. + + .. versionadded:: 0.15.1 + + Parameters + ---------- + positions : iterable of 3-tuples + The points in space to be sampled + strengths : iterable of float, optional + An iterable of values that represents the relative probabilty of each + point. + + Attributes + ---------- + positions : numpy.ndarray + The points in space to be sampled with shape (N, 3) + strengths : numpy.ndarray or None + An array of relative probabilities for each mesh point + """ + + def __init__( + self, + positions: Sequence[Sequence[float]], + strengths: Sequence[float] | None = None + ): + self.positions = positions + self.strengths = strengths + + @property + def positions(self) -> np.ndarray: + return self._positions + + @positions.setter + def positions(self, positions): + positions = np.array(positions, dtype=float) + if positions.ndim != 2: + raise ValueError('positions must be a 2D array') + elif positions.shape[1] != 3: + raise ValueError('Each position must have 3 values') + self._positions = positions + + @property + def strengths(self) -> np.ndarray: + return self._strengths + + @strengths.setter + def strengths(self, strengths): + if strengths is not None: + strengths = np.array(strengths, dtype=float) + if strengths.ndim != 1: + raise ValueError('strengths must be a 1D array') + elif strengths.size != self.positions.shape[0]: + raise ValueError('strengths must have the same length as positions') + self._strengths = strengths + + @property + def num_strength_bins(self) -> int: + if self.strengths is None: + raise ValueError('Strengths are not set') + return self.strengths.size + + def to_xml_element(self) -> ET.Element: + """Return XML representation of the spatial distribution + + Returns + ------- + element : lxml.etree._Element + XML element containing spatial distribution data + + """ + element = ET.Element('space') + element.set('type', 'cloud') + + subelement = ET.SubElement(element, 'coords') + subelement.text = ' '.join(str(e) for e in self.positions.flatten()) + + if self.strengths is not None: + subelement = ET.SubElement(element, 'strengths') + subelement.text = ' '.join(str(e) for e in self.strengths) + + return element + + @classmethod + def from_xml_element(cls, elem: ET.Element) -> PointCloud: + """Generate spatial distribution from an XML element + + Parameters + ---------- + elem : lxml.etree._Element + XML element + + Returns + ------- + openmc.stats.PointCloud + Spatial distribution generated from XML element + + + """ + coord_data = get_text(elem, 'coords') + positions = np.array([float(b) for b in coord_data.split()]).reshape((-1, 3)) + + strengths = get_text(elem, 'strengths') + if strengths is not None: + strengths = [float(b) for b in strengths.split()] + + return cls(positions, strengths) + + class Box(Spatial): """Uniform distribution of coordinates in a rectangular cuboid. diff --git a/src/distribution_spatial.cpp b/src/distribution_spatial.cpp index 8dbd7d88706..5c193a95d29 100644 --- a/src/distribution_spatial.cpp +++ b/src/distribution_spatial.cpp @@ -26,6 +26,8 @@ unique_ptr SpatialDistribution::create(pugi::xml_node node) return UPtrSpace {new SphericalIndependent(node)}; } else if (type == "mesh") { return UPtrSpace {new MeshSpatial(node)}; + } else if (type == "cloud") { + return UPtrSpace {new PointCloud(node)}; } else if (type == "box") { return UPtrSpace {new SpatialBox(node)}; } else if (type == "fission") { @@ -298,6 +300,49 @@ Position MeshSpatial::sample(uint64_t* seed) const return this->sample_mesh(seed).second; } +//============================================================================== +// PointCloud implementation +//============================================================================== + +PointCloud::PointCloud(pugi::xml_node node) +{ + if (check_for_node(node, "coords")) { + point_cloud_ = get_node_position_array(node, "coords"); + } else { + fatal_error("No coordinates were provided for the PointCloud " + "spatial distribution"); + } + + std::vector strengths; + + if (check_for_node(node, "strengths")) + strengths = get_node_array(node, "strengths"); + else + strengths.resize(point_cloud_.size(), 1.0); + + if (strengths.size() != point_cloud_.size()) { + fatal_error( + fmt::format("Number of entries for the strengths array {} does " + "not match the number of spatial points provided {}.", + strengths.size(), point_cloud_.size())); + } + + point_idx_dist_.assign(strengths); +} + +PointCloud::PointCloud( + std::vector point_cloud, gsl::span strengths) +{ + point_cloud_.assign(point_cloud.begin(), point_cloud.end()); + point_idx_dist_.assign(strengths); +} + +Position PointCloud::sample(uint64_t* seed) const +{ + int32_t index = point_idx_dist_.sample(seed); + return point_cloud_[index]; +} + //============================================================================== // SpatialBox implementation //============================================================================== diff --git a/src/xml_interface.cpp b/src/xml_interface.cpp index 6ce465bafb9..840d3f5b871 100644 --- a/src/xml_interface.cpp +++ b/src/xml_interface.cpp @@ -4,6 +4,7 @@ #include "openmc/error.h" #include "openmc/string_utils.h" +#include "openmc/vector.h" namespace openmc { @@ -48,6 +49,24 @@ bool get_node_value_bool(pugi::xml_node node, const char* name) return false; } +vector get_node_position_array( + pugi::xml_node node, const char* name, bool lowercase) +{ + vector coords = get_node_array(node, name, lowercase); + if (coords.size() % 3 != 0) { + fatal_error(fmt::format( + "Incorect number of coordinates in Position array ({}) for \"{}\"", + coords.size(), name)); + } + vector positions; + positions.reserve(coords.size() / 3); + auto it = coords.begin(); + for (size_t i = 0; i < coords.size(); i += 3) { + positions.push_back({coords[i], coords[i + 1], coords[i + 2]}); + } + return positions; +} + Position get_node_position( pugi::xml_node node, const char* name, bool lowercase) { diff --git a/tests/unit_tests/test_source.py b/tests/unit_tests/test_source.py index 32650d54936..c88fbcbe62d 100644 --- a/tests/unit_tests/test_source.py +++ b/tests/unit_tests/test_source.py @@ -1,3 +1,4 @@ +from collections import Counter from math import pi import openmc @@ -49,6 +50,61 @@ def test_spherical_uniform(): assert isinstance(sph_indep_function, openmc.stats.SphericalIndependent) +def test_point_cloud(): + positions = [(1, 0, 2), (0, 1, 0), (0, 0, 3), (4, 9, 2)] + strengths = [1, 2, 3, 4] + + space = openmc.stats.PointCloud(positions, strengths) + np.testing.assert_equal(space.positions, positions) + np.testing.assert_equal(space.strengths, strengths) + + src = openmc.IndependentSource(space=space) + assert src.space == space + np.testing.assert_equal(src.space.positions, positions) + np.testing.assert_equal(src.space.strengths, strengths) + + elem = src.to_xml_element() + src = openmc.IndependentSource.from_xml_element(elem) + np.testing.assert_equal(src.space.positions, positions) + np.testing.assert_equal(src.space.strengths, strengths) + + +def test_point_cloud_invalid(): + with pytest.raises(ValueError, match='2D'): + openmc.stats.PointCloud([1, 0, 2, 0, 1, 0]) + + with pytest.raises(ValueError, match='3 values'): + openmc.stats.PointCloud([(1, 0, 2, 3), (4, 5, 2, 3)]) + + with pytest.raises(ValueError, match='1D'): + openmc.stats.PointCloud([(1, 0, 2), (4, 5, 2)], [(1, 2), (3, 4)]) + + with pytest.raises(ValueError, match='same length'): + openmc.stats.PointCloud([(1, 0, 2), (4, 5, 2)], [1, 2, 4]) + + +def test_point_cloud_strengths(run_in_tmpdir, sphere_box_model): + positions = [(1., 0., 2.), (0., 1., 0.), (0., 0., 3.), (-1., -1., 2.)] + strengths = [1, 2, 3, 4] + space = openmc.stats.PointCloud(positions, strengths) + + model = sphere_box_model[0] + model.settings.run_mode = 'fixed source' + model.settings.source = openmc.IndependentSource(space=space) + + try: + model.init_lib() + n_samples = 50_000 + sites = openmc.lib.sample_external_source(n_samples) + finally: + model.finalize_lib() + + count = Counter(s.r for s in sites) + for i, (strength, position) in enumerate(zip(strengths, positions)): + sampled_strength = count[position] / n_samples + expected_strength = pytest.approx(strength/sum(strengths), abs=0.02) + assert sampled_strength == expected_strength, f'Strength incorrect for {positions[i]}' + def test_source_file(): filename = 'source.h5' From d30b2e801413faaee07282969955991abdf7baeb Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 13 Nov 2024 15:58:22 -0600 Subject: [PATCH 095/184] Fix docstring for Model.plot (#3198) --- openmc/model/model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index 0ea6db8db88..ffad8f503c7 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -820,14 +820,14 @@ def plot( Parameters ---------- - n_samples : dict + n_samples : int, optional The number of source particles to sample and add to plot. Defaults to None which doesn't plot any particles on the plot. plane_tolerance: float When plotting a plane the source locations within the plane +/- the plane_tolerance will be included and those outside of the plane_tolerance will not be shown - source_kwargs : dict + source_kwargs : dict, optional Keyword arguments passed to :func:`matplotlib.pyplot.scatter`. **kwargs Keyword arguments passed to :func:`openmc.Universe.plot` From fbb115921c71997d37e62748a077bdd369667786 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Mon, 18 Nov 2024 08:52:59 -0600 Subject: [PATCH 096/184] Apply weight windows at collisions in multigroup transport mode. (#3199) --- src/physics_mg.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/physics_mg.cpp b/src/physics_mg.cpp index 361cf5affcf..3cc0532d2b1 100644 --- a/src/physics_mg.cpp +++ b/src/physics_mg.cpp @@ -19,6 +19,7 @@ #include "openmc/settings.h" #include "openmc/simulation.h" #include "openmc/tallies/tally.h" +#include "openmc/weight_windows.h" namespace openmc { @@ -27,6 +28,9 @@ void collision_mg(Particle& p) // Add to the collision counter for the particle p.n_collision()++; + if (settings::weight_window_checkpoint_collision) + apply_weight_windows(p); + // Sample the reaction type sample_reaction(p); From 172867b1df18e98c3c0ff5d5645e966f8583a63e Mon Sep 17 00:00:00 2001 From: John Tramm Date: Wed, 20 Nov 2024 14:15:41 -0600 Subject: [PATCH 097/184] Random Ray Adjoint Mode (#3191) Co-authored-by: Paul Romano --- docs/source/methods/random_ray.rst | 45 +++- docs/source/usersguide/random_ray.rst | 29 ++- include/openmc/mgxs.h | 2 + .../openmc/random_ray/flat_source_domain.h | 21 +- .../openmc/random_ray/random_ray_simulation.h | 9 +- openmc/deplete/openmc_operator.py | 2 +- openmc/settings.py | 9 + src/mgxs_interface.cpp | 2 +- src/random_ray/flat_source_domain.cpp | 214 ++++++++------- src/random_ray/linear_source_domain.cpp | 40 ++- src/random_ray/random_ray.cpp | 20 +- src/random_ray/random_ray_simulation.cpp | 151 +++++++++-- src/settings.cpp | 4 + .../__init__.py | 0 .../inputs_true.dat | 245 ++++++++++++++++++ .../results_true.dat | 9 + .../random_ray_adjoint_fixed_source/test.py | 20 ++ .../random_ray_adjoint_k_eff/__init__.py | 0 .../random_ray_adjoint_k_eff/inputs_true.dat | 110 ++++++++ .../random_ray_adjoint_k_eff/results_true.dat | 171 ++++++++++++ .../random_ray_adjoint_k_eff/test.py | 20 ++ 21 files changed, 956 insertions(+), 167 deletions(-) create mode 100644 tests/regression_tests/random_ray_adjoint_fixed_source/__init__.py create mode 100644 tests/regression_tests/random_ray_adjoint_fixed_source/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_adjoint_fixed_source/results_true.dat create mode 100644 tests/regression_tests/random_ray_adjoint_fixed_source/test.py create mode 100644 tests/regression_tests/random_ray_adjoint_k_eff/__init__.py create mode 100644 tests/regression_tests/random_ray_adjoint_k_eff/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_adjoint_k_eff/results_true.dat create mode 100644 tests/regression_tests/random_ray_adjoint_k_eff/test.py diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst index 3d98747e4af..c827c351dd0 100644 --- a/docs/source/methods/random_ray.rst +++ b/docs/source/methods/random_ray.rst @@ -542,7 +542,7 @@ in that cell for the iteration from Equation :eq:`phi_naive` to: .. math:: :label: phi_missed_one - \phi_{i,g,n}^{missed} = \frac{Q_{i,g,n} }{\Sigma_{t,i,g}} + \phi_{i,g,n}^{missed} = \frac{Q_{i,g,n} }{\Sigma_{t,i,g}} as the streaming operator has gone to zero. While this is obviously innacurate as it ignores transport, for most problems where the region is only occasionally @@ -1060,6 +1060,49 @@ random ray and Monte Carlo, however. develop the scattering source by way of inactive batches before beginning active batches. +------------------------ +Adjoint Flux Solver Mode +------------------------ + +The random ray solver in OpenMC can also be used to solve for the adjoint flux, +:math:`\psi^{\dagger}`. In combination with the regular (forward) flux solution, +the adjoint flux is useful for perturbation methods as well as for computing +weight windows for subsequent Monte Carlo simulations. The adjoint flux can be +thought of as the "backwards" flux, representing the flux where a particle is +born at an absoprtion point (and typical absorption energy), and then undergoes +transport with a transposed scattering matrix. That is, instead of sampling a +particle and seeing where it might go as in a standard forward solve, we will +sample an absorption location and see where the particle that was absorbed there +might have come from. Notably, for typical neutron absorption at low energy +levels, this means that adjoint flux particles are typically sampled at a low +energy and then upscatter (via a transposed scattering matrix) over their +lifetimes. + +In OpenMC, the random ray adjoint solver is implemented simply by transposing +the scattering matrix, swapping :math:`\nu\Sigma_f` and :math:`\chi`, and then +running a normal transport solve. When no external fixed source is present, no +additional changes are needed in the transport process. However, if an external +fixed forward source is present in the simulation problem, then an additional +step is taken to compute the accompanying fixed adjoint source. In OpenMC, the +adjoint flux does *not* represent a response function for a particular detector +region. Rather, the adjoint flux is the global response, making it appropriate +for use with weight window generation schemes for global variance reduction. +Thus, if using a fixed source, the external source for the adjoint mode is +simply computed as being :math:`1 / \phi`, where :math:`\phi` is the forward +scalar flux that results from a normal forward solve (which OpenMC will run +first automatically when in adjoint mode). The adjoint external source will be +computed for each source region in the simulation mesh, independent of any +tallies. The adjoint external source is always flat, even when a linear +scattering and fission source shape is used. When in adjoint mode, all reported +results (e.g., tallies, eigenvalues, etc.) are derived from the adjoint flux, +even when the physical meaning is not necessarily obvious. These values are +still reported, though we emphasize that the primary use case for adjoint mode +is for producing adjoint flux tallies to support subsequent perturbation studies +and weight window generation. + +Note that the adjoint :math:`k_{eff}` is statistically the same as the forward +:math:`k_{eff}`, despite the flux distributions taking different shapes. + --------------------------- Fundamental Sources of Bias --------------------------- diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index 5ca0ab6bed9..2b9cf67240b 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -558,7 +558,7 @@ following methods are currently available in OpenMC: - Cons * - ``simulation_averaged`` - Accumulates total active ray lengths in each FSR over all iterations, - improving the estimate of the volume in each cell each iteration. + improving the estimate of the volume in each cell each iteration. - * Virtually unbiased after several iterations * Asymptotically approaches the true analytical volume * Typically most efficient in terms of speed vs. accuracy @@ -593,6 +593,33 @@ estimator, the following code would be used: settings.random_ray['volume_estimator'] = 'naive' +----------------- +Adjoint Flux Mode +----------------- + +The adjoint flux random ray solver mode can be enabled as: +entire +:: + + settings.random_ray['adjoint'] = True + +When enabled, OpenMC will first run a forward transport simulation followed by +an adjoint transport simulation. The purpose of the forward solve is to compute +the adjoint external source when an external source is present in the +simulation. Simulation settings (e.g., number of rays, batches, etc.) will be +identical for both simulations. At the conclusion of the run, all results (e.g., +tallies, plots, etc.) will be derived from the adjoint flux rather than the +forward flux but are not labeled any differently. The initial forward flux +solution will not be stored or available in the final statepoint file. Those +wishing to do analysis requiring both the forward and adjoint solutions will +need to run two separate simulations and load both statepoint files. + +.. note:: + When adjoint mode is selected, OpenMC will always perform a full forward + solve and then run a full adjoint solve immediately afterwards. Statepoint + and tally results will be derived from the adjoint flux, but will not be + labeled any differently. + --------------------------------------- Putting it All Together: Example Inputs --------------------------------------- diff --git a/include/openmc/mgxs.h b/include/openmc/mgxs.h index 3fb0608104f..9b1602f299a 100644 --- a/include/openmc/mgxs.h +++ b/include/openmc/mgxs.h @@ -86,8 +86,10 @@ class Mgxs { bool fissionable; // Is this fissionable bool is_isotropic { true}; // used to skip search for angle indices if isotropic + bool exists_in_model {true}; // Is this present in model Mgxs() = default; + Mgxs(bool exists) : exists_in_model(exists) {} //! \brief Constructor that loads the Mgxs object from the HDF5 file //! diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index 5c50f7fb0a0..b5b5db7062e 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -111,13 +111,17 @@ class FlatSourceDomain { virtual void all_reduce_replicated_source_regions(); void convert_external_sources(); void count_external_source_regions(); + void set_adjoint_sources(const vector& forward_flux); virtual void flux_swap(); virtual double evaluate_flux_at_point(Position r, int64_t sr, int g) const; double compute_fixed_source_normalization_factor() const; + void flatten_xs(); + void transpose_scattering_matrix(); //---------------------------------------------------------------------------- // Static Data members static bool volume_normalized_flux_tallies_; + static bool adjoint_; // If the user wants outputs based on the adjoint flux //---------------------------------------------------------------------------- // Static data members @@ -150,6 +154,19 @@ class FlatSourceDomain { vector source_; vector external_source_; vector external_source_present_; + vector scalar_flux_final_; + + // 2D arrays stored in 1D representing values for all materials x energy + // groups + int n_materials_; + vector sigma_t_; + vector nu_sigma_f_; + vector sigma_f_; + vector chi_; + + // 3D arrays stored in 1D representing values for all materials x energy + // groups x energy groups + vector sigma_s_; protected: //---------------------------------------------------------------------------- @@ -190,10 +207,6 @@ class FlatSourceDomain { vector material_; vector volume_naive_; - // 2D arrays stored in 1D representing values for all source regions x energy - // groups - vector scalar_flux_final_; - // Volumes for each tally and bin/score combination. This intermediate data // structure is used when tallying quantities that must be normalized by // volume (i.e., flux). The vector is index by tally index, while the inner 2D diff --git a/include/openmc/random_ray/random_ray_simulation.h b/include/openmc/random_ray/random_ray_simulation.h index 55bac6905c6..01f12bffb17 100644 --- a/include/openmc/random_ray/random_ray_simulation.h +++ b/include/openmc/random_ray/random_ray_simulation.h @@ -20,6 +20,8 @@ class RandomRaySimulation { //---------------------------------------------------------------------------- // Methods void compute_segment_correction_factors(); + void prepare_fixed_sources(); + void prepare_fixed_sources_adjoint(vector& forward_flux); void simulate(); void reduce_simulation_statistics(); void output_simulation_results() const; @@ -30,8 +32,13 @@ class RandomRaySimulation { int64_t n_external_source_regions) const; //---------------------------------------------------------------------------- - // Data members + // Accessors + FlatSourceDomain* domain() const { return domain_.get(); } + private: + //---------------------------------------------------------------------------- + // Data members + // Contains all flat source region data unique_ptr domain_; diff --git a/openmc/deplete/openmc_operator.py b/openmc/deplete/openmc_operator.py index 5837cd2c187..049e0dd37ee 100644 --- a/openmc/deplete/openmc_operator.py +++ b/openmc/deplete/openmc_operator.py @@ -208,7 +208,7 @@ def _get_burnable_mats(self) -> tuple[list[str], dict[str, float], list[str]]: if nuclide in self.nuclides_with_data or self._decay_nucs: model_nuclides.add(nuclide) else: - msg = (f"Nuclilde {nuclide} in material {mat.id} is not " + msg = (f"Nuclide {nuclide} in material {mat.id} is not " "present in the depletion chain and has no cross " "section data.") warn(msg) diff --git a/openmc/settings.py b/openmc/settings.py index 0a78fb564f8..a350de72ecc 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -170,6 +170,9 @@ class Settings: cm/cm^3. When disabled, flux tallies will be reported in units of cm (i.e., total distance traveled by neutrons in the spatial tally region). + :adjoint: + Whether to run the random ray solver in adjoint mode (bool). The + default is 'False'. .. versionadded:: 0.15.0 resonance_scattering : dict @@ -1113,6 +1116,8 @@ def random_ray(self, random_ray: dict): ('flat', 'linear', 'linear_xy')) elif key == 'volume_normalized_flux_tallies': cv.check_type('volume normalized flux tallies', random_ray[key], bool) + elif key == 'adjoint': + cv.check_type('adjoint', random_ray[key], bool) else: raise ValueError(f'Unable to set random ray to "{key}" which is ' 'unsupported by OpenMC') @@ -1916,6 +1921,10 @@ def _random_ray_from_xml_element(self, root): self.random_ray['volume_normalized_flux_tallies'] = ( child.text in ('true', '1') ) + elif child.tag == 'adjoint': + self.random_ray['adjoint'] = ( + child.text in ('true', '1') + ) def to_xml_element(self, mesh_memo=None): """Create a 'settings' element to be written to an XML file. diff --git a/src/mgxs_interface.cpp b/src/mgxs_interface.cpp index 0febc612bf0..ed734d401ea 100644 --- a/src/mgxs_interface.cpp +++ b/src/mgxs_interface.cpp @@ -146,7 +146,7 @@ void MgxsInterface::create_macro_xs() num_energy_groups_, num_delayed_groups_); } else { // Preserve the ordering of materials by including a blank entry - macro_xs_.emplace_back(); + macro_xs_.emplace_back(false); } } } diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 62768b55f0f..19913c03c16 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -27,6 +27,7 @@ namespace openmc { RandomRayVolumeEstimator FlatSourceDomain::volume_estimator_ { RandomRayVolumeEstimator::HYBRID}; bool FlatSourceDomain::volume_normalized_flux_tallies_ {false}; +bool FlatSourceDomain::adjoint_ {false}; FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) { @@ -134,31 +135,23 @@ void FlatSourceDomain::update_neutron_source(double k_eff) double inverse_k_eff = 1.0 / k_eff; - // Temperature and angle indices, if using multiple temperature - // data sets and/or anisotropic data sets. - // TODO: Currently assumes we are only using single temp/single angle data. - const int t = 0; - const int a = 0; - // Add scattering source #pragma omp parallel for for (int sr = 0; sr < n_source_regions_; sr++) { int material = material_[sr]; - for (int e_out = 0; e_out < negroups_; e_out++) { - double sigma_t = data::mg.macro_xs_[material].get_xs( - MgxsType::TOTAL, e_out, nullptr, nullptr, nullptr, t, a); - double scatter_source = 0.0f; + for (int g_out = 0; g_out < negroups_; g_out++) { + double sigma_t = sigma_t_[material * negroups_ + g_out]; + double scatter_source = 0.0; - for (int e_in = 0; e_in < negroups_; e_in++) { - double scalar_flux = scalar_flux_old_[sr * negroups_ + e_in]; - - double sigma_s = data::mg.macro_xs_[material].get_xs( - MgxsType::NU_SCATTER, e_in, &e_out, nullptr, nullptr, t, a); + for (int g_in = 0; g_in < negroups_; g_in++) { + double scalar_flux = scalar_flux_old_[sr * negroups_ + g_in]; + double sigma_s = + sigma_s_[material * negroups_ * negroups_ + g_out * negroups_ + g_in]; scatter_source += sigma_s * scalar_flux; } - source_[sr * negroups_ + e_out] = scatter_source / sigma_t; + source_[sr * negroups_ + g_out] = scatter_source / sigma_t; } } @@ -167,20 +160,17 @@ void FlatSourceDomain::update_neutron_source(double k_eff) for (int sr = 0; sr < n_source_regions_; sr++) { int material = material_[sr]; - for (int e_out = 0; e_out < negroups_; e_out++) { - double sigma_t = data::mg.macro_xs_[material].get_xs( - MgxsType::TOTAL, e_out, nullptr, nullptr, nullptr, t, a); - double fission_source = 0.0f; - - for (int e_in = 0; e_in < negroups_; e_in++) { - double scalar_flux = scalar_flux_old_[sr * negroups_ + e_in]; - double nu_sigma_f = data::mg.macro_xs_[material].get_xs( - MgxsType::NU_FISSION, e_in, nullptr, nullptr, nullptr, t, a); - double chi = data::mg.macro_xs_[material].get_xs( - MgxsType::CHI_PROMPT, e_in, &e_out, nullptr, nullptr, t, a); + for (int g_out = 0; g_out < negroups_; g_out++) { + double sigma_t = sigma_t_[material * negroups_ + g_out]; + double fission_source = 0.0; + + for (int g_in = 0; g_in < negroups_; g_in++) { + double scalar_flux = scalar_flux_old_[sr * negroups_ + g_in]; + double nu_sigma_f = nu_sigma_f_[material * negroups_ + g_in]; + double chi = chi_[material * negroups_ + g_out]; fission_source += nu_sigma_f * scalar_flux * chi; } - source_[sr * negroups_ + e_out] += + source_[sr * negroups_ + g_out] += fission_source * inverse_k_eff / sigma_t; } } @@ -188,7 +178,7 @@ void FlatSourceDomain::update_neutron_source(double k_eff) // Add external source if in fixed source mode if (settings::run_mode == RunMode::FIXED_SOURCE) { #pragma omp parallel for - for (int se = 0; se < n_source_elements_; se++) { + for (int64_t se = 0; se < n_source_elements_; se++) { source_[se] += external_source_[se]; } } @@ -206,8 +196,8 @@ void FlatSourceDomain::normalize_scalar_flux_and_volumes( // Normalize scalar flux to total distance travelled by all rays this iteration #pragma omp parallel for - for (int64_t e = 0; e < scalar_flux_new_.size(); e++) { - scalar_flux_new_[e] *= normalization_factor; + for (int64_t se = 0; se < scalar_flux_new_.size(); se++) { + scalar_flux_new_[se] *= normalization_factor; } // Accumulate cell-wise ray length tallies collected this iteration, then @@ -223,16 +213,7 @@ void FlatSourceDomain::normalize_scalar_flux_and_volumes( void FlatSourceDomain::set_flux_to_flux_plus_source( int64_t idx, double volume, int material, int g) { - // Temperature and angle indices, if using multiple temperature - // data sets and/or anisotropic data sets. - // TODO: Currently assumes we are only using single temp/single - // angle data. - const int t = 0; - const int a = 0; - - double sigma_t = data::mg.macro_xs_[material].get_xs( - MgxsType::TOTAL, g, nullptr, nullptr, nullptr, t, a); - + double sigma_t = sigma_t_[material * negroups_ + g]; scalar_flux_new_[idx] /= (sigma_t * volume); scalar_flux_new_[idx] += source_[idx]; } @@ -337,13 +318,6 @@ double FlatSourceDomain::compute_k_eff(double k_eff_old) const double fission_rate_old = 0; double fission_rate_new = 0; - // Temperature and angle indices, if using multiple temperature - // data sets and/or anisotropic data sets. - // TODO: Currently assumes we are only using single temp/single - // angle data. - const int t = 0; - const int a = 0; - // Vector for gathering fission source terms for Shannon entropy calculation vector p(n_source_regions_, 0.0f); @@ -363,8 +337,7 @@ double FlatSourceDomain::compute_k_eff(double k_eff_old) const for (int g = 0; g < negroups_; g++) { int64_t idx = (sr * negroups_) + g; - double nu_sigma_f = data::mg.macro_xs_[material].get_xs( - MgxsType::NU_FISSION, g, nullptr, nullptr, nullptr, t, a); + double nu_sigma_f = nu_sigma_f_[material * negroups_ + g]; sr_fission_source_old += nu_sigma_f * scalar_flux_old_[idx]; sr_fission_source_new += nu_sigma_f * scalar_flux_new_[idx]; } @@ -548,7 +521,7 @@ double FlatSourceDomain::compute_fixed_source_normalization_factor() const { // If we are not in fixed source mode, then there are no external sources // so no normalization is needed. - if (settings::run_mode != RunMode::FIXED_SOURCE) { + if (settings::run_mode != RunMode::FIXED_SOURCE || adjoint_) { return 1.0; } @@ -559,17 +532,10 @@ double FlatSourceDomain::compute_fixed_source_normalization_factor() const for (int sr = 0; sr < n_source_regions_; sr++) { int material = material_[sr]; double volume = volume_[sr] * simulation_volume_; - for (int e = 0; e < negroups_; e++) { - // Temperature and angle indices, if using multiple temperature - // data sets and/or anisotropic data sets. - // TODO: Currently assumes we are only using single temp/single - // angle data. - const int t = 0; - const int a = 0; - double sigma_t = data::mg.macro_xs_[material].get_xs( - MgxsType::TOTAL, e, nullptr, nullptr, nullptr, t, a); + for (int g = 0; g < negroups_; g++) { + double sigma_t = sigma_t_[material * negroups_ + g]; simulation_external_source_strength += - external_source_[sr * negroups_ + e] * sigma_t * volume; + external_source_[sr * negroups_ + g] * sigma_t * volume; } } @@ -603,13 +569,6 @@ void FlatSourceDomain::random_ray_tally() // Reset our tally volumes to zero reset_tally_volumes(); - // Temperature and angle indices, if using multiple temperature - // data sets and/or anisotropic data sets. - // TODO: Currently assumes we are only using single temp/single - // angle data. - const int t = 0; - const int a = 0; - double source_normalization_factor = compute_fixed_source_normalization_factor(); @@ -644,21 +603,15 @@ void FlatSourceDomain::random_ray_tally() break; case SCORE_TOTAL: - score = flux * volume * - data::mg.macro_xs_[material].get_xs( - MgxsType::TOTAL, g, NULL, NULL, NULL, t, a); + score = flux * volume * sigma_t_[material * negroups_ + g]; break; case SCORE_FISSION: - score = flux * volume * - data::mg.macro_xs_[material].get_xs( - MgxsType::FISSION, g, NULL, NULL, NULL, t, a); + score = flux * volume * sigma_f_[material * negroups_ + g]; break; case SCORE_NU_FISSION: - score = flux * volume * - data::mg.macro_xs_[material].get_xs( - MgxsType::NU_FISSION, g, NULL, NULL, NULL, t, a); + score = flux * volume * nu_sigma_f_[material * negroups_ + g]; break; case SCORE_EVENTS: @@ -957,9 +910,8 @@ void FlatSourceDomain::output_to_vtk() const for (int g = 0; g < negroups_; g++) { int64_t source_element = fsr * negroups_ + g; float flux = evaluate_flux_at_point(voxel_positions[i], fsr, g); - float Sigma_f = data::mg.macro_xs_[mat].get_xs( - MgxsType::FISSION, g, nullptr, nullptr, nullptr, 0, 0); - total_fission += Sigma_f * flux; + double sigma_f = sigma_f_[mat * negroups_ + g]; + total_fission += sigma_f * flux; } total_fission = convert_to_big_endian(total_fission); std::fwrite(&total_fission, sizeof(float), 1, plot); @@ -977,10 +929,10 @@ void FlatSourceDomain::apply_external_source_to_source_region( const auto& discrete_energies = discrete->x(); const auto& discrete_probs = discrete->prob(); - for (int e = 0; e < discrete_energies.size(); e++) { - int g = data::mg.get_group_index(discrete_energies[e]); + for (int i = 0; i < discrete_energies.size(); i++) { + int g = data::mg.get_group_index(discrete_energies[i]); external_source_[source_region * negroups_ + g] += - discrete_probs[e] * strength_factor; + discrete_probs[i] * strength_factor; } } @@ -1074,27 +1026,107 @@ void FlatSourceDomain::convert_external_sources() } } // End loop over external sources +// Divide the fixed source term by sigma t (to save time when applying each +// iteration) +#pragma omp parallel for + for (int sr = 0; sr < n_source_regions_; sr++) { + int material = material_[sr]; + for (int g = 0; g < negroups_; g++) { + double sigma_t = sigma_t_[material * negroups_ + g]; + external_source_[sr * negroups_ + g] /= sigma_t; + } + } +} + +void FlatSourceDomain::flux_swap() +{ + scalar_flux_old_.swap(scalar_flux_new_); +} + +void FlatSourceDomain::flatten_xs() +{ // Temperature and angle indices, if using multiple temperature // data sets and/or anisotropic data sets. // TODO: Currently assumes we are only using single temp/single angle data. const int t = 0; const int a = 0; -// Divide the fixed source term by sigma t (to save time when applying each -// iteration) + n_materials_ = data::mg.macro_xs_.size(); + for (auto& m : data::mg.macro_xs_) { + for (int g_out = 0; g_out < negroups_; g_out++) { + if (m.exists_in_model) { + double sigma_t = + m.get_xs(MgxsType::TOTAL, g_out, NULL, NULL, NULL, t, a); + sigma_t_.push_back(sigma_t); + + double nu_Sigma_f = + m.get_xs(MgxsType::NU_FISSION, g_out, NULL, NULL, NULL, t, a); + nu_sigma_f_.push_back(nu_Sigma_f); + + double sigma_f = + m.get_xs(MgxsType::FISSION, g_out, NULL, NULL, NULL, t, a); + sigma_f_.push_back(sigma_f); + + double chi = + m.get_xs(MgxsType::CHI_PROMPT, g_out, &g_out, NULL, NULL, t, a); + chi_.push_back(chi); + + for (int g_in = 0; g_in < negroups_; g_in++) { + double sigma_s = + m.get_xs(MgxsType::NU_SCATTER, g_in, &g_out, NULL, NULL, t, a); + sigma_s_.push_back(sigma_s); + } + } else { + sigma_t_.push_back(0); + nu_sigma_f_.push_back(0); + sigma_f_.push_back(0); + chi_.push_back(0); + for (int g_in = 0; g_in < negroups_; g_in++) { + sigma_s_.push_back(0); + } + } + } + } +} + +void FlatSourceDomain::set_adjoint_sources(const vector& forward_flux) +{ + // Set the external source to 1/forward_flux + // The forward flux is given in terms of total for the forward simulation + // so we must convert it to a "per batch" quantity +#pragma omp parallel for + for (int64_t se = 0; se < n_source_elements_; se++) { + external_source_[se] = 1.0 / forward_flux[se]; + } + + // Divide the fixed source term by sigma t (to save time when applying each + // iteration) #pragma omp parallel for for (int sr = 0; sr < n_source_regions_; sr++) { int material = material_[sr]; - for (int e = 0; e < negroups_; e++) { - double sigma_t = data::mg.macro_xs_[material].get_xs( - MgxsType::TOTAL, e, nullptr, nullptr, nullptr, t, a); - external_source_[sr * negroups_ + e] /= sigma_t; + for (int g = 0; g < negroups_; g++) { + double sigma_t = sigma_t_[material * negroups_ + g]; + external_source_[sr * negroups_ + g] /= sigma_t; } } } -void FlatSourceDomain::flux_swap() + +void FlatSourceDomain::transpose_scattering_matrix() { - scalar_flux_old_.swap(scalar_flux_new_); + // Transpose the inner two dimensions for each material + for (int m = 0; m < n_materials_; ++m) { + int material_offset = m * negroups_ * negroups_; + for (int i = 0; i < negroups_; ++i) { + for (int j = i + 1; j < negroups_; ++j) { + // Calculate indices of the elements to swap + int idx1 = material_offset + i * negroups_ + j; + int idx2 = material_offset + j * negroups_ + i; + + // Swap the elements to transpose the matrix + std::swap(sigma_s_[idx1], sigma_s_[idx2]); + } + } + } } } // namespace openmc diff --git a/src/random_ray/linear_source_domain.cpp b/src/random_ray/linear_source_domain.cpp index 5c3fa91c182..f3f6fa0687d 100644 --- a/src/random_ray/linear_source_domain.cpp +++ b/src/random_ray/linear_source_domain.cpp @@ -56,40 +56,30 @@ void LinearSourceDomain::update_neutron_source(double k_eff) double inverse_k_eff = 1.0 / k_eff; - // Temperature and angle indices, if using multiple temperature - // data sets and/or anisotropic data sets. - // TODO: Currently assumes we are only using single temp/single - // angle data. - const int t = 0; - const int a = 0; - #pragma omp parallel for for (int sr = 0; sr < n_source_regions_; sr++) { int material = material_[sr]; MomentMatrix invM = mom_matrix_[sr].inverse(); - for (int e_out = 0; e_out < negroups_; e_out++) { - double sigma_t = data::mg.macro_xs_[material].get_xs( - MgxsType::TOTAL, e_out, nullptr, nullptr, nullptr, t, a); + for (int g_out = 0; g_out < negroups_; g_out++) { + double sigma_t = sigma_t_[material * negroups_ + g_out]; double scatter_flat = 0.0f; double fission_flat = 0.0f; MomentArray scatter_linear = {0.0, 0.0, 0.0}; MomentArray fission_linear = {0.0, 0.0, 0.0}; - for (int e_in = 0; e_in < negroups_; e_in++) { + for (int g_in = 0; g_in < negroups_; g_in++) { // Handles for the flat and linear components of the flux - double flux_flat = scalar_flux_old_[sr * negroups_ + e_in]; - MomentArray flux_linear = flux_moments_old_[sr * negroups_ + e_in]; + double flux_flat = scalar_flux_old_[sr * negroups_ + g_in]; + MomentArray flux_linear = flux_moments_old_[sr * negroups_ + g_in]; // Handles for cross sections - double sigma_s = data::mg.macro_xs_[material].get_xs( - MgxsType::NU_SCATTER, e_in, &e_out, nullptr, nullptr, t, a); - double nu_sigma_f = data::mg.macro_xs_[material].get_xs( - MgxsType::NU_FISSION, e_in, nullptr, nullptr, nullptr, t, a); - double chi = data::mg.macro_xs_[material].get_xs( - MgxsType::CHI_PROMPT, e_in, &e_out, nullptr, nullptr, t, a); + double sigma_s = + sigma_s_[material * negroups_ * negroups_ + g_out * negroups_ + g_in]; + double nu_sigma_f = nu_sigma_f_[material * negroups_ + g_in]; + double chi = chi_[material * negroups_ + g_out]; // Compute source terms for flat and linear components of the flux scatter_flat += sigma_s * flux_flat; @@ -99,7 +89,7 @@ void LinearSourceDomain::update_neutron_source(double k_eff) } // Compute the flat source term - source_[sr * negroups_ + e_out] = + source_[sr * negroups_ + g_out] = (scatter_flat + fission_flat * inverse_k_eff) / sigma_t; // Compute the linear source terms @@ -107,7 +97,7 @@ void LinearSourceDomain::update_neutron_source(double k_eff) // are not well known, we will leave the source gradients as zero // so as to avoid causing any numerical instability. if (simulation::current_batch > 10) { - source_gradients_[sr * negroups_ + e_out] = + source_gradients_[sr * negroups_ + g_out] = invM * ((scatter_linear + fission_linear * inverse_k_eff) / sigma_t); } } @@ -116,7 +106,7 @@ void LinearSourceDomain::update_neutron_source(double k_eff) if (settings::run_mode == RunMode::FIXED_SOURCE) { // Add external source to flat source term if in fixed source mode #pragma omp parallel for - for (int se = 0; se < n_source_elements_; se++) { + for (int64_t se = 0; se < n_source_elements_; se++) { source_[se] += external_source_[se]; } } @@ -133,9 +123,9 @@ void LinearSourceDomain::normalize_scalar_flux_and_volumes( // Normalize flux to total distance travelled by all rays this iteration #pragma omp parallel for - for (int64_t e = 0; e < scalar_flux_new_.size(); e++) { - scalar_flux_new_[e] *= normalization_factor; - flux_moments_new_[e] *= normalization_factor; + for (int64_t se = 0; se < scalar_flux_new_.size(); se++) { + scalar_flux_new_[se] *= normalization_factor; + flux_moments_new_[se] *= normalization_factor; } // Accumulate cell-wise ray length tallies collected this iteration, then diff --git a/src/random_ray/random_ray.cpp b/src/random_ray/random_ray.cpp index eb64cb7d26e..7a359f35664 100644 --- a/src/random_ray/random_ray.cpp +++ b/src/random_ray/random_ray.cpp @@ -316,17 +316,9 @@ void RandomRay::attenuate_flux_flat_source(double distance, bool is_active) int64_t source_element = source_region * negroups_; int material = this->material(); - // Temperature and angle indices, if using multiple temperature - // data sets and/or anisotropic data sets. - // TODO: Currently assumes we are only using single temp/single - // angle data. - const int t = 0; - const int a = 0; - // MOC incoming flux attenuation + source contribution/attenuation equation for (int g = 0; g < negroups_; g++) { - float sigma_t = data::mg.macro_xs_[material].get_xs( - MgxsType::TOTAL, g, NULL, NULL, NULL, t, a); + float sigma_t = domain_->sigma_t_[material * negroups_ + g]; float tau = sigma_t * distance; float exponential = cjosey_exponential(tau); // exponential = 1 - exp(-tau) float new_delta_psi = @@ -388,13 +380,6 @@ void RandomRay::attenuate_flux_linear_source(double distance, bool is_active) int64_t source_element = source_region * negroups_; int material = this->material(); - // Temperature and angle indices, if using multiple temperature - // data sets and/or anisotropic data sets. - // TODO: Currently assumes we are only using single temp/single - // angle data. - const int t = 0; - const int a = 0; - Position& centroid = domain->centroid_[source_region]; Position midpoint = r() + u() * (distance / 2.0); @@ -422,8 +407,7 @@ void RandomRay::attenuate_flux_linear_source(double distance, bool is_active) for (int g = 0; g < negroups_; g++) { // Compute tau, the optical thickness of the ray segment - float sigma_t = data::mg.macro_xs_[material].get_xs( - MgxsType::TOTAL, g, NULL, NULL, NULL, t, a); + float sigma_t = domain_->sigma_t_[material * negroups_ + g]; float tau = sigma_t * distance; // If tau is very small, set it to zero to avoid numerical issues. diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index 57959e80179..a9180c68e7b 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -23,6 +23,22 @@ namespace openmc { void openmc_run_random_ray() { + ////////////////////////////////////////////////////////// + // Run forward simulation + ////////////////////////////////////////////////////////// + + // Check if adjoint calculation is needed. If it is, we will run the forward + // calculation first and then the adjoint calculation later. + bool adjoint_needed = FlatSourceDomain::adjoint_; + + // Configure the domain for forward simulation + FlatSourceDomain::adjoint_ = false; + + // If we're going to do an adjoint simulation afterwards, report that this is + // the initial forward flux solve. + if (adjoint_needed && mpi::master) + header("FORWARD FLUX SOLVE", 3); + // Initialize OpenMC general data structures openmc_simulation_init(); @@ -30,26 +46,93 @@ void openmc_run_random_ray() if (mpi::master) validate_random_ray_inputs(); - // Initialize Random Ray Simulation Object - RandomRaySimulation sim; + // Declare forward flux so that it can be saved for later adjoint simulation + vector forward_flux; + + { + // Initialize Random Ray Simulation Object + RandomRaySimulation sim; + + // Initialize fixed sources, if present + sim.prepare_fixed_sources(); + + // Begin main simulation timer + simulation::time_total.start(); + + // Execute random ray simulation + sim.simulate(); + + // End main simulation timer + simulation::time_total.stop(); + + // Normalize and save the final forward flux + forward_flux = sim.domain()->scalar_flux_final_; + + double source_normalization_factor = + sim.domain()->compute_fixed_source_normalization_factor() / + (settings::n_batches - settings::n_inactive); + +#pragma omp parallel for + for (uint64_t i = 0; i < forward_flux.size(); i++) { + forward_flux[i] *= source_normalization_factor; + } + + // Finalize OpenMC + openmc_simulation_finalize(); + + // Reduce variables across MPI ranks + sim.reduce_simulation_statistics(); + + // Output all simulation results + sim.output_simulation_results(); + } + + ////////////////////////////////////////////////////////// + // Run adjoint simulation (if enabled) + ////////////////////////////////////////////////////////// + + if (adjoint_needed) { + reset_timers(); + + // Configure the domain for adjoint simulation + FlatSourceDomain::adjoint_ = true; + + if (mpi::master) + header("ADJOINT FLUX SOLVE", 3); + + // Initialize OpenMC general data structures + openmc_simulation_init(); - // Begin main simulation timer - simulation::time_total.start(); + // Initialize Random Ray Simulation Object + RandomRaySimulation adjoint_sim; - // Execute random ray simulation - sim.simulate(); + // Initialize adjoint fixed sources, if present + adjoint_sim.prepare_fixed_sources_adjoint(forward_flux); - // End main simulation timer - openmc::simulation::time_total.stop(); + // Transpose scattering matrix + adjoint_sim.domain()->transpose_scattering_matrix(); - // Finalize OpenMC - openmc_simulation_finalize(); + // Swap nu_sigma_f and chi + adjoint_sim.domain()->nu_sigma_f_.swap(adjoint_sim.domain()->chi_); - // Reduce variables across MPI ranks - sim.reduce_simulation_statistics(); + // Begin main simulation timer + simulation::time_total.start(); - // Output all simulation results - sim.output_simulation_results(); + // Execute random ray simulation + adjoint_sim.simulate(); + + // End main simulation timer + simulation::time_total.stop(); + + // Finalize OpenMC + openmc_simulation_finalize(); + + // Reduce variables across MPI ranks + adjoint_sim.reduce_simulation_statistics(); + + // Output all simulation results + adjoint_sim.output_simulation_results(); + } } // Enforces restrictions on inputs in random ray mode. While there are @@ -254,16 +337,31 @@ RandomRaySimulation::RandomRaySimulation() default: fatal_error("Unknown random ray source shape"); } + + // Convert OpenMC native MGXS into a more efficient format + // internal to the random ray solver + domain_->flatten_xs(); } -void RandomRaySimulation::simulate() +void RandomRaySimulation::prepare_fixed_sources() { if (settings::run_mode == RunMode::FIXED_SOURCE) { // Transfer external source user inputs onto random ray source regions domain_->convert_external_sources(); domain_->count_external_source_regions(); } +} + +void RandomRaySimulation::prepare_fixed_sources_adjoint( + vector& forward_flux) +{ + if (settings::run_mode == RunMode::FIXED_SOURCE) { + domain_->set_adjoint_sources(forward_flux); + } +} +void RandomRaySimulation::simulate() +{ // Random ray power iteration loop while (simulation::current_batch < settings::n_batches) { @@ -314,18 +412,20 @@ void RandomRaySimulation::simulate() } // Execute all tallying tasks, if this is an active batch - if (simulation::current_batch > settings::n_inactive && mpi::master) { - - // Generate mapping between source regions and tallies - if (!domain_->mapped_all_tallies_) { - domain_->convert_source_regions_to_tallies(); - } - - // Use above mapping to contribute FSR flux data to appropriate tallies - domain_->random_ray_tally(); + if (simulation::current_batch > settings::n_inactive) { // Add this iteration's scalar flux estimate to final accumulated estimate domain_->accumulate_iteration_flux(); + + if (mpi::master) { + // Generate mapping between source regions and tallies + if (!domain_->mapped_all_tallies_) { + domain_->convert_source_regions_to_tallies(); + } + + // Use above mapping to contribute FSR flux data to appropriate tallies + domain_->random_ray_tally(); + } } // Set phi_old = phi_new @@ -448,6 +548,9 @@ void RandomRaySimulation::print_results_random_ray( } fmt::print(" Volume Estimator Type = {}\n", estimator); + std::string adjoint_true = (FlatSourceDomain::adjoint_) ? "ON" : "OFF"; + fmt::print(" Adjoint Flux Mode = {}\n", adjoint_true); + header("Timing Statistics", 4); show_time("Total time for initialization", time_initialize.elapsed()); show_time("Reading cross sections", time_read_xs.elapsed(), 1); diff --git a/src/settings.cpp b/src/settings.cpp index 3a905bdf961..d52177ae88f 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -301,6 +301,10 @@ void get_run_parameters(pugi::xml_node node_base) FlatSourceDomain::volume_normalized_flux_tallies_ = get_node_value_bool(random_ray_node, "volume_normalized_flux_tallies"); } + if (check_for_node(random_ray_node, "adjoint")) { + FlatSourceDomain::adjoint_ = + get_node_value_bool(random_ray_node, "adjoint"); + } } } diff --git a/tests/regression_tests/random_ray_adjoint_fixed_source/__init__.py b/tests/regression_tests/random_ray_adjoint_fixed_source/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regression_tests/random_ray_adjoint_fixed_source/inputs_true.dat b/tests/regression_tests/random_ray_adjoint_fixed_source/inputs_true.dat new file mode 100644 index 00000000000..686987512a0 --- /dev/null +++ b/tests/regression_tests/random_ray_adjoint_fixed_source/inputs_true.dat @@ -0,0 +1,245 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 10 + 5 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + True + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_adjoint_fixed_source/results_true.dat b/tests/regression_tests/random_ray_adjoint_fixed_source/results_true.dat new file mode 100644 index 00000000000..a216fa9fcc3 --- /dev/null +++ b/tests/regression_tests/random_ray_adjoint_fixed_source/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +-7.235364E+03 +3.367109E+09 +tally 2: +4.818311E+05 +6.269371E+10 +tally 3: +1.515641E+06 +4.598791E+11 diff --git a/tests/regression_tests/random_ray_adjoint_fixed_source/test.py b/tests/regression_tests/random_ray_adjoint_fixed_source/test.py new file mode 100644 index 00000000000..0295e36e9ac --- /dev/null +++ b/tests/regression_tests/random_ray_adjoint_fixed_source/test.py @@ -0,0 +1,20 @@ +import os + +from openmc.examples import random_ray_three_region_cube + +from tests.testing_harness import TolerantPyAPITestHarness + + +class MGXSTestHarness(TolerantPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + + +def test_random_ray_adjoint_fixed_source(): + model = random_ray_three_region_cube() + model.settings.random_ray['adjoint'] = True + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/regression_tests/random_ray_adjoint_k_eff/__init__.py b/tests/regression_tests/random_ray_adjoint_k_eff/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regression_tests/random_ray_adjoint_k_eff/inputs_true.dat b/tests/regression_tests/random_ray_adjoint_k_eff/inputs_true.dat new file mode 100644 index 00000000000..725702a4912 --- /dev/null +++ b/tests/regression_tests/random_ray_adjoint_k_eff/inputs_true.dat @@ -0,0 +1,110 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.126 0.126 + 10 10 + -0.63 -0.63 + +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 + + + 1.26 1.26 + 2 2 + -1.26 -1.26 + +2 2 +2 5 + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + multi-group + + 100.0 + 20.0 + + + -1.26 -1.26 -1 1.26 1.26 1 + + + True + True + + + + + 2 2 + -1.26 -1.26 + 1.26 1.26 + + + 1 + + + 1e-05 0.0635 10.0 100.0 1000.0 500000.0 1000000.0 20000000.0 + + + 1 2 + flux fission nu-fission + analog + + + diff --git a/tests/regression_tests/random_ray_adjoint_k_eff/results_true.dat b/tests/regression_tests/random_ray_adjoint_k_eff/results_true.dat new file mode 100644 index 00000000000..1690d46e966 --- /dev/null +++ b/tests/regression_tests/random_ray_adjoint_k_eff/results_true.dat @@ -0,0 +1,171 @@ +k-combined: +1.006640E+00 1.812967E-03 +tally 1: +6.684129E+00 +8.939821E+00 +2.685967E+00 +1.443592E+00 +0.000000E+00 +0.000000E+00 +6.358774E+00 +8.091444E+00 +9.687217E-01 +1.878029E-01 +0.000000E+00 +0.000000E+00 +5.963160E+00 +7.117108E+00 +1.932332E-01 +7.473914E-03 +0.000000E+00 +0.000000E+00 +5.137593E+00 +5.283310E+00 +1.714616E-01 +5.884834E-03 +1.086218E-06 +2.361752E-13 +4.857253E+00 +4.719856E+00 +5.689580E-02 +6.476286E-04 +2.989356E-03 +1.787808E-06 +4.830516E+00 +4.666801E+00 +7.203015E-03 +1.037676E-05 +3.620020E+00 +2.620927E+00 +5.161382E+00 +5.328124E+00 +6.786255E-02 +9.210763E-04 +5.531943E+00 +6.120553E+00 +5.414034E+00 +5.864661E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.632338E+00 +6.347626E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.682608E+00 +6.462382E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.310716E+00 +5.645180E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.945409E+00 +4.893171E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.842688E+00 +4.690352E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +5.117198E+00 +5.237280E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +6.938711E+00 +9.633345E+00 +2.835258E+00 +1.608212E+00 +0.000000E+00 +0.000000E+00 +6.549505E+00 +8.584036E+00 +1.015138E+00 +2.061993E-01 +0.000000E+00 +0.000000E+00 +6.050651E+00 +7.327711E+00 +1.992816E-01 +7.948424E-03 +0.000000E+00 +0.000000E+00 +5.113981E+00 +5.234801E+00 +1.732323E-01 +6.006619E-03 +1.097435E-06 +2.410627E-13 +4.837033E+00 +4.680541E+00 +5.760042E-02 +6.637112E-04 +3.026377E-03 +1.832205E-06 +4.827049E+00 +4.660105E+00 +7.319913E-03 +1.071647E-05 +3.678770E+00 +2.706730E+00 +5.175337E+00 +5.356957E+00 +6.923046E-02 +9.586177E-04 +5.643451E+00 +6.370016E+00 +6.693323E+00 +8.964322E+00 +2.753307E+00 +1.516683E+00 +0.000000E+00 +0.000000E+00 +6.358384E+00 +8.090233E+00 +9.912008E-01 +1.965868E-01 +0.000000E+00 +0.000000E+00 +5.957484E+00 +7.103246E+00 +1.974033E-01 +7.798286E-03 +0.000000E+00 +0.000000E+00 +5.130744E+00 +5.268844E+00 +1.749233E-01 +6.123348E-03 +1.108148E-06 +2.457474E-13 +4.857340E+00 +4.720019E+00 +5.816659E-02 +6.768049E-04 +3.056125E-03 +1.868351E-06 +4.830629E+00 +4.667018E+00 +7.366289E-03 +1.085264E-05 +3.702077E+00 +2.741125E+00 +5.164864E+00 +5.335279E+00 +6.947917E-02 +9.655086E-04 +5.663725E+00 +6.415806E+00 diff --git a/tests/regression_tests/random_ray_adjoint_k_eff/test.py b/tests/regression_tests/random_ray_adjoint_k_eff/test.py new file mode 100644 index 00000000000..44cf1182ae6 --- /dev/null +++ b/tests/regression_tests/random_ray_adjoint_k_eff/test.py @@ -0,0 +1,20 @@ +import os + +from openmc.examples import random_ray_lattice + +from tests.testing_harness import TolerantPyAPITestHarness + + +class MGXSTestHarness(TolerantPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + + +def test_random_ray_basic(): + model = random_ray_lattice() + model.settings.random_ray['adjoint'] = True + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() From ae37d6c0da33b6941f726e506db78736fd1d51d1 Mon Sep 17 00:00:00 2001 From: Zoe Prieto <101403129+zoeprieto@users.noreply.github.com> Date: Fri, 22 Nov 2024 12:16:49 -0300 Subject: [PATCH 098/184] Statistical weights in IndependentSource (#3195) Co-authored-by: Paul Wilson Co-authored-by: Paul Romano --- docs/source/usersguide/settings.rst | 20 ++++++ include/openmc/settings.h | 47 +++++++------- openmc/settings.py | 25 ++++++++ src/finalize.cpp | 1 + src/settings.cpp | 7 +++ src/source.cpp | 21 +++++-- src/tallies/tally.cpp | 3 +- .../test_uniform_source_sampling.py | 63 +++++++++++++++++++ 8 files changed, 157 insertions(+), 30 deletions(-) create mode 100644 tests/unit_tests/test_uniform_source_sampling.py diff --git a/docs/source/usersguide/settings.rst b/docs/source/usersguide/settings.rst index 349aa34d07a..ca11d648748 100644 --- a/docs/source/usersguide/settings.rst +++ b/docs/source/usersguide/settings.rst @@ -183,6 +183,7 @@ source distributions and has four main attributes that one can set: :attr:`IndependentSource.energy`, which defines the energy distribution, and :attr:`IndependentSource.time`, which defines the time distribution. + The spatial distribution can be set equal to a sub-class of :class:`openmc.stats.Spatial`; common choices are :class:`openmc.stats.Point` or :class:`openmc.stats.Box`. To independently specify distributions in the @@ -225,6 +226,7 @@ distribution. This could be a probability mass function (:class:`openmc.stats.Tabular`). By default, if no time distribution is specified, particles are started at :math:`t=0`. + As an example, to create an isotropic, 10 MeV monoenergetic source uniformly distributed over a cube centered at the origin with an edge length of 10 cm, and emitting a pulse of particles from 0 to 10 µs, one @@ -252,6 +254,24 @@ sampled 70% of the time and another that should be sampled 30% of the time:: settings.source = [src1, src2] +When the relative strengths are several orders of magnitude different, it may +happen that not enough statistics are obtained from the lower strength source. +This can be improved by sampling among the sources with equal probability, +applying the source strength as a weight on the sampled source particles. The +:attr:`Settings.uniform_source_sampling` attribute can be used to enable this +option:: + + src1 = openmc.IndependentSource() + src1.strength = 100.0 + ... + + src2 = openmc.IndependentSource() + src2.strength = 1.0 + ... + + settings.source = [src1, src2] + settings.uniform_source_sampling = True + Finally, the :attr:`IndependentSource.particle` attribute can be used to indicate the source should be composed of particles other than neutrons. For example, the following would generate a photon source:: diff --git a/include/openmc/settings.h b/include/openmc/settings.h index 1e44c08801d..9a4ce56ec8f 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -44,29 +44,30 @@ extern "C" bool entropy_on; //!< calculate Shannon entropy? extern "C" bool event_based; //!< use event-based mode (instead of history-based) extern bool legendre_to_tabular; //!< convert Legendre distributions to tabular? -extern bool material_cell_offsets; //!< create material cells offsets? -extern "C" bool output_summary; //!< write summary.h5? -extern bool output_tallies; //!< write tallies.out? -extern bool particle_restart_run; //!< particle restart run? -extern "C" bool photon_transport; //!< photon transport turned on? -extern "C" bool reduce_tallies; //!< reduce tallies at end of batch? -extern bool res_scat_on; //!< use resonance upscattering method? -extern "C" bool restart_run; //!< restart run? -extern "C" bool run_CE; //!< run with continuous-energy data? -extern bool source_latest; //!< write latest source at each batch? -extern bool source_separate; //!< write source to separate file? -extern bool source_write; //!< write source in HDF5 files? -extern bool source_mcpl_write; //!< write source in mcpl files? -extern bool surf_source_write; //!< write surface source file? -extern bool surf_mcpl_write; //!< write surface mcpl file? -extern bool surf_source_read; //!< read surface source file? -extern bool survival_biasing; //!< use survival biasing? -extern bool temperature_multipole; //!< use multipole data? -extern "C" bool trigger_on; //!< tally triggers enabled? -extern bool trigger_predict; //!< predict batches for triggers? -extern bool ufs_on; //!< uniform fission site method on? -extern bool urr_ptables_on; //!< use unresolved resonance prob. tables? -extern "C" bool weight_windows_on; //!< are weight windows are enabled? +extern bool material_cell_offsets; //!< create material cells offsets? +extern "C" bool output_summary; //!< write summary.h5? +extern bool output_tallies; //!< write tallies.out? +extern bool particle_restart_run; //!< particle restart run? +extern "C" bool photon_transport; //!< photon transport turned on? +extern "C" bool reduce_tallies; //!< reduce tallies at end of batch? +extern bool res_scat_on; //!< use resonance upscattering method? +extern "C" bool restart_run; //!< restart run? +extern "C" bool run_CE; //!< run with continuous-energy data? +extern bool source_latest; //!< write latest source at each batch? +extern bool source_separate; //!< write source to separate file? +extern bool source_write; //!< write source in HDF5 files? +extern bool source_mcpl_write; //!< write source in mcpl files? +extern bool surf_source_write; //!< write surface source file? +extern bool surf_mcpl_write; //!< write surface mcpl file? +extern bool surf_source_read; //!< read surface source file? +extern bool survival_biasing; //!< use survival biasing? +extern bool temperature_multipole; //!< use multipole data? +extern "C" bool trigger_on; //!< tally triggers enabled? +extern bool trigger_predict; //!< predict batches for triggers? +extern bool uniform_source_sampling; //!< sample sources uniformly? +extern bool ufs_on; //!< uniform fission site method on? +extern bool urr_ptables_on; //!< use unresolved resonance prob. tables? +extern "C" bool weight_windows_on; //!< are weight windows are enabled? extern bool weight_window_checkpoint_surface; //!< enable weight window check //!< upon surface crossing? extern bool weight_window_checkpoint_collision; //!< enable weight window check diff --git a/openmc/settings.py b/openmc/settings.py index a350de72ecc..77598b204fc 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -266,6 +266,9 @@ class Settings: Maximum number of batches simulated. If this is set, the number of batches specified via ``batches`` is interpreted as the minimum number of batches + uniform_source_sampling : bool + Whether to sampling among multiple sources uniformly, applying their + strengths as weights to sampled particles. ufs_mesh : openmc.RegularMesh Mesh to be used for redistributing source sites via the uniform fission site (UFS) method. @@ -328,6 +331,7 @@ def __init__(self, **kwargs): self._photon_transport = None self._plot_seed = None self._ptables = None + self._uniform_source_sampling = None self._seed = None self._survival_biasing = None @@ -575,6 +579,15 @@ def photon_transport(self, photon_transport: bool): cv.check_type('photon transport', photon_transport, bool) self._photon_transport = photon_transport + @property + def uniform_source_sampling(self) -> bool: + return self._uniform_source_sampling + + @uniform_source_sampling.setter + def uniform_source_sampling(self, uniform_source_sampling: bool): + cv.check_type('strength as weights', uniform_source_sampling, bool) + self._uniform_source_sampling = uniform_source_sampling + @property def plot_seed(self): return self._plot_seed @@ -1221,6 +1234,11 @@ def _create_statepoint_subelement(self, root): subelement.text = ' '.join( str(x) for x in self._statepoint['batches']) + def _create_uniform_source_sampling_subelement(self, root): + if self._uniform_source_sampling is not None: + element = ET.SubElement(root, "uniform_source_sampling") + element.text = str(self._uniform_source_sampling).lower() + def _create_sourcepoint_subelement(self, root): if self._sourcepoint: element = ET.SubElement(root, "source_point") @@ -1702,6 +1720,11 @@ def _photon_transport_from_xml_element(self, root): if text is not None: self.photon_transport = text in ('true', '1') + def _uniform_source_sampling_from_xml_element(self, root): + text = get_text(root, 'uniform_source_sampling') + if text is not None: + self.uniform_source_sampling = text in ('true', '1') + def _plot_seed_from_xml_element(self, root): text = get_text(root, 'plot_seed') if text is not None: @@ -1957,6 +1980,7 @@ def to_xml_element(self, mesh_memo=None): self._create_energy_mode_subelement(element) self._create_max_order_subelement(element) self._create_photon_transport_subelement(element) + self._create_uniform_source_sampling_subelement(element) self._create_plot_seed_subelement(element) self._create_ptables_subelement(element) self._create_seed_subelement(element) @@ -2063,6 +2087,7 @@ def from_xml_element(cls, elem, meshes=None): settings._energy_mode_from_xml_element(elem) settings._max_order_from_xml_element(elem) settings._photon_transport_from_xml_element(elem) + settings._uniform_source_sampling_from_xml_element(elem) settings._plot_seed_from_xml_element(elem) settings._ptables_from_xml_element(elem) settings._seed_from_xml_element(elem) diff --git a/src/finalize.cpp b/src/finalize.cpp index 08c2fced308..981ec5cbaf1 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -133,6 +133,7 @@ int openmc_finalize() settings::trigger_on = false; settings::trigger_predict = false; settings::trigger_batch_interval = 1; + settings::uniform_source_sampling = false; settings::ufs_on = false; settings::urr_ptables_on = true; settings::verbosity = 7; diff --git a/src/settings.cpp b/src/settings.cpp index d52177ae88f..5e11949bb6d 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -72,6 +72,7 @@ bool survival_biasing {false}; bool temperature_multipole {false}; bool trigger_on {false}; bool trigger_predict {false}; +bool uniform_source_sampling {false}; bool ufs_on {false}; bool urr_ptables_on {true}; bool weight_windows_on {false}; @@ -786,6 +787,12 @@ void read_settings_xml(pugi::xml_node root) sourcepoint_batch = statepoint_batch; } + // Check is the user specified to convert strength to statistical weight + if (check_for_node(root, "uniform_source_sampling")) { + uniform_source_sampling = + get_node_value_bool(root, "uniform_source_sampling"); + } + // Check if the user has specified to write surface source if (check_for_node(root, "surf_source_write")) { surf_source_write = true; diff --git a/src/source.cpp b/src/source.cpp index 15fe8433ba5..9d3cae6cf7f 100644 --- a/src/source.cpp +++ b/src/source.cpp @@ -616,18 +616,27 @@ SourceSite sample_external_source(uint64_t* seed) // Sample from among multiple source distributions int i = 0; if (model::external_sources.size() > 1) { - double xi = prn(seed) * total_strength; - double c = 0.0; - for (; i < model::external_sources.size(); ++i) { - c += model::external_sources[i]->strength(); - if (xi < c) - break; + if (settings::uniform_source_sampling) { + i = prn(seed) * model::external_sources.size(); + } else { + double xi = prn(seed) * total_strength; + double c = 0.0; + for (; i < model::external_sources.size(); ++i) { + c += model::external_sources[i]->strength(); + if (xi < c) + break; + } } } // Sample source site from i-th source distribution SourceSite site {model::external_sources[i]->sample_with_constraints(seed)}; + // Set particle creation weight + if (settings::uniform_source_sampling) { + site.wgt *= model::external_sources[i]->strength(); + } + // If running in MG, convert site.E to group if (!settings::run_CE) { site.E = lower_bound_index(data::mg.rev_energy_bins_.begin(), diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index ba899611c3b..4f33abf6beb 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -751,7 +751,8 @@ void Tally::accumulate() if (mpi::master || !settings::reduce_tallies) { // Calculate total source strength for normalization double total_source = 0.0; - if (settings::run_mode == RunMode::FIXED_SOURCE) { + if (settings::run_mode == RunMode::FIXED_SOURCE && + !settings::uniform_source_sampling) { for (const auto& s : model::external_sources) { total_source += s->strength(); } diff --git a/tests/unit_tests/test_uniform_source_sampling.py b/tests/unit_tests/test_uniform_source_sampling.py new file mode 100644 index 00000000000..ece8afe8946 --- /dev/null +++ b/tests/unit_tests/test_uniform_source_sampling.py @@ -0,0 +1,63 @@ +import openmc +import pytest + + +@pytest.fixture +def sphere_model(): + mat = openmc.Material() + mat.add_nuclide('Li6', 1.0) + mat.set_density('g/cm3', 1.0) + sphere = openmc.Sphere(r=1.0, boundary_type='vacuum') + cell = openmc.Cell(region=-sphere, fill=mat) + model = openmc.Model() + model.geometry = openmc.Geometry([cell]) + + model.settings.particles = 100 + model.settings.batches = 1 + model.settings.source = openmc.IndependentSource( + energy=openmc.stats.delta_function(1.0e3), + strength=100.0 + ) + model.settings.run_mode = "fixed source" + model.settings.surf_source_write = { + "max_particles": 100, + } + + tally = openmc.Tally() + tally.scores = ['flux'] + model.tallies = [tally] + return model + + +def test_source_weight(run_in_tmpdir, sphere_model): + # Run OpenMC without uniform source sampling and check that banked particles + # have weight 1 + sphere_model.settings.uniform_source_sampling = False + sphere_model.run() + particles = openmc.ParticleList.from_hdf5('surface_source.h5') + assert set(p.wgt for p in particles) == {1.0} + + # Run with uniform source sampling and check that banked particles have + # weight == strength + sphere_model.settings.uniform_source_sampling = True + sphere_model.run() + particles = openmc.ParticleList.from_hdf5('surface_source.h5') + strength = sphere_model.settings.source[0].strength + assert set(p.wgt for p in particles) == {strength} + + +def test_tally_mean(run_in_tmpdir, sphere_model): + # Run without uniform source sampling + sphere_model.settings.uniform_source_sampling = False + sp_file = sphere_model.run() + with openmc.StatePoint(sp_file) as sp: + reference_mean = sp.tallies[sphere_model.tallies[0].id].mean + + # Run with uniform source sampling + sphere_model.settings.uniform_source_sampling = True + sp_file = sphere_model.run() + with openmc.StatePoint(sp_file) as sp: + mean = sp.tallies[sphere_model.tallies[0].id].mean + + # Check that tally means match + assert mean == pytest.approx(reference_mean) From dd01c40ae1f320dc74a5ec2674b92b09b9d03361 Mon Sep 17 00:00:00 2001 From: Kevin Sawatzky <66632997+nuclearkevin@users.noreply.github.com> Date: Sat, 23 Nov 2024 08:09:47 -0600 Subject: [PATCH 099/184] Enable adaptive mesh support on libMesh tallies (#3185) Co-authored-by: Patrick Shriwise --- include/openmc/mesh.h | 11 +++++- src/mesh.cpp | 86 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 82 insertions(+), 15 deletions(-) diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index f0ad5724706..5edb03003f9 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -948,6 +948,7 @@ class LibMesh : public UnstructuredMesh { private: void initialize() override; void set_mesh_pointer_from_filename(const std::string& filename); + void build_eqn_sys(); // Methods @@ -966,7 +967,8 @@ class LibMesh : public UnstructuredMesh { vector> pl_; //!< per-thread point locators unique_ptr - equation_systems_; //!< pointer to the equation systems of the mesh + equation_systems_; //!< pointer to the libMesh EquationSystems + //!< instance std::string eq_system_name_; //!< name of the equation system holding OpenMC results std::unordered_map @@ -975,6 +977,13 @@ class LibMesh : public UnstructuredMesh { libMesh::BoundingBox bbox_; //!< bounding box of the mesh libMesh::dof_id_type first_element_id_; //!< id of the first element in the mesh + + const bool adaptive_; //!< whether this mesh has adaptivity enabled or not + std::vector + bin_to_elem_map_; //!< mapping bin indices to dof indices for active + //!< elements + std::vector elem_to_bin_map_; //!< mapping dof indices to bin indices for + //!< active elements }; #endif diff --git a/src/mesh.cpp b/src/mesh.cpp index b90fead0339..0b051e63f62 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -2922,7 +2922,7 @@ void MOABMesh::write(const std::string& base_filename) const const std::string LibMesh::mesh_lib_type = "libmesh"; -LibMesh::LibMesh(pugi::xml_node node) : UnstructuredMesh(node) +LibMesh::LibMesh(pugi::xml_node node) : UnstructuredMesh(node), adaptive_(false) { // filename_ and length_multiplier_ will already be set by the // UnstructuredMesh constructor @@ -2933,6 +2933,7 @@ LibMesh::LibMesh(pugi::xml_node node) : UnstructuredMesh(node) // create the mesh from a pointer to a libMesh Mesh LibMesh::LibMesh(libMesh::MeshBase& input_mesh, double length_multiplier) + : adaptive_(input_mesh.n_active_elem() != input_mesh.n_elem()) { m_ = &input_mesh; set_length_multiplier(length_multiplier); @@ -2941,6 +2942,7 @@ LibMesh::LibMesh(libMesh::MeshBase& input_mesh, double length_multiplier) // create the mesh from an input file LibMesh::LibMesh(const std::string& filename, double length_multiplier) + : adaptive_(false) { set_mesh_pointer_from_filename(filename); set_length_multiplier(length_multiplier); @@ -2955,6 +2957,15 @@ void LibMesh::set_mesh_pointer_from_filename(const std::string& filename) m_->read(filename_); } +// build a libMesh equation system for storing values +void LibMesh::build_eqn_sys() +{ + eq_system_name_ = fmt::format("mesh_{}_system", id_); + equation_systems_ = make_unique(*m_); + libMesh::ExplicitSystem& eq_sys = + equation_systems_->add_system(eq_system_name_); +} + // intialize from mesh file void LibMesh::initialize() { @@ -2982,13 +2993,6 @@ void LibMesh::initialize() filename_)); } - // create an equation system for storing values - eq_system_name_ = fmt::format("mesh_{}_system", id_); - - equation_systems_ = make_unique(*m_); - libMesh::ExplicitSystem& eq_sys = - equation_systems_->add_system(eq_system_name_); - for (int i = 0; i < num_threads(); i++) { pl_.emplace_back(m_->sub_point_locator()); pl_.back()->set_contains_point_tol(FP_COINCIDENT); @@ -2999,6 +3003,21 @@ void LibMesh::initialize() auto first_elem = *m_->elements_begin(); first_element_id_ = first_elem->id(); + // if the mesh is adaptive elements aren't guaranteed by libMesh to be + // contiguous in ID space, so we need to map from bin indices (defined over + // active elements) to global dof ids + if (adaptive_) { + bin_to_elem_map_.reserve(m_->n_active_local_elem()); + elem_to_bin_map_.resize(m_->n_local_elem(), -1); + for (auto it = m_->active_local_elements_begin(); + it != m_->active_local_elements_end(); it++) { + auto elem = *it; + + bin_to_elem_map_.push_back(elem->id()); + elem_to_bin_map_[elem->id()] = bin_to_elem_map_.size() - 1; + } + } + // bounding box for the mesh for quick rejection checks bbox_ = libMesh::MeshTools::create_bounding_box(*m_); libMesh::Point ll = bbox_.min(); @@ -3056,7 +3075,7 @@ std::string LibMesh::library() const int LibMesh::n_bins() const { - return m_->n_elem(); + return m_->n_active_elem(); } int LibMesh::n_surface_bins() const @@ -3079,6 +3098,18 @@ int LibMesh::n_surface_bins() const void LibMesh::add_score(const std::string& var_name) { + if (adaptive_) { + warning(fmt::format( + "Exodus output cannot be provided as unstructured mesh {} is adaptive.", + this->id_)); + + return; + } + + if (!equation_systems_) { + build_eqn_sys(); + } + // check if this is a new variable std::string value_name = var_name + "_mean"; if (!variable_map_.count(value_name)) { @@ -3100,14 +3131,28 @@ void LibMesh::add_score(const std::string& var_name) void LibMesh::remove_scores() { - auto& eqn_sys = equation_systems_->get_system(eq_system_name_); - eqn_sys.clear(); - variable_map_.clear(); + if (equation_systems_) { + auto& eqn_sys = equation_systems_->get_system(eq_system_name_); + eqn_sys.clear(); + variable_map_.clear(); + } } void LibMesh::set_score_data(const std::string& var_name, const vector& values, const vector& std_dev) { + if (adaptive_) { + warning(fmt::format( + "Exodus output cannot be provided as unstructured mesh {} is adaptive.", + this->id_)); + + return; + } + + if (!equation_systems_) { + build_eqn_sys(); + } + auto& eqn_sys = equation_systems_->get_system(eq_system_name_); if (!eqn_sys.is_initialized()) { @@ -3125,6 +3170,10 @@ void LibMesh::set_score_data(const std::string& var_name, for (auto it = m_->local_elements_begin(); it != m_->local_elements_end(); it++) { + if (!(*it)->active()) { + continue; + } + auto bin = get_bin_from_element(*it); // set value @@ -3143,6 +3192,14 @@ void LibMesh::set_score_data(const std::string& var_name, void LibMesh::write(const std::string& filename) const { + if (adaptive_) { + warning(fmt::format( + "Exodus output cannot be provided as unstructured mesh {} is adaptive.", + this->id_)); + + return; + } + write_message(fmt::format( "Writing file: {}.e for unstructured mesh {}", filename, this->id_)); libMesh::ExodusII_IO exo(*m_); @@ -3176,7 +3233,8 @@ int LibMesh::get_bin(Position r) const int LibMesh::get_bin_from_element(const libMesh::Elem* elem) const { - int bin = elem->id() - first_element_id_; + int bin = + adaptive_ ? elem_to_bin_map_[elem->id()] : elem->id() - first_element_id_; if (bin >= n_bins() || bin < 0) { fatal_error(fmt::format("Invalid bin: {}", bin)); } @@ -3191,7 +3249,7 @@ std::pair, vector> LibMesh::plot( const libMesh::Elem& LibMesh::get_element_from_bin(int bin) const { - return m_->elem_ref(bin); + return adaptive_ ? m_->elem_ref(bin_to_elem_map_.at(bin)) : m_->elem_ref(bin); } double LibMesh::volume(int bin) const From 2d988a69a10dec9e2964782d20a08927574d763a Mon Sep 17 00:00:00 2001 From: Ebny Walid Ahammed <69362074+magnoxemo@users.noreply.github.com> Date: Sat, 23 Nov 2024 13:10:39 -0600 Subject: [PATCH 100/184] External sources alias sampler (#3201) Co-authored-by: Paul Romano --- include/openmc/source.h | 3 +++ openmc/model/model.py | 4 ++-- src/settings.cpp | 7 +++++++ src/source.cpp | 18 +++++------------- tests/regression_tests/source/results_true.dat | 2 +- .../unit_tests/test_uniform_source_sampling.py | 12 ++++++++++++ 6 files changed, 30 insertions(+), 16 deletions(-) diff --git a/include/openmc/source.h b/include/openmc/source.h index 5cc0356e3e9..6733eaeffca 100644 --- a/include/openmc/source.h +++ b/include/openmc/source.h @@ -36,6 +36,9 @@ namespace model { extern vector> external_sources; +// Probability distribution for selecting external sources +extern DiscreteIndex external_sources_probability; + } // namespace model //============================================================================== diff --git a/openmc/model/model.py b/openmc/model/model.py index ffad8f503c7..49cb0a83ef5 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -885,7 +885,7 @@ def sample_external_source( n_samples: int = 1000, prn_seed: int | None = None, **init_kwargs - ) -> list[openmc.SourceParticle]: + ) -> openmc.ParticleList: """Sample external source and return source particles. .. versionadded:: 0.15.1 @@ -902,7 +902,7 @@ def sample_external_source( Returns ------- - list of openmc.SourceParticle + openmc.ParticleList List of samples source particles """ import openmc.lib diff --git a/src/settings.cpp b/src/settings.cpp index 5e11949bb6d..61eda79967a 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -563,6 +563,13 @@ void read_settings_xml(pugi::xml_node root) model::external_sources.push_back(make_unique(path)); } + // Build probability mass function for sampling external sources + vector source_strengths; + for (auto& s : model::external_sources) { + source_strengths.push_back(s->strength()); + } + model::external_sources_probability.assign(source_strengths); + // If no source specified, default to isotropic point source at origin with // Watt spectrum. No default source is needed in random ray mode. if (model::external_sources.empty() && diff --git a/src/source.cpp b/src/source.cpp index 9d3cae6cf7f..2116809f2dd 100644 --- a/src/source.cpp +++ b/src/source.cpp @@ -44,7 +44,10 @@ namespace openmc { namespace model { vector> external_sources; -} + +DiscreteIndex external_sources_probability; + +} // namespace model //============================================================================== // Source implementation @@ -608,24 +611,13 @@ void initialize_source() SourceSite sample_external_source(uint64_t* seed) { - // Determine total source strength - double total_strength = 0.0; - for (auto& s : model::external_sources) - total_strength += s->strength(); - // Sample from among multiple source distributions int i = 0; if (model::external_sources.size() > 1) { if (settings::uniform_source_sampling) { i = prn(seed) * model::external_sources.size(); } else { - double xi = prn(seed) * total_strength; - double c = 0.0; - for (; i < model::external_sources.size(); ++i) { - c += model::external_sources[i]->strength(); - if (xi < c) - break; - } + i = model::external_sources_probability.sample(seed); } } diff --git a/tests/regression_tests/source/results_true.dat b/tests/regression_tests/source/results_true.dat index 94d9ced1551..673d27c8af8 100644 --- a/tests/regression_tests/source/results_true.dat +++ b/tests/regression_tests/source/results_true.dat @@ -1,2 +1,2 @@ k-combined: -3.054101E-01 1.167865E-03 +2.959436E-01 2.782384E-03 diff --git a/tests/unit_tests/test_uniform_source_sampling.py b/tests/unit_tests/test_uniform_source_sampling.py index ece8afe8946..7f805e37d27 100644 --- a/tests/unit_tests/test_uniform_source_sampling.py +++ b/tests/unit_tests/test_uniform_source_sampling.py @@ -61,3 +61,15 @@ def test_tally_mean(run_in_tmpdir, sphere_model): # Check that tally means match assert mean == pytest.approx(reference_mean) + + +def test_multiple_sources(sphere_model): + low_strength_src = openmc.IndependentSource( + energy=openmc.stats.delta_function(1.0e6), strength=1e-7) + sphere_model.settings.source.append(low_strength_src) + sphere_model.settings.uniform_source_sampling = True + + # Sample particles from source and make sure 1 MeV shows up despite + # negligible strength + particles = sphere_model.sample_external_source(100) + assert {p.E for p in particles} == {1.0e3, 1.0e6} From a9fe2a05c114796ceb7a640e9d68c823dfa4da16 Mon Sep 17 00:00:00 2001 From: Kevin Sawatzky <66632997+nuclearkevin@users.noreply.github.com> Date: Tue, 26 Nov 2024 21:23:29 -0600 Subject: [PATCH 101/184] Fix bin index to DoF ID mapping bug in adaptive libMesh meshes (#3206) --- src/mesh.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/mesh.cpp b/src/mesh.cpp index 0b051e63f62..ca644021070 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -3007,10 +3007,10 @@ void LibMesh::initialize() // contiguous in ID space, so we need to map from bin indices (defined over // active elements) to global dof ids if (adaptive_) { - bin_to_elem_map_.reserve(m_->n_active_local_elem()); - elem_to_bin_map_.resize(m_->n_local_elem(), -1); - for (auto it = m_->active_local_elements_begin(); - it != m_->active_local_elements_end(); it++) { + bin_to_elem_map_.reserve(m_->n_active_elem()); + elem_to_bin_map_.resize(m_->n_elem(), -1); + for (auto it = m_->active_elements_begin(); it != m_->active_elements_end(); + it++) { auto elem = *it; bin_to_elem_map_.push_back(elem->id()); From a940216d3a10800fc7d63f305cb481196c86e3fb Mon Sep 17 00:00:00 2001 From: Kevin Sawatzky <66632997+nuclearkevin@users.noreply.github.com> Date: Mon, 2 Dec 2024 22:41:41 -0600 Subject: [PATCH 102/184] Ensure libMesh::ReplicatedMesh is used for LibMesh tallies (#3208) --- src/mesh.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/mesh.cpp b/src/mesh.cpp index ca644021070..c572d255a36 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -2935,6 +2935,11 @@ LibMesh::LibMesh(pugi::xml_node node) : UnstructuredMesh(node), adaptive_(false) LibMesh::LibMesh(libMesh::MeshBase& input_mesh, double length_multiplier) : adaptive_(input_mesh.n_active_elem() != input_mesh.n_elem()) { + if (!dynamic_cast(&input_mesh)) { + fatal_error("At present LibMesh tallies require a replicated mesh. Please " + "ensure 'input_mesh' is a libMesh::ReplicatedMesh."); + } + m_ = &input_mesh; set_length_multiplier(length_multiplier); initialize(); @@ -2952,7 +2957,8 @@ LibMesh::LibMesh(const std::string& filename, double length_multiplier) void LibMesh::set_mesh_pointer_from_filename(const std::string& filename) { filename_ = filename; - unique_m_ = make_unique(*settings::libmesh_comm, n_dimension_); + unique_m_ = + make_unique(*settings::libmesh_comm, n_dimension_); m_ = unique_m_.get(); m_->read(filename_); } From de8132a5a431660f5ff515cc7894ea0f283d3bec Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Tue, 3 Dec 2024 08:22:46 +0100 Subject: [PATCH 103/184] adding unstrucutred mesh file suffix to docstring (#3211) Co-authored-by: Paul Romano --- openmc/mesh.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openmc/mesh.py b/openmc/mesh.py index 6afe5d36eea..ef5c6c78410 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -2072,7 +2072,9 @@ class UnstructuredMesh(MeshBase): Parameters ---------- filename : path-like - Location of the unstructured mesh file + Location of the unstructured mesh file. Supported files for 'moab' + library are .h5 and .vtk. Supported files for 'libmesh' library are + exodus mesh files .exo. library : {'moab', 'libmesh'} Mesh library used for the unstructured mesh tally mesh_id : int From 775c41512283d178a49c787dc5fdfe617bac4938 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Mon, 16 Dec 2024 08:40:02 -0600 Subject: [PATCH 104/184] Write and read mesh name attribute (#3221) --- docs/source/io_formats/statepoint.rst | 5 +- docs/source/io_formats/tallies.rst | 7 +- include/openmc/mesh.h | 20 ++-- openmc/mesh.py | 92 ++++++++++--------- src/mesh.cpp | 55 +++++------ .../tally_slice_merge/inputs_true.dat | 2 +- .../tally_slice_merge/test.py | 3 + tests/unit_tests/test_mesh.py | 21 +++++ 8 files changed, 126 insertions(+), 79 deletions(-) diff --git a/docs/source/io_formats/statepoint.rst b/docs/source/io_formats/statepoint.rst index f61e967ca8a..82d20a76302 100644 --- a/docs/source/io_formats/statepoint.rst +++ b/docs/source/io_formats/statepoint.rst @@ -72,7 +72,10 @@ The current version of the statepoint file format is 18.1. **/tallies/meshes/mesh /** -:Datasets: - **type** (*char[]*) -- Type of mesh. +:Attributes: - **id** (*int*) -- ID of the mesh + - **type** (*char[]*) -- Type of mesh. + +:Datasets: - **name** (*char[]*) -- Name of the mesh. - **dimension** (*int*) -- Number of mesh cells in each dimension. - **Regular Mesh Only:** - **lower_left** (*double[]*) -- Coordinates of lower-left corner of diff --git a/docs/source/io_formats/tallies.rst b/docs/source/io_formats/tallies.rst index 78101ab668d..9f29a949acb 100644 --- a/docs/source/io_formats/tallies.rst +++ b/docs/source/io_formats/tallies.rst @@ -109,7 +109,7 @@ The ```` element accepts the following sub-elements: prematurely if there are no hits in any bins at the first evalulation. It is the user's responsibility to specify enough particles per batch to get a nonzero score in at least one bin. - + *Default*: False :scores: @@ -329,6 +329,11 @@ If a mesh is desired as a filter for a tally, it must be specified in a separate element with the tag name ````. This element has the following attributes/sub-elements: + :name: + An optional string name to identify the mesh in output files. + + *Default*: "" + :type: The type of mesh. This can be either "regular", "rectilinear", "cylindrical", "spherical", or "unstructured". diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index 5edb03003f9..bd86d54fb72 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -132,13 +132,18 @@ class Mesh { int32_t id() const { return id_; } + const std::string& name() const { return name_; } + //! Set the mesh ID void set_id(int32_t id = -1); + //! Write the mesh data to an HDF5 group + void to_hdf5(hid_t group) const; + //! Write mesh data to an HDF5 group // //! \param[in] group HDF5 group - virtual void to_hdf5(hid_t group) const = 0; + virtual void to_hdf5_inner(hid_t group) const = 0; //! Find the mesh lines that intersect an axis-aligned slice plot // @@ -202,7 +207,8 @@ class Mesh { // Data members xt::xtensor lower_left_; //!< Lower-left coordinates of mesh xt::xtensor upper_right_; //!< Upper-right coordinates of mesh - int id_ {-1}; //!< User-specified ID + int id_ {-1}; //!< Mesh ID + std::string name_; //!< User-specified name int n_dimension_ {-1}; //!< Number of dimensions }; @@ -410,7 +416,7 @@ class RegularMesh : public StructuredMesh { std::pair, vector> plot( Position plot_ll, Position plot_ur) const override; - void to_hdf5(hid_t group) const override; + void to_hdf5_inner(hid_t group) const override; //! Get the coordinate for the mesh grid boundary in the positive direction //! @@ -460,7 +466,7 @@ class RectilinearMesh : public StructuredMesh { std::pair, vector> plot( Position plot_ll, Position plot_ur) const override; - void to_hdf5(hid_t group) const override; + void to_hdf5_inner(hid_t group) const override; //! Get the coordinate for the mesh grid boundary in the positive direction //! @@ -506,7 +512,7 @@ class CylindricalMesh : public PeriodicStructuredMesh { std::pair, vector> plot( Position plot_ll, Position plot_ur) const override; - void to_hdf5(hid_t group) const override; + void to_hdf5_inner(hid_t group) const override; double volume(const MeshIndex& ijk) const override; @@ -570,7 +576,7 @@ class SphericalMesh : public PeriodicStructuredMesh { std::pair, vector> plot( Position plot_ll, Position plot_ur) const override; - void to_hdf5(hid_t group) const override; + void to_hdf5_inner(hid_t group) const override; double r(int i) const { return grid_[0][i]; } double theta(int i) const { return grid_[1][i]; } @@ -632,7 +638,7 @@ class UnstructuredMesh : public Mesh { void surface_bins_crossed(Position r0, Position r1, const Direction& u, vector& bins) const override; - void to_hdf5(hid_t group) const override; + void to_hdf5_inner(hid_t group) const override; std::string bin_label(int bin) const override; diff --git a/openmc/mesh.py b/openmc/mesh.py index ef5c6c78410..0b6f6b84e4b 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -99,21 +99,40 @@ def from_hdf5(cls, group: h5py.Group): Instance of a MeshBase subclass """ + mesh_type = 'regular' if 'type' not in group.attrs else group.attrs['type'].decode() + mesh_id = int(group.name.split('/')[-1].lstrip('mesh ')) + mesh_name = '' if not 'name' in group else group['name'][()].decode() - mesh_type = group['type'][()].decode() if mesh_type == 'regular': - return RegularMesh.from_hdf5(group) + return RegularMesh.from_hdf5(group, mesh_id, mesh_name) elif mesh_type == 'rectilinear': - return RectilinearMesh.from_hdf5(group) + return RectilinearMesh.from_hdf5(group, mesh_id, mesh_name) elif mesh_type == 'cylindrical': - return CylindricalMesh.from_hdf5(group) + return CylindricalMesh.from_hdf5(group, mesh_id, mesh_name) elif mesh_type == 'spherical': - return SphericalMesh.from_hdf5(group) + return SphericalMesh.from_hdf5(group, mesh_id, mesh_name) elif mesh_type == 'unstructured': - return UnstructuredMesh.from_hdf5(group) + return UnstructuredMesh.from_hdf5(group, mesh_id, mesh_name) else: raise ValueError('Unrecognized mesh type: "' + mesh_type + '"') + def to_xml_element(self): + """Return XML representation of the mesh + + Returns + ------- + element : lxml.etree._Element + XML element containing mesh data + + """ + elem = ET.Element("mesh") + + elem.set("id", str(self._id)) + if self.name: + elem.set("name", self.name) + + return elem + @classmethod def from_xml_element(cls, elem: ET.Element): """Generates a mesh from an XML element @@ -132,18 +151,21 @@ def from_xml_element(cls, elem: ET.Element): mesh_type = get_text(elem, 'type') if mesh_type == 'regular' or mesh_type is None: - return RegularMesh.from_xml_element(elem) + mesh = RegularMesh.from_xml_element(elem) elif mesh_type == 'rectilinear': - return RectilinearMesh.from_xml_element(elem) + mesh = RectilinearMesh.from_xml_element(elem) elif mesh_type == 'cylindrical': - return CylindricalMesh.from_xml_element(elem) + mesh = CylindricalMesh.from_xml_element(elem) elif mesh_type == 'spherical': - return SphericalMesh.from_xml_element(elem) + mesh = SphericalMesh.from_xml_element(elem) elif mesh_type == 'unstructured': - return UnstructuredMesh.from_xml_element(elem) + mesh = UnstructuredMesh.from_xml_element(elem) else: raise ValueError(f'Unrecognized mesh type "{mesh_type}" found.') + mesh.name = get_text(elem, 'name', default='') + return mesh + def get_homogenized_materials( self, model: openmc.Model, @@ -791,11 +813,9 @@ def __repr__(self): return string @classmethod - def from_hdf5(cls, group: h5py.Group): - mesh_id = int(group.name.split('/')[-1].lstrip('mesh ')) - + def from_hdf5(cls, group: h5py.Group, mesh_id: int, name: str): # Read and assign mesh properties - mesh = cls(mesh_id) + mesh = cls(mesh_id=mesh_id, name=name) mesh.dimension = group['dimension'][()] mesh.lower_left = group['lower_left'][()] if 'width' in group: @@ -899,9 +919,7 @@ def to_xml_element(self): XML element containing mesh data """ - - element = ET.Element("mesh") - element.set("id", str(self._id)) + element = super().to_xml_element() if self._dimension is not None: subelement = ET.SubElement(element, "dimension") @@ -937,10 +955,6 @@ def from_xml_element(cls, elem: ET.Element): mesh_id = int(get_text(elem, 'id')) mesh = cls(mesh_id=mesh_id) - mesh_type = get_text(elem, 'type') - if mesh_type is not None: - mesh.type = mesh_type - dimension = get_text(elem, 'dimension') if dimension is not None: mesh.dimension = [int(x) for x in dimension.split()] @@ -1235,11 +1249,9 @@ def __repr__(self): return string @classmethod - def from_hdf5(cls, group: h5py.Group): - mesh_id = int(group.name.split('/')[-1].lstrip('mesh ')) - + def from_hdf5(cls, group: h5py.Group, mesh_id: int, name: str): # Read and assign mesh properties - mesh = cls(mesh_id=mesh_id) + mesh = cls(mesh_id=mesh_id, name=name) mesh.x_grid = group['x_grid'][()] mesh.y_grid = group['y_grid'][()] mesh.z_grid = group['z_grid'][()] @@ -1279,8 +1291,7 @@ def to_xml_element(self): """ - element = ET.Element("mesh") - element.set("id", str(self._id)) + element = super().to_xml_element() element.set("type", "rectilinear") subelement = ET.SubElement(element, "x_grid") @@ -1541,12 +1552,11 @@ def get_indices_at_coords( return (r_index, phi_index, z_index) @classmethod - def from_hdf5(cls, group: h5py.Group): - mesh_id = int(group.name.split('/')[-1].lstrip('mesh ')) - + def from_hdf5(cls, group: h5py.Group, mesh_id: int, name: str): # Read and assign mesh properties mesh = cls( mesh_id=mesh_id, + name=name, r_grid = group['r_grid'][()], phi_grid = group['phi_grid'][()], z_grid = group['z_grid'][()], @@ -1647,8 +1657,7 @@ def to_xml_element(self): """ - element = ET.Element("mesh") - element.set("id", str(self._id)) + element = super().to_xml_element() element.set("type", "cylindrical") subelement = ET.SubElement(element, "r_grid") @@ -1926,15 +1935,14 @@ def __repr__(self): return string @classmethod - def from_hdf5(cls, group: h5py.Group): - mesh_id = int(group.name.split('/')[-1].lstrip('mesh ')) - + def from_hdf5(cls, group: h5py.Group, mesh_id: int, name: str): # Read and assign mesh properties mesh = cls( r_grid = group['r_grid'][()], theta_grid = group['theta_grid'][()], phi_grid = group['phi_grid'][()], mesh_id=mesh_id, + name=name ) if 'origin' in group: mesh.origin = group['origin'][()] @@ -1951,8 +1959,7 @@ def to_xml_element(self): """ - element = ET.Element("mesh") - element.set("id", str(self._id)) + element = super().to_xml_element() element.set("type", "spherical") subelement = ET.SubElement(element, "r_grid") @@ -2444,8 +2451,7 @@ def write_data_to_vtk( writer.Write() @classmethod - def from_hdf5(cls, group: h5py.Group): - mesh_id = int(group.name.split('/')[-1].lstrip('mesh ')) + def from_hdf5(cls, group: h5py.Group, mesh_id: int, name: str): filename = group['filename'][()].decode() library = group['library'][()].decode() if 'options' in group.attrs: @@ -2453,7 +2459,7 @@ def from_hdf5(cls, group: h5py.Group): else: options = None - mesh = cls(filename=filename, library=library, mesh_id=mesh_id, options=options) + mesh = cls(filename=filename, library=library, mesh_id=mesh_id, name=name, options=options) mesh._has_statepoint_data = True vol_data = group['volumes'][()] mesh.volumes = np.reshape(vol_data, (vol_data.shape[0],)) @@ -2480,9 +2486,9 @@ def to_xml_element(self): """ - element = ET.Element("mesh") - element.set("id", str(self._id)) + element = super().to_xml_element() element.set("type", "unstructured") + element.set("library", self._library) if self.options is not None: element.set('options', self.options) diff --git a/src/mesh.cpp b/src/mesh.cpp index c572d255a36..2ee616f28dc 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -109,6 +109,8 @@ Mesh::Mesh(pugi::xml_node node) { // Read mesh id id_ = std::stoi(get_node_value(node, "id")); + if (check_for_node(node, "name")) + name_ = get_node_value(node, "name"); } void Mesh::set_id(int32_t id) @@ -236,6 +238,28 @@ vector Mesh::material_volumes( return result; } +void Mesh::to_hdf5(hid_t group) const +{ + // Create group for mesh + std::string group_name = fmt::format("mesh {}", id_); + hid_t mesh_group = create_group(group, group_name.c_str()); + + // Write mesh type + write_attribute(mesh_group, "type", this->get_mesh_type()); + + // Write mesh ID + write_attribute(mesh_group, "id", id_); + + // Write mesh name + write_dataset(mesh_group, "name", name_); + + // Write mesh data + this->to_hdf5_inner(mesh_group); + + // Close group + close_group(mesh_group); +} + //============================================================================== // Structured Mesh implementation //============================================================================== @@ -389,11 +413,8 @@ std::string UnstructuredMesh::bin_label(int bin) const return fmt::format("Mesh Index ({})", bin); }; -void UnstructuredMesh::to_hdf5(hid_t group) const +void UnstructuredMesh::to_hdf5_inner(hid_t mesh_group) const { - hid_t mesh_group = create_group(group, fmt::format("mesh {}", id_)); - - write_dataset(mesh_group, "type", mesh_type); write_dataset(mesh_group, "filename", filename_); write_dataset(mesh_group, "library", this->library()); if (!options_.empty()) { @@ -453,8 +474,6 @@ void UnstructuredMesh::to_hdf5(hid_t group) const write_dataset(mesh_group, "volumes", volumes); write_dataset(mesh_group, "connectivity", connectivity); write_dataset(mesh_group, "element_types", elem_types); - - close_group(mesh_group); } void UnstructuredMesh::set_length_multiplier(double length_multiplier) @@ -948,17 +967,13 @@ std::pair, vector> RegularMesh::plot( return {axis_lines[0], axis_lines[1]}; } -void RegularMesh::to_hdf5(hid_t group) const +void RegularMesh::to_hdf5_inner(hid_t mesh_group) const { - hid_t mesh_group = create_group(group, "mesh " + std::to_string(id_)); - write_dataset(mesh_group, "type", "regular"); write_dataset(mesh_group, "dimension", get_x_shape()); write_dataset(mesh_group, "lower_left", lower_left_); write_dataset(mesh_group, "upper_right", upper_right_); write_dataset(mesh_group, "width", width_); - - close_group(mesh_group); } xt::xtensor RegularMesh::count_sites( @@ -1138,16 +1153,12 @@ std::pair, vector> RectilinearMesh::plot( return {axis_lines[0], axis_lines[1]}; } -void RectilinearMesh::to_hdf5(hid_t group) const +void RectilinearMesh::to_hdf5_inner(hid_t mesh_group) const { - hid_t mesh_group = create_group(group, "mesh " + std::to_string(id_)); - write_dataset(mesh_group, "type", "rectilinear"); write_dataset(mesh_group, "x_grid", grid_[0]); write_dataset(mesh_group, "y_grid", grid_[1]); write_dataset(mesh_group, "z_grid", grid_[2]); - - close_group(mesh_group); } double RectilinearMesh::volume(const MeshIndex& ijk) const @@ -1417,17 +1428,13 @@ std::pair, vector> CylindricalMesh::plot( return {axis_lines[0], axis_lines[1]}; } -void CylindricalMesh::to_hdf5(hid_t group) const +void CylindricalMesh::to_hdf5_inner(hid_t mesh_group) const { - hid_t mesh_group = create_group(group, "mesh " + std::to_string(id_)); - write_dataset(mesh_group, "type", "cylindrical"); write_dataset(mesh_group, "r_grid", grid_[0]); write_dataset(mesh_group, "phi_grid", grid_[1]); write_dataset(mesh_group, "z_grid", grid_[2]); write_dataset(mesh_group, "origin", origin_); - - close_group(mesh_group); } double CylindricalMesh::volume(const MeshIndex& ijk) const @@ -1733,17 +1740,13 @@ std::pair, vector> SphericalMesh::plot( return {axis_lines[0], axis_lines[1]}; } -void SphericalMesh::to_hdf5(hid_t group) const +void SphericalMesh::to_hdf5_inner(hid_t mesh_group) const { - hid_t mesh_group = create_group(group, "mesh " + std::to_string(id_)); - write_dataset(mesh_group, "type", SphericalMesh::mesh_type); write_dataset(mesh_group, "r_grid", grid_[0]); write_dataset(mesh_group, "theta_grid", grid_[1]); write_dataset(mesh_group, "phi_grid", grid_[2]); write_dataset(mesh_group, "origin", origin_); - - close_group(mesh_group); } double SphericalMesh::volume(const MeshIndex& ijk) const diff --git a/tests/regression_tests/tally_slice_merge/inputs_true.dat b/tests/regression_tests/tally_slice_merge/inputs_true.dat index 356962f8e4f..7159d833a5a 100644 --- a/tests/regression_tests/tally_slice_merge/inputs_true.dat +++ b/tests/regression_tests/tally_slice_merge/inputs_true.dat @@ -308,7 +308,7 @@ - + 2 2 -50.0 -50.0 50.0 50.0 diff --git a/tests/regression_tests/tally_slice_merge/test.py b/tests/regression_tests/tally_slice_merge/test.py index 090e3144838..aec73979b91 100644 --- a/tests/regression_tests/tally_slice_merge/test.py +++ b/tests/regression_tests/tally_slice_merge/test.py @@ -149,6 +149,9 @@ def _get_results(self, hash_output=False): sum2 = mesh_tally.summation(filter_type=openmc.MeshFilter, filter_bins=[(2, 1), (2, 2)]) + mesh = mesh_tally.find_filter(openmc.MeshFilter).mesh + assert mesh.name == 'mesh' + # Merge the mesh tally slices merge_tally = sum1.merge(sum2) diff --git a/tests/unit_tests/test_mesh.py b/tests/unit_tests/test_mesh.py index ed08be816f5..27322165f64 100644 --- a/tests/unit_tests/test_mesh.py +++ b/tests/unit_tests/test_mesh.py @@ -357,6 +357,27 @@ def test_CylindricalMesh_get_indices_at_coords(): assert mesh.get_indices_at_coords([102, 199.1, 299]) == (0, 3, 0) # forth angle quadrant +def test_mesh_name_roundtrip(run_in_tmpdir): + + mesh = openmc.RegularMesh() + mesh.name = 'regular-mesh' + mesh.lower_left = (-1, -1, -1) + mesh.width = (1, 1, 1) + mesh.dimension = (1, 1, 1) + + mesh_filter = openmc.MeshFilter(mesh) + tally = openmc.Tally() + tally.filters = [mesh_filter] + tally.scores = ['flux'] + + openmc.Tallies([tally]).export_to_xml() + + xml_tallies = openmc.Tallies.from_xml() + + mesh = xml_tallies[0].find_filter(openmc.MeshFilter).mesh + assert mesh.name == 'regular-mesh' + + def test_umesh_roundtrip(run_in_tmpdir, request): umesh = openmc.UnstructuredMesh(request.path.parent / 'test_mesh_tets.e', 'moab') umesh.output = True From 3a001d3de2130da08d33f995f54b248ff2edcaad Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Mon, 6 Jan 2025 13:40:13 +0100 Subject: [PATCH 105/184] updated link to log mapping technique (#3241) --- docs/source/methods/cross_sections.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/methods/cross_sections.rst b/docs/source/methods/cross_sections.rst index 2cafa869162..21a1ef8dfeb 100644 --- a/docs/source/methods/cross_sections.rst +++ b/docs/source/methods/cross_sections.rst @@ -290,7 +290,7 @@ scattering information in the water while the fuel can be simulated with linear or even isotropic scattering. .. _logarithmic mapping technique: - https://laws.lanl.gov/vhosts/mcnp.lanl.gov/pdf_files/la-ur-14-24530.pdf + https://mcnp.lanl.gov/pdf_files/TechReport_2014_LANL_LA-UR-14-24530_Brown.pdf .. _Hwang: https://doi.org/10.13182/NSE87-A16381 .. _Josey: https://doi.org/10.1016/j.jcp.2015.08.013 .. _WMP Library: https://github.com/mit-crpg/WMP_Library From 393334829db331f1b95d36d0da32a9ae786590ab Mon Sep 17 00:00:00 2001 From: Masoud Date: Tue, 7 Jan 2025 10:02:05 +0330 Subject: [PATCH 106/184] Adding '#define _USE_MATH_DEFINES' to make M_PI declared in Intel and MSVC compilers (#3238) Co-authored-by: Paul Romano --- src/external/quartic_solver.cpp | 1 + src/mesh.cpp | 7 ++++--- src/plot.cpp | 2 ++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/external/quartic_solver.cpp b/src/external/quartic_solver.cpp index 0b280e83c29..915020ffaa3 100644 --- a/src/external/quartic_solver.cpp +++ b/src/external/quartic_solver.cpp @@ -1,4 +1,5 @@ #include +#define _USE_MATH_DEFINES // to make M_PI declared in Intel and MSVC compilers #include #include #include diff --git a/src/mesh.cpp b/src/mesh.cpp index 2ee616f28dc..4b1878c5a2c 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -1,7 +1,8 @@ #include "openmc/mesh.h" -#include // for copy, equal, min, min_element -#include // for ceil -#include // for size_t +#include // for copy, equal, min, min_element +#define _USE_MATH_DEFINES // to make M_PI declared in Intel and MSVC compilers +#include // for ceil +#include // for size_t #include #include diff --git a/src/plot.cpp b/src/plot.cpp index 348138570c1..de7d475e1c5 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -1,6 +1,8 @@ #include "openmc/plot.h" #include +#define _USE_MATH_DEFINES // to make M_PI declared in Intel and MSVC compilers +#include #include #include #include From 5ad1a4aa1e67ed6a4b88ebe84af0f1b117d14cd5 Mon Sep 17 00:00:00 2001 From: Baptiste Mouginot <15145274+bam241@users.noreply.github.com> Date: Tue, 7 Jan 2025 22:50:02 +0100 Subject: [PATCH 107/184] Differentiate materials in DAGMC universes (#3056) Co-authored-by: Baptiste Mouginot Co-authored-by: Patrick Shriwise Co-authored-by: azimG Co-authored-by: Paul Romano --- docs/source/io_formats/geometry.rst | 34 +- docs/source/pythonapi/capi.rst | 1 + include/openmc/capi.h | 3 + include/openmc/cell.h | 8 +- include/openmc/dagmc.h | 15 + include/openmc/surface.h | 8 +- openmc/__init__.py | 1 + openmc/dagmc.py | 625 ++++++++++++++++++++ openmc/geometry.py | 2 +- openmc/lib/__init__.py | 1 + openmc/lib/dagmc.py | 43 ++ openmc/model/model.py | 119 +++- openmc/universe.py | 821 ++++++++------------------- src/cell.cpp | 4 +- src/dagmc.cpp | 102 +++- src/particle.cpp | 7 +- src/plot.cpp | 2 +- src/surface.cpp | 8 +- tests/unit_tests/dagmc/test_model.py | 256 +++++++++ 19 files changed, 1416 insertions(+), 644 deletions(-) create mode 100644 openmc/dagmc.py create mode 100644 openmc/lib/dagmc.py create mode 100644 tests/unit_tests/dagmc/test_model.py diff --git a/docs/source/io_formats/geometry.rst b/docs/source/io_formats/geometry.rst index ac48e48d2d1..6d0a37a24fa 100644 --- a/docs/source/io_formats/geometry.rst +++ b/docs/source/io_formats/geometry.rst @@ -407,13 +407,33 @@ Each ```` element can have the following attributes or sub-eleme *Default*: None + :material_overrides: + This element contains information on material overrides to be applied to the + DAGMC universe. It has the following attributes and sub-elements: - .. note:: A geometry.xml file containing only a DAGMC model for a file named `dagmc.h5m` (no CSG) - looks as follows + :cell: + Material override information for a single cell. It contains the following + attributes and sub-elements: - .. code-block:: xml + :id: + The cell ID in the DAGMC geometry for which the material override will + apply. - - - - + :materials: + A list of material IDs that will apply to instances of the cell. If the + list contains only one ID, it will replace the original material + assignment of all instances of the DAGMC cell. If the list contains more + than one material, each material ID of the list will be assigned to the + various instances of the DAGMC cell. + + *Default*: None + +.. note:: A geometry.xml file containing only a DAGMC model for a file named + `dagmc.h5m` (no CSG) looks as follows: + + .. code-block:: xml + + + + + diff --git a/docs/source/pythonapi/capi.rst b/docs/source/pythonapi/capi.rst index 995ad97fa74..9ceff83fde8 100644 --- a/docs/source/pythonapi/capi.rst +++ b/docs/source/pythonapi/capi.rst @@ -19,6 +19,7 @@ Functions finalize find_cell find_material + dagmc_universe_cell_ids global_bounding_box global_tallies hard_reset diff --git a/include/openmc/capi.h b/include/openmc/capi.h index 9401156a64f..8edd99c0785 100644 --- a/include/openmc/capi.h +++ b/include/openmc/capi.h @@ -29,6 +29,9 @@ int openmc_cell_set_temperature( int32_t index, double T, const int32_t* instance, bool set_contained = false); int openmc_cell_set_translation(int32_t index, const double xyz[]); int openmc_cell_set_rotation(int32_t index, const double rot[], size_t rot_len); +int openmc_dagmc_universe_get_cell_ids( + int32_t univ_id, int32_t* ids, size_t* n); +int openmc_dagmc_universe_get_num_cells(int32_t univ_id, size_t* n); int openmc_energy_filter_get_bins( int32_t index, const double** energies, size_t* n); int openmc_energy_filter_set_bins( diff --git a/include/openmc/cell.h b/include/openmc/cell.h index 70843140bad..032475ce982 100644 --- a/include/openmc/cell.h +++ b/include/openmc/cell.h @@ -320,7 +320,6 @@ class Cell { int32_t universe_; //!< Universe # this cell is in int32_t fill_; //!< Universe # filling this cell int32_t n_instances_ {0}; //!< Number of instances of this cell - GeometryType geom_type_; //!< Geometric representation type (CSG, DAGMC) //! \brief Index corresponding to this cell in distribcell arrays int distribcell_index_ {C_NONE}; @@ -350,6 +349,13 @@ class Cell { vector rotation_; vector offset_; //!< Distribcell offset table + + // Accessors + const GeometryType& geom_type() const { return geom_type_; } + GeometryType& geom_type() { return geom_type_; } + +private: + GeometryType geom_type_; //!< Geometric representation type (CSG, DAGMC) }; struct CellInstanceItem { diff --git a/include/openmc/dagmc.h b/include/openmc/dagmc.h index 2facf4fc05e..47fcfe237e3 100644 --- a/include/openmc/dagmc.h +++ b/include/openmc/dagmc.h @@ -29,6 +29,12 @@ void check_dagmc_root_univ(); #include "openmc/particle.h" #include "openmc/position.h" #include "openmc/surface.h" +#include "openmc/vector.h" + +#include // for shared_ptr, unique_ptr +#include +#include +#include // for pair class UWUW; @@ -133,6 +139,10 @@ class DAGUniverse : public Universe { void legacy_assign_material( std::string mat_string, std::unique_ptr& c) const; + //! Assign a material overriding normal assignement to a cell + //! \param[in] c The OpenMC cell to which the material is assigned + void override_assign_material(std::unique_ptr& c) const; + //! Return the index into the model cells vector for a given DAGMC volume //! handle in the universe //! \param[in] vol MOAB handle to the DAGMC volume set @@ -184,6 +194,11 @@ class DAGUniverse : public Universe { //!< generate new material IDs for the universe bool has_graveyard_; //!< Indicates if the DAGMC geometry has a "graveyard" //!< volume + std::unordered_map> + material_overrides_; //!< Map of material overrides + //!< keys correspond to the DAGMCCell id + //!< values are a list of material ids used + //!< for the override }; //============================================================================== diff --git a/include/openmc/surface.h b/include/openmc/surface.h index af235301c14..498f71d4f9b 100644 --- a/include/openmc/surface.h +++ b/include/openmc/surface.h @@ -38,7 +38,6 @@ class Surface { int id_; //!< Unique ID std::string name_; //!< User-defined name unique_ptr bc_; //!< Boundary condition - GeometryType geom_type_; //!< Geometry type indicator (CSG or DAGMC) bool surf_source_ {false}; //!< Activate source banking for the surface? explicit Surface(pugi::xml_node surf_node); @@ -91,6 +90,13 @@ class Surface { //! Get the BoundingBox for this surface. virtual BoundingBox bounding_box(bool /*pos_side*/) const { return {}; } + // Accessors + const GeometryType& geom_type() const { return geom_type_; } + GeometryType& geom_type() { return geom_type_; } + +private: + GeometryType geom_type_; //!< Geometry type indicator (CSG or DAGMC) + protected: virtual void to_hdf5_inner(hid_t group_id) const = 0; }; diff --git a/openmc/__init__.py b/openmc/__init__.py index 566d287068f..bb972b4e6ad 100644 --- a/openmc/__init__.py +++ b/openmc/__init__.py @@ -15,6 +15,7 @@ from openmc.weight_windows import * from openmc.surface import * from openmc.universe import * +from openmc.dagmc import * from openmc.source import * from openmc.settings import * from openmc.lattice import * diff --git a/openmc/dagmc.py b/openmc/dagmc.py new file mode 100644 index 00000000000..8ab0aaf69e7 --- /dev/null +++ b/openmc/dagmc.py @@ -0,0 +1,625 @@ +from collections.abc import Iterable, Mapping +from numbers import Integral + +import h5py +import lxml.etree as ET +import numpy as np +import warnings + +import openmc +import openmc.checkvalue as cv +from ._xml import get_text +from .checkvalue import check_type, check_value +from .surface import _BOUNDARY_TYPES +from .bounding_box import BoundingBox +from .utility_funcs import input_path + + +class DAGMCUniverse(openmc.UniverseBase): + """A reference to a DAGMC file to be used in the model. + + .. versionadded:: 0.13.0 + + Parameters + ---------- + filename : str + Path to the DAGMC file used to represent this universe. + universe_id : int, optional + Unique identifier of the universe. If not specified, an identifier will + automatically be assigned. + name : str, optional + Name of the universe. If not specified, the name is the empty string. + auto_geom_ids : bool + Set IDs automatically on initialization (True) or report overlaps in ID + space between CSG and DAGMC (False) + auto_mat_ids : bool + Set IDs automatically on initialization (True) or report overlaps in ID + space between OpenMC and UWUW materials (False) + material_overrides : dict, optional + A dictionary of material overrides. The keys are material name strings + and the values are Iterables of openmc.Material objects. If a material + name is found in the DAGMC file, the material will be replaced with the + openmc.Material object in the value. + + Attributes + ---------- + id : int + Unique identifier of the universe + name : str + Name of the universe + filename : str + Path to the DAGMC file used to represent this universe. + auto_geom_ids : bool + Set IDs automatically on initialization (True) or report overlaps in ID + space between CSG and DAGMC (False) + auto_mat_ids : bool + Set IDs automatically on initialization (True) or report overlaps in ID + space between OpenMC and UWUW materials (False) + bounding_box : openmc.BoundingBox + Lower-left and upper-right coordinates of an axis-aligned bounding box + of the universe. + + .. versionadded:: 0.13.1 + material_names : list of str + Return a sorted list of materials names that are contained within the + DAGMC h5m file. This is useful when naming openmc.Material() objects as + each material name present in the DAGMC h5m file must have a matching + openmc.Material() with the same name. + + .. versionadded:: 0.13.2 + n_cells : int + The number of cells in the DAGMC model. This is the number of cells at + runtime and accounts for the implicit complement whether or not is it + present in the DAGMC file. + + .. versionadded:: 0.13.2 + n_surfaces : int + The number of surfaces in the model. + + .. versionadded:: 0.13.2 + material_overrides : dict + A dictionary of material overrides. Keys are cell IDs; values are + iterables of :class:`openmc.Material` objects. The material assignment + of each DAGMC cell ID key will be replaced with the + :class:`~openmc.Material` object in the value. If the value contains + multiple :class:`~openmc.Material` objects, each Material in the list + will be assigned to the corresponding instance of the cell. + + .. versionadded:: 0.15.1 + """ + + def __init__(self, + filename: cv.PathLike, + universe_id=None, + name='', + auto_geom_ids=False, + auto_mat_ids=False, + material_overrides=None): + super().__init__(universe_id, name) + # Initialize class attributes + self.filename = filename + self.auto_geom_ids = auto_geom_ids + self.auto_mat_ids = auto_mat_ids + self._material_overrides = {} + if material_overrides is not None: + self.material_overrides = material_overrides + + def __repr__(self): + string = super().__repr__() + string += '{: <16}=\t{}\n'.format('\tGeom', 'DAGMC') + string += '{: <16}=\t{}\n'.format('\tFile', self.filename) + return string + + @property + def bounding_box(self): + with h5py.File(self.filename) as dagmc_file: + coords = dagmc_file['tstt']['nodes']['coordinates'][()] + lower_left_corner = coords.min(axis=0) + upper_right_corner = coords.max(axis=0) + return openmc.BoundingBox(lower_left_corner, upper_right_corner) + + @property + def filename(self): + return self._filename + + @filename.setter + def filename(self, val: cv.PathLike): + cv.check_type('DAGMC filename', val, cv.PathLike) + self._filename = input_path(val) + + @property + def material_overrides(self): + return self._material_overrides + + @material_overrides.setter + def material_overrides(self, val): + cv.check_type('material overrides', val, Mapping) + for key, value in val.items(): + self.add_material_override(key, value) + + def replace_material_assignment(self, material_name: str, material: openmc.Material): + """Replace a material assignment within the DAGMC universe. + + Replace the material assignment of all cells filled with a material in + the DAGMC universe. The universe must be synchronized in an initialized + Model (see :meth:`~openmc.DAGMCUniverse.sync_dagmc_cells`) before + calling this method. + + .. versionadded:: 0.15.1 + + Parameters + ---------- + material_name : str + Material name to replace + material : openmc.Material + Material to replace the material_name with + + """ + if material_name not in self.material_names: + raise ValueError( + f"No material with name '{material_name}' found in the DAGMC universe") + + if not self.cells: + raise RuntimeError("This DAGMC universe has not been synchronized " + "on an initialized Model.") + + for cell in self.cells.values(): + if cell.fill is None: + continue + if isinstance(cell.fill, openmc.Iterable): + cell.fill = list(map(lambda x: material if x.name == material_name else x, cell.fill)) + else: + cell.fill = material if cell.fill.name == material_name else cell.fill + + def add_material_override(self, key, overrides=None): + """Add a material override to the universe. + + .. versionadded:: 0.15 + + Parameters + ---------- + key : openmc.DAGMCCell or int + Cell object or ID of the Cell to override + value : openmc.Material or Iterable of openmc.Material + Material(s) to be applied to the Cell passed as the key + + """ + # Ensure that they key is a valid type + if not isinstance(key, (int, openmc.DAGMCCell)): + raise ValueError("Unrecognized key type. " + "Must be an integer or openmc.DAGMCCell object") + + # Ensure that overrides is an iterable of openmc.Material + overrides = overrides if isinstance(overrides, openmc.Iterable) else [overrides] + cv.check_iterable_type('material objects', overrides, (openmc.Material, type(None))) + + # if a DAGMCCell is passed, redcue the key to the ID of the cell + if isinstance(key, openmc.DAGMCCell): + key = key.id + + if key not in self.cells: + raise ValueError(f"Cell ID '{key}' not found in DAGMC universe") + + self._material_overrides[key] = overrides + + @property + def auto_geom_ids(self): + return self._auto_geom_ids + + @auto_geom_ids.setter + def auto_geom_ids(self, val): + cv.check_type('DAGMC automatic geometry ids', val, bool) + self._auto_geom_ids = val + + @property + def auto_mat_ids(self): + return self._auto_mat_ids + + @auto_mat_ids.setter + def auto_mat_ids(self, val): + cv.check_type('DAGMC automatic material ids', val, bool) + self._auto_mat_ids = val + + @property + def material_names(self): + dagmc_file_contents = h5py.File(self.filename) + material_tags_hex = dagmc_file_contents['/tstt/tags/NAME'].get( + 'values') + material_tags_ascii = [] + for tag in material_tags_hex: + candidate_tag = tag.tobytes().decode().replace('\x00', '') + # tags might be for temperature or reflective surfaces + if candidate_tag.startswith('mat:'): + # if name ends with _comp remove it, it is not parsed + if candidate_tag.endswith('_comp'): + candidate_tag = candidate_tag[:-5] + # removes first 4 characters as openmc.Material name should be + # set without the 'mat:' part of the tag + material_tags_ascii.append(candidate_tag[4:]) + + return sorted(set(material_tags_ascii)) + + def _n_geom_elements(self, geom_type): + """ + Helper function for retrieving the number geometric entities in a DAGMC + file + + Parameters + ---------- + geom_type : str + The type of geometric entity to count. One of {'Volume', 'Surface'}. Returns + the runtime number of voumes in the DAGMC model (includes implicit complement). + + Returns + ------- + int + Number of geometry elements of the specified type + """ + cv.check_value('geometry type', geom_type, ('volume', 'surface')) + + def decode_str_tag(tag_val): + return tag_val.tobytes().decode().replace('\x00', '') + + with h5py.File(self.filename) as dagmc_file: + category_data = dagmc_file['tstt/tags/CATEGORY/values'] + category_strs = map(decode_str_tag, category_data) + n = sum([v == geom_type.capitalize() for v in category_strs]) + + # check for presence of an implicit complement in the file and + # increment the number of cells if it doesn't exist + if geom_type == 'volume': + name_data = dagmc_file['tstt/tags/NAME/values'] + name_strs = map(decode_str_tag, name_data) + if not sum(['impl_complement' in n for n in name_strs]): + n += 1 + return n + + @property + def n_cells(self): + return self._n_geom_elements('volume') + + @property + def n_surfaces(self): + return self._n_geom_elements('surface') + + def create_xml_subelement(self, xml_element, memo=None): + if memo is None: + memo = set() + + if self in memo: + return + + memo.add(self) + + # Ensure that the material overrides are up-to-date + for cell in self.cells.values(): + if cell.fill is None: + continue + self.add_material_override(cell, cell.fill) + + # Set xml element values + dagmc_element = ET.Element('dagmc_universe') + dagmc_element.set('id', str(self.id)) + + if self.auto_geom_ids: + dagmc_element.set('auto_geom_ids', 'true') + if self.auto_mat_ids: + dagmc_element.set('auto_mat_ids', 'true') + dagmc_element.set('filename', str(self.filename)) + if self._material_overrides: + mat_element = ET.Element('material_overrides') + for key in self._material_overrides: + cell_overrides = ET.Element('cell_override') + cell_overrides.set("id", str(key)) + material_element = ET.Element('material_ids') + material_element.text = ' '.join( + str(t.id) for t in self._material_overrides[key]) + cell_overrides.append(material_element) + mat_element.append(cell_overrides) + dagmc_element.append(mat_element) + xml_element.append(dagmc_element) + + def bounding_region( + self, + bounded_type: str = 'box', + boundary_type: str = 'vacuum', + starting_id: int = 10000, + padding_distance: float = 0. + ): + """Creates a either a spherical or box shaped bounding region around + the DAGMC geometry. + + .. versionadded:: 0.13.1 + + Parameters + ---------- + bounded_type : str + The type of bounding surface(s) to use when constructing the region. + Options include a single spherical surface (sphere) or a rectangle + made from six planes (box). + boundary_type : str + Boundary condition that defines the behavior for particles hitting + the surface. Defaults to vacuum boundary condition. Passed into the + surface construction. + starting_id : int + Starting ID of the surface(s) used in the region. For bounded_type + 'box', the next 5 IDs will also be used. Defaults to 10000 to reduce + the chance of an overlap of surface IDs with the DAGMC geometry. + padding_distance : float + Distance between the bounding region surfaces and the minimal + bounding box. Allows for the region to be larger than the DAGMC + geometry. + + Returns + ------- + openmc.Region + Region instance + """ + + check_type('boundary type', boundary_type, str) + check_value('boundary type', boundary_type, _BOUNDARY_TYPES) + check_type('starting surface id', starting_id, Integral) + check_type('bounded type', bounded_type, str) + check_value('bounded type', bounded_type, ('box', 'sphere')) + + bbox = self.bounding_box.expand(padding_distance, True) + + if bounded_type == 'sphere': + radius = np.linalg.norm(bbox.upper_right - bbox.center) + bounding_surface = openmc.Sphere( + surface_id=starting_id, + x0=bbox.center[0], + y0=bbox.center[1], + z0=bbox.center[2], + boundary_type=boundary_type, + r=radius, + ) + + return -bounding_surface + + if bounded_type == 'box': + # defines plane surfaces for all six faces of the bounding box + lower_x = openmc.XPlane(bbox[0][0], surface_id=starting_id) + upper_x = openmc.XPlane(bbox[1][0], surface_id=starting_id+1) + lower_y = openmc.YPlane(bbox[0][1], surface_id=starting_id+2) + upper_y = openmc.YPlane(bbox[1][1], surface_id=starting_id+3) + lower_z = openmc.ZPlane(bbox[0][2], surface_id=starting_id+4) + upper_z = openmc.ZPlane(bbox[1][2], surface_id=starting_id+5) + + region = +lower_x & -upper_x & +lower_y & -upper_y & +lower_z & -upper_z + + for surface in region.get_surfaces().values(): + surface.boundary_type = boundary_type + + return region + + def bounded_universe(self, bounding_cell_id=10000, **kwargs): + """Returns an openmc.Universe filled with this DAGMCUniverse and bounded + with a cell. Defaults to a box cell with a vacuum surface however this + can be changed using the kwargs which are passed directly to + DAGMCUniverse.bounding_region(). + + Parameters + ---------- + bounding_cell_id : int + The cell ID number to use for the bounding cell, defaults to 10000 to reduce + the chance of overlapping ID numbers with the DAGMC geometry. + + Returns + ------- + openmc.Universe + Universe instance + """ + bounding_cell = openmc.Cell( + fill=self, cell_id=bounding_cell_id, region=self.bounding_region(**kwargs)) + return openmc.Universe(cells=[bounding_cell]) + + @classmethod + def from_hdf5(cls, group): + """Create DAGMC universe from HDF5 group + + Parameters + ---------- + group : h5py.Group + Group in HDF5 file + + Returns + ------- + openmc.DAGMCUniverse + DAGMCUniverse instance + + """ + id = int(group.name.split('/')[-1].lstrip('universe ')) + fname = group['filename'][()].decode() + name = group['name'][()].decode() if 'name' in group else None + + out = cls(fname, universe_id=id, name=name) + + out.auto_geom_ids = bool(group.attrs['auto_geom_ids']) + out.auto_mat_ids = bool(group.attrs['auto_mat_ids']) + + return out + + @classmethod + def from_xml_element(cls, elem, mats = None): + """Generate DAGMC universe from XML element + + Parameters + ---------- + elem : lxml.etree._Element + `` element + mats : dict + Dictionary mapping material ID strings to :class:`openmc.Material` + instances (defined in :meth:`openmc.Geometry.from_xml`) + + Returns + ------- + openmc.DAGMCUniverse + DAGMCUniverse instance + + """ + id = int(get_text(elem, 'id')) + fname = get_text(elem, 'filename') + + out = cls(fname, universe_id=id) + + name = get_text(elem, 'name') + if name is not None: + out.name = name + + out.auto_geom_ids = bool(elem.get('auto_geom_ids')) + out.auto_mat_ids = bool(elem.get('auto_mat_ids')) + + el_mat_override = elem.find('material_overrides') + if el_mat_override is not None: + if mats is None: + raise ValueError("Material overrides found in DAGMC universe " + "but no materials were provided to populate " + "the mapping.") + out._material_overrides = {} + for elem in el_mat_override.findall('cell_override'): + cell_id = int(get_text(elem, 'id')) + mat_ids = get_text(elem, 'material_ids').split(' ') + mat_objs = [mats[mat_id] for mat_id in mat_ids] + out._material_overrides[cell_id] = mat_objs + + return out + + def _partial_deepcopy(self): + """Clone all of the openmc.DAGMCUniverse object's attributes except for + its cells, as they are copied within the clone function. This should + only to be used within the openmc.UniverseBase.clone() context. + """ + clone = openmc.DAGMCUniverse(name=self.name, filename=self.filename) + clone.volume = self.volume + clone.auto_geom_ids = self.auto_geom_ids + clone.auto_mat_ids = self.auto_mat_ids + return clone + + def add_cell(self, cell): + """Add a cell to the universe. + + Parameters + ---------- + cell : openmc.DAGMCCell + Cell to add + + """ + if not isinstance(cell, openmc.DAGMCCell): + msg = f'Unable to add a DAGMCCell to DAGMCUniverse ' \ + f'ID="{self._id}" since "{cell}" is not a DAGMCCell' + raise TypeError(msg) + + cell_id = cell.id + + if cell_id not in self._cells: + self._cells[cell_id] = cell + + def remove_cell(self, cell): + """Remove a cell from the universe. + + Parameters + ---------- + cell : openmc.Cell + Cell to remove + + """ + + if not isinstance(cell, openmc.DAGMCCell): + msg = f'Unable to remove a Cell from Universe ID="{self._id}" ' \ + f'since "{cell}" is not a Cell' + raise TypeError(msg) + + # If the Cell is in the Universe's list of Cells, delete it + self._cells.pop(cell.id, None) + + def sync_dagmc_cells(self, mats: Iterable[openmc.Material]): + """Synchronize DAGMC cell information between Python and C API + + .. versionadded:: 0.15.1 + + Parameters + ---------- + mats : iterable of openmc.Material + Iterable of materials to assign to the DAGMC cells + + """ + import openmc.lib + if not openmc.lib.is_initialized: + raise RuntimeError("This universe must be part of an openmc.Model " + "initialized via Model.init_lib before calling " + "this method.") + + dagmc_cell_ids = openmc.lib.dagmc.dagmc_universe_cell_ids(self.id) + if len(dagmc_cell_ids) != self.n_cells: + raise ValueError( + f"Number of cells in DAGMC universe {self.id} does not match " + f"the number of cells in the Python universe." + ) + + mats_per_id = {mat.id: mat for mat in mats} + for dag_cell_id in dagmc_cell_ids: + dag_cell = openmc.lib.cells[dag_cell_id] + if isinstance(dag_cell.fill, Iterable): + fill = [mats_per_id[mat.id] for mat in dag_cell.fill if mat] + else: + fill = mats_per_id[dag_cell.fill.id] if dag_cell.fill else None + self.add_cell(openmc.DAGMCCell(cell_id=dag_cell_id, fill=fill)) + + +class DAGMCCell(openmc.Cell): + """A cell class for DAGMC-based geometries. + + .. versionadded:: 0.15.1 + + Parameters + ---------- + cell_id : int or None, optional + Unique identifier for the cell. If None, an identifier will be + automatically assigned. + name : str, optional + Name of the cell. + fill : openmc.Material or None, optional + Material filling the cell. If None, the cell is filled with vacuum. + + Attributes + ---------- + DAG_parent_universe : int + The parent universe of the cell. + + """ + def __init__(self, cell_id=None, name='', fill=None): + super().__init__(cell_id, name, fill, None) + + @property + def DAG_parent_universe(self): + """Get the parent universe of the cell.""" + return self._parent_universe + + @DAG_parent_universe.setter + def DAG_parent_universe(self, universe): + """Set the parent universe of the cell.""" + self._parent_universe = universe.id + + def bounding_box(self): + return BoundingBox.infinite() + + def get_all_cells(self, memo=None): + return {} + + def get_all_universes(self, memo=None): + return {} + + def clone(self, clone_materials=True, clone_regions=True, memo=None): + warnings.warn("clone is not available for cells in a DAGMC universe") + return self + + def plot(self, *args, **kwargs): + raise TypeError("plot is not available for DAGMC cells.") + + def create_xml_subelement(self, xml_element, memo=None): + raise TypeError("create_xml_subelement is not available for DAGMC cells.") + + @classmethod + def from_xml_element(cls, elem, surfaces, materials, get_universe): + raise TypeError("from_xml_element is not available for DAGMC cells.") diff --git a/openmc/geometry.py b/openmc/geometry.py index a52dc4418d2..28e1e48eca2 100644 --- a/openmc/geometry.py +++ b/openmc/geometry.py @@ -217,7 +217,7 @@ def get_universe(univ_id): # Add any DAGMC universes for e in elem.findall('dagmc_universe'): - dag_univ = openmc.DAGMCUniverse.from_xml_element(e) + dag_univ = openmc.DAGMCUniverse.from_xml_element(e, mats) universes[dag_univ.id] = dag_univ # Dictionary that maps each universe to a list of cells/lattices that diff --git a/openmc/lib/__init__.py b/openmc/lib/__init__.py index 9bb2efb38af..5fe35b9745d 100644 --- a/openmc/lib/__init__.py +++ b/openmc/lib/__init__.py @@ -68,6 +68,7 @@ def _uwuw_enabled(): from .math import * from .plot import * from .weight_windows import * +from .dagmc import * # Flag to denote whether or not openmc.lib.init has been called # TODO: Establish and use a flag in the C++ code to represent the status of the diff --git a/openmc/lib/dagmc.py b/openmc/lib/dagmc.py new file mode 100644 index 00000000000..18ec81a4be8 --- /dev/null +++ b/openmc/lib/dagmc.py @@ -0,0 +1,43 @@ +from ctypes import c_int, c_int32, POINTER, c_size_t + +import numpy as np + +from . import _dll +from .error import _error_handler + + +__all__ = [ + 'dagmc_universe_cell_ids' +] + +# DAGMC functions +_dll.openmc_dagmc_universe_get_cell_ids.argtypes = [c_int32, POINTER(c_int32), POINTER(c_size_t)] +_dll.openmc_dagmc_universe_get_cell_ids.restype = c_int +_dll.openmc_dagmc_universe_get_cell_ids.errcheck = _error_handler +_dll.openmc_dagmc_universe_get_num_cells.argtypes = [c_int32, POINTER(c_size_t)] +_dll.openmc_dagmc_universe_get_num_cells.restype = c_int +_dll.openmc_dagmc_universe_get_num_cells.errcheck = _error_handler + + +def dagmc_universe_cell_ids(universe_id: int) -> np.ndarray: + """Return an array of cell IDs for a DAGMC universe. + + Parameters + ---------- + dagmc_id : int + ID of the DAGMC universe to get cell IDs from. + + Returns + ------- + numpy.ndarray + DAGMC cell IDs for the universe. + + """ + n = c_size_t() + _dll.openmc_dagmc_universe_get_num_cells(universe_id, n) + cell_ids = np.empty(n.value, dtype=np.int32) + + _dll.openmc_dagmc_universe_get_cell_ids( + universe_id, cell_ids.ctypes.data_as(POINTER(c_int32)), n + ) + return cell_ids diff --git a/openmc/model/model.py b/openmc/model/model.py index 49cb0a83ef5..5137ea1abfc 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1,7 +1,6 @@ from __future__ import annotations from collections.abc import Iterable from functools import lru_cache -import os from pathlib import Path from numbers import Integral from tempfile import NamedTemporaryFile @@ -15,8 +14,9 @@ import openmc._xml as xml from openmc.dummy_comm import DummyCommunicator from openmc.executor import _process_CLI_arguments -from openmc.checkvalue import check_type, check_value, PathLike +from openmc.checkvalue import check_type, check_value from openmc.exceptions import InvalidIDError +import openmc.lib from openmc.utility_funcs import change_directory @@ -324,6 +324,28 @@ def init_lib(self, threads=None, geometry_debug=False, restart_file=None, # communicator openmc.lib.init(args=args, intracomm=intracomm, output=output) + def sync_dagmc_universes(self): + """Synchronize all DAGMC universes in the current geometry. + + This method iterates over all DAGMC universes in the geometry and + synchronizes their cells with the current material assignments. Requires + that the model has been initialized via :meth:`Model.init_lib`. + + .. versionadded:: 0.15.1 + + """ + if self.is_initialized: + if self.materials: + materials = self.materials + else: + materials = list(self.geometry.get_all_materials().values()) + for univ in self.geometry.get_all_universes().values(): + if isinstance(univ, openmc.DAGMCUniverse): + univ.sync_dagmc_cells(materials) + else: + raise ValueError("The model must be initialized before calling " + "this method") + def finalize_lib(self): """Finalize simulation and free memory allocated for the C API @@ -1154,51 +1176,86 @@ def update_material_volumes(self, names_or_ids, volume): self._change_py_lib_attribs(names_or_ids, volume, 'material', 'volume') - def differentiate_depletable_mats(self, diff_volume_method: str): + def differentiate_depletable_mats(self, diff_volume_method: str = None): """Assign distribmats for each depletable material .. versionadded:: 0.14.0 + .. versionchanged:: 0.15.1 + diff_volume_method default is None, do not set volumes on the new + material ovjects. Is now a convenience method for + differentiate_mats(diff_volume_method, depletable_only=True) + + Parameters + ---------- + diff_volume_method : str + Specifies how the volumes of the new materials should be found. + - None: Do not assign volumes to the new materials (Default) + - 'divide_equally': Divide the original material volume equally between the new materials + - 'match cell': Set the volume of the material to the volume of the cell they fill + """ + self.differentiate_mats(diff_volume_method, depletable_only=True) + + def differentiate_mats(self, diff_volume_method: str = None, depletable_only: bool = True): + """Assign distribmats for each material + + .. versionadded:: 0.15.1 + Parameters ---------- diff_volume_method : str Specifies how the volumes of the new materials should be found. - Default is to 'divide equally' which divides the original material - volume equally between the new materials, 'match cell' sets the - volume of the material to volume of the cell they fill. + - None: Do not assign volumes to the new materials (Default) + - 'divide_equally': Divide the original material volume equally between the new materials + - 'match cell': Set the volume of the material to the volume of the cell they fill + depletable_only : bool + Default is True, only depletable materials will be differentiated. If False, all materials will be + differentiated. """ + check_value('volume differentiation method', diff_volume_method, ("divide equally", "match cell", None)) + # Count the number of instances for each cell and material self.geometry.determine_paths(instances_only=True) - # Extract all depletable materials which have multiple instances - distribmats = set( - [mat for mat in self.materials - if mat.depletable and mat.num_instances > 1]) - - if diff_volume_method == 'divide equally': - for mat in distribmats: - if mat.volume is None: - raise RuntimeError("Volume not specified for depletable " - f"material with ID={mat.id}.") - mat.volume /= mat.num_instances - - if distribmats: - # Assign distribmats to cells - for cell in self.geometry.get_all_material_cells().values(): - if cell.fill in distribmats: - mat = cell.fill - if diff_volume_method == 'divide equally': - cell.fill = [mat.clone() for _ in range(cell.num_instances)] - elif diff_volume_method == 'match cell': - for _ in range(cell.num_instances): - cell.fill = mat.clone() + # Find all or depletable_only materials which have multiple instance + distribmats = set() + for mat in self.materials: + # Differentiate all materials with multiple instances + diff_mat = mat.num_instances > 1 + # If depletable_only is True, differentiate only depletable materials + if depletable_only: + diff_mat = diff_mat and mat.depletable + if diff_mat: + # Assign volumes to the materials according to requirements + if diff_volume_method == "divide equally": + if mat.volume is None: + raise RuntimeError( + "Volume not specified for " + f"material with ID={mat.id}.") + else: + mat.volume /= mat.num_instances + elif diff_volume_method == "match cell": + for cell in self.geometry.get_all_material_cells().values(): + if cell.fill == mat: if not cell.volume: raise ValueError( f"Volume of cell ID={cell.id} not specified. " "Set volumes of cells prior to using " - "diff_volume_method='match cell'." - ) - cell.fill.volume = cell.volume + "diff_volume_method='match cell'.") + distribmats.add(mat) + + if not distribmats: + return + + # Assign distribmats to cells + for cell in self.geometry.get_all_material_cells().values(): + if cell.fill in distribmats: + mat = cell.fill + if diff_volume_method != 'match cell': + cell.fill = [mat.clone() for _ in range(cell.num_instances)] + elif diff_volume_method == 'match cell': + cell.fill = mat.clone() + cell.fill.volume = cell.volume if self.materials is not None: self.materials = openmc.Materials( diff --git a/openmc/universe.py b/openmc/universe.py index 648b773df6f..85ce6fd9656 100644 --- a/openmc/universe.py +++ b/openmc/universe.py @@ -2,22 +2,16 @@ import math from abc import ABC, abstractmethod from collections.abc import Iterable -from numbers import Integral, Real +from numbers import Real from pathlib import Path from tempfile import TemporaryDirectory import warnings -import h5py -import lxml.etree as ET import numpy as np import openmc import openmc.checkvalue as cv -from ._xml import get_text -from .checkvalue import check_type, check_value from .mixin import IDManagerMixin -from .surface import _BOUNDARY_TYPES -from .utility_funcs import input_path class UniverseBase(ABC, IDManagerMixin): @@ -55,6 +49,10 @@ def __repr__(self): def name(self): return self._name + @property + def cells(self): + return self._cells + @name.setter def name(self, name): if name is not None: @@ -135,6 +133,130 @@ def create_xml_subelement(self, xml_element, memo=None): """ + def _determine_paths(self, path='', instances_only=False): + """Count the number of instances for each cell in the universe, and + record the count in the :attr:`Cell.num_instances` properties.""" + + univ_path = path + f'u{self.id}' + + for cell in self.cells.values(): + cell_path = f'{univ_path}->c{cell.id}' + fill = cell._fill + fill_type = cell.fill_type + + # If universe-filled, recursively count cells in filling universe + if fill_type == 'universe': + fill._determine_paths(cell_path + '->', instances_only) + # If lattice-filled, recursively call for all universes in lattice + elif fill_type == 'lattice': + latt = fill + + # Count instances in each universe in the lattice + for index in latt._natural_indices: + latt_path = '{}->l{}({})->'.format( + cell_path, latt.id, ",".join(str(x) for x in index)) + univ = latt.get_universe(index) + univ._determine_paths(latt_path, instances_only) + + else: + if fill_type == 'material': + mat = fill + elif fill_type == 'distribmat': + mat = fill[cell._num_instances] + else: + mat = None + + if mat is not None: + mat._num_instances += 1 + if not instances_only: + mat._paths.append(f'{cell_path}->m{mat.id}') + + # Append current path + cell._num_instances += 1 + if not instances_only: + cell._paths.append(cell_path) + + def add_cells(self, cells): + """Add multiple cells to the universe. + + Parameters + ---------- + cells : Iterable of openmc.Cell + Cells to add + + """ + + if not isinstance(cells, Iterable): + msg = f'Unable to add Cells to Universe ID="{self._id}" since ' \ + f'"{cells}" is not iterable' + raise TypeError(msg) + + for cell in cells: + self.add_cell(cell) + + @abstractmethod + def add_cell(self, cell): + pass + + @abstractmethod + def remove_cell(self, cell): + pass + + def clear_cells(self): + """Remove all cells from the universe.""" + + self._cells.clear() + + def get_all_cells(self, memo=None): + """Return all cells that are contained within the universe + + Returns + ------- + cells : dict + Dictionary whose keys are cell IDs and values are :class:`Cell` + instances + + """ + + if memo is None: + memo = set() + elif self in memo: + return {} + memo.add(self) + + # Add this Universe's cells to the dictionary + cells = {} + cells.update(self._cells) + + # Append all Cells in each Cell in the Universe to the dictionary + for cell in self._cells.values(): + cells.update(cell.get_all_cells(memo)) + + return cells + + def get_all_materials(self, memo=None): + """Return all materials that are contained within the universe + + Returns + ------- + materials : dict + Dictionary whose keys are material IDs and values are + :class:`Material` instances + + """ + + if memo is None: + memo = set() + + materials = {} + + # Append all Cells in each Cell in the Universe to the dictionary + cells = self.get_all_cells(memo) + for cell in cells.values(): + materials.update(cell.get_all_materials(memo)) + + return materials + @abstractmethod def _partial_deepcopy(self): """Deepcopy all parameters of an openmc.UniverseBase object except its cells. @@ -182,93 +304,6 @@ def clone(self, clone_materials=True, clone_regions=True, memo=None): return memo[self] - -class Universe(UniverseBase): - """A collection of cells that can be repeated. - - Parameters - ---------- - universe_id : int, optional - Unique identifier of the universe. If not specified, an identifier will - automatically be assigned - name : str, optional - Name of the universe. If not specified, the name is the empty string. - cells : Iterable of openmc.Cell, optional - Cells to add to the universe. By default no cells are added. - - Attributes - ---------- - id : int - Unique identifier of the universe - name : str - Name of the universe - cells : dict - Dictionary whose keys are cell IDs and values are :class:`Cell` - instances - volume : float - Volume of the universe in cm^3. This can either be set manually or - calculated in a stochastic volume calculation and added via the - :meth:`Universe.add_volume_information` method. - bounding_box : openmc.BoundingBox - Lower-left and upper-right coordinates of an axis-aligned bounding box - of the universe. - - """ - - def __init__(self, universe_id=None, name='', cells=None): - super().__init__(universe_id, name) - - if cells is not None: - self.add_cells(cells) - - def __repr__(self): - string = super().__repr__() - string += '{: <16}=\t{}\n'.format('\tGeom', 'CSG') - string += '{: <16}=\t{}\n'.format('\tCells', list(self._cells.keys())) - return string - - @property - def cells(self): - return self._cells - - @property - def bounding_box(self) -> openmc.BoundingBox: - regions = [c.region for c in self.cells.values() - if c.region is not None] - if regions: - return openmc.Union(regions).bounding_box - else: - return openmc.BoundingBox.infinite() - - @classmethod - def from_hdf5(cls, group, cells): - """Create universe from HDF5 group - - Parameters - ---------- - group : h5py.Group - Group in HDF5 file - cells : dict - Dictionary mapping cell IDs to instances of :class:`openmc.Cell`. - - Returns - ------- - openmc.Universe - Universe instance - - """ - universe_id = int(group.name.split('/')[-1].lstrip('universe ')) - cell_ids = group['cells'][()] - - # Create this Universe - universe = cls(universe_id) - - # Add each Cell to the Universe - for cell_id in cell_ids: - universe.add_cell(cells[cell_id]) - - return universe - def find(self, point): """Find cells/universes/lattices which contain a given point @@ -528,98 +563,37 @@ def plot(self, origin=None, width=None, pixels=40000, axes.imshow(img, extent=(x_min, x_max, y_min, y_max), **kwargs) return axes - def add_cell(self, cell): - """Add a cell to the universe. + def get_nuclides(self): + """Returns all nuclides in the universe - Parameters - ---------- - cell : openmc.Cell - Cell to add + Returns + ------- + nuclides : list of str + List of nuclide names """ - if not isinstance(cell, openmc.Cell): - msg = f'Unable to add a Cell to Universe ID="{self._id}" since ' \ - f'"{cell}" is not a Cell' - raise TypeError(msg) + nuclides = [] - cell_id = cell.id + # Append all Nuclides in each Cell in the Universe to the dictionary + for cell in self.cells.values(): + for nuclide in cell.get_nuclides(): + if nuclide not in nuclides: + nuclides.append(nuclide) - if cell_id not in self._cells: - self._cells[cell_id] = cell + return nuclides - def add_cells(self, cells): - """Add multiple cells to the universe. + def get_nuclide_densities(self): + """Return all nuclides contained in the universe - Parameters - ---------- - cells : Iterable of openmc.Cell - Cells to add + Returns + ------- + nuclides : dict + Dictionary whose keys are nuclide names and values are 2-tuples of + (nuclide, density) """ - - if not isinstance(cells, Iterable): - msg = f'Unable to add Cells to Universe ID="{self._id}" since ' \ - f'"{cells}" is not iterable' - raise TypeError(msg) - - for cell in cells: - self.add_cell(cell) - - def remove_cell(self, cell): - """Remove a cell from the universe. - - Parameters - ---------- - cell : openmc.Cell - Cell to remove - - """ - - if not isinstance(cell, openmc.Cell): - msg = f'Unable to remove a Cell from Universe ID="{self._id}" ' \ - f'since "{cell}" is not a Cell' - raise TypeError(msg) - - # If the Cell is in the Universe's list of Cells, delete it - self._cells.pop(cell.id, None) - - def clear_cells(self): - """Remove all cells from the universe.""" - - self._cells.clear() - - def get_nuclides(self): - """Returns all nuclides in the universe - - Returns - ------- - nuclides : list of str - List of nuclide names - - """ - - nuclides = [] - - # Append all Nuclides in each Cell in the Universe to the dictionary - for cell in self.cells.values(): - for nuclide in cell.get_nuclides(): - if nuclide not in nuclides: - nuclides.append(nuclide) - - return nuclides - - def get_nuclide_densities(self): - """Return all nuclides contained in the universe - - Returns - ------- - nuclides : dict - Dictionary whose keys are nuclide names and values are 2-tuples of - (nuclide, density) - - """ - nuclides = {} + nuclides = {} if self._atoms: volume = self.volume @@ -636,150 +610,20 @@ def get_nuclide_densities(self): return nuclides - def get_all_cells(self, memo=None): - """Return all cells that are contained within the universe - - Returns - ------- - cells : dict - Dictionary whose keys are cell IDs and values are :class:`Cell` - instances - - """ - - if memo is None: - memo = set() - elif self in memo: - return {} - memo.add(self) - - # Add this Universe's cells to the dictionary - cells = {} - cells.update(self._cells) - - # Append all Cells in each Cell in the Universe to the dictionary - for cell in self._cells.values(): - cells.update(cell.get_all_cells(memo)) - - return cells - - def get_all_materials(self, memo=None): - """Return all materials that are contained within the universe - - Returns - ------- - materials : dict - Dictionary whose keys are material IDs and values are - :class:`Material` instances - - """ - - if memo is None: - memo = set() - - materials = {} - - # Append all Cells in each Cell in the Universe to the dictionary - cells = self.get_all_cells(memo) - for cell in cells.values(): - materials.update(cell.get_all_materials(memo)) - - return materials - - def create_xml_subelement(self, xml_element, memo=None): - if memo is None: - memo = set() - - # Iterate over all Cells - for cell in self._cells.values(): - - # If the cell was already written, move on - if cell in memo: - continue - - memo.add(cell) - - # Create XML subelement for this Cell - cell_element = cell.create_xml_subelement(xml_element, memo) - - # Append the Universe ID to the subelement and add to Element - cell_element.set("universe", str(self._id)) - xml_element.append(cell_element) - - def _determine_paths(self, path='', instances_only=False): - """Count the number of instances for each cell in the universe, and - record the count in the :attr:`Cell.num_instances` properties.""" - - univ_path = path + f'u{self.id}' - - for cell in self.cells.values(): - cell_path = f'{univ_path}->c{cell.id}' - fill = cell._fill - fill_type = cell.fill_type - - # If universe-filled, recursively count cells in filling universe - if fill_type == 'universe': - fill._determine_paths(cell_path + '->', instances_only) - - # If lattice-filled, recursively call for all universes in lattice - elif fill_type == 'lattice': - latt = fill - - # Count instances in each universe in the lattice - for index in latt._natural_indices: - latt_path = '{}->l{}({})->'.format( - cell_path, latt.id, ",".join(str(x) for x in index)) - univ = latt.get_universe(index) - univ._determine_paths(latt_path, instances_only) - - else: - if fill_type == 'material': - mat = fill - elif fill_type == 'distribmat': - mat = fill[cell._num_instances] - else: - mat = None - - if mat is not None: - mat._num_instances += 1 - if not instances_only: - mat._paths.append(f'{cell_path}->m{mat.id}') - - # Append current path - cell._num_instances += 1 - if not instances_only: - cell._paths.append(cell_path) - - def _partial_deepcopy(self): - """Clone all of the openmc.Universe object's attributes except for its cells, - as they are copied within the clone function. This should only to be - used within the openmc.UniverseBase.clone() context. - """ - clone = openmc.Universe(name=self.name) - clone.volume = self.volume - return clone - -class DAGMCUniverse(UniverseBase): - """A reference to a DAGMC file to be used in the model. - .. versionadded:: 0.13.0 +class Universe(UniverseBase): + """A collection of cells that can be repeated. Parameters ---------- - filename : path-like - Path to the DAGMC file used to represent this universe. universe_id : int, optional Unique identifier of the universe. If not specified, an identifier will - automatically be assigned. + automatically be assigned name : str, optional Name of the universe. If not specified, the name is the empty string. - auto_geom_ids : bool - Set IDs automatically on initialization (True) or report overlaps in ID - space between CSG and DAGMC (False) - auto_mat_ids : bool - Set IDs automatically on initialization (True) or report overlaps in ID - space between OpenMC and UWUW materials (False) + cells : Iterable of openmc.Cell, optional + Cells to add to the universe. By default no cells are added. Attributes ---------- @@ -787,334 +631,133 @@ class DAGMCUniverse(UniverseBase): Unique identifier of the universe name : str Name of the universe - filename : str - Path to the DAGMC file used to represent this universe. - auto_geom_ids : bool - Set IDs automatically on initialization (True) or report overlaps in ID - space between CSG and DAGMC (False) - auto_mat_ids : bool - Set IDs automatically on initialization (True) or report overlaps in ID - space between OpenMC and UWUW materials (False) + cells : dict + Dictionary whose keys are cell IDs and values are :class:`Cell` + instances + volume : float + Volume of the universe in cm^3. This can either be set manually or + calculated in a stochastic volume calculation and added via the + :meth:`Universe.add_volume_information` method. bounding_box : openmc.BoundingBox Lower-left and upper-right coordinates of an axis-aligned bounding box of the universe. - .. versionadded:: 0.13.1 - material_names : list of str - Return a sorted list of materials names that are contained within the - DAGMC h5m file. This is useful when naming openmc.Material() objects - as each material name present in the DAGMC h5m file must have a - matching openmc.Material() with the same name. - - .. versionadded:: 0.13.2 - n_cells : int - The number of cells in the DAGMC model. This is the number of cells at - runtime and accounts for the implicit complement whether or not is it - present in the DAGMC file. - - .. versionadded:: 0.13.2 - n_surfaces : int - The number of surfaces in the model. - - .. versionadded:: 0.13.2 - """ - def __init__(self, - filename: cv.PathLike, - universe_id=None, - name='', - auto_geom_ids=False, - auto_mat_ids=False): + def __init__(self, universe_id=None, name='', cells=None): super().__init__(universe_id, name) - # Initialize class attributes - self.filename = filename - self.auto_geom_ids = auto_geom_ids - self.auto_mat_ids = auto_mat_ids + + if cells is not None: + self.add_cells(cells) def __repr__(self): string = super().__repr__() - string += '{: <16}=\t{}\n'.format('\tGeom', 'DAGMC') - string += '{: <16}=\t{}\n'.format('\tFile', self.filename) + string += '{: <16}=\t{}\n'.format('\tGeom', 'CSG') + string += '{: <16}=\t{}\n'.format('\tCells', list(self._cells.keys())) return string @property - def bounding_box(self): - with h5py.File(self.filename) as dagmc_file: - coords = dagmc_file['tstt']['nodes']['coordinates'][()] - lower_left_corner = coords.min(axis=0) - upper_right_corner = coords.max(axis=0) - return openmc.BoundingBox(lower_left_corner, upper_right_corner) - - @property - def filename(self): - return self._filename - - @filename.setter - def filename(self, val: cv.PathLike): - cv.check_type('DAGMC filename', val, cv.PathLike) - self._filename = input_path(val) - - @property - def auto_geom_ids(self): - return self._auto_geom_ids - - @auto_geom_ids.setter - def auto_geom_ids(self, val): - cv.check_type('DAGMC automatic geometry ids', val, bool) - self._auto_geom_ids = val - - @property - def auto_mat_ids(self): - return self._auto_mat_ids - - @auto_mat_ids.setter - def auto_mat_ids(self, val): - cv.check_type('DAGMC automatic material ids', val, bool) - self._auto_mat_ids = val - - @property - def material_names(self): - dagmc_file_contents = h5py.File(self.filename) - material_tags_hex = dagmc_file_contents['/tstt/tags/NAME'].get( - 'values') - material_tags_ascii = [] - for tag in material_tags_hex: - candidate_tag = tag.tobytes().decode().replace('\x00', '') - # tags might be for temperature or reflective surfaces - if candidate_tag.startswith('mat:'): - # removes first 4 characters as openmc.Material name should be - # set without the 'mat:' part of the tag - material_tags_ascii.append(candidate_tag[4:]) - - return sorted(set(material_tags_ascii)) - - def get_all_cells(self, memo=None): - return {} - - def get_all_materials(self, memo=None): - return {} + def bounding_box(self) -> openmc.BoundingBox: + regions = [c.region for c in self.cells.values() + if c.region is not None] + if regions: + return openmc.Union(regions).bounding_box + else: + return openmc.BoundingBox.infinite() - def _n_geom_elements(self, geom_type): - """ - Helper function for retrieving the number geometric entities in a DAGMC - file + @classmethod + def from_hdf5(cls, group, cells): + """Create universe from HDF5 group Parameters ---------- - geom_type : str - The type of geometric entity to count. One of {'Volume', 'Surface'}. Returns - the runtime number of voumes in the DAGMC model (includes implicit complement). + group : h5py.Group + Group in HDF5 file + cells : dict + Dictionary mapping cell IDs to instances of :class:`openmc.Cell`. Returns ------- - int - Number of geometry elements of the specified type - """ - cv.check_value('geometry type', geom_type, ('volume', 'surface')) - - def decode_str_tag(tag_val): - return tag_val.tobytes().decode().replace('\x00', '') - - with h5py.File(self.filename) as dagmc_file: - category_data = dagmc_file['tstt/tags/CATEGORY/values'] - category_strs = map(decode_str_tag, category_data) - n = sum([v == geom_type.capitalize() for v in category_strs]) - - # check for presence of an implicit complement in the file and - # increment the number of cells if it doesn't exist - if geom_type == 'volume': - name_data = dagmc_file['tstt/tags/NAME/values'] - name_strs = map(decode_str_tag, name_data) - if not sum(['impl_complement' in n for n in name_strs]): - n += 1 - return n + openmc.Universe + Universe instance - @property - def n_cells(self): - return self._n_geom_elements('volume') + """ + universe_id = int(group.name.split('/')[-1].lstrip('universe ')) + cell_ids = group['cells'][()] - @property - def n_surfaces(self): - return self._n_geom_elements('surface') + # Create this Universe + universe = cls(universe_id) - def create_xml_subelement(self, xml_element, memo=None): - if memo is None: - memo = set() + # Add each Cell to the Universe + for cell_id in cell_ids: + universe.add_cell(cells[cell_id]) - if self in memo: - return + return universe - memo.add(self) - # Set xml element values - dagmc_element = ET.Element('dagmc_universe') - dagmc_element.set('id', str(self.id)) - - if self.auto_geom_ids: - dagmc_element.set('auto_geom_ids', 'true') - if self.auto_mat_ids: - dagmc_element.set('auto_mat_ids', 'true') - dagmc_element.set('filename', str(self.filename)) - xml_element.append(dagmc_element) - - def bounding_region( - self, - bounded_type: str = 'box', - boundary_type: str = 'vacuum', - starting_id: int = 10000, - padding_distance: float = 0. - ): - """Creates a either a spherical or box shaped bounding region around - the DAGMC geometry. - - .. versionadded:: 0.13.1 + def add_cell(self, cell): + """Add a cell to the universe. Parameters ---------- - bounded_type : str - The type of bounding surface(s) to use when constructing the region. - Options include a single spherical surface (sphere) or a rectangle - made from six planes (box). - boundary_type : str - Boundary condition that defines the behavior for particles hitting - the surface. Defaults to vacuum boundary condition. Passed into the - surface construction. - starting_id : int - Starting ID of the surface(s) used in the region. For bounded_type - 'box', the next 5 IDs will also be used. Defaults to 10000 to reduce - the chance of an overlap of surface IDs with the DAGMC geometry. - padding_distance : float - Distance between the bounding region surfaces and the minimal - bounding box. Allows for the region to be larger than the DAGMC - geometry. + cell : openmc.Cell + Cell to add - Returns - ------- - openmc.Region - Region instance """ - check_type('boundary type', boundary_type, str) - check_value('boundary type', boundary_type, _BOUNDARY_TYPES) - check_type('starting surface id', starting_id, Integral) - check_type('bounded type', bounded_type, str) - check_value('bounded type', bounded_type, ('box', 'sphere')) - - bbox = self.bounding_box.expand(padding_distance, True) - - if bounded_type == 'sphere': - radius = np.linalg.norm(bbox.upper_right - bbox.center) - bounding_surface = openmc.Sphere( - surface_id=starting_id, - x0=bbox.center[0], - y0=bbox.center[1], - z0=bbox.center[2], - boundary_type=boundary_type, - r=radius, - ) - - return -bounding_surface - - if bounded_type == 'box': - # defines plane surfaces for all six faces of the bounding box - lower_x = openmc.XPlane(bbox[0][0], surface_id=starting_id) - upper_x = openmc.XPlane(bbox[1][0], surface_id=starting_id+1) - lower_y = openmc.YPlane(bbox[0][1], surface_id=starting_id+2) - upper_y = openmc.YPlane(bbox[1][1], surface_id=starting_id+3) - lower_z = openmc.ZPlane(bbox[0][2], surface_id=starting_id+4) - upper_z = openmc.ZPlane(bbox[1][2], surface_id=starting_id+5) - - region = +lower_x & -upper_x & +lower_y & -upper_y & +lower_z & -upper_z - - for surface in region.get_surfaces().values(): - surface.boundary_type = boundary_type - - return region - - def bounded_universe(self, bounding_cell_id=10000, **kwargs): - """Returns an openmc.Universe filled with this DAGMCUniverse and bounded - with a cell. Defaults to a box cell with a vacuum surface however this - can be changed using the kwargs which are passed directly to - DAGMCUniverse.bounding_region(). + if not isinstance(cell, openmc.Cell): + msg = f'Unable to add a Cell to Universe ID="{self._id}" since ' \ + f'"{cell}" is not a Cell' + raise TypeError(msg) - Parameters - ---------- - bounding_cell_id : int - The cell ID number to use for the bounding cell, defaults to 10000 to reduce - the chance of overlapping ID numbers with the DAGMC geometry. + cell_id = cell.id - Returns - ------- - openmc.Universe - Universe instance - """ - bounding_cell = openmc.Cell( - fill=self, cell_id=bounding_cell_id, region=self.bounding_region(**kwargs)) - return openmc.Universe(cells=[bounding_cell]) + if cell_id not in self._cells: + self._cells[cell_id] = cell - @classmethod - def from_hdf5(cls, group): - """Create DAGMC universe from HDF5 group + def remove_cell(self, cell): + """Remove a cell from the universe. Parameters ---------- - group : h5py.Group - Group in HDF5 file - - Returns - ------- - openmc.DAGMCUniverse - DAGMCUniverse instance + cell : openmc.Cell + Cell to remove """ - id = int(group.name.split('/')[-1].lstrip('universe ')) - fname = group['filename'][()].decode() - name = group['name'][()].decode() if 'name' in group else None - - out = cls(fname, universe_id=id, name=name) - - out.auto_geom_ids = bool(group.attrs['auto_geom_ids']) - out.auto_mat_ids = bool(group.attrs['auto_mat_ids']) - return out - - @classmethod - def from_xml_element(cls, elem): - """Generate DAGMC universe from XML element + if not isinstance(cell, openmc.Cell): + msg = f'Unable to remove a Cell from Universe ID="{self._id}" ' \ + f'since "{cell}" is not a Cell' + raise TypeError(msg) - Parameters - ---------- - elem : lxml.etree._Element - `` element + # If the Cell is in the Universe's list of Cells, delete it + self._cells.pop(cell.id, None) - Returns - ------- - openmc.DAGMCUniverse - DAGMCUniverse instance + def create_xml_subelement(self, xml_element, memo=None): + if memo is None: + memo = set() - """ - id = int(get_text(elem, 'id')) - fname = get_text(elem, 'filename') + # Iterate over all Cells + for cell in self._cells.values(): - out = cls(fname, universe_id=id) + # If the cell was already written, move on + if cell in memo: + continue - name = get_text(elem, 'name') - if name is not None: - out.name = name + memo.add(cell) - out.auto_geom_ids = bool(elem.get('auto_geom_ids')) - out.auto_mat_ids = bool(elem.get('auto_mat_ids')) + # Create XML subelement for this Cell + cell_element = cell.create_xml_subelement(xml_element, memo) - return out + # Append the Universe ID to the subelement and add to Element + cell_element.set("universe", str(self._id)) + xml_element.append(cell_element) def _partial_deepcopy(self): - """Clone all of the openmc.DAGMCUniverse object's attributes except for - its cells, as they are copied within the clone function. This should - only to be used within the openmc.UniverseBase.clone() context. + """Clone all of the openmc.Universe object's attributes except for its cells, + as they are copied within the clone function. This should only to be + used within the openmc.UniverseBase.clone() context. """ - clone = openmc.DAGMCUniverse(name=self.name, filename=self.filename) + clone = openmc.Universe(name=self.name) clone.volume = self.volume - clone.auto_geom_ids = self.auto_geom_ids - clone.auto_mat_ids = self.auto_mat_ids return clone diff --git a/src/cell.cpp b/src/cell.cpp index 88876678706..d4d28fb70e6 100644 --- a/src/cell.cpp +++ b/src/cell.cpp @@ -252,12 +252,12 @@ void Cell::to_hdf5(hid_t cell_group) const // default constructor CSGCell::CSGCell() { - geom_type_ = GeometryType::CSG; + geom_type() = GeometryType::CSG; } CSGCell::CSGCell(pugi::xml_node cell_node) { - geom_type_ = GeometryType::CSG; + geom_type() = GeometryType::CSG; if (check_for_node(cell_node, "id")) { id_ = std::stoi(get_node_value(cell_node, "id")); diff --git a/src/dagmc.cpp b/src/dagmc.cpp index b79676c3626..134e31ecf3c 100644 --- a/src/dagmc.cpp +++ b/src/dagmc.cpp @@ -72,6 +72,23 @@ DAGUniverse::DAGUniverse(pugi::xml_node node) adjust_material_ids_ = get_node_value_bool(node, "auto_mat_ids"); } + // get material assignment overloading + if (check_for_node(node, "material_overrides")) { + auto mat_node = node.child("material_overrides"); + // loop over all subelements (each subelement corresponds to a material) + for (pugi::xml_node cell_node : mat_node.children("cell_override")) { + // Store assignment reference name + int32_t ref_assignment = std::stoi(get_node_value(cell_node, "id")); + + // Get mat name for each assignement instances + vector instance_mats = + get_node_array(cell_node, "material_ids"); + + // Store mat name for each instances + material_overrides_.emplace(ref_assignment, instance_mats); + } + } + initialize(); } @@ -211,12 +228,13 @@ void DAGUniverse::init_geometry() if (mat_str == "graveyard") { graveyard = vol_handle; } - // material void checks if (mat_str == "void" || mat_str == "vacuum" || mat_str == "graveyard") { c->material_.push_back(MATERIAL_VOID); } else { - if (uses_uwuw()) { + if (material_overrides_.count(c->id_)) { + override_assign_material(c); + } else if (uses_uwuw()) { uwuw_assign_material(vol_handle, c); } else { legacy_assign_material(mat_str, c); @@ -609,6 +627,33 @@ void DAGUniverse::uwuw_assign_material( fatal_error("DAGMC was not configured with UWUW."); #endif // OPENMC_UWUW } + +void DAGUniverse::override_assign_material(std::unique_ptr& c) const +{ + // if Cell ID matches an override key, use it to override the material + // assignment else if UWUW is used, get the material assignment from the DAGMC + // metadata + // Notify User that an override is being applied on a DAGMCCell + write_message(fmt::format("Applying override for DAGMCCell {}", c->id_), 8); + + if (settings::verbosity >= 10) { + auto msg = fmt::format("Assigning DAGMC cell {} material(s) based on " + "override information (see input XML).", + c->id_); + write_message(msg, 10); + } + + // Override the material assignment for each cell instance using the legacy + // assignement + for (auto mat_id : material_overrides_.at(c->id_)) { + if (model::material_map.find(mat_id) == model::material_map.end()) { + fatal_error(fmt::format( + "Material with ID '{}' not found for DAGMC cell {}", mat_id, c->id_)); + } + c->material_.push_back(mat_id); + } +} + //============================================================================== // DAGMC Cell implementation //============================================================================== @@ -616,7 +661,7 @@ void DAGUniverse::uwuw_assign_material( DAGCell::DAGCell(std::shared_ptr dag_ptr, int32_t dag_idx) : Cell {}, dagmc_ptr_(dag_ptr), dag_index_(dag_idx) { - geom_type_ = GeometryType::DAG; + geom_type() = GeometryType::DAG; }; std::pair DAGCell::distance( @@ -719,7 +764,7 @@ BoundingBox DAGCell::bounding_box() const DAGSurface::DAGSurface(std::shared_ptr dag_ptr, int32_t dag_idx) : Surface {}, dagmc_ptr_(dag_ptr), dag_index_(dag_idx) { - geom_type_ = GeometryType::DAG; + geom_type() = GeometryType::DAG; } // empty constructor moab::EntityHandle DAGSurface::mesh_handle() const @@ -818,12 +863,61 @@ int32_t next_cell(int32_t surf, int32_t curr_cell, int32_t univ) return univp->cell_index(new_vol); } +extern "C" int openmc_dagmc_universe_get_cell_ids( + int32_t univ_id, int32_t* ids, size_t* n) +{ + // make sure the universe id is a DAGMC Universe + const auto& univ = model::universes[model::universe_map[univ_id]]; + if (univ->geom_type() != GeometryType::DAG) { + set_errmsg(fmt::format("Universe {} is not a DAGMC Universe", univ_id)); + return OPENMC_E_INVALID_TYPE; + } + + std::vector dag_cell_ids; + for (const auto& cell_index : univ->cells_) { + const auto& cell = model::cells[cell_index]; + if (cell->geom_type() == GeometryType::CSG) { + set_errmsg(fmt::format("Cell {} is not a DAGMC Cell", cell->id_)); + return OPENMC_E_INVALID_TYPE; + } + dag_cell_ids.push_back(cell->id_); + } + std::copy(dag_cell_ids.begin(), dag_cell_ids.end(), ids); + *n = dag_cell_ids.size(); + return 0; +} + +extern "C" int openmc_dagmc_universe_get_num_cells(int32_t univ_id, size_t* n) +{ + // make sure the universe id is a DAGMC Universe + const auto& univ = model::universes[model::universe_map[univ_id]]; + if (univ->geom_type() != GeometryType::DAG) { + set_errmsg(fmt::format("Universe {} is not a DAGMC universe", univ_id)); + return OPENMC_E_INVALID_TYPE; + } + *n = univ->cells_.size(); + return 0; +} + } // namespace openmc #else namespace openmc { +extern "C" int openmc_dagmc_universe_get_cell_ids( + int32_t univ_id, int32_t* ids, size_t* n) +{ + set_errmsg("OpenMC was not configured with DAGMC"); + return OPENMC_E_UNASSIGNED; +}; + +extern "C" int openmc_dagmc_universe_get_num_cells(int32_t univ_id, size_t* n) +{ + set_errmsg("OpenMC was not configured with DAGMC"); + return OPENMC_E_UNASSIGNED; +}; + void read_dagmc_universes(pugi::xml_node node) { if (check_for_node(node, "dagmc_universe")) { diff --git a/src/particle.cpp b/src/particle.cpp index 64c50c9438f..0ea8650143d 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -533,7 +533,7 @@ void Particle::cross_surface(const Surface& surf) // if we're crossing a CSG surface, make sure the DAG history is reset #ifdef DAGMC - if (surf.geom_type_ == GeometryType::CSG) + if (surf.geom_type() == GeometryType::CSG) history().reset(); #endif @@ -548,7 +548,7 @@ void Particle::cross_surface(const Surface& surf) #ifdef DAGMC // in DAGMC, we know what the next cell should be - if (surf.geom_type_ == GeometryType::DAG) { + if (surf.geom_type() == GeometryType::DAG) { int32_t i_cell = next_cell(std::abs(surface()), cell_last(n_coord() - 1), lowest_coord().universe) - 1; @@ -668,7 +668,8 @@ void Particle::cross_reflective_bc(const Surface& surf, Direction new_u) // the lower universes. // (unless we're using a dagmc model, which has exactly one universe) n_coord() = 1; - if (surf.geom_type_ != GeometryType::DAG && !neighbor_list_find_cell(*this)) { + if (surf.geom_type() != GeometryType::DAG && + !neighbor_list_find_cell(*this)) { mark_as_lost("Couldn't find particle after reflecting from surface " + std::to_string(surf.id_) + "."); return; diff --git a/src/plot.cpp b/src/plot.cpp index de7d475e1c5..85131d67a01 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -1303,7 +1303,7 @@ void ProjectionPlot::create_output() const int32_t i_surface = std::abs(p.surface()) - 1; if (i_surface > 0 && - model::surfaces[i_surface]->geom_type_ == GeometryType::DAG) { + model::surfaces[i_surface]->geom_type() == GeometryType::DAG) { #ifdef DAGMC int32_t i_cell = next_cell(i_surface, p.cell_last(p.n_coord() - 1), p.lowest_coord().universe); diff --git a/src/surface.cpp b/src/surface.cpp index 50ef2a12830..dbcaf849848 100644 --- a/src/surface.cpp +++ b/src/surface.cpp @@ -165,9 +165,9 @@ void Surface::to_hdf5(hid_t group_id) const { hid_t surf_group = create_group(group_id, fmt::format("surface {}", id_)); - if (geom_type_ == GeometryType::DAG) { + if (geom_type() == GeometryType::DAG) { write_string(surf_group, "geom_type", "dagmc", false); - } else if (geom_type_ == GeometryType::CSG) { + } else if (geom_type() == GeometryType::CSG) { write_string(surf_group, "geom_type", "csg", false); if (bc_) { @@ -189,11 +189,11 @@ void Surface::to_hdf5(hid_t group_id) const CSGSurface::CSGSurface() : Surface {} { - geom_type_ = GeometryType::CSG; + geom_type() = GeometryType::CSG; }; CSGSurface::CSGSurface(pugi::xml_node surf_node) : Surface {surf_node} { - geom_type_ = GeometryType::CSG; + geom_type() = GeometryType::CSG; }; //============================================================================== diff --git a/tests/unit_tests/dagmc/test_model.py b/tests/unit_tests/dagmc/test_model.py new file mode 100644 index 00000000000..9fdfbcebc68 --- /dev/null +++ b/tests/unit_tests/dagmc/test_model.py @@ -0,0 +1,256 @@ +from pathlib import Path + +import lxml.etree as ET +import numpy as np +import pytest +import openmc +from openmc.utility_funcs import change_directory + +pytestmark = pytest.mark.skipif( + not openmc.lib._dagmc_enabled(), + reason="DAGMC CAD geometry is not enabled.") + + +@pytest.fixture() +def model(request): + pitch = 1.26 + + mats = {} + mats["no-void fuel"] = openmc.Material(1, name="no-void fuel") + mats["no-void fuel"].add_nuclide("U235", 0.03) + mats["no-void fuel"].add_nuclide("U238", 0.97) + mats["no-void fuel"].add_nuclide("O16", 2.0) + mats["no-void fuel"].set_density("g/cm3", 10.0) + + mats["41"] = openmc.Material(name="41") + mats["41"].add_nuclide("H1", 2.0) + mats["41"].add_element("O", 1.0) + mats["41"].set_density("g/cm3", 1.0) + mats["41"].add_s_alpha_beta("c_H_in_H2O") + + p = Path(request.fspath).parent / "dagmc.h5m" + + daguniv = openmc.DAGMCUniverse(p, auto_geom_ids=True) + + lattice = openmc.RectLattice() + lattice.dimension = [2, 2] + lattice.lower_left = [-pitch, -pitch] + lattice.pitch = [pitch, pitch] + lattice.universes = [ + [daguniv, daguniv], + [daguniv, daguniv]] + + box = openmc.model.RectangularParallelepiped(-pitch, pitch, -pitch, pitch, -5, 5) + + root = openmc.Universe(cells=[openmc.Cell(region=-box, fill=lattice)]) + + settings = openmc.Settings() + settings.batches = 100 + settings.inactive = 10 + settings.particles = 1000 + + ll, ur = root.bounding_box + mat_vol = openmc.VolumeCalculation([mats["no-void fuel"]], 1000000, ll, ur) + cell_vol = openmc.VolumeCalculation(list(root.cells.values()), 1000000, ll, ur) + settings.volume_calculations = [mat_vol, cell_vol] + + model = openmc.Model() + model.materials = openmc.Materials(mats.values()) + model.geometry = openmc.Geometry(root=root) + model.settings = settings + + with change_directory(tmpdir=True): + try: + model.init_lib() + model.sync_dagmc_universes() + yield model + finally: + model.finalize_lib() + openmc.reset_auto_ids() + + +def test_dagmc_replace_material_assignment(model): + mats = {} + + mats["foo"] = openmc.Material(name="foo") + mats["foo"].add_nuclide("H1", 2.0) + mats["foo"].add_element("O", 1.0) + mats["foo"].set_density("g/cm3", 1.0) + mats["foo"].add_s_alpha_beta("c_H_in_H2O") + + for univ in model.geometry.get_all_universes().values(): + if not isinstance(univ, openmc.DAGMCUniverse): + break + + cells_with_41 = [] + for cell in univ.cells.values(): + if cell.fill is None: + continue + if cell.fill.name == "41": + cells_with_41.append(cell.id) + univ.replace_material_assignment("41", mats["foo"]) + for cell_id in cells_with_41: + assert univ.cells[cell_id] == mats["foo"] + + +def test_dagmc_add_material_override_with_id(model): + mats = {} + mats["foo"] = openmc.Material(name="foo") + mats["foo"].add_nuclide("H1", 2.0) + mats["foo"].add_element("O", 1.0) + mats["foo"].set_density("g/cm3", 1.0) + mats["foo"].add_s_alpha_beta("c_H_in_H2O") + + for univ in model.geometry.get_all_universes().values(): + if not isinstance(univ, openmc.DAGMCUniverse): + break + + cells_with_41 = [] + for cell in univ.cells.values(): + if cell.fill is None: + continue + if cell.fill.name == "41": + cells_with_41.append(cell.id) + univ.add_material_override(cell.id, mats["foo"]) + for cell_id in cells_with_41: + assert univ.cells[cell_id] == mats["foo"] + + +def test_dagmc_add_material_override_with_cell(model): + mats = {} + mats["foo"] = openmc.Material(name="foo") + mats["foo"].add_nuclide("H1", 2.0) + mats["foo"].add_element("O", 1.0) + mats["foo"].set_density("g/cm3", 1.0) + mats["foo"].add_s_alpha_beta("c_H_in_H2O") + + for univ in model.geometry.get_all_universes().values(): + if not isinstance(univ, openmc.DAGMCUniverse): + break + + cells_with_41 = [] + for cell in univ.cells.values(): + if cell.fill is None: + continue + if cell.fill.name == "41": + cells_with_41.append(cell.id) + univ.add_material_override(cell, mats["foo"]) + for cell_id in cells_with_41: + assert univ.cells[cell_id] == mats["foo"] + + +def test_model_differentiate_depletable_with_dagmc(model, run_in_tmpdir): + model.calculate_volumes() + + # Get the volume of the no-void fuel material before differentiation + volume_before = np.sum([m.volume for m in model.materials if m.name == "no-void fuel"]) + + # Differentiate the depletable materials + model.differentiate_depletable_mats(diff_volume_method="divide equally") + # Get the volume of the no-void fuel material after differentiation + volume_after = np.sum([m.volume for m in model.materials if "fuel" in m.name]) + assert np.isclose(volume_before, volume_after) + assert len(model.materials) == 4*2 +1 + + +def test_model_differentiate_with_dagmc(model): + root = model.geometry.root_universe + ll, ur = root.bounding_box + model.calculate_volumes() + # Get the volume of the no-void fuel material before differentiation + volume_before = np.sum([m.volume for m in model.materials if m.name == "no-void fuel"]) + + # Differentiate all the materials + model.differentiate_mats(depletable_only=False) + + # Get the volume of the no-void fuel material after differentiation + mat_vol = openmc.VolumeCalculation(model.materials, 1000000, ll, ur) + model.settings.volume_calculations = [mat_vol] + model.init_lib() # need to reinitialize the lib after differentiating the materials + model.calculate_volumes() + volume_after = np.sum([m.volume for m in model.materials if "fuel" in m.name]) + assert np.isclose(volume_before, volume_after) + assert len(model.materials) == 4*2 + 4 + + +def test_bad_override_cell_id(model): + for univ in model.geometry.get_all_universes().values(): + if isinstance(univ, openmc.DAGMCUniverse): + break + with pytest.raises(ValueError, match="Cell ID '1' not found in DAGMC universe"): + univ.material_overrides = {1: model.materials[0]} + + +def test_bad_override_type(model): + not_a_dag_cell = openmc.Cell() + for univ in model.geometry.get_all_universes().values(): + if isinstance(univ, openmc.DAGMCUniverse): + break + with pytest.raises(ValueError, match="Unrecognized key type. Must be an integer or openmc.DAGMCCell object"): + univ.material_overrides = {not_a_dag_cell: model.materials[0]} + + +def test_bad_replacement_mat_name(model): + for univ in model.geometry.get_all_universes().values(): + if isinstance(univ, openmc.DAGMCUniverse): + break + with pytest.raises(ValueError, match="No material with name 'not_a_mat' found in the DAGMC universe"): + univ.replace_material_assignment("not_a_mat", model.materials[0]) + + +def test_dagmc_xml(model): + # Set the environment + mats = {} + mats["no-void fuel"] = openmc.Material(1, name="no-void fuel") + mats["no-void fuel"].add_nuclide("U235", 0.03) + mats["no-void fuel"].add_nuclide("U238", 0.97) + mats["no-void fuel"].add_nuclide("O16", 2.0) + mats["no-void fuel"].set_density("g/cm3", 10.0) + + mats[5] = openmc.Material(name="41") + mats[5].add_nuclide("H1", 2.0) + mats[5].add_element("O", 1.0) + mats[5].set_density("g/cm3", 1.0) + mats[5].add_s_alpha_beta("c_H_in_H2O") + + for univ in model.geometry.get_all_universes().values(): + if isinstance(univ, openmc.DAGMCUniverse): + dag_univ = univ + break + + for k, v in mats.items(): + if isinstance(k, int): + dag_univ.add_material_override(k, v) + model.materials.append(v) + elif isinstance(k, str): + dag_univ.replace_material_assignment(k, v) + + # Tesing the XML subelement generation + root = ET.Element('dagmc_universe') + dag_univ.create_xml_subelement(root) + dagmc_ele = root.find('dagmc_universe') + + assert dagmc_ele.get('id') == str(dag_univ.id) + assert dagmc_ele.get('filename') == str(dag_univ.filename) + assert dagmc_ele.get('auto_geom_ids') == str(dag_univ.auto_geom_ids).lower() + + override_eles = dagmc_ele.find('material_overrides').findall('cell_override') + assert len(override_eles) == 4 + + for i, override_ele in enumerate(override_eles): + cell_id = override_ele.get('id') + assert dag_univ.material_overrides[int(cell_id)][0].id == int(override_ele.find('material_ids').text) + + model.export_to_model_xml() + + xml_model = openmc.Model.from_model_xml() + + for univ in xml_model.geometry.get_all_universes().values(): + if isinstance(univ, openmc.DAGMCUniverse): + xml_dagmc_univ = univ + break + + assert xml_dagmc_univ._material_overrides.keys() == dag_univ._material_overrides.keys() + + for xml_mats, model_mats in zip(xml_dagmc_univ._material_overrides.values(), dag_univ._material_overrides.values()): + assert all([xml_mat.id == orig_mat.id for xml_mat, orig_mat in zip(xml_mats, model_mats)]) From 8c7200fad3e37f6274fe7ce292acdf77d8f9fafe Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Tue, 7 Jan 2025 17:31:25 -0600 Subject: [PATCH 108/184] Enable UWUW library when building with DAGMC in CI (#3246) --- tools/ci/gha-install.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/ci/gha-install.py b/tools/ci/gha-install.py index 282389a8f19..d52c4c8254c 100644 --- a/tools/ci/gha-install.py +++ b/tools/ci/gha-install.py @@ -31,6 +31,7 @@ def install(omp=False, mpi=False, phdf5=False, dagmc=False, libmesh=False, ncrys if dagmc: cmake_cmd.append('-DOPENMC_USE_DAGMC=ON') + cmake_cmd.append('-DOPENMC_USE_UWUW=ON') dagmc_path = os.environ.get('HOME') + '/DAGMC' cmake_cmd.append('-DCMAKE_PREFIX_PATH=' + dagmc_path) From 6a5d80efd6a41a968c69702d4b0239f8fc6c6b2b Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Wed, 8 Jan 2025 17:27:09 +0100 Subject: [PATCH 109/184] updated docker file to latest DAGMC (#3251) --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4a94e3c0b11..583682ef595 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,7 +48,7 @@ ENV DD_REPO='https://github.com/pshriwise/double-down' ENV DD_INSTALL_DIR=$HOME/Double_down # DAGMC variables -ENV DAGMC_BRANCH='v3.2.3' +ENV DAGMC_BRANCH='v3.2.4' ENV DAGMC_REPO='https://github.com/svalinn/DAGMC' ENV DAGMC_INSTALL_DIR=$HOME/DAGMC/ From 10a63bbd27c114386fc1085ee57a36681dd60aba Mon Sep 17 00:00:00 2001 From: Micah Gale Date: Wed, 8 Jan 2025 11:47:25 -0600 Subject: [PATCH 110/184] Move to support python 3.13 (#3165) --- .github/workflows/ci.yml | 18 +++++++++--------- .readthedocs.yaml | 2 +- pyproject.toml | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5973c3cd48d..c5051b8d0bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-22.04 strategy: matrix: - python-version: ["3.10"] + python-version: ["3.11"] mpi: [n, y] omp: [n, y] dagmc: [n] @@ -35,34 +35,34 @@ jobs: vectfit: [n] include: - - python-version: "3.11" + - python-version: "3.12" omp: n mpi: n - - python-version: "3.12" + - python-version: "3.13" omp: n mpi: n - dagmc: y - python-version: "3.10" + python-version: "3.11" mpi: y omp: y - ncrystal: y - python-version: "3.10" + python-version: "3.11" mpi: n omp: n - libmesh: y - python-version: "3.10" + python-version: "3.11" mpi: y omp: y - libmesh: y - python-version: "3.10" + python-version: "3.11" mpi: n omp: y - event: y - python-version: "3.10" + python-version: "3.11" omp: y mpi: n - vectfit: y - python-version: "3.10" + python-version: "3.11" omp: n mpi: y name: "Python ${{ matrix.python-version }} (omp=${{ matrix.omp }}, diff --git a/.readthedocs.yaml b/.readthedocs.yaml index b7b69f9fe59..7b69b64b626 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -3,7 +3,7 @@ version: 2 build: os: "ubuntu-20.04" tools: - python: "3.10" + python: "3.12" sphinx: configuration: docs/source/conf.py diff --git a/pyproject.toml b/pyproject.toml index d0419d550f4..e4e8472f1aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ authors = [ ] description = "OpenMC" version = "0.15.1-dev" -requires-python = ">=3.10" +requires-python = ">=3.11" license = {file = "LICENSE"} classifiers = [ "Development Status :: 4 - Beta", @@ -21,9 +21,9 @@ classifiers = [ "Topic :: Scientific/Engineering", "Programming Language :: C++", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] dependencies = [ "numpy", From 4492f9db10f8c8189f72795c14e36e8f32b153b0 Mon Sep 17 00:00:00 2001 From: Andrew Davis Date: Wed, 8 Jan 2025 18:16:01 +0000 Subject: [PATCH 111/184] Fix type comparison (#3244) --- src/weight_windows.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/weight_windows.cpp b/src/weight_windows.cpp index 68f7550ae5a..e798a2f7abd 100644 --- a/src/weight_windows.cpp +++ b/src/weight_windows.cpp @@ -737,7 +737,7 @@ WeightWindowsGenerator::WeightWindowsGenerator(pugi::xml_node node) int32_t mesh_idx = model::mesh_map[mesh_id]; max_realizations_ = std::stoi(get_node_value(node, "max_realizations")); - int active_batches = settings::n_batches - settings::n_inactive; + int32_t active_batches = settings::n_batches - settings::n_inactive; if (max_realizations_ > active_batches) { auto msg = fmt::format("The maximum number of specified tally realizations ({}) is " From 8ba66f9fe30072b3935b8d1b5282354509031f3e Mon Sep 17 00:00:00 2001 From: Adam Nelson <1037107+nelsonag@users.noreply.github.com> Date: Wed, 8 Jan 2025 13:58:59 -0600 Subject: [PATCH 112/184] Fix for erroneously non-zero tally results of photon threshold reactions (#3242) Co-authored-by: Paul Romano --- src/photon.cpp | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/photon.cpp b/src/photon.cpp index 08062a2e9f5..4b2fb41978f 100644 --- a/src/photon.cpp +++ b/src/photon.cpp @@ -328,15 +328,19 @@ PhotonInteraction::PhotonInteraction(hid_t group) } // Take logarithm of energies and cross sections since they are log-log - // interpolated + // interpolated. Note that cross section libraries converted from ACE files + // represent zero as exp(-500) to avoid log-log interpolation errors. For + // values below exp(-499) we store the log as -900, for which exp(-900) + // evaluates to zero. + double limit = std::exp(-499.0); energy_ = xt::log(energy_); - coherent_ = xt::where(coherent_ > 0.0, xt::log(coherent_), -500.0); - incoherent_ = xt::where(incoherent_ > 0.0, xt::log(incoherent_), -500.0); + coherent_ = xt::where(coherent_ > limit, xt::log(coherent_), -900.0); + incoherent_ = xt::where(incoherent_ > limit, xt::log(incoherent_), -900.0); photoelectric_total_ = xt::where( - photoelectric_total_ > 0.0, xt::log(photoelectric_total_), -500.0); + photoelectric_total_ > limit, xt::log(photoelectric_total_), -900.0); pair_production_total_ = xt::where( - pair_production_total_ > 0.0, xt::log(pair_production_total_), -500.0); - heating_ = xt::where(heating_ > 0.0, xt::log(heating_), -500.0); + pair_production_total_ > limit, xt::log(pair_production_total_), -900.0); + heating_ = xt::where(heating_ > limit, xt::log(heating_), -900.0); } PhotonInteraction::~PhotonInteraction() From 0d4a85d3a81b74c013fc30435870ea3f7ed4eb41 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 8 Jan 2025 14:00:57 -0600 Subject: [PATCH 113/184] Remove top-level import of openmc.lib (#3250) --- openmc/model/model.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index 5137ea1abfc..402d6c0610f 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -16,7 +16,6 @@ from openmc.executor import _process_CLI_arguments from openmc.checkvalue import check_type, check_value from openmc.exceptions import InvalidIDError -import openmc.lib from openmc.utility_funcs import change_directory From 1eca46f536382c79884c8069676ef15d356e1e6c Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Wed, 8 Jan 2025 22:15:00 -0600 Subject: [PATCH 114/184] Write mesh type as a dataset always (#3253) --- docs/source/io_formats/statepoint.rst | 2 +- openmc/mesh.py | 2 +- src/mesh.cpp | 7 +------ 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/docs/source/io_formats/statepoint.rst b/docs/source/io_formats/statepoint.rst index 82d20a76302..fd00a3e7735 100644 --- a/docs/source/io_formats/statepoint.rst +++ b/docs/source/io_formats/statepoint.rst @@ -73,9 +73,9 @@ The current version of the statepoint file format is 18.1. **/tallies/meshes/mesh /** :Attributes: - **id** (*int*) -- ID of the mesh - - **type** (*char[]*) -- Type of mesh. :Datasets: - **name** (*char[]*) -- Name of the mesh. + - **type** (*char[]*) -- Type of mesh. - **dimension** (*int*) -- Number of mesh cells in each dimension. - **Regular Mesh Only:** - **lower_left** (*double[]*) -- Coordinates of lower-left corner of diff --git a/openmc/mesh.py b/openmc/mesh.py index 0b6f6b84e4b..e4d83d81d3d 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -99,7 +99,7 @@ def from_hdf5(cls, group: h5py.Group): Instance of a MeshBase subclass """ - mesh_type = 'regular' if 'type' not in group.attrs else group.attrs['type'].decode() + mesh_type = 'regular' if 'type' not in group.keys() else group['type'][()].decode() mesh_id = int(group.name.split('/')[-1].lstrip('mesh ')) mesh_name = '' if not 'name' in group else group['name'][()].decode() diff --git a/src/mesh.cpp b/src/mesh.cpp index 4b1878c5a2c..97bf710caa6 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -246,7 +246,7 @@ void Mesh::to_hdf5(hid_t group) const hid_t mesh_group = create_group(group, group_name.c_str()); // Write mesh type - write_attribute(mesh_group, "type", this->get_mesh_type()); + write_dataset(mesh_group, "type", this->get_mesh_type()); // Write mesh ID write_attribute(mesh_group, "id", id_); @@ -308,7 +308,6 @@ Position StructuredMesh::sample_element( UnstructuredMesh::UnstructuredMesh(pugi::xml_node node) : Mesh(node) { - // check the mesh type if (check_for_node(node, "type")) { auto temp = get_node_value(node, "type", true, true); @@ -970,7 +969,6 @@ std::pair, vector> RegularMesh::plot( void RegularMesh::to_hdf5_inner(hid_t mesh_group) const { - write_dataset(mesh_group, "type", "regular"); write_dataset(mesh_group, "dimension", get_x_shape()); write_dataset(mesh_group, "lower_left", lower_left_); write_dataset(mesh_group, "upper_right", upper_right_); @@ -1156,7 +1154,6 @@ std::pair, vector> RectilinearMesh::plot( void RectilinearMesh::to_hdf5_inner(hid_t mesh_group) const { - write_dataset(mesh_group, "type", "rectilinear"); write_dataset(mesh_group, "x_grid", grid_[0]); write_dataset(mesh_group, "y_grid", grid_[1]); write_dataset(mesh_group, "z_grid", grid_[2]); @@ -1431,7 +1428,6 @@ std::pair, vector> CylindricalMesh::plot( void CylindricalMesh::to_hdf5_inner(hid_t mesh_group) const { - write_dataset(mesh_group, "type", "cylindrical"); write_dataset(mesh_group, "r_grid", grid_[0]); write_dataset(mesh_group, "phi_grid", grid_[1]); write_dataset(mesh_group, "z_grid", grid_[2]); @@ -1743,7 +1739,6 @@ std::pair, vector> SphericalMesh::plot( void SphericalMesh::to_hdf5_inner(hid_t mesh_group) const { - write_dataset(mesh_group, "type", SphericalMesh::mesh_type); write_dataset(mesh_group, "r_grid", grid_[0]); write_dataset(mesh_group, "theta_grid", grid_[1]); write_dataset(mesh_group, "phi_grid", grid_[2]); From 7c142a361fc91a8f2fc69a163b314afc92d4452c Mon Sep 17 00:00:00 2001 From: "Joseph F. Specht IV" <102691489+jspecht3@users.noreply.github.com> Date: Fri, 10 Jan 2025 13:18:03 -0600 Subject: [PATCH 115/184] Change `Zernike` documentation in polynomial.py (#3258) --- openmc/polynomial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmc/polynomial.py b/openmc/polynomial.py index 1259c61ca26..341cdff476d 100644 --- a/openmc/polynomial.py +++ b/openmc/polynomial.py @@ -92,7 +92,7 @@ class Zernike(Polynomial): Parameters ---------- coef : Iterable of float - A list of coefficients of each term in radial only Zernike polynomials + A list of coefficients of each term in Zernike polynomials radius : float Domain of Zernike polynomials to be applied on. Default is 1. From 51f0e6f35039a61925477033a8eea4b2c871a728 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 10 Jan 2025 15:11:32 -0600 Subject: [PATCH 116/184] Add Patrick Shriwise to technical committee (#3255) --- docs/source/devguide/contributing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/devguide/contributing.rst b/docs/source/devguide/contributing.rst index c08b1a362f7..cda0313892b 100644 --- a/docs/source/devguide/contributing.rst +++ b/docs/source/devguide/contributing.rst @@ -109,7 +109,7 @@ Leadership Team The TC consists of the following individuals: - `Paul Romano `_ -- `Sterling Harper `_ +- `Patrick Shriwise `_ - `Adam Nelson `_ - `Benoit Forget `_ From c226c783c4e7d6c63a1c84ed5f1ee4a9beaead1c Mon Sep 17 00:00:00 2001 From: Ethan Peterson Date: Fri, 10 Jan 2025 17:20:00 -0500 Subject: [PATCH 117/184] Bug fix for Polygon 'yz' basis (#3259) --- openmc/model/surface_composite.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index 1f25b9ca636..c7655694d3c 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -1252,11 +1252,11 @@ def _get_convex_hull_surfs(self, qhull): else: op = operator.neg if basis == 'xy': - surf = openmc.Plane(a=dx, b=dy, d=-c) + surf = openmc.Plane(a=dx, b=dy, c=0.0, d=-c) elif basis == 'yz': - surf = openmc.Plane(b=dx, c=dy, d=-c) + surf = openmc.Plane(a=0.0, b=dx, c=dy, d=-c) elif basis == 'xz': - surf = openmc.Plane(a=dx, c=dy, d=-c) + surf = openmc.Plane(a=dx, b=0.0, c=dy, d=-c) else: y0 = -c/dy r2 = dy**2 / dx**2 From d2edf0ce4e8992155010dd2a2578a0ebbd0a1f90 Mon Sep 17 00:00:00 2001 From: Jan Malec Date: Sat, 11 Jan 2025 02:23:35 +0100 Subject: [PATCH 118/184] Fix path handling for thermal ACE generation (#3171) Co-authored-by: Paul Romano --- openmc/data/njoy.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openmc/data/njoy.py b/openmc/data/njoy.py index 1bf44891ef4..f5ef836f9ad 100644 --- a/openmc/data/njoy.py +++ b/openmc/data/njoy.py @@ -426,7 +426,7 @@ def make_ace(filename, temperatures=None, acer=True, xsdir=None, def make_ace_thermal(filename, filename_thermal, temperatures=None, - ace='ace', xsdir=None, output_dir=None, error=0.001, + ace=None, xsdir=None, output_dir=None, error=0.001, iwt=2, evaluation=None, evaluation_thermal=None, table_name=None, zaids=None, nmix=None, **kwargs): """Generate thermal scattering ACE file from ENDF files @@ -441,7 +441,7 @@ def make_ace_thermal(filename, filename_thermal, temperatures=None, Temperatures in Kelvin to produce data at. If omitted, data is produced at all temperatures given in the ENDF thermal scattering sublibrary. ace : str, optional - Path of ACE file to write + Path of ACE file to write. Default to ``"ace"``. xsdir : str, optional Path of xsdir file to write. Defaults to ``"xsdir"`` in the same directory as ``ace`` @@ -589,7 +589,7 @@ def make_ace_thermal(filename, filename_thermal, temperatures=None, commands += 'stop\n' run(commands, tapein, tapeout, **kwargs) - ace = output_dir / ace + ace = (output_dir / "ace") if ace is None else Path(ace) xsdir = (ace.parent / "xsdir") if xsdir is None else Path(xsdir) with ace.open('w') as ace_file, xsdir.open('w') as xsdir_file: # Concatenate ACE and xsdir files together From cf3f0201a0f8de3eb61d3193f6f6a1995a493dfc Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Sun, 12 Jan 2025 17:50:48 -0600 Subject: [PATCH 119/184] Update to a consistent definition of the r2 parameter for cones (#3254) Co-authored-by: Matthew Nyberg Co-authored-by: Paul Romano --- openmc/model/surface_composite.py | 45 +++++++++++--------- openmc/surface.py | 71 ++++++++++++++++--------------- 2 files changed, 60 insertions(+), 56 deletions(-) diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index c7655694d3c..88433118b7f 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -729,7 +729,7 @@ def __neg__(self): class XConeOneSided(CompositeSurface): - """One-sided cone parallel the x-axis + r"""One-sided cone parallel the x-axis A one-sided cone is composed of a normal cone surface and a "disambiguation" surface that eliminates the ambiguity as to which region of space is @@ -742,15 +742,16 @@ class XConeOneSided(CompositeSurface): Parameters ---------- x0 : float, optional - x-coordinate of the apex. Defaults to 0. + x-coordinate of the apex in [cm]. y0 : float, optional - y-coordinate of the apex. Defaults to 0. + y-coordinate of the apex in [cm]. z0 : float, optional - z-coordinate of the apex. Defaults to 0. + z-coordinate of the apex in [cm]. r2 : float, optional - Parameter related to the aperture [:math:`\\rm cm^2`]. - It can be interpreted as the increase in the radius squared per cm along - the cone's axis of revolution. + The square of the slope of the cone. It is defined as + :math:`\left(\frac{r}{h}\right)^2` for a radius, :math:`r` and an axial + distance :math:`h` from the apex. An easy way to define this quantity is + to take the square of the radius of the cone (in cm) 1 cm from the apex. up : bool Whether to select the side of the cone that extends to infinity in the positive direction of the coordinate axis (the positive half-space of @@ -783,7 +784,7 @@ def __neg__(self): class YConeOneSided(CompositeSurface): - """One-sided cone parallel the y-axis + r"""One-sided cone parallel the y-axis A one-sided cone is composed of a normal cone surface and a "disambiguation" surface that eliminates the ambiguity as to which region of space is @@ -796,15 +797,16 @@ class YConeOneSided(CompositeSurface): Parameters ---------- x0 : float, optional - x-coordinate of the apex. Defaults to 0. + x-coordinate of the apex in [cm]. y0 : float, optional - y-coordinate of the apex. Defaults to 0. + y-coordinate of the apex in [cm]. z0 : float, optional - z-coordinate of the apex. Defaults to 0. + z-coordinate of the apex in [cm]. r2 : float, optional - Parameter related to the aperture [:math:`\\rm cm^2`]. - It can be interpreted as the increase in the radius squared per cm along - the cone's axis of revolution. + The square of the slope of the cone. It is defined as + :math:`\left(\frac{r}{h}\right)^2` for a radius, :math:`r` and an axial + distance :math:`h` from the apex. An easy way to define this quantity is + to take the square of the radius of the cone (in cm) 1 cm from the apex. up : bool Whether to select the side of the cone that extends to infinity in the positive direction of the coordinate axis (the positive half-space of @@ -836,7 +838,7 @@ def __init__(self, x0=0., y0=0., z0=0., r2=1., up=True, **kwargs): class ZConeOneSided(CompositeSurface): - """One-sided cone parallel the z-axis + r"""One-sided cone parallel the z-axis A one-sided cone is composed of a normal cone surface and a "disambiguation" surface that eliminates the ambiguity as to which region of space is @@ -849,15 +851,16 @@ class ZConeOneSided(CompositeSurface): Parameters ---------- x0 : float, optional - x-coordinate of the apex. Defaults to 0. + x-coordinate of the apex in [cm]. y0 : float, optional - y-coordinate of the apex. Defaults to 0. + y-coordinate of the apex in [cm]. z0 : float, optional - z-coordinate of the apex. Defaults to 0. + z-coordinate of the apex in [cm]. r2 : float, optional - Parameter related to the aperture [:math:`\\rm cm^2`]. - It can be interpreted as the increase in the radius squared per cm along - the cone's axis of revolution. + The square of the slope of the cone. It is defined as + :math:`\left(\frac{r}{h}\right)^2` for a radius, :math:`r` and an axial + distance :math:`h` from the apex. An easy way to define this quantity is + to take the square of the radius of the cone (in cm) 1 cm from the apex. up : bool Whether to select the side of the cone that extends to infinity in the positive direction of the coordinate axis (the positive half-space of diff --git a/openmc/surface.py b/openmc/surface.py index c95025f949b..6919952ec9c 100644 --- a/openmc/surface.py +++ b/openmc/surface.py @@ -1753,7 +1753,7 @@ def evaluate(self, point): class Cone(QuadricMixin, Surface): - """A conical surface parallel to the x-, y-, or z-axis. + r"""A conical surface parallel to the x-, y-, or z-axis. .. Note:: This creates a double cone, which is two one-sided cones that meet at their apex. @@ -1763,24 +1763,22 @@ class Cone(QuadricMixin, Surface): Parameters ---------- x0 : float, optional - x-coordinate of the apex in [cm]. Defaults to 0. + x-coordinate of the apex in [cm]. y0 : float, optional - y-coordinate of the apex in [cm]. Defaults to 0. + y-coordinate of the apex in [cm]. z0 : float, optional - z-coordinate of the apex in [cm]. Defaults to 0. + z-coordinate of the apex in [cm]. r2 : float, optional - Parameter related to the aperture [:math:`\\rm cm^2`]. - It can be interpreted as the increase in the radius squared per cm along - the cone's axis of revolution. + The square of the slope of the cone. It is defined as + :math:`\left(\frac{r}{h}\right)^2` for a radius, :math:`r` and an axial + distance :math:`h` from the apex. An easy way to define this quantity is + to take the square of the radius of the cone (in cm) 1 cm from the apex. dx : float, optional x-component of the vector representing the axis of the cone. - Defaults to 0. dy : float, optional y-component of the vector representing the axis of the cone. - Defaults to 0. dz : float, optional z-component of the vector representing the axis of the cone. - Defaults to 1. surface_id : int, optional Unique identifier for the surface. If not specified, an identifier will automatically be assigned. @@ -1805,7 +1803,7 @@ class Cone(QuadricMixin, Surface): z0 : float z-coordinate of the apex in [cm] r2 : float - Parameter related to the aperature [cm^2] + Parameter related to the aperture dx : float x-component of the vector representing the axis of the cone. dy : float @@ -1911,7 +1909,7 @@ def to_xml_element(self): class XCone(QuadricMixin, Surface): - """A cone parallel to the x-axis of the form :math:`(y - y_0)^2 + (z - z_0)^2 = + r"""A cone parallel to the x-axis of the form :math:`(y - y_0)^2 + (z - z_0)^2 = r^2 (x - x_0)^2`. .. Note:: @@ -1921,15 +1919,16 @@ class XCone(QuadricMixin, Surface): Parameters ---------- x0 : float, optional - x-coordinate of the apex in [cm]. Defaults to 0. + x-coordinate of the apex in [cm]. y0 : float, optional - y-coordinate of the apex in [cm]. Defaults to 0. + y-coordinate of the apex in [cm]. z0 : float, optional - z-coordinate of the apex in [cm]. Defaults to 0. + z-coordinate of the apex in [cm]. r2 : float, optional - Parameter related to the aperture [:math:`\\rm cm^2`]. - It can be interpreted as the increase in the radius squared per cm along - the cone's axis of revolution. + The square of the slope of the cone. It is defined as + :math:`\left(\frac{r}{h}\right)^2` for a radius, :math:`r` and an axial + distance :math:`h` from the apex. An easy way to define this quantity is + to take the square of the radius of the cone (in cm) 1 cm from the apex. boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles @@ -1953,7 +1952,7 @@ class XCone(QuadricMixin, Surface): z0 : float z-coordinate of the apex in [cm] r2 : float - Parameter related to the aperature + Parameter related to the aperture boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. @@ -2012,7 +2011,7 @@ def evaluate(self, point): class YCone(QuadricMixin, Surface): - """A cone parallel to the y-axis of the form :math:`(x - x_0)^2 + (z - z_0)^2 = + r"""A cone parallel to the y-axis of the form :math:`(x - x_0)^2 + (z - z_0)^2 = r^2 (y - y_0)^2`. .. Note:: @@ -2022,15 +2021,16 @@ class YCone(QuadricMixin, Surface): Parameters ---------- x0 : float, optional - x-coordinate of the apex in [cm]. Defaults to 0. + x-coordinate of the apex in [cm]. y0 : float, optional - y-coordinate of the apex in [cm]. Defaults to 0. + y-coordinate of the apex in [cm]. z0 : float, optional - z-coordinate of the apex in [cm]. Defaults to 0. + z-coordinate of the apex in [cm]. r2 : float, optional - Parameter related to the aperture [:math:`\\rm cm^2`]. - It can be interpreted as the increase in the radius squared per cm along - the cone's axis of revolution. + The square of the slope of the cone. It is defined as + :math:`\left(\frac{r}{h}\right)^2` for a radius, :math:`r` and an axial + distance :math:`h` from the apex. An easy way to define this quantity is + to take the square of the radius of the cone (in cm) 1 cm from the apex. boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles @@ -2054,7 +2054,7 @@ class YCone(QuadricMixin, Surface): z0 : float z-coordinate of the apex in [cm] r2 : float - Parameter related to the aperature + Parameter related to the aperture boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. @@ -2113,7 +2113,7 @@ def evaluate(self, point): class ZCone(QuadricMixin, Surface): - """A cone parallel to the z-axis of the form :math:`(x - x_0)^2 + (y - y_0)^2 = + r"""A cone parallel to the z-axis of the form :math:`(x - x_0)^2 + (y - y_0)^2 = r^2 (z - z_0)^2`. .. Note:: @@ -2123,15 +2123,16 @@ class ZCone(QuadricMixin, Surface): Parameters ---------- x0 : float, optional - x-coordinate of the apex in [cm]. Defaults to 0. + x-coordinate of the apex in [cm]. y0 : float, optional - y-coordinate of the apex in [cm]. Defaults to 0. + y-coordinate of the apex in [cm]. z0 : float, optional - z-coordinate of the apex in [cm]. Defaults to 0. + z-coordinate of the apex in [cm]. r2 : float, optional - Parameter related to the aperature [cm^2]. - This is the square of the radius of the cone 1 cm from. - This can also be treated as the square of the slope of the cone relative to its axis. + The square of the slope of the cone. It is defined as + :math:`\left(\frac{r}{h}\right)^2` for a radius, :math:`r` and an axial + distance :math:`h` from the apex. An easy way to define this quantity is + to take the square of the radius of the cone (in cm) 1 cm from the apex. boundary_type : {'transmission', 'vacuum', 'reflective', 'white'}, optional Boundary condition that defines the behavior for particles hitting the surface. Defaults to transmissive boundary condition where particles @@ -2155,7 +2156,7 @@ class ZCone(QuadricMixin, Surface): z0 : float z-coordinate of the apex in [cm] r2 : float - Parameter related to the aperature + Parameter related to the aperture. boundary_type : {'transmission', 'vacuum', 'reflective', 'white'} Boundary condition that defines the behavior for particles hitting the surface. From d39a4140114704f064cb994467049e8b1e9bf203 Mon Sep 17 00:00:00 2001 From: azimG Date: Mon, 13 Jan 2025 07:19:07 +0100 Subject: [PATCH 120/184] Set Model attributes only if needed (#3209) Co-authored-by: Paul Romano --- openmc/model/model.py | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index 402d6c0610f..1c261061aef 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -64,22 +64,11 @@ class Model: def __init__(self, geometry=None, materials=None, settings=None, tallies=None, plots=None): - self.geometry = openmc.Geometry() - self.materials = openmc.Materials() - self.settings = openmc.Settings() - self.tallies = openmc.Tallies() - self.plots = openmc.Plots() - - if geometry is not None: - self.geometry = geometry - if materials is not None: - self.materials = materials - if settings is not None: - self.settings = settings - if tallies is not None: - self.tallies = tallies - if plots is not None: - self.plots = plots + self.geometry = openmc.Geometry() if geometry is None else geometry + self.materials = openmc.Materials() if materials is None else materials + self.settings = openmc.Settings() if settings is None else settings + self.tallies = openmc.Tallies() if tallies is None else tallies + self.plots = openmc.Plots() if plots is None else plots @property def geometry(self) -> openmc.Geometry | None: From 549cc0973c9002da86c91564b2951a1c3f5c2d4b Mon Sep 17 00:00:00 2001 From: Adam Nelson <1037107+nelsonag@users.noreply.github.com> Date: Tue, 14 Jan 2025 09:51:47 -0600 Subject: [PATCH 121/184] Enable the LegendreFilter filter to be used in photon tallies for orders greater than P0. (#3245) Co-authored-by: Paul Romano --- src/physics.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/physics.cpp b/src/physics.cpp index 69e74f2eafd..34dc4ce1cbb 100644 --- a/src/physics.cpp +++ b/src/physics.cpp @@ -293,8 +293,8 @@ void sample_photon_reaction(Particle& p) // Coherent (Rayleigh) scattering prob += micro.coherent; if (prob > cutoff) { - double mu = element.rayleigh_scatter(alpha, p.current_seed()); - p.u() = rotate_angle(p.u(), mu, nullptr, p.current_seed()); + p.mu() = element.rayleigh_scatter(alpha, p.current_seed()); + p.u() = rotate_angle(p.u(), p.mu(), nullptr, p.current_seed()); p.event() = TallyEvent::SCATTER; p.event_mt() = COHERENT; return; @@ -303,10 +303,10 @@ void sample_photon_reaction(Particle& p) // Incoherent (Compton) scattering prob += micro.incoherent; if (prob > cutoff) { - double alpha_out, mu; + double alpha_out; int i_shell; element.compton_scatter( - alpha, true, &alpha_out, &mu, &i_shell, p.current_seed()); + alpha, true, &alpha_out, &p.mu(), &i_shell, p.current_seed()); // Determine binding energy of shell. The binding energy is 0.0 if // doppler broadening is not used. @@ -322,9 +322,9 @@ void sample_photon_reaction(Particle& p) double E_electron = (alpha - alpha_out) * MASS_ELECTRON_EV - e_b; int electron = static_cast(ParticleType::electron); if (E_electron >= settings::energy_cutoff[electron]) { - double mu_electron = (alpha - alpha_out * mu) / + double mu_electron = (alpha - alpha_out * p.mu()) / std::sqrt(alpha * alpha + alpha_out * alpha_out - - 2.0 * alpha * alpha_out * mu); + 2.0 * alpha * alpha_out * p.mu()); Direction u = rotate_angle(p.u(), mu_electron, &phi, p.current_seed()); p.create_secondary(p.wgt(), u, E_electron, ParticleType::electron); } @@ -338,7 +338,7 @@ void sample_photon_reaction(Particle& p) phi += PI; p.E() = alpha_out * MASS_ELECTRON_EV; - p.u() = rotate_angle(p.u(), mu, &phi, p.current_seed()); + p.u() = rotate_angle(p.u(), p.mu(), &phi, p.current_seed()); p.event() = TallyEvent::SCATTER; p.event_mt() = INCOHERENT; return; From 6d731934f4e0b62c3be6094c283a19730d8ce08f Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 15 Jan 2025 06:35:37 -0600 Subject: [PATCH 122/184] Fix bug in WeightWindowGenerator for empty energy bounds (#3263) --- openmc/weight_windows.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openmc/weight_windows.py b/openmc/weight_windows.py index 5df9f71dcc7..1ddbfe10fe1 100644 --- a/openmc/weight_windows.py +++ b/openmc/weight_windows.py @@ -897,7 +897,10 @@ def from_xml_element(cls, elem: ET.Element, meshes: dict) -> WeightWindowGenerat mesh_id = int(get_text(elem, 'mesh')) mesh = meshes[mesh_id] - energy_bounds = [float(x) for x in get_text(elem, 'energy_bounds').split()] + if (energy_bounds := get_text(elem, 'energy_bounds')) is not None: + energy_bounds = [float(x) for x in energy_bounds.split()] + else: + energy_bounds = None particle_type = get_text(elem, 'particle_type') wwg = cls(mesh, energy_bounds, particle_type) From 32662b409a7780a9cf75732acf4e049a6ed6954b Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Thu, 16 Jan 2025 09:36:13 -0600 Subject: [PATCH 123/184] Add constant for invalid surface tokens. (#3260) Co-authored-by: Paul Romano --- include/openmc/cell.h | 1 - include/openmc/constants.h | 4 ++++ include/openmc/particle_data.h | 22 +++++++++++++++++----- src/boundary_condition.cpp | 6 ++---- src/dagmc.cpp | 2 +- src/geometry.cpp | 8 ++++---- src/output.cpp | 4 ++-- src/particle.cpp | 12 ++++++------ src/plot.cpp | 6 +++--- src/tallies/filter_musurface.cpp | 2 +- src/tallies/filter_surface.cpp | 2 +- 11 files changed, 41 insertions(+), 28 deletions(-) diff --git a/include/openmc/cell.h b/include/openmc/cell.h index 032475ce982..d01020f8e7a 100644 --- a/include/openmc/cell.h +++ b/include/openmc/cell.h @@ -29,7 +29,6 @@ namespace openmc { enum class Fill { MATERIAL, UNIVERSE, LATTICE }; -// TODO: Convert to enum constexpr int32_t OP_LEFT_PAREN {std::numeric_limits::max()}; constexpr int32_t OP_RIGHT_PAREN {std::numeric_limits::max() - 1}; constexpr int32_t OP_COMPLEMENT {std::numeric_limits::max() - 2}; diff --git a/include/openmc/constants.h b/include/openmc/constants.h index 605ae1839d8..a83a4a07d4b 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -350,6 +350,10 @@ enum class RandomRaySourceShape { FLAT, LINEAR, LINEAR_XY }; enum class GeometryType { CSG, DAG }; +// a surface token cannot be zero due to the unsigned nature of zero for integer +// representations. This value represents no surface. +constexpr int32_t SURFACE_NONE {0}; + } // namespace openmc #endif // OPENMC_CONSTANTS_H diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index 164148cce10..66137c5f688 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -185,10 +185,14 @@ struct CacheDataMG { struct BoundaryInfo { double distance {INFINITY}; //!< distance to nearest boundary - int surface_index {0}; //!< if boundary is surface, index in surfaces vector - int coord_level; //!< coordinate level after crossing boundary + int surface { + SURFACE_NONE}; //!< surface token, non-zero if boundary is surface + int coord_level; //!< coordinate level after crossing boundary array lattice_translation {}; //!< which way lattice indices will change + + // TODO: off-by-one + int surface_index() const { return std::abs(surface) - 1; } }; /* @@ -226,7 +230,7 @@ class GeometryState { void init_from_r_u(Position r_a, Direction u_a) { clear(); - surface() = 0; + surface() = SURFACE_NONE; material() = C_NONE; r() = r_a; u() = u_a; @@ -296,10 +300,17 @@ class GeometryState { Direction& u_local() { return coord_[n_coord_ - 1].u; } const Direction& u_local() const { return coord_[n_coord_ - 1].u; } - // Surface that the particle is on + // Surface token for the surface that the particle is currently on int& surface() { return surface_; } const int& surface() const { return surface_; } + // Surface index based on the current value of the surface_ attribute + int surface_index() const + { + // TODO: off-by-one + return std::abs(surface_) - 1; + } + // Boundary information BoundaryInfo& boundary() { return boundary_; } @@ -337,7 +348,8 @@ class GeometryState { Position r_last_; //!< previous coordinates Direction u_last_; //!< previous direction coordinates - int surface_ {0}; //!< index for surface particle is on + int surface_ { + SURFACE_NONE}; //!< surface token for surface the particle is currently on BoundaryInfo boundary_; //!< Info about the next intersection diff --git a/src/boundary_condition.cpp b/src/boundary_condition.cpp index b58054dce8c..7216ac89649 100644 --- a/src/boundary_condition.cpp +++ b/src/boundary_condition.cpp @@ -129,8 +129,7 @@ TranslationalPeriodicBC::TranslationalPeriodicBC(int i_surf, int j_surf) void TranslationalPeriodicBC::handle_particle( Particle& p, const Surface& surf) const { - // TODO: off-by-one on surface indices throughout this function. - int i_particle_surf = std::abs(p.surface()) - 1; + int i_particle_surf = p.surface_index(); // Figure out which of the two BC surfaces were struck then find the // particle's new location and surface. @@ -255,8 +254,7 @@ RotationalPeriodicBC::RotationalPeriodicBC(int i_surf, int j_surf) void RotationalPeriodicBC::handle_particle( Particle& p, const Surface& surf) const { - // TODO: off-by-one on surface indices throughout this function. - int i_particle_surf = std::abs(p.surface()) - 1; + int i_particle_surf = p.surface_index(); // Figure out which of the two BC surfaces were struck to figure out if a // forward or backward rotation is required. Specify the other surface as diff --git a/src/dagmc.cpp b/src/dagmc.cpp index 134e31ecf3c..94c220850f3 100644 --- a/src/dagmc.cpp +++ b/src/dagmc.cpp @@ -847,7 +847,7 @@ void check_dagmc_root_univ() int32_t next_cell(int32_t surf, int32_t curr_cell, int32_t univ) { - auto surfp = dynamic_cast(model::surfaces[surf - 1].get()); + auto surfp = dynamic_cast(model::surfaces[surf].get()); auto cellp = dynamic_cast(model::cells[curr_cell].get()); auto univp = static_cast(model::universes[univ].get()); diff --git a/src/geometry.cpp b/src/geometry.cpp index 445b19faac1..9e975c00dee 100644 --- a/src/geometry.cpp +++ b/src/geometry.cpp @@ -421,15 +421,15 @@ BoundaryInfo distance_to_boundary(GeometryState& p) // have to explicitly check which half-space the particle would be // traveling into if the surface is crossed if (c.is_simple() || d == INFTY) { - info.surface_index = level_surf_cross; + info.surface = level_surf_cross; } else { Position r_hit = r + d_surf * u; Surface& surf {*model::surfaces[std::abs(level_surf_cross) - 1]}; Direction norm = surf.normal(r_hit); if (u.dot(norm) > 0) { - info.surface_index = std::abs(level_surf_cross); + info.surface = std::abs(level_surf_cross); } else { - info.surface_index = -std::abs(level_surf_cross); + info.surface = -std::abs(level_surf_cross); } } @@ -441,7 +441,7 @@ BoundaryInfo distance_to_boundary(GeometryState& p) } else { if (d == INFINITY || (d - d_lat) / d >= FP_REL_PRECISION) { d = d_lat; - info.surface_index = 0; + info.surface = SURFACE_NONE; info.lattice_translation = level_lat_trans; info.coord_level = i + 1; } diff --git a/src/output.cpp b/src/output.cpp index a430fe9a6c6..8494450c409 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -200,9 +200,9 @@ void print_particle(Particle& p) } // Display miscellaneous info. - if (p.surface() != 0) { + if (p.surface() != SURFACE_NONE) { // Surfaces identifiers are >= 1, but indices are >= 0 so we need -1 - const Surface& surf {*model::surfaces[std::abs(p.surface()) - 1]}; + const Surface& surf {*model::surfaces[p.surface_index()]}; fmt::print(" Surface = {}\n", (p.surface() > 0) ? surf.id_ : -surf.id_); } fmt::print(" Weight = {}\n", p.wgt()); diff --git a/src/particle.cpp b/src/particle.cpp index 0ea8650143d..65e8065fded 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -108,7 +108,7 @@ void Particle::from_source(const SourceSite* src) { // Reset some attributes clear(); - surface() = 0; + surface() = SURFACE_NONE; cell_born() = C_NONE; material() = C_NONE; n_collision() = 0; @@ -277,7 +277,7 @@ void Particle::event_cross_surface() n_coord_last() = n_coord(); // Set surface that particle is on and adjust coordinate levels - surface() = boundary().surface_index; + surface() = boundary().surface; n_coord() = boundary().coord_level; if (boundary().lattice_translation[0] != 0 || @@ -291,7 +291,7 @@ void Particle::event_cross_surface() } else { // Particle crosses surface // TODO: off-by-one - const auto& surf {model::surfaces[std::abs(surface()) - 1].get()}; + const auto& surf {model::surfaces[surface_index()].get()}; // If BC, add particle to surface source before crossing surface if (surf->surf_source_ && surf->bc_) { add_surf_source_to_bank(*this, *surf); @@ -328,7 +328,7 @@ void Particle::event_collide() score_surface_tally(*this, model::active_meshsurf_tallies); // Clear surface component - surface() = 0; + surface() = SURFACE_NONE; if (settings::run_CE) { collision(*this); @@ -549,7 +549,7 @@ void Particle::cross_surface(const Surface& surf) #ifdef DAGMC // in DAGMC, we know what the next cell should be if (surf.geom_type() == GeometryType::DAG) { - int32_t i_cell = next_cell(std::abs(surface()), cell_last(n_coord() - 1), + int32_t i_cell = next_cell(surface_index(), cell_last(n_coord() - 1), lowest_coord().universe) - 1; // save material and temp @@ -587,7 +587,7 @@ void Particle::cross_surface(const Surface& surf) // the particle is really traveling tangent to a surface, if we move it // forward a tiny bit it should fix the problem. - surface() = 0; + surface() = SURFACE_NONE; n_coord() = 1; r() += TINY_BIT * u(); diff --git a/src/plot.cpp b/src/plot.cpp index 85131d67a01..b36ed6f5d93 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -1301,7 +1301,7 @@ void ProjectionPlot::create_output() const while (intersection_found) { bool inside_cell = false; - int32_t i_surface = std::abs(p.surface()) - 1; + int32_t i_surface = p.surface_index(); if (i_surface > 0 && model::surfaces[i_surface]->geom_type() == GeometryType::DAG) { #ifdef DAGMC @@ -1334,13 +1334,13 @@ void ProjectionPlot::create_output() const this_line_segments[tid][horiz].emplace_back( color_by_ == PlotColorBy::mats ? p.material() : p.lowest_coord().cell, - dist.distance, std::abs(dist.surface_index)); + dist.distance, std::abs(dist.surface)); // Advance particle for (int lev = 0; lev < p.n_coord(); ++lev) { p.coord(lev).r += dist.distance * p.coord(lev).u; } - p.surface() = dist.surface_index; + p.surface() = dist.surface; p.n_coord_last() = p.n_coord(); p.n_coord() = dist.coord_level; if (dist.lattice_translation[0] != 0 || diff --git a/src/tallies/filter_musurface.cpp b/src/tallies/filter_musurface.cpp index 58fe39b9113..340149d4cff 100644 --- a/src/tallies/filter_musurface.cpp +++ b/src/tallies/filter_musurface.cpp @@ -12,7 +12,7 @@ void MuSurfaceFilter::get_all_bins( const Particle& p, TallyEstimator estimator, FilterMatch& match) const { // Get surface normal (and make sure it is a unit vector) - const auto surf {model::surfaces[std::abs(p.surface()) - 1].get()}; + const auto surf {model::surfaces[p.surface_index()].get()}; auto n = surf->normal(p.r()); n /= n.norm(); diff --git a/src/tallies/filter_surface.cpp b/src/tallies/filter_surface.cpp index a84d1772822..1fbf8d44e38 100644 --- a/src/tallies/filter_surface.cpp +++ b/src/tallies/filter_surface.cpp @@ -47,7 +47,7 @@ void SurfaceFilter::set_surfaces(gsl::span surfaces) void SurfaceFilter::get_all_bins( const Particle& p, TallyEstimator estimator, FilterMatch& match) const { - auto search = map_.find(std::abs(p.surface()) - 1); + auto search = map_.find(p.surface_index()); if (search != map_.end()) { match.bins_.push_back(search->second); if (p.surface() < 0) { From bd874f1b3ebf0e75851da750dd179593a0f7e7f0 Mon Sep 17 00:00:00 2001 From: Joshua Einstein-Curtis <33426885+jeinstei@users.noreply.github.com> Date: Thu, 16 Jan 2025 09:38:19 -0600 Subject: [PATCH 124/184] Update plots.py for PathLike to string handling error (#3261) Co-authored-by: Paul Romano --- openmc/plots.py | 3 ++- tests/unit_tests/test_plots.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/openmc/plots.py b/openmc/plots.py index 7532d9d5cb1..2bb3ef65f4a 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -250,9 +250,10 @@ def voxel_to_vtk(voxel_file: PathLike, output: PathLike = 'plot.vti'): writer.SetInputData(grid) else: writer.SetInput(grid) + output = str(output) if not output.endswith(".vti"): output += ".vti" - writer.SetFileName(str(output)) + writer.SetFileName(output) writer.Write() return output diff --git a/tests/unit_tests/test_plots.py b/tests/unit_tests/test_plots.py index a1e15016a2e..4efc12c9326 100644 --- a/tests/unit_tests/test_plots.py +++ b/tests/unit_tests/test_plots.py @@ -90,7 +90,7 @@ def test_voxel_plot(run_in_tmpdir): assert Path('test_voxel_plot.vti').is_file() vox_plot.filename = 'h5_voxel_plot' - vox_plot.to_vtk('another_test_voxel_plot.vti') + vox_plot.to_vtk(Path('another_test_voxel_plot.vti')) assert Path('h5_voxel_plot.h5').is_file() assert Path('another_test_voxel_plot.vti').is_file() From 3bf1486f4980dd520810ad33e0aeea5d14dd6a87 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 20 Jan 2025 07:18:50 -0600 Subject: [PATCH 125/184] Fix bug in Surface.normalize (#3270) --- openmc/surface.py | 2 +- tests/unit_tests/test_surface.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/openmc/surface.py b/openmc/surface.py index 6919952ec9c..840c3125d88 100644 --- a/openmc/surface.py +++ b/openmc/surface.py @@ -309,7 +309,7 @@ def normalize(self, coeffs=None): coeffs = self._get_base_coeffs() coeffs = np.asarray(coeffs) nonzeros = ~np.isclose(coeffs, 0., rtol=0., atol=self._atol) - norm_factor = np.abs(coeffs[nonzeros][0]) + norm_factor = coeffs[nonzeros][0] return tuple([c/norm_factor for c in coeffs]) def is_equal(self, other): diff --git a/tests/unit_tests/test_surface.py b/tests/unit_tests/test_surface.py index 12cd8c9d20d..e9560223d13 100644 --- a/tests/unit_tests/test_surface.py +++ b/tests/unit_tests/test_surface.py @@ -753,3 +753,16 @@ def test_ztorus(): assert isinstance(sr, openmc.YTorus) sr = s.rotate((0., 90., 0.)) assert isinstance(sr, openmc.XTorus) + + +def test_normalize(): + """Test that equivalent planes give same normalized coefficients""" + p1 = openmc.Plane(a=0.0, b=1.0, c=0.0, d=1.0) + p2 = openmc.Plane(a=0.0, b=2.0, c=0.0, d=2.0) + assert p1.normalize() == p2.normalize() + + p2 = openmc.Plane(a=0.0, b=-1.0, c=0.0, d=-1.0) + assert p1.normalize() == p2.normalize() + + p2 = openmc.YPlane(1.0) + assert p1.normalize() == p2.normalize() From 9170bf34ad4b0bdc93c433a8b77263c52515b663 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 20 Jan 2025 07:51:13 -0600 Subject: [PATCH 126/184] Update recognized thermal scattering materials for ENDF/B-VIII.1 (#3267) --- openmc/data/njoy.py | 67 +++++++++++++++++++++++++++++++ openmc/data/thermal.py | 89 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 145 insertions(+), 11 deletions(-) diff --git a/openmc/data/njoy.py b/openmc/data/njoy.py index f5ef836f9ad..e025cff0d98 100644 --- a/openmc/data/njoy.py +++ b/openmc/data/njoy.py @@ -18,21 +18,45 @@ 'c_Al27': ThermalTuple('al27', [13027], 1), 'c_Al_in_Al2O3': ThermalTuple('asap00', [13027], 1), 'c_Be': ThermalTuple('be', [4009], 1), + 'c_Be_distinct': ThermalTuple('besd', [4009], 1), 'c_Be_in_BeO': ThermalTuple('bebeo', [4009], 1), 'c_Be_in_Be2C': ThermalTuple('bebe2c', [4009], 1), + 'c_Be_in_BeF2': ThermalTuple('bebef2', [4009], 1), 'c_Be_in_FLiBe': ThermalTuple('beflib', [4009], 1), 'c_C6H6': ThermalTuple('benz', [1001, 6000, 6012], 2), + 'c_C_in_Be2C': ThermalTuple('cbe2c', [6000, 6012, 6013], 1), + 'c_C_in_C5O2H8': ThermalTuple('clucit', [6000, 6012, 6013], 1), + 'c_C_in_C8H8': ThermalTuple('cc8h8', [6000, 6012, 6013], 1), + 'c_C_in_CF2': ThermalTuple('ccf2', [6000, 6012, 6013], 1), 'c_C_in_SiC': ThermalTuple('csic', [6000, 6012, 6013], 1), + 'c_C_in_UC_100p': ThermalTuple('cuc100', [6000, 6012, 6013], 1), + 'c_C_in_UC_10p': ThermalTuple('cuc10', [6000, 6012, 6013], 1), + 'c_C_in_UC_5p': ThermalTuple('cuc5', [6000, 6012, 6013], 1), + 'c_C_in_UC': ThermalTuple('cinuc', [6000, 6012, 6013], 1), + 'c_C_in_UC_HALEU': ThermalTuple('cuchal', [6000, 6012, 6013], 1), + 'c_C_in_UC_HEU': ThermalTuple('cucheu', [6000, 6012, 6013], 1), + 'c_C_in_ZrC': ThermalTuple('czrc', [6000, 6012, 6013], 1), 'c_Ca_in_CaH2': ThermalTuple('cacah2', [20040, 20042, 20043, 20044, 20046, 20048], 1), + 'c_D_in_7LiD': ThermalTuple('dlid', [1002], 1), 'c_D_in_D2O': ThermalTuple('dd2o', [1002], 1), 'c_D_in_D2O_solid': ThermalTuple('dice', [1002], 1), + 'c_F_in_Be2': ThermalTuple('fbef2', [9019], 1), + 'c_F_in_CF2': ThermalTuple('fcf2', [9019], 1), 'c_F_in_FLiBe': ThermalTuple('fflibe', [9019], 1), + 'c_F_in_HF': ThermalTuple('f_hf', [9019], 1), + 'c_F_in_MgF2': ThermalTuple('fmgf2', [9019], 1), 'c_Fe56': ThermalTuple('fe56', [26056], 1), 'c_Graphite': ThermalTuple('graph', [6000, 6012, 6013], 1), 'c_Graphite_10p': ThermalTuple('grph10', [6000, 6012, 6013], 1), + 'c_Graphite_20p': ThermalTuple('grph20', [6000, 6012, 6013], 1), 'c_Graphite_30p': ThermalTuple('grph30', [6000, 6012, 6013], 1), + 'c_Graphite_distinct': ThermalTuple('grphsd', [6000, 6012, 6013], 1), + 'c_H_in_7LiH': ThermalTuple('hlih', [1001], 1), 'c_H_in_C5O2H8': ThermalTuple('lucite', [1001], 1), + 'c_H_in_C8H8': ThermalTuple('hc8h8', [1001], 1), 'c_H_in_CaH2': ThermalTuple('hcah2', [1001], 1), + 'c_H1_in_CaH2': ThermalTuple('h1cah2', [1001], 1), + 'c_H2_in_CaH2': ThermalTuple('h2cah2', [1001], 1), 'c_H_in_CH2': ThermalTuple('hch2', [1001], 1), 'c_H_in_CH4_liquid': ThermalTuple('lch4', [1001], 1), 'c_H_in_CH4_solid': ThermalTuple('sch4', [1001], 1), @@ -49,24 +73,67 @@ 'c_H_in_ZrH2': ThermalTuple('hzrh2', [1001], 1), 'c_H_in_ZrHx': ThermalTuple('hzrhx', [1001], 1), 'c_Li_in_FLiBe': ThermalTuple('liflib', [3006, 3007], 1), + 'c_Li_in_7LiD': ThermalTuple('lilid', [3007], 1), + 'c_Li_in_7LiH': ThermalTuple('lilih', [3007], 1), 'c_Mg24': ThermalTuple('mg24', [12024], 1), + 'c_Mg_in_MgF2': ThermalTuple('mgmgf2', [12024, 12025, 12026], 1), + 'c_Mg_in_MgO': ThermalTuple('mgmgo', [12024, 12025, 12026], 1), + 'c_N_in_UN_100p': ThermalTuple('nun100', [7014, 7015], 1), + 'c_N_in_UN_10p': ThermalTuple('nun10', [7014, 7015], 1), + 'c_N_in_UN_5p': ThermalTuple('nun5', [7014, 7015], 1), 'c_N_in_UN': ThermalTuple('n-un', [7014, 7015], 1), + 'c_N_in_UN_HALEU': ThermalTuple('nunhal', [7014, 7015], 1), + 'c_N_in_UN_HEU': ThermalTuple('nunheu', [7014, 7015], 1), 'c_O_in_Al2O3': ThermalTuple('osap00', [8016, 8017, 8018], 1), 'c_O_in_BeO': ThermalTuple('obeo', [8016, 8017, 8018], 1), + 'c_O_in_C5O2H8': ThermalTuple('olucit', [8016, 8017, 8018], 1), 'c_O_in_D2O': ThermalTuple('od2o', [8016, 8017, 8018], 1), 'c_O_in_H2O_solid': ThermalTuple('oice', [8016, 8017, 8018], 1), + 'c_O_in_MgO': ThermalTuple('omgo', [8016, 8017, 8018], 1), + 'c_O_in_PuO2': ThermalTuple('opuo2', [8016, 8017, 8018], 1), + 'c_O_in_SiO2_alpha': ThermalTuple('osio2a', [8016, 8017, 8018], 1), + 'c_O_in_UO2_100p': ThermalTuple('ouo200', [8016, 8017, 8018], 1), + 'c_O_in_UO2_10p': ThermalTuple('ouo210', [8016, 8017, 8018], 1), + 'c_O_in_UO2_5p': ThermalTuple('ouo25', [8016, 8017, 8018], 1), 'c_O_in_UO2': ThermalTuple('ouo2', [8016, 8017, 8018], 1), + 'c_O_in_UO2_HALEU': ThermalTuple('ouo2hl', [8016, 8017, 8018], 1), + 'c_O_in_UO2_HEU': ThermalTuple('ouo2he', [8016, 8017, 8018], 1), 'c_ortho_D': ThermalTuple('orthod', [1002], 1), 'c_ortho_H': ThermalTuple('orthoh', [1001], 1), 'c_para_D': ThermalTuple('parad', [1002], 1), 'c_para_H': ThermalTuple('parah', [1001], 1), + 'c_Pu_in_PuO2': ThermalTuple('puo2', [94239, 94240, 94241, 94242, 94243], 1), 'c_Si28': ThermalTuple('si00', [14028], 1), 'c_Si_in_SiC': ThermalTuple('sisic', [14028, 14029, 14030], 1), + 'c_Si_in_SiO2_alpha': ThermalTuple('si_o2a', [14028, 14029, 14030], 1), 'c_SiO2_alpha': ThermalTuple('sio2-a', [8016, 8017, 8018, 14028, 14029, 14030], 3), 'c_SiO2_beta': ThermalTuple('sio2-b', [8016, 8017, 8018, 14028, 14029, 14030], 3), + 'c_U_metal_100p': ThermalTuple('u-100p', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_metal_10p': ThermalTuple('u-10p', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_metal_5p': ThermalTuple('u-5p', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_metal': ThermalTuple('umetal', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_metal_HALEU': ThermalTuple('uhaleu', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_metal_HEU': ThermalTuple('u-heu', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_in_UC_100p': ThermalTuple('uc-100', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_in_UC_10p': ThermalTuple('uc-10', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_in_UC_5p': ThermalTuple('uc-5', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_in_UC': ThermalTuple('uc-nat', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_in_UC_HALEU': ThermalTuple('uc-hal', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_in_UC_HEU': ThermalTuple('uc-heu', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_in_UN_100p': ThermalTuple('un-100', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_in_UN_10p': ThermalTuple('un-10', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_in_UN_5p': ThermalTuple('un-5', [92233, 92234, 92235, 92236, 92238], 1), 'c_U_in_UN': ThermalTuple('u-un', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_in_UN_HALEU': ThermalTuple('un-hal', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_in_UN_HEU': ThermalTuple('un-heu', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_in_UO2_100p': ThermalTuple('uo2100', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_in_UO2_10p': ThermalTuple('uo2-10', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_in_UO2_5p': ThermalTuple('uo2-5', [92233, 92234, 92235, 92236, 92238], 1), 'c_U_in_UO2': ThermalTuple('uuo2', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_in_UO2_HALEU': ThermalTuple('uo2hal', [92233, 92234, 92235, 92236, 92238], 1), + 'c_U_in_UO2_HEU': ThermalTuple('uo2heu', [92233, 92234, 92235, 92236, 92238], 1), 'c_Y_in_YH2': ThermalTuple('yyh2', [39089], 1), + 'c_Zr_in_ZrC': ThermalTuple('zrzrc', [40000, 40090, 40091, 40092, 40094, 40096], 1), 'c_Zr_in_ZrH': ThermalTuple('zrzrh', [40000, 40090, 40091, 40092, 40094, 40096], 1), 'c_Zr_in_ZrH2': ThermalTuple('zrzrh2', [40000, 40090, 40091, 40092, 40094, 40096], 1), 'c_Zr_in_ZrHx': ThermalTuple('zrzrhx', [40000, 40090, 40091, 40092, 40094, 40096], 1), diff --git a/openmc/data/thermal.py b/openmc/data/thermal.py index 6df171d3a4f..df4dc150354 100644 --- a/openmc/data/thermal.py +++ b/openmc/data/thermal.py @@ -32,30 +32,54 @@ 'c_Al_in_Al2O3': ('asap00', 'asap', 'al(al2o3)'), 'c_Be': ('be', 'be-metal', 'be-met', 'be00', 'be-metal', 'be metal', '4-be'), 'c_BeO': ('beo',), - 'c_Be_in_BeO': ('bebeo', 'be-beo', 'be-o', 'be/o', 'bbeo00', 'be(beo)'), - 'c_Be_in_Be2C': ('bebe2c',), + 'c_Be_distinct': ('besd', 'be+sd'), + 'c_Be_in_BeO': ('bebeo', 'be-beo', 'be-o', 'be/o', 'bbeo00', 'be(beo)', 'be_beo'), + 'c_Be_in_Be2C': ('bebe2c', 'be(be2c)'), + 'c_Be_in_BeF2': ('bebef2', 'be in bef2'), 'c_Be_in_FLiBe': ('beflib', 'be(flibe)'), 'c_C6H6': ('benz', 'c6h6', 'benzine'), - 'c_C_in_SiC': ('csic', 'c-sic', 'c(3c-sic)'), - 'c_Ca_in_CaH2': ('cah', 'cah00', 'cacah2', 'ca(cah2)'), + 'c_C_in_Be2C': ('cbe2c', 'c(be2c)'), + 'c_C_in_C5O2H8': ('clucit', 'c(lucite)'), + 'c_C_in_C8H8': ('cc8h8', 'c(polystyr'), + 'c_C_in_CF2': ('ccf2', 'c(teflon)'), + 'c_C_in_SiC': ('csic', 'c-sic', 'c(3c-sic)', 'c_sic'), + 'c_C_in_UC_100p': ('cuc100', 'cinuc_100p'), + 'c_C_in_UC_10p': ('cuc10', 'cinuc_10p'), + 'c_C_in_UC_5p': ('cuc5', 'cinuc_5p'), + 'c_C_in_UC': ('cinuc', 'cinuc_nat'), + 'c_C_in_UC_HALEU': ('cuchal', 'cinuc_haleu'), + 'c_C_in_UC_HEU': ('cucheu', 'cinuc_heu'), + 'c_C_in_ZrC': ('czrc', 'c(zrc)'), + 'c_Ca_in_CaH2': ('cah', 'cah00', 'cacah2', 'ca(cah2)', 'ca_cah2'), + 'c_D_in_7LiD': ('dlid', 'd(7lid)'), 'c_D_in_D2O': ('dd2o', 'd-d2o', 'hwtr', 'hw', 'dhw00', 'd(d2o)'), - 'c_D_in_D2O_ice': ('dice',), + 'c_D_in_D2O_solid': ('dice',), + 'c_F_in_Be2': ('fbef2', 'f in bef2'), + 'c_F_in_CF2': ('fcf2', 'f(teflon)'), 'c_F_in_FLiBe': ('fflibe', 'f(flibe)'), + 'c_F_in_HF': ('f_hf',), + 'c_F_in_MgF2': ('fmgf2', 'f in mgf2'), 'c_Fe56': ('fe', 'fe56', 'fe-56', '26-fe- 56'), 'c_Graphite': ('graph', 'grph', 'gr', 'gr00', 'graphite'), 'c_Graphite_10p': ('grph10', '10p graphit'), + 'c_Graphite_20p': ('grph20', '20 graphite'), 'c_Graphite_30p': ('grph30', '30p graphit'), + 'c_Graphite_distinct': ('grphsd', 'grph+sd'), + 'c_H_in_7LiH': ('hlih', 'h(7lih)'), 'c_H_in_C5O2H8': ('lucite', 'c5o2h8', 'h-luci', 'h(lucite)'), + 'c_H_in_C8H8': ('hc8h8', 'h(polystyr'), 'c_H_in_CaH2': ('hcah2', 'hca00', 'h(cah2)'), + 'c_H1_in_CaH2': ('h1cah2', 'h1_cah2'), + 'c_H2_in_CaH2': ('h2cah2', 'h2_cah2'), 'c_H_in_CH2': ('hch2', 'poly', 'pol', 'h-poly', 'pol00', 'h(ch2)'), 'c_H_in_CH4_liquid': ('lch4', 'lmeth', 'l-ch4'), 'c_H_in_CH4_solid': ('sch4', 'smeth', 's-ch4'), 'c_H_in_CH4_solid_phase_II': ('sch4p2',), 'c_H_in_H2O': ('hh2o', 'h-h2o', 'lwtr', 'lw', 'lw00', 'h(h2o)'), 'c_H_in_H2O_solid': ('hice', 'h-ice', 'ice00', 'h(ice-ih)', 'h(ice)'), - 'c_H_in_HF': ('hhf', 'h(hf)'), + 'c_H_in_HF': ('hhf', 'h(hf)', 'h_hf'), 'c_H_in_Mesitylene': ('mesi00', 'mesi', 'mesi-phii'), - 'c_H_in_ParaffinicOil': ('hparaf', 'h(paraffin'), + 'c_H_in_ParaffinicOil': ('hparaf', 'h(paraffin', 'h(paraffini'), 'c_H_in_Toluene': ('tol00', 'tol', 'tolue-phii'), 'c_H_in_UH3': ('huh3', 'h(uh3)'), 'c_H_in_YH2': ('hyh2', 'h-yh2', 'h(yh2)'), @@ -63,24 +87,67 @@ 'c_H_in_ZrH2': ('hzrh2', 'h(zrh2)'), 'c_H_in_ZrHx': ('hzrhx', 'h(zrhx)'), 'c_Li_in_FLiBe': ('liflib', 'li(flibe)'), + 'c_Li_in_7LiD': ('lilid', '7li(7lid)'), + 'c_Li_in_7LiH': ('lilih', '7li(7lih)'), 'c_Mg24': ('mg', 'mg24', 'mg00', '24-mg'), - 'c_N_in_UN': ('n-un', 'n(un)', 'n(un) l'), + 'c_Mg_in_MgF2': ('mgmgf2', 'mg in mgf2'), + 'c_Mg_in_MgO': ('mgmgo', 'mg in mgo'), + 'c_N_in_UN_100p': ('nun100', 'n-un-100p'), + 'c_N_in_UN_10p': ('nun10', 'n-un-10p'), + 'c_N_in_UN_5p': ('nun5', 'n-un-5p'), + 'c_N_in_UN': ('n-un', 'n(un)', 'n(un) l', 'ninun'), + 'c_N_in_UN_HALEU': ('nunhal', 'n-un-haleu'), + 'c_N_in_UN_HEU': ('nunheu', 'n-un-heu'), 'c_O_in_Al2O3': ('osap00', 'osap', 'o(al2o3)'), - 'c_O_in_BeO': ('obeo', 'o-beo', 'o-be', 'o/be', 'obeo00', 'o(beo)'), + 'c_O_in_BeO': ('obeo', 'o-beo', 'o-be', 'o/be', 'obeo00', 'o(beo)', 'o_beo'), + 'c_O_in_C5O2H8': ('olucit', 'o(lucite)'), 'c_O_in_D2O': ('od2o', 'o-d2o', 'ohw00', 'o(d2o)'), 'c_O_in_H2O_solid': ('oice', 'o-ice', 'o(ice-ih)'), + 'c_O_in_MgO': ('omgo', 'o in mgo'), + 'c_O_in_PuO2': ('opuo2', 'o in puo2'), + 'c_O_in_SiO2_alpha': ('osio2a', 'o_sio2a'), + 'c_O_in_UO2_100p': ('ouo200', 'o-uo2-100p'), + 'c_O_in_UO2_10p': ('ouo210', 'oinuo2-10p'), + 'c_O_in_UO2_5p': ('ouo25', 'oinuo2-5p'), 'c_O_in_UO2': ('ouo2', 'o-uo2', 'o2-u', 'o2/u', 'ouo200', 'o(uo2)'), + 'c_O_in_UO2_HALEU': ('ouo2hl', 'ouo2-haleu'), + 'c_O_in_UO2_HEU': ('ouo2he', 'o_uo2-heu'), 'c_ortho_D': ('orthod', 'orthoD', 'dortho', 'od200', 'ortod', 'ortho-d'), 'c_ortho_H': ('orthoh', 'orthoH', 'hortho', 'oh200', 'ortoh', 'ortho-h'), 'c_para_D': ('parad', 'paraD', 'dpara', 'pd200', 'para-d'), 'c_para_H': ('parah', 'paraH', 'hpara', 'ph200', 'para-h'), + 'c_Pu_in_PuO2': ('puo2', 'pu in puo2'), 'c_Si28': ('si00', 'sili', 'si'), - 'c_Si_in_SiC': ('sisic', 'si-sic', 'si(3c-sic)'), + 'c_Si_in_SiC': ('sisic', 'si-sic', 'si(3c-sic)', 'si_sic'), + 'c_Si_in_SiO2_alpha': ('si_o2a', 'si_sio2a'), 'c_SiO2_alpha': ('sio2', 'sio2a', 'sio2alpha'), 'c_SiO2_beta': ('sio2b', 'sio2beta'), - 'c_U_in_UN': ('u-un', 'u(un)', 'u(un) l'), + 'c_U_metal_100p': ('u-100p',), + 'c_U_metal_10p': ('u-10p',), + 'c_U_metal_5p': ('u-5p',), + 'c_U_metal': ('umetal', 'u-metal'), + 'c_U_metal_HALEU': ('uhaleu', 'u-haleu'), + 'c_U_metal_HEU': ('u-heu',), + 'c_U_in_UC_100p': ('uc-100', 'uinuc_100p'), + 'c_U_in_UC_10p': ('uc-10', 'uinuc_10p'), + 'c_U_in_UC_5p': ('uc-5', 'uinuc_5p'), + 'c_U_in_UC': ('uc-nat', 'uinuc_nat'), + 'c_U_in_UC_HALEU': ('uc-hal', 'uinuc_haleu'), + 'c_U_in_UC_HEU': ('uc-heu', 'uinuc_heu'), + 'c_U_in_UN_100p': ('un-100', 'u-un-100p'), + 'c_U_in_UN_10p': ('un-10', 'u-un-10p'), + 'c_U_in_UN_5p': ('un-5', 'u-un-5p'), + 'c_U_in_UN': ('u-un', 'u(un)', 'u(un) l', 'uinun'), + 'c_U_in_UN_HALEU': ('un-hal', 'u-un-haleu'), + 'c_U_in_UN_HEU': ('un-heu', 'u-un-heu'), + 'c_U_in_UO2_100p': ('uo2100', 'uuo2-100p'), + 'c_U_in_UO2_10p': ('uo2-10', 'uuo2-10p'), + 'c_U_in_UO2_5p': ('uo2-5', 'uuo2-5p'), 'c_U_in_UO2': ('uuo2', 'u-uo2', 'u-o2', 'u/o2', 'uuo200', 'u(uo2)'), + 'c_U_in_UO2_HALEU': ('uo2hal', 'uuo2-haleu'), + 'c_U_in_UO2_HEU': ('uo2heu', 'u_uo2-heu'), 'c_Y_in_YH2': ('yyh2', 'y-yh2', 'y(yh2)'), + 'c_Zr_in_ZrC': ('zrzrc', 'zr(zrc)'), 'c_Zr_in_ZrH': ('zrzrh', 'zr-zrh', 'zr-h', 'zr/h', 'zr(zrh)'), 'c_Zr_in_ZrH2': ('zrzrh2', 'zr(zrh2)'), 'c_Zr_in_ZrHx': ('zrzrhx', 'zr(zrhx)'), From 560bd22bce0b3d21d6a5962b4a5da917d4ef6be1 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Tue, 21 Jan 2025 16:27:01 -0600 Subject: [PATCH 127/184] Tweak To Sphinx Install Documentation (#3271) --- docs/source/devguide/docbuild.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/devguide/docbuild.rst b/docs/source/devguide/docbuild.rst index 389a423f3a2..f723db06ea9 100644 --- a/docs/source/devguide/docbuild.rst +++ b/docs/source/devguide/docbuild.rst @@ -12,7 +12,7 @@ Python API. That is, from the root directory of the OpenMC repository: .. code-block:: sh - python -m pip install .[docs] + python -m pip install ".[docs]" ----------------------------------- Building Documentation as a Webpage From 70897800264098b2b3c68c993b89b5582e42f387 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Fri, 24 Jan 2025 14:29:00 -0600 Subject: [PATCH 128/184] Add test for flux bias with weight windows in multigroup mode (#3202) Co-authored-by: Paul Romano --- src/physics_mg.cpp | 6 +-- tests/unit_tests/weightwindows/test_ww_mg.py | 51 ++++++++++++++++++++ tests/unit_tests/weightwindows/ww_mg.txt | 27 +++++++++++ 3 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 tests/unit_tests/weightwindows/test_ww_mg.py create mode 100644 tests/unit_tests/weightwindows/ww_mg.txt diff --git a/src/physics_mg.cpp b/src/physics_mg.cpp index 3cc0532d2b1..e967afd7995 100644 --- a/src/physics_mg.cpp +++ b/src/physics_mg.cpp @@ -28,12 +28,12 @@ void collision_mg(Particle& p) // Add to the collision counter for the particle p.n_collision()++; - if (settings::weight_window_checkpoint_collision) - apply_weight_windows(p); - // Sample the reaction type sample_reaction(p); + if (settings::weight_window_checkpoint_collision) + apply_weight_windows(p); + // Display information about collision if ((settings::verbosity >= 10) || p.trace()) { write_message(fmt::format(" Energy Group = {}", p.g()), 1); diff --git a/tests/unit_tests/weightwindows/test_ww_mg.py b/tests/unit_tests/weightwindows/test_ww_mg.py new file mode 100644 index 00000000000..23e09ea0c07 --- /dev/null +++ b/tests/unit_tests/weightwindows/test_ww_mg.py @@ -0,0 +1,51 @@ +import numpy as np +import openmc + + +def test_weight_windows_mg(request, run_in_tmpdir): + # import basic random ray model + model = openmc.examples.random_ray_three_region_cube() + + # create a mesh tally + mesh = openmc.RegularMesh.from_domain(model.geometry, (3, 3, 3)) + mesh_tally = openmc.Tally() + mesh_tally.filters = [openmc.MeshFilter(mesh)] + mesh_tally.scores = ['flux'] + model.tallies = [mesh_tally] + + # replace random ray settings with fixed source settings + settings = openmc.Settings() + settings.particles = 5000 + settings.batches = 10 + settings.energy_mode = 'multi-group' + settings.run_mode = 'fixed source' + space = openmc.stats.Point((1, 1, 1)) + energy = openmc.stats.delta_function(1e6) + settings.source = openmc.IndependentSource(space=space, energy=energy) + model.settings = settings + + # perform analog simulation + statepoint = model.run() + + # extract flux from analog simulation + with openmc.StatePoint(statepoint) as sp: + tally_out = sp.get_tally(id=mesh_tally.id) + flux_analog = tally_out.mean + + # load the weight windows for this problem and apply them + ww_lower_bnds = np.loadtxt(request.path.parent / 'ww_mg.txt') + weight_windows = openmc.WeightWindows(mesh, lower_ww_bounds=ww_lower_bnds, upper_bound_ratio=5.0) + model.settings.weight_windows = weight_windows + model.settings.weight_windows_on = True + + # re-run with weight windows + statepoint = model.run() + with openmc.StatePoint(statepoint) as sp: + tally_out = sp.get_tally(id=mesh_tally.id) + flux_ww = tally_out.mean + + # the sum of the fluxes should approach the same value (no bias introduced) + analog_sum = flux_analog.sum() + ww_sum = flux_ww.sum() + assert np.allclose(analog_sum, ww_sum, rtol=1e-2) + diff --git a/tests/unit_tests/weightwindows/ww_mg.txt b/tests/unit_tests/weightwindows/ww_mg.txt new file mode 100644 index 00000000000..7bcc59d97b5 --- /dev/null +++ b/tests/unit_tests/weightwindows/ww_mg.txt @@ -0,0 +1,27 @@ +5.000000000000000278e-02 +1.023184121346435924e-02 +3.006624096325660397e-03 +1.030178532774538719e-02 +6.058877789444589400e-03 +2.216914234856166583e-03 +3.061186967456217597e-03 +2.148267952185671601e-03 +1.026171712186230980e-03 +1.040022203443005666e-02 +6.040633813799485378e-03 +2.364143118752318716e-03 +6.119726639841410569e-03 +4.329097093078606955e-03 +1.873104469085542763e-03 +2.246957229279350661e-03 +1.851165248260521617e-03 +7.825824911598703530e-04 +3.021300894848398706e-03 +2.286420236345795311e-03 +9.318583473482396160e-04 +2.234702678114806364e-03 +1.813664566119888152e-03 +7.969287848384389462e-04 +1.017895970086981662e-03 +7.707144532950136471e-04 +3.386087166633241791e-04 From f207d4220afa0582942b2b8f5281e3151ebac429 Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Fri, 24 Jan 2025 16:49:58 -0600 Subject: [PATCH 129/184] Adding methods to automatically apply results to existing Tally objects. (#2671) Co-authored-by: Paul Romano --- openmc/arithmetic.py | 52 +++---------- openmc/cell.py | 3 +- openmc/mgxs/mgxs.py | 2 +- openmc/model/model.py | 24 +++++- openmc/statepoint.py | 40 ++++++---- openmc/tallies.py | 104 ++++++++++++++++++++------ openmc/universe.py | 3 +- tests/regression_tests/source/test.py | 2 +- tests/unit_tests/test_tallies.py | 77 ++++++++++++++++++- 9 files changed, 223 insertions(+), 84 deletions(-) diff --git a/openmc/arithmetic.py b/openmc/arithmetic.py index 821014e36df..92c42284c01 100644 --- a/openmc/arithmetic.py +++ b/openmc/arithmetic.py @@ -93,9 +93,9 @@ class CrossNuclide: Parameters ---------- - left_nuclide : openmc.Nuclide or CrossNuclide + left_nuclide : str or CrossNuclide The left nuclide in the outer product - right_nuclide : openmc.Nuclide or CrossNuclide + right_nuclide : str or CrossNuclide The right nuclide in the outer product binary_op : str The tally arithmetic binary operator (e.g., '+', '-', etc.) used to @@ -103,9 +103,9 @@ class CrossNuclide: Attributes ---------- - left_nuclide : openmc.Nuclide or CrossNuclide + left_nuclide : str or CrossNuclide The left nuclide in the outer product - right_nuclide : openmc.Nuclide or CrossNuclide + right_nuclide : str or CrossNuclide The right nuclide in the outer product binary_op : str The tally arithmetic binary operator (e.g., '+', '-', etc.) used to @@ -134,7 +134,7 @@ def left_nuclide(self): @left_nuclide.setter def left_nuclide(self, left_nuclide): cv.check_type('left_nuclide', left_nuclide, - (openmc.Nuclide, CrossNuclide, AggregateNuclide)) + (str, CrossNuclide, AggregateNuclide)) self._left_nuclide = left_nuclide @property @@ -144,7 +144,7 @@ def right_nuclide(self): @right_nuclide.setter def right_nuclide(self, right_nuclide): cv.check_type('right_nuclide', right_nuclide, - (openmc.Nuclide, CrossNuclide, AggregateNuclide)) + (str, CrossNuclide, AggregateNuclide)) self._right_nuclide = right_nuclide @property @@ -159,26 +159,7 @@ def binary_op(self, binary_op): @property def name(self): - - string = '' - - # If the Summary was linked, the left nuclide is a Nuclide object - if isinstance(self.left_nuclide, openmc.Nuclide): - string += '(' + self.left_nuclide.name - # If the Summary was not linked, the left nuclide is the ZAID - else: - string += '(' + str(self.left_nuclide) - - string += ' ' + self.binary_op + ' ' - - # If the Summary was linked, the right nuclide is a Nuclide object - if isinstance(self.right_nuclide, openmc.Nuclide): - string += self.right_nuclide.name + ')' - # If the Summary was not linked, the right nuclide is the ZAID - else: - string += str(self.right_nuclide) + ')' - - return string + return f'({self.left_nuclide} {self.binary_op} {self.right_nuclide})' class CrossFilter: @@ -440,7 +421,7 @@ class AggregateNuclide: Parameters ---------- - nuclides : Iterable of str or openmc.Nuclide or CrossNuclide + nuclides : Iterable of str or CrossNuclide The nuclides included in the aggregation aggregate_op : str The tally aggregation operator (e.g., 'sum', 'avg', etc.) used @@ -448,7 +429,7 @@ class AggregateNuclide: Attributes ---------- - nuclides : Iterable of str or openmc.Nuclide or CrossNuclide + nuclides : Iterable of str or CrossNuclide The nuclides included in the aggregation aggregate_op : str The tally aggregation operator (e.g., 'sum', 'avg', etc.) used @@ -473,13 +454,7 @@ def __eq__(self, other): return str(other) == str(self) def __repr__(self): - - # Append each nuclide in the aggregate to the string - string = f'{self.aggregate_op}(' - names = [nuclide.name if isinstance(nuclide, openmc.Nuclide) - else str(nuclide) for nuclide in self.nuclides] - string += ', '.join(map(str, names)) + ')' - return string + return f'{self.aggregate_op}{self.name}' @property def nuclides(self): @@ -502,12 +477,9 @@ def aggregate_op(self, aggregate_op): @property def name(self): - # Append each nuclide in the aggregate to the string - names = [nuclide.name if isinstance(nuclide, openmc.Nuclide) - else str(nuclide) for nuclide in self.nuclides] - string = '(' + ', '.join(map(str, names)) + ')' - return string + names = [str(nuclide) for nuclide in self.nuclides] + return '(' + ', '.join(map(str, names)) + ')' class AggregateFilter: diff --git a/openmc/cell.py b/openmc/cell.py index fe3939bbe39..2e0f2ff13bc 100644 --- a/openmc/cell.py +++ b/openmc/cell.py @@ -403,9 +403,8 @@ def get_nuclide_densities(self): if self._atoms is not None: volume = self.volume for name, atoms in self._atoms.items(): - nuclide = openmc.Nuclide(name) density = 1.0e-24 * atoms.n/volume # density in atoms/b-cm - nuclides[name] = (nuclide, density) + nuclides[name] = (name, density) else: raise RuntimeError( 'Volume information is needed to calculate microscopic ' diff --git a/openmc/mgxs/mgxs.py b/openmc/mgxs/mgxs.py index 588b6f64e2a..1bc7f938786 100644 --- a/openmc/mgxs/mgxs.py +++ b/openmc/mgxs/mgxs.py @@ -956,7 +956,7 @@ def _compute_xs(self): self.xs_tally._nuclides = [] nuclides = self.get_nuclides() for nuclide in nuclides: - self.xs_tally.nuclides.append(openmc.Nuclide(nuclide)) + self.xs_tally.nuclides.append(nuclide) # Remove NaNs which may have resulted from divide-by-zero operations self.xs_tally._mean = np.nan_to_num(self.xs_tally.mean) diff --git a/openmc/model/model.py b/openmc/model/model.py index 1c261061aef..80859c55db4 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -14,7 +14,7 @@ import openmc._xml as xml from openmc.dummy_comm import DummyCommunicator from openmc.executor import _process_CLI_arguments -from openmc.checkvalue import check_type, check_value +from openmc.checkvalue import check_type, check_value, PathLike from openmc.exceptions import InvalidIDError from openmc.utility_funcs import change_directory @@ -604,7 +604,8 @@ def import_properties(self, filename): def run(self, particles=None, threads=None, geometry_debug=False, restart_file=None, tracks=False, output=True, cwd='.', openmc_exec='openmc', mpi_args=None, event_based=None, - export_model_xml=True, **export_kwargs): + export_model_xml=True, apply_tally_results=False, + **export_kwargs): """Run OpenMC If the C API has been initialized, then the C API is used, otherwise, @@ -654,6 +655,11 @@ def run(self, particles=None, threads=None, geometry_debug=False, to True. .. versionadded:: 0.13.3 + apply_tally_results : bool + Whether to apply results of the final statepoint file to the + model's tally objects. + + .. versionadded:: 0.15.1 **export_kwargs Keyword arguments passed to either :meth:`Model.export_to_model_xml` or :meth:`Model.export_to_xml`. @@ -725,6 +731,10 @@ def run(self, particles=None, threads=None, geometry_debug=False, if mtime >= tstart: # >= allows for poor clock resolution tstart = mtime last_statepoint = sp + + if apply_tally_results: + self.apply_tally_results(last_statepoint) + return last_statepoint def calculate_volumes(self, threads=None, output=True, cwd='.', @@ -932,6 +942,16 @@ def sample_external_source( n_samples=n_samples, prn_seed=prn_seed ) + def apply_tally_results(self, statepoint: PathLike | openmc.StatePoint): + """Apply results from a statepoint to tally objects on the Model + + Parameters + ---------- + statepoint : PathLike or openmc.StatePoint + Statepoint file used to update tally results + """ + self.tallies.add_results(statepoint) + def plot_geometry(self, output=True, cwd='.', openmc_exec='openmc', export_model_xml=True, **export_kwargs): """Creates plot images as specified by the Model.plots attribute diff --git a/openmc/statepoint.py b/openmc/statepoint.py index 13aac0caf7e..5720f8cd05a 100644 --- a/openmc/statepoint.py +++ b/openmc/statepoint.py @@ -448,14 +448,11 @@ def tallies(self): nuclide_names = group['nuclides'][()] # Add all nuclides to the Tally - for name in nuclide_names: - nuclide = openmc.Nuclide(name.decode().strip()) - tally.nuclides.append(nuclide) + tally.nuclides = [name.decode().strip() for name in nuclide_names] # Add the scores to the Tally scores = group['score_bins'][()] - for score in scores: - tally.scores.append(score.decode()) + tally.scores = [score.decode() for score in scores] # Add Tally to the global dictionary of all Tallies tally.sparse = self.sparse @@ -526,15 +523,16 @@ def add_volume_information(self, volume_calc): def get_tally(self, scores=[], filters=[], nuclides=[], name=None, id=None, estimator=None, exact_filters=False, - exact_nuclides=False, exact_scores=False): + exact_nuclides=False, exact_scores=False, + multiply_density=None, derivative=None): """Finds and returns a Tally object with certain properties. This routine searches the list of Tallies and returns the first Tally found which satisfies all of the input parameters. NOTE: If any of the "exact" parameters are False (default), the input - parameters do not need to match the complete Tally specification and - may only represent a subset of the Tally's properties. If an "exact" + parameters do not need to match the complete Tally specification and may + only represent a subset of the Tally's properties. If an "exact" parameter is True then number of scores, filters, or nuclides in the parameters must precisely match those of any matching Tally. @@ -561,9 +559,15 @@ def get_tally(self, scores=[], filters=[], nuclides=[], to those in the matching Tally. If False (default), the nuclides in the parameters may be a subset of those in the matching Tally. exact_scores : bool - If True, the number of scores in the parameters must be identical - to those in the matching Tally. If False (default), the scores - in the parameters may be a subset of those in the matching Tally. + If True, the number of scores in the parameters must be identical to + those in the matching Tally. If False (default), the scores in the + parameters may be a subset of those in the matching Tally. Default + is None (no check). + multiply_density : bool, optional + If not None, the Tally must have the multiply_density attribute set + to the same value as this parameter. + derivative : openmc.TallyDerivative, optional + TallyDerivative object to match. Returns ------- @@ -591,17 +595,25 @@ def get_tally(self, scores=[], filters=[], nuclides=[], if id and id != test_tally.id: continue - # Determine if Tally has queried estimator - if estimator and estimator != test_tally.estimator: + # Determine if Tally has queried estimator, only move on to next tally + # if the estimator is both specified and the tally estimtor does not + # match + if estimator is not None and estimator != test_tally.estimator: continue # The number of filters, nuclides and scores must exactly match if exact_scores and len(scores) != test_tally.num_scores: continue - if exact_nuclides and len(nuclides) != test_tally.num_nuclides: + if exact_nuclides and nuclides and len(nuclides) != test_tally.num_nuclides: + continue + if exact_nuclides and not nuclides and test_tally.nuclides != ['total']: continue if exact_filters and len(filters) != test_tally.num_filters: continue + if derivative is not None and derivative != test_tally.derivative: + continue + if multiply_density is not None and multiply_density != test_tally.multiply_density: + continue # Determine if Tally has the queried score(s) if scores: diff --git a/openmc/tallies.py b/openmc/tallies.py index f39ffa0789f..b0d78d357c4 100644 --- a/openmc/tallies.py +++ b/openmc/tallies.py @@ -1,3 +1,4 @@ +from __future__ import annotations from collections.abc import Iterable, MutableSequence import copy from functools import partial, reduce @@ -33,7 +34,7 @@ _FILTER_CLASSES = (openmc.Filter, openmc.CrossFilter, openmc.AggregateFilter) # Valid types of estimators -ESTIMATOR_TYPES = ['tracklength', 'collision', 'analog'] +ESTIMATOR_TYPES = {'tracklength', 'collision', 'analog'} class Tally(IDManagerMixin): @@ -65,7 +66,9 @@ class Tally(IDManagerMixin): scores : list of str List of defined scores, e.g. 'flux', 'fission', etc. estimator : {'analog', 'tracklength', 'collision'} - Type of estimator for the tally + Type of estimator for the tally. If unset (None), OpenMC will automatically + select an appropriate estimator based on the tally filters and scores + with a preference for 'tracklength'. triggers : list of openmc.Trigger List of tally triggers num_scores : int @@ -131,6 +134,36 @@ def __init__(self, tally_id=None, name=''): self._sp_filename = None self._results_read = False + def __eq__(self, other): + if other.id != self.id: + return False + if other.name != self.name: + return False + # estimators are automatically set based on the tally filters and scores + # during OpenMC initialization if this value is None, so it is not + # considered a requirement for equivalence if it is unset on either + # tally as it implies that the user is allowing OpenMC to select an + # appropriate estimator. If the value is explicitly set on both tallies, + # then the values must match for the tallies to be considered equivalent. + if self.estimator is not None and other.estimator is not None and other.estimator != self.estimator: + return False + if other.filters != self.filters: + return False + # for tallies are loaded from statpoint files + # an empty nuclide list is equivalent to a list with 'total' + other_nuclides = other.nuclides.copy() + self_nuclides = self.nuclides.copy() + if 'total' in other_nuclides: + other_nuclides.remove('total') + if 'total' in self_nuclides: + self_nuclides.remove('total') + if other_nuclides != self_nuclides: + return False + for attr in {'scores', 'triggers', 'derivative', 'multiply_density'}: + if getattr(other, attr) != getattr(self, attr): + return False + return True + def __repr__(self): parts = ['Tally'] parts.append('{: <15}=\t{}'.format('ID', self.id)) @@ -266,7 +299,8 @@ def estimator(self): @estimator.setter def estimator(self, estimator): - cv.check_value('estimator', estimator, ESTIMATOR_TYPES) + # allow the estimator to be set to None (let OpenMC choose the estimator at runtime) + cv.check_value('estimator', estimator, ESTIMATOR_TYPES | {None}) self._estimator = estimator @property @@ -304,8 +338,16 @@ def _read_results(self): # Open the HDF5 statepoint file with h5py.File(self._sp_filename, 'r') as f: + # Set number of realizations + group = f[f'tallies/tally {self.id}'] + self.num_realizations = int(group['n_realizations'][()]) + + # Update nuclides + nuclide_names = group['nuclides'][()] + self.nuclides = [name.decode().strip() for name in nuclide_names] + # Extract Tally data from the file - data = f[f'tallies/tally {self.id}/results'] + data = group['results'] sum_ = data[:, :, 0] sum_sq = data[:, :, 1] @@ -511,7 +553,7 @@ def remove_nuclide(self, nuclide): Parameters ---------- - nuclide : openmc.Nuclide + nuclide : str Nuclide to remove """ @@ -883,6 +925,21 @@ def to_xml_element(self): return element + def add_results(self, statepoint: cv.PathLike | openmc.StatePoint): + """Add results from the provided statepoint file to this tally instance + + .. versionadded:: 0.15.1 + + Parameters + ---------- + statepoint : openmc.PathLike or openmc.StatePoint + Statepoint used to update tally results + """ + if isinstance(statepoint, openmc.StatePoint): + self._sp_filename = statepoint._f.filename + else: + self._sp_filename = str(statepoint) + @classmethod def from_xml_element(cls, elem, **kwargs): """Generate tally object from an XML element @@ -1020,17 +1077,10 @@ def get_nuclide_index(self, nuclide): in the Tally. """ - # Look for the user-requested nuclide in all of the Tally's Nuclides + # Look for the user-requested nuclide in all of the Tally's nuclides for i, test_nuclide in enumerate(self.nuclides): - # If the Summary was linked, then values are Nuclide objects - if isinstance(test_nuclide, openmc.Nuclide): - if test_nuclide.name == nuclide: - return i - - # If the Summary has not been linked, then values are ZAIDs - else: - if test_nuclide == nuclide: - return i + if test_nuclide == nuclide: + return i msg = (f'Unable to get the nuclide index for Tally since "{nuclide}" ' 'is not one of the nuclides') @@ -1357,9 +1407,7 @@ def get_pandas_dataframe(self, filters=True, nuclides=True, scores=True, column_name = 'nuclide' for nuclide in self.nuclides: - if isinstance(nuclide, openmc.Nuclide): - nuclides.append(nuclide.name) - elif isinstance(nuclide, openmc.AggregateNuclide): + if isinstance(nuclide, openmc.AggregateNuclide): nuclides.append(nuclide.name) column_name = f'{nuclide.aggregate_op}(nuclide)' else: @@ -2350,7 +2398,8 @@ def __truediv__(self, other): new_tally.name = self.name new_tally._mean = self.mean / other new_tally._std_dev = self.std_dev * np.abs(1. / other) - new_tally.estimator = self.estimator + if self.estimator is not None: + new_tally.estimator = self.estimator new_tally.with_summary = self.with_summary new_tally.num_realizations = self.num_realizations @@ -2643,8 +2692,8 @@ def get_slice(self, scores=[], filters=[], filter_bins=[], nuclides=[], # Determine the nuclide indices from any of the requested nuclides for nuclide in self.nuclides: - if nuclide.name not in nuclides: - nuclide_index = self.get_nuclide_index(nuclide.name) + if nuclide not in nuclides: + nuclide_index = self.get_nuclide_index(nuclide) nuclide_indices.append(nuclide_index) # Loop over indices in reverse to remove excluded Nuclides @@ -3166,6 +3215,19 @@ def merge_tallies(self): # Continue iterating from the first loop break + def add_results(self, statepoint: cv.PathLike | openmc.StatePoint): + """Add results from the provided statepoint file + + .. versionadded:: 0.15.1 + + Parameters + ---------- + statepoint : openmc.PathLike or openmc.StatePoint + Statepoint used to update tally results + """ + for tally in self: + tally.add_results(statepoint) + def _create_tally_subelements(self, root_element): for tally in self: root_element.append(tally.to_xml_element()) diff --git a/openmc/universe.py b/openmc/universe.py index 85ce6fd9656..71ffa726be6 100644 --- a/openmc/universe.py +++ b/openmc/universe.py @@ -598,9 +598,8 @@ def get_nuclide_densities(self): if self._atoms: volume = self.volume for name, atoms in self._atoms.items(): - nuclide = openmc.Nuclide(name) density = 1.0e-24 * atoms.n/volume # density in atoms/b-cm - nuclides[name] = (nuclide, density) + nuclides[name] = (name, density) else: raise RuntimeError( 'Volume information is needed to calculate microscopic cross ' diff --git a/tests/regression_tests/source/test.py b/tests/regression_tests/source/test.py index c1f4dccff94..96efa77eb7a 100644 --- a/tests/regression_tests/source/test.py +++ b/tests/regression_tests/source/test.py @@ -11,7 +11,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) mat1 = openmc.Material(material_id=1, temperature=294) mat1.set_density('g/cm3', 4.5) - mat1.add_nuclide(openmc.Nuclide('U235'), 1.0) + mat1.add_nuclide('U235', 1.0) self._model.materials = openmc.Materials([mat1]) sphere = openmc.Sphere(surface_id=1, r=10.0, boundary_type='vacuum') diff --git a/tests/unit_tests/test_tallies.py b/tests/unit_tests/test_tallies.py index 54444331257..f044df5d050 100644 --- a/tests/unit_tests/test_tallies.py +++ b/tests/unit_tests/test_tallies.py @@ -41,4 +41,79 @@ def test_xml_roundtrip(run_in_tmpdir): assert len(new_tally.triggers) == 1 assert new_tally.triggers[0].trigger_type == tally.triggers[0].trigger_type assert new_tally.triggers[0].threshold == tally.triggers[0].threshold - assert new_tally.triggers[0].scores == tally.triggers[0].scores \ No newline at end of file + assert new_tally.triggers[0].scores == tally.triggers[0].scores + assert new_tally.multiply_density == tally.multiply_density + + +def test_tally_equivalence(): + tally_a = openmc.Tally() + tally_b = openmc.Tally(tally_id=tally_a.id) + + tally_a.name = 'new name' + assert tally_a != tally_b + tally_b.name = tally_a.name + assert tally_a == tally_b + + assert tally_a == tally_b + ef_a = openmc.EnergyFilter([0.0, 0.1, 1.0, 10.0e6]) + ef_b = openmc.EnergyFilter([0.0, 0.1, 1.0, 10.0e6]) + + tally_a.filters = [ef_a] + assert tally_a != tally_b + tally_b.filters = [ef_b] + assert tally_a == tally_b + + tally_a.scores = ['flux', 'absorption', 'fission', 'scatter'] + assert tally_a != tally_b + tally_b.scores = ['flux', 'absorption', 'fission', 'scatter'] + assert tally_a == tally_b + + tally_a.nuclides = [] + tally_b.nuclides = [] + assert tally_a == tally_b + + tally_a.nuclides = ['total'] + assert tally_a == tally_b + + # a tally with an estimator set to None is equal to + # a tally with an estimator specified + tally_a.estimator = 'collision' + assert tally_a == tally_b + tally_b.estimator = 'collision' + assert tally_a == tally_b + + tally_a.multiply_density = False + assert tally_a != tally_b + tally_b.multiply_density = False + assert tally_a == tally_b + + trigger_a = openmc.Trigger('rel_err', 0.025) + trigger_b = openmc.Trigger('rel_err', 0.025) + + tally_a.triggers = [trigger_a] + assert tally_a != tally_b + tally_b.triggers = [trigger_b] + assert tally_a == tally_b + + +def test_tally_application(sphere_model, run_in_tmpdir): + # Create a tally with most possible gizmos + tally = openmc.Tally(name='test tally') + ef = openmc.EnergyFilter([0.0, 0.1, 1.0, 10.0e6]) + mesh = openmc.RegularMesh.from_domain(sphere_model.geometry, (2, 2, 2)) + mf = openmc.MeshFilter(mesh) + tally.filters = [ef, mf] + tally.scores = ['flux', 'absorption', 'fission', 'scatter'] + sphere_model.tallies = [tally] + + # run the simulation and apply results + sp_file = sphere_model.run(apply_tally_results=True) + with openmc.StatePoint(sp_file) as sp: + assert tally in sp.tallies.values() + sp_tally = sp.tallies[tally.id] + + # at this point the tally information regarding results should be the same + assert (sp_tally.mean == tally.mean).all() + assert (sp_tally.std_dev == tally.std_dev).all() + assert sp_tally.nuclides == tally.nuclides + From 7a18108724edbdc0adf004308c3955824d9b102c Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Fri, 24 Jan 2025 18:06:15 -0600 Subject: [PATCH 130/184] Adjust for secondary particle energy directly in heating scores (#3227) Co-authored-by: Paul Romano --- include/openmc/particle.h | 6 ++++ include/openmc/particle_data.h | 12 ++++--- src/particle.cpp | 19 ++++++++--- src/physics.cpp | 9 +++-- src/physics_mg.cpp | 9 +++-- src/tallies/tally_scoring.cpp | 13 ++------ src/track_output.cpp | 4 +-- src/weight_windows.cpp | 2 +- tests/unit_tests/weightwindows/test.py | 46 ++++++++++++++++++++++++-- 9 files changed, 83 insertions(+), 37 deletions(-) diff --git a/include/openmc/particle.h b/include/openmc/particle.h index 6a2e67049fd..b1f9fabd110 100644 --- a/include/openmc/particle.h +++ b/include/openmc/particle.h @@ -53,6 +53,12 @@ class Particle : public ParticleData { //! \param type Particle type void create_secondary(double wgt, Direction u, double E, ParticleType type); + //! split a particle + // + //! creates a new particle with weight wgt + //! \param wgt Weight of the new particle + void split(double wgt); + //! initialize from a source site // //! initializes a particle from data stored in a source site. The source diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index 66137c5f688..2255d1a7d88 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -430,7 +430,7 @@ class ParticleData : public GeometryState { int delayed_group_ {0}; int n_bank_ {0}; - int n_bank_second_ {0}; + double bank_second_E_ {0.0}; double wgt_bank_ {0.0}; int n_delayed_bank_[MAX_DELAYED_GROUPS]; @@ -544,11 +544,13 @@ class ParticleData : public GeometryState { int& delayed_group() { return delayed_group_; } // delayed group // Post-collision data - int& n_bank() { return n_bank_; } // number of banked fission sites - int& n_bank_second() + double& bank_second_E() { - return n_bank_second_; - } // number of secondaries banked + return bank_second_E_; + } // energy of last reaction secondaries + const double& bank_second_E() const { return bank_second_E_; } + + int& n_bank() { return n_bank_; } // number of banked fission sites double& wgt_bank() { return wgt_bank_; } // weight of banked fission sites int* n_delayed_bank() { diff --git a/src/particle.cpp b/src/particle.cpp index 65e8065fded..4dbab3213ac 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -91,17 +91,25 @@ void Particle::create_secondary( return; } - secondary_bank().emplace_back(); - - auto& bank {secondary_bank().back()}; + auto& bank = secondary_bank().emplace_back(); bank.particle = type; bank.wgt = wgt; bank.r = r(); bank.u = u; bank.E = settings::run_CE ? E : g(); bank.time = time(); + bank_second_E() += bank.E; +} - n_bank_second() += 1; +void Particle::split(double wgt) +{ + auto& bank = secondary_bank().emplace_back(); + bank.particle = type(); + bank.wgt = wgt; + bank.r = r(); + bank.u = u(); + bank.E = settings::run_CE ? E() : g(); + bank.time = time(); } void Particle::from_source(const SourceSite* src) @@ -356,7 +364,7 @@ void Particle::event_collide() // Reset banked weight during collision n_bank() = 0; - n_bank_second() = 0; + bank_second_E() = 0.0; wgt_bank() = 0.0; zero_delayed_bank(); @@ -417,6 +425,7 @@ void Particle::event_revive_from_secondary() from_source(&secondary_bank().back()); secondary_bank().pop_back(); n_event() = 0; + bank_second_E() = 0.0; // Subtract secondary particle energy from interim pulse-height results if (!model::active_pulse_height_tallies.empty() && diff --git a/src/physics.cpp b/src/physics.cpp index 34dc4ce1cbb..c324ce6b9cf 100644 --- a/src/physics.cpp +++ b/src/physics.cpp @@ -239,11 +239,10 @@ void create_fission_sites(Particle& p, int i_nuclide, const Reaction& rx) } // Write fission particles to nuBank - p.nu_bank().emplace_back(); - NuBank* nu_bank_entry = &p.nu_bank().back(); - nu_bank_entry->wgt = site.wgt; - nu_bank_entry->E = site.E; - nu_bank_entry->delayed_group = site.delayed_group; + NuBank& nu_bank_entry = p.nu_bank().emplace_back(); + nu_bank_entry.wgt = site.wgt; + nu_bank_entry.E = site.E; + nu_bank_entry.delayed_group = site.delayed_group; } // If shared fission bank was full, and no fissions could be added, diff --git a/src/physics_mg.cpp b/src/physics_mg.cpp index e967afd7995..e381b27c9dc 100644 --- a/src/physics_mg.cpp +++ b/src/physics_mg.cpp @@ -188,11 +188,10 @@ void create_fission_sites(Particle& p) } // Write fission particles to nuBank - p.nu_bank().emplace_back(); - NuBank* nu_bank_entry = &p.nu_bank().back(); - nu_bank_entry->wgt = site.wgt; - nu_bank_entry->E = site.E; - nu_bank_entry->delayed_group = site.delayed_group; + NuBank& nu_bank_entry = p.nu_bank().emplace_back(); + nu_bank_entry.wgt = site.wgt; + nu_bank_entry.E = site.E; + nu_bank_entry.delayed_group = site.delayed_group; } // If shared fission bank was full, and no fissions could be added, diff --git a/src/tallies/tally_scoring.cpp b/src/tallies/tally_scoring.cpp index e798161ec2f..02cb4856719 100644 --- a/src/tallies/tally_scoring.cpp +++ b/src/tallies/tally_scoring.cpp @@ -964,14 +964,9 @@ void score_general_ce_nonanalog(Particle& p, int i_tally, int start_index, // The energy deposited is the difference between the pre-collision // and post-collision energy... score = E - p.E(); - // ...less the energy of any secondary particles since they will be // transported individually later - const auto& bank = p.secondary_bank(); - for (auto it = bank.end() - p.n_bank_second(); it < bank.end(); - ++it) { - score -= it->E; - } + score -= p.bank_second_E(); score *= p.wgt_last(); } else { @@ -1500,13 +1495,9 @@ void score_general_ce_analog(Particle& p, int i_tally, int start_index, // The energy deposited is the difference between the pre-collision and // post-collision energy... score = E - p.E(); - // ...less the energy of any secondary particles since they will be // transported individually later - const auto& bank = p.secondary_bank(); - for (auto it = bank.end() - p.n_bank_second(); it < bank.end(); ++it) { - score -= it->E; - } + score -= p.bank_second_E(); score *= p.wgt_last(); } diff --git a/src/track_output.cpp b/src/track_output.cpp index 5c1436de716..f4344d50f74 100644 --- a/src/track_output.cpp +++ b/src/track_output.cpp @@ -31,8 +31,8 @@ int n_tracks_written; //! Number of tracks written void add_particle_track(Particle& p) { - p.tracks().emplace_back(); - p.tracks().back().particle = p.type(); + auto& track = p.tracks().emplace_back(); + track.particle = p.type(); } void write_particle_track(Particle& p) diff --git a/src/weight_windows.cpp b/src/weight_windows.cpp index e798a2f7abd..9579f71c066 100644 --- a/src/weight_windows.cpp +++ b/src/weight_windows.cpp @@ -114,7 +114,7 @@ void apply_weight_windows(Particle& p) // Create secondaries and divide weight among all particles int i_split = std::round(n_split); for (int l = 0; l < i_split - 1; l++) { - p.create_secondary(weight / n_split, p.u(), p.E(), p.type()); + p.split(weight / n_split); } // remaining weight is applied to current particle p.wgt() = weight / n_split; diff --git a/tests/unit_tests/weightwindows/test.py b/tests/unit_tests/weightwindows/test.py index 79aadbef0de..d6e509522fa 100644 --- a/tests/unit_tests/weightwindows/test.py +++ b/tests/unit_tests/weightwindows/test.py @@ -4,7 +4,6 @@ import numpy as np import pytest from uncertainties import ufloat - import openmc import openmc.lib from openmc.stats import Discrete, Point @@ -224,6 +223,47 @@ def test_lower_ww_bounds_shape(): assert ww.lower_ww_bounds.shape == (2, 3, 4, 1) +def test_photon_heating(run_in_tmpdir): + water = openmc.Material() + water.add_nuclide('H1', 1.0) + water.add_nuclide('O16', 2.0) + water.set_density('g/cm3', 1.0) + + box = openmc.model.RectangularParallelepiped( + -300, 300, -300, 300, -300, 300, boundary_type='reflective') + cell = openmc.Cell(region=-box, fill=water) + model = openmc.Model() + model.geometry = openmc.Geometry([cell]) + + mesh = openmc.RegularMesh.from_domain(model.geometry, dimension=(5, 5, 5)) + wwg = openmc.WeightWindowGenerator(mesh, particle_type='photon') + model.settings.weight_window_generators = [wwg] + + space = openmc.stats.Point((0, 0, 0)) + energy = openmc.stats.delta_function(5e6) + model.settings.source = openmc.IndependentSource( + space=space, energy=energy, particle='photon') + + model.settings.run_mode = 'fixed source' + model.settings.batches = 5 + model.settings.particles = 100 + + tally = openmc.Tally() + tally.scores = ['heating'] + tally.filters = [ + openmc.ParticleFilter(['photon']), + openmc.MeshFilter(mesh) + ] + model.tallies = [tally] + + sp_file = model.run() + with openmc.StatePoint(sp_file) as sp: + tally_mean = sp.tallies[tally.id].mean + + # these values should be nearly identical + assert np.all(tally_mean >= 0) + + def test_roundtrip(run_in_tmpdir, model, wws): model.settings.weight_windows = wws @@ -249,11 +289,11 @@ def test_ww_attrs_python(model): # is successful wws = openmc.WeightWindows(mesh, lower_bounds, upper_bound_ratio=10.0) - assert wws.energy_bounds == None + assert wws.energy_bounds is None wwg = openmc.WeightWindowGenerator(mesh) - assert wwg.energy_bounds == None + assert wwg.energy_bounds is None def test_ww_attrs_capi(run_in_tmpdir, model): model.export_to_xml() From 2bea7f338b63c59e9a49f27989c6e9e71299a584 Mon Sep 17 00:00:00 2001 From: Andrew Davis Date: Sat, 25 Jan 2025 01:59:52 +0000 Subject: [PATCH 131/184] Added missing documentation (#3275) Co-authored-by: Jonathan Shimwell Co-authored-by: Paul Romano --- docs/source/io_formats/settings.rst | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index 34b73fc55c5..06149318e61 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -252,11 +252,29 @@ to false. *Default*: true +-------------------------------- +```` Element +-------------------------------- + +This element indicates the maximum number of lost particles. + + *Default*: 10 + +------------------------------------ +```` Element +------------------------------------ + + +This element indicates the maximum number of lost particles, relative to the +total number of particles. + + *Default*: 1.0e-6 + ------------------------------------- ```` Element ------------------------------------- -This element indicates the number of neutrons to run in flight concurrently +This element indicates the number of particles to run in flight concurrently when using event-based parallelism. A higher value uses more memory, but may be more efficient computationally. From a8768b78450d3f2d480062d441c10e0c30eec254 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Mon, 27 Jan 2025 22:54:32 -0600 Subject: [PATCH 132/184] FW-CADIS Weight Window Generation with Random Ray (#3273) Co-authored-by: Olek <45364492+yardasol@users.noreply.github.com> Co-authored-by: Paul Romano --- docs/source/io_formats/settings.rst | 2 +- docs/source/methods/index.rst | 1 + docs/source/methods/random_ray.rst | 2 + docs/source/methods/variance_reduction.rst | 134 ++++++ docs/source/usersguide/index.rst | 1 + docs/source/usersguide/random_ray.rst | 191 +++++++- docs/source/usersguide/variance_reduction.rst | 162 +++++++ include/openmc/weight_windows.h | 20 +- openmc/weight_windows.py | 16 +- src/random_ray/random_ray_simulation.cpp | 1 + src/weight_windows.cpp | 121 +++-- .../weightwindows_fw_cadis/__init__.py | 0 .../weightwindows_fw_cadis/inputs_true.dat | 260 +++++++++++ .../weightwindows_fw_cadis/results_true.dat | 442 ++++++++++++++++++ .../weightwindows_fw_cadis/test.py | 33 ++ tests/testing_harness.py | 34 ++ 16 files changed, 1351 insertions(+), 69 deletions(-) create mode 100644 docs/source/methods/variance_reduction.rst create mode 100644 docs/source/usersguide/variance_reduction.rst create mode 100644 tests/regression_tests/weightwindows_fw_cadis/__init__.py create mode 100644 tests/regression_tests/weightwindows_fw_cadis/inputs_true.dat create mode 100644 tests/regression_tests/weightwindows_fw_cadis/results_true.dat create mode 100644 tests/regression_tests/weightwindows_fw_cadis/test.py diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index 06149318e61..4da57fe2a3e 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -1398,7 +1398,7 @@ mesh-based weight windows. *Default*: true :method: - Method used to update weight window values (currently only 'magic' is supported) + Method used to update weight window values (one of 'magic' or 'fw_cadis') *Default*: magic diff --git a/docs/source/methods/index.rst b/docs/source/methods/index.rst index 08aa7d05341..75c421c8773 100644 --- a/docs/source/methods/index.rst +++ b/docs/source/methods/index.rst @@ -20,4 +20,5 @@ Theory and Methodology energy_deposition parallelization cmfd + variance_reduction random_ray \ No newline at end of file diff --git a/docs/source/methods/random_ray.rst b/docs/source/methods/random_ray.rst index c827c351dd0..eb22b0544da 100644 --- a/docs/source/methods/random_ray.rst +++ b/docs/source/methods/random_ray.rst @@ -1060,6 +1060,8 @@ random ray and Monte Carlo, however. develop the scattering source by way of inactive batches before beginning active batches. +.. _adjoint: + ------------------------ Adjoint Flux Solver Mode ------------------------ diff --git a/docs/source/methods/variance_reduction.rst b/docs/source/methods/variance_reduction.rst new file mode 100644 index 00000000000..353ae5077ea --- /dev/null +++ b/docs/source/methods/variance_reduction.rst @@ -0,0 +1,134 @@ +.. _methods_variance_reduction: + +================== +Variance Reduction +================== + +.. _methods_variance_reduction_intro: + +------------ +Introduction +------------ + +Transport problems can sometimes involve a significant degree of attenuation +between the source and a detector (tally) region, which can result in a flux +differential of ten orders of magnitude (or more) throughout the simulation +domain. As Monte Carlo uncertainties tend to be inversely proportional to the +physical flux density, it can be extremely difficult to accurately resolve +tallies in locations that are optically far from the source. This issue is +particularly common in fixed source simulations, where some tally locations may +not experience a single scoring event, even after billions of analog histories. + +Variance reduction techniques aim to either flatten the global uncertainty +distribution, such that all regions of phase space have a fairly similar +uncertainty, or to reduce the uncertainty in specific locations (such as a +detector). There are two strategies available in OpenMC for variance reduction: +the Monte Carlo MAGIC method and the FW-CADIS method. Both strategies work by +developing a weight window mesh that can be utilized by subsequent Monte Carlo +solves to split particles heading towards areas of lower flux densities while +terminating particles in higher flux regions---all while maintaining a fair +game. + +------------ +MAGIC Method +------------ + +The Method of Automatic Generation of Importances by Calculation, or `MAGIC +method `_, is an iterative +technique that uses spatial flux information :math:`\phi(r)` obtained from a +normal Monte Carlo solve to produce weight windows :math:`w(r)` that can be +utilized by a subsequent iteration of Monte Carlo. While the first generation of +weight windows produced may only help to reduce variance slightly, use of these +weights to generate another set of weight windows results in a progressively +improving iterative scheme. + +Equation :eq:`magic` defines how the lower bound of weight windows +:math:`w_{\ell}(r)` are generated with MAGIC using forward flux information. +Here, we can see that the flux at location :math:`r` is normalized by the +maximum flux in any group at that location. We can also see that the weights are +divided by a factor of two, which accounts for the typical :math:`5\times` +factor separating the lower and upper weight window bounds in OpenMC. + +.. math:: + :label: magic + + w_{\ell}(r) = \frac{\phi(r)}{2\,\text{max}(\phi(r))} + +A major advantage of this technique is that it does not require any special +transport machinery; it simply uses multiple Monte Carlo simulations to +iteratively improve a set of weight windows (which are typically defined on a +mesh covering the simulation domain). The downside to this method is that as the +flux differential increases between areas near and far from the source, it +requires more outer Monte Carlo iterations, each of which can be expensive in +itself. Additionally, computation of weight windows based on regular (forward) +neutron flux tally information does not produce the most numerically effective +set of weight windows. Nonetheless, MAGIC remains a simple and effective +technique for generating weight windows. + +-------- +FW-CADIS +-------- + +As discussed in the previous section, computation of weight windows based on +regular (forward) neutron flux tally information does not produce the most +numerically efficient set of weight windows. It is highly preferable to generate +weight windows based on spatial adjoint flux :math:`\phi^{\dag}(r)` +information. The adjoint flux is essentially the "reverse" simulation problem, +where we sample a random point and assume this is where a particle was absorbed, +and then trace it backwards (upscattering in energy), until we sample the point +where it was born from. + +The Forward-Weighted Consistent Adjoint Driven Importance Sampling method, or +`FW-CADIS method `_, produces weight windows +for global variance reduction given adjoint flux information throughout the +entire domain. The weight window lower bound is defined in Equation +:eq:`fw_cadis`, and also involves a normalization step not shown here. + +.. math:: + :label: fw_cadis + + w_{\ell}(r) = \frac{1}{2\phi^{\dag}(r)} + +While the algorithm itself is quite simple, it requires estimates of the global +adjoint flux distribution, which is difficult to generate directly with Monte +Carlo transport. Thus, FW-CADIS typically uses an alternative solver (often +deterministic) that can be more readily adapted for generating adjoint flux +information, and which is often much cheaper than Monte Carlo given that a rough +solution is often sufficient for weight window generation. + +The FW-CADIS implementation in OpenMC utilizes its own internal random ray +multigroup transport solver to generate the adjoint source distribution. No +coupling to any external transport is solver is necessary. The random ray solver +operates on the same geometry as the Monte Carlo solver, so no redefinition of +the simulation geometry is required. More details on how the adjoint flux is +computed are given in the :ref:`adjoint methods section `. + +More information on the workflow is available in the :ref:`user guide +`, but generally production of weight windows with FW-CADIS +involves several stages (some of which are highly automated). These tasks +include generation of approximate multigroup cross section data for use by the +random ray solver, running of the random ray solver in normal (forward flux) +mode to generate a source for the adjoint solver, running of the random ray +solver in adjoint mode to generate adjoint flux tallies, and finally the +production of weight windows via the FW-CADIS method. As is discussed in the +user guide, most of these steps are automated together, making the additional +burden on the user fairly small. + +The major advantage of this technique is that it typically produces much more +numerically efficient weight windows as compared to those generated with MAGIC, +sometimes with an order-of-magnitude improvement in the figure of merit +(Equation :eq:`variance_fom`), which accounts for both the variance and the +execution time. Another major advantage is that the cost of the random ray +solver is typically negligible compared to the cost of the subsequent Monte +Carlo solve itself, making it a very cheap method to deploy. The downside to +this method is that it introduces a second transport method into the mix (random +ray), such that there are more free input parameters for the user to know about +and adjust, potentially making the method more complex to use. However, as many +of the parameters have natural choices, much of this parameterization can be +handled automatically behind the scenes without the need for the user to be +aware of this. + +.. math:: + :label: variance_fom + + \text{FOM} = \frac{1}{\text{Time} \times \sigma^2} diff --git a/docs/source/usersguide/index.rst b/docs/source/usersguide/index.rst index 03a77e87063..74651c0114b 100644 --- a/docs/source/usersguide/index.rst +++ b/docs/source/usersguide/index.rst @@ -25,6 +25,7 @@ essential aspects of using OpenMC to perform simulations. processing parallel volume + variance_reduction random_ray troubleshoot \ No newline at end of file diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index 2b9cf67240b..b6852b7ca8f 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -435,10 +435,11 @@ Inputting Multigroup Cross Sections (MGXS) Multigroup cross sections for use with OpenMC's random ray solver are input the same way as with OpenMC's traditional multigroup Monte Carlo mode. There is more information on generating multigroup cross sections via OpenMC in the -:ref:`multigroup materials ` user guide. You may also wish to -use an existing multigroup library. An example of using OpenMC's Python -interface to generate a correctly formatted ``mgxs.h5`` input file is given -in the `OpenMC Jupyter notebook collection +:ref:`multigroup materials ` user guide. You may also wish to use +an existing ``mgxs.h5`` MGXS library file, or define your own given a known set +of cross section data values (e.g., as taken from a benchmark specification). An +example of using OpenMC's Python interface to generate a correctly formatted +``mgxs.h5`` input file is given in the `OpenMC Jupyter notebook collection `_. .. note:: @@ -447,6 +448,184 @@ in the `OpenMC Jupyter notebook collection separate materials can be defined each with a separate multigroup dataset corresponding to a given temperature. +.. _mgxs_gen: + +------------------------------------------- +Generating Multigroup Cross Sections (MGXS) +------------------------------------------- + +OpenMC is capable of generating multigroup cross sections by way of flux +collapsing data based on flux solutions obtained from a continuous energy Monte +Carlo solve. While it is a circular excercise in some respects to use continuous +energy Monte Carlo to generate cross sections to be used by a reduced-fidelity +multigroup transport solver, there are many use cases where this is nonetheless +highly desirable. For instance, generation of a multigroup library may enable +the same set of approximate multigroup cross section data to be used across a +variety of problem types (or through a multidimensional parameter sweep of +design variables) with only modest errors and at greatly reduced cost as +compared to using only continuous energy Monte Carlo. + +We give here a quick summary of how to produce a multigroup cross section data +file (``mgxs.h5``) from a starting point of a typical continuous energy Monte +Carlo input file. Notably, continuous energy input files define materials as a +mixture of nuclides with different densities, whereas multigroup materials are +simply defined by which name they correspond to in a ``mgxs.h5`` library file. + +To generate the cross section data, we begin with a continuous energy Monte +Carlo input deck and add in the required tallies that will be needed to generate +our library. In this example, we will specify material-wise cross sections and a +two group energy decomposition:: + + # Define geometry + ... + ... + geometry = openmc.Geometry() + ... + ... + + # Initialize MGXS library with a finished OpenMC geometry object + mgxs_lib = openmc.mgxs.Library(geometry) + + # Pick energy group structure + groups = mgxs.EnergyGroups(mgxs.GROUP_STRUCTURES['CASMO-2']) + mgxs_lib.energy_groups = groups + + # Disable transport correction + mgxs_lib.correction = None + + # Specify needed cross sections for random ray + mgxs_lib.mgxs_types = ['total', 'absorption', 'nu-fission', 'fission', + 'nu-scatter matrix', 'multiplicity matrix', 'chi'] + + # Specify a "cell" domain type for the cross section tally filters + mgxs_lib.domain_type = "material" + + # Specify the cell domains over which to compute multi-group cross sections + mgxs_lib.domains = geom.get_all_materials().values() + + # Do not compute cross sections on a nuclide-by-nuclide basis + mgxs_lib.by_nuclide = False + + # Check the library - if no errors are raised, then the library is satisfactory. + mgxs_lib.check_library_for_openmc_mgxs() + + # Construct all tallies needed for the multi-group cross section library + mgxs_lib.build_library() + + # Create a "tallies.xml" file for the MGXS Library + tallies = openmc.Tallies() + mgxs_lib.add_to_tallies_file(tallies, merge=True) + + # Export + tallies.export_to_xml() + + ... + +When selecting an energy decomposition, you can manually define group boundaries +or pick out a group structure already known to OpenMC (a list of which can be +found at :class:`openmc.mgxs.GROUP_STRUCTURES`). Once the above input deck has +been run, the resulting statepoint file will contain the needed flux and +reaction rate tally data so that a MGXS library file can be generated. Below is +the postprocessing script needed to generate the ``mgxs.h5`` library file given +a statepoint file (e.g., ``statepoint.100.h5``) file and summary file (e.g., +``summary.h5``) that resulted from running our previous example:: + + import openmc + import openmc.mgxs as mgxs + + summary = openmc.Summary('summary.h5') + geom = summary.geometry + mats = summary.materials + + statepoint_filename = 'statepoint.100.h5' + sp = openmc.StatePoint(statepoint_filename) + + groups = mgxs.EnergyGroups(mgxs.GROUP_STRUCTURES['CASMO-2']) + mgxs_lib = openmc.mgxs.Library(geom) + mgxs_lib.energy_groups = groups + mgxs_lib.correction = None + mgxs_lib.mgxs_types = ['total', 'absorption', 'nu-fission', 'fission', + 'nu-scatter matrix', 'multiplicity matrix', 'chi'] + + # Specify a "cell" domain type for the cross section tally filters + mgxs_lib.domain_type = "material" + + # Specify the cell domains over which to compute multi-group cross sections + mgxs_lib.domains = geom.get_all_materials().values() + + # Do not compute cross sections on a nuclide-by-nuclide basis + mgxs_lib.by_nuclide = False + + # Check the library - if no errors are raised, then the library is satisfactory. + mgxs_lib.check_library_for_openmc_mgxs() + + # Construct all tallies needed for the multi-group cross section library + mgxs_lib.build_library() + + mgxs_lib.load_from_statepoint(sp) + + names = [] + for mat in mgxs_lib.domains: names.append(mat.name) + + # Create a MGXS File which can then be written to disk + mgxs_file = mgxs_lib.create_mg_library(xs_type='macro', xsdata_names=names) + + # Write the file to disk using the default filename of "mgxs.h5" + mgxs_file.export_to_hdf5("mgxs.h5") + +Notably, the postprocessing script needs to match the same +:class:`openmc.mgxs.Library` settings that were used to generate the tallies, +but otherwise is able to discern the rest of the simulation details from the +statepoint and summary files. Once the postprocessing script is successfully +run, the ``mgxs.h5`` file can be loaded by subsequent runs of OpenMC. + +If you want to convert continuous energy material objects in an OpenMC input +deck to multigroup ones from a ``mgxs.h5`` library, you can follow the below +example. Here we begin with the original continuous energy materials we used to +generate our MGXS library:: + + fuel = openmc.Material(name='UO2 (2.4%)') + fuel.set_density('g/cm3', 10.29769) + fuel.add_nuclide('U234', 4.4843e-6) + fuel.add_nuclide('U235', 5.5815e-4) + fuel.add_nuclide('U238', 2.2408e-2) + fuel.add_nuclide('O16', 4.5829e-2) + + water = openmc.Material(name='Hot borated water') + water.set_density('g/cm3', 0.740582) + water.add_nuclide('H1', 4.9457e-2) + water.add_nuclide('O16', 2.4672e-2) + water.add_nuclide('B10', 8.0042e-6) + water.add_nuclide('B11', 3.2218e-5) + water.add_s_alpha_beta('c_H_in_H2O') + + materials = openmc.Materials([fuel, water]) + +Once the ``mgxs.h5`` library file has been generated, we can then manually make +the necessary edits to the material definitions so that they load from the +multigroup library instead of defining their isotopic contents, as:: + + # Instantiate some Macroscopic Data + fuel_data = openmc.Macroscopic('UO2 (2.4%)') + water_data = openmc.Macroscopic('Hot borated water') + + # Instantiate some Materials and register the appropriate Macroscopic objects + fuel= openmc.Material(name='UO2 (2.4%)') + fuel.set_density('macro', 1.0) + fuel.add_macroscopic(fuel_data) + + water= openmc.Material(name='Hot borated water') + water.set_density('macro', 1.0) + water.add_macroscopic(water_data) + + # Instantiate a Materials collection and export to XML + materials = openmc.Materials([fuel, water]) + materials.cross_sections = "mgxs.h5" + +In the above example, our ``fuel`` and ``water`` materials will now load MGXS +data from the ``mgxs.h5`` file instead of loading continuous energy isotopic +cross section data. + -------------- Linear Sources -------------- @@ -597,9 +776,7 @@ estimator, the following code would be used: Adjoint Flux Mode ----------------- -The adjoint flux random ray solver mode can be enabled as: -entire -:: +The adjoint flux random ray solver mode can be enabled as:: settings.random_ray['adjoint'] = True diff --git a/docs/source/usersguide/variance_reduction.rst b/docs/source/usersguide/variance_reduction.rst new file mode 100644 index 00000000000..124f342034a --- /dev/null +++ b/docs/source/usersguide/variance_reduction.rst @@ -0,0 +1,162 @@ +.. _variance_reduction: + +================== +Variance Reduction +================== + +Global variance reduction in OpenMC is accomplished by weight windowing +techniques. OpenMC is capable of generating weight windows using either the +MAGIC or FW-CADIS methods. Both techniques will produce a ``weight_windows.h5`` +file that can be loaded and used later on. In this section, we break down the +steps required to both generate and then apply weight windows. + +.. _ww_generator: + +------------------------------------ +Generating Weight Windows with MAGIC +------------------------------------ + +As discussed in the :ref:`methods section `, MAGIC +is an iterative method that uses flux tally information from a Monte Carlo +simulation to produce weight windows for a user-defined mesh. While generating +the weight windows, OpenMC is capable of applying the weight windows generated +from a previous batch while processing the next batch, allowing for progressive +improvement in the weight window quality across iterations. + +The typical way of generating weight windows is to define a mesh and then add an +:class:`openmc.WeightWindowGenerator` object to an :attr:`openmc.Settings` +instance, as follows:: + + # Define weight window spatial mesh + ww_mesh = openmc.RegularMesh() + ww_mesh.dimension = (10, 10, 10) + ww_mesh.lower_left = (0.0, 0.0, 0.0) + ww_mesh.upper_right = (100.0, 100.0, 100.0) + + # Create weight window object and adjust parameters + wwg = openmc.WeightWindowGenerator( + method='magic', + mesh=ww_mesh, + max_realizations=settings.batches + ) + + # Add generator to Settings instance + settings.weight_window_generators = wwg + +Notably, the :attr:`max_realizations` attribute is adjusted to the number of +batches, such that all iterations are used to refine the weight window +parameters. + +With the :class:`~openmc.WeightWindowGenerator` instance added to the +:attr:`~openmc.Settings`, the rest of the problem can be defined as normal. When +running, note that the second iteration and beyond may be several orders of +magnitude slower than the first. As the weight windows are applied in each +iteration, particles may be agressively split, resulting in a large number of +secondary (split) particles being generated per initial source particle. This is +not necessarily a bad thing, as the split particles are much more efficient at +exploring low flux regions of phase space as compared to initial particles. +Thus, even though the reported "particles/second" metric of OpenMC may be much +lower when generating (or just applying) weight windows as compared to analog +MC, it typically leads to an overall improvement in the figure of merit +accounting for the reduction in the variance. + +.. warning:: + The number of particles per batch may need to be adjusted downward + significantly to result in reasonable runtimes when weight windows are being + generated or used. + +At the end of the simulation, a ``weight_windows.h5`` file will be saved to disk +for later use. Loading it in another subsequent simulation will be discussed in +the "Using Weight Windows" section below. + +------------------------------------------------------ +Generating Weight Windows with FW-CADIS and Random Ray +------------------------------------------------------ + +Weight window generation with FW-CADIS and random ray in OpenMC uses the same +exact strategy as with MAGIC. An :class:`openmc.WeightWindowGenerator` object is +added to the :attr:`openmc.Settings` object, and a ``weight_windows.h5`` will be +generated at the end of the simulation. The only difference is that the code +must be run in random ray mode. A full description of how to enable and setup +random ray mode can be found in the :ref:`Random Ray User Guide `. + +.. note:: + It is a long term goal for OpenMC to be able to generate FW-CADIS weight + windows with only a few tweaks to an existing continuous energy Monte Carlo + input deck. However, at the present time, the workflow requires several + steps to generate multigroup cross section data and to configure the random + ray solver. A high level overview of the current workflow for generation of + weight windows with FW-CADIS using random ray is given below. + +1. Produce approximate multigroup cross section data (stored in a ``mgxs.h5`` + library). There is more information on generating multigroup cross sections + via OpenMC in the :ref:`multigroup materials ` user guide, and a + specific example of generating cross section data for use with random ray in + the :ref:`random ray MGXS guide `. + +2. Make a copy of your continuous energy Python input file. You'll edit the new + file to work in multigroup mode with random ray for producing weight windows. + +3. Adjust the material definitions in your new multigroup Python file to utilize + the multigroup cross sections instead of nuclide-wise continuous energy data. + There is a specific example of making this conversion in the :ref:`random ray + MGXS guide `. + +4. Configure OpenMC to run in random ray mode (by adding several standard random + ray input flags and settings to the :attr:`openmc.Settings.random_ray` + dictionary). More information can be found in the :ref:`Random Ray User + Guide `. + +5. Add in a :class:`~openmc.WeightWindowGenerator` in a similar manner as for + MAGIC generation with Monte Carlo and set the :attr:`method` attribute set to + ``"fw_cadis"``:: + + # Define weight window spatial mesh + ww_mesh = openmc.RegularMesh() + ww_mesh.dimension = (10, 10, 10) + ww_mesh.lower_left = (0.0, 0.0, 0.0) + ww_mesh.upper_right = (100.0, 100.0, 100.0) + + # Create weight window object and adjust parameters + wwg = openmc.WeightWindowGenerator( + method='fw_cadis', + mesh=ww_mesh, + max_realizations=settings.batches + ) + + # Add generator to openmc.settings object + settings.weight_window_generators = wwg + + +.. warning:: + If using FW-CADIS weight window generation, ensure that the selected weight + window mesh does not subdivide any cells in the problem. In the future, this + restriction is intended to be relaxed, but for now subdivision of cells by a + mesh tally will result in undefined behavior. + +6. When running your multigroup random ray input deck, OpenMC will automatically + run a forward solve followed by an adjoint solve, with a + ``weight_windows.h5`` file generated at the end. The ``weight_windows.h5`` + file will contain FW-CADIS generated weight windows. This file can be used in + identical manner as one generated with MAGIC, as described below. + +-------------------- +Using Weight Windows +-------------------- + +To use a ``weight_windows.h5`` weight window file with OpenMC's Monte Carlo +solver, the Python input just needs to load the h5 file:: + + settings.weight_window_checkpoints = {'collision': True, 'surface': True} + settings.survival_biasing = False + settings.weight_windows = openmc.hdf5_to_wws('weight_windows.h5') + settings.weight_windows_on = True + +The :class:`~openmc.WeightWindowGenerator` instance is not needed to load an +existing ``weight_windows.h5`` file. Inclusion of a +:class:`~openmc.WeightWindowGenerator` instance will cause OpenMC to generate +*new* weight windows and thus overwrite the existing ``weight_windows.h5`` file. +Weight window mesh information is embedded into the weight window file, so the +mesh does not need to be redefined. Monte Carlo solves that load a weight window +file as above will utilize weight windows to reduce the variance of the +simulation. diff --git a/include/openmc/weight_windows.h b/include/openmc/weight_windows.h index 9852fe679d0..2d4d556948b 100644 --- a/include/openmc/weight_windows.h +++ b/include/openmc/weight_windows.h @@ -17,9 +17,7 @@ namespace openmc { -enum class WeightWindowUpdateMethod { - MAGIC, -}; +enum class WeightWindowUpdateMethod { MAGIC, FW_CADIS }; //============================================================================== // Constants @@ -127,8 +125,9 @@ class WeightWindows { //! \param[in] threshold Relative error threshold. Results over this //! threshold will be ignored \param[in] ratio Ratio of upper to lower //! weight window bounds - void update_magic(const Tally* tally, const std::string& value = "mean", - double threshold = 1.0, double ratio = 5.0); + void update_weights(const Tally* tally, const std::string& value = "mean", + double threshold = 1.0, double ratio = 5.0, + WeightWindowUpdateMethod method = WeightWindowUpdateMethod::MAGIC); // NOTE: This is unused for now but may be used in the future //! Write weight window settings to an HDF5 file @@ -221,12 +220,11 @@ class WeightWindowsGenerator { void create_tally(); // Data members - int32_t tally_idx_; //!< Index of the tally used to update the weight windows - int32_t ww_idx_; //!< Index of the weight windows object being generated - std::string method_; //!< Method used to update weight window. Only "magic" - //!< is valid for now. - int32_t max_realizations_; //!< Maximum number of tally realizations - int32_t update_interval_; //!< Determines how often updates occur + int32_t tally_idx_; //!< Index of the tally used to update the weight windows + int32_t ww_idx_; //!< Index of the weight windows object being generated + WeightWindowUpdateMethod method_; //!< Method used to update weight window. + int32_t max_realizations_; //!< Maximum number of tally realizations + int32_t update_interval_; //!< Determines how often updates occur bool on_the_fly_; //!< Whether or not to keep tally results between batches or //!< realizations diff --git a/openmc/weight_windows.py b/openmc/weight_windows.py index 1ddbfe10fe1..0502d309f5e 100644 --- a/openmc/weight_windows.py +++ b/openmc/weight_windows.py @@ -660,9 +660,8 @@ class WeightWindowGenerator: maximum and minimum energy for the data available at runtime. particle_type : {'neutron', 'photon'} Particle type the weight windows apply to - method : {'magic'} - The weight window generation methodology applied during an update. Only - 'magic' is currently supported. + method : {'magic', 'fw_cadis'} + The weight window generation methodology applied during an update. max_realizations : int The upper limit for number of tally realizations when generating weight windows. @@ -680,9 +679,8 @@ class WeightWindowGenerator: energies in [eV] for a single bin particle_type : {'neutron', 'photon'} Particle type the weight windows apply to - method : {'magic'} - The weight window generation methodology applied during an update. Only - 'magic' is currently supported. + method : {'magic', 'fw_cadis'} + The weight window generation methodology applied during an update. max_realizations : int The upper limit for number of tally realizations when generating weight windows. @@ -767,7 +765,7 @@ def method(self) -> str: @method.setter def method(self, m: str): cv.check_type('generation method', m, str) - cv.check_value('generation method', m, {'magic'}) + cv.check_value('generation method', m, ('magic', 'fw_cadis')) self._method = m if self._update_parameters is not None: try: @@ -800,7 +798,7 @@ def update_parameters(self) -> dict: return self._update_parameters def _check_update_parameters(self, params: dict): - if self.method == 'magic': + if self.method == 'magic' or self.method == 'fw_cadis': check_params = self._MAGIC_PARAMS for key, val in params.items(): @@ -843,7 +841,7 @@ def _sanitize_update_parameters(cls, method: str, update_parameters: dict): update_parameters : dict The update parameters as-read from the XML node (keys: str, values: str) """ - if method == 'magic': + if method == 'magic' or method == 'fw_cadis': check_params = cls._MAGIC_PARAMS for param, param_type in check_params.items(): diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index a9180c68e7b..a8e6d1b8229 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -173,6 +173,7 @@ void validate_random_ray_inputs() case FilterType::MATERIAL: case FilterType::MESH: case FilterType::UNIVERSE: + case FilterType::PARTICLE: break; default: fatal_error("Invalid filter specified. Only cell, cell_instance, " diff --git a/src/weight_windows.cpp b/src/weight_windows.cpp index 9579f71c066..19ae7c07782 100644 --- a/src/weight_windows.cpp +++ b/src/weight_windows.cpp @@ -22,6 +22,7 @@ #include "openmc/particle.h" #include "openmc/particle_data.h" #include "openmc/physics_common.h" +#include "openmc/random_ray/flat_source_domain.h" #include "openmc/search.h" #include "openmc/settings.h" #include "openmc/tallies/filter_energy.h" @@ -482,8 +483,8 @@ void WeightWindows::set_bounds( upper_ww_ *= ratio; } -void WeightWindows::update_magic( - const Tally* tally, const std::string& value, double threshold, double ratio) +void WeightWindows::update_weights(const Tally* tally, const std::string& value, + double threshold, double ratio, WeightWindowUpdateMethod method) { /////////////////////////// // Setup and checks @@ -624,20 +625,44 @@ void WeightWindows::update_magic( auto mesh_vols = this->mesh()->volumes(); int e_bins = new_bounds.shape()[0]; - for (int e = 0; e < e_bins; e++) { - // select all - auto group_view = xt::view(new_bounds, e); - // divide by volume of mesh elements - for (int i = 0; i < group_view.size(); i++) { - group_view[i] /= mesh_vols[i]; + if (method == WeightWindowUpdateMethod::MAGIC) { + // If we are computing weight windows with forward fluxes derived from a + // Monte Carlo or forward random ray solve, we use the MAGIC algorithm. + for (int e = 0; e < e_bins; e++) { + // select all + auto group_view = xt::view(new_bounds, e); + + // divide by volume of mesh elements + for (int i = 0; i < group_view.size(); i++) { + group_view[i] /= mesh_vols[i]; + } + + double group_max = + *std::max_element(group_view.begin(), group_view.end()); + // normalize values in this energy group by the maximum value for this + // group + if (group_max > 0.0) + group_view /= 2.0 * group_max; } + } else { + // If we are computing weight windows with adjoint fluxes derived from an + // adjoint random ray solve, we use the FW-CADIS algorithm. + for (int e = 0; e < e_bins; e++) { + // select all + auto group_view = xt::view(new_bounds, e); + + // divide by volume of mesh elements + for (int i = 0; i < group_view.size(); i++) { + group_view[i] /= mesh_vols[i]; + } + } + + xt::noalias(new_bounds) = 1.0 / new_bounds; - double group_max = *std::max_element(group_view.begin(), group_view.end()); - // normalize values in this energy group by the maximum value for this - // group - if (group_max > 0.0) - group_view /= 2.0 * group_max; + auto max_val = xt::amax(new_bounds)(); + + xt::noalias(new_bounds) = new_bounds / (2.0 * max_val); } // make sure that values where the mean is zero are set s.t. the weight window @@ -760,37 +785,51 @@ WeightWindowsGenerator::WeightWindowsGenerator(pugi::xml_node node) e_bounds.push_back(data::energy_max[p_type]); } - // set method and parameters for updates - method_ = get_node_value(node, "method"); - if (method_ == "magic") { - // parse non-default update parameters if specified - if (check_for_node(node, "update_parameters")) { - pugi::xml_node params_node = node.child("update_parameters"); - if (check_for_node(params_node, "value")) - tally_value_ = get_node_value(params_node, "value"); - if (check_for_node(params_node, "threshold")) - threshold_ = std::stod(get_node_value(params_node, "threshold")); - if (check_for_node(params_node, "ratio")) { - ratio_ = std::stod(get_node_value(params_node, "ratio")); - } + // set method + std::string method_string = get_node_value(node, "method"); + if (method_string == "magic") { + method_ = WeightWindowUpdateMethod::MAGIC; + if (settings::solver_type == SolverType::RANDOM_RAY && + FlatSourceDomain::adjoint_) { + fatal_error("Random ray weight window generation with MAGIC cannot be " + "done in adjoint mode."); } - // check update parameter values - if (tally_value_ != "mean" && tally_value_ != "rel_err") { - fatal_error(fmt::format("Unsupported tally value '{}' specified for " - "weight window generation.", - tally_value_)); + } else if (method_string == "fw_cadis") { + method_ = WeightWindowUpdateMethod::FW_CADIS; + if (settings::solver_type != SolverType::RANDOM_RAY) { + fatal_error("FW-CADIS can only be run in random ray solver mode."); } - if (threshold_ <= 0.0) - fatal_error(fmt::format("Invalid relative error threshold '{}' (<= 0.0) " - "specified for weight window generation", - ratio_)); - if (ratio_ <= 1.0) - fatal_error(fmt::format("Invalid weight window ratio '{}' (<= 1.0) " - "specified for weight window generation")); + FlatSourceDomain::adjoint_ = true; } else { fatal_error(fmt::format( - "Unknown weight window update method '{}' specified", method_)); + "Unknown weight window update method '{}' specified", method_string)); + } + + // parse non-default update parameters if specified + if (check_for_node(node, "update_parameters")) { + pugi::xml_node params_node = node.child("update_parameters"); + if (check_for_node(params_node, "value")) + tally_value_ = get_node_value(params_node, "value"); + if (check_for_node(params_node, "threshold")) + threshold_ = std::stod(get_node_value(params_node, "threshold")); + if (check_for_node(params_node, "ratio")) { + ratio_ = std::stod(get_node_value(params_node, "ratio")); + } + } + + // check update parameter values + if (tally_value_ != "mean" && tally_value_ != "rel_err") { + fatal_error(fmt::format("Unsupported tally value '{}' specified for " + "weight window generation.", + tally_value_)); } + if (threshold_ <= 0.0) + fatal_error(fmt::format("Invalid relative error threshold '{}' (<= 0.0) " + "specified for weight window generation", + ratio_)); + if (ratio_ <= 1.0) + fatal_error(fmt::format("Invalid weight window ratio '{}' (<= 1.0) " + "specified for weight window generation")); // create a matching weight windows object auto wws = WeightWindows::create(); @@ -860,7 +899,7 @@ void WeightWindowsGenerator::update() const tally->n_realizations_ % update_interval_ != 0) return; - wws->update_magic(tally, tally_value_, threshold_, ratio_); + wws->update_weights(tally, tally_value_, threshold_, ratio_, method_); // if we're not doing on the fly generation, reset the tally results once // we're done with the update @@ -944,7 +983,7 @@ extern "C" int openmc_weight_windows_update_magic(int32_t ww_idx, // get the WeightWindows object const auto& wws = variance_reduction::weight_windows.at(ww_idx); - wws->update_magic(tally, value, threshold, ratio); + wws->update_weights(tally, value, threshold, ratio); return 0; } diff --git a/tests/regression_tests/weightwindows_fw_cadis/__init__.py b/tests/regression_tests/weightwindows_fw_cadis/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regression_tests/weightwindows_fw_cadis/inputs_true.dat b/tests/regression_tests/weightwindows_fw_cadis/inputs_true.dat new file mode 100644 index 00000000000..586bbb12026 --- /dev/null +++ b/tests/regression_tests/weightwindows_fw_cadis/inputs_true.dat @@ -0,0 +1,260 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 10 + 5 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + + 1 + neutron + 10 + 1 + true + fw_cadis + + + + 6 6 6 + 0.0 0.0 0.0 + 30.0 30.0 30.0 + + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + naive + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/weightwindows_fw_cadis/results_true.dat b/tests/regression_tests/weightwindows_fw_cadis/results_true.dat new file mode 100644 index 00000000000..e39b2e23900 --- /dev/null +++ b/tests/regression_tests/weightwindows_fw_cadis/results_true.dat @@ -0,0 +1,442 @@ +RegularMesh + ID = 1 + Name = + Dimensions = 3 + Voxels = [6 6 6] + Lower left = [0. 0. 0.] + Upper Right = [np.float64(30.0), np.float64(30.0), np.float64(30.0)] + Width = [5. 5. 5.] +Lower Bounds +1.50e-01 +1.54e-01 +1.43e-01 +1.30e-01 +1.30e-01 +1.97e-01 +1.46e-01 +1.25e-01 +1.34e-01 +1.32e-01 +1.27e-01 +3.40e-01 +1.36e-01 +1.13e-01 +1.09e-01 +1.19e-01 +1.25e-01 +3.08e-01 +1.38e-01 +1.18e-01 +9.49e-02 +1.19e-01 +1.35e-01 +1.96e-01 +1.12e-01 +9.60e-02 +9.33e-02 +9.74e-02 +1.37e-01 +2.76e-01 +4.53e-02 +1.31e-01 +2.47e-01 +7.36e-02 +1.72e-01 +1.38e-01 +1.55e-01 +1.54e-01 +1.48e-01 +1.36e-01 +1.29e-01 +1.88e-01 +1.40e-01 +1.51e-01 +1.28e-01 +1.18e-01 +1.29e-01 +2.86e-01 +1.42e-01 +1.26e-01 +1.28e-01 +1.16e-01 +1.22e-01 +2.81e-01 +1.38e-01 +1.22e-01 +1.04e-01 +9.81e-02 +1.08e-01 +1.97e-01 +1.34e-01 +1.32e-01 +1.10e-01 +8.69e-02 +1.03e-01 +1.33e-01 +2.51e-01 +3.39e-01 +1.13e-01 +6.92e-02 +1.54e-01 +5.71e-02 +1.60e-01 +1.52e-01 +1.34e-01 +1.32e-01 +1.28e-01 +2.53e-01 +1.33e-01 +1.35e-01 +1.15e-01 +1.32e-01 +1.37e-01 +3.13e-01 +1.27e-01 +1.36e-01 +1.35e-01 +1.10e-01 +1.08e-01 +8.81e-02 +1.51e-01 +1.32e-01 +1.13e-01 +9.89e-02 +9.25e-02 +5.04e-02 +1.40e-01 +1.56e-01 +1.40e-01 +9.19e-02 +6.54e-02 +2.05e-02 +3.35e-01 +1.88e-01 +3.16e-01 +1.80e-01 +5.78e-02 +5.36e-03 +1.43e-01 +1.35e-01 +1.47e-01 +1.33e-01 +1.47e-01 +4.95e-01 +1.42e-01 +1.45e-01 +1.27e-01 +1.31e-01 +1.26e-01 +1.43e-01 +1.49e-01 +1.23e-01 +1.24e-01 +1.13e-01 +9.69e-02 +1.07e-01 +1.65e-01 +1.50e-01 +1.24e-01 +1.07e-01 +8.90e-02 +8.81e-02 +1.48e-01 +1.58e-01 +1.20e-01 +7.93e-02 +7.19e-02 +3.94e-02 +2.29e-01 +1.95e-01 +2.66e-01 +1.51e-01 +3.52e-02 +3.68e-03 +1.48e-01 +1.18e-01 +1.47e-01 +1.41e-01 +1.37e-01 +2.71e-01 +1.70e-01 +1.70e-01 +1.29e-01 +1.33e-01 +1.28e-01 +2.79e-01 +1.84e-01 +1.42e-01 +1.18e-01 +1.25e-01 +1.26e-01 +1.64e-01 +1.73e-01 +1.37e-01 +1.12e-01 +9.01e-02 +1.13e-01 +1.68e-01 +1.60e-01 +1.52e-01 +1.30e-01 +8.71e-02 +7.72e-02 +3.76e-02 +1.57e-01 +9.31e-02 +1.63e-01 +6.80e-02 +6.22e-02 +3.37e-03 +2.47e-01 +2.17e-01 +1.48e-01 +2.01e-01 +1.54e-01 +5.69e-02 +2.89e-01 +3.59e-01 +1.17e-01 +8.64e-02 +3.34e-01 +1.50e-01 +5.00e-01 +2.16e-01 +1.22e-01 +1.66e-01 +1.44e-01 +4.98e-02 +4.28e-01 +1.17e-01 +8.73e-02 +1.11e-01 +1.09e-01 +9.69e-02 +3.26e-01 +2.28e-01 +2.49e-01 +3.55e-02 +1.69e-02 +3.95e-03 +1.03e-01 +4.89e-02 +1.90e-01 +4.61e-02 +9.75e-03 +3.00e-04 +Upper Bounds +7.49e-01 +7.71e-01 +7.13e-01 +6.51e-01 +6.48e-01 +9.85e-01 +7.31e-01 +6.26e-01 +6.69e-01 +6.59e-01 +6.33e-01 +1.70e+00 +6.82e-01 +5.67e-01 +5.45e-01 +5.94e-01 +6.24e-01 +1.54e+00 +6.92e-01 +5.91e-01 +4.75e-01 +5.96e-01 +6.77e-01 +9.82e-01 +5.59e-01 +4.80e-01 +4.67e-01 +4.87e-01 +6.84e-01 +1.38e+00 +2.26e-01 +6.55e-01 +1.24e+00 +3.68e-01 +8.59e-01 +6.88e-01 +7.73e-01 +7.70e-01 +7.42e-01 +6.78e-01 +6.46e-01 +9.40e-01 +7.00e-01 +7.57e-01 +6.39e-01 +5.92e-01 +6.43e-01 +1.43e+00 +7.12e-01 +6.29e-01 +6.40e-01 +5.78e-01 +6.09e-01 +1.40e+00 +6.90e-01 +6.10e-01 +5.21e-01 +4.91e-01 +5.42e-01 +9.87e-01 +6.71e-01 +6.60e-01 +5.52e-01 +4.35e-01 +5.14e-01 +6.65e-01 +1.26e+00 +1.70e+00 +5.63e-01 +3.46e-01 +7.72e-01 +2.86e-01 +8.00e-01 +7.58e-01 +6.70e-01 +6.59e-01 +6.41e-01 +1.26e+00 +6.65e-01 +6.76e-01 +5.76e-01 +6.60e-01 +6.86e-01 +1.57e+00 +6.35e-01 +6.81e-01 +6.75e-01 +5.51e-01 +5.39e-01 +4.40e-01 +7.56e-01 +6.62e-01 +5.67e-01 +4.95e-01 +4.63e-01 +2.52e-01 +6.98e-01 +7.78e-01 +6.99e-01 +4.59e-01 +3.27e-01 +1.02e-01 +1.67e+00 +9.38e-01 +1.58e+00 +9.00e-01 +2.89e-01 +2.68e-02 +7.16e-01 +6.75e-01 +7.35e-01 +6.63e-01 +7.34e-01 +2.47e+00 +7.11e-01 +7.23e-01 +6.34e-01 +6.54e-01 +6.32e-01 +7.14e-01 +7.45e-01 +6.15e-01 +6.19e-01 +5.64e-01 +4.85e-01 +5.37e-01 +8.26e-01 +7.51e-01 +6.21e-01 +5.36e-01 +4.45e-01 +4.40e-01 +7.40e-01 +7.92e-01 +5.99e-01 +3.96e-01 +3.60e-01 +1.97e-01 +1.14e+00 +9.76e-01 +1.33e+00 +7.55e-01 +1.76e-01 +1.84e-02 +7.42e-01 +5.90e-01 +7.37e-01 +7.07e-01 +6.84e-01 +1.36e+00 +8.52e-01 +8.49e-01 +6.46e-01 +6.63e-01 +6.42e-01 +1.40e+00 +9.19e-01 +7.08e-01 +5.89e-01 +6.24e-01 +6.28e-01 +8.21e-01 +8.64e-01 +6.87e-01 +5.58e-01 +4.50e-01 +5.64e-01 +8.40e-01 +7.99e-01 +7.62e-01 +6.51e-01 +4.36e-01 +3.86e-01 +1.88e-01 +7.85e-01 +4.66e-01 +8.13e-01 +3.40e-01 +3.11e-01 +1.68e-02 +1.24e+00 +1.09e+00 +7.40e-01 +1.00e+00 +7.72e-01 +2.85e-01 +1.44e+00 +1.79e+00 +5.85e-01 +4.32e-01 +1.67e+00 +7.50e-01 +2.50e+00 +1.08e+00 +6.10e-01 +8.32e-01 +7.19e-01 +2.49e-01 +2.14e+00 +5.87e-01 +4.36e-01 +5.57e-01 +5.47e-01 +4.84e-01 +1.63e+00 +1.14e+00 +1.25e+00 +1.78e-01 +8.43e-02 +1.97e-02 +5.17e-01 +2.44e-01 +9.49e-01 +2.31e-01 +4.87e-02 +1.50e-03 \ No newline at end of file diff --git a/tests/regression_tests/weightwindows_fw_cadis/test.py b/tests/regression_tests/weightwindows_fw_cadis/test.py new file mode 100644 index 00000000000..7a4718ed5b2 --- /dev/null +++ b/tests/regression_tests/weightwindows_fw_cadis/test.py @@ -0,0 +1,33 @@ +import os + +import openmc +from openmc.examples import random_ray_three_region_cube + +from tests.testing_harness import WeightWindowPyAPITestHarness + + +class MGXSTestHarness(WeightWindowPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + + +def test_random_ray_adjoint_fixed_source(): + model = random_ray_three_region_cube() + + ww_mesh = openmc.RegularMesh() + n = 6 + width = 30.0 + ww_mesh.dimension = (n, n, n) + ww_mesh.lower_left = (0.0, 0.0, 0.0) + ww_mesh.upper_right = (width, width, width) + + wwg = openmc.WeightWindowGenerator( + method="fw_cadis", mesh=ww_mesh, max_realizations=model.settings.batches) + model.settings.weight_window_generators = wwg + model.settings.random_ray['volume_estimator'] = 'naive' + + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/testing_harness.py b/tests/testing_harness.py index 81527452b84..ddae89e02e9 100644 --- a/tests/testing_harness.py +++ b/tests/testing_harness.py @@ -441,6 +441,40 @@ def _compare_results(self): assert compare, 'Results do not agree' +class WeightWindowPyAPITestHarness(PyAPITestHarness): + def _get_results(self): + """Digest info in the weight window file and return as a string.""" + ww = openmc.hdf5_to_wws()[0] + + # Access the weight window bounds + lower_bound = ww.lower_ww_bounds + upper_bound = ww.upper_ww_bounds + + # Flatten both arrays + flattened_lower_bound = lower_bound.flatten() + flattened_upper_bound = upper_bound.flatten() + + # Convert each element to a string in scientific notation with 2 decimal places + formatted_lower_bound = [f'{x:.2e}' for x in flattened_lower_bound] + formatted_upper_bound = [f'{x:.2e}' for x in flattened_upper_bound] + + # Concatenate the formatted arrays + concatenated_strings = ["Lower Bounds"] + formatted_lower_bound + \ + ["Upper Bounds"] + formatted_upper_bound + + # Join the concatenated strings into a single string with newline characters + final_string = '\n'.join(concatenated_strings) + + # Prepend the mesh text description and return final string + return str(ww.mesh) + final_string + + def _cleanup(self): + super()._cleanup() + f = 'weight_windows.h5' + if os.path.exists(f): + os.remove(f) + + class PlotTestHarness(TestHarness): """Specialized TestHarness for running OpenMC plotting tests.""" def __init__(self, plot_names, voxel_convert_checks=[]): From 8626ce5c4352634d18d62ab328448abfa99bdc02 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Mon, 27 Jan 2025 23:31:59 -0800 Subject: [PATCH 133/184] Rely on std::filesystem for file_utils (#3042) Co-authored-by: Andrew Johnson Co-authored-by: Paul Romano --- include/openmc/file_utils.h | 8 +++- src/cross_sections.cpp | 25 +++++----- src/dagmc.cpp | 4 +- src/file_utils.cpp | 61 ++++++++---------------- src/simulation.cpp | 3 +- src/state_point.cpp | 10 ++-- tests/cpp_unit_tests/test_file_utils.cpp | 9 ++++ 7 files changed, 53 insertions(+), 67 deletions(-) diff --git a/include/openmc/file_utils.h b/include/openmc/file_utils.h index 6e9812d5e83..db65640344c 100644 --- a/include/openmc/file_utils.h +++ b/include/openmc/file_utils.h @@ -5,7 +5,10 @@ namespace openmc { -// TODO: replace with std::filesystem when switch to C++17 is made +// NOTE: This is a thin wrapper over std::filesystem because we +// pass strings around a lot. Objects like settings::path_input +// are extern std::string to play with other libraries and languages + //! Determine if a path is a directory //! \param[in] path Path to check //! \return Whether the path is a directory @@ -18,7 +21,8 @@ bool file_exists(const std::string& filename); //! Determine directory containing given file //! \param[in] filename Path to file -//! \return Name of directory containing file +//! \return Name of directory containing file excluding the final directory +//! separator std::string dir_name(const std::string& filename); // Gets the file extension of whatever string is passed in. This is defined as diff --git a/src/cross_sections.cpp b/src/cross_sections.cpp index 248ae9019d9..b1bfde03d13 100644 --- a/src/cross_sections.cpp +++ b/src/cross_sections.cpp @@ -23,6 +23,7 @@ #include "pugixml.hpp" #include // for getenv +#include #include namespace openmc { @@ -282,15 +283,16 @@ void read_ce_cross_sections(const vector>& nuc_temps, void read_ce_cross_sections_xml() { // Check if cross_sections.xml exists - const auto& filename = settings::path_cross_sections; - if (dir_exists(filename)) { + std::filesystem::path filename(settings::path_cross_sections); + if (!std::filesystem::exists(filename)) { + fatal_error( + "Cross sections XML file '" + filename.string() + "' does not exist."); + } + + if (std::filesystem::is_directory(filename)) { fatal_error("OPENMC_CROSS_SECTIONS is set to a directory. " "It should be set to an XML file."); } - if (!file_exists(filename)) { - // Could not find cross_sections.xml file - fatal_error("Cross sections XML file '" + filename + "' does not exist."); - } write_message("Reading cross sections XML file...", 5); @@ -309,15 +311,10 @@ void read_ce_cross_sections_xml() } else { // If no directory is listed in cross_sections.xml, by default select the // directory in which the cross_sections.xml file resides - - // TODO: Use std::filesystem functionality when C++17 is adopted - auto pos = filename.rfind("/"); - if (pos == std::string::npos) { - // No '\\' found, so the file must be in the same directory as - // materials.xml - directory = settings::path_input; + if (filename.has_parent_path()) { + directory = filename.parent_path().string(); } else { - directory = filename.substr(0, pos); + directory = settings::path_input; } } diff --git a/src/dagmc.cpp b/src/dagmc.cpp index 94c220850f3..52c45f21c28 100644 --- a/src/dagmc.cpp +++ b/src/dagmc.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -56,7 +57,8 @@ DAGUniverse::DAGUniverse(pugi::xml_node node) if (check_for_node(node, "filename")) { filename_ = get_node_value(node, "filename"); if (!starts_with(filename_, "/")) { - filename_ = dir_name(settings::path_input) + filename_; + std::filesystem::path d(dir_name(settings::path_input)); + filename_ = (d / filename_).string(); } } else { fatal_error("Must specify a file for the DAGMC universe"); diff --git a/src/file_utils.cpp b/src/file_utils.cpp index 197c9767cb0..517f82d157e 100644 --- a/src/file_utils.cpp +++ b/src/file_utils.cpp @@ -1,65 +1,42 @@ #include "openmc/file_utils.h" -#include // any_of -#include // for isalpha -#include // for ifstream -#include +#include namespace openmc { bool dir_exists(const std::string& path) { - struct stat s; - if (stat(path.c_str(), &s) != 0) - return false; - - return s.st_mode & S_IFDIR; + std::filesystem::path d(path); + return std::filesystem::is_directory(d); } bool file_exists(const std::string& filename) { - // rule out file being a path to a directory - if (dir_exists(filename)) + std::filesystem::path p(filename); + if (!std::filesystem::exists(p)) { return false; - - std::ifstream s {filename}; - return s.good(); + } + if (std::filesystem::is_directory(p)) { + return false; + } + return true; } std::string dir_name(const std::string& filename) { - size_t pos = filename.find_last_of("\\/"); - return (std::string::npos == pos) ? "" : filename.substr(0, pos + 1); + std::filesystem::path p(filename); + return (p.parent_path()).string(); } std::string get_file_extension(const std::string& filename) { - // try our best to work on windows... -#if defined(_WIN32) || defined(_WIN64) - const char sep_char = '\\'; -#else - const char sep_char = '/'; -#endif - - // check that at least one letter is present - const auto last_period_pos = filename.find_last_of('.'); - const auto last_sep_pos = filename.find_last_of(sep_char); - - // no file extension. In the first case, we are only given - // a file name. In the second, we have been given a file path. - // If that's the case, periods are allowed in directory names, - // but have the interpretation as preceding a file extension - // after the last separator. - if (last_period_pos == std::string::npos || - (last_sep_pos < std::string::npos && last_period_pos < last_sep_pos)) - return ""; - - const std::string ending = filename.substr(last_period_pos + 1); - - // check that at least one character is present. - const bool has_alpha = std::any_of(ending.begin(), ending.end(), - [](char x) { return static_cast(std::isalpha(x)); }); - return has_alpha ? ending : ""; + std::filesystem::path p(filename); + auto ext = p.extension(); + if (!ext.empty()) { + // path::extension includes the period + return ext.string().substr(1); + } + return ""; } } // namespace openmc diff --git a/src/simulation.cpp b/src/simulation.cpp index 09b3764732b..030e447f135 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -446,10 +446,9 @@ void finalize_batch() // Write a continously-overwritten source point if requested. if (settings::source_latest) { - // note: correct file extension appended automatically auto filename = settings::path_output + "source"; gsl::span bankspan(simulation::source_bank); - write_source_point(filename.c_str(), bankspan, simulation::work_index, + write_source_point(filename, bankspan, simulation::work_index, settings::source_mcpl_write); } } diff --git a/src/state_point.cpp b/src/state_point.cpp index adc026fa3c2..fcc389df189 100644 --- a/src/state_point.cpp +++ b/src/state_point.cpp @@ -52,9 +52,7 @@ extern "C" int openmc_statepoint_write(const char* filename, bool* write_source) // If a file name was specified, ensure it has .h5 file extension const auto extension = get_file_extension(filename_); - if (extension == "") { - filename_.append(".h5"); - } else if (extension != "h5") { + if (extension != "h5") { warning("openmc_statepoint_write was passed a file extension differing " "from .h5, but an hdf5 file will be written."); } @@ -578,8 +576,10 @@ void write_source_point(std::string filename, gsl::span source_bank, // Dispatch to appropriate function based on file type if (use_mcpl) { + filename.append(".mcpl"); write_mcpl_source_point(filename.c_str(), source_bank, bank_index); } else { + filename.append(".h5"); write_h5_source_point(filename.c_str(), source_bank, bank_index); } } @@ -602,9 +602,7 @@ void write_h5_source_point(const char* filename, std::string filename_(filename); const auto extension = get_file_extension(filename_); - if (extension == "") { - filename_.append(".h5"); - } else if (extension != "h5") { + if (extension != "h5") { warning("write_source_point was passed a file extension differing " "from .h5, but an hdf5 file will be written."); } diff --git a/tests/cpp_unit_tests/test_file_utils.cpp b/tests/cpp_unit_tests/test_file_utils.cpp index 3b7a74346bd..8b0d99d76da 100644 --- a/tests/cpp_unit_tests/test_file_utils.cpp +++ b/tests/cpp_unit_tests/test_file_utils.cpp @@ -30,3 +30,12 @@ TEST_CASE("Test file_exists") // Note: not clear how to portably test where a file should exist. REQUIRE(!file_exists("./should_not_exist/really_do_not_make_this_please")); } + +TEST_CASE("Test dir_name") +{ + REQUIRE(dir_name("") == ""); + REQUIRE(dir_name("/") == "/"); + REQUIRE(dir_name("hello") == ""); + REQUIRE(dir_name("hello/world") == "hello"); + REQUIRE(dir_name("/path/to/dir/") == "/path/to/dir"); +} From 27f3afefa472567304588edfb6fe18ab8362aa9e Mon Sep 17 00:00:00 2001 From: Skywalker <49872736+cn-skywalker@users.noreply.github.com> Date: Tue, 28 Jan 2025 15:57:27 +0800 Subject: [PATCH 134/184] Fix the bug in the Material.from_xml_element function (#3278) Co-authored-by: Paul Romano --- openmc/material.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openmc/material.py b/openmc/material.py index a6401216adb..343b2fff293 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -1582,7 +1582,6 @@ def from_xml_element(cls, elem: ET.Element) -> Material: if 'volume' in elem.attrib: mat.volume = float(elem.get('volume')) - mat.depletable = bool(elem.get('depletable')) # Get each nuclide for nuclide in elem.findall('nuclide'): @@ -1592,6 +1591,9 @@ def from_xml_element(cls, elem: ET.Element) -> Material: elif 'wo' in nuclide.attrib: mat.add_nuclide(name, float(nuclide.attrib['wo']), 'wo') + # Get depletable attribute + mat.depletable = elem.get('depletable') in ('true', '1') + # Get each S(a,b) table for sab in elem.findall('sab'): fraction = float(sab.get('fraction', 1.0)) From 860d739f4c30c0a59885caad0534d4fee293f53b Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Tue, 28 Jan 2025 17:44:32 +0100 Subject: [PATCH 135/184] Doc typo fix for rand ray mgxs (#3280) Co-authored-by: Jon Shimwell --- docs/source/usersguide/random_ray.rst | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index b6852b7ca8f..b797a721690 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -111,7 +111,7 @@ detector from the core. In this case, rays sampled in the moderator region and heading toward the detector will begin life with a highly scattered thermal spectrum and will have an inaccurate fast spectrum. If the dead zone length is only 20 cm, we might imagine such rays writing to the detector tally within -their active lengths, despite their innaccurate estimate of the uncollided fast +their active lengths, despite their inaccurate estimate of the uncollided fast angular flux. Thus, an inactive length of 100--200 cm would ensure that any such rays would still be within their inactive regions, and only rays that have actually traversed through the core (and thus have an accurate representation of @@ -487,7 +487,7 @@ two group energy decomposition:: mgxs_lib = openmc.mgxs.Library(geometry) # Pick energy group structure - groups = mgxs.EnergyGroups(mgxs.GROUP_STRUCTURES['CASMO-2']) + groups = openmc.mgxs.EnergyGroups(openmc.mgxs.GROUP_STRUCTURES['CASMO-2']) mgxs_lib.energy_groups = groups # Disable transport correction @@ -501,7 +501,7 @@ two group energy decomposition:: mgxs_lib.domain_type = "material" # Specify the cell domains over which to compute multi-group cross sections - mgxs_lib.domains = geom.get_all_materials().values() + mgxs_lib.domains = geometry.get_all_materials().values() # Do not compute cross sections on a nuclide-by-nuclide basis mgxs_lib.by_nuclide = False @@ -531,7 +531,6 @@ a statepoint file (e.g., ``statepoint.100.h5``) file and summary file (e.g., ``summary.h5``) that resulted from running our previous example:: import openmc - import openmc.mgxs as mgxs summary = openmc.Summary('summary.h5') geom = summary.geometry @@ -540,7 +539,7 @@ a statepoint file (e.g., ``statepoint.100.h5``) file and summary file (e.g., statepoint_filename = 'statepoint.100.h5' sp = openmc.StatePoint(statepoint_filename) - groups = mgxs.EnergyGroups(mgxs.GROUP_STRUCTURES['CASMO-2']) + groups = openmc.mgxs.EnergyGroups(openmc.mgxs.GROUP_STRUCTURES['CASMO-2']) mgxs_lib = openmc.mgxs.Library(geom) mgxs_lib.energy_groups = groups mgxs_lib.correction = None From d9c8e594c76f7f99b6acf5027de93ccfd26173e9 Mon Sep 17 00:00:00 2001 From: Skywalker <49872736+cn-skywalker@users.noreply.github.com> Date: Wed, 29 Jan 2025 03:44:04 +0800 Subject: [PATCH 136/184] fix the bug in function differentiate_mats() (#3277) Co-authored-by: Paul Romano --- openmc/model/model.py | 27 ++++++++++++++----- .../test_deplete_coupled_operator.py | 4 +-- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index 80859c55db4..c8329c2fbbf 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1199,7 +1199,7 @@ def differentiate_depletable_mats(self, diff_volume_method: str = None): diff_volume_method : str Specifies how the volumes of the new materials should be found. - None: Do not assign volumes to the new materials (Default) - - 'divide_equally': Divide the original material volume equally between the new materials + - 'divide equally': Divide the original material volume equally between the new materials - 'match cell': Set the volume of the material to the volume of the cell they fill """ self.differentiate_mats(diff_volume_method, depletable_only=True) @@ -1214,7 +1214,7 @@ def differentiate_mats(self, diff_volume_method: str = None, depletable_only: bo diff_volume_method : str Specifies how the volumes of the new materials should be found. - None: Do not assign volumes to the new materials (Default) - - 'divide_equally': Divide the original material volume equally between the new materials + - 'divide equally': Divide the original material volume equally between the new materials - 'match cell': Set the volume of the material to the volume of the cell they fill depletable_only : bool Default is True, only depletable materials will be differentiated. If False, all materials will be @@ -1225,9 +1225,15 @@ def differentiate_mats(self, diff_volume_method: str = None, depletable_only: bo # Count the number of instances for each cell and material self.geometry.determine_paths(instances_only=True) + # Get list of materials + if self.materials: + materials = self.materials + else: + materials = list(self.geometry.get_all_materials().values()) + # Find all or depletable_only materials which have multiple instance distribmats = set() - for mat in self.materials: + for mat in materials: # Differentiate all materials with multiple instances diff_mat = mat.num_instances > 1 # If depletable_only is True, differentiate only depletable materials @@ -1259,11 +1265,20 @@ def differentiate_mats(self, diff_volume_method: str = None, depletable_only: bo for cell in self.geometry.get_all_material_cells().values(): if cell.fill in distribmats: mat = cell.fill - if diff_volume_method != 'match cell': + + # Clone materials + if cell.num_instances > 1: cell.fill = [mat.clone() for _ in range(cell.num_instances)] - elif diff_volume_method == 'match cell': + else: cell.fill = mat.clone() - cell.fill.volume = cell.volume + + # For 'match cell', assign volumes based on the cells + if diff_volume_method == 'match cell': + if cell.fill_type == 'distribmat': + for clone_mat in cell.fill: + clone_mat.volume = cell.volume + else: + cell.fill.volume = cell.volume if self.materials is not None: self.materials = openmc.Materials( diff --git a/tests/unit_tests/test_deplete_coupled_operator.py b/tests/unit_tests/test_deplete_coupled_operator.py index fe79d621b12..ba291e07dd5 100644 --- a/tests/unit_tests/test_deplete_coupled_operator.py +++ b/tests/unit_tests/test_deplete_coupled_operator.py @@ -114,7 +114,7 @@ def test_diff_volume_method_divide_equally(model_with_volumes): ) all_cells = list(operator.model.geometry.get_all_cells().values()) - assert all_cells[0].fill[0].volume == 51 - assert all_cells[1].fill[0].volume == 51 + assert all_cells[0].fill.volume == 51 + assert all_cells[1].fill.volume == 51 # mat2 is not depletable assert all_cells[2].fill.volume is None From 59c398be84fa5e88279dc5b980f4a79c6f57a951 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 31 Jan 2025 10:12:24 -0600 Subject: [PATCH 137/184] Consolidate plotting capabilities in Model.plot (#3282) Co-authored-by: Jonathan Shimwell --- openmc/cell.py | 56 +--------- openmc/geometry.py | 55 +--------- openmc/model/model.py | 237 ++++++++++++++++++++++++++++++++--------- openmc/plots.py | 85 +++++++++++++++ openmc/region.py | 39 +------ openmc/universe.py | 242 +++--------------------------------------- 6 files changed, 290 insertions(+), 424 deletions(-) diff --git a/openmc/cell.py b/openmc/cell.py index 2e0f2ff13bc..cd0573e8b8e 100644 --- a/openmc/cell.py +++ b/openmc/cell.py @@ -10,6 +10,7 @@ import openmc.checkvalue as cv from ._xml import get_text from .mixin import IDManagerMixin +from .plots import add_plot_params from .region import Region, Complement from .surface import Halfspace from .bounding_box import BoundingBox @@ -559,64 +560,11 @@ def clone(self, clone_materials=True, clone_regions=True, memo=None): return memo[self] + @add_plot_params def plot(self, *args, **kwargs): """Display a slice plot of the cell. .. versionadded:: 0.14.0 - - Parameters - ---------- - origin : iterable of float - Coordinates at the origin of the plot. If left as None then the - bounding box center will be used to attempt to ascertain the origin. - Defaults to (0, 0, 0) if the bounding box is not finite - width : iterable of float - Width of the plot in each basis direction. If left as none then the - bounding box width will be used to attempt to ascertain the plot - width. Defaults to (10, 10) if the bounding box is not finite - pixels : Iterable of int or int - If iterable of ints provided, then this directly sets the number of - pixels to use in each basis direction. If int provided, then this - sets the total number of pixels in the plot and the number of pixels - in each basis direction is calculated from this total and the image - aspect ratio. - basis : {'xy', 'xz', 'yz'} - The basis directions for the plot - color_by : {'cell', 'material'} - Indicate whether the plot should be colored by cell or by material - colors : dict - Assigns colors to specific materials or cells. Keys are instances of - :class:`Cell` or :class:`Material` and values are RGB 3-tuples, RGBA - 4-tuples, or strings indicating SVG color names. Red, green, blue, - and alpha should all be floats in the range [0.0, 1.0], for example: - - .. code-block:: python - - # Make water blue - water = openmc.Cell(fill=h2o) - water.plot(colors={water: (0., 0., 1.)}) - seed : int - Seed for the random number generator - openmc_exec : str - Path to OpenMC executable. - axes : matplotlib.Axes - Axes to draw to - legend : bool - Whether a legend showing material or cell names should be drawn - legend_kwargs : dict - Keyword arguments passed to :func:`matplotlib.pyplot.legend`. - outline : bool - Whether outlines between color boundaries should be drawn - axis_units : {'km', 'm', 'cm', 'mm'} - Units used on the plot axis - **kwargs - Keyword arguments passed to :func:`matplotlib.pyplot.imshow` - - Returns - ------- - matplotlib.axes.Axes - Axes containing resulting image - """ # Create dummy universe but preserve used_ids next_id = openmc.Universe.next_id diff --git a/openmc/geometry.py b/openmc/geometry.py index 28e1e48eca2..c069d579695 100644 --- a/openmc/geometry.py +++ b/openmc/geometry.py @@ -9,6 +9,7 @@ import openmc import openmc._xml as xml +from .plots import add_plot_params from .checkvalue import check_type, check_less_than, check_greater_than, PathLike @@ -747,62 +748,10 @@ def clone(self) -> Geometry: clone.root_universe = self.root_universe.clone() return clone + @add_plot_params def plot(self, *args, **kwargs): """Display a slice plot of the geometry. .. versionadded:: 0.14.0 - - Parameters - ---------- - origin : iterable of float - Coordinates at the origin of the plot. If left as None then the - bounding box center will be used to attempt to ascertain the origin. - Defaults to (0, 0, 0) if the bounding box is not finite - width : iterable of float - Width of the plot in each basis direction. If left as none then the - bounding box width will be used to attempt to ascertain the plot - width. Defaults to (10, 10) if the bounding box is not finite - pixels : Iterable of int or int - If iterable of ints provided, then this directly sets the number of - pixels to use in each basis direction. If int provided, then this - sets the total number of pixels in the plot and the number of pixels - in each basis direction is calculated from this total and the image - aspect ratio. - basis : {'xy', 'xz', 'yz'} - The basis directions for the plot - color_by : {'cell', 'material'} - Indicate whether the plot should be colored by cell or by material - colors : dict - Assigns colors to specific materials or cells. Keys are instances of - :class:`Cell` or :class:`Material` and values are RGB 3-tuples, RGBA - 4-tuples, or strings indicating SVG color names. Red, green, blue, - and alpha should all be floats in the range [0.0, 1.0], for - example:: - - # Make water blue - water = openmc.Cell(fill=h2o) - universe.plot(..., colors={water: (0., 0., 1.)) - seed : int - Seed for the random number generator - openmc_exec : str - Path to OpenMC executable. - axes : matplotlib.Axes - Axes to draw to - legend : bool - Whether a legend showing material or cell names should be drawn - legend_kwargs : dict - Keyword arguments passed to :func:`matplotlib.pyplot.legend`. - outline : bool - Whether outlines between color boundaries should be drawn - axis_units : {'km', 'm', 'cm', 'mm'} - Units used on the plot axis - **kwargs - Keyword arguments passed to :func:`matplotlib.pyplot.imshow` - - Returns - ------- - matplotlib.axes.Axes - Axes containing resulting image """ - return self.root_universe.plot(*args, **kwargs) diff --git a/openmc/model/model.py b/openmc/model/model.py index c8329c2fbbf..8dd13ef6b3c 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -1,9 +1,10 @@ from __future__ import annotations -from collections.abc import Iterable +from collections.abc import Iterable, Sequence from functools import lru_cache from pathlib import Path -from numbers import Integral -from tempfile import NamedTemporaryFile +import math +from numbers import Integral, Real +from tempfile import NamedTemporaryFile, TemporaryDirectory import warnings import h5py @@ -16,6 +17,7 @@ from openmc.executor import _process_CLI_arguments from openmc.checkvalue import check_type, check_value, PathLike from openmc.exceptions import InvalidIDError +from openmc.plots import add_plot_params from openmc.utility_funcs import change_directory @@ -130,6 +132,10 @@ def plots(self, plots): for plot in plots: self._plots.append(plot) + @property + def bounding_box(self) -> openmc.BoundingBox: + return self.geometry.bounding_box + @property def is_initialized(self) -> bool: try: @@ -827,78 +833,207 @@ def calculate_volumes(self, threads=None, output=True, cwd='.', openmc.lib.materials[domain_id].volume = \ vol_calc.volumes[domain_id].n + @add_plot_params def plot( self, + origin: Sequence[float] | None = None, + width: Sequence[float] | None = None, + pixels: int | Sequence[int] = 40000, + basis: str = 'xy', + color_by: str = 'cell', + colors: dict | None = None, + seed: int | None = None, + openmc_exec: PathLike = 'openmc', + axes=None, + legend: bool = False, + axis_units: str = 'cm', + outline: bool | str = False, n_samples: int | None = None, plane_tolerance: float = 1., + legend_kwargs: dict | None = None, source_kwargs: dict | None = None, + contour_kwargs: dict | None = None, **kwargs, ): - """Display a slice plot of the geometry. + """Display a slice plot of the model. .. versionadded:: 0.15.1 - - Parameters - ---------- - n_samples : int, optional - The number of source particles to sample and add to plot. Defaults - to None which doesn't plot any particles on the plot. - plane_tolerance: float - When plotting a plane the source locations within the plane +/- - the plane_tolerance will be included and those outside of the - plane_tolerance will not be shown - source_kwargs : dict, optional - Keyword arguments passed to :func:`matplotlib.pyplot.scatter`. - **kwargs - Keyword arguments passed to :func:`openmc.Universe.plot` - - Returns - ------- - matplotlib.axes.Axes - Axes containing resulting image """ + import matplotlib.image as mpimg + import matplotlib.patches as mpatches + import matplotlib.pyplot as plt check_type('n_samples', n_samples, int | None) - check_type('plane_tolerance', plane_tolerance, float) + check_type('plane_tolerance', plane_tolerance, Real) + if legend_kwargs is None: + legend_kwargs = {} + legend_kwargs.setdefault('bbox_to_anchor', (1.05, 1)) + legend_kwargs.setdefault('loc', 2) + legend_kwargs.setdefault('borderaxespad', 0.0) if source_kwargs is None: source_kwargs = {} source_kwargs.setdefault('marker', 'x') - ax = self.geometry.plot(**kwargs) - if n_samples: - # Sample external source particles - particles = self.sample_external_source(n_samples) + # Determine extents of plot + if basis == 'xy': + x, y, z = 0, 1, 2 + xlabel, ylabel = f'x [{axis_units}]', f'y [{axis_units}]' + elif basis == 'yz': + x, y, z = 1, 2, 0 + xlabel, ylabel = f'y [{axis_units}]', f'z [{axis_units}]' + elif basis == 'xz': + x, y, z = 0, 2, 1 + xlabel, ylabel = f'x [{axis_units}]', f'z [{axis_units}]' + + bb = self.bounding_box + # checks to see if bounding box contains -inf or inf values + if np.isinf(bb.extent[basis]).any(): + if origin is None: + origin = (0, 0, 0) + if width is None: + width = (10, 10) + else: + if origin is None: + # if nan values in the bb.center they get replaced with 0.0 + # this happens when the bounding_box contains inf values + with warnings.catch_warnings(): + warnings.simplefilter("ignore", RuntimeWarning) + origin = np.nan_to_num(bb.center) + if width is None: + bb_width = bb.width + width = (bb_width[x], bb_width[y]) + + if isinstance(pixels, int): + aspect_ratio = width[0] / width[1] + pixels_y = math.sqrt(pixels / aspect_ratio) + pixels = (int(pixels / pixels_y), int(pixels_y)) + + axis_scaling_factor = {'km': 0.00001, 'm': 0.01, 'cm': 1, 'mm': 10} + + x_min = (origin[x] - 0.5*width[0]) * axis_scaling_factor[axis_units] + x_max = (origin[x] + 0.5*width[0]) * axis_scaling_factor[axis_units] + y_min = (origin[y] - 0.5*width[1]) * axis_scaling_factor[axis_units] + y_max = (origin[y] + 0.5*width[1]) * axis_scaling_factor[axis_units] + + with TemporaryDirectory() as tmpdir: + if seed is not None: + self.settings.plot_seed = seed + + # Create plot object matching passed arguments + plot = openmc.Plot() + plot.origin = origin + plot.width = width + plot.pixels = pixels + plot.basis = basis + plot.color_by = color_by + if colors is not None: + plot.colors = colors + self.plots.append(plot) + + # Run OpenMC in geometry plotting mode + self.plot_geometry(False, cwd=tmpdir, openmc_exec=openmc_exec) + + # Read image from file + img_path = Path(tmpdir) / f'plot_{plot.id}.png' + if not img_path.is_file(): + img_path = img_path.with_suffix('.ppm') + img = mpimg.imread(str(img_path)) + + # Create a figure sized such that the size of the axes within + # exactly matches the number of pixels specified + if axes is None: + px = 1/plt.rcParams['figure.dpi'] + fig, axes = plt.subplots() + axes.set_xlabel(xlabel) + axes.set_ylabel(ylabel) + params = fig.subplotpars + width = pixels[0]*px/(params.right - params.left) + height = pixels[1]*px/(params.top - params.bottom) + fig.set_size_inches(width, height) + + if outline: + # Combine R, G, B values into a single int + rgb = (img * 256).astype(int) + image_value = (rgb[..., 0] << 16) + \ + (rgb[..., 1] << 8) + (rgb[..., 2]) + + # Set default arguments for contour() + if contour_kwargs is None: + contour_kwargs = {} + contour_kwargs.setdefault('colors', 'k') + contour_kwargs.setdefault('linestyles', 'solid') + contour_kwargs.setdefault('algorithm', 'serial') + + axes.contour( + image_value, + origin="upper", + levels=np.unique(image_value), + extent=(x_min, x_max, y_min, y_max), + **contour_kwargs + ) - # Determine plotting parameters and bounding box of geometry - bbox = self.geometry.bounding_box - origin = kwargs.get('origin', None) - basis = kwargs.get('basis', 'xy') - indices = {'xy': (0, 1, 2), 'xz': (0, 2, 1), 'yz': (1, 2, 0)}[basis] + # add legend showing which colors represent which material + # or cell if that was requested + if legend: + if plot.colors == {}: + raise ValueError("Must pass 'colors' dictionary if you " + "are adding a legend via legend=True.") - # Infer origin if not provided - if np.isinf(bbox.extent[basis]).any(): - if origin is None: - origin = (0, 0, 0) - else: - if origin is None: - # if nan values in the bbox.center they get replaced with 0.0 - # this happens when the bounding_box contains inf values - with warnings.catch_warnings(): - warnings.simplefilter("ignore", RuntimeWarning) - origin = np.nan_to_num(bbox.center) + if color_by == "cell": + expected_key_type = openmc.Cell + else: + expected_key_type = openmc.Material + + patches = [] + for key, color in plot.colors.items(): + + if isinstance(key, int): + raise TypeError( + "Cannot use IDs in colors dict for auto legend.") + elif not isinstance(key, expected_key_type): + raise TypeError( + "Color dict key type does not match color_by") + + # this works whether we're doing cells or materials + label = key.name if key.name != '' else key.id + + # matplotlib takes RGB on 0-1 scale rather than 0-255. at + # this point PlotBase has already checked that 3-tuple + # based colors are already valid, so if the length is three + # then we know it just needs to be converted to the 0-1 + # format. + if len(color) == 3 and not isinstance(color, str): + scaled_color = ( + color[0]/255, color[1]/255, color[2]/255) + else: + scaled_color = color + + key_patch = mpatches.Patch(color=scaled_color, label=label) + patches.append(key_patch) + + axes.legend(handles=patches, **legend_kwargs) - slice_index = indices[2] - slice_value = origin[slice_index] + # Plot image and return the axes + if outline != 'only': + axes.imshow(img, extent=(x_min, x_max, y_min, y_max), **kwargs) + + if n_samples: + # Sample external source particles + particles = self.sample_external_source(n_samples) + + # Get points within tolerance of the slice plane + slice_value = origin[z] xs = [] ys = [] tol = plane_tolerance for particle in particles: - if (slice_value - tol < particle.r[slice_index] < slice_value + tol): - xs.append(particle.r[indices[0]]) - ys.append(particle.r[indices[1]]) - ax.scatter(xs, ys, **source_kwargs) - return ax + if (slice_value - tol < particle.r[z] < slice_value + tol): + xs.append(particle.r[x]) + ys.append(particle.r[y]) + axes.scatter(xs, ys, **source_kwargs) + + return axes def sample_external_source( self, diff --git a/openmc/plots.py b/openmc/plots.py index 2bb3ef65f4a..a59a7f0eb4a 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -165,6 +165,91 @@ 'yellowgreen': (154, 205, 50) } +_PLOT_PARAMS = """ + Parameters + ---------- + origin : iterable of float + Coordinates at the origin of the plot. If left as None, + the center of the bounding box will be used to attempt to ascertain + the origin with infinite values being replaced by 0. + width : iterable of float + Width of the plot in each basis direction. If left as none then the + width of the bounding box will be used to attempt to + ascertain the plot width. Defaults to (10, 10) if the bounding box + contains inf values. + pixels : Iterable of int or int + If iterable of ints provided then this directly sets the number of + pixels to use in each basis direction. If int provided then this + sets the total number of pixels in the plot and the number of + pixels in each basis direction is calculated from this total and + the image aspect ratio. + basis : {'xy', 'xz', 'yz'} + The basis directions for the plot + color_by : {'cell', 'material'} + Indicate whether the plot should be colored by cell or by material + colors : dict + Assigns colors to specific materials or cells. Keys are instances of + :class:`Cell` or :class:`Material` and values are RGB 3-tuples, RGBA + 4-tuples, or strings indicating SVG color names. Red, green, blue, + and alpha should all be floats in the range [0.0, 1.0], for example: + + .. code-block:: python + + # Make water blue + water = openmc.Cell(fill=h2o) + universe.plot(..., colors={water: (0., 0., 1.)) + seed : int + Seed for the random number generator + openmc_exec : str + Path to OpenMC executable. + axes : matplotlib.Axes + Axes to draw to + + .. versionadded:: 0.13.1 + legend : bool + Whether a legend showing material or cell names should be drawn + + .. versionadded:: 0.14.0 + outline : bool or str + Whether outlines between color boundaries should be drawn. If set to + 'only', only outlines will be drawn. + + .. versionadded:: 0.14.0 + axis_units : {'km', 'm', 'cm', 'mm'} + Units used on the plot axis + + .. versionadded:: 0.14.0 + n_samples : int, optional + The number of source particles to sample and add to plot. Defaults + to None which doesn't plot any particles on the plot. + plane_tolerance: float + When plotting a plane the source locations within the plane +/- + the plane_tolerance will be included and those outside of the + plane_tolerance will not be shown + legend_kwargs : dict + Keyword arguments passed to :func:`matplotlib.pyplot.legend`. + + .. versionadded:: 0.14.0 + source_kwargs : dict, optional + Keyword arguments passed to :func:`matplotlib.pyplot.scatter`. + contour_kwargs : dict, optional + Keyword arguments passed to :func:`matplotlib.pyplot.contour`. + **kwargs + Keyword arguments passed to :func:`matplotlib.pyplot.imshow`. + + Returns + ------- + matplotlib.axes.Axes + Axes containing resulting image +""" + + +# Decorator for consistently adding plot parameters to docstrings (Model.plot, +# Geometry.plot, Universe.plot, etc.) +def add_plot_params(func): + func.__doc__ += _PLOT_PARAMS + return func + def _get_plot_image(plot, cwd): from IPython.display import Image diff --git a/openmc/region.py b/openmc/region.py index e1cb834757a..cb9f3abd23a 100644 --- a/openmc/region.py +++ b/openmc/region.py @@ -8,6 +8,7 @@ import openmc from .bounding_box import BoundingBox +from .plots import add_plot_params class Region(ABC): @@ -343,47 +344,11 @@ def rotate(self, rotation, pivot=(0., 0., 0.), order='xyz', inplace=False, return type(self)(n.rotate(rotation, pivot=pivot, order=order, inplace=inplace, memo=memo) for n in self) + @add_plot_params def plot(self, *args, **kwargs): """Display a slice plot of the region. .. versionadded:: 0.15.0 - - Parameters - ---------- - origin : iterable of float - Coordinates at the origin of the plot. If left as None then the - bounding box center will be used to attempt to ascertain the origin. - Defaults to (0, 0, 0) if the bounding box is not finite - width : iterable of float - Width of the plot in each basis direction. If left as none then the - bounding box width will be used to attempt to ascertain the plot - width. Defaults to (10, 10) if the bounding box is not finite - pixels : Iterable of int or int - If iterable of ints provided, then this directly sets the number of - pixels to use in each basis direction. If int provided, then this - sets the total number of pixels in the plot and the number of pixels - in each basis direction is calculated from this total and the image - aspect ratio. - basis : {'xy', 'xz', 'yz'} - The basis directions for the plot - seed : int - Seed for the random number generator - openmc_exec : str - Path to OpenMC executable. - axes : matplotlib.Axes - Axes to draw to - outline : bool - Whether outlines between color boundaries should be drawn - axis_units : {'km', 'm', 'cm', 'mm'} - Units used on the plot axis - **kwargs - Keyword arguments passed to :func:`matplotlib.pyplot.imshow` - - Returns - ------- - matplotlib.axes.Axes - Axes containing resulting image - """ for key in ('color_by', 'colors', 'legend', 'legend_kwargs'): if key in kwargs: diff --git a/openmc/universe.py b/openmc/universe.py index 71ffa726be6..02b794deed4 100644 --- a/openmc/universe.py +++ b/openmc/universe.py @@ -1,17 +1,14 @@ from __future__ import annotations -import math from abc import ABC, abstractmethod from collections.abc import Iterable from numbers import Real -from pathlib import Path -from tempfile import TemporaryDirectory -import warnings import numpy as np import openmc import openmc.checkvalue as cv from .mixin import IDManagerMixin +from .plots import add_plot_params class UniverseBase(ABC, IDManagerMixin): @@ -334,234 +331,21 @@ def find(self, point): return [self, cell] + cell.fill.find(p) return [] - # default kwargs that are passed to plt.legend in the plot method below. - _default_legend_kwargs = {'bbox_to_anchor': ( - 1.05, 1), 'loc': 2, 'borderaxespad': 0.0} - - def plot(self, origin=None, width=None, pixels=40000, - basis='xy', color_by='cell', colors=None, seed=None, - openmc_exec='openmc', axes=None, legend=False, axis_units='cm', - legend_kwargs=_default_legend_kwargs, outline=False, - **kwargs): + @add_plot_params + def plot(self, *args, **kwargs): """Display a slice plot of the universe. + """ + model = openmc.Model() + model.geometry = openmc.Geometry(self) - Parameters - ---------- - origin : iterable of float - Coordinates at the origin of the plot. If left as None, - universe.bounding_box.center will be used to attempt to ascertain - the origin with infinite values being replaced by 0. - width : iterable of float - Width of the plot in each basis direction. If left as none then the - universe.bounding_box.width() will be used to attempt to - ascertain the plot width. Defaults to (10, 10) if the bounding_box - contains inf values - pixels : Iterable of int or int - If iterable of ints provided then this directly sets the number of - pixels to use in each basis direction. If int provided then this - sets the total number of pixels in the plot and the number of - pixels in each basis direction is calculated from this total and - the image aspect ratio. - basis : {'xy', 'xz', 'yz'} - The basis directions for the plot - color_by : {'cell', 'material'} - Indicate whether the plot should be colored by cell or by material - colors : dict - Assigns colors to specific materials or cells. Keys are instances of - :class:`Cell` or :class:`Material` and values are RGB 3-tuples, RGBA - 4-tuples, or strings indicating SVG color names. Red, green, blue, - and alpha should all be floats in the range [0.0, 1.0], for example: - - .. code-block:: python - - # Make water blue - water = openmc.Cell(fill=h2o) - universe.plot(..., colors={water: (0., 0., 1.)) - seed : int - Seed for the random number generator - openmc_exec : str - Path to OpenMC executable. - axes : matplotlib.Axes - Axes to draw to - - .. versionadded:: 0.13.1 - legend : bool - Whether a legend showing material or cell names should be drawn - - .. versionadded:: 0.14.0 - legend_kwargs : dict - Keyword arguments passed to :func:`matplotlib.pyplot.legend`. - - .. versionadded:: 0.14.0 - outline : bool - Whether outlines between color boundaries should be drawn - - .. versionadded:: 0.14.0 - axis_units : {'km', 'm', 'cm', 'mm'} - Units used on the plot axis - - .. versionadded:: 0.14.0 - **kwargs - Keyword arguments passed to :func:`matplotlib.pyplot.imshow` - - Returns - ------- - matplotlib.axes.Axes - Axes containing resulting image + # Determine whether any materials contains macroscopic data and if + # so, set energy mode accordingly + for mat in self.get_all_materials().values(): + if mat._macroscopic is not None: + model.settings.energy_mode = 'multi-group' + break - """ - import matplotlib.image as mpimg - import matplotlib.patches as mpatches - import matplotlib.pyplot as plt - - # Determine extents of plot - if basis == 'xy': - x, y = 0, 1 - xlabel, ylabel = f'x [{axis_units}]', f'y [{axis_units}]' - elif basis == 'yz': - x, y = 1, 2 - xlabel, ylabel = f'y [{axis_units}]', f'z [{axis_units}]' - elif basis == 'xz': - x, y = 0, 2 - xlabel, ylabel = f'x [{axis_units}]', f'z [{axis_units}]' - - bb = self.bounding_box - # checks to see if bounding box contains -inf or inf values - if np.isinf(bb.extent[basis]).any(): - if origin is None: - origin = (0, 0, 0) - if width is None: - width = (10, 10) - else: - if origin is None: - # if nan values in the bb.center they get replaced with 0.0 - # this happens when the bounding_box contains inf values - with warnings.catch_warnings(): - warnings.simplefilter("ignore", RuntimeWarning) - origin = np.nan_to_num(bb.center) - if width is None: - bb_width = bb.width - x_width = bb_width['xyz'.index(basis[0])] - y_width = bb_width['xyz'.index(basis[1])] - width = (x_width, y_width) - - if isinstance(pixels, int): - aspect_ratio = width[0] / width[1] - pixels_y = math.sqrt(pixels / aspect_ratio) - pixels = (int(pixels / pixels_y), int(pixels_y)) - - axis_scaling_factor = {'km': 0.00001, 'm': 0.01, 'cm': 1, 'mm': 10} - - x_min = (origin[x] - 0.5*width[0]) * axis_scaling_factor[axis_units] - x_max = (origin[x] + 0.5*width[0]) * axis_scaling_factor[axis_units] - y_min = (origin[y] - 0.5*width[1]) * axis_scaling_factor[axis_units] - y_max = (origin[y] + 0.5*width[1]) * axis_scaling_factor[axis_units] - - with TemporaryDirectory() as tmpdir: - model = openmc.Model() - model.geometry = openmc.Geometry(self) - if seed is not None: - model.settings.plot_seed = seed - - # Determine whether any materials contains macroscopic data and if - # so, set energy mode accordingly - for mat in self.get_all_materials().values(): - if mat._macroscopic is not None: - model.settings.energy_mode = 'multi-group' - break - - # Create plot object matching passed arguments - plot = openmc.Plot() - plot.origin = origin - plot.width = width - plot.pixels = pixels - plot.basis = basis - plot.color_by = color_by - if colors is not None: - plot.colors = colors - model.plots.append(plot) - - # Run OpenMC in geometry plotting mode - model.plot_geometry(False, cwd=tmpdir, openmc_exec=openmc_exec) - - # Read image from file - img_path = Path(tmpdir) / f'plot_{plot.id}.png' - if not img_path.is_file(): - img_path = img_path.with_suffix('.ppm') - img = mpimg.imread(str(img_path)) - - # Create a figure sized such that the size of the axes within - # exactly matches the number of pixels specified - if axes is None: - px = 1/plt.rcParams['figure.dpi'] - fig, axes = plt.subplots() - axes.set_xlabel(xlabel) - axes.set_ylabel(ylabel) - params = fig.subplotpars - width = pixels[0]*px/(params.right - params.left) - height = pixels[1]*px/(params.top - params.bottom) - fig.set_size_inches(width, height) - - if outline: - # Combine R, G, B values into a single int - rgb = (img * 256).astype(int) - image_value = (rgb[..., 0] << 16) + \ - (rgb[..., 1] << 8) + (rgb[..., 2]) - - axes.contour( - image_value, - origin="upper", - colors="k", - linestyles="solid", - linewidths=1, - levels=np.unique(image_value), - extent=(x_min, x_max, y_min, y_max), - ) - - # add legend showing which colors represent which material - # or cell if that was requested - if legend: - if plot.colors == {}: - raise ValueError("Must pass 'colors' dictionary if you " - "are adding a legend via legend=True.") - - if color_by == "cell": - expected_key_type = openmc.Cell - else: - expected_key_type = openmc.Material - - patches = [] - for key, color in plot.colors.items(): - - if isinstance(key, int): - raise TypeError( - "Cannot use IDs in colors dict for auto legend.") - elif not isinstance(key, expected_key_type): - raise TypeError( - "Color dict key type does not match color_by") - - # this works whether we're doing cells or materials - label = key.name if key.name != '' else key.id - - # matplotlib takes RGB on 0-1 scale rather than 0-255. at - # this point PlotBase has already checked that 3-tuple - # based colors are already valid, so if the length is three - # then we know it just needs to be converted to the 0-1 - # format. - if len(color) == 3 and not isinstance(color, str): - scaled_color = ( - color[0]/255, color[1]/255, color[2]/255) - else: - scaled_color = color - - key_patch = mpatches.Patch(color=scaled_color, label=label) - patches.append(key_patch) - - axes.legend(handles=patches, **legend_kwargs) - - # Plot image and return the axes - axes.imshow(img, extent=(x_min, x_max, y_min, y_max), **kwargs) - return axes + return model.plot(*args, **kwargs) def get_nuclides(self): """Returns all nuclides in the universe From 6e0f156d33527c5bdd693021bf419ae40c6cd649 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Tue, 4 Feb 2025 10:38:37 -0600 Subject: [PATCH 138/184] Fix Tabular.from_xml_element for histogram case (#3287) --- openmc/stats/univariate.py | 5 +++-- src/distribution.cpp | 6 +++++- tests/unit_tests/test_stats.py | 33 +++++++++++++++++++++++---------- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/openmc/stats/univariate.py b/openmc/stats/univariate.py index 0dc6f385685..e0475bf78fb 100644 --- a/openmc/stats/univariate.py +++ b/openmc/stats/univariate.py @@ -1116,8 +1116,9 @@ def from_xml_element(cls, elem: ET.Element): """ interpolation = get_text(elem, 'interpolation') params = [float(x) for x in get_text(elem, 'parameters').split()] - x = params[:len(params)//2] - p = params[len(params)//2:] + m = (len(params) + 1)//2 # +1 for when len(params) is odd + x = params[:m] + p = params[m:] return cls(x, p, interpolation) def integral(self): diff --git a/src/distribution.cpp b/src/distribution.cpp index a6b4acd58b1..8c700460f44 100644 --- a/src/distribution.cpp +++ b/src/distribution.cpp @@ -258,8 +258,12 @@ Tabular::Tabular(pugi::xml_node node) interp_ = Interpolation::histogram; } - // Read and initialize tabular distribution + // Read and initialize tabular distribution. If number of parameters is odd, + // add an extra zero for the 'p' array. auto params = get_node_array(node, "parameters"); + if (params.size() % 2 != 0) { + params.push_back(0.0); + } std::size_t n = params.size() / 2; const double* x = params.data(); const double* p = x + n; diff --git a/tests/unit_tests/test_stats.py b/tests/unit_tests/test_stats.py index 643e115564b..386181f34d2 100644 --- a/tests/unit_tests/test_stats.py +++ b/tests/unit_tests/test_stats.py @@ -195,19 +195,10 @@ def test_watt(): def test_tabular(): + # test linear-linear sampling x = np.array([0.0, 5.0, 7.0, 10.0]) p = np.array([10.0, 20.0, 5.0, 6.0]) d = openmc.stats.Tabular(x, p, 'linear-linear') - elem = d.to_xml_element('distribution') - - d = openmc.stats.Tabular.from_xml_element(elem) - assert all(d.x == x) - assert all(d.p == p) - assert d.interpolation == 'linear-linear' - assert len(d) == len(x) - - # test linear-linear sampling - d = openmc.stats.Tabular(x, p) n_samples = 100_000 samples = d.sample(n_samples) assert_sample_mean(samples, d.mean()) @@ -242,6 +233,28 @@ def test_tabular(): d.cdf() +def test_tabular_from_xml(): + x = np.array([0.0, 5.0, 7.0, 10.0]) + p = np.array([10.0, 20.0, 5.0, 6.0]) + d = openmc.stats.Tabular(x, p, 'linear-linear') + elem = d.to_xml_element('distribution') + + d = openmc.stats.Tabular.from_xml_element(elem) + assert all(d.x == x) + assert all(d.p == p) + assert d.interpolation == 'linear-linear' + assert len(d) == len(x) + + # Make sure XML roundtrip works with len(x) == len(p) + 1 + x = np.array([0.0, 5.0, 7.0, 10.0]) + p = np.array([10.0, 20.0, 5.0]) + d = openmc.stats.Tabular(x, p, 'histogram') + elem = d.to_xml_element('distribution') + d = openmc.stats.Tabular.from_xml_element(elem) + assert all(d.x == x) + assert all(d.p == p) + + def test_legendre(): # Pu239 elastic scattering at 100 keV coeffs = [1.000e+0, 1.536e-1, 1.772e-2, 5.945e-4, 3.497e-5, 1.881e-5] From 7e033b25ada8f8582f1a95fe74d0f6e11bbfd16b Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Tue, 11 Feb 2025 09:15:43 +0100 Subject: [PATCH 139/184] added terminal output showing compile options selected (#3291) Co-authored-by: Jon Shimwell --- CMakeLists.txt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 575e45373ae..e15de1b6f66 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,17 @@ option(OPENMC_USE_MCPL "Enable MCPL" option(OPENMC_USE_NCRYSTAL "Enable support for NCrystal scattering" OFF) option(OPENMC_USE_UWUW "Enable UWUW" OFF) +message(STATUS "OPENMC_USE_OPENMP ${OPENMC_USE_OPENMP}") +message(STATUS "OPENMC_BUILD_TESTS ${OPENMC_BUILD_TESTS}") +message(STATUS "OPENMC_ENABLE_PROFILE ${OPENMC_ENABLE_PROFILE}") +message(STATUS "OPENMC_ENABLE_COVERAGE ${OPENMC_ENABLE_COVERAGE}") +message(STATUS "OPENMC_USE_DAGMC ${OPENMC_USE_DAGMC}") +message(STATUS "OPENMC_USE_LIBMESH ${OPENMC_USE_LIBMESH}") +message(STATUS "OPENMC_USE_MPI ${OPENMC_USE_MPI}") +message(STATUS "OPENMC_USE_MCPL ${OPENMC_USE_MCPL}") +message(STATUS "OPENMC_USE_NCRYSTAL ${OPENMC_USE_NCRYSTAL}") +message(STATUS "OPENMC_USE_UWUW ${OPENMC_USE_UWUW}") + # Warnings for deprecated options foreach(OLD_OPT IN ITEMS "openmp" "profile" "coverage" "dagmc" "libmesh") if(DEFINED ${OLD_OPT}) From 27ce2ceee3e666d211cdd5d9db25300dada67fa1 Mon Sep 17 00:00:00 2001 From: Thomas Kittelmann Date: Tue, 11 Feb 2025 15:18:27 +0100 Subject: [PATCH 140/184] Updates for building with NCrystal support (and fix CI) (#3274) --- CMakeLists.txt | 10 +++- cmake/OpenMCConfig.cmake.in | 10 +++- docs/source/usersguide/install.rst | 5 +- include/openmc/ncrystal_interface.h | 16 ++----- .../ncrystal/results_true.dat | 14 +++--- tools/ci/gha-install-ncrystal.sh | 46 ------------------- tools/ci/gha-install.py | 2 - tools/ci/gha-install.sh | 4 +- tools/ci/gha-script.sh | 2 - 9 files changed, 33 insertions(+), 76 deletions(-) delete mode 100755 tools/ci/gha-install-ncrystal.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index e15de1b6f66..f7079aebcdc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -121,7 +121,15 @@ endmacro() #=============================================================================== if(OPENMC_USE_NCRYSTAL) - find_package(NCrystal REQUIRED) + if(NOT DEFINED "NCrystal_DIR") + #Invocation of "ncrystal-config --show cmakedir" is needed to find NCrystal + #when it is installed from Python wheels: + execute_process( + COMMAND "ncrystal-config" "--show" "cmakedir" + OUTPUT_VARIABLE "NCrystal_DIR" OUTPUT_STRIP_TRAILING_WHITESPACE + ) + endif() + find_package(NCrystal 3.8.0 REQUIRED) message(STATUS "Found NCrystal: ${NCrystal_DIR} (version ${NCrystal_VERSION})") endif() diff --git a/cmake/OpenMCConfig.cmake.in b/cmake/OpenMCConfig.cmake.in index b3b901de427..b02bbffe5cf 100644 --- a/cmake/OpenMCConfig.cmake.in +++ b/cmake/OpenMCConfig.cmake.in @@ -10,6 +10,14 @@ if(@OPENMC_USE_DAGMC@) endif() if(@OPENMC_USE_NCRYSTAL@) + if(NOT DEFINED "NCrystal_DIR") + #Invocation of "ncrystal-config --show cmakedir" is needed to find NCrystal + #when it is installed from Python wheels: + execute_process( + COMMAND "ncrystal-config" "--show" "cmakedir" + OUTPUT_VARIABLE "NCrystal_DIR" OUTPUT_STRIP_TRAILING_WHITESPACE + ) + endif() find_package(NCrystal REQUIRED) message(STATUS "Found NCrystal: ${NCrystal_DIR} (version ${NCrystal_VERSION})") endif() @@ -41,4 +49,4 @@ endif() if(@OPENMC_USE_UWUW@ AND NOT ${DAGMC_BUILD_UWUW}) message(FATAL_ERROR "UWUW is enabled in OpenMC but the DAGMC installation discovered was not configured with UWUW.") -endif() \ No newline at end of file +endif() diff --git a/docs/source/usersguide/install.rst b/docs/source/usersguide/install.rst index 1c0b7fa5b32..b86f5fee032 100644 --- a/docs/source/usersguide/install.rst +++ b/docs/source/usersguide/install.rst @@ -287,9 +287,8 @@ Prerequisites Adding this option allows the creation of materials from NCrystal, which replaces the scattering kernel treatment of ACE files with a modular, on-the-fly approach. To use it `install - `_ and `initialize - `_ - NCrystal and turn on the option in the CMake configuration step:: + `_ NCrystal and + turn on the option in the CMake configuration step:: cmake -DOPENMC_USE_NCRYSTAL=on .. diff --git a/include/openmc/ncrystal_interface.h b/include/openmc/ncrystal_interface.h index 5a3882df9c3..22422e378cf 100644 --- a/include/openmc/ncrystal_interface.h +++ b/include/openmc/ncrystal_interface.h @@ -2,7 +2,6 @@ #define OPENMC_NCRYSTAL_INTERFACE_H #ifdef NCRYSTAL -#include "NCrystal/NCRNG.hh" #include "NCrystal/NCrystal.hh" #endif @@ -58,19 +57,10 @@ class NCrystalMat { //---------------------------------------------------------------------------- // Trivial methods when compiling without NCRYSTAL - std::string cfg() const - { - return ""; - } - double xs(const Particle& p) const - { - return -1.0; - } + std::string cfg() const { return ""; } + double xs(const Particle& p) const { return -1.0; } void scatter(Particle& p) const {} - operator bool() const - { - return false; - } + operator bool() const { return false; } #endif private: diff --git a/tests/regression_tests/ncrystal/results_true.dat b/tests/regression_tests/ncrystal/results_true.dat index 3ef0306fdf4..28c02c51c44 100644 --- a/tests/regression_tests/ncrystal/results_true.dat +++ b/tests/regression_tests/ncrystal/results_true.dat @@ -106,8 +106,8 @@ 104 1 1.82e+00 1.83e+00 1 total current 1.10e-05 4.07e-06 105 1 1.83e+00 1.85e+00 1 total current 1.50e-05 4.01e-06 106 1 1.85e+00 1.87e+00 1 total current 1.90e-05 4.82e-06 -107 1 1.87e+00 1.88e+00 1 total current 2.20e-05 3.89e-06 -108 1 1.88e+00 1.90e+00 1 total current 2.10e-05 3.79e-06 +107 1 1.87e+00 1.88e+00 1 total current 2.30e-05 3.96e-06 +108 1 1.88e+00 1.90e+00 1 total current 2.00e-05 3.94e-06 109 1 1.90e+00 1.92e+00 1 total current 1.50e-05 3.42e-06 110 1 1.92e+00 1.94e+00 1 total current 2.20e-05 5.12e-06 111 1 1.94e+00 1.95e+00 1 total current 2.10e-05 5.86e-06 @@ -118,12 +118,12 @@ 116 1 2.02e+00 2.04e+00 1 total current 1.30e-05 3.35e-06 117 1 2.04e+00 2.06e+00 1 total current 1.90e-05 4.07e-06 118 1 2.06e+00 2.08e+00 1 total current 1.30e-05 3.00e-06 -119 1 2.08e+00 2.09e+00 1 total current 1.40e-05 4.00e-06 -120 1 2.09e+00 2.11e+00 1 total current 3.10e-05 5.47e-06 +119 1 2.08e+00 2.09e+00 1 total current 1.50e-05 4.28e-06 +120 1 2.09e+00 2.11e+00 1 total current 3.00e-05 4.94e-06 121 1 2.11e+00 2.13e+00 1 total current 2.30e-05 5.39e-06 122 1 2.13e+00 2.15e+00 1 total current 2.20e-05 4.67e-06 -123 1 2.15e+00 2.16e+00 1 total current 1.70e-05 4.48e-06 -124 1 2.16e+00 2.18e+00 1 total current 1.60e-05 4.00e-06 +123 1 2.15e+00 2.16e+00 1 total current 1.80e-05 4.67e-06 +124 1 2.16e+00 2.18e+00 1 total current 1.50e-05 4.01e-06 125 1 2.18e+00 2.20e+00 1 total current 1.80e-05 4.16e-06 126 1 2.20e+00 2.22e+00 1 total current 1.80e-05 4.42e-06 127 1 2.22e+00 2.23e+00 1 total current 1.80e-05 5.54e-06 @@ -135,7 +135,7 @@ 133 1 2.32e+00 2.34e+00 1 total current 2.30e-05 3.96e-06 134 1 2.34e+00 2.36e+00 1 total current 1.90e-05 3.48e-06 135 1 2.36e+00 2.37e+00 1 total current 1.60e-05 3.71e-06 -136 1 2.37e+00 2.39e+00 1 total current 1.60e-05 4.27e-06 +136 1 2.37e+00 2.39e+00 1 total current 1.70e-05 4.48e-06 137 1 2.39e+00 2.41e+00 1 total current 2.30e-05 4.48e-06 138 1 2.41e+00 2.43e+00 1 total current 2.10e-05 3.48e-06 139 1 2.43e+00 2.44e+00 1 total current 1.60e-05 2.21e-06 diff --git a/tools/ci/gha-install-ncrystal.sh b/tools/ci/gha-install-ncrystal.sh deleted file mode 100755 index 16f77e13e2b..00000000000 --- a/tools/ci/gha-install-ncrystal.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash -set -ex -cd $HOME - -#Use the NCrystal develop branch (in the near future we can move this to master): -git clone https://github.com/mctools/ncrystal --branch develop --single-branch --depth 1 ncrystal_src - -SRC_DIR="$PWD/ncrystal_src" -BLD_DIR="$PWD/ncrystal_bld" -INST_DIR="$PWD/ncrystal_inst" -PYTHON=$(which python3) - -CPU_COUNT=1 - -mkdir "$BLD_DIR" -cd ncrystal_bld - -cmake \ - "${SRC_DIR}" \ - -DBUILD_SHARED_LIBS=ON \ - -DNCRYSTAL_NOTOUCH_CMAKE_BUILD_TYPE=ON \ - -DNCRYSTAL_MODIFY_RPATH=OFF \ - -DCMAKE_BUILD_TYPE=Release \ - -DNCRYSTAL_ENABLE_EXAMPLES=OFF \ - -DNCRYSTAL_ENABLE_SETUPSH=OFF \ - -DNCRYSTAL_ENABLE_DATA=EMBED \ - -DCMAKE_INSTALL_PREFIX="${INST_DIR}" \ - -DPython3_EXECUTABLE="$PYTHON" - -make -j${CPU_COUNT:-1} -make install - -#Note: There is no "make test" or "make ctest" functionality for NCrystal -# yet. If it appears in the future, we should add it here. - -# Output the configuration to the log - -"${INST_DIR}/bin/ncrystal-config" --setup - -# Change environmental variables - -eval $( "${INST_DIR}/bin/ncrystal-config" --setup ) - -# Check installation worked - -nctool --test diff --git a/tools/ci/gha-install.py b/tools/ci/gha-install.py index d52c4c8254c..1eb9a55b4dc 100644 --- a/tools/ci/gha-install.py +++ b/tools/ci/gha-install.py @@ -42,8 +42,6 @@ def install(omp=False, mpi=False, phdf5=False, dagmc=False, libmesh=False, ncrys if ncrystal: cmake_cmd.append('-DOPENMC_USE_NCRYSTAL=ON') - ncrystal_path = os.environ.get('HOME') + '/ncrystal_inst' - cmake_cmd.append(f'-DCMAKE_PREFIX_PATH={ncrystal_path}') # Build in coverage mode for coverage testing cmake_cmd.append('-DOPENMC_ENABLE_COVERAGE=on') diff --git a/tools/ci/gha-install.sh b/tools/ci/gha-install.sh index cff7dc834f5..50f110ed4e6 100755 --- a/tools/ci/gha-install.sh +++ b/tools/ci/gha-install.sh @@ -16,7 +16,9 @@ fi # Install NCrystal if needed if [[ $NCRYSTAL = 'y' ]]; then - ./tools/ci/gha-install-ncrystal.sh + pip install 'ncrystal>=4.0.0' + #Basic quick verification: + nctool --test fi # Install vectfit for WMP generation if needed diff --git a/tools/ci/gha-script.sh b/tools/ci/gha-script.sh index 9de84f3e553..4733907eb25 100755 --- a/tools/ci/gha-script.sh +++ b/tools/ci/gha-script.sh @@ -16,8 +16,6 @@ fi # Check NCrystal installation if [[ $NCRYSTAL = 'y' ]]; then - # Change environmental variables - eval $( "${HOME}/ncrystal_inst/bin/ncrystal-config" --setup ) nctool --test fi From 04393200cc79defe5cbfc5484376ea5d753f067c Mon Sep 17 00:00:00 2001 From: John Tramm Date: Tue, 11 Feb 2025 11:23:26 -0600 Subject: [PATCH 141/184] Random Ray Source Region Refactor (#3288) --- CMakeLists.txt | 1 + include/openmc/openmp_interface.h | 12 +- .../openmc/random_ray/flat_source_domain.h | 132 +----- .../openmc/random_ray/linear_source_domain.h | 30 +- include/openmc/random_ray/source_region.h | 400 ++++++++++++++++++ src/random_ray/flat_source_domain.cpp | 290 +++++-------- src/random_ray/linear_source_domain.cpp | 129 ++---- src/random_ray/random_ray.cpp | 60 ++- src/random_ray/random_ray_simulation.cpp | 2 +- src/random_ray/source_region.cpp | 236 +++++++++++ 10 files changed, 832 insertions(+), 460 deletions(-) create mode 100644 include/openmc/random_ray/source_region.h create mode 100644 src/random_ray/source_region.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index f7079aebcdc..94caec757ab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -401,6 +401,7 @@ list(APPEND libopenmc_SOURCES src/random_ray/flat_source_domain.cpp src/random_ray/linear_source_domain.cpp src/random_ray/moment_matrix.cpp + src/random_ray/source_region.cpp src/reaction.cpp src/reaction_product.cpp src/scattdata.cpp diff --git a/include/openmc/openmp_interface.h b/include/openmc/openmp_interface.h index fe517415baa..30bd6f008d4 100644 --- a/include/openmc/openmp_interface.h +++ b/include/openmc/openmp_interface.h @@ -36,13 +36,15 @@ inline int thread_num() class OpenMPMutex { public: - OpenMPMutex() + void init() { #ifdef _OPENMP omp_init_lock(&mutex_); #endif } + OpenMPMutex() { init(); } + ~OpenMPMutex() { #ifdef _OPENMP @@ -62,14 +64,10 @@ class OpenMPMutex { // rather, it produces two different mutexes. // Copy constructor - OpenMPMutex(const OpenMPMutex& other) { OpenMPMutex(); } + OpenMPMutex(const OpenMPMutex& other) { init(); } // Copy assignment operator - OpenMPMutex& operator=(const OpenMPMutex& other) - { - OpenMPMutex(); - return *this; - } + OpenMPMutex& operator=(const OpenMPMutex& other) { return *this; } //! Lock the mutex. // diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index b5b5db7062e..011ff7ccdf8 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -4,83 +4,12 @@ #include "openmc/constants.h" #include "openmc/openmp_interface.h" #include "openmc/position.h" +#include "openmc/random_ray/source_region.h" #include "openmc/source.h" +#include namespace openmc { -//---------------------------------------------------------------------------- -// Helper Functions - -// The hash_combine function is the standard hash combine function from boost -// that is typically used for combining multiple hash values into a single hash -// as is needed for larger objects being stored in a hash map. The function is -// taken from: -// https://www.boost.org/doc/libs/1_55_0/doc/html/hash/reference.html#boost.hash_combine -// which carries the following license: -// -// Boost Software License - Version 1.0 - August 17th, 2003 -// Permission is hereby granted, free of charge, to any person or organization -// obtaining a copy of the software and accompanying documentation covered by -// this license (the "Software") to use, reproduce, display, distribute, -// execute, and transmit the Software, and to prepare derivative works of the -// Software, and to permit third-parties to whom the Software is furnished to -// do so, all subject to the following: -// The copyright notices in the Software and this entire statement, including -// the above license grant, this restriction and the following disclaimer, -// must be included in all copies of the Software, in whole or in part, and -// all derivative works of the Software, unless such copies or derivative -// works are solely in the form of machine-executable object code generated by -// a source language processor. -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT -// SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE -// FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, -// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -// DEALINGS IN THE SOFTWARE. -inline void hash_combine(size_t& seed, const size_t v) -{ - seed ^= (v + 0x9e3779b9 + (seed << 6) + (seed >> 2)); -} - -//---------------------------------------------------------------------------- -// Helper Structs - -// A mapping object that is used to map between a specific random ray -// source region and an OpenMC native tally bin that it should score to -// every iteration. -struct TallyTask { - int tally_idx; - int filter_idx; - int score_idx; - int score_type; - TallyTask(int tally_idx, int filter_idx, int score_idx, int score_type) - : tally_idx(tally_idx), filter_idx(filter_idx), score_idx(score_idx), - score_type(score_type) - {} - TallyTask() = default; - - // Comparison and Hash operators are defined to allow usage of the - // TallyTask struct as a key in an unordered_set - bool operator==(const TallyTask& other) const - { - return tally_idx == other.tally_idx && filter_idx == other.filter_idx && - score_idx == other.score_idx && score_type == other.score_type; - } - - struct HashFunctor { - size_t operator()(const TallyTask& task) const - { - size_t seed = 0; - hash_combine(seed, task.tally_idx); - hash_combine(seed, task.filter_idx); - hash_combine(seed, task.score_idx); - hash_combine(seed, task.score_type); - return seed; - } - }; -}; - /* * The FlatSourceDomain class encompasses data and methods for storing * scalar flux and source region for all flat source regions in a @@ -108,15 +37,16 @@ class FlatSourceDomain { void random_ray_tally(); virtual void accumulate_iteration_flux(); void output_to_vtk() const; - virtual void all_reduce_replicated_source_regions(); + void all_reduce_replicated_source_regions(); void convert_external_sources(); void count_external_source_regions(); void set_adjoint_sources(const vector& forward_flux); - virtual void flux_swap(); + void flux_swap(); virtual double evaluate_flux_at_point(Position r, int64_t sr, int g) const; double compute_fixed_source_normalization_factor() const; void flatten_xs(); void transpose_scattering_matrix(); + void serialize_final_fluxes(vector& flux); //---------------------------------------------------------------------------- // Static Data members @@ -129,7 +59,6 @@ class FlatSourceDomain { //---------------------------------------------------------------------------- // Public Data members - bool mapped_all_tallies_ {false}; // If all source regions have been visited int64_t n_source_regions_ {0}; // Total number of source regions in the model @@ -140,22 +69,6 @@ class FlatSourceDomain { // in model::cells vector source_region_offsets_; - // 1D arrays representing values for all source regions - vector lock_; - vector volume_; - vector volume_t_; - vector position_recorded_; - vector position_; - - // 2D arrays stored in 1D representing values for all source regions x energy - // groups - vector scalar_flux_old_; - vector scalar_flux_new_; - vector source_; - vector external_source_; - vector external_source_present_; - vector scalar_flux_final_; - // 2D arrays stored in 1D representing values for all materials x energy // groups int n_materials_; @@ -168,20 +81,22 @@ class FlatSourceDomain { // groups x energy groups vector sigma_s_; + // The abstract container holding all source region-specific data + SourceRegionContainer source_regions_; + protected: //---------------------------------------------------------------------------- // Methods void apply_external_source_to_source_region( - Discrete* discrete, double strength_factor, int64_t source_region); + Discrete* discrete, double strength_factor, int64_t sr); void apply_external_source_to_cell_instances(int32_t i_cell, Discrete* discrete, double strength_factor, int target_material_id, const vector& instances); void apply_external_source_to_cell_and_children(int32_t i_cell, Discrete* discrete, double strength_factor, int32_t target_material_id); - virtual void set_flux_to_flux_plus_source( - int64_t idx, double volume, int material, int g); - void set_flux_to_source(int64_t idx); - virtual void set_flux_to_old_flux(int64_t idx); + virtual void set_flux_to_flux_plus_source(int64_t sr, double volume, int g); + void set_flux_to_source(int64_t sr, int g); + virtual void set_flux_to_old_flux(int64_t sr, int g); //---------------------------------------------------------------------------- // Private data members @@ -193,20 +108,6 @@ class FlatSourceDomain { simulation_volume_; // Total physical volume of the simulation domain, as // defined by the 3D box of the random ray source - // 2D array representing values for all source elements x tally - // tasks - vector> tally_task_; - - // 1D array representing values for all source regions, with each region - // containing a set of volume tally tasks. This more complicated data - // structure is convenient for ensuring that volumes are only tallied once per - // source region, regardless of how many energy groups are used for tallying. - vector> volume_task_; - - // 1D arrays representing values for all source regions - vector material_; - vector volume_naive_; - // Volumes for each tally and bin/score combination. This intermediate data // structure is used when tallying quantities that must be normalized by // volume (i.e., flux). The vector is index by tally index, while the inner 2D @@ -250,15 +151,6 @@ T convert_to_big_endian(T in) return out; } -template -void parallel_fill(vector& arr, T value) -{ -#pragma omp parallel for schedule(static) - for (int i = 0; i < arr.size(); i++) { - arr[i] = value; - } -} - } // namespace openmc #endif // OPENMC_RANDOM_RAY_FLAT_SOURCE_DOMAIN_H diff --git a/include/openmc/random_ray/linear_source_domain.h b/include/openmc/random_ray/linear_source_domain.h index 4812d14337c..67fdd99f880 100644 --- a/include/openmc/random_ray/linear_source_domain.h +++ b/include/openmc/random_ray/linear_source_domain.h @@ -18,48 +18,22 @@ namespace openmc { class LinearSourceDomain : public FlatSourceDomain { public: - //---------------------------------------------------------------------------- - // Constructors - LinearSourceDomain(); - //---------------------------------------------------------------------------- // Methods void update_neutron_source(double k_eff) override; - double compute_k_eff(double k_eff_old) const; void normalize_scalar_flux_and_volumes( double total_active_distance_per_iteration) override; void batch_reset() override; - void convert_source_regions_to_tallies(); - void reset_tally_volumes(); - void random_ray_tally(); void accumulate_iteration_flux() override; void output_to_vtk() const; - void all_reduce_replicated_source_regions() override; - void convert_external_sources(); - void count_external_source_regions(); - void flux_swap() override; double evaluate_flux_at_point(Position r, int64_t sr, int g) const override; - //---------------------------------------------------------------------------- - // Public Data members - - vector source_gradients_; - vector flux_moments_old_; - vector flux_moments_new_; - vector flux_moments_t_; - vector centroid_; - vector centroid_iteration_; - vector centroid_t_; - vector mom_matrix_; - vector mom_matrix_t_; - protected: //---------------------------------------------------------------------------- // Methods - void set_flux_to_flux_plus_source( - int64_t idx, double volume, int material, int g) override; - void set_flux_to_old_flux(int64_t idx) override; + void set_flux_to_flux_plus_source(int64_t sr, double volume, int g) override; + void set_flux_to_old_flux(int64_t sr, int g) override; }; // class LinearSourceDomain diff --git a/include/openmc/random_ray/source_region.h b/include/openmc/random_ray/source_region.h new file mode 100644 index 00000000000..0dc617a643a --- /dev/null +++ b/include/openmc/random_ray/source_region.h @@ -0,0 +1,400 @@ +#ifndef OPENMC_RANDOM_RAY_SOURCE_REGION_H +#define OPENMC_RANDOM_RAY_SOURCE_REGION_H + +#include "openmc/openmp_interface.h" +#include "openmc/position.h" +#include "openmc/random_ray/moment_matrix.h" +#include "openmc/settings.h" + +namespace openmc { + +//---------------------------------------------------------------------------- +// Helper Functions + +// The hash_combine function is the standard hash combine function from boost +// that is typically used for combining multiple hash values into a single hash +// as is needed for larger objects being stored in a hash map. The function is +// taken from: +// https://www.boost.org/doc/libs/1_55_0/doc/html/hash/reference.html#boost.hash_combine +// which carries the following license: +// +// Boost Software License - Version 1.0 - August 17th, 2003 +// Permission is hereby granted, free of charge, to any person or organization +// obtaining a copy of the software and accompanying documentation covered by +// this license (the "Software") to use, reproduce, display, distribute, +// execute, and transmit the Software, and to prepare derivative works of the +// Software, and to permit third-parties to whom the Software is furnished to +// do so, all subject to the following: +// The copyright notices in the Software and this entire statement, including +// the above license grant, this restriction and the following disclaimer, +// must be included in all copies of the Software, in whole or in part, and +// all derivative works of the Software, unless such copies or derivative +// works are solely in the form of machine-executable object code generated by +// a source language processor. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +// SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +// FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +inline void hash_combine(size_t& seed, const size_t v) +{ + seed ^= (v + 0x9e3779b9 + (seed << 6) + (seed >> 2)); +} + +//---------------------------------------------------------------------------- +// Helper Structs + +// A mapping object that is used to map between a specific random ray +// source region and an OpenMC native tally bin that it should score to +// every iteration. +struct TallyTask { + int tally_idx; + int filter_idx; + int score_idx; + int score_type; + TallyTask(int tally_idx, int filter_idx, int score_idx, int score_type) + : tally_idx(tally_idx), filter_idx(filter_idx), score_idx(score_idx), + score_type(score_type) + {} + TallyTask() = default; + + // Comparison and Hash operators are defined to allow usage of the + // TallyTask struct as a key in an unordered_set + bool operator==(const TallyTask& other) const + { + return tally_idx == other.tally_idx && filter_idx == other.filter_idx && + score_idx == other.score_idx && score_type == other.score_type; + } + + struct HashFunctor { + size_t operator()(const TallyTask& task) const + { + size_t seed = 0; + hash_combine(seed, task.tally_idx); + hash_combine(seed, task.filter_idx); + hash_combine(seed, task.score_idx); + hash_combine(seed, task.score_type); + return seed; + } + }; +}; + +class SourceRegion { +public: + //---------------------------------------------------------------------------- + // Constructors + SourceRegion(int negroups, bool is_linear); + SourceRegion() = default; + + //---------------------------------------------------------------------------- + // Public Data members + + // Scalar fields + int material_ {0}; + OpenMPMutex lock_; + double volume_ {0.0}; + double volume_t_ {0.0}; + double volume_naive_ {0.0}; + int position_recorded_ {0}; + int external_source_present_ {0}; + Position position_ {0.0, 0.0, 0.0}; + Position centroid_ {0.0, 0.0, 0.0}; + Position centroid_iteration_ {0.0, 0.0, 0.0}; + Position centroid_t_ {0.0, 0.0, 0.0}; + MomentMatrix mom_matrix_ {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; + MomentMatrix mom_matrix_t_ {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; + // A set of volume tally tasks. This more complicated data structure is + // convenient for ensuring that volumes are only tallied once per source + // region, regardless of how many energy groups are used for tallying. + std::unordered_set volume_task_; + + // Energy group-wise 1D arrays + vector scalar_flux_old_; + vector scalar_flux_new_; + vector source_; + vector external_source_; + vector scalar_flux_final_; + + vector source_gradients_; + vector flux_moments_old_; + vector flux_moments_new_; + vector flux_moments_t_; + + // 2D array representing values for all energy groups x tally + // tasks. Each group may have a different number of tally tasks + // associated with it, necessitating the use of a jagged array. + vector> tally_task_; + +}; // class SourceRegion + +class SourceRegionContainer { +public: + //---------------------------------------------------------------------------- + // Constructors + SourceRegionContainer(int negroups, bool is_linear) + : negroups_(negroups), is_linear_(is_linear) + {} + SourceRegionContainer() = default; + + //---------------------------------------------------------------------------- + // Public Accessors + int& material(int64_t sr) { return material_[sr]; } + const int& material(int64_t sr) const { return material_[sr]; } + + OpenMPMutex& lock(int64_t sr) { return lock_[sr]; } + const OpenMPMutex& lock(int64_t sr) const { return lock_[sr]; } + + double& volume(int64_t sr) { return volume_[sr]; } + const double& volume(int64_t sr) const { return volume_[sr]; } + + double& volume_t(int64_t sr) { return volume_t_[sr]; } + const double& volume_t(int64_t sr) const { return volume_t_[sr]; } + + double& volume_naive(int64_t sr) { return volume_naive_[sr]; } + const double& volume_naive(int64_t sr) const { return volume_naive_[sr]; } + + int& position_recorded(int64_t sr) { return position_recorded_[sr]; } + const int& position_recorded(int64_t sr) const + { + return position_recorded_[sr]; + } + + int& external_source_present(int64_t sr) + { + return external_source_present_[sr]; + } + const int& external_source_present(int64_t sr) const + { + return external_source_present_[sr]; + } + + Position& position(int64_t sr) { return position_[sr]; } + const Position& position(int64_t sr) const { return position_[sr]; } + + Position& centroid(int64_t sr) { return centroid_[sr]; } + const Position& centroid(int64_t sr) const { return centroid_[sr]; } + + Position& centroid_iteration(int64_t sr) { return centroid_iteration_[sr]; } + const Position& centroid_iteration(int64_t sr) const + { + return centroid_iteration_[sr]; + } + + Position& centroid_t(int64_t sr) { return centroid_t_[sr]; } + const Position& centroid_t(int64_t sr) const { return centroid_t_[sr]; } + + MomentMatrix& mom_matrix(int64_t sr) { return mom_matrix_[sr]; } + const MomentMatrix& mom_matrix(int64_t sr) const { return mom_matrix_[sr]; } + + MomentMatrix& mom_matrix_t(int64_t sr) { return mom_matrix_t_[sr]; } + const MomentMatrix& mom_matrix_t(int64_t sr) const + { + return mom_matrix_t_[sr]; + } + + MomentArray& source_gradients(int64_t sr, int g) + { + return source_gradients_[index(sr, g)]; + } + const MomentArray& source_gradients(int64_t sr, int g) const + { + return source_gradients_[index(sr, g)]; + } + MomentArray& source_gradients(int64_t se) { return source_gradients_[se]; } + const MomentArray& source_gradients(int64_t se) const + { + return source_gradients_[se]; + } + + MomentArray& flux_moments_old(int64_t sr, int g) + { + return flux_moments_old_[index(sr, g)]; + } + const MomentArray& flux_moments_old(int64_t sr, int g) const + { + return flux_moments_old_[index(sr, g)]; + } + MomentArray& flux_moments_old(int64_t se) { return flux_moments_old_[se]; } + const MomentArray& flux_moments_old(int64_t se) const + { + return flux_moments_old_[se]; + } + + MomentArray& flux_moments_new(int64_t sr, int g) + { + return flux_moments_new_[index(sr, g)]; + } + const MomentArray& flux_moments_new(int64_t sr, int g) const + { + return flux_moments_new_[index(sr, g)]; + } + MomentArray& flux_moments_new(int64_t se) { return flux_moments_new_[se]; } + const MomentArray& flux_moments_new(int64_t se) const + { + return flux_moments_new_[se]; + } + + MomentArray& flux_moments_t(int64_t sr, int g) + { + return flux_moments_t_[index(sr, g)]; + } + const MomentArray& flux_moments_t(int64_t sr, int g) const + { + return flux_moments_t_[index(sr, g)]; + } + MomentArray& flux_moments_t(int64_t se) { return flux_moments_t_[se]; } + const MomentArray& flux_moments_t(int64_t se) const + { + return flux_moments_t_[se]; + } + + double& scalar_flux_old(int64_t sr, int g) + { + return scalar_flux_old_[index(sr, g)]; + } + const double& scalar_flux_old(int64_t sr, int g) const + { + return scalar_flux_old_[index(sr, g)]; + } + double& scalar_flux_old(int64_t se) { return scalar_flux_old_[se]; } + const double& scalar_flux_old(int64_t se) const + { + return scalar_flux_old_[se]; + } + + double& scalar_flux_new(int64_t sr, int g) + { + return scalar_flux_new_[index(sr, g)]; + } + const double& scalar_flux_new(int64_t sr, int g) const + { + return scalar_flux_new_[index(sr, g)]; + } + double& scalar_flux_new(int64_t se) { return scalar_flux_new_[se]; } + const double& scalar_flux_new(int64_t se) const + { + return scalar_flux_new_[se]; + } + + double& scalar_flux_final(int64_t sr, int g) + { + return scalar_flux_final_[index(sr, g)]; + } + const double& scalar_flux_final(int64_t sr, int g) const + { + return scalar_flux_final_[index(sr, g)]; + } + double& scalar_flux_final(int64_t se) { return scalar_flux_final_[se]; } + const double& scalar_flux_final(int64_t se) const + { + return scalar_flux_final_[se]; + } + + float& source(int64_t sr, int g) { return source_[index(sr, g)]; } + const float& source(int64_t sr, int g) const { return source_[index(sr, g)]; } + float& source(int64_t se) { return source_[se]; } + const float& source(int64_t se) const { return source_[se]; } + + float& external_source(int64_t sr, int g) + { + return external_source_[index(sr, g)]; + } + const float& external_source(int64_t sr, int g) const + { + return external_source_[index(sr, g)]; + } + float& external_source(int64_t se) { return external_source_[se]; } + const float& external_source(int64_t se) const + { + return external_source_[se]; + } + + vector& tally_task(int64_t sr, int g) + { + return tally_task_[index(sr, g)]; + } + const vector& tally_task(int64_t sr, int g) const + { + return tally_task_[index(sr, g)]; + } + vector& tally_task(int64_t se) { return tally_task_[se]; } + const vector& tally_task(int64_t se) const + { + return tally_task_[se]; + } + + std::unordered_set& volume_task(int64_t sr) + { + return volume_task_[sr]; + } + const std::unordered_set& volume_task( + int64_t sr) const + { + return volume_task_[sr]; + } + + //---------------------------------------------------------------------------- + // Public Methods + + void push_back(const SourceRegion& sr); + void assign(int n_source_regions, const SourceRegion& source_region); + void flux_swap(); + void mpi_sync_ranks(bool reduce_position); + +private: + //---------------------------------------------------------------------------- + // Private Data Members + int64_t n_source_regions_ {0}; + int negroups_ {0}; + bool is_linear_ {false}; + + // SoA storage for scalar fields (one item per source region) + vector material_; + vector lock_; + vector volume_; + vector volume_t_; + vector volume_naive_; + vector position_recorded_; + vector external_source_present_; + vector position_; + vector centroid_; + vector centroid_iteration_; + vector centroid_t_; + vector mom_matrix_; + vector mom_matrix_t_; + // A set of volume tally tasks. This more complicated data structure is + // convenient for ensuring that volumes are only tallied once per source + // region, regardless of how many energy groups are used for tallying. + vector> volume_task_; + + // SoA energy group-wise 2D arrays flattened to 1D + vector scalar_flux_old_; + vector scalar_flux_new_; + vector scalar_flux_final_; + vector source_; + vector external_source_; + + vector source_gradients_; + vector flux_moments_old_; + vector flux_moments_new_; + vector flux_moments_t_; + + // SoA 3D array representing values for all source regions x energy groups x + // tally tasks. The outer two dimensions (source regions and energy groups) + // are flattened to 1D. Each group may have a different number of tally tasks + // associated with it, necessitating the use of a jagged array for the inner + // dimension. + vector> tally_task_; + + //---------------------------------------------------------------------------- + // Private Methods + + // Helper function for indexing + inline int index(int64_t sr, int g) const { return sr * negroups_ + g; } +}; + +} // namespace openmc + +#endif // OPENMC_RANDOM_RAY_SOURCE_REGION_H \ No newline at end of file diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 19913c03c16..39a50ace60d 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -46,40 +46,17 @@ FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) } // Initialize cell-wise arrays - lock_.resize(n_source_regions_); - material_.resize(n_source_regions_); - position_recorded_.assign(n_source_regions_, 0); - position_.resize(n_source_regions_); - volume_.assign(n_source_regions_, 0.0); - volume_t_.assign(n_source_regions_, 0.0); - volume_naive_.assign(n_source_regions_, 0.0); - - // Initialize element-wise arrays - scalar_flux_new_.assign(n_source_elements_, 0.0); - scalar_flux_final_.assign(n_source_elements_, 0.0); - source_.resize(n_source_elements_); - - tally_task_.resize(n_source_elements_); - volume_task_.resize(n_source_regions_); - - if (settings::run_mode == RunMode::EIGENVALUE) { - // If in eigenvalue mode, set starting flux to guess of unity - scalar_flux_old_.assign(n_source_elements_, 1.0); - } else { - // If in fixed source mode, set starting flux to guess of zero - // and initialize external source arrays - scalar_flux_old_.assign(n_source_elements_, 0.0); - external_source_.assign(n_source_elements_, 0.0); - external_source_present_.assign(n_source_regions_, false); - } + bool is_linear = RandomRay::source_shape_ != RandomRaySourceShape::FLAT; + source_regions_ = SourceRegionContainer(negroups_, is_linear); + source_regions_.assign(n_source_regions_, SourceRegion(negroups_, is_linear)); - // Initialize material array + // Initialize materials int64_t source_region_id = 0; for (int i = 0; i < model::cells.size(); i++) { Cell& cell = *model::cells[i]; if (cell.type_ == Fill::MATERIAL) { for (int j = 0; j < cell.n_instances_; j++) { - material_[source_region_id++] = cell.material(j); + source_regions_.material(source_region_id++) = cell.material(j); } } } @@ -113,17 +90,23 @@ FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) void FlatSourceDomain::batch_reset() { - // Reset scalar fluxes, iteration volume tallies, and region hit flags to - // zero - parallel_fill(scalar_flux_new_, 0.0); - parallel_fill(volume_, 0.0); +// Reset scalar fluxes and iteration volume tallies to zero +#pragma omp parallel for + for (int64_t sr = 0; sr < n_source_regions_; sr++) { + source_regions_.volume(sr) = 0.0; + } +#pragma omp parallel for + for (int64_t se = 0; se < n_source_elements_; se++) { + source_regions_.scalar_flux_new(se) = 0.0; + } } void FlatSourceDomain::accumulate_iteration_flux() { #pragma omp parallel for for (int64_t se = 0; se < n_source_elements_; se++) { - scalar_flux_final_[se] += scalar_flux_new_[se]; + source_regions_.scalar_flux_final(se) += + source_regions_.scalar_flux_new(se); } } @@ -137,40 +120,40 @@ void FlatSourceDomain::update_neutron_source(double k_eff) // Add scattering source #pragma omp parallel for - for (int sr = 0; sr < n_source_regions_; sr++) { - int material = material_[sr]; + for (int64_t sr = 0; sr < n_source_regions_; sr++) { + int material = source_regions_.material(sr); for (int g_out = 0; g_out < negroups_; g_out++) { double sigma_t = sigma_t_[material * negroups_ + g_out]; double scatter_source = 0.0; for (int g_in = 0; g_in < negroups_; g_in++) { - double scalar_flux = scalar_flux_old_[sr * negroups_ + g_in]; + double scalar_flux = source_regions_.scalar_flux_old(sr, g_in); double sigma_s = sigma_s_[material * negroups_ * negroups_ + g_out * negroups_ + g_in]; scatter_source += sigma_s * scalar_flux; } - source_[sr * negroups_ + g_out] = scatter_source / sigma_t; + source_regions_.source(sr, g_out) = scatter_source / sigma_t; } } // Add fission source #pragma omp parallel for - for (int sr = 0; sr < n_source_regions_; sr++) { - int material = material_[sr]; + for (int64_t sr = 0; sr < n_source_regions_; sr++) { + int material = source_regions_.material(sr); for (int g_out = 0; g_out < negroups_; g_out++) { double sigma_t = sigma_t_[material * negroups_ + g_out]; double fission_source = 0.0; for (int g_in = 0; g_in < negroups_; g_in++) { - double scalar_flux = scalar_flux_old_[sr * negroups_ + g_in]; + double scalar_flux = source_regions_.scalar_flux_old(sr, g_in); double nu_sigma_f = nu_sigma_f_[material * negroups_ + g_in]; double chi = chi_[material * negroups_ + g_out]; fission_source += nu_sigma_f * scalar_flux * chi; } - source_[sr * negroups_ + g_out] += + source_regions_.source(sr, g_out) += fission_source * inverse_k_eff / sigma_t; } } @@ -179,7 +162,7 @@ void FlatSourceDomain::update_neutron_source(double k_eff) if (settings::run_mode == RunMode::FIXED_SOURCE) { #pragma omp parallel for for (int64_t se = 0; se < n_source_elements_; se++) { - source_[se] += external_source_[se]; + source_regions_.source(se) += source_regions_.external_source(se); } } @@ -194,38 +177,42 @@ void FlatSourceDomain::normalize_scalar_flux_and_volumes( double volume_normalization_factor = 1.0 / (total_active_distance_per_iteration * simulation::current_batch); -// Normalize scalar flux to total distance travelled by all rays this iteration +// Normalize scalar flux to total distance travelled by all rays this +// iteration #pragma omp parallel for - for (int64_t se = 0; se < scalar_flux_new_.size(); se++) { - scalar_flux_new_[se] *= normalization_factor; + for (int64_t se = 0; se < n_source_elements_; se++) { + source_regions_.scalar_flux_new(se) *= normalization_factor; } // Accumulate cell-wise ray length tallies collected this iteration, then // update the simulation-averaged cell-wise volume estimates #pragma omp parallel for for (int64_t sr = 0; sr < n_source_regions_; sr++) { - volume_t_[sr] += volume_[sr]; - volume_naive_[sr] = volume_[sr] * normalization_factor; - volume_[sr] = volume_t_[sr] * volume_normalization_factor; + source_regions_.volume_t(sr) += source_regions_.volume(sr); + source_regions_.volume_naive(sr) = + source_regions_.volume(sr) * normalization_factor; + source_regions_.volume(sr) = + source_regions_.volume_t(sr) * volume_normalization_factor; } } void FlatSourceDomain::set_flux_to_flux_plus_source( - int64_t idx, double volume, int material, int g) + int64_t sr, double volume, int g) { - double sigma_t = sigma_t_[material * negroups_ + g]; - scalar_flux_new_[idx] /= (sigma_t * volume); - scalar_flux_new_[idx] += source_[idx]; + double sigma_t = sigma_t_[source_regions_.material(sr) * negroups_ + g]; + source_regions_.scalar_flux_new(sr, g) /= (sigma_t * volume); + source_regions_.scalar_flux_new(sr, g) += source_regions_.source(sr, g); } -void FlatSourceDomain::set_flux_to_old_flux(int64_t idx) +void FlatSourceDomain::set_flux_to_old_flux(int64_t sr, int g) { - scalar_flux_new_[idx] = scalar_flux_old_[idx]; + source_regions_.scalar_flux_new(sr, g) = + source_regions_.scalar_flux_old(sr, g); } -void FlatSourceDomain::set_flux_to_source(int64_t idx) +void FlatSourceDomain::set_flux_to_source(int64_t sr, int g) { - scalar_flux_new_[idx] = source_[idx]; + source_regions_.scalar_flux_new(sr, g) = source_regions_.source(sr, g); } // Combine transport flux contributions and flat source contributions from the @@ -235,20 +222,16 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() int64_t n_hits = 0; #pragma omp parallel for reduction(+ : n_hits) - for (int sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions_; sr++) { - double volume_simulation_avg = volume_[sr]; - double volume_iteration = volume_naive_[sr]; + double volume_simulation_avg = source_regions_.volume(sr); + double volume_iteration = source_regions_.volume_naive(sr); // Increment the number of hits if cell was hit this iteration if (volume_iteration) { n_hits++; } - // Check if an external source is present in this source region - bool external_source_present = - external_source_present_.size() && external_source_present_[sr]; - // The volume treatment depends on the volume estimator type // and whether or not an external source is present in the cell. double volume; @@ -260,7 +243,7 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() volume = volume_simulation_avg; break; case RandomRayVolumeEstimator::HYBRID: - if (external_source_present) { + if (source_regions_.external_source_present(sr)) { volume = volume_iteration; } else { volume = volume_simulation_avg; @@ -270,17 +253,14 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() fatal_error("Invalid volume estimator type"); } - int material = material_[sr]; for (int g = 0; g < negroups_; g++) { - int64_t idx = (sr * negroups_) + g; - // There are three scenarios we need to consider: if (volume_iteration > 0.0) { // 1. If the FSR was hit this iteration, then the new flux is equal to // the flat source from the previous iteration plus the contributions // from rays passing through the source region (computed during the // transport sweep) - set_flux_to_flux_plus_source(idx, volume, material, g); + set_flux_to_flux_plus_source(sr, volume, g); } else if (volume_simulation_avg > 0.0) { // 2. If the FSR was not hit this iteration, but has been hit some // previous iteration, then we need to make a choice about what @@ -294,10 +274,10 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() // in the cell we will use the previous iteration's flux estimate. This // injects a small degree of correlation into the simulation, but this // is going to be trivial when the miss rate is a few percent or less. - if (external_source_present) { - set_flux_to_old_flux(idx); + if (source_regions_.external_source_present(sr)) { + set_flux_to_old_flux(sr, g); } else { - set_flux_to_source(idx); + set_flux_to_source(sr, g); } } // If the FSR was not hit this iteration, and it has never been hit in @@ -322,24 +302,25 @@ double FlatSourceDomain::compute_k_eff(double k_eff_old) const vector p(n_source_regions_, 0.0f); #pragma omp parallel for reduction(+ : fission_rate_old, fission_rate_new) - for (int sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions_; sr++) { // If simulation averaged volume is zero, don't include this cell - double volume = volume_[sr]; + double volume = source_regions_.volume(sr); if (volume == 0.0) { continue; } - int material = material_[sr]; + int material = source_regions_.material(sr); double sr_fission_source_old = 0; double sr_fission_source_new = 0; for (int g = 0; g < negroups_; g++) { - int64_t idx = (sr * negroups_) + g; double nu_sigma_f = nu_sigma_f_[material * negroups_ + g]; - sr_fission_source_old += nu_sigma_f * scalar_flux_old_[idx]; - sr_fission_source_new += nu_sigma_f * scalar_flux_new_[idx]; + sr_fission_source_old += + nu_sigma_f * source_regions_.scalar_flux_old(sr, g); + sr_fission_source_new += + nu_sigma_f * source_regions_.scalar_flux_new(sr, g); } // Compute total fission rates in FSR @@ -361,7 +342,7 @@ double FlatSourceDomain::compute_k_eff(double k_eff_old) const double inverse_sum = 1 / fission_rate_new; #pragma omp parallel for reduction(+ : H) - for (int sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions_; sr++) { // Only if FSR has non-negative and non-zero fission source if (p[sr] > 0.0f) { // Normalize to total weight of bank sites. p_i for better performance @@ -419,11 +400,11 @@ void FlatSourceDomain::convert_source_regions_to_tallies() // Attempt to generate mapping for all source regions #pragma omp parallel for - for (int sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions_; sr++) { // If this source region has not been hit by a ray yet, then // we aren't going to be able to map it, so skip it. - if (!position_recorded_[sr]) { + if (!source_regions_.position_recorded(sr)) { all_source_regions_mapped = false; continue; } @@ -432,8 +413,8 @@ void FlatSourceDomain::convert_source_regions_to_tallies() // crossing through this source region is used to estabilish // the spatial location of the source region Particle p; - p.r() = position_[sr]; - p.r_last() = position_[sr]; + p.r() = source_regions_.position(sr); + p.r_last() = source_regions_.position(sr); bool found = exhaustive_find_cell(p); // Loop over energy groups (so as to support energy filters) @@ -449,7 +430,7 @@ void FlatSourceDomain::convert_source_regions_to_tallies() // If this task has already been populated, we don't need to do // it again. - if (tally_task_[source_element].size() > 0) { + if (source_regions_.tally_task(sr, g).size() > 0) { continue; } @@ -479,11 +460,11 @@ void FlatSourceDomain::convert_source_regions_to_tallies() // If a valid tally, filter, and score combination has been found, // then add it to the list of tally tasks for this source element. TallyTask task(i_tally, filter_index, score_index, score_bin); - tally_task_[source_element].push_back(task); + source_regions_.tally_task(sr, g).push_back(task); // Also add this task to the list of volume tasks for this source // region. - volume_task_[sr].insert(task); + source_regions_.volume_task(sr).insert(task); } } } @@ -529,13 +510,13 @@ double FlatSourceDomain::compute_fixed_source_normalization_factor() const // total external source strength in the simulation. double simulation_external_source_strength = 0.0; #pragma omp parallel for reduction(+ : simulation_external_source_strength) - for (int sr = 0; sr < n_source_regions_; sr++) { - int material = material_[sr]; - double volume = volume_[sr] * simulation_volume_; + for (int64_t sr = 0; sr < n_source_regions_; sr++) { + int material = source_regions_.material(sr); + double volume = source_regions_.volume(sr) * simulation_volume_; for (int g = 0; g < negroups_; g++) { double sigma_t = sigma_t_[material * negroups_ + g]; simulation_external_source_strength += - external_source_[sr * negroups_ + g] * sigma_t * volume; + source_regions_.external_source(sr, g) * sigma_t * volume; } } @@ -576,7 +557,7 @@ void FlatSourceDomain::random_ray_tally() // element, we check if there are any scores needed and apply // them. #pragma omp parallel for - for (int sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions_; sr++) { // The fsr.volume_ is the unitless fractional simulation averaged volume // (i.e., it is the FSR's fraction of the overall simulation volume). The // simulation_volume_ is the total 3D physical volume in cm^3 of the @@ -586,15 +567,15 @@ void FlatSourceDomain::random_ray_tally() // eigenvalue solves, but useful in fixed source solves for returning the // flux shape with a magnitude that makes sense relative to the fixed // source strength. - double volume = volume_[sr] * simulation_volume_; + double volume = source_regions_.volume(sr) * simulation_volume_; - double material = material_[sr]; + double material = source_regions_.material(sr); for (int g = 0; g < negroups_; g++) { - int idx = sr * negroups_ + g; - double flux = scalar_flux_new_[idx] * source_normalization_factor; + double flux = + source_regions_.scalar_flux_new(sr, g) * source_normalization_factor; // Determine numerical score value - for (auto& task : tally_task_[idx]) { + for (auto& task : source_regions_.tally_task(sr, g)) { double score; switch (task.score_type) { @@ -637,7 +618,7 @@ void FlatSourceDomain::random_ray_tally() // for normalizing the flux. We store this volume in a separate tensor. // We only contribute to each volume tally bin once per FSR. if (volume_normalized_flux_tallies_) { - for (const auto& task : volume_task_[sr]) { + for (const auto& task : source_regions_.volume_task(sr)) { if (task.score_type == SCORE_FLUX) { #pragma omp atomic tally_volumes_[task.tally_idx](task.filter_idx, task.score_idx) += @@ -676,7 +657,6 @@ void FlatSourceDomain::random_ray_tally() void FlatSourceDomain::all_reduce_replicated_source_regions() { #ifdef OPENMC_MPI - // If we only have 1 MPI rank, no need // to reduce anything. if (mpi::n_procs <= 1) @@ -684,80 +664,15 @@ void FlatSourceDomain::all_reduce_replicated_source_regions() simulation::time_bank_sendrecv.start(); - // The "position_recorded" variable needs to be allreduced (and maxed), - // as whether or not a cell was hit will affect some decisions in how the - // source is calculated in the next iteration so as to avoid dividing - // by zero. We take the max rather than the sum as the hit values are - // expected to be zero or 1. - MPI_Allreduce(MPI_IN_PLACE, position_recorded_.data(), n_source_regions_, - MPI_INT, MPI_MAX, mpi::intracomm); - - // The position variable is more complicated to reduce than the others, - // as we do not want the sum of all positions in each cell, rather, we - // want to just pick any single valid position. Thus, we perform a gather - // and then pick the first valid position we find for all source regions - // that have had a position recorded. This operation does not need to - // be broadcast back to other ranks, as this value is only used for the - // tally conversion operation, which is only performed on the master rank. - // While this is expensive, it only needs to be done for active batches, - // and only if we have not mapped all the tallies yet. Once tallies are - // fully mapped, then the position vector is fully populated, so this - // operation can be skipped. - // First, we broadcast the fully mapped tally status variable so that // all ranks are on the same page int mapped_all_tallies_i = static_cast(mapped_all_tallies_); MPI_Bcast(&mapped_all_tallies_i, 1, MPI_INT, 0, mpi::intracomm); - // Then, we perform the gather of position data, if needed - if (simulation::current_batch > settings::n_inactive && - !mapped_all_tallies_i) { - - // Master rank will gather results and pick valid positions - if (mpi::master) { - // Initialize temporary vector for receiving positions - vector> all_position; - all_position.resize(mpi::n_procs); - for (int i = 0; i < mpi::n_procs; i++) { - all_position[i].resize(n_source_regions_); - } - - // Copy master rank data into gathered vector for convenience - all_position[0] = position_; + bool reduce_position = + simulation::current_batch > settings::n_inactive && !mapped_all_tallies_i; - // Receive all data into gather vector - for (int i = 1; i < mpi::n_procs; i++) { - MPI_Recv(all_position[i].data(), n_source_regions_ * 3, MPI_DOUBLE, i, - 0, mpi::intracomm, MPI_STATUS_IGNORE); - } - - // Scan through gathered data and pick first valid cell posiiton - for (int sr = 0; sr < n_source_regions_; sr++) { - if (position_recorded_[sr] == 1) { - for (int i = 0; i < mpi::n_procs; i++) { - if (all_position[i][sr].x != 0.0 || all_position[i][sr].y != 0.0 || - all_position[i][sr].z != 0.0) { - position_[sr] = all_position[i][sr]; - break; - } - } - } - } - } else { - // Other ranks just send in their data - MPI_Send(position_.data(), n_source_regions_ * 3, MPI_DOUBLE, 0, 0, - mpi::intracomm); - } - } - - // For the rest of the source region data, we simply perform an all reduce, - // as these values will be needed on all ranks for transport during the - // next iteration. - MPI_Allreduce(MPI_IN_PLACE, volume_.data(), n_source_regions_, MPI_DOUBLE, - MPI_SUM, mpi::intracomm); - - MPI_Allreduce(MPI_IN_PLACE, scalar_flux_new_.data(), n_source_elements_, - MPI_DOUBLE, MPI_SUM, mpi::intracomm); + source_regions_.mpi_sync_ranks(reduce_position); simulation::time_bank_sendrecv.stop(); #endif @@ -766,7 +681,7 @@ void FlatSourceDomain::all_reduce_replicated_source_regions() double FlatSourceDomain::evaluate_flux_at_point( Position r, int64_t sr, int g) const { - return scalar_flux_final_[sr * negroups_ + g] / + return source_regions_.scalar_flux_final(sr, g) / (settings::n_batches - settings::n_inactive); } @@ -894,7 +809,7 @@ void FlatSourceDomain::output_to_vtk() const std::fprintf(plot, "SCALARS Materials int\n"); std::fprintf(plot, "LOOKUP_TABLE default\n"); for (int fsr : voxel_indices) { - int mat = material_[fsr]; + int mat = source_regions_.material(fsr); mat = convert_to_big_endian(mat); std::fwrite(&mat, sizeof(int), 1, plot); } @@ -906,7 +821,7 @@ void FlatSourceDomain::output_to_vtk() const int64_t fsr = voxel_indices[i]; float total_fission = 0.0; - int mat = material_[fsr]; + int mat = source_regions_.material(fsr); for (int g = 0; g < negroups_; g++) { int64_t source_element = fsr * negroups_ + g; float flux = evaluate_flux_at_point(voxel_positions[i], fsr, g); @@ -922,16 +837,16 @@ void FlatSourceDomain::output_to_vtk() const } void FlatSourceDomain::apply_external_source_to_source_region( - Discrete* discrete, double strength_factor, int64_t source_region) + Discrete* discrete, double strength_factor, int64_t sr) { - external_source_present_[source_region] = true; + source_regions_.external_source_present(sr) = 1; const auto& discrete_energies = discrete->x(); const auto& discrete_probs = discrete->prob(); for (int i = 0; i < discrete_energies.size(); i++) { int g = data::mg.get_group_index(discrete_energies[i]); - external_source_[source_region * negroups_ + g] += + source_regions_.external_source(sr, g) += discrete_probs[i] * strength_factor; } } @@ -983,8 +898,8 @@ void FlatSourceDomain::count_external_source_regions() { n_external_source_regions_ = 0; #pragma omp parallel for reduction(+ : n_external_source_regions_) - for (int sr = 0; sr < n_source_regions_; sr++) { - if (external_source_present_[sr]) { + for (int64_t sr = 0; sr < n_source_regions_; sr++) { + if (source_regions_.external_source_present(sr)) { n_external_source_regions_++; } } @@ -1029,18 +944,17 @@ void FlatSourceDomain::convert_external_sources() // Divide the fixed source term by sigma t (to save time when applying each // iteration) #pragma omp parallel for - for (int sr = 0; sr < n_source_regions_; sr++) { - int material = material_[sr]; + for (int64_t sr = 0; sr < n_source_regions_; sr++) { for (int g = 0; g < negroups_; g++) { - double sigma_t = sigma_t_[material * negroups_ + g]; - external_source_[sr * negroups_ + g] /= sigma_t; + double sigma_t = sigma_t_[source_regions_.material(sr) * negroups_ + g]; + source_regions_.external_source(sr, g) /= sigma_t; } } } void FlatSourceDomain::flux_swap() { - scalar_flux_old_.swap(scalar_flux_new_); + source_regions_.flux_swap(); } void FlatSourceDomain::flatten_xs() @@ -1096,17 +1010,16 @@ void FlatSourceDomain::set_adjoint_sources(const vector& forward_flux) // so we must convert it to a "per batch" quantity #pragma omp parallel for for (int64_t se = 0; se < n_source_elements_; se++) { - external_source_[se] = 1.0 / forward_flux[se]; + source_regions_.external_source(se) = 1.0 / forward_flux[se]; } // Divide the fixed source term by sigma t (to save time when applying each // iteration) #pragma omp parallel for - for (int sr = 0; sr < n_source_regions_; sr++) { - int material = material_[sr]; + for (int64_t sr = 0; sr < n_source_regions_; sr++) { for (int g = 0; g < negroups_; g++) { - double sigma_t = sigma_t_[material * negroups_ + g]; - external_source_[sr * negroups_ + g] /= sigma_t; + double sigma_t = sigma_t_[source_regions_.material(sr) * negroups_ + g]; + source_regions_.external_source(sr, g) /= sigma_t; } } } @@ -1129,4 +1042,15 @@ void FlatSourceDomain::transpose_scattering_matrix() } } +void FlatSourceDomain::serialize_final_fluxes(vector& flux) +{ + // Ensure array is correct size + flux.resize(n_source_regions_ * negroups_); +// Serialize the final fluxes for output +#pragma omp parallel for + for (int64_t se = 0; se < n_source_elements_; se++) { + flux[se] = source_regions_.scalar_flux_final(se); + } +} + } // namespace openmc diff --git a/src/random_ray/linear_source_domain.cpp b/src/random_ray/linear_source_domain.cpp index f3f6fa0687d..fb6502a3ef1 100644 --- a/src/random_ray/linear_source_domain.cpp +++ b/src/random_ray/linear_source_domain.cpp @@ -20,33 +20,17 @@ namespace openmc { // LinearSourceDomain implementation //============================================================================== -LinearSourceDomain::LinearSourceDomain() : FlatSourceDomain() -{ - // First order spatial moment of the scalar flux - flux_moments_old_.assign(n_source_elements_, {0.0, 0.0, 0.0}); - flux_moments_new_.assign(n_source_elements_, {0.0, 0.0, 0.0}); - flux_moments_t_.assign(n_source_elements_, {0.0, 0.0, 0.0}); - // Source gradients given by M inverse multiplied by source moments - source_gradients_.assign(n_source_elements_, {0.0, 0.0, 0.0}); - - centroid_.assign(n_source_regions_, {0.0, 0.0, 0.0}); - centroid_iteration_.assign(n_source_regions_, {0.0, 0.0, 0.0}); - centroid_t_.assign(n_source_regions_, {0.0, 0.0, 0.0}); - mom_matrix_.assign(n_source_regions_, {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}); - mom_matrix_t_.assign(n_source_regions_, {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}); -} - void LinearSourceDomain::batch_reset() { FlatSourceDomain::batch_reset(); #pragma omp parallel for - for (int64_t se = 0; se < n_source_elements_; se++) { - flux_moments_new_[se] = {0.0, 0.0, 0.0}; + for (int64_t sr = 0; sr < n_source_regions_; sr++) { + source_regions_.centroid_iteration(sr) = {0.0, 0.0, 0.0}; + source_regions_.mom_matrix(sr) = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; } #pragma omp parallel for - for (int64_t sr = 0; sr < n_source_regions_; sr++) { - centroid_iteration_[sr] = {0.0, 0.0, 0.0}; - mom_matrix_[sr] = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; + for (int64_t se = 0; se < n_source_elements_; se++) { + source_regions_.flux_moments_new(se) = {0.0, 0.0, 0.0}; } } @@ -57,10 +41,9 @@ void LinearSourceDomain::update_neutron_source(double k_eff) double inverse_k_eff = 1.0 / k_eff; #pragma omp parallel for - for (int sr = 0; sr < n_source_regions_; sr++) { - - int material = material_[sr]; - MomentMatrix invM = mom_matrix_[sr].inverse(); + for (int64_t sr = 0; sr < n_source_regions_; sr++) { + int material = source_regions_.material(sr); + MomentMatrix invM = source_regions_.mom_matrix(sr).inverse(); for (int g_out = 0; g_out < negroups_; g_out++) { double sigma_t = sigma_t_[material * negroups_ + g_out]; @@ -72,8 +55,8 @@ void LinearSourceDomain::update_neutron_source(double k_eff) for (int g_in = 0; g_in < negroups_; g_in++) { // Handles for the flat and linear components of the flux - double flux_flat = scalar_flux_old_[sr * negroups_ + g_in]; - MomentArray flux_linear = flux_moments_old_[sr * negroups_ + g_in]; + double flux_flat = source_regions_.scalar_flux_old(sr, g_in); + MomentArray flux_linear = source_regions_.flux_moments_old(sr, g_in); // Handles for cross sections double sigma_s = @@ -89,7 +72,7 @@ void LinearSourceDomain::update_neutron_source(double k_eff) } // Compute the flat source term - source_[sr * negroups_ + g_out] = + source_regions_.source(sr, g_out) = (scatter_flat + fission_flat * inverse_k_eff) / sigma_t; // Compute the linear source terms @@ -97,7 +80,7 @@ void LinearSourceDomain::update_neutron_source(double k_eff) // are not well known, we will leave the source gradients as zero // so as to avoid causing any numerical instability. if (simulation::current_batch > 10) { - source_gradients_[sr * negroups_ + g_out] = + source_regions_.source_gradients(sr, g_out) = invM * ((scatter_linear + fission_linear * inverse_k_eff) / sigma_t); } } @@ -107,7 +90,7 @@ void LinearSourceDomain::update_neutron_source(double k_eff) // Add external source to flat source term if in fixed source mode #pragma omp parallel for for (int64_t se = 0; se < n_source_elements_; se++) { - source_[se] += external_source_[se]; + source_regions_.source(se) += source_regions_.external_source(se); } } @@ -123,48 +106,44 @@ void LinearSourceDomain::normalize_scalar_flux_and_volumes( // Normalize flux to total distance travelled by all rays this iteration #pragma omp parallel for - for (int64_t se = 0; se < scalar_flux_new_.size(); se++) { - scalar_flux_new_[se] *= normalization_factor; - flux_moments_new_[se] *= normalization_factor; + for (int64_t se = 0; se < n_source_elements_; se++) { + source_regions_.scalar_flux_new(se) *= normalization_factor; + source_regions_.flux_moments_new(se) *= normalization_factor; } // Accumulate cell-wise ray length tallies collected this iteration, then // update the simulation-averaged cell-wise volume estimates #pragma omp parallel for for (int64_t sr = 0; sr < n_source_regions_; sr++) { - centroid_t_[sr] += centroid_iteration_[sr]; - mom_matrix_t_[sr] += mom_matrix_[sr]; - volume_t_[sr] += volume_[sr]; - volume_naive_[sr] = volume_[sr] * normalization_factor; - volume_[sr] = volume_t_[sr] * volume_normalization_factor; - if (volume_t_[sr] > 0.0) { - double inv_volume = 1.0 / volume_t_[sr]; - centroid_[sr] = centroid_t_[sr]; - centroid_[sr] *= inv_volume; - mom_matrix_[sr] = mom_matrix_t_[sr]; - mom_matrix_[sr] *= inv_volume; + source_regions_.centroid_t(sr) += source_regions_.centroid_iteration(sr); + source_regions_.mom_matrix_t(sr) += source_regions_.mom_matrix(sr); + source_regions_.volume_t(sr) += source_regions_.volume(sr); + source_regions_.volume_naive(sr) = + source_regions_.volume(sr) * normalization_factor; + source_regions_.volume(sr) = + source_regions_.volume_t(sr) * volume_normalization_factor; + if (source_regions_.volume_t(sr) > 0.0) { + double inv_volume = 1.0 / source_regions_.volume_t(sr); + source_regions_.centroid(sr) = source_regions_.centroid_t(sr); + source_regions_.centroid(sr) *= inv_volume; + source_regions_.mom_matrix(sr) = source_regions_.mom_matrix_t(sr); + source_regions_.mom_matrix(sr) *= inv_volume; } } } void LinearSourceDomain::set_flux_to_flux_plus_source( - int64_t idx, double volume, int material, int g) + int64_t sr, double volume, int g) { - scalar_flux_new_[idx] /= volume; - scalar_flux_new_[idx] += source_[idx]; - flux_moments_new_[idx] *= (1.0 / volume); + source_regions_.scalar_flux_new(sr, g) /= volume; + source_regions_.scalar_flux_new(sr, g) += source_regions_.source(sr, g); + source_regions_.flux_moments_new(sr, g) *= (1.0 / volume); } -void LinearSourceDomain::set_flux_to_old_flux(int64_t idx) +void LinearSourceDomain::set_flux_to_old_flux(int64_t sr, int g) { - scalar_flux_new_[idx] = scalar_flux_old_[idx]; - flux_moments_new_[idx] = flux_moments_old_[idx]; -} - -void LinearSourceDomain::flux_swap() -{ - FlatSourceDomain::flux_swap(); - flux_moments_old_.swap(flux_moments_new_); + source_regions_.scalar_flux_new(g) = source_regions_.scalar_flux_old(g); + source_regions_.flux_moments_new(g) = source_regions_.flux_moments_old(g); } void LinearSourceDomain::accumulate_iteration_flux() @@ -175,48 +154,20 @@ void LinearSourceDomain::accumulate_iteration_flux() // Accumulate scalar flux moments #pragma omp parallel for for (int64_t se = 0; se < n_source_elements_; se++) { - flux_moments_t_[se] += flux_moments_new_[se]; + source_regions_.flux_moments_t(se) += source_regions_.flux_moments_new(se); } } -void LinearSourceDomain::all_reduce_replicated_source_regions() -{ -#ifdef OPENMC_MPI - FlatSourceDomain::all_reduce_replicated_source_regions(); - simulation::time_bank_sendrecv.start(); - - // We are going to assume we can safely cast Position, MomentArray, - // and MomentMatrix to contiguous arrays of doubles for the MPI - // allreduce operation. This is a safe assumption as typically - // compilers will at most pad to 8 byte boundaries. If a new FP32 MomentArray - // type is introduced, then there will likely be padding, in which case this - // function will need to become more complex. - if (sizeof(MomentArray) != 3 * sizeof(double) || - sizeof(MomentMatrix) != 6 * sizeof(double)) { - fatal_error("Unexpected buffer padding in linear source domain reduction."); - } - - MPI_Allreduce(MPI_IN_PLACE, static_cast(flux_moments_new_.data()), - n_source_elements_ * 3, MPI_DOUBLE, MPI_SUM, mpi::intracomm); - MPI_Allreduce(MPI_IN_PLACE, static_cast(mom_matrix_.data()), - n_source_regions_ * 6, MPI_DOUBLE, MPI_SUM, mpi::intracomm); - MPI_Allreduce(MPI_IN_PLACE, static_cast(centroid_iteration_.data()), - n_source_regions_ * 3, MPI_DOUBLE, MPI_SUM, mpi::intracomm); - - simulation::time_bank_sendrecv.stop(); -#endif -} - double LinearSourceDomain::evaluate_flux_at_point( Position r, int64_t sr, int g) const { double phi_flat = FlatSourceDomain::evaluate_flux_at_point(r, sr, g); - Position local_r = r - centroid_[sr]; - MomentArray phi_linear = flux_moments_t_[sr * negroups_ + g]; + Position local_r = r - source_regions_.centroid(sr); + MomentArray phi_linear = source_regions_.flux_moments_t(sr, g); phi_linear *= 1.0 / (settings::n_batches - settings::n_inactive); - MomentMatrix invM = mom_matrix_[sr].inverse(); + MomentMatrix invM = source_regions_.mom_matrix(sr).inverse(); MomentArray phi_solved = invM * phi_linear; return phi_flat + phi_solved.dot(local_r); diff --git a/src/random_ray/random_ray.cpp b/src/random_ray/random_ray.cpp index 7a359f35664..9c2a890ec5a 100644 --- a/src/random_ray/random_ray.cpp +++ b/src/random_ray/random_ray.cpp @@ -309,11 +309,9 @@ void RandomRay::attenuate_flux_flat_source(double distance, bool is_active) int i_cell = lowest_coord().cell; // The source region is the spatial region index - int64_t source_region = - domain_->source_region_offsets_[i_cell] + cell_instance(); + int64_t sr = domain_->source_region_offsets_[i_cell] + cell_instance(); // The source element is the energy-specific region index - int64_t source_element = source_region * negroups_; int material = this->material(); // MOC incoming flux attenuation + source contribution/attenuation equation @@ -322,7 +320,7 @@ void RandomRay::attenuate_flux_flat_source(double distance, bool is_active) float tau = sigma_t * distance; float exponential = cjosey_exponential(tau); // exponential = 1 - exp(-tau) float new_delta_psi = - (angular_flux_[g] - domain_->source_[source_element + g]) * exponential; + (angular_flux_[g] - domain_->source_regions_.source(sr, g)) * exponential; delta_psi_[g] = new_delta_psi; angular_flux_[g] -= new_delta_psi; } @@ -332,28 +330,28 @@ void RandomRay::attenuate_flux_flat_source(double distance, bool is_active) if (is_active) { // Aquire lock for source region - domain_->lock_[source_region].lock(); + domain_->source_regions_.lock(sr).lock(); // Accumulate delta psi into new estimate of source region flux for // this iteration for (int g = 0; g < negroups_; g++) { - domain_->scalar_flux_new_[source_element + g] += delta_psi_[g]; + domain_->source_regions_.scalar_flux_new(sr, g) += delta_psi_[g]; } // Accomulate volume (ray distance) into this iteration's estimate // of the source region's volume - domain_->volume_[source_region] += distance; + domain_->source_regions_.volume(sr) += distance; // Tally valid position inside the source region (e.g., midpoint of // the ray) if not done already - if (!domain_->position_recorded_[source_region]) { + if (!domain_->source_regions_.position_recorded(sr)) { Position midpoint = r() + u() * (distance / 2.0); - domain_->position_[source_region] = midpoint; - domain_->position_recorded_[source_region] = 1; + domain_->source_regions_.position(sr) = midpoint; + domain_->source_regions_.position_recorded(sr) = 1; } // Release lock - domain_->lock_[source_region].unlock(); + domain_->source_regions_.lock(sr).unlock(); } } @@ -373,14 +371,12 @@ void RandomRay::attenuate_flux_linear_source(double distance, bool is_active) int i_cell = lowest_coord().cell; // The source region is the spatial region index - int64_t source_region = - domain_->source_region_offsets_[i_cell] + cell_instance(); + int64_t sr = domain_->source_region_offsets_[i_cell] + cell_instance(); // The source element is the energy-specific region index - int64_t source_element = source_region * negroups_; int material = this->material(); - Position& centroid = domain->centroid_[source_region]; + Position& centroid = domain_->source_regions_.centroid(sr); Position midpoint = r() + u() * (distance / 2.0); // Determine the local position of the midpoint and the ray origin @@ -393,7 +389,7 @@ void RandomRay::attenuate_flux_linear_source(double distance, bool is_active) // be no estimate of its centroid. We detect this by checking if it has // any accumulated volume. If its volume is zero, just use the midpoint // of the ray as the region's centroid. - if (domain->volume_t_[source_region]) { + if (domain_->source_regions_.volume_t(sr)) { rm_local = midpoint - centroid; r0_local = r() - centroid; } else { @@ -420,9 +416,10 @@ void RandomRay::attenuate_flux_linear_source(double distance, bool is_active) // calculated from the source gradients dot product with local centroid // and direction, respectively. float spatial_source = - domain_->source_[source_element + g] + - rm_local.dot(domain->source_gradients_[source_element + g]); - float dir_source = u().dot(domain->source_gradients_[source_element + g]); + domain_->source_regions_.source(sr, g) + + rm_local.dot(domain_->source_regions_.source_gradients(sr, g)); + float dir_source = + u().dot(domain_->source_regions_.source_gradients(sr, g)); float gn = exponentialG(tau); float f1 = 1.0f - tau * gn; @@ -465,33 +462,33 @@ void RandomRay::attenuate_flux_linear_source(double distance, bool is_active) rm_local, u(), distance); // Aquire lock for source region - domain_->lock_[source_region].lock(); + domain_->source_regions_.lock(sr).lock(); // Accumulate deltas into the new estimate of source region flux for this // iteration for (int g = 0; g < negroups_; g++) { - domain_->scalar_flux_new_[source_element + g] += delta_psi_[g]; - domain->flux_moments_new_[source_element + g] += delta_moments_[g]; + domain_->source_regions_.scalar_flux_new(sr, g) += delta_psi_[g]; + domain_->source_regions_.flux_moments_new(sr, g) += delta_moments_[g]; } // Accumulate the volume (ray segment distance), centroid, and spatial // momement estimates into the running totals for the iteration for this // source region. The centroid and spatial momements estimates are scaled by // the ray segment length as part of length averaging of the estimates. - domain_->volume_[source_region] += distance; - domain->centroid_iteration_[source_region] += midpoint * distance; + domain_->source_regions_.volume(sr) += distance; + domain_->source_regions_.centroid_iteration(sr) += midpoint * distance; moment_matrix_estimate *= distance; - domain->mom_matrix_[source_region] += moment_matrix_estimate; + domain_->source_regions_.mom_matrix(sr) += moment_matrix_estimate; // Tally valid position inside the source region (e.g., midpoint of // the ray) if not done already - if (!domain_->position_recorded_[source_region]) { - domain_->position_[source_region] = midpoint; - domain_->position_recorded_[source_region] = 1; + if (!domain_->source_regions_.position_recorded(sr)) { + domain_->source_regions_.position(sr) = midpoint; + domain_->source_regions_.position_recorded(sr) = 1; } // Release lock - domain_->lock_[source_region].unlock(); + domain_->source_regions_.lock(sr).unlock(); } } @@ -537,11 +534,10 @@ void RandomRay::initialize_ray(uint64_t ray_id, FlatSourceDomain* domain) // Initialize ray's starting angular flux to starting location's isotropic // source int i_cell = lowest_coord().cell; - int64_t source_region_idx = - domain_->source_region_offsets_[i_cell] + cell_instance(); + int64_t sr = domain_->source_region_offsets_[i_cell] + cell_instance(); for (int g = 0; g < negroups_; g++) { - angular_flux_[g] = domain_->source_[source_region_idx * negroups_ + g]; + angular_flux_[g] = domain_->source_regions_.source(sr, g); } } diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index a8e6d1b8229..c4f719b2385 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -66,7 +66,7 @@ void openmc_run_random_ray() simulation::time_total.stop(); // Normalize and save the final forward flux - forward_flux = sim.domain()->scalar_flux_final_; + sim.domain()->serialize_final_fluxes(forward_flux); double source_normalization_factor = sim.domain()->compute_fixed_source_normalization_factor() / diff --git a/src/random_ray/source_region.cpp b/src/random_ray/source_region.cpp new file mode 100644 index 00000000000..0a20af8a976 --- /dev/null +++ b/src/random_ray/source_region.cpp @@ -0,0 +1,236 @@ +#include "openmc/random_ray/source_region.h" + +#include "openmc/error.h" +#include "openmc/message_passing.h" + +namespace openmc { + +//============================================================================== +// SourceRegion implementation +//============================================================================== +SourceRegion::SourceRegion(int negroups, bool is_linear) +{ + if (settings::run_mode == RunMode::EIGENVALUE) { + // If in eigenvalue mode, set starting flux to guess of 1 + scalar_flux_old_.assign(negroups, 1.0); + } else { + // If in fixed source mode, set starting flux to guess of zero + // and initialize external source arrays + scalar_flux_old_.assign(negroups, 0.0); + external_source_.assign(negroups, 0.0); + } + + scalar_flux_new_.assign(negroups, 0.0); + source_.resize(negroups); + scalar_flux_final_.assign(negroups, 0.0); + + tally_task_.resize(negroups); + + if (is_linear) { + source_gradients_.resize(negroups); + flux_moments_old_.resize(negroups); + flux_moments_new_.resize(negroups); + flux_moments_t_.resize(negroups); + } +} + +//============================================================================== +// SourceRegionContainer implementation +//============================================================================== +void SourceRegionContainer::push_back(const SourceRegion& sr) +{ + n_source_regions_++; + + // Scalar fields + material_.push_back(sr.material_); + lock_.push_back(sr.lock_); + volume_.push_back(sr.volume_); + volume_t_.push_back(sr.volume_t_); + volume_naive_.push_back(sr.volume_naive_); + position_recorded_.push_back(sr.position_recorded_); + external_source_present_.push_back(sr.external_source_present_); + position_.push_back(sr.position_); + volume_task_.push_back(sr.volume_task_); + + // Only store these fields if is_linear_ is true + if (is_linear_) { + centroid_.push_back(sr.centroid_); + centroid_iteration_.push_back(sr.centroid_iteration_); + centroid_t_.push_back(sr.centroid_t_); + mom_matrix_.push_back(sr.mom_matrix_); + mom_matrix_t_.push_back(sr.mom_matrix_t_); + } + + // Energy-dependent fields + for (int g = 0; g < negroups_; ++g) { + scalar_flux_old_.push_back(sr.scalar_flux_old_[g]); + scalar_flux_new_.push_back(sr.scalar_flux_new_[g]); + scalar_flux_final_.push_back(sr.scalar_flux_final_[g]); + source_.push_back(sr.source_[g]); + if (settings::run_mode == RunMode::FIXED_SOURCE) { + external_source_.push_back(sr.external_source_[g]); + } + + // Only store these fields if is_linear_ is true + if (is_linear_) { + source_gradients_.push_back(sr.source_gradients_[g]); + flux_moments_old_.push_back(sr.flux_moments_old_[g]); + flux_moments_new_.push_back(sr.flux_moments_new_[g]); + flux_moments_t_.push_back(sr.flux_moments_t_[g]); + } + + // Tally tasks + tally_task_.emplace_back(sr.tally_task_[g]); + } +} + +void SourceRegionContainer::assign( + int n_source_regions, const SourceRegion& source_region) +{ + // Clear existing data + n_source_regions_ = 0; + material_.clear(); + lock_.clear(); + volume_.clear(); + volume_t_.clear(); + volume_naive_.clear(); + position_recorded_.clear(); + external_source_present_.clear(); + position_.clear(); + + if (is_linear_) { + centroid_.clear(); + centroid_iteration_.clear(); + centroid_t_.clear(); + mom_matrix_.clear(); + mom_matrix_t_.clear(); + } + + scalar_flux_old_.clear(); + scalar_flux_new_.clear(); + scalar_flux_final_.clear(); + source_.clear(); + external_source_.clear(); + + if (is_linear_) { + source_gradients_.clear(); + flux_moments_old_.clear(); + flux_moments_new_.clear(); + flux_moments_t_.clear(); + } + + tally_task_.clear(); + volume_task_.clear(); + + // Fill with copies of source_region + for (int i = 0; i < n_source_regions; ++i) { + push_back(source_region); + } +} + +void SourceRegionContainer::flux_swap() +{ + scalar_flux_old_.swap(scalar_flux_new_); + if (is_linear_) { + flux_moments_old_.swap(flux_moments_new_); + } +} + +void SourceRegionContainer::mpi_sync_ranks(bool reduce_position) +{ +#ifdef OPENMC_MPI + + // The "position_recorded" variable needs to be allreduced (and maxed), + // as whether or not a cell was hit will affect some decisions in how the + // source is calculated in the next iteration so as to avoid dividing + // by zero. We take the max rather than the sum as the hit values are + // expected to be zero or 1. + MPI_Allreduce(MPI_IN_PLACE, position_recorded_.data(), 1, MPI_INT, MPI_MAX, + mpi::intracomm); + + // The position variable is more complicated to reduce than the others, + // as we do not want the sum of all positions in each cell, rather, we + // want to just pick any single valid position. Thus, we perform a gather + // and then pick the first valid position we find for all source regions + // that have had a position recorded. This operation does not need to + // be broadcast back to other ranks, as this value is only used for the + // tally conversion operation, which is only performed on the master rank. + // While this is expensive, it only needs to be done for active batches, + // and only if we have not mapped all the tallies yet. Once tallies are + // fully mapped, then the position vector is fully populated, so this + // operation can be skipped. + + // Then, we perform the gather of position data, if needed + if (reduce_position) { + + // Master rank will gather results and pick valid positions + if (mpi::master) { + // Initialize temporary vector for receiving positions + vector> all_position; + all_position.resize(mpi::n_procs); + for (int i = 0; i < mpi::n_procs; i++) { + all_position[i].resize(n_source_regions_); + } + + // Copy master rank data into gathered vector for convenience + all_position[0] = position_; + + // Receive all data into gather vector + for (int i = 1; i < mpi::n_procs; i++) { + MPI_Recv(all_position[i].data(), n_source_regions_ * 3, MPI_DOUBLE, i, + 0, mpi::intracomm, MPI_STATUS_IGNORE); + } + + // Scan through gathered data and pick first valid cell posiiton + for (int64_t sr = 0; sr < n_source_regions_; sr++) { + if (position_recorded_[sr] == 1) { + for (int i = 0; i < mpi::n_procs; i++) { + if (all_position[i][sr].x != 0.0 || all_position[i][sr].y != 0.0 || + all_position[i][sr].z != 0.0) { + position_[sr] = all_position[i][sr]; + break; + } + } + } + } + } else { + // Other ranks just send in their data + MPI_Send(position_.data(), n_source_regions_ * 3, MPI_DOUBLE, 0, 0, + mpi::intracomm); + } + } + + // For the rest of the source region data, we simply perform an all reduce, + // as these values will be needed on all ranks for transport during the + // next iteration. + MPI_Allreduce(MPI_IN_PLACE, volume_.data(), n_source_regions_, MPI_DOUBLE, + MPI_SUM, mpi::intracomm); + + MPI_Allreduce(MPI_IN_PLACE, scalar_flux_new_.data(), + n_source_regions_ * negroups_, MPI_DOUBLE, MPI_SUM, mpi::intracomm); + + if (is_linear_) { + // We are going to assume we can safely cast Position, MomentArray, + // and MomentMatrix to contiguous arrays of doubles for the MPI + // allreduce operation. This is a safe assumption as typically + // compilers will at most pad to 8 byte boundaries. If a new FP32 + // MomentArray type is introduced, then there will likely be padding, in + // which case this function will need to become more complex. + if (sizeof(MomentArray) != 3 * sizeof(double) || + sizeof(MomentMatrix) != 6 * sizeof(double)) { + fatal_error( + "Unexpected buffer padding in linear source domain reduction."); + } + + MPI_Allreduce(MPI_IN_PLACE, static_cast(flux_moments_new_.data()), + n_source_regions_ * negroups_ * 3, MPI_DOUBLE, MPI_SUM, mpi::intracomm); + MPI_Allreduce(MPI_IN_PLACE, static_cast(mom_matrix_.data()), + n_source_regions_ * 6, MPI_DOUBLE, MPI_SUM, mpi::intracomm); + MPI_Allreduce(MPI_IN_PLACE, static_cast(centroid_iteration_.data()), + n_source_regions_ * 3, MPI_DOUBLE, MPI_SUM, mpi::intracomm); + } + +#endif +} + +} // namespace openmc \ No newline at end of file From e9ddf885e79aed1ade482d2d0fc5a170cfb30a22 Mon Sep 17 00:00:00 2001 From: Jose Ignacio Marquez Damian <22483345+marquezj@users.noreply.github.com> Date: Wed, 12 Feb 2025 01:12:52 +0100 Subject: [PATCH 142/184] Correct normalization of thermal elastic in non standard ENDF-6 files (#3234) Co-authored-by: Paul Romano --- openmc/data/thermal.py | 137 +++++++++++++++----------- tests/unit_tests/test_data_thermal.py | 6 +- 2 files changed, 85 insertions(+), 58 deletions(-) diff --git a/openmc/data/thermal.py b/openmc/data/thermal.py index df4dc150354..4ecc7c040e3 100644 --- a/openmc/data/thermal.py +++ b/openmc/data/thermal.py @@ -5,7 +5,6 @@ from io import StringIO import itertools import os -import re import tempfile from warnings import warn @@ -871,7 +870,7 @@ def from_ace(cls, ace_or_filename, name=None): @classmethod def from_njoy(cls, filename, filename_thermal, temperatures=None, evaluation=None, evaluation_thermal=None, - use_endf_data=True, **kwargs): + use_endf_data=True, divide_incoherent_elastic=False, **kwargs): """Generate thermal scattering data by running NJOY. Parameters @@ -894,8 +893,13 @@ def from_njoy(cls, filename, filename_thermal, temperatures=None, use_endf_data : bool If the material has incoherent elastic scattering, the ENDF data will be used rather than the ACE data. + divide_incoherent_elastic : bool + Divide incoherent elastic cross section by number of principal + atoms. This is not part of the ENDF-6 standard but it is how it is + processed by NJOY. **kwargs - Keyword arguments passed to :func:`openmc.data.njoy.make_ace_thermal` + Keyword arguments passed to + :func:`openmc.data.njoy.make_ace_thermal` Returns ------- @@ -920,7 +924,7 @@ def from_njoy(cls, filename, filename_thermal, temperatures=None, # Load ENDF data to replace incoherent elastic if use_endf_data: - data_endf = cls.from_endf(filename_thermal) + data_endf = cls.from_endf(filename_thermal, divide_incoherent_elastic) if data_endf.elastic is not None: # Get appropriate temperatures if temperatures is None: @@ -938,7 +942,7 @@ def from_njoy(cls, filename, filename_thermal, temperatures=None, return data @classmethod - def from_endf(cls, ev_or_filename): + def from_endf(cls, ev_or_filename, divide_incoherent_elastic=False): """Generate thermal scattering data from an ENDF file Parameters @@ -946,6 +950,10 @@ def from_endf(cls, ev_or_filename): ev_or_filename : openmc.data.endf.Evaluation or str ENDF evaluation to read from. If given as a string, it is assumed to be the filename for the ENDF file. + divide_incoherent_elastic : bool + Divide incoherent elastic cross section by number of principal + atoms. This is not part of the ENDF-6 standard but it is how it is + processed by NJOY. Returns ------- @@ -958,56 +966,6 @@ def from_endf(cls, ev_or_filename): else: ev = endf.Evaluation(ev_or_filename) - # Read coherent/incoherent elastic data - elastic = None - if (7, 2) in ev.section: - # Define helper functions to avoid duplication - def get_coherent_elastic(file_obj): - # Get structure factor at first temperature - params, S = endf.get_tab1_record(file_obj) - strT = _temperature_str(params[0]) - n_temps = params[2] - bragg_edges = S.x - xs = {strT: CoherentElastic(bragg_edges, S.y)} - distribution = {strT: CoherentElasticAE(xs[strT])} - - # Get structure factor for subsequent temperatures - for _ in range(n_temps): - params, S = endf.get_list_record(file_obj) - strT = _temperature_str(params[0]) - xs[strT] = CoherentElastic(bragg_edges, S) - distribution[strT] = CoherentElasticAE(xs[strT]) - return xs, distribution - - def get_incoherent_elastic(file_obj): - params, W = endf.get_tab1_record(file_obj) - bound_xs = params[0] - xs = {} - distribution = {} - for T, debye_waller in zip(W.x, W.y): - strT = _temperature_str(T) - xs[strT] = IncoherentElastic(bound_xs, debye_waller) - distribution[strT] = IncoherentElasticAE(debye_waller) - return xs, distribution - - file_obj = StringIO(ev.section[7, 2]) - lhtr = endf.get_head_record(file_obj)[2] - if lhtr == 1: - # coherent elastic - xs, distribution = get_coherent_elastic(file_obj) - elif lhtr == 2: - # incoherent elastic - xs, distribution = get_incoherent_elastic(file_obj) - elif lhtr == 3: - # mixed coherent / incoherent elastic - xs_c, dist_c = get_coherent_elastic(file_obj) - xs_i, dist_i = get_incoherent_elastic(file_obj) - assert sorted(xs_c) == sorted(xs_i) - xs = {T: Sum([xs_c[T], xs_i[T]]) for T in xs_c} - distribution = {T: MixedElasticAE(dist_c[T], dist_i[T]) for T in dist_c} - - elastic = ThermalScatteringReaction(xs, distribution) - # Read incoherent inelastic data assert (7, 4) in ev.section, 'No MF=7, MT=4 found in thermal scattering' file_obj = StringIO(ev.section[7, 4]) @@ -1022,6 +980,7 @@ def get_incoherent_elastic(file_obj): data['A0'] = awr = B[2] data['e_max'] = energy_max = B[3] data['M0'] = B[5] + free_xs = data['free_atom_xs'] / data['M0'] # Get information about non-principal atoms n_non_principal = params[5] @@ -1066,6 +1025,74 @@ def get_incoherent_elastic(file_obj): _, Teff = endf.get_tab1_record(file_obj) data['effective_temperature'].append(Teff) + # Read coherent/incoherent elastic data + elastic = None + if (7, 2) in ev.section: + # Define helper functions to avoid duplication + def get_coherent_elastic(file_obj): + # Get structure factor at first temperature + params, S = endf.get_tab1_record(file_obj) + strT = _temperature_str(params[0]) + n_temps = params[2] + bragg_edges = S.x + xs = {strT: CoherentElastic(bragg_edges, S.y)} + distribution = {strT: CoherentElasticAE(xs[strT])} + + # Get structure factor for subsequent temperatures + for _ in range(n_temps): + params, S = endf.get_list_record(file_obj) + strT = _temperature_str(params[0]) + xs[strT] = CoherentElastic(bragg_edges, S) + distribution[strT] = CoherentElasticAE(xs[strT]) + return xs, distribution + + def get_incoherent_elastic(file_obj, natom): + params, W = endf.get_tab1_record(file_obj) + bound_xs = params[0]/natom + + # Check whether divide_incoherent_elastic was applied correctly + if abs(free_xs - bound_xs/(1 + 1/data['A0'])**2) > 0.5: + if divide_incoherent_elastic: + msg = ( + 'Thermal scattering evaluation follows ENDF-6 ' + 'definition of bound cross section but ' + 'divide_incoherent_elastic=True.' + ) + else: + msg = ( + 'Thermal scattering evaluation follows NJOY ' + 'definition of bound cross section but ' + 'divide_incoherent_elastic=False.' + ) + warn(msg) + + xs = {} + distribution = {} + for T, debye_waller in zip(W.x, W.y): + strT = _temperature_str(T) + xs[strT] = IncoherentElastic(bound_xs, debye_waller) + distribution[strT] = IncoherentElasticAE(debye_waller) + return xs, distribution + + file_obj = StringIO(ev.section[7, 2]) + lhtr = endf.get_head_record(file_obj)[2] + natom = data['M0'] if divide_incoherent_elastic else 1 + if lhtr == 1: + # coherent elastic + xs, distribution = get_coherent_elastic(file_obj) + elif lhtr == 2: + # incoherent elastic + xs, distribution = get_incoherent_elastic(file_obj, natom) + elif lhtr == 3: + # mixed coherent / incoherent elastic + xs_c, dist_c = get_coherent_elastic(file_obj) + xs_i, dist_i = get_incoherent_elastic(file_obj, natom) + assert sorted(xs_c) == sorted(xs_i) + xs = {T: Sum([xs_c[T], xs_i[T]]) for T in xs_c} + distribution = {T: MixedElasticAE(dist_c[T], dist_i[T]) for T in dist_c} + + elastic = ThermalScatteringReaction(xs, distribution) + name = ev.target['zsymam'].strip() instance = cls(name, awr, energy_max, kTs) if elastic is not None: diff --git a/tests/unit_tests/test_data_thermal.py b/tests/unit_tests/test_data_thermal.py index 9f5a8cb4046..c9fd9045b73 100644 --- a/tests/unit_tests/test_data_thermal.py +++ b/tests/unit_tests/test_data_thermal.py @@ -41,7 +41,7 @@ def hzrh(): """H in ZrH thermal scattering data.""" endf_data = os.environ['OPENMC_ENDF_DATA'] filename = os.path.join(endf_data, 'thermal_scatt', 'tsl-HinZrH.endf') - return openmc.data.ThermalScattering.from_endf(filename) + return openmc.data.ThermalScattering.from_endf(filename, divide_incoherent_elastic=True) @pytest.fixture(scope='module') @@ -64,7 +64,7 @@ def sio2(): """SiO2 thermal scattering data.""" endf_data = os.environ['OPENMC_ENDF_DATA'] filename = os.path.join(endf_data, 'thermal_scatt', 'tsl-SiO2.endf') - return openmc.data.ThermalScattering.from_endf(filename) + return openmc.data.ThermalScattering.from_endf(filename, divide_incoherent_elastic=True) def test_h2o_attributes(h2o): @@ -144,7 +144,7 @@ def test_continuous_dist(h2o_njoy): def test_h2o_endf(): endf_data = os.environ['OPENMC_ENDF_DATA'] filename = os.path.join(endf_data, 'thermal_scatt', 'tsl-HinH2O.endf') - h2o = openmc.data.ThermalScattering.from_endf(filename) + h2o = openmc.data.ThermalScattering.from_endf(filename, divide_incoherent_elastic=True) assert not h2o.elastic assert h2o.atomic_weight_ratio == pytest.approx(0.99917) assert h2o.energy_max == pytest.approx(3.99993) From 18c3112415aa19947737ffcd70641a194faa98dd Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Wed, 12 Feb 2025 07:03:17 -0600 Subject: [PATCH 143/184] Adding tmate action to CI for debugging (#3138) --- .github/workflows/ci.yml | 20 +++++++++++++++++++- docs/source/devguide/tests.rst | 24 ++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5051b8d0bb..e69a13db906 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,6 +88,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + fetch-depth: 2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 @@ -98,7 +100,17 @@ jobs: run: | echo "OPENMC_CROSS_SECTIONS=$HOME/nndc_hdf5/cross_sections.xml" >> $GITHUB_ENV echo "OPENMC_ENDF_DATA=$HOME/endf-b-vii.1" >> $GITHUB_ENV - + # get the sha of the last branch commit + # for push and workflow_dispatch events, use the current reference head + BRANCH_SHA=HEAD + # for a pull_request event, use the last reference of the parents of the merge commit + if [ "${{ github.event_name }}" == "pull_request" ]; then + BRANCH_SHA=$(git rev-list --parents -n 1 HEAD | rev | cut -d" " -f 1 | rev) + fi + COMMIT_MESSAGE=$(git log $BRANCH_SHA -1 --pretty=%B) + echo ${COMMIT_MESSAGE} + echo "COMMIT_MESSAGE=${COMMIT_MESSAGE}" >> $GITHUB_ENV + - name: Apt dependencies shell: bash run: | @@ -153,6 +165,12 @@ jobs: CTEST_OUTPUT_ON_FAILURE=1 make test -C $GITHUB_WORKSPACE/build/ $GITHUB_WORKSPACE/tools/ci/gha-script.sh + - name: Setup tmate debug session + continue-on-error: true + if: ${{ contains(env.COMMIT_MESSAGE, '[gha-debug]') }} + uses: mxschmitt/action-tmate@v3 + timeout-minutes: 10 + - name: after_success shell: bash run: | diff --git a/docs/source/devguide/tests.rst b/docs/source/devguide/tests.rst index d8fc53b3ad0..3665ad9d406 100644 --- a/docs/source/devguide/tests.rst +++ b/docs/source/devguide/tests.rst @@ -84,6 +84,30 @@ that, consider the following: limit the number of threads that OpenBLAS uses internally; this can be done by setting the :envvar:`OPENBLAS_NUM_THREADS` environment variable to 1. +Debugging Tests in CI +--------------------- + +.. _tmate: `_ + +Tests can be debugged in CI using a feature called `tmate`_. CI debugging can be +enabled by including "[gha-debug]" in the commit message. When the test fails, a +link similar to the one shown below will be provided in the GitHub Actions +output after failure occurs. Logging into the provided link will allow you to +debug the test in the CI environment. The following is an example of the output +shown in the CI log that provides the link to the tmate session: + +.. code-block:: text + :linenos: + + Created new session successfully + ssh 2VcykjU7vNdvAzEjQcc839GM2@nyc1.tmate.io + https://tmate.io/t/2VcykjU7vNdvAzEjQcc839GM2 + Entering main loop + Web shell: https://tmate.io/t/2VcykjU7vNdvAzEjQcc839GM2 + SSH: ssh 2VcykjU7vNdvAzEjQcc839GM2@nyc1.tmate.io + ... + + Generating XML Inputs --------------------- From 78bf4cf145d2477fd0efed51c6506d3dda940d43 Mon Sep 17 00:00:00 2001 From: rherrero-pf <156206440+rherrero-pf@users.noreply.github.com> Date: Thu, 13 Feb 2025 22:26:36 +0100 Subject: [PATCH 144/184] Add VTU export for Unstructured meshes (#3290) Co-authored-by: Paul Romano --- openmc/mesh.py | 85 ++++++++++++++++++++++------------- tests/unit_tests/test_mesh.py | 82 +++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 31 deletions(-) diff --git a/openmc/mesh.py b/openmc/mesh.py index e4d83d81d3d..4c594c9c6c2 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -9,6 +9,7 @@ import h5py import lxml.etree as ET import numpy as np +from pathlib import Path import openmc import openmc.checkvalue as cv @@ -2358,55 +2359,71 @@ def write_vtk_mesh(self, **kwargs): self.write_data_to_vtk(**kwargs) def write_data_to_vtk( - self, - filename: PathLike | None = None, - datasets: dict | None = None, - volume_normalization: bool = True + self, + filename: PathLike | None = None, + datasets: dict | None = None, + volume_normalization: bool = True, ): """Map data to unstructured VTK mesh elements. + If filename is None, then a filename will be generated based on the mesh + ID, and exported to VTK format. + Parameters ---------- filename : str or pathlib.Path - Name of the VTK file to write + Name of the VTK file to write. If the filename ends in '.vtu' then a + binary VTU format file will be written, if the filename ends in + '.vtk' then a legacy VTK file will be written. datasets : dict - Dictionary whose keys are the data labels - and values are numpy appropriately sized arrays - of the data + Dictionary whose keys are the data labels and values are numpy + appropriately sized arrays of the data volume_normalization : bool - Whether or not to normalize the data by the - volume of the mesh elements + Whether or not to normalize the data by the volume of the mesh + elements """ - import vtk - from vtk.util import numpy_support as nps + from vtkmodules.util import numpy_support + from vtkmodules import vtkCommonCore + from vtkmodules import vtkCommonDataModel + from vtkmodules import vtkIOLegacy + from vtkmodules import vtkIOXML if self.connectivity is None or self.vertices is None: - raise RuntimeError('This mesh has not been ' - 'loaded from a statepoint file.') + raise RuntimeError( + "This mesh has not been loaded from a statepoint file." + ) if filename is None: - filename = f'mesh_{self.id}.vtk' + filename = f"mesh_{self.id}.vtk" - writer = vtk.vtkUnstructuredGridWriter() + if Path(filename).suffix == ".vtk": + writer = vtkIOLegacy.vtkUnstructuredGridWriter() + + elif Path(filename).suffix == ".vtu": + writer = vtkIOXML.vtkXMLUnstructuredGridWriter() + writer.SetCompressorTypeToZLib() + writer.SetDataModeToBinary() writer.SetFileName(str(filename)) - grid = vtk.vtkUnstructuredGrid() + grid = vtkCommonDataModel.vtkUnstructuredGrid() - vtk_pnts = vtk.vtkPoints() - vtk_pnts.SetData(nps.numpy_to_vtk(self.vertices)) - grid.SetPoints(vtk_pnts) + points = vtkCommonCore.vtkPoints() + points.SetData(numpy_support.numpy_to_vtk(self.vertices)) + grid.SetPoints(points) n_skipped = 0 for elem_type, conn in zip(self.element_types, self.connectivity): if elem_type == self._LINEAR_TET: - elem = vtk.vtkTetra() + elem = vtkCommonDataModel.vtkTetra() elif elem_type == self._LINEAR_HEX: - elem = vtk.vtkHexahedron() + elem = vtkCommonDataModel.vtkHexahedron() elif elem_type == self._UNSUPPORTED_ELEM: n_skipped += 1 + continue else: - raise RuntimeError(f'Invalid element type {elem_type} found') + raise RuntimeError(f"Invalid element type {elem_type} found") + for i, c in enumerate(conn): if c == -1: break @@ -2415,30 +2432,36 @@ def write_data_to_vtk( grid.InsertNextCell(elem.GetCellType(), elem.GetPointIds()) if n_skipped > 0: - warnings.warn(f'{n_skipped} elements were not written because ' - 'they are not of type linear tet/hex') + warnings.warn( + f"{n_skipped} elements were not written because " + "they are not of type linear tet/hex" + ) # check that datasets are the correct size datasets_out = [] if datasets is not None: for name, data in datasets.items(): if data.shape != self.dimension: - raise ValueError(f'Cannot apply dataset "{name}" with ' - f'shape {data.shape} to mesh {self.id} ' - f'with dimensions {self.dimension}') + raise ValueError( + f'Cannot apply dataset "{name}" with ' + f"shape {data.shape} to mesh {self.id} " + f"with dimensions {self.dimension}" + ) if volume_normalization: for name, data in datasets.items(): if np.issubdtype(data.dtype, np.integer): - warnings.warn(f'Integer data set "{name}" will ' - 'not be volume-normalized.') + warnings.warn( + f'Integer data set "{name}" will ' + "not be volume-normalized." + ) continue data /= self.volumes # add data to the mesh for name, data in datasets.items(): datasets_out.append(data) - arr = vtk.vtkDoubleArray() + arr = vtkCommonCore.vtkDoubleArray() arr.SetName(name) arr.SetNumberOfTuples(data.size) diff --git a/tests/unit_tests/test_mesh.py b/tests/unit_tests/test_mesh.py index 27322165f64..3f307dda820 100644 --- a/tests/unit_tests/test_mesh.py +++ b/tests/unit_tests/test_mesh.py @@ -1,8 +1,11 @@ from math import pi +from pathlib import Path import numpy as np import pytest import openmc +import openmc.lib +from openmc.utility_funcs import change_directory @pytest.mark.parametrize("val_left,val_right", [(0, 0), (-1., -1.), (2.0, 2)]) @@ -398,6 +401,85 @@ def test_umesh_roundtrip(run_in_tmpdir, request): assert umesh.id == xml_mesh.id +@pytest.fixture(scope='module') +def simple_umesh(request): + """Fixture returning UnstructuredMesh with all attributes""" + surf1 = openmc.Sphere(r=20.0, boundary_type="vacuum") + material1 = openmc.Material() + material1.add_element("H", 1.0) + material1.set_density('g/cm3', 1.0) + + materials = openmc.Materials([material1]) + cell1 = openmc.Cell(region=-surf1, fill=material1) + geometry = openmc.Geometry([cell1]) + + umesh = openmc.UnstructuredMesh( + filename=request.path.parent.parent + / "regression_tests/external_moab/test_mesh_tets.h5m", + library="moab", + mesh_id=1 + ) + # setting ID to make it easier to get the mesh from the statepoint later + mesh_filter = openmc.MeshFilter(umesh) + + # Create flux mesh tally to score alpha production + mesh_tally = openmc.Tally(name="test_tally") + mesh_tally.filters = [mesh_filter] + mesh_tally.scores = ["total"] + + tallies = openmc.Tallies([mesh_tally]) + + settings = openmc.Settings() + settings.run_mode = "fixed source" + settings.batches = 2 + settings.particles = 100 + settings.source = openmc.IndependentSource( + space=openmc.stats.Point((0.1, 0.1, 0.1)) + ) + + model = openmc.Model( + materials=materials, geometry=geometry, settings=settings, tallies=tallies + ) + + with change_directory(tmpdir=True): + statepoint_file = model.run() + with openmc.StatePoint(statepoint_file) as sp: + return sp.meshes[1] + + +@pytest.mark.skipif(not openmc.lib._dagmc_enabled(), reason="DAGMC not enabled.") +@pytest.mark.parametrize('export_type', ('.vtk', '.vtu')) +def test_umesh(run_in_tmpdir, simple_umesh, export_type): + """Performs a minimal UnstructuredMesh simulation, reads in the resulting + statepoint file and writes the mesh data to vtk and vtkhdf files. It is + necessary to read in the unstructured mesh from a statepoint file to ensure + it has all the required attributes + """ + # Get VTK modules + vtkIOLegacy = pytest.importorskip("vtkmodules.vtkIOLegacy") + vtkIOXML = pytest.importorskip("vtkmodules.vtkIOXML") + + # Sample some random data and write to VTK + rng = np.random.default_rng() + ref_data = rng.random(simple_umesh.dimension) + filename = f"test_mesh{export_type}" + simple_umesh.write_data_to_vtk(datasets={"mean": ref_data}, filename=filename) + + assert Path(filename).exists() + + if export_type == ".vtk": + reader = vtkIOLegacy.vtkGenericDataObjectReader() + elif export_type == ".vtu": + reader = vtkIOXML.vtkXMLGenericDataObjectReader() + reader.SetFileName(str(filename)) + reader.Update() + + # Get mean from file and make sure it matches original data + arr = reader.GetOutput().GetCellData().GetArray("mean") + mean = np.array([arr.GetTuple1(i) for i in range(ref_data.size)]) + np.testing.assert_almost_equal(mean, ref_data) + + def test_mesh_get_homogenized_materials(): """Test the get_homogenized_materials method""" # Simple model with 1 cm of Fe56 next to 1 cm of H1 From 0ceb49bc5cb9c7fa8853db6822f82ff73a909a7e Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 13 Feb 2025 18:04:04 -0600 Subject: [PATCH 145/184] Avoid error in CI from newlines in commit message (#3302) --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e69a13db906..de99bbbe20a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,10 +107,10 @@ jobs: if [ "${{ github.event_name }}" == "pull_request" ]; then BRANCH_SHA=$(git rev-list --parents -n 1 HEAD | rev | cut -d" " -f 1 | rev) fi - COMMIT_MESSAGE=$(git log $BRANCH_SHA -1 --pretty=%B) + COMMIT_MESSAGE=$(git log $BRANCH_SHA -1 --pretty=%B | tr '\n' ' ') echo ${COMMIT_MESSAGE} echo "COMMIT_MESSAGE=${COMMIT_MESSAGE}" >> $GITHUB_ENV - + - name: Apt dependencies shell: bash run: | @@ -170,7 +170,7 @@ jobs: if: ${{ contains(env.COMMIT_MESSAGE, '[gha-debug]') }} uses: mxschmitt/action-tmate@v3 timeout-minutes: 10 - + - name: after_success shell: bash run: | From 02e225b8f66d7f22b80e4f0c8cf78090178638e6 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Fri, 14 Feb 2025 05:03:19 +0100 Subject: [PATCH 146/184] Avoid end of life ubuntu 20.04 in ReadTheDocs runner (#3301) Co-authored-by: Jon Shimwell --- .readthedocs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 7b69b64b626..8bbc7d01f0b 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,7 +1,7 @@ version: 2 build: - os: "ubuntu-20.04" + os: "ubuntu-24.04" tools: python: "3.12" From be4396c12bd09ad0ea81eadc90e16888bab93dff Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Fri, 14 Feb 2025 17:39:09 +0100 Subject: [PATCH 147/184] adding non elastic MT number (#3285) Co-authored-by: Paul Romano --- openmc/data/reaction.py | 10 +++++----- src/reaction.cpp | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/openmc/data/reaction.py b/openmc/data/reaction.py index 8f90d2de471..65b59582cf8 100644 --- a/openmc/data/reaction.py +++ b/openmc/data/reaction.py @@ -25,11 +25,11 @@ from .uncorrelated import UncorrelatedAngleEnergy -REACTION_NAME = {1: '(n,total)', 2: '(n,elastic)', 4: '(n,level)', - 5: '(n,misc)', 11: '(n,2nd)', 16: '(n,2n)', 17: '(n,3n)', - 18: '(n,fission)', 19: '(n,f)', 20: '(n,nf)', 21: '(n,2nf)', - 22: '(n,na)', 23: '(n,n3a)', 24: '(n,2na)', 25: '(n,3na)', - 27: '(n,absorption)', 28: '(n,np)', 29: '(n,n2a)', +REACTION_NAME = {1: '(n,total)', 2: '(n,elastic)', 3: "(n,nonelastic)", + 4: '(n,level)', 5: '(n,misc)', 11: '(n,2nd)', 16: '(n,2n)', + 17: '(n,3n)', 18: '(n,fission)', 19: '(n,f)', 20: '(n,nf)', + 21: '(n,2nf)', 22: '(n,na)', 23: '(n,n3a)', 24: '(n,2na)', + 25: '(n,3na)', 27: '(n,absorption)', 28: '(n,np)', 29: '(n,n2a)', 30: '(n,2n2a)', 32: '(n,nd)', 33: '(n,nt)', 34: '(n,n3He)', 35: '(n,nd2a)', 36: '(n,nt2a)', 37: '(n,4n)', 38: '(n,3nf)', 41: '(n,2np)', 42: '(n,3np)', 44: '(n,n2p)', 45: '(n,npa)', diff --git a/src/reaction.cpp b/src/reaction.cpp index 0643cb08bdd..3bdc8374679 100644 --- a/src/reaction.cpp +++ b/src/reaction.cpp @@ -175,6 +175,7 @@ std::unordered_map REACTION_NAME_MAP { // Normal ENDF-based reactions {TOTAL_XS, "(n,total)"}, {ELASTIC, "(n,elastic)"}, + {N_NONELASTIC, "(n,nonelastic)"}, {N_LEVEL, "(n,level)"}, {N_2ND, "(n,2nd)"}, {N_2N, "(n,2n)"}, From 11587786e06641851a2c28a6b753105b02575541 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Sat, 15 Feb 2025 05:58:30 +0100 Subject: [PATCH 148/184] removed old command line scripts (#3300) Co-authored-by: Paul Romano --- docs/source/_images/plotmeshtally.png | Bin 72643 -> 0 bytes docs/source/io_formats/plots.rst | 14 +- docs/source/usersguide/plots.rst | 10 +- docs/source/usersguide/processing.rst | 11 +- docs/source/usersguide/scripts.rst | 139 -------- docs/source/usersguide/settings.rst | 7 +- pyproject.toml | 11 +- scripts/openmc-ace-to-hdf5 | 156 --------- scripts/openmc-plot-mesh-tally | 344 -------------------- scripts/openmc-track-combine | 24 -- scripts/openmc-track-to-vtk | 41 --- scripts/openmc-update-inputs | 307 ----------------- scripts/openmc-update-mgxs | 212 ------------ scripts/openmc-voxel-to-vtk | 22 -- tests/regression_tests/track_output/test.py | 5 +- tests/testing_harness.py | 4 +- 16 files changed, 23 insertions(+), 1284 deletions(-) delete mode 100644 docs/source/_images/plotmeshtally.png delete mode 100755 scripts/openmc-ace-to-hdf5 delete mode 100755 scripts/openmc-plot-mesh-tally delete mode 100755 scripts/openmc-track-combine delete mode 100755 scripts/openmc-track-to-vtk delete mode 100755 scripts/openmc-update-inputs delete mode 100755 scripts/openmc-update-mgxs delete mode 100755 scripts/openmc-voxel-to-vtk diff --git a/docs/source/_images/plotmeshtally.png b/docs/source/_images/plotmeshtally.png deleted file mode 100644 index d874b4906ab84bcf11617f116a4ecd7360aa2ce2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 72643 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�+IC#>D)3I^1hid;L>$ap*P%UA~gYNtAFR)_j z>gsy3AP1+Ea3yd8wOH_7ugyQW0F)A-k7xpJmyVV;4~PLWmHzN#Tz=XO+T7W)PTWMU z)FFaWL<^4f$Up%Q`@aAC|6O7(7WIpi7L0T)Xsd){8A=uQLh#8rgxQrV1unU`l(K-OTIL&)Ffj1AX2X-)D-M0)_ zMCQ~#W9jI-ZUm}8nrgKE6@F{HKMXCI?>dBTZGgr;+|OJ~D`{@o5sasv&w}AQWfhe( zAZ-YckG;`?IPOGGw@W}2^6~fAwI37%#j~0HW`ncaB|tB#2C)T!;6EJC0o7{83&49x z&!uYXcAco*wW8UkM>^axolz=-91(iR1{;wdkKoRqes! zliwtd40uZ(xvGc~NGUK7+)an7u7so{ zUzAJ0MlE!HznAYrjFfWap1j-baC5b{-H?cuOdHR#7CFV*YLea1UHRn;&7B4xZZYvU z44yl&5F2?TXG%edNQ1HjRV6Dm?b1c^>rdMvG)bY#;C@Nt@BgqOz=Yfu)H9E2`3t#u ziFD2(?$5D)bUyzqGYQc!bFKc0NrS6gNg5-g zUQoT>WGZ7Jt4$BKvVZl+o(6&IBFeoauPG6wRplj|+FhoV66-mrg-MbR2Np#{0m zYW;o#7G!RyXd^Wo`|O82F1#>u z1LV-=wDllje_Z$Gn^n#Gce13xqgUQ;*M;6*GrDd80Y@#!81S|y?EV7=lk~F%ex2F7 zSL0Ho0Neu zf3~{Ju$xBPKdhSjqe>$7(06L9+3C+HP2fN|1rhsqnUrXfUHb&UmgcTtjO*q`Sv+M7 zMGVVw?LY@02MtV=QfKrFUhf3a7sKZ7I4i7?#N+`v%_`M?GyFKNro@H4Lbop4SVvhA zF5xJxjGb)~l+D^+QFmu7rN4!`KllTShNRYID7MF`+qBP^VxFdx`i|4-EF-ccB!xt4 zx?#pFrtqFFGT16#Xy5;GvXS}SKX2K@?h!Cdv{alIqx8KhyS+~ee>Lnpd(QLxF>>F_ zJHC3Ygwl&)OwG5(8fb6gTTQFqMP|KtS5z>jxJ*XgSKDL2!=S)6J#IChYdU3zC7xYH z`vPT6qv&+I8YfP9+%yR*LLwFoJ9g$`ErUe2Vch)bL}hu9xEROjBu*J6l($pqoEf?D z$N*aHW!+9gspHA$glih(Qb=7gMo7UEb>9J@4tU7``9_e(Z5?u9G@kXDW#dW*YUm{q z84>Dh*1K5jz$SqEIA%nzA=@`8F!Sef+{>le^nfNsUE#9lTGu*Com%3q1ADlluq6Yw z^c3vzkofkRHvHuz?s4w*!w4Dmr8ji5aWZ#UT;CKCukxuR@~d34Aujg8X8seY=F-27 z#PYHcnmfU=p&NWgYLeZVy9sZO4&%3a8$eWcv@sTNU@iIfZH!8Cnp!5MS!&t9L#J5} z>O&`Af5GX>eYMK&$#vL3z`&2+FWqvrV?W9k(G*wlQcsnp|EAieR4-Q>xquKl-PW-4 zoR0mPr7*vz+08Kcs@_D==I5BHH%kAkm?)Ocjj^tdyl~cW2kt(~!b->?j81`l)p`<% zS)vVugTyw|n2VS7wFTaETriRDYo5JGBGiTUk#zss2`7&@vlLGWO1G8AtQ9VUUwh|kx_560FcIE7% z>ALHm{q?8jQHuBrEneF0n{pE5tlw%R)*?ry3M{e3w3|JX`~5*SspoHBPrrSrpQ4p% zXMX`@c5utD=FyhN?o%rm1F+DiGin4N47_(Wop}eDTP7z|Z~lF2)5g|J$a!m<*c+P~ zQ+ZL^!TL_kr?a?Y-l9*alU6?!W)&l_e7~UA)M;6Z}UU(_B;LFAqj<~ezR<`D>qolm)11{X}CF;z({Ht^AL99lH@!+6oNMl z7_E>c>xn`X+qVoj-MNWO*A9IZQSSoh^rH&Bq}?_N)*nE(PhABpG~fXER2&-$4n3;j zuEr7ccZ!I}>Q}K2PolGlOxm0N@ZjKMXMUF1y4i!P?Nq@twL4Z?g{sX<8jFBt`q>j` z6JpS)W%-mrlsRi+4^BA+e>4fd`~KH^W$n+izlugekT>r|h|a7PG6`L-ru3w++z4yH z=x&NWR;o8z%7#kBQqs;C-W=z?9#*yBX|3!N&F#gUK*C>ss9aJ1f{T-toVSf!5_380 zom3=ycs@^&LZE-W!W*Z~yB3Ii=IEM>S>viYwcGw@mTE2fZ+%^6V@^$hZ)2%~)vMxH zmnf!#P%WZ9c2Y`~Nw9X7F-T@o>&#jlkj6yio${QA)MVs`u*BrG#b@Wa&UJQlkL_Q& za3#}O+iqFs7IWy0$f-vLP~NciaE>Ihf0M!jh(-pPNSjQYih#}C9e#vUG|!QD^f*&R za*1Q(nUw0ZZ7PqmWI*0mHll;yF*6)G*p5fQJ|DJ4G6yi$tG z>j}hC-B~0=+iAR8Y9i~4^?TDaPFKMC%^-YwTUohrCaG1=(+>94Klav|+N@H}vv#{n z=;nGoj>NcgAvopw+KZ&I7G+!)M``$W=*vXFfk(_JhE6?0XA+y(a6`hHymEC$u2!&Z zJ8$9U;v_v<9FO+iz_Q4`DiKSx;+De5V>ha*+nD4>`sM(t00oWX+pB{Lq?;<{TgBc* z6i`(GoI+dg5&B@~mm)CQp9(uUT@2)!gZ=I?%8^Bm(!|ZNmC5f?l2%j_kIc$X$TvyI z4<&KEU1-`2!B`G43$ba(U)Hjc6Ah?5Q7gr8uU0{2ev#O+lzga{(_UF0&fiqBmUe6L zg~RgoNoqk=9MBylj0KDS$+2MfS2e^Xe21q8e+hPSQFVJh6Ejn-3ugTRzma=gB^U~_ z68@Bc`1oz$WqQ}=Z0aK**e30fpR_;AdAjH2IdBBqr(?!`j{7?gKgkA~IigaZNmsVl zctVa$qbz?)PRW(!u-s-*W)QI=1hap|{JT(1*M;Ph^)K}FKN!RCB>-XcHOreCK_Wk<-iOo}{jDzitYY|N5Vlaacy zxkuTp1I7%22%W42Ust(fi&2zh#9LH7sfR9dhRX!8B}yk`yC{uj=36Q|&H2ddFZG!|Hsj* zW>%%B$q}lNU))AXCJ7+Uajz$|;{i7b(}XU(bt%lCPt`u{m1uOhcZY3!khM*$$>Y~` zv(6jel2lb=OU9?usXKBS=d=ax>OtvS!eNC6o|!#@#j~dFGh+AssyuyGKR%0e5|W$J z_2|U<(@jEre03Lgv(E8ger%d98JzPvd!=@4F5m(1yI8x#;}T7`Cy9rb{g;mPtfZ7z z<5?X~y{V|dqWzG=+I_V{d*Qsq@NE!T93<)@0=%ixOp{IaO*`goR;4)3;=FBkXp zlWe3TXD!3uEIV|*DAFmYYr)2~B*ekZQFY>Mc`1`*XUX+#zTUJgIn`snsxkr86)9fs zr>(z|G)mry-d%XQW!~cq&MT~|GP#Su`p)cmb~A-ISu)F;?t9+Mke@E{okl-{JOq+j zLe^u*War|hdmY4fRj8|Wb<*=~6fwV5=ZR!&q*He#PI+JeIPmcFae}{(FB}Kt^3#aM zt^%!#t^B9u5~ytcr;zxg;h9v=yI)DyvU6U$9OFrj*nL!D@_72xeZYhCj=o)4T0bd| zfC7_ECcyaycd0F)TcN z@jfR5hEw;!{7yLKPsCXN;ARf-TxV!mI1zf=JVjB}iRFq$0E9xi&i2`oCu~lmBeZBg zR{Hu^S;01%pv#60I{~t9dG;G#1Mv*6MSohI?xx73$2eX?`Ku>p`zI??HUd$-l(aff z=MAMkq@21R@nF=KrO%m={DTc6xq`quHnVRZFE@kTnkyWgrpHX%*@9L4)p{)Iy*w&2 zl*XKOF7~Hz=I$cxir+@I;l)vf+W49zw0JceDCZT76_|vuiT4!c>R)|74+)RBf6aR{ zkzB2LRry>$P=b|eHzG`}hZO@L7JIYx)d0<*adH`E{Q>|Kr}gLGM*vzrW5} zvWPtB!M5R#317w#3D1hpVHC zOkaD_wP+V|7bnDl)+~&N5TqeXAb-VQo;kfe~HQb7^3f%pk#5j9ZJh8%xiNoBUb!SYR%x_qGZnGu zY(}KqGTv2x)h&jKma0tD^#JZCkm{}Lekc|t!K4zbTpY{5jQotP!wc37PFIifQ zZh?p~P+Gxq(e!?(&FYr*e6G0}H!+q;5)(sAT%1)!pOvLp33EJvjoh$$L{yYo!olIN zj*|M;bGvG+c(vl4OmI=p@~XjN5^f?xvu>5+Setx=BO%6S`T;KAIsKJob-SRDTH53J z$ZS78;p8zD(csXr$ZtuS+V!g&HtihbdLd}i+r|E~ZJn!!Gt~y#iQ~LQlCF;RSw_{; z{~s;lii=C0ul!VjKKk9u-IHKh@l&4whnlt>jMq2P`_PZmM+L6#@VS@ z;+|CyQ<4?K7}5|JV0T1EFwP!&lJ7^EN5?_(MY4@i&%-iL*@Zjl_*u%W9E{kD9`-4~ zR1G*e>LZ8n{Jg*<>P`m4v%?jO3A&fp*0;0!cP~ zwHYQ(3_5mxW|bt`#L;t9w{psPv!X%Su1d<}o3Cz>N;1G2N-45u8&54%WUc%-h?i3w zKm||{Fdrv-Y}WcQirsFHFRG-e>LpjsVEn~Ow5pI6dE!bzaWl(|=3M?I&w5ZskPEfV zUQjU8hW7SZE`&Mmw;s!O-Wxv=f4d+(jfno?=%rH~ZGP8UkD?U!47=ZCXwZ2Z>S0hk zz4AwoU#Q=!d(}5e5Q*K_kmSOByz%QCKk}oALUZ)F>LF2h;&;h!9CeZr{w(eB?I-c^N>pLv{7yF` zcy9_QD7?&UmPBL6$6z&;Kgi|GDt`sU3BD*OcBpL>jaF5Oxt-9>6y6nTaxS^-VLIE{ z`pV8Yq^Ut^+=r~nG!KQFp@RDbl12Re{U$S92TdyW7q?-24pwl1{TinZaZLvXL z{i@lQGEi7J8IAw=S_k*p66I!`L{$;D6h(gaVOm9i7w zR{iGZVdu}2bbp5%41M0vcmv3ixpr=xUo@L18# z^-LbIKR6t9WaR^AC!;Ew^uDr6=DQX_+vwKkmS~PuoxZrJbD#nTJWsGu!4FD_gsXx) ztA>k5Zh@=nM@O-1D3z$azkhQ(?%<8?x};wARZ~hPPEXSC ztVPT_WL*h*AU{YiXbY6u!iiWiRwZ+ro+fUbO9_k`=2SI?;x)+c^ip*iS}xxOaIfRW zvw;-jz+n-q<}v;pct**>V1rU8>SbhdC<~M=FFf`SmDUp%#TZ1IStRQd*>UuJNPV}$ z6^E8;+xhc5ADU%v*XEKk2LRz%K78<2P<(T?$MVC?K8=;)ORb8TfDM$2hYFi{j+=Uc zO=yOOXTh$j_wwf22r@BW;q^C$UM=Dk8X79lN|jP)xI%vikR5sDd1y_3e&Fnq@~OK- zHoCud&xMMz^pIIYl&ENAXHp90UA>tm#vWHjewTjfxl1ENd}{LzJnicMRIiF+UQI>W zzH7I4?n!e@OeH5TnwZNNUeI12Oj6g!Co2qgV$DV>@~3=SU9o?@d*It#yVK!o=EXUx ztzyQ=z4sD>&*8D6juV?=9}mWQh+RpCS=R=$L?opIlKdixOub;6DNhvQK!Mt$jZv^* zuiS9q>>l_J4RW<{q(4L{d)NO<4KY2z@yFnk@BQR43bE*GU7MGacNfXunJYy}!C{KjuUm+2m04`^0d zMt!UWbAK{9!|N`(k(3Ws5daHjeP-2`nBRF|`S2MbZ<{$M7nT#(>nC z7jUGjr=AUvM9pUS! z5dNB~s@N@ZtG?RXNm2<+{b$o)D_Ufx@`p;JP~ePTe^lq}D~5#A0FERezz7TtCw@bR zGrKz4@e1U%VHzash!QS~QP$#4|ulQCX6NaV#5=5x4O~L7RWP=ym$*o-(l0n`NvXJ-+tfcdnc5j3$g? zCxZ3qHQDmd!J+@FySEIBG78&8#{w0QQb1BdKq=`4k)fod8>G9t6_peaDQW2jDQS={ z2c(-pYUmnZsA0~+Z=dVh`|R)A=lgrk`@_p0!@RTByWX|#=YHyN- zSde#Ba=4AWirEb!j?2puaE!wm9W4MrGQUq8-0qK$Djfxg9y(OtTT*jvLwRyLUNGld zd!G9{!l)@e>6zYyRFf`k7+N8dOX4y5KXjYVlVpNF4cu+K0nt@%T2#fad@6V7C8AlZ zp|E4y@N8EZe951Sj15g#XxyCR)yZ&7fTx%UZJWaQZgar@tpzZIX4eSnl z*xElkEL7yDqGtcrOqC;shlQ~4&cNE0#c(*y%KvrUjpDo!pZM2D{W=82l=R(97>B_8=8R>VxJe3}CTN)ZJ^vARCDzb2JFl7NbfnqX` z92_$Ns;~UNSX9%VCQv^65Rywkc&rr7I>qCPUWn~~l0Ob#OsBulhR@aIFCA8>R9;LC z)#n}cSwFDslyq!?pXc6ye6PqGS5i7&pVTJ;?3?M>3r<3?uk=oxMMXrU2)P$|t>*Tb zjg;5NnV)TI2&>5d^gkO$YYE?Q z4V;Ize*Pl#xCNRw7{_m|aYQmGl@Z&}-dlqF%S7*xI##;~qfmHJRv@yr`Rcf4`2C;W(1t2fC9$%0z ziPgqzD!O~7i)*-q-%R!}3EO!jpAlH=m9#u!c<}RU`(^{Sf^mh?^XcP`sHRiegIB_R zw9`W=e%|eB8Lb)RPZzkVi<3edJ+9E+*9EtCaVuuzvYAsuL%FkOW^>2LKTKP9Ge@IVJM7YQhBI79i40Y zdhx|qc|{6-o^E{<`CUXHpadbe!PVSlu% zPSsrMOh?>?Zg=K*SIseD{yZ^X2D6l}>WF7+*E9vcNJ*>euHdDh(q)Z}-TKm?9u zGKd0tEui|!Cv|^H7u)AHZJr2>hm#Z-<-Y#5r~(o$AePh6G6Wq!(TZLP$nyGp=B>I2 ztsTq>O-d@U5oI`d@1N{2fP(~gS9IUCO1w3Sh z*x92tZ@Oh~sw0WhR~Pl3X`^=|j8^rsE~0!_Jo$6Zo?$%zyEfw$o_gzUPhC=!vwe%Y zX9(4`vc5Xp`BQFysQIf`?ZCzp{9t}yDq2@3u*_3zTaCY@_T^elxyb5VuU|ar2M1ms z5nWjssI6Vs>9?eFr&Z6c6`q>bx;Ae{d8(!r+%xN2+Kjo9M5u>AqL7_Jcx^5h945bX zEVz=s#x|}Z5c#>e8}DoS%;++S62x#(3D}UkcSW01HiKM53zef=fupRdn(C1ZT8!$t zERl-bv;?#TMJAM9tAp%nXOD*<+pf1Dx4(Ev^Ug?rA_j;)HkN*1q^l!XFipu{W)3BC zeZC6`X=oG|7gtq9Q^5wy%F3jF;r%;XfnRm?*jQZLF3=lYOE*qeO+VMqJ7{R=%%=t4 zfIP7>)8R{jc{DaBi8@lbfG-NTXQl(JArR-xU>11Zc>aSE|1G)AIGDG9djg2hoa*2x z7SIAwllgRA`491bzfS~2zs%D;QLAffz*v~>HkbymG62K*m55kPO|3-J7J0z$P)l`-E-<%m>iX>FisGb=*q&o{$RdXUAo77G;mTJ{Vb#G8zuE` zjA4!QCYPmwSY&}jykcx$0&hb8JeS^|KPrznm#$l|PIXKXhhaTEJ+a_OMsH38C|_Mo zAnn?;@j=BlVTXg_XaW1s?5WouRBKATED*3<5>_=+q`f+>CT=#0ZlbUcIvumyhq3Ut z9?#8+$9si^GT3VSMdJh;IU_YlyR~&a$0|jIXgN6 zEdfW}*athgub?sOxXmNAi)4rJi&4M*Qq45h14fKZw*lJjwfn|_C_M0B+UP{9=@cRC z`_l${mQs9|e<<1(#tm3zjU(=sm`g?X`uXne--2RdkatmicN-%IEP$R6)P`_^>X^<> zsN63y+de5?Hd$e3M%3o~fCLncolfI?)aLE$Nm?u#Tkv+;Hc-1dzq8}Fu^hrEV9S5f zfWVq=DSg?Nw54_>q$BsfjwyQ+dKw(8c$?U`*sZnJ8um-A)@HWlm8^Qq@8%W$ zQI2SS;uzh!!^m2`d^lzd@7aVanjB>Qily5;B_!HFL%&y&l0Y;o2F8+;!@!BT_&lma zUb5}}XxFG_jZ7+Xo!Fc%D^zUR@dBn1^PtoUF^QAVbZmUS75%`)=1d-b*@Hh=yytD< z#Hx{6qJ-;M{ch71!UWJj(O=~(u;CfshWiZGR>3tUQU1%~5n3mKWiBA zM5;eF?(XCOtx9=bET_>e2E?zB1;1AFaw-Uf+)8)nqpKynxV+EHbvO&78AADd!+D+! z-5$#6AV>biHL?kvHHldOd_ckqA=1*2-GRkNDo5t9LSq&@eno@fcgwAHJ zQpA&yUK6>*WvPB=f2v=ob@Y|wUvF}y?PrDH@M*t?NEJLcvz6T|9^INO9uuy^8d|V}N z|DUUG%%$5~BzaXnjHK1=h(KPtlCS!y#YWnT{_Qm5s$uw>qN|5UVHwwOsQfe?AJkkg zvNK4*K|$~|mShAbml7%QRn&Mf#~15!gIC+<#8y3Lw?p*f$k(`hRPQ#yk!0Uh=sa`J zG?@k2$btG8)p9|C^rR`d^piiQ>c~<|4SnTjgJYP5a(jGsY#ME|$p&(dgu_*ThGjuh zRomaDM7P>}Tq%7_w7-mGN6=6*b2utErgrEC<@Fge_)Z%vp+TGIvNH?E-lr&B`A}{H zVT++lUU|>sbgn1*j9*#hBso4OxBi_aBH`wvf7RUr0yLcJ{{f+r@mij$X-_f_tUV1( zmxMCvX(xU(T;8ikZgMuiQn&=zD&f+!Z>e^I1#0Hree(Kld}#GyNk)r)f=&^^bk-1s z$E0Jq!cT%{$7<))i^HxYdY9J)D4E8e90%iLQxdLzW6cSPEIcQSZgD#NQi{=UUXvZN z^%Y_mTwQF|U24EBO}Ah!JAwhe(ek<{sz zIkp%G*^DvQ&X!syJ11gu66#Y9*;chAVRXh%t<}zjoxAWLFOi*7=fHD+Yoe6f2AZUh zH^AP=%FYfn%|N^Z4n4lv^W&}ZhQs^$qZ^Lx6RQ9My%g@GSeX5qj5U`!@Kra#NPVE# zH9)_CV96hE?1j2{e`VW zmM_(u0X{-^I;vd!GP9F1o$ItgPx`5L#n!(y@F?N@I1V%!2HGhdP)P_77jUxrJX%sV zECw=!0PU_%>9bF6^1?y0msyjFb%Z}!IV;kmtt+n6)vFrdA*E5(56O8+FpDA_*i*TY zVmu#_bjgqh27Dg4*C%*D1;FzzF144Jmq6?<`hzd&sA!>Sc#S{JK^#04C3)q&yoT>{ zb%&~7b3jSKSlmOjN~>}Y7{JRA5V02&Of{La5I$vMn%(h0b3-oX0LEoVSBd#lHrBQA!%)o12^4+om#ARRD7lIsteV%OIze&&|fm`!v_r z%8L2gD<$BJ3@9G;G&JTai6Bo-|Ia{6i8$ptAp?2&{lB4BzBY2WBys{oSHbn026CVb z1C`E3#)1ERl4Z^e8{n5w`aL$i+P=zb+eolHkWJBguv?JSr>U-q1Md5a645=}cIyOH z({l=!k^caXn#QrLrly?S#;^Cc<{kJ6od>&+0ogw&u4ADA-uv0dj{fPgV95awyX@?2 zpPYPt*AVaj&#)`LQ9+J1UZXvjbHRg2RJ8Kl5yIPnQjmAw2|OFSPW3RjFRmDD{B)mS zZUeaTd9iWBi1awpsd-cY&KH<0iu1mJ-*Xz5b&5)L<=<`I9x#U! z-X>*u#C@xiVUi10>HN&Ts4#Cn&wBXrg@&dnZ{)JNMTDBta**xw2ny|8Tbty9CzW-O zn+rH7!(BlHs^|UkjoT7b1n4HCa_hx~5Z9sOXoR0394R`)jm9ua+vt(gt0Q(6T#p7h zsk8z%PL*#%)(QHjU`P?(BkzGk?82-0e#uuLP$M$0^^AR{ZX+CHnE2H*mag|v%`-oo zN8+%64J3VAEaocF*T3wXv}AB}2o4`8oKLa5|A@j;3=_Xrf5uG|xh{JfV#U4cR(`5e zA{dX}nWUh~hM$^d;jlE@vdza<_>8{4yq_{bU+1~`jnE(M_RSgaL0Nu_s`saHJErXT zBBitKc5DqFI?Cy_dm0Sq#mhi%`P#r5F5`N#bXhI=DMvheUAaT5|JH#mFk6>CZK?vGjL)b)dzqPKkyz>=Xn8Hw8TgqG6y; z-qPGWH#hg};)45r(39%b%;e$_$BMOD-s#nOM$9u#&y%G`*xfD3>+J+FZ|Yrlj{;LE zk0vgnJtnzNmNWwWO>25$XVkh*6)}ZWVo)#YmTF*YO)c>aCD* zp&C?HH2s!o^^+72O{%!-LgB}aDn|Fw8jL;!;_%#MJ0V2@I_zw0c^)k@kEKNB>oe_A zHRk;adg2-qHR**wWT)04%1#nRivoeECwq7-}_;!@IEot$8k0|<6p%*vVAj{WlrM1HHV@TkD$aAKa1 z-h&(t?fn~D!j`(7^A*Wy*ipbn_+U+zCu2$@XCojmE|Wd5?-5oH-+30@5b^9__~$BHW(bdLy?Bk~P+8CIecK@Gg!m1J zRa#s=sHg&dp5E!@B_#}5peP4a)5#q>7T%F6`%t}P^LN%cU^coZ6&v<>xGI^N9;?2o z*+Ff1@g6NXoyq)#xX^KwIP3y}u;_R{lCIn$@GIL}F=aS}fgzZHP%5M`-XTyF)qB<; zCau`mQVJ?kj}h7VEu5UwX-~uV3#Kmmixl&jL@B&IJz;b9GDRVIb=RcXDd&8oJY75SqB!rjVo<;2_{L2eaOHW zCWztNPH)S@qu^f=-}kCc2n}^tP8E8JS{({33r*%jzaCEMRcY}Y_hfN-kXHDk(x)bE z%)-|fXVCo-)9cOC%CjNGx%Zco6=j*TAI~`1qe!qK2x0iR z5GNLbuJiudruH!XyQEw4GlztB2QE8sexI;GvYH-MpqeEHfzM z#89?jvUX#b^63`Bud+SiXbKrERexV<+Eskk9nVN>QEH@H{CKVN?wO9V^w#e07k1l= zWNS!R2zz&V->R3m7W)TUNS{JV$7vyE)AG1PYo5Rj#ZKIo{HUWCIo8JT0U6(H! zD%}vjPLDIt7d@ZrdJj~*mWUvW{mDmuYE|t&$DG=o_4UYG3Xk{MCMXBybf}y#NBI}^ z6M@NZ>X)mcxw!C+Hq+MIL&q;#F8mq?>U2iOaV*)+?Ig<|cO=`rHRTXhWe65Md28c{ zpWv1MlD_Y+o{wQQ8(e3Ha5rDqGw;p*>-$!fBjNJpe?6_l2K&*|tE3eun)A9UC>!3- z+GXM!kOqR7V66 zndEf$WmEXTzDjd-F+in^Qd!Y)U+g6fG-`Q!Hd0^j4$a{_cSCZ&fOdL6NEPszMCe3R zVt0y@Os}5}eC5TQIz?XUt&Acq@LVEOU)Tl@c#7L8VeFcdCL8Xce9e9>GT)McCjaqO zMOP*Ye;uXU6xW(Gqoy6+ahVpSuBB4E$xs zhpF>00RA!KHmlx-3#*xO*HpIc_umtMxEko|`%94!x+K63yuZ!3oGE)=e14rfVp9Wr zd$XmZUXVw!QTPg1yj&DOE9wLdbxnkBpw6|(15B^3g9mK`uf0!u|Bk!R4_0x zr6@3}#&tM$3V{&M$(5Lb?{R?O?TT?RW^2y1uv4U|Mo7C16T>6h-xF@l*N&->zfC}aT z-X`&{r6Tk7gN<29BHG?siQQtMy-s@BQ}%$W+l08dkDortwdpp9^s*Ssb*#8}F{M8T zYv!fRUS9AUc5&~%7YfG4##XYSM-CBzQP{vEtAR&?0A$ZSb`~U|poyMPnovtlNqw_)~E3$J-RRSn2@2JNO4Z(Js{t>7aefVg!o%dn!x7d~djwjx{}92TW?+ zqaUorlUu?Utl*x{%f7T7PnVFK|)a26F_i?;oCkA%n|}089_K-aQBc z=p9^tB+7*Sg!_MDF7_}pZ#L@)E&qMa38yh{-=0JJu$$(pdi`T$=*P{bdEcn?+Ivt2 z-65)Z&wT|&rfu7~%j|fje?AXR#8&Ph@|h;fzY(qdjz_wu#F)!g2_a?iz(9`tyuSJL z)}!TUWjLyKTH-uvL9psfriv|<;rbW6WU`i&ec-hiU+9eIOH$<%NT2OOaX9yz7Bh-$ z2zU->clvt$F(+Yb$x}Wo>7Guq$6YM$mI+PgK1%Jg84C9%c_<7o;ad5ZiJ=7-5k|k) zEuYA&X4c@4no~2EtclPQ(R|~xb_yl#Fq&&U+RB|pzV83+!FA-d)I6ywB6vp7o@=^A z8jw+eQNQCI%!He9x&FgJIwABloC*gM-%`GR*_A9*6JcZqSBYiH0m?I$05?CW>{z>l zZ>zH{Nr?g93${%Zsj^YwvUQF#*^;kP$%Z{F23lM7TXvvQRMt(q-wxMsJ~Rs=+s?QV zX|KPD*{br-*Pwl`C;FFt3Ep45hhFK3+>Zl4Cj!4JfoB#is~t(%m*ZOJR=Ju{I%;mG zUZ3En`5AXdb;CW%F8^+!>fb4+hdwt#Z3NkO4@bAgEJ|u1*sj>0)5Sp35mbImN7es^ z!1f*Q@5O0TK24eyrmClTFMvm|Wc;+$9ffoMFgA)M8K2ECP|RnOf;e_wC+lu>cyoEl zjGK?D9Rpzutw4)od0vg{u%A`mjL9f^PgIyuTXHO4<~w@4R+B^2Wsk)l7Yfb6-yE-1 zt_xlw-o0yS^aH%!ZM;$m|$tUg(0dZMsSRhDV!EQSa?8z{54+$J=sPx@TZOuW>v z@pzMw3h1T8ksDfC7}9@ByY4IB$Gydaa&c;=Ts_S#!tabW$jYGqCpsx5n?B5lLp+*} zbZ%BW20pcujg7{~j!gYqn;$AaJA+QTii!T`W}mXPUT)=Sp|S*_p=701Q6(BRuR8Q>qMgL!cN8)gA3Z^$Mrt*on!eIl1+bn6 z=~eQrvqRJ(7Q?rpK1C^bH5tNjTG=N!$TtRmv&#>@MYen!AaygQ{vU9u42sm{JBWnnyBl%jH{KM zO=|RnrlHM)*N=(JtN&N_ zVBtEj`4JaN_B8W$SpP+e8p#5V_(TpWp^i#2!juMybP$7v7hEFiho(uXc_;gu9FuW4 zWfIP&dCC+Ng-_{B^pgYJw1uOlDUG~s5JjKi&o@J>(__NajL076FjmGee+}&dG!b|{ z$uGt=;?w*e*iUSpgN!A(_yz4+9$f$Dj|2hO@WCY{n`i?#DR8a#zxj_s=l{+=?lLc| ze-rXc@#RA)2?FSL8xG`;99gijPJ;2biI9H>op%=V#Q6B5R51f}b&za|xR#^DkOe!t zxZK}I;p5>ATk(`4980ds9x9+{E+V449aPCT`1cA|fIiTiZff?}#jB0}8TW!rM2@!S~wIE2g`s zNDeNIY_SgXR~5r7BErI^0aXO`@?yb;Uh8dUZc7vZ`?RfNiz=NK*oJpYBmRf(d&+QTnzaC&Ou z5gDwbA?`@`R~h#qb3fze7Zmf}6z)KM3|=oNhCk67vpFSs&Nt|HReUL3ekOX_hK+yQ zP07O2Jw4#+%1~)4ff({qPH=t_!=SBXZ`iPjShg%Z|5dlbX^Mr#7yZWsRtOu=bQBJl zC_+WclMAnvNtKP$t7K7$@SD=K5zC%*-h6i*K#ACSKq?y|9(qTHk|TErnUq& zb6{JjGgf~GMd=6cg>GbjDx$*pIZ9QCjJW@CEsuJjUVFe@zVCjDlOmoJ>zsM;Yrikh zzbHhlv6UDymk@`q|C%<%m@EaBH*6JG>JN)pVzZVE|Bv(i{M?8vtuG&4wU3u|NOgV2 zryXyAccJ3E*8qOGQ}^WpJTHymLR4;DI=nvM*b-i7lr%9}sx@)Yg0oG*kJy9-i%&JOM2 zzvac>sC!i8MGgB(G0gGr4Aaso;Svfy$1gRgV@AF-=0!Y?IsH{F_dzn1>}u&M*N9_2 zT}##CXKZE<@m_PnQeRj?EJX`O%rtO8huhjf1Wku6F39FLeuPqTV;6@NlsWZf zTs1vA;m8mpWOGj%OVc^r+!DO!7|8Whf3#ij%h}>(5>n}@LI!VP%C7F!CjeQ=z{{b? zm{saz>rm0Mw~U_~Z2OpVZlgT}Gu?!=E6;2J#Ghl(mpZ?hAiY`n&x;3 z=);r7+5j2^)I_musCd%;qO>l2?xqP=6uIaNZ8$^nWZHm1Q=1<`jbm=L*t+2Ey`V~> zoJg<5+>OuS=Aq0&+6Maz_;>Q3Z@p>~Tq?Do*dii$xM*o5P;^v#nM~W zu739pVva$%*QQlN3!Vx2HMKiljw*k&W|I=uy4pX!(Er7x>^3=#9EGQ)SyNuE%d0iB zVULiqp1wOy<7U&eADpdd?eyJ_jqa%vU^NM4kvVCP-}|lYQGr+wN#EP;)1Ru?PqFWL z&Lg=OHM%R}RPAE-gI}gSmpG)qdKev4HU*z=7wr7fi){MPvkZkeQN&#HN;?H&X@PQ7 zWAn;v_k(0-zEcNOZ$o3uL;F5i*YWpjk**o3-f>68cdrNeoQ_r!OC_k&AkN>g+@*_| zeHRve3m- zNa(C7YN01~G?;2D5`x zrk4-?Ul5>EqI-`>wfw6hLyuHEJtzInk$;OaKZt5?ZJ?#} zPxpUlo*yP>NTLc(Eyd8An@0B`i<-lJHN+wHO~G!NN_bQh&$A;E!Utd;3*T+Xj64f} zzf#H+lJ4K?e4JXNt{%`gxV6QB+w_E}Fp}EPFrAVxX7L5h(A7qe%m_{Q!cH+=I4-gh zjf_ae^Y*9N@bJ$1nMf_xK|$xkgu-#vGU&&6!=iDDc!rdBS6#X$9`$@mW8pX0SAI-d z_{b|A+8Cdz{%JrwtvOYFVLo<5dhXHc@35Mls#=ZP(SFWWj-X8|2;x^zw7wdfNZBJdq+iMEyR0dE&u2X{ z--kSBXe||84UOs!*}r@Ioz2{$rECc*M`gr^sh2}?v=*}zKcY{e9#Kixvf}SNvDY25 zz@7iv;Eeo9U8vA9tL?Y=k^ZehTm@Tm@S$L}F)N68r5;(aDVlqPdukTUF4d ze}Zi#JB;MTc`vo<)s17dDB~>$g;U$<<^(q$4bpRUZsfOfYq+a41V@Bf6&L-Cm9cwe z_br|FkivTC!mR6Ld0ceYDxpQ{;%r{N6;`k=c6A`!yotZ@Q1t54q^z6vVA@op^Kl;L z@|TFH{TE-2;X5I3YBra3z*?>OS$V#4GDM1IH2CtWNAbw!cuQb#Z`Lp?B8kZQmudvW z2`{ctH(7V(F1uqRJ^Z7ce*y>)CsJNXcIbxv`usYZZh>|ZyqI0uHUWip!z-FU)bT&VZ`)(Hg>{pfP z49jAk;?S7wUEIyFt9_d~m&=*eOBkbVnb57jz+CTa##Em$x_(FH3 zC#)f`{|_Wm1ZwU!e6Ex_7fl)#I_^t>)VmOhs_Y0UIvV!B(^K=2^}_M-HVQQ)~x5*ld*LHm!lG1I_-fsihuuBtt`N`Jw^5s&lrM_CJd zHB0%_$Pnz;m$&SqdIJsT>lC#*5kI-7Be(`-BK`fK)fJx}*u{{B%07g?cRoR`Y$FB_ zyVI%3R|(3$6O;#M?3`$}JTN-hD%WWELCsj=auYHs&F;j!HRn>y&$}&R)VlJ}z;ZUM zet!}W?HSSINpv5|MZh@!bDA*oOcr5i8mqsccfN6RN7D07Xs$0mDPfoF-UeL={;{un zm)6rJ%*U&R9oPb$rtpn-v(W|GJJ5$xDm#-HrB>1D0j>JH6Q^hJME6vDs_~u=t8Cu3 ztvWhH$NQ#|w|H&fS78Acd}Z1@3FHX<7V!%K8lj?S#A-W#McMn7A_Hs}k5kk!NpWe} zyf0QTe#@w9n{tF!5f+Ls%o61|t+|oLBc@(lf^C1d`<5v>-amL78mpN`r+W4@I?CXztK1m>1l5qx;3-*45Bar|nK}H0 z9hKok4HTQgnwHz=&Pj@UllJRz#-aw=L~=#lvT6*7nA2W{tDfKU;(79>!6+9Mk6T>l z4w5oVCH+^{T_zzjymW#B?0*L0c+4yLOgnauBZ~XFIBa_3s-YQ-#Jq@ro{re93Cmr> zDk~NQ_1Mj&31nE?%w7D z{?;WgGs!;X(B!EZ$N+*XJ{8TDRjp*)FP?$xACb2p>y>e%dC}3zfMIoTC~JEQ7)rp` zGRB5IT-~Rr9zUWC-~@KAb$O zUVU*Mr*7{>LVo}6E!xb9pJh}L<};?>V&O_&ex%PgHT9^afSB%`$`DXhuW+pw}B`zs`JI? zVJlo5JiL4N_{eYz0Qow1ep}*)Zs~)NTfPD}A#NfG<{HoAmGW(K*VrV6z->z3nd>wg z!lx|(1)%jlxB5RgfxGS3KK+oF*#9-N`xdl;W)At401RC-R_<&~9(i+eQW{ryTs4vis zMn>|IM{1>_D#k@1zc8W9;b4UV(`mjGwu(UU_}AmFucZG);FJ$~`qc9K2UviOe5oc2 zVOUl{Dk`PZ0qO)athPkixklvzF*Y^V{X>0xe4rmgYAZWL{{I*H)BVdQ{8K#0dA_m@ z_bAbAI7rNT-YDeNiKBcXbc{ymi88hbFhb|25rCkHXKrjx$+7vL1Ad=V65IPOBpkX` z=u^QeN&BP2=mumP6ZPhDustV82r%|<02V`~n$+>s9f}9(H;RgZqsa!k^;Tz`#@_!V@SWl7`AO2UHMlFRn@w|i7`xnH8y330JFs_e zt?Bn(%fMXV;VV1@k>K{~WFTqo)4wAX$2hG+Q-MX-oLaClV9ogH<)N2dX!%yd2GR1I z-*!xr(M$`zY+bRg>eRZ8PAY5f+ieE9=?>mo$6|#D#QZN2ZNpV2NL7;ptwKLr(38*y zTTfH0rjO5l16NT)RMV*fzKRmuY{=-aEodYs@#@=l7vaIGI2ySOyJ`(||K@!L$LKx8 zTp(mVZG3A$3L5=}IX_0V=Xhz2=|dLzCjN^M^Y~|)({7t#^=j{0GlFiH$2v}4Z5l_b z8JTFlCT7amLb=SYu1V3P0d>DK0QdHlvTr?5>$#(LmCywr^63$}*r{kEC;IIze3IxD z?SFVcMj9f8>%7{Vo^iFit+2c=W0bsUFZiY9Z5wvUUaIxH+=}-?+j1GG!4dBLtG9Uu zwnG2%gVEGpcRN`23p)|rV-uMLnv3YJflA-Y14Hy~E(J># z^|z^x#}U~O2+!|gP!9p@Jw=9q^DU8zv~>3mdeOS@%^2&!)>)Qn!w}o z#FLWG%8y+fbzgHBu}Lx{<8v4qthmA(IX>eL_ae;1>dfpbHm*?VRKz8;o1^r9t~T|G z6RGP_@nTqXbX>Q4P&<#x3Tc`=56+RpYVdF0wg-u%ywtb-&ByabV@(=+uDSThv+`RI zCq3(ynK0(N(9R7}zozC~odsvP=YB|$p@^m@m{(8{j?fXuATf*6f_3kK1lD5bewOIE zz*i*|Vn8M~H@MPY+h>+6;$3;zCm*$(W67?Q&`evLW_9^B{rrXVTt%87MdgTiuR^r{ zzA{I2n0h=St%AbT_rm6(v?t*u`k67o zp$S#_uc zSRBtJyee{+!B4M|F>(-AxlRX6QVM$wQd5=C(p^k{=wVI%C9BMm%e>HrMaFm2{r&sPgd6`*Okbip?LK)|Vd+yAL+SA0#AiPxD+H}K|TCf;4F+R$M>fplFo%AwfZU*+%?Rx(}aP(6aO1*jb^e;{`Koh z-BOXEfkqK#iZ+0e;%({41>X_cwIgY zTiI>Av)=`<>Rn2F7`p{(}r=-M}q>#fL zVzl>95P@zxdEL)LH}6rFJr7+y2Vq)%_mwP|MB#kxY?6i zfaD*u;pH8KD+Ut+{Pgmo&z_00hcKqkjGW&g^>uDmw;FrL3zCSgw=Z}y-pBs~aePtB zK|wdcfZN;KbKbHUP_M-Q$T4;iY$8^E1X(0`Qsb#6$()$cWJZ?v0{W3(F3qtkdGSi!Q5xglXJ{jg{3*$Ks_4fLn3N z8fAWz|B@$zf1R{h6M}+9&+gnIiMaC~2pRx3Gvy)g0L2=wg7C0zJqL;WS}_TN63ya{ xzh9_PH?G0u|4|L_!yXjy{+}V^|0V`F#N4bD{Yiq?*>?S>a#BjrvR7||{x7F^=dl0) diff --git a/docs/source/io_formats/plots.rst b/docs/source/io_formats/plots.rst index 04d5720c84f..c1fa7833083 100644 --- a/docs/source/io_formats/plots.rst +++ b/docs/source/io_formats/plots.rst @@ -71,9 +71,9 @@ sub-elements: the PNG file format. The "voxel" plot type produces a binary datafile containing voxel grid positioning and the cell or material (specified by the ``color`` tag) at the center of each voxel. Voxel plot files can be - processed into VTK files using the :ref:`scripts_voxel` script provided with - OpenMC and subsequently viewed with a 3D viewer such as VISIT or Paraview. - See the :ref:`io_voxel` for information about the datafile structure. + processed into VTK files using the :func:`openmc.voxel_to_vtk` function and + subsequently viewed with a 3D viewer such as VISIT or Paraview. See the + :ref:`io_voxel` for information about the datafile structure. .. note:: High-resolution voxel files produced by OpenMC can be quite large, but the equivalent VTK files will be significantly smaller. @@ -152,17 +152,17 @@ attributes or sub-elements. These are not used in "voxel" plots: *Default*: 255 255 255 (white) :show_overlaps: - Indicates whether overlapping regions of different cells are shown. + Indicates whether overlapping regions of different cells are shown. *Default*: None :overlap_color: - Specifies the RGB color of overlapping regions of different cells. Does not - do anything if ``show_overlaps`` is "false" or not specified. Should be 3 + Specifies the RGB color of overlapping regions of different cells. Does not + do anything if ``show_overlaps`` is "false" or not specified. Should be 3 integers separated by spaces. *Default*: 255 0 0 (red) - + :meshlines: The ``meshlines`` sub-element allows for plotting the boundaries of a regular mesh on top of a plot. Only one ``meshlines`` element is allowed per diff --git a/docs/source/usersguide/plots.rst b/docs/source/usersguide/plots.rst index 2d588519c55..d453ea4537a 100644 --- a/docs/source/usersguide/plots.rst +++ b/docs/source/usersguide/plots.rst @@ -111,11 +111,11 @@ The voxel plot data is written to an :ref:`HDF5 file `. The voxel file can subsequently be converted into a standard mesh format that can be viewed in `ParaView `_, `VisIt `_, etc. This typically -will compress the size of the file significantly. The provided -:ref:`scripts_voxel` script can convert the HDF5 voxel file to VTK formats. Once -processed into a standard 3D file format, colors and masks can be defined using -the stored ID numbers to better explore the geometry. The process for doing this -will depend on the 3D viewer, but should be straightforward. +will compress the size of the file significantly. The +:func:`openmc.voxel_to_vtk` function can convert the HDF5 voxel file to VTK +formats. Once processed into a standard 3D file format, colors and masks can be +defined using the stored ID numbers to better explore the geometry. The process +for doing this will depend on the 3D viewer, but should be straightforward. .. note:: 3D voxel plotting can be very computer intensive for the viewing program (Visit, ParaView, etc.) if the number of voxels is large (>10 diff --git a/docs/source/usersguide/processing.rst b/docs/source/usersguide/processing.rst index 3103b7b7cf4..10944b5e265 100644 --- a/docs/source/usersguide/processing.rst +++ b/docs/source/usersguide/processing.rst @@ -41,12 +41,9 @@ Plotting in 2D -------------- The `example notebook`_ also demonstrates how to plot a structured mesh tally in -two dimensions using the Python API. One can also use the :ref:`scripts_plot` -script which provides an interactive GUI to explore and plot structured mesh -tallies for any scores and filter bins. - -.. image:: ../_images/plotmeshtally.png - :width: 400px +two dimensions using the Python API. One can also use the `openmc-plotter +`_ application that provides an +interactive GUI to explore and plot a much wider variety of tallies. .. _usersguide_track: @@ -81,7 +78,7 @@ of three, e.g., if we wanted particles 3 and 4 from batch 1 and generation 2:: After running OpenMC, the working directory will contain a file of the form "track_(batch #)_(generation #)_(particle #).h5" for each particle tracked. These track files can be converted into VTK poly data files with the -:ref:`scripts_track` script. +:class:`openmc.Tracks` class. ---------------------- Source Site Processing diff --git a/docs/source/usersguide/scripts.rst b/docs/source/usersguide/scripts.rst index 78ee6f77581..0879d63efda 100644 --- a/docs/source/usersguide/scripts.rst +++ b/docs/source/usersguide/scripts.rst @@ -53,142 +53,3 @@ flags: .. note:: If you're using the Python API, :func:`openmc.run` is equivalent to running ``openmc`` from the command line. - -.. _scripts_ace: - ----------------------- -``openmc-ace-to-hdf5`` ----------------------- - -This script can be used to create HDF5 nuclear data libraries used by OpenMC if -you have existing ACE files. There are four different ways you can specify ACE -libraries that are to be converted: - -1. List each ACE library as a positional argument. This is very useful in - conjunction with the usual shell utilities (``ls``, ``find``, etc.). -2. Use the ``--xml`` option to specify a pre-v0.9 cross_sections.xml file. -3. Use the ``--xsdir`` option to specify a MCNP xsdir file. -4. Use the ``--xsdata`` option to specify a Serpent xsdata file. - -The script does not use any extra information from cross_sections.xml/ xsdir/ -xsdata files to determine whether the nuclide is metastable. Instead, the -``--metastable`` argument can be used to specify whether the ZAID naming convention -follows the NNDC data convention (1000*Z + A + 300 + 100*m), or the MCNP data -convention (essentially the same as NNDC, except that the first metastable state -of Am242 is 95242 and the ground state is 95642). - -The optional ``--fission_energy_release`` argument will accept an HDF5 file -containing a library of fission energy release (ENDF MF=1 MT=458) data. A -library built from ENDF/B-VII.1 data is released with OpenMC and can be found at -openmc/data/fission_Q_data_endb71.h5. This data is necessary for -'fission-q-prompt' and 'fission-q-recoverable' tallies, but is not needed -otherwise. - --h, --help show help message and exit - --d DESTINATION, --destination DESTINATION - Directory to create new library in - --m META, --metastable META - How to interpret ZAIDs for metastable nuclides. META - can be either 'nndc' or 'mcnp'. (default: nndc) - ---xml XML Old-style cross_sections.xml that lists ACE libraries - ---xsdir XSDIR MCNP xsdir file that lists ACE libraries - ---xsdata XSDATA Serpent xsdata file that lists ACE libraries - ---fission_energy_release FISSION_ENERGY_RELEASE - HDF5 file containing fission energy release data - -.. _scripts_plot: - --------------------------- -``openmc-plot-mesh-tally`` --------------------------- - -``openmc-plot-mesh-tally`` provides a graphical user interface for plotting mesh -tallies. The path to the statepoint file can be provided as an optional arugment -(if omitted, a file dialog will be presented). - -.. _scripts_track_combine: - ------------------------- -``openmc-track-combine`` ------------------------- - -This script combines multiple HDF5 :ref:`particle track files -` into a single HDF5 particle track file. The filenames of the -particle track files should be given as posititional arguments. The output -filename can also be changed with the ``-o`` flag: - --o OUT, --out OUT Output HDF5 particle track file - -.. _scripts_track: - ------------------------ -``openmc-track-to-vtk`` ------------------------ - -This script converts HDF5 :ref:`particle track files ` to VTK -poly data that can be viewed with ParaView or VisIt. The filenames of the -particle track files should be given as posititional arguments. The output -filename can also be changed with the ``-o`` flag: - --o OUT, --out OUT Output VTK poly filename - ------------------------- -``openmc-update-inputs`` ------------------------- - -If you have existing XML files that worked in a previous version of OpenMC that -no longer work with the current version, you can try to update these files using -``openmc-update-inputs``. If any of the given files do not match the most -up-to-date formatting, then they will be automatically rewritten. The old -out-of-date files will not be deleted; they will be moved to a new file with -'.original' appended to their name. - -Formatting changes that will be made: - -geometry.xml - Lattices containing 'outside' attributes/tags will be replaced with lattices - containing 'outer' attributes, and the appropriate cells/universes will be - added. Any 'surfaces' attributes/elements on a cell will be renamed 'region'. - -materials.xml - Nuclide names will be changed from ACE aliases (e.g., Am-242m) to HDF5/GNDS - names (e.g., Am242_m1). Thermal scattering table names will be changed from - ACE aliases (e.g., HH2O) to HDF5/GNDS names (e.g., c_H_in_H2O). - ----------------------- -``openmc-update-mgxs`` ----------------------- - -This script updates OpenMC's deprecated multi-group cross section XML files to -the latest HDF5-based format. - --i IN, --input IN Input XML file --o OUT, --output OUT Output file in HDF5 format - -.. _scripts_voxel: - ---------------------------- -``openmc-voxel-to-vtk`` ---------------------------- - -When OpenMC generates :ref:`voxel plots `, they are in an -:ref:`HDF5 format ` that is not terribly useful by itself. The -``openmc-voxel-to-vtk`` script converts a voxel HDF5 file to a `VTK -`_ file. To run this script, you will need to have the VTK -Python bindings installed. To convert a voxel file, simply provide the path to -the file: - -.. code-block:: sh - - openmc-voxel-to-vtk voxel_1.h5 - -The ``openmc-voxel-to-vtk`` script also takes the following optional -command-line arguments: - --o, --output Path to output VTK file diff --git a/docs/source/usersguide/settings.rst b/docs/source/usersguide/settings.rst index ca11d648748..1b2d4bc1a5e 100644 --- a/docs/source/usersguide/settings.rst +++ b/docs/source/usersguide/settings.rst @@ -751,11 +751,10 @@ instance, whereas the :meth:`openmc.Track.filter` method returns a new with more than one process, a separate track file will be written for each MPI process with the filename ``tracks_p#.h5`` where # is the rank of the corresponding process. Multiple track files can be - combined with the :ref:`scripts_track_combine` script: + combined with the :meth:`openmc.Tracks.combine` method:: - .. code-block:: sh - - openmc-track-combine tracks_p*.h5 --out tracks.h5 + track_files = [f"tracks_p{rank}.h5" for rank in range(32)] + openmc.Tracks.combine(track_files, "tracks.h5") ----------------------- Restarting a Simulation diff --git a/pyproject.toml b/pyproject.toml index e4e8472f1aa..1f8de10e3b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,19 +59,10 @@ Repository = "https://github.com/openmc-dev/openmc" Issues = "https://github.com/openmc-dev/openmc/issues" [tool.setuptools.packages.find] -include = ['openmc*', 'scripts*'] +include = ['openmc*'] exclude = ['tests*'] [tool.setuptools.package-data] "openmc.data.effective_dose" = ["**/*.txt"] "openmc.data" = ["*.txt", "*.DAT", "*.json", "*.h5"] "openmc.lib" = ["libopenmc.dylib", "libopenmc.so"] - -[project.scripts] -openmc-ace-to-hdf5 = "scripts.openmc_ace_to_hdf5:main" -openmc-plot-mesh-tally = "scripts.openmc_plot_mesh_tally:main" -openmc-track-combine = "scripts.openmc_track_combine:main" -openmc-track-to-vtk = "scripts.openmc_track_to_vtk:main" -openmc-update-inputs = "scripts.openmc_update_inputs:main" -openmc-update-mgxs = "scripts.openmc_update_mgxs:main" -openmc-voxel-to-vtk = "scripts.openmc_voxel_to_vtk:main" diff --git a/scripts/openmc-ace-to-hdf5 b/scripts/openmc-ace-to-hdf5 deleted file mode 100755 index 66ae7e2a948..00000000000 --- a/scripts/openmc-ace-to-hdf5 +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env python3 - -"""This script can be used to create HDF5 nuclear data libraries used by -OpenMC. There are four different ways you can specify ACE libraries that are to -be converted: - -1. List each ACE library as a positional argument. This is very useful in - conjunction with the usual shell utilities (ls, find, etc.). -2. Use the --xsdir option to specify a MCNP xsdir file. -3. Use the --xsdata option to specify a Serpent xsdata file. - -The script does not use any extra information from xsdir/xsdata files to -determine whether the nuclide is metastable. Instead, the --metastable argument -can be used to specify whether the ZAID naming convention follows the NNDC data -convention (1000*Z + A + 300 + 100*m), or the MCNP data convention (essentially -the same as NNDC, except that the first metastable state of Am242 is 95242 and -the ground state is 95642). - -""" - -import argparse -from functools import partial -import os -from pathlib import Path -import warnings - -import openmc.data -from openmc.data.ace import TableType - - -def ace_to_hdf5(destination, xsdir, xsdata, libraries, metastable, libver): - - if not destination.is_dir(): - destination.mkdir(parents=True, exist_ok=True) - - ace_libraries = [] - if xsdir is not None: - ace_libraries.extend(openmc.data.ace.get_libraries_from_xsdir(xsdir)) - elif xsdata is not None: - ace_libraries.extend(openmc.data.ace.get_libraries_from_xsdata(xsdata)) - else: - ace_libraries = [Path(lib) for lib in libraries] - - converted = {} - library = openmc.data.DataLibrary() - - for path in ace_libraries: - # Check that ACE library exists - if not os.path.exists(path): - warnings.warn(f"ACE library '{path}' does not exist.") - continue - - lib = openmc.data.ace.Library(path) - for table in lib.tables: - # Check type of the ACE table and determine appropriate class / - # conversion function - if table.data_type == TableType.NEUTRON_CONTINUOUS: - name = table.zaid - cls = openmc.data.IncidentNeutron - converter = partial(cls.from_ace, metastable_scheme=metastable) - elif table.data_type == TableType.THERMAL_SCATTERING: - # Adjust name to be the new thermal scattering name - name = openmc.data.get_thermal_name(table.zaid) - cls = openmc.data.ThermalScattering - converter = cls.from_ace - else: - print(f"Can't convert ACE table {table.name}") - continue - - if name not in converted: - try: - data = converter(table) - except Exception as e: - print(f"Failed to convert {table.name}: {e}") - continue - - print(f"Converting {table.name} (ACE) to {data.name} (HDF5)") - - # Determine output filename - outfile = destination / (data.name.replace(".", "_") + ".h5") - data.export_to_hdf5(outfile, "w", libver=libver) - - # Register with library - library.register_file(outfile) - - # Add nuclide to list - converted[name] = outfile - else: - # Read existing HDF5 file - data = cls.from_hdf5(converted[name]) - - # Add data for new temperature - try: - print(f"Converting {table.name} (ACE) to {data.name} (HDF5)") - if table.data_type == TableType.NEUTRON_CONTINUOUS: - data.add_temperature_from_ace(table, metastable) - else: - data.add_temperature_from_ace(table) - except Exception as e: - print(f"Failed to convert {table.name}: {e}") - continue - - # Re-export - data.export_to_hdf5(converted[name], "w", libver=libver) - - # Write cross_sections.xml - library.export_to_xml(destination / "cross_sections.xml") - - -if __name__ == "__main__": - - class CustomFormatter( - argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter - ): - pass - - parser = argparse.ArgumentParser( - description=__doc__, formatter_class=CustomFormatter - ) - parser.add_argument("libraries", nargs="*", help="ACE libraries to convert to HDF5") - parser.add_argument( - "-d", - "--destination", - type=Path, - default=Path.cwd(), - help="Directory to create new library in", - ) - parser.add_argument( - "-m", - "--metastable", - choices=["mcnp", "nndc"], - default="nndc", - help="How to interpret ZAIDs for metastable nuclides", - ) - parser.add_argument("--xsdir", help="MCNP xsdir file that lists " "ACE libraries") - parser.add_argument( - "--xsdata", help="Serpent xsdata file that lists " "ACE libraries" - ) - parser.add_argument( - "--libver", - choices=["earliest", "latest"], - default="earliest", - help="Output HDF5 versioning. Use " - "'earliest' for backwards compatibility or 'latest' " - "for performance", - ) - args = parser.parse_args() - - ace_to_hdf5( - destination=args.destination, - xsdir=args.xsdir, - xsdata=args.xsdata, - libraries=args.libraries, - metastable=args.metastable, - libver=args.libver, - ) diff --git a/scripts/openmc-plot-mesh-tally b/scripts/openmc-plot-mesh-tally deleted file mode 100755 index 2a4fcd74341..00000000000 --- a/scripts/openmc-plot-mesh-tally +++ /dev/null @@ -1,344 +0,0 @@ -#!/usr/bin/env python3 - -"""Python script to plot tally data generated by OpenMC.""" - - -import os -import sys -import argparse -import tkinter as tk -import tkinter.filedialog as filedialog -import tkinter.font as font -import tkinter.messagebox as messagebox -import tkinter.ttk as ttk - -import matplotlib -matplotlib.use("TkAgg") -from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg -from matplotlib.backends.backend_tkagg import NavigationToolbar2Tk -from matplotlib.figure import Figure -import matplotlib.pyplot as plt -import numpy as np - -from openmc import StatePoint, MeshFilter, UnstructuredMesh - -_COMBOBOX_SELECTED = '<>' - - -def mesh_filter_check(filter): - """ - Check that the filter is a usable mesh filter - """ - return isinstance(filter, MeshFilter) and not isinstance(filter.mesh, UnstructuredMesh) - - -class MeshPlotter(tk.Frame): - def __init__(self, parent, filename): - super().__init__(parent) - - self.labels = { - 'Cell': 'Cell:', - 'Cellborn': 'Cell born:', - 'Surface': 'Surface:', - 'Material': 'Material:', - 'Universe': 'Universe:', - 'Energy': 'Energy in:', - 'Energyout': 'Energy out:' - } - - self.filterBoxes = {} - - # Read data from source or leakage fraction file - self.get_file_data(filename) - - # Set up top-level window - top = self.winfo_toplevel() - top.title('Mesh Tally Plotter: ' + filename) - top.rowconfigure(0, weight=1) - top.columnconfigure(0, weight=1) - self.grid(sticky=tk.W+tk.N) - - # Create widgets and draw to screen - self.create_widgets() - self.update() - - def create_widgets(self): - figureFrame = tk.Frame(self) - figureFrame.grid(row=0, column=0) - - # Create the Figure and Canvas - self.dpi = 100 - self.fig = Figure((5.0, 5.0), dpi=self.dpi) - self.canvas = FigureCanvasTkAgg(self.fig, master=figureFrame) - self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1) - - # Create the navigation toolbar, tied to the canvas - self.mpl_toolbar = NavigationToolbar2Tk(self.canvas, figureFrame) - self.mpl_toolbar.update() - self.canvas._tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=1) - - # Create frame for comboboxes - self.selectFrame = tk.Frame(self) - self.selectFrame.grid(row=1, column=0, sticky=tk.W+tk.E) - - # Tally selection - labelTally = tk.Label(self.selectFrame, text='Tally:') - labelTally.grid(row=0, column=0, sticky=tk.W) - self.tallyBox = ttk.Combobox(self.selectFrame, state='readonly') - self.tallyBox['values'] = [self.datafile.tallies[i].id - for i in self.meshTallies] - self.tallyBox.current(0) - self.tallyBox.grid(row=0, column=1, sticky=tk.W+tk.E) - self.tallyBox.bind(_COMBOBOX_SELECTED, self.update) - - # Planar basis selection - labelBasis = tk.Label(self.selectFrame, text='Basis:') - labelBasis.grid(row=1, column=0, sticky=tk.W) - self.basisBox = ttk.Combobox(self.selectFrame, state='readonly') - self.basisBox['values'] = ('xy', 'yz', 'xz') - self.basisBox.current(0) - self.basisBox.grid(row=1, column=1, sticky=tk.W+tk.E) - self.basisBox.bind(_COMBOBOX_SELECTED, self.update) - - # Axial level - labelAxial = tk.Label(self.selectFrame, text='Axial level:') - labelAxial.grid(row=2, column=0, sticky=tk.W) - self.axialBox = ttk.Combobox(self.selectFrame, state='readonly') - self.axialBox.grid(row=2, column=1, sticky=tk.W+tk.E) - self.axialBox.bind(_COMBOBOX_SELECTED, self.redraw) - - # Option for mean/uncertainty - labelMean = tk.Label(self.selectFrame, text='Mean/Uncertainty:') - labelMean.grid(row=3, column=0, sticky=tk.W) - self.meanBox = ttk.Combobox(self.selectFrame, state='readonly') - self.meanBox['values'] = ('Mean', 'Absolute uncertainty', - 'Relative uncertainty') - self.meanBox.current(0) - self.meanBox.grid(row=3, column=1, sticky=tk.W+tk.E) - self.meanBox.bind(_COMBOBOX_SELECTED, self.update) - - # Scores - labelScore = tk.Label(self.selectFrame, text='Score:') - labelScore.grid(row=4, column=0, sticky=tk.W) - self.scoreBox = ttk.Combobox(self.selectFrame, state='readonly') - self.scoreBox.grid(row=4, column=1, sticky=tk.W+tk.E) - self.scoreBox.bind(_COMBOBOX_SELECTED, self.redraw) - - # Filter label - boldfont = font.Font(weight='bold') - labelFilters = tk.Label(self.selectFrame, text='Filters:', - font=boldfont) - labelFilters.grid(row=5, column=0, sticky=tk.W) - - def update(self, event=None): - widget = event.widget if event else None - - tally_id = self.meshTallies[self.tallyBox.current()] - selectedTally = self.datafile.tallies[tally_id] - - # Get mesh for selected tally - self.mesh = selectedTally.find_filter(MeshFilter).mesh - - # Get mesh dimensions - if len(self.mesh.dimension) == 2: - self.nx, self.ny = self.mesh.dimension - self.nz = 1 - else: - self.nx, self.ny, self.nz = self.mesh.dimension - - # Repopulate comboboxes baesd on current basis selection - text = self.basisBox.get() - if text == 'xy': - self.axialBox['values'] = [str(i+1) for i in range(self.nz)] - elif text == 'yz': - self.axialBox['values'] = [str(i+1) for i in range(self.nx)] - else: - self.axialBox['values'] = [str(i+1) for i in range(self.ny)] - self.axialBox.current(0) - - # If update() was called by a change in the basis combobox, we don't - # need to repopulate the filters - if widget == self.basisBox: - self.redraw() - return - - # Update scores - self.scoreBox['values'] = selectedTally.scores - self.scoreBox.current(0) - - # Remove any filter labels/comboboxes that exist - for row in range(6, self.selectFrame.grid_size()[1]): - for w in self.selectFrame.grid_slaves(row=row): - w.grid_forget() - w.destroy() - - # create a label/combobox for each filter in selected tally - count = 0 - for f in selectedTally.filters: - filterType = f.short_name - if filterType == 'Mesh': - continue - count += 1 - - # Create label and combobox for this filter - label = tk.Label(self.selectFrame, text=self.labels[filterType]) - label.grid(row=count+6, column=0, sticky=tk.W) - combobox = ttk.Combobox(self.selectFrame, state='readonly') - self.filterBoxes[filterType] = combobox - - # Set combobox items - if filterType in ['Energy', 'Energyout']: - combobox['values'] = ['{} to {}'.format(*ebin) - for ebin in f.bins] - else: - combobox['values'] = [str(i) for i in f.bins] - - combobox.current(0) - combobox.grid(row=count+6, column=1, sticky=tk.W+tk.E) - combobox.bind(_COMBOBOX_SELECTED, self.redraw) - - # If There are no filters, leave a 'None available' message - if count == 0: - count += 1 - label = tk.Label(self.selectFrame, text="None Available") - label.grid(row=count+6, column=0, sticky=tk.W) - - self.redraw() - - def redraw(self, event=None): - basis = self.basisBox.current() + 1 - axial_level = self.axialBox.current() + 1 - mbvalue = self.meanBox.get() - - # Get selected tally - tally_id = self.meshTallies[self.tallyBox.current()] - selectedTally = self.datafile.tallies[tally_id] - - # Create spec_list - spec_list = [] - for f in selectedTally.filters: - if f.short_name == 'Mesh': - mesh_filter = f - continue - elif f.short_name in ['Energy', 'Energyout']: - index = self.filterBoxes[f.short_name].current() - ebin = f.bins[index] - spec_list.append((type(f), (ebin,))) - else: - index = self.filterBoxes[f.short_name].current() - spec_list.append((type(f), (index,))) - - dims = (self.nx, self.ny, self.nz) - - text = self.basisBox.get() - if text == 'xy': - h_ind = 0 - v_ind = 1 - elif text == 'yz': - h_ind = 1 - v_ind = 2 - else: - h_ind = 0 - v_ind = 2 - - axial_ind = 3 - (h_ind + v_ind) - dims = (dims[h_ind], dims[v_ind]) - - mesh_dim = len(self.mesh.dimension) - if mesh_dim == 3: - mesh_indices = [0,0,0] - else: - mesh_indices = [0,0] - - matrix = np.zeros(dims) - for i in range(dims[0]): - for j in range(dims[1]): - if mesh_dim == 3: - mesh_indices[h_ind] = i + 1 - mesh_indices[v_ind] = j + 1 - mesh_indices[axial_ind] = axial_level - else: - mesh_indices[0] = i + 1 - mesh_indices[1] = j + 1 - filters, filter_bins = zip(*spec_list + [ - (type(mesh_filter), (tuple(mesh_indices),))]) - mean = selectedTally.get_values( - [self.scoreBox.get()], filters, filter_bins) - stdev = selectedTally.get_values( - [self.scoreBox.get()], filters, filter_bins, - value='std_dev') - if mbvalue == 'Mean': - matrix[i, j] = mean - elif mbvalue == 'Absolute uncertainty': - matrix[i, j] = stdev - else: - if mean > 0.: - matrix[i, j] = stdev/mean - else: - matrix[i, j] = 0. - - # Clear the figure - self.fig.clear() - - # Make figure, set up color bar - self.axes = self.fig.add_subplot(111) - cax = self.axes.imshow(matrix.transpose(), vmin=0.0, vmax=matrix.max(), - interpolation='none', origin='lower') - self.fig.colorbar(cax) - - self.axes.set_xticks([]) - self.axes.set_yticks([]) - self.axes.set_aspect('equal') - - # Draw canvas - self.canvas.draw() - - def get_file_data(self, filename): - # Create StatePoint object and read in data - self.datafile = StatePoint(filename) - - meshes = self.datafile.meshes - if any(isinstance(m, UnstructuredMesh) for m in meshes.values()): - warn_msg = "Unstructured meshes are present in the" \ - " statepoint file but are not currently" \ - " supported by this script" - messagebox.showwarning("Unstructured Meshes", message=warn_msg) - - # Find which tallies are mesh tallies - self.meshTallies = [] - for itally, tally in self.datafile.tallies.items(): - if any([mesh_filter_check(f) for f in tally.filters]): - self.meshTallies.append(itally) - - if not self.meshTallies: - messagebox.showerror("Invalid StatePoint File", - "File does not contain mesh tallies!") - sys.exit(1) - - -if __name__ == '__main__': - parser = argparse.ArgumentParser() - parser.add_argument('statepoint', nargs='?', help='Statepoint file') - args = parser.parse_args() - - # Hide root window - root = tk.Tk() - root.withdraw() - - # If no filename given as command-line argument, open file dialog - if args.statepoint is None: - filename = filedialog.askopenfilename(title='Select statepoint file', - initialdir='.') - else: - filename = args.statepoint - - if filename: - # Check to make sure file exists - if not os.path.isfile(filename): - messagebox.showerror("File not found", - "Could not find regular file: " + filename) - sys.exit(1) - - app = MeshPlotter(root, filename) - root.deiconify() - root.mainloop() diff --git a/scripts/openmc-track-combine b/scripts/openmc-track-combine deleted file mode 100755 index f69f2151c68..00000000000 --- a/scripts/openmc-track-combine +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python3 - -"""Combine multiple HDF5 particle track files.""" - -import argparse - -import openmc - - -def main(): - # Parse command-line arguments - parser = argparse.ArgumentParser( - description='Combine particle track files into a single .h5 file.') - parser.add_argument('input', metavar='IN', nargs='+', - help='Input HDF5 particle track filename(s).') - parser.add_argument('-o', '--out', metavar='OUT', default='tracks.h5', - help='Output HDF5 particle track file.') - - args = parser.parse_args() - openmc.Tracks.combine(args.input, args.out) - - -if __name__ == '__main__': - main() diff --git a/scripts/openmc-track-to-vtk b/scripts/openmc-track-to-vtk deleted file mode 100755 index 82439bea738..00000000000 --- a/scripts/openmc-track-to-vtk +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env python3 - -"""Convert HDF5 particle track to VTK poly data. - -""" - -import argparse - -import openmc -import vtk - - -def _parse_args(): - # Create argument parser. - parser = argparse.ArgumentParser( - description='Convert particle track file(s) to a .pvtp file.') - parser.add_argument('input', metavar='IN', type=str, - help='Input particle track data filename.') - parser.add_argument('-o', '--out', metavar='OUT', type=str, dest='out', - help='Output VTK poly data filename.') - - # Parse and return commandline arguments. - return parser.parse_args() - - -def main(): - # Parse commandline arguments. - args = _parse_args() - - # Make sure that the output filename ends with '.pvtp'. - if not args.out: - args.out = 'tracks.pvtp' - elif not args.out.endswith('.pvtp'): - args.out += '.pvtp' - - # Write coordinate values to points array. - track_file = openmc.Tracks(args.input) - track_file.write_to_vtk(args.out) - -if __name__ == '__main__': - main() diff --git a/scripts/openmc-update-inputs b/scripts/openmc-update-inputs deleted file mode 100755 index 5ee1f1ca0bc..00000000000 --- a/scripts/openmc-update-inputs +++ /dev/null @@ -1,307 +0,0 @@ -#!/usr/bin/env python3 -"""Update OpenMC's input XML files to the latest format.""" - -import argparse -from itertools import chain -from random import randint -from shutil import move -import xml.etree.ElementTree as ET - -import openmc.data - - -description = "Update OpenMC's input XML files to the latest format." -epilog = """\ -If any of the given files do not match the most up-to-date formatting, then they -will be automatically rewritten. The old out-of-date files will not be deleted; -they will be moved to a new file with '.original' appended to their name. - -Formatting changes that will be made: - -geometry.xml: Lattices containing 'outside' attributes/tags will be replaced - with lattices containing 'outer' attributes, and the appropriate - cells/universes will be added. Any 'surfaces' attributes/elements on a cell - will be renamed 'region'. - -materials.xml: Nuclide names will be changed from ACE aliases (e.g., Am-242m) to - HDF5/GNDS names (e.g., Am242_m1). Thermal scattering table names will be - changed from ACE aliases (e.g., HH2O) to HDF5/GNDS names (e.g., c_H_in_H2O). - -""" - - -def parse_args(): - """Read the input files from the commandline.""" - # Create argument parser. - parser = argparse.ArgumentParser( - description=description, - epilog=epilog, - formatter_class=argparse.RawTextHelpFormatter) - parser.add_argument('input', metavar='IN', type=str, nargs='+', - help='Input XML file(s).') - - # Parse and return commandline arguments. - return parser.parse_args() - - -def get_universe_ids(geometry_root): - """Return a set of universe id numbers.""" - root = geometry_root - out = set() - - # Get the ids of universes defined by cells. - for cell in root.iter('cell'): - # Get universe attributes/elements - if 'universe' in cell.attrib: - uid = cell.attrib['universe'] - out.add(int(uid)) - elif cell.find('universe') is not None: - elem = cell.find('universe') - uid = elem.text - out.add(int(uid)) - else: - # Default to universe 0 - out.add(0) - - # Get the ids of universes defined by lattices. - for lat in root.iter('lattice'): - # Get id attributes. - if 'id' in lat.attrib: - uid = lat.attrib['id'] - out.add(int(uid)) - - # Get id elements. - elif lat.find('id') is not None: - elem = lat.find('id') - uid = elem.text - out.add(int(uid)) - - return out - - -def get_cell_ids(geometry_root): - """Return a set of cell id numbers.""" - root = geometry_root - out = set() - - # Get the ids of universes defined by cells. - for cell in root.iter('cell'): - # Get id attributes. - if 'id' in cell.attrib: - cid = cell.attrib['id'] - out.add(int(cid)) - - # Get id elements. - elif cell.find('id') is not None: - elem = cell.find('id') - cid = elem.text - out.add(int(cid)) - - return out - - -def find_new_id(current_ids, preferred=None): - """Return a new id that is not already present in current_ids.""" - distance_from_preferred = 21 - max_random_attempts = 10000 - - # First, try to find an id near the preferred number. - if preferred is not None: - assert isinstance(preferred, int) - for i in range(1, distance_from_preferred): - if (preferred - i not in current_ids) and (preferred - i > 0): - return preferred - i - if (preferred + i not in current_ids) and (preferred + i > 0): - return preferred + i - - # If that was unsuccessful, attempt to randomly guess a new id number. - for i in range(max_random_attempts): - num = randint(1, 2147483647) - if num not in current_inds: - return num - - # Raise an error if an id was not found. - raise RuntimeError('Could not find a unique id number for a new universe.') - - -def get_lat_id(lattice_element): - """Return the id integer of the lattice_element.""" - assert isinstance(lattice_element, ET.Element) - if 'id' in lattice_element.attrib: - return int(lattice_element.attrib['id'].strip()) - elif any([child.tag == 'id' for child in lattice_element]): - elem = lattice_element.find('id') - return int(elem.text.strip()) - else: - raise RuntimeError('Could not find the id for a lattice.') - - -def pop_lat_outside(lattice_element): - """Return lattice's outside material and remove from attributes/elements.""" - assert isinstance(lattice_element, ET.Element) - - # Check attributes. - if 'outside' in lattice_element.attrib: - material = lattice_element.attrib['outside'].strip() - del lattice_element.attrib['outside'] - - # Check subelements. - elif any([child.tag == 'outside' for child in lattice_element]): - elem = lattice_element.find('outside') - material = elem.text.strip() - lattice_element.remove(elem) - - # No 'outside' specified. This means the outside is a void. - else: - material = 'void' - - return material - - -def update_geometry(geometry_root): - """Update the given XML geometry tree. Return True if changes were made.""" - root = geometry_root - was_updated = False - - # Get a set of already-used universe and cell ids. - uids = get_universe_ids(root) - cids = get_cell_ids(root) - taken_ids = uids.union(cids) - - # Replace 'outside' with 'outer' in lattices. - for lat in chain(root.iter('lattice'), root.iter('hex_lattice')): - # Get the lattice's id. - lat_id = get_lat_id(lat) - - # Ignore lattices that have 'outer' specified. - if any([child.tag == 'outer' for child in lat]): continue - if 'outer' in lat.attrib: continue - - # Pop the 'outside' material. - material = pop_lat_outside(lat) - - # Get an id number for a new outer universe. Ideally, the id should - # be close to the lattice's id. - new_uid = find_new_id(taken_ids, preferred=lat_id) - assert new_uid not in taken_ids - - # Add the new universe filled with the old 'outside' material to the - # geometry. - new_cell = ET.Element('cell') - new_cell.attrib['id'] = str(new_uid) - new_cell.attrib['universe'] = str(new_uid) - new_cell.attrib['material'] = material - root.append(new_cell) - taken_ids.add(new_uid) - - # Add the new universe to the lattice's 'outer' attribute. - lat.attrib['outer'] = str(new_uid) - - was_updated = True - - for surface in root.findall('surface'): - for attribute in ['type', 'coeffs', 'boundary']: - if surface.find(attribute) is not None: - value = surface.find(attribute).text - surface.attrib[attribute] = value - - child_element = surface.find(attribute) - surface.remove(child_element) - was_updated = True - - # Remove 'type' from lattice definitions. - for lat in root.iter('lattice'): - elem = lat.find('type') - if elem is not None: - lat.remove(elem) - was_updated = True - if 'type' in lat.attrib: - del lat.attrib['type'] - was_updated = True - - # Change 'width' to 'pitch' in lattice definitions. - for lat in root.iter('lattice'): - elem = lat.find('width') - if elem is not None: - elem.tag = 'pitch' - was_updated = True - if 'width' in lat.attrib: - lat.attrib['pitch'] = lat.attrib['width'] - del lat.attrib['width'] - was_updated = True - - # Change 'surfaces' to 'region' in cell definitions - for cell in root.iter('cell'): - elem = cell.find('surfaces') - if elem is not None: - elem.tag = 'region' - was_updated = True - if 'surfaces' in cell.attrib: - cell.attrib['region'] = cell.attrib['surfaces'] - del cell.attrib['surfaces'] - was_updated = True - - return was_updated - -def update_materials(root): - """Update the given XML materials tree. Return True if changes were made.""" - was_updated = False - - for material in root.findall('material'): - for nuclide in material.findall('nuclide'): - if 'name' in nuclide.attrib: - nucname = nuclide.attrib['name'].replace('-', '') - # If a nuclide name is in the ZAID notation (e.g., a number), - # convert it to the proper nuclide name. - if nucname.strip().isnumeric(): - nucname = openmc.data.ace.get_metadata(int(nucname))[0] - nucname = nucname.replace('Nat', '0') - if nucname.endswith('m'): - nucname = nucname[:-1] + '_m1' - nuclide.set('name', nucname) - was_updated = True - - elif nuclide.find('name') is not None: - name_elem = nuclide.find('name') - nucname = name_elem.text - nucname = nucname.replace('-', '') - nucname = nucname.replace('Nat', '0') - if nucname.endswith('m'): - nucname = nucname[:-1] + '_m1' - name_elem.text = nucname - was_updated = True - - for sab in material.findall('sab'): - if 'name' in sab.attrib: - sabname = sab.attrib['name'] - sab.set('name', openmc.data.get_thermal_name(sabname)) - was_updated = True - - elif sab.find('name') is not None: - name_elem = sab.find('name') - sabname = name_elem.text - name_elem.text = openmc.data.get_thermal_name(sabname) - was_updated = True - - return was_updated - - -if __name__ == '__main__': - args = parse_args() - for fname in args.input: - # Parse the XML data. - tree = ET.parse(fname) - root = tree.getroot() - was_updated = False - - if root.tag == 'geometry': - was_updated = update_geometry(root) - elif root.tag == 'materials': - was_updated = update_materials(root) - - if was_updated: - # Move the original geometry file to preserve it. - move(fname, fname + '.original') - - # Write a new geometry file. - tree.write(fname, xml_declaration=True) \ No newline at end of file diff --git a/scripts/openmc-update-mgxs b/scripts/openmc-update-mgxs deleted file mode 100755 index aac6959b7e5..00000000000 --- a/scripts/openmc-update-mgxs +++ /dev/null @@ -1,212 +0,0 @@ -#!/usr/bin/env python3 -"""Update OpenMC's deprecated multi-group cross section XML files to the latest -HDF5-based format. - -""" - -import os -import warnings -import xml.etree.ElementTree as ET - -import argparse -import numpy as np - -import openmc.mgxs_library - - -def parse_args(): - """Read the input files from the commandline.""" - # Create argument parser - parser = argparse.ArgumentParser(description=__doc__, - formatter_class=argparse.RawTextHelpFormatter) - parser.add_argument('-i', '--input', type=argparse.FileType('r'), - help='input XML file') - parser.add_argument('-o', '--output', nargs='?', default='', - help='output file, in HDF5 format') - args = vars(parser.parse_args()) - - if args['output'] == '': - filename = args['input'].name - extension = os.path.splitext(filename) - if extension == '.xml': - filename = filename[:filename.rfind('.')] + '.h5' - args['output'] = filename - - # Parse and return commandline arguments. - return args - - -def get_data(element, entry): - value = element.find(entry) - if value is not None: - value = value.text.strip() - elif entry in element.attrib: - value = element.attrib[entry].strip() - else: - value = None - - return value - - -def main(): - args = parse_args() - - # Parse the XML data. - tree = ET.parse(args['input']) - root = tree.getroot() - - # Get old metadata - group_structure = tree.find('group_structure').text.strip() - group_structure = np.array(group_structure.split(), dtype=float) - # Convert from MeV to eV - group_structure *= 1.e6 - energy_groups = openmc.mgxs.EnergyGroups(group_structure) - - inverse_velocity = tree.find('inverse-velocity') - if inverse_velocity is not None: - inverse_velocity = inverse_velocity.text.split() - inverse_velocity = np.array(inverse_velocity, dtype=float) - else: - inverse_velocity = None - - xsd = [] - names = [] - - # Now move on to the cross section data itself - for xsdata_elem in root.iter('xsdata'): - name = get_data(xsdata_elem, 'name') - - temperature = get_data(xsdata_elem, 'kT') - if temperature is not None: - temperature = float(temperature) / openmc.data.K_BOLTZMANN * 1.E6 - else: - temperature = 294. - temperatures = [temperature] - - awr = get_data(xsdata_elem, 'awr') - if awr is not None: - awr = float(awr) - - representation = get_data(xsdata_elem, 'representation') - if representation is None: - representation = 'isotropic' - if representation == 'angle': - n_azi = int(get_data(xsdata_elem, 'num_azimuthal')) - n_pol = int(get_data(xsdata_elem, 'num_polar')) - - scatter_format = get_data(xsdata_elem, 'scatt_type') - if scatter_format is None: - scatter_format = 'legendre' - - order = int(get_data(xsdata_elem, 'order')) - - tab_leg = get_data(xsdata_elem, 'tabular_legendre') - if tab_leg is not None: - warnings.warn('The tabular_legendre option has moved to the ' - 'settings.xml file and must be added manually') - - # Either add the data to a previously existing xsdata (if it is - # for the same 'name' but a different temperature), or create a - # new one. - try: - # It is in our list, so store that entry - i = names.index(name) - except ValueError: - # It is not in our list, so add it - i = -1 - xsd.append(openmc.XSdata(name, energy_groups, - temperatures=temperatures, - representation=representation)) - if awr is not None: - xsd[-1].atomic_weight_ratio = awr - if representation == 'angle': - xsd[-1].num_azimuthal = n_azi - xsd[-1].num_polar = n_pol - xsd[-1].scatter_format = scatter_format - xsd[-1].order = order - names.append(name) - - if scatter_format == 'legendre': - order_dim = order + 1 - else: - order_dim = order - - if i != -1: - xsd[i].add_temperature(temperature) - - total = get_data(xsdata_elem, 'total') - if total is not None: - total = np.array(total.split(), dtype=float) - total.shape = xsd[i].xs_shapes['[G]'] - xsd[i].set_total(total, temperature) - - if inverse_velocity is not None: - xsd[i].set_inverse_velocity(inverse_velocity, temperature) - - absorption = get_data(xsdata_elem, 'absorption') - absorption = np.array(absorption.split(), dtype=float) - absorption.shape = xsd[i].xs_shapes['[G]'] - xsd[i].set_absorption(absorption, temperature) - - scatter = get_data(xsdata_elem, 'scatter') - scatter = np.array(scatter.split(), dtype=float) - # This is now a flattened-array of something that started with a - # shape of [Order][G][G']; we need to unflatten and then switch the - # ordering - in_shape = (order_dim, energy_groups.num_groups, - energy_groups.num_groups) - if representation == 'angle': - in_shape = (n_pol, n_azi) + in_shape - scatter.shape = in_shape - scatter = np.swapaxes(scatter, 2, 3) - scatter = np.swapaxes(scatter, 3, 4) - else: - scatter.shape = in_shape - scatter = np.swapaxes(scatter, 0, 1) - scatter = np.swapaxes(scatter, 1, 2) - - xsd[i].set_scatter_matrix(scatter, temperature) - - multiplicity = get_data(xsdata_elem, 'multiplicity') - if multiplicity is not None: - multiplicity = np.array(multiplicity.split(), dtype=float) - multiplicity.shape = xsd[i].xs_shapes["[G][G']"] - xsd[i].set_multiplicity_matrix(multiplicity, temperature) - - fission = get_data(xsdata_elem, 'fission') - if fission is not None: - fission = np.array(fission.split(), dtype=float) - fission.shape = xsd[i].xs_shapes['[G]'] - xsd[i].set_fission(fission, temperature) - - kappa_fission = get_data(xsdata_elem, 'kappa_fission') - if kappa_fission is not None: - kappa_fission = np.array(kappa_fission.split(), dtype=float) - kappa_fission.shape = xsd[i].xs_shapes['[G]'] - xsd[i].set_kappa_fission(kappa_fission, temperature) - - chi = get_data(xsdata_elem, 'chi') - if chi is not None: - chi = np.array(chi.split(), dtype=float) - chi.shape = xsd[i].xs_shapes['[G]'] - xsd[i].set_chi(chi, temperature) - else: - chi = None - - nu_fission = get_data(xsdata_elem, 'nu_fission') - if nu_fission is not None: - nu_fission = np.array(nu_fission.split(), dtype=float) - if chi is not None: - nu_fission.shape = xsd[i].xs_shapes['[G]'] - else: - nu_fission.shape = xsd[i].xs_shapes["[G][G']"] - xsd[i].set_nu_fission(nu_fission, temperature) - - # Build library as we go, but first we have enough to initialize it - lib = openmc.MGXSLibrary(energy_groups) - lib.add_xsdatas(xsd) - lib.export_to_hdf5(args['output']) - - -if __name__ == '__main__': - main() diff --git a/scripts/openmc-voxel-to-vtk b/scripts/openmc-voxel-to-vtk deleted file mode 100755 index 3f59ffd4262..00000000000 --- a/scripts/openmc-voxel-to-vtk +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python3 - -from argparse import ArgumentParser - -import openmc - - -if __name__ == "__main__": - # Process command line arguments - parser = ArgumentParser('Converts a voxel HDF5 file to a VTK file') - parser.add_argument("voxel_file", help="Path to voxel h5 file") - parser.add_argument( - "-o", - "--output", - action="store", - default="plot", - help="Path to output VTK file.", - ) - args = parser.parse_args() - print("Reading and translating data...") - openmc.voxel_to_vtk(args.voxel_file, args.output) - print(f"Written VTK file {args.output}...") diff --git a/tests/regression_tests/track_output/test.py b/tests/regression_tests/track_output/test.py index 62526117dfb..299e1aa34a4 100644 --- a/tests/regression_tests/track_output/test.py +++ b/tests/regression_tests/track_output/test.py @@ -1,7 +1,6 @@ import glob import os from pathlib import Path -from subprocess import call import numpy as np import openmc @@ -26,8 +25,8 @@ def _get_results(self): # For MPI mode, combine track files if config['mpi']: - call(['../../../scripts/openmc-track-combine', '-o', 'tracks.h5'] + - glob.glob('tracks_p*.h5')) + track_files = list(glob.glob('tracks_p*.h5')) + openmc.Tracks.combine(track_files, 'tracks.h5') # Get string of track file information outstr = '' diff --git a/tests/testing_harness.py b/tests/testing_harness.py index ddae89e02e9..6de475e6e04 100644 --- a/tests/testing_harness.py +++ b/tests/testing_harness.py @@ -1,5 +1,4 @@ from difflib import unified_diff -from subprocess import check_call import filecmp import glob import h5py @@ -487,8 +486,7 @@ def _run_openmc(self): # Check that voxel h5 can be converted to vtk for voxel_h5_filename in self._voxel_convert_checks: - check_call(['../../../scripts/openmc-voxel-to-vtk'] + - glob.glob(voxel_h5_filename)) + openmc.voxel_to_vtk(voxel_h5_filename) def _test_output_created(self): """Make sure *.png has been created.""" From a5b26de041003d30ee097e83c5540f72f912bb67 Mon Sep 17 00:00:00 2001 From: Olek <45364492+yardasol@users.noreply.github.com> Date: Mon, 17 Feb 2025 11:02:51 -0600 Subject: [PATCH 149/184] Random ray consistency changes (#3298) --- openmc/settings.py | 20 +++++++------- src/random_ray/flat_source_domain.cpp | 27 +++++-------------- .../random_ray_adjoint_k_eff/results_true.dat | 2 +- 3 files changed, 17 insertions(+), 32 deletions(-) diff --git a/openmc/settings.py b/openmc/settings.py index 77598b204fc..b29393d9ac6 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -1110,27 +1110,27 @@ def random_ray(self, random_ray: dict): if not isinstance(random_ray, Mapping): raise ValueError(f'Unable to set random_ray from "{random_ray}" ' 'which is not a dict.') - for key in random_ray: + for key, value in random_ray.items(): if key == 'distance_active': - cv.check_type('active ray length', random_ray[key], Real) - cv.check_greater_than('active ray length', random_ray[key], 0.0) + cv.check_type('active ray length', value, Real) + cv.check_greater_than('active ray length', value, 0.0) elif key == 'distance_inactive': - cv.check_type('inactive ray length', random_ray[key], Real) + cv.check_type('inactive ray length', value, Real) cv.check_greater_than('inactive ray length', - random_ray[key], 0.0, True) + value, 0.0, True) elif key == 'ray_source': - cv.check_type('random ray source', random_ray[key], SourceBase) + cv.check_type('random ray source', value, SourceBase) elif key == 'volume_estimator': - cv.check_value('volume estimator', random_ray[key], + cv.check_value('volume estimator', value, ('naive', 'simulation_averaged', 'hybrid')) elif key == 'source_shape': - cv.check_value('source shape', random_ray[key], + cv.check_value('source shape', value, ('flat', 'linear', 'linear_xy')) elif key == 'volume_normalized_flux_tallies': - cv.check_type('volume normalized flux tallies', random_ray[key], bool) + cv.check_type('volume normalized flux tallies', value, bool) elif key == 'adjoint': - cv.check_type('adjoint', random_ray[key], bool) + cv.check_type('adjoint', value, bool) else: raise ValueError(f'Unable to set random ray to "{key}" which is ' 'unsupported by OpenMC') diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 39a50ace60d..098fd17b085 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -118,7 +118,7 @@ void FlatSourceDomain::update_neutron_source(double k_eff) double inverse_k_eff = 1.0 / k_eff; - // Add scattering source + // Add scattering + fission source #pragma omp parallel for for (int64_t sr = 0; sr < n_source_regions_; sr++) { int material = source_regions_.material(sr); @@ -126,35 +126,20 @@ void FlatSourceDomain::update_neutron_source(double k_eff) for (int g_out = 0; g_out < negroups_; g_out++) { double sigma_t = sigma_t_[material * negroups_ + g_out]; double scatter_source = 0.0; + double fission_source = 0.0; for (int g_in = 0; g_in < negroups_; g_in++) { double scalar_flux = source_regions_.scalar_flux_old(sr, g_in); double sigma_s = sigma_s_[material * negroups_ * negroups_ + g_out * negroups_ + g_in]; - scatter_source += sigma_s * scalar_flux; - } - - source_regions_.source(sr, g_out) = scatter_source / sigma_t; - } - } - - // Add fission source -#pragma omp parallel for - for (int64_t sr = 0; sr < n_source_regions_; sr++) { - int material = source_regions_.material(sr); - - for (int g_out = 0; g_out < negroups_; g_out++) { - double sigma_t = sigma_t_[material * negroups_ + g_out]; - double fission_source = 0.0; - - for (int g_in = 0; g_in < negroups_; g_in++) { - double scalar_flux = source_regions_.scalar_flux_old(sr, g_in); double nu_sigma_f = nu_sigma_f_[material * negroups_ + g_in]; double chi = chi_[material * negroups_ + g_out]; + + scatter_source += sigma_s * scalar_flux; fission_source += nu_sigma_f * scalar_flux * chi; } - source_regions_.source(sr, g_out) += - fission_source * inverse_k_eff / sigma_t; + source_regions_.source(sr, g_out) = + (scatter_source + fission_source * inverse_k_eff) / sigma_t; } } diff --git a/tests/regression_tests/random_ray_adjoint_k_eff/results_true.dat b/tests/regression_tests/random_ray_adjoint_k_eff/results_true.dat index 1690d46e966..657c841b561 100644 --- a/tests/regression_tests/random_ray_adjoint_k_eff/results_true.dat +++ b/tests/regression_tests/random_ray_adjoint_k_eff/results_true.dat @@ -1,5 +1,5 @@ k-combined: -1.006640E+00 1.812967E-03 +1.006640E+00 1.812969E-03 tally 1: 6.684129E+00 8.939821E+00 From 81b7388624f70a54b7d0555acde9045c6f6b9935 Mon Sep 17 00:00:00 2001 From: Gavin Ridley Date: Mon, 17 Feb 2025 21:11:54 -0600 Subject: [PATCH 150/184] Raytrace plots (#2655) Co-authored-by: Patrick Shriwise Co-authored-by: Paul Romano --- docs/source/_images/phong_triso.png | Bin 0 -> 170621 bytes docs/source/io_formats/plots.rst | 110 ++- docs/source/pythonapi/base.rst | 3 +- docs/source/releasenotes/0.14.0.rst | 2 +- docs/source/usersguide/plots.rst | 136 +++- include/openmc/cell.h | 2 +- include/openmc/dagmc.h | 3 +- include/openmc/particle.h | 4 - include/openmc/particle_data.h | 14 +- include/openmc/plot.h | 261 ++++++- include/openmc/position.h | 20 +- include/openmc/surface.h | 2 +- openmc/_xml.py | 4 +- openmc/model/model.py | 23 +- openmc/plots.py | 510 +++++++++---- src/dagmc.cpp | 9 +- src/particle.cpp | 7 - src/particle_data.cpp | 44 +- src/plot.cpp | 674 +++++++++++++----- src/position.cpp | 7 - src/surface.cpp | 2 +- .../plot_projections/plots.xml | 45 +- .../plot_projections/results_true.dat | 2 +- .../regression_tests/plot_projections/test.py | 5 +- tests/unit_tests/test_plots.py | 66 +- 25 files changed, 1483 insertions(+), 472 deletions(-) create mode 100644 docs/source/_images/phong_triso.png diff --git a/docs/source/_images/phong_triso.png b/docs/source/_images/phong_triso.png new file mode 100644 index 0000000000000000000000000000000000000000..7f1ac61cab667c9c8d05ed3943d8e85afa967d1d GIT binary patch literal 170621 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�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
  • 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

    N_{XJ7X8HT)gSf#KU2mfqx? z+++ZN?DC>g3;HJHgbbY*51rG3#0Iw0bc9?ggWj_YI3@w)XE@9nzN5c0TcjD%RC&Kn zKjR!3VU3t0j71rSd;$r^lE)#yUc|)wnNsha6AL3=(j!uM+b>9V6bIQ_Zs05qbM$=d zjL{3jy`F}JnKXQ|o%#-Y;qWX{vLh!8Dl|v77D~lR(pfhtq`< zd_uFJFK5SER25{tt7HYx=>DBd?=>=3&ya!F?<0pbt(;T|$;RG!Lfv{2vX}VVBxm=! zx-f_x!r5B>nHYVyruN35Wz=>m30?Bs|R;rkqY7y3nFbY zX}^L7KSCz&%!@wy+sPm}a1e^PslP@}Z7BiID-qeW$)SWMZ`k{3Jl`Ge? zIOvhkB226YV*(|sMdu%D?h+Ku$)?1PLOa=qy8Da9uGM%s&ge`n824qQ?bRUNtm=D8 zXJbkd`DTZwxq9D_;MQzi@SgoBGk%3K&f)CYzXfRX#uewpNu>X&;U-faIr%$mv2@V> zU99MIfBOF>VE*edt<{?R;C%dsUVluGf$xj}!jq}Bcir3ljW_%21x-rPK1>?kCmJ066NFIdMTEi+x3mP-us!2zMDBKS7R#R9N00;_Ci8ER~3ezwx;ApNXB!+? z61^DED)FYH+FY0*ze~%rr8jIrhnjl+S?mEQ;Su9#3DerlB{ z7b^{Rul55BPR;dqcfz zo@~M!;rS~;RmyOhj!42`VnA&?d!Dai5w7jFa7&&4@0BKM9B^n6Erc>>+kh9nCI?Gcgt%ngWeffgdozq5DMeAS@cWbxct1EU4f_ zF9+5+`iKNMka6IE#J{qz3O$)2wm@VQ!5g}#VV@8I!VPJ@1Q)yhTH1D#Hqk~GN|s?m z-M4&EfyWdacu}l^R}ob!`d8Ik1L(fo2BCMgur{EXabz?@&cKZ#33hpseb_F@sZrRN z7u)XeXU;s^5cRq$XVw9JR0?G6%A18}R?^QuU7DHw?=*eSX-MCgY0w$V`ii3DtPFp2 zb8rA2Ym0vP|NpZ(I|!v@;%jMCZ1f9Wgv0ko+Z|DZW5Q?vP7ebzxbZ5JSXk*_8UcX+ za>JFl{Y#g7FHf9^*frM~?VJHW@cla*DVVKi>A zpy;`0`iEXx4iFGY*GXt|PqhZu=FX z!|;1YX{rB)sH)XIF#Dnng?G>|dyw1x4$H#^mT>RO>3VvKo$g68LjFe!aWgU!`t&Ncjgdov;73qc zmalfV5maiST>&~NoK=tX%ZvgwY;g@*}d_qeI(IQo%Ze~#;jj`Vq; z7=s&Yi=;Eb?_nr*>}#asvB`HeSV3!o9#OBlfm6CrU6#~$cYmLeqrJ5QzcnEh!(-$8 zL*cn+MtH{!Q~a)&hfA?P2|)bF9p*GlIN7=(m~hyv z`FYqz_FKlk-2}5A5PuFVORC#!2|P$Gj8!hF=IQ5UCl>?S*QX`wQ2FWU(-m1E$j|VT zIl#&g0KZ%S4D3Ex*2b^y`u|n-l~Gki-P#AF^MG`>z#*keI;6WhloX^p4lOMWl1fQ; zcXxNUbSNbVccbqe$u1&ID; zmw416b~}r9Ko<VG}KndmQjfJu(IY{5*l7Kqs)Q0Uc zRZRI5(EJvJUU#+T84pX9fR;{UYn6=Aamx%d)X1ugvn!xAZ49OMQ$ykic0^X2sB0`3 zaOH_e;=KLkH(A0)iu3zg7X#Y5o0+2#;d$viHY=y)xAy>i_v{V%c^3S0o{OVMytsJJ zsOi}y^loWQn~_8@L`)rU+<-PPV6(294>)(sd2R20St1um0rBQW=Yx;bH@_x7QzC@}-1&9=DKBwB+}pom5o~K5l+sTsXO{?N^}%#%ob~@i z;HuwP90QOGL5)wZo#FjCwX?noJFm8+d3l(-m_lVy;YG3yP-(4zP+L=gP7k>&);cf? zMHOEO@B>1ZP{hBVj*8?u-@hZ!2IB6oFyCc z{)Ogqkow%|s+ma`5UgU0SVzequI!?x)&;g1b>tUABw^H4&XT&HImw(<;ZXPog-*h4 zFaF$c!HP3tBa_;08Oc+5tr<}x!BWx?Vw7_&%K`-wWJC{^)VD8Iw7xS6*8MzGM56%W zkAHBL1dJcDS^@PY8)|wm-B?aPBKr%6tYFBnvRNXiB{Hx*K=4vJLLUQi?z}5+b||d&)rczrt?~OY z0$;9xiH$hW$WOte60D7M{&3h>eu?`Jx$ zc}U|Clq7St1*5+Gat}k604DgXgqg(3RD6UD7_6iYEPM!X@aFf|B!C!B82|bT6d)dI$JQ1uw$=Na40fQc)s9;yO6SJj)>aJxB#6e{ zbfVFCiBHA;Vsx})W#tW%`V(D^%ex7j!1!vNV*^iDV8%hR7On) z-~>GdPY;`!kpp>79}Ml#0j*$55zlP{2RbLju~W&j2o6W4V@~*Bnd3_@gEN zfgD1(1VD=4&+G&O)ycx*JX%^5V$G97U%jlIe`uk`~@bmsONJ5aI+4akV^3t1vfn_xkM#QLJPh?<4P zkP0!sb|}yn+|yv$mJna!uIl3okS*^v?0YuXLNIP1c$|?Y8`Awt_2K=<5j@gk!g4_$ zqv0DHSpY9J0MT^hAa?y`1hHEa$sz~smzl|qjovKDE49wvp zBI@vzJ&V1j52djTWB`iRF*_|U7h&#dofPGo;CDq(m8EN0MFax{9-c!&-)NSAvQZVQ zs3G*EsRR(V?anCtXa3B|^ox)xwGY>adSI`Z^XePuK|onIZb?6?HtI4FDgau&sMzywgzdQY!O%5uLB`1|z(y?|g*#V1l{_Ka%& zK?Fn@w33$&iNhM(u0Za;XKKK!5Fp&(VOpB>=EKFsyQat_Vpwc3q$0=0Lb+^enC_#a zC?B8BfP5Wx!}(c_<++fIP;O$chA2eL&-<9L-$c-)#w4msj9NYh8uFf8QPRnhmi5qjJi`SS>I-{b`q>qX*mE#X=H zpEm6CmAdY3J_S=U`fauVrnk?QJVj8-`_3ArTp+yCe6Lm)zKuN2+X^+*p2DKz$-Tmwq6;PiuifyO;e*1nGDHk!F5IA{tu5x)d2ynK9o-0HVi`GnYc z``2*&8MO;4E$nQ=NXx@G*DX(64 z=H{Bjt*fni-BRbusaE{-1DSma2%tF;vm8JH_DN9eXeg#IT?42?RmG&Zuwucs$Wk5e zi)@vc*bOuB7OY=fn5%#Vya_ecyb^XUA(e(@&4f7z5YF&dU5WR=(}vsgstJcHaC+J1 zc9!H6MiYS4XXo~&4py$YK@3?jMLMGGu1t0bxR$Uzzjw^7fJ}{J{YI}FJ^_Pu4MU`mDikm-O`?Qot_~=+Q8}Yw+XW zIUtz9Z!Kf%9u9Q|12yY9O|jsBkPo44x3zvvz@7$$pmNqv{x>TUgNAPlry!_*SsvVTVv znf7cnUE%o)e#5yWCUu?M1VxC-V8_w4;yYJ;#DprV7}yn;)GXQJrH`7{qG&?kk;GMU z?mA!|Y1ruRQU=gGJ3A`usavtb>}|kT#bI|nOs$Ht8_?RSDbq@T;W4h%wypd5m^Rpb zA2mg>Gc*`Z{?PHT{kO`8{JrwW=OQ>(d>9n$?f@D;yyp1vn|yR_=+OGleZ=a8$23@+ zuYsZ(md7aow3b0fp=Rp9%>|GkpE@P@UUg1R06xif_dmZk9oT;fV-0D-d$G=DAmGk`T4Ids{lCvMdTMuh_$gR?C7FwlqyAWMV$F$=*T zm@pVG_`eWUx3IN1A0^%K*rXLbn{v{|rJ)#&?;lWDkXBaju63OxEfk_o=M-IOLN4J$ z>ZqkT14%p>){5iNWr5{QS(x?6K1mleFaQ`1^>@bmTbTqfBClk%G_}+;7bbI(te9WC zc-Sm%5M^eI#gsHFw6hDQn9vV8U(#jQR6g~Fw+qk|dxVb1OZgIA4raaFj{faANN;XN z&jz%6Ea=*Nj3oWEBFQQVa(A_~GwqVs`N!|yTaL`GtdI@#B8D!^d~mv}{_j|T_vq}v zNk1ErA)HXgV5uQ}Q<4Klhk!e@$XE!(x<7mjzzQtRgAXbx7N^9JWGnPVW9S#L$ zXz#H}orVg?5)D(;P<)@aswm(G;Hf$ax{R;I~ss6t+UnNKlb!EcNUTH=(IsY9KlH+ib}0g4uaMpZof; zr6rgYgCT3&-d^rb&@4E1$>+7^L;^wr^`YfKBxwUhw#-7V1ofd1On(=TSWGC`O*#~S zBU(Zk8Bhv;Fi?$><;C513L4yS@VX`*8Gs^X?z~e=KtNVBNWFPAk&6ozCDw&R*s!(1 zvI^i8pYs&RTbm{(meZs7D7K-3Z&i!*y73F9o@{=8DPgxHAueF&O;0zOsSDHHzu1QM z!dh}{b4DD9HBrCZgaO?7OWuzdW4B21S?D``m0#OVIJh;B$Cy}!$rBTJA^|!4FG)!j7 zGtq&lQ!uulDX9fwQ~((&>RTLYgxy4E&hJ1@T>AadROP}4Co`RN6@uB*w0BFdR!5A3 ztg>wNz%Q94v@o+5)=giNmKQqmg23Np)Y?Uno|Fi`M6dSF3sPc`zV5*5&xTrT^*hfAARk5i`f(tu~?A2mE_R}R-!M+bEVL(NNx>V|X z`Fl9MnKPh<@*F#mUy7w*A%m7_d-Hz!g7?amNHUEN(+=Uw63{LXNK&GIf&UBh-KB8j zT1YSV?PQjKVtm1?9OZ*-gy6bP(SY=x91oyd#izRWVY-Z3-NC#MtJ8;*OjV3A@R(yF@3t~eT(}1)a$oA90Vpy zua@;kN9_Xiw1h2T6A{87O zd$4RsP-|JV!&h+>4>a4SMpo8%8SwfXUkxkukUjUUI>3xPhd55ly8xv)L<2f6SKfBG z*Ky7gJYuLFXrmEP1|XyD`Y3Z}7^bJh3eelgd%zQ=c3LuBN{^vTWWWePEO&#pO3r6% zGqFoVj4X{J_3XUxZoYkj?x6b)9eEOE+;8Nb)L0el>vI>=a&xs}Sp;)JlW3}ltSE8A zJM=|U-#zc>Kk`e5w+%j?XdIpDcb@!0gYv3(2tR$>L_ZJF2n{;l41MF*nJsc2a$W4B(?{#O4Y3p1qJzOk zGKjuzS(w`U@k7QRUBlwxAqfvp3|ktp`|)7$>Cbw3mIE4r^{;B-P6PSgjhpheEKQf_ zI*1MBJ%vZh8tMIYQKiNLzo2fC!N3(3mx6+I$FQvY#Yu&5qd&}@Mzp3DeG7Z&$l-d% z!jsxcZ5Y(ySFkO2yo{}ESuFC{iRdgj8<~KsX2u7DK3&5%_5(1=MjNkxgW= zD>{C5QBqZTsuoUI)0L9bflH!J=_Z#0kx_{q$rCP_W-V8g1TDVNVoicP)Nic$by$}C zERmGB^nKT~oDl_(U|jLJxM9$tfUOWbORKl#B{WG%vC9^+n-`wL#l^dSwD9onAW}#+ z_ygDMAywthS{OG#r}0}Q-X`p*q%gd?4>_@$KkrmcW2dB!6(TPje(K{AK)5lLxo~Rpyk5D87DK;IOcMIEf1xy*J z0`f#O-egmOfr=gP_zixbSnTdja5gg64(~FSmjMY`SV3&SqQ$=}DR^b*D7!W99$7@- z{pLO_zPSolZT@6`{KYvz-{}P|D2ZLEdGY2W@%KKcQ(}}$)>u!FOSlBP)<7j2-`#1G zzKI6mch7PR!iYqUOj56#H|vZ;LrY8KaLazs=Ea|MAPQSwIErke0k5O4M~nju zn3Q_9xF1EEA?li9NM0an2@e;ppfCYVGx-#0eTWaxA5mO5XI+as`iYe>EQ1n2r=`;#392cV=4J;3UPzx@*H22YIY3g9cfIR;Jf#y zndC%bZ4@@IJ%x66Ge4o&6ucKpVF6!a69?+jZleC@YTTLl z<|UXEH~InX?8fK82zv5V{L$%17o8E@D(l!NJUTj{ zeWXY96!-u|xX)o`3q?^3h;3uzW9awQVa$PXXw+|Fc}-Y*qhX~RO`crt!(YD`iqIfP z2D;Eccyxn>|G$t|QmAy7yINYQ@bDV&(B`4wjE8Df+tQp6nB-Om=;_6Trdp z*ZouFX;S(MXkf8vWq&4RK>6~EUIFCxw|!4D=8%$gTDrw(K)zMl@a~jL@-j0qV*+;Y znBp!9QN_`QsE4Bd2cCQ?{CviymjY?n7PwqoMebkPO@o?52DmrJ%Kb|H4&-+o%1Jc8 zSC_?-nvLbMSJu=lxnqIFaE|^wtQ5B9a4FdEI@1@RiuoWF2%{8eg?3oJzhV9O)ZFPa zZiiNsh`WoDmq*F@dMlO+=R+Z-fr)9?my|nrsrhY`Lz;W9atz`KCo{8vy}kYPa7%Fw zPOW{F6WEXG5Up)_7ZnS#MaDHEj(F~jvM;G#qOLxH4Y(*Wzu<6^bn`ES-QVfBL3zIBP z*PgV=Ps|~LN0^(o8~wftt4WzQ$iR_bOuWZemycS?ld%4E9PcTTxjvl#8oHFaQYcZi zpv^W)Ky(@p8x{0Mk{cDJ!jtPUrNT*?MXrN|hm2-q1b1=%Jg3a38dWQbVJ8&ue(id@ zGqrh4=-`Lv>HZRsyXMu}U($%q&id0c-ZZltlZ9)j&^MOOL^MIKU~8ORf8X09E#Gh3 z7Cn7ue;~QxZfcsE#GOMTI9+q8flG&sD9L+!<=$moP~bBs(A(>~z!Nf$2zobg@+2Gd zH(+TE!0pedhQVr8Bs1M2d^kQjHH?<9*3;W9N)k|OC%nv%p6--z#g*Tr5&Kzb_B>p# zun|GepduL#?@HC6Vc}v05*a_&=Z6qG>d{o;11hI{*B7-D4dh7)=0H zUPh7V>*u+-x4v5z6blQTFN0aAbhD59h&KV|eo`*>QzR1CTkkjPDEK|<^m|@DrEG(kD{fwg5Va=dz9y%_wc2mp z!bh`jQgI46-`M46l8`A~$%U-fm4>ZlI!bzRNn#Xk+;=ippQWO?9%1)PtJm)AETrH@ zm@Nq;$d)v;thY;_IcQEbo@saLCg^-u_3kYWNJb09c7J;jhLsp-Xy-fb-xaxu$Xm@I z2az*J>)bSvCRgDZ1HQ0X?q)rKTC)OPI364mZuH|?aX3$mVpR0^k#~Py(k9l{`jM|d zzM%Jz4T^N|IDDl7Ye!1}5+bhbTq`FZSoRa%mq)xW1w83oe>q=A#hEM^W!J*Ej^PH) z;(k#La$%{Wc;2SN@6jUE)SQ$uj|Ckl91Gi;iWu4&8YIj7SESH-+nUOo^-e=+KD7vI z^_{n~U$)}Yd+8Uj51dqy{6u)mPt05r2U*Cc6r-jdXK$x2pnOy+^D>*flnU^_hdRSw z1jmDb5rwm|d{San!sXgRNDSRQ&_=b4_MLl6qK%uN^~R!7l^L&iI2~@4 zDeGw6tkhCG5oXMtMq!l2`gPxSh4Z(Io?CG5sgPU(VzsE=DJPfS;+41FCgfX*Fl-SLxM<>xYQ)O>3|UNsFpwKe1twvdMEfRyj-91?bWWs0^bwsqtx$qgqxCA z#@b4JIkoJ8qXcvah^2^3+W|DPto}7D={S#THiPxi@Mmi-k$7`2?H{+2*fjb41bZxD)ol<09`>mHQtmm2%@`BcJP z%5%6Vg_Gm3xsHWAyn-XCNPpk`an0DIrCf<6&SC83rG12}s~>THW;3*N`-~)JGpgo8O5h&sC|X> zbuL0hb9W|q22@o1oq&RMGTvpadre2GU5 zJc7XWY3X`?2Y$&=)Oig2`ZXZArw*W|hv*IfbW(Z}GapIAItLw55F zDE)TD5KE&|SY<3EZ{e9oveP8j;F6bu-amL>vI9rlFKfxjlt}CYmnz?ko}t00Wq>k} z*p_~XcJvX6vZma1KXOy-!k@AkX4Yu)oWK1(8fX!{yln8HUT)r?sqCO>c-Qg3(?D-I z#IX0ZYMw#ox|c?sHoazfg(9Pk4J#r zu^2kB;odPwDOUjz>c5u_oZ-a^0p(u@u^gl9ethuX94ytNZsfZ%?R$nh&t0DD-gPL? z&uOl&l|fV-EGC@|K1?;3ovr0&(4vM>!gu)H%d%?`aN}vQkovaap>kuhm6m0XNr|)< zmfZte{e(lw%d6E_7^JbL{aOp$)~T1XX+p?+{Kj+UH}<>!K3Qjb^+~9G;myIx@$9nLRZ?u=ghw^M_fYWQ;6as?SNwMiT>ZAu) zkZ*7(p!O)f@GO)}Hp6Xr>^}ei{b5}G`3sfiE)3Xngj8l`dYjQ-dHWIlN=%8Vc!e^j zFFKunw!Z#DE{u37wzBXI>1({u$lg7(esc+vQCvaRic%*!UVO=^3%RyO;`VJ~7^=^b+&R_Fb;GqL2_J#4qeGInKXCRVzu4<_u#^M*0(&C)Xz6jbT!3Xhq^O<4eok3K7>o>+H`>Ocd4*-V!J=nH%xk3JT*FT zo4HR-bC~wbxD!sW4lqM(t|I1AOwkQ0X6UucgLWRrj_9}rATe|DG>k`wJG9&a#2KlT z!6=&M9m*x6fy}-YgyKB|buqtgjBbGI$_O#G;nq`<0!%{o3|U;y5GceppF*X-dzm3? zoujW!wL7{}CSIfUZ)z@GGv*38?CZm# z<~Nm)j$8=CYJT67G)|V%=I6(UtrIO_J`ks^`x^nH7$G_u?w1pYDv_sj#7gBBZi>M_V|48 z3)Pnlp7LSE@hMy~0=Zo#9g3g>SW3O2K!_F!t|*R2y&X>o5R_cU{j)cIF4Dv$KxEwo zWWrl6sXvNcBlw28An)ARd@B1j!Zhd0$^}(kl8n^29B51H_Z%28n&jjsgF=kn@q8De z+}wOA1#xpfltWm+WIxV<9@C{d!E--14G|g8jMvq{vJPV$!4A^?qaN1!ejJYofC&C8 zJ4>3s5sit4e@f*qxT8g|4_qwP@m&>2E+0%^b}T7o)%gr=U$Mw50vD_{5&}L{)TU6m z)+V`WEl{~;J97K*Z84-_oiBMGrK7s-&6WSat$UnMC%i$xvue+x$1K;!@^4x&fT3NZ zc^_rsXi4vJ5XNM2ezktStIHRRUY+0AcupBr-X0CLgvpAs%sCk$ER5pMz<4{RAY)ds zRa~C@imA4WO3b7(TEFwp7~eiOvY3cDMEO0~%^AMM1zIK|gUnfZifa@U>vFl2Zod>N zb(Nl^CzMkrpgF5CGY7jcIY(D76APD?4p=Xsq^?at172_5BNS~rdaLEw-)heA>D0V! zhBmuj`;SUXiq#+jQ-eE=u{R94m0#a~dp(jqzauo;_bktUYc*&6`n8Y?F9|GgTK=e# zV&DG$^2@}!@^>sQ4u00p`o&wePdSAarvchxSmKhMfZ*rTnkt+3GF)Boer(}8>J)B` z{!c3lwCz8CvgT~&hlff+gC6EH6=_Jw^CCs(gqwC4F_VYo>6C&5r?sO=g-(9)QYI_B zX>=)0fzX=)-(M)Cl9vm(RZ#?#4FRhXl|@pR%MskIo?#fY*%C968stbQz0!Fg0M2=v zXuTwAF>driM>KA_%x2fvafeYXWMCKBqKPF4oGOoOU--cioQ9wM7Fxkkd=~653VB9ZU#2f$FpwCQm#8H9-j|R@G!@G zF5iRLnL@i|vG#uM-=;^C3_1m`r2kg3RR}VBi4-$`R56{nL^%y;^QPew76_{wambZQ zF&@QuqM%BS!;B7oko`GcoBN@T{aIR>u>$KIiv9>1I5cV+bYe3L-fxrFYvLRp?i3HeRg)5Jh2$T1$`@=}GA9v5 zGo?nRnOC+=9flw%%uCW$lZ4OE{UW8WSCqO%@B*2y@xS5YZ6MRCP@SEqXy^vkl5t;phbk%yFGC`&7(c2E z88dGTw4HxpHKnE`2UbSOQ`$`gtE-Uv+tkw@>K z5duI>COW|HTXs&3sA%9PAu)0f<35TzyocQ@U8!4Qu@vt!1=t)%z0UiTVd*%kXR zxVhD^zW-%L=DR|XfmfJXQsB#Q)e@APASxWH``2&zV#TW*;g1NI_u*|J42CPXkJQn= z{;wq0{CDUmWN*TU%t2www{Wi4ru-i)K|3>nrPI4&Z1hp0r|dA4^nbOQhs&%<_fB~T z{T@m>J^B=GTX^)kF`7t?+KC6{gU`Iy5jG0H$c_ipN>=5Tqh?*o8t+2$Hm;)@LCMjh zu_RY)y5@y_%g3kn$5CsvhjOuj0t|dr?90!DeP!aIRXJk5OYKW|gH~=KLrqQiZo&4O z+5wOfDv(BmF>|YWa-`KKXYJ2#v%WOl8j8!oYBgc`FNG8uFVx&d#VT(_3 z?d+gGv{PTkOnd2eZ00gxy|41-qM-wqDV-0glE_S(PIkwIBN02t0rK$l=~ZPk%Tc$_ z3m)RE9%yOef%#>1^)fu=v*c&x_%1V1r#kss$fWgNmG=wsm<817GJrUWaOY1uUt@C$ zL`EvI&^l|QxO`r{Bi1SSEaG zulXezI}2Tm?BuWdeutzm4fu`lV`VyydlW6uj$-g$$5&gK%kH4O*45V0PniF6bu~xL z9zS>%y5tuUhh0Vy&dS6jO@&#ezZsMA?`g;wn8mIPvUn)v^bZ1VEJUV+MQ~FElMa;H zWdOnF43Bg%&wB}ziQ@#YCNuT*-I}%8Rz$IibnED(?wD?g5Qz~8)=$}<79#fY@2@V= zS0XbP%x=iUfI|bmq(Yh=v8l-4)nHwaT5+br0+=b0*U`?f2Q>1j>?L++V%|av=K_MY ztJcl)VOSdJ#&>R~>Y;IpdA({rZs2W`afU&{R*@z>Lqr(m8cP#3rpB*qqQA4;A4yV$+ z2Axvo-ukZobkFFFGF_?#FsnbM&<==e01&@wirbaFyH7~4I$>Hj= zaHK{O>%xW8(rn9=Q~1znYud_+NueTp1%F+|#p6#mo1O;38bF-P88R)z{1qXPu-PQ^ zwBcUMzuMSbE~Ulk)i?puR5mU_V5;ow@fs=j2og3sO>*2PY6CH@wZ1i9d4WBY&%hjc zxikpuzr759Ug^lqsWRoSS;^yk);$2~9+yJ3Z-LnWz}|VN-1E9r@qN?O9C6k)e{JeR zJWOX=2~4~s4#SN0(+;-kB1*{0DK?`nsDjF(i$N=v7Im(a{^9CsN(FF3AO+8=hUGSj zoszE`D@jrSehoanXU9m8?Hjw1p~jESk7I z{SNAMp zxH;lof=LBtz5>tyFlFZ4W)`Wx5RlYpFrz(P*FU8GXUJwkLjOknitbX55G5+--OXu3 zjsh{mvd=f3p4iAeXCU2&TB@e4)yK+p&P2dg;bqP_>qkG*$u2%ZW`Hb7gs|y8(^5!_ zj;as{X1T#|O8$U3MV@F_U0IoFU+kxTh>eY14wMR-h@@Fk#%3zfElmV$PYr3hoq9XA z_(4Fl`4;TwmVp3u6LoW?WA~zC!rr%=oR%_6p&@{aaye9*-0%3pQ%~v3gg(pSL-zR_ zK;E;%vwtnBSKwNRr(J^h;ue07(X(%2PhIci-na;d<;s~Y88=rSHX z3pce$6&4-3@=+Vx*{zAXt`e((#J|s>EJg?vK?J{({Wg`NzyL=dtCTP^^htx1#J=3h zn(^A<06kn=-j`pP&&ucXWf~EMo?0rq0v5^>8AG;}P+7L$?s5Eby`5VQ1pK^tyhCO> zhaPsmDbSG=Fz&44tN{gZDT~p+p*;{T*_#b*65}*<6=D@P_$>3I!x;bgb92W*_59U6 zQq2LPRMxCQ2Pz30#1si`BxSQVW7OLD!PaQSY+u7|@pd<2CO^!`7^I(9 zWR(U~u8YX|#LmRq0L`1`@E@GSv@3HszqDwArJR8m8{n2e%6V%Lt1*?9|m~ignx!th&fKg`)!Lj1=xg%PMUJnU+jx3C=JZ5+&f`q0Hj@#GFQ- z#*&in&}iO~ZIy-;oUB}}2y}=4JLeWo^|`qrUu5eH^BjI0;#Dov%FFMJaYrz?JT<)% zxg@9FTK@JGi2m+J?!6fTsGSt!e3YyLw$l;=kf8E1j5Im}%KKL!5ZFkc=^89;MPFX# zdeF4YW^U_1zZstiKH}XbZa@Zh)rqhhi~9}BKYYKFhYi0Ngf%haJDn6&P`&d=5w^0I zBo-EFwe%eaJxj^08E^=J64(c%qlqeo3KX9Sd;h=2BKXo#Sx2w9iK|#&)X5pXn892a zY3Gn=Hxmn`eEj|GACUeBHysa;+XFR_f^>9)vw>m3zqM_lDO1+kcWg|c-_e0yBJ3h3 zId>kS>sSq#o8I*)uP*$!RbcpwDJWUd$GRKey~UN4m%@QVyEL-X z5B1F@w)0&mmhlkT!B!^ zdE@YqyC<^8?8Xq1SRF@NnZPR3JU_+UBeG)E#-af1hxdNB=BsXE^nuHv0)4O@4^*$K8q+{&`Oj?&6Hw1T_#v`g`u` zDA#6BiH`?~0_m;MguBi$#Swj>&ClrU&t*9>PR0hsU&YO8*FmAjo-3ni%n_n3oxCfj zMZGuc@$YmN%v2XJFosIYa?%y5DKxFxYVO0}EQU08&4#D&PtN!%*H5ZQW< zd2ar11(blfd}My*GE#`jn4d0g-&Ubp$x=1fzml z(kfO_D9hncJB?k;%&NZ&=SyhVXc-Gx}FQIY@ALmsq%&6#z5y`=S)Ue4hYp?9FrJ*z%o zdHy{_x_uiJvbi0&Gn_%U58)dV;Bn%=V}yi0CRWqt(`ixRNq|)=b!SxxWS*Sm)Z33t<;!+s6kr2dsoe`8=~q;y^&; z8S(P1tt)9!1ZkU`0Qp6;QBkA)(VIN=J^M-y3~+yMWUxj9qzvHJ5C?91#&LO(0l?6n z5jGO#j)!p(HD%Dh*NFY+$LGfbzX7xQKmFqw905-N yJs#j2{(m3;zrOeXHu`^${~rVTAEQt2e?sK$_)+HpV>|@{{@zL|N|cEiL;nk;Ob(U+ literal 0 HcmV?d00001 diff --git a/docs/source/io_formats/plots.rst b/docs/source/io_formats/plots.rst index c1fa7833083..1a42a4281d7 100644 --- a/docs/source/io_formats/plots.rst +++ b/docs/source/io_formats/plots.rst @@ -7,13 +7,18 @@ Geometry Plotting Specification -- plots.xml Basic plotting capabilities are available in OpenMC by creating a plots.xml file and subsequently running with the ``--plot`` command-line flag. The root element of the plots.xml is simply ```` and any number output plots can be -defined with ```` sub-elements. Two plot types are currently implemented +defined with ```` sub-elements. Four plot types are currently implemented in openMC: * ``slice`` 2D pixel plot along one of the major axes. Produces a PNG image file. * ``voxel`` 3D voxel data dump. Produces an HDF5 file containing voxel xyz position and cell or material id. +* ``wireframe_raytrace`` 2D pixel plot of a three-dimensional view of a + geometry using wireframes around cells or materials and coloring by depth + through each material. +* ``solid_raytrace`` 2D pixel plot of a three-dimensional view of a geometry + with solid colored surfaces of a set of cells or materials. ------------------ @@ -66,21 +71,22 @@ sub-elements: *Default*: None - Required entry :type: - Keyword for type of plot to be produced. Currently only "slice" and "voxel" - plots are implemented. The "slice" plot type creates 2D pixel maps saved in - the PNG file format. The "voxel" plot type produces a binary datafile - containing voxel grid positioning and the cell or material (specified by the - ``color`` tag) at the center of each voxel. Voxel plot files can be - processed into VTK files using the :func:`openmc.voxel_to_vtk` function and - subsequently viewed with a 3D viewer such as VISIT or Paraview. See the - :ref:`io_voxel` for information about the datafile structure. + Keyword for type of plot to be produced. Currently "slice", "voxel", + "wireframe_raytrace", and "solid_raytrace" plots are implemented. The + "slice" plot type creates 2D pixel maps saved in the PNG file format. The + "voxel" plot type produces a binary datafile containing voxel grid + positioning and the cell or material (specified by the ``color`` tag) at the + center of each voxel. Voxel plot files can be processed into VTK files using + the :func:`openmc.voxel_to_vtk` function and subsequently viewed with a 3D + viewer such as VISIT or Paraview. See :ref:`io_voxel` for information about + the datafile structure. .. note:: High-resolution voxel files produced by OpenMC can be quite large, but the equivalent VTK files will be significantly smaller. *Default*: "slice" -```` elements of ``type`` "slice" and "voxel" must contain the ``pixels`` +All ```` elements must contain the ``pixels`` attribute or sub-element: :pixels: @@ -96,7 +102,7 @@ attribute or sub-element: ``width``/``pixels`` along that basis direction may not appear in the plot. - *Default*: None - Required entry for "slice" and "voxel" plots + *Default*: None - Required entry for all plots ```` elements of ``type`` "slice" can also contain the following attributes or sub-elements. These are not used in "voxel" plots: @@ -125,6 +131,11 @@ attributes or sub-elements. These are not used in "voxel" plots: Specifies the custom color for the cell or material. Should be 3 integers separated by spaces. + :xs: + The attenuation coefficient for volume rendering of color in units of + inverse centimeters. Zero corresponds to transparency. Only for plot type + "wireframe_raytrace". + As an example, if your plot is colored by material and you want material 23 to be blue, the corresponding ``color`` element would look like: @@ -191,3 +202,80 @@ attributes or sub-elements. These are not used in "voxel" plots: *Default*: 0 0 0 (black) *Default*: None + +```` elements of ``type`` "wireframe_raytrace" or "solid_raytrace" can contain the +following attributes or sub-elements. + + :camera_position: + Location in 3D Cartesian space the camera is at. + + + *Default*: None - Required for all ``wireframe_raytrace`` or + ``solid_raytrace`` plots + + :look_at: + Location in 3D Cartesian space the camera is looking at. + + + *Default*: None - Required for all ``wireframe_raytrace`` or + ``solid_raytrace`` plots + + :field_of_view: + The horizontal field of view in degrees. Defaults to roughly the same value + as for the human eye. + + *Default*: 70 + + :orthographic_width: + If set to a nonzero value, an orthographic rather than perspective + projection for the camera is employed. An orthographic projection puts out + parallel rays from the camera of a width prescribed here in the horizontal + direction, with the width in the vertical direction decided by the pixel + aspect ratio. + + *Default*: 0 + +```` elements of ``type`` "solid_raytrace" can contain the following attributes or +sub-elements. + + :opaque_ids: + List of integer IDs of cells or materials to be treated as visible in the + plot. Whether the integers are interpreted as cell or material IDs depends + on ``color_by``. + + *Default*: None - Required for all phong plots + + :light_position: + Location in 3D Cartesian space of the light. + + + *Default*: Same location as ``camera_position`` + + :diffuse_fraction: + Fraction of light originating from non-directional sources. If set to one, + the coloring is not influenced by surface curvature, and no shadows appear. + If set to zero, only regions illuminated by the light are not black. + + + *Default*: 0.1 + +```` elements of ``type`` "wireframe_raytrace" can contain the following +attributes or sub-elements. + + :wireframe_color: + RGB value of the wireframe's color + + *Default*: 0, 0, 0 (black) + + :wireframe_thickness: + Integer number of pixels that the wireframe takes up. The value is a radius + of the wireframe. Setting to zero removes any wireframing. + + *Default*: 0 + + :wireframe_ids: + Integer IDs of cells or materials of regions to draw wireframes around. + Whether the integers are interpreted as cell or material IDs depends on + ``color_by``. + + *Default*: None diff --git a/docs/source/pythonapi/base.rst b/docs/source/pythonapi/base.rst index 23df02f2ee7..d6d04444f2a 100644 --- a/docs/source/pythonapi/base.rst +++ b/docs/source/pythonapi/base.rst @@ -165,7 +165,8 @@ Geometry Plotting :template: myclass.rst openmc.Plot - openmc.ProjectionPlot + openmc.WireframeRayTracePlot + openmc.SolidRayTracePlot openmc.Plots Running OpenMC diff --git a/docs/source/releasenotes/0.14.0.rst b/docs/source/releasenotes/0.14.0.rst index 445edfa077a..f82aa30850d 100644 --- a/docs/source/releasenotes/0.14.0.rst +++ b/docs/source/releasenotes/0.14.0.rst @@ -52,7 +52,7 @@ Compatibility Notes and Deprecations New Features ------------ -- A new :class:`openmc.ProjectionPlot` class enables the generation of orthographic or +- A new :class:`openmc.WireframeRayTracePlot` class enables the generation of orthographic or perspective projection plots. (`#1926 `_) - The :class:`openmc.model.RightCircularCylinder` class now supports optional diff --git a/docs/source/usersguide/plots.rst b/docs/source/usersguide/plots.rst index d453ea4537a..da0c69bdd8d 100644 --- a/docs/source/usersguide/plots.rst +++ b/docs/source/usersguide/plots.rst @@ -122,27 +122,82 @@ for doing this will depend on the 3D viewer, but should be straightforward. million or so). Thus if you want an accurate picture that renders smoothly, consider using only one voxel in a certain direction. ----------------- -Projection Plots ----------------- +---------------------- +Solid Ray-traced Plots +---------------------- + +.. image:: ../_images/phong_triso.png + :width: 300px + +The :class:`openmc.SolidRayTracePlot` class allows three dimensional +visualization of detailed geometric features without voxelization. The plot +above visualizes a geometry created by :class:`openmc.TRISO`, with the materials +in the fuel kernel distinguished by color. It was enclosed in a bounding box +such that some kernels are cut off, revealing the inner structure of the kernel. + +The `Phong reflection model +`_ approximates how light +reflects off of a surface. On a diffusely light-scattering material, the Phong +model prescribes the amount of light reflected from a surface as proportional to +the dot product between the normal vector of the surface and the vector between +that point on the surface and the light. With this assumption, visually +appealing plots of simulation geometries can be created. + +Solid ray-traced plots use the same ray tracing functions that neutrons and +photons do in OpenMC, so any input that does not leak particles can be +visualized in 3D using a solid ray-traced plot. That being said, these plots are +not useful for detecting overlap or undefined regions, so it is recommended to +use the slice plot approach for geometry debugging. + +Only a few inputs are required for a solid ray-traced plot. The camera location, +where the camera is looking, and a set of opaque material or cell IDs are +required. The colors of materials or cells are prescribed in the same way as +slice plots. The set of IDs that are opaque in the plot must correspond to +materials if coloring by material, or cells if coloring by cell. + +A minimal solid ray-traced plot input could be:: + + plot = openmc.SolidRayTracePlot() + plot.pixels = (600, 600) + plot.camera_position = (10.0, 20.0, -30.0) + plot.look_at = (4.0, 5.0, 1.0) + plot.color_by = 'cell' + + # optional. defaults to camera_position + plot.light_position = (10, 20, 30) + + # controls ambient lighting. Defaults to 10% + plot.diffuse_fraction = 0.1 + plot.opaque_domains = [cell2, cell3] + +These plots are then stored into a :class:`openmc.Plots` instance, just like the +slice plots. + +--------------- +Wireframe Plots +--------------- .. only:: html .. image:: ../_images/hexlat_anim.gif :width: 200px -The :class:`openmc.ProjectionPlot` class presents an alternative method of -producing 3D visualizations of OpenMC geometries. It was developed to overcome -the primary shortcoming of voxel plots, that an enormous number of voxels must -be employed to capture detailed geometric features. Projection plots perform -volume rendering on material or cell volumes, with colors specified in the same -manner as slice plots. This is done using the native ray tracing capabilities +The :class:`openmc.WireframeRayTracePlot` class also produces 3D visualizations +of OpenMC geometries without voxelization but is intended to show the inside of +a model using wireframing of cell or material boundaries in addition to cell +coloring based on the path length of camera rays through the model. The coloring +in these plots is a bit like turning the model into partially transparent +colored glass that can be seen through, without any refractive effects. This is +called volume rendering. The colors are specified in exactly the same interface +employed by slice plots. + +Similar to solid ray-traced plots, these use the native ray tracing capabilities within OpenMC, so any geometry in which particles successfully run without -overlaps or leaks will work with projection plots. +overlaps or leaks will work with wireframe plots. -One drawback of projection plots is that particle tracks cannot be overlaid on +One drawback of wireframe plots is that particle tracks cannot be overlaid on them at present. Moreover, checking for overlap regions is not currently -possible with projection plots. The image heading this section can be created by +possible with wireframe plots. The image heading this section can be created by adding the following code to the hexagonal lattice example packaged with OpenMC, before exporting to plots.xml. @@ -152,7 +207,7 @@ before exporting to plots.xml. import numpy as np for i in range(100): phi = 2 * np.pi * i/100 - thisp = openmc.ProjectionPlot(plot_id = 4 + i) + thisp = openmc.WireframeRayTracePlot(plot_id = 4 + i) thisp.filename = 'frame%s'%(str(i).zfill(3)) thisp.look_at = [0, 0, 0] thisp.camera_position = [r * np.cos(phi), r * np.sin(phi), 6 * np.sin(phi)] @@ -167,42 +222,45 @@ before exporting to plots.xml. plot_file.append(thisp) -This generates a sequence of png files which can be joined to form a gif. Each +This generates a sequence of png files that can be joined to form a gif. Each image specifies a different camera position using some simple periodic functions -to create a perfectly looped gif. :attr:`ProjectionPlot.look_at` defines where -the camera's centerline should point at. :attr:`ProjectionPlot.camera_position` -similarly defines where the camera is situated in the universe level we seek to -plot. The other settings resemble those employed by :class:`openmc.Plot`, with -the exception of the :class:`ProjectionPlot.set_transparent` method and -:attr:`ProjectionPlot.xs` dictionary. These are used to control volume rendering -of material volumes. "xs" here stands for cross section, and it defines material -opacities in units of inverse centimeters. Setting this value to a large number -would make a material or cell opaque, and setting it to zero makes a material -transparent. Thus, the :class:`ProjectionPlot.set_transparent` can be used to -make all materials in the geometry transparent. From there, individual material -or cell opacities can be tuned to produce the desired result. +to create a perfectly looped gif. :attr:`~WireframeRayTracePlot.look_at` defines +where the camera's centerline should point at. +:attr:`~WireframeRayTracePlot.camera_position` similarly defines where the +camera is situated in the universe level we seek to plot. The other settings +resemble those employed by :class:`openmc.Plot`, with the exception of the +:meth:`~WireframeRayTracePlot.set_transparent` method and +:attr:`~WireframeRayTracePlot.xs` dictionary. These are used to control volume +rendering of material volumes. "xs" here stands for cross section, and it +defines material opacities in units of inverse centimeters. Setting this value +to a large number would make a material or cell opaque, and setting it to zero +makes a material transparent. Thus, the +:meth:`~WireframeRayTracePlot.set_transparent` method can be used to make all +materials in the geometry transparent. From there, individual material or cell +opacities can be tuned to produce the desired result. Two camera projections are available when using these plots, perspective and orthographic. The default, perspective projection, is a cone of rays passing through each pixel which radiate from the camera position and span the field of view in the x and y positions. The horizontal field of view can be set with the -:attr: `ProjectionPlot.horizontal_field_of_view` attribute, which is to be -specified in units of degrees. The field of view only influences behavior in +:attr:`~WireframeRayTracePlot.horizontal_field_of_view` attribute, which is to +be specified in units of degrees. The field of view only influences behavior in perspective projection mode. In the orthographic projection, rays follow the same angle but originate from different points. The horizontal width of this plane of ray starting points may -be set with the :attr: `ProjectionPlot.orthographic_width` element. If this -element is nonzero, the orthographic projection is employed. Left to its default -value of zero, the perspective projection is employed. - -Lastly, projection plots come packaged with wireframe generation that can target -either all surface/cell/material boundaries in the geometry, or only wireframing -around specific regions. In the above example, we have set only the fuel region -from the hexagonal lattice example to have a wireframe drawn around it. This is -accomplished by setting the :attr: `ProjectionPlot.wireframe_domains`, which may -be set to either material IDs or cell IDs. The -:attr:`ProjectionPlot.wireframe_thickness` attribute sets the wireframe +be set with the :attr:`~WireframeRayTracePlot.orthographic_width` attribute. If +this element is nonzero, the orthographic projection is employed. Left to its +default value of zero, the perspective projection is employed. + +Most importantly, wireframe plots come packaged with wireframe generation that +can target either all surface/cell/material boundaries in the geometry, or only +wireframing around specific regions. In the above example, we have set only the +fuel region from the hexagonal lattice example to have a wireframe drawn around +it. This is accomplished by setting the +:attr:`~WireframeRayTracePlot.wireframe_domains` attribute, which may be set to +either material IDs or cell IDs. The +:attr:`~WireframeRayTracePlot.wireframe_thickness` attribute sets the wireframe thickness in units of pixels. .. note:: When setting specific material or cell regions to have wireframes diff --git a/include/openmc/cell.h b/include/openmc/cell.h index d01020f8e7a..d7a3cad182b 100644 --- a/include/openmc/cell.h +++ b/include/openmc/cell.h @@ -115,7 +115,7 @@ class Region { //! //! Uses the comobination of half-spaces and binary operators to determine //! if short circuiting can be used. Short cicuiting uses the relative and - //! absolute depth of parenthases in the expression. + //! absolute depth of parentheses in the expression. bool contains_complex(Position r, Direction u, int32_t on_surface) const; //! BoundingBox if the paritcle is in a simple cell. diff --git a/include/openmc/dagmc.h b/include/openmc/dagmc.h index 47fcfe237e3..9c4e47fdfd3 100644 --- a/include/openmc/dagmc.h +++ b/include/openmc/dagmc.h @@ -49,7 +49,8 @@ class DAGSurface : public Surface { double evaluate(Position r) const override; double distance(Position r, Direction u, bool coincident) const override; Direction normal(Position r) const override; - Direction reflect(Position r, Direction u, GeometryState* p) const override; + Direction reflect( + Position r, Direction u, GeometryState* p = nullptr) const override; inline void to_hdf5_inner(hid_t group_id) const override {}; diff --git a/include/openmc/particle.h b/include/openmc/particle.h index b1f9fabd110..aaac864e11e 100644 --- a/include/openmc/particle.h +++ b/include/openmc/particle.h @@ -39,10 +39,6 @@ class Particle : public ParticleData { double speed() const; - //! moves the particle by the distance length to its next location - //! \param length the distance the particle is moved - void move_distance(double length); - //! create a secondary particle // //! stores the current phase space attributes of the particle in the diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index 2255d1a7d88..e2fac79e372 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -191,6 +191,13 @@ struct BoundaryInfo { array lattice_translation {}; //!< which way lattice indices will change + void reset() + { + distance = INFINITY; + surface = SURFACE_NONE; + coord_level = 0; + lattice_translation = {0, 0, 0}; + } // TODO: off-by-one int surface_index() const { return std::abs(surface) - 1; } }; @@ -226,6 +233,12 @@ class GeometryState { n_coord_last_ = 1; } + //! moves the particle by the specified distance to its next location + //! \param distance the distance the particle is moved + void move_distance(double distance); + + void advance_to_boundary_from_void(); + // Initialize all internal state from position and direction void init_from_r_u(Position r_a, Direction u_a) { @@ -565,7 +578,6 @@ class ParticleData : public GeometryState { int& cell_born() { return cell_born_; } const int& cell_born() const { return cell_born_; } - // index of the current and last material // Total number of collisions suffered by particle int& n_collision() { return n_collision_; } const int& n_collision() const { return n_collision_; } diff --git a/include/openmc/plot.h b/include/openmc/plot.h index ee6c3fbec58..69e801c5fc3 100644 --- a/include/openmc/plot.h +++ b/include/openmc/plot.h @@ -4,6 +4,7 @@ #include #include #include +#include #include "pugixml.hpp" #include "xtensor/xarray.hpp" @@ -61,6 +62,14 @@ struct RGBColor { return red == other.red && green == other.green && blue == other.blue; } + RGBColor& operator*=(const double x) + { + red *= x; + green *= x; + blue *= x; + return *this; + } + // Members uint8_t red, green, blue; }; @@ -70,9 +79,14 @@ const RGBColor WHITE {255, 255, 255}; const RGBColor RED {255, 0, 0}; const RGBColor BLACK {0, 0, 0}; -/* - * PlottableInterface classes just have to have a unique ID in the plots.xml - * file, and guarantee being able to create output in some way. +/** + * \class PlottableInterface + * \brief Interface for plottable objects. + * + * PlottableInterface classes must have a unique ID in the plots.xml file. + * They guarantee the ability to create output in some form. This interface + * is designed to be implemented by classes that produce plot-relevant data + * which can be visualized. */ class PlottableInterface { private: @@ -232,8 +246,8 @@ T SlicePlotBase::get_map() const data.set_overlap(y, x); } } // inner for - } // outer for - } // omp parallel + } + } return data; } @@ -268,32 +282,110 @@ class Plot : public PlottableInterface, public SlicePlotBase { RGBColor meshlines_color_; //!< Color of meshlines on the plot }; -class ProjectionPlot : public PlottableInterface { - +/** + * \class RaytracePlot + * \brief Base class for plots that generate images through ray tracing. + * + * This class serves as a base for plots that create their visuals by tracing + * rays from a camera through the problem geometry. It inherits from + * PlottableInterface, ensuring that it provides an implementation for + * generating output specific to ray-traced visualization. WireframeRayTracePlot + * and SolidRayTracePlot provide concrete implementations of this class. + */ +class RayTracePlot : public PlottableInterface { public: - ProjectionPlot(pugi::xml_node plot); + RayTracePlot(pugi::xml_node plot); + + // Standard getters. No setting since it's done from XML. + const Position& camera_position() const { return camera_position_; } + const Position& look_at() const { return look_at_; } + const double& horizontal_field_of_view() const + { + return horizontal_field_of_view_; + } - virtual void create_output() const; virtual void print_info() const; -private: +protected: + Direction camera_x_axis() const + { + return {camera_to_model_[0], camera_to_model_[3], camera_to_model_[6]}; + } + + Direction camera_y_axis() const + { + return {camera_to_model_[1], camera_to_model_[4], camera_to_model_[7]}; + } + + Direction camera_z_axis() const + { + return {camera_to_model_[2], camera_to_model_[5], camera_to_model_[8]}; + } + void set_output_path(pugi::xml_node plot_node); + + /* + * Gets the starting position and direction for the pixel corresponding + * to this horizontal and vertical position. + */ + std::pair get_pixel_ray(int horiz, int vert) const; + + std::array pixels_; // pixel dimension of resulting image + +private: void set_look_at(pugi::xml_node node); void set_camera_position(pugi::xml_node node); void set_field_of_view(pugi::xml_node node); void set_pixels(pugi::xml_node node); - void set_opacities(pugi::xml_node node); void set_orthographic_width(pugi::xml_node node); + + double horizontal_field_of_view_ {70.0}; // horiz. f.o.v. in degrees + Position camera_position_; // where camera is + Position look_at_; // point camera is centered looking at + + Direction up_ {0.0, 0.0, 1.0}; // which way is up + + /* The horizontal thickness, if using an orthographic projection. + * If set to zero, we assume using a perspective projection. + */ + double orthographic_width_ {C_NONE}; + + /* + * Cached camera-to-model matrix with column vectors of axes. The x-axis is + * the vector between the camera_position_ and look_at_; the y-axis is the + * cross product of the x-axis with the up_ vector, and the z-axis is the + * cross product of the x and y axes. + */ + std::array camera_to_model_; +}; + +class ProjectionRay; + +/** + * \class WireframeRayTracePlot + * \brief Creates plots that are like colorful x-ray imaging + * + * WireframeRayTracePlot is a specialized form of RayTracePlot designed for + * creating projection plots. This involves tracing rays from a camera through + * the problem geometry and rendering the results based on depth of penetration + * through materials or cells and their colors. + */ +class WireframeRayTracePlot : public RayTracePlot { + + friend class ProjectionRay; + +public: + WireframeRayTracePlot(pugi::xml_node plot); + + virtual void create_output() const; + virtual void print_info() const; + +private: + void set_opacities(pugi::xml_node node); void set_wireframe_thickness(pugi::xml_node node); void set_wireframe_ids(pugi::xml_node node); void set_wireframe_color(pugi::xml_node node); - /* If starting the particle from outside the geometry, we have to - * find a distance to the boundary in a non-standard surface intersection - * check. It's an exhaustive search over surfaces in the top-level universe. - */ - static int advance_to_boundary_from_void(GeometryState& p); - /* Checks if a vector of two TrackSegments is equivalent. We define this * to mean not having matching intersection lengths, but rather having * a matching sequence of surface/cell/material intersections. @@ -314,35 +406,138 @@ class ProjectionPlot : public PlottableInterface { * if two surfaces bound a single cell, it allows drawing that sharp edge * where the surfaces intersect. */ - int surface; // last surface ID intersected in this segment + int surface_index {-1}; // last surface index intersected in this segment TrackSegment(int id_a, double length_a, int surface_a) - : id(id_a), length(length_a), surface(surface_a) + : id(id_a), length(length_a), surface_index(surface_a) {} }; + // which color IDs should be wireframed. If empty, all cells are wireframed. + vector wireframe_ids_; + + // Thickness of the wireframe lines. Can set to zero for no wireframe. + int wireframe_thickness_ {1}; + + RGBColor wireframe_color_ {BLACK}; // wireframe color + vector xs_; // macro cross section values for cell volume rendering +}; + +/** + * \class SolidRayTracePlot + * \brief Plots 3D objects as the eye might see them. + * + * Plots a geometry with single-scattered Phong lighting plus a diffuse lighting + * contribution. The result is a physically reasonable, aesthetic 3D view of a + * geometry. + */ +class SolidRayTracePlot : public RayTracePlot { + friend class PhongRay; + +public: + SolidRayTracePlot(pugi::xml_node plot); + + virtual void create_output() const; + virtual void print_info() const; + +private: + void set_opaque_ids(pugi::xml_node node); + void set_light_position(pugi::xml_node node); + void set_diffuse_fraction(pugi::xml_node node); + + std::unordered_set opaque_ids_; + + double diffuse_fraction_ {0.1}; + + // By default, the light is at the camera unless otherwise specified. + Position light_location_; +}; + +// Base class that implements ray tracing logic, not necessarily through +// defined regions of the geometry but also outside of it. +class Ray : public GeometryState { + +public: + Ray(Position r, Direction u) { init_from_r_u(r, u); } + + // Called at every surface intersection within the model + virtual void on_intersection() = 0; + + /* + * Traces the ray through the geometry, calling on_intersection + * at every surface boundary. + */ + void trace(); + + // Stops the ray and exits tracing when called from on_intersection + void stop() { stop_ = true; } + + // Sets the dist_ variable + void compute_distance(); + +protected: + // Records how far the ray has traveled + double traversal_distance_ {0.0}; + +private: // Max intersections before we assume ray tracing is caught in an infinite // loop: static const int MAX_INTERSECTIONS = 1000000; - std::array pixels_; // pixel dimension of resulting image - double horizontal_field_of_view_ {70.0}; // horiz. f.o.v. in degrees - Position camera_position_; // where camera is - Position look_at_; // point camera is centered looking at - Direction up_ {0.0, 0.0, 1.0}; // which way is up + bool hit_something_ {false}; + bool stop_ {false}; - // which color IDs should be wireframed. If empty, all cells are wireframed. - vector wireframe_ids_; + unsigned event_counter_ {0}; +}; - /* The horizontal thickness, if using an orthographic projection. - * If set to zero, we assume using a perspective projection. +class ProjectionRay : public Ray { +public: + ProjectionRay(Position r, Direction u, const WireframeRayTracePlot& plot, + vector& line_segments) + : Ray(r, u), plot_(plot), line_segments_(line_segments) + {} + + virtual void on_intersection() override; + +private: + /* Store a reference to the plot object which is running this ray, in order + * to access some of the plot settings which influence the behavior where + * intersections are. */ - double orthographic_width_ {0.0}; + const WireframeRayTracePlot& plot_; - // Thickness of the wireframe lines. Can set to zero for no wireframe. - int wireframe_thickness_ {1}; + /* The ray runs through the geometry, and records the lengths of ray segments + * and cells they lie in along the way. + */ + vector& line_segments_; +}; - RGBColor wireframe_color_ {BLACK}; // wireframe color - vector xs_; // macro cross section values for cell volume rendering +class PhongRay : public Ray { +public: + PhongRay(Position r, Direction u, const SolidRayTracePlot& plot) + : Ray(r, u), plot_(plot) + { + result_color_ = plot_.not_found_; + } + + virtual void on_intersection() override; + + const RGBColor& result_color() { return result_color_; } + +private: + const SolidRayTracePlot& plot_; + + /* After the ray is reflected, it is moving towards the + * camera. It does that in order to see if the exposed surface + * is shadowed by something else. + */ + bool reflected_ {false}; + + // Have to record the first hit ID, so that if the region + // does get shadowed, we recall what its color should be + // when tracing from the surface to the light. + int orig_hit_id_ {-1}; + + RGBColor result_color_; }; //=============================================================================== diff --git a/include/openmc/position.h b/include/openmc/position.h index dc60ab35a04..5d291d26b95 100644 --- a/include/openmc/position.h +++ b/include/openmc/position.h @@ -94,8 +94,24 @@ struct Position { //! \result Reflected vector Position reflect(Position n) const; - //! Rotate the position based on a rotation matrix - Position rotate(const vector& rotation) const; + //! Rotate the position by applying a rotation matrix + template + Position rotate(const T& rotation) const + { + return {x * rotation[0] + y * rotation[1] + z * rotation[2], + x * rotation[3] + y * rotation[4] + z * rotation[5], + x * rotation[6] + y * rotation[7] + z * rotation[8]}; + } + + //! Rotate the position by applying the inverse of a rotation matrix + //! using the fact that rotation matrices are orthonormal. + template + Position inverse_rotate(const T& rotation) const + { + return {x * rotation[0] + y * rotation[3] + z * rotation[6], + x * rotation[1] + y * rotation[4] + z * rotation[7], + x * rotation[2] + y * rotation[5] + z * rotation[8]}; + } // Data members double x = 0.; diff --git a/include/openmc/surface.h b/include/openmc/surface.h index 498f71d4f9b..2397a64cc4c 100644 --- a/include/openmc/surface.h +++ b/include/openmc/surface.h @@ -62,7 +62,7 @@ class Surface { Position r, Direction u, GeometryState* p = nullptr) const; virtual Direction diffuse_reflect( - Position r, Direction u, uint64_t* seed, GeometryState* p = nullptr) const; + Position r, Direction u, uint64_t* seed) const; //! Evaluate the equation describing the surface. //! diff --git a/openmc/_xml.py b/openmc/_xml.py index b40ecb258a9..17389a82f0c 100644 --- a/openmc/_xml.py +++ b/openmc/_xml.py @@ -82,7 +82,7 @@ def reorder_attributes(root): def get_elem_tuple(elem, name, dtype=int): - '''Helper function to get a tuple of values from an elem + """Helper function to get a tuple of values from an elem Parameters ---------- @@ -97,7 +97,7 @@ def get_elem_tuple(elem, name, dtype=int): ------- tuple of dtype Data read from the tuple - ''' + """ subelem = elem.find(name) if subelem is not None: return tuple([dtype(x) for x in subelem.text.split()]) diff --git a/openmc/model/model.py b/openmc/model/model.py index 8dd13ef6b3c..5e93d98040c 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -124,7 +124,7 @@ def plots(self) -> openmc.Plots | None: @plots.setter def plots(self, plots): - check_type('plots', plots, Iterable, openmc.Plot) + check_type('plots', plots, Iterable, openmc.PlotBase) if isinstance(plots, openmc.Plots): self._plots = plots else: @@ -220,7 +220,8 @@ def from_xml(cls, geometry='geometry.xml', materials='materials.xml', materials = openmc.Materials.from_xml(materials) geometry = openmc.Geometry.from_xml(geometry, materials) settings = openmc.Settings.from_xml(settings) - tallies = openmc.Tallies.from_xml(tallies) if Path(tallies).exists() else None + tallies = openmc.Tallies.from_xml( + tallies) if Path(tallies).exists() else None plots = openmc.Plots.from_xml(plots) if Path(plots).exists() else None return cls(geometry, materials, settings, tallies, plots) @@ -242,12 +243,16 @@ def from_model_xml(cls, path='model.xml'): model = cls() meshes = {} - model.settings = openmc.Settings.from_xml_element(root.find('settings'), meshes) - model.materials = openmc.Materials.from_xml_element(root.find('materials')) - model.geometry = openmc.Geometry.from_xml_element(root.find('geometry'), model.materials) + model.settings = openmc.Settings.from_xml_element( + root.find('settings'), meshes) + model.materials = openmc.Materials.from_xml_element( + root.find('materials')) + model.geometry = openmc.Geometry.from_xml_element( + root.find('geometry'), model.materials) if root.find('tallies') is not None: - model.tallies = openmc.Tallies.from_xml_element(root.find('tallies'), meshes) + model.tallies = openmc.Tallies.from_xml_element( + root.find('tallies'), meshes) if root.find('plots') is not None: model.plots = openmc.Plots.from_xml_element(root.find('plots')) @@ -538,11 +543,13 @@ def export_to_model_xml(self, path='model.xml', remove_surfs=False): if self.tallies: tallies_element = self.tallies.to_xml_element(mesh_memo) - xml.clean_indentation(tallies_element, level=1, trailing_indent=self.plots) + xml.clean_indentation( + tallies_element, level=1, trailing_indent=self.plots) fh.write(ET.tostring(tallies_element, encoding="unicode")) if self.plots: plots_element = self.plots.to_xml_element() - xml.clean_indentation(plots_element, level=1, trailing_indent=False) + xml.clean_indentation( + plots_element, level=1, trailing_indent=False) fh.write(ET.tostring(plots_element, encoding="unicode")) fh.write("\n") diff --git a/openmc/plots.py b/openmc/plots.py index a59a7f0eb4a..24607659844 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -555,13 +555,21 @@ def colorize(self, geometry, seed=1): else: domains = geometry.get_all_cells().values() - # Set the seed for the random number generator rng = np.random.RandomState(seed) # Generate random colors for each feature for domain in domains: self.colors[domain] = rng.randint(0, 256, (3,)) + def _colors_to_xml(self, element): + for domain, color in sorted(self._colors.items(), + key=lambda x: self._get_id(x[0])): + subelement = ET.SubElement(element, "color") + subelement.set("id", str(self._get_id(domain))) + if isinstance(color, str): + color = _SVG_COLORS[color.lower()] + subelement.set("rgb", ' '.join(str(x) for x in color)) + def to_xml_element(self): """Save common plot attributes to XML element @@ -887,13 +895,7 @@ def to_xml_element(self): subelement.text = ' '.join(map(str, self._width)) if self._colors: - for domain, color in sorted(self._colors.items(), - key=lambda x: PlotBase._get_id(x[0])): - subelement = ET.SubElement(element, "color") - subelement.set("id", str(PlotBase._get_id(domain))) - if isinstance(color, str): - color = _SVG_COLORS[color.lower()] - subelement.set("rgb", ' '.join(str(x) for x in color)) + self._colors_to_xml(element) if self._show_overlaps: subelement = ET.SubElement(element, "show_overlaps") @@ -1051,7 +1053,8 @@ def to_vtk(self, output: PathLike | None = None, """ if self.type != 'voxel': - raise ValueError('Generating a VTK file only works for voxel plots') + raise ValueError( + 'Generating a VTK file only works for voxel plots') # Create plots.xml Plots([self]).export_to_xml(cwd) @@ -1072,20 +1075,17 @@ def to_vtk(self, output: PathLike | None = None, return voxel_to_vtk(h5_voxel_file, output) -class ProjectionPlot(PlotBase): +class RayTracePlot(PlotBase): """Definition of a camera's view of OpenMC geometry - Colors are defined in the same manner as the Plot class, but with the addition - of a coloring parameter resembling a macroscopic cross section in units of inverse - centimeters. The volume rendering technique is used to color regions of the model. - An infinite cross section denotes a fully opaque region, and zero represents a - transparent region which will expose the color of the regions behind it. - The camera projection may either by orthographic or perspective. Perspective - projections are more similar to a pinhole camera, and orthographic projections - preserve parallel lines and distances. + projections are more similar to a pinhole camera, and orthographic + projections preserve parallel lines and distances. - .. versionadded:: 0.14.0 + This is an abstract base class that :class:`WireframeRayTracePlot` and + :class:`SolidRayTracePlot` finish the implementation of. + + .. versionadded:: 0.15.1 Parameters ---------- @@ -1113,21 +1113,6 @@ class ProjectionPlot(PlotBase): unlike with the default perspective projection. The height of the array is deduced from the ratio of pixel dimensions for the image. Defaults to zero, i.e. using perspective projection. - wireframe_thickness : int - Line thickness employed for drawing wireframes around cells or - material regions. Can be set to zero for no wireframes at all. - Defaults to one pixel. - wireframe_color : tuple of ints - RGB color of the wireframe lines. Defaults to black. - wireframe_domains : iterable of either Material or Cells - If provided, the wireframe is only drawn around these. - If color_by is by material, it must be a list of materials, else cells. - xs : dict - A mapping from cell/material IDs to floats. The floating point values - are macroscopic cross sections influencing the volume rendering opacity - of each geometric region. Zero corresponds to perfect transparency, and - infinity equivalent to opaque. These must be set by the user, but default - values can be obtained using the set_transparent method. """ def __init__(self, plot_id=None, name=''): @@ -1138,10 +1123,6 @@ def __init__(self, plot_id=None, name=''): self._look_at = (0.0, 0.0, 0.0) self._up = (0.0, 0.0, 1.0) self._orthographic_width = 0.0 - self._wireframe_thickness = 1 - self._wireframe_color = _SVG_COLORS['black'] - self._wireframe_domains = [] - self._xs = {} @property def horizontal_field_of_view(self): @@ -1195,6 +1176,161 @@ def orthographic_width(self, orthographic_width): assert orthographic_width >= 0.0 self._orthographic_width = orthographic_width + def _check_domains_consistent_with_color_by(self, domains): + """Check domains are the same as the type we are coloring by""" + for region in domains: + # if an integer is passed, we have to assume it was a valid ID + if isinstance(region, int): + continue + + if self._color_by == 'material': + if not isinstance(region, openmc.Material): + raise Exception('Domain list must be materials if ' + 'color_by=material') + else: + if not isinstance(region, openmc.Cell): + raise Exception('Domain list must be cells if ' + 'color_by=cell') + + def to_xml_element(self): + """Return XML representation of the ray trace plot + + Returns + ------- + element : lxml.etree._Element + XML element containing plot data + + """ + + element = super().to_xml_element() + element.set("id", str(self._id)) + + subelement = ET.SubElement(element, "camera_position") + subelement.text = ' '.join(map(str, self._camera_position)) + + subelement = ET.SubElement(element, "look_at") + subelement.text = ' '.join(map(str, self._look_at)) + + subelement = ET.SubElement(element, "horizontal_field_of_view") + subelement.text = str(self._horizontal_field_of_view) + + # do not need to write if orthographic_width == 0.0 + if self._orthographic_width > 0.0: + subelement = ET.SubElement(element, "orthographic_width") + subelement.text = str(self._orthographic_width) + + return element + + def __repr__(self): + string = '' + string += '{: <16}=\t{}\n'.format('\tID', self._id) + string += '{: <16}=\t{}\n'.format('\tName', self._name) + string += '{: <16}=\t{}\n'.format('\tFilename', self._filename) + string += '{: <16}=\t{}\n'.format('\tHorizontal FOV', + self._horizontal_field_of_view) + string += '{: <16}=\t{}\n'.format('\tOrthographic width', + self._orthographic_width) + string += '{: <16}=\t{}\n'.format('\tCamera position', + self._camera_position) + string += '{: <16}=\t{}\n'.format('\tLook at', self._look_at) + string += '{: <16}=\t{}\n'.format('\tUp', self._up) + string += '{: <16}=\t{}\n'.format('\tPixels', self._pixels) + string += '{: <16}=\t{}\n'.format('\tColor by', self._color_by) + string += '{: <16}=\t{}\n'.format('\tBackground', self._background) + string += '{: <16}=\t{}\n'.format('\tColors', self._colors) + string += '{: <16}=\t{}\n'.format('\tLevel', self._level) + return string + + def _read_xml_attributes(self, elem): + """Helper function called by from_xml_element + of child classes. These are common vaues to be + read by any ray traced plot. + + Returns + ------- + None + """ + + if "filename" in elem.keys(): + self.filename = elem.get("filename") + self.color_by = elem.get("color_by") + + horizontal_fov = elem.find("horizontal_field_of_view") + if horizontal_fov is not None: + self.horizontal_field_of_view = float(horizontal_fov.text) + + if (tmp := elem.find("orthographic_width")) is not None: + self.orthographic_width = float(tmp) + + self.pixels = get_elem_tuple(elem, "pixels") + self.camera_position = get_elem_tuple(elem, "camera_position", float) + self.look_at = get_elem_tuple(elem, "look_at", float) + + if elem.find("background") is not None: + self.background = get_elem_tuple(elem, "background") + + # Set masking information + if (mask_elem := elem.find("mask")) is not None: + mask_components = [int(x) + for x in mask_elem.get("components").split()] + # TODO: set mask components(needs geometry information) + background = mask_elem.get("background") + if background is not None: + self.mask_background = tuple( + [int(x) for x in background.split()]) + + # Set universe level + level = elem.find("level") + if level is not None: + self.level = int(level.text) + + +class WireframeRayTracePlot(RayTracePlot): + """Plots wireframes of geometry with volume rendered colors + + Colors are defined in the same manner as the Plot class, but with the + addition of a coloring parameter resembling a macroscopic cross section in + units of inverse centimeters. The volume rendering technique is used to + color regions of the model. An infinite cross section denotes a fully opaque + region, and zero represents a transparent region which will expose the color + of the regions behind it. + + .. versionchanged:: 0.15.1 + Renamed from ProjectionPlot to WireframeRayTracePlot + + Parameters + ---------- + plot_id : int + Unique identifier for the plot + name : str + Name of the plot + + Attributes + ---------- + wireframe_thickness : int + Line thickness employed for drawing wireframes around cells or material + regions. Can be set to zero for no wireframes at all. Defaults to one + pixel. + wireframe_color : tuple of ints + RGB color of the wireframe lines. Defaults to black. + wireframe_domains : iterable of either Material or Cells + If provided, the wireframe is only drawn around these. If color_by is by + material, it must be a list of materials, else cells. + xs : dict + A mapping from cell/material IDs to floats. The floating point values + are macroscopic cross sections influencing the volume rendering opacity + of each geometric region. Zero corresponds to perfect transparency, and + infinity equivalent to opaque. These must be set by the user, but + default values can be obtained using the :meth:`set_transparent` method. + """ + + def __init__(self, plot_id=None, name=''): + super().__init__(plot_id, name) + self._wireframe_thickness = 1 + self._wireframe_color = _SVG_COLORS['black'] + self._wireframe_domains = [] + self._xs = {} + @property def wireframe_thickness(self): return self._wireframe_thickness @@ -1221,15 +1357,6 @@ def wireframe_domains(self): @wireframe_domains.setter def wireframe_domains(self, wireframe_domains): - for region in wireframe_domains: - if self._color_by == 'material': - if not isinstance(region, openmc.Material): - raise Exception('Must provide a list of materials for \ - wireframe_region if color_by=Material') - else: - if not isinstance(region, openmc.Cell): - raise Exception('Must provide a list of cells for \ - wireframe_region if color_by=cell') self._wireframe_domains = wireframe_domains @property @@ -1266,6 +1393,18 @@ def set_transparent(self, geometry): for domain in domains: self.xs[domain] = 0.0 + def __repr__(self): + string = 'Wireframe Ray-traced Plot\n' + string += super().__repr__() + string += '{: <16}=\t{}\n'.format('\tWireframe thickness', + self._wireframe_thickness) + string += '{: <16}=\t{}\n'.format('\tWireframe color', + self._wireframe_color) + string += '{: <16}=\t{}\n'.format('\tWireframe domains', + self._wireframe_domains) + string += '{: <16}=\t{}\n'.format('\tTransparencies', self._xs) + return string + def to_xml_element(self): """Return XML representation of the projection plot @@ -1275,15 +1414,8 @@ def to_xml_element(self): XML element containing plot data """ - element = super().to_xml_element() - element.set("type", "projection") - - subelement = ET.SubElement(element, "camera_position") - subelement.text = ' '.join(map(str, self._camera_position)) - - subelement = ET.SubElement(element, "look_at") - subelement.text = ' '.join(map(str, self._look_at)) + element.set("type", "wireframe_raytrace") subelement = ET.SubElement(element, "wireframe_thickness") subelement.text = str(self._wireframe_thickness) @@ -1294,6 +1426,8 @@ def to_xml_element(self): color = _SVG_COLORS[color.lower()] subelement.text = ' '.join(str(x) for x in color) + self._check_domains_consistent_with_color_by(self.wireframe_domains) + if self._wireframe_domains: id_list = [x.id for x in self._wireframe_domains] subelement = ET.SubElement(element, "wireframe_ids") @@ -1311,43 +1445,8 @@ def to_xml_element(self): subelement.set("rgb", ' '.join(str(x) for x in color)) subelement.set("xs", str(self._xs[domain])) - subelement = ET.SubElement(element, "horizontal_field_of_view") - subelement.text = str(self._horizontal_field_of_view) - - # do not need to write if orthographic_width == 0.0 - if self._orthographic_width > 0.0: - subelement = ET.SubElement(element, "orthographic_width") - subelement.text = str(self._orthographic_width) - return element - def __repr__(self): - string = 'Projection Plot\n' - string += '{: <16}=\t{}\n'.format('\tID', self._id) - string += '{: <16}=\t{}\n'.format('\tName', self._name) - string += '{: <16}=\t{}\n'.format('\tFilename', self._filename) - string += '{: <16}=\t{}\n'.format('\tHorizontal FOV', - self._horizontal_field_of_view) - string += '{: <16}=\t{}\n'.format('\tOrthographic width', - self._orthographic_width) - string += '{: <16}=\t{}\n'.format('\tWireframe thickness', - self._wireframe_thickness) - string += '{: <16}=\t{}\n'.format('\tWireframe color', - self._wireframe_color) - string += '{: <16}=\t{}\n'.format('\tWireframe domains', - self._wireframe_domains) - string += '{: <16}=\t{}\n'.format('\tCamera position', - self._camera_position) - string += '{: <16}=\t{}\n'.format('\tLook at', self._look_at) - string += '{: <16}=\t{}\n'.format('\tUp', self._up) - string += '{: <16}=\t{}\n'.format('\tPixels', self._pixels) - string += '{: <16}=\t{}\n'.format('\tColor by', self._color_by) - string += '{: <16}=\t{}\n'.format('\tBackground', self._background) - string += '{: <16}=\t{}\n'.format('\tColors', self._colors) - string += '{: <16}=\t{}\n'.format('\tTransparencies', self._xs) - string += '{: <16}=\t{}\n'.format('\tLevel', self._level) - return string - @classmethod def from_xml_element(cls, elem): """Generate plot object from an XML element @@ -1359,60 +1458,193 @@ def from_xml_element(cls, elem): Returns ------- - openmc.ProjectionPlot - ProjectionPlot object + openmc.WireframeRayTracePlot + WireframeRayTracePlot object """ - plot_id = int(elem.get("id")) - plot = cls(plot_id) - if "filename" in elem.keys(): - plot.filename = elem.get("filename") - plot.color_by = elem.get("color_by") - plot.type = "projection" - horizontal_fov = elem.find("horizontal_field_of_view") - if horizontal_fov is not None: - plot.horizontal_field_of_view = float(horizontal_fov.text) - - tmp = elem.find("orthographic_width") - if tmp is not None: - plot.orthographic_width = float(tmp) + plot_id = int(elem.get("id")) + plot_name = get_text(elem, 'name', '') + plot = cls(plot_id, plot_name) + plot.type = "wireframe_raytrace" - plot.pixels = get_elem_tuple(elem, "pixels") - plot.camera_position = get_elem_tuple(elem, "camera_position", float) - plot.look_at = get_elem_tuple(elem, "look_at", float) + plot._read_xml_attributes(elem) - # Attempt to get wireframe thickness. May not be present - wireframe_thickness = elem.get("wireframe_thickness") - if wireframe_thickness: - plot.wireframe_thickness = int(wireframe_thickness) + # Attempt to get wireframe thickness.May not be present + wireframe_thickness = elem.find("wireframe_thickness") + if wireframe_thickness is not None: + plot.wireframe_thickness = int(wireframe_thickness.text) wireframe_color = elem.get("wireframe_color") if wireframe_color: plot.wireframe_color = [int(item) for item in wireframe_color] # Set plot colors - colors = {} - xs = {} for color_elem in elem.findall("color"): - uid = color_elem.get("id") - colors[uid] = get_elem_tuple(color_elem, "rgb") - xs[uid] = float(color_elem.get("xs")) + uid = int(color_elem.get("id")) + plot.colors[uid] = tuple(int(i) + for i in get_text(color_elem, 'rgb').split()) + plot.xs[uid] = float(color_elem.get("xs")) - # Set masking information - mask_elem = elem.find("mask") - if mask_elem is not None: - mask_components = [int(x) - for x in mask_elem.get("components").split()] - # TODO: set mask components (needs geometry information) - background = mask_elem.get("background") - if background is not None: - plot.mask_background = tuple( - [int(x) for x in background.split()]) + return plot - # Set universe level - level = elem.find("level") - if level is not None: - plot.level = int(level.text) + +class SolidRayTracePlot(RayTracePlot): + """Phong shading-based rendering of an OpenMC geometry + + This class defines a plot that uses Phong shading to enhance the + visualization of an OpenMC geometry by incorporating diffuse lighting and + configurable opacity for certain regions. It extends :class:`RayTracePlot` + by adding parameters related to lighting and transparency. + + .. versionadded:: 0.15.1 + + Parameters + ---------- + plot_id : int, optional + Unique identifier for the plot + name : str, optional + Name of the plot + + Attributes + ---------- + light_position : tuple or list of float + Position of the light source in 3D space. Defaults to None, which places + the light at the camera position. + diffuse_fraction : float + Fraction of lighting that is diffuse (non-directional). Defaults to 0.1. + Must be between 0 and 1. + opaque_domains : list + List of domains (e.g., cells or materials) that should be rendered as + opaque rather than allowing transparency. + """ + + def __init__(self, plot_id=None, name=''): + super().__init__(plot_id, name) + self._light_position = None + self._diffuse_fraction = 0.1 + self._opaque_domains = [] + + @property + def light_position(self): + return self._light_position + + @light_position.setter + def light_position(self, x): + cv.check_type('plot light position', x, Iterable, Real) + cv.check_length('plot light position', x, 3) + self._light_position = x + + @property + def diffuse_fraction(self): + return self._diffuse_fraction + + @diffuse_fraction.setter + def diffuse_fraction(self, x): + cv.check_type('diffuse fraction', x, Real) + cv.check_greater_than('diffuse fraction', x, 0.0, equality=True) + cv.check_less_than('diffuse fraction', x, 1.0, equality=True) + self._diffuse_fraction = x + + @property + def opaque_domains(self): + return self._opaque_domains + + @opaque_domains.setter + def opaque_domains(self, x): + # Note that _check_domains_consistent_with_color_by checks + # the types within later. This is because we don't necessarily + # know what types are acceptable until the user has set the + # color_by attribute, too. + cv.check_type('opaque domains', x, Iterable) + self._opaque_domains = x + + def __repr__(self): + string = 'Solid Ray-traced Plot\n' + string += super().__repr__() + string += '{: <16}=\t{}\n'.format('\tDiffuse Fraction', + self._diffuse_fraction) + string += '{: <16}=\t{}\n'.format('\tLight position', + self._light_position) + string += '{: <16}=\t{}\n'.format('\tOpaque domains', + self._opaque_domains) + return string + + def to_xml_element(self): + """Return XML representation of the solid ray-traced plot + + Returns + ------- + element : lxml.etree._Element + XML element containing plot data + + """ + element = super().to_xml_element() + element.set("type", "solid_raytrace") + + # no light position means put it at the camera + if self._light_position: + subelement = ET.SubElement(element, "light_position") + subelement.text = ' '.join(map(str, self._light_position)) + + # no diffuse fraction defaults to 0.1 + if self._diffuse_fraction: + subelement = ET.SubElement(element, "diffuse_fraction") + subelement.text = str(self._diffuse_fraction) + + self._check_domains_consistent_with_color_by(self.opaque_domains) + subelement = ET.SubElement(element, "opaque_ids") + + # Extract all IDs, or use the integer value passed in + # explicitly if that was given + subelement.text = ' '.join( + [str(domain) if isinstance(domain, int) else + str(domain.id) for domain in self._opaque_domains]) + + if self._colors: + self._colors_to_xml(element) + + return element + + def _read_phong_attributes(self, elem): + """Read attributes specific to the Phong plot from an XML element""" + if elem.find('light_position') is not None: + self.light_position = get_elem_tuple(elem, 'light_position', float) + + diffuse_fraction = elem.find('diffuse_fraction') + if diffuse_fraction is not None: + self.diffuse_fraction = float(diffuse_fraction.text) + + if elem.find('opaque_ids') is not None: + self.opaque_domains = list(get_elem_tuple(elem, 'opaque_ids', int)) + + @classmethod + def from_xml_element(cls, elem): + """Generate plot object from an XML element + + Parameters + ---------- + elem : lxml.etree._Element + XML element + + Returns + ------- + openmc.WireframeRayTracePlot + WireframeRayTracePlot object + + """ + + plot_id = int(elem.get("id")) + plot_name = get_text(elem, 'name', '') + plot = cls(plot_id, plot_name) + plot.type = "solid_raytrace" + + plot._read_xml_attributes(elem) + plot._read_phong_attributes(elem) + + # Set plot colors + for color_elem in elem.findall("color"): + uid = color_elem.get("id") + plot.colors[uid] = get_elem_tuple(color_elem, "rgb") return plot @@ -1434,13 +1666,13 @@ class Plots(cv.CheckedList): Parameters ---------- - plots : Iterable of openmc.Plot or openmc.ProjectionPlot + plots : Iterable of openmc.PlotBase plots to add to the collection """ def __init__(self, plots=None): - super().__init__((Plot, ProjectionPlot), 'plots collection') + super().__init__(PlotBase, 'plots collection') self._plots_file = ET.Element("plots") if plots is not None: self += plots @@ -1450,7 +1682,7 @@ def append(self, plot): Parameters ---------- - plot : openmc.Plot or openmc.ProjectionPlot + plot : openmc.PlotBase Plot to append """ @@ -1582,10 +1814,14 @@ def from_xml_element(cls, elem): plots = cls() for e in elem.findall('plot'): plot_type = e.get('type') - if plot_type == 'projection': - plots.append(ProjectionPlot.from_xml_element(e)) - else: + if plot_type == 'wireframe_raytrace': + plots.append(WireframeRayTracePlot.from_xml_element(e)) + elif plot_type == 'solid_raytrace': + plots.append(SolidRayTracePlot.from_xml_element(e)) + elif plot_type in ('slice', 'voxel'): plots.append(Plot.from_xml_element(e)) + else: + raise ValueError("Unknown plot type: {}".format(plot_type)) return plots @classmethod diff --git a/src/dagmc.cpp b/src/dagmc.cpp index 52c45f21c28..e24b3fbf629 100644 --- a/src/dagmc.cpp +++ b/src/dagmc.cpp @@ -808,15 +808,12 @@ Direction DAGSurface::normal(Position r) const Direction DAGSurface::reflect(Position r, Direction u, GeometryState* p) const { Expects(p); - p->history().reset_to_last_intersection(); - moab::ErrorCode rval; - moab::EntityHandle surf = dagmc_ptr_->entity_by_index(2, dag_index_); double pnt[3] = {r.x, r.y, r.z}; double dir[3]; - rval = dagmc_ptr_->get_angle(surf, pnt, dir, &p->history()); + moab::ErrorCode rval = + dagmc_ptr_->get_angle(mesh_handle(), pnt, dir, &p->history()); MB_CHK_ERR_CONT(rval); - p->last_dir() = u.reflect(dir); - return p->last_dir(); + return u.reflect(dir); } //============================================================================== diff --git a/src/particle.cpp b/src/particle.cpp index 4dbab3213ac..5b46e2e639b 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -75,13 +75,6 @@ double Particle::speed() const } } -void Particle::move_distance(double length) -{ - for (int j = 0; j < n_coord(); ++j) { - coord(j).r += length * coord(j).u; - } -} - void Particle::create_secondary( double wgt, Direction u, double E, ParticleType type) { diff --git a/src/particle_data.cpp b/src/particle_data.cpp index d8a5bb9d99c..fef8359a356 100644 --- a/src/particle_data.cpp +++ b/src/particle_data.cpp @@ -15,6 +15,11 @@ namespace openmc { +void GeometryState::mark_as_lost(const char* message) +{ + fatal_error(message); +} + void GeometryState::mark_as_lost(const std::string& message) { mark_as_lost(message.c_str()); @@ -25,11 +30,6 @@ void GeometryState::mark_as_lost(const std::stringstream& message) mark_as_lost(message.str()); } -void GeometryState::mark_as_lost(const char* message) -{ - fatal_error(message); -} - void LocalCoord::rotate(const vector& rotation) { r = r.rotate(rotation); @@ -56,6 +56,40 @@ GeometryState::GeometryState() clear(); } +void GeometryState::advance_to_boundary_from_void() +{ + auto root_coord = this->coord(0); + const auto& root_universe = model::universes[model::root_universe]; + boundary().reset(); + + for (auto c_i : root_universe->cells_) { + auto dist = + model::cells.at(c_i)->distance(root_coord.r, root_coord.u, 0, this); + if (dist.first < boundary().distance) { + boundary().distance = dist.first; + boundary().surface = dist.second; + } + } + + // if no intersection or near-infinite intersection, reset + // boundary information + if (boundary().distance > 1e300) { + boundary().distance = INFTY; + boundary().surface = SURFACE_NONE; + return; + } + + // move the particle up to (and just past) the boundary + move_distance(boundary().distance + TINY_BIT); +} + +void GeometryState::move_distance(double length) +{ + for (int j = 0; j < n_coord(); ++j) { + coord(j).r += length * coord(j).u; + } +} + ParticleData::ParticleData() { zero_delayed_bank(); diff --git a/src/plot.cpp b/src/plot.cpp index b36ed6f5d93..093f89f8cec 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -204,18 +204,21 @@ void read_plots_xml(pugi::xml_node root) int id = std::stoi(id_string); if (check_for_node(node, "type")) { std::string type_str = get_node_value(node, "type", true); - if (type_str == "slice") + if (type_str == "slice") { model::plots.emplace_back( std::make_unique(node, Plot::PlotType::slice)); - else if (type_str == "voxel") + } else if (type_str == "voxel") { model::plots.emplace_back( std::make_unique(node, Plot::PlotType::voxel)); - else if (type_str == "projection") - model::plots.emplace_back(std::make_unique(node)); - else + } else if (type_str == "wireframe_raytrace") { + model::plots.emplace_back( + std::make_unique(node)); + } else if (type_str == "solid_raytrace") { + model::plots.emplace_back(std::make_unique(node)); + } else { fatal_error( fmt::format("Unsupported plot type '{}' in plot {}", type_str, id)); - + } model::plot_map[model::plots.back()->id()] = model::plots.size() - 1; } else { fatal_error(fmt::format("Must specify plot type in plot {}", id)); @@ -264,8 +267,8 @@ void Plot::create_image() const } data(x, y) = colors_[model::material_map[id]]; } // color_by if-else - } // x for loop - } // y for loop + } + } // draw mesh lines if present if (index_meshlines_mesh_ >= 0) { @@ -1036,26 +1039,49 @@ RGBColor random_color(void) int(prn(&model::plotter_seed) * 255), int(prn(&model::plotter_seed) * 255)}; } -ProjectionPlot::ProjectionPlot(pugi::xml_node node) : PlottableInterface(node) +RayTracePlot::RayTracePlot(pugi::xml_node node) : PlottableInterface(node) { - set_output_path(node); set_look_at(node); set_camera_position(node); set_field_of_view(node); set_pixels(node); - set_opacities(node); set_orthographic_width(node); - set_wireframe_thickness(node); - set_wireframe_ids(node); - set_wireframe_color(node); + set_output_path(node); if (check_for_node(node, "orthographic_width") && check_for_node(node, "field_of_view")) fatal_error("orthographic_width and field_of_view are mutually exclusive " "parameters."); + + // Get centerline vector for camera-to-model. We create vectors around this + // that form a pixel array, and then trace rays along that. + auto up = up_ / up_.norm(); + Direction looking_direction = look_at_ - camera_position_; + looking_direction /= looking_direction.norm(); + if (std::abs(std::abs(looking_direction.dot(up)) - 1.0) < 1e-9) + fatal_error("Up vector cannot align with vector between camera position " + "and look_at!"); + Direction cam_yaxis = looking_direction.cross(up); + cam_yaxis /= cam_yaxis.norm(); + Direction cam_zaxis = cam_yaxis.cross(looking_direction); + cam_zaxis /= cam_zaxis.norm(); + + // Cache the camera-to-model matrix + camera_to_model_ = {looking_direction.x, cam_yaxis.x, cam_zaxis.x, + looking_direction.y, cam_yaxis.y, cam_zaxis.y, looking_direction.z, + cam_yaxis.z, cam_zaxis.z}; +} + +WireframeRayTracePlot::WireframeRayTracePlot(pugi::xml_node node) + : RayTracePlot(node) +{ + set_opacities(node); + set_wireframe_thickness(node); + set_wireframe_ids(node); + set_wireframe_color(node); } -void ProjectionPlot::set_wireframe_color(pugi::xml_node plot_node) +void WireframeRayTracePlot::set_wireframe_color(pugi::xml_node plot_node) { // Copy plot background color if (check_for_node(plot_node, "wireframe_color")) { @@ -1068,7 +1094,7 @@ void ProjectionPlot::set_wireframe_color(pugi::xml_node plot_node) } } -void ProjectionPlot::set_output_path(pugi::xml_node node) +void RayTracePlot::set_output_path(pugi::xml_node node) { // Set output file path std::string filename; @@ -1089,33 +1115,7 @@ void ProjectionPlot::set_output_path(pugi::xml_node node) path_plot_ = filename; } -// Advances to the next boundary from outside the geometry -// Returns -1 if no intersection found, and the surface index -// if an intersection was found. -int ProjectionPlot::advance_to_boundary_from_void(GeometryState& p) -{ - constexpr double scoot = 1e-5; - double min_dist = {INFINITY}; - auto coord = p.coord(0); - Universe* uni = model::universes[model::root_universe].get(); - int intersected_surface = -1; - for (auto c_i : uni->cells_) { - auto dist = model::cells.at(c_i)->distance(coord.r, coord.u, 0, &p); - if (dist.first < min_dist) { - min_dist = dist.first; - intersected_surface = dist.second; - } - } - if (min_dist > 1e300) - return -1; - else { // advance the particle - for (int j = 0; j < p.n_coord(); ++j) - p.coord(j).r += (min_dist + scoot) * p.coord(j).u; - return std::abs(intersected_surface); - } -} - -bool ProjectionPlot::trackstack_equivalent( +bool WireframeRayTracePlot::trackstack_equivalent( const std::vector& track1, const std::vector& track2) const { @@ -1125,7 +1125,7 @@ bool ProjectionPlot::trackstack_equivalent( return false; for (int i = 0; i < track1.size(); ++i) { if (track1[i].id != track2[i].id || - track1[i].surface != track2[i].surface) { + track1[i].surface_index != track2[i].surface_index) { return false; } } @@ -1152,7 +1152,7 @@ bool ProjectionPlot::trackstack_equivalent( if (t1_i == track1.size() && t2_i == track2.size()) break; // Check if surface different - if (track1[t1_i].surface != track2[t2_i].surface) + if (track1[t1_i].surface_index != track2[t2_i].surface_index) return false; // Pretty sure this should not be used: @@ -1160,7 +1160,7 @@ bool ProjectionPlot::trackstack_equivalent( // t1_i != track1.size() - 1 && // track1[t1_i+1].id != track2[t2_i+1].id) return false; if (t2_i != 0 && t1_i != 0 && - track1[t1_i - 1].surface != track2[t2_i - 1].surface) + track1[t1_i - 1].surface_index != track2[t2_i - 1].surface_index) return false; // Check if neighboring cells are different @@ -1176,44 +1176,58 @@ bool ProjectionPlot::trackstack_equivalent( } } -void ProjectionPlot::create_output() const +std::pair RayTracePlot::get_pixel_ray( + int horiz, int vert) const { - // Get centerline vector for camera-to-model. We create vectors around this - // that form a pixel array, and then trace rays along that. - auto up = up_ / up_.norm(); - Direction looking_direction = look_at_ - camera_position_; - looking_direction /= looking_direction.norm(); - if (std::abs(std::abs(looking_direction.dot(up)) - 1.0) < 1e-9) - fatal_error("Up vector cannot align with vector between camera position " - "and look_at!"); - Direction cam_yaxis = looking_direction.cross(up); - cam_yaxis /= cam_yaxis.norm(); - Direction cam_zaxis = cam_yaxis.cross(looking_direction); - cam_zaxis /= cam_zaxis.norm(); - - // Transformation matrix for directions - std::vector camera_to_model = {looking_direction.x, cam_yaxis.x, - cam_zaxis.x, looking_direction.y, cam_yaxis.y, cam_zaxis.y, - looking_direction.z, cam_yaxis.z, cam_zaxis.z}; - - // Now we convert to the polar coordinate system with the polar angle - // measuring the angle from the vector up_. Phi is the rotation about up_. For - // now, up_ is hard-coded to be +z. + // Compute field of view in radians constexpr double DEGREE_TO_RADIAN = M_PI / 180.0; double horiz_fov_radians = horizontal_field_of_view_ * DEGREE_TO_RADIAN; double p0 = static_cast(pixels_[0]); double p1 = static_cast(pixels_[1]); double vert_fov_radians = horiz_fov_radians * p1 / p0; - double dphi = horiz_fov_radians / p0; - double dmu = vert_fov_radians / p1; + // focal_plane_dist can be changed to alter the perspective distortion + // effect. This is in units of cm. This seems to look good most of the + // time. TODO let this variable be set through XML. + constexpr double focal_plane_dist = 10.0; + const double dx = 2.0 * focal_plane_dist * std::tan(0.5 * horiz_fov_radians); + const double dy = p1 / p0 * dx; + + std::pair result; + + // Generate the starting position/direction of the ray + if (orthographic_width_ == C_NONE) { // perspective projection + Direction camera_local_vec; + camera_local_vec.x = focal_plane_dist; + camera_local_vec.y = -0.5 * dx + horiz * dx / p0; + camera_local_vec.z = 0.5 * dy - vert * dy / p1; + camera_local_vec /= camera_local_vec.norm(); + + result.first = camera_position_; + result.second = camera_local_vec.rotate(camera_to_model_); + } else { // orthographic projection + + double x_pix_coord = (static_cast(horiz) - p0 / 2.0) / p0; + double y_pix_coord = (static_cast(vert) - p1 / 2.0) / p1; + + result.first = camera_position_ + + camera_y_axis() * x_pix_coord * orthographic_width_ + + camera_z_axis() * y_pix_coord * orthographic_width_; + result.second = camera_x_axis(); + } + + return result; +} + +void WireframeRayTracePlot::create_output() const +{ size_t width = pixels_[0]; size_t height = pixels_[1]; ImageData data({width, height}, not_found_); - // This array marks where the initial wireframe was drawn. - // We convolve it with a filter that gets adjusted with the - // wireframe thickness in order to thicken the lines. + // This array marks where the initial wireframe was drawn. We convolve it with + // a filter that gets adjusted with the wireframe thickness in order to + // thicken the lines. xt::xtensor wireframe_initial({width, height}, 0); /* Holds all of the track segments for the current rendered line of pixels. @@ -1242,16 +1256,13 @@ void ProjectionPlot::create_output() const const int n_threads = num_threads(); const int tid = thread_num(); - GeometryState p; - p.u() = {1.0, 0.0, 0.0}; - int vert = tid; for (int iter = 0; iter <= pixels_[1] / n_threads; iter++) { - // Save bottom line of current work chunk to compare against later - // I used to have this inside the below if block, but it causes a - // spurious line to be drawn at the bottom of the image. Not sure - // why, but moving it here fixes things. + // Save bottom line of current work chunk to compare against later. This + // used to be inside the below if block, but it causes a spurious line to + // be drawn at the bottom of the image. Not sure why, but moving it here + // fixes things. if (tid == n_threads - 1) old_segments = this_line_segments[n_threads - 1]; @@ -1259,126 +1270,48 @@ void ProjectionPlot::create_output() const for (int horiz = 0; horiz < pixels_[0]; ++horiz) { - // Projection mode below decides ray starting conditions - Position init_r; - Direction init_u; - - // Generate the starting position/direction of the ray - if (orthographic_width_ == 0.0) { // perspective projection - double this_phi = - -horiz_fov_radians / 2.0 + dphi * horiz + 0.5 * dphi; - double this_mu = - -vert_fov_radians / 2.0 + dmu * vert + M_PI / 2.0 + 0.5 * dmu; - Direction camera_local_vec; - camera_local_vec.x = std::cos(this_phi) * std::sin(this_mu); - camera_local_vec.y = std::sin(this_phi) * std::sin(this_mu); - camera_local_vec.z = std::cos(this_mu); - init_u = camera_local_vec.rotate(camera_to_model); - init_r = camera_position_; - } else { // orthographic projection - init_u = looking_direction; - - double x_pix_coord = (static_cast(horiz) - p0 / 2.0) / p0; - double y_pix_coord = (static_cast(vert) - p1 / 2.0) / p0; - - init_r = camera_position_; - init_r += cam_yaxis * x_pix_coord * orthographic_width_; - init_r += cam_zaxis * y_pix_coord * orthographic_width_; - } - - // Resets internal geometry state of particle - p.init_from_r_u(init_r, init_u); - - bool hitsomething = false; - bool intersection_found = true; - int loop_counter = 0; + // RayTracePlot implements camera ray generation + std::pair ru = get_pixel_ray(horiz, vert); this_line_segments[tid][horiz].clear(); + ProjectionRay ray( + ru.first, ru.second, *this, this_line_segments[tid][horiz]); - int first_surface = - -1; // surface first passed when entering the model - bool first_inside_model = true; // false after entering the model - while (intersection_found) { - bool inside_cell = false; - - int32_t i_surface = p.surface_index(); - if (i_surface > 0 && - model::surfaces[i_surface]->geom_type() == GeometryType::DAG) { -#ifdef DAGMC - int32_t i_cell = next_cell(i_surface, - p.cell_last(p.n_coord() - 1), p.lowest_coord().universe); - inside_cell = i_cell >= 0; -#else - fatal_error( - "Not compiled for DAGMC, but somehow you have a DAGCell!"); -#endif - } else { - inside_cell = exhaustive_find_cell(p); - } - - if (inside_cell) { - - // This allows drawing wireframes with surface intersection - // edges on the model boundary for the same cell. - if (first_inside_model) { - this_line_segments[tid][horiz].emplace_back( - color_by_ == PlotColorBy::mats ? p.material() - : p.lowest_coord().cell, - 0.0, first_surface); - first_inside_model = false; - } - - hitsomething = true; - intersection_found = true; - auto dist = distance_to_boundary(p); - this_line_segments[tid][horiz].emplace_back( - color_by_ == PlotColorBy::mats ? p.material() - : p.lowest_coord().cell, - dist.distance, std::abs(dist.surface)); - - // Advance particle - for (int lev = 0; lev < p.n_coord(); ++lev) { - p.coord(lev).r += dist.distance * p.coord(lev).u; - } - p.surface() = dist.surface; - p.n_coord_last() = p.n_coord(); - p.n_coord() = dist.coord_level; - if (dist.lattice_translation[0] != 0 || - dist.lattice_translation[1] != 0 || - dist.lattice_translation[2] != 0) { - cross_lattice(p, dist); - } - - } else { - first_surface = advance_to_boundary_from_void(p); - intersection_found = - first_surface != -1; // -1 if no surface found - } - loop_counter++; - if (loop_counter > MAX_INTERSECTIONS) - fatal_error("Infinite loop in projection plot"); - } + ray.trace(); // Now color the pixel based on what we have intersected... // Loops backwards over intersections. Position current_color( not_found_.red, not_found_.green, not_found_.blue); const auto& segments = this_line_segments[tid][horiz]; - for (unsigned i = segments.size(); i-- > 0;) { + + // There must be at least two cell intersections to color, front and + // back of the cell. Maybe an infinitely thick cell could be present + // with no back, but why would you want to color that? It's easier to + // just skip that edge case and not even color it. + if (segments.size() <= 1) + continue; + + for (int i = segments.size() - 2; i >= 0; --i) { int colormap_idx = segments[i].id; RGBColor seg_color = colors_[colormap_idx]; Position seg_color_vec( seg_color.red, seg_color.green, seg_color.blue); - double mixing = std::exp(-xs_[colormap_idx] * segments[i].length); + double mixing = + std::exp(-xs_[colormap_idx] * + (segments[i + 1].length - segments[i].length)); current_color = current_color * mixing + (1.0 - mixing) * seg_color_vec; - RGBColor result; - result.red = static_cast(current_color.x); - result.green = static_cast(current_color.y); - result.blue = static_cast(current_color.z); - data(horiz, vert) = result; } + // save result converting from double-precision color coordinates to + // byte-sized + RGBColor result; + result.red = static_cast(current_color.x); + result.green = static_cast(current_color.y); + result.blue = static_cast(current_color.z); + data(horiz, vert) = result; + // Check to draw wireframe in horizontal direction. No inter-thread // comm. if (horiz > 0) { @@ -1451,9 +1384,8 @@ void ProjectionPlot::create_output() const #endif } -void ProjectionPlot::print_info() const +void RayTracePlot::print_info() const { - fmt::print("Plot Type: Projection\n"); fmt::print("Camera position: {} {} {}\n", camera_position_.x, camera_position_.y, camera_position_.z); fmt::print("Look at: {} {} {}\n", look_at_.x, look_at_.y, look_at_.z); @@ -1462,7 +1394,13 @@ void ProjectionPlot::print_info() const fmt::print("Pixels: {} {}\n", pixels_[0], pixels_[1]); } -void ProjectionPlot::set_opacities(pugi::xml_node node) +void WireframeRayTracePlot::print_info() const +{ + fmt::print("Plot Type: Wireframe ray-traced\n"); + RayTracePlot::print_info(); +} + +void WireframeRayTracePlot::set_opacities(pugi::xml_node node) { xs_.resize(colors_.size(), 1e6); // set to large value for opaque by default @@ -1492,7 +1430,7 @@ void ProjectionPlot::set_opacities(pugi::xml_node node) } } -void ProjectionPlot::set_orthographic_width(pugi::xml_node node) +void RayTracePlot::set_orthographic_width(pugi::xml_node node) { if (check_for_node(node, "orthographic_width")) { double orthographic_width = @@ -1503,7 +1441,7 @@ void ProjectionPlot::set_orthographic_width(pugi::xml_node node) } } -void ProjectionPlot::set_wireframe_thickness(pugi::xml_node node) +void WireframeRayTracePlot::set_wireframe_thickness(pugi::xml_node node) { if (check_for_node(node, "wireframe_thickness")) { int wireframe_thickness = @@ -1514,7 +1452,7 @@ void ProjectionPlot::set_wireframe_thickness(pugi::xml_node node) } } -void ProjectionPlot::set_wireframe_ids(pugi::xml_node node) +void WireframeRayTracePlot::set_wireframe_ids(pugi::xml_node node) { if (check_for_node(node, "wireframe_ids")) { wireframe_ids_ = get_node_array(node, "wireframe_ids"); @@ -1529,7 +1467,7 @@ void ProjectionPlot::set_wireframe_ids(pugi::xml_node node) std::sort(wireframe_ids_.begin(), wireframe_ids_.end()); } -void ProjectionPlot::set_pixels(pugi::xml_node node) +void RayTracePlot::set_pixels(pugi::xml_node node) { vector pxls = get_node_array(node, "pixels"); if (pxls.size() != 2) @@ -1539,19 +1477,19 @@ void ProjectionPlot::set_pixels(pugi::xml_node node) pixels_[1] = pxls[1]; } -void ProjectionPlot::set_camera_position(pugi::xml_node node) +void RayTracePlot::set_camera_position(pugi::xml_node node) { vector camera_pos = get_node_array(node, "camera_position"); if (camera_pos.size() != 3) { - fatal_error( - fmt::format("look_at element must have three floating point values")); + fatal_error(fmt::format( + "camera_position element must have three floating point values")); } camera_position_.x = camera_pos[0]; camera_position_.y = camera_pos[1]; camera_position_.z = camera_pos[2]; } -void ProjectionPlot::set_look_at(pugi::xml_node node) +void RayTracePlot::set_look_at(pugi::xml_node node) { vector look_at = get_node_array(node, "look_at"); if (look_at.size() != 3) { @@ -1562,7 +1500,7 @@ void ProjectionPlot::set_look_at(pugi::xml_node node) look_at_.z = look_at[2]; } -void ProjectionPlot::set_field_of_view(pugi::xml_node node) +void RayTracePlot::set_field_of_view(pugi::xml_node node) { // Defaults to 70 degree horizontal field of view (see .h file) if (check_for_node(node, "field_of_view")) { @@ -1576,6 +1514,352 @@ void ProjectionPlot::set_field_of_view(pugi::xml_node node) } } +SolidRayTracePlot::SolidRayTracePlot(pugi::xml_node node) : RayTracePlot(node) +{ + set_opaque_ids(node); + set_diffuse_fraction(node); + set_light_position(node); +} + +void SolidRayTracePlot::print_info() const +{ + fmt::print("Plot Type: Solid ray-traced\n"); + RayTracePlot::print_info(); +} + +void SolidRayTracePlot::create_output() const +{ + size_t width = pixels_[0]; + size_t height = pixels_[1]; + ImageData data({width, height}, not_found_); + +#pragma omp parallel for schedule(dynamic) collapse(2) + for (int horiz = 0; horiz < pixels_[0]; ++horiz) { + for (int vert = 0; vert < pixels_[1]; ++vert) { + // RayTracePlot implements camera ray generation + std::pair ru = get_pixel_ray(horiz, vert); + PhongRay ray(ru.first, ru.second, *this); + ray.trace(); + data(horiz, vert) = ray.result_color(); + } + } + +#ifdef USE_LIBPNG + output_png(path_plot(), data); +#else + output_ppm(path_plot(), data); +#endif +} + +void SolidRayTracePlot::set_opaque_ids(pugi::xml_node node) +{ + if (check_for_node(node, "opaque_ids")) { + auto opaque_ids_tmp = get_node_array(node, "opaque_ids"); + + // It is read in as actual ID values, but we have to convert to indices in + // mat/cell array + for (auto& x : opaque_ids_tmp) + x = color_by_ == PlotColorBy::mats ? model::material_map[x] + : model::cell_map[x]; + + opaque_ids_.insert(opaque_ids_tmp.begin(), opaque_ids_tmp.end()); + } +} + +void SolidRayTracePlot::set_light_position(pugi::xml_node node) +{ + if (check_for_node(node, "light_position")) { + auto light_pos_tmp = get_node_array(node, "light_position"); + + if (light_pos_tmp.size() != 3) + fatal_error("Light position must be given as 3D coordinates"); + + light_location_.x = light_pos_tmp[0]; + light_location_.y = light_pos_tmp[1]; + light_location_.z = light_pos_tmp[2]; + } else { + light_location_ = camera_position(); + } +} + +void SolidRayTracePlot::set_diffuse_fraction(pugi::xml_node node) +{ + if (check_for_node(node, "diffuse_fraction")) { + diffuse_fraction_ = std::stod(get_node_value(node, "diffuse_fraction")); + if (diffuse_fraction_ < 0.0 || diffuse_fraction_ > 1.0) { + fatal_error("Must have 0 <= diffuse fraction <= 1"); + } + } +} + +void Ray::compute_distance() +{ + boundary() = distance_to_boundary(*this); +} + +void Ray::trace() +{ + // To trace the ray from its origin all the way through the model, we have + // to proceed in two phases. In the first, the ray may or may not be found + // inside the model. If the ray is already in the model, phase one can be + // skipped. Otherwise, the ray has to be advanced to the boundary of the + // model where all the cells are defined. Importantly, this is assuming that + // the model is convex, which is a very reasonable assumption for any + // radiation transport model. + // + // After phase one is done, we can starting tracing from cell to cell within + // the model. This step can use neighbor lists to accelerate the ray tracing. + + // Attempt to initialize the particle. We may have to enter a loop to move + // it up to the edge of the model. + bool inside_cell = exhaustive_find_cell(*this, settings::verbosity >= 10); + + // Advance to the boundary of the model + while (!inside_cell) { + advance_to_boundary_from_void(); + inside_cell = exhaustive_find_cell(*this, settings::verbosity >= 10); + + // If true this means no surface was intersected. See cell.cpp and search + // for numeric_limits to see where we return it. + if (surface() == std::numeric_limits::max()) { + warning(fmt::format("Lost a ray, r = {}, u = {}", r(), u())); + return; + } + + // Exit this loop and enter into cell-to-cell ray tracing (which uses + // neighbor lists) + if (inside_cell) + break; + + // if there is no intersection with the model, we're done + if (boundary().surface == SURFACE_NONE) + return; + + event_counter_++; + if (event_counter_ > MAX_INTERSECTIONS) { + warning("Likely infinite loop in ray traced plot"); + return; + } + } + + // Call the specialized logic for this type of ray. This is for the + // intersection for the first intersection if we had one. + if (boundary().surface != SURFACE_NONE) { + // set the geometry state's surface attribute to be used for + // surface normal computation + surface() = boundary().surface; + on_intersection(); + if (stop_) + return; + } + + // reset surface attribute to zero after the first intersection so that it + // doesn't perturb surface crossing logic from here on out + surface() = 0; + + // This is the ray tracing loop within the model. It exits after exiting + // the model, which is equivalent to assuming that the model is convex. + // It would be nice to factor out the on_intersection at the end of this + // loop and then do "while (inside_cell)", but we can't guarantee it's + // on a surface in that case. There might be some other way to set it + // up that is perhaps a little more elegant, but this is what works just + // fine. + while (true) { + + compute_distance(); + + // There are no more intersections to process + // if we hit the edge of the model, so stop + // the particle in that case. Also, just exit + // if a negative distance was somehow computed. + if (boundary().distance == INFTY || boundary().distance == INFINITY || + boundary().distance < 0) { + return; + } + + // See below comment where call_on_intersection is checked in an + // if statement for an explanation of this. + bool call_on_intersection {true}; + if (boundary().distance < 10 * TINY_BIT) { + call_on_intersection = false; + } + + // DAGMC surfaces expect us to go a little bit further than the advance + // distance to properly check cell inclusion. + boundary().distance += TINY_BIT; + + // Advance particle, prepare for next intersection + for (int lev = 0; lev < n_coord(); ++lev) { + coord(lev).r += boundary().distance * coord(lev).u; + } + surface() = boundary().surface; + n_coord_last() = n_coord(); + n_coord() = boundary().coord_level; + if (boundary().lattice_translation[0] != 0 || + boundary().lattice_translation[1] != 0 || + boundary().lattice_translation[2] != 0) { + cross_lattice(*this, boundary(), settings::verbosity >= 10); + } + + // Record how far the ray has traveled + traversal_distance_ += boundary().distance; + inside_cell = neighbor_list_find_cell(*this, settings::verbosity >= 10); + + // Call the specialized logic for this type of ray. Note that we do not + // call this if the advance distance is very small. Unfortunately, it seems + // darn near impossible to get the particle advanced to the model boundary + // and through it without sometimes accidentally calling on_intersection + // twice. This incorrectly shades the region as occluded when it might not + // actually be. By screening out intersection distances smaller than a + // threshold 10x larger than the scoot distance used to advance up to the + // model boundary, we can avoid that situation. + if (call_on_intersection) { + on_intersection(); + if (stop_) + return; + } + + if (!inside_cell) + return; + + event_counter_++; + if (event_counter_ > MAX_INTERSECTIONS) { + warning("Likely infinite loop in ray traced plot"); + return; + } + } +} + +void ProjectionRay::on_intersection() +{ + // This records a tuple with the following info + // + // 1) ID (material or cell depending on color_by_) + // 2) Distance traveled by the ray through that ID + // 3) Index of the intersected surface (starting from 1) + + line_segments_.emplace_back( + plot_.color_by_ == PlottableInterface::PlotColorBy::mats + ? material() + : lowest_coord().cell, + traversal_distance_, boundary().surface_index()); +} + +void PhongRay::on_intersection() +{ + // Check if we hit an opaque material or cell + int hit_id = plot_.color_by_ == PlottableInterface::PlotColorBy::mats + ? material() + : lowest_coord().cell; + + // If we are reflected and have advanced beyond the camera, + // the ray is done. This is checked here because we should + // kill the ray even if the material is not opaque. + if (reflected_ && (r() - plot_.camera_position()).dot(u()) >= 0.0) { + stop(); + return; + } + + // Anything that's not opaque has zero impact on the plot. + if (plot_.opaque_ids_.find(hit_id) == plot_.opaque_ids_.end()) + return; + + if (!reflected_) { + // reflect the particle and set the color to be colored by + // the normal or the diffuse lighting contribution + reflected_ = true; + result_color_ = plot_.colors_[hit_id]; + Direction to_light = plot_.light_location_ - r(); + to_light /= to_light.norm(); + + // TODO + // Not sure what can cause a surface token to be invalid here, although it + // sometimes happens for a few pixels. It's very very rare, so proceed by + // coloring the pixel with the overlap color. It seems to happen only for a + // few pixels on the outer boundary of a hex lattice. + // + // We cannot detect it in the outer loop, and it only matters here, so + // that's why the error handling is a little different than for a lost + // ray. + if (surface() == 0) { + result_color_ = plot_.overlap_color_; + stop(); + return; + } + + // Get surface pointer + const auto& surf = model::surfaces.at(surface_index()); + + Direction normal = surf->normal(r_local()); + normal /= normal.norm(); + + // Need to apply translations to find the normal vector in + // the base level universe's coordinate system. + for (int lev = n_coord() - 2; lev >= 0; --lev) { + if (coord(lev + 1).rotated) { + const Cell& c {*model::cells[coord(lev).cell]}; + normal = normal.inverse_rotate(c.rotation_); + } + } + + // use the normal opposed to the ray direction + if (normal.dot(u()) > 0.0) { + normal *= -1.0; + } + + // Facing away from the light means no lighting + double dotprod = normal.dot(to_light); + dotprod = std::max(0.0, dotprod); + + double modulation = + plot_.diffuse_fraction_ + (1.0 - plot_.diffuse_fraction_) * dotprod; + result_color_ *= modulation; + + // Now point the particle to the camera. We now begin + // checking to see if it's occluded by another surface + u() = to_light; + + orig_hit_id_ = hit_id; + + // OpenMC native CSG and DAGMC surfaces have some slight differences + // in how they interpret particles that are sitting on a surface. + // I don't know exactly why, but this makes everything work beautifully. + if (surf->geom_type() == GeometryType::DAG) { + surface() = 0; + } else { + surface() = -surface(); // go to other side + } + + // Must fully restart coordinate search. Why? Not sure. + clear(); + + // Note this could likely be faster if we cached the previous + // cell we were in before the reflection. This is the easiest + // way to fully initialize all the sub-universe coordinates and + // directions though. + bool found = exhaustive_find_cell(*this); + if (!found) { + fatal_error("Lost particle after reflection."); + } + + // Must recalculate distance to boundary due to the + // direction change + compute_distance(); + + } else { + // If it's not facing the light, we color with the diffuse contribution, so + // next we check if we're going to occlude the last reflected surface. if + // so, color by the diffuse contribution instead + + if (orig_hit_id_ == -1) + fatal_error("somehow a ray got reflected but not original ID set?"); + + result_color_ = plot_.colors_[orig_hit_id_]; + result_color_ *= plot_.diffuse_fraction_; + stop(); + } +} + extern "C" int openmc_id_map(const void* plot, int32_t* data_out) { diff --git a/src/position.cpp b/src/position.cpp index 5b3613b2897..0361e99a0a9 100644 --- a/src/position.cpp +++ b/src/position.cpp @@ -75,13 +75,6 @@ Position Position::operator-() const return {-x, -y, -z}; } -Position Position::rotate(const vector& rotation) const -{ - return {x * rotation[0] + y * rotation[1] + z * rotation[2], - x * rotation[3] + y * rotation[4] + z * rotation[5], - x * rotation[6] + y * rotation[7] + z * rotation[8]}; -} - std::ostream& operator<<(std::ostream& os, Position r) { os << "(" << r.x << ", " << r.y << ", " << r.z << ")"; diff --git a/src/surface.cpp b/src/surface.cpp index dbcaf849848..30a8d1f211f 100644 --- a/src/surface.cpp +++ b/src/surface.cpp @@ -141,7 +141,7 @@ Direction Surface::reflect(Position r, Direction u, GeometryState* p) const } Direction Surface::diffuse_reflect( - Position r, Direction u, uint64_t* seed, GeometryState* p) const + Position r, Direction u, uint64_t* seed) const { // Diffuse reflect direction according to the normal. // cosine distribution diff --git a/tests/regression_tests/plot_projections/plots.xml b/tests/regression_tests/plot_projections/plots.xml index 85689da1475..50d129cd97b 100644 --- a/tests/regression_tests/plot_projections/plots.xml +++ b/tests/regression_tests/plot_projections/plots.xml @@ -1,7 +1,7 @@ - + 0. 0. 0. 20. 20. 20. 200 200 @@ -11,7 +11,7 @@ 70 - + 0. 0. 0. 10. 10. 0. 25 25 @@ -22,7 +22,7 @@ example1 - + 0. 0. 0. 20. 20. 20. 200 200 @@ -31,7 +31,7 @@ 2 - + 0. 0. 0. 0. 10.0 20. 200 200 @@ -39,7 +39,7 @@ example3.png - + 0. 0. 0. 10. 10. 10. 25 25 @@ -52,4 +52,39 @@ + + 0. 0. 0. + 10. 10. 10. + 200 200 + phong.png + 1 3 + + + + + + + 0. 0. 0. + 10. 10. 10. + 0.5 + 200 200 + phong_diffuse.png + 1 3 + + + + + + + 0. 0. 0. + 10. 10. 10. + 0. 10. 10. + 200 200 + phong_move_light.png + 1 3 + + + + + diff --git a/tests/regression_tests/plot_projections/results_true.dat b/tests/regression_tests/plot_projections/results_true.dat index 1a67da3002a..672280fdd1d 100644 --- a/tests/regression_tests/plot_projections/results_true.dat +++ b/tests/regression_tests/plot_projections/results_true.dat @@ -1 +1 @@ -24fb0f41ee018ea086962dbd6bcd0b536d11d4b34644bfef4f0e74f8b462fe41a84af39c7ff79046d5d7cfe209084eac54712fa0ec01038e97eb43df1abd0334 \ No newline at end of file +025804f1522eafd6e0e9566ce6b9b5603962f278de222c842fe3e06471290bb575676255bcd55e4f084bdcca4ee56d3c219827cb1ef2b5c3a90f7666986b55e9 \ No newline at end of file diff --git a/tests/regression_tests/plot_projections/test.py b/tests/regression_tests/plot_projections/test.py index cf18503f4b8..37a6ecf28d8 100644 --- a/tests/regression_tests/plot_projections/test.py +++ b/tests/regression_tests/plot_projections/test.py @@ -2,5 +2,8 @@ from tests.regression_tests import config def test_plot(): - harness = PlotTestHarness(('plot_1.png', 'example1.png', 'example2.png', 'example3.png', 'orthographic_example1.png')) + harness = PlotTestHarness(('plot_1.png', 'example1.png', 'example2.png', + 'example3.png', 'orthographic_example1.png', + 'phong.png', 'phong_diffuse.png', + 'phong_move_light.png')) harness.main() diff --git a/tests/unit_tests/test_plots.py b/tests/unit_tests/test_plots.py index 4efc12c9326..fad574ee697 100644 --- a/tests/unit_tests/test_plots.py +++ b/tests/unit_tests/test_plots.py @@ -4,6 +4,8 @@ import openmc.examples import pytest +from openmc.plots import _SVG_COLORS + @pytest.fixture(scope='module') def myplot(): @@ -42,7 +44,7 @@ def myplot(): @pytest.fixture(scope='module') def myprojectionplot(): - plot = openmc.ProjectionPlot(name='myprojectionplot') + plot = openmc.WireframeRayTracePlot(name='myprojectionplot') plot.look_at = (0.0, 0.0, 0.0) plot.camera_position = (4.0, 3.0, 0.0) plot.pixels = (500, 500) @@ -118,6 +120,31 @@ def test_repr_proj(myprojectionplot): assert isinstance(r, str) +def test_projection_plot_roundtrip(myprojectionplot): + + elem = myprojectionplot.to_xml_element() + + xml_plot = openmc.WireframeRayTracePlot.from_xml_element(elem) + + svg_colors = _SVG_COLORS + + assert xml_plot.name == myprojectionplot.name + assert xml_plot.look_at == myprojectionplot.look_at + assert xml_plot.camera_position == myprojectionplot.camera_position + assert xml_plot.pixels == myprojectionplot.pixels + assert xml_plot.filename == myprojectionplot.filename + assert xml_plot.background == svg_colors[myprojectionplot.background] + assert xml_plot.color_by == myprojectionplot.color_by + expected_colors = {m.id: svg_colors[c] for m, c in myprojectionplot.colors.items()} + assert xml_plot.colors == expected_colors + # TODO: needs geometry information + # assert xml_plot.mask_components == myprojectionplot.mask_components + assert xml_plot.mask_background == svg_colors[myprojectionplot.mask_background] + # assert xml_plot.overlap_color == svg_colors[myprojectionplot.overlap_color] + assert xml_plot.wireframe_thickness == myprojectionplot.wireframe_thickness + assert xml_plot.level == myprojectionplot.level + + def test_from_geometry(): width = 25. s = openmc.Sphere(r=width/2, boundary_type='vacuum') @@ -182,7 +209,7 @@ def test_plots(run_in_tmpdir): plots = openmc.Plots([p1, p2]) assert len(plots) == 2 - p3 = openmc.ProjectionPlot(name='plot3') + p3 = openmc.WireframeRayTracePlot(name='plot3') plots = openmc.Plots([p1, p2, p3]) assert len(plots) == 3 @@ -223,6 +250,41 @@ def test_voxel_plot_roundtrip(): assert new_plot.color_by == plot.color_by +def test_phong_plot_roundtrip(): + plot = openmc.SolidRayTracePlot(name='my phong plot') + plot.id = 2300 + plot.filename = 'phong1' + plot.pixels = (50, 50) + plot.look_at = (11., 12., 13.) + plot.camera_position = (22., 23., 24.) + plot.diffuse_fraction = 0.5 + plot.horizontal_field_of_view = 90.0 + plot.color_by = 'material' + plot.light_position = (8., 9., 10.) + plot.opaque_domains = [6, 7, 8] + + elem = plot.to_xml_element() + + repr(plot) + + new_plot = openmc.SolidRayTracePlot.from_xml_element(elem) + + assert new_plot.name == plot.name + assert new_plot.id == plot.id + assert new_plot.filename == plot.filename + assert new_plot.pixels == plot.pixels + assert new_plot.look_at == plot.look_at + assert new_plot.camera_position == plot.camera_position + assert new_plot.diffuse_fraction == plot.diffuse_fraction + assert new_plot.horizontal_field_of_view == plot.horizontal_field_of_view + assert new_plot.color_by == plot.color_by + assert new_plot.light_position == plot.light_position + assert new_plot.opaque_domains == plot.opaque_domains + + # ensure the new object is valid to re-write to XML + new_elem = new_plot.to_xml_element() + + def test_plot_directory(run_in_tmpdir): pwr_pin = openmc.examples.pwr_pin_cell() From 3011a14a1339299526c7adb6343854c911c577ed Mon Sep 17 00:00:00 2001 From: John Tramm Date: Tue, 18 Feb 2025 17:38:59 -0600 Subject: [PATCH 151/184] Random Ray Explicit Void Treatment (#3299) Co-authored-by: Paul Romano --- include/openmc/random_ray/random_ray.h | 2 + include/openmc/random_ray/source_region.h | 78 ++++-- src/random_ray/flat_source_domain.cpp | 120 +++++++-- src/random_ray/linear_source_domain.cpp | 16 +- src/random_ray/random_ray.cpp | 182 ++++++++++++- src/random_ray/random_ray_simulation.cpp | 17 ++ src/random_ray/source_region.cpp | 6 + .../random_ray_void/__init__.py | 0 .../random_ray_void/flat/inputs_true.dat | 245 ++++++++++++++++++ .../random_ray_void/flat/results_true.dat | 9 + .../random_ray_void/linear/inputs_true.dat | 245 ++++++++++++++++++ .../random_ray_void/linear/results_true.dat | 9 + .../regression_tests/random_ray_void/test.py | 72 +++++ 13 files changed, 948 insertions(+), 53 deletions(-) create mode 100644 tests/regression_tests/random_ray_void/__init__.py create mode 100644 tests/regression_tests/random_ray_void/flat/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_void/flat/results_true.dat create mode 100644 tests/regression_tests/random_ray_void/linear/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_void/linear/results_true.dat create mode 100644 tests/regression_tests/random_ray_void/test.py diff --git a/include/openmc/random_ray/random_ray.h b/include/openmc/random_ray/random_ray.h index 96c38da7b1c..4325a6420da 100644 --- a/include/openmc/random_ray/random_ray.h +++ b/include/openmc/random_ray/random_ray.h @@ -27,7 +27,9 @@ class RandomRay : public Particle { void event_advance_ray(); void attenuate_flux(double distance, bool is_active); void attenuate_flux_flat_source(double distance, bool is_active); + void attenuate_flux_flat_source_void(double distance, bool is_active); void attenuate_flux_linear_source(double distance, bool is_active); + void attenuate_flux_linear_source_void(double distance, bool is_active); void initialize_ray(uint64_t ray_id, FlatSourceDomain* domain); uint64_t transport_history_based_single_ray(); diff --git a/include/openmc/random_ray/source_region.h b/include/openmc/random_ray/source_region.h index 0dc617a643a..e41d6b82463 100644 --- a/include/openmc/random_ray/source_region.h +++ b/include/openmc/random_ray/source_region.h @@ -91,37 +91,61 @@ class SourceRegion { //---------------------------------------------------------------------------- // Public Data members + //--------------------------------------- // Scalar fields - int material_ {0}; + + int material_ {0}; //!< Index in openmc::model::materials array OpenMPMutex lock_; - double volume_ {0.0}; - double volume_t_ {0.0}; - double volume_naive_ {0.0}; - int position_recorded_ {0}; - int external_source_present_ {0}; - Position position_ {0.0, 0.0, 0.0}; - Position centroid_ {0.0, 0.0, 0.0}; - Position centroid_iteration_ {0.0, 0.0, 0.0}; - Position centroid_t_ {0.0, 0.0, 0.0}; - MomentMatrix mom_matrix_ {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; - MomentMatrix mom_matrix_t_ {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; + double volume_ { + 0.0}; //!< Volume (computed from the sum of ray crossing lengths) + double volume_t_ {0.0}; //!< Volume totaled over all iterations + double volume_sq_ {0.0}; //!< Volume squared + double volume_sq_t_ {0.0}; //!< Volume squared totaled over all iterations + double volume_naive_ {0.0}; //!< Volume as integrated from this iteration only + int position_recorded_ {0}; //!< Has the position been recorded yet? + int external_source_present_ { + 0}; //!< Is an external source present in this region? + Position position_ { + 0.0, 0.0, 0.0}; //!< A position somewhere inside the region + Position centroid_ {0.0, 0.0, 0.0}; //!< The centroid + Position centroid_iteration_ { + 0.0, 0.0, 0.0}; //!< The centroid integrated from this iteration only + Position centroid_t_ { + 0.0, 0.0, 0.0}; //!< The centroid accumulated over all iterations + MomentMatrix mom_matrix_ { + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; //!< The spatial moment matrix + MomentMatrix mom_matrix_t_ {0.0, 0.0, 0.0, 0.0, 0.0, + 0.0}; //!< The spatial moment matrix accumulated over all iterations + // A set of volume tally tasks. This more complicated data structure is // convenient for ensuring that volumes are only tallied once per source // region, regardless of how many energy groups are used for tallying. std::unordered_set volume_task_; + //--------------------------------------- // Energy group-wise 1D arrays - vector scalar_flux_old_; - vector scalar_flux_new_; - vector source_; - vector external_source_; - vector scalar_flux_final_; - - vector source_gradients_; - vector flux_moments_old_; - vector flux_moments_new_; - vector flux_moments_t_; + vector + scalar_flux_old_; //!< The scalar flux from the previous iteration + vector + scalar_flux_new_; //!< The scalar flux from the current iteration + vector + source_; //!< The total source term (fission + scattering + external) + vector external_source_; //!< The external source term + vector scalar_flux_final_; //!< The scalar flux accumulated over all + //!< active iterations (used for plotting, + //!< or computing adjoint sources) + + vector source_gradients_; //!< The linear source gradients + vector + flux_moments_old_; //!< The linear flux moments from the previous iteration + vector + flux_moments_new_; //!< The linear flux moments from the current iteration + vector + flux_moments_t_; //!< The linear flux moments accumulated over all active + //!< iterations (used for plotting) + + //--------------------------------------- // 2D array representing values for all energy groups x tally // tasks. Each group may have a different number of tally tasks // associated with it, necessitating the use of a jagged array. @@ -152,6 +176,12 @@ class SourceRegionContainer { double& volume_t(int64_t sr) { return volume_t_[sr]; } const double& volume_t(int64_t sr) const { return volume_t_[sr]; } + double& volume_sq(int64_t sr) { return volume_sq_[sr]; } + const double& volume_sq(int64_t sr) const { return volume_sq_[sr]; } + + double& volume_sq_t(int64_t sr) { return volume_sq_t_[sr]; } + const double& volume_sq_t(int64_t sr) const { return volume_sq_t_[sr]; } + double& volume_naive(int64_t sr) { return volume_naive_[sr]; } const double& volume_naive(int64_t sr) const { return volume_naive_[sr]; } @@ -355,6 +385,8 @@ class SourceRegionContainer { vector lock_; vector volume_; vector volume_t_; + vector volume_sq_; + vector volume_sq_t_; vector volume_naive_; vector position_recorded_; vector external_source_present_; @@ -397,4 +429,4 @@ class SourceRegionContainer { } // namespace openmc -#endif // OPENMC_RANDOM_RAY_SOURCE_REGION_H \ No newline at end of file +#endif // OPENMC_RANDOM_RAY_SOURCE_REGION_H diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index 098fd17b085..cfe747eec5e 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -94,6 +94,7 @@ void FlatSourceDomain::batch_reset() #pragma omp parallel for for (int64_t sr = 0; sr < n_source_regions_; sr++) { source_regions_.volume(sr) = 0.0; + source_regions_.volume_sq(sr) = 0.0; } #pragma omp parallel for for (int64_t se = 0; se < n_source_elements_; se++) { @@ -118,11 +119,19 @@ void FlatSourceDomain::update_neutron_source(double k_eff) double inverse_k_eff = 1.0 / k_eff; +// Reset all source regions to zero (important for void regions) +#pragma omp parallel for + for (int64_t se = 0; se < n_source_elements_; se++) { + source_regions_.source(se) = 0.0; + } + // Add scattering + fission source #pragma omp parallel for for (int64_t sr = 0; sr < n_source_regions_; sr++) { int material = source_regions_.material(sr); - + if (material == MATERIAL_VOID) { + continue; + } for (int g_out = 0; g_out < negroups_; g_out++) { double sigma_t = sigma_t_[material * negroups_ + g_out]; double scatter_source = 0.0; @@ -174,8 +183,12 @@ void FlatSourceDomain::normalize_scalar_flux_and_volumes( #pragma omp parallel for for (int64_t sr = 0; sr < n_source_regions_; sr++) { source_regions_.volume_t(sr) += source_regions_.volume(sr); + source_regions_.volume_sq_t(sr) += source_regions_.volume_sq(sr); source_regions_.volume_naive(sr) = source_regions_.volume(sr) * normalization_factor; + source_regions_.volume_sq(sr) = + (source_regions_.volume_sq_t(sr) / source_regions_.volume_t(sr)) * + volume_normalization_factor; source_regions_.volume(sr) = source_regions_.volume_t(sr) * volume_normalization_factor; } @@ -184,9 +197,17 @@ void FlatSourceDomain::normalize_scalar_flux_and_volumes( void FlatSourceDomain::set_flux_to_flux_plus_source( int64_t sr, double volume, int g) { - double sigma_t = sigma_t_[source_regions_.material(sr) * negroups_ + g]; - source_regions_.scalar_flux_new(sr, g) /= (sigma_t * volume); - source_regions_.scalar_flux_new(sr, g) += source_regions_.source(sr, g); + int material = source_regions_.material(sr); + if (material == MATERIAL_VOID) { + source_regions_.scalar_flux_new(sr, g) /= volume; + source_regions_.scalar_flux_new(sr, g) += + 0.5f * source_regions_.external_source(sr, g) * + source_regions_.volume_sq(sr); + } else { + double sigma_t = sigma_t_[source_regions_.material(sr) * negroups_ + g]; + source_regions_.scalar_flux_new(sr, g) /= (sigma_t * volume); + source_regions_.scalar_flux_new(sr, g) += source_regions_.source(sr, g); + } } void FlatSourceDomain::set_flux_to_old_flux(int64_t sr, int g) @@ -296,6 +317,9 @@ double FlatSourceDomain::compute_k_eff(double k_eff_old) const } int material = source_regions_.material(sr); + if (material == MATERIAL_VOID) { + continue; + } double sr_fission_source_old = 0; double sr_fission_source_new = 0; @@ -499,7 +523,13 @@ double FlatSourceDomain::compute_fixed_source_normalization_factor() const int material = source_regions_.material(sr); double volume = source_regions_.volume(sr) * simulation_volume_; for (int g = 0; g < negroups_; g++) { - double sigma_t = sigma_t_[material * negroups_ + g]; + // For non-void regions, we store the external source pre-divided by + // sigma_t. We need to multiply non-void regions back up by sigma_t + // to get the total source strength in the expected units. + double sigma_t = 1.0; + if (material != MATERIAL_VOID) { + sigma_t = sigma_t_[material * negroups_ + g]; + } simulation_external_source_strength += source_regions_.external_source(sr, g) * sigma_t * volume; } @@ -561,7 +591,7 @@ void FlatSourceDomain::random_ray_tally() // Determine numerical score value for (auto& task : source_regions_.tally_task(sr, g)) { - double score; + double score = 0.0; switch (task.score_type) { case SCORE_FLUX: @@ -569,15 +599,21 @@ void FlatSourceDomain::random_ray_tally() break; case SCORE_TOTAL: - score = flux * volume * sigma_t_[material * negroups_ + g]; + if (material != MATERIAL_VOID) { + score = flux * volume * sigma_t_[material * negroups_ + g]; + } break; case SCORE_FISSION: - score = flux * volume * sigma_f_[material * negroups_ + g]; + if (material != MATERIAL_VOID) { + score = flux * volume * sigma_f_[material * negroups_ + g]; + } break; case SCORE_NU_FISSION: - score = flux * volume * nu_sigma_f_[material * negroups_ + g]; + if (material != MATERIAL_VOID) { + score = flux * volume * nu_sigma_f_[material * negroups_ + g]; + } break; case SCORE_EVENTS: @@ -800,21 +836,37 @@ void FlatSourceDomain::output_to_vtk() const } // Plot fission source - std::fprintf(plot, "SCALARS total_fission_source float\n"); - std::fprintf(plot, "LOOKUP_TABLE default\n"); - for (int i = 0; i < Nx * Ny * Nz; i++) { - int64_t fsr = voxel_indices[i]; + if (settings::run_mode == RunMode::EIGENVALUE) { + std::fprintf(plot, "SCALARS total_fission_source float\n"); + std::fprintf(plot, "LOOKUP_TABLE default\n"); + for (int i = 0; i < Nx * Ny * Nz; i++) { + int64_t fsr = voxel_indices[i]; - float total_fission = 0.0; - int mat = source_regions_.material(fsr); - for (int g = 0; g < negroups_; g++) { - int64_t source_element = fsr * negroups_ + g; - float flux = evaluate_flux_at_point(voxel_positions[i], fsr, g); - double sigma_f = sigma_f_[mat * negroups_ + g]; - total_fission += sigma_f * flux; + float total_fission = 0.0; + int mat = source_regions_.material(fsr); + if (mat != MATERIAL_VOID) { + for (int g = 0; g < negroups_; g++) { + int64_t source_element = fsr * negroups_ + g; + float flux = evaluate_flux_at_point(voxel_positions[i], fsr, g); + double sigma_f = sigma_f_[mat * negroups_ + g]; + total_fission += sigma_f * flux; + } + } + total_fission = convert_to_big_endian(total_fission); + std::fwrite(&total_fission, sizeof(float), 1, plot); + } + } else { + std::fprintf(plot, "SCALARS external_source float\n"); + std::fprintf(plot, "LOOKUP_TABLE default\n"); + for (int i = 0; i < Nx * Ny * Nz; i++) { + int64_t fsr = voxel_indices[i]; + float total_external = 0.0f; + for (int g = 0; g < negroups_; g++) { + total_external += source_regions_.external_source(fsr, g); + } + total_external = convert_to_big_endian(total_external); + std::fwrite(&total_external, sizeof(float), 1, plot); } - total_fission = convert_to_big_endian(total_fission); - std::fwrite(&total_fission, sizeof(float), 1, plot); } std::fclose(plot); @@ -847,7 +899,12 @@ void FlatSourceDomain::apply_external_source_to_cell_instances(int32_t i_cell, for (int j : instances) { int cell_material_idx = cell.material(j); - int cell_material_id = model::materials[cell_material_idx]->id(); + int cell_material_id; + if (cell_material_idx == MATERIAL_VOID) { + cell_material_id = MATERIAL_VOID; + } else { + cell_material_id = model::materials[cell_material_idx]->id(); + } if (target_material_id == C_NONE || cell_material_id == target_material_id) { int64_t source_region = source_region_offsets_[i_cell] + j; @@ -930,8 +987,12 @@ void FlatSourceDomain::convert_external_sources() // iteration) #pragma omp parallel for for (int64_t sr = 0; sr < n_source_regions_; sr++) { + int material = source_regions_.material(sr); + if (material == MATERIAL_VOID) { + continue; + } for (int g = 0; g < negroups_; g++) { - double sigma_t = sigma_t_[source_regions_.material(sr) * negroups_ + g]; + double sigma_t = sigma_t_[material * negroups_ + g]; source_regions_.external_source(sr, g) /= sigma_t; } } @@ -994,8 +1055,11 @@ void FlatSourceDomain::set_adjoint_sources(const vector& forward_flux) // The forward flux is given in terms of total for the forward simulation // so we must convert it to a "per batch" quantity #pragma omp parallel for - for (int64_t se = 0; se < n_source_elements_; se++) { - source_regions_.external_source(se) = 1.0 / forward_flux[se]; + for (int64_t sr = 0; sr < n_source_regions_; sr++) { + for (int g = 0; g < negroups_; g++) { + source_regions_.external_source(sr, g) = + 1.0 / forward_flux[sr * negroups_ + g]; + } } // Divide the fixed source term by sigma t (to save time when applying each @@ -1003,6 +1067,10 @@ void FlatSourceDomain::set_adjoint_sources(const vector& forward_flux) #pragma omp parallel for for (int64_t sr = 0; sr < n_source_regions_; sr++) { for (int g = 0; g < negroups_; g++) { + int material = source_regions_.material(sr); + if (material == MATERIAL_VOID) { + continue; + } double sigma_t = sigma_t_[source_regions_.material(sr) * negroups_ + g]; source_regions_.external_source(sr, g) /= sigma_t; } diff --git a/src/random_ray/linear_source_domain.cpp b/src/random_ray/linear_source_domain.cpp index fb6502a3ef1..75d078d1b9b 100644 --- a/src/random_ray/linear_source_domain.cpp +++ b/src/random_ray/linear_source_domain.cpp @@ -43,6 +43,9 @@ void LinearSourceDomain::update_neutron_source(double k_eff) #pragma omp parallel for for (int64_t sr = 0; sr < n_source_regions_; sr++) { int material = source_regions_.material(sr); + if (material == MATERIAL_VOID) { + continue; + } MomentMatrix invM = source_regions_.mom_matrix(sr).inverse(); for (int g_out = 0; g_out < negroups_; g_out++) { @@ -118,10 +121,14 @@ void LinearSourceDomain::normalize_scalar_flux_and_volumes( source_regions_.centroid_t(sr) += source_regions_.centroid_iteration(sr); source_regions_.mom_matrix_t(sr) += source_regions_.mom_matrix(sr); source_regions_.volume_t(sr) += source_regions_.volume(sr); + source_regions_.volume_sq_t(sr) += source_regions_.volume_sq(sr); source_regions_.volume_naive(sr) = source_regions_.volume(sr) * normalization_factor; source_regions_.volume(sr) = source_regions_.volume_t(sr) * volume_normalization_factor; + source_regions_.volume_sq(sr) = + (source_regions_.volume_sq_t(sr) / source_regions_.volume_t(sr)) * + volume_normalization_factor; if (source_regions_.volume_t(sr) > 0.0) { double inv_volume = 1.0 / source_regions_.volume_t(sr); source_regions_.centroid(sr) = source_regions_.centroid_t(sr); @@ -135,8 +142,13 @@ void LinearSourceDomain::normalize_scalar_flux_and_volumes( void LinearSourceDomain::set_flux_to_flux_plus_source( int64_t sr, double volume, int g) { - source_regions_.scalar_flux_new(sr, g) /= volume; - source_regions_.scalar_flux_new(sr, g) += source_regions_.source(sr, g); + int material = source_regions_.material(sr); + if (material == MATERIAL_VOID) { + FlatSourceDomain::set_flux_to_flux_plus_source(sr, volume, g); + } else { + source_regions_.scalar_flux_new(sr, g) /= volume; + source_regions_.scalar_flux_new(sr, g) += source_regions_.source(sr, g); + } source_regions_.flux_moments_new(sr, g) *= (1.0 / volume); } diff --git a/src/random_ray/random_ray.cpp b/src/random_ray/random_ray.cpp index 9c2a890ec5a..a8a827775ec 100644 --- a/src/random_ray/random_ray.cpp +++ b/src/random_ray/random_ray.cpp @@ -274,13 +274,22 @@ void RandomRay::event_advance_ray() void RandomRay::attenuate_flux(double distance, bool is_active) { + switch (source_shape_) { case RandomRaySourceShape::FLAT: - attenuate_flux_flat_source(distance, is_active); + if (this->material() == MATERIAL_VOID) { + attenuate_flux_flat_source_void(distance, is_active); + } else { + attenuate_flux_flat_source(distance, is_active); + } break; case RandomRaySourceShape::LINEAR: case RandomRaySourceShape::LINEAR_XY: - attenuate_flux_linear_source(distance, is_active); + if (this->material() == MATERIAL_VOID) { + attenuate_flux_linear_source_void(distance, is_active); + } else { + attenuate_flux_linear_source(distance, is_active); + } break; default: fatal_error("Unknown source shape for random ray transport."); @@ -355,6 +364,56 @@ void RandomRay::attenuate_flux_flat_source(double distance, bool is_active) } } +// Alternative flux attenuation function for true void regions. +void RandomRay::attenuate_flux_flat_source_void(double distance, bool is_active) +{ + // The number of geometric intersections is counted for reporting purposes + n_event()++; + + // Determine source region index etc. + int i_cell = lowest_coord().cell; + + // The source region is the spatial region index + int64_t sr = domain_->source_region_offsets_[i_cell] + cell_instance(); + + // If ray is in the active phase (not in dead zone), make contributions to + // source region bookkeeping + if (is_active) { + + // Aquire lock for source region + domain_->source_regions_.lock(sr).lock(); + + // Accumulate delta psi into new estimate of source region flux for + // this iteration + for (int g = 0; g < negroups_; g++) { + domain_->source_regions_.scalar_flux_new(sr, g) += + angular_flux_[g] * distance; + } + + // Accomulate volume (ray distance) into this iteration's estimate + // of the source region's volume + domain_->source_regions_.volume(sr) += distance; + domain_->source_regions_.volume_sq(sr) += distance * distance; + + // Tally valid position inside the source region (e.g., midpoint of + // the ray) if not done already + if (!domain_->source_regions_.position_recorded(sr)) { + Position midpoint = r() + u() * (distance / 2.0); + domain_->source_regions_.position(sr) = midpoint; + domain_->source_regions_.position_recorded(sr) = 1; + } + + // Release lock + domain_->source_regions_.lock(sr).unlock(); + } + + // Add source to incoming angular flux, assuming void region + for (int g = 0; g < negroups_; g++) { + angular_flux_[g] += + domain_->source_regions_.external_source(sr, g) * distance; + } +} + void RandomRay::attenuate_flux_linear_source(double distance, bool is_active) { // Cast domain to LinearSourceDomain @@ -492,6 +551,125 @@ void RandomRay::attenuate_flux_linear_source(double distance, bool is_active) } } +// If traveling through a void region, the source term is either zero +// or an external source. As all external sources are currently assumed +// to be flat, we don't really need this function and could instead just call +// the "attenuate_flux_flat_source_void" function and get the same numerical and +// tally results. However, computation of the flux moments in void regions is +// nonetheless useful as this information is still used by the plotter when +// estimating the flux at specific pixel coordinates. Thus, plots will look +// nicer/more accurate if we record flux moments, so this function is useful. +void RandomRay::attenuate_flux_linear_source_void( + double distance, bool is_active) +{ + // Cast domain to LinearSourceDomain + LinearSourceDomain* domain = dynamic_cast(domain_); + if (!domain) { + fatal_error("RandomRay::attenuate_flux_linear_source() called with " + "non-LinearSourceDomain domain."); + } + + // The number of geometric intersections is counted for reporting purposes + n_event()++; + + // Determine source region index etc. + int i_cell = lowest_coord().cell; + + // The source region is the spatial region index + int64_t sr = domain_->source_region_offsets_[i_cell] + cell_instance(); + + Position& centroid = domain_->source_regions_.centroid(sr); + Position midpoint = r() + u() * (distance / 2.0); + + // Determine the local position of the midpoint and the ray origin + // relative to the source region's centroid + Position rm_local; + Position r0_local; + + // In the first few iterations of the simulation, the source region + // may not yet have had any ray crossings, in which case there will + // be no estimate of its centroid. We detect this by checking if it has + // any accumulated volume. If its volume is zero, just use the midpoint + // of the ray as the region's centroid. + if (domain_->source_regions_.volume_t(sr)) { + rm_local = midpoint - centroid; + r0_local = r() - centroid; + } else { + rm_local = {0.0, 0.0, 0.0}; + r0_local = -u() * 0.5 * distance; + } + double distance_2 = distance * distance; + + // Compared to linear flux attenuation through solid regions, + // transport through a void region is greatly simplified. Here we + // compute the updated flux moments. + for (int g = 0; g < negroups_; g++) { + float spatial_source = domain_->source_regions_.source(sr, g); + float new_delta_psi = (angular_flux_[g] - spatial_source) * distance; + float h1 = 0.5f; + h1 = h1 * angular_flux_[g]; + h1 = h1 * distance_2; + spatial_source = spatial_source * distance + new_delta_psi; + + // Store contributions for this group into arrays, so that they can + // be accumulated into the source region's estimates inside of the locked + // region. + delta_moments_[g] = r0_local * spatial_source + u() * h1; + + // If 2D mode is enabled, the z-component of the flux moments is forced + // to zero + if (source_shape_ == RandomRaySourceShape::LINEAR_XY) { + delta_moments_[g].z = 0.0; + } + } + + // If ray is in the active phase (not in dead zone), make contributions to + // source region bookkeeping + if (is_active) { + // Compute an estimate of the spatial moments matrix for the source + // region based on parameters from this ray's crossing + MomentMatrix moment_matrix_estimate; + moment_matrix_estimate.compute_spatial_moments_matrix( + rm_local, u(), distance); + + // Aquire lock for source region + domain_->source_regions_.lock(sr).lock(); + + // Accumulate delta psi into new estimate of source region flux for + // this iteration, and update flux momements + for (int g = 0; g < negroups_; g++) { + domain_->source_regions_.scalar_flux_new(sr, g) += + angular_flux_[g] * distance; + domain_->source_regions_.flux_moments_new(sr, g) += delta_moments_[g]; + } + + // Accumulate the volume (ray segment distance), centroid, and spatial + // momement estimates into the running totals for the iteration for this + // source region. The centroid and spatial momements estimates are scaled by + // the ray segment length as part of length averaging of the estimates. + domain_->source_regions_.volume(sr) += distance; + domain_->source_regions_.centroid_iteration(sr) += midpoint * distance; + moment_matrix_estimate *= distance; + domain_->source_regions_.mom_matrix(sr) += moment_matrix_estimate; + + // Tally valid position inside the source region (e.g., midpoint of + // the ray) if not done already + if (!domain_->source_regions_.position_recorded(sr)) { + domain_->source_regions_.position(sr) = midpoint; + domain_->source_regions_.position_recorded(sr) = 1; + } + + // Release lock + domain_->source_regions_.lock(sr).unlock(); + } + + // Add source to incoming angular flux, assuming void region + for (int g = 0; g < negroups_; g++) { + angular_flux_[g] += + domain_->source_regions_.external_source(sr, g) * distance; + } +} + void RandomRay::initialize_ray(uint64_t ray_id, FlatSourceDomain* domain) { domain_ = domain; diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index c4f719b2385..f1477c3fe52 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -194,6 +194,23 @@ void validate_random_ray_inputs() fatal_error("Non-isothermal MGXS detected. Only isothermal XS data sets " "supported in random ray mode."); } + for (int g = 0; g < data::mg.num_energy_groups_; g++) { + if (material.exists_in_model) { + // Temperature and angle indices, if using multiple temperature + // data sets and/or anisotropic data sets. + // TODO: Currently assumes we are only using single temp/single angle + // data. + const int t = 0; + const int a = 0; + double sigma_t = + material.get_xs(MgxsType::TOTAL, g, NULL, NULL, NULL, t, a); + if (sigma_t <= 0.0) { + fatal_error("No zero or negative total macroscopic cross sections " + "allowed in random ray mode. If the intention is to make " + "a void material, use a cell fill of 'None' instead."); + } + } + } } // Validate ray source diff --git a/src/random_ray/source_region.cpp b/src/random_ray/source_region.cpp index 0a20af8a976..4ac86ea8cbb 100644 --- a/src/random_ray/source_region.cpp +++ b/src/random_ray/source_region.cpp @@ -46,6 +46,8 @@ void SourceRegionContainer::push_back(const SourceRegion& sr) lock_.push_back(sr.lock_); volume_.push_back(sr.volume_); volume_t_.push_back(sr.volume_t_); + volume_sq_.push_back(sr.volume_sq_); + volume_sq_t_.push_back(sr.volume_sq_t_); volume_naive_.push_back(sr.volume_naive_); position_recorded_.push_back(sr.position_recorded_); external_source_present_.push_back(sr.external_source_present_); @@ -93,6 +95,8 @@ void SourceRegionContainer::assign( lock_.clear(); volume_.clear(); volume_t_.clear(); + volume_sq_.clear(); + volume_sq_t_.clear(); volume_naive_.clear(); position_recorded_.clear(); external_source_present_.clear(); @@ -205,6 +209,8 @@ void SourceRegionContainer::mpi_sync_ranks(bool reduce_position) // next iteration. MPI_Allreduce(MPI_IN_PLACE, volume_.data(), n_source_regions_, MPI_DOUBLE, MPI_SUM, mpi::intracomm); + MPI_Allreduce(MPI_IN_PLACE, volume_sq_.data(), n_source_regions_, MPI_DOUBLE, + MPI_SUM, mpi::intracomm); MPI_Allreduce(MPI_IN_PLACE, scalar_flux_new_.data(), n_source_regions_ * negroups_, MPI_DOUBLE, MPI_SUM, mpi::intracomm); diff --git a/tests/regression_tests/random_ray_void/__init__.py b/tests/regression_tests/random_ray_void/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regression_tests/random_ray_void/flat/inputs_true.dat b/tests/regression_tests/random_ray_void/flat/inputs_true.dat new file mode 100644 index 00000000000..617a6605374 --- /dev/null +++ b/tests/regression_tests/random_ray_void/flat/inputs_true.dat @@ -0,0 +1,245 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 40 + 20 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + flat + + + + + 1 + + + 2 + + + 3 + + + 6 + flux + tracklength + + + 5 + flux + tracklength + + + 4 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_void/flat/results_true.dat b/tests/regression_tests/random_ray_void/flat/results_true.dat new file mode 100644 index 00000000000..bf395f25f78 --- /dev/null +++ b/tests/regression_tests/random_ray_void/flat/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +1.760401E+00 +1.554914E-01 +tally 2: +1.056204E-01 +5.741779E-04 +tally 3: +7.286803E-03 +2.706427E-06 diff --git a/tests/regression_tests/random_ray_void/linear/inputs_true.dat b/tests/regression_tests/random_ray_void/linear/inputs_true.dat new file mode 100644 index 00000000000..4b6a682d9db --- /dev/null +++ b/tests/regression_tests/random_ray_void/linear/inputs_true.dat @@ -0,0 +1,245 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 40 + 20 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + linear + + + + + 1 + + + 2 + + + 3 + + + 6 + flux + tracklength + + + 5 + flux + tracklength + + + 4 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_void/linear/results_true.dat b/tests/regression_tests/random_ray_void/linear/results_true.dat new file mode 100644 index 00000000000..77fade82951 --- /dev/null +++ b/tests/regression_tests/random_ray_void/linear/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +1.762592E+00 +1.558698E-01 +tally 2: +1.082935E-01 +6.029854E-04 +tally 3: +7.301975E-03 +2.717553E-06 diff --git a/tests/regression_tests/random_ray_void/test.py b/tests/regression_tests/random_ray_void/test.py new file mode 100644 index 00000000000..b48a7794d7e --- /dev/null +++ b/tests/regression_tests/random_ray_void/test.py @@ -0,0 +1,72 @@ +import os + +import openmc +from openmc.utility_funcs import change_directory +from openmc.examples import random_ray_three_region_cube +import pytest + +from tests.testing_harness import TolerantPyAPITestHarness + + +class MGXSTestHarness(TolerantPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + + +@pytest.mark.parametrize("shape", ["flat", "linear"]) +def test_random_ray_void(shape): + with change_directory(shape): + openmc.reset_auto_ids() + model = random_ray_three_region_cube() + + # There is some different logic for void depending on linear + # vs. flat, so we test both + model.settings.random_ray['source_shape'] = shape + + # As we are testing linear sources, need to have more than + # 10 inactive batches so the moments start getting computed + model.settings.inactive = 20 + model.settings.batches = 40 + + # Begin by getting handles to the cells, and setting the + # source and void areas to have no fill. We leave the absorber + # as solid. + absorber_cell = model.geometry.get_cells_by_name( + 'infinite absorber region', matching=True)[0] + void_cell = model.geometry.get_cells_by_name( + 'infinite void region', matching=True)[0] + source_cell = model.geometry.get_cells_by_name( + 'infinite source region', matching=True)[0] + + void_cell.fill = None + source_cell.fill = None + + # We also need to redefine all three tallies to use cell + # filters instead of material ones + estimator = 'tracklength' + absorber_filter = openmc.CellFilter(absorber_cell) + absorber_tally = openmc.Tally(name="Absorber Tally") + absorber_tally.filters = [absorber_filter] + absorber_tally.scores = ['flux'] + absorber_tally.estimator = estimator + + void_filter = openmc.CellFilter(void_cell) + void_tally = openmc.Tally(name="Void Tally") + void_tally.filters = [void_filter] + void_tally.scores = ['flux'] + void_tally.estimator = estimator + + source_filter = openmc.CellFilter(source_cell) + source_tally = openmc.Tally(name="Source Tally") + source_tally.filters = [source_filter] + source_tally.scores = ['flux'] + source_tally.estimator = estimator + + tallies = openmc.Tallies([source_tally, void_tally, absorber_tally]) + model.tallies = tallies + + harness = MGXSTestHarness('statepoint.40.h5', model) + harness.main() From d96e6860e627a1eb164184b153d41346785982ad Mon Sep 17 00:00:00 2001 From: Sam Pasmann <46267220+spasmann@users.noreply.github.com> Date: Tue, 18 Feb 2025 22:44:15 -0500 Subject: [PATCH 152/184] Randomized Quasi-Monte Carlo Sampling in The Random Ray Method (#3268) Co-authored-by: John Tramm --- docs/source/io_formats/settings.rst | 6 + docs/source/usersguide/random_ray.rst | 18 ++ include/openmc/constants.h | 1 + include/openmc/random_dist.h | 11 ++ include/openmc/random_ray/random_ray.h | 3 + openmc/settings.py | 8 + src/random_dist.cpp | 5 + src/random_ray/random_ray.cpp | 124 ++++++++++++- src/settings.cpp | 11 ++ .../random_ray_halton_samples/__init__.py | 0 .../random_ray_halton_samples/inputs_true.dat | 110 +++++++++++ .../results_true.dat | 171 ++++++++++++++++++ .../random_ray_halton_samples/test.py | 19 ++ 13 files changed, 480 insertions(+), 7 deletions(-) create mode 100644 tests/regression_tests/random_ray_halton_samples/__init__.py create mode 100644 tests/regression_tests/random_ray_halton_samples/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_halton_samples/results_true.dat create mode 100644 tests/regression_tests/random_ray_halton_samples/test.py diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index 4da57fe2a3e..3ddba1d5eda 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -457,6 +457,12 @@ found in the :ref:`random ray user guide `. *Default*: None + :sample_method: + Specifies the method for sampling the starting ray distribution. This + element can be set to "prng" or "halton". + + *Default*: prng + ---------------------------------- ```` Element ---------------------------------- diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index b797a721690..eff1764d4fe 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -299,6 +299,24 @@ acceptable ray source for a two-dimensional 2x2 lattice would look like: provide physical particle fixed sources in addition to the random ray source. +-------------------------- +Quasi-Monte Carlo Sampling +-------------------------- + +By default OpenMC will use a pseudorandom number generator (PRNG) to sample ray +starting locations from a uniform distribution in space and angle. +Alternatively, a randomized Halton sequence may be sampled from, which is a form +of Randomized Qusi-Monte Carlo (RQMC) sampling. RQMC sampling with random ray +has been shown to offer reduced variance as compared to regular PRNG sampling, +as the Halton sequence offers a more uniform distribution of sampled points. +Randomized Halton sampling can be enabled as:: + + settings.random_ray['sample_method'] = 'halton' + +Default behavior using OpenMC's native PRNG can be manually specified as:: + + settings.random_ray['sample_method'] = 'prng' + .. _subdivision_fsr: ---------------------------------- diff --git a/include/openmc/constants.h b/include/openmc/constants.h index a83a4a07d4b..cfcacbabf00 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -344,6 +344,7 @@ enum class SolverType { MONTE_CARLO, RANDOM_RAY }; enum class RandomRayVolumeEstimator { NAIVE, SIMULATION_AVERAGED, HYBRID }; enum class RandomRaySourceShape { FLAT, LINEAR, LINEAR_XY }; +enum class RandomRaySampleMethod { PRNG, HALTON }; //============================================================================== // Geometry Constants diff --git a/include/openmc/random_dist.h b/include/openmc/random_dist.h index 0fb186edca0..11e88ab8cce 100644 --- a/include/openmc/random_dist.h +++ b/include/openmc/random_dist.h @@ -16,6 +16,17 @@ namespace openmc { double uniform_distribution(double a, double b, uint64_t* seed); +//============================================================================== +//! Sample an integer from uniform distribution [a, b] +// +//! \param a Lower bound of uniform distribution +//! \param b Upper bound of uniform distribtion +//! \param seed A pointer to the pseudorandom seed +//! \return Sampled variate +//============================================================================== + +int64_t uniform_int_distribution(int64_t a, int64_t b, uint64_t* seed); + //============================================================================== //! Samples an energy from the Maxwell fission distribution based on a direct //! sampling scheme. diff --git a/include/openmc/random_ray/random_ray.h b/include/openmc/random_ray/random_ray.h index 4325a6420da..df1ad70bc0b 100644 --- a/include/openmc/random_ray/random_ray.h +++ b/include/openmc/random_ray/random_ray.h @@ -33,6 +33,8 @@ class RandomRay : public Particle { void initialize_ray(uint64_t ray_id, FlatSourceDomain* domain); uint64_t transport_history_based_single_ray(); + SourceSite sample_prng(); + SourceSite sample_halton(); //---------------------------------------------------------------------------- // Static data members @@ -40,6 +42,7 @@ class RandomRay : public Particle { static double distance_active_; // Active ray length static unique_ptr ray_source_; // Starting source for ray sampling static RandomRaySourceShape source_shape_; // Flag for linear source + static RandomRaySampleMethod sample_method_; // Flag for sampling method //---------------------------------------------------------------------------- // Public data members diff --git a/openmc/settings.py b/openmc/settings.py index b29393d9ac6..110d57c19f3 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -173,6 +173,9 @@ class Settings: :adjoint: Whether to run the random ray solver in adjoint mode (bool). The default is 'False'. + :sample_method: + Sampling method for the ray starting location and direction of travel. + Options are `prng` (default) or 'halton`. .. versionadded:: 0.15.0 resonance_scattering : dict @@ -1131,6 +1134,9 @@ def random_ray(self, random_ray: dict): cv.check_type('volume normalized flux tallies', value, bool) elif key == 'adjoint': cv.check_type('adjoint', value, bool) + elif key == 'sample_method': + cv.check_value('sample method', value, + ('prng', 'halton')) else: raise ValueError(f'Unable to set random ray to "{key}" which is ' 'unsupported by OpenMC') @@ -1948,6 +1954,8 @@ def _random_ray_from_xml_element(self, root): self.random_ray['adjoint'] = ( child.text in ('true', '1') ) + elif child.tag == 'sample_method': + self.random_ray['sample_method'] = child.text def to_xml_element(self, mesh_memo=None): """Create a 'settings' element to be written to an XML file. diff --git a/src/random_dist.cpp b/src/random_dist.cpp index 1aa35a689cf..b05b76f99ec 100644 --- a/src/random_dist.cpp +++ b/src/random_dist.cpp @@ -12,6 +12,11 @@ double uniform_distribution(double a, double b, uint64_t* seed) return a + (b - a) * prn(seed); } +int64_t uniform_int_distribution(int64_t a, int64_t b, uint64_t* seed) +{ + return a + static_cast(prn(seed) * (b - a + 1)); +} + double maxwell_spectrum(double T, uint64_t* seed) { // Set the random numbers diff --git a/src/random_ray/random_ray.cpp b/src/random_ray/random_ray.cpp index a8a827775ec..9917cf0c5b9 100644 --- a/src/random_ray/random_ray.cpp +++ b/src/random_ray/random_ray.cpp @@ -9,6 +9,9 @@ #include "openmc/search.h" #include "openmc/settings.h" #include "openmc/simulation.h" + +#include "openmc/distribution_spatial.h" +#include "openmc/random_dist.h" #include "openmc/source.h" namespace openmc { @@ -174,6 +177,57 @@ float exponentialG2(float tau) return num / den; } +// Implementation of the Fisher-Yates shuffle algorithm. +// Algorithm adapted from: +// https://en.cppreference.com/w/cpp/algorithm/random_shuffle#Version_3 +void fisher_yates_shuffle(vector& arr, uint64_t* seed) +{ + // Loop over the array from the last element down to the second + for (int i = arr.size() - 1; i > 0; --i) { + // Generate a random index in the range [0, i] + int j = uniform_int_distribution(0, i, seed); + std::swap(arr[i], arr[j]); + } +} + +// Function to generate randomized Halton sequence samples +// +// Algorithm adapted from: +// A. B. Owen. A randomized halton algorithm in r. Arxiv, 6 2017. +// URL https://arxiv.org/abs/1706.02808 +vector rhalton(int dim, uint64_t* seed, int64_t skip = 0) +{ + if (dim > 10) { + fatal_error("Halton sampling dimension too large"); + } + int64_t b, res, dig; + double b2r, ans; + const std::array primes = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29}; + vector halton(dim, 0.0); + + vector perm; + for (int D = 0; D < dim; ++D) { + b = primes[D]; + perm.resize(b); + b2r = 1.0 / b; + res = skip; + ans = 0.0; + + while ((1.0 - b2r) < 1.0) { + std::iota(perm.begin(), perm.end(), 0); + fisher_yates_shuffle(perm, seed); + dig = res % b; + ans += perm[dig] * b2r; + res = (res - dig) / b; + b2r /= b; + } + + halton[D] = ans; + } + + return halton; +} + //============================================================================== // RandomRay implementation //============================================================================== @@ -183,6 +237,7 @@ double RandomRay::distance_inactive_; double RandomRay::distance_active_; unique_ptr RandomRay::ray_source_; RandomRaySourceShape RandomRay::source_shape_ {RandomRaySourceShape::FLAT}; +RandomRaySampleMethod RandomRay::sample_method_ {RandomRaySampleMethod::PRNG}; RandomRay::RandomRay() : angular_flux_(data::mg.num_energy_groups_), @@ -684,14 +739,19 @@ void RandomRay::initialize_ray(uint64_t ray_id, FlatSourceDomain* domain) // set identifier for particle id() = simulation::work_index[mpi::rank] + ray_id; - // set random number seed - int64_t particle_seed = - (simulation::current_batch - 1) * settings::n_particles + id(); - init_particle_seeds(particle_seed, seeds()); - stream() = STREAM_TRACKING; + // generate source site using sample method + SourceSite site; + switch (sample_method_) { + case RandomRaySampleMethod::PRNG: + site = sample_prng(); + break; + case RandomRaySampleMethod::HALTON: + site = sample_halton(); + break; + default: + fatal_error("Unknown sample method for random ray transport."); + } - // Sample from ray source distribution - SourceSite site {ray_source_->sample(current_seed())}; site.E = lower_bound_index( data::mg.rev_energy_bins_.begin(), data::mg.rev_energy_bins_.end(), site.E); site.E = negroups_ - site.E - 1.; @@ -719,4 +779,54 @@ void RandomRay::initialize_ray(uint64_t ray_id, FlatSourceDomain* domain) } } +SourceSite RandomRay::sample_prng() +{ + // set random number seed + int64_t particle_seed = + (simulation::current_batch - 1) * settings::n_particles + id(); + init_particle_seeds(particle_seed, seeds()); + stream() = STREAM_TRACKING; + + // Sample from ray source distribution + SourceSite site {ray_source_->sample(current_seed())}; + + return site; +} + +SourceSite RandomRay::sample_halton() +{ + SourceSite site; + + // Set random number seed + int64_t batch_seed = (simulation::current_batch - 1) * settings::n_particles; + int64_t skip = id(); + init_particle_seeds(batch_seed, seeds()); + stream() = STREAM_TRACKING; + + // Calculate next samples in LDS across 5 dimensions + vector samples = rhalton(5, current_seed(), skip = skip); + + // Get spatial box of ray_source_ + SpatialBox* sb = dynamic_cast( + dynamic_cast(RandomRay::ray_source_.get())->space()); + + // Sample spatial distribution + Position xi {samples[0], samples[1], samples[2]}; + // make a small shift in position to avoid geometry floating point issues + Position shift {FP_COINCIDENT, FP_COINCIDENT, FP_COINCIDENT}; + site.r = (sb->lower_left() + shift) + + xi * ((sb->upper_right() - shift) - (sb->lower_left() + shift)); + + // Sample Polar cosine and azimuthal angles + double mu = 2.0 * samples[3] - 1.0; + double azi = 2.0 * PI * samples[4]; + // Convert to Cartesian coordinates + double c = std::sqrt(1.0 - mu * mu); + site.u.x = mu; + site.u.y = std::cos(azi) * c; + site.u.z = std::sin(azi) * c; + + return site; +} + } // namespace openmc diff --git a/src/settings.cpp b/src/settings.cpp index 61eda79967a..60263c84653 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -306,6 +306,17 @@ void get_run_parameters(pugi::xml_node node_base) FlatSourceDomain::adjoint_ = get_node_value_bool(random_ray_node, "adjoint"); } + if (check_for_node(random_ray_node, "sample_method")) { + std::string temp_str = + get_node_value(random_ray_node, "sample_method", true, true); + if (temp_str == "prng") { + RandomRay::sample_method_ = RandomRaySampleMethod::PRNG; + } else if (temp_str == "halton") { + RandomRay::sample_method_ = RandomRaySampleMethod::HALTON; + } else { + fatal_error("Unrecognized sample method: " + temp_str); + } + } } } diff --git a/tests/regression_tests/random_ray_halton_samples/__init__.py b/tests/regression_tests/random_ray_halton_samples/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regression_tests/random_ray_halton_samples/inputs_true.dat b/tests/regression_tests/random_ray_halton_samples/inputs_true.dat new file mode 100644 index 00000000000..624ab495f41 --- /dev/null +++ b/tests/regression_tests/random_ray_halton_samples/inputs_true.dat @@ -0,0 +1,110 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.126 0.126 + 10 10 + -0.63 -0.63 + +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 + + + 1.26 1.26 + 2 2 + -1.26 -1.26 + +2 2 +2 5 + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + multi-group + + 100.0 + 20.0 + + + -1.26 -1.26 -1 1.26 1.26 1 + + + True + halton + + + + + 2 2 + -1.26 -1.26 + 1.26 1.26 + + + 1 + + + 1e-05 0.0635 10.0 100.0 1000.0 500000.0 1000000.0 20000000.0 + + + 1 2 + flux fission nu-fission + analog + + + diff --git a/tests/regression_tests/random_ray_halton_samples/results_true.dat b/tests/regression_tests/random_ray_halton_samples/results_true.dat new file mode 100644 index 00000000000..7eb307da05b --- /dev/null +++ b/tests/regression_tests/random_ray_halton_samples/results_true.dat @@ -0,0 +1,171 @@ +k-combined: +8.388051E-01 7.383265E-03 +tally 1: +5.033308E+00 +5.072162E+00 +1.917335E+00 +7.360725E-01 +4.666410E+00 +4.360038E+00 +2.851812E+00 +1.629362E+00 +4.365590E-01 +3.818884E-02 +1.062497E+00 +2.262071E-01 +1.697621E+00 +5.829333E-01 +5.639912E-02 +6.427568E-04 +1.372642E-01 +3.807294E-03 +2.376683E+00 +1.151027E+00 +8.060902E-02 +1.323179E-03 +1.961862E-01 +7.837693E-03 +7.145452E+00 +1.037540E+01 +8.551803E-02 +1.486269E-03 +2.081363E-01 +8.803955E-03 +2.053205E+01 +8.469498E+01 +3.235618E-02 +2.102891E-04 +8.006311E-02 +1.287559E-03 +1.326545E+01 +3.519484E+01 +1.867471E-01 +6.975133E-03 +5.194275E-01 +5.396284E-02 +7.558115E+00 +1.142535E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +3.386211E+00 +2.294414E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.827274E+00 +6.782305E-01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.702858E+00 +1.489752E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +7.475537E+00 +1.133971E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.828685E+01 +6.719606E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.143600E+01 +2.615734E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.590713E+00 +4.224107E+00 +1.705847E+00 +5.831967E-01 +4.151691E+00 +3.454497E+00 +2.730853E+00 +1.495413E+00 +4.072633E-01 +3.325944E-02 +9.911973E-01 +1.970084E-01 +1.664732E+00 +5.598668E-01 +5.385645E-02 +5.858353E-04 +1.310758E-01 +3.470126E-03 +2.312239E+00 +1.088485E+00 +7.662778E-02 +1.195422E-03 +1.864967E-01 +7.080943E-03 +7.105765E+00 +1.025960E+01 +8.287512E-02 +1.396474E-03 +2.017039E-01 +8.272053E-03 +2.099024E+01 +8.854251E+01 +3.191885E-02 +2.048368E-04 +7.898095E-02 +1.254175E-03 +1.355820E+01 +3.676862E+01 +1.815102E-01 +6.590727E-03 +5.048614E-01 +5.098890E-02 +5.093659E+00 +5.192360E+00 +1.874632E+00 +7.031793E-01 +4.562478E+00 +4.165199E+00 +2.870213E+00 +1.650126E+00 +4.244068E-01 +3.608745E-02 +1.032921E+00 +2.137598E-01 +1.703400E+00 +5.873029E-01 +5.464557E-02 +6.042855E-04 +1.329964E-01 +3.579413E-03 +2.389118E+00 +1.163674E+00 +7.832237E-02 +1.251254E-03 +1.906210E-01 +7.411654E-03 +7.162706E+00 +1.042515E+01 +8.273831E-02 +1.391799E-03 +2.013709E-01 +8.244359E-03 +2.043145E+01 +8.383557E+01 +3.096158E-02 +1.924116E-04 +7.661226E-02 +1.178098E-03 +1.314148E+01 +3.454143E+01 +1.771732E-01 +6.279887E-03 +4.927984E-01 +4.858410E-02 diff --git a/tests/regression_tests/random_ray_halton_samples/test.py b/tests/regression_tests/random_ray_halton_samples/test.py new file mode 100644 index 00000000000..478b6502648 --- /dev/null +++ b/tests/regression_tests/random_ray_halton_samples/test.py @@ -0,0 +1,19 @@ +import os + +from openmc.examples import random_ray_lattice + +from tests.testing_harness import TolerantPyAPITestHarness + + +class MGXSTestHarness(TolerantPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + +def test_random_ray_halton_samples(): + model = random_ray_lattice() + model.settings.random_ray['sample_method'] = 'halton' + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() From 06a88526cfb75786bf69eb453bd6dda01ffad864 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 19 Feb 2025 07:48:39 -0600 Subject: [PATCH 153/184] Relax requirement on polar/azimuthal axis for wwinp conversion (#3307) --- openmc/weight_windows.py | 16 +++++++++++++--- .../regression_tests/weightwindows/ww_n_cyl.txt | 2 +- .../regression_tests/weightwindows/ww_n_sph.txt | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/openmc/weight_windows.py b/openmc/weight_windows.py index 0502d309f5e..d52b81925aa 100644 --- a/openmc/weight_windows.py +++ b/openmc/weight_windows.py @@ -513,9 +513,19 @@ def wwinp_to_wws(path: PathLike) -> list[WeightWindows]: line_arr = np.fromstring(wwinp.readline(), sep=' ') xyz2 = line_arr[:3] - # oriented polar and azimuthal vectors aren't yet supported - if np.count_nonzero(xyz1) or np.count_nonzero(xyz2): - raise NotImplementedError('Custom sphere/cylinder orientations are not supported') + # Get polar and azimuthal axes + polar_axis = xyz1 - xyz0 + azimuthal_axis = xyz2 - xyz0 + + # Check for polar axis other than (0, 0, 1) + norm = np.linalg.norm(polar_axis) + if not np.isclose(polar_axis[2]/norm, 1.0): + raise NotImplementedError('Polar axis not aligned to z-axis not supported') + + # Check for azimuthal axis other than (1, 0, 0) + norm = np.linalg.norm(azimuthal_axis) + if not np.isclose(azimuthal_axis[0]/norm, 1.0): + raise NotImplementedError('Azimuthal axis not aligned to x-axis not supported') # read geometry type nwg = int(line_arr[-1]) diff --git a/tests/regression_tests/weightwindows/ww_n_cyl.txt b/tests/regression_tests/weightwindows/ww_n_cyl.txt index 89232628fa1..6f6144ca302 100644 --- a/tests/regression_tests/weightwindows/ww_n_cyl.txt +++ b/tests/regression_tests/weightwindows/ww_n_cyl.txt @@ -2,7 +2,7 @@ 1 8.0000 7.0000 8.0000 0.0000 0.0000 -9.0001 2.0000 2.0000 4.0000 0.0000 0.0000 0.0000 -0.0000 0.0000 0.0000 2.0000 +1.0000 0.0000 -9.0001 2.0000 0.0000 3.0000 3.0200 1.0000 5.0000 6.0001 1.0000 0.0000 4.0000 8.0080 1.0000 3.0000 14.002 diff --git a/tests/regression_tests/weightwindows/ww_n_sph.txt b/tests/regression_tests/weightwindows/ww_n_sph.txt index 3f9b8dab6d2..a4418ceeb58 100644 --- a/tests/regression_tests/weightwindows/ww_n_sph.txt +++ b/tests/regression_tests/weightwindows/ww_n_sph.txt @@ -2,7 +2,7 @@ 1 8.0000 7.0000 8.0000 0.0000 0.0000 -9.0001 2.0000 4.0000 4.0000 0.0000 0.0000 0.0000 -0.0000 0.0000 0.0000 3.0000 +1.0000 0.0000 -9.0001 3.0000 0.0000 3.0000 3.0200 1.0000 5.0000 6.0001 1.0000 0.0000 2.0000 0.25000 1.0000 1.0000 0.50000 From bcc2a4c5f0260d010ae7f5989c453e7799d7378b Mon Sep 17 00:00:00 2001 From: Gavin Ridley Date: Wed, 19 Feb 2025 15:58:14 -0600 Subject: [PATCH 154/184] simplify mechanism to detect if geometry entity is DAG (#3269) --- include/openmc/cell.h | 12 ++++------ include/openmc/dagmc.h | 6 +++++ include/openmc/surface.h | 50 ++++++++++++++++++--------------------- include/openmc/universe.h | 10 ++++---- src/cell.cpp | 8 ------- src/dagmc.cpp | 11 ++------- src/surface.cpp | 39 ++++++++++++------------------ 7 files changed, 56 insertions(+), 80 deletions(-) diff --git a/include/openmc/cell.h b/include/openmc/cell.h index d7a3cad182b..05abbfdfa6f 100644 --- a/include/openmc/cell.h +++ b/include/openmc/cell.h @@ -349,12 +349,8 @@ class Cell { vector offset_; //!< Distribcell offset table - // Accessors - const GeometryType& geom_type() const { return geom_type_; } - GeometryType& geom_type() { return geom_type_; } - -private: - GeometryType geom_type_; //!< Geometric representation type (CSG, DAGMC) + // Right now, either CSG or DAGMC cells are used. + virtual GeometryType geom_type() const = 0; }; struct CellInstanceItem { @@ -368,7 +364,7 @@ class CSGCell : public Cell { public: //---------------------------------------------------------------------------- // Constructors - CSGCell(); + CSGCell() = default; explicit CSGCell(pugi::xml_node cell_node); //---------------------------------------------------------------------------- @@ -395,6 +391,8 @@ class CSGCell : public Cell { bool is_simple() const override { return region_.is_simple(); } + virtual GeometryType geom_type() const override { return GeometryType::CSG; } + protected: //! Returns the beginning position of a parenthesis block (immediately before //! two surface tokens) in the RPN given a starting position at the end of diff --git a/include/openmc/dagmc.h b/include/openmc/dagmc.h index 9c4e47fdfd3..82ef1b644f3 100644 --- a/include/openmc/dagmc.h +++ b/include/openmc/dagmc.h @@ -54,6 +54,8 @@ class DAGSurface : public Surface { inline void to_hdf5_inner(hid_t group_id) const override {}; + virtual GeometryType geom_type() const override { return GeometryType::DAG; } + // Accessor methods moab::DagMC* dagmc_ptr() const { return dagmc_ptr_.get(); } int32_t dag_index() const { return dag_index_; } @@ -78,6 +80,8 @@ class DAGCell : public Cell { void to_hdf5_inner(hid_t group_id) const override; + virtual GeometryType geom_type() const override { return GeometryType::DAG; } + // Accessor methods moab::DagMC* dagmc_ptr() const { return dagmc_ptr_.get(); } int32_t dag_index() const { return dag_index_; } @@ -165,6 +169,8 @@ class DAGUniverse : public Universe { void to_hdf5(hid_t universes_group) const override; + virtual GeometryType geom_type() const override { return GeometryType::DAG; } + // Data Members std::shared_ptr dagmc_instance_; //!< DAGMC Instance for this universe diff --git a/include/openmc/surface.h b/include/openmc/surface.h index 2397a64cc4c..549e8f6abac 100644 --- a/include/openmc/surface.h +++ b/include/openmc/surface.h @@ -90,30 +90,26 @@ class Surface { //! Get the BoundingBox for this surface. virtual BoundingBox bounding_box(bool /*pos_side*/) const { return {}; } - // Accessors - const GeometryType& geom_type() const { return geom_type_; } - GeometryType& geom_type() { return geom_type_; } - -private: - GeometryType geom_type_; //!< Geometry type indicator (CSG or DAGMC) + /* Must specify if this is a CSG or DAGMC-type surface. Only + * the DAGMC surface should return the DAG type geometry, so + * by default, this returns the CSG. The main difference is that + * if the geom_type is found to be DAG in the geometry handling code, + * some DAGMC-specific operations get carried out like resetting + * the particle's intersection history when necessary. + */ + virtual GeometryType geom_type() const { return GeometryType::CSG; } protected: virtual void to_hdf5_inner(hid_t group_id) const = 0; }; -class CSGSurface : public Surface { -public: - explicit CSGSurface(pugi::xml_node surf_node); - CSGSurface(); -}; - //============================================================================== //! A plane perpendicular to the x-axis. // //! The plane is described by the equation \f$x - x_0 = 0\f$ //============================================================================== -class SurfaceXPlane : public CSGSurface { +class SurfaceXPlane : public Surface { public: explicit SurfaceXPlane(pugi::xml_node surf_node); double evaluate(Position r) const override; @@ -131,7 +127,7 @@ class SurfaceXPlane : public CSGSurface { //! The plane is described by the equation \f$y - y_0 = 0\f$ //============================================================================== -class SurfaceYPlane : public CSGSurface { +class SurfaceYPlane : public Surface { public: explicit SurfaceYPlane(pugi::xml_node surf_node); double evaluate(Position r) const override; @@ -149,7 +145,7 @@ class SurfaceYPlane : public CSGSurface { //! The plane is described by the equation \f$z - z_0 = 0\f$ //============================================================================== -class SurfaceZPlane : public CSGSurface { +class SurfaceZPlane : public Surface { public: explicit SurfaceZPlane(pugi::xml_node surf_node); double evaluate(Position r) const override; @@ -167,7 +163,7 @@ class SurfaceZPlane : public CSGSurface { //! The plane is described by the equation \f$A x + B y + C z - D = 0\f$ //============================================================================== -class SurfacePlane : public CSGSurface { +class SurfacePlane : public Surface { public: explicit SurfacePlane(pugi::xml_node surf_node); double evaluate(Position r) const override; @@ -185,7 +181,7 @@ class SurfacePlane : public CSGSurface { //! \f$(y - y_0)^2 + (z - z_0)^2 - R^2 = 0\f$ //============================================================================== -class SurfaceXCylinder : public CSGSurface { +class SurfaceXCylinder : public Surface { public: explicit SurfaceXCylinder(pugi::xml_node surf_node); double evaluate(Position r) const override; @@ -204,7 +200,7 @@ class SurfaceXCylinder : public CSGSurface { //! \f$(x - x_0)^2 + (z - z_0)^2 - R^2 = 0\f$ //============================================================================== -class SurfaceYCylinder : public CSGSurface { +class SurfaceYCylinder : public Surface { public: explicit SurfaceYCylinder(pugi::xml_node surf_node); double evaluate(Position r) const override; @@ -223,7 +219,7 @@ class SurfaceYCylinder : public CSGSurface { //! \f$(x - x_0)^2 + (y - y_0)^2 - R^2 = 0\f$ //============================================================================== -class SurfaceZCylinder : public CSGSurface { +class SurfaceZCylinder : public Surface { public: explicit SurfaceZCylinder(pugi::xml_node surf_node); double evaluate(Position r) const override; @@ -242,7 +238,7 @@ class SurfaceZCylinder : public CSGSurface { //! \f$(x - x_0)^2 + (y - y_0)^2 + (z - z_0)^2 - R^2 = 0\f$ //============================================================================== -class SurfaceSphere : public CSGSurface { +class SurfaceSphere : public Surface { public: explicit SurfaceSphere(pugi::xml_node surf_node); double evaluate(Position r) const override; @@ -261,7 +257,7 @@ class SurfaceSphere : public CSGSurface { //! \f$(y - y_0)^2 + (z - z_0)^2 - R^2 (x - x_0)^2 = 0\f$ //============================================================================== -class SurfaceXCone : public CSGSurface { +class SurfaceXCone : public Surface { public: explicit SurfaceXCone(pugi::xml_node surf_node); double evaluate(Position r) const override; @@ -279,7 +275,7 @@ class SurfaceXCone : public CSGSurface { //! \f$(x - x_0)^2 + (z - z_0)^2 - R^2 (y - y_0)^2 = 0\f$ //============================================================================== -class SurfaceYCone : public CSGSurface { +class SurfaceYCone : public Surface { public: explicit SurfaceYCone(pugi::xml_node surf_node); double evaluate(Position r) const override; @@ -297,7 +293,7 @@ class SurfaceYCone : public CSGSurface { //! \f$(x - x_0)^2 + (y - y_0)^2 - R^2 (z - z_0)^2 = 0\f$ //============================================================================== -class SurfaceZCone : public CSGSurface { +class SurfaceZCone : public Surface { public: explicit SurfaceZCone(pugi::xml_node surf_node); double evaluate(Position r) const override; @@ -315,7 +311,7 @@ class SurfaceZCone : public CSGSurface { //! 0\f$ //============================================================================== -class SurfaceQuadric : public CSGSurface { +class SurfaceQuadric : public Surface { public: explicit SurfaceQuadric(pugi::xml_node surf_node); double evaluate(Position r) const override; @@ -333,7 +329,7 @@ class SurfaceQuadric : public CSGSurface { //! \f$(x-x_0)^2/B^2 + (\sqrt{(y-y_0)^2 + (z-z_0)^2} - A)^2/C^2 -1 \f$ //============================================================================== -class SurfaceXTorus : public CSGSurface { +class SurfaceXTorus : public Surface { public: explicit SurfaceXTorus(pugi::xml_node surf_node); double evaluate(Position r) const override; @@ -350,7 +346,7 @@ class SurfaceXTorus : public CSGSurface { //! \f$(y-y_0)^2/B^2 + (\sqrt{(x-x_0)^2 + (z-z_0)^2} - A)^2/C^2 -1 \f$ //============================================================================== -class SurfaceYTorus : public CSGSurface { +class SurfaceYTorus : public Surface { public: explicit SurfaceYTorus(pugi::xml_node surf_node); double evaluate(Position r) const override; @@ -367,7 +363,7 @@ class SurfaceYTorus : public CSGSurface { //! \f$(z-z_0)^2/B^2 + (\sqrt{(x-x_0)^2 + (y-y_0)^2} - A)^2/C^2 -1 \f$ //============================================================================== -class SurfaceZTorus : public CSGSurface { +class SurfaceZTorus : public Surface { public: explicit SurfaceZTorus(pugi::xml_node surf_node); double evaluate(Position r) const override; diff --git a/include/openmc/universe.h b/include/openmc/universe.h index 9fea06bccba..e8fbacfdc7b 100644 --- a/include/openmc/universe.h +++ b/include/openmc/universe.h @@ -38,13 +38,13 @@ class Universe { BoundingBox bounding_box() const; - const GeometryType& geom_type() const { return geom_type_; } - GeometryType& geom_type() { return geom_type_; } + /* By default, universes are CSG universes. The DAGMC + * universe overrides standard behaviors, and in the future, + * other things might too. + */ + virtual GeometryType geom_type() const { return GeometryType::CSG; } unique_ptr partitioner_; - -private: - GeometryType geom_type_ = GeometryType::CSG; }; //============================================================================== diff --git a/src/cell.cpp b/src/cell.cpp index d4d28fb70e6..fa8be4496f1 100644 --- a/src/cell.cpp +++ b/src/cell.cpp @@ -249,16 +249,8 @@ void Cell::to_hdf5(hid_t cell_group) const // CSGCell implementation //============================================================================== -// default constructor -CSGCell::CSGCell() -{ - geom_type() = GeometryType::CSG; -} - CSGCell::CSGCell(pugi::xml_node cell_node) { - geom_type() = GeometryType::CSG; - if (check_for_node(cell_node, "id")) { id_ = std::stoi(get_node_value(cell_node, "id")); } else { diff --git a/src/dagmc.cpp b/src/dagmc.cpp index e24b3fbf629..ea5be04ba67 100644 --- a/src/dagmc.cpp +++ b/src/dagmc.cpp @@ -129,8 +129,6 @@ void DAGUniverse::set_id() void DAGUniverse::initialize() { - geom_type() = GeometryType::DAG; - #ifdef OPENMC_UWUW // read uwuw materials from the .h5m file if present read_uwuw_materials(); @@ -661,10 +659,7 @@ void DAGUniverse::override_assign_material(std::unique_ptr& c) const //============================================================================== DAGCell::DAGCell(std::shared_ptr dag_ptr, int32_t dag_idx) - : Cell {}, dagmc_ptr_(dag_ptr), dag_index_(dag_idx) -{ - geom_type() = GeometryType::DAG; -}; + : Cell {}, dagmc_ptr_(dag_ptr), dag_index_(dag_idx) {}; std::pair DAGCell::distance( Position r, Direction u, int32_t on_surface, GeometryState* p) const @@ -765,9 +760,7 @@ BoundingBox DAGCell::bounding_box() const DAGSurface::DAGSurface(std::shared_ptr dag_ptr, int32_t dag_idx) : Surface {}, dagmc_ptr_(dag_ptr), dag_index_(dag_idx) -{ - geom_type() = GeometryType::DAG; -} // empty constructor +{} // empty constructor moab::EntityHandle DAGSurface::mesh_handle() const { diff --git a/src/surface.cpp b/src/surface.cpp index 30a8d1f211f..3658b3fa12f 100644 --- a/src/surface.cpp +++ b/src/surface.cpp @@ -187,15 +187,6 @@ void Surface::to_hdf5(hid_t group_id) const close_group(surf_group); } -CSGSurface::CSGSurface() : Surface {} -{ - geom_type() = GeometryType::CSG; -}; -CSGSurface::CSGSurface(pugi::xml_node surf_node) : Surface {surf_node} -{ - geom_type() = GeometryType::CSG; -}; - //============================================================================== // Generic functions for x-, y-, and z-, planes. //============================================================================== @@ -218,7 +209,7 @@ double axis_aligned_plane_distance( // SurfaceXPlane implementation //============================================================================== -SurfaceXPlane::SurfaceXPlane(pugi::xml_node surf_node) : CSGSurface(surf_node) +SurfaceXPlane::SurfaceXPlane(pugi::xml_node surf_node) : Surface(surf_node) { read_coeffs(surf_node, id_, {&x0_}); } @@ -258,7 +249,7 @@ BoundingBox SurfaceXPlane::bounding_box(bool pos_side) const // SurfaceYPlane implementation //============================================================================== -SurfaceYPlane::SurfaceYPlane(pugi::xml_node surf_node) : CSGSurface(surf_node) +SurfaceYPlane::SurfaceYPlane(pugi::xml_node surf_node) : Surface(surf_node) { read_coeffs(surf_node, id_, {&y0_}); } @@ -298,7 +289,7 @@ BoundingBox SurfaceYPlane::bounding_box(bool pos_side) const // SurfaceZPlane implementation //============================================================================== -SurfaceZPlane::SurfaceZPlane(pugi::xml_node surf_node) : CSGSurface(surf_node) +SurfaceZPlane::SurfaceZPlane(pugi::xml_node surf_node) : Surface(surf_node) { read_coeffs(surf_node, id_, {&z0_}); } @@ -338,7 +329,7 @@ BoundingBox SurfaceZPlane::bounding_box(bool pos_side) const // SurfacePlane implementation //============================================================================== -SurfacePlane::SurfacePlane(pugi::xml_node surf_node) : CSGSurface(surf_node) +SurfacePlane::SurfacePlane(pugi::xml_node surf_node) : Surface(surf_node) { read_coeffs(surf_node, id_, {&A_, &B_, &C_, &D_}); } @@ -457,7 +448,7 @@ Direction axis_aligned_cylinder_normal( //============================================================================== SurfaceXCylinder::SurfaceXCylinder(pugi::xml_node surf_node) - : CSGSurface(surf_node) + : Surface(surf_node) { read_coeffs(surf_node, id_, {&y0_, &z0_, &radius_}); } @@ -500,7 +491,7 @@ BoundingBox SurfaceXCylinder::bounding_box(bool pos_side) const //============================================================================== SurfaceYCylinder::SurfaceYCylinder(pugi::xml_node surf_node) - : CSGSurface(surf_node) + : Surface(surf_node) { read_coeffs(surf_node, id_, {&x0_, &z0_, &radius_}); } @@ -544,7 +535,7 @@ BoundingBox SurfaceYCylinder::bounding_box(bool pos_side) const //============================================================================== SurfaceZCylinder::SurfaceZCylinder(pugi::xml_node surf_node) - : CSGSurface(surf_node) + : Surface(surf_node) { read_coeffs(surf_node, id_, {&x0_, &y0_, &radius_}); } @@ -587,7 +578,7 @@ BoundingBox SurfaceZCylinder::bounding_box(bool pos_side) const // SurfaceSphere implementation //============================================================================== -SurfaceSphere::SurfaceSphere(pugi::xml_node surf_node) : CSGSurface(surf_node) +SurfaceSphere::SurfaceSphere(pugi::xml_node surf_node) : Surface(surf_node) { read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &radius_}); } @@ -753,7 +744,7 @@ Direction axis_aligned_cone_normal( // SurfaceXCone implementation //============================================================================== -SurfaceXCone::SurfaceXCone(pugi::xml_node surf_node) : CSGSurface(surf_node) +SurfaceXCone::SurfaceXCone(pugi::xml_node surf_node) : Surface(surf_node) { read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &radius_sq_}); } @@ -785,7 +776,7 @@ void SurfaceXCone::to_hdf5_inner(hid_t group_id) const // SurfaceYCone implementation //============================================================================== -SurfaceYCone::SurfaceYCone(pugi::xml_node surf_node) : CSGSurface(surf_node) +SurfaceYCone::SurfaceYCone(pugi::xml_node surf_node) : Surface(surf_node) { read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &radius_sq_}); } @@ -817,7 +808,7 @@ void SurfaceYCone::to_hdf5_inner(hid_t group_id) const // SurfaceZCone implementation //============================================================================== -SurfaceZCone::SurfaceZCone(pugi::xml_node surf_node) : CSGSurface(surf_node) +SurfaceZCone::SurfaceZCone(pugi::xml_node surf_node) : Surface(surf_node) { read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &radius_sq_}); } @@ -849,7 +840,7 @@ void SurfaceZCone::to_hdf5_inner(hid_t group_id) const // SurfaceQuadric implementation //============================================================================== -SurfaceQuadric::SurfaceQuadric(pugi::xml_node surf_node) : CSGSurface(surf_node) +SurfaceQuadric::SurfaceQuadric(pugi::xml_node surf_node) : Surface(surf_node) { read_coeffs( surf_node, id_, {&A_, &B_, &C_, &D_, &E_, &F_, &G_, &H_, &J_, &K_}); @@ -1009,7 +1000,7 @@ double torus_distance(double x1, double x2, double x3, double u1, double u2, // SurfaceXTorus implementation //============================================================================== -SurfaceXTorus::SurfaceXTorus(pugi::xml_node surf_node) : CSGSurface(surf_node) +SurfaceXTorus::SurfaceXTorus(pugi::xml_node surf_node) : Surface(surf_node) { read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &A_, &B_, &C_}); } @@ -1062,7 +1053,7 @@ Direction SurfaceXTorus::normal(Position r) const // SurfaceYTorus implementation //============================================================================== -SurfaceYTorus::SurfaceYTorus(pugi::xml_node surf_node) : CSGSurface(surf_node) +SurfaceYTorus::SurfaceYTorus(pugi::xml_node surf_node) : Surface(surf_node) { read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &A_, &B_, &C_}); } @@ -1115,7 +1106,7 @@ Direction SurfaceYTorus::normal(Position r) const // SurfaceZTorus implementation //============================================================================== -SurfaceZTorus::SurfaceZTorus(pugi::xml_node surf_node) : CSGSurface(surf_node) +SurfaceZTorus::SurfaceZTorus(pugi::xml_node surf_node) : Surface(surf_node) { read_coeffs(surf_node, id_, {&x0_, &y0_, &z0_, &A_, &B_, &C_}); } From aa4de82258424ae9f1dbbb7650ae0ba6b1b6c8b5 Mon Sep 17 00:00:00 2001 From: Gavin Ridley Date: Wed, 19 Feb 2025 19:03:20 -0600 Subject: [PATCH 155/184] remove gsl-lite dependency (#3225) Co-authored-by: Paul Romano --- .gitmodules | 3 - CMakeLists.txt | 15 +- MANIFEST.in | 1 - cmake/OpenMCConfig.cmake.in | 1 - include/openmc/cell.h | 7 +- include/openmc/distribution.h | 6 +- include/openmc/distribution_spatial.h | 5 +- include/openmc/interpolate.h | 12 +- include/openmc/material.h | 10 +- include/openmc/mcpl_interface.h | 10 +- include/openmc/mesh.h | 6 +- include/openmc/nuclide.h | 10 +- include/openmc/photon.h | 3 +- include/openmc/reaction.h | 8 +- include/openmc/span.h | 237 ++++++++++++++++++ include/openmc/state_point.h | 14 +- include/openmc/tallies/filter.h | 5 +- include/openmc/tallies/filter_azimuthal.h | 5 +- include/openmc/tallies/filter_cell.h | 5 +- include/openmc/tallies/filter_cell_instance.h | 9 +- include/openmc/tallies/filter_collision.h | 4 +- include/openmc/tallies/filter_delayedgroup.h | 5 +- include/openmc/tallies/filter_energy.h | 5 +- include/openmc/tallies/filter_energyfunc.h | 3 +- include/openmc/tallies/filter_material.h | 5 +- include/openmc/tallies/filter_mu.h | 5 +- include/openmc/tallies/filter_musurface.h | 2 - include/openmc/tallies/filter_particle.h | 3 +- include/openmc/tallies/filter_polar.h | 5 +- include/openmc/tallies/filter_sph_harm.h | 4 +- include/openmc/tallies/filter_surface.h | 5 +- include/openmc/tallies/filter_time.h | 5 +- include/openmc/tallies/filter_universe.h | 5 +- include/openmc/tallies/tally.h | 6 +- include/openmc/volume_calc.h | 1 - include/openmc/weight_windows.h | 12 +- src/cell.cpp | 16 +- src/dagmc.cpp | 4 +- src/distribution.cpp | 8 +- src/distribution_spatial.cpp | 4 +- src/geometry_aux.cpp | 2 +- src/material.cpp | 15 +- src/mcpl_interface.cpp | 6 +- src/mesh.cpp | 12 +- src/nuclide.cpp | 25 +- src/reaction.cpp | 8 +- src/secondary_thermal.cpp | 5 +- src/simulation.cpp | 6 +- src/state_point.cpp | 8 +- src/surface.cpp | 1 - src/tallies/filter.cpp | 5 +- src/tallies/filter_azimuthal.cpp | 4 +- src/tallies/filter_cell.cpp | 8 +- src/tallies/filter_cell_instance.cpp | 27 +- src/tallies/filter_collision.cpp | 4 +- src/tallies/filter_delayedgroup.cpp | 2 +- src/tallies/filter_distribcell.cpp | 6 +- src/tallies/filter_energy.cpp | 6 +- src/tallies/filter_energyfunc.cpp | 4 +- src/tallies/filter_material.cpp | 8 +- src/tallies/filter_mesh.cpp | 1 - src/tallies/filter_mu.cpp | 4 +- src/tallies/filter_particle.cpp | 2 +- src/tallies/filter_polar.cpp | 4 +- src/tallies/filter_sph_harm.cpp | 8 +- src/tallies/filter_surface.cpp | 8 +- src/tallies/filter_time.cpp | 2 +- src/tallies/filter_universe.cpp | 8 +- src/tallies/filter_zernike.cpp | 4 +- src/tallies/tally.cpp | 11 +- src/weight_windows.cpp | 11 +- vendor/gsl-lite | 1 - 72 files changed, 458 insertions(+), 247 deletions(-) create mode 100644 include/openmc/span.h delete mode 160000 vendor/gsl-lite diff --git a/.gitmodules b/.gitmodules index 4dde5fd5ee5..f84d09bb1f9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,6 @@ [submodule "vendor/pugixml"] path = vendor/pugixml url = https://github.com/zeux/pugixml.git -[submodule "vendor/gsl-lite"] - path = vendor/gsl-lite - url = https://github.com/martinmoene/gsl-lite.git [submodule "vendor/xtensor"] path = vendor/xtensor url = https://github.com/xtensor-stack/xtensor.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 94caec757ab..35694a14bc8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -293,19 +293,6 @@ if (NOT xtensor_FOUND) add_subdirectory(vendor/xtensor) endif() -#=============================================================================== -# GSL header-only library -#=============================================================================== - -find_package_write_status(gsl-lite) -if (NOT gsl-lite_FOUND) - add_subdirectory(vendor/gsl-lite) - - # Make sure contract violations throw exceptions - target_compile_definitions(gsl-lite-v1 INTERFACE GSL_THROW_ON_CONTRACT_VIOLATION) - target_compile_definitions(gsl-lite-v1 INTERFACE gsl_CONFIG_ALLOWS_NONSTRICT_SPAN_COMPARISON=1) -endif() - #=============================================================================== # Catch2 library #=============================================================================== @@ -519,7 +506,7 @@ endif() # target_link_libraries treats any arguments starting with - but not -l as # linker flags. Thus, we can pass both linker flags and libraries together. target_link_libraries(libopenmc ${ldflags} ${HDF5_LIBRARIES} ${HDF5_HL_LIBRARIES} - xtensor gsl::gsl-lite-v1 fmt::fmt ${CMAKE_DL_LIBS}) + xtensor fmt::fmt ${CMAKE_DL_LIBS}) if(TARGET pugixml::pugixml) target_link_libraries(libopenmc pugixml::pugixml) diff --git a/MANIFEST.in b/MANIFEST.in index b73218af0dc..cdc7e2abcf0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -43,6 +43,5 @@ recursive-include vendor *.hh recursive-include vendor *.hpp recursive-include vendor *.pc.in recursive-include vendor *.natvis -include vendor/gsl-lite/include/gsl/gsl prune docs/build prune docs/source/pythonapi/generated/ diff --git a/cmake/OpenMCConfig.cmake.in b/cmake/OpenMCConfig.cmake.in index b02bbffe5cf..21c55230334 100644 --- a/cmake/OpenMCConfig.cmake.in +++ b/cmake/OpenMCConfig.cmake.in @@ -1,7 +1,6 @@ get_filename_component(OpenMC_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY) find_package(fmt REQUIRED HINTS ${OpenMC_CMAKE_DIR}/../fmt) -find_package(gsl-lite REQUIRED HINTS ${OpenMC_CMAKE_DIR}/../gsl-lite) find_package(pugixml REQUIRED HINTS ${OpenMC_CMAKE_DIR}/../pugixml) find_package(xtl REQUIRED HINTS ${OpenMC_CMAKE_DIR}/../xtl) find_package(xtensor REQUIRED HINTS ${OpenMC_CMAKE_DIR}/../xtensor) diff --git a/include/openmc/cell.h b/include/openmc/cell.h index 05abbfdfa6f..98709fb33ea 100644 --- a/include/openmc/cell.h +++ b/include/openmc/cell.h @@ -10,7 +10,6 @@ #include "hdf5.h" #include "pugixml.hpp" -#include #include "openmc/bounding_box.h" #include "openmc/constants.h" @@ -128,7 +127,7 @@ class Region { void add_precedence(); //! Add parenthesis to enforce precedence - gsl::index add_parentheses(gsl::index start); + int64_t add_parentheses(int64_t start); //! Remove complement operators from the expression void remove_complement_ops(); @@ -418,8 +417,8 @@ struct CellInstance { return index_cell == other.index_cell && instance == other.instance; } - gsl::index index_cell; - gsl::index instance; + int64_t index_cell; + int64_t instance; }; //! Structure necessary for inserting CellInstance into hashed STL data diff --git a/include/openmc/distribution.h b/include/openmc/distribution.h index e70c803de2f..854cf7d7719 100644 --- a/include/openmc/distribution.h +++ b/include/openmc/distribution.h @@ -7,10 +7,10 @@ #include // for size_t #include "pugixml.hpp" -#include #include "openmc/constants.h" #include "openmc/memory.h" // for unique_ptr +#include "openmc/span.h" #include "openmc/vector.h" // for vector namespace openmc { @@ -44,9 +44,9 @@ class DiscreteIndex { public: DiscreteIndex() {}; DiscreteIndex(pugi::xml_node node); - DiscreteIndex(gsl::span p); + DiscreteIndex(span p); - void assign(gsl::span p); + void assign(span p); //! Sample a value from the distribution //! \param seed Pseudorandom number seed pointer diff --git a/include/openmc/distribution_spatial.h b/include/openmc/distribution_spatial.h index 8ff766d1333..9c3bc743ffb 100644 --- a/include/openmc/distribution_spatial.h +++ b/include/openmc/distribution_spatial.h @@ -6,6 +6,7 @@ #include "openmc/distribution.h" #include "openmc/mesh.h" #include "openmc/position.h" +#include "openmc/span.h" namespace openmc { @@ -104,7 +105,7 @@ class SphericalIndependent : public SpatialDistribution { class MeshSpatial : public SpatialDistribution { public: explicit MeshSpatial(pugi::xml_node node); - explicit MeshSpatial(int32_t mesh_id, gsl::span strengths); + explicit MeshSpatial(int32_t mesh_id, span strengths); //! Sample a position from the distribution //! \param seed Pseudorandom number seed pointer @@ -144,7 +145,7 @@ class PointCloud : public SpatialDistribution { public: explicit PointCloud(pugi::xml_node node); explicit PointCloud( - std::vector point_cloud, gsl::span strengths); + std::vector point_cloud, span strengths); //! Sample a position from the distribution //! \param seed Pseudorandom number seed pointer diff --git a/include/openmc/interpolate.h b/include/openmc/interpolate.h index db501f71b66..31ae8b0e3f7 100644 --- a/include/openmc/interpolate.h +++ b/include/openmc/interpolate.h @@ -4,10 +4,9 @@ #include #include -#include - #include "openmc/error.h" #include "openmc/search.h" +#include "openmc/span.h" namespace openmc { @@ -36,8 +35,8 @@ inline double interpolate_log_log( return y0 * std::exp(f * std::log(y1 / y0)); } -inline double interpolate_lagrangian(gsl::span xs, - gsl::span ys, int idx, double x, int order) +inline double interpolate_lagrangian( + span xs, span ys, int idx, double x, int order) { double output {0.0}; @@ -56,9 +55,8 @@ inline double interpolate_lagrangian(gsl::span xs, return output; } -inline double interpolate(gsl::span xs, - gsl::span ys, double x, - Interpolation i = Interpolation::lin_lin) +inline double interpolate(span xs, span ys, + double x, Interpolation i = Interpolation::lin_lin) { int idx = lower_bound_index(xs.begin(), xs.end(), x); diff --git a/include/openmc/material.h b/include/openmc/material.h index 9235be356f6..31fab2ae273 100644 --- a/include/openmc/material.h +++ b/include/openmc/material.h @@ -4,9 +4,9 @@ #include #include +#include "openmc/span.h" #include "pugixml.hpp" #include "xtensor/xtensor.hpp" -#include #include #include "openmc/bremsstrahlung.h" @@ -118,21 +118,21 @@ class Material { // //! \param[in] density Density value //! \param[in] units Units of density - void set_density(double density, gsl::cstring_span units); + void set_density(double density, const std::string& units); //! Set temperature of the material void set_temperature(double temperature) { temperature_ = temperature; }; //! Get nuclides in material //! \return Indices into the global nuclides vector - gsl::span nuclides() const + span nuclides() const { return {nuclide_.data(), nuclide_.size()}; } //! Get densities of each nuclide in material //! \return Densities in [atom/b-cm] - gsl::span densities() const + span densities() const { return {atom_density_.data(), atom_density_.size()}; } @@ -210,7 +210,7 @@ class Material { //---------------------------------------------------------------------------- // Private data members - gsl::index index_; + int64_t index_; bool depletable_ {false}; //!< Is the material depletable? bool fissionable_ { diff --git a/include/openmc/mcpl_interface.h b/include/openmc/mcpl_interface.h index 1f0c94d6dec..e5c182280c8 100644 --- a/include/openmc/mcpl_interface.h +++ b/include/openmc/mcpl_interface.h @@ -2,10 +2,9 @@ #define OPENMC_MCPL_INTERFACE_H #include "openmc/particle_data.h" +#include "openmc/span.h" #include "openmc/vector.h" -#include - #include namespace openmc { @@ -30,13 +29,14 @@ vector mcpl_source_sites(std::string path); // //! \param[in] filename Path to MCPL file //! \param[in] source_bank Vector of SourceSites to write to file for this -//! MPI rank +//! MPI rank. Note that this can't be const due to +//! it being used as work space by MPI. //! \param[in] bank_indx Pointer to vector of site index ranges over all //! MPI ranks. This can be computed by calling //! calculate_parallel_index_vector on //! source_bank.size(). -void write_mcpl_source_point(const char* filename, - gsl::span source_bank, vector const& bank_index); +void write_mcpl_source_point(const char* filename, span source_bank, + const vector& bank_index); } // namespace openmc #endif // OPENMC_MCPL_INTERFACE_H diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index bd86d54fb72..3aef49c7e68 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -9,13 +9,13 @@ #include "hdf5.h" #include "pugixml.hpp" #include "xtensor/xtensor.hpp" -#include #include "openmc/bounding_box.h" #include "openmc/error.h" #include "openmc/memory.h" // for unique_ptr #include "openmc/particle.h" #include "openmc/position.h" +#include "openmc/span.h" #include "openmc/vector.h" #include "openmc/xml_interface.h" @@ -179,8 +179,8 @@ class Mesh { //! \param[out] Array of (material index, volume) for desired element //! \param[inout] seed Pseudorandom number seed //! \return Number of materials within element - int material_volumes(int n_sample, int bin, gsl::span volumes, - uint64_t* seed) const; + int material_volumes( + int n_sample, int bin, span volumes, uint64_t* seed) const; //! Determine volume of materials within a single mesh elemenet // diff --git a/include/openmc/nuclide.h b/include/openmc/nuclide.h index 1cc9d297287..329c776d032 100644 --- a/include/openmc/nuclide.h +++ b/include/openmc/nuclide.h @@ -7,7 +7,6 @@ #include #include // for pair -#include #include #include "openmc/array.h" @@ -17,6 +16,7 @@ #include "openmc/particle.h" #include "openmc/reaction.h" #include "openmc/reaction_product.h" +#include "openmc/span.h" #include "openmc/urr.h" #include "openmc/vector.h" #include "openmc/wmp.h" @@ -81,8 +81,8 @@ class Nuclide { //! \param[in] energy Energy group boundaries in [eV] //! \param[in] flux Flux in each energy group (not normalized per eV) //! \return Reaction rate - double collapse_rate(int MT, double temperature, - gsl::span energy, gsl::span flux) const; + double collapse_rate(int MT, double temperature, span energy, + span flux) const; //============================================================================ // Data members @@ -91,7 +91,7 @@ class Nuclide { int A_; //!< Mass number int metastable_; //!< Metastable state double awr_; //!< Atomic weight ratio - gsl::index index_; //!< Index in the nuclides array + int64_t index_; //!< Index in the nuclides array // Temperature dependent cross section data vector kTs_; //!< temperatures in eV (k*T) @@ -138,7 +138,7 @@ class Nuclide { // //! \param[in] T Temperature in [K] //! \return Temperature index and interpolation factor - std::pair find_temperature(double T) const; + std::pair find_temperature(double T) const; static int XS_TOTAL; static int XS_ABSORPTION; diff --git a/include/openmc/photon.h b/include/openmc/photon.h index 9901c8c6db5..1fee6c9f5b6 100644 --- a/include/openmc/photon.h +++ b/include/openmc/photon.h @@ -7,7 +7,6 @@ #include "openmc/vector.h" #include "xtensor/xtensor.hpp" -#include #include #include @@ -61,7 +60,7 @@ class PhotonInteraction { // Data members std::string name_; //!< Name of element, e.g. "Zr" int Z_; //!< Atomic number - gsl::index index_; //!< Index in global elements vector + int64_t index_; //!< Index in global elements vector // Microscopic cross sections xt::xtensor energy_; diff --git a/include/openmc/reaction.h b/include/openmc/reaction.h index 93987d9d7ee..d5f04d136d0 100644 --- a/include/openmc/reaction.h +++ b/include/openmc/reaction.h @@ -7,10 +7,10 @@ #include #include "hdf5.h" -#include #include "openmc/particle_data.h" #include "openmc/reaction_product.h" +#include "openmc/span.h" #include "openmc/vector.h" namespace openmc { @@ -33,7 +33,7 @@ class Reaction { //! \param[in] i_temp Temperature index //! \param[in] i_grid Energy grid index //! \param[in] interp_factor Interpolation factor between grid points - double xs(gsl::index i_temp, gsl::index i_grid, double interp_factor) const; + double xs(int64_t i_temp, int64_t i_grid, double interp_factor) const; //! Calculate cross section // @@ -47,8 +47,8 @@ class Reaction { //! \param[in] flux Flux in each energy group (not normalized per eV) //! \param[in] grid Nuclide energy grid //! \return Reaction rate - double collapse_rate(gsl::index i_temp, gsl::span energy, - gsl::span flux, const vector& grid) const; + double collapse_rate(int64_t i_temp, span energy, + span flux, const vector& grid) const; //! Cross section at a single temperature struct TemperatureXS { diff --git a/include/openmc/span.h b/include/openmc/span.h new file mode 100644 index 00000000000..723bccd76e2 --- /dev/null +++ b/include/openmc/span.h @@ -0,0 +1,237 @@ +#ifndef OPENMC_SPAN_H +#define OPENMC_SPAN_H +#include // for std::size_t, std::ptrdiff_t +#include // for std::begin, std::end +#include // for std::out_of_range +#include + +#include "openmc/vector.h" + +namespace openmc { + +template +class span { +public: + using value_type = T; + using pointer = T*; + using const_pointer = const T*; + using reference = T&; + using const_reference = const T&; + using iterator = T*; + using const_iterator = const T*; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + + /** + * @brief Default constructor for an empty span. + */ + span() noexcept : data_(nullptr), size_(0) {} + + /** + * @brief Constructs a span from a pointer and size. + * @param ptr Pointer to the first element. + * @param count Number of elements in the span. + */ + span(pointer ptr, size_type count) : data_(ptr), size_(count) {} + + /** + * @brief Constructs a span from two pointers marking the span range. + * @param first Pointer to the first element. + * @param last Pointer past the last element. + * @throws std::out_of_range if last < first. + */ + span(pointer first, pointer last) : data_(first), size_(last - first) + { + if (last < first) { + throw std::out_of_range("span: last pointer is before first pointer"); + } + } + + /** + * @brief Constructs a span from a non-const std::vector. + * @param vec Reference to the vector to create a span from. + */ + span(std::vector& vec) : data_(vec.data()), size_(vec.size()) {} + + /** + * @brief Constructs a span from a const std::vector. + * + * This is handling the semantics that a span is used + * for read-only access into a vector. + * @param vec Reference to the const vector to create a span from. + */ + template::value>> + span(const std::vector>& vec) + : data_(vec.data()), size_(vec.size()) + {} + + /** + * @brief Constructs a read-only span from a non-const span. + */ + template::value>> + span(const span>& other) noexcept + : data_(other.data()), size_(other.size()) + {} + + /** + * @brief Access an element without bounds checking. + * @param index Index of the element to access. + * @return Reference to the accessed element. + */ + reference operator[](size_type index) { return data_[index]; } + + /** + * @brief Access an element without bounds checking (const version). + * @param index Index of the element to access. + * @return Const reference to the accessed element. + */ + const_reference operator[](size_type index) const { return data_[index]; } + + /** + * @brief Access an element with bounds checking. + * @param index Index of the element to access. + * @return Reference to the accessed element. + * @throws std::out_of_range if index is out of range. + */ + reference at(size_type index) + { + if (index >= size_) { + throw std::out_of_range("span: index out of range"); + } + return data_[index]; + } + + /** + * @brief Access an element with bounds checking (const version). + * @param index Index of the element to access. + * @return Const reference to the accessed element. + * @throws std::out_of_range if index is out of range. + */ + const_reference at(size_type index) const + { + if (index >= size_) { + throw std::out_of_range("span: index out of range"); + } + return data_[index]; + } + + /** + * @brief Get a pointer to the underlying data. + * @return Pointer to the data, or nullptr if the span is empty. + */ + pointer data() noexcept { return data_; } + + /** + * @brief Get a const pointer to the underlying data. + * @return Const pointer to the data, or nullptr if the span is empty. + */ + const_pointer data() const noexcept { return data_; } + + /** + * @brief Get the number of elements in the span. + * @return The size of the span. + */ + size_type size() const noexcept { return size_; } + + /** + * @brief Check if the span is empty. + * @return True if the span is empty, false otherwise. + */ + bool empty() const noexcept { return size_ == 0; } + + /** + * @brief Get an iterator to the beginning of the span. + * @return Iterator pointing to the first element. + */ + iterator begin() noexcept { return data_; } + + /** + * @brief Get a const iterator to the beginning of the span. + * @return Const iterator pointing to the first element. + */ + const_iterator begin() const noexcept { return data_; } + + /** + * @brief Get a const iterator to the beginning of the span. + * @return Const iterator pointing to the first element. + */ + const_iterator cbegin() const noexcept { return data_; } + + /** + * @brief Get an iterator to the end of the span. + * @return Iterator pointing past the last element. + */ + iterator end() noexcept { return data_ + size_; } + + /** + * @brief Get a const iterator to the end of the span. + * @return Const iterator pointing past the last element. + */ + const_iterator end() const noexcept { return data_ + size_; } + + /** + * @brief Get a const iterator to the end of the span. + * @return Const iterator pointing past the last element. + */ + const_iterator cend() const noexcept { return data_ + size_; } + + /** + * @brief Access the first element. + * @return Reference to the first element. + * @throws std::out_of_range if the span is empty. + */ + reference front() + { + if (empty()) { + throw std::out_of_range("span::front(): span is empty"); + } + return data_[0]; + } + + /** + * @brief Access the first element (const version). + * @return Const reference to the first element. + * @throws std::out_of_range if the span is empty. + */ + const_reference front() const + { + if (empty()) { + throw std::out_of_range("span::front(): span is empty"); + } + return data_[0]; + } + + /** + * @brief Access the last element. + * @return Reference to the last element. + * @throws std::out_of_range if the span is empty. + */ + reference back() + { + if (empty()) { + throw std::out_of_range("span::back(): span is empty"); + } + return data_[size_ - 1]; + } + + /** + * @brief Access the last element (const version). + * @return Const reference to the last element. + * @throws std::out_of_range if the span is empty. + */ + const_reference back() const + { + if (empty()) { + throw std::out_of_range("span::back(): span is empty"); + } + return data_[size_ - 1]; + } + +private: + pointer data_; + size_type size_; +}; + +} // namespace openmc +#endif // OPENMC_SPAN_H diff --git a/include/openmc/state_point.h b/include/openmc/state_point.h index f0d41e1697f..fb1aaf7b985 100644 --- a/include/openmc/state_point.h +++ b/include/openmc/state_point.h @@ -4,13 +4,12 @@ #include #include -#include - #include "hdf5.h" #include "openmc/capi.h" #include "openmc/particle.h" #include "openmc/shared_array.h" +#include "openmc/span.h" #include "openmc/vector.h" namespace openmc { @@ -34,15 +33,18 @@ void load_state_point(); // values on each rank, used to create global indexing. This vector // can be created by calling calculate_parallel_index_vector on // source_bank.size() if such a vector is not already available. -void write_h5_source_point(const char* filename, - gsl::span source_bank, const vector& bank_index); +// +// The source_bank variable is used as work space if MPI is used, +// so it cannot be given as a const span. +void write_h5_source_point(const char* filename, span source_bank, + const vector& bank_index); -void write_source_point(std::string, gsl::span source_bank, +void write_source_point(std::string, span source_bank, const vector& bank_index, bool use_mcpl); // This appends a source bank specification to an HDF5 file // that's already open. It is used internally by write_source_point. -void write_source_bank(hid_t group_id, gsl::span source_bank, +void write_source_bank(hid_t group_id, span source_bank, const vector& bank_index); void read_source_bank( diff --git a/include/openmc/tallies/filter.h b/include/openmc/tallies/filter.h index 210ab284ba9..31dd609ed10 100644 --- a/include/openmc/tallies/filter.h +++ b/include/openmc/tallies/filter.h @@ -6,7 +6,6 @@ #include #include "pugixml.hpp" -#include #include "openmc/constants.h" #include "openmc/hdf5_interface.h" @@ -130,7 +129,7 @@ class Filter { //! \return Number of bins int n_bins() const { return n_bins_; } - gsl::index index() const { return index_; } + int64_t index() const { return index_; } //---------------------------------------------------------------------------- // Data members @@ -140,7 +139,7 @@ class Filter { private: int32_t id_ {C_NONE}; - gsl::index index_; + int64_t index_; }; //============================================================================== diff --git a/include/openmc/tallies/filter_azimuthal.h b/include/openmc/tallies/filter_azimuthal.h index 37e1ef07356..4853c554590 100644 --- a/include/openmc/tallies/filter_azimuthal.h +++ b/include/openmc/tallies/filter_azimuthal.h @@ -4,8 +4,7 @@ #include "openmc/vector.h" #include -#include - +#include "openmc/span.h" #include "openmc/tallies/filter.h" namespace openmc { @@ -39,7 +38,7 @@ class AzimuthalFilter : public Filter { //---------------------------------------------------------------------------- // Accessors - void set_bins(gsl::span bins); + void set_bins(span bins); private: //---------------------------------------------------------------------------- diff --git a/include/openmc/tallies/filter_cell.h b/include/openmc/tallies/filter_cell.h index b7ea01ce6cb..ac6539466f8 100644 --- a/include/openmc/tallies/filter_cell.h +++ b/include/openmc/tallies/filter_cell.h @@ -4,8 +4,7 @@ #include #include -#include - +#include "openmc/span.h" #include "openmc/tallies/filter.h" #include "openmc/vector.h" @@ -42,7 +41,7 @@ class CellFilter : public Filter { const vector& cells() const { return cells_; } - void set_cells(gsl::span cells); + void set_cells(span cells); protected: //---------------------------------------------------------------------------- diff --git a/include/openmc/tallies/filter_cell_instance.h b/include/openmc/tallies/filter_cell_instance.h index f500f48896f..69499765b22 100644 --- a/include/openmc/tallies/filter_cell_instance.h +++ b/include/openmc/tallies/filter_cell_instance.h @@ -4,9 +4,8 @@ #include #include -#include - #include "openmc/cell.h" +#include "openmc/span.h" #include "openmc/tallies/filter.h" #include "openmc/vector.h" @@ -22,7 +21,7 @@ class CellInstanceFilter : public Filter { // Constructors, destructors CellInstanceFilter() = default; - CellInstanceFilter(gsl::span instances); + CellInstanceFilter(span instances); ~CellInstanceFilter() = default; //---------------------------------------------------------------------------- @@ -47,7 +46,7 @@ class CellInstanceFilter : public Filter { const std::unordered_set& cells() const { return cells_; } - void set_cell_instances(gsl::span instances); + void set_cell_instances(span instances); private: //---------------------------------------------------------------------------- @@ -60,7 +59,7 @@ class CellInstanceFilter : public Filter { std::unordered_set cells_; //! A map from cell/instance indices to filter bin indices. - std::unordered_map map_; + std::unordered_map map_; //! Indicates if filter uses only material-filled cells bool material_cells_only_; diff --git a/include/openmc/tallies/filter_collision.h b/include/openmc/tallies/filter_collision.h index d2dab70ca06..7d42a5ddd35 100644 --- a/include/openmc/tallies/filter_collision.h +++ b/include/openmc/tallies/filter_collision.h @@ -1,9 +1,9 @@ #ifndef OPENMC_TALLIES_FILTER_COLLISIONS_H #define OPENMC_TALLIES_FILTER_COLLISIONS_H -#include #include +#include "openmc/span.h" #include "openmc/tallies/filter.h" #include "openmc/vector.h" @@ -39,7 +39,7 @@ class CollisionFilter : public Filter { // Accessors const vector& bins() const { return bins_; } - void set_bins(gsl::span bins); + void set_bins(span bins); protected: //---------------------------------------------------------------------------- diff --git a/include/openmc/tallies/filter_delayedgroup.h b/include/openmc/tallies/filter_delayedgroup.h index 71919b2ece5..7d11447ab13 100644 --- a/include/openmc/tallies/filter_delayedgroup.h +++ b/include/openmc/tallies/filter_delayedgroup.h @@ -1,8 +1,7 @@ #ifndef OPENMC_TALLIES_FILTER_DELAYEDGROUP_H #define OPENMC_TALLIES_FILTER_DELAYEDGROUP_H -#include - +#include "openmc/span.h" #include "openmc/tallies/filter.h" #include "openmc/vector.h" @@ -42,7 +41,7 @@ class DelayedGroupFilter : public Filter { const vector& groups() const { return groups_; } - void set_groups(gsl::span groups); + void set_groups(span groups); private: //---------------------------------------------------------------------------- diff --git a/include/openmc/tallies/filter_energy.h b/include/openmc/tallies/filter_energy.h index e35e01a6dae..cf8a8aa0f58 100644 --- a/include/openmc/tallies/filter_energy.h +++ b/include/openmc/tallies/filter_energy.h @@ -1,8 +1,7 @@ #ifndef OPENMC_TALLIES_FILTER_ENERGY_H #define OPENMC_TALLIES_FILTER_ENERGY_H -#include - +#include "openmc/span.h" #include "openmc/tallies/filter.h" #include "openmc/vector.h" @@ -38,7 +37,7 @@ class EnergyFilter : public Filter { // Accessors const vector& bins() const { return bins_; } - void set_bins(gsl::span bins); + void set_bins(span bins); bool matches_transport_groups() const { return matches_transport_groups_; } diff --git a/include/openmc/tallies/filter_energyfunc.h b/include/openmc/tallies/filter_energyfunc.h index d77ef0fa839..e03c23dda7e 100644 --- a/include/openmc/tallies/filter_energyfunc.h +++ b/include/openmc/tallies/filter_energyfunc.h @@ -2,6 +2,7 @@ #define OPENMC_TALLIES_FILTER_ENERGYFUNC_H #include "openmc/constants.h" +#include "openmc/span.h" #include "openmc/tallies/filter.h" #include "openmc/vector.h" @@ -42,7 +43,7 @@ class EnergyFunctionFilter : public Filter { const vector& energy() const { return energy_; } const vector& y() const { return y_; } Interpolation interpolation() const { return interpolation_; } - void set_data(gsl::span energy, gsl::span y); + void set_data(span energy, span y); void set_interpolation(const std::string& interpolation); private: diff --git a/include/openmc/tallies/filter_material.h b/include/openmc/tallies/filter_material.h index aa5df5b3ab2..ccfe5403d5e 100644 --- a/include/openmc/tallies/filter_material.h +++ b/include/openmc/tallies/filter_material.h @@ -4,8 +4,7 @@ #include #include -#include - +#include "openmc/span.h" #include "openmc/tallies/filter.h" #include "openmc/vector.h" @@ -44,7 +43,7 @@ class MaterialFilter : public Filter { const vector& materials() const { return materials_; } - void set_materials(gsl::span materials); + void set_materials(span materials); protected: //---------------------------------------------------------------------------- diff --git a/include/openmc/tallies/filter_mu.h b/include/openmc/tallies/filter_mu.h index b0ee40eac9f..d6e7f1798e2 100644 --- a/include/openmc/tallies/filter_mu.h +++ b/include/openmc/tallies/filter_mu.h @@ -1,8 +1,7 @@ #ifndef OPENMC_TALLIES_FILTER_MU_H #define OPENMC_TALLIES_FILTER_MU_H -#include - +#include "openmc/span.h" #include "openmc/tallies/filter.h" #include "openmc/vector.h" @@ -38,7 +37,7 @@ class MuFilter : public Filter { //---------------------------------------------------------------------------- // Accessors - void set_bins(gsl::span bins); + void set_bins(span bins); protected: //---------------------------------------------------------------------------- diff --git a/include/openmc/tallies/filter_musurface.h b/include/openmc/tallies/filter_musurface.h index 2ca19e3a259..fa6816836d9 100644 --- a/include/openmc/tallies/filter_musurface.h +++ b/include/openmc/tallies/filter_musurface.h @@ -1,8 +1,6 @@ #ifndef OPENMC_TALLIES_FILTER_MU_SURFACE_H #define OPENMC_TALLIES_FILTER_MU_SURFACE_H -#include - #include "openmc/tallies/filter_mu.h" #include "openmc/vector.h" diff --git a/include/openmc/tallies/filter_particle.h b/include/openmc/tallies/filter_particle.h index a181d5cee64..863a6d282fe 100644 --- a/include/openmc/tallies/filter_particle.h +++ b/include/openmc/tallies/filter_particle.h @@ -2,6 +2,7 @@ #define OPENMC_TALLIES_FILTER_PARTICLE_H #include "openmc/particle.h" +#include "openmc/span.h" #include "openmc/tallies/filter.h" #include "openmc/vector.h" @@ -38,7 +39,7 @@ class ParticleFilter : public Filter { const vector& particles() const { return particles_; } - void set_particles(gsl::span particles); + void set_particles(span particles); private: //---------------------------------------------------------------------------- diff --git a/include/openmc/tallies/filter_polar.h b/include/openmc/tallies/filter_polar.h index 78bb25aa45e..c7c73c89f54 100644 --- a/include/openmc/tallies/filter_polar.h +++ b/include/openmc/tallies/filter_polar.h @@ -3,8 +3,7 @@ #include -#include - +#include "openmc/span.h" #include "openmc/tallies/filter.h" #include "openmc/vector.h" @@ -39,7 +38,7 @@ class PolarFilter : public Filter { //---------------------------------------------------------------------------- // Accessors - void set_bins(gsl::span bins); + void set_bins(span bins); private: //---------------------------------------------------------------------------- diff --git a/include/openmc/tallies/filter_sph_harm.h b/include/openmc/tallies/filter_sph_harm.h index 5d4a4bd999c..a6904c30103 100644 --- a/include/openmc/tallies/filter_sph_harm.h +++ b/include/openmc/tallies/filter_sph_harm.h @@ -3,8 +3,6 @@ #include -#include - #include "openmc/tallies/filter.h" namespace openmc { @@ -46,7 +44,7 @@ class SphericalHarmonicsFilter : public Filter { SphericalHarmonicsCosine cosine() const { return cosine_; } - void set_cosine(gsl::cstring_span cosine); + void set_cosine(const std::string& cosine); private: //---------------------------------------------------------------------------- diff --git a/include/openmc/tallies/filter_surface.h b/include/openmc/tallies/filter_surface.h index 3537f1cc748..e78243f5ff2 100644 --- a/include/openmc/tallies/filter_surface.h +++ b/include/openmc/tallies/filter_surface.h @@ -4,8 +4,7 @@ #include #include -#include - +#include "openmc/span.h" #include "openmc/tallies/filter.h" #include "openmc/vector.h" @@ -40,7 +39,7 @@ class SurfaceFilter : public Filter { //---------------------------------------------------------------------------- // Accessors - void set_surfaces(gsl::span surfaces); + void set_surfaces(span surfaces); private: //---------------------------------------------------------------------------- diff --git a/include/openmc/tallies/filter_time.h b/include/openmc/tallies/filter_time.h index 105ef9880a7..3ce557abda6 100644 --- a/include/openmc/tallies/filter_time.h +++ b/include/openmc/tallies/filter_time.h @@ -1,8 +1,7 @@ #ifndef OPENMC_TALLIES_FILTER_TIME_H #define OPENMC_TALLIES_FILTER_TIME_H -#include - +#include "openmc/span.h" #include "openmc/tallies/filter.h" #include "openmc/vector.h" @@ -38,7 +37,7 @@ class TimeFilter : public Filter { // Accessors const vector& bins() const { return bins_; } - void set_bins(gsl::span bins); + void set_bins(span bins); protected: //---------------------------------------------------------------------------- diff --git a/include/openmc/tallies/filter_universe.h b/include/openmc/tallies/filter_universe.h index d4894353bc9..461434ec441 100644 --- a/include/openmc/tallies/filter_universe.h +++ b/include/openmc/tallies/filter_universe.h @@ -4,8 +4,7 @@ #include #include -#include - +#include "openmc/span.h" #include "openmc/tallies/filter.h" #include "openmc/vector.h" @@ -40,7 +39,7 @@ class UniverseFilter : public Filter { //---------------------------------------------------------------------------- // Accessors - void set_universes(gsl::span universes); + void set_universes(span universes); private: //---------------------------------------------------------------------------- diff --git a/include/openmc/tallies/tally.h b/include/openmc/tallies/tally.h index 2f0cddcf0ae..48f678ced0f 100644 --- a/include/openmc/tallies/tally.h +++ b/include/openmc/tallies/tally.h @@ -3,6 +3,7 @@ #include "openmc/constants.h" #include "openmc/memory.h" // for unique_ptr +#include "openmc/span.h" #include "openmc/tallies/filter.h" #include "openmc/tallies/trigger.h" #include "openmc/vector.h" @@ -10,7 +11,6 @@ #include "pugixml.hpp" #include "xtensor/xfixed.hpp" #include "xtensor/xtensor.hpp" -#include #include #include @@ -93,7 +93,7 @@ class Tally { //! \brief Check if this tally has a specified type of filter bool has_filter(FilterType filter_type) const; - void set_filters(gsl::span filters); + void set_filters(span filters); //! Given already-set filters, set the stride lengths void set_strides(); @@ -192,7 +192,7 @@ class Tally { //! Whether to multiply by atom density for reaction rates bool multiply_density_ {true}; - gsl::index index_; + int64_t index_; }; //============================================================================== diff --git a/include/openmc/volume_calc.h b/include/openmc/volume_calc.h index 376b8c707dd..fa8d3d65ece 100644 --- a/include/openmc/volume_calc.h +++ b/include/openmc/volume_calc.h @@ -14,7 +14,6 @@ #include "pugixml.hpp" #include "xtensor/xtensor.hpp" -#include #ifdef _OPENMP #include #endif diff --git a/include/openmc/weight_windows.h b/include/openmc/weight_windows.h index 2d4d556948b..6f2ef07079f 100644 --- a/include/openmc/weight_windows.h +++ b/include/openmc/weight_windows.h @@ -4,7 +4,6 @@ #include #include -#include #include #include @@ -12,6 +11,7 @@ #include "openmc/memory.h" #include "openmc/mesh.h" #include "openmc/particle.h" +#include "openmc/span.h" #include "openmc/tallies/tally.h" #include "openmc/vector.h" @@ -104,7 +104,7 @@ class WeightWindows { //! Set the weight window ID void set_id(int32_t id = -1); - void set_energy_bounds(gsl::span bounds); + void set_energy_bounds(span bounds); void set_mesh(const std::unique_ptr& mesh); @@ -148,9 +148,9 @@ class WeightWindows { void set_bounds(const xt::xtensor& lower_bounds, double ratio); void set_bounds( - gsl::span lower_bounds, gsl::span upper_bounds); + span lower_bounds, span upper_bounds); - void set_bounds(gsl::span lower_bounds, double ratio); + void set_bounds(span lower_bounds, double ratio); void set_particle_type(ParticleType p_type); @@ -192,8 +192,8 @@ class WeightWindows { private: //---------------------------------------------------------------------------- // Data members - int32_t id_; //!< Unique ID - gsl::index index_; //!< Index into weight windows vector + int32_t id_; //!< Unique ID + int64_t index_; //!< Index into weight windows vector ParticleType particle_type_ { ParticleType::neutron}; //!< Particle type to apply weight windows to vector energy_bounds_; //!< Energy boundaries [eV] diff --git a/src/cell.cpp b/src/cell.cpp index fa8be4496f1..4b26992299e 100644 --- a/src/cell.cpp +++ b/src/cell.cpp @@ -2,6 +2,7 @@ #include "openmc/cell.h" #include +#include #include #include #include @@ -10,7 +11,6 @@ #include #include -#include #include "openmc/capi.h" #include "openmc/constants.h" @@ -137,7 +137,7 @@ void Cell::set_temperature(double T, int32_t instance, bool set_contained) auto contained_cells = this->get_contained_cells(instance); for (const auto& entry : contained_cells) { auto& cell = model::cells[entry.first]; - Expects(cell->type_ == Fill::MATERIAL); + assert(cell->type_ == Fill::MATERIAL); auto& instances = entry.second; for (auto instance : instances) { cell->set_temperature(T, instance); @@ -179,7 +179,7 @@ void Cell::import_properties_hdf5(hid_t group) // Modify temperatures for the cell sqrtkT_.clear(); sqrtkT_.resize(temps.size()); - for (gsl::index i = 0; i < temps.size(); ++i) { + for (int64_t i = 0; i < temps.size(); ++i) { this->set_temperature(temps[i], i); } @@ -570,7 +570,7 @@ void Region::apply_demorgan( //! precedence than unions using parentheses. //============================================================================== -gsl::index Region::add_parentheses(gsl::index start) +int64_t Region::add_parentheses(int64_t start) { int32_t start_token = expression_[start]; // Add left parenthesis and set new position to be after parenthesis @@ -642,7 +642,7 @@ void Region::add_precedence() int32_t current_op = 0; std::size_t current_dist = 0; - for (gsl::index i = 0; i < expression_.size(); i++) { + for (int64_t i = 0; i < expression_.size(); i++) { int32_t token = expression_[i]; if (token == OP_UNION || token == OP_INTERSECTION) { @@ -938,7 +938,7 @@ BoundingBox Region::bounding_box_complex(vector postfix) const } } - Ensures(i_stack == 0); + assert(i_stack == 0); return stack.front(); } @@ -1210,8 +1210,8 @@ struct ParentCell { lattice_index < other.lattice_index); } - gsl::index cell_index; - gsl::index lattice_index; + int64_t cell_index; + int64_t lattice_index; }; //! Structure used to insert ParentCell into hashed STL data structures diff --git a/src/dagmc.cpp b/src/dagmc.cpp index ea5be04ba67..13436088652 100644 --- a/src/dagmc.cpp +++ b/src/dagmc.cpp @@ -1,5 +1,7 @@ #include "openmc/dagmc.h" +#include + #include "openmc/constants.h" #include "openmc/container_util.h" #include "openmc/error.h" @@ -800,7 +802,7 @@ Direction DAGSurface::normal(Position r) const Direction DAGSurface::reflect(Position r, Direction u, GeometryState* p) const { - Expects(p); + assert(p); double pnt[3] = {r.x, r.y, r.z}; double dir[3]; moab::ErrorCode rval = diff --git a/src/distribution.cpp b/src/distribution.cpp index 8c700460f44..ca00cbde4eb 100644 --- a/src/distribution.cpp +++ b/src/distribution.cpp @@ -8,8 +8,6 @@ #include // for runtime_error #include // for string, stod -#include - #include "openmc/error.h" #include "openmc/math_functions.h" #include "openmc/random_dist.h" @@ -30,12 +28,12 @@ DiscreteIndex::DiscreteIndex(pugi::xml_node node) assign({params.data() + n, n}); } -DiscreteIndex::DiscreteIndex(gsl::span p) +DiscreteIndex::DiscreteIndex(span p) { assign(p); } -void DiscreteIndex::assign(gsl::span p) +void DiscreteIndex::assign(span p) { prob_.assign(p.begin(), p.end()); @@ -417,7 +415,7 @@ double Mixture::sample(uint64_t* seed) const p, [](const DistPair& pair, double p) { return pair.first < p; }); // This should not happen. Catch it - Ensures(it != distribution_.cend()); + assert(it != distribution_.cend()); // Sample the chosen distribution return it->second->sample(seed); diff --git a/src/distribution_spatial.cpp b/src/distribution_spatial.cpp index 5c193a95d29..d2b0f413bd1 100644 --- a/src/distribution_spatial.cpp +++ b/src/distribution_spatial.cpp @@ -262,7 +262,7 @@ MeshSpatial::MeshSpatial(pugi::xml_node node) elem_idx_dist_.assign(strengths); } -MeshSpatial::MeshSpatial(int32_t mesh_idx, gsl::span strengths) +MeshSpatial::MeshSpatial(int32_t mesh_idx, span strengths) : mesh_idx_(mesh_idx) { check_element_types(); @@ -331,7 +331,7 @@ PointCloud::PointCloud(pugi::xml_node node) } PointCloud::PointCloud( - std::vector point_cloud, gsl::span strengths) + std::vector point_cloud, span strengths) { point_cloud_.assign(point_cloud.begin(), point_cloud.end()); point_idx_dist_.assign(strengths); diff --git a/src/geometry_aux.cpp b/src/geometry_aux.cpp index 050d4db968c..2f8a5574337 100644 --- a/src/geometry_aux.cpp +++ b/src/geometry_aux.cpp @@ -344,7 +344,7 @@ void prepare_distribcell(const std::vector* user_distribcells) // By default, add material cells to the list of distributed cells if (settings::material_cell_offsets) { - for (gsl::index i = 0; i < model::cells.size(); ++i) { + for (int64_t i = 0; i < model::cells.size(); ++i) { if (model::cells[i]->type_ == Fill::MATERIAL) distribcells.insert(i); } diff --git a/src/material.cpp b/src/material.cpp index aad7008e703..3ba9ab96c00 100644 --- a/src/material.cpp +++ b/src/material.cpp @@ -1,6 +1,7 @@ #include "openmc/material.h" #include // for min, max, sort, fill +#include #include #include #include @@ -933,7 +934,7 @@ void Material::calculate_photon_xs(Particle& p) const void Material::set_id(int32_t id) { - Expects(id >= 0 || id == C_NONE); + assert(id >= 0 || id == C_NONE); // Clear entry in material map if an ID was already assigned before if (id_ != C_NONE) { @@ -961,9 +962,9 @@ void Material::set_id(int32_t id) model::material_map[id] = index_; } -void Material::set_density(double density, gsl::cstring_span units) +void Material::set_density(double density, const std::string& units) { - Expects(density >= 0.0); + assert(density >= 0.0); if (nuclide_.empty()) { throw std::runtime_error {"No nuclides exist in material yet."}; @@ -1006,8 +1007,8 @@ void Material::set_densities( const vector& name, const vector& density) { auto n = name.size(); - Expects(n > 0); - Expects(n == density.size()); + assert(n > 0); + assert(n == density.size()); if (n != nuclide_.size()) { nuclide_.resize(n); @@ -1017,7 +1018,7 @@ void Material::set_densities( } double sum_density = 0.0; - for (gsl::index i = 0; i < n; ++i) { + for (int64_t i = 0; i < n; ++i) { const auto& nuc {name[i]}; if (data::nuclide_map.find(nuc) == data::nuclide_map.end()) { int err = openmc_load_nuclide(nuc.c_str(), nullptr, 0); @@ -1026,7 +1027,7 @@ void Material::set_densities( } nuclide_[i] = data::nuclide_map.at(nuc); - Expects(density[i] > 0.0); + assert(density[i] > 0.0); atom_density_(i) = density[i]; sum_density += density[i]; diff --git a/src/mcpl_interface.cpp b/src/mcpl_interface.cpp index 5c3df026ce5..a8de5fdb0e0 100644 --- a/src/mcpl_interface.cpp +++ b/src/mcpl_interface.cpp @@ -112,7 +112,7 @@ vector mcpl_source_sites(std::string path) #ifdef OPENMC_MCPL void write_mcpl_source_bank(mcpl_outfile_t file_id, - gsl::span source_bank, const vector& bank_index) + span source_bank, const vector& bank_index) { int64_t dims_size = settings::n_particles; int64_t count_size = simulation::work_per_rank; @@ -188,8 +188,8 @@ void write_mcpl_source_bank(mcpl_outfile_t file_id, //============================================================================== -void write_mcpl_source_point(const char* filename, - gsl::span source_bank, const vector& bank_index) +void write_mcpl_source_point(const char* filename, span source_bank, + const vector& bank_index) { std::string filename_(filename); const auto extension = get_file_extension(filename_); diff --git a/src/mesh.cpp b/src/mesh.cpp index 97bf710caa6..3e1f25c4b27 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -1,9 +1,9 @@ #include "openmc/mesh.h" -#include // for copy, equal, min, min_element +#include // for copy, equal, min, min_element +#include #define _USE_MATH_DEFINES // to make M_PI declared in Intel and MSVC compilers #include // for ceil #include // for size_t -#include #include #ifdef OPENMC_MPI @@ -116,7 +116,7 @@ Mesh::Mesh(pugi::xml_node node) void Mesh::set_id(int32_t id) { - Expects(id >= 0 || id == C_NONE); + assert(id >= 0 || id == C_NONE); // Clear entry in mesh map in case one was already assigned if (id_ != C_NONE) { @@ -154,7 +154,7 @@ vector Mesh::volumes() const } int Mesh::material_volumes( - int n_sample, int bin, gsl::span result, uint64_t* seed) const + int n_sample, int bin, span result, uint64_t* seed) const { vector materials; vector hits; @@ -3184,13 +3184,13 @@ void LibMesh::set_score_data(const std::string& var_name, // set value vector value_dof_indices; dof_map.dof_indices(*it, value_dof_indices, value_num); - Ensures(value_dof_indices.size() == 1); + assert(value_dof_indices.size() == 1); eqn_sys.solution->set(value_dof_indices[0], values.at(bin)); // set std dev vector std_dev_dof_indices; dof_map.dof_indices(*it, std_dev_dof_indices, std_dev_num); - Ensures(std_dev_dof_indices.size() == 1); + assert(std_dev_dof_indices.size() == 1); eqn_sys.solution->set(std_dev_dof_indices[0], std_dev.at(bin)); } } diff --git a/src/nuclide.cpp b/src/nuclide.cpp index f720f848bc2..d78b7a0101d 100644 --- a/src/nuclide.cpp +++ b/src/nuclide.cpp @@ -21,7 +21,8 @@ #include "xtensor/xview.hpp" #include // for sort, min_element -#include // for to_string, stoi +#include +#include // for to_string, stoi namespace openmc { @@ -999,19 +1000,19 @@ void Nuclide::calculate_urr_xs(int i_temp, Particle& p) const } } -std::pair Nuclide::find_temperature(double T) const +std::pair Nuclide::find_temperature(double T) const { - Expects(T >= 0.0); + assert(T >= 0.0); // Determine temperature index - gsl::index i_temp = 0; + int64_t i_temp = 0; double f = 0.0; double kT = K_BOLTZMANN * T; - gsl::index n = kTs_.size(); + int64_t n = kTs_.size(); switch (settings::temperature_method) { case TemperatureMethod::NEAREST: { double max_diff = INFTY; - for (gsl::index t = 0; t < n; ++t) { + for (int64_t t = 0; t < n; ++t) { double diff = std::abs(kTs_[t] - kT); if (diff < max_diff) { i_temp = t; @@ -1038,17 +1039,17 @@ std::pair Nuclide::find_temperature(double T) const f = (kT - kTs_[i_temp]) / (kTs_[i_temp + 1] - kTs_[i_temp]); } - Ensures(i_temp >= 0 && i_temp < n); + assert(i_temp >= 0 && i_temp < n); return {i_temp, f}; } double Nuclide::collapse_rate(int MT, double temperature, - gsl::span energy, gsl::span flux) const + span energy, span flux) const { - Expects(MT > 0); - Expects(energy.size() > 0); - Expects(energy.size() == flux.size() + 1); + assert(MT > 0); + assert(energy.size() > 0); + assert(energy.size() == flux.size() + 1); int i_rx = reaction_index_[MT]; if (i_rx < 0) @@ -1056,7 +1057,7 @@ double Nuclide::collapse_rate(int MT, double temperature, const auto& rx = reactions_[i_rx]; // Determine temperature index - gsl::index i_temp; + int64_t i_temp; double f; std::tie(i_temp, f) = this->find_temperature(temperature); diff --git a/src/reaction.cpp b/src/reaction.cpp index 3bdc8374679..60a14b2b3cc 100644 --- a/src/reaction.cpp +++ b/src/reaction.cpp @@ -65,8 +65,7 @@ Reaction::Reaction(hid_t group, const vector& temperatures) } } -double Reaction::xs( - gsl::index i_temp, gsl::index i_grid, double interp_factor) const +double Reaction::xs(int64_t i_temp, int64_t i_grid, double interp_factor) const { // If energy is below threshold, return 0. Otherwise interpolate between // nearest grid points @@ -82,9 +81,8 @@ double Reaction::xs(const NuclideMicroXS& micro) const return this->xs(micro.index_temp, micro.index_grid, micro.interp_factor); } -double Reaction::collapse_rate(gsl::index i_temp, - gsl::span energy, gsl::span flux, - const vector& grid) const +double Reaction::collapse_rate(int64_t i_temp, span energy, + span flux, const vector& grid) const { // Find index corresponding to first energy const auto& xs = xs_[i_temp].value; diff --git a/src/secondary_thermal.cpp b/src/secondary_thermal.cpp index 0b8e1ab42db..8b9e8737c66 100644 --- a/src/secondary_thermal.cpp +++ b/src/secondary_thermal.cpp @@ -4,10 +4,9 @@ #include "openmc/random_lcg.h" #include "openmc/search.h" -#include - #include "xtensor/xview.hpp" +#include #include // for log, exp namespace openmc { @@ -40,7 +39,7 @@ void CoherentElasticAE::sample( const auto& energies {xs_.bragg_edges()}; - Expects(E_in >= energies.front()); + assert(E_in >= energies.front()); const int i = lower_bound_index(energies.begin(), energies.end(), E_in); diff --git a/src/simulation.cpp b/src/simulation.cpp index 030e447f135..a3c6495ae7d 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -439,7 +439,7 @@ void finalize_batch() int w = std::to_string(settings::n_max_batches).size(); std::string source_point_filename = fmt::format("{0}source.{1:0{2}}", settings::path_output, simulation::current_batch, w); - gsl::span bankspan(simulation::source_bank); + span bankspan(simulation::source_bank); write_source_point(source_point_filename, bankspan, simulation::work_index, settings::source_mcpl_write); } @@ -447,7 +447,7 @@ void finalize_batch() // Write a continously-overwritten source point if requested. if (settings::source_latest) { auto filename = settings::path_output + "source"; - gsl::span bankspan(simulation::source_bank); + span bankspan(simulation::source_bank); write_source_point(filename, bankspan, simulation::work_index, settings::source_mcpl_write); } @@ -469,7 +469,7 @@ void finalize_batch() // Get span of source bank and calculate parallel index vector auto surf_work_index = mpi::calculate_parallel_index_vector( simulation::surf_source_bank.size()); - gsl::span surfbankspan(simulation::surf_source_bank.begin(), + span surfbankspan(simulation::surf_source_bank.begin(), simulation::surf_source_bank.size()); // Write surface source file diff --git a/src/state_point.cpp b/src/state_point.cpp index fcc389df189..ed8c6ed41c0 100644 --- a/src/state_point.cpp +++ b/src/state_point.cpp @@ -567,7 +567,7 @@ hid_t h5banktype() return banktype; } -void write_source_point(std::string filename, gsl::span source_bank, +void write_source_point(std::string filename, span source_bank, const vector& bank_index, bool use_mcpl) { std::string ext = use_mcpl ? "mcpl" : "h5"; @@ -584,8 +584,8 @@ void write_source_point(std::string filename, gsl::span source_bank, } } -void write_h5_source_point(const char* filename, - gsl::span source_bank, const vector& bank_index) +void write_h5_source_point(const char* filename, span source_bank, + const vector& bank_index) { // When using parallel HDF5, the file is written to collectively by all // processes. With MPI-only, the file is opened and written by the master @@ -620,7 +620,7 @@ void write_h5_source_point(const char* filename, file_close(file_id); } -void write_source_bank(hid_t group_id, gsl::span source_bank, +void write_source_bank(hid_t group_id, span source_bank, const vector& bank_index) { hid_t banktype = h5banktype(); diff --git a/src/surface.cpp b/src/surface.cpp index 3658b3fa12f..0002275c393 100644 --- a/src/surface.cpp +++ b/src/surface.cpp @@ -7,7 +7,6 @@ #include #include -#include #include "openmc/array.h" #include "openmc/container_util.h" diff --git a/src/tallies/filter.cpp b/src/tallies/filter.cpp index 074212db44d..0ce1ee20b37 100644 --- a/src/tallies/filter.cpp +++ b/src/tallies/filter.cpp @@ -1,7 +1,8 @@ #include "openmc/tallies/filter.h" #include // for max -#include // for strcpy +#include +#include // for strcpy #include #include @@ -162,7 +163,7 @@ Filter* Filter::create(const std::string& type, int32_t id) void Filter::set_id(int32_t id) { - Expects(id >= 0 || id == C_NONE); + assert(id >= 0 || id == C_NONE); // Clear entry in filter map if an ID was already assigned before if (id_ != C_NONE) { diff --git a/src/tallies/filter_azimuthal.cpp b/src/tallies/filter_azimuthal.cpp index e77aa8bdc1e..6525f326d34 100644 --- a/src/tallies/filter_azimuthal.cpp +++ b/src/tallies/filter_azimuthal.cpp @@ -34,14 +34,14 @@ void AzimuthalFilter::from_xml(pugi::xml_node node) this->set_bins(bins); } -void AzimuthalFilter::set_bins(gsl::span bins) +void AzimuthalFilter::set_bins(span bins) { // Clear existing bins bins_.clear(); bins_.reserve(bins.size()); // Copy bins, ensuring they are valid - for (gsl::index i = 0; i < bins.size(); ++i) { + for (int64_t i = 0; i < bins.size(); ++i) { if (i > 0 && bins[i] <= bins[i - 1]) { throw std::runtime_error { "Azimuthal bins must be monotonically increasing."}; diff --git a/src/tallies/filter_cell.cpp b/src/tallies/filter_cell.cpp index 794d2ae08b0..b545801ea8e 100644 --- a/src/tallies/filter_cell.cpp +++ b/src/tallies/filter_cell.cpp @@ -1,5 +1,7 @@ #include "openmc/tallies/filter_cell.h" +#include + #include #include "openmc/capi.h" @@ -25,7 +27,7 @@ void CellFilter::from_xml(pugi::xml_node node) this->set_cells(cells); } -void CellFilter::set_cells(gsl::span cells) +void CellFilter::set_cells(span cells) { // Clear existing cells cells_.clear(); @@ -34,8 +36,8 @@ void CellFilter::set_cells(gsl::span cells) // Update cells and mapping for (auto& index : cells) { - Expects(index >= 0); - Expects(index < model::cells.size()); + assert(index >= 0); + assert(index < model::cells.size()); cells_.push_back(index); map_[index] = cells_.size() - 1; } diff --git a/src/tallies/filter_cell_instance.cpp b/src/tallies/filter_cell_instance.cpp index 3e78a5bbed9..0634175dd84 100644 --- a/src/tallies/filter_cell_instance.cpp +++ b/src/tallies/filter_cell_instance.cpp @@ -1,5 +1,6 @@ #include "openmc/tallies/filter_cell_instance.h" +#include #include #include @@ -12,7 +13,7 @@ namespace openmc { -CellInstanceFilter::CellInstanceFilter(gsl::span instances) +CellInstanceFilter::CellInstanceFilter(span instances) { this->set_cell_instances(instances); } @@ -21,26 +22,26 @@ void CellInstanceFilter::from_xml(pugi::xml_node node) { // Get cell IDs/instances auto cells = get_node_array(node, "bins"); - Expects(cells.size() % 2 == 0); + assert(cells.size() % 2 == 0); // Convert into vector of CellInstance vector instances; - for (gsl::index i = 0; i < cells.size() / 2; ++i) { + for (int64_t i = 0; i < cells.size() / 2; ++i) { int32_t cell_id = cells[2 * i]; - gsl::index instance = cells[2 * i + 1]; + int64_t instance = cells[2 * i + 1]; auto search = model::cell_map.find(cell_id); if (search == model::cell_map.end()) { throw std::runtime_error {fmt::format( "Could not find cell {} specified on tally filter.", cell_id)}; } - gsl::index index = search->second; + int64_t index = search->second; instances.push_back({index, instance}); } this->set_cell_instances(instances); } -void CellInstanceFilter::set_cell_instances(gsl::span instances) +void CellInstanceFilter::set_cell_instances(span instances) { // Clear existing cells cell_instances_.clear(); @@ -50,8 +51,8 @@ void CellInstanceFilter::set_cell_instances(gsl::span instances) // Update cells and mapping for (auto& x : instances) { - Expects(x.index_cell >= 0); - Expects(x.index_cell < model::cells.size()); + assert(x.index_cell >= 0); + assert(x.index_cell < model::cells.size()); cell_instances_.push_back(x); cells_.insert(x.index_cell); map_[x] = cell_instances_.size() - 1; @@ -72,8 +73,8 @@ void CellInstanceFilter::set_cell_instances(gsl::span instances) void CellInstanceFilter::get_all_bins( const Particle& p, TallyEstimator estimator, FilterMatch& match) const { - gsl::index index_cell = p.lowest_coord().cell; - gsl::index instance = p.cell_instance(); + int64_t index_cell = p.lowest_coord().cell; + int64_t instance = p.cell_instance(); if (cells_.count(index_cell) > 0) { auto search = map_.find({index_cell, instance}); @@ -88,13 +89,13 @@ void CellInstanceFilter::get_all_bins( return; for (int i = 0; i < p.n_coord() - 1; i++) { - gsl::index index_cell = p.coord(i).cell; + int64_t index_cell = p.coord(i).cell; // if this cell isn't used on the filter, move on if (cells_.count(index_cell) == 0) continue; // if this cell is used in the filter, check the instance as well - gsl::index instance = cell_instance_at_level(p, i); + int64_t instance = cell_instance_at_level(p, i); auto search = map_.find({index_cell, instance}); if (search != map_.end()) { match.bins_.push_back(search->second); @@ -108,7 +109,7 @@ void CellInstanceFilter::to_statepoint(hid_t filter_group) const Filter::to_statepoint(filter_group); size_t n = cell_instances_.size(); xt::xtensor data({n, 2}); - for (gsl::index i = 0; i < n; ++i) { + for (int64_t i = 0; i < n; ++i) { const auto& x = cell_instances_[i]; data(i, 0) = model::cells[x.index_cell]->id_; data(i, 1) = x.instance; diff --git a/src/tallies/filter_collision.cpp b/src/tallies/filter_collision.cpp index fbb186a2381..c614c3c83ce 100644 --- a/src/tallies/filter_collision.cpp +++ b/src/tallies/filter_collision.cpp @@ -19,7 +19,7 @@ void CollisionFilter::from_xml(pugi::xml_node node) this->set_bins(bins); } -void CollisionFilter::set_bins(gsl::span bins) +void CollisionFilter::set_bins(span bins) { // Clear existing bins bins_.clear(); @@ -27,7 +27,7 @@ void CollisionFilter::set_bins(gsl::span bins) map_.clear(); // Copy bins - for (gsl::index i = 0; i < bins.size(); ++i) { + for (int64_t i = 0; i < bins.size(); ++i) { bins_.push_back(bins[i]); map_[bins[i]] = i; } diff --git a/src/tallies/filter_delayedgroup.cpp b/src/tallies/filter_delayedgroup.cpp index c6ec217666a..01e39e554a9 100644 --- a/src/tallies/filter_delayedgroup.cpp +++ b/src/tallies/filter_delayedgroup.cpp @@ -11,7 +11,7 @@ void DelayedGroupFilter::from_xml(pugi::xml_node node) this->set_groups(groups); } -void DelayedGroupFilter::set_groups(gsl::span groups) +void DelayedGroupFilter::set_groups(span groups) { // Clear existing groups groups_.clear(); diff --git a/src/tallies/filter_distribcell.cpp b/src/tallies/filter_distribcell.cpp index c754dbd44ab..821e843daeb 100644 --- a/src/tallies/filter_distribcell.cpp +++ b/src/tallies/filter_distribcell.cpp @@ -1,5 +1,7 @@ #include "openmc/tallies/filter_distribcell.h" +#include + #include #include "openmc/cell.h" @@ -29,8 +31,8 @@ void DistribcellFilter::from_xml(pugi::xml_node node) void DistribcellFilter::set_cell(int32_t cell) { - Expects(cell >= 0); - Expects(cell < model::cells.size()); + assert(cell >= 0); + assert(cell < model::cells.size()); cell_ = cell; n_bins_ = model::cells[cell]->n_instances_; } diff --git a/src/tallies/filter_energy.cpp b/src/tallies/filter_energy.cpp index 4767dd175f0..0b954cce3a3 100644 --- a/src/tallies/filter_energy.cpp +++ b/src/tallies/filter_energy.cpp @@ -21,14 +21,14 @@ void EnergyFilter::from_xml(pugi::xml_node node) this->set_bins(bins); } -void EnergyFilter::set_bins(gsl::span bins) +void EnergyFilter::set_bins(span bins) { // Clear existing bins bins_.clear(); bins_.reserve(bins.size()); // Copy bins, ensuring they are valid - for (gsl::index i = 0; i < bins.size(); ++i) { + for (int64_t i = 0; i < bins.size(); ++i) { if (i > 0 && bins[i] <= bins[i - 1]) { throw std::runtime_error { "Energy bins must be monotonically increasing."}; @@ -46,7 +46,7 @@ void EnergyFilter::set_bins(gsl::span bins) if (!settings::run_CE) { if (n_bins_ == data::mg.num_energy_groups_) { matches_transport_groups_ = true; - for (gsl::index i = 0; i < n_bins_ + 1; ++i) { + for (int64_t i = 0; i < n_bins_ + 1; ++i) { if (data::mg.rev_energy_bins_[i] != bins_[i]) { matches_transport_groups_ = false; break; diff --git a/src/tallies/filter_energyfunc.cpp b/src/tallies/filter_energyfunc.cpp index 93ae24b2ada..fc4ba0ff91e 100644 --- a/src/tallies/filter_energyfunc.cpp +++ b/src/tallies/filter_energyfunc.cpp @@ -36,7 +36,7 @@ void EnergyFunctionFilter::from_xml(pugi::xml_node node) } void EnergyFunctionFilter::set_data( - gsl::span energy, gsl::span y) + span energy, span y) { // Check for consistent sizes with new data if (energy.size() != y.size()) { @@ -48,7 +48,7 @@ void EnergyFunctionFilter::set_data( y_.reserve(y.size()); // Copy over energy values, ensuring they are valid - for (gsl::index i = 0; i < energy.size(); ++i) { + for (int64_t i = 0; i < energy.size(); ++i) { if (i > 0 && energy[i] <= energy[i - 1]) { throw std::runtime_error { "Energy bins must be monotonically increasing."}; diff --git a/src/tallies/filter_material.cpp b/src/tallies/filter_material.cpp index 6669ab2fa39..215c9af72ff 100644 --- a/src/tallies/filter_material.cpp +++ b/src/tallies/filter_material.cpp @@ -1,5 +1,7 @@ #include "openmc/tallies/filter_material.h" +#include + #include #include "openmc/capi.h" @@ -24,7 +26,7 @@ void MaterialFilter::from_xml(pugi::xml_node node) this->set_materials(mats); } -void MaterialFilter::set_materials(gsl::span materials) +void MaterialFilter::set_materials(span materials) { // Clear existing materials materials_.clear(); @@ -33,8 +35,8 @@ void MaterialFilter::set_materials(gsl::span materials) // Update materials and mapping for (auto& index : materials) { - Expects(index >= 0); - Expects(index < model::materials.size()); + assert(index >= 0); + assert(index < model::materials.size()); materials_.push_back(index); map_[index] = materials_.size() - 1; } diff --git a/src/tallies/filter_mesh.cpp b/src/tallies/filter_mesh.cpp index 03f7da97847..4edfbec4b9b 100644 --- a/src/tallies/filter_mesh.cpp +++ b/src/tallies/filter_mesh.cpp @@ -1,7 +1,6 @@ #include "openmc/tallies/filter_mesh.h" #include -#include #include "openmc/capi.h" #include "openmc/constants.h" diff --git a/src/tallies/filter_mu.cpp b/src/tallies/filter_mu.cpp index 95bb3b21082..63915a53347 100644 --- a/src/tallies/filter_mu.cpp +++ b/src/tallies/filter_mu.cpp @@ -31,14 +31,14 @@ void MuFilter::from_xml(pugi::xml_node node) this->set_bins(bins); } -void MuFilter::set_bins(gsl::span bins) +void MuFilter::set_bins(span bins) { // Clear existing bins bins_.clear(); bins_.reserve(bins.size()); // Copy bins, ensuring they are valid - for (gsl::index i = 0; i < bins.size(); ++i) { + for (int64_t i = 0; i < bins.size(); ++i) { if (i > 0 && bins[i] <= bins[i - 1]) { throw std::runtime_error {"Mu bins must be monotonically increasing."}; } diff --git a/src/tallies/filter_particle.cpp b/src/tallies/filter_particle.cpp index 56d93a47f40..eef1d1e63c5 100644 --- a/src/tallies/filter_particle.cpp +++ b/src/tallies/filter_particle.cpp @@ -18,7 +18,7 @@ void ParticleFilter::from_xml(pugi::xml_node node) this->set_particles(types); } -void ParticleFilter::set_particles(gsl::span particles) +void ParticleFilter::set_particles(span particles) { // Clear existing particles particles_.clear(); diff --git a/src/tallies/filter_polar.cpp b/src/tallies/filter_polar.cpp index d132ccf4236..29be6a437e3 100644 --- a/src/tallies/filter_polar.cpp +++ b/src/tallies/filter_polar.cpp @@ -32,14 +32,14 @@ void PolarFilter::from_xml(pugi::xml_node node) this->set_bins(bins); } -void PolarFilter::set_bins(gsl::span bins) +void PolarFilter::set_bins(span bins) { // Clear existing bins bins_.clear(); bins_.reserve(bins.size()); // Copy bins, ensuring they are valid - for (gsl::index i = 0; i < bins.size(); ++i) { + for (int64_t i = 0; i < bins.size(); ++i) { if (i > 0 && bins[i] <= bins[i - 1]) { throw std::runtime_error {"Polar bins must be monotonically increasing."}; } diff --git a/src/tallies/filter_sph_harm.cpp b/src/tallies/filter_sph_harm.cpp index 359df379b2f..7441f32fb88 100644 --- a/src/tallies/filter_sph_harm.cpp +++ b/src/tallies/filter_sph_harm.cpp @@ -1,9 +1,9 @@ #include "openmc/tallies/filter_sph_harm.h" +#include #include // For pair #include -#include #include "openmc/capi.h" #include "openmc/error.h" @@ -30,7 +30,7 @@ void SphericalHarmonicsFilter::set_order(int order) n_bins_ = (order_ + 1) * (order_ + 1); } -void SphericalHarmonicsFilter::set_cosine(gsl::cstring_span cosine) +void SphericalHarmonicsFilter::set_cosine(const std::string& cosine) { if (cosine == "scatter") { cosine_ = SphericalHarmonicsCosine::scatter; @@ -39,7 +39,7 @@ void SphericalHarmonicsFilter::set_cosine(gsl::cstring_span cosine) } else { throw std::invalid_argument {fmt::format("Unrecognized cosine type, \"{}\" " "in spherical harmonics filter", - gsl::to_string(cosine))}; + cosine)}; } } @@ -87,7 +87,7 @@ void SphericalHarmonicsFilter::to_statepoint(hid_t filter_group) const std::string SphericalHarmonicsFilter::text_label(int bin) const { - Expects(bin >= 0 && bin < n_bins_); + assert(bin >= 0 && bin < n_bins_); for (int n = 0; n < order_ + 1; n++) { if (bin < (n + 1) * (n + 1)) { int m = (bin - n * n) - n; diff --git a/src/tallies/filter_surface.cpp b/src/tallies/filter_surface.cpp index 1fbf8d44e38..82f3d71789e 100644 --- a/src/tallies/filter_surface.cpp +++ b/src/tallies/filter_surface.cpp @@ -1,5 +1,7 @@ #include "openmc/tallies/filter_surface.h" +#include + #include #include "openmc/error.h" @@ -26,7 +28,7 @@ void SurfaceFilter::from_xml(pugi::xml_node node) this->set_surfaces(surfaces); } -void SurfaceFilter::set_surfaces(gsl::span surfaces) +void SurfaceFilter::set_surfaces(span surfaces) { // Clear existing surfaces surfaces_.clear(); @@ -35,8 +37,8 @@ void SurfaceFilter::set_surfaces(gsl::span surfaces) // Update surfaces and mapping for (auto& index : surfaces) { - Expects(index >= 0); - Expects(index < model::surfaces.size()); + assert(index >= 0); + assert(index < model::surfaces.size()); surfaces_.push_back(index); map_[index] = surfaces_.size() - 1; } diff --git a/src/tallies/filter_time.cpp b/src/tallies/filter_time.cpp index 0378aa28de3..948d1347a0f 100644 --- a/src/tallies/filter_time.cpp +++ b/src/tallies/filter_time.cpp @@ -21,7 +21,7 @@ void TimeFilter::from_xml(pugi::xml_node node) this->set_bins(bins); } -void TimeFilter::set_bins(gsl::span bins) +void TimeFilter::set_bins(span bins) { // Clear existing bins bins_.clear(); diff --git a/src/tallies/filter_universe.cpp b/src/tallies/filter_universe.cpp index 96ad0212d50..48114cfad59 100644 --- a/src/tallies/filter_universe.cpp +++ b/src/tallies/filter_universe.cpp @@ -1,5 +1,7 @@ #include "openmc/tallies/filter_universe.h" +#include + #include #include "openmc/cell.h" @@ -24,7 +26,7 @@ void UniverseFilter::from_xml(pugi::xml_node node) this->set_universes(universes); } -void UniverseFilter::set_universes(gsl::span universes) +void UniverseFilter::set_universes(span universes) { // Clear existing universes universes_.clear(); @@ -33,8 +35,8 @@ void UniverseFilter::set_universes(gsl::span universes) // Update universes and mapping for (auto& index : universes) { - Expects(index >= 0); - Expects(index < model::universes.size()); + assert(index >= 0); + assert(index < model::universes.size()); universes_.push_back(index); map_[index] = universes_.size() - 1; } diff --git a/src/tallies/filter_zernike.cpp b/src/tallies/filter_zernike.cpp index eb4c8bdfd00..af5b595aabb 100644 --- a/src/tallies/filter_zernike.cpp +++ b/src/tallies/filter_zernike.cpp @@ -1,11 +1,11 @@ #include "openmc/tallies/filter_zernike.h" +#include #include #include #include // For pair #include -#include #include "openmc/capi.h" #include "openmc/error.h" @@ -57,7 +57,7 @@ void ZernikeFilter::to_statepoint(hid_t filter_group) const std::string ZernikeFilter::text_label(int bin) const { - Expects(bin >= 0 && bin < n_bins_); + assert(bin >= 0 && bin < n_bins_); for (int n = 0; n < order_ + 1; n++) { int last = (n + 1) * (n + 2) / 2; if (bin < last) { diff --git a/src/tallies/tally.cpp b/src/tallies/tally.cpp index 4f33abf6beb..96d684f71a8 100644 --- a/src/tallies/tally.cpp +++ b/src/tallies/tally.cpp @@ -39,7 +39,8 @@ #include #include // for max -#include // for size_t +#include +#include // for size_t #include namespace openmc { @@ -140,7 +141,7 @@ Tally::Tally(pugi::xml_node node) // Check for the presence of certain filter types bool has_energyout = energyout_filter_ >= 0; int particle_filter_index = C_NONE; - for (gsl::index j = 0; j < filters_.size(); ++j) { + for (int64_t j = 0; j < filters_.size(); ++j) { int i_filter = filters_[j]; const auto& f = model::tally_filters[i_filter].get(); @@ -345,7 +346,7 @@ Tally* Tally::create(int32_t id) void Tally::set_id(int32_t id) { - Expects(id >= 0 || id == C_NONE); + assert(id >= 0 || id == C_NONE); // Clear entry in tally map if an ID was already assigned before if (id_ != C_NONE) { @@ -401,7 +402,7 @@ bool Tally::has_filter(FilterType filter_type) const return false; } -void Tally::set_filters(gsl::span filters) +void Tally::set_filters(span filters) { // Clear old data. filters_.clear(); @@ -1370,7 +1371,7 @@ extern "C" int openmc_tally_set_filters( try { // Convert indices to filter pointers vector filters; - for (gsl::index i = 0; i < n; ++i) { + for (int64_t i = 0; i < n; ++i) { int32_t i_filt = indices[i]; filters.push_back(model::tally_filters.at(i_filt).get()); } diff --git a/src/weight_windows.cpp b/src/weight_windows.cpp index 19ae7c07782..9daf21f750a 100644 --- a/src/weight_windows.cpp +++ b/src/weight_windows.cpp @@ -1,6 +1,7 @@ #include "openmc/weight_windows.h" #include +#include #include #include #include @@ -32,7 +33,6 @@ #include "openmc/xml_interface.h" #include -#include namespace openmc { @@ -289,7 +289,7 @@ void WeightWindows::allocate_ww_bounds() void WeightWindows::set_id(int32_t id) { - Expects(id >= 0 || id == C_NONE); + assert(id >= 0 || id == C_NONE); // Clear entry in mesh map in case one was already assigned if (id_ != C_NONE) { @@ -317,7 +317,7 @@ void WeightWindows::set_id(int32_t id) variance_reduction::ww_map[id] = index_; } -void WeightWindows::set_energy_bounds(gsl::span bounds) +void WeightWindows::set_energy_bounds(span bounds) { energy_bounds_.clear(); energy_bounds_.insert(energy_bounds_.begin(), bounds.begin(), bounds.end()); @@ -452,7 +452,7 @@ void WeightWindows::set_bounds( } void WeightWindows::set_bounds( - gsl::span lower_bounds, gsl::span upper_bounds) + span lower_bounds, span upper_bounds) { check_bounds(lower_bounds, upper_bounds); auto shape = this->bounds_size(); @@ -466,8 +466,7 @@ void WeightWindows::set_bounds( xt::adapt(upper_bounds.data(), upper_ww_.shape()); } -void WeightWindows::set_bounds( - gsl::span lower_bounds, double ratio) +void WeightWindows::set_bounds(span lower_bounds, double ratio) { this->check_bounds(lower_bounds); diff --git a/vendor/gsl-lite b/vendor/gsl-lite deleted file mode 160000 index 913e86d49c6..00000000000 --- a/vendor/gsl-lite +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 913e86d49c6a1acca980f4e325378f9dc393493a From 7638661fadaa30efac059374c8191d1c20c274b4 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Thu, 20 Feb 2025 13:07:07 -0600 Subject: [PATCH 156/184] Add nuclides_to_ignore argument on Model export methods (#3309) Co-authored-by: Patrick Shriwise --- openmc/model/model.py | 18 +++++++++++++----- tests/unit_tests/test_model.py | 18 ++++++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index 5e93d98040c..c7f358ce8e9 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -441,7 +441,8 @@ def deplete(self, timesteps, method='cecm', final_step=True, depletion_operator.cleanup_when_done = True depletion_operator.finalize() - def export_to_xml(self, directory='.', remove_surfs=False): + def export_to_xml(self, directory: PathLike = '.', remove_surfs: bool = False, + nuclides_to_ignore: Iterable[str] | None = None): """Export model to separate XML files. Parameters @@ -454,6 +455,9 @@ def export_to_xml(self, directory='.', remove_surfs=False): exporting. .. versionadded:: 0.13.1 + nuclides_to_ignore : list of str + Nuclides to ignore when exporting to XML. + """ # Create directory if required d = Path(directory) @@ -467,18 +471,19 @@ def export_to_xml(self, directory='.', remove_surfs=False): # for all materials in the geometry and use that to automatically build # a collection. if self.materials: - self.materials.export_to_xml(d) + self.materials.export_to_xml(d, nuclides_to_ignore=nuclides_to_ignore) else: materials = openmc.Materials(self.geometry.get_all_materials() .values()) - materials.export_to_xml(d) + materials.export_to_xml(d, nuclides_to_ignore=nuclides_to_ignore) if self.tallies: self.tallies.export_to_xml(d) if self.plots: self.plots.export_to_xml(d) - def export_to_model_xml(self, path='model.xml', remove_surfs=False): + def export_to_model_xml(self, path: PathLike = 'model.xml', remove_surfs: bool = False, + nuclides_to_ignore: Iterable[str] | None = None): """Export model to a single XML file. .. versionadded:: 0.13.3 @@ -491,6 +496,8 @@ def export_to_model_xml(self, path='model.xml', remove_surfs=False): remove_surfs : bool Whether or not to remove redundant surfaces from the geometry when exporting. + nuclides_to_ignore : list of str + Nuclides to ignore when exporting to XML. """ xml_path = Path(path) @@ -536,7 +543,8 @@ def export_to_model_xml(self, path='model.xml', remove_surfs=False): fh.write("\n") # Write the materials collection to the open XML file first. # This will write the XML header also - materials._write_xml(fh, False, level=1) + materials._write_xml(fh, False, level=1, + nuclides_to_ignore=nuclides_to_ignore) # Write remaining elements as a tree fh.write(ET.tostring(geometry_element, encoding="unicode")) fh.write(ET.tostring(settings_element, encoding="unicode")) diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 4b567c56d62..4d7d5be2bca 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -593,6 +593,24 @@ def test_single_xml_exec(run_in_tmpdir): pincell_model.run(path='subdir') +def test_nuclides_to_ignore(run_in_tmpdir, pin_model_attributes): + """Test nuclides_to_ignore when exporting a model XML""" + materials, geometry, settings = pin_model_attributes[:3] + model = openmc.Model(geometry=geometry, settings=settings) + + # grab one of the nuclides present in this model as a test + test_nuclide = list(materials[0].get_nuclides())[0] + + # exclude the test nuclide from the XML file during export + model.export_to_model_xml(nuclides_to_ignore=[test_nuclide]) + + # ensure that the nuclide doesn't appear after reading in + # the resulting XML model + xml_model = openmc.Model.from_model_xml() + for material in xml_model.materials: + assert test_nuclide not in material.get_nuclides() + + def test_model_plot(): # plots the geometry with source location and checks the resulting # matplotlib includes the correct coordinates for the scatter plot for all From 2b788ea6e0cc3ffa31daec33584214bbc667d622 Mon Sep 17 00:00:00 2001 From: Zoe Prieto <101403129+zoeprieto@users.noreply.github.com> Date: Thu, 20 Feb 2025 22:51:05 -0300 Subject: [PATCH 157/184] Streamline use of CompositeSurface with SurfaceFilter (#3167) Co-authored-by: Patrick Shriwise Co-authored-by: Paul Romano --- openmc/model/surface_composite.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index 88433118b7f..3a17b616965 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -33,6 +33,10 @@ def rotate(self, rotation, pivot=(0., 0., 0.), order='xyz', inplace=False): setattr(surf, name, s.rotate(rotation, pivot, order, inplace)) return surf + @property + def component_surfaces(self): + return [getattr(self, name) for name in self._surface_names] + @property def boundary_type(self): return getattr(self, self._surface_names[0]).boundary_type From d643ad0c416e91f61ff736438faff19009b88c70 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 21 Feb 2025 11:47:38 -0600 Subject: [PATCH 158/184] Simulation of decay photons through the D1S method (#3235) D1S FTW! --- CMakeLists.txt | 2 + docs/source/conf.py | 2 +- docs/source/pythonapi/base.rst | 1 + docs/source/pythonapi/capi.rst | 1 + docs/source/pythonapi/deplete.rst | 12 + docs/source/usersguide/decay_sources.rst | 92 +++++++ docs/source/usersguide/index.rst | 2 +- include/openmc/chain.h | 97 +++++++ include/openmc/endf.h | 4 + include/openmc/particle.h | 3 +- include/openmc/particle_data.h | 6 + include/openmc/reaction.h | 4 +- include/openmc/reaction_product.h | 6 + include/openmc/settings.h | 1 + include/openmc/tallies/filter.h | 1 + .../openmc/tallies/filter_parent_nuclide.h | 56 ++++ openmc/deplete/abc.py | 111 ++++---- openmc/deplete/d1s.py | 246 ++++++++++++++++++ openmc/filter.py | 31 ++- openmc/lib/core.py | 1 + openmc/lib/filter.py | 12 +- openmc/material.py | 5 +- openmc/settings.py | 25 +- src/chain.cpp | 116 +++++++++ src/initialize.cpp | 21 +- src/nuclide.cpp | 27 +- src/particle.cpp | 6 +- src/physics.cpp | 33 ++- src/reaction.cpp | 34 ++- src/reaction_product.cpp | 23 ++ src/settings.cpp | 6 + src/tallies/filter.cpp | 3 + src/tallies/filter_parent_nuclide.cpp | 79 ++++++ tests/chain_ni.xml | 173 ++++++++++++ tests/regression_tests/surface_source/test.py | 25 +- tests/unit_tests/test_d1s.py | 132 ++++++++++ 36 files changed, 1298 insertions(+), 101 deletions(-) create mode 100644 docs/source/usersguide/decay_sources.rst create mode 100644 include/openmc/chain.h create mode 100644 include/openmc/tallies/filter_parent_nuclide.h create mode 100644 openmc/deplete/d1s.py create mode 100644 src/chain.cpp create mode 100644 src/tallies/filter_parent_nuclide.cpp create mode 100644 tests/chain_ni.xml create mode 100644 tests/unit_tests/test_d1s.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 35694a14bc8..f76fcb247d9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -342,6 +342,7 @@ list(APPEND libopenmc_SOURCES src/boundary_condition.cpp src/bremsstrahlung.cpp src/cell.cpp + src/chain.cpp src/cmfd_solver.cpp src/cross_sections.cpp src/dagmc.cpp @@ -424,6 +425,7 @@ list(APPEND libopenmc_SOURCES src/tallies/filter_meshsurface.cpp src/tallies/filter_mu.cpp src/tallies/filter_musurface.cpp + src/tallies/filter_parent_nuclide.cpp src/tallies/filter_particle.cpp src/tallies/filter_polar.cpp src/tallies/filter_sph_harm.cpp diff --git a/docs/source/conf.py b/docs/source/conf.py index 726269f0b7c..60bd406fba5 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -52,7 +52,7 @@ templates_path = ['_templates'] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = {'.rst': 'restructuredtext'} # The encoding of source files. #source_encoding = 'utf-8' diff --git a/docs/source/pythonapi/base.rst b/docs/source/pythonapi/base.rst index d6d04444f2a..bfd94272748 100644 --- a/docs/source/pythonapi/base.rst +++ b/docs/source/pythonapi/base.rst @@ -145,6 +145,7 @@ Constructing Tallies openmc.TimeFilter openmc.ZernikeFilter openmc.ZernikeRadialFilter + openmc.ParentNuclideFilter openmc.ParticleFilter openmc.RegularMesh openmc.RectilinearMesh diff --git a/docs/source/pythonapi/capi.rst b/docs/source/pythonapi/capi.rst index 9ceff83fde8..c8e0e874dbf 100644 --- a/docs/source/pythonapi/capi.rst +++ b/docs/source/pythonapi/capi.rst @@ -79,6 +79,7 @@ Classes MeshSurfaceFilter MuFilter Nuclide + ParentNuclideFilter ParticleFilter PolarFilter RectilinearMesh diff --git a/docs/source/pythonapi/deplete.rst b/docs/source/pythonapi/deplete.rst index 8731a9a1383..d7a779b1298 100644 --- a/docs/source/pythonapi/deplete.rst +++ b/docs/source/pythonapi/deplete.rst @@ -287,3 +287,15 @@ the following abstract base classes: abc.Integrator abc.SIIntegrator abc.DepSystemSolver + +D1S Functions +------------- + +.. autosummary:: + :toctree: generated + :nosignatures: + :template: myfunction.rst + + d1s.prepare_tallies + d1s.time_correction_factors + d1s.apply_time_correction diff --git a/docs/source/usersguide/decay_sources.rst b/docs/source/usersguide/decay_sources.rst new file mode 100644 index 00000000000..e612206969c --- /dev/null +++ b/docs/source/usersguide/decay_sources.rst @@ -0,0 +1,92 @@ +.. usersguide_decay_sources: + +============= +Decay Sources +============= + +Through the :ref:`depletion ` capabilities in OpenMC, it +is possible to simulate radiation emitted from the decay of activated materials. +For fusion energy systems, this is commonly done using what is known as the +`rigorous 2-step `_ (R2S) method. +In this method, a neutron transport calculation is used to determine the neutron +flux and reaction rates over a cell- or mesh-based spatial discretization of the +model. Then, the neutron flux in each discrete region is used to predict the +activated material composition using a depletion solver. Finally, a photon +transport calculation with a source based on the activity and energy spectrum of +the activated materials is used to determine a desired physical response (e.g., +a dose rate) at one or more locations of interest. + +Once a depletion simulation has been completed in OpenMC, the intrinsic decay +source can be determined as follows. First the activated material composition +can be determined using the :class:`openmc.deplete.Results` object. Indexing an +instance of this class with the timestep index returns a +:class:`~openmc.deplete.StepResult` object, which itself has a +:meth:`~openmc.deplete.StepResult.get_material` method. Once the activated +:class:`~openmc.Material` has been obtained, the +:meth:`~openmc.Material.get_decay_photon_energy` method will give the energy +spectrum of the decay photon source. The integral of the spectrum also indicates +the intensity of the source in units of [Bq]. Altogether, the workflow looks as +follows:: + + results = openmc.deplete.Results("depletion_results.h5") + + # Get results at last timestep + step = results[-1] + + # Get activated material composition for ID=1 + activated_mat = step.get_material('1') + + # Determine photon source + photon_energy = activated_mat.get_decay_photon_energy() + +By default, the :meth:`~openmc.Material.get_decay_photon_energy` method will +eliminate spectral lines with very low intensity, but this behavior can be +configured with the ``clip_tolerance`` argument. + +Direct 1-Step (D1S) Calculations +================================ + +OpenMC also includes built-in capability for performing shutdown dose rate +calculations using the `direct 1-step `_ +(D1S) method. In this method, a single coupled neutron--photon transport +calculation is used where the prompt photon production is replaced with photons +produced from the decay of radionuclides in an activated material. To obtain +properly scaled results, it is also necessary to apply time correction factors. +A normal neutron transport calculation can be extended to a D1S calculation with +a few helper functions. First, import the ``d1s`` submodule, which is part of +:mod:`openmc.deplete`:: + + from openmc.deplete import d1s + +First, you need to instruct OpenMC to use decay photon data instead of prompt +photon data. This is done with an attribute on the :class:`~openmc.Settings` +class:: + + model = openmc.Model() + ... + model.settings.use_decay_photons = True + +To prepare any tallies for use of the D1S method, you should call the +:func:`~openmc.deplete.d1s.prepare_tallies` function, which adds a +:class:`openmc.ParentNuclideFilter` (used later for assigning time correction +factors) to any applicable tally and returns a list of possible radionuclides +based on the :ref:`chain file `. Once the tallies are prepared, +the model can be simulated:: + + output_path = model.run() + +Finally, the time correction factors need to be computed and applied to the +relevant tallies. This can be done with the aid of the +:func:`~openmc.deplete.d1s.time_correction_factors` and +:func:`~openmc.deplete.d1s.apply_time_correction` functions:: + + # Compute time correction factors based on irradiation schedule + factors = d1s.time_correction_factors(nuclides, timesteps, source_rates) + + # Get tally from statepoint + with openmc.StatePoint(output_path) as sp: + dose_tally = sp.get_tally(name='dose tally') + + # Apply time correction factors + tally = d1s.apply_time_correction(tally, factors, time_index) + diff --git a/docs/source/usersguide/index.rst b/docs/source/usersguide/index.rst index 74651c0114b..aef9b1a1c1a 100644 --- a/docs/source/usersguide/index.rst +++ b/docs/source/usersguide/index.rst @@ -21,6 +21,7 @@ essential aspects of using OpenMC to perform simulations. tallies plots depletion + decay_sources scripts processing parallel @@ -28,4 +29,3 @@ essential aspects of using OpenMC to perform simulations. variance_reduction random_ray troubleshoot - \ No newline at end of file diff --git a/include/openmc/chain.h b/include/openmc/chain.h new file mode 100644 index 00000000000..a3bc6f3a364 --- /dev/null +++ b/include/openmc/chain.h @@ -0,0 +1,97 @@ +//! \file chain.h +//! \brief Depletion chain and associated information + +#ifndef OPENMC_CHAIN_H +#define OPENMC_CHAIN_H + +#include +#include +#include + +#include "pugixml.hpp" + +#include "openmc/angle_energy.h" // for AngleEnergy +#include "openmc/distribution.h" // for UPtrDist +#include "openmc/memory.h" // for unique_ptr +#include "openmc/vector.h" + +namespace openmc { + +//============================================================================== +// Data for a nuclide in the depletion chain +//============================================================================== + +class ChainNuclide { +public: + // Types + struct Product { + std::string name; //!< Reaction product name + double branching_ratio; //!< Branching ratio + }; + + // Constructors, destructors + ChainNuclide(pugi::xml_node node); + ~ChainNuclide(); + + //! Compute the decay constant for the nuclide + //! \return Decay constant in [1/s] + double decay_constant() const { return std::log(2.0) / half_life_; } + + const Distribution* photon_energy() const { return photon_energy_.get(); } + const std::unordered_map>& reaction_products() const + { + return reaction_products_; + } + +private: + // Data members + std::string name_; //!< Name of nuclide + double half_life_ {0.0}; //!< Half-life in [s] + double decay_energy_ {0.0}; //!< Decay energy in [eV] + std::unordered_map> + reaction_products_; //!< Map of MT to reaction products + UPtrDist photon_energy_; //!< Decay photon energy distribution +}; + +//============================================================================== +// Angle-energy distribution for decay photon +//============================================================================== + +class DecayPhotonAngleEnergy : public AngleEnergy { +public: + explicit DecayPhotonAngleEnergy(const Distribution* dist) + : photon_energy_(dist) + {} + + //! Sample distribution for an angle and energy + //! \param[in] E_in Incoming energy in [eV] + //! \param[out] E_out Outgoing energy in [eV] + //! \param[out] mu Outgoing cosine with respect to current direction + //! \param[inout] seed Pseudorandom seed pointer + void sample( + double E_in, double& E_out, double& mu, uint64_t* seed) const override; + +private: + const Distribution* photon_energy_; +}; + +//============================================================================== +// Global variables +//============================================================================== + +namespace data { + +extern std::unordered_map chain_nuclide_map; +extern vector> chain_nuclides; + +} // namespace data + +//============================================================================== +// Non-member functions +//============================================================================== + +void read_chain_file_xml(); + +} // namespace openmc + +#endif // OPENMC_CHAIN_H diff --git a/include/openmc/endf.h b/include/openmc/endf.h index e580874b4b1..4a737eb8816 100644 --- a/include/openmc/endf.h +++ b/include/openmc/endf.h @@ -53,6 +53,10 @@ class Polynomial : public Function1D { //! \param[in] dset Dataset containing coefficients explicit Polynomial(hid_t dset); + //! Construct polynomial from coefficients + //! \param[in] coef Polynomial coefficients + explicit Polynomial(vector coef) : coef_(coef) {} + //! Evaluate the polynomials //! \param[in] x independent variable //! \return Polynomial evaluated at x diff --git a/include/openmc/particle.h b/include/openmc/particle.h index aaac864e11e..0f37719b94f 100644 --- a/include/openmc/particle.h +++ b/include/openmc/particle.h @@ -47,7 +47,8 @@ class Particle : public ParticleData { //! \param u Direction of the secondary particle //! \param E Energy of the secondary particle in [eV] //! \param type Particle type - void create_secondary(double wgt, Direction u, double E, ParticleType type); + //! \return Whether a secondary particle was created + bool create_secondary(double wgt, Direction u, double E, ParticleType type); //! split a particle // diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index e2fac79e372..6b318385ea7 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -49,6 +49,9 @@ struct SourceSite { int delayed_group {0}; int surf_id {0}; ParticleType particle; + + // Extra attributes that don't show up in source written to file + int parent_nuclide {-1}; int64_t parent_id; int64_t progeny_id; }; @@ -441,6 +444,7 @@ class ParticleData : public GeometryState { int event_nuclide_; int event_mt_; int delayed_group_ {0}; + int parent_nuclide_ {-1}; int n_bank_ {0}; double bank_second_E_ {0.0}; @@ -555,6 +559,8 @@ class ParticleData : public GeometryState { const int& event_nuclide() const { return event_nuclide_; } int& event_mt() { return event_mt_; } // MT number of collision int& delayed_group() { return delayed_group_; } // delayed group + const int& parent_nuclide() const { return parent_nuclide_; } + int& parent_nuclide() { return parent_nuclide_; } // Parent nuclide // Post-collision data double& bank_second_E() diff --git a/include/openmc/reaction.h b/include/openmc/reaction.h index d5f04d136d0..3314d18666f 100644 --- a/include/openmc/reaction.h +++ b/include/openmc/reaction.h @@ -26,7 +26,9 @@ class Reaction { //! Construct reaction from HDF5 data //! \param[in] group HDF5 group containing reaction data //! \param[in] temperatures Desired temperatures for cross sections - explicit Reaction(hid_t group, const vector& temperatures); + //! \param[in] name Name of the nuclide + explicit Reaction( + hid_t group, const vector& temperatures, std::string name); //! Calculate cross section given temperautre/grid index, interpolation factor // diff --git a/include/openmc/reaction_product.h b/include/openmc/reaction_product.h index ce4fa8fc766..4fbbc1b626a 100644 --- a/include/openmc/reaction_product.h +++ b/include/openmc/reaction_product.h @@ -7,6 +7,7 @@ #include "hdf5.h" #include "openmc/angle_energy.h" +#include "openmc/chain.h" #include "openmc/endf.h" #include "openmc/memory.h" // for unique_ptr #include "openmc/particle.h" @@ -37,6 +38,10 @@ class ReactionProduct { //! \param[in] group HDF5 group containing data explicit ReactionProduct(hid_t group); + //! Construct reaction product for decay photon from chain nuclide product + //! \param[in] product Chain nuclide product + explicit ReactionProduct(const ChainNuclide::Product& product); + //! Sample an outgoing angle and energy //! \param[in] E_in Incoming energy in [eV] //! \param[out] E_out Outgoing energy in [eV] @@ -50,6 +55,7 @@ class ReactionProduct { unique_ptr yield_; //!< Yield as a function of energy vector applicability_; //!< Applicability of distribution vector distribution_; //!< Secondary angle-energy distribution + int parent_nuclide_ = -1; //!< Index of chain nuclide that is parent }; } // namespace openmc diff --git a/include/openmc/settings.h b/include/openmc/settings.h index 9a4ce56ec8f..12835fdef9a 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -67,6 +67,7 @@ extern bool trigger_predict; //!< predict batches for triggers? extern bool uniform_source_sampling; //!< sample sources uniformly? extern bool ufs_on; //!< uniform fission site method on? extern bool urr_ptables_on; //!< use unresolved resonance prob. tables? +extern bool use_decay_photons; //!< use decay photons for D1S extern "C" bool weight_windows_on; //!< are weight windows are enabled? extern bool weight_window_checkpoint_surface; //!< enable weight window check //!< upon surface crossing? diff --git a/include/openmc/tallies/filter.h b/include/openmc/tallies/filter.h index 31dd609ed10..3e982d0cf45 100644 --- a/include/openmc/tallies/filter.h +++ b/include/openmc/tallies/filter.h @@ -36,6 +36,7 @@ enum class FilterType { MESH_SURFACE, MU, MUSURFACE, + PARENT_NUCLIDE, PARTICLE, POLAR, SPHERICAL_HARMONICS, diff --git a/include/openmc/tallies/filter_parent_nuclide.h b/include/openmc/tallies/filter_parent_nuclide.h new file mode 100644 index 00000000000..53f8a5fa41d --- /dev/null +++ b/include/openmc/tallies/filter_parent_nuclide.h @@ -0,0 +1,56 @@ +#ifndef OPENMC_TALLIES_FILTER_PARENT_NUCLIDE_H +#define OPENMC_TALLIES_FILTER_PARENT_NUCLIDE_H + +#include +#include + +#include "openmc/span.h" +#include "openmc/tallies/filter.h" +#include "openmc/vector.h" + +namespace openmc { + +//============================================================================== +//! Bins events by parent nuclide (for decay photons) +//============================================================================== + +class ParentNuclideFilter : public Filter { +public: + //---------------------------------------------------------------------------- + // Constructors, destructors + + ~ParentNuclideFilter() = default; + + //---------------------------------------------------------------------------- + // Methods + + std::string type_str() const override { return "parentnuclide"; } + FilterType type() const override { return FilterType::PARENT_NUCLIDE; } + + void from_xml(pugi::xml_node node) override; + + void get_all_bins(const Particle& p, TallyEstimator estimator, + FilterMatch& match) const override; + + void to_statepoint(hid_t filter_group) const override; + + std::string text_label(int bin) const override; + + //---------------------------------------------------------------------------- + // Accessors + + const vector& bins() const { return bins_; } + void set_bins(span bins); + +protected: + //---------------------------------------------------------------------------- + // Data members + + vector bins_; + vector nuclides_; + + std::unordered_map map_; +}; + +} // namespace openmc +#endif // OPENMC_TALLIES_FILTER_PARENT_NUCLIDE_H diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index 784023f26ff..fc98239a561 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -41,6 +41,63 @@ _SECONDS_PER_DAY = 24*60*60 _SECONDS_PER_JULIAN_YEAR = 365.25*24*60*60 + +def _normalize_timesteps( + timesteps: Sequence[float] | Sequence[tuple[float, str]], + source_rates: float | Sequence[float], + timestep_units: str = 's', + operator: TransportOperator | None = None, +): + if not isinstance(source_rates, Sequence): + # Ensure that rate is single value if that is the case + source_rates = [source_rates] * len(timesteps) + + if len(source_rates) != len(timesteps): + raise ValueError( + "Number of time steps ({}) != number of powers ({})".format( + len(timesteps), len(source_rates))) + + # Get list of times / units + if isinstance(timesteps[0], Sequence): + times, units = zip(*timesteps) + else: + times = timesteps + units = [timestep_units] * len(timesteps) + + # Determine number of seconds for each timestep + seconds = [] + for timestep, unit, rate in zip(times, units, source_rates): + # Make sure values passed make sense + check_type('timestep', timestep, Real) + check_greater_than('timestep', timestep, 0.0, False) + check_type('timestep units', unit, str) + check_type('source rate', rate, Real) + check_greater_than('source rate', rate, 0.0, True) + + if unit in ('s', 'sec'): + seconds.append(timestep) + elif unit in ('min', 'minute'): + seconds.append(timestep*_SECONDS_PER_MINUTE) + elif unit in ('h', 'hr', 'hour'): + seconds.append(timestep*_SECONDS_PER_HOUR) + elif unit in ('d', 'day'): + seconds.append(timestep*_SECONDS_PER_DAY) + elif unit in ('a', 'year'): + seconds.append(timestep*_SECONDS_PER_JULIAN_YEAR) + elif unit.lower() == 'mwd/kg': + watt_days_per_kg = 1e6*timestep + kilograms = 1e-3*operator.heavy_metal + if rate == 0.0: + raise ValueError("Cannot specify a timestep in [MWd/kg] when" + " the power is zero.") + days = watt_days_per_kg * kilograms / rate + seconds.append(days*_SECONDS_PER_DAY) + else: + raise ValueError(f"Invalid timestep unit '{unit}'") + + return (np.asarray(seconds), np.asarray(source_rates)) + + OperatorResult = namedtuple('OperatorResult', ['k', 'rates']) OperatorResult.__doc__ = """\ Result of applying transport operator @@ -551,10 +608,10 @@ class Integrator(ABC): def __init__( self, operator: TransportOperator, - timesteps: Sequence[float], + timesteps: Sequence[float] | Sequence[tuple[float, str]], power: Optional[Union[float, Sequence[float]]] = None, power_density: Optional[Union[float, Sequence[float]]] = None, - source_rates: Optional[Sequence[float]] = None, + source_rates: Optional[Union[float, Sequence[float]]] = None, timestep_units: str = 's', solver: str = "cram48" ): @@ -582,53 +639,9 @@ def __init__( elif source_rates is None: raise ValueError("Either power, power_density, or source_rates must be set") - if not isinstance(source_rates, Iterable): - # Ensure that rate is single value if that is the case - source_rates = [source_rates] * len(timesteps) - - if len(source_rates) != len(timesteps): - raise ValueError( - "Number of time steps ({}) != number of powers ({})".format( - len(timesteps), len(source_rates))) - - # Get list of times / units - if isinstance(timesteps[0], Iterable): - times, units = zip(*timesteps) - else: - times = timesteps - units = [timestep_units] * len(timesteps) - - # Determine number of seconds for each timestep - seconds = [] - for timestep, unit, rate in zip(times, units, source_rates): - # Make sure values passed make sense - check_type('timestep', timestep, Real) - check_greater_than('timestep', timestep, 0.0, False) - check_type('timestep units', unit, str) - check_type('source rate', rate, Real) - check_greater_than('source rate', rate, 0.0, True) - - if unit in ('s', 'sec'): - seconds.append(timestep) - elif unit in ('min', 'minute'): - seconds.append(timestep*_SECONDS_PER_MINUTE) - elif unit in ('h', 'hr', 'hour'): - seconds.append(timestep*_SECONDS_PER_HOUR) - elif unit in ('d', 'day'): - seconds.append(timestep*_SECONDS_PER_DAY) - elif unit in ('a', 'year'): - seconds.append(timestep*_SECONDS_PER_JULIAN_YEAR) - elif unit.lower() == 'mwd/kg': - watt_days_per_kg = 1e6*timestep - kilograms = 1e-3*operator.heavy_metal - if rate == 0.0: - raise ValueError("Cannot specify a timestep in [MWd/kg] when" - " the power is zero.") - days = watt_days_per_kg * kilograms / rate - seconds.append(days*_SECONDS_PER_DAY) - else: - raise ValueError(f"Invalid timestep unit '{unit}'") - + # Normalize timesteps and source rates + seconds, source_rates = _normalize_timesteps( + timesteps, source_rates, timestep_units, operator) self.timesteps = np.asarray(seconds) self.source_rates = np.asarray(source_rates) diff --git a/openmc/deplete/d1s.py b/openmc/deplete/d1s.py new file mode 100644 index 00000000000..22fc6ec23da --- /dev/null +++ b/openmc/deplete/d1s.py @@ -0,0 +1,246 @@ +"""D1S module + +This module contains functionality to support the direct 1-step (D1S) method for +shutdown dose rate calculations. + +""" + +from copy import deepcopy +from typing import Sequence +from math import log, prod + +import numpy as np + +import openmc +from openmc.data import half_life +from .abc import _normalize_timesteps +from .chain import Chain + + +def get_radionuclides(model: openmc.Model, chain_file: str | None = None) -> list[str]: + """Determine all radionuclides that can be produced during D1S. + + Parameters + ---------- + model : openmc.Model + Model that should be used for determining what nuclides are present + chain_file : str, optional + Which chain file to use for inspecting decay data. If None is passed, + defaults to ``openmc.config['chain_file']`` + + Returns + ------- + List of nuclide names + + """ + # Determine what nuclides appear in model + model_nuclides = set(model.geometry.get_all_nuclides()) + + # Load chain file + if chain_file is None: + chain_file = openmc.config['chain_file'] + chain = Chain.from_xml(chain_file) + + radionuclides = set() + for nuclide in chain.nuclides: + # Restrict to set of nuclides present in model + if nuclide.name not in model_nuclides: + continue + + # Loop over reactions and add any targets that are unstable + for rx_tuple in nuclide.reactions: + target = rx_tuple.target + if target is None: + continue + target_nuclide = chain[target] + if target_nuclide.half_life is not None: + radionuclides.add(target_nuclide.name) + + return list(radionuclides) + + +def time_correction_factors( + nuclides: list[str], + timesteps: Sequence[float] | Sequence[tuple[float, str]], + source_rates: float | Sequence[float], + timestep_units: str = 's' +) -> dict[str, np.ndarray]: + """Calculate time correction factors for the D1S method. + + This function determines the time correction factor that should be applied + to photon tallies as part of the D1S method. + + Parameters + ---------- + nuclides : list of str + The name of the nuclide to find the time correction for, e.g., 'Ni65' + timesteps : iterable of float or iterable of tuple + Array of timesteps. Note that values are not cumulative. The units are + specified by the `timestep_units` argument when `timesteps` is an + iterable of float. Alternatively, units can be specified for each step + by passing a sequence of (value, unit) tuples. + source_rates : float or iterable of float + Source rate in [neutron/sec] for each interval in `timesteps` + timestep_units : {'s', 'min', 'h', 'd', 'a'}, optional + Units for values specified in the `timesteps` argument. 's' means + seconds, 'min' means minutes, 'h' means hours, and 'a' means Julian + years. + + Returns + ------- + dict + Dictionary mapping nuclide to an array of time correction factors for + each time. + + """ + + # Determine normalized timesteps and source rates + timesteps, source_rates = _normalize_timesteps( + timesteps, source_rates, timestep_units) + + # Calculate decay rate for each nuclide + decay_rate = np.array([log(2.0) / half_life(x) for x in nuclides]) + + n_timesteps = len(timesteps) + 1 + n_nuclides = len(nuclides) + + # Create a 2D array for the time correction factors + h = np.zeros((n_timesteps, n_nuclides)) + + for i, (dt, rate) in enumerate(zip(timesteps, source_rates)): + # Precompute the exponential terms. Since (1 - exp(-x)) is susceptible to + # roundoff error, use expm1 instead (which computes exp(x) - 1) + g = np.exp(-decay_rate*dt) + one_minus_g = -np.expm1(-decay_rate*dt) + + # Eq. (4) in doi:10.1016/j.fusengdes.2019.111399 + h[i + 1] = rate*one_minus_g + h[i]*g + + return {nuclides[i]: h[:, i] for i in range(n_nuclides)} + + +def apply_time_correction( + tally: openmc.Tally, + time_correction_factors: dict[str, np.ndarray], + index: int = -1, + sum_nuclides: bool = True +) -> openmc.Tally: + """Apply time correction factors to a tally. + + This function applies the time correction factors at the given index to a + tally that contains a :class:`~openmc.ParentNuclideFilter`. When + `sum_nuclides` is True, values over all parent nuclides will be summed, + leaving a single value for each filter combination. + + Parameters + ---------- + tally : openmc.Tally + Tally to apply the time correction factors to + time_correction_factors : dict + Time correction factors as returned by :func:`time_correction_factors` + index : int, optional + Index to use for the correction factors + sum_nuclides : bool + Whether to sum over the parent nuclides + + Returns + ------- + openmc.Tally + Derived tally with time correction factors applied + + """ + # Make sure the tally contains a ParentNuclideFilter + for i_filter, filter in enumerate(tally.filters): + if isinstance(filter, openmc.ParentNuclideFilter): + break + else: + raise ValueError('Tally must contain a ParentNuclideFilter') + + # Get list of radionuclides based on tally filter + radionuclides = [str(x) for x in tally.filters[i_filter].bins] + tcf = np.array([time_correction_factors[x][index] for x in radionuclides]) + + # Create copy of tally + new_tally = deepcopy(tally) + + # Determine number of bins in other filters + n_bins_before = prod([f.num_bins for f in tally.filters[:i_filter]]) + n_bins_after = prod([f.num_bins for f in tally.filters[i_filter + 1:]]) + + # Reshape sum and sum_sq, apply TCF, and sum along that axis + _, n_nuclides, n_scores = new_tally.shape + n_radionuclides = len(radionuclides) + shape = (n_bins_before, n_radionuclides, n_bins_after, n_nuclides, n_scores) + tally_sum = new_tally.sum.reshape(shape) + tally_sum_sq = new_tally.sum_sq.reshape(shape) + + # Apply TCF, broadcasting to the correct dimensions + tcf.shape = (1, -1, 1, 1, 1) + new_tally._sum = tally_sum * tcf + new_tally._sum_sq = tally_sum_sq * (tcf*tcf) + new_tally._mean = None + new_tally._std_dev = None + + shape = (-1, n_nuclides, n_scores) + + if sum_nuclides: + # Query the mean and standard deviation + mean = new_tally.mean + std_dev = new_tally.std_dev + + # Sum over parent nuclides (note that when combining different bins for + # parent nuclide, we can't work directly on sum_sq) + new_tally._mean = mean.sum(axis=1).reshape(shape) + new_tally._std_dev = np.linalg.norm(std_dev, axis=1).reshape(shape) + new_tally._derived = True + + # Remove ParentNuclideFilter + new_tally.filters.pop(i_filter) + else: + new_tally._sum.shape = shape + new_tally._sum_sq.shape = shape + + return new_tally + + +def prepare_tallies( + model: openmc.Model, + nuclides: list[str] | None = None, + chain_file: str | None = None +) -> list[str]: + """Prepare tallies for the D1S method. + + This function adds a :class:`~openmc.ParentNuclideFilter` to any tally that + has a particle filter with a single 'photon' bin. + + Parameters + ---------- + model : openmc.Model + Model to prepare tallies for + nuclides : list of str, optional + Nuclides to use for the parent nuclide filter. If None, radionuclides + are determined from :func:`get_radionuclides`. + chain_file : str, optional + Chain file to use for inspecting decay data. If None, defaults to + ``openmc.config['chain_file']`` + + Returns + ------- + list of str + List of parent nuclides being filtered on + + """ + if nuclides is None: + nuclides = get_radionuclides(model, chain_file) + filter = openmc.ParentNuclideFilter(nuclides) + + # Apply parent nuclide filter to any tally that has a particle filter with a + # single 'photon' bin + for tally in model.tallies: + for f in tally.filters: + if isinstance(f, openmc.ParticleFilter): + if list(f.bins) == ['photon']: + tally.filters.append(filter) + break + + return nuclides diff --git a/openmc/filter.py b/openmc/filter.py index b5926af5ad9..35a7e7c7e96 100644 --- a/openmc/filter.py +++ b/openmc/filter.py @@ -25,7 +25,7 @@ 'energyout', 'mu', 'musurface', 'polar', 'azimuthal', 'distribcell', 'delayedgroup', 'energyfunction', 'cellfrom', 'materialfrom', 'legendre', 'spatiallegendre', 'sphericalharmonics', 'zernike', 'zernikeradial', 'particle', 'cellinstance', - 'collision', 'time' + 'collision', 'time', 'parentnuclide' ) _CURRENT_NAMES = ( @@ -732,7 +732,7 @@ class SurfaceFilter(WithIDFilter): class ParticleFilter(Filter): - """Bins tally events based on the Particle type. + """Bins tally events based on the particle type. Parameters ---------- @@ -788,6 +788,33 @@ def from_xml_element(cls, elem, **kwargs): return cls(bins, filter_id=filter_id) +class ParentNuclideFilter(ParticleFilter): + """Bins tally events based on the parent nuclide + + Parameters + ---------- + bins : str, or iterable of str + Names of nuclides (e.g., 'Ni65') + filter_id : int + Unique identifier for the filter + + Attributes + ---------- + bins : iterable of str + Names of nuclides + id : int + Unique identifier for the filter + num_bins : Integral + The number of filter bins + + """ + @Filter.bins.setter + def bins(self, bins): + bins = np.atleast_1d(bins) + cv.check_iterable_type('filter bins', bins, str) + self._bins = bins + + class MeshFilter(Filter): """Bins tally event locations by mesh elements. diff --git a/openmc/lib/core.py b/openmc/lib/core.py index 8561602e670..577913dcb8f 100644 --- a/openmc/lib/core.py +++ b/openmc/lib/core.py @@ -25,6 +25,7 @@ class _SourceSite(Structure): ('delayed_group', c_int), ('surf_id', c_int), ('particle', c_int), + ('parent_nuclide', c_int), ('parent_id', c_int64), ('progeny_id', c_int64)] diff --git a/openmc/lib/filter.py b/openmc/lib/filter.py index b30f5e66282..b6086c56711 100644 --- a/openmc/lib/filter.py +++ b/openmc/lib/filter.py @@ -21,9 +21,10 @@ 'CellInstanceFilter', 'CollisionFilter', 'DistribcellFilter', 'DelayedGroupFilter', 'EnergyFilter', 'EnergyoutFilter', 'EnergyFunctionFilter', 'LegendreFilter', 'MaterialFilter', 'MaterialFromFilter', 'MeshFilter', 'MeshBornFilter', - 'MeshSurfaceFilter', 'MuFilter', 'MuSurfaceFilter', 'ParticleFilter', 'PolarFilter', - 'SphericalHarmonicsFilter', 'SpatialLegendreFilter', 'SurfaceFilter', 'UniverseFilter', - 'ZernikeFilter', 'ZernikeRadialFilter', 'filters' + 'MeshSurfaceFilter', 'MuFilter', 'MuSurfaceFilter', 'ParentNuclideFilter', + 'ParticleFilter', 'PolarFilter', 'SphericalHarmonicsFilter', + 'SpatialLegendreFilter', 'SurfaceFilter', 'UniverseFilter', 'ZernikeFilter', + 'ZernikeRadialFilter', 'filters' ] # Tally functions @@ -544,6 +545,10 @@ class MuSurfaceFilter(Filter): filter_type = 'musurface' +class ParentNuclideFilter(Filter): + filter_type = 'parentnuclide' + + class ParticleFilter(Filter): filter_type = 'particle' @@ -647,6 +652,7 @@ class ZernikeRadialFilter(ZernikeFilter): 'meshsurface': MeshSurfaceFilter, 'mu': MuFilter, 'musurface': MuSurfaceFilter, + 'parentnuclide': ParentNuclideFilter, 'particle': ParticleFilter, 'polar': PolarFilter, 'sphericalharmonics': SphericalHarmonicsFilter, diff --git a/openmc/material.py b/openmc/material.py index 343b2fff293..64458f57133 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -308,8 +308,9 @@ def get_decay_photon_energy( Returns ------- - Decay photon energy distribution. The integral of this distribution is - the total intensity of the photon source in the requested units. + Univariate or None + Decay photon energy distribution. The integral of this distribution + is the total intensity of the photon source in the requested units. """ cv.check_value('units', units, {'Bq', 'Bq/g', 'Bq/cm3'}) diff --git a/openmc/settings.py b/openmc/settings.py index 110d57c19f3..0e2a1839907 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -275,6 +275,8 @@ class Settings: ufs_mesh : openmc.RegularMesh Mesh to be used for redistributing source sites via the uniform fission site (UFS) method. + use_decay_photons : bool + Produce decay photons from neutron reactions instead of prompt verbosity : int Verbosity during simulation between 1 and 10. Verbosity levels are described in :ref:`verbosity`. @@ -393,6 +395,7 @@ def __init__(self, **kwargs): self._weight_window_checkpoints = {} self._max_history_splits = None self._max_tracks = None + self._use_decay_photons = None self._random_ray = {} @@ -1143,6 +1146,15 @@ def random_ray(self, random_ray: dict): self._random_ray = random_ray + @property + def use_decay_photons(self) -> bool: + return self._use_decay_photons + + @use_decay_photons.setter + def use_decay_photons(self, value): + cv.check_type('use decay photons', value, bool) + self._use_decay_photons = value + def _create_run_mode_subelement(self, root): elem = ET.SubElement(root, "run_mode") elem.text = self._run_mode.value @@ -1431,6 +1443,11 @@ def _create_ufs_mesh_subelement(self, root, mesh_memo=None): root.append(self.ufs_mesh.to_xml_element()) if mesh_memo is not None: mesh_memo.add(self.ufs_mesh.id) + def _create_use_decay_photons_subelement(self, root): + if self._use_decay_photons is not None: + element = ET.SubElement(root, "use_decay_photons") + element.text = str(self._use_decay_photons).lower() + def _create_resonance_scattering_subelement(self, root): res = self.resonance_scattering if res: @@ -1957,6 +1974,11 @@ def _random_ray_from_xml_element(self, root): elif child.tag == 'sample_method': self.random_ray['sample_method'] = child.text + def _use_decay_photons_from_xml_element(self, root): + text = get_text(root, 'use_decay_photons') + if text is not None: + self.use_decay_photons = text in ('true', '1') + def to_xml_element(self, mesh_memo=None): """Create a 'settings' element to be written to an XML file. @@ -2021,6 +2043,7 @@ def to_xml_element(self, mesh_memo=None): self._create_max_history_splits_subelement(element) self._create_max_tracks_subelement(element) self._create_random_ray_subelement(element) + self._create_use_decay_photons_subelement(element) # Clean the indentation in the file to be user-readable clean_indentation(element) @@ -2126,8 +2149,8 @@ def from_xml_element(cls, elem, meshes=None): settings._max_history_splits_from_xml_element(elem) settings._max_tracks_from_xml_element(elem) settings._random_ray_from_xml_element(elem) + settings._use_decay_photons_from_xml_element(elem) - # TODO: Get volume calculations return settings @classmethod diff --git a/src/chain.cpp b/src/chain.cpp new file mode 100644 index 00000000000..e279d1f5916 --- /dev/null +++ b/src/chain.cpp @@ -0,0 +1,116 @@ +//! \file chain.cpp +//! \brief Depletion chain and associated information + +#include "openmc/chain.h" + +#include // for getenv +#include // for make_unique +#include // for stod + +#include +#include + +#include "openmc/distribution.h" // for distribution_from_xml +#include "openmc/error.h" +#include "openmc/reaction.h" +#include "openmc/xml_interface.h" // for get_node_value + +namespace openmc { + +//============================================================================== +// ChainNuclide implementation +//============================================================================== + +ChainNuclide::ChainNuclide(pugi::xml_node node) +{ + name_ = get_node_value(node, "name"); + if (check_for_node(node, "half_life")) { + half_life_ = std::stod(get_node_value(node, "half_life")); + } + if (check_for_node(node, "decay_energy")) { + decay_energy_ = std::stod(get_node_value(node, "decay_energy")); + } + + // Read reactions to store MT -> product map + for (pugi::xml_node reaction_node : node.children("reaction")) { + std::string rx_name = get_node_value(reaction_node, "type"); + if (!reaction_node.attribute("target")) + continue; + std::string rx_target = get_node_value(reaction_node, "target"); + double branching_ratio = 1.0; + if (reaction_node.attribute("branching_ratio")) { + branching_ratio = + std::stod(get_node_value(reaction_node, "branching_ratio")); + } + int mt = reaction_type(rx_name); + reaction_products_[mt].push_back({rx_target, branching_ratio}); + } + + for (pugi::xml_node source_node : node.children("source")) { + auto particle = get_node_value(source_node, "particle"); + if (particle == "photon") { + photon_energy_ = distribution_from_xml(source_node); + break; + } + } + + // Set entry in mapping + data::chain_nuclide_map[name_] = data::chain_nuclides.size(); +} + +ChainNuclide::~ChainNuclide() +{ + data::chain_nuclide_map.erase(name_); +} + +//============================================================================== +// DecayPhotonAngleEnergy implementation +//============================================================================== + +void DecayPhotonAngleEnergy::sample( + double E_in, double& E_out, double& mu, uint64_t* seed) const +{ + E_out = photon_energy_->sample(seed); + mu = Uniform(-1., 1.).sample(seed); +} + +//============================================================================== +// Global variables +//============================================================================== + +namespace data { + +std::unordered_map chain_nuclide_map; +vector> chain_nuclides; + +} // namespace data + +//============================================================================== +// Non-member functions +//============================================================================== + +void read_chain_file_xml() +{ + char* chain_file_path = std::getenv("OPENMC_CHAIN_FILE"); + if (!chain_file_path) { + return; + } + + write_message(5, "Reading chain file: {}...", chain_file_path); + + pugi::xml_document doc; + auto result = doc.load_file(chain_file_path); + if (!result) { + fatal_error( + fmt::format("Error processing chain file: {}", chain_file_path)); + } + + // Get root element + pugi::xml_node root = doc.document_element(); + + for (auto node : root.children("nuclide")) { + data::chain_nuclides.push_back(std::make_unique(node)); + } +} + +} // namespace openmc diff --git a/src/initialize.cpp b/src/initialize.cpp index cc1eac9cf35..5f717b137f0 100644 --- a/src/initialize.cpp +++ b/src/initialize.cpp @@ -12,6 +12,7 @@ #include #include "openmc/capi.h" +#include "openmc/chain.h" #include "openmc/constants.h" #include "openmc/cross_sections.h" #include "openmc/error.h" @@ -164,16 +165,17 @@ void initialize_mpi(MPI_Comm intracomm) MPI_Get_address(&b.delayed_group, &disp[5]); MPI_Get_address(&b.surf_id, &disp[6]); MPI_Get_address(&b.particle, &disp[7]); - MPI_Get_address(&b.parent_id, &disp[8]); - MPI_Get_address(&b.progeny_id, &disp[9]); - for (int i = 9; i >= 0; --i) { + MPI_Get_address(&b.parent_nuclide, &disp[8]); + MPI_Get_address(&b.parent_id, &disp[9]); + MPI_Get_address(&b.progeny_id, &disp[10]); + for (int i = 10; i >= 0; --i) { disp[i] -= disp[0]; } - int blocks[] {3, 3, 1, 1, 1, 1, 1, 1, 1, 1}; + int blocks[] {3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1}; MPI_Datatype types[] {MPI_DOUBLE, MPI_DOUBLE, MPI_DOUBLE, MPI_DOUBLE, - MPI_DOUBLE, MPI_INT, MPI_INT, MPI_INT, MPI_LONG, MPI_LONG}; - MPI_Type_create_struct(10, blocks, disp, types, &mpi::source_site); + MPI_DOUBLE, MPI_INT, MPI_INT, MPI_INT, MPI_INT, MPI_LONG, MPI_LONG}; + MPI_Type_create_struct(11, blocks, disp, types, &mpi::source_site); MPI_Type_commit(&mpi::source_site); } #endif // OPENMC_MPI @@ -371,6 +373,9 @@ bool read_model_xml() } } + // Read data from chain file + read_chain_file_xml(); + // Read materials and cross sections if (!check_for_node(root, "materials")) { fatal_error(fmt::format( @@ -423,6 +428,10 @@ void read_separate_xml_files() if (settings::run_mode != RunMode::PLOTTING) { read_cross_sections_xml(); } + + // Read data from chain file + read_chain_file_xml(); + read_materials_xml(); read_geometry_xml(); diff --git a/src/nuclide.cpp b/src/nuclide.cpp index d78b7a0101d..eaac2f756e7 100644 --- a/src/nuclide.cpp +++ b/src/nuclide.cpp @@ -248,7 +248,8 @@ Nuclide::Nuclide(hid_t group, const vector& temperature) for (auto name : group_names(rxs_group)) { if (starts_with(name, "reaction_")) { hid_t rx_group = open_group(rxs_group, name.c_str()); - reactions_.push_back(make_unique(rx_group, temps_to_read)); + reactions_.push_back( + make_unique(rx_group, temps_to_read, name_)); // Check for 0K elastic scattering const auto& rx = reactions_.back(); @@ -375,15 +376,15 @@ void Nuclide::create_derived( int j = rx->xs_[t].threshold; int n = rx->xs_[t].value.size(); auto xs = xt::adapt(rx->xs_[t].value); + auto pprod = xt::view(xs_[t], xt::range(j, j + n), XS_PHOTON_PROD); for (const auto& p : rx->products_) { if (p.particle_ == ParticleType::photon) { - auto pprod = xt::view(xs_[t], xt::range(j, j + n), XS_PHOTON_PROD); for (int k = 0; k < n; ++k) { double E = grid_[t].energy[k + j]; - // For fission, artificially increase the photon yield to account - // for delayed photons + // For fission, artificially increase the photon yield to + // account for delayed photons double f = 1.0; if (settings::delayed_photon_scaling) { if (is_fission(rx->mt_)) { @@ -469,8 +470,8 @@ void Nuclide::create_derived( } } } else { - // Otherwise, assume that any that have 0 K elastic scattering data are - // resonant + // Otherwise, assume that any that have 0 K elastic scattering data + // are resonant resonant_ = !energy_0K_.empty(); } @@ -781,8 +782,8 @@ void Nuclide::calculate_xs( } for (int j = 0; j < DEPLETION_RX.size(); ++j) { - // If reaction is present and energy is greater than threshold, set the - // reaction xs appropriately + // If reaction is present and energy is greater than threshold, set + // the reaction xs appropriately int i_rx = reaction_index_[DEPLETION_RX[j]]; if (i_rx >= 0) { const auto& rx = reactions_[i_rx]; @@ -819,9 +820,9 @@ void Nuclide::calculate_xs( // Initialize URR probability table treatment to false micro.use_ptable = false; - // If there is S(a,b) data for this nuclide, we need to set the sab_scatter - // and sab_elastic cross sections and correct the total and elastic cross - // sections. + // If there is S(a,b) data for this nuclide, we need to set the + // sab_scatter and sab_elastic cross sections and correct the total and + // elastic cross sections. if (i_sab >= 0) this->calculate_sab_xs(i_sab, sab_frac, p); @@ -984,8 +985,8 @@ void Nuclide::calculate_urr_xs(int i_temp, Particle& p) const } // Set elastic, absorption, fission, total, and capture x/s. Note that the - // total x/s is calculated as a sum of partials instead of the table-provided - // value + // total x/s is calculated as a sum of partials instead of the + // table-provided value micro.elastic = elastic; micro.absorption = capture + fission; micro.fission = fission; diff --git a/src/particle.cpp b/src/particle.cpp index 5b46e2e639b..c63c846741c 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -75,13 +75,13 @@ double Particle::speed() const } } -void Particle::create_secondary( +bool Particle::create_secondary( double wgt, Direction u, double E, ParticleType type) { // If energy is below cutoff for this particle, don't create secondary // particle if (E < settings::energy_cutoff[static_cast(type)]) { - return; + return false; } auto& bank = secondary_bank().emplace_back(); @@ -92,6 +92,7 @@ void Particle::create_secondary( bank.E = settings::run_CE ? E : g(); bank.time = time(); bank_second_E() += bank.E; + return true; } void Particle::split(double wgt) @@ -137,6 +138,7 @@ void Particle::from_source(const SourceSite* src) E_last() = E(); time() = src->time; time_last() = src->time; + parent_nuclide() = src->parent_nuclide; } void Particle::event_calculate_xs() diff --git a/src/physics.cpp b/src/physics.cpp index c324ce6b9cf..e04f54c0b13 100644 --- a/src/physics.cpp +++ b/src/physics.cpp @@ -2,6 +2,7 @@ #include "openmc/bank.h" #include "openmc/bremsstrahlung.h" +#include "openmc/chain.h" #include "openmc/constants.h" #include "openmc/distribution_multi.h" #include "openmc/eigenvalue.h" @@ -1145,9 +1146,21 @@ void sample_secondary_photons(Particle& p, int i_nuclide) // Sample the number of photons produced double y_t = p.neutron_xs(i_nuclide).photon_prod / p.neutron_xs(i_nuclide).total; - int y = static_cast(y_t); - if (prn(p.current_seed()) <= y_t - y) - ++y; + double photon_wgt = p.wgt(); + int y = 1; + + if (settings::use_decay_photons) { + // For decay photons, sample a single photon and modify the weight + if (y_t <= 0.0) + return; + photon_wgt *= y_t; + } else { + // For prompt photons, sample an integral number of photons with weight + // equal to the neutron's weight + y = static_cast(y_t); + if (prn(p.current_seed()) <= y_t - y) + ++y; + } // Sample each secondary photon for (int i = 0; i < y; ++i) { @@ -1170,15 +1183,19 @@ void sample_secondary_photons(Particle& p, int i_nuclide) // release and deposition. See D. P. Griesheimer, S. J. Douglass, and M. H. // Stedry, "Self-consistent energy normalization for quasistatic reactor // calculations", Proc. PHYSOR, Cambridge, UK, Mar 29-Apr 2, 2020. - double wgt; + double wgt = photon_wgt; if (settings::run_mode == RunMode::EIGENVALUE && !is_fission(rx->mt_)) { - wgt = simulation::keff * p.wgt(); - } else { - wgt = p.wgt(); + wgt *= simulation::keff; } // Create the secondary photon - p.create_secondary(wgt, u, E, ParticleType::photon); + bool created_photon = p.create_secondary(wgt, u, E, ParticleType::photon); + + // Tag secondary particle with parent nuclide + if (created_photon && settings::use_decay_photons) { + p.secondary_bank().back().parent_nuclide = + rx->products_[i_product].parent_nuclide_; + } } } diff --git a/src/reaction.cpp b/src/reaction.cpp index 60a14b2b3cc..97147343831 100644 --- a/src/reaction.cpp +++ b/src/reaction.cpp @@ -1,17 +1,20 @@ #include "openmc/reaction.h" +#include // for remove_if #include #include #include // for move #include +#include "openmc/chain.h" #include "openmc/constants.h" #include "openmc/endf.h" #include "openmc/hdf5_interface.h" #include "openmc/random_lcg.h" #include "openmc/search.h" #include "openmc/secondary_uncorrelated.h" +#include "openmc/settings.h" namespace openmc { @@ -19,7 +22,8 @@ namespace openmc { // Reaction implementation //============================================================================== -Reaction::Reaction(hid_t group, const vector& temperatures) +Reaction::Reaction( + hid_t group, const vector& temperatures, std::string name) { read_attribute(group, "Q_value", q_value_); read_attribute(group, "mt", mt_); @@ -63,6 +67,34 @@ Reaction::Reaction(hid_t group, const vector& temperatures) close_group(pgroup); } } + + if (settings::use_decay_photons) { + // Remove photon products for D1S method + products_.erase( + std::remove_if(products_.begin(), products_.end(), + [](const auto& p) { return p.particle_ == ParticleType::photon; }), + products_.end()); + + // Determine product for D1S method + auto nuclide_it = data::chain_nuclide_map.find(name); + if (nuclide_it != data::chain_nuclide_map.end()) { + const auto& chain_nuc = data::chain_nuclides[nuclide_it->second]; + const auto& rx_products = chain_nuc->reaction_products(); + auto product_it = rx_products.find(mt_); + if (product_it != rx_products.end()) { + auto decay_products = product_it->second; + for (const auto& decay_product : decay_products) { + auto product_it = data::chain_nuclide_map.find(decay_product.name); + if (product_it != data::chain_nuclide_map.end()) { + const auto& product_nuc = data::chain_nuclides[product_it->second]; + if (product_nuc->photon_energy()) { + products_.emplace_back(decay_product); + } + } + } + } + } + } } double Reaction::xs(int64_t i_temp, int64_t i_grid, double interp_factor) const diff --git a/src/reaction_product.cpp b/src/reaction_product.cpp index 4cef8d3a958..3ba2c0cfb05 100644 --- a/src/reaction_product.cpp +++ b/src/reaction_product.cpp @@ -83,6 +83,29 @@ ReactionProduct::ReactionProduct(hid_t group) } } +ReactionProduct::ReactionProduct(const ChainNuclide::Product& product) +{ + particle_ = ParticleType::photon; + emission_mode_ = EmissionMode::delayed; + + // Get chain nuclide object for radionuclide + parent_nuclide_ = data::chain_nuclide_map.at(product.name); + const auto& chain_nuc = data::chain_nuclides[parent_nuclide_].get(); + + // Determine decay constant in [s^-1] + decay_rate_ = chain_nuc->decay_constant(); + + // Determine number of photons per decay and set yield + double photon_per_sec = chain_nuc->photon_energy()->integral(); + double photon_per_decay = photon_per_sec / decay_rate_; + vector coef = {product.branching_ratio * photon_per_decay}; + yield_ = make_unique(coef); + + // Set decay photon angle-energy distribution + distribution_.push_back( + make_unique(chain_nuc->photon_energy())); +} + void ReactionProduct::sample( double E_in, double& E_out, double& mu, uint64_t* seed) const { diff --git a/src/settings.cpp b/src/settings.cpp index 60263c84653..e2f5a033b50 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -75,6 +75,7 @@ bool trigger_predict {false}; bool uniform_source_sampling {false}; bool ufs_on {false}; bool urr_ptables_on {true}; +bool use_decay_photons {false}; bool weight_windows_on {false}; bool weight_window_checkpoint_surface {false}; bool weight_window_checkpoint_collision {true}; @@ -1124,6 +1125,11 @@ void read_settings_xml(pugi::xml_node root) get_node_value_bool(ww_checkpoints, "surface"); } } + + if (check_for_node(root, "use_decay_photons")) { + settings::use_decay_photons = + get_node_value_bool(root, "use_decay_photons"); + } } void free_memory_settings() diff --git a/src/tallies/filter.cpp b/src/tallies/filter.cpp index 0ce1ee20b37..6430d9ae100 100644 --- a/src/tallies/filter.cpp +++ b/src/tallies/filter.cpp @@ -28,6 +28,7 @@ #include "openmc/tallies/filter_meshsurface.h" #include "openmc/tallies/filter_mu.h" #include "openmc/tallies/filter_musurface.h" +#include "openmc/tallies/filter_parent_nuclide.h" #include "openmc/tallies/filter_particle.h" #include "openmc/tallies/filter_polar.h" #include "openmc/tallies/filter_sph_harm.h" @@ -137,6 +138,8 @@ Filter* Filter::create(const std::string& type, int32_t id) return Filter::create(id); } else if (type == "musurface") { return Filter::create(id); + } else if (type == "parentnuclide") { + return Filter::create(id); } else if (type == "particle") { return Filter::create(id); } else if (type == "polar") { diff --git a/src/tallies/filter_parent_nuclide.cpp b/src/tallies/filter_parent_nuclide.cpp new file mode 100644 index 00000000000..d0494107529 --- /dev/null +++ b/src/tallies/filter_parent_nuclide.cpp @@ -0,0 +1,79 @@ +#include "openmc/tallies/filter_parent_nuclide.h" + +#include // for int64_t + +#include + +#include "openmc/capi.h" +#include "openmc/chain.h" +#include "openmc/search.h" +#include "openmc/settings.h" +#include "openmc/xml_interface.h" + +namespace openmc { + +//============================================================================== +// ParentNuclideFilter implementation +//============================================================================== + +void ParentNuclideFilter::from_xml(pugi::xml_node node) +{ + nuclides_ = get_node_array(node, "bins"); + + // Convert nuclides to indices in data::chain_nuclides + std::vector bins; + for (const auto& nuclide : nuclides_) { + auto it = data::chain_nuclide_map.find(nuclide); + if (it != data::chain_nuclide_map.end()) { + bins.push_back(it->second); + } else { + // The default value of parent_nuclide is -1, so to prevent a score to + // this bin assign the value -2. + bins.push_back(-2); + } + } + this->set_bins(bins); +} + +void ParentNuclideFilter::set_bins(span bins) +{ + // Clear existing bins + bins_.clear(); + bins_.reserve(bins.size()); + map_.clear(); + + // Set bins based on chain nuclide indexing + for (int64_t i = 0; i < bins.size(); ++i) { + bins_.push_back(bins[i]); + map_[bins[i]] = i; + } + + n_bins_ = bins_.size(); +} + +void ParentNuclideFilter::get_all_bins( + const Particle& p, TallyEstimator estimator, FilterMatch& match) const +{ + // Get the particle's parent nuclide + int parent_nuclide = p.parent_nuclide(); + + // Find bin matching parent nuclide + auto search = map_.find(parent_nuclide); + if (search != map_.end()) { + match.bins_.push_back(search->second); + match.weights_.push_back(1.0); + } +} + +void ParentNuclideFilter::to_statepoint(hid_t filter_group) const +{ + Filter::to_statepoint(filter_group); + write_dataset(filter_group, "bins", nuclides_); +} + +std::string ParentNuclideFilter::text_label(int bin) const +{ + return fmt::format("Parent Nuclide {}", nuclides_[bin]); +} + +} // namespace openmc diff --git a/tests/chain_ni.xml b/tests/chain_ni.xml new file mode 100644 index 00000000000..fc96d3d8b36 --- /dev/null +++ b/tests/chain_ni.xml @@ -0,0 +1,173 @@ + + + + + + + + + 557.6039 640.4896 655.72 678.8113 5847.93 5858.68 6448.81 6450.12 6499.05 6499.18 126000.0 4.281895544855688e-11 8.113092795419058e-12 2.881383486075155e-13 8.900165070824795e-12 6.680021392530635e-10 1.3098276096378766e-09 7.960187308411459e-11 1.5666266690172673e-10 2.8061406316612144e-14 4.106367573258232e-14 1.0245835494664239e-17 + + + 105210.0 231210.0 1.040592667426837e-17 8.004558980206438e-09 + + + 36.67648 65.20168 562.7812 610.8647 5155.619 5764.491 6377.571 2.257651440763611e-08 2.654244519541021e-09 1.1943434358627441e-08 2.0711244046714524e-10 3.863926291321637e-09 9.499586470057554e-10 5.918783030777615e-11 + + + + + + + + + + + + + + + + + + 780.0 6915.0 6930.0 7649.0 142651.0 189000.0 192343.0 334800.0 382000.0 1099245.0 1291590.0 1481700.0 1.0526870077721752e-12 1.0971242349369013e-11 2.1648606887732874e-11 3.9280951004469605e-12 1.8390802445841403e-09 1.622717862868359e-12 5.553301130705051e-09 4.868153588605077e-10 3.245435725736718e-11 1.018706213911803e-07 7.789045741768123e-08 1.0637817101025909e-10 + + + 750.0 6070.0 83569.95 130946.9 134942.1 141725.4 184634.1 191417.4 273599.0 327091.1 333874.4 465943.0 1091536.0 1283881.0 1565200.0 1.4515384373262843e-10 5.994999523918275e-11 1.4063554811525778e-10 2.3619560003972782e-09 2.703447959538686e-11 2.7218387619845277e-12 4.520385317374064e-11 4.498173915871091e-12 8.16767990977074e-08 8.519268780058884e-13 8.275861100628631e-14 9.574035390923319e-08 1.6299297619569002e-11 8.567950315944936e-12 3.245435725736718e-10 + + + + + + 120340.0 177610.0 297900.0 333000.0 349700.0 440500.0 542600.0 561400.0 603300.0 618400.0 657300.0 686000.0 696900.0 748100.0 769400.0 806300.0 925600.0 945400.0 978000.0 984100.0 989200.0 1027420.0 1097800.0 1205070.0 1275000.0 1285700.0 1381400.0 1403900.0 1538800.0 1618900.0 1645950.0 1659300.0 1837200.0 1879400.0 1889000.0 1899300.0 1972700.0 1999800.0 2011600.0 2177100.0 2230800.0 2484400.0 2754400.0 2920000.0 3191000.0 3204200.0 3239100.0 3364900.0 0.00010275887633317896 3.874515009283797e-05 0.00042956579450755135 4.2956579450755134e-06 3.116457724858706e-06 4.21142935791717e-06 1.263428807375151e-06 1.0949716330584643e-06 1.431885981691838e-06 1.802491765188549e-05 4.2956579450755134e-06 7.749030018567594e-06 2.1899432661169287e-06 1.558228862429353e-05 3.116457724858706e-06 3.7060578349671097e-06 6.569829798350785e-06 2.105714678958585e-06 1.431885981691838e-06 1.1792002202168078e-05 1.1792002202168078e-05 0.0008254401541517653 1.3476573945334946e-05 0.000842285871583434 1.1792002202168078e-05 7.159429908459189e-06 7.749030018567594e-06 2.2741718532752718e-06 5.306400990975635e-06 7.075201321300846e-06 0.00013476573945334945 1.4992688514185127e-05 2.6953147890669893e-06 5.053715229500604e-06 3.4533720734920796e-06 1.431885981691838e-06 1.1792002202168076e-06 2.526857614750302e-06 8.507087302992683e-05 4.042972183600483e-06 2.105714678958585e-06 2.3584004404336153e-06 1.4824231339868439e-05 1.3476573945334947e-06 1.6003431560085245e-06 8.422858715834341e-07 1.0949716330584643e-06 8.422858715834341e-07 + + + 613000.0 738800.1 773300.1 786899.9 873600.1 977699.9 1057900.0 1113610.0 1223500.0 1493500.0 1546600.0 1675000.0 1747100.0 1966500.0 2024800.0 2089000.0 2332110.0 2359120.0 2652590.0 2692270.0 2772900.0 2950510.0 3978000.0 7.969724371709087e-07 1.032441566335041e-06 1.1773456458206606e-05 1.5214928345990077e-06 1.267910695499173e-06 2.535821390998346e-05 1.267910695499173e-06 2.1735611922842964e-05 1.3947017650490904e-05 2.227900222091404e-06 2.173561192284297e-06 2.8980815897123955e-05 1.0867805961421485e-06 9.056504967851236e-05 1.7931879836345447e-05 4.709382583282643e-06 0.00014490407948561978 2.4452563413198337e-06 0.0004890512682639667 3.622601987140494e-07 0.0006882943775566939 0.00028980815897123956 9.056504967851236e-05 + + + + + + 619.9594 707.9362 726.02 741.741 6349.85 6362.71 7015.36 7016.95 7070.49 7070.66 510998.9 810759.2 863951.0 1674725.0 5.276288639112756e-10 1.3715809425619083e-10 3.759818598537813e-12 1.1767757246637665e-10 8.777123989590544e-09 1.719716697150835e-08 1.057271626846154e-09 2.0783510382283326e-09 5.201844116739253e-13 7.595270495013994e-13 3.374000922877054e-08 1.1259402850084962e-07 7.768987966558623e-10 5.854889482044179e-10 + + + 632864.1 1496834.0 2307600.0 1.3699223176071195e-09 1.1185811981783752e-07 8.491254034754873e-13 + + + 40.23492 70.00108 628.9213 675.1537 5583.843 6258.984 6938.274 803647.2 809913.1 810759.2 856839.0 863104.9 863951.0 1667613.0 1673879.0 1674725.0 2.3068291731266982e-07 2.4528685530196402e-08 1.3309519902564132e-07 1.9030055102434398e-09 4.481805021434518e-08 1.1035282426757231e-08 6.934054278993223e-10 3.366561452175404e-11 3.231448617974384e-12 4.640000480603615e-13 1.6314869068937087e-13 1.553797593311725e-14 2.2436836794554425e-15 3.3782714575729324e-14 3.2026247731116077e-15 4.614238015862169e-16 + + + + + + + + + + 751.0021 852.3381 876.89 883.6421 7417.82 7435.78 8222.319 8224.59 8287.88 8288.141 347140.0 826100.0 1173228.0 1332492.0 2158570.0 2505692.0 7.0614888325101086e-15 3.3564685966506987e-15 5.761418570090495e-17 2.3600648888691433e-15 1.3290046971181013e-13 2.5954235427719607e-13 1.6253028424014347e-14 3.186921843254948e-14 1.3932264913028456e-17 2.024904561736029e-17 3.125208966828774e-13 3.1668784197198245e-13 4.160694871171375e-09 4.166220240624728e-09 5.000334346926039e-14 8.333890578210064e-17 + + + 47.97734 78.78671 772.594 818.3802 6497.883 7313.626 8133.5 318151.8 1164895.0 1172220.0 1173228.0 1324159.0 1331484.0 1332492.0 1491392.0 2497359.0 2504684.0 2505692.0 2.430147074908485e-12 3.047352927658078e-13 1.6375715794161468e-12 1.653965175572544e-14 5.18885111875044e-13 1.2976030141139345e-13 8.558901456876446e-15 4.161944954758106e-09 6.241042306757062e-13 6.095415902793419e-14 8.94050113917305e-15 4.736991738545178e-13 4.616171159887578e-14 6.779692031857936e-15 5.000334346926038e-12 6.46709908869101e-21 6.3170890582832295e-22 9.678980517533168e-23 + + + + + + 751.028 852.3382 876.89 883.6886 7417.82 7435.78 8222.319 8224.59 8287.88 8288.141 67415.0 841700.0 909200.0 7.866502846971466e-08 3.756939402646081e-08 6.436568219422988e-10 2.637348005677196e-08 1.4671031864629482e-06 2.8654120987125446e-06 1.794509040062992e-07 3.5187803327436886e-07 1.538314140492697e-10 2.235774236842793e-10 9.880497991981766e-05 9.262966867482906e-07 4.219796017408879e-06 + + + 48.00067 78.83981 772.8435 818.5774 6498.158 7313.756 8133.501 59082.2 66406.9 67415.0 413499.9 833367.2 840691.9 841700.0 900867.2 908191.9 909200.0 1255285.0 2.6847340674754763e-05 3.3502043712197297e-06 1.814135374119958e-05 1.8444716489566865e-07 5.715307736044776e-06 1.430796977323619e-06 9.449890224831685e-08 1.2103610040177662e-05 1.246919266677299e-06 1.902588515965013e-07 5.134423559703298e-06 2.917834563257115e-10 2.862256762052218e-11 4.385088515066407e-12 9.241353278125446e-10 9.030363477255e-11 1.3801262380678854e-11 0.00011155702097900803 + + + + + + 1128900.0 1172900.0 1886300.0 1985000.0 2083000.0 2097000.0 2301800.0 2345900.0 3158200.0 3271000.0 3369500.0 3519000.0 4063100.0 0.0008553051126342747 0.006430865508528381 3.2154327542641905e-05 0.00012218644466203924 2.5723462034113525e-05 7.07395205938122e-05 0.0011318323295009952 0.0001028938481364541 6.430865508528381e-05 1.1254014639924668e-05 2.8938894788377717e-05 6.430865508528381e-06 2.5723462034113525e-05 + + + 1251800.0 1796100.0 1945400.0 2044800.0 2059000.0 2156900.0 2255700.0 3013160.0 4142070.0 5315000.0 2.5415396620531326e-05 0.00010782289475376927 2.9266214290308802e-05 9.318978760861486e-05 2.5415396620531326e-05 0.00018483924814931876 3.080654135821979e-05 0.0019870219176051766 0.005221708760218255 1.925408834888737e-05 + + + + + + 931100.0 1346100.0 0.11552453009332422 0.23104906018664845 + + + 5029800.0 5960900.0 7307000.0 0.11552453009332422 0.11552453009332422 2.079441541679836 + + + + + + 683.2094 778.5527 799.73 813.244 6873.16 6888.41 7606.53 7608.44 7666.77 7666.98 127164.0 161860.0 304100.0 379940.0 510998.9 541900.0 673440.0 696000.0 755300.0 906980.0 1046680.0 1224000.0 1279990.0 1350520.0 1377630.0 1603280.0 1730440.0 1757550.0 1897420.0 1919520.0 2133040.0 2730910.0 2804200.0 3177280.0 1.8638833690911883e-08 6.653293496572297e-09 1.4203876186268797e-10 5.409592966721624e-09 3.0832339673226584e-07 6.028687625463321e-07 3.74301586798566e-08 7.349918311628966e-08 2.4690572137876036e-11 3.5966442331457405e-11 9.014158418349326e-07 1.2284000197554473e-09 1.0604892256881559e-10 3.6233381877678664e-09 4.684800934777033e-06 1.9884172981652923e-10 2.6556417693274235e-09 4.860575617737382e-11 2.916345370642429e-10 3.3140288302754875e-09 7.246676375535733e-09 3.402402932416167e-09 5.2140720263001e-10 1.0604892256881559e-10 4.418705107033983e-06 2.1209784513763118e-10 2.827971268501749e-09 3.110768395351924e-07 1.502359736391554e-09 6.628057660550975e-07 1.546546787461894e-09 1.0737453410092578e-09 5.30244612844078e-09 6.009438945566218e-10 + + + 84790.04 154079.8 457929.9 531290.1 1129120.0 1342650.0 1504620.0 1757390.0 1884550.0 1.124957970946228e-09 3.2450710700371965e-09 1.5738594689680403e-08 1.0762819048956704e-09 1.8388736063544118e-09 6.652395693576253e-07 3.0611837094017556e-07 9.194368031772059e-07 3.488451400289987e-06 + + + 43.92123 75.12743 700.3887 749.9867 6033.611 6776.5 7523.621 119455.1 126238.4 127164.0 154151.1 160934.4 161860.0 665731.1 672514.4 673440.0 747591.1 754374.4 755300.0 1216291.0 1223074.0 1224000.0 1369921.0 1376704.0 1377630.0 1749841.0 1756624.0 1757550.0 1889711.0 1896494.0 1897420.0 1911811.0 1918594.0 1919520.0 2125331.0 2132114.0 2133040.0 7.252641897852818e-06 9.885065183674125e-07 4.453862207006767e-06 5.575973168934215e-08 1.3531437967587472e-06 3.3801747364315905e-07 2.1887831296943824e-08 1.739732899248527e-08 1.7397323584033486e-09 2.636641229198142e-10 1.272622225762379e-11 1.2627950688719498e-12 1.9160584871075314e-13 9.58686478614484e-13 9.268187990163621e-14 1.4090839543995835e-14 8.632381215411233e-14 8.34074667834699e-15 1.2155339619769639e-15 3.6814000810433283e-13 3.538499049712814e-14 5.158725716665081e-15 4.21544467211042e-10 4.0607899933642303e-11 2.1687004665322787e-10 1.822910020070542e-11 1.7482517516525527e-12 6.153697369032366e-11 7.181277376570916e-14 6.8808054292926055e-15 3.195035376537882e-13 3.115187100458958e-11 2.9826259472479385e-12 1.482406852545487e-10 6.031532471101388e-14 5.768616813006974e-15 4.833646927113489e-13 + + + + + + + + + + + + 1072500.0 2.890064045563861e-13 + + + 44.06754 75.28693 701.5928 750.7433 6035.851 6777.615 7523.68 6.689278658453111e-13 8.913674252402247e-14 4.1657845563004787e-13 5.378516121164032e-15 1.2583954438026756e-13 3.16650156158609e-14 2.0701132819599697e-15 + + + 683.3271 778.5529 799.73 813.5109 6873.16 6888.41 7606.53 7608.44 7666.77 7666.98 510998.9 1.7758787515451584e-15 6.581346326607483e-16 1.368589539770362e-17 5.248596182059754e-16 2.920762305855841e-14 5.711006429349971e-14 3.5457762265416177e-15 6.962612245690403e-15 2.338949640535961e-18 3.4071167337635547e-18 2.1386473937172572e-19 + + + + + + + + + + + + + + + + + + + + + + + + + + + + 66945.0 2.1704054025041888e-10 + + + + + + + + + + + + 820.0389 927.6652 951.42 959.5527 7984.67 8005.709 8862.7 8865.34 8933.11 8933.4 366270.0 507900.0 609500.0 770600.0 852700.0 954500.0 1115530.0 1481840.0 1623420.0 1724920.0 6.930967733828076e-11 4.4848940639261224e-11 6.225110448569713e-13 3.0091057755445835e-11 1.4120610720530457e-09 2.7533685191404717e-09 1.7112326402414573e-10 3.3482649978514024e-10 1.9318959861680173e-13 2.7929997279698636e-13 3.6755804457910027e-06 2.2374667416695354e-07 1.181887674027053e-07 7.92135402897521e-08 7.398075516810562e-08 1.3533064969775417e-09 1.1800832653644161e-05 1.804408662636722e-05 3.807302278163484e-07 3.0494506398560603e-07 + + + 56.92597 90.91714 855.942 897.216 6991.356 7880.445 8773.007 357291.1 365173.4 366270.0 412919.9 498921.1 506803.4 507900.0 514460.0 600521.1 608403.4 609500.0 656069.9 761621.1 769503.4 770600.0 1022350.0 1106551.0 1114434.0 1115530.0 1472861.0 1480743.0 1481840.0 1614441.0 1622324.0 1623420.0 1715941.0 1723824.0 1724920.0 2137900.0 2.167548391930537e-08 2.8616659297729836e-09 1.5876524466163717e-08 5.919283450210211e-11 4.8238674145478966e-09 1.1910611683428657e-09 8.251524863747208e-11 6.6528004309537836e-09 6.652800430953784e-10 9.941708721258623e-11 4.2452174979371803e-07 1.968970885650002e-10 1.9600207433178643e-11 2.840463936760989e-12 6.807646077773136e-07 6.748578656939676e-11 6.701302996997783e-12 9.996408692926368e-13 2.172327512457944e-05 2.732867560693677e-11 2.7011821751739386e-12 4.0351373644973695e-13 7.786723266486576e-06 1.953037934211798e-09 1.9247158318161006e-10 3.950919133474779e-11 1.818843931937816e-09 1.7899733933356285e-10 1.4705028396157966e-09 3.076300301948419e-11 3.022997962967563e-12 4.574485034565062e-11 2.213900812679635e-11 2.1742581302894385e-12 5.0020380532292804e-11 4.589424322094249e-05 + + + diff --git a/tests/regression_tests/surface_source/test.py b/tests/regression_tests/surface_source/test.py index 251e9e9ad4a..83b78e61deb 100644 --- a/tests/regression_tests/surface_source/test.py +++ b/tests/regression_tests/surface_source/test.py @@ -10,6 +10,17 @@ from tests.regression_tests import config +def assert_structured_arrays_close(arr1, arr2, rtol=1e-5, atol=1e-8): + assert arr1.dtype == arr2.dtype + + for field in arr1.dtype.names: + data1, data2 = arr1[field], arr2[field] + if data1.dtype.names: + assert_structured_arrays_close(data1, data2, rtol=rtol, atol=atol) + else: + np.testing.assert_allclose(data1, data2, rtol=rtol, atol=atol) + + @pytest.fixture def model(request): openmc.reset_auto_ids() @@ -76,16 +87,10 @@ def _compare_output(self): """Make sure the current surface_source.h5 agree with the reference.""" if self._model.settings.surf_source_write: with h5py.File("surface_source_true.h5", 'r') as f: - source_true = f['source_bank'][()] - # Convert dtye from mixed to a float for comparison assertion - source_true.dtype = 'float64' + source_true = np.sort(f['source_bank'][()]) with h5py.File("surface_source.h5", 'r') as f: - source_test = f['source_bank'][()] - # Convert dtye from mixed to a float for comparison assertion - source_test.dtype = 'float64' - np.testing.assert_allclose(np.sort(source_true), - np.sort(source_test), - atol=1e-07) + source_test = np.sort(f['source_bank'][()]) + assert_structured_arrays_close(source_true, source_test, atol=1e-07) def execute_test(self): """Build input XMLs, run OpenMC, check output and results.""" @@ -132,4 +137,4 @@ def test_surface_source_read(model): harness = SurfaceSourceTestHarness('statepoint.10.h5', model, 'inputs_true_read.dat') - harness.main() \ No newline at end of file + harness.main() diff --git a/tests/unit_tests/test_d1s.py b/tests/unit_tests/test_d1s.py new file mode 100644 index 00000000000..7c92dd6d9b8 --- /dev/null +++ b/tests/unit_tests/test_d1s.py @@ -0,0 +1,132 @@ +from pathlib import Path +from math import exp + +import numpy as np +import pytest +import openmc +import openmc.deplete +from openmc.deplete import d1s + + +CHAIN_PATH = Path(__file__).parents[1] / "chain_ni.xml" + + +@pytest.fixture +def model(): + """Simple model with natural Ni""" + mat = openmc.Material() + mat.add_element('Ni', 1.0) + geom = openmc.Geometry([openmc.Cell(fill=mat)]) + return openmc.Model(geometry=geom) + + +def test_get_radionuclides(model): + # Check that radionuclides are correct and are unstable + nuclides = d1s.get_radionuclides(model, CHAIN_PATH) + assert sorted(nuclides) == [ + 'Co58', 'Co60', 'Co61', 'Co62', 'Co64', + 'Fe55', 'Fe59', 'Fe61', 'Ni57', 'Ni59', 'Ni63', 'Ni65' + ] + for nuc in nuclides: + assert openmc.data.half_life(nuc) is not None + + +@pytest.mark.parametrize("nuclide", ['Co60', 'Ni63', 'H3', 'Na24', 'K40']) +def test_time_correction_factors(nuclide): + # Irradiation schedule turning unit neutron source on and off + timesteps = [1.0, 1.0, 1.0] + source_rates = [1.0, 0.0, 1.0] + + # Compute expected solution + decay_rate = openmc.data.decay_constant(nuclide) + g = exp(-decay_rate) + expected = [0.0, (1 - g), (1 - g)*g, (1 - g)*(1 + g*g)] + + # Test against expected solution + tcf = d1s.time_correction_factors([nuclide], timesteps, source_rates) + assert tcf[nuclide] == pytest.approx(expected) + + # Make sure all values at first timestep and onward are positive (K40 case + # has very small decay constant that stresses this) + assert np.all(tcf[nuclide][1:] > 0.0) + + # Timesteps as a tuple + timesteps = [(1.0, 's'), (1.0, 's'), (1.0, 's')] + tcf = d1s.time_correction_factors([nuclide], timesteps, source_rates) + assert tcf[nuclide] == pytest.approx(expected) + + # Test changing units + timesteps = [1.0/60.0, 1.0/60.0, 1.0/60.0] + tcf = d1s.time_correction_factors([nuclide], timesteps, source_rates, + timestep_units='min') + assert tcf[nuclide] == pytest.approx(expected) + + +def test_prepare_tallies(model): + tally = openmc.Tally() + tally.filters = [openmc.ParticleFilter('photon')] + tally.scores = ['flux'] + model.tallies = [tally] + + # Check that prepare_tallies adds a ParentNuclideFilter + nuclides = ['Co58', 'Co60', 'Fe55'] + d1s.prepare_tallies(model, nuclides, chain_file=CHAIN_PATH) + assert tally.contains_filter(openmc.ParentNuclideFilter) + assert list(tally.filters[-1].bins) == nuclides + + # Get rid of parent nuclide filter + tally.filters.pop() + + # With no nuclides specified, filter should use get_radionuclides + radionuclides = d1s.get_radionuclides(model, CHAIN_PATH) + d1s.prepare_tallies(model, chain_file=CHAIN_PATH) + assert tally.contains_filter(openmc.ParentNuclideFilter) + assert sorted(tally.filters[-1].bins) == sorted(radionuclides) + + +def test_apply_time_correction(run_in_tmpdir): + # Make simple sphere model with elemental Ni + mat = openmc.Material() + mat.add_element('Ni', 1.0) + sphere = openmc.Sphere(r=10.0, boundary_type='vacuum') + cell = openmc.Cell(fill=mat, region=-sphere) + model = openmc.Model() + model.geometry = openmc.Geometry([cell]) + model.settings.run_mode = 'fixed source' + model.settings.batches = 3 + model.settings.particles = 10 + model.settings.photon_transport = True + model.settings.use_decay_photons = True + particle_filter = openmc.ParticleFilter('photon') + tally = openmc.Tally() + tally.filters = [particle_filter] + tally.scores = ['flux'] + model.tallies = [tally] + + # Prepare tallies for D1S and compute time correction factors + nuclides = d1s.prepare_tallies(model, chain_file=CHAIN_PATH) + factors = d1s.time_correction_factors(nuclides, [1.0e10], [1.0]) + + # Run OpenMC and get tally result + with openmc.config.patch('chain_file', CHAIN_PATH): + output_path = model.run() + with openmc.StatePoint(output_path) as sp: + tally = sp.tallies[tally.id] + flux = tally.mean.flatten() + + # Apply TCF and make sure results are consistent + result = d1s.apply_time_correction(tally, factors, sum_nuclides=False) + tcf = np.array([factors[nuc][-1] for nuc in nuclides]) + assert result.mean.flatten() == pytest.approx(tcf * flux) + + # Make sure summed results match a manual sum + result_summed = d1s.apply_time_correction(tally, factors) + assert result_summed.mean.flatten()[0] == pytest.approx(result.mean.sum()) + + # Make sure various tally methods work + result.get_values() + result_summed.get_values() + result.get_reshaped_data() + result_summed.get_reshaped_data() + result.get_pandas_dataframe() + result_summed.get_pandas_dataframe() From 6ae2001400c19a62c2a9e33a26dc6757818d61ff Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Fri, 21 Feb 2025 14:58:37 -0600 Subject: [PATCH 159/184] Enable overlap plotting from Python API (#3310) Co-authored-by: Paul Romano --- openmc/model/model.py | 5 +++++ openmc/plots.py | 13 +++++++++---- tests/unit_tests/test_model.py | 11 +++++++++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/openmc/model/model.py b/openmc/model/model.py index c7f358ce8e9..e8558f4ad05 100644 --- a/openmc/model/model.py +++ b/openmc/model/model.py @@ -863,6 +863,8 @@ def plot( legend: bool = False, axis_units: str = 'cm', outline: bool | str = False, + show_overlaps: bool = False, + overlap_color: Sequence[int] | str | None = None, n_samples: int | None = None, plane_tolerance: float = 1., legend_kwargs: dict | None = None, @@ -941,6 +943,9 @@ def plot( plot.pixels = pixels plot.basis = basis plot.color_by = color_by + plot.show_overlaps = show_overlaps + if overlap_color is not None: + plot.overlap_color = overlap_color if colors is not None: plot.colors = colors self.plots.append(plot) diff --git a/openmc/plots.py b/openmc/plots.py index 24607659844..9c1e31575e5 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -209,16 +209,21 @@ legend : bool Whether a legend showing material or cell names should be drawn + .. versionadded:: 0.14.0 + axis_units : {'km', 'm', 'cm', 'mm'} + Units used on the plot axis + .. versionadded:: 0.14.0 outline : bool or str Whether outlines between color boundaries should be drawn. If set to 'only', only outlines will be drawn. .. versionadded:: 0.14.0 - axis_units : {'km', 'm', 'cm', 'mm'} - Units used on the plot axis - - .. versionadded:: 0.14.0 + show_overlaps: bool + Indicate whether or not overlapping regions are shown. + Default is False. + overlap_color: Iterable of int or str + Color to apply to overlapping regions. Default is red. n_samples : int, optional The number of source particles to sample and add to plot. Defaults to None which doesn't plot any particles on the plot. diff --git a/tests/unit_tests/test_model.py b/tests/unit_tests/test_model.py index 4d7d5be2bca..ee5d8895ca3 100644 --- a/tests/unit_tests/test_model.py +++ b/tests/unit_tests/test_model.py @@ -638,3 +638,14 @@ def test_model_plot(): plot = model.plot(n_samples=1, plane_tolerance=0.1, basis="xy") coords = plot.axes.collections[0].get_offsets().data.flatten() assert (coords == np.array([])).all() + + # modify model to include another cell that overlaps the original cell entirely + model.geometry.root_universe.add_cell(openmc.Cell(region=-surface)) + axes = model.plot(show_overlaps=True) + white = np.array((1.0, 1.0, 1.0)) + red = np.array((1.0, 0.0, 0.0)) + axes_image = axes.get_images()[0] + image_data = axes_image.get_array() + # ensure that all of the data in the image data is either white or red + test_mask = (image_data == white) | (image_data == red) + assert np.all(test_mask), "Colors other than white or red found in overlap plot image" From fefe825e65a46d67f39a985d4b40ec7828d00711 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 21 Feb 2025 17:44:09 -0600 Subject: [PATCH 160/184] Handle reflex angles in CylinderSector (#3303) --- openmc/model/surface_composite.py | 18 ++++++++++-------- tests/unit_tests/test_surface_composite.py | 13 +++++++++++++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/openmc/model/surface_composite.py b/openmc/model/surface_composite.py index 3a17b616965..5962897de5c 100644 --- a/openmc/model/surface_composite.py +++ b/openmc/model/surface_composite.py @@ -135,6 +135,11 @@ def __init__(self, if theta2 <= theta1: raise ValueError('theta2 must be greater than theta1.') + # Determine whether the angle between theta1 and theta2 is a reflex + # angle, in which case we need to use a union between the planar + # half-spaces + self._reflex = (theta2 - theta1 > 180.0) + phi1 = pi / 180 * theta1 phi2 = pi / 180 * theta2 @@ -169,6 +174,9 @@ def __init__(self, **kwargs) self.plane2 = openmc.Plane.from_points(p1, p2_plane2, p3_plane2, **kwargs) + if axis == 'y': + self.plane1.flip_normal() + self.plane2.flip_normal() @classmethod def from_theta_alpha(cls, @@ -223,17 +231,11 @@ def from_theta_alpha(cls, return cls(r1, r2, theta1, theta2, center=center, axis=axis, **kwargs) def __neg__(self): - if isinstance(self.inner_cyl, openmc.YCylinder): - return -self.outer_cyl & +self.inner_cyl & +self.plane1 & -self.plane2 + if self._reflex: + return -self.outer_cyl & +self.inner_cyl & (-self.plane1 | +self.plane2) else: return -self.outer_cyl & +self.inner_cyl & -self.plane1 & +self.plane2 - def __pos__(self): - if isinstance(self.inner_cyl, openmc.YCylinder): - return +self.outer_cyl | -self.inner_cyl | -self.plane1 | +self.plane2 - else: - return +self.outer_cyl | -self.inner_cyl | +self.plane1 | -self.plane2 - class IsogonalOctagon(CompositeSurface): r"""Infinite isogonal octagon composite surface diff --git a/tests/unit_tests/test_surface_composite.py b/tests/unit_tests/test_surface_composite.py index da7ffbcc470..a04fc73c59e 100644 --- a/tests/unit_tests/test_surface_composite.py +++ b/tests/unit_tests/test_surface_composite.py @@ -207,6 +207,19 @@ def test_cylinder_sector(axis, indices, center): assert point_neg[indices] in -s assert point_neg[indices] not in +s + # Check __contains__ for sector with reflex angle + s_reflex = openmc.model.CylinderSector( + r1, r2, 0., 270., center=center, axis=axis.lower()) + points = [ + np.array([c1 + r1 + d, c2 + 0.01, 0.]), + np.array([c1, c2 + r1 + d, 0.]), + np.array([c1 - r1 - d, c2, 0.]), + np.array([c1 - 0.01, c2 - r1 - d, 0.]) + ] + for point_neg in points: + assert point_neg[indices] in -s_reflex + assert point_neg[indices] not in +s_reflex + # translate method t = uniform(-5.0, 5.0) s_t = s.translate((t, t, t)) From a2a5c2af19c21094215aaaaba2b66924a5fe7bd7 Mon Sep 17 00:00:00 2001 From: Ahnaf Tahmid Chowdhury Date: Sat, 22 Feb 2025 05:48:11 +0600 Subject: [PATCH 161/184] Add Versioning Support from `version.txt` (#3140) Co-authored-by: Jonathan Shimwell Co-authored-by: Paul Wilson Co-authored-by: Paul Romano --- .git_archival.txt | 3 + .gitattributes | 1 + .github/workflows/ci.yml | 5 +- CMakeLists.txt | 34 +++----- cmake/Modules/GetVersionFromGit.cmake | 109 ++++++++++++++++++++++++++ docs/source/conf.py | 8 +- include/openmc/version.h.in | 6 +- pyproject.toml | 6 +- src/mcpl_interface.cpp | 4 +- src/output.cpp | 21 +++-- 10 files changed, 153 insertions(+), 44 deletions(-) create mode 100644 .git_archival.txt create mode 100644 .gitattributes create mode 100644 cmake/Modules/GetVersionFromGit.cmake diff --git a/.git_archival.txt b/.git_archival.txt new file mode 100644 index 00000000000..f2f118c0d67 --- /dev/null +++ b/.git_archival.txt @@ -0,0 +1,3 @@ +commit: $Format:%H$ +commit-date: $Format:%cI$ +describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$ \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000000..82bf71c1c5b --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +.git_archival.txt export-subst \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de99bbbe20a..7fe3e1557b6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,9 +87,10 @@ jobs: RDMAV_FORK_SAFE: 1 steps: - - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 with: - fetch-depth: 2 + fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 diff --git a/CMakeLists.txt b/CMakeLists.txt index f76fcb247d9..385ce85aaaf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,11 +1,18 @@ cmake_minimum_required(VERSION 3.16 FATAL_ERROR) project(openmc C CXX) -# Set version numbers -set(OPENMC_VERSION_MAJOR 0) -set(OPENMC_VERSION_MINOR 15) -set(OPENMC_VERSION_RELEASE 1) -set(OPENMC_VERSION ${OPENMC_VERSION_MAJOR}.${OPENMC_VERSION_MINOR}.${OPENMC_VERSION_RELEASE}) +# Set module path +set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/Modules) + +include(GetVersionFromGit) + +# Output version information +message(STATUS "OpenMC version: ${OPENMC_VERSION}") +message(STATUS "OpenMC dev state: ${OPENMC_DEV_STATE}") +message(STATUS "OpenMC commit hash: ${OPENMC_COMMIT_HASH}") +message(STATUS "OpenMC commit count: ${OPENMC_COMMIT_COUNT}") + +# Generate version.h configure_file(include/openmc/version.h.in "${CMAKE_BINARY_DIR}/include/openmc/version.h" @ONLY) # Setup output directories @@ -13,9 +20,6 @@ set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) -# Set module path -set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/Modules) - # Enable correct usage of CXX_EXTENSIONS if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.22) cmake_policy(SET CMP0128 NEW) @@ -240,8 +244,6 @@ endif() #=============================================================================== # Update git submodules as needed #=============================================================================== - -find_package(Git) if(GIT_FOUND AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.git") option(GIT_SUBMODULE "Check submodules during build" ON) if(GIT_SUBMODULE) @@ -493,18 +495,6 @@ if (OPENMC_USE_MPI) target_compile_definitions(libopenmc PUBLIC -DOPENMC_MPI) endif() -# Set git SHA1 hash as a compile definition -if(GIT_FOUND) - execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse HEAD - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - RESULT_VARIABLE GIT_SHA1_SUCCESS - OUTPUT_VARIABLE GIT_SHA1 - ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) - if(GIT_SHA1_SUCCESS EQUAL 0) - target_compile_definitions(libopenmc PRIVATE -DGIT_SHA1="${GIT_SHA1}") - endif() -endif() - # target_link_libraries treats any arguments starting with - but not -l as # linker flags. Thus, we can pass both linker flags and libraries together. target_link_libraries(libopenmc ${ldflags} ${HDF5_LIBRARIES} ${HDF5_HL_LIBRARIES} diff --git a/cmake/Modules/GetVersionFromGit.cmake b/cmake/Modules/GetVersionFromGit.cmake new file mode 100644 index 00000000000..ee6bac827e3 --- /dev/null +++ b/cmake/Modules/GetVersionFromGit.cmake @@ -0,0 +1,109 @@ +# GetVersionFromGit.cmake +# Standalone script to retrieve versioning information from Git or .git_archival.txt. +# Customizable for any project by setting variables before including this file. + +# Configurable variables: +# - VERSION_PREFIX: Prefix for version tags (default: "v"). +# - VERSION_SUFFIX: Suffix for version tags (default: "[~+-]([a-zA-Z0-9]+)"). +# - VERSION_REGEX: Regex to extract version (default: "(?[0-9]+\\.[0-9]+\\.[0-9]+)"). +# - ARCHIVAL_FILE: Path to .git_archival.txt (default: "${CMAKE_SOURCE_DIR}/.git_archival.txt"). +# - DESCRIBE_NAME_KEY: Key for describe name in .git_archival.txt (default: "describe-name: "). +# - COMMIT_HASH_KEY: Key for commit hash in .git_archival.txt (default: "commit: "). + +# Default Format Example: +# 1.2.3 v1.2.3 v1.2.3-rc1 + +set(VERSION_PREFIX "v" CACHE STRING "Prefix used in version tags") +set(VERSION_SUFFIX "[~+-]([a-zA-Z0-9]+)" CACHE STRING "Suffix used in version tags") +set(VERSION_REGEX "?([0-9]+\\.[0-9]+\\.[0-9]+)" CACHE STRING "Regex for extracting version") +set(ARCHIVAL_FILE "${CMAKE_SOURCE_DIR}/.git_archival.txt" CACHE STRING "Path to .git_archival.txt") +set(DESCRIBE_NAME_KEY "describe-name: " CACHE STRING "Key for describe name in .git_archival.txt") +set(COMMIT_HASH_KEY "commit: " CACHE STRING "Key for commit hash in .git_archival.txt") + + +# Combine prefix and regex +set(VERSION_REGEX_WITH_PREFIX "^${VERSION_PREFIX}${VERSION_REGEX}") + +# Find Git +find_package(Git) + +# Attempt to retrieve version from Git +if(EXISTS "${CMAKE_SOURCE_DIR}/.git" AND GIT_FOUND) + message(STATUS "Using git describe for versioning") + + # Extract the version string + execute_process( + COMMAND git describe --tags --dirty + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE VERSION_STRING + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + + # Extract the commit hash + execute_process( + COMMAND git rev-parse HEAD + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE COMMIT_HASH + OUTPUT_STRIP_TRAILING_WHITESPACE + ) +else() + message(STATUS "Using archival file for versioning: ${ARCHIVAL_FILE}") + if(EXISTS "${ARCHIVAL_FILE}") + file(READ "${ARCHIVAL_FILE}" ARCHIVAL_CONTENT) + + # Extract the describe-name line + string(REGEX MATCH "${DESCRIBE_NAME_KEY}([^\\n]+)" VERSION_STRING "${ARCHIVAL_CONTENT}") + if(VERSION_STRING MATCHES "${DESCRIBE_NAME_KEY}(.*)") + set(VERSION_STRING "${CMAKE_MATCH_1}") + else() + message(FATAL_ERROR "Could not extract version from ${ARCHIVAL_FILE}") + endif() + + # Extract the commit hash + string(REGEX MATCH "${COMMIT_HASH_KEY}([a-f0-9]+)" COMMIT_HASH "${ARCHIVAL_CONTENT}") + if(COMMIT_HASH MATCHES "${COMMIT_HASH_KEY}([a-f0-9]+)") + set(COMMIT_HASH "${CMAKE_MATCH_1}") + else() + message(FATAL_ERROR "Could not extract commit hash from ${ARCHIVAL_FILE}") + endif() + else() + message(FATAL_ERROR "Neither git describe nor ${ARCHIVAL_FILE} is available for versioning.") + endif() +endif() + +# Ensure version string format +if(VERSION_STRING MATCHES "${VERSION_REGEX_WITH_PREFIX}") + set(VERSION_NO_SUFFIX "${CMAKE_MATCH_1}") +else() + message(FATAL_ERROR "Invalid version format: Missing base version in ${VERSION_STRING}") +endif() + +# Check for development state +if(VERSION_STRING MATCHES "-([0-9]+)-g([0-9a-f]+)") + set(DEV_STATE "true") + set(COMMIT_COUNT "${CMAKE_MATCH_1}") + string(REGEX REPLACE "-([0-9]+)-g([0-9a-f]+)" "" VERSION_WITHOUT_META "${VERSION_STRING}") +else() + set(DEV_STATE "false") + set(VERSION_WITHOUT_META "${VERSION_STRING}") +endif() + +# Split and set version components +string(REPLACE "." ";" VERSION_LIST "${VERSION_NO_SUFFIX}") +list(GET VERSION_LIST 0 VERSION_MAJOR) +list(GET VERSION_LIST 1 VERSION_MINOR) +list(GET VERSION_LIST 2 VERSION_PATCH) + +# Increment patch number for dev versions +if(DEV_STATE) + math(EXPR VERSION_PATCH "${VERSION_PATCH} + 1") +endif() + +# Export variables +set(OPENMC_VERSION_MAJOR "${VERSION_MAJOR}") +set(OPENMC_VERSION_MINOR "${VERSION_MINOR}") +set(OPENMC_VERSION_PATCH "${VERSION_PATCH}") +set(OPENMC_VERSION "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}") +set(OPENMC_COMMIT_HASH "${COMMIT_HASH}") +set(OPENMC_DEV_STATE "${DEV_STATE}") +set(OPENMC_COMMIT_COUNT "${COMMIT_COUNT}") diff --git a/docs/source/conf.py b/docs/source/conf.py index 60bd406fba5..b9db835077b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -68,10 +68,14 @@ # |version| and |release|, also used in various other places throughout the # built documents. # + +import openmc + # The short X.Y version. -version = "0.15" +version = ".".join(openmc.__version__.split('.')[:2]) + # The full version, including alpha/beta/rc tags. -release = "0.15.1-dev" +release = openmc.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/include/openmc/version.h.in b/include/openmc/version.h.in index e1c2b0541a5..6dfc7c7dd67 100644 --- a/include/openmc/version.h.in +++ b/include/openmc/version.h.in @@ -9,8 +9,10 @@ namespace openmc { // clang-format off constexpr int VERSION_MAJOR {@OPENMC_VERSION_MAJOR@}; constexpr int VERSION_MINOR {@OPENMC_VERSION_MINOR@}; -constexpr int VERSION_RELEASE {@OPENMC_VERSION_RELEASE@}; -constexpr bool VERSION_DEV {true}; +constexpr int VERSION_RELEASE {@OPENMC_VERSION_PATCH@}; +constexpr bool VERSION_DEV {@OPENMC_DEV_STATE@}; +constexpr const char* VERSION_COMMIT_COUNT = "@OPENMC_COMMIT_COUNT@"; +constexpr const char* VERSION_COMMIT_HASH = "@OPENMC_COMMIT_HASH@"; constexpr std::array VERSION {VERSION_MAJOR, VERSION_MINOR, VERSION_RELEASE}; // clang-format on diff --git a/pyproject.toml b/pyproject.toml index 1f8de10e3b3..6e8ed798e78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools", "wheel"] +requires = ["setuptools", "setuptools-scm", "wheel"] build-backend = "setuptools.build_meta" [project] @@ -8,7 +8,7 @@ authors = [ {name = "The OpenMC Development Team", email = "openmc@anl.gov"}, ] description = "OpenMC" -version = "0.15.1-dev" +dynamic = ["version"] requires-python = ">=3.11" license = {file = "LICENSE"} classifiers = [ @@ -66,3 +66,5 @@ exclude = ['tests*'] "openmc.data.effective_dose" = ["**/*.txt"] "openmc.data" = ["*.txt", "*.DAT", "*.json", "*.h5"] "openmc.lib" = ["libopenmc.dylib", "libopenmc.so"] + +[tool.setuptools_scm] diff --git a/src/mcpl_interface.cpp b/src/mcpl_interface.cpp index a8de5fdb0e0..83ef6332097 100644 --- a/src/mcpl_interface.cpp +++ b/src/mcpl_interface.cpp @@ -207,8 +207,8 @@ void write_mcpl_source_point(const char* filename, span source_bank, if (mpi::master) { file_id = mcpl_create_outfile(filename_.c_str()); if (VERSION_DEV) { - line = fmt::format("OpenMC {0}.{1}.{2}-development", VERSION_MAJOR, - VERSION_MINOR, VERSION_RELEASE); + line = fmt::format("OpenMC {0}.{1}.{2}-dev{3}", VERSION_MAJOR, + VERSION_MINOR, VERSION_RELEASE, VERSION_COMMIT_COUNT); } else { line = fmt::format( "OpenMC {0}.{1}.{2}", VERSION_MAJOR, VERSION_MINOR, VERSION_RELEASE); diff --git a/src/output.cpp b/src/output.cpp index 8494450c409..63e7fe55331 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -75,13 +75,12 @@ void title() // Write version information fmt::print( " | The OpenMC Monte Carlo Code\n" - " Copyright | 2011-2024 MIT, UChicago Argonne LLC, and contributors\n" + " Copyright | 2011-2025 MIT, UChicago Argonne LLC, and contributors\n" " License | https://docs.openmc.org/en/latest/license.html\n" - " Version | {}.{}.{}{}\n", - VERSION_MAJOR, VERSION_MINOR, VERSION_RELEASE, VERSION_DEV ? "-dev" : ""); -#ifdef GIT_SHA1 - fmt::print(" Git SHA1 | {}\n", GIT_SHA1); -#endif + " Version | {}.{}.{}{}{}\n", + VERSION_MAJOR, VERSION_MINOR, VERSION_RELEASE, VERSION_DEV ? "-dev" : "", + VERSION_COMMIT_COUNT); + fmt::print(" Commit Hash | {}\n", VERSION_COMMIT_HASH); // Write the date and time fmt::print(" Date/Time | {}\n", time_stamp()); @@ -291,12 +290,10 @@ void print_usage() void print_version() { if (mpi::master) { - fmt::print("OpenMC version {}.{}.{}\n", VERSION_MAJOR, VERSION_MINOR, - VERSION_RELEASE); -#ifdef GIT_SHA1 - fmt::print("Git SHA1: {}\n", GIT_SHA1); -#endif - fmt::print("Copyright (c) 2011-2024 MIT, UChicago Argonne LLC, and " + fmt::print("OpenMC version {}.{}.{}{}{}\n", VERSION_MAJOR, VERSION_MINOR, + VERSION_RELEASE, VERSION_DEV ? "-dev" : "", VERSION_COMMIT_COUNT); + fmt::print("Commit hash: {}\n", VERSION_COMMIT_HASH); + fmt::print("Copyright (c) 2011-2025 MIT, UChicago Argonne LLC, and " "contributors\nMIT/X license at " "\n"); } From 53066768deb4ca809be34c27f9cd9b7cf281a748 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Mon, 24 Feb 2025 09:49:51 -0600 Subject: [PATCH 162/184] Random Ray Void Accuracy Fix (#3316) --- src/random_ray/flat_source_domain.cpp | 10 ++++------ src/random_ray/linear_source_domain.cpp | 3 +-- src/random_ray/random_ray.cpp | 1 + .../random_ray_void/flat/results_true.dat | 4 ++-- .../random_ray_void/linear/results_true.dat | 4 ++-- 5 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index cfe747eec5e..cd4304b6205 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -187,8 +187,7 @@ void FlatSourceDomain::normalize_scalar_flux_and_volumes( source_regions_.volume_naive(sr) = source_regions_.volume(sr) * normalization_factor; source_regions_.volume_sq(sr) = - (source_regions_.volume_sq_t(sr) / source_regions_.volume_t(sr)) * - volume_normalization_factor; + source_regions_.volume_sq_t(sr) / source_regions_.volume_t(sr); source_regions_.volume(sr) = source_regions_.volume_t(sr) * volume_normalization_factor; } @@ -463,12 +462,11 @@ void FlatSourceDomain::convert_source_regions_to_tallies() auto filter_weight = filter_iter.weight_; // Loop over scores - for (auto score_index = 0; score_index < tally.scores_.size(); - score_index++) { - auto score_bin = tally.scores_[score_index]; + for (int score = 0; score < tally.scores_.size(); score++) { + auto score_bin = tally.scores_[score]; // If a valid tally, filter, and score combination has been found, // then add it to the list of tally tasks for this source element. - TallyTask task(i_tally, filter_index, score_index, score_bin); + TallyTask task(i_tally, filter_index, score, score_bin); source_regions_.tally_task(sr, g).push_back(task); // Also add this task to the list of volume tasks for this source diff --git a/src/random_ray/linear_source_domain.cpp b/src/random_ray/linear_source_domain.cpp index 75d078d1b9b..a8300ba3f84 100644 --- a/src/random_ray/linear_source_domain.cpp +++ b/src/random_ray/linear_source_domain.cpp @@ -127,8 +127,7 @@ void LinearSourceDomain::normalize_scalar_flux_and_volumes( source_regions_.volume(sr) = source_regions_.volume_t(sr) * volume_normalization_factor; source_regions_.volume_sq(sr) = - (source_regions_.volume_sq_t(sr) / source_regions_.volume_t(sr)) * - volume_normalization_factor; + source_regions_.volume_sq_t(sr) / source_regions_.volume_t(sr); if (source_regions_.volume_t(sr) > 0.0) { double inv_volume = 1.0 / source_regions_.volume_t(sr); source_regions_.centroid(sr) = source_regions_.centroid_t(sr); diff --git a/src/random_ray/random_ray.cpp b/src/random_ray/random_ray.cpp index 9917cf0c5b9..f9ebb6f9d01 100644 --- a/src/random_ray/random_ray.cpp +++ b/src/random_ray/random_ray.cpp @@ -703,6 +703,7 @@ void RandomRay::attenuate_flux_linear_source_void( // source region. The centroid and spatial momements estimates are scaled by // the ray segment length as part of length averaging of the estimates. domain_->source_regions_.volume(sr) += distance; + domain_->source_regions_.volume_sq(sr) += distance_2; domain_->source_regions_.centroid_iteration(sr) += midpoint * distance; moment_matrix_estimate *= distance; domain_->source_regions_.mom_matrix(sr) += moment_matrix_estimate; diff --git a/tests/regression_tests/random_ray_void/flat/results_true.dat b/tests/regression_tests/random_ray_void/flat/results_true.dat index bf395f25f78..bd2f2d3b416 100644 --- a/tests/regression_tests/random_ray_void/flat/results_true.dat +++ b/tests/regression_tests/random_ray_void/flat/results_true.dat @@ -1,6 +1,6 @@ tally 1: -1.760401E+00 -1.554914E-01 +2.354630E+00 +2.777456E-01 tally 2: 1.056204E-01 5.741779E-04 diff --git a/tests/regression_tests/random_ray_void/linear/results_true.dat b/tests/regression_tests/random_ray_void/linear/results_true.dat index 77fade82951..e289b2f1360 100644 --- a/tests/regression_tests/random_ray_void/linear/results_true.dat +++ b/tests/regression_tests/random_ray_void/linear/results_true.dat @@ -1,6 +1,6 @@ tally 1: -1.762592E+00 -1.558698E-01 +2.356821E+00 +2.782548E-01 tally 2: 1.082935E-01 6.029854E-04 From c794065d46c275886ec9fea4eeb6e302b304586f Mon Sep 17 00:00:00 2001 From: Patrick Shriwise Date: Mon, 24 Feb 2025 16:11:18 -0600 Subject: [PATCH 163/184] Fix access order issues after applying tally results from `Model.run`. (#3313) Co-authored-by: Paul Romano --- openmc/statepoint.py | 3 +- openmc/tallies.py | 53 +++++++++++++++++++++++++------- tests/unit_tests/test_tallies.py | 32 ++++++++++++++++++- 3 files changed, 75 insertions(+), 13 deletions(-) diff --git a/openmc/statepoint.py b/openmc/statepoint.py index 5720f8cd05a..f2fd48066e4 100644 --- a/openmc/statepoint.py +++ b/openmc/statepoint.py @@ -6,6 +6,7 @@ import h5py import numpy as np +from pathlib import Path from uncertainties import ufloat import openmc @@ -415,7 +416,7 @@ def tallies(self): # Create Tally object and assign basic properties tally = openmc.Tally(tally_id) - tally._sp_filename = self._f.filename + tally._sp_filename = Path(self._f.filename) tally.name = group['name'][()].decode() if 'name' in group else '' # Check if tally has multiply_density attribute diff --git a/openmc/tallies.py b/openmc/tallies.py index b0d78d357c4..585e54a7ef3 100644 --- a/openmc/tallies.py +++ b/openmc/tallies.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections.abc import Iterable, MutableSequence import copy -from functools import partial, reduce +from functools import partial, reduce, wraps from itertools import product from numbers import Integral, Real import operator @@ -179,6 +179,25 @@ def __repr__(self): parts.append('{: <15}=\t{}'.format('Multiply dens.', self.multiply_density)) return '\n\t'.join(parts) + @staticmethod + def ensure_results(f): + """A decorator to be applied to any method that might use tally results. + Results will be loaded if appropriate based on the tally properties. + + Args: + f function: Tally method to wrap + + Returns: + function: Wrapped function that reads tally results before calling + the methodif necessary + """ + @wraps(f) + def read(self): + if self._sp_filename is not None and not self.derived: + self._read_results() + return f(self) + return read + @property def name(self): return self._name @@ -218,6 +237,7 @@ def filters(self, filters): self._filters = cv.CheckedList(_FILTER_CLASSES, 'tally filters', filters) @property + @ensure_results def nuclides(self): return self._nuclides @@ -314,6 +334,7 @@ def triggers(self, triggers): triggers) @property + @ensure_results def num_realizations(self): return self._num_realizations @@ -340,11 +361,11 @@ def _read_results(self): with h5py.File(self._sp_filename, 'r') as f: # Set number of realizations group = f[f'tallies/tally {self.id}'] - self.num_realizations = int(group['n_realizations'][()]) + self._num_realizations = int(group['n_realizations'][()]) # Update nuclides nuclide_names = group['nuclides'][()] - self.nuclides = [name.decode().strip() for name in nuclide_names] + self._nuclides = [name.decode().strip() for name in nuclide_names] # Extract Tally data from the file data = group['results'] @@ -368,13 +389,11 @@ def _read_results(self): self._results_read = True @property + @ensure_results def sum(self): if not self._sp_filename or self.derived: return None - # Make sure results have been read - self._read_results() - if self.sparse: return np.reshape(self._sum.toarray(), self.shape) else: @@ -386,13 +405,11 @@ def sum(self, sum): self._sum = sum @property + @ensure_results def sum_sq(self): if not self._sp_filename or self.derived: return None - # Make sure results have been read - self._read_results() - if self.sparse: return np.reshape(self._sum_sq.toarray(), self.shape) else: @@ -935,10 +952,24 @@ def add_results(self, statepoint: cv.PathLike | openmc.StatePoint): statepoint : openmc.PathLike or openmc.StatePoint Statepoint used to update tally results """ + # derived tallies are populated with data based on combined tallies + # and should not be modified + if self.derived: + return + if isinstance(statepoint, openmc.StatePoint): - self._sp_filename = statepoint._f.filename + self._sp_filename = Path(statepoint._f.filename) else: - self._sp_filename = str(statepoint) + self._sp_filename = Path(str(statepoint)) + + # reset these properties to ensure that any results access after this + # point are based on the current statepoint file + self._sum = None + self._sum_sq = None + self._mean = None + self._std_dev = None + self._num_realizations = 0 + self._results_read = False @classmethod def from_xml_element(cls, elem, **kwargs): diff --git a/tests/unit_tests/test_tallies.py b/tests/unit_tests/test_tallies.py index f044df5d050..ccc6ff34383 100644 --- a/tests/unit_tests/test_tallies.py +++ b/tests/unit_tests/test_tallies.py @@ -106,14 +106,44 @@ def test_tally_application(sphere_model, run_in_tmpdir): tally.scores = ['flux', 'absorption', 'fission', 'scatter'] sphere_model.tallies = [tally] + # FIRST RUN # run the simulation and apply results sp_file = sphere_model.run(apply_tally_results=True) + # before calling for any property requiring results (including the equivalence check below), + # the following internal attributes of the original should be unset + assert tally._mean is None + assert tally._std_dev is None + assert tally._sum is None + assert tally._sum_sq is None + assert tally._num_realizations == 0 + # the statepoint file property should be set, however + assert tally._sp_filename == sp_file + with openmc.StatePoint(sp_file) as sp: assert tally in sp.tallies.values() sp_tally = sp.tallies[tally.id] # at this point the tally information regarding results should be the same - assert (sp_tally.mean == tally.mean).all() assert (sp_tally.std_dev == tally.std_dev).all() + assert (sp_tally.mean == tally.mean).all() assert sp_tally.nuclides == tally.nuclides + # SECOND RUN + # change the number of particles and ensure that the results are different + sphere_model.settings.particles += 1 + sp_file = sphere_model.run(apply_tally_results=True) + + assert (sp_tally.std_dev != tally.std_dev).any() + assert (sp_tally.mean != tally.mean).any() + + # now re-read data from the new stateopint file and + # ensure that the new results match those in + # the latest statepoint + with openmc.StatePoint(sp_file) as sp: + assert tally in sp.tallies.values() + sp_tally = sp.tallies[tally.id] + + # at this point the tally information regarding results should be the same + assert (sp_tally.std_dev == tally.std_dev).all() + assert (sp_tally.mean == tally.mean).all() + assert sp_tally.nuclides == tally.nuclides From cba132c4b487a2c8cd6318f6a049354575e2a7f4 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Mon, 24 Feb 2025 17:25:11 -0600 Subject: [PATCH 164/184] Random Ray Linear Source Stability Improvement (#3322) --- src/random_ray/linear_source_domain.cpp | 16 +- .../linear/results_true.dat | 12 +- .../linear_xy/results_true.dat | 12 +- .../linear_xy/results_true.dat | 168 +++++++++--------- .../test.py | 2 +- .../random_ray_void/linear/results_true.dat | 12 +- .../hybrid/results_true.dat | 12 +- .../naive/results_true.dat | 12 +- .../simulation_averaged/results_true.dat | 12 +- 9 files changed, 132 insertions(+), 126 deletions(-) diff --git a/src/random_ray/linear_source_domain.cpp b/src/random_ray/linear_source_domain.cpp index a8300ba3f84..3819c7d703d 100644 --- a/src/random_ray/linear_source_domain.cpp +++ b/src/random_ray/linear_source_domain.cpp @@ -78,13 +78,19 @@ void LinearSourceDomain::update_neutron_source(double k_eff) source_regions_.source(sr, g_out) = (scatter_flat + fission_flat * inverse_k_eff) / sigma_t; - // Compute the linear source terms - // In the first 10 iterations when the centroids and spatial moments - // are not well known, we will leave the source gradients as zero - // so as to avoid causing any numerical instability. - if (simulation::current_batch > 10) { + // Compute the linear source terms. In the first 10 iterations when the + // centroids and spatial moments are not well known, we will leave the + // source gradients as zero so as to avoid causing any numerical + // instability. If a negative source is encountered, this region must be + // very small/noisy or have poorly developed spatial moments, so we zero + // the source gradients (effectively making this a flat source region + // temporarily), so as to improve stability. + if (simulation::current_batch > 10 && + source_regions_.source(sr, g_out) >= 0.0) { source_regions_.source_gradients(sr, g_out) = invM * ((scatter_linear + fission_linear * inverse_k_eff) / sigma_t); + } else { + source_regions_.source_gradients(sr, g_out) = {0.0, 0.0, 0.0}; } } } diff --git a/tests/regression_tests/random_ray_fixed_source_linear/linear/results_true.dat b/tests/regression_tests/random_ray_fixed_source_linear/linear/results_true.dat index 46200b19c8d..2fc08d0a429 100644 --- a/tests/regression_tests/random_ray_fixed_source_linear/linear/results_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_linear/linear/results_true.dat @@ -1,9 +1,9 @@ tally 1: -2.339084E+00 -2.747299E-01 +2.339085E+00 +2.747304E-01 tally 2: -1.090051E-01 -6.073265E-04 +1.089827E-01 +6.069324E-04 tally 3: -7.300117E-03 -2.715425E-06 +7.300831E-03 +2.715940E-06 diff --git a/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/results_true.dat b/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/results_true.dat index ea711a92567..f4c8546aae4 100644 --- a/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/results_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_linear/linear_xy/results_true.dat @@ -1,9 +1,9 @@ tally 1: -2.335701E+00 -2.742860E-01 +2.335703E+00 +2.742866E-01 tally 2: -1.081600E-01 -5.980902E-04 +1.081884E-01 +5.983316E-04 tally 3: -7.295015E-03 -2.711589E-06 +7.295389E-03 +2.711859E-06 diff --git a/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/results_true.dat b/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/results_true.dat index 43a6d825816..49813144d26 100644 --- a/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/results_true.dat +++ b/tests/regression_tests/random_ray_fixed_source_subcritical/linear_xy/results_true.dat @@ -1,168 +1,168 @@ tally 1: -1.583461E+02 -1.007023E+03 +1.583465E+02 +1.007029E+03 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.891511E+01 -1.392800E+02 +5.891526E+01 +1.392807E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.028333E+01 -1.649217E+01 +2.028338E+01 +1.649224E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.453681E+01 -2.413486E+01 +2.453687E+01 +2.413498E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.235799E+01 -1.099237E+02 +5.235812E+01 +1.099243E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -9.394382E+01 -3.543494E+02 +9.394406E+01 +3.543513E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.676269E+01 -1.303097E+02 +5.676284E+01 +1.303104E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.007853E+02 -1.616208E+03 +2.007858E+02 +1.616216E+03 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -7.319692E+01 -2.146291E+02 +7.319709E+01 +2.146301E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.566725E+01 -2.640687E+01 +2.566730E+01 +2.640697E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.934697E+01 -3.453012E+01 +2.934703E+01 +3.453026E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.737202E+01 -1.318574E+02 +5.737215E+01 +1.318580E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -9.495251E+01 -3.613355E+02 +9.495275E+01 +3.613372E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.799236E+01 -1.356172E+02 +5.799250E+01 +1.356178E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -1.066411E+02 -4.570264E+02 -1.254011E+01 -6.325603E+00 -3.052011E+01 -3.746896E+01 -4.977289E+01 -9.940494E+01 -2.345784E+00 -2.208432E-01 -5.709169E+00 -1.308139E+00 -1.953078E+01 -1.528285E+01 -1.998229E-01 -1.599785E-03 -4.863290E-01 -9.476135E-03 -2.307190E+01 -2.133049E+01 -2.432475E-01 -2.371063E-03 -5.920158E-01 -1.404471E-02 -5.299849E+01 -1.125570E+02 -1.959909E-01 -1.539183E-03 -4.770085E-01 -9.117394E-03 -1.186130E+02 -5.641029E+02 -5.781214E-02 -1.341752E-04 -1.430521E-01 -8.215282E-04 -7.995712E+01 -2.570085E+02 -3.452925E-01 -4.844032E-03 -9.604136E-01 -3.747567E-02 -1.556790E+02 -9.726171E+02 +1.066414E+02 +4.570291E+02 +1.254014E+01 +6.325640E+00 +3.052020E+01 +3.746918E+01 +4.977302E+01 +9.940548E+01 +2.345790E+00 +2.208444E-01 +5.709184E+00 +1.308146E+00 +1.953082E+01 +1.528292E+01 +1.998234E-01 +1.599792E-03 +4.863302E-01 +9.476180E-03 +2.307196E+01 +2.133059E+01 +2.432481E-01 +2.371074E-03 +5.920173E-01 +1.404478E-02 +5.299862E+01 +1.125576E+02 +1.959914E-01 +1.539191E-03 +4.770098E-01 +9.117441E-03 +1.186133E+02 +5.641060E+02 +5.781230E-02 +1.341759E-04 +1.430525E-01 +8.215326E-04 +7.995734E+01 +2.570099E+02 +3.452934E-01 +4.844058E-03 +9.604162E-01 +3.747587E-02 +1.556795E+02 +9.726232E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.816710E+01 -1.357523E+02 +5.816726E+01 +1.357531E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.018475E+01 -1.633543E+01 +2.018480E+01 +1.633551E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -2.453470E+01 -2.413604E+01 +2.453476E+01 +2.413615E+01 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.269366E+01 -1.113096E+02 +5.269380E+01 +1.113102E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -9.550190E+01 -3.658245E+02 +9.550216E+01 +3.658264E+02 0.000000E+00 0.000000E+00 0.000000E+00 0.000000E+00 -5.846305E+01 -1.379618E+02 +5.846320E+01 +1.379625E+02 0.000000E+00 0.000000E+00 0.000000E+00 diff --git a/tests/regression_tests/random_ray_fixed_source_subcritical/test.py b/tests/regression_tests/random_ray_fixed_source_subcritical/test.py index 6c2c569c65e..e2f3cf17582 100644 --- a/tests/regression_tests/random_ray_fixed_source_subcritical/test.py +++ b/tests/regression_tests/random_ray_fixed_source_subcritical/test.py @@ -17,7 +17,7 @@ def _cleanup(self): @pytest.mark.parametrize("shape", ["flat", "linear_xy"]) -def test_random_ray_source(shape): +def test_random_ray_fixed_source_subcritical(shape): with change_directory(shape): openmc.reset_auto_ids() diff --git a/tests/regression_tests/random_ray_void/linear/results_true.dat b/tests/regression_tests/random_ray_void/linear/results_true.dat index e289b2f1360..befa507a59b 100644 --- a/tests/regression_tests/random_ray_void/linear/results_true.dat +++ b/tests/regression_tests/random_ray_void/linear/results_true.dat @@ -1,9 +1,9 @@ tally 1: -2.356821E+00 -2.782548E-01 +2.356818E+00 +2.782542E-01 tally 2: -1.082935E-01 -6.029854E-04 +1.082843E-01 +6.028734E-04 tally 3: -7.301975E-03 -2.717553E-06 +7.302705E-03 +2.718076E-06 diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/results_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/results_true.dat index 46200b19c8d..2fc08d0a429 100644 --- a/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/results_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator_linear/hybrid/results_true.dat @@ -1,9 +1,9 @@ tally 1: -2.339084E+00 -2.747299E-01 +2.339085E+00 +2.747304E-01 tally 2: -1.090051E-01 -6.073265E-04 +1.089827E-01 +6.069324E-04 tally 3: -7.300117E-03 -2.715425E-06 +7.300831E-03 +2.715940E-06 diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/naive/results_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/naive/results_true.dat index 02bfc526626..5258ffd9c84 100644 --- a/tests/regression_tests/random_ray_volume_estimator_linear/naive/results_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator_linear/naive/results_true.dat @@ -1,9 +1,9 @@ tally 1: -2.339575E+00 -2.748441E-01 +2.339567E+00 +2.748423E-01 tally 2: -1.086360E-01 -6.030557E-04 +1.085878E-01 +6.024509E-04 tally 3: -7.298937E-03 -2.741185E-06 +7.299803E-03 +2.741867E-06 diff --git a/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/results_true.dat b/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/results_true.dat index 93e54feea99..1e8aa9fb75f 100644 --- a/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/results_true.dat +++ b/tests/regression_tests/random_ray_volume_estimator_linear/simulation_averaged/results_true.dat @@ -1,9 +1,9 @@ tally 1: -2.670874E+02 -4.432876E+05 +2.670850E+02 +4.432939E+05 tally 2: -1.117459E-01 -6.497788E-04 +1.116994E-01 +6.491358E-04 tally 3: -7.563753E-03 -2.947210E-06 +7.564527E-03 +2.947794E-06 From 244d630fd2c34d8473a0919bc614b499f8642414 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 24 Feb 2025 17:25:33 -0600 Subject: [PATCH 165/184] Clarify effect of CMAKE_BUILD_TYPE in docs (#3321) --- docs/source/usersguide/install.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/source/usersguide/install.rst b/docs/source/usersguide/install.rst index b86f5fee032..bb54594defb 100644 --- a/docs/source/usersguide/install.rst +++ b/docs/source/usersguide/install.rst @@ -425,13 +425,16 @@ OpenMC can be configured for debug, release, or release with debug info by setti the `CMAKE_BUILD_TYPE` option. Debug - Enable debug compiler flags with no optimization `-O0 -g`. + Enable debug compiler flags with no optimization. On most platforms/compilers, + this is equivalent to `-O0 -g`. Release - Disable debug and enable optimization `-O3 -DNDEBUG`. + Disable debug and enable optimization. On most platforms/compilers, this is + equivalent to `-O3 -DNDEBUG`. RelWithDebInfo - (Default if no type is specified.) Enable optimization and debug `-O2 -g`. + (Default if no type is specified.) Enable optimization and debug. On most + platforms/compilers, this is equivalent to `-O2 -g`. Example of configuring for Debug mode: From fed4b2861687aae2943358f7be6e3cbb1057de59 Mon Sep 17 00:00:00 2001 From: Micah Gale Date: Mon, 24 Feb 2025 21:49:12 -0600 Subject: [PATCH 166/184] Mark a canonical URL for docs (#3324) --- docs/source/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index b9db835077b..618cbe64838 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -127,6 +127,7 @@ import sphinx_rtd_theme html_theme = 'sphinx_rtd_theme' html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +html_baseurl = "https://docs.openmc.org/en/stable/" html_logo = '_images/openmc_logo.png' From 1729b3bf91f4a99c57f5f330c5e18560c658a084 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Mon, 24 Feb 2025 21:49:47 -0600 Subject: [PATCH 167/184] Fixes for problems encountered with version determination (#3320) --- .readthedocs.yaml | 4 +++- Dockerfile | 2 +- LICENSE | 2 +- cmake/Modules/GetVersionFromGit.cmake | 5 +++++ docs/source/conf.py | 7 ++----- docs/source/license.rst | 2 +- man/man1/openmc.1 | 2 +- 7 files changed, 14 insertions(+), 10 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 8bbc7d01f0b..3578144b254 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -4,7 +4,9 @@ build: os: "ubuntu-24.04" tools: python: "3.12" - + jobs: + post_checkout: + - git fetch --unshallow || true sphinx: configuration: docs/source/conf.py diff --git a/Dockerfile b/Dockerfile index 583682ef595..1ae615a50f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -194,7 +194,7 @@ ENV LIBMESH_INSTALL_DIR=$HOME/LIBMESH # clone and install openmc RUN mkdir -p ${HOME}/OpenMC && cd ${HOME}/OpenMC \ - && git clone --shallow-submodules --recurse-submodules --single-branch -b ${openmc_branch} --depth=1 ${OPENMC_REPO} \ + && git clone --shallow-submodules --recurse-submodules --single-branch -b ${openmc_branch} ${OPENMC_REPO} \ && mkdir build && cd build ; \ if [ ${build_dagmc} = "on" ] && [ ${build_libmesh} = "on" ]; then \ cmake ../openmc \ diff --git a/LICENSE b/LICENSE index 1f9198c1e3b..8a60b66bf9f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2011-2024 Massachusetts Institute of Technology, UChicago Argonne +Copyright (c) 2011-2025 Massachusetts Institute of Technology, UChicago Argonne LLC, and OpenMC contributors Permission is hereby granted, free of charge, to any person obtaining a copy of diff --git a/cmake/Modules/GetVersionFromGit.cmake b/cmake/Modules/GetVersionFromGit.cmake index ee6bac827e3..42b3725f29b 100644 --- a/cmake/Modules/GetVersionFromGit.cmake +++ b/cmake/Modules/GetVersionFromGit.cmake @@ -39,6 +39,11 @@ if(EXISTS "${CMAKE_SOURCE_DIR}/.git" AND GIT_FOUND) OUTPUT_STRIP_TRAILING_WHITESPACE ) + # If no tags are found, instruct user to fetch them + if(VERSION_STRING STREQUAL "") + message(FATAL_ERROR "No git tags found. Run 'git fetch --tags' and try again.") + endif() + # Extract the commit hash execute_process( COMMAND git rev-parse HEAD diff --git a/docs/source/conf.py b/docs/source/conf.py index 618cbe64838..8aeff5cdc7a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -62,7 +62,7 @@ # General information about the project. project = 'OpenMC' -copyright = '2011-2024, Massachusetts Institute of Technology, UChicago Argonne LLC, and OpenMC contributors' +copyright = '2011-2025, Massachusetts Institute of Technology, UChicago Argonne LLC, and OpenMC contributors' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -71,11 +71,8 @@ import openmc -# The short X.Y version. -version = ".".join(openmc.__version__.split('.')[:2]) - # The full version, including alpha/beta/rc tags. -release = openmc.__version__ +version = release = openmc.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/source/license.rst b/docs/source/license.rst index 23315e8fe0b..0a90a744134 100644 --- a/docs/source/license.rst +++ b/docs/source/license.rst @@ -4,7 +4,7 @@ License Agreement ================= -Copyright © 2011-2024 Massachusetts Institute of Technology, UChicago Argonne +Copyright © 2011-2025 Massachusetts Institute of Technology, UChicago Argonne LLC, and OpenMC contributors Permission is hereby granted, free of charge, to any person obtaining a copy of diff --git a/man/man1/openmc.1 b/man/man1/openmc.1 index d121a8956d9..7826a759a1e 100644 --- a/man/man1/openmc.1 +++ b/man/man1/openmc.1 @@ -63,7 +63,7 @@ Indicates the default path to an HDF5 file that contains multi-group cross section libraries if the user has not specified the tag in .I materials.xml\fP. .SH LICENSE -Copyright \(co 2011-2024 Massachusetts Institute of Technology, UChicago +Copyright \(co 2011-2025 Massachusetts Institute of Technology, UChicago Argonne LLC, and OpenMC contributors. .PP Permission is hereby granted, free of charge, to any person obtaining a copy of From 27258c009c60a6ca9b3be3e300c0811eab3de72f Mon Sep 17 00:00:00 2001 From: John Tramm Date: Tue, 25 Feb 2025 10:34:28 -0600 Subject: [PATCH 168/184] Random Ray Adjoint Source Logic Improvement (#3325) --- src/random_ray/flat_source_domain.cpp | 27 ++++++++++++------- .../inputs_true.dat | 3 ++- .../results_true.dat | 12 ++++----- .../random_ray_adjoint_fixed_source/test.py | 2 ++ 4 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index cd4304b6205..ae0ecb824fc 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -1049,14 +1049,21 @@ void FlatSourceDomain::flatten_xs() void FlatSourceDomain::set_adjoint_sources(const vector& forward_flux) { - // Set the external source to 1/forward_flux - // The forward flux is given in terms of total for the forward simulation - // so we must convert it to a "per batch" quantity + // Set the external source to 1/forward_flux. If the forward flux is negative + // or zero, set the adjoint source to zero, as this is likely a very small + // source region that we don't need to bother trying to vector particles + // towards. Flux negativity in random ray is not related to the flux being + // small in magnitude, but rather due to the source region being physically + // small in volume and thus having a noisy flux estimate. #pragma omp parallel for for (int64_t sr = 0; sr < n_source_regions_; sr++) { for (int g = 0; g < negroups_; g++) { - source_regions_.external_source(sr, g) = - 1.0 / forward_flux[sr * negroups_ + g]; + double flux = forward_flux[sr * negroups_ + g]; + if (flux <= 0.0) { + source_regions_.external_source(sr, g) = 0.0; + } else { + source_regions_.external_source(sr, g) = 1.0 / flux; + } } } @@ -1064,12 +1071,12 @@ void FlatSourceDomain::set_adjoint_sources(const vector& forward_flux) // iteration) #pragma omp parallel for for (int64_t sr = 0; sr < n_source_regions_; sr++) { + int material = source_regions_.material(sr); + if (material == MATERIAL_VOID) { + continue; + } for (int g = 0; g < negroups_; g++) { - int material = source_regions_.material(sr); - if (material == MATERIAL_VOID) { - continue; - } - double sigma_t = sigma_t_[source_regions_.material(sr) * negroups_ + g]; + double sigma_t = sigma_t_[material * negroups_ + g]; source_regions_.external_source(sr, g) /= sigma_t; } } diff --git a/tests/regression_tests/random_ray_adjoint_fixed_source/inputs_true.dat b/tests/regression_tests/random_ray_adjoint_fixed_source/inputs_true.dat index 686987512a0..65ffd5af7f2 100644 --- a/tests/regression_tests/random_ray_adjoint_fixed_source/inputs_true.dat +++ b/tests/regression_tests/random_ray_adjoint_fixed_source/inputs_true.dat @@ -191,7 +191,7 @@ fixed source - 90 + 500 10 5 @@ -214,6 +214,7 @@ True True + naive diff --git a/tests/regression_tests/random_ray_adjoint_fixed_source/results_true.dat b/tests/regression_tests/random_ray_adjoint_fixed_source/results_true.dat index a216fa9fcc3..7178e2f111b 100644 --- a/tests/regression_tests/random_ray_adjoint_fixed_source/results_true.dat +++ b/tests/regression_tests/random_ray_adjoint_fixed_source/results_true.dat @@ -1,9 +1,9 @@ tally 1: --7.235364E+03 -3.367109E+09 +5.790516E+04 +6.740859E+08 tally 2: -4.818311E+05 -6.269371E+10 +6.885455E+04 +9.482551E+08 tally 3: -1.515641E+06 -4.598791E+11 +1.956327E+05 +7.654469E+09 diff --git a/tests/regression_tests/random_ray_adjoint_fixed_source/test.py b/tests/regression_tests/random_ray_adjoint_fixed_source/test.py index 0295e36e9ac..6c2790fa093 100644 --- a/tests/regression_tests/random_ray_adjoint_fixed_source/test.py +++ b/tests/regression_tests/random_ray_adjoint_fixed_source/test.py @@ -16,5 +16,7 @@ def _cleanup(self): def test_random_ray_adjoint_fixed_source(): model = random_ray_three_region_cube() model.settings.random_ray['adjoint'] = True + model.settings.random_ray['volume_estimator'] = 'naive' + model.settings.particles = 500 harness = MGXSTestHarness('statepoint.10.h5', model) harness.main() From 865c80a5f9a51546c9ed56d059798a68ede241fe Mon Sep 17 00:00:00 2001 From: Olek <45364492+yardasol@users.noreply.github.com> Date: Tue, 25 Feb 2025 13:50:00 -0600 Subject: [PATCH 169/184] Reflect multigroup MicroXS in IndependentOperator docstrings (#3327) Co-authored-by: Paul Romano --- openmc/deplete/independent_operator.py | 74 ++++++++++++++------------ openmc/deplete/openmc_operator.py | 15 +----- 2 files changed, 42 insertions(+), 47 deletions(-) diff --git a/openmc/deplete/independent_operator.py b/openmc/deplete/independent_operator.py index 226682ffa53..333ead0f8a0 100644 --- a/openmc/deplete/independent_operator.py +++ b/openmc/deplete/independent_operator.py @@ -1,7 +1,7 @@ """Transport-independent transport operator for depletion. This module implements a transport operator that runs independently of any -transport solver by using user-provided one-group cross sections. +transport solver by using user-provided multigroup fluxes and cross sections. """ @@ -24,14 +24,12 @@ class IndependentOperator(OpenMCOperator): - """Transport-independent transport operator that uses one-group cross - sections to calculate reaction rates. + """Transport-independent transport operator based on multigroup data. - Instances of this class can be used to perform depletion using one-group - cross sections and constant flux or constant power. Normally, a user needn't - call methods of this class directly. Instead, an instance of this class is - passed to an integrator class, such as - :class:`openmc.deplete.CECMIntegrator`. + Instances of this class can be used to perform depletion using multigroup + cross sections and multigroup fluxes. Normally, a user needn't call methods + of this class directly. Instead, an instance of this class is passed to an + integrator class, such as :class:`openmc.deplete.CECMIntegrator`. Note that passing an empty :class:`~openmc.deplete.MicroXS` instance to the ``micro_xs`` argument allows a decay-only calculation to be run. @@ -57,43 +55,41 @@ class IndependentOperator(OpenMCOperator): ``openmc.config['chain_file']``. keff : 2-tuple of float, optional keff eigenvalue and uncertainty from transport calculation. - Default is None. prev_results : Results, optional Results from a previous depletion calculation. normalization_mode : {"fission-q", "source-rate"} - Indicate how reaction rates should be calculated. - ``"fission-q"`` uses the fission Q values from the depletion chain to - compute the flux based on the power. ``"source-rate"`` uses a the - source rate (assumed to be neutron flux) to calculate the - reaction rates. + Indicate how reaction rates should be calculated. ``"fission-q"`` uses + the fission Q values from the depletion chain to compute the flux based + on the power. ``"source-rate"`` uses a the source rate (assumed to be + neutron flux) to calculate the reaction rates. fission_q : dict, optional Dictionary of nuclides and their fission Q values [eV]. If not given, - values will be pulled from the ``chain_file``. Only applicable - if ``"normalization_mode" == "fission-q"``. + values will be pulled from the ``chain_file``. Only applicable if + ``"normalization_mode" == "fission-q"``. reduce_chain : bool, optional - If True, use :meth:`openmc.deplete.Chain.reduce` to reduce the - depletion chain up to ``reduce_chain_level``. + If True, use :meth:`openmc.deplete.Chain.reduce` to reduce the depletion + chain up to ``reduce_chain_level``. reduce_chain_level : int, optional - Depth of the search when reducing the depletion chain. Only used - if ``reduce_chain`` evaluates to true. The default value of - ``None`` implies no limit on the depth. + Depth of the search when reducing the depletion chain. Only used if + ``reduce_chain`` evaluates to true. The default value of ``None`` + implies no limit on the depth. fission_yield_opts : dict of str to option, optional Optional arguments to pass to the :class:`openmc.deplete.helpers.FissionYieldHelper` object. Will be - passed directly on to the helper. Passing a value of None will use - the defaults for the associated helper. + passed directly on to the helper. Passing a value of None will use the + defaults for the associated helper. Attributes ---------- materials : openmc.Materials All materials present in the model - cross_sections : MicroXS - Object containing one-group cross-sections in [cm^2]. + cross_sections : list of MicroXS + Object containing multigroup cross-sections in [b] for each material. output_dir : pathlib.Path Path to output directory to save results. round_number : bool - Whether or not to round output to OpenMC to 8 digits. - Useful in testing, as OpenMC is incredibly sensitive to exact values. + Whether or not to round output to OpenMC to 8 digits. Useful in testing, + as OpenMC is incredibly sensitive to exact values. number : openmc.deplete.AtomNumber Total number of atoms in simulation. nuclides_with_data : set of str @@ -109,8 +105,8 @@ class IndependentOperator(OpenMCOperator): local_mats : list of str All burnable material IDs being managed by a single process prev_res : Results or None - Results from a previous depletion calculation. ``None`` if no - results are to be used. + Results from a previous depletion calculation. ``None`` if no results + are to be used. """ @@ -279,14 +275,26 @@ def _load_previous_results(self): self.prev_res.append(new_res) def _get_nuclides_with_data(self, cross_sections: list[MicroXS]) -> set[str]: - """Finds nuclides with cross section data""" + """Finds nuclides with cross section data + + Parameters + ---------- + cross_sections : iterable of :class`~openmc.deplete.MicroXS` + List of multigroup cross-section data. + + Returns + ------- + nuclides : set of str + Set of nuclide names that have cross secton data + + """ return set(cross_sections[0].nuclides) class _IndependentRateHelper(ReactionRateHelper): - """Class for generating one-group reaction rates with flux and - one-group cross sections. + """Class for generating reaction rates with multigroup fluxes and + multigroup cross sections. - This class does not generate tallies, and instead stores cross sections + This class does not generate tallies and instead stores cross sections for each nuclide and transmutation reaction relevant for a depletion calculation. The reaction rate is calculated by multiplying the flux by the cross sections. diff --git a/openmc/deplete/openmc_operator.py b/openmc/deplete/openmc_operator.py index 049e0dd37ee..64cb0a7e5c4 100644 --- a/openmc/deplete/openmc_operator.py +++ b/openmc/deplete/openmc_operator.py @@ -247,20 +247,7 @@ def _load_previous_results(self): @abstractmethod def _get_nuclides_with_data(self, cross_sections): - """Find nuclides with cross section data - - Parameters - ---------- - cross_sections : str or pandas.DataFrame - Path to continuous energy cross section library, or object - containing one-group cross-sections. - - Returns - ------- - nuclides : set of str - Set of nuclide names that have cross secton data - - """ + """Find nuclides with cross section data.""" def _extract_number(self, local_mats, volume, all_nuclides, prev_res=None): """Construct AtomNumber using geometry From e060534ff1d79885ef2b20ae169a8b9b5e63c3c5 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 26 Feb 2025 08:14:53 -0600 Subject: [PATCH 170/184] Compute material volumes in mesh elements based on raytracing (#3129) Co-authored-by: Olek <45364492+yardasol@users.noreply.github.com> Co-authored-by: Patrick Shriwise --- docs/source/pythonapi/base.rst | 1 + include/openmc/bounding_box.h | 4 + include/openmc/capi.h | 4 +- include/openmc/mesh.h | 95 +++++-- openmc/lib/mesh.py | 135 +++++----- openmc/mesh.py | 227 +++++++++++++--- src/mesh.cpp | 461 ++++++++++++++++++++++++++------- src/particle.cpp | 1 - tests/unit_tests/test_lib.py | 36 +-- tests/unit_tests/test_mesh.py | 88 ++++++- 10 files changed, 814 insertions(+), 238 deletions(-) diff --git a/docs/source/pythonapi/base.rst b/docs/source/pythonapi/base.rst index bfd94272748..e5b5b17c317 100644 --- a/docs/source/pythonapi/base.rst +++ b/docs/source/pythonapi/base.rst @@ -152,6 +152,7 @@ Constructing Tallies openmc.CylindricalMesh openmc.SphericalMesh openmc.UnstructuredMesh + openmc.MeshMaterialVolumes openmc.Trigger openmc.TallyDerivative openmc.Tally diff --git a/include/openmc/bounding_box.h b/include/openmc/bounding_box.h index 40f603583b4..d02d92cb41e 100644 --- a/include/openmc/bounding_box.h +++ b/include/openmc/bounding_box.h @@ -4,6 +4,7 @@ #include // for min, max #include "openmc/constants.h" +#include "openmc/position.h" namespace openmc { @@ -54,6 +55,9 @@ struct BoundingBox { zmax = std::max(zmax, other.zmax); return *this; } + + inline Position min() const { return {xmin, ymin, zmin}; } + inline Position max() const { return {xmax, ymax, zmax}; } }; } // namespace openmc diff --git a/include/openmc/capi.h b/include/openmc/capi.h index 8edd99c0785..3802692c35a 100644 --- a/include/openmc/capi.h +++ b/include/openmc/capi.h @@ -110,8 +110,8 @@ int openmc_mesh_get_id(int32_t index, int32_t* id); int openmc_mesh_set_id(int32_t index, int32_t id); int openmc_mesh_get_n_elements(int32_t index, size_t* n); int openmc_mesh_get_volumes(int32_t index, double* volumes); -int openmc_mesh_material_volumes(int32_t index, int n_sample, int bin, - int result_size, void* result, int* hits, uint64_t* seed); +int openmc_mesh_material_volumes(int32_t index, int nx, int ny, int nz, + int max_mats, int32_t* materials, double* volumes); int openmc_meshsurface_filter_get_mesh(int32_t index, int32_t* index_mesh); int openmc_meshsurface_filter_set_mesh(int32_t index, int32_t index_mesh); int openmc_new_filter(const char* type, int32_t* index); diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index 3aef49c7e68..943e49a5ff1 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -69,14 +69,70 @@ extern const libMesh::Parallel::Communicator* libmesh_comm; } // namespace settings #endif -class Mesh { +//============================================================================== +//! Helper class for keeping track of volume for each material in a mesh element +// +//! This class is used in Mesh::material_volumes to manage for each mesh element +//! a list of (material, volume) pairs. The openmc.lib.Mesh class allocates two +//! 2D arrays, one for materials and one for volumes. Because we don't know a +//! priori how many materials there are in each element but at the same time we +//! can't dynamically size an array at runtime for performance reasons, we +//! assume a maximum number of materials per element. For each element, the set +//! of material indices are stored in a hash table with twice as many slots as +//! the assumed maximum number of materials per element. Collision resolution is +//! handled by open addressing with linear probing. +//============================================================================== + +namespace detail { + +class MaterialVolumes { public: - // Types, aliases - struct MaterialVolume { - int32_t material; //!< material index - double volume; //!< volume in [cm^3] - }; + MaterialVolumes(int32_t* mats, double* vols, int table_size) + : materials_(mats), volumes_(vols), table_size_(table_size) + {} + + //! Add volume for a given material in a mesh element + // + //! \param[in] index_elem Index of the mesh element + //! \param[in] index_material Index of the material within the model + //! \param[in] volume Volume to add + void add_volume(int index_elem, int index_material, double volume); + void add_volume_unsafe(int index_elem, int index_material, double volume); + + // Accessors + int32_t& materials(int i, int j) { return materials_[i * table_size_ + j]; } + const int32_t& materials(int i, int j) const + { + return materials_[i * table_size_ + j]; + } + + double& volumes(int i, int j) { return volumes_[i * table_size_ + j]; } + const double& volumes(int i, int j) const + { + return volumes_[i * table_size_ + j]; + } + + bool table_full() const { return table_full_; } +private: + int32_t* materials_; //!< material index (bins, table_size) + double* volumes_; //!< volume in [cm^3] (bins, table_size) + int table_size_; //!< Size of hash table for each mesh element + bool table_full_ {false}; //!< Whether the hash table is full + + // Value used to indicate an empty slot in the hash table. We use -2 because + // the value -1 is used to indicate a void material. + static constexpr int EMPTY {-2}; +}; + +} // namespace detail + +//============================================================================== +//! Base mesh class +//============================================================================== + +class Mesh { +public: // Constructors and destructor Mesh() = default; Mesh(pugi::xml_node node); @@ -172,24 +228,17 @@ class Mesh { virtual std::string get_mesh_type() const = 0; - //! Determine volume of materials within a single mesh elemenet - // - //! \param[in] n_sample Number of samples within each element - //! \param[in] bin Index of mesh element - //! \param[out] Array of (material index, volume) for desired element - //! \param[inout] seed Pseudorandom number seed - //! \return Number of materials within element - int material_volumes( - int n_sample, int bin, span volumes, uint64_t* seed) const; - - //! Determine volume of materials within a single mesh elemenet + //! Determine volume of materials within each mesh element // - //! \param[in] n_sample Number of samples within each element - //! \param[in] bin Index of mesh element - //! \param[inout] seed Pseudorandom number seed - //! \return Vector of (material index, volume) for desired element - vector material_volumes( - int n_sample, int bin, uint64_t* seed) const; + //! \param[in] nx Number of samples in x direction + //! \param[in] ny Number of samples in y direction + //! \param[in] nz Number of samples in z direction + //! \param[in] max_materials Maximum number of materials in a single mesh + //! element + //! \param[inout] materials Array storing material indices + //! \param[inout] volumes Array storing volumes + void material_volumes(int nx, int ny, int nz, int max_materials, + int32_t* materials, double* volumes) const; //! Determine bounding box of mesh // diff --git a/openmc/lib/mesh.py b/openmc/lib/mesh.py index 16bec019863..3a720f98a31 100644 --- a/openmc/lib/mesh.py +++ b/openmc/lib/mesh.py @@ -1,7 +1,7 @@ from collections.abc import Mapping, Sequence -from ctypes import (c_int, c_int32, c_char_p, c_double, POINTER, Structure, - create_string_buffer, c_uint64, c_size_t) -from random import getrandbits +from ctypes import (c_int, c_int32, c_char_p, c_double, POINTER, + create_string_buffer, c_size_t) +from math import sqrt import sys from weakref import WeakValueDictionary @@ -10,24 +10,20 @@ from ..exceptions import AllocationError, InvalidIDError from . import _dll -from .core import _FortranObjectWithID +from .core import _FortranObjectWithID, quiet_dll from .error import _error_handler -from .material import Material from .plot import _Position from ..bounding_box import BoundingBox +from ..mesh import MeshMaterialVolumes __all__ = [ 'Mesh', 'RegularMesh', 'RectilinearMesh', 'CylindricalMesh', - 'SphericalMesh', 'UnstructuredMesh', 'meshes' + 'SphericalMesh', 'UnstructuredMesh', 'meshes', 'MeshMaterialVolumes' ] -class _MaterialVolume(Structure): - _fields_ = [ - ("material", c_int32), - ("volume", c_double) - ] - +arr_2d_int32 = np.ctypeslib.ndpointer(dtype=np.int32, ndim=2, flags='CONTIGUOUS') +arr_2d_double = np.ctypeslib.ndpointer(dtype=np.double, ndim=2, flags='CONTIGUOUS') # Mesh functions _dll.openmc_extend_meshes.argtypes = [c_int32, c_char_p, POINTER(c_int32), @@ -51,8 +47,7 @@ class _MaterialVolume(Structure): _dll.openmc_mesh_bounding_box.restype = c_int _dll.openmc_mesh_bounding_box.errcheck = _error_handler _dll.openmc_mesh_material_volumes.argtypes = [ - c_int32, c_int, c_int, c_int, POINTER(_MaterialVolume), - POINTER(c_int), POINTER(c_uint64)] + c_int32, c_int, c_int, c_int, c_int, arr_2d_int32, arr_2d_double] _dll.openmc_mesh_material_volumes.restype = c_int _dll.openmc_mesh_material_volumes.errcheck = _error_handler _dll.openmc_mesh_get_plot_bins.argtypes = [ @@ -190,58 +185,81 @@ def bounding_box(self) -> BoundingBox: def material_volumes( self, - n_samples: int = 10_000, - prn_seed: int | None = None - ) -> list[list[tuple[Material, float]]]: - """Determine volume of materials in each mesh element + n_samples: int | tuple[int, int, int] = 10_000, + max_materials: int = 4, + output: bool = True, + ) -> MeshMaterialVolumes: + """Determine volume of materials in each mesh element. + + This method works by raytracing repeatedly through the mesh to count the + estimated volume of each material in all mesh elements. Three sets of + rays are used: one set parallel to the x-axis, one parallel to the + y-axis, and one parallel to the z-axis. .. versionadded:: 0.15.0 + .. versionchanged:: 0.15.1 + Material volumes are now determined by raytracing rather than by + point sampling. + Parameters ---------- - n_samples : int - Number of samples in each mesh element - prn_seed : int - Pseudorandom number generator (PRNG) seed; if None, one will be - generated randomly. + n_samples : int or 3-tuple of int + Total number of rays to sample. The number of rays in each direction + is determined by the aspect ratio of the mesh bounding box. When + specified as a 3-tuple, it is interpreted as the number of rays in + the x, y, and z dimensions. + max_materials : int, optional + Estimated maximum number of materials in any given mesh element. + output : bool, optional + Whether or not to show output. Returns ------- - List of tuple of (material, volume) for each mesh element. Void volume - is represented by having a value of None in the first element of a - tuple. + MeshMaterialVolumes + Dictionary-like object that maps material IDs to an array of volumes + equal in size to the number of mesh elements. """ - if n_samples <= 0: - raise ValueError("Number of samples must be positive") - if prn_seed is None: - prn_seed = getrandbits(63) - prn_seed = c_uint64(prn_seed) - - # Preallocate space for MaterialVolume results - size = 16 - result = (_MaterialVolume * size)() - - hits = c_int() # Number of materials hit in a given element - volumes = [] - for i_element in range(self.n_elements): - while True: - try: + if isinstance(n_samples, int): + # Determine number of rays in each direction based on aspect ratios + # and using the relation (nx*ny + ny*nz + nx*nz) = n_samples + width_x, width_y, width_z = self.bounding_box.width + ax = width_x / width_z + ay = width_y / width_z + f = sqrt(n_samples/(ax*ay + ax + ay)) + nx = round(f * ax) + ny = round(f * ay) + nz = round(f) + else: + nx, ny, nz = n_samples + + # Value indicating an empty slot in the hash table (matches C++) + EMPTY_SLOT = -2 + + # Preallocate arrays for material indices and volumes + n = self.n_elements + slot_factor = 2 + table_size = slot_factor*max_materials + materials = np.full((n, table_size), EMPTY_SLOT, dtype=np.int32) + volumes = np.zeros((n, table_size), dtype=np.float64) + + # Run material volume calculation + while True: + try: + with quiet_dll(output): _dll.openmc_mesh_material_volumes( - self._index, n_samples, i_element, size, result, hits, prn_seed) - except AllocationError: - # Increase size of result array and try again - size *= 2 - result = (_MaterialVolume * size)() - else: - # If no error, break out of loop - break + self._index, nx, ny, nz, table_size, materials, volumes) + except AllocationError: + # Increase size of result array and try again + table_size *= 2 + materials = np.full((n, table_size), EMPTY_SLOT, dtype=np.int32) + volumes = np.zeros((n, table_size), dtype=np.float64) + else: + # If no error, break out of loop + break - volumes.append([ - (Material(index=r.material), r.volume) - for r in result[:hits.value] - ]) - return volumes + return MeshMaterialVolumes(materials, volumes) def get_plot_bins( self, @@ -306,7 +324,7 @@ class RegularMesh(Mesh): The lower-left corner of the structured mesh. If only two coordinate are given, it is assumed that the mesh is an x-y mesh. upper_right : numpy.ndarray - The upper-right corner of the structrued mesh. If only two coordinate + The upper-right corner of the structured mesh. If only two coordinate are given, it is assumed that the mesh is an x-y mesh. width : numpy.ndarray The width of mesh cells in each direction. @@ -395,7 +413,7 @@ class RectilinearMesh(Mesh): lower_left : numpy.ndarray The lower-left corner of the structured mesh. upper_right : numpy.ndarray - The upper-right corner of the structrued mesh. + The upper-right corner of the structured mesh. width : numpy.ndarray The width of mesh cells in each direction. n_elements : int @@ -500,7 +518,7 @@ class CylindricalMesh(Mesh): lower_left : numpy.ndarray The lower-left corner of the structured mesh. upper_right : numpy.ndarray - The upper-right corner of the structrued mesh. + The upper-right corner of the structured mesh. width : numpy.ndarray The width of mesh cells in each direction. n_elements : int @@ -605,7 +623,7 @@ class SphericalMesh(Mesh): lower_left : numpy.ndarray The lower-left corner of the structured mesh. upper_right : numpy.ndarray - The upper-right corner of the structrued mesh. + The upper-right corner of the structured mesh. width : numpy.ndarray The width of mesh cells in each direction. n_elements : int @@ -719,7 +737,6 @@ def __getitem__(self, key): raise KeyError(str(e)) return _get_mesh(index.value) - def __iter__(self): for i in range(len(self)): yield _get_mesh(i).id diff --git a/openmc/mesh.py b/openmc/mesh.py index 4c594c9c6c2..08025f374ec 100644 --- a/openmc/mesh.py +++ b/openmc/mesh.py @@ -1,7 +1,7 @@ from __future__ import annotations import warnings from abc import ABC, abstractmethod -from collections.abc import Iterable, Sequence +from collections.abc import Iterable, Sequence, Mapping from functools import wraps from math import pi, sqrt, atan2 from numbers import Integral, Real @@ -21,6 +21,120 @@ from .utility_funcs import input_path +class MeshMaterialVolumes(Mapping): + """Results from a material volume in mesh calculation. + + This class provides multiple ways of accessing information about material + volumes in individual mesh elements. First, the class behaves like a + dictionary that maps material IDs to an array of volumes equal in size to + the number of mesh elements. Second, the class provides a :meth:`by_element` + method that gives all the material volumes for a specific mesh element. + + .. versionadded:: 0.15.1 + + Parameters + ---------- + materials : numpy.ndarray + Array of shape (elements, max_materials) storing material IDs + volumes : numpy.ndarray + Array of shape (elements, max_materials) storing material volumes + + See Also + -------- + openmc.MeshBase.material_volumes + + Examples + -------- + If you want to get the volume of a specific material in every mesh element, + index the object with the material ID: + + >>> volumes = mesh.material_volumes(...) + >>> volumes + {1: <32121 nonzero volumes> + 2: <338186 nonzero volumes> + 3: <49120 nonzero volumes>} + + If you want the volume of all materials in a specific mesh element, use the + :meth:`by_element` method: + + >>> volumes = mesh.material_volumes(...) + >>> volumes.by_element(42) + [(2, 31.87963824195591), (1, 6.129949130817542)] + + """ + def __init__(self, materials: np.ndarray, volumes: np.ndarray): + self._materials = materials + self._volumes = volumes + + @property + def num_elements(self) -> int: + return self._volumes.shape[0] + + def __iter__(self): + for mat in np.unique(self._materials): + if mat > 0: + yield mat + + def __len__(self) -> int: + return (np.unique(self._materials) > 0).sum() + + def __repr__(self) -> str: + ids, counts = np.unique(self._materials, return_counts=True) + return '{' + '\n '.join( + f'{id}: <{count} nonzero volumes>' for id, count in zip(ids, counts) if id > 0) + '}' + + def __getitem__(self, material_id: int) -> np.ndarray: + volumes = np.zeros(self.num_elements) + for i in range(self._volumes.shape[1]): + indices = (self._materials[:, i] == material_id) + volumes[indices] = self._volumes[indices, i] + return volumes + + def by_element(self, index_elem: int) -> list[tuple[int | None, float]]: + """Get a list of volumes for each material within a specific element. + + Parameters + ---------- + index_elem : int + Mesh element index + + Returns + ------- + list of tuple of (material ID, volume) + + """ + table_size = self._volumes.shape[1] + return [ + (m if m > -1 else None, self._volumes[index_elem, i]) + for i in range(table_size) + if (m := self._materials[index_elem, i]) != -2 + ] + + def save(self, filename: PathLike): + """Save material volumes to a .npz file. + + Parameters + ---------- + filename : path-like + Filename where data will be saved + """ + np.savez_compressed( + filename, materials=self._materials, volumes=self._volumes) + + @classmethod + def from_npz(cls, filename: PathLike) -> MeshMaterialVolumes: + """Generate material volumes from a .npz file + + Parameters + ---------- + filename : path-like + File where data will be read from + + """ + filedata = np.load(filename) + return cls(filedata['materials'], filedata['volumes']) + + class MeshBase(IDManagerMixin, ABC): """A mesh that partitions geometry for tallying purposes. @@ -170,8 +284,7 @@ def from_xml_element(cls, elem: ET.Element): def get_homogenized_materials( self, model: openmc.Model, - n_samples: int = 10_000, - prn_seed: int | None = None, + n_samples: int | tuple[int, int, int] = 10_000, include_void: bool = True, **kwargs ) -> list[openmc.Material]: @@ -184,15 +297,15 @@ def get_homogenized_materials( model : openmc.Model Model containing materials to be homogenized and the associated geometry. - n_samples : int - Number of samples in each mesh element. - prn_seed : int, optional - Pseudorandom number generator (PRNG) seed; if None, one will be - generated randomly. + n_samples : int or 2-tuple of int + Total number of rays to sample. The number of rays in each direction + is determined by the aspect ratio of the mesh bounding box. When + specified as a 3-tuple, it is interpreted as the number of rays in + the x, y, and z dimensions. include_void : bool, optional Whether homogenization should include voids. **kwargs - Keyword-arguments passed to :func:`openmc.lib.init`. + Keyword-arguments passed to :meth:`MeshBase.material_volumes`. Returns ------- @@ -200,34 +313,8 @@ def get_homogenized_materials( Homogenized material in each mesh element """ - import openmc.lib - - with change_directory(tmpdir=True): - # In order to get mesh into model, we temporarily replace the - # tallies with a single mesh tally using the current mesh - original_tallies = model.tallies - new_tally = openmc.Tally() - new_tally.filters = [openmc.MeshFilter(self)] - new_tally.scores = ['flux'] - model.tallies = [new_tally] - - # Export model to XML - model.export_to_model_xml() - - # Get material volume fractions - openmc.lib.init(**kwargs) - mesh = openmc.lib.tallies[new_tally.id].filters[0].mesh - mat_volume_by_element = [ - [ - (mat.id if mat is not None else None, volume) - for mat, volume in mat_volume_list - ] - for mat_volume_list in mesh.material_volumes(n_samples, prn_seed) - ] - openmc.lib.finalize() - - # Restore original tallies - model.tallies = original_tallies + vols = self.material_volumes(model, n_samples, **kwargs) + mat_volume_by_element = [vols.by_element(i) for i in range(vols.num_elements)] # Create homogenized material for each element materials = model.geometry.get_all_materials() @@ -274,6 +361,72 @@ def get_homogenized_materials( return homogenized_materials + def material_volumes( + self, + model: openmc.Model, + n_samples: int | tuple[int, int, int] = 10_000, + max_materials: int = 4, + **kwargs + ) -> MeshMaterialVolumes: + """Determine volume of materials in each mesh element. + + This method works by raytracing repeatedly through the mesh to count the + estimated volume of each material in all mesh elements. Three sets of + rays are used: one set parallel to the x-axis, one parallel to the + y-axis, and one parallel to the z-axis. + + .. versionadded:: 0.15.1 + + Parameters + ---------- + model : openmc.Model + Model containing materials. + n_samples : int or 3-tuple of int + Total number of rays to sample. The number of rays in each direction + is determined by the aspect ratio of the mesh bounding box. When + specified as a 3-tuple, it is interpreted as the number of rays in + the x, y, and z dimensions. + max_materials : int, optional + Estimated maximum number of materials in any given mesh element. + **kwargs : dict + Keyword arguments passed to :func:`openmc.lib.init` + + Returns + ------- + Dictionary-like object that maps material IDs to an array of volumes + equal in size to the number of mesh elements. + + """ + import openmc.lib + + with change_directory(tmpdir=True): + # In order to get mesh into model, we temporarily replace the + # tallies with a single mesh tally using the current mesh + original_tallies = model.tallies + new_tally = openmc.Tally() + new_tally.filters = [openmc.MeshFilter(self)] + new_tally.scores = ['flux'] + model.tallies = [new_tally] + + # Export model to XML + model.export_to_model_xml() + + # Get material volume fractions + kwargs.setdefault('output', True) + if 'args' in kwargs: + kwargs['args'] = ['-c'] + kwargs['args'] + kwargs.setdefault('args', ['-c']) + openmc.lib.init(**kwargs) + mesh = openmc.lib.tallies[new_tally.id].filters[0].mesh + volumes = mesh.material_volumes( + n_samples, max_materials, output=kwargs['output']) + openmc.lib.finalize() + + # Restore original tallies + model.tallies = original_tallies + + return volumes + class StructuredMesh(MeshBase): """A base class for structured mesh functionality diff --git a/src/mesh.cpp b/src/mesh.cpp index 3e1f25c4b27..a24b51a0be2 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -6,10 +6,15 @@ #include // for size_t #include +#ifdef _MSC_VER +#include // for _InterlockedCompareExchange +#endif + #ifdef OPENMC_MPI #include "mpi.h" #endif +#include "xtensor/xadapt.hpp" #include "xtensor/xbuilder.hpp" #include "xtensor/xeval.hpp" #include "xtensor/xmath.hpp" @@ -29,13 +34,16 @@ #include "openmc/memory.h" #include "openmc/message_passing.h" #include "openmc/openmp_interface.h" +#include "openmc/output.h" #include "openmc/particle_data.h" #include "openmc/plot.h" #include "openmc/random_dist.h" #include "openmc/search.h" #include "openmc/settings.h" +#include "openmc/string_utils.h" #include "openmc/tallies/filter.h" #include "openmc/tallies/tally.h" +#include "openmc/timer.h" #include "openmc/volume_calc.h" #include "openmc/xml_interface.h" @@ -102,6 +110,113 @@ inline bool check_intersection_point(double x1, double x0, double y1, double y0, return false; } +//! Atomic compare-and-swap for signed 32-bit integer +// +//! \param[in,out] ptr Pointer to value to update +//! \param[in] expected Value to compare to +//! \param[in] desired If comparison is successful, value to update to +//! \return True if the comparison was successful and the value was updated +inline bool atomic_cas_int32(int32_t* ptr, int32_t& expected, int32_t desired) +{ +#if defined(__GNUC__) || defined(__clang__) + // For gcc/clang, use the __atomic_compare_exchange_n intrinsic + return __atomic_compare_exchange_n( + ptr, &expected, desired, false, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST); + +#elif defined(_MSC_VER) + // For MSVC, use the _InterlockedCompareExchange intrinsic + int32_t old_val = + _InterlockedCompareExchange(reinterpret_cast(ptr), + static_cast(desired), static_cast(expected)); + return (old_val == expected); + +#else +#error "No compare-and-swap implementation available for this compiler." +#endif +} + +namespace detail { + +//============================================================================== +// MaterialVolumes implementation +//============================================================================== + +void MaterialVolumes::add_volume( + int index_elem, int index_material, double volume) +{ + // This method handles adding elements to the materials hash table, + // implementing open addressing with linear probing. Consistency across + // multiple threads is handled by with an atomic compare-and-swap operation. + // Ideally, we would use #pragma omp atomic compare, but it was introduced in + // OpenMP 5.1 and is not widely supported yet. + + // Loop for linear probing + for (int attempt = 0; attempt < table_size_; ++attempt) { + // Determine slot to check + int slot = (index_material + attempt) % table_size_; + int32_t* slot_ptr = &this->materials(index_elem, slot); + + // Non-atomic read of current material + int32_t current_val = *slot_ptr; + + // Found the desired material; accumulate volume + if (current_val == index_material) { +#pragma omp atomic + this->volumes(index_elem, slot) += volume; + return; + } + + // Slot appears to be empty; attempt to claim + if (current_val == EMPTY) { + // Attempt compare-and-swap from EMPTY to index_material + int32_t expected_val = EMPTY; + bool claimed_slot = + atomic_cas_int32(slot_ptr, expected_val, index_material); + + // If we claimed the slot or another thread claimed it but the same + // material was inserted, proceed to accumulate + if (claimed_slot || (expected_val == index_material)) { +#pragma omp atomic + this->volumes(index_elem, slot) += volume; + return; + } + } + } + + // If table is full, set a flag that can be checked later + table_full_ = true; +} + +void MaterialVolumes::add_volume_unsafe( + int index_elem, int index_material, double volume) +{ + // Linear probe + for (int attempt = 0; attempt < table_size_; ++attempt) { + int slot = (index_material + attempt) % table_size_; + + // Read current material + int32_t current_val = this->materials(index_elem, slot); + + // Found the desired material; accumulate volume + if (current_val == index_material) { + this->volumes(index_elem, slot) += volume; + return; + } + + // Claim empty slot + if (current_val == EMPTY) { + this->materials(index_elem, slot) = index_material; + this->volumes(index_elem, slot) += volume; + return; + } + } + + // If table is full, set a flag that can be checked later + table_full_ = true; +} + +} // namespace detail + //============================================================================== // Mesh implementation //============================================================================== @@ -153,90 +268,235 @@ vector Mesh::volumes() const return volumes; } -int Mesh::material_volumes( - int n_sample, int bin, span result, uint64_t* seed) const +void Mesh::material_volumes(int nx, int ny, int nz, int table_size, + int32_t* materials, double* volumes) const { - vector materials; - vector hits; + if (mpi::master) { + header("MESH MATERIAL VOLUMES CALCULATION", 7); + } + write_message(7, "Number of rays (x) = {}", nx); + write_message(7, "Number of rays (y) = {}", ny); + write_message(7, "Number of rays (z) = {}", nz); + int64_t n_total = nx * ny + ny * nz + nx * nz; + write_message(7, "Total number of rays = {}", n_total); + write_message( + 7, "Maximum number of materials per mesh element = {}", table_size); -#pragma omp parallel - { - vector local_materials; - vector local_hits; - GeometryState geom; + Timer timer; + timer.start(); -#pragma omp for - for (int i = 0; i < n_sample; ++i) { - // Get seed for i-th sample - uint64_t seed_i = future_seed(3 * i, *seed); - - // Sample position and set geometry state - geom.r() = this->sample_element(bin, &seed_i); - geom.u() = {1., 0., 0.}; - geom.n_coord() = 1; - - // If this location is not in the geometry at all, move on to next block - if (!exhaustive_find_cell(geom)) - continue; - - int i_material = geom.material(); - - // Check if this material was previously hit and if so, increment count - auto it = - std::find(local_materials.begin(), local_materials.end(), i_material); - if (it == local_materials.end()) { - local_materials.push_back(i_material); - local_hits.push_back(1); - } else { - local_hits[it - local_materials.begin()]++; - } - } // omp for + // Create object for keeping track of materials/volumes + detail::MaterialVolumes result(materials, volumes, table_size); - // Reduce index/hits lists from each thread into a single copy - reduce_indices_hits(local_materials, local_hits, materials, hits); - } // omp parallel + // Determine bounding box + auto bbox = this->bounding_box(); - // Advance RNG seed - advance_prn_seed(3 * n_sample, seed); + std::array n_rays = {nx, ny, nz}; - // Make sure span passed in is large enough - if (hits.size() > result.size()) { - return -1; + // Determine effective width of rays + Position width((bbox.xmax - bbox.xmin) / nx, (bbox.ymax - bbox.ymin) / ny, + (bbox.zmax - bbox.zmin) / nz); + + // Set flag for mesh being contained within model + bool out_of_model = false; + +#pragma omp parallel + { + // Preallocate vector for mesh indices and length fractions and particle + std::vector bins; + std::vector length_fractions; + Particle p; + + SourceSite site; + site.E = 1.0; + site.particle = ParticleType::neutron; + + for (int axis = 0; axis < 3; ++axis) { + // Set starting position and direction + site.r = {0.0, 0.0, 0.0}; + site.r[axis] = bbox.min()[axis]; + site.u = {0.0, 0.0, 0.0}; + site.u[axis] = 1.0; + + // Determine width of rays and number of rays in other directions + int ax1 = (axis + 1) % 3; + int ax2 = (axis + 2) % 3; + double min1 = bbox.min()[ax1]; + double min2 = bbox.min()[ax2]; + double d1 = width[ax1]; + double d2 = width[ax2]; + int n1 = n_rays[ax1]; + int n2 = n_rays[ax2]; + + // Divide rays in first direction over MPI processes by computing starting + // and ending indices + int min_work = n1 / mpi::n_procs; + int remainder = n1 % mpi::n_procs; + int n1_local = (mpi::rank < remainder) ? min_work + 1 : min_work; + int i1_start = mpi::rank * min_work + std::min(mpi::rank, remainder); + int i1_end = i1_start + n1_local; + + // Loop over rays on face of bounding box +#pragma omp for collapse(2) + for (int i1 = i1_start; i1 < i1_end; ++i1) { + for (int i2 = 0; i2 < n2; ++i2) { + site.r[ax1] = min1 + (i1 + 0.5) * d1; + site.r[ax2] = min2 + (i2 + 0.5) * d2; + + p.from_source(&site); + + // Determine particle's location + if (!exhaustive_find_cell(p)) { + out_of_model = true; + continue; + } + + // Set birth cell attribute + if (p.cell_born() == C_NONE) + p.cell_born() = p.lowest_coord().cell; + + // Initialize last cells from current cell + for (int j = 0; j < p.n_coord(); ++j) { + p.cell_last(j) = p.coord(j).cell; + } + p.n_coord_last() = p.n_coord(); + + while (true) { + // Ray trace from r_start to r_end + Position r0 = p.r(); + double max_distance = bbox.max()[axis] - r0[axis]; + + // Find the distance to the nearest boundary + BoundaryInfo boundary = distance_to_boundary(p); + + // Advance particle forward + double distance = std::min(boundary.distance, max_distance); + p.move_distance(distance); + + // Determine what mesh elements were crossed by particle + bins.clear(); + length_fractions.clear(); + this->bins_crossed(r0, p.r(), p.u(), bins, length_fractions); + + // Add volumes to any mesh elements that were crossed + int i_material = p.material(); + if (i_material != C_NONE) { + i_material = model::materials[i_material]->id(); + } + for (int i_bin = 0; i_bin < bins.size(); i_bin++) { + int mesh_index = bins[i_bin]; + double length = distance * length_fractions[i_bin]; + + // Add volume to result + result.add_volume(mesh_index, i_material, length * d1 * d2); + } + + if (distance == max_distance) + break; + + // cross next geometric surface + for (int j = 0; j < p.n_coord(); ++j) { + p.cell_last(j) = p.coord(j).cell; + } + p.n_coord_last() = p.n_coord(); + + // Set surface that particle is on and adjust coordinate levels + p.surface() = boundary.surface; + p.n_coord() = boundary.coord_level; + + if (boundary.lattice_translation[0] != 0 || + boundary.lattice_translation[1] != 0 || + boundary.lattice_translation[2] != 0) { + // Particle crosses lattice boundary + cross_lattice(p, boundary); + } else { + // Particle crosses surface + const auto& surf {model::surfaces[p.surface_index()].get()}; + p.cross_surface(*surf); + } + } + } + } + } } - // Convert hits to fractions - for (int i_mat = 0; i_mat < hits.size(); ++i_mat) { - double fraction = double(hits[i_mat]) / n_sample; - result[i_mat].material = materials[i_mat]; - result[i_mat].volume = fraction * this->volume(bin); + // Check for errors + if (out_of_model) { + throw std::runtime_error("Mesh not fully contained in geometry."); + } else if (result.table_full()) { + throw std::runtime_error("Maximum number of materials for mesh material " + "volume calculation insufficient."); } - return hits.size(); -} -vector Mesh::material_volumes( - int n_sample, int bin, uint64_t* seed) const -{ - // Create result vector with space for 8 pairs - vector result; - result.reserve(8); + // Compute time for raytracing + double t_raytrace = timer.elapsed(); - int size = -1; - while (true) { - // Get material volumes - size = this->material_volumes( - n_sample, bin, {result.data(), result.data() + result.capacity()}, seed); - - // If capacity was sufficient, resize the vector and return - if (size >= 0) { - result.resize(size); - break; +#ifdef OPENMC_MPI + // Combine results from multiple MPI processes + if (mpi::n_procs > 1) { + int total = this->n_bins() * table_size; + if (mpi::master) { + // Allocate temporary buffer for receiving data + std::vector mats(total); + std::vector vols(total); + + for (int i = 1; i < mpi::n_procs; ++i) { + // Receive material indices and volumes from process i + MPI_Recv( + mats.data(), total, MPI_INT, i, i, mpi::intracomm, MPI_STATUS_IGNORE); + MPI_Recv(vols.data(), total, MPI_DOUBLE, i, i, mpi::intracomm, + MPI_STATUS_IGNORE); + + // Combine with existing results; we can call thread unsafe version of + // add_volume because each thread is operating on a different element +#pragma omp for + for (int index_elem = 0; index_elem < n_bins(); ++index_elem) { + for (int k = 0; k < table_size; ++k) { + int index = index_elem * table_size + k; + result.add_volume_unsafe(index_elem, mats[index], vols[index]); + } + } + } + } else { + // Send material indices and volumes to process 0 + MPI_Send(materials, total, MPI_INT, 0, mpi::rank, mpi::intracomm); + MPI_Send(volumes, total, MPI_DOUBLE, 0, mpi::rank, mpi::intracomm); } + } + + // Report time for MPI communication + double t_mpi = timer.elapsed() - t_raytrace; +#else + double t_mpi = 0.0; +#endif - // Otherwise, increase capacity of the vector - result.reserve(2 * result.capacity()); + // Normalize based on known volumes of elements + for (int i = 0; i < this->n_bins(); ++i) { + // Estimated total volume in element i + double volume = 0.0; + for (int j = 0; j < table_size; ++j) { + volume += result.volumes(i, j); + } + // Renormalize volumes based on known volume of element i + double norm = this->volume(i) / volume; + for (int j = 0; j < table_size; ++j) { + result.volumes(i, j) *= norm; + } } - return result; + // Show elapsed time + timer.stop(); + double t_total = timer.elapsed(); + double t_normalize = t_total - t_raytrace - t_mpi; + if (mpi::master) { + header("Timing Statistics", 7); + show_time("Total time elapsed", t_total); + show_time("Ray tracing", t_raytrace, 1); + show_time("Ray tracing (per ray)", t_raytrace / n_total, 1); + show_time("MPI communication", t_mpi, 1); + show_time("Normalization", t_normalize, 1); + std::fflush(stdout); + } } void Mesh::to_hdf5(hid_t group) const @@ -616,8 +876,8 @@ xt::xtensor StructuredMesh::count_sites( } // raytrace through the mesh. The template class T will do the tallying. -// A modern optimizing compiler can recognize the noop method of T and eleminate -// that call entirely. +// A modern optimizing compiler can recognize the noop method of T and +// eliminate that call entirely. template void StructuredMesh::raytrace_mesh( Position r0, Position r1, const Direction& u, T tally) const @@ -685,7 +945,8 @@ void StructuredMesh::raytrace_mesh( if (traveled_distance >= total_distance) return; - // If we have not reached r1, we have hit a surface. Tally outward current + // If we have not reached r1, we have hit a surface. Tally outward + // current tally.surface(ijk, k, distances[k].max_surface, false); // Update cell and calculate distance to next surface in k-direction. @@ -697,15 +958,16 @@ void StructuredMesh::raytrace_mesh( // Check if we have left the interior of the mesh in_mesh = ((ijk[k] >= 1) && (ijk[k] <= shape_[k])); - // If we are still inside the mesh, tally inward current for the next cell + // If we are still inside the mesh, tally inward current for the next + // cell if (in_mesh) tally.surface(ijk, k, !distances[k].max_surface, true); } else { // not inside mesh - // For all directions outside the mesh, find the distance that we need to - // travel to reach the next surface. Use the largest distance, as only - // this will cross all outer surfaces. + // For all directions outside the mesh, find the distance that we need + // to travel to reach the next surface. Use the largest distance, as + // only this will cross all outer surfaces. int k_max {0}; for (int k = 0; k < n; ++k) { if ((ijk[k] < 1 || ijk[k] > shape_[k]) && @@ -719,7 +981,8 @@ void StructuredMesh::raytrace_mesh( if (traveled_distance >= total_distance) return; - // Calculate the new cell index and update all distances to next surfaces. + // Calculate the new cell index and update all distances to next + // surfaces. ijk = get_indices(r0 + (traveled_distance + TINY_BIT) * u, in_mesh); for (int k = 0; k < n; ++k) { distances[k] = @@ -1581,7 +1844,8 @@ double SphericalMesh::find_theta_crossing( const double b = r.dot(u) * cos_t_2 - r.z * u.z; const double c = r.dot(r) * cos_t_2 - r.z * r.z; - // if factor of s^2 is zero, direction of flight is parallel to theta surface + // if factor of s^2 is zero, direction of flight is parallel to theta + // surface if (std::abs(a) < FP_PRECISION) { // if b vanishes, direction of flight is within theta surface and crossing // is not possible @@ -1589,7 +1853,8 @@ double SphericalMesh::find_theta_crossing( return INFTY; const double s = -0.5 * c / b; - // Check if solution is in positive direction of flight and has correct sign + // Check if solution is in positive direction of flight and has correct + // sign if ((s > l) && (std::signbit(r.z + s * u.z) == sgn)) return s; @@ -1943,22 +2208,25 @@ extern "C" int openmc_mesh_bounding_box(int32_t index, double* ll, double* ur) return 0; } -extern "C" int openmc_mesh_material_volumes(int32_t index, int n_sample, - int bin, int result_size, void* result, int* hits, uint64_t* seed) +extern "C" int openmc_mesh_material_volumes(int32_t index, int nx, int ny, + int nz, int table_size, int32_t* materials, double* volumes) { - auto result_ = reinterpret_cast(result); - if (!result_) { - set_errmsg("Invalid result pointer passed to openmc_mesh_material_volumes"); - return OPENMC_E_INVALID_ARGUMENT; - } - if (int err = check_mesh(index)) return err; - int n = model::meshes[index]->material_volumes( - n_sample, bin, {result_, result_ + result_size}, seed); - *hits = n; - return (n == -1) ? OPENMC_E_ALLOCATE : 0; + try { + model::meshes[index]->material_volumes( + nx, ny, nz, table_size, materials, volumes); + } catch (const std::exception& e) { + set_errmsg(e.what()); + if (starts_with(e.what(), "Mesh")) { + return OPENMC_E_GEOMETRY; + } else { + return OPENMC_E_ALLOCATE; + } + } + + return 0; } extern "C" int openmc_mesh_get_plot_bins(int32_t index, Position origin, @@ -2394,7 +2662,8 @@ void MOABMesh::intersect_track(const moab::CartVect& start, moab::ErrorCode rval; vector tris; // get all intersections with triangles in the tet mesh - // (distances are relative to the start point, not the previous intersection) + // (distances are relative to the start point, not the previous + // intersection) rval = kdtree_->ray_intersect_triangles(kdtree_root_, FP_COINCIDENT, dir.array(), start.array(), tris, hits, 0, track_len); if (rval != moab::MB_SUCCESS) { @@ -2975,8 +3244,8 @@ void LibMesh::build_eqn_sys() void LibMesh::initialize() { if (!settings::libmesh_comm) { - fatal_error( - "Attempting to use an unstructured mesh without a libMesh communicator."); + fatal_error("Attempting to use an unstructured mesh without a libMesh " + "communicator."); } // assuming that unstructured meshes used in OpenMC are 3D @@ -3276,8 +3545,8 @@ void read_meshes(pugi::xml_node root) // Check to make sure multiple meshes in the same file don't share IDs int id = std::stoi(get_node_value(node, "id")); if (contains(mesh_ids, id)) { - fatal_error(fmt::format( - "Two or more meshes use the same unique ID '{}' in the same input file", + fatal_error(fmt::format("Two or more meshes use the same unique ID " + "'{}' in the same input file", id)); } mesh_ids.insert(id); diff --git a/src/particle.cpp b/src/particle.cpp index c63c846741c..c51011d6b7f 100644 --- a/src/particle.cpp +++ b/src/particle.cpp @@ -293,7 +293,6 @@ void Particle::event_cross_surface() event() = TallyEvent::LATTICE; } else { // Particle crosses surface - // TODO: off-by-one const auto& surf {model::surfaces[surface_index()].get()}; // If BC, add particle to surface source before crossing surface if (surf->surf_source_ && surf->bc_) { diff --git a/tests/unit_tests/test_lib.py b/tests/unit_tests/test_lib.py index 64c16c238ea..8ab35335fc0 100644 --- a/tests/unit_tests/test_lib.py +++ b/tests/unit_tests/test_lib.py @@ -601,18 +601,18 @@ def test_regular_mesh(lib_init): mesh.set_parameters(lower_left=(-0.63, -0.63, -0.5), upper_right=(0.63, 0.63, 0.5)) vols = mesh.material_volumes() - assert len(vols) == 4 - for elem_vols in vols: + assert vols.num_elements == 4 + for i in range(vols.num_elements): + elem_vols = vols.by_element(i) assert sum(f[1] for f in elem_vols) == pytest.approx(1.26 * 1.26 / 4) - # If the mesh extends beyond the boundaries of the model, the volumes should - # still be reported correctly + # If the mesh extends beyond the boundaries of the model, we should get a + # GeometryError mesh.dimension = (1, 1, 1) mesh.set_parameters(lower_left=(-1.0, -1.0, -0.5), upper_right=(1.0, 1.0, 0.5)) - vols = mesh.material_volumes(100_000) - for elem_vols in vols: - assert sum(f[1] for f in elem_vols) == pytest.approx(1.26 * 1.26, 1e-2) + with pytest.raises(exc.GeometryError, match="not fully contained"): + vols = mesh.material_volumes() def test_regular_mesh_get_plot_bins(lib_init): @@ -683,11 +683,11 @@ def test_rectilinear_mesh(lib_init): mesh.set_grid([-w/2, -w/4, w/2], [-w/2, -w/4, w/2], [-0.5, 0.5]) vols = mesh.material_volumes() - assert len(vols) == 4 - assert sum(f[1] for f in vols[0]) == pytest.approx(w/4 * w/4) - assert sum(f[1] for f in vols[1]) == pytest.approx(w/4 * 3*w/4) - assert sum(f[1] for f in vols[2]) == pytest.approx(3*w/4 * w/4) - assert sum(f[1] for f in vols[3]) == pytest.approx(3*w/4 * 3*w/4) + assert vols.num_elements == 4 + assert sum(f[1] for f in vols.by_element(0)) == pytest.approx(w/4 * w/4) + assert sum(f[1] for f in vols.by_element(1)) == pytest.approx(w/4 * 3*w/4) + assert sum(f[1] for f in vols.by_element(2)) == pytest.approx(3*w/4 * w/4) + assert sum(f[1] for f in vols.by_element(3)) == pytest.approx(3*w/4 * 3*w/4) def test_cylindrical_mesh(lib_init): @@ -737,11 +737,11 @@ def test_cylindrical_mesh(lib_init): mesh.set_grid(r_grid, phi_grid, z_grid) vols = mesh.material_volumes() - assert len(vols) == 6 + assert vols.num_elements == 6 for i in range(0, 6, 2): - assert sum(f[1] for f in vols[i]) == pytest.approx(pi * 0.25**2 / 3) + assert sum(f[1] for f in vols.by_element(i)) == pytest.approx(pi * 0.25**2 / 3) for i in range(1, 6, 2): - assert sum(f[1] for f in vols[i]) == pytest.approx(pi * (0.5**2 - 0.25**2) / 3) + assert sum(f[1] for f in vols.by_element(i)) == pytest.approx(pi * (0.5**2 - 0.25**2) / 3) def test_spherical_mesh(lib_init): @@ -795,14 +795,14 @@ def test_spherical_mesh(lib_init): mesh.set_grid(r_grid, theta_grid, phi_grid) vols = mesh.material_volumes() - assert len(vols) == 12 + assert vols.num_elements == 12 d_theta = theta_grid[1] - theta_grid[0] d_phi = phi_grid[1] - phi_grid[0] for i in range(0, 12, 2): - assert sum(f[1] for f in vols[i]) == pytest.approx( + assert sum(f[1] for f in vols.by_element(i)) == pytest.approx( 0.25**3 / 3 * d_theta * d_phi * 2/pi) for i in range(1, 12, 2): - assert sum(f[1] for f in vols[i]) == pytest.approx( + assert sum(f[1] for f in vols.by_element(i)) == pytest.approx( (0.5**3 - 0.25**3) / 3 * d_theta * d_phi * 2/pi) diff --git a/tests/unit_tests/test_mesh.py b/tests/unit_tests/test_mesh.py index 3f307dda820..f1f5e9f6796 100644 --- a/tests/unit_tests/test_mesh.py +++ b/tests/unit_tests/test_mesh.py @@ -1,4 +1,5 @@ from math import pi +from tempfile import TemporaryDirectory from pathlib import Path import numpy as np @@ -505,7 +506,7 @@ def test_mesh_get_homogenized_materials(): mesh.lower_left = (-1., -1., -1.) mesh.upper_right = (1., 1., 1.) mesh.dimension = (3, 1, 1) - m1, m2, m3 = mesh.get_homogenized_materials(model, n_samples=1_000_000) + m1, m2, m3 = mesh.get_homogenized_materials(model, n_samples=10_000) # Left mesh element should be only Fe56 assert m1.get_mass_density('Fe56') == pytest.approx(5.0) @@ -521,7 +522,7 @@ def test_mesh_get_homogenized_materials(): mesh_void.lower_left = (0.5, 0.5, -1.) mesh_void.upper_right = (1.5, 1.5, 1.) mesh_void.dimension = (1, 1, 1) - m4, = mesh_void.get_homogenized_materials(model, n_samples=1_000_000) + m4, = mesh_void.get_homogenized_materials(model, n_samples=(100, 100, 0)) # Mesh element that overlaps void should have half density assert m4.get_mass_density('H1') == pytest.approx(0.5, rel=1e-2) @@ -531,3 +532,86 @@ def test_mesh_get_homogenized_materials(): m5, = mesh_void.get_homogenized_materials( model, n_samples=1000, include_void=False) assert m5.get_mass_density('H1') == pytest.approx(1.0) + + +@pytest.fixture +def sphere_model(): + # Model with three materials separated by planes x=0 and z=0 + mats = [] + for i in range(3): + mat = openmc.Material() + mat.add_nuclide('H1', 1.0) + mat.set_density('g/cm3', float(i + 1)) + mats.append(mat) + + sph = openmc.Sphere(r=25.0, boundary_type='vacuum') + x0 = openmc.XPlane(0.0) + z0 = openmc.ZPlane(0.0) + cell1 = openmc.Cell(fill=mats[0], region=-sph & +x0 & +z0) + cell2 = openmc.Cell(fill=mats[1], region=-sph & -x0 & +z0) + cell3 = openmc.Cell(fill=mats[2], region=-sph & -z0) + model = openmc.Model() + model.geometry = openmc.Geometry([cell1, cell2, cell3]) + model.materials = openmc.Materials(mats) + return model + + +@pytest.mark.parametrize("n_rays", [1000, (10, 10, 0), (10, 0, 10), (0, 10, 10)]) +def test_material_volumes_regular_mesh(sphere_model, n_rays): + """Test the material_volumes method on a regular mesh""" + mesh = openmc.RegularMesh() + mesh.lower_left = (-1., -1., -1.) + mesh.upper_right = (1., 1., 1.) + mesh.dimension = (2, 2, 2) + volumes = mesh.material_volumes(sphere_model, n_rays) + mats = sphere_model.materials + np.testing.assert_almost_equal(volumes[mats[0].id], [0., 0., 0., 0., 0., 1., 0., 1.]) + np.testing.assert_almost_equal(volumes[mats[1].id], [0., 0., 0., 0., 1., 0., 1., 0.]) + np.testing.assert_almost_equal(volumes[mats[2].id], [1., 1., 1., 1., 0., 0., 0., 0.]) + assert volumes.by_element(4) == [(mats[1].id, 1.)] + assert volumes.by_element(0) == [(mats[2].id, 1.)] + + +def test_material_volumes_cylindrical_mesh(sphere_model): + """Test the material_volumes method on a cylindrical mesh""" + cyl_mesh = openmc.CylindricalMesh( + [0., 1.], [-1., 0., 1.,], [0.0, pi/4, 3*pi/4, 5*pi/4, 7*pi/4, 2*pi]) + volumes = cyl_mesh.material_volumes(sphere_model, (0, 100, 100)) + mats = sphere_model.materials + np.testing.assert_almost_equal(volumes[mats[0].id], [ + 0., 0., 0., 0., 0., + pi/8, pi/8, 0., pi/8, pi/8 + ]) + np.testing.assert_almost_equal(volumes[mats[1].id], [ + 0., 0., 0., 0., 0., + 0., pi/8, pi/4, pi/8, 0. + ]) + np.testing.assert_almost_equal(volumes[mats[2].id], [ + pi/8, pi/4, pi/4, pi/4, pi/8, + 0., 0., 0., 0., 0. + ]) + + +def test_mesh_material_volumes_serialize(): + materials = np.array([ + [1, -1, -2], + [-1, -2, -2], + [2, 1, -2], + [2, -2, -2] + ]) + volumes = np.array([ + [0.5, 0.5, 0.0], + [1.0, 0.0, 0.0], + [0.5, 0.5, 0.0], + [1.0, 0.0, 0.0] + ]) + volumes = openmc.MeshMaterialVolumes(materials, volumes) + with TemporaryDirectory() as tmpdir: + path = f'{tmpdir}/volumes.npz' + volumes.save(path) + new_volumes = openmc.MeshMaterialVolumes.from_npz(path) + + assert new_volumes.by_element(0) == [(1, 0.5), (None, 0.5)] + assert new_volumes.by_element(1) == [(None, 1.0)] + assert new_volumes.by_element(2) == [(2, 0.5), (1, 0.5)] + assert new_volumes.by_element(3) == [(2, 1.0)] From c26fde6665594620720c8d586698e18bbbb137ad Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Fri, 28 Feb 2025 15:00:40 +0100 Subject: [PATCH 171/184] Adding per kg as unit option on material functions (#3329) Co-authored-by: Jon Shimwell --- openmc/material.py | 28 +++++++++++++++++----------- tests/unit_tests/test_material.py | 5 +++++ 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/openmc/material.py b/openmc/material.py index 64458f57133..41780ce3607 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -300,7 +300,7 @@ def get_decay_photon_energy( clip_tolerance : float Maximum fraction of :math:`\sum_i x_i p_i` for discrete distributions that will be discarded. - units : {'Bq', 'Bq/g', 'Bq/cm3'} + units : {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3'} Specifies the units on the integral of the distribution. volume : float, optional Volume of the material. If not passed, defaults to using the @@ -313,7 +313,7 @@ def get_decay_photon_energy( is the total intensity of the photon source in the requested units. """ - cv.check_value('units', units, {'Bq', 'Bq/g', 'Bq/cm3'}) + cv.check_value('units', units, {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3'}) if units == 'Bq': multiplier = volume if volume is not None else self.volume if multiplier is None: @@ -322,6 +322,8 @@ def get_decay_photon_energy( multiplier = 1 elif units == 'Bq/g': multiplier = 1.0 / self.get_mass_density() + elif units == 'Bq/kg': + multiplier = 1000.0 / self.get_mass_density() dists = [] probs = [] @@ -1132,16 +1134,16 @@ def get_element_atom_densities(self, element: str | None = None) -> dict[str, fl def get_activity(self, units: str = 'Bq/cm3', by_nuclide: bool = False, volume: float | None = None) -> dict[str, float] | float: """Returns the activity of the material or for each nuclide in the - material in units of [Bq], [Bq/g] or [Bq/cm3]. + material in units of [Bq], [Bq/g], [Bq/kg] or [Bq/cm3]. .. versionadded:: 0.13.1 Parameters ---------- - units : {'Bq', 'Bq/g', 'Bq/cm3'} + units : {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3'} Specifies the type of activity to return, options include total - activity [Bq], specific [Bq/g] or volumetric activity [Bq/cm3]. - Default is volumetric activity [Bq/cm3]. + activity [Bq], specific [Bq/g, Bq/kg] or volumetric activity + [Bq/cm3]. Default is volumetric activity [Bq/cm3]. by_nuclide : bool Specifies if the activity should be returned for the material as a whole or per nuclide. Default is False. @@ -1159,7 +1161,7 @@ def get_activity(self, units: str = 'Bq/cm3', by_nuclide: bool = False, of the material is returned as a float. """ - cv.check_value('units', units, {'Bq', 'Bq/g', 'Bq/cm3'}) + cv.check_value('units', units, {'Bq', 'Bq/g', 'Bq/kg', 'Bq/cm3'}) cv.check_type('by_nuclide', by_nuclide, bool) if units == 'Bq': @@ -1168,6 +1170,8 @@ def get_activity(self, units: str = 'Bq/cm3', by_nuclide: bool = False, multiplier = 1 elif units == 'Bq/g': multiplier = 1.0 / self.get_mass_density() + elif units == 'Bq/kg': + multiplier = 1000.0 / self.get_mass_density() activity = {} for nuclide, atoms_per_bcm in self.get_nuclide_atom_densities().items(): @@ -1179,15 +1183,15 @@ def get_activity(self, units: str = 'Bq/cm3', by_nuclide: bool = False, def get_decay_heat(self, units: str = 'W', by_nuclide: bool = False, volume: float | None = None) -> dict[str, float] | float: """Returns the decay heat of the material or for each nuclide in the - material in units of [W], [W/g] or [W/cm3]. + material in units of [W], [W/g], [W/kg] or [W/cm3]. .. versionadded:: 0.13.3 Parameters ---------- - units : {'W', 'W/g', 'W/cm3'} + units : {'W', 'W/g', 'W/kg', 'W/cm3'} Specifies the units of decay heat to return. Options include total - heat [W], specific [W/g] or volumetric heat [W/cm3]. + heat [W], specific [W/g, W/kg] or volumetric heat [W/cm3]. Default is total heat [W]. by_nuclide : bool Specifies if the decay heat should be returned for the material as a @@ -1206,7 +1210,7 @@ def get_decay_heat(self, units: str = 'W', by_nuclide: bool = False, of the material is returned as a float. """ - cv.check_value('units', units, {'W', 'W/g', 'W/cm3'}) + cv.check_value('units', units, {'W', 'W/g', 'W/kg', 'W/cm3'}) cv.check_type('by_nuclide', by_nuclide, bool) if units == 'W': @@ -1215,6 +1219,8 @@ def get_decay_heat(self, units: str = 'W', by_nuclide: bool = False, multiplier = 1 elif units == 'W/g': multiplier = 1.0 / self.get_mass_density() + elif units == 'W/kg': + multiplier = 1000.0 / self.get_mass_density() decayheat = {} for nuclide, atoms_per_bcm in self.get_nuclide_atom_densities().items(): diff --git a/tests/unit_tests/test_material.py b/tests/unit_tests/test_material.py index c6a07cff977..ec55a775637 100644 --- a/tests/unit_tests/test_material.py +++ b/tests/unit_tests/test_material.py @@ -577,6 +577,7 @@ def test_get_activity(): m4.add_nuclide("H3", 1) m4.set_density('g/cm3', 1.5) assert pytest.approx(m4.get_activity(units='Bq/g')) == 355978108155965.94 # [Bq/g] + assert pytest.approx(m4.get_activity(units='Bq/kg')) == 355978108155965940 # [Bq/kg] assert pytest.approx(m4.get_activity(units='Bq/g', by_nuclide=True)["H3"]) == 355978108155965.94 # [Bq/g] assert pytest.approx(m4.get_activity(units='Bq/cm3')) == 355978108155965.94*3/2 # [Bq/cc] assert pytest.approx(m4.get_activity(units='Bq/cm3', by_nuclide=True)["H3"]) == 355978108155965.94*3/2 # [Bq/cc] @@ -626,6 +627,7 @@ def test_get_decay_heat(): m4.add_nuclide("I135", 1) m4.set_density('g/cm3', 1.5) assert pytest.approx(m4.get_decay_heat(units='W/g')) == 40175.15720273193 # [W/g] + assert pytest.approx(m4.get_decay_heat(units='W/kg')) == 40175157.20273193 # [W/kg] assert pytest.approx(m4.get_decay_heat(units='W/g', by_nuclide=True)["I135"]) == 40175.15720273193 # [W/g] assert pytest.approx(m4.get_decay_heat(units='W/cm3')) == 40175.15720273193*3/2 # [W/cc] assert pytest.approx(m4.get_decay_heat(units='W/cm3', by_nuclide=True)["I135"]) == 40175.15720273193*3/2 #[W/cc] @@ -656,6 +658,9 @@ def test_decay_photon_energy(): assert src.p * 2.0 == pytest.approx(src_v2.p) src_per_cm3 = m.get_decay_photon_energy(units='Bq/cm3', volume=100.0) assert (src.p == src_per_cm3.p).all() + src_per_bqg = m.get_decay_photon_energy(units='Bq/g') + src_per_bqkg = m.get_decay_photon_energy(units='Bq/kg') + assert pytest.approx(src_per_bqg.integral()) == src_per_bqkg.integral() / 1000. # If we add Xe135 (which has a tabular distribution), the photon source # should be a mixture distribution From 8fb48f125fd252a3d7d50c1cee500460bd9c3959 Mon Sep 17 00:00:00 2001 From: rzehumat <49885819+rzehumat@users.noreply.github.com> Date: Fri, 28 Feb 2025 17:38:31 +0100 Subject: [PATCH 172/184] Manually fix broken links (#3331) --- docs/source/devguide/styleguide.rst | 2 +- docs/source/devguide/tests.rst | 6 +++--- docs/source/devguide/workflow.rst | 2 +- docs/source/index.rst | 2 +- docs/source/io_formats/settings.rst | 2 +- docs/source/methods/cross_sections.rst | 4 ++-- docs/source/methods/geometry.rst | 2 +- docs/source/methods/photon_physics.rst | 8 ++++---- docs/source/usersguide/data.rst | 6 +++--- docs/source/usersguide/parallel.rst | 2 +- openmc/examples.py | 4 ++-- openmc/lib/weight_windows.py | 2 +- openmc/mgxs/__init__.py | 2 +- 13 files changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/source/devguide/styleguide.rst b/docs/source/devguide/styleguide.rst index b266f5f0222..b8ec2d40f84 100644 --- a/docs/source/devguide/styleguide.rst +++ b/docs/source/devguide/styleguide.rst @@ -55,7 +55,7 @@ Source Files Use a ``.cpp`` suffix for code files and ``.h`` for header files. Header files should always use include guards with the following style (See -`SF.8 `_): +`SF.8 `_): .. code-block:: C++ diff --git a/docs/source/devguide/tests.rst b/docs/source/devguide/tests.rst index 3665ad9d406..f2e39441a87 100644 --- a/docs/source/devguide/tests.rst +++ b/docs/source/devguide/tests.rst @@ -87,9 +87,9 @@ that, consider the following: Debugging Tests in CI --------------------- -.. _tmate: `_ - -Tests can be debugged in CI using a feature called `tmate`_. CI debugging can be +Tests can be debugged in CI using a feature called +`tmate `_. +CI debugging can be enabled by including "[gha-debug]" in the commit message. When the test fails, a link similar to the one shown below will be provided in the GitHub Actions output after failure occurs. Logging into the provided link will allow you to diff --git a/docs/source/devguide/workflow.rst b/docs/source/devguide/workflow.rst index d6a3c6ba4f4..d9d36c161c2 100644 --- a/docs/source/devguide/workflow.rst +++ b/docs/source/devguide/workflow.rst @@ -126,7 +126,7 @@ reinstalling it). While the same effect can be achieved using the :envvar:`PYTHONPATH` environment variable, this is generally discouraged as it can interfere with virtual environments. -.. _git: http://git-scm.com/ +.. _git: https://git-scm.com/ .. _GitHub: https://github.com/ .. _git flow: https://nvie.com/git-model .. _valgrind: https://www.valgrind.org/ diff --git a/docs/source/index.rst b/docs/source/index.rst index a01055374b0..97666e7c3cf 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -12,7 +12,7 @@ files produced by NJOY. Parallelism is enabled via a hybrid MPI and OpenMP programming model. OpenMC was originally developed by members of the `Computational Reactor Physics -Group `_ at the `Massachusetts Institute of Technology +Group `_ at the `Massachusetts Institute of Technology `_ starting in 2011. Various universities, laboratories, and other organizations now contribute to the development of OpenMC. For more information on OpenMC, feel free to post a message on the `OpenMC Discourse diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index 3ddba1d5eda..0c225b1edab 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -238,7 +238,7 @@ based on the recommended value in LA-UR-14-24530_. .. note:: This element is not used in the multi-group :ref:`energy_mode`. -.. _LA-UR-14-24530: https://laws.lanl.gov/vhosts/mcnp.lanl.gov/pdf_files/la-ur-14-24530.pdf +.. _LA-UR-14-24530: https://mcnp.lanl.gov/pdf_files/TechReport_2014_LANL_LA-UR-14-24530_Brown.pdf --------------------------- ```` diff --git a/docs/source/methods/cross_sections.rst b/docs/source/methods/cross_sections.rst index 21a1ef8dfeb..ad64b0e38ed 100644 --- a/docs/source/methods/cross_sections.rst +++ b/docs/source/methods/cross_sections.rst @@ -295,11 +295,11 @@ or even isotropic scattering. .. _Josey: https://doi.org/10.1016/j.jcp.2015.08.013 .. _WMP Library: https://github.com/mit-crpg/WMP_Library .. _MCNP: https://mcnp.lanl.gov -.. _Serpent: http://montecarlo.vtt.fi +.. _Serpent: https://serpent.vtt.fi/serpent/ .. _NJOY: https://www.njoy21.io/NJOY21/ .. _ENDF/B data: https://www.nndc.bnl.gov/endf-b8.0/ .. _Leppanen: https://doi.org/10.1016/j.anucene.2009.03.019 -.. _algorithms: http://ab-initio.mit.edu/wiki/index.php/Faddeeva_Package +.. _algorithms: http://ab-initio.mit.edu/faddeeva/ .. _NCrystal: https://github.com/mctools/ncrystal .. _NCrystal paper: https://doi.org/10.1016/j.cpc.2019.07.015 .. _using plugins: https://doi.org/10.1016/j.cpc.2021.108082 diff --git a/docs/source/methods/geometry.rst b/docs/source/methods/geometry.rst index b282ffdee37..fa8bb3cf75e 100644 --- a/docs/source/methods/geometry.rst +++ b/docs/source/methods/geometry.rst @@ -1066,5 +1066,5 @@ surface is known as in :ref:`reflection`. .. _constructive solid geometry: https://en.wikipedia.org/wiki/Constructive_solid_geometry .. _surfaces: https://en.wikipedia.org/wiki/Surface .. _MCNP: https://mcnp.lanl.gov -.. _Serpent: http://montecarlo.vtt.fi +.. _Serpent: https://serpent.vtt.fi/serpent/ .. _Monte Carlo Performance benchmark: https://github.com/mit-crpg/benchmarks/tree/master/mc-performance/openmc diff --git a/docs/source/methods/photon_physics.rst b/docs/source/methods/photon_physics.rst index bc912b943df..42c68431a18 100644 --- a/docs/source/methods/photon_physics.rst +++ b/docs/source/methods/photon_physics.rst @@ -1059,16 +1059,16 @@ emitted photon. .. _anomalous scattering: http://pd.chem.ucl.ac.uk/pdnn/diff1/anomscat.htm -.. _Kahn's rejection method: https://laws.lanl.gov/vhosts/mcnp.lanl.gov/pdf_files/aecu-3259_kahn.pdf +.. _Kahn's rejection method: https://mcnp.lanl.gov/pdf_files/TechReport_1956_RC_AECU-3259RM-1237-AEC_Kahn.pdf .. _Klein-Nishina: https://en.wikipedia.org/wiki/Klein%E2%80%93Nishina_formula -.. _LA-UR-04-0487: https://laws.lanl.gov/vhosts/mcnp.lanl.gov/pdf_files/la-ur-04-0487.pdf +.. _LA-UR-04-0487: https://mcnp.lanl.gov/pdf_files/TechReport_2004_LANL_LA-UR-04-0487_Sood.pdf -.. _LA-UR-04-0488: https://laws.lanl.gov/vhosts/mcnp.lanl.gov/pdf_files/la-ur-04-0488.pdf +.. _LA-UR-04-0488: https://mcnp.lanl.gov/pdf_files/TechReport_2004_LANL_LA-UR-04-0488_SoodWhite.pdf .. _Kaltiaisenaho: https://aaltodoc.aalto.fi/bitstream/handle/123456789/21004/master_Kaltiaisenaho_Toni_2016.pdf -.. _Salvat: https://www.oecd-nea.org/globalsearch/download.php?doc=77434 +.. _Salvat: https://doi.org/10.1787/32da5043-en .. _Sternheimer: https://doi.org/10.1103/PhysRevB.26.6067 diff --git a/docs/source/usersguide/data.rst b/docs/source/usersguide/data.rst index 6af2973388b..a1611de6352 100644 --- a/docs/source/usersguide/data.rst +++ b/docs/source/usersguide/data.rst @@ -12,7 +12,7 @@ responsible for specifying one or more of the following: file (commonly named ``cross_sections.xml``) contains a listing of other data files, in particular neutron cross sections, photon cross sections, and windowed multipole data. Each of those files, in turn, uses a `HDF5 - `_ format (see :ref:`io_nuclear_data`). In + `_ format (see :ref:`io_nuclear_data`). In order to run transport simulations with continuous-energy cross sections, you need to specify this file. @@ -291,12 +291,12 @@ distributed with any pre-existing multigroup cross section libraries. However, if a multigroup library file is downloaded or generated, the path to the file needs to be specified as described in :ref:`usersguide_data_runtime`. For an example of how to create a multigroup library, see the `example notebook -<../examples/mg-mode-part-i.ipynb>`__. +`_. .. _NJOY: http://www.njoy21.io/ .. _NNDC: https://www.nndc.bnl.gov/endf .. _MCNP: https://mcnp.lanl.gov -.. _Serpent: http://montecarlo.vtt.fi +.. _Serpent: https://serpent.vtt.fi/serpent/ .. _ENDF/B: https://www.nndc.bnl.gov/endf-b7.1/acefiles.html .. _JEFF: https://www.oecd-nea.org/dbdata/jeff/jeff33/ .. _TENDL: https://tendl.web.psi.ch/tendl_2017/tendl2017.html diff --git a/docs/source/usersguide/parallel.rst b/docs/source/usersguide/parallel.rst index be418960486..e00ed5e97ba 100644 --- a/docs/source/usersguide/parallel.rst +++ b/docs/source/usersguide/parallel.rst @@ -102,4 +102,4 @@ performance on a machine when running in parallel: settings.output = {'tallies': False} .. _Haswell-EP: http://www.anandtech.com/show/8423/intel-xeon-e5-version-3-up-to-18-haswell-ep-cores-/4 -.. _bound: https://wiki.mpich.org/mpich/index.php/Using_the_Hydra_Process_Manager#Process-core_Binding +.. _bound: https://github.com/pmodels/mpich/blob/main/doc/wiki/how_to/Using_the_Hydra_Process_Manager.md#process-core-binding diff --git a/openmc/examples.py b/openmc/examples.py index 538b6ea4ac7..8d4bd1f04c3 100644 --- a/openmc/examples.py +++ b/openmc/examples.py @@ -11,7 +11,7 @@ def pwr_pin_cell() -> openmc.Model: This model is a single fuel pin with 2.4 w/o enriched UO2 corresponding to a beginning-of-cycle condition and borated water. The specifications are from - the `BEAVRS `_ benchmark. Note that the + the `BEAVRS `_ benchmark. Note that the number of particles/batches is initially set very low for testing purposes. Returns @@ -442,7 +442,7 @@ def pwr_assembly() -> openmc.Model: """Create a PWR assembly model. This model is a reflected 17x17 fuel assembly from the the `BEAVRS - `_ benchmark. The fuel is 2.4 w/o + `_ benchmark. The fuel is 2.4 w/o enriched UO2 corresponding to a beginning-of-cycle condition. Note that the number of particles/batches is initially set very low for testing purposes. diff --git a/openmc/lib/weight_windows.py b/openmc/lib/weight_windows.py index d92f019179f..ed442d33ff4 100644 --- a/openmc/lib/weight_windows.py +++ b/openmc/lib/weight_windows.py @@ -276,7 +276,7 @@ def max_split(self, max_split): def update_magic(self, tally, value='mean', threshold=1.0, ratio=5.0): """Update weight window values using the MAGIC method - Reference: https://inis.iaea.org/search/48022314 + Reference: https://inis.iaea.org/records/231pm-zzy35 Parameters ---------- diff --git a/openmc/mgxs/__init__.py b/openmc/mgxs/__init__.py index 4f12a8fb42c..6a29a8383ca 100644 --- a/openmc/mgxs/__init__.py +++ b/openmc/mgxs/__init__.py @@ -23,7 +23,7 @@ - activation_ energy group structures "VITAMIN-J-42", "VITAMIN-J-175", "TRIPOLI-315", "CCFE-709_" and "UKAEA-1102_" -.. _CASMO: https://www.studsvik.com/SharepointFiles/CASMO-5%20Development%20and%20Applications.pdf +.. _CASMO: http://large.stanford.edu/courses/2013/ph241/dalvi1/docs/c5.physor2006.pdf .. _SCALE44: https://www-nds.iaea.org/publications/indc/indc-czr-0001.pdf .. _SCALE252: https://oecd-nea.org/science/wpncs/amct/workingarea/meeting2013/EGAMCT2013_08.pdf .. _MPACT: https://vera.ornl.gov/mpact/ From 39ad29d82e429b5e6f18cfb5f369bbfdddd93ed5 Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 28 Feb 2025 13:50:35 -0600 Subject: [PATCH 173/184] Fix reading of horizontal field of view for ray-traced plots (#3330) Co-authored-by: Patrick Shriwise --- openmc/plots.py | 78 +++++++++++++++++++ src/plot.cpp | 12 +-- .../plot_projections/results_true.dat | 2 +- 3 files changed, 86 insertions(+), 6 deletions(-) diff --git a/openmc/plots.py b/openmc/plots.py index 9c1e31575e5..40fcf0c78da 100644 --- a/openmc/plots.py +++ b/openmc/plots.py @@ -1312,6 +1312,50 @@ class WireframeRayTracePlot(RayTracePlot): Attributes ---------- + id : int + Unique identifier + name : str + Name of the plot + pixels : Iterable of int + Number of pixels to use in each direction + filename : str + Path to write the plot to + color_by : {'cell', 'material'} + Indicate whether the plot should be colored by cell or by material + background : Iterable of int or str + Color of the background + mask_components : Iterable of openmc.Cell or openmc.Material or int + The cells or materials (or corresponding IDs) to mask + mask_background : Iterable of int or str + Color to apply to all cells/materials listed in mask_components + show_overlaps : bool + Indicate whether or not overlapping regions are shown + overlap_color : Iterable of int or str + Color to apply to overlapping regions + colors : dict + Dictionary indicating that certain cells/materials should be + displayed with a particular color. The keys can be of type + :class:`~openmc.Cell`, :class:`~openmc.Material`, or int (ID for a + cell/material). + level : int + Universe depth to plot at + horizontal_field_of_view : float + Field of view horizontally, in units of degrees, defaults to 70. + camera_position : tuple or list of ndarray + Position of the camera in 3D space. Defaults to (1, 0, 0). + look_at : tuple or list of ndarray + The center of the camera's image points to this place in 3D space. + Set to (0, 0, 0) by default. + up : tuple or list of ndarray + Which way is up for the camera. Must not be parallel to the + line between look_at and camera_position. Set to (0, 0, 1) by default. + orthographic_width : float + If set to a nonzero value, an orthographic projection is used. + All rays traced from the orthographic pixel array travel in the + same direction. The width of the starting array must be specified, + unlike with the default perspective projection. The height of the + array is deduced from the ratio of pixel dimensions for the image. + Defaults to zero, i.e. using perspective projection. wireframe_thickness : int Line thickness employed for drawing wireframes around cells or material regions. Can be set to zero for no wireframes at all. Defaults to one @@ -1512,6 +1556,40 @@ class SolidRayTracePlot(RayTracePlot): Attributes ---------- + id : int + Unique identifier + name : str + Name of the plot + pixels : Iterable of int + Number of pixels to use in each direction + filename : str + Path to write the plot to + color_by : {'cell', 'material'} + Indicate whether the plot should be colored by cell or by material + overlap_color : Iterable of int or str + Color to apply to overlapping regions + colors : dict + Dictionary indicating that certain cells/materials should be + displayed with a particular color. The keys can be of type + :class:`~openmc.Cell`, :class:`~openmc.Material`, or int (ID for a + cell/material). + horizontal_field_of_view : float + Field of view horizontally, in units of degrees, defaults to 70. + camera_position : tuple or list of ndarray + Position of the camera in 3D space. Defaults to (1, 0, 0). + look_at : tuple or list of ndarray + The center of the camera's image points to this place in 3D space. + Set to (0, 0, 0) by default. + up : tuple or list of ndarray + Which way is up for the camera. Must not be parallel to the + line between look_at and camera_position. Set to (0, 0, 1) by default. + orthographic_width : float + If set to a nonzero value, an orthographic projection is used. + All rays traced from the orthographic pixel array travel in the + same direction. The width of the starting array must be specified, + unlike with the default perspective projection. The height of the + array is deduced from the ratio of pixel dimensions for the image. + Defaults to zero, i.e. using perspective projection. light_position : tuple or list of float Position of the light source in 3D space. Defaults to None, which places the light at the camera position. diff --git a/src/plot.cpp b/src/plot.cpp index 093f89f8cec..dbc25b21e4d 100644 --- a/src/plot.cpp +++ b/src/plot.cpp @@ -1083,7 +1083,7 @@ WireframeRayTracePlot::WireframeRayTracePlot(pugi::xml_node node) void WireframeRayTracePlot::set_wireframe_color(pugi::xml_node plot_node) { - // Copy plot background color + // Copy plot wireframe color if (check_for_node(plot_node, "wireframe_color")) { vector w_rgb = get_node_array(plot_node, "wireframe_color"); if (w_rgb.size() == 3) { @@ -1503,13 +1503,15 @@ void RayTracePlot::set_look_at(pugi::xml_node node) void RayTracePlot::set_field_of_view(pugi::xml_node node) { // Defaults to 70 degree horizontal field of view (see .h file) - if (check_for_node(node, "field_of_view")) { - double fov = std::stod(get_node_value(node, "field_of_view", true)); + if (check_for_node(node, "horizontal_field_of_view")) { + double fov = + std::stod(get_node_value(node, "horizontal_field_of_view", true)); if (fov < 180.0 && fov > 0.0) { horizontal_field_of_view_ = fov; } else { - fatal_error(fmt::format( - "Field of view for plot {} out-of-range. Must be in (0, 180).", id())); + fatal_error(fmt::format("Horizontal field of view for plot {} " + "out-of-range. Must be in (0, 180) degrees.", + id())); } } } diff --git a/tests/regression_tests/plot_projections/results_true.dat b/tests/regression_tests/plot_projections/results_true.dat index 672280fdd1d..d5c6a7a58b1 100644 --- a/tests/regression_tests/plot_projections/results_true.dat +++ b/tests/regression_tests/plot_projections/results_true.dat @@ -1 +1 @@ -025804f1522eafd6e0e9566ce6b9b5603962f278de222c842fe3e06471290bb575676255bcd55e4f084bdcca4ee56d3c219827cb1ef2b5c3a90f7666986b55e9 \ No newline at end of file +6b90dfcf3059f86d623bb6496bb92d5b6ea2788b79639b61f865b31b503b84df9af64e59eacb04ccb02a225cfdb51bb7fa4b4f71e8e6ea20b2714266b34886ce \ No newline at end of file From e2557bbe22b8f7e6f8bd6253aaedd2c6f11c15cc Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 28 Feb 2025 20:04:27 -0600 Subject: [PATCH 174/184] Update pugixml to v1.15 (#3332) --- vendor/pugixml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/pugixml b/vendor/pugixml index 41b6ff21c45..ee86beb30e4 160000 --- a/vendor/pugixml +++ b/vendor/pugixml @@ -1 +1 @@ -Subproject commit 41b6ff21c455865bb8ef67c5952b7f895b62bacc +Subproject commit ee86beb30e4973f5feffe3ce63bfa4fbadf72f38 From 239f7fed5e39eadfdad4f47c08f3f7b633862ab9 Mon Sep 17 00:00:00 2001 From: ahman24 <79746189+ahman24@users.noreply.github.com> Date: Wed, 5 Mar 2025 08:26:38 +0900 Subject: [PATCH 175/184] Implement user-configurable random number stride (#3067) Co-authored-by: Paul Romano --- docs/source/io_formats/settings.rst | 9 ++++++ docs/source/io_formats/statepoint.rst | 1 + include/openmc/capi.h | 2 ++ include/openmc/random_lcg.h | 14 +++++++++ openmc/lib/settings.py | 10 +++++++ openmc/settings.py | 25 ++++++++++++++++ openmc/statepoint.py | 6 ++++ src/finalize.cpp | 2 ++ src/initialize.cpp | 5 ++-- src/random_lcg.cpp | 12 +++++++- src/settings.cpp | 6 ++++ src/state_point.cpp | 8 +++++ tests/regression_tests/stride/__init__.py | 0 tests/regression_tests/stride/inputs_true.dat | 25 ++++++++++++++++ .../regression_tests/stride/results_true.dat | 2 ++ tests/regression_tests/stride/test.py | 29 +++++++++++++++++++ 16 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 tests/regression_tests/stride/__init__.py create mode 100644 tests/regression_tests/stride/inputs_true.dat create mode 100644 tests/regression_tests/stride/results_true.dat create mode 100644 tests/regression_tests/stride/test.py diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index 0c225b1edab..5174cfd8b19 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -538,6 +538,15 @@ pseudo-random number generator. *Default*: 1 +-------------------- +```` Element +-------------------- + +The ``stride`` element is used to specify how many random numbers are allocated +for each source particle history. + + *Default*: 152,917 + .. _source_element: -------------------- diff --git a/docs/source/io_formats/statepoint.rst b/docs/source/io_formats/statepoint.rst index fd00a3e7735..3b103176965 100644 --- a/docs/source/io_formats/statepoint.rst +++ b/docs/source/io_formats/statepoint.rst @@ -23,6 +23,7 @@ The current version of the statepoint file format is 18.1. bank is present (1) or not (0). :Datasets: - **seed** (*int8_t*) -- Pseudo-random number generator seed. + - **stride** (*uint64_t*) -- Pseudo-random number generator stride. - **energy_mode** (*char[]*) -- Energy mode of the run, either 'continuous-energy' or 'multi-group'. - **run_mode** (*char[]*) -- Run mode used, either 'eigenvalue' or diff --git a/include/openmc/capi.h b/include/openmc/capi.h index 3802692c35a..54257d09385 100644 --- a/include/openmc/capi.h +++ b/include/openmc/capi.h @@ -71,6 +71,7 @@ int openmc_get_nuclide_index(const char name[], int* index); int openmc_add_unstructured_mesh( const char filename[], const char library[], int* id); int64_t openmc_get_seed(); +uint64_t openmc_get_stride(); int openmc_get_tally_index(int32_t id, int32_t* index); void openmc_get_tally_next_id(int32_t* id); int openmc_global_tallies(double** ptr); @@ -137,6 +138,7 @@ int openmc_reset_timers(); int openmc_run(); int openmc_sample_external_source(size_t n, uint64_t* seed, void* sites); void openmc_set_seed(int64_t new_seed); +void openmc_set_stride(uint64_t new_stride); int openmc_set_n_batches( int32_t n_batches, bool set_max_batches, bool add_statepoint_batch); int openmc_simulation_finalize(); diff --git a/include/openmc/random_lcg.h b/include/openmc/random_lcg.h index 4157b7cfe7a..5aecafed3cb 100644 --- a/include/openmc/random_lcg.h +++ b/include/openmc/random_lcg.h @@ -15,6 +15,7 @@ constexpr int STREAM_SOURCE {1}; constexpr int STREAM_URR_PTABLE {2}; constexpr int STREAM_VOLUME {3}; constexpr int64_t DEFAULT_SEED {1}; +constexpr uint64_t DEFAULT_STRIDE {152917ULL}; //============================================================================== //! Generate a pseudo-random number using a linear congruential generator. @@ -98,5 +99,18 @@ extern "C" int64_t openmc_get_seed(); extern "C" void openmc_set_seed(int64_t new_seed); +//============================================================================== +//! Get OpenMC's stride. +//============================================================================== + +extern "C" uint64_t openmc_get_stride(); + +//============================================================================== +//! Set OpenMC's stride. +//! @param new_stride Stride. +//============================================================================== + +extern "C" void openmc_set_stride(uint64_t new_stride); + } // namespace openmc #endif // OPENMC_RANDOM_LCG_H diff --git a/openmc/lib/settings.py b/openmc/lib/settings.py index 062670ef843..4fba8d48b6e 100644 --- a/openmc/lib/settings.py +++ b/openmc/lib/settings.py @@ -12,6 +12,8 @@ _dll.openmc_set_seed.argtypes = [c_int64] _dll.openmc_get_seed.restype = c_int64 +_dll.openmc_set_stride.argtypes = [c_int64] +_dll.openmc_get_stride.restype = c_int64 _dll.openmc_get_n_batches.argtypes = [POINTER(c_int), c_bool] _dll.openmc_get_n_batches.restype = c_int _dll.openmc_get_n_batches.errcheck = _error_handler @@ -68,6 +70,14 @@ def seed(self): def seed(self, seed): _dll.openmc_set_seed(seed) + @property + def stride(self): + return _dll.openmc_get_stride() + + @stride.setter + def stride(self, stride): + _dll.openmc_set_stride(stride) + def set_batches(self, n_batches, set_max_batches=True, add_sp_batch=True): """Set number of batches or maximum number of batches diff --git a/openmc/settings.py b/openmc/settings.py index 0e2a1839907..882a17b68bb 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -193,6 +193,8 @@ class Settings: The type of calculation to perform (default is 'eigenvalue') seed : int Seed for the linear congruential pseudorandom number generator + stride : int + Number of random numbers allocated for each source particle history source : Iterable of openmc.SourceBase Distribution of source sites in space, angle, and energy sourcepoint : dict @@ -338,6 +340,7 @@ def __init__(self, **kwargs): self._ptables = None self._uniform_source_sampling = None self._seed = None + self._stride = None self._survival_biasing = None # Shannon entropy mesh @@ -614,6 +617,16 @@ def seed(self, seed: int): cv.check_greater_than('random number generator seed', seed, 0) self._seed = seed + @property + def stride(self) -> int: + return self._stride + + @stride.setter + def stride(self, stride: int): + cv.check_type('random number generator stride', stride, Integral) + cv.check_greater_than('random number generator stride', stride, 0) + self._stride = stride + @property def survival_biasing(self) -> bool: return self._survival_biasing @@ -1336,6 +1349,11 @@ def _create_seed_subelement(self, root): element = ET.SubElement(root, "seed") element.text = str(self._seed) + def _create_stride_subelement(self, root): + if self._stride is not None: + element = ET.SubElement(root, "stride") + element.text = str(self._stride) + def _create_survival_biasing_subelement(self, root): if self._survival_biasing is not None: element = ET.SubElement(root, "survival_biasing") @@ -1763,6 +1781,11 @@ def _seed_from_xml_element(self, root): if text is not None: self.seed = int(text) + def _stride_from_xml_element(self, root): + text = get_text(root, 'stride') + if text is not None: + self.stride = int(text) + def _survival_biasing_from_xml_element(self, root): text = get_text(root, 'survival_biasing') if text is not None: @@ -2014,6 +2037,7 @@ def to_xml_element(self, mesh_memo=None): self._create_plot_seed_subelement(element) self._create_ptables_subelement(element) self._create_seed_subelement(element) + self._create_stride_subelement(element) self._create_survival_biasing_subelement(element) self._create_cutoff_subelement(element) self._create_entropy_mesh_subelement(element, mesh_memo) @@ -2122,6 +2146,7 @@ def from_xml_element(cls, elem, meshes=None): settings._plot_seed_from_xml_element(elem) settings._ptables_from_xml_element(elem) settings._seed_from_xml_element(elem) + settings._stride_from_xml_element(elem) settings._survival_biasing_from_xml_element(elem) settings._cutoff_from_xml_element(elem) settings._entropy_mesh_from_xml_element(elem, meshes) diff --git a/openmc/statepoint.py b/openmc/statepoint.py index f2fd48066e4..715becf4882 100644 --- a/openmc/statepoint.py +++ b/openmc/statepoint.py @@ -99,6 +99,8 @@ class StatePoint: and whose values are time values in seconds. seed : int Pseudorandom number generator seed + stride : int + Number of random numbers allocated for each particle history source : numpy.ndarray of compound datatype Array of source sites. The compound datatype has fields 'r', 'u', 'E', 'wgt', 'delayed_group', 'surf_id', and 'particle', corresponding to @@ -356,6 +358,10 @@ def runtime(self): def seed(self): return self._f['seed'][()] + @property + def stride(self): + return self._f['stride'][()] + @property def source(self): return self._f['source_bank'][()] if self.source_present else None diff --git a/src/finalize.cpp b/src/finalize.cpp index 981ec5cbaf1..ad6f0cf629b 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -159,6 +159,7 @@ int openmc_finalize() model::root_universe = -1; model::plotter_seed = 1; openmc::openmc_set_seed(DEFAULT_SEED); + openmc::openmc_set_stride(DEFAULT_STRIDE); // Deallocate arrays free_memory(); @@ -221,5 +222,6 @@ int openmc_hard_reset() // Reset the random number generator state openmc::openmc_set_seed(DEFAULT_SEED); + openmc::openmc_set_stride(DEFAULT_STRIDE); return 0; } diff --git a/src/initialize.cpp b/src/initialize.cpp index 5f717b137f0..4b821bee146 100644 --- a/src/initialize.cpp +++ b/src/initialize.cpp @@ -99,9 +99,10 @@ int openmc_init(int argc, char* argv[], const void* intracomm) } #endif - // Initialize random number generator -- if the user specifies a seed, it - // will be re-initialized later + // Initialize random number generator -- if the user specifies a seed and/or + // stride, it will be re-initialized later openmc::openmc_set_seed(DEFAULT_SEED); + openmc::openmc_set_stride(DEFAULT_STRIDE); // Copy previous locale and set locale to C. This is a workaround for an issue // whereby when openmc_init is called from the plotter, the Qt application diff --git a/src/random_lcg.cpp b/src/random_lcg.cpp index ca4467719c4..29457569b94 100644 --- a/src/random_lcg.cpp +++ b/src/random_lcg.cpp @@ -10,7 +10,7 @@ int64_t master_seed {1}; // LCG parameters constexpr uint64_t prn_mult {6364136223846793005ULL}; // multiplication constexpr uint64_t prn_add {1442695040888963407ULL}; // additive factor, c -constexpr uint64_t prn_stride {152917LL}; // stride between particles +uint64_t prn_stride {DEFAULT_STRIDE}; // stride between particles //============================================================================== // PRN @@ -133,4 +133,14 @@ extern "C" void openmc_set_seed(int64_t new_seed) master_seed = new_seed; } +extern "C" uint64_t openmc_get_stride() +{ + return prn_stride; +} + +extern "C" void openmc_set_stride(uint64_t new_stride) +{ + prn_stride = new_stride; +} + } // namespace openmc diff --git a/src/settings.cpp b/src/settings.cpp index e2f5a033b50..cd925be709b 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -514,6 +514,12 @@ void read_settings_xml(pugi::xml_node root) openmc_set_seed(seed); } + // Copy random number stride if specified + if (check_for_node(root, "stride")) { + auto stride = std::stoull(get_node_value(root, "stride")); + openmc_set_stride(stride); + } + // Check for electron treatment if (check_for_node(root, "electron_treatment")) { auto temp_str = get_node_value(root, "electron_treatment", true, true); diff --git a/src/state_point.cpp b/src/state_point.cpp index ed8c6ed41c0..3b822715cff 100644 --- a/src/state_point.cpp +++ b/src/state_point.cpp @@ -89,6 +89,9 @@ extern "C" int openmc_statepoint_write(const char* filename, bool* write_source) // Write out random number seed write_dataset(file_id, "seed", openmc_get_seed()); + // Write out random number stride + write_dataset(file_id, "stride", openmc_get_stride()); + // Write run information write_dataset(file_id, "energy_mode", settings::run_CE ? "continuous-energy" : "multi-group"); @@ -399,6 +402,11 @@ extern "C" int openmc_statepoint_load(const char* filename) read_dataset(file_id, "seed", seed); openmc_set_seed(seed); + // Read and overwrite random number stride + uint64_t stride; + read_dataset(file_id, "stride", stride); + openmc_set_stride(stride); + // It is not impossible for a state point to be generated from a CE run but // to be loaded in to an MG run (or vice versa), check to prevent that. read_dataset(file_id, "energy_mode", word); diff --git a/tests/regression_tests/stride/__init__.py b/tests/regression_tests/stride/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regression_tests/stride/inputs_true.dat b/tests/regression_tests/stride/inputs_true.dat new file mode 100644 index 00000000000..f93ec33d100 --- /dev/null +++ b/tests/regression_tests/stride/inputs_true.dat @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + eigenvalue + 1000 + 10 + 5 + + + -4 -4 -4 4 4 4 + + + 1529170 + + diff --git a/tests/regression_tests/stride/results_true.dat b/tests/regression_tests/stride/results_true.dat new file mode 100644 index 00000000000..a654111500a --- /dev/null +++ b/tests/regression_tests/stride/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +2.978080E-01 6.106774E-03 diff --git a/tests/regression_tests/stride/test.py b/tests/regression_tests/stride/test.py new file mode 100644 index 00000000000..f911af1f5f7 --- /dev/null +++ b/tests/regression_tests/stride/test.py @@ -0,0 +1,29 @@ +import pytest +import openmc + +from tests.testing_harness import PyAPITestHarness + + +@pytest.fixture +def model(): + u = openmc.Material() + u.add_nuclide('U235', 1.0) + u.set_density('g/cm3', 4.5) + sph = openmc.Sphere(r=10.0, boundary_type='vacuum') + cell = openmc.Cell(fill=u, region=-sph) + model = openmc.Model() + model.geometry = openmc.Geometry([cell]) + + model.settings.batches = 10 + model.settings.inactive = 5 + model.settings.particles = 1000 + model.settings.stride = 1_529_170 + model.settings.source = openmc.IndependentSource( + space=openmc.stats.Box([-4, -4, -4], [4, 4, 4]) + ) + return model + + +def test_seed(model): + harness = PyAPITestHarness('statepoint.10.h5', model) + harness.main() From ced8929128498c4837552ad95285956d517e12b5 Mon Sep 17 00:00:00 2001 From: Thomas Kittelmann Date: Wed, 5 Mar 2025 22:45:27 +0100 Subject: [PATCH 176/184] NCrystal becomes runtime rather than buildtime dependency (#3328) Co-authored-by: Paul Romano --- .github/workflows/ci.yml | 8 +- CMakeLists.txt | 25 +--- CODEOWNERS | 3 +- cmake/OpenMCConfig.cmake.in | 13 -- docs/source/usersguide/install.rst | 20 ++-- include/openmc/ncrystal_interface.h | 40 +++---- include/openmc/ncrystal_load.h | 127 ++++++++++++++++++++ openmc/lib/__init__.py | 3 - openmc/material.py | 6 +- src/material.cpp | 2 +- src/ncrystal_interface.cpp | 79 ++----------- src/ncrystal_load.cpp | 151 ++++++++++++++++++++++++ src/output.cpp | 5 - src/source.cpp | 2 +- tests/regression_tests/ncrystal/test.py | 5 +- tools/ci/gha-install.py | 8 +- tools/ci/gha-install.sh | 9 +- tools/ci/gha-script.sh | 5 - 18 files changed, 331 insertions(+), 180 deletions(-) create mode 100644 include/openmc/ncrystal_load.h create mode 100644 src/ncrystal_load.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7fe3e1557b6..877ec68338e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,6 @@ jobs: mpi: [n, y] omp: [n, y] dagmc: [n] - ncrystal: [n] libmesh: [n] event: [n] vectfit: [n] @@ -45,10 +44,6 @@ jobs: python-version: "3.11" mpi: y omp: y - - ncrystal: y - python-version: "3.11" - mpi: n - omp: n - libmesh: y python-version: "3.11" mpi: y @@ -66,7 +61,7 @@ jobs: omp: n mpi: y name: "Python ${{ matrix.python-version }} (omp=${{ matrix.omp }}, - mpi=${{ matrix.mpi }}, dagmc=${{ matrix.dagmc }}, ncrystal=${{ matrix.ncrystal }}, + mpi=${{ matrix.mpi }}, dagmc=${{ matrix.dagmc }}, libmesh=${{ matrix.libmesh }}, event=${{ matrix.event }} vectfit=${{ matrix.vectfit }})" @@ -75,7 +70,6 @@ jobs: PHDF5: ${{ matrix.mpi }} OMP: ${{ matrix.omp }} DAGMC: ${{ matrix.dagmc }} - NCRYSTAL: ${{ matrix.ncrystal }} EVENT: ${{ matrix.event }} VECTFIT: ${{ matrix.vectfit }} LIBMESH: ${{ matrix.libmesh }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 385ce85aaaf..461abe508ab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,7 +37,6 @@ option(OPENMC_USE_DAGMC "Enable support for DAGMC (CAD) geometry" option(OPENMC_USE_LIBMESH "Enable support for libMesh unstructured mesh tallies" OFF) option(OPENMC_USE_MPI "Enable MPI" OFF) option(OPENMC_USE_MCPL "Enable MCPL" OFF) -option(OPENMC_USE_NCRYSTAL "Enable support for NCrystal scattering" OFF) option(OPENMC_USE_UWUW "Enable UWUW" OFF) message(STATUS "OPENMC_USE_OPENMP ${OPENMC_USE_OPENMP}") @@ -48,7 +47,6 @@ message(STATUS "OPENMC_USE_DAGMC ${OPENMC_USE_DAGMC}") message(STATUS "OPENMC_USE_LIBMESH ${OPENMC_USE_LIBMESH}") message(STATUS "OPENMC_USE_MPI ${OPENMC_USE_MPI}") message(STATUS "OPENMC_USE_MCPL ${OPENMC_USE_MCPL}") -message(STATUS "OPENMC_USE_NCRYSTAL ${OPENMC_USE_NCRYSTAL}") message(STATUS "OPENMC_USE_UWUW ${OPENMC_USE_UWUW}") # Warnings for deprecated options @@ -120,23 +118,6 @@ macro(find_package_write_status pkg) endif() endmacro() -#=============================================================================== -# NCrystal Scattering Support -#=============================================================================== - -if(OPENMC_USE_NCRYSTAL) - if(NOT DEFINED "NCrystal_DIR") - #Invocation of "ncrystal-config --show cmakedir" is needed to find NCrystal - #when it is installed from Python wheels: - execute_process( - COMMAND "ncrystal-config" "--show" "cmakedir" - OUTPUT_VARIABLE "NCrystal_DIR" OUTPUT_STRIP_TRAILING_WHITESPACE - ) - endif() - find_package(NCrystal 3.8.0 REQUIRED) - message(STATUS "Found NCrystal: ${NCrystal_DIR} (version ${NCrystal_VERSION})") -endif() - #=============================================================================== # DAGMC Geometry Support - need DAGMC/MOAB #=============================================================================== @@ -372,6 +353,7 @@ list(APPEND libopenmc_SOURCES src/mgxs.cpp src/mgxs_interface.cpp src/ncrystal_interface.cpp + src/ncrystal_load.cpp src/nuclide.cpp src/output.cpp src/particle.cpp @@ -548,11 +530,6 @@ if (OPENMC_USE_MCPL) target_link_libraries(libopenmc MCPL::mcpl) endif() -if(OPENMC_USE_NCRYSTAL) - target_compile_definitions(libopenmc PRIVATE NCRYSTAL) - target_link_libraries(libopenmc NCrystal::NCrystal) -endif() - #=============================================================================== # Log build info that this executable can report later #=============================================================================== diff --git a/CODEOWNERS b/CODEOWNERS index 6d452e36653..c77366de78d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -60,7 +60,8 @@ Dockerfile @shimwell src/random_ray/ @jtramm # NCrystal interface -src/ncrystal_interface.cpp @marquezj +src/ncrystal_interface.cpp @marquezj @tkittel +src/ncrystal_load.cpp @marquezj @tkittel # MCPL interface src/mcpl_interface.cpp @ebknudsen diff --git a/cmake/OpenMCConfig.cmake.in b/cmake/OpenMCConfig.cmake.in index 21c55230334..5cab4790abc 100644 --- a/cmake/OpenMCConfig.cmake.in +++ b/cmake/OpenMCConfig.cmake.in @@ -8,19 +8,6 @@ if(@OPENMC_USE_DAGMC@) find_package(DAGMC REQUIRED HINTS @DAGMC_DIR@) endif() -if(@OPENMC_USE_NCRYSTAL@) - if(NOT DEFINED "NCrystal_DIR") - #Invocation of "ncrystal-config --show cmakedir" is needed to find NCrystal - #when it is installed from Python wheels: - execute_process( - COMMAND "ncrystal-config" "--show" "cmakedir" - OUTPUT_VARIABLE "NCrystal_DIR" OUTPUT_STRIP_TRAILING_WHITESPACE - ) - endif() - find_package(NCrystal REQUIRED) - message(STATUS "Found NCrystal: ${NCrystal_DIR} (version ${NCrystal_VERSION})") -endif() - if(@OPENMC_USE_LIBMESH@) include(FindPkgConfig) list(APPEND CMAKE_PREFIX_PATH @LIBMESH_PREFIX@) diff --git a/docs/source/usersguide/install.rst b/docs/source/usersguide/install.rst index bb54594defb..0aa561ee3d7 100644 --- a/docs/source/usersguide/install.rst +++ b/docs/source/usersguide/install.rst @@ -284,13 +284,13 @@ Prerequisites * NCrystal_ library for defining materials with enhanced thermal neutron transport - Adding this option allows the creation of materials from NCrystal, which - replaces the scattering kernel treatment of ACE files with a modular, - on-the-fly approach. To use it `install - `_ NCrystal and - turn on the option in the CMake configuration step:: - - cmake -DOPENMC_USE_NCRYSTAL=on .. + OpenMC supports the creation of materials from NCrystal, which replaces + the scattering kernel treatment of ACE files with a modular, on-the-fly + approach. OpenMC does not need any particular build option to use this, + but NCrystal must be installed on the system. Refer to `NCrystal + documentation + `_ for how this is + achieved. * libMesh_ mesh library framework for numerical simulations of partial differential equations @@ -393,12 +393,6 @@ OPENMC_USE_MCPL Turns on support for reading MCPL_ source files and writing MCPL source points and surface sources. (Default: off) -OPENMC_USE_NCRYSTAL - Turns on support for NCrystal materials. NCrystal must be `installed - `_ and `initialized - `_. - (Default: off) - OPENMC_USE_LIBMESH Enables the use of unstructured mesh tallies with libMesh_. (Default: off) diff --git a/include/openmc/ncrystal_interface.h b/include/openmc/ncrystal_interface.h index 22422e378cf..e9bd8ae79b9 100644 --- a/include/openmc/ncrystal_interface.h +++ b/include/openmc/ncrystal_interface.h @@ -1,10 +1,7 @@ #ifndef OPENMC_NCRYSTAL_INTERFACE_H #define OPENMC_NCRYSTAL_INTERFACE_H -#ifdef NCRYSTAL -#include "NCrystal/NCrystal.hh" -#endif - +#include "openmc/ncrystal_load.h" #include "openmc/particle.h" #include // for uint64_t @@ -17,28 +14,25 @@ namespace openmc { // Constants //============================================================================== -extern "C" const bool NCRYSTAL_ENABLED; - //! Energy in [eV] to switch between NCrystal and ENDF constexpr double NCRYSTAL_MAX_ENERGY {5.0}; //============================================================================== -// Wrapper class an NCrystal material +// Wrapper class for an NCrystal material //============================================================================== class NCrystalMat { public: //---------------------------------------------------------------------------- // Constructors - NCrystalMat() = default; + NCrystalMat() = default; // empty object explicit NCrystalMat(const std::string& cfg); //---------------------------------------------------------------------------- // Methods -#ifdef NCRYSTAL - //! Return configuration string - std::string cfg() const; + //! Return configuration string: + const std::string& cfg() const { return cfg_; } //! Get cross section from NCrystal material // @@ -52,25 +46,21 @@ class NCrystalMat { void scatter(Particle& p) const; //! Whether the object holds a valid NCrystal material - operator bool() const; -#else + operator bool() const { return !cfg_.empty(); } - //---------------------------------------------------------------------------- - // Trivial methods when compiling without NCRYSTAL - std::string cfg() const { return ""; } - double xs(const Particle& p) const { return -1.0; } - void scatter(Particle& p) const {} - operator bool() const { return false; } -#endif + NCrystalMat clone() const + { + NCrystalMat c; + c.cfg_ = cfg_; + c.proc_ = proc_.clone(); + return c; + } private: //---------------------------------------------------------------------------- // Data members (only present when compiling with NCrystal support) -#ifdef NCRYSTAL - std::string cfg_; //!< NCrystal configuration string - std::shared_ptr - ptr_; //!< Pointer to NCrystal material object -#endif + std::string cfg_; //!< NCrystal configuration string + NCrystalScatProc proc_; //!< NCrystal scatter process }; //============================================================================== diff --git a/include/openmc/ncrystal_load.h b/include/openmc/ncrystal_load.h new file mode 100644 index 00000000000..d85f240906c --- /dev/null +++ b/include/openmc/ncrystal_load.h @@ -0,0 +1,127 @@ +//! \file ncrystal_load.h +//! \brief Helper class taking care of loading NCrystal at runtime. + +#ifndef OPENMC_NCRYSTAL_LOAD_H +#define OPENMC_NCRYSTAL_LOAD_H + +#include // for swap +#include // for function +#include // for shared_ptr +#include // for move + +namespace NCrystalVirtualAPI { + +// NOTICE: Do NOT make ANY changes in the NCrystalVirtualAPI::VirtAPI_Type1_v1 +// class, it is required to stay exactly constant over time and compatible with +// the same definition used to compile the NCrystal library! But changes to +// white space, comments, and formatting is of course allowed. This API was +// introduced in NCrystal 4.1.0. + +//! Abstract base class for NCrystal interface which must be declared exactly as +// it is in NCrystal itself. + +class VirtAPI_Type1_v1 { +public: + // Note: neutron must be an array of length 4 with values {ekin,ux,uy,uz} + class ScatterProcess; + virtual const ScatterProcess* createScatter(const char* cfgstr) const = 0; + virtual const ScatterProcess* cloneScatter(const ScatterProcess*) const = 0; + virtual void deallocateScatter(const ScatterProcess*) const = 0; + virtual double crossSectionUncached( + const ScatterProcess&, const double* neutron) const = 0; + virtual void sampleScatterUncached(const ScatterProcess&, + std::function& rng, double* neutron) const = 0; + // Plumbing: + static constexpr unsigned interface_id = 1001; + virtual ~VirtAPI_Type1_v1() = default; + VirtAPI_Type1_v1() = default; + VirtAPI_Type1_v1(const VirtAPI_Type1_v1&) = delete; + VirtAPI_Type1_v1& operator=(const VirtAPI_Type1_v1&) = delete; + VirtAPI_Type1_v1(VirtAPI_Type1_v1&&) = delete; + VirtAPI_Type1_v1& operator=(VirtAPI_Type1_v1&&) = delete; +}; + +} // namespace NCrystalVirtualAPI + +namespace openmc { + +using NCrystalAPI = NCrystalVirtualAPI::VirtAPI_Type1_v1; + +//! Function which locates and loads NCrystal at runtime using the virtual API +std::shared_ptr load_ncrystal_api(); + +//! Class encapsulating exactly the parts of NCrystal needed by OpenMC + +class NCrystalScatProc final { +public: + //! Empty constructor which does not load NCrystal + NCrystalScatProc() {} + + //! Load NCrystal and instantiate a scattering process + //! \param cfgstr NCrystal cfg-string defining the material. + NCrystalScatProc(const char* cfgstr) + : api_(load_ncrystal_api()), p_(api_->createScatter(cfgstr)) + {} + + // Note: Neutron state array is {ekin,ux,uy,uz} + + //! Returns total scattering cross section in units of barns per atom. + //! \param neutron_state array {ekin,ux,uy,uz} with ekin (eV) and direction. + double cross_section(const double* neutron_state) const + { + return api_->crossSectionUncached(*p_, neutron_state); + } + + //! Returns total scattering cross section in units of barns per atom. + //! \param rng function returning random numbers in the unit interval + //! \param neutron_state array {ekin,ux,uy,uz} with ekin (eV) and direction. + void scatter(std::function& rng, double* neutron_state) const + { + api_->sampleScatterUncached(*p_, rng, neutron_state); + } + + //! Clones the object which is otherwise move-only + NCrystalScatProc clone() const + { + NCrystalScatProc c; + if (p_) { + c.api_ = api_; + c.p_ = api_->cloneScatter(p_); + } + return c; + } + + // Plumbing (move-only semantics, but supports explicit clone): + NCrystalScatProc(const NCrystalScatProc&) = delete; + NCrystalScatProc& operator=(const NCrystalScatProc&) = delete; + + NCrystalScatProc(NCrystalScatProc&& o) : api_(std::move(o.api_)), p_(nullptr) + { + std::swap(p_, o.p_); + } + + NCrystalScatProc& operator=(NCrystalScatProc&& o) + { + if (p_) { + api_->deallocateScatter(p_); + p_ = nullptr; + } + std::swap(api_, o.api_); + std::swap(p_, o.p_); + return *this; + } + + ~NCrystalScatProc() + { + if (p_) + api_->deallocateScatter(p_); + } + +private: + std::shared_ptr api_; + const NCrystalAPI::ScatterProcess* p_ = nullptr; +}; + +} // namespace openmc + +#endif diff --git a/openmc/lib/__init__.py b/openmc/lib/__init__.py index 5fe35b9745d..15642b42be3 100644 --- a/openmc/lib/__init__.py +++ b/openmc/lib/__init__.py @@ -40,9 +40,6 @@ def _dagmc_enabled(): return c_bool.in_dll(_dll, "DAGMC_ENABLED").value -def _ncrystal_enabled(): - return c_bool.in_dll(_dll, "NCRYSTAL_ENABLED").value - def _coord_levels(): return c_int.in_dll(_dll, "n_coord_levels").value diff --git a/openmc/material.py b/openmc/material.py index 41780ce3607..b104b374e6c 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -429,7 +429,11 @@ def from_ncrystal(cls, cfg, **kwargs) -> Material: """ - import NCrystal + try: + import NCrystal + except ModuleNotFoundError as e: + raise RuntimeError('The .from_ncrystal method requires' + ' NCrystal to be installed.') from e nc_mat = NCrystal.createInfo(cfg) def openmc_natabund(Z): diff --git a/src/material.cpp b/src/material.cpp index 3ba9ab96c00..a1937aca459 100644 --- a/src/material.cpp +++ b/src/material.cpp @@ -368,7 +368,7 @@ Material& Material::clone() mat->name_ = name_; mat->nuclide_ = nuclide_; mat->element_ = element_; - mat->ncrystal_mat_ = ncrystal_mat_; + mat->ncrystal_mat_ = ncrystal_mat_.clone(); mat->atom_density_ = atom_density_; mat->density_ = density_; mat->density_gpcc_ = density_gpcc_; diff --git a/src/ncrystal_interface.cpp b/src/ncrystal_interface.cpp index b39f62d9020..935d2b8850a 100644 --- a/src/ncrystal_interface.cpp +++ b/src/ncrystal_interface.cpp @@ -7,89 +7,34 @@ namespace openmc { //============================================================================== -// Constants +// NCrystalMat implementation //============================================================================== -#ifdef NCRYSTAL -const bool NCRYSTAL_ENABLED = true; -#else -const bool NCRYSTAL_ENABLED = false; -#endif - -//============================================================================== -// NCrystal wrapper class for the OpenMC random number generator -//============================================================================== - -#ifdef NCRYSTAL -class NCrystalRNGWrapper : public NCrystal::RNGStream { -public: - constexpr NCrystalRNGWrapper(uint64_t* seed) noexcept : openmc_seed_(seed) {} - -protected: - double actualGenerate() override - { - return std::max( - std::numeric_limits::min(), prn(openmc_seed_)); - } - -private: - uint64_t* openmc_seed_; -}; -#endif - -//============================================================================== -// NCrystal implementation -//============================================================================== - -NCrystalMat::NCrystalMat(const std::string& cfg) -{ -#ifdef NCRYSTAL - cfg_ = cfg; - ptr_ = NCrystal::FactImpl::createScatter(cfg); -#else - fatal_error("Your build of OpenMC does not support NCrystal materials."); -#endif -} - -#ifdef NCRYSTAL -std::string NCrystalMat::cfg() const -{ - return cfg_; -} +NCrystalMat::NCrystalMat(const std::string& cfg) : cfg_(cfg), proc_(cfg.c_str()) +{} double NCrystalMat::xs(const Particle& p) const { // Calculate scattering XS per atom with NCrystal, only once per material - NCrystal::CachePtr dummy_cache; - auto nc_energy = NCrystal::NeutronEnergy {p.E()}; - return ptr_->crossSection(dummy_cache, nc_energy, {p.u().x, p.u().y, p.u().z}) - .get(); + double neutron_state[4] = {p.E(), p.u().x, p.u().y, p.u().z}; + return proc_.cross_section(neutron_state); } void NCrystalMat::scatter(Particle& p) const { - NCrystalRNGWrapper rng(p.current_seed()); // Initialize RNG - // create a cache pointer for multi thread physics - NCrystal::CachePtr dummy_cache; - auto nc_energy = NCrystal::NeutronEnergy {p.E()}; - auto outcome = ptr_->sampleScatter( - dummy_cache, rng, nc_energy, {p.u().x, p.u().y, p.u().z}); - + // Scatter with NCrystal, using the OpenMC RNG stream: + uint64_t* seed = p.current_seed(); + std::function rng = [&seed]() { return prn(seed); }; + double neutron_state[4] = {p.E(), p.u().x, p.u().y, p.u().z}; + proc_.scatter(rng, neutron_state); // Modify attributes of particle - p.E() = outcome.ekin.get(); + p.E() = neutron_state[0]; Direction u_old {p.u()}; - p.u() = - Direction(outcome.direction[0], outcome.direction[1], outcome.direction[2]); + p.u() = Direction(neutron_state[1], neutron_state[2], neutron_state[3]); p.mu() = u_old.dot(p.u()); p.event_mt() = ELASTIC; } -NCrystalMat::operator bool() const -{ - return ptr_.get(); -} -#endif - //============================================================================== // Functions //============================================================================== diff --git a/src/ncrystal_load.cpp b/src/ncrystal_load.cpp new file mode 100644 index 00000000000..b69f3a27f4e --- /dev/null +++ b/src/ncrystal_load.cpp @@ -0,0 +1,151 @@ +#include "openmc/ncrystal_load.h" + +#include // for isspace +#include // for strtoul +#include // for shared_ptr +#include // for mutex, lock_guard +#include + +#include +#include // for popen, pclose + +#include "openmc/error.h" + +#ifdef _WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include // for LoadLibrary, GetProcAddress +#else +#include // for dlopen, dlsym, dlerror +#endif + +namespace openmc { +namespace { + +struct NCrystalConfig { + std::string shlibpath; + unsigned long intversion = 0; + std::string symbol_namespace; +}; + +NCrystalConfig query_ncrystal_config() +{ +#ifdef _WIN32 + FILE* pipe = _popen("ncrystal-config --show " + "intversion shlibpath namespace", + "r"); +#else + FILE* pipe = popen("ncrystal-config --show " + "intversion shlibpath namespace 2>/dev/null", + "r"); +#endif + if (!pipe) + return {}; // failure + auto readLine = [pipe](std::string& tgt) -> bool { + // Read line and discard trailing whitespace (including newline chars). + char buffer[4096]; + if (fgets(buffer, sizeof(buffer), pipe) == NULL) + return false; + tgt = buffer; + while (!tgt.empty() && std::isspace(tgt.back())) + tgt.pop_back(); + return true; + }; + auto parseIntVersion = [](const std::string& s) { + char* str_end = nullptr; + unsigned long v = std::strtoul(s.c_str(), &str_end, 10); + return (v >= 2002000 && v < 999999999 && str_end == s.c_str() + s.size()) + ? v + : 0; + }; + + NCrystalConfig res; + if (!readLine(res.shlibpath) || + !(res.intversion = parseIntVersion(res.shlibpath)) || + !readLine(res.shlibpath) || res.shlibpath.empty() || + !readLine(res.symbol_namespace)) { + res.intversion = 0; // failure + } + +#ifdef _WIN32 + auto returnCode = _pclose(pipe); +#else + auto returnCode = pclose(pipe); +#endif + if (returnCode == 0 && res.intversion >= 2002000) + return res; + return {}; // failure +} + +struct NCrystalAPIDB { + std::mutex mtx; + std::shared_ptr api; + using FctSignature = void* (*)(int); + FctSignature ncrystal_access_virtapi_fct = nullptr; +}; + +void* load_virtapi_raw(unsigned interface_id, NCrystalAPIDB& db) +{ + if (!db.ncrystal_access_virtapi_fct) { + auto cfg = query_ncrystal_config(); + if (!(cfg.intversion >= 4001000)) { + // This is the most likely error message people will see: + fatal_error("Could not locate a functioning and recent enough" + " NCrystal installation (required since geometry" + " contains NCrystal materials)."); + } +#ifdef _WIN32 + auto handle = LoadLibrary(cfg.shlibpath.c_str()); +#else + dlerror(); // clear previous errors + void* handle = dlopen(cfg.shlibpath.c_str(), RTLD_LOCAL | RTLD_LAZY); +#endif + if (!handle) + fatal_error("Loading of the NCrystal library failed"); + + std::string symbol = + fmt::format("ncrystal{}_access_virtual_api", cfg.symbol_namespace); + +#ifdef _WIN32 + void* addr = (void*)(intptr_t)GetProcAddress(handle, symbol.c_str()); + if (!addr) + fatal_error("GetProcAddress(" + "ncrystal_access_virtual_api) failed"); +#else + dlerror(); // clear previous errors + void* addr = dlsym(handle, symbol.c_str()); + if (!addr) + fatal_error("dlsym(ncrystal_access_virtual_api) failed"); +#endif + db.ncrystal_access_virtapi_fct = + reinterpret_cast(addr); + } + + void* result = (*db.ncrystal_access_virtapi_fct)(interface_id); + if (!result) + fatal_error("NCrystal installation does not support required interface."); + + return result; +} + +NCrystalAPIDB& get_ncrystal_api_db() +{ + static NCrystalAPIDB db; + return db; +} +} // namespace + +std::shared_ptr load_ncrystal_api() +{ + auto& db = get_ncrystal_api_db(); + std::lock_guard lock(db.mtx); + if (!db.api) { + void* raw_api = load_virtapi_raw(NCrystalAPI::interface_id, db); + if (!raw_api) + fatal_error("Problems loading NCrystal."); + db.api = *reinterpret_cast*>(raw_api); + } + return db.api; +} +} // namespace openmc diff --git a/src/output.cpp b/src/output.cpp index 63e7fe55331..e20868efbb1 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -314,7 +314,6 @@ void print_build_info() std::string profiling(n); std::string coverage(n); std::string mcpl(n); - std::string ncrystal(n); std::string uwuw(n); #ifdef PHDF5 @@ -332,9 +331,6 @@ void print_build_info() #ifdef OPENMC_MCPL mcpl = y; #endif -#ifdef NCRYSTAL - ncrystal = y; -#endif #ifdef USE_LIBPNG png = y; #endif @@ -362,7 +358,6 @@ void print_build_info() fmt::print("DAGMC support: {}\n", dagmc); fmt::print("libMesh support: {}\n", libmesh); fmt::print("MCPL support: {}\n", mcpl); - fmt::print("NCrystal support: {}\n", ncrystal); fmt::print("Coverage testing: {}\n", coverage); fmt::print("Profiling flags: {}\n", profiling); fmt::print("UWUW support: {}\n", uwuw); diff --git a/src/source.cpp b/src/source.cpp index 2116809f2dd..c1b4ce260a3 100644 --- a/src/source.cpp +++ b/src/source.cpp @@ -4,7 +4,7 @@ #define HAS_DYNAMIC_LINKING #endif -#include // for move +#include // for move #ifdef HAS_DYNAMIC_LINKING #include // for dlopen, dlsym, dlclose, dlerror diff --git a/tests/regression_tests/ncrystal/test.py b/tests/regression_tests/ncrystal/test.py index e1c1e5ed6f0..8da05e1cfd7 100644 --- a/tests/regression_tests/ncrystal/test.py +++ b/tests/regression_tests/ncrystal/test.py @@ -6,12 +6,13 @@ import openmc import openmc.lib import pytest +import shutil from tests.testing_harness import PyAPITestHarness pytestmark = pytest.mark.skipif( - not openmc.lib._ncrystal_enabled(), - reason="NCrystal materials are not enabled.") + not shutil.which('ncrystal-config'), + reason="NCrystal is not installed.") def pencil_beam_model(cfg, E0, N): diff --git a/tools/ci/gha-install.py b/tools/ci/gha-install.py index 1eb9a55b4dc..1cc792f8d78 100644 --- a/tools/ci/gha-install.py +++ b/tools/ci/gha-install.py @@ -3,7 +3,7 @@ import subprocess -def install(omp=False, mpi=False, phdf5=False, dagmc=False, libmesh=False, ncrystal=False): +def install(omp=False, mpi=False, phdf5=False, dagmc=False, libmesh=False): # Create build directory and change to it shutil.rmtree('build', ignore_errors=True) os.mkdir('build') @@ -40,9 +40,6 @@ def install(omp=False, mpi=False, phdf5=False, dagmc=False, libmesh=False, ncrys libmesh_path = os.environ.get('HOME') + '/LIBMESH' cmake_cmd.append('-DCMAKE_PREFIX_PATH=' + libmesh_path) - if ncrystal: - cmake_cmd.append('-DOPENMC_USE_NCRYSTAL=ON') - # Build in coverage mode for coverage testing cmake_cmd.append('-DOPENMC_ENABLE_COVERAGE=on') @@ -59,11 +56,10 @@ def main(): mpi = (os.environ.get('MPI') == 'y') phdf5 = (os.environ.get('PHDF5') == 'y') dagmc = (os.environ.get('DAGMC') == 'y') - ncrystal = (os.environ.get('NCRYSTAL') == 'y') libmesh = (os.environ.get('LIBMESH') == 'y') # Build and install - install(omp, mpi, phdf5, dagmc, libmesh, ncrystal) + install(omp, mpi, phdf5, dagmc, libmesh) if __name__ == '__main__': main() diff --git a/tools/ci/gha-install.sh b/tools/ci/gha-install.sh index 50f110ed4e6..d8a3a7600e4 100755 --- a/tools/ci/gha-install.sh +++ b/tools/ci/gha-install.sh @@ -14,12 +14,9 @@ if [[ $DAGMC = 'y' ]]; then ./tools/ci/gha-install-dagmc.sh fi -# Install NCrystal if needed -if [[ $NCRYSTAL = 'y' ]]; then - pip install 'ncrystal>=4.0.0' - #Basic quick verification: - nctool --test -fi +# Install NCrystal and verify installation +pip install 'ncrystal>=4.1.0' +nctool --test # Install vectfit for WMP generation if needed if [[ $VECTFIT = 'y' ]]; then diff --git a/tools/ci/gha-script.sh b/tools/ci/gha-script.sh index 4733907eb25..c7c634ffa33 100755 --- a/tools/ci/gha-script.sh +++ b/tools/ci/gha-script.sh @@ -14,10 +14,5 @@ if [[ $EVENT == 'y' ]]; then args="${args} --event " fi -# Check NCrystal installation -if [[ $NCRYSTAL = 'y' ]]; then - nctool --test -fi - # Run regression and unit tests pytest --cov=openmc -v $args tests From 557b714d8753fa9cf0e70a5fc6cdb52f8df0bdad Mon Sep 17 00:00:00 2001 From: Lewis Gross <43077972+lewisgross1296@users.noreply.github.com> Date: Wed, 5 Mar 2025 19:07:50 -0600 Subject: [PATCH 177/184] add continue feature for depletion (#3272) Co-authored-by: Connor Moreno Co-authored-by: Patrick Shriwise Co-authored-by: Lewis Gross Co-authored-by: Paul Romano --- openmc/deplete/abc.py | 75 ++++++++++++++++++---- openmc/deplete/results.py | 34 ++++++++-- tests/unit_tests/test_deplete_continue.py | 76 +++++++++++++++++++++++ 3 files changed, 169 insertions(+), 16 deletions(-) create mode 100644 tests/unit_tests/test_deplete_continue.py diff --git a/openmc/deplete/abc.py b/openmc/deplete/abc.py index fc98239a561..ab087b98e59 100644 --- a/openmc/deplete/abc.py +++ b/openmc/deplete/abc.py @@ -1,6 +1,7 @@ """abc module. -This module contains Abstract Base Classes for implementing operator, integrator, depletion system solver, and operator helper classes +This module contains Abstract Base Classes for implementing operator, +integrator, depletion system solver, and operator helper classes """ from __future__ import annotations @@ -24,7 +25,8 @@ from openmc import Material from .stepresult import StepResult from .chain import Chain -from .results import Results +from .results import Results, _SECONDS_PER_MINUTE, _SECONDS_PER_HOUR, \ + _SECONDS_PER_DAY, _SECONDS_PER_JULIAN_YEAR from .pool import deplete from .reaction_rates import ReactionRates from .transfer_rates import TransferRates @@ -36,12 +38,6 @@ "Integrator", "SIIntegrator", "DepSystemSolver", "add_params"] -_SECONDS_PER_MINUTE = 60 -_SECONDS_PER_HOUR = 60*60 -_SECONDS_PER_DAY = 24*60*60 -_SECONDS_PER_JULIAN_YEAR = 365.25*24*60*60 - - def _normalize_timesteps( timesteps: Sequence[float] | Sequence[tuple[float, str]], source_rates: float | Sequence[float], @@ -572,6 +568,18 @@ class Integrator(ABC): :attr:`solver`. .. versionadded:: 0.12 + continue_timesteps : bool, optional + Whether or not to treat the current solve as a continuation of a + previous simulation. Defaults to `False`. When `False`, the depletion + steps provided are appended to any previous steps. If `True`, the + timesteps provided to the `Integrator` must exacly match any that + exist in the `prev_results` passed to the `Operator`. The `power`, + `power_density`, or `source_rates` must match as well. The + method of specifying `power`, `power_density`, or + `source_rates` should be the same as the initial run. + + .. versionadded:: 0.15.1 + Attributes ---------- operator : openmc.deplete.abc.TransportOperator @@ -613,7 +621,8 @@ def __init__( power_density: Optional[Union[float, Sequence[float]]] = None, source_rates: Optional[Union[float, Sequence[float]]] = None, timestep_units: str = 's', - solver: str = "cram48" + solver: str = "cram48", + continue_timesteps: bool = False, ): # Check number of stages previously used if operator.prev_res is not None: @@ -625,6 +634,8 @@ def __init__( "this uses {}".format( self.__class__.__name__, res.data.shape[0], self._num_stages)) + elif continue_timesteps: + raise ValueError("Continuation run requires passing prev_results.") self.operator = operator self.chain = operator.chain @@ -642,6 +653,35 @@ def __init__( # Normalize timesteps and source rates seconds, source_rates = _normalize_timesteps( timesteps, source_rates, timestep_units, operator) + + if continue_timesteps: + # Get timesteps and source rates from previous results + prev_times = operator.prev_res.get_times(timestep_units) + prev_source_rates = operator.prev_res.get_source_rates() + prev_timesteps = np.diff(prev_times) + + # Make sure parameters from the previous results are consistent with + # those passed to operator + num_prev = len(prev_timesteps) + if not np.array_equal(prev_timesteps, timesteps[:num_prev]): + raise ValueError( + "You are attempting to continue a run in which the previous timesteps " + "do not have the same initial timesteps as those provided to the " + "Integrator. Please make sure you are using the correct timesteps." + ) + if not np.array_equal(prev_source_rates, source_rates[:num_prev]): + raise ValueError( + "You are attempting to continue a run in which the previous results " + "do not have the same initial source rates, powers, or power densities " + "as those provided to the Integrator. Please make sure you are using " + "the correct powers, power densities, or source rates and previous " + "results file." + ) + + # Run with only the new time steps and source rates provided + seconds = seconds[num_prev:] + source_rates = source_rates[num_prev:] + self.timesteps = np.asarray(seconds) self.source_rates = np.asarray(source_rates) @@ -932,6 +972,18 @@ class SIIntegrator(Integrator): :attr:`solver`. .. versionadded:: 0.12 + continue_timesteps : bool, optional + Whether or not to treat the current solve as a continuation of a + previous simulation. Defaults to `False`. If `False`, all time + steps and source rates will be run in an append fashion and will run + after whatever time steps exist, if any. If `True`, the timesteps + provided to the `Integrator` must match exactly those that exist + in the `prev_results` passed to the `Opereator`. The `power`, + `power_density`, or `source_rates` must match as well. The + method of specifying `power`, `power_density`, or + `source_rates` should be the same as the initial run. + + .. versionadded:: 0.15.1 Attributes ---------- @@ -973,13 +1025,14 @@ def __init__( source_rates: Optional[Sequence[float]] = None, timestep_units: str = 's', n_steps: int = 10, - solver: str = "cram48" + solver: str = "cram48", + continue_timesteps: bool = False, ): check_type("n_steps", n_steps, Integral) check_greater_than("n_steps", n_steps, 0) super().__init__( operator, timesteps, power, power_density, source_rates, - timestep_units=timestep_units, solver=solver) + timestep_units=timestep_units, solver=solver, continue_timesteps=continue_timesteps) self.n_steps = n_steps def _get_bos_data_from_operator(self, step_index, step_power, n_bos): diff --git a/openmc/deplete/results.py b/openmc/deplete/results.py index f897a88422c..132ec572c76 100644 --- a/openmc/deplete/results.py +++ b/openmc/deplete/results.py @@ -17,6 +17,11 @@ __all__ = ["Results", "ResultsList"] +_SECONDS_PER_MINUTE = 60 +_SECONDS_PER_HOUR = 60*60 +_SECONDS_PER_DAY = 24*60*60 +_SECONDS_PER_JULIAN_YEAR = 365.25*24*60*60 # 365.25 due to the leap year + def _get_time_as(seconds: float, units: str) -> float: """Converts the time in seconds to time in different units @@ -31,13 +36,13 @@ def _get_time_as(seconds: float, units: str) -> float: """ if units == "a": - return seconds / (60 * 60 * 24 * 365.25) # 365.25 due to the leap year + return seconds / _SECONDS_PER_JULIAN_YEAR if units == "d": - return seconds / (60 * 60 * 24) + return seconds / _SECONDS_PER_DAY elif units == "h": - return seconds / (60 * 60) + return seconds / _SECONDS_PER_HOUR elif units == "min": - return seconds / 60 + return seconds / _SECONDS_PER_MINUTE else: return seconds @@ -71,7 +76,6 @@ def __init__(self, filename='depletion_results.h5'): data.append(StepResult.from_hdf5(fh, i)) super().__init__(data) - @classmethod def from_hdf5(cls, filename: PathLike): """Load in depletion results from a previous file @@ -460,6 +464,26 @@ def get_times(self, time_units: str = "d") -> np.ndarray: return _get_time_as(times, time_units) + def get_source_rates(self) -> np.ndarray: + """ + .. versionadded:: 0.15.1 + + Returns + ------- + numpy.ndarray + 1-D vector of source rates at each point in the depletion simulation + with the units originally defined by the user. + + """ + # Results duplicate the final source rate at the final simulation time + source_rates = np.fromiter( + (r.source_rate for r in self), + dtype=self[0].source_rate.dtype, + count=len(self)-1, + ) + + return source_rates + def get_step_where( self, time, time_units: str = "d", atol: float = 1e-6, rtol: float = 1e-3 ) -> int: diff --git a/tests/unit_tests/test_deplete_continue.py b/tests/unit_tests/test_deplete_continue.py new file mode 100644 index 00000000000..53c4c56d29f --- /dev/null +++ b/tests/unit_tests/test_deplete_continue.py @@ -0,0 +1,76 @@ +"""Unit tests for openmc.deplete continue run capability. + +These tests run in two steps: first a normal run and then a continue run using the previous results +""" + +import pytest +import openmc.deplete + +from tests import dummy_operator + + +def test_continue(run_in_tmpdir): + """Test to ensure that a properly defined continue run works""" + # set up the problem + bundle = dummy_operator.SCHEMES['predictor'] + operator = dummy_operator.DummyOperator() + + # initial depletion + bundle.solver(operator, [1.0, 2.0], [1.0, 2.0]).integrate() + + # set up continue run + prev_res = openmc.deplete.Results(operator.output_dir / "depletion_results.h5") + operator = dummy_operator.DummyOperator(prev_res) + + # if continue run happens, test passes + bundle.solver(operator, [1.0, 2.0, 3.0, 4.0], [1.0, 2.0, 3.0, 4.0], + continue_timesteps=True).integrate() + + +def test_mismatched_initial_times(run_in_tmpdir): + """Test to ensure that a continue run with different initial steps is properly caught""" + # set up the problem + bundle = dummy_operator.SCHEMES['predictor'] + operator = dummy_operator.DummyOperator() + + # perform initial steps + bundle.solver(operator, [0.75, 0.75], [1.0, 1.0]).integrate() + + # restart + prev_res = openmc.deplete.Results(operator.output_dir / "depletion_results.h5") + operator = dummy_operator.DummyOperator(prev_res) + + with pytest.raises( + ValueError, + match="You are attempting to continue a run in which the previous timesteps " + "do not have the same initial timesteps as those provided to the " + "Integrator. Please make sure you are using the correct timesteps.", + ): + bundle.solver( + operator, [0.75, 0.5, 0.75], [1.0, 1.0, 1.0], continue_timesteps=True + ).integrate() + + +def test_mismatched_initial_source_rates(run_in_tmpdir): + """Test to ensure that a continue run with different initial steps is properly caught""" + # set up the problem + bundle = dummy_operator.SCHEMES['predictor'] + operator = dummy_operator.DummyOperator() + + # perform initial steps + bundle.solver(operator, [0.75, 0.75], [1.0, 1.0]).integrate() + + # restart + prev_res = openmc.deplete.Results(operator.output_dir / "depletion_results.h5") + operator = dummy_operator.DummyOperator(prev_res) + + with pytest.raises( + ValueError, + match="You are attempting to continue a run in which the previous results " + "do not have the same initial source rates, powers, or power densities " + "as those provided to the Integrator. Please make sure you are using " + "the correct powers, power densities, or source rates and previous results file.", + ): + bundle.solver( + operator, [0.75, 0.75, 0.75], [1.0, 2.0, 1.0], continue_timesteps=True + ).integrate() From e878933b9027fa8cdef9852c806cccba908844eb Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Wed, 5 Mar 2025 23:48:04 -0600 Subject: [PATCH 178/184] Fix bug in Mesh::material_volumes for void materials (#3337) --- include/openmc/mesh.h | 4 --- src/mesh.cpp | 66 ++++++++++++++++++++++++++++--------------- 2 files changed, 44 insertions(+), 26 deletions(-) diff --git a/include/openmc/mesh.h b/include/openmc/mesh.h index 943e49a5ff1..8faf45f1b99 100644 --- a/include/openmc/mesh.h +++ b/include/openmc/mesh.h @@ -119,10 +119,6 @@ class MaterialVolumes { double* volumes_; //!< volume in [cm^3] (bins, table_size) int table_size_; //!< Size of hash table for each mesh element bool table_full_ {false}; //!< Whether the hash table is full - - // Value used to indicate an empty slot in the hash table. We use -2 because - // the value -1 is used to indicate a void material. - static constexpr int EMPTY {-2}; }; } // namespace detail diff --git a/src/mesh.cpp b/src/mesh.cpp index a24b51a0be2..101da746849 100644 --- a/src/mesh.cpp +++ b/src/mesh.cpp @@ -69,6 +69,10 @@ const bool LIBMESH_ENABLED = true; const bool LIBMESH_ENABLED = false; #endif +// Value used to indicate an empty slot in the hash table. We use -2 because +// the value -1 is used to indicate a void material. +constexpr int32_t EMPTY = -2; + namespace model { std::unordered_map mesh_map; @@ -113,7 +117,7 @@ inline bool check_intersection_point(double x1, double x0, double y1, double y0, //! Atomic compare-and-swap for signed 32-bit integer // //! \param[in,out] ptr Pointer to value to update -//! \param[in] expected Value to compare to +//! \param[in,out] expected Value to compare to //! \param[in] desired If comparison is successful, value to update to //! \return True if the comparison was successful and the value was updated inline bool atomic_cas_int32(int32_t* ptr, int32_t& expected, int32_t desired) @@ -152,8 +156,10 @@ void MaterialVolumes::add_volume( // Loop for linear probing for (int attempt = 0; attempt < table_size_; ++attempt) { - // Determine slot to check + // Determine slot to check, making sure it is positive int slot = (index_material + attempt) % table_size_; + if (slot < 0) + slot += table_size_; int32_t* slot_ptr = &this->materials(index_elem, slot); // Non-atomic read of current material @@ -192,7 +198,10 @@ void MaterialVolumes::add_volume_unsafe( { // Linear probe for (int attempt = 0; attempt < table_size_; ++attempt) { + // Determine slot to check, making sure it is positive int slot = (index_material + attempt) % table_size_; + if (slot < 0) + slot += table_size_; // Read current material int32_t current_val = this->materials(index_elem, slot); @@ -274,13 +283,15 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, if (mpi::master) { header("MESH MATERIAL VOLUMES CALCULATION", 7); } + write_message(7, "Number of mesh elements = {}", n_bins()); write_message(7, "Number of rays (x) = {}", nx); write_message(7, "Number of rays (y) = {}", ny); write_message(7, "Number of rays (z) = {}", nz); - int64_t n_total = nx * ny + ny * nz + nx * nz; + int64_t n_total = static_cast(nx) * ny + + static_cast(ny) * nz + + static_cast(nx) * nz; write_message(7, "Total number of rays = {}", n_total); - write_message( - 7, "Maximum number of materials per mesh element = {}", table_size); + write_message(7, "Table size per mesh element = {}", table_size); Timer timer; timer.start(); @@ -294,8 +305,9 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, std::array n_rays = {nx, ny, nz}; // Determine effective width of rays - Position width((bbox.xmax - bbox.xmin) / nx, (bbox.ymax - bbox.ymin) / ny, - (bbox.zmax - bbox.zmin) / nz); + Position width((nx > 0) ? (bbox.xmax - bbox.xmin) / nx : 0.0, + (ny > 0) ? (bbox.ymax - bbox.ymin) / ny : 0.0, + (nz > 0) ? (bbox.zmax - bbox.zmin) / nz : 0.0); // Set flag for mesh being contained within model bool out_of_model = false; @@ -327,6 +339,9 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, double d2 = width[ax2]; int n1 = n_rays[ax1]; int n2 = n_rays[ax2]; + if (n1 == 0 || n2 == 0) { + continue; + } // Divide rays in first direction over MPI processes by computing starting // and ending indices @@ -442,8 +457,8 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, for (int i = 1; i < mpi::n_procs; ++i) { // Receive material indices and volumes from process i - MPI_Recv( - mats.data(), total, MPI_INT, i, i, mpi::intracomm, MPI_STATUS_IGNORE); + MPI_Recv(mats.data(), total, MPI_INT32_T, i, i, mpi::intracomm, + MPI_STATUS_IGNORE); MPI_Recv(vols.data(), total, MPI_DOUBLE, i, i, mpi::intracomm, MPI_STATUS_IGNORE); @@ -453,13 +468,15 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, for (int index_elem = 0; index_elem < n_bins(); ++index_elem) { for (int k = 0; k < table_size; ++k) { int index = index_elem * table_size + k; - result.add_volume_unsafe(index_elem, mats[index], vols[index]); + if (mats[index] != EMPTY) { + result.add_volume_unsafe(index_elem, mats[index], vols[index]); + } } } } } else { // Send material indices and volumes to process 0 - MPI_Send(materials, total, MPI_INT, 0, mpi::rank, mpi::intracomm); + MPI_Send(materials, total, MPI_INT32_T, 0, mpi::rank, mpi::intracomm); MPI_Send(volumes, total, MPI_DOUBLE, 0, mpi::rank, mpi::intracomm); } } @@ -484,19 +501,24 @@ void Mesh::material_volumes(int nx, int ny, int nz, int table_size, } } - // Show elapsed time + // Get total time and normalization time timer.stop(); double t_total = timer.elapsed(); - double t_normalize = t_total - t_raytrace - t_mpi; - if (mpi::master) { - header("Timing Statistics", 7); - show_time("Total time elapsed", t_total); - show_time("Ray tracing", t_raytrace, 1); - show_time("Ray tracing (per ray)", t_raytrace / n_total, 1); - show_time("MPI communication", t_mpi, 1); - show_time("Normalization", t_normalize, 1); - std::fflush(stdout); - } + double t_norm = t_total - t_raytrace - t_mpi; + + // Show timing statistics + if (settings::verbosity < 7 || !mpi::master) + return; + header("Timing Statistics", 7); + fmt::print(" Total time elapsed = {:.4e} seconds\n", t_total); + fmt::print(" Ray tracing = {:.4e} seconds\n", t_raytrace); + fmt::print(" MPI communication = {:.4e} seconds\n", t_mpi); + fmt::print(" Normalization = {:.4e} seconds\n", t_norm); + fmt::print(" Calculation rate = {:.4e} rays/seconds\n", + n_total / t_raytrace); + fmt::print(" Calculation rate (per thread) = {:.4e} rays/seconds\n", + n_total / (t_raytrace * mpi::n_procs * num_threads())); + std::fflush(stdout); } void Mesh::to_hdf5(hid_t group) const From e12c65dff91e5654210a1a5b82f4bf57a0203386 Mon Sep 17 00:00:00 2001 From: Stefano Segantin <92783079+SteSeg@users.noreply.github.com> Date: Thu, 6 Mar 2025 08:44:10 -0500 Subject: [PATCH 179/184] openmc.Material.mix_materials() allows for **kwargs (#3336) --- openmc/material.py | 15 +++++++-------- tests/unit_tests/test_material.py | 6 ++++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/openmc/material.py b/openmc/material.py index b104b374e6c..f40ac805f70 100644 --- a/openmc/material.py +++ b/openmc/material.py @@ -1467,7 +1467,7 @@ def to_xml_element( @classmethod def mix_materials(cls, materials, fracs: Iterable[float], - percent_type: str = 'ao', name: str | None = None) -> Material: + percent_type: str = 'ao', **kwargs) -> Material: """Mix materials together based on atom, weight, or volume fractions .. versionadded:: 0.12 @@ -1482,10 +1482,8 @@ def mix_materials(cls, materials, fracs: Iterable[float], Type of percentage, must be one of 'ao', 'wo', or 'vo', to signify atom percent (molar percent), weight percent, or volume percent, optional. Defaults to 'ao' - name : str - The name for the new material, optional. Defaults to concatenated - names of input materials with percentages indicated inside - parentheses. + **kwargs + Keyword arguments passed to :class:`openmc.Material` Returns ------- @@ -1544,10 +1542,11 @@ def mix_materials(cls, materials, fracs: Iterable[float], openmc.data.AVOGADRO # Create the new material with the desired name - if name is None: - name = '-'.join([f'{m.name}({f})' for m, f in + if "name" not in kwargs: + kwargs["name"] = '-'.join([f'{m.name}({f})' for m, f in zip(materials, fracs)]) - new_mat = cls(name=name) + + new_mat = cls(**kwargs) # Compute atom fractions of nuclides and add them to the new material tot_nuclides_per_cc = np.sum([dens for dens in nuclides_per_cc.values()]) diff --git a/tests/unit_tests/test_material.py b/tests/unit_tests/test_material.py index ec55a775637..cb71cd09bef 100644 --- a/tests/unit_tests/test_material.py +++ b/tests/unit_tests/test_material.py @@ -533,11 +533,13 @@ def test_mix_materials(): dens4 = 1. / (f0 / m1dens + f1 / m2dens) dens5 = f0*m1dens + f1*m2dens m3 = openmc.Material.mix_materials([m1, m2], [f0, f1], percent_type='ao') - m4 = openmc.Material.mix_materials([m1, m2], [f0, f1], percent_type='wo') - m5 = openmc.Material.mix_materials([m1, m2], [f0, f1], percent_type='vo') + m4 = openmc.Material.mix_materials([m1, m2], [f0, f1], percent_type='wo', material_id=999) + m5 = openmc.Material.mix_materials([m1, m2], [f0, f1], percent_type='vo', name='m5') assert m3.density == pytest.approx(dens3) assert m4.density == pytest.approx(dens4) assert m5.density == pytest.approx(dens5) + assert m4.id == 999 + assert m5.name == 'm5' def test_get_activity(): From e360cb467e9b97412c4fcbfbc58c0ff4226b5014 Mon Sep 17 00:00:00 2001 From: Jonathan Shimwell Date: Thu, 6 Mar 2025 17:39:05 +0100 Subject: [PATCH 180/184] added stable and unstable nuclides to the Chain object (#3338) Co-authored-by: Paul Romano --- openmc/deplete/chain.py | 20 ++++++++++++++++++-- tests/unit_tests/test_deplete_chain.py | 8 ++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/openmc/deplete/chain.py b/openmc/deplete/chain.py index 2e81250dcfb..4998a2c3d90 100644 --- a/openmc/deplete/chain.py +++ b/openmc/deplete/chain.py @@ -12,6 +12,7 @@ from collections.abc import Mapping, Iterable from numbers import Real, Integral from warnings import warn +from typing import List import lxml.etree as ET import scipy.sparse as sp @@ -244,6 +245,10 @@ class Chain: Reactions that are tracked in the depletion chain nuclide_dict : dict of str to int Maps a nuclide name to an index in nuclides. + stable_nuclides : list of openmc.deplete.Nuclide + List of stable nuclides available in the chain. + unstable_nuclides : list of openmc.deplete.Nuclide + List of unstable nuclides available in the chain. fission_yields : None or iterable of dict List of effective fission yields for materials. Each dictionary should be of the form ``{parent: {product: yield}}`` with @@ -256,7 +261,7 @@ class Chain: """ def __init__(self): - self.nuclides = [] + self.nuclides: List[Nuclide] = [] self.reactions = [] self.nuclide_dict = {} self._fission_yields = None @@ -272,7 +277,18 @@ def __len__(self): """Number of nuclides in chain.""" return len(self.nuclides) - def add_nuclide(self, nuclide): + + @property + def stable_nuclides(self) -> List[Nuclide]: + """List of stable nuclides available in the chain""" + return [nuc for nuc in self.nuclides if nuc.half_life is None] + + @property + def unstable_nuclides(self) -> List[Nuclide]: + """List of unstable nuclides available in the chain""" + return [nuc for nuc in self.nuclides if nuc.half_life is not None] + + def add_nuclide(self, nuclide: Nuclide): """Add a nuclide to the depletion chain Parameters diff --git a/tests/unit_tests/test_deplete_chain.py b/tests/unit_tests/test_deplete_chain.py index 9753a53f738..13267cb051e 100644 --- a/tests/unit_tests/test_deplete_chain.py +++ b/tests/unit_tests/test_deplete_chain.py @@ -86,6 +86,14 @@ def test_from_endf(endf_chain): assert nuc == chain[nuc.name] +def test_unstable_nuclides(simple_chain: Chain): + assert [nuc.name for nuc in simple_chain.unstable_nuclides] == ["A", "B"] + + +def test_stable_nuclides(simple_chain: Chain): + assert [nuc.name for nuc in simple_chain.stable_nuclides] == ["H1", "C"] + + def test_from_xml(simple_chain): """Read chain_test.xml and ensure all values are correct.""" # Unfortunately, this routine touches a lot of the code, but most of From 9bfce4ee1c65496f06b718a1fb9fb54d16f3a1a2 Mon Sep 17 00:00:00 2001 From: Stefano Segantin <92783079+SteSeg@users.noreply.github.com> Date: Thu, 6 Mar 2025 16:33:26 -0500 Subject: [PATCH 181/184] Determine nuclides correctly for DAGMC models in d1s.get_radionuclides (#3335) Co-authored-by: Paul Romano --- openmc/deplete/d1s.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openmc/deplete/d1s.py b/openmc/deplete/d1s.py index 22fc6ec23da..8f6bffa8656 100644 --- a/openmc/deplete/d1s.py +++ b/openmc/deplete/d1s.py @@ -33,8 +33,10 @@ def get_radionuclides(model: openmc.Model, chain_file: str | None = None) -> lis List of nuclide names """ - # Determine what nuclides appear in model - model_nuclides = set(model.geometry.get_all_nuclides()) + + # Determine what nuclides appear in the model + model_nuclides = {nuc for mat in model._materials_by_id.values() + for nuc in mat.get_nuclides()} # Load chain file if chain_file is None: From e8c9134ff6392e1e47c284d36607fc194c9537ac Mon Sep 17 00:00:00 2001 From: Perry <100789850+yrrepy@users.noreply.github.com> Date: Thu, 6 Mar 2025 16:00:29 -0800 Subject: [PATCH 182/184] Add option for survival biasing source normalization (#3070) Co-authored-by: Patrick Shriwise Co-authored-by: Paul Romano --- docs/source/io_formats/settings.rst | 7 ++++ include/openmc/particle_data.h | 13 +++++-- include/openmc/settings.h | 1 + openmc/settings.py | 53 +++++++++++++++++------------ src/physics.cpp | 8 ++++- src/physics_mg.cpp | 8 ++++- src/settings.cpp | 5 +++ src/simulation.cpp | 5 ++- tests/unit_tests/test_settings.py | 2 ++ 9 files changed, 76 insertions(+), 26 deletions(-) diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index 5174cfd8b19..0d5367727a2 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -81,6 +81,13 @@ time. *Default*: 1.0 + :survival_normalization: + If this element is set to "true", this will enable the use of survival + biasing source normalization, whereby the weight parameters, weight and + weight_avg, are multiplied per history by the start weight of said history. + + *Default*: false + :energy_neutron: The energy under which neutrons will be killed. diff --git a/include/openmc/particle_data.h b/include/openmc/particle_data.h index 6b318385ea7..15ae57893a7 100644 --- a/include/openmc/particle_data.h +++ b/include/openmc/particle_data.h @@ -434,6 +434,7 @@ class ParticleData : public GeometryState { int g_last_; double wgt_ {1.0}; + double wgt_born_ {1.0}; double mu_; double time_ {0.0}; double time_last_ {0.0}; @@ -532,12 +533,20 @@ class ParticleData : public GeometryState { int& g_last() { return g_last_; } const int& g_last() const { return g_last_; } - // Statistic weight of particle. Setting to zero - // indicates that the particle is dead. + // Statistic weight of particle. Setting to zero indicates that the particle + // is dead. double& wgt() { return wgt_; } double wgt() const { return wgt_; } + + // Statistic weight of particle at birth + double& wgt_born() { return wgt_born_; } + double wgt_born() const { return wgt_born_; } + + // Statistic weight of particle at last collision double& wgt_last() { return wgt_last_; } const double& wgt_last() const { return wgt_last_; } + + // Whether particle is alive bool alive() const { return wgt_ != 0.0; } // Polar scattering angle after a collision diff --git a/include/openmc/settings.h b/include/openmc/settings.h index 12835fdef9a..f3aff08b0c0 100644 --- a/include/openmc/settings.h +++ b/include/openmc/settings.h @@ -61,6 +61,7 @@ extern bool surf_source_write; //!< write surface source file? extern bool surf_mcpl_write; //!< write surface mcpl file? extern bool surf_source_read; //!< read surface source file? extern bool survival_biasing; //!< use survival biasing? +extern bool survival_normalization; //!< use survival normalization? extern bool temperature_multipole; //!< use multipole data? extern "C" bool trigger_on; //!< tally triggers enabled? extern bool trigger_predict; //!< predict batches for triggers? diff --git a/openmc/settings.py b/openmc/settings.py index 882a17b68bb..2f054ebd198 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -50,15 +50,19 @@ class Settings: Indicate whether fission neutrons should be created or not. cutoff : dict Dictionary defining weight cutoff, energy cutoff and time cutoff. The - dictionary may have ten keys, 'weight', 'weight_avg', 'energy_neutron', - 'energy_photon', 'energy_electron', 'energy_positron', 'time_neutron', - 'time_photon', 'time_electron', and 'time_positron'. Value for 'weight' - should be a float indicating weight cutoff below which particle undergo - Russian roulette. Value for 'weight_avg' should be a float indicating - weight assigned to particles that are not killed after Russian roulette. - Value of energy should be a float indicating energy in eV below which - particle type will be killed. Value of time should be a float in - seconds. Particles will be killed exactly at the specified time. + dictionary may have the following keys, 'weight', 'weight_avg', + 'survival_normalization', 'energy_neutron', 'energy_photon', + 'energy_electron', 'energy_positron', 'time_neutron', 'time_photon', + 'time_electron', and 'time_positron'. Value for 'weight' should be a + float indicating weight cutoff below which particle undergo Russian + roulette. Value for 'weight_avg' should be a float indicating weight + assigned to particles that are not killed after Russian roulette. Value + of energy should be a float indicating energy in eV below which particle + type will be killed. Value of time should be a float in seconds. + Particles will be killed exactly at the specified time. Value for + 'survival_normalization' is a bool indicating whether or not the weight + cutoff parameters will be applied relative to the particle's starting + weight or to its current weight. delayed_photon_scaling : bool Indicate whether to scale the fission photon yield by (EGP + EGD)/EGP where EGP is the energy release of prompt photons and EGD is the energy @@ -162,20 +166,20 @@ class Settings: 'naive', 'simulation_averaged', or 'hybrid'. The default is 'hybrid'. :source_shape: - Assumed shape of the source distribution within each source - region. Options are 'flat' (default), 'linear', or 'linear_xy'. + Assumed shape of the source distribution within each source region. + Options are 'flat' (default), 'linear', or 'linear_xy'. :volume_normalized_flux_tallies: - Whether to normalize flux tallies by volume (bool). The default - is 'False'. When enabled, flux tallies will be reported in units of - cm/cm^3. When disabled, flux tallies will be reported in units - of cm (i.e., total distance traveled by neutrons in the spatial - tally region). + Whether to normalize flux tallies by volume (bool). The default is + 'False'. When enabled, flux tallies will be reported in units of + cm/cm^3. When disabled, flux tallies will be reported in units of cm + (i.e., total distance traveled by neutrons in the spatial tally + region). :adjoint: Whether to run the random ray solver in adjoint mode (bool). The default is 'False'. :sample_method: - Sampling method for the ray starting location and direction of travel. - Options are `prng` (default) or 'halton`. + Sampling method for the ray starting location and direction of + travel. Options are `prng` (default) or 'halton`. .. versionadded:: 0.15.0 resonance_scattering : dict @@ -899,6 +903,8 @@ def cutoff(self, cutoff: dict): cv.check_type('average survival weight', cutoff[key], Real) cv.check_greater_than('average survival weight', cutoff[key], 0.0) + elif key == 'survival_normalization': + cv.check_type('survival normalization', cutoff[key], bool) elif key in ['energy_neutron', 'energy_photon', 'energy_electron', 'energy_positron']: cv.check_type('energy cutoff', cutoff[key], Real) @@ -1364,7 +1370,8 @@ def _create_cutoff_subelement(self, root): element = ET.SubElement(root, "cutoff") for key, value in self._cutoff.items(): subelement = ET.SubElement(element, key) - subelement.text = str(value) + subelement.text = str(value) if key != 'survival_normalization' \ + else str(value).lower() def _create_entropy_mesh_subelement(self, root, mesh_memo=None): if self.entropy_mesh is None: @@ -1797,10 +1804,14 @@ def _cutoff_from_xml_element(self, root): self.cutoff = {} for key in ('energy_neutron', 'energy_photon', 'energy_electron', 'energy_positron', 'weight', 'weight_avg', 'time_neutron', - 'time_photon', 'time_electron', 'time_positron'): + 'time_photon', 'time_electron', 'time_positron', + 'survival_normalization'): value = get_text(elem, key) if value is not None: - self.cutoff[key] = float(value) + if key == 'survival_normalization': + self.cutoff[key] = value in ('true', '1') + else: + self.cutoff[key] = float(value) def _entropy_mesh_from_xml_element(self, root, meshes): text = get_text(root, 'entropy_mesh') diff --git a/src/physics.cpp b/src/physics.cpp index e04f54c0b13..72c04a5caf6 100644 --- a/src/physics.cpp +++ b/src/physics.cpp @@ -153,7 +153,13 @@ void sample_neutron_reaction(Particle& p) // Play russian roulette if survival biasing is turned on if (settings::survival_biasing) { - if (p.wgt() < settings::weight_cutoff) { + // if survival normalization is on, use normalized weight cutoff and + // normalized weight survive + if (settings::survival_normalization) { + if (p.wgt() < settings::weight_cutoff * p.wgt_born()) { + russian_roulette(p, settings::weight_survive * p.wgt_born()); + } + } else if (p.wgt() < settings::weight_cutoff) { russian_roulette(p, settings::weight_survive); } } diff --git a/src/physics_mg.cpp b/src/physics_mg.cpp index e381b27c9dc..c97a3d6f3dc 100644 --- a/src/physics_mg.cpp +++ b/src/physics_mg.cpp @@ -68,7 +68,13 @@ void sample_reaction(Particle& p) // Play Russian roulette if survival biasing is turned on if (settings::survival_biasing) { - if (p.wgt() < settings::weight_cutoff) { + // if survival normalization is applicable, use normalized weight cutoff and + // normalized weight survive + if (settings::survival_normalization) { + if (p.wgt() < settings::weight_cutoff * p.wgt_born()) { + russian_roulette(p, settings::weight_survive * p.wgt_born()); + } + } else if (p.wgt() < settings::weight_cutoff) { russian_roulette(p, settings::weight_survive); } } diff --git a/src/settings.cpp b/src/settings.cpp index cd925be709b..67fdfea666e 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -69,6 +69,7 @@ bool surf_source_write {false}; bool surf_mcpl_write {false}; bool surf_source_read {false}; bool survival_biasing {false}; +bool survival_normalization {false}; bool temperature_multipole {false}; bool trigger_on {false}; bool trigger_predict {false}; @@ -624,6 +625,10 @@ void read_settings_xml(pugi::xml_node root) if (check_for_node(node_cutoff, "weight_avg")) { weight_survive = std::stod(get_node_value(node_cutoff, "weight_avg")); } + if (check_for_node(node_cutoff, "survival_normalization")) { + survival_normalization = + get_node_value_bool(node_cutoff, "survival_normalization"); + } if (check_for_node(node_cutoff, "energy_neutron")) { energy_cutoff[0] = std::stod(get_node_value(node_cutoff, "energy_neutron")); diff --git a/src/simulation.cpp b/src/simulation.cpp index a3c6495ae7d..74a7dbe6392 100644 --- a/src/simulation.cpp +++ b/src/simulation.cpp @@ -586,6 +586,9 @@ void initialize_history(Particle& p, int64_t index_source) // Reset weight window ratio p.ww_factor() = 0.0; + // set particle history start weight + p.wgt_born() = p.wgt(); + // Reset pulse_height_storage std::fill(p.pht_storage().begin(), p.pht_storage().end(), 0); @@ -610,7 +613,7 @@ void initialize_history(Particle& p, int64_t index_source) write_message("Simulating Particle {}", p.id()); } -// Add paricle's starting weight to count for normalizing tallies later +// Add particle's starting weight to count for normalizing tallies later #pragma omp atomic simulation::total_weight += p.wgt(); diff --git a/tests/unit_tests/test_settings.py b/tests/unit_tests/test_settings.py index 02a47625162..397d7043427 100644 --- a/tests/unit_tests/test_settings.py +++ b/tests/unit_tests/test_settings.py @@ -26,6 +26,7 @@ def test_export_to_xml(run_in_tmpdir): s.plot_seed = 100 s.survival_biasing = True s.cutoff = {'weight': 0.25, 'weight_avg': 0.5, 'energy_neutron': 1.0e-5, + 'survival_normalization': True, '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, @@ -99,6 +100,7 @@ def test_export_to_xml(run_in_tmpdir): assert s.seed == 17 assert s.survival_biasing assert s.cutoff == {'weight': 0.25, 'weight_avg': 0.5, + 'survival_normalization': True, '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, From 9b5678b5f03d87a54dea9a8a2d2d20d850206ec6 Mon Sep 17 00:00:00 2001 From: John Tramm Date: Thu, 6 Mar 2025 20:48:31 -0600 Subject: [PATCH 183/184] Random Ray Source Region Mesh Subdivision (Cell-Under-Voxel Geometry) (#3333) Co-authored-by: Paul Romano --- docs/source/_images/2x2_sr_mesh.png | Bin 0 -> 109424 bytes docs/source/io_formats/settings.rst | 18 + docs/source/usersguide/random_ray.rst | 102 +- docs/source/usersguide/variance_reduction.rst | 12 +- include/openmc/constants.h | 4 + .../openmc/random_ray/flat_source_domain.h | 51 +- include/openmc/random_ray/parallel_map.h | 193 + include/openmc/random_ray/random_ray.h | 19 +- .../openmc/random_ray/random_ray_simulation.h | 10 +- include/openmc/random_ray/source_region.h | 326 +- openmc/settings.py | 55 +- src/finalize.cpp | 3 + src/random_ray/flat_source_domain.cpp | 466 +- src/random_ray/linear_source_domain.cpp | 35 +- src/random_ray/random_ray.cpp | 309 +- src/random_ray/random_ray_simulation.cpp | 189 +- src/random_ray/source_region.cpp | 227 +- src/settings.cpp | 26 + src/weight_windows.cpp | 13 +- .../random_ray_fixed_source_mesh/__init__.py | 0 .../flat/inputs_true.dat | 271 + .../flat/results_true.dat | 9 + .../linear/inputs_true.dat | 271 + .../linear/results_true.dat | 9 + .../random_ray_fixed_source_mesh/test.py | 53 + .../random_ray_k_eff_mesh/__init__.py | 0 .../random_ray_k_eff_mesh/inputs_true.dat | 119 + .../random_ray_k_eff_mesh/results_true.dat | 171 + .../random_ray_k_eff_mesh/test.py | 36 + .../weightwindows_fw_cadis_mesh/__init__.py | 0 .../flat/inputs_true.dat | 265 + .../flat/results_true.dat | 6760 +++++++++++++++++ .../linear/inputs_true.dat | 265 + .../linear/results_true.dat | 6760 +++++++++++++++++ .../weightwindows_fw_cadis_mesh/test.py | 49 + 35 files changed, 16637 insertions(+), 459 deletions(-) create mode 100644 docs/source/_images/2x2_sr_mesh.png create mode 100644 include/openmc/random_ray/parallel_map.h create mode 100644 tests/regression_tests/random_ray_fixed_source_mesh/__init__.py create mode 100644 tests/regression_tests/random_ray_fixed_source_mesh/flat/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_mesh/flat/results_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_mesh/linear/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_mesh/linear/results_true.dat create mode 100644 tests/regression_tests/random_ray_fixed_source_mesh/test.py create mode 100644 tests/regression_tests/random_ray_k_eff_mesh/__init__.py create mode 100644 tests/regression_tests/random_ray_k_eff_mesh/inputs_true.dat create mode 100644 tests/regression_tests/random_ray_k_eff_mesh/results_true.dat create mode 100644 tests/regression_tests/random_ray_k_eff_mesh/test.py create mode 100644 tests/regression_tests/weightwindows_fw_cadis_mesh/__init__.py create mode 100644 tests/regression_tests/weightwindows_fw_cadis_mesh/flat/inputs_true.dat create mode 100644 tests/regression_tests/weightwindows_fw_cadis_mesh/flat/results_true.dat create mode 100644 tests/regression_tests/weightwindows_fw_cadis_mesh/linear/inputs_true.dat create mode 100644 tests/regression_tests/weightwindows_fw_cadis_mesh/linear/results_true.dat create mode 100644 tests/regression_tests/weightwindows_fw_cadis_mesh/test.py diff --git a/docs/source/_images/2x2_sr_mesh.png b/docs/source/_images/2x2_sr_mesh.png new file mode 100644 index 0000000000000000000000000000000000000000..5cdc684d3b94c2e4f92032b0a8ee284e024c2519 GIT binary patch literal 109424 zcmZ^~1z1~8(>EL-P@E9lgS%7Qt+=+hI~0c^#ogVDLy?x^PO;M9THGlP#oggc|M&Ag zdVgH!a5iUlXLn>bJHOdPsj0ldKqW>6000>Ba?%^+e%5P z$xBH=)LfmcZ0#)p0J*5Nj8{6E3*f-QCh_*|Dxzr5apy_LO-Jk6;N6MESWecU$lVUS zxR&Yp-Czw3fUzB2q9%j+H5b#TXc9yY2vr7dqIKQm-DLqpEO4Ob(esWUP_S?pKlrUW z;N`h18aF#-3?6_g$)BD(rXX>P%OzTS8;lN6i;Fa3W)2Y>ZU&`eL*S?$ebYC0@1|J# z$2dRnSmO{MhI8Nngrf#=p$Ot!vX|3JTEZIc38UMTIT1CQ3JOdl>zJWe*d(Go z_0P7o0cSWbeEGFh`ASypk-KhLIPF4wx%nd_cVfe5U9_~iEj1D3TyiMzC%+eRl0J#3 zDqP`lvhzZmgIW&-9t2cCA0t>fm{(Xo22G_%5u%M)wLL^X(!9E5btawDwD>t{63WIw zzd~&hg!tu0k>4iJ@e@^z{b) ztEfYPK_e=^k5-X34oK(DY*iTG8IGiVoQCdhGB1GGR#7mA3Cc*I0LS=m3+oZOjr5^$ zZ0AiJu5$OS2A6Vl^5N-BJ5wuDjPApQ+yI`{wS1wJL{fis?M_TUf`|0oL(v%Kod`)> zq60U?zpHv0gI@Aq{xRKd{ZU9$B zIu|LF@I8*?hhI<3`pg-=m#QyMUc_1`d+*%l_`1`k@kSX+GLS4-gfg+4ZVTiA!o*%g z;X|PgwJpL`PuP<~r={%4|mDr2$$(-bo|B{S=L zS`xNwR((bvEe|zJ+A`+;M8)(aN>52g@#C2Gufm3T)Lrj7<)51#Q-kY*W8mKusuk)M z8n4Gtz56A_DesbDRF7(B1Msa$)qN{XBV}IJhj;Jw4 zu&}Uby^?a8zu;E|XXTeNqS99RdfkgCeD#=8ys~#iGhbd8R#qKV=a#J$JgG$#NKMI4 zEL2QYa+g$;kraNHX?}N;mKRJe#jaf~aZ)R)yQV9q+$H-`1veXLm1t2j{yUx0-o_!* zM90=^3YoW#bA$MZ`ww>?;S$kB?EBc0tVE?fx-2>jI;m3EQvcFyC4J>JmS0BT4vV?u zW7@URZ==bhm{}rR8N4@~YZj}H&tp9k!ItW_E_2ED`?jsCb))B%X-Rnfc3m@m@1q;L zuJ8|34xD(Kc;U0@veoq+mI#-amV9`K^gs?IM_S7Ngr8k$R1pNy+NaVcMJ)W2J)82aiUm z-OisB&dkin%=^sh5pgRFH?DMSeLSHbuI5LUakYl?w1?{P)Mc3$Tw|P7{KxOw%^#0% ze*t$xCqrGTNScEmcz+4i3c4^ca_q}ZD!EF#x}l2F(ru%we#(FzD7;e~RXN%uIYYY* zvAwqSnWO`D2LIewyC1r2LMQqx{|EaIS^x;sQqWS%%6|xTTri6@ZFKl}kaJ|=(*@TY z_f5v@z1QDf=Xw)+lU#()PR^0*ysC^boNG0D@ArJ4gkeNt#|P%4b9i(}St#95rr>>J z?>MeKx|S&R7&3>2fw+Mf9PFb{+Vu7%@;T|j{6uTGa$CQPpbo8?APX-I-*2UF(c_8! zB7eyL(*k*OhiAlS{F6|vkhjn)4}p`ftm#Z+tmLalUEfPW^)z(OO~?O;%qdhVRNR1X zP;W5P@6sC6-e;V!yyzvV{W9LyTQDfmXl%5zqwrZfdSZLX{`#&W=4;pE$kW|3Tr~pg z>$cY=uWDa$V|DW5`j_nOJRU3OROFX;vPa#1>e!dBQ>jy7tZIrX!_x014@~sm z>aP*ybLSpm9+d^We!7y2kbSeQyHGDQhCRNWpDmE@<9`nS(sSE;8}l=_LWsgJy4}6` zy{`6&<=U`zwU<%rwDO5)z|5Il|LceM)>Ttg&8s!7zb4IB-0t}f)>Lj5FPoMdIu<;J zkF|W(COh4lbDfOaH~n{q4Et6{p5UJR5KIwnPAXgYyz>2CWD zsl({`qeZM3uZYoU#5Bb_r^~gR`>E~myr_Ywq~_pe?vthtZQp>ckeP_F!Gggu3QbWJ zRXl%<$F~nz*R2nV(2R^JU1JyH;Z7po_bMukE`W9+cz^&Z z0Ch40Cu96()(w8N3u^@XR#kW{U_1%~@P-e-vBaxq4EWuG1`r(R;CH16buLE>WM!2x zFT~0_Sg@7>d%tmB&(zhsGwhFb0K6pL`9HI}ytpAzRR?6hARgc&<4eg_lE8MbT9!KU zR?5l%CRiC602g5k0K!Ueu$vfm0{{rg;ec1LCm4217r_5NDv+iC;lIj%D@tfe$;-o@ zn&z&SmX2;VPVOQADr^|5dE2)-?mEg!g62*RY^D}YW|nMT4$gm@0EE2+VMPZ^cTzz;=HzP4&M6=uz|O(N&c($FtHJ8#?dWdm#p>ur{Vyi} zkB_vao4Kp4v%9U6Bjhh%Q!}Rz?jlrFe>?imzkiL>(#!V0dUAC853yhZvj3&9bFy); z|0i!)Q{lgF1=Vc5EbVorZ5?3hf%PHE$-&DZ{7-}bL;9~S|65a?|No}{jr6~pzH_s5 zm2z@`b?PqqU$*&=e*Zi1KNEd6C{^u=B)1s)t?Eh&sQB=Ak)*1jn93U?( z@zx9Ou*18Crr&SvuA=^-qxBUx;WA%j2mlD)piHK7S1%oGw4Z7t|jS|kd>T2=8 zA-X6vnWmU0O|HWsU7hZs&T^rMz~=$4l(Q>!6D07c>ZG_e)VZ|ORwY$+crklHYA49} zG+ymj`-euww)59hUSG{2Ge&IiGZB#Poq?NxFA-zojS zuDlO?wmx?3ot_wsTe7upGw4tKOj!0OJJPiy%pIYTXH)@4Z-$;Eg%_VreFoMal$v}&FX_#_BB@RnIK)N@(kJd(Hmcm8WvM@Npq~>0;Sht~bT$y=bQ!tx zHNUe~IHqq+7<1ZQO@4?vsbrvPE&F)$F5}ahZ+PC*)ml-?n}nU4q~U(=NBxZPbyr$< zi|n;GzN%*(G&d=lXHTgvD?=^uTSxqJ?6yRcxZRtcye2%g1^`9+tUPxD6Y%`y3V;ad~}c9j=`5yC;iI=kzT`wh|#w(Z2g$c z2&!cnQ`KZ$X_>w=gEmcByoTK_6KrDM0Xo%wTh*%2OZ^3kzEQ7Q2`pfjL zXQ#{iLZj@AIg#hia-YfLR^O_&2he1|=#t2Ov%(fv3a{#Y^`vK-$KS`X;qwlO`p=Z-6QvovFEq{nXc1A)7I83 z7W$JSQjz2A0eWl-)Y&#F-Z0>bKSIxK1F7W>4Fsk(^8r3`2r ziHu>DXU2E-=}UzmL%7P_EY&z==ckHtajd&Ufjpm|T znuS^%8hpfF@jYMC=YD@@oIw_lWppwN;Zo{R2FkXkAsv^kHzp*!XG;oLz)LYGyf<&< zKCY4NA&VM)k2Q*OL+OieRn5_kmDDA-&Skc5%B}kS=y0hyqN?>GeFd}jSV-9{v|Dv< zOh+B5)n>eCj(>gmo;U`7Ij@>M!L8+Wf26prYvyNT_dj)t7vHuf^K@iwP>zFqX+Z1Mg)8b*O|li4rAw#V={`lph9SjL<$bGYraez|VOx*hr+ zqyCzj*Zww8V#KIA9`xtjI)8QWpMzcbqhIssB);0I3ZF2h+UZAixTk06Kk>ZqlCo>M z&|V^ejrPKT9x+w1`b_a>2 zZndF)Z7mhz6MEGw6x)!Dg;~PKw}p_xx`I>`a65WqUqLPFoR&(wq@oTocxO&SM=sM? zmY`H^J!I$|Z{T^K4)JAA@aq>Gqv?r9n6@N6O;DOp0_+ag?}4H)b82T%uZe#N3(fzd zFGBOq3L~2j+R0`t{-q}k^K%YqNjgQJ2htFYQel`h04<8^b>ZY1zh6aUURY?A3@?K^ z?*OZvY^e*NiBCWnGhHDiV_GOUvzGCi@9POHE$VuqnIG$F>cL z6TBR$t{T>qg{S_LbfI_gb$rCG)Gnv@7kWMc`?uPb@{AXVsf;)}(L(+2=F~L^nnX1%G1j4YSqkvbr9zC|)UEu7CRFg|%S1SJBP#gwM5<~oq;Miy=GZ&# zCbf^Ys$oTyc+|I1cc&aC?#H|?hrNr~Y}qt?FwDP*%XSo=rsvZWeAaZ`a(W0<7q!Ui zOWBkY)ykZ33&%BAfFNfERkw8*l{yZ<9197x9o9z=uRcAMn= zZ;0g)u;F}c+5Gx%c5XEjFwL>JpHW}@8=@K(Y&ciW{qH2YiQ!QMw$!bZKPIXDuO5V8 z8muYKNmKxp$BA#$K$0X9%PRrSrvIawngSp=bTU|`N!&yW2zFhNq{^MycP9TIbzp`Y zKoL(Amw^@L(*ULRhtaQ$ga47>c%|HFn>IDrsTk2}Z*h?-DPq@Nsc$XAZPN>LZJi?C6fjpkn# zHAx9fp~-%jhx_X&GBC55sVl&G3rYr3^%p~?UVC|pWK2qH=PBT``y>6s-UJzrAUKqg zHpl|zq;KV6_DXATVFlGBio;gMIeijr8ZKlo{;Vtw(O<{71Uo0|IPD~I|5XE_B+S`B z9Jho(@q#x6!}#DrE;N)^_~7!@e;FnmJ1{KN6ft<~FK1X?Y1&K|Y*0;xcm!gUZ0SK| z+<~{SrBudDM8|7%#(#M_2%e3FMhzR*6MkY~SD?3Sx-gbXE8mn0h0YGues=gc~9x%j=U zzcEhwk@a}uiqBBeiL^C72eKzaJ|UE+8gKs;xEy$M_njr0A2@w&Tg)rH~cF!gdo0)QwXs0i|KLAl70ENxX@$X@%pZ6{!=*gvEg<*mNJ^5z+8ibf9J98trwGgNuUw zjX!>ttrEb|e$fGD3I5`T`)B3`+QA!5=e~coV&qavp3sfLb{%ELq9A;j1hMzYh9O84 zXu;;IU2j56NVhjUD?YIv3tQx^bGkC|tQYgcuhih5cg(44iXegR=FG8FtgA5WA_YIL^z1fru2UUgrmnyg-aYQ}(${0TzoEgYmpiOyQ zP9tCwYk3V0aVeon)`0le{!!{zu1-k3&b?! z^tX@3#Rw{h$%6q)8R3Wjwb`-$LzPsW3`e%OaiX(M;)JG-9wLfvzF-K%^pY&iG5cj< z{oxPKcocsxd}t(yN_L)b62il~;7Cn#vxkk!&$c!&#`hp0je<=tSl!_tB)1*`*Sm7} zsSkg@+?Bag@^&Ex7Qb>J!imi=Xk9NN*;0c>Xn%iL6rvkLS~u+vy@&`BBU!`#IVLtd zPM}*pkji?(b|vYXQ9sOqPh+L4=v?Iunr}$V>>oT3q<lM%!ksk) zSjn@V8TeMVk|*`Ge^H%HIucSyX(9214^DBt#3F}8g>5jW{uYsez47Nny~Icbn%S5Bq%nyQcTI`63c00dr}W zekUE#tJkyU?pvq6PY!X}om~C4b*b#H*K8;lU&5?jhZtjVZ%u_h&p(Mq0VE+A8dc0^WLqLmi4Xn@LQG*# z2w8@#y_IUnsd81iJD6QybI7rqYSo@|qm%?j{V1sQl0?J@hgi})u<28EJ}xRepZQ^R zDuHPuA(TDPhgFwWKX}CItx{HLVN1P`u=pm|1x7IuG!)Ca*6OkN0&VyB9!pdQLh&Oy zar>?>MPO!8d$EY90BfVwx0#s@8n)-@85boo%`YwWZ4%XDXZGaVLQBAC9djoSAIfzn zP$oB=*gJ-2w=OwdZKttD6W%g{iU@A@%I&|CDTc5|&3aVm!^h3w4dD-MGWky%Xx^#p z4qyhJ`^3FENTE9Dd%*_dtx30}q=e(W?t~rX`iQsRA3-~D`*JFwC}d99c5>WmuS} z95Xj``gGtwr1&b@v-S6{r6bpAvg3Md6A4ucZ$>?WB&DOU{Kgw)BXP1MC5nEIv5*8k zsl0nL!;GZZMEByN=RQODmO-f{7bPadsLvni1?%xCk(@4%`@-B%Uv)Kq5Ocfw zz_7esLiQw=%33VpvXR_uq}u%Q^MuXk>tt7q38nfcGJj-v#30#ksygYVaKX~*JyQ<( z4=?dk`@33YLWA&6AHRI5eu4DTKU0?C(I8d)DBwRsIOz)aKMU|V`90^hHfKZm>)2{K zGz_|YFvh}`xTNkql&7IwsmZ%oyjw9^r191}-Vq}hDts|#G{q=4lA>$4i;ouN9VXGA zdC}IVvU|kvJ>O%9S`;D38^?wY7SV<82yi52HXTQT;5f~=qJ1(NP{9`mQocK#1af9+ zl!{GNw;J1xyB{j!R1}64%Fst+U@S*jX|(3tb#aBs4U}H^KbEqUZW?DiAj!ORC*?k` z#Sch7Hrrn$EcCyLXhe3}W_eA~o?o{EwrsaICI6XNCr(9BLZK!)x3>F177AVEexU?; zE>4T!BvIRbFdVDzSLb4T5CMqrK6+xB*I}ly2u{S6xvAk_K-l9olea@Gmg2zi?MHOe z$8xb>W>joC@YFq;dcDu~M-iG6Gu#KZTL;*D>`!_#NsZQ4E$jIsWa=+!KQbr9-dV*r z6KU?`2^?b1rV(CJFA;lV2(~{T-hNmtq9%F3n&`vBpsJ#XpT_nk+)Y}NVx;CYPGtnu zjamq)sXPfPmAGVwbYD_#c!?MC+GosQ6XP)!XMWH6 zF^Ac2uhkp9w*Jr) zT5q;q{}eXSi#Ua3&xHko{cWely)Xg|e_RXq;d9+TpUFRPCs0MTQ&>BaXOTLQS+S9| z4kGZ4Yt7#d#VAGjKhLMIioN&p9)~y!2u_=D-*LFhJ-#_yJ8%d}cl1ortp}4?lKA>Ns7at<$O8(-df%h} z0Ty}T+CxhJ-cL_snU_7E>aAm>o2ixrs;BMtfRS<11;^_w%Zct&(gpnSYhjAXfTYLM z^kN;!ty$3H$jA-O>bZAH6B+yTd4u97+NWOL_zPhRb!o<%T`nUaS2%U zWLfpdVOgZ8{(O`;{uyrT)mfjs(T|`DNB2Y|O(cs} zhMp^FX>gTTs}0qLJmu$_a@Uzc+P1Sd&(8*R7XufD_xDjE4MXpJtE)V9I3gc5;9^jg zL3l-ysy2xqX|d4Z#6r5sm>kxPeJ`!rZg+|spf_F6Qehr}@JC46k7{(^i--#Qhgv(N z-U8mW^Pk&~-A!{6HWxojdmSR787d$4v^%}N{-K%lF?wPN+r4djEO4LS7&H{>e^_~3 zAB-bjTN6>INqI1UVx#zq33yjmdm6ihC}GAVE-wuiPuYO^79S@>q;s*Ji&vd+pB_#r z^3+pFiV^~D{Jx%3XWe;rT@!uWpI`DWSlSpsFpT1wA2p{OxJGKd6W5j&O+oP7OAz!8 zAR?DmNp*=p_p{8;ULCCz4%>2CaOGA`FuY_F{O%j8w+z#M{o0{31&tK2eo%1D-)@3pq*COhTA5eSWGvAqrQcUlL_>lj0 z3!q7ncnL)AF7|rb*?qN5f}7vG9~J`E&S$#?ve%bQWS7>AlzuD?<<3}90?Q<}Y($MuB@vJ_X7KRVOT!{& zz~4Z59QmvxuqG`~T~b=ima7*J4oFAI!E8#E_NDk!Ffxnq2PsV(UiY|#n7SZ{dLoA} zv#1?3`mJWGbYN+n<@a=S1EjAYy3e69sUR0KstObxxoJ1I-(MjnaB%rg z-jPyrKq?%WNY?F*;+&?_ZVGc8hES9{38IQ9B~o8zR7_P0RRqIe>2lpf_RY6mA_hJ)x)O3x9+LCI`ej%s#Rc<7&uC&?3Tka+6dimNP< z?)M8HmsoB~hQ^=f69L77h5?myxBNCfP2&qTv7*7-)^297gc6W~Va_%is&A2Iq!=%i+8prQC( z&@|p9-coe!^6eK*IZdoiSx2`;R{j9`Dwgr1OA#}GX`+RPVtSV9F#UFWQC6B43w9UV zR#9>9p!h&>6Ib|8EHum4jIQINQlopg*^$a)J-5a-4Hlf_g5bRtwxdok8xlbF4F01q~(cPaT*7OeKw5NXBF9boQ!A7Uy*P>U+^|eNjtu9reQ-?zXBd z=j8!J6l9#Aa_ep&9Ts?%bhVxNl_b_s#W?t7u&d zTzwc+IRngTQC|I0tw||eKE}fJ3LXhtjarEiiYvd61&|FDa{E!nO;eLXNLi*z_O^@2 ztnZ6>V0!ySuyl}AXp~QU?rg`~rp|lc1v6zEvy$(M?-{h$?ac zEHrlP@VO|2bdu;h_^>#hrv?jt3daVsc3^c}vR9RSrJzh-^y&Fa$v%51_uq^_cMi%e zJvp7{W}JXAVi^noC5#ZKRL~KNGtU{>$Q(lXjrU=Dz~x79RO$~~#Nn~IHc7YL5>|yAv5~OCI8Qv@ur8kHtuv-p3jDz!B zoD=|W4hjr|5+*ti90Td_sG{p75*g%7(BSJSK}rx#0)N`P_dcQ964pN{tYKhvW-LH! zA~TjC9tkc589f?&%4(`HIG>S@(pG(KiOHBJ7F1^z`WZbK`Z)Yb8|E((D87a-V zuq6e|@&MphNR2Jt4`y2kOtJIICTck~LiBQeWI$Mm`|1PYT#jrcLtVal)Gy65^S^^; zgK$KJL9Z!d2we=_sBhTeVxo@RPx)Xr`Gy<`TF>)CQuH1}Wf((%4+k+wqv$9SYC!O5 z5rRWy!Yn0W$tr349RyJtUUidKKr4xE&-yQ_+zd+HXWP^KF7Pn5ioz+4i@yT|^+Euq zW}HLb>8;SzINiy&{#9Ag*dwT#ys6rR7S*os&**XS(wQy0^Ma)=Z;hf`Ajo^vf%`=F zL%0I)LFzzMQ?qH8TXL8XL4q8~-QU6@lL(-2FeAu<2;F^A-lI@Q1ET$?GM(TafmF7BqMbKgo7Loo0=ocTv&@A6^NfJ&M6}$ zjRO3O|AXK`v}#!4*xjjdrWW{@Zjm4~z0(0+i@%kK2Drc>Cgkc zQ=jy2s;KOn=O`mI((vrJB22{&K-ic_K#wT!XbJ`N(!$-(ROAq)Upf;Cf%YNdD6pX^ zh-=>CC@rYz)ofa!NkUsbJCnlf>jCSjHW&FBDX5ncrs^2Y*#So6k(llRh9Fzgiz54H zZ&ONBf1**=Fc9@lI6gg6P#9GpBPrUuky%)R%uxGT70-IO#QP*nu#s^)sW?Q;u@y+XfkAdC;;H57&5YVV zQ!z9Zii+IDVZZX=VMBmNlM|PY3UK|Lw)-?jA>}t)X*SWU)j{22#O#8vXTU# z2#R~ka?c1v(MNqBoe>M7wr8~Xj03#HhM8YCs#wYSG?gg?qYEww4Y_51>jpxqV-$!@ z@i#@J1Spd3?S9`vnxi)h=oq1`2L}T~Be_1Gk-+S&ivv>Mpigj|aC1lyb@>>j1-3p^ zTLm{;mXp;wyi(70-6S7IkMpagw+5v+=rK zGPb+}IT7%W6G0U=^2`r#?$VskQWs^h1A@f`;5YT;a5Z@#vZ(u3Ie^a4}i?h(khDpW-c15ax zJ{5tD!Vm~in3nPPpPONp^ICl@pK;mYWKE}(omS^>tKRp)?3Mq@l)_mGnAXcni3D?5 zBK)w2XB*KOVJq@bLAeJ(@9mR zop}1;?qI~jmH{F?KbtzAwi<{SHPA;t!9?*HATcd8Z7t_dDUyNL6Ey^IB$~15fMAC6*~B7EscG zLDUV)$vZnzC6FLSlD@Ys>P4Xkn@8U{!(P)+f@s16ISasnDs&a0FlG;{uOX+6#-Mg?+y+ItnX2k6~RZEif#H?$_SUYq{l!XF+NmP{p8;4v6t$ zyRr}qiK6!pDRv~eY`${#)EY1OKc!LF1H+`BO6q-3c}A^QM9Mq@4(N}B5yA*z3;V?o zxA}|p7+a_4h$5%P=di$V$JO|vP)P|Yd>UfC#YkZaqU=oiCtC&(^;^8k zvn<B~T8O$a-DF2P3&l%DWW9Txc zswZ6S@jL#&Pnf9whKNB`U@0AJT2xc26h<}Fld`~vZ9PfY7s?y61uZTWLdOT_v6xuE z;L2sKegO1X338WQS~nO@pcz#&9+@`qSO= ztT5Su@J|-v?`u4i4t#K2X|mlLcV36dpJg`{gIHW<1VL6Qh*3>!Xi-`X4w$KiGIbnZ z98Lj$`rB0}*K4fpIys1%S&9pb?!KiLQZToY3`guOdHOnkGj~xN1^W7D;X8d0)pLt> zzC)y(P&cEj?P!s7h#_@G`Wa3qQKW4KqG_5l`1y>_xuHGY5n+VR&dp*eH&z7FalCK% zCF&$uO}R!=lUP$k*dg+8?*6KuhwsAsMi@+BdI2b)^-;YWZxJ^Od>c8cHS zdMFkWaw(P#!(eo4%2bndnF)Svd(5II79e=JzEAtxQh zW54rjK9tH-1l~YpI!uP}tJD=xPvN`F(K8x@a*?!~e1d+k8KP!m_3P#Bjt1YW#?&)| zcAj|e;iL?WAup4BT1HF8HSaI>3$kD4wE|vFKK2r9hFn(e6+IGaPr}lY+*H3?iSAh5 z+&?*VF9q%%N05?$pJG3eW(3D#@^JmBs+`z=bwXR+D(x6zK0hB@2d!ehF!IYM<&9%* z;#T-GUp9Dh^eB3=a>BUwaQC!!q^1W|iAnZ|nVll(6J^KzmLdLeOTn?&xTtxOvCaR5 zN#y%v{ikxicP;l2IL{ZjqVFv^x2sg0bLR|B-WLOkRZ#_FH&&fFg@+`a-wHKZVns9M z(Qbq?3>E3wS{YSarI!0nbW4}`@`~d(H8X_&2$IXR#|@NZ!oX9yEwzn-re@<0O(H5~ zZy3Fa`ky;tKfXJ)hAU&*dP;1LV<{~o$#|RF?mCy!7}GkJJE)a&D34{3qkSGtyulqj z&qM{wce9CU=k6G}7GtvJd*x}|<>i0z-dc+e=^Xjsv%AhUM036d)i0JRb&Iy>yW{Gp z9OxHunIw7TvXm{mEVhy9x|HMY?K{2ioG)jV&4z;$wc`_8rCOsgq$%Id}j#YGhPKU@Y<30mgafQ++`EASO7EhIjyGgG&A1PmCpVKR$-bhG! zSSBUZ)Ti4g{$=6KDqZzI?Z^aHrY_fe2S-nrmJmm7DesmE!UJeFWC?1u4Fm!k0!t zW5=}ojtjyL?@`rX&j)Plyg{!n|3bBX0X1tRmp9UZ2IwG4ap|D1A?<1#btyrJp64Hf zbNH*awnf}trMEiItzQS3PnT{Zbv+3`2)3Sgpi4*pkQ3$K$I$o~g73I#F`*&VWup;l zd?%Z6u!TfR!m6Nuk(7$urZ(sl9LJ)WP=0yIJsPzk_Yjt}NTiehvtY1SZin4=|`7}E?WmP$_*QJ5D*c#Gmy1TV~#W{G5_+UExl&LPoTYn;=Ro@+c zPqvoN_mr`EU!ll@>FCElj|Is@o-C4-sheBv2C;lI)~MrfFnPJ#wm+>h!EKTn_j69F z#iU7_$NtcJxMiLe@y%T9zd{E07{EgKJ8VwPxGT-VRv ze29r?n$#gnR9g?NHtN1bCd?sSoJ+FVt`8P<5yxgi@F~H=2zF6i$K-gk`9AS^0nWe6 zT?EMyB9`h7jusRLl|8d^mzTMMjcpDShmP`JuX*Jy*Y|%J#0P&0emTUs{8Li^CBfmTRr>JgIm|KEKvg*ppHRtoUDDnA#S;@rmFMx1EEoh|JeVl5t z{i_foiQrulX0%1&u^3fk&O#X4ih~?Uc!t(>8UmtKVnFY;MP2~T_d(2{ZWIrMgB~0V zGt~3DJz)s(KYW*(*q^C*1UVDD^^!^xt@KV9luMxLHDh686uk-a`_;bKMWK{3WYLgRr)jhn8TL`&a(ieYn>8@Ci| z4B$sK20%CNjnzT<%E&u7d&e+%;WKfOv_#RW!$h-eC`8>!U`sn7i%jR3N7;kCSQvRs;X@d%) zIb;}(uYXXG2hsjve?y_k`-dG^8+{9P@)t~4@{_e!&1DJr!dZ?><0ZiGPC_eUy`&c; z-YpG_d6ii39;O<6_-1n+CO?6y&VK%|6+qzLt*qb0Bf{ZRU!p`C~ke@fml3mvLwNE>!I8J;L8(*fkxD zB$=GH{_>PzlCZel=eU)GL)?T3zP)MUZCCzWg7F&G3tmErA5cmJl@@tzfoxhbb$O1zyb)m!vQg))TWCA;4pwtCdqHq z|DZzv;Ew9=ku_2AGKw}aPcMO&ignbQ#PR~nZYOEPL>H8Q4u3+_Zc_qFpa9%MJ^6xD zlAtfdTKE)T3>cWe2v?iR|CGiqnOa`(2$35h;(|sBAoT6Sumy40aZ_ws|L0w?)Hgbc z=s}?gadZIAp1lo;4HztyiW68oY==r149yV;SRW@3#2FUg{*QKi@Mg4UzY;pFo25?4 zkO}2~-f?uKM?^s?6`I z?^WaMY>_>4(SGAX$g^(E@=dj?yjXnLXZ{# z0qGLylu)`;x}@W~h`;}L&K?eD_uiRz-kE!6=6Rp@nQClbhA${xinqR$oa*W8Ds;^> zru7d&_asUpu<6u8Pq`FTe0aw$xZO2SaU5{&odp~LzKYX`-z2D&^Z*t0MDa})j?Xbk zR^VQ!KzRq_oo_w>xX~&`DAWwHA&VL>10!&1I%L39)7(IMX8eKr1`2B#dQ*7x|23C>@FK{8bo= zbS0+&CQc@jL-MAPp>DeaJ}C{KAfLsj=sSudQjitSqEe8h(?Nd=p#%zfD&9p~gY7#X z%F4h0=*}K3DI~xabq#D-{tAQoQ=n4$&@+L%U&i3TFRuez7LtSKLs80*xFp6r}O&vPJejLV*N!#+`U~;5JK??zjtpDM-hY zT?Xk3mZbOT3x>>o4ZHPIzjA42gQ5R>FCkxpr|e0`vwDcuu>IRpIDi=gf))Z`alXD# zV0K7&l8Zd4<%Wiz^zg}pxWCmW;`lOfioQmLnv};SK5gE&z>0K)VcyZ;k+FYBE_|f$ z!U~I50Ar^B(bQtcEA_0jwI^q+_hZ%!teJK7FiZ>*I1FSKM4C%$X`4KT^Z)IV1f14z zl3ui`{B6S*QGbEcJEzegYdbCI4siKW@KW1n zO6!W8hnhbNk$BjU@0=72-AI4;APGv-OGf~UgONF=m>F19_Qd>sCF4qNy3@f-)~tRc zv5yCI@*T-@h2YXf4_;bf7COk&ISh%o#-rknJ{)&pRI)hoOygy(yzuIzcQSY3k{Vog z42JehBuM;w79*7mWv<7+l)W7Go4SyhnraL$mRLuEVVJVo75ogg9IP=-u2KL2%YsTeL#eulJevoI<&|HT)wQCu8w7}>Xl?_`kfv8b%u zbFAT^>lv)=v+G)ldf^cz3(gU;Q-3_tf-m+fs-3eFzlA(gid7|0G~PliWUgP1me=kc zpT^6%sqvHZK=%4apr&h)_Rk`*RY67;pmM}SDtapK3#s?C5`Ox+%~ee(483yGyum?% z)zqJ1Vy<&;KQ)q|Bci#zEKnn=FK>Q+*NDofTNuRw;v$?N1d+j3+{=DL!5EFRhuU!X zP5ZaiDrGiqZ}bHQjdtLQ70h>MZwx37l|(44D`5-_ulopCVBv93JxIPwwrzvB_+7-# z0$h`nGvp;Cv;P#Xmj|^csPih%oCb~j0gLJBfnKx160~EONU@3=e_yfivKd$pd3)6J zSn(*BQVXJlG2)qzmeAy`TQ^p z1a6#jztN*d@q;}J@K zvz%=d@m_Z8s#Nw%8&aHtRTkJnx6?=is0$0~kbnlADv?b*-ykjVFFqmB*0yqMCd_wq zD4Gh9ev+zfnDnqr!q^rNXwZj*PxOKJ*H$Sc8^pAa>P|4%)&AxbS*nG_i~_kkUwMb(_CFkb@+} zT5&lSlkFNCsQnM+3L}YrYQB&WFcU-k7WdIn`K{`1wovG~wjabyiz-RHf02@3LDa7> zYA7K&qTTx~Z;g>;wv-D!4C)`)^9x7K%H*@ZG?Ij5FG02r$X51c)2AfzQcdiXF?mUr zLSr*_B(+`Ax?cBns-a&gW0Ig>I34C2z?US{mpdi-BYiZ>Nq$VO4Z zxHO^KX%&8fYUp9gU3&3JYEL;+^$a;D1tkfkB`Tkwjru%!q>xYVm~$MYbViqgs-^VE zz81*YkSyD+h}V(>V`Sw-m-2leF0kQo>y`14q=1%3KXvC@Ne9+kF%z&R$r=p%408h51pd-GY_xhZY1)*A?jW=Zx`EezslE0G8~|}Y=H+L+OcHXlE~#Ia;KBV<)%r#C;Zg1 zT@)Ippcg!*nW_l7>JJr%zVXPaIUhNZJ?Ewz%z}`4i#iuh+o`&K>==r05p;ET*?94u zA1T5J1oeMuJy{v1;`p3`ccdj$U@w?mJOcLUbWg z67uw#c~KkF6%&?epSjoEVp5WA9YnLayk-*w^3=aJG{WLOZ zp_43AaRBed^cz>V)K`tGWKSFnhSo(hk54_Dv4o>b+Z$WDu7xuzmYi;Zr)-=3u|t>{jkdc;mM)1!8;S~Qacukg}) zaK2~TD>$36`kqxAPva_j_)+Y=$Ti42T}CVFZFK5-LxU3%)$>poxUR{WZwPL_rB^9w$c3OTN!_?ZYM^X$@dwSni;N9BdjDKdc zZ*Y_$i?)qs&HT_%J)AexY}xECz<3CK6K?C30qU}_y|XTazj($|UJ))#^05A#Uha7MC5v3c#_ zTm*TDnK^s+!gkAUMt!m?>6KRld~lGZA7iUhF8O7KfO>BL?a_o`{N31C)sKRUgF2GpDz zW<9zHp!N*~xhNiBXKQ7u$N@Op+}aR>+p8Y#@JGIfC7b)-wHX*Idbe5j=|mSwL%qNWZFrSdEb<%d?n4}-=9kL`V~ zV>V7~5^H~{CO1@au?FS&yjmKgDs>?}ugj=e9Es-sL|osfw+x5zPhPM*4jXHIiqI;i zN!o>9c?32Nsga8;?i8rpj?AyqI0u=U)OK5$D4V@I_*mv$5G6PNbl|kASyHs6Tggp4 zHRwusE#&}-dzqJOxms;%K)9k-@hTEKc5>;6uJyX^-29$Q)5dYN3T?=5i#fN1T{{P{oMFmjgHS=eb?F6%@NSoZMT?sF zagoU~-rH2va26JGXq{NReO*kFV?T=9?osG-_MxJ(=Ha}U+nS+uTag`oAn9O}BHxVZ z7k`U3R*agE;}QShL35c+Z!dpp<|yYm3B#`M$>SwgACr@xFV7hBo+DClr*u>rpy$Vi zTg5t~99t1(>;}&~8s^jgSXsD>etF#cIUsaCEeiD~)^}N$rHnzAkXtzU`LV&khRuPh57rObF3gqr&)(;6#@yXlYY-C$hEk$4C9Wn~F_({;liOs(Ybd6z z;hV0?!J(8R^vi71gO472UQ7{Hdm2$KRd%tbPyNJYSXuCXw09=M3Q%FhSsn;SF9)vC z?|sB%<}%0=Stflo%}MPaN*Jd*vwfUtv(No~K4<*la5=hl#@znOXP=PLhQcYoIlYLa zX?WB(XHLnK2;bX8JgfKYHd*LK#^pBtt0Gl<`3(=nxL%79h_`c@KDNrPJBbqBIk)aN zZL5ouS#=L-S6s?FyB>eN)47M#Sss&CF(KrS2iEy=j*k`Z?~IEY8_)G=`E2mQoh&)y zEG@?S2A#%dnT=I_iffw5!j!I>jPg9z(+bT)GWl17jWVaqSwqcndp;-T=Rb%h4?1r2 z(x+c)`BAmR#Y?_n-{sxTc^P^}9GH2_SV0s9GWWCA8D7xA%WbjGlAxmJm@8 zN+MD?htD%g$+kT9>wgoL`K*ULu^^8Z4^bMlMKw4(qli(=pEi_wb3&EPXqrwDa+Jr& zf`a)q=iAGJ?*K5fC&+cB5YVjs>emaIhuP26@DqGdxSI&7OtKou5Jw}Z*9ZD_o3K^v`p0*e zV-4j4SE5#HG@{Ekhl%9C{2wcWuY`zgsSf70PU56V+73s8MpbO!d?To!d?=9M`?_Z$ zwN&WD1-`^@)Z>svxV)0hpa0GKC5+)Kf^aq#(SO@MZA3;zx(T3!o77_GE)B#ekhfI< zhN6CU)0xOu2G`^Bvv`I4AE5NgX_B4ij{F8`{)+TZ*84uTwY{K$ zz{W05_bnL-Kmt|5_P=*^>el6Dcee>Y_V7ly`?qhb@3OcJUF!`J_R)S5G~VstI0U8m z@IhlaABiJ4@)_{is4CKeS&b4B1HaFebDyw$W;ESq)fM&x$h{fQHG`&DCrrtx&H=7% z;;@}rz@GRAG|*Bz88X;h&QOmTqMfOufcg%i0db4wdbK9^lF*h4c`NinfppX9+Kj^v zt$ymN-=VWBW9lgz4JjW5$Z?!ap?bCiX1c)k;GL309_6=DNJE9@BHK5*Ez}+vAby?9 zvd9c@Iv_xDcM1|dCfWN|eobf^OMk#!PFfaL*enV}OE%KWp#P8fGtj)AK&rzbp+7)d zD@Ry`HoQH*m>7_6TjWF5Hht+W^FmPjrQR>mae!aiQ`Qh+Z!--n@omCuo~QLf+>reQ zA*~oX8OMKKd5|)PJ&z?`JH0kK2DS-!Hq{N3$Xlq~y(IWh^MvpGMWL=Od7T15j$Hse zS4%y~DG?U*;LJ!98oho*{k+#Rxjmht=NFz zGy^2eAf~<hN(X^( zl;*M|apT`e-YJQJ^^7Dy#%Hh+ebrAv$=wO#OuMhH?4=Ju?m!~_58Z0I6&v6p;jCbh zkBacqT&w+y011(;;7H1SshZ!1HloxGXT*Q;mJjBjyZ3{=oL}{CVqLC+?U-h~-${Dc zV-3+Z=OjLNh!*f^ZAA4v+qgg}_+?7QnIe$k-;Sw{Jm!I@{vW=hx9K>;$>n``@MQyHZ#hAc8Y8l?X^p7asvRdnN0i-c<5a z%Gl(zV*ag5JP7>o8N_AuDbtG9pSb?7C(okpaTswjFq#XE-R*;SujHm8^Iy za`o`O<(lA!*KnSTp?vL(>4YO7_`y|^%o>W5ufd)B;O+|i_r9S}?RCkR$q1#6nHjzZ z$&@lFx}tD^GY6b{gU(RUx2L(Rsn9Hg-IGyu9ug6a$j{6Hky@m+y6ZptY4)=|% zspxjj>8bn<*%QuX)$PW*hlql}H$PnpM-!}2g_j=%0nW&EU;jlw)Oi# z0N}9bVT7M-G{QWKt;4igSYY{*p%i!E8^9t%_-e^*V|d&|nZMJaQO;90HsQsqki9`ouir!7%+%$i zmw-e57LSDb7`rH_4|Xod@ymT!HELUMi$sVBO1mW6%KTLjuvppdjfFZ&Ed1Bx0rB3vEzyFu;Kq ze19APdRXmGE^bFS-hJEJaJ!cXfoS)*zhoqW=b{|JQCEoIy{l7;9~9spXiA{BdQX1ve(1t%5eVW@StZ_$Vzx!~6`o`z!ss&h)=sJwu zgeZ<95YB*<%qv3pzaAX~U)&V(C`~`7g+JDTze}n3@`kpv;NG2Iu_xF7Q*)Ax3EhX= zB&mOI=q`I%<{zN^wb%CrxV@1vQi7Id z;_@HV9c|SY;D8|-==aT%g5<6j_>>C&I`T5Ll+={!U^!GHY0?Wi<+7%+JvSbDrow$HxI>aK-+E&9ac7w5mziCt z#2smPO-`ItM^LEod8NygOgc<}Z3`uAr_XD!61p%$SzZYC1jdxXaVAB1khH0%Aidm& z>Z@&YDx*phh4-wmhsl@expqrh+^EvP6Y`M~fd;JOZQBpHIe;-u@rNwZ+cr{vqJF-hY~r>x)?J}`^Npp**EhcHqQ zi1I^7D}s@vn(elzO%l}SwOQAKz^#OA+(}QF(cFQ2mCPf7MG*}VG;ma6-On&C1=cs! z?56@of7%xxVsCWrQyGbKQbK?p!^7zw0djrb?zT)>(&AW`BlzF_i46~@NJjSSpu;Xs z2cEP5J8QYiD8y1UuY@G9kknokCO0rO`_spQ*QV_C+lbkXW_TUqe3>zMlx6sLPCtfr zPptbd@67nO&jU-UWG$EX|6yf~%7`Z`r^I2zh2*N0>zfJ{q`9#$y!bX-RQbVGAI zXWgCZ2E;g0UeZmgKjC@*dbSoOtTX~k6}6lU{>CRUq)^qWuMPo?kXOVijeUTq-3 zL0nu6*^zqZolwj3%HbI4nrU6$nfv1Aox8IIz{JF?u=G~aR&Kalw|`(YvggG#a4AlpiUw)Z=ugoSBE-Wdzuwj@O zxxHCsTc&WjTs1OTF+4*lNa91Ws{Am zZ%ib76IuH>ek_}Rxy5YWpKG?}hLp(ulqxg9tke*vLvd*Rk!q`PczgM`*y-j@Xr0ID zaPRAo=Bouqo!1v*mhZ+;MbM;@Kg#;;tqmWM9f~A--b{bWj#8|(jdm+)=9gm(;|_mk zV4SQ})-anO*tp{ieSIKKBiNuEy*wgX%>u8h${oPAV%fS)-aE2*CU7EJR-GOHt8eiZ zwsILQm2=!@ahSE;aBhM5Re=3f((|S5Z7SVCs>RArmsVx(pN}dP_1Kzj05c0 zgt^thKOern%f?PJy;{Rg_V#4T&b3b2rSR);{z(ycmGBZZCk07abL!AfyqL9$ejWDu zCfaemIj6cqiYu|?itU$Vxx0O7$3*jFjVg`o3T-_WO{{yBvTsx0Z10H074V6bZaNSC zcK5%U(MjI*Dt^|NEj7@(y79c*RxiLl(zgb=#!>H2x2wnH%rOyJF5y>{Ang%N>Ea`r zAr!Y8Lb6;8m2>5unwfn2>cvKO+jqpLQ%;}ZZ~XfAwQeb;e;_I@^>fntMfr>O zF*h6pOG#2Cx69%R$z5)Yh})~}mg;7`YKvC4=?3fH@9k8&N^X)Q7GyESaabrF7e6-6 zvz*7|C%YIM3y9pk22NRlLPok|uG3urDw&v|4i?FJIM)v6d z7RuWxW-;;4FAZ{Bynl}^O(b=*r`2oU(mrXiS%UT&NaC@7C?1RtJrIpJ7IDn%aK8~U zJrH%)YF{6E&$7nuIP?AB!*sUK$&QL}zcuxkwT)Sry=IkW8xThl`pb?4q$fJ#uKJ@ftlNy0_VG66Y>U%0o zJ3i}(m=n6Pwr)B2^=qTkUew0<;u$!0LwzsmsIdLQ=G=u-Ha@_4exmx*JUqr5lo6p< z@4A}Oquu-2!2}=Pd7yXPbX-M-#qcGv`ze2QFrmc!ysNKpW)Ac8w*@x^>47{?!`p9z zN5=+&d|2MLSIvu^)~jb`y`@=h(%f@|PpDa+hRYDctdgfB>u}#4jbIiRvyPsD1H6{*PTZ zx*rL&Cls7hl9F6D?-{by5QfQ9#Y_}j5AGm?kEs0H46G()opmhf`DWwoZ^K`mT=};w0j!F8n(kb$@!VN({t1r=2wFDiamfFHJ zF#GFDS@b(MKzO54Oh@~-#S(8726?Cnk$R-cVlv)eTb|g8Q)c^Y4pozcf@HBPvx~H$5Q5AMn8kJJ989;Vvw)Q4|~&8|rWmAOL@TKZBDh#_m{I z|8+f{-vdkl&jS62XWjKF;AHQlSS*Pg7SXGFxTn<#q(n~@M)wFnZ}kM>B>(FUE0DrI z#l70tU5Ot8e1B^~v0=_*+$(?idw13tk^sYN;vv}QUO| z|CMe6aVB$ijASK+J-&A$7sw;Mw!%8G4mYnz|AvJOE^7Hq!SHH8`~4u)kTQhy zd22o(etADm?(FsR_f^@aR4s44?-4^s2oDvU#cq*i{eQ*$!QKZt4eQ-M;oepT5C$q# zCh-yU?<8?|SveB;>M=eP{ds>k^}SzEK^XL8j?zsA-$SIEt{h$dv))etd!NJ11XIIN z{{E|JRB!@e#)=-of%N037Mo+(`x^c7h%n(Qs_xEcP^=1$@7 z@{kLr*Uto)w#G*O=*i<&&eJJ{bEr$O|Z7KgH3Kwv^z=@|7_GMFn6US;s{`N&1S|*FwM({!mq| z%D%QNF8?-anX$~!knCI)9TAHT2#aIN`FJ`pGpVz}!a3uOB>-LCd_F$q;_}E9jIX?J z6ci^pt~I9=vj!P~#J>p^!|h0=4<&?D1V1dPvQ-h?40~X2FQp)d_q_#gZq%zvJ z`>7sr9wmvw0{-}=RQ-8m(=2?=UI)6srk$$O1?^@GGF;Tn9pk;GK zN?)unr5sigFa*$A56>BpY>iF@r%MB!*{Yw46*fcv5DekA&#^6XdUx}s8d2X|%|a!H zhnzfH2rw>gXQU5aM=Caji3Q8Y+g z@aEi4!7+C`?;$u0#oBA2$1cG6>$1S)4AkS0Hzz*TN_Fk{rQS9=^kS~qjEtdTjRzuB z&>n`Dq*|)Csl&!*LaVu2RhscB?`KxM@U50Ed{$gH36X_{dPdubAm|xkfIH)zgz+e^ zA3lgdfdHupvPQGaQ)o5=bU()U%|7PRzU_KW0o~PwTySgm0xEIT`y!%2HH?SRhcL&9v_XlsC(2Q(Bado(u{N>agnWWd+IAkbM2Q`qR;@H@LK} ziwotMrwL_>iulJM>7}5x09mvs8jgXAtZz_~i)!blu?>d47!Cd?W3wOoT94JusfI_! z$GJs@`a~LdrS3l z8ZcM2w3ze^?Mod{3o<#A=>3ERU$gGX8pW5o9XRS3XcsIrmmw<0U^SDlkok8joGZ52 z4#z!$)Ji6Mv%`%q%iP``BuBoChoEqNcD{Cd zwc0(0#Gdz;UuRam#@vozQOz{#nF*<{T8-lEG9$T0-~95bSZQ~kl}kDQGtO4;&wE0e zOZnRI@<-3duARWHouDq;AmM&@)v3@|9-K$ggidX~pRC(skmLP}Tz{ZPc^%LFQGZ{^ zg%fcukmb~nVSiqimGsP-+)&4+_;(fa8 zphfF&vLLa;tl2|1vfZD{iYKY6KP5UJVV;hXmbzBB9C?*0G^R!LjXKVTZfVs>y{b_n zvHqlYs;7gt?1@vsvs}PlW1?$PM$70qj6cnE^uoDp$i6P5wTC#ebo8Izc@wiVHGct~S8TMudnxm+dp~P<6 zYTR2RPQS7P`aH#mPWsdFwW_{I%ndW2@d(un&9>)#ohbO)UEmUJ{Uv#?EL14UYxI@Z zNsGbs8&0z2Q9+9H;jDq(7$1j|mnq+f=I5@d`8tIsmSx=N52&`Q7-D8`l5R0;4~{-! z)c^jb5oD2e+B1tr>BRCTd;|EQckLYB+ZfxIK$HI zxLJ>6kg*Wb8g7dl1IKeJo{^Te0U0?R!UPE>e2cUz{D*@3jIM-cQ3nY^vCvA`$Et_) zDQwIoOGsZfV0alc2mAWY(VlEVSwf407&RkL#jZ`Bklyl|&2UnO&4dlr21x5EKkFBI zM!hY!UKory0y~xHB+FCCJiCI4eOc1l^SVuueA{i|)cr`+QHFE=5i~+Vqvrh~W%wg? zuF%)?Rm0&M$CJF*?=O|e4wF7!WtCn)$)*U@0;XH(YW&=0M&}RbZS8~(Yv+J}GNSNp z)AgxN%WmeK2r`b!n)kDB!`iMdi+Yk!sIV5K?DoEMEFf#PDxuWS%#_0WrwJzIg*exL zOl|zw6ZW1{tKjoB9xfIObuuAW{9+bpBY;+S-So{z9PU4kj0GJ~c)T zbuqe$Rnt$bnWPY3lo7>na^atHHP;liD4i1}9vm?)dx)*BtM}SU^qR~JH8~12?Yk#O z`&`!PSz>>qA|Q7<_(qQd3l8ejybY~5Ar=s}dwZsbi5`L(qfe@hpVq}G6015wxr1&m zpq~fge)2`5^HH5;+FpooFyf084TXZK9yt-o6aE3(`m>P;tz9mDVYfe*4E!8=Q%us~ zJKD8}?*Xdg%XyiKXN5`Pd%f6VZqEfRN8Y{ZoE3l~XvFx7T z6IyzHF(`x~r^HUgZfk>AVOpMnRa?g`$W+yG4)(gC4bf~q?=O`4d_Je&95*%gfe&lm zR4J`X&T{z&PW`6&1A|Kpm}m_GA%UqY8HpyFeF|2Gtz5ysg3=6i7W`1>_K) z{J5!U{=QMt<7^*RQWC9bQR^N&cu-a9VIU-UII;6cx~%la+;~gl_44E`E4~J`dFTS? z()J?WxKqm?YOeZ|FHfHrVOMT0RUQTy@*)>}u8h<(I}bnKDXB&<*GzHIo*R9FDH*AJ z-;mW{1VnF7`Q8ebb&X<0VO#}&ufkSdBvB|iS2I(C=H(Qx}nK|3+$ zV+^99zNf6^m%bwsSLAuuHN1H$#uHfFZ*_X&^Dz{D9`_tf8U*H34!-Efg51F|HmIR{ zs7gk}5^Yaa5!eQJq>*+#lao$Gi1JEkR)#NSic+g8uLHZKb>B-gi5v!P@(j)d2hULcFyC-d=29vH{oNB30DvhDweb5VSLTWn)gHy zN4=VaR-d8-+e@|RoZPrWinp2yvHENUD;m8_5ig?CC*oMqsGo%gJ`2r7guNv0`$n{7bavhI($!mba$2S6jj-c@ zM$bACbBPi@@0mXUAA~h#*2U`dd5%Q(Xk?B)TuVFFQC#fq_M(*^pQdwlQeJ%@ppEM{sdFf*N zlDi$P?#8mi2PBd6j)d}2J?a_Dp_ROuckS1yQ8)P6mmuUf4eAj(tZ+rZAtOL;fVKi8gXuvrpfb8k^F~UI{fmmPX zVO%Ww?3uZQ*5d?16RMZgdmj>5ymJH# zyRXD5-~FSYl*FnmveX_pw<^Jkh3i2=!ROsN?;XlpWn2W+yR(8mfV{)jl=>xQ*eu9HGtPds2nFk|uRk^D4AUjP_JZqJ44M&mp3`YpmL>Jz6m3wy3hVUSVQ|6}{&31_Rv{+FHUW*TCQ((jKQI zSSQ;X#~nKQ9|2Y@!SZ#b;&t6d#JkRf;M`_N zle7^!J)5U&ebRn!?wwh};P=7Im85Nx{%NpH$;OKS<9iTMPYOwjxAa@=Wejn!&5C&t z*?%^+BBF?P&{U0OeD8CFeSJs49C{|aM-mAsQ54?yF!%x+@^l9(mG#$XUh4F#^H8#S5eR6N@^x$oo<$b_h#)Q>54csu4ek}26=ybzeQVMthElSpk+#=PEl zqRDrFYXjH@%aYNQ3>$_`H9f0-$_SAsrhWUXPHqx2J^PB;ysedk|qA5q6NdKDMWfit9j8u=H52!@}Sle zp}mPXJo+QY?>Y;n&9VP+{!$Xf&%>{6Q7=)#@Plpob#M+m?(cDhjHxuQ+pV#7pKD_H z2cnpZ z<-}llm&uzzN0Jp~P)@b>ugi1^8G#InHudr;-USC;RussXdMP#{G- za@L8(n7#iI5y?RN(J3lQzRB&L{7Ve?x9nmw>`d&DQSr=T!-(HiE-L?&Rle`)wP#2N zG;2~&N7&kT9NGJ^>k#6Len1nLkLxeJTqXMl7n^pb=mro{yz^nJFsh#0QIs30`D%=# zrHG!}L!|qFNVyJ}DS6I0r&SphD#@jkz7VE(k6rt=&Y3ByMBKM{&u>Hi15TqzS2yT1d*V>Jr1-{yh~EM8?@fMq zl-Rs8mQwsjfyz_N&#j+J|8v&{%@5&q8Iv6LK25t_KT4xKeFWqE9UVBnlwXjlv1##< z@InBEXW#)Xk3(t?Jx@c)Y4>znvb=ZG3Df~d#q}S22Zx)FLK*Sq%vi+?a-q1 z`B6e5J+mkA?+V;JD@`7KYqM`}bF{KDav)1o0-Sm@4(r+t+Jo~e2Uk~@f{wTc35f;h zB67~NG>x$QCp$0c*@}3=)4MI^6!Uea%X)jC$~AODByuiGTs9$zRfk=%aI)91)h2Dr z85R9UuO_3|KfMX7cvMyZ%n6fWaS->0CW^_1VNC{j75$HsbW}^CT5gXHm>;W%nJwzj z=TntBd-{HBjU{%h^7m10qa)7Otz)FtT1tWBj_y9tyy z<3|QeVSCCnj6OwAc8flL{8S>W9i=ecORgc05~iVDNB6tY!sX|Qiw9_6<8Z5N@)c>$ zj|__)B@eppPt`SJuSHBH<;Ngj0P-3dYIHkM*h&Xssl!C=h}65g0bevAN29*;5vWUq@S;B z4ezSBKfol)7k3pP^(RO|6J=CN&BF$F<0G! z5!+$y^twBEhWRiY>i9fu|Cj%41myzb8@Ex-%o1gQ><^#(5!|objy%khw+6;~^|E)i zU*Mt#!YTP7xwrHK-+4c?{28X3_X?-)`YM6RA?Z+(7u41tToV@e-01Sf#}MgHW_@NH z$)35frH7`^p=Q^SnuCs6Y@s-vGP8@ZgAk}&h;3nGw#Ccj{I6U=lj=aV#k9TTjXEMQ zH6Va6_{d&UMPF+j52i<0YV=Ns4!%NGy(qA99{XvI84tE3m;bp3rFbI4GtWP8{kJ+S z?ibK^9h#r?EL5g}^QdfBUsoTYe04UieK_84^DfuJzx{3LR(Q^j&o#W~@_F{`D!Q^S zwcRR+3HY2v-qAXw^yadpIn%5MjMM7!I=RF3-rWfBg}->arR}jb=jq4Vq}F^r!vW>H zs6zR@?H`lqrh0yrBq(wDGpvuvYV$RL)mE)VB7-Vj6_3t2waK`sb1ug49~oz5K#cOR zsN@T9es~4C#m`@vKudumRAJb#nOdGiB_Z9R^97gh{26+?E2M8d&l6v!Gid6R^2ZLb z#LGGjRxL8Wd4DRb4^l86f~OKB4rLTb1HR1;0MkavXkxr5`Re9rDkG;;zpY`v_5{Y( zoK98lzEj5p&%Jspm*mLERQD53{F40ZJhOU=2r!N|_4USGq)y{V`C?1n8d8R(!$#LM6jdMKPW|FaJ+_tAz?=Y4L@Wb=C{L&L^atUfn{SP00%aBl$kPF-t zrE8DnxRA4l6_)G=+r?t6CE}^1wrw*rtC+7_>W&Az%-2*;O?cKbH+W9hN~>$PPME(H z?i#rG>T_M-{K}v1wRiT&^Ix(VmLnI=(H_t`Wz(3w(m-}aGYQ&x8AT3t^s<;STOT`> zlX@w{9zem&Jf7HjM%DEd-R_-iXjEe<-_luuleucduVGru?Ir<1sjfLMze9nqk)oK* z`Ye8i7Vo2*E*^*ZJ_*8u4grdLDxG=~X8BK2o1ZRzDQj{;UN-cK<}<|KZ8zWYotJ(@ z>dCZlkZIH)XVi|)2V*Ozm#C^%wx0#gB{|!R^BiPL$=;@K;Z#`Xmw4L#!=i@9irf0+I&=lsa^G zcXu~PN=k#2beGhjOS+{SB!xpumvl&Xcl-u@zVG`F7Z=RTp1t;-`s}stTR>eTdccp) zWSZFSthc3aF1O=zv7^Tb@m%xo?|OY>6J=dN-&|wkdgxNc@bB3-$t}+~&>Sr}0mYE; zSFM{0i*%6(wCe`(OLvkK+$|S(OE-tC=O0@oHXiPZTB33E8-`u^NK6*nJX071p6CvI z&M`97ze_XM#O!RSWVmbQ1X2ECj5LyQ`=ao~GU6`_DYi;v0%r#B4KTr1=#sRn11>%S^v(Ei#d$h@sCErrNAwuIL!!F)jSe~B$nq}B?_qCkIpEB ztiHKBW>V4KPf(y`G|{t5(W6`t@dUHDfx(UWBra?~-@N#X&G*w&Pfy-g7dAgmrjjBw zg?}Bqes>7AK|r+5aGFP_7Tg;@tWl^>O3xFJo)-?Zuc06DwtXY@Le@gGk15i39%GF_ z+2UYSPlx@K@UHP42yA-}Qu~0N@``xX!;I$Yg@DA??NMoxUV-$;Ow>(ACr2$e@U)x6 zYWbS;^^&Pq%7 zUwMDH!Txe)ezVjd2d^S|{n4XtIC<+efa)EKi*@8>W&^+$`%0cSeCG1H+Sd}r6mCwt z8#U)Y`vr5xZ{H_OTVj=~QxhRXQxyBJ6{O)WSp-M}-~llU;F@{vUJo zUo}lVShD<;Z24PDoZM#uPeF_Yl*55e1zwnfP4nyd-z;3r4@={QKNR%+>5l7d=F?O+ z82_IZzyS`kX~wUZp|W_jR7SJ;LbV)`yIG$7N4=cWVd>ko3eTuik1JJq9?1pi@Aha^ zdu08v2h%ed6q$B3eil*=#si&%v22EU%A{{4-m^RCU|VDjc1AjvM~o}kP3fGgs$xZy>+PjbQO1tjLcU^pQ1oRYV`FXnRDfpjB+=QeJ9L z;;`lIjO-5HIlfysLDPD{B;tzzBZ2#y9QU}Uyl8S$8}%`-Dnvn}LC)NO=VAOtv9ZZe z?B%0QAPmN4ThqaSWFT#pW2`DhXsqdX7P== z_phcDC^cK0O=1+vos!)N^aKiajds3+xp%#)8$GS9R1s|~|Y zDV?XGZp|t3axxv^ztj{wG!2hGOu?I*+M3CV5S;wtJlacuhIkp3)xcxmv+wh8^!m{J zPe=vD-sF1Jbg_-=TGFIdTF;wkonqJ+jXxBNmyc&o_rHx{+g(q(<`wymjOryfQ@rlA zw%>aVMe=pTwqaF-`r@ozCH;xL8lx=w;qc~Qqi%@$ENdXH*PQWW=)}vSX+B+yZcT#w zjp&~em;n{>-6A| z_~K%ag%mt(_0FfD`eX@wlmHSTZgP5tuPvlpg2 zic~MgL1#cEjsJs8Y71%nY+*ZBRdq)LsE>0c=4F@4#^^TAWtX?+u`_+gmoOO`Z$d|)~kD!nSW?q`GIiTuw{7jXg z1^HCiwLJa%L;RUeE}jjbdt(X{UUp$Q?J0dMW5e1Kzu3$l!EOva5fnj{`D!BQM>b9_ z71{x^2j6_8Ucdgzkk8WF2w5)$JFG!N7)~;{D-u9(VJXCGFTh#fgz~JBi#X82>N)ix zMFeeQ#UBP%3%Dbuk9%Uuf|_dGKAB7#nG(KY2?xbKz?odarsNB&?z)>;(SSyzG|bS# zPMC$&-p-A6lI`-5eYt&|v16-+#JP|vT0Z$!GXlJ7iRY3a7G@nWREZQi^8DQ@H0ziI zD=zUp;(a6rnJy+Nl#3;PYKr(rt{7qplD4Wpcr}>abU-jgLsONeA6aZ5R#%ZFlJ~jo zU2}Qqfj?*`5PSIu^yc^Z!$*zL zP2!)6SIQPg53Z57a|1^(QN$R$xS}G4QApM)RC^ti1AGFdEx4PIVz$kfN(0vH>!yeJ zpWeD6oMa*W){wfyl$vsrvTq3@_7~e&`FMv?O&gMJ=Sj-LOr~qo)pmZ`uh%_(XI=o_ zp(mDs2LL|pOVbfO!s_h74bn>hSisjF8=Cx`bn?aEmthZb05>+Yv@t?sUmyYF;gm<}O;{7kh}b6PeukZG#4M&D}6?e(b( zuif9+#G1ZU;pj1YJMfhS00MH7LYi0US`hoX65ik5CkSX^5B(e>RQBD)lBybF}N7+wxsQ3{=@hk({EaV>TELI8dL_NDkpSO@ZhS+y~ zfBvBmJ2*Mja43o%-rJj15E#A$i1V`hLs>^!n4u;=1bQGSdY57rLWUS{OeV${YFn$wo7FX3FQhD$vY>;?{hkJjEo>b=ElM}K6(h547P013su9l-sm!8dme@&}>CwI)qu zwDI%mKi=dwMXbc_G`Z6m6;-(*H3pX9GNLm1R}#Cz1A|Tq_%2VPfo164aw-5r`@B3O z2~!@9;V%*dK8dtzW09;`Q~C!vz?FP3oSyr+gS_vCoe2CI?CyR$XRiJ&{sgdyy`Rv* zwhe=cqY0rp0_YfmYhmp9uV&kj`Q;ZWEURyez}NShB$h0W`F~)P4EQ@;>|tPUFcmW~ z(?06o=9Rc!hGGPzb(mxs!~?m^A@Y!a#f^SraBQI%Dk5(QK`Q9T^XN$bK`jQ14to^d z(kVe}SpPr*4q=9WFiiq;lu1`qrbq(f8v@-%3b*@5t@cS@pds>F!vH__eRcn_z4hnG za$lNQPbn6bpWPGo?*IT_WCkdv`R|H8mz0P_Kgke$;bDgy9#R=YkV z#Q&INDP`rw*z2;9KQyMtQ2OZPwH{%Mgs+j+|9BRcRF?Y$J?f^reWE%dh_Ti4vpx06 zKR5=cEJ_%Tq&H|M=1vU{&jz^SzHk6eYN{NO3b4Ka>XQ|-TAXtqQhud~s{2E4&gRM9 z{6nU|zvQxNn?g*4t3!lZ>!quq!m}B2#FYOVv2b65vKYD#u{6RW5ipF1^y-)!&y4wH zE8Ggf$3mNKSrQ-({I>SyucjA5)?uHt^k3gaExO)zx^*1`{cgwIQD@``dW+-1WSuTX9%Dx@!T?T zmPIfMbu^E#2+~EXxqi&s|8+!5((nlON-K%dWShnhaDn%}K>LsA0|B6m_mvS|i#%#= zQToKi_20CY6J0s?ITinLXLp}q;r(@)F03QX)zeejywl>}7z!D(z`PcCaEyEC@M~2^ z0%^L;p=L>9|C|IHCCm|kiM?G@rGtzk!g_nIk0%Xf$T zLe=2^Mi_AGx>xxm$^i*shP&5@<1p~)rZaZwp80Qgz`+20bccBCtGm>9$)jx>QQs!DmsBGZBau$HUI>R)gC%Wno&|>BbJ~->Q?pOc);_)I9L=InWIi?@{$Zb0tgz zf30>7(T_#HNHrSLw=bU5Vljq!G4daYB*sYo7f1V8Uw2`VQhzOw!nC}}Umc2Xl%oKr z4m#=TxM3+1dC3Au#<^eFJz&<9iZm(zhwD8MO8s2}M>VLW#KzRo5UI(`{{MYM9P=0k z0VF;T9=U0~-W^q(CDI4E6@4W7uf^nVzy!=`m4%?wp_5Zo;9vhM_68pK9im2Ud^pZ* zK0>@O6TS>zIilYDy^&Q8z^NgDl;d^HRgvc#Q4t0HjinaYZ?TJgT}?mhw5nO?Yd5}o zLrCB{uqU?hn{R)w9*vle@5nRg^Qnn3TRSX{xVN=B@sO3o{D;@+^q;T_hc~^ndSJ&j z3qG25Z-Fb3VcM!a+;(?%Tf%W;dS>2i4i@2s^Mu4pBqi|V-D{X9Mta*aD8lYi&Y+qe zFyho75c?AAUH*pQ9upP<-ZVT-8QItJCUDfKrQ*3h{m%M`w|6BdVRgl~w*D5I+6wv= z0|qEyk4wNvsRtI6&g>XZ$akH4hEf$UyQvlY+D%i{VMp2`thJc{WH zKN5uN0bR+u?rar$sd$-v;xWR5^yJ^hsn{7nCPplj$Pc&k~{+O-!YU zCLW=SaoqMKrV!Rmz%BBu$x{Xl)03)A4(2E#Q z=Z{_Gjeo=s_{GV|y&wc(l6Ij98Irvm`N_%N6>KFX4C}p9PAx9IwNZO=>^!PLYNJdY zGHH!scP{NKtF0?TZ~7-H$SZ7xcvXc!O#gtgLa^KR}2WE}or~Y8?kJRfPja$W&=irU~jV=8@M5 zT&lP?tu_W6^S3-l5@RBW_6pnKr~FPlODK;dH}^nFn}9Uc9;=*HaxZA_Lxz<6G5B z)PTin?pqsQD}B%q1~ap6S+Xf`U0d?3&#S;>=Z@uttjc-m*}Zv?30o^DkNOA|re}wz zQjiHSpY7ZI4(xLO+~z` zvZ+mp5!KwKCN0Uo&-AtjeeRddS(>DjQUgnU`carghmB4}{RSa#NR*@~epXaOv$wL_ zKTyJfyEp=eO!qVv!C3EN*nj>&qsBgk&|VpstwNXY|g<#Jn5fd2?x|Hq}qzVZCmWQUYBcf^yv3 z%;V@c-X}$C?5d{lFeLrCbl$mZR2fRp(`+G=IgltnH7qnjyyo81t7?MI3U?5|_>l@n zoqmx-xL|1@Wy`0N$O(&ZaFfJBY=%X?ElqX8j)tirQ0gGw5Y2dm(&4X@uIK+cA+@Z7 ztX=8n6h2)ze7CVyGA%Pp{9GroVHqTIpZFbW>)?k-2)*lJCUU$PTZrSzPBtH5db35m zxxE)d$0sV4w&*_3H{Cd7H<7yFOK%WGwO++;aX_U!SHQM9>^b1TGM=oWe+^qhzsHu) zlYOt}D?b$2F5t$Js8`RwIbbzXYRbDVXsVY3Zpzz9Wihq*{F=sa9y-@~OH-rPhp~lO zAHz5klXHH$C@3)`5!a%To&@h^vt*`eqkX+8La6va`DWRP$7*oUVBUMsd+)e)nDuI5 zujakRO_LhEa}or`yJoDVR+KW+JIen^}?fz|Et$ucE@1-h~5q3=?EeBpO&28s* zybZO>#-#-BhE+r%y(~S^-R)&0+Gy{A2%n>E1Tuw9B=OAhFUsVJmwd%|0FbWrxlT>A z(k-|?cRS&-Y@-wCjYhEAOFyOh`kHJr=A#}<%Ns=CS!*~#;Hq+AA2;Vol!U9md{yQjn@tVo{knej}vq5u((PW3~ zbKKSxl6)7kAi17F;)Pa|2)M1?q54!UkVqS-6`aMJ8`lp#=CyMut zCeuz|L*q%M3i@t^&PnX*lsbJ%?Eu-aDX>`=*__O4nfx z$$kDnP}WWp?KAnG6VU?Ik++pIW9Dhc^~JuM!W2G?%L;Lm*GFT<=ex|&gqQjSW?K0g zTG<*(1@+o8SSQyfzs-M!4q8s@wvq7Mk}$#1-S*X5n5E5p94`P#(lb`!S!*X2NhcHu zj}W;BqP9E=cx(fDoPRbObRmm%Nyn-wPaNquZX##Ptq0N_Z!7IQ?izIiUww1)S4~Z; znAyQ~vV)q**95RjV!0If1UZQC{*K)PJ;(FL7)GcIwDPJX|6`u5lxE+~_P zN7rf3OSY~%&t{ljTf zMKMjRZX4qa+QjhuQL?N|xecJP(4TZ#uPw`}__%qQ(LzKg!Wnny%NDXup1o~!+Ms=Q zK?=wjOX;73^jfwjstoQeew?)7-LUhyf=sU=aM~!Qs~jj%xf-hrs$!l!vXsCxI8PlI03!8@bo1RfJ(NiIB^P#`V+?I;4QcuUr>D)!Qr zLd`P&g2sx|pH&)y#s`hFn6f8RLXg|3>n2|BPB+d{KI`&z9AC5R<;ejy+3>zk@&`mt z_?d%xRqvH_{`vik>-|VYx!aojLpwMOTZtOal)V<|c$J%lAl`Y_{GqEFH#)QLVphp z)GvAyVvig6zhd1a6*dk=E`v#9AVI8*s?J=s@JjGf#~Nc^Hd6_YZ~rUV1dAx-?vQLUwyp6HmOSI(Yay}?LR@X@9zE{S@4#O?v?ItG&gPrli(m8ueN zEI~UnKwDlBSCGl?VmqEm=k?^vf5(=O?TRqSSMnZ&fF&#x>}VnT^T%S&ZeB|_>Gb9K z;=GS_4jn~4(hVQJrVfjLAfs*n*VvIi*0I6moIw&m5<<9fD$P1I7MV$Ca6Q?f!CWjf zZ7d!dq^(n}2SGnV&_1PjcL^@PPmM)(Q%fqa7xgw=M(sCpjQtprsuCW;lMuhy%RVdV zp-qPBSC4gk>?KdV@o{-hWkB?koGz2;tGzE$RG4Higz)HRjoE-smp;gQJcn!x;wL?* zxBZC3`&`}Ra5a%AgU=|bx@Ir98++gnC0oKU!q1E(V}8Y!ueGWpf@x=nL^^v&jv)oM z7N<61E`|E7K!maoVCJBvAE|y~vXf&<{#t{P-z6Xxm*z?g84(_+%a00yVjYHY1RQgd zSakmEWXt!3rRu?7SJqSc9Y#Zfu-Lg5B!Y&Rk}j!=WU5qBZBhefE_Mw}mdFSH{K++y zPt8Xb=y#wGYGXkQ<7m&Grl2cnPQ=2I0-VW#4#~YxFm+}^`NDyEh-oQXz`K2NZj>4; zmmY5q2bd+4>4OBKALOu*KpCA1@xL;%8Fy&o1cXHl%Fe!g=faynS>@n&+fKtYm(K;q zP~rKLNU+za%UdrFqdjhVQCgH36;k^D!p^7eSKFC0%Ce}AzO3$(lbVZ_V=bnh9=;FpW$t z`E2?%(83IOu93s2m@Q&wb4&&#<)sU#)sxH`6U8E~gPIj|!l0TIQeq3llP*LJ)oWl? zSj4{Cwz=3sYXmh*3sR#G%p$vK(C*`Sw`S=k9sr32TUWZrQ>jMhomwCd8&TjTr4~_B zpF<#SwRQPI>EP@kk#z0}8@egVndKB39S~G=vuGy1wi}8*|7(qyxZP(Tp0`lyG9D4c z{0204_(#PDvH;FHS)ycgQYSZMT@G3$8)L-D9rD|ZW&*UZcMF*2em{zqheaWkHot?_ z1m<(Q6^uu?YwrrU4~%?rOS`JHw7LoObVM~~Z5Gs!@s7|0YmV}ZY0E~w$J7(!6^k^O zYD5*(J6v{+EO`pd*P0ywl1)J2XvMFIq0|BYhzRnm1AT^9m$ECw!smC-k9<=1X%5h1L&Czbyn%pOx_eZY}P<+)P*Q>2R?v^F+nJm3fZ!F=+7tA zwNpOYsnR8kSez+B&{<&Fffh#?Xzq6o-!pw#fq9v-=7>FlfHM%vH=ODFtC2Vd?!Qz@ zOU&dD-0YOBa?QY0`k?B6EQc=L_u0%gF-mU;(FXrVHar6oAngSlDa#)q&-iA6Nn)r{ zzh6Bgym$;q_WLygKRB2g^h%rU9y-&X{~^> zMGQ6y3eb`9vLEKLFO;`O#4 ztt=0o!v89<4*?AeX1}r(a^{zG`+t)YQ8BM?%80nWd6+Cpz$gkh`I`v;qh_P>FP1Rk zW^sUY$(BmMdHgvE^fQpgsARhW6HciQTB8R#@E@oeB?cTuI#XXTh9UZWbbZD;_MtCe zw0TVEBx}BclcL_E&OYNbKYRd<#=wr3>wy6$-q>tdKMZ6a4Q&6zoOR}N1URW& z8~Ns0&%nXq;bVnY@;dOV}OhP9& zgA2`k#(F|O;eb0$X7nR(EISB+F5DN0pdAsvLbb%dnAj)1fQHUmzZM1(V>f328?YA` zA=%_mu+N#epuH^NDul?%P#$|LhJ!BGadq_1|)V3#KnFv%wu8KEbGnkMg%SfZjs%?BqDN_Sh1xB7$0=R<0*}34tx=-09L#V3Y`ndiM-#!VLne_2-$bciUkgb zaN>4gpE(4f2(1=zKw=@IiW^oejNGs1FL3uoGc5i za%BM*@Vr!#pAluGtBIs&{PR44 zIxKEaZtL{5^zP@wkS(wALFr)2-}oKlXU=-E`YWp6_dn!JE`?hpf|H6vD3*_TBgL1s zbe{@x^2+#-i8o%CZ~bvk{Vu?DO!`HFZds}^HF4xy0R}lJ(m${|*sX%bf^H_SwUZFp zgVV?~y-2H!B&HqJ)Pd9*)v#C|avb0r%Qb9A_WDwe{!qyDgbb=P=QJ|qlr9gER?jdG4sJLQ8ROBO3gK8QZ66lp4WjJ3M zpoLgQ$Sj;T64Zzx;DPl?M!$~`6jO~FvOz!v_(YdTE@e{^@h~f^1Em{cjY#veX2RAq zRM>AkRTDydCNapI76%JWvv*w4Nx#-mlozc1R&+8;0N#F_*eLwfkB2j4^2IZ&{`O@? zwys3eVfg!&%mM;{N7>8jbfTQH1l(7UQa^ zw`wVcimo(E;p%%As12QBBd&Bi6zCs~oJ8Alv*eA+pNbRu*u?gg&LMAbNOu+?eu&9IJofF(2O}+m{>~VR&4kU88f+BPtI<8W34qUCYkP!j8hHbrmg-~w0>_G zH~y{_0_9SA2llsv%gBp;q;g~#62du#*Wo;^GE;n8R=eWNdL0g=&i;W4IAIQ5OM0Pr zV+LMzD6_}1#N-D>?V*Quyljuu;nc)?4$}FB8;v5o*R4N@@sRk*pj|A!;MJZf_HvA$ z`GDF~3pKG6mK|(dfJ2VhS`uCV&V6>x#X*V5=?&uSeRAiDRJSGj z>~3~u5v2TsvQ&e$ti*wG@|mUW&9~%6kw2?ET#>}_5__3a;%lvDR5MP=??t-MF1lR? zRb6H--)#{CuJNeh05tDHrNGG*3*#E4l!A=U6@J$Jh#EATBTKERagGS;;yrBg)%r0} z`F$$~mpHTz5hPqJ=Fn_oagf5I6XpSAN+^?zah*HLVCp=u+K6(nxl2_d;8o&=ufOoa zn5YqnsOtNbb83Id-D zWI2H))Koj}`@vHz;*p6Zo4CnGlP8=%fQQ6BUbAW`6ZX_0i{-UWT8P;DJ64{~}@G1u{X)lON*V<0R2`5byUfNyn); z6)#?KZiI{VI1R*Su!KvzKTZuXg9@j_R*HCA#Gz)o^-q55!pX?h+}=7U6<6IH;AP|h z&#js#1j@zlJp3A8OzblAF`utyATbr?F^%`OOMB{jOdK(3#x7XTY?j--4Gx1P5K{h0 zo<$W-nPDAX_?0iz-?#9?dL3NWn$@pA+AmvJ%|Cfi7T{#I%WYKKC93r2LU_YJ_F3t} z5UQ-7kyza{83oEug#_YgODelsP)^zw0$kgyxIqP}asMqUCutmjufLH$@sp;r76k@AoUskx{P=RV=+WGWgZIfZtlvEok$-_vwpLjY^x2Hy$xz z*_SzMt{>CDw$^ZG+|5s}kDC^2)0d++^F(>)=;0qPc$91U-?(m?E?UM>R^gJg5vK;B zFz%7?^lrMZZu;r6W|!XFK&wbXTZxN0m)zeHicsx+h|H-JuF!9iv-8=zQ!CRb8dy#+ zUGa=S&Sqm%!aFEUKdL%zdv46O`9n@9G4Td%ps1`_sqNBaLCi+RP5OiUWtOFu0bzd@pB-j~ zU~|K9H|d1O0M;tVQ6|_zZ2#Su8M9F#p{h#R_&l0`!pD-Ivf-UBad&tJOKY-%0Zr%A zug+OPJLbm^95IjLt^^_bQ`34mDnU6c;R72q1Ri(f_2vTyh5JZOoz|qR)N9q}b+!48 zYWaWej`BcPHeHSK`GI=`6{3eD@VX;;K6v)?U5ak=_w=q`n&Yl+7(JOEBA<4S#=~e) zXdDBwp^?oX&JI!iL6Chn+Z)bZPkj@e=|KfV&U2$C?Y_ee2*Oo6A9F4H*TbEa;t{DQ>U(WXmtqLONZF&tR!eWh*N18Wc4suj9 zHsoQ8bsjcq_U&qhe58EV?2ZJ*br$q1)IGl^@PGdt($MKbThnTS(d@iWKBL|1GRTnh zyU>_cKfLU>1TfAfcr)MV@b~MNpypj0|q{S0TEUoSn zKV{v%cKYHGIJrmSy?>?g9iJQvL+sY}VNHr6IrIw&iHxMu>JK%6 z*W8vHIm)F$3tL@xn zKD5&he|#)`FxoR1SF~-BIZBt=S$7@Yc*-XA(D2^Mo4K=o<|WMYV73$Xa$!1Y z>n1jC7`Qk8>Z&K^C1AwszCYjB4DIx}lIF?M0?^w~6Ad;ntmP z*(?K8v1LXWT;>kHs1u(DW0$X0HEKbc*W|0*wu4KnN27}Ij7p})j)`4wA9XD19|Nuq zu<5-BU8XTaxUOjp1_}Jg4iPByUIJFnbVV(7&YbCYr$p$SSD_r!-<^mUIVdrN*Cnbf zcZdozb9_#&Q3>>A`!KtIo|^FdCi!YFxnsGz+;+RuF8I^VQlUy})wjWpB3LS<_zG#q zQn`F~uF;LiYZHOv9+lPxc^*&qnW(ovu16yLKVJ-0 zn|w;H%L}$$Xv-X4zR)_kAxsE}LhAAFpGpr-puV{!nn*ifY3(8eeHd>FQ2a!V^dOkf zE*70%xV_nxw0$d#Z~3+OJNT$-etY@^M?GO@pkLML^njpbkc=dBOn6;>F4^fcyLV^c zCkjtLAxO#v6P?*8ptbn1Q^31vubEx~!NR-H!b|422Rrl0`8A6B@TASptp&Vh`t*)s zf=usKwCQa*z0Bh2Nc?)Iyf=c`^`Fa5n6R9sq4Jl=WAUKDR{jE0f_5MO!w1*)k78jK z(H|#{jJ(4Os1q!$Cd=m+$CBe4Mnw7R^3cZRZ#~8g3X>G=K2gWseumqBmrXCu2USu< za>JGRm7xY?95&`|j*T46Gv@?8H0dqk@9u5T)aHkR+AsK`QPf>TE2!U+S^AYn*{c81 z6Uhe003v55_XoL(518ymfqWStQ-86(@t_Ae70(_rUmV}$dG0F?2tKL>WVR`__E$ACQ<&eCPMG}+iP5SJ~epsAS{z>8F zf)C_!;R*NVEuqi;mvNx(>z8t!kBNNZliprBu1%JFD?6K}bo6%Vi0Z6+Y~Ye7BT`(>~1Y||W)BZ-p&K8XyTS9|en zi>}nq<#99_^{9Nko0CaoyQHtXr{Yy!p$K4*E_t~3h5|gZnW6Q2zgPhsq9o@`JdAC8 zFTS*a>M7S~Vvkdf6RTd!9-;eVQfA)tlGpq2q8vGa*IpBe=WbH9@-JjueF~^ zQ}0iV#wZxfO&y)-ji9TA?P4TiCej5N;-3S2JIjOdRw#!|1GaFF6yJ*>MiS^%DQnN7 zhgA18Eo}@J()M7FJT3Wz*jsr(aEZx3Iq`Qb&WgB#+51x`Vi* zDXF7pXN*bnXIy=g<&Badwp6^!|ieUUn!sPypY z`_q3YS7CJcVaHxIEGaNNM~tgQN+C@vZ4~7GgXgf|bJ(Z6Qmx_uQ6(s?NDFCjJ_nG3 zB+FgrCiR6*Di3f@R{vNcfF^-V>G|i4&s#qR`l>_;VG@Vm0d`7t$Frg&ej!jWo9u&J z3kj6hNJ|FuHJ(HlQtrKDzMhDeh7}5KbxeXvu&u7=Wtv%xjba6|HA7ibB(0!iRI>67R-TPY4Cq-}m(hc7^htB@@cQ+}i^g$qg?~Aoy0Kr$Il;OL5 z0Y0+LNT;YG;_y!}o_~{y-%vtcRp4`^^;9B*i#C{8F1<3I>YBaWB%?l~+tZheMUi~O0iak2f{7>_ z#aq(6&=a_fkEq1L8p2uVFf8*YdD1Op8BZIO%&@(AE0uyk$aMeP^jAN#i7^9*R*x`P ziZ0;u&#k^-`@Z*o6LJ&9!I-2bkpKW*q42Xt2q}#L5R;IhHXIAH$h6dtXPYaVW&Ir% zWf%u?;6oWu0H_oKoSkyQ*2L+UXt~&cRwCg3{?`mH>>0hTlQ227gz8FmE7XI8kP&t0 zYkWP4%b_q?YY>1`@x!bkIvfU3gyB9j=dAD-Ycku_lrpd|9H3+l{lG|5;fzyE-w-ix z4a-Y0T}1W*F=3&O_b{`^$e|e1$#Cyp`URm2>4pUvb_)v|ZXg1YDENv~b$Y%4|3bLa z=1xA{19>(1J%KlXQ2~>xya~6%Tkc1oB;6i>$M#%iOeQ#Rbd*ZG7dh~hpItvvSW)?kax+xC)cg=wm(J-_}itic( zrx7cJtpkk6-=jvnFdM~{Fmo0R34!j;J^qLdr*hb7GXJ%;ffqH}!;jWwyv8GQ4dB+K z7r~!O%MX-4qxXqs`QW~8AwNa$5&&27z(MiWIEKCj*3HQA? zoF4|9Sm<9^3;^HcK%gC=_~by`%U3VGfPVlBEWlDp#0w(;cL$81_=zi^yeIbsJZhK} zVWL}xYfrmG{eh?3Vl zDAH*5oSn=%J5T&%T;DNOu1586kC=DXTvVOH#w2I_Xx51#Y&eAwngl&-zhC!kB^PN? z)v0q2JY`4}qWcQ{7W+jU-}_b;w}6>4V-78#xyj|tNR{Wq63D9eFn|0pfR%uN5X8FL z6)gT)Qz`{Vc{*I;Hs_1-hzzxwdudc7_Ct^ zI*ElkoMDRHnwb}!UlDgY?~y)%SK$5Ve&-{AYUEIMe~NaZ1dt{Tqi=talA8(cI7T-A zWo;_KY|2%9pX@n0c;}rcM}9qoT+ADXxzx;kpJqtxT>yaZH)=cWH($%#G}cA z!e|5kjte-bs8OmK?*)9Yxg0EN`wm9OVoL}N-URwKQ4e!*gV-B?C|lnN9-y`kjtud3 zUc^Draw7nF0vAK23Cu$m7sX{F)zGeP+j{4k6WEa3?~^|+Xbq$4qH{(H3M?1^G9-Lt zX5^vEcrX)RWZ5XU`ChUD_n0p*7Q-`mYltUH6TG3A%!JgJz+};a@%yE-*7cfb*r%Qa zF6_Rtk`f_;62e)pgIx(Sf_^FZO-ck*Pptn5Z2kNk^&*1SfB{bYcQ++~R%?vsF*)E6 zX!s56$AtliK|TU~9V7uSfhr#B+HDmYE;`Y+M<Y9=PIe|rt5;Vqge|?sa zq099BBGbpha%@`Hav!l80nSPm^5}yAhf}k5lKmP?jF*TjV#V=cOJO|d$h(zN={%{1 z@Zu@R2DtjP5%b0_Fp8P@j5WSr1H}$P%5c@$_|Z`;K1eic{SDcyL9pbr1dlgj zDg>tRdRC__#m!N+UcXd_``~4RfIOmP`eE-4tZ=70=1zYW@}I^&S>SO8`e;<>42y%R zt`r@l9GinE772y>&i5qo<|S=72mJ7}{Jkd38aZ(|@*k#7Xoh?(cvA-VwT?54a*6nq zqxmObcAvc`#3<1xO4RnAz>D}3=r1&~jeyrDJR)Z1 zn*aSCe`X;k<6c-i0S!tx@y0IZuwI3K`h9p=InnBh)2zT3^+Po0!oktZ8(+}ZeoAdC zX?>9D?&}XbfSf?8E=1;4o8Hi~w7HX4Tgbi=x{XYm(SE&5PSL-97Nr{gmk z8{~Q!Fk7z$#&%>6uTVwXvaRG)%B6JBRWfz9VS8kMn6*a}+(W!~w=FEm1s(!$2%YKg zNYC)xncsYT`3foUcWKt#`zq9@ChSDyR_C;I%fjwu3o8HF^0ncfw3Q+Ep+ zk^7IpBhHqVLRd)PD&6SS+G!VO*@Vw+Oh9c6N2vFV?cu~agFNUItJP_SLcKsQD4$eH zj9DFPXzQDsl$@%A&4YXx-nz{tq=?3S@T1n5Rhnp;N#aD(0uF@?5}ub4WkLoKs(~!p zVf!aB1Ps#Z^RgOFvoy=;mv@qty2Be|6FV2sTp?v2&dBpuuz)HtmAmlo=Jr?nYZOL- zd70F%he73UFjK%|v}7EmWDq#wHz15|a#wTr8)#YDdD7uL2?iP2lz~rNc|BDrR6wsl z3&J!My5nuLSg|&_+Or2vH6l_d(8i*5Y>LmKGd1NevhM2v>sa3GkD5t-v)4|hqHb0+ zah9Fkn50fT`RFp3D;^k{*?d6O@9sKBx3^PkZu&+$DwM5`tkFvd0xZ?2EIUyi5{TE-nktJyBo+vK`n#T55qvbd+A4oaKZU%IZxHk%1Of7E zI@$puxnO0pnGNh_h6<5^z_2)+(8qQ52A&8&1w<`zunPNj+3WhP`+4x0sNKu7{OQpn z-G@%(*=IF2kC*5T7l7fhN6R1#4HLys{1RPU@h|m&~Y}2lYBQQ+4D`j}>O|O5~Fp zyat=Zc=UwTRWdt$0?|oWUWSGq&jvG?1ZY%4j5PbU~<++0sR-t?*58r;hdVbz3RdWPkcP zJpIqETJmUqGV7#h36!zlHqk?|ZBf}6WNfc=4RhUqRP^M+u!+Rjjzx#nE^7=btidz) zL(?S?!xx!Q!xXq%KBwR*SxrGjEyyvTQC01mp#3dWxuwosDsP|#)D&Jqdm@(^D~HyQ zzmFo>nyy5D6gPlTqv6&kVJseOjm*+N_CQFms3*$l@jop9Mt`_>A4TAh@Kn`5<|=ZP zV11w?td1G_#!RIFEplhff#%m|^jObY)u5LY@)_~sysygL{e5_}rr~%cIsB9k|8{Gz zviSuMKN*G9>qfAg5ahR>I!|VZZ6|bQw;a6!d-Vxj3gE*S-dFV%wdcGVJaIdH- z-W!@zpv`7Ct#B#d0b^iIJ!}L2W!4Wn?8@rzH%0SDLBHAYrJ%1<+G>0@BNi`w z7M5A8MH9e3YpiOe(!kk+xnSF4HHQI}93{vKuIKJ}?NEzcGCH{4O+M6++HM(;Aw*B2 z!P27Wi9*s!2{F@ck2UAqy6op^88-El3f2MgrksqcaDj1~oD!O|rKm06Pp}<|F3Kof zf&eMhxi^i=R%_i?2e`w^s=LI>>RcuPbQok5?-H4J-62ODgPx4bW0=&<1B4(HKo-bG zkMLCtP~gLJsN#6&k>DT5c~{aY_pMSq`8XFF5I7G;=63xV(O)Td^{)P4n^&HN&RDH# z(p`!R+tVpNa-A1gq+$uVMv_2q3oZdda18`^cS3M?mtc(( z+}+*X8+Uhicei)v%DLx0-~9pK7!1aS?p;-@)>>O;Rn0=Tau2IH1|U9vN_oWiPJq>? z$me1t&Grt`a4nmBgX(CIk4f^;|1!LrYnE#eN_)IStvOyiek7)zeQ$HsxxHgdzLZ|Y zi6f`3n$f2cOj(5;Re(vUNQn`xjIePkQy$sJVMeLuRzkIv_kI53KG9lGg3n9Cs(5ju zve>c0>AX7GoUS3{L$zkUtR$Ah0@on7t<|^n?MvWG1re|!pO-p|<4G{Jsj>sWtH*~y zYG+^jRB21aIp^ZjY^fb=q|GOPM4)hE+DJ{B48!}A7k;qaled0f3)vhcyGX0fJ%-Vq zZo9M7(@-WZT>*CDWX{CeHLNeOe18Gv>SA`ma3rHqec5mn|L{J1VfKBMZco{3kp)L) zBxl6PmnnT5Fiw*|nmpC1L9lMLN$K;>Q-0)TleC}pS%a4DFFaj38SBRrTVmP$>f3mR zX_W2^h@)yTAw-;bo>oj$u5Kn(IRV`&IBq{!zin5|9#V(Z&sFj(R@eNbs&9W>RVYtI zVrRUq;G(DNE0|XY&9Eqbq-&6{H>@`@n4m3CFR3s7uzlRp{$2CYj|PwVr!$>X#2v_rBbQ(I1!03f6RmC+znS#koOQNLc#w&zzIKm% zX~*JVXcwI+s4;d#D?>}6evXa>(ut8ky$1fSzWEB8=3loSP*-t5@@U9j8FDlUfDAyOqjO77-oS?ah+-;0|5(N>ZsS)HRGA#Q?pyl!>BqYWGqZq>j=m@ueWsT3@98i zhcOKKW3NDdzVwgG#l{Qnl6B8!(chBh(QS3jkIyx4bm|``j%A>lbBO{VGzJp%ZvrG} zWdZ`|f5=AA7&`qa?@>nXR*B_Wjz3qnNfxWje&(gs+X_bHp8Y}5+m2E>M3i*1smQb2 zT6McLg8r>u2V!$4z=4J)2nF)+*RGe+#*b5S0_iwJFMD#nZAgDUqBd3NS7{XZiDsEq zambf*;-~Q(gX^gc6Lq|?n39@V|1ITyKxhY@`%YSAz9y!#3Y=u-M@@F~O$t>zjr!8# zFlEwDd5wT zI=rV_JddfX;i&0&%Mk{Tyv$wo@NJ=WAn0bqpJ3Xb`0U{@fXoEnknRJ6Pm11k%k`T> z?rS{Gi=!0FZKdY^=ppkTEG5Y!I?3y2Qb21}q~<$?xfR3Fuj?w{FJsG%n>o3glePSs zO>N`vEXSApcKVj(43cF5WCChdH)xZ(DxOp*;+&R0~8ocWu>$R}t3l zOF0c=k=K36K5o&rL?$1OUs!(T*FAk}?O5FMblf7={k%g(-D4@_bgU6jH%Kko?oPfi z!Xl~&{mh5=rM-eAi6CXZw8=;F=(gN~on^(YkXpM+gs?{Yn5$9H-hIQ(X`}ephIP&~ zo#MdSDJo3LyU@_}Q|gaaV!PgU(VLo**%9HkCgR)tY&Fz6cI-7{2uz|C5-skV3U|($ z`!;NhWW^<-95{U?+>a!Il;fQ%{m{@5r8X}g|96cx-Y8f+N(-!3tLjGt&KS$qD2I+a zv}{|}a@VhNt^}i>>0D}8DyWZ7vYPJm8cA^-)OW92PT3w)t9U8wx15ulcX!S&_1T_? zSb8~o;9(bH_ZskLzqJ<>NuTVt%*5$+a65SW0}nA0K>je&=uN_0xBHc~&CZqTZucyI z`xXhV=Fz)^F9+*qx83h*N2i5%aE^H{QAk=!fe&?VXTRokD38u z?}60oAY7VMd>DZ|%4qrpi#>cyxr{n_zu}^ z%@miZZfhD#4JInM#L&cC)>d6o>r0t*kwUhU>q)R^vpqAUV5xj8>+|-GbIUVkMbB4! z9qBb)?(6ZNoFyDR7RV&`2qaOR3;5AAd^3_x)gSv<7fV|$>nk?hPD*COEl=KsxM82r z2eVGR16ir>M}7~P`m9>+dJ%z7u1s->i;A8gXb8E{*7smkl$% zf-GLH-j=vT{0L{FvxN16n)TT|;VN#E3-fp@B?jx-DOQ^rRVj-W4EptLTdp9W439(` zp^{X_*&J;ud$XRd=8aPe|1j^|PTu50u4OrVDD>H|t0S-C^{&l^&as_y(05hh8C?{PlBWCVEa~ee+CJFUv*n0)VyG{u8r>!RLGZ3!pi3Xd?wCVKY z8!xD>aN$F%c?cE>0ndzfb!?6*6028JHZ%e^SA?q@M#O>Tt!Z9RP5qphiZF1oMCkSK z2je$Cy-s>KN*Rya)=0hcn;tSqrE&9)RQ_438nu!5qO6~Oj)SBy#m@NMbEA7TvEbvC zF$c+yWM)B%wLHlmDpKNka)=IO@6_$U_cM$?h$Yo$aWQ1wh||6O(TQaI-*w(SxjG{I zxFV9ROqQHX{Ni}Fxi)`?+@-}bDC$3`Gr5ifAAvYXDGb( zc(0fj>4;%~G!_>}7l7fYYrh_Cqa*wYNhe)4>_jauFGsWqVwf8LP z`M7^e_cUD?uQn;fb#2||AY8~`-S_2VgKmQXtiA+l(>0d5+td8TMCjvoWa%f*A9SAH z+j`egZz(41-obj`8V82B?gbWc=s=QA&D#)U%a&617JOT@xP_6P~b;f=o=PE(|h2LP|urGe&3 zR?lQhnzQZnslpL~5;Z{k(Y>eg`bHWJjGSe6zhu`v3fVVkBm%Lu>k#stlcwvDws|w_ zItR1OCMqrm^JW0s_}$X5`>-g<6V#)n)F^R7lUB0RMwJ^CJflTNoP-C~PR&8rJ0%S% zd`qx$uCBA)^kwi19#E5yguZ%5_9_bdmg79Eo3BVTZ)2yba%n01*S!8tPgI1>a$O9t zT)hOMjpTjkmqc(x@2G_hoNLJVSuUdTmPzmX z>QHukN?azXmDBQc$zp91I4)e9*+o|ySrB5e_MufpIb9EEom${8%bm z1_BZ)4I;ESk;M|7kS|%S6bu~^JU~EzN7(Vbs#=U*f|u1KJUWZBL?q31SPuuGfd9?J zyk@_t)iP-oseRtJ8cl2bu6W$4sGOGyMFhNkz9vv<0p8pJey?A!g7Fw-M}s0jIa5u? z>BRGC!t_G9U?i%g&i;-x+gazW8YljBE6M_4e?$TnAw3UB+Fgq+z+ZJ|lfUQV$D>D+nZN*Zq4 zoG&v!2j6nYs$|>=0YCi_>vQ&p078DoE=RsM1w42tSqd>LM62H4b;TdAieXk#hrBhdjn|k;~K$hXQY@nZ% zS{)QAbU1Qz%<)XZnm@1;j+oE?JwrDJkP%HR{_&sOGpPo!kICp;b+dyb%zT2j!6dXk zE?cL8>x=d#}j4omriBp#$w5^eoNacy*Mt|HL3;Jwh5rya-oM z2^?4HPGnu74-#NPuSaq)ZhEFKP9Q!9OCQBoOUh^pE%}*c(nkW;!SCJp^B4eRNv; z^=GM3GZ-d_SV49yfQ)t^$sPz`su9b*>9rS1T``Pq;Pdgvnx#h#)rC56Nk$AJJx?oN zb0G<*Z{(bHv8bNoNZvM7KsSmu-3?`evE?8Ma;vsq4P}A1rsc+y<+DWO;4m*W(C=|16TvrT*#+DYA8&SkS8NjAk2rOMZ4;t~59jzLHGFJH1)bviT(D|27 zSmAz-_~W6<^#7`q0n8&nqxsRxzF{NtDq|X&4{ChU0nrrw#`{gcT9~0zR{^Yh|%+i7Gz+t!Crbus?y+fBaxcdZg6hyC16t zau0)tS1e7w{PWD#Nv40DiLpvJ%6<#W5|8*R@9sb);2*)XKcPM#{(^c!jWG39drBe;0GXQkZRn>%=|CCIi#SMeLG5%yi9aT2(pKO6%k+P^1~}FQM)^Jvp53F>2;E=Sm6t|wKK-9;P=6US!}|i@ zyt3n=HjDcF z6%hz^{(G>09dx355J)$^4f{8(*xxlPeFeOxFtqWB{=eqqpYFhreBmW<%EE*EcUAv7 z{{P<~ci_RzwCY6g9c#U`1aR7^{SscEE-#{bRXq+Zdv4KBemgwD(VX}js;uEBfGnw`tLzc=y0ho+w_)JK-UvbwrZ#hc6&437RSl~)+#yxWJno}>S zJyp)?usNElMLW0TyjV7DWNNcVo-=8_cbZI?Rp0aU!<917zY_r{8eKyr>6+FB7BoR> zC{G_QKkzD&nJLN|po>Mq)RcM@K$TP-PL}0U5?q=&!T%>6?-P2*K?^gn-v=1FkaBTM zV?8>UAaR#!Y4A>cigTuDshF#SaBi(_Ue!4Q3c~$i$tXWx%tykGBO8QoSDKhTKzSKJKSlqe<2BVr&o9Qb ztaZY)T+&thzOfHLB?f=G+K@E=;(V6e*w%q)u zFD419K=}%R;{ESSpg)MID;{}o>Nk5)ZX;RnUI4;|F-|!Q3gkPj>;X{WnCi{b#}L9q z8f*$*7Fum3D9x9zgLu^)c7j1cBXip~8BjQpC8E5X@Chg#sz&y!N6J$r*gY^30Stq! zmM%1b*O+FP7M~P#QTGbR?nos7e1=3CX3;GrFSGT1q(VHuU*hmnwoLBMD_g~1K(PB0 z61GCVg7c-+xUUE=Us^*+b*gQAaoSV)jda1YGKdm`KTuGK1)(uqGIhZDz^0H+>g@Yk)!`&wzP8bC#TPH)lmK zKsO37rHKei9*~t3@V=k+zc}a}Fq@k;>)s zJesTZF~P?7s%I((G${~6nREN?3cZ(M+N4B@J=}H3nGqx@y(-1wQd5nQ4)8J7UtYK`c9jAwwNsK9iZY5a`#B)Ly%+0s5duE?x#E!*I8}y!`k~+ylveEt= z8gPx#FEYTbj2p3NFm!~@-y)TQa!Ump>q;*oD?!VT<*Fm@UVK;Mwa07jSQX-E?OEqA@sUa^9r=GadfQC(#;$zp zr<6WT!tAoli&+i3wf!X~^&h4z_4`N5V_UnGuKp@-L(>n*lH4yzUOiOLZF2<-n_ZM& z@uj#RPmt@ev@q-sU6lti_1-@8&5<;)SD4GpS8|m|@5x3wEtrQ@UEsSIk-{*Kdtf(4 zXC-@AqWj4OFJ;)6qA+uv@o!kN_ZpwJw)0aWb-xl7#T4n67rFJ3RmmI08%p?Rg~hHe^PJkT%~qBy9e~Dd zi0Uuf+}&zZ6Y)r*mU7hV4}5{XGr;eBlRlld&8|MxE9r2LjmA^&E4c7LmirM;|D>C? zWsQ%U&OR@Gkj2#fK5yavT~)>ej;y`X!EPaXK=4UKm{<#OASlkTN||KNa6IZbnToOi ziT!x>~TE)Y}x5{Ti^f!iEDwDdL`uDi8?~dM_9Ou}iCa-TuUHEqy3H?o z5M_sWnPOSX2wlydu@ypc+pDvgJnMB^KU&SXG)>VX2X<~ zGf2HfAEwZ5ov`7I$!{U?E~8|4^%D{oXxe!kGpuTM2mSlTXX0!dK}qSX72PVg7p09^l38= zG05-LLasoHFHI*HE%2K*D@*sgr}yS%6Jini`!?+&VhgF`wwxKZve9~n&52|+=;|Dw zJ42(N(N7D*NhP_r5BA^fRBh^`=_h)%7P(wI^M~T`3Ms4G6MjHJklt4w9FDm9IKwZ* zbv?_`fK?uf#sjsc6WU2HX>knS;cnCv=6Yv4CTgqBz=hS>>${%JW%;{#smNGJY-#6mvj=Sn!a__nw0n;)u_Nk2DDSsUOEla@{Dzj z=*}8^q$sj}oTuI|RlGb?mi=)$mYtuy@?H(^)L(BDALxQM_tPG2f!Fb25NcIXD0cNj zI5>NP7TRrl3gQ=J@H8Sl?@{NdBu)0j)d>4YD5Za`DV}$<8(2Wfj~8}8PQd>ql9}+p zFXSUO>=~EPxV2)V9%Ea6oo50c~%* zOlGAm2|rV1X8t!`*cuM~e0}0WriaAdlh_~@;2E9_uOrrKeSZz1=j3%313{o);Kv{m zg7~aM!k^!;w5DV7mdS-9nFsazUitVzX^mCya=!7lqY!#4t1K4xM*}x?tU-A)c`p#$ zm+|OY^^gsCPhWt0Bs<5#;iXfZgqk=&SFWxRE#VPwpo=~xqqY`kSP(SMP+E3 zXq@LEgNz=hChJ*C4I~REmgQD!s{`F?SbNH&Bb!%B zogS5BONAfHi9RkwUUsc_iU~7E5i3rdUz9TNKDivk+m8FI8X$u^0ks{q4#xqU5BJXlY)z=BpRJek-Ge>~@R~eno1M^3Yd0DMfOgq->))lBEfN&U4 zNh&~GlHF89ft%$Ghx~`%IG`V9&fB>$qhrPAaeKwB67IT$5~KQp0!Hq*sHoc_%_l}?mJ<2TdO5jbi$Y)YyM8N1OApyN zO7Xo1A?jwADbSGU@b|AUzd?+SUYhV8wHizTGUzFAW-&xJ1|I56aXKs72`%Ful~?Ec+c9j3h^T7N`G6L_m-G>~nK!8#EE0s2X9L3;Eq5tP*{ILF zJdFM-yZ#JlIN!#uSakU)^qL*L9s`5$4XjUD4UWPq2y4}6$%S+R&xUOJId*Y9Umx#l zA0HHi<3NL0p2NKl`}`n2xAh3aAwMWDFA>9)f?~I~0{eFKkw63&(7DDdFMTSTRVl%X zu~^vhC9lpvdp8Xhm@B$()UbSupD56GX*P6-CWTMS7BoVv&>j6bMXWWRfZ=N7YhtUW0!*E#?E0yP9kw*Adxh5i4L`2v(p4uBAxR3YAfll8Adt?z)y zXwdKR|7!{GIXx!~2(qheN%9~2{t_Tc1oZSG2}^wa-}e9c+;bNJpsW}U_+JB50s|sR zJvW&C#|XqE0Z70};UmKTaLaFaATu3-B**hF1O5`A}&6)M-t`gH1!{359P0edqqWlm`-^%3Z%M zj1vBa|AkU0k+&1PEg8RD~bQMkJpP0nDj(pf`C5smTds+ z_%G$>sDN>P*e#x|2%z+tSaHv?gP_166^7vRPOQIsJPr6Bd{(BgK0T-q0sQR}{vCV4 zSEMZk!nD5`gChZWp@ZthtR|P|%uIoOLBAU|RraRhk0JQKx!MyGaEKCvmPCsqC>=VL zU|--RQ0^!n@$v6&X{7?3t$_&#DdTC4_wkk&^_s~a(^(}WLixKsUO*plK%7`{1dqu8 zUwXo9B`Ljv*nZ)f8q(j57bOg%-K0(g7$|1QQFHdEZ^tTXYP)7Xo7F}=p6jdcC$p&-mFPXt=lbO~o|h!eMpwvZlxUcSZ|c#Q-KYTpU$}rwQp5 z9PRO?Z`r)w}0Q!^ftB#xclbjxGqh6%(c_V_S_{Vl@X@c#SZA}#9BS&hfh&rRlTXu@w z9}Q>Sr`{H;vtNB7cO7l@gMN3oLH@gQsmP^(PG!=yBpNMoxA|6z#e2|1`#O+F0%??3 zZ273;30KYIozOy_xpO5LU(Q>K!OMctb12E4HF$gZ;qYh)|8e#47tjJuo9|UXf68rP z!(A4g;{83`$BNHsu_3onE%@_>ZI615L6M|uq%3qM)vo<3>-}HL8LbR|RF-PTuNQNs$cfkwCZ1KrY{AhAB86^Ys$Y2q0KxevShR zX_}L%RpF`yp#Eea8N_ME!mdfS&sn~xWk0|AqDbT&fabjkHb~?;9OAe#z&yT5@DvH#romfs^_-rx`#CL`R4>& zclaQ|fcd4%#iQ-z8{?zBD$eVPak_f)mWx570(U=bRFd{jUolDGpU~f9SP9L3XlQt( z_~gqboR`K04Q6j&`J`B+P3$cB3HLpAeUh0R?^Pq2mP%NUm8zF_(Sn1kCUtZzPmak) z4EzvKsopZeX80}gPRBviyMl=&tNQh&hc3AMTzqe8DX5a~E9sXw)W=|fuWzA#-dXgh zNDyC!6eKJflKh_wGoNZA9VfyC38xg$we#veC%v2Z68PCoEp41u3nc*Y5Mbbg(O>!V z7xqtGrAgk5e}Vmezr%>AoT`<$uS&zt#wm`tXF#9qI!E^=P6&R!3auWU@K=F02%*5cy3}R7BN-Kv~h1wi?%DWUwcL(C3@5_wP!!=i4<2O%%}9 z2zJk6+m70FXG&st#=feAu_orPg93)#Rt;6Qu#*Z;RSaR_FWpq*% z8vPD_wT_~)@Wt!ufhEQt{Z)(a1J!x}!YpM4G+ks}URWv!0~&WtKbVBc-u<+DNm($d z{j_(OOMYGsnVRIG(AZCQ(rg66IRNc$xeihmw&|AxA*i}G+09;;?NLW&pGF4zNUY6< zA{BUn=F0V%O+WwEo#aaglhnJaqEIBN_*%+FJ91u{%@RR*=k`Vz_h_~c@7vDHThw50 z5et=;PIj6kGJM&fkk@<%R#d$KLsNz`PUp=;VpS=2${X`qf-~hlxpz#*)so;ds-TRnrVbsI zy5ibWs!hYM_QNYt`zE^iqYu}~m{{S6=%LMSpCnu!`so^$-6h5j4j-*-9|`Xz^BjxJZXbI|Nh)Gcs##MV>)_PO7{!-j$*vT-usPD`p3 z$B39zF4}Y4d&km=_UmVE=a}ByDr_`AA59z@Ek@qWO*&Dk=0$hZQ>ote8M;PO;z0AI z>(f(6ro~3K5Bh&q-@DT-9acvS*sd*936*_#S4?p~m}`@F@P)__r=ZsDMI5zwJmg$l zotKzOXF8_E);6s9z+SIFnub$OZ5A?jB)P^u7#aR^13jsGp~d;pUVQU=%G+R7to@3@ zSN@g#lOl$b3RkxA5O|6`IzH}c{TWsx)vJZ168p_tKlO#>h#BkN)a~@b<24fkRpm$p z4JkqS$X(p}%Yp70X|dqKew>u@^qBhmekF_O@w`zEVM80oQ2q{By>17}; zxa&otG{X>t3p_96Y)goGh#^KkCO>%;0X#AAY0?}T^eQu=D{!)xsQf!uF z=^{r*P7e&%t)?{g_qcCs7R45#IXKwC^I+98Dbw9^x$3*1qU548`+SjzZr&ni?z@Q! zyc~;2rbD_NEt|^VOm+Lo6=&A>)6M*tda21RH(r5sT*uYTPj!zluFR$QZ?6h?TQ2xT zRZY zPdHn3Sn&Haq3RAgdk)dgy;hy8wE5(F%Dgg2m=o1!C44UE}cym&#n$*X>Bggoej$vVk$Y}JC10t_uwTp zCb54jCOsYg+@Q5Vw9j{Zc&U#60Qt82Q4CR%i~LJZPG&F(j}|X1BUY4C&htl(@To;b zKPq;XeKk%RXu)mrMehh7A2nfRDj7yc=rxi3&xT#Mi_;fN%_)()FPGJq64Ln{R&M6( zy6JdGU?TS9tqzG94=c9o=^B+(t#ay10|@8HW9TU)H+}pTwC;l%9&XsP*=*I}4qI~L zbq?+(1bgpgue@hU8jq5W?&%IDe6-PM(R{K4 zXhlBtMSd5D=Tz#!{Jd)+hZ_RgI@nLDL>8z7rDm`>**lFT?BZ(2lc3`7s@5t?E-Hx4 zsulyC^^@W6%&YHEAs4;YWeQao6Tf?Oc1R*NvQDgU#ZMU-GSy5WJ_()l7$g&s15&~Q z>ecV9et8Kf514nSMx!2M2EuE4`&%ZBQ(#AX4Fy+Xxx|xZaM3X3XF-cpA?)L9OFCH$3O}3 zwX2On=XiLY=9HI8yb2=Q;*btKg|NZ8KEnoE__Wfez*%Y%ITtB39S)ifp^t*o3XjG; z4+_SFbQg!dnkY+U^l63iky4gxnY$nT`UP@4{^15yc{-g%#Z9LQ@C@dt^}hG6c17~A z-#U7rd0uCW7drQ=Xg&_qW#wQBlhK!bm3NF4?^RyYkohj1mSO8lY>e zBRW0%)t^hy@=K;rg$Sj$qoVmJH8P5PC?0dCq7SLgSY$I0!Q&F!Gv$-Z1;L!}l7o3v zIp3f+lBY_HvcZFwVj{*p8{VwY_$X-@VU#1rWocQ}hha$dr~3%JMV3)io}T|UNuS{5 z?a*2Eo*EHcFSa$qFt*Bw)P}ug5r1i}{=GwHgtV~m)$%I{!*SUpw6fhh0NhZWzx@7{ zsw6^XS=dSC4>Jh`MTJcxoCFPY9A)Git?yAp!UFJMxNwI${MI$FwWV=yALkTb6?RaX zSXEFn=sg0sIvaKe%!$RXJ;Fy6pa+Ut=FpV!flo=48&Zh@-t>Ee#*Kb%%6Ffp!Gm>K z#u3rkOm!8b{fT(A)IloMh1%0Trias`o9ilFVy-lZV2S8fo36Xka9VRNZODGrGAfeB zNItjq)2ogN$4XL_!H{v&NBcJA6>qhR#-n zN7107_KZ!N@x8KjFC;BzsaV!Nn4g<7XHG9Z5JG;!Gnrgo4n+2^2JO^eiR+qAi2?p1 z@+i|cqrRmGDzZr}AX0tvmYSrIZpwGG?+XSAqOO;BS(l58Jw{qcde;Y5orG)6JcVBP zS)w(IY7t{EZwCC&DjI3aBD&gq=Ztn9dHJ^^Wu#c(KA%!N*scX|A)Lxv#Sa@<6;t7Y zVYz8UgOq+UXy#GAo~1Rtd9hB5`uBt@7L^rGQEq-z{cWg1x454(8;0j;{|>3W#Dp( z-9=48r2FzM&A1FT0+BA=B++g}OqT*1eeIgbWM(F?N)yZ4jg-{Xg^g0a^R}z#YE*&G zBniz~sl98OSM9E4kKGqs?=x!Z$Y~Fid&a~aKLsIxrxaF4+bK;KM7~V2Kd#3txi`$C z24gh;lZ zj1eZtOB7&26A_1P+}(ZHE*w@m$*#yerSs`!&1ZZhwED>ttLdc#`$5IH*?e~fS(Zwr zU+{WN7R#2)6p%xaFrHZW^cgRS%9N{*f$rg8K6D^To~~2qXEX|7>1)_v-Km@>pKEoV zNTrwVv@rjHx&ZJc+hm|CFe8Tm(8m45Ut&x7EfFKcAt9$Eqg`MbRRkAzYd046f#H+|--(VWRIBi(k8Bhc$*9D(VrTrml`PQEPf13B|d^J25~GB-auaA12Q zDN_*9Tkg>98G}0#eiIY^g|2CRM$jTGi`#6jR2w$Jv^;4aZR4Kt1L$ta=!*!_-sxa> zx7U7!`43Rp>ILAtSfJWoZbf5wTcx~oD2Jy!_jX#BIe`U-r(^UOCu{J_XK;7LCeGi? z+x!Csa><_S%Mq^SEIr?djeidD0<)WtgsipeqsGRxa(F>+RN&Ed!6tv3$04gP;2?jYKE>pD%cJ zfMVM%W8vNZhL}?Gy+DEp?tHjAbZ~mDdR0Op? zFu;H{fg#AEOG$fSvSxR6dj3s6A7JRq0#8IVBV67;R+Wmu8j+qJ39PT; zz1RzUurJcJ#nj1%fBB@JB$bQQ?NrFDK;m|_6hqH=l(4tU0dD1PXk20L2Km$ zp!4L&sMGRfv2j)ic$xPP_d;qk_|J}D!=!-!n>~M=$x{22l*KRq+Py}EJoY762j}Wc5JF4?EJL}H_~|FEh^d%5-5Ku-y;Q-Ydn2c=S;Th>v9Nq{Y3P$y}u3Bqcs2_ zHwsGaMp_!CWa(+)^p{$x&j3^$kE9jdU*H4Hq{kwTO=o~$ECUl{i~3%2HMXln0Np*- z76WbiKj9r?VtPmKhU!d+b4`p1Wb9rU3XZ2SDF|`zn*n{>~Zm&GWiNo~CtUrEl?vuikkd5?E=OqH5 zWYMTk{q2I`N$-r6X}!D~3#GN&Wm@@&QpY$%!rn`c!jEbStePx}!N_OD{Z@fq+4L)^ zG`FuLEOO9CnNy2={NkFSTe3H3bL?Yr!owy6>=do9Ae$9427xYWZt5%i$|b$5wwM=P zh{gnb0v)M7ATI$P8uzisOmFN$ zqMe%iFtvA>$c3%o>aAE^`L@4 z0>{zlL;LL$>rz2c)!l$=8AsW5{9GXcS}ww}WjYkg3%YQ9SntMK%FOJdK2wVf3L(>y z3-b@&?9bPpX-)MJFdXEA1@67o5t7qF1h@!bEGT@|PkC((&0wYBsI%I#WTc`~%6hHf zJJnLGGoDunG=3QZ5Lt2HC9oue6S!Rl`d|165a4)6&gG3`>s3>e{P6)joKKs+nc+U+ zWK7?5qRwPzp1Fi1GL)}9!Yh{SLk`qg$$-i*F|x#?TY_A#f5BckfB;P-ZEvAHtV1c% z;hH#!5~s#=-ckV5sxC3|s<_Cq>$~zMJA7dx57SP`1GcRi!H@|s0)o*FzVwiJ-E8Bl z9~6<%ynf49CT04RQx?byn>)u3?br21)k$sIE4n!rl*3*WgTJ@D^iPhrfjBj_gSvwB z+B^4+>PL|Z*^9+$-H8SiXhl+Suk9MgFEJ|?KOk)tRnK%)*%@OenHLE%HuJke-ST3T z>*r)=Mjun;aA_EPW~STSUbp(^Wh9cSancA1oN>$i`&?clX~j#v%71eKgniQM^W_|e zdbUlo&>hS&X&2u&YfN3*j|N2h{66Ptt#cbD@tKJo(HXnVm}t~6E79BtDmod%G`_t` z6GWzF!iERKMCsJKit^+oCCU{@c#>^%6cWESq$&hShR@0Ug#A&Sjj2#~gu}${B)tVv zi-x0blZ0*fRholv`#nm|l+rVInK|y#K}*!2Y{q>gpho{hP~1=~Y$oby1k202*KX`y z>~UTPH;r(>%A#909Hz?zcu#IYGk6^~*V^L)C(f#|UDk@3gHD8MU@sJ8Rt!-H8n{=6_ zA(5aT78tuuU2A1Eb1{yFWran!y)3_9(+$CyZ4WFK*Dcz;8^WTE36IR*sf+c(0Ly36 z{E*6`LXFBOdQ$HO_sTEE;P4#>8&n4CCHQahP=666k}iS=*Lfa^HE*~LcPFe%#ZS-) zP&!JLr3L)<^W8|Jn9ttGApIh#T8*nLQchZ|M0@FUq+0SObaMGf$P|CW{OhEfD#H@D z&G+M{hrRwf=hL}M5zgtxd}U4JNO$#``ZSjx1&J3|qD;CPa1>M&u}QpJ%{E#4c&7b# z^GP%%vL=LyM5^!W|lXOqP52CqLc0 znJ)+*RXVeYGhS6yd^ROR{;24Ewk12Jyhn)+E+|@%%#zUFxlX#07j?v{GnGWRaCL#8nS5nO0fJ2w&hH5nM$lqwt8j81vkH4_8~Lr?@AbY5Uzg;_9n2eTnxu;G{^6&IBp5{u2 zvmaR?XW!V+%ec{vOA9q?J>S4PM}1iyH|A{`3RJsWi&^kv%bPbNEv8Azu8xekB90%6 zGZJxRW-`L(OYZO3ac8f)FEX!f+l*0LFsaKOwFzI)#MZT)q3xd&8aoZvfpj2SRJBNK zITiH{`vj3r#PeTM6(E05#3!o*7)Z ze3_VQ;?6yVx}*3tf9#-YusOHIF-je~hTGERkJ|ZA@N~=}4|as7s2Z8-2fMfHPfvGL zT0}JFYltzzx@x9++)KgG|K^!wH*RVhZ{}REIh1VF(BhEtV@#@+z;V`X z*uv)p|E!h!rMOR^WK#a?bGDJmwkG5=+tQL>Ppbs>Bp_iSKDg_BbRjZ>6NaoAJ(#as(P}c43Bj+q|QUk3_P7M%N*aqr;!Y_3a(0y3*J{DO=(ko8^mxogQ6KJLPkz9m-Bm3&fGhx&k@k)f zt`o~25r`%OvCzKDQA>{sbVi5>giGWHP;N^wFG?<1kKS|S5fB+IG_$6g!x3YOQmzWr zj422wq^0+I~urqwU$NcHEx<;a%>x( z21>2{{mUWO^?nU~nuwu_F2nepYc5JIhNFi5{fDqxLQi}Hx6Vuat9zQ<00Op|rq$E6 z+Sl!Mp`42)P=ud>UE~6i zmn6q7`EWYdJP{+VOiC?*wcm0~_+e{l{kd}86$t_Dkj&?xS!mJO>@j-0sD$@^t>aw; zdDWYa32sNz9ahcyla4RHntvLhk7v1N_fLm@f_fJ9);Fa8MsFvOVtF(l(I=e6vX=IG z28F1!t(F~GKaoFpV{EaH_f(N5dXy+g@)Xh_XrsRGVS*D)3;W_*);z|90Yn9czy;SS zzE-4kJU_l0c`bu}REIUCMr#^r>ya=eFxr0Oo7}rKGc@$Rm7A4fSj(c)d22PHr0#;D zgjjF$7RWBsTlZb$eNVylHHvN1dxhJxNp}g$n_2|J=>L)R)^SxeQUA8$K}rM;B_Pt> zUDDm%T_PbJB8_w--QA6Ji*$Fl^r0lB-wn$BJkRg_=l1>_X3wlyGiRSY*Z!`xRAi14 zJ*yoc&3?^#YqkK8k}@nO6{`aOu9tgTZmCl~;CAV(FtZzV-R_XEGh*5>B|vQWgAwcX z8qIo%7yha)Ln^t*M_Y$?*%>3o^>x+~;}O_JBa@> z2r$5Qd_`MiyZ)UD_YWg-HEBYO7Xz`H%kD{Rlta6ixnL`nXJHz(CZ{9Tas905)? z$UgH;#tw6hy|gSh64)DGE=Ukpk{_2Gd_!ja4L_p*Nj{qVy_3|QM~RvseCIL)No%D3Un6Ot_xBcY82IO@6jD`Nv#>1cAJ0$#xF9s7mLaKsMRAb4&0pK|1r z0`2k8x=gfMt>q+@A7jP=*?>E01iHpPbcs_1Eu6cTkJ9*B<4cnTz$ASFFVD*f1-8$aiQAzS-ScKBu6tmGDtz2`r_9ks z!}=Z}mep>oCI+&TRhP)`O<29M<*XqRBPOze=FjxwvZ5A|GDf$dck7B39If_&^FpT2 z?xOrXF@uxOXEr*}n_SI=;!Q69n*JoEp0M~9W;~v{lv`fw2o}&cUKgM-3?%vrVaL!? z<$ZVL$_!|}C2#-MH(!ZGW--On@yAKWY7nSivZDo%r&0&IV3b4|!_-zL25DlDK--8b z3yI}`i5h;)$8)100|Ew~#=}e@Q;X_s1}8~PTr_YyzMf3P2n9}L$qyGo46rlDTYDe* zqFhO1i(*+G?b4r|W~n?r7aY-XKwo-r8V zcQjVm$m`gOxNBg!)u}~E`V!krt8PLoaMV4O#q#N0CM4Rs8q^8H)FQmf3+8!4Bx&{uxx903Zx_FD7L}UuzDQ z|92peG=N-$JG+%a(Y-dR9fPaAG^LG@FZWA zRgVDw#}WWVqc^ALasWEtM)X5RfId?G|LP;vA^=mopCR49ZAI}9JYxHAgny>;txN*5 z))_Q9kUhd-VB>UukPzyXfXctAQCZTy{Eu^l0rE&2;@DZ<4y%s6fj`hdKVF=0_!1N1 zY32`3_}kqUpgmPWIKp{SlC}ZPk|;)Djr$*vh1huGvN2Uc?)#RHwYM$U;nJ`{gdrDl7IufGFAHn;!1^Ng#W=jAIWF(gbS5K8~<#9 z1wP<67^-aiOk+!@D;yG!ipL9}5d0O~=R$CYK|Y|TCDls?T=W*YI2L8`?k(>++0*s^ zOTnoG_~=71?4OW$lsEnl(}fLqrUHJBjv@Yp{O~^m1epnAH3m1=bRZz)=fm~^2?G}8 zuMt)}^$3yzEb3~XH!SQ5>0p1D-2a{Zkr~iQ*$3M_vOl5h7>K(6%YuLZ`vH*YGQ22- zEPnLM;y`=sK!BvJDpXD~%>HQ^kRLR;6q1svQzK&k&j1f!7Cjqa5Jh?;<4_8G8`uY* z8UhZcS4lU5c{1n_SR*NU;<&Q!m0f7@j~m;Q11bQ#85I5M$!jYQI~30Mhf*k<=O65+ zEC1V>^f17mQPUZee}ULtp&y17gw-RdAb^%;eIm?iTwW3p0mU@$Q0PQW^b5P@AX%XZ ziH#ZAYC#Jkvmv$_21LN^TzyBBQ#o4Aj~*o_5D$wG9Y++upSwOGR;Z#)y$_Tq#NPR#>R64;j8hQ^ zmy7~h=gm;LjPIDI_7EfE9Iw_d#TXbV#Og?1!ItgQ(3<^m&-&JajzVurpbwFb7VmTw zo~%C@fBhcPO>0Ktg)i?EI=28pa%EuCP(VR@p*ftx5L;PgEFBSy59rN_;X#2mF=vkR z5%LFZD+u{L-+Vi^ee=Zm4My1})I>ke*MK6+dor=`Fu?p1+`)lW=Z-{XdIb6vseTc8 z1My&Ul?ZxP&pzOO+7}&)oCs@YDrFpPUV3FBA14kFol`fWMr%QDh!`b&k{=%_6i8sgX$TYZW^jX80QH^^%}wx&7d6=WI{ z0^O3<^(A3x!l*oSkU=nblI{jA03f-Uq~0CiLg>8suYAk#;G~OdJGrBKvZ-W^*FhaD zgj{w@-7EnVIELUm%Ovsjt1b{v&Zpio%~-_wmr07Oo2LdtN{AjfG3g35p7@*QJXr(r z`1)m5>*kZ{?AqLhbW>{|?DR>KpFy1*9m;X4bSVX26S--_K}LjZpa zS{`lbywlO|HsiqKAS(sIng7ZoPZ&O-38ThrI=OTf#t+&m^5FRu%&3Xm+QP79$upfI zJU(?+$HWjGBeWnAp;jexbbSeX8ge#pVrj*LoO)l#ya~zDB`q}t)OF@;!&W5I5=7HX zGt1wZL%-15%2+5y#Qzk4g1{~(#aNF4zkt>+G`)p}foBTqgRoh}U7%pFi)C`~e2KF= z@KUFo-izCCQ$)SCkscLKR_;xb#yEhwN;(XUorfgkh+)BYnVR#%8f%+bCf)HXFWCfD z`n#Y5XK@Gj>~E9vT(izAFyXzNY3yBxbeYS~Nc+}b^liac@y(sTKa?6UBSER1M;KYj zLyk{t(;1#hr(a%7!t?8HWhN6_Clu?b*-ceRdoL1k^oOvtHpYX=yhDCC+lCfb9Z@Wn@XG`p!aj$+kyQPCadnK_Vwk~M!d_g?0&j!k=b~T7BJ*w=rEK*gOLmCAnoHC~@Ex5)|`%2BZVxvfQbeQZ-*Vz}|HGp(PO6GyPt>?~1Pv%Yp?V}P$k z&Z?hNJB!M$N<)6NrRdtZ7PDH5%}mUq!9BX6#au~~f+h|QhoZuaDyQbaAaUo5X+pi& zlndP%4-G{fG@4cnu<)(PMP_i-z6 zlaUmSt;9BzIke98Ennq|;~1DmJG45OPVZ|qALv{iZD|AI^=z7oNkk{N!-6XN#mqQQ2#r}=yBRzn;u z%@+eU!ZXhIwLL`&AYk&xW4xhIf1y*u(f zsPfRIuKXnWP@FlTMQN@|X^uHmSy$=Zm2_xmM>R`d=veIQ_L_m|I$`Flru%O$tG&Ur zk;AB$I)>EUWQ@(awUNnmv39~^I$^afUm?16%aC^k=bAw;Zr|+Bf1HSkg#Ue;b>~sZ zZQAeTsC{x}>D%~a?&h@hlKUWbT(k81q-~0$AbjPjhC;;PCOvERhyq2GQmsOS2HX@b zqs?(tZ@xerOmFscoSvHH;ldfzYja$^JqMD5^MzESqY{r(U2!lO+s7+p+2s?%gYPB> zj`L$jwP(-q5#2T4L#ZM?n=RvPn0\O3+|R```>Do86nkp25zPfJVHq@R|dY7^MW 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. *Default*: prng + :source_region_meshes: + Relates meshes to spatial domains for subdividing source regions with each domain. + + :mesh: + Contains an ``id`` attribute and one or more ```` sub-elements. + + :id: + The unique identifier for the mesh. + + :domain: + Each domain element has an ``id`` attribute and a ``type`` attribute. + + :id: + The unique identifier for the domain. + + :type: + The type of the domain. Can be ``material``, ``cell``, or ``universe``. + ---------------------------------- ```` Element ---------------------------------- diff --git a/docs/source/usersguide/random_ray.rst b/docs/source/usersguide/random_ray.rst index eff1764d4fe..f28aed78451 100644 --- a/docs/source/usersguide/random_ray.rst +++ b/docs/source/usersguide/random_ray.rst @@ -319,20 +319,24 @@ Default behavior using OpenMC's native PRNG can be manually specified as:: .. _subdivision_fsr: ----------------------------------- -Subdivision of Flat Source Regions ----------------------------------- - -While the scattering and fission sources in Monte Carlo -are treated continuously, they are assumed to be invariant (flat) within a -MOC or random ray flat source region (FSR). This introduces bias into the -simulation, which can be remedied by reducing the physical size of the FSR -to dimensions below that of typical mean free paths of particles. +----------------------------- +Subdivision of Source Regions +----------------------------- -In OpenMC, this subdivision currently must be done manually. The level of +While the scattering and fission sources in Monte Carlo are treated +continuously, they are assumed to have a shape (flat or linear) within a MOC or +random ray source region (SR). This introduces bias into the simulation that can +be remedied by reducing the physical size of the SR to be smaller than the +typical mean free paths of particles. While use of linear sources in OpenMC +greatly reduces the error stemming from this approximation, subdivision is still +typically required. + +In OpenMC, this subdivision can be done either manually by the user (by defining +additional surfaces and cells in the geometry) or automatically by assigning a +mesh to one or more cells, universes, or material types. The level of subdivision needed will be dependent on the fidelity the user requires. For -typical light water reactor analysis, consider the following example subdivision -of a two-dimensional 2x2 reflective pincell lattice: +typical light water reactor analysis, consider the following example of manual +subdivision of a two-dimensional 2x2 reflective pincell lattice: .. figure:: ../_images/2x2_materials.jpeg :class: with-border @@ -344,9 +348,79 @@ of a two-dimensional 2x2 reflective pincell lattice: :class: with-border :width: 400 - FSR decomposition for an asymmetrical 2x2 lattice (1.26 cm pitch) + Manual decomposition for an asymmetrical 2x2 lattice (1.26 cm pitch) + +Geometry cells can also be subdivided into small source regions by assigning a +mesh to a list of domains, with each domain being of type +:class:`openmc.Material`, :class:`openmc.Cell`, or :class:`openmc.Universe`. The +idea of defining a source region as a combination of a base geometry cell and a +mesh element is known as "cell-under-voxel" style geometry, although in OpenMC +the mesh can be any kind and is not restricted to 3D regular voxels. An example +of overlaying a simple 2D mesh over a geometry is given as:: + + sr_mesh = openmc.RegularMesh() + sr_mesh.dimension = (n, n) + sr_mesh.lower_left = (0.0, 0.0) + sr_mesh.upper_right = (x, y) + domain = geometry.root_universe + settings.random_ray['source_region_meshes'] = [(sr_mesh, [domain])] + +In the above example, we apply a single :math:`n \times n` uniform mesh over the +entire domain by assigning it to the root universe of the geometry. +Alternatively, we might want to apply a finer or coarser mesh to different +regions of a 3D problem, for instance, as:: + + fuel = openmc.Material(name='UO2 fuel') + ... + water = openmc.Material(name='hot borated water') + ... + clad = openmc.Material(name='Zr cladding') + ... + + coarse_mesh = openmc.RegularMesh() + coarse_mesh.dimension = (n, n, n) + coarse_mesh.lower_left = (0.0, 0.0, 0.0) + coarse_mesh.upper_right = (x, y, z) + + fine_mesh = openmc.RegularMesh() + fine_mesh.dimension = (2*n, 2*n, 2*n) + fine_mesh.lower_left = (0.0, 0.0, 0.0) + fine_mesh.upper_right = (x, y, z) + + settings.random_ray['source_region_meshes'] = [(fine_mesh, [fuel, clad]), (coarse_mesh, [water])] + +Note that we don't need to adjust the outer bounds of the mesh to tightly wrap +the domain we assign the mesh to. Rather, OpenMC will dynamically generate +source regions based on the mesh bins rays actually visit, such that no +additional memory is wasted even if a domain only intersects a few mesh bins. +Going back to our 2x2 lattice example, if using a mesh-based subdivision, this +might look as below: + +.. figure:: ../_images/2x2_sr_mesh.png + :class: with-border + :width: 400 -In the future, automated subdivision of FSRs via mesh overlay may be supported. + 20x20 overlaid "cell-under-voxel" mesh decomposition for an asymmetrical 2x2 lattice (1.26 cm pitch) + +Note that mesh-bashed subdivision is much easier for a user to implement but +does have a few downsides compared to manual subdivision. Manual subdivision can +be done with the specifics of the geometry in mind. As in the pincell example, +it is more efficient to subdivide the fuel region into azimuthal sectors and +radial rings as opposed to a Cartesian mesh. This is more efficient because the +regions are a more uniform size and follow the material boundaries closer, +resulting in the need for fewer source regions. Fewer source regions tends to +equate to a faster computational speed and/or the need for fewer rays per batch +to achieve good statistics. Additionally, applying a mesh often tends to create +a few very small source regions, as shown in the above picture where corners of +the mesh happen to intersect close to the actual fuel-moderator interface. These +small regions are rarely visited by rays, which can result in inaccurate +estimates of the source within those small regions and, thereby, numerical +instability. However, OpenMC utilizes several techniques to detect these small +source regions and mitigate instabilities that are associated with them. In +conclusion, mesh overlay is a great way to subdivide any geometry into smaller +source regions. It can be used while retaining stability, though typically at +the cost of generating more source regions relative to an optimal manual +subdivision. .. _usersguide_flux_norm: diff --git a/docs/source/usersguide/variance_reduction.rst b/docs/source/usersguide/variance_reduction.rst index 124f342034a..f8ee9c35c48 100644 --- a/docs/source/usersguide/variance_reduction.rst +++ b/docs/source/usersguide/variance_reduction.rst @@ -130,9 +130,15 @@ random ray mode can be found in the :ref:`Random Ray User Guide `. .. warning:: If using FW-CADIS weight window generation, ensure that the selected weight - window mesh does not subdivide any cells in the problem. In the future, this - restriction is intended to be relaxed, but for now subdivision of cells by a - mesh tally will result in undefined behavior. + window mesh does not subdivide any source regions in the problem. This can + be ensured by assigning the weight window tally mesh to the root universe so + as to create source region boundaries that conform to the mesh, as in the + example below. + +:: + + root = model.geometry.root_universe + settings.random_ray['source_region_meshes'] = [(ww_mesh, [root])] 6. When running your multigroup random ray input deck, OpenMC will automatically run a forward solve followed by an adjoint solve, with a diff --git a/include/openmc/constants.h b/include/openmc/constants.h index cfcacbabf00..2a7ce199046 100644 --- a/include/openmc/constants.h +++ b/include/openmc/constants.h @@ -59,6 +59,10 @@ constexpr double RADIAL_MESH_TOL {1e-10}; // Maximum number of random samples per history constexpr int MAX_SAMPLE {100000}; +// Avg. number of hits per batch to be defined as a "small" +// source region in the random ray solver +constexpr double MIN_HITS_PER_BATCH {1.5}; + // ============================================================================ // MATH AND PHYSICAL CONSTANTS diff --git a/include/openmc/random_ray/flat_source_domain.h b/include/openmc/random_ray/flat_source_domain.h index 011ff7ccdf8..39d87d663a2 100644 --- a/include/openmc/random_ray/flat_source_domain.h +++ b/include/openmc/random_ray/flat_source_domain.h @@ -4,8 +4,10 @@ #include "openmc/constants.h" #include "openmc/openmp_interface.h" #include "openmc/position.h" +#include "openmc/random_ray/parallel_map.h" #include "openmc/random_ray/source_region.h" #include "openmc/source.h" +#include #include namespace openmc { @@ -37,7 +39,6 @@ class FlatSourceDomain { void random_ray_tally(); virtual void accumulate_iteration_flux(); void output_to_vtk() const; - void all_reduce_replicated_source_regions(); void convert_external_sources(); void count_external_source_regions(); void set_adjoint_sources(const vector& forward_flux); @@ -47,12 +48,34 @@ class FlatSourceDomain { void flatten_xs(); void transpose_scattering_matrix(); void serialize_final_fluxes(vector& flux); + void apply_meshes(); + void apply_mesh_to_cell_instances(int32_t i_cell, int32_t mesh_idx, + int target_material_id, const vector& instances, + bool is_target_void); + void apply_mesh_to_cell_and_children(int32_t i_cell, int32_t mesh_idx, + int32_t target_material_id, bool is_target_void); + void prepare_base_source_regions(); + SourceRegionHandle get_subdivided_source_region_handle( + int64_t sr, int mesh_bin, Position r, double dist, Direction u); + void finalize_discovered_source_regions(); + int64_t n_source_regions() const + { + return source_regions_.n_source_regions(); + } + int64_t n_source_elements() const + { + return source_regions_.n_source_regions() * negroups_; + } //---------------------------------------------------------------------------- // Static Data members static bool volume_normalized_flux_tallies_; static bool adjoint_; // If the user wants outputs based on the adjoint flux + // Static variables to store source region meshes and domains + static std::unordered_map>> + mesh_domain_map_; + //---------------------------------------------------------------------------- // Static data members static RandomRayVolumeEstimator volume_estimator_; @@ -61,7 +84,6 @@ class FlatSourceDomain { // Public Data members bool mapped_all_tallies_ {false}; // If all source regions have been visited - int64_t n_source_regions_ {0}; // Total number of source regions in the model int64_t n_external_source_regions_ {0}; // Total number of source regions with // non-zero external source terms @@ -84,6 +106,27 @@ class FlatSourceDomain { // The abstract container holding all source region-specific data SourceRegionContainer source_regions_; + // Base source region container. When source region subdivision via mesh + // is in use, this container holds the original (non-subdivided) material + // filled cell instance source regions. These are useful as they can be + // initialized with external source and mesh domain information ahead of time. + // Then, dynamically discovered source regions can be initialized by cloning + // their base region. + SourceRegionContainer base_source_regions_; + + // Parallel hash map holding all source regions discovered during + // a single iteration. This is a threadsafe data structure that is cleaned + // out after each iteration and stored in the "source_regions_" container. + // It is keyed with a SourceRegionKey, which combines the base source + // region index and the mesh bin. + ParallelMap + discovered_source_regions_; + + // Map that relates a SourceRegionKey to the index at which the source + // region can be found in the "source_regions_" container. + std::unordered_map + source_region_map_; + protected: //---------------------------------------------------------------------------- // Methods @@ -100,9 +143,7 @@ class FlatSourceDomain { //---------------------------------------------------------------------------- // Private data members - int negroups_; // Number of energy groups in simulation - int64_t n_source_elements_ {0}; // Total number of source regions in the model - // times the number of energy groups + int negroups_; // Number of energy groups in simulation double simulation_volume_; // Total physical volume of the simulation domain, as diff --git a/include/openmc/random_ray/parallel_map.h b/include/openmc/random_ray/parallel_map.h new file mode 100644 index 00000000000..7f4f06d9996 --- /dev/null +++ b/include/openmc/random_ray/parallel_map.h @@ -0,0 +1,193 @@ +#ifndef OPENMC_RANDOM_RAY_PARALLEL_HASH_MAP_H +#define OPENMC_RANDOM_RAY_PARALLEL_HASH_MAP_H + +#include "openmc/openmp_interface.h" + +#include +#include + +namespace openmc { + +/* + * The ParallelMap class allows for threadsafe access to a map-like data + * structure. It is implemented as a hash table with a fixed number of buckets, + * each of which contains a mutex lock and an unordered_map. The class provides + * a subset of the functionality of std::unordered_map. Users must first lock + * the object (using the key) before accessing or modifying the map. The object + * is locked by bucket, allowing for multiple threads to manipulate different + * keys simultaneously, though sometimes threads will need to wait if keys + * happen to be in the same bucket. The ParallelMap will generate pointers to + * hold values, rather than direct storage of values, so as to allow for + * pointers to values to remain valid even after the lock has been released + * (though locking of those values is then left to the user). Iterators to the + * class are provided but are not threadsafe, and are meant to be used only in a + * serial context (e.g., to dump the contents of the map to another data + * structure). + */ + +template +class ParallelMap { + + //---------------------------------------------------------------------------- + // Helper structs and classes + + struct Bucket { + OpenMPMutex lock_; + std::unordered_map, HashFunctor> map_; + }; + + // The iterator yields a pair: (const KeyType&, ValueType&) + class iterator { + public: + using iterator_category = std::forward_iterator_tag; + using value_type = std::pair; + using difference_type = std::ptrdiff_t; + using pointer = void; // Not providing pointer semantics. + using reference = value_type; + + iterator(std::vector* buckets, std::size_t bucket_index, + typename std::unordered_map, + HashFunctor>::iterator inner_it) + : buckets_(buckets), bucket_index_(bucket_index), inner_it_(inner_it) + { + // Advance to the first valid element if necessary. + advance_to_valid(); + } + + // Dereference returns a pair of (key, value). + reference operator*() const + { + return {inner_it_->first, *inner_it_->second}; + } + + iterator& operator++() + { + ++inner_it_; + advance_to_valid(); + return *this; + } + + iterator operator++(int) + { + iterator tmp = *this; + ++(*this); + return tmp; + } + + bool operator==(const iterator& other) const + { + // Two iterators are equal if they refer to the same bucket vector and are + // both at end, or if they have the same bucket index and inner iterator. + return buckets_ == other.buckets_ && + bucket_index_ == other.bucket_index_ && + (bucket_index_ == buckets_->size() || + inner_it_ == other.inner_it_); + } + + bool operator!=(const iterator& other) const { return !(*this == other); } + + private: + // Helper function: if we are at the end of the current bucket, advance to + // the next non-empty bucket. + void advance_to_valid() + { + while (bucket_index_ < buckets_->size() && + inner_it_ == (*buckets_)[bucket_index_].map_.end()) { + ++bucket_index_; + if (bucket_index_ < buckets_->size()) + inner_it_ = (*buckets_)[bucket_index_].map_.begin(); + } + } + + std::vector* buckets_; + std::size_t bucket_index_; + typename std::unordered_map, + HashFunctor>::iterator inner_it_; + }; + +public: + //---------------------------------------------------------------------------- + // Constructor + ParallelMap(int n_buckets = 1000) : buckets_(n_buckets) {} + + //---------------------------------------------------------------------------- + // Public Methods + void lock(const KeyType& key) + { + Bucket& bucket = get_bucket(key); + bucket.lock_.lock(); + } + + void unlock(const KeyType& key) + { + Bucket& bucket = get_bucket(key); + bucket.lock_.unlock(); + } + + void clear() + { + for (auto& bucket : buckets_) { + bucket.map_.clear(); + } + } + + bool contains(const KeyType& key) + { + Bucket& bucket = get_bucket(key); + // C++20 + // return bucket.map_.contains(key); + return bucket.map_.find(key) != bucket.map_.end(); + } + + ValueType& operator[](const KeyType& key) + { + Bucket& bucket = get_bucket(key); + return *bucket.map_[key].get(); + } + + ValueType* emplace(KeyType key, const ValueType& value) + { + Bucket& bucket = get_bucket(key); + // Attempt to emplace the new element into the unordered_map within the + auto result = bucket.map_.emplace(key, std::make_unique(value)); + auto it = result.first; + return it->second.get(); + } + + // Return iterator to first element. + iterator begin() + { + std::size_t bucket_index = 0; + auto inner_it = buckets_.empty() + ? typename std::unordered_map, HashFunctor>::iterator() + : buckets_[0].map_.begin(); + return iterator(&buckets_, bucket_index, inner_it); + } + + // Return iterator to one-past-last element. + iterator end() + { + // End is signaled by bucket_index_ equal to buckets_.size() + return iterator(&buckets_, buckets_.size(), + typename std::unordered_map, + HashFunctor>::iterator()); + } + +private: + //---------------------------------------------------------------------------- + // Private Methods + Bucket& get_bucket(const KeyType& key) + { + return buckets_[hash(key) % buckets_.size()]; + } + + //---------------------------------------------------------------------------- + // Private Data Fields + HashFunctor hash; + vector buckets_; +}; + +} // namespace openmc + +#endif // OPENMC_RANDOM_RAY_PARALLEL_HASH_MAP_H diff --git a/include/openmc/random_ray/random_ray.h b/include/openmc/random_ray/random_ray.h index df1ad70bc0b..abf2a26881f 100644 --- a/include/openmc/random_ray/random_ray.h +++ b/include/openmc/random_ray/random_ray.h @@ -25,11 +25,17 @@ class RandomRay : public Particle { //---------------------------------------------------------------------------- // Methods void event_advance_ray(); - void attenuate_flux(double distance, bool is_active); - void attenuate_flux_flat_source(double distance, bool is_active); - void attenuate_flux_flat_source_void(double distance, bool is_active); - void attenuate_flux_linear_source(double distance, bool is_active); - void attenuate_flux_linear_source_void(double distance, bool is_active); + void attenuate_flux(double distance, bool is_active, double offset = 0.0); + void attenuate_flux_inner( + double distance, bool is_active, int64_t sr, int mesh_bin, Position r); + void attenuate_flux_flat_source( + SourceRegionHandle& srh, double distance, bool is_active, Position r); + void attenuate_flux_flat_source_void( + SourceRegionHandle& srh, double distance, bool is_active, Position r); + void attenuate_flux_linear_source( + SourceRegionHandle& srh, double distance, bool is_active, Position r); + void attenuate_flux_linear_source_void( + SourceRegionHandle& srh, double distance, bool is_active, Position r); void initialize_ray(uint64_t ray_id, FlatSourceDomain* domain); uint64_t transport_history_based_single_ray(); @@ -42,6 +48,7 @@ class RandomRay : public Particle { static double distance_active_; // Active ray length static unique_ptr ray_source_; // Starting source for ray sampling static RandomRaySourceShape source_shape_; // Flag for linear source + static bool mesh_subdivision_enabled_; // Flag for mesh subdivision static RandomRaySampleMethod sample_method_; // Flag for sampling method //---------------------------------------------------------------------------- @@ -55,6 +62,8 @@ class RandomRay : public Particle { // Private data members vector delta_psi_; vector delta_moments_; + vector mesh_bins_; + vector mesh_fractional_lengths_; int negroups_; FlatSourceDomain* domain_ {nullptr}; // pointer to domain that has flat source diff --git a/include/openmc/random_ray/random_ray_simulation.h b/include/openmc/random_ray/random_ray_simulation.h index 01f12bffb17..b94e7401b3e 100644 --- a/include/openmc/random_ray/random_ray_simulation.h +++ b/include/openmc/random_ray/random_ray_simulation.h @@ -20,10 +20,13 @@ class RandomRaySimulation { //---------------------------------------------------------------------------- // Methods void compute_segment_correction_factors(); - void prepare_fixed_sources(); - void prepare_fixed_sources_adjoint(vector& forward_flux); + void apply_fixed_sources_and_mesh_domains(); + void prepare_fixed_sources_adjoint(vector& forward_flux, + SourceRegionContainer& forward_source_regions, + SourceRegionContainer& forward_base_source_regions, + std::unordered_map& + forward_source_region_map); void simulate(); - void reduce_simulation_statistics(); void output_simulation_results() const; void instability_check( int64_t n_hits, double k_eff, double& avg_miss_rate) const; @@ -63,6 +66,7 @@ class RandomRaySimulation { void openmc_run_random_ray(); void validate_random_ray_inputs(); +void openmc_reset_random_ray(); } // namespace openmc diff --git a/include/openmc/random_ray/source_region.h b/include/openmc/random_ray/source_region.h index e41d6b82463..5c5b31f392e 100644 --- a/include/openmc/random_ray/source_region.h +++ b/include/openmc/random_ray/source_region.h @@ -44,7 +44,7 @@ inline void hash_combine(size_t& seed, const size_t v) } //---------------------------------------------------------------------------- -// Helper Structs +// Helper Structs and Classes // A mapping object that is used to map between a specific random ray // source region and an OpenMC native tally bin that it should score to @@ -81,11 +81,234 @@ struct TallyTask { }; }; +// The SourceRegionKey combines a base source region (i.e., a material +// filled cell instance) with a mesh bin. This key is used as a handle +// for dynamically discovered source regions when subdividing source +// regions with meshes. +class SourceRegionKey { +public: + int64_t base_source_region_id; + int64_t mesh_bin; + SourceRegionKey() = default; + SourceRegionKey(int64_t source_region, int64_t bin) + : base_source_region_id(source_region), mesh_bin(bin) + {} + + // Equality operator required by the unordered_map + bool operator==(const SourceRegionKey& other) const + { + return base_source_region_id == other.base_source_region_id && + mesh_bin == other.mesh_bin; + } + + // Less than operator required by std::sort + bool operator<(const SourceRegionKey& other) const + { + if (base_source_region_id < other.base_source_region_id) { + return true; + } else if (base_source_region_id > other.base_source_region_id) { + return false; + } else { + return mesh_bin < other.mesh_bin; + } + } + + // Hashing functor required by the unordered_map + struct HashFunctor { + size_t operator()(const SourceRegionKey& key) const + { + size_t seed = 0; + hash_combine(seed, key.base_source_region_id); + hash_combine(seed, key.mesh_bin); + return seed; + } + }; +}; + +// Forward declaration of SourceRegion +class SourceRegion; + +class SourceRegionHandle { +public: + //---------------------------------------------------------------------------- + // Constructors + SourceRegionHandle(SourceRegion& sr); + SourceRegionHandle() = default; + + // All fields are commented/described in the SourceRegion class definition + // below + + //---------------------------------------------------------------------------- + // Public Data members + int negroups_; + bool is_numerical_fp_artifact_ {false}; + bool is_linear_ {false}; + + // Scalar fields + int* material_; + int* is_small_; + int* n_hits_; + int* birthday_; + OpenMPMutex* lock_; + double* volume_; + double* volume_t_; + double* volume_sq_; + double* volume_sq_t_; + double* volume_naive_; + int* position_recorded_; + int* external_source_present_; + Position* position_; + Position* centroid_; + Position* centroid_iteration_; + Position* centroid_t_; + MomentMatrix* mom_matrix_; + MomentMatrix* mom_matrix_t_; + // A set of volume tally tasks. This more complicated data structure is + // convenient for ensuring that volumes are only tallied once per source + // region, regardless of how many energy groups are used for tallying. + std::unordered_set* volume_task_; + + // Mesh that subdivides this source region + int* mesh_; + int64_t* parent_sr_; + + // Energy group-wise 1D arrays + double* scalar_flux_old_; + double* scalar_flux_new_; + float* source_; + float* external_source_; + double* scalar_flux_final_; + + MomentArray* source_gradients_; + MomentArray* flux_moments_old_; + MomentArray* flux_moments_new_; + MomentArray* flux_moments_t_; + + // 2D array representing values for all energy groups x tally + // tasks. Each group may have a different number of tally tasks + // associated with it, necessitating the use of a jagged array. + vector* tally_task_; + + //---------------------------------------------------------------------------- + // Public Accessors + + int& material() { return *material_; } + const int material() const { return *material_; } + + int& is_small() { return *is_small_; } + const int is_small() const { return *is_small_; } + + int& n_hits() { return *n_hits_; } + const int n_hits() const { return *n_hits_; } + + void lock() { lock_->lock(); } + void unlock() { lock_->unlock(); } + + double& volume() { return *volume_; } + const double volume() const { return *volume_; } + + double& volume_t() { return *volume_t_; } + const double volume_t() const { return *volume_t_; } + + double& volume_sq() { return *volume_sq_; } + const double volume_sq() const { return *volume_sq_; } + + double& volume_sq_t() { return *volume_sq_t_; } + const double volume_sq_t() const { return *volume_sq_t_; } + + double& volume_naive() { return *volume_naive_; } + const double volume_naive() const { return *volume_naive_; } + + int& position_recorded() { return *position_recorded_; } + const int position_recorded() const { return *position_recorded_; } + + int& external_source_present() { return *external_source_present_; } + const int external_source_present() const + { + return *external_source_present_; + } + + Position& position() { return *position_; } + const Position position() const { return *position_; } + + Position& centroid() { return *centroid_; } + const Position centroid() const { return *centroid_; } + + Position& centroid_iteration() { return *centroid_iteration_; } + const Position centroid_iteration() const { return *centroid_iteration_; } + + Position& centroid_t() { return *centroid_t_; } + const Position centroid_t() const { return *centroid_t_; } + + MomentMatrix& mom_matrix() { return *mom_matrix_; } + const MomentMatrix mom_matrix() const { return *mom_matrix_; } + + MomentMatrix& mom_matrix_t() { return *mom_matrix_t_; } + const MomentMatrix mom_matrix_t() const { return *mom_matrix_t_; } + + std::unordered_set& volume_task() + { + return *volume_task_; + } + const std::unordered_set& volume_task() + const + { + return *volume_task_; + } + + int& mesh() { return *mesh_; } + const int mesh() const { return *mesh_; } + + int64_t& parent_sr() { return *parent_sr_; } + const int64_t parent_sr() const { return *parent_sr_; } + + double& scalar_flux_old(int g) { return scalar_flux_old_[g]; } + const double scalar_flux_old(int g) const { return scalar_flux_old_[g]; } + + double& scalar_flux_new(int g) { return scalar_flux_new_[g]; } + const double scalar_flux_new(int g) const { return scalar_flux_new_[g]; } + + double& scalar_flux_final(int g) { return scalar_flux_final_[g]; } + const double scalar_flux_final(int g) const { return scalar_flux_final_[g]; } + + float& source(int g) { return source_[g]; } + const float source(int g) const { return source_[g]; } + + float& external_source(int g) { return external_source_[g]; } + const float external_source(int g) const { return external_source_[g]; } + + MomentArray& source_gradients(int g) { return source_gradients_[g]; } + const MomentArray source_gradients(int g) const + { + return source_gradients_[g]; + } + + MomentArray& flux_moments_old(int g) { return flux_moments_old_[g]; } + const MomentArray flux_moments_old(int g) const + { + return flux_moments_old_[g]; + } + + MomentArray& flux_moments_new(int g) { return flux_moments_new_[g]; } + const MomentArray flux_moments_new(int g) const + { + return flux_moments_new_[g]; + } + + MomentArray& flux_moments_t(int g) { return flux_moments_t_[g]; } + const MomentArray flux_moments_t(int g) const { return flux_moments_t_[g]; } + + vector& tally_task(int g) { return tally_task_[g]; } + const vector& tally_task(int g) const { return tally_task_[g]; } + +}; // class SourceRegionHandle + class SourceRegion { public: //---------------------------------------------------------------------------- // Constructors SourceRegion(int negroups, bool is_linear); + SourceRegion(const SourceRegionHandle& handle, int64_t parent_sr); SourceRegion() = default; //---------------------------------------------------------------------------- @@ -104,7 +327,13 @@ class SourceRegion { double volume_naive_ {0.0}; //!< Volume as integrated from this iteration only int position_recorded_ {0}; //!< Has the position been recorded yet? int external_source_present_ { - 0}; //!< Is an external source present in this region? + 0}; //!< Is an external source present in this region? + int is_small_ {0}; //!< Is it "small", receiving < 1.5 hits per iteration? + int n_hits_ {0}; //!< Number of total hits (ray crossings) + // Mesh that subdivides this source region + int mesh_ {C_NONE}; //!< Index in openmc::model::meshes array that subdivides + //!< this source region + int64_t parent_sr_ {C_NONE}; //!< Index of a parent source region Position position_ { 0.0, 0.0, 0.0}; //!< A position somewhere inside the region Position centroid_ {0.0, 0.0, 0.0}; //!< The centroid @@ -150,7 +379,6 @@ class SourceRegion { // tasks. Each group may have a different number of tally tasks // associated with it, necessitating the use of a jagged array. vector> tally_task_; - }; // class SourceRegion class SourceRegionContainer { @@ -165,28 +393,34 @@ class SourceRegionContainer { //---------------------------------------------------------------------------- // Public Accessors int& material(int64_t sr) { return material_[sr]; } - const int& material(int64_t sr) const { return material_[sr]; } + const int material(int64_t sr) const { return material_[sr]; } + + int& is_small(int64_t sr) { return is_small_[sr]; } + const int is_small(int64_t sr) const { return is_small_[sr]; } + + int& n_hits(int64_t sr) { return n_hits_[sr]; } + const int n_hits(int64_t sr) const { return n_hits_[sr]; } OpenMPMutex& lock(int64_t sr) { return lock_[sr]; } const OpenMPMutex& lock(int64_t sr) const { return lock_[sr]; } double& volume(int64_t sr) { return volume_[sr]; } - const double& volume(int64_t sr) const { return volume_[sr]; } + const double volume(int64_t sr) const { return volume_[sr]; } double& volume_t(int64_t sr) { return volume_t_[sr]; } - const double& volume_t(int64_t sr) const { return volume_t_[sr]; } + const double volume_t(int64_t sr) const { return volume_t_[sr]; } double& volume_sq(int64_t sr) { return volume_sq_[sr]; } - const double& volume_sq(int64_t sr) const { return volume_sq_[sr]; } + const double volume_sq(int64_t sr) const { return volume_sq_[sr]; } double& volume_sq_t(int64_t sr) { return volume_sq_t_[sr]; } - const double& volume_sq_t(int64_t sr) const { return volume_sq_t_[sr]; } + const double volume_sq_t(int64_t sr) const { return volume_sq_t_[sr]; } double& volume_naive(int64_t sr) { return volume_naive_[sr]; } - const double& volume_naive(int64_t sr) const { return volume_naive_[sr]; } + const double volume_naive(int64_t sr) const { return volume_naive_[sr]; } int& position_recorded(int64_t sr) { return position_recorded_[sr]; } - const int& position_recorded(int64_t sr) const + const int position_recorded(int64_t sr) const { return position_recorded_[sr]; } @@ -195,31 +429,31 @@ class SourceRegionContainer { { return external_source_present_[sr]; } - const int& external_source_present(int64_t sr) const + const int external_source_present(int64_t sr) const { return external_source_present_[sr]; } Position& position(int64_t sr) { return position_[sr]; } - const Position& position(int64_t sr) const { return position_[sr]; } + const Position position(int64_t sr) const { return position_[sr]; } Position& centroid(int64_t sr) { return centroid_[sr]; } - const Position& centroid(int64_t sr) const { return centroid_[sr]; } + const Position centroid(int64_t sr) const { return centroid_[sr]; } Position& centroid_iteration(int64_t sr) { return centroid_iteration_[sr]; } - const Position& centroid_iteration(int64_t sr) const + const Position centroid_iteration(int64_t sr) const { return centroid_iteration_[sr]; } Position& centroid_t(int64_t sr) { return centroid_t_[sr]; } - const Position& centroid_t(int64_t sr) const { return centroid_t_[sr]; } + const Position centroid_t(int64_t sr) const { return centroid_t_[sr]; } MomentMatrix& mom_matrix(int64_t sr) { return mom_matrix_[sr]; } - const MomentMatrix& mom_matrix(int64_t sr) const { return mom_matrix_[sr]; } + const MomentMatrix mom_matrix(int64_t sr) const { return mom_matrix_[sr]; } MomentMatrix& mom_matrix_t(int64_t sr) { return mom_matrix_t_[sr]; } - const MomentMatrix& mom_matrix_t(int64_t sr) const + const MomentMatrix mom_matrix_t(int64_t sr) const { return mom_matrix_t_[sr]; } @@ -228,12 +462,12 @@ class SourceRegionContainer { { return source_gradients_[index(sr, g)]; } - const MomentArray& source_gradients(int64_t sr, int g) const + const MomentArray source_gradients(int64_t sr, int g) const { return source_gradients_[index(sr, g)]; } MomentArray& source_gradients(int64_t se) { return source_gradients_[se]; } - const MomentArray& source_gradients(int64_t se) const + const MomentArray source_gradients(int64_t se) const { return source_gradients_[se]; } @@ -242,12 +476,12 @@ class SourceRegionContainer { { return flux_moments_old_[index(sr, g)]; } - const MomentArray& flux_moments_old(int64_t sr, int g) const + const MomentArray flux_moments_old(int64_t sr, int g) const { return flux_moments_old_[index(sr, g)]; } MomentArray& flux_moments_old(int64_t se) { return flux_moments_old_[se]; } - const MomentArray& flux_moments_old(int64_t se) const + const MomentArray flux_moments_old(int64_t se) const { return flux_moments_old_[se]; } @@ -256,12 +490,12 @@ class SourceRegionContainer { { return flux_moments_new_[index(sr, g)]; } - const MomentArray& flux_moments_new(int64_t sr, int g) const + const MomentArray flux_moments_new(int64_t sr, int g) const { return flux_moments_new_[index(sr, g)]; } MomentArray& flux_moments_new(int64_t se) { return flux_moments_new_[se]; } - const MomentArray& flux_moments_new(int64_t se) const + const MomentArray flux_moments_new(int64_t se) const { return flux_moments_new_[se]; } @@ -270,12 +504,12 @@ class SourceRegionContainer { { return flux_moments_t_[index(sr, g)]; } - const MomentArray& flux_moments_t(int64_t sr, int g) const + const MomentArray flux_moments_t(int64_t sr, int g) const { return flux_moments_t_[index(sr, g)]; } MomentArray& flux_moments_t(int64_t se) { return flux_moments_t_[se]; } - const MomentArray& flux_moments_t(int64_t se) const + const MomentArray flux_moments_t(int64_t se) const { return flux_moments_t_[se]; } @@ -284,12 +518,12 @@ class SourceRegionContainer { { return scalar_flux_old_[index(sr, g)]; } - const double& scalar_flux_old(int64_t sr, int g) const + const double scalar_flux_old(int64_t sr, int g) const { return scalar_flux_old_[index(sr, g)]; } double& scalar_flux_old(int64_t se) { return scalar_flux_old_[se]; } - const double& scalar_flux_old(int64_t se) const + const double scalar_flux_old(int64_t se) const { return scalar_flux_old_[se]; } @@ -298,12 +532,12 @@ class SourceRegionContainer { { return scalar_flux_new_[index(sr, g)]; } - const double& scalar_flux_new(int64_t sr, int g) const + const double scalar_flux_new(int64_t sr, int g) const { return scalar_flux_new_[index(sr, g)]; } double& scalar_flux_new(int64_t se) { return scalar_flux_new_[se]; } - const double& scalar_flux_new(int64_t se) const + const double scalar_flux_new(int64_t se) const { return scalar_flux_new_[se]; } @@ -312,34 +546,31 @@ class SourceRegionContainer { { return scalar_flux_final_[index(sr, g)]; } - const double& scalar_flux_final(int64_t sr, int g) const + const double scalar_flux_final(int64_t sr, int g) const { return scalar_flux_final_[index(sr, g)]; } double& scalar_flux_final(int64_t se) { return scalar_flux_final_[se]; } - const double& scalar_flux_final(int64_t se) const + const double scalar_flux_final(int64_t se) const { return scalar_flux_final_[se]; } float& source(int64_t sr, int g) { return source_[index(sr, g)]; } - const float& source(int64_t sr, int g) const { return source_[index(sr, g)]; } + const float source(int64_t sr, int g) const { return source_[index(sr, g)]; } float& source(int64_t se) { return source_[se]; } - const float& source(int64_t se) const { return source_[se]; } + const float source(int64_t se) const { return source_[se]; } float& external_source(int64_t sr, int g) { return external_source_[index(sr, g)]; } - const float& external_source(int64_t sr, int g) const + const float external_source(int64_t sr, int g) const { return external_source_[index(sr, g)]; } float& external_source(int64_t se) { return external_source_[se]; } - const float& external_source(int64_t se) const - { - return external_source_[se]; - } + const float external_source(int64_t se) const { return external_source_[se]; } vector& tally_task(int64_t sr, int g) { @@ -365,13 +596,26 @@ class SourceRegionContainer { return volume_task_[sr]; } + int& mesh(int64_t sr) { return mesh_[sr]; } + const int mesh(int64_t sr) const { return mesh_[sr]; } + + int64_t& parent_sr(int64_t sr) { return parent_sr_[sr]; } + const int64_t parent_sr(int64_t sr) const { return parent_sr_[sr]; } + //---------------------------------------------------------------------------- // Public Methods void push_back(const SourceRegion& sr); void assign(int n_source_regions, const SourceRegion& source_region); void flux_swap(); - void mpi_sync_ranks(bool reduce_position); + int64_t n_source_regions() const { return n_source_regions_; } + int64_t n_source_elements() const { return n_source_regions_ * negroups_; } + int& negroups() { return negroups_; } + const int negroups() const { return negroups_; } + bool& is_linear() { return is_linear_; } + const bool is_linear() const { return is_linear_; } + SourceRegionHandle get_source_region_handle(int64_t sr); + void adjoint_reset(); private: //---------------------------------------------------------------------------- @@ -382,6 +626,10 @@ class SourceRegionContainer { // SoA storage for scalar fields (one item per source region) vector material_; + vector is_small_; + vector n_hits_; + vector mesh_; + vector parent_sr_; vector lock_; vector volume_; vector volume_t_; diff --git a/openmc/settings.py b/openmc/settings.py index 2f054ebd198..ead2c0d0026 100644 --- a/openmc/settings.py +++ b/openmc/settings.py @@ -7,11 +7,12 @@ import lxml.etree as ET +import openmc import openmc.checkvalue as cv from openmc.checkvalue import PathLike from openmc.stats.multivariate import MeshSpatial from ._xml import clean_indentation, get_text, reorder_attributes -from .mesh import _read_meshes, RegularMesh +from .mesh import _read_meshes, RegularMesh, MeshBase from .source import SourceBase, MeshSource, IndependentSource from .utility_funcs import input_path from .volume import VolumeCalculation @@ -180,6 +181,12 @@ class Settings: :sample_method: Sampling method for the ray starting location and direction of travel. Options are `prng` (default) or 'halton`. + :source_region_meshes: + List of tuples where each tuple contains a mesh and a list of + domains. Each domain is an instance of openmc.Material, openmc.Cell, + or openmc.Universe. The mesh will be applied to the listed domains + to subdivide source regions so as to improve accuracy and/or conform + with tally meshes. .. versionadded:: 0.15.0 resonance_scattering : dict @@ -1156,6 +1163,18 @@ def random_ray(self, random_ray: dict): cv.check_type('volume normalized flux tallies', value, bool) elif key == 'adjoint': cv.check_type('adjoint', value, bool) + elif key == 'source_region_meshes': + cv.check_type('source region meshes', value, Iterable) + for mesh, domains in value: + cv.check_type('mesh', mesh, MeshBase) + cv.check_type('domains', domains, Iterable) + valid_types = (openmc.Material, openmc.Cell, openmc.Universe) + for domain in domains: + if not isinstance(domain, valid_types): + raise ValueError( + f'Invalid domain type: {type(domain)}. Expected ' + 'openmc.Material, openmc.Cell, or openmc.Universe.') + cv.check_type('adjoint', value, bool) elif key == 'sample_method': cv.check_value('sample method', value, ('prng', 'halton')) @@ -1573,7 +1592,7 @@ def _create_weight_window_generators_subelement(self, root, mesh_memo=None): root.append(wwg.mesh.to_xml_element()) if mesh_memo is not None: - mesh_memo.add(wwg.mesh) + mesh_memo.add(wwg.mesh.id) def _create_weight_windows_file_element(self, root): if self.weight_windows_file is not None: @@ -1604,13 +1623,25 @@ def _create_max_tracks_subelement(self, root): elem = ET.SubElement(root, "max_tracks") elem.text = str(self._max_tracks) - def _create_random_ray_subelement(self, root): + def _create_random_ray_subelement(self, root, mesh_memo=None): if self._random_ray: element = ET.SubElement(root, "random_ray") for key, value in self._random_ray.items(): if key == 'ray_source' and isinstance(value, SourceBase): source_element = value.to_xml_element() element.append(source_element) + elif key == 'source_region_meshes': + subelement = ET.SubElement(element, 'source_region_meshes') + for mesh, domains in value: + mesh_elem = ET.SubElement(subelement, 'mesh') + mesh_elem.set('id', str(mesh.id)) + for domain in domains: + domain_elem = ET.SubElement(mesh_elem, 'domain') + domain_elem.set('id', str(domain.id)) + domain_elem.set('type', domain.__class__.__name__.lower()) + if mesh_memo is not None and mesh.id not in mesh_memo: + root.append(mesh.to_xml_element()) + mesh_memo.add(mesh.id) else: subelement = ET.SubElement(element, key) subelement.text = str(value) @@ -2007,6 +2038,22 @@ def _random_ray_from_xml_element(self, root): ) elif child.tag == 'sample_method': self.random_ray['sample_method'] = child.text + elif child.tag == 'source_region_meshes': + self.random_ray['source_region_meshes'] = [] + for mesh_elem in child.findall('mesh'): + mesh = MeshBase.from_xml_element(mesh_elem) + domains = [] + for domain_elem in mesh_elem.findall('domain'): + domain_id = int(domain_elem.get('id')) + domain_type = domain_elem.get('type') + if domain_type == 'material': + domain = openmc.Material(domain_id) + elif domain_type == 'cell': + domain = openmc.Cell(domain_id) + elif domain_type == 'universe': + domain = openmc.Universe(domain_id) + domains.append(domain) + self.random_ray['source_region_meshes'].append((mesh, domains)) def _use_decay_photons_from_xml_element(self, root): text = get_text(root, 'use_decay_photons') @@ -2077,7 +2124,7 @@ def to_xml_element(self, mesh_memo=None): self._create_weight_window_checkpoints_subelement(element) self._create_max_history_splits_subelement(element) self._create_max_tracks_subelement(element) - self._create_random_ray_subelement(element) + self._create_random_ray_subelement(element, mesh_memo) self._create_use_decay_photons_subelement(element) # Clean the indentation in the file to be user-readable diff --git a/src/finalize.cpp b/src/finalize.cpp index ad6f0cf629b..ed3d0e7f464 100644 --- a/src/finalize.cpp +++ b/src/finalize.cpp @@ -17,6 +17,7 @@ #include "openmc/photon.h" #include "openmc/plot.h" #include "openmc/random_lcg.h" +#include "openmc/random_ray/random_ray_simulation.h" #include "openmc/settings.h" #include "openmc/simulation.h" #include "openmc/source.h" @@ -174,6 +175,8 @@ int openmc_finalize() MPI_Type_free(&mpi::source_site); #endif + openmc_reset_random_ray(); + return 0; } diff --git a/src/random_ray/flat_source_domain.cpp b/src/random_ray/flat_source_domain.cpp index ae0ecb824fc..33651574fa0 100644 --- a/src/random_ray/flat_source_domain.cpp +++ b/src/random_ray/flat_source_domain.cpp @@ -1,6 +1,7 @@ #include "openmc/random_ray/flat_source_domain.h" #include "openmc/cell.h" +#include "openmc/constants.h" #include "openmc/eigenvalue.h" #include "openmc/geometry.h" #include "openmc/material.h" @@ -14,6 +15,7 @@ #include "openmc/tallies/tally.h" #include "openmc/tallies/tally_scoring.h" #include "openmc/timer.h" +#include "openmc/weight_windows.h" #include @@ -28,6 +30,8 @@ RandomRayVolumeEstimator FlatSourceDomain::volume_estimator_ { RandomRayVolumeEstimator::HYBRID}; bool FlatSourceDomain::volume_normalized_flux_tallies_ {false}; bool FlatSourceDomain::adjoint_ {false}; +std::unordered_map>> + FlatSourceDomain::mesh_domain_map_; FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) { @@ -35,20 +39,21 @@ FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) // indices, and store the material type The reason for the offsets is that // some cell types may not have material fills, and therefore do not // produce FSRs. Thus, we cannot index into the global arrays directly + int base_source_regions = 0; for (const auto& c : model::cells) { if (c->type_ != Fill::MATERIAL) { source_region_offsets_.push_back(-1); } else { - source_region_offsets_.push_back(n_source_regions_); - n_source_regions_ += c->n_instances_; - n_source_elements_ += c->n_instances_ * negroups_; + source_region_offsets_.push_back(base_source_regions); + base_source_regions += c->n_instances_; } } - // Initialize cell-wise arrays + // Initialize source regions. bool is_linear = RandomRay::source_shape_ != RandomRaySourceShape::FLAT; source_regions_ = SourceRegionContainer(negroups_, is_linear); - source_regions_.assign(n_source_regions_, SourceRegion(negroups_, is_linear)); + source_regions_.assign( + base_source_regions, SourceRegion(negroups_, is_linear)); // Initialize materials int64_t source_region_id = 0; @@ -62,7 +67,7 @@ FlatSourceDomain::FlatSourceDomain() : negroups_(data::mg.num_energy_groups_) } // Sanity check - if (source_region_id != n_source_regions_) { + if (source_region_id != base_source_regions) { fatal_error("Unexpected number of source regions"); } @@ -92,12 +97,13 @@ void FlatSourceDomain::batch_reset() { // Reset scalar fluxes and iteration volume tallies to zero #pragma omp parallel for - for (int64_t sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions(); sr++) { source_regions_.volume(sr) = 0.0; source_regions_.volume_sq(sr) = 0.0; } + #pragma omp parallel for - for (int64_t se = 0; se < n_source_elements_; se++) { + for (int64_t se = 0; se < n_source_elements(); se++) { source_regions_.scalar_flux_new(se) = 0.0; } } @@ -105,7 +111,7 @@ void FlatSourceDomain::batch_reset() void FlatSourceDomain::accumulate_iteration_flux() { #pragma omp parallel for - for (int64_t se = 0; se < n_source_elements_; se++) { + for (int64_t se = 0; se < n_source_elements(); se++) { source_regions_.scalar_flux_final(se) += source_regions_.scalar_flux_new(se); } @@ -121,13 +127,13 @@ void FlatSourceDomain::update_neutron_source(double k_eff) // Reset all source regions to zero (important for void regions) #pragma omp parallel for - for (int64_t se = 0; se < n_source_elements_; se++) { + for (int64_t se = 0; se < n_source_elements(); se++) { source_regions_.source(se) = 0.0; } // Add scattering + fission source #pragma omp parallel for - for (int64_t sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions(); sr++) { int material = source_regions_.material(sr); if (material == MATERIAL_VOID) { continue; @@ -155,7 +161,7 @@ void FlatSourceDomain::update_neutron_source(double k_eff) // Add external source if in fixed source mode if (settings::run_mode == RunMode::FIXED_SOURCE) { #pragma omp parallel for - for (int64_t se = 0; se < n_source_elements_; se++) { + for (int64_t se = 0; se < n_source_elements(); se++) { source_regions_.source(se) += source_regions_.external_source(se); } } @@ -174,14 +180,14 @@ void FlatSourceDomain::normalize_scalar_flux_and_volumes( // Normalize scalar flux to total distance travelled by all rays this // iteration #pragma omp parallel for - for (int64_t se = 0; se < n_source_elements_; se++) { + for (int64_t se = 0; se < n_source_elements(); se++) { source_regions_.scalar_flux_new(se) *= normalization_factor; } // Accumulate cell-wise ray length tallies collected this iteration, then // update the simulation-averaged cell-wise volume estimates #pragma omp parallel for - for (int64_t sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions(); sr++) { source_regions_.volume_t(sr) += source_regions_.volume(sr); source_regions_.volume_sq_t(sr) += source_regions_.volume_sq(sr); source_regions_.volume_naive(sr) = @@ -225,9 +231,10 @@ void FlatSourceDomain::set_flux_to_source(int64_t sr, int g) int64_t FlatSourceDomain::add_source_to_scalar_flux() { int64_t n_hits = 0; + double inverse_batch = 1.0 / simulation::current_batch; #pragma omp parallel for reduction(+ : n_hits) - for (int64_t sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions(); sr++) { double volume_simulation_avg = source_regions_.volume(sr); double volume_iteration = source_regions_.volume_naive(sr); @@ -237,6 +244,14 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() n_hits++; } + // Set the SR to small status if its expected number of hits + // per iteration is less than 1.5 + if (source_regions_.n_hits(sr) * inverse_batch < MIN_HITS_PER_BATCH) { + source_regions_.is_small(sr) = 1; + } else { + source_regions_.is_small(sr) = 0; + } + // The volume treatment depends on the volume estimator type // and whether or not an external source is present in the cell. double volume; @@ -248,7 +263,8 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() volume = volume_simulation_avg; break; case RandomRayVolumeEstimator::HYBRID: - if (source_regions_.external_source_present(sr)) { + if (source_regions_.external_source_present(sr) || + source_regions_.is_small(sr)) { volume = volume_iteration; } else { volume = volume_simulation_avg; @@ -279,16 +295,12 @@ int64_t FlatSourceDomain::add_source_to_scalar_flux() // in the cell we will use the previous iteration's flux estimate. This // injects a small degree of correlation into the simulation, but this // is going to be trivial when the miss rate is a few percent or less. - if (source_regions_.external_source_present(sr)) { + if (source_regions_.external_source_present(sr) && !adjoint_) { set_flux_to_old_flux(sr, g); } else { set_flux_to_source(sr, g); } } - // If the FSR was not hit this iteration, and it has never been hit in - // any iteration (i.e., volume is zero), then we want to set this to 0 - // to avoid dividing anything by a zero volume. This happens implicitly - // given that the new scalar flux arrays are set to zero each iteration. } } @@ -304,10 +316,10 @@ double FlatSourceDomain::compute_k_eff(double k_eff_old) const double fission_rate_new = 0; // Vector for gathering fission source terms for Shannon entropy calculation - vector p(n_source_regions_, 0.0f); + vector p(n_source_regions(), 0.0f); #pragma omp parallel for reduction(+ : fission_rate_old, fission_rate_new) - for (int64_t sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions(); sr++) { // If simulation averaged volume is zero, don't include this cell double volume = source_regions_.volume(sr); @@ -350,7 +362,7 @@ double FlatSourceDomain::compute_k_eff(double k_eff_old) const double inverse_sum = 1 / fission_rate_new; #pragma omp parallel for reduction(+ : H) - for (int64_t sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions(); sr++) { // Only if FSR has non-negative and non-zero fission source if (p[sr] > 0.0f) { // Normalize to total weight of bank sites. p_i for better performance @@ -408,7 +420,7 @@ void FlatSourceDomain::convert_source_regions_to_tallies() // Attempt to generate mapping for all source regions #pragma omp parallel for - for (int64_t sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions(); sr++) { // If this source region has not been hit by a ray yet, then // we aren't going to be able to map it, so skip it. @@ -423,6 +435,7 @@ void FlatSourceDomain::convert_source_regions_to_tallies() Particle p; p.r() = source_regions_.position(sr); p.r_last() = source_regions_.position(sr); + p.u() = {1.0, 0.0, 0.0}; bool found = exhaustive_find_cell(p); // Loop over energy groups (so as to support energy filters) @@ -517,7 +530,7 @@ double FlatSourceDomain::compute_fixed_source_normalization_factor() const // total external source strength in the simulation. double simulation_external_source_strength = 0.0; #pragma omp parallel for reduction(+ : simulation_external_source_strength) - for (int64_t sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions(); sr++) { int material = source_regions_.material(sr); double volume = source_regions_.volume(sr) * simulation_volume_; for (int g = 0; g < negroups_; g++) { @@ -570,7 +583,7 @@ void FlatSourceDomain::random_ray_tally() // element, we check if there are any scores needed and apply // them. #pragma omp parallel for - for (int64_t sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions(); sr++) { // The fsr.volume_ is the unitless fractional simulation averaged volume // (i.e., it is the FSR's fraction of the overall simulation volume). The // simulation_volume_ is the total 3D physical volume in cm^3 of the @@ -673,30 +686,6 @@ void FlatSourceDomain::random_ray_tally() openmc::simulation::time_tallies.stop(); } -void FlatSourceDomain::all_reduce_replicated_source_regions() -{ -#ifdef OPENMC_MPI - // If we only have 1 MPI rank, no need - // to reduce anything. - if (mpi::n_procs <= 1) - return; - - simulation::time_bank_sendrecv.start(); - - // First, we broadcast the fully mapped tally status variable so that - // all ranks are on the same page - int mapped_all_tallies_i = static_cast(mapped_all_tallies_); - MPI_Bcast(&mapped_all_tallies_i, 1, MPI_INT, 0, mpi::intracomm); - - bool reduce_position = - simulation::current_batch > settings::n_inactive && !mapped_all_tallies_i; - - source_regions_.mpi_sync_ranks(reduce_position); - - simulation::time_bank_sendrecv.stop(); -#endif -} - double FlatSourceDomain::evaluate_flux_at_point( Position r, int64_t sr, int g) const { @@ -765,8 +754,9 @@ void FlatSourceDomain::output_to_vtk() const // Relate voxel spatial locations to random ray source regions vector voxel_indices(Nx * Ny * Nz); vector voxel_positions(Nx * Ny * Nz); - -#pragma omp parallel for collapse(3) + vector weight_windows(Nx * Ny * Nz); + float min_weight = 1e20; +#pragma omp parallel for collapse(3) reduction(min : min_weight) for (int z = 0; z < Nz; z++) { for (int y = 0; y < Ny; y++) { for (int x = 0; x < Nx; x++) { @@ -776,12 +766,49 @@ void FlatSourceDomain::output_to_vtk() const sample.x = ll.x + x_delta / 2.0 + x * x_delta; Particle p; p.r() = sample; + p.r_last() = sample; + p.E() = 1.0; + p.E_last() = 1.0; + p.u() = {1.0, 0.0, 0.0}; + bool found = exhaustive_find_cell(p); + if (!found) { + voxel_indices[z * Ny * Nx + y * Nx + x] = -1; + voxel_positions[z * Ny * Nx + y * Nx + x] = sample; + weight_windows[z * Ny * Nx + y * Nx + x] = 0.0; + continue; + } + int i_cell = p.lowest_coord().cell; - int64_t source_region_idx = - source_region_offsets_[i_cell] + p.cell_instance(); - voxel_indices[z * Ny * Nx + y * Nx + x] = source_region_idx; + int64_t sr = source_region_offsets_[i_cell] + p.cell_instance(); + if (RandomRay::mesh_subdivision_enabled_) { + int mesh_idx = base_source_regions_.mesh(sr); + int mesh_bin; + if (mesh_idx == C_NONE) { + mesh_bin = 0; + } else { + mesh_bin = model::meshes[mesh_idx]->get_bin(p.r()); + } + SourceRegionKey sr_key {sr, mesh_bin}; + auto it = source_region_map_.find(sr_key); + if (it != source_region_map_.end()) { + sr = it->second; + } else { + sr = -1; + } + } + + voxel_indices[z * Ny * Nx + y * Nx + x] = sr; voxel_positions[z * Ny * Nx + y * Nx + x] = sample; + + if (variance_reduction::weight_windows.size() == 1) { + WeightWindow ww = + variance_reduction::weight_windows[0]->get_weight_window(p); + float weight = ww.lower_weight; + weight_windows[z * Ny * Nx + y * Nx + x] = weight; + if (weight < min_weight) + min_weight = weight; + } } } } @@ -798,10 +825,14 @@ void FlatSourceDomain::output_to_vtk() const std::fprintf(plot, "BINARY\n"); std::fprintf(plot, "DATASET STRUCTURED_POINTS\n"); std::fprintf(plot, "DIMENSIONS %d %d %d\n", Nx, Ny, Nz); - std::fprintf(plot, "ORIGIN 0 0 0\n"); + std::fprintf(plot, "ORIGIN %lf %lf %lf\n", ll.x, ll.y, ll.z); std::fprintf(plot, "SPACING %lf %lf %lf\n", x_delta, y_delta, z_delta); std::fprintf(plot, "POINT_DATA %d\n", Nx * Ny * Nz); + int64_t num_neg = 0; + int64_t num_samples = 0; + float min_flux = 0.0; + float max_flux = -1.0e20; // Plot multigroup flux data for (int g = 0; g < negroups_; g++) { std::fprintf(plot, "SCALARS flux_group_%d float\n", g); @@ -809,12 +840,36 @@ void FlatSourceDomain::output_to_vtk() const for (int i = 0; i < Nx * Ny * Nz; i++) { int64_t fsr = voxel_indices[i]; int64_t source_element = fsr * negroups_ + g; - float flux = evaluate_flux_at_point(voxel_positions[i], fsr, g); + float flux = 0; + if (fsr >= 0) { + flux = evaluate_flux_at_point(voxel_positions[i], fsr, g); + if (flux < 0.0) + flux = FlatSourceDomain::evaluate_flux_at_point( + voxel_positions[i], fsr, g); + } + if (flux < 0.0) { + num_neg++; + if (flux < min_flux) { + min_flux = flux; + } + } + if (flux > max_flux) + max_flux = flux; + num_samples++; flux = convert_to_big_endian(flux); std::fwrite(&flux, sizeof(float), 1, plot); } } + // Slightly negative fluxes can be normal when sampling corners of linear + // source regions. However, very common and high magnitude negative fluxes + // may indicate numerical instability. + if (num_neg > 0) { + warning(fmt::format("{} plot samples ({:.4f}%) contained negative fluxes " + "(minumum found = {:.2e} maximum_found = {:.2e})", + num_neg, (100.0 * num_neg) / num_samples, min_flux, max_flux)); + } + // Plot FSRs std::fprintf(plot, "SCALARS FSRs float\n"); std::fprintf(plot, "LOOKUP_TABLE default\n"); @@ -828,7 +883,9 @@ void FlatSourceDomain::output_to_vtk() const std::fprintf(plot, "SCALARS Materials int\n"); std::fprintf(plot, "LOOKUP_TABLE default\n"); for (int fsr : voxel_indices) { - int mat = source_regions_.material(fsr); + int mat = -1; + if (fsr >= 0) + mat = source_regions_.material(fsr); mat = convert_to_big_endian(mat); std::fwrite(&mat, sizeof(int), 1, plot); } @@ -839,15 +896,16 @@ void FlatSourceDomain::output_to_vtk() const std::fprintf(plot, "LOOKUP_TABLE default\n"); for (int i = 0; i < Nx * Ny * Nz; i++) { int64_t fsr = voxel_indices[i]; - float total_fission = 0.0; - int mat = source_regions_.material(fsr); - if (mat != MATERIAL_VOID) { - for (int g = 0; g < negroups_; g++) { - int64_t source_element = fsr * negroups_ + g; - float flux = evaluate_flux_at_point(voxel_positions[i], fsr, g); - double sigma_f = sigma_f_[mat * negroups_ + g]; - total_fission += sigma_f * flux; + if (fsr >= 0) { + int mat = source_regions_.material(fsr); + if (mat != MATERIAL_VOID) { + for (int g = 0; g < negroups_; g++) { + int64_t source_element = fsr * negroups_ + g; + float flux = evaluate_flux_at_point(voxel_positions[i], fsr, g); + double sigma_f = sigma_f_[mat * negroups_ + g]; + total_fission += sigma_f * flux; + } } } total_fission = convert_to_big_endian(total_fission); @@ -859,14 +917,29 @@ void FlatSourceDomain::output_to_vtk() const for (int i = 0; i < Nx * Ny * Nz; i++) { int64_t fsr = voxel_indices[i]; float total_external = 0.0f; - for (int g = 0; g < negroups_; g++) { - total_external += source_regions_.external_source(fsr, g); + if (fsr >= 0) { + for (int g = 0; g < negroups_; g++) { + total_external += source_regions_.external_source(fsr, g); + } } total_external = convert_to_big_endian(total_external); std::fwrite(&total_external, sizeof(float), 1, plot); } } + // Plot weight window data + if (variance_reduction::weight_windows.size() == 1) { + std::fprintf(plot, "SCALARS weight_window_lower float\n"); + std::fprintf(plot, "LOOKUP_TABLE default\n"); + for (int i = 0; i < Nx * Ny * Nz; i++) { + float weight = weight_windows[i]; + if (weight == 0.0) + weight = min_weight; + weight = convert_to_big_endian(weight); + std::fwrite(&weight, sizeof(float), 1, plot); + } + } + std::fclose(plot); } } @@ -938,7 +1011,7 @@ void FlatSourceDomain::count_external_source_regions() { n_external_source_regions_ = 0; #pragma omp parallel for reduction(+ : n_external_source_regions_) - for (int64_t sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions(); sr++) { if (source_regions_.external_source_present(sr)) { n_external_source_regions_++; } @@ -984,7 +1057,7 @@ void FlatSourceDomain::convert_external_sources() // Divide the fixed source term by sigma t (to save time when applying each // iteration) #pragma omp parallel for - for (int64_t sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions(); sr++) { int material = source_regions_.material(sr); if (material == MATERIAL_VOID) { continue; @@ -1056,7 +1129,7 @@ void FlatSourceDomain::set_adjoint_sources(const vector& forward_flux) // small in magnitude, but rather due to the source region being physically // small in volume and thus having a noisy flux estimate. #pragma omp parallel for - for (int64_t sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions(); sr++) { for (int g = 0; g < negroups_; g++) { double flux = forward_flux[sr * negroups_ + g]; if (flux <= 0.0) { @@ -1064,13 +1137,16 @@ void FlatSourceDomain::set_adjoint_sources(const vector& forward_flux) } else { source_regions_.external_source(sr, g) = 1.0 / flux; } + if (flux > 0.0) { + source_regions_.external_source_present(sr) = 1; + } } } // Divide the fixed source term by sigma t (to save time when applying each // iteration) #pragma omp parallel for - for (int64_t sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions(); sr++) { int material = source_regions_.material(sr); if (material == MATERIAL_VOID) { continue; @@ -1103,12 +1179,252 @@ void FlatSourceDomain::transpose_scattering_matrix() void FlatSourceDomain::serialize_final_fluxes(vector& flux) { // Ensure array is correct size - flux.resize(n_source_regions_ * negroups_); + flux.resize(n_source_regions() * negroups_); // Serialize the final fluxes for output #pragma omp parallel for - for (int64_t se = 0; se < n_source_elements_; se++) { + for (int64_t se = 0; se < n_source_elements(); se++) { flux[se] = source_regions_.scalar_flux_final(se); } } +void FlatSourceDomain::apply_mesh_to_cell_instances(int32_t i_cell, + int32_t mesh_idx, int target_material_id, const vector& instances, + bool is_target_void) +{ + Cell& cell = *model::cells[i_cell]; + if (cell.type_ != Fill::MATERIAL) + return; + for (int32_t j : instances) { + int cell_material_idx = cell.material(j); + int cell_material_id = (cell_material_idx == C_NONE) + ? C_NONE + : model::materials[cell_material_idx]->id(); + + if ((target_material_id == C_NONE && !is_target_void) || + cell_material_id == target_material_id) { + int64_t sr = source_region_offsets_[i_cell] + j; + if (source_regions_.mesh(sr) != C_NONE) { + // print out the source region that is broken: + fatal_error(fmt::format("Source region {} already has mesh idx {} " + "applied, but trying to apply mesh idx {}", + sr, source_regions_.mesh(sr), mesh_idx)); + } + source_regions_.mesh(sr) = mesh_idx; + } + } +} + +void FlatSourceDomain::apply_mesh_to_cell_and_children(int32_t i_cell, + int32_t mesh_idx, int32_t target_material_id, bool is_target_void) +{ + Cell& cell = *model::cells[i_cell]; + + if (cell.type_ == Fill::MATERIAL) { + vector instances(cell.n_instances_); + std::iota(instances.begin(), instances.end(), 0); + apply_mesh_to_cell_instances( + i_cell, mesh_idx, target_material_id, instances, is_target_void); + } else if (target_material_id == C_NONE && !is_target_void) { + for (int j = 0; j < cell.n_instances_; j++) { + std::unordered_map> cell_instance_list = + cell.get_contained_cells(j, nullptr); + for (const auto& pair : cell_instance_list) { + int32_t i_child_cell = pair.first; + apply_mesh_to_cell_instances(i_child_cell, mesh_idx, target_material_id, + pair.second, is_target_void); + } + } + } +} + +void FlatSourceDomain::apply_meshes() +{ + // Skip if there are no mappings between mesh IDs and domains + if (mesh_domain_map_.empty()) + return; + + // Loop over meshes + for (int mesh_idx = 0; mesh_idx < model::meshes.size(); mesh_idx++) { + Mesh* mesh = model::meshes[mesh_idx].get(); + int mesh_id = mesh->id(); + + // Skip if mesh id is not present in the map + if (mesh_domain_map_.find(mesh_id) == mesh_domain_map_.end()) + continue; + + // Loop over domains associated with the mesh + for (auto& domain : mesh_domain_map_[mesh_id]) { + Source::DomainType domain_type = domain.first; + int domain_id = domain.second; + + if (domain_type == Source::DomainType::MATERIAL) { + for (int i_cell = 0; i_cell < model::cells.size(); i_cell++) { + if (domain_id == C_NONE) { + apply_mesh_to_cell_and_children(i_cell, mesh_idx, domain_id, true); + } else { + apply_mesh_to_cell_and_children(i_cell, mesh_idx, domain_id, false); + } + } + } else if (domain_type == Source::DomainType::CELL) { + int32_t i_cell = model::cell_map[domain_id]; + apply_mesh_to_cell_and_children(i_cell, mesh_idx, C_NONE, false); + } else if (domain_type == Source::DomainType::UNIVERSE) { + int32_t i_universe = model::universe_map[domain_id]; + Universe& universe = *model::universes[i_universe]; + for (int32_t i_cell : universe.cells_) { + apply_mesh_to_cell_and_children(i_cell, mesh_idx, C_NONE, false); + } + } + } + } +} + +void FlatSourceDomain::prepare_base_source_regions() +{ + std::swap(source_regions_, base_source_regions_); + source_regions_.negroups() = base_source_regions_.negroups(); + source_regions_.is_linear() = base_source_regions_.is_linear(); +} + +SourceRegionHandle FlatSourceDomain::get_subdivided_source_region_handle( + int64_t sr, int mesh_bin, Position r, double dist, Direction u) +{ + SourceRegionKey sr_key {sr, mesh_bin}; + + // Case 1: Check if the source region key is already present in the permanent + // map. This is the most common condition, as any source region visited in a + // previous power iteration will already be present in the permanent map. If + // the source region key is found, we translate the key into a specific 1D + // source region index and return a handle its position in the + // source_regions_ vector. + auto it = source_region_map_.find(sr_key); + if (it != source_region_map_.end()) { + int64_t sr = it->second; + return source_regions_.get_source_region_handle(sr); + } + + // Case 2: Check if the source region key is present in the temporary (thread + // safe) map. This is a common occurrence in the first power iteration when + // the source region has already been visited already by some other ray. We + // begin by locking the temporary map before any operations are performed. The + // lock is not global over the full data structure -- it will be dependent on + // which key is used. + discovered_source_regions_.lock(sr_key); + + // If the key is found in the temporary map, then we return a handle to the + // source region that is stored in the temporary map. + if (discovered_source_regions_.contains(sr_key)) { + SourceRegionHandle handle {discovered_source_regions_[sr_key]}; + discovered_source_regions_.unlock(sr_key); + return handle; + } + + // Case 3: The source region key is not present anywhere, but it is only due + // to floating point artifacts. These artifacts occur when the overlaid mesh + // overlaps with actual geometry surfaces. In these cases, roundoff error may + // result in the ray tracer detecting an additional (very short) segment + // though a mesh bin that is actually past the physical source region + // boundary. This is a result of the the multi-level ray tracing treatment in + // OpenMC, which depending on the number of universes in the hierarchy etc can + // result in the wrong surface being selected as the nearest. This can happen + // in a lattice when there are two directions that both are very close in + // distance, within the tolerance of FP_REL_PRECISION, and the are thus + // treated as being equivalent so alternative logic is used. However, when we + // go and ray trace on this with the mesh tracer we may go past the surface + // bounding the current source region. + // + // To filter out this case, before we create the new source region, we double + // check that the actual starting point of this segment (r) is still in the + // same geometry source region that we started in. If an artifact is detected, + // we discard the segment (and attenuation through it) as it is not really a + // valid source region and will have only an infinitessimally small cell + // combined with the mesh bin. Thankfully, this is a fairly rare condition, + // and only triggers for very short ray lengths. It can be fixed by decreasing + // the value of FP_REL_PRECISION in constants.h, but this may have unknown + // consequences for the general ray tracer, so for now we do the below sanity + // checks before generating phantom source regions. A significant extra cost + // is incurred in instantiating the GeometryState object and doing a cell + // lookup, but again, this is going to be an extremely rare thing to check + // after the first power iteration has completed. + + // Sanity check on source region id + GeometryState gs; + gs.r() = r + TINY_BIT * u; + gs.u() = {1.0, 0.0, 0.0}; + exhaustive_find_cell(gs); + int gs_i_cell = gs.lowest_coord().cell; + int64_t sr_found = source_region_offsets_[gs_i_cell] + gs.cell_instance(); + if (sr_found != sr) { + discovered_source_regions_.unlock(sr_key); + SourceRegionHandle handle; + handle.is_numerical_fp_artifact_ = true; + return handle; + } + + // Sanity check on mesh bin + int mesh_idx = base_source_regions_.mesh(sr); + if (mesh_idx == C_NONE) { + if (mesh_bin != 0) { + discovered_source_regions_.unlock(sr_key); + SourceRegionHandle handle; + handle.is_numerical_fp_artifact_ = true; + return handle; + } + } else { + Mesh* mesh = model::meshes[mesh_idx].get(); + int bin_found = mesh->get_bin(r + TINY_BIT * u); + if (bin_found != mesh_bin) { + discovered_source_regions_.unlock(sr_key); + SourceRegionHandle handle; + handle.is_numerical_fp_artifact_ = true; + return handle; + } + } + + // Case 4: The source region key is valid, but is not present anywhere. This + // condition only occurs the first time the source region is discovered + // (typically in the first power iteration). In this case, we need to handle + // creation of the new source region and its storage into the parallel map. + // The new source region is created by copying the base source region, so as + // to inherit material, external source, and some flux properties etc. We + // also pass the base source region id to allow the new source region to + // know which base source region it is derived from. + SourceRegion* sr_ptr = discovered_source_regions_.emplace( + sr_key, {base_source_regions_.get_source_region_handle(sr), sr}); + discovered_source_regions_.unlock(sr_key); + SourceRegionHandle handle {*sr_ptr}; + return handle; +} + +void FlatSourceDomain::finalize_discovered_source_regions() +{ + // Extract keys for entries with a valid volume. + vector keys; + for (const auto& pair : discovered_source_regions_) { + if (pair.second.volume_ > 0.0) { + keys.push_back(pair.first); + } + } + + if (!keys.empty()) { + // Sort the keys, so as to ensure reproducible ordering given that source + // regions may have been added to discovered_source_regions_ in an arbitrary + // order due to shared memory threading. + std::sort(keys.begin(), keys.end()); + + // Append the source regions in the sorted key order. + for (const auto& key : keys) { + const SourceRegion& sr = discovered_source_regions_[key]; + source_region_map_[key] = source_regions_.n_source_regions(); + source_regions_.push_back(sr); + } + + // If any new source regions were discovered, we need to update the + // tally mapping between source regions and tally bins. + mapped_all_tallies_ = false; + } + + discovered_source_regions_.clear(); +} + } // namespace openmc diff --git a/src/random_ray/linear_source_domain.cpp b/src/random_ray/linear_source_domain.cpp index 3819c7d703d..81412164eca 100644 --- a/src/random_ray/linear_source_domain.cpp +++ b/src/random_ray/linear_source_domain.cpp @@ -24,12 +24,12 @@ void LinearSourceDomain::batch_reset() { FlatSourceDomain::batch_reset(); #pragma omp parallel for - for (int64_t sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions(); sr++) { source_regions_.centroid_iteration(sr) = {0.0, 0.0, 0.0}; source_regions_.mom_matrix(sr) = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; } #pragma omp parallel for - for (int64_t se = 0; se < n_source_elements_; se++) { + for (int64_t se = 0; se < n_source_elements(); se++) { source_regions_.flux_moments_new(se) = {0.0, 0.0, 0.0}; } } @@ -40,8 +40,14 @@ void LinearSourceDomain::update_neutron_source(double k_eff) double inverse_k_eff = 1.0 / k_eff; +// Reset all source regions to zero (important for void regions) #pragma omp parallel for - for (int64_t sr = 0; sr < n_source_regions_; sr++) { + for (int64_t se = 0; se < n_source_elements(); se++) { + source_regions_.source(se) = 0.0; + } + +#pragma omp parallel for + for (int64_t sr = 0; sr < n_source_regions(); sr++) { int material = source_regions_.material(sr); if (material == MATERIAL_VOID) { continue; @@ -98,7 +104,7 @@ void LinearSourceDomain::update_neutron_source(double k_eff) if (settings::run_mode == RunMode::FIXED_SOURCE) { // Add external source to flat source term if in fixed source mode #pragma omp parallel for - for (int64_t se = 0; se < n_source_elements_; se++) { + for (int64_t se = 0; se < n_source_elements(); se++) { source_regions_.source(se) += source_regions_.external_source(se); } } @@ -115,7 +121,7 @@ void LinearSourceDomain::normalize_scalar_flux_and_volumes( // Normalize flux to total distance travelled by all rays this iteration #pragma omp parallel for - for (int64_t se = 0; se < n_source_elements_; se++) { + for (int64_t se = 0; se < n_source_elements(); se++) { source_regions_.scalar_flux_new(se) *= normalization_factor; source_regions_.flux_moments_new(se) *= normalization_factor; } @@ -123,7 +129,7 @@ void LinearSourceDomain::normalize_scalar_flux_and_volumes( // Accumulate cell-wise ray length tallies collected this iteration, then // update the simulation-averaged cell-wise volume estimates #pragma omp parallel for - for (int64_t sr = 0; sr < n_source_regions_; sr++) { + for (int64_t sr = 0; sr < n_source_regions(); sr++) { source_regions_.centroid_t(sr) += source_regions_.centroid_iteration(sr); source_regions_.mom_matrix_t(sr) += source_regions_.mom_matrix(sr); source_regions_.volume_t(sr) += source_regions_.volume(sr); @@ -154,13 +160,22 @@ void LinearSourceDomain::set_flux_to_flux_plus_source( source_regions_.scalar_flux_new(sr, g) /= volume; source_regions_.scalar_flux_new(sr, g) += source_regions_.source(sr, g); } - source_regions_.flux_moments_new(sr, g) *= (1.0 / volume); + // If a source region is small, then the moments are likely noisy, so we zero + // them. This is reasonable, given that small regions can get by with a flat + // source approximation anyhow. + if (source_regions_.is_small(sr)) { + source_regions_.flux_moments_new(sr, g) = {0.0, 0.0, 0.0}; + } else { + source_regions_.flux_moments_new(sr, g) *= (1.0 / volume); + } } void LinearSourceDomain::set_flux_to_old_flux(int64_t sr, int g) { - source_regions_.scalar_flux_new(g) = source_regions_.scalar_flux_old(g); - source_regions_.flux_moments_new(g) = source_regions_.flux_moments_old(g); + source_regions_.scalar_flux_new(sr, g) = + source_regions_.scalar_flux_old(sr, g); + source_regions_.flux_moments_new(sr, g) = + source_regions_.flux_moments_old(sr, g); } void LinearSourceDomain::accumulate_iteration_flux() @@ -170,7 +185,7 @@ void LinearSourceDomain::accumulate_iteration_flux() // Accumulate scalar flux moments #pragma omp parallel for - for (int64_t se = 0; se < n_source_elements_; se++) { + for (int64_t se = 0; se < n_source_elements(); se++) { source_regions_.flux_moments_t(se) += source_regions_.flux_moments_new(se); } } diff --git a/src/random_ray/random_ray.cpp b/src/random_ray/random_ray.cpp index f9ebb6f9d01..9a9d1394aac 100644 --- a/src/random_ray/random_ray.cpp +++ b/src/random_ray/random_ray.cpp @@ -237,6 +237,7 @@ double RandomRay::distance_inactive_; double RandomRay::distance_active_; unique_ptr RandomRay::ray_source_; RandomRaySourceShape RandomRay::source_shape_ {RandomRaySourceShape::FLAT}; +bool RandomRay::mesh_subdivision_enabled_ {false}; RandomRaySampleMethod RandomRay::sample_method_ {RandomRaySampleMethod::PRNG}; RandomRay::RandomRay() @@ -313,7 +314,7 @@ void RandomRay::event_advance_ray() wgt() = 0.0; } - attenuate_flux(distance_alive, true); + attenuate_flux(distance_alive, true, distance_dead); distance_travelled_ = distance_alive; } else { distance_travelled_ += distance; @@ -327,23 +328,84 @@ void RandomRay::event_advance_ray() } } -void RandomRay::attenuate_flux(double distance, bool is_active) +void RandomRay::attenuate_flux(double distance, bool is_active, double offset) { + // Determine source region index etc. + int i_cell = lowest_coord().cell; + + // The base source region is the spatial region index + int64_t sr = domain_->source_region_offsets_[i_cell] + cell_instance(); + + // Perform ray tracing across mesh + if (mesh_subdivision_enabled_) { + // Determine the mesh index for the base source region, if any + int mesh_idx = domain_->base_source_regions_.mesh(sr); + + if (mesh_idx == C_NONE) { + // If there's no mesh being applied to this cell, then + // we just attenuate the flux as normal, and set + // the mesh bin to 0 + attenuate_flux_inner(distance, is_active, sr, 0, r()); + } else { + // If there is a mesh being applied to this cell, then + // we loop over all the bin crossings and attenuate + // separately. + Mesh* mesh = model::meshes[mesh_idx].get(); + + // We adjust the start and end positions of the ray slightly + // to accomodate for floating point precision issues that tend + // to occur at mesh boundaries that overlap with geometry lattice + // boundaries. + Position start = r() + (offset + TINY_BIT) * u(); + Position end = start + (distance - 2.0 * TINY_BIT) * u(); + double reduced_distance = (end - start).norm(); + + // Ray trace through the mesh and record bins and lengths + mesh_bins_.resize(0); + mesh_fractional_lengths_.resize(0); + mesh->bins_crossed(start, end, u(), mesh_bins_, mesh_fractional_lengths_); + + // Loop over all mesh bins and attenuate flux + for (int b = 0; b < mesh_bins_.size(); b++) { + double physical_length = reduced_distance * mesh_fractional_lengths_[b]; + attenuate_flux_inner( + physical_length, is_active, sr, mesh_bins_[b], start); + start += physical_length * u(); + } + } + } else { + attenuate_flux_inner(distance, is_active, sr, C_NONE, r()); + } +} + +void RandomRay::attenuate_flux_inner( + double distance, bool is_active, int64_t sr, int mesh_bin, Position r) +{ + SourceRegionHandle srh; + if (mesh_subdivision_enabled_) { + srh = domain_->get_subdivided_source_region_handle( + sr, mesh_bin, r, distance, u()); + if (srh.is_numerical_fp_artifact_) { + return; + } + } else { + srh = domain_->source_regions_.get_source_region_handle(sr); + } switch (source_shape_) { case RandomRaySourceShape::FLAT: if (this->material() == MATERIAL_VOID) { - attenuate_flux_flat_source_void(distance, is_active); + attenuate_flux_flat_source_void(srh, distance, is_active, r); } else { - attenuate_flux_flat_source(distance, is_active); + attenuate_flux_flat_source(srh, distance, is_active, r); } break; case RandomRaySourceShape::LINEAR: case RandomRaySourceShape::LINEAR_XY: if (this->material() == MATERIAL_VOID) { - attenuate_flux_linear_source_void(distance, is_active); + attenuate_flux_linear_source_void(srh, distance, is_active, r); } else { - attenuate_flux_linear_source(distance, is_active); + attenuate_flux_linear_source(srh, distance, is_active, r); } break; default: @@ -364,18 +426,13 @@ void RandomRay::attenuate_flux(double distance, bool is_active) // than use of many atomic operations corresponding to each energy group // individually (at least on CPU). Several other bookkeeping tasks are also // performed when inside the lock. -void RandomRay::attenuate_flux_flat_source(double distance, bool is_active) +void RandomRay::attenuate_flux_flat_source( + SourceRegionHandle& srh, double distance, bool is_active, Position r) { // The number of geometric intersections is counted for reporting purposes n_event()++; - // Determine source region index etc. - int i_cell = lowest_coord().cell; - - // The source region is the spatial region index - int64_t sr = domain_->source_region_offsets_[i_cell] + cell_instance(); - - // The source element is the energy-specific region index + // Get material int material = this->material(); // MOC incoming flux attenuation + source contribution/attenuation equation @@ -383,115 +440,99 @@ void RandomRay::attenuate_flux_flat_source(double distance, bool is_active) float sigma_t = domain_->sigma_t_[material * negroups_ + g]; float tau = sigma_t * distance; float exponential = cjosey_exponential(tau); // exponential = 1 - exp(-tau) - float new_delta_psi = - (angular_flux_[g] - domain_->source_regions_.source(sr, g)) * exponential; + float new_delta_psi = (angular_flux_[g] - srh.source(g)) * exponential; delta_psi_[g] = new_delta_psi; angular_flux_[g] -= new_delta_psi; } // If ray is in the active phase (not in dead zone), make contributions to // source region bookkeeping - if (is_active) { - // Aquire lock for source region - domain_->source_regions_.lock(sr).lock(); + // Aquire lock for source region + srh.lock(); + if (is_active) { // Accumulate delta psi into new estimate of source region flux for // this iteration for (int g = 0; g < negroups_; g++) { - domain_->source_regions_.scalar_flux_new(sr, g) += delta_psi_[g]; + srh.scalar_flux_new(g) += delta_psi_[g]; } // Accomulate volume (ray distance) into this iteration's estimate // of the source region's volume - domain_->source_regions_.volume(sr) += distance; + srh.volume() += distance; - // Tally valid position inside the source region (e.g., midpoint of - // the ray) if not done already - if (!domain_->source_regions_.position_recorded(sr)) { - Position midpoint = r() + u() * (distance / 2.0); - domain_->source_regions_.position(sr) = midpoint; - domain_->source_regions_.position_recorded(sr) = 1; - } + srh.n_hits() += 1; + } - // Release lock - domain_->source_regions_.lock(sr).unlock(); + // Tally valid position inside the source region (e.g., midpoint of + // the ray) if not done already + if (!srh.position_recorded()) { + Position midpoint = r + u() * (distance / 2.0); + srh.position() = midpoint; + srh.position_recorded() = 1; } + + // Release lock + srh.unlock(); } // Alternative flux attenuation function for true void regions. -void RandomRay::attenuate_flux_flat_source_void(double distance, bool is_active) +void RandomRay::attenuate_flux_flat_source_void( + SourceRegionHandle& srh, double distance, bool is_active, Position r) { // The number of geometric intersections is counted for reporting purposes n_event()++; - // Determine source region index etc. - int i_cell = lowest_coord().cell; - - // The source region is the spatial region index - int64_t sr = domain_->source_region_offsets_[i_cell] + cell_instance(); + int material = this->material(); // If ray is in the active phase (not in dead zone), make contributions to // source region bookkeeping if (is_active) { // Aquire lock for source region - domain_->source_regions_.lock(sr).lock(); + srh.lock(); // Accumulate delta psi into new estimate of source region flux for // this iteration for (int g = 0; g < negroups_; g++) { - domain_->source_regions_.scalar_flux_new(sr, g) += - angular_flux_[g] * distance; + srh.scalar_flux_new(g) += angular_flux_[g] * distance; } // Accomulate volume (ray distance) into this iteration's estimate // of the source region's volume - domain_->source_regions_.volume(sr) += distance; - domain_->source_regions_.volume_sq(sr) += distance * distance; + srh.volume() += distance; + srh.volume_sq() += distance * distance; + srh.n_hits() += 1; // Tally valid position inside the source region (e.g., midpoint of // the ray) if not done already - if (!domain_->source_regions_.position_recorded(sr)) { - Position midpoint = r() + u() * (distance / 2.0); - domain_->source_regions_.position(sr) = midpoint; - domain_->source_regions_.position_recorded(sr) = 1; + if (!srh.position_recorded()) { + Position midpoint = r + u() * (distance / 2.0); + srh.position() = midpoint; + srh.position_recorded() = 1; } // Release lock - domain_->source_regions_.lock(sr).unlock(); + srh.unlock(); } // Add source to incoming angular flux, assuming void region for (int g = 0; g < negroups_; g++) { - angular_flux_[g] += - domain_->source_regions_.external_source(sr, g) * distance; + angular_flux_[g] += srh.external_source(g) * distance; } } -void RandomRay::attenuate_flux_linear_source(double distance, bool is_active) +void RandomRay::attenuate_flux_linear_source( + SourceRegionHandle& srh, double distance, bool is_active, Position r) { - // Cast domain to LinearSourceDomain - LinearSourceDomain* domain = dynamic_cast(domain_); - if (!domain) { - fatal_error("RandomRay::attenuate_flux_linear_source() called with " - "non-LinearSourceDomain domain."); - } - // The number of geometric intersections is counted for reporting purposes n_event()++; - // Determine source region index etc. - int i_cell = lowest_coord().cell; - - // The source region is the spatial region index - int64_t sr = domain_->source_region_offsets_[i_cell] + cell_instance(); - - // The source element is the energy-specific region index int material = this->material(); - Position& centroid = domain_->source_regions_.centroid(sr); - Position midpoint = r() + u() * (distance / 2.0); + Position& centroid = srh.centroid(); + Position midpoint = r + u() * (distance / 2.0); // Determine the local position of the midpoint and the ray origin // relative to the source region's centroid @@ -503,9 +544,9 @@ void RandomRay::attenuate_flux_linear_source(double distance, bool is_active) // be no estimate of its centroid. We detect this by checking if it has // any accumulated volume. If its volume is zero, just use the midpoint // of the ray as the region's centroid. - if (domain_->source_regions_.volume_t(sr)) { + if (srh.volume_t()) { rm_local = midpoint - centroid; - r0_local = r() - centroid; + r0_local = r - centroid; } else { rm_local = {0.0, 0.0, 0.0}; r0_local = -u() * 0.5 * distance; @@ -530,10 +571,8 @@ void RandomRay::attenuate_flux_linear_source(double distance, bool is_active) // calculated from the source gradients dot product with local centroid // and direction, respectively. float spatial_source = - domain_->source_regions_.source(sr, g) + - rm_local.dot(domain_->source_regions_.source_gradients(sr, g)); - float dir_source = - u().dot(domain_->source_regions_.source_gradients(sr, g)); + srh.source(g) + rm_local.dot(srh.source_gradients(g)); + float dir_source = u().dot(srh.source_gradients(g)); float gn = exponentialG(tau); float f1 = 1.0f - tau * gn; @@ -566,44 +605,47 @@ void RandomRay::attenuate_flux_linear_source(double distance, bool is_active) } } + // Compute an estimate of the spatial moments matrix for the source + // region based on parameters from this ray's crossing + MomentMatrix moment_matrix_estimate; + moment_matrix_estimate.compute_spatial_moments_matrix( + rm_local, u(), distance); + + // Aquire lock for source region + srh.lock(); + // If ray is in the active phase (not in dead zone), make contributions to // source region bookkeeping - if (is_active) { - // Compute an estimate of the spatial moments matrix for the source - // region based on parameters from this ray's crossing - MomentMatrix moment_matrix_estimate; - moment_matrix_estimate.compute_spatial_moments_matrix( - rm_local, u(), distance); - - // Aquire lock for source region - domain_->source_regions_.lock(sr).lock(); + if (is_active) { // Accumulate deltas into the new estimate of source region flux for this // iteration for (int g = 0; g < negroups_; g++) { - domain_->source_regions_.scalar_flux_new(sr, g) += delta_psi_[g]; - domain_->source_regions_.flux_moments_new(sr, g) += delta_moments_[g]; + srh.scalar_flux_new(g) += delta_psi_[g]; + srh.flux_moments_new(g) += delta_moments_[g]; } // Accumulate the volume (ray segment distance), centroid, and spatial // momement estimates into the running totals for the iteration for this - // source region. The centroid and spatial momements estimates are scaled by - // the ray segment length as part of length averaging of the estimates. - domain_->source_regions_.volume(sr) += distance; - domain_->source_regions_.centroid_iteration(sr) += midpoint * distance; + // source region. The centroid and spatial momements estimates are scaled + // by the ray segment length as part of length averaging of the estimates. + srh.volume() += distance; + srh.centroid_iteration() += midpoint * distance; moment_matrix_estimate *= distance; - domain_->source_regions_.mom_matrix(sr) += moment_matrix_estimate; + srh.mom_matrix() += moment_matrix_estimate; - // Tally valid position inside the source region (e.g., midpoint of - // the ray) if not done already - if (!domain_->source_regions_.position_recorded(sr)) { - domain_->source_regions_.position(sr) = midpoint; - domain_->source_regions_.position_recorded(sr) = 1; - } + srh.n_hits() += 1; + } - // Release lock - domain_->source_regions_.lock(sr).unlock(); + // Tally valid position inside the source region (e.g., midpoint of + // the ray) if not done already + if (!srh.position_recorded()) { + srh.position() = midpoint; + srh.position_recorded() = 1; } + + // Release lock + srh.unlock(); } // If traveling through a void region, the source term is either zero @@ -615,26 +657,13 @@ void RandomRay::attenuate_flux_linear_source(double distance, bool is_active) // estimating the flux at specific pixel coordinates. Thus, plots will look // nicer/more accurate if we record flux moments, so this function is useful. void RandomRay::attenuate_flux_linear_source_void( - double distance, bool is_active) + SourceRegionHandle& srh, double distance, bool is_active, Position r) { - // Cast domain to LinearSourceDomain - LinearSourceDomain* domain = dynamic_cast(domain_); - if (!domain) { - fatal_error("RandomRay::attenuate_flux_linear_source() called with " - "non-LinearSourceDomain domain."); - } - // The number of geometric intersections is counted for reporting purposes n_event()++; - // Determine source region index etc. - int i_cell = lowest_coord().cell; - - // The source region is the spatial region index - int64_t sr = domain_->source_region_offsets_[i_cell] + cell_instance(); - - Position& centroid = domain_->source_regions_.centroid(sr); - Position midpoint = r() + u() * (distance / 2.0); + Position& centroid = srh.centroid(); + Position midpoint = r + u() * (distance / 2.0); // Determine the local position of the midpoint and the ray origin // relative to the source region's centroid @@ -646,9 +675,9 @@ void RandomRay::attenuate_flux_linear_source_void( // be no estimate of its centroid. We detect this by checking if it has // any accumulated volume. If its volume is zero, just use the midpoint // of the ray as the region's centroid. - if (domain_->source_regions_.volume_t(sr)) { + if (srh.volume_t()) { rm_local = midpoint - centroid; - r0_local = r() - centroid; + r0_local = r - centroid; } else { rm_local = {0.0, 0.0, 0.0}; r0_local = -u() * 0.5 * distance; @@ -659,7 +688,7 @@ void RandomRay::attenuate_flux_linear_source_void( // transport through a void region is greatly simplified. Here we // compute the updated flux moments. for (int g = 0; g < negroups_; g++) { - float spatial_source = domain_->source_regions_.source(sr, g); + float spatial_source = srh.external_source(g); float new_delta_psi = (angular_flux_[g] - spatial_source) * distance; float h1 = 0.5f; h1 = h1 * angular_flux_[g]; @@ -688,41 +717,41 @@ void RandomRay::attenuate_flux_linear_source_void( rm_local, u(), distance); // Aquire lock for source region - domain_->source_regions_.lock(sr).lock(); + srh.lock(); // Accumulate delta psi into new estimate of source region flux for // this iteration, and update flux momements for (int g = 0; g < negroups_; g++) { - domain_->source_regions_.scalar_flux_new(sr, g) += - angular_flux_[g] * distance; - domain_->source_regions_.flux_moments_new(sr, g) += delta_moments_[g]; + srh.scalar_flux_new(g) += angular_flux_[g] * distance; + srh.flux_moments_new(g) += delta_moments_[g]; } // Accumulate the volume (ray segment distance), centroid, and spatial // momement estimates into the running totals for the iteration for this // source region. The centroid and spatial momements estimates are scaled by // the ray segment length as part of length averaging of the estimates. - domain_->source_regions_.volume(sr) += distance; - domain_->source_regions_.volume_sq(sr) += distance_2; - domain_->source_regions_.centroid_iteration(sr) += midpoint * distance; + srh.volume() += distance; + srh.volume_sq() += distance_2; + srh.centroid_iteration() += midpoint * distance; moment_matrix_estimate *= distance; - domain_->source_regions_.mom_matrix(sr) += moment_matrix_estimate; + srh.mom_matrix() += moment_matrix_estimate; // Tally valid position inside the source region (e.g., midpoint of // the ray) if not done already - if (!domain_->source_regions_.position_recorded(sr)) { - domain_->source_regions_.position(sr) = midpoint; - domain_->source_regions_.position_recorded(sr) = 1; + if (!srh.position_recorded()) { + srh.position() = midpoint; + srh.position_recorded() = 1; } + srh.n_hits() += 1; + // Release lock - domain_->source_regions_.lock(sr).unlock(); + srh.unlock(); } // Add source to incoming angular flux, assuming void region for (int g = 0; g < negroups_; g++) { - angular_flux_[g] += - domain_->source_regions_.external_source(sr, g) * distance; + angular_flux_[g] += srh.external_source(g) * distance; } } @@ -738,7 +767,7 @@ void RandomRay::initialize_ray(uint64_t ray_id, FlatSourceDomain* domain) wgt() = 1.0; // set identifier for particle - id() = simulation::work_index[mpi::rank] + ray_id; + id() = ray_id; // generate source site using sample method SourceSite site; @@ -775,8 +804,26 @@ void RandomRay::initialize_ray(uint64_t ray_id, FlatSourceDomain* domain) int i_cell = lowest_coord().cell; int64_t sr = domain_->source_region_offsets_[i_cell] + cell_instance(); - for (int g = 0; g < negroups_; g++) { - angular_flux_[g] = domain_->source_regions_.source(sr, g); + SourceRegionHandle srh; + if (mesh_subdivision_enabled_) { + int mesh_idx = domain_->base_source_regions_.mesh(sr); + int mesh_bin; + if (mesh_idx == C_NONE) { + mesh_bin = 0; + } else { + Mesh* mesh = model::meshes[mesh_idx].get(); + mesh_bin = mesh->get_bin(r()); + } + srh = + domain_->get_subdivided_source_region_handle(sr, mesh_bin, r(), 0.0, u()); + } else { + srh = domain_->source_regions_.get_source_region_handle(sr); + } + + if (!srh.is_numerical_fp_artifact_) { + for (int g = 0; g < negroups_; g++) { + angular_flux_[g] = srh.source(g); + } } } diff --git a/src/random_ray/random_ray_simulation.cpp b/src/random_ray/random_ray_simulation.cpp index f1477c3fe52..bf01d2f5766 100644 --- a/src/random_ray/random_ray_simulation.cpp +++ b/src/random_ray/random_ray_simulation.cpp @@ -14,6 +14,7 @@ #include "openmc/tallies/tally.h" #include "openmc/tallies/tally_scoring.h" #include "openmc/timer.h" +#include "openmc/weight_windows.h" namespace openmc { @@ -48,13 +49,17 @@ void openmc_run_random_ray() // Declare forward flux so that it can be saved for later adjoint simulation vector forward_flux; + SourceRegionContainer forward_source_regions; + SourceRegionContainer forward_base_source_regions; + std::unordered_map + forward_source_region_map; { // Initialize Random Ray Simulation Object RandomRaySimulation sim; // Initialize fixed sources, if present - sim.prepare_fixed_sources(); + sim.apply_fixed_sources_and_mesh_domains(); // Begin main simulation timer simulation::time_total.start(); @@ -77,12 +82,13 @@ void openmc_run_random_ray() forward_flux[i] *= source_normalization_factor; } + forward_source_regions = sim.domain()->source_regions_; + forward_source_region_map = sim.domain()->source_region_map_; + forward_base_source_regions = sim.domain()->base_source_regions_; + // Finalize OpenMC openmc_simulation_finalize(); - // Reduce variables across MPI ranks - sim.reduce_simulation_statistics(); - // Output all simulation results sim.output_simulation_results(); } @@ -107,7 +113,9 @@ void openmc_run_random_ray() RandomRaySimulation adjoint_sim; // Initialize adjoint fixed sources, if present - adjoint_sim.prepare_fixed_sources_adjoint(forward_flux); + adjoint_sim.prepare_fixed_sources_adjoint(forward_flux, + forward_source_regions, forward_base_source_regions, + forward_source_region_map); // Transpose scattering matrix adjoint_sim.domain()->transpose_scattering_matrix(); @@ -127,9 +135,6 @@ void openmc_run_random_ray() // Finalize OpenMC openmc_simulation_finalize(); - // Reduce variables across MPI ranks - adjoint_sim.reduce_simulation_statistics(); - // Output all simulation results adjoint_sim.output_simulation_results(); } @@ -321,12 +326,36 @@ void validate_random_ray_inputs() #ifdef OPENMC_MPI if (mpi::n_procs > 1) { warning( - "Domain replication in random ray is supported, but suffers from poor " - "scaling of source all-reduce operations. Performance may severely " - "degrade beyond just a few MPI ranks. Domain decomposition may be " - "implemented in the future to provide efficient scaling."); + "MPI parallelism is not supported by the random ray solver. All work " + "will be performed by rank 0. Domain decomposition may be implemented in " + "the future to provide efficient MPI scaling."); } #endif + + // Warn about instability resulting from linear sources in small regions + // when generating weight windows with FW-CADIS and an overlaid mesh. + /////////////////////////////////////////////////////////////////// + if (RandomRay::source_shape_ == RandomRaySourceShape::LINEAR && + RandomRay::mesh_subdivision_enabled_ && + variance_reduction::weight_windows.size() > 0) { + warning( + "Linear sources may result in negative fluxes in small source regions " + "generated by mesh subdivision. Negative sources may result in low " + "quality FW-CADIS weight windows. We recommend you use flat source mode " + "when generating weight windows with an overlaid mesh tally."); + } +} + +void openmc_reset_random_ray() +{ + FlatSourceDomain::volume_estimator_ = RandomRayVolumeEstimator::HYBRID; + FlatSourceDomain::volume_normalized_flux_tallies_ = false; + FlatSourceDomain::adjoint_ = false; + FlatSourceDomain::mesh_domain_map_.clear(); + RandomRay::ray_source_.reset(); + RandomRay::source_shape_ = RandomRaySourceShape::FLAT; + RandomRay::mesh_subdivision_enabled_ = false; + RandomRay::sample_method_ = RandomRaySampleMethod::PRNG; } //============================================================================== @@ -361,19 +390,29 @@ RandomRaySimulation::RandomRaySimulation() domain_->flatten_xs(); } -void RandomRaySimulation::prepare_fixed_sources() +void RandomRaySimulation::apply_fixed_sources_and_mesh_domains() { if (settings::run_mode == RunMode::FIXED_SOURCE) { // Transfer external source user inputs onto random ray source regions domain_->convert_external_sources(); domain_->count_external_source_regions(); } + domain_->apply_meshes(); } void RandomRaySimulation::prepare_fixed_sources_adjoint( - vector& forward_flux) + vector& forward_flux, SourceRegionContainer& forward_source_regions, + SourceRegionContainer& forward_base_source_regions, + std::unordered_map& + forward_source_region_map) { if (settings::run_mode == RunMode::FIXED_SOURCE) { + if (RandomRay::mesh_subdivision_enabled_) { + domain_->source_regions_ = forward_source_regions; + domain_->source_region_map_ = forward_source_region_map; + domain_->base_source_regions_ = forward_base_source_regions; + domain_->source_regions_.adjoint_reset(); + } domain_->set_adjoint_sources(forward_flux); } } @@ -382,75 +421,93 @@ void RandomRaySimulation::simulate() { // Random ray power iteration loop while (simulation::current_batch < settings::n_batches) { - // Initialize the current batch initialize_batch(); initialize_generation(); - // Reset total starting particle weight used for normalizing tallies - simulation::total_weight = 1.0; - - // Update source term (scattering + fission) - domain_->update_neutron_source(k_eff_); - - // Reset scalar fluxes, iteration volume tallies, and region hit flags to - // zero - domain_->batch_reset(); + // MPI not supported in random ray solver, so all work is done by rank 0 + // TODO: Implement domain decomposition for MPI parallelism + if (mpi::master) { + + // Reset total starting particle weight used for normalizing tallies + simulation::total_weight = 1.0; + + // Update source term (scattering + fission) + domain_->update_neutron_source(k_eff_); + + // Reset scalar fluxes, iteration volume tallies, and region hit flags to + // zero + domain_->batch_reset(); + + // At the beginning of the simulation, if mesh subvivision is in use, we + // need to swap the main source region container into the base container, + // as the main source region container will be used to hold the true + // subdivided source regions. The base container will therefore only + // contain the external source region information, the mesh indices, + // material properties, and initial guess values for the flux/source. + if (RandomRay::mesh_subdivision_enabled_ && + simulation::current_batch == 1 && !FlatSourceDomain::adjoint_) { + domain_->prepare_base_source_regions(); + } - // Start timer for transport - simulation::time_transport.start(); + // Start timer for transport + simulation::time_transport.start(); // Transport sweep over all random rays for the iteration #pragma omp parallel for schedule(dynamic) \ reduction(+ : total_geometric_intersections_) - for (int i = 0; i < simulation::work_per_rank; i++) { - RandomRay ray(i, domain_.get()); - total_geometric_intersections_ += - ray.transport_history_based_single_ray(); - } + for (int i = 0; i < settings::n_particles; i++) { + RandomRay ray(i, domain_.get()); + total_geometric_intersections_ += + ray.transport_history_based_single_ray(); + } - simulation::time_transport.stop(); + simulation::time_transport.stop(); - // If using multiple MPI ranks, perform all reduce on all transport results - domain_->all_reduce_replicated_source_regions(); + // If using mesh subdivision, add any newly discovered source regions + // to the main source region container. + if (RandomRay::mesh_subdivision_enabled_) { + domain_->finalize_discovered_source_regions(); + } - // Normalize scalar flux and update volumes - domain_->normalize_scalar_flux_and_volumes( - settings::n_particles * RandomRay::distance_active_); + // Normalize scalar flux and update volumes + domain_->normalize_scalar_flux_and_volumes( + settings::n_particles * RandomRay::distance_active_); - // Add source to scalar flux, compute number of FSR hits - int64_t n_hits = domain_->add_source_to_scalar_flux(); + // Add source to scalar flux, compute number of FSR hits + int64_t n_hits = domain_->add_source_to_scalar_flux(); - if (settings::run_mode == RunMode::EIGENVALUE) { - // Compute random ray k-eff - k_eff_ = domain_->compute_k_eff(k_eff_); + if (settings::run_mode == RunMode::EIGENVALUE) { + // Compute random ray k-eff + k_eff_ = domain_->compute_k_eff(k_eff_); - // Store random ray k-eff into OpenMC's native k-eff variable - global_tally_tracklength = k_eff_; - } + // Store random ray k-eff into OpenMC's native k-eff variable + global_tally_tracklength = k_eff_; + } - // Execute all tallying tasks, if this is an active batch - if (simulation::current_batch > settings::n_inactive) { + // Execute all tallying tasks, if this is an active batch + if (simulation::current_batch > settings::n_inactive) { - // Add this iteration's scalar flux estimate to final accumulated estimate - domain_->accumulate_iteration_flux(); + // Add this iteration's scalar flux estimate to final accumulated + // estimate + domain_->accumulate_iteration_flux(); - if (mpi::master) { // Generate mapping between source regions and tallies if (!domain_->mapped_all_tallies_) { domain_->convert_source_regions_to_tallies(); } - // Use above mapping to contribute FSR flux data to appropriate tallies + // Use above mapping to contribute FSR flux data to appropriate + // tallies domain_->random_ray_tally(); } - } - // Set phi_old = phi_new - domain_->flux_swap(); + // Set phi_old = phi_new + domain_->flux_swap(); - // Check for any obvious insabilities/nans/infs - instability_check(n_hits, k_eff_, avg_miss_rate_); + // Check for any obvious insabilities/nans/infs + instability_check(n_hits, k_eff_, avg_miss_rate_); + } // End MPI master work // Finalize the current batch finalize_generation(); @@ -458,27 +515,13 @@ void RandomRaySimulation::simulate() } // End random ray power iteration loop } -void RandomRaySimulation::reduce_simulation_statistics() -{ - // Reduce number of intersections -#ifdef OPENMC_MPI - if (mpi::n_procs > 1) { - uint64_t total_geometric_intersections_reduced = 0; - MPI_Reduce(&total_geometric_intersections_, - &total_geometric_intersections_reduced, 1, MPI_UNSIGNED_LONG, MPI_SUM, 0, - mpi::intracomm); - total_geometric_intersections_ = total_geometric_intersections_reduced; - } -#endif -} - void RandomRaySimulation::output_simulation_results() const { // Print random ray results if (mpi::master) { print_results_random_ray(total_geometric_intersections_, avg_miss_rate_ / settings::n_batches, negroups_, - domain_->n_source_regions_, domain_->n_external_source_regions_); + domain_->n_source_regions(), domain_->n_external_source_regions_); if (model::plots.size() > 0) { domain_->output_to_vtk(); } @@ -490,8 +533,8 @@ void RandomRaySimulation::output_simulation_results() const void RandomRaySimulation::instability_check( int64_t n_hits, double k_eff, double& avg_miss_rate) const { - double percent_missed = ((domain_->n_source_regions_ - n_hits) / - static_cast(domain_->n_source_regions_)) * + double percent_missed = ((domain_->n_source_regions() - n_hits) / + static_cast(domain_->n_source_regions())) * 100.0; avg_miss_rate += percent_missed; diff --git a/src/random_ray/source_region.cpp b/src/random_ray/source_region.cpp index 4ac86ea8cbb..6c0df0157f1 100644 --- a/src/random_ray/source_region.cpp +++ b/src/random_ray/source_region.cpp @@ -2,9 +2,36 @@ #include "openmc/error.h" #include "openmc/message_passing.h" +#include "openmc/simulation.h" namespace openmc { +//============================================================================== +// SourceRegionHandle implementation +//============================================================================== +SourceRegionHandle::SourceRegionHandle(SourceRegion& sr) + : negroups_(sr.scalar_flux_old_.size()), material_(&sr.material_), + is_small_(&sr.is_small_), n_hits_(&sr.n_hits_), + is_linear_(sr.source_gradients_.size() > 0), lock_(&sr.lock_), + volume_(&sr.volume_), volume_t_(&sr.volume_t_), volume_sq_(&sr.volume_sq_), + volume_sq_t_(&sr.volume_sq_t_), volume_naive_(&sr.volume_naive_), + position_recorded_(&sr.position_recorded_), + external_source_present_(&sr.external_source_present_), + position_(&sr.position_), centroid_(&sr.centroid_), + centroid_iteration_(&sr.centroid_iteration_), centroid_t_(&sr.centroid_t_), + mom_matrix_(&sr.mom_matrix_), mom_matrix_t_(&sr.mom_matrix_t_), + volume_task_(&sr.volume_task_), mesh_(&sr.mesh_), + parent_sr_(&sr.parent_sr_), scalar_flux_old_(sr.scalar_flux_old_.data()), + scalar_flux_new_(sr.scalar_flux_new_.data()), source_(sr.source_.data()), + external_source_(sr.external_source_.data()), + scalar_flux_final_(sr.scalar_flux_final_.data()), + source_gradients_(sr.source_gradients_.data()), + flux_moments_old_(sr.flux_moments_old_.data()), + flux_moments_new_(sr.flux_moments_new_.data()), + flux_moments_t_(sr.flux_moments_t_.data()), + tally_task_(sr.tally_task_.data()) +{} + //============================================================================== // SourceRegion implementation //============================================================================== @@ -25,7 +52,6 @@ SourceRegion::SourceRegion(int negroups, bool is_linear) scalar_flux_final_.assign(negroups, 0.0); tally_task_.resize(negroups); - if (is_linear) { source_gradients_.resize(negroups); flux_moments_old_.resize(negroups); @@ -34,15 +60,37 @@ SourceRegion::SourceRegion(int negroups, bool is_linear) } } +SourceRegion::SourceRegion(const SourceRegionHandle& handle, int64_t parent_sr) + : SourceRegion(handle.negroups_, handle.is_linear_) +{ + material_ = handle.material(); + mesh_ = handle.mesh(); + parent_sr_ = parent_sr; + for (int g = 0; g < scalar_flux_new_.size(); g++) { + scalar_flux_old_[g] = handle.scalar_flux_old(g); + source_[g] = handle.source(g); + } + + if (settings::run_mode == RunMode::FIXED_SOURCE) { + external_source_present_ = handle.external_source_present(); + for (int g = 0; g < scalar_flux_new_.size(); g++) { + external_source_[g] = handle.external_source(g); + } + } +} + //============================================================================== // SourceRegionContainer implementation //============================================================================== + void SourceRegionContainer::push_back(const SourceRegion& sr) { n_source_regions_++; // Scalar fields material_.push_back(sr.material_); + is_small_.push_back(sr.is_small_); + n_hits_.push_back(sr.n_hits_); lock_.push_back(sr.lock_); volume_.push_back(sr.volume_); volume_t_.push_back(sr.volume_t_); @@ -53,6 +101,8 @@ void SourceRegionContainer::push_back(const SourceRegion& sr) external_source_present_.push_back(sr.external_source_present_); position_.push_back(sr.position_); volume_task_.push_back(sr.volume_task_); + mesh_.push_back(sr.mesh_); + parent_sr_.push_back(sr.parent_sr_); // Only store these fields if is_linear_ is true if (is_linear_) { @@ -92,6 +142,8 @@ void SourceRegionContainer::assign( // Clear existing data n_source_regions_ = 0; material_.clear(); + is_small_.clear(); + n_hits_.clear(); lock_.clear(); volume_.clear(); volume_t_.clear(); @@ -101,6 +153,8 @@ void SourceRegionContainer::assign( position_recorded_.clear(); external_source_present_.clear(); position_.clear(); + mesh_.clear(); + parent_sr_.clear(); if (is_linear_) { centroid_.clear(); @@ -140,103 +194,88 @@ void SourceRegionContainer::flux_swap() } } -void SourceRegionContainer::mpi_sync_ranks(bool reduce_position) +SourceRegionHandle SourceRegionContainer::get_source_region_handle(int64_t sr) { -#ifdef OPENMC_MPI - - // The "position_recorded" variable needs to be allreduced (and maxed), - // as whether or not a cell was hit will affect some decisions in how the - // source is calculated in the next iteration so as to avoid dividing - // by zero. We take the max rather than the sum as the hit values are - // expected to be zero or 1. - MPI_Allreduce(MPI_IN_PLACE, position_recorded_.data(), 1, MPI_INT, MPI_MAX, - mpi::intracomm); - - // The position variable is more complicated to reduce than the others, - // as we do not want the sum of all positions in each cell, rather, we - // want to just pick any single valid position. Thus, we perform a gather - // and then pick the first valid position we find for all source regions - // that have had a position recorded. This operation does not need to - // be broadcast back to other ranks, as this value is only used for the - // tally conversion operation, which is only performed on the master rank. - // While this is expensive, it only needs to be done for active batches, - // and only if we have not mapped all the tallies yet. Once tallies are - // fully mapped, then the position vector is fully populated, so this - // operation can be skipped. - - // Then, we perform the gather of position data, if needed - if (reduce_position) { - - // Master rank will gather results and pick valid positions - if (mpi::master) { - // Initialize temporary vector for receiving positions - vector> all_position; - all_position.resize(mpi::n_procs); - for (int i = 0; i < mpi::n_procs; i++) { - all_position[i].resize(n_source_regions_); - } - - // Copy master rank data into gathered vector for convenience - all_position[0] = position_; - - // Receive all data into gather vector - for (int i = 1; i < mpi::n_procs; i++) { - MPI_Recv(all_position[i].data(), n_source_regions_ * 3, MPI_DOUBLE, i, - 0, mpi::intracomm, MPI_STATUS_IGNORE); - } - - // Scan through gathered data and pick first valid cell posiiton - for (int64_t sr = 0; sr < n_source_regions_; sr++) { - if (position_recorded_[sr] == 1) { - for (int i = 0; i < mpi::n_procs; i++) { - if (all_position[i][sr].x != 0.0 || all_position[i][sr].y != 0.0 || - all_position[i][sr].z != 0.0) { - position_[sr] = all_position[i][sr]; - break; - } - } - } - } - } else { - // Other ranks just send in their data - MPI_Send(position_.data(), n_source_regions_ * 3, MPI_DOUBLE, 0, 0, - mpi::intracomm); - } + SourceRegionHandle handle; + handle.negroups_ = negroups(); + handle.material_ = &material(sr); + handle.is_small_ = &is_small(sr); + handle.n_hits_ = &n_hits(sr); + handle.is_linear_ = is_linear(); + handle.lock_ = &lock(sr); + handle.volume_ = &volume(sr); + handle.volume_t_ = &volume_t(sr); + handle.volume_sq_ = &volume_sq(sr); + handle.volume_sq_t_ = &volume_sq_t(sr); + handle.volume_naive_ = &volume_naive(sr); + handle.position_recorded_ = &position_recorded(sr); + handle.external_source_present_ = &external_source_present(sr); + handle.position_ = &position(sr); + handle.volume_task_ = &volume_task(sr); + handle.mesh_ = &mesh(sr); + handle.parent_sr_ = &parent_sr(sr); + handle.scalar_flux_old_ = &scalar_flux_old(sr, 0); + handle.scalar_flux_new_ = &scalar_flux_new(sr, 0); + handle.source_ = &source(sr, 0); + handle.external_source_ = &external_source(sr, 0); + handle.scalar_flux_final_ = &scalar_flux_final(sr, 0); + handle.tally_task_ = &tally_task(sr, 0); + + if (handle.is_linear_) { + handle.centroid_ = ¢roid(sr); + handle.centroid_iteration_ = ¢roid_iteration(sr); + handle.centroid_t_ = ¢roid_t(sr); + handle.mom_matrix_ = &mom_matrix(sr); + handle.mom_matrix_t_ = &mom_matrix_t(sr); + handle.source_gradients_ = &source_gradients(sr, 0); + handle.flux_moments_old_ = &flux_moments_old(sr, 0); + handle.flux_moments_new_ = &flux_moments_new(sr, 0); + handle.flux_moments_t_ = &flux_moments_t(sr, 0); } - // For the rest of the source region data, we simply perform an all reduce, - // as these values will be needed on all ranks for transport during the - // next iteration. - MPI_Allreduce(MPI_IN_PLACE, volume_.data(), n_source_regions_, MPI_DOUBLE, - MPI_SUM, mpi::intracomm); - MPI_Allreduce(MPI_IN_PLACE, volume_sq_.data(), n_source_regions_, MPI_DOUBLE, - MPI_SUM, mpi::intracomm); - - MPI_Allreduce(MPI_IN_PLACE, scalar_flux_new_.data(), - n_source_regions_ * negroups_, MPI_DOUBLE, MPI_SUM, mpi::intracomm); - - if (is_linear_) { - // We are going to assume we can safely cast Position, MomentArray, - // and MomentMatrix to contiguous arrays of doubles for the MPI - // allreduce operation. This is a safe assumption as typically - // compilers will at most pad to 8 byte boundaries. If a new FP32 - // MomentArray type is introduced, then there will likely be padding, in - // which case this function will need to become more complex. - if (sizeof(MomentArray) != 3 * sizeof(double) || - sizeof(MomentMatrix) != 6 * sizeof(double)) { - fatal_error( - "Unexpected buffer padding in linear source domain reduction."); - } + return handle; +} - MPI_Allreduce(MPI_IN_PLACE, static_cast(flux_moments_new_.data()), - n_source_regions_ * negroups_ * 3, MPI_DOUBLE, MPI_SUM, mpi::intracomm); - MPI_Allreduce(MPI_IN_PLACE, static_cast(mom_matrix_.data()), - n_source_regions_ * 6, MPI_DOUBLE, MPI_SUM, mpi::intracomm); - MPI_Allreduce(MPI_IN_PLACE, static_cast(centroid_iteration_.data()), - n_source_regions_ * 3, MPI_DOUBLE, MPI_SUM, mpi::intracomm); +void SourceRegionContainer::adjoint_reset() +{ + std::fill(n_hits_.begin(), n_hits_.end(), 0); + std::fill(volume_.begin(), volume_.end(), 0.0); + std::fill(volume_t_.begin(), volume_t_.end(), 0.0); + std::fill(volume_sq_.begin(), volume_sq_.end(), 0.0); + std::fill(volume_sq_t_.begin(), volume_sq_t_.end(), 0.0); + std::fill(volume_naive_.begin(), volume_naive_.end(), 0.0); + std::fill( + external_source_present_.begin(), external_source_present_.end(), 0); + std::fill(external_source_.begin(), external_source_.end(), 0.0); + std::fill(centroid_.begin(), centroid_.end(), Position {0.0, 0.0, 0.0}); + std::fill(centroid_iteration_.begin(), centroid_iteration_.end(), + Position {0.0, 0.0, 0.0}); + std::fill(centroid_t_.begin(), centroid_t_.end(), Position {0.0, 0.0, 0.0}); + std::fill(mom_matrix_.begin(), mom_matrix_.end(), + MomentMatrix {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}); + std::fill(mom_matrix_t_.begin(), mom_matrix_t_.end(), + MomentMatrix {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}); + for (auto& task_set : volume_task_) { + task_set.clear(); + } + std::fill(scalar_flux_old_.begin(), scalar_flux_old_.end(), 0.0); + std::fill(scalar_flux_new_.begin(), scalar_flux_new_.end(), 0.0); + std::fill(scalar_flux_final_.begin(), scalar_flux_final_.end(), 0.0); + std::fill(source_.begin(), source_.end(), 0.0f); + std::fill(external_source_.begin(), external_source_.end(), 0.0f); + + std::fill(source_gradients_.begin(), source_gradients_.end(), + MomentArray {0.0, 0.0, 0.0}); + std::fill(flux_moments_old_.begin(), flux_moments_old_.end(), + MomentArray {0.0, 0.0, 0.0}); + std::fill(flux_moments_new_.begin(), flux_moments_new_.end(), + MomentArray {0.0, 0.0, 0.0}); + std::fill(flux_moments_t_.begin(), flux_moments_t_.end(), + MomentArray {0.0, 0.0, 0.0}); + + for (auto& task_set : tally_task_) { + task_set.clear(); } - -#endif } -} // namespace openmc \ No newline at end of file +} // namespace openmc diff --git a/src/settings.cpp b/src/settings.cpp index 67fdfea666e..49b6590ecc7 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -1,4 +1,5 @@ #include "openmc/settings.h" +#include "openmc/random_ray/flat_source_domain.h" #include // for ceil, pow #include // for numeric_limits @@ -319,6 +320,31 @@ void get_run_parameters(pugi::xml_node node_base) fatal_error("Unrecognized sample method: " + temp_str); } } + if (check_for_node(random_ray_node, "source_region_meshes")) { + pugi::xml_node node_source_region_meshes = + random_ray_node.child("source_region_meshes"); + for (pugi::xml_node node_mesh : + node_source_region_meshes.children("mesh")) { + int mesh_id = std::stoi(node_mesh.attribute("id").value()); + for (pugi::xml_node node_domain : node_mesh.children("domain")) { + int domain_id = std::stoi(node_domain.attribute("id").value()); + std::string domain_type = node_domain.attribute("type").value(); + Source::DomainType type; + if (domain_type == "material") { + type = Source::DomainType::MATERIAL; + } else if (domain_type == "cell") { + type = Source::DomainType::CELL; + } else if (domain_type == "universe") { + type = Source::DomainType::UNIVERSE; + } else { + throw std::runtime_error("Unknown domain type: " + domain_type); + } + FlatSourceDomain::mesh_domain_map_[mesh_id].emplace_back( + type, domain_id); + RandomRay::mesh_subdivision_enabled_ = true; + } + } + } } } diff --git a/src/weight_windows.cpp b/src/weight_windows.cpp index 9daf21f750a..0efa5a6aeee 100644 --- a/src/weight_windows.cpp +++ b/src/weight_windows.cpp @@ -657,11 +657,18 @@ void WeightWindows::update_weights(const Tally* tally, const std::string& value, } } - xt::noalias(new_bounds) = 1.0 / new_bounds; - + // We take the inverse, but are careful not to divide by zero e.g. if some + // mesh bins are not reachable in the physical geometry. + xt::noalias(new_bounds) = + xt::where(xt::not_equal(new_bounds, 0.0), 1.0 / new_bounds, 0.0); auto max_val = xt::amax(new_bounds)(); - xt::noalias(new_bounds) = new_bounds / (2.0 * max_val); + + // For bins that were missed, we use the minimum weight window value. This + // shouldn't matter except for plotting. + auto min_val = xt::amin(new_bounds)(); + xt::noalias(new_bounds) = + xt::where(xt::not_equal(new_bounds, 0.0), new_bounds, min_val); } // make sure that values where the mean is zero are set s.t. the weight window diff --git a/tests/regression_tests/random_ray_fixed_source_mesh/__init__.py b/tests/regression_tests/random_ray_fixed_source_mesh/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regression_tests/random_ray_fixed_source_mesh/flat/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_mesh/flat/inputs_true.dat new file mode 100644 index 00000000000..67c2ffc15e1 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_mesh/flat/inputs_true.dat @@ -0,0 +1,271 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 30 + 15 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + + + + + + + + + + + + flat + + + 24 24 24 + 0.0 0.0 0.0 + 30.0 30.0 30.0 + + + 36 36 36 + 0.0 0.0 0.0 + 30.0 30.0 30.0 + + + 30 30 30 + 0.0 0.0 0.0 + 30.0 30.0 30.0 + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_fixed_source_mesh/flat/results_true.dat b/tests/regression_tests/random_ray_fixed_source_mesh/flat/results_true.dat new file mode 100644 index 00000000000..0b0eaa44798 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_mesh/flat/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +1.750296E+00 +2.051829E-01 +tally 2: +8.045199E-02 +4.384408E-04 +tally 3: +5.216828E-03 +1.840861E-06 diff --git a/tests/regression_tests/random_ray_fixed_source_mesh/linear/inputs_true.dat b/tests/regression_tests/random_ray_fixed_source_mesh/linear/inputs_true.dat new file mode 100644 index 00000000000..10b7c74f180 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_mesh/linear/inputs_true.dat @@ -0,0 +1,271 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 90 + 30 + 15 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + + + + + + + + + + + + linear + + + 24 24 24 + 0.0 0.0 0.0 + 30.0 30.0 30.0 + + + 36 36 36 + 0.0 0.0 0.0 + 30.0 30.0 30.0 + + + 30 30 30 + 0.0 0.0 0.0 + 30.0 30.0 30.0 + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/random_ray_fixed_source_mesh/linear/results_true.dat b/tests/regression_tests/random_ray_fixed_source_mesh/linear/results_true.dat new file mode 100644 index 00000000000..23754a15734 --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_mesh/linear/results_true.dat @@ -0,0 +1,9 @@ +tally 1: +1.780361E+00 +2.137912E-01 +tally 2: +8.230400E-02 +4.596391E-04 +tally 3: +5.207797E-03 +1.834531E-06 diff --git a/tests/regression_tests/random_ray_fixed_source_mesh/test.py b/tests/regression_tests/random_ray_fixed_source_mesh/test.py new file mode 100644 index 00000000000..0b93b2a7a6a --- /dev/null +++ b/tests/regression_tests/random_ray_fixed_source_mesh/test.py @@ -0,0 +1,53 @@ +import os + +import openmc +from openmc.examples import random_ray_three_region_cube +from openmc.utility_funcs import change_directory +import pytest + +from tests.testing_harness import TolerantPyAPITestHarness + + +class MGXSTestHarness(TolerantPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + + +def make_mesh(dim): + width = 30.0 + mesh = openmc.RegularMesh() + mesh.dimension = (dim, dim, dim) + mesh.lower_left = (0.0, 0.0, 0.0) + mesh.upper_right = (width, width, width) + return mesh + + +@pytest.mark.parametrize("shape", ["flat", "linear"]) +def test_random_ray_fixed_source_mesh(shape): + with change_directory(shape): + openmc.reset_auto_ids() + model = random_ray_three_region_cube() + + # We will apply three different mesh resolutions to three different domain types + source_universe = model.geometry.get_universes_by_name('source universe')[0] + void_cell = model.geometry.get_cells_by_name('infinite void region')[0] + absorber_mat = model.geometry.get_materials_by_name('absorber')[0] + + model.settings.random_ray['source_region_meshes'] = [ + (make_mesh(24), [source_universe]), + (make_mesh(36), [void_cell]), + (make_mesh(30), [absorber_mat]) + ] + + # We also test flat/linear source shapes to ensure they are both + # working correctly with the mesh overlay logic + model.settings.random_ray['source_shape'] = shape + + model.settings.inactive = 15 + model.settings.batches = 30 + + harness = MGXSTestHarness('statepoint.30.h5', model) + harness.main() diff --git a/tests/regression_tests/random_ray_k_eff_mesh/__init__.py b/tests/regression_tests/random_ray_k_eff_mesh/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regression_tests/random_ray_k_eff_mesh/inputs_true.dat b/tests/regression_tests/random_ray_k_eff_mesh/inputs_true.dat new file mode 100644 index 00000000000..cdc717198f3 --- /dev/null +++ b/tests/regression_tests/random_ray_k_eff_mesh/inputs_true.dat @@ -0,0 +1,119 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0.126 0.126 + 10 10 + -0.63 -0.63 + +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 + + + 1.26 1.26 + 2 2 + -1.26 -1.26 + +2 2 +2 5 + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 10 + 5 + multi-group + + 100.0 + 20.0 + + + -1.26 -1.26 -1 1.26 1.26 1 + + + True + + + + + + + + 40 40 + -1.26 -1.26 + 1.26 1.26 + + + + + 2 2 + -1.26 -1.26 + 1.26 1.26 + + + 1 + + + 1e-05 0.0635 10.0 100.0 1000.0 500000.0 1000000.0 20000000.0 + + + 1 2 + flux fission nu-fission + analog + + + diff --git a/tests/regression_tests/random_ray_k_eff_mesh/results_true.dat b/tests/regression_tests/random_ray_k_eff_mesh/results_true.dat new file mode 100644 index 00000000000..535db3b5512 --- /dev/null +++ b/tests/regression_tests/random_ray_k_eff_mesh/results_true.dat @@ -0,0 +1,171 @@ +k-combined: +8.379203E-01 8.057199E-03 +tally 1: +5.080171E+00 +5.167984E+00 +1.880341E+00 +7.079266E-01 +4.576373E+00 +4.193319E+00 +2.859914E+00 +1.638769E+00 +4.243332E-01 +3.607732E-02 +1.032742E+00 +2.136998E-01 +1.692643E+00 +5.794069E-01 +5.445214E-02 +5.995212E-04 +1.325256E-01 +3.551193E-03 +2.372336E+00 +1.147031E+00 +7.807378E-02 +1.242019E-03 +1.900160E-01 +7.356955E-03 +7.135636E+00 +1.035026E+01 +8.273225E-02 +1.392069E-03 +2.013562E-01 +8.245961E-03 +2.044034E+01 +8.394042E+01 +3.100485E-02 +1.932097E-04 +7.671933E-02 +1.182985E-03 +1.313652E+01 +3.451847E+01 +1.764978E-01 +6.230420E-03 +4.909196E-01 +4.820140E-02 +7.585874E+00 +1.150936E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +3.386790E+00 +2.295327E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.820058E+00 +6.729238E-01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +2.694013E+00 +1.481363E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +7.453818E+00 +1.128202E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.823642E+01 +6.682239E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +1.137903E+01 +2.590265E+01 +0.000000E+00 +0.000000E+00 +0.000000E+00 +0.000000E+00 +4.589529E+00 +4.219985E+00 +1.712446E+00 +5.873675E-01 +4.167750E+00 +3.479202E+00 +2.728285E+00 +1.492132E+00 +4.104063E-01 +3.377132E-02 +9.988468E-01 +2.000404E-01 +1.660506E+00 +5.567967E-01 +5.427391E-02 +5.944708E-04 +1.320918E-01 +3.521277E-03 +2.305889E+00 +1.082691E+00 +7.695793E-02 +1.205644E-03 +1.873002E-01 +7.141490E-03 +7.076637E+00 +1.017946E+01 +8.323139E-02 +1.408687E-03 +2.025710E-01 +8.344398E-03 +2.095897E+01 +8.825741E+01 +3.236513E-02 +2.104220E-04 +8.008525E-02 +1.288373E-03 +1.358006E+01 +3.689205E+01 +1.862562E-01 +6.941428E-03 +5.180621E-01 +5.370209E-02 +5.067386E+00 +5.141748E+00 +1.912704E+00 +7.328484E-01 +4.655140E+00 +4.340941E+00 +2.858992E+00 +1.637705E+00 +4.331050E-01 +3.759875E-02 +1.054091E+00 +2.227118E-01 +1.692974E+00 +5.796396E-01 +5.560487E-02 +6.246120E-04 +1.353311E-01 +3.699815E-03 +2.368737E+00 +1.143184E+00 +7.950085E-02 +1.286135E-03 +1.934892E-01 +7.618268E-03 +7.119767E+00 +1.030095E+01 +8.427627E-02 +1.442764E-03 +2.051141E-01 +8.546249E-03 +2.047651E+01 +8.426275E+01 +3.183786E-02 +2.037156E-04 +7.878057E-02 +1.247311E-03 +1.326415E+01 +3.519010E+01 +1.833688E-01 +6.726832E-03 +5.100312E-01 +5.204188E-02 diff --git a/tests/regression_tests/random_ray_k_eff_mesh/test.py b/tests/regression_tests/random_ray_k_eff_mesh/test.py new file mode 100644 index 00000000000..cffdaf8bb4c --- /dev/null +++ b/tests/regression_tests/random_ray_k_eff_mesh/test.py @@ -0,0 +1,36 @@ +import os + +import openmc +from openmc.examples import random_ray_lattice + +from tests.testing_harness import TolerantPyAPITestHarness + + +class MGXSTestHarness(TolerantPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + + +def test_random_ray_k_eff_mesh(): + model = random_ray_lattice() + + # The model already has some geometrical subdivisions + # up to a 10x10 grid in the moderator region. So, we + # increase the resolution 40x40 applied over the full + # 2x2 lattice. + pitch = 1.26 + dim = 40 + mesh = openmc.RegularMesh() + mesh.dimension = (dim, dim) + mesh.lower_left = (-pitch, -pitch) + mesh.upper_right = (pitch, pitch) + + root = model.geometry.root_universe + + model.settings.random_ray['source_region_meshes'] = [(mesh, [root])] + + harness = MGXSTestHarness('statepoint.10.h5', model) + harness.main() diff --git a/tests/regression_tests/weightwindows_fw_cadis_mesh/__init__.py b/tests/regression_tests/weightwindows_fw_cadis_mesh/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/inputs_true.dat b/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/inputs_true.dat new file mode 100644 index 00000000000..ffb2ac112da --- /dev/null +++ b/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/inputs_true.dat @@ -0,0 +1,265 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 750 + 30 + 20 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + + 1 + neutron + 10 + 1 + true + fw_cadis + + + + 15 15 15 + 0.0 0.0 0.0 + 30.0 30.0 30.0 + + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + + + + + + flat + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/results_true.dat b/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/results_true.dat new file mode 100644 index 00000000000..e14b7e19989 --- /dev/null +++ b/tests/regression_tests/weightwindows_fw_cadis_mesh/flat/results_true.dat @@ -0,0 +1,6760 @@ +RegularMesh + ID = 1 + Name = + Dimensions = 3 + Voxels = [15 15 15] + Lower left = [0. 0. 0.] + Upper Right = [np.float64(30.0), np.float64(30.0), np.float64(30.0)] + Width = [2. 2. 2.] +Lower Bounds +1.71e-01 +1.73e-01 +1.72e-01 +1.73e-01 +1.71e-01 +1.63e-01 +1.66e-01 +1.70e-01 +1.68e-01 +1.76e-01 +1.73e-01 +1.79e-01 +2.62e-01 +4.44e-01 +1.68e-01 +1.63e-01 +1.76e-01 +1.73e-01 +1.52e-01 +1.66e-01 +1.72e-01 +1.68e-01 +1.68e-01 +1.66e-01 +1.67e-01 +1.76e-01 +1.73e-01 +2.44e-01 +3.63e-01 +1.23e-01 +1.71e-01 +1.66e-01 +1.69e-01 +1.71e-01 +1.67e-01 +1.71e-01 +1.64e-01 +1.56e-01 +1.67e-01 +1.64e-01 +1.68e-01 +1.82e-01 +2.60e-01 +4.13e-01 +1.55e-01 +1.68e-01 +1.62e-01 +1.66e-01 +1.65e-01 +1.62e-01 +1.64e-01 +1.63e-01 +1.66e-01 +1.63e-01 +1.67e-01 +1.58e-01 +1.79e-01 +2.52e-01 +4.10e-01 +1.35e-01 +1.65e-01 +1.74e-01 +1.67e-01 +1.71e-01 +1.66e-01 +1.62e-01 +1.60e-01 +1.66e-01 +1.55e-01 +1.67e-01 +1.66e-01 +1.82e-01 +2.68e-01 +3.46e-01 +1.32e-01 +1.70e-01 +1.70e-01 +1.68e-01 +1.68e-01 +1.69e-01 +1.59e-01 +1.62e-01 +1.59e-01 +1.57e-01 +1.55e-01 +1.72e-01 +1.80e-01 +2.59e-01 +2.97e-01 +1.07e-01 +1.68e-01 +1.60e-01 +1.58e-01 +1.61e-01 +1.58e-01 +1.59e-01 +1.56e-01 +1.53e-01 +1.57e-01 +1.52e-01 +1.59e-01 +1.54e-01 +2.42e-01 +3.30e-01 +1.22e-01 +1.64e-01 +1.65e-01 +1.69e-01 +1.64e-01 +1.60e-01 +1.66e-01 +1.60e-01 +1.52e-01 +1.45e-01 +1.53e-01 +1.57e-01 +1.40e-01 +2.08e-01 +1.90e-01 +7.93e-02 +1.65e-01 +1.65e-01 +1.71e-01 +1.72e-01 +1.63e-01 +1.61e-01 +1.57e-01 +1.54e-01 +1.49e-01 +1.41e-01 +1.45e-01 +1.53e-01 +2.36e-01 +1.80e-01 +5.76e-02 +1.67e-01 +1.75e-01 +1.74e-01 +1.71e-01 +1.66e-01 +1.54e-01 +1.55e-01 +1.52e-01 +1.53e-01 +1.41e-01 +1.53e-01 +1.49e-01 +2.14e-01 +1.76e-01 +6.71e-02 +1.72e-01 +1.66e-01 +1.74e-01 +1.70e-01 +1.69e-01 +1.57e-01 +1.50e-01 +1.53e-01 +1.48e-01 +1.47e-01 +1.62e-01 +1.49e-01 +2.03e-01 +1.35e-01 +4.07e-02 +1.81e-01 +1.80e-01 +1.72e-01 +1.65e-01 +1.84e-01 +1.74e-01 +1.58e-01 +1.68e-01 +1.59e-01 +1.57e-01 +1.58e-01 +1.48e-01 +2.16e-01 +1.05e-01 +3.37e-02 +2.61e-01 +2.52e-01 +2.37e-01 +2.51e-01 +2.64e-01 +2.43e-01 +2.39e-01 +2.32e-01 +2.32e-01 +2.29e-01 +2.22e-01 +2.19e-01 +2.07e-01 +8.02e-02 +2.21e-02 +5.00e-01 +3.68e-01 +3.88e-01 +2.65e-01 +2.86e-01 +3.17e-01 +2.60e-01 +2.39e-01 +1.88e-01 +1.97e-01 +1.84e-01 +1.50e-01 +9.99e-02 +3.88e-02 +1.90e-02 +2.18e-01 +1.48e-01 +1.61e-01 +8.76e-02 +9.43e-02 +1.16e-01 +8.45e-02 +8.13e-02 +7.00e-02 +6.16e-02 +6.49e-02 +4.46e-02 +3.71e-02 +1.62e-02 +7.33e-03 +1.70e-01 +1.69e-01 +1.70e-01 +1.68e-01 +1.63e-01 +1.59e-01 +1.75e-01 +1.69e-01 +1.59e-01 +1.64e-01 +1.77e-01 +1.79e-01 +2.63e-01 +3.92e-01 +1.48e-01 +1.73e-01 +1.74e-01 +1.70e-01 +1.66e-01 +1.68e-01 +1.62e-01 +1.62e-01 +1.67e-01 +1.60e-01 +1.64e-01 +1.67e-01 +1.80e-01 +2.42e-01 +3.72e-01 +1.45e-01 +1.71e-01 +1.65e-01 +1.67e-01 +1.65e-01 +1.69e-01 +1.70e-01 +1.64e-01 +1.68e-01 +1.60e-01 +1.70e-01 +1.70e-01 +1.74e-01 +2.62e-01 +3.29e-01 +1.15e-01 +1.73e-01 +1.75e-01 +1.70e-01 +1.69e-01 +1.69e-01 +1.63e-01 +1.66e-01 +1.63e-01 +1.61e-01 +1.61e-01 +1.70e-01 +1.79e-01 +2.47e-01 +3.64e-01 +1.32e-01 +1.72e-01 +1.73e-01 +1.71e-01 +1.66e-01 +1.63e-01 +1.63e-01 +1.67e-01 +1.60e-01 +1.56e-01 +1.63e-01 +1.72e-01 +1.71e-01 +2.52e-01 +3.43e-01 +1.40e-01 +1.68e-01 +1.70e-01 +1.67e-01 +1.66e-01 +1.72e-01 +1.61e-01 +1.56e-01 +1.56e-01 +1.53e-01 +1.55e-01 +1.65e-01 +1.78e-01 +2.53e-01 +3.66e-01 +1.33e-01 +1.70e-01 +1.69e-01 +1.60e-01 +1.64e-01 +1.70e-01 +1.63e-01 +1.53e-01 +1.50e-01 +1.50e-01 +1.48e-01 +1.53e-01 +1.66e-01 +2.34e-01 +2.99e-01 +1.15e-01 +1.65e-01 +1.63e-01 +1.59e-01 +1.58e-01 +1.59e-01 +1.54e-01 +1.54e-01 +1.47e-01 +1.40e-01 +1.38e-01 +1.27e-01 +1.22e-01 +1.99e-01 +1.77e-01 +7.13e-02 +1.58e-01 +1.75e-01 +1.68e-01 +1.66e-01 +1.66e-01 +1.58e-01 +1.56e-01 +1.44e-01 +1.48e-01 +1.45e-01 +1.37e-01 +1.40e-01 +2.01e-01 +1.36e-01 +4.17e-02 +1.65e-01 +1.76e-01 +1.69e-01 +1.69e-01 +1.57e-01 +1.67e-01 +1.56e-01 +1.50e-01 +1.53e-01 +1.43e-01 +1.51e-01 +1.41e-01 +2.04e-01 +1.50e-01 +4.51e-02 +1.69e-01 +1.75e-01 +1.71e-01 +1.63e-01 +1.69e-01 +1.67e-01 +1.60e-01 +1.50e-01 +1.53e-01 +1.44e-01 +1.57e-01 +1.49e-01 +2.02e-01 +1.35e-01 +4.08e-02 +1.79e-01 +1.72e-01 +1.73e-01 +1.70e-01 +1.74e-01 +1.78e-01 +1.68e-01 +1.59e-01 +1.57e-01 +1.62e-01 +1.62e-01 +1.57e-01 +1.95e-01 +9.20e-02 +3.15e-02 +2.48e-01 +2.65e-01 +2.54e-01 +2.58e-01 +2.81e-01 +2.53e-01 +2.33e-01 +2.40e-01 +2.16e-01 +2.19e-01 +2.43e-01 +2.22e-01 +1.99e-01 +6.18e-02 +1.77e-02 +3.13e-01 +2.98e-01 +2.98e-01 +3.16e-01 +3.67e-01 +3.51e-01 +2.81e-01 +2.76e-01 +2.24e-01 +1.66e-01 +1.70e-01 +1.65e-01 +1.10e-01 +3.67e-02 +1.54e-02 +1.11e-01 +1.10e-01 +1.18e-01 +1.08e-01 +1.34e-01 +1.41e-01 +9.92e-02 +9.96e-02 +9.39e-02 +5.19e-02 +5.40e-02 +5.45e-02 +4.22e-02 +1.88e-02 +7.33e-03 +1.73e-01 +1.68e-01 +1.74e-01 +1.68e-01 +1.60e-01 +1.60e-01 +1.61e-01 +1.59e-01 +1.70e-01 +1.61e-01 +1.62e-01 +1.76e-01 +2.90e-01 +4.08e-01 +1.58e-01 +1.72e-01 +1.68e-01 +1.62e-01 +1.66e-01 +1.59e-01 +1.63e-01 +1.58e-01 +1.60e-01 +1.59e-01 +1.65e-01 +1.63e-01 +1.77e-01 +2.45e-01 +3.92e-01 +1.64e-01 +1.71e-01 +1.74e-01 +1.72e-01 +1.57e-01 +1.65e-01 +1.63e-01 +1.63e-01 +1.69e-01 +1.58e-01 +1.61e-01 +1.61e-01 +1.77e-01 +2.45e-01 +3.36e-01 +1.29e-01 +1.57e-01 +1.68e-01 +1.66e-01 +1.63e-01 +1.63e-01 +1.59e-01 +1.61e-01 +1.65e-01 +1.56e-01 +1.59e-01 +1.65e-01 +1.60e-01 +2.43e-01 +3.30e-01 +1.08e-01 +1.62e-01 +1.62e-01 +1.61e-01 +1.54e-01 +1.63e-01 +1.64e-01 +1.50e-01 +1.57e-01 +1.51e-01 +1.50e-01 +1.67e-01 +1.72e-01 +2.44e-01 +2.79e-01 +8.59e-02 +1.61e-01 +1.64e-01 +1.57e-01 +1.55e-01 +1.57e-01 +1.57e-01 +1.51e-01 +1.49e-01 +1.54e-01 +1.56e-01 +1.67e-01 +1.70e-01 +2.42e-01 +3.48e-01 +1.32e-01 +1.67e-01 +1.73e-01 +1.64e-01 +1.62e-01 +1.60e-01 +1.61e-01 +1.56e-01 +1.47e-01 +1.51e-01 +1.54e-01 +1.57e-01 +1.59e-01 +2.26e-01 +2.53e-01 +9.32e-02 +1.63e-01 +1.63e-01 +1.60e-01 +1.55e-01 +1.57e-01 +1.58e-01 +1.54e-01 +1.47e-01 +1.46e-01 +1.48e-01 +1.43e-01 +1.50e-01 +2.09e-01 +1.68e-01 +6.28e-02 +1.57e-01 +1.64e-01 +1.64e-01 +1.60e-01 +1.60e-01 +1.58e-01 +1.58e-01 +1.57e-01 +1.47e-01 +1.50e-01 +1.52e-01 +1.43e-01 +2.11e-01 +1.66e-01 +6.17e-02 +1.65e-01 +1.72e-01 +1.62e-01 +1.64e-01 +1.66e-01 +1.57e-01 +1.57e-01 +1.50e-01 +1.40e-01 +1.48e-01 +1.46e-01 +1.44e-01 +1.95e-01 +1.25e-01 +4.98e-02 +1.61e-01 +1.66e-01 +1.70e-01 +1.64e-01 +1.67e-01 +1.67e-01 +1.68e-01 +1.58e-01 +1.43e-01 +1.45e-01 +1.46e-01 +1.43e-01 +1.94e-01 +1.25e-01 +3.60e-02 +1.61e-01 +1.74e-01 +1.74e-01 +1.76e-01 +1.73e-01 +1.72e-01 +1.68e-01 +1.61e-01 +1.59e-01 +1.56e-01 +1.59e-01 +1.57e-01 +1.77e-01 +1.09e-01 +3.74e-02 +2.48e-01 +2.48e-01 +2.49e-01 +2.42e-01 +2.47e-01 +2.55e-01 +2.19e-01 +2.25e-01 +2.25e-01 +2.17e-01 +2.21e-01 +2.26e-01 +2.15e-01 +6.24e-02 +1.90e-02 +3.68e-01 +2.72e-01 +3.41e-01 +3.58e-01 +3.64e-01 +2.32e-01 +2.34e-01 +2.20e-01 +1.88e-01 +1.78e-01 +1.34e-01 +1.25e-01 +1.07e-01 +4.68e-02 +1.29e-02 +1.29e-01 +8.63e-02 +1.42e-01 +1.36e-01 +1.37e-01 +8.94e-02 +8.09e-02 +8.37e-02 +5.69e-02 +6.46e-02 +3.15e-02 +3.86e-02 +3.51e-02 +2.57e-02 +1.29e-02 +1.69e-01 +1.62e-01 +1.66e-01 +1.70e-01 +1.69e-01 +1.61e-01 +1.58e-01 +1.51e-01 +1.52e-01 +1.53e-01 +1.52e-01 +1.73e-01 +2.54e-01 +4.12e-01 +1.51e-01 +1.68e-01 +1.75e-01 +1.65e-01 +1.59e-01 +1.62e-01 +1.56e-01 +1.57e-01 +1.55e-01 +1.59e-01 +1.66e-01 +1.59e-01 +1.67e-01 +2.35e-01 +3.25e-01 +1.29e-01 +1.62e-01 +1.64e-01 +1.68e-01 +1.59e-01 +1.51e-01 +1.59e-01 +1.53e-01 +1.53e-01 +1.55e-01 +1.63e-01 +1.60e-01 +1.65e-01 +2.33e-01 +2.78e-01 +1.07e-01 +1.62e-01 +1.63e-01 +1.60e-01 +1.61e-01 +1.54e-01 +1.57e-01 +1.59e-01 +1.51e-01 +1.51e-01 +1.58e-01 +1.54e-01 +1.61e-01 +2.39e-01 +2.48e-01 +1.01e-01 +1.55e-01 +1.54e-01 +1.61e-01 +1.66e-01 +1.55e-01 +1.53e-01 +1.56e-01 +1.51e-01 +1.48e-01 +1.53e-01 +1.63e-01 +1.62e-01 +2.42e-01 +3.21e-01 +1.18e-01 +1.66e-01 +1.52e-01 +1.62e-01 +1.62e-01 +1.56e-01 +1.52e-01 +1.55e-01 +1.57e-01 +1.55e-01 +1.53e-01 +1.53e-01 +1.64e-01 +2.46e-01 +3.03e-01 +1.11e-01 +1.63e-01 +1.59e-01 +1.68e-01 +1.56e-01 +1.52e-01 +1.55e-01 +1.56e-01 +1.49e-01 +1.49e-01 +1.51e-01 +1.45e-01 +1.49e-01 +2.22e-01 +2.88e-01 +1.02e-01 +1.66e-01 +1.62e-01 +1.61e-01 +1.50e-01 +1.50e-01 +1.51e-01 +1.59e-01 +1.46e-01 +1.43e-01 +1.42e-01 +1.41e-01 +1.51e-01 +2.22e-01 +1.88e-01 +6.51e-02 +1.60e-01 +1.59e-01 +1.57e-01 +1.61e-01 +1.55e-01 +1.57e-01 +1.53e-01 +1.53e-01 +1.45e-01 +1.44e-01 +1.47e-01 +1.59e-01 +2.35e-01 +1.91e-01 +5.69e-02 +1.62e-01 +1.59e-01 +1.63e-01 +1.59e-01 +1.55e-01 +1.54e-01 +1.50e-01 +1.47e-01 +1.50e-01 +1.46e-01 +1.43e-01 +1.43e-01 +2.21e-01 +1.37e-01 +5.36e-02 +1.66e-01 +1.65e-01 +1.50e-01 +1.68e-01 +1.65e-01 +1.61e-01 +1.55e-01 +1.49e-01 +1.47e-01 +1.51e-01 +1.43e-01 +1.50e-01 +1.92e-01 +9.97e-02 +2.93e-02 +1.73e-01 +1.65e-01 +1.70e-01 +7.83e-02 +1.73e-01 +1.78e-01 +1.65e-01 +1.50e-01 +1.60e-01 +1.55e-01 +1.57e-01 +1.55e-01 +1.89e-01 +8.91e-02 +2.68e-02 +2.32e-01 +2.36e-01 +2.40e-01 +2.31e-01 +2.41e-01 +2.39e-01 +2.01e-01 +2.12e-01 +2.21e-01 +2.12e-01 +2.04e-01 +2.02e-01 +2.14e-01 +8.26e-02 +2.65e-02 +3.07e-01 +2.51e-01 +2.10e-01 +2.95e-01 +2.86e-01 +2.41e-01 +1.69e-01 +1.21e-01 +1.92e-01 +1.82e-01 +1.29e-01 +1.24e-01 +1.04e-01 +4.53e-02 +1.74e-02 +1.16e-01 +8.31e-02 +8.94e-02 +9.43e-02 +1.16e-01 +9.01e-02 +7.46e-02 +3.83e-02 +6.35e-02 +6.43e-02 +4.23e-02 +4.16e-02 +3.17e-02 +2.60e-02 +1.26e-02 +1.63e-01 +1.58e-01 +1.60e-01 +1.62e-01 +1.60e-01 +1.59e-01 +1.60e-01 +1.51e-01 +1.47e-01 +1.55e-01 +1.51e-01 +1.68e-01 +2.47e-01 +3.18e-01 +1.27e-01 +1.64e-01 +1.61e-01 +1.56e-01 +1.63e-01 +1.63e-01 +1.54e-01 +1.50e-01 +1.49e-01 +1.56e-01 +1.58e-01 +1.58e-01 +1.64e-01 +2.39e-01 +3.04e-01 +1.13e-01 +1.65e-01 +1.63e-01 +1.62e-01 +1.57e-01 +1.57e-01 +1.57e-01 +1.53e-01 +1.49e-01 +1.57e-01 +1.63e-01 +1.63e-01 +1.70e-01 +2.39e-01 +2.29e-01 +8.46e-02 +1.62e-01 +1.63e-01 +1.58e-01 +1.58e-01 +1.56e-01 +1.55e-01 +1.50e-01 +1.51e-01 +1.52e-01 +1.60e-01 +1.57e-01 +1.64e-01 +2.46e-01 +2.16e-01 +7.36e-02 +1.56e-01 +1.54e-01 +1.56e-01 +1.62e-01 +1.52e-01 +1.50e-01 +1.55e-01 +1.52e-01 +1.52e-01 +1.50e-01 +1.58e-01 +1.67e-01 +2.47e-01 +3.21e-01 +1.15e-01 +1.57e-01 +1.55e-01 +1.56e-01 +1.58e-01 +1.48e-01 +1.43e-01 +1.42e-01 +1.56e-01 +1.49e-01 +1.52e-01 +1.51e-01 +1.58e-01 +2.31e-01 +2.63e-01 +9.53e-02 +1.48e-01 +1.56e-01 +1.59e-01 +1.52e-01 +1.60e-01 +1.43e-01 +1.43e-01 +1.44e-01 +1.45e-01 +1.43e-01 +1.48e-01 +1.51e-01 +2.09e-01 +2.10e-01 +7.84e-02 +1.57e-01 +1.60e-01 +1.55e-01 +1.53e-01 +1.57e-01 +1.44e-01 +1.44e-01 +1.36e-01 +1.36e-01 +1.46e-01 +1.43e-01 +1.46e-01 +2.17e-01 +1.67e-01 +7.08e-02 +1.59e-01 +1.56e-01 +1.56e-01 +1.54e-01 +1.53e-01 +1.52e-01 +1.50e-01 +1.42e-01 +1.37e-01 +1.39e-01 +1.41e-01 +1.47e-01 +2.03e-01 +1.75e-01 +5.51e-02 +1.63e-01 +1.62e-01 +1.57e-01 +1.53e-01 +1.45e-01 +1.52e-01 +1.52e-01 +1.43e-01 +1.41e-01 +1.38e-01 +1.39e-01 +1.52e-01 +2.05e-01 +1.61e-01 +6.97e-02 +1.62e-01 +1.56e-01 +1.58e-01 +1.55e-01 +1.58e-01 +1.56e-01 +1.53e-01 +1.53e-01 +1.49e-01 +1.41e-01 +1.44e-01 +1.57e-01 +2.11e-01 +1.22e-01 +4.27e-02 +1.66e-01 +1.69e-01 +1.61e-01 +1.62e-01 +1.68e-01 +1.74e-01 +1.60e-01 +1.57e-01 +1.57e-01 +1.62e-01 +1.48e-01 +1.54e-01 +2.17e-01 +1.14e-01 +3.49e-02 +2.43e-01 +2.30e-01 +2.33e-01 +2.43e-01 +2.46e-01 +2.48e-01 +2.46e-01 +2.21e-01 +2.22e-01 +2.12e-01 +2.00e-01 +2.02e-01 +2.14e-01 +8.94e-02 +3.01e-02 +2.79e-01 +2.63e-01 +2.52e-01 +2.63e-01 +3.00e-01 +2.68e-01 +2.93e-01 +1.90e-01 +2.03e-01 +1.54e-01 +1.13e-01 +1.27e-01 +8.82e-02 +3.87e-02 +1.55e-02 +1.03e-01 +9.09e-02 +9.09e-02 +8.99e-02 +1.07e-01 +8.75e-02 +1.17e-01 +5.68e-02 +7.16e-02 +5.22e-02 +3.31e-02 +3.49e-02 +2.69e-02 +1.66e-02 +1.14e-02 +1.46e-01 +1.55e-01 +1.57e-01 +1.59e-01 +1.63e-01 +1.54e-01 +1.49e-01 +1.48e-01 +1.45e-01 +1.48e-01 +1.48e-01 +1.54e-01 +2.45e-01 +2.69e-01 +8.34e-02 +1.54e-01 +1.59e-01 +1.57e-01 +1.51e-01 +1.55e-01 +1.57e-01 +1.60e-01 +1.49e-01 +1.54e-01 +1.50e-01 +1.54e-01 +1.62e-01 +2.41e-01 +2.97e-01 +1.15e-01 +1.60e-01 +1.61e-01 +1.61e-01 +1.53e-01 +1.56e-01 +1.52e-01 +1.53e-01 +1.57e-01 +1.52e-01 +1.52e-01 +1.55e-01 +1.69e-01 +2.49e-01 +2.52e-01 +8.15e-02 +1.59e-01 +1.59e-01 +1.52e-01 +1.50e-01 +1.53e-01 +1.59e-01 +1.47e-01 +1.51e-01 +1.51e-01 +1.56e-01 +1.56e-01 +1.64e-01 +2.40e-01 +2.46e-01 +8.06e-02 +1.58e-01 +1.59e-01 +1.58e-01 +1.56e-01 +1.53e-01 +1.46e-01 +1.48e-01 +1.48e-01 +1.47e-01 +1.53e-01 +1.48e-01 +1.62e-01 +2.39e-01 +2.66e-01 +1.02e-01 +1.55e-01 +1.54e-01 +1.57e-01 +1.52e-01 +1.48e-01 +1.50e-01 +1.40e-01 +1.37e-01 +1.50e-01 +1.50e-01 +1.46e-01 +1.52e-01 +2.23e-01 +2.22e-01 +8.99e-02 +1.58e-01 +1.60e-01 +1.54e-01 +1.47e-01 +1.47e-01 +1.45e-01 +1.42e-01 +1.28e-01 +1.33e-01 +1.40e-01 +1.51e-01 +1.53e-01 +2.08e-01 +1.44e-01 +5.19e-02 +1.57e-01 +1.54e-01 +1.54e-01 +1.42e-01 +1.51e-01 +1.44e-01 +1.35e-01 +1.28e-01 +1.11e-01 +1.40e-01 +1.49e-01 +1.55e-01 +2.22e-01 +1.31e-01 +4.13e-02 +1.56e-01 +1.53e-01 +1.57e-01 +1.42e-01 +1.52e-01 +1.49e-01 +1.45e-01 +1.32e-01 +1.30e-01 +1.27e-01 +1.41e-01 +1.41e-01 +2.11e-01 +1.55e-01 +4.43e-02 +1.49e-01 +1.52e-01 +1.53e-01 +1.56e-01 +1.47e-01 +1.45e-01 +1.40e-01 +1.37e-01 +1.43e-01 +1.35e-01 +1.34e-01 +1.42e-01 +1.98e-01 +1.25e-01 +3.99e-02 +1.57e-01 +1.48e-01 +1.49e-01 +1.52e-01 +1.50e-01 +1.48e-01 +1.50e-01 +1.48e-01 +1.44e-01 +1.43e-01 +1.35e-01 +1.39e-01 +1.94e-01 +1.07e-01 +3.11e-02 +1.62e-01 +1.61e-01 +1.61e-01 +1.63e-01 +1.63e-01 +1.60e-01 +1.55e-01 +1.55e-01 +1.50e-01 +1.56e-01 +1.43e-01 +1.32e-01 +1.83e-01 +1.02e-01 +2.51e-02 +2.34e-01 +2.25e-01 +2.20e-01 +2.27e-01 +2.41e-01 +2.35e-01 +2.13e-01 +2.04e-01 +2.13e-01 +2.16e-01 +2.20e-01 +1.80e-01 +1.76e-01 +9.68e-02 +3.54e-02 +3.12e-01 +2.64e-01 +2.26e-01 +2.68e-01 +2.60e-01 +2.43e-01 +2.35e-01 +1.81e-01 +1.60e-01 +1.47e-01 +1.51e-01 +1.34e-01 +7.35e-02 +4.05e-02 +2.19e-02 +1.06e-01 +9.90e-02 +8.77e-02 +9.61e-02 +9.91e-02 +7.56e-02 +1.05e-01 +6.20e-02 +5.67e-02 +3.97e-02 +4.31e-02 +4.78e-02 +2.87e-02 +1.59e-02 +8.92e-03 +1.61e-01 +1.59e-01 +1.57e-01 +1.59e-01 +1.51e-01 +1.49e-01 +1.54e-01 +1.51e-01 +1.53e-01 +1.49e-01 +1.52e-01 +1.48e-01 +2.27e-01 +1.82e-01 +8.22e-02 +1.61e-01 +1.54e-01 +1.57e-01 +1.56e-01 +1.53e-01 +1.49e-01 +1.46e-01 +1.41e-01 +1.51e-01 +1.51e-01 +1.52e-01 +1.52e-01 +2.22e-01 +2.08e-01 +7.81e-02 +1.54e-01 +1.58e-01 +1.52e-01 +1.53e-01 +1.48e-01 +1.49e-01 +1.45e-01 +1.43e-01 +1.44e-01 +1.48e-01 +1.58e-01 +1.55e-01 +2.25e-01 +2.48e-01 +1.01e-01 +1.57e-01 +1.48e-01 +1.51e-01 +1.47e-01 +1.46e-01 +1.52e-01 +1.48e-01 +1.47e-01 +1.46e-01 +1.48e-01 +1.49e-01 +1.57e-01 +2.21e-01 +1.69e-01 +6.52e-02 +1.57e-01 +1.55e-01 +1.55e-01 +1.53e-01 +1.51e-01 +1.55e-01 +1.46e-01 +1.49e-01 +1.42e-01 +1.45e-01 +1.45e-01 +1.57e-01 +2.19e-01 +1.36e-01 +4.96e-02 +1.57e-01 +1.50e-01 +1.53e-01 +1.57e-01 +1.54e-01 +1.50e-01 +1.45e-01 +1.40e-01 +1.30e-01 +1.38e-01 +1.42e-01 +1.51e-01 +2.13e-01 +1.41e-01 +4.31e-02 +1.56e-01 +1.57e-01 +1.53e-01 +1.52e-01 +1.51e-01 +1.44e-01 +1.42e-01 +1.28e-01 +1.21e-01 +1.26e-01 +1.40e-01 +1.53e-01 +2.16e-01 +1.61e-01 +4.81e-02 +1.48e-01 +1.48e-01 +1.47e-01 +1.49e-01 +1.49e-01 +1.43e-01 +1.40e-01 +1.23e-01 +8.97e-02 +1.34e-01 +1.43e-01 +1.48e-01 +2.10e-01 +1.68e-01 +5.02e-02 +1.53e-01 +7.09e-02 +1.47e-01 +1.48e-01 +1.43e-01 +1.46e-01 +6.84e-02 +1.27e-01 +1.24e-01 +1.31e-01 +1.42e-01 +1.48e-01 +2.01e-01 +1.56e-01 +5.15e-02 +1.50e-01 +1.52e-01 +1.52e-01 +1.47e-01 +1.42e-01 +1.35e-01 +1.39e-01 +1.38e-01 +1.37e-01 +1.35e-01 +1.36e-01 +1.36e-01 +1.92e-01 +1.22e-01 +4.76e-02 +1.52e-01 +1.61e-01 +1.49e-01 +1.45e-01 +1.40e-01 +1.40e-01 +1.34e-01 +1.35e-01 +1.38e-01 +1.38e-01 +1.27e-01 +1.35e-01 +1.77e-01 +7.50e-02 +2.64e-02 +1.58e-01 +1.56e-01 +1.50e-01 +1.45e-01 +1.50e-01 +1.50e-01 +1.42e-01 +1.42e-01 +1.52e-01 +1.46e-01 +1.38e-01 +1.30e-01 +1.44e-01 +6.83e-02 +2.86e-02 +2.28e-01 +2.11e-01 +2.08e-01 +2.12e-01 +2.17e-01 +2.09e-01 +2.06e-01 +1.89e-01 +2.08e-01 +1.88e-01 +1.88e-01 +1.66e-01 +1.33e-01 +4.13e-02 +1.70e-02 +2.67e-01 +2.70e-01 +2.31e-01 +1.72e-01 +1.66e-01 +2.16e-01 +1.96e-01 +1.52e-01 +1.41e-01 +1.31e-01 +1.44e-01 +1.17e-01 +4.82e-02 +2.09e-02 +8.63e-03 +9.47e-02 +1.04e-01 +9.02e-02 +5.55e-02 +5.39e-02 +6.95e-02 +7.10e-02 +4.92e-02 +4.74e-02 +3.52e-02 +4.69e-02 +4.03e-02 +1.69e-02 +9.25e-03 +5.98e-03 +1.46e-01 +1.50e-01 +1.48e-01 +1.53e-01 +1.56e-01 +1.57e-01 +1.56e-01 +1.54e-01 +1.44e-01 +1.42e-01 +1.44e-01 +1.52e-01 +2.10e-01 +1.56e-01 +5.53e-02 +1.50e-01 +1.57e-01 +1.57e-01 +1.57e-01 +1.54e-01 +1.51e-01 +1.52e-01 +1.42e-01 +1.42e-01 +1.39e-01 +1.45e-01 +1.50e-01 +2.11e-01 +1.18e-01 +3.92e-02 +1.49e-01 +1.48e-01 +1.55e-01 +1.54e-01 +1.51e-01 +1.50e-01 +1.48e-01 +1.48e-01 +1.41e-01 +1.38e-01 +1.45e-01 +1.54e-01 +2.08e-01 +1.62e-01 +5.89e-02 +1.48e-01 +1.49e-01 +1.51e-01 +1.50e-01 +1.51e-01 +1.49e-01 +1.48e-01 +1.47e-01 +1.34e-01 +1.33e-01 +1.53e-01 +1.54e-01 +1.96e-01 +1.55e-01 +6.15e-02 +1.51e-01 +1.53e-01 +1.59e-01 +1.48e-01 +1.51e-01 +1.48e-01 +1.45e-01 +1.41e-01 +1.38e-01 +1.37e-01 +1.49e-01 +1.46e-01 +2.06e-01 +1.25e-01 +3.47e-02 +1.51e-01 +1.47e-01 +1.52e-01 +1.53e-01 +1.44e-01 +1.40e-01 +1.45e-01 +1.38e-01 +1.39e-01 +1.34e-01 +1.36e-01 +1.50e-01 +1.96e-01 +1.29e-01 +4.48e-02 +1.49e-01 +1.43e-01 +1.49e-01 +1.46e-01 +1.48e-01 +1.42e-01 +1.35e-01 +1.27e-01 +1.24e-01 +1.26e-01 +1.29e-01 +1.38e-01 +1.87e-01 +1.53e-01 +5.84e-02 +1.46e-01 +1.51e-01 +1.44e-01 +1.45e-01 +1.45e-01 +1.41e-01 +1.31e-01 +1.24e-01 +1.29e-01 +1.30e-01 +1.43e-01 +1.42e-01 +1.91e-01 +1.36e-01 +5.08e-02 +1.39e-01 +1.40e-01 +1.43e-01 +1.39e-01 +1.43e-01 +1.39e-01 +1.35e-01 +1.27e-01 +1.23e-01 +1.22e-01 +1.41e-01 +1.37e-01 +1.86e-01 +1.09e-01 +3.41e-02 +1.44e-01 +1.41e-01 +1.44e-01 +1.37e-01 +1.37e-01 +1.39e-01 +1.29e-01 +1.33e-01 +1.20e-01 +1.18e-01 +1.29e-01 +1.36e-01 +1.94e-01 +1.23e-01 +3.96e-02 +1.50e-01 +1.47e-01 +1.48e-01 +1.39e-01 +1.35e-01 +1.36e-01 +1.34e-01 +1.40e-01 +1.32e-01 +1.27e-01 +1.23e-01 +1.26e-01 +1.61e-01 +8.34e-02 +2.18e-02 +1.58e-01 +1.51e-01 +1.50e-01 +1.46e-01 +1.45e-01 +1.35e-01 +1.30e-01 +1.41e-01 +1.39e-01 +1.36e-01 +1.32e-01 +1.22e-01 +1.45e-01 +5.48e-02 +1.56e-02 +2.28e-01 +2.05e-01 +2.09e-01 +1.89e-01 +2.03e-01 +1.97e-01 +1.63e-01 +1.67e-01 +1.68e-01 +1.92e-01 +1.83e-01 +1.55e-01 +1.14e-01 +3.43e-02 +1.15e-02 +2.24e-01 +2.31e-01 +1.67e-01 +1.43e-01 +1.53e-01 +1.76e-01 +1.13e-01 +9.77e-02 +8.62e-02 +1.49e-01 +1.29e-01 +8.35e-02 +4.44e-02 +1.21e-02 +4.84e-03 +7.75e-02 +7.83e-02 +5.37e-02 +5.23e-02 +4.63e-02 +6.06e-02 +4.19e-02 +3.16e-02 +2.72e-02 +5.09e-02 +4.06e-02 +2.76e-02 +1.94e-02 +8.81e-03 +2.34e-03 +1.54e-01 +1.58e-01 +1.62e-01 +1.48e-01 +1.57e-01 +1.52e-01 +1.49e-01 +1.54e-01 +1.42e-01 +1.36e-01 +1.36e-01 +1.50e-01 +2.08e-01 +1.23e-01 +3.56e-02 +1.51e-01 +1.53e-01 +1.59e-01 +1.57e-01 +1.46e-01 +1.55e-01 +1.50e-01 +1.49e-01 +1.41e-01 +1.41e-01 +1.42e-01 +1.48e-01 +2.00e-01 +1.32e-01 +4.13e-02 +1.48e-01 +1.50e-01 +1.53e-01 +1.52e-01 +1.54e-01 +1.42e-01 +1.48e-01 +1.50e-01 +1.40e-01 +1.37e-01 +1.38e-01 +1.53e-01 +2.20e-01 +1.65e-01 +5.51e-02 +1.52e-01 +1.44e-01 +1.49e-01 +1.46e-01 +1.48e-01 +1.48e-01 +1.45e-01 +1.46e-01 +1.38e-01 +1.40e-01 +1.43e-01 +1.39e-01 +1.95e-01 +1.78e-01 +7.38e-02 +1.48e-01 +1.45e-01 +1.44e-01 +1.47e-01 +1.46e-01 +1.44e-01 +1.42e-01 +1.45e-01 +1.43e-01 +1.37e-01 +1.43e-01 +1.49e-01 +1.94e-01 +1.37e-01 +4.11e-02 +1.52e-01 +1.48e-01 +1.45e-01 +1.38e-01 +1.38e-01 +1.40e-01 +1.41e-01 +1.32e-01 +1.36e-01 +1.36e-01 +1.37e-01 +1.48e-01 +2.02e-01 +1.53e-01 +5.25e-02 +1.45e-01 +1.41e-01 +1.44e-01 +1.36e-01 +1.36e-01 +1.36e-01 +1.33e-01 +1.32e-01 +1.26e-01 +1.32e-01 +1.37e-01 +1.38e-01 +1.71e-01 +1.19e-01 +4.26e-02 +1.40e-01 +1.43e-01 +1.46e-01 +1.39e-01 +1.38e-01 +1.34e-01 +1.29e-01 +1.26e-01 +1.28e-01 +1.28e-01 +1.34e-01 +1.39e-01 +1.70e-01 +7.71e-02 +3.01e-02 +1.38e-01 +1.12e-01 +1.38e-01 +1.41e-01 +1.44e-01 +1.46e-01 +1.34e-01 +1.11e-01 +1.24e-01 +1.20e-01 +1.22e-01 +1.33e-01 +1.84e-01 +8.77e-02 +2.26e-02 +1.45e-01 +1.37e-01 +1.37e-01 +1.29e-01 +1.34e-01 +1.37e-01 +1.34e-01 +1.29e-01 +1.23e-01 +1.19e-01 +1.21e-01 +1.25e-01 +1.84e-01 +9.19e-02 +2.31e-02 +1.48e-01 +1.32e-01 +1.36e-01 +1.34e-01 +1.41e-01 +1.34e-01 +1.32e-01 +1.27e-01 +1.27e-01 +1.20e-01 +1.10e-01 +1.17e-01 +1.65e-01 +9.68e-02 +2.83e-02 +1.48e-01 +1.35e-01 +1.35e-01 +1.43e-01 +1.44e-01 +1.40e-01 +1.43e-01 +1.35e-01 +1.34e-01 +1.29e-01 +1.17e-01 +9.99e-02 +9.90e-02 +5.49e-02 +1.66e-02 +2.22e-01 +2.00e-01 +2.03e-01 +2.10e-01 +2.09e-01 +1.99e-01 +1.86e-01 +1.93e-01 +1.79e-01 +1.90e-01 +1.70e-01 +1.45e-01 +1.22e-01 +4.35e-02 +1.22e-02 +2.30e-01 +1.84e-01 +1.79e-01 +1.67e-01 +1.72e-01 +1.61e-01 +1.13e-01 +1.16e-01 +1.06e-01 +1.13e-01 +1.20e-01 +8.32e-02 +4.37e-02 +1.70e-02 +5.87e-03 +7.01e-02 +5.48e-02 +4.65e-02 +4.76e-02 +5.00e-02 +5.48e-02 +3.77e-02 +3.45e-02 +3.12e-02 +3.14e-02 +3.88e-02 +2.77e-02 +1.27e-02 +6.38e-03 +1.50e-03 +1.48e-01 +1.54e-01 +1.58e-01 +1.56e-01 +1.55e-01 +1.46e-01 +1.44e-01 +1.48e-01 +1.42e-01 +1.44e-01 +1.40e-01 +1.54e-01 +1.94e-01 +1.38e-01 +5.04e-02 +1.51e-01 +1.53e-01 +1.49e-01 +1.49e-01 +1.49e-01 +1.53e-01 +1.51e-01 +1.50e-01 +1.45e-01 +1.36e-01 +1.42e-01 +1.57e-01 +2.09e-01 +1.55e-01 +4.56e-02 +1.55e-01 +1.48e-01 +1.43e-01 +1.44e-01 +1.46e-01 +1.54e-01 +1.50e-01 +1.51e-01 +1.54e-01 +1.38e-01 +1.36e-01 +1.52e-01 +2.10e-01 +1.66e-01 +5.44e-02 +1.48e-01 +1.47e-01 +1.46e-01 +1.43e-01 +1.49e-01 +1.48e-01 +1.51e-01 +1.49e-01 +1.42e-01 +1.38e-01 +1.39e-01 +1.49e-01 +1.94e-01 +1.42e-01 +5.42e-02 +1.43e-01 +1.39e-01 +1.46e-01 +1.48e-01 +1.48e-01 +1.42e-01 +1.41e-01 +1.46e-01 +1.40e-01 +1.38e-01 +1.43e-01 +1.43e-01 +1.95e-01 +1.20e-01 +3.81e-02 +1.49e-01 +1.42e-01 +1.47e-01 +1.42e-01 +1.43e-01 +1.43e-01 +1.42e-01 +1.41e-01 +1.36e-01 +1.33e-01 +1.38e-01 +1.42e-01 +1.87e-01 +1.10e-01 +4.00e-02 +1.46e-01 +1.44e-01 +1.42e-01 +1.42e-01 +1.42e-01 +1.43e-01 +1.36e-01 +1.37e-01 +1.32e-01 +1.37e-01 +1.30e-01 +1.42e-01 +1.97e-01 +1.41e-01 +4.74e-02 +1.42e-01 +1.46e-01 +1.45e-01 +1.44e-01 +1.40e-01 +1.37e-01 +1.29e-01 +1.32e-01 +1.28e-01 +1.22e-01 +1.29e-01 +1.44e-01 +1.79e-01 +8.11e-02 +2.49e-02 +1.36e-01 +1.40e-01 +1.42e-01 +1.45e-01 +1.42e-01 +1.35e-01 +1.28e-01 +1.25e-01 +1.21e-01 +1.20e-01 +1.24e-01 +1.37e-01 +1.76e-01 +8.67e-02 +2.55e-02 +1.41e-01 +1.39e-01 +1.39e-01 +1.42e-01 +1.37e-01 +1.33e-01 +1.30e-01 +1.23e-01 +1.22e-01 +1.24e-01 +1.16e-01 +1.28e-01 +1.68e-01 +1.03e-01 +3.30e-02 +1.41e-01 +1.35e-01 +1.34e-01 +1.33e-01 +1.33e-01 +1.34e-01 +1.37e-01 +1.31e-01 +1.21e-01 +1.17e-01 +1.11e-01 +1.13e-01 +1.57e-01 +8.80e-02 +2.97e-02 +1.53e-01 +1.39e-01 +1.45e-01 +1.45e-01 +1.38e-01 +1.49e-01 +1.41e-01 +1.36e-01 +1.24e-01 +1.23e-01 +1.14e-01 +9.70e-02 +1.01e-01 +5.74e-02 +1.83e-02 +2.39e-01 +1.91e-01 +1.79e-01 +2.07e-01 +1.97e-01 +2.03e-01 +1.92e-01 +2.07e-01 +1.99e-01 +2.04e-01 +1.76e-01 +1.35e-01 +1.07e-01 +3.67e-02 +1.30e-02 +1.91e-01 +1.54e-01 +1.49e-01 +1.68e-01 +1.82e-01 +1.72e-01 +1.48e-01 +1.49e-01 +1.30e-01 +1.06e-01 +9.60e-02 +7.07e-02 +4.00e-02 +1.71e-02 +7.45e-03 +7.31e-02 +6.14e-02 +6.36e-02 +5.75e-02 +6.28e-02 +5.80e-02 +4.73e-02 +4.31e-02 +4.49e-02 +3.41e-02 +3.27e-02 +2.30e-02 +1.26e-02 +7.67e-03 +3.56e-03 +1.42e-01 +1.47e-01 +1.56e-01 +1.59e-01 +1.58e-01 +1.56e-01 +1.58e-01 +1.63e-01 +1.48e-01 +1.51e-01 +1.49e-01 +1.44e-01 +1.75e-01 +9.73e-02 +3.77e-02 +1.45e-01 +1.50e-01 +1.49e-01 +1.50e-01 +1.53e-01 +1.52e-01 +1.51e-01 +1.54e-01 +1.52e-01 +1.51e-01 +1.48e-01 +1.46e-01 +1.93e-01 +1.30e-01 +4.16e-02 +1.46e-01 +1.48e-01 +1.46e-01 +1.53e-01 +1.47e-01 +1.54e-01 +1.58e-01 +1.45e-01 +1.47e-01 +1.46e-01 +1.45e-01 +1.50e-01 +1.99e-01 +1.42e-01 +4.85e-02 +1.46e-01 +1.48e-01 +1.56e-01 +1.44e-01 +1.54e-01 +1.56e-01 +1.52e-01 +1.46e-01 +1.44e-01 +1.37e-01 +1.38e-01 +1.46e-01 +1.94e-01 +1.11e-01 +3.61e-02 +1.45e-01 +1.48e-01 +1.51e-01 +1.46e-01 +1.54e-01 +1.54e-01 +1.45e-01 +1.44e-01 +1.48e-01 +1.40e-01 +1.46e-01 +1.49e-01 +1.93e-01 +1.06e-01 +3.16e-02 +1.45e-01 +1.48e-01 +1.50e-01 +1.56e-01 +1.46e-01 +1.51e-01 +1.44e-01 +1.41e-01 +1.32e-01 +1.41e-01 +1.41e-01 +1.40e-01 +2.04e-01 +1.15e-01 +3.10e-02 +1.41e-01 +1.46e-01 +1.41e-01 +1.48e-01 +1.45e-01 +1.45e-01 +1.44e-01 +1.38e-01 +1.34e-01 +1.34e-01 +1.38e-01 +1.42e-01 +1.85e-01 +1.26e-01 +3.53e-02 +1.38e-01 +1.45e-01 +1.45e-01 +1.49e-01 +1.41e-01 +1.40e-01 +1.33e-01 +1.34e-01 +1.33e-01 +1.36e-01 +1.35e-01 +1.37e-01 +1.76e-01 +1.16e-01 +4.10e-02 +1.35e-01 +1.39e-01 +1.44e-01 +1.43e-01 +1.44e-01 +1.40e-01 +1.27e-01 +1.29e-01 +1.24e-01 +1.24e-01 +1.21e-01 +1.25e-01 +1.74e-01 +9.17e-02 +2.60e-02 +1.39e-01 +1.35e-01 +1.40e-01 +1.43e-01 +1.45e-01 +1.32e-01 +1.32e-01 +1.29e-01 +1.20e-01 +1.22e-01 +1.14e-01 +1.15e-01 +1.43e-01 +8.26e-02 +2.82e-02 +1.37e-01 +1.36e-01 +1.37e-01 +1.38e-01 +1.39e-01 +1.32e-01 +1.34e-01 +1.31e-01 +1.19e-01 +1.18e-01 +1.10e-01 +1.13e-01 +1.51e-01 +5.50e-02 +1.63e-02 +1.36e-01 +1.38e-01 +1.40e-01 +1.44e-01 +1.37e-01 +1.43e-01 +1.44e-01 +1.39e-01 +1.26e-01 +1.21e-01 +1.14e-01 +1.05e-01 +1.24e-01 +4.77e-02 +1.43e-02 +1.96e-01 +1.79e-01 +1.80e-01 +1.95e-01 +1.99e-01 +1.94e-01 +1.81e-01 +1.92e-01 +1.80e-01 +1.60e-01 +1.61e-01 +1.34e-01 +7.13e-02 +2.07e-02 +7.42e-03 +1.76e-01 +9.49e-02 +1.22e-01 +1.31e-01 +1.35e-01 +1.34e-01 +1.06e-01 +1.42e-01 +1.15e-01 +7.49e-02 +8.42e-02 +5.97e-02 +2.86e-02 +1.13e-02 +3.88e-03 +5.74e-02 +3.06e-02 +3.68e-02 +4.09e-02 +5.27e-02 +4.71e-02 +3.30e-02 +4.67e-02 +3.85e-02 +2.55e-02 +2.40e-02 +1.51e-02 +1.04e-02 +6.23e-03 +3.39e-03 +1.51e-01 +1.61e-01 +1.68e-01 +1.62e-01 +1.62e-01 +1.64e-01 +1.64e-01 +1.55e-01 +1.53e-01 +1.51e-01 +1.54e-01 +1.45e-01 +1.43e-01 +3.50e-02 +1.38e-02 +1.65e-01 +1.68e-01 +1.64e-01 +1.54e-01 +1.51e-01 +1.54e-01 +1.57e-01 +1.50e-01 +1.50e-01 +1.49e-01 +1.55e-01 +1.49e-01 +1.60e-01 +6.68e-02 +2.43e-02 +1.59e-01 +1.68e-01 +1.59e-01 +1.54e-01 +1.55e-01 +1.66e-01 +1.64e-01 +1.60e-01 +1.47e-01 +1.49e-01 +1.42e-01 +1.60e-01 +2.06e-01 +1.03e-01 +2.77e-02 +1.56e-01 +1.59e-01 +1.54e-01 +1.54e-01 +1.59e-01 +1.65e-01 +5.07e-02 +1.43e-01 +1.52e-01 +1.45e-01 +1.35e-01 +1.44e-01 +1.79e-01 +9.16e-02 +3.06e-02 +1.50e-01 +1.51e-01 +1.54e-01 +1.55e-01 +1.59e-01 +1.59e-01 +1.53e-01 +1.47e-01 +1.53e-01 +1.51e-01 +1.40e-01 +1.45e-01 +1.90e-01 +1.07e-01 +2.94e-02 +1.50e-01 +1.52e-01 +1.48e-01 +1.54e-01 +1.52e-01 +1.51e-01 +1.50e-01 +1.43e-01 +1.47e-01 +1.46e-01 +1.38e-01 +1.35e-01 +1.78e-01 +9.46e-02 +3.09e-02 +1.41e-01 +1.42e-01 +1.53e-01 +1.56e-01 +1.53e-01 +1.48e-01 +1.48e-01 +1.41e-01 +1.38e-01 +1.40e-01 +1.40e-01 +1.30e-01 +1.49e-01 +9.60e-02 +3.33e-02 +1.42e-01 +1.46e-01 +1.40e-01 +1.50e-01 +1.45e-01 +1.45e-01 +1.50e-01 +1.45e-01 +1.41e-01 +1.33e-01 +1.36e-01 +1.37e-01 +1.71e-01 +7.74e-02 +2.54e-02 +1.45e-01 +1.43e-01 +1.47e-01 +1.51e-01 +1.49e-01 +1.54e-01 +1.42e-01 +1.37e-01 +1.34e-01 +1.33e-01 +1.27e-01 +1.27e-01 +1.66e-01 +7.29e-02 +2.38e-02 +1.44e-01 +1.39e-01 +1.51e-01 +1.49e-01 +1.48e-01 +1.46e-01 +1.38e-01 +1.29e-01 +1.23e-01 +1.18e-01 +1.23e-01 +1.14e-01 +1.60e-01 +7.60e-02 +2.69e-02 +1.34e-01 +1.33e-01 +1.39e-01 +1.41e-01 +1.50e-01 +1.43e-01 +1.31e-01 +1.28e-01 +1.23e-01 +1.19e-01 +1.12e-01 +1.08e-01 +1.37e-01 +5.02e-02 +1.48e-02 +1.33e-01 +5.34e-02 +1.34e-01 +1.36e-01 +1.35e-01 +1.30e-01 +1.37e-01 +1.40e-01 +1.27e-01 +1.22e-01 +1.08e-01 +1.01e-01 +1.15e-01 +4.47e-02 +1.28e-02 +1.73e-01 +1.60e-01 +1.60e-01 +1.70e-01 +1.65e-01 +1.66e-01 +1.67e-01 +1.80e-01 +1.81e-01 +1.46e-01 +1.35e-01 +1.18e-01 +6.95e-02 +2.21e-02 +7.42e-03 +1.30e-01 +7.03e-02 +8.47e-02 +1.05e-01 +8.11e-02 +8.16e-02 +7.32e-02 +7.94e-02 +1.08e-01 +7.59e-02 +5.62e-02 +4.38e-02 +2.63e-02 +6.47e-03 +2.09e-03 +5.13e-02 +2.67e-02 +3.48e-02 +3.62e-02 +2.93e-02 +2.83e-02 +2.47e-02 +2.60e-02 +3.29e-02 +2.55e-02 +1.63e-02 +1.59e-02 +1.02e-02 +3.46e-03 +1.50e-03 +2.17e-01 +2.25e-01 +2.43e-01 +2.43e-01 +2.31e-01 +2.22e-01 +2.22e-01 +2.12e-01 +2.21e-01 +1.98e-01 +1.99e-01 +2.04e-01 +1.57e-01 +3.11e-02 +7.85e-03 +2.30e-01 +2.10e-01 +2.47e-01 +2.20e-01 +2.21e-01 +2.29e-01 +2.22e-01 +2.06e-01 +1.98e-01 +2.04e-01 +1.90e-01 +1.83e-01 +1.78e-01 +5.75e-02 +1.61e-02 +2.24e-01 +2.08e-01 +2.36e-01 +2.21e-01 +2.12e-01 +2.36e-01 +2.08e-01 +2.30e-01 +2.06e-01 +2.08e-01 +1.93e-01 +1.84e-01 +2.10e-01 +9.89e-02 +3.10e-02 +2.25e-01 +2.06e-01 +2.18e-01 +2.25e-01 +2.19e-01 +2.39e-01 +2.22e-01 +2.20e-01 +2.12e-01 +2.02e-01 +1.75e-01 +1.62e-01 +1.73e-01 +6.25e-02 +1.80e-02 +2.16e-01 +2.06e-01 +2.12e-01 +2.21e-01 +2.09e-01 +2.25e-01 +2.54e-01 +2.18e-01 +2.16e-01 +2.24e-01 +1.99e-01 +1.77e-01 +1.57e-01 +7.90e-02 +2.92e-02 +2.13e-01 +1.96e-01 +2.07e-01 +2.04e-01 +2.00e-01 +2.17e-01 +2.17e-01 +2.01e-01 +2.06e-01 +2.15e-01 +1.92e-01 +1.66e-01 +1.68e-01 +8.03e-02 +2.44e-02 +1.90e-01 +1.93e-01 +2.12e-01 +2.07e-01 +2.00e-01 +2.08e-01 +2.04e-01 +1.97e-01 +1.68e-01 +1.90e-01 +1.82e-01 +1.49e-01 +1.55e-01 +6.10e-02 +1.96e-02 +1.94e-01 +1.89e-01 +2.00e-01 +2.16e-01 +2.22e-01 +2.06e-01 +2.02e-01 +1.99e-01 +1.77e-01 +1.77e-01 +1.83e-01 +1.68e-01 +1.29e-01 +3.50e-02 +1.16e-02 +1.93e-01 +2.00e-01 +2.02e-01 +2.11e-01 +2.16e-01 +1.93e-01 +2.15e-01 +2.05e-01 +1.80e-01 +1.82e-01 +1.71e-01 +1.65e-01 +1.61e-01 +4.47e-02 +1.50e-02 +1.94e-01 +2.06e-01 +2.19e-01 +2.03e-01 +2.12e-01 +1.96e-01 +2.02e-01 +1.81e-01 +1.66e-01 +1.60e-01 +1.62e-01 +1.62e-01 +1.56e-01 +4.92e-02 +1.53e-02 +1.78e-01 +1.86e-01 +2.05e-01 +2.04e-01 +1.97e-01 +2.15e-01 +1.88e-01 +1.63e-01 +1.57e-01 +1.38e-01 +1.47e-01 +1.39e-01 +1.01e-01 +4.20e-02 +1.54e-02 +1.47e-01 +1.52e-01 +1.77e-01 +1.74e-01 +1.84e-01 +1.91e-01 +2.00e-01 +1.85e-01 +1.68e-01 +1.42e-01 +1.34e-01 +1.28e-01 +9.77e-02 +2.94e-02 +1.17e-02 +1.28e-01 +1.39e-01 +1.56e-01 +1.44e-01 +1.64e-01 +1.66e-01 +1.39e-01 +1.87e-01 +1.87e-01 +1.57e-01 +1.18e-01 +9.64e-02 +6.30e-02 +1.82e-02 +8.08e-03 +7.18e-02 +4.39e-02 +5.20e-02 +6.11e-02 +4.96e-02 +4.82e-02 +4.41e-02 +5.38e-02 +8.79e-02 +5.77e-02 +2.70e-02 +2.61e-02 +1.49e-02 +4.93e-03 +2.78e-03 +2.74e-02 +1.41e-02 +1.81e-02 +1.93e-02 +1.48e-02 +1.51e-02 +1.60e-02 +1.57e-02 +2.80e-02 +2.17e-02 +1.10e-02 +8.28e-03 +4.76e-03 +2.53e-03 +9.95e-04 +2.15e-01 +2.13e-01 +2.69e-01 +1.97e-01 +2.97e-01 +2.44e-01 +2.16e-01 +2.27e-01 +1.95e-01 +1.69e-01 +1.21e-01 +1.27e-01 +1.07e-01 +2.85e-02 +7.97e-03 +2.71e-01 +1.94e-01 +3.20e-01 +3.60e-01 +2.90e-01 +2.34e-01 +1.99e-01 +2.04e-01 +1.73e-01 +1.40e-01 +1.29e-01 +1.08e-01 +8.99e-02 +4.08e-02 +1.48e-02 +2.36e-01 +2.27e-01 +2.95e-01 +2.38e-01 +1.88e-01 +2.38e-01 +1.95e-01 +2.03e-01 +1.85e-01 +1.65e-01 +1.78e-01 +1.40e-01 +7.70e-02 +4.33e-02 +2.87e-02 +2.64e-01 +2.27e-01 +2.72e-01 +3.07e-01 +2.33e-01 +2.58e-01 +2.71e-01 +2.36e-01 +2.17e-01 +1.73e-01 +1.26e-01 +9.14e-02 +6.26e-02 +3.03e-02 +1.37e-02 +2.60e-01 +1.95e-01 +2.57e-01 +3.08e-01 +2.65e-01 +2.75e-01 +2.70e-01 +2.25e-01 +1.83e-01 +1.71e-01 +1.54e-01 +8.13e-02 +5.27e-02 +2.61e-02 +1.31e-02 +2.09e-01 +1.59e-01 +2.23e-01 +2.30e-01 +1.63e-01 +1.84e-01 +2.17e-01 +1.62e-01 +1.39e-01 +1.79e-01 +1.60e-01 +1.05e-01 +5.74e-02 +2.47e-02 +1.55e-02 +1.50e-01 +1.80e-01 +2.09e-01 +2.09e-01 +1.83e-01 +2.00e-01 +1.82e-01 +1.57e-01 +1.32e-01 +1.22e-01 +9.76e-02 +6.91e-02 +4.61e-02 +2.65e-02 +1.49e-02 +1.12e-01 +1.42e-01 +1.70e-01 +1.73e-01 +1.86e-01 +1.93e-01 +1.34e-01 +1.27e-01 +9.53e-02 +8.75e-02 +9.91e-02 +7.87e-02 +4.52e-02 +2.11e-02 +7.92e-03 +1.22e-01 +1.39e-01 +1.69e-01 +2.22e-01 +1.74e-01 +1.60e-01 +1.21e-01 +1.47e-01 +1.19e-01 +1.07e-01 +9.89e-02 +8.43e-02 +5.88e-02 +2.01e-02 +8.74e-03 +9.13e-02 +1.61e-01 +2.14e-01 +1.61e-01 +1.43e-01 +1.01e-01 +1.25e-01 +1.42e-01 +1.15e-01 +9.15e-02 +8.87e-02 +6.91e-02 +5.19e-02 +2.72e-02 +8.44e-03 +9.87e-02 +1.23e-01 +1.61e-01 +1.11e-01 +1.42e-01 +1.45e-01 +1.19e-01 +1.11e-01 +8.68e-02 +5.91e-02 +7.60e-02 +6.84e-02 +4.18e-02 +2.17e-02 +9.67e-03 +8.56e-02 +7.21e-02 +1.04e-01 +9.72e-02 +9.63e-02 +1.38e-01 +1.19e-01 +7.48e-02 +6.52e-02 +5.51e-02 +5.18e-02 +5.14e-02 +3.71e-02 +9.91e-03 +3.65e-03 +4.31e-02 +5.32e-02 +6.28e-02 +4.91e-02 +5.90e-02 +8.59e-02 +7.51e-02 +7.34e-02 +6.11e-02 +6.19e-02 +4.42e-02 +3.60e-02 +2.68e-02 +1.01e-02 +4.00e-03 +1.92e-02 +2.02e-02 +2.30e-02 +1.98e-02 +2.54e-02 +3.35e-02 +2.56e-02 +3.19e-02 +3.49e-02 +2.85e-02 +1.72e-02 +1.18e-02 +8.37e-03 +4.19e-03 +2.02e-03 +1.38e-02 +7.42e-03 +8.36e-03 +8.57e-03 +9.08e-03 +1.23e-02 +8.17e-03 +1.02e-02 +1.45e-02 +1.35e-02 +6.28e-03 +3.59e-03 +3.28e-03 +1.49e-03 +5.93e-04 +7.32e-02 +7.34e-02 +9.95e-02 +6.68e-02 +1.09e-01 +8.20e-02 +7.49e-02 +7.94e-02 +7.32e-02 +5.71e-02 +3.93e-02 +3.97e-02 +3.57e-02 +2.10e-02 +7.29e-03 +1.10e-01 +5.23e-02 +1.07e-01 +1.46e-01 +1.24e-01 +8.85e-02 +7.13e-02 +6.52e-02 +6.37e-02 +5.06e-02 +3.96e-02 +3.23e-02 +2.70e-02 +1.79e-02 +8.68e-03 +8.76e-02 +6.94e-02 +1.01e-01 +9.79e-02 +5.28e-02 +8.50e-02 +7.16e-02 +6.22e-02 +6.61e-02 +4.64e-02 +6.19e-02 +5.31e-02 +2.85e-02 +1.22e-02 +1.28e-02 +8.68e-02 +7.27e-02 +1.06e-01 +1.20e-01 +8.06e-02 +8.67e-02 +9.61e-02 +8.95e-02 +8.71e-02 +6.56e-02 +4.39e-02 +3.10e-02 +2.08e-02 +1.24e-02 +5.85e-03 +1.06e-01 +7.84e-02 +9.79e-02 +1.14e-01 +1.02e-01 +9.64e-02 +1.00e-01 +7.46e-02 +7.16e-02 +5.01e-02 +5.24e-02 +2.38e-02 +1.47e-02 +8.69e-03 +5.50e-03 +6.48e-02 +5.48e-02 +7.05e-02 +7.64e-02 +6.19e-02 +5.28e-02 +6.84e-02 +5.17e-02 +4.47e-02 +5.44e-02 +5.78e-02 +4.35e-02 +2.02e-02 +8.44e-03 +4.29e-03 +6.15e-02 +5.80e-02 +7.42e-02 +8.23e-02 +4.82e-02 +5.80e-02 +5.99e-02 +5.52e-02 +5.07e-02 +4.11e-02 +3.25e-02 +2.74e-02 +1.58e-02 +7.93e-03 +7.33e-03 +3.07e-02 +5.33e-02 +5.17e-02 +5.32e-02 +5.86e-02 +6.36e-02 +3.92e-02 +4.20e-02 +3.02e-02 +2.37e-02 +2.60e-02 +2.49e-02 +1.38e-02 +1.14e-02 +8.22e-03 +3.87e-02 +4.20e-02 +5.55e-02 +6.88e-02 +6.66e-02 +6.23e-02 +3.77e-02 +4.86e-02 +4.16e-02 +3.47e-02 +2.92e-02 +2.68e-02 +2.21e-02 +9.30e-03 +3.87e-03 +2.33e-02 +4.93e-02 +6.49e-02 +6.24e-02 +4.58e-02 +2.96e-02 +4.02e-02 +4.66e-02 +4.25e-02 +2.79e-02 +2.67e-02 +2.26e-02 +1.37e-02 +1.36e-02 +7.38e-03 +3.11e-02 +5.02e-02 +5.87e-02 +3.06e-02 +4.64e-02 +4.05e-02 +2.71e-02 +3.77e-02 +3.09e-02 +2.18e-02 +2.34e-02 +1.91e-02 +1.39e-02 +1.02e-02 +7.91e-03 +2.26e-02 +2.44e-02 +3.23e-02 +2.90e-02 +2.99e-02 +4.72e-02 +4.07e-02 +3.01e-02 +2.01e-02 +1.70e-02 +1.57e-02 +1.51e-02 +1.50e-02 +4.17e-03 +1.63e-03 +1.86e-02 +1.84e-02 +2.18e-02 +2.46e-02 +1.84e-02 +3.15e-02 +2.72e-02 +2.27e-02 +1.73e-02 +2.03e-02 +1.19e-02 +1.20e-02 +1.14e-02 +4.19e-03 +1.45e-03 +6.21e-03 +9.69e-03 +9.74e-03 +7.36e-03 +1.15e-02 +1.57e-02 +1.41e-02 +1.64e-02 +1.88e-02 +1.03e-02 +1.09e-02 +6.84e-03 +4.29e-03 +2.69e-03 +2.28e-03 +4.54e-03 +5.03e-03 +5.40e-03 +5.12e-03 +6.04e-03 +1.01e-02 +6.83e-03 +6.36e-03 +1.16e-02 +7.66e-03 +5.81e-03 +3.71e-03 +1.48e-03 +1.00e-03 +1.45e-03 +Upper Bounds +8.55e-01 +8.64e-01 +8.59e-01 +8.67e-01 +8.53e-01 +8.16e-01 +8.31e-01 +8.50e-01 +8.39e-01 +8.80e-01 +8.66e-01 +8.96e-01 +1.31e+00 +2.22e+00 +8.38e-01 +8.15e-01 +8.82e-01 +8.67e-01 +7.61e-01 +8.32e-01 +8.60e-01 +8.42e-01 +8.42e-01 +8.32e-01 +8.34e-01 +8.80e-01 +8.65e-01 +1.22e+00 +1.82e+00 +6.13e-01 +8.56e-01 +8.32e-01 +8.43e-01 +8.53e-01 +8.33e-01 +8.57e-01 +8.19e-01 +7.78e-01 +8.33e-01 +8.19e-01 +8.41e-01 +9.08e-01 +1.30e+00 +2.06e+00 +7.76e-01 +8.38e-01 +8.08e-01 +8.28e-01 +8.25e-01 +8.10e-01 +8.21e-01 +8.13e-01 +8.31e-01 +8.17e-01 +8.36e-01 +7.91e-01 +8.95e-01 +1.26e+00 +2.05e+00 +6.73e-01 +8.25e-01 +8.69e-01 +8.34e-01 +8.56e-01 +8.29e-01 +8.11e-01 +8.01e-01 +8.32e-01 +7.75e-01 +8.33e-01 +8.31e-01 +9.11e-01 +1.34e+00 +1.73e+00 +6.60e-01 +8.48e-01 +8.49e-01 +8.41e-01 +8.41e-01 +8.43e-01 +7.95e-01 +8.08e-01 +7.96e-01 +7.85e-01 +7.74e-01 +8.58e-01 +8.98e-01 +1.29e+00 +1.49e+00 +5.33e-01 +8.40e-01 +7.98e-01 +7.91e-01 +8.03e-01 +7.88e-01 +7.96e-01 +7.81e-01 +7.66e-01 +7.83e-01 +7.62e-01 +7.93e-01 +7.72e-01 +1.21e+00 +1.65e+00 +6.11e-01 +8.18e-01 +8.25e-01 +8.44e-01 +8.18e-01 +8.00e-01 +8.28e-01 +8.00e-01 +7.60e-01 +7.23e-01 +7.65e-01 +7.83e-01 +7.01e-01 +1.04e+00 +9.49e-01 +3.97e-01 +8.24e-01 +8.27e-01 +8.57e-01 +8.58e-01 +8.13e-01 +8.03e-01 +7.83e-01 +7.70e-01 +7.46e-01 +7.07e-01 +7.24e-01 +7.65e-01 +1.18e+00 +9.01e-01 +2.88e-01 +8.34e-01 +8.73e-01 +8.70e-01 +8.56e-01 +8.29e-01 +7.71e-01 +7.75e-01 +7.58e-01 +7.64e-01 +7.06e-01 +7.64e-01 +7.43e-01 +1.07e+00 +8.82e-01 +3.35e-01 +8.58e-01 +8.29e-01 +8.72e-01 +8.48e-01 +8.46e-01 +7.87e-01 +7.50e-01 +7.65e-01 +7.38e-01 +7.34e-01 +8.10e-01 +7.43e-01 +1.02e+00 +6.73e-01 +2.03e-01 +9.03e-01 +9.01e-01 +8.60e-01 +8.26e-01 +9.18e-01 +8.72e-01 +7.91e-01 +8.39e-01 +7.95e-01 +7.87e-01 +7.88e-01 +7.38e-01 +1.08e+00 +5.23e-01 +1.69e-01 +1.31e+00 +1.26e+00 +1.18e+00 +1.25e+00 +1.32e+00 +1.21e+00 +1.20e+00 +1.16e+00 +1.16e+00 +1.14e+00 +1.11e+00 +1.10e+00 +1.03e+00 +4.01e-01 +1.10e-01 +2.50e+00 +1.84e+00 +1.94e+00 +1.32e+00 +1.43e+00 +1.59e+00 +1.30e+00 +1.19e+00 +9.40e-01 +9.83e-01 +9.19e-01 +7.52e-01 +4.99e-01 +1.94e-01 +9.48e-02 +1.09e+00 +7.40e-01 +8.05e-01 +4.38e-01 +4.72e-01 +5.81e-01 +4.22e-01 +4.06e-01 +3.50e-01 +3.08e-01 +3.25e-01 +2.23e-01 +1.85e-01 +8.09e-02 +3.66e-02 +8.48e-01 +8.45e-01 +8.50e-01 +8.39e-01 +8.13e-01 +7.97e-01 +8.76e-01 +8.45e-01 +7.95e-01 +8.18e-01 +8.84e-01 +8.93e-01 +1.31e+00 +1.96e+00 +7.41e-01 +8.63e-01 +8.69e-01 +8.49e-01 +8.32e-01 +8.42e-01 +8.08e-01 +8.09e-01 +8.36e-01 +7.99e-01 +8.20e-01 +8.33e-01 +9.00e-01 +1.21e+00 +1.86e+00 +7.23e-01 +8.57e-01 +8.27e-01 +8.35e-01 +8.27e-01 +8.44e-01 +8.48e-01 +8.19e-01 +8.39e-01 +8.01e-01 +8.48e-01 +8.52e-01 +8.71e-01 +1.31e+00 +1.65e+00 +5.73e-01 +8.64e-01 +8.75e-01 +8.50e-01 +8.46e-01 +8.43e-01 +8.17e-01 +8.28e-01 +8.15e-01 +8.07e-01 +8.04e-01 +8.51e-01 +8.94e-01 +1.24e+00 +1.82e+00 +6.58e-01 +8.61e-01 +8.65e-01 +8.57e-01 +8.28e-01 +8.15e-01 +8.13e-01 +8.36e-01 +8.01e-01 +7.82e-01 +8.17e-01 +8.58e-01 +8.55e-01 +1.26e+00 +1.71e+00 +6.99e-01 +8.39e-01 +8.48e-01 +8.35e-01 +8.30e-01 +8.61e-01 +8.03e-01 +7.79e-01 +7.79e-01 +7.65e-01 +7.73e-01 +8.23e-01 +8.92e-01 +1.26e+00 +1.83e+00 +6.65e-01 +8.50e-01 +8.43e-01 +8.01e-01 +8.18e-01 +8.51e-01 +8.15e-01 +7.63e-01 +7.50e-01 +7.52e-01 +7.40e-01 +7.66e-01 +8.29e-01 +1.17e+00 +1.50e+00 +5.74e-01 +8.24e-01 +8.15e-01 +7.95e-01 +7.88e-01 +7.97e-01 +7.70e-01 +7.69e-01 +7.35e-01 +6.98e-01 +6.91e-01 +6.35e-01 +6.08e-01 +9.93e-01 +8.87e-01 +3.56e-01 +7.90e-01 +8.73e-01 +8.38e-01 +8.31e-01 +8.29e-01 +7.89e-01 +7.80e-01 +7.18e-01 +7.42e-01 +7.23e-01 +6.87e-01 +7.01e-01 +1.01e+00 +6.82e-01 +2.08e-01 +8.24e-01 +8.78e-01 +8.46e-01 +8.43e-01 +7.84e-01 +8.35e-01 +7.79e-01 +7.48e-01 +7.63e-01 +7.14e-01 +7.56e-01 +7.03e-01 +1.02e+00 +7.48e-01 +2.25e-01 +8.45e-01 +8.73e-01 +8.56e-01 +8.16e-01 +8.44e-01 +8.36e-01 +7.99e-01 +7.51e-01 +7.67e-01 +7.20e-01 +7.86e-01 +7.45e-01 +1.01e+00 +6.73e-01 +2.04e-01 +8.93e-01 +8.59e-01 +8.64e-01 +8.51e-01 +8.70e-01 +8.91e-01 +8.41e-01 +7.94e-01 +7.85e-01 +8.08e-01 +8.08e-01 +7.83e-01 +9.73e-01 +4.60e-01 +1.58e-01 +1.24e+00 +1.32e+00 +1.27e+00 +1.29e+00 +1.40e+00 +1.26e+00 +1.17e+00 +1.20e+00 +1.08e+00 +1.09e+00 +1.21e+00 +1.11e+00 +9.96e-01 +3.09e-01 +8.83e-02 +1.56e+00 +1.49e+00 +1.49e+00 +1.58e+00 +1.83e+00 +1.75e+00 +1.41e+00 +1.38e+00 +1.12e+00 +8.28e-01 +8.50e-01 +8.27e-01 +5.48e-01 +1.84e-01 +7.71e-02 +5.56e-01 +5.50e-01 +5.89e-01 +5.41e-01 +6.72e-01 +7.04e-01 +4.96e-01 +4.98e-01 +4.70e-01 +2.59e-01 +2.70e-01 +2.72e-01 +2.11e-01 +9.38e-02 +3.66e-02 +8.67e-01 +8.42e-01 +8.70e-01 +8.39e-01 +7.98e-01 +8.02e-01 +8.06e-01 +7.94e-01 +8.48e-01 +8.04e-01 +8.10e-01 +8.81e-01 +1.45e+00 +2.04e+00 +7.89e-01 +8.58e-01 +8.42e-01 +8.10e-01 +8.30e-01 +7.96e-01 +8.14e-01 +7.91e-01 +8.00e-01 +7.95e-01 +8.24e-01 +8.16e-01 +8.87e-01 +1.22e+00 +1.96e+00 +8.18e-01 +8.56e-01 +8.69e-01 +8.58e-01 +7.87e-01 +8.24e-01 +8.16e-01 +8.16e-01 +8.43e-01 +7.92e-01 +8.05e-01 +8.03e-01 +8.83e-01 +1.22e+00 +1.68e+00 +6.47e-01 +7.87e-01 +8.38e-01 +8.29e-01 +8.15e-01 +8.16e-01 +7.97e-01 +8.04e-01 +8.23e-01 +7.82e-01 +7.95e-01 +8.24e-01 +7.98e-01 +1.22e+00 +1.65e+00 +5.42e-01 +8.09e-01 +8.09e-01 +8.05e-01 +7.72e-01 +8.14e-01 +8.20e-01 +7.50e-01 +7.87e-01 +7.53e-01 +7.48e-01 +8.36e-01 +8.59e-01 +1.22e+00 +1.39e+00 +4.29e-01 +8.07e-01 +8.21e-01 +7.86e-01 +7.76e-01 +7.84e-01 +7.85e-01 +7.56e-01 +7.44e-01 +7.72e-01 +7.80e-01 +8.33e-01 +8.48e-01 +1.21e+00 +1.74e+00 +6.61e-01 +8.36e-01 +8.64e-01 +8.19e-01 +8.11e-01 +8.00e-01 +8.07e-01 +7.80e-01 +7.34e-01 +7.56e-01 +7.70e-01 +7.83e-01 +7.97e-01 +1.13e+00 +1.26e+00 +4.66e-01 +8.16e-01 +8.15e-01 +7.98e-01 +7.75e-01 +7.87e-01 +7.92e-01 +7.70e-01 +7.35e-01 +7.31e-01 +7.42e-01 +7.14e-01 +7.50e-01 +1.05e+00 +8.42e-01 +3.14e-01 +7.86e-01 +8.18e-01 +8.21e-01 +8.01e-01 +7.99e-01 +7.90e-01 +7.89e-01 +7.83e-01 +7.33e-01 +7.48e-01 +7.58e-01 +7.15e-01 +1.06e+00 +8.32e-01 +3.09e-01 +8.27e-01 +8.58e-01 +8.09e-01 +8.21e-01 +8.32e-01 +7.85e-01 +7.86e-01 +7.51e-01 +7.01e-01 +7.38e-01 +7.30e-01 +7.22e-01 +9.77e-01 +6.23e-01 +2.49e-01 +8.03e-01 +8.31e-01 +8.48e-01 +8.19e-01 +8.34e-01 +8.33e-01 +8.42e-01 +7.89e-01 +7.16e-01 +7.24e-01 +7.28e-01 +7.16e-01 +9.71e-01 +6.27e-01 +1.80e-01 +8.04e-01 +8.69e-01 +8.69e-01 +8.82e-01 +8.64e-01 +8.59e-01 +8.38e-01 +8.03e-01 +7.94e-01 +7.80e-01 +7.95e-01 +7.83e-01 +8.87e-01 +5.44e-01 +1.87e-01 +1.24e+00 +1.24e+00 +1.24e+00 +1.21e+00 +1.24e+00 +1.28e+00 +1.10e+00 +1.13e+00 +1.12e+00 +1.09e+00 +1.11e+00 +1.13e+00 +1.07e+00 +3.12e-01 +9.52e-02 +1.84e+00 +1.36e+00 +1.70e+00 +1.79e+00 +1.82e+00 +1.16e+00 +1.17e+00 +1.10e+00 +9.39e-01 +8.88e-01 +6.70e-01 +6.23e-01 +5.35e-01 +2.34e-01 +6.47e-02 +6.45e-01 +4.31e-01 +7.11e-01 +6.80e-01 +6.87e-01 +4.47e-01 +4.04e-01 +4.19e-01 +2.85e-01 +3.23e-01 +1.57e-01 +1.93e-01 +1.76e-01 +1.29e-01 +6.44e-02 +8.45e-01 +8.09e-01 +8.31e-01 +8.52e-01 +8.43e-01 +8.06e-01 +7.88e-01 +7.57e-01 +7.58e-01 +7.66e-01 +7.61e-01 +8.67e-01 +1.27e+00 +2.06e+00 +7.56e-01 +8.40e-01 +8.74e-01 +8.23e-01 +7.97e-01 +8.08e-01 +7.78e-01 +7.85e-01 +7.76e-01 +7.95e-01 +8.30e-01 +7.96e-01 +8.37e-01 +1.18e+00 +1.62e+00 +6.44e-01 +8.11e-01 +8.19e-01 +8.40e-01 +7.94e-01 +7.57e-01 +7.93e-01 +7.66e-01 +7.66e-01 +7.76e-01 +8.15e-01 +7.99e-01 +8.24e-01 +1.16e+00 +1.39e+00 +5.36e-01 +8.08e-01 +8.15e-01 +8.00e-01 +8.03e-01 +7.68e-01 +7.86e-01 +7.93e-01 +7.55e-01 +7.56e-01 +7.89e-01 +7.68e-01 +8.06e-01 +1.20e+00 +1.24e+00 +5.05e-01 +7.77e-01 +7.72e-01 +8.04e-01 +8.28e-01 +7.75e-01 +7.64e-01 +7.80e-01 +7.54e-01 +7.38e-01 +7.65e-01 +8.13e-01 +8.11e-01 +1.21e+00 +1.61e+00 +5.92e-01 +8.28e-01 +7.61e-01 +8.10e-01 +8.12e-01 +7.80e-01 +7.60e-01 +7.76e-01 +7.84e-01 +7.75e-01 +7.67e-01 +7.63e-01 +8.21e-01 +1.23e+00 +1.51e+00 +5.55e-01 +8.13e-01 +7.95e-01 +8.40e-01 +7.80e-01 +7.59e-01 +7.73e-01 +7.80e-01 +7.46e-01 +7.45e-01 +7.55e-01 +7.27e-01 +7.44e-01 +1.11e+00 +1.44e+00 +5.11e-01 +8.31e-01 +8.12e-01 +8.03e-01 +7.49e-01 +7.48e-01 +7.53e-01 +7.93e-01 +7.32e-01 +7.17e-01 +7.10e-01 +7.05e-01 +7.55e-01 +1.11e+00 +9.42e-01 +3.25e-01 +8.00e-01 +7.96e-01 +7.87e-01 +8.07e-01 +7.73e-01 +7.86e-01 +7.64e-01 +7.65e-01 +7.27e-01 +7.21e-01 +7.35e-01 +7.93e-01 +1.18e+00 +9.54e-01 +2.85e-01 +8.11e-01 +7.95e-01 +8.15e-01 +7.97e-01 +7.74e-01 +7.69e-01 +7.52e-01 +7.33e-01 +7.51e-01 +7.28e-01 +7.13e-01 +7.16e-01 +1.10e+00 +6.86e-01 +2.68e-01 +8.31e-01 +8.25e-01 +7.50e-01 +8.42e-01 +8.24e-01 +8.05e-01 +7.74e-01 +7.47e-01 +7.33e-01 +7.57e-01 +7.16e-01 +7.50e-01 +9.60e-01 +4.98e-01 +1.46e-01 +8.65e-01 +8.27e-01 +8.48e-01 +3.91e-01 +8.66e-01 +8.92e-01 +8.23e-01 +7.50e-01 +8.02e-01 +7.77e-01 +7.85e-01 +7.73e-01 +9.44e-01 +4.45e-01 +1.34e-01 +1.16e+00 +1.18e+00 +1.20e+00 +1.16e+00 +1.20e+00 +1.19e+00 +1.01e+00 +1.06e+00 +1.10e+00 +1.06e+00 +1.02e+00 +1.01e+00 +1.07e+00 +4.13e-01 +1.32e-01 +1.53e+00 +1.26e+00 +1.05e+00 +1.47e+00 +1.43e+00 +1.20e+00 +8.45e-01 +6.05e-01 +9.60e-01 +9.09e-01 +6.43e-01 +6.18e-01 +5.19e-01 +2.26e-01 +8.71e-02 +5.78e-01 +4.15e-01 +4.47e-01 +4.72e-01 +5.79e-01 +4.50e-01 +3.73e-01 +1.91e-01 +3.18e-01 +3.21e-01 +2.12e-01 +2.08e-01 +1.59e-01 +1.30e-01 +6.29e-02 +8.13e-01 +7.92e-01 +7.98e-01 +8.10e-01 +8.01e-01 +7.94e-01 +7.98e-01 +7.55e-01 +7.35e-01 +7.76e-01 +7.54e-01 +8.38e-01 +1.23e+00 +1.59e+00 +6.35e-01 +8.22e-01 +8.05e-01 +7.80e-01 +8.16e-01 +8.13e-01 +7.71e-01 +7.48e-01 +7.45e-01 +7.82e-01 +7.91e-01 +7.88e-01 +8.20e-01 +1.19e+00 +1.52e+00 +5.64e-01 +8.23e-01 +8.14e-01 +8.08e-01 +7.83e-01 +7.86e-01 +7.83e-01 +7.67e-01 +7.43e-01 +7.86e-01 +8.17e-01 +8.13e-01 +8.52e-01 +1.19e+00 +1.15e+00 +4.23e-01 +8.08e-01 +8.13e-01 +7.88e-01 +7.92e-01 +7.82e-01 +7.73e-01 +7.50e-01 +7.57e-01 +7.60e-01 +8.01e-01 +7.85e-01 +8.21e-01 +1.23e+00 +1.08e+00 +3.68e-01 +7.79e-01 +7.72e-01 +7.80e-01 +8.09e-01 +7.59e-01 +7.50e-01 +7.76e-01 +7.62e-01 +7.59e-01 +7.48e-01 +7.90e-01 +8.33e-01 +1.23e+00 +1.60e+00 +5.75e-01 +7.85e-01 +7.73e-01 +7.79e-01 +7.88e-01 +7.39e-01 +7.13e-01 +7.08e-01 +7.78e-01 +7.45e-01 +7.62e-01 +7.55e-01 +7.92e-01 +1.16e+00 +1.31e+00 +4.76e-01 +7.40e-01 +7.79e-01 +7.94e-01 +7.60e-01 +7.99e-01 +7.16e-01 +7.14e-01 +7.21e-01 +7.25e-01 +7.16e-01 +7.41e-01 +7.53e-01 +1.04e+00 +1.05e+00 +3.92e-01 +7.83e-01 +8.02e-01 +7.76e-01 +7.66e-01 +7.84e-01 +7.21e-01 +7.22e-01 +6.78e-01 +6.82e-01 +7.30e-01 +7.15e-01 +7.28e-01 +1.08e+00 +8.33e-01 +3.54e-01 +7.96e-01 +7.79e-01 +7.80e-01 +7.68e-01 +7.67e-01 +7.60e-01 +7.52e-01 +7.12e-01 +6.86e-01 +6.95e-01 +7.05e-01 +7.37e-01 +1.02e+00 +8.76e-01 +2.76e-01 +8.13e-01 +8.09e-01 +7.87e-01 +7.66e-01 +7.26e-01 +7.62e-01 +7.59e-01 +7.17e-01 +7.04e-01 +6.89e-01 +6.94e-01 +7.59e-01 +1.02e+00 +8.04e-01 +3.49e-01 +8.12e-01 +7.81e-01 +7.92e-01 +7.74e-01 +7.90e-01 +7.78e-01 +7.64e-01 +7.66e-01 +7.45e-01 +7.07e-01 +7.19e-01 +7.86e-01 +1.05e+00 +6.12e-01 +2.13e-01 +8.28e-01 +8.45e-01 +8.05e-01 +8.12e-01 +8.40e-01 +8.71e-01 +8.00e-01 +7.85e-01 +7.85e-01 +8.08e-01 +7.39e-01 +7.70e-01 +1.09e+00 +5.71e-01 +1.75e-01 +1.22e+00 +1.15e+00 +1.17e+00 +1.21e+00 +1.23e+00 +1.24e+00 +1.23e+00 +1.10e+00 +1.11e+00 +1.06e+00 +1.00e+00 +1.01e+00 +1.07e+00 +4.47e-01 +1.50e-01 +1.39e+00 +1.31e+00 +1.26e+00 +1.31e+00 +1.50e+00 +1.34e+00 +1.46e+00 +9.51e-01 +1.01e+00 +7.68e-01 +5.65e-01 +6.36e-01 +4.41e-01 +1.94e-01 +7.74e-02 +5.17e-01 +4.55e-01 +4.54e-01 +4.50e-01 +5.33e-01 +4.37e-01 +5.86e-01 +2.84e-01 +3.58e-01 +2.61e-01 +1.66e-01 +1.75e-01 +1.34e-01 +8.28e-02 +5.70e-02 +7.29e-01 +7.75e-01 +7.84e-01 +7.97e-01 +8.15e-01 +7.72e-01 +7.47e-01 +7.39e-01 +7.27e-01 +7.42e-01 +7.41e-01 +7.72e-01 +1.22e+00 +1.35e+00 +4.17e-01 +7.68e-01 +7.93e-01 +7.83e-01 +7.57e-01 +7.76e-01 +7.86e-01 +7.98e-01 +7.43e-01 +7.69e-01 +7.51e-01 +7.70e-01 +8.08e-01 +1.20e+00 +1.48e+00 +5.74e-01 +7.98e-01 +8.06e-01 +8.03e-01 +7.63e-01 +7.79e-01 +7.61e-01 +7.64e-01 +7.83e-01 +7.61e-01 +7.60e-01 +7.76e-01 +8.43e-01 +1.25e+00 +1.26e+00 +4.08e-01 +7.94e-01 +7.94e-01 +7.59e-01 +7.50e-01 +7.63e-01 +7.94e-01 +7.36e-01 +7.53e-01 +7.57e-01 +7.78e-01 +7.78e-01 +8.22e-01 +1.20e+00 +1.23e+00 +4.03e-01 +7.88e-01 +7.93e-01 +7.89e-01 +7.79e-01 +7.63e-01 +7.30e-01 +7.40e-01 +7.42e-01 +7.36e-01 +7.64e-01 +7.39e-01 +8.10e-01 +1.19e+00 +1.33e+00 +5.12e-01 +7.76e-01 +7.68e-01 +7.84e-01 +7.62e-01 +7.39e-01 +7.49e-01 +7.00e-01 +6.85e-01 +7.48e-01 +7.48e-01 +7.30e-01 +7.60e-01 +1.12e+00 +1.11e+00 +4.49e-01 +7.89e-01 +8.02e-01 +7.72e-01 +7.35e-01 +7.33e-01 +7.24e-01 +7.11e-01 +6.42e-01 +6.64e-01 +7.00e-01 +7.57e-01 +7.64e-01 +1.04e+00 +7.18e-01 +2.59e-01 +7.83e-01 +7.69e-01 +7.68e-01 +7.09e-01 +7.54e-01 +7.20e-01 +6.74e-01 +6.39e-01 +5.53e-01 +6.99e-01 +7.47e-01 +7.76e-01 +1.11e+00 +6.53e-01 +2.06e-01 +7.78e-01 +7.65e-01 +7.86e-01 +7.10e-01 +7.60e-01 +7.43e-01 +7.27e-01 +6.62e-01 +6.51e-01 +6.35e-01 +7.04e-01 +7.07e-01 +1.06e+00 +7.73e-01 +2.21e-01 +7.46e-01 +7.60e-01 +7.63e-01 +7.80e-01 +7.34e-01 +7.23e-01 +7.02e-01 +6.87e-01 +7.13e-01 +6.75e-01 +6.69e-01 +7.11e-01 +9.92e-01 +6.25e-01 +1.99e-01 +7.86e-01 +7.39e-01 +7.47e-01 +7.61e-01 +7.48e-01 +7.41e-01 +7.50e-01 +7.38e-01 +7.18e-01 +7.15e-01 +6.76e-01 +6.94e-01 +9.71e-01 +5.36e-01 +1.55e-01 +8.10e-01 +8.03e-01 +8.04e-01 +8.15e-01 +8.13e-01 +8.02e-01 +7.77e-01 +7.76e-01 +7.50e-01 +7.78e-01 +7.13e-01 +6.60e-01 +9.13e-01 +5.08e-01 +1.25e-01 +1.17e+00 +1.12e+00 +1.10e+00 +1.13e+00 +1.21e+00 +1.17e+00 +1.07e+00 +1.02e+00 +1.07e+00 +1.08e+00 +1.10e+00 +8.99e-01 +8.82e-01 +4.84e-01 +1.77e-01 +1.56e+00 +1.32e+00 +1.13e+00 +1.34e+00 +1.30e+00 +1.22e+00 +1.17e+00 +9.07e-01 +7.99e-01 +7.33e-01 +7.55e-01 +6.69e-01 +3.68e-01 +2.03e-01 +1.10e-01 +5.31e-01 +4.95e-01 +4.39e-01 +4.81e-01 +4.96e-01 +3.78e-01 +5.26e-01 +3.10e-01 +2.83e-01 +1.99e-01 +2.15e-01 +2.39e-01 +1.44e-01 +7.93e-02 +4.46e-02 +8.04e-01 +7.95e-01 +7.83e-01 +7.95e-01 +7.54e-01 +7.47e-01 +7.72e-01 +7.56e-01 +7.67e-01 +7.43e-01 +7.58e-01 +7.40e-01 +1.14e+00 +9.11e-01 +4.11e-01 +8.04e-01 +7.68e-01 +7.85e-01 +7.81e-01 +7.65e-01 +7.45e-01 +7.31e-01 +7.07e-01 +7.56e-01 +7.56e-01 +7.59e-01 +7.59e-01 +1.11e+00 +1.04e+00 +3.91e-01 +7.71e-01 +7.89e-01 +7.60e-01 +7.67e-01 +7.41e-01 +7.47e-01 +7.26e-01 +7.15e-01 +7.19e-01 +7.39e-01 +7.88e-01 +7.73e-01 +1.12e+00 +1.24e+00 +5.06e-01 +7.87e-01 +7.41e-01 +7.55e-01 +7.36e-01 +7.30e-01 +7.59e-01 +7.40e-01 +7.37e-01 +7.32e-01 +7.38e-01 +7.44e-01 +7.85e-01 +1.11e+00 +8.43e-01 +3.26e-01 +7.83e-01 +7.76e-01 +7.73e-01 +7.64e-01 +7.54e-01 +7.73e-01 +7.28e-01 +7.45e-01 +7.11e-01 +7.24e-01 +7.27e-01 +7.83e-01 +1.10e+00 +6.80e-01 +2.48e-01 +7.83e-01 +7.52e-01 +7.66e-01 +7.85e-01 +7.69e-01 +7.48e-01 +7.23e-01 +6.99e-01 +6.49e-01 +6.92e-01 +7.08e-01 +7.57e-01 +1.07e+00 +7.06e-01 +2.16e-01 +7.82e-01 +7.85e-01 +7.66e-01 +7.58e-01 +7.56e-01 +7.22e-01 +7.09e-01 +6.42e-01 +6.03e-01 +6.32e-01 +6.99e-01 +7.64e-01 +1.08e+00 +8.03e-01 +2.40e-01 +7.40e-01 +7.41e-01 +7.33e-01 +7.45e-01 +7.43e-01 +7.15e-01 +6.99e-01 +6.15e-01 +4.48e-01 +6.70e-01 +7.13e-01 +7.40e-01 +1.05e+00 +8.40e-01 +2.51e-01 +7.67e-01 +3.55e-01 +7.36e-01 +7.41e-01 +7.16e-01 +7.29e-01 +3.42e-01 +6.33e-01 +6.18e-01 +6.57e-01 +7.08e-01 +7.42e-01 +1.00e+00 +7.79e-01 +2.57e-01 +7.51e-01 +7.62e-01 +7.59e-01 +7.36e-01 +7.09e-01 +6.77e-01 +6.93e-01 +6.91e-01 +6.86e-01 +6.77e-01 +6.81e-01 +6.82e-01 +9.60e-01 +6.08e-01 +2.38e-01 +7.58e-01 +8.07e-01 +7.45e-01 +7.27e-01 +7.02e-01 +6.98e-01 +6.68e-01 +6.74e-01 +6.92e-01 +6.88e-01 +6.37e-01 +6.76e-01 +8.84e-01 +3.75e-01 +1.32e-01 +7.90e-01 +7.78e-01 +7.50e-01 +7.25e-01 +7.49e-01 +7.50e-01 +7.08e-01 +7.12e-01 +7.61e-01 +7.28e-01 +6.88e-01 +6.51e-01 +7.19e-01 +3.41e-01 +1.43e-01 +1.14e+00 +1.06e+00 +1.04e+00 +1.06e+00 +1.09e+00 +1.05e+00 +1.03e+00 +9.47e-01 +1.04e+00 +9.40e-01 +9.42e-01 +8.31e-01 +6.65e-01 +2.06e-01 +8.49e-02 +1.33e+00 +1.35e+00 +1.16e+00 +8.61e-01 +8.28e-01 +1.08e+00 +9.81e-01 +7.58e-01 +7.04e-01 +6.54e-01 +7.20e-01 +5.86e-01 +2.41e-01 +1.04e-01 +4.32e-02 +4.74e-01 +5.21e-01 +4.51e-01 +2.77e-01 +2.70e-01 +3.48e-01 +3.55e-01 +2.46e-01 +2.37e-01 +1.76e-01 +2.35e-01 +2.02e-01 +8.43e-02 +4.62e-02 +2.99e-02 +7.31e-01 +7.48e-01 +7.39e-01 +7.66e-01 +7.81e-01 +7.83e-01 +7.80e-01 +7.68e-01 +7.20e-01 +7.09e-01 +7.19e-01 +7.61e-01 +1.05e+00 +7.78e-01 +2.76e-01 +7.52e-01 +7.85e-01 +7.84e-01 +7.87e-01 +7.71e-01 +7.57e-01 +7.62e-01 +7.09e-01 +7.08e-01 +6.96e-01 +7.23e-01 +7.49e-01 +1.06e+00 +5.92e-01 +1.96e-01 +7.43e-01 +7.38e-01 +7.76e-01 +7.70e-01 +7.56e-01 +7.48e-01 +7.41e-01 +7.42e-01 +7.06e-01 +6.90e-01 +7.25e-01 +7.69e-01 +1.04e+00 +8.10e-01 +2.94e-01 +7.40e-01 +7.47e-01 +7.57e-01 +7.48e-01 +7.55e-01 +7.44e-01 +7.42e-01 +7.35e-01 +6.70e-01 +6.64e-01 +7.63e-01 +7.71e-01 +9.80e-01 +7.74e-01 +3.07e-01 +7.55e-01 +7.66e-01 +7.97e-01 +7.40e-01 +7.53e-01 +7.39e-01 +7.26e-01 +7.07e-01 +6.89e-01 +6.87e-01 +7.44e-01 +7.30e-01 +1.03e+00 +6.26e-01 +1.73e-01 +7.55e-01 +7.36e-01 +7.61e-01 +7.64e-01 +7.21e-01 +7.01e-01 +7.26e-01 +6.89e-01 +6.95e-01 +6.71e-01 +6.81e-01 +7.49e-01 +9.78e-01 +6.44e-01 +2.24e-01 +7.44e-01 +7.16e-01 +7.45e-01 +7.32e-01 +7.41e-01 +7.10e-01 +6.77e-01 +6.35e-01 +6.21e-01 +6.30e-01 +6.43e-01 +6.90e-01 +9.34e-01 +7.67e-01 +2.92e-01 +7.32e-01 +7.55e-01 +7.18e-01 +7.24e-01 +7.26e-01 +7.05e-01 +6.53e-01 +6.21e-01 +6.43e-01 +6.52e-01 +7.13e-01 +7.09e-01 +9.53e-01 +6.82e-01 +2.54e-01 +6.97e-01 +7.00e-01 +7.14e-01 +6.97e-01 +7.15e-01 +6.93e-01 +6.77e-01 +6.33e-01 +6.17e-01 +6.11e-01 +7.05e-01 +6.87e-01 +9.29e-01 +5.46e-01 +1.70e-01 +7.20e-01 +7.06e-01 +7.18e-01 +6.87e-01 +6.85e-01 +6.94e-01 +6.44e-01 +6.64e-01 +5.99e-01 +5.89e-01 +6.44e-01 +6.80e-01 +9.68e-01 +6.14e-01 +1.98e-01 +7.51e-01 +7.34e-01 +7.39e-01 +6.97e-01 +6.75e-01 +6.82e-01 +6.70e-01 +7.02e-01 +6.60e-01 +6.35e-01 +6.16e-01 +6.32e-01 +8.05e-01 +4.17e-01 +1.09e-01 +7.89e-01 +7.57e-01 +7.50e-01 +7.29e-01 +7.26e-01 +6.77e-01 +6.48e-01 +7.04e-01 +6.97e-01 +6.79e-01 +6.60e-01 +6.12e-01 +7.24e-01 +2.74e-01 +7.82e-02 +1.14e+00 +1.03e+00 +1.04e+00 +9.43e-01 +1.02e+00 +9.87e-01 +8.16e-01 +8.35e-01 +8.39e-01 +9.59e-01 +9.14e-01 +7.74e-01 +5.71e-01 +1.72e-01 +5.75e-02 +1.12e+00 +1.16e+00 +8.35e-01 +7.13e-01 +7.64e-01 +8.80e-01 +5.67e-01 +4.88e-01 +4.31e-01 +7.46e-01 +6.47e-01 +4.18e-01 +2.22e-01 +6.04e-02 +2.42e-02 +3.88e-01 +3.92e-01 +2.68e-01 +2.61e-01 +2.31e-01 +3.03e-01 +2.09e-01 +1.58e-01 +1.36e-01 +2.55e-01 +2.03e-01 +1.38e-01 +9.69e-02 +4.41e-02 +1.17e-02 +7.71e-01 +7.88e-01 +8.12e-01 +7.42e-01 +7.85e-01 +7.60e-01 +7.46e-01 +7.71e-01 +7.12e-01 +6.80e-01 +6.82e-01 +7.51e-01 +1.04e+00 +6.16e-01 +1.78e-01 +7.57e-01 +7.66e-01 +7.96e-01 +7.87e-01 +7.31e-01 +7.75e-01 +7.52e-01 +7.45e-01 +7.07e-01 +7.05e-01 +7.08e-01 +7.39e-01 +9.99e-01 +6.60e-01 +2.06e-01 +7.40e-01 +7.48e-01 +7.67e-01 +7.62e-01 +7.72e-01 +7.12e-01 +7.41e-01 +7.52e-01 +6.99e-01 +6.87e-01 +6.89e-01 +7.63e-01 +1.10e+00 +8.27e-01 +2.75e-01 +7.59e-01 +7.20e-01 +7.47e-01 +7.32e-01 +7.40e-01 +7.41e-01 +7.25e-01 +7.31e-01 +6.89e-01 +7.01e-01 +7.16e-01 +6.95e-01 +9.76e-01 +8.88e-01 +3.69e-01 +7.39e-01 +7.23e-01 +7.22e-01 +7.34e-01 +7.32e-01 +7.19e-01 +7.08e-01 +7.23e-01 +7.13e-01 +6.86e-01 +7.13e-01 +7.47e-01 +9.68e-01 +6.86e-01 +2.05e-01 +7.59e-01 +7.40e-01 +7.23e-01 +6.89e-01 +6.89e-01 +6.98e-01 +7.04e-01 +6.61e-01 +6.80e-01 +6.80e-01 +6.86e-01 +7.42e-01 +1.01e+00 +7.65e-01 +2.63e-01 +7.27e-01 +7.07e-01 +7.21e-01 +6.82e-01 +6.82e-01 +6.79e-01 +6.66e-01 +6.58e-01 +6.32e-01 +6.61e-01 +6.87e-01 +6.88e-01 +8.56e-01 +5.97e-01 +2.13e-01 +7.02e-01 +7.13e-01 +7.28e-01 +6.93e-01 +6.91e-01 +6.69e-01 +6.46e-01 +6.28e-01 +6.40e-01 +6.38e-01 +6.71e-01 +6.96e-01 +8.50e-01 +3.86e-01 +1.50e-01 +6.91e-01 +5.58e-01 +6.91e-01 +7.07e-01 +7.18e-01 +7.31e-01 +6.70e-01 +5.57e-01 +6.18e-01 +6.02e-01 +6.11e-01 +6.66e-01 +9.21e-01 +4.38e-01 +1.13e-01 +7.24e-01 +6.84e-01 +6.84e-01 +6.47e-01 +6.69e-01 +6.83e-01 +6.69e-01 +6.47e-01 +6.14e-01 +5.97e-01 +6.05e-01 +6.26e-01 +9.22e-01 +4.59e-01 +1.15e-01 +7.38e-01 +6.58e-01 +6.82e-01 +6.72e-01 +7.05e-01 +6.71e-01 +6.60e-01 +6.37e-01 +6.35e-01 +6.02e-01 +5.50e-01 +5.85e-01 +8.26e-01 +4.84e-01 +1.41e-01 +7.40e-01 +6.74e-01 +6.74e-01 +7.15e-01 +7.22e-01 +6.98e-01 +7.17e-01 +6.74e-01 +6.70e-01 +6.46e-01 +5.85e-01 +5.00e-01 +4.95e-01 +2.74e-01 +8.29e-02 +1.11e+00 +9.99e-01 +1.02e+00 +1.05e+00 +1.05e+00 +9.96e-01 +9.29e-01 +9.64e-01 +8.97e-01 +9.51e-01 +8.48e-01 +7.23e-01 +6.11e-01 +2.17e-01 +6.09e-02 +1.15e+00 +9.21e-01 +8.94e-01 +8.34e-01 +8.60e-01 +8.05e-01 +5.65e-01 +5.80e-01 +5.28e-01 +5.64e-01 +6.00e-01 +4.16e-01 +2.18e-01 +8.48e-02 +2.93e-02 +3.50e-01 +2.74e-01 +2.33e-01 +2.38e-01 +2.50e-01 +2.74e-01 +1.88e-01 +1.72e-01 +1.56e-01 +1.57e-01 +1.94e-01 +1.39e-01 +6.35e-02 +3.19e-02 +7.49e-03 +7.41e-01 +7.68e-01 +7.90e-01 +7.78e-01 +7.76e-01 +7.30e-01 +7.18e-01 +7.38e-01 +7.09e-01 +7.21e-01 +7.00e-01 +7.72e-01 +9.68e-01 +6.90e-01 +2.52e-01 +7.57e-01 +7.65e-01 +7.45e-01 +7.47e-01 +7.46e-01 +7.65e-01 +7.54e-01 +7.52e-01 +7.26e-01 +6.82e-01 +7.10e-01 +7.86e-01 +1.05e+00 +7.77e-01 +2.28e-01 +7.74e-01 +7.42e-01 +7.17e-01 +7.21e-01 +7.29e-01 +7.68e-01 +7.52e-01 +7.57e-01 +7.68e-01 +6.89e-01 +6.78e-01 +7.59e-01 +1.05e+00 +8.28e-01 +2.72e-01 +7.40e-01 +7.34e-01 +7.31e-01 +7.16e-01 +7.44e-01 +7.39e-01 +7.56e-01 +7.44e-01 +7.10e-01 +6.88e-01 +6.97e-01 +7.44e-01 +9.71e-01 +7.11e-01 +2.71e-01 +7.13e-01 +6.97e-01 +7.29e-01 +7.42e-01 +7.39e-01 +7.08e-01 +7.05e-01 +7.31e-01 +6.98e-01 +6.88e-01 +7.17e-01 +7.13e-01 +9.73e-01 +6.01e-01 +1.91e-01 +7.45e-01 +7.09e-01 +7.35e-01 +7.10e-01 +7.14e-01 +7.15e-01 +7.08e-01 +7.06e-01 +6.78e-01 +6.67e-01 +6.90e-01 +7.12e-01 +9.35e-01 +5.48e-01 +2.00e-01 +7.32e-01 +7.22e-01 +7.10e-01 +7.08e-01 +7.09e-01 +7.14e-01 +6.80e-01 +6.85e-01 +6.58e-01 +6.83e-01 +6.50e-01 +7.09e-01 +9.87e-01 +7.05e-01 +2.37e-01 +7.08e-01 +7.29e-01 +7.25e-01 +7.21e-01 +7.00e-01 +6.83e-01 +6.44e-01 +6.62e-01 +6.42e-01 +6.10e-01 +6.47e-01 +7.20e-01 +8.94e-01 +4.05e-01 +1.25e-01 +6.82e-01 +6.98e-01 +7.11e-01 +7.24e-01 +7.09e-01 +6.74e-01 +6.40e-01 +6.25e-01 +6.06e-01 +6.01e-01 +6.22e-01 +6.85e-01 +8.81e-01 +4.33e-01 +1.27e-01 +7.04e-01 +6.95e-01 +6.94e-01 +7.08e-01 +6.83e-01 +6.66e-01 +6.52e-01 +6.14e-01 +6.08e-01 +6.22e-01 +5.79e-01 +6.40e-01 +8.39e-01 +5.15e-01 +1.65e-01 +7.06e-01 +6.74e-01 +6.69e-01 +6.66e-01 +6.63e-01 +6.70e-01 +6.87e-01 +6.53e-01 +6.04e-01 +5.84e-01 +5.54e-01 +5.65e-01 +7.87e-01 +4.40e-01 +1.48e-01 +7.67e-01 +6.96e-01 +7.26e-01 +7.26e-01 +6.91e-01 +7.44e-01 +7.07e-01 +6.80e-01 +6.22e-01 +6.16e-01 +5.70e-01 +4.85e-01 +5.06e-01 +2.87e-01 +9.16e-02 +1.20e+00 +9.57e-01 +8.97e-01 +1.03e+00 +9.83e-01 +1.01e+00 +9.58e-01 +1.03e+00 +9.95e-01 +1.02e+00 +8.79e-01 +6.76e-01 +5.34e-01 +1.83e-01 +6.50e-02 +9.53e-01 +7.68e-01 +7.45e-01 +8.42e-01 +9.10e-01 +8.62e-01 +7.39e-01 +7.46e-01 +6.49e-01 +5.31e-01 +4.80e-01 +3.53e-01 +2.00e-01 +8.55e-02 +3.73e-02 +3.66e-01 +3.07e-01 +3.18e-01 +2.87e-01 +3.14e-01 +2.90e-01 +2.36e-01 +2.15e-01 +2.24e-01 +1.70e-01 +1.63e-01 +1.15e-01 +6.32e-02 +3.84e-02 +1.78e-02 +7.11e-01 +7.35e-01 +7.78e-01 +7.95e-01 +7.89e-01 +7.78e-01 +7.90e-01 +8.15e-01 +7.38e-01 +7.56e-01 +7.46e-01 +7.19e-01 +8.73e-01 +4.87e-01 +1.89e-01 +7.24e-01 +7.51e-01 +7.47e-01 +7.52e-01 +7.63e-01 +7.61e-01 +7.53e-01 +7.70e-01 +7.59e-01 +7.54e-01 +7.39e-01 +7.28e-01 +9.65e-01 +6.51e-01 +2.08e-01 +7.30e-01 +7.39e-01 +7.32e-01 +7.64e-01 +7.36e-01 +7.69e-01 +7.88e-01 +7.27e-01 +7.35e-01 +7.32e-01 +7.27e-01 +7.50e-01 +9.97e-01 +7.08e-01 +2.42e-01 +7.31e-01 +7.41e-01 +7.80e-01 +7.20e-01 +7.72e-01 +7.80e-01 +7.58e-01 +7.29e-01 +7.22e-01 +6.85e-01 +6.91e-01 +7.32e-01 +9.70e-01 +5.55e-01 +1.80e-01 +7.25e-01 +7.42e-01 +7.54e-01 +7.29e-01 +7.69e-01 +7.68e-01 +7.23e-01 +7.19e-01 +7.39e-01 +6.99e-01 +7.30e-01 +7.43e-01 +9.67e-01 +5.29e-01 +1.58e-01 +7.27e-01 +7.38e-01 +7.50e-01 +7.78e-01 +7.29e-01 +7.53e-01 +7.20e-01 +7.07e-01 +6.58e-01 +7.03e-01 +7.03e-01 +6.98e-01 +1.02e+00 +5.77e-01 +1.55e-01 +7.04e-01 +7.29e-01 +7.04e-01 +7.41e-01 +7.23e-01 +7.27e-01 +7.22e-01 +6.89e-01 +6.68e-01 +6.72e-01 +6.90e-01 +7.11e-01 +9.24e-01 +6.32e-01 +1.76e-01 +6.92e-01 +7.24e-01 +7.27e-01 +7.47e-01 +7.06e-01 +7.02e-01 +6.65e-01 +6.69e-01 +6.63e-01 +6.78e-01 +6.76e-01 +6.84e-01 +8.81e-01 +5.82e-01 +2.05e-01 +6.77e-01 +6.97e-01 +7.18e-01 +7.16e-01 +7.22e-01 +6.99e-01 +6.33e-01 +6.46e-01 +6.22e-01 +6.20e-01 +6.04e-01 +6.27e-01 +8.72e-01 +4.59e-01 +1.30e-01 +6.94e-01 +6.76e-01 +6.98e-01 +7.17e-01 +7.24e-01 +6.61e-01 +6.58e-01 +6.43e-01 +5.98e-01 +6.12e-01 +5.72e-01 +5.75e-01 +7.13e-01 +4.13e-01 +1.41e-01 +6.83e-01 +6.80e-01 +6.85e-01 +6.92e-01 +6.94e-01 +6.60e-01 +6.70e-01 +6.56e-01 +5.95e-01 +5.92e-01 +5.52e-01 +5.66e-01 +7.57e-01 +2.75e-01 +8.14e-02 +6.80e-01 +6.88e-01 +6.98e-01 +7.21e-01 +6.86e-01 +7.14e-01 +7.21e-01 +6.93e-01 +6.28e-01 +6.06e-01 +5.69e-01 +5.25e-01 +6.18e-01 +2.39e-01 +7.13e-02 +9.78e-01 +8.94e-01 +9.01e-01 +9.75e-01 +9.93e-01 +9.72e-01 +9.04e-01 +9.60e-01 +9.01e-01 +8.02e-01 +8.03e-01 +6.71e-01 +3.57e-01 +1.04e-01 +3.71e-02 +8.79e-01 +4.74e-01 +6.09e-01 +6.57e-01 +6.73e-01 +6.70e-01 +5.29e-01 +7.08e-01 +5.73e-01 +3.74e-01 +4.21e-01 +2.98e-01 +1.43e-01 +5.64e-02 +1.94e-02 +2.87e-01 +1.53e-01 +1.84e-01 +2.04e-01 +2.63e-01 +2.36e-01 +1.65e-01 +2.34e-01 +1.92e-01 +1.28e-01 +1.20e-01 +7.57e-02 +5.20e-02 +3.12e-02 +1.69e-02 +7.55e-01 +8.03e-01 +8.38e-01 +8.11e-01 +8.12e-01 +8.21e-01 +8.19e-01 +7.76e-01 +7.65e-01 +7.57e-01 +7.69e-01 +7.23e-01 +7.14e-01 +1.75e-01 +6.92e-02 +8.24e-01 +8.38e-01 +8.21e-01 +7.68e-01 +7.55e-01 +7.68e-01 +7.83e-01 +7.52e-01 +7.51e-01 +7.46e-01 +7.74e-01 +7.44e-01 +8.02e-01 +3.34e-01 +1.21e-01 +7.93e-01 +8.41e-01 +7.94e-01 +7.70e-01 +7.76e-01 +8.31e-01 +8.21e-01 +7.99e-01 +7.37e-01 +7.45e-01 +7.12e-01 +8.00e-01 +1.03e+00 +5.17e-01 +1.38e-01 +7.80e-01 +7.93e-01 +7.72e-01 +7.70e-01 +7.94e-01 +8.27e-01 +2.53e-01 +7.14e-01 +7.62e-01 +7.24e-01 +6.76e-01 +7.22e-01 +8.97e-01 +4.58e-01 +1.53e-01 +7.51e-01 +7.55e-01 +7.71e-01 +7.75e-01 +7.94e-01 +7.94e-01 +7.67e-01 +7.35e-01 +7.64e-01 +7.55e-01 +7.00e-01 +7.25e-01 +9.50e-01 +5.35e-01 +1.47e-01 +7.48e-01 +7.62e-01 +7.42e-01 +7.69e-01 +7.62e-01 +7.55e-01 +7.49e-01 +7.14e-01 +7.33e-01 +7.29e-01 +6.88e-01 +6.77e-01 +8.92e-01 +4.73e-01 +1.54e-01 +7.07e-01 +7.12e-01 +7.63e-01 +7.78e-01 +7.67e-01 +7.39e-01 +7.39e-01 +7.04e-01 +6.89e-01 +7.02e-01 +7.00e-01 +6.50e-01 +7.45e-01 +4.80e-01 +1.67e-01 +7.10e-01 +7.31e-01 +7.02e-01 +7.48e-01 +7.26e-01 +7.23e-01 +7.48e-01 +7.26e-01 +7.06e-01 +6.63e-01 +6.82e-01 +6.87e-01 +8.55e-01 +3.87e-01 +1.27e-01 +7.27e-01 +7.16e-01 +7.34e-01 +7.54e-01 +7.46e-01 +7.70e-01 +7.11e-01 +6.87e-01 +6.70e-01 +6.64e-01 +6.35e-01 +6.35e-01 +8.28e-01 +3.65e-01 +1.19e-01 +7.19e-01 +6.95e-01 +7.56e-01 +7.46e-01 +7.40e-01 +7.30e-01 +6.89e-01 +6.43e-01 +6.17e-01 +5.90e-01 +6.17e-01 +5.70e-01 +8.00e-01 +3.80e-01 +1.34e-01 +6.68e-01 +6.64e-01 +6.96e-01 +7.05e-01 +7.48e-01 +7.14e-01 +6.55e-01 +6.42e-01 +6.17e-01 +5.96e-01 +5.59e-01 +5.39e-01 +6.83e-01 +2.51e-01 +7.41e-02 +6.65e-01 +2.67e-01 +6.72e-01 +6.79e-01 +6.77e-01 +6.50e-01 +6.85e-01 +6.99e-01 +6.33e-01 +6.12e-01 +5.40e-01 +5.07e-01 +5.77e-01 +2.23e-01 +6.42e-02 +8.66e-01 +8.00e-01 +8.00e-01 +8.48e-01 +8.27e-01 +8.32e-01 +8.36e-01 +9.02e-01 +9.05e-01 +7.29e-01 +6.76e-01 +5.88e-01 +3.48e-01 +1.10e-01 +3.71e-02 +6.49e-01 +3.52e-01 +4.24e-01 +5.24e-01 +4.05e-01 +4.08e-01 +3.66e-01 +3.97e-01 +5.38e-01 +3.80e-01 +2.81e-01 +2.19e-01 +1.32e-01 +3.23e-02 +1.05e-02 +2.57e-01 +1.34e-01 +1.74e-01 +1.81e-01 +1.46e-01 +1.42e-01 +1.23e-01 +1.30e-01 +1.65e-01 +1.27e-01 +8.17e-02 +7.95e-02 +5.09e-02 +1.73e-02 +7.50e-03 +1.08e+00 +1.13e+00 +1.22e+00 +1.22e+00 +1.16e+00 +1.11e+00 +1.11e+00 +1.06e+00 +1.11e+00 +9.89e-01 +9.97e-01 +1.02e+00 +7.84e-01 +1.55e-01 +3.93e-02 +1.15e+00 +1.05e+00 +1.23e+00 +1.10e+00 +1.10e+00 +1.15e+00 +1.11e+00 +1.03e+00 +9.91e-01 +1.02e+00 +9.48e-01 +9.15e-01 +8.91e-01 +2.88e-01 +8.05e-02 +1.12e+00 +1.04e+00 +1.18e+00 +1.10e+00 +1.06e+00 +1.18e+00 +1.04e+00 +1.15e+00 +1.03e+00 +1.04e+00 +9.63e-01 +9.19e-01 +1.05e+00 +4.94e-01 +1.55e-01 +1.12e+00 +1.03e+00 +1.09e+00 +1.12e+00 +1.10e+00 +1.19e+00 +1.11e+00 +1.10e+00 +1.06e+00 +1.01e+00 +8.73e-01 +8.08e-01 +8.67e-01 +3.12e-01 +9.00e-02 +1.08e+00 +1.03e+00 +1.06e+00 +1.10e+00 +1.04e+00 +1.13e+00 +1.27e+00 +1.09e+00 +1.08e+00 +1.12e+00 +9.93e-01 +8.86e-01 +7.84e-01 +3.95e-01 +1.46e-01 +1.07e+00 +9.78e-01 +1.03e+00 +1.02e+00 +1.00e+00 +1.09e+00 +1.08e+00 +1.01e+00 +1.03e+00 +1.07e+00 +9.59e-01 +8.28e-01 +8.42e-01 +4.01e-01 +1.22e-01 +9.48e-01 +9.63e-01 +1.06e+00 +1.04e+00 +9.98e-01 +1.04e+00 +1.02e+00 +9.83e-01 +8.41e-01 +9.51e-01 +9.08e-01 +7.46e-01 +7.77e-01 +3.05e-01 +9.78e-02 +9.70e-01 +9.47e-01 +9.99e-01 +1.08e+00 +1.11e+00 +1.03e+00 +1.01e+00 +9.93e-01 +8.83e-01 +8.87e-01 +9.14e-01 +8.38e-01 +6.43e-01 +1.75e-01 +5.80e-02 +9.67e-01 +9.99e-01 +1.01e+00 +1.06e+00 +1.08e+00 +9.67e-01 +1.08e+00 +1.02e+00 +8.98e-01 +9.10e-01 +8.57e-01 +8.25e-01 +8.06e-01 +2.24e-01 +7.49e-02 +9.70e-01 +1.03e+00 +1.10e+00 +1.01e+00 +1.06e+00 +9.81e-01 +1.01e+00 +9.04e-01 +8.30e-01 +8.02e-01 +8.09e-01 +8.10e-01 +7.80e-01 +2.46e-01 +7.63e-02 +8.90e-01 +9.28e-01 +1.02e+00 +1.02e+00 +9.83e-01 +1.07e+00 +9.40e-01 +8.16e-01 +7.84e-01 +6.91e-01 +7.33e-01 +6.96e-01 +5.05e-01 +2.10e-01 +7.72e-02 +7.36e-01 +7.62e-01 +8.87e-01 +8.71e-01 +9.21e-01 +9.57e-01 +1.00e+00 +9.26e-01 +8.38e-01 +7.10e-01 +6.68e-01 +6.38e-01 +4.88e-01 +1.47e-01 +5.84e-02 +6.38e-01 +6.97e-01 +7.81e-01 +7.21e-01 +8.22e-01 +8.29e-01 +6.94e-01 +9.33e-01 +9.36e-01 +7.85e-01 +5.91e-01 +4.82e-01 +3.15e-01 +9.11e-02 +4.04e-02 +3.59e-01 +2.19e-01 +2.60e-01 +3.06e-01 +2.48e-01 +2.41e-01 +2.20e-01 +2.69e-01 +4.39e-01 +2.89e-01 +1.35e-01 +1.30e-01 +7.47e-02 +2.46e-02 +1.39e-02 +1.37e-01 +7.05e-02 +9.05e-02 +9.63e-02 +7.38e-02 +7.57e-02 +7.98e-02 +7.83e-02 +1.40e-01 +1.09e-01 +5.52e-02 +4.14e-02 +2.38e-02 +1.27e-02 +4.98e-03 +1.08e+00 +1.06e+00 +1.34e+00 +9.87e-01 +1.49e+00 +1.22e+00 +1.08e+00 +1.13e+00 +9.74e-01 +8.45e-01 +6.07e-01 +6.33e-01 +5.34e-01 +1.43e-01 +3.98e-02 +1.35e+00 +9.68e-01 +1.60e+00 +1.80e+00 +1.45e+00 +1.17e+00 +9.94e-01 +1.02e+00 +8.65e-01 +7.01e-01 +6.46e-01 +5.40e-01 +4.50e-01 +2.04e-01 +7.38e-02 +1.18e+00 +1.14e+00 +1.48e+00 +1.19e+00 +9.38e-01 +1.19e+00 +9.74e-01 +1.02e+00 +9.23e-01 +8.23e-01 +8.89e-01 +7.01e-01 +3.85e-01 +2.16e-01 +1.44e-01 +1.32e+00 +1.14e+00 +1.36e+00 +1.53e+00 +1.16e+00 +1.29e+00 +1.36e+00 +1.18e+00 +1.09e+00 +8.67e-01 +6.32e-01 +4.57e-01 +3.13e-01 +1.52e-01 +6.87e-02 +1.30e+00 +9.74e-01 +1.29e+00 +1.54e+00 +1.33e+00 +1.38e+00 +1.35e+00 +1.13e+00 +9.14e-01 +8.56e-01 +7.71e-01 +4.06e-01 +2.63e-01 +1.30e-01 +6.54e-02 +1.05e+00 +7.94e-01 +1.12e+00 +1.15e+00 +8.13e-01 +9.20e-01 +1.09e+00 +8.11e-01 +6.93e-01 +8.96e-01 +8.00e-01 +5.26e-01 +2.87e-01 +1.23e-01 +7.75e-02 +7.48e-01 +9.01e-01 +1.04e+00 +1.05e+00 +9.13e-01 +1.00e+00 +9.12e-01 +7.83e-01 +6.62e-01 +6.12e-01 +4.88e-01 +3.46e-01 +2.31e-01 +1.33e-01 +7.44e-02 +5.62e-01 +7.11e-01 +8.51e-01 +8.66e-01 +9.29e-01 +9.65e-01 +6.70e-01 +6.37e-01 +4.77e-01 +4.37e-01 +4.96e-01 +3.94e-01 +2.26e-01 +1.06e-01 +3.96e-02 +6.10e-01 +6.97e-01 +8.44e-01 +1.11e+00 +8.69e-01 +7.98e-01 +6.03e-01 +7.36e-01 +5.96e-01 +5.37e-01 +4.95e-01 +4.22e-01 +2.94e-01 +1.01e-01 +4.37e-02 +4.56e-01 +8.07e-01 +1.07e+00 +8.05e-01 +7.16e-01 +5.03e-01 +6.26e-01 +7.10e-01 +5.73e-01 +4.58e-01 +4.44e-01 +3.45e-01 +2.60e-01 +1.36e-01 +4.22e-02 +4.93e-01 +6.13e-01 +8.04e-01 +5.55e-01 +7.11e-01 +7.23e-01 +5.94e-01 +5.56e-01 +4.34e-01 +2.96e-01 +3.80e-01 +3.42e-01 +2.09e-01 +1.08e-01 +4.84e-02 +4.28e-01 +3.61e-01 +5.20e-01 +4.86e-01 +4.81e-01 +6.92e-01 +5.95e-01 +3.74e-01 +3.26e-01 +2.76e-01 +2.59e-01 +2.57e-01 +1.85e-01 +4.96e-02 +1.83e-02 +2.15e-01 +2.66e-01 +3.14e-01 +2.45e-01 +2.95e-01 +4.30e-01 +3.75e-01 +3.67e-01 +3.05e-01 +3.09e-01 +2.21e-01 +1.80e-01 +1.34e-01 +5.03e-02 +2.00e-02 +9.60e-02 +1.01e-01 +1.15e-01 +9.88e-02 +1.27e-01 +1.67e-01 +1.28e-01 +1.60e-01 +1.75e-01 +1.43e-01 +8.60e-02 +5.91e-02 +4.19e-02 +2.09e-02 +1.01e-02 +6.92e-02 +3.71e-02 +4.18e-02 +4.29e-02 +4.54e-02 +6.16e-02 +4.09e-02 +5.12e-02 +7.23e-02 +6.74e-02 +3.14e-02 +1.80e-02 +1.64e-02 +7.47e-03 +2.97e-03 +3.66e-01 +3.67e-01 +4.98e-01 +3.34e-01 +5.46e-01 +4.10e-01 +3.75e-01 +3.97e-01 +3.66e-01 +2.85e-01 +1.96e-01 +1.98e-01 +1.79e-01 +1.05e-01 +3.65e-02 +5.51e-01 +2.62e-01 +5.35e-01 +7.29e-01 +6.22e-01 +4.43e-01 +3.56e-01 +3.26e-01 +3.18e-01 +2.53e-01 +1.98e-01 +1.61e-01 +1.35e-01 +8.96e-02 +4.34e-02 +4.38e-01 +3.47e-01 +5.06e-01 +4.90e-01 +2.64e-01 +4.25e-01 +3.58e-01 +3.11e-01 +3.30e-01 +2.32e-01 +3.09e-01 +2.65e-01 +1.43e-01 +6.12e-02 +6.39e-02 +4.34e-01 +3.64e-01 +5.30e-01 +6.02e-01 +4.03e-01 +4.34e-01 +4.80e-01 +4.48e-01 +4.35e-01 +3.28e-01 +2.19e-01 +1.55e-01 +1.04e-01 +6.22e-02 +2.93e-02 +5.28e-01 +3.92e-01 +4.89e-01 +5.69e-01 +5.08e-01 +4.82e-01 +5.01e-01 +3.73e-01 +3.58e-01 +2.50e-01 +2.62e-01 +1.19e-01 +7.33e-02 +4.35e-02 +2.75e-02 +3.24e-01 +2.74e-01 +3.53e-01 +3.82e-01 +3.09e-01 +2.64e-01 +3.42e-01 +2.59e-01 +2.24e-01 +2.72e-01 +2.89e-01 +2.17e-01 +1.01e-01 +4.22e-02 +2.14e-02 +3.08e-01 +2.90e-01 +3.71e-01 +4.11e-01 +2.41e-01 +2.90e-01 +2.99e-01 +2.76e-01 +2.53e-01 +2.06e-01 +1.62e-01 +1.37e-01 +7.91e-02 +3.97e-02 +3.67e-02 +1.54e-01 +2.67e-01 +2.58e-01 +2.66e-01 +2.93e-01 +3.18e-01 +1.96e-01 +2.10e-01 +1.51e-01 +1.19e-01 +1.30e-01 +1.25e-01 +6.88e-02 +5.68e-02 +4.11e-02 +1.94e-01 +2.10e-01 +2.78e-01 +3.44e-01 +3.33e-01 +3.12e-01 +1.89e-01 +2.43e-01 +2.08e-01 +1.74e-01 +1.46e-01 +1.34e-01 +1.11e-01 +4.65e-02 +1.94e-02 +1.16e-01 +2.46e-01 +3.24e-01 +3.12e-01 +2.29e-01 +1.48e-01 +2.01e-01 +2.33e-01 +2.12e-01 +1.40e-01 +1.33e-01 +1.13e-01 +6.83e-02 +6.78e-02 +3.69e-02 +1.55e-01 +2.51e-01 +2.94e-01 +1.53e-01 +2.32e-01 +2.02e-01 +1.35e-01 +1.89e-01 +1.55e-01 +1.09e-01 +1.17e-01 +9.56e-02 +6.95e-02 +5.08e-02 +3.96e-02 +1.13e-01 +1.22e-01 +1.62e-01 +1.45e-01 +1.49e-01 +2.36e-01 +2.03e-01 +1.50e-01 +1.01e-01 +8.51e-02 +7.83e-02 +7.56e-02 +7.49e-02 +2.08e-02 +8.15e-03 +9.29e-02 +9.19e-02 +1.09e-01 +1.23e-01 +9.19e-02 +1.58e-01 +1.36e-01 +1.13e-01 +8.64e-02 +1.01e-01 +5.95e-02 +6.00e-02 +5.68e-02 +2.10e-02 +7.24e-03 +3.10e-02 +4.84e-02 +4.87e-02 +3.68e-02 +5.75e-02 +7.85e-02 +7.07e-02 +8.18e-02 +9.39e-02 +5.13e-02 +5.46e-02 +3.42e-02 +2.14e-02 +1.34e-02 +1.14e-02 +2.27e-02 +2.51e-02 +2.70e-02 +2.56e-02 +3.02e-02 +5.07e-02 +3.42e-02 +3.18e-02 +5.82e-02 +3.83e-02 +2.90e-02 +1.85e-02 +7.38e-03 +5.00e-03 +7.27e-03 \ No newline at end of file diff --git a/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/inputs_true.dat b/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/inputs_true.dat new file mode 100644 index 00000000000..6aa91ca798a --- /dev/null +++ b/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/inputs_true.dat @@ -0,0 +1,265 @@ + + + + mgxs.h5 + + + + + + + + + + + + + + + + + + + + + 2.5 2.5 2.5 + 12 12 12 + 0.0 0.0 0.0 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 +1 1 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 +2 2 2 2 2 2 2 2 2 2 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 +3 3 3 3 3 3 3 3 3 3 3 3 + + + + + + + + + + fixed source + 750 + 30 + 20 + + + 100.0 1.0 + + + universe + 1 + + + multi-group + + + 1 + neutron + 10 + 1 + true + fw_cadis + + + + 15 15 15 + 0.0 0.0 0.0 + 30.0 30.0 30.0 + + + 500.0 + 100.0 + + + 0.0 0.0 0.0 30.0 30.0 30.0 + + + True + + + + + + linear + + + + + 1 + + + 2 + + + 3 + + + 3 + flux + tracklength + + + 2 + flux + tracklength + + + 1 + flux + tracklength + + + diff --git a/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/results_true.dat b/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/results_true.dat new file mode 100644 index 00000000000..3afdc56ca66 --- /dev/null +++ b/tests/regression_tests/weightwindows_fw_cadis_mesh/linear/results_true.dat @@ -0,0 +1,6760 @@ +RegularMesh + ID = 1 + Name = + Dimensions = 3 + Voxels = [15 15 15] + Lower left = [0. 0. 0.] + Upper Right = [np.float64(30.0), np.float64(30.0), np.float64(30.0)] + Width = [2. 2. 2.] +Lower Bounds +1.77e-01 +1.81e-01 +1.81e-01 +1.81e-01 +1.78e-01 +1.74e-01 +1.70e-01 +1.76e-01 +1.80e-01 +1.88e-01 +1.75e-01 +1.93e-01 +2.76e-01 +4.28e-01 +1.58e-01 +1.74e-01 +1.82e-01 +1.81e-01 +1.63e-01 +1.78e-01 +1.81e-01 +1.77e-01 +1.77e-01 +1.74e-01 +1.72e-01 +1.81e-01 +1.82e-01 +2.58e-01 +3.27e-01 +1.07e-01 +1.82e-01 +1.75e-01 +1.81e-01 +1.82e-01 +1.76e-01 +1.77e-01 +1.74e-01 +1.66e-01 +1.66e-01 +1.69e-01 +1.76e-01 +1.87e-01 +2.67e-01 +4.16e-01 +1.57e-01 +1.76e-01 +1.74e-01 +1.72e-01 +1.76e-01 +1.67e-01 +1.68e-01 +1.70e-01 +1.75e-01 +1.73e-01 +1.73e-01 +1.65e-01 +1.84e-01 +2.67e-01 +3.99e-01 +1.28e-01 +1.75e-01 +1.82e-01 +1.82e-01 +1.84e-01 +1.72e-01 +1.72e-01 +1.67e-01 +1.67e-01 +1.66e-01 +1.74e-01 +1.69e-01 +1.90e-01 +2.82e-01 +3.31e-01 +1.29e-01 +1.80e-01 +1.79e-01 +1.79e-01 +1.78e-01 +1.76e-01 +1.69e-01 +1.68e-01 +1.63e-01 +1.70e-01 +1.66e-01 +1.71e-01 +1.92e-01 +2.70e-01 +2.88e-01 +1.02e-01 +1.76e-01 +1.72e-01 +1.68e-01 +1.71e-01 +1.66e-01 +1.68e-01 +1.65e-01 +1.66e-01 +1.62e-01 +1.60e-01 +1.61e-01 +1.66e-01 +2.39e-01 +3.32e-01 +1.23e-01 +1.82e-01 +1.81e-01 +1.79e-01 +1.70e-01 +1.67e-01 +1.72e-01 +1.65e-01 +1.62e-01 +1.47e-01 +1.56e-01 +1.56e-01 +1.44e-01 +2.01e-01 +1.76e-01 +7.55e-02 +1.76e-01 +1.76e-01 +1.80e-01 +1.82e-01 +1.71e-01 +1.67e-01 +1.65e-01 +1.62e-01 +1.54e-01 +1.55e-01 +1.54e-01 +1.55e-01 +2.28e-01 +1.67e-01 +5.36e-02 +1.77e-01 +1.85e-01 +1.83e-01 +1.81e-01 +1.79e-01 +1.61e-01 +1.64e-01 +1.58e-01 +1.61e-01 +1.60e-01 +1.55e-01 +1.53e-01 +2.33e-01 +1.74e-01 +6.51e-02 +1.81e-01 +1.79e-01 +1.83e-01 +1.79e-01 +1.80e-01 +1.69e-01 +1.58e-01 +1.66e-01 +1.59e-01 +1.59e-01 +1.70e-01 +1.56e-01 +2.11e-01 +1.28e-01 +3.74e-02 +1.90e-01 +1.90e-01 +1.82e-01 +1.71e-01 +1.90e-01 +1.82e-01 +1.72e-01 +1.79e-01 +1.69e-01 +1.69e-01 +1.66e-01 +1.51e-01 +2.22e-01 +9.62e-02 +3.14e-02 +2.75e-01 +2.73e-01 +2.51e-01 +2.62e-01 +2.73e-01 +2.67e-01 +2.53e-01 +2.43e-01 +2.41e-01 +2.42e-01 +2.39e-01 +2.23e-01 +2.12e-01 +7.28e-02 +2.10e-02 +5.00e-01 +3.50e-01 +3.91e-01 +2.55e-01 +2.81e-01 +3.18e-01 +2.54e-01 +2.37e-01 +1.86e-01 +1.97e-01 +1.81e-01 +1.50e-01 +9.74e-02 +3.76e-02 +1.86e-02 +2.17e-01 +1.42e-01 +1.63e-01 +8.18e-02 +9.18e-02 +1.14e-01 +8.12e-02 +7.94e-02 +6.97e-02 +6.23e-02 +6.28e-02 +4.48e-02 +3.69e-02 +1.59e-02 +7.16e-03 +1.71e-01 +1.76e-01 +1.78e-01 +1.74e-01 +1.77e-01 +1.76e-01 +1.81e-01 +1.77e-01 +1.69e-01 +1.76e-01 +1.83e-01 +1.84e-01 +2.71e-01 +3.99e-01 +1.54e-01 +1.79e-01 +1.79e-01 +1.80e-01 +1.75e-01 +1.76e-01 +1.71e-01 +1.74e-01 +1.74e-01 +1.72e-01 +1.73e-01 +1.77e-01 +1.85e-01 +2.54e-01 +3.67e-01 +1.34e-01 +1.80e-01 +1.76e-01 +1.74e-01 +1.77e-01 +1.76e-01 +1.77e-01 +1.74e-01 +1.78e-01 +1.72e-01 +1.80e-01 +1.81e-01 +1.81e-01 +2.66e-01 +3.32e-01 +1.15e-01 +1.82e-01 +1.86e-01 +1.81e-01 +1.83e-01 +1.73e-01 +1.67e-01 +1.72e-01 +1.75e-01 +1.70e-01 +1.69e-01 +1.75e-01 +1.84e-01 +2.58e-01 +3.55e-01 +1.26e-01 +1.80e-01 +1.82e-01 +1.79e-01 +1.73e-01 +1.66e-01 +1.70e-01 +1.77e-01 +1.69e-01 +1.68e-01 +1.69e-01 +1.78e-01 +1.83e-01 +2.75e-01 +3.02e-01 +1.32e-01 +1.75e-01 +1.78e-01 +1.77e-01 +1.69e-01 +1.79e-01 +1.69e-01 +1.65e-01 +1.63e-01 +1.63e-01 +1.67e-01 +1.74e-01 +1.87e-01 +2.69e-01 +3.43e-01 +1.20e-01 +1.80e-01 +1.82e-01 +1.68e-01 +1.67e-01 +1.77e-01 +1.70e-01 +1.59e-01 +1.60e-01 +1.58e-01 +1.61e-01 +1.65e-01 +1.67e-01 +2.34e-01 +2.93e-01 +1.13e-01 +1.71e-01 +1.72e-01 +1.65e-01 +1.65e-01 +1.65e-01 +1.60e-01 +1.57e-01 +1.46e-01 +1.43e-01 +1.50e-01 +1.48e-01 +1.06e-01 +1.93e-01 +1.68e-01 +7.02e-02 +1.69e-01 +1.86e-01 +1.75e-01 +1.74e-01 +1.77e-01 +1.65e-01 +1.61e-01 +1.54e-01 +1.53e-01 +1.55e-01 +1.47e-01 +1.41e-01 +1.98e-01 +1.23e-01 +3.84e-02 +1.78e-01 +1.86e-01 +1.76e-01 +1.77e-01 +1.65e-01 +1.75e-01 +1.66e-01 +1.54e-01 +1.59e-01 +1.54e-01 +1.63e-01 +1.52e-01 +2.17e-01 +1.45e-01 +4.33e-02 +1.80e-01 +1.87e-01 +1.79e-01 +1.73e-01 +1.81e-01 +1.76e-01 +1.67e-01 +1.60e-01 +1.63e-01 +1.60e-01 +1.69e-01 +1.55e-01 +2.16e-01 +1.34e-01 +4.11e-02 +1.88e-01 +1.80e-01 +1.84e-01 +1.81e-01 +1.84e-01 +1.87e-01 +1.74e-01 +1.69e-01 +1.66e-01 +1.73e-01 +1.70e-01 +1.62e-01 +2.01e-01 +8.29e-02 +2.86e-02 +2.58e-01 +2.81e-01 +2.59e-01 +2.69e-01 +2.95e-01 +2.69e-01 +2.46e-01 +2.56e-01 +2.33e-01 +2.31e-01 +2.59e-01 +2.41e-01 +2.08e-01 +5.36e-02 +1.40e-02 +3.12e-01 +2.97e-01 +2.71e-01 +3.04e-01 +3.70e-01 +3.54e-01 +2.77e-01 +2.79e-01 +2.19e-01 +1.62e-01 +1.62e-01 +1.63e-01 +1.08e-01 +3.47e-02 +1.48e-02 +1.09e-01 +1.08e-01 +1.05e-01 +1.01e-01 +1.35e-01 +1.42e-01 +9.52e-02 +9.99e-02 +9.32e-02 +5.18e-02 +5.18e-02 +5.34e-02 +4.23e-02 +1.87e-02 +7.19e-03 +1.80e-01 +1.75e-01 +1.82e-01 +1.75e-01 +1.71e-01 +1.70e-01 +1.69e-01 +1.65e-01 +1.75e-01 +1.71e-01 +1.68e-01 +1.86e-01 +3.12e-01 +4.27e-01 +1.66e-01 +1.81e-01 +1.77e-01 +1.70e-01 +1.73e-01 +1.73e-01 +1.72e-01 +1.64e-01 +1.66e-01 +1.66e-01 +1.73e-01 +1.70e-01 +1.89e-01 +2.63e-01 +4.02e-01 +1.64e-01 +1.79e-01 +1.80e-01 +1.83e-01 +1.71e-01 +1.73e-01 +1.71e-01 +1.70e-01 +1.75e-01 +1.69e-01 +1.68e-01 +1.68e-01 +1.80e-01 +2.49e-01 +3.42e-01 +1.29e-01 +1.63e-01 +1.73e-01 +1.74e-01 +1.71e-01 +1.68e-01 +1.65e-01 +1.68e-01 +1.73e-01 +1.72e-01 +1.65e-01 +1.72e-01 +1.69e-01 +2.52e-01 +3.19e-01 +1.06e-01 +1.67e-01 +1.70e-01 +1.72e-01 +1.64e-01 +1.71e-01 +1.71e-01 +1.57e-01 +1.64e-01 +1.61e-01 +1.62e-01 +1.73e-01 +1.79e-01 +2.63e-01 +2.66e-01 +7.74e-02 +1.68e-01 +1.72e-01 +1.64e-01 +1.66e-01 +1.65e-01 +1.66e-01 +1.58e-01 +1.59e-01 +1.63e-01 +1.63e-01 +1.70e-01 +1.75e-01 +2.51e-01 +3.47e-01 +1.31e-01 +1.72e-01 +1.79e-01 +1.70e-01 +1.70e-01 +1.69e-01 +1.71e-01 +1.70e-01 +1.55e-01 +1.59e-01 +1.65e-01 +1.70e-01 +1.71e-01 +2.43e-01 +2.46e-01 +9.20e-02 +1.70e-01 +1.70e-01 +1.66e-01 +1.63e-01 +1.66e-01 +1.63e-01 +1.59e-01 +1.58e-01 +1.57e-01 +1.60e-01 +1.55e-01 +1.53e-01 +2.20e-01 +1.63e-01 +6.37e-02 +1.70e-01 +1.70e-01 +1.73e-01 +1.69e-01 +1.66e-01 +1.64e-01 +1.63e-01 +1.64e-01 +1.52e-01 +1.54e-01 +1.53e-01 +1.49e-01 +2.12e-01 +1.64e-01 +6.14e-02 +1.74e-01 +1.78e-01 +1.73e-01 +1.75e-01 +1.74e-01 +1.65e-01 +1.67e-01 +1.59e-01 +1.48e-01 +1.57e-01 +1.55e-01 +1.59e-01 +2.06e-01 +1.23e-01 +4.80e-02 +1.69e-01 +1.78e-01 +1.77e-01 +1.74e-01 +1.77e-01 +1.73e-01 +1.74e-01 +1.68e-01 +1.52e-01 +1.58e-01 +1.58e-01 +1.55e-01 +2.12e-01 +1.22e-01 +3.34e-02 +1.75e-01 +1.85e-01 +1.86e-01 +1.90e-01 +1.80e-01 +1.79e-01 +1.76e-01 +1.71e-01 +1.67e-01 +1.71e-01 +1.69e-01 +1.69e-01 +1.86e-01 +1.02e-01 +3.54e-02 +2.66e-01 +2.66e-01 +2.64e-01 +2.50e-01 +2.60e-01 +2.58e-01 +2.34e-01 +2.42e-01 +2.35e-01 +2.29e-01 +2.33e-01 +2.42e-01 +2.28e-01 +5.44e-02 +1.71e-02 +3.77e-01 +2.75e-01 +3.41e-01 +3.51e-01 +3.64e-01 +2.31e-01 +2.32e-01 +2.18e-01 +1.82e-01 +1.77e-01 +1.28e-01 +1.16e-01 +1.03e-01 +4.16e-02 +1.06e-02 +1.33e-01 +8.71e-02 +1.41e-01 +1.34e-01 +1.37e-01 +8.83e-02 +7.82e-02 +8.36e-02 +5.61e-02 +6.39e-02 +2.90e-02 +3.69e-02 +3.46e-02 +2.60e-02 +1.24e-02 +1.77e-01 +1.71e-01 +1.75e-01 +1.79e-01 +1.73e-01 +1.71e-01 +1.64e-01 +1.66e-01 +1.59e-01 +1.61e-01 +1.55e-01 +1.79e-01 +2.67e-01 +4.26e-01 +1.56e-01 +1.75e-01 +1.82e-01 +1.71e-01 +1.65e-01 +1.69e-01 +1.66e-01 +1.69e-01 +1.63e-01 +1.65e-01 +1.74e-01 +1.69e-01 +1.76e-01 +2.47e-01 +3.31e-01 +1.30e-01 +1.69e-01 +1.73e-01 +1.77e-01 +1.67e-01 +1.55e-01 +1.67e-01 +1.59e-01 +1.63e-01 +1.58e-01 +1.70e-01 +1.70e-01 +1.84e-01 +2.44e-01 +2.71e-01 +1.02e-01 +1.69e-01 +1.70e-01 +1.73e-01 +1.69e-01 +1.66e-01 +1.67e-01 +1.64e-01 +1.60e-01 +1.62e-01 +1.63e-01 +1.63e-01 +1.67e-01 +2.48e-01 +2.40e-01 +9.84e-02 +1.62e-01 +1.64e-01 +1.69e-01 +1.71e-01 +1.62e-01 +1.58e-01 +1.62e-01 +1.58e-01 +1.62e-01 +1.61e-01 +1.71e-01 +1.67e-01 +2.50e-01 +3.22e-01 +1.17e-01 +1.74e-01 +1.60e-01 +1.69e-01 +1.72e-01 +1.67e-01 +1.60e-01 +1.65e-01 +1.68e-01 +1.62e-01 +1.60e-01 +1.63e-01 +1.76e-01 +2.53e-01 +2.99e-01 +1.10e-01 +1.69e-01 +1.67e-01 +1.76e-01 +1.59e-01 +1.59e-01 +1.58e-01 +1.64e-01 +1.59e-01 +1.55e-01 +1.59e-01 +1.51e-01 +1.60e-01 +2.50e-01 +2.88e-01 +1.01e-01 +1.72e-01 +1.71e-01 +1.68e-01 +1.63e-01 +1.59e-01 +1.61e-01 +1.66e-01 +1.56e-01 +1.54e-01 +1.51e-01 +1.49e-01 +1.58e-01 +2.33e-01 +1.85e-01 +6.62e-02 +1.69e-01 +1.65e-01 +1.63e-01 +1.67e-01 +1.62e-01 +1.63e-01 +1.63e-01 +1.61e-01 +1.55e-01 +1.53e-01 +1.59e-01 +1.69e-01 +2.48e-01 +1.87e-01 +5.29e-02 +1.71e-01 +1.66e-01 +1.73e-01 +1.68e-01 +1.65e-01 +1.64e-01 +1.59e-01 +1.52e-01 +1.60e-01 +1.52e-01 +1.48e-01 +1.54e-01 +2.32e-01 +1.35e-01 +5.41e-02 +1.76e-01 +1.78e-01 +1.68e-01 +1.78e-01 +1.73e-01 +1.69e-01 +1.62e-01 +1.61e-01 +1.59e-01 +1.61e-01 +1.58e-01 +1.60e-01 +2.06e-01 +9.41e-02 +2.78e-02 +1.88e-01 +1.77e-01 +1.78e-01 +8.59e-02 +1.82e-01 +1.90e-01 +1.76e-01 +1.63e-01 +1.69e-01 +1.69e-01 +1.69e-01 +1.68e-01 +1.97e-01 +7.83e-02 +2.27e-02 +2.47e-01 +2.50e-01 +2.59e-01 +2.51e-01 +2.57e-01 +2.55e-01 +2.15e-01 +2.28e-01 +2.42e-01 +2.25e-01 +2.12e-01 +2.14e-01 +2.28e-01 +7.82e-02 +2.46e-02 +3.10e-01 +2.49e-01 +2.07e-01 +2.92e-01 +2.88e-01 +2.33e-01 +1.62e-01 +1.13e-01 +1.90e-01 +1.80e-01 +1.16e-01 +1.07e-01 +9.41e-02 +4.21e-02 +1.63e-02 +1.17e-01 +8.58e-02 +8.78e-02 +9.13e-02 +1.15e-01 +8.57e-02 +6.78e-02 +3.63e-02 +6.34e-02 +6.38e-02 +4.06e-02 +3.60e-02 +2.77e-02 +2.56e-02 +1.21e-02 +1.71e-01 +1.68e-01 +1.63e-01 +1.68e-01 +1.72e-01 +1.66e-01 +1.65e-01 +1.51e-01 +1.57e-01 +1.63e-01 +1.57e-01 +1.78e-01 +2.58e-01 +3.15e-01 +1.24e-01 +1.72e-01 +1.71e-01 +1.66e-01 +1.69e-01 +1.71e-01 +1.63e-01 +1.58e-01 +1.51e-01 +1.62e-01 +1.68e-01 +1.67e-01 +1.76e-01 +2.54e-01 +3.04e-01 +1.12e-01 +1.75e-01 +1.72e-01 +1.70e-01 +1.68e-01 +1.63e-01 +1.64e-01 +1.63e-01 +1.54e-01 +1.65e-01 +1.69e-01 +1.68e-01 +1.76e-01 +2.52e-01 +2.21e-01 +8.02e-02 +1.68e-01 +1.71e-01 +1.65e-01 +1.66e-01 +1.63e-01 +1.64e-01 +1.61e-01 +1.62e-01 +1.61e-01 +1.66e-01 +1.64e-01 +1.72e-01 +2.60e-01 +2.09e-01 +7.14e-02 +1.63e-01 +1.61e-01 +1.63e-01 +1.69e-01 +1.61e-01 +1.62e-01 +1.62e-01 +1.61e-01 +1.58e-01 +1.54e-01 +1.64e-01 +1.76e-01 +2.59e-01 +3.27e-01 +1.16e-01 +1.60e-01 +1.60e-01 +1.63e-01 +1.68e-01 +1.62e-01 +1.54e-01 +1.50e-01 +1.64e-01 +1.63e-01 +1.58e-01 +1.58e-01 +1.68e-01 +2.43e-01 +2.59e-01 +9.33e-02 +1.61e-01 +1.63e-01 +1.68e-01 +1.62e-01 +1.69e-01 +1.53e-01 +1.53e-01 +1.57e-01 +1.50e-01 +1.54e-01 +1.57e-01 +1.59e-01 +2.17e-01 +2.06e-01 +7.58e-02 +1.71e-01 +1.70e-01 +1.63e-01 +1.57e-01 +1.61e-01 +1.53e-01 +1.56e-01 +1.49e-01 +1.49e-01 +1.59e-01 +1.49e-01 +1.55e-01 +2.27e-01 +1.62e-01 +6.88e-02 +1.71e-01 +1.67e-01 +1.66e-01 +1.60e-01 +1.61e-01 +1.62e-01 +1.63e-01 +1.54e-01 +1.50e-01 +1.48e-01 +1.51e-01 +1.55e-01 +2.15e-01 +1.65e-01 +4.95e-02 +1.71e-01 +1.73e-01 +1.68e-01 +1.62e-01 +1.57e-01 +1.62e-01 +1.59e-01 +1.56e-01 +1.53e-01 +1.47e-01 +1.49e-01 +1.62e-01 +2.14e-01 +1.58e-01 +6.86e-02 +1.70e-01 +1.66e-01 +1.69e-01 +1.66e-01 +1.67e-01 +1.62e-01 +1.63e-01 +1.63e-01 +1.58e-01 +1.52e-01 +1.54e-01 +1.67e-01 +2.26e-01 +1.15e-01 +4.03e-02 +1.74e-01 +1.82e-01 +1.70e-01 +1.72e-01 +1.77e-01 +1.85e-01 +1.68e-01 +1.65e-01 +1.63e-01 +1.74e-01 +1.63e-01 +1.66e-01 +2.29e-01 +1.07e-01 +3.27e-02 +2.58e-01 +2.47e-01 +2.50e-01 +2.57e-01 +2.60e-01 +2.66e-01 +2.59e-01 +2.39e-01 +2.35e-01 +2.29e-01 +2.16e-01 +2.25e-01 +2.33e-01 +8.96e-02 +3.01e-02 +2.73e-01 +2.51e-01 +2.52e-01 +2.63e-01 +3.05e-01 +2.69e-01 +2.91e-01 +1.85e-01 +2.00e-01 +1.47e-01 +1.01e-01 +1.17e-01 +8.80e-02 +3.85e-02 +1.53e-02 +1.00e-01 +8.71e-02 +9.09e-02 +9.00e-02 +1.08e-01 +8.70e-02 +1.17e-01 +5.47e-02 +7.04e-02 +5.07e-02 +3.08e-02 +3.14e-02 +2.64e-02 +1.69e-02 +1.16e-02 +1.60e-01 +1.64e-01 +1.63e-01 +1.67e-01 +1.70e-01 +1.62e-01 +1.54e-01 +1.52e-01 +1.52e-01 +1.55e-01 +1.57e-01 +1.63e-01 +2.61e-01 +2.58e-01 +7.58e-02 +1.59e-01 +1.68e-01 +1.63e-01 +1.58e-01 +1.65e-01 +1.65e-01 +1.67e-01 +1.52e-01 +1.58e-01 +1.56e-01 +1.60e-01 +1.69e-01 +2.49e-01 +2.88e-01 +1.11e-01 +1.66e-01 +1.69e-01 +1.70e-01 +1.63e-01 +1.63e-01 +1.59e-01 +1.63e-01 +1.65e-01 +1.58e-01 +1.58e-01 +1.60e-01 +1.75e-01 +2.57e-01 +2.53e-01 +8.19e-02 +1.67e-01 +1.68e-01 +1.58e-01 +1.58e-01 +1.61e-01 +1.67e-01 +1.54e-01 +1.55e-01 +1.61e-01 +1.61e-01 +1.63e-01 +1.70e-01 +2.53e-01 +2.35e-01 +7.59e-02 +1.63e-01 +1.66e-01 +1.68e-01 +1.64e-01 +1.59e-01 +1.59e-01 +1.57e-01 +1.58e-01 +1.58e-01 +1.57e-01 +1.58e-01 +1.66e-01 +2.50e-01 +2.58e-01 +9.81e-02 +1.60e-01 +1.61e-01 +1.64e-01 +1.61e-01 +1.60e-01 +1.56e-01 +1.51e-01 +1.50e-01 +1.63e-01 +1.64e-01 +1.57e-01 +1.64e-01 +2.37e-01 +2.18e-01 +9.03e-02 +1.68e-01 +1.70e-01 +1.64e-01 +1.56e-01 +1.58e-01 +1.53e-01 +1.51e-01 +1.43e-01 +1.51e-01 +1.53e-01 +1.55e-01 +1.63e-01 +2.19e-01 +1.33e-01 +4.89e-02 +1.67e-01 +1.60e-01 +1.61e-01 +1.55e-01 +1.64e-01 +1.56e-01 +1.49e-01 +1.45e-01 +1.44e-01 +1.51e-01 +1.57e-01 +1.64e-01 +2.32e-01 +1.18e-01 +3.65e-02 +1.62e-01 +1.67e-01 +1.68e-01 +1.55e-01 +1.59e-01 +1.57e-01 +1.58e-01 +1.46e-01 +1.43e-01 +1.39e-01 +1.48e-01 +1.54e-01 +2.26e-01 +1.45e-01 +4.05e-02 +1.55e-01 +1.63e-01 +1.63e-01 +1.67e-01 +1.56e-01 +1.53e-01 +1.50e-01 +1.49e-01 +1.49e-01 +1.45e-01 +1.47e-01 +1.53e-01 +2.10e-01 +1.20e-01 +3.82e-02 +1.65e-01 +1.59e-01 +1.61e-01 +1.61e-01 +1.56e-01 +1.56e-01 +1.60e-01 +1.59e-01 +1.54e-01 +1.53e-01 +1.48e-01 +1.52e-01 +2.07e-01 +1.01e-01 +3.00e-02 +1.77e-01 +1.71e-01 +1.68e-01 +1.71e-01 +1.75e-01 +1.69e-01 +1.64e-01 +1.68e-01 +1.61e-01 +1.61e-01 +1.53e-01 +1.46e-01 +1.95e-01 +9.40e-02 +2.21e-02 +2.46e-01 +2.38e-01 +2.31e-01 +2.41e-01 +2.54e-01 +2.50e-01 +2.28e-01 +2.20e-01 +2.21e-01 +2.26e-01 +2.35e-01 +2.02e-01 +1.88e-01 +9.58e-02 +3.52e-02 +3.06e-01 +2.59e-01 +2.23e-01 +2.61e-01 +2.57e-01 +2.36e-01 +2.32e-01 +1.78e-01 +1.53e-01 +1.37e-01 +1.47e-01 +1.36e-01 +7.24e-02 +4.03e-02 +2.23e-02 +1.04e-01 +9.82e-02 +8.79e-02 +9.20e-02 +9.75e-02 +7.29e-02 +1.04e-01 +6.06e-02 +5.37e-02 +3.67e-02 +4.29e-02 +4.85e-02 +2.87e-02 +1.59e-02 +9.17e-03 +1.71e-01 +1.67e-01 +1.65e-01 +1.69e-01 +1.59e-01 +1.60e-01 +1.62e-01 +1.62e-01 +1.57e-01 +1.49e-01 +1.52e-01 +1.53e-01 +2.42e-01 +1.80e-01 +8.19e-02 +1.68e-01 +1.63e-01 +1.67e-01 +1.65e-01 +1.63e-01 +1.54e-01 +1.59e-01 +1.52e-01 +1.59e-01 +1.52e-01 +1.49e-01 +1.55e-01 +2.25e-01 +2.01e-01 +7.57e-02 +1.63e-01 +1.59e-01 +1.62e-01 +1.61e-01 +1.57e-01 +1.56e-01 +1.50e-01 +1.51e-01 +1.48e-01 +1.53e-01 +1.61e-01 +1.58e-01 +2.32e-01 +2.49e-01 +1.01e-01 +1.67e-01 +1.54e-01 +1.55e-01 +1.52e-01 +1.55e-01 +1.61e-01 +1.57e-01 +1.54e-01 +1.54e-01 +1.54e-01 +1.56e-01 +1.64e-01 +2.25e-01 +1.56e-01 +6.14e-02 +1.67e-01 +1.65e-01 +1.61e-01 +1.57e-01 +1.58e-01 +1.62e-01 +1.55e-01 +1.57e-01 +1.50e-01 +1.54e-01 +1.55e-01 +1.68e-01 +2.32e-01 +1.28e-01 +4.68e-02 +1.63e-01 +1.57e-01 +1.60e-01 +1.63e-01 +1.61e-01 +1.58e-01 +1.50e-01 +1.46e-01 +1.43e-01 +1.52e-01 +1.55e-01 +1.61e-01 +2.25e-01 +1.35e-01 +4.13e-02 +1.64e-01 +1.62e-01 +1.60e-01 +1.62e-01 +1.61e-01 +1.51e-01 +1.50e-01 +1.41e-01 +1.46e-01 +1.49e-01 +1.51e-01 +1.64e-01 +2.31e-01 +1.50e-01 +4.56e-02 +1.53e-01 +1.55e-01 +1.54e-01 +1.57e-01 +1.55e-01 +1.49e-01 +1.50e-01 +1.44e-01 +1.43e-01 +1.47e-01 +1.51e-01 +1.57e-01 +2.19e-01 +1.63e-01 +4.89e-02 +1.64e-01 +6.61e-02 +1.57e-01 +1.53e-01 +1.48e-01 +1.53e-01 +7.58e-02 +1.40e-01 +1.38e-01 +1.40e-01 +1.50e-01 +1.57e-01 +2.10e-01 +1.54e-01 +5.14e-02 +1.56e-01 +1.58e-01 +1.61e-01 +1.57e-01 +1.52e-01 +1.47e-01 +1.51e-01 +1.48e-01 +1.44e-01 +1.48e-01 +1.44e-01 +1.43e-01 +2.07e-01 +1.20e-01 +4.74e-02 +1.62e-01 +1.71e-01 +1.62e-01 +1.57e-01 +1.52e-01 +1.51e-01 +1.44e-01 +1.43e-01 +1.46e-01 +1.46e-01 +1.36e-01 +1.45e-01 +1.86e-01 +7.07e-02 +2.51e-02 +1.70e-01 +1.71e-01 +1.63e-01 +1.56e-01 +1.60e-01 +1.61e-01 +1.56e-01 +1.55e-01 +1.59e-01 +1.53e-01 +1.48e-01 +1.41e-01 +1.55e-01 +6.13e-02 +2.68e-02 +2.40e-01 +2.25e-01 +2.21e-01 +2.27e-01 +2.31e-01 +2.20e-01 +2.21e-01 +1.99e-01 +2.15e-01 +1.98e-01 +2.02e-01 +1.80e-01 +1.35e-01 +3.74e-02 +1.62e-02 +2.64e-01 +2.69e-01 +2.32e-01 +1.55e-01 +1.50e-01 +1.99e-01 +1.79e-01 +1.44e-01 +1.30e-01 +1.25e-01 +1.44e-01 +1.15e-01 +4.22e-02 +1.96e-02 +8.51e-03 +9.50e-02 +1.04e-01 +8.97e-02 +4.80e-02 +4.87e-02 +6.44e-02 +6.38e-02 +4.70e-02 +4.40e-02 +3.28e-02 +4.72e-02 +4.03e-02 +1.52e-02 +8.66e-03 +6.15e-03 +1.56e-01 +1.57e-01 +1.54e-01 +1.60e-01 +1.65e-01 +1.66e-01 +1.65e-01 +1.58e-01 +1.47e-01 +1.40e-01 +1.36e-01 +1.59e-01 +2.22e-01 +1.52e-01 +5.31e-02 +1.61e-01 +1.62e-01 +1.69e-01 +1.63e-01 +1.58e-01 +1.58e-01 +1.59e-01 +1.47e-01 +1.45e-01 +1.39e-01 +1.30e-01 +1.48e-01 +2.15e-01 +1.09e-01 +3.46e-02 +1.58e-01 +1.57e-01 +1.62e-01 +1.63e-01 +1.59e-01 +1.57e-01 +1.56e-01 +1.58e-01 +1.52e-01 +1.45e-01 +1.50e-01 +1.60e-01 +2.17e-01 +1.56e-01 +5.66e-02 +1.60e-01 +1.57e-01 +1.60e-01 +1.57e-01 +1.60e-01 +1.58e-01 +1.54e-01 +1.53e-01 +1.47e-01 +1.42e-01 +1.57e-01 +1.66e-01 +2.10e-01 +1.46e-01 +6.02e-02 +1.62e-01 +1.61e-01 +1.68e-01 +1.54e-01 +1.54e-01 +1.59e-01 +1.50e-01 +1.49e-01 +1.50e-01 +1.46e-01 +1.58e-01 +1.59e-01 +2.16e-01 +1.18e-01 +3.24e-02 +1.61e-01 +1.55e-01 +1.60e-01 +1.59e-01 +1.50e-01 +1.45e-01 +1.50e-01 +1.45e-01 +1.47e-01 +1.46e-01 +1.48e-01 +1.58e-01 +2.13e-01 +1.24e-01 +4.33e-02 +1.58e-01 +1.55e-01 +1.58e-01 +1.51e-01 +1.57e-01 +1.54e-01 +1.50e-01 +1.39e-01 +1.40e-01 +1.40e-01 +1.39e-01 +1.49e-01 +2.03e-01 +1.50e-01 +5.78e-02 +1.51e-01 +1.59e-01 +1.53e-01 +1.53e-01 +1.52e-01 +1.52e-01 +1.46e-01 +1.43e-01 +1.42e-01 +1.41e-01 +1.52e-01 +1.52e-01 +2.06e-01 +1.37e-01 +5.13e-02 +1.55e-01 +1.52e-01 +1.56e-01 +1.49e-01 +1.53e-01 +1.49e-01 +1.45e-01 +1.39e-01 +1.35e-01 +1.33e-01 +1.48e-01 +1.46e-01 +1.98e-01 +1.08e-01 +3.36e-02 +1.53e-01 +1.53e-01 +1.49e-01 +1.43e-01 +1.45e-01 +1.48e-01 +1.38e-01 +1.42e-01 +1.29e-01 +1.29e-01 +1.38e-01 +1.45e-01 +2.11e-01 +1.20e-01 +3.89e-02 +1.60e-01 +1.55e-01 +1.59e-01 +1.49e-01 +1.48e-01 +1.48e-01 +1.47e-01 +1.50e-01 +1.41e-01 +1.37e-01 +1.31e-01 +1.36e-01 +1.77e-01 +7.63e-02 +1.94e-02 +1.69e-01 +1.64e-01 +1.65e-01 +1.57e-01 +1.52e-01 +1.48e-01 +1.44e-01 +1.54e-01 +1.48e-01 +1.49e-01 +1.40e-01 +1.31e-01 +1.53e-01 +4.85e-02 +1.42e-02 +2.42e-01 +2.23e-01 +2.24e-01 +2.03e-01 +2.18e-01 +2.15e-01 +1.75e-01 +1.74e-01 +1.73e-01 +2.02e-01 +2.01e-01 +1.69e-01 +1.22e-01 +3.16e-02 +1.08e-02 +2.16e-01 +2.25e-01 +1.60e-01 +1.34e-01 +1.44e-01 +1.68e-01 +1.02e-01 +8.69e-02 +7.20e-02 +1.43e-01 +1.29e-01 +8.21e-02 +4.29e-02 +1.08e-02 +4.27e-03 +7.46e-02 +7.65e-02 +5.07e-02 +4.76e-02 +4.31e-02 +5.72e-02 +3.77e-02 +2.92e-02 +2.28e-02 +4.79e-02 +4.01e-02 +2.76e-02 +1.93e-02 +8.26e-03 +2.13e-03 +1.63e-01 +1.66e-01 +1.71e-01 +1.63e-01 +1.67e-01 +1.59e-01 +1.56e-01 +1.61e-01 +1.49e-01 +1.45e-01 +1.38e-01 +1.54e-01 +2.17e-01 +1.17e-01 +3.57e-02 +1.59e-01 +1.61e-01 +1.68e-01 +1.63e-01 +1.62e-01 +1.62e-01 +1.60e-01 +1.56e-01 +1.49e-01 +1.47e-01 +1.44e-01 +1.52e-01 +2.09e-01 +1.26e-01 +3.94e-02 +1.57e-01 +1.61e-01 +1.58e-01 +1.59e-01 +1.60e-01 +1.51e-01 +1.56e-01 +1.60e-01 +1.49e-01 +1.45e-01 +1.44e-01 +1.61e-01 +2.41e-01 +1.63e-01 +5.31e-02 +1.60e-01 +1.54e-01 +1.60e-01 +1.53e-01 +1.56e-01 +1.57e-01 +1.55e-01 +1.53e-01 +1.46e-01 +1.47e-01 +1.49e-01 +1.47e-01 +2.08e-01 +1.76e-01 +7.44e-02 +1.55e-01 +1.52e-01 +1.53e-01 +1.52e-01 +1.52e-01 +1.51e-01 +1.51e-01 +1.53e-01 +1.48e-01 +1.40e-01 +1.50e-01 +1.61e-01 +2.05e-01 +1.29e-01 +4.01e-02 +1.60e-01 +1.58e-01 +1.50e-01 +1.47e-01 +1.42e-01 +1.46e-01 +1.50e-01 +1.46e-01 +1.45e-01 +1.42e-01 +1.48e-01 +1.60e-01 +2.17e-01 +1.50e-01 +5.21e-02 +1.53e-01 +1.49e-01 +1.52e-01 +1.45e-01 +1.45e-01 +1.43e-01 +1.40e-01 +1.38e-01 +1.37e-01 +1.36e-01 +1.46e-01 +1.49e-01 +1.88e-01 +1.14e-01 +4.21e-02 +1.48e-01 +1.50e-01 +1.52e-01 +1.43e-01 +1.48e-01 +1.44e-01 +1.36e-01 +1.34e-01 +1.37e-01 +1.37e-01 +1.46e-01 +1.47e-01 +1.84e-01 +7.19e-02 +2.90e-02 +1.48e-01 +1.19e-01 +1.48e-01 +1.48e-01 +1.49e-01 +1.54e-01 +1.45e-01 +1.21e-01 +1.29e-01 +1.30e-01 +1.31e-01 +1.39e-01 +1.93e-01 +7.96e-02 +2.03e-02 +1.53e-01 +1.46e-01 +1.46e-01 +1.41e-01 +1.47e-01 +1.45e-01 +1.42e-01 +1.37e-01 +1.29e-01 +1.27e-01 +1.30e-01 +1.33e-01 +1.95e-01 +8.81e-02 +2.19e-02 +1.56e-01 +1.42e-01 +1.45e-01 +1.43e-01 +1.52e-01 +1.48e-01 +1.44e-01 +1.40e-01 +1.41e-01 +1.31e-01 +1.18e-01 +1.31e-01 +1.83e-01 +9.56e-02 +2.75e-02 +1.63e-01 +1.47e-01 +1.48e-01 +1.57e-01 +1.55e-01 +1.50e-01 +1.56e-01 +1.42e-01 +1.44e-01 +1.39e-01 +1.29e-01 +1.20e-01 +1.52e-01 +5.41e-02 +1.62e-02 +2.32e-01 +2.08e-01 +2.20e-01 +2.22e-01 +2.23e-01 +2.17e-01 +1.99e-01 +2.05e-01 +1.92e-01 +2.03e-01 +1.97e-01 +1.60e-01 +1.37e-01 +4.00e-02 +1.07e-02 +2.25e-01 +1.79e-01 +1.71e-01 +1.67e-01 +1.69e-01 +1.54e-01 +1.02e-01 +1.09e-01 +9.88e-02 +1.08e-01 +1.18e-01 +8.29e-02 +4.15e-02 +1.49e-02 +5.20e-03 +6.83e-02 +5.09e-02 +4.28e-02 +4.83e-02 +4.92e-02 +5.19e-02 +3.56e-02 +3.22e-02 +2.88e-02 +3.04e-02 +3.83e-02 +2.79e-02 +1.21e-02 +5.81e-03 +1.30e-03 +1.57e-01 +1.62e-01 +1.67e-01 +1.62e-01 +1.61e-01 +1.50e-01 +1.52e-01 +1.57e-01 +1.53e-01 +1.51e-01 +1.43e-01 +1.61e-01 +2.04e-01 +1.43e-01 +5.22e-02 +1.59e-01 +1.63e-01 +1.58e-01 +1.58e-01 +1.58e-01 +1.62e-01 +1.60e-01 +1.58e-01 +1.58e-01 +1.43e-01 +1.49e-01 +1.60e-01 +2.21e-01 +1.56e-01 +4.57e-02 +1.63e-01 +1.57e-01 +1.51e-01 +1.54e-01 +1.53e-01 +1.60e-01 +1.55e-01 +1.60e-01 +1.60e-01 +1.45e-01 +1.40e-01 +1.59e-01 +2.23e-01 +1.66e-01 +5.44e-02 +1.53e-01 +1.54e-01 +1.54e-01 +1.50e-01 +1.57e-01 +1.53e-01 +1.59e-01 +1.56e-01 +1.48e-01 +1.45e-01 +1.45e-01 +1.58e-01 +2.07e-01 +1.44e-01 +5.45e-02 +1.50e-01 +1.46e-01 +1.56e-01 +1.56e-01 +1.54e-01 +1.48e-01 +1.50e-01 +1.54e-01 +1.46e-01 +1.46e-01 +1.50e-01 +1.52e-01 +2.09e-01 +1.16e-01 +3.68e-02 +1.56e-01 +1.50e-01 +1.56e-01 +1.48e-01 +1.50e-01 +1.51e-01 +1.48e-01 +1.49e-01 +1.43e-01 +1.41e-01 +1.46e-01 +1.53e-01 +2.02e-01 +1.07e-01 +3.92e-02 +1.55e-01 +1.55e-01 +1.53e-01 +1.49e-01 +1.47e-01 +1.48e-01 +1.41e-01 +1.44e-01 +1.41e-01 +1.41e-01 +1.37e-01 +1.51e-01 +2.06e-01 +1.37e-01 +4.68e-02 +1.52e-01 +1.53e-01 +1.54e-01 +1.53e-01 +1.48e-01 +1.43e-01 +1.32e-01 +1.41e-01 +1.35e-01 +1.27e-01 +1.35e-01 +1.52e-01 +1.90e-01 +7.26e-02 +2.22e-02 +1.47e-01 +1.47e-01 +1.49e-01 +1.50e-01 +1.49e-01 +1.42e-01 +1.39e-01 +1.32e-01 +1.28e-01 +1.26e-01 +1.34e-01 +1.45e-01 +1.87e-01 +8.17e-02 +2.42e-02 +1.49e-01 +1.48e-01 +1.47e-01 +1.50e-01 +1.45e-01 +1.43e-01 +1.38e-01 +1.30e-01 +1.29e-01 +1.31e-01 +1.22e-01 +1.36e-01 +1.78e-01 +1.04e-01 +3.33e-02 +1.48e-01 +1.45e-01 +1.44e-01 +1.42e-01 +1.41e-01 +1.43e-01 +1.46e-01 +1.38e-01 +1.29e-01 +1.25e-01 +1.18e-01 +1.25e-01 +1.79e-01 +8.54e-02 +2.89e-02 +1.64e-01 +1.47e-01 +1.55e-01 +1.56e-01 +1.47e-01 +1.59e-01 +1.48e-01 +1.44e-01 +1.36e-01 +1.33e-01 +1.26e-01 +1.10e-01 +1.31e-01 +5.72e-02 +1.80e-02 +2.46e-01 +2.05e-01 +1.90e-01 +2.18e-01 +2.16e-01 +2.14e-01 +2.02e-01 +2.24e-01 +2.13e-01 +2.23e-01 +1.86e-01 +1.51e-01 +1.18e-01 +3.41e-02 +1.21e-02 +1.87e-01 +1.53e-01 +1.50e-01 +1.67e-01 +1.83e-01 +1.69e-01 +1.39e-01 +1.42e-01 +1.29e-01 +1.05e-01 +9.27e-02 +6.73e-02 +3.93e-02 +1.65e-02 +7.05e-03 +7.21e-02 +6.08e-02 +6.40e-02 +5.54e-02 +6.28e-02 +5.59e-02 +4.39e-02 +4.02e-02 +4.46e-02 +3.36e-02 +3.13e-02 +2.12e-02 +1.23e-02 +7.79e-03 +3.56e-03 +1.51e-01 +1.56e-01 +1.63e-01 +1.66e-01 +1.67e-01 +1.65e-01 +1.66e-01 +1.70e-01 +1.53e-01 +1.56e-01 +1.51e-01 +1.51e-01 +1.83e-01 +9.73e-02 +3.81e-02 +1.53e-01 +1.59e-01 +1.54e-01 +1.57e-01 +1.61e-01 +1.59e-01 +1.57e-01 +1.58e-01 +1.58e-01 +1.58e-01 +1.52e-01 +1.52e-01 +2.00e-01 +1.30e-01 +4.17e-02 +1.54e-01 +1.56e-01 +1.53e-01 +1.62e-01 +1.55e-01 +1.59e-01 +1.66e-01 +1.53e-01 +1.56e-01 +1.56e-01 +1.52e-01 +1.57e-01 +2.13e-01 +1.42e-01 +4.79e-02 +1.53e-01 +1.59e-01 +1.67e-01 +1.55e-01 +1.64e-01 +1.66e-01 +1.61e-01 +1.55e-01 +1.54e-01 +1.50e-01 +1.45e-01 +1.54e-01 +2.09e-01 +1.08e-01 +3.44e-02 +1.54e-01 +1.57e-01 +1.60e-01 +1.54e-01 +1.62e-01 +1.59e-01 +1.56e-01 +1.54e-01 +1.57e-01 +1.52e-01 +1.52e-01 +1.56e-01 +2.03e-01 +9.74e-02 +2.86e-02 +1.52e-01 +1.57e-01 +1.58e-01 +1.62e-01 +1.56e-01 +1.57e-01 +1.52e-01 +1.48e-01 +1.44e-01 +1.47e-01 +1.50e-01 +1.51e-01 +2.12e-01 +1.09e-01 +2.91e-02 +1.51e-01 +1.54e-01 +1.48e-01 +1.58e-01 +1.54e-01 +1.52e-01 +1.51e-01 +1.44e-01 +1.44e-01 +1.43e-01 +1.45e-01 +1.51e-01 +1.99e-01 +1.23e-01 +3.39e-02 +1.48e-01 +1.50e-01 +1.55e-01 +1.59e-01 +1.47e-01 +1.46e-01 +1.37e-01 +1.40e-01 +1.41e-01 +1.47e-01 +1.42e-01 +1.45e-01 +1.87e-01 +1.10e-01 +3.91e-02 +1.43e-01 +1.48e-01 +1.52e-01 +1.52e-01 +1.56e-01 +1.49e-01 +1.38e-01 +1.38e-01 +1.34e-01 +1.30e-01 +1.26e-01 +1.34e-01 +1.85e-01 +8.58e-02 +2.44e-02 +1.48e-01 +1.45e-01 +1.47e-01 +1.50e-01 +1.53e-01 +1.41e-01 +1.37e-01 +1.35e-01 +1.27e-01 +1.27e-01 +1.23e-01 +1.24e-01 +1.55e-01 +8.14e-02 +2.87e-02 +1.44e-01 +1.47e-01 +1.45e-01 +1.44e-01 +1.45e-01 +1.40e-01 +1.42e-01 +1.37e-01 +1.27e-01 +1.28e-01 +1.17e-01 +1.21e-01 +1.63e-01 +5.00e-02 +1.53e-02 +1.47e-01 +1.48e-01 +1.47e-01 +1.53e-01 +1.45e-01 +1.50e-01 +1.52e-01 +1.47e-01 +1.34e-01 +1.29e-01 +1.22e-01 +1.13e-01 +1.35e-01 +4.55e-02 +1.41e-02 +2.05e-01 +1.89e-01 +1.85e-01 +2.07e-01 +2.14e-01 +2.10e-01 +1.91e-01 +1.99e-01 +1.97e-01 +1.79e-01 +1.74e-01 +1.47e-01 +7.71e-02 +1.94e-02 +6.96e-03 +1.71e-01 +9.07e-02 +1.18e-01 +1.23e-01 +1.31e-01 +1.29e-01 +1.02e-01 +1.39e-01 +1.15e-01 +7.59e-02 +8.17e-02 +5.28e-02 +2.69e-02 +1.04e-02 +3.19e-03 +5.55e-02 +2.88e-02 +3.51e-02 +3.72e-02 +5.09e-02 +4.45e-02 +3.21e-02 +4.62e-02 +3.95e-02 +2.61e-02 +2.32e-02 +1.25e-02 +9.63e-03 +6.12e-03 +3.33e-03 +1.57e-01 +1.68e-01 +1.77e-01 +1.71e-01 +1.70e-01 +1.76e-01 +1.72e-01 +1.62e-01 +1.62e-01 +1.58e-01 +1.58e-01 +1.48e-01 +1.49e-01 +3.17e-02 +1.30e-02 +1.73e-01 +1.78e-01 +1.74e-01 +1.61e-01 +1.60e-01 +1.64e-01 +1.67e-01 +1.62e-01 +1.58e-01 +1.56e-01 +1.59e-01 +1.53e-01 +1.62e-01 +5.38e-02 +2.31e-02 +1.66e-01 +1.78e-01 +1.66e-01 +1.64e-01 +1.63e-01 +1.71e-01 +1.77e-01 +1.69e-01 +1.55e-01 +1.57e-01 +1.50e-01 +1.66e-01 +2.15e-01 +9.77e-02 +2.59e-02 +1.65e-01 +1.70e-01 +1.68e-01 +1.65e-01 +1.69e-01 +1.74e-01 +6.32e-02 +1.61e-01 +1.64e-01 +1.57e-01 +1.44e-01 +1.50e-01 +1.92e-01 +8.51e-02 +2.93e-02 +1.60e-01 +1.58e-01 +1.61e-01 +1.66e-01 +1.68e-01 +1.70e-01 +1.63e-01 +1.61e-01 +1.62e-01 +1.60e-01 +1.44e-01 +1.42e-01 +1.97e-01 +1.03e-01 +2.80e-02 +1.55e-01 +1.62e-01 +1.58e-01 +1.62e-01 +1.63e-01 +1.64e-01 +1.60e-01 +1.52e-01 +1.55e-01 +1.57e-01 +1.44e-01 +1.41e-01 +1.88e-01 +9.04e-02 +2.91e-02 +1.49e-01 +1.51e-01 +1.62e-01 +1.64e-01 +1.61e-01 +1.56e-01 +1.57e-01 +1.49e-01 +1.46e-01 +1.48e-01 +1.45e-01 +1.41e-01 +1.63e-01 +9.25e-02 +3.25e-02 +1.52e-01 +1.51e-01 +1.52e-01 +1.59e-01 +1.57e-01 +1.54e-01 +1.55e-01 +1.52e-01 +1.48e-01 +1.42e-01 +1.43e-01 +1.45e-01 +1.82e-01 +6.97e-02 +2.31e-02 +1.55e-01 +1.54e-01 +1.58e-01 +1.59e-01 +1.58e-01 +1.62e-01 +1.50e-01 +1.47e-01 +1.42e-01 +1.42e-01 +1.33e-01 +1.37e-01 +1.74e-01 +6.53e-02 +2.18e-02 +1.52e-01 +1.45e-01 +1.60e-01 +1.58e-01 +1.58e-01 +1.54e-01 +1.50e-01 +1.37e-01 +1.30e-01 +1.26e-01 +1.30e-01 +1.23e-01 +1.71e-01 +7.09e-02 +2.53e-02 +1.42e-01 +1.41e-01 +1.47e-01 +1.47e-01 +1.58e-01 +1.48e-01 +1.36e-01 +1.35e-01 +1.33e-01 +1.29e-01 +1.19e-01 +1.14e-01 +1.45e-01 +4.24e-02 +1.27e-02 +1.42e-01 +6.20e-02 +1.44e-01 +1.45e-01 +1.42e-01 +1.37e-01 +1.42e-01 +1.47e-01 +1.33e-01 +1.30e-01 +1.15e-01 +1.07e-01 +1.24e-01 +3.83e-02 +1.19e-02 +1.85e-01 +1.70e-01 +1.73e-01 +1.81e-01 +1.77e-01 +1.76e-01 +1.75e-01 +1.93e-01 +1.94e-01 +1.58e-01 +1.45e-01 +1.24e-01 +7.16e-02 +2.05e-02 +7.17e-03 +1.27e-01 +6.47e-02 +8.27e-02 +1.04e-01 +7.86e-02 +7.86e-02 +7.04e-02 +6.74e-02 +1.02e-01 +7.60e-02 +5.37e-02 +4.16e-02 +2.54e-02 +6.01e-03 +1.97e-03 +5.06e-02 +2.49e-02 +3.45e-02 +3.64e-02 +2.84e-02 +2.70e-02 +2.34e-02 +2.43e-02 +3.12e-02 +2.55e-02 +1.56e-02 +1.52e-02 +9.91e-03 +3.26e-03 +1.47e-03 +2.30e-01 +2.43e-01 +2.55e-01 +2.55e-01 +2.51e-01 +2.39e-01 +2.30e-01 +2.27e-01 +2.29e-01 +2.07e-01 +2.02e-01 +2.14e-01 +1.61e-01 +2.95e-02 +7.43e-03 +2.45e-01 +2.22e-01 +2.61e-01 +2.31e-01 +2.30e-01 +2.40e-01 +2.34e-01 +2.18e-01 +2.03e-01 +2.11e-01 +1.97e-01 +1.92e-01 +1.80e-01 +4.88e-02 +1.49e-02 +2.38e-01 +2.17e-01 +2.52e-01 +2.29e-01 +2.25e-01 +2.44e-01 +2.15e-01 +2.39e-01 +2.19e-01 +2.19e-01 +2.01e-01 +1.92e-01 +2.20e-01 +9.45e-02 +2.94e-02 +2.36e-01 +2.27e-01 +2.34e-01 +2.40e-01 +2.35e-01 +2.56e-01 +2.38e-01 +2.37e-01 +2.25e-01 +2.08e-01 +1.82e-01 +1.60e-01 +1.81e-01 +5.67e-02 +1.61e-02 +2.29e-01 +2.18e-01 +2.26e-01 +2.37e-01 +2.21e-01 +2.35e-01 +2.69e-01 +2.31e-01 +2.30e-01 +2.32e-01 +2.01e-01 +1.47e-01 +1.58e-01 +7.81e-02 +2.93e-02 +2.28e-01 +2.05e-01 +2.20e-01 +2.25e-01 +2.12e-01 +2.31e-01 +2.33e-01 +2.13e-01 +2.09e-01 +2.24e-01 +1.97e-01 +1.71e-01 +1.79e-01 +7.52e-02 +2.31e-02 +2.03e-01 +2.07e-01 +2.27e-01 +2.19e-01 +2.11e-01 +2.22e-01 +2.17e-01 +2.08e-01 +1.77e-01 +2.01e-01 +1.89e-01 +1.57e-01 +1.63e-01 +5.48e-02 +1.75e-02 +2.07e-01 +2.13e-01 +2.09e-01 +2.25e-01 +2.36e-01 +2.27e-01 +2.11e-01 +2.10e-01 +1.87e-01 +1.89e-01 +1.88e-01 +1.79e-01 +1.33e-01 +3.03e-02 +1.06e-02 +2.08e-01 +2.14e-01 +2.12e-01 +2.27e-01 +2.27e-01 +2.05e-01 +2.29e-01 +2.21e-01 +1.93e-01 +2.02e-01 +1.81e-01 +1.73e-01 +1.67e-01 +4.08e-02 +1.39e-02 +2.00e-01 +2.19e-01 +2.28e-01 +2.14e-01 +2.28e-01 +2.06e-01 +2.17e-01 +1.92e-01 +1.79e-01 +1.74e-01 +1.72e-01 +1.71e-01 +1.60e-01 +4.62e-02 +1.43e-02 +1.82e-01 +1.92e-01 +2.16e-01 +2.14e-01 +2.12e-01 +2.26e-01 +1.94e-01 +1.68e-01 +1.64e-01 +1.50e-01 +1.55e-01 +1.48e-01 +1.04e-01 +4.02e-02 +1.44e-02 +1.56e-01 +1.60e-01 +1.93e-01 +1.87e-01 +1.94e-01 +2.01e-01 +2.07e-01 +1.85e-01 +1.76e-01 +1.49e-01 +1.39e-01 +1.38e-01 +1.04e-01 +2.84e-02 +1.17e-02 +1.33e-01 +1.46e-01 +1.65e-01 +1.58e-01 +1.74e-01 +1.70e-01 +1.37e-01 +1.94e-01 +1.93e-01 +1.64e-01 +1.20e-01 +1.02e-01 +6.63e-02 +1.85e-02 +8.29e-03 +6.84e-02 +4.09e-02 +5.11e-02 +6.13e-02 +4.88e-02 +4.65e-02 +3.93e-02 +4.74e-02 +8.51e-02 +5.59e-02 +2.40e-02 +2.44e-02 +1.30e-02 +4.68e-03 +2.65e-03 +2.59e-02 +1.30e-02 +1.81e-02 +1.96e-02 +1.49e-02 +1.49e-02 +1.50e-02 +1.42e-02 +2.71e-02 +2.16e-02 +1.05e-02 +7.72e-03 +4.11e-03 +2.35e-03 +9.64e-04 +2.21e-01 +2.15e-01 +2.72e-01 +1.92e-01 +3.01e-01 +2.43e-01 +2.13e-01 +2.18e-01 +1.88e-01 +1.66e-01 +1.13e-01 +1.21e-01 +1.08e-01 +2.92e-02 +8.37e-03 +2.75e-01 +1.89e-01 +3.28e-01 +3.64e-01 +2.91e-01 +2.34e-01 +1.86e-01 +1.90e-01 +1.53e-01 +1.35e-01 +1.21e-01 +1.01e-01 +8.79e-02 +3.91e-02 +1.37e-02 +2.37e-01 +2.28e-01 +2.92e-01 +2.21e-01 +1.70e-01 +2.29e-01 +1.86e-01 +1.95e-01 +1.65e-01 +1.42e-01 +1.73e-01 +1.38e-01 +7.39e-02 +4.16e-02 +2.91e-02 +2.73e-01 +2.33e-01 +2.78e-01 +3.06e-01 +2.31e-01 +2.52e-01 +2.69e-01 +2.39e-01 +2.13e-01 +1.63e-01 +1.20e-01 +8.39e-02 +5.83e-02 +2.86e-02 +1.32e-02 +2.63e-01 +1.92e-01 +2.54e-01 +3.11e-01 +2.65e-01 +2.75e-01 +2.71e-01 +2.21e-01 +1.81e-01 +1.68e-01 +1.49e-01 +7.05e-02 +5.03e-02 +2.54e-02 +1.28e-02 +2.09e-01 +1.61e-01 +2.21e-01 +2.28e-01 +1.55e-01 +1.78e-01 +2.16e-01 +1.50e-01 +1.32e-01 +1.76e-01 +1.57e-01 +1.01e-01 +5.61e-02 +2.32e-02 +1.53e-02 +1.50e-01 +1.81e-01 +2.03e-01 +2.04e-01 +1.79e-01 +1.94e-01 +1.81e-01 +1.52e-01 +1.26e-01 +1.13e-01 +9.16e-02 +6.72e-02 +3.94e-02 +2.35e-02 +1.45e-02 +1.06e-01 +1.43e-01 +1.61e-01 +1.57e-01 +1.80e-01 +1.80e-01 +1.24e-01 +1.22e-01 +8.18e-02 +7.30e-02 +9.33e-02 +7.39e-02 +3.89e-02 +2.07e-02 +8.17e-03 +1.13e-01 +1.37e-01 +1.61e-01 +2.21e-01 +1.71e-01 +1.51e-01 +1.16e-01 +1.45e-01 +1.13e-01 +1.05e-01 +9.41e-02 +7.14e-02 +5.35e-02 +1.91e-02 +7.31e-03 +8.55e-02 +1.58e-01 +2.08e-01 +1.60e-01 +1.37e-01 +8.33e-02 +1.16e-01 +1.39e-01 +1.15e-01 +9.22e-02 +8.69e-02 +6.24e-02 +4.30e-02 +2.48e-02 +7.57e-03 +9.22e-02 +1.13e-01 +1.60e-01 +1.04e-01 +1.40e-01 +1.35e-01 +1.08e-01 +1.02e-01 +8.56e-02 +5.91e-02 +7.46e-02 +6.28e-02 +3.81e-02 +2.04e-02 +9.21e-03 +7.85e-02 +6.83e-02 +1.01e-01 +9.42e-02 +9.47e-02 +1.38e-01 +1.13e-01 +6.40e-02 +6.03e-02 +5.21e-02 +4.63e-02 +4.67e-02 +3.57e-02 +9.24e-03 +3.46e-03 +3.82e-02 +4.99e-02 +6.21e-02 +4.80e-02 +5.79e-02 +8.37e-02 +6.86e-02 +6.66e-02 +5.19e-02 +5.77e-02 +4.00e-02 +3.46e-02 +2.69e-02 +9.16e-03 +3.73e-03 +1.66e-02 +1.94e-02 +2.26e-02 +1.79e-02 +2.45e-02 +3.08e-02 +2.00e-02 +2.89e-02 +3.35e-02 +2.83e-02 +1.58e-02 +1.10e-02 +7.32e-03 +3.83e-03 +1.85e-03 +1.35e-02 +7.32e-03 +8.41e-03 +8.10e-03 +9.00e-03 +1.12e-02 +5.57e-03 +9.30e-03 +1.48e-02 +1.39e-02 +6.30e-03 +3.42e-03 +2.99e-03 +1.40e-03 +5.26e-04 +7.56e-02 +7.51e-02 +1.02e-01 +6.63e-02 +1.09e-01 +8.00e-02 +7.46e-02 +7.69e-02 +7.23e-02 +5.75e-02 +3.77e-02 +3.71e-02 +3.59e-02 +2.14e-02 +7.54e-03 +1.13e-01 +5.14e-02 +1.11e-01 +1.48e-01 +1.23e-01 +8.67e-02 +6.58e-02 +5.93e-02 +5.79e-02 +5.00e-02 +3.78e-02 +2.92e-02 +2.64e-02 +1.76e-02 +8.34e-03 +8.72e-02 +6.82e-02 +1.03e-01 +9.93e-02 +4.68e-02 +8.00e-02 +6.54e-02 +5.74e-02 +5.88e-02 +4.17e-02 +6.05e-02 +5.26e-02 +2.82e-02 +1.18e-02 +1.25e-02 +8.93e-02 +6.93e-02 +1.05e-01 +1.22e-01 +7.82e-02 +8.44e-02 +9.53e-02 +9.08e-02 +8.61e-02 +6.38e-02 +4.26e-02 +2.94e-02 +1.95e-02 +1.16e-02 +5.85e-03 +1.07e-01 +7.48e-02 +9.61e-02 +1.16e-01 +1.00e-01 +9.56e-02 +1.01e-01 +7.22e-02 +7.15e-02 +4.92e-02 +5.10e-02 +2.14e-02 +1.39e-02 +8.55e-03 +5.42e-03 +6.30e-02 +5.46e-02 +6.94e-02 +7.56e-02 +5.76e-02 +4.99e-02 +6.77e-02 +4.79e-02 +4.29e-02 +5.31e-02 +5.73e-02 +4.32e-02 +2.01e-02 +8.20e-03 +4.18e-03 +6.21e-02 +5.98e-02 +7.34e-02 +7.91e-02 +4.60e-02 +5.73e-02 +6.01e-02 +5.26e-02 +5.01e-02 +3.98e-02 +3.12e-02 +2.62e-02 +1.44e-02 +6.44e-03 +6.95e-03 +2.95e-02 +5.38e-02 +4.94e-02 +4.81e-02 +5.65e-02 +6.07e-02 +3.70e-02 +3.88e-02 +2.59e-02 +2.05e-02 +2.51e-02 +2.29e-02 +1.19e-02 +1.08e-02 +8.34e-03 +3.62e-02 +4.00e-02 +5.20e-02 +6.63e-02 +6.42e-02 +5.90e-02 +3.69e-02 +4.87e-02 +4.00e-02 +3.31e-02 +2.85e-02 +2.46e-02 +2.05e-02 +8.72e-03 +3.45e-03 +2.17e-02 +4.81e-02 +6.28e-02 +6.24e-02 +4.43e-02 +2.41e-02 +3.73e-02 +4.41e-02 +4.25e-02 +2.80e-02 +2.62e-02 +2.10e-02 +1.12e-02 +1.28e-02 +6.90e-03 +2.96e-02 +4.88e-02 +5.84e-02 +2.83e-02 +4.58e-02 +3.78e-02 +2.43e-02 +3.47e-02 +3.08e-02 +2.18e-02 +2.32e-02 +1.70e-02 +1.26e-02 +9.95e-03 +8.03e-03 +2.02e-02 +2.21e-02 +3.09e-02 +2.79e-02 +2.92e-02 +4.73e-02 +3.85e-02 +2.69e-02 +1.89e-02 +1.70e-02 +1.44e-02 +1.42e-02 +1.51e-02 +4.21e-03 +1.55e-03 +1.72e-02 +1.70e-02 +2.18e-02 +2.50e-02 +1.84e-02 +3.18e-02 +2.53e-02 +2.01e-02 +1.37e-02 +1.89e-02 +1.02e-02 +1.15e-02 +1.16e-02 +3.85e-03 +1.22e-03 +5.45e-03 +9.53e-03 +9.49e-03 +6.68e-03 +1.08e-02 +1.59e-02 +1.34e-02 +1.53e-02 +1.86e-02 +1.03e-02 +1.07e-02 +6.50e-03 +3.98e-03 +2.38e-03 +2.29e-03 +4.46e-03 +5.06e-03 +5.22e-03 +4.55e-03 +5.72e-03 +1.00e-02 +6.07e-03 +5.95e-03 +1.20e-02 +8.03e-03 +5.95e-03 +3.61e-03 +1.35e-03 +8.82e-04 +1.47e-03 +Upper Bounds +8.83e-01 +9.04e-01 +9.04e-01 +9.06e-01 +8.90e-01 +8.71e-01 +8.51e-01 +8.82e-01 +8.99e-01 +9.42e-01 +8.75e-01 +9.67e-01 +1.38e+00 +2.14e+00 +7.89e-01 +8.70e-01 +9.12e-01 +9.06e-01 +8.15e-01 +8.92e-01 +9.06e-01 +8.84e-01 +8.83e-01 +8.68e-01 +8.58e-01 +9.07e-01 +9.12e-01 +1.29e+00 +1.63e+00 +5.33e-01 +9.10e-01 +8.73e-01 +9.05e-01 +9.08e-01 +8.81e-01 +8.85e-01 +8.70e-01 +8.31e-01 +8.30e-01 +8.46e-01 +8.79e-01 +9.36e-01 +1.33e+00 +2.08e+00 +7.86e-01 +8.80e-01 +8.72e-01 +8.61e-01 +8.81e-01 +8.35e-01 +8.38e-01 +8.52e-01 +8.74e-01 +8.63e-01 +8.63e-01 +8.27e-01 +9.20e-01 +1.33e+00 +2.00e+00 +6.39e-01 +8.76e-01 +9.10e-01 +9.11e-01 +9.20e-01 +8.60e-01 +8.61e-01 +8.34e-01 +8.36e-01 +8.28e-01 +8.72e-01 +8.44e-01 +9.49e-01 +1.41e+00 +1.65e+00 +6.46e-01 +9.01e-01 +8.97e-01 +8.94e-01 +8.90e-01 +8.81e-01 +8.46e-01 +8.38e-01 +8.15e-01 +8.49e-01 +8.29e-01 +8.56e-01 +9.61e-01 +1.35e+00 +1.44e+00 +5.12e-01 +8.79e-01 +8.59e-01 +8.42e-01 +8.55e-01 +8.31e-01 +8.41e-01 +8.24e-01 +8.31e-01 +8.11e-01 +7.99e-01 +8.06e-01 +8.32e-01 +1.19e+00 +1.66e+00 +6.17e-01 +9.09e-01 +9.03e-01 +8.95e-01 +8.51e-01 +8.33e-01 +8.62e-01 +8.26e-01 +8.10e-01 +7.35e-01 +7.80e-01 +7.82e-01 +7.20e-01 +1.00e+00 +8.79e-01 +3.78e-01 +8.81e-01 +8.79e-01 +8.98e-01 +9.09e-01 +8.56e-01 +8.36e-01 +8.27e-01 +8.09e-01 +7.69e-01 +7.73e-01 +7.71e-01 +7.76e-01 +1.14e+00 +8.33e-01 +2.68e-01 +8.83e-01 +9.23e-01 +9.16e-01 +9.05e-01 +8.94e-01 +8.03e-01 +8.19e-01 +7.89e-01 +8.04e-01 +7.99e-01 +7.77e-01 +7.67e-01 +1.16e+00 +8.69e-01 +3.26e-01 +9.04e-01 +8.93e-01 +9.15e-01 +8.96e-01 +8.99e-01 +8.47e-01 +7.92e-01 +8.32e-01 +7.94e-01 +7.96e-01 +8.49e-01 +7.81e-01 +1.06e+00 +6.40e-01 +1.87e-01 +9.49e-01 +9.48e-01 +9.08e-01 +8.57e-01 +9.51e-01 +9.10e-01 +8.62e-01 +8.93e-01 +8.43e-01 +8.47e-01 +8.29e-01 +7.55e-01 +1.11e+00 +4.81e-01 +1.57e-01 +1.38e+00 +1.37e+00 +1.26e+00 +1.31e+00 +1.37e+00 +1.34e+00 +1.26e+00 +1.21e+00 +1.21e+00 +1.21e+00 +1.20e+00 +1.12e+00 +1.06e+00 +3.64e-01 +1.05e-01 +2.50e+00 +1.75e+00 +1.96e+00 +1.28e+00 +1.40e+00 +1.59e+00 +1.27e+00 +1.18e+00 +9.31e-01 +9.87e-01 +9.06e-01 +7.50e-01 +4.87e-01 +1.88e-01 +9.31e-02 +1.08e+00 +7.08e-01 +8.14e-01 +4.09e-01 +4.59e-01 +5.72e-01 +4.06e-01 +3.97e-01 +3.49e-01 +3.11e-01 +3.14e-01 +2.24e-01 +1.84e-01 +7.93e-02 +3.58e-02 +8.56e-01 +8.80e-01 +8.90e-01 +8.72e-01 +8.83e-01 +8.79e-01 +9.07e-01 +8.87e-01 +8.46e-01 +8.81e-01 +9.13e-01 +9.22e-01 +1.35e+00 +1.99e+00 +7.68e-01 +8.95e-01 +8.96e-01 +9.01e-01 +8.77e-01 +8.78e-01 +8.57e-01 +8.71e-01 +8.72e-01 +8.61e-01 +8.65e-01 +8.84e-01 +9.25e-01 +1.27e+00 +1.84e+00 +6.71e-01 +9.00e-01 +8.80e-01 +8.68e-01 +8.83e-01 +8.81e-01 +8.83e-01 +8.68e-01 +8.89e-01 +8.61e-01 +9.00e-01 +9.04e-01 +9.07e-01 +1.33e+00 +1.66e+00 +5.75e-01 +9.10e-01 +9.28e-01 +9.04e-01 +9.15e-01 +8.67e-01 +8.37e-01 +8.58e-01 +8.74e-01 +8.49e-01 +8.43e-01 +8.73e-01 +9.20e-01 +1.29e+00 +1.77e+00 +6.31e-01 +8.99e-01 +9.11e-01 +8.97e-01 +8.66e-01 +8.29e-01 +8.49e-01 +8.85e-01 +8.47e-01 +8.39e-01 +8.47e-01 +8.91e-01 +9.17e-01 +1.38e+00 +1.51e+00 +6.62e-01 +8.74e-01 +8.88e-01 +8.84e-01 +8.46e-01 +8.97e-01 +8.43e-01 +8.27e-01 +8.17e-01 +8.15e-01 +8.35e-01 +8.69e-01 +9.33e-01 +1.35e+00 +1.72e+00 +6.01e-01 +9.01e-01 +9.12e-01 +8.41e-01 +8.34e-01 +8.86e-01 +8.49e-01 +7.94e-01 +7.99e-01 +7.91e-01 +8.03e-01 +8.26e-01 +8.34e-01 +1.17e+00 +1.46e+00 +5.66e-01 +8.53e-01 +8.62e-01 +8.25e-01 +8.26e-01 +8.26e-01 +7.99e-01 +7.85e-01 +7.29e-01 +7.14e-01 +7.51e-01 +7.41e-01 +5.28e-01 +9.65e-01 +8.41e-01 +3.51e-01 +8.43e-01 +9.30e-01 +8.75e-01 +8.72e-01 +8.86e-01 +8.25e-01 +8.06e-01 +7.70e-01 +7.66e-01 +7.77e-01 +7.36e-01 +7.06e-01 +9.91e-01 +6.14e-01 +1.92e-01 +8.89e-01 +9.28e-01 +8.82e-01 +8.85e-01 +8.26e-01 +8.77e-01 +8.28e-01 +7.72e-01 +7.97e-01 +7.70e-01 +8.14e-01 +7.60e-01 +1.08e+00 +7.25e-01 +2.17e-01 +8.99e-01 +9.33e-01 +8.95e-01 +8.67e-01 +9.07e-01 +8.80e-01 +8.33e-01 +7.98e-01 +8.16e-01 +7.99e-01 +8.47e-01 +7.75e-01 +1.08e+00 +6.72e-01 +2.06e-01 +9.39e-01 +8.99e-01 +9.19e-01 +9.05e-01 +9.21e-01 +9.37e-01 +8.70e-01 +8.47e-01 +8.32e-01 +8.66e-01 +8.48e-01 +8.12e-01 +1.00e+00 +4.14e-01 +1.43e-01 +1.29e+00 +1.41e+00 +1.29e+00 +1.35e+00 +1.47e+00 +1.34e+00 +1.23e+00 +1.28e+00 +1.17e+00 +1.16e+00 +1.29e+00 +1.20e+00 +1.04e+00 +2.68e-01 +6.98e-02 +1.56e+00 +1.49e+00 +1.36e+00 +1.52e+00 +1.85e+00 +1.77e+00 +1.39e+00 +1.40e+00 +1.09e+00 +8.12e-01 +8.12e-01 +8.15e-01 +5.39e-01 +1.74e-01 +7.42e-02 +5.46e-01 +5.41e-01 +5.27e-01 +5.05e-01 +6.76e-01 +7.08e-01 +4.76e-01 +5.00e-01 +4.66e-01 +2.59e-01 +2.59e-01 +2.67e-01 +2.12e-01 +9.34e-02 +3.59e-02 +8.98e-01 +8.76e-01 +9.11e-01 +8.74e-01 +8.57e-01 +8.49e-01 +8.45e-01 +8.23e-01 +8.73e-01 +8.56e-01 +8.41e-01 +9.32e-01 +1.56e+00 +2.14e+00 +8.32e-01 +9.04e-01 +8.85e-01 +8.48e-01 +8.66e-01 +8.66e-01 +8.58e-01 +8.22e-01 +8.28e-01 +8.32e-01 +8.65e-01 +8.49e-01 +9.47e-01 +1.32e+00 +2.01e+00 +8.20e-01 +8.96e-01 +9.02e-01 +9.15e-01 +8.55e-01 +8.63e-01 +8.56e-01 +8.51e-01 +8.73e-01 +8.47e-01 +8.41e-01 +8.38e-01 +8.99e-01 +1.24e+00 +1.71e+00 +6.45e-01 +8.13e-01 +8.67e-01 +8.68e-01 +8.57e-01 +8.41e-01 +8.27e-01 +8.41e-01 +8.63e-01 +8.60e-01 +8.27e-01 +8.62e-01 +8.45e-01 +1.26e+00 +1.59e+00 +5.28e-01 +8.37e-01 +8.48e-01 +8.61e-01 +8.18e-01 +8.55e-01 +8.57e-01 +7.87e-01 +8.18e-01 +8.05e-01 +8.09e-01 +8.66e-01 +8.95e-01 +1.31e+00 +1.33e+00 +3.87e-01 +8.40e-01 +8.59e-01 +8.21e-01 +8.32e-01 +8.27e-01 +8.30e-01 +7.89e-01 +7.95e-01 +8.16e-01 +8.14e-01 +8.48e-01 +8.74e-01 +1.25e+00 +1.74e+00 +6.56e-01 +8.62e-01 +8.97e-01 +8.49e-01 +8.48e-01 +8.43e-01 +8.56e-01 +8.49e-01 +7.73e-01 +7.97e-01 +8.23e-01 +8.50e-01 +8.53e-01 +1.22e+00 +1.23e+00 +4.60e-01 +8.51e-01 +8.51e-01 +8.30e-01 +8.17e-01 +8.30e-01 +8.17e-01 +7.95e-01 +7.91e-01 +7.83e-01 +7.99e-01 +7.74e-01 +7.63e-01 +1.10e+00 +8.15e-01 +3.18e-01 +8.51e-01 +8.51e-01 +8.64e-01 +8.45e-01 +8.30e-01 +8.19e-01 +8.14e-01 +8.18e-01 +7.60e-01 +7.72e-01 +7.64e-01 +7.45e-01 +1.06e+00 +8.19e-01 +3.07e-01 +8.70e-01 +8.92e-01 +8.63e-01 +8.73e-01 +8.70e-01 +8.23e-01 +8.34e-01 +7.96e-01 +7.41e-01 +7.85e-01 +7.76e-01 +7.97e-01 +1.03e+00 +6.15e-01 +2.40e-01 +8.46e-01 +8.88e-01 +8.83e-01 +8.69e-01 +8.83e-01 +8.65e-01 +8.68e-01 +8.40e-01 +7.58e-01 +7.91e-01 +7.91e-01 +7.77e-01 +1.06e+00 +6.09e-01 +1.67e-01 +8.76e-01 +9.27e-01 +9.32e-01 +9.51e-01 +9.02e-01 +8.97e-01 +8.78e-01 +8.56e-01 +8.35e-01 +8.55e-01 +8.47e-01 +8.43e-01 +9.29e-01 +5.11e-01 +1.77e-01 +1.33e+00 +1.33e+00 +1.32e+00 +1.25e+00 +1.30e+00 +1.29e+00 +1.17e+00 +1.21e+00 +1.18e+00 +1.15e+00 +1.16e+00 +1.21e+00 +1.14e+00 +2.72e-01 +8.55e-02 +1.89e+00 +1.37e+00 +1.71e+00 +1.75e+00 +1.82e+00 +1.16e+00 +1.16e+00 +1.09e+00 +9.10e-01 +8.87e-01 +6.38e-01 +5.79e-01 +5.13e-01 +2.08e-01 +5.28e-02 +6.66e-01 +4.35e-01 +7.04e-01 +6.72e-01 +6.85e-01 +4.41e-01 +3.91e-01 +4.18e-01 +2.80e-01 +3.20e-01 +1.45e-01 +1.85e-01 +1.73e-01 +1.30e-01 +6.20e-02 +8.83e-01 +8.54e-01 +8.74e-01 +8.97e-01 +8.66e-01 +8.53e-01 +8.20e-01 +8.29e-01 +7.95e-01 +8.07e-01 +7.74e-01 +8.95e-01 +1.34e+00 +2.13e+00 +7.78e-01 +8.75e-01 +9.11e-01 +8.53e-01 +8.24e-01 +8.43e-01 +8.30e-01 +8.44e-01 +8.16e-01 +8.23e-01 +8.70e-01 +8.45e-01 +8.81e-01 +1.24e+00 +1.66e+00 +6.51e-01 +8.43e-01 +8.65e-01 +8.86e-01 +8.33e-01 +7.75e-01 +8.33e-01 +7.93e-01 +8.14e-01 +7.91e-01 +8.49e-01 +8.48e-01 +9.21e-01 +1.22e+00 +1.35e+00 +5.11e-01 +8.45e-01 +8.50e-01 +8.67e-01 +8.43e-01 +8.32e-01 +8.35e-01 +8.18e-01 +8.02e-01 +8.09e-01 +8.16e-01 +8.13e-01 +8.33e-01 +1.24e+00 +1.20e+00 +4.92e-01 +8.11e-01 +8.21e-01 +8.46e-01 +8.56e-01 +8.12e-01 +7.90e-01 +8.10e-01 +7.88e-01 +8.08e-01 +8.07e-01 +8.57e-01 +8.34e-01 +1.25e+00 +1.61e+00 +5.85e-01 +8.68e-01 +7.99e-01 +8.45e-01 +8.59e-01 +8.33e-01 +7.98e-01 +8.23e-01 +8.40e-01 +8.12e-01 +8.01e-01 +8.15e-01 +8.78e-01 +1.27e+00 +1.49e+00 +5.48e-01 +8.43e-01 +8.37e-01 +8.81e-01 +7.97e-01 +7.95e-01 +7.90e-01 +8.20e-01 +7.94e-01 +7.75e-01 +7.96e-01 +7.56e-01 +8.00e-01 +1.25e+00 +1.44e+00 +5.06e-01 +8.60e-01 +8.53e-01 +8.41e-01 +8.13e-01 +7.94e-01 +8.04e-01 +8.30e-01 +7.78e-01 +7.68e-01 +7.54e-01 +7.46e-01 +7.89e-01 +1.17e+00 +9.25e-01 +3.31e-01 +8.47e-01 +8.26e-01 +8.14e-01 +8.34e-01 +8.09e-01 +8.13e-01 +8.14e-01 +8.04e-01 +7.77e-01 +7.65e-01 +7.94e-01 +8.47e-01 +1.24e+00 +9.33e-01 +2.64e-01 +8.55e-01 +8.28e-01 +8.67e-01 +8.38e-01 +8.25e-01 +8.22e-01 +7.96e-01 +7.62e-01 +7.99e-01 +7.61e-01 +7.38e-01 +7.68e-01 +1.16e+00 +6.75e-01 +2.70e-01 +8.80e-01 +8.88e-01 +8.39e-01 +8.92e-01 +8.67e-01 +8.43e-01 +8.12e-01 +8.07e-01 +7.97e-01 +8.03e-01 +7.88e-01 +8.01e-01 +1.03e+00 +4.71e-01 +1.39e-01 +9.42e-01 +8.84e-01 +8.89e-01 +4.30e-01 +9.12e-01 +9.52e-01 +8.80e-01 +8.13e-01 +8.44e-01 +8.47e-01 +8.47e-01 +8.42e-01 +9.87e-01 +3.92e-01 +1.14e-01 +1.24e+00 +1.25e+00 +1.29e+00 +1.25e+00 +1.29e+00 +1.28e+00 +1.08e+00 +1.14e+00 +1.21e+00 +1.12e+00 +1.06e+00 +1.07e+00 +1.14e+00 +3.91e-01 +1.23e-01 +1.55e+00 +1.24e+00 +1.03e+00 +1.46e+00 +1.44e+00 +1.17e+00 +8.10e-01 +5.67e-01 +9.52e-01 +9.00e-01 +5.78e-01 +5.33e-01 +4.71e-01 +2.10e-01 +8.15e-02 +5.84e-01 +4.29e-01 +4.39e-01 +4.57e-01 +5.73e-01 +4.28e-01 +3.39e-01 +1.82e-01 +3.17e-01 +3.19e-01 +2.03e-01 +1.80e-01 +1.39e-01 +1.28e-01 +6.06e-02 +8.53e-01 +8.42e-01 +8.16e-01 +8.39e-01 +8.62e-01 +8.30e-01 +8.25e-01 +7.57e-01 +7.84e-01 +8.17e-01 +7.86e-01 +8.91e-01 +1.29e+00 +1.58e+00 +6.19e-01 +8.62e-01 +8.53e-01 +8.28e-01 +8.47e-01 +8.54e-01 +8.13e-01 +7.89e-01 +7.56e-01 +8.10e-01 +8.39e-01 +8.37e-01 +8.80e-01 +1.27e+00 +1.52e+00 +5.61e-01 +8.76e-01 +8.58e-01 +8.51e-01 +8.40e-01 +8.16e-01 +8.22e-01 +8.17e-01 +7.68e-01 +8.23e-01 +8.47e-01 +8.38e-01 +8.80e-01 +1.26e+00 +1.11e+00 +4.01e-01 +8.41e-01 +8.55e-01 +8.26e-01 +8.31e-01 +8.17e-01 +8.22e-01 +8.05e-01 +8.08e-01 +8.05e-01 +8.32e-01 +8.20e-01 +8.61e-01 +1.30e+00 +1.05e+00 +3.57e-01 +8.15e-01 +8.06e-01 +8.14e-01 +8.44e-01 +8.05e-01 +8.12e-01 +8.12e-01 +8.06e-01 +7.90e-01 +7.71e-01 +8.19e-01 +8.79e-01 +1.29e+00 +1.63e+00 +5.79e-01 +7.99e-01 +7.98e-01 +8.14e-01 +8.39e-01 +8.09e-01 +7.70e-01 +7.51e-01 +8.22e-01 +8.15e-01 +7.92e-01 +7.89e-01 +8.42e-01 +1.22e+00 +1.30e+00 +4.67e-01 +8.04e-01 +8.15e-01 +8.41e-01 +8.12e-01 +8.44e-01 +7.64e-01 +7.65e-01 +7.83e-01 +7.51e-01 +7.69e-01 +7.83e-01 +7.95e-01 +1.09e+00 +1.03e+00 +3.79e-01 +8.53e-01 +8.48e-01 +8.17e-01 +7.87e-01 +8.03e-01 +7.66e-01 +7.80e-01 +7.47e-01 +7.47e-01 +7.94e-01 +7.46e-01 +7.73e-01 +1.13e+00 +8.10e-01 +3.44e-01 +8.55e-01 +8.35e-01 +8.31e-01 +8.01e-01 +8.04e-01 +8.11e-01 +8.17e-01 +7.71e-01 +7.52e-01 +7.41e-01 +7.55e-01 +7.73e-01 +1.08e+00 +8.26e-01 +2.48e-01 +8.55e-01 +8.65e-01 +8.39e-01 +8.10e-01 +7.84e-01 +8.10e-01 +7.97e-01 +7.80e-01 +7.63e-01 +7.36e-01 +7.45e-01 +8.12e-01 +1.07e+00 +7.90e-01 +3.43e-01 +8.52e-01 +8.31e-01 +8.47e-01 +8.29e-01 +8.34e-01 +8.10e-01 +8.16e-01 +8.13e-01 +7.91e-01 +7.58e-01 +7.69e-01 +8.34e-01 +1.13e+00 +5.77e-01 +2.01e-01 +8.71e-01 +9.08e-01 +8.51e-01 +8.61e-01 +8.87e-01 +9.24e-01 +8.39e-01 +8.24e-01 +8.13e-01 +8.68e-01 +8.13e-01 +8.31e-01 +1.14e+00 +5.34e-01 +1.63e-01 +1.29e+00 +1.23e+00 +1.25e+00 +1.29e+00 +1.30e+00 +1.33e+00 +1.29e+00 +1.19e+00 +1.17e+00 +1.14e+00 +1.08e+00 +1.12e+00 +1.16e+00 +4.48e-01 +1.51e-01 +1.37e+00 +1.25e+00 +1.26e+00 +1.31e+00 +1.52e+00 +1.34e+00 +1.46e+00 +9.26e-01 +1.00e+00 +7.33e-01 +5.03e-01 +5.86e-01 +4.40e-01 +1.93e-01 +7.63e-02 +5.00e-01 +4.36e-01 +4.55e-01 +4.50e-01 +5.38e-01 +4.35e-01 +5.87e-01 +2.74e-01 +3.52e-01 +2.53e-01 +1.54e-01 +1.57e-01 +1.32e-01 +8.46e-02 +5.80e-02 +7.99e-01 +8.20e-01 +8.15e-01 +8.33e-01 +8.51e-01 +8.10e-01 +7.68e-01 +7.62e-01 +7.58e-01 +7.77e-01 +7.87e-01 +8.14e-01 +1.30e+00 +1.29e+00 +3.79e-01 +7.97e-01 +8.41e-01 +8.13e-01 +7.89e-01 +8.26e-01 +8.24e-01 +8.37e-01 +7.59e-01 +7.92e-01 +7.78e-01 +7.98e-01 +8.46e-01 +1.24e+00 +1.44e+00 +5.54e-01 +8.32e-01 +8.44e-01 +8.48e-01 +8.16e-01 +8.13e-01 +7.97e-01 +8.17e-01 +8.24e-01 +7.90e-01 +7.89e-01 +7.98e-01 +8.75e-01 +1.29e+00 +1.26e+00 +4.10e-01 +8.34e-01 +8.40e-01 +7.91e-01 +7.91e-01 +8.06e-01 +8.33e-01 +7.71e-01 +7.76e-01 +8.05e-01 +8.07e-01 +8.15e-01 +8.50e-01 +1.26e+00 +1.18e+00 +3.80e-01 +8.13e-01 +8.32e-01 +8.39e-01 +8.18e-01 +7.93e-01 +7.93e-01 +7.84e-01 +7.91e-01 +7.91e-01 +7.83e-01 +7.90e-01 +8.30e-01 +1.25e+00 +1.29e+00 +4.90e-01 +8.01e-01 +8.04e-01 +8.21e-01 +8.03e-01 +7.98e-01 +7.82e-01 +7.55e-01 +7.52e-01 +8.14e-01 +8.19e-01 +7.83e-01 +8.19e-01 +1.18e+00 +1.09e+00 +4.51e-01 +8.40e-01 +8.48e-01 +8.18e-01 +7.81e-01 +7.89e-01 +7.65e-01 +7.53e-01 +7.17e-01 +7.54e-01 +7.65e-01 +7.76e-01 +8.17e-01 +1.09e+00 +6.64e-01 +2.45e-01 +8.36e-01 +7.99e-01 +8.04e-01 +7.74e-01 +8.20e-01 +7.78e-01 +7.45e-01 +7.25e-01 +7.18e-01 +7.55e-01 +7.87e-01 +8.22e-01 +1.16e+00 +5.88e-01 +1.82e-01 +8.12e-01 +8.33e-01 +8.42e-01 +7.73e-01 +7.95e-01 +7.85e-01 +7.91e-01 +7.28e-01 +7.16e-01 +6.95e-01 +7.39e-01 +7.71e-01 +1.13e+00 +7.23e-01 +2.02e-01 +7.76e-01 +8.13e-01 +8.17e-01 +8.37e-01 +7.81e-01 +7.65e-01 +7.51e-01 +7.47e-01 +7.44e-01 +7.26e-01 +7.33e-01 +7.63e-01 +1.05e+00 +6.00e-01 +1.91e-01 +8.27e-01 +7.94e-01 +8.07e-01 +8.05e-01 +7.80e-01 +7.80e-01 +8.00e-01 +7.93e-01 +7.69e-01 +7.64e-01 +7.39e-01 +7.58e-01 +1.04e+00 +5.07e-01 +1.50e-01 +8.87e-01 +8.55e-01 +8.41e-01 +8.56e-01 +8.73e-01 +8.47e-01 +8.19e-01 +8.38e-01 +8.05e-01 +8.07e-01 +7.65e-01 +7.30e-01 +9.77e-01 +4.70e-01 +1.10e-01 +1.23e+00 +1.19e+00 +1.16e+00 +1.21e+00 +1.27e+00 +1.25e+00 +1.14e+00 +1.10e+00 +1.11e+00 +1.13e+00 +1.17e+00 +1.01e+00 +9.39e-01 +4.79e-01 +1.76e-01 +1.53e+00 +1.30e+00 +1.11e+00 +1.31e+00 +1.28e+00 +1.18e+00 +1.16e+00 +8.91e-01 +7.64e-01 +6.86e-01 +7.35e-01 +6.78e-01 +3.62e-01 +2.02e-01 +1.12e-01 +5.19e-01 +4.91e-01 +4.40e-01 +4.60e-01 +4.87e-01 +3.65e-01 +5.18e-01 +3.03e-01 +2.69e-01 +1.84e-01 +2.14e-01 +2.43e-01 +1.44e-01 +7.93e-02 +4.59e-02 +8.53e-01 +8.34e-01 +8.24e-01 +8.44e-01 +7.93e-01 +7.98e-01 +8.11e-01 +8.08e-01 +7.84e-01 +7.45e-01 +7.60e-01 +7.66e-01 +1.21e+00 +9.01e-01 +4.10e-01 +8.39e-01 +8.13e-01 +8.33e-01 +8.25e-01 +8.17e-01 +7.68e-01 +7.94e-01 +7.61e-01 +7.94e-01 +7.58e-01 +7.43e-01 +7.76e-01 +1.12e+00 +1.01e+00 +3.78e-01 +8.15e-01 +7.96e-01 +8.12e-01 +8.03e-01 +7.86e-01 +7.79e-01 +7.51e-01 +7.57e-01 +7.42e-01 +7.66e-01 +8.04e-01 +7.92e-01 +1.16e+00 +1.25e+00 +5.04e-01 +8.34e-01 +7.68e-01 +7.75e-01 +7.60e-01 +7.73e-01 +8.04e-01 +7.87e-01 +7.68e-01 +7.69e-01 +7.72e-01 +7.79e-01 +8.20e-01 +1.12e+00 +7.80e-01 +3.07e-01 +8.35e-01 +8.24e-01 +8.03e-01 +7.87e-01 +7.88e-01 +8.12e-01 +7.74e-01 +7.87e-01 +7.48e-01 +7.70e-01 +7.77e-01 +8.41e-01 +1.16e+00 +6.39e-01 +2.34e-01 +8.17e-01 +7.87e-01 +7.98e-01 +8.15e-01 +8.06e-01 +7.88e-01 +7.52e-01 +7.32e-01 +7.17e-01 +7.61e-01 +7.74e-01 +8.04e-01 +1.13e+00 +6.75e-01 +2.06e-01 +8.22e-01 +8.10e-01 +8.02e-01 +8.08e-01 +8.05e-01 +7.56e-01 +7.48e-01 +7.03e-01 +7.31e-01 +7.43e-01 +7.54e-01 +8.22e-01 +1.16e+00 +7.52e-01 +2.28e-01 +7.64e-01 +7.75e-01 +7.68e-01 +7.87e-01 +7.73e-01 +7.45e-01 +7.48e-01 +7.22e-01 +7.13e-01 +7.34e-01 +7.57e-01 +7.86e-01 +1.10e+00 +8.15e-01 +2.44e-01 +8.21e-01 +3.30e-01 +7.87e-01 +7.66e-01 +7.39e-01 +7.65e-01 +3.79e-01 +6.99e-01 +6.88e-01 +6.98e-01 +7.52e-01 +7.85e-01 +1.05e+00 +7.70e-01 +2.57e-01 +7.79e-01 +7.89e-01 +8.06e-01 +7.85e-01 +7.59e-01 +7.34e-01 +7.57e-01 +7.39e-01 +7.18e-01 +7.39e-01 +7.21e-01 +7.16e-01 +1.03e+00 +6.02e-01 +2.37e-01 +8.09e-01 +8.54e-01 +8.12e-01 +7.85e-01 +7.60e-01 +7.56e-01 +7.18e-01 +7.13e-01 +7.31e-01 +7.31e-01 +6.81e-01 +7.25e-01 +9.31e-01 +3.54e-01 +1.26e-01 +8.50e-01 +8.54e-01 +8.14e-01 +7.80e-01 +8.02e-01 +8.05e-01 +7.81e-01 +7.77e-01 +7.97e-01 +7.65e-01 +7.40e-01 +7.05e-01 +7.73e-01 +3.06e-01 +1.34e-01 +1.20e+00 +1.12e+00 +1.11e+00 +1.13e+00 +1.15e+00 +1.10e+00 +1.10e+00 +9.95e-01 +1.08e+00 +9.88e-01 +1.01e+00 +9.01e-01 +6.74e-01 +1.87e-01 +8.09e-02 +1.32e+00 +1.34e+00 +1.16e+00 +7.75e-01 +7.51e-01 +9.97e-01 +8.93e-01 +7.22e-01 +6.51e-01 +6.25e-01 +7.18e-01 +5.73e-01 +2.11e-01 +9.82e-02 +4.26e-02 +4.75e-01 +5.18e-01 +4.49e-01 +2.40e-01 +2.44e-01 +3.22e-01 +3.19e-01 +2.35e-01 +2.20e-01 +1.64e-01 +2.36e-01 +2.02e-01 +7.60e-02 +4.33e-02 +3.07e-02 +7.81e-01 +7.87e-01 +7.72e-01 +8.00e-01 +8.25e-01 +8.31e-01 +8.27e-01 +7.89e-01 +7.35e-01 +7.02e-01 +6.79e-01 +7.96e-01 +1.11e+00 +7.59e-01 +2.66e-01 +8.05e-01 +8.08e-01 +8.45e-01 +8.16e-01 +7.90e-01 +7.90e-01 +7.97e-01 +7.35e-01 +7.24e-01 +6.97e-01 +6.50e-01 +7.41e-01 +1.08e+00 +5.46e-01 +1.73e-01 +7.91e-01 +7.84e-01 +8.12e-01 +8.14e-01 +7.93e-01 +7.86e-01 +7.80e-01 +7.88e-01 +7.62e-01 +7.26e-01 +7.48e-01 +8.00e-01 +1.08e+00 +7.80e-01 +2.83e-01 +7.99e-01 +7.86e-01 +8.00e-01 +7.83e-01 +8.01e-01 +7.89e-01 +7.68e-01 +7.67e-01 +7.34e-01 +7.08e-01 +7.83e-01 +8.28e-01 +1.05e+00 +7.28e-01 +3.01e-01 +8.10e-01 +8.05e-01 +8.41e-01 +7.70e-01 +7.70e-01 +7.93e-01 +7.51e-01 +7.44e-01 +7.48e-01 +7.29e-01 +7.92e-01 +7.97e-01 +1.08e+00 +5.90e-01 +1.62e-01 +8.06e-01 +7.76e-01 +8.01e-01 +7.96e-01 +7.50e-01 +7.27e-01 +7.51e-01 +7.26e-01 +7.36e-01 +7.31e-01 +7.41e-01 +7.92e-01 +1.07e+00 +6.18e-01 +2.17e-01 +7.91e-01 +7.77e-01 +7.90e-01 +7.54e-01 +7.85e-01 +7.70e-01 +7.48e-01 +6.96e-01 +6.99e-01 +7.02e-01 +6.96e-01 +7.43e-01 +1.02e+00 +7.52e-01 +2.89e-01 +7.56e-01 +7.94e-01 +7.67e-01 +7.64e-01 +7.61e-01 +7.60e-01 +7.28e-01 +7.14e-01 +7.09e-01 +7.03e-01 +7.62e-01 +7.59e-01 +1.03e+00 +6.86e-01 +2.56e-01 +7.75e-01 +7.59e-01 +7.81e-01 +7.45e-01 +7.66e-01 +7.46e-01 +7.25e-01 +6.93e-01 +6.75e-01 +6.66e-01 +7.38e-01 +7.32e-01 +9.88e-01 +5.41e-01 +1.68e-01 +7.63e-01 +7.64e-01 +7.46e-01 +7.17e-01 +7.23e-01 +7.40e-01 +6.88e-01 +7.11e-01 +6.45e-01 +6.43e-01 +6.92e-01 +7.27e-01 +1.06e+00 +5.99e-01 +1.94e-01 +7.99e-01 +7.77e-01 +7.94e-01 +7.47e-01 +7.40e-01 +7.39e-01 +7.34e-01 +7.52e-01 +7.06e-01 +6.86e-01 +6.55e-01 +6.81e-01 +8.86e-01 +3.82e-01 +9.69e-02 +8.46e-01 +8.18e-01 +8.26e-01 +7.84e-01 +7.60e-01 +7.38e-01 +7.20e-01 +7.68e-01 +7.41e-01 +7.47e-01 +7.02e-01 +6.54e-01 +7.66e-01 +2.43e-01 +7.12e-02 +1.21e+00 +1.12e+00 +1.12e+00 +1.01e+00 +1.09e+00 +1.07e+00 +8.74e-01 +8.72e-01 +8.67e-01 +1.01e+00 +1.01e+00 +8.47e-01 +6.09e-01 +1.58e-01 +5.39e-02 +1.08e+00 +1.12e+00 +8.01e-01 +6.68e-01 +7.20e-01 +8.39e-01 +5.11e-01 +4.35e-01 +3.60e-01 +7.14e-01 +6.43e-01 +4.11e-01 +2.15e-01 +5.39e-02 +2.14e-02 +3.73e-01 +3.83e-01 +2.54e-01 +2.38e-01 +2.15e-01 +2.86e-01 +1.89e-01 +1.46e-01 +1.14e-01 +2.39e-01 +2.00e-01 +1.38e-01 +9.65e-02 +4.13e-02 +1.06e-02 +8.16e-01 +8.31e-01 +8.55e-01 +8.17e-01 +8.37e-01 +7.96e-01 +7.81e-01 +8.05e-01 +7.46e-01 +7.27e-01 +6.91e-01 +7.68e-01 +1.08e+00 +5.87e-01 +1.78e-01 +7.93e-01 +8.07e-01 +8.40e-01 +8.16e-01 +8.12e-01 +8.09e-01 +7.98e-01 +7.79e-01 +7.43e-01 +7.35e-01 +7.18e-01 +7.62e-01 +1.05e+00 +6.30e-01 +1.97e-01 +7.86e-01 +8.04e-01 +7.90e-01 +7.96e-01 +8.02e-01 +7.57e-01 +7.80e-01 +7.99e-01 +7.43e-01 +7.26e-01 +7.20e-01 +8.04e-01 +1.21e+00 +8.14e-01 +2.65e-01 +8.01e-01 +7.70e-01 +8.02e-01 +7.66e-01 +7.78e-01 +7.86e-01 +7.74e-01 +7.63e-01 +7.30e-01 +7.36e-01 +7.45e-01 +7.37e-01 +1.04e+00 +8.80e-01 +3.72e-01 +7.76e-01 +7.61e-01 +7.65e-01 +7.59e-01 +7.59e-01 +7.54e-01 +7.53e-01 +7.66e-01 +7.41e-01 +7.01e-01 +7.50e-01 +8.07e-01 +1.03e+00 +6.47e-01 +2.01e-01 +8.00e-01 +7.91e-01 +7.50e-01 +7.33e-01 +7.09e-01 +7.28e-01 +7.48e-01 +7.31e-01 +7.26e-01 +7.09e-01 +7.38e-01 +8.00e-01 +1.08e+00 +7.50e-01 +2.61e-01 +7.64e-01 +7.45e-01 +7.60e-01 +7.26e-01 +7.24e-01 +7.17e-01 +6.99e-01 +6.92e-01 +6.87e-01 +6.82e-01 +7.32e-01 +7.47e-01 +9.42e-01 +5.70e-01 +2.11e-01 +7.38e-01 +7.50e-01 +7.58e-01 +7.14e-01 +7.38e-01 +7.21e-01 +6.81e-01 +6.69e-01 +6.84e-01 +6.84e-01 +7.32e-01 +7.33e-01 +9.18e-01 +3.59e-01 +1.45e-01 +7.38e-01 +5.93e-01 +7.38e-01 +7.39e-01 +7.46e-01 +7.70e-01 +7.25e-01 +6.07e-01 +6.46e-01 +6.49e-01 +6.56e-01 +6.97e-01 +9.65e-01 +3.98e-01 +1.02e-01 +7.63e-01 +7.31e-01 +7.32e-01 +7.05e-01 +7.34e-01 +7.26e-01 +7.09e-01 +6.85e-01 +6.43e-01 +6.34e-01 +6.51e-01 +6.67e-01 +9.76e-01 +4.41e-01 +1.09e-01 +7.80e-01 +7.12e-01 +7.26e-01 +7.17e-01 +7.58e-01 +7.39e-01 +7.19e-01 +7.02e-01 +7.03e-01 +6.55e-01 +5.91e-01 +6.53e-01 +9.17e-01 +4.78e-01 +1.37e-01 +8.17e-01 +7.34e-01 +7.39e-01 +7.85e-01 +7.75e-01 +7.52e-01 +7.79e-01 +7.11e-01 +7.19e-01 +6.96e-01 +6.43e-01 +6.00e-01 +7.61e-01 +2.70e-01 +8.10e-02 +1.16e+00 +1.04e+00 +1.10e+00 +1.11e+00 +1.11e+00 +1.08e+00 +9.96e-01 +1.02e+00 +9.59e-01 +1.01e+00 +9.84e-01 +7.98e-01 +6.84e-01 +2.00e-01 +5.36e-02 +1.12e+00 +8.97e-01 +8.53e-01 +8.36e-01 +8.45e-01 +7.72e-01 +5.11e-01 +5.45e-01 +4.94e-01 +5.38e-01 +5.92e-01 +4.14e-01 +2.08e-01 +7.44e-02 +2.60e-02 +3.41e-01 +2.55e-01 +2.14e-01 +2.42e-01 +2.46e-01 +2.60e-01 +1.78e-01 +1.61e-01 +1.44e-01 +1.52e-01 +1.91e-01 +1.39e-01 +6.05e-02 +2.90e-02 +6.48e-03 +7.87e-01 +8.11e-01 +8.37e-01 +8.09e-01 +8.06e-01 +7.52e-01 +7.59e-01 +7.86e-01 +7.65e-01 +7.53e-01 +7.16e-01 +8.07e-01 +1.02e+00 +7.17e-01 +2.61e-01 +7.93e-01 +8.13e-01 +7.88e-01 +7.89e-01 +7.88e-01 +8.08e-01 +8.00e-01 +7.90e-01 +7.89e-01 +7.16e-01 +7.47e-01 +7.98e-01 +1.11e+00 +7.82e-01 +2.29e-01 +8.16e-01 +7.85e-01 +7.57e-01 +7.70e-01 +7.67e-01 +7.99e-01 +7.75e-01 +7.99e-01 +8.00e-01 +7.27e-01 +7.00e-01 +7.95e-01 +1.12e+00 +8.32e-01 +2.72e-01 +7.66e-01 +7.71e-01 +7.72e-01 +7.50e-01 +7.83e-01 +7.66e-01 +7.97e-01 +7.79e-01 +7.41e-01 +7.26e-01 +7.24e-01 +7.90e-01 +1.04e+00 +7.19e-01 +2.72e-01 +7.50e-01 +7.28e-01 +7.78e-01 +7.78e-01 +7.70e-01 +7.41e-01 +7.52e-01 +7.68e-01 +7.29e-01 +7.30e-01 +7.52e-01 +7.62e-01 +1.04e+00 +5.78e-01 +1.84e-01 +7.79e-01 +7.50e-01 +7.82e-01 +7.38e-01 +7.48e-01 +7.53e-01 +7.41e-01 +7.47e-01 +7.17e-01 +7.06e-01 +7.29e-01 +7.63e-01 +1.01e+00 +5.33e-01 +1.96e-01 +7.73e-01 +7.74e-01 +7.65e-01 +7.47e-01 +7.35e-01 +7.39e-01 +7.07e-01 +7.21e-01 +7.07e-01 +7.06e-01 +6.86e-01 +7.53e-01 +1.03e+00 +6.87e-01 +2.34e-01 +7.58e-01 +7.65e-01 +7.72e-01 +7.64e-01 +7.41e-01 +7.16e-01 +6.58e-01 +7.03e-01 +6.75e-01 +6.34e-01 +6.76e-01 +7.61e-01 +9.52e-01 +3.63e-01 +1.11e-01 +7.33e-01 +7.35e-01 +7.46e-01 +7.50e-01 +7.43e-01 +7.09e-01 +6.97e-01 +6.61e-01 +6.39e-01 +6.31e-01 +6.70e-01 +7.26e-01 +9.35e-01 +4.08e-01 +1.21e-01 +7.44e-01 +7.40e-01 +7.34e-01 +7.49e-01 +7.23e-01 +7.13e-01 +6.92e-01 +6.51e-01 +6.43e-01 +6.54e-01 +6.11e-01 +6.81e-01 +8.92e-01 +5.18e-01 +1.67e-01 +7.39e-01 +7.24e-01 +7.18e-01 +7.10e-01 +7.07e-01 +7.17e-01 +7.28e-01 +6.89e-01 +6.45e-01 +6.27e-01 +5.89e-01 +6.27e-01 +8.94e-01 +4.27e-01 +1.44e-01 +8.20e-01 +7.35e-01 +7.73e-01 +7.79e-01 +7.33e-01 +7.94e-01 +7.38e-01 +7.21e-01 +6.82e-01 +6.66e-01 +6.30e-01 +5.52e-01 +6.56e-01 +2.86e-01 +9.02e-02 +1.23e+00 +1.02e+00 +9.49e-01 +1.09e+00 +1.08e+00 +1.07e+00 +1.01e+00 +1.12e+00 +1.07e+00 +1.12e+00 +9.32e-01 +7.55e-01 +5.90e-01 +1.70e-01 +6.04e-02 +9.35e-01 +7.67e-01 +7.48e-01 +8.36e-01 +9.13e-01 +8.43e-01 +6.94e-01 +7.11e-01 +6.45e-01 +5.23e-01 +4.64e-01 +3.36e-01 +1.97e-01 +8.24e-02 +3.52e-02 +3.61e-01 +3.04e-01 +3.20e-01 +2.77e-01 +3.14e-01 +2.79e-01 +2.19e-01 +2.01e-01 +2.23e-01 +1.68e-01 +1.56e-01 +1.06e-01 +6.15e-02 +3.90e-02 +1.78e-02 +7.55e-01 +7.78e-01 +8.17e-01 +8.31e-01 +8.37e-01 +8.23e-01 +8.31e-01 +8.49e-01 +7.67e-01 +7.79e-01 +7.53e-01 +7.56e-01 +9.13e-01 +4.87e-01 +1.90e-01 +7.63e-01 +7.96e-01 +7.72e-01 +7.84e-01 +8.07e-01 +7.97e-01 +7.85e-01 +7.92e-01 +7.90e-01 +7.89e-01 +7.62e-01 +7.60e-01 +9.99e-01 +6.50e-01 +2.08e-01 +7.70e-01 +7.78e-01 +7.67e-01 +8.10e-01 +7.76e-01 +7.97e-01 +8.29e-01 +7.63e-01 +7.81e-01 +7.78e-01 +7.59e-01 +7.86e-01 +1.06e+00 +7.09e-01 +2.39e-01 +7.64e-01 +7.94e-01 +8.36e-01 +7.73e-01 +8.21e-01 +8.29e-01 +8.06e-01 +7.74e-01 +7.69e-01 +7.48e-01 +7.27e-01 +7.70e-01 +1.04e+00 +5.38e-01 +1.72e-01 +7.68e-01 +7.86e-01 +7.98e-01 +7.72e-01 +8.10e-01 +7.95e-01 +7.80e-01 +7.69e-01 +7.86e-01 +7.59e-01 +7.61e-01 +7.80e-01 +1.02e+00 +4.87e-01 +1.43e-01 +7.60e-01 +7.84e-01 +7.88e-01 +8.10e-01 +7.80e-01 +7.87e-01 +7.60e-01 +7.40e-01 +7.18e-01 +7.35e-01 +7.48e-01 +7.57e-01 +1.06e+00 +5.45e-01 +1.46e-01 +7.56e-01 +7.71e-01 +7.41e-01 +7.88e-01 +7.68e-01 +7.62e-01 +7.57e-01 +7.22e-01 +7.18e-01 +7.13e-01 +7.27e-01 +7.54e-01 +9.95e-01 +6.13e-01 +1.70e-01 +7.39e-01 +7.50e-01 +7.74e-01 +7.93e-01 +7.34e-01 +7.32e-01 +6.83e-01 +7.01e-01 +7.06e-01 +7.36e-01 +7.12e-01 +7.25e-01 +9.34e-01 +5.51e-01 +1.95e-01 +7.17e-01 +7.39e-01 +7.59e-01 +7.62e-01 +7.78e-01 +7.43e-01 +6.89e-01 +6.89e-01 +6.69e-01 +6.52e-01 +6.30e-01 +6.68e-01 +9.23e-01 +4.29e-01 +1.22e-01 +7.39e-01 +7.23e-01 +7.35e-01 +7.50e-01 +7.65e-01 +7.07e-01 +6.87e-01 +6.74e-01 +6.34e-01 +6.34e-01 +6.14e-01 +6.22e-01 +7.74e-01 +4.07e-01 +1.43e-01 +7.20e-01 +7.36e-01 +7.25e-01 +7.20e-01 +7.27e-01 +7.02e-01 +7.08e-01 +6.86e-01 +6.37e-01 +6.42e-01 +5.84e-01 +6.05e-01 +8.13e-01 +2.50e-01 +7.65e-02 +7.35e-01 +7.38e-01 +7.34e-01 +7.63e-01 +7.24e-01 +7.52e-01 +7.60e-01 +7.36e-01 +6.72e-01 +6.44e-01 +6.12e-01 +5.65e-01 +6.76e-01 +2.27e-01 +7.04e-02 +1.03e+00 +9.44e-01 +9.23e-01 +1.03e+00 +1.07e+00 +1.05e+00 +9.55e-01 +9.95e-01 +9.86e-01 +8.94e-01 +8.68e-01 +7.36e-01 +3.85e-01 +9.70e-02 +3.48e-02 +8.54e-01 +4.54e-01 +5.88e-01 +6.14e-01 +6.53e-01 +6.43e-01 +5.10e-01 +6.95e-01 +5.75e-01 +3.80e-01 +4.09e-01 +2.64e-01 +1.34e-01 +5.20e-02 +1.59e-02 +2.77e-01 +1.44e-01 +1.76e-01 +1.86e-01 +2.54e-01 +2.23e-01 +1.61e-01 +2.31e-01 +1.97e-01 +1.30e-01 +1.16e-01 +6.26e-02 +4.81e-02 +3.06e-02 +1.67e-02 +7.86e-01 +8.39e-01 +8.83e-01 +8.55e-01 +8.50e-01 +8.79e-01 +8.60e-01 +8.12e-01 +8.09e-01 +7.89e-01 +7.90e-01 +7.42e-01 +7.44e-01 +1.58e-01 +6.48e-02 +8.67e-01 +8.89e-01 +8.72e-01 +8.03e-01 +7.98e-01 +8.19e-01 +8.35e-01 +8.10e-01 +7.91e-01 +7.79e-01 +7.97e-01 +7.65e-01 +8.12e-01 +2.69e-01 +1.15e-01 +8.28e-01 +8.91e-01 +8.28e-01 +8.22e-01 +8.14e-01 +8.56e-01 +8.86e-01 +8.46e-01 +7.76e-01 +7.84e-01 +7.49e-01 +8.31e-01 +1.07e+00 +4.89e-01 +1.30e-01 +8.26e-01 +8.49e-01 +8.39e-01 +8.27e-01 +8.44e-01 +8.70e-01 +3.16e-01 +8.04e-01 +8.19e-01 +7.85e-01 +7.20e-01 +7.52e-01 +9.62e-01 +4.26e-01 +1.46e-01 +8.01e-01 +7.89e-01 +8.03e-01 +8.28e-01 +8.39e-01 +8.52e-01 +8.17e-01 +8.07e-01 +8.12e-01 +8.02e-01 +7.19e-01 +7.09e-01 +9.85e-01 +5.13e-01 +1.40e-01 +7.74e-01 +8.11e-01 +7.91e-01 +8.10e-01 +8.13e-01 +8.20e-01 +7.99e-01 +7.62e-01 +7.73e-01 +7.84e-01 +7.21e-01 +7.04e-01 +9.41e-01 +4.52e-01 +1.46e-01 +7.47e-01 +7.56e-01 +8.11e-01 +8.22e-01 +8.04e-01 +7.79e-01 +7.83e-01 +7.46e-01 +7.28e-01 +7.40e-01 +7.25e-01 +7.07e-01 +8.17e-01 +4.63e-01 +1.62e-01 +7.58e-01 +7.56e-01 +7.60e-01 +7.96e-01 +7.87e-01 +7.69e-01 +7.75e-01 +7.58e-01 +7.39e-01 +7.10e-01 +7.14e-01 +7.27e-01 +9.10e-01 +3.49e-01 +1.16e-01 +7.76e-01 +7.69e-01 +7.90e-01 +7.93e-01 +7.91e-01 +8.11e-01 +7.48e-01 +7.33e-01 +7.10e-01 +7.10e-01 +6.65e-01 +6.83e-01 +8.72e-01 +3.27e-01 +1.09e-01 +7.58e-01 +7.27e-01 +8.00e-01 +7.89e-01 +7.90e-01 +7.69e-01 +7.48e-01 +6.87e-01 +6.49e-01 +6.32e-01 +6.50e-01 +6.16e-01 +8.53e-01 +3.55e-01 +1.26e-01 +7.12e-01 +7.03e-01 +7.35e-01 +7.34e-01 +7.90e-01 +7.42e-01 +6.78e-01 +6.75e-01 +6.64e-01 +6.43e-01 +5.93e-01 +5.68e-01 +7.24e-01 +2.12e-01 +6.37e-02 +7.12e-01 +3.10e-01 +7.21e-01 +7.24e-01 +7.11e-01 +6.84e-01 +7.09e-01 +7.37e-01 +6.65e-01 +6.48e-01 +5.74e-01 +5.37e-01 +6.18e-01 +1.92e-01 +5.93e-02 +9.23e-01 +8.49e-01 +8.63e-01 +9.07e-01 +8.87e-01 +8.82e-01 +8.74e-01 +9.65e-01 +9.71e-01 +7.90e-01 +7.23e-01 +6.21e-01 +3.58e-01 +1.02e-01 +3.58e-02 +6.34e-01 +3.24e-01 +4.14e-01 +5.18e-01 +3.93e-01 +3.93e-01 +3.52e-01 +3.37e-01 +5.09e-01 +3.80e-01 +2.69e-01 +2.08e-01 +1.27e-01 +3.00e-02 +9.84e-03 +2.53e-01 +1.24e-01 +1.72e-01 +1.82e-01 +1.42e-01 +1.35e-01 +1.17e-01 +1.22e-01 +1.56e-01 +1.28e-01 +7.82e-02 +7.58e-02 +4.95e-02 +1.63e-02 +7.34e-03 +1.15e+00 +1.21e+00 +1.27e+00 +1.27e+00 +1.26e+00 +1.19e+00 +1.15e+00 +1.14e+00 +1.15e+00 +1.04e+00 +1.01e+00 +1.07e+00 +8.04e-01 +1.47e-01 +3.71e-02 +1.23e+00 +1.11e+00 +1.30e+00 +1.15e+00 +1.15e+00 +1.20e+00 +1.17e+00 +1.09e+00 +1.02e+00 +1.06e+00 +9.84e-01 +9.61e-01 +9.02e-01 +2.44e-01 +7.45e-02 +1.19e+00 +1.09e+00 +1.26e+00 +1.15e+00 +1.13e+00 +1.22e+00 +1.07e+00 +1.19e+00 +1.10e+00 +1.09e+00 +1.01e+00 +9.62e-01 +1.10e+00 +4.73e-01 +1.47e-01 +1.18e+00 +1.14e+00 +1.17e+00 +1.20e+00 +1.17e+00 +1.28e+00 +1.19e+00 +1.18e+00 +1.12e+00 +1.04e+00 +9.10e-01 +8.02e-01 +9.06e-01 +2.84e-01 +8.05e-02 +1.14e+00 +1.09e+00 +1.13e+00 +1.18e+00 +1.11e+00 +1.18e+00 +1.35e+00 +1.16e+00 +1.15e+00 +1.16e+00 +1.00e+00 +7.35e-01 +7.88e-01 +3.90e-01 +1.47e-01 +1.14e+00 +1.03e+00 +1.10e+00 +1.12e+00 +1.06e+00 +1.15e+00 +1.16e+00 +1.07e+00 +1.05e+00 +1.12e+00 +9.87e-01 +8.55e-01 +8.93e-01 +3.76e-01 +1.15e-01 +1.02e+00 +1.04e+00 +1.14e+00 +1.09e+00 +1.05e+00 +1.11e+00 +1.08e+00 +1.04e+00 +8.85e-01 +1.00e+00 +9.45e-01 +7.84e-01 +8.14e-01 +2.74e-01 +8.75e-02 +1.04e+00 +1.06e+00 +1.04e+00 +1.12e+00 +1.18e+00 +1.13e+00 +1.06e+00 +1.05e+00 +9.34e-01 +9.47e-01 +9.38e-01 +8.93e-01 +6.67e-01 +1.52e-01 +5.28e-02 +1.04e+00 +1.07e+00 +1.06e+00 +1.13e+00 +1.13e+00 +1.03e+00 +1.15e+00 +1.11e+00 +9.64e-01 +1.01e+00 +9.05e-01 +8.67e-01 +8.37e-01 +2.04e-01 +6.93e-02 +1.00e+00 +1.10e+00 +1.14e+00 +1.07e+00 +1.14e+00 +1.03e+00 +1.08e+00 +9.61e-01 +8.93e-01 +8.70e-01 +8.58e-01 +8.56e-01 +8.00e-01 +2.31e-01 +7.13e-02 +9.12e-01 +9.60e-01 +1.08e+00 +1.07e+00 +1.06e+00 +1.13e+00 +9.68e-01 +8.42e-01 +8.21e-01 +7.51e-01 +7.76e-01 +7.41e-01 +5.22e-01 +2.01e-01 +7.22e-02 +7.80e-01 +8.00e-01 +9.64e-01 +9.33e-01 +9.70e-01 +1.00e+00 +1.03e+00 +9.27e-01 +8.81e-01 +7.47e-01 +6.96e-01 +6.90e-01 +5.22e-01 +1.42e-01 +5.84e-02 +6.66e-01 +7.30e-01 +8.24e-01 +7.89e-01 +8.68e-01 +8.50e-01 +6.87e-01 +9.72e-01 +9.67e-01 +8.19e-01 +6.02e-01 +5.08e-01 +3.32e-01 +9.24e-02 +4.15e-02 +3.42e-01 +2.04e-01 +2.56e-01 +3.06e-01 +2.44e-01 +2.32e-01 +1.97e-01 +2.37e-01 +4.25e-01 +2.80e-01 +1.20e-01 +1.22e-01 +6.52e-02 +2.34e-02 +1.33e-02 +1.30e-01 +6.51e-02 +9.03e-02 +9.81e-02 +7.43e-02 +7.44e-02 +7.50e-02 +7.08e-02 +1.36e-01 +1.08e-01 +5.23e-02 +3.86e-02 +2.05e-02 +1.18e-02 +4.82e-03 +1.10e+00 +1.08e+00 +1.36e+00 +9.59e-01 +1.50e+00 +1.22e+00 +1.06e+00 +1.09e+00 +9.39e-01 +8.29e-01 +5.63e-01 +6.05e-01 +5.41e-01 +1.46e-01 +4.18e-02 +1.38e+00 +9.47e-01 +1.64e+00 +1.82e+00 +1.45e+00 +1.17e+00 +9.32e-01 +9.52e-01 +7.63e-01 +6.75e-01 +6.06e-01 +5.05e-01 +4.40e-01 +1.95e-01 +6.83e-02 +1.18e+00 +1.14e+00 +1.46e+00 +1.11e+00 +8.51e-01 +1.15e+00 +9.29e-01 +9.75e-01 +8.23e-01 +7.12e-01 +8.66e-01 +6.90e-01 +3.69e-01 +2.08e-01 +1.46e-01 +1.36e+00 +1.17e+00 +1.39e+00 +1.53e+00 +1.15e+00 +1.26e+00 +1.34e+00 +1.20e+00 +1.06e+00 +8.13e-01 +6.01e-01 +4.19e-01 +2.92e-01 +1.43e-01 +6.62e-02 +1.31e+00 +9.58e-01 +1.27e+00 +1.56e+00 +1.32e+00 +1.38e+00 +1.36e+00 +1.10e+00 +9.06e-01 +8.42e-01 +7.44e-01 +3.53e-01 +2.51e-01 +1.27e-01 +6.42e-02 +1.05e+00 +8.06e-01 +1.10e+00 +1.14e+00 +7.77e-01 +8.88e-01 +1.08e+00 +7.49e-01 +6.61e-01 +8.78e-01 +7.85e-01 +5.07e-01 +2.81e-01 +1.16e-01 +7.65e-02 +7.49e-01 +9.07e-01 +1.01e+00 +1.02e+00 +8.96e-01 +9.71e-01 +9.06e-01 +7.59e-01 +6.31e-01 +5.64e-01 +4.58e-01 +3.36e-01 +1.97e-01 +1.17e-01 +7.24e-02 +5.31e-01 +7.17e-01 +8.03e-01 +7.87e-01 +9.01e-01 +9.00e-01 +6.18e-01 +6.08e-01 +4.09e-01 +3.65e-01 +4.67e-01 +3.69e-01 +1.95e-01 +1.03e-01 +4.09e-02 +5.66e-01 +6.84e-01 +8.03e-01 +1.10e+00 +8.56e-01 +7.53e-01 +5.80e-01 +7.25e-01 +5.65e-01 +5.23e-01 +4.71e-01 +3.57e-01 +2.67e-01 +9.56e-02 +3.66e-02 +4.28e-01 +7.89e-01 +1.04e+00 +7.98e-01 +6.86e-01 +4.17e-01 +5.80e-01 +6.93e-01 +5.76e-01 +4.61e-01 +4.35e-01 +3.12e-01 +2.15e-01 +1.24e-01 +3.78e-02 +4.61e-01 +5.64e-01 +7.98e-01 +5.21e-01 +7.00e-01 +6.75e-01 +5.39e-01 +5.12e-01 +4.28e-01 +2.95e-01 +3.73e-01 +3.14e-01 +1.91e-01 +1.02e-01 +4.60e-02 +3.92e-01 +3.42e-01 +5.06e-01 +4.71e-01 +4.73e-01 +6.89e-01 +5.67e-01 +3.20e-01 +3.01e-01 +2.61e-01 +2.32e-01 +2.34e-01 +1.79e-01 +4.62e-02 +1.73e-02 +1.91e-01 +2.50e-01 +3.10e-01 +2.40e-01 +2.89e-01 +4.18e-01 +3.43e-01 +3.33e-01 +2.60e-01 +2.88e-01 +2.00e-01 +1.73e-01 +1.35e-01 +4.58e-02 +1.87e-02 +8.30e-02 +9.70e-02 +1.13e-01 +8.94e-02 +1.22e-01 +1.54e-01 +1.00e-01 +1.45e-01 +1.68e-01 +1.41e-01 +7.91e-02 +5.48e-02 +3.66e-02 +1.92e-02 +9.23e-03 +6.77e-02 +3.66e-02 +4.21e-02 +4.05e-02 +4.50e-02 +5.61e-02 +2.78e-02 +4.65e-02 +7.38e-02 +6.93e-02 +3.15e-02 +1.71e-02 +1.50e-02 +7.02e-03 +2.63e-03 +3.78e-01 +3.76e-01 +5.09e-01 +3.31e-01 +5.46e-01 +4.00e-01 +3.73e-01 +3.85e-01 +3.61e-01 +2.87e-01 +1.88e-01 +1.85e-01 +1.80e-01 +1.07e-01 +3.77e-02 +5.65e-01 +2.57e-01 +5.55e-01 +7.39e-01 +6.13e-01 +4.33e-01 +3.29e-01 +2.97e-01 +2.89e-01 +2.50e-01 +1.89e-01 +1.46e-01 +1.32e-01 +8.81e-02 +4.17e-02 +4.36e-01 +3.41e-01 +5.13e-01 +4.96e-01 +2.34e-01 +4.00e-01 +3.27e-01 +2.87e-01 +2.94e-01 +2.08e-01 +3.02e-01 +2.63e-01 +1.41e-01 +5.88e-02 +6.26e-02 +4.47e-01 +3.47e-01 +5.27e-01 +6.11e-01 +3.91e-01 +4.22e-01 +4.77e-01 +4.54e-01 +4.31e-01 +3.19e-01 +2.13e-01 +1.47e-01 +9.76e-02 +5.82e-02 +2.92e-02 +5.35e-01 +3.74e-01 +4.80e-01 +5.78e-01 +5.02e-01 +4.78e-01 +5.06e-01 +3.61e-01 +3.58e-01 +2.46e-01 +2.55e-01 +1.07e-01 +6.95e-02 +4.27e-02 +2.71e-02 +3.15e-01 +2.73e-01 +3.47e-01 +3.78e-01 +2.88e-01 +2.50e-01 +3.39e-01 +2.40e-01 +2.15e-01 +2.65e-01 +2.87e-01 +2.16e-01 +1.00e-01 +4.10e-02 +2.09e-02 +3.10e-01 +2.99e-01 +3.67e-01 +3.96e-01 +2.30e-01 +2.86e-01 +3.00e-01 +2.63e-01 +2.50e-01 +1.99e-01 +1.56e-01 +1.31e-01 +7.20e-02 +3.22e-02 +3.48e-02 +1.47e-01 +2.69e-01 +2.47e-01 +2.40e-01 +2.82e-01 +3.03e-01 +1.85e-01 +1.94e-01 +1.29e-01 +1.02e-01 +1.25e-01 +1.15e-01 +5.95e-02 +5.42e-02 +4.17e-02 +1.81e-01 +2.00e-01 +2.60e-01 +3.31e-01 +3.21e-01 +2.95e-01 +1.85e-01 +2.44e-01 +2.00e-01 +1.66e-01 +1.43e-01 +1.23e-01 +1.02e-01 +4.36e-02 +1.72e-02 +1.08e-01 +2.40e-01 +3.14e-01 +3.12e-01 +2.21e-01 +1.20e-01 +1.86e-01 +2.21e-01 +2.13e-01 +1.40e-01 +1.31e-01 +1.05e-01 +5.58e-02 +6.38e-02 +3.45e-02 +1.48e-01 +2.44e-01 +2.92e-01 +1.42e-01 +2.29e-01 +1.89e-01 +1.21e-01 +1.73e-01 +1.54e-01 +1.09e-01 +1.16e-01 +8.49e-02 +6.30e-02 +4.98e-02 +4.02e-02 +1.01e-01 +1.10e-01 +1.55e-01 +1.40e-01 +1.46e-01 +2.37e-01 +1.93e-01 +1.34e-01 +9.47e-02 +8.48e-02 +7.21e-02 +7.09e-02 +7.53e-02 +2.10e-02 +7.74e-03 +8.60e-02 +8.49e-02 +1.09e-01 +1.25e-01 +9.19e-02 +1.59e-01 +1.27e-01 +1.01e-01 +6.87e-02 +9.44e-02 +5.12e-02 +5.73e-02 +5.81e-02 +1.92e-02 +6.10e-03 +2.72e-02 +4.77e-02 +4.74e-02 +3.34e-02 +5.41e-02 +7.93e-02 +6.70e-02 +7.67e-02 +9.31e-02 +5.17e-02 +5.34e-02 +3.25e-02 +1.99e-02 +1.19e-02 +1.14e-02 +2.23e-02 +2.53e-02 +2.61e-02 +2.28e-02 +2.86e-02 +5.00e-02 +3.04e-02 +2.98e-02 +5.98e-02 +4.02e-02 +2.97e-02 +1.80e-02 +6.73e-03 +4.41e-03 +7.35e-03 \ No newline at end of file diff --git a/tests/regression_tests/weightwindows_fw_cadis_mesh/test.py b/tests/regression_tests/weightwindows_fw_cadis_mesh/test.py new file mode 100644 index 00000000000..680e9dc6df7 --- /dev/null +++ b/tests/regression_tests/weightwindows_fw_cadis_mesh/test.py @@ -0,0 +1,49 @@ +import os + +import openmc +from openmc.utility_funcs import change_directory +from openmc.examples import random_ray_three_region_cube +import pytest + +from tests.testing_harness import WeightWindowPyAPITestHarness + + +class MGXSTestHarness(WeightWindowPyAPITestHarness): + def _cleanup(self): + super()._cleanup() + f = 'mgxs.h5' + if os.path.exists(f): + os.remove(f) + + +@pytest.mark.parametrize("shape", ["flat", "linear"]) +def test_weight_windows_fw_cadis_mesh(shape): + with change_directory(shape): + openmc.reset_auto_ids() + + model = random_ray_three_region_cube() + + # The base model has a resolution of 12, so we overlay + # something else for FW-CADIS + n = 15 + width = 30.0 + ww_mesh = openmc.RegularMesh() + ww_mesh.dimension = (n, n, n) + ww_mesh.lower_left = (0.0, 0.0, 0.0) + ww_mesh.upper_right = (width, width, width) + + wwg = openmc.WeightWindowGenerator( + method="fw_cadis", mesh=ww_mesh, max_realizations=model.settings.batches) + model.settings.weight_window_generators = wwg + + root = model.geometry.root_universe + model.settings.random_ray['source_region_meshes'] = [(ww_mesh, [root])] + + model.settings.particles = 750 + model.settings.batches = 30 + model.settings.inactive = 20 + + model.settings.random_ray['source_shape'] = shape + + harness = MGXSTestHarness('statepoint.30.h5', model) + harness.main() From 906548db20d4ce3c322e7317992c73b51daeb11d Mon Sep 17 00:00:00 2001 From: Paul Romano Date: Fri, 7 Mar 2025 14:49:36 -0600 Subject: [PATCH 184/184] Release notes for 0.15.1 (#3340) Co-authored-by: Patrick Shriwise --- CONTRIBUTING.md | 2 +- docs/source/devguide/docker.rst | 9 +- docs/source/devguide/styleguide.rst | 9 +- docs/source/devguide/user-input.rst | 4 +- docs/source/devguide/workflow.rst | 2 +- docs/source/methods/cross_sections.rst | 4 +- docs/source/methods/depletion.rst | 2 +- docs/source/methods/geometry.rst | 2 +- docs/source/methods/neutron_physics.rst | 12 +- docs/source/methods/parallelization.rst | 6 +- docs/source/methods/photon_physics.rst | 2 +- docs/source/methods/tallies.rst | 2 +- docs/source/publications.rst | 10 +- docs/source/pythonapi/deplete.rst | 2 +- docs/source/quickinstall.rst | 49 ++--- docs/source/releasenotes/0.15.1.rst | 224 +++++++++++++++++++++++ docs/source/releasenotes/index.rst | 1 + docs/source/usersguide/basics.rst | 2 +- docs/source/usersguide/beginners.rst | 12 +- docs/source/usersguide/data.rst | 18 +- docs/source/usersguide/decay_sources.rst | 16 +- docs/source/usersguide/install.rst | 54 ++---- docs/source/usersguide/parallel.rst | 2 +- include/openmc/random_dist.h | 4 +- openmc/data/ace.py | 6 +- openmc/data/endf.py | 8 +- openmc/data/fission_energy.py | 2 +- openmc/deplete/integrators.py | 4 +- openmc/examples.py | 5 +- openmc/mgxs/__init__.py | 6 +- openmc/tallies.py | 2 +- src/math_functions.cpp | 2 +- tools/dev/generate_release_notes.py | 17 ++ 33 files changed, 352 insertions(+), 150 deletions(-) create mode 100644 docs/source/releasenotes/0.15.1.rst create mode 100644 tools/dev/generate_release_notes.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 378ca346a85..184c522d438 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,7 @@ openmc@anl.gov. ## Resources - [GitHub Repository](https://github.com/openmc-dev/openmc) -- [Documentation](http://docs.openmc.org/en/latest) +- [Documentation](https://docs.openmc.org/en/latest) - [Discussion Forum](https://openmc.discourse.group) - [Slack Community](https://openmc.slack.com/signup) (If you don't see your domain listed, contact openmc@anl.gov) diff --git a/docs/source/devguide/docker.rst b/docs/source/devguide/docker.rst index 0b21911686b..50ff29bd227 100644 --- a/docs/source/devguide/docker.rst +++ b/docs/source/devguide/docker.rst @@ -45,12 +45,11 @@ Now you can run the following to create a `Docker container`_ called This command will open an interactive shell running from within the Docker container where you have access to use OpenMC. -.. note:: The ``docker run`` command supports many - `options `_ +.. note:: The ``docker run`` command supports many options_ for spawning containers -- including `mounting volumes`_ from the host filesystem -- which many users will find useful. -.. _Docker image: https://docs.docker.com/engine/reference/commandline/images/ +.. _Docker image: https://docs.docker.com/get-started/docker-concepts/the-basics/what-is-an-image/ .. _Docker container: https://www.docker.com/resources/what-container -.. _options: https://docs.docker.com/engine/reference/commandline/run/ -.. _mounting volumes: https://docs.docker.com/storage/volumes/ +.. _options: https://docs.docker.com/reference/cli/docker/container/run/ +.. _mounting volumes: https://docs.docker.com/engine/storage/volumes/ diff --git a/docs/source/devguide/styleguide.rst b/docs/source/devguide/styleguide.rst index b8ec2d40f84..2c882b0341f 100644 --- a/docs/source/devguide/styleguide.rst +++ b/docs/source/devguide/styleguide.rst @@ -47,7 +47,8 @@ is more difficult to comment out a large section of code that uses C-style comments.) Do not use C-style casting. Always use the C++-style casts ``static_cast``, -``const_cast``, or ``reinterpret_cast``. (See `ES.49 `_) +``const_cast``, or ``reinterpret_cast``. (See `ES.49 +`_) Source Files ------------ @@ -156,11 +157,11 @@ Prefer pathlib_ when working with filesystem paths over functions in the os_ module or other standard-library modules. Functions that accept arguments that represent a filesystem path should work with both strings and Path_ objects. -.. _C++ Core Guidelines: http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines -.. _PEP8: https://www.python.org/dev/peps/pep-0008/ +.. _C++ Core Guidelines: https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines +.. _PEP8: https://peps.python.org/pep-0008/ .. _numpydoc: https://numpydoc.readthedocs.io/en/latest/format.html .. _numpy: https://numpy.org/ -.. _scipy: https://www.scipy.org/ +.. _scipy: https://scipy.org/ .. _matplotlib: https://matplotlib.org/ .. _pandas: https://pandas.pydata.org/ .. _h5py: https://www.h5py.org/ diff --git a/docs/source/devguide/user-input.rst b/docs/source/devguide/user-input.rst index a26f98b1979..bbae3b7158c 100644 --- a/docs/source/devguide/user-input.rst +++ b/docs/source/devguide/user-input.rst @@ -55,6 +55,6 @@ developer or send a message to the `developers mailing list`_. .. _property attribute: https://docs.python.org/3.6/library/functions.html#property -.. _XML Schema Part 2: http://www.w3.org/TR/xmlschema-2/ -.. _boolean: http://www.w3.org/TR/xmlschema-2/#boolean +.. _XML Schema Part 2: https://www.w3.org/TR/xmlschema-2/ +.. _boolean: https://www.w3.org/TR/xmlschema-2/#boolean .. _developers mailing list: https://groups.google.com/forum/?fromgroups=#!forum/openmc-dev diff --git a/docs/source/devguide/workflow.rst b/docs/source/devguide/workflow.rst index d9d36c161c2..d600c236f13 100644 --- a/docs/source/devguide/workflow.rst +++ b/docs/source/devguide/workflow.rst @@ -129,7 +129,7 @@ can interfere with virtual environments. .. _git: https://git-scm.com/ .. _GitHub: https://github.com/ .. _git flow: https://nvie.com/git-model -.. _valgrind: https://www.valgrind.org/ +.. _valgrind: https://valgrind.org/ .. _style guide: https://docs.openmc.org/en/latest/devguide/styleguide.html .. _pull request: https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests .. _openmc-dev/openmc: https://github.com/openmc-dev/openmc diff --git a/docs/source/methods/cross_sections.rst b/docs/source/methods/cross_sections.rst index ad64b0e38ed..a66abb3ed40 100644 --- a/docs/source/methods/cross_sections.rst +++ b/docs/source/methods/cross_sections.rst @@ -295,8 +295,8 @@ or even isotropic scattering. .. _Josey: https://doi.org/10.1016/j.jcp.2015.08.013 .. _WMP Library: https://github.com/mit-crpg/WMP_Library .. _MCNP: https://mcnp.lanl.gov -.. _Serpent: https://serpent.vtt.fi/serpent/ -.. _NJOY: https://www.njoy21.io/NJOY21/ +.. _Serpent: https://serpent.vtt.fi +.. _NJOY: https://www.njoy21.io/ .. _ENDF/B data: https://www.nndc.bnl.gov/endf-b8.0/ .. _Leppanen: https://doi.org/10.1016/j.anucene.2009.03.019 .. _algorithms: http://ab-initio.mit.edu/faddeeva/ diff --git a/docs/source/methods/depletion.rst b/docs/source/methods/depletion.rst index 87a9976dd0a..edcf2c3f534 100644 --- a/docs/source/methods/depletion.rst +++ b/docs/source/methods/depletion.rst @@ -114,7 +114,7 @@ The predictor method only requires one evaluation and its error converges as twice as expensive as the predictor method, but achieves an error of :math:`\mathcal{O}(h^2)`. An exhaustive description of time integration methods and their merits can be found in the `thesis of Colin Josey -`_. +`_. OpenMC does not rely on a single time integration method but rather has several classes that implement different algorithms. For example, the diff --git a/docs/source/methods/geometry.rst b/docs/source/methods/geometry.rst index fa8bb3cf75e..05cda4b6423 100644 --- a/docs/source/methods/geometry.rst +++ b/docs/source/methods/geometry.rst @@ -1066,5 +1066,5 @@ surface is known as in :ref:`reflection`. .. _constructive solid geometry: https://en.wikipedia.org/wiki/Constructive_solid_geometry .. _surfaces: https://en.wikipedia.org/wiki/Surface .. _MCNP: https://mcnp.lanl.gov -.. _Serpent: https://serpent.vtt.fi/serpent/ +.. _Serpent: https://serpent.vtt.fi .. _Monte Carlo Performance benchmark: https://github.com/mit-crpg/benchmarks/tree/master/mc-performance/openmc diff --git a/docs/source/methods/neutron_physics.rst b/docs/source/methods/neutron_physics.rst index 70ace4a3532..fe8b8ad8500 100644 --- a/docs/source/methods/neutron_physics.rst +++ b/docs/source/methods/neutron_physics.rst @@ -1743,19 +1743,19 @@ types. .. _Watt fission spectrum: https://doi.org/10.1103/PhysRev.87.1037 -.. _Foderaro: http://hdl.handle.net/1721.1/1716 +.. _Foderaro: https://dspace.mit.edu/handle/1721.1/1716 .. _OECD: https://www.oecd-nea.org/tools/abstract/detail/NEA-1792 .. _NJOY: https://www.njoy21.io/NJOY2016/ -.. _PREPRO: https://www-nds.iaea.org/ndspub/endf/prepro/ +.. _PREPRO: https://www-nds.iaea.org/public/endf/prepro/ .. _ENDF-6 Format: https://www.oecd-nea.org/dbdata/data/manual-endf/endf102.pdf -.. _Monte Carlo Sampler: https://permalink.lanl.gov/object/tr?what=info:lanl-repo/lareport/LA-09721-MS +.. _Monte Carlo Sampler: https://mcnp.lanl.gov/pdf_files/TechReport_1983_LANL_LA-9721-MS_EverettCashwell.pdf -.. _LA-UR-14-27694: https://permalink.lanl.gov/object/tr?what=info:lanl-repo/lareport/LA-UR-14-27694 +.. _LA-UR-14-27694: https://www.osti.gov/biblio/1159204 .. _MC21: https://www.osti.gov/biblio/903083 @@ -1763,6 +1763,4 @@ types. .. _Sutton and Brown: https://www.osti.gov/biblio/307911 -.. _lectures: https://laws.lanl.gov/vhosts/mcnp.lanl.gov/pdf_files/la-ur-05-4983.pdf - -.. _MCNP Manual: https://laws.lanl.gov/vhosts/mcnp.lanl.gov/pdf_files/la-ur-03-1987.pdf +.. _lectures: https://mcnp.lanl.gov/pdf_files/TechReport_2005_LANL_LA-UR-05-4983_Brown.pdf diff --git a/docs/source/methods/parallelization.rst b/docs/source/methods/parallelization.rst index d9fc15c9fab..87ac4859037 100644 --- a/docs/source/methods/parallelization.rst +++ b/docs/source/methods/parallelization.rst @@ -609,17 +609,17 @@ is actually independent of the number of nodes: .. _first paper: https://doi.org/10.2307/2280232 -.. _work of Forrest Brown: http://hdl.handle.net/2027.42/24996 +.. _work of Forrest Brown: https://deepblue.lib.umich.edu/handle/2027.42/24996 .. _Brissenden and Garlick: https://doi.org/10.1016/0306-4549(86)90095-2 -.. _MPICH: http://www.mpich.org +.. _MPICH: https://www.mpich.org .. _binomial tree: https://www.mcs.anl.gov/~thakur/papers/ijhpca-coll.pdf .. _Geary: https://doi.org/10.2307/2342070 -.. _Barnett: http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.51.7772 +.. _Barnett: https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.51.7772 .. _single-instruction multiple-data: https://en.wikipedia.org/wiki/SIMD diff --git a/docs/source/methods/photon_physics.rst b/docs/source/methods/photon_physics.rst index 42c68431a18..22d2c7f26a0 100644 --- a/docs/source/methods/photon_physics.rst +++ b/docs/source/methods/photon_physics.rst @@ -1059,7 +1059,7 @@ emitted photon. .. _anomalous scattering: http://pd.chem.ucl.ac.uk/pdnn/diff1/anomscat.htm -.. _Kahn's rejection method: https://mcnp.lanl.gov/pdf_files/TechReport_1956_RC_AECU-3259RM-1237-AEC_Kahn.pdf +.. _Kahn's rejection method: https://doi.org/10.2172/4353680 .. _Klein-Nishina: https://en.wikipedia.org/wiki/Klein%E2%80%93Nishina_formula diff --git a/docs/source/methods/tallies.rst b/docs/source/methods/tallies.rst index 4b56d39559a..57e05d84f85 100644 --- a/docs/source/methods/tallies.rst +++ b/docs/source/methods/tallies.rst @@ -522,4 +522,4 @@ improve the estimate of the percentile. .. _unpublished rational approximation: https://stackedboxes.org/2017/05/01/acklams-normal-quantile-function/ -.. _MC21: http://www.osti.gov/bridge/servlets/purl/903083-HT5p1o/903083.pdf +.. _MC21: https://www.osti.gov/servlets/purl/903083 diff --git a/docs/source/publications.rst b/docs/source/publications.rst index 88d0a97b977..a2d60d5af16 100644 --- a/docs/source/publications.rst +++ b/docs/source/publications.rst @@ -138,8 +138,8 @@ Geometry and Visualization *Trans. Am. Nucl. Soc.*, **114**, 391-394 (2016). - Derek M. Lax, "`Memory efficient indexing algorithm for physical properties in - OpenMC `_," S. M. Thesis, Massachusetts - Institute of Technology (2015). + OpenMC `_," S. M. Thesis, + Massachusetts Institute of Technology (2015). - Derek Lax, William Boyd, Nicholas Horelik, Benoit Forget, and Kord Smith, "A memory efficient algorithm for classifying unique regions in constructive @@ -399,7 +399,8 @@ Doppler Broadening - Jonathan A. Walsh, Benoit Forget, Kord S. Smith, Brian C. Kiedrowski, and Forrest B. Brown, "`Direct, on-the-fly calculation of unresolved resonance region cross sections in Monte Carlo simulations - `_," *Proc. Joint Int. Conf. M&C+SNA+MC*, + `_," *Proc. Joint Int. Conf. + M&C+SNA+MC*, Nashville, Tennessee, Apr. 19--23 (2015). - Colin Josey, Benoit Forget, and Kord Smith, "`Windowed multipole sensitivity @@ -596,7 +597,8 @@ Depletion - Matthew S. Ellis, Colin Josey, Benoit Forget, and Kord Smith, "`Spatially Continuous Depletion Algorithm for Monte Carlo Simulations - `_," *Trans. Am. Nucl. Soc.*, **115**, + `_," *Trans. Am. Nucl. Soc.*, + **115**, 1221-1224 (2016). - Anas Gul, K. S. Chaudri, R. Khan, and M. Azeen, "`Development and verification diff --git a/docs/source/pythonapi/deplete.rst b/docs/source/pythonapi/deplete.rst index d7a779b1298..5d02fa6b980 100644 --- a/docs/source/pythonapi/deplete.rst +++ b/docs/source/pythonapi/deplete.rst @@ -26,7 +26,7 @@ provided to obtain reaction rates from cross-section data. Several classes are provided that implement different time-integration algorithms for depletion calculations, which are described in detail in Colin Josey's thesis, `Development and analysis of high order neutron transport-depletion coupling -algorithms `_. +algorithms `_. .. autosummary:: :toctree: generated diff --git a/docs/source/quickinstall.rst b/docs/source/quickinstall.rst index 323cd7fd48d..21526242f18 100644 --- a/docs/source/quickinstall.rst +++ b/docs/source/quickinstall.rst @@ -8,56 +8,35 @@ This quick install guide outlines the basic steps needed to install OpenMC on your computer. For more detailed instructions on configuring and installing OpenMC, see :ref:`usersguide_install` in the User's Manual. --------------------------------------------------- -Installing on Linux/Mac with Mamba and conda-forge --------------------------------------------------- +---------------------------------- +Installing on Linux/Mac with Conda +---------------------------------- -`Conda `_ is an open source package management +`Conda `_ is an open source package management system and environments management system for installing multiple versions of software packages and their dependencies and switching easily between them. -`Mamba `_ is a cross-platform package -manager and is compatible with `conda` packages. -OpenMC can be installed in a `conda` environment with `mamba`. -First, `conda` should be installed with one of the following installers: -`Miniconda `_, -`Anaconda `_, or `Miniforge `_. -Once you have `conda` installed on your system, OpenMC can be installed via the -`conda-forge` channel with `mamba`. +OpenMC can be installed in a `conda` environment. First, `conda` should be +`installed `_ +with either Anaconda Distribution or Miniconda. Once you have `conda` installed +on your system, OpenMC can be installed via the `conda-forge` channel. First, add the `conda-forge` channel with: .. code-block:: sh conda config --add channels conda-forge + conda config --set channel_priority strict -Then create and activate a new conda enviroment called `openmc-env` in -which to install OpenMC. +Then create and activate a new conda enviroment called `openmc-env` (or whatever +you wish) with OpenMC installed. .. code-block:: sh - conda create -n openmc-env + conda create --name openmc-env openmc conda activate openmc-env -Then install `mamba`, which will be used to install OpenMC. - -.. code-block:: sh - - conda install mamba - -To list the versions of OpenMC that are available on the `conda-forge` channel, -in your terminal window or an Anaconda Prompt run: - -.. code-block:: sh - - mamba search openmc - -OpenMC can then be installed with: - -.. code-block:: sh - - mamba install openmc - -You are now in a conda environment called `openmc-env` that has OpenMC installed. +You are now in a conda environment called `openmc-env` that has OpenMC +installed. ------------------------------------------- Installing on Linux/Mac/Windows with Docker diff --git a/docs/source/releasenotes/0.15.1.rst b/docs/source/releasenotes/0.15.1.rst new file mode 100644 index 00000000000..d879b50edd5 --- /dev/null +++ b/docs/source/releasenotes/0.15.1.rst @@ -0,0 +1,224 @@ +==================== +What's New in 0.15.1 +==================== + +.. currentmodule:: openmc + +------- +Summary +------- + +This release of OpenMC includes many bug fixes, performance improvements, and +several notable new features. The random ray solver continues to receive many +updates and improvements, which are listed below in more detail. A new +:class:`~openmc.SolidRayTracePlot` class has been added that enables attractive +3D visualization using Phong shading. Several composite surfaces have been +introduced (which help to further expand the capabilities of the +`openmc_mcnp_adapter `_). +The :meth:`openmc.Mesh.material_volumes` method has been completely +reimplemented with a new approach based on ray tracing that greatly improves +performance and can be executed in parallel. Tally results can be automatically +applied to input :class:`~openmc.Tally` objects with :meth:`openmc.Model.run`, +bypassing boilerplate code for collecting tally results from statepoint files. +Finally, a new :mod:`openmc.deplete.d1s` submodule has been added that enables +Direct 1-Step (D1S) calculations of shutdown dose rate for fusion applications. + +------------------------------------ +Compatibility Notes and Deprecations +------------------------------------ + +The ``openmc.ProjectionPlot`` class has been renamed to +:class:`openmc.WireframeRayTracePlot` to be in better alignment with the newly +introduced :class:`openmc.SolidRayTracePlot` class. + +NCrystal has been moved from a build-time dependency to a runtime dependency, +which means there is no longer a ``OPENMC_USE_NCRYSTAL`` CMake option. Instead, +OpenMC will look for an installed version of NCrystal using the +``ncrystal-config`` command. + +------------ +New Features +------------ + +- Numerous improvements have been made in the random ray solver: + - Calculation of Shannon entropy now works with random ray (`#3030 `_) + - Support for linear sources (`#3072 `_) + - Ability to slove for adjoint flux (`#3191 `_) + - Support randomized Quasi-Monte Carlo sampling (`#3268 `_) + - FW-CADIS weight window generation (`#3273 `_) + - Source region mesh subdivision(`#3333 `_) +- Several new composite surfaces have been added: + - :class:`openmc.model.OrthogonalBox` (`#3118 `_) + - :class:`openmc.model.ConicalFrustum` (`#3151 `_) + - :class:`openmc.model.Vessel` (`#3168 `_) +- The :meth:`openmc.Model.plot` method now supports plotting source sites + (`#2863 `_) +- The :func:`openmc.stats.delta_function` convenience function can be used for + specifying distributions with a single point (`#3090 + `_) +- Added a :meth:`openmc,Material.get_element_atom_densities` method (`#3103 + `_) +- Several third-party dependencies have been removed: + - Cython (`#3111 `_) + - gsl-lite (`#3225 `_) +- Added a new :class:`openmc.MuSurfaceFilter` class that filters tally events by + the cosine of angle of a surface crossing (`#2768 + `_) +- Introduced a :class:`openmc.ParticleList` class for manipulating a list of + source particles (`#3148 `_) +- Support dose coefficients from ICRP 74 in + :func:`openmc.data.dose_coefficients` (`#3020 + `_) +- Introduced a new :attr:`openmc.Settings.uniform_source_sampling` option + (`#3195 `_) +- Ability to differentiate materials in DAGMC universes (`#3056 + `_) +- Added methods to automatically apply results to existing Tally objects. + (`#2671 `_) +- Implemented a new :class:`openmc.SolidRayTracePlot` class that can produce a + 3D visualization based on Phong shading (`#2655 + `_) +- The :meth:`openmc.UnstructuredMesh.write_data_to_vtk` method now supports + writing a VTU file (`#3290 `_) +- Composite surfaces now have a + :attr:`~openmc.CompositeSurface.component_surfaces` attribute that provides + the underlying primitive surfaces (`#3167 + `_) +- A new :mod:`openmc.deplete.d1s` submodule has been added that enables Direct + 1-Step (D1S) calculations of shutdown dose rate for fusion applications + (`#3235 `_) + +--------------------------- +Bug Fixes and Small Changes +--------------------------- + +- run microxs with mpi (`#3028 `_) +- Rely on std::filesystem for file_utils (`#3042 `_) +- Random Ray Normalization Improvements (`#3051 `_) +- Alternative Random Ray Volume Estimators (`#3060 `_) +- Random Ray Testing Simplification (`#3061 `_) +- Fix hyperlinks in `random_ray.rst` (`#3064 `_) +- Add missing show_overlaps option to plots.xml input file documentation (`#3068 `_) +- Remove use of pkg_resources package (`#3069 `_) +- Add option for survival biasing source normalization (`#3070 `_) +- Enforce sequence type when setting ``Setting.track`` (`#3071 `_) +- Moving most of setup.py to pyproject.toml (`#3074 `_) +- Enforce non-negative percents for ``material.add_nuclide`` to prevent unintended ao/wo flipping (`#3075 `_) +- Include batch statistics discussion in methodology introduction (`#3076 `_) +- Add -DCMAKE_BUILD_TYPE=Release flag for MOAB in Dockerfile (`#3077 `_) +- Adjust decay data reader to better handle non-normalized branching ratios (`#3080 `_) +- Correct openmc.Geometry initializer to accept iterables of ``openmc.Cell`` (`#3081 `_) +- Replace all deprecated Python typing imports and syntax with updated forms (`#3085 `_) +- Fix ParticleFilter to work with set inputs (`#3092 `_) +- packages used for testing moved to tests section of pyprojects.toml (`#3094 `_) +- removed unused which function in CI scripts (`#3095 `_) +- Improve description of probabilities for ``openmc.stats.Tabular`` class (`#3099 `_) +- Ensure RegularMesh repr shows value for width of the mesh (`#3100 `_) +- Replacing endf c functions with package (`#3101 `_) +- Fix random ray solver to correctly simulate fixed source problems with fissionable materials (`#3106 `_) +- Improve error for nuclide temperature not found (`#3110 `_) +- Added error if cross sections path is a folder (`#3115 `_) +- Implement bounding_box operation for meshes (`#3119 `_) +- allowing varible offsets for ``polygon.offset`` (`#3120 `_) +- Write surface source files per batch (`#3124 `_) +- Mat ids reset (`#3125 `_) +- Tweaking title of feature issue template (`#3127 `_) +- Fix a typo in feature request template (`#3128 `_) +- Update quickinstall instructions for macOS (`#3130 `_) +- adapt the openmc-update-inputs script for surfaces (`#3131 `_) +- Theory documentation on PCG random number generator (`#3134 `_) +- Adding tmate action to CI for debugging (`#3138 `_) +- Add Versioning Support from `version.txt` (`#3140 `_) +- Correct failure due to progress bar values (`#3143 `_) +- Avoid writing subnormal nuclide densities to XML (`#3144 `_) +- Immediately resolve complement operators for regions (`#3145 `_) +- Improve Detection of libMesh Installation via `LIBMESH_ROOT` and CMake's PkgConfig (`#3149 `_) +- Fix for UWUW Macro Conflict (`#3150 `_) +- Consistency in treatment of paths for files specified within the Model class (`#3153 `_) +- Improve clipping of Mixture distributions (`#3154 `_) +- Fix check for trigger score name (`#3155 `_) +- Prepare point query data structures on meshes when applying Weight Windows (`#3157 `_) +- Add PointCloud spatial distribution (`#3161 `_) +- Update fmt submodule to version 11.0.2 (`#3162 `_) +- Move to support python 3.13 (`#3165 `_) +- avoid zero division if source rate of previous result is zero (`#3169 `_) +- Fix path handling for thermal ACE generation (`#3171 `_) +- Update `fmt` Formatters for Compatibility with Versions below 11 (`#3172 `_) +- added subfolders to txt search command in pyproject (`#3174 `_) +- added list to doc string arg for plot_xs (`#3178 `_) +- enable polymorphism for mix_materials (`#3180 `_) +- Fix plot_xs type hint (`#3184 `_) +- Enable adaptive mesh support on libMesh tallies (`#3185 `_) +- Reset values of lattice offset tables when allocated (`#3188 `_) +- Update surface_composite.py (`#3189 `_) +- add export_model_xml arguments to ``Model.plot_geometry`` and ``Model.calculate_volumes`` (`#3190 `_) +- Fixes in MicroXS.from_multigroup_flux (`#3192 `_) +- Fix documentation typo in ``boundary_type`` (`#3196 `_) +- Fix docstring for ``Model.plot`` (`#3198 `_) +- Apply weight windows at collisions in multigroup transport mode. (`#3199 `_) +- External sources alias sampler (`#3201 `_) +- Add test for flux bias with weight windows in multigroup mode (`#3202 `_) +- Fix bin index to DoF ID mapping bug in adaptive libMesh meshes (`#3206 `_) +- Ensure ``libMesh::ReplicatedMesh`` is used for LibMesh tallies (`#3208 `_) +- Set Model attributes only if needed (`#3209 `_) +- adding unstrucutred mesh file suffix to docstring (`#3211 `_) +- Write and read mesh name attribute (`#3221 `_) +- Adjust for secondary particle energy directly in heating scores (`#3227 `_) +- Correct normalization of thermal elastic in non standard ENDF-6 files (`#3234 `_) +- Adding '#define _USE_MATH_DEFINES' to make M_PI declared in Intel and MSVC compilers (`#3238 `_) +- updated link to log mapping technique (`#3241 `_) +- Fix for erroneously non-zero tally results of photon threshold reactions (`#3242 `_) +- Fix type comparison (`#3244 `_) +- Enable the LegendreFilter filter to be used in photon tallies for orders greater than P0. (`#3245 `_) +- Enable UWUW library when building with DAGMC in CI (`#3246 `_) +- Remove top-level import of ``openmc.lib`` (`#3250 `_) +- updated docker file to latest DAGMC (`#3251 `_) +- Write mesh type as a dataset always (`#3253 `_) +- Update to a consistent definition of the r2 parameter for cones (`#3254 `_) +- Add Patrick Shriwise to technical committee (`#3255 `_) +- Change `Zernike` documentation in polynomial.py (`#3258 `_) +- Bug fix for Polygon 'yz' basis (`#3259 `_) +- Add constant for invalid surface tokens. (`#3260 `_) +- Update plots.py for PathLike to string handling error (`#3261 `_) +- Fix bug in WeightWindowGenerator for empty energy bounds (`#3263 `_) +- Update recognized thermal scattering materials for ENDF/B-VIII.1 (`#3267 `_) +- simplify mechanism to detect if geometry entity is DAG (`#3269 `_) +- Fix bug in ``Surface.normalize`` (`#3270 `_) +- Tweak To Sphinx Install Documentation (`#3271 `_) +- add continue feature for depletion (`#3272 `_) +- Updates for building with NCrystal support (and fix CI) (`#3274 `_) +- Added missing documentation (`#3275 `_) +- fix the bug in function differentiate_mats() (`#3277 `_) +- Fix the bug in the ``Material.from_xml_element`` function (`#3278 `_) +- Doc typo fix for rand ray mgxs (`#3280 `_) +- Consolidate plotting capabilities in Model.plot (`#3282 `_) +- adding non elastic MT number (`#3285 `_) +- Fix ``Tabular.from_xml_element`` for histogram case (`#3287 `_) +- Random Ray Source Region Refactor (`#3288 `_) +- added terminal output showing compile options selected (`#3291 `_) +- Random ray consistency changes (`#3298 `_) +- Random Ray Explicit Void Treatment (`#3299 `_) +- removed old command line scripts (`#3300 `_) +- Avoid end of life ubuntu 20.04 in ReadTheDocs runner (`#3301 `_) +- Avoid error in CI from newlines in commit message (`#3302 `_) +- Handle reflex angles in CylinderSector (`#3303 `_) +- Relax requirement on polar/azimuthal axis for wwinp conversion (`#3307 `_) +- Add nuclides_to_ignore argument on Model export methods (`#3309 `_) +- Enable overlap plotting from Python API (`#3310 `_) +- Fix access order issues after applying tally results from `Model.run` (`#3313 `_) +- Random Ray Void Accuracy Fix (`#3316 `_) +- Fixes for problems encountered with version determination (`#3320 `_) +- Clarify effect of CMAKE_BUILD_TYPE in docs (`#3321 `_) +- Random Ray Linear Source Stability Improvement (`#3322 `_) +- Mark a canonical URL for docs (`#3324 `_) +- Random Ray Adjoint Source Logic Improvement (`#3325 `_) +- Reflect multigroup MicroXS in IndependentOperator docstrings (`#3327 `_) +- NCrystal becomes runtime rather than buildtime dependency (`#3328 `_) +- Adding per kg as unit option on material functions (`#3329 `_) +- Fix reading of horizontal field of view for ray-traced plots (`#3330 `_) +- Manually fix broken links (`#3331 `_) +- Update pugixml to v1.15 (`#3332 `_) +- Determine nuclides correctly for DAGMC models in d1s.get_radionuclides (`#3335 `_) +- openmc.Material.mix_materials() allows for keyword arguments (`#3336 `_) +- Fix bug in ``Mesh::material_volumes`` for void materials (`#3337 `_) +- added stable and unstable nuclides to the Chain object (`#3338 `_) diff --git a/docs/source/releasenotes/index.rst b/docs/source/releasenotes/index.rst index bde1205891e..2927cc458e2 100644 --- a/docs/source/releasenotes/index.rst +++ b/docs/source/releasenotes/index.rst @@ -7,6 +7,7 @@ Release Notes .. toctree:: :maxdepth: 1 + 0.15.1 0.15.0 0.14.0 0.13.3 diff --git a/docs/source/usersguide/basics.rst b/docs/source/usersguide/basics.rst index 60b599588eb..c0bc2f976f4 100644 --- a/docs/source/usersguide/basics.rst +++ b/docs/source/usersguide/basics.rst @@ -53,7 +53,7 @@ eXtensible Markup Language (XML) Unlike many other Monte Carlo codes which use an arbitrary-format ASCII file with "cards" to specify a particular geometry, materials, and associated run settings, the input files for OpenMC are structured in a set of `XML -`_ files. XML, which stands for eXtensible Markup +`_ files. XML, which stands for eXtensible Markup Language, is a simple format that allows data to be exchanged efficiently between different programs and interfaces. diff --git a/docs/source/usersguide/beginners.rst b/docs/source/usersguide/beginners.rst index eef927b846c..6876a33240a 100644 --- a/docs/source/usersguide/beginners.rst +++ b/docs/source/usersguide/beginners.rst @@ -109,8 +109,8 @@ familiar with. Whether you plan on working in Linux, macOS, or Windows, you should be comfortable working in a command line environment. There are many resources online for learning command line environments. If you are using Linux or Mac OS X (also Unix-derived), `this tutorial -`_ will help you get acquainted with -commonly-used commands. +`_ will help you get acquainted +with commonly-used commands. To reap the full benefits of OpenMC, you should also have basic proficiency in the use of `Python `_, as OpenMC includes a rich Python @@ -127,8 +127,8 @@ are hosted at `GitHub`_. In order to receive updates to the code directly, submit `bug reports`_, and perform other development tasks, you may want to sign up for a free account on GitHub. Once you have an account, you can follow `these instructions -`_ on -how to set up your computer for using GitHub. +`_ +on how to set up your computer for using GitHub. If you are new to nuclear engineering, you may want to review the NRC's `Reactor Concepts Manual`_. This manual describes the basics of nuclear power for @@ -149,9 +149,9 @@ and `Volume II`_. You may also find it helpful to review the following terms: .. _neutron transport: https://en.wikipedia.org/wiki/Neutron_transport .. _discretization: https://en.wikipedia.org/wiki/Discretization .. _constructive solid geometry: https://en.wikipedia.org/wiki/Constructive_solid_geometry -.. _git: http://git-scm.com/ +.. _git: https://git-scm.com/ .. _git tutorials: https://git-scm.com/doc -.. _Reactor Concepts Manual: http://www.tayloredge.com/periodic/trivia/ReactorConcepts.pdf +.. _Reactor Concepts Manual: https://www.tayloredge.com/periodic/trivia/ReactorConcepts.pdf .. _Volume I: https://www.standards.doe.gov/standards-documents/1000/1019-bhdbk-1993-v1 .. _Volume II: https://www.standards.doe.gov/standards-documents/1000/1019-bhdbk-1993-v2 .. _OpenMC source code: https://github.com/openmc-dev/openmc diff --git a/docs/source/usersguide/data.rst b/docs/source/usersguide/data.rst index a1611de6352..2a9cd36dbb4 100644 --- a/docs/source/usersguide/data.rst +++ b/docs/source/usersguide/data.rst @@ -12,9 +12,9 @@ responsible for specifying one or more of the following: file (commonly named ``cross_sections.xml``) contains a listing of other data files, in particular neutron cross sections, photon cross sections, and windowed multipole data. Each of those files, in turn, uses a `HDF5 - `_ format (see :ref:`io_nuclear_data`). In - order to run transport simulations with continuous-energy cross sections, you - need to specify this file. + `_ format (see + :ref:`io_nuclear_data`). In order to run transport simulations with + continuous-energy cross sections, you need to specify this file. - **Depletion chain (XML)** -- A :ref:`depletion chain XML ` file contains decay data, fission product yields, and information on what @@ -69,7 +69,7 @@ If you want to persistently set the environment variables used to initialized the configuration, export them from your shell profile (``.profile`` or ``.bashrc`` in bash_). -.. _bash: http://www.linuxfromscratch.org/blfs/view/6.3/postlfs/profile.html +.. _bash: https://www.linuxfromscratch.org/blfs/view/6.3/postlfs/profile.html -------------------------------- Continuous-Energy Cross Sections @@ -290,16 +290,16 @@ calculation to be performed. Therefore, at this point in time, OpenMC is not distributed with any pre-existing multigroup cross section libraries. However, if a multigroup library file is downloaded or generated, the path to the file needs to be specified as described in :ref:`usersguide_data_runtime`. For an -example of how to create a multigroup library, see the `example notebook -`_. +example of how to create a multigroup library, see this `MG mode notebook +`_. -.. _NJOY: http://www.njoy21.io/ +.. _NJOY: https://www.njoy21.io/ .. _NNDC: https://www.nndc.bnl.gov/endf .. _MCNP: https://mcnp.lanl.gov -.. _Serpent: https://serpent.vtt.fi/serpent/ +.. _Serpent: https://serpent.vtt.fi .. _ENDF/B: https://www.nndc.bnl.gov/endf-b7.1/acefiles.html .. _JEFF: https://www.oecd-nea.org/dbdata/jeff/jeff33/ -.. _TENDL: https://tendl.web.psi.ch/tendl_2017/tendl2017.html +.. _TENDL: https://tendl.web.psi.ch/tendl_2023/tendl2023.html .. _Seltzer and Berger: https://doi.org/10.1016/0092-640X(86)90014-8 .. _NIST ESTAR database: https://physics.nist.gov/PhysRefData/Star/Text/ESTAR.html .. _Biggs et al.: https://doi.org/10.1016/0092-640X(75)90030-3 diff --git a/docs/source/usersguide/decay_sources.rst b/docs/source/usersguide/decay_sources.rst index e612206969c..a228f8e66bb 100644 --- a/docs/source/usersguide/decay_sources.rst +++ b/docs/source/usersguide/decay_sources.rst @@ -47,14 +47,14 @@ Direct 1-Step (D1S) Calculations ================================ OpenMC also includes built-in capability for performing shutdown dose rate -calculations using the `direct 1-step `_ -(D1S) method. In this method, a single coupled neutron--photon transport -calculation is used where the prompt photon production is replaced with photons -produced from the decay of radionuclides in an activated material. To obtain -properly scaled results, it is also necessary to apply time correction factors. -A normal neutron transport calculation can be extended to a D1S calculation with -a few helper functions. First, import the ``d1s`` submodule, which is part of -:mod:`openmc.deplete`:: +calculations using the `direct 1-step +`_ (D1S) method. In this method, +a single coupled neutron--photon transport calculation is used where the prompt +photon production is replaced with photons produced from the decay of +radionuclides in an activated material. To obtain properly scaled results, it is +also necessary to apply time correction factors. A normal neutron transport +calculation can be extended to a D1S calculation with a few helper functions. +First, import the ``d1s`` submodule, which is part of :mod:`openmc.deplete`:: from openmc.deplete import d1s diff --git a/docs/source/usersguide/install.rst b/docs/source/usersguide/install.rst index 0aa561ee3d7..d294770e0e0 100644 --- a/docs/source/usersguide/install.rst +++ b/docs/source/usersguide/install.rst @@ -8,56 +8,35 @@ Installation and Configuration .. _install_conda: --------------------------------------------------- -Installing on Linux/Mac with Mamba and conda-forge --------------------------------------------------- +---------------------------------- +Installing on Linux/Mac with Conda +---------------------------------- -`Conda `_ is an open source package management -systems and environments management system for installing multiple versions of +`Conda`_ is an open source package management +system and environments management system for installing multiple versions of software packages and their dependencies and switching easily between them. -`Mamba `_ is a cross-platform package -manager and is compatible with `conda` packages. -OpenMC can be installed in a `conda` environment with `mamba`. -First, `conda` should be installed with one of the following installers: -`Miniconda `_, -`Anaconda `_, or `Miniforge `_. -Once you have `conda` installed on your system, OpenMC can be installed via the -`conda-forge` channel with `mamba`. +OpenMC can be installed in a `conda` environment. First, `conda` should be +`installed `_ +with either Anaconda Distribution or Miniconda. Once you have `conda` installed +on your system, OpenMC can be installed via the `conda-forge` channel. First, add the `conda-forge` channel with: .. code-block:: sh conda config --add channels conda-forge + conda config --set channel_priority strict -Then create and activate a new conda enviroment called `openmc-env` in -which to install OpenMC. +Then create and activate a new conda enviroment called `openmc-env` (or whatever +you wish) with OpenMC installed. .. code-block:: sh - conda create -n openmc-env + conda create --name openmc-env openmc conda activate openmc-env -Then install `mamba`, which will be used to install OpenMC. - -.. code-block:: sh - - conda install mamba - -To list the versions of OpenMC that are available on the `conda-forge` channel, -in your terminal window or an Anaconda Prompt run: - -.. code-block:: sh - - mamba search openmc - -OpenMC can then be installed with: - -.. code-block:: sh - - mamba install openmc - -You are now in a conda environment called `openmc-env` that has OpenMC installed. +You are now in a conda environment called `openmc-env` that has OpenMC +installed. ------------------------------------------- Installing on Linux/Mac/Windows with Docker @@ -557,7 +536,7 @@ distributions. notebook `_. - `h5py `_ + `h5py `_ h5py provides Python bindings to the HDF5 library. Since OpenMC outputs various HDF5 files, h5py is needed to provide access to data within these files from Python. @@ -610,5 +589,6 @@ wrapper is used when installing h5py: CC= HDF5_MPI=ON HDF5_DIR= python -m pip install --no-binary=h5py h5py +.. _Mamba: https://mamba.readthedocs.io/en/latest/ .. _Conda: https://conda.io/en/latest/ .. _pip: https://pip.pypa.io/en/stable/ diff --git a/docs/source/usersguide/parallel.rst b/docs/source/usersguide/parallel.rst index e00ed5e97ba..ecbdd20b626 100644 --- a/docs/source/usersguide/parallel.rst +++ b/docs/source/usersguide/parallel.rst @@ -101,5 +101,5 @@ performance on a machine when running in parallel: settings = openmc.Settings() settings.output = {'tallies': False} -.. _Haswell-EP: http://www.anandtech.com/show/8423/intel-xeon-e5-version-3-up-to-18-haswell-ep-cores-/4 +.. _Haswell-EP: https://www.anandtech.com/show/8423/intel-xeon-e5-version-3-up-to-18-haswell-ep-cores-/4 .. _bound: https://github.com/pmodels/mpich/blob/main/doc/wiki/how_to/Using_the_Hydra_Process_Manager.md#process-core-binding diff --git a/include/openmc/random_dist.h b/include/openmc/random_dist.h index 11e88ab8cce..32f055b53d3 100644 --- a/include/openmc/random_dist.h +++ b/include/openmc/random_dist.h @@ -64,8 +64,8 @@ extern "C" double watt_spectrum(double a, double b, uint64_t* seed); //! Samples from a normal distribution with a given mean and standard deviation //! The PDF is defined as s(x) = (1/2*sigma*sqrt(2) * e-((mu-x)/2*sigma)^2 //! Its sampled according to -//! http://www-pdg.lbl.gov/2009/reviews/rpp2009-rev-monte-carlo-techniques.pdf -//! section 33.4.4 +//! https://pdg.lbl.gov/2023/reviews/rpp2023-rev-monte-carlo-techniques.pdf +//! section 42.4.4 //! //! \param mean mean of the Gaussian distribution //! \param std_dev standard deviation of the Gaussian distribution diff --git a/openmc/data/ace.py b/openmc/data/ace.py index 1247593a806..6ccb76c9223 100644 --- a/openmc/data/ace.py +++ b/openmc/data/ace.py @@ -9,9 +9,9 @@ ENDF data has been reconstructed and Doppler-broadened, the ACER module generates ACE-format cross sections. -.. _MCNP: https://laws.lanl.gov/vhosts/mcnp.lanl.gov/ -.. _NJOY: http://t2.lanl.gov/codes.shtml -.. _ENDF: http://www.nndc.bnl.gov/endf +.. _MCNP: https://mcnp.lanl.gov/ +.. _NJOY: https://www.njoy21.io/ +.. _ENDF: https://www.nndc.bnl.gov/endf-library/ """ diff --git a/openmc/data/endf.py b/openmc/data/endf.py index 63cd092ebb1..eca37446933 100644 --- a/openmc/data/endf.py +++ b/openmc/data/endf.py @@ -1,9 +1,9 @@ """Module for parsing and manipulating data from ENDF evaluations. -All the classes and functions in this module are based on document -ENDF-102 titled "Data Formats and Procedures for the Evaluated Nuclear -Data File ENDF-6". The latest version from June 2009 can be found at -http://www-nds.iaea.org/ndspub/documents/endf/endf102/endf102.pdf +All the classes and functions in this module are based on document ENDF-102 +titled "Data Formats and Procedures for the Evaluated Nuclear Data File ENDF-6". +The version from September 2023 can be found at +https://www.nndc.bnl.gov/endfdocs/ENDF-102-2023.pdf """ import io diff --git a/openmc/data/fission_energy.py b/openmc/data/fission_energy.py index 870881dbaf7..3c7998ee214 100644 --- a/openmc/data/fission_energy.py +++ b/openmc/data/fission_energy.py @@ -44,7 +44,7 @@ class from the usual OpenMC HDF5 data files. ---------- [1] D. G. Madland, "Total prompt energy release in the neutron-induced fission of ^235U, ^238U, and ^239Pu", Nuclear Physics A 772:113--137 (2006). - + Attributes ---------- diff --git a/openmc/deplete/integrators.py b/openmc/deplete/integrators.py index a877c4900f6..50810c88a36 100644 --- a/openmc/deplete/integrators.py +++ b/openmc/deplete/integrators.py @@ -446,7 +446,7 @@ class SICELIIntegrator(SIIntegrator): `_. Detailed algorithm can be found in section 3.2 in `Colin Josey's thesis - `_. + `_. """ _num_stages = 2 @@ -512,7 +512,7 @@ class SILEQIIntegrator(SIIntegrator): `_. Detailed algorithm can be found in Section 3.2 in `Colin Josey's thesis - `_. + `_. """ _num_stages = 2 diff --git a/openmc/examples.py b/openmc/examples.py index 8d4bd1f04c3..5578d513ead 100644 --- a/openmc/examples.py +++ b/openmc/examples.py @@ -11,8 +11,9 @@ def pwr_pin_cell() -> openmc.Model: This model is a single fuel pin with 2.4 w/o enriched UO2 corresponding to a beginning-of-cycle condition and borated water. The specifications are from - the `BEAVRS `_ benchmark. Note that the - number of particles/batches is initially set very low for testing purposes. + the `BEAVRS `_ benchmark. Note that + the number of particles/batches is initially set very low for testing + purposes. Returns ------- diff --git a/openmc/mgxs/__init__.py b/openmc/mgxs/__init__.py index 6a29a8383ca..901c1eabb6b 100644 --- a/openmc/mgxs/__init__.py +++ b/openmc/mgxs/__init__.py @@ -14,7 +14,7 @@ - "SHEM-361_" designed for LWR analysis to eliminate self-shielding calculations of thermal resonances ([HFA2005]_, [SAN2007]_, [HEB2008]_) - "SCALE-X" (where X is 44 which is designed for criticality analysis - and 252 is designed for thermal reactors) for the SCALE code suite + and 252 is designed for thermal reactors) for the SCALE code suite ([ZAL1999]_ and [REARDEN2013]_) - "MPACT-X" (where X is 51 (PWR), 60 (BWR), 69 (Magnox)) from the MPACT_ reactor physics code ([KIM2019]_ and [KIM2020]_) @@ -28,12 +28,12 @@ .. _SCALE252: https://oecd-nea.org/science/wpncs/amct/workingarea/meeting2013/EGAMCT2013_08.pdf .. _MPACT: https://vera.ornl.gov/mpact/ .. _XMAS-172: https://www-nds.iaea.org/wimsd/energy.htm -.. _SHEM-361: https://www.polymtl.ca/merlin/downloads/FP214.pdf +.. _SHEM-361: http://merlin.polymtl.ca/downloads/FP214.pdf .. _activation: https://fispact.ukaea.uk/wiki/Keyword:GETXS .. _VITAMIN-J-42: https://www.oecd-nea.org/dbdata/nds_jefreports/jefreport-10.pdf .. _CCFE-709: https://fispact.ukaea.uk/wiki/CCFE-709_group_structure .. _UKAEA-1102: https://fispact.ukaea.uk/wiki/UKAEA-1102_group_structure -.. _ECCO-1968: http://serpent.vtt.fi/mediawiki/index.php/ECCO_1968-group_structure +.. _ECCO-1968: https://serpent.vtt.fi/mediawiki/index.php/ECCO_1968-group_structure .. [SAR1990] Sartori, E., OECD/NEA Data Bank: Standard Energy Group Structures of Cross Section Libraries for Reactor Shielding, Reactor Cell and Fusion Neutronics Applications: VITAMIN-J, ECCO-33, ECCO-2000 and XMAS JEF/DOC-315 diff --git a/openmc/tallies.py b/openmc/tallies.py index 585e54a7ef3..ebb313551ab 100644 --- a/openmc/tallies.py +++ b/openmc/tallies.py @@ -1502,7 +1502,7 @@ def get_pandas_dataframe(self, filters=True, nuclides=True, scores=True, df.columns = pd.MultiIndex.from_tuples(columns) # Modify the df.to_string method so that it prints formatted strings. - # Credit to http://stackoverflow.com/users/3657742/chrisb for this trick + # Credit to https://stackoverflow.com/users/3657742/chrisb for this trick df.to_string = partial(df.to_string, float_format=float_format.format) return df diff --git a/src/math_functions.cpp b/src/math_functions.cpp index ba1c6c3db14..5469b56c87c 100644 --- a/src/math_functions.cpp +++ b/src/math_functions.cpp @@ -650,7 +650,7 @@ void calc_zn(int n, double rho, double phi, double zn[]) // =========================================================================== // Determine vector of sin(n*phi) and cos(n*phi). This takes advantage of the // following recurrence relations so that only a single sin/cos have to be - // evaluated (http://mathworld.wolfram.com/Multiple-AngleFormulas.html) + // evaluated (https://mathworld.wolfram.com/Multiple-AngleFormulas.html) // // sin(nx) = 2 cos(x) sin((n-1)x) - sin((n-2)x) // cos(nx) = 2 cos(x) cos((n-1)x) - cos((n-2)x) diff --git a/tools/dev/generate_release_notes.py b/tools/dev/generate_release_notes.py new file mode 100644 index 00000000000..dee46ac49e5 --- /dev/null +++ b/tools/dev/generate_release_notes.py @@ -0,0 +1,17 @@ +import argparse +import re +import subprocess + +parser = argparse.ArgumentParser() +parser.add_argument('tag') +args = parser.parse_args() + +proc = subprocess.run(["git", "log", "--format=%s", f"{args.tag}.."], capture_output=True, text=True) +data = [] +for line in proc.stdout.rstrip().split('\n'): + m = re.match(r'(.*) \(\#(\d+)\)', line) + if m is not None: + data.append(m.groups()) + +for comment, num in sorted(data, key=lambda x: int(x[1])): + print(f'- {comment} (`#{num} `_)')
  • `BTVz zMB)c?1qpIG95ua!pEsVu;}p{9 z#_cYhfCb4xjcWC{`%mhpT$(D17i8z$d9tvt z8ho#&DRBQf2v1mhowJjS1A2ZLM4YaK$B*K3!aP^_q71u$(dVFjXKR|VesD>?+PAf% z31+Vb-Lp{`fxm*C`D?4&;Jz=rJ1-C=BPy>w)FW>35X>JfJiGa6O1{sx0+X7B2&*Z&Y!Xj1B^Cvgz~u>-!zsX$VxDOPUCSdE-qe@ zbpH-nsY^$`kKo#)9C9Xh_XNI!jva&i?O!Xw!|870mM0UiM&^)mm&65Gd zqpH*%i4*Pz<8x9vn=ZvX?}EJz!6oTzIL<`|p-1jP-@xMdYFPE*Y(OWje%1W?Bovbu zd}tF$gl3Yu^~oxJgrFW>JUy~(k@1Z#*%gGzd!&naQ z!J)vTqe(zW;iH)MXcPEz-u|rIt8qABlQnD2n+&+Y@b7yLKHAbQwpWHAB#U@ z!o?|w((+m+*Sew2@2LhckqlTo=UH!PFaj4$?VbchBj}OH??P+-!36W?44&Qio6guC zmHY-A{oV8r99)1Kn<+7}m0yi-8a2a%6_&Yz`Q@ZX<#u%&w5_A;z< zs7n9bL+uzF2cgBs&ZNJ}1W}K8 zTs#LG;MChF{*Eqyro9$n&lAf=(67(qaRjxgkN%(~au=#UOP41)ld?O%H zepJX+(hc8xlxs?&hTy^(i97J*1>~`9WU&AH?Q=*R4v71kEkNcntGRD#xcliPgR{dv>#X^Z{Wo?q!m}HRSP$u4 zK!RE2_Btw1hKp0sJ)hUO(>(yVK(dTjm}$y{=-3GRh#8kE#L>+M4)k?G6tF89Fdl5(FvmjjSg?^nDedmR0KxKc8tgDj3 z=5WWwyDzmE0l8-fw(m1zw6o8JgrbY#v96$++ossY$LK%6)5i@Mf$Z zRtrj>9uw+;^YsQ_=8yLwdx7iAzA<@%9P=%}X8QIj#q+DGBXCvp@VTddaDE+fO&k4j zp%}!$y1=NS=Wx<%I(W!_655}?ImmFX1OY33(}CmL=cISa7>`&kg6Jw&rj96Fzw!8! z#~jgOeQ;-@jzV-MGM0p{8lbn$IYOK?5L}r8 zPoC*fSGl}C-tMmtCFan1W9?DjX1Mp+vbfuWPT=k(^sX2+3=Oq9B`=(0MssEaAL@-f zOpvn`H`vOi1fsT&7ktK{O(MId&o$gQI}gV{^sniAf%5ycqW+{BsP<}#)@bh#+-;Gi zwWpyT@kCQRXgW){9^LZ2EzXJVrtY-aDUcjdvuJ68Yll2{a6M(3+YfGxaYzYTm&2KN zONQ0v15kWCb5t|_IZ(eYB^KGpPEZd%hxR#Z#`za-q2STsfk>%2xMep#sw!)-6A@7F zt4Gf1@8#fX&+}&iB9m}r%;`z|t3&ANW7CCUf`sEpWLrWu)7`vfjQek|&zEj@E+1)_ z#^%ql#;OZvgbIMF;=X&r5~DEZtBA8T=z@=8BH83`WB@38&C=C>`y7{_wl&Cb3-JAD z#$D(^j@iIdV#IIdc*G7(K#kSI!WpWGaHWJaSpQfx^evz@+c&_B2poUgU8g~qL%ykF zD+#F2DcyO004}h3)E`bG$84-$pYQBO%{!%$1rJaj&stUa32Pfh1n;cR7pJ>iGd|!G z380S{Gl9sJ*N?x%QNjN5Y7cYb7%&KznMRW&smO9;ufkP!PrLzTTcZ} zVEsDA%rP@lu^Ce6$ZNgoD}!9O#*Vp_jKPX8t9kks=^*2VUH5)p!sC1KIl02M;hcOZ z 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 I=Y?4Z0NP<`Z~y=R literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-21/inputs_true.dat b/tests/regression_tests/surface_source_write/case-21/inputs_true.dat new file mode 100644 index 00000000000..893b8b5b975 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-21/inputs_true.dat @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 4 + 300 + 3 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-21/results_true.dat b/tests/regression_tests/surface_source_write/case-21/results_true.dat new file mode 100644 index 00000000000..7ccce7cc3a6 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-21/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +1.149925E+00 2.542255E-01 diff --git a/tests/regression_tests/surface_source_write/case-21/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-21/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..ed87ce849f788a346f99843aa263565fd326040c GIT binary patch literal 2144 zcmeHI!A`KklY} zzdej5Nr*X3OP&ekQ8*a|lwq1ggLcq|OOs&n&780s#X)cUJyR_U--OKEEUH<~jpI0K nusoVGSFHZC=n6-qen}4IqrvUfbzNWu)_zFyYF=;a|J;Euz71;T literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-a01/inputs_true.dat b/tests/regression_tests/surface_source_write/case-a01/inputs_true.dat new file mode 100644 index 00000000000..a65c9f770eb --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-a01/inputs_true.dat @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 1 2 3 + 200 + 2 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-a01/results_true.dat b/tests/regression_tests/surface_source_write/case-a01/results_true.dat new file mode 100644 index 00000000000..8979eb55477 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-a01/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.169123E-02 9.144814E-03 diff --git a/tests/regression_tests/surface_source_write/case-a01/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-a01/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..4a86235679085a07458904653a5b63f162059dea GIT binary patch literal 6616 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 LC5*DhS910bE;gYLeTFMBG>ducP#FN z=pYOE7`c2{G2H!|uFoDUCF$kQq)p5?jO)XSGp})T98XL?c!rhn86EsKg(L1=yM3JY zeP_GMnMfU%4B*zk$kp4L4+Xa?ReVw<$cCMt6MYPrbt7=-=fQHE5B_fjlJoV;q_h`# zpqW*-<4XY*ZHix-FR^+6jc%#SlxizMD!NZ)QjgTY++TO5iQa65`8&L0uMN*3vzc7< z8slQ-Luf?TjrIGSNj-1ue)CeKY0MM#FqDdf#geWmZTSX4Ek<2YFI&MSEZCSku@<72 g7E`WJ^I(JNZ + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -4 -4 -20 4 4 20 + + + + 300 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-d01/results_true.dat b/tests/regression_tests/surface_source_write/case-d01/results_true.dat new file mode 100644 index 00000000000..7fb415cb159 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-d01/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +9.263843E-01 3.193920E-02 diff --git a/tests/regression_tests/surface_source_write/case-d01/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-d01/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..8f8cd604dc7d685af08c32e69922749b6ad01654 GIT binary patch literal 33344 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 literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-d02/inputs_true.dat b/tests/regression_tests/surface_source_write/case-d02/inputs_true.dat new file mode 100644 index 00000000000..6877634f12c --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-d02/inputs_true.dat @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -4 -4 -20 4 4 20 + + + + 1 + 300 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-d02/results_true.dat b/tests/regression_tests/surface_source_write/case-d02/results_true.dat new file mode 100644 index 00000000000..7fb415cb159 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-d02/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +9.263843E-01 3.193920E-02 diff --git a/tests/regression_tests/surface_source_write/case-d02/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-d02/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..3abc471e043152b09e736a5f09012f5f0a95191b GIT binary patch literal 31472 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 + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -4 -4 -20 4 4 20 + + + + 300 + 2 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-d03/results_true.dat b/tests/regression_tests/surface_source_write/case-d03/results_true.dat new file mode 100644 index 00000000000..7fb415cb159 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-d03/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +9.263843E-01 3.193920E-02 diff --git a/tests/regression_tests/surface_source_write/case-d03/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-d03/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..8435e8f17f4284ea1916ca71e048265aea650f19 GIT binary patch literal 33344 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 TcuCG7%T29+iWS#a5jp + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -4 -4 -20 4 4 20 + + + + 1 + 300 + 2 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-d04/results_true.dat b/tests/regression_tests/surface_source_write/case-d04/results_true.dat new file mode 100644 index 00000000000..7fb415cb159 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-d04/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +9.263843E-01 3.193920E-02 diff --git a/tests/regression_tests/surface_source_write/case-d04/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-d04/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..6c45d92e6117a936f05040f5ce367471f98b32fa GIT binary patch literal 31472 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 literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-d05/inputs_true.dat b/tests/regression_tests/surface_source_write/case-d05/inputs_true.dat new file mode 100644 index 00000000000..865633fbb8c --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-d05/inputs_true.dat @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -4 -4 -20 4 4 20 + + + + 300 + 2 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-d05/results_true.dat b/tests/regression_tests/surface_source_write/case-d05/results_true.dat new file mode 100644 index 00000000000..7fb415cb159 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-d05/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +9.263843E-01 3.193920E-02 diff --git a/tests/regression_tests/surface_source_write/case-d05/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-d05/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..24b04a862e42c6038d6b0e65184b43715c8145d3 GIT binary patch literal 33344 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�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 + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -4 -4 -20 4 4 20 + + + + 300 + 2 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-d06/results_true.dat b/tests/regression_tests/surface_source_write/case-d06/results_true.dat new file mode 100644 index 00000000000..7fb415cb159 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-d06/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +9.263843E-01 3.193920E-02 diff --git a/tests/regression_tests/surface_source_write/case-d06/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-d06/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..3eb95bef96a07b77a50c9ef9df25e0a849075c08 GIT binary patch literal 29496 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 literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-d07/inputs_true.dat b/tests/regression_tests/surface_source_write/case-d07/inputs_true.dat new file mode 100644 index 00000000000..77498b64db3 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-d07/inputs_true.dat @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -4 -4 -20 4 4 20 + + + + 101 102 103 104 105 106 + 300 + 7 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-d07/results_true.dat b/tests/regression_tests/surface_source_write/case-d07/results_true.dat new file mode 100644 index 00000000000..63ea1a64d41 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-d07/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +9.756086E-01 4.639209E-02 diff --git a/tests/regression_tests/surface_source_write/case-d07/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-d07/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..0332f2dcfb100123fe3a4ee3a47f690be2af1bab GIT binary patch literal 33344 zcmeF4c{~+g{Qqs)%bF!*-qk-|zP5p1E`9p69&Y=Y8Ji%sFRjSMzz zqMAAq&mT|8UsLfhOm8vbJN|$A|C9x^H=+N!iYZw&?oSzHKf{gTdc@7e%4WZ%#bL*P zcj11C;0OFKQx3Af&h~3oDrIv1OjL|h|7`7@Y>s(2+u+%AO>NLm{m1#!=k?F$MgQ&# zB>O)#xn@vPPamm3{sErSQYq8^vx{fS@$IHkm#O~?dGSo;|BsDMEac#+V>m>>l!|{| z2$ebumHOn=?>+wgJQWtm>0HTw{q`jJU}7}+pE>;3Z{5g;;br_ke>J`5|7ML-!S3JJ zv=@;-{73R%_Bm#M=wCn6{Ey=)cm3CI-47o7_Yj2g!2j6(uiskRI9YhuSnogRa>UJf z>Nnc|@jJ?0>{E_I_wNo7Dpxlb+x_;|R63Lg{Kq!sZ=5Y$j@er|&D1IV|2Up<7v*mk zO?mA<^@fIq=HD8{NUmqJ)Cb7!T!*{&zgcjSz5Jhp0|M=A4fS?xq@KzF_kCw_eWm36 z|LyrLkmG*)i0-%~R<RErwU{T=QIW9RbJ~ zKIz1A7HosQRi)>L3LwpCq!>%;02kxWzKxSD2aWxKpR|fMpb{22{?=Kx1dhY>NmBiz zXS$yJ(N)+g9sXjq`V5E7H_3uVnHv-SkhaS-y&i=4d@cF;s1*vR`D6}ymO&k_eJ^({ zUxJd@xg(ov9Tv=)J_QnTQ%k1S=b0LExTN5A4+sCeoaJ5a>oWEpB5>^fqUVD8nUMou zrExvji_jN8=-UO6TjTm`8=5M?-GpP?MLSv`ACKTQcb9swzew+8dY=KZ?#i9>D$VZ+ z9D54RKW*$kFT8y2l%F3qj1E_t1Be2+o$@@daX=^0Y3)xE)?~`{-;!1 zl3!hdT_kN`lL1xq<61=6Ur29G4ju=l^Tppu7@Nw@MV_DeuzQBxhxiT&VcZ#8`BW`> zU~&8hu&ON^=D(M9c5KK3;S5eI86uU?{Ku|?0#8i{oP*@-{_%t9jyRq_o{}&f|A9!O z;$@f(bp*empa`ZHtkinpQ5~SIf4XzOXes=3AZx?xuWiu7#q%6uZBK>OJr0Ie= zi9Sb8KHV;hWn^YelpmJErpcsvIjZXn3%J#-2^`9LI$O`!R$ex@TxtZ*|HMn=u-z>g ztYw_PP*kJ_1g(#<8av$%S!y_wT(_1%H@YFqjRAtFv)9!gX4?Y<4y7L~p3ZRQ9f?!O zbmb|8IOj-?b@K0gc8t1KKY#y@eBiVDs}fguH{9HA%ub(@2lX5-{8UWsL2kuXyjy$4 zaZb*X8N&2NqVdrS*?iI%&M_YGFH-bTz-VOS8b_SILA#A_uS^CsLD{#``%YYJh2i0! zb|?8tpv;j$)awGx=j3qA;*j%4UL{@KwZ&HwIcYP{_u7>i6NTNbUs<~W8|HfIigpE1 z4B4qz|M@L6vs`Z#Q%*u3Ewh}otu-caDC;TbERM*fu9yA%Js>Q-Flu`j148Cvy^|@M z%a>Ha{041qzrlQPBJb+<27^u*n;8|yMx};Ibcb%zjMpM?DE;7{{_sC9yq->fA|acv zovkbE7RPRdmcwJ~#1Z!@x*NCD+W_YU_vXMSRd91aj*q~#DZl9RPTQqKLO<(qTsLDR z)*nhe+|wCT;g<#6jXy@-gHZquJOlpR%Mdab>v9*q*Zk=_;5ArNBWGC-tO_=sIDD!L zQau+j{2Eumq?tu+E416;V7l&; z0{9so`sr75i=`3n`^|Hmxn?c;Ynv8-^HzHThcX^4o5k4}DXF#RwKB#Y5y5TwdM3^x z*=aj%3zjrMhc?!Gi8|Hrr-5l}<7hKjk!pK9rc4CQ&0&{Rv$i2{Y{_~56L$p3+ju)P z{gH%hkj*d=WuwOqd#`$wS2hXaM74wUk~@KkH4l#reI-0m#QMo%JP)+I9azq3XN*<_ zjCkMNW=Y^smX8pbLrh4>fnj6gvOx{x@?gdh{c=gfr24dABug9Mc8a>Q>1aKO@uEu| z_*M!Q`KzbwO=Ls6&iNe<+<$gX&T=AvO6Yp*zDaRGYsx9MNU-_k&ZM6Q+tTVjY1=PFO)vJm+^B3p;83>P!ekEa2XFhZC);fl5u6jU zLJib=S3@qz@%21s662*vXs1q%1ANBejBC~AE*oRcF;=HLm} zq#Ijs6AQ*7a#3?Ig@1;}urBPqbII~+(Buj}4)b~>!ICH$9vQU^?M8Cx%M_!|Hh zR9~2>)`5;(hx+&$Yh+MME2iPjD*}fy9!QhzModW92wPCc1uH&8FK790Kdwpm^=zg3 zq+=U!p2*?vIGPRjB?B4be3*MZvrbuZOg@SHa}Aeb<}vq`~y>ru*@f^=Gyo z-N&WCnNDSfW6F{f@=8Vq6S@^GyV$S|>hgQ+2%2bwBVQs?_T{Gomn%A}9a|H@2FDz@ zUP*Sr9I_6~Tw%$#v#lz~h;Yqn+XBgXIp(>QCaHY#1kT^e=Vi;H^N-_Y5Z@e+jBK+% z*Cl0NtTSmHn!)9*&lR`UT0XS`h@bKtUof-0%qQq zhP4XG?vO_8z0xA~W{4rvUy!g~YM1-*k6Xa?LiuYewX(oy+XvwzFPg#YfwAB;CSR~F zW-$K#4n~5Wzj4U?C%*(0bh)QI-DoWr`)}PsWa85yQm`}qG zX{5*`_aI%1G$so!_@4G^fU4>-$K$-(;EfwM(k+wn;kZ&$Wk*Rk$heUzZ|5%gzvW;( ziw_sFvn|J-m9n3*sTDy~zO-kxpKO8O!@SM}#1}(9->P317d3&h46*Gm#HG-LGt$cU zJLm3S%S{WI-QU5IP2U)P>sf|bGmD(kwVL8w|MqCBe2I?+eb@OOD9TV{xc&CBhNXNi(ZJ^8M15cS2}^e;a475P zYz{epOxD}i_e-oiqF<`CtK-pfOl~8|6{Br~pO$9c?M^5Mb<(t6HCc6V1=ZJp`+jTC zUpBtGeyYu_N1kjqJfVR-@M~lT)GU4A;JN0ks|Cd=v@!Q&F`HX)O7jBZ*D#CShfz z>#uAu(ZI}>o;2I9T?r3b)#;70bio^4kFP#>_8r=nt@mUr+=xmALU)5nZ32h#`bTLN zC#djZsP8*2Yzg}am83%>xjvAPi0^J+*R8LF%wBn;*=HKTQE4-o3j1cz%_zNtJ5d3Z zD;{oIEucW)P}*(wcqM$@bJyyK2r?mIb1p^w_e}W&>Uba0sx1RlnkTmJR5S$|o_DXX zo$dzJ4rz|NHz}a5aUa(2;&+>qqe|9;_YcVSLIy7Jt@?_JW9RM1N3W(!VLLWGqV;{& z4X~w*t4>#zz$kfPAn~Z=&K4>S^yRhC<_M!51P*1~AY#QJuwgQ0$7O#MdMgT`^*r zk@;dLK@X)L;A6u0^$(tZ!nsE-Qm5IG%5%w2`n;4dZ_t z`ep5~#Xpp!dPoaho!kIAREX^lrC+a_r3dHy65Hbvw74H;nJ$0rORtWc9PwVa@pcc~ zGA`tnEAt+BuVz@wHINU~EFDYQMETK5V@DIy7(PP0rsS-d#cAJ~uRC5Sk7ujZA+xHQnR;WO7?$tJ$b~gox_OyA{0LwAUU?&*+zy(vMoTrGG{Dyn zMee(?siO)*uf=RH68rI#@tW))Gc-eX(^A2cya)>$Bj*D)CT#i(5<E_C9&Dinhnc@Qe)&t<%4 z`DZwOcR3ZjhGXFtWtZ>akJ^Ez&XdGU`bK#A^~*aqkG(@aY~hjn@q^gE{u?Ku(PaCP zm_fi^>pomt)iz_lQuOWa9GfI~ABfcwR;IMuVjrJ@05Hx;Ii^^uVK z1(!I!_ug7C$5K-MQA-y;){-6anqGyl$kl+lJ|ao++R4Hy0Q-mhxcmkUGDc1}`%GH3|8Uw5fk}aWM@4 zq1`w>*a3QvpA;;5QvtNGcNX1>YfxWXNd)Wi&%rMCpgKGr?|@adZi4UA(@ z=DkM<9LjjGnash*|La?7wlVDF!KxZ+`Zk8};5@|hP@(ccoo8Szr}TMy-3GX}>tUeG z$_~J>X~+HC#d_#HhmlvqNyKq-O1o_#bMPdYIX!E(}%)tnK%6RDc^bTM&!hq529O$Olc?Xko%SJR*s$nhGp1KBLiB$~f$NZ!|S}EfE52ask zBijr2w?}zN)gLr_AxzFr3K0;+bzvK_IhaDa>)<4luPK{w3n)74d4H#8Eo7&8MH|em~9R0@_6}Tp#D~12sCqa^QqK@2%qe1TQyT zed{IM4ZfB?tr+zM=p!{Zlky+L@}cx=U9uj$A9DVZ?bAJbXt2$Wqt-9(Pqh#KzDUU8 z_~E?mzcPWMGJUe7%T2h}*yf9$Q7>?zKj2HHr-D|v4PNzLUaAF-uIBE1OTt^())BG}^s_)@~IsT?X!QIsS2t$V-zcC9nhU@Wkv`25BECcR- z-F){q)x!(*7F_0&tk80WnfN;C-)R=SYhNDp10^+CpsufkWvByKn$GRa=_( zG5YAhO#bn-Hos(^sUqLMt=7@A{t9hh-&NH9(FIuV|JHr~qznjV%g782>7bnHa8LDa zB8ReHWjc#9`0$2e`%5ltdN&Ev7tiPp5l}{GZ6EBbeUS%KMFg@G4%ETL$(!pDwGL21 z=jM`L&4+IFI(fa})lq^T$~d}*%)#Sdt zAorqt7+6-tr+lmjmW&+U9jPXb@|TBh;NHETz@dx>=CkzRI!271X=JMxfb_aMGI!|Z zk&vL;exZUYh<$pRrF}RB8n@mYJHD$6FtzLUTXC;J`(i>xZJ3GU%WDZ_`5+W3(FZQZpBRTB3c`sS8>90vh^wX_2XC&KUf zzU!@cnr0>X(x%S|X`j14YDwnc$>CUzf@B6xOz@Msm5m?!3`es^y(RiyHI#T=?UA** z4$|j`M}64b3V`(_$>QFWzunq0`C6Bkpoh}0t#AN2m9W)1EqYZDnf^$^M4}taE=s6j znl~*3&uz{CzIR9_sXx2mp&CB>;eBOLrmP{bKYlA3vsOFp{a?lp@O}Y~6S*xHd0!2~ z>kpQ~aVaAGh7dAr6|$Fxt`e*|*d^6?p#%C*bK6GxlmZUR3g(baLrCi;#-H2hj}i1x z`Zey)IA_m+>kslX7-#fEJ6C;J6)AxXhPdK4#ljoo)j1TPYe7(Suz3ZCHuFYpihN5CyHO+OX>m&bvqM;Cq=FyQ!}k84Y@@h#45}JCZ0^54?uKzO{@q!BC>I{b45WN5rslU#c!cWj>p*;sEww8}P7w4^#selo{`dzHwpbqsk44I0)eU!@ zHJem1YtCPLOn>wOsZT-N526aqRe=Dm& zMH4hGv$oU_D1=c*z4va_ZvjE)Bd&yXJ_l%^$H+sy{{<&}--p2Q;+cA?c~j@}5oJbX zK8{V;`9&`BD+zii{b079fk_*?(|uF(9m&Bk}qBh)3|&EgU&fmv{*b681$DA!c{nTKQQR(JdI&=vxRG9JvR*TqfQ~EW_bcQqU9Y?pj+n--C!+Db!f6#?l z29a2qbf|w<2^7xOWv(6T2AOIf?40DCz<1x)rbfYY$g`RcVk;Xx2=VQ299)Oa*26!n zTDUQ<0kISw5lU6<~2p^rL1nW(K8C?fokGp;iP6net z?AN}DE(iPKWKykpyP>d^ST;w$Bm>C$1N%A5BrS8C^x41AFU3r+-@)-pY}iL zu0Oc~`@ON_n6+O!d|HWkb4b1i=jvz&gvJ|SvD5|kbqpHl$H|C$4PwOcYf3$wl-W;h z`|S>Hzcb2*)Ti<`$BVEd(_fRY%5M8i9sexg7aMmg@OBySG#{gK&+3Bids~)iF{z?I zhZ$LRD`^qzMyUs1kA&mo*}6BNkw3wX^hC$jayAU-y!TLVyQTaVESEd7jESlQTrNDl zsrd?-{Ecf^%}t8HCZt zw}ET5f$z+W>cPr7_)uB57_x@?NE&WwM;O_hYL(8o%&BK7Ie)w#cl|CpM*UJ9IZu~P zU*I7>!)fHpwrcP%1dMf9@>=h^@KETw#i&~k*vl-$v)_Urc@(QlvxGHt!5s4WBV&0c zOO$uWBO6FHx2}|_VDoXV@?F}f!~BuJp^Q7T^~C(luOL;iBbudLU&6MJK-_K`5lLwe z>g%Dv?xL3yzTcteH_p#1Z??mOn9n)URuPn&W%RU^8W*eCe8$Ca{?U z^-A}inQz1_O`Ev89D1SO(!$GY3*Ny+ofj%zDW?J6fV`rZ)57RVIo_v>E)nO0|Hi@3 zzanCZh5HG1?6tA+$^%NO^KvZB)@%z)v?J)DtUt5$;N|lkYumM1rw7X3UA3sDO%yXW zxSzYprxmd3kA)vLeg|G(P+qjxy%~y>9kOIv%!t-db3c6?ago5GEFXMb8rgj0goM<& z?}>h<(g~YOWhTZ(q_Dk_x+`_u+u_hp=hjU%6>w2%-0{5bN^s|%?TE`2FGRKaz0Bf6 zUJK?h2i!4D;Z?=Ry0FotW104o63BcU>niD#lIr&a4rRQat%v2>MYHrU^%;(wNVNB1 z8CgW?-e(@k%RR7}q5RF|kk4Sr_}9Ceit|9>{>0pYp2G+i)3s+?>5>-A!F9+?-jl4! z(!gwLH#47Yo1YV?aPM%gP%?o-=?Am*;2fh8T{ZrPs@UO}aN2X@a>(r;=GL;RjlkoD z#YVr8TKLMDQMhKZ5w^(~kmx_Y2MiZukH#`hE|}x=Np$nK?wR#F*j~%Z&qe3O=H;wC z$$ee^8v~&q^0)FK>p`|1GjG4jEsS|Qxv*SPOcwDG^t+gt+751EJ#nwrR6_bEl@-lS z6@Zte9g*55hz`% ztHE!RShu9KO7KE(t@8PhHmG0Jn$BcHLZ51#Y}vtSMz9-YJiymml5ymOgs{fvvwU5> z9Aor$J)ti}g<*p`ZL5X4K+z{fHxY|!xOTYt!dNZ_m`-Y*3A!_eD0B>;s|%|na47Z6 zwce6O&_-j~RXI%et>A6lSSoA-Edy(PVFxIf8tFONSqBS$TzNM>jDfx|{Y#DL)Hv$ffmD@t#^&F%s3K z#oYv(U+5ONKS>AbO4~erUB?&95llJ#bM3{M^_G~+*(Yu`$ED}x+!|V%eS9;^oc=aj z&w4sf@1j*R>qjwRfsY$rsLNrSvnDJOOuoa@2iIM(lx>If$6BMM-5Owj$wn^(Oi<*nrNBOyb|k@YLzv;t&fE!e~Q87BOy zUc)#BX2MM9o<~7AEzrB-o8|97$pp=Mw37pFG!MWO_FVd$KfmG4pjHce^u*-*+CCZc(2)na<|+*VnL@y z&_h{&q{tjRXHp*x!x z`G-T_Z~HUsLKir;f~~2R^a=6zbE9gfU!C4^Z{Zv{*C3j^Mf_Oi z(biQP*%~2kKhnx|Tdb98;EQLuf|rCd02NC@@#w8iNKHC|CSPBHI!9iYr7&Vz=IH>^GoqfWabAK|s0#n(?rj zM2bSNNRT=0-1~9ly<_yijZoz|;{m>o3-{|CW?(4i*D|ch=c2hNDv#lI`|LU{7P~GL zJ~Yb_U-za8UiK|nEiByyf30>g&&(7;q4mJdJsxwn3oB;X4aR*ee-io&@GVNSQ*@KX zImnBKyR3dAsbI^yi%i!(G{T=>5sx#^+Cb#R8h-Wfi%@m#M=gwM#PK)Ec%Vk+;C91$ zzPn|2bmQfZ*giO7ms`t=@EtwS=vmol8l92G$ zW!}X5Whlof)Ms(N=#0hMg*VS|Tub&@E4%O_L-af%tc8`ppYv*s%a>*trQ?tk+SmYY zTvwBKl6{W|NCg#6U`~X1@HdXt2GL@3M=9imhKV;1U_qu&V=9i$!+D%3u&Uq9kHDd{ z+iX2co3thAf+UdO%6qi;yP0P=??O1&`6_fmvwedKZzYRC^8TaeH|(u|+hq?-&|3Wj zKK9DRysEDk%pspYrs~h5ctE8U$iy6_qmCBBhD)V0R=@3mRZ{%AIqOn^o}s09d3rYd z&SDYG(4dWGKRV>YO1qK3q3kDWOlLgve$HX9QRu)TWaI1pv^+5?jLbz$?W`oWrap(Q z2BA57el)_|Lt%2!zuEve8MrBXoeX;Xz+kF>#@u>V&k!g#rq-PlT7`z1aUuJkcVsJi z$|FK1baE_VJz)2~?=(Hy8DPh+sJAUu-++m`@dpNvK2VWNI+&f*NYFzW56E9kJ#~Wo zx^qxn?D!d=oN03l?i%O8z1e&-< zQFGTHa-f*G&&cQ5@n$nuL1dar!dBXGcs>-6L2mWm7{s`80sD~G=_1-*c*novZhY5M z*qChgtwT2*@oBVGuD3cz&_mh&;OlDedg_uN9N+$X0v5fzOfOCQXMITfj;s7fsB6I; zgF)H4k~a9j;k?U0emV4(R}KDIA&&~>DFzi*&wYKmnQShcL)L+$72V8zrKNzK{NmOu zB_e`!4hC>EFxG)8Z@TS@VHHqD)VcTGmNp<^(qMI0O$$*H+x6?`oeY8=%JSJl=HQ7# z!4HoMch!;idY3JGNB;Cj^nSg0DQHv;#MmE)nog7gj6^k@o!Jg|`;J)JHg7sf?QjpZK?n5Fo8o^f3}kIpJg}98O=VlEJ+Yy4~!N!nHm?{zvqR?XP0g; zet6jf>_`w$b-a9L!+05JzkB+4S?ez3Yw9hw96I8>+21(o&0kgkQ#Hg+ZLrJJqYjU! zn5re)wPpS7z=%^mZBV8Fez(*jO|2=0L+_7;cz2#fqNSB;c-(>rdMNF-onS92s`!MG z6>;Y#K;Rb|WzG1R^>3KtaVrkBB8?Pp=mPRHo9kA`*%#BQ+1_>KG@c|Qh&;peC?5C4I#X&5XARcB703JBUKapGe44p0u(G z6Lo~1rzi3A4ngE##^!>GVh4D?=VWGZZ8luBJIq+Tbq#7yS6^!)L*!8E z!JoB&_jg3GUGWo~%*ajVoyL6Wo#gOG!io;ux-9O|1*rQaR(t<`4z088uBR`qhW3V+ zj_2AcqsO@)(+OnFeSa3du4Z=qK}T05or$8wz6+^XoYLh(B0YI5COkT!q6gnb8NmSH zr!?&K!?F(is7?Ujw;IZ%F6`gePwXF1)}LKuOW}HSccwk<-mZX6?xaqgf0WOw58=fjTmpnQ*}VOU)$R7qY_Y02CM)XMmR#9uE* z4=LK`K4Uyc@Hfi-&VI5GynIyBR1aRgGV>e<fqwh7swDuI<@B!aJj9F*lM@ zQ=0;l9l!2>eYjz2o$t1|rp9()aD|2EOREC9$+MwbLXx-+l2VW5EY8Mw*Uwf5XRb?; z(Xa{o?&Ts#+@rTU9Axq!ug>^E+KU}v#3kSA?RYJ`u|ml6eY+f56!(nxH6*U9q3n-Z zlR3EErazJp!J&%4of#U~joQPxX`~VG{o7{wqOMkOZkWTFzn}r$f&LMzlM8{u?=ua$ z(URx^AKl9DSLR+fNX#D>M?#w71<>{P#E={NyN~5dC}3Q5%fQa5b(RGgTtZJ+Q^9LJ zIiLG>t-M2({TC;1TnK@;PjSNJgw zZXsz7$5xP4E4<|)TReEWi2}O2iNV~nNotrazodel!SfKNvU4AL* zSnSiQzrqTJ;DnX^ryPblsDIP=(w0}9(5n6nL)aK{m;AFr=Ux>N{F*Wz zFp*7#$Ag0pU*xAN%+wp~mEU{52U2RtyXqTPFXj}&;zgP^p)8%SHD`rNS7Rkmu^!u- zWT}asJoc#OQ}^8UboTt*Mz4yXl)dWMcMYbWs15@{)`zhCT4is_)(%gd7VDBUD}c`@ zTOS^LQ4jR$wzN~JUPYWr&MM{cKPKp*^y?*LJ$Q0y=ko2(J!Y=wkqSlb*XD|f<7M0m2Y^ufusbT)c4B;@NlZilQZk}k}A6xK^?#y;p+L!39G)H8d&BtyULoM|H$!uc&=TYk6{HjO7?R7zr-PM)j*V||(@JI^)4 z^KAy=GSUsOs{FG6^>tZPho{{3qcib7e#(9oFIfjMAtArstyp=aOahB(*xh2-p^kV` zTUmcwRSz;V*4yt)wFU-b3>;sFJAnKKPsX$=CY0mk{sYNe#C{^B-S~(ADof$;)f4ty z$ne!FRJUh8N7h~Da_3morHG`FX7K`m|T49+{?{Jp;L)iDY z$XVz;f<7`i)SGv9C&8~N^(>pk@wZX7eELoe!Fl(ZDx5Yb%Obh%@7k8p6oF2G$LaJQ zZ9qQe#HUxL-S_(pSG%7C?B5Ry+4cDq=V< zDE~{$`tnyWoa@&y+n4z;&kPd-aXmopNlnrgG#2Rz7NPGL3nOqS>*@08jArH+L?q)g zPMeD&=pMs;$JG>OI5Ac>;-BPepv}>+N^$d2a4+;n`L~#M$TMU-#Ncube6OW`bSs8| z5U>BnDG@hnB>?pJ`Po;sqDoJ}y1N@c$_S2J9%?tdfX z6&UR3{6R__+ z`#j}D9SEZmVl8sHh1@$L{XsydnqW7|@)4Up@tNy-?rkPPk}rp5`k~GBK?N)B@*;!A z!@GHw<-^K|wOOZb^*}#kA={g#7%(WY5$DcbiKe<*KYH_kIA21k2OlTL{ou~p+{vXk z3THUazkE(zdxZzdVw1f8Zm=2Vx-K5`tjPxaw*^zeSzF-m=1`4Pp(f)(%8|mJcWDp{%Ep)LEW)12nLRdtXN2l~VZi zN<>-0a0`^zlcT=c)c_5wn~rGqFG2%{Z>`y}f9`QHxmle0&PpcQY6V;ew%11zJv?Pc zqhkT2(i2@^r1|0YWie$S+=am|Wlb)uE4ew=y2}b_TVL=f`qB-89?EzyoAdCZXK_Ff zH&(E^!+Y@8jNi@6ITM>66W|d^;82#&Y|fe$tvlbbx5Iu5)GU$BMD)zGE%shT@Teu=uNp# z^I`LGez7zq#hdpK^ialw*?Mq}%x@0`e=eOGVOmPiuN!WE zUM@~(PaURD`rqgC&DKNPA2k%&xB|g-?J8T}^V>rOagNIW{`6ZuWY+ZP6OZkI2X;2{ zS)@fkJ-)pL5w!0?h+=fox#Wd&@chLBxAf9h^J8Vc%TuZBmt$64-WO0*kX-&n1iLT4_HLwk zCmgvL)!kE60lybsNs_r*2!pi8Lxn9vK>YZCx6&!K1#|RuHlW(?xiGRWjPLqnJkx~A zyqt8I&RrP=YY3db)l-|zdy@A0ES=%pju{Ucs#d|gujzd+IoJp@zVxuimX-s}nq}%1 zoz39+!%e2`#xH=N&d#tazZTBHb!h52d`YO2#4vs<_NLe_FdygJNXM1rA%viZvfZBT zZ#YM7z5LB`Iw|Z5ht5#(`7R)Ms%B_!SsO6@DR6`@^gXE3p<1@3rxCI-L_DlAhyiu{ zuX&?Y&;@g1?0@?AEbWEn4>h0n_^z0-U+0#9rJEAUK+Gqe<>S_M@Y0jgqtrLr;7b0R zAo%$k5dDZ59oL(CUG!`{l|P|#_|$z$`vZmD4h8<00N@LFPlKaltcW8FaNw&E_3lRk293H2LrLjV+nB`ln?W|g*$8J!Hm zwbxnn5UPUU`sU*#`6Pu`weKM4`CIvn)>t<_Es;hf(uGr5t^RPLmT6{XHKc*KJ6+h> zSNTwNgGhMXh90Oc5bwt-{0>Ahv1<1GUN}ca`j}Q?ts26ZXY|@$k`vd1&Bu`wzGc>2 zu#=$YZ|ycJ-lA}}Qw`A!e3C4u@Q35Py8VOBq6Tp1)dik>_eQ8LEw&eJt_QwCZ*A9$ zJ^)wrGq!jBSU3kS568XN?L_y{AU6bLxK(=9u=zOmJ?P&hX&Mmp{LSA&{1b!Ojc1a44sTv$ZQW}zR!eAq6IAEtB%dm*-Z{Mwz|55RNF&fL1; zR1p21S@L?!O7wakRqiUzL;{C0UMr9}#Ds(-^Di=@ZdSk&?*1AmJMbIUGps!~tlA4E zyNz16ozH~brxuwxf)DWC8ac=NnZJRuW}nctRb2!QrQHICW!(mqR-mV#e}K@Qw` zufUOOwtB7obB|NZ=D65l>=l7>NJvzW_L`;4IB#B#%+Z^q-On@#eoYy7W^;01aI2ns zD~CiyX^xhS%-9dv7Ex=hIkj%kxBmFgqbusc)c}hMbTM znA7G{6?Q~yW}XS*?n?go{y7&mABVfYsr;MmdIE>C{>;|H$?}lD@y~Nqk*^O}yOq5- zv7Ge0>nT5~!Cp@O;ojCp$oSc@R*I(yUVM2Y?q*65sNOL8s6T4qoUb~HmlmB+L!z`b z4lI)SGk!TAr$8m1qs)2(K@Vjdovr6!L&vS?Gir!s^y^)n8~@BZmsH;p4Q^?McT`R; z?XQAxkh6T3?05?(z2U0q;_44JGzy>Jb86uns?F`|NnPU*udh*$Pdhvd9svrxQlGE4 zUj`nu>V11A{FH8{{&2fTBi(XF@SLVv#;%x_vkysoD#pB2*?&b$vXi8L-sQo-0A z?=+kCvteW&<`&5;uJx%E=1BJ3<%_R|H2YlIPIT8mlTn)VwmMs6eZdM*DNEwGC8gbz zX9&|9s}-{LoV=}ueQV`&kh#w{!&!AcDCz-E9ZcSvV&UxC0i|E|g(@iL0iEU(TTV&o zA$B&0^HoFVz7C$v!OuTeGnw`Fb_vY=$F3VW>FU^gobDB`%ry4DCB#w6cs&<~9aL~T z-IBxvrH7=JZBfHGZo9RQhO~jT?^JSZSENAFA$`mB%bI}M=e^A@TcnZklBRc$9OiBp z=HMVsNr9S|%#4T3%hBu8I5BXTl~7M9zlx@UjZ!yXC9=6R!E2F7^p zUS{+bLg^dF1Vm~(;Q;jXq2;Pa3}j-Tv7RN)|4{n13fZsm`AwI?jIrNqK0w8*{l5dN zhRNqg!r<3H`d;pK5K&Zd&ERzvcx7l^dE-JQFxay?>&~Uq$aY%&8#hIc6YTak4mCsX z{#UB)c>YNCP+{JQU&}H5s9(W(R&nqgf2#-ka5qp$ds3LuT@3Wi4R#!f+ku)l7{>1W zLVTVOWjs(N>%o%|^U|0@r~lk9Q)Ztln2d5`6VIsc&=xmAqbHv`+19l{b@~)_c&!ln z>Fj3gs4ziUNPPy;eZ=`~%Ij@>9VE{Axo(BF?Sq+dM6A7SRo6)=KFqF}{^gTC@K#C6e>@nAM5TQOrL{0aF8HxY|NB>~TBxTvg>!Hnm!2J?_GD7Uh8%MaSC6w| z^Km-tS4qBHe3jtWly;k~2j>Wi`?R{Js9>u;2Wy(I;=tVFp5Bjq+yi(Q+vl8-%>i%r zpYpvIkp}%8lCO5XU5?a?6!fI*BR(%}0UU(+9)obyV*%_!y(1E=ATuw=Zm+hKe`GsB z4`qCttw&DWM*{g?I#W+$ZrwiJxoZ?^u?$5kM!p4?mmfSN=G_U9{O~P4Tyq$grZhr*9|Wx*-C6Rwpv;nU`-;xTftJt_MS6!y5K}&6z{1~y7+%wKmic^g!JPFr z`K6NuGzgi8?R?Wy+2A+;#>{R~-Eqd?W+twW!%4j`Wc|0!txl)l=j#U?-z2O=FGmgx zUa)c}a479Y{$ggH7T4p|ywt=PGw$Fs!iXpc~15IIj z9pv%8EIM&~6KaF-m{YqF+XYIyq0<@9dM6=pl5Ommxj5#~x|`B1l~gS$z=3+K>k_C-8(oVm_IKHd`2)ks!E0*|vV zy%-w{C7Lcy1&1D>G`9KFCoCWOX^VT&Z{3O-=R3&q?kXVIjnc0-%u+HEaWeSl%Y~hIB4XtcQV(k7Dzs&{R6)a+Z<(B5v_S2ZwsKn)`Ou8V=W1(XiO&V4 zET4^J4t^c{o$*fLYH4MhgUCMj(=+l`L*6=kh?DqM2U>Zv%$?8H13F2qF3$R?^(a-N zDxRG?QPJ+U4@Kc~*V8Rz&aCkRr08+?{?l5V$Tgb%o!L7CF#GS3U6~nGK*1{Pjj(tT z7+GnV`V4kJJ&mQK4|njR$*j!`H>-&4Hl-hIC3A=g3GtXn^R+*~hGZ|R9g6T1#B}l> z7ktkxhvUyix-(a_!Ll2!I-ck2fZeG@OV%=Rqcq(iIYk$b5X$FooCjG>_jrs&aJylr z&rfb+?;!&>dQ=nnXttMEB0o@2qB>AxSU zf#jUJOO>@x7UN-p8|`Gcu$pC6W~K}+fH~BEy=i+EG(VO`@NoLH311}#$#=fkJ6qgQsGoxPbwvB`Oi$C#@ zYLn90Au$|?EN5@GD{-eJ*$W`&T? z+09i)y9@0981`{xSqV(u@9?fZX)RjJ#a7TRM(iI@#_QSZ);E87qpce~GhdI?E%tB8 z*jxy`Y^&vY&ecFE>94+g_EYl>icfB*nNR&4n^4`cV|(flxvlixZ<|z?dqs=8*I$DeERcucMsf)m9G|jf))%9;^dzbMCdOQyZd6 zrfd7e5n}>}vV085b|WSvWRI$Csg61?c2M;log0rLrs|is-ZP;G=&mDeH$NN?dS3fq zC9Tc}`3819oUvaKBk77C*FA~rP$)S@WB}g&oR{$Lyb~{lP46ZlkxvhO%Z*V&2DcpD zdIMtcWz*1>7d`dhQo@r=9Yos@D*0<=}VROt_hhl{a^_!|jFqqDy?#7Xg;68*b9D5W>RFiJPC-=muiF$$GqESzvGG zNF43c67cB(Bek!sD%w;SyK-E555aDfddz0&2`?4f{8VwK{$Mm&B*_RHCT#eU!b94r z`(AH4D{o>lY=_nv?p{a3K0)4aQ$(?299g{osB`cVX99<^{n5@|L7F0v&C(GH}#J>wiSw07*Gyc=R-cWsC zq>l@kSoStqa3w8*bNBr^W&8VS8Bne1r#nsG4g^2G=Z-h81z(B=1y9LRqqbW#BafdT z-j6})Zx&<@o(R@8^H8H42$S>)JyCTQ4Cf4cG|@Lsw1AB-&ndS1e+7%$Y{U88nn9hd zfep8*94h#PHTd^(;(cJ094j0^P6ZFI8}Kp81m>qy<;R3aAT zlmLN((bJyWy8!3+dQVHC4d~S%2cwHui1(vYa;#@@@O-g1Mz>>bOx+KLO7JbYzYGcg zMKXPr(+t)>2uhO`Yz2d3;T(5bjK)VK2Z zR#vQiGh3?1CQ)oRW?bgIya&j-|626^K`}h-T*gx>atSQ4&+rqE%|VRCk7lp0nR~rI zF%D2sSzU_o5!R-{$ht7S*r4Esb#ln)VTYV_(|jOjp~PFfvkTg_g%o{TT?Sgr6l1w1 z)zD?@gp-3*b`kuVvYryJOaJIOBb*#%{9+mMl*=_O%!C;`8gkl;?Q%D;GkM41{;m|r zGhN*AC^89Z$nMK~$BUw3^{e`i`x_BBly;*dXHB#p2?5V(?Y@a$8k;Pah#KkxizL6ah$-cXPx-dC^?&c2UaV|N)fuoyww zK9_gP5h;VrnzKrcATXLuuBo5}=9Kqvj)GF4SjXFqo!3D_tL457UZ1-?U4)y8kdm;_ zW$GfTg35^Lt6_Pn1SzCVQhA^A?pFA|%12Jw>}%82vhj*mYqjLKEjMg~DB=ix-l_~4# zY&|bd4e(uCuZE>gT0fNY`g5H+T)r`I8&d|ndn9xsNO0=;6nUqvFi)-X=5wmNJ{Vzu zEPXC}?MvX??dfdJv>8cQ9NUGnA6!Kc?jxtN?>b8$^KlxKqE_)@#QQ$}W;dL(TC%lJ z;Q}|JA2xV-vGvq=$cbKtPR%wDn)1d@!@35rKYp=TI;jdqu6n3n@-ZBIVc(*=?$OYK z%STh^L!#EbnfX7&GF2q%>mq(^J`Qtcc!h1Y7`fjveLoXrziPJK@;Q_kkGFCnWL-#Y zu0?_6!)4g72kmA4J+0vMGuh*8TN~gPnD(sdaWQ<6FI#uT_9eKJQgSOCoqIjsY!043 zL#OSrlR=`$YBBXs{p||#a_+8*AJ=SFAn2it2Xk?1f(=4!K8hiGw7$#N?^nbUyxu%) sZs~;E>4$9G{?DB6+bavLtNFyZaDO;=MA^wf9}JJS>k@4b09Q@iRR910 literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-d08/inputs_true.dat b/tests/regression_tests/surface_source_write/case-d08/inputs_true.dat new file mode 100644 index 00000000000..561a3833736 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-d08/inputs_true.dat @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -4 -4 -20 4 4 20 + + + + 101 102 103 104 105 106 + 300 + 8 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-d08/results_true.dat b/tests/regression_tests/surface_source_write/case-d08/results_true.dat new file mode 100644 index 00000000000..63ea1a64d41 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-d08/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +9.756086E-01 4.639209E-02 diff --git a/tests/regression_tests/surface_source_write/case-d08/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-d08/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..2525592045ca7912138b0559d72d9d4eaeda6118 GIT binary patch literal 33344 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 literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-e01/inputs_true.dat b/tests/regression_tests/surface_source_write/case-e01/inputs_true.dat new file mode 100644 index 00000000000..99a5a289929 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-e01/inputs_true.dat @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 4 5 6 7 8 9 + 300 + 2 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-e01/results_true.dat b/tests/regression_tests/surface_source_write/case-e01/results_true.dat new file mode 100644 index 00000000000..8979eb55477 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-e01/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.169123E-02 9.144814E-03 diff --git a/tests/regression_tests/surface_source_write/case-e01/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-e01/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..3047519e8c52b140a4756266192c9d81c7d204fb GIT binary patch literal 33344 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 300 + 3 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-e02/results_true.dat b/tests/regression_tests/surface_source_write/case-e02/results_true.dat new file mode 100644 index 00000000000..8979eb55477 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-e02/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +5.169123E-02 9.144814E-03 diff --git a/tests/regression_tests/surface_source_write/case-e02/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-e02/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..80615285332359f6d38365cce0cffbbfba1ba116 GIT binary patch literal 33344 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 literal 0 HcmV?d00001 diff --git a/tests/regression_tests/surface_source_write/case-e03/inputs_true.dat b/tests/regression_tests/surface_source_write/case-e03/inputs_true.dat new file mode 100644 index 00000000000..a3421298b94 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-e03/inputs_true.dat @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + eigenvalue + 100 + 5 + 1 + + + -2.0 -2.0 -2.0 2.0 2.0 2.0 + + + + 4 5 6 7 8 9 + 300 + 3 + + 1 + + diff --git a/tests/regression_tests/surface_source_write/case-e03/results_true.dat b/tests/regression_tests/surface_source_write/case-e03/results_true.dat new file mode 100644 index 00000000000..d793a7e4216 --- /dev/null +++ b/tests/regression_tests/surface_source_write/case-e03/results_true.dat @@ -0,0 +1,2 @@ +k-combined: +4.752377E-02 6.773395E-03 diff --git a/tests/regression_tests/surface_source_write/case-e03/surface_source_true.h5 b/tests/regression_tests/surface_source_write/case-e03/surface_source_true.h5 new file mode 100644 index 0000000000000000000000000000000000000000..becc9254a3f1c326d78c58a23e0be68087b3b5d2 GIT binary patch literal 33344 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