Skip to content

Commit

Permalink
Merge branch 'main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
cainrinkMac authored Feb 5, 2024
2 parents 909041e + f88cfd0 commit e71e2a2
Show file tree
Hide file tree
Showing 42 changed files with 6,712 additions and 3,584 deletions.
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
}
}
}

},
"features": {
"ghcr.io/devcontainers/features/github-cli:1": {}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/pypi-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install dependencies
Expand All @@ -34,4 +34,4 @@ jobs:
- name: Build package
run: python -m build
- name: pypi-publish
uses: pypa/gh-action-pypi-publish@v1.8.10
uses: pypa/gh-action-pypi-publish@v1.8.11
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ instance/

# Sphinx documentation
docs/_build/
docs/source/user_guide/observatory_info.csv

# PyBuilder
target/
Expand Down Expand Up @@ -140,3 +141,4 @@ dmypy.json
*OmniSim*
!coverage.xml
docs/source/api/auto_api/
pgHardware
4 changes: 2 additions & 2 deletions .readthedocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ sphinx:
fail_on_warning: true

# Optionally build your docs in additional formats such as PDF and ePub
formats:
- pdf
# formats:
# - pdf

# Optional but recommended, declare the Python requirements required
# to build your documentation
Expand Down
3 changes: 3 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ pyscope

|License| |Zenodo| |PyPI Version| |PyPI Python Versions| |PyPI Downloads| |Astropy| |GitHub CI| |Code Coverage| |Documentation Status| |Codespaces Status| |pre-commit| |Black| |isort| |Donate|

.. image:: https://github.com/WWGolay/pyscope/blob/main/docs/source/images/pyscope_logo_white.png
:alt: pyscope logo

This is the repository for `pyscope <https://pyscope.readthedocs.io/en/latest/>`_,
a pure-Python package for robotic scheduling, operation, and control of small
optical telescopes.
Expand Down
6,039 changes: 3,389 additions & 2,650 deletions coverage.xml

Large diffs are not rendered by default.

File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
11 changes: 11 additions & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
from packaging.version import parse
from sphinx_astropy.conf.v2 import *

sys.path.insert(0, pathlib.Path(__file__).parents[0].resolve().as_posix())

import headerCSVGenerator

sys.path.insert(0, pathlib.Path(__file__).parents[2].resolve().as_posix())

import pyscope
Expand Down Expand Up @@ -57,6 +61,13 @@

extensions = list(map(lambda x: x.replace("viewcode", "linkcode"), extensions))

# Generate CSV for header info
print("Generating CSV for header info...")
targetPath = os.path.join(
os.path.dirname(__file__), "user_guide", "observatory_info.csv"
)
headerCSVGenerator.HeaderCSVGenerator().generate_csv(targetPath)


def linkcode_resolve(domain, info):
"""
Expand Down
118 changes: 118 additions & 0 deletions docs/source/headerCSVGenerator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import ast
import csv
import inspect
import re

from pyscope.observatory import Observatory


class HeaderCSVGenerator:
"""Generates a CSV file containing the header information for the Observatory class.
The CSV file contains the following columns:
- Header Key: The key of the header
- Header Value: The value of the header
- Header Description: The description of the header
The CSV file is generated by parsing the Observatory class for the info dictionaries
and then combining them into a master dictionary. The master dictionary is then
output to a CSV file.
"""

def __init__(self):
pass

def get_info_dicts(self):
descriptors = inspect.getmembers(
Observatory, predicate=inspect.isdatadescriptor
)
info = []
for descriptor in descriptors:
if "info" in descriptor[0]:
info.append(descriptor)

source_list = []
for descriptor in info:
source_list.append(inspect.getsource(descriptor[1].fget))

# Split source into lines, remove tabs
source_lines = []
for source in source_list:
source_lines.append(source.split("\n"))

# Remove leading whitespace
stripped_lines = []
for source in source_lines:
for line in source:
stripped_lines.append(line.lstrip())

# Return parts of source_list that contain 'info = {...}'
info_dict = []
for property in source_list:
# Use regex to find info = {[^}]} and add it to info_dict
info_dict.append(re.findall(r"info = {[^}]*}", property)[0])

# Remove 'info = ' from each string
for i, property in enumerate(info_dict):
info_dict[i] = property[7:]

# Encase any references to self. in quotes
for i, property in enumerate(info_dict):
info_dict[i] = re.sub(r"self\.([a-zA-Z0-9_.\[\]]+)", r'"\1"', property)

# Find references to str()
for i, property in enumerate(info_dict):
info_dict[i] = re.sub(r"(str\(([a-zA-Z\"\_\.\[\]]+)\))", r"\2", property)

# Replace any references to self.etc. with None
for i, property in enumerate(info_dict):
# Use regex "\(\s+(.*?\],)"gm
# info_dict[i] = re.sub(r'\(\\n\s+(.*?\],)', 'None', property)
group = re.findall(r"\(\n\s+([\s\S]*?\],)", property)
# replace the group with None
if group:
info_dict[i] = property.replace(group[0], "None,")

# Enclose any parts with (sep="dms") in quotes
for i, property in enumerate(info_dict):
info_dict[i] = re.sub(
r"(\"\S+\(sep=\"dms\"\))",
lambda m: '"' + m.group(1).replace('"', " ") + '"',
property,
)

# Remove any parts matching \% i(?=\)) (or replace with "")
for i, property in enumerate(info_dict):
info_dict[i] = re.sub(r"( \% i(?=\)))", "", property)

# Enclose in quotes any parts matching 'not \"\S+\" is None'
for i, property in enumerate(info_dict):
info_dict[i] = re.sub(
r"(not \"\S+\" is None)",
lambda m: '"' + m.group(1).replace('"', " ") + '"',
property,
)

# Pass each info_dict through ast.literal_eval to convert to dictionary
info_dict_parsed = []
for info in info_dict:
info_dict_parsed.append(ast.literal_eval(info))

return info_dict_parsed

def generate_csv(self, filename):
info_dicts = self.get_info_dicts()
# Add each dictionary to make a master dictionary
master_dict = {}
for info in info_dicts:
master_dict.update(info)
# Output to csv in the format key, value, description
# key is the key of the dictionary
# value is the first part of the tuple (the value)
# description is the second part of the tuple (the description)
with open(filename, "w", newline="") as csv_file:
writer = csv.writer(csv_file)
# Write header
writer.writerow(["Header Key", "Header Value", "Header Description"])
for key, value in master_dict.items():
writer.writerow([key, value[0], value[1]])
Binary file removed docs/source/images/pyscope_logo_small.png
Binary file not shown.
Binary file removed docs/source/images/pyscope_logo_small_gray.png
Binary file not shown.
15 changes: 15 additions & 0 deletions docs/source/user_guide/header.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Header
======

This is a page listing all potential header keywords for an `Observatory` object.

.. Note::
This is auto-generated from the `info` dictionaries in the `Observatory` class. Some
of the information is not available for all observatories, and some header values may
contain nonsense due to the auto-generation script.

.. csv-table:: Sample Header
:file: observatory_info.csv
:widths: 4, 6, 10
:header-rows: 1

1 change: 1 addition & 0 deletions docs/source/user_guide/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ User Guide
:maxdepth: 2

examples
header
logging
config
help
2 changes: 1 addition & 1 deletion pyscope/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@

import logging

__version__ = "0.1.5"
__version__ = "0.1.6"

from . import utils
from . import observatory
Expand Down
80 changes: 77 additions & 3 deletions pyscope/observatory/ascom_camera.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import logging

import numpy as np
from astropy.time import Time

from .ascom_device import ASCOMDevice
from .camera import Camera

Expand All @@ -15,17 +18,54 @@ def __init__(self, identifier, alpaca=False, device_number=0, protocol="http"):
device_number=device_number,
protocol=protocol,
)
self._last_exposure_duration = None
self._last_exposure_start_time = None
self._image_data_type = None
self._DoTranspose = True
self._camera_time = True

def AbortExposure(self):
logger.debug(f"ASCOMCamera.AbortExposure() called")
self._device.AbortExposure()

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 <https://ascom-standards.org/Help/Developer/html/P_ASCOM_DriverAccess_Camera_MaxADU.htm>`_
"""
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")
self._device.PulseGuide(Direction, Duration)

def StartExposure(self, Duration, Light):
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):
Expand Down Expand Up @@ -85,6 +125,11 @@ def CameraYSize(self):
logger.debug(f"ASCOMCamera.CameraYSize property called")
return self._device.CameraYSize

@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")
Expand Down Expand Up @@ -213,7 +258,14 @@ def HeatSinkTemperature(self):
@property
def ImageArray(self):
logger.debug(f"ASCOMCamera.ImageArray property called")
return self._device.ImageArray
img_array = self._device.ImageArray
# Convert to numpy array and check if it is the correct data type
if self._image_data_type is None:
self.SetImageDataType()
img_array = np.array(img_array, dtype=self._image_data_type)
if self._DoTranspose:
img_array = np.transpose(img_array)
return img_array

@property
def ImageReady(self):
Expand All @@ -228,12 +280,34 @@ def IsPulseGuiding(self):
@property
def LastExposureDuration(self):
logger.debug(f"ASCOMCamera.LastExposureDuration property called")
return self._device.LastExposureDuration
last_exposure_duration = self._device.LastExposureDuration
if last_exposure_duration is None or last_exposure_duration == 0:
last_exposure_duration = self.LastInputExposureDuration
self._camera_time = False
return last_exposure_duration

@property
def LastExposureStartTime(self):
logger.debug(f"ASCOMCamera.LastExposureStartTime property called")
return self._device.LastExposureStartTime
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):
Expand Down
4 changes: 3 additions & 1 deletion pyscope/observatory/maxim.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import platform
import time

from astropy.time import Time

from .autofocus import Autofocus
from .camera import Camera
from .device import Device
Expand Down Expand Up @@ -124,7 +126,7 @@ def PulseGuide(self, Direction, Duration):
def StartExposure(self, Duration, Light):
logger.debug(f"StartExposure called with Duration={Duration}, Light={Light}")
self._last_exposure_duration = Duration
self._last_exposure_start_time = time.time()
self._last_exposure_start_time = str(Time.now())
self._com_object.Expose(Duration, Light)

def StopExposure(self):
Expand Down
Loading

0 comments on commit e71e2a2

Please sign in to comment.