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
build/
tresults*/
test_results*/
# test cache
gwinc/test/cache
test/*/*.h5
gwinc.egg-info/
.eggs/
gwinc/_version.py
# Byte-compiled / optimized / DLL files
__pycache__/
......@@ -18,3 +27,6 @@ __pycache__/
MANIFEST
# Mac OS Files
*.DS_Store
image: ligo/software:stretch
stages:
- test
- deploy
- dist
- 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
before_script:
- echo $CI_COMMIT_SHA | cut -b1-8 > gitID.txt
image: $CI_REGISTRY_IMAGE/gwinc/base:$CI_COMMIT_REF_NAME
script:
- apt-get update -qq
- apt-get install -y -qq python3-yaml python3-scipy python3-matplotlib python3-ipython lalsimulation-python3 python3-pypdf2
- git clone https://gitlab-ci-token:ci_token@git.ligo.org/gwinc/inspiral_range.git
- export PYTHONPATH=inspiral_range
- export MPLBACKEND=agg
- for ifo in aLIGO Aplus Voyager CE1 CE2; do
- 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
- mkdir -p ifo
- export PYTHONPATH=/inspiral_range
- for ifo in $(python3 -c "import gwinc; print(' '.join(gwinc.IFOS))"); do
- python3 -m gwinc $ifo -s ifo/$ifo.png -s ifo/$ifo.h5
- done
- python3 -m gwinc.ifo -s ifo/all_compare.png
artifacts:
when: always
expire_in: 4w
paths:
- aLIGO.png
- Aplus.png
- Voyager.png
- CE1.png
- CE2.png
- gwinc_test_report.pdf
- ifo
# 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:
stage: deploy
dependencies:
- test
only:
- master
needs:
- job: generate_budgets
artifacts: true
image: $CI_REGISTRY_IMAGE/gwinc/base:$CI_COMMIT_REF_NAME
script:
- mkdir public
- for ifo in aLIGO Aplus Voyager CE1 CE2; do
- mv $ifo.png public/
- done
- mv gwinc_test_report.pdf public/ || true
- apt-get install -y -qq python3-pip python3-dev make
- pip3 install sphinx sphinx-rtd-theme
- cd docs
- make html
- cd ..
- mv ./build/sphinx/html/* public/
- rm -rf public
- apt-get install -y -qq python3-sphinx-rtd-theme
- cd docs
- make html
- cd ..
- 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:
paths:
- public
expire_in: 4w
only:
- master
- dist/*
......@@ -2,11 +2,48 @@
The pygwinc project welcomes your contributions. Our policy is that
all contributions should be peer-reviewed. To facilitate the review,
please do not commit your work to this repository yourself. Instead,
fork the repository and send a merge request.
If your change affects the shape of a noise curve, your commit message
should make note of that, and provide a justification. It will also
be necessary to update the reference curves stored in DCC entry
[T18xxxxx](https://dcc.ligo.org/LIGO-T18xxxxx), after the change is
applied.
please do not commit your work to this repository directly. Instead,
please fork the repository and create a [merge
request](https://git.ligo.org/gwinc/pygwinc/-/merge_requests/new)
against the main pygwinc master branch.
When submitting code for merge, please follow good coding practice.
Respect the existing coding style, which for `pygwinc` is standard
[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
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
is organized into a heirarchical folder-like structure, with two
......@@ -17,53 +18,22 @@ pairs.
Bindings are available for most platforms including Python [1] and
Matlab.
[0] https://en.wikipedia.org/wiki/Hierarchical_Data_Format
[1] http://www.h5py.org/
## version history
v1
- first versioned schema release
* [0] https://en.wikipedia.org/wiki/Hierarchical_Data_Format
* [1] http://www.h5py.org/
## schema
The following describes the noise budget schema. Specific strings are
enclosed in single quotes (''), and variables are described in
brackets (<>). Group objects are indicated by a closing '/'
separator, data set are indicated by a closing ':' followed by a
specification of their length and type (e.g. "(N),float"), and
attributes are specified in the .attrs[] dictionary format. Optional
elements are enclosed in 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 -->
<!-- ... -->
<!-- ``` -->
brackets (<>). Group objects are indicated by a closing '/', data
sets are indicated by a closing ':' followed by a specification of
their length and type (e.g. "(N),float"), and attributes are specified
in the .attrs[] dictionary format. Optional elements are enclosed in
parentheses.
## Top-level Budget
## top-level attributes
The following two root attributes expected: a string describing the schema,
and an int schema version number:
......@@ -72,33 +42,81 @@ and an int schema version number:
/.attrs['SCHEMA_VERSION'] = 1
```
Top-level attributes are generally used for specifying overall plot styling, but the
following root attributes are typically defined:
The following root root attributes are defined:
```
/.attrs['title'] = <experiment description string (e.g. 'H1 Strain Budget')>
/.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
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/
/'Total': (N),float
/<noise_0>: (N),float
/<noise_1>: (N),float
/<noise_2>/
/'Total': (N),float
/<noise_3>: (N),float
/<noise_4>: (N),float
...
/traces/'Total': (N),float
/traces/<noise_0>: (N),float
/traces/<noise_1>: (N),float
/traces/<noise_2>/
/traces/<noise_2>/'Total': (N),float
/traces/<noise_2>/<noise_3>: (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
include setup.cfg
include tox.ini
recursive-include gwinc *.yaml
recursive-include matlab *.m
recursive-include docs *
exclude IFO.md
global-exclude *.git*
prune docs
prune matlab
[![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
provided in either .yml or .mat files (see below). Once the detector
description is loaded, the noise budget can be calculated and plotted:
For custom plotting, parameter optimization, etc. all functionality can be
accessed directly through the `gwinc` library interface:
```python
>>> 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
>>> freq = np.logspace(1, 3, 1000)
>>> Budget = gwinc.load_budget('aLIGO')
>>> ifo = gwinc.precompIFO(freq, Budget.ifo)
>>> traces = Budget(freq, ifo=ifo).run()
>>> fig = gwinc.plot_noise(freq, traces)
>>> fig.show()
>>> budget = gwinc.load_budget('aLIGO', freq=freq)
```
or frequency specification string ('FLO:[NPOINTS:]FHI'):
```python
>>> 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
the package directly:
```shell
$ python3 -m gwinc aLIGO
## noise functions
The `pygwinc` analytical noise functions are available in the
`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
package/module, .yaml YAML file, and MATLAB gwinc .mat or .m files.
Additionally `BudgetItem`s have two other methods, `load` and
`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)
* [A+](https://git.ligo.org/gwinc/pygwinc/blob/master/gwinc/ifo/Aplus)
* [Voyager](https://git.ligo.org/gwinc/pygwinc/blob/master/gwinc/ifo/Voyager)
* [Cosmic Explorer](https://git.ligo.org/gwinc/pygwinc/blob/master/gwinc/ifo/CE)
Generally these methods are not called directly. Instead, the `Noise`
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`)
## 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
# MyBudget/__init__.py
import numpy as np
from gwinc import nb
from gwinc import noise
class ExcessGas(nb.Noise):
"""Excess gas"""
class SuspensionThermal(nb.Noise):
"""Suspension thermal noise"""
style = dict(
label='Excess Gas',
label='Suspension Thermal',
color='#ad900d',
linestyle='--',
)
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):
"""My measured noise"""
style = dict(
label='Measured Noise',
color='#838209',
......@@ -79,40 +324,184 @@ class MeasuredNoise(nb.Noise):
def load(self):
psd, freq = np.loadtxt('/path/to/measured/psd.txt')
self.data = self.interpolate(f, psd)
self.data = self.interpolate(freq, psd)
def calc(self):
return self.data
class MyCalibration(nb.Calibration):
def calc(self):
return np.ones_like(self.freq) * 1234
class MyBudget(nb.Budget):
noises = [
ExcessGas,
SuspensionThermal,
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
MATLAB python interface (see the CLI '--matlab' option above). This
also allows for easy direct comparison between the pygwinc and
matgwinc noise budgets.
Other than the necessary `freq` initialization argument that defines
the frequency array, any additional keyword arguments are assigned as
class attributes to the budget object, and to all of it's constituent
sub noises/calibrations/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
Note that the `SuspensionThermal` Noise class above uses the
`suspension_thermal` analytic noise calculation function, which takes
a "suspension" Struct as input argument. In this case, this
suspension Struct is extracted from the `self.ifo` Struct at
`self.ifo.Suspension`.
If a budget module defined as a package includes an `ifo.yaml`
[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)
* [A+ comparison](https://gwinc.docs.ligo.org/pygwinc/A+_test.png)
<!-- * [aLIGO comparison](https://gwinc.docs.ligo.org/pygwinc/aLIGO_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:
-rm -rf $(BUILDDIR)/*
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:
$(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 @@
# 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.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
import os
import sys
#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 ------------------------------------------------
......@@ -33,11 +39,12 @@
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.todo',
'sphinx.ext.coverage',
#'sphinx.ext.todo',
#'sphinx.ext.coverage',
'sphinx.ext.mathjax',
'sphinx.ext.githubpages',
#'sphinx.ext.githubpages',
'sphinx.ext.napoleon',
'sphinx.ext.viewcode',
]
# Add any paths that contain templates here, relative to this directory.
......@@ -54,7 +61,7 @@ master_doc = 'index'
# General information about the project.
project = 'GWINC'
copyright = '2018, LIGO Laboratory'
copyright = '2021, LIGO Laboratory'
author = 'LIGO Laboratory'
# The version info for the project you're documenting, acts as replacement for
......@@ -62,7 +69,7 @@ author = 'LIGO Laboratory'
# built documents.
#
# The short X.Y version.
version = '0.0.0'
version = gwinc.__version__
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
......@@ -74,7 +81,7 @@ language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# 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.
#pygments_style = 'sphinx'
......@@ -84,9 +91,10 @@ pygments_style = 'default'
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = True
# Autodoc settings
autodoc_default_flags = ["members", "undoc-members"]
# -- Options for sourcelinks
srclink_project = 'https://git.ligo.org/gwinc/pygwinc'
srclink_src_path = 'gwinc'
srclink_branch = 'master'
......@@ -96,24 +104,41 @@ srclink_branch = 'master'
#useful for downloading the ipynb files
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
# a list of builtin themes.
#
import sphinx_rtd_theme
html_theme = "sphinx_rtd_theme"
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
#import sphinx_rtd_theme
#html_theme = "sphinx_rtd_theme"
#html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
#import jupyter_sphinx_theme
#html_theme = "jupyter_sphinx_theme"
#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
# further. For a list of options available for each theme, see the
# 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,
# relative to this directory. They are copied after the builtin static files,
......@@ -125,118 +150,24 @@ html_static_path = ['_static']
#
# This is required for the alabaster theme
# 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 = {
'**': [
'localtoc.html',
'about.html',
#'globaltoc.html',
'navigation.html',
'relations.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.
htmlhelp_basename = 'GWINC'
html_logo = 'logo/LIGO_F0900035-v1.jpg'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# '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')
def setup(app):
app.add_stylesheet('my_theme.css')
#updates the coloring
#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
:maxdepth: 2
install
quickstart
api
dev
Install
################################################################################
.. _install:
docs/logo/LIGO_F0900035-v1.jpg

267 KiB

*.mat
.ipynb_checkpoints/
from __future__ import division
import os
import sys
import logging
import importlib
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 .precomp import precompIFO
from .plot import plot_trace
from .plot import plot_budget
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.
Return loaded module and module path.
......@@ -24,6 +70,7 @@ def _load_module(name_or_path):
path = os.path.join(path, '__init__.py')
spec = importlib.util.spec_from_file_location(modname, path)
mod = importlib.util.module_from_spec(spec)
sys.modules[mod.__name__] = mod
spec.loader.exec_module(mod)
else:
mod = importlib.import_module(name_or_path)
......@@ -34,70 +81,105 @@ def _load_module(name_or_path):
return mod, path
def load_budget(name_or_path):
"""Load GWINC IFO Budget by name or from path.
def load_budget(name_or_path, freq=None, bname=None):
"""Load GWINC Budget
Named IFOs should correspond to one of the IFOs available in the
gwinc package (see gwinc.available_ifos()). If a path is provided
it should either be a budget package (directory) or module (ending
in .py), or an IFO struct definition (see gwinc.Struct).
Accepts either the name of a built-in canonical budget (see
gwinc.IFOS), the path to a budget package (directory) or module
(ending in .py), or the path to an IFO Struct definition file (see
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
'ifo.yaml' file, that file will be loaded into a Struct and
assigned as an attribute to the returned Budget class.
If `bname` is specified the Budget class with that name will be
loaded from the budget module. Otherwise, the Budget class with
the same name as the budget module will be loaded.
If a bare struct is provided the base aLIGO budget definition will
be assumed.
If the budget is a package directory which includes an 'ifo.yaml'
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
structure, frequency array, and plot style dictionary, with the
last three being None if they are not defined in the budget.
Returns the instantiated Budget object. If a frequency array or
frequency specification string (see `freq_from_spec()`) is
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
if os.path.exists(name_or_path):
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:
logging.info("loading struct {}...".format(path))
ifo = Struct.from_file(path)
bname = 'aLIGO'
modname = 'gwinc.ifo.aLIGO'
logging.info("loading budget {}...".format(modname))
logger.info("loading struct {}...".format(path))
ifo = Struct.from_file(path, _pass_inherit=True)
inherit_ifo = ifo.get('+inherit', None)
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:
modname = path
logging.info("loading module path {}...".format(modname))
raise RuntimeError(
"Unknown file type: {} (supported types: {}).".format(
ext, Struct.STRUCT_EXT))
else:
if name_or_path not in available_ifos():
raise RuntimeError("Unknonw IFO '{}' (available IFOs: {}).".format(
if name_or_path not in IFOS:
raise RuntimeError("Unknown IFO '{}' (available IFOs: {}).".format(
name_or_path,
available_ifos(),
IFOS,
))
bname = name_or_path
modname = 'gwinc.ifo.'+name_or_path
logging.info("loading module {}...".format(modname))
mod, modpath = _load_module(modname)
bname = bname or name_or_path
modname = 'gwinc.ifo.' + name_or_path
logger.info(f"loading budget '{bname}' from {modname}...")
mod, modpath = load_module(modname)
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')
if not ifo and ifopath:
if not ifo and os.path.exists(ifopath):
ifo = Struct.from_file(ifopath)
Budget.ifo = ifo
return Budget
return Budget(freq=freq, ifo=ifo)
def gwinc(freq, ifo, source=None, plot=False, PRfixed=True):
"""Calculate strain noise budget for a specified interferometer model.
Argument `freq` is the frequency array for which the noises will
be calculated, and `ifoin` is the IFO model (see the `load_ifo()`
function).
be calculated, and `ifo` is the IFO model (see the `load_budget()`
function). The nominal 'aLIGO' budget structure will be used.
If `source` structure provided, so evaluates the sensitivity of
the detector to several potential gravitational wave
......@@ -111,30 +193,29 @@ def gwinc(freq, ifo, source=None, plot=False, PRfixed=True):
# assume generic aLIGO configuration
# FIXME: how do we allow adding arbitrary addtional noise sources
# from just ifo description, without having to specify full budget
Budget = load_budget('aLIGO')
ifo = precompIFO(freq, ifo, PRfixed)
traces = Budget(freq, ifo=ifo).run()
plot_style = getattr(Budget, 'plot_style', {})
budget = load_budget('aLIGO', freq)
traces = budget.run()
plot_style = getattr(budget, 'plot_style', {})
# construct matgwinc-compatible noises structure
noises = {}
for name, (data, style) in traces.items():
noises[style.get('label', name)] = data
noises['Freq'] = freq
for name, trace in traces.items():
noises[name] = trace.psd
noises['Total'] = traces.psd
noises['Freq'] = traces.freq
pbs = ifo.gwinc.pbs
parm = ifo.gwinc.parm
finesse = ifo.gwinc.finesse
prfactor = ifo.gwinc.prfactor
if ifo.Laser.Power * prfactor != pbs:
pass
#warning(['Thermal lensing limits input power to ' num2str(pbs/prfactor, 3) ' W']);
logger.warning("Thermal lensing limits input power to {} W".format(pbs/prfactor))
# report astrophysical scores if so desired
score = None
if source:
logging.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("Source score calculation currently not supported. See `inspiral-range` package for similar functionality:")
logger.warning("https://git.ligo.org/gwinc/inspiral-range")
# score = int731(freq, noises['Total'], 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):
# output graphics
if plot:
logging.info('Laser Power: %7.2f Watt' % ifo.Laser.Power)
logging.info('SRM Detuning: %7.2f degree' % (ifo.Optics.SRM.Tunephase*180/pi))
logging.info('SRM transmission: %9.4f' % ifo.Optics.SRM.Transmittance)
logging.info('ITM transmission: %9.4f' % ifo.Optics.ITM.Transmittance)
logging.info('PRM transmission: %9.4f' % ifo.Optics.PRM.Transmittance)
logging.info('Finesse: %7.2f' % finesse)
logging.info('Power Recycling Gain: %7.2f' % prfactor)
logging.info('Arm Power: %7.2f kW' % (parm/1000))
logging.info('Power on BS: %7.2f W' % pbs)
logger.info('Laser Power: %7.2f Watt' % ifo.Laser.Power)
logger.info('SRM Detuning: %7.2f degree' % (ifo.Optics.SRM.Tunephase*180/np.pi))
logger.info('SRM transmission: %9.4f' % ifo.Optics.SRM.Transmittance)
logger.info('ITM transmission: %9.4f' % ifo.Optics.ITM.Transmittance)
logger.info('PRM transmission: %9.4f' % ifo.Optics.PRM.Transmittance)
logger.info('Finesse: %7.2f' % finesse)
logger.info('Power Recycling Gain: %7.2f' % prfactor)
logger.info('Arm Power: %7.2f kW' % (parm/1000))
logger.info('Power on BS: %7.2f W' % pbs)
# coating and substrate thermal load on the ITM
PowAbsITM = (pbs/2) * \
np.hstack([(finesse*2/np.pi) * ifo.Optics.ITM.CoatingAbsorption,
(2 * ifo.Materials.MassThickness) * ifo.Optics.ITM.SubstrateAbsorption])
logging.info('Thermal load on ITM: %8.3f W' % sum(PowAbsITM))
logging.info('Thermal load on BS: %8.3f W' %
(ifo.Materials.MassThickness*ifo.Optics.SubstrateAbsorption*pbs))
PowAbsITM = (
(pbs/2)
* np.hstack([
(finesse*2/np.pi) * ifo.Optics.ITM.CoatingAbsorption,
(2 * ifo.Materials.MassThickness) * ifo.Optics.ITM.SubstrateAbsorption])
)
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):
logging.info('Lensing limited input power: %7.2f W' % (pbs/prfactor))
logger.info('Lensing limited input power: %7.2f W' % (pbs/prfactor))
if score:
logging.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))
logging.info('Stochastic Omega: %4.1g Universes' % score.Omega)
logger.info('BNS Inspiral Range: ' + str(score.effr0ns) + ' Mpc/ z = ' + str(score.zHorizonNS))
logger.info('BBH Inspiral Range: ' + str(score.effr0bh) + ' Mpc/ z = ' + str(score.zHorizonBH))
logger.info('Stochastic Omega: %4.1g Universes' % score.Omega)
plot_noise(ifo, traces, **plot_style)
traces.plot(**plot_style)
return score, noises, ifo
from __future__ import print_function
import os
import signal
import argparse
import numpy as np
from IPython.terminal.embed import InteractiveShellEmbed
import logging
logging.basicConfig(format='%(message)s',
level=os.getenv('LOG_LEVEL', logging.INFO))
import argparse
from . import available_ifos, load_budget, plot_noise
from .precomp import precompIFO
from . import (
__version__,
IFOS,
DEFAULT_FREQ,
InvalidFrequencySpec,
load_budget,
logger,
)
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():
# description += " '{}'\n".format(ifo)
description += """
By default a GWINC noise budget of the specified IFO will calculated,
and plotted with an interactive plotter. If the --save option is
specified the plot will be saved directly to a file (without display)
(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.
By default the noise budget of the specified IFO will be loaded and
plotted with an interactive plotter. Individual IFO parameters can be
overriden with the --ifo option:
gwinc --ifo Optics.SRM.Tunephase=3.14 ...
If the inspiral_range package is installed, various figures of merit
can be calculated for the resultant spectrum with the --fom argument,
e.g.:
If the --save option is specified the plot will be saved directly to a
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.
gwinc --fom horizon ...
gwinc --fom range:m1=20,m2=20 ...
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:
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'
FLO = 5
FHI = 6000
NPOINTS = 3000
RANGE_PARAMS = dict(m1=1.4, m2=1.4)
DATA_SAVE_FORMATS = ['.hdf5', '.h5']
parser = argparse.ArgumentParser(
prog='gwinc',
description=description,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('--flo', '-fl', default=FLO, type=float,
help="lower frequency bound in Hz [{}]".format(FLO))
parser.add_argument('--fhi', '--fh', default=FHI, type=float,
help="upper frequency bound in Hz [{}]".format(FHI))
parser.add_argument('--npoints', '-n', default=NPOINTS,
help="number of frequency points [{}]".format(NPOINTS))
parser.add_argument('--title', '-t',
help="plot title")
parser.add_argument('--fom',
help="calculate inspiral range for resultant spectrum ('func[:param=val,param=val]')")
parser.add_argument(
'--version', '-v', action='version', version=__version__)
parser.add_argument(
'--freq', '-f', metavar='FLO:[NPOINTS:]FHI',
help="logarithmic frequency array specification in Hz [{}]".format(DEFAULT_FREQ))
parser.add_argument(
'--ifo', '-o', metavar='PARAM=VAL', default=[],
#nargs='+', action='extend',
action='append',
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.add_argument('--interactive', '-i', action='store_true',
help="interactive plot with interactive shell")
group.add_argument('--save', '-s',
help="save budget traces (.hdf5/.h5) or plot (.pdf/.png/.svg) to file")
group.add_argument('--yaml', '-y', action='store_true',
help="print IFO as yaml to stdout and exit")
group.add_argument('--text', '-x', action='store_true',
help="print IFO as text table to stdout and exit")
group.add_argument('--diff', '-d', metavar='IFO',
help="show differences table between another IFO description")
group.add_argument('--no-plot', '-np', action='store_false', dest='plot',
help="supress plotting")
parser.add_argument('IFO',
help="IFO name, description file path (.yaml, .mat, .m), budget module (.py), or HDF5 data file (.hdf5, .h5)")
group.add_argument(
'--interactive', '-i', action='store_true',
help="launch interactive shell after budget processing")
group.add_argument(
'--save', '-s', metavar='PATH', action='append',
help="save plot (.png/.pdf/.svg) or budget traces (.hdf5/.h5) to file (may be specified multiple times)")
group.add_argument(
'--yaml', '-y', action='store_true',
help="print IFO as yaml to stdout and exit (budget not calculated)")
group.add_argument(
'--text', '-x', action='store_true',
help="print IFO as text table to stdout and exit (budget not calculated)")
group.add_argument(
'--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():
......@@ -89,55 +126,90 @@ def main():
##########
# initial arg processing
if os.path.splitext(os.path.basename(args.IFO))[1] in ['.hdf5', '.h5']:
Budget = None
freq, traces, attrs = io.load_hdf5(args.IFO)
ifo = getattr(attrs, 'IFO', None)
plot_style = attrs
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
budget = None
name = args.IFO
trace = load_hdf5(args.IFO)
freq = trace.freq
ifo = trace.ifo
plot_style = trace.plot_style
else:
Budget = load_budget(args.IFO)
ifo = Budget.ifo
# FIXME: this should be done only if specified, to allow for
# using any FREQ specified in the Budget
freq = np.logspace(np.log10(args.flo), np.log10(args.fhi), args.npoints)
plot_style = getattr(Budget, 'plot_style', {})
traces = None
try:
budget = load_budget(args.IFO, freq=args.freq, bname=args.bname)
except InvalidFrequencySpec as e:
parser.error(e)
except RuntimeError as e:
parser.exit(2, f"Error: {e}\n")
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 not ifo:
parser.exit(2, "no IFO structure available.")
parser.exit(2, "Error: IFO structure not provided.\n")
print(ifo.to_yaml(), end='')
return
if args.text:
if not ifo:
parser.exit(2, "no IFO structure available.")
parser.exit(2, "Error: IFO structure not provided.\n")
print(ifo.to_txt(), end='')
return
if args.diff:
if not ifo:
parser.exit(2, "no IFO structure available.")
fmt = '{:30} {:>20} {:>20}'
Budget = load_ifo(args.diff)
diffs = ifo.diff(ifoo)
parser.exit(2, "Error: IFO structure not provided.\n")
dbudget = load_budget(args.diff)
diffs = ifo.diff(dbudget.ifo)
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('', '-----', '-----'))
for p in diffs:
k = str(p[0])
v = repr(p[1])
ov = repr(p[2])
print(fmt.format(k, v, ov))
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:
plot_style['title'] = args.title
elif Budget:
plot_style['title'] = "GWINC Noise Budget: {}".format(Budget.name)
else:
plot_style['title'] = "GWINC Noise Budget: {}".format(args.IFO)
if args.plot:
if args.save:
if args.subbudget:
try:
budget[args.subbudget]
except KeyError:
parser.exit(3, f"Error: Unknown budget item '{args.subbudget}'.\n")
out_data_files = set()
out_plot_files = set()
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
# matplotlib usable on systems without a display. There must
# be a better way. 'AGG' is a backend that works without
......@@ -149,111 +221,150 @@ def main():
matplotlib.use('AGG')
try:
from matplotlib import pyplot as plt
except ImportError as e:
parser.exit(5, f"ImportError: {e}\n")
except RuntimeError:
logging.warning("no display, plotting disabled.")
args.plot = False
parser.exit(10, "Error: Could not open display for plotting.\n")
if args.fom:
import inspiral_range
if args.range:
try:
range_func, fargs = args.fom.split(':')
except ValueError:
range_func = args.fom
fargs = ''
range_params = {}
for param in fargs.split(','):
if not param:
continue
p,v = param.split('=')
if not v:
raise ValueError('missing parameter value "{}"'.format(p))
if p != 'approximant':
v = float(v)
range_params[p] = v
import inspiral_range
except ImportError as e:
parser.exit(5, f"ImportError: {e}\n")
logger_ir = logging.getLogger('inspiral_range')
logger_ir.setLevel(logger.getEffectiveLevel())
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter('%(name)s: %(message)s'))
logger_ir.addHandler(handler)
for paramval in args.waveform_parameter:
try:
param, val = paramval.split('=')
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
if not traces:
if ifo:
logging.info("precomputing ifo...")
ifo = precompIFO(freq, ifo)
logging.info("calculating budget...")
traces = Budget(freq=freq, ifo=ifo).run()
# logging.info('recycling factor: {: >0.3f}'.format(ifo.gwinc.prfactor))
# logging.info('BS power: {: >0.3f} W'.format(ifo.gwinc.pbs))
# logging.info('arm finesse: {: >0.3f}'.format(ifo.gwinc.finesse))
# logging.info('arm power: {: >0.3f} kW'.format(ifo.gwinc.parm/1000))
if args.fom:
logging.info("calculating inspiral {}...".format(range_func))
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(
if not trace:
logger.info("calculating budget...")
trace = budget.run()
if args.range:
logger.info("calculating inspiral ranges...")
metrics, H = inspiral_range.all_ranges(freq, trace.psd, **RANGE_PARAMS)
print(f"{H.params['approximant']} {H.params['m1']}/{H.params['m2']} M_solar:")
for metric, (value, unit) in metrics.items():
if unit is None:
unit = ''
print(f" {metric}: {value:0.1f} {unit}")
range_func = 'range'
subtitle = 'inspiral {func} {m1}/{m2} $\mathrm{{M}}_\odot$: {fom:.0f} {unit}'.format(
func=range_func,
m1=H.params['m1'],
m2=H.params['m2'],
fom=fom,
)
plot_style['title'] += '\n{}'.format(fom_title)
fom=metrics[range_func][0],
unit=metrics[range_func][1] or '',
)
else:
subtitle = None
##########
# output
if args.subbudget:
trace = trace[args.subbudget]
name += f': {args.subbudget}'
# save noise traces to HDF5 file
if args.save and os.path.splitext(args.save)[1] in ['.hdf5', '.h5']:
logging.info("saving budget traces {}...".format(args.save))
if ifo:
plot_style['IFO'] = ifo.to_yaml()
io.save_hdf5(
path=args.save,
freq=freq,
traces=traces,
**plot_style
)
if args.title:
plot_style['title'] = args.title
else:
plot_style['title'] = "GWINC Noise Budget: {}".format(name)
##########
# interactive
# interactive shell plotting
elif args.interactive:
ipshell = InteractiveShellEmbed(
if args.interactive:
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={
'freq': freq,
'traces': traces,
'ifo': ifo,
'budget': budget,
'trace': trace,
'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")
>>> plt.savefig("foo.pdf")
''')
ipshell.enable_pylab()
ipshell.run_code("plot_noise(freq, traces, **plot_style)")
ipshell.run_code("plt.title('{}')".format(plot_style['title']))
ipshell()
# save noise trace to HDF5 file
if out_data_files:
for path in out_data_files:
logger.info("saving budget trace: {}".format(path))
io.save_hdf5(
trace=trace,
path=path,
ifo=ifo,
plot_style=plot_style,
)
# standard plotting
elif args.plot:
logging.info("plotting noises...")
if args.plot or out_plot_files:
logger.debug("plotting noises...")
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
plot_noise(
freq,
traces,
if subtitle:
plot_style['title'] += '\n' + subtitle
trace.plot(
ax=ax,
**plot_style
)
fig.tight_layout()
if args.save:
fig.savefig(
args.save,
)
if out_plot_files:
for path in out_plot_files:
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:
plt.show()
......