PyAres 1.2.0__tar.gz → 1.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.
Files changed (72) hide show
  1. {pyares-1.2.0 → pyares-1.3.0}/PKG-INFO +3 -3
  2. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Analyzing/__init__.py +2 -1
  3. pyares-1.3.0/PyAres/Analyzing/analysis_service.py +242 -0
  4. pyares-1.3.0/PyAres/Analyzing/analyzer_models.py +248 -0
  5. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Demo/Analyzers/analyzer_wiki.py +6 -4
  6. pyares-1.3.0/PyAres/Demo/Analyzers/ax_analyzer_test.py +80 -0
  7. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Demo/Planners/planner_test.py +5 -5
  8. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Planning/planner_models.py +69 -17
  9. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Planning/planning_service.py +61 -19
  10. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/ares_data_schema_utils.py +29 -11
  11. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/ares_service_base.py +62 -56
  12. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/device_status_code_utils.py +1 -1
  13. {pyares-1.2.0 → pyares-1.3.0}/PyAres/__init__.py +1 -0
  14. {pyares-1.2.0 → pyares-1.3.0}/pyproject.toml +1 -1
  15. {pyares-1.2.0 → pyares-1.3.0}/tests/test_analyzer.py +131 -4
  16. {pyares-1.2.0 → pyares-1.3.0}/tests/test_analyzer_integration.py +4 -1
  17. {pyares-1.2.0 → pyares-1.3.0}/tests/test_ares_data_schema_utils.py +29 -11
  18. pyares-1.3.0/tests/test_planner_analysis_data.py +182 -0
  19. pyares-1.2.0/PyAres/Analyzing/analysis_service.py +0 -147
  20. pyares-1.2.0/PyAres/Analyzing/analyzer_models.py +0 -66
  21. {pyares-1.2.0 → pyares-1.3.0}/.github/dependabot.yml +0 -0
  22. {pyares-1.2.0 → pyares-1.3.0}/.github/workflows/pypi.yaml +0 -0
  23. {pyares-1.2.0 → pyares-1.3.0}/.github/workflows/python-tests.yaml +0 -0
  24. {pyares-1.2.0 → pyares-1.3.0}/.gitignore +0 -0
  25. {pyares-1.2.0 → pyares-1.3.0}/LICENSE +0 -0
  26. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Demo/Analyzers/airship_analyzer.py +0 -0
  27. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Demo/Analyzers/analyzer_test.py +0 -0
  28. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Demo/Analyzers/analyzer_test_tools_demo.py +0 -0
  29. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Demo/Devices/device_test.py +0 -0
  30. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Demo/Devices/failure_test_device.py +0 -0
  31. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Demo/Devices/hotplate.py +0 -0
  32. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Demo/Devices/random_number_device.py +0 -0
  33. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Demo/Devices/rotary_mixer.py +0 -0
  34. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Demo/Planners/airship_planner.py +0 -0
  35. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Demo/Planners/planner_test_tools_demo.py +0 -0
  36. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Demo/Planners/planner_wiki.py +0 -0
  37. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Device/__init__.py +0 -0
  38. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Device/device_models.py +0 -0
  39. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Device/device_service.py +0 -0
  40. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Device/device_warnings.py +0 -0
  41. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Models/__init__.py +0 -0
  42. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Models/ares_data_models.py +0 -0
  43. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Planning/__init__.py +0 -0
  44. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/ares_data_type_utils.py +0 -0
  45. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/ares_device_command_utils.py +0 -0
  46. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/ares_objective_status_utils.py +0 -0
  47. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/ares_outcome_utils.py +0 -0
  48. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/ares_plan_status_code_utils.py +0 -0
  49. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/ares_struct_utils.py +0 -0
  50. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/ares_value_utils.py +0 -0
  51. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/datamodel_version_utils.py +0 -0
  52. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/grpc_base.py +0 -0
  53. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/logging_utils.py +0 -0
  54. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/metadata_interceptors.py +0 -0
  55. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/param_history_info_utils.py +0 -0
  56. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/plan_request_utils.py +0 -0
  57. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/plan_response_utils.py +0 -0
  58. {pyares-1.2.0 → pyares-1.3.0}/PyAres/Utils/planning_param_utils.py +0 -0
  59. {pyares-1.2.0 → pyares-1.3.0}/PyAres/test_tools/__init__.py +0 -0
  60. {pyares-1.2.0 → pyares-1.3.0}/PyAres/test_tools/analyzer_tester.py +0 -0
  61. {pyares-1.2.0 → pyares-1.3.0}/PyAres/test_tools/planner_tester.py +0 -0
  62. {pyares-1.2.0 → pyares-1.3.0}/README.md +0 -0
  63. {pyares-1.2.0 → pyares-1.3.0}/setup.cfg +0 -0
  64. {pyares-1.2.0 → pyares-1.3.0}/setup.py +0 -0
  65. {pyares-1.2.0 → pyares-1.3.0}/tests/__init__.py +0 -0
  66. {pyares-1.2.0 → pyares-1.3.0}/tests/mock_grpc_context.py +0 -0
  67. {pyares-1.2.0 → pyares-1.3.0}/tests/test_ares_data_type_utils.py +0 -0
  68. {pyares-1.2.0 → pyares-1.3.0}/tests/test_ares_device.py +0 -0
  69. {pyares-1.2.0 → pyares-1.3.0}/tests/test_ares_struct_utils.py +0 -0
  70. {pyares-1.2.0 → pyares-1.3.0}/tests/test_ares_value_type_conversions.py +0 -0
  71. {pyares-1.2.0 → pyares-1.3.0}/tests/test_batch_planning_regressions.py +0 -0
  72. {pyares-1.2.0 → pyares-1.3.0}/tests/test_planner.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: PyAres
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: The official Python library for ARES.
5
5
  Project-URL: Homepage, https://github.com/AFRL-ARES/PyAres
6
6
  Author-email: AFRL <AFRL.RX.AREScontact@us.af.mil>
@@ -9,7 +9,7 @@ License-File: LICENSE
9
9
  Classifier: Operating System :: OS Independent
10
10
  Classifier: Programming Language :: Python :: 3
11
11
  Requires-Python: >=3.10
12
- Requires-Dist: ares-datamodel>=0.31.0
12
+ Requires-Dist: ares-datamodel>=0.36.0
13
13
  Description-Content-Type: text/markdown
14
14
 
15
15
  # PyAres
@@ -1,9 +1,10 @@
1
1
  from .analysis_service import AresAnalyzerService
2
- from .analyzer_models import AnalysisResponse, AnalysisRequest, InfoResponse
2
+ from .analyzer_models import AnalysisResponse, AnalysisRequest, InfoResponse, Objective
3
3
 
4
4
  __all__ = [
5
5
  "AnalysisResponse",
6
6
  "AnalysisRequest",
7
7
  "InfoResponse",
8
8
  "AresAnalyzerService",
9
+ "Objective"
9
10
  ]
@@ -0,0 +1,242 @@
1
+ # Standard Imports
2
+ import grpc
3
+ from typing import Callable, Awaitable, Union, Mapping, Dict, Optional, List
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 import ares_value_utils
18
+ from ..Utils.ares_service_base import AresServiceWrapperBase, AresBaseService
19
+
20
+ # Import python models
21
+ from ..Models import ares_data_models, RequestMetadata, AresSchemaEntry
22
+ from .analyzer_models import AnalysisRequest, AnalysisResponse, ObjectiveSchema
23
+
24
+ # Type hints for the user's custom logic
25
+ AnalyzeLogicFunction = Callable[[AnalysisRequest], Union[AnalysisResponse, Awaitable[AnalysisResponse]]]
26
+
27
+
28
+ class AresAnalyzerServiceWrapper(AresServiceWrapperBase, analyzer_service_grpc.AresRemoteAnalyzerServiceServicer):
29
+ """A wrapper around the gRPC service to expose native Python objects for analysis."""
30
+
31
+ def __init__(self, name: str, version: str, description: str, timeout: int, custom_analysis_logic: AnalyzeLogicFunction):
32
+ super().__init__(name, version, description, timeout)
33
+ self._custom_analysis_logic = custom_analysis_logic
34
+ self._analysis_parameters: Dict[str, ares_data_schema_pb2.AresValueSchema] = {}
35
+ self._objective_outputs: Dict[str, ares_data_schema_pb2.AresValueSchema] = {}
36
+
37
+ def Analyze(self, request: analyzer_service.AnalysisRequest, context) -> analysis_pb2.AnalysisResponse:
38
+ print("Received an analysis request!")
39
+ try:
40
+ python_request = AnalysisRequest(
41
+ inputs=ares_struct_utils.ares_struct_to_dict(request.inputs),
42
+ settings=ares_struct_utils.ares_struct_to_dict(request.settings),
43
+ metadata=RequestMetadata(request.metadata),
44
+ )
45
+
46
+ python_response = self._custom_analysis_logic(python_request)
47
+ python_response = self._resolve_awaitable(python_response)
48
+
49
+ if not isinstance(python_response, AnalysisResponse):
50
+ print("Analysis response was an invalid type.")
51
+ proto_response = analysis_pb2.AnalysisResponse()
52
+ proto_response.analysis_outcome = ares_outcome_enum_pb2.FAILURE
53
+ proto_response.error_string = (
54
+ "The user's custom analysis logic returned an invalid type; "
55
+ "expected AnalysisResponse."
56
+ )
57
+ return proto_response
58
+
59
+ if python_response.deprecated_result_usage:
60
+ print(
61
+ "WARNING: AnalysisResponse(result=...) usage is deprecated and will be "
62
+ "removed in a future major version. Please construct objectives explicitly."
63
+ )
64
+
65
+ print("Sending AnalysisResponse.....")
66
+ proto_response = analysis_pb2.AnalysisResponse()
67
+
68
+ for obj in python_response.objectives:
69
+ obj_proto = analysis_pb2.Objective()
70
+ obj_proto.objective_name = obj.objective_name
71
+
72
+ ares_value_utils.py_to_ares_value(obj.objective_value, obj_proto.objective_value)
73
+
74
+ if obj.objective_metadata:
75
+ ares_struct_utils.dict_to_ares_struct(
76
+ obj.objective_metadata, obj_proto.objective_metadata
77
+ )
78
+
79
+ proto_response.objectives.append(obj_proto)
80
+
81
+ proto_response.analysis_outcome = ares_outcome_utils.python_ares_outcome_to_proto_ares_outcome(
82
+ python_response.outcome
83
+ )
84
+ proto_response.error_string = python_response.error_string
85
+
86
+ return proto_response
87
+
88
+ except Exception as e:
89
+ context.set_code(grpc.StatusCode.INTERNAL)
90
+ context.set_details(f"Error in custom analysis logic: {e}")
91
+ proto_response = analysis_pb2.AnalysisResponse()
92
+ proto_response.analysis_outcome = ares_outcome_enum_pb2.FAILURE
93
+ proto_response.error_string = str(e)
94
+ return proto_response
95
+
96
+ def GetAnalysisParameters(self, request, context):
97
+ print("Analysis Parameters Requested")
98
+ try:
99
+ analysis_param_response = analyzer_service.AnalysisParametersResponse()
100
+
101
+ for key, value in self._analysis_parameters.items():
102
+ map_entry = analysis_param_response.parameter_schema.fields[key]
103
+ map_entry.CopyFrom(value)
104
+
105
+ return analysis_param_response
106
+
107
+ except Exception as e:
108
+ print(f"Exception while trying to respond to ARES with analysis parameters! {e}")
109
+
110
+ def GetAnalyzerCapabilities(self, request, context) -> analyzer_capabilities_pb2.AnalyzerCapabilities:
111
+ print("Capabilities Requested!")
112
+ capabilities = analyzer_capabilities_pb2.AnalyzerCapabilities(timeout_seconds=self._timeout)
113
+
114
+ try:
115
+ for key, value in self._settings.items():
116
+ settings_entry = capabilities.settings_schema.fields[key]
117
+ settings_entry.CopyFrom(value)
118
+
119
+ for key, value in self._objective_outputs.items():
120
+ objective_entry = capabilities.objective_output_schema.fields[key]
121
+ objective_entry.CopyFrom(value)
122
+
123
+ return capabilities
124
+
125
+ except Exception as e:
126
+ print(f"Exception while trying to respond to ARES capabilities request! {e}")
127
+ return capabilities
128
+
129
+ def ValidateInputs(self, request: analyzer_service.ParameterValidationRequest, context):
130
+ response = analyzer_service.ParameterValidationResult(success=True)
131
+ provided_params: Mapping[str, ares_data_schema_pb2.AresValueSchema] = request.input_schema.fields
132
+
133
+ for stored_key, stored_schema in self._analysis_parameters.items():
134
+ if stored_key in provided_params:
135
+ matching_schema = provided_params[stored_key]
136
+ if stored_schema.type != matching_schema.type:
137
+ message = (
138
+ f"Schema Mismatch! {stored_key} was provided with the value type "
139
+ f"{stored_schema.type}, but the value type {matching_schema} was expected!"
140
+ )
141
+ response.messages.append(message)
142
+ else:
143
+ if not stored_schema.optional:
144
+ message = (
145
+ f"Schema Missing! {stored_key} is marked as a required piece of data for analysis, "
146
+ f"but no assignment was found in the provided schema!"
147
+ )
148
+ response.messages.append(message)
149
+
150
+ if len(response.messages) != 0:
151
+ response.success = False
152
+
153
+ return response
154
+
155
+
156
+ class AresAnalyzerService(AresBaseService):
157
+ """Manages the gRPC server for the AresAnalyzerService."""
158
+
159
+ def __init__(
160
+ self,
161
+ custom_analysis_logic: AnalyzeLogicFunction,
162
+ name: str,
163
+ version: str,
164
+ description: str = "",
165
+ timeout: int = 30,
166
+ use_localhost: bool = True,
167
+ port: int = 7083,
168
+ max_message_size: int = -1,
169
+ ):
170
+ """Initializes the AresAnalyzerService."""
171
+ super().__init__(
172
+ service_name=name,
173
+ description=description,
174
+ version=version,
175
+ port=port,
176
+ use_localhost=use_localhost,
177
+ max_message_size=max_message_size,
178
+ )
179
+
180
+ self._service_wrapper = AresAnalyzerServiceWrapper(
181
+ name=name,
182
+ version=version,
183
+ description=description,
184
+ timeout=timeout,
185
+ custom_analysis_logic=custom_analysis_logic,
186
+ )
187
+ analyzer_service_grpc.add_AresRemoteAnalyzerServiceServicer_to_server(
188
+ self._service_wrapper, self.get_server()
189
+ )
190
+
191
+ def add_analysis_parameter(
192
+ self,
193
+ parameter_name: str,
194
+ parameter_type: ares_data_models.AresDataType,
195
+ optional: bool = False,
196
+ struct_schema: Optional[Dict[str, AresSchemaEntry]] = None,
197
+ list_element_schema: Optional[AresSchemaEntry] = None,
198
+ ) -> None:
199
+ """Adds an analysis parameter that will be reported to ARES.
200
+
201
+ If `parameter_type` is LIST, `list_element_schema` can be used to describe
202
+ the shape of each element in that list.
203
+ """
204
+ self._service_wrapper._analysis_parameters[parameter_name] = ares_data_schema_utils.create_settings_schema_entry(
205
+ setting_type=parameter_type,
206
+ optional=optional,
207
+ choices=[],
208
+ struct_schema=struct_schema,
209
+ list_element_schema=list_element_schema,
210
+ )
211
+
212
+ def add_objective_output(
213
+ self,
214
+ objective_name: str,
215
+ objective_type: ares_data_models.AresDataType,
216
+ objective_description: str = "",
217
+ optional: bool = False,
218
+ struct_schema: Optional[Dict[str, AresSchemaEntry]] = None,
219
+ list_element_schema: Optional[AresSchemaEntry] = None,
220
+ ) -> None:
221
+ """Adds an analysis objective to the advertised outputs of this analyzer.
222
+
223
+ If `objective_type` is LIST, `list_element_schema` can be used to describe
224
+ the schema of each element in that list.
225
+ """
226
+ self._service_wrapper._objective_outputs[objective_name] = ares_data_schema_utils.create_settings_schema_entry(
227
+ setting_type=objective_type,
228
+ optional=optional,
229
+ choices=[],
230
+ struct_schema=struct_schema,
231
+ list_element_schema=list_element_schema,
232
+ description=objective_description,
233
+ )
234
+
235
+ def get_objective_schema(self) -> List[ObjectiveSchema]:
236
+ """Return a fresh Python representation of the configured objective schema."""
237
+ schemas: List[ObjectiveSchema] = []
238
+ for name, proto_schema in self._service_wrapper._objective_outputs.items():
239
+ schemas.append(ObjectiveSchema(name, proto_schema))
240
+
241
+ return schemas
242
+
@@ -0,0 +1,248 @@
1
+ from typing import Dict, Any, List, Optional
2
+ import warnings
3
+ from ..Models import Outcome, RequestMetadata, AresDataType, AresSchemaEntry
4
+ from ares_datamodel import ares_data_schema_pb2
5
+ from ..Utils import ares_data_type_utils
6
+
7
+ class AnalysisRequest:
8
+ """ Represents an analysis request received from ARES. """
9
+
10
+ def __init__(self, inputs: Dict[str, Any], settings: Dict[str, Any], metadata: RequestMetadata):
11
+ self.inputs = inputs
12
+ self.settings = settings
13
+ self.request_metadata = metadata
14
+
15
+ def __str__(self) -> str:
16
+ """Returns a string representation of the AnalysisRequest with all information organized."""
17
+ metadata_str = str(self.request_metadata).replace('\n', '\n\t')
18
+ return (f"AnalysisRequest object with:\n"
19
+ f" inputs: {self.inputs}\n"
20
+ f" settings: {self.settings}\n"
21
+ f" metadata: {metadata_str}")
22
+
23
+ def __repr__(self) -> str:
24
+ return self.__str__()
25
+
26
+ class Objective:
27
+ """ Represents a single analysis objective in the new AnalysisResponse model. """
28
+
29
+ def __init__(self, objective_name: str, objective_value: Any, objective_metadata: Optional[Dict[str, Any]] = None):
30
+ """
31
+ Initializes a new Objective.
32
+
33
+ Args:
34
+ objective_name: A name the user wants associated with this objective.
35
+ objective_value: The value associated with this objective, as calculated by the analyzer.
36
+ objective_metadata: Optional metadata associated with this objective, represented as a dictionary.
37
+ """
38
+ self.objective_name = objective_name
39
+ self.objective_value = objective_value
40
+ self.objective_metadata = objective_metadata or {}
41
+
42
+ def __str__(self) -> str:
43
+ return (f"Objective object with:\n"
44
+ f" objective_name: {self.objective_name}\n"
45
+ f" objective_value: {self.objective_value}\n"
46
+ f" objective_metadata: {self.objective_metadata}")
47
+
48
+ def __repr__(self) -> str:
49
+ return self.__str__()
50
+
51
+
52
+ class ObjectiveSchema:
53
+ """A schema representing the expected form of an objective."""
54
+
55
+ def __init__(
56
+ self,
57
+ objective_name: str,
58
+ proto_objective_schema: ares_data_schema_pb2.AresValueSchema,
59
+ ):
60
+ self.objective_name = objective_name
61
+ # Convert basic scalar fields
62
+ self.objective_type: AresDataType = ares_data_type_utils.proto_ares_type_to_python_ares_type(proto_objective_schema.type)
63
+ self.objective_description: str = proto_objective_schema.description
64
+ self.optional: bool = proto_objective_schema.optional
65
+
66
+ # Convert nested struct schema, if present
67
+ self.struct_schema: Optional[Dict[str, AresSchemaEntry]] = None
68
+ if proto_objective_schema.struct_schema.fields:
69
+ self.struct_schema = {}
70
+ for field_name, field_schema in proto_objective_schema.struct_schema.fields.items():
71
+ self.struct_schema[field_name] = self._proto_value_schema_to_ares_schema_entry(field_schema)
72
+
73
+ # Convert list element schema, if present
74
+ self.list_element_schema: Optional[AresSchemaEntry] = None
75
+ # Treat type UNKNOWN (0) as "no list element schema configured"
76
+ if proto_objective_schema.list_element_schema.type != 0:
77
+ self.list_element_schema = self._proto_value_schema_to_ares_schema_entry(
78
+ proto_objective_schema.list_element_schema
79
+ )
80
+
81
+ @staticmethod
82
+ def _proto_value_schema_to_ares_schema_entry(
83
+ proto: ares_data_schema_pb2.AresValueSchema,
84
+ ) -> AresSchemaEntry:
85
+ """Convert a proto AresValueSchema into a Python AresSchemaEntry."""
86
+ py_type = ares_data_type_utils.proto_ares_type_to_python_ares_type(proto.type)
87
+
88
+ # Extract choices if present
89
+ choices: List[Any] = []
90
+ if proto.string_choices.strings:
91
+ choices = list(proto.string_choices.strings)
92
+ elif proto.number_choices.numbers:
93
+ choices = list(proto.number_choices.numbers)
94
+
95
+ entry = AresSchemaEntry(
96
+ type=py_type,
97
+ optional=proto.optional,
98
+ description=proto.description,
99
+ choices=choices,
100
+ struct_schema=None,
101
+ list_element_schema=None,
102
+ min_number_value=getattr(proto, "min_number_value", None),
103
+ max_number_value=getattr(proto, "max_number_value", None),
104
+ )
105
+
106
+ # Nested struct schema
107
+ if proto.struct_schema.fields:
108
+ entry.struct_schema = {}
109
+ for field_name, field_schema in proto.struct_schema.fields.items():
110
+ entry.struct_schema[field_name] = ObjectiveSchema._proto_value_schema_to_ares_schema_entry(
111
+ field_schema
112
+ )
113
+
114
+ # Nested list element schema
115
+ if proto.list_element_schema.type != 0:
116
+ entry.list_element_schema = ObjectiveSchema._proto_value_schema_to_ares_schema_entry(
117
+ proto.list_element_schema
118
+ )
119
+
120
+ return entry
121
+
122
+ def __str__(self) -> str:
123
+ struct_keys = list(self.struct_schema.keys()) if self.struct_schema else []
124
+ list_elem_type = (
125
+ self.list_element_schema.type.name
126
+ if self.list_element_schema and hasattr(self.list_element_schema.type, "name")
127
+ else None
128
+ )
129
+
130
+ return (
131
+ f"ObjectiveSchema(\n"
132
+ f" objective_name={self.objective_name!r},\n"
133
+ f" objective_type={self.objective_type.name if hasattr(self.objective_type, 'name') else self.objective_type},\n"
134
+ f" objective_description={self.objective_description!r},\n"
135
+ f" optional={self.optional},\n"
136
+ f" struct_schema_keys={struct_keys},\n"
137
+ f" list_element_type={list_elem_type}\n"
138
+ f")"
139
+ )
140
+
141
+ def __repr__(self) -> str:
142
+ return self.__str__()
143
+
144
+
145
+ class AnalysisResponse:
146
+ """ Represents the result of an analysis process using objectives.
147
+
148
+ Preferred usage:
149
+ - Construct with a list of Objective instances:
150
+ AnalysisResponse(
151
+ objectives=[
152
+ Objective("primary_metric", 0.87, {"units": "accuracy"})
153
+ ],
154
+ outcome=Outcome.SUCCESS,
155
+ error_string=""
156
+ )
157
+
158
+ Deprecated usage:
159
+ - Construct with a single scalar result (will be removed in a future major version):
160
+ AnalysisResponse(result=0.87)
161
+ """
162
+
163
+ def __init__(
164
+ self,
165
+ objectives: Optional[List[Objective]] = None,
166
+ outcome: Outcome = Outcome.SUCCESS,
167
+ error_string: str = "",
168
+ result: Optional[float] = None,
169
+ ):
170
+ """
171
+ Initializes an AnalysisResponse message.
172
+
173
+ Args:
174
+ objectives: A list of Objective instances representing the objectives returned by analysis.
175
+ outcome: An Outcome value that represents whether analysis was done successfully.
176
+ error_string: An optional string argument for passing why analysis failed to ARES.
177
+ result: DEPRECATED. A single numeric result that will be wrapped into a default Objective.
178
+ This parameter is deprecated and will be removed in a future major version.
179
+ """
180
+ self._deprecated_result_usage = False
181
+
182
+ if objectives is not None and result is not None:
183
+ raise ValueError("AnalysisResponse cannot be constructed with both 'objectives' and deprecated 'result'. "
184
+ "Use 'objectives' only.")
185
+
186
+ if objectives is None and result is not None:
187
+ # Deprecated path: single scalar result
188
+ warnings.warn(
189
+ "AnalysisResponse(result=...) is deprecated; use "
190
+ "AnalysisResponse(objectives=[Objective(...)]) instead. "
191
+ "Support will be removed in a future major version.",
192
+ DeprecationWarning
193
+ )
194
+ self._deprecated_result_usage = True
195
+
196
+ # Use a default objective name for backward compatibility
197
+ default_objective = Objective(
198
+ objective_name="result",
199
+ objective_value=result,
200
+ objective_metadata=None,
201
+ )
202
+ self.objectives: List[Objective] = [default_objective]
203
+ elif objectives is not None:
204
+ self.objectives = objectives
205
+ else:
206
+ # No objectives and no result provided: treat as empty objectives
207
+ self.objectives = []
208
+
209
+ self.outcome = outcome
210
+ self.error_string = error_string
211
+
212
+ @property
213
+ def deprecated_result_usage(self) -> bool:
214
+ """Indicates whether this AnalysisResponse was created via the deprecated 'result' parameter."""
215
+ return self._deprecated_result_usage
216
+
217
+ def __str__(self) -> str:
218
+ if not self.objectives:
219
+ objectives_str = "\t- (none)"
220
+
221
+ else:
222
+ objectives_str = "\n".join(
223
+ [f"\t- {obj.objective_name}: value={obj.objective_value}, metadata={obj.objective_metadata}"
224
+ for obj in self.objectives])
225
+
226
+ return (f"AnalysisResponse object with:\n"
227
+ f" objectives:\n{objectives_str}\n"
228
+ f" outcome: {self.outcome}\n"
229
+ f" error_string: {self.error_string}")
230
+
231
+ def __repr__(self) -> str:
232
+ return self.__str__()
233
+
234
+ class InfoResponse:
235
+ """ A response message that provides basic information about your analyzer. """
236
+
237
+ def __init__(self, name: str, version: str, description: str = ""):
238
+ """Initializes a new InfoResponse message.
239
+
240
+ Args:
241
+ name: The name of your analyzer, to be displayed in ARES.
242
+ version: The specific version of your analyzer. This information is saved in experiment results that use your analyzer.
243
+ description: An optional (but recommended) string that gives a basic description of your analyzer.
244
+ """
245
+
246
+ self.name = name
247
+ self.version = version
248
+ self.description = description
@@ -1,4 +1,4 @@
1
- from PyAres import AresAnalyzerService, AnalysisRequest, AnalysisResponse, AresDataType, Outcome, Limits
1
+ from PyAres import *
2
2
 
3
3
  def analyze_sample(request: AnalysisRequest) -> AnalysisResponse:
4
4
  # 1. Extract inputs
@@ -6,15 +6,17 @@ def analyze_sample(request: AnalysisRequest) -> AnalysisResponse:
6
6
  raw_value = request.inputs.get("Growth_Metric")
7
7
 
8
8
  if raw_value is None:
9
- return AnalysisResponse(result=0.0, outcome=Outcome.FAILURE)
9
+ return AnalysisResponse(objectives=[], outcome=Outcome.FAILURE, error_string="No raw value provided, cannot analyze")
10
10
 
11
11
  # 2. Perform Logic
12
12
  print(f"Analyzing sample with value: {raw_value}")
13
13
 
14
- calculated_score = raw_value * 1.5
14
+ calculated_score = raw_value * 1.5
15
+
16
+ objective_score = Objective("Calculated Score", calculated_score)
15
17
 
16
18
  # 3. Return Result
17
- return AnalysisResponse(result=calculated_score, outcome=Outcome.SUCCESS)
19
+ return AnalysisResponse(objectives=[objective_score], outcome=Outcome.SUCCESS)
18
20
 
19
21
  if __name__ == "__main__":
20
22
  service = AresAnalyzerService(
@@ -0,0 +1,80 @@
1
+ from PyAres import *
2
+ import math
3
+ import random
4
+
5
+ def analyze(request: AnalysisRequest) -> AnalysisResponse:
6
+ """
7
+ Evaluates the parameters chosen by your AX Planner and returns a simulated yield.
8
+ """
9
+
10
+ try:
11
+ # 1. Extract the values ARES OS logged for this specific iteration.
12
+ # Ensure the parameter names in your ARES Campaign match these strings exactly.
13
+ temperature = request.inputs["Temperature"]
14
+ concentration = request.inputs["Concentration"]
15
+
16
+ # 2. Define the "Hidden Ground Truth"
17
+ # This is the goal your AX Planner is trying to discover.
18
+ ideal_temp = 165.0
19
+ ideal_conc = 3.2
20
+ max_yield = 100.0
21
+
22
+ # 3. Calculate the distance penalty (the variances control the "width" of the peak)
23
+ temp_variance = 400.0
24
+ conc_variance = 2.0
25
+
26
+ distance_penalty = (((temperature - ideal_temp) ** 2) / temp_variance) + \
27
+ (((concentration - ideal_conc) ** 2) / conc_variance)
28
+
29
+ # 4. Calculate theoretical yield and add noise
30
+ simulated_yield = max_yield * math.exp(-distance_penalty)
31
+ noise = random.gauss(0, 1.5) # mean=0, std_dev=1.5
32
+
33
+ # Clamp the final yield between 0 and 100%
34
+ final_yield = max(0.0, min(100.0, simulated_yield + noise))
35
+
36
+ print(f"[Demo Analyzer] Received T={temperature:.1f}, C={concentration:.1f} | Calculated Yield: {final_yield:.2f}%")
37
+
38
+ # Preferred new usage: return an objective-based AnalysisResponse
39
+ return AnalysisResponse(
40
+ objectives=[
41
+ Objective(
42
+ objective_name="yield",
43
+ objective_value=final_yield,
44
+ objective_metadata={"units": "%"}
45
+ )
46
+ ]
47
+ )
48
+
49
+ except Exception as e:
50
+ print(f"[Demo Analyzer] Error during analysis: {e}")
51
+ # If extraction fails (e.g., missing parameter names), return a terrible score so the planner learns to avoid it
52
+ return AnalysisResponse(
53
+ objectives=[
54
+ Objective(
55
+ objective_name="yield",
56
+ objective_value=None,
57
+ objective_metadata={"error": str(e)}
58
+ )
59
+ ],
60
+ outcome=Outcome.FAILURE,
61
+ error_string=str(e),
62
+ )
63
+
64
+
65
+
66
+ if __name__ == "__main__":
67
+ # Initialize the Analyzer Service
68
+ demo_analyzer = AresAnalyzerService(custom_analysis_logic=analyze,
69
+ name="Simulated Yield Demo",
70
+ version="1.0.0",
71
+ description="Calculates a simulated material yield based on a hidden ideal Temperature (165) and Concentration (3.2).",
72
+ port=8200)
73
+
74
+ demo_analyzer.add_analysis_parameter("Temperature", AresDataType.NUMBER)
75
+ demo_analyzer.add_analysis_parameter("Concentration", AresDataType.NUMBER)
76
+
77
+ demo_analyzer.add_objective_output("yield", AresDataType.NUMBER, "A numeric value that indicates the experiments yielded result")
78
+
79
+ print("Starting PyAres Simulated Goal Analyzer...")
80
+ demo_analyzer.start()
@@ -8,12 +8,12 @@ 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]
11
+ for objective_set in request.analysis_objectives:
12
+ print(f"Received a total of {len(request.analysis_objectives)} objective sets")
13
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}")
14
+ for objective in objective_set:
15
+ print(f"Objective Name: {objective.objective_name}")
16
+ print(f"Objective Value: {objective.objective_value}")
17
17
 
18
18
 
19
19
  for param in request.parameters: