PyAres 0.1.4__tar.gz → 0.2.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.1.4 → pyares-0.2.0}/PKG-INFO +2 -2
- {pyares-0.1.4 → pyares-0.2.0}/PyAres/Analyzing/analysis_service.py +22 -7
- {pyares-0.1.4/PyAres/Test → pyares-0.2.0/PyAres/Demo}/analyzer_test.py +2 -0
- pyares-0.2.0/PyAres/Demo/analyzer_test_tools_demo.py +19 -0
- pyares-0.2.0/PyAres/Demo/analyzer_wiki.py +31 -0
- pyares-0.2.0/PyAres/Demo/hotplate.py +72 -0
- pyares-0.2.0/PyAres/Demo/planner_test_tools_demo.py +21 -0
- pyares-0.2.0/PyAres/Demo/planner_wiki.py +29 -0
- pyares-0.2.0/PyAres/Demo/rotary_mixer.py +37 -0
- {pyares-0.1.4 → pyares-0.2.0}/PyAres/Device/device_service.py +23 -6
- {pyares-0.1.4 → pyares-0.2.0}/PyAres/Models/ares_data_models.py +7 -1
- {pyares-0.1.4 → pyares-0.2.0}/PyAres/Planning/planner_models.py +28 -10
- {pyares-0.1.4 → pyares-0.2.0}/PyAres/Planning/planning_service.py +33 -13
- {pyares-0.1.4 → pyares-0.2.0}/PyAres/Utils/ares_data_schema_utils.py +5 -13
- {pyares-0.1.4 → pyares-0.2.0}/PyAres/Utils/ares_data_type_utils.py +2 -2
- {pyares-0.1.4 → pyares-0.2.0}/PyAres/Utils/ares_outcome_utils.py +1 -1
- {pyares-0.1.4 → pyares-0.2.0}/PyAres/Utils/ares_struct_utils.py +32 -16
- {pyares-0.1.4 → pyares-0.2.0}/PyAres/Utils/ares_value_utils.py +16 -13
- pyares-0.2.0/PyAres/Utils/param_history_info_utils.py +10 -0
- pyares-0.2.0/PyAres/Utils/plan_request_utils.py +11 -0
- pyares-0.2.0/PyAres/Utils/plan_response_utils.py +16 -0
- pyares-0.2.0/PyAres/Utils/planning_param_utils.py +20 -0
- pyares-0.2.0/PyAres/test_tools/__init__.py +2 -0
- pyares-0.2.0/PyAres/test_tools/analyzer_tester.py +92 -0
- pyares-0.2.0/PyAres/test_tools/planner_tester.py +86 -0
- {pyares-0.1.4 → pyares-0.2.0}/PyAres.egg-info/PKG-INFO +2 -2
- {pyares-0.1.4 → pyares-0.2.0}/PyAres.egg-info/SOURCES.txt +17 -4
- {pyares-0.1.4 → pyares-0.2.0}/PyAres.egg-info/requires.txt +1 -1
- {pyares-0.1.4 → pyares-0.2.0}/README.md +5 -1
- {pyares-0.1.4 → pyares-0.2.0}/pyproject.toml +2 -5
- {pyares-0.1.4 → pyares-0.2.0}/LICENSE +0 -0
- {pyares-0.1.4 → pyares-0.2.0}/PyAres/Analyzing/__init__.py +0 -0
- {pyares-0.1.4 → pyares-0.2.0}/PyAres/Analyzing/analyzer_models.py +0 -0
- {pyares-0.1.4/PyAres/Test → pyares-0.2.0/PyAres/Demo}/device_test.py +0 -0
- {pyares-0.1.4/PyAres/Test → pyares-0.2.0/PyAres/Demo}/planner_test.py +0 -0
- {pyares-0.1.4 → pyares-0.2.0}/PyAres/Device/__init__.py +0 -0
- {pyares-0.1.4 → pyares-0.2.0}/PyAres/Device/device_models.py +0 -0
- {pyares-0.1.4 → pyares-0.2.0}/PyAres/Device/device_warnings.py +0 -0
- {pyares-0.1.4 → pyares-0.2.0}/PyAres/Models/__init__.py +0 -0
- {pyares-0.1.4 → pyares-0.2.0}/PyAres/Planning/__init__.py +0 -0
- {pyares-0.1.4 → pyares-0.2.0}/PyAres/Utils/ares_device_command_utils.py +0 -0
- {pyares-0.1.4 → pyares-0.2.0}/PyAres/__init__.py +0 -0
- {pyares-0.1.4 → pyares-0.2.0}/PyAres.egg-info/dependency_links.txt +0 -0
- {pyares-0.1.4 → pyares-0.2.0}/PyAres.egg-info/top_level.txt +0 -0
- {pyares-0.1.4 → pyares-0.2.0}/setup.cfg +0 -0
- {pyares-0.1.4 → pyares-0.2.0}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyAres
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: The official Python library for ARES.
|
|
5
5
|
Author-email: AFRL <AFRL.RX.AREScontact@us.af.mil>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -9,5 +9,5 @@ Requires-Python: >=3.10
|
|
|
9
9
|
License-File: LICENSE
|
|
10
10
|
Requires-Dist: grpcio-tools>=1.75.1
|
|
11
11
|
Requires-Dist: grpcio<2.0.0,>=1.71.0
|
|
12
|
-
Requires-Dist: ares-datamodel>=0.
|
|
12
|
+
Requires-Dist: ares-datamodel>=0.15.1b1
|
|
13
13
|
Dynamic: license-file
|
|
@@ -177,7 +177,8 @@ class AresAnalyzerService:
|
|
|
177
177
|
description: str = "",
|
|
178
178
|
timeout: int = 30,
|
|
179
179
|
use_localhost: bool = True,
|
|
180
|
-
port: int = 7083
|
|
180
|
+
port: int = 7083,
|
|
181
|
+
max_message_size: int = -1):
|
|
181
182
|
"""
|
|
182
183
|
Initializes the AresAnalyzerService.
|
|
183
184
|
|
|
@@ -190,11 +191,17 @@ class AresAnalyzerService:
|
|
|
190
191
|
description (str): A brief description of your analyzer.
|
|
191
192
|
use_localhost (bool): If true, binds to localhost. Otherwise, binds to [::].
|
|
192
193
|
port (int): The port that your analyzer service will serve on. Defaults to port 7083.
|
|
194
|
+
max_message_size (int): The max size, in megabytes, of the messages your Analysis service is capable of sending. Increasing this can help transfer data like images, but may result in some loss in performance
|
|
193
195
|
"""
|
|
196
|
+
|
|
194
197
|
self.info = InfoResponse(name=name, version=version, description=description)
|
|
195
198
|
self._capabilities = analyzer_capabilities_pb2.AnalyzerCapabilities(settings_schema={})
|
|
196
199
|
self._port = port
|
|
197
|
-
|
|
200
|
+
server_options = []
|
|
201
|
+
if max_message_size != -1:
|
|
202
|
+
print("Setting Custom Max Message Size")
|
|
203
|
+
server_options.append(('grpc.max_receive_message_length', max_message_size * 1024 * 1024))
|
|
204
|
+
self._server = grpc.server(futures.ThreadPoolExecutor(max_workers=10), options=server_options)
|
|
198
205
|
self._service_wrapper = AresAnalyzerServiceWrapper(info=self.info, timeout=timeout, custom_analysis_logic=custom_analysis_logic)
|
|
199
206
|
analyzer_service_grpc.add_AresRemoteAnalyzerServiceServicer_to_server(self._service_wrapper, self._server)
|
|
200
207
|
|
|
@@ -238,13 +245,21 @@ class AresAnalyzerService:
|
|
|
238
245
|
"""
|
|
239
246
|
self._capabilities.timeout_seconds = new_timeout
|
|
240
247
|
|
|
241
|
-
def start(self):
|
|
242
|
-
"""
|
|
243
|
-
Starts the service on the specified port, and waits for termination.
|
|
248
|
+
def start(self, wait_for_termination: bool = True):
|
|
249
|
+
"""
|
|
250
|
+
Starts the service on the specified port, and waits for termination.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
wait_for_termination (bool): A boolean value that determines whether the start method will use the "wait_for_termination" blocking call.
|
|
254
|
+
If true, the gRPC service will keep the main thread alive but at the cost of blocking any continued execution of your python logic.
|
|
255
|
+
Setting this value to false will allow you to continue execution after starting your service, however this should ONLY be done if you have
|
|
256
|
+
another mechanism for keeping your process alive (such as a GUI, or a loop). Defaults to true.
|
|
244
257
|
"""
|
|
245
|
-
print(f"Starting Ares
|
|
258
|
+
print(f"Starting Ares Device Service on port {self._port}...")
|
|
246
259
|
self._server.start()
|
|
247
|
-
|
|
260
|
+
|
|
261
|
+
if wait_for_termination:
|
|
262
|
+
self._server.wait_for_termination()
|
|
248
263
|
|
|
249
264
|
def stop(self):
|
|
250
265
|
"""
|
|
@@ -23,5 +23,7 @@ if __name__ == "__main__":
|
|
|
23
23
|
|
|
24
24
|
#Add Analysis Parameters
|
|
25
25
|
pythonDemoAnalyzer.add_analysis_parameter("Temperature", AresDataType.NUMBER)
|
|
26
|
+
pythonDemoAnalyzer.add_setting(setting_name="", setting_type=AresDataType.NULL, optional=True, constraints=[])
|
|
27
|
+
pythonDemoAnalyzer.start(wait_for_termination=True)
|
|
26
28
|
|
|
27
29
|
pythonDemoAnalyzer.start()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from PyAres.test_tools import AnalyzerTestClient
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
if __name__ == "__main__":
|
|
5
|
+
client = AnalyzerTestClient()
|
|
6
|
+
|
|
7
|
+
# 1. Health Checks
|
|
8
|
+
client.check_status()
|
|
9
|
+
client.get_info()
|
|
10
|
+
|
|
11
|
+
# 2. Run a Mock Analysis
|
|
12
|
+
# Replace these keys with actual data your analyzer expects
|
|
13
|
+
sample_inputs = {
|
|
14
|
+
"Temperature": 130.5
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
client.run_analysis(inputs=sample_inputs)
|
|
18
|
+
|
|
19
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from PyAres import AresAnalyzerService, AnalysisRequest, Analysis, AresDataType, Outcome
|
|
2
|
+
|
|
3
|
+
def analyze_sample(request: AnalysisRequest) -> Analysis:
|
|
4
|
+
# 1. Extract inputs
|
|
5
|
+
# 'Growth_Metric' would come from a sensor or previous step
|
|
6
|
+
raw_value = request.inputs.get("Growth_Metric")
|
|
7
|
+
|
|
8
|
+
if raw_value is None:
|
|
9
|
+
return Analysis(result=0.0, outcome=Outcome.FAILURE)
|
|
10
|
+
|
|
11
|
+
# 2. Perform Logic
|
|
12
|
+
print(f"Analyzing sample with value: {raw_value}")
|
|
13
|
+
|
|
14
|
+
calculated_score = raw_value * 1.5 # Placeholder logic
|
|
15
|
+
is_success = calculated_score > 10.0 # Define success criteria
|
|
16
|
+
|
|
17
|
+
# 3. Return Result
|
|
18
|
+
return Analysis(result=calculated_score, outcome=Outcome.SUCCESS)
|
|
19
|
+
|
|
20
|
+
if __name__ == "__main__":
|
|
21
|
+
service = AresAnalyzerService(
|
|
22
|
+
analyze_sample,
|
|
23
|
+
"Growth Analyzer",
|
|
24
|
+
"0.1.0",
|
|
25
|
+
"Calculates growth viability"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Define what data we need from ARES
|
|
29
|
+
service.add_analysis_parameter("Growth_Metric", AresDataType.NUMBER)
|
|
30
|
+
|
|
31
|
+
service.start()
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from PyAres import AresDeviceService, AresDataType, DeviceSchemaEntry, DeviceCommandDescriptor
|
|
2
|
+
|
|
3
|
+
# --- PART 1: The Simulated Hardware ---
|
|
4
|
+
class VirtualHotplate:
|
|
5
|
+
def __init__(self):
|
|
6
|
+
self.target_temp = 25.0 # Start at room temp
|
|
7
|
+
|
|
8
|
+
def set_temperature(self, temp: float):
|
|
9
|
+
"""Simulates setting the heater."""
|
|
10
|
+
print(f"[Hardware] Heating to {temp}°C...")
|
|
11
|
+
self.target_temp = temp
|
|
12
|
+
return {} # Return empty dict if no data needs to be sent back
|
|
13
|
+
|
|
14
|
+
def get_temperature(self):
|
|
15
|
+
"""Simulates reading the sensor."""
|
|
16
|
+
# In a real device, you'd read a serial port here.
|
|
17
|
+
print("[Hardware] Retrieving the current temperature...")
|
|
18
|
+
return { "current_temp": self.target_temp }
|
|
19
|
+
|
|
20
|
+
def get_state(self):
|
|
21
|
+
"""Required: Tells ARES the current status for logging."""
|
|
22
|
+
return { "current_temp": self.target_temp }
|
|
23
|
+
|
|
24
|
+
def safe_mode(self):
|
|
25
|
+
"""Required: A safety fallback (e.g., turn off heat)."""
|
|
26
|
+
print("[Hardware] SAFE MODE TRIGGERED: Heater off.")
|
|
27
|
+
self.target_temp = 0.0
|
|
28
|
+
|
|
29
|
+
# --- PART 2: The Ares Service Wrapper ---
|
|
30
|
+
if __name__ == "__main__":
|
|
31
|
+
# 1. Initialize the hardware
|
|
32
|
+
my_hotplate = VirtualHotplate()
|
|
33
|
+
|
|
34
|
+
# 2. Define the Service Info
|
|
35
|
+
service = AresDeviceService(
|
|
36
|
+
my_hotplate.safe_mode,
|
|
37
|
+
my_hotplate.get_state,
|
|
38
|
+
"My Virtual Hotplate", # Device Name
|
|
39
|
+
"A simulated lab hotplate", # Description
|
|
40
|
+
"1.0.0" # Version
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# 3. Define Command: Set Temperature
|
|
44
|
+
# This schema tells ARES to draw a Number Input box in the UI
|
|
45
|
+
input_schema = {
|
|
46
|
+
"temp": DeviceSchemaEntry(AresDataType.NUMBER, "Target Temperature", "Celsius")
|
|
47
|
+
}
|
|
48
|
+
set_cmd = DeviceCommandDescriptor(
|
|
49
|
+
"Set Temp",
|
|
50
|
+
"Sets the hotplate target temperature",
|
|
51
|
+
input_schema,
|
|
52
|
+
{} # No output expected
|
|
53
|
+
)
|
|
54
|
+
service.add_new_command(set_cmd, my_hotplate.set_temperature)
|
|
55
|
+
|
|
56
|
+
# 4. Define Command: Get Temperature
|
|
57
|
+
# This schema tells ARES to expect a number back
|
|
58
|
+
output_schema = {
|
|
59
|
+
"current_temp": DeviceSchemaEntry(AresDataType.NUMBER, "Current Temperature", "Celsius")
|
|
60
|
+
}
|
|
61
|
+
get_cmd = DeviceCommandDescriptor(
|
|
62
|
+
"Get Temp",
|
|
63
|
+
"Reads the current temperature",
|
|
64
|
+
{}, # No input needed
|
|
65
|
+
output_schema
|
|
66
|
+
)
|
|
67
|
+
service.add_new_command(get_cmd, my_hotplate.get_temperature)
|
|
68
|
+
|
|
69
|
+
# 5. Start the Service
|
|
70
|
+
# This will block and listen for ARES connections
|
|
71
|
+
print("Virtual Hotplate Service Running...")
|
|
72
|
+
service.start()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from PyAres import PlanRequest, PlanningParameter, AresDataType, ParameterHistoryItem
|
|
2
|
+
from PyAres.test_tools import PlannerTestClient
|
|
3
|
+
|
|
4
|
+
if __name__ == "__main__":
|
|
5
|
+
client = PlannerTestClient()
|
|
6
|
+
|
|
7
|
+
# 1. Health Checks
|
|
8
|
+
client.check_status()
|
|
9
|
+
client.get_info()
|
|
10
|
+
|
|
11
|
+
# 2. Mock Data
|
|
12
|
+
planning_parameters = []
|
|
13
|
+
planning_parameters.append(PlanningParameter("Param One", 0, 100, [ParameterHistoryItem(123, 128)], AresDataType.NUMBER, False, False, "Random Planner"))
|
|
14
|
+
planning_parameters.append(PlanningParameter("Param Two", 0, 500, [ParameterHistoryItem(132, 122)], AresDataType.NUMBER, False, False, "Random Planner"))
|
|
15
|
+
planning_parameters.append(PlanningParameter("Param Three", 0, 500, [ParameterHistoryItem(141, 121)], AresDataType.NUMBER, False, False, "Random Planner"))
|
|
16
|
+
setting_dict = {"Random Setting" : 4000}
|
|
17
|
+
analysis_results = [10, 20, 30]
|
|
18
|
+
|
|
19
|
+
# 3. Create and Send the Request
|
|
20
|
+
request = PlanRequest(planning_parameters, setting_dict, analysis_results)
|
|
21
|
+
response = client.run_planning(request)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from PyAres import AresPlannerService, PlanRequest, PlanResponse, AresDataType
|
|
2
|
+
import random
|
|
3
|
+
|
|
4
|
+
def generate_plan(request: PlanRequest) -> PlanResponse:
|
|
5
|
+
planned_values = []
|
|
6
|
+
names = []
|
|
7
|
+
|
|
8
|
+
# Iterate through every parameter configured in the ARES Experiment
|
|
9
|
+
for param in request.parameters:
|
|
10
|
+
# Simple Logic: Pick a random value within the allowed range
|
|
11
|
+
val = random.uniform(param.minimum_value, param.maximum_value)
|
|
12
|
+
|
|
13
|
+
names.append(param.name)
|
|
14
|
+
planned_values.append(val)
|
|
15
|
+
|
|
16
|
+
return PlanResponse(parameter_names=names, parameter_values=planned_values)
|
|
17
|
+
|
|
18
|
+
if __name__ == "__main__":
|
|
19
|
+
service = AresPlannerService(
|
|
20
|
+
generate_plan,
|
|
21
|
+
"Random Search Planner",
|
|
22
|
+
"This planner picks random values within bounds.",
|
|
23
|
+
"1.0.0"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Tell ARES we can plan for Numeric values
|
|
27
|
+
service.add_supported_type(AresDataType.NUMBER)
|
|
28
|
+
|
|
29
|
+
service.start()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from PyAres import AresDeviceService, AresDataType, DeviceSchemaEntry, DeviceCommandDescriptor
|
|
2
|
+
|
|
3
|
+
# 1. Define your hardware logic
|
|
4
|
+
def set_speed(rpm: float):
|
|
5
|
+
print(f"Setting motor speed to {rpm}")
|
|
6
|
+
# Hardware communication goes here...
|
|
7
|
+
return {} # Return empty dict if no data needs to be sent back
|
|
8
|
+
|
|
9
|
+
def get_status():
|
|
10
|
+
# Return a dictionary matching your state schema
|
|
11
|
+
return { "rpm": 1200 }
|
|
12
|
+
|
|
13
|
+
def safe_mode():
|
|
14
|
+
print("Stopping motor immediately!")
|
|
15
|
+
|
|
16
|
+
# 2. Initialize Service
|
|
17
|
+
service = AresDeviceService(
|
|
18
|
+
safe_mode,
|
|
19
|
+
get_status,
|
|
20
|
+
"Rotary Mixer",
|
|
21
|
+
"High-speed mixer control",
|
|
22
|
+
"1.0.0",
|
|
23
|
+
port=7101
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# 3. Define the 'Set Speed' Command
|
|
27
|
+
# Input: One number (Speed)
|
|
28
|
+
input_schema = {
|
|
29
|
+
"rpm": DeviceSchemaEntry(AresDataType.NUMBER, "Speed in RPM", "RPM")
|
|
30
|
+
}
|
|
31
|
+
cmd_descriptor = DeviceCommandDescriptor("Set Speed", "Sets mixer speed", input_schema, {})
|
|
32
|
+
|
|
33
|
+
# 4. Register the command
|
|
34
|
+
service.add_new_command(cmd_descriptor, set_speed)
|
|
35
|
+
|
|
36
|
+
# 5. Start
|
|
37
|
+
service.start()
|
|
@@ -23,8 +23,8 @@ from ..Utils import ares_data_type_utils
|
|
|
23
23
|
|
|
24
24
|
# Type hint for the user's custom methods
|
|
25
25
|
EnterSafeModeMethod = Callable[[], None]
|
|
26
|
-
DeviceCommandMethod = Callable[..., Dict[str,
|
|
27
|
-
DeviceStateMethod = Callable[[], Dict[str,
|
|
26
|
+
DeviceCommandMethod = Callable[..., Dict[str, Any]]
|
|
27
|
+
DeviceStateMethod = Callable[[], Dict[str, Any]]
|
|
28
28
|
|
|
29
29
|
class AresDeviceServiceWrapper(device_service_grpc.AresRemoteDeviceServiceServicer):
|
|
30
30
|
"""
|
|
@@ -211,7 +211,14 @@ class AresDeviceServiceWrapper(device_service_grpc.AresRemoteDeviceServiceServic
|
|
|
211
211
|
|
|
212
212
|
class AresDeviceService:
|
|
213
213
|
""" Manages the gRPC service for the AresDeviceSerivce """
|
|
214
|
-
def __init__(self,
|
|
214
|
+
def __init__(self,
|
|
215
|
+
enter_safe_mode_logic: EnterSafeModeMethod,
|
|
216
|
+
get_device_state_logic: DeviceStateMethod,
|
|
217
|
+
device_name: str,
|
|
218
|
+
description: str,
|
|
219
|
+
version: str,
|
|
220
|
+
use_localhost: bool = True,
|
|
221
|
+
port: int = 7100):
|
|
215
222
|
"""
|
|
216
223
|
Initializes the AresDeviceService
|
|
217
224
|
|
|
@@ -273,11 +280,21 @@ class AresDeviceService:
|
|
|
273
280
|
new_ares_value = ares_value_utils.create_ares_value(setting_value)
|
|
274
281
|
self._service_wrapper._current_settings[setting_name] = new_ares_value
|
|
275
282
|
|
|
276
|
-
def start(self):
|
|
277
|
-
"""
|
|
283
|
+
def start(self, wait_for_termination: bool = True):
|
|
284
|
+
"""
|
|
285
|
+
Starts the service on the specified port, and waits for termination.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
wait_for_termination (bool): A boolean value that determines whether the start method will use the "wait_for_termination" blocking call.
|
|
289
|
+
If true, the gRPC service will keep the main thread alive but at the cost of blocking any continued execution of your python logic.
|
|
290
|
+
Setting this value to false will allow you to continue execution after starting your service, however this should ONLY be done if you have
|
|
291
|
+
another mechanism for keeping your process alive (such as a GUI, or a loop). Defaults to true.
|
|
292
|
+
"""
|
|
278
293
|
print(f"Starting Ares Device Service on port {self._port}...")
|
|
279
294
|
self._server.start()
|
|
280
|
-
|
|
295
|
+
|
|
296
|
+
if wait_for_termination:
|
|
297
|
+
self._server.wait_for_termination()
|
|
281
298
|
|
|
282
299
|
def stop(self):
|
|
283
300
|
""" Stops the service, terminating the connection. """
|
|
@@ -24,4 +24,10 @@ class RequestMetadata():
|
|
|
24
24
|
self.system_name = proto_metadata.system_name
|
|
25
25
|
self.campaign_name = proto_metadata.campaign_name
|
|
26
26
|
self.campaign_id = proto_metadata.campaign_id
|
|
27
|
-
self.experiment_id = proto_metadata.experiment_id
|
|
27
|
+
self.experiment_id = proto_metadata.experiment_id
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_default_values(cls):
|
|
31
|
+
""" Alternative constructor for creating fake metadata """
|
|
32
|
+
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)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Dict, Any, List
|
|
1
|
+
from typing import Dict, Any, List, Sequence
|
|
2
2
|
from ..Models import Outcome, AresDataType, RequestMetadata
|
|
3
3
|
|
|
4
4
|
class ParameterHistoryItem:
|
|
@@ -38,23 +38,41 @@ class PlanningParameter:
|
|
|
38
38
|
planner_name: The name of the planner ARES requested be used to plan for this parameter.
|
|
39
39
|
initial_value: An optional initial value for the given parameter
|
|
40
40
|
"""
|
|
41
|
-
self.name
|
|
42
|
-
self.minimum_value
|
|
43
|
-
self.maximum_value
|
|
44
|
-
self.param_history
|
|
45
|
-
self.data_type
|
|
46
|
-
self.is_planned
|
|
47
|
-
self.is_result
|
|
48
|
-
self.planner_name
|
|
41
|
+
self.name: str = name
|
|
42
|
+
self.minimum_value: float = minimum_value
|
|
43
|
+
self.maximum_value: float = maximum_value
|
|
44
|
+
self.param_history: List = param_history
|
|
45
|
+
self.data_type: AresDataType = data_type
|
|
46
|
+
self.is_planned: bool = is_planned
|
|
47
|
+
self.is_result: bool = is_result
|
|
48
|
+
self.planner_name: str = planner_name
|
|
49
49
|
self.initial_value = initial_value
|
|
50
50
|
|
|
51
|
+
|
|
52
|
+
class ParamHistoryInfo:
|
|
53
|
+
"""
|
|
54
|
+
Represents the history of a given parameter.
|
|
55
|
+
|
|
56
|
+
Designed to provide a more user-friendly abstraction for interacting with a param history object.
|
|
57
|
+
"""
|
|
58
|
+
def __init__(self, planned_value: Any, achieved_value: Any):
|
|
59
|
+
"""
|
|
60
|
+
Initializes a ParamHistoryInfo.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
planned_value (Any): The value given directly from the planner.
|
|
64
|
+
achieved_value (Any): An optional value that represents the real world achieved value, which may differ from the planners target value.
|
|
65
|
+
"""
|
|
66
|
+
self.planned_value = planned_value
|
|
67
|
+
self.achieved_value = achieved_value
|
|
68
|
+
|
|
51
69
|
class PlanRequest:
|
|
52
70
|
"""
|
|
53
71
|
Represents a PlanRequest message received from ARES.
|
|
54
72
|
|
|
55
73
|
Designed to provide a more user-friendly abstraction for interacting with a plan request message.
|
|
56
74
|
"""
|
|
57
|
-
def __init__(self, parameters: list[PlanningParameter], settings: Dict[str, Any], analysis_results:
|
|
75
|
+
def __init__(self, parameters: list[PlanningParameter], settings: Dict[str, Any], analysis_results: Sequence[float], metadata: RequestMetadata = RequestMetadata.from_default_values()):
|
|
58
76
|
"""
|
|
59
77
|
Initializes a PlanRequest.
|
|
60
78
|
|
|
@@ -166,19 +166,27 @@ class AresPlannerService:
|
|
|
166
166
|
"""
|
|
167
167
|
Manages the gRPC server for the AresPlannerService
|
|
168
168
|
"""
|
|
169
|
-
def __init__(self, custom_plan_logic: PlanLogicFunction,
|
|
169
|
+
def __init__(self, custom_plan_logic: PlanLogicFunction,
|
|
170
|
+
service_name: str,
|
|
171
|
+
service_description: str,
|
|
172
|
+
service_version: str,
|
|
173
|
+
timeout: int = 30,
|
|
174
|
+
use_localhost: bool = True,
|
|
175
|
+
port: int = 7082,
|
|
176
|
+
max_message_size: int = -1):
|
|
170
177
|
"""
|
|
171
178
|
Initializes the AresPlannerService
|
|
172
179
|
|
|
173
180
|
Args:
|
|
174
|
-
custom_plan_logic: A callable function that will be executed when a PlanRequest is received.
|
|
181
|
+
custom_plan_logic (PlanLogicFunction): A callable function that will be executed when a PlanRequest is received.
|
|
175
182
|
This function should accept a 'PyARES.AresPlanning.PlanRequest' object and return a
|
|
176
183
|
'PyARES.AresPlanning.PlanResponse' object (or an awaitable that resolves to one).
|
|
177
|
-
service_name: The name descriptor that is associated with your planner service.
|
|
178
|
-
service_description: A brief description describing your implementation of the planner service.
|
|
179
|
-
service_version: The version of your planner service.
|
|
180
|
-
use_localhost: An optional value that allows the user to specify whether to host the service on the local network. Defaults to True.
|
|
181
|
-
port: The port that your planner service will serve on. Defaults to port 7082.
|
|
184
|
+
service_name (str): The name descriptor that is associated with your planner service.
|
|
185
|
+
service_description (str): A brief description describing your implementation of the planner service.
|
|
186
|
+
service_version (str): The version of your planner service.
|
|
187
|
+
use_localhost (bool): An optional value that allows the user to specify whether to host the service on the local network. Defaults to True.
|
|
188
|
+
port (int): The port that your planner service will serve on. Defaults to port 7082.
|
|
189
|
+
max_message_size (int): The max size, in megabytes, of the messages your Planning service is capable of sending. Increasing this can help transfer heavy data like images, but may result in some loss in performance
|
|
182
190
|
"""
|
|
183
191
|
#Public Values, designed to be accessible to the user
|
|
184
192
|
self.service_name = service_name
|
|
@@ -187,7 +195,11 @@ class AresPlannerService:
|
|
|
187
195
|
|
|
188
196
|
#Private values, mostly related to the service
|
|
189
197
|
self._port = port
|
|
190
|
-
|
|
198
|
+
server_options = []
|
|
199
|
+
if max_message_size != -1:
|
|
200
|
+
print("Setting Custom Max Message Size")
|
|
201
|
+
server_options.append(('grpc.max_receive_message_length', max_message_size * 1024 * 1024))
|
|
202
|
+
self._server = grpc.server(futures.ThreadPoolExecutor(max_workers=10), options=server_options)
|
|
191
203
|
self._service_wrapper = AresPlannerServiceWrapper(service_name, service_description, service_version, timeout, custom_plan_logic)
|
|
192
204
|
planner_service_grpc.add_AresRemotePlannerServiceServicer_to_server(self._service_wrapper, self._server)
|
|
193
205
|
if(use_localhost):
|
|
@@ -236,13 +248,21 @@ class AresPlannerService:
|
|
|
236
248
|
"""
|
|
237
249
|
self._service_wrapper._timeout = new_timeout
|
|
238
250
|
|
|
239
|
-
def start(self):
|
|
240
|
-
"""
|
|
241
|
-
Starts the service on the specified port, and waits for termination.
|
|
251
|
+
def start(self, wait_for_termination: bool = True):
|
|
252
|
+
"""
|
|
253
|
+
Starts the service on the specified port, and waits for termination.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
wait_for_termination (bool): A boolean value that determines whether the start method will use the "wait_for_termination" blocking call.
|
|
257
|
+
If true, the gRPC service will keep the main thread alive but at the cost of blocking any continued execution of your python logic.
|
|
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
|
+
another mechanism for keeping your process alive (such as a GUI, or a loop). Defaults to true.
|
|
242
260
|
"""
|
|
243
|
-
print(f"Starting Ares
|
|
261
|
+
print(f"Starting Ares Device Service on port {self._port}...")
|
|
244
262
|
self._server.start()
|
|
245
|
-
|
|
263
|
+
|
|
264
|
+
if wait_for_termination:
|
|
265
|
+
self._server.wait_for_termination()
|
|
246
266
|
|
|
247
267
|
def stop(self):
|
|
248
268
|
"""
|
|
@@ -1,18 +1,6 @@
|
|
|
1
|
-
from typing import Union
|
|
2
|
-
|
|
3
|
-
#Datamodel Imports
|
|
1
|
+
from typing import Union
|
|
4
2
|
from ares_datamodel import ares_data_schema_pb2
|
|
5
|
-
|
|
6
3
|
from ..Models import ares_data_models
|
|
7
|
-
# def ares_schema_to_dict(schema: ares_data_schema_pb2.AresDataSchema) -> dict:
|
|
8
|
-
# """Converts an AresDataSchemaSimplified to a dictionary for user logic."""
|
|
9
|
-
# result = {}
|
|
10
|
-
# for key, entry in schema.fields.items():
|
|
11
|
-
# result[key] = {
|
|
12
|
-
# "type": ares_data_schema_pb2.AresDataType.Name(entry.type),
|
|
13
|
-
# "is_array": entry.is_array
|
|
14
|
-
# }
|
|
15
|
-
# return result
|
|
16
4
|
|
|
17
5
|
def create_settings_schema_entry(
|
|
18
6
|
setting_type: ares_data_models.AresDataType,
|
|
@@ -40,5 +28,9 @@ def create_settings_schema_entry(
|
|
|
40
28
|
schema_entry = ares_data_schema_pb2.SchemaEntry(type=setting_type.value, optional=optional)
|
|
41
29
|
schema_entry.number_choices.numbers.extend(choices)
|
|
42
30
|
|
|
31
|
+
else:
|
|
32
|
+
schema_entry = ares_data_schema_pb2.SchemaEntry(type=setting_type.value, optional=optional)
|
|
33
|
+
|
|
34
|
+
|
|
43
35
|
return schema_entry
|
|
44
36
|
|
|
@@ -4,7 +4,7 @@ from ..Models import AresDataType
|
|
|
4
4
|
|
|
5
5
|
def python_ares_type_to_proto_ares_type(py_value: AresDataType) -> ares_data_type_pb2.AresDataType:
|
|
6
6
|
""" A method to convert from the python AresDataType class to the protobuf version """
|
|
7
|
-
return py_value.value
|
|
7
|
+
return ares_data_type_pb2.AresDataType(py_value.value)
|
|
8
8
|
|
|
9
9
|
def proto_ares_type_to_python_ares_type(proto_value: ares_data_type_pb2.AresDataType) -> AresDataType:
|
|
10
10
|
""" A method to convert from the protobuf AresDataType class to the python version """
|
|
@@ -28,7 +28,7 @@ def determine_python_ares_data_type(value: Union[int, float, str, bool, list]):
|
|
|
28
28
|
return AresDataType.STRING_ARRAY
|
|
29
29
|
elif(all(isinstance(x, (int, float)) for x in value)):
|
|
30
30
|
return AresDataType.NUMBER_ARRAY
|
|
31
|
-
elif(all(isinstance(x, bool)
|
|
31
|
+
elif(all(isinstance(x, bool) for x in value)):
|
|
32
32
|
return AresDataType.BOOL_ARRAY
|
|
33
33
|
else:
|
|
34
34
|
return AresDataType.UNKNOWN
|
|
@@ -3,7 +3,7 @@ from ares_datamodel import ares_outcome_enum_pb2
|
|
|
3
3
|
|
|
4
4
|
def python_ares_outcome_to_proto_ares_outcome(py_value: Outcome) -> ares_outcome_enum_pb2.Outcome:
|
|
5
5
|
""" A method to convert from the python AresDataType class to the protobuf version """
|
|
6
|
-
return py_value.value
|
|
6
|
+
return ares_outcome_enum_pb2.Outcome(py_value.value)
|
|
7
7
|
|
|
8
8
|
def proto_ares_outcome_to_python_ares_outcome(proto_value: ares_outcome_enum_pb2.Outcome) -> Outcome:
|
|
9
9
|
""" A method to convert from the protobuf AresDataType class to the python version """
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
from ares_datamodel import ares_struct_pb2
|
|
2
|
-
from typing import Union, Dict
|
|
2
|
+
from typing import Union, Dict, Any
|
|
3
3
|
|
|
4
4
|
from . import ares_value_utils
|
|
5
5
|
import copy
|
|
6
6
|
|
|
7
|
-
def ares_struct_to_dict(ares_struct: ares_struct_pb2.AresStruct) -> Dict[str,
|
|
7
|
+
def ares_struct_to_dict(ares_struct: ares_struct_pb2.AresStruct) -> Dict[str, Any]:
|
|
8
8
|
"""Converts an AresStruct protobuf message to a Python dictionary."""
|
|
9
9
|
return {k: ares_value_utils.ares_value_to_py(v) for k, v in ares_struct.fields.items()}
|
|
10
10
|
|
|
@@ -62,6 +62,9 @@ def create_bool_struct(key: str, value: bool) -> ares_struct_pb2.AresStruct:
|
|
|
62
62
|
Returns:
|
|
63
63
|
(AresStruct): A new AresStruct containing the provided key and value.
|
|
64
64
|
"""
|
|
65
|
+
new_struct = ares_struct_pb2.AresStruct()
|
|
66
|
+
new_struct.fields[key] = ares_value_utils.create_bool(value)
|
|
67
|
+
return new_struct
|
|
65
68
|
|
|
66
69
|
def create_null_struct(key: str) -> ares_struct_pb2.AresStruct:
|
|
67
70
|
"""
|
|
@@ -168,7 +171,16 @@ def copy_struct(existing_struct: ares_struct_pb2.AresStruct) -> ares_struct_pb2.
|
|
|
168
171
|
"""
|
|
169
172
|
return copy.deepcopy(existing_struct)
|
|
170
173
|
|
|
171
|
-
def
|
|
174
|
+
def create_empty_struct() -> ares_struct_pb2.AresStruct:
|
|
175
|
+
"""
|
|
176
|
+
Creates a new empty AresStruct
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
(AresStruct): The newly created empty AresStruct.
|
|
180
|
+
"""
|
|
181
|
+
return ares_struct_pb2.AresStruct()
|
|
182
|
+
|
|
183
|
+
def create_ares_struct(key: str, value: Any):
|
|
172
184
|
"""
|
|
173
185
|
Creates a new AresStruct using the provided key and value.
|
|
174
186
|
|
|
@@ -181,28 +193,32 @@ def create_ares_struct(key: str, value: any):
|
|
|
181
193
|
"""
|
|
182
194
|
|
|
183
195
|
if(isinstance(value, str)):
|
|
184
|
-
return create_string_struct(value)
|
|
196
|
+
return create_string_struct(key, value)
|
|
185
197
|
|
|
186
198
|
elif(isinstance(value, (int, float))):
|
|
187
|
-
return create_number_struct(value)
|
|
199
|
+
return create_number_struct(key, value)
|
|
188
200
|
|
|
189
201
|
elif(isinstance(value, bool)):
|
|
190
|
-
return create_bool_struct(value)
|
|
191
|
-
|
|
192
|
-
elif(isinstance(value, (list[int], list[float]))):
|
|
193
|
-
return create_number_array_struct(value)
|
|
202
|
+
return create_bool_struct(key, value)
|
|
194
203
|
|
|
195
204
|
elif(isinstance(value, bytes)):
|
|
196
|
-
return create_bytes_array_struct(value)
|
|
197
|
-
|
|
198
|
-
elif(isinstance(value, list[str])):
|
|
199
|
-
return create_string_array_struct(value)
|
|
205
|
+
return create_bytes_array_struct(key, value)
|
|
200
206
|
|
|
201
|
-
elif(isinstance(value, list
|
|
202
|
-
|
|
207
|
+
elif(isinstance(value, list)):
|
|
208
|
+
if(len(value) == 0):
|
|
209
|
+
return create_null_struct(key)
|
|
210
|
+
|
|
211
|
+
elif(all(isinstance(item, str) for item in value)):
|
|
212
|
+
return create_string_array_struct(key, value)
|
|
213
|
+
|
|
214
|
+
elif(all(isinstance(item, (float, int)) for item in value)):
|
|
215
|
+
return create_number_array_struct(key, value)
|
|
216
|
+
|
|
217
|
+
elif(all(isinstance(item, bool) for item in value)):
|
|
218
|
+
return create_bool_array_struct(key, value)
|
|
203
219
|
|
|
204
220
|
else:
|
|
205
|
-
return create_null_struct()
|
|
221
|
+
return create_null_struct(key)
|
|
206
222
|
|
|
207
223
|
|
|
208
224
|
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
from ares_datamodel import ares_struct_pb2
|
|
2
2
|
from ares_datamodel import ares_data_type_pb2
|
|
3
|
-
from typing import Union
|
|
3
|
+
from typing import Union, Any
|
|
4
4
|
|
|
5
|
-
from . import ares_struct_utils
|
|
6
5
|
from . import ares_data_type_utils
|
|
7
6
|
from ..Models import AresDataType
|
|
8
7
|
|
|
@@ -33,14 +32,13 @@ def py_to_ares_value(py_value, ares_value: ares_struct_pb2.AresValue):
|
|
|
33
32
|
ares_value.number_value = py_value
|
|
34
33
|
elif isinstance(py_value, bytes):
|
|
35
34
|
ares_value.bytes_value = py_value
|
|
36
|
-
elif isinstance(py_value, dict):
|
|
37
|
-
ares_struct_utils.dict_to_ares_struct(py_value, ares_value.struct_value)
|
|
38
35
|
elif isinstance(py_value, list):
|
|
39
|
-
for
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
36
|
+
if(all(isinstance(x, str) for x in py_value)):
|
|
37
|
+
ares_value.string_array_value.strings.extend(py_value)
|
|
38
|
+
elif(all(isinstance(x, (int, float)) for x in py_value)):
|
|
39
|
+
ares_value.number_array_value.numbers.extend(py_value)
|
|
40
|
+
elif(all(isinstance(x, bool) for x in py_value)):
|
|
41
|
+
ares_value.bool_array_value.bools.extend(py_value)
|
|
44
42
|
else:
|
|
45
43
|
raise TypeError(f"Unsupported type for AresValue: {type(py_value)}")
|
|
46
44
|
|
|
@@ -137,7 +135,9 @@ def create_bool_array(value: list[bool]) -> ares_struct_pb2.AresValue:
|
|
|
137
135
|
Returns:
|
|
138
136
|
(AresValue): A new AresValue containing the provided list of booleans.
|
|
139
137
|
"""
|
|
140
|
-
|
|
138
|
+
ares_value = ares_struct_pb2.AresValue()
|
|
139
|
+
ares_value.bool_array_value.bools.extend(value)
|
|
140
|
+
return ares_value
|
|
141
141
|
|
|
142
142
|
def create_default(python_datatype: AresDataType) -> ares_struct_pb2.AresValue:
|
|
143
143
|
"""
|
|
@@ -177,7 +177,7 @@ def create_default(python_datatype: AresDataType) -> ares_struct_pb2.AresValue:
|
|
|
177
177
|
else:
|
|
178
178
|
return create_null()
|
|
179
179
|
|
|
180
|
-
def create_ares_value(value:
|
|
180
|
+
def create_ares_value(value: Any) -> ares_struct_pb2.AresValue:
|
|
181
181
|
"""
|
|
182
182
|
Creates a new AresValue using the provided value.
|
|
183
183
|
If the provided value is not valid for use in an AresValue, a null AresValue is returned.
|
|
@@ -198,7 +198,7 @@ def create_ares_value(value: any) -> ares_struct_pb2.AresValue:
|
|
|
198
198
|
return create_bool(value)
|
|
199
199
|
|
|
200
200
|
elif(isinstance(value, bytes)):
|
|
201
|
-
return
|
|
201
|
+
return create_bytes(value)
|
|
202
202
|
|
|
203
203
|
elif(isinstance(value, list)):
|
|
204
204
|
if all(isinstance(x, (int, float)) for x in value):
|
|
@@ -209,6 +209,9 @@ def create_ares_value(value: any) -> ares_struct_pb2.AresValue:
|
|
|
209
209
|
|
|
210
210
|
elif all(isinstance(x, bool) for x in value):
|
|
211
211
|
return create_bool_array(value)
|
|
212
|
-
|
|
212
|
+
|
|
213
|
+
else:
|
|
214
|
+
return create_null()
|
|
215
|
+
|
|
213
216
|
else:
|
|
214
217
|
return create_null()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from ..Planning import ParameterHistoryItem
|
|
2
|
+
from ares_datamodel.planning import plan_pb2
|
|
3
|
+
from . import ares_value_utils
|
|
4
|
+
|
|
5
|
+
def convert_proto_param_history_to_python(proto: plan_pb2.ParameterHistoryInfo) -> ParameterHistoryItem:
|
|
6
|
+
return ParameterHistoryItem(proto.planned_value, proto.achieved_value)
|
|
7
|
+
|
|
8
|
+
def convert_python_param_history_to_proto(python: ParameterHistoryItem) -> plan_pb2.ParameterHistoryInfo:
|
|
9
|
+
info = plan_pb2.ParameterHistoryInfo(planned_value=ares_value_utils.create_ares_value(python.planned_value), achieved_value=ares_value_utils.create_ares_value(python.achieved_value))
|
|
10
|
+
return info
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from ares_datamodel.planning import plan_pb2
|
|
2
|
+
from ..Planning import PlanRequest
|
|
3
|
+
from . import planning_param_utils
|
|
4
|
+
from . import ares_struct_utils
|
|
5
|
+
|
|
6
|
+
def python_plan_request_to_proto(request: PlanRequest) -> plan_pb2.PlanningRequest:
|
|
7
|
+
proto_request = plan_pb2.PlanningRequest()
|
|
8
|
+
proto_request.analysis_results.extend(request.analysis_results)
|
|
9
|
+
ares_struct_utils.dict_to_ares_struct(request.settings, proto_request.adapter_settings)
|
|
10
|
+
proto_request.planning_parameters.extend([planning_param_utils.convert_python_plan_param_to_proto(p) for p in request.parameters])
|
|
11
|
+
return proto_request
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from ares_datamodel.planning import plan_pb2
|
|
2
|
+
from ..Planning import PlanResponse
|
|
3
|
+
from . import ares_outcome_utils
|
|
4
|
+
|
|
5
|
+
def proto_plan_response_to_python(proto_response: plan_pb2.PlanningResponse) -> PlanResponse:
|
|
6
|
+
param_names = []
|
|
7
|
+
param_values = []
|
|
8
|
+
|
|
9
|
+
for param in proto_response.planned_parameters:
|
|
10
|
+
param_names.append(param.parameter_name)
|
|
11
|
+
param_values.append(param.parameter_value)
|
|
12
|
+
|
|
13
|
+
python_response = PlanResponse(param_names, param_values)
|
|
14
|
+
python_response.outcome = ares_outcome_utils.proto_ares_outcome_to_python_ares_outcome(proto_response.planning_outcome)
|
|
15
|
+
python_response.error_string = proto_response.error_string
|
|
16
|
+
return python_response
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from ..Planning import PlanningParameter, PlanResponse
|
|
2
|
+
from ares_datamodel.planning import plan_pb2
|
|
3
|
+
from typing import List
|
|
4
|
+
from . import param_history_info_utils
|
|
5
|
+
from . import ares_data_type_utils
|
|
6
|
+
from . import ares_value_utils
|
|
7
|
+
|
|
8
|
+
def convert_python_plan_param_to_proto(param: PlanningParameter) -> plan_pb2.PlanningParameter:
|
|
9
|
+
new_proto_param = plan_pb2.PlanningParameter()
|
|
10
|
+
new_proto_param.parameter_name = param.name
|
|
11
|
+
new_proto_param.minimum_value = param.minimum_value
|
|
12
|
+
new_proto_param.maximum_value = param.maximum_value
|
|
13
|
+
new_proto_param.parameter_history.extend([param_history_info_utils.convert_python_param_history_to_proto(p) for p in param.param_history])
|
|
14
|
+
new_proto_param.data_type = ares_data_type_utils.python_ares_type_to_proto_ares_type(param.data_type)
|
|
15
|
+
new_proto_param.is_planned = param.is_planned
|
|
16
|
+
new_proto_param.is_result = param.is_result
|
|
17
|
+
new_proto_param.planner_name = param.planner_name
|
|
18
|
+
ares_value_utils.py_to_ares_value(param.initial_value, new_proto_param.initial_value)
|
|
19
|
+
|
|
20
|
+
return new_proto_param
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import grpc
|
|
2
|
+
import time
|
|
3
|
+
from typing import Dict
|
|
4
|
+
|
|
5
|
+
from ..Utils import ares_struct_utils
|
|
6
|
+
from ares_datamodel.analyzing.remote import ares_remote_analyzer_service_pb2_grpc as analyzer_service_grpc
|
|
7
|
+
from ares_datamodel.analyzing.remote import ares_remote_analyzer_service_pb2 as analyzer_service
|
|
8
|
+
from ares_datamodel import ares_analyzer_management_service_pb2
|
|
9
|
+
from ares_datamodel.connection import connection_status_pb2
|
|
10
|
+
from ares_datamodel.analyzing import analysis_pb2
|
|
11
|
+
from ares_datamodel import ares_outcome_enum_pb2
|
|
12
|
+
|
|
13
|
+
class AnalyzerTestClient:
|
|
14
|
+
def __init__(self, port=7083, host='localhost'):
|
|
15
|
+
"""
|
|
16
|
+
Initializes an AnalyzerTestClient, useful for testing your PyAres Analyzers.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
port (int): The port that your analyzer test client will use to try and reach your service. Defaults to 7083.
|
|
20
|
+
host (str): The host name that the client will use to try and reach your service. Defaults to localhost.
|
|
21
|
+
"""
|
|
22
|
+
self.target = f"{host}:{port}"
|
|
23
|
+
self.channel = grpc.insecure_channel(self.target)
|
|
24
|
+
self.stub = analyzer_service_grpc.AresRemoteAnalyzerServiceStub(self.channel)
|
|
25
|
+
print(f"Client connected to {self.target}")
|
|
26
|
+
|
|
27
|
+
def get_info(self):
|
|
28
|
+
"""Used to test that your Analyzer Service is providing info properly. If successful outputs the name, version and description of your service."""
|
|
29
|
+
print("\n--- Testing GetInfo ---")
|
|
30
|
+
try:
|
|
31
|
+
request = ares_analyzer_management_service_pb2.AnalyzerInfoRequest()
|
|
32
|
+
response = self.stub.GetInfo(request)
|
|
33
|
+
print(f"Success! Connected to: {response.name} (version {response.version})")
|
|
34
|
+
print(f"Description: {response.description}")
|
|
35
|
+
|
|
36
|
+
except grpc.RpcError as e:
|
|
37
|
+
print(f"RPC Failed: {e.code()} - {e.details()}")
|
|
38
|
+
|
|
39
|
+
def check_status(self):
|
|
40
|
+
"""Checks the status of your Analyzer Service"""
|
|
41
|
+
print("\n--- Testing Connection Status ---")
|
|
42
|
+
try:
|
|
43
|
+
request = connection_status_pb2.ConnectionStatusRequest()
|
|
44
|
+
response: connection_status_pb2.ConnectionStatus = self.stub.GetConnectionStatus(request)
|
|
45
|
+
print(f"Status: {response.status}")
|
|
46
|
+
|
|
47
|
+
except grpc.RpcError as e:
|
|
48
|
+
print(f"RPC Failed: {e.code()} - {e.details()}")
|
|
49
|
+
|
|
50
|
+
def run_analysis(self, inputs: Dict, settings: Dict = { }):
|
|
51
|
+
"""
|
|
52
|
+
This method is responsible for testing your analysis loop. Simply provide whatever values your analyzer needs to test your logic.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
inputs (Dict): The inputs required by your analyzer. Ensure the names (keys) match exactly as your analyzer expects, otherwise analysis will fail
|
|
56
|
+
settings (Dict): A dictionary that contains any specific settings you want to pass to your analyzer. Defaults to an empty dictionary.
|
|
57
|
+
"""
|
|
58
|
+
print(f"\n--- Testing Analysis ---")
|
|
59
|
+
if settings is None:
|
|
60
|
+
settings = {}
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
input_struct = ares_struct_utils.create_empty_struct()
|
|
64
|
+
ares_struct_utils.dict_to_ares_struct(inputs, input_struct)
|
|
65
|
+
|
|
66
|
+
settings_struct = ares_struct_utils.create_empty_struct()
|
|
67
|
+
ares_struct_utils.dict_to_ares_struct(settings, input_struct)
|
|
68
|
+
|
|
69
|
+
request = analyzer_service.AnalysisRequest(inputs=input_struct, settings=settings_struct)
|
|
70
|
+
|
|
71
|
+
start_time = time.perf_counter()
|
|
72
|
+
response: analysis_pb2.Analysis = self.stub.Analyze(request)
|
|
73
|
+
end_time = time.perf_counter()
|
|
74
|
+
|
|
75
|
+
if response.analysis_outcome == ares_outcome_enum_pb2.SUCCESS:
|
|
76
|
+
print(f"Analysis Successful!")
|
|
77
|
+
print(f"Result: {response.result}")
|
|
78
|
+
|
|
79
|
+
elif response.analysis_outcome == ares_outcome_enum_pb2.WARNING:
|
|
80
|
+
print("Analysis was successful, but analyzer emitted a warning.")
|
|
81
|
+
print(f"Result: {response.result}")
|
|
82
|
+
print(f"#### WARNING: {response.error_string} #####")
|
|
83
|
+
|
|
84
|
+
else:
|
|
85
|
+
print(f"Analysis Failed (Outcome: {response.analysis_outcome})")
|
|
86
|
+
print(f"Error: {response.error_string}")
|
|
87
|
+
|
|
88
|
+
elapsed_time = end_time - start_time
|
|
89
|
+
print(f"Analysis request was completed in: {elapsed_time:.4f} seconds")
|
|
90
|
+
|
|
91
|
+
except grpc.RpcError as e:
|
|
92
|
+
print(f"RPC Failed: {e.code()} - {e.details()}")
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import grpc
|
|
2
|
+
import time
|
|
3
|
+
from ..Utils import plan_request_utils, plan_response_utils
|
|
4
|
+
from ..Planning import PlanRequest, PlanResponse
|
|
5
|
+
from ..Models import Outcome
|
|
6
|
+
from ares_datamodel.planning.remote import ares_remote_planner_service_pb2_grpc as planner_service_grpc
|
|
7
|
+
from ares_datamodel import ares_planner_management_service_pb2
|
|
8
|
+
from ares_datamodel.connection import connection_status_pb2
|
|
9
|
+
from ares_datamodel.planning import plan_pb2
|
|
10
|
+
from ares_datamodel import ares_outcome_enum_pb2
|
|
11
|
+
from ares_datamodel.connection import connection_info_pb2
|
|
12
|
+
|
|
13
|
+
class PlannerTestClient:
|
|
14
|
+
def __init__(self, port=7082, host='localhost'):
|
|
15
|
+
"""
|
|
16
|
+
Initializes an PlannerTestClient, useful for testing your PyAres Planners.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
port (int): The port that your planner test client will use to try and reach your service. Defaults to 7083.
|
|
20
|
+
host (str): The host name that the client will use to try and reach your service. Defaults to localhost.
|
|
21
|
+
"""
|
|
22
|
+
self.target = f"{host}:{port}"
|
|
23
|
+
self.channel = grpc.insecure_channel(self.target)
|
|
24
|
+
self.stub = planner_service_grpc.AresRemotePlannerServiceStub(self.channel)
|
|
25
|
+
print(f"Client connected to {self.target}")
|
|
26
|
+
|
|
27
|
+
def get_info(self):
|
|
28
|
+
"""Used to test that your Planner Service is providing info properly. If successful outputs the name, version and description of your service."""
|
|
29
|
+
print("\n--- Testing GetInfo ---")
|
|
30
|
+
try:
|
|
31
|
+
request = ares_planner_management_service_pb2.PlannerInfoRequest()
|
|
32
|
+
response: connection_info_pb2.InfoResponse = self.stub.GetInfo(request)
|
|
33
|
+
print(f"Success! Connected to: {response.name} (version {response.version})")
|
|
34
|
+
print(f"Description: {response.description}")
|
|
35
|
+
|
|
36
|
+
except grpc.RpcError as e:
|
|
37
|
+
print(f"RPC Failed: {e.code()} - {e.details()}")
|
|
38
|
+
|
|
39
|
+
def check_status(self):
|
|
40
|
+
"""Checks the status of your Planner Service"""
|
|
41
|
+
print("\n--- Testing Connection Status ---")
|
|
42
|
+
try:
|
|
43
|
+
request = connection_status_pb2.ConnectionStatusRequest()
|
|
44
|
+
response: connection_status_pb2.ConnectionStatus = self.stub.GetConnectionStatus(request)
|
|
45
|
+
print(f"Status: {response.status}")
|
|
46
|
+
|
|
47
|
+
except grpc.RpcError as e:
|
|
48
|
+
print(f"RPC Failed: {e.code()} - {e.details()}")
|
|
49
|
+
|
|
50
|
+
def run_planning(self, request: PlanRequest) -> PlanResponse:
|
|
51
|
+
"""
|
|
52
|
+
This method is responsible for testing your planning loop. Simply provide whatever values your planner needs to test your logic.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
request (PlanRequest): The self crafted plan request. This should include all relevant data for your planner.
|
|
56
|
+
"""
|
|
57
|
+
print("\n--- Testing Planning Logic ---")
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
proto_request = plan_request_utils.python_plan_request_to_proto(request)
|
|
61
|
+
|
|
62
|
+
start_time = time.perf_counter()
|
|
63
|
+
response: plan_pb2.PlanningResponse = self.stub.Plan(proto_request)
|
|
64
|
+
end_time = time.perf_counter()
|
|
65
|
+
|
|
66
|
+
if response.planning_outcome == ares_outcome_enum_pb2.SUCCESS:
|
|
67
|
+
print(f"Planning Successful!")
|
|
68
|
+
|
|
69
|
+
elif response.planning_outcome == ares_outcome_enum_pb2.WARNING:
|
|
70
|
+
print("Planning was successful, but planner emitted a warning.")
|
|
71
|
+
print(f"#### WARNING: {response.error_string} #####")
|
|
72
|
+
|
|
73
|
+
else:
|
|
74
|
+
print(f"Planning Failed (Outcome: {response.planning_outcome})")
|
|
75
|
+
print(f"Error: {response.error_string}")
|
|
76
|
+
|
|
77
|
+
elapsed_time = end_time - start_time
|
|
78
|
+
print(f"Planning request was completed in: {elapsed_time:.4f} seconds")
|
|
79
|
+
|
|
80
|
+
return plan_response_utils.proto_plan_response_to_python(response)
|
|
81
|
+
|
|
82
|
+
except grpc.RpcError as e:
|
|
83
|
+
print(f"RPC Failed: {e.code()} - {e.details()}")
|
|
84
|
+
return PlanResponse([], [], error_string="GRPC FAILURE", planning_outcome=Outcome.FAILURE)
|
|
85
|
+
|
|
86
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyAres
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: The official Python library for ARES.
|
|
5
5
|
Author-email: AFRL <AFRL.RX.AREScontact@us.af.mil>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -9,5 +9,5 @@ Requires-Python: >=3.10
|
|
|
9
9
|
License-File: LICENSE
|
|
10
10
|
Requires-Dist: grpcio-tools>=1.75.1
|
|
11
11
|
Requires-Dist: grpcio<2.0.0,>=1.71.0
|
|
12
|
-
Requires-Dist: ares-datamodel>=0.
|
|
12
|
+
Requires-Dist: ares-datamodel>=0.15.1b1
|
|
13
13
|
Dynamic: license-file
|
|
@@ -12,6 +12,15 @@ PyAres.egg-info/top_level.txt
|
|
|
12
12
|
PyAres/Analyzing/__init__.py
|
|
13
13
|
PyAres/Analyzing/analysis_service.py
|
|
14
14
|
PyAres/Analyzing/analyzer_models.py
|
|
15
|
+
PyAres/Demo/analyzer_test.py
|
|
16
|
+
PyAres/Demo/analyzer_test_tools_demo.py
|
|
17
|
+
PyAres/Demo/analyzer_wiki.py
|
|
18
|
+
PyAres/Demo/device_test.py
|
|
19
|
+
PyAres/Demo/hotplate.py
|
|
20
|
+
PyAres/Demo/planner_test.py
|
|
21
|
+
PyAres/Demo/planner_test_tools_demo.py
|
|
22
|
+
PyAres/Demo/planner_wiki.py
|
|
23
|
+
PyAres/Demo/rotary_mixer.py
|
|
15
24
|
PyAres/Device/__init__.py
|
|
16
25
|
PyAres/Device/device_models.py
|
|
17
26
|
PyAres/Device/device_service.py
|
|
@@ -21,12 +30,16 @@ PyAres/Models/ares_data_models.py
|
|
|
21
30
|
PyAres/Planning/__init__.py
|
|
22
31
|
PyAres/Planning/planner_models.py
|
|
23
32
|
PyAres/Planning/planning_service.py
|
|
24
|
-
PyAres/Test/analyzer_test.py
|
|
25
|
-
PyAres/Test/device_test.py
|
|
26
|
-
PyAres/Test/planner_test.py
|
|
27
33
|
PyAres/Utils/ares_data_schema_utils.py
|
|
28
34
|
PyAres/Utils/ares_data_type_utils.py
|
|
29
35
|
PyAres/Utils/ares_device_command_utils.py
|
|
30
36
|
PyAres/Utils/ares_outcome_utils.py
|
|
31
37
|
PyAres/Utils/ares_struct_utils.py
|
|
32
|
-
PyAres/Utils/ares_value_utils.py
|
|
38
|
+
PyAres/Utils/ares_value_utils.py
|
|
39
|
+
PyAres/Utils/param_history_info_utils.py
|
|
40
|
+
PyAres/Utils/plan_request_utils.py
|
|
41
|
+
PyAres/Utils/plan_response_utils.py
|
|
42
|
+
PyAres/Utils/planning_param_utils.py
|
|
43
|
+
PyAres/test_tools/__init__.py
|
|
44
|
+
PyAres/test_tools/analyzer_tester.py
|
|
45
|
+
PyAres/test_tools/planner_tester.py
|
|
@@ -145,4 +145,8 @@ The central component to your PyAres device is your AresDeviceService. This clas
|
|
|
145
145
|
|
|
146
146
|
### 📄 License
|
|
147
147
|
|
|
148
|
-
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))
|
|
148
|
+
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>
|
|
149
|
+
|
|
150
|
+
### CLEARANCE
|
|
151
|
+
Distribution A. Approved for public release: distribution unlimited. AFRL-2025-5332.
|
|
152
|
+
|
|
@@ -2,9 +2,6 @@
|
|
|
2
2
|
requires = [
|
|
3
3
|
"setuptools>=61.0.0",
|
|
4
4
|
"wheel",
|
|
5
|
-
"grpcio",
|
|
6
|
-
"grpcio-tools>=1.74.0",
|
|
7
|
-
"ares-datamodel",
|
|
8
5
|
"googleapis-common-protos>=1.70.0"
|
|
9
6
|
]
|
|
10
7
|
build-backend = "setuptools.build_meta"
|
|
@@ -14,7 +11,7 @@ include = ["PyAres*"]
|
|
|
14
11
|
|
|
15
12
|
[project]
|
|
16
13
|
name = "PyAres"
|
|
17
|
-
version = "0.
|
|
14
|
+
version = "0.2.0"
|
|
18
15
|
authors = [{ name = "AFRL", email = "AFRL.RX.AREScontact@us.af.mil" }]
|
|
19
16
|
description = "The official Python library for ARES."
|
|
20
17
|
license = "MIT"
|
|
@@ -23,7 +20,7 @@ requires-python = ">= 3.10"
|
|
|
23
20
|
dependencies = [
|
|
24
21
|
"grpcio-tools (>=1.75.1)",
|
|
25
22
|
"grpcio (>=1.71.0,<2.0.0)",
|
|
26
|
-
"ares-datamodel (>=0.
|
|
23
|
+
"ares-datamodel (>=0.15.1b1)",
|
|
27
24
|
]
|
|
28
25
|
|
|
29
26
|
[project.urls]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|