Bugs and inconsistencies found during the architecture review#
Found while reading the api_generalisation branch (head b8bc7d8) for the
ground-up design comparison in groundup_design.md. Every item was verified
by reading the code at the cited lines (line numbers refer to b8bc7d8).
Items are grouped by how sure I am that they are wrong rather than merely
fragile.
Status: every item except 11 is fixed on this branch; each carries a
Resolution line. Item 11 is a design-level change and is deferred to
groundup_design.md.
Confirmed bugs#
1. IsobaricAnalogPNObservation silently drops two solver arguments#
Where:
src/rxmc/ias_pn_observation.py:52-53(constructor signature) and:113-120(theset_up_solvercall).What: the constructor accepts
wavelengths_beyond_rangeandzeros_per_node, documents them (:92-94), andset_up_solvertakes them (:209-210), but the call inside__init__never forwards them. The defaults are always used. The elastic observation forwards both (src/rxmc/elastic_diffxs_observation.py:149-150).Why it matters: a user tuning the Lagrange basis size for the (p,n) IAS channel gets no effect and no error.
Fix: add the two keyword arguments to the
set_up_solvercall.Resolution: forwarded;
test_reaction_observation.py::TestSolverSettingsForwardingasserts the kwargs reachset_up_solverfor both reaction observations.
2. BatchedAdaptiveMetropolisSampler adapts its proposal during burn-in#
Where:
src/rxmc/param_sampling.py:376-388.What: the class docstring (
:304), thesampledocstring (:354) and theburnparameter docstring (:366-368) all say the proposal covariance is replaced only after non-burn batches. The code recomputesself.proposal_covandself.argsunconditionally; onlyrecord_batchis guarded byif not burn.Why it matters: burn-in batches feed the adaptation, so the behaviour differs from what the docstring promises and from
AdaptiveMetropolisSampler. Either the doc or the code is wrong. Adapting during burn-in is arguably the better behaviour, so the likely fix is to the docstrings.Fix: decide, then make the docstrings and the
if not burn:guard agree.Resolution: kept the code behaviour (adapt after every batch, burn-in included) and fixed the three docstrings.
samplenow also refreshesself.proposalso it never goes stale; pinned bytest_sampler.py::TestSamplerPriors::test_batched_adaptive_updates_proposal_after_burn_batch.
3. Parameter defines __eq__ without __hash__#
Where:
src/rxmc/params.py:39-48.What: defining
__eq__sets__hash__ = None, soParameterobjects are unhashable. Every uniqueness check in the package keys onid(p)orp.namefor this reason (covariance.py,constraint.py,evidence.py), and nothing documents it.Why it matters:
set(model.params)or{p: value}raisesTypeErrorat runtime. It is a trap for anyone extending the package, and it is the root reason the by-identity routing needsid()bookkeeping.Fix: either drop
__eq__(identity is the sharing semantics anyway) or add__hash__ = object.__hash__. Seegroundup_design.md§2.1.Resolution: value-based
__hash__consistent with the existing value__eq__;boundsis coerced to a 2-tuple of floats so the hash is stable;__repr__added. Identity routing incovariance.py/evidence.pyis untouched. Newtest/test_params.py.
4. Two independent pint UnitRegistry instances#
Where:
src/rxmc/elastic_diffxs_observation.py:24andsrc/rxmc/ias_pn_observation.py:12.What: each module builds its own registry. pint refuses to combine quantities from different registries.
Why it matters: latent today because no code path mixes the two, but any helper that takes a quantity from one module into the other will raise.
DEFAULT_LMAX = 20is likewise duplicated (:27and:14).Fix: one
uregin a shared module (observation_from_measurement.pyis the natural home; its docstring already says it holds what the two share).Resolution:
ureg,DEFAULT_LMAX,XS_UNIT,RUTHERFORD_UNITandMB_PER_Blive inobservation_from_measurement.py(now exported fromrxmc); both observation modules import and re-export them.test_reaction_observation.py::TestSharedUnits.
Inconsistencies between the two sampler front ends#
CalibrationConfig and Walker are meant to be two drivers over the same
posterior. They are not.
5. Walker.log_posterior evaluates the likelihood when the prior is -inf#
Where:
src/rxmc/walker.py:140-143versussrc/rxmc/config.py:405-411.What: the config path short-circuits on a non-finite prior and skips the forward model. The walker path always calls
Evidence.log_likelihoodfirst. The Gibbs conditional has the same split:config.conditional_posterior(config.py:579-583) short-circuits, the inline closure atwalker.py:130-132does not.Why it matters: out-of-bounds proposals cost a full reaction-model solve in the walker. With bounds also enforced inside the kernels this is a performance bug, not a correctness bug, but it is a silent divergence.
Resolution:
Walker.log_posteriorand the Gibbs closure evaluate the prior first and return-infwithout touching the likelihood.test_sampler.py::TestWalkerPosterior::test_*_skips_likelihood_when_prior_neg_inf.
6. Tempering exists only on the config path#
Where:
config.py:245-252, 384, 583; no counterpart inwalker.py.What:
likelihood_scalingscales the likelihood (and the Gibbs conditionals) inCalibrationConfig.Walkerhas no such knob; the only way to temper isEvidence(weights=...).examples/overconfidence.ipynbdemonstrates both and prints a check that they agree.Why it matters: two names for one concept, with one of them reachable from only one driver.
Resolution:
Walker(..., likelihood_scaling=)added with the same semantics as the config (scales the likelihood in the model block and the Gibbs conditionals, never the prior).test_sampler.py::TestWalkerPosterior::test_*_applies_likelihood_scaling*.
7. List-of-scipy priors are accepted by one driver and rejected by the other#
Where:
config.py:133-135, 153-155, 184-185(list branches inParameterConfig) versusparam_sampling.py:66-70(_validate_objectrequiresprior.logpdf) andwalker.py:131, 160(callssampler.prior.logpdfdirectly).What:
ParameterConfigspecial-cases a plain list of frozen scipy distributions in three places. ASamplerbuilt with the same list fails at construction because a list has nologpdf.Why it matters: the prior protocol is documented as one thing and implemented as two.
IndependentPrioralready exists to wrap a list;ParameterConfigcould wrap in__init__and delete all three branches.Resolution:
rxmc.priors.as_priorwraps a list/tuple inIndependentPrior; bothParameterConfig.__init__andSampler.__init__call it, and the four list branches inParameterConfigare gone. Behaviour change:x0for a list prior is now seeded (IndependentPriordefault seed) instead of drawing from numpy’s global state, andconfig.priorreturns the wrapper. Tests intest_config.py,test_sampler.py,test_priors.py.
8. ParameterConfig._infer_dim misreads priors whose mean is a method#
Where:
src/rxmc/config.py:92-98.What: if the prior has no integer
dim, the fallback isint(np.size(dist.mean)). For any object whosemeanis a method this is1, regardless of the true dimension.Why it matters: a custom multi-dimensional prior exposing
mean()is reported as one-dimensional and rejected atconfig.py:109-113with a misleading message. Frozen scipy multivariate distributions happen to exposemeanas an array, which is why the tests pass.Fix: call
meanif callable, or requiredimand drop the guess.Resolution: an integer
dimwins; otherwisemeanis called when it is a method.test_config.py::test_infer_dim_calls_mean_method.
Fragile, not wrong#
These are not bugs today but each is one refactor away from becoming one.
10. Model and observation compatibility is checked by string, or not at all#
elastic_diffxs_model.py:137, 174compareobservation.quantitytoself.quantity.ias_pn_model.py:135, 159reach straight forobservation.constraint_workspacewith no check. Pairing an elastic model with an IAS observation fails inside jitr with a shape error.Resolution: each model checks
isinstanceagainst its observation class first inevaluateandvisualizable_model_predictionand raises a namedValueError(a string check cannot work: both observations reportquantity == "dXS/dA").test_reaction_models.py::TestObservationTypeChecks.
12. The burn-in loop in Walker.walk duplicates the main loop#
walker.py:207-221versus:229-241: identical bodies apart fromburn=Trueand the progress string. Any change to one must be mirrored.Resolution: one
_run_batch(steps, burn)sweep plus_batch_message; the burn-in line prints no acceptance fraction because nothing is recorded during burn-in.test_sampler.py::TestWalkerPosterior::test_burn_message_has_no_acceptance_fraction.
13. prior_transform clips the unit cube only at the top level#
config.py:492-506clipsuto[eps, 1-eps];ParameterConfig.prior_transformandIndependentPrior.prior_transform(priors.py:273-277) do not. Calling either directly with an exact0.0or1.0returns±inf. (TruncatedNormalPrioris finite at the boundary by construction.)Resolution:
rxmc.priors.clip_unit_cubeis applied in all four transforms.test_priors.py::TestUnitCubeClipping,test_config.py::test_prior_transform_boundary_finite.
Already fixed on this branch#
The
_rowsrow-count check inmodel_comparison.pywas reported by an earlier read as unable to fire. Atb8bc7d8it is called withoutnfor the model samples and withnfor the covariance samples (:138, 143), which is correct.