From 7e8901be6ddc72f38f9ca831cfa8d91410a4a1fc Mon Sep 17 00:00:00 2001 From: pgriffin17 <31374077+pgriffin17@users.noreply.github.com> Date: Tue, 9 Jan 2024 18:14:41 -0600 Subject: [PATCH 01/33] Get ImageArray working with maxim, fix missing imports/variables in obs --- pyscope/observatory/maxim.py | 33 ++++++++++- pyscope/observatory/observatory.py | 91 +++++++++++++++++++++++------- 2 files changed, 102 insertions(+), 22 deletions(-) diff --git a/pyscope/observatory/maxim.py b/pyscope/observatory/maxim.py index 3fa703f9..7c0c2228 100755 --- a/pyscope/observatory/maxim.py +++ b/pyscope/observatory/maxim.py @@ -3,6 +3,7 @@ import time from astropy.time import Time +from win32com.client import Dispatch from .autofocus import Autofocus from .camera import Camera @@ -20,7 +21,7 @@ def __init__(self): raise Exception("This class is only available on Windows.") else: from win32com.client import Dispatch - + print("Running Windows") self._app = Dispatch("MaxIm.Application") self._app.LockApp = True @@ -191,7 +192,8 @@ def CanAsymmetricBin(self): @property def CanFastReadout(self): logger.debug("_MaximCameraCanFastReadout called") - raise NotImplementedError + # Not implemented, return False + return False @property def CanGetCoolerPower(self): @@ -233,6 +235,21 @@ def CoolerPower(self): logger.debug("_MaximCameraCoolerPower called") return self._com_object.CoolerPower + @property + def Description(self): + logger.debug("_MaximCameraDescription called") + return "MaxIm camera for pyscope" + + @property + def DriverVersion(self): + logger.debug("_MaximCameraDriverVersion called") + return "Custom pyscope MaxIm driver" + + @property + def DriverInfo(self): + logger.debug("_MaximCameraDriverInfo called") + return "Custom pyscope MaxIm driver" + @property def ElectronsPerADU(self): logger.debug("_MaximCameraElectronsPerADU called") @@ -291,7 +308,12 @@ def GainMin(self): @property def Gains(self): logger.debug("_MaximCameraGains called") - return self._com_object.Speeds + return self._com_object.Speeds.replace("(", "").replace(")", "") + + @property + def HasFilterWheel(self): + logger.debug("_MaximCameraHasFilterWheel called") + return self._com_object.HasFilterWheel @property def HasShutter(self): @@ -318,6 +340,11 @@ def ImageReady(self): logger.debug("_MaximCameraImageReady called") return self._com_object.ImageReady + @property + def InterfaceVersion(self): + logger.debug("_MaximCameraInterfaceVersion called") + raise NotImplementedError + @property def IsPulseGuiding(self): logger.debug("_MaximCameraIsPulseGuiding called") diff --git a/pyscope/observatory/observatory.py b/pyscope/observatory/observatory.py index e526962d..f3178200 100644 --- a/pyscope/observatory/observatory.py +++ b/pyscope/observatory/observatory.py @@ -7,12 +7,15 @@ import tempfile import threading import time +import os +import datetime from ast import literal_eval import numpy as np from astropy import coordinates as coord from astropy import time as astrotime from astropy import units as u +from astropy import wcs as astropywcs from astropy.io import fits from astroquery.mpc import MPC @@ -1020,7 +1023,7 @@ def sun_altaz(self, t=None): if t is None: t = self.observatory_time else: - t = Time(t) + t = astrotime.Time(t) sun = coord.get_sun(t).transform_to( coord.AltAz(obstime=t, location=self.observatory_location) @@ -1036,7 +1039,7 @@ def moon_altaz(self, t=None): if t is None: t = self.observatory_time else: - t = Time(t) + t = astrotime.Time(t) moon = coord.get_body("moon", t).transform_to( coord.AltAz(obstime=t, location=self.observatory_location) @@ -1915,7 +1918,7 @@ def recenter( logger.info("Settling for %.2f seconds" % self.settle_time) time.sleep(self.settle_time) - if not check_and_refine and attempt_number > 0: + if not check_and_refine and attempt > 0: logger.info( "Check and recenter is off, single-shot recentering complete" ) @@ -1937,7 +1940,7 @@ def recenter( logger.info("Searching for a WCS solution...") if type(self._wcs) is WCS: self._wcs.Solve( - filename, + temp_image, ra_key="TELRAIC", dec_key="TELDECIC", ra_dec_units=("hour", "deg"), @@ -1952,7 +1955,7 @@ def recenter( else: for i, wcs in enumerate(self._wcs): solution_found = wcs.Solve( - filename, + temp_image, ra_key="TELRAIC", dec_key="TELDECIC", ra_dec_units=("hour", "deg"), @@ -1980,7 +1983,7 @@ def recenter( ) try: hdulist = fits.open(temp_image) - w = astropy.wcs.WCS(hdulist[0].header) + w = astropywcs.WCS(hdulist[0].header) center_coord = w.pixel_to_world( int(self.camera.CameraXSize / 2), int(self.camera.CameraYSize / 2) @@ -2035,7 +2038,7 @@ def recenter( logger.debug("Error in x pixels is %.2f" % error_x_pixels) logger.debug("Error in y pixels is %.2f" % error_y_pixels) - if max(error_x_pixels, error_y_pixels) <= max_pixel_error: + if max(error_x_pixels, error_y_pixels) <= tolerance: break logger.info("Offsetting next slew coordinates") @@ -2455,9 +2458,9 @@ def camera_info(self): "CAMERA": (self.camera.Name, "Name of camera"), "CAMDRVER": (self.camera.DriverVersion, "Camera driver version"), "CAMDRV": (self.camera.DriverInfo[0], "Camera driver info"), - "CAMINTF": (self.camera.InterfaceVersion, "Camera interface version"), + "CAMINTF": (None, "Camera interface version"), "CAMDESC": (self.camera.Description, "Camera description"), - "SENSOR": (self.camera.SensorName, "Name of sensor"), + "SENSOR": (None, "Name of sensor"), "WIDTH": (self.camera.CameraXSize, "Width of sensor in pixels"), "HEIGHT": (self.camera.CameraYSize, "Height of sensor in pixels"), "XPIXSIZE": (self.camera.PixelSizeX, "Pixel width in microns"), @@ -2470,10 +2473,10 @@ def camera_info(self): self.camera.HasShutter, "Whether a camera mechanical shutter is present", ), - "MINEXP": (self.camera.ExposureMin, "Minimum exposure time [seconds]"), - "MAXEXP": (self.camera.ExposureMax, "Maximum exposure time [seconds]"), + "MINEXP": (None, "Minimum exposure time [seconds]"), + "MAXEXP": (None, "Maximum exposure time [seconds]"), "EXPRESL": ( - self.camera.ExposureResolution, + None, "Exposure time resolution [seconds]", ), "MAXBINSX": (self.camera.MaxBinX, "Maximum binning factor in width"), @@ -2487,11 +2490,11 @@ def camera_info(self): "Can camera set temperature", ), "CANPULSE": (self.camera.CanPulseGuide, "Can camera pulse guide"), - "FULLWELL": (self.camera.FullWellCapacity, "Full well capacity [e-]"), - "MAXADU": (self.camera.MaxADU, "Camera maximum ADU value possible"), - "E-ADU": (self.camera.ElectronsPerADU, "Gain [e- per ADU]"), + "FULLWELL": (None, "Full well capacity [e-]"), + "MAXADU": (None, "Camera maximum ADU value possible"), + "E-ADU": (None, "Gain [e- per ADU]"), "EGAIN": (None, "Electronic gain"), - "CANFASTR": (self.camera.CanFastReadout, "Can camera fast readout"), + "CANFASTR": (None, "Can camera fast readout"), "READMDS": (None, "Possible readout modes"), "GAINS": (None, "Possible electronic gains"), "GAINMIN": (None, "Minimum possible electronic gain"), @@ -2499,7 +2502,7 @@ def camera_info(self): "OFFSETS": (None, "Possible offsets"), "OFFSETMN": (None, "Minimum possible offset"), "OFFSETMX": (None, "Maximum possible offset"), - "CAMSUPAC": (str(self.camera.SupportedActions), "Camera supported actions"), + "CAMSUPAC": (None, "Camera supported actions"), } try: info["PCNTCOMP"] = (self.camera.PercentCompleted, info["PCNTCOMP"][1]) @@ -2603,10 +2606,47 @@ def camera_info(self): info["CMOS-TMP"] = (self.camera.CMOSTemperature, info["CMOS-TMP"][1]) except: pass + try: + info["CANINTF"] = (self.camera.InterfaceVersion, info["CANINTF"][1]) + except: + pass + try: + info["SENSOR"] = (self.camera.SensorName, info["SENSOR"][1]) + except: + pass + try: + info["MINEXP"] = (self.camera.ExposureMin, info["MINEXP"][1]) + info["MAXEXP"] = (self.camera.ExposureMax, info["MAXEXP"][1]) + except: + pass + try: + info["EXPRESL"] = (self.camera.ExposureResolution, info["EXPRESL"][1]) + except: + pass + try: + info["FULLWELL"] = (self.camera.FullWellCapacity, info["FULLWELL"][1]) + except: + pass + try: + info["MAXADU"] = (self.camera.MaxADU, info["MAXADU"][1]) + except: + pass + try: + info["E-ADU"] = (self.camera.ElectronsPerADU, info["E-ADU"][1]) + except: + pass try: info["EGAIN"] = (self.camera.Gain, info["EGAIN"][1]) except: pass + try: + info["CANFASTR"] = (self.camera.CanFastReadout, info["CANFASTR"][1]) + except: + pass + try: + info["CAMSUPAC"] = (str(self.camera.SupportedActions), info["CAMSUPAC"][1]) + except: + pass return info @@ -3421,10 +3461,16 @@ def telescope_info(self): ), "TELTRCKS": (str(self.telescope.TrackingRates), "Telescope tracking rates"), "TELSUPAC": ( - str(self.telescope.SupportedActions), + None, "Telescope supported actions", ), } + # Sometimes, TELDRV has /r or /n in it and it breaks + # This is a hack to fix that + info["TELDRV"] = ( + info["TELDRV"][0].replace("\r", "\\r").replace("\n", "\\n"), + info["TELDRV"][1], + ) try: info["TELALT"] = (self.telescope.Altitude, info["TELALT"][1]) except: @@ -3585,6 +3631,13 @@ def telescope_info(self): ) except: pass + try: + info["TELSUPAC"] = ( + str(self.telescope.SupportedActions), + info["TELSUPAC"][1], + ) + except: + pass return info @property @@ -3944,7 +3997,7 @@ def filters(self, value, position=None): ) else: self._filters[position] = ( - char(value) if value is not None or value != "" else None + chr(value) if value is not None or value != "" else None ) self._config["filter_wheel"]["filters"] = ( ", ".join(self._filters) if self._filters is not None else "" From 7fe020f729028b1f83923cabd0b2e4e3c751bfb6 Mon Sep 17 00:00:00 2001 From: pgriffin17 <31374077+pgriffin17@users.noreply.github.com> Date: Tue, 9 Jan 2024 18:18:06 -0600 Subject: [PATCH 02/33] run black/isort --- pyscope/observatory/maxim.py | 1 + pyscope/observatory/observatory.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pyscope/observatory/maxim.py b/pyscope/observatory/maxim.py index 7c0c2228..72ae067b 100755 --- a/pyscope/observatory/maxim.py +++ b/pyscope/observatory/maxim.py @@ -21,6 +21,7 @@ def __init__(self): raise Exception("This class is only available on Windows.") else: from win32com.client import Dispatch + print("Running Windows") self._app = Dispatch("MaxIm.Application") self._app.LockApp = True diff --git a/pyscope/observatory/observatory.py b/pyscope/observatory/observatory.py index f3178200..76dc0695 100644 --- a/pyscope/observatory/observatory.py +++ b/pyscope/observatory/observatory.py @@ -1,14 +1,14 @@ import configparser +import datetime import importlib import json import logging +import os import shutil import sys import tempfile import threading import time -import os -import datetime from ast import literal_eval import numpy as np From a4c62241e71f4c97a420d3ca97b782acbe0c2e8b Mon Sep 17 00:00:00 2001 From: pgriffin17 <31374077+pgriffin17@users.noreply.github.com> Date: Wed, 10 Jan 2024 21:51:11 -0600 Subject: [PATCH 03/33] Added click script to solve images in a folder using pinpoint in MaxIm --- pyscope/utils/pinpoint_solve.py | 55 +++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 pyscope/utils/pinpoint_solve.py diff --git a/pyscope/utils/pinpoint_solve.py b/pyscope/utils/pinpoint_solve.py new file mode 100644 index 00000000..c23fbc50 --- /dev/null +++ b/pyscope/utils/pinpoint_solve.py @@ -0,0 +1,55 @@ +import os +import time +import logging +import click +from win32com.client import Dispatch + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +def save_image(filepath): + maxim.SaveFile(filepath, 3, False, 1) + +def open_image(filepath): + maxim.OpenFile(filepath) + +def platesolve_image(filepath, new_filepath): + open_image(filepath) + maxim.PinPointSolve() + try: + while maxim.PinPointStatus == 3: + time.sleep(0.1) + if maxim.PinPointStatus == 2: + logger.info('Solve successful') + else: + logger.info('Solve failed') + maxim.PinPointStop() + except Exception as e: + logger.error(f'Solve failed: {e}, saving unsolved image') + save_image(new_filepath) + logger.info(f'Saved to {new_filepath}') + +@click.command() +@click.argument('input_dir', type=click.Path(exists=True), + help="""Directory containing images to solve.""") +@click.option('-o','--out-dir','output_dir', default=None, type=click.Path(), + help="""Directory to save solved images to. If not specified, solved images will be saved to the same directory as the input images.""") +def pinpoint_solve(input_dir, output_dir=None): + if output_dir is None: + output_dir = input_dir + day_images = os.listdir(input_dir) + day_filepaths = [os.path.join(input_dir, filename) for filename in day_images] + new_filepaths = [os.path.join(output_dir, filename) for filename in day_images] + + global maxim + maxim = Dispatch("Maxim.Document") + + # Create end_dir if it doesn't exist + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + for filepath, new_filepath in zip(day_filepaths, new_filepaths): + platesolve_image(filepath, new_filepath) + +if __name__ == '__main__': + pinpoint_solve() \ No newline at end of file From 46dfb14487a406e971277b971ea439c0db8fe8e2 Mon Sep 17 00:00:00 2001 From: pgriffin17 <31374077+pgriffin17@users.noreply.github.com> Date: Wed, 10 Jan 2024 21:53:39 -0600 Subject: [PATCH 04/33] ran black and isort --- pyscope/utils/pinpoint_solve.py | 37 +++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/pyscope/utils/pinpoint_solve.py b/pyscope/utils/pinpoint_solve.py index c23fbc50..254a0372 100644 --- a/pyscope/utils/pinpoint_solve.py +++ b/pyscope/utils/pinpoint_solve.py @@ -1,18 +1,22 @@ +import logging import os import time -import logging + import click from win32com.client import Dispatch logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) + def save_image(filepath): maxim.SaveFile(filepath, 3, False, 1) + def open_image(filepath): maxim.OpenFile(filepath) + def platesolve_image(filepath, new_filepath): open_image(filepath) maxim.PinPointSolve() @@ -20,20 +24,30 @@ def platesolve_image(filepath, new_filepath): while maxim.PinPointStatus == 3: time.sleep(0.1) if maxim.PinPointStatus == 2: - logger.info('Solve successful') + logger.info("Solve successful") else: - logger.info('Solve failed') + logger.info("Solve failed") maxim.PinPointStop() except Exception as e: - logger.error(f'Solve failed: {e}, saving unsolved image') + logger.error(f"Solve failed: {e}, saving unsolved image") save_image(new_filepath) - logger.info(f'Saved to {new_filepath}') + logger.info(f"Saved to {new_filepath}") + @click.command() -@click.argument('input_dir', type=click.Path(exists=True), - help="""Directory containing images to solve.""") -@click.option('-o','--out-dir','output_dir', default=None, type=click.Path(), - help="""Directory to save solved images to. If not specified, solved images will be saved to the same directory as the input images.""") +@click.argument( + "input_dir", + type=click.Path(exists=True), + help="""Directory containing images to solve.""", +) +@click.option( + "-o", + "--out-dir", + "output_dir", + default=None, + type=click.Path(), + help="""Directory to save solved images to. If not specified, solved images will be saved to the same directory as the input images.""", +) def pinpoint_solve(input_dir, output_dir=None): if output_dir is None: output_dir = input_dir @@ -51,5 +65,6 @@ def pinpoint_solve(input_dir, output_dir=None): for filepath, new_filepath in zip(day_filepaths, new_filepaths): platesolve_image(filepath, new_filepath) -if __name__ == '__main__': - pinpoint_solve() \ No newline at end of file + +if __name__ == "__main__": + pinpoint_solve() From 5e8c08179e3a57617531eae572440d9f51baefd7 Mon Sep 17 00:00:00 2001 From: pgriffin17 <31374077+pgriffin17@users.noreply.github.com> Date: Wed, 10 Jan 2024 21:55:27 -0600 Subject: [PATCH 05/33] setup logger to match pyscope --- pyscope/utils/pinpoint_solve.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyscope/utils/pinpoint_solve.py b/pyscope/utils/pinpoint_solve.py index 254a0372..ab9499a4 100644 --- a/pyscope/utils/pinpoint_solve.py +++ b/pyscope/utils/pinpoint_solve.py @@ -5,7 +5,6 @@ import click from win32com.client import Dispatch -logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) From c29d90b329b8e9a9f61dc5c1967ee3e2f5b02b78 Mon Sep 17 00:00:00 2001 From: pgriffin17 <31374077+pgriffin17@users.noreply.github.com> Date: Thu, 11 Jan 2024 19:56:27 -0600 Subject: [PATCH 06/33] Tested with MaxIm, added logging to console --- pyscope/utils/pinpoint_solve.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/pyscope/utils/pinpoint_solve.py b/pyscope/utils/pinpoint_solve.py index ab9499a4..1eb5d55a 100644 --- a/pyscope/utils/pinpoint_solve.py +++ b/pyscope/utils/pinpoint_solve.py @@ -5,7 +5,18 @@ import click from win32com.client import Dispatch +# Remove default stream handler +logging.getLogger().handlers = [] + +logging.basicConfig(level=logging.INFO, format="%(message)s") logger = logging.getLogger(__name__) +logger.propagate = False + + +def setup_logger(): + # Add a StreamHandler to log to the console + console_handler = logging.StreamHandler() + logger.addHandler(console_handler) def save_image(filepath): @@ -19,6 +30,7 @@ def open_image(filepath): def platesolve_image(filepath, new_filepath): open_image(filepath) maxim.PinPointSolve() + logger.info(f"Attempting to solve {filepath}") try: while maxim.PinPointStatus == 3: time.sleep(0.1) @@ -34,7 +46,8 @@ def platesolve_image(filepath, new_filepath): @click.command() -@click.argument( +@click.option( + "-i", "input_dir", type=click.Path(exists=True), help="""Directory containing images to solve.""", @@ -48,8 +61,14 @@ def platesolve_image(filepath, new_filepath): help="""Directory to save solved images to. If not specified, solved images will be saved to the same directory as the input images.""", ) def pinpoint_solve(input_dir, output_dir=None): + # Add input_dir to the end of the current working directory if it is not an absolute path + if not os.path.isabs(input_dir): + input_dir = os.path.join(os.getcwd(), input_dir) if output_dir is None: output_dir = input_dir + else: + if not os.path.isabs(output_dir): + output_dir = os.path.join(os.getcwd(), output_dir) day_images = os.listdir(input_dir) day_filepaths = [os.path.join(input_dir, filename) for filename in day_images] new_filepaths = [os.path.join(output_dir, filename) for filename in day_images] @@ -66,4 +85,5 @@ def pinpoint_solve(input_dir, output_dir=None): if __name__ == "__main__": + setup_logger() pinpoint_solve() From fc04030a8940572b7314e7f8be16b3ddf6a779c2 Mon Sep 17 00:00:00 2001 From: pgriffin17 <31374077+pgriffin17@users.noreply.github.com> Date: Thu, 11 Jan 2024 20:25:01 -0600 Subject: [PATCH 07/33] Updated docs and logging --- pyscope/utils/__init__.py | 2 + pyscope/utils/pinpoint_solve.py | 69 ++++++++++++++++++++++++++------- 2 files changed, 56 insertions(+), 15 deletions(-) diff --git a/pyscope/utils/__init__.py b/pyscope/utils/__init__.py index 112b7c3b..c2a1fba7 100644 --- a/pyscope/utils/__init__.py +++ b/pyscope/utils/__init__.py @@ -4,8 +4,10 @@ from ._html_line_parser import _get_number_from_line from .airmass import airmass from .pyscope_exception import PyscopeException +from .pinpoint_solve import pinpoint_solve __all__ = [ "airmass", + "pinpoint_solve", "PyscopeException", ] diff --git a/pyscope/utils/pinpoint_solve.py b/pyscope/utils/pinpoint_solve.py index 1eb5d55a..b5ceae90 100644 --- a/pyscope/utils/pinpoint_solve.py +++ b/pyscope/utils/pinpoint_solve.py @@ -5,18 +5,7 @@ import click from win32com.client import Dispatch -# Remove default stream handler -logging.getLogger().handlers = [] - -logging.basicConfig(level=logging.INFO, format="%(message)s") logger = logging.getLogger(__name__) -logger.propagate = False - - -def setup_logger(): - # Add a StreamHandler to log to the console - console_handler = logging.StreamHandler() - logger.addHandler(console_handler) def save_image(filepath): @@ -60,7 +49,54 @@ def platesolve_image(filepath, new_filepath): type=click.Path(), help="""Directory to save solved images to. If not specified, solved images will be saved to the same directory as the input images.""", ) -def pinpoint_solve(input_dir, output_dir=None): +@click.option( + "-v", + "--verbose", + count=True, + type=click.IntRange(0, 1), + default=0, + help="""Verbosity level. -v prints info messages""", +) +def pinpoint_solve_cli(input_dir, output_dir=None, verbose=-1): + """ Platesolve images in input_dir and save them to output_dir. \b + + Platesolve images in input_dir and save them to output_dir. If output_dir is not specified, solved images will be saved to the same directory as the input images. + Usage: python pinpoint_solve.py -i input_dir -o output_dir + + Parameters + ---------- + input_dir : str + Directory containing images to solve. + output_dir : str (optional) + Directory to save solved images to. If not specified, solved images will be saved to the same directory as the input images. + verboxe : int (optional), default=-1 + Verbosity level. -v prints info messages + + Returns + ------- + None + + Examples + -------- + File directory structure:: + + cwd/ + test_images/ + image1.fit + image2.fit + image3.fit + solved_images/ + + Command + `python pinpoint_solve.py -i "test_images" -o "solved_images"` + + .. Note:: + You may also pass in absolute paths for `input_dir` and `output_dir`. + """ + if verbose > -1: + logger.setLevel(int(10 * (2-verbose))) + logger.addHandler(logging.StreamHandler()) + logger.debug(f"Starting pinpoint_solve_cli({input_dir}, {output_dir})") # Add input_dir to the end of the current working directory if it is not an absolute path if not os.path.isabs(input_dir): input_dir = os.path.join(os.getcwd(), input_dir) @@ -83,7 +119,10 @@ def pinpoint_solve(input_dir, output_dir=None): for filepath, new_filepath in zip(day_filepaths, new_filepaths): platesolve_image(filepath, new_filepath) + logger.debug(f"Finished pinpoint_solve_cli({input_dir}, {output_dir})") + +pinpoint_solve = pinpoint_solve_cli.callback + -if __name__ == "__main__": - setup_logger() - pinpoint_solve() +# if __name__ == "__main__": +# pinpoint_solve_cli() \ No newline at end of file From 136726f092ecfc6626a09a926d343b91a54444f1 Mon Sep 17 00:00:00 2001 From: pgriffin17 <31374077+pgriffin17@users.noreply.github.com> Date: Thu, 11 Jan 2024 20:25:33 -0600 Subject: [PATCH 08/33] black/isort --- pyscope/utils/__init__.py | 2 +- pyscope/utils/pinpoint_solve.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pyscope/utils/__init__.py b/pyscope/utils/__init__.py index c2a1fba7..0b752d78 100644 --- a/pyscope/utils/__init__.py +++ b/pyscope/utils/__init__.py @@ -3,8 +3,8 @@ from ._get_image_source_catalog import _get_image_source_catalog from ._html_line_parser import _get_number_from_line from .airmass import airmass -from .pyscope_exception import PyscopeException from .pinpoint_solve import pinpoint_solve +from .pyscope_exception import PyscopeException __all__ = [ "airmass", diff --git a/pyscope/utils/pinpoint_solve.py b/pyscope/utils/pinpoint_solve.py index b5ceae90..e256dbd6 100644 --- a/pyscope/utils/pinpoint_solve.py +++ b/pyscope/utils/pinpoint_solve.py @@ -58,7 +58,7 @@ def platesolve_image(filepath, new_filepath): help="""Verbosity level. -v prints info messages""", ) def pinpoint_solve_cli(input_dir, output_dir=None, verbose=-1): - """ Platesolve images in input_dir and save them to output_dir. \b + """Platesolve images in input_dir and save them to output_dir. \b Platesolve images in input_dir and save them to output_dir. If output_dir is not specified, solved images will be saved to the same directory as the input images. Usage: python pinpoint_solve.py -i input_dir -o output_dir @@ -86,7 +86,7 @@ def pinpoint_solve_cli(input_dir, output_dir=None, verbose=-1): image2.fit image3.fit solved_images/ - + Command `python pinpoint_solve.py -i "test_images" -o "solved_images"` @@ -94,7 +94,7 @@ def pinpoint_solve_cli(input_dir, output_dir=None, verbose=-1): You may also pass in absolute paths for `input_dir` and `output_dir`. """ if verbose > -1: - logger.setLevel(int(10 * (2-verbose))) + logger.setLevel(int(10 * (2 - verbose))) logger.addHandler(logging.StreamHandler()) logger.debug(f"Starting pinpoint_solve_cli({input_dir}, {output_dir})") # Add input_dir to the end of the current working directory if it is not an absolute path @@ -121,8 +121,9 @@ def pinpoint_solve_cli(input_dir, output_dir=None, verbose=-1): logger.debug(f"Finished pinpoint_solve_cli({input_dir}, {output_dir})") + pinpoint_solve = pinpoint_solve_cli.callback # if __name__ == "__main__": -# pinpoint_solve_cli() \ No newline at end of file +# pinpoint_solve_cli() From ad41c913a27d58f45f0059e582107522f4a5a691 Mon Sep 17 00:00:00 2001 From: pgriffin17 <31374077+pgriffin17@users.noreply.github.com> Date: Sat, 13 Jan 2024 14:54:06 -0600 Subject: [PATCH 09/33] Verified functionality of pinpoint_wcs --- pyscope/observatory/pinpoint_wcs.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/pyscope/observatory/pinpoint_wcs.py b/pyscope/observatory/pinpoint_wcs.py index c546c6d7..3d8c3337 100644 --- a/pyscope/observatory/pinpoint_wcs.py +++ b/pyscope/observatory/pinpoint_wcs.py @@ -2,6 +2,9 @@ import platform import time +import astropy.coordinates as coord +import astropy.io.fits as pyfits + logger = logging.getLogger(__name__) from ..utils import _force_async @@ -29,7 +32,7 @@ def Solve( ra_dec_units=("hour", "deg"), solve_timeout=60, catalog=3, - catalog_path="C:\GSC-1.1", + catalog_path="C:\\GSC11", ): logger.debug( f"""PinpointWCS.Solve( @@ -40,13 +43,15 @@ def Solve( self._solver.AttachFITS(filepath) - if kwargs.get("ra", None) is None or kwargs.get("dec", None) is None: + if ra is None or dec is None: with pyfits.open(filepath) as hdul: ra = hdul[0].header[ra_key] dec = hdul[0].header[dec_key] else: - ra = kwargs.get("ra", self._solver.TargetRightAscension) - dec = kwargs.get("dec", self._solver.TargetDeclination) + ra = self._solver.TargetRightAscension + dec = self._solver.TargetDeclination + + print(f"RA: {ra}, DEC: {dec}") obj = coord.SkyCoord(ra, dec, unit=ra_dec_units, frame="icrs") self._solver.RightAscension = obj.ra.hour @@ -54,11 +59,12 @@ def Solve( self._solver.ArcsecPerPixelHoriz = scale_est self._solver.ArcsecPerPixelVert = scale_est - self.Catalog = catalog - self.CatalogPath = catalog_path + self._solver.Catalog = catalog + self._solver.CatalogPath = catalog_path solved = None - solved = self._async_solve() + # solved = self._async_solve() + solved = self._solver.Solve() start_solve = time.time() while time.time() - start_solve < solve_timeout and solved is None: time.sleep(1) @@ -66,7 +72,10 @@ def Solve( if solved is None: raise Exception("Pinpoint solve timed out.") else: - return solved + print("Solved!") + print(f"RA: {self._solver.RightAscension}") + print(f"DEC: {self._solver.Declination}") + # return solved self._solver.UpdateFITS() self._solver.DetachFITS() @@ -74,4 +83,5 @@ def Solve( @_force_async def _async_solve(self): - self.Solver.Solve() + # Currently doesn't work properly. Needed to synchronously solve. + self._solver.Solve() From f3e342f9fcb2b59b54644e78e88831cb978e93ed Mon Sep 17 00:00:00 2001 From: pgriffin17 <31374077+pgriffin17@users.noreply.github.com> Date: Sat, 13 Jan 2024 15:04:28 -0600 Subject: [PATCH 10/33] Updated PinPoint documentation --- pyscope/observatory/pinpoint_wcs.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyscope/observatory/pinpoint_wcs.py b/pyscope/observatory/pinpoint_wcs.py index 3d8c3337..a7aff358 100644 --- a/pyscope/observatory/pinpoint_wcs.py +++ b/pyscope/observatory/pinpoint_wcs.py @@ -12,6 +12,13 @@ class PinpointWCS(WCS): + """PinpointWCS is a wrapper around the PinPoint plate solver. \b + + PinPoint is a commercial plate solver. This class is a wrapper around the + COM interface to PinPoint. The 64-bit version of PinPoint is required (it is + a separate download from the 32-bit version, and may be called an add-on). + """ + def __init__(self): logger.debug("PinpointWCS.__init__() called") if platform.system() != "Windows": From a719704f0b2eda0a55bcf252f2f995dd130404e1 Mon Sep 17 00:00:00 2001 From: pgriffin17 <31374077+pgriffin17@users.noreply.github.com> Date: Sat, 13 Jan 2024 18:26:23 -0600 Subject: [PATCH 11/33] Add SaveImageAsFITS and VerifyLatestExposure from legacy pyscope --- pyscope/observatory/maxim.py | 42 ++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/pyscope/observatory/maxim.py b/pyscope/observatory/maxim.py index 72ae067b..d2f87b75 100755 --- a/pyscope/observatory/maxim.py +++ b/pyscope/observatory/maxim.py @@ -125,6 +125,10 @@ def PulseGuide(self, Direction, Duration): ) raise NotImplementedError + def SaveImageAsFits(self, filename): + logger.debug(f"SaveImageAsFits called with filename={filename}") + self._com_object.SaveImage(filename) + def StartExposure(self, Duration, Light): logger.debug(f"StartExposure called with Duration={Duration}, Light={Light}") self._last_exposure_duration = Duration @@ -135,6 +139,44 @@ def StopExposure(self): logger.debug("_MaximCameraStopExposure called") self._com_object.AbortExposure() + def VerifyLatestExposure(self): + """Verify that the last exposure is complete. \b + + Make sure that the image that was returned by Maxim was in fact generated + by the most recent call to Expose(). I have seen cases where a camera + dropout occurs (e.g. if a USB cable gets unplugged and plugged back in) + where Maxim will claim that an exposure is complete but just return the + same (old) image over and over again. + + Return without error if the image from Maxim appears to be newer than + the supplied UTC datetime object based on the DATE-OBS header. + Raise an exception if the image is older or if there is an error + accessing the image. + """ + logger.debug("_MaximCameraVerifyLatestExposure called") + + try: + # Change to document + image = self._app.Document + except Exception as e: + raise Exception(f"Unable to access MaxIm camera image: {e}") + + if image is None: + raise Exception("No current image available from MaxIm") + + # Get the DATE-OBS header + image_timestamp = image.GetFITSKey["DATE-OBS"] + image_datetime = Time(image_timestamp, format="fits") + + logger.debug(f"Image timestamp: {image_timestamp}") + logger.debug(f"Image timestamp UTC: {image_datetime}") + logger.debug(f"Exposure start time: {self._last_exposure_start_time}") + + if image_datetime < self._last_exposure_start_time: + raise Exception( + "Image is too old; possibly the result of an earlier exposure. There may be a connection problem with the camera" + ) + @property def BayerOffsetX(self): logger.debug("_MaximCameraBayerOffsetX called") From 4d5022159f4a858b7636eb7eda5abb4798d47beb Mon Sep 17 00:00:00 2001 From: pgriffin17 <31374077+pgriffin17@users.noreply.github.com> Date: Sat, 13 Jan 2024 18:27:02 -0600 Subject: [PATCH 12/33] Changes to allow MaxIm image saving to preserve header info --- pyscope/observatory/observatory.py | 206 +++++++++++++++++++++-------- 1 file changed, 152 insertions(+), 54 deletions(-) diff --git a/pyscope/observatory/observatory.py b/pyscope/observatory/observatory.py index 76dc0695..2cbbdf53 100644 --- a/pyscope/observatory/observatory.py +++ b/pyscope/observatory/observatory.py @@ -35,6 +35,7 @@ def __init__(self, config_path=None, **kwargs): logger.debug("config_path: %s" % config_path) logger.debug("kwargs: %s" % kwargs) + # TODO: Add allowed_overwrite keys to config file and parser to check which keys can be overwritten (especially from MaxIm). self._config = configparser.ConfigParser() self._config["site"] = {} self._config["camera"] = {} @@ -1150,6 +1151,89 @@ def get_current_object(self): frame=coord.FK4(equinox="B1950"), ) return obj + + def generate_header_dict(self): + """Generates the header information for the observatory as a dictionary + + Returns + ------- + dict + The header information + """ + hdr_dict = {} + hdr_dict.update(self.observatory_info) + hdr_dict.update(self.camera_info) + hdr_dict.update(self.telescope_info) + hdr_dict.update(self.cover_calibrator_info) + hdr_dict.update(self.dome_info) + hdr_dict.update(self.filter_wheel_info) + hdr_dict.update(self.focuser_info) + hdr_dict.update(self.observing_conditions_info) + hdr_dict.update(self.rotator_info) + hdr_dict.update(self.safety_monitor_info) + hdr_dict.update(self.switch_info) + hdr_dict.update(self.threads_info) + hdr_dict.update(self.autofocus_info) + hdr_dict.update(self.wcs_info) + + return hdr_dict + + def generate_header_info(self, filename, frametyp=None, custom_header=None, history=None, maxim=False, allowed_overwrite=[]): + """Generates the header information for the observatory""" + if not maxim: + hdr = fits.Header() + else: + hdr = fits.getheader(filename) + + # The commented out part is unnecessary, as the fits header is automatically generated + # when the image is saved. + # hdr["SIMPLE"] = True + # hdr["BITPIX"] = (16, "8 unsigned int, 16 & 32 int, -32 & -64 real") + # hdr["NAXIS"] = (2, "number of axes") + # hdr["NAXIS1"] = (len(img_array), "fastest changing axis") + # hdr["NAXIS2"] = ( + # len(img_array[0]), + # "next to fastest changing axis", + # ) + hdr["BSCALE"] = (1, "physical=BZERO + BSCALE*array_value") + hdr["BZERO"] = (32768, "physical=BZERO + BSCALE*array_value") + if maxim: + hdr["SWUPDATE"] = ("pyscope", "Software used to update file") + hdr["SWVERSIO"] = (__version__, "Version of software used to update file") + else: + hdr["SWCREATE"] = ("pyscope", "Software used to create file") + hdr["SWVERSIO"] = (__version__, "Version of software used to create file") + hdr["ROWORDER"] = ("TOP-DOWN", "Row order of image") + + if frametyp is not None: + hdr["FRAMETYP"] = (frametyp, "Frame type") + elif self.last_camera_shutter_status: + hdr["FRAMETYP"] = ("Light", "Frame type") + elif not self.last_camera_shutter_status: + hdr["FRAMETYP"] = ("Dark", "Frame type") + + hdr_dict = self.generate_header_dict() + + if custom_header is not None: + hdr_dict.update(custom_header) + + self.safe_update_header(hdr, hdr_dict, maxim=maxim, allowed_overwrite=allowed_overwrite) + + if history is not None: + if type(history) is str: + history = [history] + for hist in history: + hdr["HISTORY"] = hist + + return hdr + + def safe_update_header(self, hdr, hdr_dict, maxim=False, allowed_overwrite=[]): + """Safely updates the header information""" + if maxim: + # Only keep the allowed_overwrite keys in the hdr_dict + hdr_dict = {k: v for k, v in hdr_dict.items() if k in allowed_overwrite} + hdr.update(hdr_dict) + def save_last_image( self, @@ -1171,60 +1255,74 @@ def save_last_image( if not self.camera.ImageReady: logger.exception("Image is not ready, cannot be saved") return False - - # Read out the image array - img_array = self.camera.ImageArray - - if img_array is None or len(img_array) == 0 or len(img_array) == 0: - logger.exception("Image array is empty, cannot be saved") - return False - - hdr = fits.Header() - - hdr["SIMPLE"] = True - hdr["BITPIX"] = (16, "8 unsigned int, 16 & 32 int, -32 & -64 real") - hdr["NAXIS"] = (2, "number of axes") - hdr["NAXIS1"] = (len(img_array), "fastest changing axis") - hdr["NAXIS2"] = ( - len(img_array[0]), - "next to fastest changing axis", - ) - hdr["BSCALE"] = (1, "physical=BZERO + BSCALE*array_value") - hdr["BZERO"] = (32768, "physical=BZERO + BSCALE*array_value") - hdr["SWCREATE"] = ("pyscope", "Software used to create file") - hdr["SWVERSIO"] = (__version__, "Version of software used to create file") - hdr["ROWORDER"] = ("TOP-DOWN", "Row order of image") - - if frametyp is not None: - hdr["FRAMETYP"] = (frametyp, "Frame type") - elif self.last_camera_shutter_status: - hdr["FRAMETYP"] = ("Light", "Frame type") - elif not self.last_camera_shutter_status: - hdr["FRAMETYP"] = ("Dark", "Frame type") - - hdr.update(self.observatory_info) - hdr.update(self.camera_info) - hdr.update(self.telescope_info) - hdr.update(self.cover_calibrator_info) - hdr.update(self.dome_info) - hdr.update(self.filter_wheel_info) - hdr.update(self.focuser_info) - hdr.update(self.observing_conditions_info) - hdr.update(self.rotator_info) - hdr.update(self.safety_monitor_info) - hdr.update(self.switch_info) - hdr.update(self.threads_info) - hdr.update(self.autofocus_info) - hdr.update(self.wcs_info) - - if custom_header is not None: - hdr.update(custom_header) - - if history is not None: - if type(history) is str: - history = [history] - for hist in history: - hdr["HISTORY"] = hist + + maxim = self.camera_driver.lower() in ("maxim", "maximdl") + + # If camera driver is Maxim, use Maxim to save the image + # This is because Maxim does not pass some of the header information + # to the camera object, so it is not available to be saved. + if not maxim: + # Read out the image array + img_array = self.camera.ImageArray + + if img_array is None or len(img_array) == 0 or len(img_array) == 0: + logger.exception("Image array is empty, cannot be saved") + return False + else: + logger.info("Using Maxim to save image") + self.camera.VerifyLatestExposure() + self.camera.SaveImageAsFits(filename) + img_array = fits.getdata(filename) + + # Moved below to separate function + # hdr = fits.Header() + + # hdr["SIMPLE"] = True + # hdr["BITPIX"] = (16, "8 unsigned int, 16 & 32 int, -32 & -64 real") + # hdr["NAXIS"] = (2, "number of axes") + # hdr["NAXIS1"] = (len(img_array), "fastest changing axis") + # hdr["NAXIS2"] = ( + # len(img_array[0]), + # "next to fastest changing axis", + # ) + # hdr["BSCALE"] = (1, "physical=BZERO + BSCALE*array_value") + # hdr["BZERO"] = (32768, "physical=BZERO + BSCALE*array_value") + # hdr["SWCREATE"] = ("pyscope", "Software used to create file") + # hdr["SWVERSIO"] = (__version__, "Version of software used to create file") + # hdr["ROWORDER"] = ("TOP-DOWN", "Row order of image") + + # if frametyp is not None: + # hdr["FRAMETYP"] = (frametyp, "Frame type") + # elif self.last_camera_shutter_status: + # hdr["FRAMETYP"] = ("Light", "Frame type") + # elif not self.last_camera_shutter_status: + # hdr["FRAMETYP"] = ("Dark", "Frame type") + + # hdr.update(self.observatory_info) + # hdr.update(self.camera_info) + # hdr.update(self.telescope_info) + # hdr.update(self.cover_calibrator_info) + # hdr.update(self.dome_info) + # hdr.update(self.filter_wheel_info) + # hdr.update(self.focuser_info) + # hdr.update(self.observing_conditions_info) + # hdr.update(self.rotator_info) + # hdr.update(self.safety_monitor_info) + # hdr.update(self.switch_info) + # hdr.update(self.threads_info) + # hdr.update(self.autofocus_info) + # hdr.update(self.wcs_info) + + # if custom_header is not None: + # hdr.update(custom_header) + + # if history is not None: + # if type(history) is str: + # history = [history] + # for hist in history: + # hdr["HISTORY"] = hist + + hdr = self.generate_header_info(filename, frametyp, custom_header, history) hdu = fits.PrimaryHDU(img_array, header=hdr) hdu.writeto(filename, overwrite=overwrite) From 00b3d182c00ad880397cdb3aa140f74de68aea4a Mon Sep 17 00:00:00 2001 From: pgriffin17 <31374077+pgriffin17@users.noreply.github.com> Date: Sat, 13 Jan 2024 19:31:08 -0600 Subject: [PATCH 13/33] Document ImageArray changes --- pyscope/observatory/ascom_camera.py | 35 +++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/pyscope/observatory/ascom_camera.py b/pyscope/observatory/ascom_camera.py index f01eec37..18e41abb 100644 --- a/pyscope/observatory/ascom_camera.py +++ b/pyscope/observatory/ascom_camera.py @@ -257,6 +257,41 @@ def HeatSinkTemperature(self): @property def ImageArray(self): + """Return the image array as a numpy array of the correct data type and in + standard FITS orientation. \b + + Return the image array as a numpy array of the correct data type. The + data type is determined by the MaxADU property. If the MaxADU property + is not defined, or if it is less than or equal to 65535, the data type + will be numpy.uint16. If the MaxADU property is greater than 65535, the + data type will be numpy.uint32. + + .. Note:: + The image array is returned in the standard FITS orientation, which + deviates from the ASCOM standard (see below). + + The image array is returned in the standard FITS orientation, with the + rows and columns transposed (if `_DoTranspose` is `True`). This is the same orientation as the + astropy.io.fits package. This is done because the ASCOM standard + specifies that the image array should be returned with the first index + being the column and the second index being the row. This is the + opposite of the FITS standard, which specifies that the first index + should be the row and the second index should be the column. The + astropy.io.fits package follows the FITS standard, so the image array + returned by the pyscope ASCOM driver is transposed to match the FITS + standard. + + Parameters + ---------- + None + + Returns + ------- + numpy.ndarray + The image array as a numpy array of the correct data type. + Rows and columns are transposed to match the FITS standard. + + """ logger.debug(f"ASCOMCamera.ImageArray property called") img_array = self._device.ImageArray # Convert to numpy array and check if it is the correct data type From 9719748c8fad496612c449b8e852c49781088a4d Mon Sep 17 00:00:00 2001 From: pgriffin17 <31374077+pgriffin17@users.noreply.github.com> Date: Sat, 13 Jan 2024 19:32:40 -0600 Subject: [PATCH 14/33] minor additions, log messages, black/isort --- pyscope/observatory/observatory.py | 35 ++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/pyscope/observatory/observatory.py b/pyscope/observatory/observatory.py index 2cbbdf53..b7d5654b 100644 --- a/pyscope/observatory/observatory.py +++ b/pyscope/observatory/observatory.py @@ -1151,10 +1151,10 @@ def get_current_object(self): frame=coord.FK4(equinox="B1950"), ) return obj - + def generate_header_dict(self): """Generates the header information for the observatory as a dictionary - + Returns ------- dict @@ -1173,12 +1173,20 @@ def generate_header_dict(self): hdr_dict.update(self.safety_monitor_info) hdr_dict.update(self.switch_info) hdr_dict.update(self.threads_info) - hdr_dict.update(self.autofocus_info) + hdr_dict.update(self.autofocus_info) hdr_dict.update(self.wcs_info) return hdr_dict - - def generate_header_info(self, filename, frametyp=None, custom_header=None, history=None, maxim=False, allowed_overwrite=[]): + + def generate_header_info( + self, + filename, + frametyp=None, + custom_header=None, + history=None, + maxim=False, + allowed_overwrite=[], + ): """Generates the header information for the observatory""" if not maxim: hdr = fits.Header() @@ -1217,7 +1225,9 @@ def generate_header_info(self, filename, frametyp=None, custom_header=None, hist if custom_header is not None: hdr_dict.update(custom_header) - self.safe_update_header(hdr, hdr_dict, maxim=maxim, allowed_overwrite=allowed_overwrite) + self.safe_update_header( + hdr, hdr_dict, maxim=maxim, allowed_overwrite=allowed_overwrite + ) if history is not None: if type(history) is str: @@ -1229,11 +1239,11 @@ def generate_header_info(self, filename, frametyp=None, custom_header=None, hist def safe_update_header(self, hdr, hdr_dict, maxim=False, allowed_overwrite=[]): """Safely updates the header information""" + logger.debug(f"Observatory.safe_update_header called") if maxim: # Only keep the allowed_overwrite keys in the hdr_dict hdr_dict = {k: v for k, v in hdr_dict.items() if k in allowed_overwrite} hdr.update(hdr_dict) - def save_last_image( self, @@ -1244,6 +1254,7 @@ def save_last_image( overwrite=False, custom_header=None, history=None, + allowed_overwrite=[], **kwargs, ): """Saves the current image""" @@ -1255,7 +1266,7 @@ def save_last_image( if not self.camera.ImageReady: logger.exception("Image is not ready, cannot be saved") return False - + maxim = self.camera_driver.lower() in ("maxim", "maximdl") # If camera driver is Maxim, use Maxim to save the image @@ -1267,13 +1278,13 @@ def save_last_image( if img_array is None or len(img_array) == 0 or len(img_array) == 0: logger.exception("Image array is empty, cannot be saved") - return False + return False else: logger.info("Using Maxim to save image") self.camera.VerifyLatestExposure() self.camera.SaveImageAsFits(filename) img_array = fits.getdata(filename) - + # Moved below to separate function # hdr = fits.Header() @@ -1322,7 +1333,9 @@ def save_last_image( # for hist in history: # hdr["HISTORY"] = hist - hdr = self.generate_header_info(filename, frametyp, custom_header, history) + hdr = self.generate_header_info( + filename, frametyp, custom_header, history, maxim, allowed_overwrite + ) hdu = fits.PrimaryHDU(img_array, header=hdr) hdu.writeto(filename, overwrite=overwrite) From 9e18079e3bfcaec742e999867b88395efd611f53 Mon Sep 17 00:00:00 2001 From: pgriffin17 <31374077+pgriffin17@users.noreply.github.com> Date: Sat, 13 Jan 2024 19:33:15 -0600 Subject: [PATCH 15/33] run black --- pyscope/observatory/ascom_camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyscope/observatory/ascom_camera.py b/pyscope/observatory/ascom_camera.py index 18e41abb..15a98cd7 100644 --- a/pyscope/observatory/ascom_camera.py +++ b/pyscope/observatory/ascom_camera.py @@ -257,7 +257,7 @@ def HeatSinkTemperature(self): @property def ImageArray(self): - """Return the image array as a numpy array of the correct data type and in + """Return the image array as a numpy array of the correct data type and in standard FITS orientation. \b Return the image array as a numpy array of the correct data type. The From 0533af9e186cebf60693347c82916a22280ad6d9 Mon Sep 17 00:00:00 2001 From: pgriffin17 <31374077+pgriffin17@users.noreply.github.com> Date: Sat, 13 Jan 2024 19:35:07 -0600 Subject: [PATCH 16/33] remove print statement --- pyscope/observatory/maxim.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyscope/observatory/maxim.py b/pyscope/observatory/maxim.py index d2f87b75..4db811a3 100755 --- a/pyscope/observatory/maxim.py +++ b/pyscope/observatory/maxim.py @@ -22,7 +22,6 @@ def __init__(self): else: from win32com.client import Dispatch - print("Running Windows") self._app = Dispatch("MaxIm.Application") self._app.LockApp = True From 091e19edf0db7e5c9d9cc48caf97ac1b6a4ed8a8 Mon Sep 17 00:00:00 2001 From: pgriffin17 <31374077+pgriffin17@users.noreply.github.com> Date: Sun, 14 Jan 2024 19:37:49 -0600 Subject: [PATCH 17/33] Moved win32com import, added note that this uses MaxIm's PinPoint --- pyscope/utils/pinpoint_solve.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/pyscope/utils/pinpoint_solve.py b/pyscope/utils/pinpoint_solve.py index e256dbd6..aec70bfd 100644 --- a/pyscope/utils/pinpoint_solve.py +++ b/pyscope/utils/pinpoint_solve.py @@ -1,9 +1,9 @@ import logging import os +import platform import time import click -from win32com.client import Dispatch logger = logging.getLogger(__name__) @@ -58,10 +58,14 @@ def platesolve_image(filepath, new_filepath): help="""Verbosity level. -v prints info messages""", ) def pinpoint_solve_cli(input_dir, output_dir=None, verbose=-1): - """Platesolve images in input_dir and save them to output_dir. \b + """Platesolve images in input_dir and save them to output_dir using PinPoint in MaxIm. \b Platesolve images in input_dir and save them to output_dir. If output_dir is not specified, solved images will be saved to the same directory as the input images. - Usage: python pinpoint_solve.py -i input_dir -o output_dir + + CLI Usage: `python pinpoint_solve.py -i input_dir -o output_dir` + + .. Note:: + This script requires MaxIm DL to be installed on your system, as it uses the PinPoint solver in MaxIm DL. \b Parameters ---------- @@ -97,6 +101,10 @@ def pinpoint_solve_cli(input_dir, output_dir=None, verbose=-1): logger.setLevel(int(10 * (2 - verbose))) logger.addHandler(logging.StreamHandler()) logger.debug(f"Starting pinpoint_solve_cli({input_dir}, {output_dir})") + if platform.system() != "Windows": + raise Exception("PinPoint is only available on Windows.") + else: + from win32com.client import Dispatch # Add input_dir to the end of the current working directory if it is not an absolute path if not os.path.isabs(input_dir): input_dir = os.path.join(os.getcwd(), input_dir) From 9e443d95c896741b3ee9c1abcd71df2af7cea9d6 Mon Sep 17 00:00:00 2001 From: pgriffin17 <31374077+pgriffin17@users.noreply.github.com> Date: Sun, 14 Jan 2024 19:38:42 -0600 Subject: [PATCH 18/33] re-run black --- pyscope/utils/pinpoint_solve.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyscope/utils/pinpoint_solve.py b/pyscope/utils/pinpoint_solve.py index aec70bfd..cf297024 100644 --- a/pyscope/utils/pinpoint_solve.py +++ b/pyscope/utils/pinpoint_solve.py @@ -61,7 +61,7 @@ def pinpoint_solve_cli(input_dir, output_dir=None, verbose=-1): """Platesolve images in input_dir and save them to output_dir using PinPoint in MaxIm. \b Platesolve images in input_dir and save them to output_dir. If output_dir is not specified, solved images will be saved to the same directory as the input images. - + CLI Usage: `python pinpoint_solve.py -i input_dir -o output_dir` .. Note:: From 00a5f4bac2f1ca99df52c1d4d503c8923dc7bcaa Mon Sep 17 00:00:00 2001 From: pgriffin17 <31374077+pgriffin17@users.noreply.github.com> Date: Sun, 14 Jan 2024 22:55:27 -0600 Subject: [PATCH 19/33] Updated _info properties, now all have info dict, reorganized some to catch possible errors --- pyscope/observatory/observatory.py | 188 +++++++++++++++-------------- 1 file changed, 100 insertions(+), 88 deletions(-) diff --git a/pyscope/observatory/observatory.py b/pyscope/observatory/observatory.py index e526962d..07941829 100644 --- a/pyscope/observatory/observatory.py +++ b/pyscope/observatory/observatory.py @@ -2402,7 +2402,8 @@ def _read_out_kwargs(self, dictionary): @property def autofocus_info(self): logger.debug("Observatory.autofocus_info() called") - return {"AUTODRIV": self.autofocus_driver} + info = {"AUTODRIV": (self.autofocus_driver, "Autofocus driver")} + return info @property def camera_info(self): @@ -2826,7 +2827,7 @@ def focuser_info(self): @property def observatory_info(self): logger.debug("Observatory.observatory_info() called") - return { + info = { "OBSNAME": (self.site_name, "Observatory name"), "OBSINSTN": (self.instrument_name, "Instrument name"), "OBSINSTD": (self.instrument_description, "Instrument description"), @@ -2838,6 +2839,7 @@ def observatory_info(self): "XPIXSCAL": (self.pixel_scale[0], "Observatory x-pixel scale"), "YPIXSCAL": (self.pixel_scale[1], "Observatory y-pixel scale"), } + return info @property def observing_conditions_info(self): @@ -3168,39 +3170,41 @@ def safety_monitor_info(self, index=None): for i in range(len(self.safety_monitor)): try: self.safety_monitor[i].Connected = True + # Should likely be broken into multiple try/except blocks + info = { + ("SM%iCONN" % i): (True, "Safety monitor connected"), + ("SM%iISSAF" % i): ( + self.safety_monitor[i].IsSafe, + "Safety monitor safe", + ), + ("SM%iNAME" % i): ( + self.safety_monitor[i].Name, + "Safety monitor name", + ), + ("SM%iDRVER" % i): ( + self.safety_monitor[i].DriverVersion, + "Safety monitor driver version", + ), + ("SM%iDRV" % i): ( + str(self.safety_monitor[i].DriverInfo), + "Safety monitor driver name", + ), + ("SM%iINTF" % i): ( + self.safety_monitor[i].InterfaceVersion, + "Safety monitor interface version", + ), + ("SM%iDESC" % i): ( + self.safety_monitor[i].Description, + "Safety monitor description", + ), + ("SM%iSUPAC" % i): ( + str(self.safety_monitor[i].SupportedActions), + "Safety monitor supported actions", + ), + } except: info = {"SM%iCONN" % i: (False, "Safety monitor connected")} - info = { - ("SM%iCONN" % i): (True, "Safety monitor connected"), - ("SM%iISSAF" % i): ( - self.safety_monitor[i].IsSafe, - "Safety monitor safe", - ), - ("SM%iNAME" % i): ( - self.safety_monitor[i].Name, - "Safety monitor name", - ), - ("SM%iDRVER" % i): ( - self.safety_monitor[i].DriverVersion, - "Safety monitor driver version", - ), - ("SM%iDRV" % i): ( - str(self.safety_monitor[i].DriverInfo), - "Safety monitor driver name", - ), - ("SM%iINTF" % i): ( - self.safety_monitor[i].InterfaceVersion, - "Safety monitor interface version", - ), - ("SM%iDESC" % i): ( - self.safety_monitor[i].Description, - "Safety monitor description", - ), - ("SM%iSUPAC" % i): ( - str(self.safety_monitor[i].SupportedActions), - "Safety monitor supported actions", - ), - } + all_info.append(info) else: return {"SM0CONN": (False, "Safety monitor connected")} @@ -3220,61 +3224,67 @@ def switch_info(self, index=None): for i in range(len(self.switch)): try: self.switch.Connected = True + try: + info = { + ("SW%iCONN" % i): (True, "Switch connected"), + ("SW%iNAME" % i): (self.switch[i].Name, "Switch name"), + ("SW%iDRVER" % i): ( + self.switch[i].DriverVersion, + "Switch driver version", + ), + ("SW%iDRV" % i): ( + str(self.switch[i].DriverInfo), + "Switch driver name", + ), + ("SW%iINTF" % i): ( + self.switch[i].InterfaceVersion, + "Switch interface version", + ), + ("SW%iDESC" % i): ( + self.switch[i].Description, + "Switch description", + ), + ("SW%iSUPAC" % i): ( + str(self.switch[i].SupportedActions), + "Switch supported actions", + ), + ("SW%iMAXSW" % i): ( + self.switch[i].MaxSwitch, + "Switch maximum switch", + ), + } + for j in range(self.switch[i].MaxSwitch): + try: + info[("SW%iSW%iNM" % (i, j))] = ( + self.switch[i].GetSwitchName(j), + "Switch %i Device %i name" % (i, j), + ) + info[("SW%iSW%iDS" % (i, j))] = ( + self.switch[i].GetSwitchDescription(j), + "Switch %i Device %i description" % (i, j), + ) + info[("SW%iSW%i" % (i, j))] = ( + self.switch[i].GetSwitch(j), + "Switch %i Device %i state" % (i, j), + ) + info[("SW%iSW%iMN" % (i, j))] = ( + self.switch[i].MinSwitchValue(j), + "Switch %i Device %i minimum value" % (i, j), + ) + info[("SW%iSW%iMX" % (i, j))] = ( + self.switch[i].MaxSwitchValue(j), + "Switch %i Device %i maximum value" % (i, j), + ) + info[("SW%iSW%iST" % (i, j))] = ( + self.switch[i].SwitchStep(j), + "Switch %i Device %i step" % (i, j), + ) + except Exception as e: + logger.debug(f"Sub-switch {j} of switch {i} gave the following error: {e}") + except Exception as e: + logger.debug(f"Switch {i} gives the following error: {e}") except: info = {("SW%iCONN" % i): (False, "Switch connected")} - info = { - ("SW%iCONN" % i): (True, "Switch connected"), - ("SW%iNAME" % i): (self.switch[i].Name, "Switch name"), - ("SW%iDRVER" % i): ( - self.switch[i].DriverVersion, - "Switch driver version", - ), - ("SW%iDRV" % i): ( - str(self.switch[i].DriverInfo), - "Switch driver name", - ), - ("SW%iINTF" % i): ( - self.switch[i].InterfaceVersion, - "Switch interface version", - ), - ("SW%iDESC" % i): ( - self.switch[i].Description, - "Switch description", - ), - ("SW%iSUPAC" % i): ( - str(self.switch[i].SupportedActions), - "Switch supported actions", - ), - ("SW%iMAXSW" % i): ( - self.switch[i].MaxSwitch, - "Switch maximum switch", - ), - } - for j in range(self.switch[i].MaxSwitch): - info[("SW%iSW%iNM" % (i, j))] = ( - self.switch[i].GetSwitchName(j), - "Switch %i Device %i name" % (i, j), - ) - info[("SW%iSW%iDS" % (i, j))] = ( - self.switch[i].GetSwitchDescription(j), - "Switch %i Device %i description" % (i, j), - ) - info[("SW%iSW%i" % (i, j))] = ( - self.switch[i].GetSwitch(j), - "Switch %i Device %i state" % (i, j), - ) - info[("SW%iSW%iMN" % (i, j))] = ( - self.switch[i].MinSwitchValue(j), - "Switch %i Device %i minimum value" % (i, j), - ) - info[("SW%iSW%iMX" % (i, j))] = ( - self.switch[i].MaxSwitchValue(j), - "Switch %i Device %i maximum value" % (i, j), - ) - info[("SW%iSW%iST" % (i, j))] = ( - self.switch[i].SwitchStep(j), - "Switch %i Device %i step" % (i, j), - ) all_info.append(info) else: @@ -3590,7 +3600,7 @@ def telescope_info(self): @property def threads_info(self): logger.debug("Observatory.threads_info() called") - return { + info = { "DEROTATE": ( not self._derotation_thread is None, "Is derotation thread active", @@ -3604,11 +3614,13 @@ def threads_info(self): "Is status monitor thread active", ), } + return info @property def wcs_info(self): logger.debug("Observatory.wcs_info() called") - return {"WCSDRV": (str(self.wcs_driver), "WCS driver")} + info = {"WCSDRV": (str(self.wcs_driver), "WCS driver")} + return info @property def observatory_location(self): From d674b329e122a455963eabb5b2458ea2d1287742 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jan 2024 12:53:52 +0000 Subject: [PATCH 20/33] Bump markdown from 3.5.1 to 3.5.2 Bumps [markdown](https://github.com/Python-Markdown/markdown) from 3.5.1 to 3.5.2. - [Release notes](https://github.com/Python-Markdown/markdown/releases) - [Changelog](https://github.com/Python-Markdown/markdown/blob/master/docs/changelog.md) - [Commits](https://github.com/Python-Markdown/markdown/compare/3.5.1...3.5.2) --- updated-dependencies: - dependency-name: markdown dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index bc4aa266..2b70a6b2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ astroquery == 0.4.6 astroscrappy == 1.1.0 click == 8.1.7 cmcrameri == 1.7.0 -markdown == 3.5.1 +markdown == 3.5.2 matplotlib == 3.8.2 numpy == 1.26.3 paramiko == 3.4.0 From ee178e608fd3dd6d9682b46a7d6d3b2eeee448b9 Mon Sep 17 00:00:00 2001 From: pgriffin17 <31374077+pgriffin17@users.noreply.github.com> Date: Mon, 15 Jan 2024 14:26:04 -0600 Subject: [PATCH 21/33] Update _info properties to prep for header documentation --- pyscope/observatory/observatory.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyscope/observatory/observatory.py b/pyscope/observatory/observatory.py index 07941829..8550669a 100644 --- a/pyscope/observatory/observatory.py +++ b/pyscope/observatory/observatory.py @@ -3204,7 +3204,7 @@ def safety_monitor_info(self, index=None): } except: info = {"SM%iCONN" % i: (False, "Safety monitor connected")} - + all_info.append(info) else: return {"SM0CONN": (False, "Safety monitor connected")} @@ -3280,7 +3280,9 @@ def switch_info(self, index=None): "Switch %i Device %i step" % (i, j), ) except Exception as e: - logger.debug(f"Sub-switch {j} of switch {i} gave the following error: {e}") + logger.debug( + f"Sub-switch {j} of switch {i} gave the following error: {e}" + ) except Exception as e: logger.debug(f"Switch {i} gives the following error: {e}") except: From 000d0c5721088c05b9bea4e171f8ab54a3c2f1fd Mon Sep 17 00:00:00 2001 From: pgriffin17 <31374077+pgriffin17@users.noreply.github.com> Date: Mon, 15 Jan 2024 14:26:13 -0600 Subject: [PATCH 22/33] Add header documentation --- .gitignore | 1 + docs/source/conf.py | 11 +++ docs/source/headerCSVGenerator.py | 117 ++++++++++++++++++++++++++++++ docs/source/user_guide/header.rst | 15 ++++ docs/source/user_guide/index.rst | 1 + 5 files changed, 145 insertions(+) create mode 100644 docs/source/headerCSVGenerator.py create mode 100644 docs/source/user_guide/header.rst diff --git a/.gitignore b/.gitignore index 84d4a9e0..a38bc724 100755 --- a/.gitignore +++ b/.gitignore @@ -70,6 +70,7 @@ instance/ # Sphinx documentation docs/_build/ +docs/source/user_guide/observatory_info.csv # PyBuilder target/ diff --git a/docs/source/conf.py b/docs/source/conf.py index 1ceb1f04..fd0ac88c 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -5,13 +5,19 @@ import sys from urllib.parse import quote + from packaging.version import parse from sphinx_astropy.conf.v2 import * +sys.path.insert(0, pathlib.Path(__file__).parents[0].resolve().as_posix()) + +import headerCSVGenerator + sys.path.insert(0, pathlib.Path(__file__).parents[2].resolve().as_posix()) import pyscope + project = "pyscope" copyright = "2023, Walter Golay" author = "Walter Golay" @@ -57,6 +63,11 @@ extensions = list(map(lambda x: x.replace("viewcode", "linkcode"), extensions)) +# Generate CSV for header info +print("Generating CSV for header info...") +targetPath = os.path.join(os.path.dirname(__file__), "user_guide", "observatory_info.csv") +headerCSVGenerator.HeaderCSVGenerator().generate_csv(targetPath) + def linkcode_resolve(domain, info): """ diff --git a/docs/source/headerCSVGenerator.py b/docs/source/headerCSVGenerator.py new file mode 100644 index 00000000..b22c3f06 --- /dev/null +++ b/docs/source/headerCSVGenerator.py @@ -0,0 +1,117 @@ +import ast +import csv +import inspect +import re + +from pyscope.observatory import Observatory + + +class HeaderCSVGenerator: + """Generates a CSV file containing the header information for the Observatory class. + + The CSV file contains the following columns: + - Header Key: The key of the header + - Header Value: The value of the header + - Header Description: The description of the header + + The CSV file is generated by parsing the Observatory class for the info dictionaries + and then combining them into a master dictionary. The master dictionary is then + output to a CSV file. + """ + def __init__(self): + pass + + def get_info_dicts(self): + descriptors = inspect.getmembers( + Observatory, predicate=inspect.isdatadescriptor + ) + info = [] + for descriptor in descriptors: + if "info" in descriptor[0]: + info.append(descriptor) + + source_list = [] + for descriptor in info: + source_list.append(inspect.getsource(descriptor[1].fget)) + + # Split source into lines, remove tabs + source_lines = [] + for source in source_list: + source_lines.append(source.split("\n")) + + # Remove leading whitespace + stripped_lines = [] + for source in source_lines: + for line in source: + stripped_lines.append(line.lstrip()) + + # Return parts of source_list that contain 'info = {...}' + info_dict = [] + for property in source_list: + # Use regex to find info = {[^}]} and add it to info_dict + info_dict.append(re.findall(r"info = {[^}]*}", property)[0]) + + # Remove 'info = ' from each string + for i, property in enumerate(info_dict): + info_dict[i] = property[7:] + + # Encase any references to self. in quotes + for i, property in enumerate(info_dict): + info_dict[i] = re.sub(r"self\.([a-zA-Z0-9_.\[\]]+)", r'"\1"', property) + + # Find references to str() + for i, property in enumerate(info_dict): + info_dict[i] = re.sub(r"(str\(([a-zA-Z\"\_\.\[\]]+)\))", r"\2", property) + + # Replace any references to self.etc. with None + for i, property in enumerate(info_dict): + # Use regex "\(\s+(.*?\],)"gm + # info_dict[i] = re.sub(r'\(\\n\s+(.*?\],)', 'None', property) + group = re.findall(r"\(\n\s+([\s\S]*?\],)", property) + # replace the group with None + if group: + info_dict[i] = property.replace(group[0], "None,") + + # Enclose any parts with (sep="dms") in quotes + for i, property in enumerate(info_dict): + info_dict[i] = re.sub( + r"(\"\S+\(sep=\"dms\"\))", + lambda m: '"' + m.group(1).replace('"', " ") + '"', + property, + ) + + # Remove any parts matching \% i(?=\)) (or replace with "") + for i, property in enumerate(info_dict): + info_dict[i] = re.sub(r"( \% i(?=\)))", "", property) + + # Enclose in quotes any parts matching 'not \"\S+\" is None' + for i, property in enumerate(info_dict): + info_dict[i] = re.sub( + r"(not \"\S+\" is None)", + lambda m: '"' + m.group(1).replace('"', " ") + '"', + property, + ) + + # Pass each info_dict through ast.literal_eval to convert to dictionary + info_dict_parsed = [] + for info in info_dict: + info_dict_parsed.append(ast.literal_eval(info)) + + return info_dict_parsed + + def generate_csv(self, filename): + info_dicts = self.get_info_dicts() + # Add each dictionary to make a master dictionary + master_dict = {} + for info in info_dicts: + master_dict.update(info) + # Output to csv in the format key, value, description + # key is the key of the dictionary + # value is the first part of the tuple (the value) + # description is the second part of the tuple (the description) + with open(filename, "w", newline="") as csv_file: + writer = csv.writer(csv_file) + # Write header + writer.writerow(["Header Key", "Header Value", "Header Description"]) + for key, value in master_dict.items(): + writer.writerow([key, value[0], value[1]]) diff --git a/docs/source/user_guide/header.rst b/docs/source/user_guide/header.rst new file mode 100644 index 00000000..21fed342 --- /dev/null +++ b/docs/source/user_guide/header.rst @@ -0,0 +1,15 @@ +Header +====== + +This is a page listing all potential header keywords for an `Observatory` object. + +.. Note:: + This is auto-generated from the `info` dictionaries in the `Observatory` class. Some + of the information is not available for all observatories, and some header values may + contain nonsense due to the auto-generation script. + +.. csv-table:: Sample Header + :file: observatory_info.csv + :widths: 4, 6, 10 + :header-rows: 1 + diff --git a/docs/source/user_guide/index.rst b/docs/source/user_guide/index.rst index 90b29f94..b9022f7a 100644 --- a/docs/source/user_guide/index.rst +++ b/docs/source/user_guide/index.rst @@ -7,6 +7,7 @@ User Guide :maxdepth: 2 examples + header logging config help From bdf4df01ac59c836356aac3ee2343bf2b83d4209 Mon Sep 17 00:00:00 2001 From: pgriffin17 <31374077+pgriffin17@users.noreply.github.com> Date: Mon, 15 Jan 2024 14:28:40 -0600 Subject: [PATCH 23/33] black/isort --- docs/source/conf.py | 6 +++--- docs/source/headerCSVGenerator.py | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index fd0ac88c..0ab0b817 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -5,7 +5,6 @@ import sys from urllib.parse import quote - from packaging.version import parse from sphinx_astropy.conf.v2 import * @@ -17,7 +16,6 @@ import pyscope - project = "pyscope" copyright = "2023, Walter Golay" author = "Walter Golay" @@ -65,7 +63,9 @@ # Generate CSV for header info print("Generating CSV for header info...") -targetPath = os.path.join(os.path.dirname(__file__), "user_guide", "observatory_info.csv") +targetPath = os.path.join( + os.path.dirname(__file__), "user_guide", "observatory_info.csv" +) headerCSVGenerator.HeaderCSVGenerator().generate_csv(targetPath) diff --git a/docs/source/headerCSVGenerator.py b/docs/source/headerCSVGenerator.py index b22c3f06..549f7306 100644 --- a/docs/source/headerCSVGenerator.py +++ b/docs/source/headerCSVGenerator.py @@ -8,16 +8,17 @@ class HeaderCSVGenerator: """Generates a CSV file containing the header information for the Observatory class. - + The CSV file contains the following columns: - Header Key: The key of the header - Header Value: The value of the header - Header Description: The description of the header - + The CSV file is generated by parsing the Observatory class for the info dictionaries and then combining them into a master dictionary. The master dictionary is then output to a CSV file. """ + def __init__(self): pass From 7631b6bdde42bd53f9efd71529d3ce771a6eafd0 Mon Sep 17 00:00:00 2001 From: Will Golay <87041778+WWGolay@users.noreply.github.com> Date: Thu, 18 Jan 2024 12:07:51 -0600 Subject: [PATCH 24/33] Update .readthedocs.yaml Remove PDF building, issue with readthedocs Signed-off-by: Will Golay <87041778+WWGolay@users.noreply.github.com> --- .readthedocs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 5f90ac59..92a2ed25 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -24,7 +24,7 @@ sphinx: # Optionally build your docs in additional formats such as PDF and ePub formats: - - pdf + # - pdf # Optional but recommended, declare the Python requirements required # to build your documentation From a9be1986c311f8ca1d79ac4cfe326f5ecebdd6a5 Mon Sep 17 00:00:00 2001 From: Will Golay <87041778+WWGolay@users.noreply.github.com> Date: Thu, 18 Jan 2024 12:08:47 -0600 Subject: [PATCH 25/33] Update .readthedocs.yaml Fix formats Signed-off-by: Will Golay <87041778+WWGolay@users.noreply.github.com> --- .readthedocs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 92a2ed25..c0fd1df7 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -23,7 +23,7 @@ sphinx: fail_on_warning: true # Optionally build your docs in additional formats such as PDF and ePub -formats: +# formats: # - pdf # Optional but recommended, declare the Python requirements required From 17d70de38b23af9ed4f025b20d194a7a38c67f99 Mon Sep 17 00:00:00 2001 From: pgriffin17 <31374077+pgriffin17@users.noreply.github.com> Date: Sun, 21 Jan 2024 15:51:12 -0600 Subject: [PATCH 26/33] Updated parameterless methods to remove () --- pyscope/observatory/ascom_telescope.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pyscope/observatory/ascom_telescope.py b/pyscope/observatory/ascom_telescope.py index 3eb19cd1..9f5f9aac 100644 --- a/pyscope/observatory/ascom_telescope.py +++ b/pyscope/observatory/ascom_telescope.py @@ -18,7 +18,7 @@ def __init__(self, identifier, alpaca=False, device_number=0, protocol="http"): def AbortSlew(self): logger.debug("ASCOMTelescope.AbortSlew() called") - self._device.AbortSlew() + self._device.AbortSlew def AxisRates(self, Axis): logger.debug(f"ASCOMTelescope.AxisRates({Axis}) called") @@ -36,7 +36,7 @@ def DestinationSideOfPier(self, RightAscension, Declination): def FindHome(self): logger.debug("ASCOMTelescope.FindHome() called") - self._device.FindHome() + self._device.FindHome def MoveAxis(self, Axis, Rate): logger.debug(f"ASCOMTelescope.MoveAxis({Axis}, {Rate}) called") @@ -44,7 +44,7 @@ def MoveAxis(self, Axis, Rate): def Park(self): logger.debug("ASCOMTelescope.Park() called") - self._device.Park() + self._device.Park def PulseGuide(self, Direction, Duration): logger.debug(f"ASCOMTelescope.PulseGuide({Direction}, {Duration}) called") @@ -52,7 +52,7 @@ def PulseGuide(self, Direction, Duration): def SetPark(self): logger.debug("ASCOMTelescope.SetPark() called") - self._device.SetPark() + self._device.SetPark def SlewToAltAz(self, Azimuth, Altitude): # pragma: no cover """ @@ -88,11 +88,11 @@ def SlewToTarget(self): # pragma: no cover ASCOM is deprecating this method. """ logger.debug("ASCOMTelescope.SlewToTarget() called") - self._device.SlewToTarget() + self._device.SlewToTarget def SlewToTargetAsync(self): logger.debug("ASCOMTelescope.SlewToTargetAsync() called") - self._device.SlewToTargetAsync() + self._device.SlewToTargetAsync def SyncToAltAz(self, Azimuth, Altitude): logger.debug(f"ASCOMTelescope.SyncToAltAz({Azimuth}, {Altitude}) called") @@ -106,11 +106,11 @@ def SyncToCoordinates(self, RightAscension, Declination): def SyncToTarget(self): logger.debug("ASCOMTelescope.SyncToTarget() called") - self._device.SyncToTarget() + self._device.SyncToTarget def Unpark(self): logger.debug("ASCOMTelescope.Unpark() called") - self._device.Unpark() + self._device.Unpark @property def AlignmentMode(self): From 3d071f6f37839461cd50ba58192867ef5a64b975 Mon Sep 17 00:00:00 2001 From: pgriffin17 <31374077+pgriffin17@users.noreply.github.com> Date: Sun, 21 Jan 2024 15:52:34 -0600 Subject: [PATCH 27/33] Update properties to give Observatory versioning info and sensor descriptions as str --- .../observatory/html_observing_conditions.py | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/pyscope/observatory/html_observing_conditions.py b/pyscope/observatory/html_observing_conditions.py index 733bcbd2..92364c48 100755 --- a/pyscope/observatory/html_observing_conditions.py +++ b/pyscope/observatory/html_observing_conditions.py @@ -282,7 +282,7 @@ def Refresh(self): def SensorDescription(self, PropertyName): logger.debug("HTMLObservingConditions.SensorDescription({PropertyName}) called") - return eval(f"self._{PropertyName.lower()}_keyword") + return str(eval(f"self._{PropertyName.lower()}_keyword")) def TimeSinceLastUpdate(self, PropertyName): logger.debug( @@ -317,6 +317,21 @@ def AveragePeriod(self, value): def CloudCover(self): logger.debug("HTMLObservingConditions.CloudCover property called") return self._cloud_cover + + @property + def Description(self): + logger.debug("HTMLObservingConditions.Description property called") + return "HTML Observing Conditions Driver" + + @property + def DriverVersion(self): + logger.debug("HTMLObservingConditions.DriverVersion property called") + return None + + @property + def DriverInfo(self): + logger.debug("HTMLObservingConditions.DriverInfo property called") + return "HTML Observing Conditions Driver" @property def DewPoint(self): @@ -328,6 +343,16 @@ def Humidity(self): logger.debug("HTMLObservingConditions.Humidity property called") return self._humidity + @property + def InterfaceVersion(self): + logger.debug("HTMLObservingConditions.InterfaceVersion property called") + return 1 + + @property + def Name(self): + logger.debug("HTMLObservingConditions.Name property called") + return self._url + @property def Pressure(self): logger.debug("HTMLObservingConditions.Pressure property called") From a33e2c5c1f021ad738d441beaa0ed7bd837ad85b Mon Sep 17 00:00:00 2001 From: pgriffin17 <31374077+pgriffin17@users.noreply.github.com> Date: Sun, 21 Jan 2024 15:53:52 -0600 Subject: [PATCH 28/33] Various changes to get filter wheels working on real hardware --- pyscope/observatory/maxim.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyscope/observatory/maxim.py b/pyscope/observatory/maxim.py index 4db811a3..e0096644 100755 --- a/pyscope/observatory/maxim.py +++ b/pyscope/observatory/maxim.py @@ -156,7 +156,7 @@ def VerifyLatestExposure(self): try: # Change to document - image = self._app.Document + image = self._com_object.Document except Exception as e: raise Exception(f"Unable to access MaxIm camera image: {e}") @@ -164,7 +164,7 @@ def VerifyLatestExposure(self): raise Exception("No current image available from MaxIm") # Get the DATE-OBS header - image_timestamp = image.GetFITSKey["DATE-OBS"] + image_timestamp = image.GetFITSKey("DATE-OBS") image_datetime = Time(image_timestamp, format="fits") logger.debug(f"Image timestamp: {image_timestamp}") @@ -561,7 +561,7 @@ def Connected(self, value): @property def Name(self): logger.debug("_MaximFilterWheelName called") - self.maxim_camera.FilterWheelName + return self.maxim_camera._com_object.FilterWheelName @property def FocusOffsets(self): @@ -571,17 +571,17 @@ def FocusOffsets(self): @property def Names(self): logger.debug("_MaximFilterWheelNames called") - return self.maxim_camera.FilterNames + return self.maxim_camera._com_object.FilterNames @property def Position(self): logger.debug("_MaximFilterWheelPosition called") - return self.maxim_camera.Filter + return self.maxim_camera._com_object.Filter @Position.setter def Position(self, value): logger.debug(f"Position setter called with value={value}") - self.maxim_camera.Filter = value + self.maxim_camera._com_object.Filter = value class _MaximPinpointWCS(WCS): From b1f13001034e8f6bef316050765c6b8015a75129 Mon Sep 17 00:00:00 2001 From: pgriffin17 <31374077+pgriffin17@users.noreply.github.com> Date: Sun, 21 Jan 2024 15:54:13 -0600 Subject: [PATCH 29/33] Working on real hardware --- pyscope/observatory/pwi_autofocus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyscope/observatory/pwi_autofocus.py b/pyscope/observatory/pwi_autofocus.py index ce9b36ca..db86e486 100755 --- a/pyscope/observatory/pwi_autofocus.py +++ b/pyscope/observatory/pwi_autofocus.py @@ -66,7 +66,7 @@ def Abort(self): def _forward_autofocus_messages(self): while True: - log_line = _autofocus.NextLogMessage + log_line = self._com_object.NextLogMessage if log_line is None: return logger.info(log_line) From 7aaf09968dd9ad94a8b732fb5d8e28a3629ba7e7 Mon Sep 17 00:00:00 2001 From: pgriffin17 <31374077+pgriffin17@users.noreply.github.com> Date: Sun, 21 Jan 2024 15:54:53 -0600 Subject: [PATCH 30/33] Add versioning properties for use in FITS headers --- pyscope/observatory/html_safety_monitor.py | 30 ++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pyscope/observatory/html_safety_monitor.py b/pyscope/observatory/html_safety_monitor.py index 3bad969a..e78718ec 100755 --- a/pyscope/observatory/html_safety_monitor.py +++ b/pyscope/observatory/html_safety_monitor.py @@ -44,3 +44,33 @@ def IsSafe(self): safe = False return safe + + @property + def DriverVersion(self): + logger.debug(f"""HTMLSafetyMonitor.DriverVersion property called""") + return "1.0" + + @property + def DriverInfo(self): + logger.debug(f"""HTMLSafetyMonitor.DriverInfo property called""") + return "HTML Safety Monitor" + + @property + def InterfaceVersion(self): + logger.debug(f"""HTMLSafetyMonitor.InterfaceVersion property called""") + return "1.0" + + @property + def Description(self): + logger.debug(f"""HTMLSafetyMonitor.Description property called""") + return "HTML Safety Monitor" + + @property + def SupportedActions(self): + logger.debug(f"""HTMLSafetyMonitor.SupportedActions property called""") + return [] + + @property + def Name(self): + logger.debug(f"""HTMLSafetyMonitor.Name property called""") + return self._url From 0916e4ec475cdadab967bd53c111e2ab08e3f830 Mon Sep 17 00:00:00 2001 From: pgriffin17 <31374077+pgriffin17@users.noreply.github.com> Date: Sun, 21 Jan 2024 15:55:30 -0600 Subject: [PATCH 31/33] Various updates to give functionality on real hardware --- pyscope/observatory/observatory.py | 91 ++++++++++++++++++++++++++---- 1 file changed, 79 insertions(+), 12 deletions(-) diff --git a/pyscope/observatory/observatory.py b/pyscope/observatory/observatory.py index b7d5654b..cf504c56 100644 --- a/pyscope/observatory/observatory.py +++ b/pyscope/observatory/observatory.py @@ -236,7 +236,7 @@ def __init__(self, config_path=None, **kwargs): "MaxIm DL must be used as the camera driver when using MaxIm DL as the filter wheel driver." ) logger.info("Using MaxIm DL as the filter wheel driver") - self._filter_wheel = self._maxim.filter_wheel + self._filter_wheel = self._maxim._filter_wheel else: self._filter_wheel = _import_driver( self.filter_wheel_driver, @@ -735,6 +735,8 @@ def connect_all(self): logger.warning("Camera failed to connect") if self.camera.CanSetCCDTemperature: self.cooler_setpoint = self._cooler_setpoint + print("Turning cooler on") + self.camera.CoolerOn = True if self.cover_calibrator is not None: self.cover_calibrator.Connected = True @@ -808,7 +810,9 @@ def disconnect_all(self): """Disconnects from the observatory""" logger.debug("Observatory.disconnect_all() called") - + # TODO: Implement safe warmup procedure + if self.camera.CoolerOn: + self.camera.CoolerOn = False self.camera.Connected = False if not self.camera.Connected: logger.info("Camera disconnected") @@ -1191,6 +1195,7 @@ def generate_header_info( if not maxim: hdr = fits.Header() else: + logger.info("Getting header from MaxIm image") hdr = fits.getheader(filename) # The commented out part is unnecessary, as the fits header is automatically generated @@ -1242,7 +1247,14 @@ def safe_update_header(self, hdr, hdr_dict, maxim=False, allowed_overwrite=[]): logger.debug(f"Observatory.safe_update_header called") if maxim: # Only keep the allowed_overwrite keys in the hdr_dict - hdr_dict = {k: v for k, v in hdr_dict.items() if k in allowed_overwrite} + # First get keys in existing header + existing_keys = hdr.keys() + # If the key is in the existing header, and not in the allowed_overwrite list, remove it + for key in existing_keys: + if key not in allowed_overwrite: + hdr_dict.pop(key, None) + + # hdr_dict = {k: v for k, v in hdr_dict.items() if k in allowed_overwrite} hdr.update(hdr_dict) def save_last_image( @@ -1267,7 +1279,8 @@ def save_last_image( logger.exception("Image is not ready, cannot be saved") return False - maxim = self.camera_driver.lower() in ("maxim", "maximdl") + maxim = self.camera_driver.lower() in ("maxim", "maximdl", "_maximcamera") + # print(self.camera_driver.lower()) # If camera driver is Maxim, use Maxim to save the image # This is because Maxim does not pass some of the header information @@ -1280,9 +1293,14 @@ def save_last_image( logger.exception("Image array is empty, cannot be saved") return False else: + # print("Using Maxim to save image") logger.info("Using Maxim to save image") + allowed_overwrite = ["AIRMASS", "OBJECT", "TELESCOP", "INSTRUME", "OBSERVER"] + logger.info(f"Overwrite allowed for header keys {allowed_overwrite}") self.camera.VerifyLatestExposure() - self.camera.SaveImageAsFits(filename) + # TODO: Below should be updated to the filepath we want to save to + filepath = os.path.join(os.getcwd(), filename) + self.camera.SaveImageAsFits(filepath) img_array = fits.getdata(filename) # Moved below to separate function @@ -2892,33 +2910,82 @@ def filter_wheel_info(self): "Filter name (from pyscope observatory object configuration)", ), "FOCOFFCG": ( - self.filter_wheel.FocusOffsets[self.filter_wheel.Position], + None, "Filter focus offset (from filter wheel object configuration)", ), "FWNAME": (self.filter_wheel.Name, "Filter wheel name"), "FWDRVER": ( - self.filter_wheel.DriverVersion, + None, "Filter wheel driver version", ), "FWDRV": ( - str(self.filter_wheel.DriverInfo), + None, "Filter wheel driver info", ), "FWINTF": ( - self.filter_wheel.InterfaceVersion, + None, "Filter wheel interface version", ), - "FWDESC": (self.filter_wheel.Description, "Filter wheel description"), + "FWDESC": (None, "Filter wheel description"), "FWALLNAM": (str(self.filter_wheel.Names), "Filter wheel names"), "FWALLOFF": ( - str(self.filter_wheel.FocusOffsets), + None, "Filter wheel focus offsets", ), "FWSUPAC": ( - str(self.filter_wheel.SupportedActions), + None, "Filter wheel supported actions", ), } + try: + info["FOCOFFCG"] = ( + self.filter_wheel.FocusOffsets[self.filter_wheel.Position], + info["FOCOFFCG"][1], + ) + except: + pass + try: + info["FWDRVER"] = ( + self.filter_wheel.DriverVersion, + info["FWDRVER"][1], + ) + except: + pass + try: + info["FWDRV"] = ( + str(self.filter_wheel.DriverInfo), + info["FWDRV"][1], + ) + except: + pass + try: + info["FWINTF"] = ( + self.filter_wheel.InterfaceVersion, + info["FWINTF"][1], + ) + except: + pass + try: + info["FWDESC"] = ( + self.filter_wheel.Description, + info["FWDESC"][1], + ) + except: + pass + try: + info["FWSUPAC"] = ( + str(self.filter_wheel.SupportedActions), + info["FWSUPAC"][1], + ) + except: + pass + try: + info["FWALLOFF"] = ( + str(self.filter_wheel.FocusOffsets), + info["FWALLOFF"][1], + ) + except: + pass return info else: return {"FWCONN": (False, "Filter wheel connected")} From ceda54b5126682f9268671a927b5859219264c6d Mon Sep 17 00:00:00 2001 From: pgriffin17 <31374077+pgriffin17@users.noreply.github.com> Date: Sun, 21 Jan 2024 15:57:29 -0600 Subject: [PATCH 32/33] add test_observatory.cfg, the working cfg file, and run black/isort --- pyscope/config/test_observatory.cfg | 154 ++++++++++++++++++ .../observatory/html_observing_conditions.py | 2 +- pyscope/observatory/html_safety_monitor.py | 2 +- pyscope/observatory/observatory.py | 10 +- 4 files changed, 164 insertions(+), 4 deletions(-) create mode 100644 pyscope/config/test_observatory.cfg diff --git a/pyscope/config/test_observatory.cfg b/pyscope/config/test_observatory.cfg new file mode 100644 index 00000000..b2ff6053 --- /dev/null +++ b/pyscope/config/test_observatory.cfg @@ -0,0 +1,154 @@ +[site] + +site_name = Winer Observatory + +instrument_name = Robert L. Mutel Telescope + +instrument_description = 20-inch PlaneWave CDK + +latitude = 31:39:56.08 # dd:mm:ss.s + +longitude = -110:36:06.42 # dd:mm:ss.s + +elevation = 1515.7 # meters + +diameter = 0.508 # meters + +focal_length = 3.454 # meters + + +[camera] + +camera_driver = maxim + +camera_ascom = False + +camera_kwargs = + +cooler_setpoint = -20 # Celsius + +cooler_tolerance = 1 # Celsius + +max_dimension = 4096 # pixels + + +[cover_calibrator] + +cover_calibrator_driver = ip_cover_calibrator + +cover_calibrator_ascom = False + +cover_calibrator_kwargs = tcp_ip:192.168.2.22,tcp_port:2101,buffer_size:1024 + +cover_calibrator_alt = 30.09397 + +cover_calibrator_az = 86.96717 + + +[dome] + +dome_driver = + +dome_ascom = + +dome_kwargs = + + +[filter_wheel] + +filter_wheel_driver = maxim + +filter_wheel_ascom = False + +filter_wheel_kwargs = + +filters = L, 6, V, B, H, W, O, 1, I, X, G, R + +filter_focus_offsets = 0, 1400, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 +# comma-separated list of focus offsets (in counts) for each filter + + +[focuser] + +focuser_driver = ASCOM.PWI3.Focuser + +focuser_ascom = True + +focuser_kwargs = + + +[observing_conditions] + +observing_conditions_driver = html_observing_conditions + +observing_conditions_ascom = False + +observing_conditions_kwargs = url:https://winer.org/Site/Weather.php + + +[rotator] + +rotator_driver = + +rotator_ascom = + +rotator_kwargs = + +rotator_reverse = + +rotator_min_angle = + +rotator_max_angle = + + +[safety_monitor] + +driver_0 = html_safety_monitor,False,url:https://winer.org/Site/Roof.php + +driver_1 = + +driver_2 = + + +[switch] + +driver_0 = + +driver_1 = + +driver_2 = + + +[telescope] + +telescope_driver = SiTech.Telescope + +telescope_ascom = True + +telescope_kwargs = + +min_altitude = 21 # degrees + +settle_time = 5 + + +[autofocus] + +autofocus_driver = pwi_autofocus + +autofocus_kwargs = + + +[wcs] + +driver_0 = maxim + +driver_1 = astrometry_net_wcs + +driver_2 = + +[scheduling] + +slew_rate = 2 # degrees per second + +instrument_reconfiguration_times = diff --git a/pyscope/observatory/html_observing_conditions.py b/pyscope/observatory/html_observing_conditions.py index 92364c48..4e17afde 100755 --- a/pyscope/observatory/html_observing_conditions.py +++ b/pyscope/observatory/html_observing_conditions.py @@ -317,7 +317,7 @@ def AveragePeriod(self, value): def CloudCover(self): logger.debug("HTMLObservingConditions.CloudCover property called") return self._cloud_cover - + @property def Description(self): logger.debug("HTMLObservingConditions.Description property called") diff --git a/pyscope/observatory/html_safety_monitor.py b/pyscope/observatory/html_safety_monitor.py index e78718ec..1921dab1 100755 --- a/pyscope/observatory/html_safety_monitor.py +++ b/pyscope/observatory/html_safety_monitor.py @@ -49,7 +49,7 @@ def IsSafe(self): def DriverVersion(self): logger.debug(f"""HTMLSafetyMonitor.DriverVersion property called""") return "1.0" - + @property def DriverInfo(self): logger.debug(f"""HTMLSafetyMonitor.DriverInfo property called""") diff --git a/pyscope/observatory/observatory.py b/pyscope/observatory/observatory.py index cf504c56..ba1077a1 100644 --- a/pyscope/observatory/observatory.py +++ b/pyscope/observatory/observatory.py @@ -1254,7 +1254,7 @@ def safe_update_header(self, hdr, hdr_dict, maxim=False, allowed_overwrite=[]): if key not in allowed_overwrite: hdr_dict.pop(key, None) - # hdr_dict = {k: v for k, v in hdr_dict.items() if k in allowed_overwrite} + # hdr_dict = {k: v for k, v in hdr_dict.items() if k in allowed_overwrite} hdr.update(hdr_dict) def save_last_image( @@ -1295,7 +1295,13 @@ def save_last_image( else: # print("Using Maxim to save image") logger.info("Using Maxim to save image") - allowed_overwrite = ["AIRMASS", "OBJECT", "TELESCOP", "INSTRUME", "OBSERVER"] + allowed_overwrite = [ + "AIRMASS", + "OBJECT", + "TELESCOP", + "INSTRUME", + "OBSERVER", + ] logger.info(f"Overwrite allowed for header keys {allowed_overwrite}") self.camera.VerifyLatestExposure() # TODO: Below should be updated to the filepath we want to save to From 303617def787cb48c85040387ca0b069aa0abe9d Mon Sep 17 00:00:00 2001 From: pgriffin17 <31374077+pgriffin17@users.noreply.github.com> Date: Sun, 21 Jan 2024 16:16:04 -0600 Subject: [PATCH 33/33] Remove win32com import at top of file --- pyscope/observatory/maxim.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyscope/observatory/maxim.py b/pyscope/observatory/maxim.py index e0096644..8379c15d 100755 --- a/pyscope/observatory/maxim.py +++ b/pyscope/observatory/maxim.py @@ -3,7 +3,6 @@ import time from astropy.time import Time -from win32com.client import Dispatch from .autofocus import Autofocus from .camera import Camera