PyAres 0.2.1__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyares-0.3.0/.github/dependabot.yml +15 -0
- pyares-0.3.0/.github/workflows/pypi.yaml +59 -0
- pyares-0.3.0/.github/workflows/python-tests.yaml +40 -0
- pyares-0.3.0/.gitignore +167 -0
- pyares-0.3.0/PKG-INFO +168 -0
- {pyares-0.2.1 → pyares-0.3.0}/PyAres/Analyzing/analysis_service.py +13 -30
- {pyares-0.2.1 → pyares-0.3.0}/PyAres/Device/device_service.py +9 -8
- {pyares-0.2.1 → pyares-0.3.0}/PyAres/Models/__init__.py +3 -2
- {pyares-0.2.1 → pyares-0.3.0}/PyAres/Models/ares_data_models.py +15 -3
- {pyares-0.2.1 → pyares-0.3.0}/PyAres/Planning/planner_models.py +1 -1
- {pyares-0.2.1 → pyares-0.3.0}/PyAres/Planning/planning_service.py +3 -3
- pyares-0.3.0/PyAres/Utils/ares_data_schema_utils.py +49 -0
- {pyares-0.2.1 → pyares-0.3.0}/PyAres/Utils/ares_data_type_utils.py +4 -6
- {pyares-0.2.1 → pyares-0.3.0}/PyAres/Utils/ares_outcome_utils.py +3 -1
- {pyares-0.2.1 → pyares-0.3.0}/PyAres/Utils/ares_struct_utils.py +27 -28
- {pyares-0.2.1 → pyares-0.3.0}/PyAres/Utils/ares_value_utils.py +46 -18
- pyares-0.3.0/pyproject.toml +32 -0
- pyares-0.3.0/setup.cfg +0 -0
- pyares-0.3.0/tests/__init__.py +0 -0
- pyares-0.3.0/tests/mock_grpc_context.py +15 -0
- pyares-0.3.0/tests/test_analyzer.py +179 -0
- pyares-0.3.0/tests/test_ares_data_schema_utils.py +77 -0
- pyares-0.3.0/tests/test_ares_data_type_utils.py +18 -0
- pyares-0.3.0/tests/test_ares_device.py +170 -0
- pyares-0.3.0/tests/test_ares_struct_utils.py +81 -0
- pyares-0.3.0/tests/test_ares_value_type_conversions.py +75 -0
- pyares-0.3.0/tests/test_planner.py +139 -0
- pyares-0.2.1/PKG-INFO +0 -13
- pyares-0.2.1/PyAres/Utils/ares_data_schema_utils.py +0 -36
- pyares-0.2.1/PyAres.egg-info/PKG-INFO +0 -13
- pyares-0.2.1/PyAres.egg-info/SOURCES.txt +0 -45
- pyares-0.2.1/PyAres.egg-info/dependency_links.txt +0 -1
- pyares-0.2.1/PyAres.egg-info/requires.txt +0 -3
- pyares-0.2.1/PyAres.egg-info/top_level.txt +0 -1
- pyares-0.2.1/pyproject.toml +0 -27
- pyares-0.2.1/setup.cfg +0 -4
- {pyares-0.2.1 → pyares-0.3.0}/LICENSE +0 -0
- {pyares-0.2.1 → pyares-0.3.0}/PyAres/Analyzing/__init__.py +0 -0
- {pyares-0.2.1 → pyares-0.3.0}/PyAres/Analyzing/analyzer_models.py +0 -0
- {pyares-0.2.1 → pyares-0.3.0}/PyAres/Demo/analyzer_test.py +0 -0
- {pyares-0.2.1 → pyares-0.3.0}/PyAres/Demo/analyzer_test_tools_demo.py +0 -0
- {pyares-0.2.1 → pyares-0.3.0}/PyAres/Demo/analyzer_wiki.py +0 -0
- {pyares-0.2.1 → pyares-0.3.0}/PyAres/Demo/device_test.py +0 -0
- {pyares-0.2.1 → pyares-0.3.0}/PyAres/Demo/hotplate.py +0 -0
- {pyares-0.2.1 → pyares-0.3.0}/PyAres/Demo/planner_test.py +0 -0
- {pyares-0.2.1 → pyares-0.3.0}/PyAres/Demo/planner_test_tools_demo.py +0 -0
- {pyares-0.2.1 → pyares-0.3.0}/PyAres/Demo/planner_wiki.py +0 -0
- {pyares-0.2.1 → pyares-0.3.0}/PyAres/Demo/rotary_mixer.py +0 -0
- {pyares-0.2.1 → pyares-0.3.0}/PyAres/Device/__init__.py +0 -0
- {pyares-0.2.1 → pyares-0.3.0}/PyAres/Device/device_models.py +0 -0
- {pyares-0.2.1 → pyares-0.3.0}/PyAres/Device/device_warnings.py +0 -0
- {pyares-0.2.1 → pyares-0.3.0}/PyAres/Planning/__init__.py +0 -0
- {pyares-0.2.1 → pyares-0.3.0}/PyAres/Utils/ares_device_command_utils.py +0 -0
- {pyares-0.2.1 → pyares-0.3.0}/PyAres/Utils/param_history_info_utils.py +0 -0
- {pyares-0.2.1 → pyares-0.3.0}/PyAres/Utils/plan_request_utils.py +0 -0
- {pyares-0.2.1 → pyares-0.3.0}/PyAres/Utils/plan_response_utils.py +0 -0
- {pyares-0.2.1 → pyares-0.3.0}/PyAres/Utils/planning_param_utils.py +0 -0
- {pyares-0.2.1 → pyares-0.3.0}/PyAres/__init__.py +0 -0
- {pyares-0.2.1 → pyares-0.3.0}/PyAres/test_tools/__init__.py +0 -0
- {pyares-0.2.1 → pyares-0.3.0}/PyAres/test_tools/analyzer_tester.py +0 -0
- {pyares-0.2.1 → pyares-0.3.0}/PyAres/test_tools/planner_tester.py +0 -0
- {pyares-0.2.1 → pyares-0.3.0}/README.md +0 -0
- {pyares-0.2.1 → pyares-0.3.0}/setup.py +0 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
- package-ecosystem: "pip"
|
|
4
|
+
directory: "/"
|
|
5
|
+
schedule:
|
|
6
|
+
interval: "daily"
|
|
7
|
+
ignore:
|
|
8
|
+
- dependency-name: "pyares"
|
|
9
|
+
- package-ecosystem: "github-actions"
|
|
10
|
+
directory: /
|
|
11
|
+
schedule:
|
|
12
|
+
interval: "weekly"
|
|
13
|
+
groups:
|
|
14
|
+
github-actions:
|
|
15
|
+
patterns: ["*"]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
name: Upload PyARES Python Package
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
release-build:
|
|
12
|
+
name: Build and Publish Python Distributions to PyPi
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
defaults:
|
|
15
|
+
run:
|
|
16
|
+
working-directory: .
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v6
|
|
20
|
+
- name: Setup Python
|
|
21
|
+
uses: actions/setup-python@v6
|
|
22
|
+
with:
|
|
23
|
+
python-version: "3.10"
|
|
24
|
+
- name: Set PyARES Package Name From Tag
|
|
25
|
+
run: echo "PYARES_PACKAGE_VERSION=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_ENV
|
|
26
|
+
- name: Build Package
|
|
27
|
+
run: |
|
|
28
|
+
python -m pip install build
|
|
29
|
+
python -m build
|
|
30
|
+
- name: Upload Distribution
|
|
31
|
+
uses: actions/upload-artifact@v6
|
|
32
|
+
with:
|
|
33
|
+
name: pyares-release-dists
|
|
34
|
+
path: dist/
|
|
35
|
+
if-no-files-found: error
|
|
36
|
+
|
|
37
|
+
pypi-publish:
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
needs:
|
|
40
|
+
- release-build
|
|
41
|
+
permissions:
|
|
42
|
+
id-token: write
|
|
43
|
+
environment:
|
|
44
|
+
name: pypi
|
|
45
|
+
url: https://pypi.org/p/pyares
|
|
46
|
+
|
|
47
|
+
steps:
|
|
48
|
+
- name: Retrieve Release Distributions
|
|
49
|
+
uses: actions/download-artifact@v7
|
|
50
|
+
with:
|
|
51
|
+
name: pyares-release-dists
|
|
52
|
+
path: dist/
|
|
53
|
+
- name: Publish Package
|
|
54
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
55
|
+
with:
|
|
56
|
+
packages-dir: dist/
|
|
57
|
+
user: __token__
|
|
58
|
+
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
59
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: PyAres Unit Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- Develop
|
|
7
|
+
pull_request:
|
|
8
|
+
branches:
|
|
9
|
+
- Develop
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build-and-test:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
|
|
15
|
+
# Optional: Test across multiple Python versions to ensure compatibility
|
|
16
|
+
strategy:
|
|
17
|
+
matrix:
|
|
18
|
+
python-version: ["3.10"]
|
|
19
|
+
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v6
|
|
22
|
+
|
|
23
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
24
|
+
uses: actions/setup-python@v6
|
|
25
|
+
with:
|
|
26
|
+
python-version: ${{ matrix.python-version }}
|
|
27
|
+
|
|
28
|
+
- name: Install Dependencies
|
|
29
|
+
env:
|
|
30
|
+
PYARES_PACKAGE_VERSION: "0.0.1"
|
|
31
|
+
run: |
|
|
32
|
+
python -m pip install --upgrade pip
|
|
33
|
+
pip install -e .
|
|
34
|
+
pip install grpcio grpcio-tools protobuf pytest
|
|
35
|
+
|
|
36
|
+
- name: Run Tests
|
|
37
|
+
run: |
|
|
38
|
+
# This discovers all files starting with 'test_' in the 'tests' directory
|
|
39
|
+
# and runs them.
|
|
40
|
+
pytest -v tests/
|
pyares-0.3.0/.gitignore
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py,cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
#Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
#uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
#poetry.lock
|
|
109
|
+
|
|
110
|
+
# pdm
|
|
111
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
112
|
+
#pdm.lock
|
|
113
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
114
|
+
# in version control.
|
|
115
|
+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
|
116
|
+
.pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
121
|
+
__pypackages__/
|
|
122
|
+
|
|
123
|
+
# Celery stuff
|
|
124
|
+
celerybeat-schedule
|
|
125
|
+
celerybeat.pid
|
|
126
|
+
|
|
127
|
+
# SageMath parsed files
|
|
128
|
+
*.sage.py
|
|
129
|
+
|
|
130
|
+
# Environments
|
|
131
|
+
.env
|
|
132
|
+
.venv
|
|
133
|
+
env/
|
|
134
|
+
venv/
|
|
135
|
+
ENV/
|
|
136
|
+
env.bak/
|
|
137
|
+
venv.bak/
|
|
138
|
+
|
|
139
|
+
# Spyder project settings
|
|
140
|
+
.spyderproject
|
|
141
|
+
.spyproject
|
|
142
|
+
|
|
143
|
+
# Rope project settings
|
|
144
|
+
.ropeproject
|
|
145
|
+
|
|
146
|
+
# mkdocs documentation
|
|
147
|
+
/site
|
|
148
|
+
|
|
149
|
+
# mypy
|
|
150
|
+
.mypy_cache/
|
|
151
|
+
.dmypy.json
|
|
152
|
+
dmypy.json
|
|
153
|
+
|
|
154
|
+
# Pyre type checker
|
|
155
|
+
.pyre/
|
|
156
|
+
|
|
157
|
+
# pytype static type analyzer
|
|
158
|
+
.pytype/
|
|
159
|
+
|
|
160
|
+
# Cython debug symbols
|
|
161
|
+
cython_debug/
|
|
162
|
+
|
|
163
|
+
# Ruff stuff:
|
|
164
|
+
.ruff_cache/
|
|
165
|
+
|
|
166
|
+
# PyPI configuration file
|
|
167
|
+
.pypirc
|
pyares-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: PyAres
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: The official Python library for ARES.
|
|
5
|
+
Project-URL: Homepage, https://github.com/AFRL-ARES/PyAres
|
|
6
|
+
Author-email: AFRL <AFRL.RX.AREScontact@us.af.mil>
|
|
7
|
+
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Requires-Dist: ares-datamodel>=0.16.0
|
|
13
|
+
Requires-Dist: grpcio-tools>=1.75.1
|
|
14
|
+
Requires-Dist: grpcio<2.0.0,>=1.71.0
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# PyAres
|
|
18
|
+
The PyAres library is designed to provide support for building planners, analyzers and devices as part of your ARES self driving labratory. PyAres leverages the power of protobuf and gRPC to communicate with your ARES system while providing a simple Pythonic API.
|
|
19
|
+
|
|
20
|
+
### ✨ Features
|
|
21
|
+
* A Pythonic API built on the performance of Protobuf and gRPC to streamline the creation of self-driving lab components
|
|
22
|
+
* Easily define custom decision-making processes with your own PyAres Planners
|
|
23
|
+
* Integrate custom data processing and intepretation workflows with PyAres Analyzers
|
|
24
|
+
* Connect and control new hardware, making your implementations ARES ready as a PyAres Device
|
|
25
|
+
|
|
26
|
+
### 🏗️ Installation
|
|
27
|
+
|
|
28
|
+
PyAres can be installed using pip:
|
|
29
|
+
```console
|
|
30
|
+
pip install PyAres
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 🧠 Planner Usage
|
|
34
|
+
|
|
35
|
+
Planners can be initialized using the AresPlannerService class. Below is a basic example of setting up a planner.
|
|
36
|
+
|
|
37
|
+
``` Python
|
|
38
|
+
from PyAres import AresPlannerService
|
|
39
|
+
from PyAres import PlanRequest
|
|
40
|
+
from PyAres import PlanResponse
|
|
41
|
+
from PyAres import AresDataType
|
|
42
|
+
|
|
43
|
+
import random
|
|
44
|
+
|
|
45
|
+
def plan(request: PlanRequest) -> PlanResponse:
|
|
46
|
+
#This is where your custom planning logic goes
|
|
47
|
+
planned_values = []
|
|
48
|
+
names = []
|
|
49
|
+
|
|
50
|
+
for param in request.parameters:
|
|
51
|
+
planned_values.append(random.uniform(param.minimum_value, param.maximum_value))
|
|
52
|
+
names.append(param.name)
|
|
53
|
+
|
|
54
|
+
return PlanResponse(parameter_names=names, parameter_values=planned_values)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
#Basic details about your planner
|
|
59
|
+
name = "Demo Planner"
|
|
60
|
+
version = "1.0.0"
|
|
61
|
+
description = "This is a test planner to demonstrate working with PyAres to create planners!"
|
|
62
|
+
pythonDemoPlanner = AresPlannerService(plan, name, description, version)
|
|
63
|
+
|
|
64
|
+
#Add Supported Types
|
|
65
|
+
pythonDemoPlanner.add_supported_type(AresDataType.NUMBER)
|
|
66
|
+
```
|
|
67
|
+
This example creates a simple planner called "Demo Planner", that supports planning for numeric values. The 'plan' method shown here is where our custom planning logic lives. For this example, we generate a random number between the minimum and maximum value of each provided parameter.
|
|
68
|
+
|
|
69
|
+
### 🔍 Analyzer Usage
|
|
70
|
+
|
|
71
|
+
Analyzers can be initialized using the AresAnalyzerService class. Below is a basic example of setting up an analyzer.
|
|
72
|
+
|
|
73
|
+
```Python
|
|
74
|
+
from PyAres import AresAnalyzerService
|
|
75
|
+
from PyAres import AnalysisRequest
|
|
76
|
+
from PyAres import Analysis
|
|
77
|
+
from PyAres import AresDataType
|
|
78
|
+
|
|
79
|
+
def analyze(request: AnalysisRequest) -> Analysis:
|
|
80
|
+
#Custom Analysis Logic
|
|
81
|
+
growth = request.inputs.get("Growth")
|
|
82
|
+
temperature = request.inputs.get("Temperature")
|
|
83
|
+
|
|
84
|
+
print(f"Growth: {growth}")
|
|
85
|
+
print(f"Temperature: {temperature}")
|
|
86
|
+
|
|
87
|
+
analysis = Analysis(result=growth, success=True)
|
|
88
|
+
return analysis
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
#Basic details about your analyzer
|
|
93
|
+
name = "Python Test Analyzer"
|
|
94
|
+
version = "0.0.1"
|
|
95
|
+
description = "This is a test analyzer to demonstrate working with PyAres to create analyzers!"
|
|
96
|
+
pythonDemoAnalyzer = AresAnalyzerService(analyze, name, version, description)
|
|
97
|
+
|
|
98
|
+
#Add Analysis Parameters
|
|
99
|
+
pythonDemoAnalyzer.add_analysis_parameter("Growth", AresDataType.NUMBER)
|
|
100
|
+
pythonDemoAnalyzer.add_analysis_parameter("Temperature", AresDataType.NUMBER)
|
|
101
|
+
|
|
102
|
+
pythonDemoAnalyzer.start()
|
|
103
|
+
```
|
|
104
|
+
This example creates a simple analyzer that expects to receive two values from ARES, growth and temperature. It then returns a simple static value of six as the analysis result.
|
|
105
|
+
|
|
106
|
+
### 💻 Device Usage
|
|
107
|
+
|
|
108
|
+
PyAres gives you the ability to create devices to communicate with your ARES system. Typically your device would be external hardware connected via serial port or USB to your computer. For demonstration purposes, below is a simulated device that has a modifiable temperature value. It's temperature can be set with the set_temperature method, or retrieved with the get_temperature method. We use a five second delay in the set_temperature method to simulate a delayed response from hardward. It also implements the get_device_state method which returns any data ARES should log as this devices state, and enter_safe_mode to fulfill the required ability for ARES to be able to reset any device to a known state.
|
|
109
|
+
|
|
110
|
+
```Python
|
|
111
|
+
class DemoDevice:
|
|
112
|
+
# A simulated device. In reality, these communications would be happening with external hardware over serial, usb, etc.
|
|
113
|
+
def __init__(self):
|
|
114
|
+
self.temperature = 0.0
|
|
115
|
+
|
|
116
|
+
def set_temperature(self, temperature: float):
|
|
117
|
+
self.temperature = temperature
|
|
118
|
+
time.sleep(5)
|
|
119
|
+
return {}
|
|
120
|
+
|
|
121
|
+
def get_temperature(self):
|
|
122
|
+
# Dictionary key should match what we defined in our schema earlier
|
|
123
|
+
return { "temperature": self.temperature }
|
|
124
|
+
|
|
125
|
+
def get_device_state(self):
|
|
126
|
+
state_dictionary = { "temperature": self.temperature }
|
|
127
|
+
return state_dictionary
|
|
128
|
+
|
|
129
|
+
def enter_safe_mode(self):
|
|
130
|
+
self.temperature = 0
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
PyAres can be used to connect this simulated device with ARES. Below is a basic example of setting up a PyAres device.
|
|
134
|
+
|
|
135
|
+
```Python
|
|
136
|
+
device = DemoDevice()
|
|
137
|
+
|
|
138
|
+
if __name__ == "__main__":
|
|
139
|
+
# Basic information about my device
|
|
140
|
+
device_name = "Demo Device"
|
|
141
|
+
description = "A device to demonstrate the PyAres device capabilities"
|
|
142
|
+
version = "1.0.0"
|
|
143
|
+
device_service = AresDeviceService(device.enter_safe_mode, device.get_device_state, device_name, description, version)
|
|
144
|
+
|
|
145
|
+
#Create Command Descriptor, then add command
|
|
146
|
+
parameter_schema = DeviceSchemaEntry(AresDataType.NUMBER, "A numeric temperature value", "Degree's Celsius")
|
|
147
|
+
input_schema = { "temperature": parameter_schema }
|
|
148
|
+
descriptor = DeviceCommandDescriptor("Set Temperature", "Set's the temperature of the demo device to the provided value.", input_schema, {})
|
|
149
|
+
device_service.add_new_command(descriptor, device.set_temperature)
|
|
150
|
+
|
|
151
|
+
output_schema = {"temperature": DeviceSchemaEntry(AresDataType.NUMBER, "The current temperature of the device", "Degree's Celsius")}
|
|
152
|
+
get_temp_desc = DeviceCommandDescriptor("Get Temperature", "Get's the current temperature of the demo device.", {}, output_schema)
|
|
153
|
+
device_service.add_new_command(get_temp_desc, device.get_temperature)
|
|
154
|
+
|
|
155
|
+
#Add Settings
|
|
156
|
+
device_service.add_setting("Allow Negative Values", True)
|
|
157
|
+
|
|
158
|
+
device_service.start()
|
|
159
|
+
```
|
|
160
|
+
The central component to your PyAres device is your AresDeviceService. This class acts as a bridge, managing all gRPC communications between PyAres and ARES, and provides the ability to define the behavior and capabilities of your device. Here we create a device with two commands; "Get Temperature" and "Set Temperature". To define commands in PyAres, you must provide a defined schema for both the input and output of the command in the form of a dictionary. This gives the PyAres user a flexible way to represent the data that your commands expect to receive, as well as the data ARES should expect to come from your commands. This information becomes part of your DeviceCommandDescriptor, which also holds a name for your command as well as a brief description. We then report our command capabilities to ARES via the add_new_command method. This method takes in our descriptor, and a reference to the method you defined for your command.
|
|
161
|
+
|
|
162
|
+
### 📄 License
|
|
163
|
+
|
|
164
|
+
The PyAres project is licensed under the MIT License - see details in [LICENSE.txt]([https://github.com/AFRL-ARES/PyAres/blob/Develop/LICENSE.txt](https://github.com/AFRL-ARES/PyAres/blob/Develop/LICENSE)) <br></br>
|
|
165
|
+
|
|
166
|
+
### CLEARANCE
|
|
167
|
+
Distribution A. Approved for public release: distribution unlimited. AFRL-2025-5332.
|
|
168
|
+
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Standard Imports
|
|
2
2
|
import grpc
|
|
3
3
|
from concurrent import futures
|
|
4
|
-
from typing import Callable, Awaitable, Union, Mapping, Dict
|
|
4
|
+
from typing import Callable, Awaitable, Union, Mapping, Dict, Optional
|
|
5
5
|
|
|
6
6
|
# Import generated protobuf and gRPC stubs
|
|
7
7
|
from ares_datamodel.analyzing.remote import ares_remote_analyzer_service_pb2 as analyzer_service
|
|
@@ -22,6 +22,7 @@ from ..Utils import ares_outcome_utils
|
|
|
22
22
|
|
|
23
23
|
# Import python models
|
|
24
24
|
from ..Models import ares_data_models, RequestMetadata
|
|
25
|
+
from ..Models import AresSchemaEntry
|
|
25
26
|
from .analyzer_models import AnalysisRequest, Analysis, InfoResponse
|
|
26
27
|
|
|
27
28
|
# Type hints for the user's custom logic
|
|
@@ -120,14 +121,7 @@ class AresAnalyzerServiceWrapper(analyzer_service_grpc.AresRemoteAnalyzerService
|
|
|
120
121
|
try:
|
|
121
122
|
for(key, value) in self._settings.items():
|
|
122
123
|
settings_entry = capabilities.settings_schema.fields[key]
|
|
123
|
-
settings_entry.
|
|
124
|
-
settings_entry.optional = value.optional
|
|
125
|
-
|
|
126
|
-
if len(value.string_choices.strings) != 0:
|
|
127
|
-
settings_entry.string_choices.strings.extend(value.string_choices.strings)
|
|
128
|
-
|
|
129
|
-
elif len(value.number_choices.numbers) != 0:
|
|
130
|
-
settings_entry.number_choices.numbers.extend(value.number_choices.numbers)
|
|
124
|
+
settings_entry.CopyFrom(value)
|
|
131
125
|
|
|
132
126
|
return capabilities
|
|
133
127
|
|
|
@@ -143,25 +137,21 @@ class AresAnalyzerServiceWrapper(analyzer_service_grpc.AresRemoteAnalyzerService
|
|
|
143
137
|
print(f"Exception while trying to respond to ARES with connection status! {e}")
|
|
144
138
|
|
|
145
139
|
def ValidateInputs(self, request: analyzer_service.ParameterValidationRequest, context):
|
|
146
|
-
print("Validating Inputs")
|
|
147
140
|
response = analyzer_service.ParameterValidationResult(success=True)
|
|
148
|
-
provided_params: Mapping[str,
|
|
141
|
+
provided_params: Mapping[str, ares_data_schema_pb2.SchemaEntry] = request.input_schema.fields
|
|
149
142
|
|
|
150
143
|
for stored_key, stored_schema in self._analysis_parameters.items():
|
|
151
144
|
if stored_key in provided_params:
|
|
152
|
-
matching_schema = provided_params
|
|
153
|
-
|
|
154
|
-
if stored_schema.type != matching_schema:
|
|
145
|
+
matching_schema = provided_params[stored_key]
|
|
146
|
+
if stored_schema.type != matching_schema.type:
|
|
155
147
|
message = f"Schema Mismatch! {stored_key} was provided with the value type {stored_schema.type}, but the value type {matching_schema} was expected!"
|
|
156
148
|
response.messages.append(message)
|
|
157
|
-
print(message)
|
|
158
149
|
else:
|
|
159
150
|
if not stored_schema.optional:
|
|
160
151
|
message = f"Schema Missing! {stored_key} is marked as a required piece of data for analysis, but no assignment was found in the provided schema!"
|
|
161
152
|
response.messages.append(message)
|
|
162
|
-
print(message)
|
|
163
153
|
|
|
164
|
-
if response.messages
|
|
154
|
+
if len(response.messages) != 0:
|
|
165
155
|
response.success = False
|
|
166
156
|
|
|
167
157
|
return response
|
|
@@ -210,7 +200,7 @@ class AresAnalyzerService:
|
|
|
210
200
|
else:
|
|
211
201
|
self._server.add_insecure_port(f'[::]:{self._port}')
|
|
212
202
|
|
|
213
|
-
def add_setting(self, setting_name: str, setting_type: ares_data_models.AresDataType, optional: bool = True, constraints: Union[list[int], list[str], list[float]] = []):
|
|
203
|
+
def add_setting(self, setting_name: str, setting_type: ares_data_models.AresDataType, optional: bool = True, constraints: Union[list[int], list[str], list[float]] = [], struct_schema: Optional[Dict[str, AresSchemaEntry]] = None):
|
|
214
204
|
"""
|
|
215
205
|
Adds an analyzer setting to be reported to ARES when capabilities are requested.
|
|
216
206
|
While most `PyAres.Models.AresDataType` options are supported, bool arrays and byte arrays
|
|
@@ -221,10 +211,11 @@ class AresAnalyzerService:
|
|
|
221
211
|
setting_type (AresDataType): The type of this settings value.
|
|
222
212
|
optional (bool): Whether the setting is optional.
|
|
223
213
|
constraints: An optional list of values to constrain the available setting choices. Can be integers, strings, or floats.
|
|
214
|
+
struct_schema: An optional dictionary defining the fields of a STRUCT type setting, using AresSchemaEntry objects.
|
|
224
215
|
"""
|
|
225
|
-
self._service_wrapper._settings[setting_name] = ares_data_schema_utils.create_settings_schema_entry(setting_type, optional, constraints)
|
|
216
|
+
self._service_wrapper._settings[setting_name] = ares_data_schema_utils.create_settings_schema_entry(setting_type, optional, constraints, struct_schema)
|
|
226
217
|
|
|
227
|
-
def add_analysis_parameter(self, parameter_name: str, parameter_type: ares_data_models.AresDataType, optional: bool = False):
|
|
218
|
+
def add_analysis_parameter(self, parameter_name: str, parameter_type: ares_data_models.AresDataType, optional: bool = False, struct_schema: Optional[Dict[str, AresSchemaEntry]] = None):
|
|
228
219
|
"""
|
|
229
220
|
Adds an analysis parameter that will be reported to ARES. Analysis parameters are inputs your analyzer accepts from ARES, and will be mapped to command outputs
|
|
230
221
|
in experiment scripts.
|
|
@@ -233,8 +224,9 @@ class AresAnalyzerService:
|
|
|
233
224
|
parameter_name (str): The name of the parameter being created.
|
|
234
225
|
parameter_type (AresDataType): The type associated with the new parameter.
|
|
235
226
|
optional (bool): Defaults to false. Determines whether your analyzer requires this information.
|
|
227
|
+
struct_schema: An optional dictionary defining the fields of a STRUCT type parameter, using AresSchemaEntry objects.
|
|
236
228
|
"""
|
|
237
|
-
self._service_wrapper._analysis_parameters[parameter_name] = ares_data_schema_utils.create_settings_schema_entry(parameter_type, optional, [])
|
|
229
|
+
self._service_wrapper._analysis_parameters[parameter_name] = ares_data_schema_utils.create_settings_schema_entry(parameter_type, optional, [], struct_schema)
|
|
238
230
|
|
|
239
231
|
def set_timeout(self, new_timeout: int):
|
|
240
232
|
"""
|
|
@@ -267,12 +259,3 @@ class AresAnalyzerService:
|
|
|
267
259
|
"""
|
|
268
260
|
print("Stopping Ares Analyzer Service...")
|
|
269
261
|
self._server.stop(0).wait()
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
@@ -128,8 +128,7 @@ class AresDeviceServiceWrapper(device_service_grpc.AresRemoteDeviceServiceServic
|
|
|
128
128
|
try:
|
|
129
129
|
for key, value in self._current_settings.items():
|
|
130
130
|
new_entry = response.settings.fields[key]
|
|
131
|
-
|
|
132
|
-
new_entry.CopyFrom(new_ares_value)
|
|
131
|
+
new_entry.CopyFrom(value)
|
|
133
132
|
|
|
134
133
|
return response
|
|
135
134
|
|
|
@@ -138,7 +137,10 @@ class AresDeviceServiceWrapper(device_service_grpc.AresRemoteDeviceServiceServic
|
|
|
138
137
|
return response
|
|
139
138
|
|
|
140
139
|
def SetSettings(self, request: device_service.SetSettingsRequest, context) -> empty_pb2.Empty:
|
|
141
|
-
|
|
140
|
+
for key, value in request.settings.fields.items():
|
|
141
|
+
if key in self._current_settings:
|
|
142
|
+
self._current_settings[key] = value
|
|
143
|
+
|
|
142
144
|
return empty_pb2.Empty()
|
|
143
145
|
|
|
144
146
|
def GetStateSchema(self, request, context) -> device_service.StateSchemaResponse:
|
|
@@ -181,6 +183,10 @@ class AresDeviceServiceWrapper(device_service_grpc.AresRemoteDeviceServiceServic
|
|
|
181
183
|
if polling_info.polling_type == device_polling_settings_pb2.PollingType.INTERVAL:
|
|
182
184
|
delay = polling_info.interval_ms/1000
|
|
183
185
|
while True:
|
|
186
|
+
if context.is_active() == False:
|
|
187
|
+
print("Client for ARES device has disconnected.")
|
|
188
|
+
break
|
|
189
|
+
|
|
184
190
|
response = self._update_device_state()
|
|
185
191
|
if isinstance(response, Awaitable):
|
|
186
192
|
response = response.__await__()
|
|
@@ -195,11 +201,6 @@ class AresDeviceServiceWrapper(device_service_grpc.AresRemoteDeviceServiceServic
|
|
|
195
201
|
ares_struct_utils.add_value_to_struct(proto_response.state, key, ares_value_utils.create_ares_value(value))
|
|
196
202
|
|
|
197
203
|
yield proto_response
|
|
198
|
-
|
|
199
|
-
if context.is_active() == False:
|
|
200
|
-
print("Client for ARES device has disconnected.")
|
|
201
|
-
break
|
|
202
|
-
|
|
203
204
|
time.sleep(delay)
|
|
204
205
|
|
|
205
206
|
else:
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
from .ares_data_models import AresDataType, Outcome, RequestMetadata
|
|
1
|
+
from .ares_data_models import AresDataType, Outcome, RequestMetadata, AresSchemaEntry
|
|
2
2
|
|
|
3
3
|
__all__ = [
|
|
4
4
|
"AresDataType",
|
|
5
5
|
"Outcome",
|
|
6
|
-
"RequestMetadata"
|
|
6
|
+
"RequestMetadata",
|
|
7
|
+
"AresSchemaEntry"
|
|
7
8
|
]
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from ares_datamodel import request_metadata_pb2
|
|
2
2
|
from enum import Enum
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Union, List, Optional, Dict
|
|
3
5
|
|
|
4
6
|
class AresDataType(Enum):
|
|
5
7
|
UNKNOWN = 0
|
|
@@ -9,8 +11,9 @@ class AresDataType(Enum):
|
|
|
9
11
|
NUMBER = 4
|
|
10
12
|
STRING_ARRAY = 5
|
|
11
13
|
NUMBER_ARRAY = 6
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
LIST = 7
|
|
15
|
+
STRUCT = 8
|
|
16
|
+
BYTE_ARRAY = 9
|
|
14
17
|
|
|
15
18
|
class Outcome(Enum):
|
|
16
19
|
UNSPECIFIED_OUTCOME = 0
|
|
@@ -30,4 +33,13 @@ class RequestMetadata():
|
|
|
30
33
|
def from_default_values(cls):
|
|
31
34
|
""" Alternative constructor for creating fake metadata """
|
|
32
35
|
default = request_metadata_pb2.RequestMetadata(system_name="TEST SYSTEM", campaign_name="TEST CAMPAIGN", campaign_id="TEST ID", experiment_id="TEST EXPERIMENT ID")
|
|
33
|
-
return cls(default)
|
|
36
|
+
return cls(default)
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class AresSchemaEntry:
|
|
40
|
+
type: AresDataType
|
|
41
|
+
optional: bool = False
|
|
42
|
+
description: str = ""
|
|
43
|
+
unit: str = ""
|
|
44
|
+
choices: Union[List[str], List[int], List[float]] = field(default_factory=list)
|
|
45
|
+
struct_schema: Optional[Dict[str, 'AresSchemaEntry']] = None
|
|
@@ -41,7 +41,7 @@ class PlanningParameter:
|
|
|
41
41
|
self.name: str = name
|
|
42
42
|
self.minimum_value: float = minimum_value
|
|
43
43
|
self.maximum_value: float = maximum_value
|
|
44
|
-
self.param_history: List = param_history
|
|
44
|
+
self.param_history: List[ParameterHistoryItem] = param_history
|
|
45
45
|
self.data_type: AresDataType = data_type
|
|
46
46
|
self.is_planned: bool = is_planned
|
|
47
47
|
self.is_result: bool = is_result
|
|
@@ -56,7 +56,7 @@ class AresPlannerServiceWrapper(planner_service_grpc.AresRemotePlannerServiceSer
|
|
|
56
56
|
for(key, value) in self._settings.items():
|
|
57
57
|
settings_entry: ares_data_schema_pb2.SchemaEntry = capabilities.settings_schema.fields[key]
|
|
58
58
|
settings_entry.type = value.type
|
|
59
|
-
settings_entry.optional
|
|
59
|
+
settings_entry.optional = value.optional
|
|
60
60
|
|
|
61
61
|
if len(value.string_choices.strings) != 0:
|
|
62
62
|
settings_entry.string_choices.strings.extend(value.string_choices.strings)
|
|
@@ -258,7 +258,7 @@ class AresPlannerService:
|
|
|
258
258
|
Setting this value to false will allow you to continue execution after starting your service, however this should ONLY be done if you have
|
|
259
259
|
another mechanism for keeping your process alive (such as a GUI, or a loop). Defaults to true.
|
|
260
260
|
"""
|
|
261
|
-
print(f"Starting Ares
|
|
261
|
+
print(f"Starting Ares Planning Service on port {self._port}...")
|
|
262
262
|
self._server.start()
|
|
263
263
|
|
|
264
264
|
if wait_for_termination:
|
|
@@ -269,4 +269,4 @@ class AresPlannerService:
|
|
|
269
269
|
Stops the service, terminating the connection.
|
|
270
270
|
"""
|
|
271
271
|
print("Stopping Ares Planning Service...")
|
|
272
|
-
self._server.stop(0).wait()
|
|
272
|
+
self._server.stop(0).wait()
|