Skip to content

Commit

Permalink
Support NumPy 2 (#61)
Browse files Browse the repository at this point in the history
  • Loading branch information
cgohlke authored Apr 26, 2024
1 parent 08593fe commit b03f635
Show file tree
Hide file tree
Showing 14 changed files with 119 additions and 57 deletions.
1 change: 1 addition & 0 deletions .github/workflows/build_wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ jobs:
CIBW_ARCHS_LINUX: auto
CIBW_ARCHS_MACOS: x86_64 arm64
CIBW_ARCHS_WINDOWS: AMD64 ARM64
CIBW_BEFORE_BUILD: python -m pip install numpy>=2.0.0rc1
- uses: actions/upload-artifact@v4
with:
path: ./wheelhouse/*.whl
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ jobs:
make html
build_wheels:
name: Test cibuildwheel
name: Test cibuildwheel and numpy 2
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
Expand All @@ -86,6 +86,8 @@ jobs:
- uses: actions/checkout@v4
- uses: pypa/cibuildwheel@v2.17.0
env:
CIBW_BEFORE_TEST: python -m pip install numpy>=2.0.0rc1
CIBW_BEFORE_BUILD: python -m pip install numpy>=2.0.0rc1
CIBW_BUILD: "cp311-manylinux_x86_64 cp312-win_amd64 cp312-macosx_x86_64"
CIBW_SKIP:
- uses: actions/upload-artifact@v4
Expand Down
9 changes: 6 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
requires = [
"setuptools>=68",
"numpy",
"Cython",
"Cython>=3.0.10",
]
build-backend = "setuptools.build_meta"

Expand Down Expand Up @@ -81,6 +81,9 @@ version = { attr = "phasorpy.version.__version__" }
[tool.setuptools.package-data]
phasorpy = ["py.typed"]

[tool.ruff.lint]
select = ["NPY201"]

[tool.pylint.format]
max-line-length = 79
max-module-lines = 2500
Expand Down Expand Up @@ -140,7 +143,7 @@ directory = "_htmlcov"
minversion = "7"
log_cli_level = "INFO"
xfail_strict = true
addopts = "-ra --strict-config --strict-markers --cov=phasorpy --cov-report html --doctest-modules --doctest-glob=*.py --doctest-glob=*.rst"
addopts = "-rfEXs --strict-config --strict-markers --cov=phasorpy --cov-report html --doctest-modules --doctest-glob=*.py --doctest-glob=*.rst"
doctest_optionflags = [
"NORMALIZE_WHITESPACE",
"ELLIPSIS",
Expand All @@ -156,7 +159,7 @@ norecursedirs = [
".pytest_cache",
"adhoc",
"build",
"docs",
"docs/_build",
"fixture",
"htmlcov",
"_htmlcov",
Expand Down
11 changes: 9 additions & 2 deletions src/phasorpy/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,16 @@ def main() -> int:


@main.command(help='Show runtime versions.')
def versions():
@click.option(
'--verbose',
default=False,
is_flag=True,
type=click.BOOL,
help='Show module paths.',
)
def versions(verbose):
"""Versions command group."""
click.echo(version.versions())
click.echo(version.versions(verbose=verbose))


@main.command(help='Fetch sample files from remote repositories.')
Expand Down
4 changes: 2 additions & 2 deletions src/phasorpy/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ def two_fractions_from_phasor(
real, imag, real_components, imag_components
)
distances_to_first_component = numpy.hypot(
(numpy.array(projected_real) - first_component_phasor[0]),
(numpy.array(projected_imag) - first_component_phasor[1]),
numpy.asarray(projected_real) - first_component_phasor[0],
numpy.asarray(projected_imag) - first_component_phasor[1],
)
fraction_of_second_component = (
distances_to_first_component / total_distance_between_components
Expand Down
4 changes: 4 additions & 0 deletions src/phasorpy/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@

from .datasets import fetch

# numpy 2.0 changed the scalar type representation,
# causing many doctests to fail.
numpy.set_printoptions(legacy='1.21')


@pytest.fixture(autouse=True)
def add_doctest_namespace(doctest_namespace):
Expand Down
2 changes: 1 addition & 1 deletion src/phasorpy/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,7 @@ def read_sdt(

# TODO: get spatial coordinates from scanner settings?
metadata = _metadata('QYXH'[-data.ndim :], data.shape, filename, H=times)
metadata['attrs']['frequency'] = 1e-6 / (times[-1] + times[1])
metadata['attrs']['frequency'] = 1e-6 / float(times[-1] + times[1])
return DataArray(data, **metadata)


Expand Down
36 changes: 19 additions & 17 deletions src/phasorpy/phasor.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,15 +218,15 @@ def phasor_from_signal(
(1.1, 0.0, 0.0)
"""
signal = numpy.array(signal, order='C', ndmin=1, copy=False)
samples = signal.shape[axis] # this also verifies axis
signal = numpy.asarray(signal, order='C')
samples = numpy.size(signal, axis) # this also verifies axis and ndim >= 1

if sample_phase is not None:
if harmonic is not None:
raise ValueError('sample_phase cannot be used with harmonic')
harmonics = [1] # value not used
sample_phase = numpy.array(
sample_phase, dtype=numpy.float64, copy=False, ndmin=1
sample_phase = numpy.atleast_1d(
numpy.asarray(sample_phase, dtype=numpy.float64)
)
if sample_phase.ndim != 1 or sample_phase.size != samples:
raise ValueError(f'{sample_phase.shape=} != ({samples},)')
Expand All @@ -237,7 +237,7 @@ def phasor_from_signal(
elif isinstance(harmonic, int):
harmonics = [harmonic]
else:
a = numpy.array(harmonic, ndmin=1)
a = numpy.atleast_1d(numpy.asarray(harmonic))
if a.dtype.kind not in 'iu' or a.ndim != 1:
raise TypeError(f'invalid {harmonic=} type')
harmonics = a.tolist()
Expand Down Expand Up @@ -358,7 +358,7 @@ def phasor_from_signal_fft(
(1.1, array([0.5, 0.0]), array([0.5, -0]))
"""
signal = numpy.array(signal, copy=False, ndmin=1)
signal = numpy.asarray(signal)
samples = numpy.size(signal, axis)
if samples < 3:
raise ValueError(f'not enough {samples=} along {axis=}')
Expand All @@ -372,7 +372,7 @@ def phasor_from_signal_fft(
f'harmonic={harmonic} out of range 1..{max_harmonic}'
)
else:
a = numpy.array(harmonic)
a = numpy.atleast_1d(numpy.asarray(harmonic))
if a.dtype.kind not in 'iu' or a.ndim != 1:
raise TypeError(f'invalid {harmonic=} type')
if numpy.any(a < 1) or numpy.any(a > max_harmonic):
Expand Down Expand Up @@ -986,7 +986,7 @@ def phasor_to_apparent_lifetime(
(array([inf, 0]), array([inf, 0]))
"""
omega = numpy.array(frequency, dtype=numpy.float64, copy=True)
omega = numpy.array(frequency, dtype=numpy.float64) # makes copy
omega *= math.pi * 2.0 * unit_conversion
return _phasor_to_apparent_lifetime(real, imag, omega, **kwargs)

Expand Down Expand Up @@ -1068,7 +1068,7 @@ def phasor_from_apparent_lifetime(
(array([1, 0.0]), array([0, 0.0]))
"""
omega = numpy.array(frequency, dtype=numpy.float64, copy=True)
omega = numpy.array(frequency, dtype=numpy.float64) # makes copy
omega *= math.pi * 2.0 * unit_conversion
if modulation_lifetime is None:
return _phasor_from_single_lifetime(phase_lifetime, omega, **kwargs)
Expand Down Expand Up @@ -1211,7 +1211,7 @@ def fraction_to_amplitude(
array([0.2, 0.2])
"""
t = numpy.array(fraction, copy=True, dtype=numpy.float64)
t = numpy.array(fraction, dtype=numpy.float64) # makes copy
t /= numpy.sum(t, axis=axis, keepdims=True)
numpy.true_divide(t, lifetime, out=t)
return t
Expand Down Expand Up @@ -1419,10 +1419,10 @@ def phasor_from_lifetime(
"""
if unit_conversion < 1e-16:
raise ValueError(f'{unit_conversion=} < 1e-16')
frequency = numpy.array(frequency, dtype=numpy.float64, ndmin=1)
frequency = numpy.atleast_1d(numpy.asarray(frequency, dtype=numpy.float64))
if frequency.ndim != 1:
raise ValueError('frequency is not one-dimensional array')
lifetime = numpy.array(lifetime, dtype=numpy.float64, ndmin=1)
lifetime = numpy.atleast_1d(numpy.asarray(lifetime, dtype=numpy.float64))
if lifetime.ndim > 2:
raise ValueError('lifetime must be one- or two-dimensional array')

Expand All @@ -1435,7 +1435,9 @@ def phasor_from_lifetime(
lifetime = lifetime.reshape(-1, 1) # move components to last axis
fraction = numpy.ones_like(lifetime) # not really used
else:
fraction = numpy.array(fraction, dtype=numpy.float64, ndmin=1)
fraction = numpy.atleast_1d(
numpy.asarray(fraction, dtype=numpy.float64)
)
if fraction.ndim > 2:
raise ValueError('fraction must be one- or two-dimensional array')

Expand Down Expand Up @@ -1541,7 +1543,7 @@ def polar_to_apparent_lifetime(
(array([1.989, 1.989]), array([1.989, 2.411]))
"""
omega = numpy.array(frequency, dtype=numpy.float64, copy=True)
omega = numpy.array(frequency, dtype=numpy.float64) # makes copy
omega *= math.pi * 2.0 * unit_conversion
return _polar_to_apparent_lifetime(phase, modulation, omega, **kwargs)

Expand Down Expand Up @@ -1611,7 +1613,7 @@ def polar_from_apparent_lifetime(
(array([0.7854, 0.7854]), array([0.7071, 0.6364]))
"""
omega = numpy.array(frequency, dtype=numpy.float64, copy=True)
omega = numpy.array(frequency, dtype=numpy.float64) # makes copy
omega *= math.pi * 2.0 * unit_conversion
if modulation_lifetime is None:
return _polar_from_single_lifetime(phase_lifetime, omega, **kwargs)
Expand Down Expand Up @@ -1702,7 +1704,7 @@ def phasor_from_fret_donor(
(array([0.1766, 0.2737, 0.1466]), array([0.3626, 0.4134, 0.2534]))
"""
omega = numpy.array(frequency, dtype=numpy.float64, copy=True)
omega = numpy.array(frequency, dtype=numpy.float64) # makes copy
omega *= math.pi * 2.0 * unit_conversion
return _phasor_from_fret_donor(
omega,
Expand Down Expand Up @@ -1820,7 +1822,7 @@ def phasor_from_fret_acceptor(
(array([0.1996, 0.05772, 0.2867]), array([0.3225, 0.3103, 0.4292]))
"""
omega = numpy.array(frequency, dtype=numpy.float64, copy=True)
omega = numpy.array(frequency, dtype=numpy.float64) # makes copy
omega *= math.pi * 2.0 * unit_conversion
return _phasor_from_fret_acceptor(
omega,
Expand Down
5 changes: 4 additions & 1 deletion src/phasorpy/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,10 @@ def plot(
i,
(re, im),
) in enumerate(
zip(numpy.array(real, ndmin=2), numpy.array(imag, ndmin=2))
zip(
numpy.atleast_2d(numpy.asarray(real)),
numpy.atleast_2d(numpy.asarray(imag)),
)
):
lbl = None
if label is not None:
Expand Down
60 changes: 34 additions & 26 deletions src/phasorpy/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,59 +5,67 @@
__version__ = '0.1.dev'


def versions(*, sep: str = '\n') -> str:
def versions(
*, sep: str = '\n', dash: str = '-', verbose: bool = False
) -> str:
"""Return versions of installed packages that phasorpy depends on.
Parameters
----------
sep : str, optional
Separator between version items. The default is a newline character.
dash : str, optional
Separator between module name and version.
verbose : bool, optional
Include paths to Python interpreter and modules.
Example
-------
>>> print(versions())
Python 3...
phasorpy 0...
numpy 1...
Python-3...
phasorpy-0...
numpy-...
...
"""
import os
import sys

try:
path = os.path.dirname(__file__)
except NameError:
path = ''

version_strings = [
f'Python {sys.version} ({sys.executable})',
f'phasorpy {__version__} ({path})',
]
if verbose:
version_strings = [f'Python{dash}{sys.version} ({sys.executable})']
else:
version_strings = [f'Python{dash}{sys.version.split()[0]}']

for module in (
'phasorpy',
'numpy',
'matplotlib',
'click',
'pooch',
'tqdm',
'xarray',
'tifffile',
'imagecodecs',
'lfdfiles',
'sdtfile',
'ptufile',
# 'scipy',
# 'skimage',
# 'sklearn',
# 'aicsimageio',
'matplotlib',
'scipy',
'skimage',
'sklearn',
'pandas',
'xarray',
'click',
'pooch',
):
try:
__import__(module)
except ModuleNotFoundError:
version_strings.append(f'{module.lower()} N/A')
version_strings.append(f'{module.lower()}{dash}n/a')
continue
lib = sys.modules[module]
version_strings.append(
f"{module.lower()} {getattr(lib, '__version__', 'unknown')}"
)
ver = f"{module.lower()}{dash}{getattr(lib, '__version__', 'unknown')}"
if verbose:
try:
path = getattr(lib, '__file__')
except NameError:
pass
else:
ver += f' ({os.path.dirname(path)})'
version_strings.append(ver)
return sep.join(version_strings)
21 changes: 21 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Pytest configuration."""

import os

import phasorpy
from phasorpy import versions
from phasorpy.utils import number_threads


def pytest_report_header(config, start_path, startdir):
"""Return versions of relevant installed packages."""
return '\n'.join(
(
f'versions: {versions(sep=", ")}',
f'number_threads: {number_threads(0)}',
f'path: {os.path.dirname(phasorpy.__file__)}',
)
)


collect_ignore = ['data']
4 changes: 2 additions & 2 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ def test_version():
def test_versions():
"""Test ``python -m phasorpy versions``."""
runner = CliRunner()
result = runner.invoke(main, ['versions'])
result = runner.invoke(main, ['versions', '--verbose'])
assert result.exit_code == 0
assert result.output.strip() == versions()
assert result.output.strip() == versions(verbose=True)


def test_fetch():
Expand Down
8 changes: 6 additions & 2 deletions tests/test_phasorpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,9 @@
def test_versions():
"""Test phasorpy.versions function."""
ver = versions()
assert f'phasorpy {__version__}' in ver
assert 'numpy' in ver
assert 'Python-' in ver
assert f'phasorpy-{__version__}\nnumpy-' in ver
assert '(' not in ver

ver = versions(sep=', ', dash=' ', verbose=True)
assert f', phasorpy {__version__} (' in ver
Loading

0 comments on commit b03f635

Please sign in to comment.