Fix weight window energy defaults being derived before the particle type is known - #4091

Merged
paulromano merged 3 commits into
openmc-dev:developfrom
GuySten:ww-particle-type-fix
Sep 1, 2026
Merged

Fix weight window energy defaults being derived before the particle type is known#4091
paulromano merged 3 commits into
openmc-dev:developfrom
GuySten:ww-particle-type-fix

Conversation

@GuySten

@GuyStenGuySten commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fix weight window energy defaults being derived before the particle type is known

Scope: this affects weight windows created through the C API during a
simulation
without an explicit energy_bounds. Photon ones previously received
neutron default energy bounds; they now receive photon ones. Weight windows
defined in XML are unaffected, as are any with an explicit energy_bounds, as
are any created before openmc_simulation_init().

Problem

WeightWindows::set_defaults() derives the default energy grid from
data::energy_min/max for the object's particle type, and only does so when the
grid is empty:

voidWeightWindows::set_defaults()
{
if (energy_bounds_.size() == 0) {
int p_type = particle_type_.transport_index();
...
energy_bounds_.push_back(data::energy_min[p_type]);
energy_bounds_.push_back(data::energy_max[p_type]);
}
}

The WeightWindows(int32_t id) constructor, which is the one the C API and
openmc.lib use, called it immediately:

WeightWindows::WeightWindows(int32_t id)
{
index_ = variance_reduction::weight_windows.size();
set_id(id);
set_defaults(); // particle type, mesh and energy grid all unknown here
}

ParticleType default-constructs to PDG_NEUTRON, so this does not error. It
silently derives neutron bounds. set_particle_type() does not re-derive them,
and because the grid is no longer empty every subsequent set_defaults() call
is a no-op. So the early call does not merely compute the wrong answer, it
prevents the right one from ever being computed.

The XML constructor is not affected: it assigns particle_type_ before calling
set_defaults() at the end.

The window in which this is observable is narrower than it first appears.
data::energy_min/max are populated by initialize_data(), which runs from
openmc_simulation_init(), not openmc_init(). Before simulation
initialization both arrays hold their static defaults of 0.0 and INFTY for
every particle, so the neutron and photon defaults coincide and nothing is
visibly wrong. The bug bites weight windows constructed through the C API once a
simulation is running -- which is the regime weight window generation operates
in, and is why WeightWindowsGenerator needs its workaround.

Two things in the existing code point at this:

  • WeightWindowsGenerator::WeightWindowsGenerator calls set_defaults() again
    after set_mesh(), set_energy_bounds() and set_particle_type(). That is a
    workaround, and it only works because the generator constructs through a path
    where the grid is still empty.
  • The energy_bounds_.size() == 0 guard is what turns an early call into a
    permanent one.

MWE

importnumpyasnpimportopenmcimportopenmc.libwater=openmc.Material()
water.set_density('g/cm3', 1.0)
water.add_nuclide('H1', 2.0)
water.add_nuclide('O16', 1.0)
sphere=openmc.Sphere(r=10.0, boundary_type='vacuum')
model=openmc.Model()
model.geometry=openmc.Geometry([openmc.Cell(fill=water, region=-sphere)])
model.settings.run_mode='fixed source'model.settings.particles=100model.settings.batches=1model.settings.photon_transport=True# so photon data is loaded toomodel.export_to_model_xml()
defmake_ww(uid, particle):
"""Create a weight window through the C API, as openmc.lib does."""mesh=openmc.lib.RegularMesh()
mesh.dimension= (2, 2, 2)
mesh.set_parameters(lower_left=(-1.0, -1.0, -1.0),
upper_right=(1.0, 1.0, 1.0))
ww=openmc.lib.WeightWindows(uid)
ww.mesh=meshww.particle=particlereturnnp.asarray(ww.energy_bounds)
openmc.lib.init()
openmc.lib.simulation_init() # populates data::energy_min/maxtry:
neutron=make_ww(900, 'neutron')
photon=make_ww(901, 'photon')
finally:
openmc.lib.simulation_finalize()
openmc.lib.finalize()
print(f'\n neutron default energy bounds {neutron}')
print(f' photon default energy bounds {photon}')
ifnp.allclose(neutron, photon):
print('\n FAIL: the photon weight window has neutron energy bounds.')
print(' Photon data extends far beyond the neutron limit, so an upper')
print(' bound equal to the neutron one cannot be right.')
raiseSystemExit(1)
print('\n PASS: each particle got its own default energy range.')

Changes

  • WeightWindows(int32_t id) no longer calls set_defaults(). A comment
    explains why, so it is not reinstated.

  • set_particle_type() calls set_defaults(), which is the point at which the
    defaults are actually determined. It is a no-op when an explicit grid has
    already been supplied.

  • openmc_weight_windows_export() calls set_defaults() once per weight window
    before writing, so objects built through the C API whose particle type was
    never set do not export an empty energy grid.

  • WeightWindowsGenerator drops its trailing set_defaults() call, which is
    now redundant. This is the workaround the bug forced, so its removal is the
    clearest demonstration that the fix works.

  • The XML constructor assigns the particle type through set_particle_type()
    instead of writing particle_type_ directly, and drops its own trailing
    set_defaults(). Two side benefits: the XML path now gets the same
    neutron/photon validation as the C API path, and every existing XML-based
    weight window test exercises the new call site.

After this, set_defaults() has exactly two call sites -- set_particle_type()
and the export backstop -- so every path that assigns a particle type derives
its defaults the same way.

bounds_size() already treats an empty energy grid as one bin:

int num_energy_bins = energy_bounds_.size() > 0 ? energy_bounds_.size() - 1 : 1;

so set_mesh() continues to allocate a correctly shaped 1 x n_spatial bounds
tensor in the window between construction and the particle type being set.

Testing

New tests/unit_tests/weightwindows/test_ww_defaults.py, which initializes a
model with both neutron and photon data and calls simulation_init() so that
data::energy_min/max are actually populated:

  • Neutron and photon weight windows must get different default energy bounds.
    Identical bounds are the signature of the defaults being derived from the
    default particle type at construction.
  • An explicit energy_bounds grid survives a later set_particle_type().
  • Populated weight window bounds survive a later set_particle_type().

Notes

Found while reviewing #4057, but it is independent of that PR and reproduces on
develop. It may allow #4057 to drop its reset_energy_bounds() addition,
since a freshly constructed openmc.lib.WeightWindows will now pick up correct
defaults on its own once the particle type is assigned.

Checklist

  • I have performed a self-review of my own code
  • I have run clang-format (version 18) on any C++ source files (if applicable)
  • I have followed the style guidelines for Python source files (if applicable)
  • I have made corresponding changes to the documentation (if applicable)
  • I have added tests that prove my fix is effective or that my feature works (if applicable)

@GuySten
GuyStenforce-pushed the ww-particle-type-fix branch from aaf8210 to 78adf76CompareAugust 31, 2026 19:05
@GuySten
GuySten marked this pull request as ready for review August 31, 2026 19:54
@paulromano
paulromano enabled auto-merge (squash) September 1, 2026 13:26
@paulromano
paulromano merged commit c6f9187 into openmc-dev:developSep 1, 2026
16 checks passed
@GuySten
GuySten deleted the ww-particle-type-fix branch September 1, 2026 14:52
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@GuySten@paulromano
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Fix weight window energy defaults being derived before the particle type is known - #4091

Merged
paulromano merged 3 commits into
openmc-dev:developfrom
GuySten:ww-particle-type-fix
Sep 1, 2026
Merged

Fix weight window energy defaults being derived before the particle type is known#4091
paulromano merged 3 commits into
openmc-dev:developfrom
GuySten:ww-particle-type-fix

Conversation

@GuySten

@GuyStenGuySten commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fix weight window energy defaults being derived before the particle type is known

Scope: this affects weight windows created through the C API during a
simulation
without an explicit energy_bounds. Photon ones previously received
neutron default energy bounds; they now receive photon ones. Weight windows
defined in XML are unaffected, as are any with an explicit energy_bounds, as
are any created before openmc_simulation_init().

Problem

WeightWindows::set_defaults() derives the default energy grid from
data::energy_min/max for the object's particle type, and only does so when the
grid is empty:

voidWeightWindows::set_defaults()
{
if (energy_bounds_.size() == 0) {
int p_type = particle_type_.transport_index();
...
energy_bounds_.push_back(data::energy_min[p_type]);
energy_bounds_.push_back(data::energy_max[p_type]);
}
}

The WeightWindows(int32_t id) constructor, which is the one the C API and
openmc.lib use, called it immediately:

WeightWindows::WeightWindows(int32_t id)
{
index_ = variance_reduction::weight_windows.size();
set_id(id);
set_defaults(); // particle type, mesh and energy grid all unknown here
}

ParticleType default-constructs to PDG_NEUTRON, so this does not error. It
silently derives neutron bounds. set_particle_type() does not re-derive them,
and because the grid is no longer empty every subsequent set_defaults() call
is a no-op. So the early call does not merely compute the wrong answer, it
prevents the right one from ever being computed.

The XML constructor is not affected: it assigns particle_type_ before calling
set_defaults() at the end.

The window in which this is observable is narrower than it first appears.
data::energy_min/max are populated by initialize_data(), which runs from
openmc_simulation_init(), not openmc_init(). Before simulation
initialization both arrays hold their static defaults of 0.0 and INFTY for
every particle, so the neutron and photon defaults coincide and nothing is
visibly wrong. The bug bites weight windows constructed through the C API once a
simulation is running -- which is the regime weight window generation operates
in, and is why WeightWindowsGenerator needs its workaround.

Two things in the existing code point at this:

  • WeightWindowsGenerator::WeightWindowsGenerator calls set_defaults() again
    after set_mesh(), set_energy_bounds() and set_particle_type(). That is a
    workaround, and it only works because the generator constructs through a path
    where the grid is still empty.
  • The energy_bounds_.size() == 0 guard is what turns an early call into a
    permanent one.

MWE

importnumpyasnpimportopenmcimportopenmc.libwater=openmc.Material()
water.set_density('g/cm3', 1.0)
water.add_nuclide('H1', 2.0)
water.add_nuclide('O16', 1.0)
sphere=openmc.Sphere(r=10.0, boundary_type='vacuum')
model=openmc.Model()
model.geometry=openmc.Geometry([openmc.Cell(fill=water, region=-sphere)])
model.settings.run_mode='fixed source'model.settings.particles=100model.settings.batches=1model.settings.photon_transport=True# so photon data is loaded toomodel.export_to_model_xml()
defmake_ww(uid, particle):
"""Create a weight window through the C API, as openmc.lib does."""mesh=openmc.lib.RegularMesh()
mesh.dimension= (2, 2, 2)
mesh.set_parameters(lower_left=(-1.0, -1.0, -1.0),
upper_right=(1.0, 1.0, 1.0))
ww=openmc.lib.WeightWindows(uid)
ww.mesh=meshww.particle=particlereturnnp.asarray(ww.energy_bounds)
openmc.lib.init()
openmc.lib.simulation_init() # populates data::energy_min/maxtry:
neutron=make_ww(900, 'neutron')
photon=make_ww(901, 'photon')
finally:
openmc.lib.simulation_finalize()
openmc.lib.finalize()
print(f'\n neutron default energy bounds {neutron}')
print(f' photon default energy bounds {photon}')
ifnp.allclose(neutron, photon):
print('\n FAIL: the photon weight window has neutron energy bounds.')
print(' Photon data extends far beyond the neutron limit, so an upper')
print(' bound equal to the neutron one cannot be right.')
raiseSystemExit(1)
print('\n PASS: each particle got its own default energy range.')

Changes

  • WeightWindows(int32_t id) no longer calls set_defaults(). A comment
    explains why, so it is not reinstated.

  • set_particle_type() calls set_defaults(), which is the point at which the
    defaults are actually determined. It is a no-op when an explicit grid has
    already been supplied.

  • openmc_weight_windows_export() calls set_defaults() once per weight window
    before writing, so objects built through the C API whose particle type was
    never set do not export an empty energy grid.

  • WeightWindowsGenerator drops its trailing set_defaults() call, which is
    now redundant. This is the workaround the bug forced, so its removal is the
    clearest demonstration that the fix works.

  • The XML constructor assigns the particle type through set_particle_type()
    instead of writing particle_type_ directly, and drops its own trailing
    set_defaults(). Two side benefits: the XML path now gets the same
    neutron/photon validation as the C API path, and every existing XML-based
    weight window test exercises the new call site.

After this, set_defaults() has exactly two call sites -- set_particle_type()
and the export backstop -- so every path that assigns a particle type derives
its defaults the same way.

bounds_size() already treats an empty energy grid as one bin:

int num_energy_bins = energy_bounds_.size() > 0 ? energy_bounds_.size() - 1 : 1;

so set_mesh() continues to allocate a correctly shaped 1 x n_spatial bounds
tensor in the window between construction and the particle type being set.

Testing

New tests/unit_tests/weightwindows/test_ww_defaults.py, which initializes a
model with both neutron and photon data and calls simulation_init() so that
data::energy_min/max are actually populated:

  • Neutron and photon weight windows must get different default energy bounds.
    Identical bounds are the signature of the defaults being derived from the
    default particle type at construction.
  • An explicit energy_bounds grid survives a later set_particle_type().
  • Populated weight window bounds survive a later set_particle_type().

Notes

Found while reviewing #4057, but it is independent of that PR and reproduces on
develop. It may allow #4057 to drop its reset_energy_bounds() addition,
since a freshly constructed openmc.lib.WeightWindows will now pick up correct
defaults on its own once the particle type is assigned.

Checklist

  • I have performed a self-review of my own code
  • I have run clang-format (version 18) on any C++ source files (if applicable)
  • I have followed the style guidelines for Python source files (if applicable)
  • I have made corresponding changes to the documentation (if applicable)
  • I have added tests that prove my fix is effective or that my feature works (if applicable)

@GuySten
GuyStenforce-pushed the ww-particle-type-fix branch from aaf8210 to 78adf76CompareAugust 31, 2026 19:05
@GuySten
GuySten marked this pull request as ready for review August 31, 2026 19:54
@paulromano
paulromano enabled auto-merge (squash) September 1, 2026 13:26
@paulromano
paulromano merged commit c6f9187 into openmc-dev:developSep 1, 2026
16 checks passed
@GuySten
GuySten deleted the ww-particle-type-fix branch September 1, 2026 14:52
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@GuySten@paulromano
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix weight window energy defaults being derived before the particle type is known - #4091

Merged
paulromano merged 3 commits into
openmc-dev:developfrom
GuySten:ww-particle-type-fix
Sep 1, 2026
Merged

Fix weight window energy defaults being derived before the particle type is known#4091
paulromano merged 3 commits into
openmc-dev:developfrom
GuySten:ww-particle-type-fix

Conversation

@GuySten

@GuyStenGuySten commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fix weight window energy defaults being derived before the particle type is known

Scope: this affects weight windows created through the C API during a
simulation
without an explicit energy_bounds. Photon ones previously received
neutron default energy bounds; they now receive photon ones. Weight windows
defined in XML are unaffected, as are any with an explicit energy_bounds, as
are any created before openmc_simulation_init().

Problem

WeightWindows::set_defaults() derives the default energy grid from
data::energy_min/max for the object's particle type, and only does so when the
grid is empty:

voidWeightWindows::set_defaults()
{
if (energy_bounds_.size() == 0) {
int p_type = particle_type_.transport_index();
...
energy_bounds_.push_back(data::energy_min[p_type]);
energy_bounds_.push_back(data::energy_max[p_type]);
}
}

The WeightWindows(int32_t id) constructor, which is the one the C API and
openmc.lib use, called it immediately:

WeightWindows::WeightWindows(int32_t id)
{
index_ = variance_reduction::weight_windows.size();
set_id(id);
set_defaults(); // particle type, mesh and energy grid all unknown here
}

ParticleType default-constructs to PDG_NEUTRON, so this does not error. It
silently derives neutron bounds. set_particle_type() does not re-derive them,
and because the grid is no longer empty every subsequent set_defaults() call
is a no-op. So the early call does not merely compute the wrong answer, it
prevents the right one from ever being computed.

The XML constructor is not affected: it assigns particle_type_ before calling
set_defaults() at the end.

The window in which this is observable is narrower than it first appears.
data::energy_min/max are populated by initialize_data(), which runs from
openmc_simulation_init(), not openmc_init(). Before simulation
initialization both arrays hold their static defaults of 0.0 and INFTY for
every particle, so the neutron and photon defaults coincide and nothing is
visibly wrong. The bug bites weight windows constructed through the C API once a
simulation is running -- which is the regime weight window generation operates
in, and is why WeightWindowsGenerator needs its workaround.

Two things in the existing code point at this:

  • WeightWindowsGenerator::WeightWindowsGenerator calls set_defaults() again
    after set_mesh(), set_energy_bounds() and set_particle_type(). That is a
    workaround, and it only works because the generator constructs through a path
    where the grid is still empty.
  • The energy_bounds_.size() == 0 guard is what turns an early call into a
    permanent one.

MWE

importnumpyasnpimportopenmcimportopenmc.libwater=openmc.Material()
water.set_density('g/cm3', 1.0)
water.add_nuclide('H1', 2.0)
water.add_nuclide('O16', 1.0)
sphere=openmc.Sphere(r=10.0, boundary_type='vacuum')
model=openmc.Model()
model.geometry=openmc.Geometry([openmc.Cell(fill=water, region=-sphere)])
model.settings.run_mode='fixed source'model.settings.particles=100model.settings.batches=1model.settings.photon_transport=True# so photon data is loaded toomodel.export_to_model_xml()
defmake_ww(uid, particle):
"""Create a weight window through the C API, as openmc.lib does."""mesh=openmc.lib.RegularMesh()
mesh.dimension= (2, 2, 2)
mesh.set_parameters(lower_left=(-1.0, -1.0, -1.0),
upper_right=(1.0, 1.0, 1.0))
ww=openmc.lib.WeightWindows(uid)
ww.mesh=meshww.particle=particlereturnnp.asarray(ww.energy_bounds)
openmc.lib.init()
openmc.lib.simulation_init() # populates data::energy_min/maxtry:
neutron=make_ww(900, 'neutron')
photon=make_ww(901, 'photon')
finally:
openmc.lib.simulation_finalize()
openmc.lib.finalize()
print(f'\n neutron default energy bounds {neutron}')
print(f' photon default energy bounds {photon}')
ifnp.allclose(neutron, photon):
print('\n FAIL: the photon weight window has neutron energy bounds.')
print(' Photon data extends far beyond the neutron limit, so an upper')
print(' bound equal to the neutron one cannot be right.')
raiseSystemExit(1)
print('\n PASS: each particle got its own default energy range.')

Changes

  • WeightWindows(int32_t id) no longer calls set_defaults(). A comment
    explains why, so it is not reinstated.

  • set_particle_type() calls set_defaults(), which is the point at which the
    defaults are actually determined. It is a no-op when an explicit grid has
    already been supplied.

  • openmc_weight_windows_export() calls set_defaults() once per weight window
    before writing, so objects built through the C API whose particle type was
    never set do not export an empty energy grid.

  • WeightWindowsGenerator drops its trailing set_defaults() call, which is
    now redundant. This is the workaround the bug forced, so its removal is the
    clearest demonstration that the fix works.

  • The XML constructor assigns the particle type through set_particle_type()
    instead of writing particle_type_ directly, and drops its own trailing
    set_defaults(). Two side benefits: the XML path now gets the same
    neutron/photon validation as the C API path, and every existing XML-based
    weight window test exercises the new call site.

After this, set_defaults() has exactly two call sites -- set_particle_type()
and the export backstop -- so every path that assigns a particle type derives
its defaults the same way.

bounds_size() already treats an empty energy grid as one bin:

int num_energy_bins = energy_bounds_.size() > 0 ? energy_bounds_.size() - 1 : 1;

so set_mesh() continues to allocate a correctly shaped 1 x n_spatial bounds
tensor in the window between construction and the particle type being set.

Testing

New tests/unit_tests/weightwindows/test_ww_defaults.py, which initializes a
model with both neutron and photon data and calls simulation_init() so that
data::energy_min/max are actually populated:

  • Neutron and photon weight windows must get different default energy bounds.
    Identical bounds are the signature of the defaults being derived from the
    default particle type at construction.
  • An explicit energy_bounds grid survives a later set_particle_type().
  • Populated weight window bounds survive a later set_particle_type().

Notes

Found while reviewing #4057, but it is independent of that PR and reproduces on
develop. It may allow #4057 to drop its reset_energy_bounds() addition,
since a freshly constructed openmc.lib.WeightWindows will now pick up correct
defaults on its own once the particle type is assigned.

Checklist

  • I have performed a self-review of my own code
  • I have run clang-format (version 18) on any C++ source files (if applicable)
  • I have followed the style guidelines for Python source files (if applicable)
  • I have made corresponding changes to the documentation (if applicable)
  • I have added tests that prove my fix is effective or that my feature works (if applicable)

@GuySten
GuyStenforce-pushed the ww-particle-type-fix branch from aaf8210 to 78adf76CompareAugust 31, 2026 19:05
@GuySten
GuySten marked this pull request as ready for review August 31, 2026 19:54
@paulromano
paulromano enabled auto-merge (squash) September 1, 2026 13:26
@paulromano
paulromano merged commit c6f9187 into openmc-dev:developSep 1, 2026
16 checks passed
@GuySten
GuySten deleted the ww-particle-type-fix branch September 1, 2026 14:52
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@GuySten@paulromano
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix weight window energy defaults being derived before the particle type is known - #4091

Merged
paulromano merged 3 commits into
openmc-dev:developfrom
GuySten:ww-particle-type-fix
Sep 1, 2026
Merged

Fix weight window energy defaults being derived before the particle type is known#4091
paulromano merged 3 commits into
openmc-dev:developfrom
GuySten:ww-particle-type-fix

Conversation

@GuySten

@GuyStenGuySten commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fix weight window energy defaults being derived before the particle type is known

Scope: this affects weight windows created through the C API during a
simulation
without an explicit energy_bounds. Photon ones previously received
neutron default energy bounds; they now receive photon ones. Weight windows
defined in XML are unaffected, as are any with an explicit energy_bounds, as
are any created before openmc_simulation_init().

Problem

WeightWindows::set_defaults() derives the default energy grid from
data::energy_min/max for the object's particle type, and only does so when the
grid is empty:

voidWeightWindows::set_defaults()
{
if (energy_bounds_.size() == 0) {
int p_type = particle_type_.transport_index();
...
energy_bounds_.push_back(data::energy_min[p_type]);
energy_bounds_.push_back(data::energy_max[p_type]);
}
}

The WeightWindows(int32_t id) constructor, which is the one the C API and
openmc.lib use, called it immediately:

WeightWindows::WeightWindows(int32_t id)
{
index_ = variance_reduction::weight_windows.size();
set_id(id);
set_defaults(); // particle type, mesh and energy grid all unknown here
}

ParticleType default-constructs to PDG_NEUTRON, so this does not error. It
silently derives neutron bounds. set_particle_type() does not re-derive them,
and because the grid is no longer empty every subsequent set_defaults() call
is a no-op. So the early call does not merely compute the wrong answer, it
prevents the right one from ever being computed.

The XML constructor is not affected: it assigns particle_type_ before calling
set_defaults() at the end.

The window in which this is observable is narrower than it first appears.
data::energy_min/max are populated by initialize_data(), which runs from
openmc_simulation_init(), not openmc_init(). Before simulation
initialization both arrays hold their static defaults of 0.0 and INFTY for
every particle, so the neutron and photon defaults coincide and nothing is
visibly wrong. The bug bites weight windows constructed through the C API once a
simulation is running -- which is the regime weight window generation operates
in, and is why WeightWindowsGenerator needs its workaround.

Two things in the existing code point at this:

  • WeightWindowsGenerator::WeightWindowsGenerator calls set_defaults() again
    after set_mesh(), set_energy_bounds() and set_particle_type(). That is a
    workaround, and it only works because the generator constructs through a path
    where the grid is still empty.
  • The energy_bounds_.size() == 0 guard is what turns an early call into a
    permanent one.

MWE

importnumpyasnpimportopenmcimportopenmc.libwater=openmc.Material()
water.set_density('g/cm3', 1.0)
water.add_nuclide('H1', 2.0)
water.add_nuclide('O16', 1.0)
sphere=openmc.Sphere(r=10.0, boundary_type='vacuum')
model=openmc.Model()
model.geometry=openmc.Geometry([openmc.Cell(fill=water, region=-sphere)])
model.settings.run_mode='fixed source'model.settings.particles=100model.settings.batches=1model.settings.photon_transport=True# so photon data is loaded toomodel.export_to_model_xml()
defmake_ww(uid, particle):
"""Create a weight window through the C API, as openmc.lib does."""mesh=openmc.lib.RegularMesh()
mesh.dimension= (2, 2, 2)
mesh.set_parameters(lower_left=(-1.0, -1.0, -1.0),
upper_right=(1.0, 1.0, 1.0))
ww=openmc.lib.WeightWindows(uid)
ww.mesh=meshww.particle=particlereturnnp.asarray(ww.energy_bounds)
openmc.lib.init()
openmc.lib.simulation_init() # populates data::energy_min/maxtry:
neutron=make_ww(900, 'neutron')
photon=make_ww(901, 'photon')
finally:
openmc.lib.simulation_finalize()
openmc.lib.finalize()
print(f'\n neutron default energy bounds {neutron}')
print(f' photon default energy bounds {photon}')
ifnp.allclose(neutron, photon):
print('\n FAIL: the photon weight window has neutron energy bounds.')
print(' Photon data extends far beyond the neutron limit, so an upper')
print(' bound equal to the neutron one cannot be right.')
raiseSystemExit(1)
print('\n PASS: each particle got its own default energy range.')

Changes

  • WeightWindows(int32_t id) no longer calls set_defaults(). A comment
    explains why, so it is not reinstated.

  • set_particle_type() calls set_defaults(), which is the point at which the
    defaults are actually determined. It is a no-op when an explicit grid has
    already been supplied.

  • openmc_weight_windows_export() calls set_defaults() once per weight window
    before writing, so objects built through the C API whose particle type was
    never set do not export an empty energy grid.

  • WeightWindowsGenerator drops its trailing set_defaults() call, which is
    now redundant. This is the workaround the bug forced, so its removal is the
    clearest demonstration that the fix works.

  • The XML constructor assigns the particle type through set_particle_type()
    instead of writing particle_type_ directly, and drops its own trailing
    set_defaults(). Two side benefits: the XML path now gets the same
    neutron/photon validation as the C API path, and every existing XML-based
    weight window test exercises the new call site.

After this, set_defaults() has exactly two call sites -- set_particle_type()
and the export backstop -- so every path that assigns a particle type derives
its defaults the same way.

bounds_size() already treats an empty energy grid as one bin:

int num_energy_bins = energy_bounds_.size() > 0 ? energy_bounds_.size() - 1 : 1;

so set_mesh() continues to allocate a correctly shaped 1 x n_spatial bounds
tensor in the window between construction and the particle type being set.

Testing

New tests/unit_tests/weightwindows/test_ww_defaults.py, which initializes a
model with both neutron and photon data and calls simulation_init() so that
data::energy_min/max are actually populated:

  • Neutron and photon weight windows must get different default energy bounds.
    Identical bounds are the signature of the defaults being derived from the
    default particle type at construction.
  • An explicit energy_bounds grid survives a later set_particle_type().
  • Populated weight window bounds survive a later set_particle_type().

Notes

Found while reviewing #4057, but it is independent of that PR and reproduces on
develop. It may allow #4057 to drop its reset_energy_bounds() addition,
since a freshly constructed openmc.lib.WeightWindows will now pick up correct
defaults on its own once the particle type is assigned.

Checklist

  • I have performed a self-review of my own code
  • I have run clang-format (version 18) on any C++ source files (if applicable)
  • I have followed the style guidelines for Python source files (if applicable)
  • I have made corresponding changes to the documentation (if applicable)
  • I have added tests that prove my fix is effective or that my feature works (if applicable)

@GuySten
GuyStenforce-pushed the ww-particle-type-fix branch from aaf8210 to 78adf76CompareAugust 31, 2026 19:05
@GuySten
GuySten marked this pull request as ready for review August 31, 2026 19:54
@paulromano
paulromano enabled auto-merge (squash) September 1, 2026 13:26
@paulromano
paulromano merged commit c6f9187 into openmc-dev:developSep 1, 2026
16 checks passed
@GuySten
GuySten deleted the ww-particle-type-fix branch September 1, 2026 14:52
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@GuySten@paulromano
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Fix weight window energy defaults being derived before the particle type is known - #4091

Merged
paulromano merged 3 commits into
openmc-dev:developfrom
GuySten:ww-particle-type-fix
Sep 1, 2026
Merged

Fix weight window energy defaults being derived before the particle type is known#4091
paulromano merged 3 commits into
openmc-dev:developfrom
GuySten:ww-particle-type-fix

Conversation

@GuySten

@GuyStenGuySten commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fix weight window energy defaults being derived before the particle type is known

Scope: this affects weight windows created through the C API during a
simulation
without an explicit energy_bounds. Photon ones previously received
neutron default energy bounds; they now receive photon ones. Weight windows
defined in XML are unaffected, as are any with an explicit energy_bounds, as
are any created before openmc_simulation_init().

Problem

WeightWindows::set_defaults() derives the default energy grid from
data::energy_min/max for the object's particle type, and only does so when the
grid is empty:

voidWeightWindows::set_defaults()
{
if (energy_bounds_.size() == 0) {
int p_type = particle_type_.transport_index();
...
energy_bounds_.push_back(data::energy_min[p_type]);
energy_bounds_.push_back(data::energy_max[p_type]);
}
}

The WeightWindows(int32_t id) constructor, which is the one the C API and
openmc.lib use, called it immediately:

WeightWindows::WeightWindows(int32_t id)
{
index_ = variance_reduction::weight_windows.size();
set_id(id);
set_defaults(); // particle type, mesh and energy grid all unknown here
}

ParticleType default-constructs to PDG_NEUTRON, so this does not error. It
silently derives neutron bounds. set_particle_type() does not re-derive them,
and because the grid is no longer empty every subsequent set_defaults() call
is a no-op. So the early call does not merely compute the wrong answer, it
prevents the right one from ever being computed.

The XML constructor is not affected: it assigns particle_type_ before calling
set_defaults() at the end.

The window in which this is observable is narrower than it first appears.
data::energy_min/max are populated by initialize_data(), which runs from
openmc_simulation_init(), not openmc_init(). Before simulation
initialization both arrays hold their static defaults of 0.0 and INFTY for
every particle, so the neutron and photon defaults coincide and nothing is
visibly wrong. The bug bites weight windows constructed through the C API once a
simulation is running -- which is the regime weight window generation operates
in, and is why WeightWindowsGenerator needs its workaround.

Two things in the existing code point at this:

  • WeightWindowsGenerator::WeightWindowsGenerator calls set_defaults() again
    after set_mesh(), set_energy_bounds() and set_particle_type(). That is a
    workaround, and it only works because the generator constructs through a path
    where the grid is still empty.
  • The energy_bounds_.size() == 0 guard is what turns an early call into a
    permanent one.

MWE

importnumpyasnpimportopenmcimportopenmc.libwater=openmc.Material()
water.set_density('g/cm3', 1.0)
water.add_nuclide('H1', 2.0)
water.add_nuclide('O16', 1.0)
sphere=openmc.Sphere(r=10.0, boundary_type='vacuum')
model=openmc.Model()
model.geometry=openmc.Geometry([openmc.Cell(fill=water, region=-sphere)])
model.settings.run_mode='fixed source'model.settings.particles=100model.settings.batches=1model.settings.photon_transport=True# so photon data is loaded toomodel.export_to_model_xml()
defmake_ww(uid, particle):
"""Create a weight window through the C API, as openmc.lib does."""mesh=openmc.lib.RegularMesh()
mesh.dimension= (2, 2, 2)
mesh.set_parameters(lower_left=(-1.0, -1.0, -1.0),
upper_right=(1.0, 1.0, 1.0))
ww=openmc.lib.WeightWindows(uid)
ww.mesh=meshww.particle=particlereturnnp.asarray(ww.energy_bounds)
openmc.lib.init()
openmc.lib.simulation_init() # populates data::energy_min/maxtry:
neutron=make_ww(900, 'neutron')
photon=make_ww(901, 'photon')
finally:
openmc.lib.simulation_finalize()
openmc.lib.finalize()
print(f'\n neutron default energy bounds {neutron}')
print(f' photon default energy bounds {photon}')
ifnp.allclose(neutron, photon):
print('\n FAIL: the photon weight window has neutron energy bounds.')
print(' Photon data extends far beyond the neutron limit, so an upper')
print(' bound equal to the neutron one cannot be right.')
raiseSystemExit(1)
print('\n PASS: each particle got its own default energy range.')

Changes

  • WeightWindows(int32_t id) no longer calls set_defaults(). A comment
    explains why, so it is not reinstated.

  • set_particle_type() calls set_defaults(), which is the point at which the
    defaults are actually determined. It is a no-op when an explicit grid has
    already been supplied.

  • openmc_weight_windows_export() calls set_defaults() once per weight window
    before writing, so objects built through the C API whose particle type was
    never set do not export an empty energy grid.

  • WeightWindowsGenerator drops its trailing set_defaults() call, which is
    now redundant. This is the workaround the bug forced, so its removal is the
    clearest demonstration that the fix works.

  • The XML constructor assigns the particle type through set_particle_type()
    instead of writing particle_type_ directly, and drops its own trailing
    set_defaults(). Two side benefits: the XML path now gets the same
    neutron/photon validation as the C API path, and every existing XML-based
    weight window test exercises the new call site.

After this, set_defaults() has exactly two call sites -- set_particle_type()
and the export backstop -- so every path that assigns a particle type derives
its defaults the same way.

bounds_size() already treats an empty energy grid as one bin:

int num_energy_bins = energy_bounds_.size() > 0 ? energy_bounds_.size() - 1 : 1;

so set_mesh() continues to allocate a correctly shaped 1 x n_spatial bounds
tensor in the window between construction and the particle type being set.

Testing

New tests/unit_tests/weightwindows/test_ww_defaults.py, which initializes a
model with both neutron and photon data and calls simulation_init() so that
data::energy_min/max are actually populated:

  • Neutron and photon weight windows must get different default energy bounds.
    Identical bounds are the signature of the defaults being derived from the
    default particle type at construction.
  • An explicit energy_bounds grid survives a later set_particle_type().
  • Populated weight window bounds survive a later set_particle_type().

Notes

Found while reviewing #4057, but it is independent of that PR and reproduces on
develop. It may allow #4057 to drop its reset_energy_bounds() addition,
since a freshly constructed openmc.lib.WeightWindows will now pick up correct
defaults on its own once the particle type is assigned.

Checklist

  • I have performed a self-review of my own code
  • I have run clang-format (version 18) on any C++ source files (if applicable)
  • I have followed the style guidelines for Python source files (if applicable)
  • I have made corresponding changes to the documentation (if applicable)
  • I have added tests that prove my fix is effective or that my feature works (if applicable)

@GuySten
GuyStenforce-pushed the ww-particle-type-fix branch from aaf8210 to 78adf76CompareAugust 31, 2026 19:05
@GuySten
GuySten marked this pull request as ready for review August 31, 2026 19:54
@paulromano
paulromano enabled auto-merge (squash) September 1, 2026 13:26
@paulromano
paulromano merged commit c6f9187 into openmc-dev:developSep 1, 2026
16 checks passed
@GuySten
GuySten deleted the ww-particle-type-fix branch September 1, 2026 14:52
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@GuySten@paulromano
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix weight window energy defaults being derived before the particle type is known - #4091

Merged
paulromano merged 3 commits into
openmc-dev:developfrom
GuySten:ww-particle-type-fix
Sep 1, 2026
Merged

Fix weight window energy defaults being derived before the particle type is known#4091
paulromano merged 3 commits into
openmc-dev:developfrom
GuySten:ww-particle-type-fix

Conversation

@GuySten

@GuyStenGuySten commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fix weight window energy defaults being derived before the particle type is known

Scope: this affects weight windows created through the C API during a
simulation
without an explicit energy_bounds. Photon ones previously received
neutron default energy bounds; they now receive photon ones. Weight windows
defined in XML are unaffected, as are any with an explicit energy_bounds, as
are any created before openmc_simulation_init().

Problem

WeightWindows::set_defaults() derives the default energy grid from
data::energy_min/max for the object's particle type, and only does so when the
grid is empty:

voidWeightWindows::set_defaults()
{
if (energy_bounds_.size() == 0) {
int p_type = particle_type_.transport_index();
...
energy_bounds_.push_back(data::energy_min[p_type]);
energy_bounds_.push_back(data::energy_max[p_type]);
}
}

The WeightWindows(int32_t id) constructor, which is the one the C API and
openmc.lib use, called it immediately:

WeightWindows::WeightWindows(int32_t id)
{
index_ = variance_reduction::weight_windows.size();
set_id(id);
set_defaults(); // particle type, mesh and energy grid all unknown here
}

ParticleType default-constructs to PDG_NEUTRON, so this does not error. It
silently derives neutron bounds. set_particle_type() does not re-derive them,
and because the grid is no longer empty every subsequent set_defaults() call
is a no-op. So the early call does not merely compute the wrong answer, it
prevents the right one from ever being computed.

The XML constructor is not affected: it assigns particle_type_ before calling
set_defaults() at the end.

The window in which this is observable is narrower than it first appears.
data::energy_min/max are populated by initialize_data(), which runs from
openmc_simulation_init(), not openmc_init(). Before simulation
initialization both arrays hold their static defaults of 0.0 and INFTY for
every particle, so the neutron and photon defaults coincide and nothing is
visibly wrong. The bug bites weight windows constructed through the C API once a
simulation is running -- which is the regime weight window generation operates
in, and is why WeightWindowsGenerator needs its workaround.

Two things in the existing code point at this:

  • WeightWindowsGenerator::WeightWindowsGenerator calls set_defaults() again
    after set_mesh(), set_energy_bounds() and set_particle_type(). That is a
    workaround, and it only works because the generator constructs through a path
    where the grid is still empty.
  • The energy_bounds_.size() == 0 guard is what turns an early call into a
    permanent one.

MWE

importnumpyasnpimportopenmcimportopenmc.libwater=openmc.Material()
water.set_density('g/cm3', 1.0)
water.add_nuclide('H1', 2.0)
water.add_nuclide('O16', 1.0)
sphere=openmc.Sphere(r=10.0, boundary_type='vacuum')
model=openmc.Model()
model.geometry=openmc.Geometry([openmc.Cell(fill=water, region=-sphere)])
model.settings.run_mode='fixed source'model.settings.particles=100model.settings.batches=1model.settings.photon_transport=True# so photon data is loaded toomodel.export_to_model_xml()
defmake_ww(uid, particle):
"""Create a weight window through the C API, as openmc.lib does."""mesh=openmc.lib.RegularMesh()
mesh.dimension= (2, 2, 2)
mesh.set_parameters(lower_left=(-1.0, -1.0, -1.0),
upper_right=(1.0, 1.0, 1.0))
ww=openmc.lib.WeightWindows(uid)
ww.mesh=meshww.particle=particlereturnnp.asarray(ww.energy_bounds)
openmc.lib.init()
openmc.lib.simulation_init() # populates data::energy_min/maxtry:
neutron=make_ww(900, 'neutron')
photon=make_ww(901, 'photon')
finally:
openmc.lib.simulation_finalize()
openmc.lib.finalize()
print(f'\n neutron default energy bounds {neutron}')
print(f' photon default energy bounds {photon}')
ifnp.allclose(neutron, photon):
print('\n FAIL: the photon weight window has neutron energy bounds.')
print(' Photon data extends far beyond the neutron limit, so an upper')
print(' bound equal to the neutron one cannot be right.')
raiseSystemExit(1)
print('\n PASS: each particle got its own default energy range.')

Changes

  • WeightWindows(int32_t id) no longer calls set_defaults(). A comment
    explains why, so it is not reinstated.

  • set_particle_type() calls set_defaults(), which is the point at which the
    defaults are actually determined. It is a no-op when an explicit grid has
    already been supplied.

  • openmc_weight_windows_export() calls set_defaults() once per weight window
    before writing, so objects built through the C API whose particle type was
    never set do not export an empty energy grid.

  • WeightWindowsGenerator drops its trailing set_defaults() call, which is
    now redundant. This is the workaround the bug forced, so its removal is the
    clearest demonstration that the fix works.

  • The XML constructor assigns the particle type through set_particle_type()
    instead of writing particle_type_ directly, and drops its own trailing
    set_defaults(). Two side benefits: the XML path now gets the same
    neutron/photon validation as the C API path, and every existing XML-based
    weight window test exercises the new call site.

After this, set_defaults() has exactly two call sites -- set_particle_type()
and the export backstop -- so every path that assigns a particle type derives
its defaults the same way.

bounds_size() already treats an empty energy grid as one bin:

int num_energy_bins = energy_bounds_.size() > 0 ? energy_bounds_.size() - 1 : 1;

so set_mesh() continues to allocate a correctly shaped 1 x n_spatial bounds
tensor in the window between construction and the particle type being set.

Testing

New tests/unit_tests/weightwindows/test_ww_defaults.py, which initializes a
model with both neutron and photon data and calls simulation_init() so that
data::energy_min/max are actually populated:

  • Neutron and photon weight windows must get different default energy bounds.
    Identical bounds are the signature of the defaults being derived from the
    default particle type at construction.
  • An explicit energy_bounds grid survives a later set_particle_type().
  • Populated weight window bounds survive a later set_particle_type().

Notes

Found while reviewing #4057, but it is independent of that PR and reproduces on
develop. It may allow #4057 to drop its reset_energy_bounds() addition,
since a freshly constructed openmc.lib.WeightWindows will now pick up correct
defaults on its own once the particle type is assigned.

Checklist

  • I have performed a self-review of my own code
  • I have run clang-format (version 18) on any C++ source files (if applicable)
  • I have followed the style guidelines for Python source files (if applicable)
  • I have made corresponding changes to the documentation (if applicable)
  • I have added tests that prove my fix is effective or that my feature works (if applicable)

@GuySten
GuyStenforce-pushed the ww-particle-type-fix branch from aaf8210 to 78adf76CompareAugust 31, 2026 19:05
@GuySten
GuySten marked this pull request as ready for review August 31, 2026 19:54
@paulromano
paulromano enabled auto-merge (squash) September 1, 2026 13:26
@paulromano
paulromano merged commit c6f9187 into openmc-dev:developSep 1, 2026
16 checks passed
@GuySten
GuySten deleted the ww-particle-type-fix branch September 1, 2026 14:52
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@GuySten@paulromano
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix weight window energy defaults being derived before the particle type is known - #4091

Merged
paulromano merged 3 commits into
openmc-dev:developfrom
GuySten:ww-particle-type-fix
Sep 1, 2026
Merged

Fix weight window energy defaults being derived before the particle type is known#4091
paulromano merged 3 commits into
openmc-dev:developfrom
GuySten:ww-particle-type-fix

Conversation

@GuySten

@GuyStenGuySten commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fix weight window energy defaults being derived before the particle type is known

Scope: this affects weight windows created through the C API during a
simulation
without an explicit energy_bounds. Photon ones previously received
neutron default energy bounds; they now receive photon ones. Weight windows
defined in XML are unaffected, as are any with an explicit energy_bounds, as
are any created before openmc_simulation_init().

Problem

WeightWindows::set_defaults() derives the default energy grid from
data::energy_min/max for the object's particle type, and only does so when the
grid is empty:

voidWeightWindows::set_defaults()
{
if (energy_bounds_.size() == 0) {
int p_type = particle_type_.transport_index();
...
energy_bounds_.push_back(data::energy_min[p_type]);
energy_bounds_.push_back(data::energy_max[p_type]);
}
}

The WeightWindows(int32_t id) constructor, which is the one the C API and
openmc.lib use, called it immediately:

WeightWindows::WeightWindows(int32_t id)
{
index_ = variance_reduction::weight_windows.size();
set_id(id);
set_defaults(); // particle type, mesh and energy grid all unknown here
}

ParticleType default-constructs to PDG_NEUTRON, so this does not error. It
silently derives neutron bounds. set_particle_type() does not re-derive them,
and because the grid is no longer empty every subsequent set_defaults() call
is a no-op. So the early call does not merely compute the wrong answer, it
prevents the right one from ever being computed.

The XML constructor is not affected: it assigns particle_type_ before calling
set_defaults() at the end.

The window in which this is observable is narrower than it first appears.
data::energy_min/max are populated by initialize_data(), which runs from
openmc_simulation_init(), not openmc_init(). Before simulation
initialization both arrays hold their static defaults of 0.0 and INFTY for
every particle, so the neutron and photon defaults coincide and nothing is
visibly wrong. The bug bites weight windows constructed through the C API once a
simulation is running -- which is the regime weight window generation operates
in, and is why WeightWindowsGenerator needs its workaround.

Two things in the existing code point at this:

  • WeightWindowsGenerator::WeightWindowsGenerator calls set_defaults() again
    after set_mesh(), set_energy_bounds() and set_particle_type(). That is a
    workaround, and it only works because the generator constructs through a path
    where the grid is still empty.
  • The energy_bounds_.size() == 0 guard is what turns an early call into a
    permanent one.

MWE

importnumpyasnpimportopenmcimportopenmc.libwater=openmc.Material()
water.set_density('g/cm3', 1.0)
water.add_nuclide('H1', 2.0)
water.add_nuclide('O16', 1.0)
sphere=openmc.Sphere(r=10.0, boundary_type='vacuum')
model=openmc.Model()
model.geometry=openmc.Geometry([openmc.Cell(fill=water, region=-sphere)])
model.settings.run_mode='fixed source'model.settings.particles=100model.settings.batches=1model.settings.photon_transport=True# so photon data is loaded toomodel.export_to_model_xml()
defmake_ww(uid, particle):
"""Create a weight window through the C API, as openmc.lib does."""mesh=openmc.lib.RegularMesh()
mesh.dimension= (2, 2, 2)
mesh.set_parameters(lower_left=(-1.0, -1.0, -1.0),
upper_right=(1.0, 1.0, 1.0))
ww=openmc.lib.WeightWindows(uid)
ww.mesh=meshww.particle=particlereturnnp.asarray(ww.energy_bounds)
openmc.lib.init()
openmc.lib.simulation_init() # populates data::energy_min/maxtry:
neutron=make_ww(900, 'neutron')
photon=make_ww(901, 'photon')
finally:
openmc.lib.simulation_finalize()
openmc.lib.finalize()
print(f'\n neutron default energy bounds {neutron}')
print(f' photon default energy bounds {photon}')
ifnp.allclose(neutron, photon):
print('\n FAIL: the photon weight window has neutron energy bounds.')
print(' Photon data extends far beyond the neutron limit, so an upper')
print(' bound equal to the neutron one cannot be right.')
raiseSystemExit(1)
print('\n PASS: each particle got its own default energy range.')

Changes

  • WeightWindows(int32_t id) no longer calls set_defaults(). A comment
    explains why, so it is not reinstated.

  • set_particle_type() calls set_defaults(), which is the point at which the
    defaults are actually determined. It is a no-op when an explicit grid has
    already been supplied.

  • openmc_weight_windows_export() calls set_defaults() once per weight window
    before writing, so objects built through the C API whose particle type was
    never set do not export an empty energy grid.

  • WeightWindowsGenerator drops its trailing set_defaults() call, which is
    now redundant. This is the workaround the bug forced, so its removal is the
    clearest demonstration that the fix works.

  • The XML constructor assigns the particle type through set_particle_type()
    instead of writing particle_type_ directly, and drops its own trailing
    set_defaults(). Two side benefits: the XML path now gets the same
    neutron/photon validation as the C API path, and every existing XML-based
    weight window test exercises the new call site.

After this, set_defaults() has exactly two call sites -- set_particle_type()
and the export backstop -- so every path that assigns a particle type derives
its defaults the same way.

bounds_size() already treats an empty energy grid as one bin:

int num_energy_bins = energy_bounds_.size() > 0 ? energy_bounds_.size() - 1 : 1;

so set_mesh() continues to allocate a correctly shaped 1 x n_spatial bounds
tensor in the window between construction and the particle type being set.

Testing

New tests/unit_tests/weightwindows/test_ww_defaults.py, which initializes a
model with both neutron and photon data and calls simulation_init() so that
data::energy_min/max are actually populated:

  • Neutron and photon weight windows must get different default energy bounds.
    Identical bounds are the signature of the defaults being derived from the
    default particle type at construction.
  • An explicit energy_bounds grid survives a later set_particle_type().
  • Populated weight window bounds survive a later set_particle_type().

Notes

Found while reviewing #4057, but it is independent of that PR and reproduces on
develop. It may allow #4057 to drop its reset_energy_bounds() addition,
since a freshly constructed openmc.lib.WeightWindows will now pick up correct
defaults on its own once the particle type is assigned.

Checklist

  • I have performed a self-review of my own code
  • I have run clang-format (version 18) on any C++ source files (if applicable)
  • I have followed the style guidelines for Python source files (if applicable)
  • I have made corresponding changes to the documentation (if applicable)
  • I have added tests that prove my fix is effective or that my feature works (if applicable)

@GuySten
GuyStenforce-pushed the ww-particle-type-fix branch from aaf8210 to 78adf76CompareAugust 31, 2026 19:05
@GuySten
GuySten marked this pull request as ready for review August 31, 2026 19:54
@paulromano
paulromano enabled auto-merge (squash) September 1, 2026 13:26
@paulromano
paulromano merged commit c6f9187 into openmc-dev:developSep 1, 2026
16 checks passed
@GuySten
GuySten deleted the ww-particle-type-fix branch September 1, 2026 14:52
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@GuySten@paulromano
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Fix weight window energy defaults being derived before the particle type is known - #4091

Merged
paulromano merged 3 commits into
openmc-dev:developfrom
GuySten:ww-particle-type-fix
Sep 1, 2026
Merged

Fix weight window energy defaults being derived before the particle type is known#4091
paulromano merged 3 commits into
openmc-dev:developfrom
GuySten:ww-particle-type-fix

Conversation

@GuySten

@GuyStenGuySten commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Fix weight window energy defaults being derived before the particle type is known

Scope: this affects weight windows created through the C API during a
simulation
without an explicit energy_bounds. Photon ones previously received
neutron default energy bounds; they now receive photon ones. Weight windows
defined in XML are unaffected, as are any with an explicit energy_bounds, as
are any created before openmc_simulation_init().

Problem

WeightWindows::set_defaults() derives the default energy grid from
data::energy_min/max for the object's particle type, and only does so when the
grid is empty:

voidWeightWindows::set_defaults()
{
if (energy_bounds_.size() == 0) {
int p_type = particle_type_.transport_index();
...
energy_bounds_.push_back(data::energy_min[p_type]);
energy_bounds_.push_back(data::energy_max[p_type]);
}
}

The WeightWindows(int32_t id) constructor, which is the one the C API and
openmc.lib use, called it immediately:

WeightWindows::WeightWindows(int32_t id)
{
index_ = variance_reduction::weight_windows.size();
set_id(id);
set_defaults(); // particle type, mesh and energy grid all unknown here
}

ParticleType default-constructs to PDG_NEUTRON, so this does not error. It
silently derives neutron bounds. set_particle_type() does not re-derive them,
and because the grid is no longer empty every subsequent set_defaults() call
is a no-op. So the early call does not merely compute the wrong answer, it
prevents the right one from ever being computed.

The XML constructor is not affected: it assigns particle_type_ before calling
set_defaults() at the end.

The window in which this is observable is narrower than it first appears.
data::energy_min/max are populated by initialize_data(), which runs from
openmc_simulation_init(), not openmc_init(). Before simulation
initialization both arrays hold their static defaults of 0.0 and INFTY for
every particle, so the neutron and photon defaults coincide and nothing is
visibly wrong. The bug bites weight windows constructed through the C API once a
simulation is running -- which is the regime weight window generation operates
in, and is why WeightWindowsGenerator needs its workaround.

Two things in the existing code point at this:

  • WeightWindowsGenerator::WeightWindowsGenerator calls set_defaults() again
    after set_mesh(), set_energy_bounds() and set_particle_type(). That is a
    workaround, and it only works because the generator constructs through a path
    where the grid is still empty.
  • The energy_bounds_.size() == 0 guard is what turns an early call into a
    permanent one.

MWE

importnumpyasnpimportopenmcimportopenmc.libwater=openmc.Material()
water.set_density('g/cm3', 1.0)
water.add_nuclide('H1', 2.0)
water.add_nuclide('O16', 1.0)
sphere=openmc.Sphere(r=10.0, boundary_type='vacuum')
model=openmc.Model()
model.geometry=openmc.Geometry([openmc.Cell(fill=water, region=-sphere)])
model.settings.run_mode='fixed source'model.settings.particles=100model.settings.batches=1model.settings.photon_transport=True# so photon data is loaded toomodel.export_to_model_xml()
defmake_ww(uid, particle):
"""Create a weight window through the C API, as openmc.lib does."""mesh=openmc.lib.RegularMesh()
mesh.dimension= (2, 2, 2)
mesh.set_parameters(lower_left=(-1.0, -1.0, -1.0),
upper_right=(1.0, 1.0, 1.0))
ww=openmc.lib.WeightWindows(uid)
ww.mesh=meshww.particle=particlereturnnp.asarray(ww.energy_bounds)
openmc.lib.init()
openmc.lib.simulation_init() # populates data::energy_min/maxtry:
neutron=make_ww(900, 'neutron')
photon=make_ww(901, 'photon')
finally:
openmc.lib.simulation_finalize()
openmc.lib.finalize()
print(f'\n neutron default energy bounds {neutron}')
print(f' photon default energy bounds {photon}')
ifnp.allclose(neutron, photon):
print('\n FAIL: the photon weight window has neutron energy bounds.')
print(' Photon data extends far beyond the neutron limit, so an upper')
print(' bound equal to the neutron one cannot be right.')
raiseSystemExit(1)
print('\n PASS: each particle got its own default energy range.')

Changes

  • WeightWindows(int32_t id) no longer calls set_defaults(). A comment
    explains why, so it is not reinstated.

  • set_particle_type() calls set_defaults(), which is the point at which the
    defaults are actually determined. It is a no-op when an explicit grid has
    already been supplied.

  • openmc_weight_windows_export() calls set_defaults() once per weight window
    before writing, so objects built through the C API whose particle type was
    never set do not export an empty energy grid.

  • WeightWindowsGenerator drops its trailing set_defaults() call, which is
    now redundant. This is the workaround the bug forced, so its removal is the
    clearest demonstration that the fix works.

  • The XML constructor assigns the particle type through set_particle_type()
    instead of writing particle_type_ directly, and drops its own trailing
    set_defaults(). Two side benefits: the XML path now gets the same
    neutron/photon validation as the C API path, and every existing XML-based
    weight window test exercises the new call site.

After this, set_defaults() has exactly two call sites -- set_particle_type()
and the export backstop -- so every path that assigns a particle type derives
its defaults the same way.

bounds_size() already treats an empty energy grid as one bin:

int num_energy_bins = energy_bounds_.size() > 0 ? energy_bounds_.size() - 1 : 1;

so set_mesh() continues to allocate a correctly shaped 1 x n_spatial bounds
tensor in the window between construction and the particle type being set.

Testing

New tests/unit_tests/weightwindows/test_ww_defaults.py, which initializes a
model with both neutron and photon data and calls simulation_init() so that
data::energy_min/max are actually populated:

  • Neutron and photon weight windows must get different default energy bounds.
    Identical bounds are the signature of the defaults being derived from the
    default particle type at construction.
  • An explicit energy_bounds grid survives a later set_particle_type().
  • Populated weight window bounds survive a later set_particle_type().

Notes

Found while reviewing #4057, but it is independent of that PR and reproduces on
develop. It may allow #4057 to drop its reset_energy_bounds() addition,
since a freshly constructed openmc.lib.WeightWindows will now pick up correct
defaults on its own once the particle type is assigned.

Checklist

  • I have performed a self-review of my own code
  • I have run clang-format (version 18) on any C++ source files (if applicable)
  • I have followed the style guidelines for Python source files (if applicable)
  • I have made corresponding changes to the documentation (if applicable)
  • I have added tests that prove my fix is effective or that my feature works (if applicable)

@GuySten
GuyStenforce-pushed the ww-particle-type-fix branch from aaf8210 to 78adf76CompareAugust 31, 2026 19:05
@GuySten
GuySten marked this pull request as ready for review August 31, 2026 19:54
@paulromano
paulromano enabled auto-merge (squash) September 1, 2026 13:26
@paulromano
paulromano merged commit c6f9187 into openmc-dev:developSep 1, 2026
16 checks passed
@GuySten
GuySten deleted the ww-particle-type-fix branch September 1, 2026 14:52
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@GuySten@paulromano