Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • wenxuan.jia/pygwinc
  • sean-leavey/pygwinc
  • sebastian.steinlechner/pygwinc
  • nicholas.demos/pygwinc
  • chris.whittle/pygwinc
  • raymond.robie/pygwinc
  • mateusz.bawaj/pygwinc
  • anchal.gupta/pygwinc
  • 40m/pygwinc
  • evan.hall/pygwinc
  • kevin.kuns/pygwinc
  • geoffrey-lovelace/pygwinc
  • brittany.kamai/pygwinc
  • daniel-brown/pygwinc
  • lee-mcculler/pygwinc
  • jameson.rollins/pygwinc
  • gwinc/pygwinc
17 results
Show changes
Commits on Source (325)
[flake8]
ignore = E226,E741,E266,W503
max-line-length = 140
# E226, missing whitespace around arithmetic operator: quantum.py currently needs large changes to avoid this
# E741, "l" is a bad variable name: Gwinc uses "l" in a few reasonable places
# E266, Too many leading '#' for block comment: There are some reasonable instances of comments that trigger this
# W503, binary operator at start of line: This allows using parentheses to break equations
#for docs and setup.py outputs #for docs and setup.py outputs
build/ build/
tresults*/
test_results*/
# test cache
gwinc/test/cache
test/*/*.h5
gwinc.egg-info/
.eggs/
gwinc/_version.py
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
......
image: igwn/base:buster
stages: stages:
- test - dist
- deploy - test
- review
- deploy
- release
# have to specify this so that all jobs execute for all commits
# including merge requests
workflow:
rules:
- if: $CI_MERGE_REQUEST_ID
- if: $CI_COMMIT_BRANCH
- if: $CI_COMMIT_TAG
test: variables:
GIT_STRATEGY: clone
# build the docker image we will use in all the jobs, with all
# dependencies pre-installed/configured.
gwinc/base:
stage: dist
variables:
IMAGE_TAG: $CI_REGISTRY_IMAGE/$CI_JOB_NAME:$CI_COMMIT_REF_NAME
GIT_STRATEGY: none
script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
- |
cat <<EOF > Dockerfile
FROM igwn/base:bullseye
RUN apt-get update -qq
RUN apt-get -y install --no-install-recommends git python3 python3-gitlab python3-setuptools-scm python3-yaml python3-scipy python3-matplotlib python3-pypdf2 python3-h5py python3-inspiral-range python3-lalsimulation
EOF
- docker build --no-cache -t $IMAGE_TAG .
- docker push $IMAGE_TAG
# create plots for the canonical IFOs
generate_budgets:
stage: test stage: test
before_script: image: $CI_REGISTRY_IMAGE/gwinc/base:$CI_COMMIT_REF_NAME
- echo $CI_COMMIT_SHA | cut -b1-8 > gitID.txt
script: script:
- rm -rf ifo gwinc_test_report.pdf - mkdir -p ifo
- mkdir ifo - export PYTHONPATH=/inspiral_range
- apt-get update -qq - for ifo in $(python3 -c "import gwinc; print(' '.join(gwinc.IFOS))"); do
- apt-get install -y -qq git python3-yaml python3-scipy python3-matplotlib python3-ipython lalsimulation-python3 python3-pypdf2 python3-h5py - python3 -m gwinc $ifo -s ifo/$ifo.png -s ifo/$ifo.h5
- git clone https://gitlab-ci-token:ci_token@git.ligo.org/gwinc/inspiral_range.git - done
- export PYTHONPATH=inspiral_range - python3 -m gwinc.ifo -s ifo/all_compare.png
- export MPLBACKEND=agg
- python3 -m gwinc.test -r gwinc_test_report.pdf
- for ifo in aLIGO Aplus Voyager CE1 CE2; do
- python3 -m gwinc $ifo -s ifo/$ifo.png
- python3 -m gwinc $ifo -s ifo/$ifo.h5
- done
- python3 -m gwinc.ifo -s ifo/all_compare.png
after_script:
- rm gitID.txt
cache:
key: "$CI_PROJECT_NAMESPACE:$CI_PROJECT_NAME:$CI_JOB_NAME"
untracked: true
artifacts: artifacts:
when: always when: always
expire_in: 4w
paths: paths:
- ifo - ifo
- gwinc_test_report.pdf
# this is a special job intended to run only for merge requests.
# budgets are compared against those from the target branch. if the
# merge request has not yet been approved and noise changes are found,
# the job will fail. once the merge request is approved the job can
# be re-run, at which point the pipeline should succeed allowing the
# merge to be merged.
noise_change_approval:
stage: review
rules:
# - if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "master"'
- if: $CI_MERGE_REQUEST_ID
image: $CI_REGISTRY_IMAGE/gwinc/base:$CI_COMMIT_REF_NAME
script:
- export PYTHONPATH=/inspiral_range
- |
cat <<EOF > merge_info_env.py
import gitlab
project_id = $CI_MERGE_REQUEST_PROJECT_ID
mr_iid = $CI_MERGE_REQUEST_IID
# this only works for public repos, otherwise need to specify
# private_token=
gl = gitlab.Gitlab('https://git.ligo.org')
project = gl.projects.get(project_id)
mr = project.mergerequests.get(mr_iid)
approvals = mr.approvals.get()
print('TARGET_URL={}'.format(project.http_url_to_repo))
print('MR_APPROVED={}'.format(approvals.approved))
EOF
- python3 merge_info_env.py > ENV
- . ENV
- if [[ $MR_APPROVED != True ]] ; then
- echo "Approval not yet given, checking for noise changes..."
- git remote add upstream $TARGET_URL
- git remote update
- TARGET_REV=upstream/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME
- if ! python3 -m gwinc.test --git-rev $TARGET_REV -r gwinc_test_report.pdf ; then
- echo "NOISE CHANGES RELATIVE TO $TARGET_REV"
- echo "Approval required to merge this branch."
- /bin/false
- else
- echo "No noise changes detected, merge may proceed."
- fi
- else
- echo "Merge request approved, noise change accepted."
- fi
artifacts:
when: on_failure
paths:
- gwinc_test_report.pdf
expose_as: 'PDF report of noise changes relative to target branch'
# generate the html doc web pages. the "pages" job has special
# meaning, as it's "public" artifact becomes the directory served
# through gitlab static pages
pages: pages:
stage: deploy stage: deploy
dependencies: only:
- test - master
needs:
- job: generate_budgets
artifacts: true
image: $CI_REGISTRY_IMAGE/gwinc/base:$CI_COMMIT_REF_NAME
script: script:
- rm -rf public - rm -rf public
- mv ifo public - apt-get install -y -qq python3-sphinx-rtd-theme
- apt-get install -y -qq python3-sphinx-rtd-theme - cd docs
- cd docs - make html
- make html - cd ..
- cd .. - mv ./build/sphinx/html public
- mv ./build/sphinx/html/* public/ - mv ifo public/
artifacts:
when: always
paths:
- public
# make pypi release on tag creation
pypi_release:
stage: release
rules:
- if: $CI_PROJECT_NAMESPACE != "gwinc"
when: never
- if: $CI_COMMIT_TAG =~ /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/
image: python:latest
script:
- echo "pypi release for $CI_COMMIT_TAG"
- pip install twine
- python setup.py sdist bdist_wheel
- TWINE_USERNAME=__token__ TWINE_PASSWORD=${PYPI_TOKEN} twine upload dist/*
artifacts: artifacts:
paths: paths:
- public - dist/*
expire_in: 4w
only:
- master
...@@ -2,25 +2,48 @@ ...@@ -2,25 +2,48 @@
The pygwinc project welcomes your contributions. Our policy is that The pygwinc project welcomes your contributions. Our policy is that
all contributions should be peer-reviewed. To facilitate the review, all contributions should be peer-reviewed. To facilitate the review,
please do not commit your work to this repository yourself. Instead, please do not commit your work to this repository directly. Instead,
fork the repository and send a merge request. please fork the repository and create a [merge
request](https://git.ligo.org/gwinc/pygwinc/-/merge_requests/new)
against the main pygwinc master branch.
`pygwinc` comes with a test suite that will calculate all canonical When submitting code for merge, please follow good coding practice.
IFO budgets with the current state of the code, and compare them Respect the existing coding style, which for `pygwinc` is standard
against cached hdf5 traces (stored in the repo with [PEP8](https://www.python.org/dev/peps/pep-0008/) (with some
[git-lfs](https://git-lfs.github.com/) ( [see exceptions). Make individual commits as logically distinct and atomic
also](https://wiki.ligo.org/Computing/GitLFS)). Contributors should as possible, and provide a complete, descriptive log of the changes
run the tests before submitting any merge requests: (including a top summary line). Review efficiency follows code
legibility.
`pygwinc` comes with a validation command that can compare budgets
from the current code against those produced from different versions
in git (by default it compares against the current HEAD). The command
can be run with:
```shell ```shell
$ python3 -m gwinc.test $ python3 -m gwinc.test
``` ```
The test suite will be run automatically upon push to git.ligo.org as Use the '--plot' or '--report' options to produce visual comparisons
part of the continuous integration, and code that fails the CI will of the noise differences. The comparison can be done against an
not be merged, unless failures are well justified. arbitrary commit using the '--git-ref' option. Traces for referenced
commits are cached, which speeds up subsequent comparison runs
significantly.
Once you submit your merge request a special CI job will determine if
there are budgets differences between your code and the master branch.
If there are, explicit approval from reviewers will be required before
your changes can be merged (see "approving noise" below).
## For reviewers: approving noise curve changes
If your change affects the shape of a noise curve, your commit message As discussed above, merge requests that generate noise changes will
should make note of that, and provide a justification. It will also cause a pipeline failure in the `review:noise_change_approval` CI job.
be necessary to update the reference curves stored in cache, but that The job will generate a report comparing the new noise traces against
should be done in a separate commit not a part of the original merge those from master, which can be found under the 'View exposed
request, so that all reviewers can see the changes introduced in the artifacts' menu item in the pipeline report. Once you have reviewed
CI test failure report. the report and the code, and understand and accept the noise changes,
click the 'Approve' button in the MR. Once sufficient approval has
been given, `review:noise_change_approval` job can be re-run, which
should now pick up that approval has been given and allow the pipeline
to succeed. Once the pipeline succeeds the merge request can be
merged. Click the 'Merge' button to finally merge the code.
# HDF5 Schema for GWINC noise trace storage # HDF5 Schema for GWINC noise trace storage
This file describes a schemata for HDF5 storage of noise trace data This file describes a schemata for HDF5 storage of noise trace data
and plot styling GWINC noise budgets. and plot styling GWINC noise budgets. Python functions for writing
budget data to and reading budget data from this format are included
in the `gwinc.io` module.
HDF5 is a heirarchical, structured data storage format [0]. Content HDF5 is a heirarchical, structured data storage format [0]. Content
is organized into a heirarchical folder-like structure, with two is organized into a heirarchical folder-like structure, with two
...@@ -17,53 +18,22 @@ pairs. ...@@ -17,53 +18,22 @@ pairs.
Bindings are available for most platforms including Python [1] and Bindings are available for most platforms including Python [1] and
Matlab. Matlab.
[0] https://en.wikipedia.org/wiki/Hierarchical_Data_Format * [0] https://en.wikipedia.org/wiki/Hierarchical_Data_Format
[1] http://www.h5py.org/ * [1] http://www.h5py.org/
## version history
v1
- first versioned schema release
## schema ## schema
The following describes the noise budget schema. Specific strings are The following describes the noise budget schema. Specific strings are
enclosed in single quotes (''), and variables are described in enclosed in single quotes (''), and variables are described in
brackets (<>). Group objects are indicated by a closing '/' brackets (<>). Group objects are indicated by a closing '/', data
separator, data set are indicated by a closing ':' followed by a sets are indicated by a closing ':' followed by a specification of
specification of their length and type (e.g. "(N),float"), and their length and type (e.g. "(N),float"), and attributes are specified
attributes are specified in the .attrs[] dictionary format. Optional in the .attrs[] dictionary format. Optional elements are enclosed in
elements are enclosed in parentheses. parentheses.
A single trace is a length N array (with optional plot style specified
in attributes:
```
/<trace>: (N),float
(/<trace>.attrs['label'] = <label>)
(/<trace>.attrs['color] = <color>)
...
```
A budget item, i.e. a collection of noises is structured as follows:
```
/<budget>/
/'Total': (N),float
/<trace_0>: (N),float
(/<trace_1>: (N),float)
```
<!-- ``` -->
<!-- /'noises'/ -->
<!-- /*<noise_0>: (N),float -->
<!-- ... -->
<!-- /\*'references'/ -->
<!-- /*<ref_0>: (N),float -->
<!-- ... -->
<!-- ``` -->
## Top-level Budget ## top-level attributes
The following two root attributes expected: a string describing the schema, The following two root attributes expected: a string describing the schema,
and an int schema version number: and an int schema version number:
...@@ -72,33 +42,81 @@ and an int schema version number: ...@@ -72,33 +42,81 @@ and an int schema version number:
/.attrs['SCHEMA_VERSION'] = 1 /.attrs['SCHEMA_VERSION'] = 1
``` ```
Top-level attributes are generally used for specifying overall plot styling, but the The following root root attributes are defined:
following root attributes are typically defined:
``` ```
/.attrs['title'] = <experiment description string (e.g. 'H1 Strain Budget')> /.attrs['title'] = <experiment description string (e.g. 'H1 Strain Budget')>
/.attrs['date'] = <ISO-formatted string (e.g. '2015-10-24T20:30:00.000000Z')> /.attrs['date'] = <ISO-formatted string (e.g. '2015-10-24T20:30:00.000000Z')>
/.attrs['ifo'] = <IFO Struct object, YAML serialized>
```
The remaining root level attributes usually pertain to the plot style.
The budget frequency array is defined in a top level 'Freq' dataset:
```
/'Freq': (N),float
```
## version history
### v1
A single trace is a length N array (with optional plot style specified
in attributes:
```
/<trace>: (N),float
/<trace>.attrs['label'] = <label>
/<trace>.attrs['color] = <color>
...
``` ```
The budget frequency array is defined in a 'Freq' dataset: A budget item, i.e. a collection of noises is structured as follows:
``` ```
/'Freq': (N), float /<budget>/
/<budget>/Total': (N),float
/<budget>/<trace_0>: (N),float
/<budget>/...
``` ```
The budget traces are defined a traces group. The overall structure The budget traces are defined a traces group. The overall structure
looks something like this: looks something like this:
``` ```
/.attrs['SCHEMA'] = 'GWINC Noise Budget'
/.attrs['SCHEMA_VERSION'] = 1
/.attrs['title'] = <experiment description string (e.g. 'H1 Strain Budget')>
/.attrs['date'] = <ISO-formatted string (e.g. '2015-10-24T20:30:00.000000Z')>
/'Freq': (N), float
/traces/ /traces/
/'Total': (N),float /traces/'Total': (N),float
/<noise_0>: (N),float /traces/<noise_0>: (N),float
/<noise_1>: (N),float /traces/<noise_1>: (N),float
/<noise_2>/ /traces/<noise_2>/
/'Total': (N),float /traces/<noise_2>/'Total': (N),float
/<noise_3>: (N),float /traces/<noise_2>/<noise_3>: (N),float
/<noise_4>: (N),float /traces/<noise_2>/...
... /traces/...
```
### v2
Each trace is given the following structure:
```
/<trace>/
/<trace>.attrs['style'] = <YAML trace style dict>
/<trace>/'PSD': (N),float
/<trace>/budget/
/<trace>/budget/<subtrace_0>/
/<trace>/budget/<subtrace_1>/
/<trace>/budget/...
```
The overall structure is:
```
/<budget>/
/<budget>/.attrs['plot_style'] = <YAML plot style dict>
/<budget>/.attrs['style'] = <YAML "Total" trace style dict>
/<budget>/.attrs[*] = <arbitrary data>
/<budget>/'Freq': (N),float
/<budget>/'PSD': (N),float
/<budget>/budget/
/<budget>/budget/<trace_0>/...
/<budget>/budget/<trace_1>/...
/<budget>/budget/...
``` ```
...@@ -2,43 +2,55 @@ ...@@ -2,43 +2,55 @@
CI-generated plots and data for all IFOs included in pygwinc. CI-generated plots and data for all IFOs included in pygwinc.
![IFO compare](https://gwinc.docs.ligo.org/pygwinc/all_compare.png) Ranges in "Mpc" are for binary neutron stars (BNS) using the
[inspiral_range](gwinc/inspiral-range>) package.
![IFO compare](https://gwinc.docs.ligo.org/pygwinc/ifo/all_compare.png)
## aLIGO ## aLIGO
* [ifo.yaml](gwinc/ifo/aLIGO/ifo.yaml) * [ifo.yaml](gwinc/ifo/aLIGO/ifo.yaml)
* [aLIGO.h5](https://gwinc.docs.ligo.org/pygwinc/aLIGO.h5) * [aLIGO.h5](https://gwinc.docs.ligo.org/pygwinc/ifo/aLIGO.h5)
![aLIGO](https://gwinc.docs.ligo.org/pygwinc/aLIGO.png) ![aLIGO](https://gwinc.docs.ligo.org/pygwinc/ifo/aLIGO.png)
## A+ ## A+
* [ifo.yaml](gwinc/ifo/Aplus/ifo.yaml) * [ifo.yaml](gwinc/ifo/Aplus/ifo.yaml)
* [Aplus.h5](https://gwinc.docs.ligo.org/pygwinc/Aplus.h5) * [Aplus.h5](https://gwinc.docs.ligo.org/pygwinc/ifo/Aplus.h5)
![Aplus](https://gwinc.docs.ligo.org/pygwinc/Aplus.png) ![Aplus](https://gwinc.docs.ligo.org/pygwinc/ifo/Aplus.png)
## Voyager ## Voyager
* [ifo.yaml](gwinc/ifo/Voyager/ifo.yaml) * [ifo.yaml](gwinc/ifo/Voyager/ifo.yaml)
* [Voyager.h5](https://gwinc.docs.ligo.org/pygwinc/Voyager.h5) * [Voyager.h5](https://gwinc.docs.ligo.org/pygwinc/ifo/Voyager.h5)
![Voyager](https://gwinc.docs.ligo.org/pygwinc/Voyager.png) ![Voyager](https://gwinc.docs.ligo.org/pygwinc/ifo/Voyager.png)
## Cosmic Explorer 1 ## Cosmic Explorer 1
* [ifo.yaml](gwinc/ifo/CE1/ifo.yaml) * [ifo.yaml](gwinc/ifo/CE1/ifo.yaml)
* [CE1.h5](https://gwinc.docs.ligo.org/pygwinc/CE1.h5) * [CE1.h5](https://gwinc.docs.ligo.org/pygwinc/ifo/CE1.h5)
![CE1](https://gwinc.docs.ligo.org/pygwinc/ifo/CE1.png)
## Cosmic Explorer 2 (Silica)
* [ifo.yaml](gwinc/ifo/CE2silica/ifo.yaml)
* [CE2silica.h5](https://gwinc.docs.ligo.org/pygwinc/ifo/CE2silica.h5)
![CE1](https://gwinc.docs.ligo.org/pygwinc/CE1.png) ![CE2 (Silica)](https://gwinc.docs.ligo.org/pygwinc/ifo/CE2silica.png)
## Cosmic Explorer 2 ## Cosmic Explorer 2 (Silicon)
* [ifo.yaml](gwinc/ifo/CE2/ifo.yaml) * [ifo.yaml](gwinc/ifo/CE2silicon/ifo.yaml)
* [CE2.h5](https://gwinc.docs.ligo.org/pygwinc/CE2.h5) * [CE2silicon.h5](https://gwinc.docs.ligo.org/pygwinc/ifo/CE2silicon.h5)
![CE2](https://gwinc.docs.ligo.org/pygwinc/CE2.png) ![CE2 (Silicon)](https://gwinc.docs.ligo.org/pygwinc/ifo/CE2silicon.png)
include CONTRIBUTIONS.md exclude IFO.md
include setup.cfg global-exclude *.git*
include tox.ini prune docs
prune matlab
recursive-include gwinc *.yaml
recursive-include matlab *.m
recursive-include docs *
...@@ -2,12 +2,14 @@ ...@@ -2,12 +2,14 @@
# Python Gravitational Wave Interferometer Noise Calculator # Python Gravitational Wave Interferometer Noise Calculator
![gwinc](https://gwinc.docs.ligo.org/pygwinc/aLIGO.png) [![aLIGO](https://gwinc.docs.ligo.org/pygwinc/ifo/aLIGO.png "Canonical
IFOs")](IFO.md)
`pygwinc` is a multi-faceted tool for processing and plotting noise `pygwinc` is a multi-faceted tool for processing and plotting noise
budgets for ground-based gravitational wave detectors. It's primary budgets for ground-based gravitational wave detectors. It's primary
feature is a collection of mostly analytic noise calculation functions feature is a collection of mostly analytic [noise calculation
for various sources of noise affecting detectors (`gwinc.noise`): functions](#noise-functions) for various sources of noise affecting
detectors (`gwinc.noise`):
* quantum noise * quantum noise
* mirror coating thermal noise * mirror coating thermal noise
...@@ -17,30 +19,29 @@ for various sources of noise affecting detectors (`gwinc.noise`): ...@@ -17,30 +19,29 @@ for various sources of noise affecting detectors (`gwinc.noise`):
* Newtonian/gravity-gradient noise * Newtonian/gravity-gradient noise
* residual gas noise * residual gas noise
See [noise functions](#noise-functions) below. `pygwinc` is also a generalized noise budgeting tool (`gwinc.nb`) that
allows users to create arbitrary noise budgets (for any experiment,
`pygwinc` also includes a generalized noise budgeting tool not just ground-based GW detectors) using measured or analytically
(`gwinc.nb`) that allows users to create arbitrary noise budgets (for calculated data. See the [budget interface](#Budget-interface)
any experiment, not just ground-based GW detectors) using measured or section below.
analytically calculated data. See the [budget
interface](#Budget-interface) section below.
`pygwinc` includes canonical budgets for various well-known current `pygwinc` includes canonical budgets for various well-known current
and future detectors (`gwinc.ifo`): and future GW detectors (`gwinc.ifo`):
* [aLIGO](https://gwinc.docs.ligo.org/pygwinc/aLIGO.png) * [aLIGO](https://gwinc.docs.ligo.org/pygwinc/ifo/aLIGO.png)
* [A+](https://gwinc.docs.ligo.org/pygwinc/Aplus.png) * [A+](https://gwinc.docs.ligo.org/pygwinc/ifo/Aplus.png)
* [Voyager](https://gwinc.docs.ligo.org/pygwinc/Voyager.png) * [Voyager](https://gwinc.docs.ligo.org/pygwinc/ifo/Voyager.png)
* [Cosmic Explorer 1](https://gwinc.docs.ligo.org/pygwinc/CE1.png) * [Cosmic Explorer 1](https://gwinc.docs.ligo.org/pygwinc/ifo/CE1.png)
* [Cosmic Explorer 2](https://gwinc.docs.ligo.org/pygwinc/CE2.png) * [Cosmic Explorer 2 (Silica)](https://gwinc.docs.ligo.org/pygwinc/ifo/CE2silica.png)
* [Cosmic Explorer 2 (Silicon)](https://gwinc.docs.ligo.org/pygwinc/ifo/CE2silicon.png)
See [IFO.md](IFO.md) for the latest CI-generated plots and hdf5 cached See [IFO.md](IFO.md) for the latest CI-generated plots and hdf5 cached
data. data.
The [`inspiral_range`](https://git.ligo.org/gwinc/inspiral-range) The [`inspiral_range`](https://git.ligo.org/gwinc/inspiral-range)
package can be used to calculate various common "inspiral range" package can be used to calculate various common "inspiral range"
figures of merit for gravitational wave detector budgets. See figures of merit for gravitational wave detector budgets. See the
[figures of merit](#figures-of-merit) section below. [inspiral range](#inspiral-range) section below.
## usage ## usage
...@@ -48,74 +49,129 @@ figures of merit for gravitational wave detector budgets. See ...@@ -48,74 +49,129 @@ figures of merit for gravitational wave detector budgets. See
### command line interface ### command line interface
`pygwinc` provides a command line interface that can be used to `pygwinc` provides a command line interface that can be used to
calculate and plot noise budgets for generic noise budgets or the calculate and plot the various canonical IFO noise budgets described
various canonical IFOs described above, save/plot hdf5 trace data, and above. For most distributions this should be available via
dump budget IFO parameters: `gwinc` at the command line, or `python3 -m gwinc` otherwise:
```shell
$ gwinc aLIGO
```
Or [custom budgets](#budget-interface) may also be processed by providing
the path to the budget module/package:
```shell
$ gwinc path/to/mybudget
```
Budget plots can be saved in various formats (.png, .svg, .pdf):
```shell ```shell
$ python3 -m gwinc aLIGO $ gwinc --save aLIGO.png aLIGO
```
Or trace data can be saved to an
[HDF5](https://en.wikipedia.org/wiki/Hierarchical_Data_Format) file:
```shell
$ gwinc --save aLIGO.hdf5 aLIGO
```
Trace HDF5 files can also be plotted directly:
```shell
$ gwinc aLIGO.hdf5
``` ```
You can play with IFO parameters and see the effects on the budget by The `--range` option can be used to include the values of various
dumping the pre-defined parameters to a [YAML-formatted parameter inspiral ranges for the overall noise in the output.
file](#yaml-parameter-files), editing the parameter file, and
re-calculating the noise budget: IFO parameters can be manipulated from the command line with the
`--ifo` option:
```shell
$ gwinc aLIGO --ifo Optics.SRM.Tunephase=3.14
```
You can also dump the IFO parameters to a [YAML-formatted parameter
file](#yaml-parameter-files):
```shell ```shell
$ python3 -m gwinc --yaml aLIGO > my_aLIGO.yaml $ gwinc --yaml aLIGO > my_aLIGO.yaml
$ edit my_aLIGO.yaml $ edit my_aLIGO.yaml
$ python3 -m gwinc -d my_aLIGO.yaml aLIGO $ gwinc -d my_aLIGO.yaml aLIGO
aLIGO my_aLIGO.yaml aLIGO my_aLIGO.yaml
Materials.Coating.Philown 5e-05 3e-05 Materials.Coating.Philown 5e-05 3e-05
$ python3 -m gwinc my_aLIGO.yaml $ gwinc my_aLIGO.yaml
``` ```
Stand-alone YAML files assume the nominal ['aLIGO' budget
Stand-alone YAML files will always assume the nominal ['aLIGO' budget
description](gwinc/ifo/aLIGO). description](gwinc/ifo/aLIGO).
[Custom budgets](#budget-interface) may also be processed by providing The command line interface also includes an "interactive" mode which
the path to the budget module/package: provides an [IPython](https://ipython.org/) shell for interacting with
a processed budget:
```shell ```shell
$ python3 -m gwinc path/to/mybudget $ gwinc -i Aplus
GWINC interactive shell
The 'ifo' Struct and 'trace' data are available for inspection.
Use the 'whos' command to view the workspace.
You may interact with the plot using the 'plt' functions, e.g.:
In [.]: plt.title("My Special Budget")
In [.]: plt.savefig("mybudget.pdf")
In [1]:
``` ```
See command help for more info: See command help for more info:
```shell ```shell
$ python3 -m gwinc -h $ gwinc --help
``` ```
### python library ### library interface
For custom plotting, parameter optimization, etc. all functionality can be For custom plotting, parameter optimization, etc. all functionality can be
accessed directly through the `gwinc` library interface: accessed directly through the `gwinc` library interface:
```python ```python
>>> import gwinc >>> import gwinc
>>> budget = gwinc.load_budget('aLIGO')
>>> trace = budget.run()
>>> fig = trace.plot()
>>> fig.show()
```
A default frequency array is used, but alternative frequencies can be
provided to `load_budget()` either in the form of a numpy array:
```python
>>> import numpy as np >>> import numpy as np
>>> freq = np.logspace(1, 3, 1000) >>> freq = np.logspace(1, 3, 1000)
>>> Budget = gwinc.load_budget('aLIGO') >>> budget = gwinc.load_budget('aLIGO', freq=freq)
>>> traces = Budget(freq).run() ```
>>> fig = gwinc.plot_noise(freq, traces) or frequency specification string ('FLO:[NPOINTS:]FHI'):
>>> fig.show() ```python
>>> budget = gwinc.load_budget('aLIGO', freq='10:1000:1000')
``` ```
The `load_budget()` function takes most of the same inputs as the The `load_budget()` function takes most of the same inputs as the
command line interface (e.g. IFO names, budget module paths, YAML command line interface (e.g. IFO names, budget module paths, YAML
parameter files), and returns the un-instantiated `Budget` class parameter files), and returns the instantiated `Budget` object defined
defined in the specified budget module (see [budget in the specified budget module (see [budget
interface](#budget-interface) below). interface](#budget-interface) below). The budget `ifo` `gwinc.Struct`
is available in the `budget.ifo` attribute.
The budget `run()` method is a convenience method that calculates all
budget noises and the noise total and returns a (possibly) nested The budget `run()` method calculates all budget noises and the noise
dictionary of a noise data, in the form of a `(data, style)` tuple total and returns a `BudgetTrace` object with `freq`, `psd`, and `asd`
where 'data' is the PSD data and 'style' is a plot style dictionary properties. The budget sub-traces are available through a dictionary
for the trace. The dictionary will be nested if the budget includes (`trace['Quantum']`) interface and via attributes
any sub-budgets. (`trace.Quantum`).
The budget `freq` and `ifo` attributes can be updated at run time by
passing them as keyword arguments to the `run()` method:
```python
>>> budget = load_budget('aLIGO')
>>> freq = np.logspace(1, 3, 1000)
>>> ifo = Struct.from_file('/path/to/ifo_alt.yaml')
>>> trace = budget.run(freq=freq, ifo=ifo)
```
## noise functions ## noise functions
`pygwinc` noise functions are available in the `gwinc.noise` package. The `pygwinc` analytical noise functions are available in the
This package includes multiple sub-modules for the different types of `gwinc.noise` package. This package includes multiple sub-modules for
noises, e.g. `suspensionthermal`, `coatingthermal`, `quantum`, etc.) the different types of noises, e.g. `suspensionthermal`,
`coatingthermal`, `quantum`, etc.)
The various noise functions need many different parameters to The various noise functions need many different parameters to
calculate their noise outputs. Many parameters are expected to be in calculate their noise outputs. Many parameters are expected to be in
...@@ -138,7 +194,7 @@ def coating_brownian(f, materials, wavelength, wBeam, dOpt): ...@@ -138,7 +194,7 @@ def coating_brownian(f, materials, wavelength, wBeam, dOpt):
``` ```
### `gwinc.Struct` objects ## `gwinc.Struct` objects
`pygwinc` provides a `Struct` class that can hold parameters in `pygwinc` provides a `Struct` class that can hold parameters in
attributes and additionally acts like a dictionary, for passing to the attributes and additionally acts like a dictionary, for passing to the
...@@ -209,8 +265,12 @@ are: ...@@ -209,8 +265,12 @@ are:
* `update(**kwargs)`: update data/attributes * `update(**kwargs)`: update data/attributes
* `calc()`: return final data array * `calc()`: return final data array
See the built-in documentation for more info (e.g. `pydoc3 Generally these methods are not called directly. Instead, the `Noise`
gwinc.nb.BudgetItem`) and `Budget` classes include a `run` method that smartly executes them
in sequence and returns a `BudgetTrace` object for the budget.
See the built-in `BudgetItem` documentation for more info
(e.g. `pydoc3 gwinc.nb.BudgetItem`)
### budget module definition ### budget module definition
...@@ -290,11 +350,10 @@ The `style` attributes of the various `Noise` classes define plot ...@@ -290,11 +350,10 @@ The `style` attributes of the various `Noise` classes define plot
style for the noise. style for the noise.
This budget can be loaded with the `gwinc.load_budget()` function, and This budget can be loaded with the `gwinc.load_budget()` function, and
processed with the `Budget.run()` method: processed as usual with the `Budget.run()` method:
```python ```python
Budget = load_budget('/path/to/MyBudget') budget = load_budget('/path/to/MyBudget', freq)
budget = Budget(freq) trace = budget.run()
traces = budget.run()
``` ```
Other than the necessary `freq` initialization argument that defines Other than the necessary `freq` initialization argument that defines
...@@ -310,28 +369,64 @@ suspension Struct is extracted from the `self.ifo` Struct at ...@@ -310,28 +369,64 @@ suspension Struct is extracted from the `self.ifo` Struct at
If a budget module defined as a package includes an `ifo.yaml` If a budget module defined as a package includes an `ifo.yaml`
[parameter file](#parameter-files) in the package directory, the [parameter file](#parameter-files) in the package directory, the
`load_budget()` function will automatically load the YAML data into a `load_budget()` function will automatically load the YAML data into an
`gwinc.Struct` and include it as an `Budget.ifo` attribute in the `ifo` `gwinc.Struct` and assign it to the `budget.ifo` attribute.
returned `Budget` class. This would provide the `self.ifo` needed in
the `SuspensionThermal` Noise class above and is therefore a
convenient way to provide parameter structures in budget packages.
Otherwise it would need to be created/loaded in some other way and
passed to the budget at instantiation, e.g.:
```python
Budget = load_budget('/path/to/MyBudget')
ifo = Struct.from_file('/path/to/MyBudget.ifo')
budget = Budget(freq, ifo=ifo)
traces = budget.run()
```
The IFOs included in `gwinc.ifo` provide examples of the use of the The IFOs included in `gwinc.ifo` provide examples of the use of the
budget interface: budget interface (e.g. [gwinc.ifo.aLIGO](gwinc/ifo/aLIGO)).
* [aLIGO](gwinc/ifo/aLIGO) ### the "precomp" decorator
* [Aplus](gwinc/ifo/Aplus)
* [Voyager](gwinc/ifo/Voyager) The `BudgetItem` supports "precomp" functions that can be used to
* [CE1](master/gwinc/ifo/CE1) calculate common derived values needed in multiple `BudgetItems`.
* [CE2](master/gwinc/ifo/CE2) They are specified using the `nb.precomp` decorator applied to the
`BudgetItem.calc()` method. These functions are executed during the
`update()` method call, supplied with the budget `freq` and `ifo`
attributes as input arguments. The execution state of the precomp
functions is cached at the Budget level, to prevent needlessly
re-calculating them multiple times. For example:
```python
from gwinc import nb
def precomp_foo(freq, ifo):
pc = Struct()
...
return pc
def precomp_bar(freq, ifo):
pc = Struct()
...
return pc
class Noise0(nb.Noise):
@nb.precomp(foo=precomp_foo)
def calc(self, foo):
...
class Noise1(nb.Noise):
@nb.precomp(foo=precomp_foo)
@nb.precomp(bar=precomp_bar)
def calc(self, foo, bar):
...
class MyBudget(nb.Budget):
noises = [
Noise0,
Noise1,
]
```
When `MyBudget.run()` is called, all the `precomp` functions will be
executed, e.g.:
```python
precomp_foo(self.freq, self.ifo)
precomp_bar(self.freq, self.ifo)
```
But the `precomp_foo` function will only be calculated once even
though it's specified as needed by both `Noise0` and `Noise1`.
### extracting single noise terms ### extracting single noise terms
...@@ -341,28 +436,27 @@ interface. The most straightforward way is to run the full budget, ...@@ -341,28 +436,27 @@ interface. The most straightforward way is to run the full budget,
and extract the noise data the output traces dictionary: and extract the noise data the output traces dictionary:
```python ```python
Budget = load_budget('/path/to/MyBudget') budget = load_budget('/path/to/MyBudget', freq)
budget = Budget(freq) trace = budget.run()
traces = budget.calc_traces() quantum_trace = trace['Quantum']
data, plot_style = traces['QuantumVacuum']
``` ```
You can also calculate the final calibrated output noise for just a You can also calculate the final calibrated output noise for just a
single term using the Budget `calc_noise()` method: single term using the Budget `calc_noise()` method:
```python ```python
data = budget.calc_noise('QuantumVacuum') data = budget.calc_noise('Quantum')
``` ```
You can also calculate a noise at it's source, without applying any You can also calculate a noise at it's source, without applying any
calibrations, by using the Budget `__getitem__` interface to extract calibrations, by using the Budget `__getitem__` interface to extract
the specific Noise BudgetItem for the noise you're interested in, and the specific Noise BudgetItem for the noise you're interested in, and
running it's `calc()` method directly: running it's `calc_trace()` method directly:
```python ```python
data = budget['QuantumVacuum'].calc() data = budget['Quantum'].calc_trace()
``` ```
# figures of merit # inspiral range
The [`inspiral_range`](https://git.ligo.org/gwinc/inspiral-range) The [`inspiral_range`](https://git.ligo.org/gwinc/inspiral-range)
package can be used to calculate various common "inspiral range" package can be used to calculate various common "inspiral range"
...@@ -375,18 +469,15 @@ import inspiral_range ...@@ -375,18 +469,15 @@ import inspiral_range
import numpy as np import numpy as np
freq = np.logspace(1, 3, 1000) freq = np.logspace(1, 3, 1000)
Budget = gwinc.load_budget('Aplus') budget = gwinc.load_budget('Aplus', freq)
traces = Budget(freq).run() trace = budget.run()
range = inspiral_range.range( range = inspiral_range.range(
freq, traces['Total'][0], freq, trace.psd,
m1=30, m2=30, m1=30, m2=30,
) )
``` ```
Note you need to extract the zeroth element of the `traces['Total']`
tuple, which is the actual PSD data.
See the [`inspiral_range`](https://git.ligo.org/gwinc/inspiral-range) See the [`inspiral_range`](https://git.ligo.org/gwinc/inspiral-range)
package for more details. package for more details.
......
"""
Requires pytest to import
"""
import os
from os import path
import os
from shutil import rmtree
import contextlib
import pytest
_options_added = False
def pytest_addoption(parser):
global _options_added
#this check fixes issues if this gets run multiple times from sub conftest.py's
if _options_added:
return
else:
_options_added = True
parser.addoption(
"--plot",
action="store_true",
dest = 'plot',
help = "Have tests update plots (it is slow)",
)
parser.addoption(
"--do-stresstest",
action = "store_true",
help = "Run slow repeated stress tests"
)
parser.addoption(
"--no-preclear",
action="store_true",
default=False,
dest='no_preclear',
help="Do not preclear tpaths",
)
parser.addoption(
"--generate",
action="store_true",
default=False,
dest="generate",
help="Generate test data",
)
@pytest.fixture
def plot(request):
return request.config.getvalue('--plot')
return request.config.option.plot
@pytest.fixture
def tpath_preclear(request):
"""
Fixture that indicates that the test path should be cleared automatically
before running each test. This cleans up the test data.
"""
tpath_raw = tpath_raw_make(request)
no_preclear = request.config.getvalue('--no-preclear')
if not no_preclear:
rmtree(tpath_raw, ignore_errors = True)
@pytest.fixture
def tpath(request):
"""
Fixture that takes the value of the special test-specific folder for test
run data and plots. Usually the <folder of the test>/test_results/test_name/
"""
tpath_raw = tpath_raw_make(request)
os.makedirs(tpath_raw, exist_ok = True)
os.utime(tpath_raw, None)
return tpath_raw
@pytest.fixture
def tpath_join(request):
"""
Fixture that joins subpaths to the value of the special test-specific folder for test
run data and plots. Usually the <folder of the test>/test_results/test_name/.
This function should be use like test_thing.save(tpath_join('output_file.png'))
"""
tpath_raw = tpath_raw_make(request)
first_call = True
def tpath_joiner(*subpath):
nonlocal first_call
if first_call:
os.makedirs(tpath_raw, exist_ok = True)
os.utime(tpath_raw, None)
first_call = False
return path.join(tpath_raw, *subpath)
return tpath_joiner
@pytest.fixture
def fpath(request):
"""
py.test fixture that returns the folder path of the test being run. Useful
for accessing data files.
"""
return fpath_raw_make(request)
@pytest.fixture
def fpath_join(request):
"""
py.test fixture that runs :code:`os.path.join(path, *arguments)` to merge subpaths
with the folder path of the current test being run. Useful for referring to
data files.
"""
def join_func(*path):
return os.path.join(fpath_raw_make(request), *path)
return join_func
@pytest.fixture
def closefigs():
import matplotlib.pyplot as plt
yield
plt.close('all')
@pytest.fixture
def test_trigger():
"""
This fixture provides a contextmanager that causes a function to call
if an AssertionError is raised. It will also call if any of its argument,
or keyword arguments is true. This allows you to conveniently force
calling using other flags or fixtures.
The primary usage of this is to plot outputs only on test failures, while also
allowing plotting to happen using the plot fixture and pytest cmdline argument
"""
run_store = []
@contextlib.contextmanager
def fail(call, **kwargs):
run_store.append(call)
def call(did_fail):
do_call = did_fail
for k, v in kwargs.items():
if v:
do_call = True
break
if do_call:
for call in run_store:
call(fail = did_fail, **kwargs)
run_store.clear()
try:
yield
except AssertionError:
call(True)
raise
else:
call(False)
return
return fail
@pytest.fixture()
def ic():
"""
Fixture to provide icecream imports without requiring that the package exist
"""
try:
from icecream import ic
return ic
except ImportError:
pass
try:
from IPython.lib.pretty import pprint
return pprint
except ImportError:
from pprint import pprint
return pprint
#these are used with the pprint fixture
try:
import icecream
except ImportError:
icecream = None
pass
try:
from IPython.lib.pretty import pprint, pretty
pformat = pretty
except ImportError:
from pprint import pprint, pformat
@pytest.fixture
def pprint(request, tpath_join):
"""
This is a fixture providing a wrapper function for pretty printing. It uses
the icecream module for pretty printing, falling back to ipythons pretty
printer if needed, then to the python build in pretty printing module.
Along with printing to stdout, this function prints into the tpath_folder to
save all output into output.txt.
"""
fname = tpath_join('output.txt')
#pushes past the dot
print('---------------:{}:--------------'.format(request.node.name))
with open(fname, 'w') as F:
def pprint(*args, F = F, pretty = True, **kwargs):
outs = []
if pretty:
for arg in args:
outs.append(
pformat(arg)
)
else:
outs = args
if F is not None:
print(*outs, file = F)
if icecream is not None:
icecream.DEFAULT_OUTPUT_FUNCTION(' '.join(outs), **kwargs)
else:
print(*outs, **kwargs)
yield pprint
def tpath_raw_make(request):
if isinstance(request.node, pytest.Function):
return relfile_test(request.node.function.__code__.co_filename, request, 'test_results')
raise RuntimeError("TPath currently only works for functions")
def fpath_raw_make(request):
if isinstance(request.node, pytest.Function):
return os.path.split(request.node.function.__code__.co_filename)[0]
raise RuntimeError("TPath currently only works for functions")
def relfile(_file_, *args, fname = None):
fpath = path.split(_file_)[0]
post = path.join(*args)
fpath = path.join(fpath, post)
#os.makedirs(fpath, exist_ok = True)
#os.utime(fpath, None)
if fname is None:
return fpath
else:
return path.join(fpath, fname)
def relfile_test(_file_, request, pre = None, post = None, fname = None):
"""
Generates a folder specific to py.test function
(provided by using the "request" fixture in the test's arguments)
"""
if isinstance(pre, (list, tuple)):
pre = path.join(pre)
testname = request.node.name
if pre is not None:
testname = path.join(pre, testname)
if isinstance(post, (list, tuple)):
post = path.join(post)
if post is not None:
return relfile(_file_, testname, post, fname = fname)
else:
return relfile(_file_, testname, fname = fname)
@pytest.fixture
def compare_noise(pprint):
"""
Fixture to compare two sets of traces
A list of noises passed, failed, and skipped are printed. Comparisons are
skipped if the psd's are sufficiently small (controlled by psd_tol) indicating
that the noise is essentially zero or if a trace is missing.
An assertion error is raised if any noises fail.
"""
import numpy as np
def compare(traces, ref_traces, psd_tol=1e-52):
passed = []
failed = []
skipped = []
if ref_traces.budget:
for ref_trace in ref_traces:
if np.all(ref_trace.psd < psd_tol):
skipped.append(ref_trace.name)
continue
try:
trace = traces[ref_trace.name]
except KeyError:
skipped.append(ref_trace.name)
continue
if np.allclose(trace.psd, ref_trace.psd, atol=0):
passed.append(trace.name)
else:
failed.append(trace.name)
else:
if np.allclose(ref_traces.psd, traces.psd, atol=0):
passed.append(traces.name)
else:
failed.append(traces.name)
pprint('Noises failed:')
pprint(40 * '-')
for noise in failed:
pprint(noise)
pprint(40 * '+')
pprint('Noises passed:')
pprint(40 * '-')
for noise in passed:
pprint(noise)
pprint(40 * '+')
pprint('Noises skipped:')
pprint(40 * '-')
for noise in skipped:
pprint(noise)
assert len(failed) == 0
return compare
def pytest_collection_modifyitems(config, items):
"""
Modifies tests to be selectively skipped with command line options
https://docs.pytest.org/en/latest/example/simple.html#control-skipping-of-tests-according-to-command-line-option
"""
# run tests marked as generate if and only if --generate is given
# skip all others in this case
if config.getoption('--generate'):
skip = pytest.mark.skip(
reason='only running tests that generate data')
for item in items:
if 'generate' not in item.keywords:
item.add_marker(skip)
else:
skip = pytest.mark.skip(
reason='generates test data: needs --generate to run')
for item in items:
if 'generate' in item.keywords:
item.add_marker(skip)
...@@ -41,7 +41,7 @@ clean: ...@@ -41,7 +41,7 @@ clean:
-rm -rf $(BUILDDIR)/* -rm -rf $(BUILDDIR)/*
livehtml: livehtml:
sphinx-autobuild -i .#* -i *.pyc -i .*.swp -i .*.swo -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html sphinx-autobuild -z ../gwinc -i '*.#*' -i '*.pyc' -i '*.swp' -i '*.swo' -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
html: html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
......
.wy-nav-content {
max-width: 1200px !important;
}
div.document {
width: auto;
margin: 30px auto 0 auto;
}
div.body{
max-width: 95%;
margin: 50px auto 0 auto;
}
API
################################################################################
.. _API:
.. py:module:: gwinc
...@@ -17,9 +17,15 @@ ...@@ -17,9 +17,15 @@
# add these directories to sys.path here. If the directory is relative to the # add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here. # documentation root, use os.path.abspath to make it absolute, like shown here.
# #
# import os import os
# import sys import sys
# sys.path.insert(0, os.path.abspath('.')) #add the previous folder to the top of path, so that conftest.py can be found
#dirty but does the trick
sys.path.insert(0, os.path.abspath('..'))
#gwinc must be importable to build the docs properly anyway, using apidoc, so
#import it now for the __version__ parameter
import gwinc # noqa
# -- General configuration ------------------------------------------------ # -- General configuration ------------------------------------------------
...@@ -33,11 +39,12 @@ ...@@ -33,11 +39,12 @@
# ones. # ones.
extensions = [ extensions = [
'sphinx.ext.autodoc', 'sphinx.ext.autodoc',
'sphinx.ext.todo', #'sphinx.ext.todo',
'sphinx.ext.coverage', #'sphinx.ext.coverage',
'sphinx.ext.mathjax', 'sphinx.ext.mathjax',
'sphinx.ext.githubpages', #'sphinx.ext.githubpages',
'sphinx.ext.napoleon', 'sphinx.ext.napoleon',
'sphinx.ext.viewcode',
] ]
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
...@@ -54,7 +61,7 @@ master_doc = 'index' ...@@ -54,7 +61,7 @@ master_doc = 'index'
# General information about the project. # General information about the project.
project = 'GWINC' project = 'GWINC'
copyright = '2018, LIGO Laboratory' copyright = '2021, LIGO Laboratory'
author = 'LIGO Laboratory' author = 'LIGO Laboratory'
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
...@@ -62,7 +69,7 @@ author = 'LIGO Laboratory' ...@@ -62,7 +69,7 @@ author = 'LIGO Laboratory'
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
version = '0.0.0' version = gwinc.__version__
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.
...@@ -74,7 +81,7 @@ language = None ...@@ -74,7 +81,7 @@ language = None
# List of patterns, relative to source directory, that match files and # List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files. # directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path # This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', '**.ipynb_checkpoints'] exclude_patterns = ['_build', ]
# The name of the Pygments (syntax highlighting) style to use. # The name of the Pygments (syntax highlighting) style to use.
#pygments_style = 'sphinx' #pygments_style = 'sphinx'
...@@ -84,9 +91,10 @@ pygments_style = 'default' ...@@ -84,9 +91,10 @@ pygments_style = 'default'
# If true, `todo` and `todoList` produce output, else they produce nothing. # If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True todo_include_todos = True
# Autodoc settings
autodoc_default_flags = ["members", "undoc-members"]
# -- Options for sourcelinks # -- Options for sourcelinks
srclink_project = 'https://git.ligo.org/gwinc/pygwinc' srclink_project = 'https://git.ligo.org/gwinc/pygwinc'
srclink_src_path = 'gwinc' srclink_src_path = 'gwinc'
srclink_branch = 'master' srclink_branch = 'master'
...@@ -96,24 +104,41 @@ srclink_branch = 'master' ...@@ -96,24 +104,41 @@ srclink_branch = 'master'
#useful for downloading the ipynb files #useful for downloading the ipynb files
html_sourcelink_suffix = '' html_sourcelink_suffix = ''
html_title = 'GWINC docs'
html_short_title = 'GWINC docs'
# The theme to use for HTML and HTML Help pages. See the documentation for # The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes. # a list of builtin themes.
# #
import sphinx_rtd_theme #import sphinx_rtd_theme
html_theme = "sphinx_rtd_theme" #html_theme = "sphinx_rtd_theme"
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] #html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
#import jupyter_sphinx_theme #import jupyter_sphinx_theme
#html_theme = "jupyter_sphinx_theme" #html_theme = "jupyter_sphinx_theme"
#html_theme_path = [jupyter_sphinx_theme.get_html_theme_path()] #html_theme_path = [jupyter_sphinx_theme.get_html_theme_path()]
#html_theme = "alabaster" html_theme = "alabaster"
# Theme options are theme-specific and customize the look and feel of a theme # Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the # further. For a list of options available for each theme, see the
# documentation. # documentation.
# #
# html_theme_options = {} html_theme_options = dict(
description = "Gravitational Wave Interferometer Noise Calculator, to determine the fundamental limiting noises in interferometer designs",
extra_nav_links = {
'repository' : 'https://git.ligo.org/gwinc/pygwinc'
},
show_powered_by = False,
show_related = True,
#page_width = 'auto',
)
napoleon_type_aliases = {
#"CustomType": "mypackage.CustomType",
#"dict-like": ":term:`dict-like <mapping>`",
}
# Add any paths that contain custom static files (such as style sheets) here, # Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files, # relative to this directory. They are copied after the builtin static files,
...@@ -125,118 +150,24 @@ html_static_path = ['_static'] ...@@ -125,118 +150,24 @@ html_static_path = ['_static']
# #
# This is required for the alabaster theme # This is required for the alabaster theme
# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
#html_sidebars = {
# '**': [
# 'about.html',
# 'navigation.html',
# 'relations.html', # needs 'show_related': True theme option to display
# 'searchbox.html',
# ''
# #'donate.html',
# ]
#}
# Custom sidebar templates, maps document names to template names.
html_sidebars = { html_sidebars = {
'**': [ '**': [
'localtoc.html', 'about.html',
#'globaltoc.html',
'navigation.html', 'navigation.html',
'relations.html', 'relations.html',
'searchbox.html', 'searchbox.html',
'srclinks.html', ]
],
'index': [
'globaltoc.html',
'navigation.html',
'relations.html',
'searchbox.html',
'srclinks.html',
],
} }
# -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder. # Output file base name for HTML help builder.
htmlhelp_basename = 'GWINC' htmlhelp_basename = 'GWINC'
html_logo = 'logo/LIGO_F0900035-v1.jpg'
# -- Options for LaTeX output --------------------------------------------- def setup(app):
app.add_stylesheet('my_theme.css')
latex_elements = { #updates the coloring
# The paper size ('letterpaper' or 'a4paper'). #app.add_stylesheet('pygments_adjust.css')
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(
master_doc,
'GWINC.tex',
'GWINC Documentation',
author,
'manual'
),
]
#http://www.sphinx-doc.org/en/master/usage/configuration.html#latex-options
#it appears that the LIGO docclass doesn't play well with all of the sphinx commands. Giving up for now
#latex_docclass = {
# 'manual' : 'ligodoc_v3',
#}
#latex_additional_files = [
# 'ligodoc_v3.tex'
#]
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(
master_doc,
'GWINC',
'GWINC Documentation',
[author],
1
)
]
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(
master_doc,
'GWINC',
'GWINC Documentation',
author,
'GWINC',
'One line description of project.',
'Miscellaneous'
),
]
#def setup(app):
# app.add_stylesheet('my_theme.css')
# app.add_stylesheet('pygments_adjust.css')
Development
################################################################################
.. _development:
Testing with py.test
================================================================================
.. py:module:: conftest
py.test fixtures and setup
--------------------------------------------------------------------------------
.. autofunction:: plot
.. autofunction:: tpath_preclear
.. autofunction:: tpath
.. autofunction:: tpath_join
.. autofunction:: fpath
.. autofunction:: fpath_join
.. autofunction:: closefigs
.. autofunction:: test_trigger
.. autofunction:: ic
.. autofunction:: pprint
py.test setup hooks
--------------------------------------------------------------------------------
.. autofunction:: pytest_addoption
py.test helper functions
--------------------------------------------------------------------------------
.. autofunction:: tpath_raw_make
.. autofunction:: fpath_raw_make
.. autofunction:: relfile
.. autofunction:: relfile_test
Git setup for development
================================================================================
PyPI Releases
================================================================================
versioning
--------------------------------------------------------------------------------
...@@ -30,6 +30,7 @@ Contents ...@@ -30,6 +30,7 @@ Contents
:maxdepth: 2 :maxdepth: 2
install install
quickstart api
dev
Install
################################################################################
.. _install:
docs/logo/LIGO_F0900035-v1.jpg

267 KiB

*.mat
.ipynb_checkpoints/
from __future__ import division from __future__ import division
import os import os
import sys
import logging import logging
import importlib import importlib
import numpy as np import numpy as np
try:
from ._version import version as __version__
except ModuleNotFoundError:
try:
import setuptools_scm
__version__ = setuptools_scm.get_version(fallback_version='?.?.?')
# FIXME: fallback_version is not available in the buster version
# (3.2.0-1)
except (ModuleNotFoundError, TypeError, LookupError):
__version__ = '?.?.?'
from .ifo import IFOS from .ifo import IFOS
from .struct import Struct from .struct import Struct
from .plot import plot_trace
from .plot import plot_budget
from .plot import plot_noise from .plot import plot_noise
def _load_module(name_or_path): logger = logging.getLogger('gwinc')
DEFAULT_FREQ = '5:3000:6000'
class InvalidFrequencySpec(Exception):
pass
def freq_from_spec(spec=None):
"""logarithmicly spaced frequency array, based on specification string
Specification string should be of form 'START:[NPOINTS:]STOP'. If
`spec` is an array, then the array is returned as-is, and if it's
None the DEFAULT_FREQ specification is used.
"""
if isinstance(spec, np.ndarray):
return spec
elif spec is None:
spec = DEFAULT_FREQ
try:
fspec = spec.split(':')
if len(fspec) == 2:
fspec = fspec[0], DEFAULT_FREQ.split(':')[1], fspec[1]
return np.logspace(
np.log10(float(fspec[0])),
np.log10(float(fspec[2])),
int(fspec[1]),
)
except (ValueError, IndexError, AttributeError):
raise InvalidFrequencySpec(f'Improper frequency specification: {spec}')
def load_module(name_or_path):
"""Load module from name or path. """Load module from name or path.
Return loaded module and module path. Return loaded module and module path.
...@@ -22,6 +70,7 @@ def _load_module(name_or_path): ...@@ -22,6 +70,7 @@ def _load_module(name_or_path):
path = os.path.join(path, '__init__.py') path = os.path.join(path, '__init__.py')
spec = importlib.util.spec_from_file_location(modname, path) spec = importlib.util.spec_from_file_location(modname, path)
mod = importlib.util.module_from_spec(spec) mod = importlib.util.module_from_spec(spec)
sys.modules[mod.__name__] = mod
spec.loader.exec_module(mod) spec.loader.exec_module(mod)
else: else:
mod = importlib.import_module(name_or_path) mod = importlib.import_module(name_or_path)
...@@ -32,62 +81,97 @@ def _load_module(name_or_path): ...@@ -32,62 +81,97 @@ def _load_module(name_or_path):
return mod, path return mod, path
def load_budget(name_or_path): def load_budget(name_or_path, freq=None, bname=None):
"""Load GWINC IFO Budget by name or from path. """Load GWINC Budget
Named IFOs should correspond to one of the IFOs available in the Accepts either the name of a built-in canonical budget (see
gwinc package (see gwinc.IFOS). If a path is provided it should gwinc.IFOS), the path to a budget package (directory) or module
either be a budget package (directory) or module (ending in .py), (ending in .py), or the path to an IFO Struct definition file (see
or an IFO struct definition (see gwinc.Struct). gwinc.Struct). If an IFO Struct is specified, the base "aLIGO"
budget definition will be used.
If a budget package path is provided, and the package includes an If `bname` is specified the Budget class with that name will be
'ifo.yaml' file, that file will be loaded into a Struct and loaded from the budget module. Otherwise, the Budget class with
assigned as an attribute to the returned Budget class. the same name as the budget module will be loaded.
If a bare struct is provided the base aLIGO budget definition will If the budget is a package directory which includes an 'ifo.yaml'
be assumed. file the ifo Struct will be loaded from that file and assigned to
the budget.ifo attribute. If a Struct definition file is provided
the base aLIGO budget definition will be assumed.
Returns a Budget class with 'ifo', 'freq', and 'plot_style', ifo Returns the instantiated Budget object. If a frequency array or
structure, frequency array, and plot style dictionary, with the frequency specification string (see `freq_from_spec()`) is
last three being None if they are not defined in the budget. provided, the budget will be instantiated with the provided array.
If a frequency array is not provided and the Budget class
definition includes a `freq` attribute defining either an array or
specification string, then that array will be used. Otherwise a
default array will be provided (see DEFAULT_FREQ).
Any included ifo will be assigned as an attribute to the returned
Budget object.
""" """
ifo = None ifo = None
if os.path.exists(name_or_path): if os.path.exists(name_or_path):
path = name_or_path.rstrip('/') path = name_or_path.rstrip('/')
bname, ext = os.path.splitext(os.path.basename(path)) base, ext = os.path.splitext(os.path.basename(path))
if ext in Struct.STRUCT_EXT: if ext in Struct.STRUCT_EXT:
logging.info("loading struct {}...".format(path)) logger.info("loading struct {}...".format(path))
ifo = Struct.from_file(path) ifo = Struct.from_file(path, _pass_inherit=True)
bname = 'aLIGO'
modname = 'gwinc.ifo.aLIGO' inherit_ifo = ifo.get('+inherit', None)
logging.info("loading budget {}...".format(modname)) if inherit_ifo is not None:
del ifo['+inherit']
# make the inherited path relative to the loaded path
# if it is a yml file or a directory
head = os.path.split(path)[0]
rel_path = os.path.join(head, inherit_ifo)
if os.path.splitext(inherit_ifo)[1] in Struct.STRUCT_EXT or os.path.exists(rel_path):
inherit_ifo = rel_path
inherit_budget = load_budget(inherit_ifo, freq=freq, bname=bname)
pre_ifo = inherit_budget.ifo
pre_ifo.update(
ifo,
overwrite_atoms=False,
clear_test=lambda v: isinstance(v, str) and v == '<unset>'
)
inherit_budget.update(ifo=pre_ifo)
return inherit_budget
else:
modname = 'gwinc.ifo.aLIGO'
bname = bname or 'aLIGO'
elif ext == '':
bname = bname or base
modname = path
else: else:
modname = path raise RuntimeError(
logging.info("loading module path {}...".format(modname)) "Unknown file type: {} (supported types: {}).".format(
ext, Struct.STRUCT_EXT))
else: else:
if name_or_path not in IFOS: if name_or_path not in IFOS:
raise RuntimeError("Unknonw IFO '{}' (available IFOs: {}).".format( raise RuntimeError("Unknown IFO '{}' (available IFOs: {}).".format(
name_or_path, name_or_path,
IFOS, IFOS,
)) ))
bname = name_or_path bname = bname or name_or_path
modname = 'gwinc.ifo.'+name_or_path modname = 'gwinc.ifo.' + name_or_path
logging.info("loading module {}...".format(modname))
mod, modpath = _load_module(modname)
logger.info(f"loading budget '{bname}' from {modname}...")
mod, modpath = load_module(modname)
Budget = getattr(mod, bname) Budget = getattr(mod, bname)
if freq is None:
freq = getattr(Budget, '_freq', None)
freq = freq_from_spec(freq)
ifopath = os.path.join(modpath, 'ifo.yaml') ifopath = os.path.join(modpath, 'ifo.yaml')
if not ifo and ifopath: if not ifo and os.path.exists(ifopath):
ifo = Struct.from_file(ifopath) ifo = Struct.from_file(ifopath)
Budget.ifo = ifo return Budget(freq=freq, ifo=ifo)
return Budget
def gwinc(freq, ifo, source=None, plot=False, PRfixed=True): def gwinc(freq, ifo, source=None, plot=False, PRfixed=True):
...@@ -109,28 +193,29 @@ def gwinc(freq, ifo, source=None, plot=False, PRfixed=True): ...@@ -109,28 +193,29 @@ def gwinc(freq, ifo, source=None, plot=False, PRfixed=True):
# assume generic aLIGO configuration # assume generic aLIGO configuration
# FIXME: how do we allow adding arbitrary addtional noise sources # FIXME: how do we allow adding arbitrary addtional noise sources
# from just ifo description, without having to specify full budget # from just ifo description, without having to specify full budget
Budget = load_budget('aLIGO') budget = load_budget('aLIGO', freq)
traces = Budget(freq, ifo=ifo).run() traces = budget.run()
plot_style = getattr(Budget, 'plot_style', {}) plot_style = getattr(budget, 'plot_style', {})
# construct matgwinc-compatible noises structure # construct matgwinc-compatible noises structure
noises = {} noises = {}
for name, (data, style) in traces.items(): for name, trace in traces.items():
noises[style.get('label', name)] = data noises[name] = trace.psd
noises['Freq'] = freq noises['Total'] = traces.psd
noises['Freq'] = traces.freq
pbs = ifo.gwinc.pbs pbs = ifo.gwinc.pbs
parm = ifo.gwinc.parm parm = ifo.gwinc.parm
finesse = ifo.gwinc.finesse finesse = ifo.gwinc.finesse
prfactor = ifo.gwinc.prfactor prfactor = ifo.gwinc.prfactor
if ifo.Laser.Power * prfactor != pbs: if ifo.Laser.Power * prfactor != pbs:
logging.warning("Thermal lensing limits input power to {} W".format(pbs/prfactor)) logger.warning("Thermal lensing limits input power to {} W".format(pbs/prfactor))
# report astrophysical scores if so desired # report astrophysical scores if so desired
score = None score = None
if source: if source:
logging.warning("Source score calculation currently not supported. See `inspiral-range` package for similar functionality:") logger.warning("Source score calculation currently not supported. See `inspiral-range` package for similar functionality:")
logging.warning("https://git.ligo.org/gwinc/inspiral-range") logger.warning("https://git.ligo.org/gwinc/inspiral-range")
# score = int731(freq, noises['Total'], ifo, source) # score = int731(freq, noises['Total'], ifo, source)
# score.Omega = intStoch(freq, noises['Total'], 0, ifo, source) # score.Omega = intStoch(freq, noises['Total'], 0, ifo, source)
...@@ -138,32 +223,34 @@ def gwinc(freq, ifo, source=None, plot=False, PRfixed=True): ...@@ -138,32 +223,34 @@ def gwinc(freq, ifo, source=None, plot=False, PRfixed=True):
# output graphics # output graphics
if plot: if plot:
logging.info('Laser Power: %7.2f Watt' % ifo.Laser.Power) logger.info('Laser Power: %7.2f Watt' % ifo.Laser.Power)
logging.info('SRM Detuning: %7.2f degree' % (ifo.Optics.SRM.Tunephase*180/np.pi)) logger.info('SRM Detuning: %7.2f degree' % (ifo.Optics.SRM.Tunephase*180/np.pi))
logging.info('SRM transmission: %9.4f' % ifo.Optics.SRM.Transmittance) logger.info('SRM transmission: %9.4f' % ifo.Optics.SRM.Transmittance)
logging.info('ITM transmission: %9.4f' % ifo.Optics.ITM.Transmittance) logger.info('ITM transmission: %9.4f' % ifo.Optics.ITM.Transmittance)
logging.info('PRM transmission: %9.4f' % ifo.Optics.PRM.Transmittance) logger.info('PRM transmission: %9.4f' % ifo.Optics.PRM.Transmittance)
logging.info('Finesse: %7.2f' % finesse) logger.info('Finesse: %7.2f' % finesse)
logging.info('Power Recycling Gain: %7.2f' % prfactor) logger.info('Power Recycling Gain: %7.2f' % prfactor)
logging.info('Arm Power: %7.2f kW' % (parm/1000)) logger.info('Arm Power: %7.2f kW' % (parm/1000))
logging.info('Power on BS: %7.2f W' % pbs) logger.info('Power on BS: %7.2f W' % pbs)
# coating and substrate thermal load on the ITM # coating and substrate thermal load on the ITM
PowAbsITM = (pbs/2) * \ PowAbsITM = (
np.hstack([(finesse*2/np.pi) * ifo.Optics.ITM.CoatingAbsorption, (pbs/2)
(2 * ifo.Materials.MassThickness) * ifo.Optics.ITM.SubstrateAbsorption]) * np.hstack([
(finesse*2/np.pi) * ifo.Optics.ITM.CoatingAbsorption,
logging.info('Thermal load on ITM: %8.3f W' % sum(PowAbsITM)) (2 * ifo.Materials.MassThickness) * ifo.Optics.ITM.SubstrateAbsorption])
logging.info('Thermal load on BS: %8.3f W' % )
(ifo.Materials.MassThickness*ifo.Optics.SubstrateAbsorption*pbs))
logger.info('Thermal load on ITM: %8.3f W' % sum(PowAbsITM))
logger.info('Thermal load on BS: %8.3f W' % (ifo.Materials.MassThickness*ifo.Optics.SubstrateAbsorption*pbs))
if (ifo.Laser.Power*prfactor != pbs): if (ifo.Laser.Power*prfactor != pbs):
logging.info('Lensing limited input power: %7.2f W' % (pbs/prfactor)) logger.info('Lensing limited input power: %7.2f W' % (pbs/prfactor))
if score: if score:
logging.info('BNS Inspiral Range: ' + str(score.effr0ns) + ' Mpc/ z = ' + str(score.zHorizonNS)) logger.info('BNS Inspiral Range: ' + str(score.effr0ns) + ' Mpc/ z = ' + str(score.zHorizonNS))
logging.info('BBH Inspiral Range: ' + str(score.effr0bh) + ' Mpc/ z = ' + str(score.zHorizonBH)) logger.info('BBH Inspiral Range: ' + str(score.effr0bh) + ' Mpc/ z = ' + str(score.zHorizonBH))
logging.info('Stochastic Omega: %4.1g Universes' % score.Omega) logger.info('Stochastic Omega: %4.1g Universes' % score.Omega)
plot_noise(ifo, traces, **plot_style) traces.plot(**plot_style)
return score, noises, ifo return score, noises, ifo
from __future__ import print_function from __future__ import print_function
import os import os
import signal import signal
import logging
import argparse import argparse
import numpy as np
from IPython.terminal.embed import InteractiveShellEmbed
import logging from . import (
logging.basicConfig( __version__,
format='%(message)s', IFOS,
level=os.getenv('LOG_LEVEL', logging.WARNING), DEFAULT_FREQ,
InvalidFrequencySpec,
load_budget,
logger,
) )
from . import io
from . import IFOS, load_budget, plot_noise logger.setLevel(os.getenv('LOG_LEVEL', 'WARNING').upper())
formatter = logging.Formatter('%(message)s')
handler = logging.StreamHandler()
handler.setFormatter(formatter)
logger.addHandler(handler)
################################################## ##################################################
description = """Plot GWINC noise budget for specified IFO. description = """GWINC noise budget tool
IFOs can be specified by name of included canonical budget (see
below), or by path to a budget module (.py), description file
(.yaml/.mat/.m), or HDF5 data file (.hdf5/.h5). Available included
IFOs are:
Available included IFOs: {} {}
""".format(', '.join(["'{}'".format(ifo) for ifo in IFOS])) """.format(', '.join(["{}".format(ifo) for ifo in IFOS]))
# for ifo in available_ifos(): # for ifo in available_ifos():
# description += " '{}'\n".format(ifo) # description += " '{}'\n".format(ifo)
description += """ description += """
By default a GWINC noise budget of the specified IFO will calculated, By default the noise budget of the specified IFO will be loaded and
and plotted with an interactive plotter. If the --save option is plotted with an interactive plotter. Individual IFO parameters can be
specified the plot will be saved directly to a file (without display) overriden with the --ifo option:
(various formats are supported, indicated by file extension). If the
requested extension is 'hdf5' or 'h5' then the noise traces and IFO
parameters will be saved to an HDF5 file (without plotting). The
input file (IFO) can be an HDF5 file saved from a previous call, in
which case all noise traces and IFO parameters will be loaded from
that file.
If the inspiral_range package is installed, various figures of merit gwinc --ifo Optics.SRM.Tunephase=3.14 ...
can be calculated for the resultant spectrum with the --fom argument,
e.g.:
gwinc --fom horizon ... If the --save option is specified the plot will be saved directly to a
gwinc --fom range:m1=20,m2=20 ... file (without display) (various file formats are supported, indicated
by file extension). If the requested extension is 'hdf5' or 'h5' then
the noise traces and IFO parameters will be saved to an HDF5 file.
See documentation for inspiral_range package for details. If the --range option is specified and the inspiral_range package is
available, various BNS (m1=m2=1.4 M_solar) range figures of merit will
be calculated for the resultant spectrum. The default waveform
parameters can be overriden with the --waveform-parameter/-wp option:
gwinc -r -wp m1=20 -wp m2=20 ...
See the inspiral_range package documentation for details.
""" """
IFO = 'aLIGO' IFO = 'aLIGO'
FLO = 5 RANGE_PARAMS = dict(m1=1.4, m2=1.4)
FHI = 6000 DATA_SAVE_FORMATS = ['.hdf5', '.h5']
NPOINTS = 3000
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
prog='gwinc', prog='gwinc',
description=description, description=description,
formatter_class=argparse.RawDescriptionHelpFormatter) formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('--flo', '-fl', default=FLO, type=float, parser.add_argument(
help="lower frequency bound in Hz [{}]".format(FLO)) '--version', '-v', action='version', version=__version__)
parser.add_argument('--fhi', '--fh', default=FHI, type=float, parser.add_argument(
help="upper frequency bound in Hz [{}]".format(FHI)) '--freq', '-f', metavar='FLO:[NPOINTS:]FHI',
parser.add_argument('--npoints', '-n', default=NPOINTS, help="logarithmic frequency array specification in Hz [{}]".format(DEFAULT_FREQ))
help="number of frequency points [{}]".format(NPOINTS)) parser.add_argument(
parser.add_argument('--title', '-t', '--ifo', '-o', metavar='PARAM=VAL', default=[],
help="plot title") #nargs='+', action='extend',
parser.add_argument('--fom', action='append',
help="calculate inspiral range for resultant spectrum ('func[:param=val,param=val]')") help="override budget IFO parameter (may be specified multiple times)")
parser.add_argument(
'--title', '-t',
help="plot title")
parser.add_argument(
'--range', '-r', action='store_true',
help="calculate inspiral ranges [m1=m2=1.4]")
parser.add_argument(
'--waveform-parameter', '-wp', metavar='PARAM=VAL', default=[],
action='append',
help="specify inspiral range parameters (may be specified multiple times)")
group = parser.add_mutually_exclusive_group() group = parser.add_mutually_exclusive_group()
group.add_argument('--interactive', '-i', action='store_true', group.add_argument(
help="interactive plot with interactive shell") '--interactive', '-i', action='store_true',
group.add_argument('--save', '-s', help="launch interactive shell after budget processing")
help="save budget traces (.hdf5/.h5) or plot (.pdf/.png/.svg) to file") group.add_argument(
group.add_argument('--yaml', '-y', action='store_true', '--save', '-s', metavar='PATH', action='append',
help="print IFO as yaml to stdout and exit") help="save plot (.png/.pdf/.svg) or budget traces (.hdf5/.h5) to file (may be specified multiple times)")
group.add_argument('--text', '-x', action='store_true', group.add_argument(
help="print IFO as text table to stdout and exit") '--yaml', '-y', action='store_true',
group.add_argument('--diff', '-d', metavar='IFO', help="print IFO as yaml to stdout and exit (budget not calculated)")
help="show differences table between another IFO description") group.add_argument(
group.add_argument('--no-plot', '-np', action='store_false', dest='plot', '--text', '-x', action='store_true',
help="supress plotting") help="print IFO as text table to stdout and exit (budget not calculated)")
parser.add_argument('IFO', group.add_argument(
help="IFO name, description file path (.yaml, .mat, .m), budget module (.py), or HDF5 data file (.hdf5, .h5)") '--diff', '-d', metavar='IFO',
help="show difference table between IFO and another IFO description (name or path) and exit (budget not calculated)")
group.add_argument(
'--list', '-l', action='store_true',
help="list all elements of Budget (budget not calculated)")
parser.add_argument(
'--no-plot', '-np', action='store_false', dest='plot',
help="suppress plotting")
parser.add_argument(
'--bname', '-b',
help="name of top-level Budget class to load (defaults to IFO name)")
parser.add_argument(
'IFO',
help="IFO name or path")
parser.add_argument(
'subbudget', metavar='SUBBUDGET', nargs='?',
help="subbudget to plot; can be nested (e.g. 'Thermal.Substrate')")
def main(): def main():
...@@ -89,56 +126,90 @@ def main(): ...@@ -89,56 +126,90 @@ def main():
########## ##########
# initial arg processing # initial arg processing
if os.path.splitext(os.path.basename(args.IFO))[1] in ['.hdf5', '.h5']: if os.path.splitext(os.path.basename(args.IFO))[1] in io.DATA_SAVE_FORMATS:
if args.freq:
parser.exit(2, "Error: Frequency specification not allowed when loading traces from file.\n")
if args.ifo:
parser.exit(2, "Error: IFO parameter specification not allowed when loading traces from file.\n")
from .io import load_hdf5 from .io import load_hdf5
Budget = None budget = None
freq, traces, attrs = load_hdf5(args.IFO) name = args.IFO
ifo = getattr(attrs, 'IFO', None) trace = load_hdf5(args.IFO)
plot_style = attrs freq = trace.freq
ifo = trace.ifo
plot_style = trace.plot_style
else: else:
Budget = load_budget(args.IFO) try:
ifo = Budget.ifo budget = load_budget(args.IFO, freq=args.freq, bname=args.bname)
# FIXME: this should be done only if specified, to allow for except InvalidFrequencySpec as e:
# using any FREQ specified in the Budget parser.error(e)
freq = np.logspace(np.log10(args.flo), np.log10(args.fhi), args.npoints) except RuntimeError as e:
plot_style = getattr(Budget, 'plot_style', {}) parser.exit(2, f"Error: {e}\n")
traces = None name = budget.name
ifo = budget.ifo
freq = budget.freq
plot_style = getattr(budget, 'plot_style', {})
trace = None
for paramval in args.ifo:
try:
param, val = paramval.split('=', 1)
ifo[param] = float(val)
except ValueError:
parser.error(f"Improper IFO parameter specification: {paramval}")
if args.yaml: if args.yaml:
if not ifo: if not ifo:
parser.exit(2, "no IFO structure available.") parser.exit(2, "Error: IFO structure not provided.\n")
print(ifo.to_yaml(), end='') print(ifo.to_yaml(), end='')
return return
if args.text: if args.text:
if not ifo: if not ifo:
parser.exit(2, "no IFO structure available.") parser.exit(2, "Error: IFO structure not provided.\n")
print(ifo.to_txt(), end='') print(ifo.to_txt(), end='')
return return
if args.diff: if args.diff:
if not ifo: if not ifo:
parser.exit(2, "no IFO structure available.") parser.exit(2, "Error: IFO structure not provided.\n")
fmt = '{:30} {:>20} {:>20}' dbudget = load_budget(args.diff)
Budget = load_budget(args.diff) diffs = ifo.diff(dbudget.ifo)
diffs = ifo.diff(Budget.ifo)
if diffs: if diffs:
w = max([len(d[0]) for d in diffs])
fmt = '{{:{}}} {{:>20}} {{:>20}}'.format(w)
print(fmt.format('', args.IFO, args.diff)) print(fmt.format('', args.IFO, args.diff))
print(fmt.format('', '-----', '-----'))
for p in diffs: for p in diffs:
k = str(p[0]) k = str(p[0])
v = repr(p[1]) v = repr(p[1])
ov = repr(p[2]) ov = repr(p[2])
print(fmt.format(k, v, ov)) print(fmt.format(k, v, ov))
return return
if args.list:
for i in budget.walk():
name = '.'.join([n.__class__.__name__ for n in i])
type = i[-1].__class__.__bases__[0].__name__
print(f'{name} ({type})')
return
if args.title: if args.subbudget:
plot_style['title'] = args.title try:
elif Budget: budget[args.subbudget]
plot_style['title'] = "GWINC Noise Budget: {}".format(Budget.name) except KeyError:
else: parser.exit(3, f"Error: Unknown budget item '{args.subbudget}'.\n")
plot_style['title'] = "GWINC Noise Budget: {}".format(args.IFO)
out_data_files = set()
if args.plot: out_plot_files = set()
if args.save: if args.save:
args.plot = False
out_files = set(args.save)
for path in out_files:
if os.path.splitext(path)[1] in io.DATA_SAVE_FORMATS:
out_data_files.add(path)
out_plot_files = out_files - out_data_files
if args.plot or out_plot_files:
if out_plot_files:
# FIXME: this silliness seems to be the only way to have # FIXME: this silliness seems to be the only way to have
# matplotlib usable on systems without a display. There must # matplotlib usable on systems without a display. There must
# be a better way. 'AGG' is a backend that works without # be a better way. 'AGG' is a backend that works without
...@@ -150,109 +221,150 @@ def main(): ...@@ -150,109 +221,150 @@ def main():
matplotlib.use('AGG') matplotlib.use('AGG')
try: try:
from matplotlib import pyplot as plt from matplotlib import pyplot as plt
except ImportError as e:
parser.exit(5, f"ImportError: {e}\n")
except RuntimeError: except RuntimeError:
logging.warning("no display, plotting disabled.") parser.exit(10, "Error: Could not open display for plotting.\n")
args.plot = False
if args.fom: if args.range:
import inspiral_range
try: try:
range_func, fargs = args.fom.split(':') import inspiral_range
except ValueError: except ImportError as e:
range_func = args.fom parser.exit(5, f"ImportError: {e}\n")
fargs = ''
range_params = {} logger_ir = logging.getLogger('inspiral_range')
for param in fargs.split(','): logger_ir.setLevel(logger.getEffectiveLevel())
if not param: handler = logging.StreamHandler()
continue handler.setFormatter(logging.Formatter('%(name)s: %(message)s'))
p,v = param.split('=') logger_ir.addHandler(handler)
if not v:
raise ValueError('missing parameter value "{}"'.format(p)) for paramval in args.waveform_parameter:
if p != 'approximant': try:
v = float(v) param, val = paramval.split('=')
range_params[p] = v if not val:
raise ValueError
except ValueError:
parser.error(f"Improper range parameter specification: {paramval}")
try:
val = float(val)
except ValueError:
pass
RANGE_PARAMS[param] = val
########## ##########
# main calculations # main calculations
if not traces: if not trace:
logging.info("calculating budget...") logger.info("calculating budget...")
traces = Budget(freq=freq, ifo=ifo).run() trace = budget.run()
# logging.info('recycling factor: {: >0.3f}'.format(ifo.gwinc.prfactor)) if args.range:
# logging.info('BS power: {: >0.3f} W'.format(ifo.gwinc.pbs)) logger.info("calculating inspiral ranges...")
# logging.info('arm finesse: {: >0.3f}'.format(ifo.gwinc.finesse)) metrics, H = inspiral_range.all_ranges(freq, trace.psd, **RANGE_PARAMS)
# logging.info('arm power: {: >0.3f} kW'.format(ifo.gwinc.parm/1000)) print(f"{H.params['approximant']} {H.params['m1']}/{H.params['m2']} M_solar:")
for metric, (value, unit) in metrics.items():
if args.fom: if unit is None:
logging.info("calculating inspiral {}...".format(range_func)) unit = ''
H = inspiral_range.CBCWaveform(freq, **range_params) print(f" {metric}: {value:0.1f} {unit}")
logging.debug("params: {}".format(H.params)) range_func = 'range'
fom = eval('inspiral_range.{}'.format(range_func))(freq, traces['Total'][0], H=H) subtitle = 'inspiral {func} {m1}/{m2} $\mathrm{{M}}_\odot$: {fom:.0f} {unit}'.format(
logging.info("{}({}) = {:.2f} Mpc".format(range_func, fargs, fom))
fom_title = '{func} {m1}/{m2} Msol: {fom:.2f} Mpc'.format(
func=range_func, func=range_func,
m1=H.params['m1'], m1=H.params['m1'],
m2=H.params['m2'], m2=H.params['m2'],
fom=fom, fom=metrics[range_func][0],
) unit=metrics[range_func][1] or '',
plot_style['title'] += '\n{}'.format(fom_title) )
else:
subtitle = None
########## if args.subbudget:
# output trace = trace[args.subbudget]
name += f': {args.subbudget}'
# save noise traces to HDF5 file if args.title:
if args.save and os.path.splitext(args.save)[1] in ['.hdf5', '.h5']: plot_style['title'] = args.title
from .io import save_hdf5 else:
logging.info("saving budget traces {}...".format(args.save)) plot_style['title'] = "GWINC Noise Budget: {}".format(name)
if ifo:
plot_style['IFO'] = ifo.to_yaml() ##########
save_hdf5( # interactive
path=args.save,
freq=freq,
traces=traces,
**plot_style
)
# interactive shell plotting # interactive shell plotting
elif args.interactive: if args.interactive:
ipshell = InteractiveShellEmbed( banner = """GWINC interactive shell
The 'ifo' Struct, 'budget', and 'trace' objects are available for
inspection. Use the 'whos' command to view the workspace.
"""
if not args.plot:
banner += """
You may plot the budget using the 'trace.plot()' method:
In [.]: trace.plot(**plot_style)
"""
banner += """
You may interact with the plot using the 'plt' functions, e.g.:
In [.]: plt.title("foo")
In [.]: plt.savefig("foo.pdf")
"""
from IPython.core import getipython
from IPython.terminal.embed import InteractiveShellEmbed
if subtitle:
plot_style['title'] += '\n' + subtitle
# deal with breaking change in ipython embedded mode
# https://github.com/ipython/ipython/issues/13966
if getipython.get_ipython() is None:
embed = InteractiveShellEmbed.instance
else:
embed = InteractiveShellEmbed
ipshell = embed(
banner1=banner,
user_ns={ user_ns={
'freq': freq,
'traces': traces,
'ifo': ifo, 'ifo': ifo,
'budget': budget,
'trace': trace,
'plot_style': plot_style, 'plot_style': plot_style,
'plot_noise': plot_noise,
}, },
banner1=''' )
GWINC interactive plotter ipshell.enable_pylab(import_all=False)
if args.plot:
ipshell.ex("fig = trace.plot(**plot_style)")
ipshell()
You may interact with plot using "plt." methods, e.g.: ##########
# output
>>> plt.title("foo") # save noise trace to HDF5 file
>>> plt.savefig("foo.pdf") if out_data_files:
''') for path in out_data_files:
ipshell.enable_pylab() logger.info("saving budget trace: {}".format(path))
ipshell.run_code("plot_noise(freq, traces, **plot_style)") io.save_hdf5(
ipshell.run_code("plt.title('{}')".format(plot_style['title'])) trace=trace,
ipshell() path=path,
ifo=ifo,
plot_style=plot_style,
)
# standard plotting # standard plotting
elif args.plot: if args.plot or out_plot_files:
logging.info("plotting noises...") logger.debug("plotting noises...")
fig = plt.figure() fig = plt.figure()
ax = fig.add_subplot(1, 1, 1) ax = fig.add_subplot(1, 1, 1)
plot_noise( if subtitle:
freq, plot_style['title'] += '\n' + subtitle
traces, trace.plot(
ax=ax, ax=ax,
**plot_style **plot_style
) )
fig.tight_layout() fig.tight_layout()
if args.save: if out_plot_files:
fig.savefig( for path in out_plot_files:
args.save, logger.info("saving budget plot: {}".format(path))
) try:
fig.savefig(path)
except Exception as e:
parser.exit(2, f"Error saving plot: {e}.\n")
else: else:
plt.show() plt.show()
......