diff --git a/docs/source/io_formats/settings.rst b/docs/source/io_formats/settings.rst index 52b52f68c..09d7db123 100644 --- a/docs/source/io_formats/settings.rst +++ b/docs/source/io_formats/settings.rst @@ -462,9 +462,23 @@ attributes/sub-elements: :library: If this attribute is given, it indicates that the source is to be instantiated from an externally compiled source function. This source can be - as complex as is required to define the source for your problem. The only - requirement is that there is a function called ``sample_source()``. More - documentation on how to build sources can be found in :ref:`custom_source`. + as complex as is required to define the source for your problem. The library + has a few basic requirements: + + * It must contain a class that inherits from ``openmc::CustomSource``; + * The class must implement a function called ``sample()``; + * There must be an ``openmc_create_source()`` function that creates the source + as a unique pointer. This function can be used to pass parameters through to + the source from the XML, if needed. + + More documentation on how to build sources can be found in :ref:`custom_source`. + + *Default*: None + + :parameters: + If this attribute is given, it provides the parameters to pass through to the + class generated using the ``library`` parameter . More documentation on how to + build parametrized sources can be found in :ref:`parameterized_custom_source`. *Default*: None diff --git a/docs/source/usersguide/settings.rst b/docs/source/usersguide/settings.rst index a879fe6d1..340d4af00 100644 --- a/docs/source/usersguide/settings.rst +++ b/docs/source/usersguide/settings.rst @@ -182,42 +182,56 @@ Custom Sources It is often the case that one may wish to simulate a complex source distribution that is not possible to represent with the classes described above. For these -situations, it is possible to define a complex source with an externally defined -source function that is loaded at runtime. A simple example source is shown +situations, it is possible to define a complex source class containing an externally +defined source function that is loaded at runtime. A simple example source is shown below. .. code-block:: c++ - #include "openmc/random_lcg.h" - #include "openmc/source.h" - #include "openmc/particle.h" + #include // for unique_ptr - // you must have external C linkage here - extern "C" openmc::Particle::Bank sample_source(uint64_t* seed) { - openmc::Particle::Bank particle; - // weight - particle.particle = openmc::Particle::Type::neutron; - particle.wgt = 1.0; - // position - double angle = 2.0 * M_PI * openmc::prn(seed); - double radius = 3.0; - particle.r.x = radius * std::cos(angle); - particle.r.y = radius * std::sin(angle); - particle.r.z = 0.0; - // angle - particle.u = {1.0, 0.0, 0.0}; - particle.E = 14.08e6; - particle.delayed_group = 0; - return particle; + #include "openmc/random_lcg.h" + #include "openmc/source.h" + #include "openmc/particle.h" + + class Source : public openmc::CustomSource + { + openmc::Particle::Bank sample(uint64_t* seed) + { + openmc::Particle::Bank particle; + // weight + particle.particle = openmc::Particle::Type::neutron; + particle.wgt = 1.0; + // position + double angle = 2.0 * M_PI * openmc::prn(seed); + double radius = 3.0; + particle.r.x = radius * std::cos(angle); + particle.r.y = radius * std::sin(angle); + particle.r.z = 0.0; + // angle + particle.u = {1.0, 0.0, 0.0}; + particle.E = 14.08e6; + particle.delayed_group = 0; + return particle; + } + }; + + extern "C" std::unique_ptr openmc_create_source(std::string parameters) + { + return std::make_unique(); } The above source creates monodirectional 14.08 MeV neutrons that are distributed in a ring with a 3 cm radius. This routine is not particularly complex, but should serve as an example upon which to build more complicated sources. - .. note:: The function signature must be declared ``extern "C"``. + .. note:: The source class must inherit from ``openmc::CustomSource`` and + implement a ``sample()`` function. - .. note:: You should only use the openmc::prn() random number generator + .. note:: The ``openmc_create_source()`` function signature must be declared + ``extern "C"``. + + .. note:: You should only use the ``openmc::prn()`` random number generator. In order to build your external source, you will need to link it against the OpenMC shared library. This can be done by writing a CMakeLists.txt file: @@ -235,6 +249,58 @@ file in your build directory. Setting the :attr:`openmc.Source.library` attribute to the path of this shared library will indicate that it should be used for sampling source particles at runtime. +.. _parameterized_custom_source: + +Custom Parameterized Sources +---------------------------- + +Some custom sources may have values (parameters) that can be changed between +runs. This is supported by using the ``openmc_create_source()`` function to +pass parameters defined in the :attr:`openmc.Source.parameters` attribute to +the source class when it is created: + +.. code-block:: c++ + + #include // for unique_ptr + + #include "openmc/source.h" + #include "openmc/particle.h" + + class Source : public openmc::CustomSource { + public: + Source(double energy) : energy_{energy} { } + + // Samples from an instance of this class. + openmc::Particle::Bank sample(uint64_t* seed) + { + openmc::Particle::Bank particle; + // weight + particle.particle = openmc::Particle::Type::neutron; + particle.wgt = 1.0; + // position + particle.r.x = 0.0; + particle.r.y = 0.0; + particle.r.z = 0.0; + // angle + particle.u = {1.0, 0.0, 0.0}; + particle.E = this->energy_; + particle.delayed_group = 0; + + return particle; + } + + private: + double energy_; + }; + + extern "C" std::unique_ptr openmc_create_source(std::string parameter) { + double energy = std::stod(parameter); + return std::make_unique(energy); + } + +As with the basic custom source functionality, the custom source library +location must be provided in the :attr:`openmc.Source.library` attribute. + --------------- Shannon Entropy --------------- diff --git a/examples/custom_source/source_ring.cpp b/examples/custom_source/source_ring.cpp index d68122dd7..bc8a8a7f8 100644 --- a/examples/custom_source/source_ring.cpp +++ b/examples/custom_source/source_ring.cpp @@ -1,26 +1,36 @@ #include // for M_PI +#include // for unique_ptr #include "openmc/random_lcg.h" #include "openmc/source.h" #include "openmc/particle.h" -// you must have external C linkage here otherwise -// dlopen will not find the file -extern "C" openmc::Particle::Bank sample_source(uint64_t* seed) +class Source : public openmc::CustomSource { - openmc::Particle::Bank particle; - // wgt - particle.particle = openmc::Particle::Type::neutron; - particle.wgt = 1.0; - // position - double angle = 2. * M_PI * openmc::prn(seed); - double radius = 3.0; - particle.r.x = radius * std::cos(angle); - particle.r.y = radius * std::sin(angle); - particle.r.z = 0.0; - // angle - particle.u = {1.0, 0.0, 0.0}; - particle.E = 14.08e6; - particle.delayed_group = 0; - return particle; + openmc::Particle::Bank sample(uint64_t* seed) + { + openmc::Particle::Bank particle; + // wgt + particle.particle = openmc::Particle::Type::neutron; + particle.wgt = 1.0; + // position + double angle = 2.0 * M_PI * openmc::prn(seed); + double radius = 3.0; + particle.r.x = radius * std::cos(angle); + particle.r.y = radius * std::sin(angle); + particle.r.z = 0.0; + // angle + particle.u = {1.0, 0.0, 0.0}; + particle.E = 14.08e6; + particle.delayed_group = 0; + return particle; + } +}; + +// A function to create a unique pointer to an instance of this class when generated +// via a plugin call using dlopen/dlsym. +// You must have external C linkage here otherwise dlopen will not find the file +extern "C" std::unique_ptr openmc_create_source(std::string parameters) +{ + return std::make_unique(); } diff --git a/examples/parameterized_custom_source/CMakeLists.txt b/examples/parameterized_custom_source/CMakeLists.txt new file mode 100644 index 000000000..3024e90cf --- /dev/null +++ b/examples/parameterized_custom_source/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.3 FATAL_ERROR) +project(openmc_sources CXX) +add_library(parameterized_source SHARED parameterized_source_ring.cpp) +find_package(OpenMC REQUIRED) +if (OpenMC_FOUND) + message(STATUS "Found OpenMC: ${OpenMC_DIR}") +endif() +target_link_libraries(parameterized_source OpenMC::libopenmc) diff --git a/examples/parameterized_custom_source/README.md b/examples/parameterized_custom_source/README.md new file mode 100644 index 000000000..9116fadea --- /dev/null +++ b/examples/parameterized_custom_source/README.md @@ -0,0 +1,23 @@ +# Building a Parameterized Custom Source + +To run this example, you first need to compile the custom source library, which +requires headers from OpenMC. A CMakeLists.txt file has been set up for you that +will search for OpenMC and build the custom library. To build the source +library, you can run: + + mkdir build && cd build + OPENMC_ROOT= cmake .. + make + +After this, you can build the model by running `python build_xml.py`. In the XML +files that are created, you should see a reference to build/libparameterized_source.so, +the custom source library that was built by CMake, and values in the parameters +attribute. The model is also set up with a mesh tally of the flux, so once you run +`openmc`, you will get a statepoint file with the tally results in it. Running +`python show_flux.py` will pull in the results from the statepoint file and display +them. If all worked well, you should see a ring "imprint" as well as a higher flux to +the right side (since the custom source has all particles moving in the positive x +direction). + +Once built, you can edit the parameters attribute on the source to change the radius of +the sampled ring or the energy of the sampled particles. diff --git a/examples/parameterized_custom_source/build_xml.py b/examples/parameterized_custom_source/build_xml.py new file mode 100644 index 000000000..5edb204df --- /dev/null +++ b/examples/parameterized_custom_source/build_xml.py @@ -0,0 +1,37 @@ +import openmc + +# Create a single material +iron = openmc.Material() +iron.set_density('g/cm3', 5.0) +iron.add_element('Fe', 1.0) +mats = openmc.Materials([iron]) +mats.export_to_xml() + +# Create a 5 cm x 5 cm box filled with iron +box = openmc.model.rectangular_prism(10.0, 10.0, boundary_type='vacuum') +cell = openmc.Cell(fill=iron, region=box) +geometry = openmc.Geometry([cell]) +geometry.export_to_xml() + +# Tell OpenMC we're going to use our custom source +settings = openmc.Settings() +settings.run_mode = 'fixed source' +settings.batches = 10 +settings.particles = 1000 +source = openmc.Source() +source.library = 'build/libparameterized_source.so' +source.parameters = 'radius=3.0, energy=14.08e6' +settings.source = source +settings.export_to_xml() + +# Finally, define a mesh tally so that we can see the resulting flux +mesh = openmc.RegularMesh() +mesh.lower_left = (-5.0, -5.0) +mesh.upper_right = (5.0, 5.0) +mesh.dimension = (50, 50) + +tally = openmc.Tally() +tally.filters = [openmc.MeshFilter(mesh)] +tally.scores = ['flux'] +tallies = openmc.Tallies([tally]) +tallies.export_to_xml() diff --git a/examples/parameterized_custom_source/parameterized_source_ring.cpp b/examples/parameterized_custom_source/parameterized_source_ring.cpp new file mode 100644 index 000000000..c88333e15 --- /dev/null +++ b/examples/parameterized_custom_source/parameterized_source_ring.cpp @@ -0,0 +1,66 @@ +#include // for M_PI +#include // for unique_ptr +#include + +#include "openmc/random_lcg.h" +#include "openmc/source.h" +#include "openmc/particle.h" + +class Source : public openmc::CustomSource +{ + public: + Source(double radius, double energy) : radius_(radius), energy_(energy) { } + + // Defines a function that can create a unique pointer to a new instance of this class + // by extracting the parameters from the provided string. + static std::unique_ptr from_string(std::string parameters) + { + std::unordered_map parameter_mapping; + + std::stringstream ss(parameters); + std::string parameter; + while (std::getline(ss, parameter, ',')) { + parameter.erase(0, parameter.find_first_not_of(' ')); + std::string key = parameter.substr(0, parameter.find_first_of('=')); + std::string value = parameter.substr(parameter.find_first_of('=') + 1, parameter.length()); + parameter_mapping[key] = value; + } + + double radius = std::stod(parameter_mapping["radius"]); + double energy = std::stod(parameter_mapping["energy"]); + return std::make_unique(radius, energy); + } + + // Samples from an instance of this class. + openmc::Particle::Bank sample(uint64_t* seed) + { + openmc::Particle::Bank particle; + // wgt + particle.particle = openmc::Particle::Type::neutron; + particle.wgt = 1.0; + // position + double angle = 2.0 * M_PI * openmc::prn(seed); + double radius = this->radius_; + particle.r.x = radius * std::cos(angle); + particle.r.y = radius * std::sin(angle); + particle.r.z = 0.0; + // angle + particle.u = {1.0, 0.0, 0.0}; + particle.E = this->energy_; + particle.delayed_group = 0; + + return particle; + } + + private: + double radius_; + double energy_; +}; + +// A function to create a unique pointer to an instance of this class when generated +// via a plugin call using dlopen/dlsym. +// You must have external C linkage here otherwise dlopen will not find the file +extern "C" std::unique_ptr openmc_create_source(std::string parameters) +{ + return Source::from_string(parameters); +} diff --git a/examples/parameterized_custom_source/show_flux.py b/examples/parameterized_custom_source/show_flux.py new file mode 100644 index 000000000..6f5494301 --- /dev/null +++ b/examples/parameterized_custom_source/show_flux.py @@ -0,0 +1,14 @@ +import matplotlib.pyplot as plt +import openmc + +# Get the flux from the statepoint +with openmc.StatePoint('statepoint.10.h5') as sp: + flux = sp.tallies[1].mean + flux.shape = (50, 50) + +# Plot the flux +fig, ax = plt.subplots() +ax.imshow(flux, origin='lower', extent=(-5.0, 5.0, -5.0, 5.0)) +ax.set_xlabel('x [cm]') +ax.set_ylabel('y [cm]') +plt.show() diff --git a/include/openmc/source.h b/include/openmc/source.h index 78e4043b9..529cb1836 100644 --- a/include/openmc/source.h +++ b/include/openmc/source.h @@ -59,6 +59,15 @@ private: UPtrDist energy_; //!< Energy distribution }; +class CustomSource { + public: + virtual ~CustomSource() {} + + virtual Particle::Bank sample(uint64_t* seed) = 0; +}; + +typedef std::unique_ptr create_custom_source_t(std::string parameters); + //============================================================================== // Functions //============================================================================== diff --git a/openmc/source.py b/openmc/source.py index cc749c324..22952b4a5 100644 --- a/openmc/source.py +++ b/openmc/source.py @@ -22,6 +22,8 @@ class Source: Source file from which sites should be sampled library : str Path to a custom source library + parameters : str + Parameters to be provided to the custom source library .. versionadded:: 0.12 strength : float @@ -41,6 +43,8 @@ class Source: Source file from which sites should be sampled library : str or None Path to a custom source library + parameters : str + Parameters to be provided to the custom source library strength : float Strength of the source particle : {'neutron', 'photon'} @@ -49,12 +53,13 @@ class Source: """ def __init__(self, space=None, angle=None, energy=None, filename=None, - library=None, strength=1.0, particle='neutron'): + library=None, parameters=None, strength=1.0, particle='neutron'): self._space = None self._angle = None self._energy = None self._file = None self._library = None + self._parameters = None if space is not None: self.space = space @@ -66,6 +71,8 @@ class Source: self.file = filename if library is not None: self.library = library + if parameters is not None: + self.parameters = parameters self.strength = strength self.particle = particle @@ -77,6 +84,10 @@ class Source: def library(self): return self._library + @property + def parameters(self): + return self._parameters + @property def space(self): return self._space @@ -107,6 +118,11 @@ class Source: cv.check_type('library', library_name, str) self._library = library_name + @parameters.setter + def parameters(self, parameters_path): + cv.check_type('parameters', parameters_path, str) + self._parameters = parameters_path + @space.setter def space(self, space): cv.check_type('spatial distribution', space, Spatial) @@ -150,6 +166,8 @@ class Source: element.set("file", self.file) if self.library is not None: element.set("library", self.library) + if self.parameters is not None: + element.set("parameters", self.parameters) if self.space is not None: element.append(self.space.to_xml_element()) if self.angle is not None: @@ -191,6 +209,10 @@ class Source: if library is not None: source.library = library + parameters = get_text(elem, 'parameters') + if parameters is not None: + source.parameters = parameters + space = elem.find('space') if space is not None: source.space = Spatial.from_xml_element(space) diff --git a/src/source.cpp b/src/source.cpp index e2cdace01..4f4ad117b 100644 --- a/src/source.cpp +++ b/src/source.cpp @@ -5,6 +5,7 @@ #endif #include // for move +#include // for unique_ptr #ifdef HAS_DYNAMIC_LINKING #include // for dlopen, dlsym, dlclose, dlerror @@ -44,9 +45,9 @@ std::vector external_sources; namespace { -using sample_t = Particle::Bank (*)(uint64_t* seed); -sample_t custom_source_function; void* custom_source_library; +std::string custom_source_parameters; +std::unique_ptr custom_source; } @@ -94,6 +95,10 @@ SourceDistribution::SourceDistribution(pugi::xml_node node) fatal_error(fmt::format("Source library '{}' does not exist.", settings::path_source_library)); } + + if (check_for_node(node, "parameters")) { + custom_source_parameters = get_node_value(node, "parameters", false, true); + } } else { // Spatial distribution for external source @@ -366,17 +371,21 @@ void load_custom_source_library() // reset errors dlerror(); - // get the function from the library - using sample_t = Particle::Bank (*)(uint64_t* seed); - custom_source_function = reinterpret_cast(dlsym(custom_source_library, "sample_source")); + // get the function to create the CustomSource from the library + auto create_custom_source = reinterpret_cast( + dlsym(custom_source_library, "openmc_create_source")); // check for any dlsym errors auto dlsym_error = dlerror(); if (dlsym_error) { + std::string error_msg = fmt::format("Couldn't open the openmc_create_source symbol: {}", dlsym_error); dlclose(custom_source_library); - fatal_error(fmt::format("Couldn't open the sample_source symbol: {}", dlsym_error)); + fatal_error(error_msg); } + // create a pointer to an instance of the CustomSource + custom_source = create_custom_source(custom_source_parameters); + #else fatal_error("Custom source libraries have not yet been implemented for " "non-POSIX systems"); @@ -385,6 +394,11 @@ void load_custom_source_library() void close_custom_source_library() { + if (custom_source.get()) { + // Make sure the custom source is destroyed before we close it's libary. + custom_source.reset(); + } + #ifdef HAS_DYNAMIC_LINKING dlclose(custom_source_library); #else @@ -395,7 +409,8 @@ void close_custom_source_library() Particle::Bank sample_custom_source_library(uint64_t* seed) { - return custom_source_function(seed); + // sample from the instance of the CustomSource + return custom_source->sample(seed); } void fill_source_bank_custom_source() diff --git a/tests/regression_tests/source_dlopen/source_sampling.cpp b/tests/regression_tests/source_dlopen/source_sampling.cpp index eaf8b74d9..ea74afdd9 100644 --- a/tests/regression_tests/source_dlopen/source_sampling.cpp +++ b/tests/regression_tests/source_dlopen/source_sampling.cpp @@ -1,11 +1,14 @@ #include +#include + #include "openmc/random_lcg.h" #include "openmc/source.h" #include "openmc/particle.h" -// you must have external C linkage here otherwise -// dlopen will not find the file -extern "C" openmc::Particle::Bank sample_source(uint64_t *seed) { +class Source : openmc::CustomSource +{ + openmc::Particle::Bank sample(uint64_t *seed) + { openmc::Particle::Bank particle; // wgt particle.particle = openmc::Particle::Type::neutron; @@ -20,4 +23,13 @@ extern "C" openmc::Particle::Bank sample_source(uint64_t *seed) { particle.E = 14.08e6; particle.delayed_group = 0; return particle; + } +}; + +// A function to create a unique pointer to an instance of this class when generated +// via a plugin call using dlopen/dlsym. +// You must have external C linkage here otherwise dlopen will not find the file +extern "C" std::unique_ptr openmc_create_source(std::string parameters) +{ + return std::make_unique(); } diff --git a/tests/regression_tests/source_parameterized_dlopen/__init__.py b/tests/regression_tests/source_parameterized_dlopen/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/source_parameterized_dlopen/inputs_true.dat b/tests/regression_tests/source_parameterized_dlopen/inputs_true.dat new file mode 100644 index 000000000..f4a0eba73 --- /dev/null +++ b/tests/regression_tests/source_parameterized_dlopen/inputs_true.dat @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + fixed source + 1000 + 10 + 0 + + diff --git a/tests/regression_tests/source_parameterized_dlopen/parameterized_source_sampling.cpp b/tests/regression_tests/source_parameterized_dlopen/parameterized_source_sampling.cpp new file mode 100644 index 000000000..3b0875bb0 --- /dev/null +++ b/tests/regression_tests/source_parameterized_dlopen/parameterized_source_sampling.cpp @@ -0,0 +1,38 @@ +#include "openmc/source.h" +#include "openmc/particle.h" + +class Source : public openmc::CustomSource { + public: + Source(double energy) : energy_(energy) { } + + // Samples from an instance of this class. + openmc::Particle::Bank sample(uint64_t* seed) + { + openmc::Particle::Bank particle; + // wgt + particle.particle = openmc::Particle::Type::neutron; + particle.wgt = 1.0; + // position + particle.r.x = 0.0; + particle.r.y = 0.0; + particle.r.z = 0.0; + // angle + particle.u = {1.0, 0.0, 0.0}; + particle.E = this->energy_; + particle.delayed_group = 0; + + return particle; + } + + private: + double energy_; +}; + +// A function to create a unique pointer to an instance of this class when generated +// via a plugin call using dlopen/dlsym. +// You must have external C linkage here otherwise dlopen will not find the file +extern "C" std::unique_ptr openmc_create_source(std::string parameter) +{ + double energy = std::stod(parameter); + return std::make_unique(energy); +} diff --git a/tests/regression_tests/source_parameterized_dlopen/results_true.dat b/tests/regression_tests/source_parameterized_dlopen/results_true.dat new file mode 100644 index 000000000..e69de29bb diff --git a/tests/regression_tests/source_parameterized_dlopen/test.py b/tests/regression_tests/source_parameterized_dlopen/test.py new file mode 100644 index 000000000..c613cde4b --- /dev/null +++ b/tests/regression_tests/source_parameterized_dlopen/test.py @@ -0,0 +1,75 @@ +from pathlib import Path +import os +import shutil +import subprocess +import textwrap + +import openmc +import pytest + +from tests.testing_harness import PyAPITestHarness + + +@pytest.fixture +def compile_source(request): + """Compile the external source""" + + # Get build directory and write CMakeLists.txt file + openmc_dir = Path(str(request.config.rootdir)) / 'build' + with open('CMakeLists.txt', 'w') as f: + f.write(textwrap.dedent(""" + cmake_minimum_required(VERSION 3.3 FATAL_ERROR) + project(openmc_sources CXX) + add_library(source SHARED parameterized_source_sampling.cpp) + find_package(OpenMC REQUIRED HINTS {}) + target_link_libraries(source OpenMC::libopenmc) + """.format(openmc_dir))) + + # Create temporary build directory and change to there + local_builddir = Path('build') + local_builddir.mkdir(exist_ok=True) + os.chdir(str(local_builddir)) + + # Run cmake/make to build the shared libary + subprocess.run(['cmake', os.path.pardir], check=True) + subprocess.run(['make'], check=True) + os.chdir(os.path.pardir) + + yield + + # Remove local build directory when test is complete + shutil.rmtree('build') + os.remove('CMakeLists.txt') + + +@pytest.fixture +def model(): + model = openmc.model.Model() + natural_lead = openmc.Material(name="natural_lead") + natural_lead.add_element('Pb', 1.0) + natural_lead.set_density('g/cm3', 11.34) + model.materials.append(natural_lead) + + # geometry + surface_sph1 = openmc.Sphere(r=100, boundary_type='vacuum') + cell_1 = openmc.Cell(fill=natural_lead, region=-surface_sph1) + model.geometry = openmc.Geometry([cell_1]) + + # settings + model.settings.batches = 10 + model.settings.inactive = 0 + model.settings.particles = 1000 + model.settings.run_mode = 'fixed source' + + # custom source from shared library + source = openmc.Source() + source.library = 'build/libsource.so' + source.parameters = '1e3' + model.settings.source = source + + return model + + +def test_dlopen_source(compile_source, model): + harness = PyAPITestHarness('statepoint.10.h5', model) + harness.main()