Phase 2 of #149: the correctness fixes - #152
Conversation
Implements the Phase 2 items from issue #149's plan of action: A1, A2, A3, A5, A6, A7 and A8. All change behavior/values (unlike Phase 0/1), so each is covered by a dedicated regression test in addition to the exhaustive availability sweep from Phase 1. A1 - load_compound() mixed energy grids across DEDX_AUTO's tabulated and Bethe-fallback tiers, and could sum uninitialized heap for a constituent shorter than the (wrong) summation length. Now carries each constituent's own energy grid, verifies all constituents resolved to the identical grid (length + memcmp), and rejects a mismatch with DEDX_ERR_INCONSISTENT_COMPOUND instead of silently mixing or truncating. load_bethe_2() now zero-initializes its output struct like read_embedded_stopping_data() already did, and the Bragg sum uses min(length_i) across constituents as belt-and-braces. A2 - material 99 (A150 tissue-equivalent plastic) is a compound, one past the last real element id, but several `<= 99`/`> 99` boundary checks across dedx.c, dedx_validate.c and dedx_embedded_metadata.c treated it as an element -- evaluating it as elemental einsteinium (Z=99) instead of decomposing its real composition. Introduces DEDX_MAX_ELEMENT_ID (98) and replaces every such literal. A3 - load_compound() seeded each constituent's I-value via the public dedx_get_i_value(), which hardcodes DEDX_GAS, silently ignoring config->compound_state for any compound whose elements_i_value wasn't already supplied by the caller (the common DEDX_AUTO case, since dedx_internal_evaluate_i_pot() only runs for program >= DEDX_DEFAULT). load_compound() now resolves compound_state itself when still default and calls the state-aware internal accessor directly. A5 - dedx_internal_validate_rho() failed for any target with no embedded density row, even for tabulated programs that never read rho. This blocked exactly one material (FERROUSOXIDE, id 159) for all 407 program/ion pairs that advertised it. Rho is now only required where a program actually needs it (program >= DEDX_DEFAULT, or DEDX_AUTO's Bethe fallback, checked at the point of use in load_bethe_2()); the missing density row is added, and material_id_supported() models the remaining gap so it isn't over-advertised for the programs that still need it. FERROUSOXIDE's I-value is deliberately left unfabricated (no raw source to verify it against, per data/README.md), so DEDX_DEFAULT/DEDX_BETHE_EXT00 -- which read a compound's own I-value directly rather than Bragg-averaging it -- still correctly fail for it. A6 - the one non-positive value in the whole embedded ICRU73 table (Na in Ar, first energy point) made dedx_internal_calculate_coefficients() silently downgrade the whole table from the requested log-log interpolation to linear, with no way for a caller to detect it (loaded_data->interpolation_mode still reported what was requested). The function now returns the mode it actually used, load_data() records that instead of the request, and a new dedx_get_effective_interpolation_mode() exposes it. A7 - an mstar_mode outside the documented DEDX_MSTAR_MODE_* set fell through dedx_mpaul.c's mode switch to a silent all-zero table with err == 0. dedx_internal_validate_config() now validates mstar_mode (DEDX_MSTAR only) and returns the new DEDX_ERR_INVALID_MSTAR_MODE. A8 - dedx_load_config() dispatched straight to load_compound() whenever elements_id was set, bypassing check_ion() (only load_config_clean()'s tabulated-program path called it). A custom compound with an ion its program doesn't support failed deep inside Bragg decomposition with DEDX_ERR_COMBINATION_NOT_FOUND instead of the clear DEDX_ERR_ION_NOT_SUPPORTED an elemental target already got. check_ion() moves into dedx_internal_validate_config() (as dedx_internal_check_ion(), relocated to dedx_validate.c) so both paths agree. Also updates tests/test_availability_exhaustive.c's ratchet baselines to match the post-fix sweep, with the counts traced by error code and program in a comment: TOTAL_COMBINATIONS 101957 -> 101886 (A2 stops misclassifying material 99), LOAD_FAILURES 407 -> 1470 (net change verified, not assumed -- 183 of the original 407 FERROUSOXIDE failures are fixed by A5, 224 remain as the documented I-value gap above, and 1246 new DEDX_AUTO grid-mismatch rejections replace what used to be silently wrong bound_mismatches/dead_configs results), BOUND_MISMATCHES 1568 -> 480, DEAD_CONFIGS 174 -> 0. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0188VRnSFzb7HvNKcMX7XT1Z
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #152 +/- ##
==========================================
+ Coverage 85.32% 86.04% +0.72%
==========================================
Files 12 12
Lines 1649 1749 +100
Branches 320 348 +28
==========================================
+ Hits 1407 1505 +98
- Misses 242 244 +2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Adding a row to dedx_embedded_compos_rows[] shifted clang-format's bin-packing decision for the whole array from two-entries-per-line to one-entry-per-line -- data values are unchanged, byte-identical, only line breaks moved. Also fixes an include-sort ordering in dedx_embedded_metadata.c and two long lines in the new Phase 2 tests (test_material_availability.c, test_interpolation.c) that the installed clang-format 18 didn't flag identically to CI's clang-format-19. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0188VRnSFzb7HvNKcMX7XT1Z
There was a problem hiding this comment.
Pull request overview
This PR implements Phase 2 (“correctness fixes”) of #149 by tightening validation and fixing several silent-miscompute or silent-degradation cases across compound loading, element/compound dispatch, interpolation-mode reporting, density validation, and MSTAR config validation, with targeted regression tests and an updated exhaustive availability baseline.
Changes:
- Fixes
DEDX_AUTOcompound loading to reject mixed energy grids, avoid summing uninitialized data, and honorcompound_statefor constituent I-value lookups. - Corrects the element/compound boundary (material id 99 is a compound) by introducing
DEDX_MAX_ELEMENT_IDand replacing off-by-one literals. - Makes interpolation downgrade observable via
dedx_get_effective_interpolation_mode(), validatesmstar_mode, unifies ion validation, and updates/extends regression tests (including the exhaustive sweep ratchet baselines).
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_validate_internal.c | Ensures custom-compound validation tests set an explicit ion. |
| tests/test_mstar.c | Adds regression coverage for invalid mstar_mode handling and default behavior. |
| tests/test_material_availability.c | Adds regression tests for A1/A2/A3/A5 behaviors and updates expectations (e.g., grid mismatch rejection). |
| tests/test_interpolation.c | Adds regression tests for effective interpolation-mode downgrade visibility and API guard behavior. |
| tests/test_error_codes.c | Updates/extends regression tests for A8 ion validation and custom-compound ion rejection. |
| tests/test_availability_exhaustive.c | Updates baseline constants and expands commentary to reflect Phase 2 outcomes. |
| src/dedx.c | Implements core correctness fixes (grid consistency checks, element boundary constant usage, effective interpolation-mode recording/accessor, rho checks, etc.). |
| src/dedx_validate.h | Exposes dedx_internal_check_ion() for shared ion validation. |
| src/dedx_validate.c | Moves ion validation into config validation, scopes rho requirements, and validates mstar_mode. |
| src/dedx_spline.h | Updates spline coefficient builder to return the effective interpolation mode. |
| src/dedx_spline.c | Implements returning the effective interpolation mode (log-log vs linear fallback). |
| src/dedx_embedded_metadata.c | Uses DEDX_MAX_ELEMENT_ID for element/compound boundary logic. |
| src/data/embedded/dedx_metadata.h | Adds missing FERROUSOXIDE density row and updates embedded row count. |
| include/dedx.h | Adds public dedx_get_effective_interpolation_mode() API documentation and declaration. |
| include/dedx_error.h | Adds DEDX_ERR_INVALID_MSTAR_MODE. |
| include/dedx_elements.h | Introduces DEDX_MAX_ELEMENT_ID and documents correct element/compound boundary usage. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…ata() load_data() persists stopping_data->ion/->target into ws->loaded_data[]->ion/target, but two producers never set them: - load_compound()'s aggregated `data` is a fresh stack local with no initializer, so these fields carried indeterminate stack values into the loaded dataset instead of the compound's own ion/target. - load_bethe_2()'s new memset(data, 0, sizeof(*data)) (added earlier in this PR for A1) zeroes them, and nothing repopulated them afterward, so every Bethe-formula dataset recorded ion/target as 0. Nothing in the public API currently reads this stored metadata back out, so this had no effect on any computed stopping power -- but it's exactly the class of silently-wrong internal state issue #149 is about, so both are now set explicitly, matching read_embedded_stopping_data()'s existing convention for the tabulated-report path. Adds regression coverage for both producers. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0188VRnSFzb7HvNKcMX7XT1Z
Critical review of Phase 2I reviewed this by building The BlockingB1 — A3 silently changes DEDX_MSTAR results for 56 compounds × 16 ions, undocumented and untestedThe description scopes A3 to " static char resolve_mstar_mode(char state, dedx_config *config, int *err) {
int target_state = config->compound_state;
...
if (target_state == DEDX_DEFAULT_STATE) {
if (dedx_internal_target_is_gas(config->target, err) != 0) ... /* config->target == the *constituent element* here */Before this PR, MSTAR compounds reached that function with
Affected MSTAR materials include BERYLLIUMOXIDE, BORONOXIDE, ADENINE, GUANINE, UREA, EYELENS_ICRP, TISSUE_SOFT_ICRUFOUR_COMPONENT, FERRICOXIDE — up to 94% change in stopping power, for all 16 MSTAR ions. The new behaviour is arguably the more correct one (a compound's state should come from the compound). But a 94% change to a published, literature-backed dataset cannot land as an unremarked side effect of a fix described as being about I-values. This needs: an explicit section in the description, a value-pinning regression test on at least one MSTAR compound, and a check of the new numbers against MSTAR/ B2 — B1 routes MSTAR ions 12–15 on gaseous compounds into
|
Fixes every blocking finding from the review, addresses each "should fix"
item, and applies the nits. Verified with the same battery as before: full
ctest (33/33) plain, under ASan+UBSan, and under Valgrind, plus a diff
against a pre-Phase-2 build for anything touching computed values.
B1 - A3's pre-loop compound_state resolution in load_compound() (added so
Bethe-type constituents get the compound's own gas/condensed state instead
of the public dedx_get_i_value()'s hardcoded gas default) has a real side
effect on DEDX_MSTAR: resolve_mstar_mode() (dedx_mstar.c) also reads
config->compound_state, and only did its own per-constituent gas check when
that field was still unresolved. It now sees the compound's resolved state
instead, for every constituent -- changing MSTAR's numeric output for gas
compounds under the default/'a'/'b' modes. Kept the new behavior (Bragg
additivity treats a compound's state as a property of the compound, not of
each atom in isolation -- the same convention the I-value fix already
applies), documented it explicitly (this commit message; a pinned regression
test), and added test_gas_compound_uses_compound_state_not_constituent() in
test_mstar.c pinning current output for a real material (BUTANE) so a future
change to this path is visible instead of silent. These values are a
regression pin, not independently verified against dedx_web or the MSTAR
literature -- flagged as such in the test's own comment.
B2 - dedx_mpaul.c's mode 'h' branch only has coefficients for ion in
{3-11,16,17,18}; ion 12-15 fell through an "illegal mode" else with err left
at DEDX_OK, computing nonsense from the branch's declared defaults
(a=5.0,b=-1,c=-1). Reachable via any resolved 'h'/'d' mode, not just an
invalid config->mstar_mode -- so A7's upstream validation could not catch it
structurally. Both illegal-mode branches (the ion 12-15 catch-all, and the
mode=='d' z2<=4/z2>=93 catch-all) now set *err (reusing
DEDX_ERR_ION_NOT_SUPPORTED_MSTAR, previously "reserved legacy code", exactly
as issue #149's A7 text itself suggested) instead of silently proceeding.
Reproduced against `main` (DEDX_MSTAR+ion 12+DEDX_ARGON returns 21936 with
err=0 there) to confirm this predates Phase 2 entirely.
B3 - dedx_internal_check_ion() indexes dedx_program_available_ions[prog]
without validating prog first; rows the table doesn't explicitly initialize
are zero-filled, not -1-terminated, so an unrecognized program id (a typo,
-1, 50, ...) walks off the end of the table. Pre-existing on the elemental
path; A8 moving check_ion() into dedx_internal_validate_config() made it
newly reachable from the custom-compound path too, which used to bypass this
function entirely. Now validates prog against dedx_get_program_list() before
calling dedx_get_ion_list(). Reproduced and fixed under ASan
(global-buffer-overflow -> clean DEDX_ERR_ION_NOT_SUPPORTED).
B4 - Three parts:
- dedx.h's DEDX_AUTO doc block now documents the one real exception to its
"falls back rather than failing outright" promise: constituents that
resolve to mismatched tiers.
- The grid-mismatch code no longer reuses DEDX_ERR_INCONSISTENT_COMPOUND
(which means "your specification is invalid" -- not true here, the
material id is perfectly valid). New DEDX_ERR_INCONSISTENT_ENERGY_GRID.
- material_id_supported() now predicts a DEDX_AUTO grid-tier mismatch the
same way find_data() resolves it (dedx_embedded_resolve_program() per
constituent) instead of only learning about it after the fact via
dedx_load_config() failing, and checks I-value availability for
program >= DEDX_DEFAULT the same way it already checked density. This is
what actually fixes the false-advertisement rate the review measured (up
11x, "the exact defect class #149 opened on"): TOTAL_COMBINATIONS
101886 -> 100222 (the compounds/programs that would fail are no longer
advertised), load_failures 1470 -> 108, bound_mismatches 480 -> 470.
Resampling constituents onto a common grid instead of rejecting the
mismatch (the review's alternative suggestion) is deliberately not taken
on here -- it's a real behavior change to the computed values themselves,
not just to what's advertised, and needs its own scoping.
B5 - Every early-return in load_compound() left config->target pointing at
whichever constituent element it was resolving when it gave up (not the
compound the caller asked for), and could leave config->i_value/
_temp_i_value clobbered too. A caller who reuses the config after a failed
load (e.g. "try AUTO, fall back to PSTAR on error") would silently see the
wrong target. Restructured around a single cleanup path that restores all
three on every failure.
S1 - dedx_get_effective_interpolation_mode() returned 0 (== DEDX_INTERPOLATION_LOG_LOG)
on DEDX_ERR_INVALID_DATASET_ID; now returns -1, so a caller that forgets to
check *err can't mistake a failed call for "log-log".
S2 - test_availability_exhaustive.c's load-failure ratchet is now asserted
per error code (LOAD_FAILURE_BASELINES[], equality per code) instead of one
aggregate ceiling, and any error code not listed is an unconditional hard
failure regardless of how the aggregate moves.
S3 - Added a provenance note for the FERROUSOXIDE density row to
data/README.md (was only in a code comment), and tightened
test_ferrous_oxide_tabulated_program() to assert the exact cited value
(5.7) instead of merely rho > 0, so a typo'd 57.0/0.57 would be caught.
S4 - Cross-referenced dedx_internal_evaluate_spline()'s isnan() self-rescue
against dedx_get_effective_interpolation_mode() in both directions (they're
deliberately redundant, not alternatives). Checked the Python/C++ exposure
gap: the C++ RAII header already exposes the raw workspace/config pointers,
so the new accessor is already usable from C++ with no wrapper needed;
Python's ctypes bindings only cover the simple/table convenience API, not
the workspace/config API this function needs at all, so wiring it in is a
larger undertaking than this PR (exposing that whole layer) -- left as a
follow-up rather than a partial fix here.
S5 - Noted for the record rather than changed: A3's own regression coverage
uses a synthetic single-element compound because A1 now rejects most real
multi-tier compounds before A3's I-value path is reached; the solid-boron
x1.13 correction question is a pre-existing physics-modeling question this
PR doesn't have the domain grounding to resolve and doesn't touch.
Nits - `// clang-format off/on` around dedx_embedded_compos_rows[] so a
single added row can't reflow the whole hand-maintained table again;
dedx_embedded_read_effective_charge()'s id boundary rename already read
correctly as a rename, left as-is.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0188VRnSFzb7HvNKcMX7XT1Z
|
Thank you for this — the differential sweep and ASan probes caught real issues the original PR missed, and the "verified solid" section confirms A1/A2/A5 and the baseline accounting held up under independent scrutiny, which is useful to know too. All blocking and should-fix items are addressed in b6bfe76, verified the same way you found them (a differential sweep against
Generated by Claude Code |
…_ion_list() Two remaining gaps after the previous review-response commit (b6bfe76): 1. B4's fix taught material_id_supported() to predict DEDX_AUTO's grid-tier mismatch and FERROUSOXIDE's I-value gap, but left MSTAR's own coefficient-availability gap (B2's fix) unmodeled -- 108 combinations (DEDX_MSTAR ions 12-15 on gas targets) stayed advertised via dedx_get_material_list_for_ion() even though dedx_load_config() now correctly rejects them with DEDX_ERR_ION_NOT_SUPPORTED_MSTAR. That's the same false- advertisement defect class issue #149 was raised about, just not yet closed for this path. element_supported_for_ion() and material_id_supported() now take an `mstar_state` parameter and probe the actual coefficient computation (dedx_internal_calculate_mspaul_coef()) under find_data()'s own default mstar_mode assumption ('b'), instead of only checking embedded-table existence. For a compound target, this has to use the compound's own resolved gas/condensed state, not each constituent's own -- load_compound() resolves config->compound_state once, from the compound's own id, before its constituent loop runs (issue #149 finding A3), so a per-constituent guess would both under- and over-advertise relative to what dedx_load_config() actually does. TOTAL_COMBINATIONS: 100222 -> 100114 (-108, no longer advertised), load_failures: 108 -> 0. LOAD_FAILURE_BASELINES is now empty -- every load failure this sweep used to hit has either been fixed outright or stopped being advertised. 2. dedx_get_ion_list() is public API, directly callable with any int, but still indexed dedx_program_available_ions[program] without a bounds check -- the previous commit's B3 fix guarded the internal dedx_internal_check_ion() call site, not this function itself. Reproduced under ASan/UBSan (dedx_get_ion_list(-1000) reads garbage; UBSan flags the negative index as UB regardless of the value). Bounds-checked against the table's actually-populated range (0..DEDX_ICRU) with an empty-list fallback, matching what dedx_internal_check_ion() already assumes is safe to call. Verification: full ctest (33/33) green under -DDEDX_WERROR=ON and under -fsanitize=address,undefined with leak detection; both fixes independently reproduced-then-verified-fixed under ASan; reformatted with clang-format-19 (no changes needed). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Pushed a small follow-up (734c562) on top of the review-response commit, closing the two gaps flagged in my earlier reply:
Verified: full |
…haustive.c
LOAD_FAILURE_BASELINES[] = {} is a GCC/Clang extension (zero-size array with
an empty initializer), not standard C -- MSVC rejects it outright (C7757),
and rejects the resulting zero-length load_failures_by_code[0] struct member
even harder ("illegal zero-sized array"), failing the windows-latest
build_and_test job.
Replaced with a one-element sentinel {-1, 0}: -1 is not a DEDX_ERR_* value
(dedx_error.h) and dedx_load_config() never sets *err to it, so it can never
match a real load failure in sweep_one()'s loop -- it exists purely to keep
the array non-empty and portable. Verified: load_failures still reads 0
(confirmed via the new load_failures[err=-1]=0 line), full ctest (33/33)
green plain and under ASan+UBSan, clang-format-19 clean, Valgrind clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0188VRnSFzb7HvNKcMX7XT1Z
|
The Generated by Claude Code |
Implements Phase 2 — "the correctness fixes" of the plan of action in #149, building on Phase 0 (#150) and Phase 1 (#151), both merged. This is the phase that changes computed values, unlike Phase 0/1: each finding below gets its own dedicated regression test, on top of the exhaustive availability sweep from Phase 1.
Updated after a critical review (see the review thread below) that found real issues beyond the original scope — an undocumented value change reaching DEDX_MSTAR, a genuinely broken MSTAR coefficient branch A7 didn't fully close, a new out-of-bounds read, an availability-API accuracy regression, and config corruption on a failed compound load. All are fixed; details in the "Review round" section below the original findings.
Changes
A1 🔴 —
load_compound()mixed energy grids across tiers, and could sum uninitialized heapDEDX_AUTOresolves each compound constituent independently — a tabulated report first, falling back to the Bethe-Bloch formula (122-point grid) only for elements no report tabulates (133-point grid family).load_compound()used to Bragg-sum every constituent overcompound_data[0].length, on the x-axis of whichever constituent happened to load last — silently mixing bin boundaries, or summing uninitialized heap for a constituent shorter than that length.load_compound()now carries each constituent's own energy grid, verifies every constituent resolved onto the identical grid (length andmemcmpof the knots), and rejects a mismatch withDEDX_ERR_INCONSISTENT_ENERGY_GRIDinstead of serving a wrong or partially-uninitialized number. (This code was reused fromDEDX_ERR_INCONSISTENT_COMPOUNDoriginally; split into its own code in review — see B4 below, the caller's compound specification here is perfectly valid, the problem is purely mismatched grids.)load_bethe_2()now zero-initializes its output struct up front (matching whatread_embedded_stopping_data()already did), and the final sum usesmin(length_i)across constituents as belt-and-braces.A2 🔴 — element/compound boundary was off by one: material 99 is a compound, not an element
dedx_elements.hruns elements 1–98, then compounds from 99 (DEDX_A150_TISSUE_EQUIVALENT_PLASTIC = 99), but five call sites acrossdedx.c,dedx_validate.canddedx_embedded_metadata.cused<= 99/> 99as the element/compound test. A150 tissue-equivalent plastic was therefore evaluated by the Bethe path as elemental einsteinium (Z=99, A=252) instead of its real composition, and got the elemental solid-state I-value correction on top.Introduces
DEDX_MAX_ELEMENT_ID(98) and replaces every such literal. Regression test assertsbragg_used == 1and Bethe ≈ tabulated within 2% for id 99 (previously ~29% off).A3 🟠 —
load_compound()silently ignoredcompound_statefor its own I-value lookupsIt seeded each constituent's I-value via the public
dedx_get_i_value(), which hardcodesDEDX_GAS— soconfig->compound_statewas silently ignored wheneverelements_i_valuewasn't already supplied by the caller, which is the common case forDEDX_AUTO(dedx_internal_evaluate_i_pot()only runs forprogram >= DEDX_DEFAULT).load_compound()now resolves the compound's own state itself when still unset, and calls the state-aware internal accessor directly. (This has a real, documented side effect onDEDX_MSTAR— see B1 below.)A5 🟠 — a missing density row blocked tabulated programs — 407 false negatives in the availability API
dedx_internal_validate_rho()used to fail whenever a target had no embedded density row, for every program, even table lookups that never readrho. Exactly one material was affected (FERROUSOXIDE, id 159), but it broke it for all 407 program/ion pairs that advertised it.rhois now only required where it's actually used (program >= DEDX_DEFAULT, orDEDX_AUTO's Bethe fallback, checked at the point of need insideload_bethe_2()), and the missing density row is added — cited indata/README.md(added in review) as the standard literature FeO/wüstite density. FERROUSOXIDE's I-value is deliberately left unfabricated — no raw source exists to verify it against — soDEDX_DEFAULT/DEDX_BETHE_EXT00, which read a compound's own I-value directly rather than Bragg-averaging it, still correctly fail for it;material_id_supported()now predicts that gap too (see B4 below), so it's a closed gap on the advertising side even though the value itself stays unfilled.A6 🟠 — a zero in the embedded ICRU73 table silently downgraded interpolation with no way to detect it
The one non-positive value in the whole ICRU73 table (Na in Ar, first energy point) made
dedx_internal_calculate_coefficients()fall back from the requested log-log interpolation to linear — silently, becauseloaded_data->interpolation_modekept reporting whatever was requested, not what was actually used. The function now returns the mode it actually applied,load_data()records that instead of the request, and a newdedx_get_effective_interpolation_mode()accessor exposes it to callers (returns-1on a badcfg_id, not0, so a caller that forgets to check*errcan't mistake failure forDEDX_INTERPOLATION_LOG_LOG— fixed in review). (Deciding the fate of the underlying zero value itself, and build-time validation intools/dat2c.py, are left for Phase 4/E4 — no authoritative replacement value exists to fabricate one from.)A7 🟠 — an invalid
mstar_modeproduced an all-zero table with no errordedx_mpaul.c's mode switch fell through an empty "illegal mode" branch, leaving the computed table all zero witherr == 0.dedx_internal_validate_config()now validatesmstar_modeagainst the documentedDEDX_MSTAR_MODE_*set (only forDEDX_MSTAR) and returns the newDEDX_ERR_INVALID_MSTAR_MODE. This only covers an invalid top-levelmstar_mode, though — review found the same "illegal mode" fallthrough is also reachable with a valid, resolved mode for specific ion/effective-charge combinations; see B2 below for that fix.A8 🟡 —
check_ion()was skipped on the custom-compound pathdedx_load_config()dispatched straight toload_compound()wheneverelements_idwas set, bypassingcheck_ion()(onlyload_config_clean()'s tabulated-program path called it). A custom compound with an ion its program doesn't support failed deep inside Bragg decomposition withDEDX_ERR_COMBINATION_NOT_FOUNDinstead of the immediate, clearDEDX_ERR_ION_NOT_SUPPORTEDan elemental target already got.check_ion()moves intodedx_internal_validate_config()(asdedx_internal_check_ion(), relocated todedx_validate.c) so both paths agree. (This surfaced a pre-existing out-of-bounds read for an unrecognizedprogramon the newly-reachable custom-compound path — see B3 below.)Review round: B1–B5, S1–S5, nits
A critical review (thread below) reproduced the branch against
mainwith a differential sweep over every(program, ion, material)triple and targeted ASan probes, and found real issues. All fixed:DEDX_MSTAR(resolve_mstar_mode()reads the sameconfig->compound_statefield). Kept the corrected behavior (a compound's gas/condensed state is a property of the compound, not each constituent atom — consistent with the I-value convention A3 already applies), documented it explicitly here and in code, and added a pinned regression test (test_gas_compound_uses_compound_state_not_constituentintest_mstar.c) so a future change to this path is visible instead of silent. These pinned values are the branch's own current output, not independently verified againstdedx_webor the MSTAR literature — that cross-check is out of scope for what a unit test can do.dedx_mpaul.c's mode'h'branch has no coefficients for ion 12-15 (or mode'd'for effective charge ≤4/≥93); both fell through to an "illegal mode"elsethat silently computed nonsense witherr == 0. Reproduced identically againstmain(DEDX_MSTAR+ ion 12 +DEDX_ARGON→21936witherr=0there too) — this predates Phase 2 entirely and is structurally distinct from A7 (reachable via a valid, resolved mode). Both branches now set*err(reusingDEDX_ERR_ION_NOT_SUPPORTED_MSTAR, exactly as issue Deep audit: DEDX_AUTO returns garbage/unusable results for 174 materials, element-boundary off-by-one, 2 API-reachable memory-safety bugs, plus API/doc inconsistencies — with a plan of action #149's own A7 text suggested).dedx_internal_check_ion()indexeddedx_program_available_ions[prog]with no bounds check; an unrecognizedprogwalked off the end of the table (reproduced as an ASan global-buffer-overflow). Pre-existing on the elemental path; A8 made it newly reachable from the custom-compound path. Now validatesprogagainstdedx_get_program_list()first.material_id_supported()didn't know about A1's grid-tier constraint or the FERROUSOXIDE I-value gap, so the availability API's false-advertisement rate went up 11x from this PR's own A1 fix (exactly the defect class Deep audit: DEDX_AUTO returns garbage/unusable results for 174 materials, element-boundary off-by-one, 2 API-reachable memory-safety bugs, plus API/doc inconsistencies — with a plan of action #149 opened on). It now predicts both, mirroringfind_data()'s actual dispatch:TOTAL_COMBINATIONS101886 → 100222, load failures 1470 → 108, bound mismatches 480 → 470. Also:dedx.h'sDEDX_AUTOdoc now documents the grid-mismatch exception explicitly, and the grid-mismatch error code is split out ofDEDX_ERR_INCONSISTENT_COMPOUND(see A1 above). Resampling onto a common grid instead of rejecting the mismatch (an alternative the review raised) is explicitly not taken on here — a real physics behavior change, not just an advertising fix, needing its own scope.load_compound()leftconfig->targetpointing at whichever constituent it was mid-resolving, not the compound the caller asked for (plusi_value/_temp_i_valuepotentially clobbered) — silent corruption for a caller that reuses the config after a failed load. Restructured around one cleanup path that restores all three on every failure.dedx_get_effective_interpolation_mode()now returns-1on error (see A6 above).test_availability_exhaustive.c's load-failure ratchet now asserts per error code (equality), not one aggregate ceiling that could absorb a new failure mode.data/README.md, and its test asserts the exact value (5.7), not just> 0.isnan()self-rescue indedx_internal_evaluate_spline()against A6's new accessor (deliberately redundant, not alternatives) in both directions. The new accessor is already usable from C++ (the RAII header exposes rawdedx_workspace/dedx_configpointers, no wrapper needed); Python's ctypes bindings don't cover the workspace/config API at all today (only the simple/table convenience functions), so wiring this one function in means exposing that whole layer first — a follow-up, not something to rush here.// clang-format off/onarounddedx_embedded_compos_rows[]so one added row can't reflow the whole hand-maintained table again.Verification
Locally, after every round of changes:
ctestsuite (33/33) green in a plain-DDEDX_WERROR=ONbuild.ctestsuite (33/33) green under-fsanitize=address,undefinedvia thesanitizepreset.test_*binary excepttest_availability_exhaustivepasses undervalgrind --leak-check=full --track-origins=yes(0 errors, all heap blocks freed).clang-format-19(CI's exact version, installed locally to match) clean on every changed file.mainand this branch, before/after), B3's ASan repro, B4's advertised-vs-load-succeeds counts.Scope
Deliberately excludes:
tools/dat2c.pybuild-time validation (A6's other half) and the Bethe evaluator de-duplication (E3) — Phase 4 territory.DEDX_AUTOconstituents onto a common grid instead of rejecting a tier mismatch (B4) — a real computed-value change needing its own scope.dedx_get_effective_interpolation_mode()(or the workspace/config API generally) to Python (S4) — needs that whole layer wired up first.dedx_config_init(),dedx_unload_config(),*errsemantics, the general list-accessor bounds-checking sweep) and Phase 4 (architecture/data generators) are otherwise untouched.Closes nothing on its own; #149 stays open for the remaining phases.
Generated by Claude Code