Skip to content

Commit c0c3fed

Browse files
authored
Merge branch 'main' into python-3.13
2 parents 3b41df7 + d4f5ed0 commit c0c3fed

16 files changed

+795
-57
lines changed

.pre-commit-config.yaml

+3-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ repos:
1212
- id: check-executables-have-shebangs
1313
- id: check-yaml
1414
- repo: https://github.com/asottile/pyupgrade
15-
rev: v3.19.0
15+
rev: v3.19.1
1616
hooks:
1717
- id: pyupgrade
1818
args: [--py38-plus]
@@ -21,7 +21,7 @@ repos:
2121
hooks:
2222
- id: rst-backticks
2323
- repo: https://github.com/hadialqattan/pycln
24-
rev: v2.4.0
24+
rev: v2.5.0
2525
hooks:
2626
- id: pycln
2727
- repo: https://github.com/PyCQA/isort
@@ -36,6 +36,6 @@ repos:
3636
args: ["-L", "thisE,thise,mye,tE,te,hist,ro,sav,ccompiler,aas,floatIn,dOmin",
3737
"-x","doc/source/_static/try-galpy.js"]
3838
- repo: https://github.com/astral-sh/ruff-pre-commit
39-
rev: v0.8.1
39+
rev: v0.8.6
4040
hooks:
4141
- id: ruff-format

HISTORY.txt

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
v1.10.2 (expected around 2025-03-01)
22
====================
33

4+
- Implemented the IAS15 integrator of Rein & Spiegel 2015.
5+
46
- Combine the drift calculations in the Python leapfrog integrator (#365).
57

68
- Move the checks for non-axisymmetric and dissipative potentials from internal to

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
[galpy](http://www.galpy.org) is a Python package for galactic dynamics. It supports orbit integration in a variety of potentials, evaluating and sampling various distribution functions, and the calculation of action-angle coordinates for all static potentials. `galpy` is an [astropy](http://www.astropy.org/) [affiliated package](http://www.astropy.org/affiliated/) and provides full support for astropy’s [Quantity](http://docs.astropy.org/en/stable/api/astropy.units.Quantity.html) framework for variables with units.
77

88
[![image](https://github.com/jobovy/galpy/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/jobovy/galpy/actions/workflows/build.yml) [![image](https://github.com/jobovy/galpy/actions/workflows/build_windows.yml/badge.svg?branch=main)](https://github.com/jobovy/galpy/actions/workflows/build_windows.yml) [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/jobovy/galpy/main.svg)](https://results.pre-commit.ci/latest/github/jobovy/galpy/main)
9-
[![image](http://codecov.io/gh/jobovy/galpy/coverage.svg?branch=main)](http://app.codecov.io/gh/jobovy/galpy?branch=main) [![image](https://readthedocs.org/projects/galpy/badge/?version=latest)](http://docs.galpy.org/en/latest/)
9+
[![image](https://codecov.io/gh/jobovy/galpy/branch/main/graph/badge.svg?token=nIqjwFncfP)](https://codecov.io/gh/jobovy/galpy) [![image](https://readthedocs.org/projects/galpy/badge/?version=latest)](http://docs.galpy.org/en/latest/)
1010
[![image](http://img.shields.io/pypi/v/galpy.svg)](https://pypi.python.org/pypi/galpy/) [![image](https://img.shields.io/pypi/pyversions/galpy?logo=python&logoColor=white)](https://pypi.python.org/pypi/galpy/) [![image](https://anaconda.org/conda-forge/galpy/badges/version.svg)](https://anaconda.org/conda-forge/galpy) [![image](https://img.shields.io/github/commits-since/jobovy/galpy/latest)](https://github.com/jobovy/galpy/commits/main)
1111
[![image](http://img.shields.io/badge/license-New%20BSD-brightgreen.svg)](https://github.com/jobovy/galpy/blob/main/LICENSE) [![image](http://img.shields.io/badge/DOI-10.1088/0067%2D%2D0049/216/2/29-blue.svg)](http://dx.doi.org/10.1088/0067-0049/216/2/29) [![image](http://img.shields.io/badge/powered%20by-AstroPy-orange.svg?style=flat)](http://www.astropy.org/) [![image](https://img.shields.io/badge/join-slack-E01563.svg?style=flat&logo=slack&logoWidth=10)](https://join.slack.com/t/galpy/shared_invite/zt-p6upr4si-mX7u8MRdtm~3bW7o8NA_Ww)
1212

doc/source/orbit.rst

+7-2
Original file line numberDiff line numberDiff line change
@@ -1014,9 +1014,11 @@ the ``orbit.integrate`` method. Currently available integrators are
10141014
* rk6_c
10151015
* dopr54_c
10161016
* dop853_c
1017+
* ias15_c
10171018

1018-
which are Runge-Kutta and Dormand-Prince methods. There are also a
1019-
number of symplectic integrators available
1019+
which are Runge-Kutta, Dormand-Prince methods and the IAS15
1020+
integrator in `Rein & Spiegel (2014) <https://doi.org/10.1093/mnras/stu2164>`_.
1021+
There are also a number of symplectic integrators available
10201022

10211023
* leapfrog_c
10221024
* symplec4_c
@@ -1045,6 +1047,9 @@ which are speedy and reliable. For example, compare
10451047
>>> timeit(o.integrate(ts,mp,method='dop853_c'))
10461048
# 4.65 ms ± 86.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
10471049

1050+
The ``ias15_c`` method uses adaptive timestepping under the hood, and
1051+
can be used in cases where very high precision is required during orbit integration.
1052+
10481053
If the C extensions are unavailable for some reason, I recommend using
10491054
the ``odeint`` pure-Python integrator, as it is the fastest. Using the
10501055
same example as above

galpy/orbit/Orbits.py

+7-17
Original file line numberDiff line numberDiff line change
@@ -1287,13 +1287,15 @@ def check_integrator(method, no_symplec=False):
12871287
"rk6_c",
12881288
"dopr54_c",
12891289
"dop853_c",
1290+
"ias15_c",
12901291
]
12911292
if no_symplec:
12921293
symplec_methods = [
12931294
"leapfrog",
12941295
"leapfrog_c",
12951296
"symplec4_c",
12961297
"symplec6_c",
1298+
"ias15_c", # practically speaking, ias15 has the same limitations as symplectic integrators in galpy
12971299
]
12981300
[valid_methods.remove(symplec_method) for symplec_method in symplec_methods]
12991301
if method.lower() not in valid_methods:
@@ -1385,6 +1387,7 @@ def integrate(
13851387
- 'dopr54_c' for a 5-4 Dormand-Prince integrator in C
13861388
- 'dop853' for a 8-5-3 Dormand-Prince integrator in Python
13871389
- 'dop853_c' for a 8-5-3 Dormand-Prince integrator in C
1390+
- 'ias15_c' for an adaptive 15th order integrator using Gauß-Radau quadrature (see IAS15 paper) in C
13881391
13891392
- 2018-10-13 - Written as parallel_map applied to regular Orbit integration - Mathew Bub (UofT)
13901393
- 2018-12-26 - Written to use OpenMP C implementation - Bovy (UofT)
@@ -1743,7 +1746,7 @@ def integrate_dxdv(
17431746
17441747
Notes
17451748
-----
1746-
- Possible integration methods are:
1749+
- Possible integration methods are the non-symplectic ones in galpy:
17471750
17481751
- 'odeint' for scipy's odeint
17491752
- 'rk4_c' for a 4th-order Runge-Kutta integrator in C
@@ -1761,22 +1764,7 @@ def integrate_dxdv(
17611764
raise AttributeError(
17621765
"integrate_dxdv is only implemented for 4D (planar) orbits"
17631766
)
1764-
if method.lower() not in [
1765-
"odeint",
1766-
"dop853",
1767-
"rk4_c",
1768-
"rk6_c",
1769-
"dopr54_c",
1770-
"dop853_c",
1771-
]:
1772-
if "leapfrog" in method.lower() or "symplec" in method.lower():
1773-
raise ValueError(
1774-
f"{method:s} is not a valid `method for integrate_dxdv, because symplectic integrators cannot be used`"
1775-
)
1776-
else:
1777-
raise ValueError(
1778-
f"{method:s} is not a valid `method for integrate_dxdv`"
1779-
)
1767+
self.check_integrator(method, no_symplec=True)
17801768
pot = flatten_potential(pot)
17811769
_check_potential_dim(self, pot)
17821770
_check_consistent_units(self, pot)
@@ -5168,6 +5156,7 @@ def bruteSOS(
51685156
- 'dopr54_c' for a 5-4 Dormand-Prince integrator in C
51695157
- 'dop853' for a 8-5-3 Dormand-Prince integrator in Python
51705158
- 'dop853_c' for a 8-5-3 Dormand-Prince integrator in C
5159+
- 'ias15_c' for an adaptive 15th order integrator using Gauß-Radau quadrature (see IAS15 paper) in C
51715160
51725161
- 2023-05-31 - Written - Bovy (UofT)
51735162
@@ -5910,6 +5899,7 @@ def plotBruteSOS(
59105899
- 'dopr54_c' for a 5-4 Dormand-Prince integrator in C
59115900
- 'dop853' for a 8-5-3 Dormand-Prince integrator in Python
59125901
- 'dop853_c' for a 8-5-3 Dormand-Prince integrator in C
5902+
- 'ias15_c' for an adaptive 15th order integrator using Gauß-Radau quadrature (see IAS15 paper) in C
59135903
59145904
- 2023-05-31 - Written - Bovy (UofT)
59155905

galpy/orbit/integratePlanarOrbit.py

+2
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,8 @@ def _parse_integrator(int_method):
618618
int_method_c = 5
619619
elif int_method.lower() == "dop853_c":
620620
int_method_c = 6
621+
elif int_method.lower() == "ias15_c":
622+
int_method_c = 7
621623
else:
622624
int_method_c = 0
623625
return int_method_c

galpy/orbit/orbit_c_ext/integrateFullOrbit.c

+11
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include <bovy_symplecticode.h>
1515
#include <leung_dop853.h>
1616
#include <bovy_rk.h>
17+
#include <wez_ias15.h>
1718
#include <integrateFullOrbit.h>
1819
//Potentials
1920
#include <galpy_potentials.h>
@@ -717,6 +718,11 @@ EXPORT void integrateFullOrbit(int nobj,
717718
odeint_deriv_func= &evalRectDeriv;
718719
dim= 6;
719720
break;
721+
case 7: //ias15
722+
odeint_func= &wez_ias15;
723+
odeint_deriv_func= &evalRectForce;
724+
dim= 3;
725+
break;
720726
}
721727
#pragma omp parallel for schedule(dynamic,ORBITS_CHUNKSIZE) private(ii,jj) num_threads(max_threads)
722728
for (ii=0; ii < nobj; ii++) {
@@ -883,6 +889,11 @@ void integrateOrbit_dxdv(double *yo,
883889
odeint_deriv_func= &evalRectDeriv_dxdv;
884890
dim= 12;
885891
break;
892+
case 7: //ias15
893+
odeint_func= &wez_ias15;
894+
odeint_deriv_func= &evalRectForce;
895+
dim= 6;
896+
break;
886897
}
887898
odeint_func(odeint_deriv_func,dim,yo,nt,-9999.99,t,npot,potentialArgs,
888899
rtol,atol,result,err);

galpy/orbit/orbit_c_ext/integrateLinearOrbit.c

+6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <math.h>
88
#include <bovy_symplecticode.h>
99
#include <bovy_rk.h>
10+
#include <wez_ias15.h>
1011
#include <leung_dop853.h>
1112
#include <integrateFullOrbit.h>
1213
//Potentials
@@ -192,6 +193,11 @@ EXPORT void integrateLinearOrbit(int nobj,
192193
odeint_deriv_func= &evalLinearDeriv;
193194
dim= 2;
194195
break;
196+
case 7: //ias15
197+
odeint_func= &wez_ias15;
198+
odeint_deriv_func= &evalLinearForce;
199+
dim= 1;
200+
break;
195201
}
196202
#pragma omp parallel for schedule(dynamic,ORBITS_CHUNKSIZE) private(ii) num_threads(max_threads)
197203
for (ii=0; ii < nobj; ii++) {

galpy/orbit/orbit_c_ext/integratePlanarOrbit.c

+6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <bovy_coords.h>
99
#include <bovy_symplecticode.h>
1010
#include <bovy_rk.h>
11+
#include <wez_ias15.h>
1112
#include <leung_dop853.h>
1213
#include <integrateFullOrbit.h>
1314
//Potentials
@@ -674,6 +675,11 @@ EXPORT void integratePlanarOrbit(int nobj,
674675
odeint_deriv_func= &evalPlanarRectDeriv;
675676
dim= 4;
676677
break;
678+
case 7: //ias15
679+
odeint_func= &wez_ias15;
680+
odeint_deriv_func= &evalPlanarRectForce;
681+
dim= 2;
682+
break;
677683
}
678684
#pragma omp parallel for schedule(dynamic,ORBITS_CHUNKSIZE) private(ii,jj) num_threads(max_threads)
679685
for (ii=0; ii < nobj; ii++) {

galpy/potential/SCFPotential.py

+43-13
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
import hashlib
22

33
import numpy
4+
import scipy
45
from numpy.polynomial.legendre import leggauss
6+
from packaging.version import parse as parse_version
57
from scipy import integrate
6-
from scipy.special import gamma, gammaln, lpmn
8+
from scipy.special import gamma, gammaln
9+
10+
_SCIPY_VERSION = parse_version(scipy.__version__)
11+
if _SCIPY_VERSION < parse_version("1.15"): # pragma: no cover
12+
from scipy.special import lpmn
13+
else:
14+
from scipy.special import assoc_legendre_p_all
715

816
from ..util import conversion, coords
917
from ..util._optional_deps import _APY_LOADED
@@ -412,7 +420,17 @@ def _compute(self, funcTilde, R, z, phi):
412420
N, L, M = Acos.shape
413421
r, theta, phi = coords.cyl_to_spher(R, z, phi)
414422

415-
PP = lpmn(M - 1, L - 1, numpy.cos(theta))[0].T ##Get the Legendre polynomials
423+
## Get the Legendre polynomials
424+
if _SCIPY_VERSION < parse_version("1.15"): # pragma: no cover
425+
PP = lpmn(M - 1, L - 1, numpy.cos(theta))[0].T
426+
else:
427+
PP = numpy.swapaxes(
428+
assoc_legendre_p_all(L - 1, M - 1, numpy.cos(theta), branch_cut=2)[
429+
0, :, :M
430+
],
431+
0,
432+
1,
433+
).T
416434
func_tilde = funcTilde(r, N, L) ## Tilde of the function of interest
417435

418436
func = numpy.zeros(
@@ -515,9 +533,15 @@ def _computeforce(self, R, z, phi=0, t=0):
515533
dPhi_dphi = self._cached_dPhi_dphi
516534

517535
else:
518-
PP, dPP = lpmn(
519-
M - 1, L - 1, numpy.cos(theta)
520-
) ##Get the Legendre polynomials
536+
## Get the Legendre polynomials
537+
if _SCIPY_VERSION < parse_version("1.15"): # pragma: no cover
538+
PP, dPP = lpmn(M - 1, L - 1, numpy.cos(theta))
539+
else:
540+
PP, dPP = assoc_legendre_p_all(
541+
L - 1, M - 1, numpy.cos(theta), branch_cut=2, diff_n=1
542+
)
543+
PP = numpy.swapaxes(PP[:, :M], 0, 1)
544+
dPP = numpy.swapaxes(dPP[:, :M], 0, 1)
521545
PP = PP.T[None, :, :]
522546
dPP = dPP.T[None, :, :]
523547
phi_tilde = self._phiTilde(r, N, L)[:, :, numpy.newaxis]
@@ -924,14 +948,13 @@ def integrand(xi, costheta):
924948
r = _xiToR(xi, a)
925949
R = r * numpy.sqrt(1 - costheta**2.0)
926950
z = r * costheta
927-
Legandre = lpmn(0, L - 1, costheta)[0].T[numpy.newaxis, :, 0]
951+
if _SCIPY_VERSION < parse_version("1.15"): # pragma: no cover
952+
PP = lpmn(0, L - 1, costheta)[0].T[numpy.newaxis, :, 0]
953+
else:
954+
PP = assoc_legendre_p_all(L - 1, 0, costheta, branch_cut=2)[0].T
928955
dV = (1.0 + xi) ** 2.0 * numpy.power(1.0 - xi, -4.0)
929956
phi_nl = (
930-
a**3
931-
* (1.0 + xi) ** l
932-
* (1.0 - xi) ** (l + 1.0)
933-
* _C(xi, N, L)[:, :]
934-
* Legandre
957+
a**3 * (1.0 + xi) ** l * (1.0 - xi) ** (l + 1.0) * _C(xi, N, L)[:, :] * PP
935958
)
936959
param[0] = R
937960
param[1] = z
@@ -1089,15 +1112,22 @@ def integrand(xi, costheta, phi):
10891112
r = _xiToR(xi, a)
10901113
R = r * numpy.sqrt(1 - costheta**2.0)
10911114
z = r * costheta
1092-
Legandre = lpmn(L - 1, L - 1, costheta)[0].T[numpy.newaxis, :, :]
1115+
if _SCIPY_VERSION < parse_version("1.15"): # pragma: no cover
1116+
PP = lpmn(L - 1, L - 1, costheta)[0].T[numpy.newaxis, :, :]
1117+
else:
1118+
PP = numpy.swapaxes(
1119+
assoc_legendre_p_all(L - 1, L - 1, costheta, branch_cut=2)[0][:, :L],
1120+
0,
1121+
1,
1122+
).T[numpy.newaxis, :, :]
10931123
dV = (1.0 + xi) ** 2.0 * numpy.power(1.0 - xi, -4.0)
10941124

10951125
phi_nl = (
10961126
-(a**3)
10971127
* (1.0 + xi) ** l
10981128
* (1.0 - xi) ** (l + 1.0)
10991129
* _C(xi, N, L)[:, :, numpy.newaxis]
1100-
* Legandre
1130+
* PP
11011131
)
11021132

11031133
return (

galpy/potential/potential_c_ext/PowerSphericalPotentialwCutoff.c

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
//PowerSphericalPotentialwCutoff
88
//3 arguments: amp, alpha, rc
99
double mass(double r2,double alpha, double rc){
10-
return 2. * M_PI * pow ( rc , 3. - alpha ) * ( gsl_sf_gamma ( 1.5 - 0.5 * alpha ) - gsl_sf_gamma_inc ( 1.5 - 0.5 * alpha , r2 / rc / rc ) );
10+
return 2. * M_PI * pow ( rc , 3. - alpha ) * ( gsl_sf_gamma ( 1.5 - 0.5 * alpha ) * gsl_sf_gamma_inc_P ( 1.5 - 0.5 * alpha , r2 / rc / rc ) );
1111
}
1212
double PowerSphericalPotentialwCutoffEval(double R,double Z, double phi,
1313
double t,

galpy/util/quadpack.py

-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
import numpy
77
from scipy.integrate import fixed_quad, quad
8-
from scipy.integrate._quadrature import vectorize1
98

109

1110
def _infunc(x, func, gfun, hfun, more_args, epsrel, epsabs):

0 commit comments

Comments
 (0)