diff options
author | DongHun Kwak <dh0128.kwak@samsung.com> | 2020-12-31 09:40:19 +0900 |
---|---|---|
committer | DongHun Kwak <dh0128.kwak@samsung.com> | 2020-12-31 09:40:19 +0900 |
commit | 8fba8334be80177304a51cae72db20bcc62e3fdd (patch) | |
tree | 977e37c5fbc5b8fe057523b340cad7950e0a58d5 | |
parent | e330f8df8309c1d617ccba1c5a8da1fdb3f6898b (diff) | |
download | python-numpy-8fba8334be80177304a51cae72db20bcc62e3fdd.tar.gz python-numpy-8fba8334be80177304a51cae72db20bcc62e3fdd.tar.bz2 python-numpy-8fba8334be80177304a51cae72db20bcc62e3fdd.zip |
Imported Upstream version 1.18.3upstream/1.18.3
-rw-r--r-- | doc/changelog/1.18.3-changelog.rst | 24 | ||||
-rw-r--r-- | doc/source/release.rst | 1 | ||||
-rw-r--r-- | doc/source/release/1.18.2-notes.rst | 2 | ||||
-rw-r--r-- | doc/source/release/1.18.3-notes.rst | 45 | ||||
-rw-r--r-- | numpy/core/src/multiarray/arraytypes.c.src | 69 | ||||
-rw-r--r-- | numpy/core/src/multiarray/convert_datatype.c | 8 | ||||
-rw-r--r-- | numpy/core/src/multiarray/getset.c | 20 | ||||
-rw-r--r-- | numpy/core/tests/test_longdouble.py | 34 | ||||
-rw-r--r-- | numpy/random/_generator.pyx | 28 | ||||
-rw-r--r-- | numpy/random/mtrand.pyx | 23 | ||||
-rw-r--r-- | numpy/random/tests/test_generator_mt19937.py | 36 | ||||
-rw-r--r-- | numpy/random/tests/test_random.py | 12 | ||||
-rw-r--r-- | pavement.py | 2 | ||||
-rwxr-xr-x | setup.py | 2 |
14 files changed, 190 insertions, 116 deletions
diff --git a/doc/changelog/1.18.3-changelog.rst b/doc/changelog/1.18.3-changelog.rst new file mode 100644 index 000000000..6ed2d4851 --- /dev/null +++ b/doc/changelog/1.18.3-changelog.rst @@ -0,0 +1,24 @@ + +Contributors +============ + +A total of 6 people contributed to this release. People with a "+" by their +names contributed a patch for the first time. + +* Charles Harris +* Max Balandat + +* @Mibu287 + +* Pan Jan + +* Sebastian Berg +* @panpiort8 + + +Pull requests merged +==================== + +A total of 5 pull requests were merged for this release. + +* `#15916 <https://github.com/numpy/numpy/pull/15916>`__: BUG: Fix eigh and cholesky methods of numpy.random.multivariate_normal +* `#15929 <https://github.com/numpy/numpy/pull/15929>`__: BUG,MAINT: Remove incorrect special case in string to number... +* `#15930 <https://github.com/numpy/numpy/pull/15930>`__: BUG: Guarantee array is in valid state after memory error occurs... +* `#15954 <https://github.com/numpy/numpy/pull/15954>`__: BUG: Check that `pvals` is 1D in `_generator.multinomial`. +* `#16017 <https://github.com/numpy/numpy/pull/16017>`__: BUG: Alpha parameter must be 1D in `generator.dirichlet` diff --git a/doc/source/release.rst b/doc/source/release.rst index d8726d5b8..31bd819d6 100644 --- a/doc/source/release.rst +++ b/doc/source/release.rst @@ -5,6 +5,7 @@ Release Notes .. toctree:: :maxdepth: 3 + 1.18.3 <release/1.18.3-notes> 1.18.2 <release/1.18.2-notes> 1.18.1 <release/1.18.1-notes> 1.18.0 <release/1.18.0-notes> diff --git a/doc/source/release/1.18.2-notes.rst b/doc/source/release/1.18.2-notes.rst index 629449b19..2681a907f 100644 --- a/doc/source/release/1.18.2-notes.rst +++ b/doc/source/release/1.18.2-notes.rst @@ -4,7 +4,7 @@ NumPy 1.18.2 Release Notes ========================== -This small elease contains a fix for a performance regression in numpy/random +This small release contains a fix for a performance regression in numpy/random and several bug/maintenance updates. The Python versions supported in this release are 3.5-3.8. Downstream diff --git a/doc/source/release/1.18.3-notes.rst b/doc/source/release/1.18.3-notes.rst new file mode 100644 index 000000000..1ebad52b8 --- /dev/null +++ b/doc/source/release/1.18.3-notes.rst @@ -0,0 +1,45 @@ +.. currentmodule:: numpy + +========================== +NumPy 1.18.3 Release Notes +========================== + +This release contains various bug/regression fixes. + +The Python versions supported in this release are 3.5-3.8. Downstream +developers should use Cython >= 0.29.15 for Python 3.8 support and OpenBLAS >= +3.7 to avoid errors on the Skylake architecture. + + +Highlights +========== + +* Fix for the `method='eigh'` and `method='cholesky'` methods in + `numpy.random.multivariate_normal`. Those were producing samples from the + wrong distribution. + + +Contributors +============ + +A total of 6 people contributed to this release. People with a "+" by their +names contributed a patch for the first time. + +* Charles Harris +* Max Balandat + +* @Mibu287 + +* Pan Jan + +* Sebastian Berg +* @panpiort8 + + + +Pull requests merged +==================== + +A total of 5 pull requests were merged for this release. + +* `#15916 <https://github.com/numpy/numpy/pull/15916>`__: BUG: Fix eigh and cholesky methods of numpy.random.multivariate_normal +* `#15929 <https://github.com/numpy/numpy/pull/15929>`__: BUG,MAINT: Remove incorrect special case in string to number... +* `#15930 <https://github.com/numpy/numpy/pull/15930>`__: BUG: Guarantee array is in valid state after memory error occurs... +* `#15954 <https://github.com/numpy/numpy/pull/15954>`__: BUG: Check that `pvals` is 1D in `_generator.multinomial`. +* `#16017 <https://github.com/numpy/numpy/pull/16017>`__: BUG: Alpha parameter must be 1D in `generator.dirichlet` diff --git a/numpy/core/src/multiarray/arraytypes.c.src b/numpy/core/src/multiarray/arraytypes.c.src index 9e108e3e1..598ee8537 100644 --- a/numpy/core/src/multiarray/arraytypes.c.src +++ b/numpy/core/src/multiarray/arraytypes.c.src @@ -1527,16 +1527,8 @@ OBJECT_to_@TOTYPE@(void *input, void *output, npy_intp n, * #oskip = 1*18,(PyArray_DESCR(aop)->elsize)*3,1*2, * 1*18,(PyArray_DESCR(aop)->elsize)*3,1*2, * 1*18,(PyArray_DESCR(aop)->elsize)*3,1*2# - * #convert = 1*18, 0*3, 1*2, - * 1*18, 0*3, 1*2, - * 0*23# - * #convstr = (Int*9, Long*2, Float*4, Complex*3, Tuple*3, Long*2)*3# */ -#if @convert@ - -#define IS_@from@ - static void @from@_to_@to@(void *input, void *output, npy_intp n, void *vaip, void *aop) @@ -1550,41 +1542,10 @@ static void int oskip = @oskip@; for (i = 0; i < n; i++, ip+=skip, op+=oskip) { - PyObject *new; PyObject *temp = PyArray_Scalar(ip, PyArray_DESCR(aip), (PyObject *)aip); if (temp == NULL) { return; } - -#if defined(NPY_PY3K) && defined(IS_STRING) - /* Work around some Python 3K */ - new = PyUnicode_FromEncodedObject(temp, "ascii", "strict"); - Py_DECREF(temp); - temp = new; - if (temp == NULL) { - return; - } -#endif - /* convert from Python object to needed one */ - { - PyObject *args; - - /* call out to the Python builtin given by convstr */ - args = Py_BuildValue("(N)", temp); -#if defined(NPY_PY3K) -#define PyInt_Type PyLong_Type -#endif - new = Py@convstr@_Type.tp_new(&Py@convstr@_Type, args, NULL); -#if defined(NPY_PY3K) -#undef PyInt_Type -#endif - Py_DECREF(args); - temp = new; - if (temp == NULL) { - return; - } - } - if (@to@_setitem(temp, op, aop)) { Py_DECREF(temp); return; @@ -1593,36 +1554,6 @@ static void } } -#undef IS_@from@ - -#else - -static void -@from@_to_@to@(void *input, void *output, npy_intp n, - void *vaip, void *aop) -{ - @fromtyp@ *ip = input; - @totyp@ *op = output; - PyArrayObject *aip = vaip; - - npy_intp i; - int skip = PyArray_DESCR(aip)->elsize; - int oskip = @oskip@; - - for (i = 0; i < n; i++, ip+=skip, op+=oskip) { - PyObject *temp = PyArray_Scalar(ip, PyArray_DESCR(aip), (PyObject *)aip); - if (temp == NULL) { - return; - } - if (@to@_setitem(temp, op, aop)) { - Py_DECREF(temp); - return; - } - Py_DECREF(temp); - } -} - -#endif /**end repeat**/ diff --git a/numpy/core/src/multiarray/convert_datatype.c b/numpy/core/src/multiarray/convert_datatype.c index 4326448dc..14025a5f9 100644 --- a/numpy/core/src/multiarray/convert_datatype.c +++ b/numpy/core/src/multiarray/convert_datatype.c @@ -212,13 +212,17 @@ PyArray_AdaptFlexibleDType(PyObject *data_obj, PyArray_Descr *data_dtype, case NPY_HALF: case NPY_FLOAT: case NPY_DOUBLE: - case NPY_LONGDOUBLE: size = 32; break; + case NPY_LONGDOUBLE: + size = 48; + break; case NPY_CFLOAT: case NPY_CDOUBLE: + size = 2 * 32; + break; case NPY_CLONGDOUBLE: - size = 64; + size = 2 * 48; break; case NPY_OBJECT: size = 64; diff --git a/numpy/core/src/multiarray/getset.c b/numpy/core/src/multiarray/getset.c index 6e5d480d0..b405ab099 100644 --- a/numpy/core/src/multiarray/getset.c +++ b/numpy/core/src/multiarray/getset.c @@ -67,28 +67,34 @@ array_shape_set(PyArrayObject *self, PyObject *val) return -1; } - /* Free old dimensions and strides */ - npy_free_cache_dim_array(self); nd = PyArray_NDIM(ret); - ((PyArrayObject_fields *)self)->nd = nd; if (nd > 0) { /* create new dimensions and strides */ - ((PyArrayObject_fields *)self)->dimensions = npy_alloc_cache_dim(2 * nd); - if (PyArray_DIMS(self) == NULL) { + npy_intp *_dimensions = npy_alloc_cache_dim(2 * nd); + if (_dimensions == NULL) { Py_DECREF(ret); - PyErr_SetString(PyExc_MemoryError,""); + PyErr_NoMemory(); return -1; } - ((PyArrayObject_fields *)self)->strides = PyArray_DIMS(self) + nd; + /* Free old dimensions and strides */ + npy_free_cache_dim_array(self); + ((PyArrayObject_fields *)self)->nd = nd; + ((PyArrayObject_fields *)self)->dimensions = _dimensions; + ((PyArrayObject_fields *)self)->strides = _dimensions + nd; + if (nd) { memcpy(PyArray_DIMS(self), PyArray_DIMS(ret), nd*sizeof(npy_intp)); memcpy(PyArray_STRIDES(self), PyArray_STRIDES(ret), nd*sizeof(npy_intp)); } } else { + /* Free old dimensions and strides */ + npy_free_cache_dim_array(self); + ((PyArrayObject_fields *)self)->nd = 0; ((PyArrayObject_fields *)self)->dimensions = NULL; ((PyArrayObject_fields *)self)->strides = NULL; } + Py_DECREF(ret); PyArray_UpdateFlags(self, NPY_ARRAY_C_CONTIGUOUS | NPY_ARRAY_F_CONTIGUOUS); return 0; diff --git a/numpy/core/tests/test_longdouble.py b/numpy/core/tests/test_longdouble.py index 2b6e1c5a2..0ba994860 100644 --- a/numpy/core/tests/test_longdouble.py +++ b/numpy/core/tests/test_longdouble.py @@ -39,22 +39,36 @@ def test_repr_roundtrip(): assert_equal(np.longdouble(repr(o)), o, "repr was %s" % repr(o)) -def test_unicode(): - np.longdouble(u"1.2") +@pytest.mark.skipif(string_to_longdouble_inaccurate, reason="Need strtold_l") +def test_repr_roundtrip_bytes(): + o = 1 + LD_INFO.eps + assert_equal(np.longdouble(repr(o).encode("ascii")), o) -def test_string(): - np.longdouble("1.2") +@pytest.mark.skipif(string_to_longdouble_inaccurate, reason="Need strtold_l") +@pytest.mark.parametrize("strtype", (np.str_, np.bytes_, str, bytes)) +def test_array_and_stringlike_roundtrip(strtype): + """ + Test that string representations of long-double roundtrip both + for array casting and scalar coercion, see also gh-15608. + """ + o = 1 + LD_INFO.eps + if strtype in (np.bytes_, bytes): + o_str = strtype(repr(o).encode("ascii")) + else: + o_str = strtype(repr(o)) -def test_bytes(): - np.longdouble(b"1.2") + # Test that `o` is correctly coerced from the string-like + assert o == np.longdouble(o_str) + # Test that arrays also roundtrip correctly: + o_strarr = np.asarray([o] * 3, dtype=strtype) + assert (o == o_strarr.astype(np.longdouble)).all() -@pytest.mark.skipif(string_to_longdouble_inaccurate, reason="Need strtold_l") -def test_repr_roundtrip_bytes(): - o = 1 + LD_INFO.eps - assert_equal(np.longdouble(repr(o).encode("ascii")), o) + # And array coercion and casting to string give the same as scalar repr: + assert (o_strarr == o_str).all() + assert (np.asarray([o] * 3).astype(strtype) == o_str).all() def test_bogus_string(): diff --git a/numpy/random/_generator.pyx b/numpy/random/_generator.pyx index 6a217e954..7bfa8e1c0 100644 --- a/numpy/random/_generator.pyx +++ b/numpy/random/_generator.pyx @@ -3643,10 +3643,9 @@ cdef class Generator: # approximately zero or when the covariance is not positive-semidefinite _factor = u * np.sqrt(abs(s)) else: - _factor = np.sqrt(s)[:, None] * vh + _factor = u * np.sqrt(s) - x = np.dot(x, _factor) - x += mean + x = mean + x @ _factor.T x.shape = tuple(final_shape) return x @@ -3749,8 +3748,8 @@ cdef class Generator: d = len(pvals) on = <np.ndarray>np.PyArray_FROM_OTF(n, np.NPY_INT64, np.NPY_ALIGNED) - parr = <np.ndarray>np.PyArray_FROM_OTF( - pvals, np.NPY_DOUBLE, np.NPY_ALIGNED | np.NPY_ARRAY_C_CONTIGUOUS) + parr = <np.ndarray>np.PyArray_FROMANY( + pvals, np.NPY_DOUBLE, 1, 1, np.NPY_ARRAY_ALIGNED | np.NPY_ARRAY_C_CONTIGUOUS) pix = <double*>np.PyArray_DATA(parr) check_array_constraint(parr, 'pvals', CONS_BOUNDED_0_1) if kahan_sum(pix, d-1) > (1.0 + 1e-12): @@ -4030,23 +4029,23 @@ cdef class Generator: Parameters ---------- - alpha : array - Parameter of the distribution (k dimension for sample of - dimension k). + alpha : sequence of floats, length k + Parameter of the distribution (length ``k`` for sample of + length ``k``). size : int or tuple of ints, optional - Output shape. If the given shape is, e.g., ``(m, n, k)``, then + Output shape. If the given shape is, e.g., ``(m, n)``, then ``m * n * k`` samples are drawn. Default is None, in which case a - single value is returned. + vector of length ``k`` is returned. Returns ------- samples : ndarray, - The drawn samples, of shape (size, alpha.ndim). + The drawn samples, of shape ``(size, k)``. Raises ------- ValueError - If any value in alpha is less than or equal to zero + If any value in ``alpha`` is less than or equal to zero Notes ----- @@ -4120,8 +4119,9 @@ cdef class Generator: cdef double acc, invacc k = len(alpha) - alpha_arr = <np.ndarray>np.PyArray_FROM_OTF( - alpha, np.NPY_DOUBLE, np.NPY_ALIGNED | np.NPY_ARRAY_C_CONTIGUOUS) + alpha_arr = <np.ndarray>np.PyArray_FROMANY( + alpha, np.NPY_DOUBLE, 1, 1, + np.NPY_ARRAY_ALIGNED | np.NPY_ARRAY_C_CONTIGUOUS) if np.any(np.less_equal(alpha_arr, 0)): raise ValueError('alpha <= 0') alpha_data = <double*>np.PyArray_DATA(alpha_arr) diff --git a/numpy/random/mtrand.pyx b/numpy/random/mtrand.pyx index d4f7d2d96..0d86d1a3c 100644 --- a/numpy/random/mtrand.pyx +++ b/numpy/random/mtrand.pyx @@ -4191,8 +4191,8 @@ cdef class RandomState: cdef long ni d = len(pvals) - parr = <np.ndarray>np.PyArray_FROM_OTF( - pvals, np.NPY_DOUBLE, np.NPY_ALIGNED | np.NPY_ARRAY_C_CONTIGUOUS) + parr = <np.ndarray>np.PyArray_FROMANY( + pvals, np.NPY_DOUBLE, 1, 1, np.NPY_ARRAY_ALIGNED | np.NPY_ARRAY_C_CONTIGUOUS) pix = <double*>np.PyArray_DATA(parr) check_array_constraint(parr, 'pvals', CONS_BOUNDED_0_1) if kahan_sum(pix, d-1) > (1.0 + 1e-12): @@ -4238,23 +4238,23 @@ cdef class RandomState: Parameters ---------- - alpha : array - Parameter of the distribution (k dimension for sample of - dimension k). + alpha : sequence of floats, length k + Parameter of the distribution (length ``k`` for sample of + length ``k``). size : int or tuple of ints, optional - Output shape. If the given shape is, e.g., ``(m, n, k)``, then + Output shape. If the given shape is, e.g., ``(m, n)``, then ``m * n * k`` samples are drawn. Default is None, in which case a - single value is returned. + vector of length ``k`` is returned. Returns ------- samples : ndarray, - The drawn samples, of shape (size, alpha.ndim). + The drawn samples, of shape ``(size, k)``. Raises ------- ValueError - If any value in alpha is less than or equal to zero + If any value in ``alpha`` is less than or equal to zero See Also -------- @@ -4332,8 +4332,9 @@ cdef class RandomState: cdef double acc, invacc k = len(alpha) - alpha_arr = <np.ndarray>np.PyArray_FROM_OTF( - alpha, np.NPY_DOUBLE, np.NPY_ALIGNED | np.NPY_ARRAY_C_CONTIGUOUS) + alpha_arr = <np.ndarray>np.PyArray_FROMANY( + alpha, np.NPY_DOUBLE, 1, 1, + np.NPY_ARRAY_ALIGNED | np.NPY_ARRAY_C_CONTIGUOUS) if np.any(np.less_equal(alpha_arr, 0)): raise ValueError('alpha <= 0') alpha_data = <double*>np.PyArray_DATA(alpha_arr) diff --git a/numpy/random/tests/test_generator_mt19937.py b/numpy/random/tests/test_generator_mt19937.py index d835f16bd..da2e76928 100644 --- a/numpy/random/tests/test_generator_mt19937.py +++ b/numpy/random/tests/test_generator_mt19937.py @@ -116,6 +116,12 @@ class TestMultinomial(object): contig = random.multinomial(100, pvals=np.ascontiguousarray(pvals)) assert_array_equal(non_contig, contig) + def test_multidimensional_pvals(self): + assert_raises(ValueError, random.multinomial, 10, [[0, 1]]) + assert_raises(ValueError, random.multinomial, 10, [[0], [1]]) + assert_raises(ValueError, random.multinomial, 10, [[[0], [1]], [[1], [0]]]) + assert_raises(ValueError, random.multinomial, 10, np.array([[0, 1], [1, 0]])) + class TestMultivariateHypergeometric(object): @@ -1044,6 +1050,12 @@ class TestRandomDist(object): alpha = np.array([5.4e-01, -1.0e-16]) assert_raises(ValueError, random.dirichlet, alpha) + # gh-15876 + assert_raises(ValueError, random.dirichlet, [[5, 1]]) + assert_raises(ValueError, random.dirichlet, [[5], [1]]) + assert_raises(ValueError, random.dirichlet, [[[5], [1]], [[1], [5]]]) + assert_raises(ValueError, random.dirichlet, np.array([[5, 1], [1, 5]])) + def test_dirichlet_alpha_non_contiguous(self): a = np.array([51.72840233779265162, -1.0, 39.74494232180943953]) alpha = a[::2] @@ -1243,6 +1255,17 @@ class TestRandomDist(object): assert_raises(ValueError, random.multivariate_normal, mean, cov, check_valid='raise', method='eigh') + # check degenerate samples from singular covariance matrix + cov = [[1, 1], [1, 1]] + if method in ('svd', 'eigh'): + samples = random.multivariate_normal(mean, cov, size=(3, 2), + method=method) + assert_array_almost_equal(samples[..., 0], samples[..., 1], + decimal=6) + else: + assert_raises(LinAlgError, random.multivariate_normal, mean, cov, + method='cholesky') + cov = np.array([[1, 0.1], [0.1, 1]], dtype=np.float32) with suppress_warnings() as sup: random.multivariate_normal(mean, cov, method=method) @@ -1260,6 +1283,19 @@ class TestRandomDist(object): assert_raises(ValueError, random.multivariate_normal, mu, np.eye(3)) + @pytest.mark.parametrize("method", ["svd", "eigh", "cholesky"]) + def test_multivariate_normal_basic_stats(self, method): + random = Generator(MT19937(self.seed)) + n_s = 1000 + mean = np.array([1, 2]) + cov = np.array([[2, 1], [1, 2]]) + s = random.multivariate_normal(mean, cov, size=(n_s,), method=method) + s_center = s - mean + cov_emp = (s_center.T @ s_center) / (n_s - 1) + # these are pretty loose and are only designed to detect major errors + assert np.all(np.abs(s_center.mean(-2)) < 0.1) + assert np.all(np.abs(cov_emp - cov) < 0.2) + def test_negative_binomial(self): random = Generator(MT19937(self.seed)) actual = random.negative_binomial(n=100, p=.12345, size=(3, 2)) diff --git a/numpy/random/tests/test_random.py b/numpy/random/tests/test_random.py index 2e2ecedf8..3785405eb 100644 --- a/numpy/random/tests/test_random.py +++ b/numpy/random/tests/test_random.py @@ -92,6 +92,12 @@ class TestMultinomial(object): assert_raises(TypeError, np.random.multinomial, 1, p, float(1)) + def test_multidimensional_pvals(self): + assert_raises(ValueError, np.random.multinomial, 10, [[0, 1]]) + assert_raises(ValueError, np.random.multinomial, 10, [[0], [1]]) + assert_raises(ValueError, np.random.multinomial, 10, [[[0], [1]], [[1], [0]]]) + assert_raises(ValueError, np.random.multinomial, 10, np.array([[0, 1], [1, 0]])) + class TestSetState(object): def setup(self): @@ -559,6 +565,12 @@ class TestRandomDist(object): alpha = np.array([5.4e-01, -1.0e-16]) assert_raises(ValueError, np.random.mtrand.dirichlet, alpha) + # gh-15876 + assert_raises(ValueError, random.dirichlet, [[5, 1]]) + assert_raises(ValueError, random.dirichlet, [[5], [1]]) + assert_raises(ValueError, random.dirichlet, [[[5], [1]], [[1], [5]]]) + assert_raises(ValueError, random.dirichlet, np.array([[5, 1], [1, 5]])) + def test_exponential(self): np.random.seed(self.seed) actual = np.random.exponential(1.1234, size=(3, 2)) diff --git a/pavement.py b/pavement.py index 09c7dbba3..88d1d767c 100644 --- a/pavement.py +++ b/pavement.py @@ -41,7 +41,7 @@ from paver.easy import Bunch, options, task, sh #----------------------------------- # Path to the release notes -RELEASE_NOTES = 'doc/source/release/1.18.2-notes.rst' +RELEASE_NOTES = 'doc/source/release/1.18.3-notes.rst' #------------------------------------------------------- @@ -58,7 +58,7 @@ Operating System :: MacOS MAJOR = 1 MINOR = 18 -MICRO = 2 +MICRO = 3 ISRELEASED = True VERSION = '%d.%d.%d' % (MAJOR, MINOR, MICRO) |