From 46862c47c695c797beb361cc66a5ae4150491704 Mon Sep 17 00:00:00 2001 From: yimoj <130720840+yimoj@users.noreply.github.com> Date: Thu, 20 Jul 2023 12:26:22 +0800 Subject: [PATCH] improve random bitgenerator code coverage (#990) * improve random bitgenerator code coverage * add error description to zipf failure * Replace misused assert in generator with appropriate exceptions --- cunumeric/random/generator.py | 12 +- tests/integration/test_random_advanced.py | 102 +++++++++++++- tests/integration/test_random_beta.py | 32 +++++ tests/integration/test_random_bitgenerator.py | 125 ++++++++++++++++++ tests/integration/test_random_creation.py | 32 ++++- tests/integration/test_random_gamma.py | 24 ++++ .../test_random_straightforward.py | 41 ++++++ 7 files changed, 357 insertions(+), 11 deletions(-) diff --git a/cunumeric/random/generator.py b/cunumeric/random/generator.py index 6a00269dd..7145de662 100644 --- a/cunumeric/random/generator.py +++ b/cunumeric/random/generator.py @@ -292,9 +292,15 @@ def random( out: Union[ndarray, None] = None, ) -> ndarray: if out is not None: - if size is not None: - assert out.shape == size - assert out.dtype == dtype + if size is not None and out.shape != size: + raise ValueError( + "size must match out.shape when used together" + ) + if out.dtype != dtype: + raise TypeError( + "Supplied output array has the wrong type. " + "Expected {}, got {}".format(dtype, out.dtype) + ) return self.bit_generator.random(size, dtype, out) def rayleigh( diff --git a/tests/integration/test_random_advanced.py b/tests/integration/test_random_advanced.py index ab8031344..c5ae2bc42 100644 --- a/tests/integration/test_random_advanced.py +++ b/tests/integration/test_random_advanced.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # +import os import sys import numpy as np @@ -20,6 +21,7 @@ import cunumeric as num +LEGATE_TEST = os.environ.get("LEGATE_TEST", None) == "1" if sys.platform == "darwin": pytestmark = pytest.mark.skip() BITGENERATOR_ARGS = [] @@ -81,7 +83,12 @@ def test_vonmises_float64(t): @pytest.mark.parametrize("t", BITGENERATOR_ARGS, ids=str) -def test_hypergeometric(t): +@pytest.mark.parametrize( + "ngood, nbad, nsample", + ((60, 440, 200), (20.0, 77, 1), ((3, 5, 7), 6, 22)), + ids=str, +) +def test_hypergeometric(t, ngood, nbad, nsample): bitgen = t(seed=42) gen = num.random.Generator(bitgen) N = 500 @@ -110,12 +117,24 @@ def test_geometric(t): @pytest.mark.parametrize("t", BITGENERATOR_ARGS, ids=str) -def test_zipf(t): +@pytest.mark.parametrize( + "s", + ( + 7.5, + pytest.param( + (1.2, 3.1415), + marks=pytest.mark.xfail, + # NumPy returns 1-dim array + # cuNumeric raises TypeError: float() argument must be a string + # or a real number, not 'tuple' + ), + ), + ids=str, +) +def test_zipf(t, s): bitgen = t(seed=42) gen = num.random.Generator(bitgen) - s = 7.5 a = gen.zipf(a=s, size=(1024 * 1024,), dtype=np.uint32) - a = np.random.zipf(s, 1024 * 1024) ref_a = np.random.zipf(s, 1024 * 1024) theo_mean = np.average(ref_a) theo_std = np.std(ref_a) @@ -173,6 +192,81 @@ def test_negative_binomial(t): assert_distribution(a, theo_mean, theo_std) +FUNC_ARGS = ( + ("binomial", (15, 0.666)), + ("negative_binomial", (15, 0.666)), + ("geometric", (0.707,)), + ("hypergeometric", (60, 440, 200)), + ("standard_t", (3.1415,)), + ("vonmises", (1.414, 3.1415)), + ("wald", (1.414, 3.1415)), + ("zipf", (7.5,)), +) + + +@pytest.mark.parametrize("func, args", FUNC_ARGS, ids=str) +@pytest.mark.parametrize("size", ((2048 * 2048), (4096,), 25535), ids=str) +def test_random_sizes(func, args, size): + seed = 42 + gen_np = np.random.Generator(np.random.PCG64(seed=seed)) + gen_num = num.random.Generator(num.random.XORWOW(seed=seed)) + a_np = getattr(gen_np, func)(*args, size=size) + a_num = getattr(gen_num, func)(*args, size=size) + assert a_np.shape == a_num.shape + + +@pytest.mark.xfail +@pytest.mark.parametrize("func, args", FUNC_ARGS, ids=str) +def test_random_size_none(func, args): + seed = 42 + gen_np = np.random.Generator(np.random.PCG64(seed=seed)) + gen_num = num.random.Generator(num.random.XORWOW(seed=seed)) + a_np = getattr(gen_np, func)(*args, size=None) + a_num = getattr(gen_num, func)(*args, size=None) + # cuNumeric returns singleton array + # NumPy returns scalar + assert np.ndim(a_np) == np.ndim(a_num) + + +class TestRandomErrors: + # cuNumeric zipf hangs on the invalid args when LEGATE_TEST=1 + @pytest.mark.skipif(LEGATE_TEST, reason="Test hang when LEGATE_TEST=1") + @pytest.mark.parametrize( + "dist, expected_exc", + ( + (0.77, ValueError), + (-5, ValueError), + (None, TypeError), + ((1, 5, 3), ValueError), + ), + ids=str, + ) + def test_zipf_invalid_dist(self, dist, expected_exc): + seed = 42 + gen_np = np.random.Generator(np.random.PCG64(seed=seed)) + gen_num = num.random.Generator(num.random.XORWOW(seed=seed)) + with pytest.raises(expected_exc): + gen_np.zipf(dist) + with pytest.raises(expected_exc): + gen_num.zipf(dist) + + @pytest.mark.skipif(LEGATE_TEST, reason="Test hang when LEGATE_TEST=1") + @pytest.mark.parametrize( + "ngood, nbad, nsample", + ((200, 60, 500), ((1, 5, 7), 6, 22)), + ids=str, + ) + def test_hypergeometric_invalid_args(self, ngood, nbad, nsample): + expected_exc = ValueError + seed = 42 + gen_np = np.random.Generator(np.random.PCG64(seed=seed)) + gen_num = num.random.Generator(num.random.XORWOW(seed=seed)) + with pytest.raises(expected_exc): + gen_np.hypergeometric(ngood, nbad, nsample) + with pytest.raises(expected_exc): + gen_num.hypergeometric(ngood, nbad, nsample) + + if __name__ == "__main__": import sys diff --git a/tests/integration/test_random_beta.py b/tests/integration/test_random_beta.py index dc451b42e..9d922f792 100644 --- a/tests/integration/test_random_beta.py +++ b/tests/integration/test_random_beta.py @@ -133,6 +133,38 @@ def test_noncentral_f_float64(t): assert_distribution(a, theo_mean, theo_std) +FUNC_ARGS = ( + ("beta", (2.0, 5.0)), + ("f", (1.0, 48.0)), + ("logseries", (0.66,)), + ("noncentral_f", (1.0, 48.0, 1.414)), +) + + +@pytest.mark.parametrize("func, args", FUNC_ARGS, ids=str) +@pytest.mark.parametrize("size", ((2048 * 2048), (4096,), 25535), ids=str) +def test_beta_sizes(func, args, size): + seed = 42 + gen_np = np.random.Generator(np.random.PCG64(seed=seed)) + gen_num = num.random.Generator(num.random.XORWOW(seed=seed)) + a_np = getattr(gen_np, func)(*args, size=size) + a_num = getattr(gen_num, func)(*args, size=size) + assert a_np.shape == a_num.shape + + +@pytest.mark.xfail +@pytest.mark.parametrize("func, args", FUNC_ARGS, ids=str) +def test_beta_size_none(func, args): + seed = 42 + gen_np = np.random.Generator(np.random.PCG64(seed=seed)) + gen_num = num.random.Generator(num.random.XORWOW(seed=seed)) + a_np = getattr(gen_np, func)(*args, size=None) + a_num = getattr(gen_num, func)(*args, size=None) + # cuNumeric returns singleton array + # NumPy returns scalar + assert np.ndim(a_np) == np.ndim(a_num) + + if __name__ == "__main__": import sys diff --git a/tests/integration/test_random_bitgenerator.py b/tests/integration/test_random_bitgenerator.py index 31155d63f..58d82631e 100644 --- a/tests/integration/test_random_bitgenerator.py +++ b/tests/integration/test_random_bitgenerator.py @@ -48,6 +48,28 @@ def test_bitgenerator_type(t): print(f"DONE for type = {t}") +@pytest.mark.parametrize("size", ((2048 * 2048), (4096,), 25535), ids=str) +def test_bitgenerator_size(size): + seed = 42 + gen_np = np.random.PCG64(seed=seed) + gen_num = num.random.XORWOW(seed=seed) + a_np = gen_np.random_raw(size=size) + a_num = gen_num.random_raw(shape=size) + assert a_np.shape == a_num.shape + + +@pytest.mark.xfail +def test_bitgenerator_size_none(): + seed = 42 + gen_np = np.random.PCG64(seed=seed) + gen_num = num.random.XORWOW(seed=seed) + a_np = gen_np.random_raw(size=None) + a_num = gen_num.random_raw(shape=None) + # cuNumeric returns singleton array + # NumPy returns scalar + assert np.ndim(a_np) == np.ndim(a_num) + + @pytest.mark.parametrize("t", BITGENERATOR_ARGS, ids=str) def test_force_build(t): t(42, True) @@ -83,6 +105,15 @@ def test_integers_int16(t): print(f"1024*1024 sum = {a.sum()}") +@pytest.mark.parametrize("t", BITGENERATOR_ARGS, ids=str) +def test_integers_endpoint(t): + high = 10 + bitgen = t(seed=42) + gen = num.random.Generator(bitgen) + a = gen.integers(high, size=100, dtype=np.int16, endpoint=True) + assert np.max(a) == high + + @pytest.mark.parametrize("t", BITGENERATOR_ARGS, ids=str) def test_random_float32(t): # top-level random function has a different signature @@ -105,6 +136,20 @@ def test_random_float64(t): assert_distribution(a, 0.5, num.sqrt(1.0 / 12.0)) +def test_random_out_stats(): + seed = 42 + dtype = np.float32 + out_shape = (2, 3, 1) + gen_np = np.random.Generator(np.random.PCG64(seed=seed)) + gen_num = num.random.Generator(num.random.XORWOW(seed=seed)) + out_np = np.empty(out_shape, dtype=dtype) + out_num = num.empty(out_shape, dtype=dtype) + gen_np.random(out=out_np, dtype=dtype) + gen_num.random(out=out_num, dtype=dtype) + assert out_np.shape == out_num.shape + assert out_np.dtype == out_np.dtype + + @pytest.mark.parametrize("t", BITGENERATOR_ARGS, ids=str) def test_lognormal_float32(t): bitgen = t(seed=42) @@ -192,6 +237,86 @@ def test_poisson(t): assert_distribution(a, theo_mean, theo_std) +FUNCS = ("poisson", "normal", "lognormal", "uniform") + + +@pytest.mark.parametrize("func", FUNCS, ids=str) +@pytest.mark.parametrize("size", ((2048 * 2048), (4096,), 25535), ids=str) +def test_random_sizes(func, size): + seed = 42 + gen_np = np.random.Generator(np.random.PCG64(seed=seed)) + gen_num = num.random.Generator(num.random.XORWOW(seed=seed)) + a_np = getattr(gen_np, func)(size=size) + a_num = getattr(gen_num, func)(size=size) + assert a_np.shape == a_num.shape + + +@pytest.mark.xfail +@pytest.mark.parametrize("func", FUNCS, ids=str) +def test_random_size_none(func): + seed = 42 + gen_np = np.random.Generator(np.random.PCG64(seed=seed)) + gen_num = num.random.Generator(num.random.XORWOW(seed=seed)) + a_np = getattr(gen_np, func)(size=None) + a_num = getattr(gen_num, func)(size=None) + # cuNumeric returns singleton array + # NumPy returns scalar + assert np.ndim(a_np) == np.ndim(a_num) + + +class TestBitGeneratorErrors: + def test_init_bitgenerator(self): + expected_exc = NotImplementedError + with pytest.raises(expected_exc): + np.random.BitGenerator() + with pytest.raises(expected_exc): + num.random.BitGenerator() + + @pytest.mark.xfail + @pytest.mark.parametrize("dtype", (np.int32, np.float128, str)) + def test_random_invalid_dtype(self, dtype): + expected_exc = TypeError + seed = 42 + gen_np = np.random.Generator(np.random.PCG64(seed=seed)) + gen_num = num.random.Generator(num.random.XORWOW(seed=seed)) + with pytest.raises(expected_exc): + gen_np.random(size=(1024 * 1024,), dtype=dtype) + # TypeError: Unsupported dtype dtype('int32') for random + with pytest.raises(expected_exc): + gen_num.random(size=(1024 * 1024,), dtype=dtype) + # NotImplementedError: type for random.uniform has to be float64 + # or float32 + + def test_random_out_dtype_mismatch(self): + expected_exc = TypeError + seed = 42 + dtype = np.float32 + out_shape = (3, 2, 3) + out_dtype = np.float64 + gen_np = np.random.Generator(np.random.PCG64(seed=seed)) + gen_num = num.random.Generator(num.random.XORWOW(seed=seed)) + out_np = np.empty(out_shape, dtype=out_dtype) + out_num = num.empty(out_shape, dtype=out_dtype) + with pytest.raises(expected_exc): + gen_np.random(out=out_np, dtype=dtype) + with pytest.raises(expected_exc): + gen_num.random(out=out_num, dtype=dtype) + + def test_random_out_shape_mismatch(self): + expected_exc = ValueError + seed = 42 + size = (1024 * 1024,) + out_shape = (3, 2, 3) + gen_np = np.random.Generator(np.random.PCG64(seed=seed)) + gen_num = num.random.Generator(num.random.XORWOW(seed=seed)) + out_np = np.empty(out_shape) + out_num = num.empty(out_shape) + with pytest.raises(expected_exc): + gen_np.random(size=size, out=out_np) + with pytest.raises(expected_exc): + gen_num.random(size=size, out=out_num) + + if __name__ == "__main__": import sys diff --git a/tests/integration/test_random_creation.py b/tests/integration/test_random_creation.py index 2acd1df6b..15bf70ec1 100644 --- a/tests/integration/test_random_creation.py +++ b/tests/integration/test_random_creation.py @@ -170,9 +170,10 @@ def test_rand(shape): ] SMALL_RNG_SIZES = [5, 1024, (1, 2)] LARGE_RNG_SIZES = [10000, (20, 50, 4)] -ALL_RNG_SIZES = SMALL_RNG_SIZES + LARGE_RNG_SIZES +ALL_RNG_SIZES = SMALL_RNG_SIZES + LARGE_RNG_SIZES + [None] INT_DTYPES = [np.int64, np.int32, np.int16] UINT_DTYPES = [np.uint64, np.uint16, np.uint0] +FLOAT_DTYPES = [np.float16, np.float128, np.float64] @pytest.mark.parametrize("size", ALL_RNG_SIZES, ids=str) @@ -182,10 +183,10 @@ def test_randint_basic_stats(low, high, size, dtype): arr_np, arr_num = gen_random_from_both( "randint", low=low, high=high, size=size, dtype=dtype ) - assert arr_np.dtype == arr_np.dtype + assert arr_np.dtype == arr_num.dtype assert arr_np.shape == arr_num.shape assert np.min(arr_num) >= low - assert np.max(arr_num) <= high + assert np.max(arr_num) < high @pytest.mark.parametrize("low", [1024, 1025, 12345], ids=str) @@ -209,6 +210,13 @@ def test_randint_high_limit(): assert np.max(arr_num) < limit +def test_random_integers_high_limit(): + limit = 10 + arr_np, arr_num = gen_random_from_both("random_integers", 10, size=100) + assert np.max(arr_np) <= limit + assert np.max(arr_num) <= limit + + @pytest.mark.xfail(reason="cuNumeric raises NotImplementedError") @pytest.mark.parametrize( "low, high", [(3000.45, 15000), (123, 456.7), (12.3, 45.6)], ids=str @@ -295,7 +303,7 @@ def test_random_integers(low, high, size): ) -@pytest.mark.parametrize("size", ALL_RNG_SIZES, ids=str) +@pytest.mark.parametrize("size", SMALL_RNG_SIZES + LARGE_RNG_SIZES, ids=str) def test_random_sample_basic_stats(size): arr_np, arr_num = gen_random_from_both("random_sample", size=size) assert arr_np.shape == arr_num.shape @@ -317,6 +325,22 @@ def test_random_sample(size): ) +@pytest.mark.parametrize("size", SMALL_RNG_SIZES, ids=str) +def test_random_std_exponential_basic_stats(size): + arr_np, arr_num = gen_random_from_both("standard_exponential", size=size) + assert arr_np.shape == arr_num.shape + assert arr_np.dtype == arr_num.dtype + + +@pytest.mark.parametrize("size", SMALL_RNG_SIZES, ids=str) +def test_random_std_gamma_basic_stats(size): + arr_np, arr_num = gen_random_from_both( + "standard_gamma", shape=3.1415, size=size + ) + assert arr_np.shape == arr_num.shape + assert arr_np.dtype == arr_num.dtype + + class TestRandomErrors: def assert_exc_from_both(self, func, exc, *args, **kwargs): with pytest.raises(exc): diff --git a/tests/integration/test_random_gamma.py b/tests/integration/test_random_gamma.py index dbca2ddfc..69fbc2f42 100644 --- a/tests/integration/test_random_gamma.py +++ b/tests/integration/test_random_gamma.py @@ -126,6 +126,30 @@ def test_noncentral_chisquare_float64(t): assert_distribution(a, theo_mean, theo_std) +@pytest.mark.parametrize("func", ("gamma", "noncentral_chisquare"), ids=str) +@pytest.mark.parametrize("size", ((2048 * 2048), (4096,), 25535), ids=str) +def test_gamma_sizes(func, size): + seed = 42 + gen_np = np.random.Generator(np.random.PCG64(seed=seed)) + gen_num = num.random.Generator(num.random.XORWOW(seed=seed)) + a_np = getattr(gen_np, func)(3.1415, 1.414, size=size) + a_num = getattr(gen_num, func)(3.1415, 1.414, size=size) + assert a_np.shape == a_num.shape + + +@pytest.mark.xfail +@pytest.mark.parametrize("func", ("gamma", "noncentral_chisquare"), ids=str) +def test_gamma_size_none(func): + seed = 42 + gen_np = np.random.Generator(np.random.PCG64(seed=seed)) + gen_num = num.random.Generator(num.random.XORWOW(seed=seed)) + a_np = getattr(gen_np, func)(3.1415, 1.414, size=None) + a_num = getattr(gen_num, func)(3.1415, 1.414, size=None) + # cuNumeric returns singleton array + # NumPy returns scalar + assert np.ndim(a_np) == np.ndim(a_num) + + if __name__ == "__main__": import sys diff --git a/tests/integration/test_random_straightforward.py b/tests/integration/test_random_straightforward.py index 584719587..b412e5341 100644 --- a/tests/integration/test_random_straightforward.py +++ b/tests/integration/test_random_straightforward.py @@ -339,6 +339,47 @@ def test_bytes(t): assert_distribution(a, theo_mean, theo_std) +FUNC_ARGS = ( + ("exponential", ()), + ("gumbel", ()), + ("laplace", ()), + ("logistic", ()), + ("pareto", (30.0,)), + ("power", (3.0,)), + ("rayleigh", (np.pi,)), + ("standard_cauchy", ()), + ("standard_exponential", ()), + ("triangular", (1.414, 2.7, 3.1415)), + ("weibull", (3.1415,)), +) + + +@pytest.mark.parametrize("t", BITGENERATOR_ARGS, ids=str) +@pytest.mark.parametrize("func, args", FUNC_ARGS, ids=str) +@pytest.mark.parametrize("size", ((2048 * 2048), (4096,), 25535), ids=str) +def test_beta_sizes(t, func, args, size): + seed = 42 + gen_np = np.random.Generator(np.random.PCG64(seed=seed)) + gen_num = num.random.Generator(t(seed=seed)) + a_np = getattr(gen_np, func)(*args, size=size) + a_num = getattr(gen_num, func)(*args, size=size) + assert a_np.shape == a_num.shape + + +@pytest.mark.xfail +@pytest.mark.parametrize("t", BITGENERATOR_ARGS, ids=str) +@pytest.mark.parametrize("func, args", FUNC_ARGS, ids=str) +def test_beta_size_none(t, func, args): + seed = 42 + gen_np = np.random.Generator(np.random.PCG64(seed=seed)) + gen_num = num.random.Generator(t(seed=seed)) + a_np = getattr(gen_np, func)(*args, size=None) + a_num = getattr(gen_num, func)(*args, size=None) + # cuNumeric returns singleton array + # NumPy returns scalar + assert np.ndim(a_np) == np.ndim(a_num) + + if __name__ == "__main__": import sys