From c68addad04a12b153a6e0036a98bdb5696017f17 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Sat, 19 Jun 2021 14:28:37 -0400 Subject: [PATCH 01/59] Replace master with main --- README.rst | 14 +++++++++++++- docs/conf.py | 2 +- docs/index.rst | 2 +- docs/status.rst | 2 +- website/public/index.html | 2 +- 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index b3cef2db..dcd57e15 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ -.. image:: https://github.com/NASA-Planetary-Science/sbpy/raw/master/logo/sbpy_logo_short.png +.. image:: https://github.com/NASA-Planetary-Science/sbpy/raw/main/logo/sbpy_logo_short.png :width: 400px :align: center :alt: sbpy @@ -67,3 +67,15 @@ Acknowledgements If you use `sbpy` in your work, please acknowledge it by citing `Mommert, Kelley, de-Val Borro, Li et al., (2019). sbpy: A Python module for small-body planetary astronomy. Journal of Open Source Software, 4(38), 1426 `_ + + +If you locally cloned this repo before 5 Feb 2021 +------------------------------------------------- + +The primary branch for this repo has been transitioned from ``master`` to ``main``. If you have a local clone of this repository and want to keep your local branch in sync with this repo, you'll need to do the following in your local clone from your terminal: +``` +git branch -m master main +git fetch origin +git branch -u origin/main main +``` +If you are using a GUI to manage your repos you'll have to find the equivalent commands as it's different for different programs. Alternatively, you can just delete your local clone and re-clone! diff --git a/docs/conf.py b/docs/conf.py index 6fb69f50..e2b3b18d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -176,7 +176,7 @@ if versionmod.version.release: edit_on_github_branch = "v" + versionmod.version.version else: - edit_on_github_branch = "master" + edit_on_github_branch = "main" edit_on_github_source_root = "" edit_on_github_doc_root = "docs" diff --git a/docs/index.rst b/docs/index.rst index f2b8df94..dfa691c4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -34,7 +34,7 @@ Current Status :alt: GitHub testing status .. image:: https://codecov.io/gh/NASA-Planetary-Science/sbpy/branch/master/graph/badge.svg - :target: https://app.codecov.io/gh/NASA-Planetary-Science/sbpy/branch/master/graph/badge.svg + :target: https://codecov.io/gh/NASA-Planetary-Science/sbpy :alt: codecov status diff --git a/docs/status.rst b/docs/status.rst index bda76203..1691e92d 100644 --- a/docs/status.rst +++ b/docs/status.rst @@ -13,7 +13,7 @@ The current development version is **v0.2dev**; its status is as follows: :alt: GitHub testing status .. image:: https://codecov.io/gh/NASA-Planetary-Science/sbpy/branch/master/graph/badge.svg - :target: https://app.codecov.io/gh/NASA-Planetary-Science/sbpy + :target: https://codecov.io/gh/NASA-Planetary-Science/sbpy :alt: codecov status .. image:: https://readthedocs.org/projects/sbpy/badge/?version=latest diff --git a/website/public/index.html b/website/public/index.html index aea0c5fb..89d71ddf 100644 --- a/website/public/index.html +++ b/website/public/index.html @@ -69,7 +69,7 @@ href="https://joss.theoj.org/papers/8b8e7bb15fb4a14f80f2afd06b6ce060#"> JOSS Publication - +

Current Version: 0.1.1

From 87b07bfac314f6db344c5f26cf764d1a70529e41 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Fri, 20 May 2022 09:15:31 -0400 Subject: [PATCH 02/59] More replace masters with mains --- .github/workflows/ci_tests.yml | 2 +- README.rst | 14 +------------- docs/index.rst | 2 +- docs/status.rst | 4 ++-- tox.ini | 2 +- 5 files changed, 6 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index d8b52cbe..6a0c2009 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -7,7 +7,7 @@ name: CI Tests on: push: branches: - - master # GitHub now defaults to 'main' as the name of the primary branch. Change this as needed. + - main # GitHub now defaults to 'main' as the name of the primary branch. Change this as needed. # tags: # run CI if specific tags are pushed pull_request: # branches: # only build on PRs against 'main' if you need to further limit when CI is run. diff --git a/README.rst b/README.rst index dcd57e15..ad8e06fc 100644 --- a/README.rst +++ b/README.rst @@ -23,7 +23,7 @@ planetary astronomy. :target: https://doi.org/10.21105/joss.01426 :alt: JOSS documentation -.. image:: https://codecov.io/gh/NASA-Planetary-Science/sbpy/branch/master/graph/badge.svg +.. image:: https://codecov.io/gh/NASA-Planetary-Science/sbpy/branch/main/graph/badge.svg :target: https://codecov.io/gh/NASA-Planetary-Science/sbpy :alt: codecov status @@ -67,15 +67,3 @@ Acknowledgements If you use `sbpy` in your work, please acknowledge it by citing `Mommert, Kelley, de-Val Borro, Li et al., (2019). sbpy: A Python module for small-body planetary astronomy. Journal of Open Source Software, 4(38), 1426 `_ - - -If you locally cloned this repo before 5 Feb 2021 -------------------------------------------------- - -The primary branch for this repo has been transitioned from ``master`` to ``main``. If you have a local clone of this repository and want to keep your local branch in sync with this repo, you'll need to do the following in your local clone from your terminal: -``` -git branch -m master main -git fetch origin -git branch -u origin/main main -``` -If you are using a GUI to manage your repos you'll have to find the equivalent commands as it's different for different programs. Alternatively, you can just delete your local clone and re-clone! diff --git a/docs/index.rst b/docs/index.rst index dfa691c4..f88389a6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -33,7 +33,7 @@ Current Status :target: https://github.com/NASA-Planetary-Science/sbpy/actions :alt: GitHub testing status -.. image:: https://codecov.io/gh/NASA-Planetary-Science/sbpy/branch/master/graph/badge.svg +.. image:: https://codecov.io/gh/NASA-Planetary-Science/sbpy/branch/main/graph/badge.svg :target: https://codecov.io/gh/NASA-Planetary-Science/sbpy :alt: codecov status diff --git a/docs/status.rst b/docs/status.rst index 1691e92d..f4f931e0 100644 --- a/docs/status.rst +++ b/docs/status.rst @@ -6,13 +6,13 @@ Status Page This page indicates the development status of `sbpy`. The development is expected to conclude in 2021. -The current development version is **v0.2dev**; its status is as follows: +The current development version is **v0.4dev**; its status is as follows: .. image:: https://github.com/NASA-Planetary-Science/sbpy/actions/workflows/ci_cron_weekly.yml/badge.svg :target: https://github.com/NASA-Planetary-Science/sbpy/actions :alt: GitHub testing status -.. image:: https://codecov.io/gh/NASA-Planetary-Science/sbpy/branch/master/graph/badge.svg +.. image:: https://codecov.io/gh/NASA-Planetary-Science/sbpy/branch/main/graph/badge.svg :target: https://codecov.io/gh/NASA-Planetary-Science/sbpy :alt: codecov status diff --git a/tox.ini b/tox.ini index a015a9db..935b9267 100644 --- a/tox.ini +++ b/tox.ini @@ -16,7 +16,7 @@ isolated_build = true # project-wide pinning of dependencies, e.g. if new versions of pytest do not # work correctly with pytest-astropy plugins. Most of the time the pinnings file # should be empty. -pypi_filter = https://raw.githubusercontent.com/astropy/ci-helpers/master/pip_pinnings.txt +pypi_filter = https://raw.githubusercontent.com/astropy/ci-helpers/main/pip_pinnings.txt # Pass through the following environemnt variables which are needed for the CI passenv = HOME WINDIR LC_ALL LC_CTYPE CC CI TEST_READ_HUGE_FILE FC_GFORTRAN From ca4d7e38bdd9977456e1e3b3de53633b12a48a38 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Thu, 26 May 2022 09:25:24 -0400 Subject: [PATCH 03/59] Expose DataClass objects on front page of documentation. --- docs/index.rst | 4 +++- docs/sbpy/data/dataclass.rst | 6 +++--- docs/sbpy/data/ephem.rst | 6 +++--- docs/sbpy/data/fieldnames.rst | 5 +++-- docs/sbpy/data/index.rst | 4 ++-- docs/sbpy/data/names.rst | 6 +++--- docs/sbpy/data/obs.rst | 6 +++--- docs/sbpy/data/orbit.rst | 6 +++--- docs/sbpy/data/phys.rst | 18 +++++++++--------- 9 files changed, 32 insertions(+), 29 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index f88389a6..cb7651c8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -59,10 +59,12 @@ Data Structures: Orbits, Ephemerides, Observations, and Physical Properties --------------------------------------------------------------------------- .. toctree:: - :maxdepth: 2 + :maxdepth: 1 + :glob: sbpy/data/index.rst sbpy/data/fieldnames.rst + sbpy/data/* Photometry and Spectroscopy --------------------------- diff --git a/docs/sbpy/data/dataclass.rst b/docs/sbpy/data/dataclass.rst index b3aebe2d..16e4b0c6 100644 --- a/docs/sbpy/data/dataclass.rst +++ b/docs/sbpy/data/dataclass.rst @@ -1,8 +1,8 @@ .. _data containers: -=============== -Data Containers -=============== +======================================= +Data Containers (`sbpy.data.DataClass`) +======================================= `sbpy` relies heavily on the use of `~sbpy.data.DataClass` data containers that are used to encapsulate data and to propagate them diff --git a/docs/sbpy/data/ephem.rst b/docs/sbpy/data/ephem.rst index f90ed7b5..89fc73e7 100644 --- a/docs/sbpy/data/ephem.rst +++ b/docs/sbpy/data/ephem.rst @@ -1,6 +1,6 @@ -=========== -Using Ephem -=========== +===================================== +Ephemeris Objects (`sbpy.data.Ephem`) +===================================== As shown above (:ref:`How to use Data Containers`), `~sbpy.data.Ephem` objects can be created on the fly. However, diff --git a/docs/sbpy/data/fieldnames.rst b/docs/sbpy/data/fieldnames.rst index 4aa883cc..e2e944ab 100644 --- a/docs/sbpy/data/fieldnames.rst +++ b/docs/sbpy/data/fieldnames.rst @@ -1,8 +1,9 @@ .. _field name list: -sbpy Field Names -================ +=================================== +Data Container Field Name Reference +=================================== The following table lists field names that are recognized by `sbpy` when accessing `~sbpy.data.DataClass` objects, i.e., diff --git a/docs/sbpy/data/index.rst b/docs/sbpy/data/index.rst index 6af4547e..cd761a99 100644 --- a/docs/sbpy/data/index.rst +++ b/docs/sbpy/data/index.rst @@ -26,11 +26,11 @@ Content .. toctree:: :maxdepth: 2 - dataclass.rst + dataclass.rst ephem.rst obs.rst orbit.rst - phys.rst + phys.rst names.rst diff --git a/docs/sbpy/data/names.rst b/docs/sbpy/data/names.rst index 39053e59..358ecb3c 100644 --- a/docs/sbpy/data/names.rst +++ b/docs/sbpy/data/names.rst @@ -1,6 +1,6 @@ -============= - Using Names -============= +=========================================== +Small-Body Name Parsing (`sbpy.data.Names`) +=========================================== `~sbpy.data.Names` is different from the other classes in `~sbpy.data` in that it does not use `~sbpy.data.DataClass` as a base class. Instead, diff --git a/docs/sbpy/data/obs.rst b/docs/sbpy/data/obs.rst index 6f7782fa..0a35d3ad 100644 --- a/docs/sbpy/data/obs.rst +++ b/docs/sbpy/data/obs.rst @@ -1,6 +1,6 @@ -=========== - Using Obs -=========== +============================================ +Observational Data Objects (`sbpy.data.Obs`) +============================================ `~sbpy.data.Obs` objects mostly share their functionality with `~sbpy.data.Ephem`, but there are some unique features tailored to observational data. diff --git a/docs/sbpy/data/orbit.rst b/docs/sbpy/data/orbit.rst index e1a34ea7..d798e851 100644 --- a/docs/sbpy/data/orbit.rst +++ b/docs/sbpy/data/orbit.rst @@ -1,6 +1,6 @@ -============= - Using Orbit -============= +====================================== +Orbit Data Objects (`sbpy.data.Orbit`) +====================================== Orbit Queries ============= diff --git a/docs/sbpy/data/phys.rst b/docs/sbpy/data/phys.rst index 854e5063..116f1f76 100644 --- a/docs/sbpy/data/phys.rst +++ b/docs/sbpy/data/phys.rst @@ -1,6 +1,6 @@ -============ - Using Phys -============ +======================================== +Physical Data Objects (`sbpy.data.Phys`) +======================================== `~sbpy.data.Phys` is designed to contain and query physical properties for small bodies; functions to query these properties are @@ -17,13 +17,13 @@ small number of asteroids: >>> phys = Phys.from_sbdb(['Ceres', '12893', '3552']) # doctest: +REMOTE_DATA >>> phys['targetname', 'H', 'diameter'] # doctest: +SKIP - targetname H diameter - km - str26 float64 float64 + targetname H diameter + mag km + str26 float64 float64 -------------------------- ------- -------- - 1 Ceres 3.34 939.4 - 12893 Mommert (1998 QS55) 13.9 5.214 - 3552 Don Quixote (1983 SA) 12.9 19.0 + 1 Ceres (A801 AA) 3.56 939.4 + 12893 Mommert (1998 QS55) 13.98 5.214 + 3552 Don Quixote (1983 SA) 12.96 19.0 Please note that the SBDB database is not complete with respect to From 83b2fcf8e800381d81da4ad49a208404dda473fe Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Thu, 26 May 2022 09:46:45 -0400 Subject: [PATCH 04/59] Do not import remote_data from astropy --- .../gas/tests/test_prodrate_remote.py | 23 +++++++++---------- sbpy/calib/tests/test_sun.py | 3 +-- sbpy/calib/tests/test_vega.py | 1 - sbpy/spectroscopy/tests/test_spec_remote.py | 1 - 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/sbpy/activity/gas/tests/test_prodrate_remote.py b/sbpy/activity/gas/tests/test_prodrate_remote.py index 4f0c585e..799346f5 100644 --- a/sbpy/activity/gas/tests/test_prodrate_remote.py +++ b/sbpy/activity/gas/tests/test_prodrate_remote.py @@ -5,7 +5,6 @@ import numpy as np import astropy.units as u from astropy.time import Time -from astropy.tests.helper import remote_data from astropy.table import Table from astroquery.lamda import Lamda from astroquery.jplspec import JPLSpec @@ -64,7 +63,7 @@ def data_path(filename): return os.path.join(data_dir, filename) -@remote_data +@pytest.mark.remote_data def test_remote_prodrate_simple_hcn(): hcn = Table.read(data_path('HCN.csv'), format="ascii.csv") @@ -109,7 +108,7 @@ def test_remote_prodrate_simple_hcn(): assert np.all(err < 0.2345) -@remote_data +@pytest.mark.remote_data def test_remote_prodrate_simple_ch3oh(): ch3oh = Table.read(data_path('CH3OH.csv'), format="ascii.csv") @@ -154,7 +153,7 @@ def test_remote_prodrate_simple_ch3oh(): # Issue #296 # MSK: disabling test as CO and HCN are no longer present in LAMDA database(?) @pytest.mark.skip -@remote_data +@pytest.mark.remote_data def test_einstein(): transition_freq_list = [(1611.7935180 * u.GHz).to('MHz'), @@ -204,7 +203,7 @@ def test_einstein(): assert np.all(err < 23.5) -@remote_data +@pytest.mark.remote_data def test_Haser_prodrate(): co = Table.read(data_path('CO.csv'), format="ascii.csv") @@ -266,7 +265,7 @@ def test_Haser_prodrate(): @pytest.mark.skipif('pyradex is None') -@remote_data +@pytest.mark.remote_data def test_Haser_pyradex(): co = Table.read(data_path('CO.csv'), format="ascii.csv") @@ -319,7 +318,7 @@ def test_Haser_pyradex(): assert np.all(err < 0.35) -@remote_data +@pytest.mark.remote_data def test_intensity_conversion(): # test untested case for intensity conversion function @@ -335,7 +334,7 @@ def test_intensity_conversion(): assert np.isclose(intl.value, 6.186509000388917e-11) -@remote_data +@pytest.mark.remote_data def test_einsteincoeff_case(): # test untested case for einstein coefficient @@ -352,7 +351,7 @@ def test_einsteincoeff_case(): assert np.isclose(round(au.value, 4), 0.0086) -@remote_data +@pytest.mark.remote_data def test_betafactor_case(): # test untested case for beta beta_factor @@ -381,7 +380,7 @@ def test_betafactor_case(): @pytest.mark.skipif('pyradex is None') -@remote_data +@pytest.mark.remote_data def test_pyradex_case(): transition_freq = (177.196 * u.GHz).to(u.MHz) mol_tag = 29002 @@ -402,7 +401,7 @@ def test_pyradex_case(): @pytest.mark.skipif('pyradex is None') -@remote_data +@pytest.mark.remote_data def test_Haser_prodrate_pyradex(mock_nonlte): co = Table.read(data_path('CO.csv'), format="ascii.csv") @@ -456,7 +455,7 @@ def test_Haser_prodrate_pyradex(mock_nonlte): assert np.all(err < 0.35) -@remote_data +@pytest.mark.remote_data def test_pyradex_cdensity(mock_nonlte): transition_freq = (177.196 * u.GHz).to(u.MHz) diff --git a/sbpy/calib/tests/test_sun.py b/sbpy/calib/tests/test_sun.py index e05dd7c1..5d661b10 100644 --- a/sbpy/calib/tests/test_sun.py +++ b/sbpy/calib/tests/test_sun.py @@ -4,7 +4,6 @@ import pytest import numpy as np import astropy.units as u -from astropy.tests.helper import remote_data from ...units import JMmag, VEGAmag from ...photometry import bandpass from .. import * @@ -136,7 +135,7 @@ def test_meta(self): assert sun.meta is None @pytest.mark.skipif('True') - @remote_data + @pytest.mark.remote_data def test_kurucz_nan_error(self): """sbpy#113 diff --git a/sbpy/calib/tests/test_vega.py b/sbpy/calib/tests/test_vega.py index 37025f10..7ba5928d 100644 --- a/sbpy/calib/tests/test_vega.py +++ b/sbpy/calib/tests/test_vega.py @@ -3,7 +3,6 @@ import pytest import numpy as np import astropy.units as u -from astropy.tests.helper import remote_data from ... import units as sbu from ...photometry import bandpass from .. import core diff --git a/sbpy/spectroscopy/tests/test_spec_remote.py b/sbpy/spectroscopy/tests/test_spec_remote.py index a9528a05..06f2eba9 100644 --- a/sbpy/spectroscopy/tests/test_spec_remote.py +++ b/sbpy/spectroscopy/tests/test_spec_remote.py @@ -3,7 +3,6 @@ import numpy as np import astropy.units as u from astropy.time import Time -from astropy.tests.helper import remote_data from astropy.table import Table from astroquery.lamda import Lamda from astroquery.jplspec import JPLSpec From 1eac3dabc605ca4a0efbedcc82a7a9bbbeffe529 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Thu, 26 May 2022 09:47:02 -0400 Subject: [PATCH 05/59] Remove unused imports. --- sbpy/spectroscopy/tests/test_spec_remote.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/sbpy/spectroscopy/tests/test_spec_remote.py b/sbpy/spectroscopy/tests/test_spec_remote.py index 06f2eba9..104bfce1 100644 --- a/sbpy/spectroscopy/tests/test_spec_remote.py +++ b/sbpy/spectroscopy/tests/test_spec_remote.py @@ -1,15 +1,5 @@ import os -import numpy as np -import astropy.units as u -from astropy.time import Time -from astropy.table import Table -from astroquery.lamda import Lamda -from astroquery.jplspec import JPLSpec -from ...activity.gas import Haser, photo_timescale -from ...data import Ephem -from .. import Spectrum - def data_path(filename): data_dir = os.path.join(os.path.dirname(__file__), 'data') From fc6c836d9cd1768805e3cc9e54f30e1c82e54576 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Fri, 27 May 2022 19:45:03 -0400 Subject: [PATCH 06/59] Revise ``Names`` parsing, and clarify A/ and I/ behavior. --- docs/sbpy/data/names.rst | 64 +++++++++++++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/docs/sbpy/data/names.rst b/docs/sbpy/data/names.rst index 358ecb3c..7fc70f63 100644 --- a/docs/sbpy/data/names.rst +++ b/docs/sbpy/data/names.rst @@ -8,6 +8,9 @@ in that it does not use `~sbpy.data.DataClass` as a base class. Instead, umbrella for functions to identify asteroid and comet names, numbers, and designations. +Cometary and Asteroidal Name Parsing +------------------------------------ + In order to distinguish if a string designates a comet or an asteroid, you can use the following code: @@ -22,23 +25,56 @@ strings and find patterns that agree with asteroid and comet names, numbers, and designations. There are separate tasks to identify asteroid and comet identifiers: - >>> Names.parse_asteroid('(228195) 6675 P-L') # doctest: +SKIP + >>> Names.parse_asteroid('(228195) 6675 P-L') {'number': 228195, 'desig': '6675 P-L'} - >>> Names.parse_asteroid('C/2001 A2-A (LINEAR)') # doctest: +SKIP - ... sbpy.data.names.TargetNameParseError: C/2001 A2-A (LINEAR) does not appear to be an asteroid identifier - >>> Names.parse_comet('12893') # doctest: +SKIP - ... sbpy.data.names.TargetNameParseError: 12893 does not appear to be a comet name - >>> Names.parse_comet('73P-C/Schwassmann Wachmann 3 C') # doctest: +SKIP + >>> Names.parse_comet('73P-C/Schwassmann Wachmann 3 C') {'type': 'P', 'number': 73, 'fragment': 'C', 'name': 'Schwassmann Wachmann 3 C'} -In order to be able to distinguish between asteroid and comet -identifiers, `sbpy` follows the MPC guideline in that it requires -comet identifiers to include the comet type in either in combination -with a number (e.g., ``'259P'``), a name (e.g., ``'P/Halley'``), or -both (e.g., ``'2P/Encke'``). For instance, the identifier ``'Halley'`` -would be identified as an asteroid, as it lacks a comet type -identifier. Hence, some caution is advised when using these routines - -identification might not be unambiguous. +These methods will raise exceptions when the name cannot be parsed as expected: + + >>> Names.parse_asteroid('C/2001 A2-A (LINEAR)') + Traceback (most recent call last): + ... + sbpy.data.names.TargetNameParseError: C/2001 A2-A (LINEAR) does not appear to be an asteroid identifier + >>> Names.parse_comet('12893') + Traceback (most recent call last): + ... + sbpy.data.names.TargetNameParseError: 12893 does not appear to be a comet name + +In order to be able to distinguish between asteroid and comet identifiers, +`sbpy` follows the MPC guideline in that it requires comet identifiers to +include the comet type in either in combination with a number (e.g., +``'259P'``), a name (e.g., ``'P/Halley'``), or both (e.g., ``'2P/Encke'``). For +instance, the identifier ``'Halley'`` would be identified as an asteroid, as it +lacks a comet type identifier. Hence, some caution is advised when using these +routines - identification might not be unambiguous. + +A/ objects: asteroids in cometary orbits +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Small bodies designated with an A/ prefix have cometary orbits, but appear +asteroidal [MPEC2018H54]_. ``sbpy`` considers them to be asteroids: + + >>> Names.asteroid_or_comet('A/2018 V3') + 'comet' + +Interstellar objects +^^^^^^^^^^^^^^^^^^^^ + +Interstellar object designations, which start with an I/, do not +give any insight into the nature of the object. For example, 1I/ʻOumuamua was +asteroidal in appearance but 2I/Borisov was cometary. ``sbpy`` raises an +exception for I/ objects: + + >>> Names.asteroid_or_comet('1I/ʻOumuamua') + Traceback (most recent call last): + File "", line 1, in + File "/disks/data0/astro/Projects/sbpy/sbpy/data/names.py", line 597, in asteroid_or_comet + raise TargetNameParseError('Target nature unclear.') + sbpy.data.names.TargetNameParseError: Target nature unclear. + + +.. [MPEC2018H54] Williams, G. V. 2018. A/ Objects. MPEC 2018-H54. https://minorplanetcenter.net/mpec/K18/K18H54.html Sorting names with a natural sort order --------------------------------------- From ceee76a3eb5891856ce5e33119be8acb00657a21 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Sat, 4 Jun 2022 13:45:48 -0400 Subject: [PATCH 07/59] Re-organize Ephem documentation. Includes adding headers, summarize from_horizons's main options first, and address Issue #241. --- docs/sbpy/data/ephem.rst | 369 ++++++++++++++++++++++++--------------- 1 file changed, 227 insertions(+), 142 deletions(-) diff --git a/docs/sbpy/data/ephem.rst b/docs/sbpy/data/ephem.rst index 89fc73e7..aaf29fbe 100644 --- a/docs/sbpy/data/ephem.rst +++ b/docs/sbpy/data/ephem.rst @@ -2,88 +2,107 @@ Ephemeris Objects (`sbpy.data.Ephem`) ===================================== -As shown above (:ref:`How to use Data Containers`), -`~sbpy.data.Ephem` objects can be created on the fly. However, -`~sbpy.data.Ephem` can also be used to access ephemerides information -from remote services with a largely uniform API. +As described in :ref:`How to use Data Containers`, `~sbpy.data.Ephem` objects +can be created on the fly. However, `~sbpy.data.Ephem` can also be used to +access ephemeris information from other sources with a largely uniform API. -For instance, the following few lines will query -ephemerides for asteroid Ceres on a given date and for the position of -Mauna Kea Observatory (IAU observatory code ``568``) from the `JPL Horizons service `_: + +JPL Horizons (`~sbpy.data.Ephem.from_horizons`) +----------------------------------------------- + +`~sbpy.data.Ephem.from_horizons` uses one or more target names, an observer +location, and the ephemeris epoch. For instance, the following few lines will +query for ephemerides of asteroid Ceres on a given date and for the position of +Mauna Kea Observatory (IAU observatory code 568) from the `JPL Horizons service +`_: + +.. doctest-remote-data:: >>> from sbpy.data import Ephem >>> from astropy.time import Time >>> epoch = Time('2018-08-03 14:20', scale='utc') # time in UT >>> eph = Ephem.from_horizons('Ceres', ... location='568', - ... epochs=epoch) # doctest: +REMOTE_DATA - >>> eph # doctest: +REMOTE_DATA +SKIP - - targetname H G solar_presence ... PABLon PABLat epoch - mag ... deg deg - str7 float64 float64 str1 ... float64 float64 object - ---------- ------- ------- -------------- ... ------- ------- ----------------- - 1 Ceres 3.34 0.12 ... 171.275 9.3473 2458334.097222222 - >>> eph.field_names # doctest: +REMOTE_DATA + ... epochs=epoch) + >>> eph['epoch', 'ra', 'dec', 'rh', 'delta', 'phase'] # doctest: +SKIP + + epoch RA DEC r delta alpha + deg deg AU AU deg + Time float64 float64 float64 float64 float64 + ----------------- --------- -------- -------------- ---------------- ------- + 2458334.097222222 169.23471 13.40412 2.573882458571 3.34213404869399 12.978 + +The full column name list in the data table can be retrieved with the +`~sbpy.data.DataClass.field_names` property: + +.. doctest-remote-data:: + + >>> eph.field_names ['targetname', 'H', 'G', 'solar_presence', 'flags', 'RA', 'DEC', 'RA_app', 'DEC_app', 'RA*cos(Dec)_rate', 'DEC_rate', 'AZ', 'EL', 'AZ_rate', 'EL_rate', 'sat_X', 'sat_Y', 'sat_PANG', 'siderealtime', 'airmass', 'magextinct', 'V', 'surfbright', 'illumination', 'illum_defect', 'sat_sep', 'sat_vis', 'ang_width', 'PDObsLon', 'PDObsLat', 'PDSunLon', 'PDSunLat', 'SubSol_ang', 'SubSol_dist', 'NPole_ang', 'NPole_dist', 'EclLon', 'EclLat', 'r', 'r_rate', 'delta', 'delta_rate', 'lighttime', 'vel_sun', 'vel_obs', 'elong', 'elongFlag', 'alpha', 'lunar_elong', 'lunar_illum', 'sat_alpha', 'sunTargetPA', 'velocityPA', 'OrbPlaneAng', 'constellation', 'TDB-UT', 'ObsEclLon', 'ObsEclLat', 'NPole_RA', 'NPole_DEC', 'GlxLon', 'GlxLat', 'solartime', 'earth_lighttime', 'RA_3sigma', 'DEC_3sigma', 'SMAA_3sigma', 'SMIA_3sigma', 'Theta_3sigma', 'Area_3sigma', 'RSS_3sigma', 'r_3sigma', 'r_rate_3sigma', 'SBand_3sigma', 'XBand_3sigma', 'DoppDelay_3sigma', 'true_anom', 'hour_angle', 'alpha_true', 'PABLon', 'PABLat', 'epoch'] -`~sbpy.data.Ephem.from_horizons` uses one or more target names, an -observer location in the form of an IAU observatory code, and a list -of discrete epochs or a range of epochs defined in a dictionary (see -`~sbpy.data.Ephem.from_horizons`) to query the JPL Horizons -service. Epochs have to be provided in the form of `~astropy.time.Time` -objects. The column names in the data table can be inquired using -`~sbpy.data.DataClass.field_names`. -`~sbpy.data.Ephem.from_horizons` is actually a wrapper around -`~astroquery.jplhorizons.HorizonsClass.ephemerides`. This function -conveniently combines the creation of a -`~astroquery.jplhorizons.HorizonsClass` query and the actual -ephemerides information retrieval into a single function. Additional -optional parameters provided to `~sbpy.data.Ephem.from_horizons` are -directly passed on to -`~astroquery.jplhorizons.HorizonsClass.ephemerides`, maintaining the -full flexibility of the latter function: +Time options +^^^^^^^^^^^^ + +In the above example a specific epoch was specified, but multiple epochs may be +requested, or even a range of epochs. All time references are +`~astropy.time.Time` objects. + +.. doctest-remote-data:: + + >>> epochs = Time(['2022-06-04', '2023-06-04']) + >>> eph = Ephem.from_horizons('Ceres', location='568', epochs=epochs) + >>> eph['epoch', 'rh', 'delta'] # doctest: +SKIP + + epoch r delta + AU AU + Time float64 float64 + --------- -------------- ---------------- + 2459734.5 2.60719514844 3.49018226961962 + 2460099.5 2.603599947555 2.18175266069806 + +Note that the total number of epochs queried using this option should be less +than a few hundred to prevent corruption of the query (see +`~astroquery.jplhorizons.HorizonsClass.ephemerides` for details). + +To specify a range of epochs: + +.. doctest-remote-data:: >>> import astropy.units as u - >>> epoch1 = Time('2018-08-03 14:20', scale='utc') - >>> epoch2 = Time('2018-08-04 07:30', scale='utc') - >>> eph = Ephem.from_horizons('Ceres', - ... location='568', - ... epochs={'start': epoch1, - ... 'stop': epoch2, - ... 'step': 10*u.minute}, - ... skip_daylight=True) # doctest: +REMOTE_DATA - >>> eph # doctest: +REMOTE_DATA +SKIP - - targetname H G ... PABLon PABLat epoch - mag ... deg deg - str7 float64 float64 ... float64 float64 object - ---------- ------- ------- ... -------- ------- ----------------- - 1 Ceres 3.34 0.12 ... 171.275 9.3473 2458334.097222222 - 1 Ceres 3.34 0.12 ... 171.2774 9.3472 2458334.104166667 - 1 Ceres 3.34 0.12 ... 171.2798 9.3471 2458334.111111111 - 1 Ceres 3.34 0.12 ... 171.2822 9.347 2458334.118055556 - 1 Ceres 3.34 0.12 ... 171.2846 9.3469 2458334.125 - 1 Ceres 3.34 0.12 ... 171.2869 9.3468 2458334.131944444 - ... ... ... ... ... ... ... - 1 Ceres 3.34 0.12 ... 171.5076 9.3369 2458334.777777778 - 1 Ceres 3.34 0.12 ... 171.5099 9.3368 2458334.784722222 - 1 Ceres 3.34 0.12 ... 171.5123 9.3367 2458334.791666667 - 1 Ceres 3.34 0.12 ... 171.5147 9.3366 2458334.798611111 - 1 Ceres 3.34 0.12 ... 171.5171 9.3365 2458334.805555556 - 1 Ceres 3.34 0.12 ... 171.5195 9.3364 2458334.8125 - -Note that ``skip_daylight`` is an optional parameter of -`~astroquery.jplhorizons.HorizonsClass.ephemerides` and it can be used -here as well. An additional feature of -`~sbpy.data.Ephem.from_horizons` is that you can automatically -concatenate queries for a number of objects: + >>> + >>> epochs = {'start': Time('2022-01-01'), + ... 'stop': Time('2023-01-01'), + ... 'step': 30 * u.day} + >>> eph = Ephem.from_horizons('Ceres', location='568', epochs=epochs) + >>> eph['epoch', 'rh', 'delta'] # doctest: +SKIP + + epoch r delta + AU AU + Time float64 float64 + --------- -------------- ---------------- + 2459580.5 2.718302197006 1.9062817959135 + 2459610.5 2.694384101844 2.22554330199232 + ... + 2459940.5 2.549833352462 2.30003114258051 + +As an alternative to ``'step'`` one could specify the number of epochs with +``''number'``. + + +Mulitple targets +^^^^^^^^^^^^^^^^ +An additional feature of `~sbpy.data.Ephem.from_horizons` is that you can +automatically concatenate queries for a number of objects: + +.. doctest-remote-data:: + + >>> epoch1 = Time('2018-08-03 14:20') >>> eph = Ephem.from_horizons(['Ceres', 'Pallas', 12893, '1983 SA'], ... location='568', - ... epochs=epoch1) # doctest: +REMOTE_DATA - >>> eph # doctest: +REMOTE_DATA +SKIP + ... epochs=epoch1) + >>> eph # doctest: +SKIP targetname H G ... PABLat epoch mag ... deg @@ -95,39 +114,29 @@ concatenate queries for a number of objects: 3552 Don Quixote (1983 SA) 12.9 0.15 ... 13.3365 2458334.097222222 -Please be aware that these queries are not simultaneous. The more -targets you query, the longer the query will take. Furthermore, keep -in mind that asteroids and comets have slightly different table -layouts (e.g., different magnitude systems: ``T-mag`` and ``N-mag`` -instead of ``V-mag``), which will complicate the interpretation of the -data. It might be safest to query asteroids and comets separately. - -Also note that the two examples shown above use different ways to -define epochs. The first example uses a dictionary that defines a -``start`` and ``stop`` epoch, as well as a ``step`` size (see -`~sbpy.data.Ephem.from_horizons` for -details). Instead of a ``step`` size, the user can also provide a -``number`` of steps as an integer; ``step`` is then automatically -derived from the interval and ``number`` in units of full minutes. The -second example uses a specific epoch as input. The -`~astropy.time.Time` object provided to ``epochs`` can also be -initialized with a list of epochs, querying multiple epochs at the -same time. Note that the total number of epochs queried using this -option should be less than a few hundred to prevent corruption of the -query (see `~astroquery.jplhorizons.HorizonsClass.ephemerides` for -details). - -Observer locations can be defined as strings using official `IAU -observatory codes -`__ or -using `~astropy.coordinates.EarthLocation` as shown in the following +Please be aware that these queries are not simultaneous. The more targets you +query, the longer the query will take. Furthermore, keep in mind that asteroids +and comets have slightly different table layouts (e.g., different magnitude +systems: ``T-mag`` and ``N-mag`` instead of ``V-mag``), which will complicate +the interpretation of the data. It might be safest to query asteroids and comets +separately. + + +Observer locations +^^^^^^^^^^^^^^^^^^ + +Observer locations can be defined as strings using official `IAU observatory +codes `__ as above, +or by using `~astropy.coordinates.EarthLocation` as shown in the following example: +.. doctest-remote-data:: + >>> from astropy.coordinates import EarthLocation - >>> lowell = EarthLocation.of_site('Lowell Observatory') # doctest: +SKIP - >>> eph = Ephem.from_horizons(1, epochs=Time('2018-01-01', format='iso'), - ... location=lowell) # doctest: +SKIP - >>> eph # doctest: +REMOTE_DATA +SKIP + >>> lowell = EarthLocation.of_site('Lowell Observatory') + >>> eph = Ephem.from_horizons(1, epochs=Time('2018-01-01'), + ... location=lowell) + >>> eph # doctest: +SKIP targetname H G solar_presence ... PABLon PABLat epoch mag ... deg deg @@ -135,15 +144,65 @@ example: ---------- ------- ------- -------------- ... -------- ------- --------- 1 Ceres 3.34 0.12 * ... 130.4303 9.2004 2458119.5 -Offering almost identical functionality, the -`~sbpy.data.Ephem.from_mpc` method will retrieve ephemerides from the -`Minor Planet Center `_: + +Optional parameters +^^^^^^^^^^^^^^^^^^^ + +`~sbpy.data.Ephem.from_horizons` is actually a wrapper around +`astroquery.jplhorizons.HorizonsClass`. Additional optional parameters provided +to `~sbpy.data.Ephem.from_horizons` are directly passed on to +`astroquery.jplhorizons.HorizonsClass.ephemerides`, maintaining the full +flexibility of the latter function. For example one may use the +``skip_daylight`` keyword argument: + +.. doctest-remote-data:: + + >>> epoch1 = Time('2018-08-03 14:20', scale='utc') + >>> epoch2 = Time('2018-08-04 07:30', scale='utc') + >>> eph = Ephem.from_horizons('Ceres', + ... location='568', + ... epochs={'start': epoch1, + ... 'stop': epoch2, + ... 'step': 10 * u.minute}, + ... skip_daylight=True) + +Or, a common option for periodic cometary targets is to limit orbit look-ups to +the apparition closest to the epochs being queried (requires +``id_type='designation'``): + +.. doctest-remote-data:: + + >>> eph = Ephem.from_horizons('2P') # doctest: +SKIP + Traceback (most recent call last): + ... + ValueError: Ambiguous target name; provide unique id: + Record # Epoch-yr >MATCH DESIG< Primary Desig Name + -------- -------- ------------- ------------- ------------------------- + 90000034 1786 2P 2P Encke + 90000035 1796 2P 2P Encke + 90000036 1805 2P 2P Encke + ... + >>> eph = Ephem.from_horizons('2P', id_type='designation', closest_apparition=True) + >>> print(eph['targetname']) + targetname + ---------- + 2P/Encke + + +Minor Planet Center's Ephemeris Service (`~sbpy.data.Ephem.from_mpc`) +--------------------------------------------------------------------- + +Offering similar functionality, the `~sbpy.data.Ephem.from_mpc` method will +retrieve ephemerides from the `Minor Planet Center's Ephemeris Service +`_: + +.. doctest-remote-data:: >>> eph = Ephem.from_mpc('2P', location='568', ... epochs={'start': Time('2018-10-22'), ... 'stop': Time('2018-10-26'), ... 'step': 1*u.day}) # doctest: +REMOTE_DATA - >>> eph # doctest: +REMOTE_DATA +SKIP + >>> eph # doctest: +SKIP Targetname Date ... Moon distance Moon altitude ... deg deg @@ -155,16 +214,22 @@ Offering almost identical functionality, the 2P 2018-10-26 00:00:00.000 ... 81.0 -56.0 2P 2018-10-23 00:00:00.000 ... 41.0 -41.0 -Finally, `~sbpy.data.Ephem.from_miriade` will retrieve ephemerides -from the `Miriade ephemeris generator -`_ at `IMCCE + +IMCCE's Miriade (`~sbpy.data.Ephem.from_miriade`) +------------------------------------------------- + +Finally, `~sbpy.data.Ephem.from_miriade` will retrieve ephemerides from the +`Miriade ephemeris generator `_ at +`Institut de Mécanique Céleste et de Calcul des Éphémérides `_: +.. doctest-remote-data:: + >>> eph = Ephem.from_miriade('2P', objtype='comet', location='568', ... epochs={'start': Time('2018-10-22'), ... 'stop': Time('2018-10-26'), - ... 'step': 1*u.day}) # doctest: +REMOTE_DATA - >>> eph # doctest: +REMOTE_DATA +SKIP + ... 'step': 1*u.day}) + >>> eph # doctest: +SKIP target epoch RA ... DEC_rate delta_rate deg ... arcsec / min km / s @@ -175,43 +240,63 @@ from the `Miriade ephemeris generator 2P 2458415.5 329.83517041666664 ... -0.055369 25.253586 2P 2458416.5 329.76366666666667 ... -0.051392 25.4700287 2P 2458417.5 329.6967958333333 ... -0.04743 25.677518 - -Ephemerides can also be derived from `~sbpy.data.Orbit` objects using -`sbpy`'s interface to `pyoorb -`_ with the function -`~sbpy.data.Ephem.from_oo`. The following example computes -ephemerides for the next ten days in steps of 1 hr for Ceres as seen -from the Discovery Channel Telescope: + + +Using an orbit and OpenOrb (`~sbpy.data.Ephem.from_oo`) +------------------------------------------------------- + +Ephemerides can also be derived from `~sbpy.data.Orbit` objects using `sbpy`'s +interface to `OpenOrb `_ with the function +`~sbpy.data.Ephem.from_oo`. The following example computes ephemerides for the +next ten days in steps of 1 hr for Ceres as seen from the Discovery Channel +Telescope: + +.. doctest-requires:: oorb >>> import numpy as np - >>> from sbpy.data import Orbit, Ephem >>> from astropy.time import Time - >>> epochs = Time(Time.now().jd + np.arange(0, 10, 1/24), format='jd') - >>> ceres = Orbit.from_horizons('1') # doctest: +REMOTE_DATA - >>> eph = Ephem.from_oo(ceres, epochs, 'G37') # doctest: +SKIP - >>> eph # doctest: +SKIP - - targetname RA ... trueanom epoch - deg ... deg - str7 float64 ... float64 object - ---------- ------------------ ... ------------------ ------------------ - 1 Ceres 238.56187075007446 ... 105.8270438687299 2458694.6423231447 - 1 Ceres 238.564318627966 ... 105.83566067245822 2458694.683989811 - 1 Ceres 238.56680284927273 ... 105.8442772820886 2458694.725656478 - 1 Ceres 238.56933812666867 ... 105.8528936974433 2458694.7673231447 - 1 Ceres 238.57193638137088 ... 105.8615099186335 2458694.808989811 - 1 Ceres 238.57460592776462 ... 105.87012594577034 2458694.850656478 - ... ... ... ... ... - 1 Ceres 239.4677754274348 ... 107.83811369526742 2458704.3923231447 - 1 Ceres 239.4726928414698 ... 107.846685468736 2458704.433989811 - 1 Ceres 239.47756694312102 ... 107.85525705166283 2458704.475656478 - 1 Ceres 239.48240809475683 ... 107.8638284438719 2458704.5173231447 - 1 Ceres 239.48722955376766 ... 107.87239964547449 2458704.558989811 - 1 Ceres 239.49204656314026 ... 107.88097065658197 2458704.600656478 - - -The properties computed by pyoorb and listed in the resulting table -are defined in the `pyoorb documentation -`_. Note that this function requires pyoorb to be installed, which is not a requirement for `sbpy`. - + >>> import astropy.units as u + >>> from sbpy.data import Orbit, Ephem + >>> + >>> ceres = Orbit.from_dict({'targetname': 'Ceres', + ... 'orbtype': 'KEP', + ... 'a': 2.77 * u.au, + ... 'e': 0.0786, + ... 'i': 10.6 * u.deg, + ... 'w': 73.6 * u.deg, + ... 'Omega': 80.3 * u.deg, + ... 'M': 320.3 * u.deg, + ... 'epoch': Time(2459735.0, format='jd'), + ... 'H': 3.3 * u.mag, + ... 'G': 0.15}) + >>> epochs = Time('2022-06-01') + np.arange(31) * u.day + >>> eph = Ephem.from_oo(ceres, epochs, 'G37') + >>> print(eph) + + targetname RA DEC ... trueanom epoch + deg deg ... deg + str5 float64 float64 ... float64 Time + ---------- ------------------ ------------------ ... ------------------ ----------------- + Ceres 97.53508969190534 26.840028524616123 ... 313.25073865769383 2459731.500800741 + Ceres 98.00890363845723 26.840660100535846 ... 313.4904467480157 2459732.500800741 + Ceres 98.48352873396654 26.83985349294406 ... 313.7302633914505 2459733.500800741 + Ceres 98.958932708787 26.83760580100297 ... 313.97018822933694 2459734.500800741 + Ceres 99.43508364177217 26.833914483074352 ... 314.2102209001088 2459735.500800741 + Ceres 99.91195006004624 26.828777353292317 ... 314.4503610392964 2459736.500800741 + Ceres 100.38950104500788 26.822192575950346 ... 314.6906082795292 2459737.500800741 + Ceres 100.86770636534342 26.814158657288353 ... 314.93096225053836 2459738.500800741 + ... ... ... ... ... ... + Ceres 108.10282016659154 26.519545221745105 ... 318.54881889008544 2459753.500800741 + Ceres 108.58838133652219 26.488326782806954 ... 318.79082784334094 2459754.500800741 + Ceres 109.07422128038874 26.455671250331037 ... 319.03293679237225 2459755.500800741 + Ceres 109.56031412985443 26.42158188064386 ... 319.2751453153177 2459756.500800741 + Ceres 110.04663355942071 26.386062295091307 ... 319.5174529874914 2459757.500800741 + Ceres 110.53315292361725 26.34911647975649 ... 319.7598593813884 2459758.500800741 + Ceres 111.01984538555946 26.310748782938763 ... 320.0023640666907 2459759.500800741 + Ceres 111.50668404363827 26.270963909983408 ... 320.24496661027257 2459760.500800741 + Ceres 111.99364205884406 26.229766915255638 ... 320.48766657620695 2459761.500800741 +The properties computed by pyoorb and listed in the resulting table are defined +in the `pyoorb documentation +`_. Note that this function +requires pyoorb to be installed, which is not a requirement for `sbpy`. \ No newline at end of file From bed7d14f5fcb75350716a428424b904dd3071cfd Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Sat, 4 Jun 2022 13:52:32 -0400 Subject: [PATCH 08/59] A/ objects are asteroids. --- docs/sbpy/data/names.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/sbpy/data/names.rst b/docs/sbpy/data/names.rst index 7fc70f63..25a9c023 100644 --- a/docs/sbpy/data/names.rst +++ b/docs/sbpy/data/names.rst @@ -8,6 +8,7 @@ in that it does not use `~sbpy.data.DataClass` as a base class. Instead, umbrella for functions to identify asteroid and comet names, numbers, and designations. + Cometary and Asteroidal Name Parsing ------------------------------------ @@ -49,6 +50,7 @@ instance, the identifier ``'Halley'`` would be identified as an asteroid, as it lacks a comet type identifier. Hence, some caution is advised when using these routines - identification might not be unambiguous. + A/ objects: asteroids in cometary orbits ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -56,7 +58,8 @@ Small bodies designated with an A/ prefix have cometary orbits, but appear asteroidal [MPEC2018H54]_. ``sbpy`` considers them to be asteroids: >>> Names.asteroid_or_comet('A/2018 V3') - 'comet' + 'asteroid' + Interstellar objects ^^^^^^^^^^^^^^^^^^^^ @@ -76,6 +79,7 @@ exception for I/ objects: .. [MPEC2018H54] Williams, G. V. 2018. A/ Objects. MPEC 2018-H54. https://minorplanetcenter.net/mpec/K18/K18H54.html + Sorting names with a natural sort order --------------------------------------- @@ -98,6 +102,7 @@ comparisons are made whenever possible: >>> sorted(comets, key=natural_sort_key) ['2P/Encke', '9P/Tempel 1', '10P/Tempel 2', '101P/Chernykh'] + Packed Numbers and Designations ------------------------------- From 4647e9d3e64188e090e19b6b34dbf6525da56e21 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Sat, 4 Jun 2022 14:15:05 -0400 Subject: [PATCH 09/59] Allow and test asteroids with A/ designations. --- sbpy/data/names.py | 29 +++++++++++++++++------------ sbpy/data/tests/test_names.py | 6 +++--- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/sbpy/data/names.py b/sbpy/data/names.py index 5b70694a..83f13ecd 100644 --- a/sbpy/data/names.py +++ b/sbpy/data/names.py @@ -10,6 +10,7 @@ """ +import re from ..exceptions import SbpyException __all__ = ['Names', 'TargetNameParseError', 'natural_sort_key'] @@ -47,7 +48,6 @@ def natural_sort_key(s): ['2P/Encke', '9P/Tempel 1', '10P/Tempel 2', '101P/Chernykh'] """ - import re keys = tuple() for k in re.split('([0-9]+)', str(s)): keys += (int(k) if k.isdigit() else k,) @@ -307,20 +307,20 @@ def parse_comet(s): """ - import re - # define comet matching pattern pat = ('^(([1-9][0-9]*[PDCX]' '(-[A-Z]{1,2})?)|[PDCX]/)' # type/number/fragm [0,1,2] '|([-]?[0-9]{3,4}[ _][A-Z]{1,2}[0-9]{0,3}(-[1-9A-Z]{0,2})?)' # designation [3,4] - '|((([dvA-Z][a-z\']? ?[A-Za-z\-]*)[ -]?[A-Z]?[1-9]*[a-z]*)' + r'|((([dvA-Z][a-z\']? ?[A-Za-z\-]*)[ -]?[A-Z]?[1-9]*[a-z]*)' '( [1-9A-Z]{1,2})*)' # name [5,6] ) # regex patterns that will be rejected - rej_pat = ('(([1-9][0-9]*[pdcxai]\b)' # small-caps comet number - '|([pdcxai]/))' # small-caps comet type + rej_pat = ('^(([1-9][0-9]*[pdcxiaCXIA]\b)' + # numbered with lower case, X, C, I, or A + '|([pdcxaiAI]/))' + # temporary designation with lower case, I, or A ) raw = s.translate(str.maketrans('()', ' ')).strip() @@ -342,7 +342,7 @@ def parse_comet(s): if len(el[0]) > 0: typnumber = el[0].replace('/', '') try: - r['type'] = re.findall('[PDCXAI]', typnumber)[0] + r['type'] = re.findall('[PDCXI]', typnumber)[0] except IndexError: pass try: @@ -449,10 +449,11 @@ def parse_asteroid(s): +----------------------------------+----------+------+-----------------+ |1A |1A | | | +----------------------------------+----------+------+-----------------+ + |A/2018 V3 |2018 V3 | | | + +----------------------------------+----------+------+-----------------+ - """ - import re + """ pat = ('(([1A][8-9][0-9]{2}[ _][A-Z]{2}[0-9]{0,3}|' '20[0-9]{2}[ _][A-Z]{2}[0-9]{0,3})' @@ -475,14 +476,16 @@ def parse_asteroid(s): # number [7,8] '|^(([1-9][0-9]*A))' # comet-style designations: 1A [10] + '|^A/([12][0-9][0-9][0-9] [A-Z][0-9]+)' + # asteroids with cometary orbits [12] ) # regex patterns that will be rejected - rej_pat = ('([1-2][0-9]{0,3}[ _][A-Z][0-9]*(\b|$))' + rej_pat = ('([CPXD]/[1-2][0-9]{0,3}[ _][A-Z][0-9]*(\b|$))' # comet desig - '|([1-9][0-9]*[PDCXAI]\b)' + '|([1-9][0-9]*[PDCXI]\b)' # comet number - '|([PDCXAI]/)' + '|([PDCXI]/)' # comet type '|([1-2][0-9]{0,3}[ _][a-z]{2}[0-9]{0,3})' ) @@ -527,6 +530,8 @@ def parse_asteroid(s): # comet-style designation elif len(el[10]) > 0: r['desig'] = el[10].strip() + elif len(el[12]) > 0: + r['desig'] = el[12].strip() if len(r) == 0: raise TargetNameParseError(('{} does not appear to be an ' diff --git a/sbpy/data/tests/test_names.py b/sbpy/data/tests/test_names.py index 7c19f768..ece06500 100644 --- a/sbpy/data/tests/test_names.py +++ b/sbpy/data/tests/test_names.py @@ -67,6 +67,7 @@ '`O`o', 'desig': '2006 RJ110'}, "(20123) A900 MA": {'number': 20123, 'desig': '1900 MA'}, "1A": {'desig': '1A'}, + "A/2018 V3": {'desig': '2018 V3'} } @@ -92,9 +93,8 @@ def test_asteroid_or_comet(): assert Names.asteroid_or_comet(comet) == 'comet', \ 'failed for {}'.format(comet) for asteroid in asteroids: - if asteroid != '2017 U1': - assert Names.asteroid_or_comet(asteroid) == 'asteroid', \ - 'failed for {}'.format(asteroid) + assert Names.asteroid_or_comet(asteroid) == 'asteroid', \ + 'failed for {}'.format(asteroid) def test_from_packed(): From 708bd7015291632e28a6e07fa5a5867c86a0bcab Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Sat, 4 Jun 2022 19:48:47 -0400 Subject: [PATCH 10/59] Link Obs.from_mpc to astroquery. --- docs/sbpy/data/obs.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sbpy/data/obs.rst b/docs/sbpy/data/obs.rst index 0a35d3ad..24ef2d64 100644 --- a/docs/sbpy/data/obs.rst +++ b/docs/sbpy/data/obs.rst @@ -5,8 +5,8 @@ Observational Data Objects (`sbpy.data.Obs`) `~sbpy.data.Obs` objects mostly share their functionality with `~sbpy.data.Ephem`, but there are some unique features tailored to observational data. -For instance, this class allows you to query observations reported to -the Minor Planet Center for a given target: +For instance, this class allows you to query observations reported to the Minor +Planet Center for a given target via `astroquery.mpc.MPCClass.get_observations`: >>> from sbpy.data import Obs >>> data = Obs.from_mpc('2019 AA', id_type='asteroid designation') # doctest: +REMOTE_DATA From 9ee2cb71296e9cd2370288ed87022ff5bc7cee4c Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Thu, 16 Jun 2022 15:10:16 -0400 Subject: [PATCH 11/59] Test comet magnitudes in Phys.from_sbdb --- sbpy/data/phys.py | 2 ++ sbpy/data/tests/test_phys_remote.py | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/sbpy/data/phys.py b/sbpy/data/phys.py index 71ef3195..71baf5c4 100644 --- a/sbpy/data/phys.py +++ b/sbpy/data/phys.py @@ -112,6 +112,8 @@ def from_sbdb(cls, targetids, references=False, notes=False): elif key == 'H': # fix for astroquery <0.4.2 columnunits[key].add(u.mag) + elif key in ['M1', 'M2', 'M1_sig', 'M2_sig']: + columnunits[key].add(u.mag) alldata.append(data) diff --git a/sbpy/data/tests/test_phys_remote.py b/sbpy/data/tests/test_phys_remote.py index 28ecb4ef..4d9bf497 100644 --- a/sbpy/data/tests/test_phys_remote.py +++ b/sbpy/data/tests/test_phys_remote.py @@ -1,7 +1,7 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst import pytest - +import astropy.units as u from sbpy.data import Phys @@ -16,3 +16,17 @@ def test_from_sbdb(): # query several objects data = Phys.from_sbdb([n+1 for n in range(5)]) assert len(data.table) == 5 + + +@pytest.mark.remote_data +def test_from_sbdb_comet(): + """Regression test for issue #349. + + As of June 2022, astroquery does not assign units to M1, M2 and their + uncertainties. + + """ + # need a comet with all both M1 and M2, and their uncertainties: + data = Phys.from_sbdb('147P') + for k in ('M1', 'M2', 'M1_sig', 'M2_sig'): + assert isinstance(data[k], u.Quantity) and data[k].unit == u.mag \ No newline at end of file From 25cff7fafaeba33b820b18ab0ca58f8d9c31c03c Mon Sep 17 00:00:00 2001 From: Jian-Yang Li Date: Thu, 23 Jun 2022 23:32:50 -0400 Subject: [PATCH 12/59] skip docstring tests for ceres --- sbpy/data/ephem.py | 4 ++-- sbpy/photometry/core.py | 46 +++++++++++++++++++++-------------------- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/sbpy/data/ephem.py b/sbpy/data/ephem.py index a7419ab7..d7a055a5 100644 --- a/sbpy/data/ephem.py +++ b/sbpy/data/ephem.py @@ -121,7 +121,7 @@ def from_horizons(cls, targetids, id_type='smallbody', >>> from sbpy.data import Ephem >>> from astropy.time import Time >>> epoch = Time('2018-05-14', scale='utc') - >>> eph = Ephem.from_horizons('ceres', epochs=epoch) # doctest: +SKIP + >>> eph = Ephem.from_horizons('ceres', epochs=epoch) # doctest: +REMOTE_DATA """ @@ -535,7 +535,7 @@ def from_miriade(cls, targetids, objtype='asteroid', >>> from sbpy.data import Ephem >>> from astropy.time import Time >>> epoch = Time('2018-05-14', scale='utc') - >>> eph = Ephem.from_horizons('ceres', epochs=epoch) # doctest: +SKIP + >>> eph = Ephem.from_horizons('ceres', epochs=epoch) # doctest: +REMOTE_DATA """ _epochs = None # avoid modifying epochs in-place diff --git a/sbpy/photometry/core.py b/sbpy/photometry/core.py index 6eb1257a..4e56d38b 100644 --- a/sbpy/photometry/core.py +++ b/sbpy/photometry/core.py @@ -149,16 +149,16 @@ class DiskIntegratedPhaseFunc(Fittable1DModel): >>> >>> # Initialize from physical parameters pulled from JPL SBDB >>> phys = Phys.from_sbdb('Ceres') # doctest: +REMOTE_DATA - >>> print(phys['targetname','H','G']) # doctest: +REMOTE_DATA + >>> print(phys['targetname','H','G']) # doctest: +SKIP targetname H G mag str17 float64 float64 ----------------- ------- ------- - 1 Ceres (A801 AA) 3.54 0.12 + 1 Ceres (A801 AA) 3.31 0.12 >>> m = HG.from_phys(phys) # doctest: +REMOTE_DATA INFO: Model initialized for 1 Ceres (A801 AA). [sbpy.photometry.core] - >>> print(m) # doctest: +REMOTE_DATA + >>> print(m) # doctest: +SKIP Model: HG Inputs: ('x',) Outputs: ('y',) @@ -167,22 +167,22 @@ class DiskIntegratedPhaseFunc(Fittable1DModel): H G mag ---- ---- - 3.54 0.12 + 3.31 0.12 >>> print(m.meta['targetname']) # doctest: +REMOTE_DATA 1 Ceres (A801 AA) - >>> print(m.radius) # doctest: +REMOTE_DATA + >>> print(m.radius) # doctest: +SKIP 469.7 km >>> >>> # Initialize from orbital elements pulled from JPL Horizons that also >>> # contain the H and G parameters >>> elem = Orbit.from_horizons('Ceres') # doctest: +REMOTE_DATA - >>> print(elem['targetname','H','G']) # doctest: +REMOTE_DATA + >>> print(elem['targetname','H','G']) # doctest: +SKIP targetname H G mag str17 float64 float64 ----------------- ------- ------- - 1 Ceres (A801 AA) 3.54 0.12 + 1 Ceres (A801 AA) 3.33 0.12 >>> m = HG.from_phys(elem) # doctest: +REMOTE_DATA INFO: Model initialized for 1 Ceres (A801 AA). [sbpy.photometry.core] >>> @@ -278,16 +278,16 @@ def from_phys(cls, phys, **kwargs): >>> >>> # Initialize from physical parameters pulled from JPL SBDB >>> phys = Phys.from_sbdb('Ceres') # doctest: +REMOTE_DATA - >>> print(phys['targetname','H','G']) # doctest: +REMOTE_DATA + >>> print(phys['targetname','H','G']) # doctest: +SKIP targetname H G mag str17 float64 float64 ----------------- ------- ------- - 1 Ceres (A801 AA) 3.54 0.12 + 1 Ceres (A801 AA) 3.31 0.12 >>> m = HG.from_phys(phys) # doctest: +REMOTE_DATA INFO: Model initialized for 1 Ceres (A801 AA). [sbpy.photometry.core] - >>> print(m) # doctest: +REMOTE_DATA + >>> print(m) # doctest: +SKIP Model: HG Inputs: ('x',) Outputs: ('y',) @@ -296,7 +296,7 @@ def from_phys(cls, phys, **kwargs): H G mag ---- ---- - 3.54 0.12 + 3.31 0.12 >>> print(m.meta['targetname']) # doctest: +REMOTE_DATA 1 Ceres (A801 AA) >>> print(m.radius) # doctest: +REMOTE_DATA @@ -306,7 +306,7 @@ def from_phys(cls, phys, **kwargs): >>> phys = Phys.from_sbdb('12893') # doctest: +REMOTE_DATA >>> print('G' in phys.field_names) # doctest: +REMOTE_DATA False - >>> m = HG.from_phys(phys) # doctest: +REMOTE_DATA +SKIP + >>> m = HG.from_phys(phys) # doctest: +SKIP Traceback (most recent call last): File "", line 1, in KeyError: 'field G not available.' @@ -359,13 +359,13 @@ def to_phys(self): >>> >>> # Initialize from physical parameters pulled from JPL SBDB >>> phys = Phys.from_sbdb('Ceres') # doctest: +REMOTE_DATA - >>> print(phys['targetname','radius','H','G']) # doctest: +REMOTE_DATA + >>> print(phys['targetname','radius','H','G']) # doctest: +SKIP targetname radius H G km mag str17 float64 float64 float64 ----------------- ------- ------- ------- - 1 Ceres (A801 AA) 469.7 3.54 0.12 + 1 Ceres (A801 AA) 469.7 3.31 0.12 >>> m = HG.from_phys(phys) # doctest: +REMOTE_DATA INFO: Model initialized for 1 Ceres (A801 AA). [sbpy.photometry.core] >>> m.wfb = 'V' # doctest: +REMOTE_DATA @@ -373,11 +373,11 @@ def to_phys(self): ... p = m.to_phys() # doctest: +REMOTE_DATA >>> print(type(p)) # doctest: +REMOTE_DATA - >>> p.table.pprint(max_width=-1) # doctest: +REMOTE_DATA + >>> p.table.pprint(max_width=-1) # doctest: +SKIP targetname diameter H G pv A km mag ----------------- -------- ---- ---- ------------------- -------------------- - 1 Ceres (A801 AA) 939.4 3.54 0.12 0.07624470768627523 0.027779803126557152 + 1 Ceres (A801 AA) 939.4 3.31 0.12 0.07624470768627523 0.027779803126557152 """ # noqa: E501 cols = {} if (self.meta is not None) and ('targetname' in self.meta.keys()): @@ -588,9 +588,10 @@ def to_mag(self, eph, unit=None, append_results=False, **kwargs): >>> from sbpy.data import Ephem >>> ceres_hg = HG(3.34 * u.mag, 0.12) >>> # parameter `eph` as `~sbpy.data.Ephem` type - >>> eph = Ephem.from_dict({'alpha': np.linspace(0,np.pi*0.9,200)*u.rad, - ... 'r': np.repeat(2.7*u.au, 200), - ... 'delta': np.repeat(1.8*u.au, 200)}) + >>> eph = Ephem.from_dict({'alpha': np.linspace( + ... 0, np.pi * 0.9, 200) * u.rad, + ... 'r': np.repeat(2.7 * u.au, 200), + ... 'delta': np.repeat(1.8 * u.au, 200)}) >>> mag1 = ceres_hg.to_mag(eph) >>> # parameter `eph` as numpy array >>> pha = np.linspace(0, 170, 200) * u.deg @@ -674,9 +675,10 @@ def to_ref(self, eph, normalized=None, append_results=False, **kwargs): >>> from sbpy.data import Ephem >>> ceres_hg = HG(3.34 * u.mag, 0.12, radius = 480 * u.km, wfb= 'V') >>> # parameter `eph` as `~sbpy.data.Ephem` type - >>> eph = Ephem.from_dict({'alpha': np.linspace(0,np.pi*0.9,200)*u.rad, - ... 'r': np.repeat(2.7*u.au, 200), - ... 'delta': np.repeat(1.8*u.au, 200)}) + >>> eph = Ephem.from_dict({'alpha': np.linspace( + ... 0, np.pi * 0.9, 200) * u.rad, + ... 'r': np.repeat(2.7 * u.au, 200), + ... 'delta': np.repeat(1.8 * u.au, 200)}) >>> with solar_fluxd.set({'V': -26.77 * u.mag}): ... ref1 = ceres_hg.to_ref(eph) ... # parameter `eph` as numpy array From 6e4eb30f60786a02e0b6d7cc95fc7af526991504 Mon Sep 17 00:00:00 2001 From: Jian-Yang Li Date: Fri, 24 Jun 2022 13:12:25 -0400 Subject: [PATCH 13/59] fix tests in docs --- docs/sbpy/data/orbit.rst | 7 ++++--- docs/sbpy/photometry.rst | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/sbpy/data/orbit.rst b/docs/sbpy/data/orbit.rst index e1a34ea7..f3a782bf 100644 --- a/docs/sbpy/data/orbit.rst +++ b/docs/sbpy/data/orbit.rst @@ -130,11 +130,12 @@ parameter with respect to Jupiter is used in the dynamical classification of comets. The Tisserand parameter can be calculated by `~sbpy.Orbit.tisserand` as follows: + >>> epoch = Time(2449400.5, format='jd', scale='tdb') >>> halley = Orbit.from_horizons('1P', id_type='designation', - ... closest_apparition=True) # doctest: +REMOTE_DATA + ... closest_apparition=True, epochs=epoch) # doctest: +REMOTE_DATA >>> T = halley.tisserand() # doctest: +REMOTE_DATA - >>> print('{:.4f}'.format(T)) # doctest: +REMOTE_DATA - -0.6164 + >>> print('{:.4f}'.format(T)) # doctest: +SKIP + -0.6050 One can also specify the planet with respect to which the Tisserand parameter is calculated with optional parameter `planet`. It also allows multiple diff --git a/docs/sbpy/photometry.rst b/docs/sbpy/photometry.rst index ef289e58..2e715b83 100644 --- a/docs/sbpy/photometry.rst +++ b/docs/sbpy/photometry.rst @@ -85,7 +85,7 @@ model parameters: >>> phys = Phys.from_sbdb('Ceres') # doctest: +REMOTE_DATA >>> m = HG.from_phys(phys) # doctest: +REMOTE_DATA INFO: Model initialized for 1 Ceres (A801 AA). [sbpy.photometry.core] - >>> print(m) # doctest: +REMOTE_DATA + >>> print(m) # doctest: +SKIP Model: HG Inputs: ('x',) Outputs: ('y',) @@ -94,7 +94,7 @@ model parameters: H G mag ---- ---- - 3.54 0.12 + 3.31 0.12 Note that model set is not supported. Only one model can be initialized with the first set of valid parameters in the input `~sbpy.data.DataClass`. From cd53c7341623f8b97011cdeb4a589b10531eae72 Mon Sep 17 00:00:00 2001 From: Jian-Yang Li Date: Fri, 24 Jun 2022 14:28:23 -0400 Subject: [PATCH 14/59] use fixed epoch for chariklo --- sbpy/data/tests/test_orbit_remote.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sbpy/data/tests/test_orbit_remote.py b/sbpy/data/tests/test_orbit_remote.py index 7d7350b9..6c01a3f9 100644 --- a/sbpy/data/tests/test_orbit_remote.py +++ b/sbpy/data/tests/test_orbit_remote.py @@ -205,6 +205,7 @@ def test_tisserand(self): jupiter = Orbit.from_horizons(599, id_type=None, epochs=epoch) assert u.isclose(halley.tisserand(jupiter), -0.60495016) - chariklo = Orbit.from_horizons('chariklo', id_type='name') + chariklo = Orbit.from_horizons('chariklo', id_type='name', + closest_apparition=True, epochs=epoch) assert u.allclose(chariklo.tisserand(['599', '699', '799', '899']), - [3.48480375, 2.93140786, 2.85804178, 3.22352516], atol=1e-5) + [3.47746154, 2.92700084, 2.85779878, 3.22653384], atol=1e-5) From 783bc722102b4737572ae7d035fd19ba119dd97e Mon Sep 17 00:00:00 2001 From: Jian-Yang Li Date: Fri, 24 Jun 2022 14:41:52 -0400 Subject: [PATCH 15/59] pep8 fixes --- sbpy/data/ephem.py | 6 ++++-- sbpy/data/tests/test_orbit_remote.py | 7 ++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/sbpy/data/ephem.py b/sbpy/data/ephem.py index d7a055a5..6f2e893e 100644 --- a/sbpy/data/ephem.py +++ b/sbpy/data/ephem.py @@ -121,7 +121,8 @@ def from_horizons(cls, targetids, id_type='smallbody', >>> from sbpy.data import Ephem >>> from astropy.time import Time >>> epoch = Time('2018-05-14', scale='utc') - >>> eph = Ephem.from_horizons('ceres', epochs=epoch) # doctest: +REMOTE_DATA + >>> eph = Ephem.from_horizons('ceres', epochs=epoch) + ... # doctest: +REMOTE_DATA """ @@ -535,7 +536,8 @@ def from_miriade(cls, targetids, objtype='asteroid', >>> from sbpy.data import Ephem >>> from astropy.time import Time >>> epoch = Time('2018-05-14', scale='utc') - >>> eph = Ephem.from_horizons('ceres', epochs=epoch) # doctest: +REMOTE_DATA + >>> eph = Ephem.from_horizons('ceres', epochs=epoch) + ... # doctest: +REMOTE_DATA """ _epochs = None # avoid modifying epochs in-place diff --git a/sbpy/data/tests/test_orbit_remote.py b/sbpy/data/tests/test_orbit_remote.py index 6c01a3f9..74e69528 100644 --- a/sbpy/data/tests/test_orbit_remote.py +++ b/sbpy/data/tests/test_orbit_remote.py @@ -201,11 +201,12 @@ def test_tisserand(self): """ epoch = Time(2449400.5, format='jd', scale='tdb') halley = Orbit.from_horizons('1P', id_type='designation', - closest_apparition=True, epochs=epoch) + closest_apparition=True, epochs=epoch) jupiter = Orbit.from_horizons(599, id_type=None, epochs=epoch) assert u.isclose(halley.tisserand(jupiter), -0.60495016) chariklo = Orbit.from_horizons('chariklo', id_type='name', - closest_apparition=True, epochs=epoch) + closest_apparition=True, epochs=epoch) assert u.allclose(chariklo.tisserand(['599', '699', '799', '899']), - [3.47746154, 2.92700084, 2.85779878, 3.22653384], atol=1e-5) + [3.47746154, 2.92700084, 2.85779878, 3.22653384], + atol=1e-5) From c93ac7f270c8c0803f1b87372c1d543393f991fe Mon Sep 17 00:00:00 2001 From: Jian-Yang Li Date: Thu, 23 Jun 2022 23:32:50 -0400 Subject: [PATCH 16/59] skip docstring tests for ceres --- sbpy/data/ephem.py | 4 ++-- sbpy/photometry/core.py | 46 +++++++++++++++++++++-------------------- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/sbpy/data/ephem.py b/sbpy/data/ephem.py index a7419ab7..d7a055a5 100644 --- a/sbpy/data/ephem.py +++ b/sbpy/data/ephem.py @@ -121,7 +121,7 @@ def from_horizons(cls, targetids, id_type='smallbody', >>> from sbpy.data import Ephem >>> from astropy.time import Time >>> epoch = Time('2018-05-14', scale='utc') - >>> eph = Ephem.from_horizons('ceres', epochs=epoch) # doctest: +SKIP + >>> eph = Ephem.from_horizons('ceres', epochs=epoch) # doctest: +REMOTE_DATA """ @@ -535,7 +535,7 @@ def from_miriade(cls, targetids, objtype='asteroid', >>> from sbpy.data import Ephem >>> from astropy.time import Time >>> epoch = Time('2018-05-14', scale='utc') - >>> eph = Ephem.from_horizons('ceres', epochs=epoch) # doctest: +SKIP + >>> eph = Ephem.from_horizons('ceres', epochs=epoch) # doctest: +REMOTE_DATA """ _epochs = None # avoid modifying epochs in-place diff --git a/sbpy/photometry/core.py b/sbpy/photometry/core.py index 6eb1257a..4e56d38b 100644 --- a/sbpy/photometry/core.py +++ b/sbpy/photometry/core.py @@ -149,16 +149,16 @@ class DiskIntegratedPhaseFunc(Fittable1DModel): >>> >>> # Initialize from physical parameters pulled from JPL SBDB >>> phys = Phys.from_sbdb('Ceres') # doctest: +REMOTE_DATA - >>> print(phys['targetname','H','G']) # doctest: +REMOTE_DATA + >>> print(phys['targetname','H','G']) # doctest: +SKIP targetname H G mag str17 float64 float64 ----------------- ------- ------- - 1 Ceres (A801 AA) 3.54 0.12 + 1 Ceres (A801 AA) 3.31 0.12 >>> m = HG.from_phys(phys) # doctest: +REMOTE_DATA INFO: Model initialized for 1 Ceres (A801 AA). [sbpy.photometry.core] - >>> print(m) # doctest: +REMOTE_DATA + >>> print(m) # doctest: +SKIP Model: HG Inputs: ('x',) Outputs: ('y',) @@ -167,22 +167,22 @@ class DiskIntegratedPhaseFunc(Fittable1DModel): H G mag ---- ---- - 3.54 0.12 + 3.31 0.12 >>> print(m.meta['targetname']) # doctest: +REMOTE_DATA 1 Ceres (A801 AA) - >>> print(m.radius) # doctest: +REMOTE_DATA + >>> print(m.radius) # doctest: +SKIP 469.7 km >>> >>> # Initialize from orbital elements pulled from JPL Horizons that also >>> # contain the H and G parameters >>> elem = Orbit.from_horizons('Ceres') # doctest: +REMOTE_DATA - >>> print(elem['targetname','H','G']) # doctest: +REMOTE_DATA + >>> print(elem['targetname','H','G']) # doctest: +SKIP targetname H G mag str17 float64 float64 ----------------- ------- ------- - 1 Ceres (A801 AA) 3.54 0.12 + 1 Ceres (A801 AA) 3.33 0.12 >>> m = HG.from_phys(elem) # doctest: +REMOTE_DATA INFO: Model initialized for 1 Ceres (A801 AA). [sbpy.photometry.core] >>> @@ -278,16 +278,16 @@ def from_phys(cls, phys, **kwargs): >>> >>> # Initialize from physical parameters pulled from JPL SBDB >>> phys = Phys.from_sbdb('Ceres') # doctest: +REMOTE_DATA - >>> print(phys['targetname','H','G']) # doctest: +REMOTE_DATA + >>> print(phys['targetname','H','G']) # doctest: +SKIP targetname H G mag str17 float64 float64 ----------------- ------- ------- - 1 Ceres (A801 AA) 3.54 0.12 + 1 Ceres (A801 AA) 3.31 0.12 >>> m = HG.from_phys(phys) # doctest: +REMOTE_DATA INFO: Model initialized for 1 Ceres (A801 AA). [sbpy.photometry.core] - >>> print(m) # doctest: +REMOTE_DATA + >>> print(m) # doctest: +SKIP Model: HG Inputs: ('x',) Outputs: ('y',) @@ -296,7 +296,7 @@ def from_phys(cls, phys, **kwargs): H G mag ---- ---- - 3.54 0.12 + 3.31 0.12 >>> print(m.meta['targetname']) # doctest: +REMOTE_DATA 1 Ceres (A801 AA) >>> print(m.radius) # doctest: +REMOTE_DATA @@ -306,7 +306,7 @@ def from_phys(cls, phys, **kwargs): >>> phys = Phys.from_sbdb('12893') # doctest: +REMOTE_DATA >>> print('G' in phys.field_names) # doctest: +REMOTE_DATA False - >>> m = HG.from_phys(phys) # doctest: +REMOTE_DATA +SKIP + >>> m = HG.from_phys(phys) # doctest: +SKIP Traceback (most recent call last): File "", line 1, in KeyError: 'field G not available.' @@ -359,13 +359,13 @@ def to_phys(self): >>> >>> # Initialize from physical parameters pulled from JPL SBDB >>> phys = Phys.from_sbdb('Ceres') # doctest: +REMOTE_DATA - >>> print(phys['targetname','radius','H','G']) # doctest: +REMOTE_DATA + >>> print(phys['targetname','radius','H','G']) # doctest: +SKIP targetname radius H G km mag str17 float64 float64 float64 ----------------- ------- ------- ------- - 1 Ceres (A801 AA) 469.7 3.54 0.12 + 1 Ceres (A801 AA) 469.7 3.31 0.12 >>> m = HG.from_phys(phys) # doctest: +REMOTE_DATA INFO: Model initialized for 1 Ceres (A801 AA). [sbpy.photometry.core] >>> m.wfb = 'V' # doctest: +REMOTE_DATA @@ -373,11 +373,11 @@ def to_phys(self): ... p = m.to_phys() # doctest: +REMOTE_DATA >>> print(type(p)) # doctest: +REMOTE_DATA - >>> p.table.pprint(max_width=-1) # doctest: +REMOTE_DATA + >>> p.table.pprint(max_width=-1) # doctest: +SKIP targetname diameter H G pv A km mag ----------------- -------- ---- ---- ------------------- -------------------- - 1 Ceres (A801 AA) 939.4 3.54 0.12 0.07624470768627523 0.027779803126557152 + 1 Ceres (A801 AA) 939.4 3.31 0.12 0.07624470768627523 0.027779803126557152 """ # noqa: E501 cols = {} if (self.meta is not None) and ('targetname' in self.meta.keys()): @@ -588,9 +588,10 @@ def to_mag(self, eph, unit=None, append_results=False, **kwargs): >>> from sbpy.data import Ephem >>> ceres_hg = HG(3.34 * u.mag, 0.12) >>> # parameter `eph` as `~sbpy.data.Ephem` type - >>> eph = Ephem.from_dict({'alpha': np.linspace(0,np.pi*0.9,200)*u.rad, - ... 'r': np.repeat(2.7*u.au, 200), - ... 'delta': np.repeat(1.8*u.au, 200)}) + >>> eph = Ephem.from_dict({'alpha': np.linspace( + ... 0, np.pi * 0.9, 200) * u.rad, + ... 'r': np.repeat(2.7 * u.au, 200), + ... 'delta': np.repeat(1.8 * u.au, 200)}) >>> mag1 = ceres_hg.to_mag(eph) >>> # parameter `eph` as numpy array >>> pha = np.linspace(0, 170, 200) * u.deg @@ -674,9 +675,10 @@ def to_ref(self, eph, normalized=None, append_results=False, **kwargs): >>> from sbpy.data import Ephem >>> ceres_hg = HG(3.34 * u.mag, 0.12, radius = 480 * u.km, wfb= 'V') >>> # parameter `eph` as `~sbpy.data.Ephem` type - >>> eph = Ephem.from_dict({'alpha': np.linspace(0,np.pi*0.9,200)*u.rad, - ... 'r': np.repeat(2.7*u.au, 200), - ... 'delta': np.repeat(1.8*u.au, 200)}) + >>> eph = Ephem.from_dict({'alpha': np.linspace( + ... 0, np.pi * 0.9, 200) * u.rad, + ... 'r': np.repeat(2.7 * u.au, 200), + ... 'delta': np.repeat(1.8 * u.au, 200)}) >>> with solar_fluxd.set({'V': -26.77 * u.mag}): ... ref1 = ceres_hg.to_ref(eph) ... # parameter `eph` as numpy array From 7eb131b63953820372f1ba8a7d2c192fc9442842 Mon Sep 17 00:00:00 2001 From: Jian-Yang Li Date: Fri, 24 Jun 2022 13:12:25 -0400 Subject: [PATCH 17/59] fix tests in docs --- docs/sbpy/data/orbit.rst | 7 ++++--- docs/sbpy/photometry.rst | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/sbpy/data/orbit.rst b/docs/sbpy/data/orbit.rst index e1a34ea7..f3a782bf 100644 --- a/docs/sbpy/data/orbit.rst +++ b/docs/sbpy/data/orbit.rst @@ -130,11 +130,12 @@ parameter with respect to Jupiter is used in the dynamical classification of comets. The Tisserand parameter can be calculated by `~sbpy.Orbit.tisserand` as follows: + >>> epoch = Time(2449400.5, format='jd', scale='tdb') >>> halley = Orbit.from_horizons('1P', id_type='designation', - ... closest_apparition=True) # doctest: +REMOTE_DATA + ... closest_apparition=True, epochs=epoch) # doctest: +REMOTE_DATA >>> T = halley.tisserand() # doctest: +REMOTE_DATA - >>> print('{:.4f}'.format(T)) # doctest: +REMOTE_DATA - -0.6164 + >>> print('{:.4f}'.format(T)) # doctest: +SKIP + -0.6050 One can also specify the planet with respect to which the Tisserand parameter is calculated with optional parameter `planet`. It also allows multiple diff --git a/docs/sbpy/photometry.rst b/docs/sbpy/photometry.rst index ef289e58..2e715b83 100644 --- a/docs/sbpy/photometry.rst +++ b/docs/sbpy/photometry.rst @@ -85,7 +85,7 @@ model parameters: >>> phys = Phys.from_sbdb('Ceres') # doctest: +REMOTE_DATA >>> m = HG.from_phys(phys) # doctest: +REMOTE_DATA INFO: Model initialized for 1 Ceres (A801 AA). [sbpy.photometry.core] - >>> print(m) # doctest: +REMOTE_DATA + >>> print(m) # doctest: +SKIP Model: HG Inputs: ('x',) Outputs: ('y',) @@ -94,7 +94,7 @@ model parameters: H G mag ---- ---- - 3.54 0.12 + 3.31 0.12 Note that model set is not supported. Only one model can be initialized with the first set of valid parameters in the input `~sbpy.data.DataClass`. From 24e34f816efe0c45b06db699ec30ba6854311467 Mon Sep 17 00:00:00 2001 From: Jian-Yang Li Date: Fri, 24 Jun 2022 14:28:23 -0400 Subject: [PATCH 18/59] use fixed epoch for chariklo --- sbpy/data/tests/test_orbit_remote.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sbpy/data/tests/test_orbit_remote.py b/sbpy/data/tests/test_orbit_remote.py index 7d7350b9..6c01a3f9 100644 --- a/sbpy/data/tests/test_orbit_remote.py +++ b/sbpy/data/tests/test_orbit_remote.py @@ -205,6 +205,7 @@ def test_tisserand(self): jupiter = Orbit.from_horizons(599, id_type=None, epochs=epoch) assert u.isclose(halley.tisserand(jupiter), -0.60495016) - chariklo = Orbit.from_horizons('chariklo', id_type='name') + chariklo = Orbit.from_horizons('chariklo', id_type='name', + closest_apparition=True, epochs=epoch) assert u.allclose(chariklo.tisserand(['599', '699', '799', '899']), - [3.48480375, 2.93140786, 2.85804178, 3.22352516], atol=1e-5) + [3.47746154, 2.92700084, 2.85779878, 3.22653384], atol=1e-5) From 16585d71768d8881a4cf683306f24f04766f5106 Mon Sep 17 00:00:00 2001 From: Jian-Yang Li Date: Fri, 24 Jun 2022 14:41:52 -0400 Subject: [PATCH 19/59] pep8 fixes --- sbpy/data/ephem.py | 6 ++++-- sbpy/data/tests/test_orbit_remote.py | 7 ++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/sbpy/data/ephem.py b/sbpy/data/ephem.py index d7a055a5..6f2e893e 100644 --- a/sbpy/data/ephem.py +++ b/sbpy/data/ephem.py @@ -121,7 +121,8 @@ def from_horizons(cls, targetids, id_type='smallbody', >>> from sbpy.data import Ephem >>> from astropy.time import Time >>> epoch = Time('2018-05-14', scale='utc') - >>> eph = Ephem.from_horizons('ceres', epochs=epoch) # doctest: +REMOTE_DATA + >>> eph = Ephem.from_horizons('ceres', epochs=epoch) + ... # doctest: +REMOTE_DATA """ @@ -535,7 +536,8 @@ def from_miriade(cls, targetids, objtype='asteroid', >>> from sbpy.data import Ephem >>> from astropy.time import Time >>> epoch = Time('2018-05-14', scale='utc') - >>> eph = Ephem.from_horizons('ceres', epochs=epoch) # doctest: +REMOTE_DATA + >>> eph = Ephem.from_horizons('ceres', epochs=epoch) + ... # doctest: +REMOTE_DATA """ _epochs = None # avoid modifying epochs in-place diff --git a/sbpy/data/tests/test_orbit_remote.py b/sbpy/data/tests/test_orbit_remote.py index 6c01a3f9..74e69528 100644 --- a/sbpy/data/tests/test_orbit_remote.py +++ b/sbpy/data/tests/test_orbit_remote.py @@ -201,11 +201,12 @@ def test_tisserand(self): """ epoch = Time(2449400.5, format='jd', scale='tdb') halley = Orbit.from_horizons('1P', id_type='designation', - closest_apparition=True, epochs=epoch) + closest_apparition=True, epochs=epoch) jupiter = Orbit.from_horizons(599, id_type=None, epochs=epoch) assert u.isclose(halley.tisserand(jupiter), -0.60495016) chariklo = Orbit.from_horizons('chariklo', id_type='name', - closest_apparition=True, epochs=epoch) + closest_apparition=True, epochs=epoch) assert u.allclose(chariklo.tisserand(['599', '699', '799', '899']), - [3.47746154, 2.92700084, 2.85779878, 3.22653384], atol=1e-5) + [3.47746154, 2.92700084, 2.85779878, 3.22653384], + atol=1e-5) From 5685a561ee676262e5bae6f33b59ced282152b82 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Sat, 4 Jun 2022 14:15:05 -0400 Subject: [PATCH 20/59] Allow and test asteroids with A/ designations. --- sbpy/data/names.py | 29 +++++++++++++++++------------ sbpy/data/tests/test_names.py | 6 +++--- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/sbpy/data/names.py b/sbpy/data/names.py index 5b70694a..83f13ecd 100644 --- a/sbpy/data/names.py +++ b/sbpy/data/names.py @@ -10,6 +10,7 @@ """ +import re from ..exceptions import SbpyException __all__ = ['Names', 'TargetNameParseError', 'natural_sort_key'] @@ -47,7 +48,6 @@ def natural_sort_key(s): ['2P/Encke', '9P/Tempel 1', '10P/Tempel 2', '101P/Chernykh'] """ - import re keys = tuple() for k in re.split('([0-9]+)', str(s)): keys += (int(k) if k.isdigit() else k,) @@ -307,20 +307,20 @@ def parse_comet(s): """ - import re - # define comet matching pattern pat = ('^(([1-9][0-9]*[PDCX]' '(-[A-Z]{1,2})?)|[PDCX]/)' # type/number/fragm [0,1,2] '|([-]?[0-9]{3,4}[ _][A-Z]{1,2}[0-9]{0,3}(-[1-9A-Z]{0,2})?)' # designation [3,4] - '|((([dvA-Z][a-z\']? ?[A-Za-z\-]*)[ -]?[A-Z]?[1-9]*[a-z]*)' + r'|((([dvA-Z][a-z\']? ?[A-Za-z\-]*)[ -]?[A-Z]?[1-9]*[a-z]*)' '( [1-9A-Z]{1,2})*)' # name [5,6] ) # regex patterns that will be rejected - rej_pat = ('(([1-9][0-9]*[pdcxai]\b)' # small-caps comet number - '|([pdcxai]/))' # small-caps comet type + rej_pat = ('^(([1-9][0-9]*[pdcxiaCXIA]\b)' + # numbered with lower case, X, C, I, or A + '|([pdcxaiAI]/))' + # temporary designation with lower case, I, or A ) raw = s.translate(str.maketrans('()', ' ')).strip() @@ -342,7 +342,7 @@ def parse_comet(s): if len(el[0]) > 0: typnumber = el[0].replace('/', '') try: - r['type'] = re.findall('[PDCXAI]', typnumber)[0] + r['type'] = re.findall('[PDCXI]', typnumber)[0] except IndexError: pass try: @@ -449,10 +449,11 @@ def parse_asteroid(s): +----------------------------------+----------+------+-----------------+ |1A |1A | | | +----------------------------------+----------+------+-----------------+ + |A/2018 V3 |2018 V3 | | | + +----------------------------------+----------+------+-----------------+ - """ - import re + """ pat = ('(([1A][8-9][0-9]{2}[ _][A-Z]{2}[0-9]{0,3}|' '20[0-9]{2}[ _][A-Z]{2}[0-9]{0,3})' @@ -475,14 +476,16 @@ def parse_asteroid(s): # number [7,8] '|^(([1-9][0-9]*A))' # comet-style designations: 1A [10] + '|^A/([12][0-9][0-9][0-9] [A-Z][0-9]+)' + # asteroids with cometary orbits [12] ) # regex patterns that will be rejected - rej_pat = ('([1-2][0-9]{0,3}[ _][A-Z][0-9]*(\b|$))' + rej_pat = ('([CPXD]/[1-2][0-9]{0,3}[ _][A-Z][0-9]*(\b|$))' # comet desig - '|([1-9][0-9]*[PDCXAI]\b)' + '|([1-9][0-9]*[PDCXI]\b)' # comet number - '|([PDCXAI]/)' + '|([PDCXI]/)' # comet type '|([1-2][0-9]{0,3}[ _][a-z]{2}[0-9]{0,3})' ) @@ -527,6 +530,8 @@ def parse_asteroid(s): # comet-style designation elif len(el[10]) > 0: r['desig'] = el[10].strip() + elif len(el[12]) > 0: + r['desig'] = el[12].strip() if len(r) == 0: raise TargetNameParseError(('{} does not appear to be an ' diff --git a/sbpy/data/tests/test_names.py b/sbpy/data/tests/test_names.py index 7c19f768..ece06500 100644 --- a/sbpy/data/tests/test_names.py +++ b/sbpy/data/tests/test_names.py @@ -67,6 +67,7 @@ '`O`o', 'desig': '2006 RJ110'}, "(20123) A900 MA": {'number': 20123, 'desig': '1900 MA'}, "1A": {'desig': '1A'}, + "A/2018 V3": {'desig': '2018 V3'} } @@ -92,9 +93,8 @@ def test_asteroid_or_comet(): assert Names.asteroid_or_comet(comet) == 'comet', \ 'failed for {}'.format(comet) for asteroid in asteroids: - if asteroid != '2017 U1': - assert Names.asteroid_or_comet(asteroid) == 'asteroid', \ - 'failed for {}'.format(asteroid) + assert Names.asteroid_or_comet(asteroid) == 'asteroid', \ + 'failed for {}'.format(asteroid) def test_from_packed(): From 2caa13eeb8ed680e0b56420136fd4f1a8688f3f7 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Wed, 6 Jul 2022 09:12:39 -0400 Subject: [PATCH 21/59] Fix some gaps in I/ logic. --- sbpy/data/names.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sbpy/data/names.py b/sbpy/data/names.py index 83f13ecd..7b332e5e 100644 --- a/sbpy/data/names.py +++ b/sbpy/data/names.py @@ -342,7 +342,7 @@ def parse_comet(s): if len(el[0]) > 0: typnumber = el[0].replace('/', '') try: - r['type'] = re.findall('[PDCXI]', typnumber)[0] + r['type'] = re.findall('[PDCX]', typnumber)[0] except IndexError: pass try: @@ -481,10 +481,10 @@ def parse_asteroid(s): ) # regex patterns that will be rejected - rej_pat = ('([CPXD]/[1-2][0-9]{0,3}[ _][A-Z][0-9]*(\b|$))' - # comet desig + rej_pat = ('([CPXDI]/[1-2][0-9]{0,3}[ _][A-Z][0-9]*(\b|$))' + # comet or interstellar desig '|([1-9][0-9]*[PDCXI]\b)' - # comet number + # comet or interstellar number '|([PDCXI]/)' # comet type '|([1-2][0-9]{0,3}[ _][a-z]{2}[0-9]{0,3})' From 254b50860ec5a6b5a017592227ffd7aa465e5e9c Mon Sep 17 00:00:00 2001 From: Jian-Yang Li Date: Wed, 6 Jul 2022 14:50:45 -0400 Subject: [PATCH 22/59] pep8 fix - add new line at the end of file --- sbpy/data/tests/test_phys_remote.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sbpy/data/tests/test_phys_remote.py b/sbpy/data/tests/test_phys_remote.py index 4d9bf497..b4ba6d3c 100644 --- a/sbpy/data/tests/test_phys_remote.py +++ b/sbpy/data/tests/test_phys_remote.py @@ -29,4 +29,4 @@ def test_from_sbdb_comet(): # need a comet with all both M1 and M2, and their uncertainties: data = Phys.from_sbdb('147P') for k in ('M1', 'M2', 'M1_sig', 'M2_sig'): - assert isinstance(data[k], u.Quantity) and data[k].unit == u.mag \ No newline at end of file + assert isinstance(data[k], u.Quantity) and data[k].unit == u.mag From 273214bff9ff6c657432da60fd1cf0bafa2e05d7 Mon Sep 17 00:00:00 2001 From: Jian-Yang Li Date: Tue, 5 Jul 2022 12:11:01 -0400 Subject: [PATCH 23/59] reorg photometry module code --- sbpy/photometry/__init__.py | 1 + sbpy/photometry/core.py | 424 +--------------------------------- sbpy/photometry/iau.py | 437 ++++++++++++++++++++++++++++++++++++ 3 files changed, 440 insertions(+), 422 deletions(-) create mode 100644 sbpy/photometry/iau.py diff --git a/sbpy/photometry/__init__.py b/sbpy/photometry/__init__.py index 916eebfd..30a9dcf2 100644 --- a/sbpy/photometry/__init__.py +++ b/sbpy/photometry/__init__.py @@ -3,4 +3,5 @@ """ from .core import * +from .iau import * from .bandpass import * diff --git a/sbpy/photometry/core.py b/sbpy/photometry/core.py index 4e56d38b..e34586fb 100644 --- a/sbpy/photometry/core.py +++ b/sbpy/photometry/core.py @@ -6,11 +6,10 @@ """ -__all__ = ['DiskIntegratedPhaseFunc', 'LinearPhaseFunc', 'HG', 'HG12BaseClass', - 'HG12', 'HG1G2', 'HG12_Pen16', 'NonmonotonicPhaseFunctionWarning'] +__all__ = ['DiskIntegratedPhaseFunc', 'LinearPhaseFunc', + 'NonmonotonicPhaseFunctionWarning'] from collections import OrderedDict -import warnings import numpy as np from scipy.integrate import quad from astropy.modeling import (Fittable1DModel, Parameter) @@ -19,7 +18,6 @@ from astropy import log from ..data import (Phys, Obs, Ephem, dataclass_input, quantity_to_dataclass) -from ..bib import cite from ..units import reflectance from ..exceptions import SbpyWarning @@ -804,421 +802,3 @@ def _parameter_units_for_data_units(self, inputs_unit, outputs_unit): return OrderedDict([('H', outputs_unit['y']), ('S', outputs_unit['y']/inputs_unit['x'])]) - -class HG(DiskIntegratedPhaseFunc): - """HG photometric phase model (Bowell et al. 1989) - - Examples - -------- - - >>> # Define the phase function for Ceres with H = 3.34, G = 0.12 - >>> import astropy.units as u - >>> from sbpy.calib import solar_fluxd - >>> from sbpy.photometry import HG - >>> ceres = HG(3.34 * u.mag, 0.12, radius = 480 * u.km, wfb = 'V') - >>> with solar_fluxd.set({'V': -26.77 * u.mag}): - ... print('geometric albedo = {0:.4f}'.format(ceres.geomalb)) - ... print('phase integral = {0:.4f}'.format(ceres.phaseint)) - geometric albedo = 0.0878 - phase integral = 0.3644 - - """ - - _unit = 'mag' - H = Parameter(description='H parameter', default=8) - G = Parameter(description='G parameter', default=0.4) - - @cite({'definition': '1989aste.conf..524B'}) - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - @G.validator - def G(self, value): - """Validate parameter G to avoid non-monotonic phase function - - If G > 1.194, the phase function could potentially be non-monotoic, - and a warning will be issued. - """ - if np.any(value > 1.194): - warnings.warn( - 'G parameter could result in a non-monotonic phase function', - NonmonotonicPhaseFunctionWarning) - - @staticmethod - def _hgphi(pha, i): - """Core function in IAU HG phase function model - - Parameters - ---------- - pha : float or array_like of float - Phase angle - i : int in [1, 2] - Choose the form of function - - Returns - ------- - numpy array of float - - Note - ---- - See Bowell et al. (1989), Eq. A4. - """ - - if i not in [1, 2]: - raise ValueError('i needs to be 1 or 2, {0} received'.format(i)) - - a, b, c = [3.332, 1.862], [0.631, 1.218], [0.986, 0.238] - pha_half = pha*0.5 - sin_pha = np.sin(pha) - tan_pha_half = np.tan(pha_half) - w = np.exp(-90.56 * tan_pha_half * tan_pha_half) - phiis = 1 - c[i-1]*sin_pha/(0.119+1.341*sin_pha - - 0.754*sin_pha*sin_pha) - phiil = np.exp(-a[i-1] * tan_pha_half**b[i-1]) - return w*phiis + (1-w)*phiil - - @staticmethod - def evaluate(pha, hh, gg): - func = (1-gg)*HG._hgphi(pha, 1)+gg*HG._hgphi(pha, 2) - if isinstance(func, u.Quantity): - func = func.value - func = -2.5 * np.log10(func) - if isinstance(hh, u.Quantity): - func = func * hh.unit - return hh + func - - @staticmethod - def fit_deriv(pha, hh, gg): - if hasattr(pha, '__iter__'): - ddh = np.ones_like(pha) - else: - ddh = 1. - phi1 = HG._hgphi(pha, 1) - phi2 = HG._hgphi(pha, 2) - ddg = 1.085736205*(phi1-phi2)/((1-gg)*phi1+gg*phi2) - return [ddh, ddg] - - def _parameter_units_for_data_units(self, inputs_unit, outputs_unit): - return OrderedDict([('H', outputs_unit['y']), - ('G', u.dimensionless_unscaled)]) - - -class HG12BaseClass(DiskIntegratedPhaseFunc): - """Base class for IAU HG1G2 model and HG12 model""" - - _unit = 'mag' - - @cite({'definition': '2010Icar..209..542M'}) - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - @property - def _G1(self): - return None - - @property - def _G2(self): - return None - - @property - def phaseint(self): - """Phase integral, q - Based on Muinonen et al. (2010) Eq. 22 - """ - return 0.009082+0.4061*self._G1+0.8092*self._G2 - - @property - def phasecoeff(self): - """Phase coefficient, k - Based on Muinonen et al. (2010) Eq. 23 - """ - return -(30*self._G1+9*self._G2)/(5*np.pi*float(self._G1+self._G2)) - - @property - def oe_amp(self): - """Opposition effect amplitude, :math:`\zeta-1` - Based on Muinonen et al. (2010) Eq. 24) - """ - tmp = float(self._G1+self._G2) - return (1-tmp)/tmp - - class _spline_positive(_spline): - """ - Define a spline class that clips negative function values - """ - - def __call__(self, x): - y = super().__call__(x) - if hasattr(y, '__iter__'): - y[y < 0] = 0 - else: - if y < 0: - y = 0 - return y - - _phi1v = (np.deg2rad([7.5, 30., 60, 90, 120, 150]), - [7.5e-1, 3.3486016e-1, 1.3410560e-1, - 5.1104756e-2, 2.1465687e-2, 3.6396989e-3], - [-1.9098593, -9.1328612e-2]) - _phi1 = _spline_positive(*_phi1v) - _phi2v = (np.deg2rad([7.5, 30., 60, 90, 120, 150]), - [9.25e-1, 6.2884169e-1, 3.1755495e-1, - 1.2716367e-1, 2.2373903e-2, 1.6505689e-4], - [-5.7295780e-1, -8.6573138e-8]) - _phi2 = _spline_positive(*_phi2v) - _phi3v = (np.deg2rad([0.0, 0.3, 1., 2., 4., 8., 12., 20., 30.]), - [1., 8.3381185e-1, 5.7735424e-1, 4.2144772e-1, 2.3174230e-1, - 1.0348178e-1, 6.1733473e-2, 1.6107006e-2, 0.], - [-1.0630097, 0]) - _phi3 = _spline_positive(*_phi3v) - - -class HG1G2(HG12BaseClass): - """HG1G2 photometric phase model (Muinonen et al. 2010) - - Examples - -------- - - >>> # Define the phase function for Themis with - >>> # H = 7.063, G1 = 0.62, G2 = 0.14 - >>> - >>> import astropy.units as u - >>> from sbpy.calib import solar_fluxd - >>> from sbpy.photometry import HG1G2 - >>> themis = HG1G2(7.063 * u.mag, 0.62, 0.14, radius = 100 * u.km, - ... wfb = 'V') - >>> with solar_fluxd.set({'V': -26.77 * u.mag}): - ... print('geometric albedo = {0:.4f}'.format(themis.geomalb)) - ... print('phase integral = {0:.4f}'.format(themis.phaseint)) - geometric albedo = 0.0656 - phase integral = 0.3742 - """ - - H = Parameter(description='H parameter', default=8) - G1 = Parameter(description='G1 parameter', default=0.2) - G2 = Parameter(description='G2 parameter', default=0.2) - - @G1.validator - def G1(self, value): - """Validate parameter G1 to avoid non-monotonic phase function - - If G1 < 0 or G2 < 0 or G1 + G2 > 1, the phase function could - potentially be non-monotoic, and a warning will be issued. - """ - if np.any(value < 0) or np.any(value + self.G2 > 1): - warnings.warn( - 'G1, G2 parameter combination might result in a non-monotonic' - ' phase function', NonmonotonicPhaseFunctionWarning) - - @G2.validator - def G2(self, value): - """Validate parameter G1 to avoid non-monotonic phase function - - If G1 < 0 or G2 < 0 or G1 + G2 > 1, the phase function could - potentially be non-monotoic, and a warning will be issued. - """ - if np.any(value < 0) or np.any(value + self.G1 > 1): - warnings.warn( - 'G1, G2 parameter combination might result in a non-monotonic' - ' phase function', NonmonotonicPhaseFunctionWarning) - - @property - def _G1(self): - return self.G1.value - - @property - def _G2(self): - return self.G2.value - - @staticmethod - def evaluate(ph, h, g1, g2): - ph = u.Quantity(ph, 'rad').to_value('rad') - func = g1*HG1G2._phi1(ph)+g2*HG1G2._phi2(ph)+(1-g1-g2)*HG1G2._phi3(ph) - if isinstance(func, u.Quantity): - func = func.value - func = -2.5 * np.log10(func) - if isinstance(h, u.Quantity): - func = func * h.unit - return h + func - - @staticmethod - def fit_deriv(ph, h, g1, g2): - if hasattr(ph, '__iter__'): - ddh = np.ones_like(ph) - else: - ddh = 1. - phi1 = HG1G2._phi1(ph) - phi2 = HG1G2._phi2(ph) - phi3 = HG1G2._phi3(ph) - dom = (g1*phi1+g2*phi2+(1-g1-g2)*phi3) - ddg1 = 1.085736205*(phi3-phi1)/dom - ddg2 = 1.085736205*(phi3-phi2)/dom - return [ddh, ddg1, ddg2] - - def _parameter_units_for_data_units(self, inputs_unit, outputs_unit): - return OrderedDict([('H', outputs_unit['y']), - ('G1', u.dimensionless_unscaled), - ('G2', u.dimensionless_unscaled)]) - - -class HG12(HG12BaseClass): - """HG12 photometric phase model (Muinonen et al. 2010) - - This system is adopted by IAU as the "standard" model for disk-integrated - phase functions of planetary objects. Note that there is a discontinuity - in the derivative for parameter G12, sometimes making the model fitting - difficult. Penttil\"a et al. (2016, Planet. Space Sci. 123, 117-125) - revised the H, G12 system such that the G12 parameter has a continuous - derivative. The revised model is implemented in class `G12_Pen16`. - - Examples - -------- - - >>> # Define the phase function for Themis with - >>> # H = 7.121, G12 = 0.68 - >>> - >>> import astropy.units as u - >>> from sbpy.calib import solar_fluxd - >>> from sbpy.photometry import HG12 - >>> themis = HG12(7.121 * u.mag, 0.68, radius = 100 * u.km, wfb = 'V') - >>> with solar_fluxd.set({'V': -26.77 * u.mag}): - ... print('geometric albedo = {0:.4f}'.format(themis.geomalb)) - ... print('phase integral = {0:.4f}'.format(themis.phaseint)) - geometric albedo = 0.0622 - phase integral = 0.3949 - - """ - - H = Parameter(description='H parameter', default=8) - G12 = Parameter(description='G12 parameter', default=0.3) - - @G12.validator - def G12(self, value): - """Validate parameter G12 to avoid non-monotonic phase function - - If G12 < -0.70 or G12 > 1.30, the phase function could potentially be - non-monotoic, and a warning will be issued. - """ - if np.any(value < -0.70) or np.any(value > 1.30): - warnings.warn( - 'G12 parameter could result in a non-monotonic phase function', - NonmonotonicPhaseFunctionWarning) - - @property - def _G1(self): - return self._G12_to_G1(self.G12.value) - - @property - def _G2(self): - return self._G12_to_G2(self.G12.value) - - @staticmethod - def _G12_to_G1(g12): - """Calculate G1 from G12""" - if g12 < 0.2: - return 0.7527*g12+0.06164 - else: - return 0.9529*g12+0.02162 - - @staticmethod - def _G12_to_G2(g12): - """Calculate G2 from G12""" - if g12 < 0.2: - return -0.9612*g12+0.6270 - else: - return -0.6125*g12+0.5572 - - @staticmethod - def evaluate(ph, h, g12): - g1 = HG12._G12_to_G1(g12) - g2 = HG12._G12_to_G2(g12) - return HG1G2.evaluate(ph, h, g1, g2) - - @staticmethod - def fit_deriv(ph, h, g12): - if hasattr(ph, '__iter__'): - ddh = np.ones_like(ph) - else: - ddh = 1. - g1 = HG12._G12_to_G1(g12) - g2 = HG12._G12_to_G2(g12) - phi1 = HG1G2._phi1(ph) - phi2 = HG1G2._phi2(ph) - phi3 = HG1G2._phi3(ph) - dom = (g1*phi1+g2*phi2+(1-g1-g2)*phi3) - if g12 < 0.2: - p1 = 0.7527 - p2 = -0.9612 - else: - p1 = 0.9529 - p2 = -0.6125 - ddg = 1.085736205*((phi3-phi1)*p1+(phi3-phi2)*p2)/dom - return [ddh, ddg] - - def _parameter_units_for_data_units(self, inputs_unit, outputs_unit): - return OrderedDict([('H', outputs_unit['y']), - ('G12', u.dimensionless_unscaled)]) - - -class HG12_Pen16(HG12): - """Revised H, G12 model by Penttil\"a et al. (2016) - - This system is the revised H, G12 system by Penttil\"a et al. (2016, - Planet. Space Sci. 123, 117-125) that has a continuous derivative with - respect to parameter G12. The original model as adopted by IAU as the - "standard" model for disk-integrated phase functions of planetary objects - is implemented in class `HG12`. - - Examples - -------- - >>> # Define the phase function for Themis with - >>> # H = 7.121, G12 = 0.68 - >>> - >>> import astropy.units as u - >>> from sbpy.calib import solar_fluxd - >>> from sbpy.photometry import HG12_Pen16 - >>> themis = HG12_Pen16(7.121 * u.mag, 0.68, radius = 100 * u.km, - ... wfb = 'V') - >>> with solar_fluxd.set({'V': -26.77 * u.mag}): - ... print('geometric albedo = {0:.4f}'.format(themis.geomalb)) - ... print('phase integral = {0:.4f}'.format(themis.phaseint)) - geometric albedo = 0.0622 - phase integral = 0.3804 - """ - - @cite({'definition': '2016P&SS..123..117P'}) - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - @staticmethod - def _G12_to_G1(g12): - """Calculate G1 from G12""" - return 0.84293649*g12 - - @staticmethod - def _G12_to_G2(g12): - """Calculate G2 from G12""" - return 0.53513350*(1-g12) - - @staticmethod - def evaluate(ph, h, g12): - g1 = HG12_Pen16._G12_to_G1(g12) - g2 = HG12_Pen16._G12_to_G2(g12) - return HG1G2.evaluate(ph, h, g1, g2) - - @staticmethod - def fit_deriv(ph, h, g12): - if hasattr(ph, '__iter__'): - ddh = np.ones_like(ph) - else: - ddh = 1. - g1 = HG12_Pen16._G12_to_G1(g12) - g2 = HG12_Pen16._G12_to_G2(g12) - phi1 = HG1G2._phi1(ph) - phi2 = HG1G2._phi2(ph) - phi3 = HG1G2._phi3(ph) - dom = (g1*phi1+g2*phi2+(1-g1-g2)*phi3) - p1 = 0.84293649 - p2 = -0.53513350 - ddg = 1.085736205*((phi3-phi1)*p1+(phi3-phi2)*p2)/dom - return [ddh, ddg] diff --git a/sbpy/photometry/iau.py b/sbpy/photometry/iau.py new file mode 100644 index 00000000..e7dacc92 --- /dev/null +++ b/sbpy/photometry/iau.py @@ -0,0 +1,437 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst +""" +sbpy photometry module +IAU HG series phase function + +created on July, 2022 + +""" + +import warnings +import numpy as np +from scipy.integrate import quad +from astropy.modeling import Parameter +from .core import DiskIntegratedPhaseFunc, NonmonotonicPhaseFunctionWarning +from ..bib import cite + + +__all__ = ['HG', 'HG12BaseClass', 'HG12', 'HG1G2', 'HG12_Pen16'] + + +class HG(DiskIntegratedPhaseFunc): + """HG photometric phase model (Bowell et al. 1989) + + Examples + -------- + + >>> # Define the phase function for Ceres with H = 3.34, G = 0.12 + >>> import astropy.units as u + >>> from sbpy.calib import solar_fluxd + >>> from sbpy.photometry import HG + >>> ceres = HG(3.34 * u.mag, 0.12, radius = 480 * u.km, wfb = 'V') + >>> with solar_fluxd.set({'V': -26.77 * u.mag}): + ... print('geometric albedo = {0:.4f}'.format(ceres.geomalb)) + ... print('phase integral = {0:.4f}'.format(ceres.phaseint)) + geometric albedo = 0.0878 + phase integral = 0.3644 + + """ + + _unit = 'mag' + H = Parameter(description='H parameter', default=8) + G = Parameter(description='G parameter', default=0.4) + + @cite({'definition': '1989aste.conf..524B'}) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + @G.validator + def G(self, value): + """Validate parameter G to avoid non-monotonic phase function + + If G > 1.194, the phase function could potentially be non-monotoic, + and a warning will be issued. + """ + if np.any(value > 1.194): + warnings.warn( + 'G parameter could result in a non-monotonic phase function', + NonmonotonicPhaseFunctionWarning) + + @staticmethod + def _hgphi(pha, i): + """Core function in IAU HG phase function model + + Parameters + ---------- + pha : float or array_like of float + Phase angle + i : int in [1, 2] + Choose the form of function + + Returns + ------- + numpy array of float + + Note + ---- + See Bowell et al. (1989), Eq. A4. + """ + + if i not in [1, 2]: + raise ValueError('i needs to be 1 or 2, {0} received'.format(i)) + + a, b, c = [3.332, 1.862], [0.631, 1.218], [0.986, 0.238] + pha_half = pha*0.5 + sin_pha = np.sin(pha) + tan_pha_half = np.tan(pha_half) + w = np.exp(-90.56 * tan_pha_half * tan_pha_half) + phiis = 1 - c[i-1]*sin_pha/(0.119+1.341*sin_pha - + 0.754*sin_pha*sin_pha) + phiil = np.exp(-a[i-1] * tan_pha_half**b[i-1]) + return w*phiis + (1-w)*phiil + + @staticmethod + def evaluate(pha, hh, gg): + func = (1-gg)*HG._hgphi(pha, 1)+gg*HG._hgphi(pha, 2) + if isinstance(func, u.Quantity): + func = func.value + func = -2.5 * np.log10(func) + if isinstance(hh, u.Quantity): + func = func * hh.unit + return hh + func + + @staticmethod + def fit_deriv(pha, hh, gg): + if hasattr(pha, '__iter__'): + ddh = np.ones_like(pha) + else: + ddh = 1. + phi1 = HG._hgphi(pha, 1) + phi2 = HG._hgphi(pha, 2) + ddg = 1.085736205*(phi1-phi2)/((1-gg)*phi1+gg*phi2) + return [ddh, ddg] + + def _parameter_units_for_data_units(self, inputs_unit, outputs_unit): + return OrderedDict([('H', outputs_unit['y']), + ('G', u.dimensionless_unscaled)]) + + +class HG12BaseClass(DiskIntegratedPhaseFunc): + """Base class for IAU HG1G2 model and HG12 model""" + + _unit = 'mag' + + @cite({'definition': '2010Icar..209..542M'}) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + @property + def _G1(self): + return None + + @property + def _G2(self): + return None + + @property + def phaseint(self): + """Phase integral, q + Based on Muinonen et al. (2010) Eq. 22 + """ + return 0.009082+0.4061*self._G1+0.8092*self._G2 + + @property + def phasecoeff(self): + """Phase coefficient, k + Based on Muinonen et al. (2010) Eq. 23 + """ + return -(30*self._G1+9*self._G2)/(5*np.pi*float(self._G1+self._G2)) + + @property + def oe_amp(self): + """Opposition effect amplitude, :math:`\zeta-1` + Based on Muinonen et al. (2010) Eq. 24) + """ + tmp = float(self._G1+self._G2) + return (1-tmp)/tmp + + class _spline_positive(_spline): + """ + Define a spline class that clips negative function values + """ + + def __call__(self, x): + y = super().__call__(x) + if hasattr(y, '__iter__'): + y[y < 0] = 0 + else: + if y < 0: + y = 0 + return y + + _phi1v = (np.deg2rad([7.5, 30., 60, 90, 120, 150]), + [7.5e-1, 3.3486016e-1, 1.3410560e-1, + 5.1104756e-2, 2.1465687e-2, 3.6396989e-3], + [-1.9098593, -9.1328612e-2]) + _phi1 = _spline_positive(*_phi1v) + _phi2v = (np.deg2rad([7.5, 30., 60, 90, 120, 150]), + [9.25e-1, 6.2884169e-1, 3.1755495e-1, + 1.2716367e-1, 2.2373903e-2, 1.6505689e-4], + [-5.7295780e-1, -8.6573138e-8]) + _phi2 = _spline_positive(*_phi2v) + _phi3v = (np.deg2rad([0.0, 0.3, 1., 2., 4., 8., 12., 20., 30.]), + [1., 8.3381185e-1, 5.7735424e-1, 4.2144772e-1, 2.3174230e-1, + 1.0348178e-1, 6.1733473e-2, 1.6107006e-2, 0.], + [-1.0630097, 0]) + _phi3 = _spline_positive(*_phi3v) + + +class HG1G2(HG12BaseClass): + """HG1G2 photometric phase model (Muinonen et al. 2010) + + Examples + -------- + + >>> # Define the phase function for Themis with + >>> # H = 7.063, G1 = 0.62, G2 = 0.14 + >>> + >>> import astropy.units as u + >>> from sbpy.calib import solar_fluxd + >>> from sbpy.photometry import HG1G2 + >>> themis = HG1G2(7.063 * u.mag, 0.62, 0.14, radius = 100 * u.km, + ... wfb = 'V') + >>> with solar_fluxd.set({'V': -26.77 * u.mag}): + ... print('geometric albedo = {0:.4f}'.format(themis.geomalb)) + ... print('phase integral = {0:.4f}'.format(themis.phaseint)) + geometric albedo = 0.0656 + phase integral = 0.3742 + """ + + H = Parameter(description='H parameter', default=8) + G1 = Parameter(description='G1 parameter', default=0.2) + G2 = Parameter(description='G2 parameter', default=0.2) + + @G1.validator + def G1(self, value): + """Validate parameter G1 to avoid non-monotonic phase function + + If G1 < 0 or G2 < 0 or G1 + G2 > 1, the phase function could + potentially be non-monotoic, and a warning will be issued. + """ + if np.any(value < 0) or np.any(value + self.G2 > 1): + warnings.warn( + 'G1, G2 parameter combination might result in a non-monotonic' + ' phase function', NonmonotonicPhaseFunctionWarning) + + @G2.validator + def G2(self, value): + """Validate parameter G1 to avoid non-monotonic phase function + + If G1 < 0 or G2 < 0 or G1 + G2 > 1, the phase function could + potentially be non-monotoic, and a warning will be issued. + """ + if np.any(value < 0) or np.any(value + self.G1 > 1): + warnings.warn( + 'G1, G2 parameter combination might result in a non-monotonic' + ' phase function', NonmonotonicPhaseFunctionWarning) + + @property + def _G1(self): + return self.G1.value + + @property + def _G2(self): + return self.G2.value + + @staticmethod + def evaluate(ph, h, g1, g2): + ph = u.Quantity(ph, 'rad').to_value('rad') + func = g1*HG1G2._phi1(ph)+g2*HG1G2._phi2(ph)+(1-g1-g2)*HG1G2._phi3(ph) + if isinstance(func, u.Quantity): + func = func.value + func = -2.5 * np.log10(func) + if isinstance(h, u.Quantity): + func = func * h.unit + return h + func + + @staticmethod + def fit_deriv(ph, h, g1, g2): + if hasattr(ph, '__iter__'): + ddh = np.ones_like(ph) + else: + ddh = 1. + phi1 = HG1G2._phi1(ph) + phi2 = HG1G2._phi2(ph) + phi3 = HG1G2._phi3(ph) + dom = (g1*phi1+g2*phi2+(1-g1-g2)*phi3) + ddg1 = 1.085736205*(phi3-phi1)/dom + ddg2 = 1.085736205*(phi3-phi2)/dom + return [ddh, ddg1, ddg2] + + def _parameter_units_for_data_units(self, inputs_unit, outputs_unit): + return OrderedDict([('H', outputs_unit['y']), + ('G1', u.dimensionless_unscaled), + ('G2', u.dimensionless_unscaled)]) + + +class HG12(HG12BaseClass): + """HG12 photometric phase model (Muinonen et al. 2010) + + This system is adopted by IAU as the "standard" model for disk-integrated + phase functions of planetary objects. Note that there is a discontinuity + in the derivative for parameter G12, sometimes making the model fitting + difficult. Penttil\"a et al. (2016, Planet. Space Sci. 123, 117-125) + revised the H, G12 system such that the G12 parameter has a continuous + derivative. The revised model is implemented in class `G12_Pen16`. + + Examples + -------- + + >>> # Define the phase function for Themis with + >>> # H = 7.121, G12 = 0.68 + >>> + >>> import astropy.units as u + >>> from sbpy.calib import solar_fluxd + >>> from sbpy.photometry import HG12 + >>> themis = HG12(7.121 * u.mag, 0.68, radius = 100 * u.km, wfb = 'V') + >>> with solar_fluxd.set({'V': -26.77 * u.mag}): + ... print('geometric albedo = {0:.4f}'.format(themis.geomalb)) + ... print('phase integral = {0:.4f}'.format(themis.phaseint)) + geometric albedo = 0.0622 + phase integral = 0.3949 + + """ + + H = Parameter(description='H parameter', default=8) + G12 = Parameter(description='G12 parameter', default=0.3) + + @G12.validator + def G12(self, value): + """Validate parameter G12 to avoid non-monotonic phase function + + If G12 < -0.70 or G12 > 1.30, the phase function could potentially be + non-monotoic, and a warning will be issued. + """ + if np.any(value < -0.70) or np.any(value > 1.30): + warnings.warn( + 'G12 parameter could result in a non-monotonic phase function', + NonmonotonicPhaseFunctionWarning) + + @property + def _G1(self): + return self._G12_to_G1(self.G12.value) + + @property + def _G2(self): + return self._G12_to_G2(self.G12.value) + + @staticmethod + def _G12_to_G1(g12): + """Calculate G1 from G12""" + if g12 < 0.2: + return 0.7527*g12+0.06164 + else: + return 0.9529*g12+0.02162 + + @staticmethod + def _G12_to_G2(g12): + """Calculate G2 from G12""" + if g12 < 0.2: + return -0.9612*g12+0.6270 + else: + return -0.6125*g12+0.5572 + + @staticmethod + def evaluate(ph, h, g12): + g1 = HG12._G12_to_G1(g12) + g2 = HG12._G12_to_G2(g12) + return HG1G2.evaluate(ph, h, g1, g2) + + @staticmethod + def fit_deriv(ph, h, g12): + if hasattr(ph, '__iter__'): + ddh = np.ones_like(ph) + else: + ddh = 1. + g1 = HG12._G12_to_G1(g12) + g2 = HG12._G12_to_G2(g12) + phi1 = HG1G2._phi1(ph) + phi2 = HG1G2._phi2(ph) + phi3 = HG1G2._phi3(ph) + dom = (g1*phi1+g2*phi2+(1-g1-g2)*phi3) + if g12 < 0.2: + p1 = 0.7527 + p2 = -0.9612 + else: + p1 = 0.9529 + p2 = -0.6125 + ddg = 1.085736205*((phi3-phi1)*p1+(phi3-phi2)*p2)/dom + return [ddh, ddg] + + def _parameter_units_for_data_units(self, inputs_unit, outputs_unit): + return OrderedDict([('H', outputs_unit['y']), + ('G12', u.dimensionless_unscaled)]) + + +class HG12_Pen16(HG12): + """Revised H, G12 model by Penttil\"a et al. (2016) + + This system is the revised H, G12 system by Penttil\"a et al. (2016, + Planet. Space Sci. 123, 117-125) that has a continuous derivative with + respect to parameter G12. The original model as adopted by IAU as the + "standard" model for disk-integrated phase functions of planetary objects + is implemented in class `HG12`. + + Examples + -------- + >>> # Define the phase function for Themis with + >>> # H = 7.121, G12 = 0.68 + >>> + >>> import astropy.units as u + >>> from sbpy.calib import solar_fluxd + >>> from sbpy.photometry import HG12_Pen16 + >>> themis = HG12_Pen16(7.121 * u.mag, 0.68, radius = 100 * u.km, + ... wfb = 'V') + >>> with solar_fluxd.set({'V': -26.77 * u.mag}): + ... print('geometric albedo = {0:.4f}'.format(themis.geomalb)) + ... print('phase integral = {0:.4f}'.format(themis.phaseint)) + geometric albedo = 0.0622 + phase integral = 0.3804 + """ + + @cite({'definition': '2016P&SS..123..117P'}) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + @staticmethod + def _G12_to_G1(g12): + """Calculate G1 from G12""" + return 0.84293649*g12 + + @staticmethod + def _G12_to_G2(g12): + """Calculate G2 from G12""" + return 0.53513350*(1-g12) + + @staticmethod + def evaluate(ph, h, g12): + g1 = HG12_Pen16._G12_to_G1(g12) + g2 = HG12_Pen16._G12_to_G2(g12) + return HG1G2.evaluate(ph, h, g1, g2) + + @staticmethod + def fit_deriv(ph, h, g12): + if hasattr(ph, '__iter__'): + ddh = np.ones_like(ph) + else: + ddh = 1. + g1 = HG12_Pen16._G12_to_G1(g12) + g2 = HG12_Pen16._G12_to_G2(g12) + phi1 = HG1G2._phi1(ph) + phi2 = HG1G2._phi2(ph) + phi3 = HG1G2._phi3(ph) + dom = (g1*phi1+g2*phi2+(1-g1-g2)*phi3) + p1 = 0.84293649 + p2 = -0.53513350 + ddg = 1.085736205*((phi3-phi1)*p1+(phi3-phi2)*p2)/dom + return [ddh, ddg] From e3b385d37d43d923e21cb083b139c84de9543a2b Mon Sep 17 00:00:00 2001 From: Jian-Yang Li Date: Tue, 5 Jul 2022 12:18:15 -0400 Subject: [PATCH 24/59] update tests and bugfixes --- sbpy/photometry/core.py | 69 ---- sbpy/photometry/iau.py | 71 ++++ sbpy/photometry/tests/test_core.py | 492 +--------------------------- sbpy/photometry/tests/test_iau.py | 509 +++++++++++++++++++++++++++++ 4 files changed, 581 insertions(+), 560 deletions(-) create mode 100644 sbpy/photometry/tests/test_iau.py diff --git a/sbpy/photometry/core.py b/sbpy/photometry/core.py index e34586fb..f631bf00 100644 --- a/sbpy/photometry/core.py +++ b/sbpy/photometry/core.py @@ -22,75 +22,6 @@ from ..exceptions import SbpyWarning -class _spline(object): - - """Cubic spline class - - Spline function is defined by function values at nodes and the first - derivatives at both ends. Outside the range of nodes, the extrapolations - are linear based on the first derivatives at the corresponding ends. - """ - - def __init__(self, x, y, dy): - """ - Spline initialization - - Parameters - ---------- - x, y : array_like float - The (x, y) values at nodes that defines the spline - dy : array_like float with two elements - The first derivatives of the left and right ends of the nodes - """ - from numpy.linalg import solve - from numpy.polynomial.polynomial import Polynomial - self.x = np.asarray(x) - self.y = np.asarray(y) - self.dy = np.asarray(dy) - n = len(self.y) - h = self.x[1:]-self.x[:-1] - r = (self.y[1:]-self.y[:-1])/(self.x[1:]-self.x[:-1]) - B = np.zeros((n-2, n)) - for i in range(n-2): - k = i+1 - B[i, i:i+3] = [h[k], 2*(h[k-1]+h[k]), h[k-1]] - C = np.empty((n-2, 1)) - for i in range(n-2): - k = i+1 - C[i] = 3*(r[k-1]*h[k]+r[k]*h[k-1]) - C[0] = C[0]-self.dy[0]*B[0, 0] - C[-1] = C[-1]-self.dy[1]*B[-1, -1] - B = B[:, 1:n-1] - dys = solve(B, C) - dys = np.array( - [self.dy[0]] + [tmp for tmp in dys.flatten()] + [self.dy[1]]) - A0 = self.y[:-1] - A1 = dys[:-1] - A2 = (3*r-2*dys[:-1]-dys[1:])/h - A3 = (-2*r+dys[:-1]+dys[1:])/h**2 - self.coef = np.array([A0, A1, A2, A3]).T - self.polys = [Polynomial(c) for c in self.coef] - self.polys.insert(0, Polynomial( - [self.y[0]-self.x[0]*self.dy[0], self.dy[0]])) - self.polys.append(Polynomial( - [self.y[-1]-self.x[-1]*self.dy[-1], self.dy[-1]])) - - def __call__(self, x): - x = np.asarray(x) - out = np.zeros_like(x) - idx = x < self.x[0] - if idx.any(): - out[idx] = self.polys[0](x[idx]) - for i in range(len(self.x)-1): - idx = (self.x[i] <= x) & (x < self.x[i+1]) - if idx.any(): - out[idx] = self.polys[i+1](x[idx]-self.x[i]) - idx = (x >= self.x[-1]) - if idx.any(): - out[idx] = self.polys[-1](x[idx]) - return out - - class NonmonotonicPhaseFunctionWarning(SbpyWarning): pass diff --git a/sbpy/photometry/iau.py b/sbpy/photometry/iau.py index e7dacc92..d075a0df 100644 --- a/sbpy/photometry/iau.py +++ b/sbpy/photometry/iau.py @@ -8,9 +8,11 @@ """ import warnings +from collections import OrderedDict import numpy as np from scipy.integrate import quad from astropy.modeling import Parameter +import astropy.units as u from .core import DiskIntegratedPhaseFunc, NonmonotonicPhaseFunctionWarning from ..bib import cite @@ -18,6 +20,75 @@ __all__ = ['HG', 'HG12BaseClass', 'HG12', 'HG1G2', 'HG12_Pen16'] +class _spline(object): + + """Cubic spline class + + Spline function is defined by function values at nodes and the first + derivatives at both ends. Outside the range of nodes, the extrapolations + are linear based on the first derivatives at the corresponding ends. + """ + + def __init__(self, x, y, dy): + """ + Spline initialization + + Parameters + ---------- + x, y : array_like float + The (x, y) values at nodes that defines the spline + dy : array_like float with two elements + The first derivatives of the left and right ends of the nodes + """ + from numpy.linalg import solve + from numpy.polynomial.polynomial import Polynomial + self.x = np.asarray(x) + self.y = np.asarray(y) + self.dy = np.asarray(dy) + n = len(self.y) + h = self.x[1:]-self.x[:-1] + r = (self.y[1:]-self.y[:-1])/(self.x[1:]-self.x[:-1]) + B = np.zeros((n-2, n)) + for i in range(n-2): + k = i+1 + B[i, i:i+3] = [h[k], 2*(h[k-1]+h[k]), h[k-1]] + C = np.empty((n-2, 1)) + for i in range(n-2): + k = i+1 + C[i] = 3*(r[k-1]*h[k]+r[k]*h[k-1]) + C[0] = C[0]-self.dy[0]*B[0, 0] + C[-1] = C[-1]-self.dy[1]*B[-1, -1] + B = B[:, 1:n-1] + dys = solve(B, C) + dys = np.array( + [self.dy[0]] + [tmp for tmp in dys.flatten()] + [self.dy[1]]) + A0 = self.y[:-1] + A1 = dys[:-1] + A2 = (3*r-2*dys[:-1]-dys[1:])/h + A3 = (-2*r+dys[:-1]+dys[1:])/h**2 + self.coef = np.array([A0, A1, A2, A3]).T + self.polys = [Polynomial(c) for c in self.coef] + self.polys.insert(0, Polynomial( + [self.y[0]-self.x[0]*self.dy[0], self.dy[0]])) + self.polys.append(Polynomial( + [self.y[-1]-self.x[-1]*self.dy[-1], self.dy[-1]])) + + def __call__(self, x): + x = np.asarray(x) + out = np.zeros_like(x) + idx = x < self.x[0] + if idx.any(): + out[idx] = self.polys[0](x[idx]) + for i in range(len(self.x)-1): + idx = (self.x[i] <= x) & (x < self.x[i+1]) + if idx.any(): + out[idx] = self.polys[i+1](x[idx]-self.x[i]) + idx = (x >= self.x[-1]) + if idx.any(): + out[idx] = self.polys[-1](x[idx]) + return out + + class HG(DiskIntegratedPhaseFunc): """HG photometric phase model (Bowell et al. 1989) diff --git a/sbpy/photometry/tests/test_core.py b/sbpy/photometry/tests/test_core.py index 0c22d31b..80f602f2 100644 --- a/sbpy/photometry/tests/test_core.py +++ b/sbpy/photometry/tests/test_core.py @@ -5,8 +5,8 @@ from astropy import units as u from astropy.modeling import Parameter from ...calib import solar_fluxd +from ...data import Ephem from ..core import * -from ...data import Ephem, Phys def setup_module(module): @@ -159,493 +159,3 @@ def test_fit(self): def test_fit_deriv(self): assert np.allclose(LinearPhaseFunc.fit_deriv(1, 1, 2), [1, 1]) - - -class TestHG: - def test_init(self): - # initialize with numbers - ceres = HG(3.34 * u.mag, 0.12, radius=480 * u.km, wfb='V') - assert u.isclose(ceres.radius, 480 * u.km) - assert isinstance(ceres.H, Parameter) - assert np.isclose(ceres.H.value, 3.34) - assert isinstance(ceres.G, Parameter) - assert np.isclose(ceres.G.value, 0.12) - assert ceres.wfb == 'V' - - @pytest.mark.remote_data - def test_from_phys(self): - # test initialization from `sbpy.data.DataClass` - phys = Phys.from_sbdb('Ceres') - m = HG.from_phys(phys) - assert np.all(m.meta['targetname'] == phys['targetname']) - assert np.isclose(m.H.value, phys['H'][0].value) - assert np.isclose(m.G.value, phys['G'][0]) - assert np.isclose(m.radius.value, phys['diameter'].value/2) - # test the case when target name is unknown - phys.table.remove_column('targetname') - m = HG.from_phys(phys) - # test initialization failure with `sbpy.data.DataClass` when H or G - # is not present - phys = Phys.from_sbdb('12893') - with pytest.raises(KeyError): - m = HG.from_phys(phys) - - def test_to_phys(self): - m = HG(3.34 * u.mag, 0.12) - p = m.to_phys() - assert isinstance(p, Phys) - assert set(p.field_names) == {'H', 'G'} - assert np.isclose(p['H'].value, 3.34) - assert p['H'].unit == u.mag - assert np.isclose(p['G'], 0.12) - m = HG(3.34 * u.mag, 0.12, radius=480 * u.km, wfb='V', - meta={'targetname': '1 Ceres'}) - p = m.to_phys() - assert isinstance(p, Phys) - assert p['targetname'] == '1 Ceres' - assert np.isclose(p['H'].value, 3.34) - assert p['H'].unit == u.mag - assert np.isclose(p['G'], 0.12) - assert np.isclose(p['diameter'].value, 960) - assert p['diameter'].unit == u.km - assert np.isclose(p['pv'], 0.0877745) - assert np.isclose(p['A'], 0.03198069) - - def test_evaluate(self): - pha_test = np.linspace(0, np.pi, 10) - phi_test = np.array( - [3.34, 4.37957695, 4.97935011, - 5.55797517, 6.20811269, 7.03560828, 8.2295693, 10.19445176, - 14.29255427, np.inf]) - assert np.allclose(HG.evaluate(pha_test, 3.34, 0.12), phi_test) - - def test_fit_deriv(self): - pha_test = np.linspace(0, np.pi*0.999, 10) - deriv_test = np.array( - [[1., 1., 1., 1., 1., 1., 1., 1., 1., 1.], - [0., -1.33897222, -2.00745805, -2.43171634, -2.6155218, - -2.46808816, -1.71295215, 0.06121683, 1.22716693, - 1.23379114]]) - assert np.allclose(np.array(HG.fit_deriv(pha_test, 3.34, 0.12)), - deriv_test) - assert np.allclose(HG.fit_deriv(1, 3.4, 0.2), [1.0, -2.031224359464]) - - def test__check_unit(self): - ceres = HG(3.34 * u.mag, 0.12) - assert ceres._unit == 'mag' - - def test_props(self): - ceres = HG(3.34 * u.mag, 0.12, radius=480 * u.km, wfb='V') - assert np.isclose(ceres.geomalb, 0.0877745) - assert np.isclose(ceres.bondalb, 0.03198069) - assert np.isclose(ceres.phaseint, 0.3643505755292945) - - def test_from_obs(self): - pha = [0., 6.31578947, 12.63157895, 18.94736842, 25.26315789, - 31.57894737, 37.89473684, 44.21052632, 50.52631579, 56.84210526, - 63.15789474, 69.47368421, 75.78947368, 82.10526316, 88.42105263, - 94.73684211, 101.05263158, 107.36842105, 113.68421053, - 120.] * u.deg - data = [3.14451639, 4.06262914, 4.1154297, 4.54870242, 4.42265052, - 4.71990531, 5.1628504, 5.16098737, 5.20971821, 5.3032115, - 5.52976173, 5.64255607, 5.84536878, 6.13724017, 6.33675472, - 6.63099954, 7.2461781, 7.32734464, 8.00147425, - 8.40595306] * u.mag - from astropy.modeling.fitting import LevMarLSQFitter - fitter = LevMarLSQFitter() - # test fit with one column - m = HG.from_obs({'alpha': pha, 'mag': data}, fitter) - assert isinstance(m, HG) - assert isinstance(m.H, Parameter) & np.isclose( - m.H.value, 3.436677) & (m.H.unit == u.mag) - assert isinstance(m.G, Parameter) & np.isclose( - m.G.value, 0.1857588) & (m.G.unit == u.dimensionless_unscaled) - # test fit with one column and `init` parameters - m = HG.from_obs({'alpha': pha, 'mag': data}, fitter, init=[3, 0.1]) - assert isinstance(m, HG) - assert isinstance(m.H, Parameter) & np.isclose( - m.H.value, 3.4366849) & (m.H.unit == u.mag) - assert isinstance(m.G, Parameter) & np.isclose( - m.G.value, 0.18576319) & (m.G.unit == u.dimensionless_unscaled) - # test fit with more than one column - m = HG.from_obs({'alpha': pha, 'mag': data, 'mag1': data, - 'mag2': data}, fitter, fields=['mag', 'mag1', 'mag2']) - assert isinstance(m, HG) - assert isinstance(m.H, Parameter) & np.allclose( - m.H.value, [3.436677]*3) & (m.H.unit == u.mag) - assert isinstance(m.G, Parameter) & np.allclose( - m.G.value, [0.1857588]*3) & (m.G.unit == u.dimensionless_unscaled) - assert 'fields' in m.meta - assert m.meta['fields'] == ['mag', 'mag1', 'mag2'] - # test fit with more than one column with `init` parameters - m = HG.from_obs({'alpha': pha, 'mag': data, 'mag1': data, - 'mag2': data}, fitter, fields=['mag', 'mag1', 'mag2'], - init=[[3., 3., 3.], [0.1, 0.1, 0.1]]) - assert isinstance(m, HG) - assert isinstance(m.H, Parameter) & np.allclose( - m.H.value, [3.4366849]*3) & (m.H.unit == u.mag) - assert isinstance(m.G, Parameter) & np.allclose( - m.G.value, [0.18576319]*3) & (m.G.unit == u.dimensionless_unscaled) - assert 'fields' in m.meta - assert m.meta['fields'] == ['mag', 'mag1', 'mag2'] - - def test_to_mag(self): - ceres = HG(3.34 * u.mag, 0.12, radius=480 * u.km, wfb='V') - eph_dict = {'alpha': np.linspace(0, np.pi*0.9, 10) * u.rad, - 'r': np.repeat(2.7*u.au, 10), - 'delta': np.repeat(1.8*u.au, 10)} - eph_test = Ephem.from_dict(eph_dict) - mag1_test = np.array( - [6.773181346311468, 7.746327508868813, 8.297741273359549, - 8.813984838536113, 9.366879943505342, 10.024055427421063, - 10.886692329621765, 12.143261499943726, 14.18326309145893, - 18.48388800989832]) * u.mag - eph1 = ceres.to_mag(eph_test, append_results=True) - assert set(eph1.field_names) == {'alpha', 'delta', 'mag', 'r'} - assert u.allclose(eph1['mag'], mag1_test) - pha_test = np.linspace(0, np.pi*0.9, 10) * u.rad - mag2_test = np.array( - [3.34, 4.313146162557345, 4.864559927048081, 5.380803492224645, - 5.9336985971938745, 6.590874081109595, 7.453510983310297, - 8.710080153632259, 10.750081745147462, - 15.050706663586855]) * u.mag - eph3 = ceres.to_mag(pha_test, append_results=True) - assert u.allclose(eph3['mag'], mag2_test) - assert set(eph3.field_names) == {'alpha', 'mag'} - mag4 = ceres.to_mag(pha_test) - assert u.allclose(mag4, mag2_test) - - def test_to_ref(self): - ceres = HG(3.34 * u.mag, 0.12, radius=480 * u.km, wfb='V') - eph_dict = {'alpha': np.linspace(0, np.pi*0.9, 10)*u.rad, - 'r': np.repeat(2.7*u.au, 10), - 'delta': np.repeat(1.8*u.au, 10)} - eph_test = Ephem.from_dict(eph_dict) - ref1_test = [2.79394901e-02, 1.14014480e-02, 6.86111195e-03, - 4.26478439e-03, 2.56294353e-03, 1.39916471e-03, - 6.32141181e-04, 1.98694761e-04, 3.03518927e-05, - 5.78010611e-07] / u.sr - eph1 = ceres.to_ref(eph_test, append_results=True) - assert set(eph1.field_names) == {'alpha', 'delta', 'ref', 'r'} - assert u.allclose(eph1['ref'], ref1_test) - pha_test = np.linspace(0, np.pi*0.9, 10) * u.rad - ref2_norm_test = np.array( - [1.00000000e+00, 4.08076452e-01, 2.45570407e-01, 1.52643601e-01, - 9.17319364e-02, 5.00783911e-02, 2.26253657e-02, 7.11161011e-03, - 1.08634383e-03, 2.06879441e-05]) - eph3 = ceres.to_ref(pha_test, append_results=True) - eph4 = ceres.to_ref(pha_test, normalized=0*u.deg, append_results=True) - assert set(eph3.field_names) == {'alpha', 'ref'} - assert set(eph4.field_names) == {'alpha', 'ref'} - assert u.allclose(eph3['ref'], ref1_test) - assert u.allclose(eph4['ref'], ref2_norm_test) - ref5 = ceres.to_ref(pha_test) - assert u.allclose(ref5, ref1_test) - - def test_g_validate(self): - with pytest.warns(NonmonotonicPhaseFunctionWarning): - m = HG(0, 1.2) - - def test_hgphi_exception(self): - with pytest.raises(ValueError): - tmp = HG._hgphi(0, 0) - with pytest.raises(ValueError): - tmp = HG._hgphi(0, 3) - - -class TestHG1G2: - def test_init(self): - # initialization with numbers - themis = HG1G2(7.063 * u.mag, 0.62, 0.14, radius=100 * u.km, wfb='V') - assert themis._unit == 'mag' - assert u.isclose(themis.radius, 100 * u.km) - assert isinstance(themis.H, Parameter) - assert np.isclose(themis.H.value, 7.063) - assert isinstance(themis.G1, Parameter) - assert np.isclose(themis.G1.value, 0.62) - assert isinstance(themis.G2, Parameter) - assert np.isclose(themis.G2.value, 0.14) - assert themis.wfb == 'V' - - @pytest.mark.remote_data - def test_from_phys(self): - # initialization with Phys, will cause exception because G1, G2 are - # not generally unavailable. - phys = Phys.from_sbdb(['Ceres']) - with pytest.raises(KeyError): - m = HG1G2.from_phys(phys) - - def test__G1_G2(self): - themis = HG1G2(7.063 * u.mag, 0.62, 0.14) - assert np.isclose(themis._G1, 0.62) - assert np.isclose(themis._G2, 0.14) - - def test_evaluate(self): - pha_test = np.linspace(0, np.pi, 10) - phi_test = np.array( - [7.063, 8.07436233, 8.68048572, - 9.29834638, 9.96574599, 10.72080704, 11.52317465, 12.15094612, - 18.65369516, 18.65389398]) - assert np.allclose(HG1G2.evaluate( - pha_test, 7.063, 0.62, 0.14), phi_test) - - def test_fit_deriv(self): - pha_test = np.linspace(0, np.pi*0.999, 10) - deriv_test = np.array( - [[1.00000000e+00, 1.00000000e+00, 1.00000000e+00, 1.00000000e+00, - 1.00000000e+00, 1.00000000e+00, 1.00000000e+00, 1.00000000e+00, - 1.00000000e+00, 1.00000000e+00], - [2.43068538e-09, -1.21097348e+00, -1.20062175e+00, - -1.14122248e+00, -1.10961252e+00, -1.16263148e+00, - -1.41529341e+00, -1.70796793e+00, 0.00000000e+00, - 0.00000000e+00], - [-6.92019713e-10, -2.07410290e+00, -2.43821941e+00, - -2.70127334e+00, -2.84126028e+00, -2.60646207e+00, - -1.48753067e+00, -1.91400644e-01, -7.75525861e+00, - -7.75525861e+00]]) - assert np.allclose(np.array(HG1G2.fit_deriv( - pha_test, 7.063, 0.62, 0.14)), deriv_test) - assert np.allclose(HG1G2.fit_deriv(0.2, 3.4, 0.62, 0.14), - [1.0, -1.1563984700303085, -1.6666940099848913]) - - def test_props(self): - themis = HG1G2(7.063 * u.mag, 0.62, 0.14, radius=100 * u.km, wfb='V') - assert np.isclose(themis.geomalb, 0.06556179) - assert np.isclose(themis.bondalb, 0.02453008) - assert np.isclose(themis.phaseint, 0.374152) - - def test_from_obs(self): - pha = [0., 6.31578947, 12.63157895, 18.94736842, 25.26315789, - 31.57894737, 37.89473684, 44.21052632, 50.52631579, 56.84210526, - 63.15789474, 69.47368421, 75.78947368, 82.10526316, 88.42105263, - 94.73684211, 101.05263158, 107.36842105, 113.68421053, - 120.] * u.deg - data = [7.14679706, 7.32220201, 7.85637226, 7.98824651, 8.2029765, - 8.27574759, 8.49437766, 9.05650671, 8.79649221, 9.33071561, - 9.24703668, 9.49069761, 9.57246629, 10.12429626, 10.14465944, - 10.51021594, 10.63215313, 11.15570421, 11.44890748, - 11.43888611] * u.mag - from astropy.modeling.fitting import LevMarLSQFitter - fitter = LevMarLSQFitter() - m = HG1G2.from_obs({'alpha': pha, 'mag': data}, fitter) - assert isinstance(m, HG1G2) - assert isinstance(m.H, Parameter) & np.isclose( - m.H.value, 7.1167) & (m.H.unit == u.mag) - assert isinstance(m.G1, Parameter) & np.isclose( - m.G1.value, 0.63922) & (m.G1.unit == u.dimensionless_unscaled) - assert isinstance(m.G2, Parameter) & np.isclose( - m.G2.value, 0.17262569) & (m.G2.unit == u.dimensionless_unscaled) - - def test_to_mag(self): - themis = HG1G2(7.063 * u.mag, 0.62, 0.14, radius=100 * u.km, wfb='V') - pha_test = np.linspace(0, np.pi, 10)*u.rad - mag_test = [7.063, 8.07436233, 8.68048572, 9.29834638, 9.96574599, - 10.72080704, 11.52317465, 12.15094612, 18.65369516, - 18.65389398] * u.mag - assert u.allclose(themis.to_mag(pha_test), mag_test) - - def test_to_ref(self): - themis = HG1G2(7.063 * u.mag, 0.62, 0.14, radius=100 * u.km, wfb='V') - pha_test = np.linspace(0, np.pi, 10)*u.rad - ref_test = [2.08689669e-02, 8.22159390e-03, 4.70442623e-03, - 2.66294623e-03, 1.44013284e-03, 7.18419542e-04, - 3.43108196e-04, 1.92452033e-04, 4.82195204e-07, - 4.82106912e-07] / u.sr - assert u.allclose(themis.to_ref(pha_test), ref_test) - - def test_g1g2_validator(self): - with pytest.warns(NonmonotonicPhaseFunctionWarning): - m = HG1G2(0, -0.2, 0.5) - m = HG1G2(0, 0.5, -0.2) - m = HG1G2(0, 0.6, 0.6) - - -class TestHG12: - def test_init(self): - themis = HG12(7.121 * u.mag, 0.68, radius=100 * u.km, wfb='V') - assert themis._unit == 'mag' - assert u.isclose(themis.radius, 100 * u.km) - assert isinstance(themis.H, Parameter) - assert np.isclose(themis.H.value, 7.121) - assert isinstance(themis.G12, Parameter) - assert np.isclose(themis.G12.value, 0.68) - assert themis.wfb == 'V' - - def test__G1_G2(self): - themis = HG12(7.121 * u.mag, 0.68) - assert np.isclose(themis._G1, 0.669592) - assert np.isclose(themis._G2, 0.1407) - themis = HG12(7.121 * u.mag, 0.1) - assert np.isclose(themis._G1, 0.13691) - assert np.isclose(themis._G2, 0.53088) - - def test_evaluate(self): - pha_test = np.linspace(0, np.pi, 10) - phi_test = np.array( - [7.121, 8.07252953, 8.67890827, - 9.2993879, 9.96817595, 10.72086969, 11.51208664, 12.12722017, - 18.70628001, 18.70647883]) - assert np.allclose(HG12.evaluate(pha_test, 7.121, 0.68), - phi_test) - - def test_fit_deriv(self): - pha_test = np.linspace(0, np.pi, 10) - phi_test = np.array( - [[1.00000000e+00, 1.00000000e+00, 1.00000000e+00, - 1.00000000e+00, 1.00000000e+00, 1.00000000e+00, - 1.00000000e+00, 1.00000000e+00, 1.00000000e+00, - 1.00000000e+00], - [2.74006218e-09, 1.10544425e-01, 3.31104623e-01, - 5.38630727e-01, 6.48853898e-01, 4.61007626e-01, - -4.18315316e-01, -1.40406486e+00, 4.72646358e+00, - 4.72646358e+00]]) - assert np.allclose(np.array(HG12.fit_deriv( - pha_test, 7.121, 0.68)), phi_test) - assert np.allclose(HG12.fit_deriv(0.2, 7.121, 0.68), - [1.0, -0.07693564214597949]) - assert np.allclose(HG12.fit_deriv(0.2, 7.121, 0.1), - [1.0, 0.6739785181393765]) - - def test_props(self): - themis = HG12(7.121 * u.mag, 0.68, radius=100 * u.km, wfb='V') - assert np.isclose(themis.geomalb, 0.06215139) - assert np.isclose(themis.bondalb, 0.02454096) - assert np.isclose(themis.phaseint, 0.3948577512) - assert np.isclose(themis.phasecoeff, -1.6777182566684201) - assert np.isclose(themis.oe_amp, 0.23412300750840437) - - def test_from_obs(self): - pha = [0., 6.31578947, 12.63157895, 18.94736842, 25.26315789, - 31.57894737, 37.89473684, 44.21052632, 50.52631579, 56.84210526, - 63.15789474, 69.47368421, 75.78947368, 82.10526316, 88.42105263, - 94.73684211, 101.05263158, 107.36842105, 113.68421053, - 120.] * u.deg - data = [6.95036472, 7.71609702, 8.04175457, 7.88226545, 8.28192813, - 8.50954834, 8.36880691, 8.73216696, 8.90742914, 9.05696656, - 9.20869753, 9.52578025, 9.8427691, 9.91588852, 10.3636637, - 10.26459992, 10.79316978, 10.79202241, 11.36950747, - 11.61018708] * u.mag - from astropy.modeling.fitting import LevMarLSQFitter - fitter = LevMarLSQFitter() - m = HG12.from_obs({'alpha': pha, 'mag': data}, fitter) - assert isinstance(m, HG12) - assert isinstance(m.H, Parameter) & np.isclose( - m.H.value, 7.13939) & (m.H.unit == u.mag) - assert isinstance(m.G12, Parameter) & np.isclose( - m.G12.value, 0.44872) & (m.G12.unit == u.dimensionless_unscaled) - - def test_to_mag(self): - pha_test = np.linspace(0, np.pi, 10) * u.rad - mag_test = [7.121, 8.07252953, 8.67890827, 9.2993879, 9.96817595, - 10.72086969, 11.51208664, 12.12722017, 18.70628001, - 18.70647883] * u.mag - themis = HG12(7.121 * u.mag, 0.68) - assert u.allclose(themis.to_mag(pha_test), mag_test) - - def test_to_ref(self): - pha_test = np.linspace(0, np.pi, 10) * u.rad - ref_test = [1.97834009e-02, 8.23548424e-03, 4.71126618e-03, - 2.66039298e-03, 1.43691333e-03, 7.18378086e-04, - 3.46630119e-04, 1.96703860e-04, 4.59397839e-07, - 4.59313722e-07] / u.sr - themis = HG12(7.121 * u.mag, 0.68, radius=100 * u.km, wfb='V') - assert u.allclose(themis.to_ref(pha_test), ref_test) - - def test_g_validator(self): - with pytest.warns(NonmonotonicPhaseFunctionWarning): - m = HG12(0, -0.71) - m = HG12(0, 1.31) - - def test_G1_G2(self): - themis = HG12(7.121 * u.mag, 0.68, radius=100 * u.km, wfb='V') - assert np.isclose(themis._G1, 0.669592) - assert np.isclose(themis._G2, 0.1407) - - -class TestHG12_Pen16: - def test_init(self): - themis = HG12_Pen16(7.121 * u.mag, 0.68, radius=100 * u.km, wfb='V') - assert themis._unit == 'mag' - assert u.isclose(themis.radius, 100*u.km) - assert isinstance(themis.H, Parameter) - assert np.isclose(themis.H.value, 7.121) - assert isinstance(themis.G12, Parameter) - assert np.isclose(themis.G12.value, 0.68) - assert themis.wfb == 'V' - - def test__G1_G2(self): - themis = HG12_Pen16(7.121 * u.mag, 0.68) - assert np.isclose(themis._G1, 0.5731968132) - assert np.isclose(themis._G2, 0.17124272) - - def test_evaluate(self): - pha_test = np.linspace(0, np.pi, 10) - phi_test = np.array( - [7.121, 8.12425116, 8.71866169, 9.32576929, 9.98752147, - 10.7522335, 11.60154855, 12.28573613, 18.49298496, 18.49318378]) - assert np.allclose(HG12_Pen16.evaluate(pha_test, 7.121, 0.68), - phi_test) - - def test_fit_deriv(self): - pha_test = np.linspace(0, np.pi, 10) - phi_test = np.array( - [[1.00000000e+00, 1.00000000e+00, 1.00000000e+00, - 1.00000000e+00, 1.00000000e+00, 1.00000000e+00, - 1.00000000e+00, 1.00000000e+00, 1.00000000e+00, - 1.00000000e+00], - [2.41923634e-09, 8.87922520e-02, 2.87810959e-01, - 4.70632181e-01, 5.65925756e-01, 4.02776061e-01, - -4.11888156e-01, -1.43862153e+00, 3.39292564e+00, - 3.39292564e+00]]) - assert np.allclose(np.array(HG12_Pen16.fit_deriv( - pha_test, 7.121, 0.68)), phi_test) - assert np.allclose(HG12_Pen16.fit_deriv(0.2, 7.121, 0.68), - [1., -0.08302351]) - - def test_props(self): - themis = HG12_Pen16(7.121 * u.mag, 0.68, radius=100 * u.km, wfb='V') - assert np.isclose(themis.geomalb, 0.06215139) - assert np.isclose(themis.bondalb, 0.02364406) - assert np.isclose(themis.phaseint, 0.38042683486452) - - def test_from_obs(self): - pha = [0., 6.31578947, 12.63157895, 18.94736842, 25.26315789, - 31.57894737, 37.89473684, 44.21052632, 50.52631579, 56.84210526, - 63.15789474, 69.47368421, 75.78947368, 82.10526316, 88.42105263, - 94.73684211, 101.05263158, 107.36842105, 113.68421053, - 120.] * u.deg - data = [7.15663893, 7.4389134, 8.00006177, 7.9044872, 8.16865497, - 8.51010016, 8.63386712, 8.65893367, 8.84895152, 9.24495642, - 9.16195702, 9.54770054, 9.60599559, 10.06129054, 10.22544773, - 10.49122575, 10.78544483, 11.12145723, 11.18055954, - 11.40468613] * u.mag - from astropy.modeling.fitting import LevMarLSQFitter - fitter = LevMarLSQFitter() - m = HG12_Pen16.from_obs({'alpha': pha, 'mag': data}, fitter) - assert isinstance(m, HG12_Pen16) - assert isinstance(m.H, Parameter) & np.isclose( - m.H.value, 7.038705) & (m.H.unit == u.mag) - assert isinstance(m.G12, Parameter) & np.isclose( - m.G12.value, 0.681691) & (m.G12.unit == u.dimensionless_unscaled) - - def test_to_mag(self): - pha_test = np.linspace(0, np.pi, 10) * u.rad - mag_test = [7.121, 8.12425116, 8.71866169, 9.32576929, 9.98752147, - 10.7522335, 11.60154855, 12.28573613, 18.49298496, - 18.49318378] * u.mag - themis = HG12_Pen16(7.121 * u.mag, 0.68) - assert u.allclose(themis.to_mag(pha_test), mag_test) - - def test_to_ref(self): - pha_test = np.linspace(0, np.pi, 10) * u.rad - ref_test = [1.97834009e-02, 7.85236516e-03, 4.54188647e-03, - 2.59652934e-03, 1.41153731e-03, 6.97923066e-04, - 3.19213708e-04, 1.69983395e-04, 5.59122499e-07, - 5.59020121e-07] / u.sr - themis = HG12_Pen16(7.121 * u.mag, 0.68, radius=100 * u.km, wfb='V') - assert u.allclose(themis.to_ref(pha_test), ref_test) - - def test_g_validator(self): - with pytest.warns(NonmonotonicPhaseFunctionWarning): - m = HG12(0, -0.71) - m = HG12(0, 1.31) diff --git a/sbpy/photometry/tests/test_iau.py b/sbpy/photometry/tests/test_iau.py new file mode 100644 index 00000000..3e68822e --- /dev/null +++ b/sbpy/photometry/tests/test_iau.py @@ -0,0 +1,509 @@ +# Licensed under a 3-clause BSD style license - see LICENSE.rst + +import numpy as np +import pytest +from astropy import units as u +from astropy.modeling import Parameter +from ...calib import solar_fluxd +from ...data import Ephem, Phys +from ..core import NonmonotonicPhaseFunctionWarning +from ..iau import * + + +def setup_module(module): + module.solar_fluxd_default = solar_fluxd.get() + solar_fluxd.set({'V': -26.77 * u.mag}) + + +def teardown_module(module): + solar_fluxd.set(module.solar_fluxd_default) + + +class TestHG: + def test_init(self): + # initialize with numbers + ceres = HG(3.34 * u.mag, 0.12, radius=480 * u.km, wfb='V') + assert u.isclose(ceres.radius, 480 * u.km) + assert isinstance(ceres.H, Parameter) + assert np.isclose(ceres.H.value, 3.34) + assert isinstance(ceres.G, Parameter) + assert np.isclose(ceres.G.value, 0.12) + assert ceres.wfb == 'V' + + @pytest.mark.remote_data + def test_from_phys(self): + # test initialization from `sbpy.data.DataClass` + phys = Phys.from_sbdb('Ceres') + m = HG.from_phys(phys) + assert np.all(m.meta['targetname'] == phys['targetname']) + assert np.isclose(m.H.value, phys['H'][0].value) + assert np.isclose(m.G.value, phys['G'][0]) + assert np.isclose(m.radius.value, phys['diameter'].value/2) + # test the case when target name is unknown + phys.table.remove_column('targetname') + m = HG.from_phys(phys) + # test initialization failure with `sbpy.data.DataClass` when H or G + # is not present + phys = Phys.from_sbdb('12893') + with pytest.raises(KeyError): + m = HG.from_phys(phys) + + def test_to_phys(self): + m = HG(3.34 * u.mag, 0.12) + p = m.to_phys() + assert isinstance(p, Phys) + assert set(p.field_names) == {'H', 'G'} + assert np.isclose(p['H'].value, 3.34) + assert p['H'].unit == u.mag + assert np.isclose(p['G'], 0.12) + m = HG(3.34 * u.mag, 0.12, radius=480 * u.km, wfb='V', + meta={'targetname': '1 Ceres'}) + p = m.to_phys() + assert isinstance(p, Phys) + assert p['targetname'] == '1 Ceres' + assert np.isclose(p['H'].value, 3.34) + assert p['H'].unit == u.mag + assert np.isclose(p['G'], 0.12) + assert np.isclose(p['diameter'].value, 960) + assert p['diameter'].unit == u.km + assert np.isclose(p['pv'], 0.0877745) + assert np.isclose(p['A'], 0.03198069) + + def test_evaluate(self): + pha_test = np.linspace(0, np.pi, 10) + phi_test = np.array( + [3.34, 4.37957695, 4.97935011, + 5.55797517, 6.20811269, 7.03560828, 8.2295693, 10.19445176, + 14.29255427, np.inf]) + assert np.allclose(HG.evaluate(pha_test, 3.34, 0.12), phi_test) + + def test_fit_deriv(self): + pha_test = np.linspace(0, np.pi*0.999, 10) + deriv_test = np.array( + [[1., 1., 1., 1., 1., 1., 1., 1., 1., 1.], + [0., -1.33897222, -2.00745805, -2.43171634, -2.6155218, + -2.46808816, -1.71295215, 0.06121683, 1.22716693, + 1.23379114]]) + assert np.allclose(np.array(HG.fit_deriv(pha_test, 3.34, 0.12)), + deriv_test) + assert np.allclose(HG.fit_deriv(1, 3.4, 0.2), [1.0, -2.031224359464]) + + def test__check_unit(self): + ceres = HG(3.34 * u.mag, 0.12) + assert ceres._unit == 'mag' + + def test_props(self): + ceres = HG(3.34 * u.mag, 0.12, radius=480 * u.km, wfb='V') + assert np.isclose(ceres.geomalb, 0.0877745) + assert np.isclose(ceres.bondalb, 0.03198069) + assert np.isclose(ceres.phaseint, 0.3643505755292945) + + def test_from_obs(self): + pha = [0., 6.31578947, 12.63157895, 18.94736842, 25.26315789, + 31.57894737, 37.89473684, 44.21052632, 50.52631579, 56.84210526, + 63.15789474, 69.47368421, 75.78947368, 82.10526316, 88.42105263, + 94.73684211, 101.05263158, 107.36842105, 113.68421053, + 120.] * u.deg + data = [3.14451639, 4.06262914, 4.1154297, 4.54870242, 4.42265052, + 4.71990531, 5.1628504, 5.16098737, 5.20971821, 5.3032115, + 5.52976173, 5.64255607, 5.84536878, 6.13724017, 6.33675472, + 6.63099954, 7.2461781, 7.32734464, 8.00147425, + 8.40595306] * u.mag + from astropy.modeling.fitting import LevMarLSQFitter + fitter = LevMarLSQFitter() + # test fit with one column + m = HG.from_obs({'alpha': pha, 'mag': data}, fitter) + assert isinstance(m, HG) + assert isinstance(m.H, Parameter) & np.isclose( + m.H.value, 3.436677) & (m.H.unit == u.mag) + assert isinstance(m.G, Parameter) & np.isclose( + m.G.value, 0.1857588) & (m.G.unit == u.dimensionless_unscaled) + # test fit with one column and `init` parameters + m = HG.from_obs({'alpha': pha, 'mag': data}, fitter, init=[3, 0.1]) + assert isinstance(m, HG) + assert isinstance(m.H, Parameter) & np.isclose( + m.H.value, 3.4366849) & (m.H.unit == u.mag) + assert isinstance(m.G, Parameter) & np.isclose( + m.G.value, 0.18576319) & (m.G.unit == u.dimensionless_unscaled) + # test fit with more than one column + m = HG.from_obs({'alpha': pha, 'mag': data, 'mag1': data, + 'mag2': data}, fitter, fields=['mag', 'mag1', 'mag2']) + assert isinstance(m, HG) + assert isinstance(m.H, Parameter) & np.allclose( + m.H.value, [3.436677]*3) & (m.H.unit == u.mag) + assert isinstance(m.G, Parameter) & np.allclose( + m.G.value, [0.1857588]*3) & (m.G.unit == u.dimensionless_unscaled) + assert 'fields' in m.meta + assert m.meta['fields'] == ['mag', 'mag1', 'mag2'] + # test fit with more than one column with `init` parameters + m = HG.from_obs({'alpha': pha, 'mag': data, 'mag1': data, + 'mag2': data}, fitter, fields=['mag', 'mag1', 'mag2'], + init=[[3., 3., 3.], [0.1, 0.1, 0.1]]) + assert isinstance(m, HG) + assert isinstance(m.H, Parameter) & np.allclose( + m.H.value, [3.4366849]*3) & (m.H.unit == u.mag) + assert isinstance(m.G, Parameter) & np.allclose( + m.G.value, [0.18576319]*3) & (m.G.unit == u.dimensionless_unscaled) + assert 'fields' in m.meta + assert m.meta['fields'] == ['mag', 'mag1', 'mag2'] + + def test_to_mag(self): + ceres = HG(3.34 * u.mag, 0.12, radius=480 * u.km, wfb='V') + eph_dict = {'alpha': np.linspace(0, np.pi*0.9, 10) * u.rad, + 'r': np.repeat(2.7*u.au, 10), + 'delta': np.repeat(1.8*u.au, 10)} + eph_test = Ephem.from_dict(eph_dict) + mag1_test = np.array( + [6.773181346311468, 7.746327508868813, 8.297741273359549, + 8.813984838536113, 9.366879943505342, 10.024055427421063, + 10.886692329621765, 12.143261499943726, 14.18326309145893, + 18.48388800989832]) * u.mag + eph1 = ceres.to_mag(eph_test, append_results=True) + assert set(eph1.field_names) == {'alpha', 'delta', 'mag', 'r'} + assert u.allclose(eph1['mag'], mag1_test) + pha_test = np.linspace(0, np.pi*0.9, 10) * u.rad + mag2_test = np.array( + [3.34, 4.313146162557345, 4.864559927048081, 5.380803492224645, + 5.9336985971938745, 6.590874081109595, 7.453510983310297, + 8.710080153632259, 10.750081745147462, + 15.050706663586855]) * u.mag + eph3 = ceres.to_mag(pha_test, append_results=True) + assert u.allclose(eph3['mag'], mag2_test) + assert set(eph3.field_names) == {'alpha', 'mag'} + mag4 = ceres.to_mag(pha_test) + assert u.allclose(mag4, mag2_test) + + def test_to_ref(self): + ceres = HG(3.34 * u.mag, 0.12, radius=480 * u.km, wfb='V') + eph_dict = {'alpha': np.linspace(0, np.pi*0.9, 10)*u.rad, + 'r': np.repeat(2.7*u.au, 10), + 'delta': np.repeat(1.8*u.au, 10)} + eph_test = Ephem.from_dict(eph_dict) + ref1_test = [2.79394901e-02, 1.14014480e-02, 6.86111195e-03, + 4.26478439e-03, 2.56294353e-03, 1.39916471e-03, + 6.32141181e-04, 1.98694761e-04, 3.03518927e-05, + 5.78010611e-07] / u.sr + eph1 = ceres.to_ref(eph_test, append_results=True) + assert set(eph1.field_names) == {'alpha', 'delta', 'ref', 'r'} + assert u.allclose(eph1['ref'], ref1_test) + pha_test = np.linspace(0, np.pi*0.9, 10) * u.rad + ref2_norm_test = np.array( + [1.00000000e+00, 4.08076452e-01, 2.45570407e-01, 1.52643601e-01, + 9.17319364e-02, 5.00783911e-02, 2.26253657e-02, 7.11161011e-03, + 1.08634383e-03, 2.06879441e-05]) + eph3 = ceres.to_ref(pha_test, append_results=True) + eph4 = ceres.to_ref(pha_test, normalized=0*u.deg, append_results=True) + assert set(eph3.field_names) == {'alpha', 'ref'} + assert set(eph4.field_names) == {'alpha', 'ref'} + assert u.allclose(eph3['ref'], ref1_test) + assert u.allclose(eph4['ref'], ref2_norm_test) + ref5 = ceres.to_ref(pha_test) + assert u.allclose(ref5, ref1_test) + + def test_g_validate(self): + with pytest.warns(NonmonotonicPhaseFunctionWarning): + m = HG(0, 1.2) + + def test_hgphi_exception(self): + with pytest.raises(ValueError): + tmp = HG._hgphi(0, 0) + with pytest.raises(ValueError): + tmp = HG._hgphi(0, 3) + + +class TestHG1G2: + def test_init(self): + # initialization with numbers + themis = HG1G2(7.063 * u.mag, 0.62, 0.14, radius=100 * u.km, wfb='V') + assert themis._unit == 'mag' + assert u.isclose(themis.radius, 100 * u.km) + assert isinstance(themis.H, Parameter) + assert np.isclose(themis.H.value, 7.063) + assert isinstance(themis.G1, Parameter) + assert np.isclose(themis.G1.value, 0.62) + assert isinstance(themis.G2, Parameter) + assert np.isclose(themis.G2.value, 0.14) + assert themis.wfb == 'V' + + @pytest.mark.remote_data + def test_from_phys(self): + # initialization with Phys, will cause exception because G1, G2 are + # not generally unavailable. + phys = Phys.from_sbdb(['Ceres']) + with pytest.raises(KeyError): + m = HG1G2.from_phys(phys) + + def test__G1_G2(self): + themis = HG1G2(7.063 * u.mag, 0.62, 0.14) + assert np.isclose(themis._G1, 0.62) + assert np.isclose(themis._G2, 0.14) + + def test_evaluate(self): + pha_test = np.linspace(0, np.pi, 10) + phi_test = np.array( + [7.063, 8.07436233, 8.68048572, + 9.29834638, 9.96574599, 10.72080704, 11.52317465, 12.15094612, + 18.65369516, 18.65389398]) + assert np.allclose(HG1G2.evaluate( + pha_test, 7.063, 0.62, 0.14), phi_test) + + def test_fit_deriv(self): + pha_test = np.linspace(0, np.pi*0.999, 10) + deriv_test = np.array( + [[1.00000000e+00, 1.00000000e+00, 1.00000000e+00, 1.00000000e+00, + 1.00000000e+00, 1.00000000e+00, 1.00000000e+00, 1.00000000e+00, + 1.00000000e+00, 1.00000000e+00], + [2.43068538e-09, -1.21097348e+00, -1.20062175e+00, + -1.14122248e+00, -1.10961252e+00, -1.16263148e+00, + -1.41529341e+00, -1.70796793e+00, 0.00000000e+00, + 0.00000000e+00], + [-6.92019713e-10, -2.07410290e+00, -2.43821941e+00, + -2.70127334e+00, -2.84126028e+00, -2.60646207e+00, + -1.48753067e+00, -1.91400644e-01, -7.75525861e+00, + -7.75525861e+00]]) + assert np.allclose(np.array(HG1G2.fit_deriv( + pha_test, 7.063, 0.62, 0.14)), deriv_test) + assert np.allclose(HG1G2.fit_deriv(0.2, 3.4, 0.62, 0.14), + [1.0, -1.1563984700303085, -1.6666940099848913]) + + def test_props(self): + themis = HG1G2(7.063 * u.mag, 0.62, 0.14, radius=100 * u.km, wfb='V') + assert np.isclose(themis.geomalb, 0.06556179) + assert np.isclose(themis.bondalb, 0.02453008) + assert np.isclose(themis.phaseint, 0.374152) + + def test_from_obs(self): + pha = [0., 6.31578947, 12.63157895, 18.94736842, 25.26315789, + 31.57894737, 37.89473684, 44.21052632, 50.52631579, 56.84210526, + 63.15789474, 69.47368421, 75.78947368, 82.10526316, 88.42105263, + 94.73684211, 101.05263158, 107.36842105, 113.68421053, + 120.] * u.deg + data = [7.14679706, 7.32220201, 7.85637226, 7.98824651, 8.2029765, + 8.27574759, 8.49437766, 9.05650671, 8.79649221, 9.33071561, + 9.24703668, 9.49069761, 9.57246629, 10.12429626, 10.14465944, + 10.51021594, 10.63215313, 11.15570421, 11.44890748, + 11.43888611] * u.mag + from astropy.modeling.fitting import LevMarLSQFitter + fitter = LevMarLSQFitter() + m = HG1G2.from_obs({'alpha': pha, 'mag': data}, fitter) + assert isinstance(m, HG1G2) + assert isinstance(m.H, Parameter) & np.isclose( + m.H.value, 7.1167) & (m.H.unit == u.mag) + assert isinstance(m.G1, Parameter) & np.isclose( + m.G1.value, 0.63922) & (m.G1.unit == u.dimensionless_unscaled) + assert isinstance(m.G2, Parameter) & np.isclose( + m.G2.value, 0.17262569) & (m.G2.unit == u.dimensionless_unscaled) + + def test_to_mag(self): + themis = HG1G2(7.063 * u.mag, 0.62, 0.14, radius=100 * u.km, wfb='V') + pha_test = np.linspace(0, np.pi, 10)*u.rad + mag_test = [7.063, 8.07436233, 8.68048572, 9.29834638, 9.96574599, + 10.72080704, 11.52317465, 12.15094612, 18.65369516, + 18.65389398] * u.mag + assert u.allclose(themis.to_mag(pha_test), mag_test) + + def test_to_ref(self): + themis = HG1G2(7.063 * u.mag, 0.62, 0.14, radius=100 * u.km, wfb='V') + pha_test = np.linspace(0, np.pi, 10)*u.rad + ref_test = [2.08689669e-02, 8.22159390e-03, 4.70442623e-03, + 2.66294623e-03, 1.44013284e-03, 7.18419542e-04, + 3.43108196e-04, 1.92452033e-04, 4.82195204e-07, + 4.82106912e-07] / u.sr + assert u.allclose(themis.to_ref(pha_test), ref_test) + + def test_g1g2_validator(self): + with pytest.warns(NonmonotonicPhaseFunctionWarning): + m = HG1G2(0, -0.2, 0.5) + m = HG1G2(0, 0.5, -0.2) + m = HG1G2(0, 0.6, 0.6) + + +class TestHG12: + def test_init(self): + themis = HG12(7.121 * u.mag, 0.68, radius=100 * u.km, wfb='V') + assert themis._unit == 'mag' + assert u.isclose(themis.radius, 100 * u.km) + assert isinstance(themis.H, Parameter) + assert np.isclose(themis.H.value, 7.121) + assert isinstance(themis.G12, Parameter) + assert np.isclose(themis.G12.value, 0.68) + assert themis.wfb == 'V' + + def test__G1_G2(self): + themis = HG12(7.121 * u.mag, 0.68) + assert np.isclose(themis._G1, 0.669592) + assert np.isclose(themis._G2, 0.1407) + themis = HG12(7.121 * u.mag, 0.1) + assert np.isclose(themis._G1, 0.13691) + assert np.isclose(themis._G2, 0.53088) + + def test_evaluate(self): + pha_test = np.linspace(0, np.pi, 10) + phi_test = np.array( + [7.121, 8.07252953, 8.67890827, + 9.2993879, 9.96817595, 10.72086969, 11.51208664, 12.12722017, + 18.70628001, 18.70647883]) + assert np.allclose(HG12.evaluate(pha_test, 7.121, 0.68), + phi_test) + + def test_fit_deriv(self): + pha_test = np.linspace(0, np.pi, 10) + phi_test = np.array( + [[1.00000000e+00, 1.00000000e+00, 1.00000000e+00, + 1.00000000e+00, 1.00000000e+00, 1.00000000e+00, + 1.00000000e+00, 1.00000000e+00, 1.00000000e+00, + 1.00000000e+00], + [2.74006218e-09, 1.10544425e-01, 3.31104623e-01, + 5.38630727e-01, 6.48853898e-01, 4.61007626e-01, + -4.18315316e-01, -1.40406486e+00, 4.72646358e+00, + 4.72646358e+00]]) + assert np.allclose(np.array(HG12.fit_deriv( + pha_test, 7.121, 0.68)), phi_test) + assert np.allclose(HG12.fit_deriv(0.2, 7.121, 0.68), + [1.0, -0.07693564214597949]) + assert np.allclose(HG12.fit_deriv(0.2, 7.121, 0.1), + [1.0, 0.6739785181393765]) + + def test_props(self): + themis = HG12(7.121 * u.mag, 0.68, radius=100 * u.km, wfb='V') + assert np.isclose(themis.geomalb, 0.06215139) + assert np.isclose(themis.bondalb, 0.02454096) + assert np.isclose(themis.phaseint, 0.3948577512) + assert np.isclose(themis.phasecoeff, -1.6777182566684201) + assert np.isclose(themis.oe_amp, 0.23412300750840437) + + def test_from_obs(self): + pha = [0., 6.31578947, 12.63157895, 18.94736842, 25.26315789, + 31.57894737, 37.89473684, 44.21052632, 50.52631579, 56.84210526, + 63.15789474, 69.47368421, 75.78947368, 82.10526316, 88.42105263, + 94.73684211, 101.05263158, 107.36842105, 113.68421053, + 120.] * u.deg + data = [6.95036472, 7.71609702, 8.04175457, 7.88226545, 8.28192813, + 8.50954834, 8.36880691, 8.73216696, 8.90742914, 9.05696656, + 9.20869753, 9.52578025, 9.8427691, 9.91588852, 10.3636637, + 10.26459992, 10.79316978, 10.79202241, 11.36950747, + 11.61018708] * u.mag + from astropy.modeling.fitting import LevMarLSQFitter + fitter = LevMarLSQFitter() + m = HG12.from_obs({'alpha': pha, 'mag': data}, fitter) + assert isinstance(m, HG12) + assert isinstance(m.H, Parameter) & np.isclose( + m.H.value, 7.13939) & (m.H.unit == u.mag) + assert isinstance(m.G12, Parameter) & np.isclose( + m.G12.value, 0.44872) & (m.G12.unit == u.dimensionless_unscaled) + + def test_to_mag(self): + pha_test = np.linspace(0, np.pi, 10) * u.rad + mag_test = [7.121, 8.07252953, 8.67890827, 9.2993879, 9.96817595, + 10.72086969, 11.51208664, 12.12722017, 18.70628001, + 18.70647883] * u.mag + themis = HG12(7.121 * u.mag, 0.68) + assert u.allclose(themis.to_mag(pha_test), mag_test) + + def test_to_ref(self): + pha_test = np.linspace(0, np.pi, 10) * u.rad + ref_test = [1.97834009e-02, 8.23548424e-03, 4.71126618e-03, + 2.66039298e-03, 1.43691333e-03, 7.18378086e-04, + 3.46630119e-04, 1.96703860e-04, 4.59397839e-07, + 4.59313722e-07] / u.sr + themis = HG12(7.121 * u.mag, 0.68, radius=100 * u.km, wfb='V') + assert u.allclose(themis.to_ref(pha_test), ref_test) + + def test_g_validator(self): + with pytest.warns(NonmonotonicPhaseFunctionWarning): + m = HG12(0, -0.71) + m = HG12(0, 1.31) + + def test_G1_G2(self): + themis = HG12(7.121 * u.mag, 0.68, radius=100 * u.km, wfb='V') + assert np.isclose(themis._G1, 0.669592) + assert np.isclose(themis._G2, 0.1407) + + +class TestHG12_Pen16: + def test_init(self): + themis = HG12_Pen16(7.121 * u.mag, 0.68, radius=100 * u.km, wfb='V') + assert themis._unit == 'mag' + assert u.isclose(themis.radius, 100*u.km) + assert isinstance(themis.H, Parameter) + assert np.isclose(themis.H.value, 7.121) + assert isinstance(themis.G12, Parameter) + assert np.isclose(themis.G12.value, 0.68) + assert themis.wfb == 'V' + + def test__G1_G2(self): + themis = HG12_Pen16(7.121 * u.mag, 0.68) + assert np.isclose(themis._G1, 0.5731968132) + assert np.isclose(themis._G2, 0.17124272) + + def test_evaluate(self): + pha_test = np.linspace(0, np.pi, 10) + phi_test = np.array( + [7.121, 8.12425116, 8.71866169, 9.32576929, 9.98752147, + 10.7522335, 11.60154855, 12.28573613, 18.49298496, 18.49318378]) + assert np.allclose(HG12_Pen16.evaluate(pha_test, 7.121, 0.68), + phi_test) + + def test_fit_deriv(self): + pha_test = np.linspace(0, np.pi, 10) + phi_test = np.array( + [[1.00000000e+00, 1.00000000e+00, 1.00000000e+00, + 1.00000000e+00, 1.00000000e+00, 1.00000000e+00, + 1.00000000e+00, 1.00000000e+00, 1.00000000e+00, + 1.00000000e+00], + [2.41923634e-09, 8.87922520e-02, 2.87810959e-01, + 4.70632181e-01, 5.65925756e-01, 4.02776061e-01, + -4.11888156e-01, -1.43862153e+00, 3.39292564e+00, + 3.39292564e+00]]) + assert np.allclose(np.array(HG12_Pen16.fit_deriv( + pha_test, 7.121, 0.68)), phi_test) + assert np.allclose(HG12_Pen16.fit_deriv(0.2, 7.121, 0.68), + [1., -0.08302351]) + + def test_props(self): + themis = HG12_Pen16(7.121 * u.mag, 0.68, radius=100 * u.km, wfb='V') + assert np.isclose(themis.geomalb, 0.06215139) + assert np.isclose(themis.bondalb, 0.02364406) + assert np.isclose(themis.phaseint, 0.38042683486452) + + def test_from_obs(self): + pha = [0., 6.31578947, 12.63157895, 18.94736842, 25.26315789, + 31.57894737, 37.89473684, 44.21052632, 50.52631579, 56.84210526, + 63.15789474, 69.47368421, 75.78947368, 82.10526316, 88.42105263, + 94.73684211, 101.05263158, 107.36842105, 113.68421053, + 120.] * u.deg + data = [7.15663893, 7.4389134, 8.00006177, 7.9044872, 8.16865497, + 8.51010016, 8.63386712, 8.65893367, 8.84895152, 9.24495642, + 9.16195702, 9.54770054, 9.60599559, 10.06129054, 10.22544773, + 10.49122575, 10.78544483, 11.12145723, 11.18055954, + 11.40468613] * u.mag + from astropy.modeling.fitting import LevMarLSQFitter + fitter = LevMarLSQFitter() + m = HG12_Pen16.from_obs({'alpha': pha, 'mag': data}, fitter) + assert isinstance(m, HG12_Pen16) + assert isinstance(m.H, Parameter) & np.isclose( + m.H.value, 7.038705) & (m.H.unit == u.mag) + assert isinstance(m.G12, Parameter) & np.isclose( + m.G12.value, 0.681691) & (m.G12.unit == u.dimensionless_unscaled) + + def test_to_mag(self): + pha_test = np.linspace(0, np.pi, 10) * u.rad + mag_test = [7.121, 8.12425116, 8.71866169, 9.32576929, 9.98752147, + 10.7522335, 11.60154855, 12.28573613, 18.49298496, + 18.49318378] * u.mag + themis = HG12_Pen16(7.121 * u.mag, 0.68) + assert u.allclose(themis.to_mag(pha_test), mag_test) + + def test_to_ref(self): + pha_test = np.linspace(0, np.pi, 10) * u.rad + ref_test = [1.97834009e-02, 7.85236516e-03, 4.54188647e-03, + 2.59652934e-03, 1.41153731e-03, 6.97923066e-04, + 3.19213708e-04, 1.69983395e-04, 5.59122499e-07, + 5.59020121e-07] / u.sr + themis = HG12_Pen16(7.121 * u.mag, 0.68, radius=100 * u.km, wfb='V') + assert u.allclose(themis.to_ref(pha_test), ref_test) + + def test_g_validator(self): + with pytest.warns(NonmonotonicPhaseFunctionWarning): + m = HG12(0, -0.71) + m = HG12(0, 1.31) From 54151cdda869f06e2a53a039eaf7741ba3007c16 Mon Sep 17 00:00:00 2001 From: Jian-Yang Li Date: Tue, 5 Jul 2022 12:27:39 -0400 Subject: [PATCH 25/59] pep8 fix --- sbpy/photometry/core.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sbpy/photometry/core.py b/sbpy/photometry/core.py index f631bf00..5e46a4eb 100644 --- a/sbpy/photometry/core.py +++ b/sbpy/photometry/core.py @@ -732,4 +732,3 @@ def fit_deriv(a, H, S): def _parameter_units_for_data_units(self, inputs_unit, outputs_unit): return OrderedDict([('H', outputs_unit['y']), ('S', outputs_unit['y']/inputs_unit['x'])]) - From a5d7b29a4f9fe3ea0cc1a0006fdf0bb966500724 Mon Sep 17 00:00:00 2001 From: Jian-Yang Li Date: Tue, 5 Jul 2022 15:18:05 -0400 Subject: [PATCH 26/59] use `DataClass.apply` --- sbpy/photometry/core.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sbpy/photometry/core.py b/sbpy/photometry/core.py index 5e46a4eb..ae04fa4a 100644 --- a/sbpy/photometry/core.py +++ b/sbpy/photometry/core.py @@ -13,7 +13,6 @@ import numpy as np from scipy.integrate import quad from astropy.modeling import (Fittable1DModel, Parameter) -from astropy.table import Column import astropy.units as u from astropy import log from ..data import (Phys, Obs, Ephem, dataclass_input, @@ -553,7 +552,7 @@ def to_mag(self, eph, unit=None, append_results=False, **kwargs): while name in eph.field_names: name = 'mag'+str(i) i += 1 - eph.table.add_column(Column(out, name=name)) + eph.apply(out, name=name) return eph else: return out @@ -646,7 +645,7 @@ def to_ref(self, eph, normalized=None, append_results=False, **kwargs): while name in eph.field_names: name = 'ref'+str(i) i += 1 - eph.table.add_column(Column(out, name=name)) + eph.apply(out, name=name) return eph else: return out From 2771578bbf8f77f6d0d6dd4c866d3f3b795ce41c Mon Sep 17 00:00:00 2001 From: Jian-Yang Li Date: Wed, 6 Jul 2022 14:27:35 -0400 Subject: [PATCH 27/59] remove exporting `HG12BaseClass` --- sbpy/photometry/iau.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sbpy/photometry/iau.py b/sbpy/photometry/iau.py index d075a0df..71a1f9dd 100644 --- a/sbpy/photometry/iau.py +++ b/sbpy/photometry/iau.py @@ -17,7 +17,7 @@ from ..bib import cite -__all__ = ['HG', 'HG12BaseClass', 'HG12', 'HG1G2', 'HG12_Pen16'] +__all__ = ['HG', 'HG12', 'HG1G2', 'HG12_Pen16'] class _spline(object): From d3353bb6a775305853ec0e602ca3200c3bb2069c Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Thu, 16 Jun 2022 15:10:16 -0400 Subject: [PATCH 28/59] Test comet magnitudes in Phys.from_sbdb --- sbpy/data/phys.py | 2 ++ sbpy/data/tests/test_phys_remote.py | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/sbpy/data/phys.py b/sbpy/data/phys.py index 71ef3195..71baf5c4 100644 --- a/sbpy/data/phys.py +++ b/sbpy/data/phys.py @@ -112,6 +112,8 @@ def from_sbdb(cls, targetids, references=False, notes=False): elif key == 'H': # fix for astroquery <0.4.2 columnunits[key].add(u.mag) + elif key in ['M1', 'M2', 'M1_sig', 'M2_sig']: + columnunits[key].add(u.mag) alldata.append(data) diff --git a/sbpy/data/tests/test_phys_remote.py b/sbpy/data/tests/test_phys_remote.py index 28ecb4ef..4d9bf497 100644 --- a/sbpy/data/tests/test_phys_remote.py +++ b/sbpy/data/tests/test_phys_remote.py @@ -1,7 +1,7 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst import pytest - +import astropy.units as u from sbpy.data import Phys @@ -16,3 +16,17 @@ def test_from_sbdb(): # query several objects data = Phys.from_sbdb([n+1 for n in range(5)]) assert len(data.table) == 5 + + +@pytest.mark.remote_data +def test_from_sbdb_comet(): + """Regression test for issue #349. + + As of June 2022, astroquery does not assign units to M1, M2 and their + uncertainties. + + """ + # need a comet with all both M1 and M2, and their uncertainties: + data = Phys.from_sbdb('147P') + for k in ('M1', 'M2', 'M1_sig', 'M2_sig'): + assert isinstance(data[k], u.Quantity) and data[k].unit == u.mag \ No newline at end of file From 13fdc73979e7edad4ef5bd6b11a41fce5ecaa718 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Thu, 7 Jul 2022 08:39:57 -0400 Subject: [PATCH 29/59] Remove old branch for old astroquery --- sbpy/data/phys.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/sbpy/data/phys.py b/sbpy/data/phys.py index 71baf5c4..d4bf8027 100644 --- a/sbpy/data/phys.py +++ b/sbpy/data/phys.py @@ -109,9 +109,6 @@ def from_sbdb(cls, targetids, references=False, notes=False): elif isinstance(val, u.CompositeUnit): for unit in val.bases: columnunits[key].add(unit) - elif key == 'H': - # fix for astroquery <0.4.2 - columnunits[key].add(u.mag) elif key in ['M1', 'M2', 'M1_sig', 'M2_sig']: columnunits[key].add(u.mag) From 14e1e9bb0ed7b27e26f18ac8686e0b0761b87eb8 Mon Sep 17 00:00:00 2001 From: Jian-Yang Li Date: Wed, 6 Jul 2022 21:42:40 -0400 Subject: [PATCH 30/59] add `__contains__` to `DataClass` --- sbpy/data/core.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/sbpy/data/core.py b/sbpy/data/core.py index 793b7cff..8902ec9c 100644 --- a/sbpy/data/core.py +++ b/sbpy/data/core.py @@ -647,6 +647,15 @@ def __setitem__(self, *args): """Refer cls.__setitem__ to self._table""" self.table.__setitem__(*args) + def __contains__(self, value): + """Use cls._translate_columns to realize the `in` operator""" + try: + _ = self._translate_columns(value) + except KeyError: + return False + else: + return True + def _translate_columns(self, target_colnames): """Translate target_colnames to the corresponding column names present in this object's table. Returns a list of actual column From 28502050344a4aa8d6191377f7c43bc5ab293e54 Mon Sep 17 00:00:00 2001 From: Jian-Yang Li Date: Wed, 6 Jul 2022 21:48:22 -0400 Subject: [PATCH 31/59] add tests for `in` operator to `DataClass` --- sbpy/data/tests/test_dataclass.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/sbpy/data/tests/test_dataclass.py b/sbpy/data/tests/test_dataclass.py index ced90b78..cb659aeb 100644 --- a/sbpy/data/tests/test_dataclass.py +++ b/sbpy/data/tests/test_dataclass.py @@ -410,8 +410,8 @@ def test_alternative_name_uniqueness(): Conf.fieldname_idx = storage[1] -def test_translate_columns(monkeypatch): - """test function that translates column names""" +def test_translate_columns_and_contains(monkeypatch): + """test function that translates column names and the `in` operator""" new_fieldnames_info = [ { @@ -439,6 +439,11 @@ def test_translate_columns(monkeypatch): with pytest.raises(KeyError): tab._translate_columns(['x']) + assert 'aa' in tab + assert 'bb' in tab + assert 'zz' in tab + assert 'y' not in tab + def test_indexing(): """make sure that indexing functionality is not compromised through From bb21268e76b1bd636fdda775a0e32ef2c5595fb4 Mon Sep 17 00:00:00 2001 From: Jian-Yang Li Date: Wed, 6 Jul 2022 22:00:07 -0400 Subject: [PATCH 32/59] update documentation --- docs/sbpy/data/dataclass.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/sbpy/data/dataclass.rst b/docs/sbpy/data/dataclass.rst index 16e4b0c6..78b8000c 100644 --- a/docs/sbpy/data/dataclass.rst +++ b/docs/sbpy/data/dataclass.rst @@ -313,6 +313,15 @@ object, you can use `~sbpy.data.DataClass.field_names`: >>> obs.field_names ['ra', 'dec', 't'] +You can also use the `in` operator to check if a field is contained in +a `~sbpy.data.DataClass` object. Alternative field names can also be +used for the `in` test: + + >>> 'ra' in obs + True + >>> 'RA' in obs + True + Each of these columns can be accessed easily, for instance: >>> obs['ra'] From 446b136b0cd32e7f3617fa7a0308cf6a1ced8f38 Mon Sep 17 00:00:00 2001 From: Jian-Yang Li Date: Fri, 8 Jul 2022 11:25:32 -0400 Subject: [PATCH 33/59] add changelog entry --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 7844e20c..8efbf6c9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -11,6 +11,8 @@ sbpy.data body's orbits with respect to planets. [#325] - Added ``Orbit.D_criterion`` to evaluate the D-criterion between two sets of orbital elements. [#325] +- Added ``DataClass.__contains__`` to enable `in` operator for ``DataClass`` + objects. [#357] 0.3.0 (2022-04-28) From ce58d96d2bda72f460316c1d0a9a7d8ba64422e2 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Fri, 8 Jul 2022 10:35:22 -0400 Subject: [PATCH 34/59] Revert "Remove old branch for old astroquery" This reverts commit 13fdc73979e7edad4ef5bd6b11a41fce5ecaa718. --- sbpy/data/phys.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sbpy/data/phys.py b/sbpy/data/phys.py index 50a66663..8b044da0 100644 --- a/sbpy/data/phys.py +++ b/sbpy/data/phys.py @@ -109,6 +109,9 @@ def from_sbdb(cls, targetids, references=False, notes=False): elif isinstance(val, u.CompositeUnit): for unit in val.bases: columnunits[key].add(unit) + elif key == 'H': + # fix for astroquery <0.4.2 + columnunits[key].add(u.mag) elif key in ['M1', 'M2', 'M1_sig', 'M2_sig']: columnunits[key].add(u.mag) elif key in ['M1', 'M2', 'M1_sig', 'M2_sig']: From 11e667193fd433c5cb42688664fb91e3f495beb0 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Fri, 8 Jul 2022 10:39:25 -0400 Subject: [PATCH 35/59] Merge H unit testing into M1, M2 testing # ci-skip --- sbpy/data/phys.py | 8 ++------ sbpy/data/tests/test_phys_remote.py | 7 +++++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/sbpy/data/phys.py b/sbpy/data/phys.py index 8b044da0..61c11f25 100644 --- a/sbpy/data/phys.py +++ b/sbpy/data/phys.py @@ -109,12 +109,8 @@ def from_sbdb(cls, targetids, references=False, notes=False): elif isinstance(val, u.CompositeUnit): for unit in val.bases: columnunits[key].add(unit) - elif key == 'H': - # fix for astroquery <0.4.2 - columnunits[key].add(u.mag) - elif key in ['M1', 'M2', 'M1_sig', 'M2_sig']: - columnunits[key].add(u.mag) - elif key in ['M1', 'M2', 'M1_sig', 'M2_sig']: + elif key in ['H', 'M1', 'M2', 'M1_sig', 'M2_sig']: + # fix for lack of units from SBDB via astroquery columnunits[key].add(u.mag) alldata.append(data) diff --git a/sbpy/data/tests/test_phys_remote.py b/sbpy/data/tests/test_phys_remote.py index b4ba6d3c..1d99a2f6 100644 --- a/sbpy/data/tests/test_phys_remote.py +++ b/sbpy/data/tests/test_phys_remote.py @@ -23,10 +23,13 @@ def test_from_sbdb_comet(): """Regression test for issue #349. As of June 2022, astroquery does not assign units to M1, M2 and their - uncertainties. + uncertainties, nor H. """ - # need a comet with all both M1 and M2, and their uncertainties: + # need a comet with both M1 and M2, and their uncertainties: data = Phys.from_sbdb('147P') for k in ('M1', 'M2', 'M1_sig', 'M2_sig'): assert isinstance(data[k], u.Quantity) and data[k].unit == u.mag + + data = Phys.from_sbdb('1') + assert isinstance(data['H'], u.Quantity) and data['H'].unit == u.mag From 64cb04ecfa4e7dd8a534dc3ef1ae8e8cd8f6524d Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Fri, 8 Jul 2022 11:06:22 -0400 Subject: [PATCH 36/59] Update change log. Add PR number --- CHANGES.rst | 15 +++++++++++++++ sbpy/data/tests/test_phys_remote.py | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 8efbf6c9..a34dcf97 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,12 +9,27 @@ sbpy.data - Added ``Orbit.tisserand`` to calculate the Tisserand parameter of small body's orbits with respect to planets. [#325] + - Added ``Orbit.D_criterion`` to evaluate the D-criterion between two sets of orbital elements. [#325] - Added ``DataClass.__contains__`` to enable `in` operator for ``DataClass`` objects. [#357] +Bug Fixes +--------- + +sbpy.data +^^^^^^^^^ + +- Cometary magnitudes obtained via ``Phys.from_sbdb`` (i.e., M1 and M2) now have + appropriate units. [#349] + +- Asteroids with A/ designations (e.g., A/2019 G2) are correctly identified by + ``Names`` as asteroids. Improved handling of interstellar object (I/) + designations: they do not parse as cometary or asteroidal. [#334, #340] + + 0.3.0 (2022-04-28) ================== diff --git a/sbpy/data/tests/test_phys_remote.py b/sbpy/data/tests/test_phys_remote.py index 1d99a2f6..4bc320ff 100644 --- a/sbpy/data/tests/test_phys_remote.py +++ b/sbpy/data/tests/test_phys_remote.py @@ -20,7 +20,7 @@ def test_from_sbdb(): @pytest.mark.remote_data def test_from_sbdb_comet(): - """Regression test for issue #349. + """Regression test for issues #349 and #358. As of June 2022, astroquery does not assign units to M1, M2 and their uncertainties, nor H. From 5f336c59197b4d0e426cd5fc3a9b25e7f3e86e34 Mon Sep 17 00:00:00 2001 From: Jian-Yang Li Date: Tue, 12 Jul 2022 15:14:18 -0400 Subject: [PATCH 37/59] bugfix `DataClass._translate_columns` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Raise a `KeyError` when a defined column name is not contained in the table. Previously a `KeyError` will not be raised as long as a column name is defined, no matter whether it’s contained in the table. This fix will impact both `_translate_columns` and `__contains__`. --- sbpy/data/core.py | 6 ++++++ sbpy/data/tests/test_dataclass.py | 12 +++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/sbpy/data/core.py b/sbpy/data/core.py index 8902ec9c..877c976a 100644 --- a/sbpy/data/core.py +++ b/sbpy/data/core.py @@ -674,11 +674,17 @@ def _translate_columns(self, target_colnames): continue # colname is an alternative column name elif colname in sum(Conf.fieldnames, []): + found = False for alt in Conf.fieldnames[Conf.fieldname_idx[colname]]: # translation available for colname if alt in self.field_names: translated_colnames[idx] = alt + found = True break + # not found in the table + if not found: + raise KeyError('field {:s} not available.'.format( + colname)) # colname is unknown, raise a KeyError else: raise KeyError('field {:s} not available.'.format( diff --git a/sbpy/data/tests/test_dataclass.py b/sbpy/data/tests/test_dataclass.py index cb659aeb..9c52de2b 100644 --- a/sbpy/data/tests/test_dataclass.py +++ b/sbpy/data/tests/test_dataclass.py @@ -417,9 +417,13 @@ def test_translate_columns_and_contains(monkeypatch): { 'fieldnames': ['zz', 'aa'], 'dimension': dimensions.length + }, + { + 'fieldnames': ['yy', 'dd'], + 'dimension': dimensions.time } ] - new_fieldnames = [['zz', 'aa']] + new_fieldnames = [['zz', 'aa'], ['yy', 'dd']] new_fieldname_idx = {} for idx, field in enumerate(new_fieldnames): for alt in field: @@ -437,12 +441,14 @@ def test_translate_columns_and_contains(monkeypatch): assert tab._translate_columns(['zz', 'bb', 'cc']) == ['aa', 'bb', 'cc'] with pytest.raises(KeyError): - tab._translate_columns(['x']) + tab._translate_columns(['x']) # undefined column name + tab._translate_columns(['dd']) # defined column name but not in table assert 'aa' in tab assert 'bb' in tab assert 'zz' in tab - assert 'y' not in tab + assert 'x' not in tab # undefined column name + assert 'dd' not in tab # defined column name but no in table def test_indexing(): From 78e3c050cae7bead78bcee5804afd6ea009bbb4c Mon Sep 17 00:00:00 2001 From: Jian-Yang Li Date: Tue, 12 Jul 2022 22:06:39 -0400 Subject: [PATCH 38/59] add keyword `epoch` to `Orbit.tisserand` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added in order to specify the epoch of planet orbit in case it’s pulled from JPL Horizons. Also updated related tests for stability and remove warning. --- sbpy/data/orbit.py | 11 ++++++++--- sbpy/data/tests/test_orbit_remote.py | 7 ++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/sbpy/data/orbit.py b/sbpy/data/orbit.py index 35b3cd92..5d152d02 100644 --- a/sbpy/data/orbit.py +++ b/sbpy/data/orbit.py @@ -711,6 +711,7 @@ def oo_propagate(self, epochs, dynmodel='N', ephfile='de430'): in_orbits=in_orbits._to_oo(), in_epoch=ooepoch, in_dynmodel=dynmodel) + print(oo_orbits, err) if err != 0: OpenOrbError('pyoorb failed with error code {:d}'.format(err)) @@ -726,7 +727,7 @@ def oo_propagate(self, epochs, dynmodel='N', ephfile='de430'): return orbits @cite({'method': '1997Icar..127...13L'}) - def tisserand(self, planet='599'): + def tisserand(self, planet='599', epoch=None): """Tisserand parameter with respect to a planet @@ -738,7 +739,11 @@ def tisserand(self, planet='599'): automaticallyl pulled from JPL Horizons. If `self` and/or `planet` contains more than one object, then `numpy` broadcasting rules apply. Default is Jupiter. - + epoch : `~astropy.time.Time`, optional + The epoch of planet orbit if pulled from JPL Horizons. This + parameter will be passed to `~sbpy.data.Orbit.from_horizons`. + If the planet orbit is passed directly, then this parameter + has no effect. Returns ------- @@ -767,7 +772,7 @@ def tisserand(self, planet='599'): if isinstance(planet, str) \ or (hasattr(planet, '__iter__') and np.all([isinstance(x, str) for x in planet])): - planet = Orbit.from_horizons(planet, id_type=None) + planet = Orbit.from_horizons(planet, id_type=None, epochs=epoch) a_p = planet['a'] t = a_p / self['a'] + 2 * np.cos(self['i']) * \ diff --git a/sbpy/data/tests/test_orbit_remote.py b/sbpy/data/tests/test_orbit_remote.py index 74e69528..d41869de 100644 --- a/sbpy/data/tests/test_orbit_remote.py +++ b/sbpy/data/tests/test_orbit_remote.py @@ -181,7 +181,7 @@ def test_oo_propagate(self): future_orbit = Orbit.from_horizons('Ceres', epochs=Time(epoch, format='jd', - scale='utc')) + scale='utc').tdb) oo_orbit = orbit.oo_propagate(epoch) @@ -207,6 +207,7 @@ def test_tisserand(self): chariklo = Orbit.from_horizons('chariklo', id_type='name', closest_apparition=True, epochs=epoch) - assert u.allclose(chariklo.tisserand(['599', '699', '799', '899']), - [3.47746154, 2.92700084, 2.85779878, 3.22653384], + assert u.allclose(chariklo.tisserand(['599', '699', '799', '899'], + epoch=epoch), + [3.47740968, 2.92990768, 2.85749431, 3.22074493], atol=1e-5) From 5721e342990a8b0769d907d11339f49c665bae8b Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Tue, 12 Jul 2022 13:26:13 -0400 Subject: [PATCH 39/59] Update website and installation instructions --- README.rst | 2 +- docs/install.rst | 42 ++- website/public/css/style.css | 568 +++++++++++++++++++++-------------- website/public/index.html | 54 ++-- website/public/team.html | 140 +++++---- 5 files changed, 471 insertions(+), 335 deletions(-) diff --git a/README.rst b/README.rst index ad8e06fc..5f000a2f 100644 --- a/README.rst +++ b/README.rst @@ -62,7 +62,7 @@ sbpy is still under development, with v1.0 scheduled for delivery in 2024. For a Acknowledgements ---------------- -`sbpy` is supported by NASA PDART Grant No. 80NSSC18K0987. +`sbpy` support provided by NASA PDART Grant Nos. 80NSSC18K0987 and 80NSSC22K0143. If you use `sbpy` in your work, please acknowledge it by citing diff --git a/docs/install.rst b/docs/install.rst index b9bfe0d5..a418e433 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -54,25 +54,49 @@ The latest development version of `sbpy` can be easily installed using: $ pip install git+https://github.com/NASA-Planetary-Science/sbpy.git -Using GitHub -^^^^^^^^^^^^ +Using conda +^^^^^^^^^^^ + +The latest stable version of `sbpy` can be installed with `Anaconda +`__ via the `conda-forge `__ +channel: + +.. code-block:: bash + + $ conda install sbpy --channel=conda-forge + +If you do not have the conda-forge channel available, add it and re-run the +installation command: + +.. code-block:: bash + + $ conda config --add channels conda-forge + $ conda install sbpy --channel=conda-forge -This way of installing `sbpy` is recommended if you plan to contribute -to the module. The current development version of `sbpy` can be -obtained from `GitHub `__ using + +Using Git+Pip +^^^^^^^^^^^^^ + +This way of installing `sbpy` is recommended if you plan to contribute to the +module. The current development version of `sbpy` can be obtained from `GitHub +`__ using: .. code-block:: bash $ git clone https://github.com/NASA-Planetary-Science/sbpy.git -This will create a new directory (``sbpy/``). In this directory, run +This will create a new directory (``sbpy/``). In this directory, run: .. code-block:: bash - $ python setup.py install --user + $ pip install . + +As above, to install optional dependencies, instead use ``pip install .[all]``. -in order to use `sbpy` in your default Python environment. If you plan to work on the code and always want to use the latest version of your code, you can install it with +If you plan to work on the code and always want to use the latest version of +your code, we recommend installing in "editable" mode with the optional +dependences and the testing dependencies: .. code-block:: bash - $ python setup.py develop --user + $ pip install -e .[all,test] diff --git a/website/public/css/style.css b/website/public/css/style.css index 1ac9e13a..a7bd05ee 100644 --- a/website/public/css/style.css +++ b/website/public/css/style.css @@ -3,19 +3,85 @@ License: none (public domain) */ -html, body, div, span, applet, object, iframe, -h1, h2, h3, h4, h5, h6, p, blockquote, pre, -a, abbr, acronym, address, big, cite, code, -del, dfn, em, img, ins, kbd, q, s, samp, -small, strike, strong, tt, var, -b, u, i, center, -dl, dt, dd, ol, ul, li, -fieldset, form, label, legend, -table, caption, tbody, tfoot, thead, tr, th, td, -article, aside, canvas, details, embed, -figure, figcaption, footer, header, hgroup, -menu, nav, output, ruby, section, summary, -time, mark, audio, video { +html, +body, +div, +span, +applet, +object, +iframe, +h1, +h2, +h3, +h4, +h5, +h6, +p, +blockquote, +pre, +a, +abbr, +acronym, +address, +big, +cite, +code, +del, +dfn, +em, +img, +ins, +kbd, +q, +s, +samp, +small, +strike, +strong, +tt, +var, +b, +u, +i, +center, +dl, +dt, +dd, +ol, +ul, +li, +fieldset, +form, +label, +legend, +table, +caption, +tbody, +tfoot, +thead, +tr, +th, +td, +article, +aside, +canvas, +details, +embed, +figure, +figcaption, +footer, +header, +hgroup, +menu, +nav, +output, +ruby, +section, +summary, +time, +mark, +audio, +video { margin: 0; padding: 0; border: 0; @@ -25,67 +91,92 @@ time, mark, audio, video { } /* HTML5 display-role reset for older browsers */ -article, aside, details, figcaption, figure, -footer, header, hgroup, menu, nav, section { +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +menu, +nav, +section { display: block; } + .footer { color: black; background-color: #f5f5f5; position: relative; } + .footer-text { - width: 100%; + width: 100%; text-align: center; padding: 12px 12px; } + .footer-text a:hover { color: #FF851B; } + .footer-stamp { position: absolute; top: 20px; right: 10px; } + @media (max-width: 710px) { - .footer-text { - width: 40%; - padding: 12px 6px; - } + .footer-text { + width: 40%; + padding: 12px 6px; + } } + @media (max-width: 593px) { - .footer-text { - width: 90%; - padding: 12px 12px; - } - .footer-stamp { - position: static; - margin: 13px; - } + .footer-text { + width: 90%; + padding: 12px 12px; + } + + .footer-stamp { + position: static; + margin: 13px; + } } + body { line-height: 1; } -ol, ul { + +ol, +ul { list-style: none; } -blockquote, q { + +blockquote, +q { quotes: none; } -blockquote:before, blockquote:after, -q:before, q:after { + +blockquote:before, +blockquote:after, +q:before, +q:after { content: ''; content: none; } + table { border-collapse: collapse; border-spacing: 0; } -body{ +body { line-height: 1.25em; font-size: 14px; - color:#333; + color: #333; font-family: 'Open Sans', sans-serif; } @@ -94,21 +185,24 @@ code { } #index { - background-color:#F5F5F5; + /* background-color: #F5F5F5; */ } -#wrapper{ - margin:0 auto; - max-width:960px; - padding:0 10px; - min-width: 320px; /* min-width of whole site */ +#wrapper { + margin: 0 auto; + max-width: 960px; + padding: 0 10px; + min-width: 320px; + /* min-width of whole site */ } -a{ +a { color: #FF5000; text-decoration: none; } -a, svg{ + +a, +svg { -webkit-transition: all 0.3s ease; -moz-transition: all 0.3s ease; -o-transition: all 0.3s ease; @@ -116,125 +210,119 @@ a, svg{ transition: all 0.3s ease; } -a:hover{ - color:#333; +a:hover { + color: #333; } -h1{ +h1 { font-size: 24px; margin-top: 10px; margin-bottom: 20px; } -h2{ +h2 { font-size: 20px; margin-bottom: 10px; } -h3{ +h3 { font-size: 16px; margin: 20px 0px 10px 0px; } -p{ +p { margin-bottom: 10px; margin-top: 10px; line-height: 1.5em; } -strong{ +strong { font-weight: bold; } -section{ +section { border-bottom: 1px #efefef solid; padding: 30px 0 30px; clear: both; } -section:first-of-type{ - margin-top:42px; +section:first-of-type { + margin-top: 42px; } -section:last-of-type{ +section:last-of-type { border-bottom: 0px; } -section *:last-child{ +section *:last-child { margin-bottom: 0px; } -ul li{ +ul li { list-style-type: square; list-style-position: outside; - list-style-color: #FF5000; + list-style-color: #FF5000; line-height: 1.5em; - margin-bottom:0.5em; + margin-bottom: 0.5em; margin-left: 30px; } -ul li:last-child{ +ul li:last-child { margin-bottom: 0px; } -cite{ +cite { font-family: monospace; } -code{ - /* background-color: #f5f5f5; */ - /* border: 1px solid #ddd; */ - /* padding: 5px; */ - margin-left: 20px; - color: #999; +pre { + background-color: #f5f5f5; + border: 1px solid #ccc; + padding: 10px; + color: black; font-family: Courier New, monospace; - /* webkit-border-radius: 3px; */ - /* -moz-border-radius: 3px; */ - /* border-radius: 3px; */ + border-radius: 3px; margin-bottom: 5px; } -.button{ +.button { padding: 8px; display: inline-block; - background-image: -webkit-gradient( - linear, - left top, - left bottom, - color-stop(0, #FC9468), - color-stop(0.89, #FF5100) - ); + background-image: -webkit-gradient(linear, + left top, + left bottom, + color-stop(0, #FC9468), + color-stop(0.89, #FF5100)); background-image: -o-linear-gradient(bottom, #FC9468 0%, #FF5100 89%); background-image: -moz-linear-gradient(bottom, #FC9468 0%, #FF5100 89%); background-image: -webkit-linear-gradient(bottom, #FC9468 0%, #FF5100 89%); background-image: -ms-linear-gradient(bottom, #FC9468 0%, #FF5100 89%); background-image: linear-gradient(to bottom, #FC9468 0%, #FF5100 89%); - border-radius: 3px; + border-radius: 3px; } -a.button, .button a{ +a.button, +.button a { color: white; } -p .button{ - padding-top:0px; - padding-bottom:0px; - padding-right:4px; - padding-left:4px; +p .button { + padding-top: 0px; + padding-bottom: 0px; + padding-right: 4px; + padding-left: 4px; } -.button:hover{ +.button:hover { cursor: pointer; - background-image: -webkit-gradient( - linear, - left top, - left bottom, - color-stop(0, #FFA47D), - color-stop(1, #FF7D45) - ); + background-image: -webkit-gradient(linear, + left top, + left bottom, + color-stop(0, #FFA47D), + color-stop(1, #FF7D45)); background-image: -o-linear-gradient(bottom, #FFA47D 0%, #FF7D45 100%); background-image: -moz-linear-gradient(bottom, #FFA47D 0%, #FF7D45 100%); background-image: -webkit-linear-gradient(bottom, #FFA47D 0%, #FF7D45 100%); @@ -243,25 +331,27 @@ p .button{ } hr { - border: none; - height: 1px; - /* Set the hr color */ - color: #333; /* old IE */ - background-color: grey; /* Modern Browsers */ + border: none; + height: 1px; + /* Set the hr color */ + color: #333; + /* old IE */ + background-color: grey; + /* Modern Browsers */ } -.right{ +.right { float: right; } /* nav */ -nav{ +nav { line-height: 42px; - position:absolute; - left:0px; - top:0px; - width:100%; - background-image: -webkit-gradient(linear,left top,left bottom,color-stop(0, #222222),color-stop(0.89, #333333)); + position: absolute; + left: 0px; + top: 0px; + width: 100%; + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #222222), color-stop(0.89, #333333)); background-image: -o-linear-gradient(bottom, #222222 0%, #333333 89%); background-image: -moz-linear-gradient(bottom, #222222 0%, #333333 89%); background-image: -webkit-linear-gradient(bottom, #222222 0%, #333333 89%); @@ -269,46 +359,48 @@ nav{ background-image: linear-gradient(to bottom, #222222 0%, #333333 89%); } -nav > *{ - display:inline-block; - vertical-align:top; +nav>* { + display: inline-block; + vertical-align: top; } -nav img{ +nav img { float: left; margin-left: 20px; margin-top: 5px; } -nav ul{ - max-width:960px; - margin:0 auto; +nav ul { + max-width: 960px; + margin: 0 auto; } -nav ul li{ - display:inline; +nav ul li { + display: inline; list-style: none; margin: 10px; } -nav li a, nav li a:link{ - color:white; +nav li a, +nav li a:link { + color: white; } -nav li a:hover{ - color:#FF5000; + +nav li a:hover { + color: #FF5000; } -nav li a.active{ - color:#FF5000; +nav li a.active { + color: #FF5000; } -nav .pull-right{ +nav .pull-right { float: right; - margin-right:10px; + margin-right: 10px; } -.search input{ +.search input { margin-top: 4px; width: 180px; height: 18px; @@ -323,11 +415,11 @@ nav .pull-right{ } #mobile-header { - display: none; + display: none; } -@media only screen and (max-width: 370px){ - .search{ +@media only screen and (max-width: 370px) { + .search { display: none; } } @@ -335,91 +427,94 @@ nav .pull-right{ /*hero*/ -section#hero{ +section#hero { text-align: center; - position:absolute; - left:0px; - top:0; - width:100%; - border-bottom: 10px; + position: absolute; + left: 0px; + top: 0; + width: 100%; + border-bottom: 10px; + z-index: -1; } -section#hero p{ +section#hero p { max-width: 600px; margin: 10px auto 20px auto; } -section#hero img{ +section#hero img { display: inline; - margin:20px 0px; - max-width:90%; + margin: 20px 0px; + max-width: 90%; } -section#hero div#documentation{ - color:white; +section#hero div#documentation { + color: white; display: inline-block; position: relative; } -section#hero div#documentation span{ +section#hero div#documentation span { font-size: 10px; - padding:10px 20px 6px 20px; + padding: 10px 20px 6px 20px; } -section#hero div#documentation a{ +section#hero div#documentation a { color: white; - text-align:center; + text-align: center; } -section#hero div#documentation ul{ +section#hero div#documentation ul { border-top: white 2px solid; text-align: left; position: absolute; - left: 179px; top: 33px; + left: 179px; + top: 33px; display: none; background-color: #FF5000; width: 107px; font-size: 12px; - z-index:1; + z-index: 1; } -section#hero div#documentation ul li{ +section#hero div#documentation ul li { border-top: white 1px solid; padding: 5px 5px; list-style: none; margin: 0px; } -section#hero div#documentation ul li:hover{ + +section#hero div#documentation ul li:hover { background-color: #ffa077; cursor: pointer; } section#hero div#documentation ul li a { - display: block; - text-decoration: none; + display: block; + text-decoration: none; } /*Front Page*/ -section.whatsnew{ - margin-top:350px; - border:1px #E8E8E8 solid; - border-right-style:none; - border-left-style:none; - border-padding=1px; +section.whatsnew { + margin-top: 350px; + border: 1px #E8E8E8 solid; + border-right-style: none; + border-left-style: none; + border-spacing: 1px; padding-top: 15px; padding-bottom: 15px; } -section.whatsnew .version{ - color:grey; +section.whatsnew .version { + color: grey; font-size: 12px; - margin-top:6px; - margin-bottom:0px; + margin-top: 6px; + margin-bottom: 0px; } -.whatsnew{ +.whatsnew { font-size: 24px; text-align: center; } @@ -434,56 +529,58 @@ section.whatsnew .version{ margin-top: 30px; }*/ -section.install .button.download{ +section.install .button.download { padding: 5px 5px; } -section.install ul li{ +section.install ul li { list-style: none; margin-left: 0px; } -section.install ul li a{ - background: #fff; +section.install ul li a { + background: #fff; border: 1px #FFE1D6 solid; padding: 10px; float: left; margin: 0px 5px -1px 5px; - height:20px; + height: 20px; line-height: 20px; - border-radius: 3px 3px 0px 0px; + border-radius: 3px 3px 0px 0px; } -section.install ul li a.right{ + +section.install ul li a.right { float: right; } /* Make tweaks here to accomodate additional tabs */ @media all and (max-width: 630px) { - section.install ul li a{ - padding:10px 2px; - font-size:12px; + section.install ul li a { + padding: 10px 2px; + font-size: 12px; } } @media all and (max-width: 420px) { - section.install ul li a{ + section.install ul li a { padding: 10px 15px; } - section.install ul li a span{ + section.install ul li a span { display: none; } } + /* end changes */ -section.install ul li:first-child a{ +section.install ul li:first-child a { margin-left: 0px; } -section.install div h3{ +section.install div h3 { color: black; margin-top: 0px; } @@ -493,85 +590,90 @@ section.install div h3{ /*Affiliated*/ -.featured{ - background-color:#fdfdfd; +.featured { + background-color: #fdfdfd; } -.featured div{ +.featured div { clear: both; padding: 20px 0px; } -.featured div:first-of-type{ +.featured div:first-of-type { padding-top: 5px; } -.featured div h3{ - color:black; +.featured div h3 { + color: black; margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid #FFE1D6; } -.featured img{ - float: left+; - margin-right:10px; +.featured img { + float: left; + margin-right: 10px; } -table{ +table { margin: 20px 0px; - word-break:overflow-wrap; + word-break: overflow-wrap; } -thead{ +thead { text-align: left; font-weight: bold; - border-bottom: 1px solid rgba(128,128,128,0.2); + border-bottom: 1px solid rgba(128, 128, 128, 0.2); } -td{ - padding:12px 5px; +td { + padding: 12px 5px; font-size: 100%; line-height: 1.3em; } td:first-child { - font-weight: bold; - } + font-weight: bold; +} /* Create gray border after every 3rd row to visually separate packages */ table tr.border-top { - border-top: 1px solid rgba(128,128,128,0.2); + border-top: 1px solid rgba(128, 128, 128, 0.2); } /* About */ -p.citation{ +p.citation { margin: 10px 40px; } -ul.team{ - -moz-column-count:5; /* Firefox */ - -webkit-column-count:5; /* Safari and Chrome */ - column-count:5; +ul.team { + -moz-column-count: 5; + /* Firefox */ + -webkit-column-count: 5; + /* Safari and Chrome */ + column-count: 5; } -@media only screen and (max-width: 920px){ - ul.team{ - -moz-column-count:2; /* Firefox */ - -webkit-column-count:2; /* Safari and Chrome */ - column-count:2; +@media only screen and (max-width: 920px) { + ul.team { + -moz-column-count: 2; + /* Firefox */ + -webkit-column-count: 2; + /* Safari and Chrome */ + column-count: 2; } } -ul.team li, ul.coordinators li{ - margin-bottom:0px; +ul.team li, +ul.coordinators li { + margin-bottom: 0px; } p.centered { @@ -579,65 +681,65 @@ p.centered { } .center { - text-align: center; - color: red; + text-align: center; + color: red; } .dropdown { - position: relative; - display: inline-block; + position: relative; + display: inline-block; } .dropdown-content { - display: none; - position: absolute; - background-color: DimGray; - min-width: 160px; - width: 100%; - height: 100% - box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); - padding: 0px 16px; - z-index: 1; + display: none; + position: absolute; + background-color: DimGray; + min-width: 160px; + width: 100%; + height: 100%; + box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); + padding: 0px 16px; + z-index: 1; } .dropdown:hover .dropdown-content { - display: block; + display: block; } .dropdown-content a { -display: block; + display: block; } .dropdown-content a:hover { -color: white; + color: white; } .dropdown a:hover { -color: #FF5000; + color: #FF5000; } .dropdown z:hover { -color: #FF5000; -width: 10; -display: none; + color: #FF5000; + width: 10; + display: none; } .dropdown z { -color: white; + color: white; } .dropdown:after { - content: ""; - position: absolute; - right: -13px; - top: 9px; - width: 0; - height: 0; - border-left: 5px solid transparent; - border-right: 5px solid transparent; - border-top: 5px solid white; + content: ""; + position: absolute; + right: -13px; + top: 9px; + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid white; } #roles-table td { padding: 7px 5px; -} +} \ No newline at end of file diff --git a/website/public/index.html b/website/public/index.html index 89d71ddf..9fd6d10e 100644 --- a/website/public/index.html +++ b/website/public/index.html @@ -24,18 +24,9 @@
  • sbpy
  • -
  • - -
  • +
  • About
  • Documentation
  • +
  • Help
  • Tutorials
  • Contributing
  • GitHub
  • @@ -72,16 +63,21 @@ -

    Current Version: 0.1.1

    +

    Current Version: 0.3

    Installation

    -

    We recommend - installing Anaconda - Python 3 before installing sbpy using: -

    pip install git+https://github.com/NASA-Planetary-Science/sbpy.git

    +

    sbpy is a Python package, installable with pip:

    +

    +

    pip install sbpy
    +

    +

    Or with Anaconda via the conda-forge channel: +

    +

    conda install sbpy --channel=conda-forge
    +

    Detailed Installation Instructions
    @@ -96,9 +92,27 @@

    Learn sbpy

    -

    Report bugs and Contribute

    -

    Please report what you believe is a bug or a mistake using the github issue tracker. If you have an idea for - how to improve sbpy, or would like to contribute code directly, please check out the contribution guidelines. +

    Get help

    +

    Questions about sbpy or how to use sbpy modules can be posted at sbpy + Discussions, hosted on GitHub. Help about using astropy and affiliated + packages (including sbpy) can also be found on Astropy's help + page. You can + also find sbpy developers on the Astropy slack workspace in the #sbpy + channel (see Astropy's help + page for connection details). +

    + sbpy + Discussions + Astropy help +
    + +
    +

    Report bugs and contribute

    +

    Please report what you believe is a bug or a mistake using sbpy's issue + tracker. If you have an idea for how to improve sbpy, or would like to + contribute code directly, please check out the contribution guidelines.

    Report issues Contribution @@ -125,7 +139,7 @@

    Acknowledge sbpy

    Partner
    - sbpy is supported by NASA PDART Grant No. 80NSSC18K0987.

    + sbpy support provided by NASA PDART Grant Nos. 80NSSC18K0987, 80NSSC22K0143.

    diff --git a/website/public/team.html b/website/public/team.html index 0f2b0a97..2680b000 100644 --- a/website/public/team.html +++ b/website/public/team.html @@ -1,95 +1,91 @@ + - - - - + + + + - - - + + + -sbpy team + sbpy team + - - - - + + + + - - - + + + -sbpy + sbpy -
    - - -
    +

    The sbpy Team

    The development of sbpy is a team effort that would - not be possible without the help of supporters that provide, - test, and document code:

    + not be possible without the help of supporters that provide, + test, and document code:

    -

    - Michael Mommert, Lowell Observatory

    -

    - Michael S. P. Kelley, University of Maryland

    -

    Miguel de Val-Borro, Planetary Science Institute

    -

    Jian-Yang Li, Planetary Science Institute

    -

    Giannina Guzman, Villanova University/Lowell Observatory

    -

    Brigitta Sipőcz, University of Washington

    -

    Josef Ďurech, Charles University Prague

    -

    Mikael Granvik, University of Helsinki

    -

    Will Grundy, Lowell Observatory

    -

    Nick Moskovitz, Lowell Observatory

    -

    Antti Penttilä, University of Helsinki

    -

    Nalin Samarasinha, Planetary Science Institute

    +

    + Michael S. P. Kelley, University of Maryland (Co-Lead Developer)

    +

    Jian-Yang Li, Planetary Science Institute (Co-Lead + Developer)

    +

    + Michael Mommert, University of St. Gallen, Switzerland (Initial Project Leader)

    +

    Miguel de Val-Borro, Planetary Science Institute

    +

    Giannina Guzman, + Villanova University/Lowell Observatory

    +

    Brigitta Sipőcz, University of Washington

    +

    Josef Ďurech, Charles University Prague

    +

    Mikael Granvik, University of Helsinki

    +

    Will Grundy, Lowell Observatory

    +

    Nick Moskovitz, Lowell Observatory

    +

    Antti Penttilä, University of Helsinki

    +

    Nalin Samarasinha, Planetary Science Institute

     

    - Become a Part of the Team! -
    -
    - + Become + a Part of the Team! +
    + + - + + \ No newline at end of file From af59e11dbba7bb4bdbfe538781ea72a62537d36d Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Wed, 13 Jul 2022 14:58:56 -0400 Subject: [PATCH 40/59] More links. --- website/public/index.html | 5 +++-- website/public/team.html | 38 +++++++++++++++++++++++--------------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/website/public/index.html b/website/public/index.html index 9fd6d10e..b564fce9 100644 --- a/website/public/index.html +++ b/website/public/index.html @@ -112,10 +112,11 @@

    Report bugs and contribute

    Please report what you believe is a bug or a mistake using sbpy's issue tracker. If you have an idea for how to improve sbpy, or would like to - contribute code directly, please check out the contribution guidelines. + contribute code directly, please check out the contribution guidelines.

    Report issues - Contribution + Contribution Guidelines diff --git a/website/public/team.html b/website/public/team.html index 2680b000..42075d23 100644 --- a/website/public/team.html +++ b/website/public/team.html @@ -60,28 +60,36 @@

    The sbpy Team

    -

    The development of sbpy is a team effort that would - not be possible without the help of supporters that provide, - test, and document code:

    +

    The development of sbpy is a team effort that would not be + possible without the help of supporters that provide, test, and document + code. Our initial team is listed below. GitHub also tracks all contributors + to the sbpy + and sbpy + Tutorials projects.

    Michael S. P. Kelley, University of Maryland (Co-Lead Developer)

    Jian-Yang Li, Planetary Science Institute (Co-Lead Developer)

    - Michael Mommert, University of St. Gallen, Switzerland (Initial Project Leader)

    -

    Miguel de Val-Borro, Planetary Science Institute

    -

    Giannina Guzman, - Villanova University/Lowell Observatory

    -

    Brigitta Sipőcz, University of Washington

    -

    Josef Ďurech, Charles University Prague

    -

    Mikael Granvik, University of Helsinki

    -

    Will Grundy, Lowell Observatory

    -

    Nick Moskovitz, Lowell Observatory

    -

    Antti Penttilä, University of Helsinki

    -

    Nalin Samarasinha, Planetary Science Institute

    + Michael Mommert, University of St. Gallen, Switzerland (Initial Project Lead)

    +

    Miguel de Val-Borro, Planetary Science + Institute

    +

    Giannina Guzman, + University of Maryland

    +

    Brigitta Sipőcz, Caltech/IPAC

    +

    Josef Ďurech, Charles + University, Prague

    +

    Mikael Granvik, + University of Helsinki

    +

    Will Grundy, Lowell Observatory

    +

    Nick Moskovitz, Lowell Observatory

    +

    Antti + Penttilä, University of Helsinki

    +

    Nalin Samarasinha, Planetary Science + Institute

     

    - Become + Become a Part of the Team!
    From 6c30414ee2540279c9fafe0e7a4a3700d0bfcd71 Mon Sep 17 00:00:00 2001 From: Jian-Yang Li Date: Fri, 15 Jul 2022 13:50:27 -0400 Subject: [PATCH 41/59] bugfix `DataClass.__setitem__` - allow to set column contents with alternative names --- sbpy/data/core.py | 12 ++++++++++-- sbpy/data/tests/test_dataclass.py | 8 ++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/sbpy/data/core.py b/sbpy/data/core.py index 877c976a..805b68b2 100644 --- a/sbpy/data/core.py +++ b/sbpy/data/core.py @@ -643,9 +643,17 @@ def __getitem__(self, ident): # return as new instance of this class for all other identifiers return self.from_table(self.table[ident]) - def __setitem__(self, *args): + def __setitem__(self, ident, val): """Refer cls.__setitem__ to self._table""" - self.table.__setitem__(*args) + # `astropy.table.Table.__setitem__` only allows to set a single + # column. Only this case is checked here for alternative column + # name. All other cases are directly passed to `table.__setitem__`. + if isinstance(ident, str): + try: + ident = self._translate_columns(ident)[0] + except KeyError: + pass + self.table.__setitem__(ident, val) def __contains__(self, value): """Use cls._translate_columns to realize the `in` operator""" diff --git a/sbpy/data/tests/test_dataclass.py b/sbpy/data/tests/test_dataclass.py index 9c52de2b..fcd79e19 100644 --- a/sbpy/data/tests/test_dataclass.py +++ b/sbpy/data/tests/test_dataclass.py @@ -313,6 +313,14 @@ def test_get_set(): assert data['z'][1] == 2 assert isinstance(data, DataClass) + # modify existing column using alternative name + data = DataClass.from_dict( + {'rh': [1, 2, 3] * u.au, + 'delta': [4, 5, 6] * u.au}) + data['r'] = [7, 8, 9] * u.au + assert u.allclose(data['rh'], [7, 8, 9] * u.au) + assert set(data.field_names) == {'rh', 'delta'} + def test_units(): """ test units on multi-row tables """ From c393d42760d81786d95d0093a90a87a8f1dde577 Mon Sep 17 00:00:00 2001 From: Jian-Yang Li Date: Fri, 15 Jul 2022 23:39:28 -0400 Subject: [PATCH 42/59] improve `DataClass._translate_columns` - try to simplify the logic and slightly improve efficiency --- sbpy/data/core.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/sbpy/data/core.py b/sbpy/data/core.py index 805b68b2..3e5c15c3 100644 --- a/sbpy/data/core.py +++ b/sbpy/data/core.py @@ -681,22 +681,17 @@ def _translate_columns(self, target_colnames): if colname in self.field_names: continue # colname is an alternative column name - elif colname in sum(Conf.fieldnames, []): + else: found = False - for alt in Conf.fieldnames[Conf.fieldname_idx[colname]]: - # translation available for colname + for alt in Conf.fieldnames[ + Conf.fieldname_idx.get(colname, slice(0))]: if alt in self.field_names: translated_colnames[idx] = alt found = True break - # not found in the table if not found: - raise KeyError('field {:s} not available.'.format( + raise KeyError('field "{:s}" not available.'.format( colname)) - # colname is unknown, raise a KeyError - else: - raise KeyError('field {:s} not available.'.format( - colname)) return translated_colnames From 2c30ca7e6779247a3f20655f009f346dec198ab6 Mon Sep 17 00:00:00 2001 From: Jian-Yang Li Date: Fri, 15 Jul 2022 23:40:33 -0400 Subject: [PATCH 43/59] improve `DataClass.__contains__` - remove the dependency on `DataClass._translate_columns` --- sbpy/data/core.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sbpy/data/core.py b/sbpy/data/core.py index 3e5c15c3..faf93a62 100644 --- a/sbpy/data/core.py +++ b/sbpy/data/core.py @@ -657,12 +657,12 @@ def __setitem__(self, ident, val): def __contains__(self, value): """Use cls._translate_columns to realize the `in` operator""" - try: - _ = self._translate_columns(value) - except KeyError: - return False - else: + if (value in self.table.colnames) or \ + (value in sum([Conf.fieldnames[Conf.fieldname_idx.get(x, slice(0))] + for x in self.table.colnames], [])): return True + else: + return False def _translate_columns(self, target_colnames): """Translate target_colnames to the corresponding column names From c2e0f05fc40dde6c602d1664850149c689ed065b Mon Sep 17 00:00:00 2001 From: Jian-Yang Li Date: Fri, 15 Jul 2022 23:44:48 -0400 Subject: [PATCH 44/59] improve `DataClass.__setitem__` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - make use of `__contains__` to simplify logic. Now `__contains__` doesn’t depend on `_translate_columns`, using `__contains__` is not going to affect efficiency. --- sbpy/data/core.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/sbpy/data/core.py b/sbpy/data/core.py index faf93a62..3324f839 100644 --- a/sbpy/data/core.py +++ b/sbpy/data/core.py @@ -648,11 +648,8 @@ def __setitem__(self, ident, val): # `astropy.table.Table.__setitem__` only allows to set a single # column. Only this case is checked here for alternative column # name. All other cases are directly passed to `table.__setitem__`. - if isinstance(ident, str): - try: - ident = self._translate_columns(ident)[0] - except KeyError: - pass + if isinstance(ident, str) and ident in self: + ident = self._translate_columns(ident)[0] self.table.__setitem__(ident, val) def __contains__(self, value): From 67c4292425a615836567576cdc10a7bdc6a38932 Mon Sep 17 00:00:00 2001 From: Jian-Yang Li Date: Fri, 15 Jul 2022 23:47:35 -0400 Subject: [PATCH 45/59] pep8 fix --- sbpy/data/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sbpy/data/core.py b/sbpy/data/core.py index 3324f839..c9aa3d4b 100644 --- a/sbpy/data/core.py +++ b/sbpy/data/core.py @@ -656,7 +656,7 @@ def __contains__(self, value): """Use cls._translate_columns to realize the `in` operator""" if (value in self.table.colnames) or \ (value in sum([Conf.fieldnames[Conf.fieldname_idx.get(x, slice(0))] - for x in self.table.colnames], [])): + for x in self.table.colnames], [])): return True else: return False From 8241a98528c7a8a58d6c34583421de39f9fe415d Mon Sep 17 00:00:00 2001 From: Jian-Yang Li Date: Tue, 19 Jul 2022 10:44:05 -0400 Subject: [PATCH 46/59] improve `_translate_columns` - use for-else logic - update docstring for `__contains__` --- sbpy/data/core.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/sbpy/data/core.py b/sbpy/data/core.py index c9aa3d4b..307841ba 100644 --- a/sbpy/data/core.py +++ b/sbpy/data/core.py @@ -653,7 +653,7 @@ def __setitem__(self, ident, val): self.table.__setitem__(ident, val) def __contains__(self, value): - """Use cls._translate_columns to realize the `in` operator""" + """To support `in` operator that allows for alternative names""" if (value in self.table.colnames) or \ (value in sum([Conf.fieldnames[Conf.fieldname_idx.get(x, slice(0))] for x in self.table.colnames], [])): @@ -679,14 +679,12 @@ def _translate_columns(self, target_colnames): continue # colname is an alternative column name else: - found = False for alt in Conf.fieldnames[ Conf.fieldname_idx.get(colname, slice(0))]: if alt in self.field_names: translated_colnames[idx] = alt - found = True break - if not found: + else: raise KeyError('field "{:s}" not available.'.format( colname)) From ece2fb6a060e03b7d48c5285a9dd15c37c5938f7 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Sun, 5 Jun 2022 19:20:35 -0400 Subject: [PATCH 47/59] Fix URLs --- sbpy/calib/solar_sources.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sbpy/calib/solar_sources.py b/sbpy/calib/solar_sources.py index 61adc31b..90417481 100644 --- a/sbpy/calib/solar_sources.py +++ b/sbpy/calib/solar_sources.py @@ -37,13 +37,13 @@ class SolarSpectra: } Kurucz1993 = { - 'filename': 'ftp://ftp.stsci.edu/cdbs/grid/k93models/standards/sun_kurucz93.fits', + 'filename': 'https://archive.stsci.edu/hlsps/reference-atlases/cdbs/grid/k93models/standards/sun_kurucz93.fits', 'description': 'Kurucz (1993) model, scaled by Colina et al. (1996)', 'bibcode': '1993KurCD..13.....K' } Castelli1996 = { - 'filename': 'ftp://ftp.stsci.edu/cdbs/grid/k93models/standards/sun_castelli.fits', + 'filename': 'https://archive.stsci.edu/hlsps/reference-atlases/cdbs/grid/k93models/standards/sun_castelli.fits', 'description': 'Castelli model, scaled and presented by Colina et al. (1996)', 'bibcode': '1996AJ....112..307C' } From 54ea76ed9920f6d7831e82f403be5a2d2a085574 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Sun, 5 Jun 2022 19:20:49 -0400 Subject: [PATCH 48/59] Remove unused imports --- sbpy/calib/core.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sbpy/calib/core.py b/sbpy/calib/core.py index 9d69fc6e..864fe8e0 100644 --- a/sbpy/calib/core.py +++ b/sbpy/calib/core.py @@ -30,8 +30,7 @@ import numpy as np from astropy.utils.state import ScienceState from astropy.utils.data import get_pkg_data_filename -from astropy.table import Table, QTable -from astropy.io import ascii +from astropy.table import Table import astropy.units as u from ..spectroscopy.sources import SpectralSource, SynphotRequired From 96556baae57d556d49119332b37594c173766483 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Sun, 5 Jun 2022 19:40:43 -0400 Subject: [PATCH 49/59] Test the Castelli1996 solar spectrum --- sbpy/calib/tests/test_sun.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/sbpy/calib/tests/test_sun.py b/sbpy/calib/tests/test_sun.py index 5d661b10..1635c473 100644 --- a/sbpy/calib/tests/test_sun.py +++ b/sbpy/calib/tests/test_sun.py @@ -134,8 +134,7 @@ def test_meta(self): sun = Sun.from_builtin('E490_2014') assert sun.meta is None - @pytest.mark.skipif('True') - @pytest.mark.remote_data + @remote_data def test_kurucz_nan_error(self): """sbpy#113 @@ -149,6 +148,25 @@ def test_kurucz_nan_error(self): fluxd = sun.observe(V, unit=u.ABmag) assert np.isclose(fluxd.value, -26.77, atol=0.005) + @remote_data + def test_castelli96(self): + """Verify Castelli1996 calibration. + + According to the FITS header: + + HISTORY Created Thu 15:49:31 16-Nov-95 + COMMENT solar model spectrum calculated by F. Castelli. + COMMENT Absolute flux normalized to a V flux of 184.2 ergs/s/cm^2/A + COMMENT For more details see Colina, Bohlin & Castelli 1996 CAL/SCS-008 + + 2022-06-05: sbpy calculates 184.5 ergs/s/cm^2/A; agreement within 0.2% + """ + + sun = Sun.from_builtin('Castelli1996') + V = bandpass('johnson v') + fluxd = sun.observe(V, unit='erg/(s cm2 AA)') + assert np.isclose(fluxd.value, 184.2, rtol=0.002) + def test_show_builtin(self, capsys): Sun.show_builtin() captured = capsys.readouterr() From f1f26af5c0daeaa30f3ceca032ae3f4dbf768528 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Sun, 5 Jun 2022 19:58:36 -0400 Subject: [PATCH 50/59] Update test and maybe we can catch these issues more quickly. --- .github/workflows/ci_cron_weekly.yml | 21 +++++++++++++++------ .github/workflows/ci_tests.yml | 4 ++-- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci_cron_weekly.yml b/.github/workflows/ci_cron_weekly.yml index 6b287e14..1969c504 100644 --- a/.github/workflows/ci_cron_weekly.yml +++ b/.github/workflows/ci_cron_weekly.yml @@ -1,4 +1,5 @@ # GitHub Actions workflow that runs on a cron schedule. +# Check URLs and run latest deps with remote tests name: Weekly CI Tests @@ -8,9 +9,18 @@ on: - cron: '0 6 * * 1' jobs: - # Weekly testing to catch links that stop working for some reason. - doc_test: + weekly: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - name: Check links in docs using tox + toxenv: linkcheck + - name: Check dev versions of key dependencies + toxenv: py39-test-devdeps + toxposargs: --remote-data + steps: - name: Checkout code uses: actions/checkout@v2 @@ -19,10 +29,9 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: 3.9 - name: Install base dependencies run: | python -m pip install --upgrade pip tox - - name: Check links in docs using tox - run: | - tox -e linkcheck + - name: Test with tox + run: tox ${{ matrix.toxargs}} -e ${{ matrix.toxenv}} -- ${{ matrix.toxposargs}} diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index 6a0c2009..b5f984f5 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -88,8 +88,8 @@ jobs: python -m pip install --upgrade pip python -m pip install tox codecov - name: Test with tox - run: | - tox -e ${{ matrix.toxenv }} + run: tox ${{ matrix.toxargs}} -e ${{ matrix.toxenv}} -- ${{ matrix.toxposargs}} + # This is an example of how to upload coverage to codecov # - name: Upload coverage to codecov # if: "contains(matrix.toxenv, '-cov')" From 9185f1cd32589bc3af070c9354a21febef1273cd Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Fri, 24 Jun 2022 17:04:35 -0400 Subject: [PATCH 51/59] Line length fixes. --- sbpy/calib/solar_sources.py | 12 ++++++++---- sbpy/calib/tests/test_sun.py | 3 ++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/sbpy/calib/solar_sources.py b/sbpy/calib/solar_sources.py index 90417481..c644a5ba 100644 --- a/sbpy/calib/solar_sources.py +++ b/sbpy/calib/solar_sources.py @@ -32,19 +32,23 @@ class SolarSpectra: 'filename': 'e490-00a_2014_hires.csv', 'wave_unit': 'um', 'flux_unit': 'W/(m2 um)', - 'description': 'E490-00a (2014) low resolution reference solar spectrum (Table 4)', + 'description': ('E490-00a (2014) low resolution reference solar ' + 'spectrum (Table 4)'), 'bibcode': 'doi:10.1520/E0490' } Kurucz1993 = { - 'filename': 'https://archive.stsci.edu/hlsps/reference-atlases/cdbs/grid/k93models/standards/sun_kurucz93.fits', + 'filename': ('https://archive.stsci.edu/hlsps/reference-atlases/cdbs/' + 'grid/k93models/standards/sun_kurucz93.fits'), 'description': 'Kurucz (1993) model, scaled by Colina et al. (1996)', 'bibcode': '1993KurCD..13.....K' } Castelli1996 = { - 'filename': 'https://archive.stsci.edu/hlsps/reference-atlases/cdbs/grid/k93models/standards/sun_castelli.fits', - 'description': 'Castelli model, scaled and presented by Colina et al. (1996)', + 'filename': ('https://archive.stsci.edu/hlsps/reference-atlases/cdbs/' + 'grid/k93models/standards/sun_castelli.fits'), + 'description': ('Castelli model, scaled and presented by Colina et ' + 'al. (1996)'), 'bibcode': '1996AJ....112..307C' } diff --git a/sbpy/calib/tests/test_sun.py b/sbpy/calib/tests/test_sun.py index 1635c473..c124e1ff 100644 --- a/sbpy/calib/tests/test_sun.py +++ b/sbpy/calib/tests/test_sun.py @@ -157,7 +157,8 @@ def test_castelli96(self): HISTORY Created Thu 15:49:31 16-Nov-95 COMMENT solar model spectrum calculated by F. Castelli. COMMENT Absolute flux normalized to a V flux of 184.2 ergs/s/cm^2/A - COMMENT For more details see Colina, Bohlin & Castelli 1996 CAL/SCS-008 + COMMENT For more details see Colina, Bohlin & Castelli 1996 + CAL/SCS-008 2022-06-05: sbpy calculates 184.5 ergs/s/cm^2/A; agreement within 0.2% """ From fd362c734291538a6b10fe470717ad6f1a760cc0 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Wed, 6 Jul 2022 09:21:33 -0400 Subject: [PATCH 52/59] Fix remote_data decorations. --- sbpy/calib/tests/test_sun.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sbpy/calib/tests/test_sun.py b/sbpy/calib/tests/test_sun.py index c124e1ff..789d6bb3 100644 --- a/sbpy/calib/tests/test_sun.py +++ b/sbpy/calib/tests/test_sun.py @@ -134,7 +134,7 @@ def test_meta(self): sun = Sun.from_builtin('E490_2014') assert sun.meta is None - @remote_data + @pytest.mark.remote_data def test_kurucz_nan_error(self): """sbpy#113 @@ -148,7 +148,7 @@ def test_kurucz_nan_error(self): fluxd = sun.observe(V, unit=u.ABmag) assert np.isclose(fluxd.value, -26.77, atol=0.005) - @remote_data + @pytest.mark.remote_data def test_castelli96(self): """Verify Castelli1996 calibration. From 3ea415e6c0c1b673f5df2bb4b8ae7b955d85fcca Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Wed, 6 Jul 2022 09:58:11 -0400 Subject: [PATCH 53/59] Reduce test precision on test_tisserand. Test is presently failing. This will help mitigate the effects of small orbital changes at JPL Horizons. --- sbpy/data/tests/test_orbit_remote.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sbpy/data/tests/test_orbit_remote.py b/sbpy/data/tests/test_orbit_remote.py index d41869de..b569cf60 100644 --- a/sbpy/data/tests/test_orbit_remote.py +++ b/sbpy/data/tests/test_orbit_remote.py @@ -187,7 +187,7 @@ def test_oo_propagate(self): elements = ['a', 'e', 'i', 'Omega', 'w', 'M'] assert all([u.isclose(oo_orbit[k][0], future_orbit[k][0]) - for k in elements]) + for k in elements]) assert u.isclose(oo_orbit['epoch'][0].utc.jd, future_orbit['epoch'][0].utc.jd) assert oo_orbit['epoch'].scale == 'utc' From da13d73555f6b1c6f0d7b43731992851f449c6a4 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Wed, 6 Jul 2022 10:26:35 -0400 Subject: [PATCH 54/59] Use NASA ADS API dev key in github tests. --- .github/workflows/ci_tests.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index b5f984f5..a32ea031 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -88,7 +88,9 @@ jobs: python -m pip install --upgrade pip python -m pip install tox codecov - name: Test with tox - run: tox ${{ matrix.toxargs}} -e ${{ matrix.toxenv}} -- ${{ matrix.toxposargs}} + env: + ADS_DEV_KEY: ${{ secrets.ADS_DEV_KEY }} + run: tox ${{ matrix.toxargs }} -e ${{ matrix.toxenv }} -- ${{ matrix.toxposargs }} # This is an example of how to upload coverage to codecov # - name: Upload coverage to codecov From 8482715480f7d43d912dcd90f4e4da5a5dc9ad53 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Wed, 6 Jul 2022 10:46:34 -0400 Subject: [PATCH 55/59] Also pass ADS_DEV_KEY through tox --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 935b9267..aad00a28 100644 --- a/tox.ini +++ b/tox.ini @@ -19,7 +19,7 @@ isolated_build = true pypi_filter = https://raw.githubusercontent.com/astropy/ci-helpers/main/pip_pinnings.txt # Pass through the following environemnt variables which are needed for the CI -passenv = HOME WINDIR LC_ALL LC_CTYPE CC CI TEST_READ_HUGE_FILE FC_GFORTRAN +passenv = HOME WINDIR LC_ALL LC_CTYPE CC CI TEST_READ_HUGE_FILE FC_GFORTRAN ADS_DEV_KEY # For coverage, we need to pass extra options to the C compiler setenv = From 2881a0244b2f34b72204c1dc24686273c2de0991 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Wed, 6 Jul 2022 11:08:33 -0400 Subject: [PATCH 56/59] Try to fix CI error for alldeps --- sbpy/data/ephem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sbpy/data/ephem.py b/sbpy/data/ephem.py index 6f2e893e..d8068752 100644 --- a/sbpy/data/ephem.py +++ b/sbpy/data/ephem.py @@ -738,7 +738,7 @@ def from_oo(cls, orbit, epochs=None, location='500', scope='full', 1 Ceres 2458529.2336329385 ... 1.326517587380005e-05 70.34353031031534 1 Ceres 2458529.2752996054 ... 1.1193369555934085e-05 70.35298818987367 """ - if not pyoorb: + if pyoorb is None: raise RequiredPackageUnavailable('pyoorb') # create a copy of orbit From ad5a3115f7e4b93ec5ac17619fc6b052164576c2 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Wed, 6 Jul 2022 11:09:03 -0400 Subject: [PATCH 57/59] Test workflow env vars --- .github/workflows/ci_tests.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index a32ea031..d043963a 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -90,7 +90,12 @@ jobs: - name: Test with tox env: ADS_DEV_KEY: ${{ secrets.ADS_DEV_KEY }} - run: tox ${{ matrix.toxargs }} -e ${{ matrix.toxenv }} -- ${{ matrix.toxposargs }} + TEST_TEST: ${{ secrets.TEST_TEST }} + TEST: "asdf" + run: | + echo $TEST + echo $TEST_TEST + tox ${{ matrix.toxargs }} -e ${{ matrix.toxenv }} -- ${{ matrix.toxposargs }} # This is an example of how to upload coverage to codecov # - name: Upload coverage to codecov From 0266ead1a2f9cfad2d5890f40a5563d2119d1074 Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Wed, 6 Jul 2022 11:26:47 -0400 Subject: [PATCH 58/59] Revert "Test workflow env vars" This reverts commit 17bc0d667f264e279ff28175685fb4fa15359489. --- .github/workflows/ci_tests.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index d043963a..a32ea031 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -90,12 +90,7 @@ jobs: - name: Test with tox env: ADS_DEV_KEY: ${{ secrets.ADS_DEV_KEY }} - TEST_TEST: ${{ secrets.TEST_TEST }} - TEST: "asdf" - run: | - echo $TEST - echo $TEST_TEST - tox ${{ matrix.toxargs }} -e ${{ matrix.toxenv }} -- ${{ matrix.toxposargs }} + run: tox ${{ matrix.toxargs }} -e ${{ matrix.toxenv }} -- ${{ matrix.toxposargs }} # This is an example of how to upload coverage to codecov # - name: Upload coverage to codecov From a1fb6b2c2ead21d1a40caac506c356db747fd3da Mon Sep 17 00:00:00 2001 From: "Michael S. P. Kelley" Date: Wed, 6 Jul 2022 11:28:42 -0400 Subject: [PATCH 59/59] Ah, github secrets cannot be used in PRs from forked repos. --- .github/workflows/ci_cron_weekly.yml | 2 ++ .github/workflows/ci_tests.yml | 5 +---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci_cron_weekly.yml b/.github/workflows/ci_cron_weekly.yml index 1969c504..7ba6cfb3 100644 --- a/.github/workflows/ci_cron_weekly.yml +++ b/.github/workflows/ci_cron_weekly.yml @@ -34,4 +34,6 @@ jobs: run: | python -m pip install --upgrade pip tox - name: Test with tox + env: + ADS_DEV_KEY: ${{ secrets.ADS_DEV_KEY }} run: tox ${{ matrix.toxargs}} -e ${{ matrix.toxenv}} -- ${{ matrix.toxposargs}} diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index a32ea031..2174b3cc 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -42,11 +42,10 @@ jobs: python: 3.8 toxenv: py38-test-alldeps - - name: Python 3.8 with all optional dependencies, remote tests, and coverage checking + - name: Python 3.8 with all optional dependencies and coverage checking os: ubuntu-latest python: 3.8 toxenv: py38-test-alldeps-cov - toxposargs: --remote-data # - name: Windows - Python 3.8 with all optional dependencies # os: windows-latest @@ -88,8 +87,6 @@ jobs: python -m pip install --upgrade pip python -m pip install tox codecov - name: Test with tox - env: - ADS_DEV_KEY: ${{ secrets.ADS_DEV_KEY }} run: tox ${{ matrix.toxargs }} -e ${{ matrix.toxenv }} -- ${{ matrix.toxposargs }} # This is an example of how to upload coverage to codecov