From d001f955a514a472c3ce9f23d0d4c1d40d2e7d71 Mon Sep 17 00:00:00 2001 From: Tim-Oliver Husser Date: Wed, 20 Dec 2023 14:36:48 +0100 Subject: [PATCH 01/14] mypy --- pyobs/application.py | 6 +++--- pyobs/cli/pyobs.py | 13 ++++++++----- pyobs/cli/pyobsw.py | 2 +- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/pyobs/application.py b/pyobs/application.py index 3a6eb09e..408ecff4 100644 --- a/pyobs/application.py +++ b/pyobs/application.py @@ -6,7 +6,7 @@ import threading from io import StringIO from logging.handlers import TimedRotatingFileHandler -from typing import Optional, Any, Dict +from typing import Optional, Any, Dict, List import yaml from pyobs.object import get_object, get_class_from_string @@ -35,7 +35,7 @@ def __init__(self, config: str, log_file: Optional[str] = None, log_level: str = # formatter for logging, and list of logging handlers formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(filename)s:%(lineno)d %(message)s") - handlers = [] + handlers: List[logging.Handler] = [] # create stdout logging handler stream_handler = logging.StreamHandler() @@ -110,7 +110,7 @@ def run(self) -> None: log.info("Closing loop...") self._loop.close() - def _signal_handler(self, sig) -> None: + def _signal_handler(self, sig: int) -> None: """React to signals and quit module.""" # stop loop diff --git a/pyobs/cli/pyobs.py b/pyobs/cli/pyobs.py index fe99ce24..dd1513ea 100755 --- a/pyobs/cli/pyobs.py +++ b/pyobs/cli/pyobs.py @@ -1,11 +1,14 @@ import argparse import os -from typing import Type, Any +from typing import Type, Any, Optional, TYPE_CHECKING + +if TYPE_CHECKING: + from pyobs.application import Application from pyobs import version -def init_cli(): +def init_cli() -> argparse.ArgumentParser: # init argument parsing # for all command line parameters we set the default to an environment variable, # so they can also be specified that way @@ -30,7 +33,7 @@ def init_cli(): return parser -def parse_cli(parser: argparse.ArgumentParser): +def parse_cli(parser: argparse.ArgumentParser) -> dict[str, Any]: from pyobs.utils.time import Time # parse args @@ -50,7 +53,7 @@ def parse_cli(parser: argparse.ArgumentParser): return vars(args) -def start_daemon(app_class, pid_file=None, **kwargs: Any) -> None: +def start_daemon(app_class: Type["Application"], pid_file: str, **kwargs: Any) -> None: """Start process as a daemon. Args: @@ -70,7 +73,7 @@ def start_daemon(app_class, pid_file=None, **kwargs: Any) -> None: run(app_class, **kwargs) -def run(app_class: Type, **kwargs: Any) -> None: +def run(app_class: Type["Application"], **kwargs: Any) -> None: """Run a pyobs application with the given options. Args: diff --git a/pyobs/cli/pyobsw.py b/pyobs/cli/pyobsw.py index 815d726b..a9c9748c 100644 --- a/pyobs/cli/pyobsw.py +++ b/pyobs/cli/pyobsw.py @@ -1,7 +1,7 @@ from .pyobs import init_cli, parse_cli, run -def main(): +def main() -> None: from pyobs.application import GuiApplication # init argument parsing and parse it From 312d8dcb7f3323c648df522968495c79aef8d595 Mon Sep 17 00:00:00 2001 From: Tim-Oliver Husser Date: Wed, 20 Dec 2023 14:42:03 +0100 Subject: [PATCH 02/14] mypy --- mypy.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mypy.ini b/mypy.ini index 6e6f94dd..908e564a 100644 --- a/mypy.ini +++ b/mypy.ini @@ -7,6 +7,9 @@ implicit_reexport = True # Minimum version supported python_version = 3.9 +packages=pyobs +exclude = docs/|test/ + [mypy-pyobs.comm.xmpp.xep_0009.*] ignore_errors = True From fc7532eef1dae2fe6191b8a5497611049ab9104c Mon Sep 17 00:00:00 2001 From: Tim-Oliver Husser Date: Wed, 20 Dec 2023 16:58:40 +0100 Subject: [PATCH 03/14] mypy --- pyobs/comm/dbus/patch.py | 2 +- pyobs/comm/dummy/dummycomm.py | 20 +++++++++++++++----- pyobs/comm/local/localcomm.py | 19 ++++++++++--------- pyobs/comm/local/localnetwork.py | 10 +++++----- 4 files changed, 31 insertions(+), 20 deletions(-) diff --git a/pyobs/comm/dbus/patch.py b/pyobs/comm/dbus/patch.py index 1dc2a8e5..7b43abd7 100644 --- a/pyobs/comm/dbus/patch.py +++ b/pyobs/comm/dbus/patch.py @@ -14,7 +14,7 @@ from dbus_next import introspection as intr -def patch(): +def patch() -> None: """ Patches dbus-next to allow for a providing the sender name. Probably a bad idea, but better than using a fork of the project. diff --git a/pyobs/comm/dummy/dummycomm.py b/pyobs/comm/dummy/dummycomm.py index a51ad887..5c336ae8 100644 --- a/pyobs/comm/dummy/dummycomm.py +++ b/pyobs/comm/dummy/dummycomm.py @@ -1,6 +1,6 @@ import inspect import logging -from typing import Any, List, Type +from typing import Any, List, Type, Dict from pyobs.comm import Comm from pyobs.events import Event @@ -22,16 +22,26 @@ def clients(self) -> List[str]: """Always return zero clients.""" return [] - def get_interfaces(self, client: str) -> List[Type[Interface]]: + async def get_interfaces(self, client: str) -> List[Type[Interface]]: """No interfaces implemented.""" return [] - def _supports_interface(self, client: str, interface: Type[Interface]) -> bool: + async def _supports_interface(self, client: str, interface: Type[Interface]) -> bool: """Interfaces are never supported.""" return False - def execute(self, client: str, method: str, signature: inspect.Signature, *args: Any) -> Future: - """Always fake a successful execution of a method.""" + async def execute(self, client: str, method: str, annotation: Dict[str, Any], *args: Any) -> Any: + """Execute a given method on a remote client. + + Args: + client (str): ID of client. + method (str): Method to call. + annotation: Method annotation. + *args: List of parameters for given method. + + Returns: + Passes through return from method call. + """ return Future(empty=True) @property diff --git a/pyobs/comm/local/localcomm.py b/pyobs/comm/local/localcomm.py index fd013ae0..7d88c892 100644 --- a/pyobs/comm/local/localcomm.py +++ b/pyobs/comm/local/localcomm.py @@ -9,8 +9,7 @@ class LocalComm(Comm): - def __init__(self, name: str, *args, **kwargs): - + def __init__(self, name: str, *args: Any, **kwargs: Any): Comm.__init__(self, *args, **kwargs) self._name = name @@ -18,7 +17,7 @@ def __init__(self, name: str, *args, **kwargs): self._network.connect_client(self) @property - def name(self) -> Optional[str]: + def name(self) -> str: """Name of this client.""" return self._name @@ -45,7 +44,7 @@ async def get_interfaces(self, client: str) -> List[Type[Interface]]: """ remote_client: LocalComm = self._network.get_client(client) - return remote_client.module.interfaces + return [] if remote_client.module is None else remote_client.module.interfaces async def _supports_interface(self, client: str, interface: Type[Interface]) -> bool: """Checks, whether the given client supports the given interface. @@ -55,10 +54,10 @@ async def _supports_interface(self, client: str, interface: Type[Interface]) -> interface: Interface to check. Returns: - Whether or not interface is supported. + Whether interface is supported. """ interfaces = await self.get_interfaces(client) - return interfaces in interfaces + return interface in interfaces async def execute(self, client: str, method: str, annotation: Dict[str, Any], *args: Any) -> Any: """Execute a given method on a remote client. @@ -74,10 +73,12 @@ async def execute(self, client: str, method: str, annotation: Dict[str, Any], *a """ remote_client = self._network.get_client(client) + if remote_client.module is None: + raise ValueError simple_results = await remote_client.module.execute(method, *args) real_results = cast_response_to_real( - simple_results, annotation["return"], self.cast_to_real_pre, self.cast_to_real_post - ) + simple_results, annotation["return"], self.cast_to_real_pre, self.cast_to_real_post + ) return real_results async def send_event(self, event: Event) -> None: @@ -94,4 +95,4 @@ async def send_event(self, event: Event) -> None: async def _register_events( self, events: List[Type[Event]], handler: Optional[Callable[[Event, str], Coroutine[Any, Any, bool]]] = None ) -> None: - pass \ No newline at end of file + pass diff --git a/pyobs/comm/local/localnetwork.py b/pyobs/comm/local/localnetwork.py index 9af6ed54..520e909d 100644 --- a/pyobs/comm/local/localnetwork.py +++ b/pyobs/comm/local/localnetwork.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Dict, List +from typing import Dict, List, Optional from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -8,18 +8,18 @@ class LocalNetwork: - _instance = None + _instance: Optional["LocalNetwork"] = None - def __new__(cls): + def __new__(cls) -> "LocalNetwork": if cls._instance is None: - print('Creating the object') + print("Creating the object") cls._instance = super(LocalNetwork, cls).__new__(cls) cls._clients: Dict[str, pyobs.comm.local.LocalComm] = {} return cls._instance - def connect_client(self, comm: pyobs.comm.local.LocalComm): + def connect_client(self, comm: pyobs.comm.local.LocalComm) -> None: self._clients[comm.name] = comm def get_client(self, name: str) -> pyobs.comm.local.LocalComm: From 977bf66966857d21b034359accc8eff214c3b5f5 Mon Sep 17 00:00:00 2001 From: Tim-Oliver Husser Date: Wed, 20 Dec 2023 21:46:25 +0100 Subject: [PATCH 04/14] mypy --- .../processors/_daobackgroundremover.py | 13 ++++--- .../processors/astrometry/_dotnet_request.py | 30 +++++++++------- .../astrometry/_dotnet_request_builder.py | 18 +++++----- .../astrometry/_dotnet_request_logger.py | 6 ++-- .../astrometry/_dotnet_response_saver.py | 34 ++++++++++++------- 5 files changed, 60 insertions(+), 41 deletions(-) diff --git a/pyobs/images/processors/_daobackgroundremover.py b/pyobs/images/processors/_daobackgroundremover.py index ae84109f..454f4f24 100644 --- a/pyobs/images/processors/_daobackgroundremover.py +++ b/pyobs/images/processors/_daobackgroundremover.py @@ -1,6 +1,7 @@ from typing import Tuple import numpy as np +import numpy.typing as npt from astropy.stats import SigmaClip from pyobs.images import Image @@ -20,18 +21,22 @@ def __call__(self, image: Image) -> Image: background = self._estimate_background(image) return self._remove_background(image, background) - def _estimate_background(self, image: Image): + def _estimate_background(self, image: Image) -> npt.NDArray[float]: from photutils.background import Background2D bkg = Background2D( - image.data, box_size=self._box_size, filter_size=self._filter_size, sigma_clip=self._sigma_clip, - bkg_estimator=self._bkg_estimator, mask=image.mask + image.data, + box_size=self._box_size, + filter_size=self._filter_size, + sigma_clip=self._sigma_clip, + bkg_estimator=self._bkg_estimator, + mask=image.mask, ) return bkg.background @staticmethod - def _remove_background(image: Image, background: np.ndarray) -> Image: + def _remove_background(image: Image, background: npt.NDArray[float]) -> Image: output_image = image.copy() output_image.data = output_image.data - background return output_image diff --git a/pyobs/images/processors/astrometry/_dotnet_request.py b/pyobs/images/processors/astrometry/_dotnet_request.py index 783baafc..5813ba38 100644 --- a/pyobs/images/processors/astrometry/_dotnet_request.py +++ b/pyobs/images/processors/astrometry/_dotnet_request.py @@ -1,25 +1,24 @@ -from typing import Dict, Optional - +from typing import Dict, Optional, Any import aiohttp import pyobs.utils.exceptions as exc class _DotNetRequest: - def __init__(self, request_data: Dict[str, any]): + def __init__(self, request_data: Dict[str, Any]): self._request_data = request_data - self._response_data: Optional[Dict[str, any]] = None + self._response_data: Optional[Dict[str, Any]] = None self._status_code: Optional[int] = None - async def _send_request(self, url: str, timeout: int): + async def _send_request(self, url: str, timeout: int) -> None: async with aiohttp.ClientSession() as session: async with session.post(url, json=self._request_data, timeout=timeout) as response: self._status_code = response.status self._response_data = await response.json() def _generate_request_error_msg(self) -> str: - if "error" not in self._response_data: + if self._response_data is None or "error" not in self._response_data: return "Could not connect to astrometry service." if self._response_data["error"] == "Could not find WCS file.": @@ -27,23 +26,28 @@ def _generate_request_error_msg(self) -> str: return f"Received error from astrometry service: {self._response_data['error']}" - def _handle_request_error(self): + def _handle_request_error(self) -> None: error_msg = self._generate_request_error_msg() raise exc.ImageError(error_msg) - def _is_request_successful(self) -> bool: - return self._status_code != 200 or "error" in self._response_data + def _has_request_error(self) -> bool: + return ( + self._status_code is None + or self._response_data is None + or self._status_code != 200 + or "error" in self._response_data + ) - async def send(self, url: str, timeout: int): + async def send(self, url: str, timeout: int) -> None: await self._send_request(url, timeout) - if self._is_request_successful(): + if self._has_request_error(): self._handle_request_error() @property - def request_data(self) -> Dict[str, any]: + def request_data(self) -> Dict[str, Any]: return self._request_data @property - def response_data(self) -> Optional[Dict[str, any]]: + def response_data(self) -> Optional[Dict[str, Any]]: return self._response_data diff --git a/pyobs/images/processors/astrometry/_dotnet_request_builder.py b/pyobs/images/processors/astrometry/_dotnet_request_builder.py index 9b333731..384dc751 100644 --- a/pyobs/images/processors/astrometry/_dotnet_request_builder.py +++ b/pyobs/images/processors/astrometry/_dotnet_request_builder.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Optional, Any, Dict import pandas as pd from astropy.io.fits import Header @@ -13,27 +13,29 @@ def __init__(self, source_count: int, radius: float): self._source_count = source_count self._radius = radius - self._request_data = {} + self._request_data: Dict[str, Any] = {} self._catalog = pd.DataFrame() self._header: Optional[Header] = None - def _filter_catalog(self): + def _filter_catalog(self) -> None: self._catalog = self._catalog.dropna(how="any") self._catalog = self._catalog[self._catalog["peak"] < 60000] - def _validate_catalog(self): + def _validate_catalog(self) -> None: if self._catalog is None or len(self._catalog) < 3: raise exc.ImageError("Not enough sources for astrometry.") - def _select_brightest_stars(self): + def _select_brightest_stars(self) -> None: self._catalog = self._catalog.sort_values("flux", ascending=False) self._catalog = self._catalog[: self._source_count] - def _validate_header(self): - if "CDELT1" not in self._header: + def _validate_header(self) -> None: + if self._header is None or "CDELT1" not in self._header: raise exc.ImageError("No CDELT1 found in header.") - def _build_request_data(self): + def _build_request_data(self) -> None: + if self._header is None: + raise exc.ImageError("No header found.") scale = abs(self._header["CDELT1"]) * 3600 self._request_data = { "ra": self._header["TEL-RA"], diff --git a/pyobs/images/processors/astrometry/_dotnet_request_logger.py b/pyobs/images/processors/astrometry/_dotnet_request_logger.py index 6ddf0274..5e32ddea 100644 --- a/pyobs/images/processors/astrometry/_dotnet_request_logger.py +++ b/pyobs/images/processors/astrometry/_dotnet_request_logger.py @@ -14,7 +14,7 @@ def __init__(self, logger: logging.Logger, image: Image, request_data: Dict[str, self._image = image self._request_data = request_data - def log_request_data(self): + def log_request_data(self) -> None: ra_dec = SkyCoord(ra=self._request_data["ra"] * u.deg, dec=self._request_data["dec"] * u.deg, frame="icrs") center_x, center_y = self._image.header["CRPIX1"], self._image.header["CRPIX2"] @@ -28,7 +28,7 @@ def log_request_data(self): center_y, ) - def log_request_result(self, image_wcs: WCS): + def log_request_result(self, image_wcs: WCS) -> None: center_x, center_y = self._image.header["CRPIX1"], self._image.header["CRPIX2"] final_ra, final_dec = image_wcs.all_pix2world(center_x, center_y, 0) final_ra_dec = SkyCoord(ra=final_ra * u.deg, dec=final_dec * u.deg, frame="icrs") @@ -41,4 +41,4 @@ def log_request_result(self, image_wcs: WCS): self._request_data["dec"], center_x, center_y, - ) \ No newline at end of file + ) diff --git a/pyobs/images/processors/astrometry/_dotnet_response_saver.py b/pyobs/images/processors/astrometry/_dotnet_response_saver.py index 19084e1f..89aceeed 100644 --- a/pyobs/images/processors/astrometry/_dotnet_response_saver.py +++ b/pyobs/images/processors/astrometry/_dotnet_response_saver.py @@ -4,6 +4,7 @@ from astropy.wcs import WCS from pyobs.images import Image +import pyobs.utils.exceptions as exc class _ResponseImageWriter: @@ -14,46 +15,53 @@ def __init__(self, response_data: Dict[str, Any], image: Image): self._image_wcs: Optional[WCS] = None @property - def response_data(self): + def response_data(self) -> Dict[str, Any]: return self._response_data @property - def image(self): + def image(self) -> Image: return self._image @property - def image_wcs(self): + def image_wcs(self) -> WCS: return self._image_wcs - def _write_response_into_header(self): + def _write_response_into_header(self) -> None: header_keywords_to_update = [ - "CTYPE1", "CTYPE2", - "CRPIX1", "CRPIX2", - "CRVAL1", "CRVAL2", - "CD1_1", "CD1_2", - "CD2_1", "CD2_2", + "CTYPE1", + "CTYPE2", + "CRPIX1", + "CRPIX2", + "CRVAL1", + "CRVAL2", + "CD1_1", + "CD1_2", + "CD2_1", + "CD2_2", ] for keyword in header_keywords_to_update: self._image.header[keyword] = self._response_data[keyword] - def _delete_old_wcs_data(self): + def _delete_old_wcs_data(self) -> None: """ astrometry.net gives a CD matrix, so we have to delete the PC matrix and the CDELT* parameters """ for keyword in ["PC1_1", "PC1_2", "PC2_1", "PC2_2", "CDELT1", "CDELT2"]: del self._image.header[keyword] - def _generate_image_wcs(self): + def _generate_image_wcs(self) -> None: self._image_wcs = WCS(self._image.header) - def _add_plate_solution_to_catalog(self): + def _add_plate_solution_to_catalog(self) -> None: + if self._image_wcs is None: + raise exc.ImageError("No WCS found.") ras, decs = self._image_wcs.all_pix2world(self._image.catalog["x"], self._image.catalog["y"], 1) self._image.catalog["ra"] = ras self._image.catalog["dec"] = decs - def _add_wcs_err_success(self): + def _add_wcs_err_success(self) -> None: self._image.header["WCSERR"] = 0 def __call__(self, *args, **kwargs) -> Image: From 75353047ad0f53f52effb7bcddeb8cf036e429be Mon Sep 17 00:00:00 2001 From: Tim-Oliver Husser Date: Wed, 20 Dec 2023 21:51:43 +0100 Subject: [PATCH 05/14] added getters for image properties --- pyobs/images/image.py | 50 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/pyobs/images/image.py b/pyobs/images/image.py index e23eaf61..1755cfe2 100644 --- a/pyobs/images/image.py +++ b/pyobs/images/image.py @@ -45,16 +45,16 @@ def __init__( """ # store - self.data = data - self.header = fits.Header() if header is None else header.copy() - self.mask = None if mask is None else mask.copy() - self.uncertainty = None if uncertainty is None else uncertainty.copy() - self.catalog = None if catalog is None else catalog.copy() - self.raw = None if raw is None else raw.copy() - self.meta = {} if meta is None else copy.deepcopy(meta) + self._data = data + self._header = fits.Header() if header is None else header.copy() + self._mask = None if mask is None else mask.copy() + self._uncertainty = None if uncertainty is None else uncertainty.copy() + self._catalog = None if catalog is None else catalog.copy() + self._raw = None if raw is None else raw.copy() + self._meta = {} if meta is None else copy.deepcopy(meta) # add basic header stuff - if data is not None: + if data is not None and self.header is not None: self.header["NAXIS1"] = data.shape[1] self.header["NAXIS2"] = data.shape[0] @@ -128,8 +128,10 @@ def _from_hdu_list(cls, data: fits.HDUList) -> "Image": # find HDU with image data for hdu in data: if ( - isinstance(hdu, fits.PrimaryHDU) and hdu.header["NAXIS"] > 0 - or isinstance(hdu, fits.ImageHDU) and hdu.name == "SCI" + isinstance(hdu, fits.PrimaryHDU) + and hdu.header["NAXIS"] > 0 + or isinstance(hdu, fits.ImageHDU) + and hdu.name == "SCI" or isinstance(hdu, fits.CompImageHDU) ): # found image HDU @@ -361,5 +363,33 @@ def get_meta_safe(self, meta_class: Type[MetaClass], default: Optional[MetaClass except: return default + @property + def data(self): + return self._data + + @property + def header(self): + return self._header + + @property + def mask(self): + return self._mask + + @property + def uncertainty(self): + return self._uncertainty + + @property + def catalog(self): + return self._catalog + + @property + def raw(self): + return self._raw + + @property + def meta(self): + return self._meta + __all__ = ["Image"] From 9782f43820eafedfcb7142243fd8774c56c78e16 Mon Sep 17 00:00:00 2001 From: Tim-Oliver Husser Date: Wed, 20 Dec 2023 22:03:40 +0100 Subject: [PATCH 06/14] added getters and setters for image properties --- pyobs/images/image.py | 122 ++++++++++++++++++++++++++---------------- 1 file changed, 75 insertions(+), 47 deletions(-) diff --git a/pyobs/images/image.py b/pyobs/images/image.py index 1755cfe2..05b3dfc6 100644 --- a/pyobs/images/image.py +++ b/pyobs/images/image.py @@ -72,10 +72,10 @@ def from_bytes(cls, data: bytes) -> Image: # create hdu with io.BytesIO(data) as bio: # read whole file - data = fits.open(bio, memmap=False, lazy_load_hdus=False) + d = fits.open(bio, memmap=False, lazy_load_hdus=False) # load image - return cls._from_hdu_list(data) + return cls._from_hdu_list(d) @classmethod def from_file(cls, filename: str) -> Image: @@ -146,19 +146,19 @@ def _from_hdu_list(cls, data: fits.HDUList) -> "Image": # mask if "MASK" in data: - image.mask = data["MASK"].data + image._mask = data["MASK"].data # uncertainties if "UNCERT" in data: - image.uncertainty = data["UNCERT"].data + image._uncertainty = data["UNCERT"].data # catalog if "CAT" in data: - image.catalog = Table(data["CAT"].data) + image._catalog = Table(data["CAT"].data) # raw if "RAW" in data: - image.raw = data["RAW"].data + image._raw = data["RAW"].data # finished return image @@ -178,21 +178,21 @@ def __deepcopy__(self) -> Image: def copy(self) -> Image: """Returns a copy of this image.""" return Image( - data=self.data, - header=self.header, - mask=self.mask, - uncertainty=self.uncertainty, - catalog=self.catalog, - raw=self.raw, - meta=self.meta, + data=self._data, + header=self._header, + mask=self._mask, + uncertainty=self._uncertainty, + catalog=self._catalog, + raw=self._raw, + meta=self._meta, ) def __truediv__(self, other: Image) -> Image: """Divides this image by other.""" img = self.copy() - if img.data is None or other.data is None: + if img._data is None or other._data is None: raise ValueError("One image in division is None.") - img.data /= other.data + img._data /= other._data return img def writeto(self, f: Any, *args: Any, **kwargs: Any) -> None: @@ -210,26 +210,26 @@ def writeto(self, f: Any, *args: Any, **kwargs: Any) -> None: hdu_list.append(hdu) # catalog? - if self.catalog is not None: - hdu = table_to_hdu(self.catalog) + if self._catalog is not None: + hdu = table_to_hdu(self._catalog) hdu.name = "CAT" hdu_list.append(hdu) # mask? - if self.mask is not None: - hdu = ImageHDU(self.mask.astype(np.uint8)) + if self._mask is not None: + hdu = ImageHDU(self._mask.astype(np.uint8)) hdu.name = "MASK" hdu_list.append(hdu) # errors? - if self.uncertainty is not None: - hdu = ImageHDU(self.uncertainty.data) + if self._uncertainty is not None: + hdu = ImageHDU(self._uncertainty.data) hdu.name = "UNCERT" hdu_list.append(hdu) # raw? - if self.raw is not None: - hdu = ImageHDU(self.raw.data) + if self._raw is not None: + hdu = ImageHDU(self._raw.data) hdu.name = "RAW" hdu_list.append(hdu) @@ -247,31 +247,31 @@ def write_catalog(self, f: Any, *args: Any, **kwargs: Any) -> None: if self.catalog is None: return - hdu = table_to_hdu(self.catalog) + hdu = table_to_hdu(self._catalog) hdu.writeto(f, *args, **kwargs) def to_ccddata(self) -> CCDData: """Convert Image to CCDData""" return CCDData( - data=self.data, - meta=self.header, - mask=None if self.mask is None else self.mask, - uncertainty=None if self.uncertainty is None else StdDevUncertainty(self.uncertainty), + data=self._data, + meta=self._header, + mask=self._mask, + uncertainty=None if self._uncertainty is None else StdDevUncertainty(self._uncertainty), unit="adu", ) def format_filename(self, formatter: FilenameFormatter) -> str: """Format filename with given formatter.""" - self.header["FNAME"] = formatter(self.header) - return str(self.header["FNAME"]) + self._header["FNAME"] = formatter(self._header) + return str(self._header["FNAME"]) @property def pixel_scale(self) -> Optional[float]: """Returns pixel scale in arcsec/pixel.""" - if "CD1_1" in self.header: - return abs(float(self.header["CD1_1"])) * 3600.0 - elif "CDELT1" in self.header: - return abs(float(self.header["CDELT1"])) * 3600.0 + if "CD1_1" in self._header: + return abs(float(self._header["CD1_1"])) * 3600.0 + elif "CDELT1" in self._header: + return abs(float(self._header["CDELT1"])) * 3600.0 else: return None @@ -286,7 +286,7 @@ def to_jpeg(self, vmin: Optional[float] = None, vmax: Optional[float] = None) -> import PIL.Image # copy data - data: NDArray[Any] = np.copy(self.data) # type: ignore + data: NDArray[Any] = np.copy(self._data) # type: ignore if data is None: raise ValueError("No data in image.") @@ -329,11 +329,11 @@ def set_meta(self, meta: Any) -> None: """ # store it - self.meta[meta.__class__] = meta + self._meta[meta.__class__] = meta def has_meta(self, meta_class: Type[MetaClass]) -> bool: """Whether meta exists.""" - return meta_class in self.meta + return meta_class in self._meta def get_meta(self, meta_class: Type[MetaClass]) -> MetaClass: """Returns meta information, assuming that it is stored under the class of the object. @@ -345,15 +345,15 @@ def get_meta(self, meta_class: Type[MetaClass]) -> MetaClass: Meta information of the given class. """ # return default? - if meta_class not in self.meta: + if meta_class not in self._meta: raise ValueError("Meta value not found.") # correct type? - if not isinstance(self.meta[meta_class], meta_class): + if not isinstance(self._meta[meta_class], meta_class): raise ValueError("Stored meta information is of wrong type.") # return it - return cast(MetaClass, self.meta[meta_class]) + return cast(MetaClass, self._meta[meta_class]) def get_meta_safe(self, meta_class: Type[MetaClass], default: Optional[MetaClass] = None) -> Optional[MetaClass]: """Calls get_meta in a safe way and returns default value in case of an exception.""" @@ -364,32 +364,60 @@ def get_meta_safe(self, meta_class: Type[MetaClass], default: Optional[MetaClass return default @property - def data(self): + def data(self) -> Optional[NDArray[Any]]: return self._data + @data.setter + def data(self, val: Optional[NDArray[Any]]): + self._data = val + @property - def header(self): + def header(self) -> Optional[fits.Header]: return self._header + @header.setter + def header(self, val: Optional[fits.Header]): + self._header = val + @property - def mask(self): + def mask(self) -> Optional[NDArray[Any]]: return self._mask + @mask.setter + def mask(self, val: Optional[NDArray[Any]]): + self._mask = val + @property - def uncertainty(self): + def uncertainty(self) -> Optional[NDArray[Any]]: return self._uncertainty + @uncertainty.setter + def uncertainty(self, val: Optional[NDArray[Any]]): + self._uncertainty = val + @property - def catalog(self): + def catalog(self) -> Optional[Table]: return self._catalog + @catalog.setter + def catalog(self, val: Optional[Table]): + self._catalog = val + @property - def raw(self): + def raw(self) -> Optional[NDArray[Any]]: return self._raw + @raw.setter + def raw(self, val: Optional[NDArray[Any]]): + self._raw = val + @property - def meta(self): + def meta(self) -> Optional[Dict[Any, Any]]: return self._meta + @meta.setter + def meta(self, val: Optional[Dict[Any, Any]]): + self._meta = val + __all__ = ["Image"] From e58043bcc82930a13e841c386e264d18e6d808ed Mon Sep 17 00:00:00 2001 From: Tim-Oliver Husser Date: Thu, 21 Dec 2023 10:33:30 +0100 Subject: [PATCH 07/14] fixed access --- pyobs/images/image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyobs/images/image.py b/pyobs/images/image.py index 05b3dfc6..786bdc94 100644 --- a/pyobs/images/image.py +++ b/pyobs/images/image.py @@ -54,7 +54,7 @@ def __init__( self._meta = {} if meta is None else copy.deepcopy(meta) # add basic header stuff - if data is not None and self.header is not None: + if data is not None and self._header is not None: self.header["NAXIS1"] = data.shape[1] self.header["NAXIS2"] = data.shape[0] From a0c853a05d9520b56ea47ac5d4bed4885c625fca Mon Sep 17 00:00:00 2001 From: Tim-Oliver Husser Date: Thu, 21 Dec 2023 10:42:42 +0100 Subject: [PATCH 08/14] added safe_getters and let the original getters raise exceptions --- pyobs/images/image.py | 57 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 7 deletions(-) diff --git a/pyobs/images/image.py b/pyobs/images/image.py index 786bdc94..0195620b 100644 --- a/pyobs/images/image.py +++ b/pyobs/images/image.py @@ -11,6 +11,7 @@ from numpy.typing import NDArray from pyobs.utils.fits import FilenameFormatter +import pyobs.utils.exceptions as exc MetaClass = TypeVar("MetaClass") @@ -364,7 +365,13 @@ def get_meta_safe(self, meta_class: Type[MetaClass], default: Optional[MetaClass return default @property - def data(self) -> Optional[NDArray[Any]]: + def data(self) -> NDArray[Any]: + if self._data is None: + raise exc.ImageError("No data found in image.") + return self._data + + @property + def safe_data(self) -> Optional[NDArray[Any]]: return self._data @data.setter @@ -372,7 +379,13 @@ def data(self, val: Optional[NDArray[Any]]): self._data = val @property - def header(self) -> Optional[fits.Header]: + def header(self) -> fits.Header: + if self._header is None: + raise exc.ImageError("No header found in image.") + return self._header + + @property + def safe_header(self) -> Optional[fits.Header]: return self._header @header.setter @@ -380,7 +393,13 @@ def header(self, val: Optional[fits.Header]): self._header = val @property - def mask(self) -> Optional[NDArray[Any]]: + def mask(self) -> NDArray[Any]: + if self._mask is None: + raise exc.ImageError("No mask found in image.") + return self._mask + + @property + def safe_mask(self) -> Optional[NDArray[Any]]: return self._mask @mask.setter @@ -388,7 +407,13 @@ def mask(self, val: Optional[NDArray[Any]]): self._mask = val @property - def uncertainty(self) -> Optional[NDArray[Any]]: + def uncertainty(self) -> NDArray[Any]: + if self._uncertainty is None: + raise exc.ImageError("No uncertainties found in image.") + return self._uncertainty + + @property + def safe_uncertainty(self) -> Optional[NDArray[Any]]: return self._uncertainty @uncertainty.setter @@ -396,7 +421,13 @@ def uncertainty(self, val: Optional[NDArray[Any]]): self._uncertainty = val @property - def catalog(self) -> Optional[Table]: + def catalog(self) -> Table: + if self._catalog is None: + raise exc.ImageError("No catalog found in image.") + return self._catalog + + @property + def safe_catalog(self) -> Optional[Table]: return self._catalog @catalog.setter @@ -404,7 +435,13 @@ def catalog(self, val: Optional[Table]): self._catalog = val @property - def raw(self) -> Optional[NDArray[Any]]: + def raw(self) -> NDArray[Any]: + if self._raw is None: + raise exc.ImageError("No raw data found in image.") + return self._raw + + @property + def safe_raw(self) -> Optional[NDArray[Any]]: return self._raw @raw.setter @@ -412,7 +449,13 @@ def raw(self, val: Optional[NDArray[Any]]): self._raw = val @property - def meta(self) -> Optional[Dict[Any, Any]]: + def meta(self) -> Dict[Any, Any]: + if self._meta is None: + raise exc.ImageError("No meta data found in image.") + return self._meta + + @property + def safe_meta(self) -> Optional[Dict[Any, Any]]: return self._meta @meta.setter From 5d6c395b940aa55ac5ca712a0a4acb84537d90e9 Mon Sep 17 00:00:00 2001 From: Tim-Oliver Husser Date: Thu, 21 Dec 2023 10:55:34 +0100 Subject: [PATCH 09/14] tests are passing again --- pyobs/images/image.py | 2 +- .../processors/_daobackgroundremover.py | 2 +- pyobs/images/processors/detection/daophot.py | 2 +- pyobs/images/processors/detection/pysep.py | 18 +++++++++++------ pyobs/images/processors/misc/smooth.py | 2 +- pyobs/images/processors/misc/softbin.py | 4 ++-- pyobs/images/processors/offsets/nstar.py | 8 +------- pyobs/images/processors/offsets/projected.py | 4 ---- pyobs/images/processors/photometry/pysep.py | 9 +++------ pyobs/modules/camera/basecamera.py | 2 +- pyobs/utils/focusseries/projection.py | 2 +- .../processors/test_removebackground.py | 2 +- tests/images/test_image.py | 20 +++++++++++++------ 13 files changed, 39 insertions(+), 38 deletions(-) diff --git a/pyobs/images/image.py b/pyobs/images/image.py index 0195620b..c3dc83f7 100644 --- a/pyobs/images/image.py +++ b/pyobs/images/image.py @@ -245,7 +245,7 @@ def to_bytes(self) -> bytes: def write_catalog(self, f: Any, *args: Any, **kwargs: Any) -> None: """Write catalog to file object.""" - if self.catalog is None: + if self._catalog is None: return hdu = table_to_hdu(self._catalog) diff --git a/pyobs/images/processors/_daobackgroundremover.py b/pyobs/images/processors/_daobackgroundremover.py index 454f4f24..0aa85258 100644 --- a/pyobs/images/processors/_daobackgroundremover.py +++ b/pyobs/images/processors/_daobackgroundremover.py @@ -30,7 +30,7 @@ def _estimate_background(self, image: Image) -> npt.NDArray[float]: filter_size=self._filter_size, sigma_clip=self._sigma_clip, bkg_estimator=self._bkg_estimator, - mask=image.mask, + mask=image.safe_mask, ) return bkg.background diff --git a/pyobs/images/processors/detection/daophot.py b/pyobs/images/processors/detection/daophot.py index 4f5d5778..fe3feafd 100644 --- a/pyobs/images/processors/detection/daophot.py +++ b/pyobs/images/processors/detection/daophot.py @@ -65,7 +65,7 @@ async def __call__(self, image: Image) -> Image: Image with attached catalog. """ - if image.data is None: + if image.safe_data is None: log.warning("No data found in image.") return image diff --git a/pyobs/images/processors/detection/pysep.py b/pyobs/images/processors/detection/pysep.py index 2b7cbb45..fc7f084e 100644 --- a/pyobs/images/processors/detection/pysep.py +++ b/pyobs/images/processors/detection/pysep.py @@ -23,16 +23,22 @@ class SepSourceDetection(SourceDetection): __module__ = "pyobs.images.processors.detection" _CATALOG_KEYS = [ - "x", "y", + "x", + "y", "peak", "flux", "fwhm", - "a", "b", "theta", + "a", + "b", + "theta", "ellipticity", "tnpix", "kronrad", - "fluxrad25", "fluxrad50", "fluxrad75", - "xwin", "ywin", + "fluxrad25", + "fluxrad50", + "fluxrad75", + "xwin", + "ywin", ] def __init__( @@ -77,7 +83,7 @@ async def __call__(self, image: Image) -> Image: Image with attached catalog. """ - if image.data is None: + if image.safe_data is None: log.warning("No data found in image.") return image @@ -104,7 +110,7 @@ async def __call__(self, image: Image) -> Image: @staticmethod def _get_mask_or_default(image: Image) -> np.ndarray: - if image.mask is not None: + if image.safe_mask is not None: return image.mask return np.zeros(image.data.shape, dtype=bool) diff --git a/pyobs/images/processors/misc/smooth.py b/pyobs/images/processors/misc/smooth.py index 7b1a4539..5a1741c4 100644 --- a/pyobs/images/processors/misc/smooth.py +++ b/pyobs/images/processors/misc/smooth.py @@ -54,7 +54,7 @@ async def __call__(self, image: Image) -> Image: """ output_image = image.copy() - if output_image.data is None: + if output_image.safe_data is None: log.warning("No data found in image.") return image diff --git a/pyobs/images/processors/misc/softbin.py b/pyobs/images/processors/misc/softbin.py index eeb8a9ff..1feb5cea 100644 --- a/pyobs/images/processors/misc/softbin.py +++ b/pyobs/images/processors/misc/softbin.py @@ -37,12 +37,12 @@ async def __call__(self, image: Image) -> Image: """ output_image = image.copy() - if output_image.data is None: + if output_image.safe_data is None: log.warning("No data found in image.") return image output_image.data = self._reshape_image(output_image.data) - if output_image.data is None: + if output_image.safe_data is None: log.warning("No data found in image after reshaping.") return image diff --git a/pyobs/images/processors/offsets/nstar.py b/pyobs/images/processors/offsets/nstar.py index a56a41e7..371f6132 100644 --- a/pyobs/images/processors/offsets/nstar.py +++ b/pyobs/images/processors/offsets/nstar.py @@ -128,12 +128,6 @@ async def _boxes_from_ref(self, image: Image, star_box_size: int) -> List: # run pipeline on 1st image img = await self.run_pipeline(image) - # check data and catalog - if img.data is None: - raise ValueError("No data found in image.") - if img.catalog is None: - raise ValueError("No catalog found in image.") - # do photometry and get catalog sources = self._fits2numpy(img.catalog) @@ -273,7 +267,7 @@ def _calculate_offsets(self, image: Image) -> Tuple[Optional[float], Optional[fl """ # data? - if image.data is None: + if image.safe_data is None: return None, None # calculate offset for each star diff --git a/pyobs/images/processors/offsets/projected.py b/pyobs/images/processors/offsets/projected.py index 73cea460..0b457b2b 100644 --- a/pyobs/images/processors/offsets/projected.py +++ b/pyobs/images/processors/offsets/projected.py @@ -82,10 +82,6 @@ def _process(self, image: Image) -> Tuple[npt.NDArray[float], npt.NDArray[float] # get image data and header data, hdr = image.data, image.header - # no data? - if data is None: - raise ValueError("Image contains no data.") - # trimsec if "TRIMSEC" in hdr: m = re.match(r"\[([0-9]+):([0-9]+),([0-9]+):([0-9]+)\]", hdr["TRIMSEC"]) diff --git a/pyobs/images/processors/photometry/pysep.py b/pyobs/images/processors/photometry/pysep.py index c559ada6..e9afaa9b 100644 --- a/pyobs/images/processors/photometry/pysep.py +++ b/pyobs/images/processors/photometry/pysep.py @@ -67,18 +67,15 @@ def _photometry(image: Image) -> Image: from pyobs.images.processors.detection import SepSourceDetection # check data - if image.data is None: + if image.safe_data is None: log.warning("No data found in image.") return image - if image.catalog is None: + if image.safe_catalog is None: log.warning("No catalog found in image.") return image - # no mask? - mask = image.mask if image.mask is not None else np.ones(image.data.shape, dtype=bool) - # remove background - data, bkg = SepSourceDetection.remove_background(image.data, mask) + data, bkg = SepSourceDetection.remove_background(image.data, image.safe_mask) # fetch catalog sources = image.catalog.copy() diff --git a/pyobs/modules/camera/basecamera.py b/pyobs/modules/camera/basecamera.py index 24640667..96ac0760 100644 --- a/pyobs/modules/camera/basecamera.py +++ b/pyobs/modules/camera/basecamera.py @@ -246,7 +246,7 @@ async def __expose(self, exposure_time: float, image_type: ImageType, broadcast: self._exposure = ExposureInfo(start=datetime.datetime.utcnow(), exposure_time=exposure_time) try: image = await self._expose(exposure_time, open_shutter, abort_event=self.expose_abort) - if image is None or image.data is None: + if image is None or image.safe_data is None: raise exc.GrabImageError("Could not take image.") except exc.PyObsError: diff --git a/pyobs/utils/focusseries/projection.py b/pyobs/utils/focusseries/projection.py index 61e48107..44feb811 100644 --- a/pyobs/utils/focusseries/projection.py +++ b/pyobs/utils/focusseries/projection.py @@ -47,7 +47,7 @@ def analyse_image(self, image: Image, focus_value: float) -> None: """ # clean data - if image.data is None: + if image.safe_data is None: return data = self._clean(image.data) diff --git a/tests/images/processors/test_removebackground.py b/tests/images/processors/test_removebackground.py index 46981b18..2ecb7d34 100644 --- a/tests/images/processors/test_removebackground.py +++ b/tests/images/processors/test_removebackground.py @@ -49,4 +49,4 @@ def test_call_const_background(mocker): image = Image(data=np.ones((20, 20))) output_image = remover(image) - np.testing.assert_array_equal(output_image.data, np.zeros((20, 20))) \ No newline at end of file + np.testing.assert_array_equal(output_image.data, np.zeros((20, 20))) diff --git a/tests/images/test_image.py b/tests/images/test_image.py index 904f51bf..340931ad 100644 --- a/tests/images/test_image.py +++ b/tests/images/test_image.py @@ -24,20 +24,28 @@ def assert_array_equal_or_none(check_value, value): def assert_equal_image_params(image, data=None, mask=None, uncertainty=None, catalog=None, raw=None, meta={}): - assert_array_equal_or_none(data, image.data) - assert_array_equal_or_none(mask, image.mask) - assert_array_equal_or_none(uncertainty, image.uncertainty) + assert_array_equal_or_none(data, image.safe_data) + assert_array_equal_or_none(mask, image.safe_mask) + assert_array_equal_or_none(uncertainty, image.safe_uncertainty) if catalog is None: - assert image.catalog is None + assert image.safe_catalog is None else: assert_array_equal_or_none(catalog.as_array(), image.catalog.as_array()) - assert_array_equal_or_none(raw, image.raw) + assert_array_equal_or_none(raw, image.safe_raw) assert image.meta == meta def assert_equal_image(image, other): - assert_equal_image_params(image, other.data, other.mask, other.uncertainty, other.catalog, other.raw, other.meta) + assert_equal_image_params( + image, + other.safe_data, + other.safe_mask, + other.safe_uncertainty, + other.safe_catalog, + other.safe_raw, + other.safe_meta, + ) @pytest.fixture() From ca6a6467d1085f323b19a65e0ae9eac9acd177d5 Mon Sep 17 00:00:00 2001 From: Tim-Oliver Husser Date: Thu, 21 Dec 2023 10:59:13 +0100 Subject: [PATCH 10/14] call safe_mask instead of mask --- pyobs/images/processors/photometry/photutil.py | 2 +- pyobs/images/processors/photometry/pysep.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pyobs/images/processors/photometry/photutil.py b/pyobs/images/processors/photometry/photutil.py index 8070aeaa..f34a447b 100644 --- a/pyobs/images/processors/photometry/photutil.py +++ b/pyobs/images/processors/photometry/photutil.py @@ -96,7 +96,7 @@ async def __call__(self, image: Image) -> Image: # do photometry phot = await loop.run_in_executor( - None, partial(aperture_photometry, image.data, aperture, mask=image.mask, error=image.uncertainty) + None, partial(aperture_photometry, image.data, aperture, mask=image.safe_mask, error=image.uncertainty) ) # calc flux diff --git a/pyobs/images/processors/photometry/pysep.py b/pyobs/images/processors/photometry/pysep.py index e9afaa9b..1b097da2 100644 --- a/pyobs/images/processors/photometry/pysep.py +++ b/pyobs/images/processors/photometry/pysep.py @@ -90,7 +90,13 @@ def _photometry(image: Image) -> Image: for diameter in [1, 2, 3, 4, 5, 6, 7, 8]: if image.pixel_scale is not None: flux, fluxerr, flag = sep.sum_circle( - data, x, y, diameter / 2.0 / image.pixel_scale, mask=image.mask, err=image.uncertainty, gain=gain + data, + x, + y, + diameter / 2.0 / image.pixel_scale, + mask=image.safe_mask, + err=image.uncertainty, + gain=gain, ) sources["fluxaper{0}".format(diameter)] = flux sources["fluxerr{0}".format(diameter)] = fluxerr From 105b85f993b7290e56f1e5c58c30126a50d88f45 Mon Sep 17 00:00:00 2001 From: Tim-Oliver Husser Date: Thu, 21 Dec 2023 16:45:41 +0100 Subject: [PATCH 11/14] added getters and setters for image properties --- pyobs/images/image.py | 6 +----- .../images/processors/astrometry/_dotnet_request_builder.py | 2 -- pyobs/images/processors/exptime/star.py | 2 +- pyobs/images/processors/offsets/brighteststar.py | 2 +- pyobs/images/processors/photometry/photutil.py | 5 +++-- pyobs/images/processors/photometry/pysep.py | 2 +- pyobs/modules/image/seeing.py | 2 +- pyobs/utils/focusseries/photometry.py | 2 +- tests/images/test_image.py | 2 +- 9 files changed, 10 insertions(+), 15 deletions(-) diff --git a/pyobs/images/image.py b/pyobs/images/image.py index c3dc83f7..2ca54c76 100644 --- a/pyobs/images/image.py +++ b/pyobs/images/image.py @@ -451,11 +451,7 @@ def raw(self, val: Optional[NDArray[Any]]): @property def meta(self) -> Dict[Any, Any]: if self._meta is None: - raise exc.ImageError("No meta data found in image.") - return self._meta - - @property - def safe_meta(self) -> Optional[Dict[Any, Any]]: + self._meta: Dict[Any, Any] = {} return self._meta @meta.setter diff --git a/pyobs/images/processors/astrometry/_dotnet_request_builder.py b/pyobs/images/processors/astrometry/_dotnet_request_builder.py index 384dc751..94d06387 100644 --- a/pyobs/images/processors/astrometry/_dotnet_request_builder.py +++ b/pyobs/images/processors/astrometry/_dotnet_request_builder.py @@ -54,8 +54,6 @@ def _build_request_data(self) -> None: def __call__(self, image: Image) -> _DotNetRequest: # set catalog and header - if image.catalog is None: - raise exc.ImageError("No catalog found in image.") self._catalog = image.catalog[["x", "y", "flux", "peak"]].to_pandas() self._header = image.header diff --git a/pyobs/images/processors/exptime/star.py b/pyobs/images/processors/exptime/star.py index 6846351f..cf5971e7 100644 --- a/pyobs/images/processors/exptime/star.py +++ b/pyobs/images/processors/exptime/star.py @@ -51,7 +51,7 @@ async def _calc_exp_time(self, image: Image) -> float: self._image = copy(image) last_exp_time = image.header["EXPTIME"] - if self._image.catalog is None: + if self._image.safe_catalog is None: log.info("No catalog found in image.") return last_exp_time diff --git a/pyobs/images/processors/offsets/brighteststar.py b/pyobs/images/processors/offsets/brighteststar.py index dce13abd..c55a6c39 100644 --- a/pyobs/images/processors/offsets/brighteststar.py +++ b/pyobs/images/processors/offsets/brighteststar.py @@ -37,7 +37,7 @@ async def __call__(self, image: Image) -> Image: """ # get catalog and sort by flux - cat = image.catalog + cat = image.safe_catalog if cat is None or len(cat) < 1: log.warning("No catalog found in image.") return image diff --git a/pyobs/images/processors/photometry/photutil.py b/pyobs/images/processors/photometry/photutil.py index f34a447b..4356a07b 100644 --- a/pyobs/images/processors/photometry/photutil.py +++ b/pyobs/images/processors/photometry/photutil.py @@ -66,7 +66,7 @@ async def __call__(self, image: Image) -> Image: return image # fetch catalog - if image.catalog is None: + if image.safe_catalog is None: log.warning("No catalog in image.") return image sources = image.catalog.copy() @@ -96,7 +96,8 @@ async def __call__(self, image: Image) -> Image: # do photometry phot = await loop.run_in_executor( - None, partial(aperture_photometry, image.data, aperture, mask=image.safe_mask, error=image.uncertainty) + None, + partial(aperture_photometry, image.data, aperture, mask=image.safe_mask, error=image.safe_uncertainty), ) # calc flux diff --git a/pyobs/images/processors/photometry/pysep.py b/pyobs/images/processors/photometry/pysep.py index 1b097da2..bb765079 100644 --- a/pyobs/images/processors/photometry/pysep.py +++ b/pyobs/images/processors/photometry/pysep.py @@ -95,7 +95,7 @@ def _photometry(image: Image) -> Image: y, diameter / 2.0 / image.pixel_scale, mask=image.safe_mask, - err=image.uncertainty, + err=image.safe_uncertainty, gain=gain, ) sources["fluxaper{0}".format(diameter)] = flux diff --git a/pyobs/modules/image/seeing.py b/pyobs/modules/image/seeing.py index 68bd825f..6557d912 100644 --- a/pyobs/modules/image/seeing.py +++ b/pyobs/modules/image/seeing.py @@ -79,7 +79,7 @@ async def process_new_image_event(self, event: Event, sender: str) -> bool: return False # get catalog - cat = image.catalog + cat = image.safe_catalog if cat is None: # no catalog found in file return False diff --git a/pyobs/utils/focusseries/photometry.py b/pyobs/utils/focusseries/photometry.py index c316e09b..5c4874c5 100644 --- a/pyobs/utils/focusseries/photometry.py +++ b/pyobs/utils/focusseries/photometry.py @@ -45,7 +45,7 @@ def analyse_image(self, image: Image, focus_value: float) -> None: self._source_detection(image) # filter - sources = image.catalog + sources = image.safe_catalog if sources is None: return sources = sources[sources["ellipticity"] < 0.1] diff --git a/tests/images/test_image.py b/tests/images/test_image.py index 340931ad..06c02c88 100644 --- a/tests/images/test_image.py +++ b/tests/images/test_image.py @@ -44,7 +44,7 @@ def assert_equal_image(image, other): other.safe_uncertainty, other.safe_catalog, other.safe_raw, - other.safe_meta, + other.meta, ) From eb9cdc3d1e12f5b9ae49c7d8cf74d945ab1d938f Mon Sep 17 00:00:00 2001 From: Tim-Oliver Husser Date: Fri, 22 Dec 2023 18:14:31 +0100 Subject: [PATCH 12/14] checking, whether script can run --- pyobs/robotic/scripts/parallel.py | 11 ++++++----- pyobs/robotic/scripts/sequential.py | 9 +++++++-- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/pyobs/robotic/scripts/parallel.py b/pyobs/robotic/scripts/parallel.py index d63c4311..efa94b11 100644 --- a/pyobs/robotic/scripts/parallel.py +++ b/pyobs/robotic/scripts/parallel.py @@ -19,6 +19,7 @@ class ParallelRunner(Script): def __init__( self, scripts: List[Dict[str, Any]], + check_all_can_run: bool = True, **kwargs: Any, ): """Initialize a new ParallelRunner. @@ -28,9 +29,11 @@ def __init__( """ Script.__init__(self, **kwargs) self.scripts = scripts + self.check_all_can_run = check_all_can_run async def can_run(self) -> bool: - return True + check_all = [self.get_object(s, Script).can_run() for s in self.scripts] + return all(check_all) if self.check_all_can_run else any(check_all) async def run( self, @@ -38,10 +41,8 @@ async def run( task_schedule: Optional[TaskSchedule] = None, task_archive: Optional[TaskArchive] = None, ) -> None: - tasks = [ - asyncio.create_task(self.get_object(s, Script).run(task_runner, task_schedule, task_archive)) - for s in self.scripts - ] + scripts = [self.get_object(s, Script) for s in self.scripts] + tasks = [asyncio.create_task(s.run(task_runner, task_schedule, task_archive)) for s in scripts if s.can_run()] await asyncio.gather(*tasks) diff --git a/pyobs/robotic/scripts/sequential.py b/pyobs/robotic/scripts/sequential.py index ca77c65d..0962618c 100644 --- a/pyobs/robotic/scripts/sequential.py +++ b/pyobs/robotic/scripts/sequential.py @@ -17,6 +17,7 @@ class SequentialRunner(Script): def __init__( self, scripts: List[Dict[str, Any]], + check_all_can_run: bool = True, **kwargs: Any, ): """Initialize a new SequentialRunner. @@ -26,9 +27,11 @@ def __init__( """ Script.__init__(self, **kwargs) self.scripts = scripts + self.check_all_can_run = check_all_can_run async def can_run(self) -> bool: - return True + check_all = [self.get_object(s, Script).can_run() for s in self.scripts] + return all(check_all) if self.check_all_can_run else check_all[0] async def run( self, @@ -37,7 +40,9 @@ async def run( task_archive: Optional[TaskArchive] = None, ) -> None: for s in self.scripts: - await self.get_object(s, Script).run(task_runner, task_schedule, task_archive) + script = self.get_object(s, Script) + if script.can_run(): + await script.run(task_runner, task_schedule, task_archive) __all__ = ["SequentialRunner"] From 4a80ab4bb8630ce8a1079a68f5221df353b6cfee Mon Sep 17 00:00:00 2001 From: Tim-Oliver Husser Date: Sat, 23 Dec 2023 11:57:08 +0100 Subject: [PATCH 13/14] 1.9.0 --- CHANGELOG.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9dd9ea29..061929f4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,7 @@ +v1.9.0 (2023-12-23) +******************* +* Added getters and safe getters for Image class. + v1.3.0 (2023-02-04) ******************* * Adopted LCO default task to new LCO portal. From 558f4a6519156b3feaab8a09a5a5e0a78a3ad760 Mon Sep 17 00:00:00 2001 From: Tim-Oliver Husser Date: Sat, 23 Dec 2023 11:57:32 +0100 Subject: [PATCH 14/14] v1.9.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 13af23c3..cde4edef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "pyobs-core" packages = [{ include = "pyobs" }] -version = "1.8.0" +version = "1.9.0" description = "robotic telescope software" authors = ["Tim-Oliver Husser "] license = "MIT"