Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support NumPy 2 #61

Merged
merged 4 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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