PyAres 1.0.1__tar.gz → 1.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.
Files changed (74) hide show
  1. {pyares-1.0.1 → pyares-1.2.0}/.gitignore +1 -0
  2. pyares-1.2.0/PKG-INFO +27 -0
  3. pyares-1.2.0/PyAres/Analyzing/analysis_service.py +147 -0
  4. {pyares-1.0.1 → pyares-1.2.0}/PyAres/Demo/Devices/device_test.py +2 -2
  5. {pyares-1.0.1 → pyares-1.2.0}/PyAres/Demo/Planners/planner_test.py +8 -0
  6. pyares-1.2.0/PyAres/Demo/Planners/planner_wiki.py +33 -0
  7. {pyares-1.0.1 → pyares-1.2.0}/PyAres/Device/device_service.py +37 -21
  8. {pyares-1.0.1 → pyares-1.2.0}/PyAres/Models/__init__.py +3 -2
  9. {pyares-1.0.1 → pyares-1.2.0}/PyAres/Models/ares_data_models.py +6 -0
  10. {pyares-1.0.1 → pyares-1.2.0}/PyAres/Planning/__init__.py +6 -2
  11. {pyares-1.0.1 → pyares-1.2.0}/PyAres/Planning/planner_models.py +86 -12
  12. pyares-1.2.0/PyAres/Planning/planning_service.py +166 -0
  13. pyares-1.2.0/PyAres/Utils/ares_objective_status_utils.py +10 -0
  14. {pyares-1.0.1 → pyares-1.2.0}/PyAres/Utils/ares_outcome_utils.py +2 -4
  15. pyares-1.2.0/PyAres/Utils/ares_plan_status_code_utils.py +9 -0
  16. pyares-1.2.0/PyAres/Utils/ares_service_base.py +137 -0
  17. {pyares-1.0.1 → pyares-1.2.0}/PyAres/Utils/ares_value_utils.py +1 -1
  18. pyares-1.2.0/PyAres/Utils/datamodel_version_utils.py +29 -0
  19. {pyares-1.0.1 → pyares-1.2.0}/PyAres/Utils/device_status_code_utils.py +2 -3
  20. pyares-1.2.0/PyAres/Utils/grpc_base.py +59 -0
  21. pyares-1.2.0/PyAres/Utils/logging_utils.py +17 -0
  22. pyares-1.2.0/PyAres/Utils/metadata_interceptors.py +69 -0
  23. {pyares-1.0.1 → pyares-1.2.0}/PyAres/Utils/plan_response_utils.py +10 -2
  24. pyares-1.2.0/PyAres/Utils/planning_param_utils.py +44 -0
  25. {pyares-1.0.1 → pyares-1.2.0}/PyAres/__init__.py +5 -1
  26. pyares-1.2.0/README.md +13 -0
  27. {pyares-1.0.1 → pyares-1.2.0}/tests/test_analyzer.py +55 -10
  28. pyares-1.2.0/tests/test_analyzer_integration.py +86 -0
  29. pyares-1.2.0/tests/test_batch_planning_regressions.py +64 -0
  30. {pyares-1.0.1 → pyares-1.2.0}/tests/test_planner.py +4 -3
  31. pyares-1.0.1/PKG-INFO +0 -168
  32. pyares-1.0.1/PyAres/Analyzing/analysis_service.py +0 -290
  33. pyares-1.0.1/PyAres/Demo/Planners/planner_wiki.py +0 -29
  34. pyares-1.0.1/PyAres/Planning/planning_service.py +0 -284
  35. pyares-1.0.1/PyAres/Utils/planning_param_utils.py +0 -20
  36. pyares-1.0.1/README.md +0 -154
  37. {pyares-1.0.1 → pyares-1.2.0}/.github/dependabot.yml +0 -0
  38. {pyares-1.0.1 → pyares-1.2.0}/.github/workflows/pypi.yaml +0 -0
  39. {pyares-1.0.1 → pyares-1.2.0}/.github/workflows/python-tests.yaml +0 -0
  40. {pyares-1.0.1 → pyares-1.2.0}/LICENSE +0 -0
  41. {pyares-1.0.1 → pyares-1.2.0}/PyAres/Analyzing/__init__.py +0 -0
  42. {pyares-1.0.1 → pyares-1.2.0}/PyAres/Analyzing/analyzer_models.py +0 -0
  43. {pyares-1.0.1 → pyares-1.2.0}/PyAres/Demo/Analyzers/airship_analyzer.py +0 -0
  44. {pyares-1.0.1 → pyares-1.2.0}/PyAres/Demo/Analyzers/analyzer_test.py +0 -0
  45. {pyares-1.0.1 → pyares-1.2.0}/PyAres/Demo/Analyzers/analyzer_test_tools_demo.py +0 -0
  46. {pyares-1.0.1 → pyares-1.2.0}/PyAres/Demo/Analyzers/analyzer_wiki.py +0 -0
  47. {pyares-1.0.1 → pyares-1.2.0}/PyAres/Demo/Devices/failure_test_device.py +0 -0
  48. {pyares-1.0.1 → pyares-1.2.0}/PyAres/Demo/Devices/hotplate.py +0 -0
  49. {pyares-1.0.1 → pyares-1.2.0}/PyAres/Demo/Devices/random_number_device.py +0 -0
  50. {pyares-1.0.1 → pyares-1.2.0}/PyAres/Demo/Devices/rotary_mixer.py +0 -0
  51. {pyares-1.0.1 → pyares-1.2.0}/PyAres/Demo/Planners/airship_planner.py +0 -0
  52. {pyares-1.0.1 → pyares-1.2.0}/PyAres/Demo/Planners/planner_test_tools_demo.py +0 -0
  53. {pyares-1.0.1 → pyares-1.2.0}/PyAres/Device/__init__.py +0 -0
  54. {pyares-1.0.1 → pyares-1.2.0}/PyAres/Device/device_models.py +0 -0
  55. {pyares-1.0.1 → pyares-1.2.0}/PyAres/Device/device_warnings.py +0 -0
  56. {pyares-1.0.1 → pyares-1.2.0}/PyAres/Utils/ares_data_schema_utils.py +0 -0
  57. {pyares-1.0.1 → pyares-1.2.0}/PyAres/Utils/ares_data_type_utils.py +0 -0
  58. {pyares-1.0.1 → pyares-1.2.0}/PyAres/Utils/ares_device_command_utils.py +0 -0
  59. {pyares-1.0.1 → pyares-1.2.0}/PyAres/Utils/ares_struct_utils.py +0 -0
  60. {pyares-1.0.1 → pyares-1.2.0}/PyAres/Utils/param_history_info_utils.py +0 -0
  61. {pyares-1.0.1 → pyares-1.2.0}/PyAres/Utils/plan_request_utils.py +0 -0
  62. {pyares-1.0.1 → pyares-1.2.0}/PyAres/test_tools/__init__.py +0 -0
  63. {pyares-1.0.1 → pyares-1.2.0}/PyAres/test_tools/analyzer_tester.py +0 -0
  64. {pyares-1.0.1 → pyares-1.2.0}/PyAres/test_tools/planner_tester.py +0 -0
  65. {pyares-1.0.1 → pyares-1.2.0}/pyproject.toml +0 -0
  66. {pyares-1.0.1 → pyares-1.2.0}/setup.cfg +0 -0
  67. {pyares-1.0.1 → pyares-1.2.0}/setup.py +0 -0
  68. {pyares-1.0.1 → pyares-1.2.0}/tests/__init__.py +0 -0
  69. {pyares-1.0.1 → pyares-1.2.0}/tests/mock_grpc_context.py +0 -0
  70. {pyares-1.0.1 → pyares-1.2.0}/tests/test_ares_data_schema_utils.py +0 -0
  71. {pyares-1.0.1 → pyares-1.2.0}/tests/test_ares_data_type_utils.py +0 -0
  72. {pyares-1.0.1 → pyares-1.2.0}/tests/test_ares_device.py +0 -0
  73. {pyares-1.0.1 → pyares-1.2.0}/tests/test_ares_struct_utils.py +0 -0
  74. {pyares-1.0.1 → pyares-1.2.0}/tests/test_ares_value_type_conversions.py +0 -0
@@ -168,6 +168,7 @@ cython_debug/
168
168
 
169
169
  # VS Code
170
170
  .vscode/
171
+ .vs/
171
172
 
172
173
  #macOS
173
174
  .DS_Store
pyares-1.2.0/PKG-INFO ADDED
@@ -0,0 +1,27 @@
1
+ Metadata-Version: 2.4
2
+ Name: PyAres
3
+ Version: 1.2.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.31.0
13
+ Description-Content-Type: text/markdown
14
+
15
+ # PyAres
16
+
17
+ 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.
18
+
19
+ You can find the main documentation on PyAres [here](https://afrl-ares.github.io/docs/pyares/intro) as well as the rest of the ARES ecosystem by visting https://afrl-ares.github.io/ to browse our centralized documentation site.
20
+
21
+ ### 📄 License
22
+
23
+ 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>
24
+
25
+ ### CLEARANCE
26
+
27
+ Distribution A. Approved for public release: distribution unlimited. AFRL-2025-5332.
@@ -0,0 +1,147 @@
1
+ # Standard Imports
2
+ import grpc
3
+ from typing import Callable, Awaitable, Union, Mapping, Dict, Optional
4
+
5
+ # Import generated protobuf and gRPC stubs
6
+ from ares_datamodel.analyzing.remote import ares_remote_analyzer_service_pb2 as analyzer_service
7
+ from ares_datamodel.analyzing.remote import ares_remote_analyzer_service_pb2_grpc as analyzer_service_grpc
8
+ from ares_datamodel.analyzing import analysis_pb2
9
+ from ares_datamodel.analyzing import analyzer_capabilities_pb2
10
+ from ares_datamodel import ares_data_schema_pb2
11
+ from ares_datamodel import ares_outcome_enum_pb2
12
+
13
+ # Import Utilities
14
+ from ..Utils import ares_struct_utils
15
+ from ..Utils import ares_data_schema_utils
16
+ from ..Utils import ares_outcome_utils
17
+ from ..Utils.ares_service_base import AresServiceWrapperBase, AresBaseService
18
+
19
+ # Import python models
20
+ from ..Models import ares_data_models, RequestMetadata, AresSchemaEntry
21
+ from .analyzer_models import AnalysisRequest, AnalysisResponse
22
+
23
+ # Type hints for the user's custom logic
24
+ AnalyzeLogicFunction = Callable[[AnalysisRequest], Union[AnalysisResponse, Awaitable[AnalysisResponse]]]
25
+
26
+ class AresAnalyzerServiceWrapper(AresServiceWrapperBase, analyzer_service_grpc.AresRemoteAnalyzerServiceServicer):
27
+ """
28
+ A wrapper around the gRPC service to expose native Python objects for analysis.
29
+ """
30
+ def __init__(self, name: str, version: str, description: str, timeout: int, custom_analysis_logic: AnalyzeLogicFunction):
31
+ super().__init__(name, version, description, timeout)
32
+ self._custom_analysis_logic = custom_analysis_logic
33
+ self._analysis_parameters: Dict[str, ares_data_schema_pb2.AresValueSchema] = {}
34
+
35
+ def Analyze(self, request: analyzer_service.AnalysisRequest, context) -> analysis_pb2.Analysis:
36
+ print("Received an analysis request!")
37
+ try:
38
+ python_request = AnalysisRequest(
39
+ inputs=ares_struct_utils.ares_struct_to_dict(request.inputs),
40
+ settings=ares_struct_utils.ares_struct_to_dict(request.settings),
41
+ metadata=RequestMetadata(request.metadata))
42
+
43
+ python_response = self._custom_analysis_logic(python_request)
44
+ python_response = self._resolve_awaitable(python_response)
45
+
46
+ if not isinstance(python_response, AnalysisResponse):
47
+ print("Analysis response was an invalid type, ")
48
+ proto_analysis = analysis_pb2.Analysis()
49
+ proto_analysis.analysis_outcome = ares_outcome_enum_pb2.FAILURE
50
+ proto_analysis.error_string = "The user's custom analysis logic returned an invalid type, analysis cannot be processed"
51
+ return proto_analysis
52
+
53
+ print("Sending Analysis Response.....")
54
+ return analysis_pb2.Analysis(
55
+ result=python_response.result,
56
+ analysis_outcome=ares_outcome_utils.python_ares_outcome_to_proto_ares_outcome(python_response.outcome),
57
+ error_string=python_response.error_string
58
+ )
59
+
60
+ except Exception as e:
61
+ context.set_code(grpc.StatusCode.INTERNAL)
62
+ context.set_details(f"Error in custom analysis logic: {e}")
63
+ return analysis_pb2.Analysis(analysis_outcome=ares_outcome_enum_pb2.FAILURE, error_string=str(e))
64
+
65
+
66
+ def GetAnalysisParameters(self, request, context):
67
+ print("Analysis Parameters Requested")
68
+ try:
69
+ analysisParamResponse = analyzer_service.AnalysisParametersResponse()
70
+
71
+ for key, value in self._analysis_parameters.items():
72
+ map_entry = analysisParamResponse.parameter_schema.fields[key]
73
+ map_entry.CopyFrom(value)
74
+
75
+ return analysisParamResponse
76
+
77
+ except Exception as e:
78
+ print(f"Exception while trying to respond to ARES with analysis parameters! {e}")
79
+
80
+ def GetAnalyzerCapabilities(self, request, context) -> analyzer_capabilities_pb2.AnalyzerCapabilities:
81
+ print("Capabilities Requested!")
82
+ capabilities = analyzer_capabilities_pb2.AnalyzerCapabilities(timeout_seconds=self._timeout)
83
+ try:
84
+ for(key, value) in self._settings.items():
85
+ settings_entry = capabilities.settings_schema.fields[key]
86
+ settings_entry.CopyFrom(value)
87
+
88
+ return capabilities
89
+
90
+ except Exception as e:
91
+ print(f"Exception while trying to respond to ARES capabilities request! {e}")
92
+ return capabilities
93
+
94
+
95
+ def ValidateInputs(self, request: analyzer_service.ParameterValidationRequest, context):
96
+ response = analyzer_service.ParameterValidationResult(success=True)
97
+ provided_params: Mapping[str, ares_data_schema_pb2.AresValueSchema] = request.input_schema.fields
98
+
99
+ for stored_key, stored_schema in self._analysis_parameters.items():
100
+ if stored_key in provided_params:
101
+ matching_schema = provided_params[stored_key]
102
+ if stored_schema.type != matching_schema.type:
103
+ message = f"Schema Mismatch! {stored_key} was provided with the value type {stored_schema.type}, but the value type {matching_schema} was expected!"
104
+ response.messages.append(message)
105
+ else:
106
+ if not stored_schema.optional:
107
+ 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!"
108
+ response.messages.append(message)
109
+
110
+ if len(response.messages) != 0:
111
+ response.success = False
112
+
113
+ return response
114
+
115
+ class AresAnalyzerService(AresBaseService):
116
+ """
117
+ Manages the gRPC server for the AresAnalyzerService.
118
+ """
119
+ def __init__(self,
120
+ custom_analysis_logic: AnalyzeLogicFunction,
121
+ name: str,
122
+ version: str,
123
+ description: str = "",
124
+ timeout: int = 30,
125
+ use_localhost: bool = True,
126
+ port: int = 7083,
127
+ max_message_size: int = -1):
128
+ """
129
+ Initializes the AresAnalyzerService.
130
+ """
131
+ super().__init__(
132
+ service_name=name,
133
+ description=description,
134
+ version=version,
135
+ port=port,
136
+ use_localhost=use_localhost,
137
+ max_message_size=max_message_size
138
+ )
139
+
140
+ self._service_wrapper = AresAnalyzerServiceWrapper(name=name, version=version, description=description, timeout=timeout, custom_analysis_logic=custom_analysis_logic)
141
+ analyzer_service_grpc.add_AresRemoteAnalyzerServiceServicer_to_server(self._service_wrapper, self.get_server())
142
+
143
+ def add_analysis_parameter(self, parameter_name: str, parameter_type: ares_data_models.AresDataType, optional: bool = False, struct_schema: Optional[Dict[str, AresSchemaEntry]] = None):
144
+ """
145
+ Adds an analysis parameter that will be reported to ARES.
146
+ """
147
+ self._service_wrapper._analysis_parameters[parameter_name] = ares_data_schema_utils.create_settings_schema_entry(parameter_type, optional, [], struct_schema)
@@ -30,7 +30,7 @@ if __name__ == "__main__":
30
30
  device_name = "Demo Device"
31
31
  description = "A device to demonstrate the PyAres device capabilities"
32
32
  version = "1.0.0"
33
- device_service = AresDeviceService(device.enter_safe_mode, device.get_device_state, device_name, description, version)
33
+ device_service = AresDeviceService(device.enter_safe_mode, device.get_device_state, device_name, description, version, False)
34
34
 
35
35
  # Create the "Set Temperature" Command
36
36
  quantity_schema = QuantitySchema(ureg.degree_Celsius, 40.0, 125.0)
@@ -40,7 +40,7 @@ if __name__ == "__main__":
40
40
  device_service.add_new_command(set_temp_descriptor, device.set_temperature)
41
41
 
42
42
  # Create the "Get Temperature" Command
43
- output_schema = {"temperature": DeviceSchemaEntry(AresDataType.QUANTITY, "The current temperature of the device", quantity_schema=quantity_schema)}
43
+ output_schema = {"temperature": DeviceSchemaEntry(AresDataType.NUMBER, "The current temperature of the device")}
44
44
  get_temp_desc = DeviceCommandDescriptor("Get Temperature", "Get's the current temperature of the demo device.", {}, output_schema)
45
45
  device_service.add_new_command(get_temp_desc, device.get_temperature)
46
46
 
@@ -8,6 +8,14 @@ def plan(request: PlanRequest) -> PlanResponse:
8
8
  gpdoods = []
9
9
  names = []
10
10
 
11
+ for i in range(len(request.analysis_results)):
12
+ currentAnalysis = request.analysis_results[i]
13
+
14
+ for j in range(len(currentAnalysis.objectives)):
15
+ currentObjective : Objective = currentAnalysis.objectives[j]
16
+ print(f"Analysis Result {i}: Objective number {j} is named {currentObjective.objective_name} and has a value of {currentObjective.objective_value}")
17
+
18
+
11
19
  for param in request.parameters:
12
20
  if param.planner_name == "GPRDood":
13
21
  gpdoods.append(param)
@@ -0,0 +1,33 @@
1
+ from PyAres import *
2
+ from typing import List
3
+ import random
4
+
5
+ def generate_plans(request: PlanRequest) -> List[Plan]:
6
+ plans = []
7
+ for i in request.previous_plan_status_codes:
8
+ print(f"Status Code: {i}")
9
+
10
+ # Make as many plans as was requested
11
+ for i in range(request.batch_size):
12
+ current_params = []
13
+ for param in request.parameters:
14
+ # Simple Logic: Pick a random value within the allowed range
15
+ val = random.uniform(param.minimum_value, param.maximum_value)
16
+ current_params.append(PlannedParameter(param.name, val))
17
+
18
+ plans.append(Plan(current_params, Outcome.SUCCESS))
19
+
20
+ return plans
21
+
22
+ if __name__ == "__main__":
23
+ service = AresPlannerService(
24
+ generate_plans,
25
+ "Random Search Planner",
26
+ "This planner picks random values within bounds.",
27
+ "1.0.0"
28
+ )
29
+
30
+ # Tell ARES we can plan for Numeric values
31
+ service.add_supported_type(AresDataType.NUMBER)
32
+
33
+ service.start()
@@ -2,7 +2,6 @@ import grpc
2
2
  import inspect
3
3
  import time
4
4
  import warnings
5
- from concurrent import futures
6
5
  from typing import Dict, Callable, Awaitable, Union, Any, Optional
7
6
 
8
7
  from ares_datamodel.device.remote import ares_remote_device_service_pb2 as device_service
@@ -16,13 +15,14 @@ from google.protobuf import empty_pb2
16
15
 
17
16
  from .device_models import DeviceCommandDescriptor
18
17
  from ..Models import Limits
19
- from .device_models import DeviceCommandDescriptor, DeviceCommandResponse, StatusCode
18
+ from .device_models import DeviceCommandDescriptor, DeviceCommandResponse
20
19
  from ..Utils import ares_device_command_utils
21
20
  from ..Utils import ares_data_schema_utils
22
21
  from ..Utils import ares_struct_utils
23
22
  from ..Utils import ares_value_utils
24
23
  from ..Utils import ares_data_type_utils
25
24
  from ..Utils import device_status_code_utils
25
+ from ..Utils.ares_service_base import AresServiceWrapperBase, AresBaseService
26
26
 
27
27
  # Type hint for the user's custom methods
28
28
  EnterSafeModeMethod = Callable[[], None]
@@ -30,15 +30,19 @@ AllowedReturns = Union[DeviceCommandResponse, Dict[str, Any], Any]
30
30
  DeviceCommandMethod = Callable[..., AllowedReturns]
31
31
  DeviceStateMethod = Callable[[], Dict[str, Any]]
32
32
 
33
- class AresDeviceServiceWrapper(device_service_grpc.AresRemoteDeviceServiceServicer):
33
+ class AresDeviceServiceWrapper(AresServiceWrapperBase, device_service_grpc.AresRemoteDeviceServiceServicer):
34
34
  """
35
35
  A wrapper around the gRPC service to expose native Python objects for devices
36
36
  """
37
37
 
38
- def __init__(self, device_name: str, description: str, version: str, enter_safe_mode: EnterSafeModeMethod, update_device_state: DeviceStateMethod):
38
+ def __init__(self, device_name: str, description: str, version: str, timeout: int, enter_safe_mode: EnterSafeModeMethod, update_device_state: DeviceStateMethod):
39
+ super().__init__(service_name=device_name, version=version, description=description, timeout=timeout)
40
+
41
+ # Preserve public attributes for backwards compatibility
39
42
  self.device_name = device_name
40
43
  self.description = description
41
44
  self.version = version
45
+
42
46
  self._enter_safe_mode = enter_safe_mode
43
47
  self._update_device_state = update_device_state
44
48
  self._setting_schema: Dict[str, ares_data_schema_pb2.AresValueSchema] = {}
@@ -48,13 +52,16 @@ class AresDeviceServiceWrapper(device_service_grpc.AresRemoteDeviceServiceServic
48
52
  self._command_methods: Dict[str, Callable] = {}
49
53
 
50
54
  def GetOperationalStatus(self, request, context) -> device_status_pb2.DeviceOperationalStatus:
51
- return device_status_pb2.DeviceOperationalStatus(operational_state=device_status_pb2.OperationalState.ACTIVE, message=f"{self.device_name} is active!")
55
+ return device_status_pb2.DeviceOperationalStatus(
56
+ operational_state=device_status_pb2.OperationalState.ACTIVE,
57
+ message=f"{self._service_name} is active!"
58
+ )
52
59
 
53
60
  def GetInfo(self, request, context) -> device_service.DeviceInfoResponse:
54
61
  info = device_service.DeviceInfoResponse()
55
- info.name = self.device_name
56
- info.description = self.description
57
- info.version = self.version
62
+ info.name = self._service_name
63
+ info.description = self._description
64
+ info.version = self._version
58
65
  return info
59
66
 
60
67
  def GetCommands(self, request, context) -> device_service.CommandsResponse:
@@ -249,7 +256,7 @@ class AresDeviceServiceWrapper(device_service_grpc.AresRemoteDeviceServiceServic
249
256
  except grpc.RpcError as e:
250
257
  print(f"gRPC error occured in device state stream")
251
258
 
252
- class AresDeviceService:
259
+ class AresDeviceService(AresBaseService):
253
260
  """ Manages the gRPC service for the AresDeviceSerivce """
254
261
  def __init__(self,
255
262
  enter_safe_mode_logic: EnterSafeModeMethod,
@@ -257,8 +264,10 @@ class AresDeviceService:
257
264
  device_name: str,
258
265
  description: str,
259
266
  version: str,
267
+ timeout: int = 30,
260
268
  use_localhost: bool = True,
261
- port: int = 7100):
269
+ port: int = 7100,
270
+ max_message_size: int = -1):
262
271
  """
263
272
  Initializes the AresDeviceService
264
273
 
@@ -270,22 +279,28 @@ class AresDeviceService:
270
279
  device_name (str): The name description of your device.
271
280
  description (str): A brief description of your device.
272
281
  version (str): The version associated with your device implementation.
282
+ timeout (int): Timeout in seconds for service calls reported to ARES.
273
283
  use_localhost (bool): An optional value that allows the user to specify whether to host the service on the local network. Defaults to True.
274
284
  port (int): The port that your device service will serve on. Defaults to port 7100.
285
+ max_message_size (int): Maximum message size for gRPC. Defaults to -1 (use library default).
275
286
  """
276
287
 
288
+ super().__init__(
289
+ service_name=device_name,
290
+ description=description,
291
+ version=version,
292
+ port=port,
293
+ use_localhost=use_localhost,
294
+ max_message_size=max_message_size
295
+ )
296
+
297
+ # Preserve these public attributes for backwards compatibility
277
298
  self.device_name = device_name
278
299
  self.description = description
279
300
  self.version = version
280
301
 
281
- self._port = port
282
- self._server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
283
- self._service_wrapper = AresDeviceServiceWrapper(device_name, description, version, enter_safe_mode_logic, get_device_state_logic)
284
- device_service_grpc.add_AresRemoteDeviceServiceServicer_to_server(self._service_wrapper, self._server)
285
- if(use_localhost):
286
- self._server.add_insecure_port(f'localhost:{self._port}')
287
- else:
288
- self._server.add_insecure_port(f'[::]:{self._port}')
302
+ self._service_wrapper = AresDeviceServiceWrapper(device_name, description, version, timeout, enter_safe_mode_logic, get_device_state_logic)
303
+ device_service_grpc.add_AresRemoteDeviceServiceServicer_to_server(self._service_wrapper, self.get_server())
289
304
 
290
305
  def add_new_command(self, cmd_descriptor: DeviceCommandDescriptor, method):
291
306
  """
@@ -345,12 +360,13 @@ class AresDeviceService:
345
360
  """
346
361
 
347
362
  print(f"Starting Ares Device Service on port {self._port}...")
348
- self._server.start()
363
+ server = self.get_server()
364
+ server.start()
349
365
 
350
366
  if wait_for_termination:
351
- self._server.wait_for_termination()
367
+ server.wait_for_termination()
352
368
 
353
369
  def stop(self):
354
370
  """ Stops the service, terminating the connection. """
355
371
  print("Stopping Ares Device Service...")
356
- self._server.stop(0).wait()
372
+ self.get_server().stop(0).wait()
@@ -1,4 +1,4 @@
1
- from .ares_data_models import AresDataType, Outcome, RequestMetadata, AresSchemaEntry, Quantity, QuantitySchema, Limits
1
+ from .ares_data_models import AresDataType, Outcome, RequestMetadata, AresSchemaEntry, Quantity, QuantitySchema, Limits, PlanStatusCode
2
2
 
3
3
  __all__ = [
4
4
  "AresDataType",
@@ -7,5 +7,6 @@ __all__ = [
7
7
  "AresSchemaEntry",
8
8
  "Quantity",
9
9
  "QuantitySchema",
10
- "Limits"
10
+ "Limits",
11
+ "PlanStatusCode"
11
12
  ]
@@ -30,6 +30,12 @@ class Outcome(Enum):
30
30
  WARNING = 3
31
31
  CANCELED = 4
32
32
 
33
+ class PlanStatusCode(Enum):
34
+ PLAN_STATUS_UNSPECIFIED = 0
35
+ PLAN_ACCEPTED = 1
36
+ PLAN_UNACHIEVABLE = 2
37
+ PLAN_FAILED = 3
38
+
33
39
  class RequestMetadata():
34
40
  def __init__(self, proto_metadata: request_metadata_pb2.RequestMetadata):
35
41
  self.system_name = proto_metadata.system_name
@@ -1,9 +1,13 @@
1
- from .planner_models import PlanningParameter, PlanRequest, PlanResponse, ParameterHistoryItem
1
+ from .planner_models import PlanningParameter, PlanRequest, PlanResponse, ParameterHistoryItem, Plan, PlannedParameter, ObjectiveStatus
2
2
  from .planning_service import AresPlannerService
3
3
 
4
4
  __all__ = [
5
5
  "PlanningParameter",
6
6
  "PlanRequest",
7
7
  "PlanResponse",
8
- "AresPlannerService"
8
+ "AresPlannerService",
9
+ "Plan",
10
+ "PlannedParameter",
11
+ "ParameterHistoryItem",
12
+ "ObjectiveStatus"
9
13
  ]
@@ -1,5 +1,6 @@
1
1
  from typing import Dict, Any, List, Sequence, Optional
2
- from ..Models import Outcome, AresDataType, RequestMetadata
2
+ from ..Models import Outcome, AresDataType, RequestMetadata, PlanStatusCode
3
+ from enum import Enum
3
4
 
4
5
  class ParameterHistoryItem:
5
6
  """ Represents a single historical parameter item """
@@ -20,6 +21,13 @@ class ParameterHistoryItem:
20
21
 
21
22
  def __repr__(self) -> str:
22
23
  return self.__str__()
24
+
25
+ class ObjectiveStatus(Enum):
26
+ """ An enum representing the current status of the objective the planner is trying to achieve (if any) """
27
+ OBJECTIVE_STATUS_UNSPECIFIED = 0
28
+ OBJECTIVE_UNACHIEVED = 1
29
+ OBJECTIVE_ACHIEVED = 2
30
+ OBJECTIVE_FAILED = 3
23
31
 
24
32
  class PlanningParameter:
25
33
  """
@@ -28,9 +36,15 @@ class PlanningParameter:
28
36
  Designed to provide a more user-friendly abstraction for the user to interact
29
37
  with planning parameters through.
30
38
  """
31
- def __init__(self, name: str, minimum_value: float,
32
- maximum_value: float, param_history: list[ParameterHistoryItem], data_type: AresDataType,
33
- is_planned: bool, is_result: bool, planner_name: str, initial_value = None):
39
+ def __init__(self, name: str,
40
+ data_type: AresDataType,
41
+ minimum_value: float = None,
42
+ maximum_value: float = None,
43
+ param_history: list[ParameterHistoryItem] = [],
44
+ is_planned: bool = False,
45
+ is_result: bool = False,
46
+ planner_name: str = "",
47
+ initial_value = None):
34
48
  """
35
49
  Initializes a PlanningParameter.
36
50
 
@@ -107,7 +121,13 @@ class PlanRequest:
107
121
 
108
122
  Designed to provide a more user-friendly abstraction for interacting with a plan request message.
109
123
  """
110
- def __init__(self, parameters: list[PlanningParameter], settings: Dict[str, Any], analysis_results: Sequence[float], metadata: RequestMetadata = RequestMetadata.from_default_values()):
124
+ def __init__(self,
125
+ parameters: list[PlanningParameter],
126
+ settings: Dict[str, Any],
127
+ analysis_results: Sequence[float],
128
+ metadata: RequestMetadata = RequestMetadata.from_default_values(),
129
+ batch_size: int = 1,
130
+ previous_plan_status_codes: List[PlanStatusCode] = None):
111
131
  """
112
132
  Initializes a PlanRequest.
113
133
 
@@ -117,8 +137,14 @@ class PlanRequest:
117
137
  self.parameters = parameters
118
138
  self.settings = settings
119
139
  self.analysis_results = analysis_results
140
+ self.batch_size = batch_size
120
141
  self.request_metadata = metadata
121
142
 
143
+ if previous_plan_status_codes is None:
144
+ self.previous_plan_status_codes = []
145
+ else:
146
+ self.previous_plan_status_codes = previous_plan_status_codes
147
+
122
148
  def __str__(self) -> str:
123
149
  param_str = "\n ".join(self.parameter_names)
124
150
  settings_str = "\n ".join([f"{k}: {v}" for k, v in self.settings.items()])
@@ -127,13 +153,15 @@ class PlanRequest:
127
153
  metadata_str = str(self.request_metadata).replace('\n', '\n ')
128
154
  return (f"PlanRequest object with:\n"
129
155
  f"parameters:\n"
130
- f" {param_str}\n"
156
+ f"{param_str}\n"
131
157
  f"settings:\n"
132
- f" {settings_str}\n"
158
+ f"{settings_str}\n"
133
159
  f"analysis_results:\n"
134
- f" {analysis_str}\n"
160
+ f"{analysis_str}\n"
135
161
  f"request_metadata:\n"
136
- f"{metadata_str}")
162
+ f"{metadata_str}"
163
+ f"batch_size:\n"
164
+ f"{self.batch_size}")
137
165
 
138
166
  def __repr__(self) -> str:
139
167
  return self.__str__()
@@ -167,7 +195,8 @@ class PlanResponse:
167
195
  parameter_values: Optional[list] = None,
168
196
  parameter_data: Optional[dict[str,Any]] = None,
169
197
  outcome: Outcome = Outcome.SUCCESS,
170
- error_string: str = ""):
198
+ error_string: str = "",
199
+ objective_status: ObjectiveStatus = ObjectiveStatus.OBJECTIVE_STATUS_UNSPECIFIED):
171
200
  """
172
201
  Initializes a PlanResponse. Using either lists of names and values or a python dictonary of name:value pairs
173
202
 
@@ -175,6 +204,9 @@ class PlanResponse:
175
204
  parameter_names: A list of names associated with planned parameters.
176
205
  parameter_values: A list of values associated with planned parameters.
177
206
  parameter_data: A python dictionary of key:value pairs of planned parameters and planned values
207
+ outcome: An enum of type Outcome that determines whether the planning process succeeded or not, defaults to SUCCESS
208
+ error_string: An optional string for specifying planning failure reasons to be relayed to ARES
209
+ objective_status: An optional value that specifies the status of the objective your planner is trying to achieve (if any)
178
210
  """
179
211
  if parameter_data is not None:
180
212
  self.parameter_names = list(parameter_data.keys())
@@ -191,13 +223,55 @@ class PlanResponse:
191
223
 
192
224
  self.outcome = outcome
193
225
  self.error_string = error_string
226
+ self.objective_status = objective_status
194
227
 
195
228
  def __str__(self):
196
229
  return (f"PlanResponse object with:\n"
197
230
  f" outcome: {self.outcome}\n"
198
231
  f" parameter_names: {self.parameter_names}\n"
199
232
  f" parameter_values: {self.parameter_values}\n"
200
- f" error_string: {self.error_string}\n")
233
+ f" error_string: {self.error_string}\n"
234
+ f" objective_status: {self.objective_status}\n")
201
235
 
202
236
  def __repr__(self) -> str:
203
- return self.__str__()
237
+ return self.__str__()
238
+
239
+ class PlannedParameter:
240
+ def __init__(self, parameter_name: str, parameter_value: Any):
241
+ self.parameter_name = parameter_name
242
+ self.parameter_value = parameter_value
243
+
244
+ def __str__(self):
245
+ # A clean, readable key-value output
246
+ return f"{self.parameter_name}: {self.parameter_value}"
247
+
248
+ def __repr__(self):
249
+ # The !r formatting flag automatically wraps strings in quotes and calls __repr__ on the values
250
+ return f"PlannedParameters(parameter_name={self.parameter_name!r}, parameter_value={self.parameter_value!r})"
251
+
252
+ class Plan:
253
+ def __init__(self,
254
+ planned_parameters: List[PlannedParameter],
255
+ outcome: Outcome,
256
+ error_string: str = "",
257
+ objective_status: ObjectiveStatus = ObjectiveStatus.OBJECTIVE_STATUS_UNSPECIFIED):
258
+ self.planned_parameters = planned_parameters
259
+ self.outcome = outcome
260
+ self.error_string = error_string
261
+ self.objective_status = objective_status
262
+
263
+ def __str__(self):
264
+ base_str = f"Plan (Outcome: {self.outcome}) \n (Objective Status: {self.objective_status})"
265
+
266
+ # Format the list of parameters into a readable string
267
+ if self.planned_parameters:
268
+ params_str = ", ".join(str(p) for p in self.planned_parameters)
269
+ base_str += f" | Parameters: [{params_str}]"
270
+
271
+ if self.error_string:
272
+ base_str += f" - Error: '{self.error_string}'"
273
+
274
+ return base_str
275
+
276
+ def __repr__(self):
277
+ return f"Plan(planned_parameters={self.planned_parameters!r}, outcome={self.outcome!r}, error_string={self.error_string!r})"