Skip to content

Commit

Permalink
feat(ch_ephem): CHIME ephemeris refactored from ch_util.
Browse files Browse the repository at this point in the history
Extracts a lot of `ch_util.ephemeris` into a separate package.

General changes:

* Solar and lunar ephemeris functions have been converted into
  `caput.time.Observer` methods: radiocosmology/caput#271
* Other functions that take a `Observer` object, now use `None`
  as a default, and load the `chime` observer during execution if
  necessary.

More comments on specific submodules follow.

Catalogs:

This contains all the `ch_util/catalogs` plus a function
(`ch_ephem.catalogs.load`) to read them and return the parsed JSON.

The standard source catalogues loaded by the `sources` submodule need
to move to `ch_ephem`.  The other catalogues could stay in `ch_util`,
but it seems simpler to me to keep them all in the same place and
provide a uniform interface to them.

Coord:

Co-ordinate transforms etc.  These are essentially unchanged from
their versions in `ch_util.ephemeris`.

Observers:

Re-creates all the `caput.time.Observers` which used to be created
by `ch_util.ephemeris`, but for the most part, they have to be
fetched via name from the `get()` function.  The exception to this
is the `chime` Observer, which can be directly imported, since it's
the one usually needed.

Position data for the observers is now in a YAML data file
`instruments.yaml` which is parsed at import time.  New instrument
`Observer` objects can be added to `ch_ephem` just by adding information
for it to the YAML file.

Also note that `Observers` returned by this submodule have an extra
attribute, `rotation`, inserted into their `__dict__`, which is not
in `caput`.  This rotation is needed by `ch_ephem.coord.peak_ra`.
If we don't want to do this to these `Observers`, one other options
would be to keep `peak_ra` in `ch_util` (moving it to, say,
`ch_util.tools`), and then there's no need for rotation information
in this module.

Like `sources`, this submodule reads data from disk at import time,
so is not automatically imported into the base `ch_ephem` object.

Pointing:

Just Galt telescope pointing routines, unchanged from
`ch_util.ephemeris`.  These are another candidate we could consider
putting somewhere else in ch_util, if we don't want them here.

Sources:

Loads the standard source catalogs, and provides the calibrator objects
`CasA`, `CygA`, `TauA`, `VirA`.  Like `observers`, this submodule
reads data from disk at import time, so is not automatically imported
into the base `ch_ephem` object.

Time:

Time utilities, largely unchanged from what was in `ch_util.ephemeris`.
  • Loading branch information
ketiltrout committed Aug 1, 2024
1 parent 22b9b1f commit 7694836
Show file tree
Hide file tree
Showing 15 changed files with 39,736 additions and 6 deletions.
40 changes: 40 additions & 0 deletions .github/workflows/linting.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: ch_ephem-linting
on:
pull_request:
branches:
- master
push:
branches:
- master

jobs:

black-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

- name: Set up Python 3.11
uses: actions/setup-python@v2
with:
python-version: "3.11"

- name: Install black
run: pip install black

- name: Check code with black
run: black --check .

ruff-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

- name: Install ruff
run: pip install ruff

- name: Install ch_ephem
run: pip install .

- name: Run ruff
run: ruff check .
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# ch_ephem
# CHIME ephemeris

CHIME ephemeris
This package provides the CHIME ephemeris routines, `ch_ephem`. Most
of this code originated in `ch_util`.

For general-purpose ephemeris routines, see `caput`.
105 changes: 104 additions & 1 deletion ch_ephem/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,104 @@
pass
"""CHIME ephemeris routines
Instrument Observrers and General Ephemeris Routines
====================================================
Any ephemeris routine which needs to know the location of the
observer on the Earth are accessible via instrument Observer
instances obtainable through :py:meth:`ch_ephem.observers.get`:
>>> import ch_ephem.observers
>>> pathfinder = ch_ephem.observers.get("pathfinder")
>>> pathfinder.solar_transit(...)
The `Observer` instances returned by this method are subclassed from
`caput.time.Observers` and can be used as normal `caput` observers.
Location and geometry data for the instrument observers are defined
in the data file `instruments.yaml` provided as part of `ch_ephem`.
Celestial Intermediate Reference System
=======================================
The precession of the Earth's axis gives noticeable shifts in object
positions over the life time of CHIME. To minimise the effects of this we
need to be careful and consistent with our ephemeris calculations.
Historically Right Ascension has been given with respect to the Vernal
Equinox which has a significant (and unnecessary) precession in the origin of
the RA axis. To avoid this we use the new Celestial Intermediate Reference
System which does not suffer from this issue.
Practically this means that when calculating RA, DEC coordinates for a source
position at a *given time* you must be careful to obtain CIRS coordinates
(and not equinox based ones). Internally using `ch_ephem.coord.object_coords` does
exactly that for you, so for any lookup of coordinates you should use that on
your requested body.
Note that the actual coordinate positions of sources must be specified using
RA, DEC coordinates in ICRS (which is roughly equivalent to J2000). The
purpose of `object_coords` is to transform into new RA, DEC coordinates taking
into account the precession and nutation of the Earth's polar axis since
then.
These kind of coordinate issues are tricky, confusing and hard to debug years
later, so if you're unsure you are recommended to seek some advice.
Radio Source Catalogs
=====================
This package provides several radio source catalogues used by CHIME. The
standard radio source catalogue used by CHIME is available as
`ch_ephem.sources.source_dictionary`:
>>> from ch_ephem.sources import source_dictionary
>>> source_dictionary['CAS_A'].ra
<Angle 23h 23m 27.94s>
The four standard CHIME cailbrators are also available by name:
>>> from ch_ephem.sources import CasA, CygA, VirA, TauA
>>> CasA.ra
<Angle 23h 23m 27.94s>
Additional catalogues which are not part of the standard radio source catalogue
are also available through the `ch_ephem.catalogs` module. Any catalog can be
accessed using :py:meth:`ch_ephem.catalogs.load`, which takes a catalogue name
and returns a dict containing the parsed JSON representation of the catalog:
>>> import ch_ephem.catalogs
>>> ch_ephem.catalogs.list()
['atnf_psrcat', 'hfb_target_list', 'primary_calibrators_perley2016', 'specfind_v2_5Jy_vollmer2009']
>>> perley2016 = ch_ephem.catalogs.load('primary_calibrators_perley2016')
>>> perley2016["CAS_A"]["ra"]
350.86642
Submodules
==========
Note: the submodules `observers` and `sources` read data from disk at import time.
.. autosummary::
:toctree: _autosummary
catalogs
coord
observers
pointing
sources
time
"""

__all__ = ["catalogs", "coord", "pointing", "time", "__version__"]

# We deliberately do not import .observers and .sources here
# because they both perform reads from disk
from . import catalogs, coord, pointing, time

from importlib.metadata import version, PackageNotFoundError

try:
__version__ = version("ch_ephem")
except PackageNotFoundError:
# package is not installed
pass

del version, PackageNotFoundError
64 changes: 64 additions & 0 deletions ch_ephem/catalogs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""CHIME ephemeris source catalogues
Functions
=========
- :py:meth:`list`
- :py:meth:`load`
"""

from __future__ import annotations
from typing import TYPE_CHECKING

import glob
import json
import pathlib

if TYPE_CHECKING:
from collections.abc import Iterable
del TYPE_CHECKING


def list() -> list[str]:
"""List available catalogues.
Returns
-------
catalogs : list
A list with the names of available catalogs
"""

cats = sorted(glob.glob("*.json", root_dir=pathlib.Path(__file__).parent))

# Strip ".json" off the end
return [cat[:-5] for cat in cats]


def load(name: str) -> Iterable:
"""Read the named catalogue and return the parsed JSON representation.
Parameters
----------
name : str
The name of the catalogue to load. This is the name of a JSON file
in the `ch_ephem/catalogs` directory, excluding the `.json` suffix.
Returns
-------
catalogue: Iterable
The parsed catalogue.
Raises
------
JSONDecodeError:
An error occurred while trying to parse the catalogue.
ValueError:
No catalogue with the given name could be found.
"""
path = pathlib.Path(__file__).with_name(name + ".json")

try:
with path.open() as f:
return json.load(f)
except FileNotFoundError as e:
raise ValueError(f"No such catalogue: {name}") from e
Loading

0 comments on commit 7694836

Please sign in to comment.