├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── documentation-update.md
│ ├── feature_request.md
│ └── testing-update.md
└── workflows
│ ├── pr-messages.yml
│ ├── python-package.yml
│ └── release-messages.yml
├── .gitignore
├── README.md
├── docs
├── .buildinfo
├── .doctrees
│ ├── environment.pickle
│ ├── getting_started.doctree
│ ├── index.doctree
│ ├── load_ests.doctree
│ └── prog_client.doctree
├── _sources
│ ├── getting_started.rst.txt
│ ├── index.rst.txt
│ ├── load_ests.rst.txt
│ └── prog_client.rst.txt
├── _static
│ ├── basic.css
│ ├── doctools.js
│ ├── documentation_options.js
│ ├── language_data.js
│ └── searchtools.js
├── genindex.html
├── getting_started.html
├── index.html
├── load_ests.html
├── objects.inv
├── prog_client.html
├── search.html
└── searchindex.js
├── examples
├── __init__.py
├── online_prog.py
└── option_scoring.py
├── forms
├── PaaS_Corporate CLA.pdf
└── PaaS_Individual CLA.pdf
├── license.pdf
├── scripts
└── test_copyright.py
├── setup.py
├── specs
├── prog_server.postman_collection.json
└── swagger.yaml
├── src
├── prog_client
│ ├── __init__.py
│ └── session.py
└── prog_server
│ ├── __init__.py
│ ├── __main__.py
│ ├── app.py
│ ├── controllers.py
│ └── models
│ ├── __init__.py
│ ├── load_ests.py
│ ├── prediction_handler.py
│ ├── prog_server.py
│ └── session.py
└── tests
├── __init__.py
├── __main__.py
├── examples.py
└── integration.py
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **Relevant Requirements**
14 | Indicate any enhancement issues (i.e., requirements) that are impacted by this issue.
15 |
16 | **To Reproduce**
17 | Steps to reproduce the behavior:
18 | 1. Go to '...'
19 | 2. Click on '....'
20 | 3. Scroll down to '....'
21 | 4. See error
22 |
23 | **Expected behavior**
24 | A clear and concise description of what you expected to happen.
25 |
26 | **Screenshots**
27 | If applicable, add screenshots to help explain your problem.
28 |
29 | **Desktop (please complete the following information):**
30 | - OS: [e.g. iOS]
31 | - Python Version [e.g. 3.7]
32 |
33 | **Additional context**
34 | Add any other context about the problem here.
35 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/documentation-update.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Documentation update
3 | about: An update to the documentation
4 | title: ''
5 | labels: documentation
6 | assignees: ''
7 |
8 | ---
9 |
10 | ** Location where change is recommended **
11 |
12 | ** Recommended change **
13 |
14 | ** Reason **
15 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Requirement Text**
11 | What must this feature do, specifically - this is the thing to test to. E.g., Ability to simulate model until a specified event has been met.
12 |
13 | **Background Information**
14 | Optional
15 |
16 | **Suggested Solution**
17 | Optional, Solution that is proposed. Requirement can be met with other solutions
18 |
19 | **DoD**
20 | What need to be completed for this feature to be complete. E.g.,
21 | - [ ] Implement ability to simulate to specified event
22 | - [ ] Add to example
23 | - [ ] Add to tutorial
24 | - [ ] Add tests
25 | - [ ] Add to change notes for next release
26 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/testing-update.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Testing update
3 | about: An update to tests or the CI/CD pipeline
4 | title: ''
5 | labels: CI/CD
6 | assignees: ''
7 |
8 | ---
9 |
10 | ** Test to Update **
11 |
12 | ** Change Recommended **
13 |
14 | ** Reason **
15 |
--------------------------------------------------------------------------------
/.github/workflows/pr-messages.yml:
--------------------------------------------------------------------------------
1 | name: Print PR Message - Non Release
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - 'dev'
7 | types: [opened]
8 |
9 | jobs:
10 | benchmark_branch:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Auto Comment
14 | uses: wow-actions/auto-comment@v1
15 | with:
16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
17 | pullRequestOpened: >
18 | Thank you for opening this PR. Each PR into dev requires a code review. For the code review, look at the following:
19 |
20 | - [ ] Reviewer should look for bugs, efficiency, readability, testing, and coverage in examples (if relevant).
21 |
22 | - [ ] Ensure that each PR adding a new feature should include a test verifying that feature.
23 |
24 | - [ ] All tests must be passing.
25 |
26 | - [ ] All errors from static analysis must be resolved.
27 |
28 | - [ ] Review the test coverage reports (if there is a change) - will be added as comment on PR if there is a change
29 |
30 | - [ ] Review the software benchmarking results (if there is a change) - will be added as comment on PR
31 |
32 | - [ ] Any added dependencies are included in requirements.txt, setup.py, and dev_guide.rst (this document)
33 |
34 | - [ ] All warnings from static analysis must be reviewed and resolved - if deemed appropriate.
35 |
--------------------------------------------------------------------------------
/.github/workflows/python-package.yml:
--------------------------------------------------------------------------------
1 | # Copyright © 2021 United States Government as represented by the Administrator of the National Aeronautics and Space Administration. All Rights Reserved.
2 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions
3 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
4 |
5 | name: Python package
6 |
7 | on:
8 | push:
9 | pull_request:
10 | paths:
11 | - prog_server
12 |
13 | jobs:
14 | test:
15 | runs-on: ubuntu-latest
16 | strategy:
17 | matrix:
18 | python-version: ['3.7', '3.8', '3.9', '3.10']
19 | steps:
20 | - uses: actions/checkout@v2
21 | - name: Set up Python ${{ matrix.python-version }}
22 | uses: actions/setup-python@v2
23 | with:
24 | python-version: ${{ matrix.python-version }}
25 | - name: Install dependencies
26 | run: |
27 | python -m pip install --upgrade pip
28 | python -m pip install -e .
29 | - name: Run tests
30 | run: python -m tests
31 | copyright:
32 | runs-on: ubuntu-latest
33 | strategy:
34 | matrix:
35 | python-version: ['3.9']
36 | steps:
37 | - uses: actions/checkout@v2
38 | - name: Set up Python ${{ matrix.python-version }}
39 | uses: actions/setup-python@v2
40 | with:
41 | python-version: ${{ matrix.python-version }}
42 | - name: Run copyright check
43 | run: |
44 | python scripts/test_copyright.py
45 | coverage:
46 | runs-on: ubuntu-latest
47 | strategy:
48 | matrix:
49 | python-version: ['3.9']
50 | steps:
51 | - uses: actions/checkout@v2
52 | - name: Set up Python ${{ matrix.python-version }}
53 | uses: actions/setup-python@v2
54 | with:
55 | python-version: ${{ matrix.python-version }}
56 | - name: Install dependencies
57 | run: |
58 | python -m pip install --upgrade pip
59 | python -m pip install -e .
60 | pip install coverage
61 | - name: Run coverage
62 | run: |
63 | coverage run -m tests
64 | coverage xml
65 | - name: "Upload coverage to Codecov"
66 | uses: codecov/codecov-action@v2
67 | with:
68 | fail_ci_if_error: true
69 |
--------------------------------------------------------------------------------
/.github/workflows/release-messages.yml:
--------------------------------------------------------------------------------
1 | name: Print PR Message - Release
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - 'release/**'
7 | types: [opened]
8 |
9 | jobs:
10 | benchmark_branch:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Auto Comment
14 | uses: wow-actions/auto-comment@v1
15 | with:
16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
17 | pullRequestOpened: >
18 | Thank you for opening this PR. Since this is a release branch, the PR must complete the release checklist, below:
19 |
20 | - [ ] Check that each new feature has corresponding tests
21 |
22 | - [ ] Confirm all dependencies are in the following: requirements.txt, setup.py, the bottom of dev_guide.rst
23 |
24 | - [ ] Confirm that all issues associated with the release have been closed (i.e., requirements have been met) or assigned to another release
25 |
26 | - [ ] Run unit tests `python -m tests`
27 |
28 | - [ ] If present, run manual tests `python -m tests.test_manual`
29 |
30 | - [ ] Review the template(s)
31 |
32 | - [ ] Review static-analysis/linter results
33 |
34 | - [ ] Review the tutorial
35 |
36 | - [ ] Run and review the examples
37 |
38 | - [ ] Check that all examples are tested
39 |
40 | - [ ] Check new files in PR for any accidentally added
41 |
42 | - [ ] Check documents
43 |
44 | - [ ] Check that all desired examples are in docs
45 |
46 | - [ ] General review: see if any updates are required
47 |
48 | - [ ] Rebuild sphinx documents: `sphinx-build sphinx_config/ docs/`
49 |
50 | - [ ] Write release notes
51 |
52 | - [ ] Update version number in src/\*/__init__.py and setup.py
53 |
54 | - [ ] For releases adding new features- ensure that NASA release process has been followed.
55 |
56 | - [ ] Confirm that on GitHub Releases page, the next release has been started and that a schedule is present including at least Release Date, Release Review Date, and Release Branch Opening Date.`
57 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | src/prog_server.egg-info/
7 |
8 | dist/
9 |
10 | build/
11 |
12 | .vscode/
13 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Prognostics As-A-Service (PaaS) Sandbox
2 | [](https://www.codefactor.io/repository/github/nasa/prog_server)
3 | [](https://github.com/nasa/prog_server/blob/master/license.pdf)
4 | [](https://github.com/nasa/prog_server/releases)
5 |
6 | The NASA Prognostics As-A-Service (PaaS) Sandbox is a simplified implementation of a Software Oriented Architecture (SOA) for performing prognostics (estimation of time until events and future system states) of engineering systems. The PaaS Sandbox is a wrapper around the [Prognostics Python Package (ProgPy)](https://nasa.github.io/progpy/), allowing one or more users to access the features of these packages through a REST API. The package is intended to be used as a research tool to prototype and benchmark Prognostics As-A-Service (PaaS) architectures and work on the challenges facing such architectures, including Generality, Communication, Security, Environmental Complexity, Utility, and Trust.
7 |
8 | This is designed to be used with the [Prognostics Python Package (ProgPy)](https://nasa.github.io/progpy/).
9 |
10 | ## Installation
11 | `pip install prog_server`
12 |
13 | ## [Documentation](https://nasa.github.io/progpy/prog_server_guide.html)
14 | See documentation [here](https://nasa.github.io/progpy/prog_server_guide.html)
15 |
16 | ## Citing this repository
17 | Use the following to cite this repository:
18 |
19 | ```
20 | @misc{2024_nasa_prog_server,
21 | author = {Christopher Teubert and Jason Watkins and Katelyn Jarvis},
22 | title = {Prognostics As-A-Service (PaaS) Sandbox},
23 | month = May,
24 | year = 2024,
25 | version = {1.7},
26 | url = {https://github.com/nasa/prog_server}
27 | }
28 | ```
29 |
30 | The corresponding reference should look like this:
31 |
32 | C. Teubert, J. Watkins, K. Jarvis, Prognostics As-A-Service (PaaS) Sandbox, v1.7, May 2024. URL https://github.com/nasa/prog_server.
33 |
34 | ## Notices
35 | Copyright © 2021 United States Government as represented by the Administrator of the National Aeronautics and Space Administration. All Rights Reserved.
36 |
37 | ## Disclaimers
38 | No Warranty: THE SUBJECT SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY OF ANY KIND, EITHER EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY THAT THE SUBJECT SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR FREEDOM FROM INFRINGEMENT, ANY WARRANTY THAT THE SUBJECT SOFTWARE WILL BE ERROR FREE, OR ANY WARRANTY THAT DOCUMENTATION, IF PROVIDED, WILL CONFORM TO THE SUBJECT SOFTWARE. THIS AGREEMENT DOES NOT, IN ANY MANNER, CONSTITUTE AN ENDORSEMENT BY GOVERNMENT AGENCY OR ANY PRIOR RECIPIENT OF ANY RESULTS, RESULTING DESIGNS, HARDWARE, SOFTWARE PRODUCTS OR ANY OTHER APPLICATIONS RESULTING FROM USE OF THE SUBJECT SOFTWARE. FURTHER, GOVERNMENT AGENCY DISCLAIMS ALL WARRANTIES AND LIABILITIES REGARDING THIRD-PARTY SOFTWARE, IF PRESENT IN THE ORIGINAL SOFTWARE, AND DISTRIBUTES IT "AS IS."
39 |
40 | Waiver and Indemnity: RECIPIENT AGREES TO WAIVE ANY AND ALL CLAIMS AGAINST THE UNITED STATES GOVERNMENT, ITS CONTRACTORS AND SUBCONTRACTORS, AS WELL AS ANY PRIOR RECIPIENT. IF RECIPIENT'S USE OF THE SUBJECT SOFTWARE RESULTS IN ANY LIABILITIES, DEMANDS, DAMAGES, EXPENSES OR LOSSES ARISING FROM SUCH USE, INCLUDING ANY DAMAGES FROM PRODUCTS BASED ON, OR RESULTING FROM, RECIPIENT'S USE OF THE SUBJECT SOFTWARE, RECIPIENT SHALL INDEMNIFY AND HOLD HARMLESS THE UNITED STATES GOVERNMENT, ITS CONTRACTORS AND SUBCONTRACTORS, AS WELL AS ANY PRIOR RECIPIENT, TO THE EXTENT PERMITTED BY LAW. RECIPIENT'S SOLE REMEDY FOR ANY SUCH MATTER SHALL BE THE IMMEDIATE, UNILATERAL TERMINATION OF THIS AGREEMENT.
41 |
--------------------------------------------------------------------------------
/docs/.buildinfo:
--------------------------------------------------------------------------------
1 | # Sphinx build info version 1
2 | # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
3 | config: 1c92f19c8c53f10ab66da278b8beacdb
4 | tags: 645f666f9bcd5a90fca523b33c5a78b7
5 |
--------------------------------------------------------------------------------
/docs/.doctrees/environment.pickle:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nasa/prog_server/f5753795ebbfb7794cb7a62a6e4bf23c925b6fd5/docs/.doctrees/environment.pickle
--------------------------------------------------------------------------------
/docs/.doctrees/getting_started.doctree:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nasa/prog_server/f5753795ebbfb7794cb7a62a6e4bf23c925b6fd5/docs/.doctrees/getting_started.doctree
--------------------------------------------------------------------------------
/docs/.doctrees/index.doctree:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nasa/prog_server/f5753795ebbfb7794cb7a62a6e4bf23c925b6fd5/docs/.doctrees/index.doctree
--------------------------------------------------------------------------------
/docs/.doctrees/load_ests.doctree:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nasa/prog_server/f5753795ebbfb7794cb7a62a6e4bf23c925b6fd5/docs/.doctrees/load_ests.doctree
--------------------------------------------------------------------------------
/docs/.doctrees/prog_client.doctree:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nasa/prog_server/f5753795ebbfb7794cb7a62a6e4bf23c925b6fd5/docs/.doctrees/prog_client.doctree
--------------------------------------------------------------------------------
/docs/_sources/getting_started.rst.txt:
--------------------------------------------------------------------------------
1 | Getting Started
2 | ===============
3 |
4 | Documentation moved to https://nasa.github.io/progpy
5 |
--------------------------------------------------------------------------------
/docs/_sources/index.rst.txt:
--------------------------------------------------------------------------------
1 | Prognostics As-A-Service (PaaS) Sandbox
2 | =============================================================
3 |
4 | Documentation moved to https://nasa.github.io/progpy
5 |
--------------------------------------------------------------------------------
/docs/_sources/load_ests.rst.txt:
--------------------------------------------------------------------------------
1 | Load Estimators
2 | ################
3 |
4 | Documentation moved to https://nasa.github.io/progpy
5 |
--------------------------------------------------------------------------------
/docs/_sources/prog_client.rst.txt:
--------------------------------------------------------------------------------
1 | prog_client
2 | ==================================
3 |
4 | Documentation moved to https://nasa.github.io/progpy
5 |
--------------------------------------------------------------------------------
/docs/_static/basic.css:
--------------------------------------------------------------------------------
1 | /*
2 | * basic.css
3 | * ~~~~~~~~~
4 | *
5 | * Sphinx stylesheet -- basic theme.
6 | *
7 | * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS.
8 | * :license: BSD, see LICENSE for details.
9 | *
10 | */
11 |
12 | /* -- main layout ----------------------------------------------------------- */
13 |
14 | div.clearer {
15 | clear: both;
16 | }
17 |
18 | div.section::after {
19 | display: block;
20 | content: '';
21 | clear: left;
22 | }
23 |
24 | /* -- relbar ---------------------------------------------------------------- */
25 |
26 | div.related {
27 | width: 100%;
28 | font-size: 90%;
29 | }
30 |
31 | div.related h3 {
32 | display: none;
33 | }
34 |
35 | div.related ul {
36 | margin: 0;
37 | padding: 0 0 0 10px;
38 | list-style: none;
39 | }
40 |
41 | div.related li {
42 | display: inline;
43 | }
44 |
45 | div.related li.right {
46 | float: right;
47 | margin-right: 5px;
48 | }
49 |
50 | /* -- sidebar --------------------------------------------------------------- */
51 |
52 | div.sphinxsidebarwrapper {
53 | padding: 10px 5px 0 10px;
54 | }
55 |
56 | div.sphinxsidebar {
57 | float: left;
58 | width: 230px;
59 | margin-left: -100%;
60 | font-size: 90%;
61 | word-wrap: break-word;
62 | overflow-wrap : break-word;
63 | }
64 |
65 | div.sphinxsidebar ul {
66 | list-style: none;
67 | }
68 |
69 | div.sphinxsidebar ul ul,
70 | div.sphinxsidebar ul.want-points {
71 | margin-left: 20px;
72 | list-style: square;
73 | }
74 |
75 | div.sphinxsidebar ul ul {
76 | margin-top: 0;
77 | margin-bottom: 0;
78 | }
79 |
80 | div.sphinxsidebar form {
81 | margin-top: 10px;
82 | }
83 |
84 | div.sphinxsidebar input {
85 | border: 1px solid #98dbcc;
86 | font-family: sans-serif;
87 | font-size: 1em;
88 | }
89 |
90 | div.sphinxsidebar #searchbox form.search {
91 | overflow: hidden;
92 | }
93 |
94 | div.sphinxsidebar #searchbox input[type="text"] {
95 | float: left;
96 | width: 80%;
97 | padding: 0.25em;
98 | box-sizing: border-box;
99 | }
100 |
101 | div.sphinxsidebar #searchbox input[type="submit"] {
102 | float: left;
103 | width: 20%;
104 | border-left: none;
105 | padding: 0.25em;
106 | box-sizing: border-box;
107 | }
108 |
109 |
110 | img {
111 | border: 0;
112 | max-width: 100%;
113 | }
114 |
115 | /* -- search page ----------------------------------------------------------- */
116 |
117 | ul.search {
118 | margin: 10px 0 0 20px;
119 | padding: 0;
120 | }
121 |
122 | ul.search li {
123 | padding: 5px 0 5px 20px;
124 | background-image: url(file.png);
125 | background-repeat: no-repeat;
126 | background-position: 0 7px;
127 | }
128 |
129 | ul.search li a {
130 | font-weight: bold;
131 | }
132 |
133 | ul.search li p.context {
134 | color: #888;
135 | margin: 2px 0 0 30px;
136 | text-align: left;
137 | }
138 |
139 | ul.keywordmatches li.goodmatch a {
140 | font-weight: bold;
141 | }
142 |
143 | /* -- index page ------------------------------------------------------------ */
144 |
145 | table.contentstable {
146 | width: 90%;
147 | margin-left: auto;
148 | margin-right: auto;
149 | }
150 |
151 | table.contentstable p.biglink {
152 | line-height: 150%;
153 | }
154 |
155 | a.biglink {
156 | font-size: 1.3em;
157 | }
158 |
159 | span.linkdescr {
160 | font-style: italic;
161 | padding-top: 5px;
162 | font-size: 90%;
163 | }
164 |
165 | /* -- general index --------------------------------------------------------- */
166 |
167 | table.indextable {
168 | width: 100%;
169 | }
170 |
171 | table.indextable td {
172 | text-align: left;
173 | vertical-align: top;
174 | }
175 |
176 | table.indextable ul {
177 | margin-top: 0;
178 | margin-bottom: 0;
179 | list-style-type: none;
180 | }
181 |
182 | table.indextable > tbody > tr > td > ul {
183 | padding-left: 0em;
184 | }
185 |
186 | table.indextable tr.pcap {
187 | height: 10px;
188 | }
189 |
190 | table.indextable tr.cap {
191 | margin-top: 10px;
192 | background-color: #f2f2f2;
193 | }
194 |
195 | img.toggler {
196 | margin-right: 3px;
197 | margin-top: 3px;
198 | cursor: pointer;
199 | }
200 |
201 | div.modindex-jumpbox {
202 | border-top: 1px solid #ddd;
203 | border-bottom: 1px solid #ddd;
204 | margin: 1em 0 1em 0;
205 | padding: 0.4em;
206 | }
207 |
208 | div.genindex-jumpbox {
209 | border-top: 1px solid #ddd;
210 | border-bottom: 1px solid #ddd;
211 | margin: 1em 0 1em 0;
212 | padding: 0.4em;
213 | }
214 |
215 | /* -- domain module index --------------------------------------------------- */
216 |
217 | table.modindextable td {
218 | padding: 2px;
219 | border-collapse: collapse;
220 | }
221 |
222 | /* -- general body styles --------------------------------------------------- */
223 |
224 | div.body {
225 | min-width: 450px;
226 | max-width: 800px;
227 | }
228 |
229 | div.body p, div.body dd, div.body li, div.body blockquote {
230 | -moz-hyphens: auto;
231 | -ms-hyphens: auto;
232 | -webkit-hyphens: auto;
233 | hyphens: auto;
234 | }
235 |
236 | a.headerlink {
237 | visibility: hidden;
238 | }
239 |
240 | a.brackets:before,
241 | span.brackets > a:before{
242 | content: "[";
243 | }
244 |
245 | a.brackets:after,
246 | span.brackets > a:after {
247 | content: "]";
248 | }
249 |
250 | h1:hover > a.headerlink,
251 | h2:hover > a.headerlink,
252 | h3:hover > a.headerlink,
253 | h4:hover > a.headerlink,
254 | h5:hover > a.headerlink,
255 | h6:hover > a.headerlink,
256 | dt:hover > a.headerlink,
257 | caption:hover > a.headerlink,
258 | p.caption:hover > a.headerlink,
259 | div.code-block-caption:hover > a.headerlink {
260 | visibility: visible;
261 | }
262 |
263 | div.body p.caption {
264 | text-align: inherit;
265 | }
266 |
267 | div.body td {
268 | text-align: left;
269 | }
270 |
271 | .first {
272 | margin-top: 0 !important;
273 | }
274 |
275 | p.rubric {
276 | margin-top: 30px;
277 | font-weight: bold;
278 | }
279 |
280 | img.align-left, figure.align-left, .figure.align-left, object.align-left {
281 | clear: left;
282 | float: left;
283 | margin-right: 1em;
284 | }
285 |
286 | img.align-right, figure.align-right, .figure.align-right, object.align-right {
287 | clear: right;
288 | float: right;
289 | margin-left: 1em;
290 | }
291 |
292 | img.align-center, figure.align-center, .figure.align-center, object.align-center {
293 | display: block;
294 | margin-left: auto;
295 | margin-right: auto;
296 | }
297 |
298 | img.align-default, figure.align-default, .figure.align-default {
299 | display: block;
300 | margin-left: auto;
301 | margin-right: auto;
302 | }
303 |
304 | .align-left {
305 | text-align: left;
306 | }
307 |
308 | .align-center {
309 | text-align: center;
310 | }
311 |
312 | .align-default {
313 | text-align: center;
314 | }
315 |
316 | .align-right {
317 | text-align: right;
318 | }
319 |
320 | /* -- sidebars -------------------------------------------------------------- */
321 |
322 | div.sidebar,
323 | aside.sidebar {
324 | margin: 0 0 0.5em 1em;
325 | border: 1px solid #ddb;
326 | padding: 7px;
327 | background-color: #ffe;
328 | width: 40%;
329 | float: right;
330 | clear: right;
331 | overflow-x: auto;
332 | }
333 |
334 | p.sidebar-title {
335 | font-weight: bold;
336 | }
337 |
338 | div.admonition, div.topic, blockquote {
339 | clear: left;
340 | }
341 |
342 | /* -- topics ---------------------------------------------------------------- */
343 |
344 | div.topic {
345 | border: 1px solid #ccc;
346 | padding: 7px;
347 | margin: 10px 0 10px 0;
348 | }
349 |
350 | p.topic-title {
351 | font-size: 1.1em;
352 | font-weight: bold;
353 | margin-top: 10px;
354 | }
355 |
356 | /* -- admonitions ----------------------------------------------------------- */
357 |
358 | div.admonition {
359 | margin-top: 10px;
360 | margin-bottom: 10px;
361 | padding: 7px;
362 | }
363 |
364 | div.admonition dt {
365 | font-weight: bold;
366 | }
367 |
368 | p.admonition-title {
369 | margin: 0px 10px 5px 0px;
370 | font-weight: bold;
371 | }
372 |
373 | div.body p.centered {
374 | text-align: center;
375 | margin-top: 25px;
376 | }
377 |
378 | /* -- content of sidebars/topics/admonitions -------------------------------- */
379 |
380 | div.sidebar > :last-child,
381 | aside.sidebar > :last-child,
382 | div.topic > :last-child,
383 | div.admonition > :last-child {
384 | margin-bottom: 0;
385 | }
386 |
387 | div.sidebar::after,
388 | aside.sidebar::after,
389 | div.topic::after,
390 | div.admonition::after,
391 | blockquote::after {
392 | display: block;
393 | content: '';
394 | clear: both;
395 | }
396 |
397 | /* -- tables ---------------------------------------------------------------- */
398 |
399 | table.docutils {
400 | margin-top: 10px;
401 | margin-bottom: 10px;
402 | border: 0;
403 | border-collapse: collapse;
404 | }
405 |
406 | table.align-center {
407 | margin-left: auto;
408 | margin-right: auto;
409 | }
410 |
411 | table.align-default {
412 | margin-left: auto;
413 | margin-right: auto;
414 | }
415 |
416 | table caption span.caption-number {
417 | font-style: italic;
418 | }
419 |
420 | table caption span.caption-text {
421 | }
422 |
423 | table.docutils td, table.docutils th {
424 | padding: 1px 8px 1px 5px;
425 | border-top: 0;
426 | border-left: 0;
427 | border-right: 0;
428 | border-bottom: 1px solid #aaa;
429 | }
430 |
431 | table.footnote td, table.footnote th {
432 | border: 0 !important;
433 | }
434 |
435 | th {
436 | text-align: left;
437 | padding-right: 5px;
438 | }
439 |
440 | table.citation {
441 | border-left: solid 1px gray;
442 | margin-left: 1px;
443 | }
444 |
445 | table.citation td {
446 | border-bottom: none;
447 | }
448 |
449 | th > :first-child,
450 | td > :first-child {
451 | margin-top: 0px;
452 | }
453 |
454 | th > :last-child,
455 | td > :last-child {
456 | margin-bottom: 0px;
457 | }
458 |
459 | /* -- figures --------------------------------------------------------------- */
460 |
461 | div.figure, figure {
462 | margin: 0.5em;
463 | padding: 0.5em;
464 | }
465 |
466 | div.figure p.caption, figcaption {
467 | padding: 0.3em;
468 | }
469 |
470 | div.figure p.caption span.caption-number,
471 | figcaption span.caption-number {
472 | font-style: italic;
473 | }
474 |
475 | div.figure p.caption span.caption-text,
476 | figcaption span.caption-text {
477 | }
478 |
479 | /* -- field list styles ----------------------------------------------------- */
480 |
481 | table.field-list td, table.field-list th {
482 | border: 0 !important;
483 | }
484 |
485 | .field-list ul {
486 | margin: 0;
487 | padding-left: 1em;
488 | }
489 |
490 | .field-list p {
491 | margin: 0;
492 | }
493 |
494 | .field-name {
495 | -moz-hyphens: manual;
496 | -ms-hyphens: manual;
497 | -webkit-hyphens: manual;
498 | hyphens: manual;
499 | }
500 |
501 | /* -- hlist styles ---------------------------------------------------------- */
502 |
503 | table.hlist {
504 | margin: 1em 0;
505 | }
506 |
507 | table.hlist td {
508 | vertical-align: top;
509 | }
510 |
511 | /* -- object description styles --------------------------------------------- */
512 |
513 | .sig {
514 | font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
515 | }
516 |
517 | .sig-name, code.descname {
518 | background-color: transparent;
519 | font-weight: bold;
520 | }
521 |
522 | .sig-name {
523 | font-size: 1.1em;
524 | }
525 |
526 | code.descname {
527 | font-size: 1.2em;
528 | }
529 |
530 | .sig-prename, code.descclassname {
531 | background-color: transparent;
532 | }
533 |
534 | .optional {
535 | font-size: 1.3em;
536 | }
537 |
538 | .sig-paren {
539 | font-size: larger;
540 | }
541 |
542 | .sig-param.n {
543 | font-style: italic;
544 | }
545 |
546 | /* C++ specific styling */
547 |
548 | .sig-inline.c-texpr,
549 | .sig-inline.cpp-texpr {
550 | font-family: unset;
551 | }
552 |
553 | .sig.c .k, .sig.c .kt,
554 | .sig.cpp .k, .sig.cpp .kt {
555 | color: #0033B3;
556 | }
557 |
558 | .sig.c .m,
559 | .sig.cpp .m {
560 | color: #1750EB;
561 | }
562 |
563 | .sig.c .s, .sig.c .sc,
564 | .sig.cpp .s, .sig.cpp .sc {
565 | color: #067D17;
566 | }
567 |
568 |
569 | /* -- other body styles ----------------------------------------------------- */
570 |
571 | ol.arabic {
572 | list-style: decimal;
573 | }
574 |
575 | ol.loweralpha {
576 | list-style: lower-alpha;
577 | }
578 |
579 | ol.upperalpha {
580 | list-style: upper-alpha;
581 | }
582 |
583 | ol.lowerroman {
584 | list-style: lower-roman;
585 | }
586 |
587 | ol.upperroman {
588 | list-style: upper-roman;
589 | }
590 |
591 | :not(li) > ol > li:first-child > :first-child,
592 | :not(li) > ul > li:first-child > :first-child {
593 | margin-top: 0px;
594 | }
595 |
596 | :not(li) > ol > li:last-child > :last-child,
597 | :not(li) > ul > li:last-child > :last-child {
598 | margin-bottom: 0px;
599 | }
600 |
601 | ol.simple ol p,
602 | ol.simple ul p,
603 | ul.simple ol p,
604 | ul.simple ul p {
605 | margin-top: 0;
606 | }
607 |
608 | ol.simple > li:not(:first-child) > p,
609 | ul.simple > li:not(:first-child) > p {
610 | margin-top: 0;
611 | }
612 |
613 | ol.simple p,
614 | ul.simple p {
615 | margin-bottom: 0;
616 | }
617 |
618 | dl.footnote > dt,
619 | dl.citation > dt {
620 | float: left;
621 | margin-right: 0.5em;
622 | }
623 |
624 | dl.footnote > dd,
625 | dl.citation > dd {
626 | margin-bottom: 0em;
627 | }
628 |
629 | dl.footnote > dd:after,
630 | dl.citation > dd:after {
631 | content: "";
632 | clear: both;
633 | }
634 |
635 | dl.field-list {
636 | display: grid;
637 | grid-template-columns: fit-content(30%) auto;
638 | }
639 |
640 | dl.field-list > dt {
641 | font-weight: bold;
642 | word-break: break-word;
643 | padding-left: 0.5em;
644 | padding-right: 5px;
645 | }
646 |
647 | dl.field-list > dt:after {
648 | content: ":";
649 | }
650 |
651 | dl.field-list > dd {
652 | padding-left: 0.5em;
653 | margin-top: 0em;
654 | margin-left: 0em;
655 | margin-bottom: 0em;
656 | }
657 |
658 | dl {
659 | margin-bottom: 15px;
660 | }
661 |
662 | dd > :first-child {
663 | margin-top: 0px;
664 | }
665 |
666 | dd ul, dd table {
667 | margin-bottom: 10px;
668 | }
669 |
670 | dd {
671 | margin-top: 3px;
672 | margin-bottom: 10px;
673 | margin-left: 30px;
674 | }
675 |
676 | dl > dd:last-child,
677 | dl > dd:last-child > :last-child {
678 | margin-bottom: 0;
679 | }
680 |
681 | dt:target, span.highlighted {
682 | background-color: #fbe54e;
683 | }
684 |
685 | rect.highlighted {
686 | fill: #fbe54e;
687 | }
688 |
689 | dl.glossary dt {
690 | font-weight: bold;
691 | font-size: 1.1em;
692 | }
693 |
694 | .versionmodified {
695 | font-style: italic;
696 | }
697 |
698 | .system-message {
699 | background-color: #fda;
700 | padding: 5px;
701 | border: 3px solid red;
702 | }
703 |
704 | .footnote:target {
705 | background-color: #ffa;
706 | }
707 |
708 | .line-block {
709 | display: block;
710 | margin-top: 1em;
711 | margin-bottom: 1em;
712 | }
713 |
714 | .line-block .line-block {
715 | margin-top: 0;
716 | margin-bottom: 0;
717 | margin-left: 1.5em;
718 | }
719 |
720 | .guilabel, .menuselection {
721 | font-family: sans-serif;
722 | }
723 |
724 | .accelerator {
725 | text-decoration: underline;
726 | }
727 |
728 | .classifier {
729 | font-style: oblique;
730 | }
731 |
732 | .classifier:before {
733 | font-style: normal;
734 | margin: 0 0.5em;
735 | content: ":";
736 | display: inline-block;
737 | }
738 |
739 | abbr, acronym {
740 | border-bottom: dotted 1px;
741 | cursor: help;
742 | }
743 |
744 | /* -- code displays --------------------------------------------------------- */
745 |
746 | pre {
747 | overflow: auto;
748 | overflow-y: hidden; /* fixes display issues on Chrome browsers */
749 | }
750 |
751 | pre, div[class*="highlight-"] {
752 | clear: both;
753 | }
754 |
755 | span.pre {
756 | -moz-hyphens: none;
757 | -ms-hyphens: none;
758 | -webkit-hyphens: none;
759 | hyphens: none;
760 | white-space: nowrap;
761 | }
762 |
763 | div[class*="highlight-"] {
764 | margin: 1em 0;
765 | }
766 |
767 | td.linenos pre {
768 | border: 0;
769 | background-color: transparent;
770 | color: #aaa;
771 | }
772 |
773 | table.highlighttable {
774 | display: block;
775 | }
776 |
777 | table.highlighttable tbody {
778 | display: block;
779 | }
780 |
781 | table.highlighttable tr {
782 | display: flex;
783 | }
784 |
785 | table.highlighttable td {
786 | margin: 0;
787 | padding: 0;
788 | }
789 |
790 | table.highlighttable td.linenos {
791 | padding-right: 0.5em;
792 | }
793 |
794 | table.highlighttable td.code {
795 | flex: 1;
796 | overflow: hidden;
797 | }
798 |
799 | .highlight .hll {
800 | display: block;
801 | }
802 |
803 | div.highlight pre,
804 | table.highlighttable pre {
805 | margin: 0;
806 | }
807 |
808 | div.code-block-caption + div {
809 | margin-top: 0;
810 | }
811 |
812 | div.code-block-caption {
813 | margin-top: 1em;
814 | padding: 2px 5px;
815 | font-size: small;
816 | }
817 |
818 | div.code-block-caption code {
819 | background-color: transparent;
820 | }
821 |
822 | table.highlighttable td.linenos,
823 | span.linenos,
824 | div.highlight span.gp { /* gp: Generic.Prompt */
825 | user-select: none;
826 | -webkit-user-select: text; /* Safari fallback only */
827 | -webkit-user-select: none; /* Chrome/Safari */
828 | -moz-user-select: none; /* Firefox */
829 | -ms-user-select: none; /* IE10+ */
830 | }
831 |
832 | div.code-block-caption span.caption-number {
833 | padding: 0.1em 0.3em;
834 | font-style: italic;
835 | }
836 |
837 | div.code-block-caption span.caption-text {
838 | }
839 |
840 | div.literal-block-wrapper {
841 | margin: 1em 0;
842 | }
843 |
844 | code.xref, a code {
845 | background-color: transparent;
846 | font-weight: bold;
847 | }
848 |
849 | h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
850 | background-color: transparent;
851 | }
852 |
853 | .viewcode-link {
854 | float: right;
855 | }
856 |
857 | .viewcode-back {
858 | float: right;
859 | font-family: sans-serif;
860 | }
861 |
862 | div.viewcode-block:target {
863 | margin: -1px -10px;
864 | padding: 0 10px;
865 | }
866 |
867 | /* -- math display ---------------------------------------------------------- */
868 |
869 | img.math {
870 | vertical-align: middle;
871 | }
872 |
873 | div.body div.math p {
874 | text-align: center;
875 | }
876 |
877 | span.eqno {
878 | float: right;
879 | }
880 |
881 | span.eqno a.headerlink {
882 | position: absolute;
883 | z-index: 1;
884 | }
885 |
886 | div.math:hover a.headerlink {
887 | visibility: visible;
888 | }
889 |
890 | /* -- printout stylesheet --------------------------------------------------- */
891 |
892 | @media print {
893 | div.document,
894 | div.documentwrapper,
895 | div.bodywrapper {
896 | margin: 0 !important;
897 | width: 100%;
898 | }
899 |
900 | div.sphinxsidebar,
901 | div.related,
902 | div.footer,
903 | #top-link {
904 | display: none;
905 | }
906 | }
--------------------------------------------------------------------------------
/docs/_static/doctools.js:
--------------------------------------------------------------------------------
1 | /*
2 | * doctools.js
3 | * ~~~~~~~~~~~
4 | *
5 | * Sphinx JavaScript utilities for all documentation.
6 | *
7 | * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS.
8 | * :license: BSD, see LICENSE for details.
9 | *
10 | */
11 |
12 | /**
13 | * select a different prefix for underscore
14 | */
15 | $u = _.noConflict();
16 |
17 | /**
18 | * make the code below compatible with browsers without
19 | * an installed firebug like debugger
20 | if (!window.console || !console.firebug) {
21 | var names = ["log", "debug", "info", "warn", "error", "assert", "dir",
22 | "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace",
23 | "profile", "profileEnd"];
24 | window.console = {};
25 | for (var i = 0; i < names.length; ++i)
26 | window.console[names[i]] = function() {};
27 | }
28 | */
29 |
30 | /**
31 | * small helper function to urldecode strings
32 | *
33 | * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL
34 | */
35 | jQuery.urldecode = function(x) {
36 | if (!x) {
37 | return x
38 | }
39 | return decodeURIComponent(x.replace(/\+/g, ' '));
40 | };
41 |
42 | /**
43 | * small helper function to urlencode strings
44 | */
45 | jQuery.urlencode = encodeURIComponent;
46 |
47 | /**
48 | * This function returns the parsed url parameters of the
49 | * current request. Multiple values per key are supported,
50 | * it will always return arrays of strings for the value parts.
51 | */
52 | jQuery.getQueryParameters = function(s) {
53 | if (typeof s === 'undefined')
54 | s = document.location.search;
55 | var parts = s.substr(s.indexOf('?') + 1).split('&');
56 | var result = {};
57 | for (var i = 0; i < parts.length; i++) {
58 | var tmp = parts[i].split('=', 2);
59 | var key = jQuery.urldecode(tmp[0]);
60 | var value = jQuery.urldecode(tmp[1]);
61 | if (key in result)
62 | result[key].push(value);
63 | else
64 | result[key] = [value];
65 | }
66 | return result;
67 | };
68 |
69 | /**
70 | * highlight a given string on a jquery object by wrapping it in
71 | * span elements with the given class name.
72 | */
73 | jQuery.fn.highlightText = function(text, className) {
74 | function highlight(node, addItems) {
75 | if (node.nodeType === 3) {
76 | var val = node.nodeValue;
77 | var pos = val.toLowerCase().indexOf(text);
78 | if (pos >= 0 &&
79 | !jQuery(node.parentNode).hasClass(className) &&
80 | !jQuery(node.parentNode).hasClass("nohighlight")) {
81 | var span;
82 | var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg");
83 | if (isInSVG) {
84 | span = document.createElementNS("http://www.w3.org/2000/svg", "tspan");
85 | } else {
86 | span = document.createElement("span");
87 | span.className = className;
88 | }
89 | span.appendChild(document.createTextNode(val.substr(pos, text.length)));
90 | node.parentNode.insertBefore(span, node.parentNode.insertBefore(
91 | document.createTextNode(val.substr(pos + text.length)),
92 | node.nextSibling));
93 | node.nodeValue = val.substr(0, pos);
94 | if (isInSVG) {
95 | var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
96 | var bbox = node.parentElement.getBBox();
97 | rect.x.baseVal.value = bbox.x;
98 | rect.y.baseVal.value = bbox.y;
99 | rect.width.baseVal.value = bbox.width;
100 | rect.height.baseVal.value = bbox.height;
101 | rect.setAttribute('class', className);
102 | addItems.push({
103 | "parent": node.parentNode,
104 | "target": rect});
105 | }
106 | }
107 | }
108 | else if (!jQuery(node).is("button, select, textarea")) {
109 | jQuery.each(node.childNodes, function() {
110 | highlight(this, addItems);
111 | });
112 | }
113 | }
114 | var addItems = [];
115 | var result = this.each(function() {
116 | highlight(this, addItems);
117 | });
118 | for (var i = 0; i < addItems.length; ++i) {
119 | jQuery(addItems[i].parent).before(addItems[i].target);
120 | }
121 | return result;
122 | };
123 |
124 | /*
125 | * backward compatibility for jQuery.browser
126 | * This will be supported until firefox bug is fixed.
127 | */
128 | if (!jQuery.browser) {
129 | jQuery.uaMatch = function(ua) {
130 | ua = ua.toLowerCase();
131 |
132 | var match = /(chrome)[ \/]([\w.]+)/.exec(ua) ||
133 | /(webkit)[ \/]([\w.]+)/.exec(ua) ||
134 | /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) ||
135 | /(msie) ([\w.]+)/.exec(ua) ||
136 | ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) ||
137 | [];
138 |
139 | return {
140 | browser: match[ 1 ] || "",
141 | version: match[ 2 ] || "0"
142 | };
143 | };
144 | jQuery.browser = {};
145 | jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true;
146 | }
147 |
148 | /**
149 | * Small JavaScript module for the documentation.
150 | */
151 | var Documentation = {
152 |
153 | init : function() {
154 | this.fixFirefoxAnchorBug();
155 | this.highlightSearchWords();
156 | this.initIndexTable();
157 | this.initOnKeyListeners();
158 | },
159 |
160 | /**
161 | * i18n support
162 | */
163 | TRANSLATIONS : {},
164 | PLURAL_EXPR : function(n) { return n === 1 ? 0 : 1; },
165 | LOCALE : 'unknown',
166 |
167 | // gettext and ngettext don't access this so that the functions
168 | // can safely bound to a different name (_ = Documentation.gettext)
169 | gettext : function(string) {
170 | var translated = Documentation.TRANSLATIONS[string];
171 | if (typeof translated === 'undefined')
172 | return string;
173 | return (typeof translated === 'string') ? translated : translated[0];
174 | },
175 |
176 | ngettext : function(singular, plural, n) {
177 | var translated = Documentation.TRANSLATIONS[singular];
178 | if (typeof translated === 'undefined')
179 | return (n == 1) ? singular : plural;
180 | return translated[Documentation.PLURALEXPR(n)];
181 | },
182 |
183 | addTranslations : function(catalog) {
184 | for (var key in catalog.messages)
185 | this.TRANSLATIONS[key] = catalog.messages[key];
186 | this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')');
187 | this.LOCALE = catalog.locale;
188 | },
189 |
190 | /**
191 | * add context elements like header anchor links
192 | */
193 | addContextElements : function() {
194 | $('div[id] > :header:first').each(function() {
195 | $('').
196 | attr('href', '#' + this.id).
197 | attr('title', _('Permalink to this headline')).
198 | appendTo(this);
199 | });
200 | $('dt[id]').each(function() {
201 | $('').
202 | attr('href', '#' + this.id).
203 | attr('title', _('Permalink to this definition')).
204 | appendTo(this);
205 | });
206 | },
207 |
208 | /**
209 | * workaround a firefox stupidity
210 | * see: https://bugzilla.mozilla.org/show_bug.cgi?id=645075
211 | */
212 | fixFirefoxAnchorBug : function() {
213 | if (document.location.hash && $.browser.mozilla)
214 | window.setTimeout(function() {
215 | document.location.href += '';
216 | }, 10);
217 | },
218 |
219 | /**
220 | * highlight the search words provided in the url in the text
221 | */
222 | highlightSearchWords : function() {
223 | var params = $.getQueryParameters();
224 | var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : [];
225 | if (terms.length) {
226 | var body = $('div.body');
227 | if (!body.length) {
228 | body = $('body');
229 | }
230 | window.setTimeout(function() {
231 | $.each(terms, function() {
232 | body.highlightText(this.toLowerCase(), 'highlighted');
233 | });
234 | }, 10);
235 | $('
' + _('Hide Search Matches') + '
')
237 | .appendTo($('#searchbox'));
238 | }
239 | },
240 |
241 | /**
242 | * init the domain index toggle buttons
243 | */
244 | initIndexTable : function() {
245 | var togglers = $('img.toggler').click(function() {
246 | var src = $(this).attr('src');
247 | var idnum = $(this).attr('id').substr(7);
248 | $('tr.cg-' + idnum).toggle();
249 | if (src.substr(-9) === 'minus.png')
250 | $(this).attr('src', src.substr(0, src.length-9) + 'plus.png');
251 | else
252 | $(this).attr('src', src.substr(0, src.length-8) + 'minus.png');
253 | }).css('display', '');
254 | if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) {
255 | togglers.click();
256 | }
257 | },
258 |
259 | /**
260 | * helper function to hide the search marks again
261 | */
262 | hideSearchWords : function() {
263 | $('#searchbox .highlight-link').fadeOut(300);
264 | $('span.highlighted').removeClass('highlighted');
265 | var url = new URL(window.location);
266 | url.searchParams.delete('highlight');
267 | window.history.replaceState({}, '', url);
268 | },
269 |
270 | /**
271 | * helper function to focus on search bar
272 | */
273 | focusSearchBar : function() {
274 | $('input[name=q]').first().focus();
275 | },
276 |
277 | /**
278 | * make the url absolute
279 | */
280 | makeURL : function(relativeURL) {
281 | return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL;
282 | },
283 |
284 | /**
285 | * get the current relative url
286 | */
287 | getCurrentURL : function() {
288 | var path = document.location.pathname;
289 | var parts = path.split(/\//);
290 | $.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() {
291 | if (this === '..')
292 | parts.pop();
293 | });
294 | var url = parts.join('/');
295 | return path.substring(url.lastIndexOf('/') + 1, path.length - 1);
296 | },
297 |
298 | initOnKeyListeners: function() {
299 | // only install a listener if it is really needed
300 | if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS &&
301 | !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS)
302 | return;
303 |
304 | $(document).keydown(function(event) {
305 | var activeElementType = document.activeElement.tagName;
306 | // don't navigate when in search box, textarea, dropdown or button
307 | if (activeElementType !== 'TEXTAREA' && activeElementType !== 'INPUT' && activeElementType !== 'SELECT'
308 | && activeElementType !== 'BUTTON') {
309 | if (event.altKey || event.ctrlKey || event.metaKey)
310 | return;
311 |
312 | if (!event.shiftKey) {
313 | switch (event.key) {
314 | case 'ArrowLeft':
315 | if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS)
316 | break;
317 | var prevHref = $('link[rel="prev"]').prop('href');
318 | if (prevHref) {
319 | window.location.href = prevHref;
320 | return false;
321 | }
322 | break;
323 | case 'ArrowRight':
324 | if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS)
325 | break;
326 | var nextHref = $('link[rel="next"]').prop('href');
327 | if (nextHref) {
328 | window.location.href = nextHref;
329 | return false;
330 | }
331 | break;
332 | case 'Escape':
333 | if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS)
334 | break;
335 | Documentation.hideSearchWords();
336 | return false;
337 | }
338 | }
339 |
340 | // some keyboard layouts may need Shift to get /
341 | switch (event.key) {
342 | case '/':
343 | if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS)
344 | break;
345 | Documentation.focusSearchBar();
346 | return false;
347 | }
348 | }
349 | });
350 | }
351 | };
352 |
353 | // quick alias for translations
354 | _ = Documentation.gettext;
355 |
356 | $(document).ready(function() {
357 | Documentation.init();
358 | });
359 |
--------------------------------------------------------------------------------
/docs/_static/documentation_options.js:
--------------------------------------------------------------------------------
1 | var DOCUMENTATION_OPTIONS = {
2 | URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
3 | VERSION: '1.3.0',
4 | LANGUAGE: 'None',
5 | COLLAPSE_INDEX: false,
6 | BUILDER: 'html',
7 | FILE_SUFFIX: '.html',
8 | LINK_SUFFIX: '.html',
9 | HAS_SOURCE: true,
10 | SOURCELINK_SUFFIX: '.txt',
11 | NAVIGATION_WITH_KEYS: false,
12 | SHOW_SEARCH_SUMMARY: true,
13 | ENABLE_SEARCH_SHORTCUTS: true,
14 | };
--------------------------------------------------------------------------------
/docs/_static/language_data.js:
--------------------------------------------------------------------------------
1 | /*
2 | * language_data.js
3 | * ~~~~~~~~~~~~~~~~
4 | *
5 | * This script contains the language-specific data used by searchtools.js,
6 | * namely the list of stopwords, stemmer, scorer and splitter.
7 | *
8 | * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS.
9 | * :license: BSD, see LICENSE for details.
10 | *
11 | */
12 |
13 | var stopwords = ["a","and","are","as","at","be","but","by","for","if","in","into","is","it","near","no","not","of","on","or","such","that","the","their","then","there","these","they","this","to","was","will","with"];
14 |
15 |
16 | /* Non-minified version is copied as a separate JS file, is available */
17 |
18 | /**
19 | * Porter Stemmer
20 | */
21 | var Stemmer = function() {
22 |
23 | var step2list = {
24 | ational: 'ate',
25 | tional: 'tion',
26 | enci: 'ence',
27 | anci: 'ance',
28 | izer: 'ize',
29 | bli: 'ble',
30 | alli: 'al',
31 | entli: 'ent',
32 | eli: 'e',
33 | ousli: 'ous',
34 | ization: 'ize',
35 | ation: 'ate',
36 | ator: 'ate',
37 | alism: 'al',
38 | iveness: 'ive',
39 | fulness: 'ful',
40 | ousness: 'ous',
41 | aliti: 'al',
42 | iviti: 'ive',
43 | biliti: 'ble',
44 | logi: 'log'
45 | };
46 |
47 | var step3list = {
48 | icate: 'ic',
49 | ative: '',
50 | alize: 'al',
51 | iciti: 'ic',
52 | ical: 'ic',
53 | ful: '',
54 | ness: ''
55 | };
56 |
57 | var c = "[^aeiou]"; // consonant
58 | var v = "[aeiouy]"; // vowel
59 | var C = c + "[^aeiouy]*"; // consonant sequence
60 | var V = v + "[aeiou]*"; // vowel sequence
61 |
62 | var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0
63 | var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1
64 | var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1
65 | var s_v = "^(" + C + ")?" + v; // vowel in stem
66 |
67 | this.stemWord = function (w) {
68 | var stem;
69 | var suffix;
70 | var firstch;
71 | var origword = w;
72 |
73 | if (w.length < 3)
74 | return w;
75 |
76 | var re;
77 | var re2;
78 | var re3;
79 | var re4;
80 |
81 | firstch = w.substr(0,1);
82 | if (firstch == "y")
83 | w = firstch.toUpperCase() + w.substr(1);
84 |
85 | // Step 1a
86 | re = /^(.+?)(ss|i)es$/;
87 | re2 = /^(.+?)([^s])s$/;
88 |
89 | if (re.test(w))
90 | w = w.replace(re,"$1$2");
91 | else if (re2.test(w))
92 | w = w.replace(re2,"$1$2");
93 |
94 | // Step 1b
95 | re = /^(.+?)eed$/;
96 | re2 = /^(.+?)(ed|ing)$/;
97 | if (re.test(w)) {
98 | var fp = re.exec(w);
99 | re = new RegExp(mgr0);
100 | if (re.test(fp[1])) {
101 | re = /.$/;
102 | w = w.replace(re,"");
103 | }
104 | }
105 | else if (re2.test(w)) {
106 | var fp = re2.exec(w);
107 | stem = fp[1];
108 | re2 = new RegExp(s_v);
109 | if (re2.test(stem)) {
110 | w = stem;
111 | re2 = /(at|bl|iz)$/;
112 | re3 = new RegExp("([^aeiouylsz])\\1$");
113 | re4 = new RegExp("^" + C + v + "[^aeiouwxy]$");
114 | if (re2.test(w))
115 | w = w + "e";
116 | else if (re3.test(w)) {
117 | re = /.$/;
118 | w = w.replace(re,"");
119 | }
120 | else if (re4.test(w))
121 | w = w + "e";
122 | }
123 | }
124 |
125 | // Step 1c
126 | re = /^(.+?)y$/;
127 | if (re.test(w)) {
128 | var fp = re.exec(w);
129 | stem = fp[1];
130 | re = new RegExp(s_v);
131 | if (re.test(stem))
132 | w = stem + "i";
133 | }
134 |
135 | // Step 2
136 | re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/;
137 | if (re.test(w)) {
138 | var fp = re.exec(w);
139 | stem = fp[1];
140 | suffix = fp[2];
141 | re = new RegExp(mgr0);
142 | if (re.test(stem))
143 | w = stem + step2list[suffix];
144 | }
145 |
146 | // Step 3
147 | re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/;
148 | if (re.test(w)) {
149 | var fp = re.exec(w);
150 | stem = fp[1];
151 | suffix = fp[2];
152 | re = new RegExp(mgr0);
153 | if (re.test(stem))
154 | w = stem + step3list[suffix];
155 | }
156 |
157 | // Step 4
158 | re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/;
159 | re2 = /^(.+?)(s|t)(ion)$/;
160 | if (re.test(w)) {
161 | var fp = re.exec(w);
162 | stem = fp[1];
163 | re = new RegExp(mgr1);
164 | if (re.test(stem))
165 | w = stem;
166 | }
167 | else if (re2.test(w)) {
168 | var fp = re2.exec(w);
169 | stem = fp[1] + fp[2];
170 | re2 = new RegExp(mgr1);
171 | if (re2.test(stem))
172 | w = stem;
173 | }
174 |
175 | // Step 5
176 | re = /^(.+?)e$/;
177 | if (re.test(w)) {
178 | var fp = re.exec(w);
179 | stem = fp[1];
180 | re = new RegExp(mgr1);
181 | re2 = new RegExp(meq1);
182 | re3 = new RegExp("^" + C + v + "[^aeiouwxy]$");
183 | if (re.test(stem) || (re2.test(stem) && !(re3.test(stem))))
184 | w = stem;
185 | }
186 | re = /ll$/;
187 | re2 = new RegExp(mgr1);
188 | if (re.test(w) && re2.test(w)) {
189 | re = /.$/;
190 | w = w.replace(re,"");
191 | }
192 |
193 | // and turn initial Y back to y
194 | if (firstch == "y")
195 | w = firstch.toLowerCase() + w.substr(1);
196 | return w;
197 | }
198 | }
199 |
200 |
201 |
202 |
203 | var splitChars = (function() {
204 | var result = {};
205 | var singles = [96, 180, 187, 191, 215, 247, 749, 885, 903, 907, 909, 930, 1014, 1648,
206 | 1748, 1809, 2416, 2473, 2481, 2526, 2601, 2609, 2612, 2615, 2653, 2702,
207 | 2706, 2729, 2737, 2740, 2857, 2865, 2868, 2910, 2928, 2948, 2961, 2971,
208 | 2973, 3085, 3089, 3113, 3124, 3213, 3217, 3241, 3252, 3295, 3341, 3345,
209 | 3369, 3506, 3516, 3633, 3715, 3721, 3736, 3744, 3748, 3750, 3756, 3761,
210 | 3781, 3912, 4239, 4347, 4681, 4695, 4697, 4745, 4785, 4799, 4801, 4823,
211 | 4881, 5760, 5901, 5997, 6313, 7405, 8024, 8026, 8028, 8030, 8117, 8125,
212 | 8133, 8181, 8468, 8485, 8487, 8489, 8494, 8527, 11311, 11359, 11687, 11695,
213 | 11703, 11711, 11719, 11727, 11735, 12448, 12539, 43010, 43014, 43019, 43587,
214 | 43696, 43713, 64286, 64297, 64311, 64317, 64319, 64322, 64325, 65141];
215 | var i, j, start, end;
216 | for (i = 0; i < singles.length; i++) {
217 | result[singles[i]] = true;
218 | }
219 | var ranges = [[0, 47], [58, 64], [91, 94], [123, 169], [171, 177], [182, 184], [706, 709],
220 | [722, 735], [741, 747], [751, 879], [888, 889], [894, 901], [1154, 1161],
221 | [1318, 1328], [1367, 1368], [1370, 1376], [1416, 1487], [1515, 1519], [1523, 1568],
222 | [1611, 1631], [1642, 1645], [1750, 1764], [1767, 1773], [1789, 1790], [1792, 1807],
223 | [1840, 1868], [1958, 1968], [1970, 1983], [2027, 2035], [2038, 2041], [2043, 2047],
224 | [2070, 2073], [2075, 2083], [2085, 2087], [2089, 2307], [2362, 2364], [2366, 2383],
225 | [2385, 2391], [2402, 2405], [2419, 2424], [2432, 2436], [2445, 2446], [2449, 2450],
226 | [2483, 2485], [2490, 2492], [2494, 2509], [2511, 2523], [2530, 2533], [2546, 2547],
227 | [2554, 2564], [2571, 2574], [2577, 2578], [2618, 2648], [2655, 2661], [2672, 2673],
228 | [2677, 2692], [2746, 2748], [2750, 2767], [2769, 2783], [2786, 2789], [2800, 2820],
229 | [2829, 2830], [2833, 2834], [2874, 2876], [2878, 2907], [2914, 2917], [2930, 2946],
230 | [2955, 2957], [2966, 2968], [2976, 2978], [2981, 2983], [2987, 2989], [3002, 3023],
231 | [3025, 3045], [3059, 3076], [3130, 3132], [3134, 3159], [3162, 3167], [3170, 3173],
232 | [3184, 3191], [3199, 3204], [3258, 3260], [3262, 3293], [3298, 3301], [3312, 3332],
233 | [3386, 3388], [3390, 3423], [3426, 3429], [3446, 3449], [3456, 3460], [3479, 3481],
234 | [3518, 3519], [3527, 3584], [3636, 3647], [3655, 3663], [3674, 3712], [3717, 3718],
235 | [3723, 3724], [3726, 3731], [3752, 3753], [3764, 3772], [3774, 3775], [3783, 3791],
236 | [3802, 3803], [3806, 3839], [3841, 3871], [3892, 3903], [3949, 3975], [3980, 4095],
237 | [4139, 4158], [4170, 4175], [4182, 4185], [4190, 4192], [4194, 4196], [4199, 4205],
238 | [4209, 4212], [4226, 4237], [4250, 4255], [4294, 4303], [4349, 4351], [4686, 4687],
239 | [4702, 4703], [4750, 4751], [4790, 4791], [4806, 4807], [4886, 4887], [4955, 4968],
240 | [4989, 4991], [5008, 5023], [5109, 5120], [5741, 5742], [5787, 5791], [5867, 5869],
241 | [5873, 5887], [5906, 5919], [5938, 5951], [5970, 5983], [6001, 6015], [6068, 6102],
242 | [6104, 6107], [6109, 6111], [6122, 6127], [6138, 6159], [6170, 6175], [6264, 6271],
243 | [6315, 6319], [6390, 6399], [6429, 6469], [6510, 6511], [6517, 6527], [6572, 6592],
244 | [6600, 6607], [6619, 6655], [6679, 6687], [6741, 6783], [6794, 6799], [6810, 6822],
245 | [6824, 6916], [6964, 6980], [6988, 6991], [7002, 7042], [7073, 7085], [7098, 7167],
246 | [7204, 7231], [7242, 7244], [7294, 7400], [7410, 7423], [7616, 7679], [7958, 7959],
247 | [7966, 7967], [8006, 8007], [8014, 8015], [8062, 8063], [8127, 8129], [8141, 8143],
248 | [8148, 8149], [8156, 8159], [8173, 8177], [8189, 8303], [8306, 8307], [8314, 8318],
249 | [8330, 8335], [8341, 8449], [8451, 8454], [8456, 8457], [8470, 8472], [8478, 8483],
250 | [8506, 8507], [8512, 8516], [8522, 8525], [8586, 9311], [9372, 9449], [9472, 10101],
251 | [10132, 11263], [11493, 11498], [11503, 11516], [11518, 11519], [11558, 11567],
252 | [11622, 11630], [11632, 11647], [11671, 11679], [11743, 11822], [11824, 12292],
253 | [12296, 12320], [12330, 12336], [12342, 12343], [12349, 12352], [12439, 12444],
254 | [12544, 12548], [12590, 12592], [12687, 12689], [12694, 12703], [12728, 12783],
255 | [12800, 12831], [12842, 12880], [12896, 12927], [12938, 12976], [12992, 13311],
256 | [19894, 19967], [40908, 40959], [42125, 42191], [42238, 42239], [42509, 42511],
257 | [42540, 42559], [42592, 42593], [42607, 42622], [42648, 42655], [42736, 42774],
258 | [42784, 42785], [42889, 42890], [42893, 43002], [43043, 43055], [43062, 43071],
259 | [43124, 43137], [43188, 43215], [43226, 43249], [43256, 43258], [43260, 43263],
260 | [43302, 43311], [43335, 43359], [43389, 43395], [43443, 43470], [43482, 43519],
261 | [43561, 43583], [43596, 43599], [43610, 43615], [43639, 43641], [43643, 43647],
262 | [43698, 43700], [43703, 43704], [43710, 43711], [43715, 43738], [43742, 43967],
263 | [44003, 44015], [44026, 44031], [55204, 55215], [55239, 55242], [55292, 55295],
264 | [57344, 63743], [64046, 64047], [64110, 64111], [64218, 64255], [64263, 64274],
265 | [64280, 64284], [64434, 64466], [64830, 64847], [64912, 64913], [64968, 65007],
266 | [65020, 65135], [65277, 65295], [65306, 65312], [65339, 65344], [65371, 65381],
267 | [65471, 65473], [65480, 65481], [65488, 65489], [65496, 65497]];
268 | for (i = 0; i < ranges.length; i++) {
269 | start = ranges[i][0];
270 | end = ranges[i][1];
271 | for (j = start; j <= end; j++) {
272 | result[j] = true;
273 | }
274 | }
275 | return result;
276 | })();
277 |
278 | function splitQuery(query) {
279 | var result = [];
280 | var start = -1;
281 | for (var i = 0; i < query.length; i++) {
282 | if (splitChars[query.charCodeAt(i)]) {
283 | if (start !== -1) {
284 | result.push(query.slice(start, i));
285 | start = -1;
286 | }
287 | } else if (start === -1) {
288 | start = i;
289 | }
290 | }
291 | if (start !== -1) {
292 | result.push(query.slice(start));
293 | }
294 | return result;
295 | }
296 |
297 |
298 |
--------------------------------------------------------------------------------
/docs/_static/searchtools.js:
--------------------------------------------------------------------------------
1 | /*
2 | * searchtools.js
3 | * ~~~~~~~~~~~~~~~~
4 | *
5 | * Sphinx JavaScript utilities for the full-text search.
6 | *
7 | * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS.
8 | * :license: BSD, see LICENSE for details.
9 | *
10 | */
11 |
12 | if (!Scorer) {
13 | /**
14 | * Simple result scoring code.
15 | */
16 | var Scorer = {
17 | // Implement the following function to further tweak the score for each result
18 | // The function takes a result array [filename, title, anchor, descr, score]
19 | // and returns the new score.
20 | /*
21 | score: function(result) {
22 | return result[4];
23 | },
24 | */
25 |
26 | // query matches the full name of an object
27 | objNameMatch: 11,
28 | // or matches in the last dotted part of the object name
29 | objPartialMatch: 6,
30 | // Additive scores depending on the priority of the object
31 | objPrio: {0: 15, // used to be importantResults
32 | 1: 5, // used to be objectResults
33 | 2: -5}, // used to be unimportantResults
34 | // Used when the priority is not in the mapping.
35 | objPrioDefault: 0,
36 |
37 | // query found in title
38 | title: 15,
39 | partialTitle: 7,
40 | // query found in terms
41 | term: 5,
42 | partialTerm: 2
43 | };
44 | }
45 |
46 | if (!splitQuery) {
47 | function splitQuery(query) {
48 | return query.split(/\s+/);
49 | }
50 | }
51 |
52 | /**
53 | * Search Module
54 | */
55 | var Search = {
56 |
57 | _index : null,
58 | _queued_query : null,
59 | _pulse_status : -1,
60 |
61 | htmlToText : function(htmlString) {
62 | var virtualDocument = document.implementation.createHTMLDocument('virtual');
63 | var htmlElement = $(htmlString, virtualDocument);
64 | htmlElement.find('.headerlink').remove();
65 | docContent = htmlElement.find('[role=main]')[0];
66 | if(docContent === undefined) {
67 | console.warn("Content block not found. Sphinx search tries to obtain it " +
68 | "via '[role=main]'. Could you check your theme or template.");
69 | return "";
70 | }
71 | return docContent.textContent || docContent.innerText;
72 | },
73 |
74 | init : function() {
75 | var params = $.getQueryParameters();
76 | if (params.q) {
77 | var query = params.q[0];
78 | $('input[name="q"]')[0].value = query;
79 | this.performSearch(query);
80 | }
81 | },
82 |
83 | loadIndex : function(url) {
84 | $.ajax({type: "GET", url: url, data: null,
85 | dataType: "script", cache: true,
86 | complete: function(jqxhr, textstatus) {
87 | if (textstatus != "success") {
88 | document.getElementById("searchindexloader").src = url;
89 | }
90 | }});
91 | },
92 |
93 | setIndex : function(index) {
94 | var q;
95 | this._index = index;
96 | if ((q = this._queued_query) !== null) {
97 | this._queued_query = null;
98 | Search.query(q);
99 | }
100 | },
101 |
102 | hasIndex : function() {
103 | return this._index !== null;
104 | },
105 |
106 | deferQuery : function(query) {
107 | this._queued_query = query;
108 | },
109 |
110 | stopPulse : function() {
111 | this._pulse_status = 0;
112 | },
113 |
114 | startPulse : function() {
115 | if (this._pulse_status >= 0)
116 | return;
117 | function pulse() {
118 | var i;
119 | Search._pulse_status = (Search._pulse_status + 1) % 4;
120 | var dotString = '';
121 | for (i = 0; i < Search._pulse_status; i++)
122 | dotString += '.';
123 | Search.dots.text(dotString);
124 | if (Search._pulse_status > -1)
125 | window.setTimeout(pulse, 500);
126 | }
127 | pulse();
128 | },
129 |
130 | /**
131 | * perform a search for something (or wait until index is loaded)
132 | */
133 | performSearch : function(query) {
134 | // create the required interface elements
135 | this.out = $('#search-results');
136 | this.title = $('' + _('Searching') + '
').appendTo(this.out);
137 | this.dots = $('').appendTo(this.title);
138 | this.status = $('
').appendTo(this.out);
139 | this.output = $('').appendTo(this.out);
140 |
141 | $('#search-progress').text(_('Preparing search...'));
142 | this.startPulse();
143 |
144 | // index already loaded, the browser was quick!
145 | if (this.hasIndex())
146 | this.query(query);
147 | else
148 | this.deferQuery(query);
149 | },
150 |
151 | /**
152 | * execute search (requires search index to be loaded)
153 | */
154 | query : function(query) {
155 | var i;
156 |
157 | // stem the searchterms and add them to the correct list
158 | var stemmer = new Stemmer();
159 | var searchterms = [];
160 | var excluded = [];
161 | var hlterms = [];
162 | var tmp = splitQuery(query);
163 | var objectterms = [];
164 | for (i = 0; i < tmp.length; i++) {
165 | if (tmp[i] !== "") {
166 | objectterms.push(tmp[i].toLowerCase());
167 | }
168 |
169 | if ($u.indexOf(stopwords, tmp[i].toLowerCase()) != -1 || tmp[i] === "") {
170 | // skip this "word"
171 | continue;
172 | }
173 | // stem the word
174 | var word = stemmer.stemWord(tmp[i].toLowerCase());
175 | var toAppend;
176 | // select the correct list
177 | if (word[0] == '-') {
178 | toAppend = excluded;
179 | word = word.substr(1);
180 | }
181 | else {
182 | toAppend = searchterms;
183 | hlterms.push(tmp[i].toLowerCase());
184 | }
185 | // only add if not already in the list
186 | if (!$u.contains(toAppend, word))
187 | toAppend.push(word);
188 | }
189 | var highlightstring = '?highlight=' + $.urlencode(hlterms.join(" "));
190 |
191 | // console.debug('SEARCH: searching for:');
192 | // console.info('required: ', searchterms);
193 | // console.info('excluded: ', excluded);
194 |
195 | // prepare search
196 | var terms = this._index.terms;
197 | var titleterms = this._index.titleterms;
198 |
199 | // array of [filename, title, anchor, descr, score]
200 | var results = [];
201 | $('#search-progress').empty();
202 |
203 | // lookup as object
204 | for (i = 0; i < objectterms.length; i++) {
205 | var others = [].concat(objectterms.slice(0, i),
206 | objectterms.slice(i+1, objectterms.length));
207 | results = results.concat(this.performObjectSearch(objectterms[i], others));
208 | }
209 |
210 | // lookup as search terms in fulltext
211 | results = results.concat(this.performTermsSearch(searchterms, excluded, terms, titleterms));
212 |
213 | // let the scorer override scores with a custom scoring function
214 | if (Scorer.score) {
215 | for (i = 0; i < results.length; i++)
216 | results[i][4] = Scorer.score(results[i]);
217 | }
218 |
219 | // now sort the results by score (in opposite order of appearance, since the
220 | // display function below uses pop() to retrieve items) and then
221 | // alphabetically
222 | results.sort(function(a, b) {
223 | var left = a[4];
224 | var right = b[4];
225 | if (left > right) {
226 | return 1;
227 | } else if (left < right) {
228 | return -1;
229 | } else {
230 | // same score: sort alphabetically
231 | left = a[1].toLowerCase();
232 | right = b[1].toLowerCase();
233 | return (left > right) ? -1 : ((left < right) ? 1 : 0);
234 | }
235 | });
236 |
237 | // for debugging
238 | //Search.lastresults = results.slice(); // a copy
239 | //console.info('search results:', Search.lastresults);
240 |
241 | // print the results
242 | var resultCount = results.length;
243 | function displayNextItem() {
244 | // results left, load the summary and display it
245 | if (results.length) {
246 | var item = results.pop();
247 | var listItem = $('');
248 | var requestUrl = "";
249 | var linkUrl = "";
250 | if (DOCUMENTATION_OPTIONS.BUILDER === 'dirhtml') {
251 | // dirhtml builder
252 | var dirname = item[0] + '/';
253 | if (dirname.match(/\/index\/$/)) {
254 | dirname = dirname.substring(0, dirname.length-6);
255 | } else if (dirname == 'index/') {
256 | dirname = '';
257 | }
258 | requestUrl = DOCUMENTATION_OPTIONS.URL_ROOT + dirname;
259 | linkUrl = requestUrl;
260 |
261 | } else {
262 | // normal html builders
263 | requestUrl = DOCUMENTATION_OPTIONS.URL_ROOT + item[0] + DOCUMENTATION_OPTIONS.FILE_SUFFIX;
264 | linkUrl = item[0] + DOCUMENTATION_OPTIONS.LINK_SUFFIX;
265 | }
266 | listItem.append($('').attr('href',
267 | linkUrl +
268 | highlightstring + item[2]).html(item[1]));
269 | if (item[3]) {
270 | listItem.append($(' (' + item[3] + ')'));
271 | Search.output.append(listItem);
272 | setTimeout(function() {
273 | displayNextItem();
274 | }, 5);
275 | } else if (DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY) {
276 | $.ajax({url: requestUrl,
277 | dataType: "text",
278 | complete: function(jqxhr, textstatus) {
279 | var data = jqxhr.responseText;
280 | if (data !== '' && data !== undefined) {
281 | var summary = Search.makeSearchSummary(data, searchterms, hlterms);
282 | if (summary) {
283 | listItem.append(summary);
284 | }
285 | }
286 | Search.output.append(listItem);
287 | setTimeout(function() {
288 | displayNextItem();
289 | }, 5);
290 | }});
291 | } else {
292 | // just display title
293 | Search.output.append(listItem);
294 | setTimeout(function() {
295 | displayNextItem();
296 | }, 5);
297 | }
298 | }
299 | // search finished, update title and status message
300 | else {
301 | Search.stopPulse();
302 | Search.title.text(_('Search Results'));
303 | if (!resultCount)
304 | Search.status.text(_('Your search did not match any documents. Please make sure that all words are spelled correctly and that you\'ve selected enough categories.'));
305 | else
306 | Search.status.text(_('Search finished, found %s page(s) matching the search query.').replace('%s', resultCount));
307 | Search.status.fadeIn(500);
308 | }
309 | }
310 | displayNextItem();
311 | },
312 |
313 | /**
314 | * search for object names
315 | */
316 | performObjectSearch : function(object, otherterms) {
317 | var filenames = this._index.filenames;
318 | var docnames = this._index.docnames;
319 | var objects = this._index.objects;
320 | var objnames = this._index.objnames;
321 | var titles = this._index.titles;
322 |
323 | var i;
324 | var results = [];
325 |
326 | for (var prefix in objects) {
327 | for (var iMatch = 0; iMatch != objects[prefix].length; ++iMatch) {
328 | var match = objects[prefix][iMatch];
329 | var name = match[4];
330 | var fullname = (prefix ? prefix + '.' : '') + name;
331 | var fullnameLower = fullname.toLowerCase()
332 | if (fullnameLower.indexOf(object) > -1) {
333 | var score = 0;
334 | var parts = fullnameLower.split('.');
335 | // check for different match types: exact matches of full name or
336 | // "last name" (i.e. last dotted part)
337 | if (fullnameLower == object || parts[parts.length - 1] == object) {
338 | score += Scorer.objNameMatch;
339 | // matches in last name
340 | } else if (parts[parts.length - 1].indexOf(object) > -1) {
341 | score += Scorer.objPartialMatch;
342 | }
343 | var objname = objnames[match[1]][2];
344 | var title = titles[match[0]];
345 | // If more than one term searched for, we require other words to be
346 | // found in the name/title/description
347 | if (otherterms.length > 0) {
348 | var haystack = (prefix + ' ' + name + ' ' +
349 | objname + ' ' + title).toLowerCase();
350 | var allfound = true;
351 | for (i = 0; i < otherterms.length; i++) {
352 | if (haystack.indexOf(otherterms[i]) == -1) {
353 | allfound = false;
354 | break;
355 | }
356 | }
357 | if (!allfound) {
358 | continue;
359 | }
360 | }
361 | var descr = objname + _(', in ') + title;
362 |
363 | var anchor = match[3];
364 | if (anchor === '')
365 | anchor = fullname;
366 | else if (anchor == '-')
367 | anchor = objnames[match[1]][1] + '-' + fullname;
368 | // add custom score for some objects according to scorer
369 | if (Scorer.objPrio.hasOwnProperty(match[2])) {
370 | score += Scorer.objPrio[match[2]];
371 | } else {
372 | score += Scorer.objPrioDefault;
373 | }
374 | results.push([docnames[match[0]], fullname, '#'+anchor, descr, score, filenames[match[0]]]);
375 | }
376 | }
377 | }
378 |
379 | return results;
380 | },
381 |
382 | /**
383 | * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
384 | */
385 | escapeRegExp : function(string) {
386 | return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
387 | },
388 |
389 | /**
390 | * search for full-text terms in the index
391 | */
392 | performTermsSearch : function(searchterms, excluded, terms, titleterms) {
393 | var docnames = this._index.docnames;
394 | var filenames = this._index.filenames;
395 | var titles = this._index.titles;
396 |
397 | var i, j, file;
398 | var fileMap = {};
399 | var scoreMap = {};
400 | var results = [];
401 |
402 | // perform the search on the required terms
403 | for (i = 0; i < searchterms.length; i++) {
404 | var word = searchterms[i];
405 | var files = [];
406 | var _o = [
407 | {files: terms[word], score: Scorer.term},
408 | {files: titleterms[word], score: Scorer.title}
409 | ];
410 | // add support for partial matches
411 | if (word.length > 2) {
412 | var word_regex = this.escapeRegExp(word);
413 | for (var w in terms) {
414 | if (w.match(word_regex) && !terms[word]) {
415 | _o.push({files: terms[w], score: Scorer.partialTerm})
416 | }
417 | }
418 | for (var w in titleterms) {
419 | if (w.match(word_regex) && !titleterms[word]) {
420 | _o.push({files: titleterms[w], score: Scorer.partialTitle})
421 | }
422 | }
423 | }
424 |
425 | // no match but word was a required one
426 | if ($u.every(_o, function(o){return o.files === undefined;})) {
427 | break;
428 | }
429 | // found search word in contents
430 | $u.each(_o, function(o) {
431 | var _files = o.files;
432 | if (_files === undefined)
433 | return
434 |
435 | if (_files.length === undefined)
436 | _files = [_files];
437 | files = files.concat(_files);
438 |
439 | // set score for the word in each file to Scorer.term
440 | for (j = 0; j < _files.length; j++) {
441 | file = _files[j];
442 | if (!(file in scoreMap))
443 | scoreMap[file] = {};
444 | scoreMap[file][word] = o.score;
445 | }
446 | });
447 |
448 | // create the mapping
449 | for (j = 0; j < files.length; j++) {
450 | file = files[j];
451 | if (file in fileMap && fileMap[file].indexOf(word) === -1)
452 | fileMap[file].push(word);
453 | else
454 | fileMap[file] = [word];
455 | }
456 | }
457 |
458 | // now check if the files don't contain excluded terms
459 | for (file in fileMap) {
460 | var valid = true;
461 |
462 | // check if all requirements are matched
463 | var filteredTermCount = // as search terms with length < 3 are discarded: ignore
464 | searchterms.filter(function(term){return term.length > 2}).length
465 | if (
466 | fileMap[file].length != searchterms.length &&
467 | fileMap[file].length != filteredTermCount
468 | ) continue;
469 |
470 | // ensure that none of the excluded terms is in the search result
471 | for (i = 0; i < excluded.length; i++) {
472 | if (terms[excluded[i]] == file ||
473 | titleterms[excluded[i]] == file ||
474 | $u.contains(terms[excluded[i]] || [], file) ||
475 | $u.contains(titleterms[excluded[i]] || [], file)) {
476 | valid = false;
477 | break;
478 | }
479 | }
480 |
481 | // if we have still a valid result we can add it to the result list
482 | if (valid) {
483 | // select one (max) score for the file.
484 | // for better ranking, we should calculate ranking by using words statistics like basic tf-idf...
485 | var score = $u.max($u.map(fileMap[file], function(w){return scoreMap[file][w]}));
486 | results.push([docnames[file], titles[file], '', null, score, filenames[file]]);
487 | }
488 | }
489 | return results;
490 | },
491 |
492 | /**
493 | * helper function to return a node containing the
494 | * search summary for a given text. keywords is a list
495 | * of stemmed words, hlwords is the list of normal, unstemmed
496 | * words. the first one is used to find the occurrence, the
497 | * latter for highlighting it.
498 | */
499 | makeSearchSummary : function(htmlText, keywords, hlwords) {
500 | var text = Search.htmlToText(htmlText);
501 | if (text == "") {
502 | return null;
503 | }
504 | var textLower = text.toLowerCase();
505 | var start = 0;
506 | $.each(keywords, function() {
507 | var i = textLower.indexOf(this.toLowerCase());
508 | if (i > -1)
509 | start = i;
510 | });
511 | start = Math.max(start - 120, 0);
512 | var excerpt = ((start > 0) ? '...' : '') +
513 | $.trim(text.substr(start, 240)) +
514 | ((start + 240 - text.length) ? '...' : '');
515 | var rv = $('').text(excerpt);
516 | $.each(hlwords, function() {
517 | rv = rv.highlightText(this, 'highlighted');
518 | });
519 | return rv;
520 | }
521 | };
522 |
523 | $(document).ready(function() {
524 | Search.init();
525 | });
526 |
--------------------------------------------------------------------------------
/docs/genindex.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Index — Prognostics As-A-Service (PaaS) Sandbox 1.3.0 documentation
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
Index
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
85 |
86 |
87 |
88 | Copyright © 2021 United States Government as represented by the Administrator of the National Aeronautics and Space Administration. All Rights Reserved.
89 |
90 |
91 |
--------------------------------------------------------------------------------
/docs/getting_started.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/load_ests.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/objects.inv:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nasa/prog_server/f5753795ebbfb7794cb7a62a6e4bf23c925b6fd5/docs/objects.inv
--------------------------------------------------------------------------------
/docs/prog_client.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/search.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Search — Prognostics As-A-Service (PaaS) Sandbox 1.3.0 documentation
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
Search
40 |
41 |
49 |
50 |
51 |
52 | Searching for multiple words only shows matches that contain
53 | all words.
54 |
55 |
56 |
57 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
104 |
105 |
106 |
107 | Copyright © 2021 United States Government as represented by the Administrator of the National Aeronautics and Space Administration. All Rights Reserved.
108 |
109 |
110 |
--------------------------------------------------------------------------------
/docs/searchindex.js:
--------------------------------------------------------------------------------
1 | Search.setIndex({docnames:["getting_started","index","load_ests","prog_client"],envversion:{"sphinx.domains.c":2,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":5,"sphinx.domains.index":1,"sphinx.domains.javascript":2,"sphinx.domains.math":2,"sphinx.domains.python":3,"sphinx.domains.rst":2,"sphinx.domains.std":2,sphinx:56},filenames:["getting_started.rst","index.rst","load_ests.rst","prog_client.rst"],objects:{},objnames:{},objtypes:{},terms:{document:[0,1,2,3],github:[0,1,2,3],http:[0,1,2,3],io:[0,1,2,3],move:[0,1,2,3],nasa:[0,1,2,3],progpi:[0,1,2,3]},titles:["Getting Started","Prognostics As-A-Service (PaaS) Sandbox","Load Estimators","prog_client"],titleterms:{A:1,As:1,estim:2,get:0,load:2,paa:1,prog_client:3,prognost:1,sandbox:1,servic:1,start:0}})
--------------------------------------------------------------------------------
/examples/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2021 United States Government as represented by the Administrator of the
2 | # National Aeronautics and Space Administration. All Rights Reserved.
3 |
4 | __all__ = ['online_prog', 'option_scoring']
5 |
--------------------------------------------------------------------------------
/examples/online_prog.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2021 United States Government as represented by the Administrator of the
2 | # National Aeronautics and Space Administration. All Rights Reserved.
3 |
4 | """
5 | This example shows how to use the PaaS Client and Server for online prognostics. Prior to running the example start the server in a terminal window with the command:
6 | python -m prog_server
7 |
8 | This example creates a session with the server to run prognostics for a Thrown Object, a simplified model of an object thrown into the air. Data is then sent to the server and a prediction is requested. The prediction is then displayed.
9 | """
10 |
11 | import prog_client
12 | from pprint import pprint
13 | from time import sleep
14 |
15 | def run_example():
16 | # Step 1: Open a session with the server for a thrown object.
17 | # Use all default configuration options.
18 | # Except for the save frequency, which we'll set to 1 second.
19 | session = prog_client.Session('ThrownObject', pred_cfg={'save_freq': 1})
20 | print(session) # Printing the Session Information
21 |
22 | # Step 2: Prepare data to send to server
23 | # The data is a dictionary of values. The keys are the names of the inputs and outputs in the model.
24 | # Format (time, value)
25 | # Note: in an actual application, the data would be received from a sensor or other source.
26 | # The structure below is used to emulate the sensor.
27 | example_data = [
28 | (0, {'x': 1.83}),
29 | (0.1, {'x': 5.81}),
30 | (0.2, {'x': 9.75}),
31 | (0.3, {'x': 13.51}),
32 | (0.4, {'x': 17.20}),
33 | (0.5, {'x': 20.87}),
34 | (0.6, {'x': 24.37}),
35 | (0.7, {'x': 27.75}),
36 | (0.8, {'x': 31.09}),
37 | (0.9, {'x': 34.30}),
38 | (1.0, {'x': 37.42}),
39 | (1.1, {'x': 40.43}),
40 | (1.2, {'x': 43.35}),
41 | (1.3, {'x': 46.17}),
42 | (1.4, {'x': 48.91}),
43 | (1.5, {'x': 51.53}),
44 | (1.6, {'x': 54.05}),
45 | (1.7, {'x': 56.50}),
46 | (1.8, {'x': 58.82}),
47 | (1.9, {'x': 61.05}),
48 | (2.0, {'x': 63.20}),
49 | (2.1, {'x': 65.23}),
50 | (2.2, {'x': 67.17}),
51 | (2.3, {'x': 69.02}),
52 | (2.4, {'x': 70.75}),
53 | (2.5, {'x': 72.40})
54 | ]
55 |
56 | # Step 3: Send data to server, checking periodically for a prediction result.
57 | LAST_PREDICTION_TIME = None
58 | for i in range(len(example_data)):
59 | # Send data to server
60 | print(f'{example_data[i][0]}s: Sending data to server... ', end='')
61 | session.send_data(time=example_data[i][0], **example_data[i][1])
62 |
63 | # Check for a prediction result
64 | status = session.get_prediction_status()
65 | if LAST_PREDICTION_TIME != status["last prediction"]:
66 | # New prediction result
67 | LAST_PREDICTION_TIME = status["last prediction"]
68 | print('Prediction Completed')
69 |
70 | # Get prediction
71 | # Prediction is returned as a type uncertain_data, so you can manipulate it like that datatype.
72 | # See https://nasa.github.io/prog_algs/uncertain_data.html
73 | t, prediction = session.get_predicted_toe()
74 | print(f'Predicted ToE (using state from {t}s): ')
75 | pprint(prediction.mean)
76 |
77 | # Get Predicted future states
78 | # You can also get the predicted future states of the model.
79 | # States are saved according to the prediction configuration parameter 'save_freq' or 'save_pts'
80 | # In this example we have it setup to save every 1 second.
81 | # Return type is UnweightedSamplesPrediction (since we're using the monte carlo predictor)
82 | # See https://nasa.github.io/prog_algs
83 | t, event_states = session.get_predicted_event_state()
84 | print(f'Predicted Event States (using state from {t}s): ')
85 | es_means = [(event_states.times[i], event_states.snapshot(i).mean) for i in range(len(event_states.times))]
86 | for time, es_mean in es_means:
87 | print(f"\t{time}s: {es_mean}")
88 |
89 | # Note: you can also get the predicted future states of the model (see get_predicted_states()) or performance parameters (see get_predicted_performance_metrics())
90 |
91 | else:
92 | print('No prediction yet')
93 | # No updated prediction, send more data and check again later.
94 | sleep(0.1)
95 |
96 | # Notice that the prediction wasn't updated every time step. It takes a bit of time to perform a prediction.
97 |
98 | # Note: You can also get the model from prog_server to work with directly.
99 | model = session.get_model()
100 |
101 | # This allows the module to be executed directly
102 | if __name__ == '__main__':
103 | run_example()
104 |
--------------------------------------------------------------------------------
/examples/option_scoring.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2021 United States Government as represented by the Administrator of the
2 | # National Aeronautics and Space Administration. All Rights Reserved.
3 |
4 | """
5 | This example demonstrates how to score multiple considered options using the PaaS Sandbox. Prior to running the example start the server in a terminal window with the command:
6 | python -m prog_server
7 |
8 | This example creates a session with the server to run prognostics for a BatteryCircuit. Three options with different loading profiles are compared by creating a session for each option and comparing the resulting prediction metrics
9 | """
10 |
11 | import prog_client
12 | import time
13 |
14 | def run_example():
15 | # Step 1: Prepare load profiles to compare
16 | # Create a load profile for each option
17 | # Each load profile has format Array[Dict]
18 | # Where each dict is in format {TIME: LOAD}
19 | # The TIME is the start of that loading in seconds
20 | # LOAD is a dict with keys corresponding to model.inputs
21 | # Note: Dict must be in order of increasing time
22 | LOAD_PROFILES = [
23 | { # Plan 0
24 | 0: {'i': 2},
25 | 600: {'i': 1},
26 | 900: {'i': 4},
27 | 1800: {'i': 2},
28 | 3000: {'i': 3}
29 | },
30 | { # Plan 1
31 | 0: {'i': 3},
32 | 900: {'i': 2},
33 | 1000: {'i': 3.5},
34 | 2000: {'i': 2.5},
35 | 2300: {'i': 3}
36 | },
37 | { # Plan 2
38 | 0: {'i': 1.25},
39 | 800: {'i': 2},
40 | 1100: {'i': 2.5},
41 | 2200: {'i': 6},
42 | }
43 | ]
44 |
45 | # Step 2: Open a session with the server for a thrown object.
46 | # We are specifying a time of interest of 2000 seconds.
47 | # This could be the end of a mission/session, or some inspection time.
48 | print('\nStarting Sessions')
49 | sessions = [prog_client.Session('BatteryCircuit', pred_cfg = {'save_pts': [2000], 'save_freq': 1e99, 'n_samples':15}, load_est = 'Variable', load_est_cfg = LOAD_PROFILES[i]) for i in range(len(LOAD_PROFILES))]
50 |
51 | # Step 3: Wait for prognostics to complete
52 | print('\nWaiting for sessions to complete (this may take a bit)')
53 | STEP = 15 # Time to wait between pinging server (s)
54 |
55 | for session in sessions:
56 | sessions_in_progress = True
57 | while sessions_in_progress:
58 | sessions_in_progress = False
59 | status = session.get_prediction_status()
60 | if status['in progress'] != 0:
61 | print(f'\tSession {session.session_id} is still in progress')
62 | sessions_in_progress = True
63 | time.sleep(STEP)
64 | print(f'\tSession {session.session_id} complete')
65 | print('All sessions complete')
66 |
67 | # Step 4: Get the results
68 | print('Getting results')
69 | results = [session.get_predicted_toe()[1] for session in sessions]
70 |
71 | # Step 5: Compare results
72 | print('\nComparing results')
73 | print('Mean ToE:')
74 | best_toe = 0
75 | best_plan = None
76 | for i in range(len(results)):
77 | mean_toe = results[i].mean['EOD']
78 | print(f'\tOption {i}: {mean_toe:0.2f}s')
79 | if mean_toe > best_toe:
80 | best_toe = mean_toe
81 | best_plan = i
82 | print(f'Best option using method 1: Option {best_plan}')
83 |
84 | print('\nSOC at point of interest (2000 sec):')
85 | best_soc = 0
86 | best_plan = None
87 | soc = [session.get_predicted_event_state()[1] for session in sessions]
88 | for i in range(len(soc)):
89 | mean_soc = soc[i].snapshot(-1).mean['EOD']
90 | print(f'\tOption {i}: {mean_soc:0.3f} SOC')
91 | if mean_soc > best_soc:
92 | best_soc = mean_soc
93 | best_plan = i
94 | print(f'Best option using method 2: Option {best_plan}')
95 |
96 | # Other metrics can be used as well, like probability of mission success given a certain mission time, uncertainty in ToE estimate, final state at end of mission, etc.
97 |
98 | # This allows the module to be executed directly
99 | if __name__ == '__main__':
100 | run_example()
101 |
--------------------------------------------------------------------------------
/forms/PaaS_Corporate CLA.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nasa/prog_server/f5753795ebbfb7794cb7a62a6e4bf23c925b6fd5/forms/PaaS_Corporate CLA.pdf
--------------------------------------------------------------------------------
/forms/PaaS_Individual CLA.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nasa/prog_server/f5753795ebbfb7794cb7a62a6e4bf23c925b6fd5/forms/PaaS_Individual CLA.pdf
--------------------------------------------------------------------------------
/license.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nasa/prog_server/f5753795ebbfb7794cb7a62a6e4bf23c925b6fd5/license.pdf
--------------------------------------------------------------------------------
/scripts/test_copyright.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2021 United States Government as represented by the Administrator of the
2 | # National Aeronautics and Space Administration. All Rights Reserved.
3 |
4 | import os
5 | COPYRIGHT_TAG = "Copyright © 2021 United States Government as represented by the Administrator" # String to check file lines for
6 |
7 | def check_copyright(directory : str, invalid_files : list) -> bool:
8 | result = True
9 |
10 | for filename in os.listdir(directory):
11 | path = os.path.join(directory, filename)
12 |
13 | # If path is subdirectory, recursively check files/subdirectories within
14 | if os.path.isdir(path):
15 | result = result and check_copyright(path, invalid_files)
16 | # If path is a file, ensure it is of type py and check for copyright
17 | elif os.path.isfile(path) and path[-2:] == "py":
18 | file = open(path, 'r')
19 | copyright_met = False
20 | # Iterate over lines in file, check each line against COPYRIGHT_TAG
21 | for line in file:
22 | if COPYRIGHT_TAG in line: # File contains copyright, skip rest of lines
23 | file.close()
24 | copyright_met = True
25 | if copyright_met:
26 | break
27 | if not copyright_met:
28 | result = False
29 | invalid_files.append(path)
30 | file.close()
31 |
32 | return result
33 |
34 | def main():
35 | print("\n\nTesting Files for Copyright Information")
36 |
37 | root = '../prog_server'
38 | invalid_files = []
39 | copyright_confirmed = check_copyright(root, invalid_files)
40 |
41 | if not copyright_confirmed:
42 | raise Exception(f"Failed test\nFiles missing copyright information: {invalid_files}")
43 |
44 | if __name__ == '__main__':
45 | main()
46 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2021 United States Government as represented by the Administrator of the
2 | # National Aeronautics and Space Administration. All Rights Reserved.
3 |
4 | from setuptools import setup, find_packages
5 | import pathlib
6 |
7 | here = pathlib.Path(__file__).parent.resolve()
8 |
9 | # Get the long description from the README file
10 | long_description = (here / 'README.md').read_text(encoding='utf-8')
11 |
12 | setup(
13 | name='prog_server',
14 | version='1.7.0',
15 | description='The NASA Prognostics As-A-Service (PaaS) Sandbox (a.k.a. prog_server) is a simplified Software Oriented Architecture (SOA) for performing prognostics of engineering systems. The PaaS Sandbox is a wrapper around the Prognostics Algorithms and Models Packages, allowing 1+ users to access these packages features through a REST API. The package is intended to be used as a research tool to prototype and benchmark Prognostics As-A-Service (PaaS) architectures and work on the challenges facing such architectures',
16 | long_description=long_description,
17 | long_description_content_type='text/markdown',
18 | url='https://nasa.github.com/progpy/prog_server_guide.html',
19 | author='Christopher Teubert',
20 | author_email='christopher.a.teubert@nasa.gov',
21 | classifiers=[
22 | 'Development Status :: 4 - Beta',
23 | 'Intended Audience :: Science/Research',
24 | 'Intended Audience :: Developers',
25 | 'Intended Audience :: Manufacturing',
26 | 'Topic :: Scientific/Engineering',
27 | 'Topic :: Scientific/Engineering :: Artificial Intelligence',
28 | 'Topic :: Scientific/Engineering :: Physics',
29 | 'License :: Other/Proprietary License ',
30 | 'Programming Language :: Python :: 3',
31 | 'Programming Language :: Python :: 3.7',
32 | 'Programming Language :: Python :: 3.8',
33 | 'Programming Language :: Python :: 3.9',
34 | 'Programming Language :: Python :: 3.10',
35 | 'Programming Language :: Python :: 3.11',
36 | 'Programming Language :: Python :: 3.12',
37 | 'Programming Language :: Python :: 3 :: Only'
38 | ],
39 | keywords=['prognostics', 'diagnostics', 'fault detection', 'fdir', 'physics modeling', 'prognostics and health management', 'PHM', 'health management', 'prognostics as a service', 'ivhm'],
40 | package_dir={"":"src"},
41 | packages=find_packages(where='src'),
42 | python_requires='>=3.7, <3.13',
43 | install_requires=[
44 | 'progpy',
45 | 'requests',
46 | 'urllib3',
47 | 'flask'
48 | ],
49 | license='NOSA',
50 | project_urls={ # Optional
51 | 'Bug Reports': 'https://github.com/nasa/prog_server/issues',
52 | 'Docs': 'https://nasa.github.io/progpy/prog_server_guide.html',
53 | 'Organization': 'https://www.nasa.gov/content/diagnostics-prognostics',
54 | 'Source': 'https://github.com/nasa/prog_server',
55 | },
56 | )
57 |
--------------------------------------------------------------------------------
/specs/prog_server.postman_collection.json:
--------------------------------------------------------------------------------
1 | {
2 | "info": {
3 | "_postman_id": "2ba3dc40-0e98-43e1-91c6-c4fc1bc7dd6d",
4 | "name": "prog_server",
5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
6 | },
7 | "item": [
8 | {
9 | "name": "Get API",
10 | "request": {
11 | "method": "GET",
12 | "header": [],
13 | "url": {
14 | "raw": "http://127.0.0.1:5000/api/v1/",
15 | "protocol": "http",
16 | "host": [
17 | "127",
18 | "0",
19 | "0",
20 | "1"
21 | ],
22 | "port": "5000",
23 | "path": [
24 | "api",
25 | "v1",
26 | ""
27 | ]
28 | }
29 | },
30 | "response": []
31 | },
32 | {
33 | "name": "Create Session",
34 | "request": {
35 | "method": "PUT",
36 | "header": [],
37 | "body": {
38 | "mode": "formdata",
39 | "formdata": [
40 | {
41 | "key": "model_name",
42 | "value": "BatteryCircuit",
43 | "type": "text"
44 | }
45 | ]
46 | },
47 | "url": {
48 | "raw": "http://127.0.0.1:5000/api/v1/session",
49 | "protocol": "http",
50 | "host": [
51 | "127",
52 | "0",
53 | "0",
54 | "1"
55 | ],
56 | "port": "5000",
57 | "path": [
58 | "api",
59 | "v1",
60 | "session"
61 | ]
62 | }
63 | },
64 | "response": []
65 | },
66 | {
67 | "name": "Get Sessions",
68 | "request": {
69 | "method": "GET",
70 | "header": [],
71 | "url": {
72 | "raw": "http://127.0.0.1:5000/api/v1/session/",
73 | "protocol": "http",
74 | "host": [
75 | "127",
76 | "0",
77 | "0",
78 | "1"
79 | ],
80 | "port": "5000",
81 | "path": [
82 | "api",
83 | "v1",
84 | "session",
85 | ""
86 | ]
87 | }
88 | },
89 | "response": []
90 | },
91 | {
92 | "name": "Get Session",
93 | "request": {
94 | "method": "GET",
95 | "header": [],
96 | "url": {
97 | "raw": "http://127.0.0.1:5000/api/v1/session/0",
98 | "protocol": "http",
99 | "host": [
100 | "127",
101 | "0",
102 | "0",
103 | "1"
104 | ],
105 | "port": "5000",
106 | "path": [
107 | "api",
108 | "v1",
109 | "session",
110 | "0"
111 | ]
112 | }
113 | },
114 | "response": []
115 | },
116 | {
117 | "name": "Add Data",
118 | "request": {
119 | "method": "POST",
120 | "header": [],
121 | "body": {
122 | "mode": "formdata",
123 | "formdata": [
124 | {
125 | "key": "i",
126 | "value": "2",
127 | "type": "text"
128 | },
129 | {
130 | "key": "v",
131 | "value": "3.915",
132 | "type": "text"
133 | },
134 | {
135 | "key": "t",
136 | "value": "32.2",
137 | "type": "text"
138 | },
139 | {
140 | "key": "time",
141 | "value": "0.1",
142 | "type": "text"
143 | }
144 | ]
145 | },
146 | "url": {
147 | "raw": "http://127.0.0.1:5000/api/v1/session/0/data",
148 | "protocol": "http",
149 | "host": [
150 | "127",
151 | "0",
152 | "0",
153 | "1"
154 | ],
155 | "port": "5000",
156 | "path": [
157 | "api",
158 | "v1",
159 | "session",
160 | "0",
161 | "data"
162 | ]
163 | }
164 | },
165 | "response": []
166 | },
167 | {
168 | "name": "Stop Session",
169 | "request": {
170 | "method": "DELETE",
171 | "header": [],
172 | "url": {
173 | "raw": "http://127.0.0.1:5000/api/v1/session/0",
174 | "protocol": "http",
175 | "host": [
176 | "127",
177 | "0",
178 | "0",
179 | "1"
180 | ],
181 | "port": "5000",
182 | "path": [
183 | "api",
184 | "v1",
185 | "session",
186 | "0"
187 | ]
188 | }
189 | },
190 | "response": []
191 | },
192 | {
193 | "name": "Get System State",
194 | "request": {
195 | "method": "GET",
196 | "header": [],
197 | "url": {
198 | "raw": "http://127.0.0.1:5000/api/v1/session/0/state",
199 | "protocol": "http",
200 | "host": [
201 | "127",
202 | "0",
203 | "0",
204 | "1"
205 | ],
206 | "port": "5000",
207 | "path": [
208 | "api",
209 | "v1",
210 | "session",
211 | "0",
212 | "state"
213 | ]
214 | }
215 | },
216 | "response": []
217 | },
218 | {
219 | "name": "Get Event State",
220 | "request": {
221 | "method": "GET",
222 | "header": [],
223 | "url": {
224 | "raw": "http://127.0.0.1:5000/api/v1/session/0/event_state",
225 | "protocol": "http",
226 | "host": [
227 | "127",
228 | "0",
229 | "0",
230 | "1"
231 | ],
232 | "port": "5000",
233 | "path": [
234 | "api",
235 | "v1",
236 | "session",
237 | "0",
238 | "event_state"
239 | ]
240 | }
241 | },
242 | "response": []
243 | },
244 | {
245 | "name": "Get Observables",
246 | "request": {
247 | "method": "GET",
248 | "header": [],
249 | "url": {
250 | "raw": "http://127.0.0.1:5000/api/v1/session/0/observables",
251 | "protocol": "http",
252 | "host": [
253 | "127",
254 | "0",
255 | "0",
256 | "1"
257 | ],
258 | "port": "5000",
259 | "path": [
260 | "api",
261 | "v1",
262 | "session",
263 | "0",
264 | "observables"
265 | ]
266 | }
267 | },
268 | "response": []
269 | },
270 | {
271 | "name": "Get Initialization Status",
272 | "request": {
273 | "method": "GET",
274 | "header": [],
275 | "url": {
276 | "raw": "http://127.0.0.1:5000/api/v1/session/0/initialized",
277 | "protocol": "http",
278 | "host": [
279 | "127",
280 | "0",
281 | "0",
282 | "1"
283 | ],
284 | "port": "5000",
285 | "path": [
286 | "api",
287 | "v1",
288 | "session",
289 | "0",
290 | "initialized"
291 | ]
292 | }
293 | },
294 | "response": []
295 | },
296 | {
297 | "name": "Get Loading",
298 | "request": {
299 | "method": "GET",
300 | "header": [],
301 | "url": {
302 | "raw": "http://127.0.0.1:5000/api/v1/session/0/loading",
303 | "protocol": "http",
304 | "host": [
305 | "127",
306 | "0",
307 | "0",
308 | "1"
309 | ],
310 | "port": "5000",
311 | "path": [
312 | "api",
313 | "v1",
314 | "session",
315 | "0",
316 | "loading"
317 | ]
318 | }
319 | },
320 | "response": []
321 | },
322 | {
323 | "name": "Set Loading",
324 | "request": {
325 | "method": "POST",
326 | "header": [],
327 | "body": {
328 | "mode": "formdata",
329 | "formdata": [
330 | {
331 | "key": "type",
332 | "value": "Const",
333 | "type": "text"
334 | },
335 | {
336 | "key": "cfg",
337 | "value": "{\"load\": 4}",
338 | "type": "text"
339 | }
340 | ]
341 | },
342 | "url": {
343 | "raw": "http://127.0.0.1:5000/api/v1/session/0/loading",
344 | "protocol": "http",
345 | "host": [
346 | "127",
347 | "0",
348 | "0",
349 | "1"
350 | ],
351 | "port": "5000",
352 | "path": [
353 | "api",
354 | "v1",
355 | "session",
356 | "0",
357 | "loading"
358 | ]
359 | }
360 | },
361 | "response": []
362 | },
363 | {
364 | "name": "Get Prediction Status",
365 | "request": {
366 | "method": "GET",
367 | "header": [],
368 | "url": {
369 | "raw": "http://127.0.0.1:5000/api/v1/session/0/prediction/status",
370 | "protocol": "http",
371 | "host": [
372 | "127",
373 | "0",
374 | "0",
375 | "1"
376 | ],
377 | "port": "5000",
378 | "path": [
379 | "api",
380 | "v1",
381 | "session",
382 | "0",
383 | "prediction",
384 | "status"
385 | ]
386 | }
387 | },
388 | "response": []
389 | },
390 | {
391 | "name": "Get Predicted States",
392 | "request": {
393 | "method": "GET",
394 | "header": [],
395 | "url": {
396 | "raw": "http://127.0.0.1:5000/api/v1/session/0/prediction/state",
397 | "protocol": "http",
398 | "host": [
399 | "127",
400 | "0",
401 | "0",
402 | "1"
403 | ],
404 | "port": "5000",
405 | "path": [
406 | "api",
407 | "v1",
408 | "session",
409 | "0",
410 | "prediction",
411 | "state"
412 | ]
413 | }
414 | },
415 | "response": []
416 | },
417 | {
418 | "name": "Get Predicted Events",
419 | "request": {
420 | "method": "GET",
421 | "header": [],
422 | "url": {
423 | "raw": "http://127.0.0.1:5000/api/v1/session/0/prediction/events",
424 | "protocol": "http",
425 | "host": [
426 | "127",
427 | "0",
428 | "0",
429 | "1"
430 | ],
431 | "port": "5000",
432 | "path": [
433 | "api",
434 | "v1",
435 | "session",
436 | "0",
437 | "prediction",
438 | "events"
439 | ]
440 | }
441 | },
442 | "response": []
443 | },
444 | {
445 | "name": "Get Predicted Event States",
446 | "request": {
447 | "method": "GET",
448 | "header": [],
449 | "url": {
450 | "raw": "http://127.0.0.1:5000/api/v1/session/0/prediction/event_state",
451 | "protocol": "http",
452 | "host": [
453 | "127",
454 | "0",
455 | "0",
456 | "1"
457 | ],
458 | "port": "5000",
459 | "path": [
460 | "api",
461 | "v1",
462 | "session",
463 | "0",
464 | "prediction",
465 | "event_state"
466 | ]
467 | }
468 | },
469 | "response": []
470 | },
471 | {
472 | "name": "Get Predicted Observables",
473 | "request": {
474 | "method": "GET",
475 | "header": [],
476 | "url": {
477 | "raw": "http://127.0.0.1:5000/api/v1/session/0/prediction/observables",
478 | "protocol": "http",
479 | "host": [
480 | "127",
481 | "0",
482 | "0",
483 | "1"
484 | ],
485 | "port": "5000",
486 | "path": [
487 | "api",
488 | "v1",
489 | "session",
490 | "0",
491 | "prediction",
492 | "observables"
493 | ]
494 | }
495 | },
496 | "response": []
497 | }
498 | ]
499 | }
--------------------------------------------------------------------------------
/specs/swagger.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | openapi: 3.0.2
3 | info:
4 | title: Prognostics As A Service (PaaS) Sandbox (prog_server)
5 | description: The PaaS Sandbox (a.k.a., prog_server) exposes a REST API that enables prognostics using the NASA PCoE progpy and progpy packages
6 | contact:
7 | name: Chris Teubert
8 | email: christopher.a.teubert@nasa.gov
9 | version: '1.4'
10 | paths:
11 | /v1/session:
12 | put:
13 | description: Create a new session
14 | requestBody:
15 | required: true
16 | content:
17 | application/json:
18 | schema:
19 | $ref: "#/components/schemas/SessionConfiguration"
20 | responses:
21 | "200":
22 | description: Session created
23 | content:
24 | application/json:
25 | schema:
26 | $ref: "#/components/schemas/Session"
27 | "400":
28 | description: Bad input
29 | get:
30 | description: Get a list of the ids of all open sessions
31 | responses:
32 | "200":
33 | description: Okay response
34 | content:
35 | application/json:
36 | schema:
37 | $ref: "#/components/schemas/Sessions"
38 | /v1/session/{id}:
39 | parameters:
40 | - name: id
41 | description: id for the active session (generated when session began)
42 | in: path
43 | required: true
44 | schema:
45 | type: integer
46 | get:
47 | description: Get the details for a specific session
48 | responses:
49 | "200":
50 | description: Okay response
51 | content:
52 | application/json:
53 | schema:
54 | $ref: "#/components/schemas/Session"
55 | delete:
56 | description: End a specific session
57 | responses:
58 | "200":
59 | description: Session deleted
60 | "400":
61 | description: Session doesn't exist
62 | /v1/session/{id}/initialized:
63 | parameters:
64 | - name: id
65 | description: id for the active session (generated when session began)
66 | in: path
67 | required: true
68 | schema:
69 | type: integer
70 | get:
71 | responses:
72 | "200":
73 | description: Okay response
74 | content:
75 | application/json:
76 | schema:
77 | type: object
78 | properties:
79 | initialized:
80 | type: boolean
81 | "400":
82 | description: Session does not exist or has ended
83 | /v1/session/{id}/state:
84 | parameters:
85 | - name: id
86 | description: id for the active session (generated when session began)
87 | in: path
88 | required: true
89 | schema:
90 | type: integer
91 | - name: return_format
92 | description: Format for the returned state
93 | in: query
94 | required: false
95 | schema:
96 | $ref: "#/components/schemas/ReturnFormat"
97 | get:
98 | description: Get the most recent estimate of the system state for the model used in this session.
99 | responses:
100 | "200":
101 | description: Okay response
102 | content:
103 | application/json:
104 | schema:
105 | $ref: "#/components/schemas/PredictionPointWithTime"
106 | "400":
107 | description: Session not active, or model not initialized
108 | /v1/session/{id}/output:
109 | parameters:
110 | - name: id
111 | description: id for the active session (generated when session began)
112 | in: path
113 | required: true
114 | schema:
115 | type: integer
116 | - name: return_format
117 | description: Format for the returned state
118 | in: query
119 | required: false
120 | schema:
121 | $ref: "#/components/schemas/ReturnFormat"
122 | get:
123 | description: Get the most recent estimate of the system output for the model used in this session.
124 | responses:
125 | "200":
126 | description: Okay response
127 | content:
128 | application/json:
129 | schema:
130 | $ref: "#/components/schemas/PredictionPointWithTime"
131 | "400":
132 | description: Session not active, or model not initialized
133 | /v1/session/{id}/event_state:
134 | parameters:
135 | - name: id
136 | in: path
137 | required: true
138 | schema:
139 | type: integer
140 | - name: return_format
141 | description: Format for the returned state
142 | in: query
143 | required: false
144 | schema:
145 | $ref: "#/components/schemas/ReturnFormat"
146 | get:
147 | description: Get the most recent estimate of the event state for the model used in this session.
148 | responses:
149 | "200":
150 | description: Okay response
151 | content:
152 | application/json:
153 | schema:
154 | $ref: "#/components/schemas/PredictionPointWithTime"
155 | "400":
156 | description: Session not active, or model not initialized
157 | /v1/session/{id}/performance_metrics:
158 | parameters:
159 | - name: id
160 | in: path
161 | required: true
162 | schema:
163 | type: integer
164 | - name: return_format
165 | description: Format for the returned state
166 | in: query
167 | required: false
168 | schema:
169 | $ref: "#/components/schemas/ReturnFormat"
170 | get:
171 | description: Get the most recent estimate for the performance metrics for the model used in the active session
172 | responses:
173 | "200":
174 | description: Okay response
175 | content:
176 | application/json:
177 | schema:
178 | $ref: "#/components/schemas/PredictionPointWithTime"
179 | "400":
180 | description: Session not active, or model not initialized
181 | /v1/session/{id}/prediction/state:
182 | parameters:
183 | - name: id
184 | in: path
185 | required: true
186 | schema:
187 | type: integer
188 | - name: return_format
189 | description: Format for the returned state
190 | in: query
191 | required: false
192 | schema:
193 | $ref: "#/components/schemas/ReturnFormat"
194 | get:
195 | description: Get the predicted state at save points (defined by pred_cfg['save_pts'] or pred_cfg['save_freq'])
196 | responses:
197 | "200":
198 | description: Okay response
199 | content:
200 | application/json:
201 | schema:
202 | $ref: "#/components/schemas/PredictionWithTime"
203 | "400":
204 | description: Session not active, or model not initialized
205 | /v1/session/{id}/prediction/output:
206 | parameters:
207 | - name: id
208 | in: path
209 | required: true
210 | schema:
211 | type: integer
212 | - name: return_format
213 | description: Format for the returned state
214 | in: query
215 | required: false
216 | schema:
217 | $ref: "#/components/schemas/ReturnFormat"
218 | get:
219 | description: Get the predicted output at save points (defined by pred_cfg['save_pts'] or pred_cfg['save_freq'])
220 | responses:
221 | "200":
222 | description: Okay response
223 | content:
224 | application/json:
225 | schema:
226 | $ref: "#/components/schemas/PredictionWithTime"
227 | "400":
228 | description: Session not active, or model not initialized
229 | /v1/session/{id}/prediction/event_state:
230 | parameters:
231 | - name: id
232 | in: path
233 | required: true
234 | schema:
235 | type: integer
236 | - name: return_format
237 | description: Format for the returned state
238 | in: query
239 | required: false
240 | schema:
241 | $ref: "#/components/schemas/ReturnFormat"
242 | get:
243 | description: Get the predicted event state at save points (defined by pred_cfg['save_pts'] or pred_cfg['save_freq'])
244 | responses:
245 | "200":
246 | description: Okay response
247 | content:
248 | application/json:
249 | schema:
250 | $ref: "#/components/schemas/PredictionWithTime"
251 | "400":
252 | description: Session not active, or model not initialized
253 | /v1/session/{id}/prediction/performance_metrics:
254 | parameters:
255 | - name: id
256 | in: path
257 | required: true
258 | schema:
259 | type: integer
260 | - name: return_format
261 | description: Format for the returned state
262 | in: query
263 | required: false
264 | schema:
265 | $ref: "#/components/schemas/ReturnFormat"
266 | get:
267 | description: Get the predicted performance metrics at save points (defined by pred_cfg['save_pts'] or pred_cfg['save_freq'])
268 | responses:
269 | "200":
270 | description: Okay response
271 | content:
272 | application/json:
273 | schema:
274 | $ref: "#/components/schemas/PredictionWithTime"
275 | "400":
276 | description: Session not active, or model not initialized
277 | /v1/session/{id}/prediction/events:
278 | parameters:
279 | - name: id
280 | in: path
281 | required: true
282 | schema:
283 | type: integer
284 | - name: return_format
285 | description: Format for the returned state
286 | in: query
287 | required: false
288 | schema:
289 | $ref: "#/components/schemas/ReturnFormat"
290 | get:
291 | responses:
292 | "200":
293 | description: Okay response
294 | content:
295 | application/json:
296 | schema:
297 | $ref: "#/components/schemas/ToEPrediction"
298 | "400":
299 | description: Session not active, or model not initialized
300 | /v1/session/{id}/prediction/status:
301 | parameters:
302 | - name: id
303 | in: path
304 | required: true
305 | schema:
306 | type: integer
307 | get:
308 | responses:
309 | "200":
310 | description: Okay response
311 | content:
312 | application/json:
313 | schema:
314 | $ref: "#/components/schemas/PredictionStatus"
315 | "400":
316 | description: Session not active, or model not initialized
317 | /v1/session/{id}/data:
318 | parameters:
319 | - name: id
320 | in: path
321 | required: true
322 | schema:
323 | type: integer
324 | post:
325 | parameters:
326 | - in: query
327 | name: time
328 | schema:
329 | type: number
330 | - in: query
331 | name: "[parameter name]"
332 | schema:
333 | type: number
334 | responses:
335 | "200":
336 | description: Okay response
337 | "400":
338 | description: Session not active
339 | /v1/session/{id}/model:
340 | parameters:
341 | - name: id
342 | in: path
343 | required: true
344 | schema:
345 | type: integer
346 | get:
347 | parameters:
348 | - name: return_format
349 | description: Format for the returned state
350 | in: query
351 | required: false
352 | schema:
353 | type: string
354 | enum:
355 | - pickle
356 | - json
357 | responses:
358 | "200":
359 | description: Okay response
360 | content:
361 | application/json:
362 | schema:
363 | oneOf:
364 | - type: object
365 | description: bytestream of the pickled [progpy.PrognosticsModel](https://nasa.github.io/progpy/api_ref/progpy/PrognosticModel.html) object
366 | - type: object
367 | description: JSON representing configuration of model
368 | example:
369 | {'config_item_1': 3.2, 'config_item_2': 1.75}
370 | "400":
371 | description: Session not active
372 |
373 | components:
374 | schemas:
375 | ToEPrediction:
376 | type: object
377 | properties:
378 | prediction_time:
379 | type: number
380 | description: Time at which the prediction was generated
381 | time_of_event:
382 | type: object
383 | properties:
384 | schema:
385 | $ref: "#/components/schemas/PredictionPoint"
386 |
387 | PredictionPointWithTime:
388 | type: object
389 | description: The requested property with the time it was calculated
390 | required:
391 | - time
392 | properties:
393 | time:
394 | type: number
395 | description: Time at which state was calculated (in simulation time). Corresponds to the time provided in the last sent data before this state estimate.
396 | event_state:
397 | type: object
398 | properties:
399 | schema:
400 | $ref: "#/components/schemas/PredictionPoint"
401 | performance_metrics:
402 | type: object
403 | properties:
404 | schema:
405 | $ref: "#/components/schemas/PredictionPoint"
406 | state:
407 | type: object
408 | properties:
409 | schema:
410 | $ref: "#/components/schemas/PredictionPoint"
411 | output:
412 | type: object
413 | properties:
414 | schema:
415 | $ref: "#/components/schemas/PredictionPoint"
416 |
417 | PredictionPoint:
418 | oneOf:
419 | - type: object
420 | description: JSON representing requested variable (e.g., state, event state, performance metric)
421 | example:
422 | {'state1': 3.2, 'state2': 1.75}
423 | - type: object
424 | description: JSON representing statistics for the requested variable (e.g., state, event state, performance metric)
425 | additionalProperties:
426 | type: object
427 | properties:
428 | schema:
429 | $ref: "#/components/schemas/Stats"
430 | example:
431 | {
432 | 'state1': {...},
433 | 'state2': {...}
434 | }
435 | - type: object
436 | description: JSON representing mean and covariance of a normal multivariate distribution approximating the requested variable (e.g., state, event state, performance metric)
437 | properties:
438 | mean:
439 | type: object
440 | cov:
441 | type: array
442 | items:
443 | type: array
444 | items:
445 | type: integer
446 | example:
447 | {
448 | "mean": {
449 | "state1": 1.2,
450 | "state2": 2.5
451 | },
452 | "cov": [[0.15, 0.003], [-0.025, 0.27]]
453 | }
454 | - type: object
455 | description: bytestream of the pickled [progpy.uncertain_data.UncertainData](https://nasa.github.io/progpy/api_ref/progpy/UncertainData.html) object
456 | SessionConfiguration:
457 | type: object
458 | required:
459 | - model
460 | properties:
461 | model:
462 | type: string
463 | description: Name of the model (from progpy.models) to use for the session.
464 | model_cfg:
465 | type: object
466 | description: Configuration JSON for the model. Configuration parameters are specific to the model and can be found in the [progpy.models documentation](https://nasa.github.io/progpy/api_ref/progpy/IncludedModels.html).
467 | state_est:
468 | type: string
469 | description: Name of the state estimator (from progpy.state_estimators) to use for the session.
470 | state_est_cfg:
471 | type: object
472 | description: Configuration JSON for the state estimator. Configuration parameters are specific to the state estimator.
473 | pred:
474 | type: string
475 | description: Name of the predictor (from progpy.predictors) to use for the session.
476 | pred_cfg:
477 | type: object
478 | description: Configuration JSON for the predictor. Configuration parameters are specific to the predictor.
479 | load_est:
480 | type: string
481 | description: Name of the load estimator used for future loading estimation. See [prog_server documentation](https://nasa.github.io/progpy/api_ref/prog_server/load_ests.html) for more information.
482 | load_est_cfg:
483 | type: object
484 | description: Configuration JSON for the load estimator used for future loading estimation. See [prog_server documentation](https://nasa.github.io/progpy/api_ref/prog_server/load_ests.html) for more information.
485 | x0:
486 | type: object
487 | description: Initial state as a json where keys match model.states.
488 | example:
489 | {
490 | 'model': 'BatteryCircuit',
491 | 'model_cfg': {'qMax': 7604, 'process_noise': 0.1},
492 | 'state_est': 'ParticleFilter',
493 | 'state_est_cfg': {'num_particles': 100},
494 | 'pred': 'MonteCarlo',
495 | 'pred_cfg': {'n_samples': 100, 'save_freq': 10},
496 | 'load_est': 'Variable',
497 | 'load_est_cfg': {'0': {'i': 4.2}, "450": {'i': 2.7}},
498 | 'x0': {'state1': 1.2}
499 | }
500 | ReturnFormat:
501 | type: string
502 | enum:
503 | - mean
504 | - multivariate_norm
505 | - metrics
506 | - uncertain_data
507 | Session:
508 | type: object
509 | properties:
510 | session_id:
511 | type: integer
512 | format: int32
513 | nullable: false
514 | model:
515 | type: object
516 | $ref: "#/components/schemas/Element"
517 | state_est:
518 | type: object
519 | $ref: "#/components/schemas/Element"
520 | pred:
521 | type: object
522 | $ref: "#/components/schemas/Element"
523 | initialized:
524 | type: boolean
525 | description: If the model has been initialized
526 |
527 | Element:
528 | type: object
529 | properties:
530 | type:
531 | type: string
532 | description: Name of the model (from progpy.models) to use for the session.
533 | cfg:
534 | type: object
535 | description: Configuration JSON for the model. Configuration parameters are specific to the model and can be found in the [progpy.models documentation](https://nasa.github.io/progpy/api_ref/progpy/IncludedModels.html).
536 |
537 | Sessions:
538 | type: array
539 | description: A list of the ids of all open sessions
540 | items:
541 | type: integer
542 | example:
543 | [0, 3, 22, 23, 24, 37]
544 |
545 | Stats:
546 | type: object
547 | properties:
548 | max:
549 | type: number
550 | mean:
551 | type: number
552 | mean absolute deviation:
553 | type: number
554 | median:
555 | type: number
556 | median absolute deviation:
557 | type: number
558 | min:
559 | type: number
560 | number of samples:
561 | type: integer
562 | std:
563 | type: number
564 | percentiles:
565 | type: object
566 | properties:
567 | 0.01:
568 | type: number
569 | nullable: true
570 | 0.1:
571 | type: number
572 | nullable: true
573 | 1:
574 | type: number
575 | nullable: true
576 | 10:
577 | type: number
578 | nullable: true
579 | 25:
580 | type: number
581 | nullable: true
582 | 50:
583 | type: number
584 | nullable: true
585 | 75:
586 | type: number
587 | nullable: true
588 |
589 | PredictionWithTime:
590 | type: object
591 | description: The predicted property with the time at which the prediciton was generated.
592 | required:
593 | - prediction_time
594 | properties:
595 | prediction_time:
596 | type: number
597 | description: The time at which the prediction was generated.
598 | event_states:
599 | type: object
600 | properties:
601 | schema:
602 | $ref: "#/components/schemas/Prediction"
603 | states:
604 | type: object
605 | properties:
606 | schema:
607 | $ref: "#/components/schemas/Prediction"
608 | outputs:
609 | type: object
610 | properties:
611 | schema:
612 | $ref: "#/components/schemas/Prediction"
613 | performance_metrics:
614 | type: object
615 | properties:
616 | schema:
617 | $ref: "#/components/schemas/Prediction"
618 |
619 | Prediction:
620 | type: array
621 | items:
622 | type: object
623 | properties:
624 | time:
625 | type: integer
626 | state:
627 | type: object
628 | properties:
629 | schema:
630 | $ref: "#/components/schemas/PredictionPoint"
631 |
632 | PredictionStatus:
633 | type: object
634 | properties:
635 | exceptions:
636 | type: array
637 | items:
638 | type: string
639 | in progress:
640 | type: integer
641 | format: int32
642 | nullable: false
643 | last prediction:
644 | type: string
--------------------------------------------------------------------------------
/src/prog_client/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2021 United States Government as represented by the Administrator of the
2 | # National Aeronautics and Space Administration. All Rights Reserved.
3 |
4 | from prog_client.session import Session
5 | __version__ = '1.7.0'
6 |
--------------------------------------------------------------------------------
/src/prog_client/session.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2021 United States Government as represented by the Administrator of the
2 | # National Aeronautics and Space Administration. All Rights Reserved.
3 |
4 | import requests, json
5 | import urllib3
6 | import pickle
7 | from progpy.uncertain_data import UncertainData
8 | from progpy.utils import containers
9 |
10 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
11 |
12 |
13 | class Session:
14 | """
15 | Create a new Session in `prog_server`
16 |
17 | Args:
18 | model (str): The model to use for this session (e.g., batt)
19 | host (str, optional): Host address for PaaS Service. Defaults to '127.0.0.1'
20 | port (int, optional): Port for PaaS Service. Defaults to 5000.
21 | model_cfg (dict, optional): Configuration for ProgModel.
22 | x0 (dict, optional): Initial state for ProgModel.
23 | load_est (str, optional): Load estimator to use.
24 | load_est_cfg (dict, optional): Configuration for load estimator.
25 | state_est (str, optional): State Estimator to use (e.g., ParticleFilter). Class name for state estimator in `progpy.state_estimators`
26 | state_est_cfg (dict, optional): Configuration for state estimator.
27 | pred (str, optional): Prediction algorithm to use (e.g., MonteCarlo). Class name for prediction algorithm in `progpy.predictors`
28 | pred_cfg (dict, optional): Configuration for prediction algorithm.
29 |
30 | Use:
31 | session = prog_client.Session(**config)
32 | """
33 |
34 | _base_url = '/api/v1'
35 | def __init__(self, model, host = '127.0.0.1', port=8555, **kwargs):
36 | self.host = 'http://' + host + ':' + str(port) + Session._base_url
37 |
38 | # Process kwargs with json value
39 | for key, value in kwargs.items():
40 | if isinstance(value, dict) or isinstance(value, list):
41 | kwargs[key] = json.dumps(value)
42 |
43 | # Start session
44 | result = requests.put(self.host + '/session', data={'model': model, **kwargs})
45 |
46 | # If error code throw Exception
47 | if result.status_code != 201:
48 | raise Exception(result.text)
49 |
50 | # Load information
51 | self.session_id = json.loads(result.text)['session_id']
52 | self.host += "/session/" + str(self.session_id)
53 |
54 | def __str__(self):
55 | return f'PaaS Session {self.session_id}'
56 |
57 | def is_init(self):
58 | """Check if session has been initialized
59 |
60 | Returns:
61 | bool: If the session has been initialized
62 | """
63 | result = requests.get(self.host + '/initialized')
64 | return json.loads(result.text)['initialized']
65 |
66 | def send_data(self, time, **kwargs):
67 | """Send data to service
68 |
69 | Args:
70 | time (float): Time for data point
71 | ... Other arguments as keywords
72 |
73 | Example:
74 | session.send_data(10.2, t=32.0, v=3.914, i=2)
75 | """
76 | result = requests.post(self.host + '/data', data={'time': time, **kwargs})
77 |
78 | # If error code throw Exception
79 | if result.status_code != 204:
80 | raise Exception(result.text)
81 |
82 | def send_loading(self, type: str, cfg: dict):
83 | """
84 | Set the future loading profile profile.
85 |
86 | Args:
87 | type (str): Type of loading profile
88 | cfg (dict): Configuration of loading profile
89 | """
90 | result = requests.post(self.host + '/loading', data={'type': type, 'cfg': json.dumps(cfg)})
91 |
92 | # If error code throw Exception
93 | if result.status_code != 204:
94 | raise Exception(result.text)
95 |
96 | def set_state(self, x):
97 | """
98 | Set the model state.
99 |
100 | Args:
101 | x (UncertainData, Dict, model.StateContainer): Model state
102 | """
103 | if isinstance(x, UncertainData):
104 | x = pickle.dumps(x)
105 | input_format = 'uncertain_data'
106 | elif isinstance(x, containers.DictLikeMatrixWrapper):
107 | x = pickle.dumps(x)
108 | input_format = 'state_container'
109 | elif isinstance(x, dict):
110 | x = {'x': json.dumps(x)}
111 | input_format = 'dict'
112 | else:
113 | raise Exception('Invalid state type ' + str(type(x)))
114 |
115 | result = requests.post(self.host + '/state', data=x, params={'format': input_format})
116 |
117 | # If error code throw Exception
118 | if result.status_code != 204:
119 | raise Exception(result.text)
120 |
121 | def get_state(self):
122 | """Get the model state
123 |
124 | Returns:
125 | tuple: \\
126 | | float: Time of state estimate
127 | | UncertainData: Model state
128 | """
129 | result = requests.get(self.host + '/state', params={'return_format': 'uncertain_data'}, stream='True')
130 |
131 | # If error code throw Exception
132 | if result.status_code != 200:
133 | raise Exception(result.text)
134 |
135 | result = pickle.load(result.raw)
136 | return (result['time'], result['state'])
137 |
138 | def get_output(self):
139 | """Get the model output
140 |
141 | Returns:
142 | tuple: \\
143 | | float: Time of state estimate
144 | | UncertainData: Model state
145 | """
146 | result = requests.get(self.host + '/output', params={'return_format': 'uncertain_data'}, stream='True')
147 |
148 | # If error code throw Exception
149 | if result.status_code != 200:
150 | raise Exception(result.text)
151 |
152 | result = pickle.load(result.raw)
153 | return (result['time'], result['output'])
154 |
155 | def get_predicted_state(self):
156 | """Get the predicted model state
157 |
158 | Returns:
159 | tuple: \\
160 | | float: Time of prediction
161 | | Prediction: Predicted model state at save points
162 | """
163 | result = requests.get(self.host + '/prediction/state', params={'return_format': 'uncertain_data'}, stream='True')
164 |
165 | # If error code throw Exception
166 | if result.status_code != 200:
167 | raise Exception(result.text)
168 |
169 | result = pickle.load(result.raw)
170 | return (result['prediction_time'], result['states'])
171 |
172 | def get_event_state(self):
173 | """Get the current event state
174 |
175 | Returns:
176 | tuple: \\
177 | | float: Time of state estimate
178 | | UncertainData: Event state
179 | """
180 | result = requests.get(self.host + '/event_state', params={'return_format': 'uncertain_data'}, stream='True')
181 |
182 | # If error code throw Exception
183 | if result.status_code != 200:
184 | raise Exception(result.text)
185 |
186 | result = pickle.load(result.raw)
187 | return (result['time'], result['event_state'])
188 |
189 | def get_predicted_output(self):
190 | """Get the predicted output
191 |
192 | Returns:
193 | tuple: \\
194 | | float: Time of prediction
195 | | Prediction: predicted Event state
196 | """
197 | result = requests.get(self.host + '/prediction/output', params={'return_format': 'uncertain_data'}, stream='True')
198 |
199 | # If error code throw Exception
200 | if result.status_code != 200:
201 | raise Exception(result.text)
202 |
203 | result = pickle.load(result.raw)
204 | return (result['prediction_time'], result['outputs'])
205 |
206 | def get_predicted_event_state(self):
207 | """Get the predicted event state
208 |
209 | Returns:
210 | tuple: \\
211 | | float: Time of prediction
212 | | Prediction: predicted Event state
213 | """
214 | result = requests.get(self.host + '/prediction/event_state', params={'return_format': 'uncertain_data'}, stream='True')
215 |
216 | # If error code throw Exception
217 | if result.status_code != 200:
218 | raise Exception(result.text)
219 |
220 | result = pickle.load(result.raw)
221 | return (result['prediction_time'], result['event_states'])
222 |
223 | def get_predicted_toe(self):
224 | """Get the predicted Time of Event (ToE)
225 |
226 | Returns:
227 | tuple: \\
228 | | float: Time of prediction
229 | | UncertainData: Prediction
230 |
231 | See also: get_prediction_status
232 | """
233 | result = requests.get(self.host + '/prediction/events', params={'return_format': 'uncertain_data'}, stream='True')
234 |
235 | # If error code throw Exception
236 | if result.status_code != 200:
237 | raise Exception(result.text)
238 |
239 | result = pickle.load(result.raw)
240 | return (result['prediction_time'], result['time_of_event'])
241 |
242 | def get_prediction_status(self):
243 | """Get the status of the prediction
244 |
245 | Returns:
246 | dict: Status of prediction
247 | """
248 | result = requests.get(self.host + '/prediction/status')
249 |
250 | # If error code throw Exception
251 | if result.status_code != 200:
252 | raise Exception(result.text)
253 |
254 | return json.loads(result.text)
255 |
256 | def get_performance_metrics(self):
257 | """Get current performance metrics
258 |
259 | Returns:
260 | tuple: \\
261 | | float: Time of state estimate
262 | | UncertainData: Performance Metrics
263 | """
264 | result = requests.get(self.host + '/performance_metrics', params={'return_format': 'uncertain_data'}, stream='True')
265 |
266 | # If error code throw Exception
267 | if result.status_code != 200:
268 | raise Exception(result.text)
269 |
270 | result = pickle.load(result.raw)
271 | return (result['time'], result['performance_metrics'])
272 |
273 | def get_predicted_performance_metrics(self):
274 | """Get predicted performance metrics
275 |
276 | Returns:
277 | tuple: \\
278 | | float: Time of prediction
279 | | Prediction: Predicted performance Metrics
280 | """
281 | result = requests.get(self.host + '/prediction/performance_metrics', params={'return_format': 'uncertain_data'}, stream='True')
282 |
283 | # If error code throw Exception
284 | if result.status_code != 200:
285 | raise Exception(result.text)
286 |
287 | result = pickle.load(result.raw)
288 | return (result['prediction_time'], result['performance_metrics'])
289 |
290 | def get_model(self):
291 | """
292 | Get the configured PrognosticsModel used by the session
293 |
294 | Returns:
295 | PrognosticsModel: configured PrognosticsModel used by the session
296 |
297 | Example:
298 | m = session.get_model()
299 | """
300 | result = requests.get(self.host + '/model', params={'return_format': 'pickle'}, stream='True')
301 |
302 | # If error code throw Exception
303 | if result.status_code != 200:
304 | raise Exception(result.text)
305 |
306 | return pickle.load(result.raw)
307 |
--------------------------------------------------------------------------------
/src/prog_server/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2021 United States Government as represented by the Administrator of the
2 | # National Aeronautics and Space Administration. All Rights Reserved.
3 |
4 | from .models.prog_server import server
5 | import time
6 |
7 | __version__ = '1.7.0'
8 |
9 | def run(**kwargs):
10 | """
11 | Start the server and block until it is stopped.
12 |
13 | Args:
14 | host (str, optional): Server host. Defaults to '127.0.0.1'.
15 | port (int, optional): Server port. Defaults to 8555.
16 | debug (bool, optional): If the server is started in debug mode
17 | """
18 | server.run(**kwargs)
19 |
20 | def start(timeout=10, **kwargs):
21 | """
22 | Start the server (not blocking).
23 |
24 | Args:
25 | timeout (float, optional): Timeout in seconds for starting the service
26 |
27 | Keyword Args:
28 | host (str, optional): Server host. Defaults to '127.0.0.1'.
29 | port (int, optional): Server port. Defaults to 8555.
30 | debug (bool, optional): If the server is started in debug mode
31 | """
32 | server.start(**kwargs)
33 | for i in range(timeout):
34 | if server.is_running():
35 | return
36 | time.sleep(1)
37 | server.stop()
38 | raise Exception("Server startup timeout")
39 |
40 | def stop(timeout=10):
41 | """
42 | Stop the server.
43 |
44 | Args:
45 | timeout (float, optional): Timeout in seconds for starting the service
46 | """
47 | server.stop()
48 | for i in range(timeout):
49 | if not server.is_running():
50 | return
51 | time.sleep(1)
52 | raise Exception("Server startup timeout")
53 |
54 | def is_running():
55 | """
56 | Check if the server is running.
57 | """
58 | return server.is_running()
59 |
--------------------------------------------------------------------------------
/src/prog_server/__main__.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2021 United States Government as represented by the Administrator of the
2 | # National Aeronautics and Space Administration. All Rights Reserved.
3 |
4 | from prog_server.models.prog_server import server
5 |
6 | import sys
7 |
8 | if __name__ == '__main__':
9 | # Run the server when package is run as a script. (e.g., python -m prog_server)
10 | debug = '--debug' in sys.argv or '-d' in sys.argv
11 | server.run(debug = debug)
12 |
--------------------------------------------------------------------------------
/src/prog_server/app.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2021 United States Government as represented by the Administrator of the
2 | # National Aeronautics and Space Administration. All Rights Reserved.
3 |
4 | from prog_server.controllers import *
5 | from flask import Flask
6 |
7 | app = Flask("prog_server")
8 | app.url_map.strict_slashes = False
9 |
10 | PREFIX = '/api/v1'
11 |
12 | app.add_url_rule(PREFIX, methods=['GET'], view_func=api_v1)
13 |
14 | # Session
15 | app.add_url_rule(PREFIX + '/session', methods=['PUT'], view_func=new_session)
16 | app.add_url_rule(PREFIX + '/session', methods=['GET'], view_func=get_sessions)
17 | app.add_url_rule(PREFIX + '/session/', methods=['GET'], view_func=get_session)
18 | app.add_url_rule(PREFIX + '/session/', methods=['DELETE'], view_func=delete_session)
19 |
20 | # Set
21 | app.add_url_rule(PREFIX + '/session//state', methods=['POST'], view_func=set_state)
22 | app.add_url_rule(PREFIX + '/session//loading', methods=['POST'], view_func=set_loading_profile)
23 | app.add_url_rule(PREFIX + '/session//data', methods=['POST'], view_func=send_data)
24 |
25 | # Get
26 | app.add_url_rule(PREFIX + '/session//loading', methods=['GET'], view_func=get_loading_profile)
27 | app.add_url_rule(PREFIX + '/session//initialized', methods=['GET'], view_func=get_initialized)
28 | app.add_url_rule(PREFIX + '/session//prediction/status', methods=['GET'], view_func=get_prediction_status)
29 | app.add_url_rule(PREFIX + '/session//model', methods=['GET'], view_func=get_model)
30 |
31 | # Get current state
32 | app.add_url_rule(PREFIX + '/session//state', methods=['GET'], view_func=get_state)
33 | app.add_url_rule(PREFIX + '/session//output', methods=['GET'], view_func=get_output)
34 | app.add_url_rule(PREFIX + '/session//event_state', methods=['GET'], view_func=get_event_state)
35 | app.add_url_rule(PREFIX + '/session//performance_metrics', methods=['GET'], view_func=get_perf_metrics)
36 |
37 | # Get Prediction
38 | app.add_url_rule(PREFIX + '/session//prediction/state', methods=['GET'], view_func=get_predicted_states)
39 | app.add_url_rule(PREFIX + '/session//prediction/output', methods=['GET'], view_func=get_predicted_output)
40 | app.add_url_rule(PREFIX + '/session//prediction/event_state', methods=['GET'], view_func=get_predicted_event_state)
41 | app.add_url_rule(PREFIX + '/session//prediction/performance_metrics', methods=['GET'], view_func=get_predicted_perf_metrics)
42 | app.add_url_rule(PREFIX + '/session//prediction/events', methods=['GET'], view_func=get_predicted_toe)
43 |
--------------------------------------------------------------------------------
/src/prog_server/controllers.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2021 United States Government as represented by the Administrator of the
2 | # National Aeronautics and Space Administration. All Rights Reserved.
3 | from concurrent.futures._base import TimeoutError
4 | from flask import request, abort, jsonify
5 | from flask import current_app as app
6 | import json
7 | import pickle
8 | from prog_server.models.session import Session
9 | from prog_server.models.load_ests import update_moving_avg
10 | from progpy.sim_result import SimResult, LazySimResult
11 | from progpy.uncertain_data import UnweightedSamples
12 | from progpy.predictors import Prediction, UnweightedSamplesPrediction
13 |
14 | session_count = 0
15 | sessions = {}
16 |
17 | def api_v1():
18 | return jsonify({'message': 'Welcome to the PaaS Sandbox API!'})
19 |
20 | # Session
21 | def new_session():
22 | """
23 | Create a new session.
24 |
25 | Args:
26 | """
27 | global session_count
28 | app.logger.debug("Creating New Session")
29 |
30 | if 'model' not in request.form:
31 | abort(400, 'model must be specified in request body')
32 |
33 | model_name = request.form['model']
34 |
35 | session_id = session_count
36 | session_count += 1
37 |
38 | try:
39 | model_cfg = json.loads(request.form.get('model_cfg', '{}'))
40 | except json.decoder.JSONDecodeError:
41 | abort(400, 'model_cfg must be valid JSON')
42 |
43 | try:
44 | load_est_cfg = json.loads(request.form.get('load_est_cfg', '{}'))
45 | except json.decoder.JSONDecodeError:
46 | abort(400, 'load_est_cfg must be valid JSON')
47 |
48 | try:
49 | pred_cfg = json.loads(request.form.get('pred_cfg', '{}'))
50 | except json.decoder.JSONDecodeError:
51 | abort(400, 'pred_cfg must be valid JSON')
52 |
53 | try:
54 | state_est_cfg = json.loads(request.form.get('state_est_cfg', '{}'))
55 | except json.decoder.JSONDecodeError:
56 | abort(400, 'state_est_cfg must be valid JSON')
57 |
58 | sessions[session_id] = Session(
59 | session_id,
60 | model_name,
61 | model_cfg=model_cfg,
62 | x0=request.form.get('x0', None),
63 | state_est_name=request.form.get('state_est', 'ParticleFilter'),
64 | state_est_cfg=state_est_cfg,
65 | load_est_name=request.form.get('load_est', 'MovingAverage'),
66 | load_est_cfg=load_est_cfg,
67 | pred_name=request.form.get('pred', 'MonteCarlo'),
68 | pred_cfg=pred_cfg
69 | )
70 |
71 | return jsonify(sessions[session_id].to_dict()), 201
72 |
73 | def get_sessions():
74 | """
75 | Get the sessions.
76 |
77 | Returns:
78 | The sessions.
79 | """
80 | app.logger.debug("Getting Active Sessions")
81 | return jsonify({'sessions': list(sessions.keys())})
82 |
83 | def get_session(session_id):
84 | """
85 | Get the session.
86 |
87 | Args:
88 | session_id: The session ID.
89 |
90 | Returns:
91 | The session.
92 | """
93 | if session_id not in sessions:
94 | abort(400, f'Session {session_id} does not exist or has ended')
95 |
96 | app.logger.debug(f"Getting details for Session {session_id}")
97 |
98 | return jsonify(sessions[session_id].to_dict())
99 |
100 | def delete_session(session_id):
101 | """
102 | Delete the session.
103 |
104 | Args:
105 | session_id: The session ID.
106 | """
107 | if session_id not in sessions:
108 | abort(400, f'Session {session_id} does not exist or has ended')
109 |
110 | app.logger.debug(f"Ending Session {session_id}")
111 | del sessions[session_id]
112 | return jsonify({'id': session_id, 'status': 'stopped'})
113 |
114 | # Set
115 | def set_state(session_id):
116 | """
117 | Set the system state for the session's model.
118 |
119 | Args:
120 | session_id: The session ID.
121 | state: The new state.
122 | """
123 | if session_id not in sessions:
124 | abort(400, f'Session {session_id} does not exist or has ended')
125 |
126 | mode = request.args.get('format', 'dict')
127 |
128 | app.logger.debug(f"Setting state for Session {session_id}. Format: {mode}")
129 |
130 | if mode == 'dict':
131 | if 'x' not in request.form:
132 | abort(400, "state ('x') must be specified in request body")
133 | x = sessions[session_id].model.StateContainer(json.loads(request.form.get('x')))
134 | elif mode in ('uncertain_data', 'state_container'):
135 | x = pickle.loads(request.get_data())
136 | else:
137 | abort(400, f'Unsupported format: {mode}')
138 |
139 | sessions[session_id].set_state(x)
140 |
141 | return '', 204
142 |
143 | def set_loading_profile(session_id):
144 | """
145 | Set the loading profile for the session's model.
146 |
147 | Args:
148 | session_id: The session ID.
149 | """
150 | if session_id not in sessions:
151 | abort(400, f'Session {session_id} does not exist or has ended')
152 |
153 | app.logger.debug(f"Setting loading profile for Session {session_id}")
154 |
155 | try:
156 | load_est_cfg = json.loads(request.form.get('cfg', '{}'))
157 | except json.decoder.JSONDecodeError:
158 | abort(400, 'cfg must be valid JSON')
159 |
160 | sessions[session_id].set_load_estimator(
161 | request.values['type'],
162 | load_est_cfg)
163 |
164 | return get_loading_profile(session_id)
165 |
166 | def send_data(session_id):
167 | """
168 | Send data to the session's model.
169 |
170 | Args:
171 | session_id: The session ID.
172 | data: The data to send.
173 | """
174 | if session_id not in sessions:
175 | abort(400, f'Session {session_id} does not exist or has ended')
176 |
177 | app.logger.debug(f"Data received from session {session_id}")
178 | values = request.form
179 | session = sessions[session_id]
180 |
181 | try:
182 | inputs = {key: float(values[key]) for key in session.model.inputs}
183 | outputs = {key: float(values[key]) for key in session.model.outputs}
184 | time = float(values['time'])
185 | except KeyError:
186 | abort(400, f'Data missing for session {session_id}. Expected inputs: {session.model.inputs} and outputs: {session.model.outputs}. Received {list(values.keys())}')
187 |
188 | # Update moving average
189 | update_moving_avg(inputs, session, session.load_est_cfg)
190 |
191 | session.add_data(time, inputs, outputs)
192 |
193 | return '', 204
194 |
195 | # Get
196 | def get_loading_profile(session_id):
197 | """
198 | Get the loading profile for the session's model.
199 |
200 | Args:
201 | session_id: The session ID.
202 |
203 | Returns:
204 | The loading profile of the session.
205 | """
206 | if session_id not in sessions:
207 | abort(400, f'Session {session_id} does not exist or has ended')
208 |
209 | return jsonify({
210 | 'type': sessions[session_id].load_est_name,
211 | 'cfg': sessions[session_id].load_est_cfg})
212 |
213 | def get_initialized(session_id):
214 | """
215 | Get the initialized state for the session's model.
216 |
217 | Args:
218 | session_id: The session ID.
219 |
220 | Returns:
221 | The initialized state of the session.
222 | """
223 | if session_id not in sessions:
224 | abort(400, f'Session {session_id} does not exist or has ended')
225 |
226 | return jsonify({'initialized': sessions[session_id].initialized})
227 |
228 | def get_prediction_status(session_id):
229 | """
230 | Get the prediction status for the session's model.
231 |
232 | Args:
233 | session_id: The session ID.
234 |
235 | Returns:
236 | The prediction status of the session.
237 | """
238 | if session_id not in sessions:
239 | abort(400, f'Session {session_id} does not exist or has ended')
240 | if not sessions[session_id].initialized:
241 | abort(400, 'Model not initialized')
242 |
243 | app.logger.debug(f"Getting prediction status for Session {session_id}")
244 |
245 | status = {
246 | 'exceptions': [],
247 | 'in progress': 0,
248 | 'last prediction': None
249 | }
250 |
251 | with sessions[session_id].locks['futures']:
252 | for future in sessions[session_id].futures:
253 | if future is not None:
254 | try:
255 | except_msg = str(future.exception(timeout=0))
256 | if except_msg != "None":
257 | status['exceptions'].append(except_msg)
258 | except TimeoutError:
259 | # Timeout Error = No error in thread
260 | # (request for exceptions timed out)
261 | pass
262 | status['in progress'] += future.running()
263 | with sessions[session_id].locks['results']:
264 | if sessions[session_id].results is not None:
265 | status['last prediction'] = sessions[session_id].results[0].strftime("%c")
266 | return jsonify(status)
267 |
268 | # Get current
269 | def get_state(session_id):
270 | """
271 | Get the system state for the session's model.
272 |
273 | Args:
274 | session_id: The session ID.
275 |
276 | Returns:
277 | The state of the session.
278 | """
279 | if session_id not in sessions:
280 | abort(400, f'Session {session_id} does not exist or has ended')
281 | if not sessions[session_id].initialized:
282 | abort(400, 'Model not initialized')
283 |
284 | mode = request.args.get('return_format', 'mean')
285 |
286 | app.logger.debug(f"Getting state for Session {session_id}. Return mode: {mode}")
287 | with sessions[session_id].locks['estimate']:
288 | if mode == 'mean':
289 | state = sessions[session_id].state_est.x.mean
290 | elif mode == 'metrics':
291 | state = sessions[session_id].state_est.x.metrics()
292 | elif mode == 'multivariate_norm':
293 | state = {
294 |
295 | 'mean': sessions[session_id].state_est.x.mean,
296 | 'cov': sessions[session_id].state_est.x.cov.tolist(),
297 | }
298 | elif mode == 'uncertain_data':
299 | return pickle.dumps({
300 | "time": sessions[session_id].state_est.t,
301 | "state": sessions[session_id].state_est.x
302 | })
303 | else:
304 | abort(400, f'Invalid return mode: {mode}')
305 | return jsonify({
306 | "time": sessions[session_id].state_est.t,
307 | "state": state})
308 |
309 | def get_output(session_id):
310 | """
311 | Get the system output for the session's model.
312 |
313 | Args:
314 | session_id: The session ID.
315 |
316 | Returns:
317 | The ouput for the session.
318 | """
319 | if session_id not in sessions:
320 | abort(400, f'Session {session_id} does not exist or has ended')
321 | if not sessions[session_id].initialized:
322 | abort(400, 'Model not initialized')
323 |
324 | mode = request.args.get('return_format', 'mean')
325 |
326 | app.logger.debug(f"Getting output for Session {session_id}. Return mode: {mode}")
327 | with sessions[session_id].locks['estimate']:
328 | if mode == 'mean':
329 | x = sessions[session_id].state_est.x.mean
330 | z = sessions[session_id].model.output(x)
331 | elif mode == 'metrics':
332 | x = sessions[session_id].state_est.x.sample(100)
333 | z = UnweightedSamples([sessions[session_id].model.output(x_) for x_ in x])
334 | z = z.metrics()
335 | elif mode == 'multivariate_norm':
336 | x = sessions[session_id].state_est.x.sample(100)
337 | z = UnweightedSamples([sessions[session_id].model.output(x_) for x_ in x])
338 | z = {
339 | 'mean': z.mean,
340 | 'cov': z.cov.tolist()
341 | }
342 | elif mode == 'uncertain_data':
343 | x = sessions[session_id].state_est.x.sample(100)
344 | z = UnweightedSamples([sessions[session_id].model.output(x_) for x_ in x])
345 | return pickle.dumps({
346 | "time": sessions[session_id].state_est.t,
347 | "output": z})
348 | else:
349 | abort(400, f'Invalid return mode: {mode}')
350 |
351 | return jsonify({
352 | "time": sessions[session_id].state_est.t,
353 | "output": z})
354 |
355 | def get_event_state(session_id):
356 | """
357 | Get the event state for the session's model.
358 |
359 | Args:
360 | session_id: The session ID.
361 |
362 | Returns:
363 | The event state of the session.
364 | """
365 | if session_id not in sessions:
366 | abort(400, f'Session {session_id} does not exist or has ended')
367 | if not sessions[session_id].initialized:
368 | abort(400, 'Model not initialized')
369 |
370 | mode = request.args.get('return_format', 'mean')
371 |
372 | app.logger.debug(f"Getting event state for Session {session_id}. Return mode: {mode}")
373 | with sessions[session_id].locks['estimate']:
374 | if mode == 'mean':
375 | x = sessions[session_id].state_est.x.mean
376 | es = sessions[session_id].model.event_state(x)
377 | elif mode == 'metrics':
378 | x = sessions[session_id].state_est.x.sample(100)
379 | es = UnweightedSamples([sessions[session_id].model.event_state(x_) for x_ in x])
380 | es = es.metrics()
381 | elif mode == 'multivariate_norm':
382 | x = sessions[session_id].state_est.x.sample(100)
383 | es = UnweightedSamples([sessions[session_id].model.event_state(x_) for x_ in x])
384 | es = {
385 | 'mean': es.mean,
386 | 'cov': es.cov.tolist()
387 | }
388 | elif mode == 'uncertain_data':
389 | x = sessions[session_id].state_est.x.sample(100)
390 | es = UnweightedSamples([sessions[session_id].model.event_state(x_) for x_ in x])
391 | return pickle.dumps({
392 | "time": sessions[session_id].state_est.t,
393 | "event_state": es})
394 | else:
395 | abort(400, f'Invalid return mode: {mode}')
396 |
397 | return jsonify({
398 | "time": sessions[session_id].state_est.t,
399 | "event_state": es})
400 |
401 | def get_perf_metrics(session_id):
402 | """
403 | Get the performance metrics for the session's model.
404 |
405 | Args:
406 | session_id: The session ID.
407 |
408 | Returns:
409 | The performance metrics of the session.
410 | """
411 | if session_id not in sessions:
412 | abort(400, f'Session {session_id} does not exist or has ended')
413 | if not sessions[session_id].initialized:
414 | abort(400, 'Model not initialized')
415 |
416 | mode = request.args.get('return_format', 'mean')
417 | app.logger.debug(f"Getting Performance Metrics for Session {session_id}")
418 |
419 | with sessions[session_id].locks['estimate']:
420 | if mode == 'mean':
421 | x = sessions[session_id].state_est.x.mean
422 | pm = sessions[session_id].model.observables(x)
423 | elif mode == 'metrics':
424 | x = sessions[session_id].state_est.x.sample(100)
425 | es = UnweightedSamples([sessions[session_id].model.observables(x_) for x_ in x])
426 | pm = es.metrics()
427 | elif mode == 'multivariate_norm':
428 | x = sessions[session_id].state_est.x.sample(100)
429 | es = UnweightedSamples([sessions[session_id].model.observables(x_) for x_ in x])
430 | pm = {
431 | 'mean': es.mean,
432 | 'cov': es.cov.tolist()
433 | }
434 | elif mode == 'uncertain_data':
435 | x = sessions[session_id].state_est.x.sample(100)
436 | pm = UnweightedSamples([sessions[session_id].model.observables(x_) for x_ in x])
437 | return pickle.dumps({
438 | "time": sessions[session_id].state_est.t,
439 | "performance_metrics": pm})
440 | else:
441 | abort(400, f'Invalid return mode: {mode}')
442 |
443 | return jsonify({
444 | "time": sessions[session_id].state_est.t,
445 | "performance_metrics": pm})
446 |
447 | def get_predicted_states(session_id):
448 | """
449 | Get the predicted states for the session's model.
450 |
451 | Args:
452 | session_id: The session ID.
453 |
454 | Returns:
455 | The predicted states of the session.
456 | """
457 | if session_id not in sessions:
458 | abort(400, f'Session {session_id} does not exist or has ended')
459 | if not sessions[session_id].initialized:
460 | abort(400, 'Model not initialized')
461 |
462 | app.logger.debug("Get predicted states for session {}".format(session_id))
463 | mode = request.args.get('return_format', 'mean')
464 | with sessions[session_id].locks['results']:
465 | if sessions[session_id].results is None:
466 | abort(400, 'No Completed Prediction')
467 |
468 | states = sessions[session_id].results[1]['states']
469 |
470 | if mode == 'mean':
471 | states = [{
472 | 'time': states.times[i],
473 | 'state': states.snapshot(i).mean
474 | } for i in range(len(states.times))]
475 | elif mode == 'metrics':
476 | states = [{
477 | 'time': states.times[i],
478 | 'state': states.snapshot(i).metrics()
479 | } for i in range(len(states.times))]
480 | elif mode == 'multivariate_norm':
481 | states = [{
482 | 'time': states.times[i],
483 | 'state': {
484 | 'mean': states.snapshot(i).mean,
485 | 'cov': states.snapshot(i).cov.tolist()
486 | }
487 | } for i in range(len(states.times))]
488 | elif mode == 'uncertain_data':
489 | return pickle.dumps({
490 | "prediction_time": sessions[session_id].results[1]['time'],
491 | "states": states})
492 | else:
493 | abort(400, f'Invalid return mode: {mode}')
494 |
495 | return jsonify({
496 | "prediction_time": sessions[session_id].results[1]['time'],
497 | "states": states})
498 |
499 | def get_predicted_output(session_id):
500 | """
501 | Get the predicted outputs for the session's model.
502 |
503 | Args:
504 | session_id: The session ID.
505 |
506 | Returns:
507 | The predicted outputs of the session.
508 | """
509 | if session_id not in sessions:
510 | abort(400, f'Session {session_id} does not exist or has ended')
511 | if not sessions[session_id].initialized:
512 | abort(400, 'Model not initialized')
513 |
514 | app.logger.debug("Get predicted outputs for session {}".format(session_id))
515 | mode = request.args.get('return_format', 'mean')
516 | with sessions[session_id].locks['results']:
517 | if sessions[session_id].results is None:
518 | abort(400, 'No Completed Prediction')
519 |
520 | zs = sessions[session_id].results[1]['outputs']
521 |
522 | if mode == 'mean':
523 | outputs = [{
524 | 'time': zs.times[i],
525 | 'state': zs.snapshot(i).mean
526 | } for i in range(len(zs.times))]
527 | elif mode == 'metrics':
528 | outputs = [{
529 | 'time': zs.times[i],
530 | 'state': zs.snapshot(i).metrics()
531 | } for i in range(len(zs.times))]
532 | elif mode == 'multivariate_norm':
533 | outputs = [{
534 | 'time': zs.times[i],
535 | 'state': {
536 | 'mean': zs.snapshot(i).mean,
537 | 'cov': zs.snapshot(i).cov.tolist()
538 | }
539 | } for i in range(len(zs.times))]
540 | elif mode == 'uncertain_data':
541 | if isinstance(zs, UnweightedSamplesPrediction) and isinstance(zs[0], LazySimResult):
542 | # LazySimResult is un-pickleable in prog_models v1.2.2, so we need to convert it to a SimResult
543 | zs2 = [SimResult(output.times, output.data) for output in zs]
544 | zs = UnweightedSamplesPrediction(zs.times, zs2)
545 | return pickle.dumps({
546 | 'prediction_time': sessions[session_id].results[1]['time'],
547 | 'outputs': zs})
548 | else:
549 | abort(400, f'Invalid return mode: {mode}')
550 |
551 | return jsonify({
552 | "prediction_time": sessions[session_id].results[1]['time'],
553 | "outputs": outputs})
554 |
555 | def get_predicted_event_state(session_id):
556 | """
557 | Get the predicted event state for the session's model.
558 |
559 | Args:
560 | session_id: The session ID.
561 |
562 | Returns:
563 | The predicted event state of the session.
564 | """
565 | if session_id not in sessions:
566 | abort(400, f'Session {session_id} does not exist or has ended')
567 | if not sessions[session_id].initialized:
568 | abort(400, 'Model not initialized')
569 |
570 | app.logger.debug("Get predicted event states for session {}".format(session_id))
571 | mode = request.args.get('return_format', 'mean')
572 | with sessions[session_id].locks['results']:
573 | if sessions[session_id].results is None:
574 | abort(400, 'No Completed Prediction')
575 |
576 | es = sessions[session_id].results[1]['event_states']
577 |
578 | if mode == 'mean':
579 | event_states = [{
580 | 'time': es.times[i],
581 | 'state': es.snapshot(i).mean
582 | } for i in range(len(es.times))]
583 | elif mode == 'metrics':
584 | event_states = [{
585 | 'time': es.times[i],
586 | 'state': es.snapshot(i).metrics()
587 | } for i in range(len(es.times))]
588 | elif mode == 'multivariate_norm':
589 | event_states = [{
590 | 'time': es.times[i],
591 | 'state': {
592 | 'mean': es.snapshot(i).mean,
593 | 'cov': es.snapshot(i).cov.tolist()
594 | }
595 | } for i in range(len(es.times))]
596 | elif mode == 'uncertain_data':
597 | if isinstance(es, UnweightedSamplesPrediction) and isinstance(es[0], LazySimResult):
598 | # LazySimResult is un-pickleable in prog_models v1.2.2, so we need to convert it to a SimResult
599 | es2 = [SimResult(event_state.times, event_state.data) for event_state in es]
600 | es = UnweightedSamplesPrediction(es.times, es2)
601 | return pickle.dumps({
602 | 'prediction_time': sessions[session_id].results[1]['time'],
603 | 'event_states': es})
604 | else:
605 | abort(400, f'Invalid return mode: {mode}')
606 |
607 | return jsonify({
608 | "prediction_time": sessions[session_id].results[1]['time'],
609 | "event_states": event_states})
610 |
611 | def get_predicted_perf_metrics(session_id):
612 | """
613 | Get the predicted performance metrics for the session's model.
614 |
615 | Args:
616 | session_id: The session ID.
617 |
618 | Returns:
619 | The predicted performance metrics of the session.
620 | """
621 | if session_id not in sessions:
622 | abort(400, f'Session {session_id} does not exist or has ended')
623 | if not sessions[session_id].initialized:
624 | abort(400, 'Model not initialized')
625 |
626 | app.logger.debug("Get predicted performance metrics for session {}".format(session_id))
627 | mode = request.args.get('return_format', 'mean')
628 | with sessions[session_id].locks['results']:
629 | if sessions[session_id].results is None:
630 | abort(400, 'No Completed Prediction')
631 |
632 | states = sessions[session_id].results[1]['states']
633 |
634 | if mode == 'mean':
635 | pm = [{
636 | 'time': states.times[i],
637 | 'state': sessions[session_id].model.observables(states.snapshot(i).mean)
638 | } for i in range(len(states.times))]
639 | elif mode == 'metrics':
640 | pm = list()
641 | for i in range(len(states.times)):
642 | samples = states.snapshot(i).sample(100)
643 | samples = UnweightedSamples([sessions[session_id].model.observables(x_) for x_ in samples])
644 | pm.append({
645 | 'time': states.times[i],
646 | 'state': samples.metrics()
647 | })
648 | elif mode == 'multivariate_norm':
649 | pm = list()
650 | for i in range(len(states.times)):
651 | samples = states.snapshot(i).sample(100)
652 | samples = UnweightedSamples([sessions[session_id].model.observables(x_) for x_ in samples])
653 | pm.append({
654 | 'time': states.times[i],
655 | 'state': {
656 | 'mean': samples.mean,
657 | 'cov': samples.cov.tolist()
658 | }
659 | })
660 | elif mode == 'uncertain_data':
661 | pm = list()
662 | for i in range(len(states.times)):
663 | samples = states.snapshot(i).sample(100)
664 | samples = UnweightedSamples([sessions[session_id].model.observables(x_) for x_ in samples])
665 | pm.append(samples)
666 |
667 | return pickle.dumps({
668 | "prediction_time": sessions[session_id].results[1]['time'],
669 | "performance_metrics": Prediction(states.times, pm)})
670 | else:
671 | abort(400, f'Invalid return mode: {mode}')
672 |
673 | return jsonify({
674 | "prediction_time": sessions[session_id].results[1]['time'],
675 | "performance_metrics": pm})
676 |
677 | def get_predicted_toe(session_id):
678 | """
679 | Get the predicted Time of Event (ToE) for the session's model.
680 |
681 | Args:
682 | session_id: The session ID.
683 |
684 | Returns:
685 | The predicted toe of the session.
686 | """
687 | if session_id not in sessions:
688 | abort(400, f'Session {session_id} does not exist or has ended')
689 | if not sessions[session_id].initialized:
690 | abort(400, 'Model not initialized')
691 |
692 | mode = request.args.get('return_format', 'metrics')
693 | app.logger.debug(f"Get prediction for session {session_id} (mode {mode})")
694 | with sessions[session_id].locks['results']:
695 | if sessions[session_id].results is None:
696 | abort(400, 'No Completed Prediction')
697 |
698 | if mode == 'mean':
699 | toe = sessions[session_id].results[1]['time of event'].mean
700 | elif mode == 'metrics':
701 | toe = sessions[session_id].results[1]['time of event'].metrics()
702 | elif mode == 'multivariate_norm':
703 | toe = {
704 | 'mean': sessions[session_id].results[1]['time of event'].mean,
705 | 'cov': sessions[session_id].results[1]['time of event'].cov.tolist()
706 | }
707 | elif mode == 'uncertain_data':
708 | return pickle.dumps({
709 | "prediction_time": sessions[session_id].results[1]['time'],
710 | 'time_of_event': sessions[session_id].results[1]['time of event']})
711 | else:
712 | abort(400, f'Invalid return mode: {mode}')
713 |
714 | return jsonify({
715 | "prediction_time": sessions[session_id].results[1]['time'],
716 | "time_of_event": toe})
717 |
718 | def get_model(session_id):
719 | if session_id not in sessions:
720 | abort(400, f'Session {session_id} does not exist or has ended')
721 |
722 | mode = request.args.get('return_format', 'json')
723 |
724 | if mode == 'json':
725 | return sessions[session_id].model.to_json()
726 | elif mode == 'pickle':
727 | return pickle.dumps(sessions[session_id].model)
728 | else:
729 | abort(400, f'Invalid return mode: {mode}')
730 |
--------------------------------------------------------------------------------
/src/prog_server/models/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2021 United States Government as represented by the Administrator of the
2 | # National Aeronautics and Space Administration. All Rights Reserved.
3 |
--------------------------------------------------------------------------------
/src/prog_server/models/load_ests.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2021 United States Government as represented by the Administrator of the
2 | # National Aeronautics and Space Administration. All Rights Reserved.
3 |
4 | from flask import abort
5 | from functools import partial
6 | from numpy.random import normal
7 | from statistics import mean
8 |
9 | def Variable(t, x=None, session=None, cfg=None):
10 | """Variable (i.e. piecewise) load estimator. The piecewise load function is defined in the load_est_cfg as ordered dictionary starting_time: load.
11 |
12 | cfg: ordered dictionary starting_time: load. First key should always be 0
13 | e.g., {'0': {'u1': 0.1}, '100': {'u1': 0.2}, '300': {'u1': 0.3}, '500': {'u1': 0.0}}
14 | """
15 | keys = list(cfg.keys())
16 | keys.reverse()
17 | for time in keys:
18 | if t > float(time):
19 | return cfg[time]
20 | return cfg[keys[-1]]
21 |
22 | def Const(t, x=None, session=None, cfg=None):
23 | """Constant load estimator. Load is assumed to be constant over time.
24 |
25 | cfg: dictionary with one key (load) where value is the constant load (dict)
26 | e.g., {'load': {'u1': 0.1}}
27 | """
28 | return cfg['load']
29 |
30 | def MovingAverage(t, x=None, session=None, cfg=None):
31 | """Moving average load estimator. Load is estimated as the mean of the last `window_size` samples. Noise can be added using the following optional configuration parameters:
32 |
33 | * base_std: standard deviation of noise
34 | * std_slope: Increase in std with time (e.g., 0.1 = increase of 0.1 in std per second)
35 | * t0: Starting time for calculation of std
36 |
37 | std of applied noise is defined as base_std + std_slope (t-t0). By default no noise is added
38 | """
39 | std = cfg.get('base_std',0) + cfg.get('std_slope', 0) * (t - cfg.get('t0', 0))
40 | load = {key : mean(session.moving_avg_loads[key]) for key in session.model.inputs}
41 | return {key : normal(load[key], std) for key in load.keys()}
42 |
43 | def update_moving_avg(u, session=None, cfg={}):
44 | for key in session.model.inputs:
45 | session.moving_avg_loads[key].append(u[key])
46 | if len(session.moving_avg_loads[key]) > cfg.get('window_size', 10):
47 | del session.moving_avg_loads[key][0] # Remove first item
48 |
49 | def build_load_est(name, cfg, session):
50 | if name not in globals():
51 | abort(400, f"{name} is not a valid load estimation method")
52 | load_est_fcn = globals()[name]
53 | return partial(load_est_fcn,
54 | cfg=cfg,
55 | session=session)
56 |
--------------------------------------------------------------------------------
/src/prog_server/models/prediction_handler.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2021 United States Government as represented by the Administrator of the
2 | # National Aeronautics and Space Administration. All Rights Reserved.
3 |
4 | from concurrent.futures import ThreadPoolExecutor as PoolExecutor
5 | from copy import deepcopy
6 | from datetime import datetime
7 | from flask import current_app as app
8 |
9 | pool = PoolExecutor(max_workers=5)
10 |
11 | # Prediction Function
12 | def predict(session):
13 | with session.locks['execution']:
14 | with session.locks['estimate']:
15 | x = deepcopy(session.state_est.x)
16 | time = session.state_est.t
17 |
18 | (_, _, states, outputs, event_states, events) = session.pred.predict(x, session.load_est, t0=time)
19 |
20 | with session.locks['results']:
21 | session.results = (
22 | datetime.now(),
23 | {
24 | 'time': time,
25 | 'time of event': events,
26 | 'states': states,
27 | 'outputs': outputs,
28 | 'event_states': event_states
29 | })
30 |
31 | def add_to_predict_queue(session):
32 | with session.locks['futures']:
33 | if (session.futures[1] is None) or session.futures[1].done():
34 | # At least one open slot
35 | app.logger.debug(f"Performing Prediction for Session {session.session_id}")
36 | session.futures[1] = session.futures[0]
37 | session.futures[0] = pool.submit(predict, session)
38 | elif session.futures[0].done():
39 | # Session 1 finished before 0
40 | app.logger.debug(f"Performing Prediction for Session {session.session_id}")
41 | session.futures[0] = pool.submit(predict, session)
42 | else:
43 | app.logger.debug(f"Prediction skipped for Session {session.session_id}")
44 |
--------------------------------------------------------------------------------
/src/prog_server/models/prog_server.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2021 United States Government as represented by the Administrator of the
2 | # National Aeronautics and Space Administration. All Rights Reserved.
3 |
4 | from prog_server.app import app
5 | from prog_server.models import session
6 |
7 | from multiprocessing import Process
8 | import requests
9 |
10 | DEFAULT_PORT = 8555
11 | DEFAULT_HOST = '127.0.0.1'
12 |
13 |
14 | class ProgServer():
15 | """
16 | This class is a wrapper for the flask server.
17 | """
18 |
19 | def __init__(self):
20 | self.process = None
21 |
22 | def run(self, host=DEFAULT_HOST, port=DEFAULT_PORT, debug=False, models={}, predictors={}, state_estimators={}, **kwargs) -> None:
23 | """Run the server (blocking)
24 |
25 | Keyword Args:
26 | host (str, optional): Server host. Defaults to '127.0.0.1'.
27 | port (int, optional): Server port. Defaults to 8555.
28 | debug (bool, optional): If the server is started in debug mode
29 | models (dict[str, PrognosticsModel]): a dictionary of extra models to consider. The key is the name used to identify it.
30 | predictors (dict[str, predictors.Predictor]): a dictionary of extra predictors to consider. The key is the name used to identify it.
31 | state_estimators (dict[str, state_estimators.StateEstimator]): a dictionary of extra estimators to consider. The key is the name used to identify it.
32 | """
33 | if not isinstance(models, dict):
34 | raise TypeError("Extra models (`model` arg in prog_server.run() or start()) must be in a dictionary in the form `name: model_name`")
35 |
36 | session.extra_models.update(models)
37 |
38 | if not isinstance(predictors, dict):
39 | raise TypeError("Custom Predictors (`predictors` arg in prog_server.run() or start()) must be in a dictionary in the form `name: pred_name`")
40 |
41 | session.extra_predictors.update(predictors)
42 |
43 | if not isinstance(state_estimators, dict):
44 | raise TypeError("Custom Estimator (`state_estimators` arg in prog_server.run() or start()) must be in a dictionary in the form `name: state_est_name`")
45 |
46 | session.extra_estimators.update(state_estimators)
47 |
48 | self.host = host
49 | self.port = port
50 | self.process = app.run(host=host, port=port, debug=debug)
51 |
52 | def start(self, host=DEFAULT_HOST, port=DEFAULT_PORT, **kwargs) -> None:
53 | """Start the server in a separate process
54 |
55 | Args:
56 | **kwargs: Arbitrary keyword arguments. See `run` for details.
57 | """
58 | if self.process and self.process.is_alive():
59 | raise RuntimeError('Server already running')
60 | self.host = host
61 | self.port = port
62 | kwargs['host'] = host
63 | kwargs['port'] = port
64 |
65 | self.process = Process(target=self.run, kwargs=kwargs)
66 | self.process.start()
67 |
68 | def stop(self) -> None:
69 | """Stop the server process"""
70 | self.process.terminate()
71 |
72 | def is_running(self):
73 | """Check if the server is running"""
74 | if not self.process:
75 | return False
76 | if (isinstance(self.process, Process) and self.process.is_alive()) or not isinstance(self.process, Process):
77 | url = f"http://{self.host}:{self.port}/api/v1/"
78 | try:
79 | requests.request("GET", url)
80 | return True
81 | except requests.exceptions.ConnectionError:
82 | return False
83 | return False
84 |
85 | server = ProgServer()
86 |
--------------------------------------------------------------------------------
/src/prog_server/models/session.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2021 United States Government as represented by the Administrator of the
2 | # National Aeronautics and Space Administration. All Rights Reserved.
3 |
4 | from prog_server.models.load_ests import build_load_est
5 | from prog_server.models.prediction_handler import add_to_predict_queue
6 |
7 | from copy import deepcopy
8 | from flask import current_app as app
9 | from flask import abort
10 | import json
11 | from progpy import models, state_estimators, predictors, PrognosticsModel
12 | from threading import Lock
13 |
14 | extra_models = {}
15 | extra_predictors = {}
16 | extra_estimators = {}
17 |
18 | class Session():
19 | def __init__(self, session_id,
20 | model_name, model_cfg={}, x0=None,
21 | state_est_name='ParticleFilter', state_est_cfg={},
22 | load_est_name='MovingAverage', load_est_cfg={},
23 | pred_name='MonteCarlo', pred_cfg={}):
24 |
25 | # Save config
26 | self.session_id = session_id
27 | self.model_name = model_name
28 | self.state_est_name = state_est_name
29 | self.state_est_cfg = state_est_cfg
30 | self.pred_name = pred_name
31 | self.initialized = True
32 | self.results = None
33 | self.futures = [None, None]
34 | self.locks = {
35 | 'estimate': Lock(),
36 | 'execution': Lock(),
37 | 'futures': Lock(),
38 | 'results': Lock()
39 | }
40 |
41 | # Model
42 | try:
43 | if model_name in extra_models:
44 | model_class = extra_models[model_name]
45 | else:
46 | model_class = getattr(models, model_name)
47 | except AttributeError:
48 | abort(400, f"Invalid model name {model_name}")
49 |
50 | app.logger.debug(f"Creating Model of type {model_name}")
51 | if isinstance(model_class, type) and issubclass(model_class, PrognosticsModel):
52 | # model_class is a class, either from progpy or custom classes
53 | try:
54 | self.model = model_class(**model_cfg)
55 | except Exception as e:
56 | abort(400, f"Could not instantiate model with input: {e}")
57 | elif isinstance(model_class, PrognosticsModel):
58 | # model_class is an instance of a PrognosticsModel- use the object instead
59 | # This happens for user models that are added to the server at startup.
60 | self.model = deepcopy(model_class)
61 | # Apply any configuration changes, overriding model config.
62 | self.model.parameters.update(model_cfg)
63 | else:
64 | abort(400, f"Invalid model type {type(model_name)} for model {model_name}. For custom classes, the model must be either an instantiated PrognosticsModel subclass or classmame")
65 | self.model_cfg = self.model.parameters
66 | self.moving_avg_loads = {key: [] for key in self.model.inputs}
67 |
68 | # Load Estimator
69 | self.set_load_estimator(load_est_name, load_est_cfg, predict_queue=False)
70 |
71 | # Initial State
72 | if x0 is None:
73 | # If initial state not provided, try initializing model without data
74 | try:
75 | x0 = self.model.initialize()
76 | app.logger.debug("Model initialized without data")
77 | except TypeError:
78 | # Model requires data to initialize. Must be initialized later
79 | app.logger.debug("Model cannot be initialized without data")
80 | self.initialized = False
81 | else:
82 | self.set_state(x0)
83 |
84 | # Predictor
85 | try:
86 | if pred_name in extra_predictors:
87 | pred_class = extra_predictors[pred_name]
88 | else:
89 | pred_class = getattr(predictors, pred_name)
90 | except AttributeError:
91 | abort(400, f"Invalid predictor name {pred_name}")
92 | app.logger.debug(f"Creating Predictor of type {self.pred_name}")
93 | if isinstance(pred_class, type) and issubclass(pred_class, predictors.Predictor):
94 | # pred_class is a class, either from progpy or custom classes
95 | try:
96 | self.pred = pred_class(self.model, **pred_cfg)
97 | except Exception as e:
98 | abort(400, f"Could not instantiate predictor with input: {e}")
99 | elif isinstance(pred_class, predictors.Predictor):
100 | # pred_class is an instance of predictors.Predictor - use the object instead
101 | # This happens for user predictors that are added to the server at startup.
102 | self.pred = deepcopy(pred_class)
103 | # Apply any configuration changes, overriding predictor config.
104 | self.pred.parameters.update(pred_cfg)
105 | else:
106 | abort(400, f"Invalid predictor type {type(pred_name)} for predictor {pred_name}. For custom classes, the predictor must be mentioned with quotes in the pred argument")
107 |
108 | self.pred_cfg = self.pred.parameters
109 |
110 | # State Estimator
111 | if self.initialized:
112 | # If state is initialized, then state estimator and predictor can
113 | # be created without data
114 | self.__initialize(x0)
115 | else:
116 | # Otherwise, will have to be initialized later
117 | # Check state estimator and predictor data
118 | try:
119 | if self.state_est_name not in extra_estimators:
120 | getattr(state_estimators, state_est_name)
121 | except AttributeError:
122 | abort(400, f"Invalid state estimator name {state_est_name}")
123 |
124 | def __initialize(self, x0, predict_queue=True):
125 | app.logger.debug("Initializing...")
126 | #Estimator
127 | try:
128 | if self.state_est_name in extra_estimators:
129 | state_est_class = extra_estimators[self.state_est_name]
130 | else:
131 | state_est_class = getattr(state_estimators, self.state_est_name)
132 | except AttributeError:
133 | abort(400, f"Invalid state estimator name {self.state_est_name}")
134 | app.logger.debug(f"Creating State Estimator of type {self.state_est_name}")
135 |
136 | if isinstance(x0, str):
137 | x0 = json.loads(x0) #loads the initial state
138 | if set(self.model.states) != set(list(x0.keys())):
139 | abort(400, f"Initial state must have every state in model. states. Had {list(x0.keys())}, needed {self.model.states}")
140 |
141 | if isinstance(state_est_class, type) and issubclass(state_est_class, state_estimators.StateEstimator):
142 | try:
143 | self.state_est = state_est_class(self.model, x0, **self.state_est_cfg)
144 | except Exception as e:
145 | abort(400, f"Could not instantiate state estimator with input: {e}")
146 | elif isinstance(state_est_class, state_estimators.StateEstimator):
147 | # state_est_class is an instance of state_estimators.StateEstimator - use the object instead
148 | # This happens for user state estimators that are added to the server at startup.
149 | self.state_est = deepcopy(state_est_class)
150 | # Apply any configuration changes, overriding estimator config
151 | self.state_est.parameters.update(self.state_est_cfg)
152 | else:
153 | abort(400, f"Invalid state estimator type {type(self.state_est_name)} for estimator {self.state_est_name}. For custom classes, the state estimator must be mentioned with quotes in the est argument")
154 |
155 | self.initialized = True
156 | if predict_queue:
157 | add_to_predict_queue(self)
158 |
159 | def set_state(self, x):
160 | app.logger.debug(f"Setting state to {x}")
161 | # Initializes (or re-initializes) state estimator
162 | self.__initialize(x)
163 |
164 | def set_load_estimator(self, name, cfg, predict_queue=True):
165 | app.logger.debug(f"Setting load estimator to {name}")
166 | self.load_est_name = name
167 | self.load_est_cfg = cfg
168 | self.load_est = build_load_est(name, cfg, self)
169 | if predict_queue:
170 | add_to_predict_queue(self)
171 |
172 | def add_data(self, time, inputs, outputs):
173 | # Add data to state estimator
174 | if not self.initialized:
175 | x0 = self.model.initialize(inputs, outputs)
176 | self.__initialize(x0)
177 | else:
178 | app.logger.debug("Adding data to state estimator")
179 | with self.locks['estimate']:
180 | self.state_est.estimate(time, inputs, outputs)
181 | add_to_predict_queue(self)
182 |
183 | def to_dict(self):
184 | return {
185 | 'session_id': self.session_id,
186 | 'model': {
187 | 'type': self.model_name,
188 | 'cfg': self.model_cfg.to_json()},
189 | 'state_estimator': {
190 | 'type': self.state_est_name,
191 | 'cfg': self.state_est_cfg},
192 | 'load_estimator': {
193 | 'type': self.load_est_name,
194 | 'cfg': self.load_est_cfg},
195 | 'predictor': {
196 | 'type': self.pred_name,
197 | 'cfg': self.pred_cfg},
198 | 'initialized': self.initialized
199 | }
200 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2021 United States Government as represented by the Administrator of the National Aeronautics and Space Administration. All Rights Reserved.
2 |
3 | __all__ = ['examples', 'integration']
4 |
--------------------------------------------------------------------------------
/tests/__main__.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2021 United States Government as represented by the Administrator of the National Aeronautics and Space Administration. All Rights Reserved.
2 |
3 | import unittest
4 | import pkgutil
5 | from importlib import import_module
6 |
7 | if __name__ == '__main__':
8 | l = unittest.TestLoader()
9 |
10 | was_successful = True
11 |
12 | for _, name, _ in pkgutil.iter_modules(['tests']):
13 | if name[0] == '_':
14 | continue
15 | print('importing tests.' + name)
16 | test_module = import_module('tests.' + name)
17 | try:
18 | test_module.run_tests()
19 | except Exception:
20 | print('Failed test: ' + name)
21 | was_successful = False
22 |
23 | if not was_successful:
24 | raise Exception("Failed test")
25 | print('All tests succeeded')
26 |
--------------------------------------------------------------------------------
/tests/examples.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2021 United States Government as represented by the Administrator of the National Aeronautics and Space Administration. All Rights Reserved.
2 |
3 | import unittest
4 | import time
5 | import prog_server
6 | import sys
7 | from io import StringIO
8 | import pkgutil
9 | from importlib import import_module
10 |
11 | TIMEOUT = 10 # Server startup timeout in seconds
12 |
13 | def make_test_function(example):
14 | def test(self):
15 | ex = import_module("examples." + example)
16 | ex.run_example()
17 | return test
18 |
19 |
20 | class TestExamples(unittest.TestCase):
21 | @classmethod
22 | def setUpClass(cls):
23 | prog_server.start()
24 | for i in range(TIMEOUT):
25 | if prog_server.is_running():
26 | return
27 | time.sleep(1)
28 | prog_server.stop()
29 | raise Exception("Server startup timeout")
30 |
31 | def setUp(self):
32 | # set stdout (so it wont print)
33 | self._stdout = sys.stdout
34 | sys.stdout = StringIO()
35 |
36 | # def test_online_prog(self):
37 | # from examples import online_prog
38 |
39 | # online_prog.run_example()
40 |
41 | def tearDown(self):
42 | # reset stdout
43 | sys.stdout = self._stdout
44 |
45 | @classmethod
46 | def tearDownClass(cls):
47 | prog_server.stop()
48 |
49 | # Wait for shutdown to complete
50 | for i in range(TIMEOUT):
51 | if not prog_server.is_running():
52 | return
53 | time.sleep(1)
54 |
55 |
56 | # This allows the module to be executed directly
57 | def run_tests():
58 | # Create tests for each example
59 | for _, name, _ in pkgutil.iter_modules(['examples']):
60 | test_func = make_test_function(name)
61 | setattr(TestExamples, 'test_{0}'.format(name), test_func)
62 |
63 | # Run tests
64 | l = unittest.TestLoader()
65 | runner = unittest.TextTestRunner()
66 | print("\n\nTesting examples (this may take some time)")
67 | result = runner.run(l.loadTestsFromTestCase(TestExamples)).wasSuccessful()
68 |
69 | if not result:
70 | raise Exception("Failed test")
71 |
72 | if __name__ == '__main__':
73 | run_tests()
74 |
--------------------------------------------------------------------------------
/tests/integration.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2021 United States Government as represented by the Administrator of the National Aeronautics and Space Administration. All Rights Reserved.
2 |
3 | import time
4 | import unittest
5 | import prog_client, prog_server
6 | from progpy.predictors import MonteCarlo
7 | from progpy.uncertain_data import MultivariateNormalDist
8 | from progpy.models import ThrownObject
9 | from progpy.models.thrown_object import LinearThrownObject
10 | from progpy.state_estimators import KalmanFilter
11 | from progpy.state_estimators import ParticleFilter
12 | from progpy.uncertain_data import MultivariateNormalDist
13 | from progpy.uncertain_data import UnweightedSamples
14 |
15 |
16 | class IntegrationTest(unittest.TestCase):
17 | @classmethod
18 | def setUpClass(cls):
19 | prog_server.start()
20 |
21 | def test_integration(self):
22 | noise = {'x': 0.1, 'v': 0.1}
23 | if 'max_x' in ThrownObject.states:
24 | # max_x was removed in the dev branch
25 | noise['max_x'] = 0
26 | session = prog_client.Session('ThrownObject', state_est_cfg={'x0_uncertainty': 0}, model_cfg={'process_noise': noise}, pred_cfg={'save_freq': 0.1})
27 | m = ThrownObject() # For comparison
28 |
29 | # State
30 | (_, x) = session.get_state()
31 | x0 = m.initialize()
32 | for key, value in x.mean.items():
33 | self.assertAlmostEqual(value, x0[key])
34 |
35 | x2 = {'x': 1, 'v': 20}
36 | session.set_state(x2)
37 | (_, x) = session.get_state()
38 | for key, value in x.mean.items():
39 | self.assertAlmostEqual(value, x2[key])
40 |
41 | mean = {'x': 2, 'v': 40}
42 | cov = [[0.1, 0], [0, 0.1]]
43 | x3 = MultivariateNormalDist(mean.keys(), list(mean.values()), cov)
44 | session.set_state(x3)
45 | (_, x) = session.get_state()
46 | for key, value in x.mean.items():
47 | self.assertAlmostEqual(value, mean[key], delta=0.5)
48 | for i in range(len(x0)):
49 | for j in range(len(x0)):
50 | self.assertAlmostEqual(x.cov[i][j], cov[i][j], delta=0.1)
51 |
52 | # Reset State - state container
53 | session.set_state(x0)
54 | (_, x) = session.get_state()
55 | for key, value in x.mean.items():
56 | self.assertAlmostEqual(value, x0[key])
57 |
58 | # Event State
59 | (_, es) = session.get_event_state()
60 | es0 = m.event_state(x0)
61 | for key, value in es.mean.items():
62 | self.assertAlmostEqual(value, es0[key])
63 |
64 | # Output
65 | (_, z) = session.get_output()
66 | z0 = m.output(x0)
67 | for key, value in z.mean.items():
68 | self.assertAlmostEqual(value, z0[key])
69 |
70 | # Performance Metrics
71 | (_, pm) = session.get_performance_metrics()
72 | self.assertDictEqual(pm.mean, {})
73 |
74 | # Prediction Status
75 | status = session.get_prediction_status()
76 | self.assertIn('exceptions', status)
77 | self.assertIn('in progress', status)
78 | self.assertIn('last prediction', status)
79 | self.assertListEqual(status['exceptions'], [])
80 | self.assertIsInstance(status['in progress'], int)
81 |
82 | for i in range(5):
83 | # Wait for prediction to complete
84 | time.sleep(0.5)
85 | status = session.get_prediction_status()
86 | if status['last prediction'] is not None:
87 | break
88 | self.assertIsNotNone(status['last prediction'], "Timeout waiting for prediction")
89 |
90 | # Prediction - ToE
91 | (t_p, ToE) = session.get_predicted_toe()
92 | self.assertAlmostEqual(t_p, -1e-99)
93 | self.assertAlmostEqual(ToE.mean['falling'], 3.8, delta=0.1)
94 | self.assertAlmostEqual(ToE.mean['impact'], 7.9, delta=0.1)
95 |
96 | # Prep Prediction
97 | (times, _, sim_states, sim_z, sim_es) = m.simulate_to_threshold(lambda t,x=None: {}, threshold_keys='impact', save_freq=0.1, dt=0.1)
98 |
99 | # Prediction - future states
100 | (t_p, states) = session.get_predicted_state()
101 | self.assertAlmostEqual(t_p, -1e-99)
102 |
103 | for i in range(len(states.times)):
104 | self.assertAlmostEqual(states.times[i], i/10, delta=0.2)
105 | for key, value in states.snapshot(i).mean.items():
106 | if i < len(sim_states): # may have one or two more states
107 | self.assertAlmostEqual(value, sim_states[i][key], delta = (i+1)/15, msg=f"snapshot at {i/10}s for key {key} should be {sim_states[i][key]} was {value}")
108 |
109 | # Prediction - future outputs
110 | (t_p, outputs) = session.get_predicted_output()
111 | self.assertAlmostEqual(t_p, -1e-99)
112 |
113 | for i in range(len(outputs.times)):
114 | self.assertAlmostEqual(outputs.times[i], i/10, delta=0.2)
115 | for key, value in outputs.snapshot(i).mean.items():
116 | if i < len(sim_z): # may have one or two more states
117 | self.assertAlmostEqual(value, sim_z[i][key], delta=(i+1)/20, msg=f"snapshot at {i/10}s for key {key} should be {sim_z[i][key]} was {value}")
118 |
119 | # Prediction - future event_states
120 | (t_p, states) = session.get_predicted_event_state()
121 | self.assertAlmostEqual(t_p, -1e-99)
122 |
123 | for i in range(len(states.times)):
124 | self.assertAlmostEqual(states.times[i], i/10, delta=0.2)
125 | for key, value in states.snapshot(i).mean.items():
126 | if i < len(sim_es): # may have one or two more states
127 | self.assertAlmostEqual(value, sim_es[i][key], delta=(i+1)/20, msg=f"snapshot at {i/10}s for key {key} should be {sim_es[i][key]} was {value}")
128 |
129 | # Prediction - future performance metrics
130 | (t_p, states) = session.get_predicted_performance_metrics()
131 | self.assertAlmostEqual(t_p, -1e-99)
132 | for i in range(len(states.times)):
133 | self.assertDictEqual(states.snapshot(i).mean, {})
134 |
135 | # State after sending data
136 | for i in range(1,11):
137 | x0 = m.next_state(x0, {}, 0.1)
138 | session.send_data(i/10.0, **m.output(x0))
139 | time.sleep(0.1)
140 |
141 | t, x_est = session.get_state()
142 | self.assertAlmostEqual(t, 1)
143 |
144 | self.assertAlmostEqual(x_est.mean['x'], x0['x'], delta=1)
145 | self.assertAlmostEqual(x_est.mean['v'], x0['v'], delta=0.75)
146 | if 'max_x' in ThrownObject.states:
147 | # max_x was removed in recent version
148 | self.assertAlmostEqual(x_est.mean['x'], x_est.mean['max_x'], delta=0.2)
149 |
150 | # TODO UPDATED PREDICTION TIME
151 |
152 | def test_dt(self):
153 | # Set dt to 1 and save_freq to 0.1
154 | session_point1 = prog_client.Session(
155 | 'ThrownObject',
156 | pred_cfg={'save_freq': 0.1, 'dt':0.1})
157 | session_1 = prog_client.Session(
158 | 'ThrownObject',
159 | pred_cfg={'save_freq': 0.1, 'dt':1})
160 |
161 | for _ in range(5):
162 | # Wait for prediction to complete
163 | time.sleep(0.5)
164 | status = session_1.get_prediction_status()
165 | if status['last prediction'] is not None:
166 | break
167 |
168 | for _ in range(5):
169 | # Wait for prediction to complete
170 | time.sleep(0.5)
171 | status = session_point1.get_prediction_status()
172 | if status['last prediction'] is not None:
173 | break
174 |
175 | result_point1 = session_point1.get_predicted_event_state()
176 | result_1 = session_1.get_predicted_event_state()
177 | self.assertEqual(len(result_1[1][1].times), 9)
178 | self.assertEqual(len(result_point1[1][1].times), 79)
179 |
180 | # result_1 is less, despite having the same save_freq,
181 | # because the time step is 1,
182 | # so it can't save as frequently
183 |
184 | def test_error_in_init(self):
185 | # Invalid model name
186 | with self.assertRaises(Exception):
187 | session = prog_client.Session("fake model")
188 |
189 | # Model init - process noise has non-existent state
190 | # with self.assertRaises(Exception):
191 | # session = prog_client.Session('ThrownObject', model_cfg={'process_noise': {'x': 0.1, 'v': 0.1, 'fake_state': 0}})
192 |
193 | # invalid predictor
194 | with self.assertRaises(Exception):
195 | session = prog_client.Session(pred='fake_pred')
196 |
197 | # invalid state est
198 | with self.assertRaises(Exception):
199 | session = prog_client.Session('ThrownObject', state_est='fake_est')
200 |
201 | # invalid predictor
202 | with self.assertRaises(Exception):
203 | session = prog_client.Session('ThrownObject', pred='fake_pred')
204 |
205 | # invalid load est
206 | with self.assertRaises(Exception):
207 | session = prog_client.Session('ThrownObject', load_est='fake_est')
208 |
209 | # Missing initial state
210 | with self.assertRaises(Exception):
211 | session = prog_client.Session('ThrownObject', x0={'x': 1.2})
212 |
213 | # Extra state
214 | x0 = {'x': 1.2, 'v': 2.3, 'max_y': 4.5}
215 | if 'max_x' in ThrownObject.states:
216 | # max_x was removed in recent version
217 | x0['max_x'] = 1.2
218 | with self.assertRaises(Exception):
219 | session = prog_client.Session('ThrownObject', x0=x0)
220 |
221 | def test_custom_models(self):
222 | # Restart server with model
223 | prog_server.stop()
224 | ball = ThrownObject(thrower_height=1.5, throwing_speed=20)
225 | prog_server.start(models={'ball': ball}, port=9883)
226 | ball_session = prog_client.Session('ball', port=9883)
227 |
228 | # Check initial state - should equal model state
229 | t, x = ball_session.get_state()
230 | gt_mean = ball.initialize()
231 | x_mean = x.mean
232 | self.assertEqual(x_mean['x'], gt_mean['x'])
233 | self.assertEqual(x_mean['v'], gt_mean['v'])
234 |
235 | # Iterate forward 1 second and compare
236 | gt_mean = ball.next_state(gt_mean, None, 1)
237 | ball_session.send_data(time=1, x=gt_mean['x'])
238 | t, x = ball_session.get_state()
239 | x_mean = x.mean
240 | self.assertEqual(x_mean['x'], gt_mean['x'])
241 | self.assertEqual(x_mean['v'], gt_mean['v'])
242 |
243 | # Restart (to reset port)
244 | prog_server.stop()
245 | prog_server.start()
246 |
247 |
248 | # Later (not yet supported) repeat test model class
249 | # class TestModel(PrognosticsModel):
250 | # inputs = ['u']
251 | # states = ['x']
252 | # outputs = ['x+1']
253 |
254 | # default_parameters = {
255 | # 'x0': {
256 | # 'x': 0
257 | # }
258 | # }
259 |
260 | # def next_state(self, x, u, dt):
261 | # x['x'] = u['u']
262 | # return x
263 |
264 | # def output(self, x):
265 | # return self.OutputContainer({'x+1': x['x']+1})
266 |
267 | # prog_server.start(models={'test': TestModel})
268 | # test_session = prog_client.Session('TestModel')
269 |
270 | # # Check initial state - should equal model state
271 | # z = test_session.get_output()
272 | # self.assertEqual(z['x+1'], 1)
273 |
274 | # # Iterate forward 1 second and compare
275 | # test_session.send_data(time=1, u=5)
276 | # z = test_session.get_output()
277 | # self.assertEqual(z['x+1'], 6)
278 |
279 | @classmethod
280 | def tearDownClass(cls):
281 | prog_server.stop()
282 |
283 | def test_custom_predictors(self):
284 | # Restart server with model
285 | prog_server.stop()
286 | #define the model
287 | ball = ThrownObject(thrower_height=1.5, throwing_speed=20)
288 | #call the external/extra predictor (here from progpy)
289 | mc = MonteCarlo(ball)
290 | with self.assertRaises(Exception):
291 | prog_server.start(port=9883, predictors=[1, 2])
292 |
293 | prog_server.start(port=9883, predictors={'mc':mc})
294 | ball_session = prog_client.Session('ThrownObject', port=9883, pred='mc')
295 |
296 | #check that the prediction completes successfully without error
297 | #get_prediction_status from the session - for errors
298 | sessions_in_progress = True
299 | STEP = 15 # Time to wait between pinging server (s)
300 | while sessions_in_progress:
301 | sessions_in_progress = False
302 | status = ball_session.get_prediction_status()
303 | if status['in progress'] != 0:
304 | print(f'\tSession {ball_session.session_id} is still in progress')
305 | sessions_in_progress = True
306 | time.sleep(STEP)
307 | print(f'\tSession {ball_session.session_id} complete')
308 | print(status)
309 | # Restart (to reset port)
310 | prog_server.stop()
311 | prog_server.start()
312 |
313 | def test_custom_estimators(self):
314 | # Restart server with model
315 | prog_server.stop()
316 |
317 | #define the custom model
318 | ball = LinearThrownObject(thrower_height=1.5, throwing_speed=20)
319 | x_guess = ball.StateContainer({'x': 1.75, 'v': 35})
320 | kf = KalmanFilter(ball, x_guess)
321 | with self.assertRaises(Exception):
322 | # state_estimators not a dictionary
323 | prog_server.start(models ={'ball':ball}, port=9883, state_estimators=[20])
324 | prog_server.start(models ={'ball':ball}, port=9883, state_estimators={'kf':kf})
325 | ball_session = prog_client.Session('ball', port=9883, state_est='kf')
326 |
327 | # time step (s)
328 | dt = 0.01
329 | x = ball.initialize()
330 | # Initial input
331 | u = ball.InputContainer({})
332 |
333 | # Iterate forward 1 second and compare
334 | x = ball.next_state(x, u, 1)
335 | ball_session.send_data(time=1, x=x['x'])
336 |
337 | t, x_s = ball_session.get_state()
338 |
339 | # To check if the output state is multivariate normal distribution
340 | self.assertIsInstance(x_s, MultivariateNormalDist)
341 |
342 | # Setup Particle Filter
343 | pf = ParticleFilter(ball, x_guess)
344 | prog_server.stop()
345 | prog_server.start(models ={'ball':ball}, port=9883, state_estimators={'pf': pf, 'kf':kf})
346 | ball_session = prog_client.Session('ball', port=9883, state_est='pf')
347 |
348 | # Iterate forward 1 second and compare
349 | x = ball.next_state(x, u, 1)
350 | ball_session.send_data(time=1, x=x['x'])
351 |
352 | t, x_s = ball_session.get_state()
353 |
354 | # Ensure that PF output is unweighted samples
355 | self.assertIsInstance(x_s, UnweightedSamples)
356 |
357 | prog_server.stop()
358 | prog_server.start()
359 |
360 |
361 | # This allows the module to be executed directly
362 | def run_tests():
363 | l = unittest.TestLoader()
364 | runner = unittest.TextTestRunner()
365 | print("\n\nTesting prog_client with prog_server")
366 | result = runner.run(l.loadTestsFromTestCase(IntegrationTest)).wasSuccessful()
367 |
368 | if not result:
369 | raise Exception("Failed test")
370 |
371 | if __name__ == '__main__':
372 | run_tests()
373 |
--------------------------------------------------------------------------------