From b5216fd9edff4f2d9680c18f3cb0478cbb86d7e1 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Thu, 18 Jan 2024 12:56:08 -0500 Subject: [PATCH] Mark required packages for dynamics and syndynes; rename test_comae to test_core --- docs/sbpy/activity/dust.rst | 12 +++++++++++- sbpy/activity/dust/dynamics.py | 8 +++++++- sbpy/activity/dust/syndynes.py | 7 ++++--- .../dust/tests/{test_comae.py => test_core.py} | 9 +++++++-- sbpy/activity/dust/tests/test_syndynes.py | 3 +++ 5 files changed, 32 insertions(+), 7 deletions(-) rename sbpy/activity/dust/tests/{test_comae.py => test_core.py} (99%) diff --git a/docs/sbpy/activity/dust.rst b/docs/sbpy/activity/dust.rst index 801aa3ff..5ca91227 100644 --- a/docs/sbpy/activity/dust.rst +++ b/docs/sbpy/activity/dust.rst @@ -214,6 +214,7 @@ State objects `sbpy` uses `~sbpy.activity.dust.dynamics.State` objects to encapsulate the position and velocity of an object at a given time. Create a `~sbpy.activity.dust.dynamics.State` for a comet at :math:`x=2` au, moving along the y-axis at a speed of 30 km/s: + .. doctest:: >>> from astropy.time import Time @@ -264,7 +265,7 @@ First, define the source of the syndynes, a comet at 2 au from the Sun: Next, initialize the syndyne object: -.. doctest:: +.. doctest-requires:: scipy >>> import numpy as np >>> from sbpy.activity.dust import Syndynes @@ -285,6 +286,7 @@ Next, initialize the syndyne object: To compute the syndynes, use the :meth:`~sbpy.activity.dust.syndynes.Syndynes.solve` method. The computed particle positions are saved in the :attr:`~sbpy.activity.dust.syndynes.Syndynes.particles` attribute. For our example, the 4 :math:`\beta`-values and the 50 ages produce 150 particles: .. doctest:: +.. doctest-requires:: scipy >>> syn.solve() >>> print(len(syn.particles)) @@ -293,6 +295,7 @@ To compute the syndynes, use the :meth:`~sbpy.activity.dust.syndynes.Syndynes.so Inspect the results using :meth:`~sbpy.activity.dust.syndynes.Syndynes.syndynes`, which returns an iterator containing each syndyne's :math:`\beta`-value and particle states. For example, we can compute the maximum linear distance from the comet to the syndyne particles: .. doctest:: +.. doctest-requires:: scipy >>> for beta, states in syn.syndynes(): ... r, v = abs(states - comet) @@ -305,6 +308,7 @@ Inspect the results using :meth:`~sbpy.activity.dust.syndynes.Syndynes.syndynes` Individual syndynes may be produced with the :meth:`~sbpy.activity.dust.syndynes.Syndynes.get_syndyne` method and a syndyne index. The index for the syndyne matches the index of the ``betas`` array. To get the :math:`\beta=0.1` syndyne from our example: .. doctest:: +.. doctest-requires:: scipy >>> print(syn.betas) [1. 0.1 0.01 0. ] @@ -318,6 +322,7 @@ Synchrones Synchrones are also simulated with the `~sbpy.activity.dust.syndynes.Syndynes` class, but instead generated with the :meth:`~sbpy.activity.dust.syndynes.Syndynes.get_synchrone` and :meth:`~sbpy.activity.dust.syndynes.Syndynes.synchrones` methods. .. doctest:: +.. doctest-requires:: scipy >>> age, states = syn.get_synchrone(24) >>> r, v = abs(states) @@ -331,6 +336,7 @@ Projecting onto the sky Syndynes and synchrones may be projected onto the sky as seen by a telescope. This requires an observer and sky coordinate frames. `sbpy` uses `astropy`'s reference frames, which may be specified as a string or an instance of a reference frame object. For precision work, the states provided to the `~sbpy.activity.dust.syndynes.Syndynes` object should be in a heliocentric coordinate frame. Here, we use a J2000 heliocentric ecliptic coordinate frame that _ and the _: `"heliocentriceclipticiau76"`: .. doctest:: +.. doctest-requires:: scipy >>> comet.frame = "heliocentriceclipticiau76" >>> observer = State( @@ -345,6 +351,7 @@ Syndynes and synchrones may be projected onto the sky as seen by a telescope. T With the observer and coordinate frames defined, the syndyne and synchrone methods will return `astropy.coordinates.SkyCoord` objects that represent the sky positions of the test particles. Here, we request the coordinate object is returned in an ICRS-based reference frame and print a sample of the coordinates: .. doctest:: +.. doctest-requires:: scipy >>> beta, states, coords = syn.get_syndyne(0, frame="icrs") >>> print("\n".join(coords[::5].to_string("hmsdms", precision=0))) @@ -360,6 +367,7 @@ Source object orbit Calculating the positions of the projected orbit of the source object may be helpful for interpreting an observation or a set of syndynes. They are calculated with the :meth:`~sbpy.activity.dust.synydnes.Syndynes.get_orbit` method: .. doctest:: +.. doctest-requires:: scipy >>> dt = np.linspace(-2, 2) * u.d >>> orbit, coords = syn.get_orbit(dt, frame="icrs") @@ -372,6 +380,7 @@ Other dynamical models In this example, we compute the syndynes of a comet orbiting β Pic (1.8 solar masses) by sub-classing `~sbpy.activity.dust.dynamics.SolarGravityAndRadiationPressure` and updating :math:`GM`, the mass of the star times the gravitational constant: .. doctest:: +.. doctest-requires:: scipy >>> import astropy.constants as const >>> from sbpy.activity.dust import SolarGravityAndRadiationPressure @@ -389,6 +398,7 @@ Plotting syndynes and synchrones Generally, we are interested in plotting syndynes and synchrones on an image of a comet. The accuracy of the coordinates object depends on the the comet and observer states, but also on whether or not light travel time is accounted for. The `sbpy` testing suite shows that arcsecond-level accuracy is possible, but this is generally not accurate enough for direct comparison to typical images of comets. Instead, it helps to compute the positions of the syndynes and synchrone coordinate objects relative to the comet, and plot the results. .. doctest:: +.. doctest-requires:: scipy,matplotlib >>> from itertools import islice >>> import matplotlib.pyplot as plt diff --git a/sbpy/activity/dust/dynamics.py b/sbpy/activity/dust/dynamics.py index 7230ae1b..b5bdc022 100644 --- a/sbpy/activity/dust/dynamics.py +++ b/sbpy/activity/dust/dynamics.py @@ -18,7 +18,11 @@ from typing import Iterable, Union, Optional, Tuple, TypeVar import numpy as np -from scipy.integrate import solve_ivp + +try: + from scipy.integrate import solve_ivp +except ImportError: + pass from astropy.time import Time import astropy.units as u @@ -33,6 +37,7 @@ from ... import data as sbd from ...data.ephem import Ephem from ...exceptions import SbpyException +from ...utils.decorators import requires from ... import time # noqa: F401 @@ -434,6 +439,7 @@ class DynamicalModel(abc.ABC): """ + @requires("scipy") def __init__(self, **kwargs): self.solver_kwargs: dict = dict( rtol=1e-8, diff --git a/sbpy/activity/dust/syndynes.py b/sbpy/activity/dust/syndynes.py index 4b0f3ddd..cfcff307 100644 --- a/sbpy/activity/dust/syndynes.py +++ b/sbpy/activity/dust/syndynes.py @@ -59,7 +59,8 @@ class Syndynes: State vector of the observer in the same reference frame as ``source``. solver : `~sbpy.activity.dust.dynamics.DynamicalModel`, optional - Solve the equations of motion with this object. + Solve the equations of motion with this object. The default solver is + `SolarGravityAndRadiationPressure`. """ @@ -69,7 +70,7 @@ def __init__( betas: Union[Iterable, u.Quantity], ages: u.Quantity, observer: Optional[State] = None, - solver: Optional[DynamicalModel] = SolarGravityAndRadiationPressure(), + solver: Optional[DynamicalModel] = None, ) -> None: if len(source) != 1: raise ValueError("Only one source state vector allowed.") @@ -86,7 +87,7 @@ def __init__( raise ValueError("source and observer frames are not equal.") self.observer = observer - self.solver = solver + self.solver = SolarGravityAndRadiationPressure() if solver is None else solver self.initialize_states() diff --git a/sbpy/activity/dust/tests/test_comae.py b/sbpy/activity/dust/tests/test_core.py similarity index 99% rename from sbpy/activity/dust/tests/test_comae.py rename to sbpy/activity/dust/tests/test_core.py index 55717ce0..a51760eb 100644 --- a/sbpy/activity/dust/tests/test_comae.py +++ b/sbpy/activity/dust/tests/test_core.py @@ -5,8 +5,13 @@ import pytest import numpy as np import astropy.units as u -import synphot -from ..comae import Afrho, Efrho, phase_HalleyMarcus + +try: + import synphot +except ImportError: + pass + +from ..core import Afrho, Efrho, phase_HalleyMarcus from ...core import CircularAperture from ....calib import solar_fluxd from ....units import VEGAmag, JMmag diff --git a/sbpy/activity/dust/tests/test_syndynes.py b/sbpy/activity/dust/tests/test_syndynes.py index 0d0868c0..ee42bf01 100644 --- a/sbpy/activity/dust/tests/test_syndynes.py +++ b/sbpy/activity/dust/tests/test_syndynes.py @@ -11,6 +11,9 @@ from ..dynamics import SolarGravity, SolarGravityAndRadiationPressure +pytest.importorskip("scipy") + + @pytest.fixture def example_syndynes(): comet = State(