Skip to content

fix: refuse fit inputs that leave no observation standing - #40

Merged
tnagler merged 1 commit into
mainfrom
fix/validate-weights
Sep 7, 2026
Merged

tnagler merged 1 commit into
mainfrom
fix/validate-weights

Conversation

@tvatter

@tvatter tvatter commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Why

Kde1d::fit terminates the process — a SIGSEGV, not an exception — on three
inputs a caller can reach directly:

fit.fit(x, Eigen::VectorXd::Constant(n, NAN));  // all-NaN weights
fit.fit(x, Eigen::VectorXd::Zero(n));           // all-zero weights
fit.fit(Eigen::VectorXd::Constant(n, NAN));     // all-NaN observations

tools::remove_nans documents three drop markers — a NaN observation, a
NaN weight, and a zero weight. fit then does w /= w.mean() and builds the
interpolation grid from the survivors. When every row carries a marker there are
none: the rescaling divides by zero and the grid is built from an empty sample.

Two weight values are not markers at all and were passing straight through:

  • an infinite weight normalizes every other one to zero;
  • a negative weight gives a density that is not the weighted one, silently.

What changed

  • check_inputs gains the infinite and negative weight checks. It is reached
    with a non-empty weights only from fit — the pdf/cdf/quantile paths
    call it with observations alone — so evaluation is unaffected.
  • fit checks for survivors immediately after remove_nans, which is where
    the answer already is, rather than re-deriving it.
  • The @throws list on fit and its "observations that are NaN are dropped"
    note now say that a NaN or zero weight drops its row too.

Partial markers stay legal and are untouched. A NaN weight and a zero
weight each drop their own row and nothing else, and they agree with each
other — the existing "works with weights" section relies on the first and the
existing boundary-expert test on the second. The new test pins that agreement
next to the refusals, so a future tightening cannot quietly turn a drop into an
error.

Testing

kde1d-unit-test: 578 assertions in 32 cases, all passing.
kde1d-invariant-test: 721 assertions in 5 cases, all passing.
No numerical expectation moved; the diff only adds refusals on inputs that
previously crashed or silently mis-weighted.

clang-format reports the same 129 pre-existing violations before and after
this change (a formatter-version skew in the repo), so these additions add
none.

Context

Found while auditing the Python bindings in
vinecopulib/pyvinecopulib#326, where this surfaced as a segfault from
pyvinecopulib.core.Kde1d.fit(x, weights) and from TorchKde1d.fit. That PR
carries a binding-level guard in the meantime, since a crash takes the
interpreter down; it can be dropped once this lands and the submodule pin
moves.

`remove_nans` treats three things as drop markers: a `NaN` observation, a
`NaN` weight, and a zero weight. `fit` then rescales by `w.mean()` and builds
the interpolation grid from what is left. When every row carries a marker
nothing is left, so the rescaling divides by zero and the grid is built from an
empty sample — which terminates the process rather than raising. Reachable from
`fit(x, weights)` with all-`NaN` or all-zero weights, and from `fit(x)` alone
with all-`NaN` observations.

Two weight values are not markers and were passing through: an infinite weight
normalizes every other one to zero, and a negative weight yields a density that
is not the weighted one, silently.

So `check_inputs` gains the two weight checks — it is reached with weights only
from `fit`, the evaluation paths calling it with observations alone — and `fit`
checks for survivors immediately after `remove_nans`, which is where the answer
already is. Partial markers stay legal and are unaffected: a `NaN` weight and a
zero weight still drop their own row and agree with each other, which the new
test pins alongside the refusals.

Found while auditing the Python bindings in vinecopulib/pyvinecopulib#326,
where the crash surfaced as a segfault in `Kde1d.fit(x, weights)`.
@tnagler
tnagler merged commit 1523a10 into main Sep 7, 2026
6 checks passed
@tnagler
tnagler deleted the fix/validate-weights branch September 7, 2026 12:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants