Skip to content
Snippets Groups Projects

Compare revisions

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

Source

Select target project
No results found

Target

Select target project
  • wenxuan.jia/pygwinc
  • sean-leavey/pygwinc
  • sebastian.steinlechner/pygwinc
  • nicholas.demos/pygwinc
  • chris.whittle/pygwinc
  • raymond.robie/pygwinc
  • mateusz.bawaj/pygwinc
  • anchal.gupta/pygwinc
  • 40m/pygwinc
  • evan.hall/pygwinc
  • kevin.kuns/pygwinc
  • geoffrey-lovelace/pygwinc
  • brittany.kamai/pygwinc
  • daniel-brown/pygwinc
  • lee-mcculler/pygwinc
  • jameson.rollins/pygwinc
  • gwinc/pygwinc
17 results
Show changes
Commits on Source (325)
[flake8]
ignore = E226,E741,E266,W503
max-line-length = 140
# E226, missing whitespace around arithmetic operator: quantum.py currently needs large changes to avoid this
# E741, "l" is a bad variable name: Gwinc uses "l" in a few reasonable places
# E266, Too many leading '#' for block comment: There are some reasonable instances of comments that trigger this
# W503, binary operator at start of line: This allows using parentheses to break equations
#for docs and setup.py outputs
build/
tresults*/
test_results*/
# test cache
gwinc/test/cache
test/*/*.h5
gwinc.egg-info/
.eggs/
gwinc/_version.py
# Byte-compiled / optimized / DLL files
__pycache__/
......
image: igwn/base:buster
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:
- rm -rf ifo gwinc_test_report.pdf
- mkdir ifo
- apt-get update -qq
- apt-get install -y -qq git python3-yaml python3-scipy python3-matplotlib python3-ipython lalsimulation-python3 python3-pypdf2 python3-h5py
- git clone https://gitlab-ci-token:ci_token@git.ligo.org/gwinc/inspiral_range.git
- export PYTHONPATH=inspiral_range
- export MPLBACKEND=agg
- python3 -m gwinc.test -r gwinc_test_report.pdf
- for ifo in aLIGO Aplus Voyager CE1 CE2; do
- python3 -m gwinc $ifo -s ifo/$ifo.png
- python3 -m gwinc $ifo -s ifo/$ifo.h5
- done
- python3 -m gwinc.ifo -s ifo/all_compare.png
after_script:
- rm gitID.txt
cache:
key: "$CI_PROJECT_NAMESPACE:$CI_PROJECT_NAME:$CI_JOB_NAME"
untracked: true
- 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:
- ifo
- 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:
- rm -rf public
- mv ifo public
- apt-get install -y -qq python3-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,25 +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.
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.
`pygwinc` comes with a test suite that will calculate all canonical
IFO budgets with the current state of the code, and compare them
against cached hdf5 traces (stored in the repo with
[git-lfs](https://git-lfs.github.com/) ( [see
also](https://wiki.ligo.org/Computing/GitLFS)). Contributors should
run the tests before submitting any merge requests:
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
```
The test suite will be run automatically upon push to git.ligo.org as
part of the continuous integration, and code that fails the CI will
not be merged, unless failures are well justified.
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
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 cache, but that
should be done in a separate commit not a part of the original merge
request, so that all reviewers can see the changes introduced in the
CI test failure report.
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/...
```
......@@ -2,43 +2,55 @@
CI-generated plots and data for all IFOs included in pygwinc.
![IFO compare](https://gwinc.docs.ligo.org/pygwinc/all_compare.png)
Ranges in "Mpc" are for binary neutron stars (BNS) using the
[inspiral_range](gwinc/inspiral-range>) package.
![IFO compare](https://gwinc.docs.ligo.org/pygwinc/ifo/all_compare.png)
## aLIGO
* [ifo.yaml](gwinc/ifo/aLIGO/ifo.yaml)
* [aLIGO.h5](https://gwinc.docs.ligo.org/pygwinc/aLIGO.h5)
* [aLIGO.h5](https://gwinc.docs.ligo.org/pygwinc/ifo/aLIGO.h5)
![aLIGO](https://gwinc.docs.ligo.org/pygwinc/aLIGO.png)
![aLIGO](https://gwinc.docs.ligo.org/pygwinc/ifo/aLIGO.png)
## A+
* [ifo.yaml](gwinc/ifo/Aplus/ifo.yaml)
* [Aplus.h5](https://gwinc.docs.ligo.org/pygwinc/Aplus.h5)
* [Aplus.h5](https://gwinc.docs.ligo.org/pygwinc/ifo/Aplus.h5)
![Aplus](https://gwinc.docs.ligo.org/pygwinc/Aplus.png)
![Aplus](https://gwinc.docs.ligo.org/pygwinc/ifo/Aplus.png)
## Voyager
* [ifo.yaml](gwinc/ifo/Voyager/ifo.yaml)
* [Voyager.h5](https://gwinc.docs.ligo.org/pygwinc/Voyager.h5)
* [Voyager.h5](https://gwinc.docs.ligo.org/pygwinc/ifo/Voyager.h5)
![Voyager](https://gwinc.docs.ligo.org/pygwinc/Voyager.png)
![Voyager](https://gwinc.docs.ligo.org/pygwinc/ifo/Voyager.png)
## Cosmic Explorer 1
* [ifo.yaml](gwinc/ifo/CE1/ifo.yaml)
* [CE1.h5](https://gwinc.docs.ligo.org/pygwinc/CE1.h5)
* [CE1.h5](https://gwinc.docs.ligo.org/pygwinc/ifo/CE1.h5)
![CE1](https://gwinc.docs.ligo.org/pygwinc/ifo/CE1.png)
## Cosmic Explorer 2 (Silica)
* [ifo.yaml](gwinc/ifo/CE2silica/ifo.yaml)
* [CE2silica.h5](https://gwinc.docs.ligo.org/pygwinc/ifo/CE2silica.h5)
![CE1](https://gwinc.docs.ligo.org/pygwinc/CE1.png)
![CE2 (Silica)](https://gwinc.docs.ligo.org/pygwinc/ifo/CE2silica.png)
## Cosmic Explorer 2
## Cosmic Explorer 2 (Silicon)
* [ifo.yaml](gwinc/ifo/CE2/ifo.yaml)
* [CE2.h5](https://gwinc.docs.ligo.org/pygwinc/CE2.h5)
* [ifo.yaml](gwinc/ifo/CE2silicon/ifo.yaml)
* [CE2silicon.h5](https://gwinc.docs.ligo.org/pygwinc/ifo/CE2silicon.h5)
![CE2](https://gwinc.docs.ligo.org/pygwinc/CE2.png)
![CE2 (Silicon)](https://gwinc.docs.ligo.org/pygwinc/ifo/CE2silicon.png)
include CONTRIBUTIONS.md
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
......@@ -2,12 +2,14 @@
# Python Gravitational Wave Interferometer Noise Calculator
![gwinc](https://gwinc.docs.ligo.org/pygwinc/aLIGO.png)
[![aLIGO](https://gwinc.docs.ligo.org/pygwinc/ifo/aLIGO.png "Canonical
IFOs")](IFO.md)
`pygwinc` is a multi-faceted tool for processing and plotting noise
budgets for ground-based gravitational wave detectors. It's primary
feature is a collection of mostly analytic noise calculation functions
for various sources of noise affecting detectors (`gwinc.noise`):
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
......@@ -17,30 +19,29 @@ for various sources of noise affecting detectors (`gwinc.noise`):
* Newtonian/gravity-gradient noise
* residual gas noise
See [noise functions](#noise-functions) below.
`pygwinc` also includes 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` 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 detectors (`gwinc.ifo`):
and future GW detectors (`gwinc.ifo`):
* [aLIGO](https://gwinc.docs.ligo.org/pygwinc/aLIGO.png)
* [A+](https://gwinc.docs.ligo.org/pygwinc/Aplus.png)
* [Voyager](https://gwinc.docs.ligo.org/pygwinc/Voyager.png)
* [Cosmic Explorer 1](https://gwinc.docs.ligo.org/pygwinc/CE1.png)
* [Cosmic Explorer 2](https://gwinc.docs.ligo.org/pygwinc/CE2.png)
* [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
[figures of merit](#figures-of-merit) section below.
figures of merit for gravitational wave detector budgets. See the
[inspiral range](#inspiral-range) section below.
## usage
......@@ -48,74 +49,129 @@ figures of merit for gravitational wave detector budgets. See
### command line interface
`pygwinc` provides a command line interface that can be used to
calculate and plot noise budgets for generic noise budgets or the
various canonical IFOs described above, save/plot hdf5 trace data, and
dump budget IFO parameters:
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
$ python3 -m gwinc aLIGO
$ gwinc --save aLIGO.png aLIGO
```
Or trace data can be saved to an
[HDF5](https://en.wikipedia.org/wiki/Hierarchical_Data_Format) file:
```shell
$ gwinc --save aLIGO.hdf5 aLIGO
```
Trace HDF5 files can also be plotted directly:
```shell
$ gwinc aLIGO.hdf5
```
You can play with IFO parameters and see the effects on the budget by
dumping the pre-defined parameters to a [YAML-formatted parameter
file](#yaml-parameter-files), editing the parameter file, and
re-calculating the noise budget:
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
$ python3 -m gwinc --yaml aLIGO > my_aLIGO.yaml
$ gwinc --yaml aLIGO > my_aLIGO.yaml
$ edit my_aLIGO.yaml
$ python3 -m gwinc -d my_aLIGO.yaml aLIGO
$ gwinc -d my_aLIGO.yaml aLIGO
aLIGO my_aLIGO.yaml
Materials.Coating.Philown 5e-05 3e-05
$ python3 -m gwinc my_aLIGO.yaml
$ gwinc my_aLIGO.yaml
```
Stand-alone YAML files will always assume the nominal ['aLIGO' budget
Stand-alone YAML files assume the nominal ['aLIGO' budget
description](gwinc/ifo/aLIGO).
[Custom budgets](#budget-interface) may also be processed by providing
the path to the budget module/package:
The command line interface also includes an "interactive" mode which
provides an [IPython](https://ipython.org/) shell for interacting with
a processed budget:
```shell
$ python3 -m gwinc path/to/mybudget
$ gwinc -i Aplus
GWINC interactive shell
The 'ifo' Struct and 'trace' data are available for inspection.
Use the 'whos' command to view the workspace.
You may interact with the plot using the 'plt' functions, e.g.:
In [.]: plt.title("My Special Budget")
In [.]: plt.savefig("mybudget.pdf")
In [1]:
```
See command help for more info:
```shell
$ python3 -m gwinc -h
$ gwinc --help
```
### python library
### library interface
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')
>>> traces = Budget(freq).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 un-instantiated `Budget` class
defined in the specified budget module (see [budget
interface](#budget-interface) below).
The budget `run()` method is a convenience method that calculates all
budget noises and the noise total and returns a (possibly) nested
dictionary of a noise data, in the form of a `(data, style)` tuple
where 'data' is the PSD data and 'style' is a plot style dictionary
for the trace. The dictionary will be nested if the budget includes
any sub-budgets.
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)
```
## noise functions
`pygwinc` 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 `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
......@@ -138,7 +194,7 @@ def coating_brownian(f, materials, wavelength, wBeam, dOpt):
```
### `gwinc.Struct` objects
## `gwinc.Struct` objects
`pygwinc` provides a `Struct` class that can hold parameters in
attributes and additionally acts like a dictionary, for passing to the
......@@ -209,8 +265,12 @@ are:
* `update(**kwargs)`: update data/attributes
* `calc()`: return final data array
See the built-in documentation for more info (e.g. `pydoc3
gwinc.nb.BudgetItem`)
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`)
### budget module definition
......@@ -290,11 +350,10 @@ The `style` attributes of the various `Noise` classes define plot
style for the noise.
This budget can be loaded with the `gwinc.load_budget()` function, and
processed with the `Budget.run()` method:
processed as usual with the `Budget.run()` method:
```python
Budget = load_budget('/path/to/MyBudget')
budget = Budget(freq)
traces = budget.run()
budget = load_budget('/path/to/MyBudget', freq)
trace = budget.run()
```
Other than the necessary `freq` initialization argument that defines
......@@ -310,28 +369,64 @@ suspension Struct is extracted from the `self.ifo` Struct at
If a budget module defined as a package includes an `ifo.yaml`
[parameter file](#parameter-files) in the package directory, the
`load_budget()` function will automatically load the YAML data into a
`gwinc.Struct` and include it as an `Budget.ifo` attribute in the
returned `Budget` class. This would provide the `self.ifo` needed in
the `SuspensionThermal` Noise class above and is therefore a
convenient way to provide parameter structures in budget packages.
Otherwise it would need to be created/loaded in some other way and
passed to the budget at instantiation, e.g.:
```python
Budget = load_budget('/path/to/MyBudget')
ifo = Struct.from_file('/path/to/MyBudget.ifo')
budget = Budget(freq, ifo=ifo)
traces = budget.run()
```
`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:
budget interface (e.g. [gwinc.ifo.aLIGO](gwinc/ifo/aLIGO)).
* [aLIGO](gwinc/ifo/aLIGO)
* [Aplus](gwinc/ifo/Aplus)
* [Voyager](gwinc/ifo/Voyager)
* [CE1](master/gwinc/ifo/CE1)
* [CE2](master/gwinc/ifo/CE2)
### 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
......@@ -341,28 +436,27 @@ 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')
budget = Budget(freq)
traces = budget.calc_traces()
data, plot_style = traces['QuantumVacuum']
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('QuantumVacuum')
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()` method directly:
running it's `calc_trace()` method directly:
```python
data = budget['QuantumVacuum'].calc()
data = budget['Quantum'].calc_trace()
```
# figures of merit
# inspiral range
The [`inspiral_range`](https://git.ligo.org/gwinc/inspiral-range)
package can be used to calculate various common "inspiral range"
......@@ -375,18 +469,15 @@ import inspiral_range
import numpy as np
freq = np.logspace(1, 3, 1000)
Budget = gwinc.load_budget('Aplus')
traces = Budget(freq).run()
budget = gwinc.load_budget('Aplus', freq)
trace = budget.run()
range = inspiral_range.range(
freq, traces['Total'][0],
freq, trace.psd,
m1=30, m2=30,
)
```
Note you need to extract the zeroth element of the `traces['Total']`
tuple, which is the actual PSD data.
See the [`inspiral_range`](https://git.ligo.org/gwinc/inspiral-range)
package for more details.
......
"""
Requires pytest to import
"""
import os
from os import path
import os
from shutil import rmtree
import contextlib
import pytest
_options_added = False
def pytest_addoption(parser):
global _options_added
#this check fixes issues if this gets run multiple times from sub conftest.py's
if _options_added:
return
else:
_options_added = True
parser.addoption(
"--plot",
action="store_true",
dest = 'plot',
help = "Have tests update plots (it is slow)",
)
parser.addoption(
"--do-stresstest",
action = "store_true",
help = "Run slow repeated stress tests"
)
parser.addoption(
"--no-preclear",
action="store_true",
default=False,
dest='no_preclear',
help="Do not preclear tpaths",
)
parser.addoption(
"--generate",
action="store_true",
default=False,
dest="generate",
help="Generate test data",
)
@pytest.fixture
def plot(request):
return request.config.getvalue('--plot')
return request.config.option.plot
@pytest.fixture
def tpath_preclear(request):
"""
Fixture that indicates that the test path should be cleared automatically
before running each test. This cleans up the test data.
"""
tpath_raw = tpath_raw_make(request)
no_preclear = request.config.getvalue('--no-preclear')
if not no_preclear:
rmtree(tpath_raw, ignore_errors = True)
@pytest.fixture
def tpath(request):
"""
Fixture that takes the value of the special test-specific folder for test
run data and plots. Usually the <folder of the test>/test_results/test_name/
"""
tpath_raw = tpath_raw_make(request)
os.makedirs(tpath_raw, exist_ok = True)
os.utime(tpath_raw, None)
return tpath_raw
@pytest.fixture
def tpath_join(request):
"""
Fixture that joins subpaths to the value of the special test-specific folder for test
run data and plots. Usually the <folder of the test>/test_results/test_name/.
This function should be use like test_thing.save(tpath_join('output_file.png'))
"""
tpath_raw = tpath_raw_make(request)
first_call = True
def tpath_joiner(*subpath):
nonlocal first_call
if first_call:
os.makedirs(tpath_raw, exist_ok = True)
os.utime(tpath_raw, None)
first_call = False
return path.join(tpath_raw, *subpath)
return tpath_joiner
@pytest.fixture
def fpath(request):
"""
py.test fixture that returns the folder path of the test being run. Useful
for accessing data files.
"""
return fpath_raw_make(request)
@pytest.fixture
def fpath_join(request):
"""
py.test fixture that runs :code:`os.path.join(path, *arguments)` to merge subpaths
with the folder path of the current test being run. Useful for referring to
data files.
"""
def join_func(*path):
return os.path.join(fpath_raw_make(request), *path)
return join_func
@pytest.fixture
def closefigs():
import matplotlib.pyplot as plt
yield
plt.close('all')
@pytest.fixture
def test_trigger():
"""
This fixture provides a contextmanager that causes a function to call
if an AssertionError is raised. It will also call if any of its argument,
or keyword arguments is true. This allows you to conveniently force
calling using other flags or fixtures.
The primary usage of this is to plot outputs only on test failures, while also
allowing plotting to happen using the plot fixture and pytest cmdline argument
"""
run_store = []
@contextlib.contextmanager
def fail(call, **kwargs):
run_store.append(call)
def call(did_fail):
do_call = did_fail
for k, v in kwargs.items():
if v:
do_call = True
break
if do_call:
for call in run_store:
call(fail = did_fail, **kwargs)
run_store.clear()
try:
yield
except AssertionError:
call(True)
raise
else:
call(False)
return
return fail
@pytest.fixture()
def ic():
"""
Fixture to provide icecream imports without requiring that the package exist
"""
try:
from icecream import ic
return ic
except ImportError:
pass
try:
from IPython.lib.pretty import pprint
return pprint
except ImportError:
from pprint import pprint
return pprint
#these are used with the pprint fixture
try:
import icecream
except ImportError:
icecream = None
pass
try:
from IPython.lib.pretty import pprint, pretty
pformat = pretty
except ImportError:
from pprint import pprint, pformat
@pytest.fixture
def pprint(request, tpath_join):
"""
This is a fixture providing a wrapper function for pretty printing. It uses
the icecream module for pretty printing, falling back to ipythons pretty
printer if needed, then to the python build in pretty printing module.
Along with printing to stdout, this function prints into the tpath_folder to
save all output into output.txt.
"""
fname = tpath_join('output.txt')
#pushes past the dot
print('---------------:{}:--------------'.format(request.node.name))
with open(fname, 'w') as F:
def pprint(*args, F = F, pretty = True, **kwargs):
outs = []
if pretty:
for arg in args:
outs.append(
pformat(arg)
)
else:
outs = args
if F is not None:
print(*outs, file = F)
if icecream is not None:
icecream.DEFAULT_OUTPUT_FUNCTION(' '.join(outs), **kwargs)
else:
print(*outs, **kwargs)
yield pprint
def tpath_raw_make(request):
if isinstance(request.node, pytest.Function):
return relfile_test(request.node.function.__code__.co_filename, request, 'test_results')
raise RuntimeError("TPath currently only works for functions")
def fpath_raw_make(request):
if isinstance(request.node, pytest.Function):
return os.path.split(request.node.function.__code__.co_filename)[0]
raise RuntimeError("TPath currently only works for functions")
def relfile(_file_, *args, fname = None):
fpath = path.split(_file_)[0]
post = path.join(*args)
fpath = path.join(fpath, post)
#os.makedirs(fpath, exist_ok = True)
#os.utime(fpath, None)
if fname is None:
return fpath
else:
return path.join(fpath, fname)
def relfile_test(_file_, request, pre = None, post = None, fname = None):
"""
Generates a folder specific to py.test function
(provided by using the "request" fixture in the test's arguments)
"""
if isinstance(pre, (list, tuple)):
pre = path.join(pre)
testname = request.node.name
if pre is not None:
testname = path.join(pre, testname)
if isinstance(post, (list, tuple)):
post = path.join(post)
if post is not None:
return relfile(_file_, testname, post, fname = fname)
else:
return relfile(_file_, testname, fname = fname)
@pytest.fixture
def compare_noise(pprint):
"""
Fixture to compare two sets of traces
A list of noises passed, failed, and skipped are printed. Comparisons are
skipped if the psd's are sufficiently small (controlled by psd_tol) indicating
that the noise is essentially zero or if a trace is missing.
An assertion error is raised if any noises fail.
"""
import numpy as np
def compare(traces, ref_traces, psd_tol=1e-52):
passed = []
failed = []
skipped = []
if ref_traces.budget:
for ref_trace in ref_traces:
if np.all(ref_trace.psd < psd_tol):
skipped.append(ref_trace.name)
continue
try:
trace = traces[ref_trace.name]
except KeyError:
skipped.append(ref_trace.name)
continue
if np.allclose(trace.psd, ref_trace.psd, atol=0):
passed.append(trace.name)
else:
failed.append(trace.name)
else:
if np.allclose(ref_traces.psd, traces.psd, atol=0):
passed.append(traces.name)
else:
failed.append(traces.name)
pprint('Noises failed:')
pprint(40 * '-')
for noise in failed:
pprint(noise)
pprint(40 * '+')
pprint('Noises passed:')
pprint(40 * '-')
for noise in passed:
pprint(noise)
pprint(40 * '+')
pprint('Noises skipped:')
pprint(40 * '-')
for noise in skipped:
pprint(noise)
assert len(failed) == 0
return compare
def pytest_collection_modifyitems(config, items):
"""
Modifies tests to be selectively skipped with command line options
https://docs.pytest.org/en/latest/example/simple.html#control-skipping-of-tests-according-to-command-line-option
"""
# run tests marked as generate if and only if --generate is given
# skip all others in this case
if config.getoption('--generate'):
skip = pytest.mark.skip(
reason='only running tests that generate data')
for item in items:
if 'generate' not in item.keywords:
item.add_marker(skip)
else:
skip = pytest.mark.skip(
reason='generates test data: needs --generate to run')
for item in items:
if 'generate' in item.keywords:
item.add_marker(skip)
......@@ -41,7 +41,7 @@ clean:
-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
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 .plot import plot_trace
from .plot import plot_budget
from .plot import plot_noise
def _load_module(name_or_path):
logger = logging.getLogger('gwinc')
DEFAULT_FREQ = '5:3000:6000'
class InvalidFrequencySpec(Exception):
pass
def freq_from_spec(spec=None):
"""logarithmicly spaced frequency array, based on specification string
Specification string should be of form 'START:[NPOINTS:]STOP'. If
`spec` is an array, then the array is returned as-is, and if it's
None the DEFAULT_FREQ specification is used.
"""
if isinstance(spec, np.ndarray):
return spec
elif spec is None:
spec = DEFAULT_FREQ
try:
fspec = spec.split(':')
if len(fspec) == 2:
fspec = fspec[0], DEFAULT_FREQ.split(':')[1], fspec[1]
return np.logspace(
np.log10(float(fspec[0])),
np.log10(float(fspec[2])),
int(fspec[1]),
)
except (ValueError, IndexError, AttributeError):
raise InvalidFrequencySpec(f'Improper frequency specification: {spec}')
def load_module(name_or_path):
"""Load module from name or path.
Return loaded module and module path.
......@@ -22,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)
......@@ -32,62 +81,97 @@ 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.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 IFOS:
raise RuntimeError("Unknonw IFO '{}' (available IFOs: {}).".format(
raise RuntimeError("Unknown IFO '{}' (available IFOs: {}).".format(
name_or_path,
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):
......@@ -109,28 +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')
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:
logging.warning("Thermal lensing limits input power to {} W".format(pbs/prfactor))
logger.warning("Thermal lensing limits input power to {} W".format(pbs/prfactor))
# report astrophysical scores if so desired
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)
......@@ -138,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/np.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 logging
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.WARNING),
from . import (
__version__,
IFOS,
DEFAULT_FREQ,
InvalidFrequencySpec,
load_budget,
logger,
)
from . import io
from . import IFOS, load_budget, plot_noise
logger.setLevel(os.getenv('LOG_LEVEL', 'WARNING').upper())
formatter = logging.Formatter('%(message)s')
handler = logging.StreamHandler()
handler.setFormatter(formatter)
logger.addHandler(handler)
##################################################
description = """Plot GWINC noise budget for specified IFO.
description = """GWINC noise budget tool
IFOs can be specified by name of included canonical budget (see
below), or by path to a budget module (.py), description file
(.yaml/.mat/.m), or HDF5 data file (.hdf5/.h5). Available included
IFOs are:
Available included IFOs: {}
{}
""".format(', '.join(["'{}'".format(ifo) for ifo in IFOS]))
""".format(', '.join(["{}".format(ifo) for ifo in IFOS]))
# for ifo in available_ifos():
# 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:
If the inspiral_range package is installed, various figures of merit
can be calculated for the resultant spectrum with the --fom argument,
e.g.:
gwinc --ifo Optics.SRM.Tunephase=3.14 ...
gwinc --fom horizon ...
gwinc --fom range:m1=20,m2=20 ...
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.
See documentation for inspiral_range package for details.
If the --range option is specified and the inspiral_range package is
available, various BNS (m1=m2=1.4 M_solar) range figures of merit will
be calculated for the resultant spectrum. The default waveform
parameters can be overriden with the --waveform-parameter/-wp option:
gwinc -r -wp m1=20 -wp m2=20 ...
See the inspiral_range package documentation for details.
"""
IFO = 'aLIGO'
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,56 +126,90 @@ def main():
##########
# initial arg processing
if os.path.splitext(os.path.basename(args.IFO))[1] in ['.hdf5', '.h5']:
if os.path.splitext(os.path.basename(args.IFO))[1] in io.DATA_SAVE_FORMATS:
if args.freq:
parser.exit(2, "Error: Frequency specification not allowed when loading traces from file.\n")
if args.ifo:
parser.exit(2, "Error: IFO parameter specification not allowed when loading traces from file.\n")
from .io import load_hdf5
Budget = None
freq, traces, attrs = load_hdf5(args.IFO)
ifo = getattr(attrs, 'IFO', None)
plot_style = attrs
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_budget(args.diff)
diffs = ifo.diff(Budget.ifo)
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
......@@ -150,109 +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:
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, traces['Total'][0], 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']:
from .io import save_hdf5
logging.info("saving budget traces {}...".format(args.save))
if ifo:
plot_style['IFO'] = ifo.to_yaml()
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()
......