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 (360)
[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__/
...@@ -18,3 +27,6 @@ __pycache__/ ...@@ -18,3 +27,6 @@ __pycache__/
MANIFEST MANIFEST
# Mac OS Files
*.DS_Store
image: ligo/software:stretch
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:
- apt-get update -qq - mkdir -p ifo
- apt-get install -y -qq python3-yaml python3-scipy python3-matplotlib python3-ipython lalsimulation-python3 python3-pypdf2 - export PYTHONPATH=/inspiral_range
- git clone https://gitlab-ci-token:ci_token@git.ligo.org/gwinc/inspiral_range.git - for ifo in $(python3 -c "import gwinc; print(' '.join(gwinc.IFOS))"); do
- export PYTHONPATH=inspiral_range - python3 -m gwinc $ifo -s ifo/$ifo.png -s ifo/$ifo.h5
- export MPLBACKEND=agg - done
- for ifo in aLIGO Aplus Voyager CE1 CE2; do - python3 -m gwinc.ifo -s ifo/all_compare.png
- python3 -m gwinc $ifo -s $ifo.png
- done
- python3 -m gwinc.test -r gwinc_test_report.pdf
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:
- aLIGO.png - ifo
- Aplus.png
- Voyager.png
- CE1.png
- CE2.png
- 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:
- mkdir public - rm -rf public
- for ifo in aLIGO Aplus Voyager CE1 CE2; do - apt-get install -y -qq python3-sphinx-rtd-theme
- mv $ifo.png public/ - cd docs
- done - make html
- mv gwinc_test_report.pdf public/ || true - cd ..
- apt-get install -y -qq python3-pip python3-dev make - mv ./build/sphinx/html public
- pip3 install sphinx sphinx-rtd-theme - mv ifo public/
- cd docs artifacts:
- make html when: always
- cd .. paths:
- mv ./build/sphinx/html/* public/ - 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,11 +2,48 @@ ...@@ -2,11 +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)
If your change affects the shape of a noise curve, your commit message against the main pygwinc master branch.
should make note of that, and provide a justification. It will also
be necessary to update the reference curves stored in DCC entry When submitting code for merge, please follow good coding practice.
[T18xxxxx](https://dcc.ligo.org/LIGO-T18xxxxx), after the change is Respect the existing coding style, which for `pygwinc` is standard
applied. [PEP8](https://www.python.org/dev/peps/pep-0008/) (with some
exceptions). Make individual commits as logically distinct and atomic
as possible, and provide a complete, descriptive log of the changes
(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
$ python3 -m gwinc.test
```
Use the '--plot' or '--report' options to produce visual comparisons
of the noise differences. The comparison can be done against an
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
As discussed above, merge requests that generate noise changes will
cause a pipeline failure in the `review:noise_change_approval` CI job.
The job will generate a report comparing the new noise traces against
those from master, which can be found under the 'View exposed
artifacts' menu item in the pipeline report. Once you have reviewed
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/...
``` ```
# Canonical IFOs
CI-generated plots and data for all IFOs included in pygwinc.
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
* [ifo.yaml](gwinc/ifo/aLIGO/ifo.yaml)
* [aLIGO.h5](https://gwinc.docs.ligo.org/pygwinc/ifo/aLIGO.h5)
![aLIGO](https://gwinc.docs.ligo.org/pygwinc/ifo/aLIGO.png)
## A+
* [ifo.yaml](gwinc/ifo/Aplus/ifo.yaml)
* [Aplus.h5](https://gwinc.docs.ligo.org/pygwinc/ifo/Aplus.h5)
![Aplus](https://gwinc.docs.ligo.org/pygwinc/ifo/Aplus.png)
## Voyager
* [ifo.yaml](gwinc/ifo/Voyager/ifo.yaml)
* [Voyager.h5](https://gwinc.docs.ligo.org/pygwinc/ifo/Voyager.h5)
![Voyager](https://gwinc.docs.ligo.org/pygwinc/ifo/Voyager.png)
## Cosmic Explorer 1
* [ifo.yaml](gwinc/ifo/CE1/ifo.yaml)
* [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)
![CE2 (Silica)](https://gwinc.docs.ligo.org/pygwinc/ifo/CE2silica.png)
## Cosmic Explorer 2 (Silicon)
* [ifo.yaml](gwinc/ifo/CE2silicon/ifo.yaml)
* [CE2silicon.h5](https://gwinc.docs.ligo.org/pygwinc/ifo/CE2silicon.h5)
![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 *
[![pipeline status](https://git.ligo.org/gwinc/pygwinc/badges/master/pipeline.svg)](https://git.ligo.org/gwinc/pygwinc/commits/master) [![pipeline status](https://git.ligo.org/gwinc/pygwinc/badges/master/pipeline.svg)](https://git.ligo.org/gwinc/pygwinc/commits/master)
# Python port of GW Interferometer Noise Calculator # Python Gravitational Wave Interferometer Noise Calculator
[![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
budgets for ground-based gravitational wave detectors. It's primary
feature is a collection of mostly analytic [noise calculation
functions](#noise-functions) for various sources of noise affecting
detectors (`gwinc.noise`):
* quantum noise
* mirror coating thermal noise
* mirror substrate thermal noise
* suspension fiber thermal noise
* seismic noise
* Newtonian/gravity-gradient noise
* residual gas noise
`pygwinc` is also a generalized noise budgeting tool (`gwinc.nb`) that
allows users to create arbitrary noise budgets (for any experiment,
not just ground-based GW detectors) using measured or analytically
calculated data. See the [budget interface](#Budget-interface)
section below.
`pygwinc` includes canonical budgets for various well-known current
and future GW detectors (`gwinc.ifo`):
* [aLIGO](https://gwinc.docs.ligo.org/pygwinc/ifo/aLIGO.png)
* [A+](https://gwinc.docs.ligo.org/pygwinc/ifo/Aplus.png)
* [Voyager](https://gwinc.docs.ligo.org/pygwinc/ifo/Voyager.png)
* [Cosmic Explorer 1](https://gwinc.docs.ligo.org/pygwinc/ifo/CE1.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
data.
The [`inspiral_range`](https://git.ligo.org/gwinc/inspiral-range)
package can be used to calculate various common "inspiral range"
figures of merit for gravitational wave detector budgets. See the
[inspiral range](#inspiral-range) section below.
## usage
### command line interface
`pygwinc` provides a command line interface that can be used to
calculate and plot the various canonical IFO noise budgets described
above. For most distributions this should be available via
`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
$ 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
```
The `--range` option can be used to include the values of various
inspiral ranges for the overall noise in the output.
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
$ gwinc --yaml aLIGO > my_aLIGO.yaml
$ edit my_aLIGO.yaml
$ gwinc -d my_aLIGO.yaml aLIGO
aLIGO my_aLIGO.yaml
Materials.Coating.Philown 5e-05 3e-05
$ gwinc my_aLIGO.yaml
```
Stand-alone YAML files assume the nominal ['aLIGO' budget
description](gwinc/ifo/aLIGO).
The command line interface also includes an "interactive" mode which
provides an [IPython](https://ipython.org/) shell for interacting with
a processed budget:
```shell
$ 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.:
![gwinc](https://gwinc.docs.ligo.org/pygwinc/aLIGO.png) In [.]: plt.title("My Special Budget")
In [.]: plt.savefig("mybudget.pdf")
This is a collection of mostly analytic noise calculations (e.g. quantum, thermal) In [1]:
```
See command help for more info:
```shell
$ gwinc --help
```
## basic usage ### library interface
`pygwinc` creates noise budgets based on detector descriptions For custom plotting, parameter optimization, etc. all functionality can be
provided in either .yml or .mat files (see below). Once the detector accessed directly through the `gwinc` library interface:
description is loaded, the noise budget can be calculated and plotted:
```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)
>>> ifo = gwinc.precompIFO(freq, Budget.ifo) ```
>>> traces = Budget(freq, ifo=ifo).run() or frequency specification string ('FLO:[NPOINTS:]FHI'):
>>> fig = gwinc.plot_noise(freq, traces) ```python
>>> fig.show() >>> budget = gwinc.load_budget('aLIGO', freq='10:1000:1000')
``` ```
The `load_budget()` function takes most of the same inputs as the
command line interface (e.g. IFO names, budget module paths, YAML
parameter files), and returns the instantiated `Budget` object defined
in the specified budget module (see [budget
interface](#budget-interface) below). The budget `ifo` `gwinc.Struct`
is available in the `budget.ifo` attribute.
The budget `run()` method calculates all budget noises and the noise
total and returns a `BudgetTrace` object with `freq`, `psd`, and `asd`
properties. The budget sub-traces are available through a dictionary
(`trace['Quantum']`) interface and via attributes
(`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)
```
## command line interface
You can make gwinc plots directly from the command line by executing ## noise functions
the package directly:
```shell The `pygwinc` analytical noise functions are available in the
$ python3 -m gwinc aLIGO `gwinc.noise` package. This package includes multiple sub-modules for
the different types of noises, e.g. `suspensionthermal`,
`coatingthermal`, `quantum`, etc.)
The various noise functions need many different parameters to
calculate their noise outputs. Many parameters are expected to be in
the form of object attributes of a class-like container that is passed
to the calculation function. The pygwinc
[`Struct`](#gwinc.Struct-objects) object is designed to hold such
parameters.
For instance, the `coating_brownian` function expects a `materials`
structure as input argument, that holds the various mirror materials
parameters (e.g. `materials.Substrate.MirrorY`):
```python
def coating_brownian(f, materials, wavelength, wBeam, dOpt):
...
# extract substructures
sub = materials.Substrate
...
# substrate properties
Ysub = sub.MirrorY
```
## `gwinc.Struct` objects
`pygwinc` provides a `Struct` class that can hold parameters in
attributes and additionally acts like a dictionary, for passing to the
noise calculation functions. `Struct`s can be created from
dictionaries, or loaded from various file formats (see below).
### YAML parameter files
The easiest way to store all budget parameters is in a YAML file.
YAML files can be loaded directly into `gwinc.Struct` objects via
the `Struct.from_file()` class method:
```python
from gwinc import Struct
ifo = Struct.from_file('/path/to/ifo.yaml')
``` ```
YAML parameter files can also be given to the `load_budget()` function
as described above, in which case the base 'aLIGO' budget structure
will be assumed and returned, with the YAML Struct inserted in the
`Budget.ifo` class attribute.
Here are the included ifo.yaml files for all the canonical IFOs:
* [aLIGO.yaml](gwinc/ifo/aLIGO/ifo.yaml)
* [Aplus.yaml](gwinc/ifo/Aplus/ifo.yaml)
* [Voyager.yaml](gwinc/ifo/Voyager/ifo.yaml)
* [CE1.yaml](gwinc/ifo/CE1/ifo.yaml)
* [CE2.yaml](gwinc/ifo/CE2/ifo.yaml)
The `Struct.from_file()` class method can also load MATLAB structs
defined in .mat files, for compatibility with
[matgwinc](https://git.ligo.org/gwinc/matgwinc), and MATLAB .m files,
although the later requires the use of the [python MATLAB
engine](https://www.mathworks.com/help/matlab/matlab-engine-for-python.html).
## budget interface
`pygwinc` provides a generic noise budget interface, `gwinc.nb`, that
can be used to define custom noise budgets (it also underlies the
"canonical" budgets included in `gwinc.ifo`). Budgets are defined in
a "budget module" which includes `BudgetItem` definitions.
### BudgetItem classes
The `gwinc.nb` package provides three `BudgetItem` classes that can be
inherited to define the various components of a budget:
* `nb.Noise`: a noise source
* `nb.Calibration`: a noise calibration
* `nb.Budget`: a group of noises
## detector description files The primary action of a `BudgetItem` happens in it's `calc()` method.
For `Noise` classes, the `calc` method should return the noise PSD at
the specified frequency points. For the `Calibration` class, `calc`
should return a frequency response. `Budget` classes should not have
a special `calc` method defined as they already know how to calculate
the overall noise from their constituent noises and calibrations.
`pygwinc` can load budget descriptions in different formats: python Additionally `BudgetItem`s have two other methods, `load` and
package/module, .yaml YAML file, and MATLAB gwinc .mat or .m files. `update`, that can be overridden by the user to handle arbitrary data
processing. These are useful for creating budgets from "live" dynamic
noise measurements and the like. The three core methods therefore
are:
`pygwinc` includes budgets for various canonical detectors: * `load()`: initial loading of static data
* `update(**kwargs)`: update data/attributes
* `calc()`: return final data array
* [aLIGO](https://git.ligo.org/gwinc/pygwinc/blob/master/gwinc/ifo/aLIGO) Generally these methods are not called directly. Instead, the `Noise`
* [A+](https://git.ligo.org/gwinc/pygwinc/blob/master/gwinc/ifo/Aplus) and `Budget` classes include a `run` method that smartly executes them
* [Voyager](https://git.ligo.org/gwinc/pygwinc/blob/master/gwinc/ifo/Voyager) in sequence and returns a `BudgetTrace` object for the budget.
* [Cosmic Explorer](https://git.ligo.org/gwinc/pygwinc/blob/master/gwinc/ifo/CE)
See the built-in `BudgetItem` documentation for more info
(e.g. `pydoc3 gwinc.nb.BudgetItem`)
## noise budgets
### budget module definition
GWINC provides an `nb` package for defining arbitrary noise budgets: A budget module is a standard python module (single `.py` file) or
package (directory containing `__inti__.py` file) containing
`BudgetItem` definitions describing the various noises and
calibrations of a budget, as well as the overall budget calculation
itself. Each budget module should include one `nb.Budget` class
definition named after the module name.
Here's an example of a budget module named `MyBudget`. It defines two
`Noise` classes and one `Calibration` class, as well as the overall
`Budget` class (name `MyBudget` that puts them all together):
```shell
$ find MyBudget
MyBudget/
MyBudget/__init__.py
MyBudget/ifo.yaml
$
```
```python ```python
# MyBudget/__init__.py
import numpy as np import numpy as np
from gwinc import nb from gwinc import nb
from gwinc import noise from gwinc import noise
class ExcessGas(nb.Noise): class SuspensionThermal(nb.Noise):
"""Excess gas""" """Suspension thermal noise"""
style = dict( style = dict(
label='Excess Gas', label='Suspension Thermal',
color='#ad900d', color='#ad900d',
linestyle='--', linestyle='--',
) )
def calc(self): def calc(self):
return noise.residualgas.gas(self.freq, self.ifo) n = noise.suspensionthermal.suspension_thermal(
self.freq, self.ifo.Suspension)
return 2 * n
class MeasuredNoise(nb.Noise): class MeasuredNoise(nb.Noise):
"""My measured noise"""
style = dict( style = dict(
label='Measured Noise', label='Measured Noise',
color='#838209', color='#838209',
...@@ -79,40 +324,184 @@ class MeasuredNoise(nb.Noise): ...@@ -79,40 +324,184 @@ class MeasuredNoise(nb.Noise):
def load(self): def load(self):
psd, freq = np.loadtxt('/path/to/measured/psd.txt') psd, freq = np.loadtxt('/path/to/measured/psd.txt')
self.data = self.interpolate(f, psd) self.data = self.interpolate(freq, psd)
def calc(self): def calc(self):
return self.data return self.data
class MyCalibration(nb.Calibration):
def calc(self):
return np.ones_like(self.freq) * 1234
class MyBudget(nb.Budget): class MyBudget(nb.Budget):
noises = [ noises = [
ExcessGas, SuspensionThermal,
MeasuredNoise, MeasuredNoise,
] ]
calibrations = [
MyCalibration,
]
``` ```
The `style` attributes of the various `Noise` classes define plot
style for the noise.
## comparison with MATLAB gwinc This budget can be loaded with the `gwinc.load_budget()` function, and
processed as usual with the `Budget.run()` method:
```python
budget = load_budget('/path/to/MyBudget', freq)
trace = budget.run()
```
`pygwinc` includes the ability use MATLAB gwinc directly via the Other than the necessary `freq` initialization argument that defines
MATLAB python interface (see the CLI '--matlab' option above). This the frequency array, any additional keyword arguments are assigned as
also allows for easy direct comparison between the pygwinc and class attributes to the budget object, and to all of it's constituent
matgwinc noise budgets. sub noises/calibrations/budgets.
If you have a local checkout of matgwinc (at e.g. /path/to/gwinc) and Note that the `SuspensionThermal` Noise class above uses the
a local installation of MATLAB and it's python interface (at `suspension_thermal` analytic noise calculation function, which takes
e.g. /opt/matlab/python/lib/python3.6/site-packages) you can run the a "suspension" Struct as input argument. In this case, this
comparison as so: suspension Struct is extracted from the `self.ifo` Struct at
```shell `self.ifo.Suspension`.
$ export GWINCPATH=/path/to/matgwinc
$ export PYTHONPATH=/opt/matlab/python/lib/python3.6/site-packages If a budget module defined as a package includes an `ifo.yaml`
$ python3 -m gwinc.test -p aLIGO [parameter file](#parameter-files) in the package directory, the
`load_budget()` function will automatically load the YAML data into an
`ifo` `gwinc.Struct` and assign it to the `budget.ifo` attribute.
The IFOs included in `gwinc.ifo` provide examples of the use of the
budget interface (e.g. [gwinc.ifo.aLIGO](gwinc/ifo/aLIGO)).
### the "precomp" decorator
The `BudgetItem` supports "precomp" functions that can be used to
calculate common derived values needed in multiple `BudgetItems`.
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
There are various way to extract single noise terms from the Budget
interface. The most straightforward way is to run the full budget,
and extract the noise data the output traces dictionary:
```python
budget = load_budget('/path/to/MyBudget', freq)
trace = budget.run()
quantum_trace = trace['Quantum']
```
You can also calculate the final calibrated output noise for just a
single term using the Budget `calc_noise()` method:
```python
data = budget.calc_noise('Quantum')
```
You can also calculate a noise at it's source, without applying any
calibrations, by using the Budget `__getitem__` interface to extract
the specific Noise BudgetItem for the noise you're interested in, and
running it's `calc_trace()` method directly:
```python
data = budget['Quantum'].calc_trace()
```
# inspiral range
The [`inspiral_range`](https://git.ligo.org/gwinc/inspiral-range)
package can be used to calculate various common "inspiral range"
figures of merit for gravitational wave detector budgets. Here's an
example of how to use it to calculate the inspiral range of the
baseline 'Aplus' detector:
```python
import gwinc
import inspiral_range
import numpy as np
freq = np.logspace(1, 3, 1000)
budget = gwinc.load_budget('Aplus', freq)
trace = budget.run()
range = inspiral_range.range(
freq, trace.psd,
m1=30, m2=30,
)
``` ```
This will produce a summary page of the various noise spectra that
differ between matgwinc and pygwinc.
Latest comparison plots from continuous integration: See the [`inspiral_range`](https://git.ligo.org/gwinc/inspiral-range)
package for more details.
<!-- ## comparison with MATLAB gwinc -->
<!-- `pygwinc` includes the ability use MATLAB gwinc directly via the -->
<!-- MATLAB python interface (see the CLI '--matlab' option above). This -->
<!-- also allows for easy direct comparison between the pygwinc and -->
<!-- matgwinc noise budgets. -->
<!-- If you have a local checkout of matgwinc (at e.g. /path/to/gwinc) and -->
<!-- a local installation of MATLAB and it's python interface (at -->
<!-- e.g. /opt/matlab/python/lib/python3.6/site-packages) you can run the -->
<!-- comparison as so: -->
<!-- ```shell -->
<!-- $ export GWINCPATH=/path/to/matgwinc -->
<!-- $ export PYTHONPATH=/opt/matlab/python/lib/python3.6/site-packages -->
<!-- $ python3 -m gwinc.test -p aLIGO -->
<!-- ``` -->
<!-- This will produce a summary page of the various noise spectra that -->
<!-- differ between matgwinc and pygwinc. -->
<!-- Latest comparison plots from continuous integration: -->
* [aLIGO comparison](https://gwinc.docs.ligo.org/pygwinc/aLIGO_test.png) <!-- * [aLIGO comparison](https://gwinc.docs.ligo.org/pygwinc/aLIGO_test.png) -->
* [A+ comparison](https://gwinc.docs.ligo.org/pygwinc/A+_test.png) <!-- * [A+ comparison](https://gwinc.docs.ligo.org/pygwinc/A+_test.png) -->
"""
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
from .ifo import available_ifos 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 .struct import Struct from .struct import Struct
from .precomp import precompIFO from .plot import plot_trace
from .plot import plot_budget
from .plot import plot_noise from .plot import plot_noise
from .io import load_hdf5, save_hdf5
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.
...@@ -24,6 +70,7 @@ def _load_module(name_or_path): ...@@ -24,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)
...@@ -34,70 +81,105 @@ def _load_module(name_or_path): ...@@ -34,70 +81,105 @@ 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.available_ifos()). If a path is provided gwinc.IFOS), the path to a budget package (directory) or module
it should either be a budget package (directory) or module (ending (ending in .py), or the path to an IFO Struct definition file (see
in .py), 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 available_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,
available_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):
"""Calculate strain noise budget for a specified interferometer model. """Calculate strain noise budget for a specified interferometer model.
Argument `freq` is the frequency array for which the noises will Argument `freq` is the frequency array for which the noises will
be calculated, and `ifoin` is the IFO model (see the `load_ifo()` be calculated, and `ifo` is the IFO model (see the `load_budget()`
function). function). The nominal 'aLIGO' budget structure will be used.
If `source` structure provided, so evaluates the sensitivity of If `source` structure provided, so evaluates the sensitivity of
the detector to several potential gravitational wave the detector to several potential gravitational wave
...@@ -111,30 +193,29 @@ def gwinc(freq, ifo, source=None, plot=False, PRfixed=True): ...@@ -111,30 +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)
ifo = precompIFO(freq, ifo, PRfixed) traces = budget.run()
traces = Budget(freq, ifo=ifo).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:
pass logger.warning("Thermal lensing limits input power to {} W".format(pbs/prfactor))
#warning(['Thermal lensing limits input power to ' num2str(pbs/prfactor, 3) ' W']);
# 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)
...@@ -142,32 +223,34 @@ def gwinc(freq, ifo, source=None, plot=False, PRfixed=True): ...@@ -142,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/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 argparse
import numpy as np
from IPython.terminal.embed import InteractiveShellEmbed
import logging import logging
logging.basicConfig(format='%(message)s', import argparse
level=os.getenv('LOG_LEVEL', logging.INFO))
from . import available_ifos, load_budget, plot_noise from . import (
from .precomp import precompIFO __version__,
IFOS,
DEFAULT_FREQ,
InvalidFrequencySpec,
load_budget,
logger,
)
from . import io from . import io
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 available_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 gwinc --ifo Optics.SRM.Tunephase=3.14 ...
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 If the --save option is specified the plot will be saved directly to a
can be calculated for the resultant spectrum with the --fom argument, file (without display) (various file formats are supported, indicated
e.g.: 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.
gwinc --fom horizon ... If the --range option is specified and the inspiral_range package is
gwinc --fom range:m1=20,m2=20 ... 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:
See documentation for inspiral_range package for details. 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,55 +126,90 @@ def main(): ...@@ -89,55 +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:
Budget = None if args.freq:
freq, traces, attrs = io.load_hdf5(args.IFO) parser.exit(2, "Error: Frequency specification not allowed when loading traces from file.\n")
ifo = getattr(attrs, 'IFO', None) if args.ifo:
plot_style = attrs parser.exit(2, "Error: IFO parameter specification not allowed when loading traces from file.\n")
from .io import load_hdf5
budget = None
name = args.IFO
trace = load_hdf5(args.IFO)
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_ifo(args.diff) diffs = ifo.diff(dbudget.ifo)
diffs = ifo.diff(ifoo)
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
...@@ -149,111 +221,150 @@ def main(): ...@@ -149,111 +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:
if ifo: logger.info("calculating budget...")
logging.info("precomputing ifo...") trace = budget.run()
ifo = precompIFO(freq, ifo)
logging.info("calculating budget...") if args.range:
traces = Budget(freq=freq, ifo=ifo).run() logger.info("calculating inspiral ranges...")
metrics, H = inspiral_range.all_ranges(freq, trace.psd, **RANGE_PARAMS)
# logging.info('recycling factor: {: >0.3f}'.format(ifo.gwinc.prfactor)) print(f"{H.params['approximant']} {H.params['m1']}/{H.params['m2']} M_solar:")
# logging.info('BS power: {: >0.3f} W'.format(ifo.gwinc.pbs)) for metric, (value, unit) in metrics.items():
# logging.info('arm finesse: {: >0.3f}'.format(ifo.gwinc.finesse)) if unit is None:
# logging.info('arm power: {: >0.3f} kW'.format(ifo.gwinc.parm/1000)) unit = ''
print(f" {metric}: {value:0.1f} {unit}")
if args.fom: range_func = 'range'
logging.info("calculating inspiral {}...".format(range_func)) subtitle = 'inspiral {func} {m1}/{m2} $\mathrm{{M}}_\odot$: {fom:.0f} {unit}'.format(
H = inspiral_range.CBCWaveform(freq, **range_params)
logging.debug("params: {}".format(H.params))
fom = eval('inspiral_range.{}'.format(range_func))(freq, noises['Total'], H=H)
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
logging.info("saving budget traces {}...".format(args.save)) else:
if ifo: plot_style['title'] = "GWINC Noise Budget: {}".format(name)
plot_style['IFO'] = ifo.to_yaml()
io.save_hdf5( ##########
path=args.save, # interactive
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()
......