From 24bae634e4347c109839d5c4b0eb0f5e2f0f481e Mon Sep 17 00:00:00 2001 From: ccolin Date: Fri, 1 Nov 2024 11:42:48 -0500 Subject: [PATCH 01/60] test docs migration to abstract parent --- pyscope/observatory/ascom_camera.py | 12 ------------ pyscope/observatory/camera.py | 12 ++++++++++++ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pyscope/observatory/ascom_camera.py b/pyscope/observatory/ascom_camera.py index 07989e01..15809d16 100644 --- a/pyscope/observatory/ascom_camera.py +++ b/pyscope/observatory/ascom_camera.py @@ -39,18 +39,6 @@ def __init__(self, identifier, alpaca=False, device_number=0, protocol="http"): self._camera_time = True def AbortExposure(self): - """ - Abort the current exposure immediately and return camera to idle. - See `CanAbortExposure` for support and possible reasons to abort. - - Parameters - ---------- - None - - Returns - ------- - None - """ logger.debug(f"ASCOMCamera.AbortExposure() called") self._device.AbortExposure() diff --git a/pyscope/observatory/camera.py b/pyscope/observatory/camera.py index 1052ebff..2a6d691b 100644 --- a/pyscope/observatory/camera.py +++ b/pyscope/observatory/camera.py @@ -10,6 +10,18 @@ def __init__(self, *args, **kwargs): @abstractmethod def AbortExposure(self): + """ + Abort the current exposure immediately and return camera to idle. + See `CanAbortExposure` for support and possible reasons to abort. + + Parameters + ---------- + None + + Returns + ------- + None + """ pass @abstractmethod From e37bcfc773dad5a4229753c6849ee41c12f583fc Mon Sep 17 00:00:00 2001 From: ccolin Date: Fri, 1 Nov 2024 11:52:43 -0500 Subject: [PATCH 02/60] camera init docs --- pyscope/observatory/camera.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pyscope/observatory/camera.py b/pyscope/observatory/camera.py index 2a6d691b..75099629 100644 --- a/pyscope/observatory/camera.py +++ b/pyscope/observatory/camera.py @@ -6,6 +6,20 @@ class Camera(ABC, metaclass=_DocstringInheritee): @abstractmethod def __init__(self, *args, **kwargs): + """ + Abstract class for camera devices. + + The class defines the interface for camera devices, including methods for controlling + exposures, guiding, and retrieving camera properties. Subclasses must implement + the abstract methods defined in this class. + + Parameters + ---------- + *args : `tuple` + Variable length argument list. + **kwargs : `dict` + Arbitrary keyword arguments. + """ pass @abstractmethod From 1e91e29707122e55f36c23a2924b978980632260 Mon Sep 17 00:00:00 2001 From: ccolin Date: Fri, 1 Nov 2024 12:32:16 -0500 Subject: [PATCH 03/60] Migrate ascom_camera docs into abstract Camera class Migrated all of the ascom_camera method and property docs that were declared in the abstract Camera class Certain properties and methods have docs for both classes still since they reference ASCOM standards or ASCOM device implementation In these cases, instead of migration, I provided a more general doc for the abstract class --- pyscope/observatory/ascom_camera.py | 212 ---------------------- pyscope/observatory/camera.py | 264 ++++++++++++++++++++++++++++ 2 files changed, 264 insertions(+), 212 deletions(-) diff --git a/pyscope/observatory/ascom_camera.py b/pyscope/observatory/ascom_camera.py index 15809d16..f88edbcf 100644 --- a/pyscope/observatory/ascom_camera.py +++ b/pyscope/observatory/ascom_camera.py @@ -104,48 +104,12 @@ def PulseGuide(self, Direction, Duration): self._device.PulseGuide(Direction, Duration) def StartExposure(self, Duration, Light): - """ - Starts an exposure with a given duration and light status. Check `ImageReady` for operation completion. - - Parameters - ---------- - Duration : `float` - The exposure duration in seconds. Can be zero if `Light` is `False`. - - Light : `bool` - Whether the exposure is a light frame (`True`) or a dark frame (`False`). - - Returns - ------- - None - - Notes - ----- - `Duration` can be shorter than `ExposureMin` if used for dark frame or bias exposure. - Bias frame also allows a `Duration` of zero. - """ logger.debug(f"ASCOMCamera.StartExposure({Duration}, {Light}) called") self._last_exposure_duration = Duration self._last_exposure_start_time = str(Time.now()) self._device.StartExposure(Duration, Light) def StopExposure(self): - """ - Stops the current exposure gracefully. - - Parameters - ---------- - None - - Returns - ------- - None - - Notes - ----- - Readout process will initiate if stop is called during an exposure. - Ignored if readout is already in process. - """ logger.debug(f"ASCOMCamera.StopExposure() called") self._device.StopExposure() @@ -173,11 +137,6 @@ def BayerOffsetY(self): # pragma: no cover @property def BinX(self): - """ - The binning factor in the X/column direction. (`int`) - - Default is 1 after camera connection is established. - """ logger.debug(f"ASCOMCamera.BinX property called") return self._device.BinX @@ -188,11 +147,6 @@ def BinX(self, value): @property def BinY(self): - """ - The binning factor in the Y/row direction. (`int`) - - Default is 1 after camera connection is established. - """ logger.debug(f"ASCOMCamera.BinY property called") return self._device.BinY @@ -220,13 +174,11 @@ def CameraState(self): @property def CameraXSize(self): - """The width of the CCD chip in unbinned pixels. (`int`)""" logger.debug(f"ASCOMCamera.CameraXSize property called") return self._device.CameraXSize @property def CameraYSize(self): - """The height of the CCD chip in unbinned pixels. (`int`)""" logger.debug(f"ASCOMCamera.CameraYSize property called") return self._device.CameraYSize @@ -238,35 +190,21 @@ def CameraTime(self): @property def CanAbortExposure(self): - """ - Whether the camera can abort exposures imminently. (`bool`) - - Aborting is not synonymous with stopping an exposure. - Aborting immediately stops the exposure and discards the data. - Used for urgent situations such as errors or temperature concerns. - See `CanStopExposure` for gracious cancellation of an exposure. - """ logger.debug(f"ASCOMCamera.CanAbortExposure property called") return self._device.CanAbortExposure @property def CanAsymmetricBin(self): - """ - Whether the camera supports asymmetric binning such that - `BinX` != `BinY`. (`bool`) - """ logger.debug(f"ASCOMCamera.CanAsymmetricBin property called") return self._device.CanAsymmetricBin @property def CanFastReadout(self): - """Whether the camera supports fast readout mode. (`bool`)""" logger.debug(f"ASCOMCamera.CanFastReadout property called") return self._device.CanFastReadout @property def CanGetCoolerPower(self): - """Whether the camera's cooler power setting can be read. (`bool`)""" logger.debug(f"ASCOMCamera.CanGetCoolerPower property called") return self._device.CanGetCoolerPower @@ -281,37 +219,21 @@ def CanPulseGuide(self): @property def CanSetCCDTemperature(self): - """ - Whether the camera's CCD temperature can be set. (`bool`) - - A false means either the camera uses an open-loop cooling system or - does not support adjusting the CCD temperature from software. - """ logger.debug(f"ASCOMCamera.CanSetCCDTemperature property called") return self._device.CanSetCCDTemperature @property def CanStopExposure(self): - """ - Whether the camera can stop exposures graciously. (`bool`) - - Stopping is not synonymous with aborting an exposure. - Stopping allows the camera to complete the current exposure cycle, then stop. - Image data up to the point of stopping is typically still available. - See `CanAbortExposure` for instant cancellation of an exposure. - """ logger.debug(f"ASCOMCamera.CanStopExposure property called") return self._device.CanStopExposure @property def CCDTemperature(self): - """The current CCD temperature in degrees Celsius. (`float`)""" logger.debug(f"ASCOMCamera.CCDTemperature property called") return self._device.CCDTemperature @property def CoolerOn(self): - """Whether the camera's cooler is on. (`bool`)""" logger.debug(f"ASCOMCamera.CoolerOn property called") return self._device.CoolerOn @@ -322,53 +244,31 @@ def CoolerOn(self, value): @property def CoolerPower(self): - """The current cooler power level as a percentage. (`float`)""" logger.debug(f"ASCOMCamera.CoolerPower property called") return self._device.CoolerPower @property def ElectronsPerADU(self): - """Gain of the camera in photoelectrons per analog-to-digital-unit. (`float`)""" logger.debug(f"ASCOMCamera.ElectronsPerADU() property called") return self._device.ElectronsPerADU @property def ExposureMax(self): - """The maximum exposure duration supported by `StartExposure` in seconds. (`float`)""" logger.debug(f"ASCOMCamera.ExposureMax property called") return self._device.ExposureMax @property def ExposureMin(self): - """ - The minimum exposure duration supported by `StartExposure` in seconds. (`float`) - - Non-zero number, except for bias frame acquisition, where an exposure < ExposureMin - may be possible. - """ logger.debug(f"ASCOMCamera.ExposureMin property called") return self._device.ExposureMin @property def ExposureResolution(self): - """ - The smallest increment in exposure duration supported by `StartExposure`. (`float`) - - This property could be useful if one wants to implement a 'spin control' interface - for fine-tuning exposure durations. - - Providing a `Duration` to `StartExposure` that is not a multiple of `ExposureResolution` - will choose the closest available value. - - A value of 0.0 indicates no minimum resolution increment, except that imposed by the - floating-point precision of `float` itself. - """ logger.debug(f"ASCOMCamera.ExposureResolution property called") return self._device.ExposureResolution @property def FastReadout(self): - """Whether the camera is in fast readout mode. (`bool`)""" logger.debug(f"ASCOMCamera.FastReadout property called") return self._device.FastReadout @@ -379,30 +279,11 @@ def FastReadout(self, value): @property def FullWellCapacity(self): - """ - The full well capacity of the camera in electrons with the - current camera settings. (`float`) - """ logger.debug(f"ASCOMCamera.FullWellCapacity property called") return self._device.FullWellCapacity @property def Gain(self): - """ - The camera's gain OR index of the selected camera gain description. - See below for more information. (`int`) - - Represents either the camera's gain in photoelectrons per analog-to-digital-unit, - or the 0-index of the selected camera gain description in the `Gains` array. - - Depending on a camera's capabilities, the driver can support none, one, or both - representation modes, but only one mode will be active at a time. - - To determine operational mode, read the `GainMin`, `GainMax`, and `Gains` properties. - - `ReadoutMode` may affect the gain of the camera, so it is recommended to set - driver behavior to ensure no conflictions occur if both `Gain` and `ReadoutMode` are used. - """ logger.debug(f"ASCOMCamera.Gain property called") return self._device.Gain @@ -413,44 +294,26 @@ def Gain(self, value): @property def GainMax(self): - """The maximum gain value supported by the camera. (`int`)""" logger.debug(f"ASCOMCamera.GainMax property called") return self._device.GainMax @property def GainMin(self): - """The minimum gain value supported by the camera. (`int`)""" logger.debug(f"ASCOMCamera.GainMin property called") return self._device.GainMin @property def Gains(self): - """ - 0-indexed array of camera gain descriptions supported by the camera. (`list` of `str`) - - Depending on implementation, the array may contain ISOs, or gain names. - """ logger.debug(f"ASCOMCamera.Gains property called") return self._device.Gains @property def HasShutter(self): - """ - Whether the camera has a mechanical shutter. (`bool`) - - If `False`, i.e. the camera has no mechanical shutter, the `StartExposure` - method will ignore the `Light` parameter. - """ logger.debug(f"ASCOMCamera.HasShutter property called") return self._device.HasShutter @property def HeatSinkTemperature(self): - """ - The current heat sink temperature in degrees Celsius. (`float`) - - The readout is only valid if `CanSetCCDTemperature` is `True`. - """ logger.debug(f"ASCOMCamera.HeatSinkTemperature property called") return self._device.HeatSinkTemperature @@ -503,27 +366,16 @@ def ImageArray(self): @property def ImageReady(self): - """ - Whether the camera has completed an exposure and the image is ready to be downloaded. (`bool`) - - If `False`, the `ImageArray` property will exit with an exception. - """ logger.debug(f"ASCOMCamera.ImageReady property called") return self._device.ImageReady @property def IsPulseGuiding(self): - """Whether the camera is currently pulse guiding. (`bool`)""" logger.debug(f"ASCOMCamera.IsPulseGuiding property called") return self._device.IsPulseGuiding @property def LastExposureDuration(self): - """ - The duration of the last exposure in seconds. (`float`) - - May differ from requested exposure time due to shutter latency, camera timing accuracy, etc. - """ logger.debug(f"ASCOMCamera.LastExposureDuration property called") last_exposure_duration = self._device.LastExposureDuration if last_exposure_duration is None or last_exposure_duration == 0: @@ -533,11 +385,6 @@ def LastExposureDuration(self): @property def LastExposureStartTime(self): - """ - The actual last exposure start time in FITS CCYY-MM-DDThh:mm:ss[.sss...] format. (`str`) - - The date string represents UTC time. - """ logger.debug(f"ASCOMCamera.LastExposureStartTime property called") last_time = self._device.LastExposureStartTime """ This code is needed to handle the case of the ASCOM ZWO driver @@ -561,33 +408,21 @@ def LastInputExposureDuration(self, value): @property def MaxADU(self): - """The maximum ADU value the camera is capable of producing. (`int`)""" logger.debug(f"ASCOMCamera.MaxADU property called") return self._device.MaxADU @property def MaxBinX(self): - """ - The maximum allowed binning factor in the X/column direction. (`int`) - - Value equivalent to `MaxBinY` if `CanAsymmetricBin` is `False`. - """ logger.debug(f"ASCOMCamera.MaxBinX property called") return self._device.MaxBinX @property def MaxBinY(self): - """ - The maximum allowed binning factor in the Y/row direction. (`int`) - - Value equivalent to `MaxBinX` if `CanAsymmetricBin` is `False`. - """ logger.debug(f"ASCOMCamera.MaxBinY property called") return self._device.MaxBinY @property def NumX(self): - """The width of the subframe in binned pixels. (`int`)""" logger.debug(f"ASCOMCamera.NumX property called") return self._device.NumX @@ -598,7 +433,6 @@ def NumX(self, value): @property def NumY(self): - """The height of the subframe in binned pixels. (`int`)""" logger.debug(f"ASCOMCamera.NumY property called") return self._device.NumY @@ -609,21 +443,6 @@ def NumY(self, value): @property def Offset(self): - """ - The camera's offset OR index of the selected camera offset description. - See below for more information. (`int`) - - Represents either the camera's offset, or the 0-index of the selected - camera offset description in the `Offsets` array. - - Depending on a camera's capabilities, the driver can support none, one, or both - representation modes, but only one mode will be active at a time. - - To determine operational mode, read the `OffsetMin`, `OffsetMax`, and `Offsets` properties. - - `ReadoutMode` may affect the gain of the camera, so it is recommended to set - driver behavior to ensure no conflictions occur if both `Gain` and `ReadoutMode` are used. - """ logger.debug(f"ASCOMCamera.Offset property called") return self._device.Offset @@ -634,52 +453,36 @@ def Offset(self, value): @property def OffsetMax(self): - """The maximum offset value supported by the camera. (`int`)""" logger.debug(f"ASCOMCamera.OffsetMax property called") return self._device.OffsetMax @property def OffsetMin(self): - """The minimum offset value supported by the camera. (`int`)""" logger.debug(f"ASCOMCamera.OffsetMin property called") return self._device.OffsetMin @property def Offsets(self): - """The array of camera offset descriptions supported by the camera. (`list` of `str`)""" logger.debug(f"ASCOMCamera.Offsets property called") return self._device.Offsets @property def PercentCompleted(self): - """ - The percentage of completion of the current operation. (`int`) - - As opposed to `CoolerPower`, this is represented as an integer - s.t. 0 <= PercentCompleted <= 100 instead of float. - """ logger.debug(f"ASCOMCamera.PercentCompleted property called") return self._device.PercentCompleted @property def PixelSizeX(self): - """The width of the CCD chip pixels in microns. (`float`)""" logger.debug(f"ASCOMCamera.PixelSizeX property called") return self._device.PixelSizeX @property def PixelSizeY(self): - """The height of the CCD chip pixels in microns. (`float`)""" logger.debug(f"ASCOMCamera.PixelSizeY property called") return self._device.PixelSizeY @property def ReadoutMode(self): - """ - Current readout mode of the camera as an index. (`int`) - - The index corresponds to the `ReadoutModes` array. - """ logger.debug(f"ASCOMCamera.ReadoutMode property called") return self._device.ReadoutMode @@ -690,17 +493,11 @@ def ReadoutMode(self, value): @property def ReadoutModes(self): - """The array of camera readout mode descriptions supported by the camera. (`list` of `str`)""" logger.debug(f"ASCOMCamera.ReadoutModes property called") return self._device.ReadoutModes @property def SensorName(self): - """ - The name of the sensor in the camera. (`str`) - - The name is the manufacturer's data sheet part number. - """ logger.debug(f"ASCOMCamera.SensorName property called") return self._device.SensorName @@ -723,12 +520,6 @@ def SensorType(self): @property def SetCCDTemperature(self): - """ - The set-target CCD temperature in degrees Celsius. (`float`) - - Contrary to `CCDTemperature`, which is the current CCD temperature, - this property is the target temperature for the cooler to reach. - """ logger.debug(f"ASCOMCamera.SetCCDTemperature property called") return self._device.SetCCDTemperature @@ -739,7 +530,6 @@ def SetCCDTemperature(self, value): @property def StartX(self): - """The set X/column position of the start subframe in binned pixels. (`int`)""" logger.debug(f"ASCOMCamera.StartX property called") return self._device.StartX @@ -750,7 +540,6 @@ def StartX(self, value): @property def StartY(self): - """The set Y/row position of the start subframe in binned pixels. (`int`)""" logger.debug(f"ASCOMCamera.StartY property called") return self._device.StartY @@ -761,7 +550,6 @@ def StartY(self, value): @property def SubExposureDuration(self): - """The duration of the subframe exposure interval in seconds. (`float`)""" logger.debug(f"ASCOMCamera.SubExposureDuration property called") return self._device.SubExposureDuration diff --git a/pyscope/observatory/camera.py b/pyscope/observatory/camera.py index 75099629..8b75c329 100644 --- a/pyscope/observatory/camera.py +++ b/pyscope/observatory/camera.py @@ -40,29 +40,89 @@ def AbortExposure(self): @abstractmethod def PulseGuide(self, Direction, Duration): + """ + Moves the scope in the given direction for the specified duration. + + Parameters + ---------- + Direction : `int` + The direction in which to move the scope. + Value representations for direction are up to the camera manufacturer, + or in case of a lack of manufacturer specification, the developer. + See :py:meth:`ASCOMCamera.PulseGuide` for an example. + Duration : `int` + The duration of the guide pulse in milliseconds. + + Returns + ------- + None + """ pass @abstractmethod def StartExposure(self, Duration, Light): + """ + Starts an exposure with a given duration and light status. Check `ImageReady` for operation completion. + + Parameters + ---------- + Duration : `float` + The exposure duration in seconds. Can be zero if `Light` is `False`. + + Light : `bool` + Whether the exposure is a light frame (`True`) or a dark frame (`False`). + + Returns + ------- + None + + Notes + ----- + `Duration` can be shorter than `ExposureMin` if used for dark frame or bias exposure. + Bias frame also allows a `Duration` of zero. + """ pass @abstractmethod def StopExposure(self): + """ + Stops the current exposure gracefully. + + Parameters + ---------- + None + + Returns + ------- + None + + Notes + ----- + Readout process will initiate if stop is called during an exposure. + Ignored if readout is already in process. + """ pass @property @abstractmethod def BayerOffsetX(self): + """The X/column offset of the Bayer filter array matrix. (`int`)""" pass @property @abstractmethod def BayerOffsetY(self): + """The Y/row offset of the Bayer filter array matrix. (`int`)""" pass @property @abstractmethod def BinX(self): + """ + The binning factor in the X/column direction. (`int`) + + Default is 1 after camera connection is established. + """ pass @BinX.setter @@ -73,6 +133,11 @@ def BinX(self, value): @property @abstractmethod def BinY(self): + """ + The binning factor in the Y/row direction. (`int`) + + Default is 1 after camera connection is established. + """ pass @BinY.setter @@ -83,61 +148,101 @@ def BinY(self, value): @property @abstractmethod def CameraState(self): + """ + The current operational state of the camera. (`enum`) + + Possible values are at the discretion of the camera manufacturer specification. + In case of a lack of one, discretion is at the developer. + See :py:meth:`ASCOMCamera.CameraState` for an example. + """ pass @property @abstractmethod def CameraXSize(self): + """The width of the CCD chip in unbinned pixels. (`int`)""" pass @property @abstractmethod def CameraYSize(self): + """The height of the CCD chip in unbinned pixels. (`int`)""" pass @property @abstractmethod def CanAbortExposure(self): + """ + Whether the camera can abort exposures imminently. (`bool`) + + Aborting is not synonymous with stopping an exposure. + Aborting immediately stops the exposure and discards the data. + Used for urgent situations such as errors or temperature concerns. + See `CanStopExposure` for gracious cancellation of an exposure. + """ pass @property @abstractmethod def CanAsymmetricBin(self): + """ + Whether the camera supports asymmetric binning such that + `BinX` != `BinY`. (`bool`) + """ pass @property @abstractmethod def CanFastReadout(self): + """Whether the camera supports fast readout mode. (`bool`)""" pass @property @abstractmethod def CanGetCoolerPower(self): + """Whether the camera's cooler power setting can be read. (`bool`)""" pass @property @abstractmethod def CanPulseGuide(self): + """Whether the camera supports pulse guiding. (`bool`)""" pass @property @abstractmethod def CanSetCCDTemperature(self): + """ + Whether the camera's CCD temperature can be set. (`bool`) + + A false means either the camera uses an open-loop cooling system or + does not support adjusting the CCD temperature from software. + """ pass @property @abstractmethod def CanStopExposure(self): + """ + Whether the camera can stop exposures graciously. (`bool`) + + Stopping is not synonymous with aborting an exposure. + Stopping allows the camera to complete the current exposure cycle, then stop. + Image data up to the point of stopping is typically still available. + See `CanAbortExposure` for instant cancellation of an exposure. + """ pass @property @abstractmethod def CCDTemperature(self): + """The current CCD temperature in degrees Celsius. (`float`)""" pass @property @abstractmethod def CoolerOn(self): + """Whether the camera's cooler is on. (`bool`)""" pass @CoolerOn.setter @@ -148,31 +253,53 @@ def CoolerOn(self, value): @property @abstractmethod def CoolerPower(self): + """The current cooler power level as a percentage. (`float`)""" pass @property @abstractmethod def ElectronsPerADU(self): + """Gain of the camera in photoelectrons per analog-to-digital-unit. (`float`)""" pass @property @abstractmethod def ExposureMax(self): + """The maximum exposure duration supported by `StartExposure` in seconds. (`float`)""" pass @property @abstractmethod def ExposureMin(self): + """ + The minimum exposure duration supported by `StartExposure` in seconds. (`float`) + + Non-zero number, except for bias frame acquisition, where an exposure < ExposureMin + may be possible. + """ pass @property @abstractmethod def ExposureResolution(self): + """ + The smallest increment in exposure duration supported by `StartExposure`. (`float`) + + This property could be useful if one wants to implement a 'spin control' interface + for fine-tuning exposure durations. + + Providing a `Duration` to `StartExposure` that is not a multiple of `ExposureResolution` + will choose the closest available value. + + A value of 0.0 indicates no minimum resolution increment, except that imposed by the + floating-point precision of `float` itself. + """ pass @property @abstractmethod def FastReadout(self): + """Whether the camera is in fast readout mode. (`bool`)""" pass @FastReadout.setter @@ -183,11 +310,30 @@ def FastReadout(self, value): @property @abstractmethod def FullWellCapacity(self): + """ + The full well capacity of the camera in electrons with the + current camera settings. (`float`) + """ pass @property @abstractmethod def Gain(self): + """ + The camera's gain OR index of the selected camera gain description. + See below for more information. (`int`) + + Represents either the camera's gain in photoelectrons per analog-to-digital-unit, + or the 0-index of the selected camera gain description in the `Gains` array. + + Depending on a camera's capabilities, the driver can support none, one, or both + representation modes, but only one mode will be active at a time. + + To determine operational mode, read the `GainMin`, `GainMax`, and `Gains` properties. + + `ReadoutMode` may affect the gain of the camera, so it is recommended to set + driver behavior to ensure no conflictions occur if both `Gain` and `ReadoutMode` are used. + """ pass @Gain.setter @@ -198,71 +344,135 @@ def Gain(self, value): @property @abstractmethod def GainMax(self): + """The maximum gain value supported by the camera. (`int`)""" pass @property @abstractmethod def GainMin(self): + """The minimum gain value supported by the camera. (`int`)""" pass @property @abstractmethod def Gains(self): + """ + 0-indexed array of camera gain descriptions supported by the camera. (`list` of `str`) + + Depending on implementation, the array may contain ISOs, or gain names. + """ pass @property @abstractmethod def HasShutter(self): + """ + Whether the camera has a mechanical shutter. (`bool`) + + If `False`, i.e. the camera has no mechanical shutter, the `StartExposure` + method will ignore the `Light` parameter. + """ pass @property @abstractmethod def HeatSinkTemperature(self): + """ + The current heat sink temperature in degrees Celsius. (`float`) + + The readout is only valid if `CanSetCCDTemperature` is `True`. + """ pass @property @abstractmethod def ImageArray(self): + """ + Retrieve the image data captured by the camera as a numpy array. + + The image array contains the pixel data from the camera sensor, formatted + as a 2D or 3D numpy array depending on the camera's capabilities and settings. + The data type and shape of the array may vary based on the camera's configuration. + + Returns + ------- + numpy.ndarray + The image data captured by the camera. + + Notes + ----- + The exact format and data type of the returned array should be documented + by the specific camera implementation. This method should handle any necessary + data type conversions and ensure the array is in a standard orientation. + """ pass @property @abstractmethod def ImageReady(self): + """ + Whether the camera has completed an exposure and the image is ready to be downloaded. (`bool`) + + If `False`, the `ImageArray` property will exit with an exception. + """ pass @property @abstractmethod def IsPulseGuiding(self): + """Whether the camera is currently pulse guiding. (`bool`)""" pass @property @abstractmethod def LastExposureDuration(self): + """ + The duration of the last exposure in seconds. (`float`) + + May differ from requested exposure time due to shutter latency, camera timing accuracy, etc. + """ pass @property @abstractmethod def LastExposureStartTime(self): + """ + The actual last exposure start time in FITS CCYY-MM-DDThh:mm:ss[.sss...] format. (`str`) + + The date string represents UTC time. + """ pass @property @abstractmethod def MaxADU(self): + """The maximum ADU value the camera is capable of producing. (`int`)""" pass @property @abstractmethod def MaxBinX(self): + """ + The maximum allowed binning factor in the X/column direction. (`int`) + + Value equivalent to `MaxBinY` if `CanAsymmetricBin` is `False`. + """ pass @property @abstractmethod def MaxBinY(self): + """ + The maximum allowed binning factor in the Y/row direction. (`int`) + + Value equivalent to `MaxBinX` if `CanAsymmetricBin` is `False`. + """ pass @property @abstractmethod def NumX(self): + """The width of the subframe in binned pixels. (`int`)""" pass @NumX.setter @@ -273,6 +483,7 @@ def NumX(self, value): @property @abstractmethod def NumY(self): + """The height of the subframe in binned pixels. (`int`)""" pass @NumY.setter @@ -283,6 +494,21 @@ def NumY(self, value): @property @abstractmethod def Offset(self): + """ + The camera's offset OR index of the selected camera offset description. + See below for more information. (`int`) + + Represents either the camera's offset, or the 0-index of the selected + camera offset description in the `Offsets` array. + + Depending on a camera's capabilities, the driver can support none, one, or both + representation modes, but only one mode will be active at a time. + + To determine operational mode, read the `OffsetMin`, `OffsetMax`, and `Offsets` properties. + + `ReadoutMode` may affect the gain of the camera, so it is recommended to set + driver behavior to ensure no conflictions occur if both `Gain` and `ReadoutMode` are used. + """ pass @Offset.setter @@ -293,36 +519,52 @@ def Offset(self, value): @property @abstractmethod def OffsetMax(self): + """The maximum offset value supported by the camera. (`int`)""" pass @property @abstractmethod def OffsetMin(self): + """The minimum offset value supported by the camera. (`int`)""" pass @property @abstractmethod def Offsets(self): + """The array of camera offset descriptions supported by the camera. (`list` of `str`)""" pass @property @abstractmethod def PercentCompleted(self): + """ + The percentage of completion of the current operation. (`int`) + + As opposed to `CoolerPower`, this is represented as an integer + s.t. 0 <= PercentCompleted <= 100 instead of float. + """ pass @property @abstractmethod def PixelSizeX(self): + """The width of the CCD chip pixels in microns. (`float`)""" pass @property @abstractmethod def PixelSizeY(self): + """The height of the CCD chip pixels in microns. (`float`)""" pass @property @abstractmethod def ReadoutMode(self): + """ + Current readout mode of the camera as an index. (`int`) + + The index corresponds to the `ReadoutModes` array. + """ pass @ReadoutMode.setter @@ -333,21 +575,40 @@ def ReadoutMode(self, value): @property @abstractmethod def ReadoutModes(self): + """The array of camera readout mode descriptions supported by the camera. (`list` of `str`)""" pass @property @abstractmethod def SensorName(self): + """ + The name of the sensor in the camera. (`str`) + + The name is the manufacturer's data sheet part number. + """ pass @property @abstractmethod def SensorType(self): + """ + The type of color information the camera sensor captures. (`enum`) + + Possible types and the corresponding values are at the discretion of the camera manufacturer. + In case of a lack of specification, discretion is at the developer. + See :py:meth:`ASCOMCamera.SensorType` for an example. + """ pass @property @abstractmethod def SetCCDTemperature(self): + """ + The set-target CCD temperature in degrees Celsius. (`float`) + + Contrary to `CCDTemperature`, which is the current CCD temperature, + this property is the target temperature for the cooler to reach. + """ pass @SetCCDTemperature.setter @@ -358,6 +619,7 @@ def SetCCDTemperature(self, value): @property @abstractmethod def StartX(self): + """The set X/column position of the start subframe in binned pixels. (`int`)""" pass @StartX.setter @@ -368,6 +630,7 @@ def StartX(self, value): @property @abstractmethod def StartY(self): + """The set Y/row position of the start subframe in binned pixels. (`int`)""" pass @StartY.setter @@ -378,6 +641,7 @@ def StartY(self, value): @property @abstractmethod def SubExposureDuration(self): + """The duration of the subframe exposure interval in seconds. (`float`)""" pass @SubExposureDuration.setter From ff3d7a487ac46297d178f7c04113166206aa9c10 Mon Sep 17 00:00:00 2001 From: ccolin Date: Fri, 1 Nov 2024 19:50:22 -0500 Subject: [PATCH 04/60] tilde quickfix --- 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 f88edbcf..c72528af 100644 --- a/pyscope/observatory/ascom_camera.py +++ b/pyscope/observatory/ascom_camera.py @@ -22,7 +22,7 @@ def __init__(self, identifier, alpaca=False, device_number=0, protocol="http"): Whether to use the Alpaca protocol for Alpaca-compatible devices. device_number : `int`, default : 0, optional The device number. This is only used if the identifier is a ProgID. - protocol : `str`, default : `http`, optional + protocol : `str`, default : "http", optional The protocol to use for Alpaca-compatible devices. """ super().__init__( From a896527d85b9758d563e64da1962171b22457701 Mon Sep 17 00:00:00 2001 From: ccolin Date: Fri, 1 Nov 2024 19:50:41 -0500 Subject: [PATCH 05/60] device abstract docs --- pyscope/observatory/device.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pyscope/observatory/device.py b/pyscope/observatory/device.py index fbfcc72f..68cabda8 100644 --- a/pyscope/observatory/device.py +++ b/pyscope/observatory/device.py @@ -6,11 +6,26 @@ class Device(ABC, metaclass=_DocstringInheritee): @abstractmethod def __init__(self, *args, **kwargs): + """ + Abstract base class for all deivce types. + + This class defines the common interface for all devices. + Includes connection status and device name properties. Subclasses must implement the + abstract methods and properties in this class. + + Parameters + ---------- + *args : `tuple` + Variable length argument list. + **kwargs : `dict` + Arbitrary keyword arguments. + """ pass @property @abstractmethod def Connected(self): + """Whether the device is connected or not. (`bool`)""" pass @Connected.setter @@ -21,4 +36,5 @@ def Connected(self, value): @property @abstractmethod def Name(self): + """The shorthand name of the device for display only. (`str`)""" pass From 02af8ebcd13a952d8db556cc54cf65eebaa8f656 Mon Sep 17 00:00:00 2001 From: ccolin Date: Fri, 1 Nov 2024 19:50:52 -0500 Subject: [PATCH 06/60] ascom_device docs --- pyscope/observatory/ascom_device.py | 95 +++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/pyscope/observatory/ascom_device.py b/pyscope/observatory/ascom_device.py index 94a2d830..b39cbba3 100644 --- a/pyscope/observatory/ascom_device.py +++ b/pyscope/observatory/ascom_device.py @@ -9,6 +9,23 @@ class ASCOMDevice(Device): def __init__(self, identifier, alpaca=False, device_type="Device", **kwargs): + """ + Represents a generic ASCOM device. + + Provides a common interface to interact with ASCOM-compatible devices. + Supports both Alpaca and COM-based ASCOM devices, allowing for cross-platform compatibility. + + Parameters + ---------- + identifier : `str` + The unique identifier for the ASCOM device. This can be the ProgID for COM devices or the device number for Alpaca devices. + alpaca : `bool`, default : `False`, optional + Whether the device is an Alpaca device and should use the appropriate communication protocol. + device_type : `str`, default : "Device", optional + The type of the ASCOM device (e.g. "Telescope", "Camera", etc.) + **kwargs : `dict`, optional + Additional keyword arguments to pass to the device constructor. + """ logger.debug(f"ASCOMDevice.__init__({identifier}, alpaca={alpaca}, {kwargs})") self._identifier = identifier self._device = None @@ -30,13 +47,42 @@ def __init__(self, identifier, alpaca=False, device_type="Device", **kwargs): raise ObservatoryException("If you are not on Windows, you must use Alpaca") def Action(self, ActionName, *ActionParameters): # pragma: no cover + """ + Invokes the device-specific custom action on the device. + + Parameters + ---------- + ActionName : `str` + The name of the action to invoke. Action names are either specified by the device driver or are well known names agreed upon and constructed by interested parties. + ActionParameters : `list` + The required parameters for the given action. Empty string if none are required. + + Returns + ------- + `str` + The result of the action. The return value is dependent on the action being invoked and the representations are set by the driver author. + + Notes + ----- + See `SupportedActions` for a list of supported actions set up by the driver author. + Action names are case-insensitive, so be aware when creating new actions. + """ logger.debug(f"ASCOMDevice.Action({ActionName}, {ActionParameters})") return self._device.Action(ActionName, *ActionParameters) def CommandBlind(self, Command, Raw): # pragma: no cover """ + Sends a command to the device and does not wait for a response. + .. deprecated:: 0.1.1 ASCOM is deprecating this method. + + Parameters + ---------- + Command : `str` + The command string to send to the device. + Raw : `bool` + If `True`, the command is set as-is. If `False`, protocol framing characters may be added onto the command. """ logger.debug(f"ASCOMDevice.CommandBlind({Command}, {Raw})") @@ -44,8 +90,22 @@ def CommandBlind(self, Command, Raw): # pragma: no cover def CommandBool(self, Command, Raw): # pragma: no cover """ + Sends a command to the device and waits for a boolean response. + .. deprecated:: 0.1.1 ASCOM is deprecating this method. + + Parameters + ---------- + Command : `str` + The command string to send to the device. + Raw : `bool` + If `True`, the command is set as-is. If `False`, protocol framing characters may be added onto the command. + + Returns + ------- + `bool` + The boolean response from the device. """ logger.debug(f"ASCOMDevice.CommandBool({Command}, {Raw})") @@ -53,8 +113,22 @@ def CommandBool(self, Command, Raw): # pragma: no cover def CommandString(self, Command, Raw): # pragma: no cover """ + Sends a command to the device and waits for a string response. + .. deprecated:: 0.1.1 ASCOM is deprecating this method. + + Parameters + ---------- + Command : `str` + The command string to send to the device. + Raw : `bool` + If `True`, the command is set as-is. If `False`, protocol framing characters may be added onto the command. + + Returns + ------- + `str` + The string response from the device. """ logger.debug(f"ASCOMDevice.CommandString({Command}, {Raw})") @@ -72,21 +146,41 @@ def Connected(self, value): @property def Description(self): + """ + The description of the device such as the manufacturer and model number. (`str`) + + Description should be limited to 64 characters so that it can be used in FITS headers. + """ logger.debug(f"ASCOMDevice.Description property") return self._device.Description @property def DriverInfo(self): + """ + Description and version information about this ASCOM driver. (`str`) + + Length of info can contain line endings and may be up to thousands of characters long. + Version data and copyright data should be included. + See `Description` for information on the device itself. + To get the version number in a parseable string, use `DriverVersion`. + """ logger.debug(f"ASCOMDevice.DriverInfo property") return self._device.DriverInfo @property def DriverVersion(self): + """ + The driver version number, containing only the major and minor version numbers. (`str`) + + The format is "n.n" where "n" is a number. + Not to be confused with `InterfaceVersion`, which is the version of the specification supported by the driver. + """ logger.debug(f"ASCOMDevice.DriverVersion property") return self._device.DriverVersion @property def InterfaceVersion(self): + """Interface version number that this device supports. (`int`)""" logger.debug(f"ASCOMDevice.InterfaceVersion property") return self._device.InterfaceVersion @@ -97,5 +191,6 @@ def Name(self): @property def SupportedActions(self): + """List of custom action names supported by this driver. (`list`)""" logger.debug(f"ASCOMDevice.SupportedActions property") return self._device.SupportedActions From cb32cb741e984867bece3b084cb8141041d397a8 Mon Sep 17 00:00:00 2001 From: ccolin Date: Fri, 1 Nov 2024 20:25:30 -0500 Subject: [PATCH 07/60] abstract cover calibrator docs --- pyscope/observatory/cover_calibrator.py | 80 +++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/pyscope/observatory/cover_calibrator.py b/pyscope/observatory/cover_calibrator.py index 2c314db6..6329d338 100644 --- a/pyscope/observatory/cover_calibrator.py +++ b/pyscope/observatory/cover_calibrator.py @@ -6,44 +6,124 @@ class CoverCalibrator(ABC, metaclass=_DocstringInheritee): @abstractmethod def __init__(self, *args, **kwargs): + """ + Abstract base class for a cover calibrator device. + + Defines the common interface for cover calibrator devices, including methods + for controlling the cover and calibrator light. Subclasses must implement the + abstract methods and properties in this class. + + Parameters + ---------- + *args : `tuple` + Variable length argument list. + **kwargs : `dict` + Arbitrary keyword arguments. + """ pass @abstractmethod def CalibratorOff(self): + """ + Turns calibrator off if the device has a calibrator. + + If the calibrator requires time to safely stabilise, `CalibratorState` must show that the calibrator is not ready yet. + When the calibrator has stabilised, `CalibratorState` must show that the calibrator is ready and off. + If a device has both cover and calibrator capabilities, the method will return `CoverState` to its original status before `CalibratorOn` was called. + An error condition arising while turning off the calibrator must be indicated in `CalibratorState`. + """ pass @abstractmethod def CalibratorOn(self, Brightness): + """ + Turns the calibrator on at the specified brightness if the device has a calibrator. + + If the calibrator requires time to safely stabilise, `CalibratorState` must show that the calibrator is not ready yet. + When the calibrator has stabilised, `CalibratorState` must show that the calibrator is ready and on. + If a device has both cover and calibrator capabilities, the method may change `CoverState`. + An error condition arising while turning on the calibrator must be indicated in `CalibratorState`. + + Parameters + ---------- + Brightness : `int` + The illumination brightness to set the calibrator at in the range 0 to `MaxBrightness`. + """ pass @abstractmethod def CloseCover(self): + """ + Starts closing the cover if the device has a cover. + + While the cover is closing, `CoverState` must show that the cover is moving. + When the cover is fully closed, `CoverState` must show that the cover is closed. + If an error condition arises while closing the cover, it must be indicated in `CoverState`. + """ pass @abstractmethod def HaltCover(self): + """ + Stops any present cover movement if the device has a cover and cover movement can be halted. + + Stops cover movement as soon as possible and sets `CoverState` to an appropriate value such as open or closed. + """ pass @abstractmethod def OpenCover(self): + """ + Starts opening the cover if the device has a cover. + + While the cover is opening, `CoverState` must show that the cover is moving. + When the cover is fully open, `CoverState` must show that the cover is open. + If an error condition arises while opening the cover, it must be indicated in `CoverState`. + """ pass @property @abstractmethod def Brightness(self): + """ + The current calibrator brightness in the range of 0 to `MaxBrightness`. + + The brightness must be 0 if the `CalibratorState` is not off. + """ pass @property @abstractmethod def CalibratorState(self): + """ + The state of the calibrator device, if present, otherwise indicate that it does not exist. + + When the calibrator is changing the state must indicate that the calibrator is busy. + If the device is unaware of the calibrator state, such as if hardware doesn't report the state and the calibrator was just powered on, it must indicate as such. + Users should be able to carry out commands like `CalibratorOn` and `CalibratorOff` regardless of this unknown state. + """ pass @property @abstractmethod def CoverState(self): + """ + The state of the cover device, if present, otherwise indicate that it does not exist. + + When the cover is changing the state must indicate that the cover is busy. + If the device is unaware of the cover state, such as if hardware doesn't report the state and the cover was just powered on, it must indicate as such. + Users should be able to carry out commands like `OpenCover`, `CloseCover`, and `HaltCover` regardless of this unknown state. + """ pass @property @abstractmethod def MaxBrightness(self): + """ + Brightness value that makes the calibrator produce the maximum illumination supported. (`int`) + + A value of 1 indicates that the calibrator can only be off or on. + A value of any other X should indicate that the calibrator has X discreet brightness levels in addition to off (0). + Value determined by the device manufacturer or driver author based on hardware capabilities. + """ pass From c00ee908f98c77e5acaee3151fe7eeef86e9d7f8 Mon Sep 17 00:00:00 2001 From: ccolin Date: Fri, 1 Nov 2024 20:51:44 -0500 Subject: [PATCH 08/60] added missing return types to abstract docs --- pyscope/observatory/cover_calibrator.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pyscope/observatory/cover_calibrator.py b/pyscope/observatory/cover_calibrator.py index 6329d338..f35ae166 100644 --- a/pyscope/observatory/cover_calibrator.py +++ b/pyscope/observatory/cover_calibrator.py @@ -86,9 +86,9 @@ def OpenCover(self): @abstractmethod def Brightness(self): """ - The current calibrator brightness in the range of 0 to `MaxBrightness`. + The current calibrator brightness in the range of 0 to `MaxBrightness`. (`int`) - The brightness must be 0 if the `CalibratorState` is not off. + The brightness must be 0 if the `CalibratorState` is `Off`. """ pass @@ -96,11 +96,12 @@ def Brightness(self): @abstractmethod def CalibratorState(self): """ - The state of the calibrator device, if present, otherwise indicate that it does not exist. + The state of the calibrator device, if present, otherwise indicate that it does not exist. (`enum`) When the calibrator is changing the state must indicate that the calibrator is busy. If the device is unaware of the calibrator state, such as if hardware doesn't report the state and the calibrator was just powered on, it must indicate as such. Users should be able to carry out commands like `CalibratorOn` and `CalibratorOff` regardless of this unknown state. + Enum values representing states is determined by the device manufacturer or driver author. """ pass @@ -108,7 +109,7 @@ def CalibratorState(self): @abstractmethod def CoverState(self): """ - The state of the cover device, if present, otherwise indicate that it does not exist. + The state of the cover device, if present, otherwise indicate that it does not exist. (`enum`) When the cover is changing the state must indicate that the cover is busy. If the device is unaware of the cover state, such as if hardware doesn't report the state and the cover was just powered on, it must indicate as such. From 281d68cfc922eee15d1463325878bc437a9fa82f Mon Sep 17 00:00:00 2001 From: ccolin Date: Fri, 1 Nov 2024 20:52:08 -0500 Subject: [PATCH 09/60] ascom_cover_calibrator docs --- pyscope/observatory/ascom_cover_calibrator.py | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/pyscope/observatory/ascom_cover_calibrator.py b/pyscope/observatory/ascom_cover_calibrator.py index d3c7d1bb..01ce4a3e 100644 --- a/pyscope/observatory/ascom_cover_calibrator.py +++ b/pyscope/observatory/ascom_cover_calibrator.py @@ -8,6 +8,23 @@ class ASCOMCoverCalibrator(ASCOMDevice, CoverCalibrator): def __init__(self, identifier, alpaca=False, device_number=0, protocol="http"): + """ + ASCOM implementation of the :py:meth:`CoverCalibrator` abstract base class. + + Provides the functionality to control an ASCOM-compatible cover calibrator device, + including methods for controlling the cover and calibrator light. + + Parameters + ---------- + identifier : `str` + The device identifier. + alpaca : `bool`, default : `False`, optional + Whether the device is an Alpaca device. + device_number : `int`, default : 0, optional + The device number. + protocol : `str`, default : "http", optional + The communication protocol to use. + """ super().__init__( identifier, alpaca=alpaca, @@ -17,22 +34,62 @@ def __init__(self, identifier, alpaca=False, device_number=0, protocol="http"): ) def CalibratorOff(self): + """ + Turns calibrator off if the device has a calibrator. + + If the calibrator requires time to safely stabilise, `CalibratorState` must return `NotReady`. + When the calibrator has stabilised, `CalibratorState` must return `Off`. + If a device has both cover and calibrator capabilities, the method will return `CoverState` to its original status before `CalibratorOn` was called. + If an error condition arises while turning off the calibrator, `CalibratorState` must return `Error`. + """ logger.debug(f"ASCOMCoverCalibrator.CalibratorOff() called") self._device.CalibratorOff() def CalibratorOn(self, Brightness): + """ + Turns the calibrator on at the specified brightness if the device has a calibrator. + + If the calibrator requires time to safely stabilise, `CalibratorState` must return `NotReady`. + When the calibrator has stabilised, `CalibratorState` must return `Ready`. + If a device has both cover and calibrator capabilities, the method may change `CoverState`. + If an error condition arises while turning on the calibrator, `CalibratorState` must return `Error`. + + Parameters + ---------- + Brightness : `int` + The illumination brightness to set the calibrator at in the range 0 to `MaxBrightness`. + """ logger.debug(f"ASCOMCoverCalibrator.CalibratorOn({Brightness}) called") self._device.CalibratorOn(Brightness) def CloseCover(self): + """ + Starts closing the cover if the device has a cover. + + While the cover is closing, `CoverState` must return `Moving`. + When the cover is fully closed, `CoverState` must return `Closed`. + If an error condition arises while closing the cover, `CoverState` must return `Error`. + """ logger.debug(f"ASCOMCoverCalibrator.CloseCover() called") self._device.CloseCover() def HaltCover(self): + """ + Stops any present cover movement if the device has a cover and cover movement can be halted. + + Stops cover movement as soon as possible and sets `CoverState` to `Open`, `Closed`, or `Unknown` appropriately. + """ logger.debug(f"ASCOMCoverCalibrator.HaltCover() called") self._device.HaltCover() def OpenCover(self): + """ + Starts opening the cover if the device has a cover. + + While the cover is opening, `CoverState` must return `Moving`. + When the cover is fully open, `CoverState` must return `Open`. + If an error condition arises while opening the cover, `CoverState` must return `Error`. + """ logger.debug(f"ASCOMCoverCalibrator.OpenCover() called") self._device.OpenCover() @@ -43,11 +100,47 @@ def Brightness(self): @property def CalibratorState(self): + """ + The state of the calibrator device, if present, otherwise return `NotPresent`. (`CalibratorStatus `_) + + When the calibrator is changing the state must be `NotReady`. + If the device is unaware of the calibrator state, such as if hardware doesn't report the state and the calibrator was just powered on, the state must be `Unknown`. + Users should be able to carry out commands like `CalibratorOn` and `CalibratorOff` regardless of this unknown state. + + Returns + ------- + `CalibratorStatus` + The state of the calibrator device in the following representations: + * 0 : `NotPresent`, the device has no calibrator. + * 1 : `Off`, the calibrator is off. + * 2 : `NotReady`, the calibrator is stabilising or hasn't finished the last command. + * 3 : `Ready`, the calibrator is ready. + * 4 : `Unknown`, the state is unknown. + * 5 : `Error`, an error occurred while changing states. + """ logger.debug(f"ASCOMCoverCalibrator.CalibratorState property called") return self._device.CalibratorState @property def CoverState(self): + """ + The state of the cover device, if present, otherwise return `NotPresent`. (`CoverStatus `_) + + When the cover is changing the state must be `Moving`. + If the device is unaware of the cover state, such as if hardware doesn't report the state and the cover was just powered on, the state must be `Unknown`. + Users should be able to carry out commands like `OpenCover`, `CloseCover`, and `HaltCover` regardless of this unknown state. + + Returns + ------- + `CoverStatus` + The state of the cover device in the following representations: + * 0 : `NotPresent`, the device has no cover. + * 1 : `Closed`, the cover is closed. + * 2 : `Moving`, the cover is moving. + * 3 : `Open`, the cover is open. + * 4 : `Unknown`, the state is unknown. + * 5 : `Error`, an error occurred while changing states. + """ logger.debug(f"ASCOMCoverCalibrator.CoverState property called") return self._device.CoverState From c81ae1963828d55168b8e58aba6c1fe01d115629 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 3 Nov 2024 14:15:43 +0100 Subject: [PATCH 10/60] pre-commit autoupdate && pre-commit run --all-files --- .pre-commit-config.yaml | 4 ++-- requirements.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9b717fc8..afc015a0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v5.0.0 hooks: - id: check-ast - id: check-case-conflict @@ -14,7 +14,7 @@ repos: - id: isort args: [--profile=black] - repo: https://github.com/psf/black - rev: 24.3.0 + rev: 24.10.0 hooks: - id: black - repo: https://github.com/pre-commit/pygrep-hooks diff --git a/requirements.txt b/requirements.txt index 68eeb74c..44f72285 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,8 +7,8 @@ ccdproc == 2.4.2 click == 8.1.7 cmcrameri == 1.9 markdown == 3.6 -numpy == 2.1.0 matplotlib == 3.9.1 +numpy == 2.1.0 oschmod == 0.3.12 paramiko == 3.4.0 photutils == 1.13.0 From db65527c413137fec42d76cad1351684543bb058 Mon Sep 17 00:00:00 2001 From: ccolin Date: Sun, 3 Nov 2024 07:24:34 -0600 Subject: [PATCH 11/60] dome abstract and ascom_dome docs --- pyscope/observatory/ascom_dome.py | 30 +++++++ pyscope/observatory/dome.py | 129 ++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+) diff --git a/pyscope/observatory/ascom_dome.py b/pyscope/observatory/ascom_dome.py index 11121981..59204607 100644 --- a/pyscope/observatory/ascom_dome.py +++ b/pyscope/observatory/ascom_dome.py @@ -8,6 +8,23 @@ class ASCOMDome(ASCOMDevice, Dome): def __init__(self, identifier, alpaca=False, device_number=0, protocol="http"): + """ + ASCOM implementation of the Dome abstract base class. + + This class provides the functionality to control an ASCOM-compatible dome device, + including methods for controlling the dome's movement and shutter. + + Parameters + ---------- + identifier : `str` + The unique device identifier. This can be the ProgID for COM devices or the device number for Alpaca devices. + alpaca : `bool`, default : `False`, optional + Whether to use the Alpaca protocol. If `False`, the COM protocol is used. + device_number : `int`, default : 0, optional + The device number for Alpaca devices. + protocol : `str`, default : "http", optional + The protocol to use for Alpaca devices. + """ super().__init__( identifier, alpaca=alpaca, @@ -114,6 +131,19 @@ def CanSyncAzimuth(self): @property def ShutterStatus(self): + """ + The status of the dome shutter or roof structure. (`ShutterState `_) + + Raises error if there's no shutter control, or if the shutter status can not be read, returns the last shutter state. + Enum values and their representations are as follows: + + * 0 : Open + * 1 : Closed + * 2 : Opening + * 3 : Closing + * 4 : Error + * Other : Unknown + """ logger.debug(f"ASCOMDome.ShutterStatus property called") shutter_status = self._device.ShutterStatus if shutter_status == 0: diff --git a/pyscope/observatory/dome.py b/pyscope/observatory/dome.py index 4acb30e7..17ecbb1f 100644 --- a/pyscope/observatory/dome.py +++ b/pyscope/observatory/dome.py @@ -6,115 +6,244 @@ class Dome(ABC, metaclass=_DocstringInheritee): @abstractmethod def __init__(self, *args, **kwargs): + """ + Abstract base class for dome devices. + + This class defines the common interface for dome devices, including methods for controlling + the dome's movement and shutter. Subclasses must implement the abstract methods and properties + in this class. + + Parameters + ---------- + *args : `tuple` + Variable length argument list. + **kwargs : `dict` + Arbitrary keyword arguments. + """ pass @abstractmethod def AbortSlew(self): + """ + Immediately stop any dome movement. + + This method will disable hardware slewing immediately, and sets `Slaved` to `False`. + """ pass @abstractmethod def CloseShutter(self): + """Closes the shutter.""" pass @abstractmethod def FindHome(self): + """ + Starts searching for the dome home position. + + The method and the homing operation should be able to run asynchronously, meaning + that any additional `FindHome` method calls shouldn't block current homing. + After the homing is complete, the `AtHome` property should be set to `True`, and the `Azimuth` synchronized to the appropriate value. + """ pass @abstractmethod def OpenShutter(self): + """Opens the shutter.""" pass @abstractmethod def Park(self): + """ + Move the dome to the park position along the dome azimuth axis. + + Sets the `AtPark` property to `True` after completion, and should raise an error if `Slaved`. + """ pass @abstractmethod def SetPark(self): + """Sets the current azimuth position as the park position.""" pass @abstractmethod def SlewToAltitude(self, Altitude): + """ + Slews the dome to the specified altitude. + + The method and the slewing operation should be able to run asynchronously, meaning + that any additional `SlewToAltitude` method calls shouldn't block current slewing. + + Parameters + ---------- + Altitude : `float` + The altitude to slew to in degrees. + """ pass @abstractmethod def SlewToAzimuth(self, Azimuth): + """ + Slews the dome to the specified azimuth. + + The method and the slewing operation should be able to run asynchronously, meaning + that any additional `SlewToAzimuth` method calls shouldn't block current slewing. + + Parameters + ---------- + Azimuth : `float` + The azimuth to slew to in degrees. + """ pass @abstractmethod def SyncToAzimuth(self, Azimuth): + """ + Synchronizes the dome azimuth counter to the specified value. + + Parameters + ---------- + Azimuth : `float` + The azimuth to synchronize to in degrees. + """ pass @property @abstractmethod def Altitude(self): + """ + Altitude of the part of the sky that the observer is planning to observe. (`float`) + + Driver should determine how to best locate the dome aperture in order to expose the desired part of the sky, + meaning it must consider all shutters, clamshell segments, or roof mechanisms present in the dome + to determine the required aperture. + + Raises error if there's no altitude control. + If altitude can not be read, returns the altitude of the last slew position. + """ pass @property @abstractmethod def AtHome(self): + """ + Whether the dome is in the home position. (`bool`) + + Normally used post `FindHome` method call. + Value is reset to `False` after any slew operation, except if the slewing just so happens to request the home position. + If the dome hardware is capable of detecting when slewing "passes by" the home position, + `AtHome` may become `True` during that slewing. + Due to some devices holding a small range of azimuths as the home position, or due to accuracy losses + from dome inertia, resolution of the home position sensor, the azimuth encoder, and/or floating point errors, + applications shouldn't assume sensed azimuths will be identical each time `AtHome` is `True`. + """ pass @property @abstractmethod def AtPark(self): + """ + Whether the dome is in the park position. (`bool`) + + Unlike `AtHome`, `AtPark` is explicitly only set after a `Park` method call, + and is reset with any slew operation. + Due to some devices holding a small range of azimuths as the park position, or due to accuracy losses + from dome inertia, resolution of the park position sensor, the azimuth encoder, and/or floating point errors, + applications shouldn't assume sensed azimuths will be identical each time `AtPark` is `True`. + """ pass @property @abstractmethod def Azimuth(self): + """ + Current azimuth of the dome. (`float`) + + If the azimuth can not be read, returns the azimuth of the last slew position. + """ pass @property @abstractmethod def CanFindHome(self): + """Whether the dome can find the home position, i.e. is it capable of `FindHome`. (`bool`)""" pass @property @abstractmethod def CanPark(self): + """Whether the dome can park, i.e. is it capable of `Park`. (`bool`)""" pass @property @abstractmethod def CanSetAltitude(self): + """Whether the dome can set the altitude, i.e. is it capable of `SlewToAltitude`. (`bool`)""" pass @property @abstractmethod def CanSetAzimuth(self): + """ + Whether the dome can set the azimuth, i.e. is it capable of `SlewToAzimuth`. (`bool`) + + In simpler terms, whether the dome is equipped with rotation control or not. + """ pass @property @abstractmethod def CanSetPark(self): + """Whether the dome can set the park position, i.e. is it capable of `SetPark`. (`bool`)""" pass @property @abstractmethod def CanSetShutter(self): + """Whether the dome can set the shutter state, i.e. is it capable of `OpenShutter` and `CloseShutter`. (`bool`)""" pass @property @abstractmethod def CanSlave(self): + """ + Whether the dome can be slaved to a telescope, i.e. is it capable of `Slaved` states. (`bool`) + + This should only be `True` if the dome has its own slaving mechanism; + a dome driver should not query a telescope driver directly. + """ pass @property @abstractmethod def CanSyncAzimuth(self): + """Whether the dome can synchronize the azimuth, i.e. is it capable of `SyncToAzimuth`. (`bool`)""" pass @property @abstractmethod def ShutterStatus(self): + """ + The status of the dome shutter or roof structure. (`enum`) + + Raises error if there's no shutter control, or if the shutter status can not be read, returns the last shutter state. + Enum values and their representations are defined by the driver. + See :py:attr:`ASCOMDome.ShutterStatus` for an example. + """ pass @property @abstractmethod def Slaved(self): + """ + Whether the dome is currently slaved to a telescope. (`bool`) + + During operations, set to `True` to enable hardware slewing, if capable, see `CanSlave`. + """ pass @property @abstractmethod def Slewing(self): + """Whether the dome is currently slewing in any direction. (`bool`)""" pass From 344dd82958da04a875d26861ef52f9e7fa19b908 Mon Sep 17 00:00:00 2001 From: ccolin Date: Sun, 3 Nov 2024 07:30:49 -0600 Subject: [PATCH 12/60] py:meth - py:attr mixup fix --- pyscope/observatory/ascom_camera.py | 2 +- pyscope/observatory/camera.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyscope/observatory/ascom_camera.py b/pyscope/observatory/ascom_camera.py index c72528af..97cc61dc 100644 --- a/pyscope/observatory/ascom_camera.py +++ b/pyscope/observatory/ascom_camera.py @@ -75,7 +75,7 @@ def SetImageDataType(self): def PulseGuide(self, Direction, Duration): """ Moves scope in the given direction for the given interval or time at the rate - given by the :py:meth:`ASCOMTelescope.GuideRateRightAscension` and :py:meth:`ASCOMTelescope.GuideRateDeclination` properties. + given by the :py:attr:`ASCOMTelescope.GuideRateRightAscension` and :py:attr:`ASCOMTelescope.GuideRateDeclination` properties. Parameters ---------- diff --git a/pyscope/observatory/camera.py b/pyscope/observatory/camera.py index 8b75c329..51c36b35 100644 --- a/pyscope/observatory/camera.py +++ b/pyscope/observatory/camera.py @@ -153,7 +153,7 @@ def CameraState(self): Possible values are at the discretion of the camera manufacturer specification. In case of a lack of one, discretion is at the developer. - See :py:meth:`ASCOMCamera.CameraState` for an example. + See :py:attr:`ASCOMCamera.CameraState` for an example. """ pass @@ -596,7 +596,7 @@ def SensorType(self): Possible types and the corresponding values are at the discretion of the camera manufacturer. In case of a lack of specification, discretion is at the developer. - See :py:meth:`ASCOMCamera.SensorType` for an example. + See :py:attr:`ASCOMCamera.SensorType` for an example. """ pass From 4c304c00ef6d8b90996739fbeffb08deadd9baea Mon Sep 17 00:00:00 2001 From: ccolin Date: Sun, 3 Nov 2024 10:09:26 -0600 Subject: [PATCH 13/60] filter wheel abstract and ascom filter wheel docs --- pyscope/observatory/ascom_filter_wheel.py | 17 ++++++++++++ pyscope/observatory/filter_wheel.py | 34 +++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/pyscope/observatory/ascom_filter_wheel.py b/pyscope/observatory/ascom_filter_wheel.py index 101a0507..b0d1c0c8 100644 --- a/pyscope/observatory/ascom_filter_wheel.py +++ b/pyscope/observatory/ascom_filter_wheel.py @@ -8,6 +8,23 @@ class ASCOMFilterWheel(ASCOMDevice, FilterWheel): def __init__(self, identifier, alpaca=False, device_number=0, protocol="http"): + """ + ASCOM implementation of the FilterWheel abstract base class. + + This class provides the functionality to control an ASCOM-compatible filter wheel device, + including properties for focus offsets, filter names, and the current filter position. + + Parameters + ---------- + identifier : `str` + The unique device identifier. This can be the ProgID for COM devices or the device number for Alpaca devices. + alpaca : `bool`, default : `False`, optional + Whether the device is an Alpaca device. + device_number : `int`, default : 0, optional + The device number for Alpaca devices. + protocol : `str`, default : "http", optional + The communication protocol to use for Alpaca devices. + """ super().__init__( identifier, alpaca=alpaca, diff --git a/pyscope/observatory/filter_wheel.py b/pyscope/observatory/filter_wheel.py index e69506bc..ee6882b3 100644 --- a/pyscope/observatory/filter_wheel.py +++ b/pyscope/observatory/filter_wheel.py @@ -6,21 +6,55 @@ class FilterWheel(ABC, metaclass=_DocstringInheritee): @abstractmethod def __init__(self, *args, **kwargs): + """ + Abstract base class for filter wheel devices. + + This class defines the common interface for filter wheel devices, including properties + for focus off sets, filter names, and the current filter positon. Subclasses must implement + the abstract methods and properties defined here. + + Parameters + ---------- + *args : `tuple` + Variable length argument list. + **kwargs : `dict` + Arbitrary keyword arguments. + """ pass @property @abstractmethod def FocusOffsets(self): + """ + Focus offsets for each filter in the wheel. (`list` of `int`) + + 0-indexed list of offsets, one offset per valid slot number. + Values are focuser and filter dependant, and are typically set up by standardizations (such as ASCOM) or by the user. + If focuser offsets aren't available, all values will be 0. + """ pass @property @abstractmethod def Names(self): + """ + Name of each filter in the wheel. (`list` of `str`) + + 0-indexed list of filter names, one name per valid slot number. + Values are typically set up by standardizations (such as ASCOM) or by the user. + If filter names aren't available, all values will be "FilterX" such that 1 <= X <= N. + """ pass @property @abstractmethod def Position(self): + """ + The current filter wheel position. (`int`) + + Given as the current slot number, of -1 if wheel is moving. + During motion, a position of -1 is mandatory, no valid slot numbers must be returned during rotation past filter positions. + """ pass @Position.setter From 93f3028ae245c7073cff87e58892e49f21115933 Mon Sep 17 00:00:00 2001 From: WWGolay Date: Tue, 5 Nov 2024 11:59:26 -0700 Subject: [PATCH 14/60] Fix repositioning condition checks for string comparison to allow repositioning to ex.: 1024,1024 Co-authored-by: pgriffin17 <31374077+pgriffin17@users.noreply.github.com> --- pyscope/telrun/sch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyscope/telrun/sch.py b/pyscope/telrun/sch.py index d4e79001..fd584047 100644 --- a/pyscope/telrun/sch.py +++ b/pyscope/telrun/sch.py @@ -537,13 +537,13 @@ def read( if "repositioning" in line.keys(): if ( line["repositioning"].startswith("t") - or line["repositioning"].startswith("1") + or line["repositioning"]=="1" or line["repositioning"].startswith("y") ): repositioning = (-1, -1) elif ( line["repositioning"].startswith("f") - or line["repositioning"].startswith("0") + or line["repositioning"]=="0" or line["repositioning"].startswith("n") ): repositioning = (0, 0) From a63fd28d89436309bc993aa569d1b76c190caba7 Mon Sep 17 00:00:00 2001 From: WWGolay Date: Tue, 5 Nov 2024 20:38:43 -0700 Subject: [PATCH 15/60] Improved performance of schedtel plot_schedule_sky function by plotting all obstimes for each target on one axis. Authored-by: Philip Griffin <31374077+pgriffin17@users.noreply.github.com> --- pyscope/telrun/schedtel.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/pyscope/telrun/schedtel.py b/pyscope/telrun/schedtel.py index 690a6cd3..fb2676a5 100644 --- a/pyscope/telrun/schedtel.py +++ b/pyscope/telrun/schedtel.py @@ -1118,20 +1118,32 @@ def plot_schedule_sky_cli(schedule_table, observatory): "Observatory must be, a string, Observatory object, or astroplan.Observer object." ) return + + # Get list of targets (and their ra/dec) and start times and insert into + # a dictionary with the target as the key and a list of start times as the value + targets = {} + for block in schedule_table: + if block["name"] == "TransitionBlock" or block["name"] == "EmptyBlock": + continue + + if block["name"] not in targets: + targets[block["name"]] = [] + obs_time = astrotime.Time(np.float64(block["start_time"].jd), format="jd") + targets[block["name"]].append(obs_time) fig, ax = plt.subplots(1, 1, figsize=(7, 7), subplot_kw={"projection": "polar"}) - for i, row in enumerate(schedule_table): - if row["name"] == "TransitionBlock" or row["name"] == "EmptyBlock": - continue + # Plot new axes for each target only, not each start time + for target in targets: ax = astroplan_plots.plot_sky( - astroplan.FixedTarget(row["target"]), + astroplan.FixedTarget(coord.SkyCoord.from_name(target)), observatory, - astrotime.Time(np.float64(row["start_time"].jd), format="jd"), + targets[target], ax=ax, style_kwargs={ - "label": row["target"].to_string("hmsdms"), + "label": target, }, ) + handles, labels = ax.get_legend_handles_labels() unique = [ (h, l) for i, (h, l) in enumerate(zip(handles, labels)) if l not in labels[:i] From 66d270ec08398e9bb2799ea3bd70947c8786d596 Mon Sep 17 00:00:00 2001 From: ccolin Date: Fri, 15 Nov 2024 17:57:26 -0600 Subject: [PATCH 16/60] abstract focuser and ascom_focuser docs --- pyscope/observatory/ascom_focuser.py | 18 +++++++ pyscope/observatory/focuser.py | 72 ++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/pyscope/observatory/ascom_focuser.py b/pyscope/observatory/ascom_focuser.py index cb5c5006..1d2a12aa 100644 --- a/pyscope/observatory/ascom_focuser.py +++ b/pyscope/observatory/ascom_focuser.py @@ -8,6 +8,24 @@ class ASCOMFocuser(ASCOMDevice, Focuser): def __init__(self, identifier, alpaca=False, device_number=0, protocol="http"): + """ + ASCOM implementation of the Focuser abstract base class. + + The class provides functionality to control an ASCOM-compatible focuser device, + including methods for halting and moving the focuser, as well as properties for + various focuser attributes. + + Parameters + ---------- + identifier : `str` + The device identifier. + alpaca : `bool`, default : `False`, optional + Flag indicating whether the device is an Alpaca device. + device_number : `int`, default : 0, optional + The device number. + protocol : `str`, default : "http", optional + The communication protocol to use. + """ super().__init__( identifier, alpaca=alpaca, diff --git a/pyscope/observatory/focuser.py b/pyscope/observatory/focuser.py index b524de3d..cbf10c10 100644 --- a/pyscope/observatory/focuser.py +++ b/pyscope/observatory/focuser.py @@ -6,19 +6,53 @@ class Focuser(ABC, metaclass=_DocstringInheritee): @abstractmethod def __init__(self, *args, **kwargs): + """ + Abstract base class for focuser devices. + + This class defines the common interface for focusr devices, including methods for halting and moving the focuser, + as well as properties for various attributes. Subcalsses must implement the methods and properties + defined in the class. + + Parameters + ---------- + *args : `tuple` + Variable length argument list. + **kwargs : `dict` + Arbitrary keyword arguments. + """ pass @abstractmethod def Halt(self): + """ + Immediately stop focuser motion due to any previous `Move` call. + + Some focusers may not support this function, and as such must implement an exception raise. + Any host software implementing the method should call the method upon initilization, and signify that + halting is not supported if an exception is raised (such as graying out the Halt button). + """ pass @abstractmethod def Move(self, Position): + """ + Moves the focuser either by the specified amount or to the specified position, based on the `Absolute` property. + + If the `Absolute` property is `True`, then the focuser should move to an exact step position, and the given `Position` parameter should be + an integer between 0 and `MaxStep`. If the `Absolute` property is `False`, then the focuser should move relatively by the given `Position` parameter, + which should be an integer between -`MaxIncrement` and `MaxIncrement`. + + Parameters + ---------- + Position : `int` + The position to move to, or the amount to move relatively. + """ pass @property @abstractmethod def Absolute(self): + """Whether the focuser is capable of absolute position, i.e. move to a specific step position. (`bool`)""" pass @Absolute.setter @@ -29,31 +63,58 @@ def Absolute(self, value): @property @abstractmethod def IsMoving(self): + """Whether the focuser is currently moving. (`bool`)""" pass @property @abstractmethod def MaxIncrement(self): + """ + The maximum increment size allowed by the focuser. (`int`) + + In other words, the maximum number of steps allowed in a single move operation. + For most focuser devices, this value is the same as the `MaxStep` property, and is normally used to limit + the increment size in relation to displaying in the host software. + """ pass @property @abstractmethod def MaxStep(self): + """ + Maximum step position allowed by the focuser. (`int`) + + The focuser supports stepping between 0 and this value. + An attempt to move the focuser beyond this limit should result in the move stopping at the limit. + """ pass @property @abstractmethod def Position(self): + """ + Current focuser position in steps. (`int`) + + Should only be valid if absolute positioning is supported, i.e. the `Absolute` property is `True`. + """ pass @property @abstractmethod def StepSize(self): + """Step size for the focuser in microns. (`float`)""" pass @property @abstractmethod def TempComp(self): + """ + State of the temperature compensation mode. (`bool`) + + Is always `False` if the focuser does not support temperature compensation. + Setting `TempComp` to `True` enables temperature tracking of the focuser, and setting it to `False` should disable that tracking. + An attempt at setting `TempComp` while `TempCompAvailable` is `False` should result in an exception. + """ pass @TempComp.setter @@ -64,11 +125,22 @@ def TempComp(self, value): @property @abstractmethod def TempCompAvailable(self): + """ + Whether the focuser has temperature compensation available. (`bool`) + + Only `True` if the focuser's temperature compsensation can be set via `TempComp`. + """ pass @property @abstractmethod def Temperature(self): + """ + Current ambient temperature measured by the focuser in degrees Celsius. (`float`) + + Raises exception if focuser not equipped to measure ambient temperature. + Most focusers with `TempCompAvailable` set to `True` should have this property available, but not all. + """ pass @Temperature.setter From 33da6346be026f3c47bfcb475c212c4a9ca43858 Mon Sep 17 00:00:00 2001 From: ccolin Date: Fri, 15 Nov 2024 18:07:44 -0600 Subject: [PATCH 17/60] fix for #282 --- pyscope/reduction/ccd_calib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyscope/reduction/ccd_calib.py b/pyscope/reduction/ccd_calib.py index cb3651ba..10425a6f 100644 --- a/pyscope/reduction/ccd_calib.py +++ b/pyscope/reduction/ccd_calib.py @@ -116,6 +116,7 @@ def ccd_calib_cli( logger.info("Loading calibration frames...") + camera_type = camera_type.lower() if camera_type == "ccd": if bias_frame is not None: logger.info(f"Loading bias frame: {bias_frame}") From 6d9af541bc004b179ad55b71eaac38463978b3a2 Mon Sep 17 00:00:00 2001 From: Alexandrea Moreno Date: Tue, 19 Nov 2024 10:48:49 -0600 Subject: [PATCH 18/60] Update astrometry_net_wcs.py Signed-off-by: Alexandrea Moreno --- pyscope/reduction/astrometry_net_wcs.py | 29 +++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/pyscope/reduction/astrometry_net_wcs.py b/pyscope/reduction/astrometry_net_wcs.py index 4837ae53..dcf9c74b 100644 --- a/pyscope/reduction/astrometry_net_wcs.py +++ b/pyscope/reduction/astrometry_net_wcs.py @@ -15,10 +15,35 @@ @click.argument("filepath", type=click.Path(exists=True)) @click.version_option() def astrometry_net_wcs_cli(filepath, **kwargs): - """Platesolve images using the Astrometry.net solver. Requires an input filepath. \f + """ + Platesolve images using the Astrometry.net solver. + + This function uses the `Astrometry.net` solver via the `astroquery.astrometry_net` package + to calculate the World Coordinate System (WCS) solution for an input FITS image. The WCS + solution is written back to the image's header. Additional keyword arguments can be passed + to customize the behavior of the solver. + + Parameters + ---------- + filepath : `str` + Path to the image file to be platesolved. The file must be a valid FITS file. + **kwargs : `dict` + Additional keyword arguments passed to `~astroquery.astrometry_net.AstrometryNet.solve_from_image()`. + + Returns + ------- + bool + `True` if the WCS was successfully updated in the image header, `False` otherwise. - Keyword arguments are passed to `~astroquery.astrometry_net.AstrometryNet.solve_from_image()`. + Raises + ------ + `TimeoutError` + Raised if the Astrometry.net solver submission times out. + `OSError` + Raised if the input file cannot be opened or updated. + `ValueError` + Raised if the input file is not a valid FITS file. """ from astroquery.astrometry_net import AstrometryNet From 3b561fb748ad83980e591a95a0ee04dbe76511bd Mon Sep 17 00:00:00 2001 From: Alexandrea Moreno Date: Tue, 19 Nov 2024 11:11:34 -0600 Subject: [PATCH 19/60] Update schedtel.py Signed-off-by: Alexandrea Moreno --- pyscope/telrun/schedtel.py | 63 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/pyscope/telrun/schedtel.py b/pyscope/telrun/schedtel.py index 656215e8..7232cdb5 100644 --- a/pyscope/telrun/schedtel.py +++ b/pyscope/telrun/schedtel.py @@ -283,6 +283,69 @@ def schedtel_cli( quiet=False, verbose=0, ): + """ + Schedule observations for an observatory. + + This function creates an observation schedule based on input catalogs or + queues. It applies constraints such as airmass, elevation, and moon separation, + and outputs a schedule file. Optionally, it generates visualizations like Gantt + charts or sky charts. + + Parameters + ---------- + catalog : `str`, optional + Path to a `.sch` file or `.cat` file containing observation blocks. If not provided, + defaults to `schedule.cat` in `$TELHOME/schedules/`. + queue : `str`, optional + Path to a queue file (`.ecsv`) with observation requests. If a catalog is provided, + entries from the catalog are added to the queue. + add_only : `bool`, optional + If True, adds catalog entries to the queue without scheduling. + ignore_order : `bool`, optional + Ignores the order of `.sch` files in the catalog, scheduling all blocks as a single group. + date : `str`, optional + Local date of the night to be scheduled (`YYYY-MM-DD`). Defaults to the current date. + length : `int`, optional + Length of the schedule in days. Defaults to `1`. + observatory : `str`, optional + Path to an observatory configuration file. Defaults to `observatory.cfg` in `$TELHOME/config/`. + max_altitude : `float`, optional + Maximum solar altitude for nighttime scheduling. Defaults to `-12` degrees (nautical twilight). + elevation : `float`, optional + Minimum target elevation in degrees. Defaults to `30`. + airmass : `float`, optional + Maximum target airmass. Defaults to `3`. + moon_separation : `float`, optional + Minimum angular separation between the Moon and targets in degrees. Defaults to `30`. + scheduler : `tuple`, optional + Custom scheduler class and module. Defaults to `astroplan.PriorityScheduler`. + gap_time : `float`, optional + Maximum transition time between observation blocks in seconds. Defaults to `60`. + resolution : `float`, optional + Time resolution for scheduling in seconds. Defaults to `5`. + name_format : `str`, optional + Format string for scheduled image names. Defaults to + `"{code}_{target}_{filter}_{exposure}s_{start_time}"`. + filename : `str`, optional + Output file name. If not specified, defaults to a file named with the UTC date + of the first observation in the current working directory. + telrun : `bool`, optional + If True, places the output file in the `$TELRUN_EXECUTE` directory or a default + `schedules/execute/` directory. + plot : `int`, optional + Type of plot to generate (1: Gantt, 2: Airmass, 3: Sky). Defaults to no plot. + yes : `bool`, optional + Automatically answers yes to prompts about unscheduled or invalid blocks. + quiet : `bool`, optional + Suppresses logging output. + verbose : `int`, optional + Controls logging verbosity. Defaults to `0`. + + Returns + ------- + astropy.table.Table + Table containing the scheduled observation blocks. + """ # Set up logging if quiet: level = logging.ERROR From 70cdb561512047708e2a665c174534ecf1082a04 Mon Sep 17 00:00:00 2001 From: Alexandrea Moreno Date: Tue, 19 Nov 2024 11:12:31 -0600 Subject: [PATCH 20/60] Update avg_fits.py Signed-off-by: Alexandrea Moreno --- pyscope/reduction/avg_fits.py | 45 ++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/pyscope/reduction/avg_fits.py b/pyscope/reduction/avg_fits.py index dc81ae4a..90e00f6f 100644 --- a/pyscope/reduction/avg_fits.py +++ b/pyscope/reduction/avg_fits.py @@ -77,33 +77,40 @@ def avg_fits_cli( outfile=None, verbose=0, ): - """Averages over a list of images into a single image. Select pre-normalize to normalize each image by its own mean before combining. This mode is most useful for combining sky flats.\b + """ + Averages a list of FITS images into a single output image. + + Averages multiple FITS images into a single output image, with options for pre-normalization, averaging mode, and output data type. Parameters ---------- - fnames : path - path of directory of images to average. - - pre_normalize : bool, default=False - Normalize each image by its own mean before combining. This mode is most useful for combining sky flats. - - mode : str, default="0" - Mode to use for averaging images (0 = median, 1 = mean). - - datatype : str, default="float32" - Data type to save out the averaged image. If pre_normalize is True, the data type will be float64. - - outfile : path - Path to save averaged image. If not specified, the averaged image will be saved in the same directory as the input images with the first image's name and _avg.fts appended to it. - - verbose : int, default=0 - Print verbose output. + fnames : list of `str` + Paths to FITS files to average. + pre_normalize : `bool`, default=`False` + Normalize each image by its own mean before combining. + mode : `str`, default=`"0"` + Averaging mode: `"0"` for median, `"1"` for mean. + datatype : `str`, default=`"float32"` + Data type for the averaged image. Defaults to `"float32"` unless pre-normalizing, where `"float64"` is used. + outfile : `str`, optional + Output path for the averaged image. Defaults to using the first input file name with `"_avg.fts"` appended. + verbose : `int`, default=`0` + Logging verbosity level. Use `-v` for `INFO` and `-vv` for `DEBUG`. Returns ------- - None + `None` + The averaged FITS image is saved to the specified output file or the default location. + + Raises + ------ + `KeyError` + If required FITS header keywords (e.g., `FRAMETYP`, `EXPTIME`) are missing. + `ValueError` + If the input images have incompatible dimensions or data types. """ + if verbose == 2: logging.basicConfig(level=logging.DEBUG) elif verbose == 1: From 4be323e89a1a99ad268656ffae0c72f97ba5bbbb Mon Sep 17 00:00:00 2001 From: Alexandrea Moreno Date: Tue, 19 Nov 2024 11:13:16 -0600 Subject: [PATCH 21/60] Update calib_images.py Signed-off-by: Alexandrea Moreno --- pyscope/reduction/calib_images.py | 81 +++++++++++++++++++++++-------- 1 file changed, 61 insertions(+), 20 deletions(-) diff --git a/pyscope/reduction/calib_images.py b/pyscope/reduction/calib_images.py index ae5d4d2f..3337af45 100644 --- a/pyscope/reduction/calib_images.py +++ b/pyscope/reduction/calib_images.py @@ -109,29 +109,65 @@ def calib_images_cli( verbose=0, fnames=(), ): - """Calibrate a set of images by recursively selecting the - appropriate flat, dark, and bias frame and then calling - ccd_calib to do the actual calibration. + """ + Calibrate a set of images using `ccd_calib`, optionally overwriting the originals. - Notes: Having an example set up of this function would be helpful. - This would allow for me to know what the environment looks like when - calling this function. + This function processes raw astronomical images in bulk by automatically selecting + matching calibration frames (`bias`, `dark`, and `flat`) and delegating the calibration + task to the `ccd_calib` function. By default, calibrated images are saved alongside the + raw images with a `_cal` suffix. If the `--in-place` option is used, the raw images + are overwritten, but this requires a backup directory (`--raw-archive-dir`) to avoid + data loss. + + + .. note:: + + Ensure all required calibration frames (`bias`, `dark`, `flat`, `flat-dark`) are present in the specified + `--calib-dir`. If any calibration frames are missing, the process will fail, and the function will + return `0` without making changes to the images. + + Parameters + ---------- + camera_type : `str` + Camera type (`ccd` or `cmos`). + image_dir : `str`, optional + Directory containing images to be calibrated. If passed, then + the `fnames` argument is ignored. + calib_dir : `str`, optional + Location of all calibration files. + raw_archive_dir : `str`, optional + Directory to archive raw images. If none given, no archiving is done, + however, the `--in-place` option is not allowed. + in_place : `bool`, optional + If given, the raw images are overwritten with the calibrated images. + If not given, the calibrated images are written to the same directory as the + raw images, but with the suffix `_cal` added to the filename prior to the extension. + astro_scrappy : `tuple` of (`int`, `int`), optional + Number of hot pixel removal iterations and estimated camera read noise. + bad_columns : `str`, optional + Comma-separated list of bad columns to fix. + zmag : `bool`, optional + If given, the zero-point magnitude is calculated for each image with + SDSS filters. If not given, the zero-point magnitude is not calculated. + verbose : `int`, optional + Print verbose output. `1` = verbose, `2` = more verbose. + fnames : `list` of `str` + List of image filenames to be calibrated. + + Returns + ------- + `int` + Returns `0` if the calibration process fails due to missing calibration frames. + `None` + Returns `None` on successful completion of all calibrations. + + Raises + ------ + `click.BadParameter` + If the `--in-place` option is used without specifying a `--raw-archive-dir`. - Args: - camera_type (_type_): _description_ - image_dir (_type_): _description_ - calib_dir (_type_): _description_ - raw_archive_dir (_type_): _description_ - in_place (_type_): _description_ - astro_scrappy (_type_): _description_ - bad_columns (_type_): _description_ - zmag (_type_): _description_ - verbose (_type_): _description_ - fnames (_type_): _description_ - - Raises: - click.BadParameter: _description_ """ + if verbose == 2: logging.basicConfig(level=logging.DEBUG) elif verbose == 1: @@ -310,5 +346,10 @@ def calib_images_cli( logger.info("Done!") +''' +Notes: Having an example set up of this function would be helpful. + This would allow for me to know what the environment looks like when + calling this function. +''' calib_images = calib_images_cli.callback From 64af6875b0850056b1448ccc67632d4f9a0c0dc0 Mon Sep 17 00:00:00 2001 From: Alexandrea Moreno Date: Tue, 19 Nov 2024 11:13:48 -0600 Subject: [PATCH 22/60] Update ccd_calib.py Signed-off-by: Alexandrea Moreno --- pyscope/reduction/ccd_calib.py | 66 ++++++++++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 7 deletions(-) diff --git a/pyscope/reduction/ccd_calib.py b/pyscope/reduction/ccd_calib.py index cb3651ba..347e9a88 100644 --- a/pyscope/reduction/ccd_calib.py +++ b/pyscope/reduction/ccd_calib.py @@ -96,13 +96,65 @@ def ccd_calib_cli( pedestal=1000, ): """ - Calibrate CCD or CMOS images using master dark, bias, and flat frames. The calibrated images are saved - with the suffix '_cal' appended to the original filename. This script may be used to pre-calibrate flats - before combining them into a master flat frame. \b +Calibrate astronomical images using master calibration frames. + +The `ccd_calib_cli` function applies bias, dark, and flat corrections to raw CCD or CMOS images +to produce calibrated versions. Calibrated images are saved with the suffix `_cal` appended +to the original filename unless the `--in-place` option is used, which overwrites the raw files. +The calibration process supports additional features like hot pixel removal and bad column correction. + +Parameters +---------- +fnames : `list` of `str` + List of filenames to calibrate. +dark_frame : `str`, optional + Path to master dark frame. If the camera type is `cmos`, the exposure time of the + dark frame must match the exposure time of the target images. +bias_frame : `str`, optional + Path to master bias frame. Ignored if the camera type is `cmos`. +flat_frame : `str`, optional + Path to master flat frame. The script assumes the master flat frame has already been + bias and dark corrected. The flat frame is normalized by the mean of the entire image + before being applied to the target images. +camera_type : `str`, optional + Camera type. Must be either `ccd` or `cmos`. Defaults to `"ccd"`. +astro_scrappy : `tuple` of (`int`, `int`), optional + Number of hot pixel removal iterations and estimated camera read noise (in + root-electrons per pixel per second). Defaults to `(1, 3)`. +bad_columns : `str`, optional + Comma-separated list of bad columns to fix by averaging the value of each pixel + in the adjacent column. Defaults to `""`. +in_place : `bool`, optional + If `True`, overwrites the input files with the calibrated images. Defaults to `False`. +pedestal : `int`, optional + Pedestal value to add to calibrated images after processing to prevent negative + pixel values. Defaults to `1000`. +verbose : `int`, optional + Verbosity level for logging output: + - `0`: Warnings only (default). + - `1`: Informational messages. + - `2`: Debug-level messages. + +Returns +------- +`None` + The function does not return any value. It writes calibrated images to disk. + +Raises +------ +`FileNotFoundError` + Raised if any of the specified input files do not exist. +`ValueError` + Raised if the calibration frames (e.g., `dark_frame`, `bias_frame`, `flat_frame`) do not + match the target images in terms of critical metadata, such as: + - Exposure time + - Binning (X and Y) + - Readout mode +`KeyError` + Raised if required header keywords (e.g., `IMAGETYP`, `FILTER`, `EXPTIME`, `GAIN`) are + missing from the calibration frames or the raw image files. +""" - - - """ if verbose == 2: logging.basicConfig(level=logging.DEBUG) elif verbose == 1: @@ -506,7 +558,7 @@ def ccd_calib_cli( else: logger.info(f"Writing calibrated image to {fname}") fits.writeto( - fname.split(".")[:-1][0] + "_cal.fts", cal_image, hdr, overwrite=True + str(fname).split(".")[:-1][0] + "_cal.fts", cal_image, hdr, overwrite=True ) logger.info("Done!") From 414b84efa1eb3ca5a3319a3da85d4995ec4b5734 Mon Sep 17 00:00:00 2001 From: Alexandrea Moreno Date: Tue, 19 Nov 2024 11:14:27 -0600 Subject: [PATCH 23/60] Update avg_fits_ccdproc.py Signed-off-by: Alexandrea Moreno --- pyscope/reduction/avg_fits_ccdproc.py | 129 +++++++++++++------------- 1 file changed, 64 insertions(+), 65 deletions(-) diff --git a/pyscope/reduction/avg_fits_ccdproc.py b/pyscope/reduction/avg_fits_ccdproc.py index 3f499667..7bacc78a 100644 --- a/pyscope/reduction/avg_fits_ccdproc.py +++ b/pyscope/reduction/avg_fits_ccdproc.py @@ -226,75 +226,74 @@ def avg_fits_ccdproc_cli( unit="adu", verbose=False, ): - """Combines images using ccdproc.combine method. + """ + Combine multiple images into a single averaged image using `ccdproc`. + + This function uses the `ccdproc.combine` method to combine a set of FITS images + into a single output image, supporting various combination methods like "median", + "average", and "sum". The output file is saved in the specified format, with + optional data scaling, weighting, and clipping algorithms. Parameters ---------- - outfile : str - path to save combined image. - - fnames : list - list of image paths to combine. - - method="median" : str, optional - method to use for averaging images. Options are "median", "average", "sum" - - datatype : np.datatype, optional - intermediate and resulting dtype for combined CCDs, by default np.uint16 - - weights : np.ndarray, optional - Weights to be used when combining images. An array with the weight values. The dimensions should match the the dimensions of the data arrays being combined., by default None - - scale : callable or np.ndarray, optional - Scaling factor to be used when combining images. Images are multiplied by scaling prior to combining them. Scaling may be either a function, which will be applied to each image to determine the scaling factor, or a list or array whose length is the number of images in the Combiner, by default None - - mem_limit : float, optional - Maximum memory which should be used while combining (in bytes), by default 16000000000.0 - - clip_extrema : bool, optional - Set to True if you want to mask pixels using an IRAF-like minmax clipping algorithm. The algorithm will mask the lowest nlow values and the highest nhigh values before combining the values to make up a single pixel in the resulting image. For example, the image will be a combination of Nimages-low-nhigh pixel values instead of the combination of Nimages. Parameters below are valid only when clip_extrema is set to True, by default False - - nlow : int, optional - Number of low values to reject from the combination, by default 1 - - nhigh : int, optional - Number of high values to reject from the combination, by default 1 - - minmax_clip : bool, optional - Set to True if you want to mask all pixels that are below minmax_clip_min or above minmax_clip_max before combining, by default False - - minmax_clip_min : float, optional - All pixels with values below min_clip will be masked, by default None - - minmax_clip_max : flaot, optional - All pixels with values above min_clip will be masked, by default None - - sigma_clip : bool, optional - Set to True if you want to reject pixels which have deviations greater than those set by the threshold values. The algorithm will first calculated a baseline value using the function specified in func and deviation based on sigma_clip_dev_func and the input data array. Any pixel with a deviation from the baseline value greater than that set by sigma_clip_high_thresh or lower than that set by sigma_clip_low_thresh will be rejected, by default False - - sigma_clip_low_thresh : int, optional - Threshold for rejecting pixels that deviate below the baseline value. If negative value, then will be convert to a positive value. If None, no rejection will be done based on low_thresh, by default 3 - - sigma_clip_high_thresh : int, optional - Threshold for rejecting pixels that deviate above the baseline value. If None, no rejection will be done based on high_thresh, by default 3 - - sigma_clip_func : callable, optional - The statistic or callable function/object used to compute the center value for the clipping. If using a callable function/object and the axis keyword is used, then it must be able to ignore NaNs (e.g., numpy.nanmean) and it must have an axis keyword to return an array with axis dimension(s) removed. The default is 'median', by default None - - sigma_clip_dev_func : callable, optional - The statistic or callable function/object used to compute the standard deviation about the center value. If using a callable function/object and the axis keyword is used, then it must be able to ignore NaNs (e.g., numpy.nanstd) and it must have an axis keyword to return an array with axis dimension(s) removed. The default is 'std', by default None - - combine_uncertainty_function : callable, optional - If None use the default uncertainty func when using average, median or sum combine, otherwise use the function provided, by default None - - overwrite_output : bool, optional - If output_file is specified, this is passed to the astropy.nddata.fits_ccddata_writer under the keyword overwrite; has no effect otherwise., by default False - - unit : str, optional - unit for CCDData objects, by default 'adu' + outfile : `str` + Path to save the combined image. + fnames : `list` of `str` + List of FITS file paths to be combined. + method : `str`, optional + Method used for combining images. Options are: + - `"average"` + - `"median"` (default) + - `"sum"` + datatype : `str`, optional + Data type for the intermediate and resulting combined image. + Options include: + - `"int16"` + - `"float32"` (default: `"uint16"`). + weights : `numpy.ndarray`, optional + Array of weights to use when combining images. Dimensions must match the data arrays. + scale : `callable` or `numpy.ndarray`, optional + Scaling factors for combining images. Can be a function applied to each image + or an array matching the number of images. + mem_limit : `float`, optional + Maximum memory (in bytes) allowed during combination (default: `16e9`). + clip_extrema : `bool`, optional + If `True`, masks the lowest `nlow` and highest `nhigh` values for each pixel + before combining. + nlow : `int`, optional + Number of low pixel values to reject when `clip_extrema=True` (default: 1). + nhigh : `int`, optional + Number of high pixel values to reject when `clip_extrema=True` (default: 1). + minmax_clip : `bool`, optional + If `True`, masks all pixels below `minmax_clip_min` or above `minmax_clip_max`. + minmax_clip_min : `float`, optional + Minimum pixel value for masking when `minmax_clip=True`. + minmax_clip_max : `float`, optional + Maximum pixel value for masking when `minmax_clip=True`. + sigma_clip : `bool`, optional + If `True`, performs sigma clipping based on deviation thresholds. + sigma_clip_low_thresh : `float`, optional + Threshold for rejecting pixels below the baseline value (default: 3). + sigma_clip_high_thresh : `float`, optional + Threshold for rejecting pixels above the baseline value (default: 3). + sigma_clip_func : `callable`, optional + Function used to calculate the baseline value for sigma clipping (default: `median`). + sigma_clip_dev_func : `callable`, optional + Function used to calculate deviations for sigma clipping (default: `std`). + combine_uncertainty_function : `callable`, optional + Custom function to compute uncertainties during combination. + overwrite_output : `bool`, optional + If `True`, overwrites the output file if it exists (default: `False`). + unit : `str`, optional + Unit of the CCD data (e.g., `"adu"`, `"counts"`, `"photon"`) (default: `"adu"`). + verbose : `bool`, optional + If `True`, enables verbose logging for debugging. + + Returns + ------- + `None` + The function writes the combined image to disk and does not return any value. - verbose : bool, optional - verbosity of logger, by default False """ if verbose: From 3ad1f0b4177489c05f7450ba49a181bb52966f10 Mon Sep 17 00:00:00 2001 From: Alexandrea Moreno Date: Tue, 19 Nov 2024 11:15:14 -0600 Subject: [PATCH 24/60] Update fitslist.py Signed-off-by: Alexandrea Moreno --- pyscope/reduction/fitslist.py | 48 ++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/pyscope/reduction/fitslist.py b/pyscope/reduction/fitslist.py index 569f6394..72e6d8bb 100644 --- a/pyscope/reduction/fitslist.py +++ b/pyscope/reduction/fitslist.py @@ -164,7 +164,53 @@ def fitslist_cli( offsets, zp_stats, ): - """List FITS files and their properties.""" + """ + List FITS files and their properties. + + This command-line tool extracts metadata from FITS files in a specified directory + and presents it in a structured table. Users can filter files by various criteria + and save the results to a CSV file. + + Parameters + ---------- + `date` : `str`, optional + Filter by observation date. Defaults to include all dates. + `filt` : `str`, optional + Filter by filter name. Defaults to include all filters. + `readout` : `str`, optional + Filter by readout mode. Defaults to include all modes. + `binning` : `str`, optional + Filter by binning configuration. Defaults to include all configurations. + `exptime` : `str`, optional + Filter by approximate exposure time (1% tolerance). Defaults to include all exposure times. + `target` : `str`, optional + Filter by target name. Defaults to include all targets. + `verbose` : `int`, optional + Verbosity level for logging. Use `-v` for more detailed output. Defaults to `0`. + `fnames` : `str`, optional + Path to the directory containing FITS files. Defaults to `./`. + `save` : `bool`, optional + Save the output table to a file named `fitslist.csv`. Defaults to `False`. + `add_keys` : `str`, optional + Comma-separated list of additional FITS header keys to include in the output table. + `offsets` : `bool`, optional + Include RA/DEC offsets in the output table. Defaults to `False`. + `zp_stats` : `bool`, optional + Include zeropoint statistics in the output table. Defaults to `False`. + + Returns + ------- + `astropy.table.Table` + A table containing the extracted FITS metadata. + + Raises + ------ + `KeyError` + If a required FITS header key is missing. + `FileNotFoundError` + If the specified directory does not exist or is empty. + + """ # Set up logging logger.setLevel(int(10 * (1 - verbose))) logger.debug( From bb65d4c340ce3d26bc021e4831746c8ca0611ef1 Mon Sep 17 00:00:00 2001 From: Alexandrea Moreno Date: Tue, 19 Nov 2024 11:15:35 -0600 Subject: [PATCH 25/60] Update maxim_pinpoint_wcs.py Signed-off-by: Alexandrea Moreno --- pyscope/reduction/maxim_pinpoint_wcs.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/pyscope/reduction/maxim_pinpoint_wcs.py b/pyscope/reduction/maxim_pinpoint_wcs.py index cbfcadd1..0560365d 100644 --- a/pyscope/reduction/maxim_pinpoint_wcs.py +++ b/pyscope/reduction/maxim_pinpoint_wcs.py @@ -21,18 +21,32 @@ ) @click.version_option() def maxim_pinpoint_wcs_cli(filepath): - """Platesolve images using the PinPoint solver in MaxIm DL. + """ + Platesolve images using the PinPoint solver in MaxIm DL. + + This script interacts with MaxIm DL's PinPoint solver via its COM interface + to solve astronomical images for their World Coordinate System (`WCS`). The + solved image is saved back to the original file if the process is successful. - .. Note:: - This script requires MaxIm DL to be installed on your system, as it uses the PinPoint solver in MaxIm DL. \b + .. note:: + This script requires MaxIm DL to be installed on a Windows system. The PinPoint + solver is accessed through MaxIm DL's COM interface. Parameters ---------- - filepath : str + `filepath` : `str` + The path to the FITS file to solve. The file must exist. Returns ------- - None + `bool` + `True` if the plate-solving process is successful, `False` otherwise. + + Raises + ------ + `Exception` + If the system is not Windows or if an unexpected error occurs during solving. + """ if type(filepath) is not str: From ef54e8c87e67e507abb7dfd1dd0cb8752da2ab1e Mon Sep 17 00:00:00 2001 From: Alexandrea Moreno Date: Tue, 19 Nov 2024 11:16:31 -0600 Subject: [PATCH 26/60] Update pinpoint_wcs.py Signed-off-by: Alexandrea Moreno --- pyscope/reduction/pinpoint_wcs.py | 41 +++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/pyscope/reduction/pinpoint_wcs.py b/pyscope/reduction/pinpoint_wcs.py index 5c407d41..00c25ab2 100644 --- a/pyscope/reduction/pinpoint_wcs.py +++ b/pyscope/reduction/pinpoint_wcs.py @@ -39,6 +39,47 @@ def Solve( catalog=3, catalog_path="C:\\GSC11", ): + """ + Solve the WCS of a FITS image using PinPoint. + + This method uses PinPoint to attach to a FITS file, solve for its astrometry based + on provided or inferred RA/DEC and scale estimates, and update the FITS file with the + resulting WCS solution. + + Parameters + ---------- + filepath : `str` + Path to the FITS file to solve. + scale_est : `float` + Estimate of the plate scale in arcseconds per pixel. + ra_key : `str`, optional + Header key for right ascension. Defaults to `"RA"`. + dec_key : `str`, optional + Header key for declination. Defaults to `"DEC"`. + ra : `float`, optional + Right ascension in the specified units. If not provided, uses the FITS header value. + dec : `float`, optional + Declination in the specified units. If not provided, uses the FITS header value. + ra_dec_units : `tuple` of `str`, optional + Units for RA and DEC. Defaults to `("hour", "deg")`. + solve_timeout : `int`, optional + Timeout in seconds for the plate-solving process. Defaults to `60`. + catalog : `int`, optional + Catalog index to use in PinPoint. Defaults to `3`. + catalog_path : `str`, optional + Path to the catalog files. Defaults to `"C:\\GSC11"`. + + Returns + ------- + `bool` + `True` if the plate-solving process completes successfully. + + Raises + ------ + `Exception` + If the system is not Windows or the plate-solving process times out. + + """ logger.debug( f"""PinpointWCS.Solve( {filepath}, {scale_est}, {ra_key}, {dec_key}, {ra}, {dec}, From 696f3d2cbf8be3a4274b7b287753e8f40397fcc7 Mon Sep 17 00:00:00 2001 From: Alexandrea Moreno Date: Tue, 19 Nov 2024 11:17:01 -0600 Subject: [PATCH 27/60] Update process_images.py Signed-off-by: Alexandrea Moreno --- pyscope/reduction/process_images.py | 68 +++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 14 deletions(-) diff --git a/pyscope/reduction/process_images.py b/pyscope/reduction/process_images.py index fb767ed6..12f7e18e 100644 --- a/pyscope/reduction/process_images.py +++ b/pyscope/reduction/process_images.py @@ -67,15 +67,25 @@ def runcmd(cmd, **kwargs): - """run a subprocess""" + """Run a subprocess""" return subprocess.run( cmd, shell=True, capture_output=True, encoding="ascii", **kwargs ) def isCalibrated(img): - """returns True if calibration was started, - False otherwise + """ + Checks if a FITS image has calibration started. + + Parameters + ---------- + img : `pathlib.Path` + Path to the FITS image file. + + Returns + ------- + bool + `True` if the FITS header contains the `CALSTART` keyword, `False` otherwise. """ try: fits.getval(img, "CALSTART") @@ -86,8 +96,8 @@ def isCalibrated(img): def isSuccessfullyCalibrated(img): - """returns True if calibration was completed successfully, - False otherwise + """Returns `True` if calibration was completed successfully, + `False` otherwise """ try: fits.getval(img, "CALSTAT") @@ -98,7 +108,7 @@ def isSuccessfullyCalibrated(img): def sort_image(img, dest): - """copy image to a directory, create it if needed""" + """Copy image to a directory, create it if needed""" if not dest.exists(): dest.mkdir(mode=0o775, parents=True) target = dest / img.name @@ -106,11 +116,25 @@ def sort_image(img, dest): def store_image(img, dest, update_db=False): - """store copy of image in long-term archive directory :dest - check that the target is older or doesn't exist - log errors - future: use s3cmd library to interact with object storage more efficiently - future: update_db=True adds image info to database + """ + Archives a FITS image in a long-term storage directory. + + Copies the file to the specified directory if the target does not exist or + is older than the source. Logs errors during the copy process. + + Parameters + ---------- + img : `pathlib.Path` + Path to the FITS image file. + dest : `pathlib.Path` + Destination directory for storing the image. + update_db : `bool`, optional + If `True`, updates the database with image metadata. (Future implementation) + + Returns + ------- + bool + `True` if the file is successfully copied, `False` otherwise. """ if not dest.exists(): dest.mkdir(mode=0o775, parents=True) @@ -127,9 +151,25 @@ def store_image(img, dest, update_db=False): def process_image(img): - """process a single image - calibrate if needed - move to reduced or failed depending on status + """ + Processes a single FITS image by calibrating and classifying it based on the outcome. + + The function begins by reading the FITS file data and header. If the image has not + already been calibrated, it applies calibration using the `calib_images` function. + Once calibrated, the image is moved to a `reduced` directory if the calibration is + successful or a `failed` directory if calibration fails. Regardless of the outcome, + the image is archived in a long-term storage directory for later use. Finally, the + image is removed from the landing directory to complete the process. + + Parameters + ---------- + img : `pathlib.Path` + Path to the FITS image file to process. + + Raises + ------ + Exception + If the image is corrupt or calibration fails. """ logger.info(f"Processing {img}...") try: From e1820f78c82121c1d23baedc5b065a549b6be749 Mon Sep 17 00:00:00 2001 From: Alexandrea Moreno Date: Tue, 19 Nov 2024 11:17:22 -0600 Subject: [PATCH 28/60] Update reduce_calibration_set.py Signed-off-by: Alexandrea Moreno --- pyscope/reduction/reduce_calibration_set.py | 28 ++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/pyscope/reduction/reduce_calibration_set.py b/pyscope/reduction/reduce_calibration_set.py index f2e0305f..26501aed 100644 --- a/pyscope/reduction/reduce_calibration_set.py +++ b/pyscope/reduction/reduce_calibration_set.py @@ -69,7 +69,33 @@ def reduce_calibration_set_cli( verbose=0, ): """ - Reduce a calibration set to master frames.\b + Reduce a calibration set to master frames. + + This function processes a calibration set of FITS images, typically used in astronomy, + to create master bias, dark, and flat calibration frames. The input calibration set + should be a directory containing subdirectories for bias, dark, and flat frames. + The resulting master calibration frames are saved in the same directory. + + Parameters + ---------- + calibration_set : `str` + Path to the calibration set directory. + camera : `str`, optional + Camera type, either `"ccd"` or `"cmos"`. Defaults to `"ccd"`. + mode : `str`, optional + Method to use for averaging images, either `"median"` or `"average"`. + Defaults to `"median"`. + pre_normalize : `bool`, optional + Pre-normalize flat images before combining. This option is useful + for sky flats. Defaults to `True`. + verbose : `int`, optional + Level of verbosity for output logs. Use higher values for more detailed output. + Defaults to `0`. + + Raises + ------ + `SystemExit` + Raised if the program encounters a critical issue and exits prematurely. """ calibration_set = Path(calibration_set).resolve() From e078802dc6589231ef34474309f1487bb8b7e089 Mon Sep 17 00:00:00 2001 From: ccolin Date: Tue, 19 Nov 2024 17:22:16 -0600 Subject: [PATCH 29/60] start of docs --- .../observatory/ascom_observing_conditions.py | 18 ++++++++++ pyscope/observatory/observing_conditions.py | 34 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/pyscope/observatory/ascom_observing_conditions.py b/pyscope/observatory/ascom_observing_conditions.py index e84e38ad..e9e40ed7 100644 --- a/pyscope/observatory/ascom_observing_conditions.py +++ b/pyscope/observatory/ascom_observing_conditions.py @@ -8,6 +8,24 @@ class ASCOMObservingConditions(ASCOMDevice, ObservingConditions): def __init__(self, identifier, alpaca=False, device_number=0, protocol="http"): + """ + ASCOM implementation of the base class. + + The class provides an interface to access a set of values useful for astronomical purposes such as + determining if it is safe to operate the observing system, recording data, or determining + refraction corrections. + + Parameters + ---------- + identifier : `str` + The device identifier. + alpaca : `bool`, default : `False`, optional + Whether the device is an Alpaca device. + device_number : `int`, default : 0, optional + The device number. + protocol : `str`, default : "http", optional + The protocol to use for communication with the device. + """ super().__init__( identifier, alpaca=alpaca, diff --git a/pyscope/observatory/observing_conditions.py b/pyscope/observatory/observing_conditions.py index afe0ddbf..739d07a8 100644 --- a/pyscope/observatory/observing_conditions.py +++ b/pyscope/observatory/observing_conditions.py @@ -6,14 +6,48 @@ class ObservingConditions(ABC, metaclass=_DocstringInheritee): @abstractmethod def __init__(self, *args, **kwargs): + """ + Abstract base class for observing conditions. + + This class defines the interface for observing conditions, including methods + for refreshing data, getting sensor descriptions, and checking time since last + update for a given property, and properties for various environmental conditions. + Subclasses must implement the abstract methods and properties in this class. + + Parameters + ---------- + *args : `tuple` + Variable length argument list. + **kwargs : `dict` + Arbitrary keyword arguments. + """ pass @abstractmethod def Refresh(self): + """Forces an immediate query to the hardware to refresh sensor values.""" pass @abstractmethod def SensorDescription(self, PropertyName): + """ + Provides the description of the sensor of the specified property. (`str`) + + The property whose name is supplied must be one of the properties specified + in the `ObservingConditions` interface, else the method should throw an exception. + + Even if the driver to the sensor isn't connected, if the sensor itself is implemented, this method must + return a valid string, for example in case an application wants to determine what sensors are available. + Parameters + ---------- + PropertyName : `str` + The name of the property for which the sensor description is required. + + Returns + ------- + `str` + The sensor description. + """ pass @abstractmethod From 3c9ef51c872b2a6c86121c6ffa54c28a6fab1ec4 Mon Sep 17 00:00:00 2001 From: ccolin Date: Tue, 19 Nov 2024 17:54:14 -0600 Subject: [PATCH 30/60] finished docs --- pyscope/observatory/observing_conditions.py | 94 +++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/pyscope/observatory/observing_conditions.py b/pyscope/observatory/observing_conditions.py index 739d07a8..5c45350c 100644 --- a/pyscope/observatory/observing_conditions.py +++ b/pyscope/observatory/observing_conditions.py @@ -38,6 +38,7 @@ def SensorDescription(self, PropertyName): Even if the driver to the sensor isn't connected, if the sensor itself is implemented, this method must return a valid string, for example in case an application wants to determine what sensors are available. + Parameters ---------- PropertyName : `str` @@ -52,11 +53,30 @@ def SensorDescription(self, PropertyName): @abstractmethod def TimeSinceLastUpdate(self, PropertyName): + """ + Time since the sensor value was last updated. (`float`) + + The property whose name is supplied must be one of the properties specified in the `ObservingConditions` interface, + else the method should throw an exception. There should be a return of a negative value if no valid value has + ever been received from the hardware. + The driver should return the time since the most recent value update of any sensor if an empty string is supplied. + + Parameters + ---------- + PropertyName : `str` + The name of the property for which time since last update is required. + """ pass @property @abstractmethod def AveragePeriod(self): + """ + The time period over which observations will be averaged. (`float`) + + This is the time period in hours over which the driver will average sensor readings. + If the driver delivers instantaneous values, then this value should be 0.0. + """ pass @AveragePeriod.setter @@ -67,64 +87,138 @@ def AveragePeriod(self, value): @property @abstractmethod def CloudCover(self): + """ + Amount of sky obscured by cloud. (`float`) + + Returns a value between 0 and 100 representing the percentage of the sky covered by cloud. + """ pass @property @abstractmethod def DewPoint(self): + """ + Atmospheric dew point at the observatory. (`float`) + + Dew point given in degrees Celsius. + """ pass @property @abstractmethod def Humidity(self): + """ + Atmospheric humidity at the observatory. (`float`) + + Humidity given as a percentage, ranging from no humidity at 0 to full humidity at 100. + """ pass @property @abstractmethod def Pressure(self): + """ + Atmospheric pressure at the observatory. (`float`) + + Pressure in hectopascals (hPa), should be at the observatory altitude and not sea level altitude. + If your pressure sensor measures sea level pressure, adjust to the observatory altitude before returning the value. + """ pass @property @abstractmethod def RainRate(self): + """ + Rate of rain at the observatory. (`float`) + + Rain rate in millimeters per hour. + Rainfall intensity is classified according to the rate of precipitation: + - Light rain: 0.1 - 2.5 mm/hr + - Moderate rain: 2.5 - 7.6 mm/hr + - Heavy rain: 7.6 - 50 mm/hr + - Violent rain: > 50 mm/hr + """ pass @property @abstractmethod def SkyBrightness(self): + """ + Sky brightness at the observatory. (`float`) + + Sky brightness in Lux. + For a scale of sky brightness, see `ASCOM `_. + """ pass @property @abstractmethod def SkyQuality(self): + """ + Sky quality at the observatory. (`float`) + + Sky quality in magnitudes per square arcsecond. + """ pass @property @abstractmethod def SkyTemperature(self): + """ + Sky temperature at the observatory. (`float`) + + Sky temperature in degrees Celsius, expected to be read by an infra-red sensor pointing at the sky. + Lower temperatures are indicative of clearer skies. + """ pass @property @abstractmethod def StarFWHM(self): + """ + Seeing at the observatory. (`float`) + + Measured as the average star full width at half maximum (FWHM) in arcseconds within a star field. + """ pass @property @abstractmethod def Temperature(self): + """ + Temperature at the observatory. (`float`) + + Ambient temperature at the observatory in degrees Celsius. + """ pass @property @abstractmethod def WindDirection(self): + """ + Wind direction at the observatory. (`float`) + + Wind direction in degrees from North, increasing clockwise, where a value of 0.0 should be interpreted + as wind speed being 0.0. + """ pass @property @abstractmethod def WindGust(self): + """ + Peak 3 second wind gust at the observatory over the last 2 minutes. (`float`) + + Wind gust in meters per second. + """ pass @property @abstractmethod def WindSpeed(self): + """ + Wind speed at the observatory. (`float`) + + Wind speed in meters per second. + """ pass From 8c1c5699f213d0b1e38ba7f1bb1dd2a58cbd8500 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Dec 2024 12:50:23 +0000 Subject: [PATCH 31/60] Bump pytest-cov from 5.0.0 to 6.0.0 Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 5.0.0 to 6.0.0. - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v5.0.0...v6.0.0) --- updated-dependencies: - dependency-name: pytest-cov dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- setup.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 4f1b5483..d70bbbc3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -69,7 +69,7 @@ docs = tests = pytest==8.1.1 - pytest-cov==5.0.0 + pytest-cov==6.0.0 pytest-doctestplus==1.2.1 dev = @@ -80,7 +80,7 @@ dev = isort==5.13.2 pre-commit==3.7.0 pytest==8.1.1 - pytest-cov==5.0.0 + pytest-cov==6.0.0 pytest-order==1.3.0 rstcheck==6.2.1 sphinx==7.2.6 From 3fca2979dddf1b5ec8c453dc68dbe0fcc45e1779 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Dec 2024 12:50:26 +0000 Subject: [PATCH 32/60] Bump paramiko from 3.4.0 to 3.5.0 Bumps [paramiko](https://github.com/paramiko/paramiko) from 3.4.0 to 3.5.0. - [Commits](https://github.com/paramiko/paramiko/compare/3.4.0...3.5.0) --- updated-dependencies: - dependency-name: paramiko dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 44f72285..b1dee6d7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ markdown == 3.6 matplotlib == 3.9.1 numpy == 2.1.0 oschmod == 0.3.12 -paramiko == 3.4.0 +paramiko == 3.5.0 photutils == 1.13.0 prettytable == 3.10.0 pywin32 == 306;platform_system=='Windows' From 7374d46b70b07ee15913a59997023ce2e4e4bf1b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Dec 2024 12:50:33 +0000 Subject: [PATCH 33/60] Bump pytest-doctestplus from 1.2.1 to 1.3.0 Bumps [pytest-doctestplus](https://github.com/scientific-python/pytest-doctestplus) from 1.2.1 to 1.3.0. - [Release notes](https://github.com/scientific-python/pytest-doctestplus/releases) - [Changelog](https://github.com/scientific-python/pytest-doctestplus/blob/main/CHANGES.rst) - [Commits](https://github.com/scientific-python/pytest-doctestplus/compare/v1.2.1...v1.3.0) --- updated-dependencies: - dependency-name: pytest-doctestplus dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 4f1b5483..9e60b9c1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -70,7 +70,7 @@ docs = tests = pytest==8.1.1 pytest-cov==5.0.0 - pytest-doctestplus==1.2.1 + pytest-doctestplus==1.3.0 dev = black==24.3.0 From cf9fa44bf8a7aa2c9e1b89ec92b3195dcc9f67aa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Dec 2024 12:50:35 +0000 Subject: [PATCH 34/60] Bump pywin32 from 306 to 308 Bumps [pywin32](https://github.com/mhammond/pywin32) from 306 to 308. - [Release notes](https://github.com/mhammond/pywin32/releases) - [Changelog](https://github.com/mhammond/pywin32/blob/main/CHANGES.txt) - [Commits](https://github.com/mhammond/pywin32/commits) --- updated-dependencies: - dependency-name: pywin32 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 44f72285..929f27ba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ oschmod == 0.3.12 paramiko == 3.4.0 photutils == 1.13.0 prettytable == 3.10.0 -pywin32 == 306;platform_system=='Windows' +pywin32 == 308;platform_system=='Windows' scikit-image == 0.24.0 scipy == 1.14.1 smplotlib == 0.0.9 From d7f36590c0b176f27f1344df0e0d186260b3f36e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Dec 2024 12:58:02 +0000 Subject: [PATCH 35/60] Bump isort/isort-action from 1.1.0 to 1.1.1 Bumps [isort/isort-action](https://github.com/isort/isort-action) from 1.1.0 to 1.1.1. - [Release notes](https://github.com/isort/isort-action/releases) - [Changelog](https://github.com/isort/isort-action/blob/master/CHANGELOG.md) - [Commits](https://github.com/isort/isort-action/compare/v1.1.0...v1.1.1) --- updated-dependencies: - dependency-name: isort/isort-action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ebed69f7..36ec6b65 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,6 @@ jobs: with: src: pyscope jupyter: true - - uses: isort/isort-action@v1.1.0 + - uses: isort/isort-action@v1.1.1 with: configuration: profile=black From 4a49003f190ed862b330d1e42e6b5b997078cabd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Dec 2024 12:58:04 +0000 Subject: [PATCH 36/60] Bump codecov/codecov-action from 4 to 5 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v4...v5) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/codecov.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 168b9200..5e6337ec 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -9,6 +9,6 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} From 01bf21c0f203a0ac659835ab5082b2799936aa90 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Dec 2024 12:58:06 +0000 Subject: [PATCH 37/60] Bump pypa/gh-action-pypi-publish from 1.10.0 to 1.12.2 Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.10.0 to 1.12.2. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.10.0...v1.12.2) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/pypi-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml index 77cf1689..bf60f669 100644 --- a/.github/workflows/pypi-publish.yml +++ b/.github/workflows/pypi-publish.yml @@ -34,4 +34,4 @@ jobs: - name: Build package run: python -m build - name: pypi-publish - uses: pypa/gh-action-pypi-publish@v1.10.0 + uses: pypa/gh-action-pypi-publish@v1.12.2 From 8a2b52cb5fa2d97f15017146d3ed6b1fd66d256d Mon Sep 17 00:00:00 2001 From: ccolin Date: Tue, 3 Dec 2024 04:24:50 -0600 Subject: [PATCH 38/60] rotator docs --- pyscope/observatory/ascom_rotator.py | 17 +++++++ pyscope/observatory/rotator.py | 76 ++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/pyscope/observatory/ascom_rotator.py b/pyscope/observatory/ascom_rotator.py index 45d18eb0..8948a234 100644 --- a/pyscope/observatory/ascom_rotator.py +++ b/pyscope/observatory/ascom_rotator.py @@ -8,6 +8,23 @@ class ASCOMRotator(ASCOMDevice, Rotator): def __init__(self, identifier, alpaca=False, device_number=0, protocol="http"): + """ + ASCOM implementation of the Rotator base class. + + The class provides an interface to control an ASCOM-compatible rotator device, including methods for + moving, halting, and properties for determining the current position and movement state of the rotator. + + Parameters + ---------- + identifier : `str` + The device identifier. + alpaca : `bool`, default : `False`, optional + Whether the device is an Alpaca device. + device_number : `int`, default : 0, optional + The device number. + protocol : `str`, default : "http", optional + The protocol to use for communication with the device. + """ super().__init__( identifier, alpaca=alpaca, diff --git a/pyscope/observatory/rotator.py b/pyscope/observatory/rotator.py index 79655dec..24e3aea5 100644 --- a/pyscope/observatory/rotator.py +++ b/pyscope/observatory/rotator.py @@ -6,51 +6,120 @@ class Rotator(ABC, metaclass=_DocstringInheritee): @abstractmethod def __init__(self, *args, **kwargs): + """ + Abstract base class for a rotator device. + + This class defines the interface for a rotator device, including methods for + moving the rotator, halting movement, and properties for determining the current + position and movement state of the rotator. Subclasses must implement the abstract + methods and properties in this class. + + Parameters + ---------- + *args : `tuple` + Variable length argument list. + **kwargs : `dict` + Arbitrary keyword arguments. + """ pass @abstractmethod def Halt(self): + """Immediately stop any movement of the rotator due to a `Move` or `MoveAbsolute` call.""" pass @abstractmethod def Move(self, Position): + """ + Move the rotator `Position` degrees from the current position. + + Calling will change `TargetPosition` to sum of the current angular position and `Position` module 360 degrees. + + Parameters + ---------- + Position : `float` + Relative position to move from current `Position`, in degrees + """ pass @abstractmethod def MoveAbsolute(self, Position): + """ + Move the rotator to the absolute `Position` degrees. + + Calling will change `TargetPosition` to `Position`. + + Parameters + ---------- + Position : `float` + Absolute position to move to, in degrees + """ pass @abstractmethod def MoveMechanical(self, Position): + """ + Move the rotator to the mechanical `Position` angle. + + This move is independent of any sync offsets, and is to be used for circumstances + where physical rotaion angle is needed (i.e. taking sky flats). + For instances such as imaging, `MoveAbsolute` should be preferred. + + Parameters + ---------- + Position : `float` + Mechanical position to move to, in degrees + """ pass @abstractmethod def Sync(self, Position): + """ + Synchronize the rotator to the `Position` angle without movement. + + Once set, along with determining sync offsets, `MoveAbsolute` and `Position` must function + in terms of synced coordinates rather than mechanical. + Implementations should make sure offsets persist across driver starts and device reboots. + + Parameters + ---------- + Position : `float` + Sync position to set, in degrees + """ pass @property @abstractmethod def CanReverse(self): + """Whether the rotator supports the `Reverse` method. (`bool`)""" pass @property @abstractmethod def IsMoving(self): + """Whether the rotator is currently moving. (`bool`)""" pass @property @abstractmethod def MechanicalPosition(self): + """The current mechanical position of the rotator, in degrees. (`float`)""" pass @property @abstractmethod def Position(self): + """The current angular position of the rotator accounting offsets, in degrees. (`float`)""" pass @property @abstractmethod def Reverse(self): + """ + Whether the rotator is in reverse mode. (`bool`) + + If `True`, the rotation and angular direction must be reversed for optics. + """ pass @Reverse.setter @@ -61,9 +130,16 @@ def Reverse(self, value): @property @abstractmethod def StepSize(self): + """The minimum step size of the rotator, in degrees. (`float`)""" pass @property @abstractmethod def TargetPosition(self): + """ + The target angular position of the rotator, in degrees. (`float`) + + Upon a `Move` or `MoveAbsolute` call, this property is set to the position angle to which + the rotator is moving. Value persists until another move call. + """ pass From 0c5fe2722b3335e7397e0b6c9afed95713038a05 Mon Sep 17 00:00:00 2001 From: ccolin Date: Tue, 3 Dec 2024 04:59:41 -0600 Subject: [PATCH 39/60] switch docs --- pyscope/observatory/ascom_switch.py | 17 ++++ pyscope/observatory/switch.py | 117 ++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) diff --git a/pyscope/observatory/ascom_switch.py b/pyscope/observatory/ascom_switch.py index f4fac71b..dfcb9ee0 100644 --- a/pyscope/observatory/ascom_switch.py +++ b/pyscope/observatory/ascom_switch.py @@ -8,6 +8,23 @@ class ASCOMSwitch(ASCOMDevice, Switch): def __init__(self, identifier, alpaca=False, device_number=0, protocol="http"): + """ + ASCOM implementation of the Switch base class. + + The class provides an interface to ASCOM-compatible switch devices, including methods for + getting and setting switch values, and getting switch names and descriptions. + + Parameters + ---------- + identifier : `str` + The device identifier. + alpaca : `bool`, default : `False`, optional + Whether the device is an Alpaca device. + device_number : `int`, default : 0, optional + The device number. + protocol : `str`, default : "http", optional + The protocol to use for communication with the device. + """ super().__init__( identifier, alpaca=alpaca, diff --git a/pyscope/observatory/switch.py b/pyscope/observatory/switch.py index eb5c0b53..980982a8 100644 --- a/pyscope/observatory/switch.py +++ b/pyscope/observatory/switch.py @@ -6,49 +6,166 @@ class Switch(ABC, metaclass=_DocstringInheritee): @abstractmethod def __init__(self, *args, **kwargs): + """ + Abstract class for switch devices. + + This class defines the common interface for switch devices, including methods for + getting and setting switch values, and getting switch names and descriptions. + Subclasses must implement the abstract methods and properties in this class. + + Parameters + ---------- + *args : `tuple` + Variable length argument list. + **kwargs : `dict` + Arbitrary keyword arguments. + """ pass @abstractmethod def CanWrite(self, ID): + """ + Whether the switch can be written to. (`bool`) + + Most devices will return `True` for this property, but some switches are + meant to be read-only, such as limit switches or sensors. + + Parameters + ---------- + ID : `int` + The switch ID number. + """ pass @abstractmethod def GetSwitch(self, ID): + """ + The current state of the switch as a boolean. (`bool`) + + This is a mandatory property for all switch devices. For multi-state switches, + this should be `True` for when the device is at the maximum value, `False` if at minimum, + and either `True` or `False` for intermediate values; specification up to driver author. + + Parameters + ---------- + ID : `int` + The switch ID number. + """ pass @abstractmethod def GetSwitchDescription(self, ID): + """ + The detailed description of the switch, to be used for features such as tooltips. (`str`) + + Parameters + ---------- + ID : `int` + The switch ID number. + """ pass @abstractmethod def GetSwitchName(self, ID): + """ + The name of the switch, to be used for display purposes. (`str`) + + Parameters + ---------- + ID : `int` + The switch ID number. + """ pass @abstractmethod def MaxSwitchValue(self, ID): + """ + The maximum value of the switch. (`float`) + + Must be greater than `MinSwitchValue`. Two-state devices should return 1.0, and only + multi-state devices should return values greater than 1.0. + + Parameters + ---------- + ID : `int` + The switch ID number. + """ pass @abstractmethod def MinSwitchValue(self, ID): + """ + The minimum value of the switch. (`float`) + + Must be less than `MaxSwitchValue`. Two-state devices should return 0.0. + """ pass @abstractmethod def SetSwitch(self, ID, State): + """ + Sets the state of the switch controller device. + + `GetSwitchValue` will also be set, to `MaxSwitchValue` if `State` is `True`, and `MinSwitchValue` if `State` is `False`. + + Parameters + ---------- + ID : `int` + The switch ID number. + State : `bool` + The state to set the switch to. + """ pass @abstractmethod def SetSwitchName(self, ID, Name): + """ + Sets the name of the switch device. + + Parameters + ---------- + ID : `int` + The switch ID number. + Name : `str` + The new name to set the switch to. + """ pass @abstractmethod def SetSwitchValue(self, ID, Value): + """ + Sets the value of the switch device. + + Values should be between `MinSwitchValue` and `MaxSwitchValue`. If the value is + intermediate in relation to `SwitchStep`, it will be set to an achievable value. + How this achieved value is determined is up to the driver author. + + Parameters + ---------- + ID : `int` + The switch ID number. + Value : `float` + The value to set the switch to. + """ pass @abstractmethod def SwitchStep(self, ID): + """ + The step size that the switch device supports. (`float`) + + Has to be greater than zero. This is the smallest increment that the switch device's value can be set to. + For two-state devices, this should be 1.0, and anything else other than 0 for multi-state devices. + + Parameters + ---------- + ID : `int` + The switch ID number. + """ pass @property @abstractmethod def MaxSwitch(self): + """The number of switch devices managed by this driver. (`int`)""" pass From dcc98224ecf5f1b7446707fe32fe6b97a50b2d43 Mon Sep 17 00:00:00 2001 From: ccolin Date: Tue, 3 Dec 2024 08:08:59 -0600 Subject: [PATCH 40/60] azimuth reference clarification --- pyscope/observatory/dome.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyscope/observatory/dome.py b/pyscope/observatory/dome.py index 17ecbb1f..c3188d40 100644 --- a/pyscope/observatory/dome.py +++ b/pyscope/observatory/dome.py @@ -92,7 +92,7 @@ def SlewToAzimuth(self, Azimuth): Parameters ---------- Azimuth : `float` - The azimuth to slew to in degrees. + The azimuth to slew to in degrees, North-referenced, CW. """ pass @@ -104,7 +104,7 @@ def SyncToAzimuth(self, Azimuth): Parameters ---------- Azimuth : `float` - The azimuth to synchronize to in degrees. + The azimuth to synchronize to in degrees, North-referenced, CW. """ pass From e6ff16e0bd0726b38c3db708c24e0d531676416a Mon Sep 17 00:00:00 2001 From: ccolin Date: Tue, 3 Dec 2024 08:09:16 -0600 Subject: [PATCH 41/60] telescope docs --- pyscope/observatory/ascom_telescope.py | 185 ++++++++++ pyscope/observatory/telescope.py | 453 +++++++++++++++++++++++++ 2 files changed, 638 insertions(+) diff --git a/pyscope/observatory/ascom_telescope.py b/pyscope/observatory/ascom_telescope.py index 8de648e2..68dcbdbc 100644 --- a/pyscope/observatory/ascom_telescope.py +++ b/pyscope/observatory/ascom_telescope.py @@ -8,6 +8,24 @@ class ASCOMTelescope(ASCOMDevice, Telescope): def __init__(self, identifier, alpaca=False, device_number=0, protocol="http"): + """ + ASCOM implementation of the Telescope base class. + + This class provides an interface for an ASCOM-compatible telescope. Methods include + slewing the telescope, halting movement, and properties for determining the current + position and movement state of the telescope. + + Parameters + ---------- + identifier : `str` + The unique device identifier. This can be the ProgID for COM devices or the device number for Alpaca devices. + alpaca : `bool`, default : `False`, optional + Whether to use the Alpaca protocol, for Alpaca devices. If `False`, the COM protocol is used. + device_number : `int`, default : 0, optional + The device number. + protocol : `str`, default : "http", optional + The communication protocol to use. + """ super().__init__( identifier, alpaca=alpaca, @@ -21,14 +39,74 @@ def AbortSlew(self): self._device.AbortSlew() def AxisRates(self, Axis): + """ + Set of rates at which the telescope may be moved about the specified axis. + + See `MoveAxis`'s description for more information. Should return an empty set + if `MoveAxis` is not supported. + + Parameters + ---------- + Axis : `TelescopeAxes `_ + The axis about which the telescope may be moved. See below for ASCOM standard. + * 0 : Primary axis, Right Ascension or Azimuth. + * 1 : Secondary axis, Declination or Altitude. + * 2 : Tertiary axis, imager rotators. + + Returns + ------- + `IAxisRates `_ + While it may look intimidating, this return is but an object representing a + collection of rates, including properties for both the number of rates, and the + actual rates, and methods for returning an enumerator for the rates, and for + disposing of the object as a whole. + + Notes + ----- + Rates must be absolute non-negative values only. Determining direction + of motion should be done by the application by applying the appropriate + sign to the rate. This is to not have the driver present a duplicate + set of positive and negative rates, therefore decluttering. + """ logger.debug(f"ASCOMTelescope.AxisRates({Axis}) called") return self._device.AxisRates(Axis) def CanMoveAxis(self, Axis): + """ + Whether the telescope can move about the specified axis with `MoveAxis`. (`bool`) + + See `MoveAxis`'s description for more information. + + Parameters + ---------- + Axis : `TelescopeAxes `_ + The axis about which the telescope may be moved. See below for ASCOM standard. + * 0 : Primary axis, Right Ascension or Azimuth. + * 1 : Secondary axis, Declination or Altitude. + * 2 : Tertiary axis, imager rotators. + """ logger.debug(f"ASCOMTelescope.CanMoveAxis({Axis}) called") return self._device.CanMoveAxis(Axis) def DestinationSideOfPier(self, RightAscension, Declination): + """ + For German equatorial mounts, prediction of which side of the pier the telescope will be on after slewing to the specified equatorial coordinates at the current instant of time. + + Parameters + ---------- + RightAscension : `float` + Right ascension coordinate (hours) of destination, not current, at current instant of time. + Declination : `float` + Declination coordinate (degrees) of destination, not current, at current instant of time. + + Returns + ------- + `PierSide `_ + The side of the pier on which the telescope will be after slewing to the specified coordinates. See below for ASCOM standard. + * 0 : Normal pointing state, mount on East side of pier, looking West. + * 1 : Through the pole pointing state, mount on West side of pier, looking East. + * -1 : Unknown or indeterminate. + """ logger.debug( f"ASCOMTelescope.DestinationSideOfPier({RightAscension}, {Declination}) called" ) @@ -39,6 +117,34 @@ def FindHome(self): self._device.FindHome() def MoveAxis(self, Axis, Rate): + """ + Move the telescope at the given rate about the specified axis. + + The rate is a value between 0.0 and the maximum value returned by `AxisRates`. + This movement will continue indefinitely until `MoveAxis` is called again with a rate of 0.0, + at which point the telescope will restore rate to the one set by `Tracking`. Tracking motion + is disabled during this movement about axis. The method can be called for each axis independently, + with each axis moving at its own rate. + + Parameters + ---------- + Axis : `TelescopeAxes `_ + The axis about which the telescope may be moved. See below for ASCOM standard. + * 0 : Primary axis, Right Ascension or Azimuth. + * 1 : Secondary axis, Declination or Altitude. + * 2 : Tertiary axis, imager rotators. + Rate : `float` + Rate of motion in degrees per second. Positive values indicate motion in one direction, + negative values in the opposite direction, and 0.0 stops motion by this method and resumes tracking motion. + + Notes + ----- + Rate must be within the values returned by `AxisRates`. Note that those values are absolute, + and the direction of motion is determined by the sign of the rate given here. This sets `Slewing` + much like `SlewToAltAz` or `SlewToCoordinates` would, and is therefore affected by `AbortSlew`. + Depending on `Tracking` state, a setting rate of 0.0 will reset the scope to the previous `TrackingRate`, + or to no movement at all. + """ logger.debug(f"ASCOMTelescope.MoveAxis({Axis}, {Rate}) called") self._device.MoveAxis(Axis, Rate) @@ -59,6 +165,25 @@ def Park(self): logger.error(f"Error in executing or accessing SetPark: {e}") def PulseGuide(self, Direction, Duration): + """ + Move the telescope in the specified direction for the specified time. + + Rate at which the telescope moves is set by `GuideRateRightAscension` and `GuideRateDeclination`. + Depending on driver implementation and the mount's capabilities, these two rates may be tied together. + For hardware capable of dual-axis movement, the method returns immediately, otherwise it returns only + after completion of the guide pulse. + + Parameters + ---------- + Direction : `GuideDirections `_ + Direction in which to move the telescope. See below for ASCOM standard. + * 0 : North or up. + * 1 : South or down. + * 2 : East or right. + * 3 : West or left. + Duration : `int` + Time in milliseconds for which to pulse the guide. Must be a positive non-zero value. + """ logger.debug(f"ASCOMTelescope.PulseGuide({Direction}, {Duration}) called") self._device.PulseGuide(Direction, Duration) @@ -126,6 +251,14 @@ def Unpark(self): @property def AlignmentMode(self): + """ + The alignment mode of the telescope. (`AlignmentModes `_) + + See below for ASCOM standard. + * 0 : Altitude-Azimuth alignment. + * 1 : Polar (equatorial) mount alignment, NOT German. + * 2 : German equatorial mount alignment. + """ logger.debug("ASCOMTelescope.AlignmentMode property accessed") return self._device.AlignmentMode @@ -273,6 +406,16 @@ def DoesRefraction(self, value): @property def EquatorialSystem(self): + """ + Equatorial coordinate system used by the telescope. (`EquatorialCoordinateType `_) + + See below for ASCOM standard. + * 0 : Custom/unknown equinox and/or reference frame. + * 1 : Topocentric coordinates. Coordinates at the current date, allowing for annual aberration and precession-nutation. Most common for amateur telescopes. + * 2 : J2000 equator and equinox. Coordinates at the J2000 epoch, ICRS reference frame. Most common for professional telescopes. + * 3 : J2050 equator and equinox. Coordinates at the J2050 epoch, ICRS reference frame. + * 4 : B1950 equinox. Coordinates at the B1950 epoch, FK4 reference frame. + """ logger.debug("ASCOMTelescope.EquatorialSystem property accessed") return self._device.EquatorialSystem @@ -323,6 +466,19 @@ def RightAscensionRate(self, value): @property def SideOfPier(self): + """ + The pointing state of the mount. (`PierSide `_) + + See below for ASCOM standard. + * 0 : Normal pointing state, mount on East side of pier, looking West. + * 1 : Through the pole pointing state, mount on West side of pier, looking East. + * -1 : Unknown or indeterminate. + + .. warning:: + The name of this property is misleading and does not reflect the true meaning of the property. + The name will not change out of preservation of compatibility. + For more information, see the 'Remarks' section of `this page `_. + """ logger.debug("ASCOMTelescope.SideOfPier property accessed") return self._device.SideOfPier @@ -413,6 +569,21 @@ def Tracking(self, value): @property def TrackingRate(self): + """ + Current tracking rate of the telescope's sidereal drive. (`DriveRates `_) + + Supported rates are contained in `TrackingRates`, and the property's rate + is guaranteed to be one of those. + If the mount's current tracking rate cannot be read, the driver should force + and report a default rate on connection. + In this circumstance, a default of `Sidereal` rate is preferred. + + See below for ASCOM standard. + * 0 : Sidereal tracking, 15.041 arcseconds per second. + * 1 : Lunar tracking, 14.685 arcseconds per second. + * 2 : Solar tracking, 15.0 arcseconds per second. + * 3 : King tracking, 15.0369 arcseconds per second. + """ logger.debug("ASCOMTelescope.TrackingRate property accessed") return self._device.TrackingRate @@ -423,6 +594,20 @@ def TrackingRate(self, value): @property def TrackingRates(self): + """ + Collection of supported tracking rates for the telescope's sidereal drive. (`TrackingRates `_) + + This collection contains all supported tracking rates. At a minimum, it will contain + an item for the default `Sidereal` rate. The collection is an iterable, and includes + properties for the number of rates, and the actual rates, and methods for returning + an enumerator for the rates, and for disposing of the object as a whole. + + See below for ASCOM standard expected collection. + * 0 : Sidereal tracking, 15.041 arcseconds per second. + * 1 : Lunar tracking, 14.685 arcseconds per second. + * 2 : Solar tracking, 15.0 arcseconds per second. + * 3 : King tracking, 15.0369 arcseconds per second + """ logger.debug("ASCOMTelescope.TrackingRates property accessed") return self._device.TrackingRates diff --git a/pyscope/observatory/telescope.py b/pyscope/observatory/telescope.py index 7825e406..3b0147ce 100644 --- a/pyscope/observatory/telescope.py +++ b/pyscope/observatory/telescope.py @@ -6,87 +6,336 @@ class Telescope(ABC, metaclass=_DocstringInheritee): @abstractmethod def __init__(self, *args, **kwargs): + """ + Abstract base class for a telescope device. + + This class defines the interface for a telescope device, including methods for + slewing the telescope, halting movement, and properties for determining the current + position and movement state of the telescope. Subclasses must implement the abstract + methods and properties in this class. + + Parameters + ---------- + *args : `tuple` + Variable length argument list. + **kwargs : `dict` + Arbitrary keyword arguments. + """ pass @abstractmethod def AbortSlew(self): + """ + Immediately stop any movement of the telescope due to any of the SlewTo*** calls. + + Does nothing if the telescope is not slewing. Tracking will return to its pre-slew state. + """ pass @abstractmethod def AxisRates(self, Axis): + """ + Set of rates at which the telescope may be moved about the specified axis. + + See `MoveAxis`'s description for more information. Should return an empty set + if `MoveAxis` is not supported. + + Parameters + ---------- + Axis : `enum` + The axis about which the telescope may be moved. See below for example values. + * 0 : Primary axis, usually corresponding to Right Ascension or Azimuth. + * 1 : Secondary axis, usually corresponding to Declination or Altitude. + * 2 : Tertiary axis, usually corresponding to imager rotators. + + Returns + ------- + `object` + This object should be an iterable collection, including properties for both + the number of rates, and the actual rates, and methods for returning an + enumerator for the rates, and for disposing of the object as a whole. + + Notes + ----- + Rates must be absolute non-negative values only. Determining direction + of motion should be done by the application by applying the appropriate + sign to the rate. This is to not have the driver present a duplicate + set of positive and negative rates, therefore decluttering. + """ pass @abstractmethod def CanMoveAxis(self, Axis): + """ + Whether the telescope can move about the specified axis with `MoveAxis`. (`bool`) + + See `MoveAxis`'s description for more information. + + Parameters + ---------- + Axis : `enum` + The axis about which the telescope may be moved. See below for example values. + * 0 : Primary axis, usually corresponding to Right Ascension or Azimuth. + * 1 : Secondary axis, usually corresponding to Declination or Altitude. + * 2 : Tertiary axis, usually corresponding to imager rotators. + """ pass @abstractmethod def DestinationSideOfPier(self, RightAscension, Declination): + """ + For German equatorial mounts, prediction of which side of the pier the telescope will be on after slewing to the specified equatorial coordinates at the current instant of time. + + Parameters + ---------- + RightAscension : `float` + Right ascension coordinate (hours) of destination, not current, at current instant of time. + Declination : `float` + Declination coordinate (degrees) of destination, not current, at current instant of time. + + Returns + ------- + `enum` + The side of the pier on which the telescope will be after slewing to the specified equatorial coordinates at the current instant of time. See below for example values. + * 0 : Normal pointing state, mount on East side of pier, looking West. + * 1 : Through the pole pointing state, mount on West side of pier, looking East. + * -1 : Unknown or indeterminate. + """ pass @abstractmethod def FindHome(self): + """ + Synchronously locate the telescope's home position. + + Returns only once the home position has been located. After locating, + `AtHome` will set to `True`. + """ pass @abstractmethod def MoveAxis(self, Axis, Rate): + """ + Move the telescope at the given rate about the specified axis. + + The rate is a value between 0.0 and the maximum value returned by `AxisRates`. + This movement will continue indefinitely until `MoveAxis` is called again with a rate of 0.0, + at which point the telescope will restore rate to the one set by `Tracking`. Tracking motion + is disabled during this movement about axis. The method can be called for each axis independently, + with each axis moving at its own rate. + + Parameters + ---------- + Axis : `enum` + The axis about which the telescope may be moved. See below for example values. + * 0 : Primary axis, usually corresponding to Right Ascension or Azimuth. + * 1 : Secondary axis, usually corresponding to Declination or Altitude. + * 2 : Tertiary axis, usually corresponding to imager rotators. + Rate : `float` + Rate of motion in degrees per second. Positive values indicate motion in one direction, + negative values in the opposite direction, and 0.0 stops motion by this method and resumes tracking motion. + + Notes + ----- + Rate must be within the values returned by `AxisRates`. Note that those values are absolute, + and the direction of motion is determined by the sign of the rate given here. This sets `Slewing` + much like `SlewToAltAz` or `SlewToCoordinates` would, and is therefore affected by `AbortSlew`. + Depending on `Tracking` state, a setting rate of 0.0 will reset the scope to the previous `TrackingRate`, + or to no movement at all. + """ pass @abstractmethod def Park(self): + """ + Moves the telescope to the park position. + + To achieve this, it stops all motion (alternatively just restrict it to a small safe range), + park, then set `AtPark` to `True`. Calling it with `AtPark` already `True` is safe to do. + """ pass @abstractmethod def PulseGuide(self, Direction, Duration): + """ + Move the telescope in the specified direction for the specified time. + + Rate at which the telescope moves is set by `GuideRateRightAscension` and `GuideRateDeclination`. + Depending on driver implementation and the mount's capabilities, these two rates may be tied together. + For hardware capable of dual-axis movement, the method returns immediately, otherwise it returns only + after completion of the guide pulse. + + Parameters + ---------- + Direction : `enum` + Direction in which to move the telescope. See below for example values. + * 0 : North or up. + * 1 : South or down. + * 2 : East or right. + * 3 : West or left. + Duration : `int` + Time in milliseconds for which to pulse the guide. Must be a positive non-zero value. + """ pass @abstractmethod def SetPark(self): + """Sets the telescope's park position to the current position.""" pass @abstractmethod def SlewToAltAz(self, Azimuth, Altitude): + """ + Move the telescope to the specified Alt/Az coordinates. + + Slew may fail if target is beyond limits imposed within the driver component. These limits + could be mechanical constraints by the mount or instruments, safety restrictions from the + building or dome enclosure, etc. + `TargetRightAscension` and `TargetDeclination` will be unchanged by this method. + + Parameters + ---------- + Azimuth : `float` + Azimuth coordinate in degrees, North-referenced, CW. + Altitude : `float` + Altitude coordinate in degrees. + """ pass @abstractmethod def SlewToAltAzAsync(self, Azimuth, Altitude): + """ + Move the telescope to the specified Alt/Az coordinates asynchronously. + + Method should only be implemented if the telescope is capable of reading `Altitude`, `Azimuth`, `RightAscension`, and `Declination` + while slewing. Returns immediately after starting the slew. + Slew may fail if target is beyond limits imposed within the driver component. These limits + could be mechanical constraints by the mount or instruments, safety restrictions from the + building or dome enclosure, etc. + `TargetRightAscension` and `TargetDeclination` will be unchanged by this method. + + Parameters + ---------- + Azimuth : `float` + Azimuth coordinate in degrees, North-referenced, CW. + Altitude : `float` + Altitude coordinate in degrees. + """ pass @abstractmethod def SlewToCoordinates(self, RightAscension, Declination): + """ + Move the telescope to the specified equatorial coordinates. + + Slew may fail if target is beyond limits imposed within the driver component. These limits + could be mechanical constraints by the mount or instruments, safety restrictions from the + building or dome enclosure, etc. + `TargetRightAscension` and `TargetDeclination` will set to the specified coordinates, irrelevant of + whether the slew was successful. + + Parameters + ---------- + RightAscension : `float` + Right ascension coordinate in hours. + Declination : `float` + Declination coordinate in degrees. + """ pass @abstractmethod def SlewToCoordinatesAsync(self, RightAscension, Declination): + """ + Move the telescope to the specified equatorial coordinates asynchronously. + + Returns immediately after starting the slew. Slew may fail if target is beyond limits imposed within the driver component. + These limits could be mechanical constraints by the mount or instruments, safety restrictions from the building or dome enclosure, etc. + `TargetRightAscension` and `TargetDeclination` will set to the specified coordinates, irrelevant of whether the slew was successful. + + Parameters + ---------- + RightAscension : `float` + Right ascension coordinate in hours. + Declination : `float` + Declination coordinate in degrees. + """ pass @abstractmethod def SlewToTarget(self): + """ + Move the telescope to the `TargetRightAscension` and `TargetDeclination` coordinates. + + Slew may fail if target is beyond limits imposed within the driver component. These limits + could be mechanical constraints by the mount or instruments, safety restrictions from the + building or dome enclosure, etc. + """ pass @abstractmethod def SlewToTargetAsync(self): + """ + Move the telescope to the `TargetRightAscension` and `TargetDeclination` coordinates asynchronously. + + Returns immediately after starting the slew. Slew may fail if target is beyond limits imposed within the driver component. + These limits could be mechanical constraints by the mount or instruments, safety restrictions from the building or dome enclosure, etc. + """ pass @abstractmethod def SyncToAltAz(self, Azimuth, Altitude): + """ + Synchronizes the telescope's current local horizontal coordinates to the specified Alt/Az coordinates. + + Parameters + ---------- + Azimuth : `float` + Azimuth coordinate in degrees, North-referenced, CW. + Altitude : `float` + Altitude coordinate in degrees. + """ pass @abstractmethod def SyncToCoordinates(self, RightAscension, Declination): + """ + Synchronizes the telescope's current equatorial coordinates to the specified RA/Dec coordinates. + + Parameters + ---------- + RightAscension : `float` + Right ascension coordinate in hours. + Declination : `float` + Declination coordinate in degrees. + """ pass @abstractmethod def SyncToTarget(self): + """ + Synchronizes the telescope's current equatorial coordinates to the `TargetRightAscension` and `TargetDeclination` coordinates. + + This method should only be used to improve the pointing accuracy of the telescope for positions close + to the position at which the sync is done, since Sync is mount-dependent. + """ pass @abstractmethod def Unpark(self): + """Takes the telescope out of the `Parked` state.""" pass @property @abstractmethod def AlignmentMode(self): + """ + The alignment mode of the telescope. (`enum`) + + See below for example values. + * 0 : Altitude-Azimuth alignment. + * 1 : Polar (equatorial) mount alignment, NOT German. + * 2 : German equatorial mount alignment. + """ pass @AlignmentMode.setter @@ -97,116 +346,174 @@ def AlignmentMode(self, value): @property @abstractmethod def Altitude(self): + """Altitude above the horizon of the telescope's current position in degrees. (`float`)""" pass @property @abstractmethod def ApertureArea(self): + """Area of the telescope's aperture in square meters, accounting for obstructions. (`float`)""" pass @property @abstractmethod def ApertureDiameter(self): + """Effective diameter of the telescope's aperture in meters. (`float`)""" pass @property @abstractmethod def AtHome(self): + """ + Whether the telescope is at the home position. (`bool`) + + Only `True` after a successful `FindHome` call, and `False` after any slewing operation. + Alternatively, `False` if the telescope is not capable of homing. + """ pass @property @abstractmethod def AtPark(self): + """ + Whether the telescope is in the `Parked` state. (`bool`) + + See `Park` and `Unpark` for more information. + Setting to `True` or `False` should only be done by those methods respectively. + While `True`, no movement commands should be accepted except `Unpark`, and an attempt + to do so should raise an error. + """ pass @property @abstractmethod def Azimuth(self): + """Azimuth of the telescope's current position in degrees, North-referenced, CW. (`float`)""" pass @property @abstractmethod def CanFindHome(self): + """Whether the telescope is capable of finding its home position with `FindHome`. (`bool`)""" pass @property @abstractmethod def CanPark(self): + """Whether the telescope is capable of parking with `Park`. (`bool`)""" pass @property @abstractmethod def CanPulseGuide(self): + """Whether the telescope is capable of pulse guiding with `PulseGuide`. (`bool`)""" pass @property @abstractmethod def CanSetDeclinationRate(self): + """Whether the telescope is capable of setting the declination rate with `DeclinationRate` for offset tracking. (`bool`)""" pass @property @abstractmethod def CanSetGuideRates(self): + """Whether the telescope is capable of setting the guide rates with `GuideRateRightAscension` and `GuideRateDeclination`. (`bool`)""" pass @property @abstractmethod def CanSetPark(self): + """Whether the telescope is capable of setting the park position with `SetPark`. (`bool`)""" pass @property @abstractmethod def CanSetPierSide(self): + """ + Whether the telescope is capable of setting the side of the pier with `SideOfPier`, i.e. the mount can be forced to flip. (`bool`) + + This is only relevant for German equatorial mounts, as non-Germans do not have to be flipped + and should therefore have `CansetPierSide` `False`. + """ pass @property @abstractmethod def CanSetRightAscensionRate(self): + """Whether the telescope is capable of setting the right ascension rate with `RightAscensionRate` for offset tracking. (`bool`)""" pass @property @abstractmethod def CanSetTracking(self): + """Whether the telescope is capable of setting the tracking state with `Tracking`. (`bool`)""" pass @property @abstractmethod def CanSlew(self): + """ + Whether the telescope is capable of slewing with `SlewToCoordinates`, or `SlewToTarget`. (`bool`) + + A `True` only guarantees that the synchronous slews are possible. + Asynchronous slew capabilies are determined by `CanSlewAsync`. + """ pass @property @abstractmethod def CanSlewAltAz(self): + """ + Whether the telescope is capable of slewing to Alt/Az coordinates with `SlewToAltAz`. (`bool`) + + A `True` only guarantees that the synchronous local horizontal slews are possible. + Asynchronous slew capabilies are determined by `CanSlewAltAzAsync`. + """ pass @property @abstractmethod def CanSlewAltAzAsync(self): + """ + Whether the telescope is capable of slewing to Alt/Az coordinates asynchronously with `SlewToAltAzAsync`. (`bool`) + + A `True` also guarantees that the synchronous local horizontal slews are possible. + """ pass @property @abstractmethod def CanSlewAsync(self): + """ + Whether the telescope is capable of slewing asynchronously with `SlewToCoordinatesAsync`, `SlewToAltAzAsync`, or `SlewToTargetAsync`. (`bool`) + + A `True` also guarantees that the synchronous slews are possible. + """ pass @property @abstractmethod def CanSync(self): + """Whether the telescope is capable of syncing with `SyncToCoordinates` or `SyncToTarget`. (`bool`)""" pass @property @abstractmethod def CanSyncAltAz(self): + """Whether the telescope is capable of syncing to Alt/Az coordinates with `SyncToAltAz`. (`bool`)""" pass @property @abstractmethod def CanUnpark(self): + """Whether the telescope is capable of unparking with `Unpark`. (`bool`)""" pass @property @abstractmethod def Declination(self): + """Declination of the telescope's current position, in the coordinate system given by `EquatorialSystem`, in degrees. (`float`)""" pass @Declination.setter @@ -217,6 +524,14 @@ def Declination(self, value): @property @abstractmethod def DeclinationRate(self): + """ + Declination tracking rate in arcseconds per second. (`float`) + + This, in conjunction with `RightAscensionRate`, is primarily used for offset tracking, + tracking objects that move relatively slowly against the equatorial coordinate system. + The supported range is telescope-dependent, but it can be expected to be a range + sufficient for guiding error corrections. + """ pass @DeclinationRate.setter @@ -227,6 +542,7 @@ def DeclinationRate(self, value): @property @abstractmethod def DoesRefraction(self): + """Whether the telescope applies atmospheric refraction to coordinates. (`bool`)""" pass @DoesRefraction.setter @@ -237,16 +553,42 @@ def DoesRefraction(self, value): @property @abstractmethod def EquatorialSystem(self): + """ + Equatorial coordinate system used by the telescope. (`enum`) + + See below for example values. + * 0 : Custom/unknown equinox and/or reference frame. + * 1 : Topocentric coordinates. Coordinates at the current date, allowing for annual aberration and precession-nutation. Most common for amateur telescopes. + * 2 : J2000 equator and equinox. Coordinates at the J2000 epoch, ICRS reference frame. Most common for professional telescopes. + * 3 : J2050 equator and equinox. Coordinates at the J2050 epoch, ICRS reference frame. + * 4 : B1950 equinox. Coordinates at the B1950 epoch, FK4 reference frame. + """ pass @property @abstractmethod def FocalLength(self): + """ + Focal length of the telescope in meters. (`float`) + + May be used by clients to calculate the field of view and plate scale of the telescope + in combination with detector pixel size and geometry. + """ pass @property @abstractmethod def GuideRateDeclination(self): + """ + Current declination movement rate offset in degrees per second. (`float`) + + This is the rate for both hardware guiding and the `PulseGuide` method. + The supported range is telescope-dependent, but it can be expected to be a range + sufficient for guiding error corrections. + If the telescope is incapable of separate guide rates for RA and Dec, this property + and `GuideRateRightAscension` may be tied together; changing one property will change the other. + Mounts must start up with a default rate, and this property must return that rate until changed. + """ pass @GuideRateDeclination.setter @@ -257,6 +599,16 @@ def GuideRateDeclination(self, value): @property @abstractmethod def GuideRateRightAscension(self): + """ + Current right ascension movement rate offset in degrees per second. (`float`) + + This is the rate for both hardware guiding and the `PulseGuide` method. + The supported range is telescope-dependent, but it can be expected to be a range + sufficient for guiding error corrections. + If the telescope is incapable of separate guide rates for RA and Dec, this property + and `GuideRateDeclination` may be tied together; changing one property will change the other. + Mounts must start up with a default rate, and this property must return that rate until changed. + """ pass @GuideRateRightAscension.setter @@ -267,16 +619,30 @@ def GuideRateRightAscension(self, value): @property @abstractmethod def IsPulseGuiding(self): + """Whether the telescope is currently pulse guiding using `PulseGuide`. (`bool`)""" pass @property @abstractmethod def RightAscension(self): + """Right ascension of the telescope's current position, in the coordinate system given by `EquatorialSystem`, in hours. (`float`)""" pass @property @abstractmethod def RightAscensionRate(self): + """ + Right ascension tracking rate in seconds per sidereal second. (`float`) + + This, in conjunction with `DeclinationRate`, is primarily used for offset tracking, + tracking objects that move relatively slowly against the equatorial coordinate system. + The supported range is telescope-dependent, but it can be expected to be a range + sufficient for guiding error corrections. + + Notes + ----- + To convet to sidereal seconds per UTC second, multiply by 0.9972695677. + """ pass @RightAscensionRate.setter @@ -287,6 +653,19 @@ def RightAscensionRate(self, value): @property @abstractmethod def SideOfPier(self): + """ + The pointing state of the mount. (`enum`) + + See below for example values. + * 0 : Normal pointing state, mount on East side of pier, looking West. + * 1 : Through the pole pointing state, mount on West side of pier, looking East. + * -1 : Unknown or indeterminate. + + .. warning:: + The name of this property is misleading and does not reflect the true meaning of the property. + The name will not change out of preservation of compatibility. + For more information, see the 'Remarks' section of `this page `_. + """ pass @SideOfPier.setter @@ -297,11 +676,18 @@ def SideOfPier(self, value): @property @abstractmethod def SiderealTime(self): + """ + Local apparent sidereal time from the telescope's internal clock in hours. (`float`) + + If the telesope has no sidereal time capability, this property should be calculated from the system + clock by the driver. + """ pass @property @abstractmethod def SiteElevation(self): + """Elevation of the telescope's site above sea level in meters. (`float`)""" pass @SiteElevation.setter @@ -312,6 +698,7 @@ def SiteElevation(self, value): @property @abstractmethod def SiteLatitude(self): + """Latitude (geodetic) of the telescope's site in degrees. (`float`)""" pass @SiteLatitude.setter @@ -322,16 +709,32 @@ def SiteLatitude(self, value): @property @abstractmethod def SiteLongitude(self): + """Longitude (geodetic) of the telescope's site in degrees. (`float`)""" pass @property @abstractmethod def Slewing(self): + """ + Whether the telescope is currently slewing due to one of the Slew methods or `MoveAxis`. (`bool`) + + Telescopes incapable of asynchronous slewing will always have this property be `False`. + Slewing for the purpose of this property excludes motion by sidereal tracking, pulse guiding, + `RightAscensionRate`, and `DeclinationRate`. + Only Slew commands, flipping due to `SideOfPier`, and `MoveAxis` should set this property to `True`. + """ pass @property @abstractmethod def SlewSettleTime(self): + """ + A set post-slew settling time in seconds. (`int`) + + This adds additional time to the end of all slew operations. + In practice, slew methods will not return, and `Slewing` will not be `False`, until the + operation has ended plus this time has elapsed. + """ pass @SlewSettleTime.setter @@ -342,6 +745,7 @@ def SlewSettleTime(self, value): @property @abstractmethod def TargetDeclination(self): + """Declination coordinate in degrees of the telescope's current slew or sync target. (`float`)""" pass @TargetDeclination.setter @@ -352,6 +756,7 @@ def TargetDeclination(self, value): @property @abstractmethod def TargetRightAscension(self): + """Right ascension coordinate in hours of the telescope's current slew or sync target. (`float`)""" pass @TargetRightAscension.setter @@ -362,6 +767,13 @@ def TargetRightAscension(self, value): @property @abstractmethod def Tracking(self): + """ + State of the telescope's sidereal tracking drive. (`bool`) + + Changing of this property will turn sidereal drive on and off. + Some telescopes may not support changing of this property, and thus + may not support turning tracking on and off. See `CanSetTracking`. + """ pass @Tracking.setter @@ -372,6 +784,21 @@ def Tracking(self, value): @property @abstractmethod def TrackingRate(self): + """ + Current tracking rate of the telescope's sidereal drive. (`enum`) + + Supported rates are contained in `TrackingRates`, and the property's rate + is guaranteed to be one of those. + If the mount's current tracking rate cannot be read, the driver should force + and report a default rate on connection. + In this circumstance, a default of `Sidereal` rate is preferred. + + See below for example values. + * 0 : Sidereal tracking, 15.041 arcseconds per second. + * 1 : Lunar tracking, 14.685 arcseconds per second. + * 2 : Solar tracking, 15.0 arcseconds per second. + * 3 : King tracking, 15.0369 arcseconds per second. + """ pass @TrackingRate.setter @@ -382,11 +809,37 @@ def TrackingRate(self, value): @property @abstractmethod def TrackingRates(self): + """ + Collection of supported tracking rates for the telescope's sidereal drive. (`object`) + + This collection should contain all supported tracking rates. At a minimum, it should contain + an item for the default `Sidereal` rate. The collection should be an iterable, and should + include properties for the number of rates, and the actual rates, and methods for returning + an enumerator for the rates, and for disposing of the object as a whole. + + See below for an example collection. + * 0 : Sidereal tracking, 15.041 arcseconds per second. + * 1 : Lunar tracking, 14.685 arcseconds per second. + * 2 : Solar tracking, 15.0 arcseconds per second. + * 3 : King tracking, 15.0369 arcseconds per second. + """ pass @property @abstractmethod def UTCDate(self): + """ + UTC date and time of the telescope's internal clock. (`datetime`) + + If the telescope has no UTC time capability, this property should be calculated from the system + clock by the driver. + If the telescope does have UTC measuring capability, changing of its internal UTC clock is permitted, + for instance for allowing clients to adjust for accuracy. + + .. warning:: + If the telescope has no UTC time capability, do NOT under any circumstances implement the property + to be writeable, as it would change the system clock. + """ pass @UTCDate.setter From d301b4d44eadbb06ce45f209bd494191cb5ee32c Mon Sep 17 00:00:00 2001 From: ccolin Date: Tue, 3 Dec 2024 10:29:51 -0600 Subject: [PATCH 42/60] autofocus docs --- pyscope/observatory/autofocus.py | 41 +++++++++++++++++++++++++++ pyscope/observatory/maxim.py | 27 ++++++++++++++++++ pyscope/observatory/pwi4_autofocus.py | 31 ++++++++++++++++++++ pyscope/observatory/pwi_autofocus.py | 24 ++++++++++++++++ 4 files changed, 123 insertions(+) diff --git a/pyscope/observatory/autofocus.py b/pyscope/observatory/autofocus.py index 1876f538..f8f60411 100644 --- a/pyscope/observatory/autofocus.py +++ b/pyscope/observatory/autofocus.py @@ -6,12 +6,53 @@ class Autofocus(ABC, metaclass=_DocstringInheritee): @abstractmethod def __init__(self, *args, **kwargs): + """ + Abstract base class for autofocus functionality on different utility platforms. + + This class provides a common interface for autofocus functionality, including the + bare minimum for any platform such as initialization, running the autofocus routine, + and aborting the autofocus routine. + Example platforms include Maxim, PWI, PWI4, etc. + Args and kwargs provide needed parameters to the platform-specific autofocus routine, + such as hosting protocol and port. + + Parameters + ---------- + *args : `tuple` + Variable length argument list. + **kwargs : `dict` + Arbitrary keyword arguments. + """ pass @abstractmethod def Run(self, *args, **kwargs): + """ + Run the autofocus routine on the given platform. + Args and kwargs provide needed parameters to the platform-specific autofocus routine, + such as exposure time and timeout. + + Parameters + ---------- + *args : `tuple` + Variable length argument list. + **kwargs : `dict` + Arbitrary keyword arguments. + """ pass @abstractmethod def Abort(self, *args, **kwargs): + """ + Abort the autofocus routine on the given platform. + Whether aborting is immediate or has a gracious exit process is platform-specific. + Args and kwargs usually should not be needed for an abort. + + Parameters + ---------- + *args : `tuple` + Variable length argument list. + **kwargs : `dict` + Arbitrary keyword arguments. + """ pass diff --git a/pyscope/observatory/maxim.py b/pyscope/observatory/maxim.py index 80ce82e1..77eb3479 100755 --- a/pyscope/observatory/maxim.py +++ b/pyscope/observatory/maxim.py @@ -66,10 +66,34 @@ def camera(self): class _MaximAutofocus(Autofocus): def __init__(self, maxim): + """ + Autofocus class for Maxim DL. + + This class provides an interface for running and aborting the autofocus routine in Maxim DL. + + Parameters + ---------- + maxim : `Maxim` + The Maxim DL object. + """ logger.debug("_MaximAutofocus.MaximAutofocus __init__ called") self.maxim = maxim def Run(self, exposure=10): + """ + Run the autofocus routine in Maxim DL. + Only returns once the autofocus routine is complete. + + Parameters + ---------- + exposure : `int`, default : 10, optional + The exposure time in seconds for the autofocus routine. + + Returns + ------- + `bool` + `True` if the autofocus routine was successful, `False` if it failed. + """ logger.debug(f"Run called with exposure={exposure}") self.maxim.Autofocus(exposure) @@ -82,6 +106,9 @@ def Run(self, exposure=10): return False def Abort(self): + """ + Abort the autofocus routine in Maxim DL. + """ logger.debug("_MaximAutofocus.Abort called") raise NotImplementedError diff --git a/pyscope/observatory/pwi4_autofocus.py b/pyscope/observatory/pwi4_autofocus.py index fad2feef..8325bac0 100644 --- a/pyscope/observatory/pwi4_autofocus.py +++ b/pyscope/observatory/pwi4_autofocus.py @@ -9,11 +9,39 @@ class PWI4Autofocus(Autofocus): def __init__(self, host="localhost", port=8220): + """ + Autofocus class for the PlaneWave Interface 4 (PWI4) utility platform. + + This class provides an interface for autofocus running, and aborting on the PWI4 platform. + + Parameters + ---------- + host : `str`, default : "localhost", optional + The host of the PWI4 server. + port : `int`, default : 8220, optional + The port of the PWI4 server. + """ self._host = host self._port = port self._app = _PWI4(host=self._host, port=self._port) def Run(self, *args, **kwargs): + """ + Run the autofocus routine on the PWI4 platform. + Only returns once the autofocus routine is complete. + + Parameters + ---------- + *args : `tuple` + Variable length argument list. + **kwargs : `dict` + Arbitrary keyword arguments. + + Returns + ------- + `float` + The best focuser position found by the autofocus routine. + """ logger.debug("Starting autofocus in PWI4Autofocus") self._app.request("/autofocus/start") logger.info("Autofocus started") @@ -30,5 +58,8 @@ def Run(self, *args, **kwargs): return self._app.status().autofocus.best_position def Abort(self): + """ + Abort the autofocus routine on the PWI4 platform. + """ logger.debug("Aborting autofocus in PWI4Autofocus") _ = self._app.focuser_stop() diff --git a/pyscope/observatory/pwi_autofocus.py b/pyscope/observatory/pwi_autofocus.py index db86e486..11d28896 100755 --- a/pyscope/observatory/pwi_autofocus.py +++ b/pyscope/observatory/pwi_autofocus.py @@ -9,6 +9,11 @@ class PWIAutofocus(Autofocus): def __init__(self): + """ + Autofocus class for PlaneWave Instruments focuser. + + This class provides an interface for autofocus running, and aborting on the PlaneWave Instruments focuser. + """ logger.debug("PWIAutofocus.__init__ called") if platform.system() != "Windows": raise Exception("This class is only available on Windows.") @@ -33,6 +38,22 @@ def __init__(self): self._com_object.PreventFilterChange = True def Run(self, exposure=10, timeout=120): + """ + Run the autofocus routine on the PlaneWave Instruments focuser. + Only returns once the autofocus routine has completed. + + Parameters + ---------- + exposure : `float`, default : 10, optional + Exposure time in seconds for the autofocus routine. + timeout : `float`, default : 120, optional + Timeout in seconds for the autofocus routine to complete. + + Returns + ------- + `float` or `None` + The best position found by the autofocus routine, or None if the autofocus routine failed. + """ logger.debug( f"PWIAutofocus.Run called with args: exposure={exposure}, timeout={timeout}" ) @@ -61,6 +82,9 @@ def Run(self, exposure=10, timeout=120): return None def Abort(self): + """ + Abort the autofocus routine on the PlaneWave Instruments focuser. + """ logger.debug("PWIAutofocus.Abort called") self._com_object.StopAutofocus From 15a4a3feed87148c3ba40aa3ecfd68134dce380e Mon Sep 17 00:00:00 2001 From: ccolin Date: Tue, 3 Dec 2024 10:46:00 -0600 Subject: [PATCH 43/60] abstract doc sentence consistency fix --- pyscope/observatory/camera.py | 2 +- pyscope/observatory/switch.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyscope/observatory/camera.py b/pyscope/observatory/camera.py index 51c36b35..af9530db 100644 --- a/pyscope/observatory/camera.py +++ b/pyscope/observatory/camera.py @@ -7,7 +7,7 @@ class Camera(ABC, metaclass=_DocstringInheritee): @abstractmethod def __init__(self, *args, **kwargs): """ - Abstract class for camera devices. + Abstract base class for camera devices. The class defines the interface for camera devices, including methods for controlling exposures, guiding, and retrieving camera properties. Subclasses must implement diff --git a/pyscope/observatory/switch.py b/pyscope/observatory/switch.py index 980982a8..6978e03c 100644 --- a/pyscope/observatory/switch.py +++ b/pyscope/observatory/switch.py @@ -7,7 +7,7 @@ class Switch(ABC, metaclass=_DocstringInheritee): @abstractmethod def __init__(self, *args, **kwargs): """ - Abstract class for switch devices. + Abstract base class for switch devices. This class defines the common interface for switch devices, including methods for getting and setting switch values, and getting switch names and descriptions. From 9362156ea46b1da9692735fed9df2ee3fa287c26 Mon Sep 17 00:00:00 2001 From: ccolin Date: Tue, 3 Dec 2024 11:10:56 -0600 Subject: [PATCH 44/60] safety monitor docs --- pyscope/observatory/ascom_safety_monitor.py | 19 +++++++++++++++ pyscope/observatory/html_safety_monitor.py | 27 +++++++++++++++++++++ pyscope/observatory/safety_monitor.py | 15 ++++++++++++ 3 files changed, 61 insertions(+) diff --git a/pyscope/observatory/ascom_safety_monitor.py b/pyscope/observatory/ascom_safety_monitor.py index edae555d..90e46787 100644 --- a/pyscope/observatory/ascom_safety_monitor.py +++ b/pyscope/observatory/ascom_safety_monitor.py @@ -8,6 +8,25 @@ class ASCOMSafetyMonitor(ASCOMDevice, SafetyMonitor): def __init__(self, identifier, alpaca=False, device_number=0, protocol="http"): + """ + ASCOM implementation of the SafetyMonitor base class. + + This class provides an interface to ASCOM-compatible safety monitors, + allowing the observatory to check if weather, power, and other + observatory-specific conditions allow safe usage of observatory equipment, + such as opening the roof or dome. + + Parameters + ---------- + identifier : `str` + The device identifier. + alpaca : `bool`, default : `False`, optional + Whether the device is an Alpaca device. + device_number : `int`, default : 0, optional + The device number. + protocol : `str`, default : "http", optional + The device communication protocol. + """ super().__init__( identifier, alpaca=alpaca, diff --git a/pyscope/observatory/html_safety_monitor.py b/pyscope/observatory/html_safety_monitor.py index 1921dab1..a0d13ada 100755 --- a/pyscope/observatory/html_safety_monitor.py +++ b/pyscope/observatory/html_safety_monitor.py @@ -9,6 +9,24 @@ class HTMLSafetyMonitor(SafetyMonitor): def __init__(self, url, check_phrase=b"ROOFPOSITION=OPEN"): + """ + HTML implementation of the SafetyMonitor base class. + + This class provides an interface to access safety monitors, + doing so using data fetched from a URL. This allows the observatory + to check if weather, power, and other observatory-specific conditions + allow safe usage of observatory equipment, such as opening the roof or dome. + Other than the method `IsSafe`, this class also provides the properties + for information about the driver, about the interface, description, and name. + + Parameters + ---------- + url : `str` + The URL to fetch data from. + check_phrase : `bytes`, default : b"ROOFPOSITION=OPEN", optional + The phrase to check for in the data fetched from the URL. + In the default case, we would check if it is safe to open the roof. + """ logger.debug( f"""HTMLSafetyMonitor.__init__( {url}, check_phrase={check_phrase}) called""" @@ -19,6 +37,9 @@ def __init__(self, url, check_phrase=b"ROOFPOSITION=OPEN"): @property def IsSafe(self): + """ + Whether the observatory equipment/action specified in the constructor's `check_phrase` is safe to use. (`bool`) + """ logger.debug(f"""HTMLSafetyMonitor.IsSafe property called""") safe = None @@ -47,30 +68,36 @@ def IsSafe(self): @property def DriverVersion(self): + """Version of the driver. (`str`)""" logger.debug(f"""HTMLSafetyMonitor.DriverVersion property called""") return "1.0" @property def DriverInfo(self): + """Information about the driver. (`str`)""" logger.debug(f"""HTMLSafetyMonitor.DriverInfo property called""") return "HTML Safety Monitor" @property def InterfaceVersion(self): + """Version of the interface. (`str`)""" logger.debug(f"""HTMLSafetyMonitor.InterfaceVersion property called""") return "1.0" @property def Description(self): + """Description of the driver. (`str`)""" logger.debug(f"""HTMLSafetyMonitor.Description property called""") return "HTML Safety Monitor" @property def SupportedActions(self): + """List of supported actions. (`list`)""" logger.debug(f"""HTMLSafetyMonitor.SupportedActions property called""") return [] @property def Name(self): + """Name of the driver/url. (`str`)""" logger.debug(f"""HTMLSafetyMonitor.Name property called""") return self._url diff --git a/pyscope/observatory/safety_monitor.py b/pyscope/observatory/safety_monitor.py index cef470b9..94e91453 100644 --- a/pyscope/observatory/safety_monitor.py +++ b/pyscope/observatory/safety_monitor.py @@ -6,9 +6,24 @@ class SafetyMonitor(ABC, metaclass=_DocstringInheritee): @abstractmethod def __init__(self, *args, **kwargs): + """ + Abstract base class for safety monitors. + + This class defines the common interface for safety monitors, a way to check if + weather, power, and other observatory-specific conditions allow safe usage of + observatory equipment. Subclasses must implement the abstract method in this class. + + Parameters + ---------- + *args : `tuple` + Variable length argument list. + **kwargs : `dict` + Arbitrary keyword arguments. + """ pass @property @abstractmethod def IsSafe(self): + """Whether the observatory is safe for use. (`bool`)""" pass From ae4025992d6122b005c1da4c4a8c9889130ca634 Mon Sep 17 00:00:00 2001 From: ccolin Date: Tue, 3 Dec 2024 11:26:39 -0600 Subject: [PATCH 45/60] simulator server docs --- pyscope/observatory/simulator_server.py | 30 +++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pyscope/observatory/simulator_server.py b/pyscope/observatory/simulator_server.py index b93f7f95..abc59750 100644 --- a/pyscope/observatory/simulator_server.py +++ b/pyscope/observatory/simulator_server.py @@ -12,6 +12,33 @@ class SimulatorServer: def __init__(self, force_update=False): + """ + Class for starting the ASCOM Alpaca Simulators server. + + This classhandles downloading, extracting, and launching the ASCOM Alpaca Simulators + server executable appropriate for the host's operating system and architecture. + Ensures correct version is downloaded and forces updating if specified. + + Parameters + ---------- + force_update : bool, default : `False`, optional + If `True`, forces download of the ASCOM Alpaca Simulators server executable. + If `False`, checks if the executable exists and skips download if it does. + + Raises + ------ + Exception + If the host's operating system is not supported. + + Notes + ----- + The server executable is downloaded from the ASCOM Initiative GitHub repository found `here `_, + and is from the latest release version of v0.3.1. + Currently supported operating systems are: + - macOS (Darwin) + - Linux (x86_64, armhf, aarch64) + - Windows (x86, x64) + """ if platform.system() == "Darwin": sys_name = "macos-x64" zip_type = ".zip" @@ -86,6 +113,9 @@ def __init__(self, force_update=False): os.chdir(current_dir) def __del__(self): + """ + Automatically kills the server process when the object is deleted. + """ if platform.system() == "Darwin" or platform.system() == "Linux": # self.process.kill() # doesn't work since sudo is needed subprocess.Popen( From b04965b826cf42e7e817e392bed8173c1353f446 Mon Sep 17 00:00:00 2001 From: WGolay Date: Tue, 3 Dec 2024 14:04:40 -0500 Subject: [PATCH 46/60] Updates --- pyscope/telrun/airmass_condition.py | 11 +- pyscope/telrun/celestialbody_condition.py | 61 ++++++ pyscope/telrun/coord_condition.py | 182 ++++++++++++++--- pyscope/telrun/hourangle_condition.py | 141 ++++++++++--- pyscope/telrun/lqs.py | 0 pyscope/telrun/lqs_gauss.py | 0 pyscope/telrun/lqs_inequality.py | 0 pyscope/telrun/lqs_minmax.py | 0 pyscope/telrun/lqs_piecewise.py | 0 pyscope/telrun/lqs_sigmoid.py | 0 pyscope/telrun/moon_condition.py | 30 +-- pyscope/telrun/snr_condition.py | 39 +++- pyscope/telrun/sun_condition.py | 23 +-- pyscope/telrun/time_condition.py | 231 +++++++++++++++++++++- 14 files changed, 615 insertions(+), 103 deletions(-) create mode 100644 pyscope/telrun/celestialbody_condition.py create mode 100644 pyscope/telrun/lqs.py create mode 100644 pyscope/telrun/lqs_gauss.py create mode 100644 pyscope/telrun/lqs_inequality.py create mode 100644 pyscope/telrun/lqs_minmax.py create mode 100644 pyscope/telrun/lqs_piecewise.py create mode 100644 pyscope/telrun/lqs_sigmoid.py diff --git a/pyscope/telrun/airmass_condition.py b/pyscope/telrun/airmass_condition.py index 0b2602b1..3803b2dd 100644 --- a/pyscope/telrun/airmass_condition.py +++ b/pyscope/telrun/airmass_condition.py @@ -6,7 +6,7 @@ class AirmassCondition(BoundaryCondition): - def __init__(self, airmass_limit=3, formula="Schoenberg1929", weight=1, **kwargs): + def __init__(self, airmass_limit=3, formula="Schoenberg1929", weight=1): """ A condition that penalizes targets for higher airmass values up to a limit. @@ -56,9 +56,6 @@ def __init__(self, airmass_limit=3, formula="Schoenberg1929", weight=1, **kwargs weight : `float`, default : 1 The weight of the condition in the final score. The default is 1. - **kwargs : `dict`, default : {} - Additional keyword arguments to pass to the `~pyscope.telrun.BoundaryCondition` constructor for storage in the `~pyscope.telrun.BoundaryCondition.kwargs` attribute. - References ---------- .. [1] `Schoenberg, E. 1929. Theoretische Photometrie, Über die Extinktion des Lichtes in der Erdatmosphäre. In Handbuch der Astrophysik. Band II, erste Hälfte. Berlin: Springer. `_ @@ -77,7 +74,7 @@ def __init__(self, airmass_limit=3, formula="Schoenberg1929", weight=1, **kwargs """ logger.debug("AirmassCondition(airmass_limit=%s, formula=%s, weight=%s)") - super().__init__(func=self._func, lqs_func=self._lqs_func, **kwargs) + super().__init__(func=self._func, lqs_func=self._lqs_func) @classmethod def from_string(self, string, airmass_limit=None, formula=None, weight=None): @@ -114,7 +111,7 @@ def __str__(self): logger.debug("AirmassCondition().__str__()") @staticmethod - def _func(target, time, location, formula="Schoenberg1929", **kwargs): + def _func(target, time, location, formula="Schoenberg1929"): """ Calculate the airmass value for the target. @@ -132,7 +129,7 @@ def _func(target, time, location, formula="Schoenberg1929", **kwargs): logger.debug("AirmassCondition._func(target=%s)" % target) @staticmethod - def _lqs_func(self, value, airmass_limit=3, **kwargs): + def _lqs_func(self, value, airmass_limit=3): """ Calculate the linear quality score for the airmass value. diff --git a/pyscope/telrun/celestialbody_condition.py b/pyscope/telrun/celestialbody_condition.py new file mode 100644 index 00000000..da7bd319 --- /dev/null +++ b/pyscope/telrun/celestialbody_condition.py @@ -0,0 +1,61 @@ +import logging + +from astropy import units as u + +from .boundary_condition import BoundaryCondition + +logger = logging.getLogger(__name__) + + +class CelestialBodyCondition(BoundaryCondition): + def __init__( + self, + body_name, + min_sep=0 * u.deg, + max_sep=180 * u.deg, + min_alt=-90 * u.deg, + max_alt=90 * u.deg, + score_type="boolean", + inclusive=True, + ): + """ """ + pass + + @classmethod + def from_string(cls, string): + pass + + def __str__(self): + pass + + @staticmethod + def _func(self, target, observer, time): + pass + + @staticmethod + def _lsq_func(self, value): + pass + + @property + def min_sep(self): + pass + + @property + def max_sep(self): + pass + + @property + def min_alt(self): + pass + + @property + def max_alt(self): + pass + + @property + def score_type(self): + pass + + @property + def inclusive(self): + pass diff --git a/pyscope/telrun/coord_condition.py b/pyscope/telrun/coord_condition.py index 4fc9fa1b..cc73d760 100644 --- a/pyscope/telrun/coord_condition.py +++ b/pyscope/telrun/coord_condition.py @@ -13,8 +13,8 @@ def __init__( min_val=None, max_val=None, score_type="boolean", + inclusive=True, ref_coord=None, - **kwargs, ): """ A restriction on the coordinates of the target viewed from a specific location @@ -48,14 +48,17 @@ def __init__( that returns 1 if the condition is met and 0 if it is not, commonly used for determining if the source is above or below the horizon. The default is "boolean". + inclusive : `bool`, default : `True` + If `True`, the condition is inclusive, meaning that the target must be within + the specified range. If `False`, the condition is exclusive, meaning that the + target must be outside the specified range. Useful for excluding certain parts + of the sky where a mount may be in jeopardy. + ref_coord : `~astropy.coordinates.SkyCoord`, default : `None` If `coord_type` is "radec" or "galactic", a user can specify a center value and `min_val` and `max_val` will be interpreted as a minimum and maximum angular separation of the target from the reference coordinate. - **kwargs : `dict`, default : {} - Additional keyword arguments to pass to the `~pyscope.telrun.BoundaryCondition` constructor. - """ logger.debug( """ @@ -65,10 +68,18 @@ def __init__( min_val=%s, max_val=%s, score_type=%s, + inclusive=%s, ref_coord=%s, - kwargs=%s )""" - % (coord_type, coord_idx, min_val, max_val, score_type, ref_coord, kwargs) + % ( + coord_type, + coord_idx, + min_val, + max_val, + score_type, + inclusive, + ref_coord, + ) ) @classmethod @@ -80,11 +91,12 @@ def from_string( min_val=None, max_val=None, score_type=None, + inclusive=None, ref_coord=None, - **kwargs, ): """ - Create a `~pyscope.telrun.CoordinateCondition` or a `list` of `~pyscope.telrun.CoordinateCondition` objects from a `str` representation of a `~pyscope.telrun.CoordinateCondition`. + Create a `~pyscope.telrun.CoordinateCondition` or a `list` of `~pyscope.telrun.CoordinateCondition` objects from a `str` + representation of a `~pyscope.telrun.CoordinateCondition`. All optional parameters are used to override the parameters extracted from the `str` representation. Parameters @@ -101,14 +113,23 @@ def from_string( score_type : `str`, default : `None` - ref_coord : `~astropy.coordinates.SkyCoord`, default : `None` + inclusive : `bool`, default : `None` - **kwargs : `dict`, default : {} + ref_coord : `~astropy.coordinates.SkyCoord`, default : `None` """ logger.debug( - "CoordinateCondition.from_string(string=%s, coord_type=%s, coord_idx=%s, min_val=%s, max_val=%s, score_type=%s, kwargs=%s)" - % (string, coord_type, coord_idx, min_val, max_val, score_type, kwargs) + "CoordinateCondition.from_string(string=%s, coord_type=%s, coord_idx=%s, min_val=%s, max_val=%s, score_type=%s, inclusive=%s, ref_coord=%s)" + % ( + string, + coord_type, + coord_idx, + min_val, + max_val, + score_type, + inclusive, + ref_coord, + ) ) def __str__(self): @@ -124,33 +145,150 @@ def __str__(self): logger.debug("CoordinateCondition().__str__()") @staticmethod - def _func(): - pass + def _func(self, target, time, location): + """ + Determine if the target meets the coordinate condition. + + Parameters + ---------- + target : `~astropy.coordinates.SkyCoord`, required + The target to check. + + time : `~astropy.time.Time`, required + The time of the observation. + + location : `~astropy.coordinates.EarthLocation`, required + The location of the observer. + + Returns + ------- + `float` or `bool` + The score value of the target. If `score_type` is "linear", the score is a `float` + value between 0 and 1. If `score_type` is "boolean", the score is a `bool` value of + `True` if the target meets the condition and `False` if it does not. + + """ + logger.debug( + "CoordinateCondition()._func(target=%s, time=%s, location=%s)" + % (target, time, location) + ) @staticmethod - def _lqs_func(): - pass + def _lqs_func(self, value): + """ + Calculate the linear quality score for the coordinate value. + + Parameters + ---------- + value : `float` or `bool`, required + + Returns + ------- + `float` + The linear quality score for the coordinate value. + + """ + logger.debug("CoordinateCondition._lqs_func(value=%s)" % value) @property def coord_type(self): - pass + """ + The type of coordinate system to use. Options are "altaz" for altitude and azimuth, "radec" for right ascension + and declination, and "galactic" for galactic latitude and longitude. + + Returns + ------- + `str` + The type of coordinate system to use. + + """ + logger.debug("CoordinateCondition().coord_type == %s" % self._coord_type) + return self._coord_type @property def coord_idx(self): - pass + """ + The index of the coordinate to use. The default is 0 for the first coordinate, e.g. altitude or right ascension. + + Returns + ------- + `int` + The index of the coordinate to use. + + """ + logger.debug("CoordinateCondition().coord_idx == %i" % self._coord_idx) + return self._coord_idx @property def min_val(self): - pass + """ + If `None`, there is no minimum value. Otherwise, the minimum value for the coordinate. + + Returns + ------- + `~astropy.units.Quantity` + The minimum value for the coordinate. + + """ + logger.debug("CoordinateCondition().min_val == %s" % self._min_val) + return self._min_val @property def max_val(self): - pass + """ + If `None`, there is no maximum value. Otherwise, the maximum value for the coordinate. + + Returns + ------- + `~astropy.units.Quantity` + The maximum value for the coordinate. + + """ + logger.debug("CoordinateCondition().max_val == %s" % self._max_val) + return self._max_val @property def score_type(self): - pass + """ + The type of scoring function to use. Options are "linear" for a linear function, commonly used for altitude, + and "boolean" for a binary function that returns 1 if the condition is met and 0 if it is not. + + Returns + ------- + `str` + The type of scoring function to use. + + """ + logger.debug("CoordinateCondition().score_type == %s" % self._score_type) + return self._score_type + + @property + def inclusive(self): + """ + If `True`, the condition is inclusive, meaning that the target must be within the specified range. If `False`, + the condition is exclusive, meaning that the target must be outside the specified range. + Essentially acts as a "not" operator. + + Returns + ------- + `bool` + If the condition is inclusive or exclusive. + + """ + logger.debug("CoordinateCondition().inclusive == %s" % self._inclusive) + return self._inclusive @property def ref_coord(self): - pass + """ + If `coord_type` is "radec" or "galactic", a user can specify a center value and `min_val` and `max_val` will be interpreted + as a minimum and maximum radial angular separation of the target from the reference coordinate. + + Returns + ------- + `~astropy.coordinates.SkyCoord` + The reference coordinate. + + """ + logger.debug("CoordinateCondition().ref_coord == %s" % self._ref_coord) + return self._ref_coord diff --git a/pyscope/telrun/hourangle_condition.py b/pyscope/telrun/hourangle_condition.py index 9647bfbe..60a29737 100644 --- a/pyscope/telrun/hourangle_condition.py +++ b/pyscope/telrun/hourangle_condition.py @@ -13,6 +13,7 @@ def __init__( min_hour_angle=-6 * u.hourangle, max_hour_angle=6 * u.hourangle, score_type="linear", + inclusive=True, ): """ A restriction on the hour angle over which a target can be observed. @@ -38,18 +39,30 @@ def __init__( for optimizing the observing time, and "boolean" for a binary function that returns 1 if the condition is met and 0 if it is not. The default is "linear". + inclusive : `bool`, default : `True` + If `True`, the condition is inclusive, meaning that the target must be within the specified range. + If `False`, the condition is exclusive, meaning that the target must be outside the specified range. + """ logger.debug( """HourAngleCondition( min_hour_angle=%s, max_hour_angle=%s, - score_type=%s + score_type=%s, + inclusive=%s )""" - % (min_hour_angle, max_hour_angle, score_type) + % (min_hour_angle, max_hour_angle, score_type, inclusive) ) @classmethod - def from_string(cls, string, min_hour_angle=None, max_hour_angle=None): + def from_string( + cls, + string, + min_hour_angle=None, + max_hour_angle=None, + score_type=None, + inclusive=None, + ): """ Create a new `~pyscope.telrun.HourAngleCondition` or a `list` of `~pyscope.telrun.HourAngleCondition` objects from a `str`. Any optional parameters are used to override the parameters extracted from the `str`. @@ -62,14 +75,24 @@ def from_string(cls, string, min_hour_angle=None, max_hour_angle=None): max_hour_angle : `~astropy.units.Quantity`, default : `None` + score_type : `str`, default : `None` + + inclusive : `bool`, default : `None` + Returns ------- `~pyscope.telrun.HourAngleCondition` or `list` of `~pyscope.telrun.HourAngleCondition` """ logger.debug( - "HourAngleCondition.from_string(string=%s, min_hour_angle=%s, max_hour_angle=%s)" - % (string, min_hour_angle, max_hour_angle) + """HourAngleCondition.from_string( + string=%s, + min_hour_angle=%s, + max_hour_angle=%s, + score_type=%s, + inclusive=%s + )""" + % (string, min_hour_angle, max_hour_angle, score_type, inclusive) ) def __str__(self): @@ -84,44 +107,106 @@ def __str__(self): """ logger.debug("HourAngleCondition().__str__()") - def __repr__(self): + @staticmethod + def _func(self, target, time, location): """ - Return a `str` representation of the `~pyscope.telrun.HourAngleCondition`. + Calculate the hour angle value for the target. + + Parameters + ---------- + target : `~astropy.coordinates.SkyCoord`, required + The target to calculate the hour angle value for. + + time : `~astropy.time.Time`, required + The time at which to calculate the hour angle value. + + location : `~astropy.coordinates.EarthLocation`, required + The location at which to calculate the hour angle value. Returns ------- - `str` - A `str` representation of the `~pyscope.telrun.HourAngleCondition`. + `~astropy.units.Quantity` + The hour angle value for the target. + """ - logger.debug("HourAngleCondition().__repr__()") - return str(self) + logger.debug( + "HourAngleCondition._func(target=%s, time=%s, location=%s)" + % (target, time, location) + ) - def __call__(self, time, location, target): + @staticmethod + def _lqs_func(self, value): """ - Compute the score for the hour angle condition. + Calculate the linear quality score for the hour angle value. Parameters ---------- - time : `~astropy.time.Time`, required - The time at which the observation is to be made. + value : `~astropy.units.Quantity`, required + The hour angle value for the target. - location : `~astropy.coordinates.EarthLocation`, required - The location of the observer. + Returns + ------- + `float` + The linear quality score for the hour angle value. - target : `~astropy.coordinates.SkyCoord`, required - The target to evaluate the condition for. + """ + logger.debug("HourAngleCondition._lqs_func(value=%s)" % value) + + @property + def min_hour_angle(self): + """ + The minimum hour angle at which the target can be observed. Returns ------- - `float` - The score for the hour angle condition from `0` to `1`. + `~astropy.units.Quantity` + The minimum hour angle at which the target can be observed """ - logger.debug( - "HourAngleCondition().__call__(time=%s, location=%s, target=%s)" - % (time, location, target) - ) + logger.debug("HourAngleCondition().min_hour_angle == %s" % self._min_hour_angle) + return self._min_hour_angle + + @property + def max_hour_angle(self): + """ + The maximum hour angle at which the target can be observed. + + Returns + ------- + `~astropy.units.Quantity` + The maximum hour angle at which the target can be observed - def plot(self, time, location, target=None, ax=None): - """ """ - pass + """ + logger.debug("HourAngleCondition().max_hour_angle == %s" % self._max_hour_angle) + return self._max_hour_angle + + @property + def score_type(self): + """ + The type of scoring function to use. The options are "linear" for a linear function, commonly used for + optimizing the observing time, and "boolean" for a binary function that returns 1 if the condition is met + and 0 if it is not. + + Returns + ------- + `str` + The type of scoring function to use. + + """ + logger.debug("HourAngleCondition().score_type == %s" % self._score_type) + return self._score_type + + @property + def inclusive(self): + """ + If `True`, the condition is inclusive, meaning that the target must be within the specified range. + If `False`, the condition is exclusive, meaning that the target must be outside the specified range. + + Returns + ------- + `bool` + If `True`, the condition is inclusive. If `False`, the condition is exclusive. + + """ + logger.debug("HourAngleCondition().inclusive == %s" % self._inclusive) + return self._inclusive diff --git a/pyscope/telrun/lqs.py b/pyscope/telrun/lqs.py new file mode 100644 index 00000000..e69de29b diff --git a/pyscope/telrun/lqs_gauss.py b/pyscope/telrun/lqs_gauss.py new file mode 100644 index 00000000..e69de29b diff --git a/pyscope/telrun/lqs_inequality.py b/pyscope/telrun/lqs_inequality.py new file mode 100644 index 00000000..e69de29b diff --git a/pyscope/telrun/lqs_minmax.py b/pyscope/telrun/lqs_minmax.py new file mode 100644 index 00000000..e69de29b diff --git a/pyscope/telrun/lqs_piecewise.py b/pyscope/telrun/lqs_piecewise.py new file mode 100644 index 00000000..e69de29b diff --git a/pyscope/telrun/lqs_sigmoid.py b/pyscope/telrun/lqs_sigmoid.py new file mode 100644 index 00000000..e69de29b diff --git a/pyscope/telrun/moon_condition.py b/pyscope/telrun/moon_condition.py index 9957d648..a8e953aa 100644 --- a/pyscope/telrun/moon_condition.py +++ b/pyscope/telrun/moon_condition.py @@ -1,36 +1,18 @@ import logging -from astropy import units as u - -from .boundary_condition import BoundaryCondition +from .celestialbody_condition import CelestialBodyCondition logger = logging.getLogger(__name__) -class MoonCondition(BoundaryCondition): - def __init__( - self, - min_sep=0 * u.deg, - max_sep=180 * u.deg, - min_alt=-90 * u.deg, - max_alt=90 * u.deg, - min_illum=0, - max_illum=1, - ): +class MoonCondition(CelestialBodyCondition): + def __init__(self, min_illum=0, max_illum=1, **kwargs): """ """ pass - def __str__(self): - pass - - def __repr__(self): - logger.debug("MoonCondition().__repr__()") - return str(self) - - def __call__(self, time=None, location=None, target=None): - """ """ + @classmethod + def from_string(cls, string, min_illum=None, max_illum=None, **kwargs): pass - def plot(self, time, location, target=None, ax=None): - """ """ + def __str__(self): pass diff --git a/pyscope/telrun/snr_condition.py b/pyscope/telrun/snr_condition.py index 48b9e31f..d123ff8b 100644 --- a/pyscope/telrun/snr_condition.py +++ b/pyscope/telrun/snr_condition.py @@ -6,4 +6,41 @@ class SNRCondition(BoundaryCondition): - pass + def __init__( + self, min_snr=None, max_snr=None, score_type="boolean", inclusive=True + ): + """ """ + pass + + @classmethod + def from_string( + cls, string, min_snr=None, max_snr=None, score_type=None, inclusive=None + ): + pass + + def __str__(self): + pass + + @staticmethod + def _func(self, target, observer, time): + pass + + @staticmethod + def _lsq_func(self, value): + pass + + @property + def min_snr(self): + pass + + @property + def max_snr(self): + pass + + @property + def score_type(self): + pass + + @property + def inclusive(self): + pass diff --git a/pyscope/telrun/sun_condition.py b/pyscope/telrun/sun_condition.py index f70deb3f..565d30e7 100644 --- a/pyscope/telrun/sun_condition.py +++ b/pyscope/telrun/sun_condition.py @@ -8,30 +8,13 @@ class SunCondition(BoundaryCondition): - def __init__( - self, - min_sep=0 * u.deg, - max_sep=180 * u.deg, - min_alt=-90 * u.deg, - max_alt=90 * u.deg, - ): + def __init__(self, **kwargs): """ """ pass - def from_string(self, string): + @classmethod + def from_string(cls, string, **kwargs): pass def __str__(self): pass - - def __repr__(self): - logger.debug("SunCondition().__repr__()") - return str(self) - - def __call__(self, time=None, location=None, target=None): - """ """ - pass - - def plot(self, time, location, target=None, ax=None): - """ """ - pass diff --git a/pyscope/telrun/time_condition.py b/pyscope/telrun/time_condition.py index 77a51e5a..0a7f6e48 100644 --- a/pyscope/telrun/time_condition.py +++ b/pyscope/telrun/time_condition.py @@ -6,4 +6,233 @@ class TimeCondition(BoundaryCondition): - pass + def __init__( + self, + time_type="local", # local, utc, specified tz/location, or lst at location + min_time=None, + max_time=None, + score_type="boolean", + inclusive=True, + ref_time=None, + ): + """ + A restriction on the time of an observation. The time can be specified + several ways: local, UTC, a specified timezone, or LST at a location. + + Parameters + ---------- + time_type : `str`, default : "local" + The type of time to use. Can be "local", "utc", a timezone + string or an `~astropy.coordinates.EarthLocation` for a timezone lookup, or "lst". + + min_time : `~astropy.time.Time`, default : `None` + The minimum time for the observation. + + max_time : `~astropy.time.Time`, default : `None` + The maximum time for the observation. + + score_type : `str`, default : "boolean" + The type of score to return. Can be "boolean" or "linear". If "boolean", + the score is `True` if the target meets the condition and `False` if it does not. + If "linear", the score is a `float` value between 0 and 1. + + inclusive : `bool`, default : `True` + Whether the min and max values are inclusive. + + ref_time : `~astropy.time.Time`, default : `None` + The reference time to use for the condition. If specified, the `min_time` and + `max_time` are relative to this time. + + """ + logger.debug( + "TimeCondition.__init__(time_type=%s, min_time=%s, max_time=%s, score_type=%s, inclusive=%s, ref_time=%s)" + % (time_type, min_time, max_time, score_type, inclusive, ref_time) + ) + + @classmethod + def from_string( + cls, + string, + time_type=None, + min_time=None, + max_time=None, + score_type=None, + inclusive=None, + ref_time=None, + ): + """ + Create a `~pyscope.telrun.TimeCondition` or a `list` of `~pyscope.telrun.TimeCondition` objects + from a `str` representation of a `~pyscope.telrun.TimeCondition`. All optional parameters are + used to override the parameters extracted from the `str` representation. + + Parameters + ---------- + string : `str`, required + + time_type : `str`, default : `None` + + min_time : `~astropy.time.Time`, default : `None` + + max_time : `~astropy.time.Time`, default : `None` + + score_type : `str`, default : `None` + + inclusive : `bool`, default : `None` + + ref_time : `~astropy.time.Time`, default : `None` + + """ + logger.debug( + "TimeCondition.from_string(string=%s, time_type=%s, min_time=%s, max_time=%s, score_type=%s, inclusive=%s, ref_time=%s)" + % (string, time_type, min_time, max_time, score_type, inclusive, ref_time) + ) + + def __str__(self): + """ + Return a `str` representation of the `~pyscope.telrun.TimeCondition`. + + Returns + ------- + `str` + A `str` representation of the `~pyscope.telrun.TimeCondition`. + + """ + logger.debug("TimeCondition().__str__()") + + @staticmethod + def _func(self, target, time, location): + """ + Check if the target meets the time condition. + + Parameters + ---------- + target : `~astropy.coordinates.SkyCoord`, required + The target to check. + + time : `~astropy.time.Time`, required + The time of the observation. + + location : `~astropy.coordinates.EarthLocation`, required + The location of the observer. + + Returns + ------- + `float` or `bool` + The score for the time condition. If `score_type` is "boolean", the score is `True` if the target + meets the condition and `False` if it does not. If `score_type` is "linear", the score is a `float` + value between 0 and 1. + + """ + logger.debug( + "TimeCondition()._func(target=%s, time=%s, location=%s)" + % (target, time, location) + ) + + @staticmethod + def _lqs_func(self, value): + """ + Calculate the linear quality score for the score value. + + Parameters + ---------- + value : `float`, required + The score value for the target. + + Returns + ------- + `float` + The linear quality score for the score value. + + """ + logger.debug("TimeCondition()._lqs_func(value=%s)" % value) + + @property + def min_time(self): + """ + The minimum time for the observation. + + Returns + ------- + `~astropy.time.Time` + The minimum time for the observation. + + """ + logger.debug("TimeCondition().min_time == %s" % self._min_time) + return self._min_time + + @property + def max_time(self): + """ + The maximum time for the observation. + + Returns + ------- + `~astropy.time.Time` + The maximum time for the observation. + + """ + logger.debug("TimeCondition().max_time == %s" % self._max_time) + return self._max_time + + @property + def time_type(self): + """ + The type of time to use. Can be "local", "utc", a timezone string or an + `~astropy.coordinates.EarthLocation` for a timezone lookup, or "lst". + + Returns + ------- + `str` + The type of time to use. + + """ + logger.debug("TimeCondition().time_type == %s" % self._time_type) + return self._time_type + + @property + def score_type(self): + """ + The type of score to return. Can be "boolean" or "linear". If "boolean", + the score is `True` if the target meets the condition and `False` if it does not. + If "linear", the score is a `float` value between 0 and 1. + + Returns + ------- + `str` + The type of score to return. + + """ + logger.debug("TimeCondition().score_type == %s" % self._score_type) + return self._score_type + + @property + def inclusive(self): + """ + Whether the min and max values are inclusive. Operates as a "not" on the + condition. If `True`, the min and max values are inclusive. If `False`, the + min and max values are exclusive. + + Returns + ------- + `bool` + Whether the min and max values are inclusive. + + """ + logger.debug("TimeCondition().inclusive == %s" % self._inclusive) + return self._inclusive + + @property + def ref_time(self): + """ + The reference `~astropy.time.Time` to use for the condition. If specified, the `min_time` and + `max_time` are relative to this time. A timezone specified in the `ref_time` + `~astropy.time.Time` object will override the `time_type` parameter. + + Returns + ------- + `~astropy.time.Time` + The reference time to use for the condition. + + """ + logger.debug("TimeCondition().ref_time == %s" % self._ref_time) + return self._ref_time From aae198f5b962dada4441970c0b132eeeaed3c278 Mon Sep 17 00:00:00 2001 From: ccolin Date: Tue, 3 Dec 2024 17:34:48 -0600 Subject: [PATCH 47/60] sphinx bulletpoint format fix --- pyscope/observatory/ascom_telescope.py | 90 +++++++++++++------------- pyscope/observatory/telescope.py | 90 +++++++++++++------------- 2 files changed, 90 insertions(+), 90 deletions(-) diff --git a/pyscope/observatory/ascom_telescope.py b/pyscope/observatory/ascom_telescope.py index 68dcbdbc..7ff1b5ae 100644 --- a/pyscope/observatory/ascom_telescope.py +++ b/pyscope/observatory/ascom_telescope.py @@ -48,10 +48,10 @@ def AxisRates(self, Axis): Parameters ---------- Axis : `TelescopeAxes `_ - The axis about which the telescope may be moved. See below for ASCOM standard. - * 0 : Primary axis, Right Ascension or Azimuth. - * 1 : Secondary axis, Declination or Altitude. - * 2 : Tertiary axis, imager rotators. + The axis about which the telescope may be moved. See below for ASCOM standard: + * 0 : Primary axis, Right Ascension or Azimuth. + * 1 : Secondary axis, Declination or Altitude. + * 2 : Tertiary axis, imager rotators. Returns ------- @@ -80,10 +80,10 @@ def CanMoveAxis(self, Axis): Parameters ---------- Axis : `TelescopeAxes `_ - The axis about which the telescope may be moved. See below for ASCOM standard. - * 0 : Primary axis, Right Ascension or Azimuth. - * 1 : Secondary axis, Declination or Altitude. - * 2 : Tertiary axis, imager rotators. + The axis about which the telescope may be moved. See below for ASCOM standard: + * 0 : Primary axis, Right Ascension or Azimuth. + * 1 : Secondary axis, Declination or Altitude. + * 2 : Tertiary axis, imager rotators. """ logger.debug(f"ASCOMTelescope.CanMoveAxis({Axis}) called") return self._device.CanMoveAxis(Axis) @@ -102,10 +102,10 @@ def DestinationSideOfPier(self, RightAscension, Declination): Returns ------- `PierSide `_ - The side of the pier on which the telescope will be after slewing to the specified coordinates. See below for ASCOM standard. - * 0 : Normal pointing state, mount on East side of pier, looking West. - * 1 : Through the pole pointing state, mount on West side of pier, looking East. - * -1 : Unknown or indeterminate. + The side of the pier on which the telescope will be after slewing to the specified coordinates. See below for ASCOM standard: + * 0 : Normal pointing state, mount on East side of pier, looking West. + * 1 : Through the pole pointing state, mount on West side of pier, looking East. + * -1 : Unknown or indeterminate. """ logger.debug( f"ASCOMTelescope.DestinationSideOfPier({RightAscension}, {Declination}) called" @@ -129,10 +129,10 @@ def MoveAxis(self, Axis, Rate): Parameters ---------- Axis : `TelescopeAxes `_ - The axis about which the telescope may be moved. See below for ASCOM standard. - * 0 : Primary axis, Right Ascension or Azimuth. - * 1 : Secondary axis, Declination or Altitude. - * 2 : Tertiary axis, imager rotators. + The axis about which the telescope may be moved. See below for ASCOM standard: + * 0 : Primary axis, Right Ascension or Azimuth. + * 1 : Secondary axis, Declination or Altitude. + * 2 : Tertiary axis, imager rotators. Rate : `float` Rate of motion in degrees per second. Positive values indicate motion in one direction, negative values in the opposite direction, and 0.0 stops motion by this method and resumes tracking motion. @@ -176,11 +176,11 @@ def PulseGuide(self, Direction, Duration): Parameters ---------- Direction : `GuideDirections `_ - Direction in which to move the telescope. See below for ASCOM standard. - * 0 : North or up. - * 1 : South or down. - * 2 : East or right. - * 3 : West or left. + Direction in which to move the telescope. See below for ASCOM standard: + * 0 : North or up. + * 1 : South or down. + * 2 : East or right. + * 3 : West or left. Duration : `int` Time in milliseconds for which to pulse the guide. Must be a positive non-zero value. """ @@ -254,10 +254,10 @@ def AlignmentMode(self): """ The alignment mode of the telescope. (`AlignmentModes `_) - See below for ASCOM standard. - * 0 : Altitude-Azimuth alignment. - * 1 : Polar (equatorial) mount alignment, NOT German. - * 2 : German equatorial mount alignment. + See below for ASCOM standard: + * 0 : Altitude-Azimuth alignment. + * 1 : Polar (equatorial) mount alignment, NOT German. + * 2 : German equatorial mount alignment. """ logger.debug("ASCOMTelescope.AlignmentMode property accessed") return self._device.AlignmentMode @@ -409,12 +409,12 @@ def EquatorialSystem(self): """ Equatorial coordinate system used by the telescope. (`EquatorialCoordinateType `_) - See below for ASCOM standard. - * 0 : Custom/unknown equinox and/or reference frame. - * 1 : Topocentric coordinates. Coordinates at the current date, allowing for annual aberration and precession-nutation. Most common for amateur telescopes. - * 2 : J2000 equator and equinox. Coordinates at the J2000 epoch, ICRS reference frame. Most common for professional telescopes. - * 3 : J2050 equator and equinox. Coordinates at the J2050 epoch, ICRS reference frame. - * 4 : B1950 equinox. Coordinates at the B1950 epoch, FK4 reference frame. + See below for ASCOM standard: + * 0 : Custom/unknown equinox and/or reference frame. + * 1 : Topocentric coordinates. Coordinates at the current date, allowing for annual aberration and precession-nutation. Most common for amateur telescopes. + * 2 : J2000 equator and equinox. Coordinates at the J2000 epoch, ICRS reference frame. Most common for professional telescopes. + * 3 : J2050 equator and equinox. Coordinates at the J2050 epoch, ICRS reference frame. + * 4 : B1950 equinox. Coordinates at the B1950 epoch, FK4 reference frame. """ logger.debug("ASCOMTelescope.EquatorialSystem property accessed") return self._device.EquatorialSystem @@ -469,10 +469,10 @@ def SideOfPier(self): """ The pointing state of the mount. (`PierSide `_) - See below for ASCOM standard. - * 0 : Normal pointing state, mount on East side of pier, looking West. - * 1 : Through the pole pointing state, mount on West side of pier, looking East. - * -1 : Unknown or indeterminate. + See below for ASCOM standard: + * 0 : Normal pointing state, mount on East side of pier, looking West. + * 1 : Through the pole pointing state, mount on West side of pier, looking East. + * -1 : Unknown or indeterminate. .. warning:: The name of this property is misleading and does not reflect the true meaning of the property. @@ -578,11 +578,11 @@ def TrackingRate(self): and report a default rate on connection. In this circumstance, a default of `Sidereal` rate is preferred. - See below for ASCOM standard. - * 0 : Sidereal tracking, 15.041 arcseconds per second. - * 1 : Lunar tracking, 14.685 arcseconds per second. - * 2 : Solar tracking, 15.0 arcseconds per second. - * 3 : King tracking, 15.0369 arcseconds per second. + See below for ASCOM standard: + * 0 : Sidereal tracking, 15.041 arcseconds per second. + * 1 : Lunar tracking, 14.685 arcseconds per second. + * 2 : Solar tracking, 15.0 arcseconds per second. + * 3 : King tracking, 15.0369 arcseconds per second. """ logger.debug("ASCOMTelescope.TrackingRate property accessed") return self._device.TrackingRate @@ -602,11 +602,11 @@ def TrackingRates(self): properties for the number of rates, and the actual rates, and methods for returning an enumerator for the rates, and for disposing of the object as a whole. - See below for ASCOM standard expected collection. - * 0 : Sidereal tracking, 15.041 arcseconds per second. - * 1 : Lunar tracking, 14.685 arcseconds per second. - * 2 : Solar tracking, 15.0 arcseconds per second. - * 3 : King tracking, 15.0369 arcseconds per second + See below for ASCOM standard expected collection: + * 0 : Sidereal tracking, 15.041 arcseconds per second. + * 1 : Lunar tracking, 14.685 arcseconds per second. + * 2 : Solar tracking, 15.0 arcseconds per second. + * 3 : King tracking, 15.0369 arcseconds per second """ logger.debug("ASCOMTelescope.TrackingRates property accessed") return self._device.TrackingRates diff --git a/pyscope/observatory/telescope.py b/pyscope/observatory/telescope.py index 3b0147ce..33e57673 100644 --- a/pyscope/observatory/telescope.py +++ b/pyscope/observatory/telescope.py @@ -43,10 +43,10 @@ def AxisRates(self, Axis): Parameters ---------- Axis : `enum` - The axis about which the telescope may be moved. See below for example values. - * 0 : Primary axis, usually corresponding to Right Ascension or Azimuth. - * 1 : Secondary axis, usually corresponding to Declination or Altitude. - * 2 : Tertiary axis, usually corresponding to imager rotators. + The axis about which the telescope may be moved. See below for example values: + * 0 : Primary axis, usually corresponding to Right Ascension or Azimuth. + * 1 : Secondary axis, usually corresponding to Declination or Altitude. + * 2 : Tertiary axis, usually corresponding to imager rotators. Returns ------- @@ -74,10 +74,10 @@ def CanMoveAxis(self, Axis): Parameters ---------- Axis : `enum` - The axis about which the telescope may be moved. See below for example values. - * 0 : Primary axis, usually corresponding to Right Ascension or Azimuth. - * 1 : Secondary axis, usually corresponding to Declination or Altitude. - * 2 : Tertiary axis, usually corresponding to imager rotators. + The axis about which the telescope may be moved. See below for example values: + * 0 : Primary axis, usually corresponding to Right Ascension or Azimuth. + * 1 : Secondary axis, usually corresponding to Declination or Altitude. + * 2 : Tertiary axis, usually corresponding to imager rotators. """ pass @@ -96,10 +96,10 @@ def DestinationSideOfPier(self, RightAscension, Declination): Returns ------- `enum` - The side of the pier on which the telescope will be after slewing to the specified equatorial coordinates at the current instant of time. See below for example values. - * 0 : Normal pointing state, mount on East side of pier, looking West. - * 1 : Through the pole pointing state, mount on West side of pier, looking East. - * -1 : Unknown or indeterminate. + The side of the pier on which the telescope will be after slewing to the specified equatorial coordinates at the current instant of time. See below for example values: + * 0 : Normal pointing state, mount on East side of pier, looking West. + * 1 : Through the pole pointing state, mount on West side of pier, looking East. + * -1 : Unknown or indeterminate. """ pass @@ -127,10 +127,10 @@ def MoveAxis(self, Axis, Rate): Parameters ---------- Axis : `enum` - The axis about which the telescope may be moved. See below for example values. - * 0 : Primary axis, usually corresponding to Right Ascension or Azimuth. - * 1 : Secondary axis, usually corresponding to Declination or Altitude. - * 2 : Tertiary axis, usually corresponding to imager rotators. + The axis about which the telescope may be moved. See below for example values: + * 0 : Primary axis, usually corresponding to Right Ascension or Azimuth. + * 1 : Secondary axis, usually corresponding to Declination or Altitude. + * 2 : Tertiary axis, usually corresponding to imager rotators. Rate : `float` Rate of motion in degrees per second. Positive values indicate motion in one direction, negative values in the opposite direction, and 0.0 stops motion by this method and resumes tracking motion. @@ -168,11 +168,11 @@ def PulseGuide(self, Direction, Duration): Parameters ---------- Direction : `enum` - Direction in which to move the telescope. See below for example values. - * 0 : North or up. - * 1 : South or down. - * 2 : East or right. - * 3 : West or left. + Direction in which to move the telescope. See below for example values: + * 0 : North or up. + * 1 : South or down. + * 2 : East or right. + * 3 : West or left. Duration : `int` Time in milliseconds for which to pulse the guide. Must be a positive non-zero value. """ @@ -331,10 +331,10 @@ def AlignmentMode(self): """ The alignment mode of the telescope. (`enum`) - See below for example values. - * 0 : Altitude-Azimuth alignment. - * 1 : Polar (equatorial) mount alignment, NOT German. - * 2 : German equatorial mount alignment. + See below for example values: + * 0 : Altitude-Azimuth alignment. + * 1 : Polar (equatorial) mount alignment, NOT German. + * 2 : German equatorial mount alignment. """ pass @@ -556,12 +556,12 @@ def EquatorialSystem(self): """ Equatorial coordinate system used by the telescope. (`enum`) - See below for example values. - * 0 : Custom/unknown equinox and/or reference frame. - * 1 : Topocentric coordinates. Coordinates at the current date, allowing for annual aberration and precession-nutation. Most common for amateur telescopes. - * 2 : J2000 equator and equinox. Coordinates at the J2000 epoch, ICRS reference frame. Most common for professional telescopes. - * 3 : J2050 equator and equinox. Coordinates at the J2050 epoch, ICRS reference frame. - * 4 : B1950 equinox. Coordinates at the B1950 epoch, FK4 reference frame. + See below for example values: + * 0 : Custom/unknown equinox and/or reference frame. + * 1 : Topocentric coordinates. Coordinates at the current date, allowing for annual aberration and precession-nutation. Most common for amateur telescopes. + * 2 : J2000 equator and equinox. Coordinates at the J2000 epoch, ICRS reference frame. Most common for professional telescopes. + * 3 : J2050 equator and equinox. Coordinates at the J2050 epoch, ICRS reference frame. + * 4 : B1950 equinox. Coordinates at the B1950 epoch, FK4 reference frame. """ pass @@ -656,10 +656,10 @@ def SideOfPier(self): """ The pointing state of the mount. (`enum`) - See below for example values. - * 0 : Normal pointing state, mount on East side of pier, looking West. - * 1 : Through the pole pointing state, mount on West side of pier, looking East. - * -1 : Unknown or indeterminate. + See below for example values: + * 0 : Normal pointing state, mount on East side of pier, looking West. + * 1 : Through the pole pointing state, mount on West side of pier, looking East. + * -1 : Unknown or indeterminate. .. warning:: The name of this property is misleading and does not reflect the true meaning of the property. @@ -793,11 +793,11 @@ def TrackingRate(self): and report a default rate on connection. In this circumstance, a default of `Sidereal` rate is preferred. - See below for example values. - * 0 : Sidereal tracking, 15.041 arcseconds per second. - * 1 : Lunar tracking, 14.685 arcseconds per second. - * 2 : Solar tracking, 15.0 arcseconds per second. - * 3 : King tracking, 15.0369 arcseconds per second. + See below for example values: + * 0 : Sidereal tracking, 15.041 arcseconds per second. + * 1 : Lunar tracking, 14.685 arcseconds per second. + * 2 : Solar tracking, 15.0 arcseconds per second. + * 3 : King tracking, 15.0369 arcseconds per second. """ pass @@ -817,11 +817,11 @@ def TrackingRates(self): include properties for the number of rates, and the actual rates, and methods for returning an enumerator for the rates, and for disposing of the object as a whole. - See below for an example collection. - * 0 : Sidereal tracking, 15.041 arcseconds per second. - * 1 : Lunar tracking, 14.685 arcseconds per second. - * 2 : Solar tracking, 15.0 arcseconds per second. - * 3 : King tracking, 15.0369 arcseconds per second. + See below for an example collection: + * 0 : Sidereal tracking, 15.041 arcseconds per second. + * 1 : Lunar tracking, 14.685 arcseconds per second. + * 2 : Solar tracking, 15.0 arcseconds per second. + * 3 : King tracking, 15.0369 arcseconds per second. """ pass From 94ca83ce5c1c33cccbcb7ef233de42159d7fd9d7 Mon Sep 17 00:00:00 2001 From: ccolin Date: Tue, 3 Dec 2024 18:15:14 -0600 Subject: [PATCH 48/60] missing docs cleanup, currently only for classes visible to readthedocs --- pyscope/observatory/ascom_camera.py | 1 + pyscope/observatory/ascom_telescope.py | 10 ++ .../observatory/collect_calibration_set.py | 70 ++++++------ .../observatory/html_observing_conditions.py | 101 ++++++++++++++++++ pyscope/observatory/ip_cover_calibrator.py | 15 +++ pyscope/observatory/maxim.py | 8 ++ pyscope/observatory/pwi4_focuser.py | 21 ++++ 7 files changed, 191 insertions(+), 35 deletions(-) diff --git a/pyscope/observatory/ascom_camera.py b/pyscope/observatory/ascom_camera.py index 97cc61dc..5399688f 100644 --- a/pyscope/observatory/ascom_camera.py +++ b/pyscope/observatory/ascom_camera.py @@ -398,6 +398,7 @@ def LastExposureStartTime(self): @property def LastInputExposureDuration(self): + """The duration of the last exposure in seconds. (`float`)""" logger.debug(f"ASCOMCamera.LastInputExposureDuration property called") return self._last_exposure_duration diff --git a/pyscope/observatory/ascom_telescope.py b/pyscope/observatory/ascom_telescope.py index 7ff1b5ae..be65fa34 100644 --- a/pyscope/observatory/ascom_telescope.py +++ b/pyscope/observatory/ascom_telescope.py @@ -193,6 +193,8 @@ def SetPark(self): def SlewToAltAz(self, Azimuth, Altitude): # pragma: no cover """ + Deprecated + .. deprecated:: 0.1.1 ASCOM is deprecating this method. """ @@ -205,6 +207,8 @@ def SlewToAltAzAsync(self, Azimuth, Altitude): def SlewToCoordinates(self, RightAscension, Declination): # pragma: no cover """ + Deprecated + .. deprecated:: 0.1.1 ASCOM is deprecating this method. """ @@ -221,6 +225,8 @@ def SlewToCoordinatesAsync(self, RightAscension, Declination): def SlewToTarget(self): # pragma: no cover """ + Deprecated + .. deprecated:: 0.1.1 ASCOM is deprecating this method. """ @@ -339,6 +345,8 @@ def CanSetTracking(self): @property def CanSlew(self): # pragma: no cover """ + Deprecated + .. deprecated:: 0.1.1 ASCOM is deprecating this property. """ @@ -348,6 +356,8 @@ def CanSlew(self): # pragma: no cover @property def CanSlewAltAz(self): # pragma: no cover """ + Deprecated + .. deprecated:: 0.1.1 ASCOM is deprecating this property. """ diff --git a/pyscope/observatory/collect_calibration_set.py b/pyscope/observatory/collect_calibration_set.py index 9d9727bf..6b8df852 100644 --- a/pyscope/observatory/collect_calibration_set.py +++ b/pyscope/observatory/collect_calibration_set.py @@ -119,47 +119,47 @@ def collect_calibration_set_cli( verbose=0, ): """ - Collects a calibration set for the observatory.\b + Collects a calibration set for the observatory. .. warning:: The filter_exposures and filter_brightnesses must be of equal length. Parameters ---------- - observatory : `str` - - camera : `str`, default="ccd" - - readouts : `list`, default=[`None`] - - binnings : `list`, default=[`None`] - - repeat : `int`, default=1 - - dark_exposures : `list`, default=[] - - filters : `list`, default=[] - - filter_exposures : `list`, default=[] - - filter_brightness : `list`, default=`None` - - home_telescope : `bool`, default=`False` - - target_counts : `int`, default=`None` - - check_cooler : `bool`, default=`True` - - tracking : `bool`, default=`True` - - dither_radius : `float`, default=0 - - save_path : `str`, default="./temp/" - - new_dir : `bool`, default=`True` - - verbose : `int`, default=0 - + observatory : `str` or :py:class:`pyscope.observatory.Observatory` + The name of the observatory, or the observatory object itself, to connect to. + camera : `str`, default : "ccd" + The type of camera to collect calibration data for, such as ccd or cmos. + readouts : `list`, default : [`None`] + The indices of readout modes to iterate through. + binnings : `list`, default : [`None`] + The binnings to iterate through. + repeat : `int`, default : 1 + The number of times to repeat each exposure. + dark_exposures : `list`, default : [] + The dark exposure times. + filters : `list`, default : [] + The filters to collect flats for. + filter_exposures : `list`, default : [] + The flat exposure times for each filter. + filter_brightness : `list`, default : `None` + The intensity of the calibrator [if present] for each filter. + home_telescope : `bool`, default : `False` + Whether to return the telescope to its home position after taking flats. + target_counts : `int`, default : `None` + The target counts for the flats. + check_cooler : `bool`, default : `True` + Whether to check if the cooler is on before taking flats. + tracking : `bool`, default : `True` + Whether to track the telescope while taking flats. + dither_radius : `float`, default : 0 + The radius to dither the telescope by while taking flats. + save_path : `str`, default : "./temp/" + The path to save the calibration set. + new_dir : `bool`, default : `True` + Whether to create a new directory for the calibration set. + verbose : `int`, default : 0 + The verbosity of the output. """ if type(observatory) == str: diff --git a/pyscope/observatory/html_observing_conditions.py b/pyscope/observatory/html_observing_conditions.py index 80419ab0..4bda46a0 100755 --- a/pyscope/observatory/html_observing_conditions.py +++ b/pyscope/observatory/html_observing_conditions.py @@ -54,6 +54,101 @@ def __init__( last_updated_units="", last_updated_numeric=True, ): + """ + This class provides an interface to gathering observing condition data via HTML. + + The class is designed to be used with an HTML page that contains observing conditions data, + sensor descriptions, and time since last update. + + Parameters + ---------- + url : `str` + The URL of the HTML page that contains the observing conditions data. + cloud_cover_keyword : `str`, default : "CLOUDCOVER", optional + The keyword that identifies the cloud cover data. + cloud_cover_units : `str`, default : "%", optional + The units of the cloud cover data. + cloud_cover_numeric : `bool`, default : `True`, optional + Whether the cloud cover data is numeric. + dew_point_keyword : `str`, default : "DEWPOINT", optional + The keyword that identifies the dew point data. + dew_point_units : `str`, default : "F", optional + The units of the dew point data. + dew_point_numeric : `bool`, default : `True`, optional + Whether the dew point data is numeric. + humidity_keyword : `str`, default : "HUMIDITY", optional + The keyword that identifies the humidity data. + humidity_units : `str`, default : "%", optional + The units of the humidity data. + humidity_numeric : `bool`, default : `True`, optional + Whether the humidity data is numeric. + pressure_keyword : `str`, default : "PRESSURE", optional + The keyword that identifies the pressure data. + pressure_units : `str`, default : "inHg", optional + The units of the pressure data. + pressure_numeric : `bool`, default : `True`, optional + Whether the pressure data is numeric. + rain_rate_keyword : `str`, default : "RAINRATE", optional + The keyword that identifies the rain rate data. + rain_rate_units : `str`, default : "inhr", optional + The units of the rain rate data. + rain_rate_numeric : `bool`, default : `True`, optional + Whether the rain rate data is numeric. + sky_brightness_keyword : `str`, default : "SKYBRIGHTNESS", optional + The keyword that identifies the sky brightness data. + sky_brightness_units : `str`, default : "magdeg2", optional + The units of the sky brightness data. + sky_brightness_numeric : `bool`, default : `True`, optional + Whether the sky brightness data is numeric. + sky_quality_keyword : `str`, default : "SKYQUALITY", optional + The keyword that identifies the sky quality data. + sky_quality_units : `str`, default : "", optional + The units of the sky quality data. + sky_quality_numeric : `bool`, default : `True`, optional + Whether the sky quality data is numeric. + sky_temperature_keyword : `str`, default : "SKYTEMPERATURE", optional + The keyword that identifies the sky temperature data. + sky_temperature_units : `str`, default : "F", optional + The units of the sky temperature data. + sky_temperature_numeric : `bool`, default : `True`, optional + Whether the sky temperature data is numeric. + star_fwhm_keyword : `str`, default : "STARFWHM", optional + The keyword that identifies the star FWHM data. + star_fwhm_units : `str`, default : "arcsec", optional + The units of the star FWHM data. + star_fwhm_numeric : `bool`, default : `True`, optional + Whether the star FWHM data is numeric. + temperature_keyword : `str`, default : "TEMPERATURE", optional + The keyword that identifies the temperature data. + temperature_units : `str`, default : "F", optional + The units of the temperature data. + temperature_numeric : `bool`, default : `True`, optional + Whether the temperature data is numeric. + wind_direction_keyword : `str`, default : "WINDDIRECTION", optional + The keyword that identifies the wind direction data. + wind_direction_units : `str`, default : "EofN", optional + The units of the wind direction data. + wind_direction_numeric : `bool`, default : `True`, optional + Whether the wind direction data is numeric. + wind_gust_keyword : `str`, default : "WINDGUST", optional + The keyword that identifies the wind gust data. + wind_gust_units : `str`, default : "mph", optional + The units of the wind gust data. + wind_gust_numeric : `bool`, default : `True`, optional + Whether the wind gust data is numeric. + wind_speed_keyword : `str`, default : "WINDSPEED", optional + The keyword that identifies the wind speed data. + wind_speed_units : `str`, default : "mph", optional + The units of the wind speed data. + wind_speed_numeric : `bool`, default : `True`, optional + Whether the wind speed data is numeric. + last_updated_keyword : `str`, default : "LASTUPDATED", optional + The keyword that identifies the last updated data. + last_updated_units : `str`, default : "", optional + The units of the last updated data. + last_updated_numeric : `bool`, default : `True`, optional + Whether the last updated data is numeric. + """ logger.debug( f"""HTMLObservingConditions.__init__( {url}, @@ -320,16 +415,19 @@ def CloudCover(self): @property def Description(self): + """Description of the driver. (`str`)""" logger.debug("HTMLObservingConditions.Description property called") return "HTML Observing Conditions Driver" @property def DriverVersion(self): + """Version of the driver. (`str`)""" logger.debug("HTMLObservingConditions.DriverVersion property called") return None @property def DriverInfo(self): + """Provides information about the driver. (`str`)""" logger.debug("HTMLObservingConditions.DriverInfo property called") return "HTML Observing Conditions Driver" @@ -345,11 +443,13 @@ def Humidity(self): @property def InterfaceVersion(self): + """Version of the interface supported by the driver. (`int`)""" logger.debug("HTMLObservingConditions.InterfaceVersion property called") return 1 @property def Name(self): + """Name/url of the driver. (`str`)""" logger.debug("HTMLObservingConditions.Name property called") return self._url @@ -405,5 +505,6 @@ def WindSpeed(self): @property def LastUpdated(self): + """Time of last update of conditions. (`str`)""" logger.debug("HTMLObservingConditions.LastUpdated property called") return self._last_updated diff --git a/pyscope/observatory/ip_cover_calibrator.py b/pyscope/observatory/ip_cover_calibrator.py index 0cc1cd1a..12462a8b 100644 --- a/pyscope/observatory/ip_cover_calibrator.py +++ b/pyscope/observatory/ip_cover_calibrator.py @@ -11,6 +11,18 @@ class IPCoverCalibrator(CoverCalibrator): def __init__(self, tcp_ip, tcp_port, buffer_size): + """ + Implements the CoverCalibrator interface for a cover calibrator that is controlled via a TCP/IP connection. + + Parameters + ---------- + tcp_ip : `str` + The IP address of the cover calibrator. + tcp_port : `int` + The port number of the cover calibrator. + buffer_size : `int` + The size of the buffer to use when sending and receiving data. + """ self._tcp_ip = tcp_ip self._tcp_port = tcp_port self._buffer_size = buffer_size @@ -80,15 +92,18 @@ def _send_packet(self, intensity): @property def tcp_ip(self): + """The IP address of the cover calibrator. (`str`)""" logger.debug("IPCoverCalibrator.tcp_ip called") return self._tcp_ip @property def tcp_port(self): + """The port number of the cover calibrator. (`int`)""" logger.debug("IPCoverCalibrator.tcp_port called") return self._tcp_port @property def buffer_size(self): + """The size of the buffer to use when sending and receiving data. (`int`)""" logger.debug("IPCoverCalibrator.buffer_size called") return self._buffer_size diff --git a/pyscope/observatory/maxim.py b/pyscope/observatory/maxim.py index 77eb3479..778b2897 100755 --- a/pyscope/observatory/maxim.py +++ b/pyscope/observatory/maxim.py @@ -14,6 +14,11 @@ class Maxim(Device): def __init__(self): + """ + This class provides an interface to Maxim DL, and its camera, filter wheel, and autofocus routines. + + This class is only available on Windows. + """ logger.debug("Maxim.Maxim __init__ called") if platform.system() != "Windows": raise Exception("This class is only available on Windows.") @@ -50,16 +55,19 @@ def Name(self): @property def app(self): + """The Maxim DL application object. (`win32com.client.CDispatch`)""" logger.debug("Maxim.app called") return self._app @property def autofocus(self): + """The autofocus object. (`_MaximAutofocus`)""" logger.debug("Maxim.autofocus called") return self._autofocus @property def camera(self): + """The camera object. (`_MaximCamera`)""" logger.debug("Maxim.camera called") return self._camera diff --git a/pyscope/observatory/pwi4_focuser.py b/pyscope/observatory/pwi4_focuser.py index 3e0fd0f7..f502593f 100644 --- a/pyscope/observatory/pwi4_focuser.py +++ b/pyscope/observatory/pwi4_focuser.py @@ -9,6 +9,20 @@ class PWI4Focuser(Focuser): def __init__(self, host="localhost", port=8220): + """ + Focuser class for the PWI4 software platform. + + This class provides an interface to the PWI4 Focuser, and enables the user to access properties of + the focuser such as position, temperature, and whether the focuser is moving, and methods to move the + focuser, enable/disable the focuser, and check if the focuser is connected. + + Parameters + ---------- + host : `str`, default : "localhost", optional + The IP address of the host computer running the PWI4 software. + port : `int`, default : 8220, optional + The port number of the host computer running the PWI4 software. + """ self._host = host self._port = port self._app = _PWI4(host=self._host, port=self._port) @@ -46,18 +60,22 @@ def Absolute(self, value): @property def Description(self): + """A short description of the device. (`str`)""" return "PWI4 Focuser" @property def DriverInfo(self): + """A short description of the driver. (`str`)""" return "PWI4Driver" @property def DriverVersion(self): + """The version of the driver. (`str`)""" return "PWI4Driver" @property def InterfaceVersion(self): + """The version of the ASCOM interface. (`int`)""" return "PWI4Interface" @property @@ -75,6 +93,7 @@ def MaxStep(self): @property def Name(self): + """The name of the device. (`str`)""" return "PWI4 Focuser" @property @@ -108,6 +127,7 @@ def Temperature(self, value): @property def Connected(self): + """Whether the focuser is connected and enabled. (`bool`)""" logger.debug("PWI4Focuser.Connected() called") if self._app.status().focuser.exists: return ( @@ -119,6 +139,7 @@ def Connected(self): @property def Enabled(self): + """Whether the focuser is enabled. (`bool`)""" logger.debug("PWI4Focuser.Enabled() called") return self._app.status().focuser.is_enabled From 5f999542e8e4b8673b0a514d379fd2ee1373ad31 Mon Sep 17 00:00:00 2001 From: ccolin Date: Tue, 3 Dec 2024 21:54:19 -0600 Subject: [PATCH 49/60] pwi4 summary docs, rest left to actual devs --- pyscope/observatory/_pwi4.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pyscope/observatory/_pwi4.py b/pyscope/observatory/_pwi4.py index b31e6e1e..266f6a16 100644 --- a/pyscope/observatory/_pwi4.py +++ b/pyscope/observatory/_pwi4.py @@ -27,11 +27,17 @@ class _PWI4: - """ - Client to the PWI4 telescope control application. - """ - def __init__(self, host="localhost", port=8220): + """ + Client to the PWI4 telescope control application. + + Parameters + ---------- + host : `str`, default : "localhost", optional + The hostname or IP address of the computer running PWI4. + port : `int`, default : 8220, optional + The port number on which PWI4 is listening for HTTP requests. + """ self.host = host self.port = port self.comm = _PWI4HttpCommunicator(host, port) From 9deffb6d1bde31ffb5752e8d8f31a766ef7695e8 Mon Sep 17 00:00:00 2001 From: WWGolay Date: Sun, 15 Dec 2024 13:17:26 -0700 Subject: [PATCH 50/60] RLMT pyscope as of 12/15/24 --- pyscope/observatory/observatory.py | 67 ++++++++++++++++-------------- pyscope/telrun/schedtel.py | 64 ++++++++++++++++++---------- pyscope/telrun/telrun_operator.py | 17 ++++++++ 3 files changed, 95 insertions(+), 53 deletions(-) diff --git a/pyscope/observatory/observatory.py b/pyscope/observatory/observatory.py index 6e93310b..2e692e14 100644 --- a/pyscope/observatory/observatory.py +++ b/pyscope/observatory/observatory.py @@ -126,6 +126,9 @@ def __init__(self, config_path=None, **kwargs): self._maxim = None + if not os.path.exists(config_path): + raise ObservatoryException("Config file '%s' not found" % config_path) + if config_path is not None: logger.info( "Using this config file to initialize the observatory: %s" % config_path @@ -2258,18 +2261,18 @@ def take_flats( self.telescope.Tracking = False logger.info("Tracking off") - if self.cover_calibrator is not None: + '''if self.cover_calibrator is not None: if self.cover_calibrator.CoverState != "NotPresent": logger.info("Opening the cover calibrator") self.cover_calibrator.OpenCover() - logger.info("Cover open") + logger.info("Cover open")''' if gain is not None: logger.info("Setting the camera gain to %i" % gain) self.camera.Gain = gain logger.info("Camera gain set") - for filt, filt_exp in zip(filters, filter_exposures): + for filt, filt_exp, idx in zip(filters, filter_exposures, range(len(filters))): # skip filters with 0 exposure time if filt_exp == 0: @@ -2287,9 +2290,9 @@ def take_flats( if type(filter_brightness) is list: logger.info( "Setting the cover calibrator brightness to %i" - % filter_brightness[i] + % filter_brightness[idx] ) - self.cover_calibrator.CalibratorOn(filter_brightness[i]) + self.cover_calibrator.CalibratorOn(filter_brightness[idx]) logger.info("Cover calibrator on") else: logger.warning("Cover calibrator not available, assuming sky flats") @@ -2351,7 +2354,7 @@ def take_flats( iter_save_name = Path( ( str(iter_save_name) - + (f"_Bright{filter_brightness[i]}" + "_{j}.fts") + + (f"_Bright{filter_brightness[idx]}" + f"_{j}.fts") ) ) else: @@ -2878,32 +2881,32 @@ def cover_calibrator_info(self): "CALSTATE": (self.cover_calibrator.CalibratorState, "Calibrator state"), "COVSTATE": (self.cover_calibrator.CoverState, "Cover state"), "BRIGHT": (None, "Brightness of cover calibrator"), - "CCNAME": (self.cover_calibrator.Name, "Cover calibrator name"), - "COVCAL": (self.cover_calibrator.Name, "Cover calibrator name"), - "CCDRVER": ( - self.cover_calibrator.DriverVersion, - "Cover calibrator driver version", - ), - "CCDRV": ( - str(self.cover_calibrator.DriverInfo), - "Cover calibrator driver info", - ), - "CCINTF": ( - self.cover_calibrator.InterfaceVersion, - "Cover calibrator interface version", - ), - "CCDESC": ( - self.cover_calibrator.Description, - "Cover calibrator description", - ), - "MAXBRITE": ( - self.cover_calibrator.MaxBrightness, - "Cover calibrator maximum possible brightness", - ), - "CCSUPAC": ( - str(self.cover_calibrator.SupportedActions), - "Cover calibrator supported actions", - ), + # "CCNAME": (self.cover_calibrator.Name, "Cover calibrator name"), + # "COVCAL": (self.cover_calibrator.Name, "Cover calibrator name"), + # "CCDRVER": ( + # self.cover_calibrator.DriverVersion, + # "Cover calibrator driver version", + # ), + # "CCDRV": ( + # str(self.cover_calibrator.DriverInfo), + # "Cover calibrator driver info", + # ), + # "CCINTF": ( + # self.cover_calibrator.InterfaceVersion, + # "Cover calibrator interface version", + # ), + # "CCDESC": ( + # self.cover_calibrator.Description, + # "Cover calibrator description", + # ), + # "MAXBRITE": ( + # self.cover_calibrator.MaxBrightness, + # "Cover calibrator maximum possible brightness", + # ), + # "CCSUPAC": ( + # str(self.cover_calibrator.SupportedActions), + # "Cover calibrator supported actions", + # ), } try: info["BRIGHT"] = (self.cover_calibrator.Brightness, info["BRIGHT"][1]) diff --git a/pyscope/telrun/schedtel.py b/pyscope/telrun/schedtel.py index fb2676a5..65894208 100644 --- a/pyscope/telrun/schedtel.py +++ b/pyscope/telrun/schedtel.py @@ -1118,37 +1118,59 @@ def plot_schedule_sky_cli(schedule_table, observatory): "Observatory must be, a string, Observatory object, or astroplan.Observer object." ) return - - # Get list of targets (and their ra/dec) and start times and insert into - # a dictionary with the target as the key and a list of start times as the value - targets = {} - for block in schedule_table: - if block["name"] == "TransitionBlock" or block["name"] == "EmptyBlock": + + # Get unique targets in the schedule + target_times = {} + + for row in schedule_table: + if row["name"] == "TransitionBlock" or row["name"] == "EmptyBlock": continue + target_string = row["target"].to_string("hmsdms") + target_name = row["name"] + if target_string not in target_times: + target_times[target_string] = { + "name": target_name, + "times": [row["start_time"]], + } + else: + target_times[target_string]["times"].append(row["start_time"]) - if block["name"] not in targets: - targets[block["name"]] = [] - obs_time = astrotime.Time(np.float64(block["start_time"].jd), format="jd") - targets[block["name"]].append(obs_time) + # targets = [t.to_string("hmsdms") for t in schedule_table["target"]] fig, ax = plt.subplots(1, 1, figsize=(7, 7), subplot_kw={"projection": "polar"}) - # Plot new axes for each target only, not each start time - for target in targets: + for target, target_dict in target_times.items(): + times = target_dict["times"] + try: + label = target_dict["name"].strip() + except: + label = target + target = coord.SkyCoord(target, unit=(u.hourangle, u.deg)) ax = astroplan_plots.plot_sky( - astroplan.FixedTarget(coord.SkyCoord.from_name(target)), + astroplan.FixedTarget(target), observatory, - targets[target], + times, ax=ax, - style_kwargs={ - "label": target, - }, + style_kwargs={"label": label}, ) handles, labels = ax.get_legend_handles_labels() - unique = [ - (h, l) for i, (h, l) in enumerate(zip(handles, labels)) if l not in labels[:i] - ] - ax.legend(*zip(*unique), loc=(1.1, 0)) + # unique = [ + # (h, l) for i, (h, l) in enumerate(zip(handles, labels)) if l not in labels[:i] + # ] + # ax.legend(*zip(*unique), loc=(1.1, 0)) + + + + # Add title to plot based on date + t0 = np.min(schedule_table["start_time"]) # -1 corrects for UTC to local time, should be cleaned up + t0 = astrotime.Time(t0, format="mjd") + t0.format = "iso" + + ax.set_title(f"Observing Schedule: Night of {t0.to_string().split(' ')[0]} UTC", + fontsize=14 + ) + + ax.legend(labels, loc=(1.1, 0)) fig.set_facecolor("white") fig.set_dpi(300) diff --git a/pyscope/telrun/telrun_operator.py b/pyscope/telrun/telrun_operator.py index cc5bca22..277c472f 100644 --- a/pyscope/telrun/telrun_operator.py +++ b/pyscope/telrun/telrun_operator.py @@ -1446,6 +1446,23 @@ def execute_block(self, *args, slew=True, **kwargs): logger.info("Setting camera readout mode to %s" % self.default_readout) self.observatory.camera.ReadoutMode = self.default_readout + # TODO: Make this better - temporary fix 2024-11-15 + # Check if focuser is outside of self.autofocus_midpoint +/- 1000 + if ( + self.observatory.focuser.Position + < self.autofocus_midpoint - 1000 + or self.observatory.focuser.Position + > self.autofocus_midpoint + 1000 + ): + logger.info( + "Focuser position is outside of autofocus_midpoint +/- 1000, moving to autofocus_midpoint..." + ) + self._focuser_status = "Moving" + self.observatory.focuser.Move(self.autofocus_midpoint) + while self.observatory.focuser.IsMoving: + time.sleep(0.1) + self._focuser_status = "Idle" + logger.info("Starting autofocus, ensuring tracking is on...") self.observatory.telescope.Tracking = True t = threading.Thread( From acdc882cc2718c6b8243d6250a214e302ba83945 Mon Sep 17 00:00:00 2001 From: WWGolay Date: Mon, 16 Dec 2024 02:06:00 -0700 Subject: [PATCH 51/60] Add ZWO Camera native driver --- pyscope/observatory/__init__.py | 3 +- pyscope/observatory/observatory.py | 119 ++++-- pyscope/observatory/zwo_camera.py | 598 +++++++++++++++++++++++++++++ 3 files changed, 681 insertions(+), 39 deletions(-) create mode 100644 pyscope/observatory/zwo_camera.py diff --git a/pyscope/observatory/__init__.py b/pyscope/observatory/__init__.py index b35554a1..fe8c4a96 100644 --- a/pyscope/observatory/__init__.py +++ b/pyscope/observatory/__init__.py @@ -45,9 +45,10 @@ from .observatory_exception import ObservatoryException from .observatory import Observatory - from .pwi_autofocus import PWIAutofocus +from .zwo_camera import ZWOCamera + # from .skyx import SkyX from .collect_calibration_set import collect_calibration_set diff --git a/pyscope/observatory/observatory.py b/pyscope/observatory/observatory.py index 2e692e14..07b4fb19 100644 --- a/pyscope/observatory/observatory.py +++ b/pyscope/observatory/observatory.py @@ -1,5 +1,6 @@ import configparser import datetime +import glob import importlib import json import logging @@ -1307,17 +1308,20 @@ def save_last_image( filepath = os.path.join(os.getcwd(), filename) self.camera.SaveImageAsFits(filepath) img_array = fits.getdata(filename) + try: + hdr = self.generate_header_info( + filename, frametyp, custom_header, history, maxim, allowed_overwrite + ) + # update RADECSYS key to RADECSYSa + if "RADECSYS" in hdr: + hdr["RADECSYSa"] = hdr["RADECSYS"] + hdr.pop("RADECSYS", None) + hdu = fits.PrimaryHDU(img_array, header=hdr) + except Exception as e: + logger.exception(f"Error generating header information: {e}") + hdu = fits.PrimaryHDU(img_array) - hdr = self.generate_header_info( - filename, frametyp, custom_header, history, maxim, allowed_overwrite - ) - - # update RADECSYS key to RADECSYSa - if "RADECSYS" in hdr: - hdr["RADECSYSa"] = hdr["RADECSYS"] - hdr.pop("RADECSYS", None) - - hdu = fits.PrimaryHDU(img_array, header=hdr) + hdu.writeto(filename, overwrite=overwrite) return True @@ -1791,7 +1795,12 @@ def run_autofocus( nsteps, endpoint=True, ) - test_positions = np.round(test_positions, -2) + test_positions = test_positions.astype(int) + + autofocus_time = self.observatory_time.isot.replace(":", "-") + + if save_path is None: + save_path = Path(os.getcwd(),"images","autofocus",f"{autofocus_time}").resolve() focus_values = [] for i, position in enumerate(test_positions): @@ -1817,43 +1826,76 @@ def run_autofocus( logger.info("Exposure complete.") logger.info("Calculating mean star fwhm...") - if save_images: - if save_path is None: - save_path = Path(self._images_path / "autofocus").resolve() - else: - save_path = Path(tempfile.gettempdir()).resolve() + if not save_images: + save_path = Path(tempfile.gettempdir(),f"{autofocus_time}").resolve() if not save_path.exists(): save_path.mkdir(parents=True) fname = ( save_path - / f"autofocus_{self.observatory_time.isot.replace(':', '-')}.fts" + / f"FOCUS{int(position)}.fit" ) self.save_last_image(fname, frametyp="Focus") - cat = detect_sources_photutils( - fname, - threshold=100, - deblend=False, - tbl_save_path=Path(str(fname).replace(".fts", ".ecsv")), - ) - focus_values.append(np.mean(cat.fwhm.value)) - logger.info("FWHM = %.1f pixels" % focus_values[-1]) + + # Removed for hotfix for PlateSolve2 + # cat = detect_sources_photutils( + # fname, + # threshold=100, + # deblend=False, + # tbl_save_path=Path(str(fname).replace(".fts", ".ecsv")), + # ) + # focus_values.append(np.mean(cat.fwhm.value)) + # logger.info("FWHM = %.1f pixels" % focus_values[-1]) # fit hyperbola to focus values - popt, pcov = curve_fit( - lambda x, x0, a, b, c: a / b * np.sqrt(b**2 + (x - x0) ** 2) + c, - test_positions, - focus_values, - p0=[midpoint, 1, 1, 0], - bounds=( - [midpoint - n_steps * step_size, 0, 0, -1e6], - [midpoint + n_steps * step_size, 1e6, 1e6, 1e6], - ), - ) - result = popt[0] - result_err = np.sqrt(np.diag(pcov))[0] - logger.info("Best focus position is %i +/- %i" % (result, result_err)) + # Removed for hotfix for PlateSolve2 + # popt, pcov = curve_fit( + # lambda x, x0, a, b, c: a / b * np.sqrt(b**2 + (x - x0) ** 2) + c, + # test_positions, + # focus_values, + # p0=[midpoint, 1, 1, 0], + # bounds=( + # [midpoint - n_steps * step_size, 0, 0, -1e6], + # [midpoint + n_steps * step_size, 1e6, 1e6, 1e6], + # ), + # ) + # + # result = popt[0] + # result_err = np.sqrt(np.diag(pcov))[0] + + # Run platesolve 2 from cmd line + platesolve2_path = Path(r'C:\Program Files (x86)\PlaneWave Instruments\PlaneWave Interface 4\PlateSolve2\PlateSolve2.exe').resolve() + #print(platesolve2_path) + results_path = Path(save_path / 'results.txt').resolve() + #print(f"Results path: {results_path}") + image_path = glob.glob(str(save_path / '*.fit'))[0] + #print(f"Image path: {image_path}") + logger.info("Running PlateSolve2...") + procid = os.spawnv(os.P_NOWAIT, platesolve2_path, [f'"{platesolve2_path}" {image_path},{results_path},180']) + + while results_path.exists() == False: + time.sleep(0.1) + # Read results + with open(results_path, 'r') as f: + results = f.read() + while results == '': + time.sleep(0.1) + with open(results_path, 'r') as f: + results = f.read() + + print(f"Results path: {results_path}") + print(results) + results = results.split(',') + # Remove whitespace + results = [x.strip() for x in results] + print(results) + result, fwhm, result_err = float(results[0]), float(results[1]), float(results[2]) + print(f"Best focus: {result}") + print(f"FWHM: {fwhm}") + print(f"Focus error: {result_err}") + + logger.info(f"Best focus position is {result} +/- {result_err}") if result < test_positions[0] or result > test_positions[-1]: logger.warning("Best focus position is outside the test range.") @@ -2747,6 +2789,7 @@ def camera_info(self): info["EXPTIME"] = (last_exposure_duration, info["EXPTIME"][1]) info["EXPOSURE"] = (last_exposure_duration, info["EXPOSURE"][1]) except: + logger.debug("Could not get last exposure duration") pass try: info["CAMTIME"] = (self.camera.CameraTime, info["CAMTIME"][1]) diff --git a/pyscope/observatory/zwo_camera.py b/pyscope/observatory/zwo_camera.py new file mode 100644 index 00000000..27b444c4 --- /dev/null +++ b/pyscope/observatory/zwo_camera.py @@ -0,0 +1,598 @@ +import logging + + +import numpy as np +from astropy.time import Time +import pathlib +import zwoasi as asi + +from .camera import Camera + +logger = logging.getLogger(__name__) + +lib_path = pathlib.Path(r'C:\Users\MACRO\Downloads\ASI_Camera_SDK\ASI_Camera_SDK\ASI_Windows_SDK_V1.37\ASI SDK\lib\x64\ASICamera2.dll') +print(lib_path) + +asi.init(lib_path) + + +class ZWOCamera(Camera): + def __init__(self, device_number=0): + logger.debug(f"ZWOCamera.__init__({device_number})") + + + + self._device = asi.Camera(device_number) + self._camera_info = self._device.get_camera_property() + # Use minimum USB bandwidth permitted + self._device.set_control_value(asi.ASI_BANDWIDTHOVERLOAD, self._device.get_controls()['BandWidth']['MinValue']) + self._controls = self._device.get_controls() + self._last_exposure_duration = None + self._last_exposure_start_time = None + self._image_data_type = None + self._DoTranspose = True + self._camera_time = True + self._binX = 1 + self._NumX = self._camera_info['MaxWidth'] + self._NumY = self._camera_info['MaxHeight'] + self._StartX = 0 + self._StartY = 0 + + def AbortExposure(self): + logger.debug(f"ASICamera.AbortExposure() called") + self._device.stop_exposure() + + def SetImageDataType(self): + """Determine the data type of the image array based on the MaxADU property. + + This method is called automatically when the ImageArray property is called + if it has not already been set (initializes to `None`). + It will choose from the following data types based on the MaxADU property: + + - numpy.uint8 : (if MaxADU <= 255) + - numpy.uint16 : (default if MaxADU is not defined, or if MaxADU <= 65535) + - numpy.uint32 : (if MaxADU > 65535) + + See Also + -------- + numpy.uint8 + numpy.uint16 + numpy.uint32 + MaxADU : ASCOM Camera interface property `ASCOM Documentation `_ + """ + logger.debug(f"ASCOMCamera.SetImageDataType() called") + try: + max_adu = self.MaxADU + if max_adu <= 255: + self._image_data_type = np.uint8 + elif max_adu <= 65535: + self._image_data_type = np.uint16 + else: + self._image_data_type = np.uint32 + except: + self._image_data_type = np.uint16 + + def PulseGuide(self, Direction, Duration): + logger.debug(f"ASCOMCamera.PulseGuide({Direction}, {Duration}) called") + pass + + def StartExposure(self, Duration, Light): + logger.debug(f"ASCOMCamera.StartExposure({Duration}, {Light}) called") + + # Set up ROI and binning + bins = self.BinX + startX = self.StartX + startY = self.StartY + + max_bin_width = int(self.CameraXSize / bins) + max_bin_height = int(self.CameraYSize / bins) + # Fix using lines 479+ in zwoasi init + if self.NumX > max_bin_width: + self.NumX = max_bin_width + if self.NumY > max_bin_height: + self.NumY = max_bin_height + + width = self.NumX + height = self.NumY + + image_type = asi.ASI_IMG_RAW16 + whbi_old = self._device.get_roi_format() + whbi_new = [width, height, bins, image_type] + if whbi_old != whbi_new: + self._device.set_roi(startX, startY, width, height, bins, image_type) + + self._last_exposure_duration = Duration + self._last_exposure_start_time = str(Time.now()) + self.Exposure = Duration + dark = not Light + print(f"Dark: {dark}") + self._device.start_exposure(dark) + + def StopExposure(self): + logger.debug(f"ASCOMCamera.StopExposure() called") + self._device.stop_exposure() + + @property + def BayerOffsetX(self): # pragma: no cover + """ + .. warning:: + This property is not implemented in the ASCOM Alpaca protocol. + """ + logger.debug(f"ASCOMCamera.BayerOffsetX property called") + pass + + @property + def BayerOffsetY(self): # pragma: no cover + """ + .. warning:: + This property is not implemented in the ASCOM Alpaca protocol. + """ + logger.debug(f"ASCOMCamera.BayerOffsetY property called") + pass + + @property + def BinX(self): + logger.debug(f"ASCOMCamera.BinX property called") + return self._BinX + + @BinX.setter + def BinX(self, value): + logger.debug(f"ASCOMCamera.BinX property set to {value}") + self._BinX = value + + @property + def BinY(self): + logger.debug(f"ASCOMCamera.BinY property called - Symmetric binning only - use BinX property") + return self.BinX + + @BinY.setter + def BinY(self, value): + logger.debug(f"ASCOMCamera.BinY setter called - Symmetric binning only - use BinX property") + pass + + @property + def CameraState(self): + logger.debug(f"ASCOMCamera.CameraState property called") + return self._device.get_exposure_status() + + @property + def CameraXSize(self): + logger.debug(f"ASCOMCamera.CameraXSize property called") + return self._camera_info['MaxWidth'] + + @property + def CameraYSize(self): + logger.debug(f"ASCOMCamera.CameraYSize property called") + return self._camera_info['MaxHeight'] + + @property + def CameraTime(self): + logger.debug(f"ASCOMCamera.CameraTime property called") + return self._camera_time + + @property + def CanAbortExposure(self): + logger.debug(f"ASCOMCamera.CanAbortExposure property called") + return False + + @property + def CanAsymmetricBin(self): + logger.debug(f"ASCOMCamera.CanAsymmetricBin property called") + return False + + @property + def CanFastReadout(self): + logger.debug(f"ASCOMCamera.CanFastReadout property called") + return False + + @property + def CanGetCoolerPower(self): + logger.debug(f"ASCOMCamera.CanGetCoolerPower property called") + return False + + @property + def CanPulseGuide(self): + logger.debug(f"ASCOMCamera.CanPulseGuide property called") + return False + + @property + def CanSetCCDTemperature(self): + logger.debug(f"ASCOMCamera.CanSetCCDTemperature property called") + return self._camera_info['IsCoolerCam'] + + @property + def CanStopExposure(self): + logger.debug(f"ASCOMCamera.CanStopExposure property called") + return True + + @property + def CCDTemperature(self): + logger.debug(f"ASCOMCamera.CCDTemperature property called") + return self._device.get_control_value(asi.ASI_TEMPERATURE)[0]/10 + + @property + def CoolerOn(self): + logger.debug(f"ASCOMCamera.CoolerOn property called") + return self._device.get_control_value(asi.ASI_COOLER_ON)[0] + + @CoolerOn.setter + def CoolerOn(self, value): + logger.debug(f"ASCOMCamera.CoolerOn property set to {value}") + self._device.set_control_value(asi.ASI_COOLER_ON, value) + + @property + def CoolerPower(self): + logger.debug(f"ASCOMCamera.CoolerPower property called") + return self._device.get_control_value(asi.ASI_COOLER_POWER)[0] + + @property + def DriverVersion(self): + logger.debug(f"ASCOMCamera.DriverVersion property called") + return "Custom Driver" + + @property + def DriverInfo(self): + logger.debug(f"ASCOMCamera.DriverInfo property called") + return ["Custom Driver for ZWO ASI Cameras", 1] + + @property + def Description(self): + logger.debug(f"ASCOMCamera.Description property called") + return self._camera_info['Name'] + + @property + def ElectronsPerADU(self): + logger.debug(f"ASCOMCamera.ElectronsPerADU() property called") + return self._device.get_camera_property()['ElecPerADU']*self.BinX*self.BinY + + @property + def Exposure(self): + ''' + Get the exposure time in seconds. The exposure time is the time that the camera will be collecting light from the sky. The exposure time must be greater than or equal to the minimum exposure time and less than or equal to the maximum exposure time. The exposure time is specified in seconds. + + Returns + ------- + float + The exposure time in seconds. + ''' + logger.debug(f"ZWOASI.Exposure property called") + # Convert to seconds + return self._device.get_control_value(asi.ASI_EXPOSURE)[0] / 1e6 + + @Exposure.setter + def Exposure(self, value): + ''' + Set the exposure time in seconds. The exposure time is the time that the camera will be collecting light from the sky. The exposure time must be greater than or equal to the minimum exposure time and less than or equal to the maximum exposure time. The exposure time is specified in seconds. + + Parameters + ---------- + value : float + The exposure time in seconds. + ''' + logger.debug(f"ZWOASI.Exposure property set to {value}") + # Convert to microseconds + value = int(value * 1e6) + self._device.set_control_value(asi.ASI_EXPOSURE, value) + + @property + def ExposureMax(self): + logger.debug(f"ASCOMCamera.ExposureMax property called") + exp_max = self._controls['Exposure']['MaxValue'] + exp_max /= 1E6 + return exp_max + + @property + def ExposureMin(self): + logger.debug(f"ASCOMCamera.ExposureMin property called") + exp_min = self._controls['Exposure']['MinValue'] + exp_min /= 1E6 + return exp_min + + @property + def ExposureResolution(self): + logger.debug(f"ASCOMCamera.ExposureResolution property called") + return False + + @property + def FastReadout(self): + logger.debug(f"ASCOMCamera.FastReadout property called") + return False + + @FastReadout.setter + def FastReadout(self, value): + logger.debug(f"ASCOMCamera.FastReadout property set to {value}") + self._device.FastReadout = value + + @property + def FullWellCapacity(self): + logger.debug(f"Not implemented in ZWO ASI") + pass + + @property + def Gain(self): + logger.debug(f"ASCOMCamera.Gain property called") + return self._device.get_control_value(asi.ASI_GAIN)[0] + + @Gain.setter + def Gain(self, value): + logger.debug(f"ZWO ASI Gain set to {value}") + self._device.set_control_value(asi.ASI_GAIN, value) + self._device.Gain = value + # Add in ElecPerADU updater here and remove in ElectronsPerADU + # property for speed up. + + @property + def GainMax(self): + logger.debug(f"ASCOMCamera.GainMax property called") + return self._controls['Gain']['MaxValue'] + + @property + def GainMin(self): + logger.debug(f"ASCOMCamera.GainMin property called") + return self._controls['Gain']['MinValue'] + + @property + def Gains(self): + logger.debug(f"No Gains property in ZWO ASI") + pass + + @property + def HasShutter(self): + logger.debug(f"ZWOASI.HasShutter property called") + return False + + @property + def HeatSinkTemperature(self): + logger.debug(f"ASCOMCamera.HeatSinkTemperature property called") + return False + + @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") + + data = self._device.get_data_after_exposure(None) + whbi = self._device.get_roi_format() + shape = [whbi[1], whbi[0]] + if whbi[3] == asi.ASI_IMG_RAW8 or whbi[3] == asi.ASI_IMG_Y8: + img = np.frombuffer(data, dtype=np.uint8) + elif whbi[3] == asi.ASI_IMG_RAW16: + img = np.frombuffer(data, dtype=np.uint16) + elif whbi[3] == asi.ASI_IMG_RGB24: + img = np.frombuffer(data, dtype=np.uint8) + shape.append(3) + else: + raise ValueError('Unsupported image type') + img = img.reshape(shape) + # Done by default in zwoasi + # if self._DoTranspose: + # img_array = np.transpose(img_array) + return img + + @property + def ImageReady(self): + logger.debug(f"ASCOMCamera.ImageReady property called") + status = self._device.get_exposure_status() + image_ready = False + if status == asi.ASI_EXP_SUCCESS: + image_ready = True + return image_ready + + @property + def IsPulseGuiding(self): + logger.debug(f"ASCOMCamera.IsPulseGuiding property called") + return False + + @property + def LastExposureDuration(self): + logger.debug(f"ASCOMCamera.LastExposureDuration property called") + return self.LastInputExposureDuration + + @property + def LastExposureStartTime(self): + logger.debug(f"ASCOMCamera.LastExposureStartTime property called") + last_time = self._device.LastExposureStartTime + """ This code is needed to handle the case of the ASCOM ZWO driver + which returns an empty string instead of None if the camera does not + support the property """ + return ( + last_time + if last_time != "" and last_time != None + else self._last_exposure_start_time + ) + + @property + def LastInputExposureDuration(self): + logger.debug(f"ASCOMCamera.LastInputExposureDuration property called") + return self._last_exposure_duration + + @LastInputExposureDuration.setter + def LastInputExposureDuration(self, value): + logger.debug(f"ASCOMCamera.LastInputExposureDuration property set to {value}") + self._last_exposure_duration = value + + @property + def MaxADU(self): + logger.debug(f"ASCOMCamera.MaxADU property called") + return 65535 + + @property + def MaxBinX(self): + logger.debug(f"ASCOMCamera.MaxBinX property called") + return self._camera_info['SupportedBins'][-1] + + @property + def MaxBinY(self): + logger.debug(f"ASCOMCamera.MaxBinY property called") + return self._camera_info['SupportedBins'][-1] + + @property + def NumX(self): + logger.debug(f"ASCOMCamera.NumX property called") + return self._NumX + + @NumX.setter + def NumX(self, value): + logger.debug(f"ASCOMCamera.NumX property set to {value}") + width = value + if width%8 != 0: + width -= width%8 # Make width a multiple of 8 + self._NumX = width + + @property + def NumY(self): + logger.debug(f"ASCOMCamera.NumY property called") + return self._NumY + + @NumY.setter + def NumY(self, value): + logger.debug(f"ASCOMCamera.NumY property set to {value}") + # Set height to next multiple of 2 + height = value + if height%2 != 0: + height -= 1 # Make height even + self._NumY = height + + @property + def Offset(self): + logger.debug(f"ASCOMCamera.Offset property called") + return self._device.get_control_value(asi.ASI_OFFSET)[0] + + @Offset.setter + def Offset(self, value): + logger.debug(f"ASCOMCamera.Offset property set to {value}") + self._device.set_control_value(asi.ASI_OFFSET, value) + + @property + def OffsetMax(self): + logger.debug(f"ASCOMCamera.OffsetMax property called") + return self._controls['Offset']['MaxValue'] + + @property + def OffsetMin(self): + logger.debug(f"ASCOMCamera.OffsetMin property called") + return self._controls['Offset']['MinValue'] + + @property + def Offsets(self): + logger.debug(f"ASCOMCamera.Offsets property called") + pass + + @property + def PercentCompleted(self): + logger.debug(f"ASCOMCamera.PercentCompleted property called") + pass + + @property + def PixelSizeX(self): + logger.debug(f"ASCOMCamera.PixelSizeX property called") + return self._camera_info['PixelSize']*self.BinX + + @property + def PixelSizeY(self): + logger.debug(f"ASCOMCamera.PixelSizeY property called") + return self._camera_info['PixelSize']*self.BinY + + @property + def ReadoutMode(self): + logger.debug(f"ASCOMCamera.ReadoutMode property called") + return False + + @ReadoutMode.setter + def ReadoutMode(self, value): + logger.debug(f"ASCOMCamera.ReadoutMode property set to {value}") + pass + + @property + def ReadoutModes(self): + logger.debug(f"ASCOMCamera.ReadoutModes property called") + return None + + @property + def SensorName(self): + logger.debug(f"ASCOMCamera.SensorName property called") + return self._camera_info['Name'] + + @property + def Name(self): + logger.debug(f"ASCOMCamera.Name property called") + return self._camera_info['Name'] + + @property + def SensorType(self): + logger.debug(f"ASCOMCamera.SensorType property called") + return 'CMOS' + + @property + def SetCCDTemperature(self): + logger.debug(f"ASCOMCamera.SetCCDTemperature property called") + return self._device.get_control_value(asi.ASI_TARGET_TEMP)[0] + + @SetCCDTemperature.setter + def SetCCDTemperature(self, value): + logger.debug(f"ASCOMCamera.SetCCDTemperature property set to {value}") + self._device.set_control_value(asi.ASI_TARGET_TEMP, value) + + @property + def StartX(self): + logger.debug(f"ASCOMCamera.StartX property called") + return self._StartX + + @StartX.setter + def StartX(self, value): + logger.debug(f"ASCOMCamera.StartX property set to {value}") + self._StartX = value + + @property + def StartY(self): + logger.debug(f"ASCOMCamera.StartY property called") + return self._StartY + + @StartY.setter + def StartY(self, value): + logger.debug(f"ASCOMCamera.StartY property set to {value}") + self._StartY = value + + @property + def SubExposureDuration(self): + logger.debug(f"ASCOMCamera.SubExposureDuration property called") + pass + + @SubExposureDuration.setter + def SubExposureDuration(self, value): + logger.debug(f"ASCOMCamera.SubExposureDuration property set to {value}") + pass From 99195b4a3e32d4a15073a71a771f9fbb7d01f829 Mon Sep 17 00:00:00 2001 From: WGolay Date: Fri, 20 Dec 2024 11:09:36 -0500 Subject: [PATCH 52/60] Move to new scheduling module --- pyscope/{telrun => scheduling}/_block.py | 0 pyscope/{telrun => scheduling}/airmass_condition.py | 0 pyscope/{telrun => scheduling}/autofocus_field.py | 0 pyscope/{telrun => scheduling}/boundary_condition.py | 0 pyscope/{telrun => scheduling}/calibration_block.py | 0 pyscope/{telrun => scheduling}/celestialbody_condition.py | 0 pyscope/{telrun => scheduling}/coord_condition.py | 0 pyscope/{telrun => scheduling}/dark_field.py | 0 pyscope/{telrun => scheduling}/exoplanet_transits.py | 0 pyscope/{telrun => scheduling}/field.py | 0 pyscope/{telrun => scheduling}/flat_field.py | 0 pyscope/{telrun => scheduling}/hourangle_condition.py | 0 pyscope/{telrun => scheduling}/light_field.py | 0 pyscope/{telrun => scheduling}/lqs.py | 0 pyscope/{telrun => scheduling}/lqs_gauss.py | 0 pyscope/{telrun => scheduling}/lqs_inequality.py | 0 pyscope/{telrun => scheduling}/lqs_minmax.py | 0 pyscope/{telrun => scheduling}/lqs_piecewise.py | 0 pyscope/{telrun => scheduling}/lqs_sigmoid.py | 0 pyscope/{telrun => scheduling}/mk_mosaic_schedule.py | 0 pyscope/{telrun => scheduling}/moon_condition.py | 0 pyscope/{telrun => scheduling}/observer.py | 0 pyscope/{telrun => scheduling}/optimizer.py | 0 pyscope/{telrun => scheduling}/prioritizer.py | 0 pyscope/{telrun => scheduling}/project.py | 0 pyscope/{telrun => scheduling}/queue.py | 0 pyscope/{telrun => scheduling}/schedule.py | 0 pyscope/{telrun => scheduling}/schedule_block.py | 0 pyscope/{telrun => scheduling}/scheduler.py | 0 pyscope/{telrun => scheduling}/snr_condition.py | 0 pyscope/{telrun => scheduling}/sun_condition.py | 0 pyscope/{telrun => scheduling}/time_condition.py | 0 pyscope/{telrun => scheduling}/transition_field.py | 0 pyscope/{telrun => scheduling}/unallocated_block.py | 0 34 files changed, 0 insertions(+), 0 deletions(-) rename pyscope/{telrun => scheduling}/_block.py (100%) rename pyscope/{telrun => scheduling}/airmass_condition.py (100%) rename pyscope/{telrun => scheduling}/autofocus_field.py (100%) rename pyscope/{telrun => scheduling}/boundary_condition.py (100%) rename pyscope/{telrun => scheduling}/calibration_block.py (100%) rename pyscope/{telrun => scheduling}/celestialbody_condition.py (100%) rename pyscope/{telrun => scheduling}/coord_condition.py (100%) rename pyscope/{telrun => scheduling}/dark_field.py (100%) rename pyscope/{telrun => scheduling}/exoplanet_transits.py (100%) rename pyscope/{telrun => scheduling}/field.py (100%) rename pyscope/{telrun => scheduling}/flat_field.py (100%) rename pyscope/{telrun => scheduling}/hourangle_condition.py (100%) rename pyscope/{telrun => scheduling}/light_field.py (100%) rename pyscope/{telrun => scheduling}/lqs.py (100%) rename pyscope/{telrun => scheduling}/lqs_gauss.py (100%) rename pyscope/{telrun => scheduling}/lqs_inequality.py (100%) rename pyscope/{telrun => scheduling}/lqs_minmax.py (100%) rename pyscope/{telrun => scheduling}/lqs_piecewise.py (100%) rename pyscope/{telrun => scheduling}/lqs_sigmoid.py (100%) rename pyscope/{telrun => scheduling}/mk_mosaic_schedule.py (100%) rename pyscope/{telrun => scheduling}/moon_condition.py (100%) rename pyscope/{telrun => scheduling}/observer.py (100%) rename pyscope/{telrun => scheduling}/optimizer.py (100%) rename pyscope/{telrun => scheduling}/prioritizer.py (100%) rename pyscope/{telrun => scheduling}/project.py (100%) rename pyscope/{telrun => scheduling}/queue.py (100%) rename pyscope/{telrun => scheduling}/schedule.py (100%) rename pyscope/{telrun => scheduling}/schedule_block.py (100%) rename pyscope/{telrun => scheduling}/scheduler.py (100%) rename pyscope/{telrun => scheduling}/snr_condition.py (100%) rename pyscope/{telrun => scheduling}/sun_condition.py (100%) rename pyscope/{telrun => scheduling}/time_condition.py (100%) rename pyscope/{telrun => scheduling}/transition_field.py (100%) rename pyscope/{telrun => scheduling}/unallocated_block.py (100%) diff --git a/pyscope/telrun/_block.py b/pyscope/scheduling/_block.py similarity index 100% rename from pyscope/telrun/_block.py rename to pyscope/scheduling/_block.py diff --git a/pyscope/telrun/airmass_condition.py b/pyscope/scheduling/airmass_condition.py similarity index 100% rename from pyscope/telrun/airmass_condition.py rename to pyscope/scheduling/airmass_condition.py diff --git a/pyscope/telrun/autofocus_field.py b/pyscope/scheduling/autofocus_field.py similarity index 100% rename from pyscope/telrun/autofocus_field.py rename to pyscope/scheduling/autofocus_field.py diff --git a/pyscope/telrun/boundary_condition.py b/pyscope/scheduling/boundary_condition.py similarity index 100% rename from pyscope/telrun/boundary_condition.py rename to pyscope/scheduling/boundary_condition.py diff --git a/pyscope/telrun/calibration_block.py b/pyscope/scheduling/calibration_block.py similarity index 100% rename from pyscope/telrun/calibration_block.py rename to pyscope/scheduling/calibration_block.py diff --git a/pyscope/telrun/celestialbody_condition.py b/pyscope/scheduling/celestialbody_condition.py similarity index 100% rename from pyscope/telrun/celestialbody_condition.py rename to pyscope/scheduling/celestialbody_condition.py diff --git a/pyscope/telrun/coord_condition.py b/pyscope/scheduling/coord_condition.py similarity index 100% rename from pyscope/telrun/coord_condition.py rename to pyscope/scheduling/coord_condition.py diff --git a/pyscope/telrun/dark_field.py b/pyscope/scheduling/dark_field.py similarity index 100% rename from pyscope/telrun/dark_field.py rename to pyscope/scheduling/dark_field.py diff --git a/pyscope/telrun/exoplanet_transits.py b/pyscope/scheduling/exoplanet_transits.py similarity index 100% rename from pyscope/telrun/exoplanet_transits.py rename to pyscope/scheduling/exoplanet_transits.py diff --git a/pyscope/telrun/field.py b/pyscope/scheduling/field.py similarity index 100% rename from pyscope/telrun/field.py rename to pyscope/scheduling/field.py diff --git a/pyscope/telrun/flat_field.py b/pyscope/scheduling/flat_field.py similarity index 100% rename from pyscope/telrun/flat_field.py rename to pyscope/scheduling/flat_field.py diff --git a/pyscope/telrun/hourangle_condition.py b/pyscope/scheduling/hourangle_condition.py similarity index 100% rename from pyscope/telrun/hourangle_condition.py rename to pyscope/scheduling/hourangle_condition.py diff --git a/pyscope/telrun/light_field.py b/pyscope/scheduling/light_field.py similarity index 100% rename from pyscope/telrun/light_field.py rename to pyscope/scheduling/light_field.py diff --git a/pyscope/telrun/lqs.py b/pyscope/scheduling/lqs.py similarity index 100% rename from pyscope/telrun/lqs.py rename to pyscope/scheduling/lqs.py diff --git a/pyscope/telrun/lqs_gauss.py b/pyscope/scheduling/lqs_gauss.py similarity index 100% rename from pyscope/telrun/lqs_gauss.py rename to pyscope/scheduling/lqs_gauss.py diff --git a/pyscope/telrun/lqs_inequality.py b/pyscope/scheduling/lqs_inequality.py similarity index 100% rename from pyscope/telrun/lqs_inequality.py rename to pyscope/scheduling/lqs_inequality.py diff --git a/pyscope/telrun/lqs_minmax.py b/pyscope/scheduling/lqs_minmax.py similarity index 100% rename from pyscope/telrun/lqs_minmax.py rename to pyscope/scheduling/lqs_minmax.py diff --git a/pyscope/telrun/lqs_piecewise.py b/pyscope/scheduling/lqs_piecewise.py similarity index 100% rename from pyscope/telrun/lqs_piecewise.py rename to pyscope/scheduling/lqs_piecewise.py diff --git a/pyscope/telrun/lqs_sigmoid.py b/pyscope/scheduling/lqs_sigmoid.py similarity index 100% rename from pyscope/telrun/lqs_sigmoid.py rename to pyscope/scheduling/lqs_sigmoid.py diff --git a/pyscope/telrun/mk_mosaic_schedule.py b/pyscope/scheduling/mk_mosaic_schedule.py similarity index 100% rename from pyscope/telrun/mk_mosaic_schedule.py rename to pyscope/scheduling/mk_mosaic_schedule.py diff --git a/pyscope/telrun/moon_condition.py b/pyscope/scheduling/moon_condition.py similarity index 100% rename from pyscope/telrun/moon_condition.py rename to pyscope/scheduling/moon_condition.py diff --git a/pyscope/telrun/observer.py b/pyscope/scheduling/observer.py similarity index 100% rename from pyscope/telrun/observer.py rename to pyscope/scheduling/observer.py diff --git a/pyscope/telrun/optimizer.py b/pyscope/scheduling/optimizer.py similarity index 100% rename from pyscope/telrun/optimizer.py rename to pyscope/scheduling/optimizer.py diff --git a/pyscope/telrun/prioritizer.py b/pyscope/scheduling/prioritizer.py similarity index 100% rename from pyscope/telrun/prioritizer.py rename to pyscope/scheduling/prioritizer.py diff --git a/pyscope/telrun/project.py b/pyscope/scheduling/project.py similarity index 100% rename from pyscope/telrun/project.py rename to pyscope/scheduling/project.py diff --git a/pyscope/telrun/queue.py b/pyscope/scheduling/queue.py similarity index 100% rename from pyscope/telrun/queue.py rename to pyscope/scheduling/queue.py diff --git a/pyscope/telrun/schedule.py b/pyscope/scheduling/schedule.py similarity index 100% rename from pyscope/telrun/schedule.py rename to pyscope/scheduling/schedule.py diff --git a/pyscope/telrun/schedule_block.py b/pyscope/scheduling/schedule_block.py similarity index 100% rename from pyscope/telrun/schedule_block.py rename to pyscope/scheduling/schedule_block.py diff --git a/pyscope/telrun/scheduler.py b/pyscope/scheduling/scheduler.py similarity index 100% rename from pyscope/telrun/scheduler.py rename to pyscope/scheduling/scheduler.py diff --git a/pyscope/telrun/snr_condition.py b/pyscope/scheduling/snr_condition.py similarity index 100% rename from pyscope/telrun/snr_condition.py rename to pyscope/scheduling/snr_condition.py diff --git a/pyscope/telrun/sun_condition.py b/pyscope/scheduling/sun_condition.py similarity index 100% rename from pyscope/telrun/sun_condition.py rename to pyscope/scheduling/sun_condition.py diff --git a/pyscope/telrun/time_condition.py b/pyscope/scheduling/time_condition.py similarity index 100% rename from pyscope/telrun/time_condition.py rename to pyscope/scheduling/time_condition.py diff --git a/pyscope/telrun/transition_field.py b/pyscope/scheduling/transition_field.py similarity index 100% rename from pyscope/telrun/transition_field.py rename to pyscope/scheduling/transition_field.py diff --git a/pyscope/telrun/unallocated_block.py b/pyscope/scheduling/unallocated_block.py similarity index 100% rename from pyscope/telrun/unallocated_block.py rename to pyscope/scheduling/unallocated_block.py From cd7720138c5243752a401488d2c29542e94f6d7c Mon Sep 17 00:00:00 2001 From: WGolay Date: Fri, 20 Dec 2024 11:13:01 -0500 Subject: [PATCH 53/60] Fix inits --- pyscope/scheduling/__init__.py | 71 ++++++++++++++++++++++++++++++ pyscope/telrun/__init__.py | 79 ++++------------------------------ 2 files changed, 79 insertions(+), 71 deletions(-) create mode 100644 pyscope/scheduling/__init__.py diff --git a/pyscope/scheduling/__init__.py b/pyscope/scheduling/__init__.py new file mode 100644 index 00000000..4b8a11e7 --- /dev/null +++ b/pyscope/scheduling/__init__.py @@ -0,0 +1,71 @@ +# isort: skip_file + +import logging + +logger = logging.getLogger(__name__) + +from .boundary_condition import BoundaryCondition +from .coord_condition import CoordinateCondition +from .hourangle_condition import HourAngleCondition +from .airmass_condition import AirmassCondition +from .sun_condition import SunCondition +from .moon_condition import MoonCondition +from .time_condition import TimeCondition +from .snr_condition import SNRCondition + +from .field import Field +from .light_field import LightField +from .autofocus_field import AutofocusField +from .dark_field import DarkField +from .flat_field import FlatField +from .transition_field import TransitionField + +from ._block import _Block +from .schedule_block import ScheduleBlock +from .calibration_block import CalibrationBlock +from .unallocated_block import UnallocatedBlock + +from .observer import Observer +from .project import Project + +from .prioritizer import Prioritizer +from .optimizer import Optimizer + +from .queue import Queue +from .schedule import Schedule +from .scheduler import Scheduler + +from .exoplanet_transits import exoplanet_transits +from .mk_mosaic_schedule import mk_mosaic_schedule +from .survey_builder import survey_builder + +__all__ = [ + "BoundaryCondition", + "CoordinateCondition", + "HourAngleCondition", + "AirmassCondition", + "SunCondition", + "MoonCondition", + "TimeCondition", + "SNRCondition", + "Field", + "LightField", + "AutofocusField", + "DarkField", + "FlatField", + "TransitionField", + "_Block", + "ScheduleBlock", + "CalibrationBlock", + "UnallocatedBlock", + "Observer", + "Project", + "Prioritizer", + "Optimizer", + "Queue", + "Schedule", + "Scheduler", + "exoplanet_transits", + "mk_mosaic_schedule", + "survey_builder", +] diff --git a/pyscope/telrun/__init__.py b/pyscope/telrun/__init__.py index 4c2c8a51..c23242f3 100644 --- a/pyscope/telrun/__init__.py +++ b/pyscope/telrun/__init__.py @@ -8,92 +8,29 @@ logger = logging.getLogger(__name__) -from .boundary_condition import BoundaryCondition -from .coord_condition import CoordinateCondition -from .hourangle_condition import HourAngleCondition -from .airmass_condition import AirmassCondition -from .sun_condition import SunCondition -from .moon_condition import MoonCondition -from .time_condition import TimeCondition -from .snr_condition import SNRCondition - -from .field import Field -from .light_field import LightField -from .autofocus_field import AutofocusField -from .dark_field import DarkField -from .flat_field import FlatField -from .transition_field import TransitionField - from .option import Option from .instrument_configuration import InstrumentConfiguration -from ._block import _Block -from .schedule_block import ScheduleBlock -from .calibration_block import CalibrationBlock -from .unallocated_block import UnallocatedBlock - -from .observer import Observer -from .project import Project - -from .prioritizer import Prioritizer -from .optimizer import Optimizer - -from .queue import Queue -from .schedule import Schedule -from .scheduler import Scheduler - - from .telrun_exception import TelrunException -from .exoplanet_transits import exoplanet_transits from .init_telrun_dir import init_telrun_dir -from .mk_mosaic_schedule import mk_mosaic_schedule from .rst import rst from . import sch, schedtab, reports from .schedtel import schedtel, plot_schedule_gantt, plot_schedule_sky from .startup import start_telrun_operator -from .survey_builder import survey_builder from .telrun_operator import TelrunOperator __all__ = [ - "BoundaryCondition", - "CoordinateCondition", - "HourAngleCondition", - "AirmassCondition", - "SunCondition", - "MoonCondition", - "TimeCondition", - "SNRCondition", - "Field", - "LightField", - "AutofocusField", - "DarkField", - "FlatField", - "TransitionField", "Option", "InstrumentConfiguration", - "_Block", - "ScheduleBlock", - "CalibrationBlock", - "UnallocatedBlock", - "Observer", - "Project", - "Prioritizer", - "Optimizer", - "Queue", - "Schedule", - "Scheduler", - "exoplanet_transits", + "TelrunException", "init_telrun_dir", - # "mk_mosaic_schedule", - # "rst", - # "sch", - # "schedtab", - # "schedtel", - # "reports", - # "plot_schedule_gantt", - # "plot_schedule_sky", + "rst", + "sch", + "schedtab", + "reports", + "schedtel", + "plot_schedule_gantt", + "plot_schedule_sky", "start_telrun_operator", - "survey_builder", "TelrunOperator", - "TelrunException", ] From a881d79c596aa0180aadeaec4524d0ea8332606e Mon Sep 17 00:00:00 2001 From: WGolay Date: Fri, 20 Dec 2024 11:13:44 -0500 Subject: [PATCH 54/60] Fix top init --- pyscope/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyscope/__init__.py b/pyscope/__init__.py index 6bf642a2..e0ae51b1 100644 --- a/pyscope/__init__.py +++ b/pyscope/__init__.py @@ -87,4 +87,4 @@ logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) -__all__ = ["analysis", "observatory", "reduction", "telrun", "utils"] +__all__ = ["analysis", "observatory", "reduction", "scheduling", "telrun", "utils"] From 83a23a98cc1ea6c63d74156982ad7d1d95b1266e Mon Sep 17 00:00:00 2001 From: WGolay Date: Fri, 20 Dec 2024 11:26:30 -0500 Subject: [PATCH 55/60] Fix docs --- docs/source/api/index.rst | 1 + docs/source/api/scheduling.rst | 22 +++++++++++++++++++ pyscope/scheduling/_block.py | 2 +- .../{telrun => scheduling}/survey_builder.py | 0 4 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 docs/source/api/scheduling.rst rename pyscope/{telrun => scheduling}/survey_builder.py (100%) diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst index 7b4ad081..9a39d19c 100755 --- a/docs/source/api/index.rst +++ b/docs/source/api/index.rst @@ -5,6 +5,7 @@ API Reference :maxdepth: 3 observatory + scheduling telrun reduction analysis diff --git a/docs/source/api/scheduling.rst b/docs/source/api/scheduling.rst new file mode 100644 index 00000000..67a48457 --- /dev/null +++ b/docs/source/api/scheduling.rst @@ -0,0 +1,22 @@ +scheduling Module +=================== + +.. automodule:: pyscope.scheduling + +Classes +------- +.. automodsumm:: pyscope.scheduling + :classes-only: + :toctree: . + +Functions +--------- +.. automodsumm:: pyscope.scheduling + :functions-only: + :toctree: . + +Variables +--------------- +.. automodsumm:: pyscope.scheduling + :variables-only: + :toctree: . diff --git a/pyscope/scheduling/_block.py b/pyscope/scheduling/_block.py index 0f0b0f88..b1f1c2f5 100644 --- a/pyscope/scheduling/_block.py +++ b/pyscope/scheduling/_block.py @@ -4,7 +4,7 @@ from astropy.time import Time -from .instrument_configuration import InstrumentConfiguration +from ..telrun import InstrumentConfiguration from .observer import Observer logger = logging.getLogger(__name__) diff --git a/pyscope/telrun/survey_builder.py b/pyscope/scheduling/survey_builder.py similarity index 100% rename from pyscope/telrun/survey_builder.py rename to pyscope/scheduling/survey_builder.py From 6380970a66ffd3db8948a240fdc3e52834433cb4 Mon Sep 17 00:00:00 2001 From: WGolay Date: Fri, 20 Dec 2024 11:39:58 -0500 Subject: [PATCH 56/60] Remove astroplan from docs reqs --- setup.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index f7cfee4f..155807e9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -62,7 +62,6 @@ console_scripts = [options.extras_require] docs = - astroplan==0.9 sphinx==8.0.2 sphinx-astropy[confv2]==1.9.1 sphinx-favicon==1.0.1 From 026a9e1d55c0bb65907b83012f64088bacf09bfd Mon Sep 17 00:00:00 2001 From: WWGolay Date: Fri, 20 Dec 2024 09:44:13 -0700 Subject: [PATCH 57/60] Hotfix commits (wwg) --- asidemo.py | 244 +++++++++++++++++++++++++++++ pyscope/observatory/observatory.py | 64 +++++--- pyscope/observatory/zwo_camera.py | 106 +++++++------ pyscope/telrun/sch.py | 4 +- pyscope/telrun/schedtab.py | 2 +- pyscope/telrun/schedtel.py | 12 +- pyscope/telrun/telrun_operator.py | 44 +++++- 7 files changed, 389 insertions(+), 87 deletions(-) create mode 100644 asidemo.py diff --git a/asidemo.py b/asidemo.py new file mode 100644 index 00000000..df492046 --- /dev/null +++ b/asidemo.py @@ -0,0 +1,244 @@ +import ctypes +import pathlib +from ctypes import * +from enum import Enum + +import cv2 +import numpy as np + +lib_path = pathlib.Path( + r"C:\Users\MACRO\Downloads\ASI_Camera_SDK\ASI_Camera_SDK\ASI_Windows_SDK_V1.37\ASI SDK\lib\x64\ASICamera2.dll" +) +print(lib_path) +asi = CDLL(lib_path) # 调用SDK lib +cameraID = 0 # 初始化相关全局变量 +width = 0 +height = 0 +bufferSize = 0 + + +class ASI_IMG_TYPE(Enum): # 定义相关枚举量 + ASI_IMG_RAW8 = 0 + ASI_IMG_RGB24 = 1 + ASI_IMG_RAW16 = 2 + ASI_IMG_Y8 = 3 + ASI_IMG_END = -1 + + +class ASI_BOOL(Enum): + ASI_FALSE = 0 + ASI_TRUE = 1 + + +class ASI_BAYER_PATTERN(Enum): + ASI_BAYER_RG = 0 + ASI_BAYER_BG = 1 + ASI_BAYER_GR = 2 + ASI_BAYER_GB = 3 + + +class ASI_EXPOSURE_STATUS(Enum): + ASI_EXP_IDLE = 0 # idle states, you can start exposure now + ASI_EXP_WORKING = 1 # exposing + ASI_EXP_SUCCESS = 2 # exposure finished and waiting for download + ASI_EXP_FAILED = 3 # exposure failed, you need to start exposure again + + +class ASI_ERROR_CODE(Enum): + ASI_SUCCESS = 0 + ASI_ERROR_INVALID_INDEX = 1 # no camera connected or index value out of boundary + ASI_ERROR_INVALID_ID = 2 # invalid ID + ASI_ERROR_INVALID_CONTROL_TYPE = 3 # invalid control type + ASI_ERROR_CAMERA_CLOSED = 4 # camera didn't open + ASI_ERROR_CAMERA_REMOVED = ( + 5 # failed to find the camera, maybe the camera has been removed + ) + ASI_ERROR_INVALID_PATH = 6 # cannot find the path of the file + ASI_ERROR_INVALID_FILEFORMAT = 7 + ASI_ERROR_INVALID_SIZE = 8 # wrong video format size + ASI_ERROR_INVALID_IMGTYPE = 9 # unsupported image formate + ASI_ERROR_OUTOF_BOUNDARY = 10 # the startpos is out of boundary + ASI_ERROR_TIMEOUT = 11 # timeout + ASI_ERROR_INVALID_SEQUENCE = 12 # stop capture first + ASI_ERROR_BUFFER_TOO_SMALL = 13 # buffer size is not big enough + ASI_ERROR_VIDEO_MODE_ACTIVE = 14 + ASI_ERROR_EXPOSURE_IN_PROGRESS = 15 + ASI_ERROR_GENERAL_ERROR = 16 # general error, eg: value is out of valid range + ASI_ERROR_INVALID_MODE = 17 # the current mode is wrong + ASI_ERROR_END = 18 + + +class ASI_CAMERA_INFO(Structure): # 定义ASI_CAMERA_INFO 结构体 + _fields_ = [ + ("Name", c_char * 64), + ("CameraID", c_int), + ("MaxHeight", c_long), + ("MaxWidth", c_long), + ("IsColorCam", c_int), + ("BayerPattern", c_int), + ("SupportedBins", c_int * 16), + ("SupportedVideoFormat", c_int * 8), + ("PixelSize", c_double), + ("MechanicalShutter", c_int), + ("ST4Port", c_int), + ("IsCoolerCam", c_int), + ("IsUSB3Host", c_int), + ("IsUSB3Camera", c_int), + ("ElecPerADU", c_float), + ("BitDepth", c_int), + ("IsTriggerCam", c_int), + ("Unused", c_char * 16), + ] + + +class ASI_CONTROL_TYPE(Enum): # Control type 定义 ASI_CONTROL_TYPE 结构体 + ASI_GAIN = 0 + ASI_EXPOSURE = 1 + ASI_GAMMA = 2 + ASI_WB_R = 3 + ASI_WB_B = 4 + ASI_OFFSET = 5 + ASI_BANDWIDTHOVERLOAD = 6 + ASI_OVERCLOCK = 7 + ASI_TEMPERATURE = 8 # return 10*temperature + ASI_FLIP = 9 + ASI_AUTO_MAX_GAIN = 10 + ASI_AUTO_MAX_EXP = 11 # micro second + ASI_AUTO_TARGET_BRIGHTNESS = 12 # target brightness + ASI_HARDWARE_BIN = 13 + ASI_HIGH_SPEED_MODE = 14 + ASI_COOLER_POWER_PERC = 15 + ASI_TARGET_TEMP = 16 # not need *10 + ASI_COOLER_ON = 17 + ASI_MONO_BIN = 18 # lead to less grid at software bin mode for color camera + ASI_FAN_ON = 19 + ASI_PATTERN_ADJUST = 20 + ASI_ANTI_DEW_HEATER = 21 + + +def init(): # 相机初始化 + global width + global height + global cameraID + + num = asi.ASIGetNumOfConnectedCameras() # 获取连接相机数 + if num == 0: + print("No camera connection!") + return + print("Number of connected cameras: ", num) + + camInfo = ASI_CAMERA_INFO() + getCameraProperty = asi.ASIGetCameraProperty + getCameraProperty.argtypes = [POINTER(ASI_CAMERA_INFO), c_int] + getCameraProperty.restype = c_int + + for i in range(0, num): + asi.ASIGetCameraProperty(camInfo, i) # 以index获取相机属性 + + cameraID = camInfo.CameraID + + err = asi.ASIOpenCamera(cameraID) # 打开相机 + if err != 0: + return + print("Open Camera Success") + + err = asi.ASIInitCamera(cameraID) # 初始化相机 + if err != 0: + return + print("Init Camera Success") + + width = camInfo.MaxWidth + height = camInfo.MaxHeight + _bin = 1 + startx = 0 + starty = 0 + imageType = ASI_IMG_TYPE.ASI_IMG_RAW16 + + err = asi.ASISetROIFormat( + cameraID, width, height, _bin, imageType.value + ) # SetROIFormat + if err != 0: + print("Set ROI Format Fail") + return + print("Set ROI Format Success") + + asi.ASISetStartPos(cameraID, startx, starty) # SetStartPos + + +def getFrame(): # 获取图像帧 + global bufferSize + buffersize = width * height + + buffer = (c_ubyte * 2 * buffersize)() + + err = asi.ASIStartExposure(cameraID, 0) # 开始曝光 + if err != 0: + print("Start Exposure Fail") + return + print("Start Exposure Success") + + getExpStatus = asi.ASIGetExpStatus + getExpStatus.argtypes = [c_int, POINTER(c_int)] + getExpStatus.restype = c_int + expStatus = c_int() + + getDataAfterExp = asi.ASIGetDataAfterExp + getDataAfterExp.argtypes = [c_int, POINTER(c_ubyte) * 2, c_int] + getDataAfterExp.restype = c_int + buffer = (c_ubyte * 2 * buffersize)() + + getExpStatus(cameraID, expStatus) + print("status: ", expStatus) + + # c_int转int + sts = expStatus.value + while sts == ASI_EXPOSURE_STATUS.ASI_EXP_WORKING.value: # 获取曝光状态 + getExpStatus(cameraID, expStatus) + sts = expStatus.value + + if sts != ASI_EXPOSURE_STATUS.ASI_EXP_SUCCESS.value: + print("Exposure Fail") + return + + err = getDataAfterExp(cameraID, buffer, buffersize) # 曝光成功,获取buffer + if err != 0: + print("GetDataAfterExp Fail") + return + print("getDataAfterExp Success") + + return buffer + + +def closeCamera(): # 关闭相机 + err = asi.ASIStopVideoCapture(cameraID) + if err != 0: + print("Stop Capture Fail") + return + print("Stop Capture Success") + + err = asi.ASICloseCamera(cameraID) + if err != 0: + print("Close Camera Fail") + return + print("Close Camera Success") + + +def setExpValue(val): # 设置曝光时间 + # setControlValue = asi.ASISetControlValue + # setControlValue.argtypes = [c_int, ASI_CONTROL_TYPE, c_long, ASI_BOOL] + # setControlValue.restype = c_int + err = asi.ASISetControlValue( + cameraID, ASI_CONTROL_TYPE.ASI_EXPOSURE.value, val, ASI_BOOL.ASI_FALSE.value + ) + + +def setGainValue(val): # 设置增益 + err = asi.ASISetControlValue( + cameraID, ASI_CONTROL_TYPE.ASI_GAIN.value, val, ASI_BOOL.ASI_FALSE.value + ) + + +def setBiasValue(val): + err = asi.ASISetControlValue( + cameraID, ASI_CONTROL_TYPE.ASI_OFFSET.value, val, ASI_BOOL.ASI_FALSE.value + ) diff --git a/pyscope/observatory/observatory.py b/pyscope/observatory/observatory.py index 07b4fb19..239949c4 100644 --- a/pyscope/observatory/observatory.py +++ b/pyscope/observatory/observatory.py @@ -1312,7 +1312,7 @@ def save_last_image( hdr = self.generate_header_info( filename, frametyp, custom_header, history, maxim, allowed_overwrite ) - # update RADECSYS key to RADECSYSa + # update RADECSYS key to RADECSYSa if "RADECSYS" in hdr: hdr["RADECSYSa"] = hdr["RADECSYS"] hdr.pop("RADECSYS", None) @@ -1321,7 +1321,6 @@ def save_last_image( logger.exception(f"Error generating header information: {e}") hdu = fits.PrimaryHDU(img_array) - hdu.writeto(filename, overwrite=overwrite) return True @@ -1760,6 +1759,7 @@ def run_autofocus( use_current_pointing=False, save_images=False, save_path=None, + binning=2, # HOTFIX for 6200 ): """Runs the autofocus routine""" @@ -1800,7 +1800,9 @@ def run_autofocus( autofocus_time = self.observatory_time.isot.replace(":", "-") if save_path is None: - save_path = Path(os.getcwd(),"images","autofocus",f"{autofocus_time}").resolve() + save_path = Path( + os.getcwd(), "images", "autofocus", f"{autofocus_time}" + ).resolve() focus_values = [] for i, position in enumerate(test_positions): @@ -1819,6 +1821,9 @@ def run_autofocus( time.sleep(0.1) logger.info("Focuser moved.") + # Set binning + self.camera.BinX = binning + logger.info("Taking %s second exposure..." % exposure) self.camera.StartExposure(exposure, True) while not self.camera.ImageReady: @@ -1827,15 +1832,14 @@ def run_autofocus( logger.info("Calculating mean star fwhm...") if not save_images: - save_path = Path(tempfile.gettempdir(),f"{autofocus_time}").resolve() + save_path = Path( + tempfile.gettempdir(), f"{autofocus_time}" + ).resolve() if not save_path.exists(): save_path.mkdir(parents=True) - fname = ( - save_path - / f"FOCUS{int(position)}.fit" - ) + fname = save_path / f"FOCUS{int(position)}.fit" - self.save_last_image(fname, frametyp="Focus") + self.save_last_image(fname, frametyp="Focus", overwrite=True) # Removed for hotfix for PlateSolve2 # cat = detect_sources_photutils( @@ -1865,32 +1869,42 @@ def run_autofocus( # result_err = np.sqrt(np.diag(pcov))[0] # Run platesolve 2 from cmd line - platesolve2_path = Path(r'C:\Program Files (x86)\PlaneWave Instruments\PlaneWave Interface 4\PlateSolve2\PlateSolve2.exe').resolve() - #print(platesolve2_path) - results_path = Path(save_path / 'results.txt').resolve() - #print(f"Results path: {results_path}") - image_path = glob.glob(str(save_path / '*.fit'))[0] - #print(f"Image path: {image_path}") + platesolve2_path = Path( + r"C:\Program Files (x86)\PlaneWave Instruments\PlaneWave Interface 4\PlateSolve2\PlateSolve2.exe" + ).resolve() + # print(platesolve2_path) + results_path = Path(save_path / "results.txt").resolve() + # print(f"Results path: {results_path}") + image_path = glob.glob(str(save_path / "*.fit"))[0] + # print(f"Image path: {image_path}") logger.info("Running PlateSolve2...") - procid = os.spawnv(os.P_NOWAIT, platesolve2_path, [f'"{platesolve2_path}" {image_path},{results_path},180']) - + procid = os.spawnv( + os.P_NOWAIT, + platesolve2_path, + [f'"{platesolve2_path}" {image_path},{results_path},180'], + ) + while results_path.exists() == False: time.sleep(0.1) # Read results - with open(results_path, 'r') as f: + with open(results_path, "r") as f: results = f.read() - while results == '': + while results == "": time.sleep(0.1) - with open(results_path, 'r') as f: + with open(results_path, "r") as f: results = f.read() print(f"Results path: {results_path}") print(results) - results = results.split(',') + results = results.split(",") # Remove whitespace results = [x.strip() for x in results] print(results) - result, fwhm, result_err = float(results[0]), float(results[1]), float(results[2]) + result, fwhm, result_err = ( + float(results[0]), + float(results[1]), + float(results[2]), + ) print(f"Best focus: {result}") print(f"FWHM: {fwhm}") print(f"Focus error: {result_err}") @@ -1973,6 +1987,7 @@ def repositioning( tolerance=3, exposure=10, readout=0, + binning=2, # HOTFIX: HARDCODED FOR ASI6200 save_images=False, save_path="./", settle_time=5, @@ -2096,6 +2111,7 @@ def repositioning( logger.info("Taking %.2f second exposure" % exposure) self.camera.ReadoutMode = readout + self.camera.BinX = binning self.camera.StartExposure(exposure, True) while not self.camera.ImageReady: time.sleep(0.1) @@ -2303,11 +2319,11 @@ def take_flats( self.telescope.Tracking = False logger.info("Tracking off") - '''if self.cover_calibrator is not None: + """if self.cover_calibrator is not None: if self.cover_calibrator.CoverState != "NotPresent": logger.info("Opening the cover calibrator") self.cover_calibrator.OpenCover() - logger.info("Cover open")''' + logger.info("Cover open")""" if gain is not None: logger.info("Setting the camera gain to %i" % gain) diff --git a/pyscope/observatory/zwo_camera.py b/pyscope/observatory/zwo_camera.py index 27b444c4..eefe0d45 100644 --- a/pyscope/observatory/zwo_camera.py +++ b/pyscope/observatory/zwo_camera.py @@ -1,16 +1,17 @@ import logging - +import pathlib import numpy as np -from astropy.time import Time -import pathlib import zwoasi as asi +from astropy.time import Time from .camera import Camera logger = logging.getLogger(__name__) -lib_path = pathlib.Path(r'C:\Users\MACRO\Downloads\ASI_Camera_SDK\ASI_Camera_SDK\ASI_Windows_SDK_V1.37\ASI SDK\lib\x64\ASICamera2.dll') +lib_path = pathlib.Path( + r"C:\Users\MACRO\Downloads\ASI_Camera_SDK\ASI_Camera_SDK\ASI_Windows_SDK_V1.37\ASI SDK\lib\x64\ASICamera2.dll" +) print(lib_path) asi.init(lib_path) @@ -20,12 +21,13 @@ class ZWOCamera(Camera): def __init__(self, device_number=0): logger.debug(f"ZWOCamera.__init__({device_number})") - - self._device = asi.Camera(device_number) self._camera_info = self._device.get_camera_property() # Use minimum USB bandwidth permitted - self._device.set_control_value(asi.ASI_BANDWIDTHOVERLOAD, self._device.get_controls()['BandWidth']['MinValue']) + self._device.set_control_value( + asi.ASI_BANDWIDTHOVERLOAD, + self._device.get_controls()["BandWidth"]["MinValue"], + ) self._controls = self._device.get_controls() self._last_exposure_duration = None self._last_exposure_start_time = None @@ -33,8 +35,8 @@ def __init__(self, device_number=0): self._DoTranspose = True self._camera_time = True self._binX = 1 - self._NumX = self._camera_info['MaxWidth'] - self._NumY = self._camera_info['MaxHeight'] + self._NumX = self._camera_info["MaxWidth"] + self._NumY = self._camera_info["MaxHeight"] self._StartX = 0 self._StartY = 0 @@ -94,7 +96,7 @@ def StartExposure(self, Duration, Light): width = self.NumX height = self.NumY - + image_type = asi.ASI_IMG_RAW16 whbi_old = self._device.get_roi_format() whbi_new = [width, height, bins, image_type] @@ -142,12 +144,16 @@ def BinX(self, value): @property def BinY(self): - logger.debug(f"ASCOMCamera.BinY property called - Symmetric binning only - use BinX property") + logger.debug( + f"ASCOMCamera.BinY property called - Symmetric binning only - use BinX property" + ) return self.BinX @BinY.setter def BinY(self, value): - logger.debug(f"ASCOMCamera.BinY setter called - Symmetric binning only - use BinX property") + logger.debug( + f"ASCOMCamera.BinY setter called - Symmetric binning only - use BinX property" + ) pass @property @@ -158,12 +164,12 @@ def CameraState(self): @property def CameraXSize(self): logger.debug(f"ASCOMCamera.CameraXSize property called") - return self._camera_info['MaxWidth'] + return self._camera_info["MaxWidth"] @property def CameraYSize(self): logger.debug(f"ASCOMCamera.CameraYSize property called") - return self._camera_info['MaxHeight'] + return self._camera_info["MaxHeight"] @property def CameraTime(self): @@ -198,7 +204,7 @@ def CanPulseGuide(self): @property def CanSetCCDTemperature(self): logger.debug(f"ASCOMCamera.CanSetCCDTemperature property called") - return self._camera_info['IsCoolerCam'] + return self._camera_info["IsCoolerCam"] @property def CanStopExposure(self): @@ -208,7 +214,7 @@ def CanStopExposure(self): @property def CCDTemperature(self): logger.debug(f"ASCOMCamera.CCDTemperature property called") - return self._device.get_control_value(asi.ASI_TEMPERATURE)[0]/10 + return self._device.get_control_value(asi.ASI_TEMPERATURE)[0] / 10 @property def CoolerOn(self): @@ -229,46 +235,46 @@ def CoolerPower(self): def DriverVersion(self): logger.debug(f"ASCOMCamera.DriverVersion property called") return "Custom Driver" - + @property def DriverInfo(self): logger.debug(f"ASCOMCamera.DriverInfo property called") return ["Custom Driver for ZWO ASI Cameras", 1] - + @property def Description(self): logger.debug(f"ASCOMCamera.Description property called") - return self._camera_info['Name'] + return self._camera_info["Name"] @property def ElectronsPerADU(self): logger.debug(f"ASCOMCamera.ElectronsPerADU() property called") - return self._device.get_camera_property()['ElecPerADU']*self.BinX*self.BinY - + return self._device.get_camera_property()["ElecPerADU"] * self.BinX * self.BinY + @property def Exposure(self): - ''' + """ Get the exposure time in seconds. The exposure time is the time that the camera will be collecting light from the sky. The exposure time must be greater than or equal to the minimum exposure time and less than or equal to the maximum exposure time. The exposure time is specified in seconds. - + Returns ------- float The exposure time in seconds. - ''' + """ logger.debug(f"ZWOASI.Exposure property called") # Convert to seconds return self._device.get_control_value(asi.ASI_EXPOSURE)[0] / 1e6 - + @Exposure.setter def Exposure(self, value): - ''' + """ Set the exposure time in seconds. The exposure time is the time that the camera will be collecting light from the sky. The exposure time must be greater than or equal to the minimum exposure time and less than or equal to the maximum exposure time. The exposure time is specified in seconds. - + Parameters ---------- value : float The exposure time in seconds. - ''' + """ logger.debug(f"ZWOASI.Exposure property set to {value}") # Convert to microseconds value = int(value * 1e6) @@ -277,15 +283,15 @@ def Exposure(self, value): @property def ExposureMax(self): logger.debug(f"ASCOMCamera.ExposureMax property called") - exp_max = self._controls['Exposure']['MaxValue'] - exp_max /= 1E6 + exp_max = self._controls["Exposure"]["MaxValue"] + exp_max /= 1e6 return exp_max @property def ExposureMin(self): logger.debug(f"ASCOMCamera.ExposureMin property called") - exp_min = self._controls['Exposure']['MinValue'] - exp_min /= 1E6 + exp_min = self._controls["Exposure"]["MinValue"] + exp_min /= 1e6 return exp_min @property @@ -324,12 +330,12 @@ def Gain(self, value): @property def GainMax(self): logger.debug(f"ASCOMCamera.GainMax property called") - return self._controls['Gain']['MaxValue'] + return self._controls["Gain"]["MaxValue"] @property def GainMin(self): logger.debug(f"ASCOMCamera.GainMin property called") - return self._controls['Gain']['MinValue'] + return self._controls["Gain"]["MinValue"] @property def Gains(self): @@ -396,7 +402,7 @@ def ImageArray(self): img = np.frombuffer(data, dtype=np.uint8) shape.append(3) else: - raise ValueError('Unsupported image type') + raise ValueError("Unsupported image type") img = img.reshape(shape) # Done by default in zwoasi # if self._DoTranspose: @@ -453,12 +459,12 @@ def MaxADU(self): @property def MaxBinX(self): logger.debug(f"ASCOMCamera.MaxBinX property called") - return self._camera_info['SupportedBins'][-1] + return self._camera_info["SupportedBins"][-1] @property def MaxBinY(self): logger.debug(f"ASCOMCamera.MaxBinY property called") - return self._camera_info['SupportedBins'][-1] + return self._camera_info["SupportedBins"][-1] @property def NumX(self): @@ -469,8 +475,8 @@ def NumX(self): def NumX(self, value): logger.debug(f"ASCOMCamera.NumX property set to {value}") width = value - if width%8 != 0: - width -= width%8 # Make width a multiple of 8 + if width % 8 != 0: + width -= width % 8 # Make width a multiple of 8 self._NumX = width @property @@ -483,8 +489,8 @@ def NumY(self, value): logger.debug(f"ASCOMCamera.NumY property set to {value}") # Set height to next multiple of 2 height = value - if height%2 != 0: - height -= 1 # Make height even + if height % 2 != 0: + height -= 1 # Make height even self._NumY = height @property @@ -500,12 +506,12 @@ def Offset(self, value): @property def OffsetMax(self): logger.debug(f"ASCOMCamera.OffsetMax property called") - return self._controls['Offset']['MaxValue'] + return self._controls["Offset"]["MaxValue"] @property def OffsetMin(self): logger.debug(f"ASCOMCamera.OffsetMin property called") - return self._controls['Offset']['MinValue'] + return self._controls["Offset"]["MinValue"] @property def Offsets(self): @@ -520,13 +526,13 @@ def PercentCompleted(self): @property def PixelSizeX(self): logger.debug(f"ASCOMCamera.PixelSizeX property called") - return self._camera_info['PixelSize']*self.BinX + return self._camera_info["PixelSize"] * self.BinX @property def PixelSizeY(self): logger.debug(f"ASCOMCamera.PixelSizeY property called") - return self._camera_info['PixelSize']*self.BinY - + return self._camera_info["PixelSize"] * self.BinY + @property def ReadoutMode(self): logger.debug(f"ASCOMCamera.ReadoutMode property called") @@ -545,17 +551,17 @@ def ReadoutModes(self): @property def SensorName(self): logger.debug(f"ASCOMCamera.SensorName property called") - return self._camera_info['Name'] - + return self._camera_info["Name"] + @property def Name(self): logger.debug(f"ASCOMCamera.Name property called") - return self._camera_info['Name'] + return self._camera_info["Name"] @property def SensorType(self): logger.debug(f"ASCOMCamera.SensorType property called") - return 'CMOS' + return "CMOS" @property def SetCCDTemperature(self): diff --git a/pyscope/telrun/sch.py b/pyscope/telrun/sch.py index fd584047..b932d439 100644 --- a/pyscope/telrun/sch.py +++ b/pyscope/telrun/sch.py @@ -537,13 +537,13 @@ def read( if "repositioning" in line.keys(): if ( line["repositioning"].startswith("t") - or line["repositioning"]=="1" + or line["repositioning"] == "1" or line["repositioning"].startswith("y") ): repositioning = (-1, -1) elif ( line["repositioning"].startswith("f") - or line["repositioning"]=="0" + or line["repositioning"] == "0" or line["repositioning"].startswith("n") ): repositioning = (0, 0) diff --git a/pyscope/telrun/schedtab.py b/pyscope/telrun/schedtab.py index 91fc9ac3..a809e347 100644 --- a/pyscope/telrun/schedtab.py +++ b/pyscope/telrun/schedtab.py @@ -93,7 +93,7 @@ def blocks_to_table(observing_blocks): ), format="mjd", ) - + # print(f"Target to hmsdms: {block.target.to_string('hmsdms')}") t["target"] = coord.SkyCoord( [ ( diff --git a/pyscope/telrun/schedtel.py b/pyscope/telrun/schedtel.py index 65894208..a685ee7b 100644 --- a/pyscope/telrun/schedtel.py +++ b/pyscope/telrun/schedtel.py @@ -1159,15 +1159,15 @@ def plot_schedule_sky_cli(schedule_table, observatory): # ] # ax.legend(*zip(*unique), loc=(1.1, 0)) - - # Add title to plot based on date - t0 = np.min(schedule_table["start_time"]) # -1 corrects for UTC to local time, should be cleaned up + t0 = np.min( + schedule_table["start_time"] + ) # -1 corrects for UTC to local time, should be cleaned up t0 = astrotime.Time(t0, format="mjd") t0.format = "iso" - - ax.set_title(f"Observing Schedule: Night of {t0.to_string().split(' ')[0]} UTC", - fontsize=14 + + ax.set_title( + f"Observing Schedule: Night of {t0.to_string().split(' ')[0]} UTC", fontsize=14 ) ax.legend(labels, loc=(1.1, 0)) diff --git a/pyscope/telrun/telrun_operator.py b/pyscope/telrun/telrun_operator.py index 277c472f..ff0be2ff 100644 --- a/pyscope/telrun/telrun_operator.py +++ b/pyscope/telrun/telrun_operator.py @@ -195,6 +195,7 @@ def __init__(self, telhome="./", gui=True, **kwargs): self._autofocus_nsteps = 5 self._autofocus_step_size = 500 self._autofocus_use_current_pointing = False + self._autofocus_binning = 1 self._autofocus_timeout = 180 self._repositioning_wcs_solver = "astrometry_net_wcs" self._repositioning_max_stability_time = 600 # seconds @@ -208,6 +209,7 @@ def __init__(self, telhome="./", gui=True, **kwargs): self._repositioning_save_images = False self._repositioning_save_path = self._images_path / "repositioning" self._repositioning_timeout = 180 + self._repositioning_binning = 1 self._wcs_solver = "astrometry_net_wcs" self._wcs_filters = None self._wcs_timeout = 30 @@ -305,6 +307,9 @@ def __init__(self, telhome="./", gui=True, **kwargs): "autofocus_use_current_pointing", fallback=self._autofocus_use_current_pointing, ) + self._autofocus_binning = self._config.getint( + "autofocus", "autofocus_binning", fallback=self._autofocus_binning + ) self._autofocus_timeout = self._config.getfloat( "autofocus", "autofocus_timeout", fallback=self._autofocus_timeout ) @@ -370,6 +375,11 @@ def __init__(self, telhome="./", gui=True, **kwargs): "repositioning_timeout", fallback=self._repositioning_timeout, ) + self._repositioning_binning = self._config.getint( + "repositioning", + "repositioning_binning", + fallback=self._repositioning_binning, + ) self._wcs_solver = self._config.get( "wcs", "wcs_solver", fallback=self._wcs_solver ) @@ -498,6 +508,9 @@ def __init__(self, telhome="./", gui=True, **kwargs): self.autofocus_use_current_pointing = kwargs.get( "autofocus_use_current_pointing", self._autofocus_use_current_pointing ) + self.autofocus_binning = kwargs.get( + "autofocus_binning", self._autofocus_binning + ) self.autofocus_timeout = kwargs.get( "autofocus_timeout", self._autofocus_timeout ) @@ -537,6 +550,9 @@ def __init__(self, telhome="./", gui=True, **kwargs): self.repositioning_timeout = kwargs.get( "repositioning_timeout", self._repositioning_timeout ) + self.repositioning_binning = kwargs.get( + "repositioning_binning", self._repositioning_binning + ) self.wcs_solver = kwargs.get("wcs_solver", self._wcs_solver) self.wcs_filters = kwargs.get("wcs_filters", self._wcs_filters) self.wcs_timeout = kwargs.get("wcs_timeout", self._wcs_timeout) @@ -1449,10 +1465,8 @@ def execute_block(self, *args, slew=True, **kwargs): # TODO: Make this better - temporary fix 2024-11-15 # Check if focuser is outside of self.autofocus_midpoint +/- 1000 if ( - self.observatory.focuser.Position - < self.autofocus_midpoint - 1000 - or self.observatory.focuser.Position - > self.autofocus_midpoint + 1000 + self.observatory.focuser.Position < self.autofocus_midpoint - 1000 + or self.observatory.focuser.Position > self.autofocus_midpoint + 1000 ): logger.info( "Focuser position is outside of autofocus_midpoint +/- 1000, moving to autofocus_midpoint..." @@ -1479,6 +1493,7 @@ def execute_block(self, *args, slew=True, **kwargs): nsteps=self.autofocus_nsteps, step_size=self.autofocus_step_size, use_current_pointing=self.autofocus_use_current_pointing, + binning=self.autofocus_binning, ) self._status_event.set() t.join() @@ -1725,6 +1740,7 @@ def execute_block(self, *args, slew=True, **kwargs): do_initial_slew=slew, readout=self.default_readout, solver=self.repositioning_wcs_solver, + binning=self.repositioning_binning, ) self._camera_status = "Idle" self._telescope_status = "Idle" @@ -2862,6 +2878,15 @@ def autofocus_use_current_pointing(self, value): self._autofocus_use_current_pointing ) + @property + def autofocus_binning(self): + return self._autofocus_binning + + @autofocus_binning.setter + def autofocus_binning(self, value): + self._autofocus_binning = int(value) + self._config["autofocus"]["autofocus_binning"] = str(self._autofocus_binning) + @property def autofocus_timeout(self): return self._autofocus_timeout @@ -3025,6 +3050,17 @@ def repositioning_timeout(self, value): self._repositioning_timeout ) + @property + def repositioning_binning(self): + return self._repositioning_binning + + @repositioning_binning.setter + def repositioning_binning(self, value): + self._repositioning_binning = value + self._config["repositioning"]["repositioning_binning"] = str( + self._repositioning_binning + ) + @property def wcs_solver(self): return self._wcs_solver From 735ff14cbccdd829baf0b66575940ff2691c63d7 Mon Sep 17 00:00:00 2001 From: WGolay Date: Fri, 20 Dec 2024 11:48:49 -0500 Subject: [PATCH 58/60] Doc compile fix --- pyscope/reduction/maxim_pinpoint_wcs.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pyscope/reduction/maxim_pinpoint_wcs.py b/pyscope/reduction/maxim_pinpoint_wcs.py index 0560365d..dc52f7f3 100644 --- a/pyscope/reduction/maxim_pinpoint_wcs.py +++ b/pyscope/reduction/maxim_pinpoint_wcs.py @@ -24,13 +24,14 @@ def maxim_pinpoint_wcs_cli(filepath): """ Platesolve images using the PinPoint solver in MaxIm DL. - This script interacts with MaxIm DL's PinPoint solver via its COM interface - to solve astronomical images for their World Coordinate System (`WCS`). The + This script interacts with MaxIm DL's PinPoint solver via its COM interface + to solve astronomical images for their World Coordinate System (`WCS`). The solved image is saved back to the original file if the process is successful. .. note:: - This script requires MaxIm DL to be installed on a Windows system. The PinPoint - solver is accessed through MaxIm DL's COM interface. + + This script requires MaxIm DL to be installed on a Windows system. The PinPoint + solver is accessed through MaxIm DL's COM interface. Parameters ---------- From b8b87673136a5dfdf0e74cd58aeaae3209dd83b0 Mon Sep 17 00:00:00 2001 From: WGolay Date: Fri, 20 Dec 2024 12:37:46 -0500 Subject: [PATCH 59/60] Formatting and versioning fixes --- asidemo.py | 244 ------------------ pyscope/observatory/ascom_cover_calibrator.py | 10 +- pyscope/observatory/ascom_device.py | 12 +- pyscope/observatory/ascom_telescope.py | 10 +- pyscope/observatory/cover_calibrator.py | 14 +- pyscope/observatory/device.py | 2 +- pyscope/observatory/dome.py | 8 +- pyscope/observatory/focuser.py | 2 +- .../observatory/html_observing_conditions.py | 2 +- pyscope/observatory/maxim.py | 2 +- pyscope/observatory/observing_conditions.py | 4 +- pyscope/observatory/pwi4_autofocus.py | 2 +- pyscope/observatory/pwi4_focuser.py | 2 +- pyscope/observatory/pwi_autofocus.py | 2 +- pyscope/observatory/rotator.py | 4 +- pyscope/observatory/simulator_server.py | 4 +- pyscope/observatory/switch.py | 2 +- pyscope/observatory/telescope.py | 16 +- pyscope/observatory/zwo_camera.py | 12 +- pyscope/reduction/astrometry_net_wcs.py | 2 +- pyscope/reduction/avg_fits.py | 1 - pyscope/reduction/calib_images.py | 11 +- pyscope/reduction/ccd_calib.py | 121 ++++----- pyscope/reduction/fitslist.py | 4 +- pyscope/reduction/pinpoint_wcs.py | 2 +- pyscope/reduction/process_images.py | 14 +- pyscope/reduction/reduce_calibration_set.py | 12 +- pyscope/telrun/schedtel.py | 14 +- requirements.txt | 5 +- 29 files changed, 152 insertions(+), 388 deletions(-) delete mode 100644 asidemo.py diff --git a/asidemo.py b/asidemo.py deleted file mode 100644 index df492046..00000000 --- a/asidemo.py +++ /dev/null @@ -1,244 +0,0 @@ -import ctypes -import pathlib -from ctypes import * -from enum import Enum - -import cv2 -import numpy as np - -lib_path = pathlib.Path( - r"C:\Users\MACRO\Downloads\ASI_Camera_SDK\ASI_Camera_SDK\ASI_Windows_SDK_V1.37\ASI SDK\lib\x64\ASICamera2.dll" -) -print(lib_path) -asi = CDLL(lib_path) # 调用SDK lib -cameraID = 0 # 初始化相关全局变量 -width = 0 -height = 0 -bufferSize = 0 - - -class ASI_IMG_TYPE(Enum): # 定义相关枚举量 - ASI_IMG_RAW8 = 0 - ASI_IMG_RGB24 = 1 - ASI_IMG_RAW16 = 2 - ASI_IMG_Y8 = 3 - ASI_IMG_END = -1 - - -class ASI_BOOL(Enum): - ASI_FALSE = 0 - ASI_TRUE = 1 - - -class ASI_BAYER_PATTERN(Enum): - ASI_BAYER_RG = 0 - ASI_BAYER_BG = 1 - ASI_BAYER_GR = 2 - ASI_BAYER_GB = 3 - - -class ASI_EXPOSURE_STATUS(Enum): - ASI_EXP_IDLE = 0 # idle states, you can start exposure now - ASI_EXP_WORKING = 1 # exposing - ASI_EXP_SUCCESS = 2 # exposure finished and waiting for download - ASI_EXP_FAILED = 3 # exposure failed, you need to start exposure again - - -class ASI_ERROR_CODE(Enum): - ASI_SUCCESS = 0 - ASI_ERROR_INVALID_INDEX = 1 # no camera connected or index value out of boundary - ASI_ERROR_INVALID_ID = 2 # invalid ID - ASI_ERROR_INVALID_CONTROL_TYPE = 3 # invalid control type - ASI_ERROR_CAMERA_CLOSED = 4 # camera didn't open - ASI_ERROR_CAMERA_REMOVED = ( - 5 # failed to find the camera, maybe the camera has been removed - ) - ASI_ERROR_INVALID_PATH = 6 # cannot find the path of the file - ASI_ERROR_INVALID_FILEFORMAT = 7 - ASI_ERROR_INVALID_SIZE = 8 # wrong video format size - ASI_ERROR_INVALID_IMGTYPE = 9 # unsupported image formate - ASI_ERROR_OUTOF_BOUNDARY = 10 # the startpos is out of boundary - ASI_ERROR_TIMEOUT = 11 # timeout - ASI_ERROR_INVALID_SEQUENCE = 12 # stop capture first - ASI_ERROR_BUFFER_TOO_SMALL = 13 # buffer size is not big enough - ASI_ERROR_VIDEO_MODE_ACTIVE = 14 - ASI_ERROR_EXPOSURE_IN_PROGRESS = 15 - ASI_ERROR_GENERAL_ERROR = 16 # general error, eg: value is out of valid range - ASI_ERROR_INVALID_MODE = 17 # the current mode is wrong - ASI_ERROR_END = 18 - - -class ASI_CAMERA_INFO(Structure): # 定义ASI_CAMERA_INFO 结构体 - _fields_ = [ - ("Name", c_char * 64), - ("CameraID", c_int), - ("MaxHeight", c_long), - ("MaxWidth", c_long), - ("IsColorCam", c_int), - ("BayerPattern", c_int), - ("SupportedBins", c_int * 16), - ("SupportedVideoFormat", c_int * 8), - ("PixelSize", c_double), - ("MechanicalShutter", c_int), - ("ST4Port", c_int), - ("IsCoolerCam", c_int), - ("IsUSB3Host", c_int), - ("IsUSB3Camera", c_int), - ("ElecPerADU", c_float), - ("BitDepth", c_int), - ("IsTriggerCam", c_int), - ("Unused", c_char * 16), - ] - - -class ASI_CONTROL_TYPE(Enum): # Control type 定义 ASI_CONTROL_TYPE 结构体 - ASI_GAIN = 0 - ASI_EXPOSURE = 1 - ASI_GAMMA = 2 - ASI_WB_R = 3 - ASI_WB_B = 4 - ASI_OFFSET = 5 - ASI_BANDWIDTHOVERLOAD = 6 - ASI_OVERCLOCK = 7 - ASI_TEMPERATURE = 8 # return 10*temperature - ASI_FLIP = 9 - ASI_AUTO_MAX_GAIN = 10 - ASI_AUTO_MAX_EXP = 11 # micro second - ASI_AUTO_TARGET_BRIGHTNESS = 12 # target brightness - ASI_HARDWARE_BIN = 13 - ASI_HIGH_SPEED_MODE = 14 - ASI_COOLER_POWER_PERC = 15 - ASI_TARGET_TEMP = 16 # not need *10 - ASI_COOLER_ON = 17 - ASI_MONO_BIN = 18 # lead to less grid at software bin mode for color camera - ASI_FAN_ON = 19 - ASI_PATTERN_ADJUST = 20 - ASI_ANTI_DEW_HEATER = 21 - - -def init(): # 相机初始化 - global width - global height - global cameraID - - num = asi.ASIGetNumOfConnectedCameras() # 获取连接相机数 - if num == 0: - print("No camera connection!") - return - print("Number of connected cameras: ", num) - - camInfo = ASI_CAMERA_INFO() - getCameraProperty = asi.ASIGetCameraProperty - getCameraProperty.argtypes = [POINTER(ASI_CAMERA_INFO), c_int] - getCameraProperty.restype = c_int - - for i in range(0, num): - asi.ASIGetCameraProperty(camInfo, i) # 以index获取相机属性 - - cameraID = camInfo.CameraID - - err = asi.ASIOpenCamera(cameraID) # 打开相机 - if err != 0: - return - print("Open Camera Success") - - err = asi.ASIInitCamera(cameraID) # 初始化相机 - if err != 0: - return - print("Init Camera Success") - - width = camInfo.MaxWidth - height = camInfo.MaxHeight - _bin = 1 - startx = 0 - starty = 0 - imageType = ASI_IMG_TYPE.ASI_IMG_RAW16 - - err = asi.ASISetROIFormat( - cameraID, width, height, _bin, imageType.value - ) # SetROIFormat - if err != 0: - print("Set ROI Format Fail") - return - print("Set ROI Format Success") - - asi.ASISetStartPos(cameraID, startx, starty) # SetStartPos - - -def getFrame(): # 获取图像帧 - global bufferSize - buffersize = width * height - - buffer = (c_ubyte * 2 * buffersize)() - - err = asi.ASIStartExposure(cameraID, 0) # 开始曝光 - if err != 0: - print("Start Exposure Fail") - return - print("Start Exposure Success") - - getExpStatus = asi.ASIGetExpStatus - getExpStatus.argtypes = [c_int, POINTER(c_int)] - getExpStatus.restype = c_int - expStatus = c_int() - - getDataAfterExp = asi.ASIGetDataAfterExp - getDataAfterExp.argtypes = [c_int, POINTER(c_ubyte) * 2, c_int] - getDataAfterExp.restype = c_int - buffer = (c_ubyte * 2 * buffersize)() - - getExpStatus(cameraID, expStatus) - print("status: ", expStatus) - - # c_int转int - sts = expStatus.value - while sts == ASI_EXPOSURE_STATUS.ASI_EXP_WORKING.value: # 获取曝光状态 - getExpStatus(cameraID, expStatus) - sts = expStatus.value - - if sts != ASI_EXPOSURE_STATUS.ASI_EXP_SUCCESS.value: - print("Exposure Fail") - return - - err = getDataAfterExp(cameraID, buffer, buffersize) # 曝光成功,获取buffer - if err != 0: - print("GetDataAfterExp Fail") - return - print("getDataAfterExp Success") - - return buffer - - -def closeCamera(): # 关闭相机 - err = asi.ASIStopVideoCapture(cameraID) - if err != 0: - print("Stop Capture Fail") - return - print("Stop Capture Success") - - err = asi.ASICloseCamera(cameraID) - if err != 0: - print("Close Camera Fail") - return - print("Close Camera Success") - - -def setExpValue(val): # 设置曝光时间 - # setControlValue = asi.ASISetControlValue - # setControlValue.argtypes = [c_int, ASI_CONTROL_TYPE, c_long, ASI_BOOL] - # setControlValue.restype = c_int - err = asi.ASISetControlValue( - cameraID, ASI_CONTROL_TYPE.ASI_EXPOSURE.value, val, ASI_BOOL.ASI_FALSE.value - ) - - -def setGainValue(val): # 设置增益 - err = asi.ASISetControlValue( - cameraID, ASI_CONTROL_TYPE.ASI_GAIN.value, val, ASI_BOOL.ASI_FALSE.value - ) - - -def setBiasValue(val): - err = asi.ASISetControlValue( - cameraID, ASI_CONTROL_TYPE.ASI_OFFSET.value, val, ASI_BOOL.ASI_FALSE.value - ) diff --git a/pyscope/observatory/ascom_cover_calibrator.py b/pyscope/observatory/ascom_cover_calibrator.py index 01ce4a3e..ee6ad39a 100644 --- a/pyscope/observatory/ascom_cover_calibrator.py +++ b/pyscope/observatory/ascom_cover_calibrator.py @@ -48,7 +48,7 @@ def CalibratorOff(self): def CalibratorOn(self, Brightness): """ Turns the calibrator on at the specified brightness if the device has a calibrator. - + If the calibrator requires time to safely stabilise, `CalibratorState` must return `NotReady`. When the calibrator has stabilised, `CalibratorState` must return `Ready`. If a device has both cover and calibrator capabilities, the method may change `CoverState`. @@ -65,7 +65,7 @@ def CalibratorOn(self, Brightness): def CloseCover(self): """ Starts closing the cover if the device has a cover. - + While the cover is closing, `CoverState` must return `Moving`. When the cover is fully closed, `CoverState` must return `Closed`. If an error condition arises while closing the cover, `CoverState` must return `Error`. @@ -76,7 +76,7 @@ def CloseCover(self): def HaltCover(self): """ Stops any present cover movement if the device has a cover and cover movement can be halted. - + Stops cover movement as soon as possible and sets `CoverState` to `Open`, `Closed`, or `Unknown` appropriately. """ logger.debug(f"ASCOMCoverCalibrator.HaltCover() called") @@ -125,11 +125,11 @@ def CalibratorState(self): def CoverState(self): """ The state of the cover device, if present, otherwise return `NotPresent`. (`CoverStatus `_) - + When the cover is changing the state must be `Moving`. If the device is unaware of the cover state, such as if hardware doesn't report the state and the cover was just powered on, the state must be `Unknown`. Users should be able to carry out commands like `OpenCover`, `CloseCover`, and `HaltCover` regardless of this unknown state. - + Returns ------- `CoverStatus` diff --git a/pyscope/observatory/ascom_device.py b/pyscope/observatory/ascom_device.py index b39cbba3..10dcf612 100644 --- a/pyscope/observatory/ascom_device.py +++ b/pyscope/observatory/ascom_device.py @@ -56,12 +56,12 @@ def Action(self, ActionName, *ActionParameters): # pragma: no cover The name of the action to invoke. Action names are either specified by the device driver or are well known names agreed upon and constructed by interested parties. ActionParameters : `list` The required parameters for the given action. Empty string if none are required. - + Returns ------- `str` The result of the action. The return value is dependent on the action being invoked and the representations are set by the driver author. - + Notes ----- See `SupportedActions` for a list of supported actions set up by the driver author. @@ -101,7 +101,7 @@ def CommandBool(self, Command, Raw): # pragma: no cover The command string to send to the device. Raw : `bool` If `True`, the command is set as-is. If `False`, protocol framing characters may be added onto the command. - + Returns ------- `bool` @@ -124,7 +124,7 @@ def CommandString(self, Command, Raw): # pragma: no cover The command string to send to the device. Raw : `bool` If `True`, the command is set as-is. If `False`, protocol framing characters may be added onto the command. - + Returns ------- `str` @@ -148,7 +148,7 @@ def Connected(self, value): def Description(self): """ The description of the device such as the manufacturer and model number. (`str`) - + Description should be limited to 64 characters so that it can be used in FITS headers. """ logger.debug(f"ASCOMDevice.Description property") @@ -158,7 +158,7 @@ def Description(self): def DriverInfo(self): """ Description and version information about this ASCOM driver. (`str`) - + Length of info can contain line endings and may be up to thousands of characters long. Version data and copyright data should be included. See `Description` for information on the device itself. diff --git a/pyscope/observatory/ascom_telescope.py b/pyscope/observatory/ascom_telescope.py index be65fa34..0e7b1b3d 100644 --- a/pyscope/observatory/ascom_telescope.py +++ b/pyscope/observatory/ascom_telescope.py @@ -52,7 +52,7 @@ def AxisRates(self, Axis): * 0 : Primary axis, Right Ascension or Azimuth. * 1 : Secondary axis, Declination or Altitude. * 2 : Tertiary axis, imager rotators. - + Returns ------- `IAxisRates `_ @@ -60,7 +60,7 @@ def AxisRates(self, Axis): collection of rates, including properties for both the number of rates, and the actual rates, and methods for returning an enumerator for the rates, and for disposing of the object as a whole. - + Notes ----- Rates must be absolute non-negative values only. Determining direction @@ -98,7 +98,7 @@ def DestinationSideOfPier(self, RightAscension, Declination): Right ascension coordinate (hours) of destination, not current, at current instant of time. Declination : `float` Declination coordinate (degrees) of destination, not current, at current instant of time. - + Returns ------- `PierSide `_ @@ -136,7 +136,7 @@ def MoveAxis(self, Axis, Rate): Rate : `float` Rate of motion in degrees per second. Positive values indicate motion in one direction, negative values in the opposite direction, and 0.0 stops motion by this method and resumes tracking motion. - + Notes ----- Rate must be within the values returned by `AxisRates`. Note that those values are absolute, @@ -357,7 +357,7 @@ def CanSlew(self): # pragma: no cover def CanSlewAltAz(self): # pragma: no cover """ Deprecated - + .. deprecated:: 0.1.1 ASCOM is deprecating this property. """ diff --git a/pyscope/observatory/cover_calibrator.py b/pyscope/observatory/cover_calibrator.py index f35ae166..5b5fffa0 100644 --- a/pyscope/observatory/cover_calibrator.py +++ b/pyscope/observatory/cover_calibrator.py @@ -8,7 +8,7 @@ class CoverCalibrator(ABC, metaclass=_DocstringInheritee): def __init__(self, *args, **kwargs): """ Abstract base class for a cover calibrator device. - + Defines the common interface for cover calibrator devices, including methods for controlling the cover and calibrator light. Subclasses must implement the abstract methods and properties in this class. @@ -38,7 +38,7 @@ def CalibratorOff(self): def CalibratorOn(self, Brightness): """ Turns the calibrator on at the specified brightness if the device has a calibrator. - + If the calibrator requires time to safely stabilise, `CalibratorState` must show that the calibrator is not ready yet. When the calibrator has stabilised, `CalibratorState` must show that the calibrator is ready and on. If a device has both cover and calibrator capabilities, the method may change `CoverState`. @@ -55,7 +55,7 @@ def CalibratorOn(self, Brightness): def CloseCover(self): """ Starts closing the cover if the device has a cover. - + While the cover is closing, `CoverState` must show that the cover is moving. When the cover is fully closed, `CoverState` must show that the cover is closed. If an error condition arises while closing the cover, it must be indicated in `CoverState`. @@ -66,7 +66,7 @@ def CloseCover(self): def HaltCover(self): """ Stops any present cover movement if the device has a cover and cover movement can be halted. - + Stops cover movement as soon as possible and sets `CoverState` to an appropriate value such as open or closed. """ pass @@ -87,7 +87,7 @@ def OpenCover(self): def Brightness(self): """ The current calibrator brightness in the range of 0 to `MaxBrightness`. (`int`) - + The brightness must be 0 if the `CalibratorState` is `Off`. """ pass @@ -110,7 +110,7 @@ def CalibratorState(self): def CoverState(self): """ The state of the cover device, if present, otherwise indicate that it does not exist. (`enum`) - + When the cover is changing the state must indicate that the cover is busy. If the device is unaware of the cover state, such as if hardware doesn't report the state and the cover was just powered on, it must indicate as such. Users should be able to carry out commands like `OpenCover`, `CloseCover`, and `HaltCover` regardless of this unknown state. @@ -122,7 +122,7 @@ def CoverState(self): def MaxBrightness(self): """ Brightness value that makes the calibrator produce the maximum illumination supported. (`int`) - + A value of 1 indicates that the calibrator can only be off or on. A value of any other X should indicate that the calibrator has X discreet brightness levels in addition to off (0). Value determined by the device manufacturer or driver author based on hardware capabilities. diff --git a/pyscope/observatory/device.py b/pyscope/observatory/device.py index 68cabda8..7f87c5ec 100644 --- a/pyscope/observatory/device.py +++ b/pyscope/observatory/device.py @@ -8,7 +8,7 @@ class Device(ABC, metaclass=_DocstringInheritee): def __init__(self, *args, **kwargs): """ Abstract base class for all deivce types. - + This class defines the common interface for all devices. Includes connection status and device name properties. Subclasses must implement the abstract methods and properties in this class. diff --git a/pyscope/observatory/dome.py b/pyscope/observatory/dome.py index c3188d40..4b4d5841 100644 --- a/pyscope/observatory/dome.py +++ b/pyscope/observatory/dome.py @@ -56,7 +56,7 @@ def OpenShutter(self): def Park(self): """ Move the dome to the park position along the dome azimuth axis. - + Sets the `AtPark` property to `True` after completion, and should raise an error if `Slaved`. """ pass @@ -186,7 +186,7 @@ def CanSetAltitude(self): def CanSetAzimuth(self): """ Whether the dome can set the azimuth, i.e. is it capable of `SlewToAzimuth`. (`bool`) - + In simpler terms, whether the dome is equipped with rotation control or not. """ pass @@ -208,7 +208,7 @@ def CanSetShutter(self): def CanSlave(self): """ Whether the dome can be slaved to a telescope, i.e. is it capable of `Slaved` states. (`bool`) - + This should only be `True` if the dome has its own slaving mechanism; a dome driver should not query a telescope driver directly. """ @@ -237,7 +237,7 @@ def ShutterStatus(self): def Slaved(self): """ Whether the dome is currently slaved to a telescope. (`bool`) - + During operations, set to `True` to enable hardware slewing, if capable, see `CanSlave`. """ pass diff --git a/pyscope/observatory/focuser.py b/pyscope/observatory/focuser.py index cbf10c10..6ea13123 100644 --- a/pyscope/observatory/focuser.py +++ b/pyscope/observatory/focuser.py @@ -71,7 +71,7 @@ def IsMoving(self): def MaxIncrement(self): """ The maximum increment size allowed by the focuser. (`int`) - + In other words, the maximum number of steps allowed in a single move operation. For most focuser devices, this value is the same as the `MaxStep` property, and is normally used to limit the increment size in relation to displaying in the host software. diff --git a/pyscope/observatory/html_observing_conditions.py b/pyscope/observatory/html_observing_conditions.py index 4bda46a0..4119dc7b 100755 --- a/pyscope/observatory/html_observing_conditions.py +++ b/pyscope/observatory/html_observing_conditions.py @@ -59,7 +59,7 @@ def __init__( The class is designed to be used with an HTML page that contains observing conditions data, sensor descriptions, and time since last update. - + Parameters ---------- url : `str` diff --git a/pyscope/observatory/maxim.py b/pyscope/observatory/maxim.py index 778b2897..899e61e3 100755 --- a/pyscope/observatory/maxim.py +++ b/pyscope/observatory/maxim.py @@ -96,7 +96,7 @@ def Run(self, exposure=10): ---------- exposure : `int`, default : 10, optional The exposure time in seconds for the autofocus routine. - + Returns ------- `bool` diff --git a/pyscope/observatory/observing_conditions.py b/pyscope/observatory/observing_conditions.py index 5c45350c..3b45814d 100644 --- a/pyscope/observatory/observing_conditions.py +++ b/pyscope/observatory/observing_conditions.py @@ -38,12 +38,12 @@ def SensorDescription(self, PropertyName): Even if the driver to the sensor isn't connected, if the sensor itself is implemented, this method must return a valid string, for example in case an application wants to determine what sensors are available. - + Parameters ---------- PropertyName : `str` The name of the property for which the sensor description is required. - + Returns ------- `str` diff --git a/pyscope/observatory/pwi4_autofocus.py b/pyscope/observatory/pwi4_autofocus.py index 8325bac0..45099909 100644 --- a/pyscope/observatory/pwi4_autofocus.py +++ b/pyscope/observatory/pwi4_autofocus.py @@ -36,7 +36,7 @@ def Run(self, *args, **kwargs): Variable length argument list. **kwargs : `dict` Arbitrary keyword arguments. - + Returns ------- `float` diff --git a/pyscope/observatory/pwi4_focuser.py b/pyscope/observatory/pwi4_focuser.py index f502593f..db4a5e17 100644 --- a/pyscope/observatory/pwi4_focuser.py +++ b/pyscope/observatory/pwi4_focuser.py @@ -11,7 +11,7 @@ class PWI4Focuser(Focuser): def __init__(self, host="localhost", port=8220): """ Focuser class for the PWI4 software platform. - + This class provides an interface to the PWI4 Focuser, and enables the user to access properties of the focuser such as position, temperature, and whether the focuser is moving, and methods to move the focuser, enable/disable the focuser, and check if the focuser is connected. diff --git a/pyscope/observatory/pwi_autofocus.py b/pyscope/observatory/pwi_autofocus.py index 11d28896..ae50ca9d 100755 --- a/pyscope/observatory/pwi_autofocus.py +++ b/pyscope/observatory/pwi_autofocus.py @@ -48,7 +48,7 @@ def Run(self, exposure=10, timeout=120): Exposure time in seconds for the autofocus routine. timeout : `float`, default : 120, optional Timeout in seconds for the autofocus routine to complete. - + Returns ------- `float` or `None` diff --git a/pyscope/observatory/rotator.py b/pyscope/observatory/rotator.py index 24e3aea5..78110361 100644 --- a/pyscope/observatory/rotator.py +++ b/pyscope/observatory/rotator.py @@ -117,7 +117,7 @@ def Position(self): def Reverse(self): """ Whether the rotator is in reverse mode. (`bool`) - + If `True`, the rotation and angular direction must be reversed for optics. """ pass @@ -138,7 +138,7 @@ def StepSize(self): def TargetPosition(self): """ The target angular position of the rotator, in degrees. (`float`) - + Upon a `Move` or `MoveAbsolute` call, this property is set to the position angle to which the rotator is moving. Value persists until another move call. """ diff --git a/pyscope/observatory/simulator_server.py b/pyscope/observatory/simulator_server.py index abc59750..e9c21853 100644 --- a/pyscope/observatory/simulator_server.py +++ b/pyscope/observatory/simulator_server.py @@ -24,12 +24,12 @@ def __init__(self, force_update=False): force_update : bool, default : `False`, optional If `True`, forces download of the ASCOM Alpaca Simulators server executable. If `False`, checks if the executable exists and skips download if it does. - + Raises ------ Exception If the host's operating system is not supported. - + Notes ----- The server executable is downloaded from the ASCOM Initiative GitHub repository found `here `_, diff --git a/pyscope/observatory/switch.py b/pyscope/observatory/switch.py index 6978e03c..a3bdf0d8 100644 --- a/pyscope/observatory/switch.py +++ b/pyscope/observatory/switch.py @@ -57,7 +57,7 @@ def GetSwitch(self, ID): def GetSwitchDescription(self, ID): """ The detailed description of the switch, to be used for features such as tooltips. (`str`) - + Parameters ---------- ID : `int` diff --git a/pyscope/observatory/telescope.py b/pyscope/observatory/telescope.py index 33e57673..d0d8ec76 100644 --- a/pyscope/observatory/telescope.py +++ b/pyscope/observatory/telescope.py @@ -27,7 +27,7 @@ def __init__(self, *args, **kwargs): def AbortSlew(self): """ Immediately stop any movement of the telescope due to any of the SlewTo*** calls. - + Does nothing if the telescope is not slewing. Tracking will return to its pre-slew state. """ pass @@ -47,14 +47,14 @@ def AxisRates(self, Axis): * 0 : Primary axis, usually corresponding to Right Ascension or Azimuth. * 1 : Secondary axis, usually corresponding to Declination or Altitude. * 2 : Tertiary axis, usually corresponding to imager rotators. - + Returns ------- `object` This object should be an iterable collection, including properties for both the number of rates, and the actual rates, and methods for returning an enumerator for the rates, and for disposing of the object as a whole. - + Notes ----- Rates must be absolute non-negative values only. Determining direction @@ -92,7 +92,7 @@ def DestinationSideOfPier(self, RightAscension, Declination): Right ascension coordinate (hours) of destination, not current, at current instant of time. Declination : `float` Declination coordinate (degrees) of destination, not current, at current instant of time. - + Returns ------- `enum` @@ -134,7 +134,7 @@ def MoveAxis(self, Axis, Rate): Rate : `float` Rate of motion in degrees per second. Positive values indicate motion in one direction, negative values in the opposite direction, and 0.0 stops motion by this method and resumes tracking motion. - + Notes ----- Rate must be within the values returned by `AxisRates`. Note that those values are absolute, @@ -432,7 +432,7 @@ def CanSetPark(self): def CanSetPierSide(self): """ Whether the telescope is capable of setting the side of the pier with `SideOfPier`, i.e. the mount can be forced to flip. (`bool`) - + This is only relevant for German equatorial mounts, as non-Germans do not have to be flipped and should therefore have `CansetPierSide` `False`. """ @@ -455,7 +455,7 @@ def CanSetTracking(self): def CanSlew(self): """ Whether the telescope is capable of slewing with `SlewToCoordinates`, or `SlewToTarget`. (`bool`) - + A `True` only guarantees that the synchronous slews are possible. Asynchronous slew capabilies are determined by `CanSlewAsync`. """ @@ -526,7 +526,7 @@ def Declination(self, value): def DeclinationRate(self): """ Declination tracking rate in arcseconds per second. (`float`) - + This, in conjunction with `RightAscensionRate`, is primarily used for offset tracking, tracking objects that move relatively slowly against the equatorial coordinate system. The supported range is telescope-dependent, but it can be expected to be a range diff --git a/pyscope/observatory/zwo_camera.py b/pyscope/observatory/zwo_camera.py index eefe0d45..41bb4770 100644 --- a/pyscope/observatory/zwo_camera.py +++ b/pyscope/observatory/zwo_camera.py @@ -1,8 +1,8 @@ import logging import pathlib +import platform import numpy as np -import zwoasi as asi from astropy.time import Time from .camera import Camera @@ -12,15 +12,19 @@ lib_path = pathlib.Path( r"C:\Users\MACRO\Downloads\ASI_Camera_SDK\ASI_Camera_SDK\ASI_Windows_SDK_V1.37\ASI SDK\lib\x64\ASICamera2.dll" ) -print(lib_path) - -asi.init(lib_path) class ZWOCamera(Camera): def __init__(self, device_number=0): logger.debug(f"ZWOCamera.__init__({device_number})") + if platform.system() != "Windows": + raise ImportError("ZWO ASI Camera SDK is only supported on Windows") + else: + import zwoasi as asi + + asi.init(lib_path) + self._device = asi.Camera(device_number) self._camera_info = self._device.get_camera_property() # Use minimum USB bandwidth permitted diff --git a/pyscope/reduction/astrometry_net_wcs.py b/pyscope/reduction/astrometry_net_wcs.py index dcf9c74b..c6116777 100644 --- a/pyscope/reduction/astrometry_net_wcs.py +++ b/pyscope/reduction/astrometry_net_wcs.py @@ -33,7 +33,7 @@ def astrometry_net_wcs_cli(filepath, **kwargs): Returns ------- bool - `True` if the WCS was successfully updated in the image header, `False` otherwise. + `True` if the WCS was successfully updated in the image header, `False` otherwise. Raises diff --git a/pyscope/reduction/avg_fits.py b/pyscope/reduction/avg_fits.py index 90e00f6f..93828671 100644 --- a/pyscope/reduction/avg_fits.py +++ b/pyscope/reduction/avg_fits.py @@ -110,7 +110,6 @@ def avg_fits_cli( If the input images have incompatible dimensions or data types. """ - if verbose == 2: logging.basicConfig(level=logging.DEBUG) elif verbose == 1: diff --git a/pyscope/reduction/calib_images.py b/pyscope/reduction/calib_images.py index 3337af45..c864bc40 100644 --- a/pyscope/reduction/calib_images.py +++ b/pyscope/reduction/calib_images.py @@ -122,8 +122,8 @@ def calib_images_cli( .. note:: - Ensure all required calibration frames (`bias`, `dark`, `flat`, `flat-dark`) are present in the specified - `--calib-dir`. If any calibration frames are missing, the process will fail, and the function will + Ensure all required calibration frames (`bias`, `dark`, `flat`, `flat-dark`) are present in the specified + `--calib-dir`. If any calibration frames are missing, the process will fail, and the function will return `0` without making changes to the images. Parameters @@ -159,7 +159,7 @@ def calib_images_cli( `int` Returns `0` if the calibration process fails due to missing calibration frames. `None` - Returns `None` on successful completion of all calibrations. + Returns `None` on successful completion of all calibrations. Raises ------ @@ -346,10 +346,11 @@ def calib_images_cli( logger.info("Done!") -''' + +""" Notes: Having an example set up of this function would be helpful. This would allow for me to know what the environment looks like when calling this function. -''' +""" calib_images = calib_images_cli.callback diff --git a/pyscope/reduction/ccd_calib.py b/pyscope/reduction/ccd_calib.py index bbaadce6..182ffd15 100644 --- a/pyscope/reduction/ccd_calib.py +++ b/pyscope/reduction/ccd_calib.py @@ -96,64 +96,64 @@ def ccd_calib_cli( pedestal=1000, ): """ -Calibrate astronomical images using master calibration frames. - -The `ccd_calib_cli` function applies bias, dark, and flat corrections to raw CCD or CMOS images -to produce calibrated versions. Calibrated images are saved with the suffix `_cal` appended -to the original filename unless the `--in-place` option is used, which overwrites the raw files. -The calibration process supports additional features like hot pixel removal and bad column correction. - -Parameters ----------- -fnames : `list` of `str` - List of filenames to calibrate. -dark_frame : `str`, optional - Path to master dark frame. If the camera type is `cmos`, the exposure time of the - dark frame must match the exposure time of the target images. -bias_frame : `str`, optional - Path to master bias frame. Ignored if the camera type is `cmos`. -flat_frame : `str`, optional - Path to master flat frame. The script assumes the master flat frame has already been - bias and dark corrected. The flat frame is normalized by the mean of the entire image - before being applied to the target images. -camera_type : `str`, optional - Camera type. Must be either `ccd` or `cmos`. Defaults to `"ccd"`. -astro_scrappy : `tuple` of (`int`, `int`), optional - Number of hot pixel removal iterations and estimated camera read noise (in - root-electrons per pixel per second). Defaults to `(1, 3)`. -bad_columns : `str`, optional - Comma-separated list of bad columns to fix by averaging the value of each pixel - in the adjacent column. Defaults to `""`. -in_place : `bool`, optional - If `True`, overwrites the input files with the calibrated images. Defaults to `False`. -pedestal : `int`, optional - Pedestal value to add to calibrated images after processing to prevent negative - pixel values. Defaults to `1000`. -verbose : `int`, optional - Verbosity level for logging output: - - `0`: Warnings only (default). - - `1`: Informational messages. - - `2`: Debug-level messages. - -Returns -------- -`None` - The function does not return any value. It writes calibrated images to disk. - -Raises ------- -`FileNotFoundError` - Raised if any of the specified input files do not exist. -`ValueError` - Raised if the calibration frames (e.g., `dark_frame`, `bias_frame`, `flat_frame`) do not - match the target images in terms of critical metadata, such as: - - Exposure time - - Binning (X and Y) - - Readout mode -`KeyError` - Raised if required header keywords (e.g., `IMAGETYP`, `FILTER`, `EXPTIME`, `GAIN`) are - missing from the calibration frames or the raw image files. -""" + Calibrate astronomical images using master calibration frames. + + The `ccd_calib_cli` function applies bias, dark, and flat corrections to raw CCD or CMOS images + to produce calibrated versions. Calibrated images are saved with the suffix `_cal` appended + to the original filename unless the `--in-place` option is used, which overwrites the raw files. + The calibration process supports additional features like hot pixel removal and bad column correction. + + Parameters + ---------- + fnames : `list` of `str` + List of filenames to calibrate. + dark_frame : `str`, optional + Path to master dark frame. If the camera type is `cmos`, the exposure time of the + dark frame must match the exposure time of the target images. + bias_frame : `str`, optional + Path to master bias frame. Ignored if the camera type is `cmos`. + flat_frame : `str`, optional + Path to master flat frame. The script assumes the master flat frame has already been + bias and dark corrected. The flat frame is normalized by the mean of the entire image + before being applied to the target images. + camera_type : `str`, optional + Camera type. Must be either `ccd` or `cmos`. Defaults to `"ccd"`. + astro_scrappy : `tuple` of (`int`, `int`), optional + Number of hot pixel removal iterations and estimated camera read noise (in + root-electrons per pixel per second). Defaults to `(1, 3)`. + bad_columns : `str`, optional + Comma-separated list of bad columns to fix by averaging the value of each pixel + in the adjacent column. Defaults to `""`. + in_place : `bool`, optional + If `True`, overwrites the input files with the calibrated images. Defaults to `False`. + pedestal : `int`, optional + Pedestal value to add to calibrated images after processing to prevent negative + pixel values. Defaults to `1000`. + verbose : `int`, optional + Verbosity level for logging output: + - `0`: Warnings only (default). + - `1`: Informational messages. + - `2`: Debug-level messages. + + Returns + ------- + `None` + The function does not return any value. It writes calibrated images to disk. + + Raises + ------ + `FileNotFoundError` + Raised if any of the specified input files do not exist. + `ValueError` + Raised if the calibration frames (e.g., `dark_frame`, `bias_frame`, `flat_frame`) do not + match the target images in terms of critical metadata, such as: + - Exposure time + - Binning (X and Y) + - Readout mode + `KeyError` + Raised if required header keywords (e.g., `IMAGETYP`, `FILTER`, `EXPTIME`, `GAIN`) are + missing from the calibration frames or the raw image files. + """ if verbose == 2: logging.basicConfig(level=logging.DEBUG) @@ -559,7 +559,10 @@ def ccd_calib_cli( else: logger.info(f"Writing calibrated image to {fname}") fits.writeto( - str(fname).split(".")[:-1][0] + "_cal.fts", cal_image, hdr, overwrite=True + str(fname).split(".")[:-1][0] + "_cal.fts", + cal_image, + hdr, + overwrite=True, ) logger.info("Done!") diff --git a/pyscope/reduction/fitslist.py b/pyscope/reduction/fitslist.py index 72e6d8bb..c0ad7ece 100644 --- a/pyscope/reduction/fitslist.py +++ b/pyscope/reduction/fitslist.py @@ -167,8 +167,8 @@ def fitslist_cli( """ List FITS files and their properties. - This command-line tool extracts metadata from FITS files in a specified directory - and presents it in a structured table. Users can filter files by various criteria + This command-line tool extracts metadata from FITS files in a specified directory + and presents it in a structured table. Users can filter files by various criteria and save the results to a CSV file. Parameters diff --git a/pyscope/reduction/pinpoint_wcs.py b/pyscope/reduction/pinpoint_wcs.py index 00c25ab2..10e81aed 100644 --- a/pyscope/reduction/pinpoint_wcs.py +++ b/pyscope/reduction/pinpoint_wcs.py @@ -41,7 +41,7 @@ def Solve( ): """ Solve the WCS of a FITS image using PinPoint. - + This method uses PinPoint to attach to a FITS file, solve for its astrometry based on provided or inferred RA/DEC and scale estimates, and update the FITS file with the resulting WCS solution. diff --git a/pyscope/reduction/process_images.py b/pyscope/reduction/process_images.py index 12f7e18e..982a8ded 100644 --- a/pyscope/reduction/process_images.py +++ b/pyscope/reduction/process_images.py @@ -119,7 +119,7 @@ def store_image(img, dest, update_db=False): """ Archives a FITS image in a long-term storage directory. - Copies the file to the specified directory if the target does not exist or + Copies the file to the specified directory if the target does not exist or is older than the source. Logs errors during the copy process. Parameters @@ -152,13 +152,13 @@ def store_image(img, dest, update_db=False): def process_image(img): """ - Processes a single FITS image by calibrating and classifying it based on the outcome. + Processes a single FITS image by calibrating and classifying it based on the outcome. - The function begins by reading the FITS file data and header. If the image has not - already been calibrated, it applies calibration using the `calib_images` function. - Once calibrated, the image is moved to a `reduced` directory if the calibration is - successful or a `failed` directory if calibration fails. Regardless of the outcome, - the image is archived in a long-term storage directory for later use. Finally, the + The function begins by reading the FITS file data and header. If the image has not + already been calibrated, it applies calibration using the `calib_images` function. + Once calibrated, the image is moved to a `reduced` directory if the calibration is + successful or a `failed` directory if calibration fails. Regardless of the outcome, + the image is archived in a long-term storage directory for later use. Finally, the image is removed from the landing directory to complete the process. Parameters diff --git a/pyscope/reduction/reduce_calibration_set.py b/pyscope/reduction/reduce_calibration_set.py index 26501aed..b52348d4 100644 --- a/pyscope/reduction/reduce_calibration_set.py +++ b/pyscope/reduction/reduce_calibration_set.py @@ -71,9 +71,9 @@ def reduce_calibration_set_cli( """ Reduce a calibration set to master frames. - This function processes a calibration set of FITS images, typically used in astronomy, - to create master bias, dark, and flat calibration frames. The input calibration set - should be a directory containing subdirectories for bias, dark, and flat frames. + This function processes a calibration set of FITS images, typically used in astronomy, + to create master bias, dark, and flat calibration frames. The input calibration set + should be a directory containing subdirectories for bias, dark, and flat frames. The resulting master calibration frames are saved in the same directory. Parameters @@ -83,13 +83,13 @@ def reduce_calibration_set_cli( camera : `str`, optional Camera type, either `"ccd"` or `"cmos"`. Defaults to `"ccd"`. mode : `str`, optional - Method to use for averaging images, either `"median"` or `"average"`. + Method to use for averaging images, either `"median"` or `"average"`. Defaults to `"median"`. pre_normalize : `bool`, optional - Pre-normalize flat images before combining. This option is useful + Pre-normalize flat images before combining. This option is useful for sky flats. Defaults to `True`. verbose : `int`, optional - Level of verbosity for output logs. Use higher values for more detailed output. + Level of verbosity for output logs. Use higher values for more detailed output. Defaults to `0`. Raises diff --git a/pyscope/telrun/schedtel.py b/pyscope/telrun/schedtel.py index b8c14490..7be79dc6 100644 --- a/pyscope/telrun/schedtel.py +++ b/pyscope/telrun/schedtel.py @@ -286,9 +286,9 @@ def schedtel_cli( """ Schedule observations for an observatory. - This function creates an observation schedule based on input catalogs or - queues. It applies constraints such as airmass, elevation, and moon separation, - and outputs a schedule file. Optionally, it generates visualizations like Gantt + This function creates an observation schedule based on input catalogs or + queues. It applies constraints such as airmass, elevation, and moon separation, + and outputs a schedule file. Optionally, it generates visualizations like Gantt charts or sky charts. Parameters @@ -297,7 +297,7 @@ def schedtel_cli( Path to a `.sch` file or `.cat` file containing observation blocks. If not provided, defaults to `schedule.cat` in `$TELHOME/schedules/`. queue : `str`, optional - Path to a queue file (`.ecsv`) with observation requests. If a catalog is provided, + Path to a queue file (`.ecsv`) with observation requests. If a catalog is provided, entries from the catalog are added to the queue. add_only : `bool`, optional If True, adds catalog entries to the queue without scheduling. @@ -324,13 +324,13 @@ def schedtel_cli( resolution : `float`, optional Time resolution for scheduling in seconds. Defaults to `5`. name_format : `str`, optional - Format string for scheduled image names. Defaults to + Format string for scheduled image names. Defaults to `"{code}_{target}_{filter}_{exposure}s_{start_time}"`. filename : `str`, optional - Output file name. If not specified, defaults to a file named with the UTC date + Output file name. If not specified, defaults to a file named with the UTC date of the first observation in the current working directory. telrun : `bool`, optional - If True, places the output file in the `$TELRUN_EXECUTE` directory or a default + If True, places the output file in the `$TELRUN_EXECUTE` directory or a default `schedules/execute/` directory. plot : `int`, optional Type of plot to generate (1: Gantt, 2: Airmass, 3: Sky). Defaults to no plot. diff --git a/requirements.txt b/requirements.txt index 8765143f..32a10b27 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,16 +7,17 @@ ccdproc == 2.4.2 click == 8.1.7 cmcrameri == 1.9 markdown == 3.6 -numpy == 2.1.2 matplotlib == 3.9.1 +numpy == 2.1.2 oschmod == 0.3.12 paramiko == 3.5.0 photutils == 1.13.0 -pywin32 == 308;platform_system=='Windows' prettytable == 3.11.0 +pywin32 == 308;platform_system=='Windows' scikit-image == 0.24.0 scipy == 1.14.1 smplotlib == 0.0.9 timezonefinder == 6.5.2 tqdm == 4.66.5 # twirl == 0.4.2 +zwoasi == 0.2.0;platform_system=='Windows' From 7c9e1561fd41e1233d7fbf42560efce302e34d10 Mon Sep 17 00:00:00 2001 From: WGolay Date: Fri, 20 Dec 2024 20:22:40 -0500 Subject: [PATCH 60/60] Fix scripts --- pyscope/bin/scripts/start_sync_manager | 3 + pyscope/bin/scripts/start_sync_manager.bat | 33 ++++++++ pyscope/bin/scripts/start_telrun | 3 + pyscope/bin/scripts/start_telrun.bat | 33 ++++++++ pyscope/scheduling/_block.py | 96 +++++++++++----------- requirements.txt | 1 + 6 files changed, 121 insertions(+), 48 deletions(-) create mode 100644 pyscope/bin/scripts/start_sync_manager create mode 100644 pyscope/bin/scripts/start_sync_manager.bat create mode 100644 pyscope/bin/scripts/start_telrun create mode 100644 pyscope/bin/scripts/start_telrun.bat diff --git a/pyscope/bin/scripts/start_sync_manager b/pyscope/bin/scripts/start_sync_manager new file mode 100644 index 00000000..cb503cb6 --- /dev/null +++ b/pyscope/bin/scripts/start_sync_manager @@ -0,0 +1,3 @@ +#!/home/$USER/anaconda3/python + +start_syncfiles diff --git a/pyscope/bin/scripts/start_sync_manager.bat b/pyscope/bin/scripts/start_sync_manager.bat new file mode 100644 index 00000000..4ca3a92a --- /dev/null +++ b/pyscope/bin/scripts/start_sync_manager.bat @@ -0,0 +1,33 @@ +@echo OFF +rem How to run a Python script in a given conda environment from a batch file. + +rem It doesn't require: +rem - conda to be in the PATH +rem - cmd.exe to be initialized with conda init + +rem Define here the path to your conda installation +set CONDAPATH=C:\Users\%USERNAME%\Anaconda3\ +rem Define here the name of the environment +set ENVNAME=base + +rem The following command activates the base environment. +rem call C:\ProgramData\Miniconda3\Scripts\activate.bat C:\ProgramData\Miniconda3 +if %ENVNAME%==base (set ENVPATH=%CONDAPATH%) else (set ENVPATH=%CONDAPATH%\envs\%ENVNAME%) + +rem Activate the conda environment +rem Using call is required here, see: https://stackoverflow.com/questions/24678144/conda-environments-and-bat-files +call %CONDAPATH%\Scripts\activate.bat %ENVPATH% + +rem Run a python script in that environment +python start_syncfiles + +rem Deactivate the environment +call conda deactivate + +rem If conda is directly available from the command line then the following code works. +rem call activate someenv +rem python script.py +rem conda deactivate + +rem One could also use the conda run command +rem conda run -n someenv python script.py diff --git a/pyscope/bin/scripts/start_telrun b/pyscope/bin/scripts/start_telrun new file mode 100644 index 00000000..5e52deee --- /dev/null +++ b/pyscope/bin/scripts/start_telrun @@ -0,0 +1,3 @@ +#!/home/$USER/anaconda3/python + +start_telrun diff --git a/pyscope/bin/scripts/start_telrun.bat b/pyscope/bin/scripts/start_telrun.bat new file mode 100644 index 00000000..1cc05a73 --- /dev/null +++ b/pyscope/bin/scripts/start_telrun.bat @@ -0,0 +1,33 @@ +@echo OFF +rem How to run a Python script in a given conda environment from a batch file. + +rem It doesn't require: +rem - conda to be in the PATH +rem - cmd.exe to be initialized with conda init + +rem Define here the path to your conda installation +set CONDAPATH=C:\Users\%USERNAME%\Anaconda3\ +rem Define here the name of the environment +set ENVNAME=base + +rem The following command activates the base environment. +rem call C:\ProgramData\Miniconda3\Scripts\activate.bat C:\ProgramData\Miniconda3 +if %ENVNAME%==base (set ENVPATH=%CONDAPATH%) else (set ENVPATH=%CONDAPATH%\envs\%ENVNAME%) + +rem Activate the conda environment +rem Using call is required here, see: https://stackoverflow.com/questions/24678144/conda-environments-and-bat-files +call %CONDAPATH%\Scripts\activate.bat %ENVPATH% + +rem Run a python script in that environment +python start_telrun + +rem Deactivate the environment +call conda deactivate + +rem If conda is directly available from the command line then the following code works. +rem call activate someenv +rem python script.py +rem conda deactivate + +rem One could also use the conda run command +rem conda run -n someenv python script.py diff --git a/pyscope/scheduling/_block.py b/pyscope/scheduling/_block.py index b1f1c2f5..accc8960 100644 --- a/pyscope/scheduling/_block.py +++ b/pyscope/scheduling/_block.py @@ -13,60 +13,60 @@ class _Block: def __init__(self, config, observer, name="", description="", **kwargs): """ - A class to represent a time range in the schedule. + Represents a `~astropy.time.Time` range in a `~pyscope.scheduling.Schedule` attributed to an `~pyscope.scheduling.Observer`. - A `~pyscope.telrun._Block` are used to represent a time range in the schedule. A `~pyscope.telrun._Block` can be - used to represent allocated time with a `~pyscope.telrun.ScheduleBlock` or unallocated time with a - `~pyscope.telrun.UnallocatedBlock`. The `~pyscope.telrun._Block` class is a base class that should not be instantiated - directly. Instead, use the `~pyscope.telrun.ScheduleBlock` or `~pyscope.telrun.UnallocatedBlock` subclasses. + A `~pyscope.scheduling._Block` can be used to represent allocated time with a `~pyscope.scheduling.ScheduleBlock` + or unallocated time with an `~pyscope.scheduling.UnallocatedBlock`. The `~pyscope.scheduling._Block` class is a base + class that should not be instantiated directly. Instead, use the `~pyscope.scheduling.ScheduleBlock` or + `~pyscope.scheduling.UnallocatedBlock` subclasses. Parameters ---------- configuration : `~pyscope.telrun.InstrumentConfiguration`, required - The `~pyscope.telrun.InstrumentConfiguration` to use for the `~pyscope.telrun._Block`. This `~pyscope.telrun.InstrumentConfiguration` will be - used to set the telescope's `~pyscope.telrun.InstrumentConfiguration` at the start of the `~pyscope.telrun._Block` and - will act as the default `~pyscope.telrun.InstrumentConfiguration` for all `~pyscope.telrun.Field` objects in the - `~pyscope.telrun._Block` if a `~pyscope.telrun.InstrumentConfiguration` has not been provided. If a `~pyscope.telrun.Field` + The `~pyscope.telrun.InstrumentConfiguration` to use for the `~pyscope.scheduling._Block`. This `~pyscope.telrun.InstrumentConfiguration` will be + used to set the telescope's `~pyscope.telrun.InstrumentConfiguration` at the start of the `~pyscope.scheduling._Block` and + will act as the default `~pyscope.telrun.InstrumentConfiguration` for all `~pyscope.scheduling.Field` objects in the + `~pyscope.scheduling._Block` if a `~pyscope.telrun.InstrumentConfiguration` has not been provided. If a `~pyscope.scheduling.Field` has a different `~pyscope.telrun.InstrumentConfiguration`, it will override the block `~pyscope.telrun.InstrumentConfiguration` for the - duration of the `~pyscope.telrun.Field`. + duration of the `~pyscope.scheduling.Field`. - observer : `~pyscope.telrun.Observer`, required - Associate this `~pyscope.telrun._Block` with an `~pyscope.telrun.Observer`. The `~pyscope.telrun.Observer` is a + observer : `~pyscope.scheduling.Observer`, required + Associate this `~pyscope.scheduling._Block` with an `~pyscope.scheduling.Observer`. The `~pyscope.scheduling.Observer` is a bookkeeping object for an `~pyscope.observatory.Observatory` with multiple users/user groups. name : `str`, default : "" - A user-defined name for the `~pyscope.telrun._Block`. This parameter does not change - the behavior of the `~pyscope.telrun._Block`, but it can be useful for identifying the - `~pyscope.telrun._Block` in a schedule. + A user-defined name for the `~pyscope.scheduling._Block`. This parameter does not change + the behavior of the `~pyscope.scheduling._Block`, but it can be useful for identifying the + `~pyscope.scheduling._Block` in a schedule. description : `str`, default : "" - A user-defined description for the `~pyscope.telrun._Block`. Similar to the `name` - parameter, this parameter does not change the behavior of the `~pyscope.telrun._Block`. + A user-defined description for the `~pyscope.scheduling._Block`. Similar to the `name` + parameter, this parameter does not change the behavior of the `~pyscope.scheduling._Block`. **kwargs : `dict`, default : {} A dictionary of keyword arguments that can be used to store additional information - about the `~pyscope.telrun._Block`. This information can be used to store any additional + about the `~pyscope.scheduling._Block`. This information can be used to store any additional information that is not covered by the `configuration`, `name`, or `description` parameters. See Also -------- - pyscope.telrun.ScheduleBlock : A subclass of `~pyscope.telrun._Block` that is used to schedule `~pyscope.telrun.Field` objects - in a `~pyscope.telrun.Schedule`. - pyscope.telrun.UnallocatedBlock : A subclass of `~pyscope.telrun._Block` that is used to represent unallocated time in a - `~pyscope.telrun.Schedule`. + pyscope.scheduling.ScheduleBlock : A subclass of `~pyscope.scheduling._Block` that is used to schedule `~pyscope.scheduling.Field` objects + in a `~pyscope.scheduling.Schedule`. + pyscope.scheduling.UnallocatedBlock : A subclass of `~pyscope.scheduling._Block` that is used to represent unallocated time in a + `~pyscope.scheduling.Schedule`. pyscope.telrun.InstrumentConfiguration : A class that represents the configuration of the telescope. - pyscope.telrun.Field : A class that represents a field to observe. + pyscope.scheduling.Field : A class that represents a field to observe. """ logger.debug( "_Block(config=%s, observer=%s, name=%s, description=%s, **kwargs=%s)" % (config, observer, name, description, kwargs) ) - self.config = config + self._uuid = uuid4() self.observer = observer self.name = name self.description = description + self.config = config self.kwargs = kwargs - self._uuid = uuid4() self._start_time = None self._end_time = None @@ -77,7 +77,7 @@ def from_string( cls, string, config=None, observer=None, name="", description="", **kwargs ): """ - Create a new `~pyscope.telrun._Block` from a string representation. Additional arguments can be provided to override + Create a new `~pyscope.scheduling._Block` from a string representation. Additional arguments can be provided to override the parsed values. Parameters @@ -86,7 +86,7 @@ def from_string( config : `~pyscope.telrun.InstrumentConfiguration`, default: `None` - observer : `~pyscope.telrun.Observer`, default: `None` + observer : `~pyscope.scheduling.Observer`, default: `None` name : `str`, default : "" @@ -96,7 +96,7 @@ def from_string( Returns ------- - block : `~pyscope.telrun._Block` + block : `~pyscope.scheduling._Block` """ logger.debug( @@ -159,7 +159,7 @@ def from_string( def __str__(self): """ - A `str` representation of the `~pyscope.telrun._Block`. + A `str` representation of the `~pyscope.scheduling._Block`. Returns ------- @@ -181,7 +181,7 @@ def __str__(self): def __repr__(self): """ - A `str` representation of the `~pyscope.telrun._Block`. + A `str` representation of the `~pyscope.scheduling._Block`. Returns ------- @@ -193,7 +193,7 @@ def __repr__(self): @property def config(self): """ - The default `~pyscope.telrun.InstrumentConfiguration` for the `~pyscope.telrun._Block`. + The default `~pyscope.telrun.InstrumentConfiguration` for the `~pyscope.scheduling._Block`. Returns ------- @@ -205,7 +205,7 @@ def config(self): @config.setter def config(self, value): """ - The default `~pyscope.telrun.InstrumentConfiguration` for the `~pyscope.telrun._Block`. + The default `~pyscope.telrun.InstrumentConfiguration` for the `~pyscope.scheduling._Block`. Parameters ---------- @@ -228,11 +228,11 @@ def config(self, value): @property def observer(self): """ - The `~pyscope.telrun.Observer` associated with the `~pyscope.telrun._Block`. + The `~pyscope.scheduling.Observer` associated with the `~pyscope.scheduling._Block`. Returns ------- - observer : `~pyscope.telrun.Observer` + observer : `~pyscope.scheduling.Observer` """ logger.debug("_Block().observer == %s" % self._observer) return self._observer @@ -240,11 +240,11 @@ def observer(self): @observer.setter def observer(self, value): """ - The `~pyscope.telrun.Observer` associated with the `~pyscope.telrun._Block`. + The `~pyscope.scheduling.Observer` associated with the `~pyscope.scheduling._Block`. Parameters ---------- - value : `~pyscope.telrun.Observer` + value : `~pyscope.scheduling.Observer` """ logger.debug("_Block().observer = %s" % value) if ( @@ -262,7 +262,7 @@ def observer(self, value): @property def name(self): """ - A user-defined `str` name for the `~pyscope.telrun._Block`. + A user-defined `str` name for the `~pyscope.scheduling._Block`. Returns ------- @@ -275,7 +275,7 @@ def name(self): @name.setter def name(self, value): """ - A user-defined `str` name for the `~pyscope.telrun._Block`. + A user-defined `str` name for the `~pyscope.scheduling._Block`. Parameters ---------- @@ -291,7 +291,7 @@ def name(self, value): @property def description(self): """ - A user-defined `str` description for the `~pyscope.telrun._Block`. + A user-defined `str` description for the `~pyscope.scheduling._Block`. Returns ------- @@ -303,7 +303,7 @@ def description(self): @description.setter def description(self, value): """ - A user-defined `str` description for the `~pyscope.telrun._Block`. + A user-defined `str` description for the `~pyscope.scheduling._Block`. Parameters ---------- @@ -320,7 +320,7 @@ def description(self, value): @property def kwargs(self): """ - Additional user-defined keyword arguments in a `dict` for the `~pyscope.telrun._Block`. + Additional user-defined keyword arguments in a `dict` for the `~pyscope.scheduling._Block`. Returns ------- @@ -333,7 +333,7 @@ def kwargs(self): @kwargs.setter def kwargs(self, value): """ - Additional user-defined keyword arguments for the `~pyscope.telrun._Block`. + Additional user-defined keyword arguments for the `~pyscope.scheduling._Block`. Parameters ---------- @@ -350,12 +350,12 @@ def kwargs(self, value): @property def ID(self): """ - A `~uuid.UUID` that uniquely identifies the `~pyscope.telrun._Block`. + A `~uuid.UUID` that uniquely identifies the `~pyscope.scheduling._Block`. Returns ------- ID : `~uuid.UUID` - The unique identifier for the `~pyscope.telrun._Block`. + The unique identifier for the `~pyscope.scheduling._Block`. """ logger.debug("_Block().ID == %s" % self._uuid) return self._uuid @@ -363,12 +363,12 @@ def ID(self): @property def start_time(self): """ - The `~astropy.time.Time` that represents the start of the `~pyscope.telrun._Block`. + The `~astropy.time.Time` that represents the start of the `~pyscope.scheduling._Block`. Returns ------- start_time : `astropy.time.Time` - The start time of the `~pyscope.telrun._Block`. + The start time of the `~pyscope.scheduling._Block`. """ logger.debug("_Block().start_time == %s" % self._start_time) return self._start_time @@ -376,12 +376,12 @@ def start_time(self): @property def end_time(self): """ - The `~astropy.time.Time` that represents the end of the `~pyscope.telrun._Block`. + The `~astropy.time.Time` that represents the end of the `~pyscope.scheduling._Block`. Returns ------- end_time : `astropy.time.Time` - The end time of the `~pyscope.telrun._Block`. + The end time of the `~pyscope.scheduling._Block`. """ logger.debug("_Block().end_time == %s" % self._end_time) return self._end_time diff --git a/requirements.txt b/requirements.txt index 32a10b27..c6b0c492 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,6 +10,7 @@ markdown == 3.6 matplotlib == 3.9.1 numpy == 2.1.2 oschmod == 0.3.12 +pandas == 2.2.3 paramiko == 3.5.0 photutils == 1.13.0 prettytable == 3.11.0