Merge pull request #1623 from DanShort12/serialised_custom_source

Add support for serialized custom sources
This commit is contained in:
Paul Romano 2020-08-31 07:35:49 -05:00 committed by GitHub
commit 726adfb46c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 488 additions and 56 deletions

View file

@ -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

View file

@ -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 <memory> // 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<Source> openmc_create_source(std::string parameters)
{
return std::make_unique<Source>();
}
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 <memory> // 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<Source> openmc_create_source(std::string parameter) {
double energy = std::stod(parameter);
return std::make_unique<Source>(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
---------------

View file

@ -1,26 +1,36 @@
#include <cmath> // for M_PI
#include <memory> // 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<Source> openmc_create_source(std::string parameters)
{
return std::make_unique<Source>();
}

View file

@ -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)

View file

@ -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=<path_to_openmc_install> 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.

View file

@ -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()

View file

@ -0,0 +1,66 @@
#include <cmath> // for M_PI
#include <memory> // for unique_ptr
#include <unordered_map>
#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<Source> from_string(std::string parameters)
{
std::unordered_map<std::string, std::string> 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<Source>(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<Source> openmc_create_source(std::string parameters)
{
return Source::from_string(parameters);
}

View file

@ -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()

View file

@ -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<CustomSource> create_custom_source_t(std::string parameters);
//==============================================================================
// Functions
//==============================================================================

View file

@ -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)

View file

@ -5,6 +5,7 @@
#endif
#include <algorithm> // for move
#include <memory> // for unique_ptr
#ifdef HAS_DYNAMIC_LINKING
#include <dlfcn.h> // for dlopen, dlsym, dlclose, dlerror
@ -44,9 +45,9 @@ std::vector<SourceDistribution> 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<CustomSource> 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<sample_t>(dlsym(custom_source_library, "sample_source"));
// get the function to create the CustomSource from the library
auto create_custom_source = reinterpret_cast<create_custom_source_t*>(
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()

View file

@ -1,11 +1,14 @@
#include <iostream>
#include <memory>
#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<Source> openmc_create_source(std::string parameters)
{
return std::make_unique<Source>();
}

View file

@ -0,0 +1,23 @@
<?xml version='1.0' encoding='utf-8'?>
<geometry>
<cell id="1" material="1" region="-1" universe="1" />
<surface boundary="vacuum" coeffs="0.0 0.0 0.0 100" id="1" type="sphere" />
</geometry>
<?xml version='1.0' encoding='utf-8'?>
<materials>
<material id="1" name="natural_lead">
<density units="g/cm3" value="11.34" />
<nuclide ao="0.014" name="Pb204" />
<nuclide ao="0.241" name="Pb206" />
<nuclide ao="0.221" name="Pb207" />
<nuclide ao="0.524" name="Pb208" />
</material>
</materials>
<?xml version='1.0' encoding='utf-8'?>
<settings>
<run_mode>fixed source</run_mode>
<particles>1000</particles>
<batches>10</batches>
<inactive>0</inactive>
<source library="build/libsource.so" parameters="1e3" strength="1.0" />
</settings>

View file

@ -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<Source> openmc_create_source(std::string parameter)
{
double energy = std::stod(parameter);
return std::make_unique<Source>(energy);
}

View file

@ -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()