├── docs ├── modes.rst ├── examples │ ├── first.png │ └── imc.png ├── tabs │ ├── api.rst │ ├── index.rst │ ├── websites.rst │ └── modes.rst ├── images │ └── screenshot.png ├── api │ └── index.rst ├── plots.rst ├── states.rst ├── _templates │ ├── autosummary │ │ ├── base.rst │ │ ├── module.rst │ │ └── class.rst │ ├── layout.html │ └── autoclass │ │ └── class.rst ├── automation.rst ├── environment.yml ├── configuration │ ├── index.rst │ ├── format.rst │ └── tabs.rst ├── cli.rst ├── overview.rst ├── index.rst ├── _static │ └── css │ │ └── custom.css ├── Makefile └── make.bat ├── .gitattributes ├── MANIFEST.in ├── .flake8 ├── .codecov.yml ├── .gitignore ├── .codeclimate.yml ├── gwsumm ├── config │ ├── matplotlib.ini │ └── defaults.ini ├── tests │ ├── __init__.py │ ├── common.py │ ├── test_mode.py │ ├── test_utils.py │ ├── test_channels.py │ ├── test_tabs.py │ ├── test_batch.py │ └── test_archive.py ├── html │ ├── tests │ │ ├── __init__.py │ │ ├── test_static.py │ │ ├── test_bootstrap.py │ │ └── test_html5.py │ ├── __init__.py │ ├── static.py │ └── bootstrap.py ├── plot │ ├── guardian │ │ ├── tests │ │ │ ├── __init__.py │ │ │ └── test_main.py │ │ ├── __init__.py │ │ └── __main__.py │ ├── triggers │ │ ├── tests │ │ │ ├── __init__.py │ │ │ └── test_main.py │ │ └── __init__.py │ ├── registry.py │ ├── __init__.py │ ├── utils.py │ └── sei.py ├── units.py ├── __init__.py ├── globalv.py ├── tabs │ ├── __init__.py │ ├── registry.py │ ├── misc.py │ └── stamp.py ├── state │ ├── __init__.py │ ├── all.py │ └── registry.py ├── io.py ├── data │ ├── __init__.py │ ├── utils.py │ └── range.py ├── mode.py └── utils.py ├── .readthedocs.yaml ├── .github └── workflows │ ├── lint.yml │ └── build.yml ├── README.rst ├── CONTRIBUTING.md └── pyproject.toml /docs/modes.rst: -------------------------------------------------------------------------------- 1 | .. automodapi:: gwsumm.mode 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | gwsumm/_version.py export-subst 2 | -------------------------------------------------------------------------------- /docs/examples/first.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gwpy/gwsumm/HEAD/docs/examples/first.png -------------------------------------------------------------------------------- /docs/examples/imc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gwpy/gwsumm/HEAD/docs/examples/imc.png -------------------------------------------------------------------------------- /docs/tabs/api.rst: -------------------------------------------------------------------------------- 1 | ####### 2 | Tab API 3 | ####### 4 | 5 | .. automodapi:: gwsumm.tabs 6 | -------------------------------------------------------------------------------- /docs/images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gwpy/gwsumm/HEAD/docs/images/screenshot.png -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE README.rst MANIFEST.in 2 | include gwsumm/_version.py 3 | include gwsumm/config/*.ini 4 | -------------------------------------------------------------------------------- /docs/api/index.rst: -------------------------------------------------------------------------------- 1 | Full API 2 | -------- 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :glob: 7 | 8 | * 9 | -------------------------------------------------------------------------------- /docs/plots.rst: -------------------------------------------------------------------------------- 1 | ##### 2 | Plots 3 | ##### 4 | 5 | .. currentmodule:: gwsumm.plot 6 | 7 | .. automodule:: gwsumm.plot 8 | -------------------------------------------------------------------------------- /docs/states.rst: -------------------------------------------------------------------------------- 1 | ###### 2 | States 3 | ###### 4 | 5 | .. currentmodule:: gwsumm.state 6 | 7 | .. automodule:: gwsumm.state 8 | :no-members: 9 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = 3 | __pycache__, 4 | .eggs/, 5 | .git/, 6 | build/, 7 | docs/, 8 | gwsumm/_version.py, 9 | per-file-ignores = 10 | __init__.py:F401, 11 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | precision: 2 3 | range: 50..100 4 | round: nearest 5 | status: 6 | project: 7 | default: 8 | target: auto 9 | threshold: 1 10 | patch: 11 | default: 12 | target: 90 13 | -------------------------------------------------------------------------------- /docs/_templates/autosummary/base.rst: -------------------------------------------------------------------------------- 1 | {% if referencefile %} 2 | .. include:: {{ referencefile }} 3 | {% endif %} 4 | 5 | {{ objname }} 6 | {{ underline }} 7 | 8 | .. currentmodule:: {{ module }} 9 | 10 | .. auto{{ objtype }}:: {{ objname }} 11 | -------------------------------------------------------------------------------- /docs/_templates/layout.html: -------------------------------------------------------------------------------- 1 | {% extends "!layout.html" %} 2 | {% set script_files = ['//gwpy.github.io/docs/stable/_static/gwpy_https.js'] + script_files + ['//gwpy.github.io/docs/stable/_static/copybutton.js']%} 3 | {% set bootswatch_css_custom = ['//gwpy.github.io/css/gwpy.css', '//gwpy.github.io/docs/stable/_static/gwpy-sphinx.css']%} 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /dist 3 | /.idea 4 | /gwsumm.egg-info 5 | .DS_store 6 | *.pyc 7 | /gwsumm/_version.py 8 | /docs/_build 9 | /docs/_generated 10 | /docs/api/modules.rst 11 | /docs/api/gwsumm.* 12 | /docs/api/api 13 | /*.patch 14 | .nfs* 15 | *.swp 16 | /.cache/ 17 | /.eggs/ 18 | /*.egg 19 | /share/css/ 20 | .sass-cache 21 | /gwsumm/html/static/ 22 | /.coverage 23 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | duplication: 3 | enabled: true 4 | config: 5 | languages: 6 | - python 7 | fixme: 8 | enabled: true 9 | config: 10 | strings: 11 | - FIXME 12 | - XXX 13 | pep8: 14 | enabled: true 15 | radon: 16 | enabled: true 17 | 18 | ratings: 19 | paths: 20 | - "**.py" 21 | 22 | exclude_paths: 23 | - gwsumm/_version.py 24 | - docs/* 25 | - gwsumm/tests/* 26 | -------------------------------------------------------------------------------- /gwsumm/config/matplotlib.ini: -------------------------------------------------------------------------------- 1 | [tab-plots] 2 | type = plot 3 | name = My plots 4 | 1 = http://matplotlib.org/mpl_examples/pylab_examples/simple_plot.png 5 | 2 = http://matplotlib.org/mpl_examples/subplots_axes_and_figures/subplot_demo.png 6 | 3 = http://matplotlib.org/mpl_examples/statistics/histogram_demo_features.png 7 | layout = 1,2 8 | afterword = All images were taken from matplotlib.org. 9 | -------------------------------------------------------------------------------- /docs/automation.rst: -------------------------------------------------------------------------------- 1 | ############################# 2 | Automatic generation for LIGO 3 | ############################# 4 | 5 | The LIGO Detector Characterization group use the `gwsumm` package to generate 6 | daily, weekly, and monthly summaries of the performance of the LIGO detectors. 7 | These data generation runs are automatically completed using the HTCondor 8 | high-throughput job scheduler. 9 | 10 | Members of the LIGO Scientific Collaboration or the Virgo Collaboration 11 | can view more details on the HTCondor configuration 12 | `here `_. 13 | -------------------------------------------------------------------------------- /docs/environment.yml: -------------------------------------------------------------------------------- 1 | name: gwsumm 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | # build 6 | - pip 7 | - setuptools 8 | - setuptools-scm 9 | - wheel 10 | # install 11 | - astropy >=3.0.0 12 | - gwdatafind >=1.1.1 13 | - gwdetchar >=2.3.2 14 | - gwpy >=3.0.9 15 | - gwtrigfind 16 | - lalsuite 17 | - lscsoft-glue >=1.60.0 18 | - lxml 19 | - markdown 20 | - MarkupPy 21 | - matplotlib >=3.5 22 | - numpy >=1.16 23 | - pygments >=2.7.0 24 | - python-dateutil 25 | - igwn-ligolw 26 | - scipy >=1.2.0 27 | # docs 28 | - numpydoc 29 | - sphinx 30 | - sphinx-automodapi !=0.17.0 31 | - sphinx_bootstrap_theme 32 | - sphinxcontrib-programoutput 33 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the OS, Python version and other tools you might need 9 | build: 10 | os: "ubuntu-24.04" 11 | tools: 12 | python: "miniconda-latest" 13 | 14 | conda: 15 | environment: docs/environment.yml 16 | 17 | # Build documentation in the docs/ directory with Sphinx 18 | sphinx: 19 | configuration: docs/conf.py 20 | 21 | # Build docs in additional formats 22 | formats: all 23 | 24 | # Set the version of Python and requirements required to build your docs 25 | python: 26 | install: 27 | - method: pip 28 | path: . 29 | extra_requirements: 30 | - doc 31 | -------------------------------------------------------------------------------- /gwsumm/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Duncan Macleod (2013) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """Test suite 20 | 21 | """ 22 | 23 | __author__ = 'Duncan Macleod ' 24 | -------------------------------------------------------------------------------- /gwsumm/html/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Alex Urban (2019) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """Unit tests for gwsumm.html 20 | """ 21 | 22 | __author__ = 'Alex Urban ' 23 | -------------------------------------------------------------------------------- /docs/_templates/autosummary/module.rst: -------------------------------------------------------------------------------- 1 | {% if referencefile %} 2 | .. include:: {{ referencefile }} 3 | {% endif %} 4 | 5 | {{ objname }} 6 | {{ underline }} 7 | 8 | .. automodule:: {{ fullname }} 9 | 10 | {% block functions %} 11 | {% if functions %} 12 | .. rubric:: Functions 13 | 14 | .. autosummary:: 15 | {% for item in functions %} 16 | {{ item }} 17 | {%- endfor %} 18 | {% endif %} 19 | {% endblock %} 20 | 21 | {% block classes %} 22 | {% if classes %} 23 | .. rubric:: Classes 24 | 25 | .. autosummary:: 26 | {% for item in classes %} 27 | {{ item }} 28 | {%- endfor %} 29 | {% endif %} 30 | {% endblock %} 31 | 32 | {% block exceptions %} 33 | {% if exceptions %} 34 | .. rubric:: Exceptions 35 | 36 | .. autosummary:: 37 | {% for item in exceptions %} 38 | {{ item }} 39 | {%- endfor %} 40 | {% endif %} 41 | {% endblock %} 42 | -------------------------------------------------------------------------------- /gwsumm/plot/guardian/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Alex Urban (2020) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """Unit tests for gwsumm.plot.guardian 20 | """ 21 | 22 | __author__ = 'Alex Urban ' 23 | -------------------------------------------------------------------------------- /gwsumm/plot/triggers/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Alex Urban (2020) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """Unit tests for gwsumm.plot.triggers 20 | """ 21 | 22 | __author__ = 'Alex Urban ' 23 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | # --------------------------- 2 | # 3 | # Check the source files for quality issues 4 | # 5 | # --------------------------- 6 | 7 | name: Lint 8 | 9 | on: 10 | push: 11 | branches: 12 | - main 13 | - master 14 | - release/** 15 | pull_request: 16 | branches: 17 | - main 18 | - master 19 | - release/** 20 | 21 | concurrency: 22 | group: ${{ github.workflow }}-${{ github.ref }} 23 | cancel-in-progress: true 24 | 25 | jobs: 26 | flake8: 27 | name: Flake8 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v5 31 | - name: Set up Python 32 | uses: actions/setup-python@v6 33 | with: 34 | python-version: '3.x' 35 | - name: Install dependencies 36 | run: | 37 | python -m pip install --upgrade pip 38 | python -m pip install "flake8>=3.7.0" 39 | - name: Lint with flake8 40 | run: python -m flake8 . 41 | -------------------------------------------------------------------------------- /docs/configuration/index.rst: -------------------------------------------------------------------------------- 1 | ######################################## 2 | How to write your own configuration file 3 | ######################################## 4 | 5 | GWSumm makes use of INI-format configuration files to customise which data 6 | should be read, how it should be manipulated, and how it should be displayed. 7 | Through the `gw_summary` executable, all input from the user, excepting a 8 | small number of command-line options, 9 | are provided through one or more INI-format configuration files. 10 | These files contain sets of ``key = value`` pairs grouped into named 11 | ``[sections]`` to define everything from the required data inputs for a given 12 | page of output, to whether or not to display the gravitational-wave amplitude 13 | spectrum with a logarithmic frequency scale. 14 | 15 | Each of the following pages will explain how to write an INI configuration 16 | file to setup pretty much any data you could want: 17 | 18 | .. toctree:: 19 | :maxdepth: 2 20 | 21 | format 22 | tabs 23 | data 24 | -------------------------------------------------------------------------------- /gwsumm/plot/guardian/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Alex Urban (2020) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """Submodule for plots of Guardian data 20 | """ 21 | 22 | __author__ = 'Alex Urban ' 23 | __credits__ = 'Duncan Macleod ' 24 | 25 | from .core import ( 26 | GuardianStatePlot, 27 | ) 28 | -------------------------------------------------------------------------------- /gwsumm/plot/triggers/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Alex Urban (2020) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """Submodule for plots of event triggers 20 | """ 21 | 22 | __author__ = 'Alex Urban ' 23 | __credits__ = 'Duncan Macleod ' 24 | 25 | from .core import ( 26 | TriggerPlotMixin, 27 | TriggerDataPlot, 28 | TriggerTimeSeriesDataPlot, 29 | TriggerHistogramPlot, 30 | TriggerRateDataPlot, 31 | ) 32 | -------------------------------------------------------------------------------- /gwsumm/units.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Duncan Macleod (2013) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """Extra units for GW data processing 20 | """ 21 | 22 | __author__ = 'Duncan Macleod ' 23 | 24 | from astropy import units 25 | import gwpy.detector.units # noqa: F401 26 | 27 | _ns = {} # collector for new units 28 | 29 | # Virgo units 30 | units.def_unit('state', namespace=_ns) 31 | 32 | # -- register new units ----- 33 | units.add_enabled_units(_ns) 34 | -------------------------------------------------------------------------------- /docs/_templates/autoclass/class.rst: -------------------------------------------------------------------------------- 1 | {% if '__init__' in methods %} 2 | {{ methods.remove('__init__') }} 3 | {% endif %} 4 | 5 | .. code-block:: python 6 | 7 | >>> from {{ module }} import {{ name }} 8 | 9 | {{ docstring }} 10 | 11 | {% block attributes_summary %} 12 | {% if attributes %} 13 | 14 | .. rubric:: Attributes Summary 15 | 16 | .. autosummary:: 17 | {% for item in attributes %} 18 | ~{{ name }}.{{ item }} 19 | {%- endfor %} 20 | 21 | {% endif %} 22 | {% endblock %} 23 | 24 | {% block methods_summary %} 25 | {% if methods %} 26 | 27 | .. rubric:: Methods Summary 28 | 29 | .. autosummary:: 30 | {% for item in methods %} 31 | ~{{ name }}.{{ item }} 32 | {%- endfor %} 33 | 34 | {% endif %} 35 | {% endblock %} 36 | 37 | {% block attributes_documentation %} 38 | {% if attributes %} 39 | 40 | .. rubric:: Attributes Documentation 41 | 42 | {% for item in attributes %} 43 | .. autoattribute:: {{ item }} 44 | {%- endfor %} 45 | 46 | {% endif %} 47 | {% endblock %} 48 | 49 | {% block methods_documentation %} 50 | {% if methods %} 51 | 52 | .. rubric:: Methods Documentation 53 | 54 | {% for item in methods %} 55 | .. automethod:: {{ item }} 56 | {%- endfor %} 57 | 58 | {% endif %} 59 | {% endblock %} 60 | -------------------------------------------------------------------------------- /gwsumm/tests/common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Duncan Macleod (2013) 3 | # 4 | # This file is part of GWpy. 5 | # 6 | # GWpy is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWpy is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWpy. If not, see . 18 | 19 | """Compatibility module 20 | """ 21 | 22 | from functools import wraps 23 | 24 | from .. import globalv 25 | 26 | 27 | # -- test decorators ---------------------------------------------------------- 28 | 29 | def empty_globalv_CHANNELS(f): 30 | @wraps(f) 31 | def wrapped_f(*args, **kwargs): 32 | _channels = globalv.CHANNELS 33 | globalv.CHANNELS = type(globalv.CHANNELS)() 34 | try: 35 | return f(*args, **kwargs) 36 | finally: 37 | globalv.CHANNELS = _channels 38 | return wrapped_f 39 | -------------------------------------------------------------------------------- /gwsumm/html/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Duncan Macleod (2013) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """HTML helpers 20 | 21 | HTML output is built upon the 22 | `markup.py module `_ and primarily 23 | formatted to fit the 24 | `twitter bootstrap library `_. 25 | """ 26 | 27 | from .static import ( 28 | get_css, 29 | get_js, 30 | ) 31 | from .html5 import ( 32 | _expand_path, 33 | load_state, 34 | load, 35 | comments_box, 36 | ldvw_qscan, 37 | dialog_box, 38 | overlay_canvas, 39 | ) 40 | from .bootstrap import ( 41 | banner, 42 | calendar, 43 | wrap_content, 44 | state_switcher, 45 | base_map_dropdown, 46 | ) 47 | 48 | __author__ = 'Duncan Macleod ' 49 | -------------------------------------------------------------------------------- /docs/_templates/autosummary/class.rst: -------------------------------------------------------------------------------- 1 | {% if referencefile %} 2 | .. include:: {{ referencefile }} 3 | {% endif %} 4 | 5 | {{ objname }} 6 | {{ underline }} 7 | 8 | .. currentmodule:: {{ module }} 9 | 10 | .. autoclass:: {{ objname }} 11 | :show-inheritance: 12 | :no-inherited-members: 13 | 14 | {% if '__init__' in methods %} 15 | {{ methods.remove('__init__') }} 16 | {% endif %} 17 | 18 | {% block attributes_summary %} 19 | {% if attributes %} 20 | 21 | .. rubric:: Attributes Summary 22 | 23 | .. autosummary:: 24 | {% for item in attributes %} 25 | ~{{ name }}.{{ item }} 26 | {%- endfor %} 27 | 28 | {% endif %} 29 | {% endblock %} 30 | 31 | {% block methods_summary %} 32 | {% if methods %} 33 | 34 | .. rubric:: Methods Summary 35 | 36 | .. autosummary:: 37 | {% for item in methods %} 38 | ~{{ name }}.{{ item }} 39 | {%- endfor %} 40 | 41 | {% endif %} 42 | {% endblock %} 43 | 44 | {% block attributes_documentation %} 45 | {% if attributes %} 46 | 47 | .. rubric:: Attributes Documentation 48 | 49 | {% for item in attributes %} 50 | .. autoattribute:: {{ item }} 51 | {%- endfor %} 52 | 53 | {% endif %} 54 | {% endblock %} 55 | 56 | {% block methods_documentation %} 57 | {% if methods %} 58 | 59 | .. rubric:: Methods Documentation 60 | 61 | {% for item in methods %} 62 | .. automethod:: {{ item }} 63 | {%- endfor %} 64 | 65 | {% endif %} 66 | {% endblock %} 67 | -------------------------------------------------------------------------------- /gwsumm/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Duncan Macleod (2013) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """Gravitational-wave interferometer summary information system 20 | 21 | """ 22 | 23 | from . import ( 24 | globalv, # creates global variables 25 | units, # registers custom units 26 | ) 27 | 28 | try: 29 | from ._version import version as __version__ 30 | except ModuleNotFoundError: 31 | try: 32 | import setuptools_scm 33 | __version__ = setuptools_scm.get_version(fallback_version='?.?.?') 34 | except (ModuleNotFoundError, TypeError, LookupError): 35 | __version__ = '?.?.?' 36 | 37 | __author__ = 'Duncan Macleod ' 38 | __credits__ = ('Alex Urban , ', 39 | 'Evan Goetz ') 40 | -------------------------------------------------------------------------------- /gwsumm/globalv.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Duncan Macleod (2013) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | 20 | """Set of global memory variables for GWSumm package 21 | """ 22 | 23 | import time 24 | 25 | from gwpy.time import to_gps 26 | from gwpy.segments import DataQualityDict 27 | from gwpy.detector import ChannelList 28 | 29 | CHANNELS = ChannelList() 30 | STATES = {} 31 | 32 | DATA = {} 33 | SPECTROGRAMS = {} 34 | SPECTRUM = {} 35 | COHERENCE_COMPONENTS = {} 36 | COHERENCE_SPECTRUM = {} 37 | SEGMENTS = DataQualityDict() 38 | TRIGGERS = {} 39 | 40 | VERBOSE = False 41 | PROFILE = False 42 | START = time.time() 43 | 44 | # run time variables 45 | MODE = 0 46 | WRITTEN_PLOTS = [] 47 | NOW = int(to_gps('now')) 48 | HTMLONLY = False 49 | 50 | # comments 51 | IFO = None 52 | HTML_COMMENTS_NAME = None 53 | -------------------------------------------------------------------------------- /docs/cli.rst: -------------------------------------------------------------------------------- 1 | .. _cli-page: 2 | 3 | ###################### 4 | Command-line interface 5 | ###################### 6 | 7 | GW Summary 8 | ========== 9 | 10 | The primary interface for GWSumm is a command-line utility called `gw_summary`. 11 | For a full explanation of the available command-line arguments and options, you 12 | can run 13 | 14 | .. command-output:: python ../bin/gw_summary --help 15 | 16 | This tool can be run in four modes: daily, weekly, and monthly analyses, and 17 | a specific range of GPS times. 18 | 19 | Day mode 20 | -------- 21 | 22 | To run in daily summary mode, the following command-line options are available: 23 | 24 | .. command-output:: python gw_summary day --help 25 | 26 | Week mode 27 | --------- 28 | 29 | The arguments in weekly mode are as follows: 30 | 31 | .. command-output:: python gw_summary week --help 32 | 33 | Month mode 34 | ---------- 35 | 36 | In monthly mode: 37 | 38 | .. command-output:: python gw_summary month --help 39 | 40 | GPS mode 41 | -------- 42 | 43 | To run within a specific (but arbitrary) range of GPS seconds: 44 | 45 | .. command-output:: python gw_summary gps --help 46 | 47 | Batch mode 48 | ========== 49 | 50 | To stage a batch of analyses with a large collection of configuration files, 51 | as is done in embarrassingly parallel fashion when the summary pages run 52 | online, you can use the `gw_summary_pipe` command-line utility. This tool 53 | uses `HT Condor `_ to schedule 54 | and run jobs in parallel. 55 | 56 | To see all the available arguments and options for this tool, you can run 57 | with `--help` as usual: 58 | 59 | .. command-output:: python gw_summary_pipe --help 60 | -------------------------------------------------------------------------------- /docs/tabs/index.rst: -------------------------------------------------------------------------------- 1 | .. _tabs: 2 | 3 | .. currentmodule:: gwsumm.tabs 4 | 5 | #################### 6 | Introduction to Tabs 7 | #################### 8 | 9 | GWsumm can be used either from the command line as described in 10 | :ref:`CLI interface ` or as a package to progromatically 11 | generate pages called "tabs". 12 | 13 | A `Tab` is a single, configurable page of output, containing some data. 14 | Each `Tab` is written in its own HTML page, and can be written to contain 15 | any set of data, with any format. 16 | 17 | The basic object provided by :mod:`gwsumm.tabs` is the `Tab`, which allows 18 | embedding of arbitrary HTML text into a standard surrounding HTML framework. 19 | The `Tab` also defines the API for other tabs. 20 | 21 | ---------------- 22 | Simple `Tab` use 23 | ---------------- 24 | 25 | A simple `Tab` can be created in only two steps 26 | 27 | .. code-block:: python 28 | 29 | from gwsumm.tabs import Tab 30 | mytab = Tab('My first tab') 31 | mytab.write_html("This tab doesn't do very much") 32 | 33 | This will create a directory under the current one, 34 | 35 | - ``my_first_tab/`` containing the HTML for the new tab 36 | 37 | The output webpage looks like: 38 | 39 | .. image:: examples/first.png 40 | :width: 80% 41 | :align: center 42 | :alt: First GWSumm example screenshot 43 | 44 | The content given to `Tab.write_html` is passed along untouched, so can contain any HTML you like. 45 | 46 | ------------------- 47 | Generating websites 48 | ------------------- 49 | 50 | The :ref:`next page ` will guide you through created groups of tabs 51 | and combining them to generate a fully-fledged website complete with 52 | navigation. 53 | -------------------------------------------------------------------------------- /gwsumm/tabs/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Duncan Macleod (2013-2016) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """This module defines the `Tab` API, and all of the built-in tab objects 20 | """ 21 | 22 | # core 23 | from .registry import ( 24 | register_tab, 25 | get_tab, 26 | ) 27 | from .core import ( 28 | _MetaTab, 29 | BaseTab, 30 | StaticTab, 31 | GpsTab, 32 | IntervalTab, 33 | EventTab, 34 | Tab, 35 | ParentTab, 36 | TabList, 37 | ) 38 | from .builtin import ( 39 | ExternalTab, 40 | PlotTab, 41 | StateTab, 42 | UrlTab, 43 | ) 44 | from .misc import ( 45 | AboutTab, 46 | Error404Tab, 47 | ) 48 | 49 | # data 50 | from .data import (ProcessedTab, DataTab) 51 | 52 | # application-specific extras 53 | from .sei import SEIWatchDogTab 54 | from .guardian import GuardianTab 55 | from .stamp import StampPEMTab 56 | from .management import AccountingTab 57 | from .etg import EventTriggerTab 58 | from .fscan import FscanTab 59 | from .gracedb import GraceDbTab 60 | 61 | __author__ = 'Duncan Macleod ' 62 | -------------------------------------------------------------------------------- /docs/configuration/format.rst: -------------------------------------------------------------------------------- 1 | ##################################### 2 | A whistle-stop tour of the INI format 3 | ##################################### 4 | 5 | The INI file syntax is an intertionally-recognised method of provided basic 6 | configuration options to any program, and is used extensively in, amongst 7 | other projects, the red-hat linux distrobution. 8 | Any `INI` file can be as simple as one line containing a `key` -- the name 9 | of a given variable, perhaps -- and a corresponding `value`: 10 | 11 | .. code-block:: ini 12 | 13 | name = John Doe 14 | 15 | Most parsers would then return a variable ``name`` containg the string value 16 | ``John Doe``, or at least something very similar. 17 | 18 | Moving beyond a simple set of variables and their values, the INI format 19 | supports grouping the key-value pairs into named sections. 20 | Any text enclosed in square brackets (``[]``) is interpreted as a section 21 | name: 22 | 23 | .. code-block:: ini 24 | 25 | [haggis] 26 | name = Haggis, Neeps, and Tatties 27 | ingredients = haggis, neeps, tatties 28 | time = 2.0 29 | 30 | [marsbar] 31 | name = Deep-fried Mars bar 32 | ingredients = Mars bar 33 | utensils = Deep-fat frier 34 | time = 0.01 35 | 36 | The above example, an extract from the author's recipe book, contains two 37 | sections with unique names, and a matchine set of keys describing the basic 38 | information about each dish. 39 | In this case, a standard file-parser would return a structured object with 40 | two sub-structures, each representing one section of the file. 41 | 42 | For more details and a good selections of examples, please see the 43 | documentation of the python :mod:`ConfigParser` module. 44 | 45 | .. warning:: 46 | 47 | By default, the python :mod:`ConfigParser` module allows separation of 48 | keys and values using either the colon (``:``) or the equals sign (``=``), 49 | however, in order to increase functionality and user-friendliness, 50 | `gwsumm` configuration files must use only the equals sign (``=``) for 51 | this purpose. Do not use colons to separate key-value pairs. -------------------------------------------------------------------------------- /gwsumm/tests/test_mode.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Duncan Macleod (2013) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """Test suite for the mode module 20 | 21 | """ 22 | 23 | import datetime 24 | 25 | import pytest 26 | 27 | from .. import (globalv, mode) 28 | 29 | __author__ = 'Duncan Macleod ' 30 | 31 | DEFAULT_MODE = mode.get_mode() 32 | 33 | 34 | def teardown_module(): 35 | """Undo any set_mode() operations from this module 36 | """ 37 | mode.set_mode(DEFAULT_MODE) 38 | 39 | 40 | def test_get_mode(): 41 | assert mode.get_mode().value == globalv.MODE 42 | assert mode.get_mode(10) == mode.Mode.day 43 | assert mode.get_mode('week') == mode.Mode.week 44 | with pytest.raises(ValueError): 45 | mode.get_mode('invalid mode') 46 | 47 | 48 | def test_set_mode(): 49 | mode.set_mode(0) 50 | assert globalv.MODE == mode.Mode(0).value 51 | 52 | mode.set_mode('GPS') 53 | assert globalv.MODE == mode.Mode.gps.value 54 | 55 | with pytest.raises(ValueError): 56 | mode.set_mode('invalid mode') 57 | 58 | 59 | @pytest.mark.parametrize('m, basestr', [ 60 | ('day', 'day/20150914'), 61 | ('week', 'week/20150914'), 62 | ('month', 'month/201509'), 63 | ('year', 'year/2015'), 64 | ]) 65 | def test_get_base(m, basestr): 66 | date = datetime.date(2015, 9, 14) 67 | mode.set_mode(m) 68 | assert mode.get_base(date) == basestr 69 | -------------------------------------------------------------------------------- /gwsumm/html/tests/test_static.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Alex Urban (2019) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """Unit tests for gwsumm.html.static 20 | """ 21 | 22 | __author__ = 'Alex Urban ' 23 | 24 | from collections import OrderedDict 25 | 26 | from .. import static 27 | 28 | 29 | # test simple utils 30 | 31 | def test_get_css(): 32 | css = static.get_css() 33 | assert isinstance(css, OrderedDict) 34 | # test dict keys 35 | assert list(css.keys()) == [ 36 | 'font-awesome', 37 | 'font-awesome-solid', 38 | 'gwbootstrap', 39 | ] 40 | # test list of files 41 | css_files = list(x.split('/')[-1] for x in css.values()) 42 | assert css_files == [ 43 | 'fontawesome.min.css', 44 | 'solid.min.css', 45 | 'gwbootstrap.min.css', 46 | ] 47 | 48 | 49 | def test_get_js(): 50 | js = static.get_js() 51 | assert isinstance(js, OrderedDict) 52 | # test dict keys 53 | assert list(js.keys()) == [ 54 | 'jquery', 55 | 'jquery-ui', 56 | 'moment', 57 | 'bootstrap', 58 | 'fancybox', 59 | 'datepicker', 60 | 'gwbootstrap', 61 | ] 62 | # test list of files 63 | js_files = list(x.split('/')[-1] for x in js.values()) 64 | assert js_files == [ 65 | 'jquery-3.7.1.min.js', 66 | 'jquery-ui.min.js', 67 | 'moment.min.js', 68 | 'bootstrap.bundle.min.js', 69 | 'fancybox.umd.js', 70 | 'bootstrap-datepicker.min.js', 71 | 'gwbootstrap-extra.min.js', 72 | ] 73 | -------------------------------------------------------------------------------- /gwsumm/state/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Duncan Macleod (2013) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """A `SummaryState` defines a sub-set of time over which a `~gwsumm.tabs.Tab` 20 | should be processed. 21 | Each `SummaryState` is normally tied to one or more data-quality flags marking 22 | times during which each of the LIGO instruments was operating in a certain 23 | configuration, or was subject to a known noise interference. 24 | 25 | ================== 26 | The state registry 27 | ================== 28 | 29 | GWSumm defines a state 'registry', simply a record of all `SummaryState` 30 | objects that have been defined (and registered) so far in a given program. 31 | The registry just makes remembering states in complicated programs a little 32 | easier. 33 | 34 | Any `SummaryState` can be registered with an arbitrary name as follows:: 35 | 36 | >>> from gwsumm.state.registry import register_state 37 | >>> register_state(mystate, 'my state') 38 | 39 | and can be recovered later:: 40 | 41 | >>> from gwsumm.state.registry import get_state 42 | >>> mystate = get_state('my state') 43 | 44 | ============= 45 | API reference 46 | ============= 47 | 48 | .. autosummary:: 49 | :toctree: api 50 | 51 | SummaryState 52 | get_state 53 | get_states 54 | register_state 55 | 56 | """ 57 | 58 | from .core import SummaryState 59 | from .registry import (get_state, get_states, register_state) 60 | from .all import (ALLSTATE, generate_all_state) 61 | 62 | __all__ = ['ALLSTATE', 'SummaryState', 'get_state', 'get_states', 63 | 'register_state', 'generate_all_state'] 64 | -------------------------------------------------------------------------------- /gwsumm/state/all.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Duncan Macleod (2013) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """Definition of the 'All' state. 20 | 21 | This is a special `SummaryState` that has valid and active segments spanning 22 | the full analysis interval. 23 | """ 24 | 25 | from gwpy.segments import (Segment, SegmentList) 26 | 27 | from ..globalv import NOW 28 | from .core import SummaryState 29 | from .registry import register_state 30 | 31 | __author__ = 'Duncan Macleod ' 32 | 33 | ALLSTATE = 'all' 34 | 35 | 36 | def generate_all_state(start, end, register=True, **kwargs): 37 | """Build a new `SummaryState` for the given [start, end) interval. 38 | 39 | Parameters 40 | ---------- 41 | start : `~gwpy.time.LIGOTimeGPS`, float 42 | the GPS start time of the current analysis 43 | end : `~gwpy.time.LIGOTimeGPS`, float 44 | the GPS end time of the current analysis 45 | register : `bool`, optional 46 | should the new `SummaryState` be registered, default `True` 47 | **kwargs 48 | other keyword arguments passed to the `SummaryState` constructor 49 | 50 | Returns 51 | ------- 52 | allstate : `SummaryState` 53 | the newly created 'All' `SummaryState` 54 | """ 55 | now = min(end, NOW) 56 | all_ = SummaryState(ALLSTATE, 57 | known=SegmentList([Segment(start, end)]), 58 | active=SegmentList([Segment(start, now)]), 59 | **kwargs) 60 | all_.ready = True 61 | if register: 62 | register_state(all_) 63 | return all_ 64 | -------------------------------------------------------------------------------- /gwsumm/plot/registry.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Duncan Macleod (2013) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """Registry for GWSumm output plot types 20 | 21 | All plot types should be registered for easy identification from the 22 | configuration INI files 23 | """ 24 | 25 | import re 26 | 27 | __author__ = 'Duncan Macleod ' 28 | 29 | _PLOTS = {} 30 | 31 | 32 | def register_plot(plot, name=None, force=False): 33 | """Register a new summary `Plot` to the given ``name`` 34 | 35 | Parameters 36 | ---------- 37 | name : `str` 38 | unique descriptive name for this type of plot, must not 39 | contain any spaces, e.g. 'timeseries' 40 | plotclass : `type` 41 | defining Class for this plot type 42 | force : `bool` 43 | overwrite existing registration for this type 44 | 45 | Raises 46 | ------ 47 | ValueError 48 | if name is already registered and ``force`` not given as `True` 49 | """ 50 | if name is None: 51 | name = plot.type 52 | if name not in _PLOTS or force: 53 | _PLOTS[name] = plot 54 | else: 55 | raise ValueError("Plot '%s' has already been registered to the %s " 56 | "class" % (name, plot.__name__)) 57 | 58 | 59 | def get_plot(name): 60 | """Query the registry for the plot class registered to the given 61 | name 62 | """ 63 | if isinstance(name, str): 64 | name = re.sub(r'[\'\"]', '', name) 65 | try: 66 | return _PLOTS[name] 67 | except KeyError: 68 | raise ValueError("No TabPlot registered with name '%s'" % name) 69 | -------------------------------------------------------------------------------- /gwsumm/io.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Duncan Macleod (2017) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """Input/output utilities 20 | """ 21 | 22 | import os.path 23 | import re 24 | 25 | from astropy.io.registry import IORegistryError 26 | 27 | from gwpy.frequencyseries import FrequencySeries 28 | 29 | __author__ = 'Duncan Macleod ' 30 | 31 | HDF5_FILENAME = re.compile(r'(?P(.hdf5|.hdf|.h5))\/') 32 | 33 | 34 | def read_frequencyseries(filename): 35 | """Read a `~gwpy.frequencyseries.FrequencySeries` from a file 36 | 37 | IF using HDF5, the filename can be given as a combined filename/path, i.e. 38 | ``test.hdf5/path/to/dataset``. 39 | 40 | Parameters 41 | ---------- 42 | filename : `str` 43 | path of file to read 44 | 45 | Returns 46 | ------- 47 | series : `~gwpy.frequencyseries.FrequencySeries` 48 | the data as read 49 | 50 | Raises 51 | ------ 52 | astropy.io.registry.IORegistryError 53 | if the input format cannot be identified or is not registered 54 | """ 55 | # try and parse path in HDF5 file if given 56 | try: 57 | ext = HDF5_FILENAME.search(filename).groupdict()['ext'] 58 | except AttributeError: # no match 59 | kwargs = {} 60 | else: 61 | kwargs = {'path': filename.rsplit(ext, 1)[1]} 62 | # read file 63 | try: 64 | return FrequencySeries.read(filename, **kwargs) 65 | except IORegistryError: 66 | if filename.endswith('.gz'): 67 | fmt = os.path.splitext(filename[:-3])[-1] 68 | else: 69 | fmt = os.path.splitext(filename)[-1] 70 | return FrequencySeries.read(filename, format=fmt.lstrip('.'), **kwargs) 71 | -------------------------------------------------------------------------------- /gwsumm/tabs/registry.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Duncan Macleod (2013) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """Registry for GWSumm data tabs. 20 | 21 | All Tabs should be registered for easy identification from the 22 | configuration INI files 23 | """ 24 | 25 | from ..utils import re_quote 26 | 27 | __author__ = 'Duncan Macleod ' 28 | 29 | __all__ = ['register_tab', 'get_tab'] 30 | 31 | _TABS = {} 32 | 33 | 34 | def register_tab(tab, name=None, force=False): 35 | """Register a new summary `Tab` to the given ``name`` 36 | 37 | Parameters 38 | ---------- 39 | tab : `type` 40 | defining Class for this tab type. 41 | name : `str`, optional 42 | unique descriptive name for this type of tab, must not 43 | contain any spaces, e.g. 'hveto'. If ``name=None``, the `Tab.type` 44 | class attribute of the given tab will be used. 45 | force : `bool` 46 | overwrite existing registration for this type 47 | 48 | Raises 49 | ------ 50 | ValueError 51 | if name is already registered and ``force`` not given as `True` 52 | """ 53 | if name is None: 54 | name = tab.type 55 | if name not in _TABS or force: 56 | _TABS[name] = tab 57 | else: 58 | raise ValueError("Tab '%s' has already been registered to the %s " 59 | "class" % (name, _TABS[name].__name__)) 60 | 61 | 62 | def get_tab(name): 63 | """Query the registry for the tab class registered to the given 64 | name 65 | """ 66 | name = re_quote.sub('', name) 67 | try: 68 | return _TABS[name] 69 | except KeyError: 70 | raise ValueError("No Tab registered with name '%s'" % name) 71 | -------------------------------------------------------------------------------- /gwsumm/data/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Duncan Macleod (2013-2016) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """Methods and classes for loading and pre-processing data 20 | 21 | Each of the sub-modules are designed to read or create the data requested 22 | only once, with the containers from the `globalv` module used as a storage 23 | buffer for each unique data type 24 | """ 25 | 26 | # read TimeSeries data 27 | from .timeseries import ( 28 | _urlpath, 29 | _get_timeseries_dict, 30 | sieve_cache, 31 | find_frames, 32 | find_best_frames, 33 | find_frame_type, 34 | frame_trend_type, 35 | get_channel_type, 36 | exclude_short_trend_segments, 37 | all_adc, 38 | get_timeseries_dict, 39 | locate_data, 40 | get_timeseries, 41 | add_timeseries, 42 | resample_timeseries_dict, 43 | filter_timeseries, 44 | ) 45 | 46 | # generate Spectrograms and FrequencySeries 47 | from .spectral import ( 48 | _get_spectrum, 49 | _get_spectrogram, 50 | get_spectrogram, 51 | add_spectrogram, 52 | get_spectrograms, 53 | size_for_spectrogram, 54 | apply_transfer_function_series, 55 | get_spectrum, 56 | ) 57 | 58 | # generate coherence Spectrograms and Spectra 59 | from .coherence import ( 60 | _get_from_list, 61 | _get_coherence_spectrogram, 62 | get_coherence_spectrogram, 63 | get_coherence_spectrum, 64 | add_coherence_component_spectrogram, 65 | get_coherence_spectrograms, 66 | complex_percentile, 67 | ) 68 | 69 | # generate TimeSeries of sensitive distance (range) 70 | from .range import ( 71 | _metadata, 72 | _segments_diff, 73 | get_range_channel, 74 | get_range, 75 | get_range_spectrogram, 76 | get_range_spectrum, 77 | ) 78 | -------------------------------------------------------------------------------- /docs/tabs/websites.rst: -------------------------------------------------------------------------------- 1 | .. _websites: 2 | 3 | .. currentmodule:: gwsumm.tabs 4 | 5 | Generating Websites 6 | =================== 7 | 8 | :ref:`As we have seen `, generating standalone pages is trivial using GWSumm. What would be more useful would be to generate linked sets of pages, aka a website. 9 | 10 | Navigation 11 | ---------- 12 | 13 | The key difference between standalone pages and a website is the ability to navigate between them. The `Tab.write_html` method will take care of that for you if you pass it all of the tabs: 14 | 15 | .. code-block:: python 16 | 17 | from gwsumm.tabs import Tab 18 | tab1 = Tab('Tab 1') 19 | tab2 = Tab('Tab 2') 20 | tabs = [tab1, tab2] 21 | tab1.write_html('This is tab 1', tabs=tabs) 22 | tab2.write_html('This is tab 2', tabs=tabs) 23 | 24 | This will write each tab into its own directory, as before, but the HTML will now contain an `` block above the banner to allow navigation between the pages. 25 | 26 | Tab parents 27 | ----------- 28 | 29 | In the above example, each tab is included as a link in the navigation bar. However, in larger websites with many pages, the navigation can quickly become cluttered and will start to overflow the width of the page. 30 | This can be avoided by declaring `~Tab.parent` for sets of tabs: 31 | 32 | .. code-block:: python 33 | 34 | tab1 = Tab('Tab 1') 35 | tab2a = Tab('A', parent='Tab 2') 36 | tab2b = Tab('B', parent=tab2a.parent) 37 | tabs = [tab1, tab2a, tab2b] 38 | tab1.write_html('This is tab 1', tabs=tabs) 39 | tab2a.write_html('This is tab 2A', tabs=tabs) 40 | tab2b.write_html('This is tab 2B', tabs=tabs) 41 | 42 | Here we have set a `parent` tab for 2A, and used the same for 2B, which creates a dropdown menu in the navigation bar linking to these tabs. 'Tab 2' is never created, but is used only for navigation. 43 | 44 | Tab groups 45 | ---------- 46 | 47 | For even larger websites, sets of tabs under a single parent can be 48 | further separated into `groups `. For example, to put 2A 49 | into group `1` and 2B into group `2`, we can write: 50 | 51 | .. code-block:: python 52 | 53 | tab1 = Tab('Tab 1') 54 | tab2a = Tab('A', parent='Tab 2', group='1') 55 | tab2b = Tab('B', parent=tab2a.parent, group='2') 56 | tabs = [tab1, tab2a, tab2b] 57 | tab1.write_html('This is tab 1', tabs=tabs) 58 | tab2a.write_html('This is tab 2A', tabs=tabs) 59 | tab2b.write_html('This is tab 2B', tabs=tabs) 60 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================================================ 2 | Gravitational-wave Summary Information Generator 3 | ================================================ 4 | 5 | GWSumm is a python toolbox used by the LIGO Scientific Collaboration to summarise and archive sundry facets of the performance of the LIGO instruments, and archive these data in a nested HTML structure. 6 | 7 | |PyPI version| |Conda version| 8 | 9 | |DOI| |License| |Supported Python versions| 10 | 11 | |Build Status| |Coverage Status| |Code Climate| 12 | 13 | https://gwsumm.readthedocs.io 14 | 15 | ------------ 16 | Installation 17 | ------------ 18 | 19 | GWSumm is best installed with `conda`_: 20 | 21 | .. code:: bash 22 | 23 | conda install -c conda-forge gwsumm 24 | 25 | but can also be installed with `pip`_: 26 | 27 | .. code:: bash 28 | 29 | python -m pip install gwsumm 30 | 31 | ------------ 32 | Contributing 33 | ------------ 34 | 35 | All code should follow the Python Style Guide outlined in `PEP 0008`_; 36 | users can use the `flake8`_ package to check their code for style issues 37 | before submitting. 38 | 39 | See `the contributions guide`_ for the recommended procedure for 40 | proposing additions/changes. 41 | 42 | .. _PEP 0008: https://www.python.org/dev/peps/pep-0008/ 43 | .. _flake8: http://flake8.pycqa.org 44 | .. _the contributions guide: https://github.com/gwpy/gwsumm/blob/master/CONTRIBUTING.md 45 | .. _conda: https://conda.io 46 | .. _pip: https://pip.pypa.io/en/stable/ 47 | 48 | .. |PyPI version| image:: https://badge.fury.io/py/gwsumm.svg 49 | :target: http://badge.fury.io/py/gwsumm 50 | .. |Conda version| image:: https://img.shields.io/conda/vn/conda-forge/gwsumm.svg 51 | :target: https://anaconda.org/conda-forge/gwsumm/ 52 | .. |DOI| image:: https://zenodo.org/badge/DOI/10.5281/zenodo.2647609.svg 53 | :target: https://doi.org/10.5281/zenodo.2647609 54 | .. |License| image:: https://img.shields.io/pypi/l/gwsumm.svg 55 | :target: https://choosealicense.com/licenses/gpl-3.0/ 56 | .. |Supported Python versions| image:: https://img.shields.io/pypi/pyversions/gwsumm.svg 57 | :target: https://pypi.org/project/gwsumm/ 58 | .. |Build Status| image:: https://github.com/gwpy/gwsumm/actions/workflows/build.yml/badge.svg?branch=master 59 | :target: https://github.com/gwpy/gwsumm/actions/workflows/build.yml 60 | .. |Coverage Status| image:: https://codecov.io/gh/gwpy/gwsumm/branch/master/graph/badge.svg 61 | :target: https://codecov.io/gh/gwpy/gwsumm 62 | .. |Code Climate| image:: https://codeclimate.com/github/gwpy/gwsumm/badges/gpa.svg 63 | :target: https://codeclimate.com/github/gwpy/gwsumm 64 | -------------------------------------------------------------------------------- /docs/tabs/modes.rst: -------------------------------------------------------------------------------- 1 | .. _modes: 2 | 3 | .. currentmodule:: gwsumm.tabs 4 | 5 | ########### 6 | `Tab` modes 7 | ########### 8 | 9 | In its simplest form, the `Tab` is essentially a blank canvas on which to write whatever you want. 10 | However, the original mandate for GWSumm was to provide a framework in which to generate automatic summaries of LIGO data, over a given timescale. 11 | 12 | To handle data processing, rather than static HTML generation, each `Tab` has a type, based on its relation to any interval in time 13 | 14 | .. currentmodule:: gwsumm.tabs.core 15 | 16 | .. autosummary:: 17 | :nosignatures: 18 | :toctree: ../api 19 | 20 | StaticTab 21 | IntervalTab 22 | EventTab 23 | 24 | The type of a `Tab` is set automatically when it is created based on the value of the :attr:`~Tab.mode` attribute, so you don't need to remember the above objects. 25 | 26 | ===== 27 | Modes 28 | ===== 29 | 30 | GWSumm currently support seven different `Tab` modes: 31 | 32 | ========== ==== ============================================================= 33 | Mode Enum Description 34 | ========== ==== ============================================================= 35 | ``STATIC`` 0 No associated time interval 36 | ``EVENT`` 1 Associated with a single GPS time, normally around an event 37 | ``GPS`` 2 Simple (arbitrary) GPS ``[start, end)`` interval 38 | ``DAY`` 10 One UTC 24-hour day 39 | ``WEEK`` 11 One 7-day week 40 | ``MONTH`` 12 One calendar month 41 | ``YEAR`` 13 One calendar year 42 | ========== ==== ============================================================= 43 | 44 | =============== 45 | Assigning modes 46 | =============== 47 | 48 | Each `Tab` will be assigned a mode (unless specified as follows, the default mode 49 | is ``STATIC``). The assignment can be done on a per-tab basis by 50 | passing the `~Tab.mode` keyword argument when creating a `Tab`, or 51 | globally, by using the :meth:`gwsumm.mode.set_mode`. The latter sets 52 | the default mode for all subsequent tabs created in this session. 53 | 54 | If a :attr:`~Tab.mode` is given that associates with a GPS time or 55 | times, these must be given via the `~IntervalTab.span` or 56 | `~EventTab.gpstime` keyword arguments, otherwise a `TypeError` will be 57 | raised. The `span` tuple is the ``(GPS start time, GPS end time)`` 58 | 59 | .. code-block:: python 60 | 61 | >>> tab = Tab('My first tab', mode='day', span=(0, 100)) 62 | >>> print(tab.mode, tab.span) 63 | (10, Segment(0, 100)) 64 | >>> tab = Tab('My first tab', mode='EVENT', gpstime=101) 65 | >>> print(tab.mode, tab.gpstime) 66 | (1, LIGOTimeGPS(101,0)) 67 | -------------------------------------------------------------------------------- /gwsumm/html/static.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Duncan Macleod (2016) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """HTML helphers 20 | 21 | This module mainly declares the resources used by standard on HTML pages 22 | """ 23 | 24 | from collections import OrderedDict 25 | 26 | __author__ = 'Duncan Macleod ' 27 | __credits__ = ('Alex Urban ,' 28 | ' Evan Goetz ') 29 | 30 | 31 | # build collection of CSS resources 32 | CSS = OrderedDict(( 33 | ('font-awesome', 'https://cdnjs.cloudflare.com/ajax/libs/' 34 | 'font-awesome/6.5.1/css/fontawesome.min.css'), 35 | ('font-awesome-solid', 'https://cdnjs.cloudflare.com/ajax/libs/' 36 | 'font-awesome/6.5.1/css/solid.min.css'), 37 | ('gwbootstrap', 'https://cdn.jsdelivr.net/npm/gwbootstrap@1.3.7/' 38 | 'lib/gwbootstrap.min.css'), 39 | )) 40 | 41 | # build collection of javascript resources 42 | JS = OrderedDict(( 43 | ('jquery', 'https://code.jquery.com/jquery-3.7.1.min.js'), 44 | ('jquery-ui', 'https://code.jquery.com/ui/1.13.2/jquery-ui.min.js'), 45 | ('moment', 'https://cdnjs.cloudflare.com/ajax/libs/' 46 | 'moment.js/2.30.1/moment.min.js'), 47 | ('bootstrap', 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/' 48 | 'dist/js/bootstrap.bundle.min.js'), 49 | ('fancybox', 'https://cdn.jsdelivr.net/npm/@fancyapps/ui@5.0/' 50 | 'dist/fancybox/fancybox.umd.js'), 51 | ('datepicker', 'https://cdnjs.cloudflare.com/ajax/libs/' 52 | 'bootstrap-datepicker/1.9.0/js/' 53 | 'bootstrap-datepicker.min.js'), 54 | ('gwbootstrap', 'https://cdn.jsdelivr.net/npm/gwbootstrap@1.3.7/' 55 | 'lib/gwbootstrap-extra.min.js'), 56 | )) 57 | 58 | 59 | # -- utilities ---------------------------------------------------------------- 60 | 61 | def get_css(): 62 | """Return a `dict` of CSS files to link in the HTML 63 | """ 64 | return CSS 65 | 66 | 67 | def get_js(): 68 | """Return a `dict` of javascript files to link in the HTML 69 | """ 70 | return JS 71 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to GWSumm 2 | 3 | This is [@alurban](//github.com/alurban)'s and [@duncanmmacleod](//github.com/duncanmmacleod/)'s workflow, which might work well for others. It is merely a verbose version of the [GitHub flow](https://guides.github.com/introduction/flow/). 4 | 5 | The basic idea is to use the `master` branch of your fork as a way of updating your fork with other people's changes that have been merged into the main repo, and then working on a dedicated _feature branch_ for each piece of work: 6 | 7 | - create the fork (if needed) by clicking _Fork_ in the upper-right corner of https://github.com/gwpy/gwsumm/ - this only needs to be done once, ever 8 | - clone the fork into a new folder dedicated for this piece of work (replace `` with yout GitHub username): 9 | 10 | ```bash 11 | git clone https://github.com//gwsumm.git gwsumm-my-work # change gwsumm-my-work as appropriate 12 | cd gwsumm-my-work 13 | ``` 14 | 15 | - link the fork to the upstream 'main' repo: 16 | 17 | ```bash 18 | git remote add upstream https://github.com/gwpy/gwsumm.git 19 | ``` 20 | 21 | - pull changes from the upstream 'main' repo onto your fork's master branch to pick up other people's changes, then push to your remote to update your fork on github.com 22 | 23 | ```bash 24 | git pull --rebase upstream master 25 | git push 26 | ``` 27 | 28 | - create a new branch on which to work 29 | 30 | ```bash 31 | git checkout -b my-new-branch 32 | ``` 33 | 34 | - make commits to that branch 35 | - push changes to your remote on github.com 36 | 37 | ```bash 38 | git push -u origin my-new-branch 39 | ``` 40 | 41 | - open a merge request on github.com 42 | - when the request is merged, you should 'delete the source branch' (there's a button), then just delete the clone of your fork and forget about it 43 | 44 | ```bash 45 | cd ../ 46 | rm -rf ./gwsumm-my-work 47 | ``` 48 | 49 | And that's it. 50 | 51 | ## Coding guidelines 52 | 53 | ### Python compatibility 54 | 55 | **GWSumm code must be compatible with Python versions >=3.6.** 56 | 57 | ### Style 58 | 59 | This package follows [PEP 8](https://www.python.org/dev/peps/pep-0008/), 60 | and all code should adhere to that as far as is reasonable. 61 | 62 | The first stage in the automated testing of pull requests is a job that runs 63 | the [`flake8`](http://flake8.pycqa.org) linter, which checks the style of code 64 | in the repo. You can run this locally before committing changes via: 65 | 66 | ```bash 67 | python -m flake8 68 | ``` 69 | 70 | ### Testing 71 | 72 | GWSumm has a relatively incomplete test suite, covering less than 40% of the codebase. 73 | All code contributions should be accompanied by (unit) tests to be executed with 74 | [`pytest`](https://docs.pytest.org/en/latest/), and should cover 75 | all new or modified lines. 76 | 77 | You can run the test suite locally from the root of the repository via: 78 | 79 | ```bash 80 | python -m pytest gwsumm/ 81 | ``` 82 | -------------------------------------------------------------------------------- /docs/overview.rst: -------------------------------------------------------------------------------- 1 | ############### 2 | What is GWSumm? 3 | ############### 4 | 5 | The `gwsumm` package ('the summary pages') is a python toolbox that can be 6 | used to generate a structured HTML webpage displaying graphical data that 7 | describe any and all aspects of gravitational-wave interferometer performance. 8 | The summary pages were developed in collaboration between the LIGO Laboratory 9 | and the GEO600 project with the goal of generating an automated daily summary 10 | of laser-interferometer operations and performance. 11 | 12 | The LIGO Summary Pages are used to characterize and monitor the status 13 | of the detectors and their subsystems. In addition, data products and 14 | webpages from other analysis tools are included in the Summary Pages. 15 | 16 | The output acts as a kind of daily magazine, allowing instrument scientists 17 | and data analysis teams a archived, searchable summary of the key figures of 18 | merit that will determine the sensitivity and ultimately the science output 19 | of these instruments. 20 | 21 | Those readers who are members of the LIGO Scientific Collaboration, the Virgo 22 | Collaboration, or KAGRA can view the current LIGO summary pages at the 23 | following sites: 24 | 25 | == ======================================================= 26 | H1 https://ldas-jobs.ligo-wa.caltech.edu/~detchar/summary/ 27 | L1 https://ldas-jobs.ligo-la.caltech.edu/~detchar/summary/ 28 | == ======================================================= 29 | 30 | Working model 31 | ============= 32 | 33 | The GWSumm package provides an abstract set of classes from which any user 34 | can build their own python program to read, manipulate, and display data. 35 | However, for the specific purpose of the LIGO instrumental summary pages, 36 | the `gw_summary` command-line executable is used to read in a number of 37 | INI-format configuration files that define what data should be read, how it 38 | should be manipulated, and how it should all be displayed. 39 | 40 | These configuration files are made up ``[tab-xxx]`` section with the following 41 | format: 42 | 43 | .. code-block:: ini 44 | 45 | [tab-IMC] 46 | name = Input mode cleaner 47 | shortname = IMC 48 | 1 = L1:IMC-PWR_IN_OUT_DQ timeseries 49 | 1-ylim = 0,80 50 | 1-title = 'Power into IMC' 51 | [html] 52 | issues = 53 | 54 | This block defines the ``IMC`` tab, with a ``name`` (and a ``shortname``): 55 | a single ``timeseries`` plot of the ``L1:IMC-PWR_IN_OUT_DQ`` channel. 56 | The plot has been customised with a y-axis limit and a title. This 57 | also defines the required ``[html]`` section, where the required key 58 | ``issues`` is defined. This example can be saved to a file called ``imc.ini``. 59 | 60 | This tab is then generated by passing it to the `gw_summary` executable, along 61 | with some GPS times over which to run: 62 | 63 | .. code-block:: bash 64 | 65 | $ gw_summary gps 'Feb 29 2020 00:00' 'Feb 29 2020 01:00' --config-file imc.ini 66 | 67 | This minimal setup will produce the following HTML page 68 | `1266969618-1266973218/imc/index.html`: 69 | 70 | .. image:: examples/imc.png 71 | :width: 80% 72 | :align: center 73 | :alt: GWSumm example screenshot 74 | 75 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ######################################################### 2 | GWSumm: the gravitational-wave summary information system 3 | ######################################################### 4 | 5 | |PyPI version| |Conda version| 6 | 7 | |DOI| |License| |Supported Python versions| 8 | 9 | The `gwsumm` package is a tool used by the 10 | `Laser Interferometer Gravitational-wave Observatory (LIGO) `_ 11 | to collect, aggregate, and visually summarise the sundry data produced 12 | throughout the experiment in order to characterise instrument performance. 13 | 14 | The output of this package, known internally as the 'summary pages', give an 15 | archive of a number of figures or merit, including time-series amplitude 16 | trends, frequency spectra and spectrograms, and transient event triggers. 17 | 18 | This package includes a collection of command-line utilities and a python 19 | module: 20 | 21 | .. code:: python 22 | 23 | import gwsumm 24 | 25 | Installation 26 | ============ 27 | 28 | GWSumm is best installed with `conda`_: 29 | 30 | .. code:: bash 31 | 32 | conda install -c conda-forge gwsumm 33 | 34 | but can also be installed with `pip`_: 35 | 36 | .. code:: bash 37 | 38 | python -m pip install gwsumm 39 | 40 | Note, users with `LIGO.ORG` credentials have access to a software 41 | container with a regularly-updated build of GWSumm. For more 42 | information please refer to the 43 | `LSCSoft Conda `_ documentation. 44 | 45 | Contributing 46 | ============ 47 | 48 | All code should follow the Python Style Guide outlined in `PEP 0008`_; 49 | users can use the `flake8`_ package to check their code for style issues 50 | before submitting. 51 | 52 | See `the contributions guide`_ for the recommended procedure for 53 | proposing additions/changes. 54 | 55 | The GWSumm project is hosted on GitHub: 56 | 57 | * Issue tickets: https://github.com/gwpy/gwsumm/issues 58 | * Source code: https://github.com/gwpy/gwsumm 59 | 60 | 61 | License 62 | ------- 63 | 64 | GWSumm is distributed under the `GNU General Public License`_. 65 | 66 | .. toctree:: 67 | :maxdepth: 1 68 | :hidden: 69 | 70 | overview 71 | cli 72 | automation 73 | tabs/index 74 | tabs/websites 75 | tabs/modes 76 | tabs/api 77 | states 78 | plots 79 | modes 80 | api/index 81 | 82 | .. _PEP 0008: https://www.python.org/dev/peps/pep-0008/ 83 | .. _flake8: http://flake8.pycqa.org 84 | .. _the contributions guide: https://github.com/gwpy/gwsumm/blob/master/CONTRIBUTING.md 85 | .. _conda: https://conda.io 86 | .. _pip: https://pip.pypa.io/en/stable/ 87 | .. _GNU General Public License: https://github.com/gwpy/gwsumm/blob/master/LICENSE 88 | 89 | .. |PyPI version| image:: https://badge.fury.io/py/gwsumm.svg 90 | :target: http://badge.fury.io/py/gwsumm 91 | .. |Conda version| image:: https://img.shields.io/conda/vn/conda-forge/gwsumm.svg 92 | :target: https://anaconda.org/conda-forge/gwsumm/ 93 | .. |DOI| image:: https://zenodo.org/badge/DOI/10.5281/zenodo.2647609.svg 94 | :target: https://doi.org/10.5281/zenodo.2647609 95 | .. |License| image:: https://img.shields.io/pypi/l/gwsumm.svg 96 | :target: https://choosealicense.com/licenses/gpl-3.0/ 97 | .. |Supported Python versions| image:: https://img.shields.io/pypi/pyversions/gwsumm.svg 98 | :target: https://pypi.org/project/gwsumm/ 99 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=77.0.3", 4 | "setuptools_scm[toml]>=3.4.3", 5 | "wheel", 6 | ] 7 | build-backend = "setuptools.build_meta" 8 | 9 | [project] 10 | name = "gwsumm" 11 | description = "A python toolbox used by the LIGO Scientific Collaboration for detector characterisation" 12 | readme = "README.rst" 13 | requires-python = ">=3.10" 14 | authors = [ 15 | { name = "Alex Urban", email = "alex.urban@ligo.org" }, 16 | { name = "Duncan Macleod", email = "duncan.macleod@ligo.org" }, 17 | ] 18 | maintainers = [ 19 | { name = "Evan Goetz", email = "evan.goetz@ligo.org" }, 20 | ] 21 | license = "GPL-3.0-or-later" 22 | license-files = [ "LICENSE" ] 23 | classifiers = [ 24 | "Development Status :: 5 - Production/Stable", 25 | "Intended Audience :: Developers", 26 | "Intended Audience :: Science/Research", 27 | "Natural Language :: English", 28 | "Operating System :: OS Independent", 29 | "Programming Language :: Python", 30 | "Programming Language :: Python :: 3", 31 | "Programming Language :: Python :: 3.10", 32 | "Programming Language :: Python :: 3.11", 33 | "Topic :: Scientific/Engineering", 34 | "Topic :: Scientific/Engineering :: Astronomy", 35 | "Topic :: Scientific/Engineering :: Physics", 36 | ] 37 | 38 | dependencies = [ 39 | "astropy >=3.0.0", 40 | "gwdatafind >=1.1.1", 41 | "gwdetchar >=2.3.2", 42 | "gwpy >=3.0.9", 43 | "gwtrigfind", 44 | "lalsuite", 45 | "lscsoft-glue >=1.60.0", 46 | "lxml", 47 | "markdown", 48 | "MarkupPy", 49 | "matplotlib >=3.5", 50 | "numpy >=1.16", 51 | "pygments >=2.7.0", 52 | "python-dateutil", 53 | "igwn-ligolw", 54 | "scipy >=1.2.0", 55 | ] 56 | 57 | dynamic = ["version"] 58 | 59 | [project.optional-dependencies] 60 | test = [ 61 | "flake8", 62 | "pytest >=3.3.0", 63 | "pytest-cov >=2.4.0", 64 | ] 65 | dev = [ 66 | "h5py", 67 | "ligo-gracedb >= 2.0.0", 68 | "pykerberos", 69 | ] 70 | doc = [ 71 | "numpydoc", 72 | "sphinx", 73 | "sphinx-automodapi", 74 | "sphinx_bootstrap_theme", 75 | "sphinxcontrib-programoutput", 76 | ] 77 | 78 | [project.scripts] 79 | gw_summary = "gwsumm.__main__:main" 80 | gw_summary_pipe = "gwsumm.batch:main" 81 | gwsumm-plot-guardian = "gwsumm.plot.guardian.__main__:main" 82 | gwsumm-plot-triggers = "gwsumm.plot.triggers.__main__:main" 83 | 84 | [project.urls] 85 | "Documentation" = "https://gwsumm.readthedocs.io" 86 | "Source Code" = "https://github.com/gwpy/gwsumm" 87 | "Bug Tracker" = "https://github.com/gwpy/gwsumm/issues" 88 | "Discussion Forum" = "https://gwdetchar.slack.com" 89 | 90 | [tool.setuptools] 91 | include-package-data = true 92 | 93 | [tool.setuptools.packages.find] 94 | include = [ "gwsumm*" ] 95 | 96 | [tool.setuptools_scm] 97 | write_to = "gwsumm/_version.py" 98 | 99 | [tool.coverage.run] 100 | source = [ "gwsumm" ] 101 | omit = [ 102 | # don't report coverage for _version.py 103 | # (generated automatically by setuptools-scm) 104 | "*/_version.py", 105 | "gwsumm/tests/*", 106 | "gwsumm/html/tests/*", 107 | # omit scripts for now, will be done in a future PR 108 | "gwsumm/__main__.py", 109 | ] 110 | 111 | [tool.coverage.report] 112 | # print report with one decimal point 113 | precision = 1 114 | 115 | [tool.pytest.ini_options] 116 | addopts = "-r a" 117 | 118 | -------------------------------------------------------------------------------- /gwsumm/plot/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Duncan Macleod (2013) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """A `Plot` is a representation of an image to be included in the HTML 20 | output a :doc:`tab `. 21 | 22 | For simple purposes, a `Plot` is just a reference to an existing image file 23 | that can be imported into an HTML page via the ```` tag. 24 | 25 | For more complicated purposes, a number of data plot classes are provided to 26 | allow users to generate images on-the-fly. 27 | The available classes are: 28 | 29 | .. autosummary:: 30 | :toctree: api 31 | 32 | TimeSeriesDataPlot 33 | SpectrogramDataPlot 34 | SegmentDataPlot 35 | StateVectorDataPlot 36 | SpectrumDataPlot 37 | TimeSeriesHistogramPlot 38 | TriggerTimeSeriesDataPlot 39 | TriggerHistogramPlot 40 | TriggerRateDataPlot 41 | """ 42 | 43 | __author__ = 'Duncan Macleod ' 44 | 45 | from .registry import ( 46 | register_plot, 47 | get_plot, 48 | ) 49 | from .utils import ( 50 | get_column_label, 51 | get_column_string, 52 | hash, 53 | ) 54 | from .core import ( 55 | format_label, 56 | SummaryPlot, 57 | DataPlot, 58 | BarPlot, 59 | PiePlot, 60 | ) 61 | from .builtin import ( 62 | undo_demodulation, 63 | TimeSeriesDataPlot, 64 | SpectrogramDataPlot, 65 | CoherenceSpectrogramDataPlot, 66 | SpectrumDataPlot, 67 | CoherenceSpectrumDataPlot, 68 | TimeSeriesHistogramPlot, 69 | TimeSeriesHistogram2dDataPlot, 70 | SpectralVarianceDataPlot, 71 | RayleighSpectrogramDataPlot, 72 | RayleighSpectrumDataPlot, 73 | ) 74 | from .segments import ( 75 | tint_hex, 76 | common_limits, 77 | SegmentDataPlot, 78 | StateVectorDataPlot, 79 | DutyDataPlot, 80 | ODCDataPlot, 81 | SegmentPiePlot, 82 | NetworkDutyPiePlot, 83 | NetworkDutyBarPlot, 84 | SegmentBarPlot, 85 | SegmentHistogramPlot, 86 | ) 87 | from .triggers import ( 88 | TriggerPlotMixin, 89 | TriggerDataPlot, 90 | TriggerTimeSeriesDataPlot, 91 | TriggerHistogramPlot, 92 | TriggerRateDataPlot, 93 | ) 94 | from .range import ( 95 | _get_params, 96 | RangePlotMixin, 97 | RangeDataPlot, 98 | RangeDataHistogramPlot, 99 | RangeSpectrogramDataPlot, 100 | RangeSpectrumDataPlot, 101 | RangeCumulativeSpectrumDataPlot, 102 | RangeCumulativeHistogramPlot, 103 | SimpleTimeVolumeDataPlot, 104 | GWpyTimeVolumeDataPlot, 105 | ) 106 | from .noisebudget import ( 107 | NoiseBudgetPlot, 108 | RelativeNoiseBudgetPlot, 109 | ) 110 | from .guardian import GuardianStatePlot 111 | from .sei import SeiWatchDogPlot 112 | -------------------------------------------------------------------------------- /gwsumm/plot/guardian/tests/test_main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Alex Urban (2020) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """Tests for the `gwsumm.plot.guardian` command-line interface 20 | """ 21 | 22 | import os 23 | import shutil 24 | 25 | from gwpy.timeseries import ( 26 | TimeSeries, 27 | TimeSeriesList, 28 | ) 29 | 30 | from .... import globalv 31 | from ....archive import write_data_archive 32 | from .. import __main__ as guardian_cli 33 | 34 | __author__ = 'Alex Urban ' 35 | 36 | # -- test configuration 37 | 38 | CONFIG = """ 39 | [tab-ISC_LOCK] 40 | type = guardian 41 | node = ISC_LOCK 42 | name = %(node)s 43 | ; node states 44 | 600 = Low noise 45 | """ 46 | 47 | # -- test data 48 | 49 | SUFFICES = [ 50 | "STATE_N", 51 | "REQUEST_N", 52 | "NOMINAL_N", 53 | "OK", 54 | "MODE", 55 | "OP", 56 | ] 57 | DATA = { 58 | key: TimeSeriesList( 59 | TimeSeries([600] * 3600 * 16, sample_rate=16, name=key, channel=key) 60 | ) for key in ["L1:GRD-ISC_LOCK_{}".format(suff) for suff in SUFFICES] 61 | } 62 | 63 | 64 | # -- utils -------------------------------------------------------------------- 65 | 66 | def _get_inputs(workdir): 67 | """Prepare, and return paths to, input data products 68 | """ 69 | # set global timeseries data 70 | globalv.DATA = DATA 71 | # get path to data files 72 | ini = os.path.join(workdir, "config.ini") 73 | archive = os.path.abspath(os.path.join(workdir, "archive.h5")) 74 | # write to data files 75 | with open(ini, 'w') as f: 76 | f.write(CONFIG) 77 | write_data_archive(archive) 78 | # reset global data and return 79 | globalv.DATA = {} 80 | return (ini, archive) 81 | 82 | 83 | # -- cli tests ---------------------------------------------------------------- 84 | 85 | def test_main(tmpdir, caplog): 86 | outdir = str(tmpdir) 87 | plot = os.path.join(outdir, "guardian.png") 88 | (ini, archive) = _get_inputs(outdir) 89 | args = [ 90 | 'ISC_LOCK', 91 | '0', '3600', 92 | ini, 93 | '--plot-params', 'title=Test figure', 94 | '--output-file', plot, 95 | '--verbose', 96 | '--archive', archive, 97 | ] 98 | # test output 99 | guardian_cli.main(args) 100 | assert os.path.exists(plot) 101 | assert len(os.listdir(outdir)) == 3 # 2 inputs, 1 output 102 | assert 'Read data archive from {}'.format(archive) in caplog.text 103 | assert 'Processing:' in caplog.text 104 | assert 'Plot saved to {}'.format(plot) in caplog.text 105 | assert 'Archive recorded as {}'.format(archive) in caplog.text 106 | # clean up 107 | shutil.rmtree(outdir, ignore_errors=True) 108 | -------------------------------------------------------------------------------- /gwsumm/state/registry.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Duncan Macleod (2013) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """Registry for `states `. 20 | """ 21 | 22 | from .. import globalv 23 | from ..utils import re_quote 24 | 25 | __author__ = 'Duncan Macleod ' 26 | 27 | __all__ = ['register_state', 'get_state', 'get_states'] 28 | 29 | 30 | def register_state(state, key=None, force=False): 31 | """Register a new `SummaryState` to the given ``key`` 32 | 33 | Parameters 34 | ---------- 35 | state : `SummaryState` 36 | defining Class for this state type. 37 | key : `str`, optional 38 | unique descriptive name for the `SummaryState` to be registered. 39 | If ``key=None``, the :attr:`~SummaryState.key` 40 | attribute of the given state will be used. 41 | force : `bool` 42 | overwrite existing registration for this key 43 | 44 | Raises 45 | ------ 46 | ValueError 47 | if key is already registered and ``force`` not given as `True` 48 | """ 49 | if key is None: 50 | key = state.key 51 | key = key.lower() 52 | if key not in globalv.STATES or force: 53 | globalv.STATES[key] = state 54 | return state 55 | raise ValueError("State %r has already been registered." % key) 56 | 57 | 58 | def get_state(key): 59 | """Query the registry for the `SummaryState` registered to the given key 60 | 61 | Parameters 62 | ---------- 63 | key : `str` 64 | registered key of desired `SummaryState`. This may not match the 65 | `~SummaryState.name` attribute` if the state was registered with 66 | a different key. 67 | 68 | Returns 69 | ------- 70 | state : `SummaryState` 71 | the `SummaryState` registered with the given key 72 | 73 | Raises 74 | ------ 75 | ValueError: 76 | if the ``key`` doesn't map to a registered `SummaryState` 77 | """ 78 | key = re_quote.sub('', key) 79 | try: 80 | return globalv.STATES[key.lower()] 81 | except KeyError: 82 | raise ValueError("No SummaryState registered with name '%s'" % key) 83 | 84 | 85 | def get_states(keys=set()): 86 | """Query the registry for a list of states (defaults to all) 87 | 88 | Parameters 89 | ---------- 90 | keys : `set` of `str` 91 | the set of state keys to query in the registry 92 | 93 | Returns 94 | ------- 95 | states : `dict` 96 | a `dict` of (``key``, `SummaryState`) pairs 97 | 98 | Raises 99 | ------ 100 | ValueError: 101 | if any of the ``keys`` doesn't map to a registered `SummaryState` 102 | """ 103 | if not keys: 104 | return globalv.STATES.copy() 105 | else: 106 | return dict((key, get_state(key)) for key in keys) 107 | -------------------------------------------------------------------------------- /docs/configuration/tabs.rst: -------------------------------------------------------------------------------- 1 | ############################# 2 | Configuring a simple HTML tab 3 | ############################# 4 | 5 | .. currentmodule:: gwsumm.tabs 6 | 7 | A :class:`Tab` is single HTML web-page containing some data. It can be as 8 | simple as containing some text, or can be told to generated a number of plots 9 | from GW interferometer data and display them in a specific format. 10 | 11 | For full technical details on the `Tab` classes available, please `read the 12 | tabs API page <../tabs>`_. 13 | 14 | ==== 15 | Role 16 | ==== 17 | 18 | A tab can take on one of two roles, depending on its configuration: 19 | 20 | ====== ====================================================================== 21 | Parent The summary-page for a set of subordinate child tabs, displayed as the 22 | heading for a dropdown menu in the HTML navigation bar of the output 23 | Child A subordinate child tab of a given parent, linked under the relevant 24 | dropdown menu in the HTML navigation bar of the output 25 | ====== ====================================================================== 26 | 27 | ============= 28 | Configuration 29 | ============= 30 | 31 | Each tab class provides a :meth:`Tab.from_ini` `classmethod`, allowing users to 32 | create a new tab from a ``[section]`` in a configuration file. 33 | Every tab should be configured with the following options: 34 | 35 | =========== =========================================================== 36 | `~Tab.type` type of tab to configure [optional, default: ``'default'``] 37 | `~Tab.name` name of this tab 38 | =========== =========================================================== 39 | 40 | The ``type`` option, whilst optional, is recommended, mainly to make the 41 | configuration more transparent to other users, who might not know which tab type 42 | is the default. 43 | 44 | Additionally, all tabs can be configured with the following keys: 45 | 46 | .. autosummary:: 47 | :nosignatures: 48 | 49 | ~Tab.shortname 50 | ~Tab.parent 51 | ~Tab.group 52 | 53 | See the detailed definitions of each attribute for defaults. 54 | 55 | ------------- 56 | `ExternalTab` 57 | ------------- 58 | 59 | The `ExternalTab` allows users to embed any HTML page from the same domain 60 | into a GWSumm page. 61 | An `ExternalTab` can be configured as follows: 62 | 63 | .. code-block:: ini 64 | 65 | [tab-external] 66 | type = external 67 | name = 'My results' 68 | url = '/~duncan.macleod/analysis/results/summary.html' 69 | 70 | .. note:: 71 | 72 | Only URLs on the same domain can be included by default on most servers. 73 | This is not a restriction of GWSumm, rather a safety measure of the Apache, 74 | and other, web server protocols. 75 | 76 | --------- 77 | `PlotTab` 78 | --------- 79 | 80 | The `PlotTab` allows users to embed any number of images into a new tab, 81 | and choose the layout. 82 | With this type of tab, users specify images to include by giving options of the 83 | form ``X = /url/to/plot``, with ``X`` an integer, increasing for each plot, 84 | and ``/url/to/plot`` the web URL at which the plot can be found. 85 | Also, users can give the following extra options: 86 | 87 | .. autosummary:: 88 | :nosignatures: 89 | 90 | ~PlotTab.foreword 91 | ~PlotTab.afterword 92 | 93 | An example `PlotTab` could be configured as follows: 94 | 95 | .. literalinclude:: ../../share/examples/matplotlib.ini 96 | :language: ini 97 | 98 | Here we have imported three examples plots from the 99 | `matplotlib `_ examples with a :attr:`~PlotTab.layout` 100 | of one plot on the top row (full size), and two plots on the second row. 101 | 102 | --------- 103 | `DataTab` 104 | --------- 105 | 106 | The workhorse of the GWSumm package, at least as used by the LIGO Scientific 107 | Collaboration is the `DataTab`. Please read this page for details on 108 | configuring one of these 109 | 110 | .. toctree:: 111 | 112 | data 113 | -------------------------------------------------------------------------------- /gwsumm/config/defaults.ini: -------------------------------------------------------------------------------- 1 | ; 2 | ; GWSumm default configuration options 3 | ; 4 | ; This file provides a set of standard options for the GWSumm command-line 5 | ; interface that users can override in their own separate INI files 6 | ; 7 | ; As a result, this file probably doesn't need to be modified very often, 8 | ; just whenever a standard is set and should be followed by default 9 | ; 10 | 11 | [calendar] 12 | start-of-week = monday 13 | start-date = 2013-07-01 14 | 15 | [channels] 16 | $(ifo(1:HPI-BS_BLRMS_Z_3_10 = unit='nm/s' 17 | 18 | [states] 19 | ; 'all' is implicitly defined as the GPS [start, stop) segment with no gaps 20 | Science = %(ifo)s:DMT-SCIENCE:1 21 | IFO Locked = %(ifo)s:DMT-UP:1 22 | PSL-ODC = $(ifo)s:PSL-ODC_SUMMARY:1 23 | IMC-ODC = $(ifo)s:IMC-ODC_SUMMARY:1 24 | 25 | [state-all] 26 | name = All 27 | description = All times 28 | 29 | [general] 30 | 31 | [html] 32 | css1 = /~%(user)s/html/bootstrap/3.0.0/css/bootstrap.min.css 33 | css2 = /~%(user)s/html/datepicker/1.2.0/css/datepicker.css 34 | css3 = /~%(user)s/html/fancybox/source/jquery.fancybox.css?v=2.1.5 35 | css4 = /~%(user)s/html/gwsummary/gwsummary.css 36 | javascript1 = /~%(user)s/html/jquery-1.10.2.min.js 37 | javascript2 = /~%(user)s/html/moment.min.js 38 | javascript3 = /~%(user)s/html/bootstrap/3.0.0/js/bootstrap.min.js 39 | javascript4 = /~%(user)s/html/datepicker/1.2.0/js/bootstrap-datepicker.js 40 | javascript5 = /~%(user)s/html/fancybox/source/jquery.fancybox.pack.js?v=2.1.5 41 | javascript6 = /~%(user)s/html/gwsummary/gwsummary.js 42 | 43 | [segment-database] 44 | url = https://segdb-er.ligo.caltech.edu 45 | 46 | [fft] 47 | ; average method 48 | method = medianmean 49 | ; PSD average length and overlap (in seconds) 50 | ;fftlength = 1 51 | ;fftstride = 0.5 52 | ; spectrogram stride 53 | ;stride = 2 54 | 55 | ; -----------------------------------------------------------------------------. 56 | ; Basic Plots 57 | 58 | [plot-spectrogram] 59 | type = 'spectrogram' 60 | format = 'amplitude' 61 | logy = True 62 | logcolor = True 63 | ylabel = 'Frequency [Hz]' 64 | 65 | [plot-median-spectrogram] 66 | type = 'spectrogram' 67 | format = 'amplitude' 68 | logy = True 69 | ylabel = 'Frequency [Hz]' 70 | ratio = median 71 | clim = 0.25,4 72 | logcolor = True 73 | colorlabel = 'Amplitude relative to median' 74 | 75 | 76 | [plot-spectrum] 77 | type = 'spectrum' 78 | format = 'amplitude' 79 | xlabel = 'Frequency [Hz]' 80 | logx = True 81 | logy = True 82 | legend-loc = 'lower left' 83 | 84 | [plot-ep-time-frequency-snr] 85 | type = triggers 86 | etg = ExcessPower 87 | ; columns 88 | x = time 89 | y = central_freq 90 | color = snr 91 | ; colour bar 92 | clim = 3,100 93 | logcolor = True 94 | colorlabel = 'Signal to noise ratio (SNR)' 95 | ; plot params 96 | edgecolor = 'none' 97 | s = 16 98 | ;size_by_log = %(color)s 99 | ;size_range = %(clim)s 100 | ylabel = 'Frequency [Hz]' 101 | logy = True 102 | 103 | [plot-ep-time-frequency-amplitude] 104 | type = triggers 105 | etg = ExcessPower 106 | ; columns 107 | x = time 108 | y = central_freq 109 | color = amplitude 110 | ; colour bar 111 | logcolor = True 112 | ; plot params 113 | edgecolor = 'none' 114 | s = 16 115 | ;size_by_log = %(color)s 116 | ylabel = 'Frequency [Hz]' 117 | logy = True 118 | 119 | [plot-omicron-time-frequency-snr] 120 | type = triggers 121 | etg = Omicron 122 | ; columns 123 | x = time 124 | y = peak_frequency 125 | color = snr 126 | ; colour bar 127 | clim = 3,100 128 | logcolor = True 129 | colorlabel = 'Signal to noise ratio (SNR)' 130 | ; plot params 131 | edgecolor = 'none' 132 | s = 16 133 | ;size_by_log = %(color)s 134 | ylabel = 'Frequency [Hz]' 135 | logy = True 136 | 137 | [plot-omicron-time-frequency-amplitude] 138 | type = triggers 139 | etg = Omicron 140 | ; columns 141 | x = time 142 | y = peak_frequency 143 | color = amplitude 144 | ; colour bar 145 | logcolor = True 146 | ; plot params 147 | edgecolor = 'none' 148 | s = 16 149 | ;size_by_log = %(color)s 150 | ylabel = 'Frequency [Hz]' 151 | logy = True 152 | -------------------------------------------------------------------------------- /gwsumm/tabs/misc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Duncan Macleod (2013-2016) 3 | # 4 | # This file is part of GWSumm 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see 18 | 19 | """This module defines some utility `Tab` subclasses, including HTTP 20 | error handlers. 21 | """ 22 | 23 | from MarkupPy import markup 24 | 25 | from .registry import (get_tab, register_tab) 26 | 27 | from gwdetchar.io import html 28 | 29 | __author__ = 'Duncan Macleod ' 30 | __all__ = ['AboutTab', 'Error404Tab'] 31 | 32 | Tab = get_tab('basic') 33 | 34 | 35 | # -- About -------------------------------------------------------------------- 36 | 37 | class AboutTab(Tab): 38 | """Page describing how the containing HTML pages were generated 39 | """ 40 | type = 'about' 41 | 42 | def __init__(self, name='About', **kwargs): 43 | super(AboutTab, self).__init__(name, **kwargs) 44 | 45 | def write_html(self, config=list(), prog=None, **kwargs): 46 | return super(AboutTab, self).write_html( 47 | html.about_this_page(config=config, prog=prog), **kwargs) 48 | 49 | 50 | register_tab(AboutTab) 51 | 52 | 53 | # -- HTTP errors -------------------------------------------------------------- 54 | 55 | class Error404Tab(Tab): 56 | """Custom HTTP 404 error page 57 | """ 58 | type = '404' 59 | 60 | def __init__(self, name='404', **kwargs): 61 | super(Error404Tab, self).__init__(name, **kwargs) 62 | 63 | def write_html(self, config=list(), top=None, **kwargs): 64 | if top is None: 65 | top = kwargs.get('base', self.path) 66 | kwargs.setdefault('title', '404: Page not found') 67 | page = markup.page() 68 | page.div(class_='alert alert-danger text-justify shadow-sm') 69 | page.p() 70 | page.strong("The page you are looking for does not exist.") 71 | page.p.close() 72 | page.p("This could be because the times for which you are looking " 73 | "were never processed (or have not happened yet), or because " 74 | "no page exists for the specific data products you want. " 75 | "Either way, if you think this is in error, please contact " 76 | "the DetChar group.") 78 | page.p("Otherwise, you might be interested in one of the following:") 79 | page.div(style="padding-top: 10px;") 80 | page.a("Take me back", role="button", class_="btn btn-lg btn-info", 81 | title="Back", href="javascript:history.back()") 82 | page.a("Take me up one level", role="button", 83 | class_="btn btn-lg btn-warning", title="Up", 84 | href="javascript:linkUp()") 85 | page.a("Take me to the top level", role="button", 86 | class_="btn btn-lg btn-success", title="Top", href=top) 87 | page.div.close() 88 | page.div.close() # alert alert-danger 89 | page.script(""" 90 | function linkUp() { 91 | var url = window.location.href; 92 | if (url.substr(-1) == '/') url = url.substr(0, url.length - 2); 93 | url = url.split('/'); 94 | url.pop(); 95 | window.location = url.join('/'); 96 | }""", type="text/javascript") 97 | return super(Error404Tab, self).write_html(page, **kwargs) 98 | 99 | 100 | register_tab(Error404Tab) 101 | -------------------------------------------------------------------------------- /gwsumm/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Duncan Macleod (2013) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """Tests for `gwsumm.utils` 20 | 21 | """ 22 | 23 | import time 24 | import re 25 | import sys 26 | from math import pi 27 | 28 | import pytest 29 | 30 | from .. import (utils, globalv) 31 | 32 | __author__ = 'Duncan Macleod ' 33 | 34 | 35 | def test_elapsed_time(): 36 | e = time.time() - globalv.START 37 | assert utils.elapsed_time() - e < .1 38 | 39 | 40 | def test_vprint(capsys): 41 | # test non-verbose 42 | globalv.VERBOSE = False 43 | utils.vprint('anything', stream=sys.stdout) 44 | out, err = capsys.readouterr() 45 | assert out == '' 46 | 47 | # test verbose 48 | globalv.VERBOSE = True 49 | utils.vprint('anything', stream=sys.stdout) 50 | out, err = capsys.readouterr() 51 | assert out == 'anything' 52 | 53 | # test profiled 54 | globalv.PROFILE = True 55 | utils.vprint('anything\n', stream=sys.stdout) 56 | out, err = capsys.readouterr() 57 | assert re.match(r'\Aanything \(\d+\.\d\d\)\n\Z', out) is not None 58 | 59 | 60 | def test_nat_sorted(): 61 | # sorted strings numerically 62 | assert utils.nat_sorted(['1', '10', '2', 'a', 'B']) == [ 63 | '1', '2', '10', 'B', 'a'] 64 | 65 | 66 | @pytest.mark.parametrize('chan, mask', [ 67 | ('L1:TEST-ODC_CHANNEL_OUT_DQ', 'L1:TEST-ODC_CHANNEL_BITMASK'), 68 | ('L1:TEST-ODC_CHANNEL_OUTMON', 'L1:TEST-ODC_CHANNEL_BITMASK'), 69 | ('L1:TEST-ODC_CHANNEL_LATCH', 'L1:TEST-ODC_CHANNEL_BITMASK'), 70 | ('L1:TEST-CHANNEL', 'L1:TEST-CHANNEL') 71 | ]) 72 | def test_get_odc_bitmask(chan, mask): 73 | assert utils.get_odc_bitmask(chan) == mask 74 | 75 | 76 | @pytest.mark.parametrize('value, out', [ 77 | ('my random content', 'my random content'), 78 | ('1', 1), 79 | ('1.', 1.), 80 | ('1,', (1,)), 81 | ('1,2,\'test\',4', (1, 2, 'test', 4)), 82 | ('[], [0], 1/(2*pi)', ([], [0], 1/(2*pi))), 83 | ('lambda x: x**2', lambda x: x ** 2), 84 | (pytest, pytest), 85 | ]) 86 | def test_safe_eval(value, out): 87 | evalue = utils.safe_eval(value) 88 | assert type(evalue) is type(out) 89 | 90 | if not isinstance(value, str): 91 | assert evalue is out 92 | elif callable(out): 93 | assert evalue(4) == out(4) 94 | else: 95 | assert evalue == out 96 | 97 | 98 | def test_safe_eval_2(): 99 | # test unsafe 100 | with pytest.raises(ValueError) as exc: 101 | utils.safe_eval("os.remove('file-that-doesnt-exist')") 102 | assert str(exc.value).startswith('Will not evaluate string containing') 103 | 104 | with pytest.raises(ValueError): 105 | utils.safe_eval("lambda x: shutil.remove('file-that-doesnt-exist')") 106 | 107 | # test locals or globals 108 | assert utils.safe_eval('test', globals_={'test': 4}) == 4 109 | assert utils.safe_eval('type(self)', 110 | locals_={'self': globalv}) == type(globalv) 111 | 112 | 113 | @pytest.mark.parametrize('ifo, host', [ 114 | ('G1', 'host.atlas.aei.uni-hannover.de'), 115 | ('H1', 'host.ligo-wa.caltech.edu'), 116 | ('L1', 'host.ligo-la.caltech.edu'), 117 | ('V1', 'host.virgo.ego.it'), 118 | ]) 119 | def test_get_default_ifo(ifo, host): 120 | assert utils.get_default_ifo(host) == ifo 121 | 122 | with pytest.raises(ValueError): 123 | utils.get_default_ifo('host.ligo.caltech.edu') 124 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # ----------------------- 2 | # 3 | # Run a full build-and-test from the git repo 4 | # using a combination of conda and pip to install 5 | # all optional dependencies. 6 | # 7 | # This is the 'full' test suite. 8 | # 9 | # ----------------------- 10 | 11 | name: Build and test 12 | 13 | on: 14 | push: 15 | branches: 16 | - main 17 | - master 18 | - release/** 19 | pull_request: 20 | branches: 21 | - main 22 | - master 23 | - release/** 24 | 25 | concurrency: 26 | group: ${{ github.workflow }}-${{ github.ref }} 27 | cancel-in-progress: true 28 | 29 | jobs: 30 | conda: 31 | name: Python ${{ matrix.python-version }} (${{ matrix.os }}) 32 | 33 | strategy: 34 | fail-fast: false 35 | matrix: 36 | os: 37 | - macOS 38 | - Ubuntu 39 | python-version: 40 | - "3.10" 41 | - "3.11" 42 | runs-on: ${{ matrix.os }}-latest 43 | 44 | # this is needed for conda environments to activate automatically 45 | defaults: 46 | run: 47 | shell: bash -el {0} 48 | 49 | steps: 50 | - name: Get source code 51 | uses: actions/checkout@v5 52 | with: 53 | fetch-depth: 0 54 | 55 | - name: Cache conda packages 56 | uses: actions/cache@v4 57 | env: 58 | # increment to reset cache 59 | CACHE_NUMBER: 0 60 | with: 61 | path: ~/conda_pkgs_dir 62 | key: ${{ runner.os }}-conda-${{ matrix.python-version }}-${{ env.CACHE_NUMBER }} 63 | restore-keys: ${{ runner.os }}-conda-${{ matrix.python-version }}- 64 | 65 | - name: Configure conda 66 | uses: conda-incubator/setup-miniconda@v3 67 | with: 68 | auto-update-conda: true 69 | miniforge-version: latest 70 | python-version: ${{ matrix.python-version }} 71 | 72 | - name: Conda info 73 | run: conda info --all 74 | 75 | - name: Install dependencies 76 | run: | 77 | # parse requirements to install as much as possible with conda 78 | conda create --name pip2conda pip2conda 79 | conda run -n pip2conda pip2conda \ 80 | --all \ 81 | --output environment.txt \ 82 | --python-version ${{ matrix.python-version }} 83 | echo "-----------------" 84 | cat environment.txt 85 | echo "-----------------" 86 | conda install --quiet --yes --name test --file environment.txt 87 | 88 | - name: Install GWSumm 89 | run: python -m pip install . --no-build-isolation -vv 90 | 91 | - name: Package list 92 | run: conda list --name test 93 | 94 | - name: Run test suite 95 | run: python -m pytest -ra --color yes --cov gwsumm --pyargs gwsumm --cov-report=xml --junitxml=pytest.xml 96 | 97 | - name: Test command-line interfaces 98 | run: | 99 | python -m coverage run --append --source gwsumm -m gwsumm --help 100 | python -m coverage run --append --source gwsumm -m gwsumm day --help 101 | python -m coverage run --append --source gwsumm -m gwsumm week --help 102 | python -m coverage run --append --source gwsumm -m gwsumm month --help 103 | python -m coverage run --append --source gwsumm -m gwsumm gps --help 104 | python -m coverage run --append --source gwsumm -m gwsumm.batch --help 105 | python -m coverage run --append --source gwsumm -m gwsumm.plot.triggers --help 106 | python -m coverage run --append --source gwsumm -m gwsumm.plot.guardian --help 107 | 108 | - name: Coverage report 109 | run: python -m coverage report --show-missing 110 | 111 | - name: Publish coverage to Codecov 112 | uses: codecov/codecov-action@v5.5.1 113 | with: 114 | files: coverage.xml 115 | flags: ${{ runner.os }}-python${{ matrix.python-version }} 116 | token: ${{ secrets.CODECOV_TOKEN }} 117 | 118 | - name: Upload test results 119 | if: always() 120 | uses: actions/upload-artifact@v5 121 | with: 122 | name: pytest-conda-${{ matrix.os }}-${{ matrix.python-version }} 123 | path: pytest.xml 124 | -------------------------------------------------------------------------------- /gwsumm/mode.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Duncan Macleod (2013) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """Job modes 20 | """ 21 | 22 | import os.path 23 | from enum import (Enum, unique) 24 | 25 | from . import globalv 26 | 27 | __author__ = 'Duncan Macleod ' 28 | 29 | 30 | # -- operating Mode ----------------------------------------------------------- 31 | 32 | # https://docs.python.org/3/library/enum.html#orderedenum 33 | class OrderedEnum(Enum): 34 | def __ge__(self, other): 35 | if self.__class__ is other.__class__: 36 | return self.value >= other.value 37 | return NotImplemented 38 | 39 | def __gt__(self, other): 40 | if self.__class__ is other.__class__: 41 | return self.value > other.value 42 | return NotImplemented 43 | 44 | def __le__(self, other): 45 | if self.__class__ is other.__class__: 46 | return self.value <= other.value 47 | return NotImplemented 48 | 49 | def __lt__(self, other): 50 | if self.__class__ is other.__class__: 51 | return self.value < other.value 52 | return NotImplemented 53 | 54 | 55 | # set mode enum 56 | @unique 57 | class Mode(OrderedEnum): 58 | """Enumeration of valid processing 'modes' 59 | 60 | Each mode provides an association with a particular GPS interval 61 | """ 62 | # no GPS associations 63 | static = 0 64 | # central GPS time (with duration) 65 | event = 1 66 | # arbitrary GPS [start, end) interval 67 | gps = 2 68 | # calendar epochs 69 | day = 10 70 | week = 11 71 | month = 12 72 | year = 13 73 | 74 | def dir_format(self): 75 | if self == Mode.day: 76 | return os.path.join('day', '%Y%m%d') 77 | elif self == Mode.week: 78 | return os.path.join('week', '%Y%m%d') 79 | elif self == Mode.month: 80 | return os.path.join('month', '%Y%m') 81 | elif self == Mode.year: 82 | return os.path.join('year', '%Y') 83 | raise ValueError("Cannot format base for Mode %s" % self) 84 | 85 | def is_calendar(self): 86 | if self >= Mode.day: 87 | return True 88 | return False 89 | 90 | 91 | # -- Mode accessors ----------------------------------------------------------- 92 | 93 | def get_mode(m=None): 94 | """Return the enum for the given mode, defaults to the current mode. 95 | """ 96 | if m is None: 97 | m = globalv.MODE 98 | if isinstance(m, (int, Enum)): 99 | return Mode(m) 100 | else: 101 | try: 102 | return Mode[str(m).lower()] 103 | except KeyError: 104 | raise ValueError("%s is not a valid Mode" % m) 105 | 106 | 107 | def set_mode(m): 108 | """Set the current mode. 109 | """ 110 | if isinstance(m, int): 111 | m = Mode(m) 112 | elif not isinstance(m, Mode): 113 | try: 114 | m = Mode[str(m).lower()] 115 | except KeyError: 116 | raise ValueError("%s is not a valid Mode" % m) 117 | globalv.MODE = m.value 118 | 119 | 120 | # -- Mode utilities ----------------------------------------------------------- 121 | 122 | def get_base(date, mode=None): 123 | """Determine the correct base attribute for the given date and mode. 124 | 125 | Parameters 126 | ---------- 127 | date : :class:`datetime.datetime` 128 | formatted date 129 | mode : `int`, `str` 130 | enumerated interger code (or name) for the required mode 131 | 132 | Returns 133 | ------- 134 | base : `str` 135 | the recommended base URL to have a correctly linked calendar 136 | """ 137 | mode = get_mode(mode) 138 | return date.strftime(mode.dir_format()) 139 | -------------------------------------------------------------------------------- /gwsumm/plot/triggers/tests/test_main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Alex Urban (2020) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """Tests for the `gwsumm.plot.triggers` command-line interface 20 | """ 21 | 22 | import os 23 | import pytest 24 | import shutil 25 | 26 | from unittest import mock 27 | 28 | from gwpy.segments import ( 29 | Segment, 30 | SegmentList, 31 | DataQualityFlag, 32 | ) 33 | 34 | from .... import globalv 35 | from .. import __main__ as triggers_cli 36 | 37 | __author__ = 'Alex Urban ' 38 | 39 | # -- test configuration 40 | 41 | CHANNEL = "H1:GDS-CALIB_STRAIN" 42 | 43 | # -- test data 44 | 45 | LOCK = DataQualityFlag( 46 | name="H1:DMT-GRD_ISC_LOCK_NOMINAL:1", 47 | active=SegmentList([Segment(2, 2048)]), 48 | known=SegmentList([Segment(0, 3600)]), 49 | ) 50 | 51 | 52 | # -- cli tests ---------------------------------------------------------------- 53 | 54 | @mock.patch( 55 | 'gwpy.segments.DataQualityFlag.query_dqsegdb', 56 | return_value=LOCK, 57 | ) 58 | def test_main(dqflag, tmpdir, caplog): 59 | outdir = str(tmpdir) 60 | plot = os.path.join(outdir, "triggers.png") 61 | args = [ 62 | CHANNEL, 63 | '0', '3600', 64 | '--snr', '1', 65 | '--state', LOCK.name, 66 | '--output-file', plot, 67 | ] 68 | # test output 69 | with pytest.warns(UserWarning) as record: 70 | triggers_cli.main(args) 71 | assert os.path.exists(plot) 72 | assert len(os.listdir(outdir)) == 1 73 | assert 'Read 0 events' in caplog.text 74 | assert "0 events in state '{}'".format(LOCK.name) in caplog.text 75 | assert '0 events remaining with snr >= 1.0' in caplog.text 76 | assert 'Plot saved to {}'.format(plot) in caplog.text 77 | # test the `UserWarning` 78 | # FIXME: once MatplotlibDeprecationWarning about colormaps is fixed, 79 | # assert that this UserWarning is the **only** warning 80 | assert (record[0].message.args[0] == 81 | "Caught ValueError: No channel-level directory found at " 82 | "/home/detchar/triggers/*/H1/GDS-CALIB_STRAIN_Omicron. Either " 83 | "the channel name or ETG names are wrong, or this channel is not " 84 | "configured for this ETG.") 85 | # clean up 86 | globalv.TRIGGERS = {} 87 | shutil.rmtree(outdir, ignore_errors=True) 88 | 89 | 90 | def test_main_with_cache_and_tiles(tmpdir, caplog): 91 | outdir = str(tmpdir) 92 | cache = os.path.join(outdir, "empty.cache") 93 | plot = os.path.join(outdir, "triggers.png") 94 | args = [ 95 | CHANNEL, 96 | '0', '3600', 97 | '--cache-file', cache, 98 | '--snr', '1', 99 | '--plot-params', 'legend-loc="upper right"', 100 | '--tiles', 101 | '--output-file', plot, 102 | ] 103 | # write an empty cache file 104 | with open(cache, 'w') as f: 105 | f.write("") 106 | # test output 107 | triggers_cli.main(args) 108 | assert os.path.exists(plot) 109 | assert len(os.listdir(outdir)) == 2 # 1 input, 1 output 110 | assert 'Read cache of 0 files' in caplog.text 111 | assert 'Read 0 events' in caplog.text 112 | assert '0 events remaining with snr >= 1.0' in caplog.text 113 | assert 'Plot saved to {}'.format(plot) in caplog.text 114 | # clean up 115 | globalv.TRIGGERS = {} 116 | shutil.rmtree(outdir, ignore_errors=True) 117 | 118 | 119 | def test_main_invalid_columns(capsys): 120 | args = [ 121 | CHANNEL, 122 | '0', '3600', 123 | '--columns', 'invalid', 124 | ] 125 | # test output 126 | with pytest.raises(SystemExit): 127 | triggers_cli.main(args) 128 | (_, err) = capsys.readouterr() 129 | assert err.endswith("--columns must receive at least two columns, got 1\n") 130 | -------------------------------------------------------------------------------- /gwsumm/html/tests/test_bootstrap.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Alex Urban (2019) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """Unit tests for gwsumm.html.bootstrap 20 | """ 21 | 22 | __author__ = 'Alex Urban ' 23 | 24 | import pytest 25 | from datetime import datetime 26 | 27 | from gwdetchar.utils.utils import parse_html 28 | 29 | from .. import bootstrap 30 | 31 | # global variables 32 | DATE = datetime.strptime('20140410', '%Y%m%d') 33 | BACKWARD = '«' 34 | CAL = ('{}') 37 | FORWARD = '»' 38 | 39 | 40 | # test utilities 41 | 42 | def test_banner(): 43 | banner = bootstrap.banner('Test', subtitle='Subtest') 44 | assert parse_html(str(banner)) == parse_html( 45 | '') 46 | # test with classes 47 | banner_wclass = bootstrap.banner( 48 | 'Test', subtitle='Subtest', titleclass='test', subtitleclass='subtest') 49 | assert parse_html(str(banner_wclass)) == parse_html( 50 | '') 52 | 53 | 54 | @pytest.mark.parametrize('mode, datefmt', [ 55 | ('day', 'April 10 2014'), 56 | ('week', 'Week of April 10 2014'), 57 | ('month', 'April 2014'), 58 | ('year', '2014'), 59 | ]) 60 | def test_calendar(mode, datefmt): 61 | backward, cal, forward = bootstrap.calendar(DATE, mode=mode) 62 | assert parse_html(str(backward)) == parse_html(str(BACKWARD)) 63 | assert parse_html(str(cal)) == parse_html( 64 | CAL.format('%ss' % mode, datefmt)) 65 | assert parse_html(str(forward)) == parse_html(str(FORWARD)) 66 | 67 | 68 | def test_calendar_no_mode(): 69 | # test with no Mode 70 | with pytest.raises(ValueError) as exc: 71 | bootstrap.calendar(DATE) 72 | assert str(exc.value).startswith('Cannot generate calendar for Mode') 73 | 74 | 75 | def test_wrap_content(): 76 | content = bootstrap.wrap_content('test') 77 | assert parse_html(str(content)) == parse_html( 78 | '
\ntest\n
') 79 | 80 | 81 | def test_state_switcher(): 82 | switcher = bootstrap.state_switcher([('Test', '#test')]) 83 | assert parse_html(str(switcher)) == parse_html( 84 | '') 93 | 94 | 95 | def test_base_map_dropdown(): 96 | menu = bootstrap.base_map_dropdown('test', id_='id') 97 | assert parse_html(str(menu)) == parse_html( 98 | '') 100 | # test with bases 101 | menu_wbases = bootstrap.base_map_dropdown('test', bases={'key': 'value'}) 102 | assert parse_html(str(menu_wbases)) == parse_html( 103 | '') 108 | -------------------------------------------------------------------------------- /gwsumm/tests/test_channels.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Duncan Macleod (2013) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """Test suite 20 | 21 | """ 22 | 23 | import pytest 24 | 25 | from astropy import units 26 | 27 | from gwpy.detector import ChannelList 28 | 29 | from .. import (globalv, channels) 30 | from ..mode import (get_mode, set_mode) 31 | 32 | from gwpy.detector import Channel 33 | 34 | from .common import empty_globalv_CHANNELS 35 | 36 | __author__ = 'Duncan Macleod ' 37 | 38 | TEST_NAME = 'H1:GDS-CALIB_STRAIN' 39 | TREND_NAME = 'H1:TEST-TREND_CHANNEL.rms,m-trend' 40 | TREND_NAME2 = 'H1:TEST-TREND_CHANNEL.mean,m-trend' 41 | TREND_NAME3 = 'H1:TEST-TREND_CHANNEL_3.mean' 42 | TREND_NAME4 = 'H1:TEST-TREND_CHANNEL_4.mean' 43 | 44 | DEFAULT_MODE = get_mode() 45 | 46 | 47 | def teardown_module(): 48 | """Undo any set_mode() operations from this module 49 | """ 50 | set_mode(DEFAULT_MODE) 51 | 52 | 53 | @empty_globalv_CHANNELS 54 | def test_get_channel(): 55 | """Test :func:`gwsumm.channels.get_channel` 56 | """ 57 | nchan = len(globalv.CHANNELS) 58 | 59 | # test simple query 60 | chan = channels.get_channel(TEST_NAME) 61 | assert len(globalv.CHANNELS) == nchan + 1 62 | assert chan.name == TEST_NAME 63 | 64 | # make sure that querying again returns the same object 65 | chan2 = channels.get_channel(TEST_NAME) 66 | assert len(globalv.CHANNELS) == nchan + 1 67 | assert chan2 is chan 68 | 69 | 70 | @empty_globalv_CHANNELS 71 | def test_get_channel_trend(): 72 | """Test get_channel for trends 73 | 74 | `get_channel` should query for the trend and the underlying 75 | raw channel 76 | """ 77 | # test simple query 78 | nchan = len(globalv.CHANNELS) 79 | chan = channels.get_channel(TREND_NAME) 80 | assert len(globalv.CHANNELS) == nchan + 2 81 | 82 | # test that raw doesn't get built again 83 | chan = channels.get_channel(TREND_NAME2) 84 | assert len(globalv.CHANNELS) == nchan + 3 85 | 86 | # test that raw matches trend 87 | raw = channels.get_channel(TREND_NAME.split('.')[0]) 88 | assert len(globalv.CHANNELS) == nchan + 3 89 | assert raw.name == TREND_NAME.split('.')[0] 90 | 91 | # test default trend type 92 | chan = channels.get_channel(TREND_NAME3) 93 | assert chan.type is None 94 | 95 | 96 | @empty_globalv_CHANNELS 97 | def test_get_channels(): 98 | names = [TEST_NAME, TREND_NAME, TREND_NAME2] 99 | nchan = len(globalv.CHANNELS) 100 | chans = channels.get_channels(names) 101 | # trend channels auto create entry for the upstream channel so '+ 1' 102 | assert len(globalv.CHANNELS) == nchan + 3 + 1 103 | for name, chan in zip(names, chans): 104 | assert name == chan.ndsname 105 | 106 | 107 | @empty_globalv_CHANNELS 108 | def test_update_missing_channel_params(): 109 | # define empty channel 110 | chan = channels.get_channel('X1:TEST:1') 111 | assert chan.unit is None 112 | 113 | # update using kwargs 114 | channels.update_missing_channel_params('X1:TEST:1', unit='meter') 115 | assert chan.unit == units.meter 116 | chan.unit = None 117 | 118 | # update from another channel 119 | c2 = Channel('X1:TEST:1', unit='V') 120 | channels.update_missing_channel_params(c2) 121 | assert chan.unit == units.volt 122 | 123 | 124 | @pytest.mark.parametrize('cstr, clist', [ 125 | ('X1:TEST,Y1:TEST,\nZ1:TEST', ['X1:TEST', 'Y1:TEST', 'Z1:TEST']), 126 | ('X1:TEST.rms,m-trend,Y1:TEST.mean,s-trend', 127 | ['X1:TEST.rms,m-trend', 'Y1:TEST.mean,s-trend']), 128 | ]) 129 | def test_split(cstr, clist): 130 | assert channels.split(cstr) == clist 131 | 132 | 133 | @pytest.mark.parametrize('cstr, clist', [ 134 | ('X1:TEST + Y1:TEST', ['X1:TEST', 'Y1:TEST']), 135 | ('X1:TEST.mean,m-trend * Y1:TEST + 1', 136 | ['X1:TEST.mean,m-trend', 'Y1:TEST']), 137 | ('G1:DER_DATA_H-rms - 4 + G1:DER_DATA_BLAH', 138 | ['G1:DER_DATA_H-rms', 'G1:DER_DATA_BLAH']), 139 | ]) 140 | def test_split_combination(cstr, clist): 141 | split = channels.split_combination(cstr) 142 | assert isinstance(split, ChannelList) 143 | assert list(map(str, split)), clist 144 | -------------------------------------------------------------------------------- /gwsumm/tests/test_tabs.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Duncan Macleod (2013) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """Tests for `gwsumm.tabs` 20 | 21 | """ 22 | 23 | import os.path 24 | 25 | import pytest 26 | 27 | from .. import tabs 28 | from ..plot import SummaryPlot 29 | 30 | __author__ = 'Duncan Macleod ' 31 | 32 | 33 | # -- gwpy.tabs.registry ------------------------------------------------------- 34 | 35 | @pytest.mark.parametrize('name, tab', [ 36 | ('basic', tabs.Tab), 37 | ('\'plots\'', tabs.PlotTab), 38 | ]) 39 | def test_get_tab(name, tab): 40 | assert tabs.get_tab(name) is tab 41 | with pytest.raises(ValueError): 42 | tabs.get_tab('fasmklwea') 43 | 44 | 45 | def test_register_tab(): 46 | class TestTab(object): 47 | type = 'test' 48 | pass 49 | 50 | tabs.register_tab(TestTab) 51 | with pytest.raises(ValueError): 52 | tabs.register_tab(TestTab) 53 | tabs.register_tab(TestTab, force=True) 54 | 55 | assert tabs.get_tab('test') is TestTab 56 | 57 | tabs.register_tab(TestTab, name='test-with-name') 58 | assert tabs.get_tab('test-with-name') is TestTab 59 | 60 | 61 | # -- test tab classes --------------------------------------------------------- 62 | 63 | class TestTab(object): 64 | TYPE = 'basic' 65 | DEFAULT_ARGS = ['Test'] 66 | 67 | @classmethod 68 | def setup_class(cls): 69 | cls.TAB = tabs.get_tab(cls.TYPE) 70 | 71 | def create(self, *args, **kwargs): 72 | args = list(args) 73 | while len(args) < len(self.DEFAULT_ARGS): 74 | args.append(self.DEFAULT_ARGS[len(args)]) 75 | return self.TAB(*args, **kwargs) 76 | 77 | def test_init(self): 78 | self._test_init('Test') 79 | 80 | def _test_init(self, *args, **kwargs): 81 | if len(args) == 0: 82 | args = self.DEFAULT_ARGS 83 | # test basic creation and defaults 84 | tab = self.create(*args, **kwargs) 85 | assert tab.type == self.TYPE 86 | assert tab.name == args[0] 87 | assert tab.shortname == kwargs.pop('shortname', tab.name) 88 | assert tab.children == kwargs.pop('children', []) 89 | assert tab.parent == kwargs.pop('parent', None) 90 | assert tab.group == kwargs.pop('group', None) 91 | assert tab.path == kwargs.pop('path', os.path.curdir) 92 | assert tab.hidden == kwargs.pop('hidden', False) 93 | return tab 94 | 95 | def test_shortname(self): 96 | tab = self.create() 97 | assert tab.shortname == tab.name 98 | tab = self.create('Test', shortname='ShortTest') 99 | assert tab.shortname == 'ShortTest' 100 | 101 | def test_index(self): 102 | tab = self.create() 103 | assert tab.index == os.path.join('test', 'index.html') 104 | tab2 = self.create('Parent') 105 | del tab.index 106 | tab.set_parent(tab2) 107 | assert tab.index == os.path.join('parent', 'test', 'index.html') 108 | 109 | 110 | # -- external tab 111 | 112 | class TestExternalTab(TestTab): 113 | TYPE = 'external' 114 | DEFAULT_ARGS = ['Test', '//test.com'] 115 | 116 | def test_init(self): 117 | tab = self._test_init() 118 | assert tab.url == '//test.com' 119 | 120 | 121 | # -- plot tab 122 | 123 | class TestPlotTab(TestTab): 124 | TYPE = 'plots' 125 | 126 | def test_init(self): 127 | plots = ['test.png'] 128 | tab = self._test_init('Test', plots=plots) 129 | assert tab.plots == list(map(SummaryPlot, plots)) 130 | assert tab.layout is None 131 | 132 | def test_add_plot(self): 133 | tab = self.create() 134 | before = tab.plots[:] 135 | plot = 'test.png' 136 | tab.add_plot(plot) 137 | assert tab.plots == before + [SummaryPlot(href=plot)] 138 | 139 | def test_layout(self): 140 | tab = self.create() 141 | tab.set_layout(1) 142 | assert tab.layout == [1] 143 | 144 | tab.set_layout((1, 2)) 145 | assert tab.layout == [1, 2] 146 | 147 | tab.set_layout((1, (1, 2))) 148 | assert tab.layout == [1, (1, 2)] 149 | 150 | with pytest.raises(ValueError): 151 | tab.set_layout('test') 152 | with pytest.raises(ValueError): 153 | tab.set_layout([1, (1, 2, 1)]) 154 | with pytest.warns(DeprecationWarning): 155 | tab.layout = [1] 156 | -------------------------------------------------------------------------------- /gwsumm/tests/test_batch.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Alex Urban (2020) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """Tests for the `gwsumm.batch` command-line interface 20 | """ 21 | 22 | import os 23 | import pytest 24 | import shutil 25 | 26 | from .. import batch 27 | 28 | __author__ = 'Alex Urban ' 29 | __credits__ = 'Evan Goetz ' 30 | 31 | 32 | # -- utilities ---------------------------------------------------------------- 33 | 34 | def _get_inputs(): 35 | """Prepare and return paths to input data products 36 | """ 37 | indir = os.getcwd() 38 | inputs = ( 39 | os.path.join(indir, "global.ini"), 40 | os.path.join(indir, "k1-test.ini"), 41 | ) 42 | # write empty input files 43 | for filename in inputs: 44 | with open(filename, 'w') as f: 45 | f.write("") 46 | return inputs 47 | 48 | 49 | # -- cli tests ---------------------------------------------------------------- 50 | 51 | def test_main(tmpdir, caplog): 52 | outdir = str(tmpdir) 53 | (global_, k1test,) = _get_inputs() 54 | args = [ 55 | '--verbose', 56 | '--ifo', 'K1', 57 | '--maxjobs', '5', 58 | '--condor-timeout', '3', 59 | '--condor-command', 'notification=false', 60 | '--condor-command', 'environment=VAR1=VAL1', 61 | '--condor-command', 'environment=VAR2=VAL2', 62 | '--config-file', k1test, 63 | '--global-config', global_, 64 | '--nds', 65 | '--multi-process', '4', 66 | '--archive', 67 | '--event-cache', '/this/cache/is/not/real.cache', 68 | '--no-htaccess', 69 | '--output-dir', outdir, 70 | ] 71 | # test log output 72 | batch.main(args) 73 | assert "Copied all INI configuration files to ./etc" in caplog.text 74 | assert "Appending VAR2=VAL2 to condor 'environment' value" in caplog.text 75 | assert " -- Configured HTML htmlnode job" in caplog.text 76 | assert " -- Configured job for config {}".format( 77 | os.path.join(outdir, "etc", os.path.basename(k1test))) in caplog.text 78 | assert "Setup complete, DAG written to: {}".format( 79 | os.path.join(outdir, "gw_summary_pipe.dag")) in caplog.text 80 | # test file output 81 | assert set(os.listdir(outdir)) == { 82 | "etc", 83 | "gw_summary_pipe_local.sub", 84 | "gw_summary_pipe.dag", 85 | "logs", 86 | "gw_summary_pipe.sub", 87 | "gw_summary_pipe.sh", 88 | } 89 | assert set(os.listdir(os.path.join(outdir, "etc"))) == { 90 | os.path.basename(k1test), 91 | os.path.basename(global_), 92 | } 93 | assert set(os.listdir(os.path.join(outdir, "logs"))) == set() 94 | # clean up 95 | for filename in (global_, k1test,): 96 | os.remove(filename) 97 | shutil.rmtree(outdir, ignore_errors=True) 98 | 99 | 100 | @pytest.mark.parametrize( 101 | 'mode', 102 | (['--day', '20170209'], 103 | ['--week', '20170209'], 104 | ['--month', '201702'], 105 | ['--year', '2017'], 106 | ['--gps-start-time', '1170633618', '--gps-end-time', '1170720018']), 107 | ) 108 | def test_main_loop_over_modes(tmpdir, caplog, mode): 109 | outdir = str(tmpdir) 110 | (global_, k1test,) = _get_inputs() 111 | args = [ 112 | '--verbose', 113 | '--ifo', 'K1', 114 | '--universe', 'local', 115 | '--config-file', k1test, 116 | '--global-config', global_, 117 | '--single-process', 118 | '--output-dir', outdir, 119 | ] 120 | # test log output 121 | batch.main(args + mode) 122 | assert "Copied all INI configuration files to ./etc" in caplog.text 123 | assert " -- Configured HTML htmlnode job" in caplog.text 124 | assert " -- Configured job for config {}".format( 125 | os.path.join(outdir, "etc", os.path.basename(k1test))) in caplog.text 126 | assert "Setup complete, DAG written to: {}".format( 127 | os.path.join(outdir, "gw_summary_pipe.dag")) in caplog.text 128 | # clean up 129 | for filename in (global_, k1test,): 130 | os.remove(filename) 131 | shutil.rmtree(outdir, ignore_errors=True) 132 | 133 | 134 | def test_main_invalid_modes(capsys): 135 | args = [ 136 | '--ifo', 'V1', 137 | '--day', '20170209', 138 | '--month', '201702', 139 | ] 140 | # test output 141 | with pytest.raises(SystemExit): 142 | batch.main(args) 143 | (_, err) = capsys.readouterr() 144 | assert err.endswith("Please give only one of --day, --month, or " 145 | "--gps-start-time and --gps-end-time.\n") 146 | -------------------------------------------------------------------------------- /gwsumm/tests/test_archive.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Duncan Macleod (2013) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """Tests for `gwsumm.archive` 20 | 21 | """ 22 | 23 | import os 24 | import tempfile 25 | 26 | import pytest 27 | 28 | import h5py 29 | 30 | from numpy import (random, testing as nptest) 31 | 32 | from gwpy.table import EventTable 33 | from gwpy.timeseries import (TimeSeries, StateVector) 34 | from gwpy.spectrogram import Spectrogram 35 | from gwpy.segments import (Segment, SegmentList) 36 | 37 | from .. import (archive, data, globalv, channels, triggers) 38 | 39 | __author__ = 'Duncan Macleod ' 40 | 41 | TEST_DATA = TimeSeries([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], epoch=100, 42 | unit='meter', sample_rate=1, channel='X1:TEST-CHANNEL', 43 | name='TEST DATA') 44 | TEST_DATA.channel = channels.get_channel(TEST_DATA.channel) 45 | 46 | 47 | # -- utilities ---------------------------------------------------------------- 48 | 49 | def empty_globalv(): 50 | globalv.DATA = type(globalv.DATA)() 51 | globalv.SPECTROGRAMS = type(globalv.SPECTROGRAMS)() 52 | globalv.SEGMENTS = type(globalv.SEGMENTS)() 53 | globalv.TRIGGERS = type(globalv.TRIGGERS)() 54 | 55 | 56 | def create(data, **metadata): 57 | SeriesClass = metadata.pop('series_class', TimeSeries) 58 | d = SeriesClass(data, **metadata) 59 | d.channel = channels.get_channel(d.channel) 60 | if not d.name: 61 | d.name = d.channel.texname 62 | return d 63 | 64 | 65 | def add_data(): 66 | data.add_timeseries(TEST_DATA) 67 | data.add_timeseries(create([1, 2, 3, 4, 5], 68 | dt=60., channel='X1:TEST-TREND.mean')) 69 | data.add_timeseries(create([1, 2, 3, 2, 1], 70 | series_class=StateVector, 71 | channel='X1:TEST-STATE_VECTOR')) 72 | data.add_spectrogram(create([[1, 2, 3], [3, 2, 1], [1, 2, 3]], 73 | series_class=Spectrogram, 74 | channel='X1:TEST-SPECTROGRAM')) 75 | t = EventTable(random.random((100, 5)), names=['time', 'a', 'b', 'c', 'd']) 76 | t.meta['segments'] = SegmentList([Segment(0, 100)]) 77 | triggers.add_triggers(t, 'X1:TEST-TABLE,testing') 78 | 79 | 80 | # -- tests -------------------------------------------------------------------- 81 | 82 | def test_write_archive(delete=True): 83 | empty_globalv() 84 | add_data() 85 | fname = tempfile.mktemp(suffix='.h5', prefix='gwsumm-tests-') 86 | try: 87 | archive.write_data_archive(fname) 88 | archive.write_data_archive(fname) # test again to validate backups 89 | finally: 90 | if delete and os.path.isfile(fname): 91 | os.remove(fname) 92 | 93 | 94 | def test_read_archive(): 95 | empty_globalv() 96 | add_data() 97 | fname = tempfile.mktemp(suffix='.h5', prefix='gwsumm-tests-') 98 | try: 99 | archive.write_data_archive(fname) 100 | except Exception: 101 | raise 102 | empty_globalv() 103 | try: 104 | archive.read_data_archive(fname) 105 | finally: 106 | os.remove(fname) 107 | # check timeseries 108 | ts = data.get_timeseries('X1:TEST-CHANNEL', 109 | [(100, 110)], query=False).join() 110 | nptest.assert_array_equal(ts.value, TEST_DATA.value) 111 | for attr in ['epoch', 'unit', 'sample_rate', 'channel', 'name']: 112 | assert getattr(ts, attr) == getattr(TEST_DATA, attr) 113 | # check trend series 114 | ts = data.get_timeseries('X1:TEST-TREND.mean,m-trend', [(0, 300)], 115 | query=False).join() 116 | assert ts.channel.type == 'm-trend' 117 | assert ts.span == (0, 300) 118 | # check triggers 119 | t = triggers.get_triggers('X1:TEST-TABLE', 'testing', [(0, 100)]) 120 | assert len(t) == 100 121 | 122 | 123 | def test_archive_load_table(): 124 | t = EventTable(random.random((100, 5)), 125 | names=['a', 'b', 'c', 'd', 'e']) 126 | empty = EventTable(names=['a', 'b']) 127 | try: 128 | fname = tempfile.mktemp(suffix='.h5', prefix='gwsumm-tests-') 129 | h5file = h5py.File(fname, mode='a') 130 | # check table gets archived and read transparently 131 | archive.archive_table(t, 'test-table', h5file) 132 | t2 = archive.load_table(h5file['test-table']) 133 | nptest.assert_array_equal(t.as_array(), t2.as_array()) 134 | assert t.dtype == t2.dtype 135 | # check empty table does not get archived, with warning 136 | with pytest.warns(UserWarning): 137 | n = archive.archive_table(empty, 'test-empty', h5file) 138 | assert n is None 139 | assert 'test-empty' not in h5file 140 | finally: 141 | if os.path.exists(fname): 142 | os.remove(fname) 143 | -------------------------------------------------------------------------------- /gwsumm/tabs/stamp.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | # Copyright (C) Duncan Macleod (2013) 3 | # 4 | # This file is part of GWSumm 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see 18 | 19 | """Custom `SummaryTab` for the output of the FScan algorithm. 20 | """ 21 | 22 | import os 23 | import re 24 | import glob 25 | 26 | from MarkupPy import markup 27 | 28 | from gwdetchar.io import html 29 | 30 | from .registry import (get_tab, register_tab) 31 | 32 | from ..mode import Mode 33 | from ..plot import get_plot 34 | from ..config import GWSummConfigParser 35 | 36 | __author__ = 'Duncan Macleod ' 37 | __all__ = ['StampPEMTab'] 38 | 39 | base = get_tab('default') 40 | SummaryPlot = get_plot(None) 41 | 42 | 43 | class StampPEMTab(base): 44 | """Custom tab displaying a summary of StampPEM results. 45 | """ 46 | type = 'stamp' 47 | 48 | def __init__(self, *args, **kwargs): 49 | if kwargs['mode'] != Mode.day: 50 | raise RuntimeError("StampPEMTab is only available in %s mode." 51 | % Mode.day.name) 52 | super(StampPEMTab, self).__init__(*args, **kwargs) 53 | 54 | @classmethod 55 | def from_ini(cls, config, section, **kwargs): 56 | """Define a new `StampPEMTab` from a `ConfigParser`. 57 | """ 58 | # parse generic configuration 59 | new = super(StampPEMTab, cls).from_ini(config, section, **kwargs) 60 | new.set_layout([2]) 61 | 62 | # work out day directory and url 63 | new.directory = os.path.normpath(config.get(section, 'base-directory')) 64 | try: 65 | home_, postbase = new.directory.split('/public_html/', 1) 66 | except ValueError as e: 67 | e.args = ('Stamp PEM directory not under \'public_html\', ' 68 | 'cannot format linkable URL',) 69 | raise 70 | else: 71 | user = os.path.split(home_)[1] 72 | new.url = '/~%s/%s' % (user, postbase.rstrip('/')) 73 | return new 74 | 75 | def process(self, config=GWSummConfigParser(), **kwargs): 76 | # find all plots 77 | self.plots = [] 78 | if isinstance(self.directory, str): 79 | plots = sorted( 80 | glob.glob(os.path.join(self.directory, 'DAY_*.png')), 81 | key=lambda p: float(re.split(r'[-_]', os.path.basename(p))[1])) 82 | for p in plots: 83 | pname = os.path.split(p)[1] 84 | self.plots.append(SummaryPlot( 85 | src=os.path.join(self.url, pname), 86 | href=os.path.join(self.url, 87 | pname.replace('.png', '.html')))) 88 | 89 | def write_state_html(self, state): 90 | """Write the '#main' HTML content for this `StampPEMTab`. 91 | """ 92 | page = markup.page() 93 | 94 | a = markup.oneliner.a('analysis', href=self.url+'/', 95 | class_='alert-link', rel='external', 96 | target='_blank') 97 | if not os.path.isdir(self.directory): 98 | page.add(html.alert(( 99 | "No %s was performed for this period, " 100 | "please try again later." % a, 101 | "If you believe these data should have been found, please " 102 | "contact %s." 103 | % markup.oneliner.a('the DetChar group', 104 | class_='alert-link', 105 | href='mailto:detchar@ligo.org'), 106 | ), context='warning', dismiss=False)) 107 | 108 | elif not self.plots: 109 | page.add(html.alert(( 110 | "This %s produced no plots." % a, 111 | "If you believe these data should have been found, please " 112 | "contact %s." 113 | % markup.oneliner.a('the DetChar group', 114 | class_='alert-link', 115 | href='mailto:detchar@ligo.org'), 116 | ), context='warning', dismiss=False)) 117 | 118 | else: 119 | page.add(str(self.scaffold_plots( 120 | aclass='fancybox-stamp plot', 121 | **{'data-fancybox-type': 'iframe'}))) 122 | page.hr(class_='row-divider') 123 | 124 | # link full results 125 | page.hr(class_='row-divider') 126 | page.div(class_='btn-group') 127 | page.a('Click here for the full Stamp PEM results', 128 | href=self.url+'/', rel='external', target='_blank', 129 | class_='btn btn-info btn-xl') 130 | page.div.close() 131 | 132 | # write to file 133 | idx = self.states.index(state) 134 | with open(self.frames[idx], 'w') as fobj: 135 | fobj.write(str(page)) 136 | return self.frames[idx] 137 | 138 | 139 | register_tab(StampPEMTab) 140 | -------------------------------------------------------------------------------- /gwsumm/plot/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Duncan Macleod (2013-2015) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """Utilies for GWSumm plotting 20 | """ 21 | 22 | import hashlib 23 | import itertools 24 | import re 25 | 26 | from matplotlib import rcParams 27 | 28 | from gwpy.plot.utils import ( # noqa: F401 29 | FIGURE_PARAMS, 30 | AXES_PARAMS, 31 | ) 32 | 33 | from gwdetchar.utils.plot import texify 34 | 35 | __author__ = 'Duncan Macleod ' 36 | 37 | # -- plotting parameters ------------------------------------------------------ 38 | 39 | LINE_PARAMS = [ 40 | 'linewidth', 'linestyle', 'color', 'label', 'alpha', 'rasterized', 41 | 'zorder', 42 | ] 43 | COLLECTION_PARAMS = [ 44 | 'cmap', 'vmin', 'vmax', 'marker', 's', 'norm', 'rasterized', 45 | ] 46 | IMAGE_PARAMS = [ 47 | 'imshow', 'cmap', 'vmin', 'vmax', 'norm', 'rasterized', 'extent', 48 | 'origin', 'interpolation', 'aspect', 49 | ] 50 | HIST_PARAMS = [ 51 | 'bins', 'range', 'normed', 'weights', 'cumulative', 'bottom', 52 | 'histtype', 'align', 'orientation', 'rwidth', 'log', 'color', 53 | 'label', 'stacked', 'logbins', 'density', 54 | ] 55 | LEGEND_PARAMS = [ 56 | 'loc', 'borderaxespad', 'ncol', 57 | ] 58 | ARTIST_PARAMS = set(itertools.chain.from_iterable([ 59 | LINE_PARAMS, 60 | COLLECTION_PARAMS, 61 | IMAGE_PARAMS, 62 | HIST_PARAMS, 63 | ])) 64 | 65 | # -- default labels for table columns ----------------------------------------- 66 | 67 | COLUMN_LABEL = { 68 | 'peal_frequency': r"Frequency [Hz]", 69 | 'central_freq': r"Frequency [Hz]", 70 | 'frequency': r"Frequency [Hz]", 71 | 'mchirp': r"Chirp mass [M$_\odot$]", 72 | 'new_snr': r"$\chi^2$-weighted signal-to-noise ratio (New SNR)", 73 | 'peak_frequency': r"Frequency [Hz]", 74 | 'rho': r"$\rho$", 75 | 'snr': r"Signal-to-noise ratio (SNR)", 76 | 'template_duration': r"Template duration [s]", 77 | } 78 | 79 | 80 | def get_column_label(column): 81 | try: 82 | return COLUMN_LABEL[column] 83 | except KeyError: 84 | return get_column_string(column) 85 | 86 | 87 | def get_column_string(column): 88 | r""" 89 | Format the string columnName (e.g. xml table column) into latex format for 90 | an axis label. 91 | 92 | Parameters 93 | ---------- 94 | column : `str` 95 | string to format 96 | 97 | Examples 98 | -------- 99 | >>> get_column_string('snr') 100 | 'SNR' 101 | >>> get_column_string('bank_chisq_dof') 102 | r'Bank $\chi^2$ DOF' 103 | """ 104 | acro = ['snr', 'ra', 'dof', 'id', 'ms', 'far'] 105 | greek = ['alpha', 'beta', 'gamma', 'delta', 'epsilon', 'zeta', 'eta', 106 | 'theta', 'iota', 'kappa', 'lamda', 'mu', 'nu', 'xi', 'omicron', 107 | 'pi', 'rho', 'sigma', 'tau', 'upsilon', 'phi', 'chi', 'psi', 108 | 'omega'] 109 | unit = ['ns'] 110 | sub = ['flow', 'fhigh', 'hrss', 'mtotal', 'mchirp'] 111 | 112 | tex = rcParams['text.usetex'] 113 | 114 | words = [] 115 | for word in re.split(r'\s', column): 116 | if word.isupper(): 117 | words.append(word) 118 | else: 119 | words.extend(re.split(r'_', word)) 120 | 121 | # parse words 122 | for i, word in enumerate(words): 123 | # get acronym in lower case 124 | if word in acro: 125 | words[i] = word.upper() 126 | # get numerical unit 127 | elif word in unit: 128 | words[i] = '$(%s)$' % word 129 | # get character with subscript text 130 | elif word in sub and tex: 131 | words[i] = r'%s$_{\mbox{\small %s}}$' % (word[0], word[1:]) 132 | # get greek word 133 | elif word in greek and tex: 134 | words[i] = r'$\%s$' % word 135 | # get starting with greek word 136 | elif re.match(r'(%s)' % '|'.join(greek), word) and tex: 137 | if word[-1].isdigit(): 138 | words[i] = r'$\%s_{%s}$''' % tuple( 139 | re.findall(r"[a-zA-Z]+|\d+", word)) 140 | elif word.endswith('sq'): 141 | words[i] = r'$\%s^2$' % word.rstrip('sq') 142 | # get everything else 143 | else: 144 | if word.isupper(): 145 | words[i] = word 146 | else: 147 | words[i] = word.title() 148 | # escape underscore 149 | words[i] = texify(words[i]) 150 | return ' '.join(words) 151 | 152 | 153 | def hash(string, num=6): 154 | """Generate an N-character hash string based using string to initialise 155 | 156 | Parameters 157 | ---------- 158 | string : `str` 159 | the initialisation string 160 | 161 | num : `int`, optional 162 | the length of the hash to produce 163 | 164 | Returns 165 | ------- 166 | hash : `str` 167 | the new hash 168 | 169 | Examples 170 | -------- 171 | >>> from gwsumm.plot.utils import hash 172 | >>> print(hash("I love gravitational waves")) 173 | 80c897 174 | """ 175 | return hashlib.md5(string.encode("utf-8")).hexdigest()[:num] 176 | -------------------------------------------------------------------------------- /docs/_static/css/custom.css: -------------------------------------------------------------------------------- 1 | /.text-primary { 2 | color: #9b59b6; 3 | } 4 | 5 | .bg-primary { 6 | background-color: #9b59b6; 7 | } 8 | 9 | .btn-primary { 10 | background-color: #9b59b6; 11 | border-color: #9b59b6; 12 | } 13 | 14 | a.text-primary:hover, a.text-primary:focus { 15 | color: #653578; 16 | } 17 | 18 | a.bg-primary:hover, a.bg-primary:focus { 19 | background-color: #653578; 20 | } 21 | 22 | .btn-primary:focus, .btn-primary.focus { 23 | background-color: #653578; 24 | border-color: #653578; 25 | } 26 | 27 | .btn-primary:hover { 28 | background-color: #653578; 29 | border-color: #653578; 30 | } 31 | 32 | .btn-primary:active, .btn-primary.active, .open > .dropdown-toggle.btn-primary { 33 | background-color: #653578; 34 | border-color: #653578 35 | } 36 | 37 | .navbar-default .navbar-nav > .active > a, .navbar-default .navbar-nav > .active > a:hover, 38 | .navbar-default .navbar-nav > .active > a:focus { 39 | background-color: #653578 40 | } 41 | 42 | .navbar-default .navbar-nav > .open > a, .navbar-default .navbar-nav > .open > a:hover, 43 | .navbar-default .navbar-nav > .open > a:focus { 44 | background-color: #653578; 45 | } 46 | 47 | .label-primary[href]:hover, .label-primary[href]:focus { 48 | background-color: #653578 49 | } 50 | 51 | a.text-primary:hover, a.text-primary:focus { 52 | color: #653578 53 | } 54 | 55 | a.bg-primary:hover, a.bg-primary:focus { 56 | background-color: #653578 57 | } 58 | 59 | .btn-primary:focus, .btn-primary.focus { 60 | background-color: #653578; 61 | border-color: #653578 62 | } 63 | 64 | .btn-primary:hover { 65 | background-color: #653578; 66 | border-color: #653578 67 | } 68 | 69 | .btn-primary:active, .btn-primary.active, .open > .dropdown-toggle.btn-primary { 70 | background-color: #653578; 71 | border-color: #653578 72 | } 73 | 74 | .navbar-default .navbar-nav > .open > a, .navbar-default .navbar-nav > .open > a:hover, 75 | .navbar-default .navbar-nav > .open > a:focus { 76 | background-color: #653578; 77 | } 78 | 79 | 80 | .label-primary[href]:hover, .label-primary[href]:focus { 81 | background-color: #653578 82 | } 83 | 84 | .btn-primary.disabled:hover, .btn-primary[disabled]:hover, fieldset[disabled] .btn-primary:hover, 85 | .btn-primary.disabled:focus, .btn-primary[disabled]:focus, fieldset[disabled] .btn-primary:focus, 86 | .btn-primary.disabled.focus, .btn-primary[disabled].focus, fieldset[disabled] .btn-primary.focus { 87 | background-color: #9b59b6; 88 | border-color: #9b59b6; 89 | } 90 | 91 | .btn-primary .badge { 92 | color: #9b59b6; 93 | } 94 | 95 | .btn-link { 96 | color: #9b59b6; 97 | } 98 | 99 | .dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus { 100 | background-color: #9b59b6; 101 | } 102 | 103 | .dropdown-menu > .active > a, .dropdown-menu > .active > a:hover, .dropdown-menu > .active > a:focus { 104 | background-color: #9b59b6; 105 | } 106 | 107 | .nav .open > a, .nav .open > a:hover, .nav .open > a:focus { 108 | border-color: #9b59b6; 109 | } 110 | 111 | .nav-pills > li.active > a, .nav-pills > li.active > a:hover, .nav-pills > li.active > a:focus { 112 | background-color: #9b59b6; 113 | } 114 | 115 | .navbar-default { 116 | background-color: #9b59b6; 117 | border-color: #9b59b6; 118 | } 119 | 120 | .navbar-default .navbar-nav > li > a:hover, .navbar-default .navbar-nav > li > a:focus { 121 | background-color: #703b86; 122 | } 123 | 124 | .navbar-default .navbar-toggle { 125 | border-color: #703b86 126 | } 127 | 128 | .navbar-default .navbar-toggle:hover, .navbar-default .navbar-toggle:focus { 129 | background-color: #703b86 130 | } 131 | 132 | @media (max-width:767px) { 133 | .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus { 134 | background-color: #703b86 135 | } 136 | 137 | .navbar-default .navbar-nav .open .dropdown-menu > .active > a, .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, 138 | .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus { 139 | background-color: #653578 140 | } 141 | } 142 | 143 | .pagination > li > a:hover, .pagination > li > span:hover, .pagination > li > a:focus, 144 | .pagination > li > span:focus { 145 | color: #703b86; 146 | } 147 | 148 | a:hover, a:focus { 149 | color: #703b86; 150 | } 151 | 152 | .btn-link:hover, .btn-link:focus { 153 | color: #703b86; 154 | } 155 | 156 | .navbar-default .navbar-nav > li > a:hover, .navbar-default .navbar-nav > li > a:focus { 157 | background-color: #703b86; 158 | } 159 | 160 | .navbar-default .navbar-toggle { 161 | border-color: #703b86; 162 | } 163 | 164 | .navbar-default .navbar-collapse, .navbar-default .navbar-form { 165 | border-color: #d34615 166 | } 167 | 168 | .pagination > li > a, .pagination > li > span { 169 | color: #9b59b6; 170 | } 171 | 172 | .label-primary { 173 | background-color: #9b59b6; 174 | } 175 | 176 | .list-group-item.active > .badge, .nav-pills > .active > a > .badge { 177 | color: #9b59b6; 178 | } 179 | 180 | a.thumbnail:hover, a.thumbnail:focus, a.thumbnail.active { 181 | border-color: #9b59b6; 182 | } 183 | 184 | .progress-bar { 185 | background-color: #9b59b6; 186 | } 187 | 188 | .list-group-item.active, .list-group-item.active:hover, .list-group-item.active:focus { 189 | background-color: #9b59b6; 190 | border-color: #9b59b6; 191 | } 192 | 193 | .panel-primary { 194 | border-color: #9b59b6; 195 | } 196 | 197 | .panel-primary > .panel-heading { 198 | background-color: #9b59b6; 199 | border-color: #9b59b6; 200 | } 201 | 202 | .panel-primary > .panel-heading + .panel-collapse > .panel-body { 203 | border-top-color: #9b59b6; 204 | } 205 | 206 | .panel-primary > .panel-heading .badge { 207 | color: #9b59b6; 208 | } 209 | 210 | .panel-primary > .panel-footer + .panel-collapse > .panel-body { 211 | border-bottom-color: #9b59b6; 212 | } 213 | 214 | .navbar-default .badge { 215 | color: #9b59b6; 216 | } 217 | -------------------------------------------------------------------------------- /gwsumm/data/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Duncan Macleod (2013) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """Utilities for data loading and pre-processing 20 | """ 21 | 22 | from collections import OrderedDict 23 | from functools import wraps 24 | 25 | from gwpy.segments import (DataQualityFlag, SegmentList, Segment) 26 | 27 | from ..channels import get_channel 28 | from ..config import GWSummConfigParser 29 | 30 | __author__ = 'Duncan Macleod ' 31 | 32 | 33 | # -- method decorators -------------------------------------------------------- 34 | 35 | def use_segmentlist(f): 36 | """Decorate a method to convert incoming segments into a `SegmentList` 37 | 38 | This assumes that the method to be decorated takes a segment list as 39 | the second positionsl argument. 40 | """ 41 | @wraps(f) 42 | def decorated_func(arg1, segments, *args, **kwargs): 43 | if isinstance(segments, DataQualityFlag): 44 | segments = segments.active 45 | elif not isinstance(segments, SegmentList): 46 | segments = SegmentList([Segment(*x) for x in segments]) 47 | return f(arg1, segments, *args, **kwargs) 48 | return decorated_func 49 | 50 | 51 | def use_configparser(f): 52 | """Decorate a method to use a valid default for 'config' 53 | 54 | This is just to allow lazy passing of `config=None` 55 | """ 56 | @wraps(f) 57 | def decorated_func(*args, **kwargs): 58 | if kwargs.get('config', None) is None: 59 | kwargs['config'] = GWSummConfigParser() 60 | return f(*args, **kwargs) 61 | return decorated_func 62 | 63 | 64 | # -- handle keys for globalv dicts -------------------------------------------- 65 | # need a key that is unique across channel(s) with a specific for of 66 | # signal-processing parameters 67 | 68 | FFT_PARAMS = OrderedDict([ 69 | ('method', str), # keep this one first (DMM) 70 | ('fftlength', float), 71 | ('overlap', float), 72 | ('window', None), 73 | ('stride', float), 74 | ('scheme', None), 75 | ]) 76 | 77 | DEFAULT_FFT_PARAMS = { 78 | 'method': 'median', 79 | } 80 | 81 | 82 | class FftParams(object): 83 | """Convenience object to hold signal-processing parameters 84 | """ 85 | __slots__ = list(FFT_PARAMS) 86 | 87 | def __init__(self, **kwargs): 88 | for slot in self.__slots__: 89 | kwargs.setdefault(slot, None) 90 | for key in kwargs: 91 | setattr(self, key, kwargs[key]) 92 | 93 | def __setattr__(self, key, val): 94 | if val is not None and FFT_PARAMS[key] is not None: # type cast 95 | val = FFT_PARAMS[key](val) 96 | super(FftParams, self).__setattr__(key, val) 97 | 98 | def __str__(self): 99 | out = [] 100 | for slot in self.__slots__: 101 | attr = getattr(self, slot) 102 | if not attr: 103 | out.append('') 104 | elif slot == 'scheme': 105 | out.append(type(attr).__name__) 106 | else: 107 | out.append(str(attr)) 108 | return ';'.join(out) 109 | 110 | def dict(self): 111 | return dict((x, getattr(self, x)) for x in self.__slots__ if 112 | getattr(self, x) is not None) 113 | 114 | 115 | def get_fftparams(channel, **defaults): 116 | # configure FftParams 117 | params = {k: v for k, v in DEFAULT_FFT_PARAMS.items() if v is not None} 118 | params.update(defaults) 119 | fftparams = FftParams(**params) 120 | 121 | # update FftParams with per-channel overrides 122 | channel = get_channel(channel) 123 | for key in fftparams.__slots__: 124 | try: 125 | setattr(fftparams, key, getattr(channel, key)) 126 | except AttributeError: 127 | try: # set attribute in channel object for future reference 128 | setattr(channel, key, defaults[key]) 129 | except KeyError: 130 | pass 131 | 132 | # set stride to something sensible 133 | if fftparams.stride is None and fftparams.overlap: 134 | fftparams.stride = fftparams.fftlength * 1.5 135 | elif fftparams.stride is None: 136 | fftparams.stride = fftparams.fftlength 137 | 138 | # sanity check parameters 139 | if fftparams.fftlength == 0: 140 | raise ZeroDivisionError("Cannot operate with FFT length of 0") 141 | if fftparams.stride == 0: 142 | raise ZeroDivisionError("Cannot generate spectrogram with stride " 143 | "length of 0") 144 | 145 | return fftparams 146 | 147 | 148 | def make_globalv_key(channels, fftparams=None): 149 | """Generate a unique key for storing data in a globalv `dict` 150 | 151 | Parameters 152 | ---------- 153 | channels : `str`, `list` 154 | one or more channels to group in this key 155 | fftparams : `FftParams` 156 | structured set of signal-processing parameters used to generate the 157 | dataset 158 | """ 159 | if not isinstance(channels, (list, tuple)): 160 | channels = [channels] 161 | channels = list(map(get_channel, channels)) 162 | parts = [] 163 | # comma-separated list of names 164 | parts.append(','.join(c.ndsname for c in channels)) 165 | # colon-separated list of FFT parameters 166 | if fftparams is not None: 167 | parts.append(fftparams) 168 | return ';'.join(map(str, parts)) 169 | -------------------------------------------------------------------------------- /gwsumm/plot/sei.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | # Copyright (C) Duncan Macleod (2013) 3 | # 4 | # This file is part of GWSumm 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see 18 | 19 | """`SummaryTab` for seismic watchdog monitoring 20 | """ 21 | 22 | import re 23 | from configparser import NoOptionError 24 | 25 | from matplotlib.pyplot import subplots 26 | from matplotlib.ticker import NullLocator 27 | 28 | from gwpy.plot import Plot 29 | from gwpy.timeseries import TimeSeriesDict 30 | 31 | from gwdetchar.utils.plot import texify 32 | 33 | from ..channels import get_channel 34 | from ..utils import re_quote 35 | from .registry import (get_plot, register_plot) 36 | 37 | 38 | class SeiWatchDogPlot(get_plot('data')): 39 | """Plot a specific SEI WatchDog trip 40 | """ 41 | type = 'watchdog' 42 | data = 'watchdog' 43 | 44 | def __init__(self, gpstime, chamber, sensor, config, outfile, ifo=None, 45 | duration=30, nds=False, datacache=None): 46 | """Configure a new `SeiWatchDogPlot`. 47 | """ 48 | super(SeiWatchDogPlot, self).__init__([], 49 | int(gpstime) - duration/2., 50 | int(gpstime) + duration/2.) 51 | 52 | # get params 53 | if ifo is None: 54 | ifo = config.get('DEFAULT', 'IFO') 55 | self.ifo = ifo 56 | self.chamber = chamber 57 | self.sensor = sensor 58 | self.gpstime = gpstime 59 | self.duration = duration 60 | self.outputfile = outfile 61 | self.use_nds = nds 62 | 63 | system = (sensor.split(' ')[0] == 'HEPI' and 64 | 'HPI' or sensor.split(' ')[0]) 65 | 66 | # get channels 67 | mapsec = 'sei-wd-map-%s' % sensor 68 | if not config.has_section(mapsec) and re.match(r'ISI ST\d ', sensor): 69 | mapsec = ('sei-wd-map-%s' 70 | % (' '.join(sensor.split(' ', 2)[::2]))) 71 | stubs = list(zip(*sorted( 72 | [o for o in config.items(mapsec) if o[0].isdigit()], 73 | key=lambda x: x[0], 74 | )))[1] 75 | if re.search(r'ISI ST\d ', sensor): 76 | stage = sensor.split(' ')[1] 77 | channels = [get_channel('%s:%s-%s_%s_%s' 78 | % (ifo, system, chamber, stage, stub)) 79 | for stub in stubs] 80 | else: 81 | channels = [get_channel('%s:%s-%s_%s' 82 | % (ifo, system, chamber, stub)) 83 | for stub in stubs] 84 | 85 | # set types 86 | for channel in channels: 87 | if not hasattr(channel, 'type') or not channel.type: 88 | channel.ctype = 'adc' 89 | 90 | self.chanlist = channels 91 | 92 | try: 93 | self.geometry = list(map( 94 | int, config.get(mapsec, 'geometry').split(','))) 95 | except NoOptionError: 96 | self.geometry = (len(channels), 1) 97 | if len(self.chanlist) != self.geometry[0] * self.geometry[1]: 98 | raise ValueError("Geometry does not match number of channels.") 99 | 100 | try: 101 | self.unit = '[%s]' % re_quote.sub('', config.get(mapsec, 'unit')) 102 | except NoOptionError: 103 | self.unit = '' 104 | 105 | @property 106 | def outputfile(self): 107 | return self._outputfile 108 | 109 | @outputfile.setter 110 | def outputfile(self, outfile): 111 | self._outputfile = outfile 112 | 113 | def draw(self): 114 | 115 | # data span 116 | start = self.gpstime - self.duration / 2. 117 | end = self.gpstime + self.duration / 2. 118 | 119 | # get data 120 | if self.use_nds: 121 | data = TimeSeriesDict.fetch(self.chanlist, start, end) 122 | else: 123 | from gwdatafind import find_urls 124 | cache = find_urls(self.ifo[0], f'{self.ifo}_R', 125 | self.start, self.end, urltype='file') 126 | if len(cache) == 0: 127 | data = {} 128 | else: 129 | data = TimeSeriesDict.read(cache, self.chanlist, start=start, 130 | end=end) 131 | 132 | # make plot 133 | plot, axes = subplots(nrows=self.geometry[0], ncols=self.geometry[1], 134 | sharex=True, subplot_kw={'xscale': 'auto-gps'}, 135 | FigureClass=Plot, figsize=[12, 6]) 136 | axes[0, 0].set_xlim(start, end) 137 | for channel, ax in zip(self.chanlist, axes.flat): 138 | ax.set_epoch(self.gpstime) 139 | # plot data 140 | try: 141 | ax.plot(data[channel]) 142 | except KeyError: 143 | ax.text(self.gpstime, 0.5, "No data", va='center', ha='center', 144 | transform=ax.transData) 145 | # plot trip indicator 146 | ax.axvline(self.gpstime, linewidth=0.5, linestyle='--', 147 | color='red') 148 | ax.set_xlabel('') 149 | ax.set_ylabel('') 150 | ax.set_title(texify(channel.name), fontsize=10) 151 | ax.xaxis.set_minor_locator(NullLocator()) 152 | ax.tick_params(axis='x', which='major', labelsize=16) 153 | ax.tick_params(axis='y', which='major', labelsize=10) 154 | plot.text(0.5, 0.02, 'Time [seconds] from trip (%s)' % self.gpstime, 155 | ha='center', va='bottom', fontsize=24) 156 | plot.text(0.01, 0.5, 'Amplitude %s' % self.unit, ha='left', 157 | va='center', rotation='vertical', fontsize=24) 158 | 159 | plot.suptitle('%s %s %s watchdog trip: %s' 160 | % (self.ifo, self.chamber, self.sensor, self.gpstime), 161 | fontsize=24) 162 | 163 | plot.save(self.outputfile) 164 | plot.close() 165 | return self.outputfile 166 | 167 | 168 | register_plot(SeiWatchDogPlot) 169 | -------------------------------------------------------------------------------- /gwsumm/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Duncan Macleod (2013) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """Utilities for GWSumm 20 | """ 21 | 22 | import re 23 | import sys 24 | from socket import getfqdn 25 | 26 | # import filter evals 27 | from math import pi # noqa: F401 28 | import numpy # noqa: F401 29 | 30 | from . import globalv 31 | 32 | re_cchar = re.compile(r"[\W_]+") 33 | re_quote = re.compile(r'^[\s\"\']+|[\s\"\']+$') 34 | re_flagdiv = re.compile(r"(&|!=|!|\|)") 35 | 36 | # define some colours 37 | WARNC = r'\033[93m' 38 | ERRC = r'\033[91m' 39 | ENDC = r'\033[0m' 40 | 41 | # bad things to eval 42 | UNSAFE_EVAL_STRS = [r'os\.(?![$\'\" ])', 'shutil', r'\.rm', r'\.mv'] 43 | UNSAFE_EVAL = re.compile(r'(%s)' % '|'.join(UNSAFE_EVAL_STRS)) 44 | 45 | 46 | # -- utilities ---------------------------------------------------------------- 47 | 48 | def elapsed_time(): 49 | """Return the time (seconds) since this job started 50 | """ 51 | import time 52 | return time.time() - globalv.START 53 | 54 | 55 | def vprint(message, verbose=True, stream=sys.stdout, profile=True): 56 | """Prints the given message to the stream. 57 | 58 | Parameters 59 | ---------- 60 | message : `str` 61 | string to print 62 | verbose : `bool`, optional, default: `True` 63 | flag to print or not, default: print 64 | stream : `file`, optional, default: `stdout` 65 | file object stream in which to print, default: stdout 66 | profile : `bool`, optional, default: `True` 67 | flag to print timestamp for debugging and profiling purposes 68 | """ 69 | if stream != sys.stderr: 70 | profile &= globalv.PROFILE 71 | verbose &= globalv.VERBOSE 72 | if profile and message.endswith("\n"): 73 | message = "%s (%.2f)\n" % (message.rstrip("\n"), elapsed_time()) 74 | if verbose: 75 | stream.write(message) 76 | stream.flush() 77 | 78 | 79 | def nat_sorted(iterable, key=None): 80 | """Sorted a list in the way that humans expect. 81 | 82 | Parameters 83 | ---------- 84 | iterable : `iterable` 85 | iterable to sort 86 | key : `callable` 87 | sorting key 88 | 89 | Returns 90 | ------- 91 | lsorted : `list` 92 | sorted() version of input ``l`` 93 | """ 94 | k = key and list(map(key, iterable)) or iterable 95 | 96 | def convert(text): 97 | if text.isdigit(): 98 | return int(text) 99 | else: 100 | return text 101 | 102 | def alphanum_key(key): 103 | return [convert(c) for c in re.split( 104 | '([0-9]+)', k[iterable.index(key)])] 105 | 106 | return sorted(iterable, key=alphanum_key) 107 | 108 | 109 | _re_odc = re.compile('(OUTMON|OUT_DQ|LATCH)') 110 | 111 | 112 | def get_odc_bitmask(odcchannel): 113 | return _re_odc.sub('BITMASK', str(odcchannel)) 114 | 115 | 116 | def safe_eval(val, strict=False, globals_=None, locals_=None): 117 | """Evaluate the given string as a line of python, if possible 118 | 119 | If the :meth:`eval` fails, a `str` is returned instead, unless 120 | `strict=True` is given. 121 | 122 | Parameters 123 | ---------- 124 | val : `str` 125 | input text to evaluate 126 | 127 | strict : `bool`, optional, default: `False` 128 | raise an exception when the `eval` call fails (`True`) otherwise 129 | return the input as a `str` (`False`, default) 130 | 131 | globals_ : `dict`, optional 132 | dict of global variables to pass to `eval`, defaults to current 133 | `globals` 134 | 135 | locals_ : `dict`, optional 136 | dict of local variables to pass to `eval`, defaults to current 137 | `locals` 138 | 139 | .. note:: 140 | 141 | Note the trailing underscore on the `globals_` and `locals_` 142 | kwargs, this is required to not clash with the builtin `globals` 143 | and `locals` methods`. 144 | 145 | Raises 146 | ------ 147 | ValueError 148 | if the input string is considered unsafe to evaluate, normally 149 | meaning it contains something that might interact with the filesystem 150 | (e.g. `os.path` calls) 151 | NameError 152 | SyntaxError 153 | if the input cannot be evaluated, and `strict=True` is given 154 | 155 | See also 156 | -------- 157 | eval 158 | for more documentation on the underlying evaluation method 159 | """ 160 | # don't evaluate non-strings 161 | if not isinstance(val, str): 162 | return val 163 | # check that we aren't evaluating something dangerous 164 | try: 165 | match = UNSAFE_EVAL.search(val).group() 166 | except AttributeError: 167 | pass 168 | else: 169 | raise ValueError("Will not evaluate string containing %r: %r" 170 | % (match, val)) 171 | # format args for eval 172 | if globals_ is None and locals_ is None: 173 | args = () 174 | elif globals_ is None and locals_ is not None: 175 | args = (globals(), locals_) 176 | elif locals_ is None and globals_ is not None: 177 | args = (globals_,) 178 | else: 179 | args = (globals_, locals_) 180 | # try and eval str 181 | try: 182 | return eval(val, *args) 183 | except (NameError, SyntaxError): 184 | return str(val) 185 | 186 | 187 | # -- IFO parsing -------------------------------------------------------------- 188 | 189 | OBSERVATORY_MAP = { 190 | 'G1': 'GEO', 191 | 'H1': 'LIGO Hanford', 192 | 'K1': 'KAGRA', 193 | 'L1': 'LIGO Livingston', 194 | 'V1': 'Virgo', 195 | } 196 | 197 | 198 | def get_default_ifo(fqdn=getfqdn()): 199 | """Find the default interferometer prefix (IFO) for the given host 200 | 201 | Parameters 202 | ---------- 203 | fqdn : `str` 204 | the fully-qualified domain name (FQDN) of the host on which you 205 | wish to find the default IFO 206 | 207 | Returns 208 | ------- 209 | IFO : `str` 210 | the upper-case X1-style prefix for the default IFO, if found, e.g. `L1` 211 | 212 | Raises 213 | ------ 214 | ValueError 215 | if not default interferometer prefix can be parsed 216 | """ 217 | if '.uni-hannover.' in fqdn or '.atlas.' in fqdn: 218 | return 'G1' 219 | elif '.ligo-wa.' in fqdn: 220 | return 'H1' 221 | elif '.ligo-la.' in fqdn: 222 | return 'L1' 223 | elif '.virgo.' in fqdn or '.ego-gw.' in fqdn: 224 | return 'V1' 225 | raise ValueError("Cannot determine default IFO for host %r" % fqdn) 226 | -------------------------------------------------------------------------------- /gwsumm/html/bootstrap.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Duncan Macleod (2013) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """Helper functions for twitter-bootstrap HTML constructs. 20 | """ 21 | 22 | from MarkupPy import markup 23 | 24 | from gwdetchar.io import html 25 | 26 | from ..mode import (Mode, get_mode) 27 | from ..utils import re_cchar 28 | 29 | __author__ = 'Duncan Macleod ' 30 | 31 | # set for bootstrap 32 | META = {'viewport': 'width=device-width, initial-scale=1.0'} 33 | 34 | 35 | # ----------------------------------------------------------------------------- 36 | # variable HTML constructs 37 | 38 | def banner(title, subtitle=None, titleclass=None, subtitleclass=None): 39 | """Construct a banner heading in bootstrap format 40 | 41 | Parameters 42 | ---------- 43 | title : `str` 44 | name of page (

) 45 | subtitle : `str`, optional 46 | description of page (

) 47 | titleclass : `str`, optional 48 | class option for

49 | subtitleclass : `str`, optional 50 | class option for

51 | 52 | Returns 53 | ------- 54 | banner : `~MarkupPy.markup.page` 55 | markup.py `page` instance 56 | """ 57 | page = markup.page() 58 | page.div(class_='banner') 59 | if titleclass is None: 60 | page.h1(str(title)) 61 | else: 62 | page.h1(str(title), class_=titleclass) 63 | if subtitle is not None and subtitleclass is not None: 64 | page.p(subtitle, class_=subtitleclass) 65 | elif subtitle is not None: 66 | page.p(subtitle) 67 | page.div.close() 68 | 69 | return page 70 | 71 | 72 | def calendar(date, tag='a', class_='nav-link dropdown-toggle', 73 | id_='calendar', dateformat=None, mode=None): 74 | """Construct a bootstrap-datepicker calendar. 75 | 76 | Parameters 77 | ---------- 78 | date : :class:`datetime.datetime`, :class:`datetime.date` 79 | active date for the calendar 80 | tag : `str` 81 | type of enclosing HTML tag, default: ```` 82 | 83 | Returns 84 | ------- 85 | calendar : `list` 86 | a list of three oneliner strings of HTML containing the calendar 87 | text and a triggering dropdown 88 | """ 89 | mode = get_mode(mode) 90 | if dateformat is None: 91 | if mode == Mode.day: 92 | dateformat = '%B %d %Y' 93 | elif mode == Mode.week: 94 | dateformat = 'Week of %B %d %Y' 95 | elif mode == Mode.month: 96 | dateformat = '%B %Y' 97 | elif mode == Mode.year: 98 | dateformat = '%Y' 99 | else: 100 | raise ValueError("Cannot generate calendar for Mode %s" % mode) 101 | datestring = date.strftime(dateformat).replace(' 0', ' ') 102 | data_date = date.strftime('%d-%m-%Y') 103 | # get navigation objects 104 | backward = markup.oneliner.a( 105 | '«', class_='nav-link step-back', title='Step backward') 106 | cal = markup.oneliner.a( 107 | datestring, id_=id_, class_=class_, title='Show/hide calendar', 108 | **{'data-date': data_date, 'data-date-format': 'dd-mm-yyyy', 109 | 'data-viewmode': '%ss' % mode.name}) 110 | forward = markup.oneliner.a( 111 | '»', class_='nav-link step-forward', title='Step forward') 112 | return [backward, cal, forward] 113 | 114 | 115 | def wrap_content(page): 116 | """Utility to wrap some HTML into the relevant

s for the main 117 | body of a page in bootstrap format 118 | 119 | Parameters 120 | ---------- 121 | page : :class:`~MarkupPy.markup.page`, `str` 122 | HTML content to be wrapped 123 | span : `int` 124 | column span of content, default: 'full' (``12``) 125 | 126 | Returns 127 | ------- 128 | wrappedpage : :class:`~MarkupPy.markup.page` 129 | A new `page` with the input content wrapped as 130 | 131 | .. code:: html 132 | 133 |
134 |
135 | """ 136 | out = markup.page() 137 | out.div(class_='container-fluid', id_='main') 138 | out.add(str(page)) 139 | out.div.close() 140 | return out 141 | 142 | 143 | def state_switcher(states, default=0): 144 | """Build a state switch button, including all of the given 145 | states, with the default selected by index 146 | """ 147 | current, chref = states[default] 148 | page = markup.page() 149 | page.ul(class_='nav navbar-nav') 150 | page.li(class_='nav-item dropdown') 151 | page.a(str(current), class_='nav-link dropdown-toggle', href='#', 152 | id_='states', role='button', title='Show/hide state menu', 153 | **{'data-bs-toggle': 'dropdown'}) 154 | page.div( 155 | class_='dropdown-menu dropdown-menu-end state-switch shadow', 156 | id_='statemenu', 157 | ) 158 | page.h6('Select below to view this page in another state (different ' 159 | 'time segments).', class_='dropdown-header') 160 | page.div('', class_='dropdown-divider') 161 | for i, (state, href) in enumerate(states): 162 | page.a(str(state), class_='dropdown-item state', title=str(state), 163 | id_='state_%s' % re_cchar.sub('_', str(state)).lower(), 164 | onclick='jQuery(this).load_state(\'%s\');' % href) 165 | page.div.close() # dropdown-menu dropdown-menu-end 166 | page.li.close() # nav-item dropdown state-switch 167 | page.ul.close() # nav navbar-nav 168 | return page 169 | 170 | 171 | def base_map_dropdown(this, id_=None, bases=dict()): 172 | """Construct a dropdown menu that links to a version of the current 173 | page on another server, based on a new base. 174 | """ 175 | # format id 176 | if id_: 177 | id_ = dict(id_=id_) 178 | else: 179 | id_ = dict() 180 | # format links 181 | baselinks = [markup.oneliner.a( 182 | key, title=key, class_='dropdown-item', **{'data-new-base': val} 183 | ) for (key, val) in bases.items() if key != this] 184 | # slam it all together 185 | page = markup.page() 186 | if baselinks: 187 | page.div(class_='dropdown base-map', **id_) 188 | page.add(str(html.dropdown( 189 | this, 190 | baselinks, 191 | class_='navbar-brand nav-link border border-white ' 192 | 'rounded dropdown-toggle', 193 | ))) 194 | page.div.close() 195 | else: 196 | page.div( 197 | str(this), 198 | class_='navbar-brand border border-white rounded', 199 | **id_, 200 | ) 201 | return page 202 | -------------------------------------------------------------------------------- /gwsumm/html/tests/test_html5.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Alex Urban (2019) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """Unit tests for gwsumm.html.html5 20 | """ 21 | 22 | __author__ = 'Alex Urban ' 23 | 24 | import os 25 | import shutil 26 | 27 | from markdown import markdown 28 | 29 | from gwdetchar.utils.utils import parse_html 30 | 31 | from .. import html5 32 | 33 | # global variables 34 | 35 | URL = 'https://github.com/gwpy/gwsumm' 36 | LOAD = """""" 44 | 45 | BOX = """
""" 64 | 65 | CONTENTS = """Heading 66 | ======= 67 | 68 | This is a test. 69 | 70 | * Bullet 1 71 | * Bullet 2""" 72 | 73 | DIALOG = ('\n
\n%s\n
') % markdown(CONTENTS) 76 | 77 | OVERLAY = ( 78 | '\n
\n' 81 | '

Overlay figures for easy comparison

\n
' 82 | '\n
\n
\n' 83 | '
\n' 84 | '

Instructions

\n%s\n
\n
\n
\n' 85 | '
\nOverlay\n' 87 | 'Download\nClear' 90 | '\n
\n
\n\n
\n
\n' 91 | '
') % markdown(html5.OVERLAY_INSTRUCTIONS) 92 | 93 | 94 | # test utilities 95 | 96 | def test_expand_path(): 97 | assert html5._expand_path(URL) == URL 98 | 99 | 100 | def test_load_state(): 101 | state = html5.load_state('test') 102 | assert parse_html(str(state)) == parse_html( 103 | '') 106 | 107 | 108 | def test_load(): 109 | # test local url 110 | content = html5.load('test') 111 | assert parse_html(content) == parse_html( 112 | '') 113 | # test non-local url 114 | success = 'jQuery(\"#main\").html(data);' 115 | errormsg = ('alert(\"Cannot load content from %r, use ' 116 | 'browser console to inspect failure.\");' % URL) 117 | content = html5.load(URL) 118 | assert parse_html(content) == parse_html(LOAD % (URL, success, errormsg)) 119 | # test with non-string error argument 120 | success = 'jQuery(\"#main\").html(data);' 121 | error = 'Failed to load content from %r' % URL 122 | errormsg = ('jQuery(\"#main\").html(\"

%s

\");' % error) 124 | content = html5.load(URL, error=1) 125 | assert parse_html(content) == parse_html(LOAD % (URL, success, errormsg)) 126 | 127 | 128 | def test_load_custom(): 129 | error = 'Error' 130 | success = 'document.write(\"Success\")' 131 | errormsg = ('jQuery(\"#main\").html(\"

%s

\");' % error) 133 | # test local url 134 | content = html5.load('test', success=success, error=error) 135 | assert parse_html(content) == parse_html( 136 | LOAD % ('test', success, errormsg)) 137 | # test non-local url 138 | content = html5.load(URL, success=success, error=error) 139 | assert parse_html(content) == parse_html(LOAD % (URL, success, errormsg)) 140 | 141 | 142 | def test_comments_box(): 143 | box = html5.comments_box('Test', identifier='test', title='Test', url=URL) 144 | assert parse_html(str(box)) == parse_html(BOX % URL) 145 | 146 | 147 | def test_ldvw_qscan_single(): 148 | button = html5.ldvw_qscan('X1:TEST', 0) 149 | assert parse_html(str(button)) == parse_html( 150 | 'Q-scan') 156 | 157 | 158 | def test_ldvw_qscan_batch(): 159 | button = html5.ldvw_qscan('X1:TEST', (0,)) 160 | assert parse_html(str(button)) == parse_html( 161 | 'Launch omega scans') 166 | 167 | 168 | def test_dialog_box(tmpdir): 169 | mdfile = os.path.join(str(tmpdir), 'test.md') 170 | with open(mdfile, 'w') as f: 171 | f.write(CONTENTS) 172 | box = html5.dialog_box(mdfile, 'Test', 'id', 'T') 173 | assert parse_html(str(box)) == parse_html(DIALOG) 174 | shutil.rmtree(str(tmpdir), ignore_errors=True) 175 | 176 | 177 | def test_overlay_canvas(): 178 | box = html5.overlay_canvas() 179 | assert parse_html(str(box)) == parse_html(OVERLAY) 180 | -------------------------------------------------------------------------------- /gwsumm/plot/guardian/__main__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) Duncan Macleod (2018) 3 | # 4 | # This file is part of GWSumm. 5 | # 6 | # GWSumm is free software: you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation, either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # GWSumm is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with GWSumm. If not, see . 18 | 19 | """Plot the segments for a given Guardian node with a given definition 20 | """ 21 | 22 | import argparse 23 | import os 24 | import re 25 | import shutil 26 | import sys 27 | 28 | from collections import OrderedDict 29 | from configparser import DEFAULTSECT 30 | 31 | from gwpy.time import to_gps 32 | 33 | from gwdetchar.utils.cli import logger 34 | 35 | from ... import globalv 36 | from ...archive import (write_data_archive, read_data_archive) 37 | from ...config import GWSummConfigParser 38 | from ...data import get_timeseries 39 | from ...state import generate_all_state 40 | from ...tabs import GuardianTab 41 | 42 | # set matplotlib backend 43 | from matplotlib import use 44 | use('Agg') 45 | 46 | __author__ = 'Duncan Macleod ' 47 | __credits__ = 'Alex Urban ' 48 | 49 | PROG = ('python -m gwsumm.plot.guardian' if sys.argv[0].endswith('.py') 50 | else os.path.basename(sys.argv[0])) 51 | LOGGER = logger(name=PROG.split('python -m ').pop()) 52 | 53 | GWSummConfigParser.OPTCRE = re.compile( 54 | r'(?P