From c310b0c1a4cd04a0343faf2fb3c87f7c5b9af308 Mon Sep 17 00:00:00 2001 From: Joshua Pritchard Date: Mon, 12 Feb 2024 09:36:10 +1100 Subject: [PATCH] merged ContourCutout with Cutout class --- cutout/cli/cutout.py | 37 ++-- cutout/cutout.py | 396 ++++++++++------------------------------- cutout/services.py | 11 +- poetry.lock | 74 +++++++- pyproject.toml | 1 + tests/conftest.py | 33 +--- tests/test_apis.py | 7 +- tests/test_cutout.py | 133 +------------- tests/test_services.py | 44 ++--- 9 files changed, 222 insertions(+), 514 deletions(-) diff --git a/cutout/cli/cutout.py b/cutout/cli/cutout.py index fb19195..2b6e529 100755 --- a/cutout/cli/cutout.py +++ b/cutout/cli/cutout.py @@ -10,7 +10,7 @@ from astroutils.io import FITSException, get_surveys from astroutils.logger import setupLogger -from cutout import ContourCutout, CornerMarker, Cutout +from cutout import CornerMarker, Cutout SURVEYS = get_surveys() SURVEYS.set_index("survey", inplace=True) @@ -48,16 +48,11 @@ help="Survey data to use for contours.", ) @click.option( - "-l", - "--clabels", + "-Q", + "--query-simbad", is_flag=True, - help="Display contour level labels.", default=False, -) -@click.option( - "--pm/--no-pm", - default=False, - help="Trigger proper motion correction for nearby stars.", + help="Query SIMBAD and apply proper motion corrections.", ) @click.option( "-e", @@ -193,8 +188,7 @@ def main( size, contours, - clabels, - pm, + query_simbad, epoch, fieldname, sbid, @@ -281,9 +275,7 @@ def main( size *= u.deg try: - CutoutClass = Cutout if not contours else ContourCutout - - cutout = CutoutClass( + cutout = Cutout( survey, position, size=size, @@ -296,7 +288,6 @@ def main( cmap=cmap, selavy=selavy, neighbours=neighbours, - contours=contours, band=band, vmin=vmin, vmax=vmax, @@ -304,10 +295,17 @@ def main( compact=True, ) - cutout.plot(clabels=clabels) + cutout.plot() - if pm: - cutout.correct_proper_motion() + if query_simbad: + simbad, newpos = cutout.query_simbad() + logger.info(f"SIMBAD results:\n {simbad.head()}") + cutout.add_markers( + newpos, + color="dodgerblue", + marker="x", + s=100, + ) if corner: span = len(cutout.data) / 4 @@ -321,6 +319,9 @@ def main( ) cutout.add_cornermarker(corner) + if contours: + cutout.add_contours(contours, position) + if psf: cutout.add_psf() diff --git a/cutout/cutout.py b/cutout/cutout.py index e4826f6..06d6ae2 100755 --- a/cutout/cutout.py +++ b/cutout/cutout.py @@ -13,14 +13,14 @@ import matplotlib.patheffects as pe import matplotlib.pyplot as plt import numpy as np -from astropy.coordinates import Distance, SkyCoord +from astropy.coordinates import SkyCoord from astropy.io import fits from astropy.time import Time from astropy.visualization import ImageNormalize, ZScaleInterval from astropy.wcs import WCS, FITSFixedWarning from astropy.wcs.utils import proj_plane_pixel_scales -from astroquery.simbad import Simbad from astroutils.io import FITSException, get_surveys +from astroutils.query import query_simbad from matplotlib.lines import Line2D from matplotlib.offsetbox import AnchoredOffsetbox, AnchoredText, AuxTransformBox from matplotlib.patches import Ellipse @@ -45,70 +45,10 @@ SURVEYS = get_surveys() SURVEYS.set_index("survey", inplace=True) -Simbad.add_votable_fields( - "otype", - "ra(d)", - "dec(d)", - "parallax", - "pmdec", - "pmra", - "distance", - "sptype", - "distance_result", -) logger = logging.getLogger(__name__) -def get_simbad(position): - simbad = Simbad.query_region(position, radius=180 * u.arcsec) - - # Catch SIMBAD failure either from None return of query or no stellar type matches in region - try: - simbad = simbad.to_pandas() - pm_types = ["*", "**", "PM*", "EB*", "Star", "PSR", "Pulsar", "Flare*"] - simbad = simbad[ - (simbad["OTYPE"].isin(pm_types)) | (simbad["SP_TYPE"].str.len() > 0) - ] - - assert len(simbad) > 0 - - except (ValueError, AssertionError): - return - - # Treat non-existent proper motion parameters as extremely distant objects - simbad["PMRA"].fillna(0, inplace=True) - simbad["PMDEC"].fillna(0, inplace=True) - simbad["PLX_VALUE"].fillna(0.01, inplace=True) - - pmra = simbad["PMRA"].values * u.mas / u.yr - pmdec = simbad["PMDEC"].values * u.mas / u.yr - - dist = Distance(parallax=simbad["PLX_VALUE"].values * u.mas) - - simbad["j2000pos"] = SkyCoord( - ra=simbad["RA_d"].values * u.deg, - dec=simbad["DEC_d"].values * u.deg, - frame="icrs", - distance=dist, - pm_ra_cosdec=pmra, - pm_dec=pmdec, - obstime="J2000", - ) - - simbad_cols = { - "MAIN_ID": "Object", - "OTYPE": "Type", - "SP_TYPE": "Spectral Type", - "DISTANCE_RESULT": "Separation (arcsec)", - "j2000pos": "j2000pos", - } - simbad = simbad.rename(columns=simbad_cols) - simbad = simbad[simbad_cols.values()].copy() - - return simbad - - @dataclass class CornerMarker: position: SkyCoord @@ -163,10 +103,7 @@ def decline(self): def in_pixel_range(self, pixmin: int, pixmax: int) -> bool: """Check whether the pixel coordinate of marker is in a valid range.""" - if any(i < pixmin or i > pixmax or np.isnan(i) for i in self.datapos): - return False - - return True + return self.wcs.footprint_contains(self.position) class Cutout: @@ -193,7 +130,7 @@ def __init__(self, survey, position, size, stokes="i", tiletype="TILES", **kwarg try: self._get_cutout() self._determine_epoch() - except FileNotFoundError as e: + except (AssertionError, FITSException, FileNotFoundError) as e: msg = f"{survey} failed: {e}" raise FITSException(msg) @@ -286,14 +223,14 @@ def _determine_epoch(self): else: msg = f"Could not detect {self.survey} epoch, PM correction disabled." logger.warning(msg) - self.mjd = None + self.obstime = None return else: epochtype = "mjd" if epoch > 3000 else "decimalyear" - self.mjd = Time(epoch, format=epochtype).mjd + self.obstime = Time(epoch, format=epochtype) def _plot_setup(self, fig, ax): """Create figure and determine normalisation parameters.""" @@ -319,7 +256,7 @@ def _plot_setup(self, fig, ax): self.set_ylabel("Dec (J2000)") # Set compact or extended label / tick configuration - if self.options.get("compact", False): + if self.options.get("compact", True): tickcolor = ( "k" if np.nanmax(np.abs(self.data)) == np.nanmax(self.data) else "gray" ) @@ -335,6 +272,12 @@ def _plot_setup(self, fig, ax): self.ax.tick_params(axis="both", direction="in", length=5, color=tickcolor) + # Change ticklabels to arcsecond offsets in offset frame + if self.offsets: + self.ax.coords[0].set_coord_type("longitude", coord_wrap=180 * u.deg) + self.ax.coords[0].set_major_formatter("s") + self.ax.coords[1].set_major_formatter("s") + # Set colourmap normalisation self.norm = self._get_cmap_normalisation() self.cmap_label = r"Flux Density (mJy beam$^{-1}$)" if self.is_radio else "" @@ -427,19 +370,24 @@ def add_annotation(self, annotation, location="upper left", **kwargs): self.ax.add_artist(text) - def add_contours(self, survey, pos, shift_epoch=None, **kwargs): + def add_contours(self, survey, position, shift_epoch=None, **kwargs): stokes = kwargs.get("stokes", "i") - colors = kwargs.get("colors", "rebeccapurple") + colors = kwargs.get("colors", "darkorange") label = kwargs.get("contourlabel", survey) - contour_cutout = ContourCutout( + contour_cutout = Cutout( survey, self.position, size=self.size, stokes=stokes, - contours=survey, ) + # If using offset frame, align contours to data + if self.offsets: + r_crpix = contour_cutout.wcs.wcs_world2pix(self.ra, self.dec, 1) + contour_cutout.wcs.wcs.crpix = [r_crpix[0], r_crpix[1]] + contour_cutout.wcs.wcs.crval = [0, 0] + datamax = np.nanmax(contour_cutout.data) perc_levels = np.array([0.3, 0.6, 0.9]) * datamax levels = kwargs.get("levels", perc_levels) @@ -450,9 +398,9 @@ def add_contours(self, survey, pos, shift_epoch=None, **kwargs): ) if shift_epoch: - contour_cutout.shift_coordinate_grid(pos, shift_epoch) + contour_cutout.shift_coordinate_grid(position, shift_epoch) - self.ax.contour( + cs = self.ax.contour( contour_cutout.data, colors=colors, linewidths=self.options.get("contourwidth", 3), @@ -464,7 +412,8 @@ def add_contours(self, survey, pos, shift_epoch=None, **kwargs): self.cs_dict[label] = Line2D([], [], color=colors) def add_cornermarker(self, marker): - if not marker.in_pixel_range(0, len(self.data)): + in_pixel_range = self.wcs.footprint_contains(marker.position) + if not in_pixel_range: msga = ( "Cornermarker will be disabled as RA and Dec are outside of data range." ) @@ -475,17 +424,28 @@ def add_cornermarker(self, marker): self.ax.add_artist(marker.raline) self.ax.add_artist(marker.decline) + def add_markers(self, coords, **kwargs): + in_pixel_range = self.wcs.footprint_contains(coords) + coords = coords[in_pixel_range] + + self.ax.scatter( + coords.ra, + coords.dec, + transform=self.ax.get_transform("world"), + **kwargs, + ) + def add_source_ellipse(self): """Overplot dashed line ellipses for the nearest source within positional uncertainty.""" # Add ellipse for source within positional uncertainty if self.plot_source: source_colour = "k" if self.stokes == "v" else "springgreen" - pos = SkyCoord( + position = SkyCoord( ra=self.source.ra_deg_cont, dec=self.source.dec_deg_cont, unit="deg" ) self.sourcepos = EllipseSkyRegion( - pos, + position, width=self.source.maj_axis * u.arcsec, height=self.source.min_axis * u.arcsec, angle=(self.source.pos_ang + 90) * u.deg, @@ -503,13 +463,13 @@ def add_source_ellipse(self): if self.plot_neighbours: neighbour_colour = "k" if self.stokes == "v" else "rebeccapurple" for _, neighbour in self.neighbours.iterrows(): - pos = SkyCoord( + position = SkyCoord( ra=neighbour.ra_deg_cont, dec=neighbour.dec_deg_cont, unit="deg", ) n = EllipseSkyRegion( - pos, + position, width=neighbour.maj_axis * u.arcsec, height=neighbour.min_axis * u.arcsec, angle=(neighbour.pos_ang + 90) * u.deg, @@ -618,6 +578,41 @@ def switch_to_offsets(self): self.offsets = True + def shift_coordinate_grid(self, pm_coord, shift_epoch): + """Shift WCS of pixel data to epoch based upon the proper motion encoded in pm_coord.""" + + # Replace pixel data / WCS with copy centred on source + contour_background = Cutout( + self.survey, + pm_coord, + self.size, + band=self.band, + ) + self.data = contour_background.data + self.wcs = contour_background.wcs + + # Astropy for some reason can't decide on calling this pm_ra or pm_ra_cosdec + try: + pm_ra = pm_coord.pm_ra + except AttributeError: + pm_ra = pm_coord.pm_ra_cosdec + + # Update CRVAL coordinates based on propagated proper motion + orig_pos = SkyCoord( + ra=self.wcs.wcs.crval[0] * u.deg, + dec=self.wcs.wcs.crval[1] * u.deg, + frame="icrs", + distance=pm_coord.distance, + pm_ra_cosdec=pm_ra, + pm_dec=pm_coord.pm_dec, + obstime=pm_coord.obstime, + ) + newpos = orig_pos.apply_space_motion(shift_epoch) + + self.wcs.wcs.crval = [newpos.ra.deg, newpos.dec.deg] + + return + def hide_coords(self, axis="both", ticks=True, labels=True): """Remove all coordinates and identifying information.""" @@ -638,6 +633,26 @@ def hide_coords(self, axis="both", ticks=True, labels=True): if ticks: lat.set_ticks_visible(False) + def query_simbad(self, epoch: Time = None): + """Query SIMBAD and apply proper motion corrections.""" + + if epoch is None and self.obstime is None: + raise FITSException( + "Date could not be inferred from data header, supply with epoch keyword." + ) + + # If obstime not set directly, check that it was set from FITS headers in get_cutout method + obstime = self.obstime if epoch is None else epoch + radius = max(180 * u.arcsec, self.size) + + # Make simbad query + simbad, newpos = query_simbad(self.position, radius=radius, obstime=obstime) + + if simbad is None: + return + + return simbad, newpos + def plot(self, fig=None, ax=None, **kwargs): """Plot survey data and position overlay.""" @@ -680,222 +695,3 @@ def set_ylabel(self, ylabel, align=False): self._align_ylabel(ylabel) else: self.ax.set_ylabel(ylabel) - - -class ContourCutout(Cutout): - def __init__(self, survey, position, size, **kwargs): - # If custom data provided for ContourCutout, pop from kwargs - # to avoid being read as radio data by Cutout sub-call. - data = kwargs.pop("data", None) - stokes = kwargs.pop("stokes", "i") - - # Other ContourCutout specific keywords are also popped - self.contours = kwargs.pop("contours", "racs-low") - self.clabels = kwargs.pop("clabels", False) - bar = kwargs.pop("bar", False) - - self.radio = Cutout(self.contours, position, size, bar=bar, **kwargs) - - super().__init__(survey, position, size, data=data, stokes=stokes, **kwargs) - - def add_pm_location(self): - """Overplot proper motion correction as an arrow.""" - - if not self.correct_pm: - raise FITSException("Must run correct_proper_motion method first.") - - name = self.simbad.iloc[0]["Object"] - oldcoord = SkyCoord(self.oldpos.ra, self.oldpos.dec, unit=u.deg) - newcoord = SkyCoord(self.pm_coord.ra, self.pm_coord.dec, unit=u.deg) - oldtime = Time(self.mjd, format="mjd").decimalyear - newtime = Time(self.radio.mjd, format="mjd").decimalyear - handles, labels = [], [] - - logger.warning(oldcoord) - logger.warning(newcoord) - logger.warning(oldcoord.separation(newcoord).arcsec) - - if oldcoord.separation(newcoord).arcsec < 1: - self.ax.scatter( - self.pm_coord.ra, - self.pm_coord.dec, - marker="x", - s=200, - color="r", - transform=self.ax.get_transform("world"), - label=f"{name} position at J{newtime:.2f}", - ) - self.ax.scatter( - self.oldpos.ra, - self.oldpos.dec, - marker="x", - s=200, - color="b", - transform=self.ax.get_transform("world"), - label=f"{name} position at J{oldtime:.2f}", - ) - self.ax.legend() - else: - dra, ddec = oldcoord.spherical_offsets_to(newcoord) - self.ax.arrow( - self.oldpos.ra.deg, - self.oldpos.dec.deg, - dra.deg, - ddec.deg, - width=8e-5, - color="r", - length_includes_head=True, - zorder=10, - transform=self.ax.get_transform("world"), - ) - - arrow_handle = Line2D( - [], - [], - ls="none", - marker=r"$\leftarrow$", - markersize=10, - color="r", - ) - arrow_label = f"Proper motion from J{oldtime:.2f}-J{newtime:.2f}" - handles.append(arrow_handle) - labels.append(arrow_label) - - self.ax.legend(handles, labels) - - def shift_coordinate_grid(self, pm_coord, shift_epoch): - """Shift WCS of pixel data to epoch based upon the proper motion encoded in pm_coord.""" - - # Replace pixel data / WCS with copy centred on source - contour_background = Cutout( - self.survey, - pm_coord, - self.size, - band=self.band, - ) - self.data = contour_background.data - self.wcs = contour_background.wcs - - # Astropy for some reason can't decide on calling this pm_ra or pm_ra_cosdec - try: - pm_ra = pm_coord.pm_ra - except AttributeError as e: - pm_ra = pm_coord.pm_ra_cosdec - - # Update CRVAL coordinates based on propagated proper motion - orig_pos = SkyCoord( - ra=self.wcs.wcs.crval[0] * u.deg, - dec=self.wcs.wcs.crval[1] * u.deg, - frame="icrs", - distance=pm_coord.distance, - pm_ra_cosdec=pm_ra, - pm_dec=pm_coord.pm_dec, - obstime=pm_coord.obstime, - ) - newpos = orig_pos.apply_space_motion(shift_epoch) - - self.wcs.wcs.crval = [newpos.ra.deg, newpos.dec.deg] - - return - - def correct_proper_motion(self, mjd=None): - """Check SIMBAD for nearby star or pulsar and plot a cross at corrected coordinates.""" - - msg = ( - "Date could not be inferred from {} data header, supply with epoch keyword." - ) - if self.radio.mjd is None: - raise FITSException(msg.format("radio")) - - if mjd is None and self.mjd is None: - raise FITSException(msg.format("contour")) - - # If mjd not set directly, check that it was set from FITS headers in get_cutout method - mjd = self.mjd if mjd is None else mjd - obstime = Time(mjd, format="mjd") - newtime = Time(self.radio.mjd, format="mjd") - - # Make simbad query - simbad = get_simbad(self.position) - - # Check if query returned any stellar matches within range - if simbad is None: - logger.debug("No high proper-motion objects within 180 arcsec.") - return - - # Calculate proper motion corrected position and separation - datapos = simbad.j2000pos.apply(lambda x: x.apply_space_motion(obstime)) - newpos = simbad.j2000pos.apply(lambda x: x.apply_space_motion(newtime)) - simbad["PM Corrected Separation (arcsec)"] = np.round( - newpos.apply(lambda x: x.separation(self.position).arcsec), - 3, - ) - - # Only display PM results if object within 15 arcsec - if simbad["PM Corrected Separation (arcsec)"].min() > 15: - logger.debug("No PM corrected objects within 15 arcsec") - - return - - logger.warning(simbad["PM Corrected Separation (arcsec)"].min()) - - self.simbad = simbad.sort_values("PM Corrected Separation (arcsec)") - logger.info(f"SIMBAD results:\n {self.simbad.head()}") - - nearest = self.simbad["PM Corrected Separation (arcsec)"].idxmin() - - self.oldpos = datapos[nearest] - self.pm_coord = newpos[nearest] - - near_object = self.simbad.loc[nearest].Object - msg = f"{near_object} proper motion corrected to <{self.pm_coord.ra:.4f}, {self.pm_coord.dec:.4f}>" - logger.info(msg) - - self.correct_pm = True - - return - - def plot(self, fig=None, ax=None, **kwargs): - self._plot_setup(fig, ax) - - self.im = self.ax.imshow(self.data, cmap=self.cmap, norm=self.norm) - - # Plot radio contours - self.radio.data *= self.sign - self.peak = np.nanmax(self.radio.data) - self.radiorms = np.sqrt(np.mean(np.square(self.radio.data))) - - if kwargs.get("rmslevels"): - self.levels = [self.radiorms * x for x in [3, 6]] - elif kwargs.get("peaklevels"): - midx = int(self.radio.data.shape[0] / 2) - midy = int(self.radio.data.shape[1] / 2) - peak = self.radio.data[midx, midy] - self.levels = np.logspace(np.log10(0.3 * peak), np.log10(0.9 * peak), 3) - else: - self.levels = [self.peak * x for x in [0.3, 0.6, 0.9]] - - contour_label = kwargs.get("contourlabel", self.contours) - contour_width = kwargs.get("contourwidth", 3) - contour_color = "k" if self.cmap == "coolwarm" else "orange" - - cs = self.ax.contour( - self.radio.data, - transform=self.ax.get_transform(self.radio.wcs), - levels=self.levels, - colors=contour_color, - linewidths=contour_width, - ) - - # Contour artist is placed inside self.cs_dict for external label / legend access - self.cs_dict[contour_label] = Line2D([], [], color=contour_color) - - if self.clabels: - self.ax.clabel(cs, fontsize=10, fmt="%1.1f mJy") - - if self.bar: - self.fig.colorbar( - self.im, - label=self.cmap_label, - ax=self.ax, - ) diff --git a/cutout/services.py b/cutout/services.py index 14e7189..dd4bafe 100644 --- a/cutout/services.py +++ b/cutout/services.py @@ -121,9 +121,14 @@ def _make_cutout(self, cutout): with fits.open(self.filepath) as hdul: header, data = hdul[self.hdulindex].header, hdul[self.hdulindex].data wcs = WCS(header, naxis=2) - except FileNotFoundError: - logger.error(f"Image path {self.filepath} does not exist.") - raise + except (OSError, FileNotFoundError): + logger.error(f"Image path {self.filepath} does not exist or corrupted.") + + # Clear corrupted file with a guard to ensure filepath is a FITS file + if Path(self.filepath).suffix == ".fits": + os.system(f"rm {self.filepath}") + + raise FITSException # If present, remove redundant polarisation/frequency axes from data cube if data.ndim == 4: diff --git a/poetry.lock b/poetry.lock index c7b4c7f..338d547 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "astropy" @@ -104,10 +104,10 @@ develop = false [package.dependencies] astropy = "^6.0.0" click = "^8.0.4" -colorlog = "^6.6.0" +colorlog = "^6.8.2" dask = {version = "^2023.5.1", extras = ["distributed"]} forced-phot = {git = "https://github.com/askap-vast/forced_phot.git"} -matplotlib = "^3.5.1" +matplotlib = "^3.8.2" numpy = "^1.22.2" pandas = "^2.1.4" pyarrow = "^14.0.1" @@ -116,7 +116,7 @@ pyarrow = "^14.0.1" type = "git" url = "https://github.com/joshoewahp/astroutils.git" reference = "HEAD" -resolved_reference = "8ef102651115d2a4c93f58a8e9367b73a71f67bf" +resolved_reference = "82baea59d0087982cffce35ede03574bd57726d4" [[package]] name = "beautifulsoup4" @@ -403,13 +403,13 @@ files = [ [[package]] name = "colorlog" -version = "6.8.0" +version = "6.8.2" description = "Add colours to the output of Python's logging module." optional = false python-versions = ">=3.6" files = [ - {file = "colorlog-6.8.0-py3-none-any.whl", hash = "sha256:4ed23b05a1154294ac99f511fabe8c1d6d4364ec1f7fc989c7fb515ccc29d375"}, - {file = "colorlog-6.8.0.tar.gz", hash = "sha256:fbb6fdf9d5685f2517f388fb29bb27d54e8654dd31f58bc2a3b217e967a95ca6"}, + {file = "colorlog-6.8.2-py3-none-any.whl", hash = "sha256:4dcbb62368e2800cb3c5abd348da7e53f6c362dda502ec27c560b2e58a66bd33"}, + {file = "colorlog-6.8.2.tar.gz", hash = "sha256:3e3e079a41feb5a1b64f978b5ea4f46040a94f11f0e8bbb8261e3dbbeca64d44"}, ] [package.dependencies] @@ -1141,6 +1141,16 @@ files = [ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, @@ -1796,6 +1806,53 @@ files = [ [package.extras] diagrams = ["jinja2", "railroad-diagrams"] +[[package]] +name = "pyqt5" +version = "5.14.0" +description = "Python bindings for the Qt cross platform application toolkit" +optional = false +python-versions = ">=3.5" +files = [ + {file = "PyQt5-5.14.0-5.14.0-cp35.cp36.cp37.cp38-abi3-macosx_10_6_intel.whl", hash = "sha256:895d4101f7f8c82bc728d7eb9da1c756955ce27a0c945eafe7f234dd03402853"}, + {file = "PyQt5-5.14.0-5.14.0-cp35.cp36.cp37.cp38-abi3-manylinux1_x86_64.whl", hash = "sha256:a757ba71c51f428b52ba404e781e2f19b4436b2c31298b8313339d5817781b65"}, + {file = "PyQt5-5.14.0-5.14.0-cp35.cp36.cp37.cp38-none-win32.whl", hash = "sha256:cc3529c0f7cbbe7491073458d5d15e7518ce544ad8c627f485e5db8a27fcaf61"}, + {file = "PyQt5-5.14.0-5.14.0-cp35.cp36.cp37.cp38-none-win_amd64.whl", hash = "sha256:0dcc128b72f83cce0fc7926c83f05a9b74b652b5eb31a4ab71693ac8829e73c8"}, + {file = "PyQt5-5.14.0.tar.gz", hash = "sha256:0145a6b7de15756366decb736c349a0cb510d706c83fda5b8cd9e0557bc1da72"}, +] + +[package.dependencies] +PyQt5-sip = ">=12.7,<13" + +[[package]] +name = "pyqt5-sip" +version = "12.13.0" +description = "The sip module support for PyQt5" +optional = false +python-versions = ">=3.7" +files = [ + {file = "PyQt5_sip-12.13.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a7e3623b2c743753625c4650ec7696362a37fb36433b61824cf257f6d3d43cca"}, + {file = "PyQt5_sip-12.13.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6e4ac714252370ca037c7d609da92388057165edd4f94e63354f6d65c3ed9d53"}, + {file = "PyQt5_sip-12.13.0-cp310-cp310-win32.whl", hash = "sha256:d5032da3fff62da055104926ffe76fd6044c1221f8ad35bb60804bcb422fe866"}, + {file = "PyQt5_sip-12.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:9a8cdd6cb66adcbe5c941723ed1544eba05cf19b6c961851b58ccdae1c894afb"}, + {file = "PyQt5_sip-12.13.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0f85fb633a522f04e48008de49dce1ff1d947011b48885b8428838973fbca412"}, + {file = "PyQt5_sip-12.13.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ec60162e034c42fb99859206d62b83b74f987d58937b3a82bdc07b5c3d190dec"}, + {file = "PyQt5_sip-12.13.0-cp311-cp311-win32.whl", hash = "sha256:205cd449d08a2b024a468fb6100cd7ed03e946b4f49706f508944006f955ae1a"}, + {file = "PyQt5_sip-12.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:1c8371682f77852256f1f2d38c41e2e684029f43330f0635870895ab01c02f6c"}, + {file = "PyQt5_sip-12.13.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7fe3375b508c5bc657d73b9896bba8a768791f1f426c68053311b046bcebdddf"}, + {file = "PyQt5_sip-12.13.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:773731b1b5ab1a7cf5621249f2379c95e3d2905e9bd96ff3611b119586daa876"}, + {file = "PyQt5_sip-12.13.0-cp312-cp312-win32.whl", hash = "sha256:fb4a5271fa3f6bc2feb303269a837a95a6d8dd16be553aa40e530de7fb81bfdf"}, + {file = "PyQt5_sip-12.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:3a4498f3b1b15f43f5d12963accdce0fd652b0bcaae6baf8008663365827444c"}, + {file = "PyQt5_sip-12.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9b984c2620a7a7eaf049221b09ae50a345317add2624c706c7d2e9e6632a9587"}, + {file = "PyQt5_sip-12.13.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:3188a06956aef86f604fb0d14421a110fad70d2a9e943dbacbfc3303f651dade"}, + {file = "PyQt5_sip-12.13.0-cp38-cp38-win32.whl", hash = "sha256:108a15f603e1886988c4b0d9d41cb74c9f9815bf05cefc843d559e8c298a10ce"}, + {file = "PyQt5_sip-12.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:db228cd737f5cbfc66a3c3e50042140cb80b30b52edc5756dbbaa2346ec73137"}, + {file = "PyQt5_sip-12.13.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5338773bbaedaa4f16a73c142fb23cc18c327be6c338813af70260b756c7bc92"}, + {file = "PyQt5_sip-12.13.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:29fa9cc964517c9fc3f94f072b9a2aeef4e7a2eda1879cb835d9e06971161cdf"}, + {file = "PyQt5_sip-12.13.0-cp39-cp39-win32.whl", hash = "sha256:96414c93f3d33963887cf562d50d88b955121fbfd73f937c8eca46643e77bf61"}, + {file = "PyQt5_sip-12.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:bbc7cd498bf19e0862097be1ad2243e824dea56726f00c11cff1b547c2d31d01"}, + {file = "PyQt5_sip-12.13.0.tar.gz", hash = "sha256:7f321daf84b9c9dbca61b80e1ef37bdaffc0e93312edae2cd7da25b953971d91"}, +] + [[package]] name = "pytest" version = "8.0.0" @@ -1932,6 +1989,7 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -2272,4 +2330,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "a775e956c9fe1e52f0b0534d9ce9e8c046a1b61a97b33cea5482e4528e98b8c6" +content-hash = "6aa258908f4672d132b1b1d4dec4e6a27cd5e75fd6cf22be57f84efce4fe10d6" diff --git a/pyproject.toml b/pyproject.toml index c54ed64..3bb3bff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ astroutils = {git = "https://github.com/joshoewahp/astroutils.git"} pandas = "^2.1.4" click = "^8.0.4" regions = "^0.7" +pyqt5 = "5.14" [tool.poetry.scripts] cutout = "cutout.cli.cutout:main" diff --git a/tests/conftest.py b/tests/conftest.py index 23128bc..da880ad 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,7 +9,7 @@ from astropy.time import Time from astropy.wcs import WCS -from cutout import ContourCutout, Cutout +from cutout import Cutout @pytest.fixture @@ -79,25 +79,6 @@ def cleanup_pyplot(): plt.close("all") -@pytest.fixture -def mock_simbad(position): - def _simbad(objects): - simbad = pd.DataFrame( - { - "Object": ["no_pm", "pm", "pm_offset"], - "j2000pos": [ - position("no_pm"), - position("pm"), - position("pm_offset"), - ], - } - ) - simbad = simbad[simbad.Object.isin(objects)] - return simbad - - return _simbad - - @pytest.fixture() def image_products(): with fits.open("tests/data/image.i.SB9602.cont.taylor.0.restored.fits") as hdul: @@ -136,7 +117,7 @@ def _data_path(coord): @pytest.fixture def cutout(position, data_path): - def _cutout(contours=None, coord="no_pm", options=None): + def _cutout(coord="no_pm", options=None): if not options: options = dict() @@ -144,19 +125,11 @@ def _cutout(contours=None, coord="no_pm", options=None): impath, selpath = data_path(coord) - if contours: - contours = impath - - CutoutClass = ContourCutout - else: - CutoutClass = Cutout - - c = CutoutClass( + c = Cutout( impath, pos, size=0.01 * u.deg, selavy=selpath, - contours=contours, **options, ) diff --git a/tests/test_apis.py b/tests/test_apis.py index f2edacb..22b31d8 100644 --- a/tests/test_apis.py +++ b/tests/test_apis.py @@ -4,7 +4,7 @@ import pytest from astropy.coordinates import SkyCoord -from cutout import ContourCutout +from cutout import Cutout cache_path = "tests/data/cache/" @@ -22,7 +22,7 @@ ("2massj", 12.68, -25.3, 0), ], ) -def test_api_contour_cutouts( +def test_api_cutouts( survey, ra, dec, @@ -41,9 +41,8 @@ def test_api_contour_cutouts( mocker.patch("cutout.services.cutout_cache", new=Path(cache_path)) mocker.patch("cutout.services.find_fields", return_value=mocked_fields[field_idx]) - ContourCutout( + Cutout( survey, position, size, - contours="gw1", ) diff --git a/tests/test_cutout.py b/tests/test_cutout.py index bf3a3cb..28ac8a7 100644 --- a/tests/test_cutout.py +++ b/tests/test_cutout.py @@ -10,7 +10,7 @@ from astroutils.io import FITSException from astroutils.logger import setupLogger -from cutout import CornerMarker, get_simbad +from cutout import CornerMarker logger = logging.getLogger() @@ -76,20 +76,6 @@ def test_cornermarker_line_parameters(ra, dec, image_products): assert marker.decline._yorig == [y + offset, y + span] -def test_get_simbad(position): - pos = position("no_pm") - simbad = get_simbad(pos) - - assert isinstance(simbad, pd.DataFrame) - - -def test_get_simbad_no_results(): - pos = SkyCoord(ra=191, dec=-80, unit="deg") - simbad = get_simbad(pos) - - assert simbad is None - - def test_getattr_overload(cutout): """Need to check as we overload __getattr__ to avoid a RecursionError.""" c = cutout() @@ -128,20 +114,6 @@ def test_cutout_plot_options(cutout, options): c.plot() -@pytest.mark.parametrize( - "options", - [ - {"rmslevels": True}, - {"peaklevels": True}, - {"clabels": True}, - {"bar": True}, - ], -) -def test_contourcutout_plot_options(cutout, options): - c = cutout(options=options, contours=True) - c.plot(**options) - - datekeys = [ ("MJD-OBS", 58712), ("MJD", 58712), @@ -249,17 +221,16 @@ def test_hide_coords(cutout, axis, ticks, labels): c.hide_coords(axis=axis, ticks=ticks, labels=labels) -@pytest.mark.parametrize("neighbours", [True, False]) @pytest.mark.parametrize("source", [True, False]) -@pytest.mark.parametrize("contours", [True, False]) +@pytest.mark.parametrize("neighbours", [True, False]) @pytest.mark.parametrize("pos_err", [1 * u.arcsec, 0 * u.arcsec]) -def test_switch_to_offsets(cutout, contours, source, neighbours, pos_err): +def test_switch_to_offsets(cutout, source, neighbours, pos_err): options = { "neighbours": neighbours, "source": source, "pos_err": pos_err, } - c = cutout(options=options, contours=contours) + c = cutout(options=options) c.plot() c.switch_to_offsets() @@ -289,102 +260,12 @@ def test_save(cutout, cleanup_cache): assert os.path.exists(f"{cache_path}/test.fits") -@pytest.mark.parametrize( - "mjd, coord", - [ - (None, "pm"), - (58000, "pm_offset"), - ], -) -def test_contourcutout_correct_proper_motion( - mjd, - coord, - cutout, - mock_simbad, - mocker, -): - simbad = mock_simbad([coord]) - mocker.patch("cutout.cutout.get_simbad", return_value=simbad) - - c = cutout(contours=True, coord="pm") - c.plot() - - c.correct_proper_motion(mjd=mjd) - - -def test_contourcutout_correct_proper_motion_no_contour_mjd(cutout): - c = cutout(contours=True) - c.mjd = None - - c.plot() - - with pytest.raises(FITSException): - c.correct_proper_motion() - - -def test_contourcutout_correct_proper_motion_no_radio_mjd(cutout): - c = cutout(contours=True) - c.radio.mjd = None - - c.plot() - - with pytest.raises(FITSException): - c.correct_proper_motion() - - -def test_contourcutout_correct_proper_motion_no_simbad( - cutout, - mocker, - caplog, -): - c = cutout(contours=True) - - mocker.patch("cutout.cutout.get_simbad", return_value=None) - - c.plot() - c.correct_proper_motion() - - assert "No high proper-motion objects" in caplog.text - - -@pytest.mark.parametrize("mjd", [None, 57000]) -def test_contourcutout_add_pm_location( - mjd, - cutout, - mock_simbad, - mocker, -): - c = cutout(contours=True) - - simbad = mock_simbad(["pm"]) - mocker.patch("cutout.cutout.get_simbad", return_value=simbad) - - c.plot() - c.correct_proper_motion(mjd) - c.add_pm_location() - - -def test_contourcutout_add_pm_location_bad_order_raises_error( - cutout, - mock_simbad, - mocker, -): - c = cutout(contours=True) - - simbad = mock_simbad(["pm"]) - mocker.patch("cutout.cutout.get_simbad", return_value=simbad) - - with pytest.raises(FITSException): - c.plot() - c.add_pm_location() - - @pytest.mark.parametrize( "shift_epoch", [Time(2020.5, format="decimalyear"), None], ) def test_add_contours(shift_epoch, cutout, position): - c = cutout(contours=True) + c = cutout() c.plot() c.add_contours( @@ -395,7 +276,7 @@ def test_add_contours(shift_epoch, cutout, position): def test_add_contours_bad_levels_raise_error(cutout, position): - c = cutout(contours=True) + c = cutout() c.plot() with pytest.raises(ValueError): @@ -407,7 +288,7 @@ def test_add_contours_bad_levels_raise_error(cutout, position): def test_shift_coordinate_grid(cutout, position): - c = cutout(contours=True) + c = cutout() c.plot() c.shift_coordinate_grid( diff --git a/tests/test_services.py b/tests/test_services.py index 76c19b3..7935a78 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -7,7 +7,7 @@ from astroutils.io import FITSException, get_surveys from astroutils.source import SelavyCatalogue -from cutout import ContourCutout, Cutout +from cutout import Cutout SURVEYS = get_surveys() @@ -246,21 +246,20 @@ def test_local_cutout_selavy_ellipses(components, params, mocker): @pytest.mark.filterwarnings("ignore::astropy.wcs.FITSFixedWarning") @pytest.mark.parametrize( - "survey, ra, dec, field_idx", + "survey, ra, dec", [ - ("skymapper", 12.68, -25.3, 0), - ("panstarrs", 12.68, -25.3, 0), - ("decam", 12.68, -25.3, 0), - ("iphas", 96.9, 11.5, 1), + ("skymapper", 12.68, -25.3), + ("panstarrs", 12.68, -25.3), + ("decam", 12.68, -25.3), + ("iphas", 96.9, 11.5), # 2MASS to test SkyView - ("2massj", 12.68, -25.3, 0), + ("2massj", 12.68, -25.3), ], ) -def test_api_contour_cutouts( +def test_api_cutouts( survey, ra, dec, - field_idx, mocker, cleanup_cache, ): @@ -272,30 +271,28 @@ def test_api_contour_cutouts( position = SkyCoord(ra=ra, dec=dec, unit="deg") mocker.patch("cutout.services.cutout_cache", new=Path(cache_path)) - mocker.patch("cutout.services.find_fields", return_value=mocked_fields[field_idx]) + # mocker.patch("cutout.services.find_fields", return_value=mocked_fields[field_idx]) - ContourCutout( + Cutout( survey, position, size, - contours="gw1", ) @pytest.mark.parametrize( - "survey, ra, dec, field_idx", + "survey, ra, dec", [ - ("skymapper", 12.68, 55.3, 0), - ("panstarrs", 12.68, 55.3, 0), - ("decam", 12.68, 55.3, 0), - ("iphas", 12.38, -25.3, 0), + ("skymapper", 12.68, 55.3), + ("panstarrs", 12.68, -55.3), + ("decam", 12.68, 55.3), + # ("iphas", 12.38, -25.3), ], ) -def test_api_contour_cutouts_out_of_zone( +def test_api_cutouts_out_of_zone( survey, ra, dec, - field_idx, mocker, cleanup_cache, ): @@ -307,14 +304,13 @@ def test_api_contour_cutouts_out_of_zone( position = SkyCoord(ra=ra, dec=dec, unit="deg") mocker.patch("cutout.services.cutout_cache", new=Path(cache_path)) - mocker.patch("cutout.services.find_fields", return_value=mocked_fields[field_idx]) + # mocker.patch("cutout.services.find_fields", return_value=mocked_fields[field_idx]) with pytest.raises(FITSException): - ContourCutout( + Cutout( survey, position, size, - contours="gw1", ) @@ -343,7 +339,6 @@ def test_bad_position_raises_error( survey, position, size=0.05 * u.deg, - contours="gw1", ) @@ -354,11 +349,10 @@ def test_skyview_invalid_survey_raises_error(mocker, cleanup_cache): mocker.patch("cutout.services.find_fields", return_value=mocked_fields[0]) with pytest.raises(FITSException): - ContourCutout( + Cutout( "2massq", position, size=0.05 * u.deg, - contours="gw1", )