Fix inconsistent out-of-range evaluation between Tabulated1D scalar and array paths - #4074
Conversation
Evaluating openmc.data.Tabulated1D on an array containing values outside the tabulated range returned zeros for those points, while scalar evaluation returns the value at the nearest tabulated endpoint. Assign boundary values to out-of-range points in the array evaluation path so that both paths agree. sum_functions is also updated to evaluate each tabulated component only where it is defined, which preserves the behavior of combined functions (e.g., fission energy release components) whose tabulated components cover different incident energy ranges. Fixes: openmc-dev#4041 Signed-off-by: Engineer <kawacukent@gmail.com>
CAOShurong
left a comment
There was a problem hiding this comment.
Context
Reviewed exact head b39ee90 against develop base 86ceaad.
Summary
The scalar/array boundary fix is useful and the new tests cover its intended interpolation behavior. I found one blocking dtype regression in sum_functions(), however: the new accumulator inherits an integer union-grid dtype and cannot add the floating-point values returned by supported functions.
Detailed findings
Blocking issue
- [Major] Please use an accumulator dtype compatible with the evaluated function values and add an integer-grid Tabulated1D + Polynomial regression. On the base, the supported combination returns [10.5, 20.0, 29.5] as float64; this head raises UFuncOutputCastingError while adding float64 into int64.
Verified areas
- Purpose/scope: focused fix for #4041, with no new dependency or public API.
- Correctness/testing: the 11 new tests pass locally; compileall and diff checking pass. The public rollup currently reports all 18 contexts successful.
- Physics/design/performance/docs: no new physics model or transport-loop allocation; the endpoint semantics are documented and the overall design remains localized.
I used AI assistance to help inspect the repository and run the base/head verification; I checked the exact diff, reproducer, and results before submitting this review.
| # Evaluate each function and add together. Tabulated functions are | ||
| # only evaluated where they are defined; values beyond a function's | ||
| # tabulated range do not contribute to the sum. | ||
| y = np.zeros_like(x) |
There was a problem hiding this comment.
np.zeros_like(x) inherits x's dtype. When a tabulated grid is integer-valued, y is int64, so adding a Polynomial (or any floating-point function result) raises UFuncOutputCastingError. This worked on develop and is used by supported sum_functions() call paths. Please initialize an accumulator that can safely represent the evaluated values and cover an integer-grid Tabulated1D + Polynomial case.
np.zeros_like(x) inherits the dtype of the union grid, so when the grid is integer-valued the accumulator cannot hold the floating-point results of combined functions such as Polynomial, raising UFuncTypeError. Initialize the accumulator with a float dtype, restored from the prior behavior of sum(f(x) for f in funcs) which promoted to float. Adds a regression test combining an integer-valued tabulated function with a polynomial (test_sum_functions_integer_grid). Co-authored-by: CAOShurong <notifications@github.com> Signed-off-by: Engineer <kawacukent@gmail.com>
|
Thanks @CAOShurong for the careful review and for catching the dtype regression — that is exactly right. Reproduced: with an integer-valued grid, Fixed (commit 56a8e5a): the accumulator in Added regression test: The full |
|
The out-of-range fix looks correct. Following on from the All points in-range here, so this is distinct from the out-of-range issue: f = openmc.data.Tabulated1D([0.0, 10.0], [0.0, 1.0]) # f(x) = x/10
f(5) # 0.5 scalar path, correct
f(np.array([5])) # array([0]) <- int input, truncated
f(np.array([5.0])) # array([0.5]) <- float input, correct
f(np.array([2, 5, 8])) # array([0, 0, 0]) correct: [0.2, 0.5, 0.8]The output dtype just tracks the input dtype: Checked against this branch specifically: the two out-of-range lines don't change it (in-range points never reach them), and for out-of-range integer input the endpoint value they assign is itself truncated. Same one-word fix you applied to y = np.zeros_like(x, dtype=float)In transport this is latent since energies are floats, but the public API accepts integer arrays (the issue's own repro passes them), so it can surface in post-processing/plotting. Happy to push the one-liner plus a small regression test, or it folds cleanly into this PR. |
…truncation np.zeros_like(x) inherits the input array's dtype. When an integer-valued array is passed, interpolated float values are silently truncated to int (e.g. f(5)=0.5 but f(np.array([5]))=[0]). Initialize with dtype=float, matching the same fix already applied to sum_functions in the prior commit. Adds test_tabulated1d_integer_input covering the exact truncation scenario reported by @dylanpulver. Co-authored-by: Dylan Pulver <notifications@github.com> Signed-off-by: Engineer <kawacukent@gmail.com>
|
Thanks @CAOShurong and @dylanpulver for catching the same dtype hazard in Reproduced (the exact case from the review): f = openmc.data.Tabulated1D([0.0, 10.0], [0.0, 1.0])
f(np.array([5])) # array([0]) <- int64 truncation, silent wrong numbers
f(np.array([5.0])) # array([0.5]) <- correctFixed (commit 22413cb): changed the output accumulator in Added test_tabulated1d_integer_input: reproduces the exact truncation scenario — verifies scalar/array consistency on integer input, output dtype is float64, and values match expected floats. Full |
paulromano
left a comment
There was a problem hiding this comment.
I took a closer look at the proposed out-of-range behavior, including both the ENDF-6 specification and existing Python callers of Tabulated1D.
ENDF-6 defines the interpolation laws between tabulated points and describes the tabulated interval as the complete region over which the independent variable is defined. It does not prescribe extrapolation below the first point or above the last point. Thus, endpoint clamping and zero extension are both extensions beyond the ENDF-6 format.
The existing Python codebase does rely on the longstanding array behavior of returning zero outside the tabulated interval. In particular, threshold reaction cross sections are evaluated on union grids in ENDF activation and photon-production processing, photoatomic HDF5 export, redundant cross-section construction, and a few other areas. The revised approach is therefore:
- Preserve the established Python array convention that values outside the tabulated interval are zero.
- Make scalar evaluation follow the same convention, resolving #4041 without changing existing array-based reaction processing.
- Document explicitly that zero extension is an OpenMC convention; it is not specified by ENDF-6.
- Continue returning floating-point arrays for integer-valued inputs, avoiding silent truncation during interpolation.
- Reject NaN and infinite arguments consistently for scalar and array inputs.
- Keep
sum_functionsmaterializing aTabulated1Don the union grid, as it did previously. This preserves the HDF5 representation used for derived fission-energy quantities. As before, this materialization cannot represent a discontinuity between a nonzero endpoint and zero extension exactly; addressing that would require a broader cross-language design change.
One related point is that the C++ Tabulated1D class currently clamps to the nearest endpoint. Again, ENDF does not require that behavior, and changing it could affect transport, so I think a cross-language extrapolation policy should be considered separately rather than silently changing the longstanding Python array semantics in this bug fix.
Description
Evaluating
openmc.data.Tabulated1Don values outside its tabulated range currently gives different answers depending on whether the input is a scalar or an array:The scalar path (
_interpolate_scalar) returns the value at the nearest tabulated endpoint, while the array path in__call__initializes the output with zeros and only fills points that fall inside an interpolation region, leaving out-of-range entries at zero.This PR makes the array path assign the boundary values (
y[0]/y[-1]) to out-of-range points so that both paths agree. This is also consistent with the existing precision handling at the domain edges (np.isclosechecks), which already assigns endpoint values to near-edge points.Changes
openmc/data/function.py:Tabulated1D.__call__: out-of-range points now receive the value of the nearest tabulated endpoint, matching the scalar path.sum_functions: each tabulated component is now explicitly evaluated only where it is defined (points outside a component's own tabulated range contribute zero). This preserves the existing behavior of combined functions — e.g.,FissionEnergyRelease.recoverable,total, and theq_*properties, which combine components that may cover different incident energy ranges on a union grid — independently of the new out-of-range semantics.Testing
Added
tests/unit_tests/test_function.pycovering:sum_functionsbehavior for components with differing domains and for polynomial+tabulated combinations.Local results: all 11 new tests pass; existing unit tests that exercise these code paths were compared before/after the change with identical outcomes (failures observed locally are due to no nuclear data being configured and are present on unmodified
developas well).Fixes: #4041
Signed-off-by: Engineer kawacukent@gmail.com