Skip to content

Commit ecfb666

Browse files
authored
Allow spatial constraints on element sources within MeshSource (#3431)
1 parent dab8af5 commit ecfb666

3 files changed

Lines changed: 94 additions & 69 deletions

File tree

include/openmc/source.h

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,9 @@ class IndependentSource : public Source {
139139
DomainType domain_type() const { return domain_type_; }
140140
const std::unordered_set<int32_t>& domain_ids() const { return domain_ids_; }
141141

142+
// Setter for spatial distribution
143+
void set_space(UPtrSpace space) { space_ = std::move(space); }
144+
142145
protected:
143146
// Indicates whether derived class already handles constraints
144147
bool constraints_applied() const override { return true; }
@@ -205,6 +208,23 @@ typedef unique_ptr<Source> create_compiled_source_t(std::string parameters);
205208
//! Mesh-based source with different distributions for each element
206209
//==============================================================================
207210

211+
// Helper class to sample spatial position on a single mesh element
212+
class MeshElementSpatial : public SpatialDistribution {
213+
public:
214+
MeshElementSpatial(int32_t mesh_index, int elem_index)
215+
: mesh_index_(mesh_index), elem_index_(elem_index)
216+
{}
217+
218+
//! Sample a position from the distribution
219+
//! \param seed Pseudorandom number seed pointer
220+
//! \return Sampled position
221+
Position sample(uint64_t* seed) const override;
222+
223+
private:
224+
int32_t mesh_index_ {C_NONE}; //!< Index in global meshes array
225+
int elem_index_; //! Index of mesh element
226+
};
227+
208228
class MeshSource : public Source {
209229
public:
210230
// Constructors
@@ -219,18 +239,15 @@ class MeshSource : public Source {
219239
double strength() const override { return space_->total_strength(); }
220240

221241
// Accessors
222-
const std::unique_ptr<Source>& source(int32_t i) const
242+
const unique_ptr<IndependentSource>& source(int32_t i) const
223243
{
224244
return sources_.size() == 1 ? sources_[0] : sources_[i];
225245
}
226246

227-
protected:
228-
bool constraints_applied() const override { return true; }
229-
230247
private:
231248
// Data members
232-
unique_ptr<MeshSpatial> space_; //!< Mesh spatial
233-
vector<std::unique_ptr<Source>> sources_; //!< Source distributions
249+
unique_ptr<MeshSpatial> space_; //!< Mesh spatial
250+
vector<unique_ptr<IndependentSource>> sources_; //!< Source distributions
234251
};
235252

236253
//==============================================================================

src/source.cpp

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,9 @@ bool Source::satisfies_spatial_constraints(Position r) const
239239
if (!domain_ids_.empty()) {
240240
if (domain_type_ == DomainType::MATERIAL) {
241241
auto mat_index = geom_state.material();
242-
if (mat_index != MATERIAL_VOID) {
242+
if (mat_index == MATERIAL_VOID) {
243+
accepted = false;
244+
} else {
243245
accepted = contains(domain_ids_, model::materials[mat_index]->id());
244246
}
245247
} else {
@@ -525,6 +527,15 @@ CompiledSourceWrapper::~CompiledSourceWrapper()
525527
#endif
526528
}
527529

530+
//==============================================================================
531+
// MeshElementSpatial implementation
532+
//==============================================================================
533+
534+
Position MeshElementSpatial::sample(uint64_t* seed) const
535+
{
536+
return model::meshes[mesh_index_]->sample_element(elem_index_, seed);
537+
}
538+
528539
//==============================================================================
529540
// MeshSource implementation
530541
//==============================================================================
@@ -539,10 +550,23 @@ MeshSource::MeshSource(pugi::xml_node node) : Source(node)
539550
// read all source distributions and populate strengths vector for MeshSpatial
540551
// object
541552
for (auto source_node : node.children("source")) {
542-
sources_.emplace_back(Source::create(source_node));
553+
auto src = Source::create(source_node);
554+
if (auto ptr = dynamic_cast<IndependentSource*>(src.get())) {
555+
src.release();
556+
sources_.emplace_back(ptr);
557+
} else {
558+
fatal_error(
559+
"The source assigned to each element must be an IndependentSource.");
560+
}
543561
strengths.push_back(sources_.back()->strength());
544562
}
545563

564+
// Set spatial distributions for each mesh element
565+
for (int elem_index = 0; elem_index < sources_.size(); ++elem_index) {
566+
sources_[elem_index]->set_space(
567+
std::make_unique<MeshElementSpatial>(mesh_idx, elem_index));
568+
}
569+
546570
// the number of source distributions should either be one or equal to the
547571
// number of mesh elements
548572
if (sources_.size() > 1 && sources_.size() != mesh->n_bins()) {
@@ -556,29 +580,12 @@ MeshSource::MeshSource(pugi::xml_node node) : Source(node)
556580

557581
SourceSite MeshSource::sample(uint64_t* seed) const
558582
{
559-
// Sample the CDF defined in initialization above
583+
// Sample a mesh element based on the relative strengths
560584
int32_t element = space_->sample_element_index(seed);
561585

562-
// Sample position and apply rejection on spatial domains
563-
Position r;
564-
do {
565-
r = space_->mesh()->sample_element(element, seed);
566-
} while (!this->satisfies_spatial_constraints(r));
567-
568-
SourceSite site;
569-
while (true) {
570-
// Sample source for the chosen element and replace the position
571-
site = source(element)->sample_with_constraints(seed);
572-
site.r = r;
573-
574-
// Apply other rejections
575-
if (satisfies_energy_constraints(site.E) &&
576-
satisfies_time_constraints(site.time)) {
577-
break;
578-
}
579-
}
580-
581-
return site;
586+
// Sample the distribution for the specific mesh element; note that the
587+
// spatial distribution has been set for each element using MeshElementSpatial
588+
return source(element)->sample_with_constraints(seed);
582589
}
583590

584591
//==============================================================================

tests/unit_tests/test_source_mesh.py

Lines changed: 41 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from itertools import product
22
from pathlib import Path
3+
from math import sqrt
4+
import random
35

46
import pytest
57
import numpy as np
@@ -334,13 +336,10 @@ def test_umesh_source_independent(run_in_tmpdir, request, void_model, library):
334336
n_elements = 12_000
335337
model.settings.source = openmc.MeshSource(uscd_mesh, n_elements*[ind_source])
336338
model.export_to_model_xml()
337-
try:
338-
openmc.lib.init()
339+
with openmc.lib.run_in_memory():
339340
openmc.lib.simulation_init()
340341
sites = openmc.lib.sample_external_source(10)
341342
openmc.lib.statepoint_write('statepoint.h5')
342-
finally:
343-
openmc.lib.finalize()
344343

345344
with openmc.StatePoint('statepoint.h5') as sp:
346345
uscd_mesh = sp.meshes[uscd_mesh.id]
@@ -351,51 +350,53 @@ def test_umesh_source_independent(run_in_tmpdir, request, void_model, library):
351350
assert site.r in bounding_box
352351

353352

354-
def test_mesh_source_file(run_in_tmpdir):
355-
# Creating a source file with a single particle
356-
source_particle = openmc.SourceParticle(time=10.0)
357-
openmc.write_source_file([source_particle], 'source.h5')
358-
file_source = openmc.FileSource('source.h5')
353+
def test_mesh_source_constraints(run_in_tmpdir):
354+
"""Test application of constraints to underlying mesh element sources"""
359355

356+
# Create simple model with two cells
357+
m1 = openmc.Material()
358+
m1.add_nuclide('H1', 1.0)
359+
m2 = m1.clone()
360+
sph = openmc.Sphere(r=100, boundary_type='vacuum')
361+
box1 = openmc.model.RectangularParallelepiped(-1, 0, -1, 1, -1, 1)
362+
box2 = openmc.model.RectangularParallelepiped(0, 2, -1, 1, -1, 1)
363+
cell1 = openmc.Cell(fill=m1, region=-box1)
364+
cell2 = openmc.Cell(fill=m2, region=-box2)
365+
outer = openmc.Cell(region=-sph & (+box1 | +box2))
360366
model = openmc.Model()
367+
model.geometry = openmc.Geometry([cell1, cell2, outer])
361368

362-
rect_prism = openmc.model.RectangularParallelepiped(
363-
-5.0, 5.0, -5.0, 5.0, -5.0, 5.0, boundary_type='vacuum')
364-
365-
mat = openmc.Material()
366-
mat.add_nuclide('H1', 1.0)
367-
368-
model.geometry = openmc.Geometry([openmc.Cell(fill=mat, region=-rect_prism)])
369-
model.settings.particles = 1000
370-
model.settings.batches = 10
371-
model.settings.run_mode = 'fixed source'
372-
369+
# Define a mesh covering the two cells: the first mesh element contains
370+
# cell1 (-1 < x < 0) and the second element contains cells2 (0 < x < 2)
373371
mesh = openmc.RegularMesh()
374-
mesh.lower_left = (-1, -2, -3)
375-
mesh.upper_right = (2, 3, 4)
376-
mesh.dimension = (1, 1, 1)
372+
mesh.lower_left = (-3., -1., -1.)
373+
mesh.upper_right = (3., 1., 1.)
374+
mesh.dimension = (2, 1, 1)
377375

378-
model.settings.source = openmc.MeshSource(mesh, [file_source])
376+
# Define a mesh source with a randomly chosen probability
377+
p = random.random()
378+
src1 = openmc.IndependentSource(strength=p, constraints={'domains': [cell1]})
379+
src2 = openmc.IndependentSource(strength=1 - p, constraints={'domains': [cell2]})
380+
model.settings.source = openmc.MeshSource(mesh, [src1, src2])
379381

382+
# Finish settings and export
383+
model.settings.particles = 100
384+
model.settings.batches = 1
380385
model.export_to_model_xml()
381386

382-
openmc.lib.init()
383-
openmc.lib.simulation_init()
384-
sites = openmc.lib.sample_external_source(10)
385-
openmc.lib.simulation_finalize()
386-
openmc.lib.finalize()
387+
with openmc.lib.run_in_memory():
388+
# Sample sites from the source
389+
sites = openmc.lib.sample_external_source(N := 1000)
387390

388-
# The mesh bounds do not contain the point of the lone source site in the
389-
# file source, so it should not appear in the set of source sites produced
390-
# from the mesh source. Additionally, the source should be located within
391-
# the mesh
392-
bbox = mesh.bounding_box
393-
for site in sites:
394-
assert site.r != (0, 0, 0)
395-
assert site.E == source_particle.E
396-
assert site.u == source_particle.u
397-
assert site.time == source_particle.time
398-
assert site.r in bbox
391+
# Check that all sites are either in cell1 or cell2
392+
xs = np.array([s.r[0] for s in sites])
393+
assert (xs >= -1.0).all()
394+
assert (xs <= 2.0).all()
395+
396+
# Check that the correct percentage of the sites are in cell1
397+
sigma = sqrt(p*(1- p)/N)
398+
frac = xs[(-1.0 <= xs) & (xs <= 0.0)].size / N
399+
assert frac == pytest.approx(p, abs=5*sigma)
399400

400401

401402
@pytest.mark.parametrize("mesh_type", ('rectangular', 'cylindrical', 'spherical'))

0 commit comments

Comments
 (0)