Skip to content

Commit cddd16a

Browse files
Infer NumPy Generator.normal result shapes (#5619)
Summary: Adds shape-aware overloads for `Generator.normal`. With `size`, the result has exactly that shape. Without `size`, it has the broadcast shape of array `loc` and `scale`, and mismatched shapes are reported. NumPy's own overloads stay as fallbacks for lists. - [x] I am a human, and I fully understand the code I am submitting. Pull Request resolved: #5619 Test Plan: Added tests to `tensor-shapes/pyrefly-numpy-stubs/test/test_random.py`. Ran: - `python3 tensor-shapes/pyrefly-numpy-stubs/run_pyrefly.py` - `"$TENSOR_SHAPES_VENV/bin/python" tensor-shapes/pyrefly-numpy-stubs/run_runtime_tests.py` - `./test.py --no-test --no-tensor-shapes --no-conformance --no-jsonschema --no-extension` (formatting and lint) Reviewed By: stroxler Differential Revision: D131296292 fbshipit-source-id: 504ae9742dcaf919d7ae30d2a213cf358f591671
1 parent b68f822 commit cddd16a

2 files changed

Lines changed: 152 additions & 3 deletions

File tree

‎tensor-shapes/pyrefly-numpy-stubs/numpy-stubs/random/__init__.pyi‎

Lines changed: 93 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,27 @@
33
# This source code is licensed under the MIT license found in the
44
# LICENSE file in the root directory of this source tree.
55

6-
from typing import Literal, overload
6+
from typing import Any, Literal, overload
77

88
from numpy._shapes import (
99
multivariate_normal_scalar_size_shape,
1010
multivariate_normal_shape,
1111
)
12-
from numpy._typing import _ArrayLikeInt_co, _ShapeLike, ArrayLike
12+
from numpy._typing import (
13+
_ArrayLikeFloat_co,
14+
_ArrayLikeInt_co,
15+
_FloatLike_co,
16+
_ShapeLike,
17+
ArrayLike,
18+
)
1319
from numpy.random._generator import (
1420
_ArrayF32,
1521
_ArrayF64,
1622
_DTypeLikeF32,
1723
_DTypeLikeF64,
1824
_DTypeLikeFloat,
1925
_MethodExp,
26+
_NDArrayLikeFloat,
2027
Generator as _NumpyGenerator,
2128
)
2229

@@ -90,7 +97,7 @@ from numpy.random.mtrand import (
9097
weibull as weibull,
9198
zipf as zipf,
9299
)
93-
from shape_extensions import Int, IntTuple, IntVar
100+
from shape_extensions import broadcast, Int, IntTuple, IntVar
94101

95102
from .. import dtype, float32, float64, ndarray
96103

@@ -336,6 +343,89 @@ class Generator(_NumpyGenerator):
336343
*,
337344
out: ArrayT,
338345
) -> ArrayT: ...
346+
@overload
347+
def normal(
348+
self, /, loc: _FloatLike_co = 0.0, scale: _FloatLike_co = 1.0, size: None = None
349+
) -> float: ...
350+
# With `size`, the result has exactly that shape, and NumPy raises if `loc` or
351+
# `scale` does not broadcast to it.
352+
@overload
353+
def normal[N: IntVar](
354+
self, /, loc: _ArrayLikeFloat_co, scale: _ArrayLikeFloat_co, size: Int[N]
355+
) -> ndarray[[N], dtype[float64]]: ...
356+
@overload
357+
def normal[Shape: IntTuple](
358+
self, /, loc: _ArrayLikeFloat_co, scale: _ArrayLikeFloat_co, size: Shape
359+
) -> ndarray[Shape, dtype[float64]]: ...
360+
@overload
361+
def normal[N: IntVar](
362+
self,
363+
/,
364+
loc: _ArrayLikeFloat_co = 0.0,
365+
scale: _ArrayLikeFloat_co = 1.0,
366+
*,
367+
size: Int[N],
368+
) -> ndarray[[N], dtype[float64]]: ...
369+
@overload
370+
def normal[Shape: IntTuple](
371+
self,
372+
/,
373+
loc: _ArrayLikeFloat_co = 0.0,
374+
scale: _ArrayLikeFloat_co = 1.0,
375+
*,
376+
size: Shape,
377+
) -> ndarray[Shape, dtype[float64]]: ...
378+
# Without `size`, the result has the broadcast shape of `loc` and `scale`.
379+
@overload
380+
def normal[LocShape: IntTuple = [], ScaleShape: IntTuple = []](
381+
self,
382+
/,
383+
loc: ndarray[LocShape] | float = 0.0,
384+
scale: ndarray[ScaleShape] | float = 1.0,
385+
size: None = None,
386+
) -> ndarray[broadcast(LocShape, ScaleShape), dtype[float64]]: ...
387+
@overload
388+
def normal(
389+
self, /, loc: _ArrayLikeFloat_co, scale: _ArrayLikeFloat_co, size: _ShapeLike
390+
) -> _ArrayF64: ...
391+
@overload
392+
def normal(
393+
self, /, loc: _ArrayLikeFloat_co, scale: _NDArrayLikeFloat, size: None
394+
) -> _ArrayF64: ...
395+
@overload
396+
def normal(
397+
self,
398+
/,
399+
loc: _ArrayLikeFloat_co = 0.0,
400+
scale: _ArrayLikeFloat_co = 1.0,
401+
*,
402+
size: _ShapeLike,
403+
) -> _ArrayF64: ...
404+
@overload
405+
def normal(
406+
self,
407+
/,
408+
loc: _ArrayLikeFloat_co = 0.0,
409+
*,
410+
scale: _NDArrayLikeFloat,
411+
size: None = None,
412+
) -> _ArrayF64: ...
413+
@overload
414+
def normal(
415+
self,
416+
/,
417+
loc: _NDArrayLikeFloat,
418+
scale: _ArrayLikeFloat_co = 1.0,
419+
size: None = None,
420+
) -> _ArrayF64: ...
421+
@overload
422+
def normal(
423+
self,
424+
/,
425+
loc: _ArrayLikeFloat_co = 0.0,
426+
scale: _ArrayLikeFloat_co = 1.0,
427+
size: None = None,
428+
) -> _ArrayF64 | Any: ...
339429

340430
def default_rng(
341431
seed: _ArrayLikeInt_co

‎tensor-shapes/pyrefly-numpy-stubs/test/test_random.py‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,65 @@ def check_standard_normal_and_standard_exponential_symbolic_size[N: IntVar, M: I
9494
)
9595

9696

97+
def check_normal_returns_float_without_size() -> None:
98+
rng = np.random.default_rng(0)
99+
assert_type(rng.normal(), float)
100+
assert_type(rng.normal(1.0, 2.0), float)
101+
102+
103+
def test_normal_int_size() -> None:
104+
rng = np.random.default_rng(0)
105+
result = rng.normal(size=5)
106+
assert_type(result, np.ndarray[[5], np.dtype[np.float64]])
107+
assert_shape(result.shape, (5,))
108+
assert_shape(rng.normal(0.0, 1.0, 5).shape, (5,))
109+
110+
111+
def test_normal_tuple_size() -> None:
112+
rng = np.random.default_rng(0)
113+
result = rng.normal(0.0, 1.0, (2, 3))
114+
assert_type(result, np.ndarray[[2, 3], np.dtype[np.float64]])
115+
assert_shape(result.shape, (2, 3))
116+
assert_shape(rng.normal(size=(2, 3)).shape, (2, 3))
117+
118+
119+
def test_normal_array_parameters_with_size() -> None:
120+
rng = np.random.default_rng(0)
121+
result = rng.normal(np.zeros(3), 1.0, size=(2, 3))
122+
assert_type(result, np.ndarray[[2, 3], np.dtype[np.float64]])
123+
assert_shape(result.shape, (2, 3))
124+
assert_shape(rng.normal(np.zeros((10, 1)), size=(10, 5)).shape, (10, 5))
125+
126+
127+
def test_normal_array_parameters_without_size() -> None:
128+
rng = np.random.default_rng(0)
129+
result = rng.normal(np.zeros(3), np.ones((2, 1)))
130+
assert_type(result, np.ndarray[[2, 3], np.dtype[np.float64]])
131+
assert_shape(result.shape, (2, 3))
132+
assert_shape(rng.normal(np.zeros(3)).shape, (3,))
133+
assert_shape(rng.normal(scale=np.ones((2, 1))).shape, (2, 1))
134+
135+
136+
def test_normal_list_size_and_parameters() -> None:
137+
rng = np.random.default_rng(0)
138+
assert_shape(rng.normal(size=[2, 3]).shape, IntTuple, runtime=(2, 3))
139+
assert_shape(rng.normal([0.0, 1.0, 2.0]).shape, IntTuple, runtime=(3,))
140+
141+
142+
def test_normal_mismatched_parameters() -> None:
143+
rng = np.random.default_rng(0)
144+
with assert_raises(ValueError):
145+
rng.normal(np.zeros(3), np.ones(2)) # E: Cannot broadcast
146+
147+
148+
def check_normal_symbolic_shapes[N: IntVar, M: IntVar](
149+
n: Int[N], loc: np.ndarray[[N, 1]], scale: np.ndarray[[M]]
150+
) -> None:
151+
rng = np.random.default_rng(0)
152+
assert_type(rng.normal(size=n), np.ndarray[[N], np.dtype[np.float64]])
153+
assert_type(rng.normal(loc, scale), np.ndarray[[N, M], np.dtype[np.float64]])
154+
155+
97156
def test_multivariate_normal_without_size() -> None:
98157
rng = np.random.default_rng(0)
99158
result = rng.multivariate_normal(np.zeros(3), np.eye(3))

0 commit comments

Comments
 (0)