From d75ae1dbe1babcf38e6537a234496e0421f3d317 Mon Sep 17 00:00:00 2001 From: Wilfred Tyler Gee Date: Fri, 19 Sep 2025 09:27:37 -1000 Subject: [PATCH 1/3] Update state machine * Adds a `simple` state file that removes the `pointing`, `tracking`, and `analyzing` states, which were effectively being skipped via options. * Defaults to new state machine. :warning: Needs to be tested on hardware for a full night before merging :warning: --- conf_files/pocs.yaml | 2 +- conf_files/state_table/simple.yaml | 60 +++++++++++++++++++ .../pocs/state/states/default/observing.py | 4 +- .../pocs/state/states/default/scheduling.py | 2 +- .../pocs/state/states/default/slewing.py | 4 +- 5 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 conf_files/state_table/simple.yaml diff --git a/conf_files/pocs.yaml b/conf_files/pocs.yaml index 67caffdfd..91746bad3 100644 --- a/conf_files/pocs.yaml +++ b/conf_files/pocs.yaml @@ -42,7 +42,7 @@ wait_delay: 180 # time in seconds before checking safety/etc while waiting. max_transition_attempts: 5 # number of transitions attempts. status_check_interval: 60 # periodic status check. -state_machine: panoptes +state_machine: simple scheduler: type: panoptes.pocs.scheduler.dispatch diff --git a/conf_files/state_table/simple.yaml b/conf_files/state_table/simple.yaml new file mode 100644 index 000000000..3f5ca0a4a --- /dev/null +++ b/conf_files/state_table/simple.yaml @@ -0,0 +1,60 @@ +--- +name: default +initial: sleeping +states: + parking: + tags: always_safe + parked: + tags: always_safe + sleeping: + tags: always_safe + housekeeping: + tags: always_safe + ready: + tags: always_safe + scheduling: + horizon: observe + slewing: + observing: +transitions: + - source: + - ready + - scheduling + - slewing + - observing + dest: parking + trigger: park + - source: parking + dest: parked + trigger: set_park + - source: parked + dest: housekeeping + trigger: clean_up + - source: housekeeping + dest: sleeping + trigger: goto_sleep + - source: parked + dest: ready + trigger: get_ready + conditions: mount_is_initialized + - source: sleeping + dest: ready + trigger: get_ready + conditions: mount_is_initialized + - source: ready + dest: scheduling + trigger: schedule + - source: scheduling + dest: slewing + trigger: start_slewing + - source: slewing + dest: observing + trigger: observe + conditions: mount_is_tracking + - source: observing + dest: observing + trigger: observe + conditions: mount_is_tracking + - source: observing + dest: scheduling + trigger: schedule diff --git a/src/panoptes/pocs/state/states/default/observing.py b/src/panoptes/pocs/state/states/default/observing.py index 57353b3e4..6de941009 100644 --- a/src/panoptes/pocs/state/states/default/observing.py +++ b/src/panoptes/pocs/state/states/default/observing.py @@ -19,5 +19,5 @@ def on_enter(event_data): pocs.logger.warning(f"Problem with imaging: {e!r}") pocs.say("Hmm, I'm not sure what happened with that exposure.") else: - pocs.next_state = "analyzing" - pocs.logger.debug("Finished with observing, going to {pocs.next_state}") + pocs.next_state = "scheduling" + pocs.say(f"Finished with observing, going to {pocs.next_state}") diff --git a/src/panoptes/pocs/state/states/default/scheduling.py b/src/panoptes/pocs/state/states/default/scheduling.py index ff4ffca2e..fcad6d435 100644 --- a/src/panoptes/pocs/state/states/default/scheduling.py +++ b/src/panoptes/pocs/state/states/default/scheduling.py @@ -39,7 +39,7 @@ def on_enter(event_data): # Make sure we are using existing observation (with pointing image) pocs.observatory.current_observation = existing_observation - pocs.next_state = "tracking" + pocs.next_state = "slewing" else: pocs.say(f"Got it! I'm going to check out: {observation.name}") diff --git a/src/panoptes/pocs/state/states/default/slewing.py b/src/panoptes/pocs/state/states/default/slewing.py index 9ef0ecc03..c6ee19877 100644 --- a/src/panoptes/pocs/state/states/default/slewing.py +++ b/src/panoptes/pocs/state/states/default/slewing.py @@ -21,8 +21,8 @@ def on_enter(event_data): if pocs.observatory.mount.slew_to_target(blocking=True) is False: raise error.PocsError("Mount did not successfully slew to target.") - pocs.say("I'm at the target, checking pointing.") - pocs.next_state = "pointing" + pocs.say("I'm at the target, going to start observing.") + pocs.next_state = "observing" except Exception as e: pocs.say(f"Wait a minute, there was a problem slewing. Sending to parking. {e}") From 0a704bd659d2e750cad32c7be4b7b45ed5597ec2 Mon Sep 17 00:00:00 2001 From: Wilfred Tyler Gee Date: Sat, 20 Sep 2025 06:40:05 -1000 Subject: [PATCH 2/3] * Remove the unused states. * Remove unused `Image` class. * Remove related tests. --- src/panoptes/pocs/images.py | 248 ------------------ src/panoptes/pocs/observatory.py | 52 +--- .../pocs/state/states/default/analyzing.py | 25 -- .../pocs/state/states/default/pointing.py | 136 ---------- .../pocs/state/states/default/tracking.py | 27 -- tests/test_images.py | 124 --------- tests/test_ioptron.py | 96 ------- 7 files changed, 1 insertion(+), 707 deletions(-) delete mode 100644 src/panoptes/pocs/images.py delete mode 100644 src/panoptes/pocs/state/states/default/analyzing.py delete mode 100644 src/panoptes/pocs/state/states/default/pointing.py delete mode 100644 src/panoptes/pocs/state/states/default/tracking.py delete mode 100644 tests/test_images.py diff --git a/src/panoptes/pocs/images.py b/src/panoptes/pocs/images.py deleted file mode 100644 index d077ff1ce..000000000 --- a/src/panoptes/pocs/images.py +++ /dev/null @@ -1,248 +0,0 @@ -"""Image helpers for FITS headers, WCS solving, and pointing comparisons. - -Provides the Image class, a lightweight utility that loads FITS headers, lazily -solves for WCS (via panoptes-utils), and offers helpers to compute the pointing -and pointing error between images. -""" - -from collections import namedtuple -from contextlib import suppress -from pathlib import Path - -from astropy import units as u -from astropy.coordinates import FK5, EarthLocation, SkyCoord -from astropy.io import fits -from astropy.time import Time -from panoptes.utils.images import fits as fits_utils - -from panoptes.pocs.base import PanBase - -OffsetError = namedtuple("OffsetError", ["delta_ra", "delta_dec", "magnitude"]) - - -class Image(PanBase): - """Represents a single FITS image and associated pointing metadata. - - Loads core header values (DATE-OBS, EXPTIME), optionally reads/solves WCS, - and exposes convenience properties for header pointing vs. WCS pointing and - their differences. - """ - - def __init__(self, fits_file: Path, wcs_file=None, location=None, *args, **kwargs): - """Object to represent a single image from a PANOPTES camera. - - Args: - fits_file (str): Name of FITS file to be read (can be .fz) - wcs_file (str, optional): Name of FITS file to use for WCS - """ - super().__init__(*args, **kwargs) - # Make sure we have a Path instance - fits_file = Path(fits_file) - assert fits_file.exists(), self.logger.warning("File does not exist: {fits_file}") - - assert fits_file.suffix in [".fits", ".fz"], self.logger.warning("File must end with .fits") - - self.wcs = None - self._wcs_file = None - self.fits_file = fits_file - - if wcs_file is not None: - self.wcs_file = wcs_file - else: - self.wcs_file = fits_file - - self.header_ext = 0 - if fits_file.suffix == ".fz": - self.header_ext = 1 - - with fits.open(str(self.fits_file.absolute()), "readonly") as hdu: - self.header = hdu[self.header_ext].header - - required_headers = ["DATE-OBS", "EXPTIME"] - for key in required_headers: - if key not in self.header: - raise KeyError(f"Missing required FITS header: {key}") - - # Location Information - if location is None: - cfg_loc = self.get_config("location") - location = EarthLocation( - lat=cfg_loc["latitude"], - lon=cfg_loc["longitude"], - height=cfg_loc["elevation"], - ) - # Time Information - self.starttime = Time(self.header["DATE-OBS"], location=location) - self.exptime = float(self.header["EXPTIME"]) * u.second - self.midtime = self.starttime + (self.exptime / 2.0) - self.sidereal = self.midtime.sidereal_time("apparent") - self.FK5_Jnow = FK5(equinox=self.midtime) - - # Coordinates from header keywords - self.header_pointing = None - self.header_ra = None - self.header_dec = None - self.header_ha = None - - # Coordinates from WCS - self.pointing = None - self.ra = None - self.dec = None - self.ha = None - - self.get_header_pointing() - self.get_wcs_pointing() - - self._luminance = None - self._pointing = None - self._pointing_error = None - - @property - def wcs_file(self): - """WCS file name - - When setting the WCS file name, the WCS information will be read, - setting the `wcs` property. - """ - return self._wcs_file - - @wcs_file.setter - def wcs_file(self, filename): - """Set the path to the FITS file containing WCS information. - - Assigning this property will attempt to read WCS from the given file - and update the `wcs` attribute if a celestial WCS is present. - - Args: - filename (str | Path | None): Path to a FITS/FITS.fz file whose header - contains a valid celestial WCS. If None, no change is made. - """ - if filename is not None: - with suppress(AssertionError): - w = fits_utils.getwcs(str(filename)) - assert w.is_celestial - - self.wcs = w - self._wcs_file = filename - self.logger.debug("WCS loaded from image") - - @property - def pointing_error(self): - """Pointing error namedtuple (delta_ra, delta_dec, magnitude) - - Returns pointing error information. The first time this is accessed - this will solve the field if not previously solved. - - Returns: - namedtuple: Pointing error information - """ - if self._pointing_error is None: - assert self.pointing is not None, self.logger.warning( - "No world coordinate system (WCS), can't get pointing_error" - ) - assert self.header_pointing is not None - - if self.wcs is None: - self.solve_field() - - mag = self.pointing.separation(self.header_pointing) - d_dec = self.pointing.dec - self.header_pointing.dec - d_ra = self.pointing.ra - self.header_pointing.ra - - self._pointing_error = OffsetError(d_ra.to(u.arcsec), d_dec.to(u.arcsec), mag.to(u.arcsec)) - - return self._pointing_error - - def get_header_pointing(self): - """Get the pointing information from the header - - The header should contain the `RA-MNT` and `DEC-MNT` keywords, from which - the header pointing coordinates are built. - """ - try: - self.header_pointing = SkyCoord( - ra=float(self.header["RA-MNT"]) * u.degree, - dec=float(self.header["DEC-MNT"]) * u.degree, - ) - - self.header_ra = self.header_pointing.ra.to(u.degree) - self.header_dec = self.header_pointing.dec.to(u.degree) - - try: - self.header_ha = float(self.header["HA-MNT"]) * u.hourangle - except KeyError: - # Compute the HA from the RA and sidereal time. - # Precess to the current equinox otherwise the - # RA - LST method will be off. - # TODO(wtgee): This conversion doesn't seem to be correct. - # wtgee: I'm not sure what I meant by the above. May 2020. - self.header_ha = ( - self.header_pointing.transform_to(self.FK5_Jnow).ra.to(u.hourangle) - self.sidereal - ) - - except Exception as e: - self.logger.warning(f"Cannot get header pointing information: {e}") - - def get_wcs_pointing(self): - """Get the pointing information from the WCS - - Builds the pointing coordinates from the plate-solved WCS. These will be - compared with the coordinates stored in the header. - """ - if self.wcs is not None: - ra = self.wcs.celestial.wcs.crval[0] - dec = self.wcs.celestial.wcs.crval[1] - - self.pointing = SkyCoord(ra=ra * u.degree, dec=dec * u.degree) - - self.ra = self.pointing.ra.to(u.degree) - self.dec = self.pointing.dec.to(u.degree) - - # Precess to the current equinox otherwise the RA - LST method will be off. - self.ha = self.pointing.transform_to(self.FK5_Jnow).ra.to(u.degree) - self.sidereal - - def solve_field(self, radius=15, **kwargs): - """Solve field and populate WCS information. - - Args: - radius (scalar): The radius (in degrees) to search near RA-Dec. Defaults to 15°. - **kwargs: Options to be passed to `get_solve_field`. - """ - solve_info = fits_utils.get_solve_field( - str(self.fits_file), - ra=self.header_pointing.ra.value, - dec=self.header_pointing.dec.value, - radius=radius, - **kwargs, - ) - - self.wcs_file = solve_info["solved_fits_file"] - self.get_wcs_pointing() - - # Remove some fields - for header in ["COMMENT", "HISTORY"]: - with suppress(KeyError): - del solve_info[header] - - return solve_info - - def compute_offset(self, ref_image): - """Compute pointing offset relative to a reference Image. - - Args: - ref_image (Image): The reference image to compare against. - - Returns: - OffsetError: Named tuple of (delta_ra, delta_dec, magnitude) in arcseconds. - """ - assert isinstance(ref_image, Image), self.logger.warning("Must pass an Image class for reference") - - mag = self.pointing.separation(ref_image.pointing) - d_dec = self.pointing.dec - ref_image.pointing.dec - d_ra = self.pointing.ra - ref_image.pointing.ra - - return OffsetError(d_ra.to(u.arcsec), d_dec.to(u.arcsec), mag.to(u.arcsec)) - - def __str__(self): - """Human-readable identifier including file path and header pointing.""" - return f"{self.fits_file}: {self.header_pointing}" diff --git a/src/panoptes/pocs/observatory.py b/src/panoptes/pocs/observatory.py index 034e41a64..17cf678a6 100644 --- a/src/panoptes/pocs/observatory.py +++ b/src/panoptes/pocs/observatory.py @@ -24,7 +24,6 @@ from panoptes.pocs.base import PanBase from panoptes.pocs.camera import AbstractCamera from panoptes.pocs.dome import AbstractDome -from panoptes.pocs.images import Image from panoptes.pocs.mount.mount import AbstractMount from panoptes.pocs.scheduler.field import Field from panoptes.pocs.scheduler.observation.base import Observation @@ -642,7 +641,7 @@ def process_observation( self.logger.debug(f"Preparing {image_path=} for upload to {bucket_name=}") # Remove images directory from path so it's stored in bucket relative to images directory. - bucket_path = Path(image_path[image_path.find(images_dir) + len(images_dir) :]) + bucket_path = Path(image_path[image_path.find(images_dir) + len(images_dir):]) self.logger.debug(f"Adding {unit_id=} to {bucket_path=}") bucket_path = Path(unit_id) / bucket_path.relative_to("/") @@ -670,55 +669,6 @@ def process_observation( metadata["status"] = "complete" self.db.insert_current("images", metadata, store_permanently=False) - def analyze_recent(self): - """Analyze the most recent exposure - - Compares the most recent exposure to the reference exposure and determines - the offset between the two. - - Returns: - dict: Offset information - """ - # Clear the offset info - self.current_offset_info = None - - pointing_image_id, pointing_image = self.current_observation.pointing_image - self.logger.debug(f"Analyzing recent image using pointing image: '{pointing_image}'") - - try: - # Get the image to compare - image_id, image_path = self.current_observation.last_exposure - - current_image = Image(image_path, location=self.earth_location) - - solve_info = current_image.solve_field(skip_solved=False) - - self.logger.debug(f"Solve Info: {solve_info}") - - # Get the offset between the two - self.current_offset_info = current_image.compute_offset(pointing_image) - self.logger.debug(f"Offset Info: {self.current_offset_info}") - - # Store the offset information - self.db.insert_current( - "offset_info", - { - "image_id": image_id, - "d_ra": self.current_offset_info.delta_ra.value, - "d_dec": self.current_offset_info.delta_dec.value, - "magnitude": self.current_offset_info.magnitude.value, - "unit": "arcsec", - }, - store_permanently=False, - ) - - except error.SolveError: - self.logger.warning("Can't solve field, skipping") - except Exception as e: - self.logger.warning(f"Problem in analyzing: {e!r}") - - return self.current_offset_info - def update_tracking(self, **kwargs): """Update tracking with rate adjustment. diff --git a/src/panoptes/pocs/state/states/default/analyzing.py b/src/panoptes/pocs/state/states/default/analyzing.py deleted file mode 100644 index d38090cc4..000000000 --- a/src/panoptes/pocs/state/states/default/analyzing.py +++ /dev/null @@ -1,25 +0,0 @@ -def on_enter(event_data): - """ """ - pocs = event_data.model - - observation = pocs.observatory.current_observation - - pocs.next_state = "tracking" - try: - if pocs.get_config("mount.settings.update_tracking", False): - pocs.logger.debug("Analyzing recent image from analyzing state") - pocs.say(f"Analyzing image {observation.current_exp_num} / {observation.min_nexp}") - pocs.observatory.analyze_recent() - - if pocs.get_config("actions.FORCE_RESCHEDULE", False): - pocs.say("Forcing a move to the scheduler") - pocs.next_state = "scheduling" - - # Check if observation set is finished - if observation.set_is_finished: - pocs.next_state = "scheduling" - pocs.say(f"Observation complete, going to {pocs.next_state}") - - except Exception as e: - pocs.logger.error(f"Problem in analyzing: {e!r}") - pocs.next_state = "parking" diff --git a/src/panoptes/pocs/state/states/default/pointing.py b/src/panoptes/pocs/state/states/default/pointing.py deleted file mode 100644 index 57f2ceeee..000000000 --- a/src/panoptes/pocs/state/states/default/pointing.py +++ /dev/null @@ -1,136 +0,0 @@ -"""State: pointing. - -Capture a short exposure, solve it, measure pointing error, optionally correct, -then proceed to 'tracking'. -""" - -import numpy as np -from panoptes.utils.time import wait_for_events - -from panoptes.pocs.images import Image - -MAX_EXTRA_TIME = 60 # second - - -def on_enter(event_data): - """Pointing State - - Take 30 second exposure and plate-solve to get the pointing error - """ - pocs = event_data.model - - pocs.next_state = "parking" - - # Get pointing parameters - pointing_config = pocs.get_config("pointing") - max_attempts = int(pointing_config.get("max_attempts", 3)) - if max_attempts == 0: - pocs.logger.info(f"Skipping pointing state, {max_attempts=}") - pocs.next_state = "tracking" - return - - should_correct = pointing_config.get("auto_correct", False) - pointing_threshold = pointing_config.get("threshold", 0.05) # degrees - exptime = pointing_config.get("exptime", 30) # seconds - - # We want about 3 iterations of waiting loop during pointing image. - wait_delay = int(exptime / 3) + 1 - - try: - pocs.say("Taking pointing picture.") - - observation = pocs.observatory.current_observation - - fits_headers = pocs.observatory.get_standard_headers(observation=observation) - fits_headers["POINTING"] = "True" - pocs.logger.debug(f"Pointing headers: {fits_headers!r}") - - primary_camera = pocs.observatory.primary_camera - - # Loop over maximum number of pointing iterations - for img_num in range(max_attempts): - pocs.logger.info(f"Pointing image {img_num + 1}/{max_attempts} on: {primary_camera}") - - # Start the exposure - camera_event = primary_camera.take_observation( - observation, - headers=fits_headers, - exptime=exptime, - filename=f"pointing{img_num:02d}", - ) - - # Wait for images to complete - maximum_duration = exptime + MAX_EXTRA_TIME - - def waiting_cb(): - pocs.logger.info(f"Waiting for pointing image {img_num + 1}/{max_attempts}") - return pocs.is_safe() - - wait_for_events( - camera_event, timeout=maximum_duration, callback=waiting_cb, sleep_delay=wait_delay - ) - - # Analyze pointing - if observation is not None: - pointing_id, pointing_path = observation.pointing_image - pointing_image = Image( - pointing_path, - location=pocs.observatory.earth_location, - ) - pocs.logger.debug(f"Pointing image: {pointing_image}") - - pocs.say("Ok, I've got the pointing picture, let's see how close we are.") - pointing_image.solve_field() - - # Store the solved image object - observation.pointing_images[pointing_id] = pointing_image - - pocs.logger.debug(f"Pointing Coords: {pointing_image.pointing}") - pocs.logger.debug(f"Pointing Error: {pointing_image.pointing_error}") - - if should_correct is False: - pocs.logger.info("Pointing correction turned off, done with pointing.") - break - - delta_ra = pointing_image.pointing_error.delta_ra.value - delta_dec = pointing_image.pointing_error.delta_dec.value - - # Correct the pointing if either axis is off. - if np.abs(delta_ra) > pointing_threshold or np.abs(delta_dec) > pointing_threshold: - pocs.say("I'm still a bit away from the field so I'm going to get closer.") - - # Tell the mount we are at the field, which is the center - pocs.say("Syncing with the latest image...") - has_field = pocs.observatory.mount.set_target_coordinates(pointing_image.pointing) - pocs.logger.debug("Coords set, calibrating") - - # Calibrate the mount - Sync the mount's known position - # with the current actual position. - pocs.observatory.mount.query("calibrate_mount") - - # Now set back to field - if has_field: - if observation.field is not None: - pocs.logger.debug("Slewing back to target") - target_set = pocs.observatory.mount.set_target_coordinates(observation.field) - - # Check if target was set. - if target_set is False: - pocs.logger.warning("Field not properly set. Parking.") - else: - pocs.observatory.mount.slew_to_target(blocking=True) - - if img_num == (max_attempts - 1): - pocs.logger.info( - "Separation outside threshold but at max corrections. " - + "Will proceed to observations." - ) - else: - pocs.logger.info("Separation is within pointing threshold, starting tracking.") - break - - pocs.next_state = "tracking" - - except Exception as e: - pocs.logger.warning(f"Error in pointing: {e!r}") - pocs.say("Hmm, I had a problem checking the pointing error. Going to park.") diff --git a/src/panoptes/pocs/state/states/default/tracking.py b/src/panoptes/pocs/state/states/default/tracking.py deleted file mode 100644 index 02da05cea..000000000 --- a/src/panoptes/pocs/state/states/default/tracking.py +++ /dev/null @@ -1,27 +0,0 @@ -"""State: tracking. - -Fine-tune tracking (if configured) after slewing/pointing, then transition to -'observing'. If tracking update is disabled, proceed directly to observing. -""" - - -def on_enter(event_data): - """The unit is tracking the target. Proceed to observations.""" - pocs = event_data.model - pocs.next_state = "parking" - - if pocs.get_config("mount.settings.update_tracking", False): - pocs.next_state = "observing" - return - - # If we came from pointing then don't try to adjust - if event_data.transition.source != "pointing": - pocs.say("Checking our tracking") - try: - pocs.observatory.update_tracking() - pocs.say("Done with tracking adjustment, going to observe") - pocs.next_state = "observing" - except Exception as e: - pocs.logger.warning(f"Problem adjusting tracking: {e}") - else: - pocs.next_state = "observing" diff --git a/tests/test_images.py b/tests/test_images.py deleted file mode 100644 index 43eaeb72c..000000000 --- a/tests/test_images.py +++ /dev/null @@ -1,124 +0,0 @@ -import os -import shutil -import tempfile - -import pytest -from astropy import units as u -from astropy.coordinates import SkyCoord -from panoptes.utils.error import SolveError, Timeout - -from panoptes.pocs.images import Image, OffsetError - - -def copy_file_to_dir(to_dir, file): - assert os.path.isfile(file) - result = os.path.join(to_dir, os.path.basename(file)) - assert not os.path.exists(result) - shutil.copy(file, to_dir) - assert os.path.exists(result) - return result - - -def test_fits_exists(unsolved_fits_file): - with pytest.raises(AssertionError): - Image(unsolved_fits_file.replace(".fits", ".fit")) - - -def test_fits_extension(): - with pytest.raises(AssertionError): - Image(os.path.join(".", "pocs", "images.py")) - - -def test_fits_noheader(noheader_fits_file): - with pytest.raises(KeyError): - Image(noheader_fits_file) - - -@pytest.mark.skip("Need to fix timeout buffer in panoptes-utils") -def test_solve_timeout(tiny_fits_file): - with tempfile.TemporaryDirectory() as tmpdir: - tiny_fits_file = copy_file_to_dir(tmpdir, tiny_fits_file) - im0 = Image(tiny_fits_file) - assert str(im0) - with pytest.raises(Timeout): - im0.solve_field(verbose=True, replace=False, radius=4, timeout=0) - - -@pytest.mark.plate_solve -def test_fail_solve(tiny_fits_file): - with tempfile.TemporaryDirectory() as tmpdir: - tiny_fits_file = copy_file_to_dir(tmpdir, tiny_fits_file) - im0 = Image(tiny_fits_file) - assert str(im0) - with pytest.raises(SolveError): - im0.solve_field(verbose=True, replace=False, radius=4) - - -@pytest.mark.plate_solve -def test_solve_field_unsolved(unsolved_fits_file, solved_fits_file): - # We place the input images into a temp directory so that output images - # are also in the temp directory. - with tempfile.TemporaryDirectory() as tmpdir: - im0 = Image(copy_file_to_dir(tmpdir, unsolved_fits_file)) - - assert isinstance(im0, Image) - assert im0.wcs is None - assert im0.pointing is None - - im0.solve_field(verbose=True, replace=True, radius=4) - - assert im0.wcs is not None - assert im0.wcs_file is not None - assert isinstance(im0.pointing, SkyCoord) - assert im0.ra is not None - assert im0.dec is not None - assert im0.ha is not None - - # Compare it to another file of known offset. - im1 = Image(copy_file_to_dir(tmpdir, solved_fits_file)) - offset_info = im0.compute_offset(im1) - expected_offset = [10.1 * u.arcsec, 5.29 * u.arcsec, 8.77 * u.arcsec] - assert u.allclose(offset_info, expected_offset, rtol=0.1) - - -def test_solve_field_solved(solved_fits_file): - im0 = Image(solved_fits_file) - - assert isinstance(im0, Image) - assert im0.wcs is not None - assert im0.wcs_file is not None - assert im0.pointing is not None - assert im0.ra is not None - assert im0.dec is not None - assert im0.ha is not None - - im0.solve_field(verbose=True, radius=4) - - assert isinstance(im0.pointing, SkyCoord) - - -def test_pointing_error_no_wcs(unsolved_fits_file): - im0 = Image(unsolved_fits_file) - - with pytest.raises(AssertionError): - im0.pointing_error - - -def test_pointing_error_passed_wcs(unsolved_fits_file, solved_fits_file): - im0 = Image(unsolved_fits_file, wcs_file=solved_fits_file) - - assert isinstance(im0.pointing_error, OffsetError) - - -@pytest.mark.plate_solve -def test_pointing_error(solved_fits_file): - im0 = Image(solved_fits_file) - - im0.solve_field(verbose=True, replace=False, radius=4) - - perr = im0.pointing_error - assert isinstance(perr, OffsetError) - - assert (perr.delta_ra.to(u.degree).value - 1.647535444553057) < 1e-5 - assert (perr.delta_dec.to(u.degree).value - 1.560722632731533) < 1e-5 - assert (perr.magnitude.to(u.degree).value - 1.9445870862060288) < 1e-5 diff --git a/tests/test_ioptron.py b/tests/test_ioptron.py index 5c7856e26..196c592a0 100644 --- a/tests/test_ioptron.py +++ b/tests/test_ioptron.py @@ -2,11 +2,9 @@ from contextlib import suppress import pytest -from astropy import units as u from astropy.coordinates import EarthLocation from panoptes.utils.config.client import get_config -from panoptes.pocs.images import OffsetError from panoptes.pocs.mount.ioptron.cem40 import Mount from panoptes.pocs.utils.location import create_location_from_config @@ -65,97 +63,3 @@ def test_unpark_park(self): assert self.mount.is_parked is False self.mount.home_and_park() assert self.mount.is_parked is True - - -def test_get_tracking_correction(mount): - offsets = [ - # HA, ΔRA, ΔDec, Magnitude - (2, -13.0881456, 1.4009, 12.154), - (2, -13.0881456, -1.4009, 12.154), - (2, 13.0881456, 1.4009, 12.154), - (14, -13.0881456, 1.4009, 12.154), - (14, 13.0881456, 1.4009, 12.154), - # Too small - (2, -13.0881456, 0.4009, 2.154), - (2, 0.0881456, 1.4009, 2.154), - # Too big - (2, -13.0881456, 99999.4009, 2.154), - (2, -99999.0881456, 1.4009, 2.154), - ] - - corrections = [ - (103.49, "south", 966.84, "east"), - (103.49, "north", 966.84, "east"), - (103.49, "south", 966.84, "west"), - (103.49, "north", 966.84, "east"), - (103.49, "north", 966.84, "west"), - # Too small - (None, "south", 966.84, "east"), - (103.49, "south", None, "east"), - # Too big - (99999.0, "south", 966.84, "east"), - (103.49, "south", 99999.0, "east"), - ] - - for offset, correction in zip(offsets, corrections): - pointing_ha = offset[0] - offset_info = OffsetError(offset[1] * u.arcsec, offset[2] * u.arcsec, offset[3] * u.arcsec) - correction_info = mount.get_tracking_correction(offset_info, pointing_ha) - - dec_info = correction_info["dec"] - expected_correction = correction[0] - if expected_correction is not None: - assert dec_info[1] == pytest.approx(expected_correction, abs=1e-2) - assert dec_info[2] == correction[1] - else: - assert dec_info == expected_correction - - ra_info = correction_info["ra"] - expected_correction = correction[2] - if expected_correction is not None: - assert ra_info[1] == pytest.approx(expected_correction, abs=1e-2) - assert ra_info[2] == correction[3] - else: - assert ra_info == expected_correction - - -def test_get_tracking_correction_custom(mount): - min_tracking = 105 - max_tracking = 950 - - offsets = [ - # HA, ΔRA, ΔDec, Magnitude - (2, -13.0881456, 1.4009, 12.154), - (2, -13.0881456, -1.4009, 12.154), - ] - - corrections = [ - (None, "south", 950.0, "east"), - (None, "north", 950.0, "east"), - ] - - for offset, correction in zip(offsets, corrections): - pointing_ha = offset[0] - offset_info = OffsetError(offset[1] * u.arcsec, offset[2] * u.arcsec, offset[3] * u.arcsec) - correction_info = mount.get_tracking_correction( - offset_info, - pointing_ha, - min_tracking_threshold=min_tracking, - max_tracking_threshold=max_tracking, - ) - - dec_info = correction_info["dec"] - expected_correction = correction[0] - if expected_correction is not None: - assert dec_info[1] == pytest.approx(expected_correction, abs=1e-2) - assert dec_info[2] == correction[1] - else: - assert dec_info == expected_correction - - ra_info = correction_info["ra"] - expected_correction = correction[2] - if expected_correction is not None: - assert ra_info[1] == pytest.approx(expected_correction, abs=1e-2) - assert ra_info[2] == correction[3] - else: - assert ra_info == expected_correction From 01f64a41f2175d2930efba3eb8728aa66c74b94d Mon Sep 17 00:00:00 2001 From: Wilfred Tyler Gee Date: Sat, 20 Sep 2025 07:01:05 -1000 Subject: [PATCH 3/3] * Since we removed states, change `simple` back to `panoptes`. --- conf_files/pocs.yaml | 2 +- conf_files/state_table/panoptes.yaml | 155 ++++++++------------- conf_files/state_table/simple.yaml | 60 -------- src/panoptes/pocs/scheduler/__init__.py | 2 +- tests/scheduler/test_dispatch_scheduler.py | 2 +- 5 files changed, 58 insertions(+), 163 deletions(-) delete mode 100644 conf_files/state_table/simple.yaml diff --git a/conf_files/pocs.yaml b/conf_files/pocs.yaml index 91746bad3..67caffdfd 100644 --- a/conf_files/pocs.yaml +++ b/conf_files/pocs.yaml @@ -42,7 +42,7 @@ wait_delay: 180 # time in seconds before checking safety/etc while waiting. max_transition_attempts: 5 # number of transitions attempts. status_check_interval: 60 # periodic status check. -state_machine: simple +state_machine: panoptes scheduler: type: panoptes.pocs.scheduler.dispatch diff --git a/conf_files/state_table/panoptes.yaml b/conf_files/state_table/panoptes.yaml index 0fa5c62ca..3f5ca0a4a 100644 --- a/conf_files/state_table/panoptes.yaml +++ b/conf_files/state_table/panoptes.yaml @@ -2,104 +2,59 @@ name: default initial: sleeping states: - parking: - tags: always_safe - parked: - tags: always_safe - sleeping: - tags: always_safe - housekeeping: - tags: always_safe - ready: - tags: always_safe - scheduling: - horizon: observe - slewing: - pointing: - tracking: - observing: - analyzing: + parking: + tags: always_safe + parked: + tags: always_safe + sleeping: + tags: always_safe + housekeeping: + tags: always_safe + ready: + tags: always_safe + scheduling: + horizon: observe + slewing: + observing: transitions: - - - source: - - ready - - scheduling - - slewing - - pointing - - tracking - - observing - - analyzing - dest: parking - trigger: park - - - source: parking - dest: parked - trigger: set_park - - - source: parked - dest: housekeeping - trigger: clean_up - - - source: housekeeping - dest: sleeping - trigger: goto_sleep - - - source: parked - dest: ready - trigger: get_ready - conditions: mount_is_initialized - - - source: sleeping - dest: ready - trigger: get_ready - conditions: mount_is_initialized - - - source: ready - dest: scheduling - trigger: schedule - - - source: analyzing - dest: scheduling - trigger: schedule - - - source: scheduling - dest: slewing - trigger: start_slewing - - - source: scheduling - dest: tracking - trigger: adjust_tracking - - - source: slewing - dest: pointing - trigger: adjust_pointing - conditions: mount_is_tracking - - - source: pointing - dest: slewing - trigger: start_slewing - conditions: mount_is_tracking - - - source: pointing - dest: tracking - trigger: track - conditions: mount_is_tracking - - - source: tracking - dest: observing - trigger: observe - conditions: mount_is_tracking - - - source: observing - dest: analyzing - trigger: analyze - - - source: observing - dest: observing - trigger: observe - conditions: mount_is_tracking - - - source: analyzing - dest: tracking - trigger: adjust_tracking - conditions: mount_is_tracking + - source: + - ready + - scheduling + - slewing + - observing + dest: parking + trigger: park + - source: parking + dest: parked + trigger: set_park + - source: parked + dest: housekeeping + trigger: clean_up + - source: housekeeping + dest: sleeping + trigger: goto_sleep + - source: parked + dest: ready + trigger: get_ready + conditions: mount_is_initialized + - source: sleeping + dest: ready + trigger: get_ready + conditions: mount_is_initialized + - source: ready + dest: scheduling + trigger: schedule + - source: scheduling + dest: slewing + trigger: start_slewing + - source: slewing + dest: observing + trigger: observe + conditions: mount_is_tracking + - source: observing + dest: observing + trigger: observe + conditions: mount_is_tracking + - source: observing + dest: scheduling + trigger: schedule diff --git a/conf_files/state_table/simple.yaml b/conf_files/state_table/simple.yaml deleted file mode 100644 index 3f5ca0a4a..000000000 --- a/conf_files/state_table/simple.yaml +++ /dev/null @@ -1,60 +0,0 @@ ---- -name: default -initial: sleeping -states: - parking: - tags: always_safe - parked: - tags: always_safe - sleeping: - tags: always_safe - housekeeping: - tags: always_safe - ready: - tags: always_safe - scheduling: - horizon: observe - slewing: - observing: -transitions: - - source: - - ready - - scheduling - - slewing - - observing - dest: parking - trigger: park - - source: parking - dest: parked - trigger: set_park - - source: parked - dest: housekeeping - trigger: clean_up - - source: housekeeping - dest: sleeping - trigger: goto_sleep - - source: parked - dest: ready - trigger: get_ready - conditions: mount_is_initialized - - source: sleeping - dest: ready - trigger: get_ready - conditions: mount_is_initialized - - source: ready - dest: scheduling - trigger: schedule - - source: scheduling - dest: slewing - trigger: start_slewing - - source: slewing - dest: observing - trigger: observe - conditions: mount_is_tracking - - source: observing - dest: observing - trigger: observe - conditions: mount_is_tracking - - source: observing - dest: scheduling - trigger: schedule diff --git a/src/panoptes/pocs/scheduler/__init__.py b/src/panoptes/pocs/scheduler/__init__.py index f1bf17d88..07819889c 100644 --- a/src/panoptes/pocs/scheduler/__init__.py +++ b/src/panoptes/pocs/scheduler/__init__.py @@ -56,7 +56,7 @@ def create_scheduler_from_config(config=None, observer=None, iers_url=None, *arg observer = site_details.observer # Read the targets from the file - fields_file = Path(scheduler_config.get("fields_file", "simple.yaml")) + fields_file = Path(scheduler_config.get("fields_file", "panoptes.yaml")) base_dir = Path(str(get_config("directories.base", default="."))) fields_dir = Path(str(get_config("directories.fields", default="./conf_files/fields"))) fields_path = base_dir / fields_dir / fields_file diff --git a/tests/scheduler/test_dispatch_scheduler.py b/tests/scheduler/test_dispatch_scheduler.py index d250d4789..801a96236 100644 --- a/tests/scheduler/test_dispatch_scheduler.py +++ b/tests/scheduler/test_dispatch_scheduler.py @@ -30,7 +30,7 @@ def field_file(): scheduler_config = get_config("scheduler", default={}) # Read the targets from the file - fields_file = scheduler_config.get("fields_file", "simple.yaml") + fields_file = scheduler_config.get("fields_file", "panoptes.yaml") fields_path = os.path.join(get_config("directories.fields"), fields_file) return fields_path