From b6775a7868873b17d78d71af7832a3e92048e193 Mon Sep 17 00:00:00 2001 From: "T. E. Pickering" Date: Mon, 17 Feb 2025 11:22:38 -0700 Subject: [PATCH 01/12] now try pyproject only --- LICENSE.rst | 25 --------- pyproject.toml | 113 ++++++++++++++++++++++++++++++++++++++-- setup.cfg | 100 ----------------------------------- setup.py | 80 ---------------------------- tox.ini | 18 ++----- wfssrv/__init__.py | 25 ++------- wfssrv/_astropy_init.py | 52 ------------------ wfssrv/conftest.py | 41 +++------------ 8 files changed, 124 insertions(+), 330 deletions(-) delete mode 100644 LICENSE.rst delete mode 100644 setup.cfg delete mode 100755 setup.py delete mode 100644 wfssrv/_astropy_init.py diff --git a/LICENSE.rst b/LICENSE.rst deleted file mode 100644 index 91bf16b..0000000 --- a/LICENSE.rst +++ /dev/null @@ -1,25 +0,0 @@ -Copyright (c) 2018, MMT Observatory -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. -* Redistributions in binary form must reproduce the above copyright notice, this - list of conditions and the following disclaimer in the documentation and/or - other materials provided with the distribution. -* Neither the name of the MMT Observatory nor the names of its contributors may be - used to endorse or promote products derived from this software without - specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pyproject.toml b/pyproject.toml index 7e7daea..a05df2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,112 @@ -[build-system] +[project] +name = "wfssrv" +dynamic = ["version"] +authors = [ + { name = "T. E. Pickering", email = "te.pickering@gmail.com"} +] +license = {file = "licenses/LICENSE.rst"} +readme = "README.rst" +description = "MMTO Wavefront Sensor Analysis Server" +requires-python = ">=3.12" +dependencies = [ + "tornado", + "redis", + "matplotlib", + "camsrv@git+https://github.com/MMTObservatory/camsrv", + "mmtwfs@git+https://github.com/MMTObservatory/mmtwfs", +] + +[project.optional-dependencies] +test = [ + "tox", + "coverage", + "pytest-astropy", + "black", + "flake8", + "codecov", +] +docs = [ + "sphinx-astropy", +] + +[project.scripts] +"wfs_header.py" = "wfssrv.scripts.wfs_header:main" +wfssrv = "wfssrv.wfssrv:main" + +[project.urls] +Repository = "https://github.com/mmtobservatory/wfssrv.git" +Documentation = "https://wfssrv.readthedocs.io/" + +[tool.setuptools] +include-package-data = true + +[tool.setuptools.package-data] +"wfssrv.templates" = ["**"] +"wfssrv.static" = ["**"] + +[tool.setuptools.packages] +find = {} -requires = ["setuptools", - "setuptools_scm", - "wheel"] +[tool.setuptools_scm] +version_file = "wfssrv/version.py" + +[build-system] +requires = [ + "setuptools", + "setuptools_scm", +] build-backend = 'setuptools.build_meta' + +[tool.pytest.ini_options] +minversion = 7.0 +testpaths = [ + "wfssrv/test", + "docs", +] +astropy_header = true +doctest_plus = "enabled" +text_file_format = "rst" +addopts = [ + "--color=yes", + "--doctest-rst", +] +xfail_strict = true +filterwarnings = [ + "error", + "ignore:numpy\\.ufunc size changed:RuntimeWarning", + "ignore:numpy\\.ndarray size changed:RuntimeWarning", + # Python 3.12 warning from dateutil imported by matplotlib + "ignore:.*utcfromtimestamp:DeprecationWarning", +] + +[tool.coverage] + + [tool.coverage.run] + omit = [ + "wfssrv/_astropy_init*", + "wfssrv/conftest.py", + "wfssrv/tests/*", + "wfssrv/version*", + "*/wfssrv/_astropy_init*", + "*/wfssrv/conftest.py", + "*/wfssrv/tests/*", + "*/wfssrv/version*", + ] + + [tool.coverage.report] + exclude_lines = [ + # Have to re-enable the standard pragma + "pragma: no cover", + # Don't complain about packages we have installed + "except ImportError", + # Don't complain if tests don't hit defensive assertion code: + "raise AssertionError", + "raise NotImplementedError", + # Don't complain about script hooks + "'def main(.*):'", + # Ignore branches that don't pertain to this version of Python + "pragma: py{ignore_python_version}", + # Don't complain about IPython completion helper + "def _ipython_key_completions_", + ] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 96b6985..0000000 --- a/setup.cfg +++ /dev/null @@ -1,100 +0,0 @@ -[metadata] -name = wfssrv -description = MMTO Wavefront Sensor Analysis Server -long_description = file: README.md -author = T. E. Pickering (MMT Observatory) -author_email = tim@mmto.org -license = BSD-3 -license_file = LICENSE.rst -url = https://github.com/MMTObservatory/WFSsrv -edit_on_github = True -github_project = MMTObservatory/WFSsrv - -[options] -zip_safe = False -packages = find: -python_requires = >=3.7 -setup_requires = setuptools_scm -install_requires = - tornado - astropy - mmtwfs - camsrv - -include_package_data = True - -[options.extras_require] -docs = - sphinx-astropy -test = - tox - pytest - pytest-cov - pytest-astropy - nose - coverage - codecov -all = - matplotlib - scipy - astropy - photutils - scikit-image - dnspython - poppy - lmfit - ccdproc - astroscrappy - redis - -[options.entry_points] -console_scripts = - wfs_header.py = wfssrv.scripts.wfs_header:main - wfssrv = wfssrv.wfssrv:main - -[options.package_data] -wfssrv.templates = *.html -wfssrv.static = */* -wfssrv.tests = coveragerc - -[tool:pytest] -testpaths = "wfssrv" "docs" -astropy_header = true -doctest_plus = enabled -text_file_format = rst -addopts = --doctest-rst - -[coverage:run] -parallel = True -branch = True -omit = - wfssrv/_astropy_init* - wfssrv/conftest.py - wfssrv/*setup_package* - wfssrv/tests/* - wfssrv/*/tests/* - wfssrv/extern/* - wfssrv/version* - */wfssrv/_astropy_init* - */wfssrv/conftest.py - */wfssrv/*setup_package* - */wfssrv/tests/* - */wfssrv/*/tests/* - */wfssrv/extern/* - */wfssrv/version* - -[coverage:report] -exclude_lines = - # Have to re-enable the standard pragma - pragma: no cover - # Don't complain about packages we have installed - except ImportError - # Don't complain if tests don't hit assertions - raise AssertionError - raise NotImplementedError - # Don't complain about script hooks - def main\(.*\): - # Ignore branches that don't pertain to this version of Python - pragma: py{ignore_python_version} - # Don't complain about IPython completion helper - def _ipython_key_completions_ diff --git a/setup.py b/setup.py deleted file mode 100755 index 8fb0105..0000000 --- a/setup.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python -# Licensed under a 3-clause BSD style license - see LICENSE.rst - -# NOTE: The configuration for the package, including the name, version, and -# other information are set in the setup.cfg file. - -import os -import sys - -from setuptools import setup -from setuptools.config import read_configuration - - -# First provide helpful messages if contributors try and run legacy commands -# for tests or docs. - -TEST_HELP = """ -Note: running tests is no longer done using 'python setup.py test'. Instead -you will need to run: - - tox -e test - -If you don't already have tox installed, you can install it with: - - pip install tox - -If you only want to run part of the test suite, you can also use pytest -directly with:: - - pip install -e .[test] - pytest - -For more information, see: - - http://docs.astropy.org/en/latest/development/testguide.html#running-tests -""" - -if 'test' in sys.argv: - print(TEST_HELP) - sys.exit(1) - -DOCS_HELP = """ -Note: building the documentation is no longer done using -'python setup.py build_docs'. Instead you will need to run: - - tox -e build_docs - -If you don't already have tox installed, you can install it with: - - pip install tox - -You can also build the documentation with Sphinx directly using:: - - pip install -e .[docs] - cd docs - make html - -For more information, see: - - http://docs.astropy.org/en/latest/install.html#builddocs -""" - -if 'build_docs' in sys.argv or 'build_sphinx' in sys.argv: - print(DOCS_HELP) - sys.exit(1) - -VERSION_TEMPLATE = """ -# Note that we need to fall back to the hard-coded version if either -# setuptools_scm can't be imported or setuptools_scm can't determine the -# version, so we catch the generic 'Exception'. -try: - from setuptools_scm import get_version - version = get_version(root='..', relative_to=__file__) -except Exception: - version = '{version}' -""".lstrip() - - -setup(use_scm_version={'write_to': os.path.join('wfssrv', 'version.py'), - 'write_to_template': VERSION_TEMPLATE}) diff --git a/tox.ini b/tox.ini index 945f4f6..7839a6b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,7 @@ [tox] envlist = - py{37,38}{,-test}{,-cov} - py{37,38}-astropylts - py{37,38}-{numpy,astropy}dev + py{312,313}{,-test}{,-cov} + py{312,313}-{numpy,astropy}dev build_docs linkcheck codestyle @@ -16,7 +15,7 @@ isolated_build = true usedevelop = True # Pass through the following environemnt variables which may be needed for the CI -passenv = HOME WINDIR LC_ALL LC_CTYPE CC CI +passenv = HOME,WINDIR,LC_ALL,LC_CTYPE,CC,CI # Run the tests in a temporary directory to make sure that we don't import # astropy from the source tree @@ -33,16 +32,10 @@ changedir = {toxworkdir}/tox_testing description = test: run tests cov: with coverage enabled - astropylts: with astropy LTS {numpy,astropy}dev: with latest master from github repo cov_report: generate HTML coverage report deps = - git+https://github.com/MMTObservatory/cwfs.git#egg=cwfs - git+https://github.com/MMTObservatory/mmtwfs.git#egg=mmtwfs - git+https://github.com/MMTObservatory/camsrv.git#egg=camsrv - git+https://github.com/MMTObservatory/indiclient.git#egg=indiclient - astropylts: astropy==4.0.* numpydev: git+https://github.com/numpy/numpy.git#egg=numpy astropydev: git+https://github.com/astropy/astropy.git#egg=astropy @@ -53,19 +46,17 @@ depends = # The following indicates which extras_require from setup.cfg will be installed extras = test - all commands = pip freeze !cov: pytest --pyargs wfssrv {toxinidir}/docs {posargs} - cov: pytest --pyargs wfssrv {toxinidir}/docs --cov wfssrv --cov-config={toxinidir}/setup.cfg {posargs} + cov: pytest --pyargs wfssrv {toxinidir}/docs --cov wfssrv --cov-config={toxinidir}/pyproject.toml {posargs} cov: coverage xml -o {toxinidir}/coverage.xml [testenv:build_docs] changedir = docs description = invoke sphinx-build to build the HTML docs extras = - all docs commands = pip freeze @@ -75,7 +66,6 @@ commands = changedir = docs description = check the links in the HTML docs extras = - all docs commands = pip freeze diff --git a/wfssrv/__init__.py b/wfssrv/__init__.py index 43f77e9..000a5a2 100644 --- a/wfssrv/__init__.py +++ b/wfssrv/__init__.py @@ -1,25 +1,8 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst -# Affiliated packages may add whatever they like to this file, but -# should keep this content at the top. -# ---------------------------------------------------------------------------- -from ._astropy_init import * # noqa -# ---------------------------------------------------------------------------- - -# Enforce Python version check during package import. -# This is the same check as the one at the top of setup.py -import sys -from distutils.version import LooseVersion - -__minimum_python_version__ = "3.7" - __all__ = [] - -class UnsupportedPythonError(Exception): - pass - - -if LooseVersion(sys.version) < LooseVersion(__minimum_python_version__): - raise UnsupportedPythonError("wfssrv does not support Python < {}" - .format(__minimum_python_version__)) +try: + from .version import version as __version__ +except ImportError: + __version__ = "" diff --git a/wfssrv/_astropy_init.py b/wfssrv/_astropy_init.py deleted file mode 100644 index 2dffe8f..0000000 --- a/wfssrv/_astropy_init.py +++ /dev/null @@ -1,52 +0,0 @@ -# Licensed under a 3-clause BSD style license - see LICENSE.rst - -__all__ = ['__version__'] - -# this indicates whether or not we are in the package's setup.py -try: - _ASTROPY_SETUP_ -except NameError: - import builtins - builtins._ASTROPY_SETUP_ = False - -try: - from .version import version as __version__ -except ImportError: - __version__ = '' - - -if not _ASTROPY_SETUP_: # noqa - import os - from warnings import warn - from astropy.config.configuration import ( - update_default_config, - ConfigurationDefaultMissingError, - ConfigurationDefaultMissingWarning) - - # Create the test function for self test - from astropy.tests.runner import TestRunner - test = TestRunner.make_test_runner_in(os.path.dirname(__file__)) - test.__test__ = False - __all__ += ['test'] - - # add these here so we only need to cleanup the namespace at the end - config_dir = None - - if not os.environ.get('ASTROPY_SKIP_CONFIG_UPDATE', False): - config_dir = os.path.dirname(__file__) - config_template = os.path.join(config_dir, __package__ + ".cfg") - if os.path.isfile(config_template): - try: - update_default_config( - __package__, config_dir, version=__version__) - except TypeError as orig_error: - try: - update_default_config(__package__, config_dir) - except ConfigurationDefaultMissingError as e: - wmsg = (e.args[0] + - " Cannot install default profile. If you are " - "importing from source, this is expected.") - warn(ConfigurationDefaultMissingWarning(wmsg)) - del e - except Exception: - raise orig_error diff --git a/wfssrv/conftest.py b/wfssrv/conftest.py index 672b273..b164677 100644 --- a/wfssrv/conftest.py +++ b/wfssrv/conftest.py @@ -3,21 +3,12 @@ # get picked up when running the tests inside an interpreter using # packagename.test -import os +try: + from pytest_astropy_header.display import TESTED_VERSIONS -from astropy.version import version as astropy_version - -# For Astropy 3.0 and later, we can use the standalone pytest plugin -if astropy_version < '3.0': - from astropy.tests.pytest_plugins import * # noqa - del pytest_report_header ASTROPY_HEADER = True -else: - try: - from pytest_astropy_header.display import PYTEST_HEADER_MODULES, TESTED_VERSIONS - ASTROPY_HEADER = True - except ImportError: - ASTROPY_HEADER = False +except ImportError: + ASTROPY_HEADER = False def pytest_configure(config): @@ -26,24 +17,6 @@ def pytest_configure(config): config.option.astropy_header = True - # Customize the following lines to add/remove entries from the list of - # packages for which version numbers are displayed when running the tests. - PYTEST_HEADER_MODULES.pop('Pandas', None) - PYTEST_HEADER_MODULES['scikit-image'] = 'skimage' - - from . import __version__ - packagename = os.path.basename(os.path.dirname(__file__)) - TESTED_VERSIONS[packagename] = __version__ - -# Uncomment the last two lines in this block to treat all DeprecationWarnings as -# exceptions. For Astropy v2.0 or later, there are 2 additional keywords, -# as follow (although default should work for most cases). -# To ignore some packages that produce deprecation warnings on import -# (in addition to 'compiler', 'scipy', 'pygments', 'ipykernel', and -# 'setuptools'), add: -# modules_to_ignore_on_import=['module_1', 'module_2'] -# To ignore some specific deprecation warning messages for Python version -# MAJOR.MINOR or later, add: -# warnings_to_ignore_by_pyver={(MAJOR, MINOR): ['Message to ignore']} -# from astropy.tests.helper import enable_deprecations_as_exceptions # noqa -# enable_deprecations_as_exceptions() + from wfssrv import __version__ + + TESTED_VERSIONS["camsrv"] = __version__ From cfab6089c208f8427c2bea916a01fc479d48fcb2 Mon Sep 17 00:00:00 2001 From: "T. E. Pickering" Date: Mon, 17 Feb 2025 21:10:54 -0700 Subject: [PATCH 02/12] ignore weird deprecation warning; fix path to tests --- pyproject.toml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a05df2d..38fd7ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,8 +61,7 @@ build-backend = 'setuptools.build_meta' [tool.pytest.ini_options] minversion = 7.0 testpaths = [ - "wfssrv/test", - "docs", + "wfssrv/tests", ] astropy_header = true doctest_plus = "enabled" @@ -76,8 +75,8 @@ filterwarnings = [ "error", "ignore:numpy\\.ufunc size changed:RuntimeWarning", "ignore:numpy\\.ndarray size changed:RuntimeWarning", - # Python 3.12 warning from dateutil imported by matplotlib - "ignore:.*utcfromtimestamp:DeprecationWarning", + # weird no event loop deprecation warning + "ignore:.*There is no current event loop:DeprecationWarning", ] [tool.coverage] From cfb8579891075c549fdcf5d48c3d67863d5577a2 Mon Sep 17 00:00:00 2001 From: "T. E. Pickering" Date: Mon, 17 Feb 2025 21:11:08 -0700 Subject: [PATCH 03/12] move old cwfssrv file out of the way --- {wfssrv => OLD}/cwfssrv.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {wfssrv => OLD}/cwfssrv.py (100%) diff --git a/wfssrv/cwfssrv.py b/OLD/cwfssrv.py similarity index 100% rename from wfssrv/cwfssrv.py rename to OLD/cwfssrv.py From 23a2851be21ed4d2e4cfa10fa1ce1ed727e1bf46 Mon Sep 17 00:00:00 2001 From: "T. E. Pickering" Date: Mon, 17 Feb 2025 21:12:13 -0700 Subject: [PATCH 04/12] update seeing publishing to use new result of vlt algorithm; publish spot ellipticity to redis as well --- wfssrv/wfssrv.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/wfssrv/wfssrv.py b/wfssrv/wfssrv.py index dbd1e29..e6d86fb 100644 --- a/wfssrv/wfssrv.py +++ b/wfssrv/wfssrv.py @@ -242,9 +242,9 @@ def get(self): log.debug("Measuring slopes...") results = self.application.wfs.measure_slopes(filename, mode=mode, plot=True) if results['slopes'] is not None: - if 'seeing' in results: - log.info(f"Seeing (zenith): {results['seeing'].round(2)}") - log.info(f"Seeing (raw): {results['raw_seeing'].round(2)}") + if 'vlt_seeing' in results: + log.info(f"Seeing (zenith): {results['vlt_seeing'].round(2)}") + log.info(f"Seeing (raw): {results['raw_vlt_seeing'].round(2)}") if self.application.wfs.connected: log.info("Publishing seeing values to redis.") self.application.update_seeing(results) @@ -742,14 +742,20 @@ def set_redis(self, key, value): def update_seeing(self, results): try: - wfs_seeing = results['seeing'].round(2).value - wfs_raw_seeing = results['raw_seeing'].round(2).value + wfs_seeing = results['vlt_seeing'].round(2).value + wfs_raw_seeing = results['raw_vlt_seeing'].round(2).value + wfs_ellipticity = results['ellipticity'].round(3) r1 = self.set_redis('wfs_seeing', wfs_seeing) r2 = self.set_redis('wfs_raw_seeing', wfs_raw_seeing) if None not in r1 and None not in r2: log.info(f"Set redis values wfs_seeing={wfs_seeing} and wfs_raw_seeing={wfs_raw_seeing}") else: log.warning("Problem sending seeing values to redis...") + r3 = self.set_redis('wfs_ellipticity', wfs_ellipticity) + if None not in r3: + log.info(f"Set redis values wfs_ellipticity={wfs_ellipticity}") + else: + log.warning("Problem sending ellipticity value to redis...") except Exception as e: log.warning(f'Error connecting to MMTO API server... : {e}') From f9df31d5bdc4e065dbafa6b56540b38897e51474 Mon Sep 17 00:00:00 2001 From: "T. E. Pickering" Date: Mon, 17 Feb 2025 21:18:28 -0700 Subject: [PATCH 05/12] fix code formatting with black --- wfssrv/tests/test_app.py | 2 +- wfssrv/wfssrv.py | 350 ++++++++++++++++++++++++--------------- 2 files changed, 218 insertions(+), 134 deletions(-) diff --git a/wfssrv/tests/test_app.py b/wfssrv/tests/test_app.py index 03a3c38..1ffc9c4 100644 --- a/wfssrv/tests/test_app.py +++ b/wfssrv/tests/test_app.py @@ -6,4 +6,4 @@ def test_wfs(): app = wfs() - assert(app is not None) + assert app is not None diff --git a/wfssrv/wfssrv.py b/wfssrv/wfssrv.py index e6d86fb..b098c09 100644 --- a/wfssrv/wfssrv.py +++ b/wfssrv/wfssrv.py @@ -35,16 +35,19 @@ import matplotlib import matplotlib.pyplot as plt -from matplotlib.backends.backend_webagg_core import (FigureCanvasWebAggCore, new_figure_manager_given_figure) +from matplotlib.backends.backend_webagg_core import ( + FigureCanvasWebAggCore, + new_figure_manager_given_figure, +) from mmtwfs.wfs import WFSFactory from mmtwfs.zernike import ZernikeVector from mmtwfs.telescope import MMT -matplotlib.use('webagg') -glog = logging.getLogger('') -log = logging.getLogger('WFSsrv') +matplotlib.use("webagg") +glog = logging.getLogger("") +log = logging.getLogger("WFSsrv") log.setLevel(logging.DEBUG) @@ -53,32 +56,34 @@ def create_default_figures(): figures = {} ax = {} data = np.zeros((512, 512)) - tel = MMT(secondary='f5') # secondary doesn't matter, just need methods for mirror forces/plots + tel = MMT( + secondary="f5" + ) # secondary doesn't matter, just need methods for mirror forces/plots forces = tel.bending_forces(zv=zv) # stub for plot showing bkg-subtracted WFS image with aperture positions - figures['slopes'], ax['slopes'] = plt.subplots() - figures['slopes'].set_label("Aperture Positions and Spot Movement") - ax['slopes'].imshow(data, cmap='Greys', origin='lower', interpolation='None') + figures["slopes"], ax["slopes"] = plt.subplots() + figures["slopes"].set_label("Aperture Positions and Spot Movement") + ax["slopes"].imshow(data, cmap="Greys", origin="lower", interpolation="None") # stub for plot showing bkg-subtracted WFS image and residuals slopes of wavefront fit - figures['residuals'], ax['residuals'] = plt.subplots() - figures['residuals'].set_label("Zernike Fit Residuals") - ax['residuals'].imshow(data, cmap='Greys', origin='lower', interpolation='None') + figures["residuals"], ax["residuals"] = plt.subplots() + figures["residuals"].set_label("Zernike Fit Residuals") + ax["residuals"].imshow(data, cmap="Greys", origin="lower", interpolation="None") # stub for zernike bar chart - figures['barchart'] = zv.bar_chart() + figures["barchart"] = zv.bar_chart() # stub for zernike fringe bar chart - figures['fringebarchart'] = zv.fringe_bar_chart() + figures["fringebarchart"] = zv.fringe_bar_chart() # stubs for mirror forces - figures['forces'] = tel.plot_forces(forces) - figures['forces'].set_label("Requested M1 Actuator Forces") + figures["forces"] = tel.plot_forces(forces) + figures["forces"].set_label("Requested M1 Actuator Forces") # stubs for mirror forces - figures['totalforces'] = tel.plot_forces(forces) - figures['totalforces'].set_label("Total M1 Actuator Forces") + figures["totalforces"] = tel.plot_forces(forces) + figures["totalforces"].set_label("Total M1 Actuator Forces") return figures @@ -88,8 +93,13 @@ class HomeHandler(tornado.web.RequestHandler): """ Serves the main HTML page. """ + def get(self): - self.render("home.html", current=self.application.wfs, wfslist=self.application.wfs_names) + self.render( + "home.html", + current=self.application.wfs, + wfslist=self.application.wfs_names, + ) class SelectHandler(tornado.web.RequestHandler): def get(self): @@ -118,7 +128,9 @@ def get(self): manager = self.application.managers[k] fig_ids.append(manager.num) figkeys.append(k) - ws_uri = "ws://{req.host}/{figdiv}/ws".format(req=self.request, figdiv=k) + ws_uri = "ws://{req.host}/{figdiv}/ws".format( + req=self.request, figdiv=k + ) ws_uris.append(ws_uri) self.render( @@ -132,7 +144,7 @@ def get(self): default_mode=self.application.wfs.default_mode, m1_gain=self.application.wfs.m1_gain, m2_gain=self.application.wfs.m2_gain, - log_uri=log_uri + log_uri=log_uri, ) except Exception as e: log.warning(f"Must specify valid wfs: {wfs}. ({e})") @@ -146,7 +158,9 @@ def get(self): if self.application.wfs.connected: log.info(f"{self.application.wfs.name} is connected.") else: - log.warning(f"Couldn't connect to {self.application.wfs.name}. Offline?") + log.warning( + f"Couldn't connect to {self.application.wfs.name}. Offline?" + ) else: log.info(f"{self.application.wfs.name} already connected") self.finish() @@ -173,42 +187,44 @@ def async_plot(self, func, *args): def make_barchart(self, zernikes, zrms, residual): log.debug("Making bar chart...") waves = zrms.value / 550.0 - self.application.figures['barchart'] = zernikes.bar_chart( + self.application.figures["barchart"] = zernikes.bar_chart( residual=residual, - title=f"Total Wavefront RMS: {zrms.round(1)} ({np.round(waves, 2)} waves)" + title=f"Total Wavefront RMS: {zrms.round(1)} ({np.round(waves, 2)} waves)", ) - return 'barchart' + return "barchart" @run_on_executor def make_fringebarchart(self, zernikes, focus, cc_x, cc_y): log.debug("Making fringe bar chart...") - self.application.figures['fringebarchart'] = zernikes.fringe_bar_chart( + self.application.figures["fringebarchart"] = zernikes.fringe_bar_chart( title="Focus: {0:0.1f} CC_X: {1:0.1f} CC_Y: {2:0.1f}".format( focus, cc_x, cc_y, ), - max_c=1500*u.nm, + max_c=1500 * u.nm, ) - return 'fringebarchart' + return "fringebarchart" @run_on_executor def make_totalforces(self, telescope, forces, m1focus): log.debug("Making total forces plot...") - self.application.figures['totalforces'] = telescope.plot_forces(forces, m1focus) - self.application.figures['totalforces'].set_label("Unmasked M1 Actuator Forces") - return 'totalforces' + self.application.figures["totalforces"] = telescope.plot_forces( + forces, m1focus + ) + self.application.figures["totalforces"].set_label( + "Unmasked M1 Actuator Forces" + ) + return "totalforces" @run_on_executor def make_pendingforces(self, telescope, forces, m1focus, limit): log.debug("Making pending forces plot...") - self.application.figures['forces'] = telescope.plot_forces( - forces, - m1focus, - limit=limit + self.application.figures["forces"] = telescope.plot_forces( + forces, m1focus, limit=limit ) - self.application.figures['forces'].set_label("Requested M1 Actuator Forces") - return 'forces' + self.application.figures["forces"].set_label("Requested M1 Actuator Forces") + return "forces" def complete_refresh(self, key): self.application.refresh_figure(key, self.application.figures[key]) @@ -226,11 +242,13 @@ def get(self): spher = self.get_argument("spher", default=False) if spher == "true": - spher_mask = ['Z11', 'Z22'] + spher_mask = ["Z11", "Z22"] log.info(f"Ignoring all spherical terms {str(spher_mask)}...") else: - spher_mask = ['Z22'] - log.info(f"Only ignoring the high-order spherical terms {str(spher_mask)}...") + spher_mask = ["Z22"] + log.info( + f"Only ignoring the high-order spherical terms {str(spher_mask)}..." + ) if os.path.isfile(filename) and not self.application.busy: self.application.busy = True @@ -240,9 +258,11 @@ def get(self): self.application.wfs.disconnect() log.debug("Measuring slopes...") - results = self.application.wfs.measure_slopes(filename, mode=mode, plot=True) - if results['slopes'] is not None: - if 'vlt_seeing' in results: + results = self.application.wfs.measure_slopes( + filename, mode=mode, plot=True + ) + if results["slopes"] is not None: + if "vlt_seeing" in results: log.info(f"Seeing (zenith): {results['vlt_seeing'].round(2)}") log.info(f"Seeing (raw): {results['raw_vlt_seeing'].round(2)}") if self.application.wfs.connected: @@ -251,91 +271,127 @@ def get(self): tel = self.application.wfs.telescope log.debug("Making slopes plot...") - self.application.figures['slopes'] = results['figures']['slopes'] - self.application.refresh_figure('slopes', self.application.figures['slopes']) + self.application.figures["slopes"] = results["figures"]["slopes"] + self.application.refresh_figure( + "slopes", self.application.figures["slopes"] + ) log.debug("Fitting wavefront...") zresults = self.application.wfs.fit_wavefront(results, plot=True) - if zresults['fit_report'].success: + if zresults["fit_report"].success: log.info(f"Residual RMS: {zresults['residual_rms'].round(2)}") - self.application.figures['residuals'] = zresults['resid_plot'] - self.application.refresh_figure('residuals', self.application.figures['residuals']) + self.application.figures["residuals"] = zresults["resid_plot"] + self.application.refresh_figure( + "residuals", self.application.figures["residuals"] + ) - zvec = zresults['zernike'] - zvec_raw = zresults['rot_zernike'] - zvec_ref = zresults['ref_zernike'] + zvec = zresults["zernike"] + zvec_raw = zresults["rot_zernike"] + zvec_ref = zresults["ref_zernike"] self.application.wavefront_fit = zvec.copy() m1gain = self.application.wfs.m1_gain # this is the total if we try to correct everything as fit - totforces, totm1focus, zv_totmasked = tel.calculate_primary_corrections(zvec.copy(), gain=m1gain) + totforces, totm1focus, zv_totmasked = ( + tel.calculate_primary_corrections(zvec.copy(), gain=m1gain) + ) - self.async_plot(self.make_barchart, zvec.copy(), zresults['zernike_rms'], zresults['residual_rms']) - self.async_plot(self.make_totalforces, tel, totforces, totm1focus) + self.async_plot( + self.make_barchart, + zvec.copy(), + zresults["zernike_rms"], + zresults["residual_rms"], + ) + self.async_plot( + self.make_totalforces, tel, totforces, totm1focus + ) log.debug("Saving files and calculating corrections...") zvec_file = self.application.datadir / (filename + ".zernike") - zvec_raw_file = self.application.datadir / (filename + ".raw.zernike") - zvec_ref_file = self.application.datadir / (filename + ".ref.zernike") + zvec_raw_file = self.application.datadir / ( + filename + ".raw.zernike" + ) + zvec_ref_file = self.application.datadir / ( + filename + ".ref.zernike" + ) zvec.save(filename=zvec_file) zvec_raw.save(filename=zvec_raw_file) zvec_ref.save(filename=zvec_ref_file) # check the RMS of the wavefront fit and only apply corrections if the fit is good enough. # M2 can be more lenient to take care of large amounts of focus or coma. - if zresults['residual_rms'] < 4000 * u.nm: + if zresults["residual_rms"] < 4000 * u.nm: self.application.has_pending_m1 = True self.application.has_pending_coma = True self.application.has_pending_focus = True log.info(f"{filename}: all proposed corrections valid.") - elif zresults['residual_rms'] <= 7000 * u.nm: + elif zresults["residual_rms"] <= 7000 * u.nm: self.application.has_pending_focus = True log.warning(f"{filename}: only focus corrections valid.") - elif zresults['residual_rms'] > 7000 * u.nm: - log.error(f"{filename}: wavefront fit too poor; no valid corrections") + elif zresults["residual_rms"] > 7000 * u.nm: + log.error( + f"{filename}: wavefront fit too poor; no valid corrections" + ) self.application.has_pending_recenter = True - self.application.pending_focus = self.application.wfs.calculate_focus(zvec.copy()) + self.application.pending_focus = ( + self.application.wfs.calculate_focus(zvec.copy()) + ) # only allow M1 corrections if we are reasonably close to good focus... if self.application.pending_focus > 150 * u.um: self.application.has_pending_m1 = False - self.application.pending_cc_x, self.application.pending_cc_y = self.application.wfs.calculate_cc(zvec.copy()) + self.application.pending_cc_x, self.application.pending_cc_y = ( + self.application.wfs.calculate_cc(zvec.copy()) + ) self.async_plot( self.make_fringebarchart, zvec.copy(), self.application.pending_focus, self.application.pending_cc_x, - self.application.pending_cc_y + self.application.pending_cc_y, ) log.debug("Calculating pending forces...") - self.application.pending_az, self.application.pending_el = self.application.wfs.calculate_recenter(results) - self.application.pending_forces, self.application.pending_m1focus, zv_masked = \ - self.application.wfs.calculate_primary(zvec.copy(), mask=spher_mask) - self.application.pending_forcefile = self.application.datadir / (filename + ".forces") - zvec_masked_file = self.application.datadir / (filename + ".masked.zernike") + self.application.pending_az, self.application.pending_el = ( + self.application.wfs.calculate_recenter(results) + ) + ( + self.application.pending_forces, + self.application.pending_m1focus, + zv_masked, + ) = self.application.wfs.calculate_primary( + zvec.copy(), mask=spher_mask + ) + self.application.pending_forcefile = ( + self.application.datadir / (filename + ".forces") + ) + zvec_masked_file = self.application.datadir / ( + filename + ".masked.zernike" + ) zv_masked.save(filename=zvec_masked_file) - limit = np.round(np.abs(self.application.pending_forces['force']).max()) + limit = np.round( + np.abs(self.application.pending_forces["force"]).max() + ) self.async_plot( self.make_pendingforces, tel, self.application.pending_forces, self.application.pending_m1focus, - limit + limit, ) else: log.error(f"Wavefront fit failed: {filename}") figures = create_default_figures() - figures['slopes'] = results['figures']['slopes'] + figures["slopes"] = results["figures"]["slopes"] self.application.refresh_figures(figures=figures) else: log.error(f"Wavefront measurement failed: {filename}") figures = create_default_figures() - figures['slopes'] = results['figures']['slopes'] + figures["slopes"] = results["figures"]["slopes"] self.application.refresh_figures(figures=figures) else: @@ -352,15 +408,23 @@ def get(self): self.application.wfs.telescope.correct_primary( self.application.pending_forces, self.application.pending_m1focus, - filename=self.application.pending_forcefile + filename=self.application.pending_forcefile, ) - max_f = self.application.pending_forces['force'].max() - min_f = self.application.pending_forces['force'].min() + max_f = self.application.pending_forces["force"].max() + min_f = self.application.pending_forces["force"].min() log.info(f"Maximum force {round(max_f, 2)} N") log.info(f"Minimum force {round(min_f, 2)} N") - log.info("Adjusting M1 focus by {0:0.1f}".format(self.application.pending_m1focus)) + log.info( + "Adjusting M1 focus by {0:0.1f}".format( + self.application.pending_m1focus + ) + ) self.application.has_pending_m1 = False - self.write("Sending forces to cell and {0:0.1f} focus to secondary...".format(self.application.pending_m1focus)) + self.write( + "Sending forces to cell and {0:0.1f} focus to secondary...".format( + self.application.pending_m1focus + ) + ) else: log.info("No M1 corrections sent") self.write("No M1 corrections sent") @@ -386,11 +450,12 @@ class ComaCorrectHandler(tornado.web.RequestHandler): def get(self): log.info("M2 coma corrections...") if self.application.has_pending_coma and self.application.wfs.connected: - self.application.wfs.secondary.correct_coma(self.application.pending_cc_x, self.application.pending_cc_y) + self.application.wfs.secondary.correct_coma( + self.application.pending_cc_x, self.application.pending_cc_y + ) self.application.has_pending_coma = False log_str = "Sending {0:0.1f}/{1:0.1f} CC_X/CC_Y to secondary...".format( - self.application.pending_cc_x, - self.application.pending_cc_y + self.application.pending_cc_x, self.application.pending_cc_y ) log.info(log_str) self.write(log_str) @@ -410,9 +475,8 @@ def get(self): # self.application.pending_el # ) log_str = "Hexapod recentering disabled. " - log_str += "Please apply mount offsets of EL={0:0.1f}\" and AZ={1:0.1f}\"...".format( - self.application.pending_el, - self.application.pending_az + log_str += 'Please apply mount offsets of EL={0:0.1f}" and AZ={1:0.1f}"...'.format( + self.application.pending_el, self.application.pending_az ) log.warning(log_str) self.write(log_str) @@ -424,7 +488,7 @@ def get(self): class RestartHandler(tornado.web.RequestHandler): def get(self): try: - wfs = self.get_argument('wfs') + wfs = self.get_argument("wfs") self.application.restart_wfs(wfs) log.info(f"restarting {wfs}") except Exception as e: @@ -454,7 +518,7 @@ def get(self): def post(self): self.set_header("Content-Type", "text/plain") try: - gain = float(self.get_body_argument('gain')) + gain = float(self.get_body_argument("gain")) if self.application.wfs is not None: if gain >= 0.0 and gain <= 1.0: self.application.wfs.m1_gain = gain @@ -476,7 +540,7 @@ def get(self): def post(self): self.set_header("Content-Type", "text/plain") try: - gain = float(self.get_body_argument('gain')) + gain = float(self.get_body_argument("gain")) if self.application.wfs is not None: if gain >= 0.0 and gain <= 1.0: self.application.wfs.m2_gain = gain @@ -510,7 +574,9 @@ def get(self): except PermissionError as e: # started getting weird permission errors on hacksaw that looks like NFS race bug. # running 'ls' in the directory clears the error... - log.warning(f"Permission error, {e.__class__}, while listing files in {p}...") + log.warning( + f"Permission error, {e.__class__}, while listing files in {p}..." + ) os.system(f"ls {p} > /dev/null") fullfiles = [] files = [] @@ -551,14 +617,14 @@ def get(self): class CompMirrorStatus(tornado.web.RequestHandler): def get(self): - compmirror = self.application.wfs_systems['newf9'].compmirror + compmirror = self.application.wfs_systems["newf9"].compmirror status = compmirror.get_mirror() self.write(json.dumps(status)) self.finish() class CompMirrorToggle(tornado.web.RequestHandler): def get(self): - compmirror = self.application.wfs_systems['newf9'].compmirror + compmirror = self.application.wfs_systems["newf9"].compmirror status = compmirror.toggle_mirror() self.write(json.dumps(status)) self.finish() @@ -567,21 +633,22 @@ class Download(tornado.web.RequestHandler): """ Handles downloading of the figure in various file formats. """ + def get(self, fig, fmt): managers = self.application.managers mimetypes = { - 'ps': 'application/postscript', - 'eps': 'application/postscript', - 'pdf': 'application/pdf', - 'svg': 'image/svg+xml', - 'png': 'image/png', - 'jpeg': 'image/jpeg', - 'tif': 'image/tiff', - 'emf': 'application/emf' + "ps": "application/postscript", + "eps": "application/postscript", + "pdf": "application/pdf", + "svg": "image/svg+xml", + "png": "image/png", + "jpeg": "image/jpeg", + "tif": "image/tiff", + "emf": "application/emf", } - self.set_header('Content-Type', mimetypes.get(fmt, 'binary')) + self.set_header("Content-Type", mimetypes.get(fmt, "binary")) buff = io.BytesIO() managers[fig].canvas.print_figure(buff, format=fmt) @@ -592,15 +659,18 @@ class LogStreamer(tornado.websocket.WebSocketHandler): """ A websocket for streaming log messages from log file to the browser. """ + def open(self): - if hasattr(self, 'set_nodelay'): + if hasattr(self, "set_nodelay"): self.set_nodelay(False) filename = self.application.logfile - self.proc = Subprocess(["tail", "-f", "-n", "0", filename], - stdout=Subprocess.STREAM, - stdin=Subprocess.STREAM, - stderr=Subprocess.STREAM, - bufsize=1) + self.proc = Subprocess( + ["tail", "-f", "-n", "0", filename], + stdout=Subprocess.STREAM, + stdin=Subprocess.STREAM, + stderr=Subprocess.STREAM, + bufsize=1, + ) self.proc.set_exit_callback(self._close) tornado.ioloop.IOLoop.current().spawn_callback(self.stream_output) @@ -621,7 +691,7 @@ def stream_output(self): try: while True: line = yield self.proc.stdout.read_until(b"\n") - self.write_line(line.decode('utf-8')) + self.write_line(line.decode("utf-8")) except StreamClosedError: pass @@ -632,10 +702,16 @@ def write_line(self, html): color = "text-danger" else: color = "text-success" - if "tornado.access" not in html and "poppy" not in html and "DEBUG" not in html: + if ( + "tornado.access" not in html + and "poppy" not in html + and "DEBUG" not in html + ): html = "%s" % (color, html) - html += "" - self.write_message(html.encode('utf-8')) + html += ( + '' + ) + self.write_message(html.encode("utf-8")) class WebSocket(tornado.websocket.WebSocketHandler): """ @@ -654,6 +730,7 @@ class WebSocket(tornado.websocket.WebSocketHandler): - ``send_binary(blob)`` is called to send binary image data to the browser. """ + supports_binary = True def open(self, figname): @@ -661,7 +738,7 @@ def open(self, figname): self.figname = figname manager = self.application.managers[figname] manager.add_web_socket(self) - if hasattr(self, 'set_nodelay'): + if hasattr(self, "set_nodelay"): self.set_nodelay(True) def on_close(self): @@ -677,10 +754,10 @@ def on_message(self, message): # Every message has a "type" and a "figure_id". message = json.loads(message) - if message['type'] == 'supports_binary': - self.supports_binary = message['value'] + if message["type"] == "supports_binary": + self.supports_binary = message["value"] else: - manager = self.application.fig_id_map[message['figure_id']] + manager = self.application.fig_id_map[message["figure_id"]] manager.handle_json(message) def send_json(self, content): @@ -691,7 +768,8 @@ def send_binary(self, blob): self.write_message(blob, binary=True) else: data_uri = "data:image/png;base64,{0}".format( - blob.encode('base64').replace('\n', '')) + blob.encode("base64").replace("\n", "") + ) self.write_message(data_uri) def restart_wfs(self, wfs): @@ -703,7 +781,7 @@ def restart_wfs(self, wfs): def close_figures(self): if self.figures is not None: - plt.close('all') + plt.close("all") def refresh_figure(self, k, figure): if k not in self.managers: @@ -742,26 +820,28 @@ def set_redis(self, key, value): def update_seeing(self, results): try: - wfs_seeing = results['vlt_seeing'].round(2).value - wfs_raw_seeing = results['raw_vlt_seeing'].round(2).value - wfs_ellipticity = results['ellipticity'].round(3) - r1 = self.set_redis('wfs_seeing', wfs_seeing) - r2 = self.set_redis('wfs_raw_seeing', wfs_raw_seeing) + wfs_seeing = results["vlt_seeing"].round(2).value + wfs_raw_seeing = results["raw_vlt_seeing"].round(2).value + wfs_ellipticity = results["ellipticity"].round(3) + r1 = self.set_redis("wfs_seeing", wfs_seeing) + r2 = self.set_redis("wfs_raw_seeing", wfs_raw_seeing) if None not in r1 and None not in r2: - log.info(f"Set redis values wfs_seeing={wfs_seeing} and wfs_raw_seeing={wfs_raw_seeing}") + log.info( + f"Set redis values wfs_seeing={wfs_seeing} and wfs_raw_seeing={wfs_raw_seeing}" + ) else: log.warning("Problem sending seeing values to redis...") - r3 = self.set_redis('wfs_ellipticity', wfs_ellipticity) + r3 = self.set_redis("wfs_ellipticity", wfs_ellipticity) if None not in r3: log.info(f"Set redis values wfs_ellipticity={wfs_ellipticity}") else: log.warning("Problem sending ellipticity value to redis...") except Exception as e: - log.warning(f'Error connecting to MMTO API server... : {e}') + log.warning(f"Error connecting to MMTO API server... : {e}") def __init__(self): - if 'WFSROOT' in os.environ: - self.datadir = pathlib.Path(os.environ['WFSROOT']) + if "WFSROOT" in os.environ: + self.datadir = pathlib.Path(os.environ["WFSROOT"]) else: self.datadir = pathlib.Path.cwd() @@ -774,7 +854,9 @@ def __init__(self): if os.path.isdir(self.datadir): self.logfile = self.datadir / "wfs.log" - formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + ) handler = logging.handlers.WatchedFileHandler(self.logfile) handler.setFormatter(formatter) glog.addHandler(handler) @@ -784,11 +866,13 @@ def __init__(self): self.wfs = None self.wfs_systems = {} - self.wfs_keys = ['newf9', 'f5', 'mmirs', 'binospec'] + self.wfs_keys = ["newf9", "f5", "mmirs", "binospec"] self.wfs_names = {} for w in self.wfs_keys: self.wfs_systems[w] = WFSFactory(wfs=w) - self.wfs_systems[w].nzern = 10 # hard-code this for now, but should be configurable/settable + self.wfs_systems[w].nzern = ( + 10 # hard-code this for now, but should be configurable/settable + ) self.wfs_names[w] = self.wfs_systems[w].name self.busy = False @@ -803,10 +887,10 @@ def __init__(self): self.refresh_figures() self.wavefront_fit = ZernikeVector(Z04=1) - if 'REDISHOST' in os.environ: - redis_host = os.environ['REDISHOST'] + if "REDISHOST" in os.environ: + redis_host = os.environ["REDISHOST"] else: - redis_host = 'redis.mmto.arizona.edu' + redis_host = "redis.mmto.arizona.edu" self.redis_server = redis.StrictRedis(host=redis_host, port=6379, db=0) handlers = [ @@ -833,15 +917,15 @@ def __init__(self): (r"/clearm2", self.ClearM2Handler), (r"/compmirror", self.CompMirrorStatus), (r"/compmirrortoggle", self.CompMirrorToggle), - (r'/download_([a-z]+).([a-z0-9.]+)', self.Download), - (r'/log', self.LogStreamer), - (r'/([a-z0-9.]+)/ws', self.WebSocket) + (r"/download_([a-z]+).([a-z0-9.]+)", self.Download), + (r"/log", self.LogStreamer), + (r"/([a-z0-9.]+)/ws", self.WebSocket), ] settings = dict( template_path=os.path.join(os.path.dirname(__file__), "templates"), static_path=os.path.join(os.path.dirname(__file__), "static"), - debug=True + debug=True, ) super(WFSsrv, self).__init__(handlers, **settings) From 516bdcf825a93d85d4877e5030a0e9e6927946b1 Mon Sep 17 00:00:00 2001 From: "T. E. Pickering" Date: Mon, 17 Feb 2025 21:54:49 -0700 Subject: [PATCH 06/12] fix docs build; update github workflow; update dockerfile; update paths in sao-wfs-server --- .github/workflows/wfssrv-tests.yml | 11 ++++------ Dockerfile | 10 +++------ docs/conf.py | 34 ++++++------------------------ scripts/sao-wfs-server | 2 +- 4 files changed, 15 insertions(+), 42 deletions(-) diff --git a/.github/workflows/wfssrv-tests.yml b/.github/workflows/wfssrv-tests.yml index 669bac3..3eb420a 100644 --- a/.github/workflows/wfssrv-tests.yml +++ b/.github/workflows/wfssrv-tests.yml @@ -10,12 +10,11 @@ jobs: matrix_tests: runs-on: ${{ matrix.os }} - if: "!contains(github.event.head_commit.message, '[ci skip]')" strategy: matrix: os: [ubuntu-latest] - python-ver: [7, 8] - tox-env: [cov, astropylts, astropydev, numpydev] + python-ver: [13] + tox-env: [cov, astropydev, numpydev] steps: - uses: actions/checkout@v1 - name: Set up python 3.${{ matrix.python-ver }} with tox environment ${{ matrix.tox-env }} on ${{ matrix.os }} @@ -39,13 +38,12 @@ jobs: doc_test: runs-on: ubuntu-latest - if: "!contains(github.event.head_commit.message, '[ci skip]')" steps: - uses: actions/checkout@v1 - name: Set up Python to build docs with sphinx uses: actions/setup-python@v1 with: - python-version: 3.8 + python-version: 3.13 - name: Install base dependencies run: | python -m pip install --upgrade pip @@ -57,13 +55,12 @@ jobs: codestyle: runs-on: ubuntu-latest - if: "!contains(github.event.head_commit.message, '[ci skip]')" steps: - uses: actions/checkout@v1 - name: Python codestyle check uses: actions/setup-python@v1 with: - python-version: 3.8 + python-version: 3.13 - name: Install base dependencies run: | python -m pip install --upgrade pip diff --git a/Dockerfile b/Dockerfile index 40bc902..cd2a12f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,9 @@ -FROM mmtobservatory/mmtwfs:latest +FROM python:3.13 -MAINTAINER T. E. Pickering "te.pickering@gmail.com" - -COPY . . +LABEL maintainer="te.pickering@gmail.com" RUN python -m pip install --upgrade pip -RUN python -m pip install git+https://github.com/MMTObservatory/camsrv.git#egg=camsrv -RUN python -m pip install git+https://github.com/MMTObservatory/cwfs.git#egg=cwfs -RUN python -m pip install -e .[all] +RUN python -m pip install -e . EXPOSE 8080 diff --git a/docs/conf.py b/docs/conf.py index 6568bf6..9ae0109 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -36,13 +36,6 @@ print('ERROR: the documentation requires the sphinx-astropy package to be installed') sys.exit(1) -# Get configuration information from setup.cfg -from configparser import ConfigParser -conf = ConfigParser() - -conf.read([os.path.join(os.path.dirname(__file__), '..', 'setup.cfg')]) -setup_cfg = dict(conf.items('metadata')) - # -- General configuration ---------------------------------------------------- # By default, highlight as Python 3. @@ -67,17 +60,17 @@ # -- Project information ------------------------------------------------------ # This does not *have* to match the package name, but typically does -project = setup_cfg['name'] -author = setup_cfg['author'] +project = "wfssrv" +author = "T. E. Pickering" copyright = '{0}, {1}'.format( - datetime.datetime.now().year, setup_cfg['author']) + datetime.datetime.now().year, "T. E. Pickering") # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. -import_module(setup_cfg['name']) -package = sys.modules[setup_cfg['name']] +import_module("wfssrv") +package = sys.modules["wfssrv"] # The short X.Y version. version = package.__version__.split('-', 1)[0] @@ -106,7 +99,7 @@ html_theme_options = { - 'logotext1': 'mmtwfs', # white, semi-bold + 'logotext1': 'wfssrv', # white, semi-bold 'logotext2': '', # orange, light 'logotext3': ':docs' # white, light } @@ -151,21 +144,8 @@ man_pages = [('index', project.lower(), project + u' Documentation', [author], 1)] - -# -- Options for the edit_on_github extension --------------------------------- - -if setup_cfg.get('edit_on_github').lower() == 'true': - - extensions += ['sphinx_astropy.ext.edit_on_github'] - - edit_on_github_project = setup_cfg['github_project'] - edit_on_github_branch = "master" - - edit_on_github_source_root = "" - edit_on_github_doc_root = "docs" - # -- Resolving issue number to links in changelog ----------------------------- -github_issues_url = 'https://github.com/{0}/issues/'.format(setup_cfg['github_project']) +github_issues_url = 'https://github.com/{0}/issues/'.format("wfssrv") # -- Turn on nitpicky mode for sphinx (to warn about references not found) ---- # diff --git a/scripts/sao-wfs-server b/scripts/sao-wfs-server index 421fdd2..fd7c2f4 100755 --- a/scripts/sao-wfs-server +++ b/scripts/sao-wfs-server @@ -13,7 +13,7 @@ if {[info exists env(WFSROOT)]} { if {[info exists env(CONDA_PREFIX)]} { set header_script [file join $env(CONDA_PREFIX) bin wfs_header.py] } else { - set header_script /mmt/conda/bin/wfs_header.py + set header_script /mmt/condaforge/envs/mmtwfs/bin/wfs_header.py } # nominally the port should be 9876, but query for it via DNS to be sure and in case it gets moved From 2600c3a0e83dfd9b0ab312cc75526358646c44c4 Mon Sep 17 00:00:00 2001 From: "T. E. Pickering" Date: Mon, 17 Feb 2025 21:57:13 -0700 Subject: [PATCH 07/12] fix oops in dockerfile --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index cd2a12f..46f8f26 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,8 @@ FROM python:3.13 LABEL maintainer="te.pickering@gmail.com" +COPY . . + RUN python -m pip install --upgrade pip RUN python -m pip install -e . From 17194a071d9883246d07b5c3f40f094aea2bcdaa Mon Sep 17 00:00:00 2001 From: "T. E. Pickering" Date: Tue, 18 Feb 2025 18:20:17 -0700 Subject: [PATCH 08/12] try preinstalling some of the github dependencies --- Dockerfile | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 46f8f26..009b003 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,8 +4,13 @@ LABEL maintainer="te.pickering@gmail.com" COPY . . -RUN python -m pip install --upgrade pip -RUN python -m pip install -e . +RUN apt-get update +RUN apt-get install -y git + +RUN python -m pip install --upgrade pip setuptools setuptools_scm +RUN python -m pip install git+https://github.com/MMTObservatory/camsrv#egg=camsrv +RUN python -m pip install git+https://github.com/MMTObservatory/mmtwfs#egg=mmtwfs +RUN python -m pip install . EXPOSE 8080 From 10727e7abf1ad3ccb52dc3541ad42c432ae3da68 Mon Sep 17 00:00:00 2001 From: "T. E. Pickering" Date: Tue, 18 Feb 2025 18:21:44 -0700 Subject: [PATCH 09/12] updates to the server and main template to reflect some changes in webagg. looks like some bugs in current version of webagg so catch those exceptions and ignore them. plots seem to work and update like they're supposed to, but mouse events don't work. --- wfssrv/templates/wfs.html | 9 ++++-- wfssrv/wfssrv.py | 66 +++++++++++++++++++++++++++++++++------ 2 files changed, 62 insertions(+), 13 deletions(-) diff --git a/wfssrv/templates/wfs.html b/wfssrv/templates/wfs.html index be688ef..c4e31b0 100644 --- a/wfssrv/templates/wfs.html +++ b/wfssrv/templates/wfs.html @@ -4,8 +4,10 @@ - - + + + + @@ -41,7 +43,7 @@ } - + @@ -84,6 +86,7 @@ // The HTML element in which to place the figure $('div#{{ figure }}') ); + {% end %} getZerns(); diff --git a/wfssrv/wfssrv.py b/wfssrv/wfssrv.py index b098c09..efa2197 100644 --- a/wfssrv/wfssrv.py +++ b/wfssrv/wfssrv.py @@ -9,6 +9,10 @@ import os import json import pathlib +import signal +import socket +from pathlib import Path + import redis import numpy as np @@ -35,8 +39,9 @@ import matplotlib import matplotlib.pyplot as plt -from matplotlib.backends.backend_webagg_core import ( - FigureCanvasWebAggCore, +from matplotlib.backends.backend_webagg import ( + FigureCanvasWebAgg, + FigureManagerWebAgg, new_figure_manager_given_figure, ) @@ -89,6 +94,15 @@ def create_default_figures(): class WFSsrv(tornado.web.Application): + + class MplJs(tornado.web.RequestHandler): + def get(self): + self.set_header('Content-Type', 'application/javascript') + + js_content = FigureManagerWebAgg.get_javascript() + + self.write(js_content) + class HomeHandler(tornado.web.RequestHandler): """ Serves the main HTML page. @@ -758,7 +772,10 @@ def on_message(self, message): self.supports_binary = message["value"] else: manager = self.application.fig_id_map[message["figure_id"]] - manager.handle_json(message) + try: + manager.handle_json(message) + except Exception as e: + pass def send_json(self, content): self.write_message(json.dumps(content)) @@ -789,10 +806,9 @@ def refresh_figure(self, k, figure): self.managers[k] = new_figure_manager_given_figure(fignum, figure) self.fig_id_map[fignum] = self.managers[k] else: - canvas = FigureCanvasWebAggCore(figure) + canvas = FigureCanvasWebAgg(figure) self.managers[k].canvas = canvas self.managers[k].canvas.manager = self.managers[k] - self.managers[k]._get_toolbar(canvas) self.managers[k]._send_event("refresh") self.managers[k].canvas.draw() @@ -894,8 +910,20 @@ def __init__(self): self.redis_server = redis.StrictRedis(host=redis_host, port=6379, db=0) handlers = [ + # Static files for the CSS and JS + (r'/_static/(.*)', + tornado.web.StaticFileHandler, + {'path': FigureManagerWebAgg.get_static_file_path()} + ), + + # Static images for the toolbar + (r'/_images/(.*)', + tornado.web.StaticFileHandler, + {'path': Path(matplotlib.get_data_path(), 'images')} + ), + (r"/", self.HomeHandler), - (r"/mpl\.js", tornado.web.RedirectHandler, dict(url="static/js/mpl.js")), + (r"/mpl.js", self.MplJs), (r"/select", self.SelectHandler), (r"/wfspage", self.WFSPageHandler), (r"/connect", self.ConnectHandler), @@ -934,12 +962,30 @@ def main(): application = WFSsrv() http_server = tornado.httpserver.HTTPServer(application) - http_server.listen(8080) - - print("http://127.0.0.1:8080/") + sockets = tornado.netutil.bind_sockets(8080, '') + http_server.add_sockets(sockets) + + for s in sockets: + addr, port = s.getsockname()[:2] + if s.family is socket.AF_INET6: + addr = f'[{addr}]' + print(f"WFSSrv listening on http://{addr}:{port}/") print("Press Ctrl+C to quit") - tornado.ioloop.IOLoop.instance().start() + ioloop = tornado.ioloop.IOLoop.instance() + + def shutdown(): + ioloop.stop() + print("Server stopped") + + old_handler = signal.signal( + signal.SIGINT, + lambda sig, frame: ioloop.add_callback_from_signal(shutdown)) + + try: + ioloop.start() + finally: + signal.signal(signal.SIGINT, old_handler) if __name__ == "__main__": From 3b4d60a0e6b21d2498c31cf7016861d124d86c8f Mon Sep 17 00:00:00 2001 From: "T. E. Pickering" Date: Tue, 18 Feb 2025 18:43:49 -0700 Subject: [PATCH 10/12] codestyle fixes --- wfssrv/wfssrv.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/wfssrv/wfssrv.py b/wfssrv/wfssrv.py index efa2197..bb09688 100644 --- a/wfssrv/wfssrv.py +++ b/wfssrv/wfssrv.py @@ -97,7 +97,7 @@ class WFSsrv(tornado.web.Application): class MplJs(tornado.web.RequestHandler): def get(self): - self.set_header('Content-Type', 'application/javascript') + self.set_header("Content-Type", "application/javascript") js_content = FigureManagerWebAgg.get_javascript() @@ -774,7 +774,7 @@ def on_message(self, message): manager = self.application.fig_id_map[message["figure_id"]] try: manager.handle_json(message) - except Exception as e: + except Exception: pass def send_json(self, content): @@ -911,17 +911,17 @@ def __init__(self): handlers = [ # Static files for the CSS and JS - (r'/_static/(.*)', + ( + r"/_static/(.*)", tornado.web.StaticFileHandler, - {'path': FigureManagerWebAgg.get_static_file_path()} + {"path": FigureManagerWebAgg.get_static_file_path()}, ), - # Static images for the toolbar - (r'/_images/(.*)', + ( + r"/_images/(.*)", tornado.web.StaticFileHandler, - {'path': Path(matplotlib.get_data_path(), 'images')} + {"path": Path(matplotlib.get_data_path(), "images")}, ), - (r"/", self.HomeHandler), (r"/mpl.js", self.MplJs), (r"/select", self.SelectHandler), @@ -962,13 +962,13 @@ def main(): application = WFSsrv() http_server = tornado.httpserver.HTTPServer(application) - sockets = tornado.netutil.bind_sockets(8080, '') + sockets = tornado.netutil.bind_sockets(8080, "") http_server.add_sockets(sockets) for s in sockets: addr, port = s.getsockname()[:2] if s.family is socket.AF_INET6: - addr = f'[{addr}]' + addr = f"[{addr}]" print(f"WFSSrv listening on http://{addr}:{port}/") print("Press Ctrl+C to quit") @@ -979,8 +979,8 @@ def shutdown(): print("Server stopped") old_handler = signal.signal( - signal.SIGINT, - lambda sig, frame: ioloop.add_callback_from_signal(shutdown)) + signal.SIGINT, lambda sig, frame: ioloop.add_callback_from_signal(shutdown) + ) try: ioloop.start() From a778880141c358e544948940a5ed405d20ee1ae9 Mon Sep 17 00:00:00 2001 From: "T. E. Pickering" Date: Wed, 19 Feb 2025 13:41:29 -0700 Subject: [PATCH 11/12] park docker workflow for now --- {.github/workflows => OLD}/docker.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {.github/workflows => OLD}/docker.yml (100%) diff --git a/.github/workflows/docker.yml b/OLD/docker.yml similarity index 100% rename from .github/workflows/docker.yml rename to OLD/docker.yml From a3651cc7226456d6d1c39f7c7c416318a83e2c66 Mon Sep 17 00:00:00 2001 From: "T. E. Pickering" Date: Wed, 26 Feb 2025 12:31:49 -0700 Subject: [PATCH 12/12] remove docker sticker from readme; clean up some links in wfs.html --- README.md | 2 -- wfssrv/templates/wfs.html | 10 +++++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 7f5c9f5..d9eb268 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,4 @@ MMT Wavefront Sensor Analysis Interface ![Python Tests](https://github.com/MMTObservatory/WFSsrv/workflows/Python%20Tests/badge.svg) -![Publish to Docker](https://github.com/MMTObservatory/WFSsrv/workflows/Publish%20to%20Docker/badge.svg) - [![codecov](https://codecov.io/gh/MMTObservatory/WFSsrv/branch/master/graph/badge.svg)](https://codecov.io/gh/MMTObservatory/WFSsrv) diff --git a/wfssrv/templates/wfs.html b/wfssrv/templates/wfs.html index c4e31b0..9048feb 100644 --- a/wfssrv/templates/wfs.html +++ b/wfssrv/templates/wfs.html @@ -4,10 +4,10 @@ - - - - + + + + @@ -43,7 +43,7 @@ } - +