Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
8b1b8dc
add properties file path to settings
gonuke Feb 16, 2026
8713fe9
add flags to indicate which properties to read from properties file
gonuke Feb 18, 2026
76c258e
Add new XML reading method to check existence of file path
gonuke Feb 18, 2026
0a7636b
add flags for which properties to read from file
gonuke Feb 18, 2026
9942d78
read properties file after other initialization
gonuke Feb 18, 2026
886c641
propagate boolean for property reads into C API and related python calls
gonuke Feb 18, 2026
fd1594a
propagate read properties flags to cells
gonuke Feb 19, 2026
5852ec0
propagate read properties flags to material
gonuke Feb 19, 2026
a9b3f28
add new booleans to header
gonuke Feb 19, 2026
1b78b84
apply clang formatting
gonuke Feb 19, 2026
cc10db6
fix formatting
gonuke Feb 20, 2026
cf9a27e
manually fix clang-format inconsistency
gonuke Feb 20, 2026
4f796fb
fix string to char* conversion
gonuke Feb 20, 2026
6a862b6
declare new XML reader
gonuke Feb 20, 2026
cfcbfc2
add missing semi-colon and guess at formatting
gonuke Feb 20, 2026
fb22bba
add default value to signature
gonuke Feb 20, 2026
9a20d6e
clang-format-15
gonuke Feb 20, 2026
015df87
add file_utils to xml_interface
gonuke Feb 20, 2026
d8c92fd
back away from new XML reader for valid file paths
gonuke Feb 21, 2026
99ca76f
Improve documentation
gonuke Feb 21, 2026
a86f00e
switch to C++ error handling
gonuke Feb 21, 2026
5474147
cleanup lib call tests
gonuke Feb 22, 2026
902e749
add temporary file for testing
gonuke Feb 22, 2026
a62b545
don't convert back to string in test
gonuke Feb 22, 2026
c7a8090
Don't generate file, but do test against Path
gonuke Feb 22, 2026
ebbdc1a
abbreviate names
gonuke Feb 23, 2026
6355305
update documentation
gonuke Feb 23, 2026
19abae7
cleanup comments and error messages
gonuke Feb 23, 2026
1726602
simplify early exit logic
gonuke Feb 23, 2026
0e9eea3
style update
gonuke Feb 23, 2026
41317cf
read the properties in the right place
gonuke Mar 4, 2026
cde42e1
unwind fine-grained import booleans
gonuke Mar 5, 2026
17aa7ef
Adding manual test for properties load via settings
pshriwise Mar 8, 2026
86ec40c
Abandoning the test fixture as it adds complexity
pshriwise Mar 8, 2026
14df979
Altering TemporarySession intracomm handling for cleaner testing
pshriwise Mar 8, 2026
bcea94f
Test code cleanup
pshriwise Mar 8, 2026
6ad63cd
Merge pull request #1 from pshriwise/properties_in_settings
gonuke Mar 8, 2026
f74b535
Doc fix
paulromano Mar 13, 2026
eb59f5a
Merge branch 'develop' into pr/gonuke/3808
paulromano Mar 13, 2026
ab73212
Use properties_file consistently, small fixes
paulromano Mar 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/source/io_formats/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,18 @@ generator during generation of colors in plots.

*Default*: 1

.. _properties_file:

-----------------------------
``<properties_file>`` Element
-----------------------------

The ``properties_file`` element has no attributes and contains the path to a
properties HDF5 file to load cell temperatures/densities and material
densities.

*Default*: None

---------------------
``<ptables>`` Element
---------------------
Expand Down
2 changes: 2 additions & 0 deletions include/openmc/settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ extern std::string path_sourcepoint; //!< path to a source file
extern std::string path_statepoint; //!< path to a statepoint file
extern std::string weight_windows_file; //!< Location of weight window file to
//!< load on simulation initialization
extern std::string properties_file; //!< Location of properties file to
//!< load on simulation initialization

// This is required because the c_str() may not be the first thing in
// std::string. Sometimes it is, but it seems libc++ may not be like that
Expand Down
4 changes: 2 additions & 2 deletions openmc/lib/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -693,8 +693,8 @@ def __init__(self, model=None, cwd=None, **init_kwargs):
self.model = model

# Determine MPI intercommunicator
self.init_kwargs.setdefault('intracomm', comm)
self.comm = self.init_kwargs['intracomm']
self.comm = self.init_kwargs.get('intracomm') or comm
self.init_kwargs['intracomm'] = self.comm

def __enter__(self):
"""Initialize the OpenMC library in a temporary directory."""
Expand Down
29 changes: 29 additions & 0 deletions openmc/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,9 @@ class Settings:
Initial seed for randomly generated plot colors.
ptables : bool
Determine whether probability tables are used.
properties_file : PathLike
Location of the properties file to load cell temperatures/densities
and material densities
random_ray : dict
Options for configuring the random ray solver. Acceptable keys are:

Expand Down Expand Up @@ -409,6 +412,7 @@ def __init__(self, **kwargs):
self._atomic_relaxation = None
self._plot_seed = None
self._ptables = None
self._properties_file = None
self._uniform_source_sampling = None
self._seed = None
self._stride = None
Expand Down Expand Up @@ -1067,6 +1071,18 @@ def temperature(self, temperature: dict):

self._temperature = temperature

@property
def properties_file(self) -> PathLike | None:
return self._properties_file

@properties_file.setter
def properties_file(self, value: PathLike | None):
if value is None:
self._properties_file = None
else:
cv.check_type('properties file', value, PathLike)
self._properties_file = input_path(value)

@property
def trace(self) -> Iterable:
return self._trace
Expand Down Expand Up @@ -1772,6 +1788,12 @@ def _create_temperature_subelements(self, root):
else:
element.text = str(value)

def _create_properties_file_element(self, root):
if self.properties_file is not None:
element = ET.Element("properties_file")
element.text = str(self.properties_file)
Copy link
Contributor

@lewisgross1296 lewisgross1296 Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something went wrong in the latest update @gonuke, I think it's here (i.e there's nothing called properties_file in the XML now.) I just re-tried and my settings looks like this

  <settings>
    <properties>/home/lgross/cnerg/gcmr/multiphysics_depletion/BOL_mp_depl/180/properties.h5</properties>
  </settings>

which leads to this behavior

                                %%%%%%%%%%%%%%%
                         %%%%%%%%%%%%%%%%%%%%%%%%
                      %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
                                  %%%%%%%%%%%%%%%%%%%%%%%%
                                   %%%%%%%%%%%%%%%%%%%%%%%%
               ###############      %%%%%%%%%%%%%%%%%%%%%%%%
              ##################     %%%%%%%%%%%%%%%%%%%%%%%
              ###################     %%%%%%%%%%%%%%%%%%%%%%%
              ####################     %%%%%%%%%%%%%%%%%%%%%%
              #####################     %%%%%%%%%%%%%%%%%%%%%
              ######################     %%%%%%%%%%%%%%%%%%%%
              #######################     %%%%%%%%%%%%%%%%%%
               #######################     %%%%%%%%%%%%%%%%%
               ######################     %%%%%%%%%%%%%%%%%
                ####################     %%%%%%%%%%%%%%%%%
                  #################     %%%%%%%%%%%%%%%%%
                   ###############     %%%%%%%%%%%%%%%%
                     ############     %%%%%%%%%%%%%%%
                        ########     %%%%%%%%%%%%%%
                                    %%%%%%%%%%%

               | The OpenMC Monte Carlo Code
     Copyright | 2011-2025 MIT, UChicago Argonne LLC, and contributors
       License | https://docs.openmc.org/en/latest/license.html
       Version | 0.15.3-dev282
   Commit Hash | 0e9eea39a8243b2223bb345d01fd091906a629f8
     Date/Time | 2026-03-05 10:59:29
 MPI Processes | 1
OpenMP Threads | 96

Reading model XML file 'model.xml' ...
ERROR: Node "filepath" is not a member of the "properties" XML node

I think you either want to use element.setAttribute to set filepath or have the code just look for the properties element

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm kind of confused why the test didn't fail though

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually

  // read properties from file
  if (check_for_node(root, "properties")) {
    properties_file = get_node_value(root, "properties");
    if (!file_exists(properties_file)) {
      fatal_error(fmt::format("File '{}' does not exist.", properties_file));
    }
  }

The test looks for a node called properties, which is why it works. Perhaps now we just call it properties instead of properties_file now that we only supply a file

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is your executable up to date?

Copy link
Contributor

@lewisgross1296 lewisgross1296 Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nvm I forgot to do make install after make and before installing the python api

EDIT: It's running now... I'll report back when it completes and hopefully it will be statistically equivalent to cardinal / OpenMC depletion. Also it printed

 Importing properties from
 /home/lgross/cnerg/gcmr/multiphysics_depletion/BOL_mp_depl/180/properties.h5...

Copy link
Contributor

@lewisgross1296 lewisgross1296 Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking much better now!
New transport only (latest commit) below

 ============================>     RESULTS     <============================

 k-effective (Collision)     = 1.03989 +/- 0.00048
 k-effective (Track-length)  = 1.04080 +/- 0.00063
 k-effective (Absorption)    = 1.04017 +/- 0.00040
 Combined k-effective        = 1.04029 +/- 0.00039
 Leakage Fraction            = 0.09232 +/- 0.00015

Depletion first transport with properties below

 ============================>     RESULTS     <============================

 k-effective (Collision)     = 1.03927 +/- 0.00053
 k-effective (Track-length)  = 1.03944 +/- 0.00065
 k-effective (Absorption)    = 1.03960 +/- 0.00045
 Combined k-effective        = 1.03954 +/- 0.00045
 Leakage Fraction            = 0.09238 +/- 0.00014

and Cardinal gave us

============================>     RESULTS     <============================

 k-effective (Collision)     = 1.04085 +/- 0.00054
 k-effective (Track-length)  = 1.04055 +/- 0.00065
 k-effective (Absorption)    = 1.04052 +/- 0.00046
 Combined k-effective        = 1.04055 +/- 0.00044
 Leakage Fraction            = 0.09225 +/- 0.00013

If you use 1 sigma, depletion is slightly outside the same CI as the other two, but 2 sigma and they all overlap. However, it maybe makes sense that depletion is more different than the other two, as there's more nuclides loaded (small statistical shifts in sampling) and the other two have the exact same model.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a clearer test would be running this entirely outside of Cardinal and compare:

  • one simulation with flat temperatures very different from the values in the properties.h5
  • one simulation with the propreties.h5
    and show that they are very different.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't need to be very long running for this

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I agree (the above results took very long to run), so I wasn't pitching it for a test. Was just confirming that the properties generated by the 180 drum case now agrees between Cardinal, standalone transport using model.xml, and depletion first step (which is how we noticed that the model.xml version wasn't loading properties correctly).

@pshriwise do you think we should add a regression test that computes eigenvalues and compares cases with different / the same properties? I think Paul's idea for a test could work well if we use openmc.lib.export_properties() to make a properties file for testing. While the eigenvalues might change over time due to floating point/OpenMC code updates, the data in the properties file will always be the same. Perhaps a UO2 cylinder divided into two axial regions: 300K and 600K? (I could make a PR into Paul's branch, if so).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't mean to dip into formal "testing" but that also makes sense...

I guess I was just thinking it would be a clearer demonstration of the capability than ensuring it matches Cardinal. I guess it would not necessarily demonstrate that it was mapping the temperatures to the correct cells, if we only looked for it to be different from a case without....

root.append(element)

def _create_trace_subelement(self, root):
if self._trace is not None:
element = ET.SubElement(root, "trace")
Expand Down Expand Up @@ -2284,6 +2306,11 @@ def _temperature_from_xml_element(self, root):
if text is not None:
self.temperature['multipole'] = text in ('true', '1')

def _properties_file_from_xml_element(self, root):
text = get_text(root, 'properties_file')
if text is not None:
self.properties_file = text

def _trace_from_xml_element(self, root):
text = get_elem_list(root, "trace", int)
if text is not None:
Expand Down Expand Up @@ -2522,6 +2549,7 @@ def to_xml_element(self, mesh_memo=None):
self._create_ifp_n_generation_subelement(element)
self._create_tabular_legendre_subelements(element)
self._create_temperature_subelements(element)
self._create_properties_file_element(element)
self._create_trace_subelement(element)
self._create_track_subelement(element)
self._create_ufs_mesh_subelement(element, mesh_memo)
Expand Down Expand Up @@ -2639,6 +2667,7 @@ def from_xml_element(cls, elem, meshes=None):
settings._ifp_n_generation_from_xml_element(elem)
settings._tabular_legendre_from_xml_element(elem)
settings._temperature_from_xml_element(elem)
settings._properties_file_from_xml_element(elem)
settings._trace_from_xml_element(elem)
settings._track_from_xml_element(elem)
settings._ufs_mesh_from_xml_element(elem, meshes)
Expand Down
1 change: 1 addition & 0 deletions src/finalize.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ int openmc_finalize()
settings::temperature_multipole = false;
settings::temperature_range = {0.0, 0.0};
settings::temperature_tolerance = 10.0;
settings::properties_file.clear();
settings::trigger_on = false;
settings::trigger_predict = false;
settings::trigger_batch_interval = 1;
Expand Down
4 changes: 4 additions & 0 deletions src/initialize.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ int openmc_init(int argc, char* argv[], const void* intracomm)
if (!read_model_xml())
read_separate_xml_files();

if (!settings::properties_file.empty()) {
openmc_properties_import(settings::properties_file.c_str());
}

// Reset locale to previous state
if (std::setlocale(LC_ALL, prev_locale.c_str()) == NULL) {
fatal_error("Cannot reset locale.");
Expand Down
9 changes: 9 additions & 0 deletions src/settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ std::string path_sourcepoint;
std::string path_statepoint;
const char* path_statepoint_c {path_statepoint.c_str()};
std::string weight_windows_file;
std::string properties_file;

int32_t n_inactive {0};
int32_t max_lost_particles {10};
Expand Down Expand Up @@ -751,6 +752,14 @@ void read_settings_xml(pugi::xml_node root)
}
}

// read properties from file
if (check_for_node(root, "properties_file")) {
properties_file = get_node_value(root, "properties_file");
if (!file_exists(properties_file)) {
fatal_error(fmt::format("File '{}' does not exist.", properties_file));
}
}

// Particle trace
if (check_for_node(root, "trace")) {
auto temp = get_node_array<int64_t>(root, "trace");
Expand Down
72 changes: 70 additions & 2 deletions tests/unit_tests/test_settings.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
from pathlib import Path

import h5py
import pytest

import openmc
import openmc.lib
import openmc.stats


def test_export_to_xml(run_in_tmpdir):

tmp_properties_file = 'properties_test.h5'

s = openmc.Settings(run_mode='fixed source', batches=1000, seed=17)
s.generations_per_batch = 10
s.inactive = 100
Expand All @@ -22,7 +31,7 @@ def test_export_to_xml(run_in_tmpdir):
s.surf_source_read = {'path': 'surface_source_1.h5'}
s.surf_source_write = {'surface_ids': [2], 'max_particles': 200}
s.surface_grazing_ratio = 0.7
s.surface_grazing_cutoff = 0.1
s.surface_grazing_cutoff = 0.1
s.confidence_intervals = True
s.ptables = True
s.plot_seed = 100
Expand All @@ -45,6 +54,7 @@ def test_export_to_xml(run_in_tmpdir):
s.tabular_legendre = {'enable': True, 'num_points': 50}
s.temperature = {'default': 293.6, 'method': 'interpolation',
'multipole': True, 'range': (200., 1000.)}
s.properties_file = tmp_properties_file
s.trace = (10, 1, 20)
s.track = [(1, 1, 1), (2, 1, 1)]
s.ufs_mesh = mesh
Expand Down Expand Up @@ -88,6 +98,7 @@ def test_export_to_xml(run_in_tmpdir):
# Make sure exporting XML works
s.export_to_xml()


# Generate settings from XML
s = openmc.Settings.from_xml()
assert s.run_mode == 'fixed source'
Expand All @@ -111,7 +122,7 @@ def test_export_to_xml(run_in_tmpdir):
assert s.surf_source_read['path'].name == 'surface_source_1.h5'
assert s.surf_source_write == {'surface_ids': [2], 'max_particles': 200}
assert s.surface_grazing_ratio == 0.7
assert s.surface_grazing_cutoff == 0.1
assert s.surface_grazing_cutoff == 0.1
assert s.confidence_intervals
assert s.ptables
assert s.plot_seed == 100
Expand All @@ -134,6 +145,7 @@ def test_export_to_xml(run_in_tmpdir):
assert s.tabular_legendre == {'enable': True, 'num_points': 50}
assert s.temperature == {'default': 293.6, 'method': 'interpolation',
'multipole': True, 'range': [200., 1000.]}
assert s.properties_file == Path(tmp_properties_file)
assert s.trace == [10, 1, 20]
assert s.track == [(1, 1, 1), (2, 1, 1)]
assert isinstance(s.ufs_mesh, openmc.RegularMesh)
Expand Down Expand Up @@ -178,3 +190,59 @@ def test_export_to_xml(run_in_tmpdir):
assert s.max_secondaries == 1_000_000
assert s.source_rejection_fraction == 0.01
assert s.free_gas_threshold == 800.0


def test_properties_file_load(tmp_path, mpi_intracomm):
model = openmc.examples.pwr_assembly()

# Session 1: export a structurally valid properties file via the C++ API,
# then collect the cell/material structure so we can patch it with h5py.
cell_instances = {} # {cell_id: n_instances} — material cells only
mat_densities = {} # {mat_id: original atom/b-cm density}

props_path = tmp_path / 'properties.h5'
with openmc.lib.TemporarySession(model, intracomm=mpi_intracomm):
openmc.lib.export_properties(str(props_path))
for cell_id, cell in openmc.lib.cells.items():
try:
cell.fill # raises NotImplementedError for non-material cells
cell_instances[cell_id] = cell.num_instances
except NotImplementedError:
pass
for mat_id, mat in openmc.lib.materials.items():
mat_densities[mat_id] = mat.get_density('atom/b-cm')

assert any(n > 1 for n in cell_instances.values())

# Patch the exported properties file overwriting temperatures
# with per-instance values and scale material atom densities.
density_factor = 0.75
with h5py.File(props_path, 'r+') as f:
cells_grp = f['geometry/cells']
for cell_id, n in cell_instances.items():
cell_grp = cells_grp[f'cell {cell_id}']
del cell_grp['temperature']
cell_grp.create_dataset(
'temperature', data=[500.0 + 5.0 * i for i in range(n)]
)

for mat_id, orig_density in mat_densities.items():
f['materials'][f'material {mat_id}'].attrs['atom_density'] = \
orig_density * density_factor

# now apply the newly patched properties file using the settings
# and load the model again, checking that the new temperature and
# density values match those in the new file
model.settings.properties_file = props_path

with openmc.lib.TemporarySession(model, intracomm=mpi_intracomm):
for cell_id, n in cell_instances.items():
cell = openmc.lib.cells[cell_id]
for i in range(n):
assert cell.get_temperature(i) == pytest.approx(500.0 + 5.0 * i)

for mat_id, orig_density in mat_densities.items():
mat = openmc.lib.materials[mat_id]
assert mat.get_density('atom/b-cm') == pytest.approx(
orig_density * density_factor, rel=1e-5
)
Loading