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 01/15] 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 02/15] 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 03/15] 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 04/15] 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 05/15] 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 06/15] 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 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 07/15] 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 08/15] 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 09/15] 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 10/15] 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 11/15] 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 12/15] 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 13/15] 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 14/15] 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 15/15] 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