From 42337f87f6413efbc80f12f8d8e51103877d8c83 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 05:38:26 +0000 Subject: [PATCH 1/2] Support Python 3.11-3.14 and NumPy 2 Fixes installation on modern Python (iancze/Starfish#160): - Migrate packaging metadata from setup.py to PEP 621 pyproject.toml with requires-python >=3.11 (was capped at <3.11) - Drop the obsolete dataclasses backport and the abandoned nptyping dependency (replaced by numpy.typing.NDArray annotations) - Relax dependency upper caps (astropy, numpy, scipy, scikit-learn, extinction, h5py, flatdict, toml, tqdm) so modern wheels are used - Replace np.trapz with scipy.integrate.trapezoid and np.Inf with np.inf for NumPy 2 compatibility (works with NumPy 1.24+ too) - Replace eval() with getattr() for extinction law lookup - Fix matplotlib "seaborn" style name removed in matplotlib 3.6 - Remove dead "samplers" entry from Starfish.__all__ - Update CI matrix to 3.11-3.14, bump stale GitHub Action versions, drop unmaintained pytest-black, build releases with python -m build - Fix Sphinx 4+ add_css_file API in docs/conf.py, unpin docs toolchain, migrate .readthedocs.yml to the current schema Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01W1cwqRt8tE5LDgfxmipTBk --- .github/workflows/ci.yml | 12 +++--- .github/workflows/deploy.yml | 12 +++--- .readthedocs.yml | 6 ++- CHANGELOG.md | 7 +++ Pipfile | 10 ++--- Starfish/__init__.py | 1 - Starfish/emulator/emulator.py | 44 +++++++++---------- Starfish/emulator/plotting.py | 5 ++- Starfish/grid_tools/interfaces.py | 9 ++-- Starfish/models/utils.py | 6 +-- Starfish/spectrum.py | 10 ++--- Starfish/transforms.py | 9 ++-- docs/conf.py | 2 +- pyproject.toml | 59 ++++++++++++++++++++++++- setup.py | 72 ------------------------------- 15 files changed, 130 insertions(+), 134 deletions(-) delete mode 100644 setup.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b583721b..cb01c4a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: strategy: fail-fast: false matrix: - version: ["3.7", "3.8", "3.9", "3.10"] + version: ["3.11", "3.12", "3.13", "3.14"] os: - ubuntu-latest - macOS-latest @@ -22,9 +22,9 @@ jobs: arch: ["x64"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup python ${{ matrix.version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.version }} architecture: ${{ matrix.arch }} @@ -33,14 +33,14 @@ jobs: pip install -U pip pip install .[test] - name: Cache Test Data - uses: actions/cache@v1 + uses: actions/cache@v4 with: path: tests/data key: ${{ runner.os }}-test-data - name: Test run: | - pytest --cov=Starfish --cov-report=xml --benchmark-skip --black + pytest --cov=Starfish --cov-report=xml --benchmark-skip - name: Coverage - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 08624e14..f22ae935 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -8,20 +8,20 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v1 + uses: actions/setup-python@v5 with: - python-version: "3.9" + python-version: "3.12" - name: Install dependencies run: | python -m pip install -U pip - pip install -U pep517 twine + pip install -U build twine - name: Build run: | - python -m pep517.build --source --binary --out-dir dist/ . + python -m build --outdir dist/ . - name: Deploy - uses: pypa/gh-action-pypi-publish@master + uses: pypa/gh-action-pypi-publish@release/v1 with: user: ${{ secrets.PYPI_USER }} password: ${{ secrets.PYPI_PASSWORD }} diff --git a/.readthedocs.yml b/.readthedocs.yml index 8257f2d9..e98e27aa 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,13 +1,17 @@ # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details version: 2 +build: + os: ubuntu-24.04 + tools: + python: "3.12" + sphinx: configuration: docs/conf.py formats: all python: - version: 3.7 install: - method: pip path: . diff --git a/CHANGELOG.md b/CHANGELOG.md index f9e067c0..c1fc20d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,13 @@ ### Changed - Minor refactoring +- Support Python 3.11 through 3.14; dropped support for Python 3.10 and earlier +- Packaging metadata moved from `setup.py` to PEP 621 `pyproject.toml` +- Removed the `nptyping` dependency in favor of `numpy.typing`, and the obsolete + `dataclasses` backport; relaxed upper version caps on `astropy`, `numpy`, + `scipy`, `scikit-learn`, `extinction`, `h5py`, `flatdict`, `toml`, and `tqdm` +- Replaced `np.trapz` with `scipy.integrate.trapezoid` and `np.Inf` with + `np.inf` for NumPy 2 compatibility ### Fixed - A bug in how fix_c0 is toggled during Chebyshev polynomial optimization diff --git a/Pipfile b/Pipfile index 14fb66fa..ad436924 100644 --- a/Pipfile +++ b/Pipfile @@ -1,22 +1,19 @@ [requires] -python_version = "3.7" +python_version = "3.11" [packages] astropy = "*" -dataclasses = "*" flatdict = "*" h5py = "*" -nptyping = "*" numpy = "*" scikit-learn = "*" scipy = "*" -toml = ">=0.10.1" +toml = ">=0.10.2" tqdm = "*" -extinction = ">=0.4.2" +extinction = ">=0.4.7" [dev-packages] black = "*" -coveralls = "*" flake8 = "*" ipython = "*" mypy = "*" @@ -29,7 +26,6 @@ sphinx-bootstrap-theme = "*" pytest-benchmark = "*" sphinx-autodoc-typehints = "*" pre-commit = "*" -pytest-black = "*" [pipenv] allow_prereleases = true diff --git a/Starfish/__init__.py b/Starfish/__init__.py index 2e6f331e..7ce2b9e9 100644 --- a/Starfish/__init__.py +++ b/Starfish/__init__.py @@ -7,7 +7,6 @@ "emulator", "grid_tools", "models", - "samplers", "spectrum", "Spectrum", "transforms", diff --git a/Starfish/emulator/emulator.py b/Starfish/emulator/emulator.py index d9e53090..40cd31df 100644 --- a/Starfish/emulator/emulator.py +++ b/Starfish/emulator/emulator.py @@ -5,7 +5,7 @@ import h5py import numpy as np -from nptyping import NDArray +from numpy.typing import NDArray from scipy.interpolate import LinearNDInterpolator from scipy.linalg import cho_factor, cho_solve from scipy.optimize import minimize @@ -68,18 +68,18 @@ class Emulator: def __init__( self, - grid_points: NDArray[float], + grid_points: NDArray[np.float64], param_names: Sequence[str], - wavelength: NDArray[float], - weights: NDArray[float], - eigenspectra: NDArray[float], - w_hat: NDArray[float], - flux_mean: NDArray[float], - flux_std: NDArray[float], - factors: NDArray[float], + wavelength: NDArray[np.float64], + weights: NDArray[np.float64], + eigenspectra: NDArray[np.float64], + w_hat: NDArray[np.float64], + flux_mean: NDArray[np.float64], + flux_std: NDArray[np.float64], + factors: NDArray[np.float64], lambda_xi: float = 1.0, - variances: Optional[NDArray[float]] = None, - lengthscales: Optional[NDArray[float]] = None, + variances: Optional[NDArray[np.float64]] = None, + lengthscales: Optional[NDArray[np.float64]] = None, name: Optional[str] = None, ): self.log = logging.getLogger(self.__class__.__name__) @@ -144,7 +144,7 @@ def lambda_xi(self, value: float): self.hyperparams["log_lambda_xi"] = np.log(value) @property - def variances(self) -> NDArray[float]: + def variances(self) -> NDArray[np.float64]: """ numpy.ndarray : The variances for each Gaussian process kernel. @@ -158,12 +158,12 @@ def variances(self) -> NDArray[float]: return np.exp(values) @variances.setter - def variances(self, values: NDArray[float]): + def variances(self, values: NDArray[np.float64]): for i, value in enumerate(values): self.hyperparams[f"log_variance:{i}"] = np.log(value) @property - def lengthscales(self) -> NDArray[float]: + def lengthscales(self) -> NDArray[np.float64]: """ numpy.ndarray : The lengthscales for each Gaussian process kernel. @@ -177,7 +177,7 @@ def lengthscales(self) -> NDArray[float]: return np.exp(values).reshape(self.ncomps, -1) @lengthscales.setter - def lengthscales(self, values: NDArray[float]): + def lengthscales(self, values: NDArray[np.float64]): for i, value in enumerate(values): for j, ls in enumerate(value): self.hyperparams[f"log_lengthscale:{i}:{j}"] = np.log(ls) @@ -332,7 +332,7 @@ def __call__( params: Sequence[float], full_cov: bool = True, reinterpret_batch: bool = False, - ) -> Tuple[NDArray[float], NDArray[float]]: + ) -> Tuple[NDArray[np.float64], NDArray[np.float64]]: """ Gets the mu and cov matrix for a given set of params @@ -394,7 +394,7 @@ def __call__( return mu, cov @property - def bulk_fluxes(self) -> NDArray[float]: + def bulk_fluxes(self) -> NDArray[np.float64]: """ numpy.ndarray: A vertically concatenated vector of the eigenspectra, flux_mean, and flux_std (in that order). Used for bulk processing with the emulator. @@ -402,8 +402,8 @@ def bulk_fluxes(self) -> NDArray[float]: return np.vstack([self.eigenspectra, self.flux_mean, self.flux_std]) def load_flux( - self, params: Union[Sequence[float], NDArray[float]], norm=False - ) -> NDArray[float]: + self, params: Union[Sequence[float], NDArray[np.float64]], norm=False + ) -> NDArray[np.float64]: """ Interpolate a model given any parameters within the grid's parameter range using eigenspectrum reconstruction @@ -426,7 +426,7 @@ def load_flux( flux *= self.norm_factor(params)[:, np.newaxis] return np.squeeze(flux) - def norm_factor(self, params: Union[Sequence[float], NDArray[float]]) -> float: + def norm_factor(self, params: Union[Sequence[float], NDArray[np.float64]]) -> float: """ Return the scaling factor for the absolute flux units in flux-normalized spectra @@ -570,7 +570,7 @@ def set_param_dict(self, params: dict): self.grid_points, self.grid_points, self.variances, self.lengthscales ) - def get_param_vector(self) -> NDArray[float]: + def get_param_vector(self) -> NDArray[np.float64]: """ Get a vector of the current trainable parameters of the emulator @@ -581,7 +581,7 @@ def get_param_vector(self) -> NDArray[float]: values = list(self.get_param_dict().values()) return np.array(values) - def set_param_vector(self, params: NDArray[float]): + def set_param_vector(self, params: NDArray[np.float64]): """ Set the current trainable parameters given a vector. Must have the same form as :meth:`get_param_vector` diff --git a/Starfish/emulator/plotting.py b/Starfish/emulator/plotting.py index 24a1b92b..8dee6b10 100644 --- a/Starfish/emulator/plotting.py +++ b/Starfish/emulator/plotting.py @@ -4,7 +4,10 @@ from matplotlib import gridspec import numpy as np -plt.style.use("seaborn") +try: + plt.style.use("seaborn-v0_8") +except OSError: + pass def plot_reconstructed(emulator, grid, folder): diff --git a/Starfish/grid_tools/interfaces.py b/Starfish/grid_tools/interfaces.py index 401ebec9..69c81bab 100644 --- a/Starfish/grid_tools/interfaces.py +++ b/Starfish/grid_tools/interfaces.py @@ -4,6 +4,7 @@ import numpy as np from astropy.io import fits, ascii +from scipy.integrate import trapezoid from scipy.interpolate import InterpolatedUnivariateSpline import Starfish.constants as C @@ -138,7 +139,7 @@ def load_flux(self, parameters, header=False, norm=True): # If we want to normalize the spectra, we must do it now since later we won't have the full EM range if norm: flux *= 1e-8 # convert from erg/cm^2/s/cm to erg/cm^2/s/A - F_bol = np.trapz(flux, self.wl_full) + F_bol = trapezoid(flux, self.wl_full) # bolometric luminosity is always 1 L_sun flux *= C.F_sun / F_bol @@ -393,7 +394,7 @@ def load_flux(self, parameters, norm=True): fl = fl[ind] if norm: - F_bol = np.trapz(fl, wl) + F_bol = trapezoid(fl, wl) fl = fl * (C.F_sun / F_bol) # the bolometric luminosity is always 1 L_sun @@ -487,7 +488,7 @@ def load_flux(self, parameters, header=False, norm=True): fl = fl[ind] if norm: - F_bol = np.trapz(fl, wl) + F_bol = trapezoid(fl, wl) fl = fl * (C.F_sun / F_bol) # the bolometric luminosity is always 1 L_sun @@ -538,7 +539,7 @@ def load_BTSettl(T, logg, Z, norm=False, trunc=False, air=False): fl = 10 ** (fl - 8.0) # now in ergs/cm^2/s/A if norm: - F_bol = np.trapz(fl, wl) + F_bol = trapezoid(fl, wl) fl = fl * (C.F_sun / F_bol) # this also means that the bolometric luminosity is always 1 L_sun diff --git a/Starfish/models/utils.py b/Starfish/models/utils.py index dc6aa9ad..71969ee9 100644 --- a/Starfish/models/utils.py +++ b/Starfish/models/utils.py @@ -1,7 +1,7 @@ import logging import numpy as np -from nptyping import NDArray +from numpy.typing import NDArray from scipy.optimize import minimize import scipy.stats as st @@ -105,7 +105,7 @@ def chi2(P, wave, resid, sigma): _amp = np.exp(log_amp) _sigma = np.exp(log_sigma) if _amp == 0 or _sigma == 0: - return np.Inf + return np.inf # Logistic prior for the widths out to 2sigma prior = st.uniform.logpdf(_sigma, 0, 2 * sigma0) # Put prior on widths and heights such that the integrated area should be less @@ -151,7 +151,7 @@ def chi2(P, wave, resid, sigma): log = logging.getLogger(__name__) -def covariance_debugger(cov: NDArray[float]): +def covariance_debugger(cov: NDArray[np.float64]): """ Special debugging information for the covariance matrix decomposition. """ diff --git a/Starfish/spectrum.py b/Starfish/spectrum.py index afaaa3a4..578c8dbe 100644 --- a/Starfish/spectrum.py +++ b/Starfish/spectrum.py @@ -1,7 +1,7 @@ import h5py import numpy as np from dataclasses import dataclass -from nptyping import NDArray +from numpy.typing import NDArray from typing import Optional @@ -26,10 +26,10 @@ class Order: name : str """ - _wave: NDArray[float] - _flux: NDArray[float] - _sigma: Optional[NDArray[float]] = None - mask: Optional[NDArray[bool]] = None + _wave: NDArray[np.float64] + _flux: NDArray[np.float64] + _sigma: Optional[NDArray[np.float64]] = None + mask: Optional[NDArray[np.bool_]] = None def __post_init__(self): if self._sigma is None: diff --git a/Starfish/transforms.py b/Starfish/transforms.py index 23adabef..44a376d8 100644 --- a/Starfish/transforms.py +++ b/Starfish/transforms.py @@ -1,6 +1,7 @@ -import extinction # This may be marked as unused, but is necessary +import extinction import numpy as np from numpy.polynomial.chebyshev import chebval +from scipy.integrate import trapezoid from scipy.interpolate import InterpolatedUnivariateSpline from scipy.special import j1 @@ -197,7 +198,7 @@ def extinct(wave, flux, Av, Rv=3.1, law="ccm89"): if Rv <= 0: raise ValueError("Rv must be positive") - law_fn = eval("extinction.{}".format(law)) + law_fn = getattr(extinction, law) if law == "fm07": A_l = law_fn(wave.astype(np.double), Av) else: @@ -263,8 +264,8 @@ def renorm(wave, flux, reference_flux): def _get_renorm_factor(wave, flux, reference_flux): - ref_int = np.trapz(reference_flux, wave) - flux_int = np.trapz(flux, wave, axis=-1) + ref_int = trapezoid(reference_flux, wave) + flux_int = trapezoid(flux, wave, axis=-1) return ref_int / flux_int diff --git a/docs/conf.py b/docs/conf.py index 201b13d1..1a4bc862 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -118,7 +118,7 @@ def setup(app): - app.add_stylesheet("overrides.css") + app.add_css_file("overrides.css") # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, diff --git a/pyproject.toml b/pyproject.toml index bf912968..7d3ff2c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,60 @@ [build-system] build-backend = "setuptools.build_meta" -requires = ["setuptools", "wheel"] +requires = ["setuptools>=77"] + +[project] +name = "astrostarfish" +description = "Covariance tools for fitting stellar spectra" +readme = "README.md" +requires-python = ">=3.11" +license = "BSD-4-Clause" +authors = [{ name = "Ian Czekala", email = "iancze@gmail.com" }] +maintainers = [{ name = "Miles Lucas", email = "mdlucas@hawaii.edu" }] +keywords = ["Science", "Astronomy", "Physics", "Data Science"] +classifiers = [ + "Intended Audience :: Science/Research", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: Scientific/Engineering :: Astronomy", + "Topic :: Scientific/Engineering :: Physics", +] +dependencies = [ + "astropy>=5.3", + "extinction>=0.4.7", + "flatdict>=4.0", + "h5py>=3.8", + "numpy>=1.24", + "scikit-learn>=1.2", + "scipy>=1.10", + "toml>=0.10.2", + "tqdm>=4.60", +] +dynamic = ["version"] + +[project.urls] +repository = "https://github.com/iancze/Starfish" +documentation = "https://starfish.rtfd.io" + +[project.optional-dependencies] +test = [ + "pytest>=8", + "pytest-benchmark>=4", + "pytest-cov>=5", +] +docs = [ + "IPython", + "nbsphinx>=0.9", + "pandoc", + "sphinx>=7", + "sphinx-autodoc-typehints>=1.25", + "sphinx-bootstrap-theme>=0.8", +] + +[tool.setuptools.dynamic] +version = { attr = "Starfish.__version__" } + +[tool.setuptools.packages.find] +include = ["Starfish*"] diff --git a/setup.py b/setup.py deleted file mode 100644 index a50001ba..00000000 --- a/setup.py +++ /dev/null @@ -1,72 +0,0 @@ -# -*- coding: utf-8 -*- -from setuptools import setup, find_packages -import os -import re - -version = "" - -with open(os.path.join("Starfish", "__init__.py"), "r") as fh: - for line in fh.readlines(): - m = re.search("__version__ = [\"'](.+)[\"']", line) - if m: - version = m.group(1) - - -with open("README.md", "r") as fh: - readme = fh.read() - -setup( - long_description=readme, - long_description_content_type="text/markdown", - name="astrostarfish", - version=version, - description="Covariance tools for fitting stellar spectra", - python_requires=">=3.7,<3.11", - project_urls={ - "repository": "https://github.com/iancze/Starfish", - "documentation": "https://starfish.rtfd.io", - }, - author="Ian Czekala", - author_email="iancze@gmail.com", - maintainer="Miles Lucas ", - license="BSD-4-Clause", - keywords="Science Astronomy Physics Data Science", - classifiers=[ - "Intended Audience :: Science/Research", - "Programming Language :: Python :: 3", - "Topic :: Scientific/Engineering :: Astronomy", - "Topic :: Scientific/Engineering :: Physics", - ], - packages=find_packages(exclude=["tests", "*.tests", "*.tests.*", "tests.*"]), - package_data={}, - install_requires=[ - "astropy>=4,<6", - "dataclasses>=0.6,<0.7", - "extinction==0.4.*", - "flatdict==4.*", - "h5py==3.*", - "nptyping==1.*", - "numpy>=1.16,<2", - "scikit-learn>=0.22,<2", - "scipy>=1.3.0,<2", - "toml>=0.10.1,<0.11", - "tqdm==4.*", - ], - extras_require={ - "docs": [ - "nbsphinx==0.*,>=0.4.2", - "pandoc==2.*", - "sphinx==2.*", - "sphinx-bootstrap-theme==0.*,>=0.7.1", - "IPython", - "sphinx-autodoc-typehints==1.10.3", - ], - "test": [ - "coveralls==1.*,>=1.8.0", - "pytest==4.*,>=4.6.0", - "pytest-black", - "pytest-cov==2.*,>=2.7.0", - "pytest-benchmark", - ], - }, -) From 28b26279c9b3d17c1500e9d75ac12759842d1fc2 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 16 Jul 2026 05:46:45 +0000 Subject: [PATCH 2/2] Fix ragged asarray in test_no_par_truncation and invalid escapes np.asarray() on the PHOENIX grid's ragged points list (58 Teff, 12 logg, 9 Z values) raises ValueError on numpy>=1.24; iterate the list directly instead. Also raw-string two matplotlib labels that trigger SyntaxWarning on Python 3.14. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01W1cwqRt8tE5LDgfxmipTBk --- Starfish/emulator/plotting.py | 4 ++-- tests/test_grid_tools/test_hdf5.py | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Starfish/emulator/plotting.py b/Starfish/emulator/plotting.py index 8dee6b10..afe6ae84 100644 --- a/Starfish/emulator/plotting.py +++ b/Starfish/emulator/plotting.py @@ -93,7 +93,7 @@ def plot_eigenspectra(emulator, params, filename=None): ) ax = plt.subplot(gs[0]) ax.plot(emulator.wl, reconstructed, lw=1) - ax.set_ylabel("$f_\lambda$ [erg/cm^2/s/A]") + ax.set_ylabel(r"$f_\lambda$ [erg/cm^2/s/A]") plt.setp(ax.get_xticklabels(), visible=False) for i in range(emulator.ncomps): ax = plt.subplot(gs[i + 1], sharex=ax) @@ -152,6 +152,6 @@ def plot_emulator(emulator): axes[i].fill_between(Ttest, m - 2 * s, m + 2 * s, color="C1", alpha=0.4) axes[-1].set_xlabel("T (K)") plt.suptitle( - f"Weights for fixed $\log g={logg[0]:.2f}$, $[Fe/H]={Z[0]:.2f}$", fontsize=20 + rf"Weights for fixed $\log g={logg[0]:.2f}$, $[Fe/H]={Z[0]:.2f}$", fontsize=20 ) plt.tight_layout(rect=[0, 0.03, 1, 0.95]) diff --git a/tests/test_grid_tools/test_hdf5.py b/tests/test_grid_tools/test_hdf5.py index f35ddc71..a6ac8785 100644 --- a/tests/test_grid_tools/test_hdf5.py +++ b/tests/test_grid_tools/test_hdf5.py @@ -149,9 +149,7 @@ def test_no_par_truncation( instrument=mock_instrument, ranges=None, ) - for cpars, gpars in zip( - creator.points, np.asarray(mock_no_alpha_grid.points).T - ): + for cpars, gpars in zip(creator.points, mock_no_alpha_grid.points): np.testing.assert_allclose(cpars, np.unique(gpars))