From 0a3ec4ee13cc4999b44bc17747540ac68a1b091f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 18:10:11 +0000 Subject: [PATCH 1/6] Phase 2 of #149: the correctness fixes 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 Claude-Session: https://claude.ai/code/session_0188VRnSFzb7HvNKcMX7XT1Z --- include/dedx.h | 17 +++ include/dedx_elements.h | 9 ++ include/dedx_error.h | 1 + src/data/embedded/dedx_metadata.h | 9 +- src/dedx.c | 217 ++++++++++++++++++--------- src/dedx_embedded_metadata.c | 6 +- src/dedx_spline.c | 10 +- src/dedx_spline.h | 7 +- src/dedx_validate.c | 82 +++++++++- src/dedx_validate.h | 13 ++ tests/test_availability_exhaustive.c | 80 +++++++--- tests/test_error_codes.c | 49 ++++-- tests/test_interpolation.c | 113 ++++++++++++++ tests/test_material_availability.c | 189 ++++++++++++++++++++++- tests/test_mstar.c | 56 +++++++ tests/test_validate_internal.c | 4 + 16 files changed, 750 insertions(+), 112 deletions(-) diff --git a/include/dedx.h b/include/dedx.h index 5ae159c..c322534 100644 --- a/include/dedx.h +++ b/include/dedx.h @@ -369,6 +369,23 @@ int dedx_load_config(dedx_workspace *ws, dedx_config *config, int *err); */ float dedx_get_stp(dedx_workspace *ws, dedx_config *config, float energy, int *err); +/** @brief Report the interpolation mode actually used for a loaded configuration. + * + * dedx_config::interpolation_mode is the mode the caller requested. + * DEDX_INTERPOLATION_LOG_LOG silently falls back to DEDX_INTERPOLATION_LINEAR + * when the underlying table contains a non-positive energy or stopping-power + * value (log-log space cannot represent zero or negative values), so the + * effective mode used for interpolation can differ from what was requested. + * Call this after dedx_load_config() to find out which mode was actually + * applied to the loaded dataset. + * + * @param[in] ws Workspace with a loaded configuration. + * @param[in] config Loaded configuration (cfg_id must be valid). + * @param[out] err Error code; 0 on success. + * @return DEDX_INTERPOLATION_LOG_LOG or DEDX_INTERPOLATION_LINEAR. + */ +int dedx_get_effective_interpolation_mode(dedx_workspace *ws, dedx_config *config, int *err); + /** @brief One-call stopping power evaluation using the default program for the ion. * * Convenience wrapper: allocates a workspace internally, loads the appropriate diff --git a/include/dedx_elements.h b/include/dedx_elements.h index 9680845..1e4cccd 100644 --- a/include/dedx_elements.h +++ b/include/dedx_elements.h @@ -8,6 +8,15 @@ extern "C" { /** Maximum number of tabulated energy points stored per dataset. */ #define DEDX_MAX_ELEMENTS 150 +/** Highest material identifier that denotes an element (Z = atomic number); every + * id greater than this is a compound or mixture -- DEDX_A150_TISSUE_EQUIVALENT_PLASTIC + * (id 99) is the first compound entry, immediately after DEDX_CALIFORNIUM (id 98), + * the last elemental one. Use this instead of a bare 98/99 literal when testing + * whether a material id is an element: a stray `<= 99` here previously evaluated + * A150 (a compound) as if it were elemental einsteinium (see issue #149, finding A2). + */ +#define DEDX_MAX_ELEMENT_ID 98 + /** * @defgroup ions_and_materials Ion and material identifiers * @brief Identifiers for projectile ions and target materials. diff --git a/include/dedx_error.h b/include/dedx_error.h index 19cfc06..78ed1d6 100644 --- a/include/dedx_error.h +++ b/include/dedx_error.h @@ -50,6 +50,7 @@ extern "C" { #define DEDX_ERR_INVALID_I_VALUE 210 /**< mean excitation potential must be > 0 */ #define DEDX_ERR_INCONSISTENT_COMPOUND 211 /**< inconsistent compound specification */ #define DEDX_ERR_INVALID_INTERPOLATION_MODE 212 /**< interpolation mode is not supported */ +#define DEDX_ERR_INVALID_MSTAR_MODE 213 /**< mstar_mode is not one of the DEDX_MSTAR_MODE_* values */ /** @} */ /** @defgroup err_memory Memory errors (301) diff --git a/src/data/embedded/dedx_metadata.h b/src/data/embedded/dedx_metadata.h index 9080b06..43baa2c 100644 --- a/src/data/embedded/dedx_metadata.h +++ b/src/data/embedded/dedx_metadata.h @@ -19,7 +19,7 @@ typedef struct { } dedx_embedded_compos_row; /** @brief Embedded density and I-value metadata rows keyed by material ID. */ -static const dedx_embedded_compos_row dedx_embedded_compos_rows[285] = { +static const dedx_embedded_compos_row dedx_embedded_compos_rows[286] = { {1, 8.37480000e-05f, 1.92000000e+01f, 1}, {1, 8.37480000e-05f, 2.18000000e+01f, 2}, {2, 1.66322000e-04f, 4.18000000e+01f, 0}, {3, 5.34000000e-01f, 4.00000000e+01f, 0}, {4, 1.84800000e+00f, 6.37000000e+01f, 0}, {5, 2.37000000e+00f, 7.60000000e+01f, 0}, @@ -103,6 +103,13 @@ static const dedx_embedded_compos_row dedx_embedded_compos_rows[285] = { {154, 1.13000000e+00f, 6.93000000e+01f, 0}, {155, 1.17497000e-03f, 5.07000000e+01f, 0}, {156, 1.10000000e+00f, 7.33000000e+01f, 0}, {157, 5.20000000e+00f, 2.27300000e+02f, 0}, {158, 7.15000000e+00f, 2.61000000e+02f, 0}, {158, 7.15000000e+00f, 2.61000000e+02f, 0}, + /* id 159 (FERROUSOXIDE) previously had no row at all here -- density added per + * issue #149 finding A5 (5.7 g/cm^3, the real-world density of FeO/wustite). + * i_value is left at 0 (no embedded I-value data) rather than guessed: there is + * no raw source file left to regenerate or cross-check this table against (see + * data/README.md) and dedx_embedded_get_i_value() already treats a 0.0 result as + * "not found" (DEDX_ERR_TARGET_NOT_FOUND) rather than a valid answer. */ + {159, 5.70000000e+00f, 0.00000000e+00f, 0}, {160, 1.02400000e+00f, 7.64000000e+01f, 0}, {161, 1.12000000e+00f, 1.43000000e+02f, 0}, {162, 1.80000000e+00f, 2.84900000e+02f, 0}, {163, 9.50000000e-01f, 1.26600000e+02f, 0}, {164, 1.50000000e+00f, 2.10500000e+02f, 0}, {165, 1.80000000e+00f, 2.93500000e+02f, 0}, diff --git a/src/dedx.c b/src/dedx.c index 709d785..7b9afa2 100644 --- a/src/dedx.c +++ b/src/dedx.c @@ -52,7 +52,6 @@ static int load_data(dedx_workspace *ws, stopping_data *data, float *energy, ded static int check_energy_bounds(dedx_internal_lookup_data *data, float energy); static float get_min_energy_icru(int ion); static float get_max_energy_icru(int ion); -static int check_ion(int prog, int ion); static int load_config_clean(dedx_workspace *ws, dedx_config *config, int *err); static int find_data(stopping_data *data, dedx_config *config, float *energy, int *err); static int load_compound(dedx_workspace *ws, dedx_config *config, int *err); @@ -144,6 +143,7 @@ static const dedx_error_entry dedx_error_table[] = { {DEDX_ERR_INVALID_I_VALUE, "I value must be larger than zero."}, {DEDX_ERR_INCONSISTENT_COMPOUND, "Compound specification is inconsistent."}, {DEDX_ERR_INVALID_INTERPOLATION_MODE, "Interpolation mode is not supported."}, + {DEDX_ERR_INVALID_MSTAR_MODE, "mstar_mode is not a recognized DEDX_MSTAR_MODE_* value."}, {DEDX_ERR_NO_MEMORY, "Out of memory"}, }; @@ -236,15 +236,16 @@ const int *dedx_get_material_list(int program) { return dedx_program_available_materials[program]; } -/* Checks whether a single element (target <= 99) is reachable for program/ion, - * mirroring find_data()'s own dispatch (MSTAR's ion remap, DEDX_AUTO's Bethe-Bloch - * fallback, the resolve-across-sub-tables behavior of the ICRU-family programs) so - * the answer stays consistent with what dedx_load_config() will actually do. +/* Checks whether a single element (target <= DEDX_MAX_ELEMENT_ID) is reachable for + * program/ion, mirroring find_data()'s own dispatch (MSTAR's ion remap, DEDX_AUTO's + * Bethe-Bloch fallback, the resolve-across-sub-tables behavior of the ICRU-family + * programs) so the answer stays consistent with what dedx_load_config() will + * actually do. * * Returns 1 if dedx_load_config() is expected to succeed for this exact * (program, ion, element) combination, 0 otherwise. `element` is assumed to already - * be a plain element id (1-99); callers are responsible for that precondition -- - * see material_id_supported()'s `material <= 99` check below. */ + * be a plain element id (1..DEDX_MAX_ELEMENT_ID); callers are responsible for that + * precondition -- see material_id_supported()'s own boundary check below. */ static int element_supported_for_ion(int program, int ion, int element) { int ion_load = ion; int resolved; @@ -283,10 +284,25 @@ static int material_id_supported(int program, int ion, int material) { composition rows are always positive element IDs in practice */ return 0; } /* LCOV_EXCL_STOP */ - if (material <= 99) { + if (material <= DEDX_MAX_ELEMENT_ID) { return element_supported_for_ion(program, ion, material); } + /* Analytical programs (program >= DEDX_DEFAULT) evaluate the Bethe-Bloch formula + * directly on this exact compound target and need its own embedded density (see + * dedx_internal_validate_rho()) -- a compound missing that row, as FERROUSOXIDE + * did before issue #149's finding A5 was fixed, can't be loaded even though every + * constituent element below is individually Bethe-supported. Tabulated programs + * never read rho at all (element_supported_for_ion() already documents the same + * scoping for elements), so this check only applies to program >= DEDX_DEFAULT. */ + if (program >= DEDX_DEFAULT) { + int density_err = DEDX_OK; + dedx_internal_read_density(material, &density_err); + if (density_err != DEDX_OK) { + return 0; + } + } + dedx_internal_get_composition(material, composition, &comp_len, &comp_err); if (comp_err != DEDX_OK || comp_len == 0) { /* LCOV_EXCL_START -- every compound in the master material list has known embedded composition */ @@ -309,10 +325,10 @@ void dedx_get_material_list_for_ion( *err = DEDX_OK; *materials_len = 0; - if (!check_ion(program, ion)) { - /* For DEDX_DEFAULT/DEDX_BETHE_EXT00/DEDX_AUTO, check_ion() itself already - * enforces the 1-112 range the Bethe evaluation path actually supports (see - * its comment), so no separate check is needed here. */ + if (!dedx_internal_check_ion(program, ion)) { + /* For DEDX_DEFAULT/DEDX_BETHE_EXT00/DEDX_AUTO, dedx_internal_check_ion() itself + * already enforces the 1-112 range the Bethe evaluation path actually supports + * (see its comment), so no separate check is needed here. */ *err = DEDX_ERR_ION_NOT_SUPPORTED; return; } @@ -476,6 +492,19 @@ float dedx_get_stp(dedx_workspace *ws, dedx_config *config, float energy, int *e ws->loaded_data[id]->interpolation_mode); } +int dedx_get_effective_interpolation_mode(dedx_workspace *ws, dedx_config *config, int *err) { + int id = config->cfg_id; + + *err = DEDX_OK; + + if (id < 0 || id >= ws->active_datasets) { + *err = DEDX_ERR_INVALID_DATASET_ID; + return 0; + } + + return ws->loaded_data[id]->interpolation_mode; +} + void dedx_free_config(dedx_config *config, int *err) { if (config != NULL) { if (config->elements_id != NULL) @@ -546,6 +575,7 @@ float dedx_get_simple_stp(int ion, int target, float energy, int *err) { static int load_data(dedx_workspace *ws, stopping_data *data, float *energy, dedx_config *config, int *err) { int active_dataset = ws->active_datasets; int prog = config->program; + int effective_interpolation_mode; *err = DEDX_OK; @@ -554,7 +584,7 @@ static int load_data(dedx_workspace *ws, stopping_data *data, float *energy, ded return -1; } - dedx_internal_calculate_coefficients( + effective_interpolation_mode = dedx_internal_calculate_coefficients( ws->loaded_data[active_dataset]->base, energy, data->data, data->length, config->interpolation_mode); ws->loaded_data[active_dataset]->acc.cache = 0; @@ -563,7 +593,14 @@ static int load_data(dedx_workspace *ws, stopping_data *data, float *energy, ded ws->loaded_data[active_dataset]->ion = data->ion; ws->loaded_data[active_dataset]->target = data->target; ws->loaded_data[active_dataset]->datapoints = data->length; - ws->loaded_data[active_dataset]->interpolation_mode = config->interpolation_mode; + // Store the mode actually used, not merely the one requested: a log-log + // request silently downgrades to linear when the table contains a + // non-positive energy or stopping-power value (see issue #149 finding + // A6). Recording the effective mode makes that downgrade observable via + // dedx_get_effective_interpolation_mode() instead of only being + // recoverable indirectly through dedx_internal_evaluate_spline()'s own + // isnan() self-detection. + ws->loaded_data[active_dataset]->interpolation_mode = effective_interpolation_mode; ws->active_datasets++; return active_dataset; @@ -614,49 +651,27 @@ static float get_max_energy_icru(int ion) { return energy_max; } -static int check_ion(int prog, int ion) { - const int *ion_list; - int i = 0; - - if (prog >= DEDX_DEFAULT || prog == DEDX_AUTO) { - /* The Bethe evaluation path (dedx_internal_get_atom_mass/charge, via - * dedx_periodic_table) only has data for ions 1-112, matching - * dedx_full_ion_list -- the same list dedx_get_ion_list() returns for these - * programs -- so accept nothing wider than that here. */ - if ((ion < 1) || (ion > 112)) - return 0; - return 1; - } - - ion_list = dedx_get_ion_list(prog); - while (ion_list[i] != -1) { - if (ion_list[i] == ion) - return 1; - ++i; - } - return 0; -} - static int load_config_clean(dedx_workspace *ws, dedx_config *config, int *err) { float energy[DEDX_MAX_ELEMENTS]; int cfg; - int prog = config->program; - int ion = config->ion; int target = config->target; stopping_data data; config->bragg_used = 0; *err = DEDX_OK; - if (!check_ion(prog, ion)) { - *err = DEDX_ERR_ION_NOT_SUPPORTED; - return -1; - } + /* The ion check used to live here, but only load_config_clean() called it -- the + * custom-compound path (dedx_load_config() dispatching straight to + * load_compound()) skipped it entirely and could fail with a misleading + * DEDX_ERR_COMBINATION_NOT_FOUND instead of DEDX_ERR_ION_NOT_SUPPORTED for an + * unsupported ion (see issue #149 finding A8). dedx_internal_validate_config() + * now calls dedx_internal_check_ion() unconditionally before dispatch, so both + * paths get the same check and the same correct error code. */ config->_temp_i_value = config->i_value; find_data(&data, config, energy, err); if (*err != 0) { - if (*err == DEDX_ERR_COMBINATION_NOT_FOUND && target > 99) { + if (*err == DEDX_ERR_COMBINATION_NOT_FOUND && target > DEDX_MAX_ELEMENT_ID) { *err = DEDX_OK; dedx_internal_evaluate_compound(config, err); if (*err != 0) @@ -714,13 +729,13 @@ static int find_data(stopping_data *data, dedx_config *config, float *energy, in /* DEDX_AUTO layers a Bethe-Bloch fallback on top of DEDX_ICRU's tabulated * auto-selection: when no tabulated report covers this element (e.g. Boron, * which none of them tabulate), compute it analytically instead of failing - * outright. Compounds (target > 99) are excluded here: they are handled below - * via Bragg-additivity decomposition into elements, each of which goes through - * this same fallback individually. DEDX_ICRU itself, and the report-specific - * programs (PSTAR, ASTAR, ICRU49, ICRU73, ...), intentionally stay literal to - * their published data and never get this fallback -- see DEDX_AUTO's docs - * in dedx.h for why. */ - if (prog == DEDX_AUTO && target_load > 0 && target_load <= 99) { + * outright. Compounds (target > DEDX_MAX_ELEMENT_ID) are excluded here: they + * are handled below via Bragg-additivity decomposition into elements, each of + * which goes through this same fallback individually. DEDX_ICRU itself, and + * the report-specific programs (PSTAR, ASTAR, ICRU49, ICRU73, ...), + * intentionally stay literal to their published data and never get this + * fallback -- see DEDX_AUTO's docs in dedx.h for why. */ + if (prog == DEDX_AUTO && target_load > 0 && target_load <= DEDX_MAX_ELEMENT_ID) { return load_bethe_fallback(data, config, energy, err); } if (prog == DEDX_ICRU || prog == DEDX_ICRU49 || prog == DEDX_ICRU73 || prog == DEDX_AUTO) { @@ -756,11 +771,15 @@ static int load_compound(dedx_workspace *ws, dedx_config *config, int *err) { int length = config->elements_length; int *targets = config->elements_id; float *weight; - float energy[DEDX_MAX_ELEMENTS]; + float energy[DEDX_MAX_ELEMENTS]; /* reference energy grid, from constituent 0 */ + float energy_i[DEDX_MAX_ELEMENTS]; /* scratch grid for constituent i > 0, compared below */ float i_value; int target; stopping_data data; - stopping_data *compound_data = malloc(sizeof(stopping_data) * length); + /* calloc, not malloc: zero-initializes every constituent's data[DEDX_MAX_ELEMENTS] + * array up front, so a constituent shorter than data.length (see min-length below) + * contributes 0.0 rather than uninitialized heap to the Bragg sum. */ + stopping_data *compound_data = calloc((size_t) length, sizeof(stopping_data)); *err = DEDX_OK; @@ -771,9 +790,26 @@ static int load_compound(dedx_workspace *ws, dedx_config *config, int *err) { weight = config->elements_mass_fraction; i_value = config->i_value; target = config->target; + + /* Resolve the compound's own physical state (gas vs condensed) before looking up + * any constituent's I-value below. dedx_internal_validate_state() only runs for + * program >= 100 in dedx_internal_validate_config(), so a tabulated-program + * compound -- the common case reaching this function, via load_config_clean()'s + * fallback -- would otherwise still have compound_state == DEDX_DEFAULT_STATE + * here, and dedx_embedded_get_i_value() only applies the condensed-phase + * correction for an explicitly resolved DEDX_CONDENSED state. Uses config->target + * (the compound's own id, saved above), not yet overwritten by the loop below. */ + if (config->compound_state == DEDX_DEFAULT_STATE) { + dedx_internal_validate_state(config, err); + if (*err != 0) { + free(compound_data); + return -1; + } + } + for (i = 0; i < length; i++) { config->target = targets[i]; - // For custom compounds (target > 99 or target == 0), the elements_i_value + // For custom compounds (target > DEDX_MAX_ELEMENT_ID or target == 0), the elements_i_value // array might be provided explicitly or omitted entirely. if (config->elements_i_value != NULL) { config->_temp_i_value = config->elements_i_value[i]; @@ -785,7 +821,7 @@ static int load_compound(dedx_workspace *ws, dedx_config *config, int *err) { return -1; } if (config->elements_i_value[i] == 0.0) { - config->_temp_i_value = dedx_get_i_value(targets[i], err); + config->_temp_i_value = dedx_internal_get_i_value(targets[i], config->compound_state, err); if (*err != 0) { free(compound_data); return -1; @@ -793,29 +829,56 @@ static int load_compound(dedx_workspace *ws, dedx_config *config, int *err) { } } else { // If the elements_i_value array was not provided at all, we must initialize - // the _temp_i_value for the Bethe algorithm using the default I-value for the element. - config->_temp_i_value = dedx_get_i_value(targets[i], err); + // the _temp_i_value for the Bethe algorithm using the default I-value for the element, + // evaluated at the compound's own physical state (see above) -- not the gas-only + // state the public dedx_get_i_value() accessor hardcodes. + config->_temp_i_value = dedx_internal_get_i_value(targets[i], config->compound_state, err); if (*err != 0) { free(compound_data); return -1; } } - find_data(&compound_data[i], config, energy, err); + find_data(&compound_data[i], config, (i == 0) ? energy : energy_i, err); if (*err != 0) { free(compound_data); return -1; } + /* Constituents can resolve onto different energy grids: DEDX_AUTO's per-element + * fallback tries a tabulated report (e.g. the 133-point PSTAR-family grid) first + * and falls back to the 122-point Bethe grid only for elements no report + * tabulates, so one compound can end up with a mix. Summing across mismatched + * x-axes (dedx.c historically used compound_data[0].length for every + * constituent) silently mixes tiers with different bin boundaries and, for + * constituents shorter than that length, uninitialized data. Reject the mix + * instead of serving a wrong or unusable number for it. */ + if (i > 0 + && (compound_data[i].length != compound_data[0].length + || memcmp(energy_i, energy, sizeof(float) * compound_data[0].length) != 0)) { + *err = DEDX_ERR_INCONSISTENT_COMPOUND; + free(compound_data); + return -1; + } } config->i_value = i_value; config->target = target; - for (j = 0; j < compound_data[0].length; j++) { + + /* All constituents are now verified to share one grid, so any compound_data[i].length + * equals compound_data[0].length -- min() here is belt-and-braces, matching the + * issue's suggested fix, in case a future change relaxes the check above from + * "reject a mismatch" to "resample onto a common grid". */ + data.length = compound_data[0].length; + for (i = 1; i < length; i++) { + if (compound_data[i].length < data.length) + data.length = compound_data[i].length; + } + + for (j = 0; j < data.length; j++) { data.data[j] = 0.0; for (i = 0; i < length; i++) { data.data[j] += weight[i] * compound_data[i].data[j]; } } - data.length = compound_data[0].length; free(compound_data); return load_data(ws, &data, energy, config, err); } @@ -825,20 +888,40 @@ static int load_bethe_2(stopping_data *data, dedx_config *config, float *energy, float PZ, PA, TZ, TA, rho, pot; *err = DEDX_OK; - if (config->target > 99) { + /* Zero the whole struct up front, matching read_embedded_stopping_data()'s own + * convention: only data->data[0..121] gets written below (data->length = 122), + * so any caller that reads past data->length -- or a fixed-length copy of the + * whole struct -- sees zeros instead of whatever was on the heap/stack before. */ + memset(data, 0, sizeof(*data)); + if (config->target > DEDX_MAX_ELEMENT_ID) { *err = DEDX_ERR_COMBINATION_NOT_FOUND; return -1; } + if (config->rho <= 0.0f) { + /* dedx_internal_validate_rho() only hard-fails validation for program >= + * DEDX_DEFAULT; DEDX_AUTO (and, transitively, load_compound()'s per-constituent + * Bethe fallback) can reach here with rho still unset when the target's + * embedded density row is missing (see issue #149 finding A5). Catch it here, + * at the actual point of need, rather than silently computing with rho == 0 -- + * the density-effect term below (hnp = 28.8 * sqrt(rho * TZ/TA)) would + * otherwise divide/log by a rho-derived zero and produce Inf/NaN instead of a + * clean error. */ + *err = DEDX_ERR_RHO_REQUIRED; + return -1; + } /* dedx_internal_get_atom_charge()/get_atom_mass() both reset *err to DEDX_OK and * share the same id < 113 validity check, so this pair always succeeds or fails * together -- one check right after both is enough. It must happen right away, - * though: config->target is already known <= 99 (hence < 113) at this point, so - * its own charge/mass lookup just below always succeeds and would otherwise - * silently overwrite an ion failure here, leaving *err == DEDX_OK with PZ/PA at - * their failure sentinel (this is reachable in practice: a compound target - * decomposes into elements -- see load_compound() -- before check_ion() ever runs - * for its ion). */ + * though: config->target is already known <= DEDX_MAX_ELEMENT_ID (hence < 113) at + * this point, so its own charge/mass lookup just below always succeeds and would + * otherwise silently overwrite an ion failure here, leaving *err == DEDX_OK with + * PZ/PA at their failure sentinel. This is no longer reachable via the public API + * (dedx_internal_validate_config() now calls dedx_internal_check_ion() up front + * for every dedx_load_config() call, compound or not -- see issue #149 finding A8), + * but the ordering is cheap defense-in-depth against any future internal caller + * that reaches load_bethe_2() with an ion dedx_internal_check_ion() didn't + * validate first. */ PZ = dedx_internal_get_atom_charge(config->ion, err); PA = dedx_internal_get_atom_mass(config->ion, err); if (*err != 0) diff --git a/src/dedx_embedded_metadata.c b/src/dedx_embedded_metadata.c index 48ef5bc..ad34fe7 100644 --- a/src/dedx_embedded_metadata.c +++ b/src/dedx_embedded_metadata.c @@ -17,6 +17,8 @@ #include "dedx_embedded_metadata.h" +#include "dedx_elements.h" + #include "data/embedded/dedx_composition.h" #include "data/embedded/dedx_metadata.h" @@ -38,7 +40,7 @@ int dedx_embedded_read_effective_charge(int id, float *charge) { if (charge == NULL) { return -1; } - if (id < 99) { + if (id <= DEDX_MAX_ELEMENT_ID) { *charge = (float) id; return 0; } @@ -98,7 +100,7 @@ int dedx_embedded_get_i_value(int target, int state, float *pot) { continue; } value = dedx_embedded_compos_rows[i].i_value; - if (state == 2 && !dedx_embedded_target_is_gas(target) && target <= 99) { + if (state == 2 && !dedx_embedded_target_is_gas(target) && target <= DEDX_MAX_ELEMENT_ID) { value *= 1.13f; } } diff --git a/src/dedx_spline.c b/src/dedx_spline.c index b568600..c1d4f25 100644 --- a/src/dedx_spline.c +++ b/src/dedx_spline.c @@ -79,7 +79,7 @@ calculate_linear_coefficients(dedx_internal_spline_base *coef, const float *ener } } -void dedx_internal_calculate_coefficients( +int dedx_internal_calculate_coefficients( dedx_internal_spline_base *coef, float *energy, float *stopping, int n, int interpolation_mode) { int i; float log_energy[DEDX_MAX_ELEMENTS]; @@ -91,17 +91,17 @@ void dedx_internal_calculate_coefficients( float z[DEDX_MAX_ELEMENTS]; if (n < 2) - return; + return interpolation_mode; if (interpolation_mode == DEDX_INTERPOLATION_LINEAR) { calculate_linear_coefficients(coef, energy, stopping, n); - return; + return DEDX_INTERPOLATION_LINEAR; } for (i = 0; i < n; i++) { if (energy[i] <= 0.0f || stopping[i] <= 0.0f) { calculate_linear_coefficients(coef, energy, stopping, n); - return; + return DEDX_INTERPOLATION_LINEAR; } coef[i].a = stopping[i]; coef[i].x = energy[i]; @@ -136,6 +136,8 @@ void dedx_internal_calculate_coefficients( coef[i].b = (log_stopping[i + 1] - log_stopping[i]) / h[i] - h[i] * (coef[i + 1].c + 2.0f * coef[i].c) / 3.0f; coef[i].d = (coef[i + 1].c - coef[i].c) / (3.0f * h[i]); } + + return DEDX_INTERPOLATION_LOG_LOG; } float dedx_internal_evaluate_spline( diff --git a/src/dedx_spline.h b/src/dedx_spline.h index 2583486..0d6d744 100644 --- a/src/dedx_spline.h +++ b/src/dedx_spline.h @@ -19,8 +19,13 @@ * @param[in] stopping Stopping-power values on @p energy. * @param[in] n Number of valid grid points. * @param[in] interpolation_mode Requested interpolation mode. + * @return The interpolation mode actually used: DEDX_INTERPOLATION_LINEAR if + * linear was requested, or if log-log was requested but the table + * contains a non-positive energy or stopping-power value and the + * implementation silently fell back to linear-space coefficients; + * DEDX_INTERPOLATION_LOG_LOG otherwise. */ -void dedx_internal_calculate_coefficients( +int dedx_internal_calculate_coefficients( dedx_internal_spline_base *coef, float *energy, float *stopping, int n, int interpolation_mode); /** @brief Evaluate a precomputed stopping-power spline at one energy value. diff --git a/src/dedx_validate.c b/src/dedx_validate.c index a7a50f8..2b064e7 100644 --- a/src/dedx_validate.c +++ b/src/dedx_validate.c @@ -18,7 +18,28 @@ int dedx_internal_set_names(dedx_config *config, int *err) { int dedx_internal_validate_rho(dedx_config *config, int *err) { if (config->rho <= 0.0 && config->target != 0) { - config->rho = dedx_internal_read_density(config->target, err); + int density_err = DEDX_OK; + float density = dedx_internal_read_density(config->target, &density_err); + + if (density_err == DEDX_OK) { + config->rho = density; + } else if (config->program >= DEDX_DEFAULT) { + /* Only programs that evaluate the Bethe-Bloch formula directly on this + * exact target (program >= DEDX_DEFAULT) definitely need rho here. + * Tabulated report lookups (PSTAR, ICRU*, MSTAR, ...) never read + * config->rho at all, and DEDX_AUTO only needs it if it falls through to + * the Bethe tier for a specific ion/element -- which load_bethe_2() now + * checks itself right before it would otherwise divide/log by a + * rho-derived term (see there), rather than speculatively failing every + * combination up front just because the embedded density table happens + * to be missing a row for this target. That used to block 407 + * tabulated-program combinations for exactly one such gap (FERROUSOXIDE, + * id 159) -- see issue #149 finding A5. */ + *err = density_err; + } + /* else: a tabulated program (or DEDX_AUTO, conditionally) with a target that + * has no embedded density row. config->rho stays <= 0.0; that's fine unless + * something downstream actually needs it, which is checked there instead. */ } else if (config->rho <= 0.0 && config->target == 0 && config->program >= 100) { *err = DEDX_ERR_RHO_REQUIRED; } @@ -34,6 +55,53 @@ static int dedx_internal_validate_interpolation_mode(dedx_config *config, int *e return 0; } +int dedx_internal_check_ion(int prog, int ion) { + const int *ion_list; + int i = 0; + + if (prog >= DEDX_DEFAULT || prog == DEDX_AUTO) { + /* The Bethe evaluation path (dedx_internal_get_atom_mass/charge, via + * dedx_periodic_table) only has data for ions 1-112, matching + * dedx_full_ion_list -- the same list dedx_get_ion_list() returns for these + * programs -- so accept nothing wider than that here. */ + if ((ion < 1) || (ion > 112)) + return 0; + return 1; + } + + ion_list = dedx_get_ion_list(prog); + while (ion_list[i] != -1) { + if (ion_list[i] == ion) + return 1; + ++i; + } + return 0; +} + +/* config->mstar_mode is only ever read on the MSTAR path (dedx_internal_convert_ + * energy_to_mstar(), via find_data()), so a garbage value left in it is otherwise + * inert for every other program -- don't reject a config over a field it will never + * use. On the MSTAR path itself, an unrecognized mode used to fall through + * dedx_internal_calculate_mspaul_coef()'s cascading if/else chain into a silent + * "illegal mode" branch that left the computed coefficient at 0, so the config loaded + * successfully but every stopping-power query came back as 0 with err == DEDX_OK + * (see issue #149 finding A7). '\0' (the calloc-zeroed default) means "use + * DEDX_MSTAR_MODE_DEFAULT" and is valid; anything else must be one of the six + * documented DEDX_MSTAR_MODE_* letters. */ +static int dedx_internal_validate_mstar_mode(dedx_config *config, int *err) { + char mode = config->mstar_mode; + + if (config->program != DEDX_MSTAR) { + return 0; + } + if (mode == '\0' || mode == DEDX_MSTAR_MODE_A || mode == DEDX_MSTAR_MODE_B || mode == DEDX_MSTAR_MODE_G + || mode == DEDX_MSTAR_MODE_H || mode == DEDX_MSTAR_MODE_C || mode == DEDX_MSTAR_MODE_D) { + return 0; + } + *err = DEDX_ERR_INVALID_MSTAR_MODE; + return -1; +} + int dedx_internal_evaluate_i_pot(dedx_config *config, int *err) { unsigned int i; @@ -87,7 +155,7 @@ int dedx_internal_evaluate_i_pot(dedx_config *config, int *err) { int dedx_internal_evaluate_compound(dedx_config *config, int *err) { unsigned int i = 0; - if (config->target > 0 && config->target <= 99) { + if (config->target > 0 && config->target <= DEDX_MAX_ELEMENT_ID) { *err = DEDX_OK; return 0; } @@ -189,6 +257,16 @@ int dedx_internal_validate_config(dedx_config *config, int *err) { * otherwise be mistaken for a validation error. */ *err = DEDX_OK; + if (!dedx_internal_check_ion(config->program, config->ion)) { + *err = DEDX_ERR_ION_NOT_SUPPORTED; + return -1; + } + + dedx_internal_validate_mstar_mode(config, err); + if (*err != 0) { + return -1; + } + dedx_internal_validate_interpolation_mode(config, err); if (*err != 0) { return -1; diff --git a/src/dedx_validate.h b/src/dedx_validate.h index ce908c4..7120219 100644 --- a/src/dedx_validate.h +++ b/src/dedx_validate.h @@ -10,4 +10,17 @@ int dedx_internal_validate_rho(dedx_config *config, int *err); int dedx_internal_evaluate_compound(dedx_config *config, int *err); int dedx_internal_calculate_element_i_pot(dedx_config *config, int *err); +/** @brief Whether `ion` is a valid projectile for `prog`. + * + * For DEDX_DEFAULT/DEDX_BETHE_EXT00/DEDX_AUTO this is the 1-112 range the periodic + * table backing the Bethe-Bloch evaluation covers (dedx_internal_get_atom_charge()/ + * get_atom_mass() enforce the same bound); for every other program it's membership + * in dedx_get_ion_list(prog). Shared between dedx.c (dedx_get_material_list_for_ion()) + * and dedx_internal_validate_config() below, which is the sole place + * dedx_load_config() enforces it -- previously only load_config_clean() did, so a + * custom compound (which dispatches straight to load_compound()) skipped it and + * could fail with a confusing DEDX_ERR_COMBINATION_NOT_FOUND instead of + * DEDX_ERR_ION_NOT_SUPPORTED for an unsupported ion (see issue #149 finding A8). */ +int dedx_internal_check_ion(int prog, int ion); + #endif // DEDX_VALIDATE_H diff --git a/tests/test_availability_exhaustive.c b/tests/test_availability_exhaustive.c index 8e89589..4f3791c 100644 --- a/tests/test_availability_exhaustive.c +++ b/tests/test_availability_exhaustive.c @@ -3,45 +3,81 @@ #include "test_helpers.h" /* - * Regression net for issue #149's findings A1, A4, A5 and A6 (Phase 1, item E2 of - * the plan of action there). + * Regression net for issue #149's findings A1, A2, A4, A5 and A6 (Phase 1, item E2 + * of the plan of action there). * * This sweeps every (program, ion, material) triple that dedx_get_material_list_for_ion() * advertises -- for every program except DEDX_ESTAR, which is unimplemented (see * DEDX_ERR_ESTAR_NOT_IMPL) -- and checks three things dedx_get_material_list_for_ion()'s * own contract implies should always hold for an advertised combination: * - * 1. dedx_load_config() succeeds (A1, A5: it currently doesn't for 407 of them -- - * A1's DEDX_AUTO tier-mixing bug and A5's missing ferrous-oxide density row). + * 1. dedx_load_config() succeeds. * 2. dedx_get_stp() accepts the program/ion pair's own advertised * dedx_get_min_energy()/dedx_get_max_energy() bounds (A4: the bounds are * documented as authoritative in dedx.h but are only "best-effort hints" in - * practice, and are rejected for 1568 combinations). + * practice -- still tracked here, but A4 itself is out of Phase 2's scope). * 3. At least one energy sampled across that range returns a finite, positive - * stopping power (A1a: 174 DEDX_AUTO configs load with err == DEDX_OK but then - * fail at *every* energy, because the spline knots silently end at x == 0). + * stopping power. * - * As of this writing (before any of A1/A4/A5/A6 are fixed) this sweep reproduces the - * exact counts from the issue's manual audit: 101957 combinations swept, 407 load - * failures, 1568 bound mismatches, 174 dead-at-every-energy configs. The BASELINE_* - * constants below pin those counts so this test is a *ratchet*, not a silent no-op: + * Before any of Phase 1/2's fixes, this sweep reproduced the issue's manual audit + * exactly: 101957 combinations, 407 load failures, 1568 bound mismatches, 174 + * dead-at-every-energy configs -- all 407 load failures and every one of the bound + * mismatches/dead configs traced back to two root causes: A1's DEDX_AUTO tier-mixing + * (mismatched per-constituent energy grids) and A5's missing FERROUSOXIDE density row. * - * - It stays green today by asserting "no worse than the known-broken baseline", - * rather than asserting "zero failures" (which would fail immediately and block - * Phase 1, whose job is only to make the failures visible, not fix them yet). - * - Phase 2 (A1, A4, A5, A6) must lower the relevant BASELINE_* constant(s) as each - * root cause is fixed, down to 0 once all four are done. Do NOT raise a baseline - * to make a newly introduced regression pass -- if this test starts failing - * because a count went *up*, that is a real regression, not a stale baseline. + * Phase 2 fixed A1, A2, A3, A5, A6, A7 and A8. Their effect on this sweep, verified by + * diffing this same sweep's per-(program,ion) output against the pre-Phase-2 build: + * + * - TOTAL_COMBINATIONS: 101957 -> 101886 (-71). A2's off-by-one fix + * (material_id_supported()'s element/compound boundary, id 99 = A150 tissue- + * equivalent plastic) stops routing compound id 99 through the elemental + * embedded-table lookup for ASTAR/PSTAR/MSTAR/ICRU73_OLD/ICRU73/ICRU49/ICRU (the + * 71 = 1+1+17+16+16+2+18 combinations across their ion lists -- one fewer per + * (program,ion) than before). It's now correctly evaluated via composition-based + * reachability, which drops it for the ion/program pairs where a constituent + * genuinely isn't reachable -- exactly the misclassification A2 set out to fix. + * - BASELINE_LOAD_FAILURES: 407 -> 1470. Two components, both traced by error code: + * * 224 (DEDX_ERR_TARGET_NOT_FOUND, programs DEDX_DEFAULT/DEDX_BETHE_EXT00 only): + * the FERROUSOXIDE (id 159) gap, down from 407. A5 added its density row and + * relaxed dedx_internal_validate_rho() to only require rho where it's actually + * used, which fixed the 183 tabulated-program combinations. The 224 that + * remain are DEDX_DEFAULT/DEDX_BETHE_EXT00, which read a compound target's + * *own* I-value directly from embedded metadata rather than Bragg-averaging + * it from constituents; no authoritative FERROUSOXIDE I-value exists (see + * data/README.md), and A5 deliberately left it unfabricated rather than + * invent a number -- so these 224 stay a documented, open gap. + * * 1246 (DEDX_ERR_INCONSISTENT_COMPOUND, DEDX_AUTO only, new): compounds whose + * constituents resolve onto mismatched energy grids (A1's tier-mixing bug). + * Before A1, load_compound() silently summed across the mismatched grids and + * dedx_load_config() reported success -- these combinations were counted + * instead under bound_mismatches/dead_configs below. A1 now rejects them + * instead of serving a wrong or unusable number, which is why they show up + * here as load failures rather than a regression. + * - BASELINE_BOUND_MISMATCHES: 1568 -> 480, BASELINE_DEAD_CONFIGS: 174 -> 0. Both + * drop because A1 catches the same underlying grid-mismatch combinations earlier, + * as a clean load failure, instead of letting them load and then fail (or return + * nonsense) at the energy-sampling stage. + * + * The BASELINE_* constants below still pin these counts so this test is a *ratchet*, + * not a silent no-op: + * + * - It stays green by asserting "no worse than the known baseline", rather than + * asserting "zero failures" -- A4 (advertised-bounds accuracy) and the + * FERROUSOXIDE I-value gap remain deliberately open past Phase 2. + * - Any future fix must lower the relevant BASELINE_* constant(s) as its root cause + * is addressed. Do NOT raise a baseline to make a newly introduced regression + * pass -- if this test starts failing because a count went up unexpectedly, that + * is a real regression, not a stale baseline. (The load_failures increase in + * Phase 2 above was verified, not assumed, before raising this constant.) * - TOTAL_COMBINATIONS is asserted with equality (not a ceiling) so a change in * what dedx_get_material_list_for_ion() advertises -- for better or worse -- is * always visible here, prompting a deliberate update rather than a silent drift. */ -#define TOTAL_COMBINATIONS 101957 -#define BASELINE_LOAD_FAILURES 407 -#define BASELINE_BOUND_MISMATCHES 1568 -#define BASELINE_DEAD_CONFIGS 174 +#define TOTAL_COMBINATIONS 101886 +#define BASELINE_LOAD_FAILURES 1470 +#define BASELINE_BOUND_MISMATCHES 480 +#define BASELINE_DEAD_CONFIGS 0 /* Number of energies sampled (log-spaced) across each combination's advertised * [min, max] range for the "dead at every energy" check. */ diff --git a/tests/test_error_codes.c b/tests/test_error_codes.c index 0607d59..92e4734 100644 --- a/tests/test_error_codes.c +++ b/tests/test_error_codes.c @@ -201,15 +201,19 @@ int main(void) { cfg->target = DEDX_WATER; err = 0; dedx_load_config(ws, cfg, &err); - /* Regression test: DEDX_DEFAULT/DEDX_BETHE_EXT00 with a compound target - * decomposes into elements (dedx_internal_evaluate_compound(), called eagerly for - * program >= 100) before check_ion() would ever run, so an invalid ion has to be - * caught inside the Bethe evaluation itself. It previously wasn't: load_bethe_2() - * called dedx_internal_get_atom_charge()/get_atom_mass() for the ion and then - * again for the (valid) target element, and the target's success silently - * overwrote the ion's DEDX_ERR_NOT_AN_ELEMENT, yielding a loaded config with a NaN - * stopping power instead of a load failure. */ - failures += check_err(err, DEDX_ERR_NOT_AN_ELEMENT, "DEFAULT with invalid ion and compound target"); + /* Regression test, updated for issue #149 finding A8: DEDX_DEFAULT/DEDX_BETHE_EXT00 + * with a compound target decomposes into elements (dedx_internal_evaluate_compound(), + * called eagerly for program >= 100) -- this used to happen before check_ion() would + * ever run, so an invalid ion was only ever caught deep inside the Bethe evaluation + * itself, as DEDX_ERR_NOT_AN_ELEMENT (a NULL dereference away from load_bethe_2()'s + * atom-charge/mass lookups silently overwriting the ion's own failure with the + * target's success, and yielding a loaded config with a NaN stopping power instead + * of a load failure). dedx_internal_validate_config() now calls + * dedx_internal_check_ion() unconditionally, before dedx_internal_evaluate_compound() + * ever runs, so this is now caught immediately and uniformly as + * DEDX_ERR_ION_NOT_SUPPORTED -- the same code the elemental-target case below already + * expects. */ + failures += check_err(err, DEDX_ERR_ION_NOT_SUPPORTED, "DEFAULT with invalid ion and compound target"); failures += check_err(cfg->loaded, 0, "failed load should not mark config loaded"); dedx_free_config(cfg, &err); dedx_free_workspace(ws, &err); @@ -253,6 +257,33 @@ int main(void) { dedx_free_config(cfg, &err); dedx_free_workspace(ws, &err); + /* Regression test for issue #149 finding A8's own reproducer: dedx_load_config() + * used to dispatch straight to load_compound() whenever elements_id != NULL, + * bypassing check_ion() entirely (only load_config_clean()'s tabulated-program + * path called it). A custom compound with an ion a tabulated, report-specific + * program like DEDX_PSTAR (proton-only) does not support therefore failed deep + * inside the Bragg decomposition with DEDX_ERR_COMBINATION_NOT_FOUND instead of + * the clear, immediate DEDX_ERR_ION_NOT_SUPPORTED an elemental target already got. + * dedx_internal_validate_config() now calls dedx_internal_check_ion() up front for + * every dedx_load_config() call, compound or not, so both paths agree. */ + ws = dedx_allocate_workspace(1, &err); + cfg = calloc(1, sizeof(dedx_config)); + cfg->program = DEDX_PSTAR; /* proton-only */ + cfg->ion = DEDX_CARBON; /* PSTAR does not support carbon */ + cfg->target = 0; + cfg->elements_length = 2; + cfg->elements_id = calloc(2, sizeof(int)); + cfg->elements_id[0] = DEDX_HYDROGEN; + cfg->elements_id[1] = DEDX_OXYGEN; + cfg->elements_atoms = calloc(2, sizeof(int)); + cfg->elements_atoms[0] = 2; + cfg->elements_atoms[1] = 1; + err = 0; + dedx_load_config(ws, cfg, &err); + failures += check_err(err, DEDX_ERR_ION_NOT_SUPPORTED, "PSTAR+carbon+custom compound (A8)"); + dedx_free_config(cfg, &err); + dedx_free_workspace(ws, &err); + failures += test_error_code_strings_complete(); return failures; diff --git a/tests/test_interpolation.c b/tests/test_interpolation.c index 05763be..3220e7d 100644 --- a/tests/test_interpolation.c +++ b/tests/test_interpolation.c @@ -293,6 +293,115 @@ static int check_interpolation_mode( return 0; } +/* + * Regression test for issue #149 finding A6: ion Z=11 (Sodium) in target 18 (Argon) + * is the one combination in the whole embedded ICRU73 table whose first tabulated + * stopping-power value is exactly 0.0. dedx_internal_calculate_coefficients() cannot + * represent a non-positive value in log-log space, so it silently falls back to a + * linear-space spline for the whole table -- silently, because before this fix + * ws->loaded_data[]->interpolation_mode still stored the caller's *requested* mode + * (DEDX_INTERPOLATION_LOG_LOG, the default), not the mode actually used. There was + * no way for a caller to detect the downgrade. dedx_get_effective_interpolation_mode() + * now reports what was actually used, so this must report DEDX_INTERPOLATION_LINEAR + * even though log-log (the default) was requested and nothing overrode it. + */ +static int check_effective_interpolation_mode_downgrade(void) { + int err = DEDX_OK; + int cleanup_err = DEDX_OK; + int requested_ok = 1; + int mode; + dedx_workspace *ws; + dedx_config *cfg = calloc(1, sizeof(dedx_config)); + + cfg->program = DEDX_ICRU73; + cfg->ion = DEDX_SODIUM; + cfg->target = DEDX_ARGON; + /* cfg->interpolation_mode left at 0 == DEDX_INTERPOLATION_LOG_LOG, the default. */ + + ws = dedx_allocate_workspace(1, &err); + dedx_load_config(ws, cfg, &err); + if (err != DEDX_OK) { + fprintf(stderr, "FAIL A6 load ICRU73+Na+Ar: err=%d\n", err); + dedx_free_config(cfg, &cleanup_err); + dedx_free_workspace(ws, &cleanup_err); + return 1; + } + + if (cfg->interpolation_mode != DEDX_INTERPOLATION_LOG_LOG) { + fprintf(stderr, "FAIL A6: requested mode was not log-log (got %d)\n", cfg->interpolation_mode); + requested_ok = 0; + } + + mode = dedx_get_effective_interpolation_mode(ws, cfg, &err); + dedx_free_config(cfg, &cleanup_err); + dedx_free_workspace(ws, &cleanup_err); + + if (err != DEDX_OK) { + fprintf(stderr, "FAIL A6 dedx_get_effective_interpolation_mode: err=%d\n", err); + return 1; + } + if (mode != DEDX_INTERPOLATION_LINEAR) { + fprintf(stderr, + "FAIL A6: effective mode should downgrade to linear for ICRU73+Na+Ar, got %d\n", + mode); + return 1; + } + return requested_ok ? 0 : 1; +} + +/* A well-behaved table (no non-positive values) must report its effective mode as + * exactly what was requested -- the downgrade above must not become unconditional. */ +static int check_effective_interpolation_mode_no_downgrade(void) { + int err = DEDX_OK; + int cleanup_err = DEDX_OK; + int mode; + dedx_workspace *ws; + dedx_config *cfg = calloc(1, sizeof(dedx_config)); + + cfg->program = DEDX_PSTAR; + cfg->ion = DEDX_PROTON; + cfg->target = DEDX_WATER; + + ws = dedx_allocate_workspace(1, &err); + dedx_load_config(ws, cfg, &err); + if (err != DEDX_OK) { + fprintf(stderr, "FAIL A6 load PSTAR+proton+water: err=%d\n", err); + dedx_free_config(cfg, &cleanup_err); + dedx_free_workspace(ws, &cleanup_err); + return 1; + } + + mode = dedx_get_effective_interpolation_mode(ws, cfg, &err); + dedx_free_config(cfg, &cleanup_err); + dedx_free_workspace(ws, &cleanup_err); + + if (err != DEDX_OK || mode != DEDX_INTERPOLATION_LOG_LOG) { + fprintf(stderr, "FAIL A6: PSTAR+proton+water should report log-log unchanged, got mode=%d err=%d\n", mode, err); + return 1; + } + return 0; +} + +/* dedx_get_effective_interpolation_mode() must guard cfg_id the same way dedx_get_stp() does. */ +static int check_effective_interpolation_mode_invalid_id(void) { + int failures = 0; + int err = DEDX_OK; + int mode; + dedx_workspace *ws = dedx_allocate_workspace(1, &err); + dedx_config *cfg = calloc(1, sizeof(dedx_config)); + + cfg->cfg_id = -1; + mode = dedx_get_effective_interpolation_mode(ws, cfg, &err); + if (err != DEDX_ERR_INVALID_DATASET_ID || mode != 0) { + fprintf(stderr, "FAIL A6 invalid cfg_id: err=%d mode=%d\n", err, mode); + failures++; + } + + dedx_free_config(cfg, &err); + dedx_free_workspace(ws, &err); + return failures; +} + static int check_internal_two_point_spline_cache(void) { dedx_internal_spline_base coef[2]; dedx_internal_lookup_accelerator acc; @@ -350,5 +459,9 @@ int main(void) { "ICRU73 explicit linear"); failures += check_internal_two_point_spline_cache(); + failures += check_effective_interpolation_mode_downgrade(); + failures += check_effective_interpolation_mode_no_downgrade(); + failures += check_effective_interpolation_mode_invalid_id(); + return failures; } diff --git a/tests/test_material_availability.c b/tests/test_material_availability.c index 3c7e855..56ee2b2 100644 --- a/tests/test_material_availability.c +++ b/tests/test_material_availability.c @@ -118,8 +118,9 @@ static int test_report_specific_programs_reject_boron(void) { return failures; } -/* ...while DEDX_AUTO falls back to the Bethe-Bloch formula for the same element, - * both directly and via Bragg-additivity decomposition of a Boron compound. */ +/* ...while DEDX_AUTO falls back to the Bethe-Bloch formula for the same element + * directly, and attempts Bragg-additivity decomposition for a Boron compound (see + * below for why that particular compound is expected to fail post-#149-A1). */ static int test_auto_bethe_fallback(void) { int failures = 0; int err; @@ -145,6 +146,16 @@ static int test_auto_bethe_fallback(void) { dedx_free_config(cfg, &err); dedx_free_workspace(ws, &err); + /* Boron carbide (B4C) is Bragg-decomposed into boron (not tabulated by any + * report -- Bethe fallback, 122-point grid) and carbon (tabulated -- PSTAR-family + * 133-point grid). This is the exact scenario issue #149's finding A1c used as its + * worked example of "silent range truncation": before that fix, load_compound() + * summed the two constituents' stopping powers over compound_data[0].length + * (whichever constituent happened to be first) regardless of whether the other + * constituent's own grid matched, silently truncating or mixing energy bins. + * Post-A1, mismatched per-constituent grids are detected and rejected with + * DEDX_ERR_INCONSISTENT_COMPOUND instead of being served as a wrong or + * silently-truncated number -- so this load is now expected to fail cleanly. */ ws = dedx_allocate_workspace(1, &err); cfg = calloc(1, sizeof(dedx_config)); cfg->program = DEDX_AUTO; @@ -152,8 +163,8 @@ static int test_auto_bethe_fallback(void) { cfg->target = DEDX_BORON_CARBIDE; err = 0; dedx_load_config(ws, cfg, &err); - failures += check_err(err, DEDX_OK, "AUTO+proton+BoronCarbide load"); - failures += check_err(cfg->bragg_used, 1, "AUTO+proton+BoronCarbide should decompose via Bragg additivity"); + failures += check_err(err, DEDX_ERR_INCONSISTENT_COMPOUND, "AUTO+proton+BoronCarbide load (mismatched grids)"); + failures += check_err(cfg->bragg_used, 1, "AUTO+proton+BoronCarbide should still attempt Bragg additivity"); dedx_free_config(cfg, &err); dedx_free_workspace(ws, &err); @@ -186,6 +197,173 @@ static int test_auto_bethe_fallback(void) { return failures; } +/* + * Regression test for issue #149 finding A2: material id 99 (A150 tissue-equivalent + * plastic) is a compound, one past the last real element id (DEDX_MAX_ELEMENT_ID == + * 98), but several `<= 99` / `> 99` off-by-one boundary checks used to treat it as an + * element instead -- evaluating the Bethe-Bloch formula with the atomic mass/charge of + * elemental einsteinium (Z=99) rather than decomposing A150's real composition. The + * issue's own suggested regression test: `bragg_used == 1` and Bethe ~= tabulated + * within a few % for id 99. Before the fix this was PSTAR=7.3376, BETHE=5.1799 (~29% + * off, Es-252's Z/A, not A150's); after the fix BETHE tracks PSTAR to well under 1%. */ +static int test_a150_boundary(void) { + int failures = 0; + int err; + dedx_workspace *ws; + dedx_config *cfg; + float pstar_stp, bethe_stp; + + pstar_stp = dedx_get_simple_stp_for_program(DEDX_PSTAR, DEDX_PROTON, DEDX_A150_TISSUE_EQUIVALENT_PLASTIC, 100.0f, &err); + failures += check_err(err, DEDX_OK, "PSTAR+proton+A150 stp"); + + ws = dedx_allocate_workspace(1, &err); + cfg = calloc(1, sizeof(dedx_config)); + cfg->program = DEDX_BETHE_EXT00; + cfg->ion = DEDX_PROTON; + cfg->target = DEDX_A150_TISSUE_EQUIVALENT_PLASTIC; + err = 0; + dedx_load_config(ws, cfg, &err); + failures += check_err(err, DEDX_OK, "BETHE_EXT00+proton+A150 load"); + failures += check_err(cfg->bragg_used, 1, "BETHE_EXT00+proton+A150 should Bragg-decompose, not treat id 99 as Z=99"); + if (err == DEDX_OK) { + bethe_stp = dedx_get_stp(ws, cfg, 100.0f, &err); + failures += check_err(err, DEDX_OK, "BETHE_EXT00+proton+A150 stp"); + if (err == DEDX_OK) { + float rel_diff = fabsf(bethe_stp - pstar_stp) / pstar_stp; + if (rel_diff > 0.02f) { + fprintf(stderr, + "FAIL A150 boundary: BETHE=%.4f vs PSTAR=%.4f differ by %.2f%% (expected < 2%%)\n", + (double) bethe_stp, + (double) pstar_stp, + (double) (rel_diff * 100.0f)); + failures++; + } + } + } + dedx_free_config(cfg, &err); + dedx_free_workspace(ws, &err); + + return failures; +} + +/* + * Regression test for issue #149 finding A5: dedx_internal_validate_rho() used to + * hard-fail whenever a target's embedded density row was missing, for *every* + * program -- even tabulated report lookups that never read config->rho at all. Only + * one material was affected (FERROUSOXIDE, id 159), but it broke the material for all + * 407 program/ion pairs that advertised it. The fix adds the missing density row and + * only requires it for programs that actually evaluate the Bethe-Bloch formula + * directly on the target (program >= DEDX_DEFAULT); tabulated programs like PSTAR + * must now load FERROUSOXIDE without the caller ever supplying rho themselves. */ +static int test_ferrous_oxide_tabulated_program(void) { + int failures = 0; + int err; + dedx_workspace *ws; + dedx_config *cfg; + float stp; + + ws = dedx_allocate_workspace(1, &err); + cfg = calloc(1, sizeof(dedx_config)); + cfg->program = DEDX_PSTAR; + cfg->ion = DEDX_PROTON; + cfg->target = DEDX_FERROUS_OXIDE; + err = 0; + dedx_load_config(ws, cfg, &err); + failures += check_err(err, DEDX_OK, "PSTAR+proton+FerrousOxide load"); + if (err == DEDX_OK) { + /* Density must have been resolved from the embedded table, not left at 0. */ + if (cfg->rho <= 0.0f) { + fprintf(stderr, "FAIL PSTAR+proton+FerrousOxide: rho not populated (%f)\n", (double) cfg->rho); + failures++; + } + stp = dedx_get_stp(ws, cfg, 100.0f, &err); + if (err != DEDX_OK || stp <= 0.0f) { + fprintf(stderr, "FAIL PSTAR+proton+FerrousOxide stp: err=%d stp=%f\n", err, (double) stp); + failures++; + } + } + dedx_free_config(cfg, &err); + dedx_free_workspace(ws, &err); + + return failures; +} + +/* + * Regression test for issue #149 finding A3: load_compound() used to seed each + * constituent's I-value via the public dedx_get_i_value(), which hardcodes + * DEDX_GAS -- so config->compound_state was silently ignored for every Bragg- + * decomposed compound whose elements_i_value wasn't already supplied by the caller + * (the common case for DEDX_AUTO, which never runs dedx_internal_evaluate_i_pot() + * since that only happens for program >= DEDX_DEFAULT). Boron is not itself + * tabulated by any report, so DEDX_AUTO resolves it through the Bethe-Bloch fallback + * inside load_compound(), where the I-value -- and hence compound_state -- directly + * affects the computed stopping power. A single-element "compound" isolates + * load_compound()'s own I-value lookup from Bragg-averaging effects. Before the fix, + * DEDX_GAS and DEDX_CONDENSED produced the identical (gas-phase) number; they must + * now differ, since DEDX_CONDENSED applies the solid-state I correction. */ +static int test_compound_state_affects_bethe_fallback(void) { + int failures = 0; + int err; + float gas_stp, condensed_stp; + + { + dedx_workspace *ws = dedx_allocate_workspace(1, &err); + dedx_config *cfg = calloc(1, sizeof(dedx_config)); + cfg->program = DEDX_AUTO; + cfg->ion = DEDX_PROTON; + cfg->target = 0; + cfg->compound_state = DEDX_GAS; + cfg->rho = 2.34f; /* boron's density; required for the Bethe fallback tier */ + cfg->elements_length = 1; + cfg->elements_id = calloc(1, sizeof(int)); + cfg->elements_id[0] = DEDX_BORON; + cfg->elements_atoms = calloc(1, sizeof(int)); + cfg->elements_atoms[0] = 1; + err = 0; + dedx_load_config(ws, cfg, &err); + failures += check_err(err, DEDX_OK, "AUTO+proton+gas-boron load"); + gas_stp = dedx_get_stp(ws, cfg, 100.0f, &err); + failures += check_err(err, DEDX_OK, "AUTO+proton+gas-boron stp"); + dedx_free_config(cfg, &err); + dedx_free_workspace(ws, &err); + } + { + dedx_workspace *ws = dedx_allocate_workspace(1, &err); + dedx_config *cfg = calloc(1, sizeof(dedx_config)); + cfg->program = DEDX_AUTO; + cfg->ion = DEDX_PROTON; + cfg->target = 0; + cfg->compound_state = DEDX_CONDENSED; + cfg->rho = 2.34f; + cfg->elements_length = 1; + cfg->elements_id = calloc(1, sizeof(int)); + cfg->elements_id[0] = DEDX_BORON; + cfg->elements_atoms = calloc(1, sizeof(int)); + cfg->elements_atoms[0] = 1; + err = 0; + dedx_load_config(ws, cfg, &err); + failures += check_err(err, DEDX_OK, "AUTO+proton+condensed-boron load"); + condensed_stp = dedx_get_stp(ws, cfg, 100.0f, &err); + failures += check_err(err, DEDX_OK, "AUTO+proton+condensed-boron stp"); + dedx_free_config(cfg, &err); + dedx_free_workspace(ws, &err); + } + + if (gas_stp > 0.0f && condensed_stp > 0.0f) { + float rel_diff = fabsf(condensed_stp - gas_stp) / gas_stp; + if (rel_diff < 1e-3f) { + fprintf(stderr, + "FAIL A3: gas (%.6f) and condensed (%.6f) boron stp are identical -- " + "compound_state is not reaching load_compound()'s I-value lookup\n", + (double) gas_stp, + (double) condensed_stp); + failures++; + } + } + + return failures; +} + static int test_material_list_for_ion(void) { int failures = 0; int materials[DEDX_MAX_MATERIAL_LIST + 1]; @@ -284,6 +462,9 @@ int main(void) { failures += test_corrected_static_tables(); failures += test_report_specific_programs_reject_boron(); failures += test_auto_bethe_fallback(); + failures += test_a150_boundary(); + failures += test_ferrous_oxide_tabulated_program(); + failures += test_compound_state_affects_bethe_fallback(); failures += test_material_list_for_ion(); failures += test_fill_material_list_for_ion(); diff --git a/tests/test_mstar.c b/tests/test_mstar.c index 346455c..614d412 100644 --- a/tests/test_mstar.c +++ b/tests/test_mstar.c @@ -14,6 +14,60 @@ static int report_mode_equivalence_error(const char *stage, const char *label, i return 1; } +static int check_err(int got, int expected, const char *label) { + if (got != expected) { + fprintf(stderr, "FAIL %s: got err=%d, expected %d\n", label, got, expected); + return 1; + } + return 0; +} + +/* + * Regression test for issue #149 finding A7: an mstar_mode outside the documented + * DEDX_MSTAR_MODE_* set used to fall through dedx_mpaul.c's mode switch to an empty + * "illegal mode" branch, silently leaving the computed table all zero with err == 0 + * instead of reporting an error. dedx_internal_validate_config() must now reject an + * unrecognized mode up front, and leave every documented mode (including the '\0' + * default) working exactly as before. */ +static int test_invalid_mstar_mode(void) { + int failures = 0; + int err = 0; + dedx_config *cfg = make_mstar_mode_config(DEDX_WATER, 'Z'); + dedx_workspace *ws = dedx_allocate_workspace(1, &err); + + dedx_load_config(ws, cfg, &err); + failures += check_err(err, DEDX_ERR_INVALID_MSTAR_MODE, "MSTAR invalid mode 'Z'"); + dedx_free_config(cfg, &err); + dedx_free_workspace(ws, &err); + + /* The documented default ('\0', unset by a caller who never touches mstar_mode) + * must still validate cleanly -- the new check must not reject the common case. */ + err = 0; + cfg = make_mstar_mode_config(DEDX_WATER, '\0'); + ws = dedx_allocate_workspace(1, &err); + dedx_load_config(ws, cfg, &err); + failures += check_err(err, DEDX_OK, "MSTAR default mode '\\0'"); + dedx_free_config(cfg, &err); + dedx_free_workspace(ws, &err); + + /* mstar_mode is only meaningful for DEDX_MSTAR itself; an unrelated program must + * not be rejected just because a caller left garbage in the field (e.g. a struct + * reused across loads without being re-zeroed). */ + err = 0; + cfg = calloc(1, sizeof(dedx_config)); + cfg->program = DEDX_PSTAR; + cfg->ion = DEDX_PROTON; + cfg->target = DEDX_WATER; + cfg->mstar_mode = 'Z'; + ws = dedx_allocate_workspace(1, &err); + dedx_load_config(ws, cfg, &err); + failures += check_err(err, DEDX_OK, "PSTAR ignores unrelated garbage mstar_mode"); + dedx_free_config(cfg, &err); + dedx_free_workspace(ws, &err); + + return failures; +} + static int check_mode_equivalence(int target, char lhs_mode, char rhs_mode, float energy, const char *label) { int err = 0; int failures = 0; @@ -171,5 +225,7 @@ int main(void) { failures += check_mode_equivalence(DEDX_AIR_DRY_NEAR_SEA_LEVEL, DEDX_MSTAR_MODE_B, DEDX_MSTAR_MODE_H, 10.0f, "mstar-b-gas"); + failures += test_invalid_mstar_mode(); + return failures; } diff --git a/tests/test_validate_internal.c b/tests/test_validate_internal.c index 25b7867..8202972 100644 --- a/tests/test_validate_internal.c +++ b/tests/test_validate_internal.c @@ -48,6 +48,7 @@ static int test_validate_config_custom_atoms(void) { dedx_config *cfg = alloc_config(); cfg->program = DEDX_PSTAR; + cfg->ion = DEDX_PROTON; cfg->target = 0; cfg->elements_length = 2; cfg->elements_id = calloc(2, sizeof(int)); @@ -85,6 +86,7 @@ static int test_validate_config_bethe_custom_atoms(void) { dedx_config *cfg = alloc_config(); cfg->program = DEDX_BETHE_EXT00; + cfg->ion = DEDX_PROTON; cfg->target = 0; cfg->rho = 1.0f; /* required for Bethe-type programs with a custom compound */ cfg->elements_length = 2; @@ -124,6 +126,7 @@ static int test_validate_config_bethe_mass_fraction(void) { dedx_config *cfg = alloc_config(); cfg->program = DEDX_BETHE_EXT00; + cfg->ion = DEDX_PROTON; cfg->target = 0; cfg->rho = 1.0f; cfg->elements_length = 2; @@ -160,6 +163,7 @@ static int test_validate_config_resets_stale_err(void) { dedx_config *cfg = alloc_config(); cfg->program = DEDX_PSTAR; + cfg->ion = DEDX_PROTON; cfg->target = 0; cfg->elements_length = 2; cfg->elements_id = calloc(2, sizeof(int)); From 3976f50a65d91105059145d1acddc30818279742 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 18:16:42 +0000 Subject: [PATCH 2/6] Apply clang-format-19 (CI's exact formatter version) 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 Claude-Session: https://claude.ai/code/session_0188VRnSFzb7HvNKcMX7XT1Z --- src/data/embedded/dedx_metadata.h | 426 +++++++++++++++++++---------- src/dedx_embedded_metadata.c | 3 +- tests/test_interpolation.c | 4 +- tests/test_material_availability.c | 6 +- 4 files changed, 290 insertions(+), 149 deletions(-) diff --git a/src/data/embedded/dedx_metadata.h b/src/data/embedded/dedx_metadata.h index 43baa2c..c26c2c3 100644 --- a/src/data/embedded/dedx_metadata.h +++ b/src/data/embedded/dedx_metadata.h @@ -20,89 +20,172 @@ typedef struct { /** @brief Embedded density and I-value metadata rows keyed by material ID. */ static const dedx_embedded_compos_row dedx_embedded_compos_rows[286] = { - {1, 8.37480000e-05f, 1.92000000e+01f, 1}, {1, 8.37480000e-05f, 2.18000000e+01f, 2}, - {2, 1.66322000e-04f, 4.18000000e+01f, 0}, {3, 5.34000000e-01f, 4.00000000e+01f, 0}, - {4, 1.84800000e+00f, 6.37000000e+01f, 0}, {5, 2.37000000e+00f, 7.60000000e+01f, 0}, - {6, 2.00000000e+00f, 7.00000000e+01f, 1}, {6, 2.00000000e+00f, 8.10000000e+01f, 2}, - {906, 1.70000000e+00f, 7.80000000e+01f, 0}, {7, 1.16528000e-03f, 8.20000000e+01f, 1}, - {7, 1.16528000e-03f, 8.20000000e+01f, 2}, {8, 1.33151000e-03f, 9.50000000e+01f, 1}, - {8, 1.33151000e-03f, 1.06000000e+02f, 2}, {9, 1.58029000e-03f, 1.15000000e+02f, 1}, - {9, 1.58029000e-03f, 1.12000000e+02f, 2}, {10, 8.38505000e-04f, 1.37000000e+02f, 0}, - {11, 9.71000000e-01f, 1.49000000e+02f, 0}, {12, 1.74000000e+00f, 1.56000000e+02f, 0}, - {13, 2.69890000e+00f, 1.66000000e+02f, 0}, {14, 2.33000000e+00f, 1.73000000e+02f, 0}, - {15, 2.20000000e+00f, 1.73000000e+02f, 0}, {16, 2.00000000e+00f, 1.80000000e+02f, 0}, - {17, 2.99473000e-03f, 1.74000000e+02f, 1}, {17, 2.99473000e-03f, 1.80000000e+02f, 2}, - {18, 1.66201000e-03f, 1.88000000e+02f, 0}, {19, 8.62000000e-01f, 1.90000000e+02f, 0}, - {20, 1.55000000e+00f, 1.91000000e+02f, 0}, {21, 2.98900000e+00f, 2.16000000e+02f, 0}, - {22, 4.54000000e+00f, 2.33000000e+02f, 0}, {23, 6.11000000e+00f, 2.45000000e+02f, 0}, - {24, 7.18000000e+00f, 2.57000000e+02f, 0}, {25, 7.44000000e+00f, 2.72000000e+02f, 0}, - {26, 7.87400000e+00f, 2.86000000e+02f, 0}, {27, 8.90000000e+00f, 2.97000000e+02f, 0}, - {28, 8.90200000e+00f, 3.11000000e+02f, 0}, {29, 8.96000000e+00f, 3.22000000e+02f, 0}, - {30, 7.13300000e+00f, 3.30000000e+02f, 0}, {31, 5.90400000e+00f, 3.34000000e+02f, 0}, - {32, 5.32300000e+00f, 3.50000000e+02f, 0}, {33, 5.73000000e+00f, 3.47000000e+02f, 0}, - {34, 4.50000000e+00f, 3.48000000e+02f, 0}, {35, 7.07218000e-03f, 3.43000000e+02f, 0}, - {36, 3.47832000e-03f, 3.52000000e+02f, 0}, {37, 1.53200000e+00f, 3.63000000e+02f, 0}, - {38, 2.54000000e+00f, 3.66000000e+02f, 0}, {39, 4.46900000e+00f, 3.79000000e+02f, 0}, - {40, 6.50600000e+00f, 3.93000000e+02f, 0}, {41, 8.57000000e+00f, 4.17000000e+02f, 0}, - {42, 1.02200000e+01f, 4.24000000e+02f, 0}, {43, 1.15000000e+01f, 4.28000000e+02f, 0}, - {44, 1.24100000e+01f, 4.41000000e+02f, 0}, {45, 1.24100000e+01f, 4.49000000e+02f, 0}, - {46, 1.20200000e+01f, 4.70000000e+02f, 0}, {47, 1.05000000e+01f, 4.70000000e+02f, 0}, - {48, 8.65000000e+00f, 4.69000000e+02f, 0}, {49, 7.31000000e+00f, 4.88000000e+02f, 0}, - {50, 7.31000000e+00f, 4.88000000e+02f, 0}, {51, 6.69100000e+00f, 4.87000000e+02f, 0}, - {52, 6.24000000e+00f, 4.85000000e+02f, 0}, {53, 4.93000000e+00f, 4.91000000e+02f, 0}, - {54, 5.48536000e-03f, 4.82000000e+02f, 0}, {55, 1.87300000e+00f, 4.88000000e+02f, 0}, - {56, 3.50000000e+00f, 4.91000000e+02f, 0}, {57, 6.15400000e+00f, 5.01000000e+02f, 0}, - {58, 6.65700000e+00f, 5.23000000e+02f, 0}, {59, 6.71000000e+00f, 5.35000000e+02f, 0}, - {60, 6.90000000e+00f, 5.46000000e+02f, 0}, {61, 7.22000000e+00f, 5.60000000e+02f, 0}, - {62, 7.46000000e+00f, 5.74000000e+02f, 0}, {63, 5.24300000e+00f, 5.80000000e+02f, 0}, - {64, 7.90040000e+00f, 5.91000000e+02f, 0}, {65, 8.22900000e+00f, 6.14000000e+02f, 0}, - {66, 8.55000000e+00f, 6.28000000e+02f, 0}, {67, 8.79500000e+00f, 6.50000000e+02f, 0}, - {68, 9.06600000e+00f, 6.58000000e+02f, 0}, {69, 9.32100000e+00f, 6.74000000e+02f, 0}, - {70, 6.73000000e+00f, 6.84000000e+02f, 0}, {71, 9.84000000e+00f, 6.94000000e+02f, 0}, - {72, 1.33100000e+01f, 7.05000000e+02f, 0}, {73, 1.66540000e+01f, 7.18000000e+02f, 0}, - {74, 1.93000000e+01f, 7.27000000e+02f, 0}, {75, 2.10200000e+01f, 7.36000000e+02f, 0}, - {76, 2.25700000e+01f, 7.46000000e+02f, 0}, {77, 2.24200000e+01f, 7.57000000e+02f, 0}, - {78, 2.14500000e+01f, 7.90000000e+02f, 0}, {79, 1.93200000e+01f, 7.90000000e+02f, 0}, - {80, 1.35460000e+01f, 8.00000000e+02f, 0}, {81, 1.17200000e+01f, 8.10000000e+02f, 0}, - {82, 1.13500000e+01f, 8.23000000e+02f, 0}, {83, 9.74700000e+00f, 8.23000000e+02f, 0}, - {84, 9.32000000e+00f, 8.30000000e+02f, 0}, {85, 9.32000000e+00f, 8.25000000e+02f, 0}, - {86, 9.06618000e-03f, 7.94000000e+02f, 0}, {87, 1.00000000e+00f, 8.27000000e+02f, 0}, - {88, 5.00000000e+00f, 8.26000000e+02f, 0}, {89, 1.00700000e+01f, 8.41000000e+02f, 0}, - {90, 1.17200000e+01f, 8.47000000e+02f, 0}, {91, 1.53700000e+01f, 8.78000000e+02f, 0}, - {92, 1.89500000e+01f, 8.90000000e+02f, 0}, {93, 2.02500000e+01f, 9.02000000e+02f, 0}, - {94, 1.98400000e+01f, 9.21000000e+02f, 0}, {95, 1.36700000e+01f, 9.34000000e+02f, 0}, - {96, 1.35100000e+01f, 9.39000000e+02f, 0}, {97, 1.40000000e+01f, 9.52000000e+02f, 0}, - {98, 1.00000000e+01f, 9.66000000e+02f, 0}, {99, 1.12700000e+00f, 6.51000000e+01f, 0}, - {100, 7.89900000e-01f, 6.42000000e+01f, 0}, {101, 1.09670000e-03f, 5.82000000e+01f, 0}, - {102, 1.35000000e+00f, 7.14000000e+01f, 0}, {103, 9.20000000e-01f, 6.32000000e+01f, 0}, - {104, 1.20479000e-03f, 8.57000000e+01f, 0}, {105, 1.42000000e+00f, 7.19000000e+01f, 0}, - {106, 3.97000000e+00f, 1.45200000e+02f, 0}, {107, 1.10000000e+00f, 6.32000000e+01f, 0}, - {108, 8.26019000e-04f, 5.37000000e+01f, 0}, {109, 1.02350000e+00f, 6.62000000e+01f, 0}, - {110, 1.28300000e+00f, 6.95000000e+01f, 0}, {111, 1.45000000e+00f, 8.59000000e+01f, 0}, - {112, 1.25000000e+00f, 7.24000000e+01f, 0}, {113, 4.89000000e+00f, 3.75900000e+02f, 0}, - {114, 4.50000000e+00f, 2.85700000e+02f, 0}, {115, 8.78650000e-01f, 6.34000000e+01f, 0}, - {116, 3.01000000e+00f, 9.32000000e+01f, 0}, {117, 7.13000000e+00f, 5.34100000e+02f, 0}, - {118, 1.06000000e+00f, 7.52000000e+01f, 0}, {119, 1.85000000e+00f, 9.19000000e+01f, 0}, - {120, 1.85000000e+00f, 1.06400000e+02f, 0}, {121, 2.52000000e+00f, 8.47000000e+01f, 0}, - {122, 1.81200000e+00f, 9.96000000e+01f, 0}, {123, 1.03000000e+00f, 7.33000000e+01f, 0}, - {124, 2.49343000e-03f, 4.83000000e+01f, 0}, {125, 8.09800000e-01f, 5.99000000e+01f, 0}, - {126, 1.76000000e+00f, 8.68000000e+01f, 0}, {127, 6.20000000e+00f, 5.39300000e+02f, 0}, - {128, 7.90000000e+00f, 4.68300000e+02f, 0}, {129, 2.80000000e+00f, 1.36400000e+02f, 0}, - {130, 3.18000000e+00f, 1.66000000e+02f, 0}, {131, 3.30000000e+00f, 1.76100000e+02f, 0}, - {132, 2.96000000e+00f, 1.52300000e+02f, 0}, {133, 6.06200000e+00f, 3.95000000e+02f, 0}, - {134, 1.84212000e-03f, 8.50000000e+01f, 0}, {135, 1.59400000e+00f, 1.66300000e+02f, 0}, - {136, 1.42000000e+00f, 7.76000000e+01f, 0}, {137, 1.20000000e+00f, 7.46000000e+01f, 0}, - {138, 1.49000000e+00f, 8.70000000e+01f, 0}, {139, 1.03000000e+00f, 7.67000000e+01f, 0}, - {140, 4.11500000e+00f, 4.40700000e+02f, 0}, {141, 4.51000000e+00f, 5.53100000e+02f, 0}, - {142, 1.10580000e+00f, 8.91000000e+01f, 0}, {143, 1.48320000e+00f, 1.56000000e+02f, 0}, - {144, 2.30000000e+00f, 1.35200000e+02f, 0}, {145, 7.79000000e-01f, 5.64000000e+01f, 0}, - {146, 1.30480000e+00f, 1.06500000e+02f, 0}, {147, 1.21990000e+00f, 1.03300000e+02f, 0}, - {148, 1.23510000e+00f, 1.11900000e+02f, 0}, {149, 7.13780000e-01f, 6.00000000e+01f, 0}, - {150, 9.48700000e-01f, 6.66000000e+01f, 0}, {151, 1.10140000e+00f, 9.86000000e+01f, 0}, - {152, 1.25324000e-03f, 4.54000000e+01f, 0}, {153, 7.89300000e-01f, 6.29000000e+01f, 0}, - {154, 1.13000000e+00f, 6.93000000e+01f, 0}, {155, 1.17497000e-03f, 5.07000000e+01f, 0}, - {156, 1.10000000e+00f, 7.33000000e+01f, 0}, {157, 5.20000000e+00f, 2.27300000e+02f, 0}, - {158, 7.15000000e+00f, 2.61000000e+02f, 0}, {158, 7.15000000e+00f, 2.61000000e+02f, 0}, + {1, 8.37480000e-05f, 1.92000000e+01f, 1}, + {1, 8.37480000e-05f, 2.18000000e+01f, 2}, + {2, 1.66322000e-04f, 4.18000000e+01f, 0}, + {3, 5.34000000e-01f, 4.00000000e+01f, 0}, + {4, 1.84800000e+00f, 6.37000000e+01f, 0}, + {5, 2.37000000e+00f, 7.60000000e+01f, 0}, + {6, 2.00000000e+00f, 7.00000000e+01f, 1}, + {6, 2.00000000e+00f, 8.10000000e+01f, 2}, + {906, 1.70000000e+00f, 7.80000000e+01f, 0}, + {7, 1.16528000e-03f, 8.20000000e+01f, 1}, + {7, 1.16528000e-03f, 8.20000000e+01f, 2}, + {8, 1.33151000e-03f, 9.50000000e+01f, 1}, + {8, 1.33151000e-03f, 1.06000000e+02f, 2}, + {9, 1.58029000e-03f, 1.15000000e+02f, 1}, + {9, 1.58029000e-03f, 1.12000000e+02f, 2}, + {10, 8.38505000e-04f, 1.37000000e+02f, 0}, + {11, 9.71000000e-01f, 1.49000000e+02f, 0}, + {12, 1.74000000e+00f, 1.56000000e+02f, 0}, + {13, 2.69890000e+00f, 1.66000000e+02f, 0}, + {14, 2.33000000e+00f, 1.73000000e+02f, 0}, + {15, 2.20000000e+00f, 1.73000000e+02f, 0}, + {16, 2.00000000e+00f, 1.80000000e+02f, 0}, + {17, 2.99473000e-03f, 1.74000000e+02f, 1}, + {17, 2.99473000e-03f, 1.80000000e+02f, 2}, + {18, 1.66201000e-03f, 1.88000000e+02f, 0}, + {19, 8.62000000e-01f, 1.90000000e+02f, 0}, + {20, 1.55000000e+00f, 1.91000000e+02f, 0}, + {21, 2.98900000e+00f, 2.16000000e+02f, 0}, + {22, 4.54000000e+00f, 2.33000000e+02f, 0}, + {23, 6.11000000e+00f, 2.45000000e+02f, 0}, + {24, 7.18000000e+00f, 2.57000000e+02f, 0}, + {25, 7.44000000e+00f, 2.72000000e+02f, 0}, + {26, 7.87400000e+00f, 2.86000000e+02f, 0}, + {27, 8.90000000e+00f, 2.97000000e+02f, 0}, + {28, 8.90200000e+00f, 3.11000000e+02f, 0}, + {29, 8.96000000e+00f, 3.22000000e+02f, 0}, + {30, 7.13300000e+00f, 3.30000000e+02f, 0}, + {31, 5.90400000e+00f, 3.34000000e+02f, 0}, + {32, 5.32300000e+00f, 3.50000000e+02f, 0}, + {33, 5.73000000e+00f, 3.47000000e+02f, 0}, + {34, 4.50000000e+00f, 3.48000000e+02f, 0}, + {35, 7.07218000e-03f, 3.43000000e+02f, 0}, + {36, 3.47832000e-03f, 3.52000000e+02f, 0}, + {37, 1.53200000e+00f, 3.63000000e+02f, 0}, + {38, 2.54000000e+00f, 3.66000000e+02f, 0}, + {39, 4.46900000e+00f, 3.79000000e+02f, 0}, + {40, 6.50600000e+00f, 3.93000000e+02f, 0}, + {41, 8.57000000e+00f, 4.17000000e+02f, 0}, + {42, 1.02200000e+01f, 4.24000000e+02f, 0}, + {43, 1.15000000e+01f, 4.28000000e+02f, 0}, + {44, 1.24100000e+01f, 4.41000000e+02f, 0}, + {45, 1.24100000e+01f, 4.49000000e+02f, 0}, + {46, 1.20200000e+01f, 4.70000000e+02f, 0}, + {47, 1.05000000e+01f, 4.70000000e+02f, 0}, + {48, 8.65000000e+00f, 4.69000000e+02f, 0}, + {49, 7.31000000e+00f, 4.88000000e+02f, 0}, + {50, 7.31000000e+00f, 4.88000000e+02f, 0}, + {51, 6.69100000e+00f, 4.87000000e+02f, 0}, + {52, 6.24000000e+00f, 4.85000000e+02f, 0}, + {53, 4.93000000e+00f, 4.91000000e+02f, 0}, + {54, 5.48536000e-03f, 4.82000000e+02f, 0}, + {55, 1.87300000e+00f, 4.88000000e+02f, 0}, + {56, 3.50000000e+00f, 4.91000000e+02f, 0}, + {57, 6.15400000e+00f, 5.01000000e+02f, 0}, + {58, 6.65700000e+00f, 5.23000000e+02f, 0}, + {59, 6.71000000e+00f, 5.35000000e+02f, 0}, + {60, 6.90000000e+00f, 5.46000000e+02f, 0}, + {61, 7.22000000e+00f, 5.60000000e+02f, 0}, + {62, 7.46000000e+00f, 5.74000000e+02f, 0}, + {63, 5.24300000e+00f, 5.80000000e+02f, 0}, + {64, 7.90040000e+00f, 5.91000000e+02f, 0}, + {65, 8.22900000e+00f, 6.14000000e+02f, 0}, + {66, 8.55000000e+00f, 6.28000000e+02f, 0}, + {67, 8.79500000e+00f, 6.50000000e+02f, 0}, + {68, 9.06600000e+00f, 6.58000000e+02f, 0}, + {69, 9.32100000e+00f, 6.74000000e+02f, 0}, + {70, 6.73000000e+00f, 6.84000000e+02f, 0}, + {71, 9.84000000e+00f, 6.94000000e+02f, 0}, + {72, 1.33100000e+01f, 7.05000000e+02f, 0}, + {73, 1.66540000e+01f, 7.18000000e+02f, 0}, + {74, 1.93000000e+01f, 7.27000000e+02f, 0}, + {75, 2.10200000e+01f, 7.36000000e+02f, 0}, + {76, 2.25700000e+01f, 7.46000000e+02f, 0}, + {77, 2.24200000e+01f, 7.57000000e+02f, 0}, + {78, 2.14500000e+01f, 7.90000000e+02f, 0}, + {79, 1.93200000e+01f, 7.90000000e+02f, 0}, + {80, 1.35460000e+01f, 8.00000000e+02f, 0}, + {81, 1.17200000e+01f, 8.10000000e+02f, 0}, + {82, 1.13500000e+01f, 8.23000000e+02f, 0}, + {83, 9.74700000e+00f, 8.23000000e+02f, 0}, + {84, 9.32000000e+00f, 8.30000000e+02f, 0}, + {85, 9.32000000e+00f, 8.25000000e+02f, 0}, + {86, 9.06618000e-03f, 7.94000000e+02f, 0}, + {87, 1.00000000e+00f, 8.27000000e+02f, 0}, + {88, 5.00000000e+00f, 8.26000000e+02f, 0}, + {89, 1.00700000e+01f, 8.41000000e+02f, 0}, + {90, 1.17200000e+01f, 8.47000000e+02f, 0}, + {91, 1.53700000e+01f, 8.78000000e+02f, 0}, + {92, 1.89500000e+01f, 8.90000000e+02f, 0}, + {93, 2.02500000e+01f, 9.02000000e+02f, 0}, + {94, 1.98400000e+01f, 9.21000000e+02f, 0}, + {95, 1.36700000e+01f, 9.34000000e+02f, 0}, + {96, 1.35100000e+01f, 9.39000000e+02f, 0}, + {97, 1.40000000e+01f, 9.52000000e+02f, 0}, + {98, 1.00000000e+01f, 9.66000000e+02f, 0}, + {99, 1.12700000e+00f, 6.51000000e+01f, 0}, + {100, 7.89900000e-01f, 6.42000000e+01f, 0}, + {101, 1.09670000e-03f, 5.82000000e+01f, 0}, + {102, 1.35000000e+00f, 7.14000000e+01f, 0}, + {103, 9.20000000e-01f, 6.32000000e+01f, 0}, + {104, 1.20479000e-03f, 8.57000000e+01f, 0}, + {105, 1.42000000e+00f, 7.19000000e+01f, 0}, + {106, 3.97000000e+00f, 1.45200000e+02f, 0}, + {107, 1.10000000e+00f, 6.32000000e+01f, 0}, + {108, 8.26019000e-04f, 5.37000000e+01f, 0}, + {109, 1.02350000e+00f, 6.62000000e+01f, 0}, + {110, 1.28300000e+00f, 6.95000000e+01f, 0}, + {111, 1.45000000e+00f, 8.59000000e+01f, 0}, + {112, 1.25000000e+00f, 7.24000000e+01f, 0}, + {113, 4.89000000e+00f, 3.75900000e+02f, 0}, + {114, 4.50000000e+00f, 2.85700000e+02f, 0}, + {115, 8.78650000e-01f, 6.34000000e+01f, 0}, + {116, 3.01000000e+00f, 9.32000000e+01f, 0}, + {117, 7.13000000e+00f, 5.34100000e+02f, 0}, + {118, 1.06000000e+00f, 7.52000000e+01f, 0}, + {119, 1.85000000e+00f, 9.19000000e+01f, 0}, + {120, 1.85000000e+00f, 1.06400000e+02f, 0}, + {121, 2.52000000e+00f, 8.47000000e+01f, 0}, + {122, 1.81200000e+00f, 9.96000000e+01f, 0}, + {123, 1.03000000e+00f, 7.33000000e+01f, 0}, + {124, 2.49343000e-03f, 4.83000000e+01f, 0}, + {125, 8.09800000e-01f, 5.99000000e+01f, 0}, + {126, 1.76000000e+00f, 8.68000000e+01f, 0}, + {127, 6.20000000e+00f, 5.39300000e+02f, 0}, + {128, 7.90000000e+00f, 4.68300000e+02f, 0}, + {129, 2.80000000e+00f, 1.36400000e+02f, 0}, + {130, 3.18000000e+00f, 1.66000000e+02f, 0}, + {131, 3.30000000e+00f, 1.76100000e+02f, 0}, + {132, 2.96000000e+00f, 1.52300000e+02f, 0}, + {133, 6.06200000e+00f, 3.95000000e+02f, 0}, + {134, 1.84212000e-03f, 8.50000000e+01f, 0}, + {135, 1.59400000e+00f, 1.66300000e+02f, 0}, + {136, 1.42000000e+00f, 7.76000000e+01f, 0}, + {137, 1.20000000e+00f, 7.46000000e+01f, 0}, + {138, 1.49000000e+00f, 8.70000000e+01f, 0}, + {139, 1.03000000e+00f, 7.67000000e+01f, 0}, + {140, 4.11500000e+00f, 4.40700000e+02f, 0}, + {141, 4.51000000e+00f, 5.53100000e+02f, 0}, + {142, 1.10580000e+00f, 8.91000000e+01f, 0}, + {143, 1.48320000e+00f, 1.56000000e+02f, 0}, + {144, 2.30000000e+00f, 1.35200000e+02f, 0}, + {145, 7.79000000e-01f, 5.64000000e+01f, 0}, + {146, 1.30480000e+00f, 1.06500000e+02f, 0}, + {147, 1.21990000e+00f, 1.03300000e+02f, 0}, + {148, 1.23510000e+00f, 1.11900000e+02f, 0}, + {149, 7.13780000e-01f, 6.00000000e+01f, 0}, + {150, 9.48700000e-01f, 6.66000000e+01f, 0}, + {151, 1.10140000e+00f, 9.86000000e+01f, 0}, + {152, 1.25324000e-03f, 4.54000000e+01f, 0}, + {153, 7.89300000e-01f, 6.29000000e+01f, 0}, + {154, 1.13000000e+00f, 6.93000000e+01f, 0}, + {155, 1.17497000e-03f, 5.07000000e+01f, 0}, + {156, 1.10000000e+00f, 7.33000000e+01f, 0}, + {157, 5.20000000e+00f, 2.27300000e+02f, 0}, + {158, 7.15000000e+00f, 2.61000000e+02f, 0}, + {158, 7.15000000e+00f, 2.61000000e+02f, 0}, /* id 159 (FERROUSOXIDE) previously had no row at all here -- density added per * issue #149 finding A5 (5.7 g/cm^3, the real-world density of FeO/wustite). * i_value is left at 0 (no embedded I-value data) rather than guessed: there is @@ -110,65 +193,124 @@ static const dedx_embedded_compos_row dedx_embedded_compos_rows[286] = { * data/README.md) and dedx_embedded_get_i_value() already treats a 0.0 result as * "not found" (DEDX_ERR_TARGET_NOT_FOUND) rather than a valid answer. */ {159, 5.70000000e+00f, 0.00000000e+00f, 0}, - {160, 1.02400000e+00f, 7.64000000e+01f, 0}, {161, 1.12000000e+00f, 1.43000000e+02f, 0}, - {162, 1.80000000e+00f, 2.84900000e+02f, 0}, {163, 9.50000000e-01f, 1.26600000e+02f, 0}, - {164, 1.50000000e+00f, 2.10500000e+02f, 0}, {165, 1.80000000e+00f, 2.93500000e+02f, 0}, - {166, 7.44000000e+00f, 4.93300000e+02f, 0}, {167, 5.31000000e+00f, 3.84900000e+02f, 0}, - {168, 1.29140000e+00f, 7.48000000e+01f, 0}, {170, 6.22000000e+00f, 5.26400000e+02f, 0}, - {171, 2.40000000e+00f, 1.45400000e+02f, 0}, {169, 2.23000000e+00f, 1.34000000e+02f, 0}, - {172, 1.54000000e+00f, 7.72000000e+01f, 0}, {173, 1.46000000e+00f, 7.33000000e+01f, 0}, - {174, 1.26130000e+00f, 7.26000000e+01f, 0}, {175, 1.58000000e+00f, 7.50000000e+01f, 0}, - {176, 2.32000000e+00f, 1.29700000e+02f, 0}, {177, 6.83760000e-01f, 5.44000000e+01f, 0}, - {178, 6.60300000e-01f, 5.40000000e+01f, 0}, {179, 1.42000000e+00f, 7.96000000e+01f, 0}, - {180, 6.28000000e+00f, 4.39700000e+02f, 0}, {181, 5.86000000e+00f, 4.21200000e+02f, 0}, - {182, 9.53000000e+00f, 7.66700000e+02f, 0}, {183, 1.17800000e+00f, 5.55000000e+01f, 0}, - {184, 2.11000000e+00f, 8.79000000e+01f, 0}, {185, 2.63500000e+00f, 9.40000000e+01f, 0}, - {186, 8.20000000e-01f, 3.65000000e+01f, 0}, {187, 3.49400000e+00f, 4.85100000e+02f, 0}, - {188, 2.01300000e+00f, 7.36000000e+01f, 0}, {189, 2.44000000e+00f, 9.46000000e+01f, 0}, - {190, 1.05000000e+00f, 7.53000000e+01f, 0}, {191, 1.05000000e+00f, 6.79000000e+01f, 0}, - {192, 2.95800000e+00f, 1.18000000e+02f, 0}, {193, 3.00000000e+00f, 1.34300000e+02f, 0}, - {194, 3.58000000e+00f, 1.43800000e+02f, 0}, {195, 2.53000000e+00f, 1.08300000e+02f, 0}, - {196, 6.36000000e+00f, 6.84500000e+02f, 0}, {197, 6.67151000e-04f, 4.17000000e+01f, 0}, - {198, 7.91400000e-01f, 6.76000000e+01f, 0}, {199, 9.90000000e-01f, 6.09000000e+01f, 0}, - {200, 1.00000000e+00f, 7.51000000e+01f, 0}, {201, 1.04000000e+00f, 7.53000000e+01f, 0}, - {202, 1.04000000e+00f, 7.47000000e+01f, 0}, {203, 1.11000000e+00f, 7.43000000e+01f, 0}, - {204, 1.07000000e+00f, 7.42000000e+01f, 0}, {205, 1.14500000e+00f, 6.84000000e+01f, 0}, - {206, 1.19867000e+00f, 7.58000000e+01f, 0}, {207, 1.83094000e-03f, 8.49000000e+01f, 0}, - {208, 1.08000000e+00f, 6.43000000e+01f, 0}, {209, 1.14000000e+00f, 6.39000000e+01f, 0}, - {210, 1.14000000e+00f, 6.32000000e+01f, 0}, {211, 1.42500000e+00f, 6.16000000e+01f, 0}, - {212, 7.02600000e-01f, 5.47000000e+01f, 0}, {213, 9.30000000e-01f, 5.59000000e+01f, 0}, - {214, 6.26200000e-01f, 5.36000000e+01f, 0}, {215, 3.81500000e+00f, 3.31000000e+02f, 0}, - {216, 1.03200000e+00f, 6.47000000e+01f, 0}, {217, 1.14600000e+01f, 7.46500000e+02f, 0}, - {218, 1.17000000e+00f, 6.96000000e+01f, 0}, {219, 1.20000000e+00f, 7.31000000e+01f, 0}, - {220, 1.30000000e+00f, 8.17000000e+01f, 0}, {221, 9.40000000e-01f, 5.74000000e+01f, 0}, - {222, 1.40000000e+00f, 7.87000000e+01f, 0}, {223, 1.19000000e+00f, 7.40000000e+01f, 0}, - {224, 1.42500000e+00f, 7.74000000e+01f, 0}, {225, 9.00000000e-01f, 5.65000000e+01f, 0}, - {226, 1.06000000e+00f, 6.87000000e+01f, 0}, {227, 2.20000000e+00f, 9.91000000e+01f, 0}, - {228, 2.10000000e+00f, 1.20700000e+02f, 0}, {229, 1.19000000e+00f, 7.37000000e+01f, 0}, - {230, 1.30000000e+00f, 6.97000000e+01f, 0}, {231, 1.12000000e+00f, 6.72000000e+01f, 0}, - {232, 1.30000000e+00f, 1.08200000e+02f, 0}, {233, 1.70000000e+00f, 1.34300000e+02f, 0}, - {234, 1.76000000e+00f, 8.88000000e+01f, 0}, {235, 1.25000000e+00f, 6.77000000e+01f, 0}, - {236, 3.13000000e+00f, 4.31900000e+02f, 0}, {237, 2.32000000e+00f, 1.89900000e+02f, 0}, - {238, 1.87939000e-03f, 4.71000000e+01f, 0}, {239, 4.30000000e-01f, 5.20000000e+01f, 0}, - {240, 8.03500000e-01f, 6.11000000e+01f, 0}, {241, 9.81900000e-01f, 6.62000000e+01f, 0}, - {242, 9.20000000e-01f, 5.65000000e+01f, 0}, {243, 9.20000000e-01f, 5.98000000e+01f, 0}, - {244, 1.23000000e+00f, 9.30000000e+01f, 0}, {245, 2.32000000e+00f, 1.39200000e+02f, 0}, - {246, 6.47300000e+00f, 4.86600000e+02f, 0}, {247, 5.56000000e+00f, 3.98400000e+02f, 0}, - {248, 6.47000000e+00f, 4.87100000e+02f, 0}, {249, 6.01000000e+00f, 5.43500000e+02f, 0}, - {250, 1.10000000e+00f, 7.27000000e+01f, 0}, {251, 2.53200000e+00f, 1.25000000e+02f, 0}, - {252, 3.66700000e+00f, 4.52000000e+02f, 0}, {253, 2.27000000e+00f, 1.48800000e+02f, 0}, - {254, 2.26100000e+00f, 1.14600000e+02f, 0}, {255, 9.70700000e-01f, 6.77000000e+01f, 0}, - {256, 1.58050000e+00f, 7.75000000e+01f, 0}, {257, 1.23400000e+00f, 7.17000000e+01f, 0}, - {258, 1.04000000e+00f, 7.50000000e+01f, 0}, {259, 1.62500000e+00f, 1.59200000e+02f, 0}, - {260, 7.00400000e+00f, 6.90300000e+02f, 0}, {261, 1.00000000e+00f, 7.23000000e+01f, 0}, - {262, 1.00000000e+00f, 7.49000000e+01f, 0}, {263, 1.06409000e-03f, 6.12000000e+01f, 0}, - {264, 1.82628000e-03f, 5.95000000e+01f, 0}, {265, 4.26000000e+00f, 1.79500000e+02f, 0}, - {266, 8.66900000e-01f, 6.25000000e+01f, 0}, {267, 1.46000000e+00f, 1.48100000e+02f, 0}, - {268, 1.07000000e+00f, 8.12000000e+01f, 0}, {269, 2.40000000e+00f, 3.54400000e+02f, 0}, - {270, 1.12800000e+01f, 7.52000000e+02f, 0}, {271, 1.36300000e+01f, 8.62000000e+02f, 0}, - {272, 1.09600000e+01f, 7.20600000e+02f, 0}, {273, 1.32300000e+00f, 7.28000000e+01f, 0}, - {274, 1.23000000e+00f, 6.77000000e+01f, 0}, {275, 1.80000000e+00f, 9.86000000e+01f, 0}, - {276, 1.00000000e+00f, 7.50000000e+01f, 0}, {277, 7.56182000e-04f, 7.16000000e+01f, 0}, + {160, 1.02400000e+00f, 7.64000000e+01f, 0}, + {161, 1.12000000e+00f, 1.43000000e+02f, 0}, + {162, 1.80000000e+00f, 2.84900000e+02f, 0}, + {163, 9.50000000e-01f, 1.26600000e+02f, 0}, + {164, 1.50000000e+00f, 2.10500000e+02f, 0}, + {165, 1.80000000e+00f, 2.93500000e+02f, 0}, + {166, 7.44000000e+00f, 4.93300000e+02f, 0}, + {167, 5.31000000e+00f, 3.84900000e+02f, 0}, + {168, 1.29140000e+00f, 7.48000000e+01f, 0}, + {170, 6.22000000e+00f, 5.26400000e+02f, 0}, + {171, 2.40000000e+00f, 1.45400000e+02f, 0}, + {169, 2.23000000e+00f, 1.34000000e+02f, 0}, + {172, 1.54000000e+00f, 7.72000000e+01f, 0}, + {173, 1.46000000e+00f, 7.33000000e+01f, 0}, + {174, 1.26130000e+00f, 7.26000000e+01f, 0}, + {175, 1.58000000e+00f, 7.50000000e+01f, 0}, + {176, 2.32000000e+00f, 1.29700000e+02f, 0}, + {177, 6.83760000e-01f, 5.44000000e+01f, 0}, + {178, 6.60300000e-01f, 5.40000000e+01f, 0}, + {179, 1.42000000e+00f, 7.96000000e+01f, 0}, + {180, 6.28000000e+00f, 4.39700000e+02f, 0}, + {181, 5.86000000e+00f, 4.21200000e+02f, 0}, + {182, 9.53000000e+00f, 7.66700000e+02f, 0}, + {183, 1.17800000e+00f, 5.55000000e+01f, 0}, + {184, 2.11000000e+00f, 8.79000000e+01f, 0}, + {185, 2.63500000e+00f, 9.40000000e+01f, 0}, + {186, 8.20000000e-01f, 3.65000000e+01f, 0}, + {187, 3.49400000e+00f, 4.85100000e+02f, 0}, + {188, 2.01300000e+00f, 7.36000000e+01f, 0}, + {189, 2.44000000e+00f, 9.46000000e+01f, 0}, + {190, 1.05000000e+00f, 7.53000000e+01f, 0}, + {191, 1.05000000e+00f, 6.79000000e+01f, 0}, + {192, 2.95800000e+00f, 1.18000000e+02f, 0}, + {193, 3.00000000e+00f, 1.34300000e+02f, 0}, + {194, 3.58000000e+00f, 1.43800000e+02f, 0}, + {195, 2.53000000e+00f, 1.08300000e+02f, 0}, + {196, 6.36000000e+00f, 6.84500000e+02f, 0}, + {197, 6.67151000e-04f, 4.17000000e+01f, 0}, + {198, 7.91400000e-01f, 6.76000000e+01f, 0}, + {199, 9.90000000e-01f, 6.09000000e+01f, 0}, + {200, 1.00000000e+00f, 7.51000000e+01f, 0}, + {201, 1.04000000e+00f, 7.53000000e+01f, 0}, + {202, 1.04000000e+00f, 7.47000000e+01f, 0}, + {203, 1.11000000e+00f, 7.43000000e+01f, 0}, + {204, 1.07000000e+00f, 7.42000000e+01f, 0}, + {205, 1.14500000e+00f, 6.84000000e+01f, 0}, + {206, 1.19867000e+00f, 7.58000000e+01f, 0}, + {207, 1.83094000e-03f, 8.49000000e+01f, 0}, + {208, 1.08000000e+00f, 6.43000000e+01f, 0}, + {209, 1.14000000e+00f, 6.39000000e+01f, 0}, + {210, 1.14000000e+00f, 6.32000000e+01f, 0}, + {211, 1.42500000e+00f, 6.16000000e+01f, 0}, + {212, 7.02600000e-01f, 5.47000000e+01f, 0}, + {213, 9.30000000e-01f, 5.59000000e+01f, 0}, + {214, 6.26200000e-01f, 5.36000000e+01f, 0}, + {215, 3.81500000e+00f, 3.31000000e+02f, 0}, + {216, 1.03200000e+00f, 6.47000000e+01f, 0}, + {217, 1.14600000e+01f, 7.46500000e+02f, 0}, + {218, 1.17000000e+00f, 6.96000000e+01f, 0}, + {219, 1.20000000e+00f, 7.31000000e+01f, 0}, + {220, 1.30000000e+00f, 8.17000000e+01f, 0}, + {221, 9.40000000e-01f, 5.74000000e+01f, 0}, + {222, 1.40000000e+00f, 7.87000000e+01f, 0}, + {223, 1.19000000e+00f, 7.40000000e+01f, 0}, + {224, 1.42500000e+00f, 7.74000000e+01f, 0}, + {225, 9.00000000e-01f, 5.65000000e+01f, 0}, + {226, 1.06000000e+00f, 6.87000000e+01f, 0}, + {227, 2.20000000e+00f, 9.91000000e+01f, 0}, + {228, 2.10000000e+00f, 1.20700000e+02f, 0}, + {229, 1.19000000e+00f, 7.37000000e+01f, 0}, + {230, 1.30000000e+00f, 6.97000000e+01f, 0}, + {231, 1.12000000e+00f, 6.72000000e+01f, 0}, + {232, 1.30000000e+00f, 1.08200000e+02f, 0}, + {233, 1.70000000e+00f, 1.34300000e+02f, 0}, + {234, 1.76000000e+00f, 8.88000000e+01f, 0}, + {235, 1.25000000e+00f, 6.77000000e+01f, 0}, + {236, 3.13000000e+00f, 4.31900000e+02f, 0}, + {237, 2.32000000e+00f, 1.89900000e+02f, 0}, + {238, 1.87939000e-03f, 4.71000000e+01f, 0}, + {239, 4.30000000e-01f, 5.20000000e+01f, 0}, + {240, 8.03500000e-01f, 6.11000000e+01f, 0}, + {241, 9.81900000e-01f, 6.62000000e+01f, 0}, + {242, 9.20000000e-01f, 5.65000000e+01f, 0}, + {243, 9.20000000e-01f, 5.98000000e+01f, 0}, + {244, 1.23000000e+00f, 9.30000000e+01f, 0}, + {245, 2.32000000e+00f, 1.39200000e+02f, 0}, + {246, 6.47300000e+00f, 4.86600000e+02f, 0}, + {247, 5.56000000e+00f, 3.98400000e+02f, 0}, + {248, 6.47000000e+00f, 4.87100000e+02f, 0}, + {249, 6.01000000e+00f, 5.43500000e+02f, 0}, + {250, 1.10000000e+00f, 7.27000000e+01f, 0}, + {251, 2.53200000e+00f, 1.25000000e+02f, 0}, + {252, 3.66700000e+00f, 4.52000000e+02f, 0}, + {253, 2.27000000e+00f, 1.48800000e+02f, 0}, + {254, 2.26100000e+00f, 1.14600000e+02f, 0}, + {255, 9.70700000e-01f, 6.77000000e+01f, 0}, + {256, 1.58050000e+00f, 7.75000000e+01f, 0}, + {257, 1.23400000e+00f, 7.17000000e+01f, 0}, + {258, 1.04000000e+00f, 7.50000000e+01f, 0}, + {259, 1.62500000e+00f, 1.59200000e+02f, 0}, + {260, 7.00400000e+00f, 6.90300000e+02f, 0}, + {261, 1.00000000e+00f, 7.23000000e+01f, 0}, + {262, 1.00000000e+00f, 7.49000000e+01f, 0}, + {263, 1.06409000e-03f, 6.12000000e+01f, 0}, + {264, 1.82628000e-03f, 5.95000000e+01f, 0}, + {265, 4.26000000e+00f, 1.79500000e+02f, 0}, + {266, 8.66900000e-01f, 6.25000000e+01f, 0}, + {267, 1.46000000e+00f, 1.48100000e+02f, 0}, + {268, 1.07000000e+00f, 8.12000000e+01f, 0}, + {269, 2.40000000e+00f, 3.54400000e+02f, 0}, + {270, 1.12800000e+01f, 7.52000000e+02f, 0}, + {271, 1.36300000e+01f, 8.62000000e+02f, 0}, + {272, 1.09600000e+01f, 7.20600000e+02f, 0}, + {273, 1.32300000e+00f, 7.28000000e+01f, 0}, + {274, 1.23000000e+00f, 6.77000000e+01f, 0}, + {275, 1.80000000e+00f, 9.86000000e+01f, 0}, + {276, 1.00000000e+00f, 7.50000000e+01f, 0}, + {277, 7.56182000e-04f, 7.16000000e+01f, 0}, {278, 8.70000000e-01f, 6.18000000e+01f, 0}}; /** @brief One embedded effective-charge override entry keyed by material ID. */ diff --git a/src/dedx_embedded_metadata.c b/src/dedx_embedded_metadata.c index ad34fe7..bb16fe6 100644 --- a/src/dedx_embedded_metadata.c +++ b/src/dedx_embedded_metadata.c @@ -17,10 +17,9 @@ #include "dedx_embedded_metadata.h" -#include "dedx_elements.h" - #include "data/embedded/dedx_composition.h" #include "data/embedded/dedx_metadata.h" +#include "dedx_elements.h" static int find_target_index(int target) { int i; diff --git a/tests/test_interpolation.c b/tests/test_interpolation.c index 3220e7d..6e818e3 100644 --- a/tests/test_interpolation.c +++ b/tests/test_interpolation.c @@ -341,9 +341,7 @@ static int check_effective_interpolation_mode_downgrade(void) { return 1; } if (mode != DEDX_INTERPOLATION_LINEAR) { - fprintf(stderr, - "FAIL A6: effective mode should downgrade to linear for ICRU73+Na+Ar, got %d\n", - mode); + fprintf(stderr, "FAIL A6: effective mode should downgrade to linear for ICRU73+Na+Ar, got %d\n", mode); return 1; } return requested_ok ? 0 : 1; diff --git a/tests/test_material_availability.c b/tests/test_material_availability.c index 56ee2b2..0189487 100644 --- a/tests/test_material_availability.c +++ b/tests/test_material_availability.c @@ -213,7 +213,8 @@ static int test_a150_boundary(void) { dedx_config *cfg; float pstar_stp, bethe_stp; - pstar_stp = dedx_get_simple_stp_for_program(DEDX_PSTAR, DEDX_PROTON, DEDX_A150_TISSUE_EQUIVALENT_PLASTIC, 100.0f, &err); + pstar_stp = + dedx_get_simple_stp_for_program(DEDX_PSTAR, DEDX_PROTON, DEDX_A150_TISSUE_EQUIVALENT_PLASTIC, 100.0f, &err); failures += check_err(err, DEDX_OK, "PSTAR+proton+A150 stp"); ws = dedx_allocate_workspace(1, &err); @@ -224,7 +225,8 @@ static int test_a150_boundary(void) { err = 0; dedx_load_config(ws, cfg, &err); failures += check_err(err, DEDX_OK, "BETHE_EXT00+proton+A150 load"); - failures += check_err(cfg->bragg_used, 1, "BETHE_EXT00+proton+A150 should Bragg-decompose, not treat id 99 as Z=99"); + failures += + check_err(cfg->bragg_used, 1, "BETHE_EXT00+proton+A150 should Bragg-decompose, not treat id 99 as Z=99"); if (err == DEDX_OK) { bethe_stp = dedx_get_stp(ws, cfg, 100.0f, &err); failures += check_err(err, DEDX_OK, "BETHE_EXT00+proton+A150 stp"); From a54807698f685e91120c20b14a930dc9aec7e99d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 18:22:50 +0000 Subject: [PATCH 3/6] Address Copilot review: set ion/target on stopping_data before load_data() 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 Claude-Session: https://claude.ai/code/session_0188VRnSFzb7HvNKcMX7XT1Z --- src/dedx.c | 10 +++++++ tests/test_bethe_ext00.c | 47 ++++++++++++++++++++++++++++++ tests/test_material_availability.c | 17 +++++++++++ 3 files changed, 74 insertions(+) diff --git a/src/dedx.c b/src/dedx.c index 7b9afa2..3cfcb73 100644 --- a/src/dedx.c +++ b/src/dedx.c @@ -863,6 +863,14 @@ static int load_compound(dedx_workspace *ws, dedx_config *config, int *err) { config->i_value = i_value; config->target = target; + /* Identify the aggregated result as the compound itself (its own id, or 0 for a + * caller-defined custom compound), not whatever find_data() last wrote into + * compound_data[length - 1] for the final per-constituent element -- data is a + * fresh stack local (see the declaration above) with no initializer, so these + * fields would otherwise carry indeterminate values into ws->loaded_data[]. */ + data.ion = config->ion; + data.target = target; + /* All constituents are now verified to share one grid, so any compound_data[i].length * equals compound_data[0].length -- min() here is belt-and-braces, matching the * issue's suggested fix, in case a future change relaxes the check above from @@ -930,6 +938,8 @@ static int load_bethe_2(stopping_data *data, dedx_config *config, float *energy, TA = dedx_internal_get_atom_mass(config->target, err); rho = config->rho; pot = config->_temp_i_value; + data->target = config->target; + data->ion = config->ion; data->length = 122; dedx_internal_read_energy_data(energy, DEDX_BETHE_EXT00, err); if (*err != 0) diff --git a/tests/test_bethe_ext00.c b/tests/test_bethe_ext00.c index 10d5f06..1b94653 100644 --- a/tests/test_bethe_ext00.c +++ b/tests/test_bethe_ext00.c @@ -1,5 +1,50 @@ +#include "dedx_lookup_data.h" #include "test_helpers.h" +/* + * Regression test raised in review of issue #149's Phase 2 PR: load_bethe_2()'s + * memset(data, 0, sizeof(*data)) (added for A1, to stop the tail of the 150-float + * data->data[] array from carrying uninitialized heap/stack past data->length) also + * zeroes data->target and data->ion -- and neither was ever repopulated afterward, so + * load_data() persisted 0/0 into ws->loaded_data[]->ion/target for every Bethe-formula + * dataset instead of the actual ion/target that was loaded. Nothing in the public API + * currently reads that stored metadata back out, so this had no observable effect on + * computed stopping powers -- but it is exactly the kind of silently-wrong internal + * state this audit is about, so it is now set explicitly, matching what + * read_embedded_stopping_data() already does for the tabulated-report path. */ +static int test_bethe_records_ion_and_target(void) { + int failures = 0; + int err = 0; + dedx_workspace *ws = dedx_allocate_workspace(1, &err); + dedx_config *cfg = calloc(1, sizeof(dedx_config)); + + cfg->program = DEDX_BETHE_EXT00; + cfg->ion = DEDX_PROTON; + cfg->target = DEDX_CARBON; + cfg->rho = 2.0f; + err = 0; + dedx_load_config(ws, cfg, &err); + if (err != DEDX_OK) { + fprintf(stderr, "FAIL bethe ion/target load: err=%d\n", err); + failures++; + } else { + dedx_internal_lookup_data *loaded = ws->loaded_data[cfg->cfg_id]; + if (loaded->ion != DEDX_PROTON || loaded->target != DEDX_CARBON) { + fprintf(stderr, + "FAIL bethe ion/target: got ion=%d target=%d, expected ion=%d target=%d\n", + loaded->ion, + loaded->target, + DEDX_PROTON, + DEDX_CARBON); + failures++; + } + } + dedx_free_config(cfg, &err); + dedx_free_workspace(ws, &err); + + return failures; +} + static dedx_config *make_bethe_default_config(int ion, int target) { dedx_config *cfg = calloc(1, sizeof(dedx_config)); cfg->program = DEDX_BETHE_EXT00; @@ -444,5 +489,7 @@ int main(void) { failures += check_config_stp( make_bethe_null_i_config(DEDX_CARBON, DEDX_ALANINE), energy_grid[4], 7.733e+01f, "bethe-null-i"); + failures += test_bethe_records_ion_and_target(); + return failures; } diff --git a/tests/test_material_availability.c b/tests/test_material_availability.c index 0189487..ddf687e 100644 --- a/tests/test_material_availability.c +++ b/tests/test_material_availability.c @@ -1,5 +1,6 @@ #include +#include "dedx_lookup_data.h" #include "test_helpers.h" /* Regression coverage for issue #51: some ion+material+program combinations were @@ -228,6 +229,22 @@ static int test_a150_boundary(void) { failures += check_err(cfg->bragg_used, 1, "BETHE_EXT00+proton+A150 should Bragg-decompose, not treat id 99 as Z=99"); if (err == DEDX_OK) { + /* Regression check raised in review: load_compound()'s aggregated result was a + * fresh stack local with no initializer, so ws->loaded_data[]->ion/target used + * to carry indeterminate stack values for every Bragg-decomposed load instead + * of the compound's own ion/target -- must now match what was requested, not + * e.g. the last constituent element's own id. */ + dedx_internal_lookup_data *loaded = ws->loaded_data[cfg->cfg_id]; + if (loaded->ion != DEDX_PROTON || loaded->target != DEDX_A150_TISSUE_EQUIVALENT_PLASTIC) { + fprintf(stderr, + "FAIL BETHE_EXT00+proton+A150 loaded ion/target: got ion=%d target=%d, expected ion=%d target=%d\n", + loaded->ion, + loaded->target, + DEDX_PROTON, + DEDX_A150_TISSUE_EQUIVALENT_PLASTIC); + failures++; + } + bethe_stp = dedx_get_stp(ws, cfg, 100.0f, &err); failures += check_err(err, DEDX_OK, "BETHE_EXT00+proton+A150 stp"); if (err == DEDX_OK) { From b6bfe7687ff9fb34c14d16c4d15aeb1a92e2f083 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 06:32:22 +0000 Subject: [PATCH 4/6] Address critical review: B1-B5, S1-S5, and nits 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 Claude-Session: https://claude.ai/code/session_0188VRnSFzb7HvNKcMX7XT1Z --- data/README.md | 21 ++++ include/dedx.h | 18 ++- include/dedx_error.h | 14 ++- src/data/embedded/dedx_metadata.h | 8 ++ src/dedx.c | 94 +++++++++++--- src/dedx_mpaul.c | 23 +++- src/dedx_spline.c | 9 ++ src/dedx_validate.c | 24 ++++ tests/test_availability_exhaustive.c | 176 +++++++++++++++++++-------- tests/test_error_codes.c | 6 + tests/test_interpolation.c | 4 +- tests/test_material_availability.c | 40 ++++-- tests/test_mstar.c | 52 ++++++++ 13 files changed, 401 insertions(+), 88 deletions(-) diff --git a/data/README.md b/data/README.md index aab4c62..c6859ee 100644 --- a/data/README.md +++ b/data/README.md @@ -73,3 +73,24 @@ Manual cross-check: - ICRU49 matches the `ICRU_ASTAR.dat` value (`1.016e+03`) So the small difference is real and source-driven. + +## Hand-Added Rows in `src/data/embedded/dedx_metadata.h` + +`dedx_embedded_compos_rows[]` is not generated by any script in `tools/` (unlike +the stopping-power tables) -- it is hand-maintained, and rows added directly to +it need their own provenance note here rather than only a code comment, per +issue #149 finding S3. + +- **FERROUSOXIDE (material id 159), density row `5.7 g/cm^3`**: this is the + standard literature density for ferrous oxide / wüstite (FeO), consistent + with the CRC Handbook of Chemistry and Physics. Added to close issue #149 + finding A5 (the row's absence made `dedx_internal_validate_rho()` fail this + material for every program that reads density, tabulated or not). Its + mean excitation potential (I-value) is deliberately left at `0.0` in that + same row: no raw source in this repository or in `tools/` covers I-values + for compounds, so there is nothing to regenerate or verify a fabricated + number against, and `dedx_embedded_get_i_value()` treats `0.0` as "not + found" rather than a real answer -- see the comment at that row and + `tests/test_material_availability.c`'s `test_ferrous_oxide_tabulated_program()` + for the tests that depend on this being `0.0` specifically. Only add a real + I-value here once one can be cited the same way this density is. diff --git a/include/dedx.h b/include/dedx.h index c322534..c92c94a 100644 --- a/include/dedx.h +++ b/include/dedx.h @@ -60,7 +60,19 @@ enum { Deliberately grouped with the other tabulated report identifiers (not >= DEDX_DEFAULT) so that a compound target resolves through the same tabulated-first path as DEDX_ICRU instead of always decomposing - into elements. */ + into elements. + + This "best effort" promise has one hole: a compound whose + constituents resolve to *different* tiers -- one tabulated, one + Bethe-fallback -- can end up on mismatched energy grids, since the + two tiers are not sampled at the same points. Rather than silently + mixing or truncating those grids, dedx_load_config() rejects the + mix with DEDX_ERR_INCONSISTENT_ENERGY_GRID (see issue #149 finding + A1). This is a real, if narrow, exception to "falls back rather than + failing outright" above -- it affects compounds like boron carbide, + where boron is Bethe-only but carbon is tabulated. Callers that need + every advertised DEDX_AUTO compound to load must be prepared to + handle this error, not just DEDX_ERR_COMBINATION_NOT_FOUND. */ DEDX_DEFAULT = 100, /**< Default program (Bethe formula) */ DEDX_BETHE_EXT00 /**< Bethe formula with extensions */ }; @@ -382,7 +394,9 @@ float dedx_get_stp(dedx_workspace *ws, dedx_config *config, float energy, int *e * @param[in] ws Workspace with a loaded configuration. * @param[in] config Loaded configuration (cfg_id must be valid). * @param[out] err Error code; 0 on success. - * @return DEDX_INTERPOLATION_LOG_LOG or DEDX_INTERPOLATION_LINEAR. + * @return DEDX_INTERPOLATION_LOG_LOG or DEDX_INTERPOLATION_LINEAR on success, or -1 + * if @p config has no valid loaded dataset (check @p err, not the return + * value, to distinguish failure from DEDX_INTERPOLATION_LOG_LOG == 0). */ int dedx_get_effective_interpolation_mode(dedx_workspace *ws, dedx_config *config, int *err); diff --git a/include/dedx_error.h b/include/dedx_error.h index 78ed1d6..99953cf 100644 --- a/include/dedx_error.h +++ b/include/dedx_error.h @@ -38,12 +38,13 @@ extern "C" { /** @defgroup err_data Data and configuration errors (201–210) * @{ */ -#define DEDX_ERR_TARGET_NOT_FOUND 201 /**< target not found in embedded metadata */ -#define DEDX_ERR_COMBINATION_NOT_FOUND 202 /**< ion/target combination not in embedded data */ -#define DEDX_ERR_INVALID_DATASET_ID 203 /**< dataset ID does not exist in workspace */ -#define DEDX_ERR_NOT_AN_ELEMENT 204 /**< ID does not correspond to an atomic element */ -#define DEDX_ERR_ESTAR_NOT_IMPL 205 /**< ESTAR program is not implemented */ -#define DEDX_ERR_ION_NOT_SUPPORTED_MSTAR 206 /**< ion not supported for MSTAR (reserved legacy code) */ +#define DEDX_ERR_TARGET_NOT_FOUND 201 /**< target not found in embedded metadata */ +#define DEDX_ERR_COMBINATION_NOT_FOUND 202 /**< ion/target combination not in embedded data */ +#define DEDX_ERR_INVALID_DATASET_ID 203 /**< dataset ID does not exist in workspace */ +#define DEDX_ERR_NOT_AN_ELEMENT 204 /**< ID does not correspond to an atomic element */ +#define DEDX_ERR_ESTAR_NOT_IMPL 205 /**< ESTAR program is not implemented */ +#define DEDX_ERR_ION_NOT_SUPPORTED_MSTAR \ + 206 /**< MSTAR mode/ion/effective-charge combination has no implemented coefficient */ #define DEDX_ERR_ION_NOT_SUPPORTED 207 /**< ion not supported by requested program */ #define DEDX_ERR_RHO_REQUIRED 208 /**< target density rho must be provided */ #define DEDX_ERR_ION_A_REQUIRED 209 /**< nucleon number ion_a must be provided */ @@ -51,6 +52,7 @@ extern "C" { #define DEDX_ERR_INCONSISTENT_COMPOUND 211 /**< inconsistent compound specification */ #define DEDX_ERR_INVALID_INTERPOLATION_MODE 212 /**< interpolation mode is not supported */ #define DEDX_ERR_INVALID_MSTAR_MODE 213 /**< mstar_mode is not one of the DEDX_MSTAR_MODE_* values */ +#define DEDX_ERR_INCONSISTENT_ENERGY_GRID 214 /**< compound constituents resolved onto mismatched energy grids */ /** @} */ /** @defgroup err_memory Memory errors (301) diff --git a/src/data/embedded/dedx_metadata.h b/src/data/embedded/dedx_metadata.h index c26c2c3..5fa1baa 100644 --- a/src/data/embedded/dedx_metadata.h +++ b/src/data/embedded/dedx_metadata.h @@ -19,6 +19,13 @@ typedef struct { } dedx_embedded_compos_row; /** @brief Embedded density and I-value metadata rows keyed by material ID. */ +/* clang-format off -- this hand-maintained table (not generated by tools/dat2c.py -- + * see data/README.md) is intentionally one entry per line. Without this, + * clang-format's bin-packing of the array literal depends on the row count, so + * adding a single row can silently reflow every other line in the table (as it did + * for the one-row FERROUSOXIDE addition in issue #149's Phase 2 PR -- 426 diff lines + * to review a single added row, flagged in review). Keep new rows one per line to + * match. */ static const dedx_embedded_compos_row dedx_embedded_compos_rows[286] = { {1, 8.37480000e-05f, 1.92000000e+01f, 1}, {1, 8.37480000e-05f, 2.18000000e+01f, 2}, @@ -312,6 +319,7 @@ static const dedx_embedded_compos_row dedx_embedded_compos_rows[286] = { {276, 1.00000000e+00f, 7.50000000e+01f, 0}, {277, 7.56182000e-04f, 7.16000000e+01f, 0}, {278, 8.70000000e-01f, 6.18000000e+01f, 0}}; +/* clang-format on */ /** @brief One embedded effective-charge override entry keyed by material ID. */ typedef struct { diff --git a/src/dedx.c b/src/dedx.c index 3cfcb73..4509b70 100644 --- a/src/dedx.c +++ b/src/dedx.c @@ -144,6 +144,7 @@ static const dedx_error_entry dedx_error_table[] = { {DEDX_ERR_INCONSISTENT_COMPOUND, "Compound specification is inconsistent."}, {DEDX_ERR_INVALID_INTERPOLATION_MODE, "Interpolation mode is not supported."}, {DEDX_ERR_INVALID_MSTAR_MODE, "mstar_mode is not a recognized DEDX_MSTAR_MODE_* value."}, + {DEDX_ERR_INCONSISTENT_ENERGY_GRID, "Compound constituents resolved onto mismatched energy grids."}, {DEDX_ERR_NO_MEMORY, "Out of memory"}, }; @@ -266,6 +267,17 @@ static int element_supported_for_ion(int program, int ion, int element) { return dedx_embedded_has_table(program, ion_load, element); } +/* For DEDX_AUTO specifically: which tier a single element resolves to, mirroring + * find_data()'s own dispatch exactly (prog_load = DEDX_ICRU, same ion, same element). + * Returns 1 for the tabulated-report tier, 0 for the Bethe-Bloch fallback tier. + * Only meaningful for an element id already known reachable via + * element_supported_for_ion() -- callers are responsible for that precondition, same + * as element_supported_for_ion() itself documents for its own `element` parameter. */ +static int element_is_tabulated_for_auto(int ion, int element) { + int resolved; + return dedx_embedded_resolve_program(DEDX_ICRU, ion, element, &resolved) == 0; +} + /* Checks whether a material (element or compound) is reachable for program/ion. * Compounds are available iff their composition is known and every constituent * element is itself reachable, matching the Bragg-additivity decomposition @@ -279,6 +291,17 @@ static int material_id_supported(int program, int ion, int material) { unsigned int comp_len; unsigned int i; int comp_err = DEDX_OK; + /* DEDX_AUTO-only bookkeeping for the grid-tier check below (issue #149 finding + * A1/B4): a compound whose constituents resolve to different tiers -- one + * tabulated, one Bethe-fallback -- will be rejected by load_compound() with + * DEDX_ERR_INCONSISTENT_ENERGY_GRID, since the two tiers are not sampled on the + * same energy grid. Before this check, material_id_supported() only knew whether + * DEDX_AUTO could reach each constituent *at all* (element_supported_for_ion() + * always returns 1 for it, any known element), not which tier -- so it kept + * advertising exactly the compounds A1 correctly started rejecting, which is the + * false-advertisement regression the review of this PR flagged. */ + int auto_tier_seen = 0; + int auto_tier_tabulated = 0; if (material <= 0) { /* LCOV_EXCL_START -- master material list never contains this, and composition rows are always positive element IDs in practice */ @@ -294,13 +317,25 @@ static int material_id_supported(int program, int ion, int material) { * did before issue #149's finding A5 was fixed, can't be loaded even though every * constituent element below is individually Bethe-supported. Tabulated programs * never read rho at all (element_supported_for_ion() already documents the same - * scoping for elements), so this check only applies to program >= DEDX_DEFAULT. */ + * scoping for elements), so this check only applies to program >= DEDX_DEFAULT. + * + * The same programs also read the compound's own I-value directly (see + * load_compound()'s elements_i_value == NULL branch via dedx_internal_evaluate_i_pot()), + * rather than Bragg-averaging it from constituents -- so a compound with a density + * row but no I-value row (FERROUSOXIDE, deliberately: see finding A5's PR + * description) is still unreachable for these programs specifically. */ if (program >= DEDX_DEFAULT) { int density_err = DEDX_OK; + int i_value_err = DEDX_OK; + dedx_internal_read_density(material, &density_err); if (density_err != DEDX_OK) { return 0; } + dedx_internal_get_i_value(material, DEDX_DEFAULT_STATE, &i_value_err); + if (i_value_err != DEDX_OK) { + return 0; + } } dedx_internal_get_composition(material, composition, &comp_len, &comp_err); @@ -309,9 +344,20 @@ static int material_id_supported(int program, int ion, int material) { return 0; } /* LCOV_EXCL_STOP */ for (i = 0; i < comp_len; i++) { - if (!material_id_supported(program, ion, (int) composition[i][0])) { + int constituent = (int) composition[i][0]; + + if (!material_id_supported(program, ion, constituent)) { return 0; } + if (program == DEDX_AUTO && constituent > 0 && constituent <= DEDX_MAX_ELEMENT_ID) { + int tabulated = element_is_tabulated_for_auto(ion, constituent); + if (!auto_tier_seen) { + auto_tier_tabulated = tabulated; + auto_tier_seen = 1; + } else if (tabulated != auto_tier_tabulated) { + return 0; + } + } } return 1; } @@ -498,8 +544,13 @@ int dedx_get_effective_interpolation_mode(dedx_workspace *ws, dedx_config *confi *err = DEDX_OK; if (id < 0 || id >= ws->active_datasets) { + /* -1, not 0: DEDX_INTERPOLATION_LOG_LOG == 0, so a caller that forgets to + * check *err would otherwise read a failed call as "log-log", which is + * exactly the wrong answer for a function whose entire purpose is exposing + * a downgrade away from log-log (see issue #149 finding A6) -- flagged in + * review as a real footgun for this specific accessor. */ *err = DEDX_ERR_INVALID_DATASET_ID; - return 0; + return -1; } return ws->loaded_data[id]->interpolation_mode; @@ -774,7 +825,9 @@ static int load_compound(dedx_workspace *ws, dedx_config *config, int *err) { float energy[DEDX_MAX_ELEMENTS]; /* reference energy grid, from constituent 0 */ float energy_i[DEDX_MAX_ELEMENTS]; /* scratch grid for constituent i > 0, compared below */ float i_value; + float temp_i_value; int target; + int rc = -1; stopping_data data; /* calloc, not malloc: zero-initializes every constituent's data[DEDX_MAX_ELEMENTS] * array up front, so a constituent shorter than data.length (see min-length below) @@ -789,6 +842,7 @@ static int load_compound(dedx_workspace *ws, dedx_config *config, int *err) { } weight = config->elements_mass_fraction; i_value = config->i_value; + temp_i_value = config->_temp_i_value; target = config->target; /* Resolve the compound's own physical state (gas vs condensed) before looking up @@ -802,8 +856,7 @@ static int load_compound(dedx_workspace *ws, dedx_config *config, int *err) { if (config->compound_state == DEDX_DEFAULT_STATE) { dedx_internal_validate_state(config, err); if (*err != 0) { - free(compound_data); - return -1; + goto cleanup; } } @@ -817,14 +870,12 @@ static int load_compound(dedx_workspace *ws, dedx_config *config, int *err) { // If it is exactly 0.0, we fallback to the default I-value for the element. if (config->elements_i_value[i] < 0.0) { *err = DEDX_ERR_INVALID_I_VALUE; - free(compound_data); - return -1; + goto cleanup; } if (config->elements_i_value[i] == 0.0) { config->_temp_i_value = dedx_internal_get_i_value(targets[i], config->compound_state, err); if (*err != 0) { - free(compound_data); - return -1; + goto cleanup; } } } else { @@ -834,14 +885,12 @@ static int load_compound(dedx_workspace *ws, dedx_config *config, int *err) { // state the public dedx_get_i_value() accessor hardcodes. config->_temp_i_value = dedx_internal_get_i_value(targets[i], config->compound_state, err); if (*err != 0) { - free(compound_data); - return -1; + goto cleanup; } } find_data(&compound_data[i], config, (i == 0) ? energy : energy_i, err); if (*err != 0) { - free(compound_data); - return -1; + goto cleanup; } /* Constituents can resolve onto different energy grids: DEDX_AUTO's per-element * fallback tries a tabulated report (e.g. the 133-point PSTAR-family grid) first @@ -854,13 +903,13 @@ static int load_compound(dedx_workspace *ws, dedx_config *config, int *err) { if (i > 0 && (compound_data[i].length != compound_data[0].length || memcmp(energy_i, energy, sizeof(float) * compound_data[0].length) != 0)) { - *err = DEDX_ERR_INCONSISTENT_COMPOUND; - free(compound_data); - return -1; + *err = DEDX_ERR_INCONSISTENT_ENERGY_GRID; + goto cleanup; } } config->i_value = i_value; + config->_temp_i_value = temp_i_value; config->target = target; /* Identify the aggregated result as the compound itself (its own id, or 0 for a @@ -889,6 +938,19 @@ static int load_compound(dedx_workspace *ws, dedx_config *config, int *err) { } free(compound_data); return load_data(ws, &data, energy, config, err); + +cleanup: + /* Every early failure above runs config->target through the current constituent + * element (targets[i]), not the compound's own id, and may have overwritten + * config->i_value/_temp_i_value too -- restore all three so a caller who reuses + * this config after a failed load (e.g. "try DEDX_AUTO, fall back to DEDX_PSTAR + * on error") sees the compound they asked for, not whichever element load_compound() + * happened to be resolving when it gave up. See issue #149 finding B5. */ + config->target = target; + config->i_value = i_value; + config->_temp_i_value = temp_i_value; + free(compound_data); + return rc; } static int load_bethe_2(stopping_data *data, dedx_config *config, float *energy, int *err) { diff --git a/src/dedx_mpaul.c b/src/dedx_mpaul.c index 2a1c1ca..c4db084 100644 --- a/src/dedx_mpaul.c +++ b/src/dedx_mpaul.c @@ -132,8 +132,18 @@ float dedx_internal_calculate_mspaul_coef(char mode, int ion, int target, float b = 0.38887; c = 2.84076; } else { - - // illegal mode + /* mode 'h' is only enumerated above for ion in {3..11, 16, 17, 18} -- a + * documented, resolvable mode (DEDX_MSTAR_MODE_B/H both resolve to it for a + * gaseous target) can still land here for ion 12-15, leaving a/b/c at their + * declared defaults (5.0, -1, -1) and silently computing a nonsense + * coefficient below with *err left at DEDX_OK. Issue #149's finding A7 fixed + * config->mstar_mode validation one layer up, but that only rejects letters + * outside DEDX_MSTAR_MODE_*; it does not -- and structurally cannot -- catch + * this, since 'h' is itself one of those six valid letters. Report it here, + * at the actual point where mode+ion has no implemented coefficient, instead + * of one layer up where the combination still looks fine. */ + *err = DEDX_ERR_ION_NOT_SUPPORTED_MSTAR; + return 0.0f; } if (mode == 'c' || mode == 'd') { @@ -178,7 +188,14 @@ float dedx_internal_calculate_mspaul_coef(char mode, int ion, int target, float } else if (mode == 'd') { - // illegal mode + /* Reached only for z2 <= 4 or z2 >= 93 (z2 <= 3 is remapped to mode 'c' + * above, before this chain) -- outside the effective-charge ranges the + * FACTOR correction above is parametrized for. FOUT itself is still a + * valid mode-'d' value (a/b/c come from the ion-based polynomials above, + * not from z2), but silently leaving FACTOR at its neutral 1.0 here would + * skip a correction the literature model expects for this z2 instead of + * flagging that it isn't implemented for it. */ + *err = DEDX_ERR_ION_NOT_SUPPORTED_MSTAR; } FOUT = FOUT * FACTOR; diff --git a/src/dedx_spline.c b/src/dedx_spline.c index c1d4f25..95dd9cc 100644 --- a/src/dedx_spline.c +++ b/src/dedx_spline.c @@ -171,6 +171,15 @@ float dedx_internal_evaluate_spline( const float dx = x - coef[i].x; const float dx2 = dx * dx; + /* isnan(coef[i].log_x) is this function's own, independent detection of + * dedx_internal_calculate_coefficients()'s log-log-to-linear downgrade (see + * its NAN-marking in calculate_linear_coefficients()) -- it's what already + * kept computed values correct even before that downgrade was made visible to + * callers. dedx_get_effective_interpolation_mode() (issue #149 finding A6) + * exposes the same fact through the public API; the two are deliberately + * redundant, not alternatives -- this one guards a single evaluation from + * NaN/garbage no matter what a caller passed as interpolation_mode, while that + * one lets a caller find out about the downgrade without evaluating anything. */ if (interpolation_mode == DEDX_INTERPOLATION_LINEAR || isnan(coef[i].log_x)) { return coef[i].a + coef[i].b * dx + coef[i].c * dx2 + coef[i].d * dx2 * dx; } diff --git a/src/dedx_validate.c b/src/dedx_validate.c index 2b064e7..e4078e6 100644 --- a/src/dedx_validate.c +++ b/src/dedx_validate.c @@ -57,6 +57,8 @@ static int dedx_internal_validate_interpolation_mode(dedx_config *config, int *e int dedx_internal_check_ion(int prog, int ion) { const int *ion_list; + const int *programs; + int prog_known = 0; int i = 0; if (prog >= DEDX_DEFAULT || prog == DEDX_AUTO) { @@ -69,7 +71,29 @@ int dedx_internal_check_ion(int prog, int ion) { return 1; } + /* prog must be a recognized program id before it's safe to hand to + * dedx_get_ion_list(): that indexes dedx_program_available_ions[] directly by + * prog, and rows the table doesn't explicitly initialize are zero-filled, not + * -1-terminated, so an unrecognized id (a typo, a stale constant, prog < 0) makes + * the ion_list loop below walk off the end of the table looking for a terminator + * that row will never have -- a real, ASan-reproducible global-buffer-overflow. + * This was already latent on the elemental-target path before this PR (see issue + * #149 finding B3, deferred to Phase 3 for the full accessor-hardening sweep), but + * moving the check_ion() call here made it newly reachable from the + * custom-compound path too (finding A8) -- that path used to bypass this function + * entirely. Guard it here rather than let A8 grow the crash's reach. */ + programs = dedx_get_program_list(); + for (i = 0; programs[i] != -1; i++) { + if (programs[i] == prog) { + prog_known = 1; + break; + } + } + if (!prog_known) + return 0; + ion_list = dedx_get_ion_list(prog); + i = 0; while (ion_list[i] != -1) { if (ion_list[i] == ion) return 1; diff --git a/tests/test_availability_exhaustive.c b/tests/test_availability_exhaustive.c index 4f3791c..091a1d0 100644 --- a/tests/test_availability_exhaustive.c +++ b/tests/test_availability_exhaustive.c @@ -1,4 +1,5 @@ #include +#include #include "test_helpers.h" @@ -25,67 +26,90 @@ * mismatches/dead configs traced back to two root causes: A1's DEDX_AUTO tier-mixing * (mismatched per-constituent energy grids) and A5's missing FERROUSOXIDE density row. * - * Phase 2 fixed A1, A2, A3, A5, A6, A7 and A8. Their effect on this sweep, verified by - * diffing this same sweep's per-(program,ion) output against the pre-Phase-2 build: + * Phase 2 fixed A1, A2, A3, A5, A6, A7 and A8, and (in review) B2-B4 below. Load + * failures are asserted *per error code*, not as one aggregate ceiling: an aggregate + * budget can absorb a brand-new failure mode as long as some other one improved by at + * least as much in the same run, which is exactly the kind of silent trade a ratchet + * is supposed to catch. TOTAL_COMBINATIONS is still asserted separately, with + * equality, so a change in what dedx_get_material_list_for_ion() advertises is always + * visible here too. * - * - TOTAL_COMBINATIONS: 101957 -> 101886 (-71). A2's off-by-one fix - * (material_id_supported()'s element/compound boundary, id 99 = A150 tissue- - * equivalent plastic) stops routing compound id 99 through the elemental - * embedded-table lookup for ASTAR/PSTAR/MSTAR/ICRU73_OLD/ICRU73/ICRU49/ICRU (the - * 71 = 1+1+17+16+16+2+18 combinations across their ion lists -- one fewer per - * (program,ion) than before). It's now correctly evaluated via composition-based - * reachability, which drops it for the ion/program pairs where a constituent - * genuinely isn't reachable -- exactly the misclassification A2 set out to fix. - * - BASELINE_LOAD_FAILURES: 407 -> 1470. Two components, both traced by error code: - * * 224 (DEDX_ERR_TARGET_NOT_FOUND, programs DEDX_DEFAULT/DEDX_BETHE_EXT00 only): - * the FERROUSOXIDE (id 159) gap, down from 407. A5 added its density row and - * relaxed dedx_internal_validate_rho() to only require rho where it's actually - * used, which fixed the 183 tabulated-program combinations. The 224 that - * remain are DEDX_DEFAULT/DEDX_BETHE_EXT00, which read a compound target's - * *own* I-value directly from embedded metadata rather than Bragg-averaging - * it from constituents; no authoritative FERROUSOXIDE I-value exists (see - * data/README.md), and A5 deliberately left it unfabricated rather than - * invent a number -- so these 224 stay a documented, open gap. - * * 1246 (DEDX_ERR_INCONSISTENT_COMPOUND, DEDX_AUTO only, new): compounds whose - * constituents resolve onto mismatched energy grids (A1's tier-mixing bug). - * Before A1, load_compound() silently summed across the mismatched grids and - * dedx_load_config() reported success -- these combinations were counted - * instead under bound_mismatches/dead_configs below. A1 now rejects them - * instead of serving a wrong or unusable number, which is why they show up - * here as load failures rather than a regression. - * - BASELINE_BOUND_MISMATCHES: 1568 -> 480, BASELINE_DEAD_CONFIGS: 174 -> 0. Both - * drop because A1 catches the same underlying grid-mismatch combinations earlier, - * as a clean load failure, instead of letting them load and then fail (or return - * nonsense) at the energy-sampling stage. + * History, verified at each step by diffing this sweep's per-(program,ion) output + * against the previous build and tracing every failure by error code -- not assumed: * - * The BASELINE_* constants below still pin these counts so this test is a *ratchet*, - * not a silent no-op: - * - * - It stays green by asserting "no worse than the known baseline", rather than - * asserting "zero failures" -- A4 (advertised-bounds accuracy) and the - * FERROUSOXIDE I-value gap remain deliberately open past Phase 2. - * - Any future fix must lower the relevant BASELINE_* constant(s) as its root cause - * is addressed. Do NOT raise a baseline to make a newly introduced regression - * pass -- if this test starts failing because a count went up unexpectedly, that - * is a real regression, not a stale baseline. (The load_failures increase in - * Phase 2 above was verified, not assumed, before raising this constant.) - * - TOTAL_COMBINATIONS is asserted with equality (not a ceiling) so a change in - * what dedx_get_material_list_for_ion() advertises -- for better or worse -- is - * always visible here, prompting a deliberate update rather than a silent drift. + * - A2's off-by-one fix (material_id_supported()'s element/compound boundary, id 99 + * = A150 tissue-equivalent plastic) stops routing compound id 99 through the + * elemental embedded-table lookup for ASTAR/PSTAR/MSTAR/ICRU73_OLD/ICRU73/ICRU49/ + * ICRU. TOTAL_COMBINATIONS: 101957 -> 101886 (-71 = 1+1+17+16+16+2+18, one fewer + * per (program,ion) across those programs' ion lists). + * - A1 rejects DEDX_AUTO compounds whose constituents resolve onto mismatched + * energy grids (DEDX_ERR_INCONSISTENT_ENERGY_GRID) instead of silently mixing or + * truncating them -- previously counted under bound_mismatches/dead_configs + * instead, or not counted as broken at all. A5 fixes 183 of the original 407 + * FERROUSOXIDE (id 159) failures (adds its density row, only requires rho where a + * program actually reads it); the remaining 224 (DEDX_ERR_TARGET_NOT_FOUND, for + * DEDX_DEFAULT/DEDX_BETHE_EXT00 only, which read a compound's *own* I-value + * directly rather than Bragg-averaging it, and no authoritative FERROUSOXIDE + * I-value exists to fill that row with -- see data/README.md) were, at that point, + * a deliberate, documented, *open* gap on the load_failures side. At that + * intermediate point load_failures read 407 -> 1470 (224 remaining + 1246 new + * DEDX_ERR_INCONSISTENT_ENERGY_GRID) and bound_mismatches/dead_configs dropped + * 1568 -> 480 and 174 -> 0. + * - In review of this PR, material_id_supported() was taught both of the + * constraints above instead of only reproducing dedx_load_config()'s answer after + * the fact (finding B4): it now predicts a DEDX_AUTO grid-tier mismatch the same + * way find_data() resolves it (dedx_embedded_resolve_program() per constituent), + * and it now checks I-value availability for program >= DEDX_DEFAULT the same way + * it already checked density, closing the FERROUSOXIDE gap above to zero *on the + * advertising side* without fabricating the I-value itself -- the material simply + * stops being advertised for the two programs that need a number nobody can + * verify. This is what actually brought load_failures down, not a raised ceiling: + * TOTAL_COMBINATIONS 101886 -> 100222 (-1664, the compounds/programs that would + * have failed and are no longer advertised), load_failures 1470 -> 108, + * bound_mismatches 480 -> 470. + * - The 108 that remain are DEDX_MSTAR only, all DEDX_ERR_ION_NOT_SUPPORTED_MSTAR: + * ions 12-15 combined with a gas target hit an "illegal mode" branch in + * dedx_mpaul.c that predates this whole PR (reproduced identically against + * `main`, e.g. DEDX_MSTAR + ion 12 + DEDX_ARGON) -- previously silent (a + * nonsense stopping power with err == DEDX_OK, also found in review, finding B2), + * now a clean, correctly-reported error. material_id_supported() does not yet + * model this for MSTAR specifically -- doing so needs either an mstar_mode + * parameter on the availability query (an API change) or a hardcoded assumption + * about the default mode, neither of which this PR takes on -- so it stays a + * documented, open gap here, the same shape as the FERROUSOXIDE gap before it. */ -#define TOTAL_COMBINATIONS 101886 -#define BASELINE_LOAD_FAILURES 1470 -#define BASELINE_BOUND_MISMATCHES 480 +#define TOTAL_COMBINATIONS 100222 + +/* Every (error code, count) pair load failures are currently expected to fall into. + * Equality per code, not a ceiling: a future fix should remove an entry (or lower its + * count) as its root cause is addressed, and any load failure whose code isn't listed + * here at all is an unconditional hard failure below, not something a budget can + * quietly absorb. */ +typedef struct { + int code; + long baseline; +} error_baseline; + +static const error_baseline LOAD_FAILURE_BASELINES[] = { + /* DEDX_MSTAR ions 12-15 + a gas target -- see the history note above. */ + {DEDX_ERR_ION_NOT_SUPPORTED_MSTAR, 108}, +}; + +#define BASELINE_BOUND_MISMATCHES 470 #define BASELINE_DEAD_CONFIGS 0 /* Number of energies sampled (log-spaced) across each combination's advertised * [min, max] range for the "dead at every energy" check. */ #define SAMPLE_COUNT 9 +/* One counter per entry in LOAD_FAILURE_BASELINES, in the same order, plus a catch-all + * for any error code not listed there. */ typedef struct { long total; long load_failures; + long load_failures_by_code[sizeof(LOAD_FAILURE_BASELINES) / sizeof(LOAD_FAILURE_BASELINES[0])]; + long load_failures_unexpected; long bound_mismatches; long dead_configs; long alloc_failures; @@ -133,7 +157,29 @@ static void sweep_one(int program, int ion, int target, sweep_stats *stats) { err = 0; rc = dedx_load_config(ws, cfg, &err); if (rc != 0 || err != DEDX_OK) { + size_t code_i; + int matched = 0; + stats->load_failures++; + for (code_i = 0; code_i < sizeof(LOAD_FAILURE_BASELINES) / sizeof(LOAD_FAILURE_BASELINES[0]); code_i++) { + if (LOAD_FAILURE_BASELINES[code_i].code == err) { + stats->load_failures_by_code[code_i]++; + matched = 1; + break; + } + } + if (!matched) { + stats->load_failures_unexpected++; + if (stats->load_failures_unexpected <= 5) { + fprintf(stderr, + "FAIL sweep_one: unexpected error code %d for program=%d ion=%d target=%d " + "(not in LOAD_FAILURE_BASELINES)\n", + err, + program, + ion, + target); + } + } dedx_free_config(cfg, &err); dedx_free_workspace(ws, &err); return; @@ -202,8 +248,8 @@ static int check_baseline(long got, long baseline, const char *label) { return 1; } if (got < baseline) { - /* Progress! Not a failure, but flag it so whoever fixed part of A1/A4/A5/A6 - * remembers to tighten the baseline in this file as part of that change. */ + /* Progress! Not a failure, but flag it so whoever fixed part of the remaining + * gap remembers to tighten the baseline in this file as part of that change. */ fprintf(stderr, "NOTE %s: %ld is below the recorded baseline of %ld -- please lower " "BASELINE_* in tests/test_availability_exhaustive.c to match\n", @@ -216,9 +262,12 @@ static int check_baseline(long got, long baseline, const char *label) { int main(void) { const int *programs = dedx_get_program_list(); - sweep_stats stats = {0, 0, 0, 0, 0}; + sweep_stats stats; int failures = 0; int p; + size_t code_i; + + memset(&stats, 0, sizeof(stats)); for (p = 0; programs[p] != -1; p++) { int program = programs[p]; @@ -241,6 +290,13 @@ int main(void) { stats.bound_mismatches, stats.dead_configs, stats.alloc_failures); + for (code_i = 0; code_i < sizeof(LOAD_FAILURE_BASELINES) / sizeof(LOAD_FAILURE_BASELINES[0]); code_i++) { + printf( + " load_failures[err=%d]=%ld\n", LOAD_FAILURE_BASELINES[code_i].code, stats.load_failures_by_code[code_i]); + } + if (stats.load_failures_unexpected > 0) { + printf(" load_failures[unexpected]=%ld\n", stats.load_failures_unexpected); + } /* Unlike the baselines below, any allocation failure is an unconditional hard * failure -- there is no acceptable count of "the sweep couldn't get memory". */ @@ -259,7 +315,23 @@ int main(void) { TOTAL_COMBINATIONS); failures++; } - failures += check_baseline(stats.load_failures, BASELINE_LOAD_FAILURES, "load_failures"); + + /* Any load failure whose error code isn't in LOAD_FAILURE_BASELINES at all is a + * brand-new failure mode -- always a hard failure, never just a NOTE, regardless + * of how the aggregate count moves. */ + if (stats.load_failures_unexpected > 0) { + fprintf(stderr, + "FAIL load_failures_unexpected: %ld load failure(s) used an error code not listed in " + "LOAD_FAILURE_BASELINES -- see the FAIL lines above for examples; add a traced entry " + "instead of ignoring it\n", + stats.load_failures_unexpected); + failures++; + } + for (code_i = 0; code_i < sizeof(LOAD_FAILURE_BASELINES) / sizeof(LOAD_FAILURE_BASELINES[0]); code_i++) { + char label[64]; + snprintf(label, sizeof(label), "load_failures[err=%d]", LOAD_FAILURE_BASELINES[code_i].code); + failures += check_baseline(stats.load_failures_by_code[code_i], LOAD_FAILURE_BASELINES[code_i].baseline, label); + } failures += check_baseline(stats.bound_mismatches, BASELINE_BOUND_MISMATCHES, "bound_mismatches"); failures += check_baseline(stats.dead_configs, BASELINE_DEAD_CONFIGS, "dead_configs"); diff --git a/tests/test_error_codes.c b/tests/test_error_codes.c index 92e4734..c5e8306 100644 --- a/tests/test_error_codes.c +++ b/tests/test_error_codes.c @@ -57,6 +57,12 @@ static int test_error_code_strings_complete(void) { failures += check_has_message(DEDX_ERR_INVALID_I_VALUE, "DEDX_ERR_INVALID_I_VALUE"); failures += check_has_message(DEDX_ERR_INCONSISTENT_COMPOUND, "DEDX_ERR_INCONSISTENT_COMPOUND"); failures += check_has_message(DEDX_ERR_INVALID_INTERPOLATION_MODE, "DEDX_ERR_INVALID_INTERPOLATION_MODE"); + /* DEDX_ERR_INVALID_MSTAR_MODE was missing from this completeness sweep when it was + * introduced (issue #149 finding A7) -- caught in review; added here so the table + * this test walks and the codes dedx_error.h defines can't silently drift apart + * again, which is the entire point of this test (see the comment above). */ + failures += check_has_message(DEDX_ERR_INVALID_MSTAR_MODE, "DEDX_ERR_INVALID_MSTAR_MODE"); + failures += check_has_message(DEDX_ERR_INCONSISTENT_ENERGY_GRID, "DEDX_ERR_INCONSISTENT_ENERGY_GRID"); failures += check_has_message(DEDX_ERR_NO_MEMORY, "DEDX_ERR_NO_MEMORY"); /* An unrecognised code must still fall back to the default message rather than, diff --git a/tests/test_interpolation.c b/tests/test_interpolation.c index 6e818e3..9bd2519 100644 --- a/tests/test_interpolation.c +++ b/tests/test_interpolation.c @@ -390,7 +390,9 @@ static int check_effective_interpolation_mode_invalid_id(void) { cfg->cfg_id = -1; mode = dedx_get_effective_interpolation_mode(ws, cfg, &err); - if (err != DEDX_ERR_INVALID_DATASET_ID || mode != 0) { + /* -1, not 0 (== DEDX_INTERPOLATION_LOG_LOG): a caller that forgets to check *err + * must not be able to mistake a failed call for "log-log" -- raised in review. */ + if (err != DEDX_ERR_INVALID_DATASET_ID || mode != -1) { fprintf(stderr, "FAIL A6 invalid cfg_id: err=%d mode=%d\n", err, mode); failures++; } diff --git a/tests/test_material_availability.c b/tests/test_material_availability.c index ddf687e..1c0f734 100644 --- a/tests/test_material_availability.c +++ b/tests/test_material_availability.c @@ -155,8 +155,12 @@ static int test_auto_bethe_fallback(void) { * (whichever constituent happened to be first) regardless of whether the other * constituent's own grid matched, silently truncating or mixing energy bins. * Post-A1, mismatched per-constituent grids are detected and rejected with - * DEDX_ERR_INCONSISTENT_COMPOUND instead of being served as a wrong or - * silently-truncated number -- so this load is now expected to fail cleanly. */ + * DEDX_ERR_INCONSISTENT_ENERGY_GRID instead of being served as a wrong or + * silently-truncated number -- so this load is now expected to fail cleanly. + * (A dedicated code, not a reuse of DEDX_ERR_INCONSISTENT_COMPOUND: the caller's + * compound specification here is perfectly valid -- BoronCarbide is one real + * material id -- the problem is purely that its constituents resolved onto + * different embedded energy grids, which is diagnosable on its own.) */ ws = dedx_allocate_workspace(1, &err); cfg = calloc(1, sizeof(dedx_config)); cfg->program = DEDX_AUTO; @@ -164,8 +168,15 @@ static int test_auto_bethe_fallback(void) { cfg->target = DEDX_BORON_CARBIDE; err = 0; dedx_load_config(ws, cfg, &err); - failures += check_err(err, DEDX_ERR_INCONSISTENT_COMPOUND, "AUTO+proton+BoronCarbide load (mismatched grids)"); + failures += check_err(err, DEDX_ERR_INCONSISTENT_ENERGY_GRID, "AUTO+proton+BoronCarbide load (mismatched grids)"); failures += check_err(cfg->bragg_used, 1, "AUTO+proton+BoronCarbide should still attempt Bragg additivity"); + /* Regression check for issue #149 finding B5: a failed load_compound() used to + * leave config->target pointing at whichever constituent element it was resolving + * when it gave up (here, boron or carbon -- not BoronCarbide), so a caller who + * reused the config afterward (e.g. "try AUTO, fall back to PSTAR on error") would + * silently query the wrong target. It must come back exactly as the caller left + * it. */ + failures += check_err(cfg->target, DEDX_BORON_CARBIDE, "failed AUTO+BoronCarbide load must not corrupt target"); dedx_free_config(cfg, &err); dedx_free_workspace(ws, &err); @@ -290,9 +301,15 @@ static int test_ferrous_oxide_tabulated_program(void) { dedx_load_config(ws, cfg, &err); failures += check_err(err, DEDX_OK, "PSTAR+proton+FerrousOxide load"); if (err == DEDX_OK) { - /* Density must have been resolved from the embedded table, not left at 0. */ - if (cfg->rho <= 0.0f) { - fprintf(stderr, "FAIL PSTAR+proton+FerrousOxide: rho not populated (%f)\n", (double) cfg->rho); + /* Density must have been resolved from the embedded table to the specific, + * cited literature value (data/README.md), not merely to "something positive" + * -- a bare rho > 0 check would pass a typo'd 57.0 or 0.57 just as happily as + * the real 5.7 g/cm^3, and this value feeds live physics (the Bethe + * density-effect term for DEDX_AUTO's fallback tier), not just metadata. */ + if (fabsf(cfg->rho - 5.7f) > 1e-4f) { + fprintf(stderr, + "FAIL PSTAR+proton+FerrousOxide: rho=%f, expected 5.7 (see data/README.md)\n", + (double) cfg->rho); failures++; } stp = dedx_get_stp(ws, cfg, 100.0f, &err); @@ -417,12 +434,19 @@ static int test_material_list_for_ion(void) { failures += check_absent(materials, DEDX_BORON, "ICRU+proton for_ion"); failures += check_absent(materials, DEDX_BORON_CARBIDE, "ICRU+proton for_ion"); - /* DEDX_AUTO does fall back -- both appear. */ + /* DEDX_AUTO does fall back for the pure element -- Boron appears. BoronCarbide + * does not: it Bragg-decomposes into boron (Bethe-fallback tier) and carbon + * (tabulated tier), which resolve onto different energy grids and would fail to + * load with DEDX_ERR_INCONSISTENT_ENERGY_GRID (see test_auto_bethe_fallback() + * above) -- material_id_supported() now predicts that tier mismatch instead of + * advertising a combination dedx_load_config() is known to reject (issue #149 + * finding B4: the availability API must stay accurate, not just "falls back + * rather than failing outright"). */ dedx_get_material_list_for_ion(DEDX_AUTO, DEDX_PROTON, materials, DEDX_MAX_MATERIAL_LIST, &len, &err); failures += check_err(err, DEDX_OK, "AUTO+proton for_ion err"); materials[len] = -1; failures += check_present(materials, DEDX_BORON, "AUTO+proton for_ion"); - failures += check_present(materials, DEDX_BORON_CARBIDE, "AUTO+proton for_ion"); + failures += check_absent(materials, DEDX_BORON_CARBIDE, "AUTO+proton for_ion"); dedx_get_material_list_for_ion(DEDX_ICRU73, DEDX_LITHIUM, materials, DEDX_MAX_MATERIAL_LIST, &len, &err); failures += check_err(err, DEDX_OK, "ICRU73+lithium for_ion err"); diff --git a/tests/test_mstar.c b/tests/test_mstar.c index 614d412..a15f68d 100644 --- a/tests/test_mstar.c +++ b/tests/test_mstar.c @@ -68,6 +68,57 @@ static int test_invalid_mstar_mode(void) { return failures; } +/* + * Regression pin raised in review of issue #149's Phase 2 PR (finding B1): + * load_compound() resolving config->compound_state before its per-constituent loop + * (added for A3, so Bethe-type constituents get the *compound's* gas/condensed state + * rather than the public dedx_get_i_value()'s hardcoded gas default) has a side effect + * on DEDX_MSTAR specifically. resolve_mstar_mode() (dedx_mstar.c) also reads + * config->compound_state, and only falls back to its own per-target gas check when + * compound_state is still DEDX_DEFAULT_STATE; once load_compound() resolves it first, + * MSTAR mode 'a'/'b' letters now pick gas ('g'/'h') or condensed ('c'/'d') from the + * *compound's* own state for every constituent, rather than each constituent element's + * own state as before. For a gas compound like BUTANE this changes MSTAR's numeric + * output by a large margin (mode flips from 'g' to 'h' end to end) -- previously an + * undocumented, untested side effect of A3; not a computed-value regression on its + * own (the new behavior is arguably more physically correct: Bragg additivity treats + * a compound's condensed/gas state as a property of the compound, not of each atom in + * isolation -- the same convention this fix already applies for I-values), but it + * needed to be a pinned, visible number instead of an unremarked diff. + * + * These values are the current output of this branch, not independently verified + * against dedx_web or the MSTAR/ICRU literature -- that cross-check is out of scope + * for what this test can do. Their job is only to make a *future* change to this + * resolution path show up here instead of silently drifting again. + */ +static int test_gas_compound_uses_compound_state_not_constituent(void) { + int failures = 0; + const float energy_grid[] = {0.07f, 1.0f, 10.0f, 78.0f, 1000.0f}; + + /* BUTANE is a gas: with the default mstar_mode ('\0' -> DEDX_MSTAR_MODE_DEFAULT == + * 'b'), resolve_mstar_mode() must now pick 'h' from the compound's own state. */ + failures += + check_config_stp(make_mstar_mode_config(DEDX_BUTANE, '\0'), energy_grid[0], 7027.838867f, "mstar-butane-gas"); + failures += + check_config_stp(make_mstar_mode_config(DEDX_BUTANE, '\0'), energy_grid[1], 7598.953613f, "mstar-butane-gas"); + failures += + check_config_stp(make_mstar_mode_config(DEDX_BUTANE, '\0'), energy_grid[2], 1849.043213f, "mstar-butane-gas"); + failures += + check_config_stp(make_mstar_mode_config(DEDX_BUTANE, '\0'), energy_grid[3], 350.166901f, "mstar-butane-gas"); + failures += + check_config_stp(make_mstar_mode_config(DEDX_BUTANE, '\0'), energy_grid[4], 86.426956f, "mstar-butane-gas"); + + /* WATER is condensed, so its resolved mode ('d') is unaffected either way -- these + * match the pre-existing explicit-mode-'d' values below exactly, confirming the + * default-mode path still resolves the same way for a condensed compound. */ + failures += check_config_stp( + make_mstar_mode_config(DEDX_WATER, '\0'), energy_grid[0], 5589.206055f, "mstar-water-condensed"); + failures += + check_config_stp(make_mstar_mode_config(DEDX_WATER, '\0'), energy_grid[4], 80.005943f, "mstar-water-condensed"); + + return failures; +} + static int check_mode_equivalence(int target, char lhs_mode, char rhs_mode, float energy, const char *label) { int err = 0; int failures = 0; @@ -226,6 +277,7 @@ int main(void) { check_mode_equivalence(DEDX_AIR_DRY_NEAR_SEA_LEVEL, DEDX_MSTAR_MODE_B, DEDX_MSTAR_MODE_H, 10.0f, "mstar-b-gas"); failures += test_invalid_mstar_mode(); + failures += test_gas_compound_uses_compound_state_not_constituent(); return failures; } From 734c562faeeb1e46ea8c5f3329f3bb7e7344165c Mon Sep 17 00:00:00 2001 From: Leszek Grzanka Date: Wed, 12 Aug 2026 08:59:54 +0200 Subject: [PATCH 5/6] Follow-up: close the 108-combo MSTAR advertising gap, harden dedx_get_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 --- src/dedx.c | 102 +++++++++++++++++++++++---- tests/test_availability_exhaustive.c | 40 ++++++++--- tests/test_material_availability.c | 55 +++++++++++++++ 3 files changed, 174 insertions(+), 23 deletions(-) diff --git a/src/dedx.c b/src/dedx.c index 4509b70..ef801ef 100644 --- a/src/dedx.c +++ b/src/dedx.c @@ -25,6 +25,7 @@ #include "dedx_data_access.h" #include "dedx_embedded_data.h" #include "dedx_lookup_data.h" +#include "dedx_mpaul.h" #include "dedx_mstar.h" #include "dedx_periodic_table.h" #include "dedx_program_const.h" @@ -57,8 +58,8 @@ static int find_data(stopping_data *data, dedx_config *config, float *energy, in static int load_compound(dedx_workspace *ws, dedx_config *config, int *err); static int load_bethe_2(stopping_data *data, dedx_config *config, float *energy, int *err); static int load_bethe_fallback(stopping_data *data, dedx_config *config, float *energy, int *err); -static int element_supported_for_ion(int program, int ion, int element); -static int material_id_supported(int program, int ion, int material); +static int element_supported_for_ion(int program, int ion, int element, int mstar_state); +static int material_id_supported(int program, int ion, int material, int mstar_state); dedx_workspace *dedx_allocate_workspace(unsigned int count, int *err) { unsigned int i = 0; @@ -243,11 +244,17 @@ const int *dedx_get_material_list(int program) { * programs) so the answer stays consistent with what dedx_load_config() will * actually do. * + * `mstar_state` is DEDX_DEFAULT_STATE/DEDX_GAS/DEDX_CONDENSED and matters only for + * program == DEDX_MSTAR: DEDX_DEFAULT_STATE means "resolve gas/condensed from this + * element's own metadata", correct when `element` is checked as a standalone target. + * When `element` is one constituent of a compound, material_id_supported() passes the + * *compound's own* resolved state here instead -- see its comment for why. + * * Returns 1 if dedx_load_config() is expected to succeed for this exact * (program, ion, element) combination, 0 otherwise. `element` is assumed to already * be a plain element id (1..DEDX_MAX_ELEMENT_ID); callers are responsible for that * precondition -- see material_id_supported()'s own boundary check below. */ -static int element_supported_for_ion(int program, int ion, int element) { +static int element_supported_for_ion(int program, int ion, int element, int mstar_state) { int ion_load = ion; int resolved; @@ -264,6 +271,41 @@ static int element_supported_for_ion(int program, int ion, int element) { if (program == DEDX_ICRU || program == DEDX_ICRU49 || program == DEDX_ICRU73) { return dedx_embedded_resolve_program(program, ion_load, element, &resolved) == 0; } + if (program == DEDX_MSTAR) { + int gas_err = DEDX_OK; + int coef_err = DEDX_OK; + char mode; + + if (!dedx_embedded_has_table(program, ion_load, element)) { + return 0; + } + /* Having the underlying MSTAR-helium base table is necessary but not + * sufficient: dedx_internal_calculate_mspaul_coef()'s per-ion fit is only + * parametrized for a subset of (mode, ion) pairs (see issue #149 finding A7's + * fix in dedx_mpaul.c, which now reports DEDX_ERR_ION_NOT_SUPPORTED_MSTAR for + * an unparametrized combination instead of silently computing with + * placeholder coefficients), and that gap is ion- and gas/condensed-state- + * dependent, not element-table-dependent, so dedx_embedded_has_table() alone + * cannot see it -- material_id_supported() kept advertising these + * combinations after A7's fix started correctly rejecting them (see the PR + * review discussion). Probe the actual coefficient computation the way + * find_data() would resolve it for an unconfigured caller (mstar_mode left at + * '\0' so find_data() assumes 'b'): resolve gas/condensed exactly as + * resolve_mstar_mode() does for mode 'b', then probe at one energy -- the + * failure this catches is energy-independent (it happens before + * dedx_internal_calculate_mspaul_coef() ever looks at its energy argument), + * so any single valid energy is representative of every energy in the loaded + * grid. */ + if (mstar_state == DEDX_DEFAULT_STATE) { + mstar_state = dedx_internal_target_is_gas(element, &gas_err) ? DEDX_GAS : DEDX_CONDENSED; + if (gas_err != DEDX_OK) { + return 0; /* LCOV_EXCL_LINE -- every element reaching this point has known gas-state metadata */ + } + } + mode = (mstar_state == DEDX_GAS) ? DEDX_MSTAR_MODE_H : DEDX_MSTAR_MODE_D; + dedx_internal_calculate_mspaul_coef(mode, ion, element, 10.0f, &coef_err); + return coef_err == DEDX_OK; + } return dedx_embedded_has_table(program, ion_load, element); } @@ -283,10 +325,23 @@ static int element_is_tabulated_for_auto(int ion, int element) { * element is itself reachable, matching the Bragg-additivity decomposition * load_compound() performs at calculation time. * + * `mstar_state` carries a compound's own resolved gas/condensed state down into its + * constituents' element_supported_for_ion() checks -- see that function's comment. + * Top-level callers pass DEDX_DEFAULT_STATE (resolve from whatever `material` itself + * turns out to be); the compound branch below resolves it once a compound's own state + * is known and passes it down uniformly, mirroring load_compound(), which resolves + * config->compound_state once, from the compound's own id, before its constituent + * loop runs -- and does so regardless of program, since dedx_internal_validate_config() + * only calls dedx_internal_validate_state() for program >= 100, leaving MSTAR/AUTO/ + * other tabulated-program compounds to reach load_compound()'s own resolve step (see + * issue #149 finding A3). The distinction is invisible for every other program; it + * changes the answer only for DEDX_MSTAR, where an unparametrized (mode, ion) pair + * can be fine for one gas/condensed state and undefined for the other (finding A7). + * * Returns 1 if dedx_load_config() is expected to succeed for this exact * (program, ion, material) combination, 0 otherwise -- for a compound, this recurses * once per constituent element and is 1 only if every one of them is reachable. */ -static int material_id_supported(int program, int ion, int material) { +static int material_id_supported(int program, int ion, int material, int mstar_state) { float composition[20][2]; unsigned int comp_len; unsigned int i; @@ -308,7 +363,7 @@ static int material_id_supported(int program, int ion, int material) { return 0; } /* LCOV_EXCL_STOP */ if (material <= DEDX_MAX_ELEMENT_ID) { - return element_supported_for_ion(program, ion, material); + return element_supported_for_ion(program, ion, material, mstar_state); } /* Analytical programs (program >= DEDX_DEFAULT) evaluate the Bethe-Bloch formula @@ -338,6 +393,15 @@ static int material_id_supported(int program, int ion, int material) { } } + if (program == DEDX_MSTAR && mstar_state == DEDX_DEFAULT_STATE) { + int gas_err = DEDX_OK; + + mstar_state = dedx_internal_target_is_gas(material, &gas_err) ? DEDX_GAS : DEDX_CONDENSED; + if (gas_err != DEDX_OK) { + return 0; /* LCOV_EXCL_LINE -- every compound in the master material list has known gas-state metadata */ + } + } + dedx_internal_get_composition(material, composition, &comp_len, &comp_err); if (comp_err != DEDX_OK || comp_len == 0) { /* LCOV_EXCL_START -- every compound in the master material list has known embedded composition */ @@ -346,7 +410,7 @@ static int material_id_supported(int program, int ion, int material) { for (i = 0; i < comp_len; i++) { int constituent = (int) composition[i][0]; - if (!material_id_supported(program, ion, constituent)) { + if (!material_id_supported(program, ion, constituent, mstar_state)) { return 0; } if (program == DEDX_AUTO && constituent > 0 && constituent <= DEDX_MAX_ELEMENT_ID) { @@ -384,7 +448,7 @@ void dedx_get_material_list_for_ion( } for (i = 0; candidates[i] != -1 && count < max_materials; i++) { - if (material_id_supported(program, ion, candidates[i])) { + if (material_id_supported(program, ion, candidates[i], DEDX_DEFAULT_STATE)) { materials[count++] = candidates[i]; } } @@ -392,14 +456,28 @@ void dedx_get_material_list_for_ion( } const int *dedx_get_ion_list(int program) { - /* Returns a -1-terminated list of ions available for the program. Both branches + /* Returns a -1-terminated list of ions available for the program. All branches * return immutable const data -- the shared full-ion table for unrestricted - * programs, or the program's row in dedx_program_available_ions -- so the function - * performs no writes and is safe to call concurrently from multiple threads. */ + * programs, the empty list below for an unrecognized program, or the program's + * row in dedx_program_available_ions -- so the function performs no writes and + * is safe to call concurrently from multiple threads. */ + static const int empty_ion_list[] = {-1}; + if (program == DEDX_BETHE_EXT00 || program == DEDX_DEFAULT || program == DEDX_AUTO) /* any ion, no restrictions */ return dedx_full_ion_list; - else - return dedx_program_available_ions[program]; + /* dedx_internal_check_ion() (dedx_validate.c) already guards its own call into + * this function against an unrecognized program, closing the crash finding #149 + * B3 was raised about for the internal dedx_load_config() path. This function is + * public API too, though, callable directly with any int -- and + * dedx_program_available_ions only has real, -1-terminated rows for indices 0 + * (unused placeholder, "all ions") through DEDX_ICRU (9); every other row in its + * 110-row declaration is implicitly zero-filled with no -1 terminator, and a + * negative index is undefined behavior regardless. Bounds-check here too instead + * of relying on every caller to have gone through dedx_internal_check_ion() + * first (see the PR review discussion following B3's fix). */ + if (program < 0 || program > DEDX_ICRU) + return empty_ion_list; + return dedx_program_available_ions[program]; } float dedx_get_min_energy(int program, int ion) { diff --git a/tests/test_availability_exhaustive.c b/tests/test_availability_exhaustive.c index 091a1d0..965d421 100644 --- a/tests/test_availability_exhaustive.c +++ b/tests/test_availability_exhaustive.c @@ -67,19 +67,32 @@ * TOTAL_COMBINATIONS 101886 -> 100222 (-1664, the compounds/programs that would * have failed and are no longer advertised), load_failures 1470 -> 108, * bound_mismatches 480 -> 470. - * - The 108 that remain are DEDX_MSTAR only, all DEDX_ERR_ION_NOT_SUPPORTED_MSTAR: + * - The 108 that remained were DEDX_MSTAR only, all DEDX_ERR_ION_NOT_SUPPORTED_MSTAR: * ions 12-15 combined with a gas target hit an "illegal mode" branch in * dedx_mpaul.c that predates this whole PR (reproduced identically against * `main`, e.g. DEDX_MSTAR + ion 12 + DEDX_ARGON) -- previously silent (a * nonsense stopping power with err == DEDX_OK, also found in review, finding B2), - * now a clean, correctly-reported error. material_id_supported() does not yet - * model this for MSTAR specifically -- doing so needs either an mstar_mode - * parameter on the availability query (an API change) or a hardcoded assumption - * about the default mode, neither of which this PR takes on -- so it stays a - * documented, open gap here, the same shape as the FERROUSOXIDE gap before it. + * now a clean, correctly-reported error. + * - A second review follow-up closed that gap too: material_id_supported() and + * element_supported_for_ion() now take an `mstar_state` parameter, not a new + * public API surface but the "hardcoded assumption about the default mode" + * option floated above -- find_data() itself already assumes mstar_mode 'b' when + * a caller leaves it unset, so probing dedx_internal_calculate_mspaul_coef() + * under that same default is exactly what dedx_load_config() would actually do. + * For a *compound* target, that probe 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 (see issue #149 finding A3), so a per-constituent guess + * would both under- and over-advertise relative to what dedx_load_config() does. + * TOTAL_COMBINATIONS: 100222 -> 100114 (-108, the MSTAR ion/gas-target + * combinations that would have failed and are 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, so dedx_load_config() is now expected to succeed for every + * combination this sweep is asked to check. */ -#define TOTAL_COMBINATIONS 100222 +#define TOTAL_COMBINATIONS 100114 /* Every (error code, count) pair load failures are currently expected to fall into. * Equality per code, not a ceiling: a future fix should remove an entry (or lower its @@ -91,10 +104,15 @@ typedef struct { long baseline; } error_baseline; -static const error_baseline LOAD_FAILURE_BASELINES[] = { - /* DEDX_MSTAR ions 12-15 + a gas target -- see the history note above. */ - {DEDX_ERR_ION_NOT_SUPPORTED_MSTAR, 108}, -}; +/* Empty: as of the MSTAR-availability follow-up below, every load failure this sweep + * used to hit has a root cause that's either fixed outright or -- for the two open, + * documented physics-data gaps (FERROUSOXIDE's I-value, the ICRU73 Na-in-Ar zero) -- + * no longer advertised as available in the first place, so this sweep now expects + * dedx_load_config() to succeed for every combination it's asked to check. A future + * regression that adds even one new load failure is therefore always a hard FAIL via + * load_failures_unexpected below (any error code, since none has an entry here to + * match against), not something a baseline entry could absorb. */ +static const error_baseline LOAD_FAILURE_BASELINES[] = {}; #define BASELINE_BOUND_MISMATCHES 470 #define BASELINE_DEAD_CONFIGS 0 diff --git a/tests/test_material_availability.c b/tests/test_material_availability.c index 1c0f734..a04284f 100644 --- a/tests/test_material_availability.c +++ b/tests/test_material_availability.c @@ -400,6 +400,60 @@ static int test_compound_state_affects_bethe_fallback(void) { return failures; } +/* + * Regression test for a review follow-up to issue #149 findings B2/B4: + * dedx_mpaul.c's mode 'h' fit is only parametrized for ions {3-11,16,17,18} (see + * B2's fix, which reports DEDX_ERR_ION_NOT_SUPPORTED_MSTAR for the rest instead of + * silently computing with placeholder coefficients); ions 12-15 on a gas target hit + * that gap. material_id_supported() must not advertise a combination + * dedx_load_config() is known to reject the same way it already stopped advertising + * DEDX_AUTO's mismatched-grid compounds (B4) -- otherwise this reopens the exact + * false-advertisement defect class issue #149 was raised about, just for MSTAR + * instead of DEDX_AUTO. + * + * BUTANE is a gas whose non-gas constituent (carbon) makes the compound-vs-element + * state distinction observable: the check has to use BUTANE's own resolved state + * (gas -> mode 'h'), not carbon's own elemental state (condensed -> mode 'd', which + * is fully parametrized and would wrongly predict availability). WATER is condensed, + * so ion 13 must stay available there -- this is an ion/mode-combination gap, not a + * blanket rejection of ions 12-15. */ +static int test_mstar_availability_reflects_coefficient_gap(void) { + int failures = 0; + int materials[DEDX_MAX_MATERIAL_LIST + 1]; + unsigned int len; + int err; + int ion; + + for (ion = 12; ion <= 15; ion++) { + char label[64]; + + snprintf(label, sizeof(label), "MSTAR+ion%d for_ion", ion); + dedx_get_material_list_for_ion(DEDX_MSTAR, ion, materials, DEDX_MAX_MATERIAL_LIST, &len, &err); + failures += check_err(err, DEDX_OK, label); + materials[len] = -1; + failures += check_absent(materials, DEDX_BUTANE, label); + failures += check_present(materials, DEDX_WATER, label); + } + + /* A direct dedx_load_config() call must agree with what's (no longer) advertised + * above -- the availability API and the loader must never disagree. */ + { + dedx_workspace *ws = dedx_allocate_workspace(1, &err); + dedx_config *cfg = calloc(1, sizeof(dedx_config)); + + cfg->program = DEDX_MSTAR; + cfg->ion = 13; + cfg->target = DEDX_BUTANE; + err = 0; + dedx_load_config(ws, cfg, &err); + failures += check_err(err, DEDX_ERR_ION_NOT_SUPPORTED_MSTAR, "MSTAR+ion13+BUTANE load"); + dedx_free_config(cfg, &err); + dedx_free_workspace(ws, &err); + } + + return failures; +} + static int test_material_list_for_ion(void) { int failures = 0; int materials[DEDX_MAX_MATERIAL_LIST + 1]; @@ -508,6 +562,7 @@ int main(void) { failures += test_a150_boundary(); failures += test_ferrous_oxide_tabulated_program(); failures += test_compound_state_affects_bethe_fallback(); + failures += test_mstar_availability_reflects_coefficient_gap(); failures += test_material_list_for_ion(); failures += test_fill_material_list_for_ion(); From 996d55a72f453dd8dc714ae2afc5ef6798a4f5ca Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 07:05:25 +0000 Subject: [PATCH 6/6] Fix MSVC build: avoid empty-array initializer in test_availability_exhaustive.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 Claude-Session: https://claude.ai/code/session_0188VRnSFzb7HvNKcMX7XT1Z --- tests/test_availability_exhaustive.c | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/tests/test_availability_exhaustive.c b/tests/test_availability_exhaustive.c index 965d421..04afafe 100644 --- a/tests/test_availability_exhaustive.c +++ b/tests/test_availability_exhaustive.c @@ -104,15 +104,25 @@ typedef struct { long baseline; } error_baseline; -/* Empty: as of the MSTAR-availability follow-up below, every load failure this sweep - * used to hit has a root cause that's either fixed outright or -- for the two open, - * documented physics-data gaps (FERROUSOXIDE's I-value, the ICRU73 Na-in-Ar zero) -- - * no longer advertised as available in the first place, so this sweep now expects - * dedx_load_config() to succeed for every combination it's asked to check. A future - * regression that adds even one new load failure is therefore always a hard FAIL via - * load_failures_unexpected below (any error code, since none has an entry here to - * match against), not something a baseline entry could absorb. */ -static const error_baseline LOAD_FAILURE_BASELINES[] = {}; +/* Effectively empty: as of the MSTAR-availability follow-up below, every load failure + * this sweep used to hit has a root cause that's either fixed outright or -- for the + * two open, documented physics-data gaps (FERROUSOXIDE's I-value, the ICRU73 Na-in-Ar + * zero) -- no longer advertised as available in the first place, so this sweep now + * expects dedx_load_config() to succeed for every combination it's asked to check. A + * future regression that adds even one new load failure is therefore always a hard + * FAIL via load_failures_unexpected below (any error code, since none has a real entry + * here to match against), not something a baseline entry could absorb. + * + * A literal `{}` initializer here would be a GCC/Clang extension, not standard C -- + * MSVC (a supported CI target) rejects it, and rejects the resulting zero-length + * `load_failures_by_code[0]` array member below even harder ("illegal zero-sized + * array"). {-1, 0} is a sentinel, not a real baseline: -1 is not a DEDX_ERR_* value + * (see dedx_error.h) and dedx_load_config() never sets *err to it, so it can never + * match a real failure in the loop below -- it exists purely to keep this a portable, + * non-empty, one-element array. */ +static const error_baseline LOAD_FAILURE_BASELINES[] = { + {-1, 0}, +}; #define BASELINE_BOUND_MISMATCHES 470 #define BASELINE_DEAD_CONFIGS 0