From c3a0d519d517d6a6ce18f962052377d1560ef441 Mon Sep 17 00:00:00 2001 From: Marcel Stimberg Date: Fri, 15 Mar 2024 10:53:38 +0100 Subject: [PATCH 1/2] Adapt to numpy's new copy keyword semantic In numpy 1.x, `array(x, copy=False)` meant : do not copy if possible. In numpy 2.0, it will raise an error if copying is not possible. This commit replaces the use of `array(x, copy=False)` by `asarray(x)`, which has the "do not copy if possible" in both numpy 1.x and numpy 2.0. --- brian2/core/network.py | 6 +-- brian2/core/variables.py | 2 +- brian2/monitors/spikemonitor.py | 3 +- brian2/units/fundamentalunits.py | 78 +++++++++++++++---------------- brian2/units/unitsafefunctions.py | 4 +- 5 files changed, 44 insertions(+), 49 deletions(-) diff --git a/brian2/core/network.py b/brian2/core/network.py index 3aaf01f76..655c745ba 100644 --- a/brian2/core/network.py +++ b/brian2/core/network.py @@ -451,10 +451,8 @@ def __init__(self, *objs, **kwds): self._schedule = None t = property( - fget=lambda self: Quantity(self.t_, dim=second.dim, copy=False), - doc=""" - Current simulation time in seconds (`Quantity`) - """, + fget=lambda self: Quantity(self.t_, dim=second.dim), + doc="Current simulation time in seconds (`Quantity`)", ) @device_override("network_get_profiling_info") diff --git a/brian2/core/variables.py b/brian2/core/variables.py index 9c8cf2ceb..eaa52b0a4 100644 --- a/brian2/core/variables.py +++ b/brian2/core/variables.py @@ -1326,7 +1326,7 @@ def set_with_index_array(self, item, value, check_units): else: indices = self.indexing(item, self.index_var) - q = Quantity(value, copy=False) + q = Quantity(value) if len(q.shape): if not len(q.shape) == 1 or len(q) != 1 and len(q) != len(indices): raise ValueError( diff --git a/brian2/monitors/spikemonitor.py b/brian2/monitors/spikemonitor.py index 424807776..5c899d4d5 100644 --- a/brian2/monitors/spikemonitor.py +++ b/brian2/monitors/spikemonitor.py @@ -258,11 +258,10 @@ def _values_dict(self, first_pos, sort_indices, used_indices, var): first_pos[current_pos] : first_pos[current_pos + 1] ], dim=dim, - copy=False, ) else: event_values[idx] = Quantity( - sorted_values[first_pos[current_pos] :], dim=dim, copy=False + sorted_values[first_pos[current_pos] :], dim=dim ) current_pos += 1 else: diff --git a/brian2/units/fundamentalunits.py b/brian2/units/fundamentalunits.py index c5e37bdd8..82270a19e 100644 --- a/brian2/units/fundamentalunits.py +++ b/brian2/units/fundamentalunits.py @@ -287,7 +287,7 @@ def f(x, *args, **kwds): # pylint: disable=C0111 ), value=x, ) - return func(np.array(x, copy=False), *args, **kwds) + return func(np.asarray(x), *args, **kwds) f._arg_units = [1] f._return_unit = 1 @@ -310,7 +310,7 @@ def wrap_function_keep_dimensions(func): """ def f(x, *args, **kwds): # pylint: disable=C0111 - return Quantity(func(np.array(x, copy=False), *args, **kwds), dim=x.dim) + return Quantity(func(np.asarray(x), *args, **kwds), dim=x.dim) f._arg_units = [None] f._return_unit = lambda u: u @@ -334,7 +334,7 @@ def wrap_function_change_dimensions(func, change_dim_func): """ def f(x, *args, **kwds): # pylint: disable=C0111 - ar = np.array(x, copy=False) + ar = np.asarray(x) return Quantity(func(ar, *args, **kwds), dim=change_dim_func(ar, x.dim)) f._arg_units = [None] @@ -357,7 +357,7 @@ def wrap_function_remove_dimensions(func): """ def f(x, *args, **kwds): # pylint: disable=C0111 - return func(np.array(x, copy=False), *args, **kwds) + return func(np.asarray(x), *args, **kwds) f._arg_units = [None] f._return_unit = 1 @@ -584,7 +584,7 @@ def __truediv__(self, value): return self.__div__(value) def __pow__(self, value): - value = np.array(value, copy=False) + value = np.asarray(value) if value.size > 1: raise TypeError("Too many exponents") return get_or_create_dimension([x * value for x in self._dims]) @@ -901,7 +901,7 @@ def in_unit(x, u, precision=None): """ if is_dimensionless(x): fail_for_dimension_mismatch(x, u, 'Non-matching unit for function "in_unit"') - return str(np.array(x / u, copy=False)) + return str(np.asarray(x / u)) else: return x.in_unit(u, precision=precision) @@ -1061,7 +1061,10 @@ class Quantity(np.ndarray): def __new__(cls, arr, dim=None, dtype=None, copy=False, force_quantity=False): # Do not create dimensionless quantities, use pure numpy arrays instead if dim is DIMENSIONLESS and not force_quantity: - arr = np.array(arr, dtype=dtype, copy=copy) + if copy: + arr = np.array(arr, dtype=dtype) + else: + arr = np.asarray(arr, dtype=dtype) if arr.shape == (): # For scalar values, return a simple Python object instead of # a numpy scalar @@ -1070,8 +1073,10 @@ def __new__(cls, arr, dim=None, dtype=None, copy=False, force_quantity=False): # All np.ndarray subclasses need something like this, see # http://www.scipy.org/Subclasses - subarr = np.array(arr, dtype=dtype, copy=copy).view(cls) - + if copy: + subarr = np.array(arr, dtype=dtype).view(cls) + else: + subarr = np.asarray(arr, dtype=dtype).view(cls) # We only want numerical datatypes if not issubclass(np.dtype(subarr.dtype).type, (np.number, np.bool_)): raise TypeError("Quantities can only be created from numerical data.") @@ -1134,18 +1139,13 @@ def __array_ufunc__(self, uf, method, *inputs, **kwargs): # Note that it is also part of the input arguments, so we don't # need to check its dimensions assert len(kwargs["out"]) == 1 - kwargs["out"] = ( - np.array( - kwargs["out"][0], - copy=False, - ), - ) + kwargs["out"] = (np.asarray(kwargs["out"][0]),) if uf.__name__ in (UFUNCS_LOGICAL + ["sign", "ones_like"]): # do not touch return value - return uf_method(*[np.array(a, copy=False) for a in inputs], **kwargs) + return uf_method(*[np.asarray(a) for a in inputs], **kwargs) elif uf.__name__ in UFUNCS_PRESERVE_DIMENSIONS: return Quantity( - uf_method(*[np.array(a, copy=False) for a in inputs], **kwargs), + uf_method(*[np.asarray(a) for a in inputs], **kwargs), dim=self.dim, ) elif uf.__name__ in UFUNCS_CHANGE_DIMENSIONS + ["power"]: @@ -1162,12 +1162,12 @@ def __array_ufunc__(self, uf, method, *inputs, **kwargs): ), value=inputs[1], ) - if np.array(inputs[1], copy=False).size != 1: + if np.asarray(inputs[1]).size != 1: raise TypeError( "Only length-1 arrays can be used as an exponent for" " quantities." ) - dim = get_dimensions(inputs[0]) ** np.array(inputs[1], copy=False) + dim = get_dimensions(inputs[0]) ** np.asarray(inputs[1]) elif uf.__name__ == "square": dim = self.dim**2 elif uf.__name__ in ("divide", "true_divide", "floor_divide"): @@ -1182,7 +1182,7 @@ def __array_ufunc__(self, uf, method, *inputs, **kwargs): else: return NotImplemented return Quantity( - uf_method(*[np.array(a, copy=False) for a in inputs], **kwargs), dim=dim + uf_method(*[np.asarray(a) for a in inputs], **kwargs), dim=dim ) elif uf.__name__ in UFUNCS_INTEGERS: # Numpy should already raise a TypeError by itself @@ -1201,10 +1201,10 @@ def __array_ufunc__(self, uf, method, *inputs, **kwargs): val2=inputs[1], ) if uf.__name__ in UFUNCS_COMPARISONS: - return uf_method(*[np.array(i, copy=False) for i in inputs], **kwargs) + return uf_method(*[np.asarray(i) for i in inputs], **kwargs) else: return Quantity( - uf_method(*[np.array(i, copy=False) for i in inputs], **kwargs), + uf_method(*[np.asarray(i) for i in inputs], **kwargs), dim=self.dim, ) elif uf.__name__ in UFUNCS_DIMENSIONLESS: @@ -1215,7 +1215,7 @@ def __array_ufunc__(self, uf, method, *inputs, **kwargs): % uf.__name__, value=inputs[0], ) - return uf_method(np.asarray(inputs[0], copy=False), *inputs[1:], **kwargs) + return uf_method(np.asarray(inputs[0]), *inputs[1:], **kwargs) elif uf.__name__ in UFUNCS_DIMENSIONLESS_TWOARGS: # Ok if both arguments are dimensionless fail_for_dimension_mismatch( @@ -1243,8 +1243,8 @@ def __array_ufunc__(self, uf, method, *inputs, **kwargs): value=inputs[1], ) return uf_method( - np.array(inputs[0], copy=False), - np.array(inputs[1], copy=False), + np.asarray(inputs[0]), + np.asarray(inputs[1]), *inputs[2:], **kwargs, ) @@ -1374,7 +1374,7 @@ def in_unit(self, u, precision=None, python_code=False): fail_for_dimension_mismatch(self, u, 'Non-matching unit for method "in_unit"') - value = np.array(self / u, copy=False) + value = np.asarray(self / u) # numpy uses the printoptions setting only in arrays, not in array # scalars, so we use this hackish way of turning the scalar first into # an array, then removing the square brackets from the output @@ -1531,7 +1531,7 @@ def top_replace(s): return type(seq)(top_replace(seq)) - return replace_with_quantity(np.array(self, copy=False).tolist(), self.dim) + return replace_with_quantity(np.asarray(self).tolist(), self.dim) #### COMPARISONS #### def _comparison(self, other, operator_str, operation): @@ -1544,7 +1544,7 @@ def _comparison(self, other, operator_str, operation): % operator_str ) fail_for_dimension_mismatch(self, other, message, value1=self, value2=other) - return operation(np.array(self, copy=False), np.array(other, copy=False)) + return operation(np.asarray(self), np.asarray(other)) def __lt__(self, other): return self._comparison(other, "<", operator.lt) @@ -1566,7 +1566,7 @@ def __ne__(self, other): #### MAKE QUANTITY PICKABLE #### def __reduce__(self): - return quantity_with_dimensions, (np.array(self, copy=False), self.dim) + return quantity_with_dimensions, (np.asarray(self), self.dim) #### REPRESENTATION #### def __repr__(self): @@ -1589,7 +1589,7 @@ def _latex(self, expr): best_unit_latex = latex(best_unit) else: # A quantity best_unit_latex = latex(best_unit.dimensions) - unitless = np.array(self / best_unit, copy=False) + unitless = np.asarray(self / best_unit) threshold = np.get_printoptions()["threshold"] // 100 if unitless.ndim == 0: sympy_quantity = float(unitless) @@ -1685,7 +1685,7 @@ def ravel(self, *args, **kwds): 'explicitly calling "ravel()", consider using "flatten()" ' "instead." ) - return np.array(self, copy=False).ravel(*args, **kwds) + return np.asarray(self).ravel(*args, **kwds) ravel._arg_units = [None] ravel._return_unit = 1 @@ -1725,9 +1725,9 @@ def clip(self, a_min, a_max, *args, **kwds): # pylint: disable=C0111 fail_for_dimension_mismatch(self, a_max, "clip") return Quantity( np.clip( - np.array(self, copy=False), - np.array(a_min, copy=False), - np.array(a_max, copy=False), + np.asarray(self), + np.asarray(a_min), + np.asarray(a_max), *args, **kwds, ), @@ -1748,7 +1748,7 @@ def dot(self, other, **kwds): # pylint: disable=C0111 def searchsorted(self, v, **kwds): # pylint: disable=C0111 fail_for_dimension_mismatch(self, v, "searchsorted") - return super().searchsorted(np.array(v, copy=False), **kwds) + return super().searchsorted(np.asarray(v), **kwds) searchsorted.__doc__ = np.ndarray.searchsorted.__doc__ searchsorted._do_not_run_doctests = True @@ -1768,7 +1768,7 @@ def prod(self, *args, **kwds): # pylint: disable=C0111 # identical if dim_exponent.size > 1: dim_exponent = dim_exponent[0] - return Quantity(np.array(prod_result, copy=False), self.dim**dim_exponent) + return Quantity(np.asarray(prod_result), self.dim**dim_exponent) prod.__doc__ = np.ndarray.prod.__doc__ prod._do_not_run_doctests = True @@ -1779,7 +1779,7 @@ def cumprod(self, *args, **kwds): # pylint: disable=C0111 "cumprod over array elements on quantities " "with dimensions is not possible." ) - return Quantity(np.array(self, copy=False).cumprod(*args, **kwds)) + return Quantity(np.asarray(self).cumprod(*args, **kwds)) cumprod.__doc__ = np.ndarray.cumprod.__doc__ cumprod._do_not_run_doctests = True @@ -2357,7 +2357,7 @@ def __getitem__(self, x): if len(matching) == 0: raise KeyError("Unit not found in registry.") - matching_values = np.array(list(matching.keys()), copy=False) + matching_values = np.asarray(list(matching.keys())) print_opts = np.get_printoptions() edgeitems, threshold = print_opts["edgeitems"], print_opts["threshold"] if x.size > threshold: @@ -2375,7 +2375,7 @@ def __getitem__(self, x): [x[use_slices].flatten() for use_slices in itertools.product(*slices)] ) else: - x_flat = np.array(x, copy=False).flatten() + x_flat = np.asarray(x).flatten() floatreps = np.tile(np.abs(x_flat), (len(matching), 1)).T / matching_values # ignore zeros, they are well represented in any unit floatreps[floatreps == 0] = np.nan diff --git a/brian2/units/unitsafefunctions.py b/brian2/units/unitsafefunctions.py index b3b904425..6548e436c 100644 --- a/brian2/units/unitsafefunctions.py +++ b/brian2/units/unitsafefunctions.py @@ -209,7 +209,6 @@ def arange(*args, **kwargs): **kwargs, ), dim=dim, - copy=False, ) else: return Quantity( @@ -220,7 +219,6 @@ def arange(*args, **kwargs): **kwargs, ), dim=dim, - copy=False, ) @@ -247,7 +245,7 @@ def linspace(start, stop, num=50, endpoint=True, retstep=False, dtype=None): retstep=retstep, dtype=dtype, ) - return Quantity(result, dim=dim, copy=False) + return Quantity(result, dim=dim) linspace._do_not_run_doctests = True From cd63d370b707bf48038186d67144c875ba92bb1c Mon Sep 17 00:00:00 2001 From: Marcel Stimberg Date: Fri, 15 Mar 2024 11:16:44 +0100 Subject: [PATCH 2/2] Temporarily use separate pyproject.toml to build against numpy 2 --- .github/workflows/test_latest.yml | 4 +- numpy2.pyproject.toml | 80 +++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 numpy2.pyproject.toml diff --git a/.github/workflows/test_latest.yml b/.github/workflows/test_latest.yml index 5fb64caee..88e68e0d8 100644 --- a/.github/workflows/test_latest.yml +++ b/.github/workflows/test_latest.yml @@ -64,7 +64,9 @@ jobs: ${{ steps.python.outputs.python-path }} -m pip install mpmath # install stable version ${{ steps.python.outputs.python-path }} -m pip install --pre pytest cython sympy pyparsing jinja2 sphinx - name: Install Brian2 - run: ${{ steps.python.outputs.python-path }} -m pip install --pre -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple --extra-index-url https://pypi.org/simple . + run: | + cp numpy2.pyproject.toml pyproject.toml + ${{ steps.python.outputs.python-path }} -m pip install --pre -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple --extra-index-url https://pypi.org/simple . - name: Run Tests run: | cd ${{ github.workspace }}/.. # move out of the workspace to avoid direct import diff --git a/numpy2.pyproject.toml b/numpy2.pyproject.toml new file mode 100644 index 000000000..0ef562cab --- /dev/null +++ b/numpy2.pyproject.toml @@ -0,0 +1,80 @@ +[project] +name = "Brian2" +authors = [ + {name = 'Marcel Stimberg'}, + {name = 'Dan Goodman'}, + {name ='Romain Brette'} +] +requires-python = '>=3.9' +dependencies = [ + 'numpy>=2.0.0b1', + 'cython>=0.29.21', + 'sympy>=1.2', + 'pyparsing', + 'jinja2>=2.7', + 'py-cpuinfo;platform_system=="Windows"', + 'setuptools>=61', + 'packaging', +] +dynamic = ["version", "readme"] +description = 'A clock-driven simulator for spiking neural networks' +keywords = ['computational neuroscience', 'simulation', 'neural networks', 'spiking neurons', 'biological neural networks', 'research'] +classifiers = [ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Science/Research', + 'License :: OSI Approved :: CEA CNRS Inria Logiciel Libre License, version 2.1 (CeCILL-2.1)', + 'Natural Language :: English', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Topic :: Scientific/Engineering :: Bio-Informatics' +] + +[project.optional-dependencies] +test = ['pytest', 'pytest-xdist>=1.22.3'] +docs = ['sphinx>=7', 'ipython>=5', 'sphinx-tabs'] + +[project.urls] +Homepage = 'https://briansimulator.org' +Documentation ='https://brian2.readthedocs.io/' +Source = 'https://github.com/brian-team/brian2' +Tracker = 'https://github.com/brian-team/brian2/issues' + +[tool.setuptools] +zip-safe = false +include-package-data = true + +[tool.setuptools.packages.find] +include = ["brian2*"] + +[tool.setuptools.dynamic] +readme = {file = 'README.rst', content-type = "text/x-rst"} + +[tool.setuptools_scm] +version_scheme = 'post-release' +local_scheme = 'no-local-version' +write_to = 'brian2/_version.py' +tag_regex = '^(?P\d+(?:\.\d+){0,2}[^\+]*(?:\+.*)?)$' +fallback_version = 'unknown' + +[build-system] +requires = [ + "setuptools>=61", + 'numpy>=2.0.0b1 ', + "wheel", + "Cython", + "setuptools_scm[toml]>=6.2" +] +build-backend = "setuptools.build_meta" + +[tool.black] +target-version = ['py39'] +include = '^/brian2/.*\.pyi?$' + +[tool.isort] +atomic = true +profile = "black" +py_version = "39" +skip_gitignore = true +# NOTE: isort has no "include" option, only "skip". +skip_glob = ["dev/*", "docs_sphinx/*", "examples/*", "tutorials/*"]