Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ def test_my_feature():
harness.main()
```

**Workflow**: Create `test.py` and `__init__.py` in `tests/regression_tests/my_test/`, run `pytest --update` to generate reference files (`inputs_true.dat`, `results_true.dat`, etc.), then verify with `pytest` without `--update`. Test results should be generated with a debug build (`-DCMAKE_BUILD_TYPE=Debug`)
**Workflow**: Create `test.py` and `__init__.py` in `tests/regression_tests/my_test/`, run `pytest --update` to generate reference files (`inputs_true.dat`, `results_true.dat`, etc.), then verify with `pytest` without `--update`. Test results should be generated with `-DOPENMC_STRICT_FP=on` to ensure reproducibility across platforms and optimization levels.

**Critical**: When modifying OpenMC code, regenerate affected test references with `pytest --update` and commit updated reference files.

Expand Down
38 changes: 38 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ option(OPENMC_USE_LIBMESH "Enable support for libMesh unstructured mesh tall
option(OPENMC_USE_MPI "Enable MPI" OFF)
option(OPENMC_USE_UWUW "Enable UWUW" OFF)
option(OPENMC_FORCE_VENDORED_LIBS "Explicitly use submodules defined in 'vendor'" OFF)
option(OPENMC_STRICT_FP "Set strict floating point flags (improves test portability)"OFF)

message(STATUS "OPENMC_USE_OPENMP ${OPENMC_USE_OPENMP}")
message(STATUS "OPENMC_BUILD_TESTS ${OPENMC_BUILD_TESTS}")
Expand All @@ -48,6 +49,7 @@ message(STATUS "OPENMC_USE_LIBMESH ${OPENMC_USE_LIBMESH}")
message(STATUS "OPENMC_USE_MPI ${OPENMC_USE_MPI}")
message(STATUS "OPENMC_USE_UWUW ${OPENMC_USE_UWUW}")
message(STATUS "OPENMC_FORCE_VENDORED_LIBS ${OPENMC_FORCE_VENDORED_LIBS}")
message(STATUS "OPENMC_STRICT_FP ${OPENMC_STRICT_FP}")

# Warnings for deprecated options
foreach(OLD_OPT IN ITEMS "openmp" "profile" "coverage" "dagmc" "libmesh")
Expand Down Expand Up @@ -89,6 +91,19 @@ if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "Choose the type of build" FORCE)
endif()

#===============================================================================
# When STRICT_FP is enabled, remove NDEBUG from RelWithDebInfo flags so that
# assert() remains active. CMake normally adds -DNDEBUG for both Release and
# RelWithDebInfo, which disables C/C++ assert() statements.
#===============================================================================

if(OPENMC_STRICT_FP)
foreach(FLAG_VAR CMAKE_CXX_FLAGS_RELWITHDEBINFO CMAKE_C_FLAGS_RELWITHDEBINFO)
string(REPLACE "-DNDEBUG" "" ${FLAG_VAR} "${${FLAG_VAR}}")
string(REPLACE "/DNDEBUG" "" ${FLAG_VAR} "${${FLAG_VAR}}")
endforeach()
endif()

#===============================================================================
# OpenMP for shared-memory parallelism (and GPU support some day!)
#===============================================================================
Expand Down Expand Up @@ -193,6 +208,26 @@ endif()
# Set compile/link flags based on which compiler is being used
#===============================================================================

# When OPENMC_STRICT_FP is enabled, disable compiler optimizations that change
# floating-point results relative to -O0, improving cross-platform and
# cross-optimization-level reproducibility for regression testing:
# -ffp-contract=off Prevents FMA contraction (fused multiply-add changes rounding)
# -fno-builtin Prevents replacing math function calls (pow, exp, log, etc.)
# with builtin versions that may differ from libm
# By default (OFF), the compiler is free to use all optimizations for best
# performance.
if(OPENMC_STRICT_FP)
include(CheckCXXCompilerFlag)
check_cxx_compiler_flag(-ffp-contract=off SUPPORTS_FP_CONTRACT_OFF)
if(SUPPORTS_FP_CONTRACT_OFF)
list(APPEND cxxflags -ffp-contract=off)
endif()
check_cxx_compiler_flag(-fno-builtin SUPPORTS_NO_BUILTIN)
if(SUPPORTS_NO_BUILTIN)
list(APPEND cxxflags -fno-builtin)
endif()
endif()

# Skip for Visual Studio which has its own configurations through GUI
if(NOT MSVC)

Expand Down Expand Up @@ -555,6 +590,9 @@ endif()
if (OPENMC_ENABLE_COVERAGE)
target_compile_definitions(libopenmc PRIVATE COVERAGEBUILD)
endif()
if (OPENMC_STRICT_FP)
target_compile_definitions(libopenmc PRIVATE OPENMC_STRICT_FP_BUILD)
endif()

#===============================================================================
# openmc executable
Expand Down
8 changes: 5 additions & 3 deletions docs/source/devguide/tests.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,11 @@ make sure you have satisfied all the prerequisites above. After you have done
that, consider the following:

- When building OpenMC, make sure you run CMake with
``-DCMAKE_BUILD_TYPE=Debug``. Building with a release build will result in
some test failures due to differences in which compiler optimizations are
used.
``-DOPENMC_STRICT_FP=on``. This prevents the compiler from applying
floating-point optimizations (such as replacing math library calls with
builtins or contracting multiply-add into FMA instructions) that can produce
bit-level differences across platforms and optimization levels. Any
``CMAKE_BUILD_TYPE`` can be used.
- Because tallies involve the sum of many floating point numbers, the
non-associativity of floating point numbers can result in different answers
especially when the number of threads is high (different order of operations).
Expand Down
19 changes: 18 additions & 1 deletion docs/source/usersguide/install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,20 @@ OPENMC_USE_MPI
options, please see the `FindMPI.cmake documentation
<https://cmake.org/cmake/help/latest/module/FindMPI.html>`_.

.. _cmake_strict_fp:

OPENMC_STRICT_FP
Disables compiler optimizations that change floating-point results relative to
unoptimized builds, improving cross-platform and cross-optimization-level
reproducibility. This disables FMA contraction (``-ffp-contract=off``) and
compiler builtin replacements of math functions like ``pow``, ``exp``, ``log``
(``-fno-builtin``). It also keeps C/C++ assertions active by removing the
``-DNDEBUG`` flag from ``RelWithDebInfo`` builds. Without this flag, these
optimizations can produce bit-level differences across platforms, compilers,
and optimization levels. This option should be used when running the test
suite. By default (off), the compiler is free to use all optimizations for
best performance. (Default: off)

OPENMC_FORCE_VENDORED_LIBS
Forces OpenMC to use the submodules located in the vendor directory, as
opposed to searching the system for already installed versions of those
Expand Down Expand Up @@ -415,7 +429,10 @@ Release

RelWithDebInfo
(Default if no type is specified.) Enable optimization and debug. On most
platforms/compilers, this is equivalent to `-O2 -g`.
platforms/compilers, this is equivalent to `-O2 -g`. When
:ref:`OPENMC_STRICT_FP <cmake_strict_fp>` is enabled, OpenMC removes the
``-DNDEBUG`` flag that CMake normally adds for this build type, so that
C/C++ assertions remain active.

Example of configuring for Debug mode:

Expand Down
2 changes: 2 additions & 0 deletions include/openmc/output.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

namespace openmc {

extern "C" const bool STRICT_FP_ENABLED;

//! \brief Display the main title banner as well as information about the
//! program developers, version, and date/time which the problem was run.
void title();
Expand Down
3 changes: 3 additions & 0 deletions openmc/lib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ def _libmesh_enabled():
def _uwuw_enabled():
return c_bool.in_dll(_dll, "UWUW_ENABLED").value

def _strict_fp_enabled():
return c_bool.in_dll(_dll, "STRICT_FP_ENABLED").value


from .error import *
from .core import *
Expand Down
11 changes: 11 additions & 0 deletions src/output.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@

namespace openmc {

#ifdef OPENMC_STRICT_FP_BUILD
const bool STRICT_FP_ENABLED = true;
#else
const bool STRICT_FP_ENABLED = false;
#endif

//==============================================================================

void title()
Expand Down Expand Up @@ -317,6 +323,7 @@ void print_build_info()
std::string coverage(n);
std::string mcpl(n);
std::string uwuw(n);
std::string strict_fp(n);

#ifdef PHDF5
phdf5 = y;
Expand Down Expand Up @@ -345,6 +352,9 @@ void print_build_info()
#ifdef OPENMC_UWUW_ENABLED
uwuw = y;
#endif
#ifdef OPENMC_STRICT_FP_BUILD
strict_fp = y;
#endif

// Wraps macro variables in quotes
#define STRINGIFY(x) STRINGIFY2(x)
Expand All @@ -363,6 +373,7 @@ void print_build_info()
fmt::print("Coverage testing: {}\n", coverage);
fmt::print("Profiling flags: {}\n", profiling);
fmt::print("UWUW support: {}\n", uwuw);
fmt::print("Strict FP: {}\n", strict_fp);
}
}

Expand Down
64 changes: 64 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,51 @@
import os
import hashlib
import pytest
import openmc
import openmc.lib

from tests.regression_tests import config as regression_config

# MD5 hash of the official NNDC HDF5 cross_sections.xml file.
# Generated via: md5sum /path/to/nndc_hdf5/cross_sections.xml
_NNDC_XS_MD5 = "2d00773012eda670bc9f95d96a31c989"

# Collected during pytest_configure, displayed at start and end of session
_environment_warnings = []


def _check_build_environment():
"""Check STRICT_FP and cross section data, collecting any warnings."""
if not openmc.lib._strict_fp_enabled():
_environment_warnings.append(
"OpenMC was NOT built with -DOPENMC_STRICT_FP=on. "
"Regression test results may not match reference values due to "
"compiler floating-point optimizations. Rebuild with "
"-DOPENMC_STRICT_FP=on for reproducible results."
)

xs_path = os.environ.get("OPENMC_CROSS_SECTIONS")
if not xs_path:
_environment_warnings.append(
"OPENMC_CROSS_SECTIONS environment variable is not set. "
"Regression tests require the NNDC HDF5 cross section data."
)
elif not os.path.isfile(xs_path):
_environment_warnings.append(
f"OPENMC_CROSS_SECTIONS ({xs_path}) is not a valid file path. "
"Regression tests require the NNDC HDF5 cross section data."
)
else:
with open(xs_path, "rb") as f:
md5 = hashlib.md5(f.read()).hexdigest()
if md5 != _NNDC_XS_MD5:
_environment_warnings.append(
f"OPENMC_CROSS_SECTIONS ({xs_path}) does not match the "
"official NNDC HDF5 dataset. Regression tests expect the "
"NNDC data; results may differ with other cross section "
"libraries."
)


def pytest_addoption(parser):
parser.addoption('--exe')
Expand All @@ -21,6 +63,28 @@ def pytest_configure(config):
if config.getoption(opt) is not None:
regression_config[opt] = config.getoption(opt)

_check_build_environment()


def _print_environment_warnings(terminalreporter):
"""Print environment warnings as a visible section."""
if _environment_warnings:
terminalreporter.section("OpenMC Environment Warnings")
for msg in _environment_warnings:
terminalreporter.line(f"WARNING: {msg}", yellow=True)
terminalreporter.line("")


def pytest_sessionstart(session):
"""Print environment warnings at the start of the test session."""
_print_environment_warnings(session.config.pluginmanager.get_plugin(
"terminalreporter"))


def pytest_terminal_summary(terminalreporter, exitstatus, config):
"""Reprint environment warnings at the end so they aren't missed."""
_print_environment_warnings(terminalreporter)


@pytest.fixture
def run_in_tmpdir(tmpdir):
Expand Down
7 changes: 5 additions & 2 deletions tools/ci/gha-install.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ def install(omp=False, mpi=False, phdf5=False, dagmc=False, libmesh=False):
os.mkdir('build')
os.chdir('build')

# Build in debug mode by default with support for MCPL
cmake_cmd = ['cmake', '-DCMAKE_BUILD_TYPE=Debug', '-DOPENMC_USE_MCPL=on']
# Build in RelWithDebInfo mode by default with support for MCPL
cmake_cmd = ['cmake', '-DCMAKE_BUILD_TYPE=RelWithDebInfo', '-DOPENMC_USE_MCPL=on']

# Turn off OpenMP if specified
if not omp:
Expand Down Expand Up @@ -43,6 +43,9 @@ def install(omp=False, mpi=False, phdf5=False, dagmc=False, libmesh=False):
# Build in coverage mode for coverage testing
cmake_cmd.append('-DOPENMC_ENABLE_COVERAGE=on')

# Enable strict FP for cross-platform reproducibility in CI
cmake_cmd.append('-DOPENMC_STRICT_FP=on')

# Build and install
cmake_cmd.append('..')
print(' '.join(cmake_cmd))
Expand Down
Loading