Skip to content

fix: validate the sampling method and reject an empty mean in multinormal_cholesky - #189

Merged
antonwolfy merged 5 commits into
masterfrom
fix/method-validation-and-empty-mean
Oct 15, 2026
Merged

antonwolfy merged 5 commits into
masterfrom
fix/method-validation-and-empty-mean

Conversation

@antonwolfy

@antonwolfy antonwolfy commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

Follow-up to the review of #187, which turned up three unrelated behaviours in the Python layer. This PR does not depend on #187 and does not touch the same code.

Problem

  1. Integer-like method ids were misrouted. choose_method returned any value that compared equal to a method id, e.g. np.int64(0), np.int64(2), False or 1.0. The callers then test the result with is, so those values fell through to the final else branch, which is BoxMuller. For example multinormal_cholesky(..., method=np.int64(2)) silently ran BoxMuller instead of BoxMuller2, and method=False ran BoxMuller instead of the default.
  2. Unsupported methods were silently replaced by the default. A misspelled name, None, or a method the function does not implement (e.g. "Box-Muller2" for lognormal, which only supports ICDF and BoxMuller) got the default method without any notice.
  3. multinormal_cholesky with an empty mean raised ZeroDivisionError (PyArray_SIZE(...) // dim with dim == 0). NumPy's multivariate_normal raises ValueError for the same input.

Changes

  • choose_method (mkl_random/mklrand.pyx) now accepts strings from the function's alias table and integers (including NumPy integers) matching one of its ids, and returns the module-level constant, so the existing is comparisons keep working. A constant passed as is, which includes every default argument, is returned before any type checks, so the default path is not slower than on master.
  • bool, float and any other unsupported value emit a UserWarning that lists the methods the called function supports, then fall back to that function's default method, in the same style as an unrecognized brng. For example: The sampling method 'Box-Muller2' is not supported. Supported methods are "ICDF", "BoxMuller"; "ICDF" will be used instead. Results for those inputs are unchanged apart from the new warning, except where the old code misrouted them (item 1).
  • ICDF, BOXMULLER, BOXMULLER2, POISNORM and PTPE are now exported from mkl_random, since the method docstrings already refer to them as mkl_random.ICDF etc. Until now that raised AttributeError.
  • The multinomial docstring no longer documents a method parameter, which that function does not take. The other method docstrings get a missing closing quote ('ICDF',).
  • multinormal_cholesky raises ValueError("mean must have at least one element") for an empty mean. oneMKL's vdRngGaussianMV also requires dimen >= 1.
  • test_randomdist_lognormal asked lognormal for "Box-Muller2", which it does not support and silently treated as ICDF. It now asserts the warning, including the supported methods it lists; the expected values are unchanged.
  • CHANGELOG.md: one Added, one Changed and two Fixed entries.

Behaviour change to be aware of

Code that passes an unsupported or misspelled method still gets the same numbers as before, but now sees a UserWarning. I chose a warning over an exception to stay backward compatible, consistent with how an unrecognized brng is handled (gh-177). Raising ValueError instead would be a small change in choose_method if that is preferred.

Testing

New tests in mkl_random/tests/test_random.py:

  • test_multinormal_cholesky_empty_mean: both the 2-D and the packed 1-D empty Cholesky factor raise ValueError.
  • test_multinormal_cholesky_integer_method: for each of ICDF, BoxMuller, BoxMuller2, the ids passed as int, np.int64 and np.uint8 give exactly the same draws as the name, and emit no warning.
  • test_unsupported_method_warns_and_uses_default: "bogus", 7, -1, 1.0, None, False, True and [1] warn and give the default (ICDF) result.
  • test_unsupported_method_uses_each_functions_default: the fallback is the called function's own default (poisson falls back to POISNORM, not ICDF), and the warning names it as the method used.
  • test_method_tokens_are_exported: each exported id is in mkl_random.__all__, emits no warning, and gives the same draws as its name.

Unlike the tests in #187, these fail on master: running them against a master build, 20 of the 25 cases fail (the other 5 are cases master already handled, such as np.int64(1), which happened to land on BoxMuller). On this branch all 25 pass.

The full suite passes (363 passed, 24 skipped), also with -W error::UserWarning, which confirms that no other call in the test suite triggers the new warning.

Timing of calls with the default method (best of 5 interleaved runs, one pinned CPU):

master this PR
choose_method(ICDF) 38 ns 33 ns
standard_normal() 288 ns 261 ns
poisson(3.0) 506 ns 503 ns

…rmal_cholesky

choose_method returned any value that compared equal to a method id, so
np.int64(0), np.int64(2), False and 1.0 passed through unchanged. The
callers then compare the result with `is`, so those values were routed to
the final else branch (BoxMuller) instead of the method they named.
Unrecognized values, such as a misspelled name, were mapped to the
default method without any notice.

- choose_method now accepts strings from the alias table and integers
  (including NumPy integers) matching an id, and returns the module-level
  constant so the `is` comparisons keep working.
- bool and float, which compare equal to the ids but are not valid
  specifications, and any other unrecognized value now emit a
  UserWarning and fall back to the function's default, like an
  unrecognized brng does.
- multinormal_cholesky raises ValueError for an empty mean instead of
  ZeroDivisionError.
- test_randomdist_lognormal asked lognormal for "Box-Muller2", which it
  does not support and silently treated as ICDF; it now asserts the
  warning.
…tests

Place them next to test_normal_family_array_methods so they do not touch
the same lines as other pull requests adding multinormal_cholesky tests.
@antonwolfy antonwolfy self-assigned this Oct 6, 2026
@antonwolfy antonwolfy added this to the 1.6.0 release milestone Oct 6, 2026
Comment thread mkl_random/mklrand.pyx Outdated
Comment thread mkl_random/mklrand.pyx Outdated
Comment thread mkl_random/mklrand.pyx
Comment thread mkl_random/mklrand.pyx
antonwolfy and others added 2 commits October 14, 2026 15:42
- choose_method returns a constant passed by identity before any type
  checks, so the default method no longer pays for isinstance and
  operator.index on every call. Strings and integers are both resolved
  through the same loop over mlist.
- The warning says the method is "not supported" and lists the methods
  the called function supports. It looks names up with .get, so a
  constant missing from _method_names no longer turns the warning into
  KeyError.
- Export ICDF, BOXMULLER, BOXMULLER2, POISNORM and PTPE from mkl_random,
  as the method docstrings already document them as mkl_random.ICDF etc.
- Drop the method entry from the multinomial docstring, which takes no
  method, and add the missing closing quote in the other ones.
- Tests: match the new wording, check the per-function supported list
  for lognormal and the default named for poisson, and cover the
  exported ids.

@vlad-perevezentsev vlad-perevezentsev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No more comments from my side
LGTM
Thank you @antonwolfy

@vlad-perevezentsev
vlad-perevezentsev self-requested a review October 15, 2026 12:02
@antonwolfy
antonwolfy merged commit df32f30 into master Oct 15, 2026
142 of 143 checks passed
@antonwolfy
antonwolfy deleted the fix/method-validation-and-empty-mean branch October 15, 2026 12:19
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