diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ab30e06a..f0695b8f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,109 +10,61 @@ on: jobs: tests: - name: ${{ matrix.name }} + name: Python ${{ matrix.python-version }} on ${{ matrix.os }} with numpy ${{ matrix.numpy-version }}, no gsl ${{ matrix.gala-nogsl }}, deps ${{ matrix.pip-test-deps }} runs-on: ${{ matrix.os }} if: github.event.pull_request.draft == false && !contains(github.event.pull_request.labels.*.name, 'docs only') strategy: fail-fast: true matrix: + python-version: ["3.10", "3.11", "3.12"] + os: ["ubuntu-latest", "macos-latest"] + numpy-version: ["latest"] + gala-nogsl: ["0"] + pip-test-deps: ["test"] include: - - name: Code style checks + - name: Oldest numpy version supported os: ubuntu-latest - python: 3.x - toxenv: codestyle + python-version: "3.11" + numpy-version: "1.24" + gala-nogsl: "0" + pip-test-deps: "test" - - name: Python 3.10 with minimal dependencies and coverage + - name: Install without GSL os: ubuntu-latest - python: '3.10' - toxenv: py310-test-cov - - - name: Python 3.9 - os: ubuntu-latest - python: '3.9' - toxenv: py39-test - - - name: Python 3.10 - os: ubuntu-latest - python: '3.10' - toxenv: py310-test - - - name: Python 3.11 - os: ubuntu-latest - python: '3.11' - toxenv: py311-test - - # Has to happen on ubuntu because galpy is finnicky on macOS - - name: Python 3.10 with all optional dependencies - os: ubuntu-latest - python: '3.10' - toxenv: py310-test-extradeps - toxposargs: --durations=50 - - - name: Python 3.10 without GSL - os: ubuntu-latest - python: '3.10' - toxenv: nogsl - - - name: Python 3.9 with oldest supported version of all dependencies - os: ubuntu-latest - python: 3.9 - toxenv: py39-test-oldestdeps - - # Mac and Windows: - - name: Python 3.10 standard tests (macOS) - os: macos-latest - python: '3.10' - toxenv: py310-test - - # - name: Python 3.9 standard tests (Windows) - # os: windows-latest - # python: 3.9 - # toxenv: py39-test + python-version: "3.11" + numpy-version: "latest" + gala-nogsl: "1" + pip-test-deps: "test" + + - name: With optional dependencies + os: ubuntu-latest # note: galpy install failed on macos here + python-version: "3.11" + numpy-version: "latest" + gala-nogsl: "0" + pip-test-deps: "test,extra" steps: - - name: Checkout code - uses: actions/checkout@v4 + - uses: actions/checkout@v4 with: fetch-depth: 0 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + # For animation tests - uses: FedericoCarboni/setup-ffmpeg@v3 if: ${{ !startsWith(matrix.os, 'mac') }} + continue-on-error: true with: # Not strictly necessary, but it may prevent rate limit # errors especially on GitHub-hosted macos machines. github-token: ${{ secrets.GITHUB_TOKEN }} + ffmpeg-version: "6.1.0" id: setup-ffmpeg - - name: Set up Python ${{ matrix.python }} on ${{ matrix.os }} - if: ${{ !startsWith(matrix.os, 'windows') }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python }} - - # Windows: - # - uses: conda-incubator/setup-miniconda@v2 - # if: startsWith(matrix.os, 'windows') - # with: - # auto-update-conda: true - # python-version: ${{ matrix.python-version }} - - # - name: Install Python dependencies - Windows - # if: startsWith(matrix.os, 'windows') - # shell: bash -l {0} - # run: | - # conda install -c conda-forge -q gsl python=3.9 libpython - # python -m pip install -e .[test] - # python -m pip install tox - - # - name: Run tests - Windows - # if: startsWith(matrix.os, 'windows') - # shell: bash -l {0} - # run: | - # tox ${{ matrix.toxargs }} -e ${{ matrix.toxenv }} ${{ matrix.toxposargs }} - # Mac: - name: Setup Mac - GSL if: startsWith(matrix.os, 'mac') @@ -127,18 +79,20 @@ jobs: sudo apt-get install gsl-bin libgsl0-dev build-essential sudo apt-get install libhdf5-serial-dev # TODO: remove when h5py has 3.11 wheels - # Any *nix: - - name: Install Python dependencies - nix - if: ${{ !startsWith(matrix.os, 'windows') }} - run: python -m pip install --upgrade tox codecov + - name: Install package and dependencies + run: python -m pip install -e ".[${{ matrix.pip-test-deps }}]" + env: + GALA_NOGSL: ${{ matrix.gala-nogsl }} - - name: Run tests - nix - if: ${{ !startsWith(matrix.os, 'windows') }} - run: tox -e ${{ matrix.toxenv }} -- ${{ matrix.toxposargs }} + - name: Update versions if testing min versions + if: matrix.numpy-version != 'latest' + run: | + python -m pip install numpy~=${{ matrix.numpy-version }} - # Coverage: - - name: Upload coverage report to codecov - uses: codecov/codecov-action@v4 - if: steps.check_files.outputs.files_exists == 'true' && runner.os == 'Linux' - with: - file: ./coverage.xml # optional + - name: Run tests + run: >- + python -m pytest -ra --cov --cov-report=xml --cov-report=term + --durations=20 . + + - name: Upload coverage report + uses: codecov/codecov-action@v4.5.0 diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 2c7b271a..8ef97e69 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -28,7 +28,7 @@ jobs: - ["1", "cp39-* cp310-*"] - ["2", "cp311-* cp312-*"] os: - - "macos-14" + # - "macos-14" - "ubuntu-latest" steps: @@ -42,12 +42,12 @@ jobs: # with: # platforms: all - # Mac: + # Mac: disable wheels on mac because of GSL issues # NOTE: need to install pipx explicitly for macos-14 - - name: Setup Mac - if: runner.os == 'macOS' - run: | - brew install gsl pipx + # - name: Setup Mac + # if: runner.os == 'macOS' + # run: | + # brew install gsl pipx # Ubuntu: - name: Setup Linux diff --git a/gala/_compat_utils.py b/gala/_compat_utils.py new file mode 100644 index 00000000..c5bf631c --- /dev/null +++ b/gala/_compat_utils.py @@ -0,0 +1,6 @@ +import numpy as np +from packaging.version import Version + +# See: https://github.com/astropy/astropy/pull/16181 +NUMPY_LT_2_0 = Version(np.__version__) < Version("2.0.0") +COPY_IF_NEEDED = False if NUMPY_LT_2_0 else None diff --git a/gala/dynamics/representation_nd.py b/gala/dynamics/representation_nd.py index 9be86472..d7dd38d4 100644 --- a/gala/dynamics/representation_nd.py +++ b/gala/dynamics/representation_nd.py @@ -6,6 +6,8 @@ import astropy.units as u import numpy as np +from gala._compat_utils import COPY_IF_NEEDED + __all__ = ["NDCartesianRepresentation", "NDCartesianDifferential"] @@ -62,7 +64,7 @@ def _apply(self, method, *args, **kwargs): apply_method = operator.methodcaller(method, *args, **kwargs) return self.__class__( [apply_method(getattr(self, component)) for component in self.components], - copy=False, + copy=COPY_IF_NEEDED, ) @@ -163,7 +165,7 @@ def get_xyz(self, xyz_axis=0): for name in self.attr_classes ] xs_value = np.concatenate(components, axis=xyz_axis) - return u.Quantity(xs_value, unit=unit, copy=False) + return u.Quantity(xs_value, unit=unit, copy=COPY_IF_NEEDED) xyz = property(get_xyz) @@ -260,6 +262,6 @@ def get_d_xyz(self, xyz_axis=0): for name in self.components ] xs_value = np.concatenate(components, axis=xyz_axis) - return u.Quantity(xs_value, unit=unit, copy=False) + return u.Quantity(xs_value, unit=unit, copy=COPY_IF_NEEDED) d_xyz = property(get_d_xyz) diff --git a/gala/potential/potential/builtin/core.py b/gala/potential/potential/builtin/core.py index 36c2448a..8889c420 100644 --- a/gala/potential/potential/builtin/core.py +++ b/gala/potential/potential/builtin/core.py @@ -5,43 +5,44 @@ try: myclassmethod = __builtins__.classmethod except AttributeError: - myclassmethod = __builtins__['classmethod'] + myclassmethod = __builtins__["classmethod"] # Third-party -from astropy.constants import G import astropy.units as u import numpy as np +from astropy.constants import G -# Project -from ..core import _potential_docstring, PotentialBase -from ..util import format_doc, sympy_wrap -from ..cpotential import CPotentialBase from gala.potential.common import PotentialParameter from gala.potential.potential.builtin.cybuiltin import ( + BurkertWrapper, + CylSplineWrapper, + FlattenedNFWWrapper, HenonHeilesWrapper, - KeplerWrapper, HernquistWrapper, IsochroneWrapper, - PlummerWrapper, JaffeWrapper, - StoneWrapper, - PowerLawCutoffWrapper, - SatohWrapper, + KeplerWrapper, KuzminWrapper, - MiyamotoNagaiWrapper, - MN3ExponentialDiskWrapper, - SphericalNFWWrapper, - FlattenedNFWWrapper, - TriaxialNFWWrapper, LeeSutoTriaxialNFWWrapper, LogarithmicWrapper, LongMuraliBarWrapper, - NullWrapper, + MiyamotoNagaiWrapper, + MN3ExponentialDiskWrapper, MultipoleWrapper, - CylSplineWrapper, - BurkertWrapper + NullWrapper, + PlummerWrapper, + PowerLawCutoffWrapper, + SatohWrapper, + SphericalNFWWrapper, + StoneWrapper, + TriaxialNFWWrapper, ) +# Project +from ..core import PotentialBase, _potential_docstring +from ..cpotential import CPotentialBase +from ..util import format_doc, sympy_wrap + __all__ = [ "NullPotential", "HenonHeilesPotential", @@ -62,7 +63,7 @@ "LongMuraliBarPotential", "MultipolePotential", "CylSplinePotential", - "BurkertPotential" + "BurkertPotential", ] @@ -70,7 +71,7 @@ def __getattr__(name): if name in __all__ and name in globals(): return globals()[name] - if not (name.startswith('MultipolePotentialLmax')): + if not (name.startswith("MultipolePotentialLmax")): raise AttributeError(f"Module {__name__!r} has no attribute {name!r}.") if name in mp_cache: @@ -78,11 +79,11 @@ def __getattr__(name): else: try: - lmax = int(name.split('Lmax')[1]) + lmax = int(name.split("Lmax")[1]) except Exception: raise ImportError("Invalid") # shouldn't ever get here! - return make_multipole_cls(lmax, timedep='TimeDependent' in name) + return make_multipole_cls(lmax, timedep="TimeDependent" in name) @format_doc(common_doc=_potential_docstring) @@ -94,6 +95,7 @@ class HenonHeilesPotential(CPotentialBase): ---------- {common_doc} """ + ndim = 2 Wrapper = HenonHeilesWrapper @@ -129,6 +131,7 @@ class KeplerPotential(CPotentialBase): Point mass value. {common_doc} """ + m = PotentialParameter("m", physical_type="mass") Wrapper = KeplerWrapper @@ -155,6 +158,7 @@ class IsochronePotential(CPotentialBase): Core concentration. {common_doc} """ + m = PotentialParameter("m", physical_type="mass") b = PotentialParameter("b", physical_type="length") @@ -166,7 +170,7 @@ def to_sympy(cls, v, p): import sympy as sy r = sy.sqrt(v["x"] ** 2 + v["y"] ** 2 + v["z"] ** 2) - expr = -p["G"] * p["m"] / (sy.sqrt(r ** 2 + p["b"] ** 2) + p["b"]) + expr = -p["G"] * p["m"] / (sy.sqrt(r**2 + p["b"] ** 2) + p["b"]) return expr, v, p def action_angle(self, w): @@ -208,6 +212,7 @@ class HernquistPotential(CPotentialBase): Core concentration. {common_doc} """ + m = PotentialParameter("m", physical_type="mass") c = PotentialParameter("c", physical_type="length") @@ -236,6 +241,7 @@ class PlummerPotential(CPotentialBase): Core concentration. {common_doc} """ + m = PotentialParameter("m", physical_type="mass") b = PotentialParameter("b", physical_type="length") @@ -247,7 +253,7 @@ def to_sympy(cls, v, p): import sympy as sy r = sy.sqrt(v["x"] ** 2 + v["y"] ** 2 + v["z"] ** 2) - expr = -p["G"] * p["m"] / sy.sqrt(r ** 2 + p["b"] ** 2) + expr = -p["G"] * p["m"] / sy.sqrt(r**2 + p["b"] ** 2) return expr, v, p @@ -264,6 +270,7 @@ class JaffePotential(CPotentialBase): Core concentration. {common_doc} """ + m = PotentialParameter("m", physical_type="mass") c = PotentialParameter("c", physical_type="length") @@ -297,6 +304,7 @@ class StonePotential(CPotentialBase): Halo radius. {common_doc} """ + m = PotentialParameter("m", physical_type="mass") r_c = PotentialParameter("r_c", physical_type="length") r_h = PotentialParameter("r_h", physical_type="length") @@ -313,9 +321,7 @@ def to_sympy(cls, v, p): expr = A * ( p["r_h"] / r * sy.atan(r / p["r_h"]) - p["r_c"] / r * sy.atan(r / p["r_c"]) - + 1.0 - / 2 - * sy.log((r ** 2 + p["r_h"] ** 2) / (r ** 2 + p["r_c"] ** 2)) + + 1.0 / 2 * sy.log((r**2 + p["r_h"] ** 2) / (r**2 + p["r_c"] ** 2)) ) return expr, v, p @@ -343,6 +349,7 @@ class PowerLawCutoffPotential(CPotentialBase, GSL_only=True): Cutoff radius. {common_doc} """ + m = PotentialParameter("m", physical_type="mass") alpha = PotentialParameter("alpha", physical_type="dimensionless") r_c = PotentialParameter("r_c", physical_type="length") @@ -364,16 +371,16 @@ def to_sympy(cls, v, p): G * alpha * m - * sy.lowergamma(3.0 / 2 - alpha / 2, r ** 2 / r_c ** 2) + * sy.lowergamma(3.0 / 2 - alpha / 2, r**2 / r_c**2) / (2 * r * sy.gamma(5.0 / 2 - alpha / 2)) + G * m - * sy.lowergamma(1 - alpha / 2, r ** 2 / r_c ** 2) + * sy.lowergamma(1 - alpha / 2, r**2 / r_c**2) / (r_c * sy.gamma(3.0 / 2 - alpha / 2)) - 3 * G * m - * sy.lowergamma(3.0 / 2 - alpha / 2, r ** 2 / r_c ** 2) + * sy.lowergamma(3.0 / 2 - alpha / 2, r**2 / r_c**2) / (2 * r * sy.gamma(5.0 / 2 - alpha / 2)) ) @@ -394,6 +401,7 @@ class BurkertPotential(CPotentialBase): The core radius. {common_doc} """ + rho = PotentialParameter("rho", physical_type="mass density") r0 = PotentialParameter("r0", physical_type="length") @@ -422,6 +430,7 @@ class SatohPotential(CPotentialBase): Scale height. {common_doc} """ + m = PotentialParameter("m", physical_type="mass") a = PotentialParameter("a", physical_type="length") b = PotentialParameter("b", physical_type="length") @@ -435,11 +444,7 @@ def to_sympy(cls, v, p): R = sy.sqrt(v["x"] ** 2 + v["y"] ** 2) z = v["z"] - term = ( - R ** 2 - + z ** 2 - + p["a"] * (p["a"] + 2 * sy.sqrt(z ** 2 + p["b"] ** 2)) - ) + term = R**2 + z**2 + p["a"] * (p["a"] + 2 * sy.sqrt(z**2 + p["b"] ** 2)) expr = -p["G"] * p["m"] / sy.sqrt(term) return expr, v, p @@ -459,6 +464,7 @@ class KuzminPotential(CPotentialBase): Flattening parameter. {common_doc} """ + m = PotentialParameter("m", physical_type="mass") a = PotentialParameter("a", physical_type="length") @@ -469,9 +475,7 @@ class KuzminPotential(CPotentialBase): def to_sympy(cls, v, p): import sympy as sy - denom = sy.sqrt( - v["x"] ** 2 + v["y"] ** 2 + (p["a"] + sy.Abs(v["z"])) ** 2 - ) + denom = sy.sqrt(v["x"] ** 2 + v["y"] ** 2 + (p["a"] + sy.Abs(v["z"])) ** 2) expr = -p["G"] * p["m"] / denom return expr, v, p @@ -495,6 +499,7 @@ class MiyamotoNagaiPotential(CPotentialBase): Scale height. {common_doc} """ + m = PotentialParameter("m", physical_type="mass") a = PotentialParameter("a", physical_type="length") b = PotentialParameter("b", physical_type="length") @@ -508,7 +513,7 @@ def to_sympy(cls, v, p): R = sy.sqrt(v["x"] ** 2 + v["y"] ** 2) z = v["z"] - term = R ** 2 + (p["a"] + sy.sqrt(z ** 2 + p["b"] ** 2)) ** 2 + term = R**2 + (p["a"] + sy.sqrt(z**2 + p["b"] ** 2)) ** 2 expr = -p["G"] * p["m"] / sy.sqrt(term) return expr, v, p @@ -582,9 +587,7 @@ def __init__( sech2_z=True, **kwargs, ): - PotentialBase.__init__( - self, *args, units=units, origin=origin, R=R, **kwargs - ) + PotentialBase.__init__(self, *args, units=units, origin=origin, R=R, **kwargs) hzR = (self.parameters["h_z"] / self.parameters["h_R"]).decompose() if positive_density: @@ -594,9 +597,9 @@ def __init__( # get b / h_R if sech2_z: - b_hR = -0.033 * hzR ** 3 + 0.262 * hzR ** 2 + 0.659 * hzR + b_hR = -0.033 * hzR**3 + 0.262 * hzR**2 + 0.659 * hzR else: - b_hR = -0.269 * hzR ** 3 + 1.08 * hzR ** 2 + 1.092 * hzR + b_hR = -0.269 * hzR**3 + 1.08 * hzR**2 + 1.092 * hzR self.positive_density = positive_density self.sech2_z = sech2_z @@ -626,10 +629,7 @@ def get_three_potentials(self): for i in range(3): name = f"disk{i+1}" pots[name] = MiyamotoNagaiPotential( - m=self._ms[i], - a=self._as[i], - b=self._b, - units=self.units + m=self._ms[i], a=self._as[i], b=self._b, units=self.units ) return pots @@ -667,6 +667,7 @@ class NFWPotential(CPotentialBase): Minor axis scale. {common_doc} """ + m = PotentialParameter("m", physical_type="mass") r_s = PotentialParameter("r_s", physical_type="length") a = PotentialParameter("a", physical_type="dimensionless", default=1.0) @@ -696,9 +697,7 @@ def to_sympy(cls, v, p): uu = ( sy.sqrt( - (v["x"] / p["a"]) ** 2 - + (v["y"] / p["b"]) ** 2 - + (v["z"] / p["c"]) ** 2 + (v["x"] / p["a"]) ** 2 + (v["y"] / p["b"]) ** 2 + (v["z"] / p["c"]) ** 2 ) / p["r_s"] ) @@ -729,9 +728,7 @@ def from_M200_c(M200, c, rho_c=None, units=None, origin=None, R=None): from astropy.cosmology import default_cosmology cosmo = default_cosmology.get() - rho_c = (3 * cosmo.H(0.0) ** 2 / (8 * np.pi * G)).to( - u.Msun / u.kpc ** 3 - ) + rho_c = (3 * cosmo.H(0.0) ** 2 / (8 * np.pi * G)).to(u.Msun / u.kpc**3) Rvir = np.cbrt(M200 / (200 * rho_c) / (4.0 / 3 * np.pi)).to(u.kpc) r_s = Rvir / c @@ -805,7 +802,7 @@ def from_circular_velocity( @staticmethod def _vc_rs_rref_to_m(v_c, r_s, r_ref): uu = r_ref / r_s - vs2 = v_c ** 2 / uu / (np.log(1 + uu) / uu ** 2 - 1 / (uu * (1 + uu))) + vs2 = v_c**2 / uu / (np.log(1 + uu) / uu**2 - 1 / (uu * (1 + uu))) return r_s * vs2 / G @@ -848,11 +845,7 @@ class LogarithmicPotential(CPotentialBase): def to_sympy(cls, v, p): import sympy as sy - r2 = ( - (v["x"] / p["q1"]) ** 2 - + (v["y"] / p["q2"]) ** 2 - + (v["z"] / p["q3"]) ** 2 - ) + r2 = (v["x"] / p["q1"]) ** 2 + (v["y"] / p["q2"]) ** 2 + (v["z"] / p["q3"]) ** 2 expr = 1.0 / 2 * p["v_c"] ** 2 * sy.log(p["r_h"] ** 2 + r2) return expr, v, p @@ -881,6 +874,7 @@ class LeeSutoTriaxialNFWPotential(CPotentialBase): Minor axis. {common_doc} """ + v_c = PotentialParameter("v_c", physical_type="speed") r_s = PotentialParameter("r_s", physical_type="length") a = PotentialParameter("a", physical_type="dimensionless", default=1.0) @@ -914,6 +908,7 @@ class LongMuraliBarPotential(CPotentialBase): Like the Miyamoto-Nagai ``c`` parameter. {common_doc} """ + m = PotentialParameter("m", physical_type="mass") a = PotentialParameter("a", physical_type="length") b = PotentialParameter("b", physical_type="length") @@ -932,14 +927,10 @@ def to_sympy(cls, v, p): z = v["z"] Tm = sy.sqrt( - (p["a"] - x) ** 2 - + y ** 2 - + (p["b"] + sy.sqrt(p["c"] ** 2 + z ** 2)) ** 2 + (p["a"] - x) ** 2 + y**2 + (p["b"] + sy.sqrt(p["c"] ** 2 + z**2)) ** 2 ) Tp = sy.sqrt( - (p["a"] + x) ** 2 - + y ** 2 - + (p["b"] + sy.sqrt(p["c"] ** 2 + z ** 2)) ** 2 + (p["a"] + x) ** 2 + y**2 + (p["b"] + sy.sqrt(p["c"] ** 2 + z**2)) ** 2 ) expr = ( @@ -968,6 +959,7 @@ class NullPotential(CPotentialBase): ---------- {common_doc} """ + Wrapper = NullWrapper @@ -993,60 +985,50 @@ def make_multipole_cls(lmax, timedep=False): # param_default = [0.] else: cls = MultipolePotential - param_default = 0. - cls_name = f'{cls.__name__}Lmax{lmax}' + param_default = 0.0 + cls_name = f"{cls.__name__}Lmax{lmax}" if cls_name in mp_cache: return mp_cache[cls_name] parameters = { - '_lmax': lmax, - 'inner': PotentialParameter('inner', default=False), - 'm': PotentialParameter('m', physical_type='mass', default=1.), - 'r_s': PotentialParameter('r_s', physical_type='length', default=1.), + "_lmax": lmax, + "inner": PotentialParameter("inner", default=False), + "m": PotentialParameter("m", physical_type="mass", default=1.0), + "r_s": PotentialParameter("r_s", physical_type="length", default=1.0), } doc_lines = [] ab_callsig = [] for l in range(lmax + 1): - for m in range(0, l+1): + for m in range(0, l + 1): if timedep: - a = f'alpha{l}{m}' - b = f'beta{l}{m}' - dtype = 'array-like' + a = f"alpha{l}{m}" + b = f"beta{l}{m}" + dtype = "array-like" else: - a = f'S{l}{m}' - b = f'T{l}{m}' - dtype = 'float' + a = f"S{l}{m}" + b = f"T{l}{m}" + dtype = "float" parameters[a] = PotentialParameter( - a, - physical_type='dimensionless', - default=param_default + a, physical_type="dimensionless", default=param_default ) parameters[b] = PotentialParameter( - b, - physical_type='dimensionless', - default=param_default + b, physical_type="dimensionless", default=param_default ) doc_lines.append(f"{a} : {dtype}\n{b} : {dtype}") ab_callsig.append(f"{a}, {b}") - ab_callsig = ', '.join(ab_callsig) + ab_callsig = ", ".join(ab_callsig) call_signature = f"{cls.__name__}(m, r_s, {ab_callsig})" - parameters['__doc__'] = ( - call_signature + cls.__doc__ + "\n".join(doc_lines) - ) + parameters["__doc__"] = call_signature + cls.__doc__ + "\n".join(doc_lines) # https://stackoverflow.com/a/58716798/623453 - parameters['__module__'] = __name__ + parameters["__module__"] = __name__ # Create a new SkyOffsetFrame subclass for this frame class. - potential_cls = type( - cls_name, - (cls, ), - parameters - ) + potential_cls = type(cls_name, (cls,), parameters) mp_cache[cls_name] = potential_cls return mp_cache[cls_name] @@ -1102,40 +1084,26 @@ class MultipolePotential(CPotentialBase, GSL_only=True): Wrapper = MultipoleWrapper - def __init__( - self, - *args, - units=None, - origin=None, - R=None, - **kwargs - ): - kwargs.pop('lmax', None) + def __init__(self, *args, units=None, origin=None, R=None, **kwargs): + kwargs.pop("lmax", None) - PotentialBase.__init__( - self, - *args, - units=units, - origin=origin, - R=R, - **kwargs) + PotentialBase.__init__(self, *args, units=units, origin=origin, R=R, **kwargs) - self._setup_wrapper({ - 'lmax': self._lmax, - 'n_coeffs': sum(range(self._lmax + 2)) - }) + self._setup_wrapper( + {"lmax": self._lmax, "n_coeffs": sum(range(self._lmax + 2))} + ) def __new__(cls, *args, **kwargs): # We don't want to call this method if we've already set up # an skyoffset frame for this class. - if not (issubclass(cls, MultipolePotential) - and cls is not MultipolePotential): + if not (issubclass(cls, MultipolePotential) and cls is not MultipolePotential): try: - lmax = kwargs['lmax'] + lmax = kwargs["lmax"] except KeyError: raise TypeError( "Can't initialize a MultipolePotential without specifying " - "the `lmax` keyword argument.") + "the `lmax` keyword argument." + ) newcls = make_multipole_cls(lmax) return newcls.__new__(newcls, *args, **kwargs) @@ -1160,9 +1128,10 @@ class CylSplinePotential(CPotentialBase): A 2D grid of potential values, evaluated at all R,z locations. {common_doc} """ - grid_R = PotentialParameter('grid_R', physical_type='length') - grid_z = PotentialParameter('grid_z', physical_type='length') - grid_Phi = PotentialParameter('grid_Phi', physical_type='specific energy') + + grid_R = PotentialParameter("grid_R", physical_type="length") + grid_z = PotentialParameter("grid_z", physical_type="length") + grid_Phi = PotentialParameter("grid_Phi", physical_type="specific energy") Wrapper = CylSplineWrapper @@ -1185,55 +1154,38 @@ def from_file(cls, filename, **kwargs): for i, line in enumerate(raw_lines): if line.startswith(start): Phi_lines.append( - [np.nan] + [float(y) for y in line[len(start):].strip().split('\t')] + [np.nan] + + [float(y) for y in line[len(start) :].strip().split("\t")] ) break - Phi_lines.extend([ - [float(y) for y in x.strip().split('\t')] for x in raw_lines[i+1:] - ]) + Phi_lines.extend( + [[float(y) for y in x.strip().split("\t")] for x in raw_lines[i + 1 :]] + ) Phi_lines = np.array(Phi_lines) gridR = Phi_lines[1:, 0] * u.kpc gridz = Phi_lines[0, 1:] * u.kpc - gridPhi = Phi_lines[1:, 1:] * (u.km/u.s) ** 2 + gridPhi = Phi_lines[1:, 1:] * (u.km / u.s) ** 2 return cls(gridR, gridz, gridPhi, **kwargs) - def __init__( - self, - *args, - units=None, - origin=None, - R=None, - **kwargs - ): - PotentialBase.__init__( - self, - *args, - units=units, - origin=origin, - R=R, - **kwargs - ) + def __init__(self, *args, units=None, origin=None, R=None, **kwargs): + PotentialBase.__init__(self, *args, units=units, origin=origin, R=R, **kwargs) - grid_R = self.parameters['grid_R'] - grid_z = self.parameters['grid_z'] - grid_Phi = self.parameters['grid_Phi'] + grid_R = self.parameters["grid_R"] + grid_z = self.parameters["grid_z"] + grid_Phi = self.parameters["grid_Phi"] Phi0 = grid_Phi[0, 0] # potential at R=0,z=0 - self._multipole_pot = self._fit_asympt( - grid_R, - grid_z, - grid_Phi - ) - Phi_Rmax = self._multipole_pot.energy([1., 0, 0] * grid_R.max()) + self._multipole_pot = self._fit_asympt(grid_R, grid_z, grid_Phi) + Phi_Rmax = self._multipole_pot.energy([1.0, 0, 0] * grid_R.max()) Mtot = -Phi_Rmax[0] * grid_R.max() if Phi0 < 0 and Mtot > 0: # assign Rscale so that it approximately equals -Mtotal/Phi(r=0), # i.e. would equal the scale radius of a Plummer potential - Rscale = (-Mtot / Phi0).to(self.units['length']) + Rscale = (-Mtot / Phi0).to(self.units["length"]) else: Rscale = grid_R[len(grid_R) // 2] # "rather arbitrary" @@ -1260,33 +1212,38 @@ def __init__( raise ValueError("CylSpline: incorrect coefs array size") grid_Phi_full = np.zeros((sizeR, sizez)) - grid_Phi_full[:, :sizez_orig-1] = grid_Phi[:, :0:-1] - grid_Phi_full[:, sizez_orig-1:] = grid_Phi + grid_Phi_full[:, : sizez_orig - 1] = grid_Phi[:, :0:-1] + grid_Phi_full[:, sizez_orig - 1 :] = grid_Phi if logScaling: grid_Phi_full = np.log(-grid_Phi_full) else: grid_Phi_full = grid_Phi_full - from scipy.interpolate import interp2d - self.spl = interp2d(grid_R_asinh, grid_z_asinh, grid_Phi_full.T, kind='cubic') + from scipy.interpolate import RectBivariateSpline + + self.spl = RectBivariateSpline(grid_R_asinh, grid_z_asinh, grid_Phi_full) # Note: if MultipolePotential parameter order changes, this needs to be updated! - multipole_pars = np.concatenate([ - [self.G, - self._multipole_pot._lmax, - sum(range(self._multipole_pot._lmax + 2))], - [x.value for x in self._multipole_pot.parameters.values()] - ]) + multipole_pars = np.concatenate( + [ + [ + self.G, + self._multipole_pot._lmax, + sum(range(self._multipole_pot._lmax + 2)), + ], + [x.value for x in self._multipole_pot.parameters.values()], + ] + ) self._c_only = { - 'log_scaling': logScaling, - 'Rscale': Rscale.value, - 'sizeR': sizeR, - 'sizez': sizez, - 'grid_R_trans': grid_R_asinh, - 'grid_z_trans': grid_z_asinh, - 'grid_Phi_trans': grid_Phi_full.T, - 'multipole_pars': multipole_pars + "log_scaling": logScaling, + "Rscale": Rscale.value, + "sizeR": sizeR, + "sizez": sizez, + "grid_R_trans": grid_R_asinh, + "grid_z_trans": grid_z_asinh, + "grid_Phi_trans": grid_Phi_full.T, + "multipole_pars": multipole_pars, } self._setup_wrapper(self._c_only) @@ -1308,26 +1265,28 @@ def _fit_asympt(self, grid_R, grid_z, grid_Phi, lmax_fit=8): maxz = np.max(grid_z.value) # first run along R at the max-z and min-z edges - points = np.concatenate(( - [[R, maxz] for R in grid_R.value], - [[R, -maxz] for R in grid_R.value] - )) - Phis = np.concatenate(( - grid_Phi[:, np.argmax(grid_z)].value, - grid_Phi[:, np.argmax(grid_z)].value - )) + points = np.concatenate( + ([[R, maxz] for R in grid_R.value], [[R, -maxz] for R in grid_R.value]) + ) + Phis = np.concatenate( + (grid_Phi[:, np.argmax(grid_z)].value, grid_Phi[:, np.argmax(grid_z)].value) + ) maxR = np.max(grid_R.value) - points = np.concatenate(( - points, - [[maxR, z] for z in grid_z.value], - [[maxR, -z] for z in grid_z.value], - )) - Phis = np.concatenate(( - Phis, - grid_Phi[np.argmax(grid_R), :].value, - grid_Phi[np.argmax(grid_R), :].value - )) + points = np.concatenate( + ( + points, + [[maxR, z] for z in grid_z.value], + [[maxR, -z] for z in grid_z.value], + ) + ) + Phis = np.concatenate( + ( + Phis, + grid_Phi[np.argmax(grid_R), :].value, + grid_Phi[np.argmax(grid_R), :].value, + ) + ) npoints = len(points) # ncoefs = lmax_fit + 1 @@ -1335,38 +1294,31 @@ def _fit_asympt(self, grid_R, grid_z, grid_Phi, lmax_fit=8): r0 = min(np.max(grid_R), np.max(grid_z)) i, j = len(grid_R) // 2, len(grid_z) // 2 - rr = np.sqrt(grid_R[i]**2 + grid_z[j]**2) - m = np.abs(grid_Phi[i, j] * rr / G).to(self.units['mass']) + rr = np.sqrt(grid_R[i] ** 2 + grid_z[j] ** 2) + m = np.abs(grid_Phi[i, j] * rr / G).to(self.units["mass"]) scale = (G * m / r0).decompose(self.units).value # find values of spherical harmonic coefficients # that best match the potential at the array of boundary points # for m-th harmonic, we may have lmax-m+1 different l-terms - matr = np.zeros((npoints, lmax_fit+1)) + matr = np.zeros((npoints, lmax_fit + 1)) # The linear system to solve in the least-square sense is M_{p,l} * S_l = R_p, # where R_p = Phi at p-th boundary point (0<=p=1.20", + "numpy>=1.24.4", "scipy>=1.8", "astropy>=5.0", "pyyaml", @@ -80,7 +80,7 @@ requires = [ "wheel", "setuptools_scm", "extension-helpers==1.*", - "oldest-supported-numpy", + "numpy>=2.0", "cython" ] build-backend = "setuptools.build_meta" diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 8fa666a6..00000000 --- a/tox.ini +++ /dev/null @@ -1,85 +0,0 @@ -# TODO: compare to The Joker tests. Remove "test" from names and call explicit job names... - -[tox] -envlist = - py{38,39,310}-test{,-extradeps,-devdeps,-oldestdeps}{,-cov} - codestyle - nogsl - -requires = - setuptools >= 30.3.0 - pip >= 19.3.1 -isolated_build = true -indexserver = - NIGHTLY = https://pypi.anaconda.org/scipy-wheels-nightly/simple - -[testenv] - -# Pass through the following environment variables which may be needed for the CI -passenv = HOME, WINDIR, LC_ALL, LC_CTYPE, CC, CI - -# Run the tests in a temporary directory to make sure that we don't import -# this package from the source tree -changedir = .tmp/{envname} - -# tox environments are constructed with so-called 'factors' (or terms) -# separated by hyphens, e.g. test-devdeps-cov. Lines below starting with factor: -# will only take effect if that factor is included in the environment name. To -# see a list of example environments that can be run, along with a description, -# run: -# -# tox -l -v -# -description = - run tests - extradeps: with all optional dependencies - devdeps: with the latest developer version of key dependencies - oldestdeps: with the oldest supported version of key dependencies - cov: and test coverage - oldestdeps: with the oldest supported version of key dependencies - -# The following provides some specific pinnings for key packages -deps = - # The oldestdeps factor is intended to be used to install the oldest - # versions of all dependencies that have a minimum version. - oldestdeps: numpy==1.20.* - oldestdeps: matplotlib==3.4.* - oldestdeps: scipy==1.7.* - oldestdeps: astropy==5.0.* - - devdeps: :NIGHTLY:numpy - devdeps: git+https://github.com/astropy/astropy.git#egg=astropy - -extras = - test - extradeps: extra - -commands = - pip freeze - !cov: pytest -v --pyargs gala {toxinidir}/docs {posargs} - cov: pytest --pyargs gala {toxinidir}/docs --cov gala --cov-config={toxinidir}/pyproject.toml {posargs} --cov-report=xml:{toxinidir}/coverage.xml --durations=16 - -# Runs pip install -e . instead of building an sdist and installing -usedevelop = False - -[testenv:local_test] -changedir = .tmp/{envname} -description = Run the tests locally (not on CI) - requires conda and tox-conda -extras = test -conda_deps = - gsl -commands = - pip freeze - pytest -v --pyargs gala {toxinidir}/docs - -[testenv:nogsl] -description = Install gala without GSL and run tests -setenv = - GALA_NOGSL = 1 - -[testenv:codestyle] -skip_install = true -changedir = . -description = check code style with flake8 -deps = flake8 -commands = flake8 gala --count