Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,17 @@ jobs:
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
pip install '.[test]'
- name: Lint with flake8
continue-on-error: true
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --extend-ignore=F821 --exclude .git,.idea,.mypy_cache,Notebooks/
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
flake8 . --count --exit-zero --max-complexity=20 --max-line-length=300 --statistics --extend-ignore=F821 --exclude .git,.idea,.mypy_cache,Notebooks/
- name: update tendrils
run: |

- name: Test with pytest
env:
FLOWS_API_TOKEN: ${{ secrets.FLOWS_API_TOKEN }}
run: |
pytest
pytest -v
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ Installs a user script `get_brightest` that can be run from the command line.

### test with:
``pip install flows_get_brightest[test]``
``pytest``

``pytest -v --cov``
``mypy flows_get_brightest/ typings/tendrils/ --config-file=pyproject.toml --check-untyped-defs``
``flake8 . --count --exit-zero --max-complexity=10 --max-line-length=120 --statistics --extend-ignore=F821 --exclude .git,.idea,.mypy_cache,Notebooks/``
``black --check --verbose flows_get_brightest/``
Note: user script only tested to work on Linux and Mac OS.

---
Expand Down
53 changes: 53 additions & 0 deletions flows_get_brightest/instruments.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

class Instrument(ABC):
"""Instrument class"""
field_hw: u.Quantity

@abstractmethod
def __init__(self, coords: SkyCoord, rotation: Optional[u.Quantity] = u.Quantity(0.0, u.deg)) -> None:
Expand Down Expand Up @@ -127,3 +128,55 @@ def _get_pa_sep(coord1: SkyCoord, coord2: SkyCoord) -> tuple[u.Quantity, u.Quant
if pa is None: raise ValueError('Position angle is None')
sep = coord1.separation(coord2)
return pa, sep

class FixedSizeInstrument(Instrument):
"""Generic Single field instrument with 7.5 arcminute field for making a finder chart."""
field_hw = u.Quantity(7.5, u.arcminute)

def __init__(self, coords: SkyCoord, rotation: numeric = u.Quantity(0.0, u.deg)):
# Coordinates
self.rotation: u.Quantity = rotation << u.deg # type: ignore
self.coords = coords # initial (target) coord
self.center_coords = self.default_point() # Hawki center coord
self.field_region = self.get_regions()

def point(self, target: Target, plan: Plan) -> list[regions.RectangleSkyRegion]:
"""point telescope to rot=rotation in degrees, alpha and delta offset in arcseconds"""
self.coords = target.coords # Assume unchanged
if plan.rotate:
self.rotation = u.Quantity(plan.rotation, u.deg)

self.center_coords = self.default_point(plan.alpha, plan.delta)
self.field_region = self.get_regions()
return [self.field_region]

def offset(self, shifta: Optional[numeric] = 0.0, shiftd: Optional[numeric] = 0.0) -> SkyCoord:
shifta = shifta << u.arcsecond # type: ignore
shiftd = shiftd << u.arcsecond # type: ignore
return self.coords.spherical_offsets_by(shifta, shiftd)

def get_regions(self) -> regions.RectangleSkyRegion:
return self.make_region(self.center_coords, self.field_hw, self.field_hw, self.rotation)

@property
def region_names(self) -> list[str]:
return ['field']

@property
def nregions(self) -> int:
return 1

def get_corners(self) -> list[Corner]:
fieldra, fielddec = self.center_coords.ra, self.center_coords.dec
quants = [fieldra, fielddec]
if not is_quantity(quants):
raise ValueError(f'center_coords must be SkyCoord with Quantity ra and dec.')
field_corners = Corner(quants[0], quants[1], self.field_hw)
return [field_corners]

@staticmethod
def make_region(coords: SkyCoord, width: u.Quantity, height: u.Quantity, angle: u.Quantity) -> regions.RectangleSkyRegion:
return regions.RectangleSkyRegion(coords, width=width, height=height, angle=angle)

def default_point(self, alpha: u.Quantity = u.Quantity(0.0, u.arcsec), delta: u.Quantity = u.Quantity(0.0, u.arcsec)) -> SkyCoord:
return self.offset(alpha, delta)
2 changes: 1 addition & 1 deletion flows_get_brightest/observer.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,5 +107,5 @@ def get_flows_observer(rot: numeric, tid: Union[int, str], shifta: numeric, shif
# Create Observer
target = Target(tid, target_info['ra'], target_info['decl'], target_info['skycoord'], target_info)
plan = Plan.from_numeric(rot, shifta, shiftd)
hawki = Hawki(target.coords)
hawki = instrument(target.coords)
return Observer(hawki, target, plan)
31 changes: 31 additions & 0 deletions flows_get_brightest/parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import argparse
import astropy.units as u
from .instruments import Hawki, FixedSizeInstrument, Instrument

def parse() -> tuple[float, int | str, float, float, bool, type[Instrument]]:
"""Parse command line input to get target, position angle (rotate), alpha and delta offsets (shifta, shiftd)
"""
parser = argparse.ArgumentParser(description='Calculate Brightest Star')
parser.add_argument('-t', '--target', help="calculate for this targetname or targetid", type=str, default='None',
action='store')
parser.add_argument('-r', '--rotate', help='rotation angle in degrees', type=float, default=0.0, action='store')
parser.add_argument('-a', '--shifta', help='shift alpha in arcsec', type=float, default=0.0, action='store')
parser.add_argument('-d', '--shiftd', help='shift delta in arcsec', type=float, default=0.0, action='store')
parser.add_argument('-p', '--plot', help='whether to query images and plot', action='store_true')
parser.add_argument('-i', '--instrument', help='instrument name', choices=['Hawki', 'FixedSize'], type=str, default='Hawki', action='store')
parser.add_argument('--size', help='Instrument FoV in arcmin (if using FixedSize instrument), finder chart will be roughly twice the size.', type=float, default=7.5, action='store')

args = parser.parse_args()
if args.target == 'None':
parser.error('target id or name not provided, use -t <targetid> or <targetname>')
elif args.target.isnumeric():
args.target = int(args.target)

if args.instrument not in ['Hawki', 'FixedSize']:
raise ValueError(f'Instrument {args.instrument} not supported, use Hawki or FixedSize')
instrument = Hawki
if args.instrument == 'FixedSize':
instrument = FixedSizeInstrument
instrument.field_hw = args.size << u.arcmin

return args.rotate, args.target, args.shifta, args.shiftd, args.plot, instrument
15 changes: 9 additions & 6 deletions flows_get_brightest/plots.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
from dataclasses import dataclass
import astropy.visualization as viz
import matplotlib
from matplotlib.markers import MarkerStyle
#from matplotlib.markers import MarkerStyle
from matplotlib.axes import Axes
import matplotlib.pyplot as plt
import numpy as np
from astropy.visualization import ZScaleInterval
Expand Down Expand Up @@ -36,12 +37,12 @@ class Plotter:
def __init__(self, obs: Observer):
self.obs = obs

def plot(self):
def plot(self) -> None:
"""not implemented yet"""
pass

def make_finding_chart(self, plot_refcat: bool = True, plot_simbad: bool = True,
savefig: bool = True, radius: numeric = 14) -> matplotlib.axes.Axes:
savefig: bool = True, radius: numeric = 14) -> Axes:
"""Make finding chart for a given observation."""
obs = self.obs
image = obs.get_image(radius=radius)
Expand Down Expand Up @@ -166,10 +167,12 @@ def plot_image(image, ax=None, scale='log', cmap=None, origin='lower', xlabel=No
stretch = viz.SinhStretch()
elif scale == 'squared':
stretch = viz.SquaredStretch()
else:
stretch = viz.LinearStretch()

# Create ImageNormalize object. Very important to use clip=False here, otherwise
# NaN points will not be plotted correctly.
norm = viz.ImageNormalize(data=image, interval=interval, vmin=vmin, vmax=vmax, stretch=stretch, clip=False)
norm = viz.ImageNormalize(data=image, interval=interval, vmin=vmin, vmax=vmax, stretch=stretch, clip=False) # type: ignore

elif isinstance(scale, (viz.ImageNormalize, matplotlib.colors.Normalize)):
norm = scale
Expand All @@ -188,9 +191,9 @@ def plot_image(image, ax=None, scale='log', cmap=None, origin='lower', xlabel=No
# Set up the colormap to use. If a bad color is defined,
# add it to the colormap:
if cmap is None:
cmap = copy.copy(plt.get_cmap('Blues'))
cmap = copy.copy(plt.get_cmap('Blues')) # type: ignore
elif isinstance(cmap, str):
cmap = copy.copy(plt.get_cmap(cmap))
cmap = copy.copy(plt.get_cmap(cmap)) # type: ignore

if color_bad:
cmap.set_bad(color_bad, 1.0)
Expand Down
11 changes: 6 additions & 5 deletions flows_get_brightest/run_get_brightest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,27 @@
from .plots import Plotter
from .auth import test_connection
from .observer import get_flows_observer
from .utils import parse
from .parser import parse

# Most useless warnings ever spammed for every operation by this package!
warnings.filterwarnings('ignore', category=ErfaWarning, append=True)
warnings.filterwarnings('ignore', message='invalid value', category=RuntimeWarning, append=True)


def main():
# Parse input
rot, tid, shifta, shiftd, make_fc = parse()
rot, tid, shifta, shiftd, make_fc, inst = parse()

# Test connection to flows:
test_connection()

# Print brightest star in field
obs = get_flows_observer(rot, tid, shifta, shiftd)
# Print brightest star in (first) field
obs = get_flows_observer(rot, tid, shifta, shiftd, inst)
obs.check_bright_stars(region=obs.regions[0])

# Make finding chart if requested
if make_fc:
Plotter(obs).make_finding_chart(radius=14)
Plotter(obs).make_finding_chart(radius=inst.field_hw.value*2)


if __name__ == '__main__':
Expand Down
52 changes: 44 additions & 8 deletions flows_get_brightest/tests.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import sys
import pytest
import astropy.units as u
import tendrils
import numpy as np
from .observer import get_flows_observer, Observer
from .auth import test_connection
from .plots import Plotter
from .utils import api_token
from .instruments import Hawki, Instrument, FixedSizeInstrument

rot, tid, shifta, shiftd = 30, 8, 10, 10
token = api_token()
Expand All @@ -19,31 +21,41 @@ def test_auth(monkeypatch):
monkeypatch.setattr('builtins.input', lambda _: 'bad_token')
test_connection()

@pytest.fixture
def fixedinstrument_obs() -> Observer:
return get_flows_observer(rot, tid, shifta, shiftd, instrument=FixedSizeInstrument)

@pytest.fixture
def observer():
return get_flows_observer(rot, tid, shifta, shiftd)
def Hawki_obs() -> Observer:
return get_flows_observer(rot, tid, shifta, shiftd, instrument=Hawki)

@pytest.fixture
def observer(request) -> Observer:
return request.getfixturevalue(request.param)

@pytest.mark.parametrize('observer', ['fixedinstrument_obs', 'Hawki_obs'], indirect=True)
def test_get_brightest(capsys, observer):
observer.check_bright_stars(region=observer.regions[0])
captured = capsys.readouterr()
assert "Brightest star has" in captured.out
sys.stdout.write(captured.out)
sys.stderr.write(captured.err)


@pytest.mark.parametrize('observer', ['fixedinstrument_obs', 'Hawki_obs'], indirect=True)
def test_plan(observer):
assert observer.plan.rotation == rot * u.deg
assert observer.plan.alpha == shifta * u.arcsec
assert observer.plan.delta == shiftd * u.arcsec
assert observer.plan.rotation == rot * u.deg # type: ignore
assert observer.plan.alpha == shifta * u.arcsec # type: ignore
assert observer.plan.delta == shiftd * u.arcsec # type: ignore
assert observer.plan.shift is True
assert observer.plan.rotate is True


@pytest.mark.parametrize('observer', ['fixedinstrument_obs', 'Hawki_obs'], indirect=True)
def test_observer(observer):
isinstance(observer, Observer)


@pytest.mark.slow
@pytest.mark.parametrize('observer', ['fixedinstrument_obs', 'Hawki_obs'], indirect=True)
def test_make_finding_chart(observer, monkeypatch):
import matplotlib.pyplot as plt
monkeypatch.setattr(plt, 'show', lambda: None)
Expand All @@ -53,3 +65,27 @@ def test_make_finding_chart(observer, monkeypatch):
title = ax.get_title()
assert title.startswith(f"{observer.target.info['target_name']}")
assert title.endswith("FC")


## End to end test with Hawki.
ARGS0 = (0, 8, 0, 0, False, 12.2)
ARGS1 = (30, 8, 0, 0, False, 11.5)
ARGS2 = (30, 8, -50, 100, False, 12.3)

@pytest.mark.slow
@pytest.mark.parametrize("args", [ARGS0, ARGS1, ARGS2])
def test_end_to_end(args: tuple[int, int | str, int, int, bool, float]) -> None:
rot, tid, shifta, shiftd, make_fc , brightest = args

# Print brightest star in field
obs = get_flows_observer(rot, tid, shifta, shiftd)
stars = obs.check_bright_stars(region=obs.regions[0])

assert pytest.approx(np.round(stars.min(), 1)) == brightest


# Make finding chart if requested
if make_fc:
plotter = Plotter(obs)
assert plotter.obs == obs

17 changes: 0 additions & 17 deletions flows_get_brightest/tests/end_to_end.py

This file was deleted.

19 changes: 0 additions & 19 deletions flows_get_brightest/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,6 @@ def __init__(self):
super().__init__(u.Quantity(0, u.deg), u.Quantity(0, u.deg), frame='icrs')
MISSING_COORDS = MissingCoords()

def parse() -> tuple[float, int | str, float, float, bool]:
"""Parse command line input to get target, position angle (rotate), alpha and delta offsets (shifta, shiftd)
"""
parser = argparse.ArgumentParser(description='Calculate Brightest Star')
parser.add_argument('-t', '--target', help="calculate for this targetname or targetid", type=str, default='None',
action='store')
parser.add_argument('-r', '--rotate', help='rotation angle in degrees', type=float, default=0.0, action='store')
parser.add_argument('-a', '--shifta', help='shift alpha in arcsec', type=float, default=0.0, action='store')
parser.add_argument('-d', '--shiftd', help='shift delta in arcsec', type=float, default=0.0, action='store')
parser.add_argument('-p', '--plot', help='whether to query images and plot', action='store_true')

args = parser.parse_args()
if args.target == 'None':
parser.error('target id or name not provided, use -t <targetid> or <targetname>')
elif args.target.isnumeric():
args.target = int(args.target)
return args.rotate, args.target, args.shifta, args.shiftd, args.plot


def api_token() -> str:
"""Try to get the API token from environment variable or from tendrils config.ini"""
token = str(os.environ.get('FLOWS_API_TOKEN'))
Expand Down
26 changes: 25 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,30 @@ test = ["pytest >= 7.1.1"]

[tool.pytest.ini_options]
minversion = "7.1.1"
addopts = "-ra -q"
addopts = "-rA -q -p no:warnings"
testpaths = ["flows_get_brightest/tests.py"]
markers = ["slow: marks tests as slow (deselect with '-m \"not slow\"')"]

[tool.black]
line-length = 120
target-version = ["py310"]
preview = true
exclude = '''
(
/(
\.eggs # exclude a few common directories in the
| \.git # root of the project
| \.idea
| \.pytest_cache
| \.mypy_cache
| \.vscode
| \.venv
| Notebooks
| build
| dist
| docs
| README.md
| LICENSE
)/
)
'''